diff --git a/package-lock.json b/package-lock.json
index 1cbb2fb223..07a6432528 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2929,7 +2929,7 @@
},
"util": {
"version": "0.10.3",
- "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"dev": true,
"requires": {
@@ -6015,9 +6015,9 @@
}
},
"eslint-plugin-react-hooks": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.0.1.tgz",
- "integrity": "sha512-xir+3KHKo86AasxlCV8AHRtIZPHljqCRRUYgASkbatmt0fad4+5GgC7zkT7o/06hdKM6MIwp8giHVXqBPaarHQ==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.3.0.tgz",
+ "integrity": "sha512-gLKCa52G4ee7uXzdLiorca7JIQZPPXRAQDXV83J4bUEeUuc5pIEyZYAZ45Xnxe5IuupxEqHS+hUhSLIimK1EMw==",
"dev": true
},
"eslint-scope": {
@@ -6966,7 +6966,6 @@
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
- "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -7144,7 +7143,6 @@
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
- "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -7161,7 +7159,6 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
- "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -7178,7 +7175,6 @@
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
"dev": true,
- "optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -7199,7 +7195,6 @@
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
- "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -7303,7 +7298,6 @@
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -7442,7 +7436,6 @@
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
- "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -7464,7 +7457,6 @@
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
- "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -9292,7 +9284,6 @@
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
- "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -9470,7 +9461,6 @@
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
- "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -9487,7 +9477,6 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
- "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -9504,7 +9493,6 @@
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
"dev": true,
- "optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -9525,7 +9513,6 @@
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
- "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -9629,7 +9616,6 @@
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -9768,7 +9754,6 @@
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
- "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -9790,7 +9775,6 @@
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
- "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -11078,7 +11062,7 @@
},
"minimist": {
"version": "0.0.8",
- "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"minipass": {
@@ -15208,7 +15192,7 @@
},
"readable-stream": {
"version": "1.0.33",
- "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.33.tgz",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.33.tgz",
"integrity": "sha1-OjYN1mwbHX/UcFOJhg7aHQ9hEmw=",
"requires": {
"core-util-is": "~1.0.0",
diff --git a/package.json b/package.json
index 7123427d77..d9b795f1c3 100644
--- a/package.json
+++ b/package.json
@@ -67,7 +67,7 @@
"eslint-config-prettier": "^6.3.0",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-react": "^7.14.3",
- "eslint-plugin-react-hooks": "^2.0.1",
+ "eslint-plugin-react-hooks": "^2.3.0",
"html-webpack-plugin": "^3.2.0",
"jest": "^24.9.0",
"json-loader": "^0.5.7",
diff --git a/resources/Desktop.svg b/resources/Desktop.svg
new file mode 100644
index 0000000000..33462d45be
--- /dev/null
+++ b/resources/Desktop.svg
@@ -0,0 +1,44 @@
+
+
\ No newline at end of file
diff --git a/resources/Mobile.svg b/resources/Mobile.svg
new file mode 100644
index 0000000000..5aed769403
--- /dev/null
+++ b/resources/Mobile.svg
@@ -0,0 +1,79 @@
+
+
\ No newline at end of file
diff --git a/src/components/App.tsx b/src/components/App.tsx
index 3a61c1de81..5aedd5fd21 100644
--- a/src/components/App.tsx
+++ b/src/components/App.tsx
@@ -16,6 +16,7 @@ import '../lib/i18n'
import '../lib/analytics'
import CodeMirrorStyle from './CodeMirrorStyle'
import { useGeneralStatus } from '../lib/generalStatus'
+import Modal from './Modal'
const App = () => {
const { initialize, initialized } = useDb()
@@ -67,6 +68,7 @@ const App = () => {
+
diff --git a/src/components/Modal/contents/DownloadOurAppModal.tsx b/src/components/Modal/contents/DownloadOurAppModal.tsx
new file mode 100644
index 0000000000..cb6f105545
--- /dev/null
+++ b/src/components/Modal/contents/DownloadOurAppModal.tsx
@@ -0,0 +1,38 @@
+import React from 'react'
+import {
+ ModalContainer,
+ ModalHeader,
+ ModalSubtitle,
+ ModalBody,
+ ModalFlex
+} from './styled'
+
+const DownloadOurAppModal = () => {
+ return (
+
+ Download our apps
+
+ Use Boostnote on your local and focus on your work!
+
+
+
+
+

+
+
+
+

+
+
+ We are planning for a launch by the end of the year.
+
+
+
+
+
+ )
+}
+
+export default DownloadOurAppModal
diff --git a/src/components/Modal/contents/styled.ts b/src/components/Modal/contents/styled.ts
new file mode 100644
index 0000000000..207bd4ed13
--- /dev/null
+++ b/src/components/Modal/contents/styled.ts
@@ -0,0 +1,79 @@
+import styled from '../../../lib/styled'
+import { textColor } from '../../../lib/styled/styleFunctions'
+
+export const ModalContainer = styled.div`
+ width: 100%;
+ ${textColor}
+ padding: 3px;
+`
+
+export const ModalHeader = styled.h1`
+ text-align: center;
+ margin-top: 10px;
+ margin-bottom: 10px;
+`
+
+export const ModalSubtitle = styled.h4`
+ text-align: center;
+ margin-bottom: 8vh;
+`
+
+export const ModalBody = styled.div`
+ margin-top: 20px;
+ padding: 0 2%;
+
+ .button {
+ display: block;
+ background-color: rgb(3, 197, 136);
+ font-size: 16px;
+ line-height: 1;
+ text-transform: uppercase;
+ color: rgb(255, 255, 255);
+ padding: 16px 32px;
+ border-width: initial;
+ border-style: none;
+ border-color: initial;
+ border-image: initial;
+ border-radius: 4px;
+ margin: auto;
+ margin-bottom: 10px;
+
+ &:not(:disabled):hover {
+ cursor: pointer;
+ opacity: 0.8;
+ }
+
+ &.darker {
+ background-color: #d7d7d7;
+ color: #000;
+ }
+
+ .subtext {
+ font-size: 12px;
+ }
+ }
+`
+
+export const ModalFlex = styled.div`
+ width: 100%;
+ display: flex;
+ flex-wrap: no-wrap;
+ justify-content: space-between;
+ align-items: top;
+
+ div {
+ flex: 1 1 0;
+ }
+
+ img {
+ max-width: 100%;
+ width: auto;
+ height: 30vh;
+ display: block;
+ margin: auto;
+ }
+
+ .center {
+ text-align: center;
+ }
+`
diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx
new file mode 100644
index 0000000000..f75822a21e
--- /dev/null
+++ b/src/components/Modal/index.tsx
@@ -0,0 +1,90 @@
+import React, { useMemo, useCallback } from 'react'
+import { useGlobalKeyDownHandler } from '../../lib/keyboard'
+import { useModal } from '../../lib/modal/store'
+import {
+ StyledModalsBackground,
+ StyledModalsContainer,
+ StyledModalsSkipButton
+} from './styled'
+import Icon from '../atoms/Icon'
+import { mdiChevronRightCircleOutline } from '@mdi/js'
+import { usePreferences } from '../../lib/preferences'
+import DownloadOurAppModal from './contents/DownloadOurAppModal'
+
+interface ModalsRenderingOptions {
+ closable: boolean
+ body: JSX.Element
+ onSkip?: () => void
+}
+
+export default () => {
+ const { modalContent, closeModal } = useModal()
+ const { setPreferences } = usePreferences()
+
+ const content = useMemo((): ModalsRenderingOptions => {
+ const basicModal: ModalsRenderingOptions = {
+ closable: true,
+ body: <>>
+ }
+
+ switch (modalContent) {
+ case 'download-app':
+ basicModal.body =
+ basicModal.onSkip = () => {
+ setPreferences({
+ 'general.enableDownloadAppModal': false
+ })
+ closeModal()
+ }
+ break
+ default:
+ break
+ }
+
+ return basicModal
+ }, [modalContent, setPreferences, closeModal])
+
+ const closeHandler = useCallback(() => {
+ if (content.onSkip != null) {
+ return content.onSkip()
+ }
+ return closeModal()
+ }, [closeModal, content])
+
+ const keydownHandler = useMemo(() => {
+ return (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ closeHandler()
+ }
+ }
+ }, [closeHandler])
+ useGlobalKeyDownHandler(keydownHandler)
+
+ const backgroundClickHandler = useMemo(() => {
+ return (event: MouseEvent) => {
+ event.preventDefault()
+ closeHandler()
+ }
+ }, [closeHandler])
+
+ if (modalContent == null) return null
+
+ return (
+ <>
+
+
+ {content.body}
+
+ {content.closable && (
+
+
+ Skip
+
+
+ )}
+
+ >
+ )
+}
diff --git a/src/components/Modal/styled.ts b/src/components/Modal/styled.ts
new file mode 100644
index 0000000000..9bba07db77
--- /dev/null
+++ b/src/components/Modal/styled.ts
@@ -0,0 +1,68 @@
+import styled from '../../lib/styled'
+import {
+ border,
+ backgroundColor,
+ contextMenuShadow,
+ iconColor
+} from '../../lib/styled/styleFunctions'
+
+const zIndexModalsBackground = 8001
+
+export const StyledModalsBackground = styled.div`
+ z-index: ${zIndexModalsBackground};
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ ${backgroundColor};
+ opacity: 0.8;
+ display: flex;
+ overflow: hidden;
+`
+
+export const StyledModalsContainer = styled.div`
+ z-index: ${zIndexModalsBackground + 1};
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ margin: auto;
+ width: 50vw;
+ height: 70vh;
+ ${border}
+ ${backgroundColor}
+ ${contextMenuShadow}
+ border-radius: 4px;
+ display: flex;
+ overflow: hidden;
+`
+
+export const StyledModalsHeader = styled.h1`
+ margin: 0;
+ padding: 1em 0;
+`
+
+export const StyledModalsSkipButton = styled.button`
+ position: absolute;
+ bottom: 0;
+ right: 2%;
+ width: auto;
+ height: 40px;
+ background-color: transparent;
+ border: none;
+ font-size: 16px;
+ white-space: nowrap;
+ cursor: pointer;
+ ${iconColor}
+
+ span {
+ line-height: 20px;
+ vertical-align: middle;
+ }
+
+ .icon {
+ vertical-align: middle;
+ }
+`
diff --git a/src/components/Router.tsx b/src/components/Router.tsx
index af8bb88cfd..374f622076 100644
--- a/src/components/Router.tsx
+++ b/src/components/Router.tsx
@@ -11,6 +11,7 @@ import useRedirectHandler from '../lib/router/redirect'
export default () => {
const routeParams = useRouteParams()
const db = useDb()
+
useRedirectHandler()
switch (routeParams.name) {
diff --git a/src/components/SideNavigator/SideNavigator.tsx b/src/components/SideNavigator/SideNavigator.tsx
index fbc1d575ca..87a4fb0248 100644
--- a/src/components/SideNavigator/SideNavigator.tsx
+++ b/src/components/SideNavigator/SideNavigator.tsx
@@ -19,7 +19,8 @@ import { usePreferences } from '../../lib/preferences'
import {
backgroundColor,
iconColor,
- textColor
+ textColor,
+ uiTextColor
} from '../../lib/styled/styleFunctions'
import SideNavigatorItem from './SideNavigatorItem'
import { useGeneralStatus } from '../../lib/generalStatus'
@@ -67,8 +68,12 @@ const StyledSideNavContainer = styled.nav`
display: flex;
flex-direction: column;
}
+
.empty {
padding: 4px;
+ padding-left: 26px;
+ margin-bottom: 4px;
+ ${uiTextColor}
user-select: none;
}
diff --git a/src/components/Tutorials/TutorialsNavigator.tsx b/src/components/Tutorials/TutorialsNavigator.tsx
index cf7143903b..0f4adadce4 100644
--- a/src/components/Tutorials/TutorialsNavigator.tsx
+++ b/src/components/Tutorials/TutorialsNavigator.tsx
@@ -1,4 +1,4 @@
-import React, { useMemo } from 'react'
+import React, { useMemo, useCallback } from 'react'
import { tutorialsTree, TutorialsNavigatorTreeItem } from '../../lib/tutorials'
import SideNavigatorItem from '../SideNavigator/SideNavigatorItem'
import {
@@ -6,12 +6,11 @@ import {
mdiFolderOutline,
mdiHelpCircleOutline
} from '@mdi/js'
-import {
- useRouteParams,
- useRouter,
- currentTutorialPathname
-} from '../../lib/router'
+import { useRouter, useCurrentTutorialPathname } from '../../lib/router'
import { useGeneralStatus } from '../../lib/generalStatus'
+import { useContextMenu, MenuTypes } from '../../lib/contextMenu'
+import { useDialog, DialogIconTypes } from '../../lib/dialog'
+import { usePreferences } from '../../lib/preferences'
interface NavigatorNode {
id: string
@@ -23,75 +22,106 @@ interface NavigatorNode {
children: NavigatorNode[]
active?: boolean
}
-interface TutorialsNavigatorProps {}
-const TutorialsNavigator = ({ }: TutorialsNavigatorProps) => {
- const routeParams = useRouteParams()
+const TutorialsNavigator = ({}) => {
const { push } = useRouter()
- const currentHref = currentTutorialPathname()
+ const currentHref = useCurrentTutorialPathname()
+ const { popup } = useContextMenu()
+ const { messageBox } = useDialog()
const { toggleSideNavOpenedItem, sideNavOpenedItemSet } = useGeneralStatus()
+ const { setPreferences } = usePreferences()
const tutorials = tutorialsTree
- function getNavigatorNodesFromTreeItem(
- tree: TutorialsNavigatorTreeItem,
- currentDepth: number,
- parentNode?: NavigatorNode,
- parentComponentPathname?: string
- ): NavigatorNode | undefined {
- if (tree.type === 'note') {
- return
- }
+ const getNavigatorNodesFromTreeItem = useCallback(
+ (
+ tree: TutorialsNavigatorTreeItem,
+ currentDepth: number,
+ parentNode?: NavigatorNode,
+ parentComponentPathname?: string
+ ): NavigatorNode | undefined => {
+ if (tree.type === 'note') {
+ return
+ }
- const componentPathname = `${parentComponentPathname != null &&
- parentComponentPathname}/${tree.absolutePath}`
- const nodeHref = `${parentNode != null ? parentNode.href : '/app'}/${
- tree.slug
- }`
+ const componentPathname = `${parentComponentPathname != null &&
+ parentComponentPathname}/${tree.absolutePath}`
+ const nodeHref = `${parentNode != null ? parentNode.href : '/app'}/${
+ tree.slug
+ }`
- const folderIsActive = currentHref.split('/notes/note:')[0] === nodeHref
+ const folderIsActive = currentHref.split('/notes/note:')[0] === nodeHref
- const notesUnderCurrentNode = tree.children.filter(
- child => child.type === 'note'
- )
+ const notesUnderCurrentNode = tree.children.filter(
+ child => child.type === 'note'
+ )
- const nodeId = `TF-${nodeHref.split('/app')[1]}`
- const currentNode = {
- id: nodeId,
- name: `${tree.label} ${
- notesUnderCurrentNode.length > 0
- ? `(${notesUnderCurrentNode.length})`
- : ''
- }`,
- iconPath:
- tree.type === 'folder'
- ? folderIsActive
- ? mdiFolderOpenOutline
- : mdiFolderOutline
- : mdiHelpCircleOutline,
- href: nodeHref,
- active: folderIsActive,
- depth: currentDepth,
- opened: sideNavOpenedItemSet.has(nodeId),
- children: []
- }
+ const nodeId = `TF-${nodeHref.split('/app')[1]}`
+ const currentNode = {
+ id: nodeId,
+ name: `${tree.label} ${
+ notesUnderCurrentNode.length > 0
+ ? `(${notesUnderCurrentNode.length})`
+ : ''
+ }`,
+ iconPath:
+ tree.type === 'folder'
+ ? folderIsActive
+ ? mdiFolderOpenOutline
+ : mdiFolderOutline
+ : mdiHelpCircleOutline,
+ href: nodeHref,
+ active: folderIsActive,
+ depth: currentDepth,
+ opened: sideNavOpenedItemSet.has(nodeId),
+ children: []
+ }
- const childrenNodes =
- tree.children.length === 0
- ? []
- : (tree.children
- .map(childrenTree =>
- getNavigatorNodesFromTreeItem(
- childrenTree,
- currentDepth + 1,
- currentNode,
- componentPathname
+ const childrenNodes =
+ tree.children.length === 0
+ ? []
+ : (tree.children
+ .map(childrenTree =>
+ getNavigatorNodesFromTreeItem(
+ childrenTree,
+ currentDepth + 1,
+ currentNode,
+ componentPathname
+ )
)
- )
- .filter(node => node != null) as NavigatorNode[])
- return {
- ...currentNode,
- children: childrenNodes
+ .filter(node => node != null) as NavigatorNode[])
+ return {
+ ...currentNode,
+ children: childrenNodes
+ }
+ },
+ [currentHref, sideNavOpenedItemSet]
+ )
+
+ const createOnContextMenuHandler = (depth: number) => {
+ return (event: React.MouseEvent) => {
+ event.preventDefault()
+ popup(event, [
+ {
+ type: MenuTypes.Normal,
+ label: 'Remove folder',
+ enabled: depth === 0,
+ onClick: () => {
+ messageBox({
+ title: `Hide tutorials`,
+ message:
+ 'You can choose to display them again in your preferences.',
+ iconType: DialogIconTypes.Warning,
+ buttons: ['Hide', 'Cancel'],
+ defaultButtonIndex: 0,
+ cancelButtonIndex: 1,
+ onClose: () => {
+ setPreferences({ 'general.tutorials': 'hide' })
+ }
+ })
+ }
+ }
+ ])
}
}
@@ -99,7 +129,7 @@ const TutorialsNavigator = ({ }: TutorialsNavigatorProps) => {
return tutorials
.map(tutorial => getNavigatorNodesFromTreeItem(tutorial, 0))
.filter(node => node != null) as NavigatorNode[]
- }, [routeParams, tutorials, toggleSideNavOpenedItem])
+ }, [tutorials, getNavigatorNodesFromTreeItem])
const redirectToTutorialNode = (node: NavigatorNode) => {
push(node.href)
@@ -122,6 +152,7 @@ const TutorialsNavigator = ({ }: TutorialsNavigatorProps) => {
onClick={() => redirectToTutorialNode(node)}
onFoldButtonClick={() => toggleSideNavOpenedItem(node.id)}
folded={node.children.length === 0 ? undefined : !node.opened}
+ onContextMenu={createOnContextMenuHandler(node.depth)}
/>
{node.children.map(child => renderNode(child, node.opened))}
diff --git a/src/components/Tutorials/TutorialsPage.tsx b/src/components/Tutorials/TutorialsPage.tsx
index 067283ea83..4543c6fe16 100644
--- a/src/components/Tutorials/TutorialsPage.tsx
+++ b/src/components/Tutorials/TutorialsPage.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useCallback, useState } from 'react'
+import React, { useCallback, useMemo } from 'react'
import { tutorialsTree, TutorialsNavigatorTreeItem } from '../../lib/tutorials'
import TwoPaneLayout from '../atoms/TwoPaneLayout'
import { useGeneralStatus } from '../../lib/generalStatus'
@@ -16,20 +16,89 @@ type TutoriasPagePicker = {
}
export default ({ pathname }: TutorialsPageProps) => {
- const [
- currentTutorialBranch,
- setCurrentTutorialBranch
- ] = useState(getCurrentNodeFromTutorialTrees)
+ const { generalStatus, setGeneralStatus } = useGeneralStatus()
+ const router = useRouter()
+
+ const searchThroughTreeForIdenticalNode = useCallback(
+ (
+ pathToSearch: string,
+ parentDepthPath: string,
+ parentAbsolutePath: string,
+ tree: TutorialsNavigatorTreeItem,
+ parentTree?: TutorialsNavigatorTreeItem
+ ): TutoriasPagePicker | null => {
+ let match = null
+ const currentDepthPath = `${parentDepthPath}/${
+ tree.type === 'note' ? 'notes/note:' : ''
+ }${tree.slug}`
+
+ const currentAbsolutePath = parentAbsolutePath + '/' + tree.absolutePath
+ if (currentDepthPath === pathToSearch) {
+ const currentTreeWithDepthAbsolutePath = {
+ ...tree,
+ absolutePath: currentAbsolutePath,
+ children: Object.entries(tree.children).map(obj => {
+ return {
+ ...obj[1],
+ absolutePath: currentAbsolutePath + '/' + obj[1].absolutePath
+ }
+ }) as TutorialsNavigatorTreeItem[]
+ }
+
+ const parentTreeWithDepthAbsolutePath =
+ parentTree != null
+ ? {
+ ...parentTree,
+ absolutePath: parentAbsolutePath,
+ children: Object.entries(parentTree.children).map(obj => {
+ return {
+ ...obj[1],
+ absolutePath: parentAbsolutePath + '/' + obj[1].absolutePath
+ }
+ })
+ }
+ : undefined
+
+ return {
+ currentTree: currentTreeWithDepthAbsolutePath,
+ parentTree: parentTreeWithDepthAbsolutePath
+ }
+ }
+
+ for (let i = 0; i < tree.children.length; i++) {
+ match = searchThroughTreeForIdenticalNode(
+ pathToSearch,
+ currentDepthPath,
+ currentAbsolutePath,
+ tree.children[i],
+ tree
+ )
+ if (match != null) {
+ break
+ }
+ }
- const [currentFolderPathname, setCurrentFolderPathname] = useState(
- getCurrentFolderPathname
+ return match
+ },
+ []
)
- const [selectedNote, setSelectedNote] = useState<
- TutorialsNavigatorTreeItem | undefined
- >()
- const { generalStatus, setGeneralStatus } = useGeneralStatus()
- const router = useRouter()
+ const currentTutorialBranch = useMemo(() => {
+ let match = null
+ for (let i = 0; i < tutorialsTree.length; i++) {
+ match = searchThroughTreeForIdenticalNode(
+ pathname,
+ '/app',
+ '',
+ tutorialsTree[i]
+ )
+ if (match != null) {
+ break
+ }
+ }
+ return match
+ }, [pathname, searchThroughTreeForIdenticalNode])
+
const updateNoteListWidth = useCallback(
(leftWidth: number) => {
setGeneralStatus({
@@ -51,16 +120,7 @@ export default ({ pathname }: TutorialsPageProps) => {
}))
}, [setGeneralStatus])
- useEffect(() => {
- setCurrentTutorialBranch(getCurrentNodeFromTutorialTrees)
- setCurrentFolderPathname(getCurrentFolderPathname)
- }, [pathname, tutorialsTree])
-
- useEffect(() => {
- setSelectedNote(getCurrentNote)
- }, [currentTutorialBranch])
-
- function getCurrentNote(): TutorialsNavigatorTreeItem | undefined {
+ const selectedNote = useMemo((): TutorialsNavigatorTreeItem | undefined => {
if (currentTutorialBranch == null) {
return undefined
}
@@ -76,88 +136,11 @@ export default ({ pathname }: TutorialsPageProps) => {
return notesChildren[0]
}
return undefined
- }
+ }, [currentTutorialBranch])
- function getCurrentFolderPathname() {
+ const currentFolderPathname = useMemo(() => {
return pathname.split('/notes')[0]
- }
-
- function getCurrentNodeFromTutorialTrees() {
- let match = null
- for (let i = 0; i < tutorialsTree.length; i++) {
- match = searchThroughTreeForIdenticalNode(
- pathname,
- '/app',
- '',
- tutorialsTree[i]
- )
- if (match != null) {
- break
- }
- }
- return match
- }
-
- function searchThroughTreeForIdenticalNode(
- pathToSearch: string,
- parentDepthPath: string,
- parentAbsolutePath: string,
- tree: TutorialsNavigatorTreeItem,
- parentTree?: TutorialsNavigatorTreeItem
- ): TutoriasPagePicker | null {
- let match = null
- const currentDepthPath = `${parentDepthPath}/${
- tree.type === 'note' ? 'notes/note:' : ''
- }${tree.slug}`
-
- const currentAbsolutePath = parentAbsolutePath + '/' + tree.absolutePath
- if (currentDepthPath === pathToSearch) {
- const currentTreeWithDepthAbsolutePath = {
- ...tree,
- absolutePath: currentAbsolutePath,
- children: Object.entries(tree.children).map(obj => {
- return {
- ...obj[1],
- absolutePath: currentAbsolutePath + '/' + obj[1].absolutePath
- }
- }) as TutorialsNavigatorTreeItem[]
- }
-
- const parentTreeWithDepthAbsolutePath =
- parentTree != null
- ? {
- ...parentTree,
- absolutePath: parentAbsolutePath,
- children: Object.entries(parentTree.children).map(obj => {
- return {
- ...obj[1],
- absolutePath: parentAbsolutePath + '/' + obj[1].absolutePath
- }
- })
- }
- : undefined
-
- return {
- currentTree: currentTreeWithDepthAbsolutePath,
- parentTree: parentTreeWithDepthAbsolutePath
- }
- }
-
- for (let i = 0; i < tree.children.length; i++) {
- match = searchThroughTreeForIdenticalNode(
- pathToSearch,
- currentDepthPath,
- currentAbsolutePath,
- tree.children[i],
- tree
- )
- if (match != null) {
- break
- }
- }
-
- return match
- }
+ }, [pathname])
const navigateUp = useCallback(() => {
if (currentTutorialBranch == null) {
@@ -199,7 +182,7 @@ export default ({ pathname }: TutorialsPageProps) => {
)
}
return
- }, [selectedNote])
+ }, [selectedNote, currentTutorialBranch, router, currentFolderPathname])
const navigateDown = useCallback(() => {
if (currentTutorialBranch == null) {
@@ -241,7 +224,7 @@ export default ({ pathname }: TutorialsPageProps) => {
)
}
return
- }, [selectedNote])
+ }, [selectedNote, currentFolderPathname, currentTutorialBranch, router])
return (
(
+ null
+ )
+
+ const openModal = useCallback((content: ModalsContentOptions) => {
+ setModalContent(content)
+ }, [])
+
+ const closeModal = useCallback(() => {
+ setModalContent(null)
+ }, [])
+
+ return {
+ modalContent,
+ closeModal,
+ openModal
+ }
+}
+
+export const {
+ StoreProvider: ModalProvider,
+ useStore: useModal
+} = createStoreContext(useModalStore, 'modal')
diff --git a/src/lib/modal/types.ts b/src/lib/modal/types.ts
new file mode 100644
index 0000000000..2f74b279ca
--- /dev/null
+++ b/src/lib/modal/types.ts
@@ -0,0 +1,14 @@
+export type ModalsContentOptions = 'download-app'
+
+export interface ModalsRenderingOptions {
+ closable: boolean
+ skippable: boolean
+ onClose?(): void
+ body: JSX.Element
+}
+
+export interface ModalsContext {
+ modalContent: ModalsContentOptions | null
+ openModal: (options: ModalsContentOptions) => void
+ closeModal: () => void
+}
diff --git a/src/lib/preferences/store.ts b/src/lib/preferences/store.ts
index 463a8de626..99295f36d4 100644
--- a/src/lib/preferences/store.ts
+++ b/src/lib/preferences/store.ts
@@ -30,6 +30,7 @@ const basePreferences: Preferences = {
'general.theme': 'dark',
'general.noteSorting': 'date-updated',
'general.enableAnalytics': true,
+ 'general.enableDownloadAppModal': true,
'general.tutorials': 'display',
// Editor
diff --git a/src/lib/preferences/types.ts b/src/lib/preferences/types.ts
index ab6a660eb1..156b2db66c 100644
--- a/src/lib/preferences/types.ts
+++ b/src/lib/preferences/types.ts
@@ -19,6 +19,7 @@ export interface Preferences {
'general.theme': GeneralThemeOptions
'general.noteSorting': GeneralNoteSortingOptions
'general.enableAnalytics': boolean
+ 'general.enableDownloadAppModal': boolean
'general.tutorials': GeneralTutorialsOptions
// Editor
diff --git a/src/lib/router/redirect.ts b/src/lib/router/redirect.ts
index 51e0828ff9..d6d69c32a9 100644
--- a/src/lib/router/redirect.ts
+++ b/src/lib/router/redirect.ts
@@ -1,24 +1,29 @@
import { useDb } from '../db'
import { useRouter } from './store'
-import { useEffect } from 'react'
+import { useEffect, useRef } from 'react'
+import { entries } from '../db/utils'
+import { useModal } from '../modal'
import { usePreferences } from '../preferences'
export default function useRedirectHandler() {
const db = useDb()
+ const { replace, pathname } = useRouter()
+ const { openModal } = useModal()
const { preferences } = usePreferences()
- const { push, pathname } = useRouter()
+ const storageMapRef = useRef(db.storageMap)
useEffect(() => {
- if (!db.initialized || db.storageMap == null) {
- return
- }
- if (
- pathname === '/app' &&
- preferences['general.tutorials'] === 'display' &&
- Object.keys(db.storageMap).length === 0
- ) {
- push('/app/tutorials/welcome-pack/guides/notes/note:storage-guide')
+ const storageEntries = entries(storageMapRef.current)
+
+ if (pathname !== '/app') return
+
+ if (storageEntries.length === 0) {
+ if (preferences['general.enableDownloadAppModal']) {
+ openModal('download-app')
+ }
+ replace('/app/storages')
+ } else {
+ replace(`/app/storages/${storageEntries[0][1].id}`)
}
- return
- }, [pathname, db.initialized, preferences['general.displayTutorials']])
+ }, [pathname, replace])
}
diff --git a/src/lib/router/utils.ts b/src/lib/router/utils.ts
index 1c785555aa..d7b51375d6 100644
--- a/src/lib/router/utils.ts
+++ b/src/lib/router/utils.ts
@@ -215,7 +215,7 @@ export const usePathnameWithoutNoteId = () => {
}, [routeParams, pathname])
}
-export const currentTutorialPathname = () => {
+export const useCurrentTutorialPathname = () => {
const routeParams = useRouteParams()
return useMemo(() => {
switch (routeParams.name) {
diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Guides/AboutOurCommunity.md b/src/lib/tutorials/files/tutorials/AboutOurCommunity.md
similarity index 100%
rename from src/lib/tutorials/files/tutorials/WelcomePack/Guides/AboutOurCommunity.md
rename to src/lib/tutorials/files/tutorials/AboutOurCommunity.md
diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Templates/Brainstorm.md b/src/lib/tutorials/files/tutorials/Brainstorm.md
similarity index 100%
rename from src/lib/tutorials/files/tutorials/WelcomePack/Templates/Brainstorm.md
rename to src/lib/tutorials/files/tutorials/Brainstorm.md
diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Templates/BugfixReport.md b/src/lib/tutorials/files/tutorials/BugfixReport.md
similarity index 100%
rename from src/lib/tutorials/files/tutorials/WelcomePack/Templates/BugfixReport.md
rename to src/lib/tutorials/files/tutorials/BugfixReport.md
diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Playground/GetStarted.md b/src/lib/tutorials/files/tutorials/GetStarted.md
similarity index 100%
rename from src/lib/tutorials/files/tutorials/WelcomePack/Playground/GetStarted.md
rename to src/lib/tutorials/files/tutorials/GetStarted.md
diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Guides/KeyboardShortcuts.md b/src/lib/tutorials/files/tutorials/KeyboardShortcuts.md
similarity index 100%
rename from src/lib/tutorials/files/tutorials/WelcomePack/Guides/KeyboardShortcuts.md
rename to src/lib/tutorials/files/tutorials/KeyboardShortcuts.md
diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Templates/MeetingNotes.md b/src/lib/tutorials/files/tutorials/MeetingNotes.md
similarity index 100%
rename from src/lib/tutorials/files/tutorials/WelcomePack/Templates/MeetingNotes.md
rename to src/lib/tutorials/files/tutorials/MeetingNotes.md
diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Guides/StorageGuide.md b/src/lib/tutorials/files/tutorials/StorageGuide.md
similarity index 100%
rename from src/lib/tutorials/files/tutorials/WelcomePack/Guides/StorageGuide.md
rename to src/lib/tutorials/files/tutorials/StorageGuide.md
diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Playground/TodaysTask.md b/src/lib/tutorials/files/tutorials/TodaysTask.md
similarity index 100%
rename from src/lib/tutorials/files/tutorials/WelcomePack/Playground/TodaysTask.md
rename to src/lib/tutorials/files/tutorials/TodaysTask.md
diff --git a/src/lib/tutorials/files/tutorials/WelcomePack/Templates/WeeklyPlanner.md b/src/lib/tutorials/files/tutorials/WeeklyPlanner.md
similarity index 100%
rename from src/lib/tutorials/files/tutorials/WelcomePack/Templates/WeeklyPlanner.md
rename to src/lib/tutorials/files/tutorials/WeeklyPlanner.md
diff --git a/src/lib/tutorials/tree.ts b/src/lib/tutorials/tree.ts
index 03d64c94ae..9ee9dcac74 100644
--- a/src/lib/tutorials/tree.ts
+++ b/src/lib/tutorials/tree.ts
@@ -14,99 +14,67 @@ export const tutorialsTree: TutorialsNavigatorTreeItem[] = [
type: 'storage',
children: [
{
- label: 'Welcome Pack',
- absolutePath: 'WelcomePack',
- slug: 'welcome-pack',
- type: 'folder',
- children: [
- {
- label: 'Playground',
- absolutePath: 'Playground',
- slug: 'playground',
- type: 'folder',
- children: [
- {
- label: 'Get Started!',
- absolutePath: 'GetStarted.md',
- slug: 'get-started',
- type: 'note',
- children: []
- },
- {
- label: "Today's Task",
- absolutePath: 'TodaysTask.md',
- slug: 'daily-task',
- type: 'note',
- children: []
- }
- ]
- },
- {
- label: 'Guides',
- absolutePath: 'Guides',
- slug: 'guides',
- type: 'folder',
- children: [
- {
- label: 'About our community',
- absolutePath: 'AboutOurCommunity.md',
- slug: 'community',
- type: 'note',
- children: []
- },
- {
- label: 'Keyboard shortcuts',
- absolutePath: 'KeyboardShortcuts.md',
- slug: 'keyboard-shortcuts',
- type: 'note',
- children: []
- },
- {
- label: 'Storage guide',
- absolutePath: 'StorageGuide.md',
- slug: 'storage-guide',
- type: 'note',
- children: []
- }
- ]
- },
- {
- label: 'Templates',
- absolutePath: 'Templates',
- slug: 'templates',
- type: 'folder',
- children: [
- {
- label: 'Brainstorm',
- absolutePath: 'Brainstorm.md',
- slug: 'brainstorm',
- type: 'note',
- children: []
- },
- {
- label: 'Bugfix Report',
- absolutePath: 'BugfixReport.md',
- slug: 'bugfix-report',
- type: 'note',
- children: []
- },
- {
- label: 'Meeting Notes',
- absolutePath: 'MeetingNotes.md',
- slug: 'meeting-notes',
- type: 'note',
- children: []
- },
- {
- label: 'Weekly Planner',
- absolutePath: 'WeeklyPlanner.md',
- slug: 'weekly-planner',
- type: 'note',
- children: []
- }
- ]
- }
- ]
+ label: 'Get Started!',
+ absolutePath: 'GetStarted.md',
+ slug: 'get-started',
+ type: 'note',
+ children: []
+ },
+ {
+ label: "Today's Task",
+ absolutePath: 'TodaysTask.md',
+ slug: 'daily-task',
+ type: 'note',
+ children: []
+ },
+ {
+ label: 'About our community',
+ absolutePath: 'AboutOurCommunity.md',
+ slug: 'community',
+ type: 'note',
+ children: []
+ },
+ {
+ label: 'Keyboard shortcuts',
+ absolutePath: 'KeyboardShortcuts.md',
+ slug: 'keyboard-shortcuts',
+ type: 'note',
+ children: []
+ },
+ {
+ label: 'Storage guide',
+ absolutePath: 'StorageGuide.md',
+ slug: 'storage-guide',
+ type: 'note',
+ children: []
+ },
+ {
+ label: 'Template - Brainstorm',
+ absolutePath: 'Brainstorm.md',
+ slug: 'brainstorm',
+ type: 'note',
+ children: []
+ },
+ {
+ label: 'Template - Bugfix Report',
+ absolutePath: 'BugfixReport.md',
+ slug: 'bugfix-report',
+ type: 'note',
+ children: []
+ },
+ {
+ label: 'Template - Meeting Notes',
+ absolutePath: 'MeetingNotes.md',
+ slug: 'meeting-notes',
+ type: 'note',
+ children: []
+ },
+ {
+ label: 'Template - Weekly Planner',
+ absolutePath: 'WeeklyPlanner.md',
+ slug: 'weekly-planner',
+ type: 'note',
+ children: []
}
]
}