From 72a80c8f57448e083b7af23ea2a02ccf0268b8c7 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Wed, 19 Feb 2025 11:50:09 +0500 Subject: [PATCH] mobile: fix moving and updating notebooks --- .../components/sheets/move-notebook/index.tsx | 45 +- .../sheets/notebook-sheet/index.tsx | 649 ------------------ .../side-menu/side-menu-notebooks.tsx | 126 +--- apps/mobile/app/screens/notebook/index.tsx | 10 +- .../app/stores/create-notebook-tree-stores.ts | 15 +- apps/mobile/app/utils/functions.ts | 19 +- apps/mobile/app/utils/notebooks.ts | 23 + 7 files changed, 92 insertions(+), 795 deletions(-) delete mode 100644 apps/mobile/app/components/sheets/notebook-sheet/index.tsx diff --git a/apps/mobile/app/components/sheets/move-notebook/index.tsx b/apps/mobile/app/components/sheets/move-notebook/index.tsx index dfe2cc983..b0d346f2b 100644 --- a/apps/mobile/app/components/sheets/move-notebook/index.tsx +++ b/apps/mobile/app/components/sheets/move-notebook/index.tsx @@ -25,14 +25,15 @@ import { FlatList } from "react-native-actions-sheet"; import create from "zustand"; import { db } from "../../../common/database"; import { useNotebook } from "../../../hooks/use-notebook"; -import { eSendEvent, presentSheet } from "../../../services/event-manager"; +import { presentSheet } from "../../../services/event-manager"; import { useNotebookStore, useNotebooks } from "../../../stores/use-notebook-store"; -import { eOnNotebookUpdated } from "../../../utils/events"; import { + checkParentSelected, findRootNotebookId, + findSelectedParent, getParentNotebookId } from "../../../utils/notebooks"; import { AppFontSize } from "../../../utils/size"; @@ -47,6 +48,10 @@ import { IconButton } from "../../ui/icon-button"; import { Pressable } from "../../ui/pressable"; import Paragraph from "../../ui/typography/paragraph"; import { AddNotebookSheet } from "../add-notebook"; +import { + useSideMenuNotebookExpandedStore, + useSideMenuNotebookSelectionStore +} from "../../side-menu/stores"; const useNotebookExpandedStore = create<{ expanded: { @@ -79,7 +84,7 @@ export const MoveNotebookSheet = ({ useEffect(() => { (async () => { for (const notebook of selectedNotebooks) { - const root = await findRootNotebookId(notebook.id); + const root = await findSelectedParent(notebook.id); if (root !== notebook.id) { setMoveToTop(true); return; @@ -107,6 +112,9 @@ export const MoveNotebookSheet = ({ context: "move-notebook", positivePress: async () => { for (const notebook of selectedNotebooks) { + if (await checkParentSelected(notebook.id, selectedNotebooks)) + continue; + const parent = await getParentNotebookId(notebook.id); const root = await findRootNotebookId(notebook.id); @@ -122,12 +130,23 @@ export const MoveNotebookSheet = ({ ); } await db.relations.add(selectedNotebook, notebook); - if (parent) { - eSendEvent(eOnNotebookUpdated, parent); - } } useNotebookStore.getState().refresh(); - eSendEvent(eOnNotebookUpdated, selectedNotebook.id); + + if ( + !useSideMenuNotebookExpandedStore.getState().expanded[ + selectedNotebook.id + ] + ) { + useSideMenuNotebookExpandedStore + .getState() + .setExpanded(selectedNotebook.id); + } + + useSideMenuNotebookSelectionStore.setState({ + enabled: false, + selection: {} + }); close?.(); } }); @@ -179,8 +198,12 @@ export const MoveNotebookSheet = ({ type="secondaryAccented" onPress={async () => { for (const notebook of selectedNotebooks) { + if ( + await checkParentSelected(notebook.id, selectedNotebooks) + ) + continue; const parent = await getParentNotebookId(notebook.id); - const root = await findRootNotebookId(notebook.id); + const root = await findSelectedParent(notebook.id); if (root !== notebook.id) { await db.relations.unlink( { @@ -189,11 +212,13 @@ export const MoveNotebookSheet = ({ }, notebook ); - eSendEvent(eOnNotebookUpdated, parent); - eSendEvent(eOnNotebookUpdated, notebook.id); } } useNotebookStore.getState().refresh(); + useSideMenuNotebookSelectionStore.setState({ + enabled: false, + selection: {} + }); close?.(); }} /> diff --git a/apps/mobile/app/components/sheets/notebook-sheet/index.tsx b/apps/mobile/app/components/sheets/notebook-sheet/index.tsx deleted file mode 100644 index b1d57bffe..000000000 --- a/apps/mobile/app/components/sheets/notebook-sheet/index.tsx +++ /dev/null @@ -1,649 +0,0 @@ -/* -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, VirtualizedGrouping } from "@notesnook/core"; -import { useThemeColors } from "@notesnook/theme"; -import React, { useEffect, useRef, useState } from "react"; -import { - Platform, - RefreshControl, - View, - useWindowDimensions -} from "react-native"; -import ActionSheet, { ActionSheetRef } from "react-native-actions-sheet"; -import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList"; -import Config from "react-native-config"; -import Icon from "react-native-vector-icons/MaterialCommunityIcons"; -import create from "zustand"; -import { notesnook } from "../../../../e2e/test.ids"; -import { db } from "../../../common/database"; -import { MMKV } from "../../../common/database/mmkv"; -import { useNotebook } from "../../../hooks/use-notebook"; -import NotebookScreen from "../../../screens/notebook"; -import { openEditor } from "../../../screens/notes/common"; -import { eSendEvent, presentSheet } from "../../../services/event-manager"; -import { createItemSelectionStore } from "../../../stores/item-selection-store"; -import useNavigationStore from "../../../stores/use-navigation-store"; -import { useSelectionStore } from "../../../stores/use-selection-store"; -import { eOnNotebookUpdated } from "../../../utils/events"; -import { deleteItems } from "../../../utils/functions"; -import { findRootNotebookId } from "../../../utils/notebooks"; -import { AppFontSize, normalize } from "../../../utils/size"; -import { Properties } from "../../properties"; -import { IconButton } from "../../ui/icon-button"; -import { Pressable } from "../../ui/pressable"; -import Paragraph from "../../ui/typography/paragraph"; -import { AddNotebookSheet } from "../add-notebook"; -import { MoveNotebookSheet } from "../move-notebook"; -import Sort from "../sort"; -import { strings } from "@notesnook/intl"; - -const useItemSelectionStore = createItemSelectionStore(true, false); - -type NotebookParentProp = { - parent?: NotebookParentProp; - item?: Notebook; -}; - -type ConfigItem = { id: string; type: string }; -class NotebookSheetConfig { - static storageKey: "$$sp"; - - static makeId(item: ConfigItem) { - return `${NotebookSheetConfig.storageKey}:${item.type}:${item.id}`; - } - - static get(item: ConfigItem) { - const value = MMKV.getInt(NotebookSheetConfig.makeId(item)); - return typeof value === "number" ? value : 0; - } - - static set(item: ConfigItem, index = 0) { - MMKV.setInt(NotebookSheetConfig.makeId(item), index); - } -} - -const useNotebookExpandedStore = create<{ - expanded: { - [id: string]: boolean; - }; - setExpanded: (id: string) => void; -}>((set, get) => ({ - expanded: {}, - setExpanded(id: string) { - set({ - expanded: { - ...get().expanded, - [id]: !get().expanded[id] - } - }); - } -})); - -export const NotebookSheet = () => { - const [collapsed, setCollapsed] = useState(false); - const currentRoute = useNavigationStore((state) => state.currentRoute); - const focusedRouteId = useNavigationStore((state) => state.focusedRouteId); - const enabled = useItemSelectionStore((state) => state.enabled); - const canShow = currentRoute === "Notebook"; - const { colors } = useThemeColors("sheet"); - const ref = useRef(null); - const currentItem = useRef(); - const { fontScale } = useWindowDimensions(); - const [root, setRoot] = useState(); - const { - onUpdate: onRequestUpdate, - notebook, - nestedNotebooks: notebooks, - nestedNotebookNotesCount: totalNotes, - groupOptions - } = useNotebook( - currentRoute === "Notebook" ? root : undefined, - undefined, - true - ); - - const renderNotebook = ({ index }: { item: boolean; index: number }) => ( - - ); - - useEffect(() => { - if (canShow) { - setImmediate(async () => { - if (!focusedRouteId) return; - const nextRoot = await findRootNotebookId(focusedRouteId); - if (nextRoot !== currentItem.current) { - useItemSelectionStore.setState({ - enabled: false, - selection: {} - }); - } - currentItem.current = nextRoot; - const snapPoint = NotebookSheetConfig.get({ - type: "notebook", - id: focusedRouteId as string - }); - - if (ref.current?.isOpen()) { - ref.current?.snapToIndex(snapPoint); - } else { - ref.current?.show(snapPoint); - } - setRoot(nextRoot); - onRequestUpdate(); - }); - } else { - if (ref.current?.isOpen()) { - useItemSelectionStore.setState({ - enabled: false, - selection: {} - }); - ref.current?.hide(); - } - } - }, [canShow, focusedRouteId]); - - return ( - { - setCollapsed(index === 0); - NotebookSheetConfig.set( - { - type: "notebook", - id: focusedRouteId as string - }, - index - ); - }} - overlayColor={colors.primary.backdrop} - closable={!canShow} - elevation={10} - indicatorStyle={{ - width: 100, - backgroundColor: colors.secondary.background - }} - keyboardHandlerEnabled={false} - snapPoints={ - Config.isTesting === "true" - ? [100] - : [Platform.OS === "android" ? 15 : 10, 100] - } - initialSnapIndex={1} - backgroundInteractionEnabled - gestureEnabled - > - {/* - { - if (!notebook) return; - - }} - style={{ - borderRadius: 100 - }} - > - - - - - */} - - - - - - - - - - - - {strings.notebooks()} - - - {enabled ? ( - <> - { - await deleteItems( - "notebook", - useItemSelectionStore.getState().getSelectedItemIds() - ); - useSelectionStore.getState().clearSelection(); - useItemSelectionStore.setState({ - enabled: false, - selection: {} - }); - return; - }} - color={colors.primary.icon} - tooltipText="Move to trash" - tooltipPosition={1} - name="delete" - size={22} - /> - - { - const ids = useItemSelectionStore - .getState() - .getSelectedItemIds(); - const notebooks = await db.notebooks.all.items(ids); - MoveNotebookSheet.present(notebooks); - }} - color={colors.primary.icon} - tooltipText="Clear selection" - tooltipPosition={1} - name="arrow-right-bold-box-outline" - size={22} - /> - - { - useSelectionStore.getState().clearSelection(); - useItemSelectionStore.setState({ - enabled: false, - selection: {} - }); - }} - color={colors.primary.icon} - tooltipText="Clear selection" - tooltipPosition={1} - name="close" - size={22} - /> - - ) : ( - <> - { - presentSheet({ - component: - }); - }} - testID="group-topic-button" - color={colors.primary.icon} - size={22} - style={{ - width: 40 * fontScale, - height: 40 * fontScale - }} - /> - - { - if (ref.current?.currentSnapIndex() !== 0) { - setCollapsed(true); - ref.current?.snapToIndex(0); - } else { - setCollapsed(false); - ref.current?.snapToIndex(1); - } - }} - color={colors.primary.icon} - size={22} - style={{ - width: 40 * fontScale, - height: 40 * fontScale - }} - /> - - { - if (!notebook) return; - AddNotebookSheet.present( - undefined, - notebook, - undefined, - undefined, - false - ); - }} - color={colors.primary.icon} - size={22} - style={{ - width: 40 * fontScale, - height: 40 * fontScale - }} - /> - - )} - - - { - eSendEvent(eOnNotebookUpdated); - }} - colors={[colors.primary.accent]} - progressBackgroundColor={colors.primary.background} - /> - } - renderItem={renderNotebook} - ListEmptyComponent={ - - - {strings.emptyPlaceholders("notebook")} - - - } - /> - - - ); -}; - -const NotebookItem = ({ - id, - totalNotes, - currentLevel = 0, - index, - parent, - items -}: { - id: string | number; - totalNotes: (id: string) => number; - currentLevel?: number; - index: number; - parent?: NotebookParentProp; - items?: VirtualizedGrouping; -}) => { - const { - nestedNotebookNotesCount, - nestedNotebooks, - notebook: item - } = useNotebook(id, items, true); - const isFocused = useNavigationStore((state) => state.focusedRouteId === id); - const { colors } = useThemeColors("sheet"); - const isSelected = useItemSelectionStore((state) => - item?.id ? state.selection[item.id] === "selected" : false - ); - const enabled = useItemSelectionStore((state) => state.enabled); - - const { fontScale } = useWindowDimensions(); - const expanded = useNotebookExpandedStore((state) => - item?.id ? state.expanded[item?.id] : undefined - ); - - return ( - 0 && currentLevel < 6 ? 15 : undefined, - width: "100%" - }} - > - { - if (enabled || !item) return; - useItemSelectionStore.setState({ - enabled: true, - selection: {} - }); - useItemSelectionStore - .getState() - .markAs(item, isSelected ? "deselected" : "selected"); - }} - testID={`notebook-sheet-item-${currentLevel}-${index}`} - onPress={() => { - if (!item) return; - if (enabled) { - useItemSelectionStore - .getState() - .markAs(item, isSelected ? "deselected" : "selected"); - return; - } - NotebookScreen.navigate(item, true); - }} - style={{ - justifyContent: "space-between", - width: "100%", - alignItems: "center", - flexDirection: "row", - paddingHorizontal: 12, - borderRadius: 0 - }} - > - - {nestedNotebooks?.placeholders.length ? ( - { - if (!item?.id) return; - useNotebookExpandedStore.getState().setExpanded(item?.id); - }} - top={0} - left={0} - bottom={0} - right={0} - style={{ - width: 35, - height: 35 - }} - name={expanded ? "chevron-down" : "chevron-right"} - /> - ) : ( - - )} - - {enabled ? ( - - ) : null} - - - {item?.title} - - - - - {item?.id && totalNotes?.(item?.id) ? ( - - {totalNotes(item?.id)} - - ) : null} - { - Properties.present(item); - }} - left={0} - right={0} - bottom={0} - top={0} - color={colors.primary.icon} - size={AppFontSize.xl} - /> - - - - {!expanded - ? null - : item && - nestedNotebooks?.placeholders.map((id, index) => ( - - ))} - - ); -}; diff --git a/apps/mobile/app/components/side-menu/side-menu-notebooks.tsx b/apps/mobile/app/components/side-menu/side-menu-notebooks.tsx index 7d35cfe5f..1fe4d23dc 100644 --- a/apps/mobile/app/components/side-menu/side-menu-notebooks.tsx +++ b/apps/mobile/app/components/side-menu/side-menu-notebooks.tsx @@ -77,8 +77,6 @@ const NotebookItem = ({ onLongPress?: () => void; }) => { const notebook = item.notebook; - const [nestedNotebooksSelected, setNestedNotebooksSelected] = - React.useState(false); const isFocused = focused; const { totalNotes, getTotalNotes } = useTotalNotes("notebook"); const getTotalNotesRef = React.useRef(getTotalNotes); @@ -89,97 +87,6 @@ const NotebookItem = ({ getTotalNotesRef.current([item.notebook.id]); }, [item.notebook]); - useEffect(() => { - if (selectionEnabled) { - const selector = db.relations.from( - { - type: "notebook", - id: item.notebook.id - }, - "notebook" - ).selector; - selector.ids().then((ids) => { - setNestedNotebooksSelected( - ids.length === 0 - ? true - : ids.every( - (id) => selectionStore.getState().selection[id] === "selected" - ) - ); - }); - } - }, [selected, item.notebook.id, selectionEnabled, selectionStore]); - - async function selectAll() { - const selector = db.relations.from( - { - type: "notebook", - id: item.notebook.id - }, - "notebook" - ).selector; - const ids = await selector.ids(); - selectionStore.setState({ - selection: { - ...selectionStore.getState().selection, - ...ids.reduce((acc: any, id) => { - acc[id] = "selected"; - return acc; - }, {}) - } - }); - setNestedNotebooksSelected(true); - } - - async function deselectAll() { - const selector = db.relations.from( - { - type: "notebook", - id: item.notebook.id - }, - "notebook" - ).selector; - const ids = await selector.ids(); - useSideMenuNotebookSelectionStore.setState({ - selection: { - ...selectionStore.getState().selection, - ...ids.reduce((acc: any, id) => { - acc[id] = "deselected"; - return acc; - }, {}) - } - }); - setNestedNotebooksSelected(false); - } - - useEffect(() => { - const unsub = selectionStore.subscribe((state) => { - if (state.enabled) { - const selector = db.relations.from( - { - type: "notebook", - id: item.notebook.id - }, - "notebook" - ).selector; - selector.ids().then((ids) => { - if (!ids.length) return; - setNestedNotebooksSelected( - ids.length === 0 - ? true - : ids.every( - (id) => selectionStore.getState().selection[id] === "selected" - ) - ); - }); - } - }); - - return () => { - unsub(); - }; - }, [item.notebook.id, selectionStore]); - useEffect(() => { const onNotebookUpdate = (id?: string) => { if (id && id !== notebook.id) return; @@ -207,11 +114,6 @@ const NotebookItem = ({ testID={`notebook-item-${item.depth}-${index}`} onPress={async () => { if (selectionEnabled) { - if (selected && !nestedNotebooksSelected) { - console.log("Select all..."); - return selectAll(); - } - await deselectAll(); selectionStore .getState() .markAs(item.notebook, selected ? "deselected" : "selected"); @@ -288,13 +190,7 @@ const NotebookItem = ({ }} > @@ -394,8 +290,6 @@ export const SideMenuNotebooks = () => { [] ); - console.log("RENDERING ROOT"); - return ( ) => { } if (data?.item?.id && params.current.item?.id !== data?.item?.id) { - const nextRootNotebookId = await findRootNotebookId(data?.item?.id); - const currentNotebookRoot = await findRootNotebookId( + const nextRootNotebookId = await findSelectedParent(data?.item?.id); + const currentNotebookRoot = await findSelectedParent( params.current.item.id ); @@ -284,8 +284,8 @@ NotebookScreen.navigate = async (item: Notebook, canGoBack?: boolean) => { }); } else if (currentRoute === "Notebook") { if (!focusedRouteId) return; - const rootNotebookId = await findRootNotebookId(focusedRouteId); - const currentNotebookRoot = await findRootNotebookId(item?.id); + const rootNotebookId = await findSelectedParent(focusedRouteId); + const currentNotebookRoot = await findSelectedParent(item?.id); if ( (rootNotebookId === currentNotebookRoot && diff --git a/apps/mobile/app/stores/create-notebook-tree-stores.ts b/apps/mobile/app/stores/create-notebook-tree-stores.ts index 64bbeafe8..e813a4807 100644 --- a/apps/mobile/app/stores/create-notebook-tree-stores.ts +++ b/apps/mobile/app/stores/create-notebook-tree-stores.ts @@ -18,10 +18,10 @@ along with this program. If not, see . */ import { Notebook } from "@notesnook/core"; import create from "zustand"; -import { db } from "../common/database"; -import { createItemSelectionStore } from "./item-selection-store"; import { persist, StateStorage } from "zustand/middleware"; +import { db } from "../common/database"; import { MMKV } from "../common/database/mmkv"; +import { createItemSelectionStore } from "./item-selection-store"; export type TreeItem = { parentId: string; @@ -68,13 +68,18 @@ export function createNotebookTreeStores( setTree(tree) { set({ tree }); }, - updateItem: (id, notebook) => { - const newTree = [...get().tree]; + updateItem: async (id, notebook) => { + const newTree = get().tree.slice(); const index = newTree.findIndex((item) => item.notebook.id === id); + const childernCount = await db.relations + .from(notebook, "notebook") + .count(); newTree[index] = { ...newTree[index], - notebook + notebook, + hasChildren: childernCount > 0 }; + set({ tree: newTree }); diff --git a/apps/mobile/app/utils/functions.ts b/apps/mobile/app/utils/functions.ts index 09e971c36..e87dfecb1 100644 --- a/apps/mobile/app/utils/functions.ts +++ b/apps/mobile/app/utils/functions.ts @@ -32,8 +32,7 @@ import { useMenuStore } from "../stores/use-menu-store"; import { useNotebookStore } from "../stores/use-notebook-store"; import { useRelationStore } from "../stores/use-relation-store"; import { useTagStore } from "../stores/use-tag-store"; -import { eOnNotebookUpdated, eUpdateNoteInEditor } from "./events"; -import { getParentNotebookId } from "./notebooks"; +import { eUpdateNoteInEditor } from "./events"; export function getObfuscatedEmail(email: string) { if (!email) return ""; @@ -79,7 +78,6 @@ function confirmDeleteAllNotes( async function deleteNotebook(id: string, deleteNotes: boolean) { const notebook = await db.notebooks.notebook(id); if (!notebook) return; - const parentId = getParentNotebookId(id); if (deleteNotes) { const noteRelations = await db.relations.from(notebook, "note").get(); if (noteRelations?.length) { @@ -89,11 +87,6 @@ async function deleteNotebook(id: string, deleteNotes: boolean) { } } await db.notebooks.moveToTrash(id); - - eSendEvent(eOnNotebookUpdated, parentId); - if (!parentId) { - useNotebookStore.getState().refresh(); - } } export const deleteItems = async ( @@ -170,9 +163,7 @@ export const deleteItems = async ( useMenuStore.getState().setColorNotes(); ToastManager.hide(); if (type === "notebook") { - deletedIds.forEach(async (id) => { - eSendEvent(eOnNotebookUpdated, await getParentNotebookId(id)); - }); + useNotebookStore.getState().refresh(); } }, actionText: "Undo" @@ -187,11 +178,9 @@ export const deleteItems = async ( Navigation.queueRoutesForUpdate(); useMenuStore.getState().setColorNotes(); if (type === "notebook") { - itemIds.forEach(async (id) => { - eSendEvent(eOnNotebookUpdated, await getParentNotebookId(id)); - }); - useMenuStore.getState().setMenuPins(); + useNotebookStore.getState().refresh(); } + useMenuStore.getState().setMenuPins(); }; export const openLinkInBrowser = async (link: string) => { diff --git a/apps/mobile/app/utils/notebooks.ts b/apps/mobile/app/utils/notebooks.ts index d24186b97..8d582ab73 100644 --- a/apps/mobile/app/utils/notebooks.ts +++ b/apps/mobile/app/utils/notebooks.ts @@ -16,6 +16,7 @@ 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 { db } from "../common/database"; import { eSendEvent } from "../services/event-manager"; import { useNotebookStore } from "../stores/use-notebook-store"; @@ -38,6 +39,28 @@ export async function findRootNotebookId(id: string) { } } +export async function checkParentSelected( + id: string, + selectedNotebooks: Notebook[] +) { + const relation = await db.relations + .to( + { + id, + type: "notebook" + }, + "notebook" + ) + .get(); + if (!relation || !relation.length) { + return false; + } else { + if (selectedNotebooks.findIndex((n) => n.id === relation[0].fromId) > -1) + return true; + return checkParentSelected(relation[0].fromId, selectedNotebooks); + } +} + export async function getParentNotebookId(id: string) { const relation = await db.relations .to(