From ed81319d2c97ca4e93d12704498dc29ab53887ea Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Fri, 8 Dec 2023 10:13:43 +0500 Subject: [PATCH] mobile: use index based grouping --- .../attachments/attachment-item.tsx | 20 +- .../attachments/download-attachments.tsx | 10 +- .../app/components/attachments/index.tsx | 18 +- .../dialogs/jump-to-section/index.tsx | 134 +++++----- apps/mobile/app/components/list/index.tsx | 55 ++-- .../app/components/list/list-item.wrapper.tsx | 239 ++++++++++++------ .../mobile/app/components/properties/index.js | 16 +- .../app/components/sheets/add-to/index.tsx | 12 +- .../sheets/add-to/notebook-item.tsx | 37 ++- .../components/sheets/manage-tags/index.tsx | 31 ++- .../components/sheets/move-notes/movenote.tsx | 42 +-- .../sheets/notebook-sheet/index.tsx | 48 ++-- .../app/hooks/use-attachment-progress.ts | 7 +- apps/mobile/app/hooks/use-db-item.ts | 91 ++++--- apps/mobile/app/hooks/use-notebook.ts | 55 ++-- apps/mobile/app/stores/use-notes-store.ts | 12 +- apps/mobile/share/search.tsx | 65 ++--- 17 files changed, 504 insertions(+), 388 deletions(-) diff --git a/apps/mobile/app/components/attachments/attachment-item.tsx b/apps/mobile/app/components/attachments/attachment-item.tsx index 315df5b85..ae9388432 100644 --- a/apps/mobile/app/components/attachments/attachment-item.tsx +++ b/apps/mobile/app/components/attachments/attachment-item.tsx @@ -18,22 +18,22 @@ along with this program. If not, see . */ import { formatBytes } from "@notesnook/common"; -import React, { useEffect, useState } from "react"; +import { Attachment, VirtualizedGrouping } from "@notesnook/core"; +import { useThemeColors } from "@notesnook/theme"; +import React from "react"; import { TouchableOpacity, View } from "react-native"; import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import { db } from "../../common/database"; import { useAttachmentProgress } from "../../hooks/use-attachment-progress"; -import { useThemeColors } from "@notesnook/theme"; +import { useDBItem } from "../../hooks/use-db-item"; import { SIZE } from "../../utils/size"; import { IconButton } from "../ui/icon-button"; import { ProgressCircleComponent } from "../ui/svg/lazy"; import Paragraph from "../ui/typography/paragraph"; import Actions from "./actions"; -import { Attachment, VirtualizedGrouping } from "@notesnook/core"; -import { useDBItem } from "../../hooks/use-db-item"; function getFileExtension(filename: string) { - var ext = /^.+\.([^.]+)$/.exec(filename); + const ext = /^.+\.([^.]+)$/.exec(filename); return ext == null ? "" : ext[1]; } @@ -46,7 +46,7 @@ export const AttachmentItem = ({ hideWhenNotDownloading, context }: { - id: string; + id: string | number; attachments?: VirtualizedGrouping; encryption?: boolean; setAttachments: (attachments: any) => void; @@ -54,7 +54,7 @@ export const AttachmentItem = ({ hideWhenNotDownloading?: boolean; context?: string; }) => { - const [attachment] = useDBItem(id, "attachment", attachments?.item); + const [attachment] = useDBItem(id, "attachment", attachments); const { colors } = useThemeColors(); const [currentProgress, setCurrentProgress] = useAttachmentProgress( @@ -68,7 +68,7 @@ export const AttachmentItem = ({ }; return (hideWhenNotDownloading && - (!currentProgress || !(currentProgress as any).value)) || + (!currentProgress || !currentProgress.value)) || !attachment ? null : ( {formatBytes(attachment.size)}{" "} - {(currentProgress as any)?.type - ? "(" + (currentProgress as any).type + "ing - tap to cancel)" + {currentProgress?.type + ? "(" + currentProgress.type + "ing - tap to cancel)" : ""} ) : null} diff --git a/apps/mobile/app/components/attachments/download-attachments.tsx b/apps/mobile/app/components/attachments/download-attachments.tsx index b8b4321fb..2c2a5aa5d 100644 --- a/apps/mobile/app/components/attachments/download-attachments.tsx +++ b/apps/mobile/app/components/attachments/download-attachments.tsx @@ -91,7 +91,7 @@ const DownloadAttachments = ({ const successResults = () => { const results = []; - for (let [key, value] of result.entries()) { + for (const [key, value] of result.entries()) { if (value.status === 1) results.push(db.attachments.attachment(key)); } return results; @@ -99,7 +99,7 @@ const DownloadAttachments = ({ const failedResults = () => { const results = []; - for (let [key, value] of result.entries()) { + for (const [key, value] of result.entries()) { if (value.status === 0) results.push(db.attachments.attachment(key)); } return results; @@ -207,11 +207,11 @@ const DownloadAttachments = ({ } - keyExtractor={(item) => item as string} - renderItem={({ item }) => { + keyExtractor={(item, index) => "attachment" + index} + renderItem={({ index }) => { return ( {}} pressable={false} hideWhenNotDownloading={true} diff --git a/apps/mobile/app/components/attachments/index.tsx b/apps/mobile/app/components/attachments/index.tsx index 625e97f91..08bfc4db0 100644 --- a/apps/mobile/app/components/attachments/index.tsx +++ b/apps/mobile/app/components/attachments/index.tsx @@ -83,14 +83,14 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => { } clearTimeout(searchTimer.current); searchTimer.current = setTimeout(async () => { - let results = await db.lookup.attachments( - attachmentSearchValue.current as string - ); + const results = await db.lookup + .attachments(attachmentSearchValue.current as string) + .sorted(); setAttachments(results); }, 300); }; - const renderItem = ({ item }: { item: string }) => ( + const renderItem = ({ item }: { item: string | number }) => ( { const onCheck = async () => { if (!attachments) return; setLoading(true); - for (let id of attachments.ids) { - const attachment = await attachments.item(id as string); + for (const id of attachments.ids) { + const attachment = (await attachments.item(id))?.item; if (!attachment) continue; - let result = await filesystem.checkAttachment(attachment.hash); + const result = await filesystem.checkAttachment(attachment.hash); if (result.failed) { await db.attachments.markAsFailed(attachment.hash, result.failed); } else { - await db.attachments.markAsFailed(id as string, undefined); + await db.attachments.markAsFailed(attachment.id, undefined); } } refresh(); @@ -306,7 +306,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => { /> } estimatedItemSize={50} - data={attachments?.ids as string[]} + data={attachments?.ids} renderItem={renderItem} /> diff --git a/apps/mobile/app/components/dialogs/jump-to-section/index.tsx b/apps/mobile/app/components/dialogs/jump-to-section/index.tsx index 0c1a1fd47..ccbd05b5d 100644 --- a/apps/mobile/app/components/dialogs/jump-to-section/index.tsx +++ b/apps/mobile/app/components/dialogs/jump-to-section/index.tsx @@ -51,19 +51,18 @@ const JumpToSectionDialog = () => { const notes = data; const [visible, setVisible] = useState(false); const [currentIndex, setCurrentIndex] = useState(0); + const currentScrollPosition = useRef(0); + const [groups, setGroups] = useState< + { + index: number; + group: GroupHeader; + }[] + >(); const offsets = useRef([]); - const timeout = useRef(); - const onPress = (item: GroupHeader) => { - const index = notes?.ids?.findIndex((i) => { - if (typeof i === "object") { - return i.title === item.title && i.type === "header"; - } else { - false; - } - }); + const onPress = (item: { index: number; group: GroupHeader }) => { scrollRef.current?.current?.scrollToIndex({ - index: index as number, + index: item.index, animated: true }); close(); @@ -97,55 +96,44 @@ const JumpToSectionDialog = () => { }, [open]); const onScroll = (data: { x: number; y: number }) => { - const y = data.y; - if (timeout) { - clearTimeout(timeout.current); - timeout.current = undefined; - } - timeout.current = setTimeout(() => { - setCurrentIndex( - offsets.current?.findIndex( - (o, i) => o <= y && offsets.current[i + 1] > y - ) || 0 - ); - }, 200); + currentScrollPosition.current = data.y; }; const close = () => { setVisible(false); }; - const loadOffsets = useCallback(() => { - notes?.ids - .filter((i) => typeof i === "object" && i.type === "header") - .map((item, index) => { - if (typeof item === "string") return; - + const loadGroupsAndOffsets = useCallback(() => { + notes?.groups?.().then((groups) => { + setGroups(groups); + offsets.current = []; + groups.map((item, index) => { let offset = 35 * index; - let ind = notes.ids.findIndex( - (i) => - typeof i === "object" && - i.title === item.title && - i.type === "header" - ); + let groupIndex = item.index; const messageState = useMessageStore.getState().message; const msgOffset = messageState?.visible ? 60 : 10; - ind = ind + 1; - ind = ind - (index + 1); - offset = offset + ind * 100 + msgOffset; + groupIndex = groupIndex + 1; + groupIndex = groupIndex - (index + 1); + offset = offset + groupIndex * 100 + msgOffset; offsets.current.push(offset); }); - }, [notes]); - useEffect(() => { - loadOffsets(); - }, [loadOffsets, notes]); + const index = offsets.current?.findIndex((o, i) => { + return ( + o <= currentScrollPosition.current + 100 && + offsets.current[i + 1] - 100 > currentScrollPosition.current + ); + }); + + setCurrentIndex(index < 0 ? 0 : index); + }); + }, [notes]); return !visible ? null : ( { - loadOffsets(); + loadGroupsAndOffsets(); }} onRequestClose={close} visible={true} @@ -178,40 +166,38 @@ const JumpToSectionDialog = () => { paddingBottom: 20 }} > - {notes?.ids - .filter((i) => typeof i === "object" && i.type === "header") - .map((item, index) => { - return typeof item === "object" && item.title ? ( - onPress(item)} - type={currentIndex === index ? "selected" : "transparent"} - customStyle={{ - minWidth: "20%", - width: null, - paddingHorizontal: 12, - margin: 5, - borderRadius: 100, - height: 25, - marginVertical: 10 + {groups?.map((item, index) => { + return ( + onPress(item)} + type={currentIndex === index ? "selected" : "transparent"} + customStyle={{ + minWidth: "20%", + width: null, + paddingHorizontal: 12, + margin: 5, + borderRadius: 100, + height: 25, + marginVertical: 10 + }} + > + - - {item.title} - - - ) : null; - })} + {item.group.title} + + + ); + })} diff --git a/apps/mobile/app/components/list/index.tsx b/apps/mobile/app/components/list/index.tsx index 4e44caf56..3eb617e81 100644 --- a/apps/mobile/app/components/list/index.tsx +++ b/apps/mobile/app/components/list/index.tsx @@ -17,13 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { - GroupHeader, - GroupingKey, - Item, - VirtualizedGrouping, - isGroupHeader -} from "@notesnook/core"; +import { GroupingKey, Item, VirtualizedGrouping } from "@notesnook/core"; import { useThemeColors } from "@notesnook/theme"; import { FlashList } from "@shopify/flash-list"; import React, { useEffect, useRef } from "react"; @@ -39,11 +33,10 @@ import { eSendEvent } from "../../services/event-manager"; import Sync from "../../services/sync"; import { RouteName } from "../../stores/use-navigation-store"; import { useSettingStore } from "../../stores/use-setting-store"; -import { eOpenJumpToDialog, eScrollEvent } from "../../utils/events"; +import { eScrollEvent } from "../../utils/events"; import { tabBarRef } from "../../utils/global-refs"; import { Footer } from "../list-items/footer"; import { Header } from "../list-items/headers/header"; -import { SectionHeader } from "../list-items/headers/section-header"; import { Empty, PlaceholderData } from "./empty"; import { ListItemWrapper } from "./list-item.wrapper"; @@ -92,35 +85,20 @@ export default function List(props: ListProps) { }; const renderItem = React.useCallback( - ({ item, index }: { item: string | GroupHeader; index: number }) => { - if (isGroupHeader(item)) { - return ( - { - eSendEvent(eOpenJumpToDialog, { - ref: scrollRef, - data: props.data - }); - }} - /> - ); - } else { - return ( - - ); - } + ({ index }: { index: number }) => { + return ( + + ); }, [ groupOptions, @@ -181,7 +159,6 @@ export default function List(props: ListProps) { onMomentumScrollEnd={() => { tabBarRef.current?.unlock(); }} - getItemType={(item: any) => (isGroupHeader(item) ? "header" : "item")} estimatedItemSize={isCompactModeEnabled ? 60 : 100} directionalLockEnabled={true} keyboardShouldPersistTaps="always" diff --git a/apps/mobile/app/components/list/list-item.wrapper.tsx b/apps/mobile/app/components/list/list-item.wrapper.tsx index 4badb6a4f..3c0908285 100644 --- a/apps/mobile/app/components/list/list-item.wrapper.tsx +++ b/apps/mobile/app/components/list/list-item.wrapper.tsx @@ -16,24 +16,29 @@ 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 React from "react"; -import { getSortValue } from "@notesnook/core/dist/utils/grouping"; import { + Color, + GroupHeader, + GroupOptions, GroupingKey, Item, - VirtualizedGrouping, - Color, - Reminder, + ItemType, + Note, Notebook, + Reminder, Tag, TrashItem, - ItemType, - Note + VirtualizedGrouping } from "@notesnook/core"; -import { useEffect, useRef, useState } from "react"; +import { getSortValue } from "@notesnook/core/dist/utils/grouping"; +import React, { useEffect, useRef, useState } from "react"; import { View } from "react-native"; -import { NoteWrapper } from "../list-items/note/wrapper"; import { db } from "../../common/database"; +import { eSendEvent } from "../../services/event-manager"; +import { RouteName } from "../../stores/use-navigation-store"; +import { eOpenJumpToDialog } from "../../utils/events"; +import { SectionHeader } from "../list-items/headers/section-header"; +import { NoteWrapper } from "../list-items/note/wrapper"; import { NotebookWrapper } from "../list-items/notebook/wrapper"; import ReminderItem from "../list-items/reminder"; import TagItem from "../list-items/tag"; @@ -45,13 +50,17 @@ export type TagsWithDateEdited = WithDateEdited; type ListItemWrapperProps = { group?: GroupingKey; items: VirtualizedGrouping | undefined; - id: string; isSheet: boolean; index: number; + renderedInRoute?: RouteName; + customAccentColor?: string; + dataType: string; + scrollRef: any; + groupOptions: GroupOptions; }; export function ListItemWrapper(props: ListItemWrapperProps) { - const { id, items, group, isSheet, index } = props; + const { items, group, isSheet, index, groupOptions } = props; const [item, setItem] = useState(); const tags = useRef(); const notebooks = useRef(); @@ -59,25 +68,34 @@ export function ListItemWrapper(props: ListItemWrapperProps) { const color = useRef(); const totalNotes = useRef(0); const attachmentsCount = useRef(0); + const [groupHeader, setGroupHeader] = useState(); + const previousIndex = useRef(); useEffect(() => { (async function () { - const { item, data } = (await items?.item(id, resolveItems)) || {}; - if (!item) return; - if (item.type === "note" && isNoteResolvedData(data)) { - tags.current = data.tags; - notebooks.current = data.notebooks; - reminder.current = data.reminder; - color.current = data.color; - attachmentsCount.current = data.attachmentsCount; - } else if (item.type === "notebook" && typeof data === "number") { - totalNotes.current = data; - } else if (item.type === "tag" && typeof data === "number") { - totalNotes.current = data; + try { + const { item, data, group } = + (await items?.item(index, resolveItems)) || {}; + if (!item) return; + if (item.type === "note" && isNoteResolvedData(data)) { + tags.current = data.tags; + notebooks.current = data.notebooks; + reminder.current = data.reminder; + color.current = data.color; + attachmentsCount.current = data.attachmentsCount; + } else if (item.type === "notebook" && typeof data === "number") { + totalNotes.current = data; + } else if (item.type === "tag" && typeof data === "number") { + totalNotes.current = data; + } + previousIndex.current = index; + setItem(item); + setGroupHeader(group); + } catch (e) { + console.log("Error", e); } - setItem(item); })(); - }, [id, items]); + }, [index, items]); if (!item) return ; @@ -85,40 +103,117 @@ export function ListItemWrapper(props: ListItemWrapperProps) { switch (type) { case "note": { return ( - + <> + {groupHeader && previousIndex.current === index ? ( + { + eSendEvent(eOpenJumpToDialog, { + ref: props.scrollRef, + data: items + }); + }} + /> + ) : null} + + + ); } case "notebook": return ( - + <> + {groupHeader && previousIndex.current === index ? ( + { + eSendEvent(eOpenJumpToDialog, { + ref: props.scrollRef, + data: items + }); + }} + /> + ) : null} + + ); case "reminder": return ( - + <> + {groupHeader && previousIndex.current === index ? ( + { + eSendEvent(eOpenJumpToDialog, { + ref: props.scrollRef, + data: items + }); + }} + /> + ) : null} + + ); case "tag": return ( - + <> + {groupHeader && previousIndex.current === index ? ( + { + eSendEvent(eOpenJumpToDialog, { + ref: props.scrollRef, + data: items + }); + }} + /> + ) : null} + + ); default: return null; @@ -136,6 +231,19 @@ function withDateEdited< return { dateEdited: latestDateEdited, items }; } +export async function resolveItems(ids: string[], items: Item[]) { + const { type } = items[0]; + if (type === "note") return resolveNotes(ids); + else if (type === "notebook") { + return Promise.all(ids.map((id) => db.notebooks.totalNotes(id))); + } else if (type === "tag") { + return Promise.all( + ids.map((id) => db.relations.from({ id, type: "tag" }, "note").count()) + ); + } + return []; +} + function getDate(item: Item, groupType?: GroupingKey): number { return ( getSortValue( @@ -151,22 +259,6 @@ function getDate(item: Item, groupType?: GroupingKey): number { ); } -async function resolveItems(ids: string[], items: Record) { - const { type } = items[ids[0]]; - if (type === "note") return resolveNotes(ids); - else if (type === "notebook") { - const data: Record = {}; - for (const id of ids) data[id] = await db.notebooks.totalNotes(id); - return data; - } else if (type === "tag") { - const data: Record = {}; - for (const id of ids) - data[id] = await db.relations.from({ id, type: "tag" }, "note").count(); - return data; - } - return {}; -} - type NoteResolvedData = { notebooks?: NotebooksWithDateEdited; reminder?: Reminder; @@ -183,7 +275,6 @@ async function resolveNotes(ids: string[]) { ...(await db.relations.from({ type: "note", ids }, "reminder").get()) ]; console.timeEnd("relations"); - const relationIds: { notebooks: Set; colors: Set; @@ -207,15 +298,15 @@ async function resolveNotes(ids: string[]) { > = {}; for (const relation of relations) { const noteId = - relation.toType === "relation" ? relation.fromId : relation.toId; + relation.toType === "reminder" ? relation.fromId : relation.toId; const data = grouped[noteId] || { notebooks: [], tags: [] }; - if (relation.toType === "relation" && !data.reminder) { - data.reminder = relation.fromId; - relationIds.reminders.add(relation.fromId); + if (relation.toType === "reminder" && !data.reminder) { + data.reminder = relation.toId; + relationIds.reminders.add(relation.toId); } else if (relation.fromType === "notebook" && data.notebooks.length < 2) { data.notebooks.push(relation.fromId); relationIds.notebooks.add(relation.fromId); @@ -226,7 +317,7 @@ async function resolveNotes(ids: string[]) { data.color = relation.fromId; relationIds.colors.add(relation.fromId); } - grouped[relation.toId] = data; + grouped[noteId] = data; } console.time("resolve"); @@ -240,10 +331,10 @@ async function resolveNotes(ids: string[]) { }; console.timeEnd("resolve"); - const data: Record = {}; + const data: NoteResolvedData[] = []; for (const noteId in grouped) { const group = grouped[noteId]; - data[noteId] = { + data.push({ color: group.color ? resolved.colors[group.color] : undefined, reminder: group.reminder ? resolved.reminders[group.reminder] : undefined, tags: withDateEdited(group.tags.map((id) => resolved.tags[id])), @@ -252,12 +343,12 @@ async function resolveNotes(ids: string[]) { ), attachmentsCount: (await db.attachments?.ofNote(noteId, "all").ids())?.length || 0 - }; + }); } return data; } -function isNoteResolvedData(data: unknown): data is NoteResolvedData { +export function isNoteResolvedData(data: unknown): data is NoteResolvedData { return ( typeof data === "object" && !!data && diff --git a/apps/mobile/app/components/properties/index.js b/apps/mobile/app/components/properties/index.js index 4f7c32b21..82c560efd 100644 --- a/apps/mobile/app/components/properties/index.js +++ b/apps/mobile/app/components/properties/index.js @@ -120,13 +120,15 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => { {item.type === "note" ? : null} - - - + {item.type === "note" ? ( + + + + ) : null} . */ -import { GroupHeader, Note } from "@notesnook/core"; +import { Note } from "@notesnook/core"; import { useThemeColors } from "@notesnook/theme"; import React, { RefObject, useCallback, useEffect } from "react"; import { Keyboard, TouchableOpacity, View } from "react-native"; @@ -135,10 +135,9 @@ const MoveNoteSheet = ({ }; const renderNotebook = useCallback( - ({ item, index }: { item: string | GroupHeader; index: number }) => - (item as GroupHeader).type === "header" ? null : ( - - ), + ({ item, index }: { item: string | number; index: number }) => ( + + ), [notebooks] ); @@ -220,12 +219,11 @@ const MoveNoteSheet = ({ }} > typeof id === "string")} + data={notebooks?.ids} style={{ width: "100%" }} estimatedItemSize={50} - keyExtractor={(item) => item as string} renderItem={renderNotebook} ListEmptyComponent={ . */ import { Notebook, VirtualizedGrouping } from "@notesnook/core"; import { useThemeColors } from "@notesnook/theme"; -import React, { useMemo } from "react"; +import React, { useEffect } from "react"; import { View, useWindowDimensions } from "react-native"; import { notesnook } from "../../../../e2e/test.ids"; import { useTotalNotes } from "../../../hooks/use-db-item"; @@ -46,31 +46,39 @@ export const NotebookItem = ({ parent, items }: { - id: string; + id: string | number; currentLevel?: number; index: number; parent?: NotebookParentProp; items?: VirtualizedGrouping; }) => { - const { nestedNotebooks, notebook: item } = useNotebook(id, items); - const ids = useMemo(() => (id ? [id] : []), [id]); - const { totalNotes: totalNotes } = useTotalNotes(ids, "notebook"); + const expanded = useNotebookExpandedStore((state) => state.expanded[id]); + const { nestedNotebooks, notebook: item } = useNotebook(id, items, expanded); + const { totalNotes: totalNotes, getTotalNotes } = useTotalNotes("notebook"); const focusedRouteId = useNavigationStore((state) => state.focusedRouteId); const { colors } = useThemeColors("sheet"); const selection = useNotebookItemSelectionStore((state) => - id ? state.selection[id] : undefined + item?.id ? state.selection[item?.id] : undefined ); const isSelected = selection === "selected"; const isFocused = focusedRouteId === id; const { fontScale } = useWindowDimensions(); - const expanded = useNotebookExpandedStore((state) => state.expanded[id]); + + useEffect(() => { + if (item?.id) { + getTotalNotes([item?.id]); + } + }, [getTotalNotes, item?.id]); const onPress = () => { if (!item) return; const state = useNotebookItemSelectionStore.getState(); if (isSelected) { - state.markAs(item, !state.initialState[id] ? undefined : "deselected"); + state.markAs( + item, + !state.initialState[item?.id] ? undefined : "deselected" + ); return; } @@ -112,7 +120,7 @@ export const NotebookItem = ({ item, !isSelected ? "selected" - : !state.initialState[id] + : !state.initialState[item?.id] ? undefined : "deselected" ); @@ -141,7 +149,8 @@ export const NotebookItem = ({ size={SIZE.xl} color={isSelected ? colors.selected.icon : colors.primary.icon} onPress={() => { - useNotebookExpandedStore.getState().setExpanded(id); + if (!item?.id) return; + useNotebookExpandedStore.getState().setExpanded(item?.id); }} top={0} left={0} @@ -201,9 +210,9 @@ export const NotebookItem = ({ alignItems: "center" }} > - {totalNotes?.(id) ? ( + {item?.id && totalNotes?.(item?.id) ? ( - {totalNotes(id)} + {totalNotes(item?.id)} ) : null} ( { - console.log("items loaded tags"); + console.log("items loaded tags", items.ids); setTags(items); }); } @@ -237,11 +237,10 @@ const ManageTagsSheet = (props: { ); const renderTag = useCallback( - ({ item }: { item: string; index: number }) => ( + ({ item, index }: { item: string | number; index: number }) => ( } - id={item as string} + id={index} onPress={onPress} /> ), @@ -303,13 +302,13 @@ const ManageTagsSheet = (props: { ) : null} typeof id === "string") as string[]} + data={tags?.ids} style={{ width: "100%" }} keyboardShouldPersistTaps keyboardDismissMode="interactive" - keyExtractor={(item) => item as string} + keyExtractor={(item) => item + "_tag"} renderItem={renderTag} ListEmptyComponent={ ; onPress: (id: string) => void; }) => { const { colors } = useThemeColors(); const [tag] = useDBItem(id, "tag", tags); - const selection = useTagItemSelection((state) => state.selection[id]); + const selection = useTagItemSelection((state) => + tag?.id ? state.selection[tag?.id] : false + ); - return ( + return !tag ? null : ( onPress(id)} + onPress={() => { + if (!tag) return; + onPress(tag.id); + }} type="gray" > {!tag ? null : ( onPress(id)} + onPress={() => { + if (!tag) return; + onPress(tag.id); + }} color={ selection === "selected" || selection === "intermediate" ? colors.selected.icon @@ -406,7 +414,6 @@ const TagItem = ({ style={{ width: 200, height: 30, - // backgroundColor: colors.secondary.background, borderRadius: 5 }} /> diff --git a/apps/mobile/app/components/sheets/move-notes/movenote.tsx b/apps/mobile/app/components/sheets/move-notes/movenote.tsx index 9936328af..9ce5d8d51 100644 --- a/apps/mobile/app/components/sheets/move-notes/movenote.tsx +++ b/apps/mobile/app/components/sheets/move-notes/movenote.tsx @@ -36,6 +36,7 @@ import { IconButton } from "../../ui/icon-button"; import { PressableButton } from "../../ui/pressable"; import Seperator from "../../ui/seperator"; import Paragraph from "../../ui/typography/paragraph"; +import { useDBItem } from "../../../hooks/use-db-item"; export const MoveNotes = ({ notebook, @@ -45,11 +46,13 @@ export const MoveNotes = ({ fwdRef: RefObject; }) => { const { colors } = useThemeColors(); - const [currentNotebook, setCurrentNotebook] = useState(notebook); + const currentNotebook = notebook; + const { height } = useWindowDimensions(); const [selectedNoteIds, setSelectedNoteIds] = useState([]); const [notes, setNotes] = useState>(); const [existingNoteIds, setExistingNoteIds] = useState([]); + useEffect(() => { db.notes?.all.sorted(db.settings.getGroupOptions("notes")).then((notes) => { setNotes(notes); @@ -85,17 +88,18 @@ export const MoveNotes = ({ ); const renderItem = React.useCallback( - ({ item }: { item: string }) => { + ({ index }: { item: string | number; index: number }) => { return ( -1} + selected={(id) => selectedNoteIds?.indexOf(id) > -1} + exists={(id) => existingNoteIds.indexOf(id) > -1} /> ); }, - [notes, select, selectedNoteIds] + [existingNoteIds, notes, select, selectedNoteIds] ); return ( @@ -128,9 +132,7 @@ export const MoveNotes = ({ } - data={(notes?.ids as string[])?.filter( - (id) => existingNoteIds?.indexOf(id) === -1 - )} + data={notes?.ids} renderItem={renderItem} /> {selectedNoteIds.length > 0 ? ( @@ -157,21 +159,19 @@ const SelectableNoteItem = ({ id, items, select, - selected + selected, + exists }: { - id: string; + id: string | number; items?: VirtualizedGrouping; select: (id: string) => void; - selected?: boolean; + selected?: (id: string) => boolean; + exists: (id: string) => boolean; }) => { const { colors } = useThemeColors(); - const [item, setItem] = useState(); + const [item] = useDBItem(id, "note", items); - useEffect(() => { - items?.item(id).then((item) => setItem(item)); - }, [id, items]); - - return !item ? null : ( + return !item || exists(item.id) ? null : ( { @@ -197,10 +197,14 @@ const SelectableNoteItem = ({ select(item?.id); }} name={ - selected ? "check-circle-outline" : "checkbox-blank-circle-outline" + selected?.(item?.id) + ? "check-circle-outline" + : "checkbox-blank-circle-outline" } type="selected" - color={selected ? colors.selected.icon : colors.primary.icon} + color={ + selected?.(item?.id) ? colors.selected.icon : colors.primary.icon + } /> { nestedNotebooks: notebooks, nestedNotebookNotesCount: totalNotes, groupOptions - } = useNotebook(currentRoute === "Notebook" ? root : undefined); + } = useNotebook( + currentRoute === "Notebook" ? root : undefined, + undefined, + true + ); const PLACEHOLDER_DATA = { heading: "Notebooks", @@ -140,17 +144,16 @@ export const NotebookSheet = () => { item, index }: { - item: string | GroupHeader; + item: string | number; index: number; - }) => - (item as GroupHeader).type === "header" ? null : ( - - ); + }) => ( + + ); const selectionContext = { selection: selection, @@ -396,7 +399,6 @@ export const NotebookSheet = () => { progressBackgroundColor={colors.primary.background} /> } - keyExtractor={(item) => item as string} renderItem={renderNotebook} ListEmptyComponent={ number; currentLevel?: number; index: number; @@ -437,7 +439,7 @@ const NotebookItem = ({ nestedNotebookNotesCount, nestedNotebooks, notebook: item - } = useNotebook(id, items); + } = useNotebook(id, items, true); const isFocused = useNavigationStore((state) => state.focusedRouteId === id); const { colors } = useThemeColors("sheet"); const selection = useSelection(); @@ -445,7 +447,9 @@ const NotebookItem = ({ selection.selection.findIndex((selected) => selected.id === item?.id) > -1; const { fontScale } = useWindowDimensions(); - const expanded = useNotebookExpandedStore((state) => state.expanded[id]); + const expanded = useNotebookExpandedStore((state) => + item?.id ? state.expanded[item?.id] : undefined + ); return ( { - useNotebookExpandedStore.getState().setExpanded(id); + if (!item?.id) return; + useNotebookExpandedStore.getState().setExpanded(item?.id); }} top={0} left={0} @@ -553,9 +558,9 @@ const NotebookItem = ({ alignItems: "center" }} > - {totalNotes?.(id) ? ( + {item?.id && totalNotes?.(item?.id) ? ( - {totalNotes(id)} + {totalNotes(item?.id)} ) : null} ( + : item && + nestedNotebooks?.ids.map((id, index) => ( . import { useEffect, useState } from "react"; import { useAttachmentStore } from "../stores/use-attachment-store"; +import { Attachment } from "@notesnook/core"; type AttachmentProgress = { type: string; @@ -27,7 +28,7 @@ type AttachmentProgress = { }; export const useAttachmentProgress = ( - attachment: any, + attachment?: Attachment, encryption?: boolean ): [ AttachmentProgress | undefined, @@ -45,7 +46,9 @@ export const useAttachmentProgress = ( ); useEffect(() => { - const attachmentProgress = progress?.[attachment?.metadata?.hash]; + const attachmentProgress = !attachment + ? null + : progress?.[attachment?.hash]; if (attachmentProgress) { const type = attachmentProgress.type; const loaded = diff --git a/apps/mobile/app/hooks/use-db-item.ts b/apps/mobile/app/hooks/use-db-item.ts index 7f722cf4f..29369c581 100644 --- a/apps/mobile/app/hooks/use-db-item.ts +++ b/apps/mobile/app/hooks/use-db-item.ts @@ -26,7 +26,7 @@ import { Tag, VirtualizedGrouping } from "@notesnook/core"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { db } from "../common/database"; import { eSendEvent, @@ -45,22 +45,36 @@ type ItemTypeKey = { shortcut: Shortcut; }; +function isValidIdOrIndex(idOrIndex?: string | number) { + return typeof idOrIndex === "number" || typeof idOrIndex === "string"; +} + export const useDBItem = ( - id?: string, + idOrIndex?: string | number, type?: T, items?: VirtualizedGrouping ): [ItemTypeKey[T] | undefined, () => void] => { const [item, setItem] = useState(); + const itemIdRef = useRef(); + const prevIdOrIndexRef = useRef(); + + if (prevIdOrIndexRef.current !== idOrIndex) { + itemIdRef.current = undefined; + prevIdOrIndexRef.current = idOrIndex; + } useEffect(() => { const onUpdateItem = (itemId?: string) => { - if (typeof itemId === "string" && itemId !== id) return; - if (!id) return; - console.log("useDBItem.onUpdateItem", id, type); + if (typeof itemId === "string" && itemId !== itemIdRef.current) return; - if (items) { - items.item(id).then((item) => { - setItem(item); + if (!isValidIdOrIndex(idOrIndex)) return; + + console.log("useDBItem.onUpdateItem", idOrIndex, type); + + if (items && typeof idOrIndex === "number") { + items.item(idOrIndex).then((item) => { + setItem(item.item); + itemIdRef.current = item.item.id; }); } else { if (!(db as any)[type + "s"][type]) { @@ -69,9 +83,14 @@ export const useDBItem = ( `db.${type}s.${type}(id: string)` ); } else { + console.log("get notebook"); + (db as any)[type + "s"] - ?.[type]?.(id) - .then((item: ItemTypeKey[T]) => setItem(item)); + ?.[type]?.(idOrIndex as string) + .then((item: ItemTypeKey[T]) => { + setItem(item); + itemIdRef.current = item.id; + }); } } }; @@ -80,46 +99,42 @@ export const useDBItem = ( return () => { eUnSubscribeEvent(eDBItemUpdate, onUpdateItem); }; - }, [id, type, items]); + }, [idOrIndex, type, items]); return [ - id ? (item as ItemTypeKey[T]) : undefined, + isValidIdOrIndex(idOrIndex) ? (item as ItemTypeKey[T]) : undefined, () => { - if (id) { - eSendEvent(eDBItemUpdate, id); + if (idOrIndex) { + eSendEvent(eDBItemUpdate, itemIdRef.current || idOrIndex); } } ]; }; -export const useTotalNotes = ( - ids: string[], - type: "notebook" | "tag" | "color" -) => { +export const useTotalNotes = (type: "notebook" | "tag" | "color") => { const [totalNotesById, setTotalNotesById] = useState<{ [id: string]: number; }>({}); - const getTotalNotes = React.useCallback(() => { - if (!ids || !ids.length || !type) return; - db.relations - .from({ type: "notebook", ids: ids as string[] }, ["notebook", "note"]) - .get() - .then((relations) => { - const totalNotesById: any = {}; - for (const id of ids) { - totalNotesById[id] = relations.filter( - (relation) => relation.fromId === id && relation.toType === "note" - )?.length; - } - setTotalNotesById(totalNotesById); - }); - console.log("useTotalNotes.getTotalNotes"); - }, [ids, type]); - - useEffect(() => { - getTotalNotes(); - }, [ids, type, getTotalNotes]); + const getTotalNotes = React.useCallback( + (ids: string[]) => { + if (!ids || !ids.length || !type) return; + db.relations + .from({ type: type, ids: ids as string[] }, ["note"]) + .get() + .then((relations) => { + const totalNotesById: any = {}; + for (const id of ids) { + totalNotesById[id] = relations.filter( + (relation) => relation.fromId === id && relation.toType === "note" + )?.length; + } + setTotalNotesById(totalNotesById); + }); + console.log("useTotalNotes.getTotalNotes"); + }, + [type] + ); return { totalNotes: (id: string) => { diff --git a/apps/mobile/app/hooks/use-notebook.ts b/apps/mobile/app/hooks/use-notebook.ts index 4cb6fca8e..de5e7c211 100644 --- a/apps/mobile/app/hooks/use-notebook.ts +++ b/apps/mobile/app/hooks/use-notebook.ts @@ -24,40 +24,47 @@ import { eGroupOptionsUpdated, eOnNotebookUpdated } from "../utils/events"; import { useDBItem, useTotalNotes } from "./use-db-item"; export const useNotebook = ( - id?: string, - items?: VirtualizedGrouping + id?: string | number, + items?: VirtualizedGrouping, + nestedNotebooks?: boolean ) => { const [item, refresh] = useDBItem(id, "notebook", items); const [groupOptions, setGroupOptions] = useState( db.settings.getGroupOptions("notebooks") ); const [notebooks, setNotebooks] = useState>(); - const { totalNotes: nestedNotebookNotesCount } = useTotalNotes( - notebooks?.ids as string[], - "notebook" - ); + const { totalNotes: nestedNotebookNotesCount, getTotalNotes } = + useTotalNotes("notebook"); const onRequestUpdate = React.useCallback(() => { - if (!id) return; - console.log("useNotebook.onRequestUpdate", id, Date.now()); - db.relations - .from( - { - type: "notebook", - id: id - }, - "notebook" - ) - .selector.sorted(db.settings.getGroupOptions("notebooks")) + if (!item?.id) return; + console.log("useNotebook.onRequestUpdate", item?.id, Date.now()); + + const selector = db.relations.from( + { + type: "notebook", + id: item.id + }, + "notebook" + ).selector; + + selector.ids().then((notebookIds) => { + getTotalNotes(notebookIds); + }); + + selector + .sorted(db.settings.getGroupOptions("notebooks")) .then((notebooks) => { setNotebooks(notebooks); }); - }, [id]); + }, [getTotalNotes, item?.id]); useEffect(() => { - console.log("useNotebook.useEffect.onRequestUpdate"); - onRequestUpdate(); - }, [onRequestUpdate]); + if (nestedNotebooks) { + console.log("useNotebook.useEffect.onRequestUpdate"); + onRequestUpdate(); + } + }, [item?.id, onRequestUpdate, nestedNotebooks]); const onUpdate = useCallback( (type: string) => { @@ -73,7 +80,9 @@ export const useNotebook = ( const onNotebookUpdate = (id?: string) => { if (typeof id === "string" && id !== id) return; setImmediate(() => { - onRequestUpdate(); + if (nestedNotebooks) { + onRequestUpdate(); + } refresh(); }); }; @@ -84,7 +93,7 @@ export const useNotebook = ( eUnSubscribeEvent(eGroupOptionsUpdated, onUpdate); eUnSubscribeEvent(eOnNotebookUpdated, onNotebookUpdate); }; - }, [onUpdate, onRequestUpdate, id, refresh]); + }, [onUpdate, onRequestUpdate, id, refresh, nestedNotebooks]); return { notebook: item, diff --git a/apps/mobile/app/stores/use-notes-store.ts b/apps/mobile/app/stores/use-notes-store.ts index ee22206f1..587278d83 100644 --- a/apps/mobile/app/stores/use-notes-store.ts +++ b/apps/mobile/app/stores/use-notes-store.ts @@ -33,11 +33,13 @@ export const useNoteStore = create((set) => ({ notes: undefined, loading: true, setLoading: (loading) => set({ loading: loading }), - setNotes: () => { - db.notes.all.grouped(db.settings.getGroupOptions("home")).then((notes) => { - set({ - notes: notes - }); + setNotes: async () => { + const notes = await db.notes.all.grouped( + db.settings.getGroupOptions("home") + ); + await notes.item(0); + set({ + notes: notes }); }, clearNotes: () => set({ notes: undefined }) diff --git a/apps/mobile/share/search.tsx b/apps/mobile/share/search.tsx index 3c0c018fb..5eb15fb4b 100644 --- a/apps/mobile/share/search.tsx +++ b/apps/mobile/share/search.tsx @@ -17,20 +17,24 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { Item, Note, Notebook, VirtualizedGrouping } from "@notesnook/core"; +import { + Item, + Note, + Notebook, + Tag, + VirtualizedGrouping +} from "@notesnook/core"; import { useThemeColors } from "@notesnook/theme"; import React, { useEffect, useRef, useState } from "react"; import { FlatList, Platform, - StatusBar, Text, TextInput, TouchableOpacity, View, useWindowDimensions } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import create from "zustand"; import { db } from "../app/common/database"; @@ -121,23 +125,29 @@ const NotebookItem = ({ items, level = 0 }: { - id: string; + id: string | number; mode: SearchMode; close?: () => void; level?: number; items?: VirtualizedGrouping; }) => { - const isExpanded = useNotebookExpandedStore((state) => state.expanded[id]); const { nestedNotebooks, notebook } = useNotebook(id, items); + const isExpanded = useNotebookExpandedStore((state) => + !notebook ? false : state.expanded[notebook.id] + ); const { colors } = useThemeColors(); - const isSelected = useShareStore( - (state) => - state.selectedNotebooks.findIndex((selectedId) => id === selectedId) > -1 + const isSelected = useShareStore((state) => + !notebook + ? false + : state.selectedNotebooks.findIndex( + (selectedId) => notebook?.id === selectedId + ) > -1 ); const set = SearchSetters[mode]; const onSelectItem = async () => { - set(id); + if (!notebook) return; + set(notebook.id); }; return !notebook ? ( @@ -172,7 +182,8 @@ const NotebookItem = ({ alignItems: "center" }} onPress={() => { - useNotebookExpandedStore.getState().setExpanded(id); + if (!notebook) return; + useNotebookExpandedStore.getState().setExpanded(notebook.id); }} activeOpacity={1} > @@ -230,10 +241,10 @@ const NotebookItem = ({ marginTop: 5 }} > - {(nestedNotebooks.ids as string[]).map((id) => ( + {nestedNotebooks.ids.map((item, index) => ( void; items?: VirtualizedGrouping; @@ -259,21 +270,23 @@ const ListItem = ({ const [item] = useDBItem( id, mode === "appendNote" ? "note" : "tag", - items as any + items as VirtualizedGrouping ); const { colors } = useThemeColors(); const isSelected = useShareStore((state) => - mode === "appendNote" ? false : state.selectedTags.indexOf(id as never) > -1 + mode === "appendNote" || !item + ? false + : state.selectedTags.indexOf(item?.id as never) > -1 ); const set = SearchSetters[mode]; const onSelectItem = async () => { - if ((item as Note)?.locked) { + if ((item as Note)?.locked || !item) { return; } - set(id); + set(item.id); }; return !item ? null : ( @@ -356,12 +369,6 @@ export const Search = ({ .then((exists) => setQueryExists(!!exists)); }; - const insets = - Platform.OS === "android" - ? { top: StatusBar.currentHeight } - : // eslint-disable-next-line react-hooks/rules-of-hooks - useSafeAreaInsets(); - const get = SearchGetters[mode]; const lookup = SearchLookup[mode]; @@ -389,16 +396,16 @@ export const Search = ({ }, [get]); const renderItem = React.useCallback( - ({ item }: { item: string }) => + ({ index }: { item: string | number; index: number }) => mode === "selectNotebooks" ? ( } /> ) : ( - + ), [close, mode, items] ); @@ -502,7 +509,7 @@ export const Search = ({ ) : null}