diff --git a/apps/web/src/app.tsx b/apps/web/src/app.tsx index b1a88c3827..c6c99af054 100644 --- a/apps/web/src/app.tsx +++ b/apps/web/src/app.tsx @@ -91,6 +91,8 @@ type DesktopAppContentsProps = { function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) { const isFocusMode = useStore((store) => store.isFocusMode); const isTablet = useTablet(); + const isSideMenuOpen = useStore((store) => store.isSideMenuOpen); + const toggleSideMenu = useStore((store) => store.toggleSideMenu); const [isNarrow, setIsNarrow] = useState(isTablet || false); const navPane = useRef(null); const middlePane = useRef(null); @@ -124,14 +126,36 @@ function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) { }} > - {!isFocusMode && isTablet ? ( - - { - setShow(state || !show); + {isTablet ? ( + + toggleSideMenu(false)} /> + + {}} + isTablet={false} + /> + ) : ( !isFocusMode && ( @@ -140,18 +164,17 @@ function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) { ref={navPane} order={1} className="nav-pane" - defaultSize={10} - minSize={3.5} - // maxSize={isNarrow ? 5 : undefined} - onResize={(size) => setIsNarrow(size <= 5)} + defaultSize={20} + minSize={18} + maxSize={22} collapsible - collapsedSize={3.5} + collapsedSize={0} > { setShow(state || !show); }} - isTablet={isNarrow} + isTablet={false} /> diff --git a/apps/web/src/components/navigation-menu/index.tsx b/apps/web/src/components/navigation-menu/index.tsx index 8265eff0a4..ce7a41827c 100644 --- a/apps/web/src/components/navigation-menu/index.tsx +++ b/apps/web/src/components/navigation-menu/index.tsx @@ -17,8 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { useCallback, useEffect, useRef, useState } from "react"; -import { Box, Button, Flex } from "@theme-ui/components"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Box, Button, Flex, Image, Text } from "@theme-ui/components"; import { Note, Notebook as NotebookIcon, @@ -36,7 +36,11 @@ import { Circle, Icon, Reminders, - User + User, + Home, + Pro, + Documentation, + Logout } from "../icons"; import NavigationItem, { SortableNavigationItem } from "./navigation-item"; import { hardNavigate, hashNavigate, navigate } from "../../navigation"; @@ -72,6 +76,15 @@ import { handleDrop } from "../../common/drop-handler"; import { Menu } from "../../hooks/use-menu"; import { RenameColorDialog } from "../../dialogs/item-dialog"; import { strings } from "@notesnook/intl"; +import Tags from "../../views/tags"; +import NotebookTree from "./notebook-tree"; +import { UserProfile } from "../../dialogs/settings/components/user-profile"; +import { SUBSCRIPTION_STATUS } from "../../common/constants"; +import { ConfirmDialog, showLogoutConfirmation } from "../../dialogs/confirm"; +import { CREATE_BUTTON_MAP, createBackup } from "../../common"; +import { TaskManager } from "../../common/task-manager"; +import { showToast } from "../../utils/toast"; +import { useStore } from "../../stores/note-store"; type Route = { id: string; @@ -79,28 +92,21 @@ type Route = { path: string; icon: Icon; tag?: string; + count?: number; }; -const navigationHistory = new Map(); function shouldSelectNavItem(route: string, pin: Notebook | Tag) { return route.endsWith(pin.id); } -const routes: Route[] = [ +const routesInit: Route[] = [ { id: "notes", title: strings.routes.Notes(), path: "/notes", icon: Note }, - { - id: "notebooks", - title: strings.routes.Notebooks(), - path: "/notebooks", - icon: NotebookIcon - }, { id: "favorites", title: strings.routes.Favorites(), path: "/favorites", icon: StarOutline }, - { id: "tags", title: strings.routes.Tags(), path: "/tags", icon: TagIcon }, { id: "reminders", title: strings.routes.Reminders(), @@ -116,12 +122,26 @@ const routes: Route[] = [ { id: "trash", title: strings.routes.Trash(), path: "/trash", icon: Trash } ]; -const settings: Route = { +const tabs = [ + { + id: "home", + icon: Home, + title: strings.routes.Home() + }, + { + id: "notebook", + icon: NotebookIcon, + title: strings.routes.Notebooks() + }, + { id: "tag", icon: TagIcon, title: strings.routes.Tags() } +] as const; + +const settings = { id: "settings", title: strings.routes.Settings(), path: "/settings", icon: Settings -}; +} as const; type NavigationMenuProps = { toggleNavigationContainer: (toggleState?: boolean) => void; @@ -130,19 +150,13 @@ type NavigationMenuProps = { function NavigationMenu(props: NavigationMenuProps) { const { toggleNavigationContainer, isTablet } = props; - const [location, previousLocation, state] = useLocation(); + const [routes, setRoutes] = useState(routesInit); + const [location] = useLocation(); const isFocusMode = useAppStore((store) => store.isFocusMode); const colors = useAppStore((store) => store.colors); const shortcuts = useAppStore((store) => store.shortcuts); const refreshNavItems = useAppStore((store) => store.refreshNavItems); - const isLoggedIn = useUserStore((store) => store.isLoggedIn); - const profile = useSettingStore((store) => store.profile); const isMobile = useMobile(); - const theme = useThemeStore((store) => store.colorScheme); - const toggleNightMode = useThemeStore((store) => store.toggleColorScheme); - const setFollowSystemTheme = useThemeStore( - (store) => store.setFollowSystemTheme - ); const [hiddenRoutes, setHiddenRoutes] = usePersistentState( "sidebarHiddenItems:routes", db.settings.getSideBarHiddenItems("routes") @@ -151,26 +165,52 @@ function NavigationMenu(props: NavigationMenuProps) { "sidebarHiddenItems:colors", db.settings.getSideBarHiddenItems("colors") ); + const [currentTab, setCurrentTab] = useState<(typeof tabs)[number]["id"]>( + tabs.find((tab) => location.includes(tab.id))?.id || "home" + ); + const notes = useStore((store) => store.notes); + + useEffect(() => { + const setCounts = async () => { + const totalNotes = await db.notes.all.count(); + const totalFavorites = await db.notes.favorites.count(); + const totalReminders = await db.reminders.all.count(); + const totalTrash = (await db.trash.all()).length; + const totalMonographs = await db.monographs.all.count(); + + setRoutes((routes) => { + return routes.map((route) => { + switch (route.id) { + case "notes": + return { ...route, count: totalNotes }; + case "favorites": + return { ...route, count: totalFavorites }; + case "reminders": + return { ...route, count: totalReminders }; + case "trash": + return { ...route, count: totalTrash }; + case "monographs": + return { ...route, count: totalMonographs }; + default: + return route; + } + }); + }); + }; + + setCounts(); + }, [notes]); + const dragTimeout = useRef(0); const _navigate = useCallback( (path: string) => { toggleNavigationContainer(true); - const nestedRoute = findNestedRoute(path); - navigate(!nestedRoute || nestedRoute === location ? path : nestedRoute); + navigate(path); }, [location, toggleNavigationContainer] ); - useEffect(() => { - if (state === "forward" || state === "neutral") - navigationHistory.set(location, true); - else if (state === "same" && location !== previousLocation) { - navigationHistory.delete(previousLocation); - navigationHistory.set(location, true); - } else navigationHistory.delete(previousLocation); - }, [location, previousLocation, state]); - const getSidebarItems = useCallback(async () => { return [ { @@ -226,6 +266,69 @@ function NavigationMenu(props: NavigationMenuProps) { borderRight: "1px solid var(--separator)" }} > + + + + + + + Notesnook + + + + + + {tabs.map((tab) => ( + setCurrentTab(tab.id)} + showTitle={false} + /> + ))} + + {isTablet && ( + + )} { @@ -242,299 +347,419 @@ function NavigationMenu(props: NavigationMenuProps) { Menu.openMenu(await getSidebarItems()); }} > - ({ - width: 3 - })} - thumbStyle={() => ({ width: 3 })} - suppressScrollX={true} - > - - !hiddenRoutes.includes(r.id))} - orderKey={`sidebarOrder:routes`} - order={() => db.settings.getSideBarOrder("routes")} - onOrderChanged={(order) => - db.settings.setSideBarOrder("routes", order) - } - renderOverlay={({ item }) => ( - - )} - renderItem={({ item }) => ( - { - if (["/notebooks", "/tags"].includes(item.path)) - dragTimeout.current = setTimeout( - () => _navigate(item.path), - 1000 - ) as unknown as number; - }} - onDragLeave={() => clearTimeout(dragTimeout.current)} - onDrop={async (e) => { - clearTimeout(dragTimeout.current); - - await handleDrop(e.dataTransfer, { - type: - item.path === "/trash" - ? "trash" - : item.path === "/favorites" - ? "favorites" - : item.path === "/notebooks" - ? "notebooks" - : undefined - }); - }} - selected={ - item.path === "/" - ? location === item.path - : location.startsWith(item.path) - } - onClick={() => { - if (!isMobile && location === item.path) - return toggleNavigationContainer(); - _navigate(item.path); - }} - menuItems={[ - { - type: "lazy-loader", - key: "sidebar-items-loader", - items: getSidebarItems + {currentTab === "notebook" ? ( + + ) : currentTab === "tag" ? ( + + + + + ) : ( + ({ + width: 3 + })} + thumbStyle={() => ({ width: 3 })} + suppressScrollX={true} + > + + !hiddenRoutes.includes(r.id))} + orderKey={`sidebarOrder:routes`} + order={() => db.settings.getSideBarOrder("routes")} + onOrderChanged={(order) => + db.settings.setSideBarOrder("routes", order) + } + renderOverlay={({ item }) => ( + - )} - /> - - !hiddenColors.includes(c.id))} - orderKey={`sidebarOrder:colors`} - order={() => db.settings.getSideBarOrder("colors")} - onOrderChanged={(order) => - db.settings.setSideBarOrder("colors", order) - } - renderOverlay={({ item }) => ( - - )} - renderItem={({ item: color }) => ( - { - _navigate(`/colors/${color.id}`); - }} - onDrop={(e) => handleDrop(e.dataTransfer, color)} - menuItems={[ - { - type: "button", - key: "rename-color", - title: strings.renameColor(), - onClick: () => RenameColorDialog.show(color) - }, - { - type: "button", - key: "remove-color", - title: strings.removeColor(), - onClick: async () => { - await db.colors.remove(color.id); - await refreshNavItems(); - } - }, - { - type: "separator", - key: "sep" - }, - { - type: "lazy-loader", - key: "sidebar-items-loader", - items: getSidebarItems + /> + )} + renderItem={({ item }) => ( + { + if (["/notebooks", "/tags"].includes(item.path)) + dragTimeout.current = setTimeout( + () => _navigate(item.path), + 1000 + ) as unknown as number; + }} + onDragLeave={() => clearTimeout(dragTimeout.current)} + onDrop={async (e) => { + clearTimeout(dragTimeout.current); + + await handleDrop(e.dataTransfer, { + type: + item.path === "/trash" + ? "trash" + : item.path === "/favorites" + ? "favorites" + : item.path === "/notebooks" + ? "notebooks" + : undefined + }); + }} + selected={ + item.path === "/" + ? location === item.path + : location.startsWith(item.path) } - ]} - /> - )} - /> - - db.settings.getSideBarOrder("shortcuts")} - onOrderChanged={(order) => - db.settings.setSideBarOrder("shortcuts", order) - } - renderOverlay={({ item }) => ( - - )} - renderItem={({ item }) => ( - { - await db.shortcuts.remove(item.id); - refreshNavItems(); + onClick={() => { + if (!isMobile && location === item.path) + return toggleNavigationContainer(); + _navigate(item.path); + }} + menuItems={[ + { + type: "lazy-loader", + key: "sidebar-items-loader", + items: getSidebarItems + } + ]} + /> + )} + /> + + !hiddenColors.includes(c.id))} + orderKey={`sidebarOrder:colors`} + order={() => db.settings.getSideBarOrder("colors")} + onOrderChanged={(order) => + db.settings.setSideBarOrder("colors", order) + } + renderOverlay={({ item }) => ( + + )} + renderItem={({ item: color }) => ( + { + _navigate(`/colors/${color.id}`); + }} + onDrop={(e) => handleDrop(e.dataTransfer, color)} + menuItems={[ + { + type: "button", + key: "rename-color", + title: strings.renameColor(), + onClick: () => RenameColorDialog.show(color) + }, + { + type: "button", + key: "remove-color", + title: strings.removeColor(), + onClick: async () => { + await db.colors.remove(color.id); + await refreshNavItems(); + } + }, + { + type: "separator", + key: "sep" + }, + { + type: "lazy-loader", + key: "sidebar-items-loader", + items: getSidebarItems } + ]} + /> + )} + /> + + db.settings.getSideBarOrder("shortcuts")} + onOrderChanged={(order) => + db.settings.setSideBarOrder("shortcuts", order) + } + renderOverlay={({ item }) => ( + handleDrop(e.dataTransfer, item)} - onClick={async () => { - if (item.type === "notebook") { - const root = (await db.notebooks.breadcrumbs(item.id)).at( - 0 - ); - if (root && root.id !== item.id) - _navigate(`/notebooks/${root.id}/${item.id}`); - else _navigate(`/notebooks/${item.id}`); - } else if (item.type === "tag") { - _navigate(`/tags/${item.id}`); + isShortcut + selected={shouldSelectNavItem(location, item)} + /> + )} + renderItem={({ item }) => ( + { + await db.shortcuts.remove(item.id); + refreshNavItems(); + } + } + ]} + icon={ + item.type === "notebook" + ? Notebook2 + : item.type === "tag" + ? Tag2 + : Topic } - }} - /> - )} - /> - - - - - {isLoggedIn === false && ( - hardNavigate("/login")} - /> - )} - {isTablet && ( - { - setFollowSystemTheme(false); - toggleNightMode(); - }} - /> - )} - { - if (!isMobile && location === settings.path) - return toggleNavigationContainer(); - hashNavigate("/settings"); - }} - selected={location.startsWith(settings.path)} - > - {isTablet ? null : ( - - )} - - + /> + + + )} ); } export default NavigationMenu; -function findNestedRoute(location: string) { - let level = location.split("/").length; - let nestedRoute = undefined; - const history = Array.from(navigationHistory.keys()); - for (let i = history.length - 1; i >= 0; --i) { - const route = history[i]; - if (!navigationHistory.get(route)) continue; - - const routeLevel = route.split("/").length; - if (route.startsWith(location) && routeLevel > level) { - level = routeLevel; - nestedRoute = route; - } - } - return nestedRoute; +type NavigationDropdownProps = { + toggleNavigationContainer: NavigationMenuProps["toggleNavigationContainer"]; +}; + +function NavigationDropdown({ + toggleNavigationContainer +}: NavigationDropdownProps) { + const isMobile = useMobile(); + const [location] = useLocation(); + const user = useUserStore((store) => store.user); + const profile = useSettingStore((store) => store.profile); + const theme = useThemeStore((store) => store.colorScheme); + const toggleNightMode = useThemeStore((store) => store.toggleColorScheme); + const setFollowSystemTheme = useThemeStore( + (store) => store.setFollowSystemTheme + ); + + const { isPro } = useMemo(() => { + const type = user?.subscription?.type; + const expiry = user?.subscription?.expiry; + if (!expiry) return { isBasic: true, remainingDays: 0 }; + return { + isTrial: type === SUBSCRIPTION_STATUS.TRIAL, + isBasic: type === SUBSCRIPTION_STATUS.BASIC, + isBeta: type === SUBSCRIPTION_STATUS.BETA, + isPro: type === SUBSCRIPTION_STATUS.PREMIUM, + isProCancelled: type === SUBSCRIPTION_STATUS.PREMIUM_CANCELED, + isProExpired: type === SUBSCRIPTION_STATUS.PREMIUM_EXPIRED + }; + }, [user]); + + const notLoggedIn = Boolean(!user || !user.id); + + return ( + + { + e.preventDefault(); + Menu.openMenu( + [ + { + type: "popup", + component: () => , + key: "profile" + }, + { + type: "separator", + key: "sep" + }, + { + type: "button", + title: strings.login(), + icon: Login.path, + key: "login", + isHidden: !notLoggedIn, + onClick: () => hardNavigate("/login") + }, + { + type: "button", + title: strings.toggleDarkLightMode(), + key: "toggle-theme-mode", + icon: theme === "dark" ? LightMode.path : DarkMode.path, + onClick: () => { + setFollowSystemTheme(false); + toggleNightMode(); + } + }, + { + type: "button", + title: strings.upgradeToPro(), + icon: Pro.path, + key: "upgrade", + isHidden: notLoggedIn || isPro + }, + { + type: "button", + title: settings.title, + key: settings.id, + icon: settings.icon.path, + onClick: () => { + if (!isMobile && location === settings.path) { + return toggleNavigationContainer(); + } + hashNavigate(settings.path); + } + }, + { + type: "button", + title: strings.helpAndSupport(), + icon: Documentation.path, + key: "help-and-support", + onClick: () => { + window.open("https://help.notesnook.com/", "_blank"); + } + }, + { + type: "button", + title: strings.logout(), + icon: Logout.path, + key: "logout", + isHidden: notLoggedIn, + onClick: async () => { + const result = await showLogoutConfirmation(); + if (!result) return; + + if (result.backup) { + try { + await createBackup({ mode: "partial" }); + } catch (e) { + logger.error(e, "Failed to take backup before logout"); + if ( + !(await ConfirmDialog.show({ + title: strings.failedToTakeBackup(), + message: strings.failedToTakeBackupMessage(), + negativeButtonText: strings.no(), + positiveButtonText: strings.yes() + })) + ) + return; + } + } + + await TaskManager.startTask({ + type: "modal", + title: strings.loggingOut(), + subtitle: strings.pleaseWait(), + action: () => db.user.logout(true) + }); + showToast("success", strings.loggedOut()); + } + } + ], + { + position: { + target: e.currentTarget, + location: "below", + yOffset: 5 + } + } + ); + }} + variant="columnCenter" + sx={{ + bg: "shade", + mr: 2, + size: 35, + borderRadius: 80, + cursor: "pointer", + position: "relative", + outline: "1px solid var(--accent)", + ":hover": { + outline: "2px solid var(--accent)" + } + }} + > + {!user || !user.id || !profile?.profilePicture ? ( + + ) : ( + + )} + + + ); } type ReorderableListProps = { diff --git a/apps/web/src/components/navigation-menu/navigation-item.tsx b/apps/web/src/components/navigation-menu/navigation-item.tsx index 3568590193..03bf089e51 100644 --- a/apps/web/src/components/navigation-menu/navigation-item.tsx +++ b/apps/web/src/components/navigation-menu/navigation-item.tsx @@ -32,7 +32,7 @@ type NavigationItemProps = { icon?: Icon; image?: string; color?: SchemeColors; - title: string; + title?: string; isTablet?: boolean; isLoading?: boolean; isShortcut?: boolean; @@ -41,6 +41,7 @@ type NavigationItemProps = { onClick?: () => void; count?: number; menuItems?: MenuItem[]; + showTitle?: boolean; }; function NavigationItem( @@ -64,6 +65,7 @@ function NavigationItem( count, sx, containerRef, + showTitle = true, ...restProps } = props; const toggleSideMenu = useAppStore((store) => store.toggleSideMenu); @@ -131,7 +133,7 @@ function NavigationItem( /> ) : Icon ? ( @@ -145,22 +147,22 @@ function NavigationItem( /> )} - - {title} - {/* {tag && ( + {showTitle && ( + + {title} + {/* {tag && ( )} */} - + + )} {children ? ( children diff --git a/apps/web/src/components/navigation-menu/notebook-tree.tsx b/apps/web/src/components/navigation-menu/notebook-tree.tsx new file mode 100644 index 0000000000..89a936b683 --- /dev/null +++ b/apps/web/src/components/navigation-menu/notebook-tree.tsx @@ -0,0 +1,176 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { Notebook } from "@notesnook/core"; +import { Button, Flex } from "@theme-ui/components"; +import { useEffect, useRef, useState } from "react"; +import { CREATE_BUTTON_MAP } from "../../common"; +import { db } from "../../common/db"; +import { store, useStore } from "../../stores/notebook-store"; +import { useStore as useSelectionStore } from "../../stores/selection-store"; +import Placeholder from "../placeholders"; +import SubNotebook from "../sub-notebook"; +import { + TreeNode, + VirtualizedTree, + VirtualizedTreeHandle +} from "../virtualized-tree"; +import { ListLoader } from "../loaders/list-loader"; + +function NotebookTree() { + const notebooks = useStore((state) => state.notebooks); + const createButton = CREATE_BUTTON_MAP.notebooks; + + useEffect(() => { + store.get().refresh(); + }, []); + + if (!notebooks) return ; + if (notebooks.length === 0) { + return ( + + + + ); + } + return ( + + +
+ +
+
+ ); +} +export default NotebookTree; + +function Tree() { + const treeRef = + useRef>( + null + ); + const setSelectedItems = useSelectionStore((store) => store.setSelectedItems); + const isSelected = useSelectionStore((store) => store.isSelected); + const selectItem = useSelectionStore((store) => store.selectItem); + const deselectItem = useSelectionStore((store) => store.deselectItem); + const toggleSelection = useSelectionStore( + (store) => store.toggleSelectionMode + ); + const notebooks = useStore((store) => store.notebooks); + const [notebookIds, setNotebookIds] = useState([]); + + useEffect(() => { + notebooks?.ids().then((ids) => setNotebookIds(ids)); + treeRef.current?.refresh(); + }, [notebooks]); + + return ( + + {notebookIds.length > 0 && ( + toggleSelection(false)} + bulkSelect={setSelectedItems} + isSelected={isSelected} + onDeselect={deselectItem} + onSelect={selectItem} + saveKey="notebook-tree" + getChildNodes={async (id, depth) => { + const nodes: TreeNode<{ + notebook: Notebook; + totalNotes: number; + }>[] = []; + if (id === "root") { + for (const id of notebookIds) { + const notebook = (await db.notebooks.notebook(id))!; + const totalNotes = await db.relations + .from(notebook, "note") + .count(); + const children = await db.relations + .from(notebook, "notebook") + .count(); + nodes.push({ + data: { notebook, totalNotes }, + depth: depth + 1, + hasChildren: children > 0, + id, + parentId: "root" + }); + } + return nodes; + } + + const subNotebooks = await db.relations + .from({ type: "notebook", id }, "notebook") + .resolve(); + + for (const notebook of subNotebooks) { + const hasChildren = + (await db.relations.from(notebook, "notebook").count()) > 0; + const totalNotes = await db.relations + .from(notebook, "note") + .count(); + nodes.push({ + parentId: id, + id: notebook.id, + data: { notebook, totalNotes }, + depth: depth + 1, + hasChildren + }); + } + + return nodes; + }} + renderItem={({ collapse, expand, expanded, index, item: node }) => ( + { + const notebook = await db.notebooks.notebook(node.id); + const totalNotes = await db.relations + .from(node.data.notebook, "note") + .count(); + treeRef.current?.refreshItem( + index, + notebook ? { notebook, totalNotes } : undefined + ); + }} + collapse={collapse} + expand={expand} + /> + )} + /> + )} + + ); +} diff --git a/apps/web/src/components/route-container/index.tsx b/apps/web/src/components/route-container/index.tsx index 2ca229b169..f7910d2c44 100644 --- a/apps/web/src/components/route-container/index.tsx +++ b/apps/web/src/components/route-container/index.tsx @@ -135,13 +135,15 @@ function Header(props: RouteContainerProps) { /> ) : ( toggleSideMenu(true)} + onClick={() => { + toggleSideMenu(true); + }} sx={{ flexShrink: 0, ml: 0, mr: 4, mt: 1, - display: ["block", "none", "none"] + display: ["block", "block", "none"] }} size={30} /> diff --git a/apps/web/src/components/sub-notebook/index.tsx b/apps/web/src/components/sub-notebook/index.tsx index 1fca89115a..9b366e3792 100644 --- a/apps/web/src/components/sub-notebook/index.tsx +++ b/apps/web/src/components/sub-notebook/index.tsx @@ -73,6 +73,10 @@ function SubNotebook(props: SubNotebookProps) { item, totalNotes }); + if (rootId === "root") { + navigate(`/notebooks/${item.id}`); + return; + } navigate(`/notebooks/${rootId}/${item.id}`); }, [expand, focus, isOpened, item, rootId, totalNotes]); diff --git a/apps/web/src/components/virtualized-tree/index.tsx b/apps/web/src/components/virtualized-tree/index.tsx index 39402ac790..25cd588939 100644 --- a/apps/web/src/components/virtualized-tree/index.tsx +++ b/apps/web/src/components/virtualized-tree/index.tsx @@ -85,8 +85,15 @@ export function VirtualizedTree(props: TreeViewProps) { treeRef, () => ({ async refresh() { - const children = await getChildNodes(rootId, -1); - setNodes(children); + // const children = await getChildNodes(rootId, -1); + // setNodes(children); + const nodes = await fetchChildren( + rootId, + -1, + expandedIds, + getChildNodes + ); + setNodes(nodes); }, async refreshItem(index, item) { const node = nodes[index]; @@ -103,6 +110,11 @@ export function VirtualizedTree(props: TreeViewProps) { return; } + // TODO: double check + if (node.hasChildren) { + expandedIds[node.id] = true; + } + const children = await fetchChildren( node.id, node.depth, diff --git a/apps/web/src/dialogs/settings/components/user-profile.tsx b/apps/web/src/dialogs/settings/components/user-profile.tsx index 5c8a3e5eae..bd4e7a5dcd 100644 --- a/apps/web/src/dialogs/settings/components/user-profile.tsx +++ b/apps/web/src/dialogs/settings/components/user-profile.tsx @@ -32,7 +32,11 @@ import { EditProfilePictureDialog } from "../../edit-profile-picture-dialog"; import { PromptDialog } from "../../prompt"; import { strings } from "@notesnook/intl"; -export function UserProfile() { +type Props = { + minimal?: boolean; +}; + +export function UserProfile({ minimal }: Props) { const user = useUserStore((store) => store.user); const profile = useSettingStore((store) => store.profile); @@ -67,7 +71,7 @@ export function UserProfile() { alignItems: "center", bg: "var(--background-secondary)", p: 2, - mb: 4 + mb: minimal ? 0 : 4 }} > @@ -110,7 +114,7 @@ export function UserProfile() { overflow: "hidden", position: "relative", ":hover #profile-picture-edit": { - visibility: "visible" + visibility: minimal ? "hidden" : "visible" } }} > @@ -165,32 +169,40 @@ export function UserProfile() { {profile?.fullName || strings.yourFullName()}{" "} - { - const fullName = await PromptDialog.show({ - title: strings.editFullName(), - description: strings.setFullNameDesc(), - defaultValue: profile?.fullName - }); - try { - await db.settings.setProfile({ - fullName: fullName || undefined + {minimal ? null : ( + { + const fullName = await PromptDialog.show({ + title: strings.editFullName(), + description: strings.setFullNameDesc(), + defaultValue: profile?.fullName }); - await useSettingStore.getState().refresh(); - showToast("success", strings.fullNameUpdated()); - } catch (e) { - showToast("error", (e as Error).message); - } - }} - /> + try { + await db.settings.setProfile({ + fullName: fullName || undefined + }); + await useSettingStore.getState().refresh(); + showToast("success", strings.fullNameUpdated()); + } catch (e) { + showToast("error", (e as Error).message); + } + }} + /> + )} - {user.email} •{" "} - {strings.memberSince( - getFormattedDate(getObjectIdTimestamp(user.id), "date") + {user.email} + {minimal ? null : ( + <> + {" "} + •{" "} + {strings.memberSince( + getFormattedDate(getObjectIdTimestamp(user.id), "date") + )} + )} diff --git a/apps/web/src/navigation/routes.tsx b/apps/web/src/navigation/routes.tsx index e58d95b172..92d398a353 100644 --- a/apps/web/src/navigation/routes.tsx +++ b/apps/web/src/navigation/routes.tsx @@ -77,10 +77,6 @@ const routes = defineRoutes({ // ), buttons: { create: CREATE_BUTTON_MAP.notes, - back: { - title: strings.goBackToNotebooks(), - onClick: () => navigate("/notebooks") - }, search: { title: strings.searchANote() } @@ -153,6 +149,9 @@ const routes = defineRoutes({ title: strings.routes.Tags(), type: "tags", component: Tags, + props: { + location: "middle-pane" + }, buttons: { create: CREATE_BUTTON_MAP.tags, search: { @@ -173,10 +172,6 @@ const routes = defineRoutes({ component: Notes, buttons: { create: CREATE_BUTTON_MAP.notes, - back: { - title: strings.goBackToTags(), - onClick: () => navigate("/tags") - }, search: { title: strings.searchANote() } diff --git a/apps/web/src/stores/app-store.ts b/apps/web/src/stores/app-store.ts index 30fd5b5821..01ab918a0f 100644 --- a/apps/web/src/stores/app-store.ts +++ b/apps/web/src/stores/app-store.ts @@ -74,7 +74,7 @@ class AppStore extends BaseStore { progress: null, type: undefined }; - colors: Color[] = []; + colors: (Color & { count: number })[] = []; notices: Notice[] = []; shortcuts: (Notebook | Tag)[] = []; lastSynced = 0; @@ -166,9 +166,16 @@ class AppStore extends BaseStore { refreshNavItems = async () => { const shortcuts = await db.shortcuts.resolved(); const colors = await db.colors.all.items(); + + let newColors: (Color & { count: number })[] = []; + for (const color of colors) { + const count = await db.colors.count(color.id); + newColors.push({ ...color, count: count ?? 0 }); + } + this.set((state) => { state.shortcuts = shortcuts; - state.colors = colors; + state.colors = newColors; }); }; diff --git a/apps/web/src/views/notebook.tsx b/apps/web/src/views/notebook.tsx index 55e50bf603..f4b47a15db 100644 --- a/apps/web/src/views/notebook.tsx +++ b/apps/web/src/views/notebook.tsx @@ -61,10 +61,6 @@ type NotebookProps = { }; function Notebook(props: NotebookProps) { const { rootId, notebookId } = props; - const [isCollapsed, setIsCollapsed] = useState(false); - - const subNotebooksPane = useRef(null); - const context = useNotesStore((store) => store.context); const notes = useNotesStore((store) => store.contextNotes); @@ -108,173 +104,11 @@ function Notebook(props: NotebookProps) { } /> - - setIsCollapsed(size <= 7)} - > - { - if (isCollapsed) subNotebooksPane.current?.expand(); - else subNotebooksPane.current?.collapse(); - }} - /> - ); } export default Notebook; -type SubNotebooksProps = { - rootId: string; - isCollapsed: boolean; - onClick: () => void; -}; -function SubNotebooks({ rootId, isCollapsed, onClick }: SubNotebooksProps) { - // sometimes the onClick event is triggered on dragEnd - // which shouldn't happen. To prevent that we make sure - // that onMouseDown & onMouseUp events got called. - const mouseEventCounter = useRef(0); - const treeRef = useRef>(null); - const setSelectedItems = useSelectionStore((store) => store.setSelectedItems); - const isSelected = useSelectionStore((store) => store.isSelected); - const selectItem = useSelectionStore((store) => store.selectItem); - const deselectItem = useSelectionStore((store) => store.deselectItem); - const toggleSelection = useSelectionStore( - (store) => store.toggleSelectionMode - ); - const rootNotebook = usePromise(() => db.notebooks.notebook(rootId)); - const notebooks = useNotebookStore((store) => store.notebooks); - - useEffect(() => { - treeRef.current?.refresh(); - }, [notebooks]); - - if (!rootId) return null; - - return ( - - { - mouseEventCounter.current = 1; - }} - onMouseUp={() => { - mouseEventCounter.current++; - }} - onClick={() => { - if (mouseEventCounter.current === 2) onClick(); - mouseEventCounter.current = 0; - }} - > - - {isCollapsed ? : } - - {rootNotebook.status === "fulfilled" && rootNotebook.value - ? rootNotebook.value.title - : ""} - - - - {/* */} - - - - - toggleSelection(false)} - bulkSelect={setSelectedItems} - isSelected={isSelected} - onDeselect={deselectItem} - onSelect={selectItem} - treeRef={treeRef} - placeholder={() => ( - - {strings.emptyPlaceholders("notebook")} - - )} - saveKey={`${rootId}-subnotebooks`} - testId="subnotebooks-list" - renderItem={({ collapse, expand, expanded, index, item: node }) => ( - { - const notebook = await db.notebooks.notebook(node.id); - const totalNotes = await db.relations - .from(node.data.notebook, "note") - .count(); - treeRef.current?.refreshItem( - index, - notebook ? { notebook, totalNotes } : undefined - ); - }} - collapse={collapse} - expand={expand} - /> - )} - /> - - ); -} - function NotebookHeader({ rootId, context @@ -334,7 +168,6 @@ function NotebookHeader({ ref={moreCrumbsRef} variant="icon" sx={{ p: 0, flexShrink: 0 }} - onClick={() => navigateCrumb("notebooks")} title={strings.notebooks()} > diff --git a/apps/web/src/views/notebooks.tsx b/apps/web/src/views/notebooks.tsx index ce9da336b9..125edfb916 100644 --- a/apps/web/src/views/notebooks.tsx +++ b/apps/web/src/views/notebooks.tsx @@ -17,42 +17,41 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import ListContainer from "../components/list-container"; import { useStore, store } from "../stores/notebook-store"; -import { hashNavigate } from "../navigation"; +import { navigate } from "../navigation"; import Placeholder from "../components/placeholders"; import { useEffect } from "react"; -import { db } from "../common/db"; -import { useSearch } from "../hooks/use-search"; import { ListLoader } from "../components/loaders/list-loader"; +import { Flex } from "@theme-ui/components"; function Notebooks() { const notebooks = useStore((state) => state.notebooks); - const refresh = useStore((state) => state.refresh); - const filteredItems = useSearch("notebooks", (query) => - db.lookup.notebooks(query).sorted() - ); - const isCompact = useStore((store) => store.viewMode === "compact"); useEffect(() => { store.get().refresh(); - }, []); + + if (notebooks && notebooks.length > 0) { + notebooks.item(0).then((item) => { + if (item && item?.item) { + navigate(`/notebooks/${item.item.id}`); + } + }); + } + }, [notebooks]); if (!notebooks) return ; - return ( - <> - } - compact={isCompact} - button={{ - onClick: () => hashNavigate("/notebooks/create") - }} - /> - - ); + + if (notebooks.length === 0) { + return ( + + + + + + ); + } + + return null; } export default Notebooks; diff --git a/apps/web/src/views/tags.tsx b/apps/web/src/views/tags.tsx index fea09211cb..c0c5100191 100644 --- a/apps/web/src/views/tags.tsx +++ b/apps/web/src/views/tags.tsx @@ -24,8 +24,14 @@ import Placeholder from "../components/placeholders"; import { useSearch } from "../hooks/use-search"; import { db } from "../common/db"; import { ListLoader } from "../components/loaders/list-loader"; +import { useEffect } from "react"; +import { navigate } from "../navigation"; -function Tags() { +type Props = { + location: "middle-pane" | "sidebar"; +}; + +function Tags({ location }: Props) { useNavigate("tags", () => store.refresh()); const tags = useStore((store) => store.tags); const refresh = useStore((store) => store.refresh); @@ -33,10 +39,18 @@ function Tags() { db.lookup.tags(query).sorted() ); + useEffect(() => { + if (location === "sidebar") return; + tags?.item(0).then((item) => { + if (item && item?.item) { + navigate(`/tags/${item.item.id}`); + } + }); + }, [tags, location]); + if (!tags) return ; return ( } diff --git a/packages/core/src/collections/colors.ts b/packages/core/src/collections/colors.ts index 378b4a6e41..d36b787844 100644 --- a/packages/core/src/collections/colors.ts +++ b/packages/core/src/collections/colors.ts @@ -112,6 +112,12 @@ export class Colors implements ICollection { ); } + async count(id: string) { + const color = await this.color(id); + if (!color) return; + return this.db.relations.from(color, "note").count(); + } + async remove(...ids: string[]) { await this.db.transaction(async () => { await this.db.relations.unlinkOfType("color", ids);