global: move item resolution logic to @notesnook/common

This commit is contained in:
Abdullah Atta
2024-02-05 12:13:42 +05:00
parent d3221abd7e
commit ef2d8bf5af
27 changed files with 188 additions and 288 deletions

View File

@@ -45,10 +45,7 @@ import { ReminderTime } from "../../ui/reminder-time";
import { TimeSince } from "../../ui/time-since"; import { TimeSince } from "../../ui/time-since";
import Heading from "../../ui/typography/heading"; import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
import { import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
NotebooksWithDateEdited,
TagsWithDateEdited
} from "../../../stores/resolve-items";
type NoteItemProps = { type NoteItemProps = {
item: Note | BaseTrashItem<Note>; item: Note | BaseTrashItem<Note>;

View File

@@ -36,10 +36,7 @@ import { tabBarRef } from "../../../utils/global-refs";
import NotePreview from "../../note-history/preview"; import NotePreview from "../../note-history/preview";
import SelectionWrapper from "../selection-wrapper"; import SelectionWrapper from "../selection-wrapper";
import { import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
NotebooksWithDateEdited,
TagsWithDateEdited
} from "../../../stores/resolve-items";
export const openNote = async ( export const openNote = async (
item: Note, item: Note,

View File

@@ -35,12 +35,6 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native"; import { View } from "react-native";
import { db } from "../../common/database"; import { db } from "../../common/database";
import { eSendEvent } from "../../services/event-manager"; import { eSendEvent } from "../../services/event-manager";
import {
NotebooksWithDateEdited,
TagsWithDateEdited,
isNoteResolvedData,
resolveItems
} from "../../stores/resolve-items";
import { RouteName } from "../../stores/use-navigation-store"; import { RouteName } from "../../stores/use-navigation-store";
import { eOpenJumpToDialog } from "../../utils/events"; import { eOpenJumpToDialog } from "../../utils/events";
import { SectionHeader } from "../list-items/headers/section-header"; import { SectionHeader } from "../list-items/headers/section-header";
@@ -48,6 +42,12 @@ import { NoteWrapper } from "../list-items/note/wrapper";
import { NotebookWrapper } from "../list-items/notebook/wrapper"; import { NotebookWrapper } from "../list-items/notebook/wrapper";
import ReminderItem from "../list-items/reminder"; import ReminderItem from "../list-items/reminder";
import TagItem from "../list-items/tag"; import TagItem from "../list-items/tag";
import {
NotebooksWithDateEdited,
TagsWithDateEdited,
isNoteResolvedData,
resolveItems
} from "@notesnook/common";
type ListItemWrapperProps<TItem = Item> = { type ListItemWrapperProps<TItem = Item> = {
group?: GroupingKey; group?: GroupingKey;
@@ -92,7 +92,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
notebooks.current = data.notebooks; notebooks.current = data.notebooks;
reminder.current = data.reminder; reminder.current = data.reminder;
color.current = data.color; color.current = data.color;
attachmentsCount.current = data.attachmentsCount || 0; attachmentsCount.current = data.attachments?.total || 0;
} else if ( } else if (
resolvedItem.item.type === "notebook" && resolvedItem.item.type === "notebook" &&
typeof data === "number" typeof data === "number"

View File

@@ -44,7 +44,7 @@ import { View } from "react-native";
import { SIZE } from "../../utils/size"; import { SIZE } from "../../utils/size";
import { IconButton } from "../../components/ui/icon-button"; import { IconButton } from "../../components/ui/icon-button";
import { PressableButton } from "../../components/ui/pressable"; import { PressableButton } from "../../components/ui/pressable";
import { resolveItems } from "../../stores/resolve-items"; import { resolveItems } from "@notesnook/common";
const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>(); const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();

View File

@@ -31,7 +31,7 @@ import {
eUnSubscribeEvent eUnSubscribeEvent
} from "../../services/event-manager"; } from "../../services/event-manager";
import Navigation, { NavigationProps } from "../../services/navigation"; import Navigation, { NavigationProps } from "../../services/navigation";
import { resolveItems } from "../../stores/resolve-items"; import { resolveItems } from "@notesnook/common";
import useNavigationStore, { import useNavigationStore, {
HeaderRightButton, HeaderRightButton,
NotesScreenParams, NotesScreenParams,

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Item, VirtualizedGrouping } from "@notesnook/core"; import { Item, VirtualizedGrouping } from "@notesnook/core";
import create, { State, StoreApi, UseBoundStore } from "zustand"; import create, { State, StoreApi, UseBoundStore } from "zustand";
import { resolveItems } from "./resolve-items"; import { resolveItems } from "@notesnook/common";
import { useSettingStore } from "./use-setting-store"; import { useSettingStore } from "./use-setting-store";
export interface DBCollectionStore<Type extends Item> extends State { export interface DBCollectionStore<Type extends Item> extends State {

View File

@@ -30,7 +30,7 @@ import { useMenuTrigger } from "../../hooks/use-menu";
import { MenuItem } from "@notesnook/ui"; import { MenuItem } from "@notesnook/ui";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { Tag } from "@notesnook/core"; import { Tag } from "@notesnook/core";
import usePromise from "../../hooks/use-promise"; import { usePromise } from "@notesnook/common";
type HeaderProps = { readonly: boolean }; type HeaderProps = { readonly: boolean };
function Header(props: HeaderProps) { function Header(props: HeaderProps) {

View File

@@ -59,6 +59,7 @@ export function FilteredList<T>(props: FilteredListProps<T>) {
inputRef={inputRef} inputRef={inputRef}
data-test-id={"filter-input"} data-test-id={"filter-input"}
autoFocus autoFocus
sx={{ m: 0 }}
placeholder={ placeholder={
items.length <= 0 ? placeholders.empty : placeholders.filter items.length <= 0 ? placeholders.empty : placeholders.filter
} }
@@ -84,7 +85,9 @@ export function FilteredList<T>(props: FilteredListProps<T>) {
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
py: 2 py: 2,
width: "100%",
mt: 1
}} }}
onClick={async () => { onClick={async () => {
await _createNewItem(query); await _createNewItem(query);

View File

@@ -31,13 +31,7 @@ import { ListLoader } from "../loaders/list-loader";
import ScrollContainer from "../scroll-container"; import ScrollContainer from "../scroll-container";
import { useKeyboardListNavigation } from "../../hooks/use-keyboard-list-navigation"; import { useKeyboardListNavigation } from "../../hooks/use-keyboard-list-navigation";
import { Context } from "./types"; import { Context } from "./types";
import { import { VirtualizedGrouping, GroupingKey, Item } from "@notesnook/core";
VirtualizedGrouping,
GroupingKey,
Item,
isGroupHeader
} from "@notesnook/core";
import { useResolvedItem } from "./resolved-item";
import { import {
ItemProps, ItemProps,
ScrollerProps, ScrollerProps,
@@ -45,6 +39,7 @@ import {
VirtuosoHandle VirtuosoHandle
} from "react-virtuoso"; } from "react-virtuoso";
import Skeleton from "react-loading-skeleton"; import Skeleton from "react-loading-skeleton";
import { useResolvedItem } from "@notesnook/common";
export const CustomScrollbarsVirtualList = forwardRef< export const CustomScrollbarsVirtualList = forwardRef<
HTMLDivElement, HTMLDivElement,

View File

@@ -26,8 +26,8 @@ import Reminder from "../reminder";
import { Context } from "./types"; import { Context } from "./types";
import { getSortValue } from "@notesnook/core/dist/utils/grouping"; import { getSortValue } from "@notesnook/core/dist/utils/grouping";
import { GroupingKey, Item } from "@notesnook/core"; import { GroupingKey, Item } from "@notesnook/core";
import { isNoteResolvedData } from "@notesnook/common";
import { Attachment } from "../attachment"; import { Attachment } from "../attachment";
import { isNoteResolvedData } from "./resolved-item";
const SINGLE_LINE_HEIGHT = 1.4; const SINGLE_LINE_HEIGHT = 1.4;
const DEFAULT_LINE_HEIGHT = const DEFAULT_LINE_HEIGHT =

View File

@@ -1,202 +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 <http://www.gnu.org/licenses/>.
*/
import {
Color,
Item,
ItemMap,
ItemType,
Reminder,
VirtualizedGrouping
} from "@notesnook/core";
import usePromise from "../../hooks/use-promise";
import {
NotebooksWithDateEdited,
TagsWithDateEdited,
WithDateEdited
} from "./types";
import { db } from "../../common/db";
import React from "react";
type ResolvedItemProps<TItemType extends ItemType> = {
type: TItemType;
items: VirtualizedGrouping<ItemMap[TItemType]>;
index: number;
children: (item: {
item: ItemMap[TItemType];
data: unknown;
}) => React.ReactNode;
};
export function ResolvedItem<TItemType extends ItemType>(
props: ResolvedItemProps<TItemType>
) {
const { index, items, children, type } = props;
const result = usePromise(
() => items.item(index, resolveItems),
[index, items]
);
if (result.status === "rejected" || !result.value) return null;
if (result.value.item.type !== type) return null;
return <>{children(result.value)}</>;
}
export function useResolvedItem(
props: Omit<ResolvedItemProps<ItemType>, "children" | "type">
) {
const { index, items } = props;
const result = usePromise(
() => items.item(index, resolveItems),
[index, items]
);
if (result.status === "rejected" || !result.value) return null;
return result.value;
}
function withDateEdited<
T extends { dateEdited: number } | { dateModified: number }
>(items: T[]): WithDateEdited<T> {
let latestDateEdited = 0;
items.forEach((item) => {
const date = "dateEdited" in item ? item.dateEdited : item.dateModified;
if (latestDateEdited < date) latestDateEdited = date;
});
return { dateEdited: latestDateEdited, items };
}
export async function resolveItems(ids: string[], items: Item[]) {
if (!ids.length || !items.length) return [];
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 [];
}
type NoteResolvedData = {
notebooks?: NotebooksWithDateEdited;
reminder?: Reminder;
color?: Color;
tags?: TagsWithDateEdited;
};
async function resolveNotes(ids: string[]) {
console.time("relations");
const relations = [
...(await db.relations
.to({ type: "note", ids }, ["notebook", "tag", "color"])
.get()),
...(await db.relations.from({ type: "note", ids }, "reminder").get())
];
console.timeEnd("relations");
const relationIds: {
notebooks: Set<string>;
colors: Set<string>;
tags: Set<string>;
reminders: Set<string>;
} = {
colors: new Set(),
notebooks: new Set(),
tags: new Set(),
reminders: new Set()
};
const grouped: Record<
string,
{
notebooks: string[];
color?: string;
tags: string[];
reminder?: string;
}
> = {};
for (const relation of relations) {
const noteId =
relation.toType === "reminder" ? relation.fromId : relation.toId;
const data = grouped[noteId] || {
notebooks: [],
tags: []
};
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);
} else if (relation.fromType === "tag" && data.tags.length < 3) {
data.tags.push(relation.fromId);
relationIds.tags.add(relation.fromId);
} else if (relation.fromType === "color" && !data.color) {
data.color = relation.fromId;
relationIds.colors.add(relation.fromId);
}
grouped[noteId] = data;
}
console.time("resolve");
const resolved = {
notebooks: await db.notebooks.all.records(
Array.from(relationIds.notebooks)
),
tags: await db.tags.all.records(Array.from(relationIds.tags)),
colors: await db.colors.all.records(Array.from(relationIds.colors)),
reminders: await db.reminders.all.records(Array.from(relationIds.reminders))
};
console.timeEnd("resolve");
const data: NoteResolvedData[] = [];
for (const noteId of ids) {
const group = grouped[noteId];
if (!group) {
data.push({});
continue;
}
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]).filter(Boolean)
),
notebooks: withDateEdited(
group.notebooks.map((id) => resolved.notebooks[id]).filter(Boolean)
)
});
}
return data;
}
export function isNoteResolvedData(data: unknown): data is NoteResolvedData {
return (
typeof data === "object" &&
!!data &&
"notebooks" in data &&
"reminder" in data &&
"color" in data &&
"tags" in data
);
}

View File

@@ -76,9 +76,12 @@ import {
isReminderActive, isReminderActive,
isReminderToday isReminderToday
} from "@notesnook/core/dist/collections/reminders"; } from "@notesnook/core/dist/collections/reminders";
import { getFormattedReminderTime, pluralize } from "@notesnook/common";
import { import {
Reminder as ReminderType, NoteResolvedData,
getFormattedReminderTime,
pluralize
} from "@notesnook/common";
import {
Color, Color,
Note, Note,
Notebook as NotebookItem, Notebook as NotebookItem,
@@ -86,22 +89,14 @@ import {
DefaultColors DefaultColors
} from "@notesnook/core"; } from "@notesnook/core";
import { MenuItem } from "@notesnook/ui"; import { MenuItem } from "@notesnook/ui";
import { import { Context } from "../list-container/types";
Context,
NotebooksWithDateEdited,
TagsWithDateEdited
} from "../list-container/types";
import { SchemeColors } from "@notesnook/theme"; import { SchemeColors } from "@notesnook/theme";
import FileSaver from "file-saver"; import FileSaver from "file-saver";
type NoteProps = { type NoteProps = NoteResolvedData & {
tags?: TagsWithDateEdited;
color?: Color;
notebooks?: NotebooksWithDateEdited;
item: Note; item: Note;
context?: Context; context?: Context;
date: number; date: number;
reminder?: ReminderType;
simplified?: boolean; simplified?: boolean;
compact?: boolean; compact?: boolean;
}; };
@@ -111,6 +106,7 @@ function Note(props: NoteProps) {
tags, tags,
color, color,
notebooks, notebooks,
attachments,
item, item,
date, date,
reminder, reminder,
@@ -121,17 +117,6 @@ function Note(props: NoteProps) {
const note = item; const note = item;
const isOpened = useStore((store) => store.selectedNote === note.id); const isOpened = useStore((store) => store.selectedNote === note.id);
const attachments = [];
// useAttachmentStore((store) =>
// store.attachments.filter((a) => a.noteIds.includes(note.id))
// );
const failed = [];
// useMemo(
// () => attachments.filter((a) => a.failed),
// [attachments]
// );
const primary: SchemeColors = color ? color.colorCode : "accent-selected"; const primary: SchemeColors = color ? color.colorCode : "accent-selected";
return ( return (
@@ -226,21 +211,21 @@ function Note(props: NoteProps) {
datetime={date} datetime={date}
/> />
{attachments.length > 0 && ( {attachments?.total ? (
<Flex sx={{ alignItems: "center", justifyContent: "center" }}> <Flex sx={{ alignItems: "center", justifyContent: "center" }}>
<Attachment size={13} /> <Attachment size={13} />
<Text variant="subBody" ml={"2px"}> <Text variant="subBody" ml={"2px"}>
{attachments.length} {attachments.total}
</Text> </Text>
</Flex> </Flex>
)} ) : null}
{failed.length > 0 && ( {attachments?.failed ? (
<Flex title={`Errors in ${failed.length} attachments.`}> <Flex title={`Errors in ${attachments.failed} attachments.`}>
<AttachmentError size={13} color="var(--icon-error)" /> <AttachmentError size={13} color="var(--icon-error)" />
<Text ml={"2px"}>{failed.length}</Text> <Text ml={"2px"}>{attachments.failed}</Text>
</Flex> </Flex>
)} ) : null}
{note.pinned && !props.context && ( {note.pinned && !props.context && (
<Pin size={13} color={primary} /> <Pin size={13} color={primary} />
@@ -289,7 +274,8 @@ export default React.memo(Note, function (prevProps, nextProps) {
prevItem.dateModified === nextItem.dateModified && prevItem.dateModified === nextItem.dateModified &&
prevProps.notebooks?.dateEdited === nextProps.notebooks?.dateEdited && prevProps.notebooks?.dateEdited === nextProps.notebooks?.dateEdited &&
prevProps.tags?.dateEdited === nextProps.tags?.dateEdited && prevProps.tags?.dateEdited === nextProps.tags?.dateEdited &&
prevProps.reminder?.dateModified === nextProps.reminder?.dateModified prevProps.reminder?.dateModified === nextProps.reminder?.dateModified &&
prevProps.attachments === nextProps.attachments
); );
}); });

View File

@@ -36,13 +36,11 @@ import { store as noteStore } from "../../stores/note-store";
import { AnimatedFlex } from "../animated"; import { AnimatedFlex } from "../animated";
import Toggle from "./toggle"; import Toggle from "./toggle";
import ScrollContainer from "../scroll-container"; import ScrollContainer from "../scroll-container";
import { getFormattedDate } from "@notesnook/common"; import { ResolvedItem, getFormattedDate, usePromise } from "@notesnook/common";
import { ScopedThemeProvider } from "../theme-provider"; import { ScopedThemeProvider } from "../theme-provider";
import { PreviewSession } from "../editor/types"; import { PreviewSession } from "../editor/types";
import usePromise from "../../hooks/use-promise";
import { ListItemWrapper } from "../list-container/list-profiles"; import { ListItemWrapper } from "../list-container/list-profiles";
import { VirtualizedList } from "../virtualized-list"; import { VirtualizedList } from "../virtualized-list";
import { ResolvedItem } from "../list-container/resolved-item";
import { SessionItem } from "../session-item"; import { SessionItem } from "../session-item";
import { COLORS } from "../../common/constants"; import { COLORS } from "../../common/constants";
import { DefaultColors } from "@notesnook/core"; import { DefaultColors } from "@notesnook/core";

View File

@@ -23,8 +23,7 @@ import { ArrowLeft, Menu, Search, Plus, Close } from "../icons";
import { useStore } from "../../stores/app-store"; import { useStore } from "../../stores/app-store";
import { useStore as useSearchStore } from "../../stores/search-store"; import { useStore as useSearchStore } from "../../stores/search-store";
import useMobile from "../../hooks/use-mobile"; import useMobile from "../../hooks/use-mobile";
import usePromise from "../../hooks/use-promise"; import { debounce, usePromise } from "@notesnook/common";
import { debounce } from "@notesnook/common";
import Field from "../field"; import Field from "../field";
export type RouteContainerButtons = { export type RouteContainerButtons = {

View File

@@ -26,7 +26,7 @@ import { useStore as useAppStore } from "../../stores/app-store";
import Field from "../field"; import Field from "../field";
import { showToast } from "../../utils/toast"; import { showToast } from "../../utils/toast";
import { ErrorText } from "../error-text"; import { ErrorText } from "../error-text";
import usePromise from "../../hooks/use-promise"; import { usePromise } from "@notesnook/common";
type UnlockProps = { type UnlockProps = {
noteId: string; noteId: string;

View File

@@ -32,9 +32,9 @@ import { store as editorStore } from "../stores/editor-store";
import { Perform } from "../common/dialog-controller"; import { Perform } from "../common/dialog-controller";
import { FilteredList } from "../components/filtered-list"; import { FilteredList } from "../components/filtered-list";
import { ItemReference, Tag } from "@notesnook/core/dist/types"; import { ItemReference, Tag } from "@notesnook/core/dist/types";
import { ResolvedItem } from "../components/list-container/resolved-item";
import { create } from "zustand"; import { create } from "zustand";
import { VirtualizedGrouping } from "@notesnook/core"; import { VirtualizedGrouping } from "@notesnook/core";
import { ResolvedItem } from "@notesnook/common";
type SelectedReference = { type SelectedReference = {
id: string; id: string;
@@ -134,6 +134,8 @@ function AddTagsDialog(props: AddTagsDialogProps) {
onCreateNewItem={async (title) => { onCreateNewItem={async (title) => {
const tagId = await db.tags.add({ title }); const tagId = await db.tags.add({ title });
if (!tagId) return; if (!tagId) return;
await useStore.getState().refresh();
setTags(useStore.getState().tags);
const { selected, setSelected } = useSelectionStore.getState(); const { selected, setSelected } = useSelectionStore.getState();
setSelected([...selected, { id: tagId, new: true, op: "add" }]); setSelected([...selected, { id: tagId, new: true, op: "add" }]);
}} }}

View File

@@ -29,7 +29,7 @@ import {
Text Text
} from "@theme-ui/components"; } from "@theme-ui/components";
import { store, useStore } from "../stores/attachment-store"; import { store, useStore } from "../stores/attachment-store";
import { formatBytes } from "@notesnook/common"; import { ResolvedItem, formatBytes, usePromise } from "@notesnook/common";
import Dialog from "../components/dialog"; import Dialog from "../components/dialog";
import { import {
ChevronDown, ChevronDown,
@@ -57,13 +57,11 @@ import {
VirtualizedGrouping VirtualizedGrouping
} from "@notesnook/core"; } from "@notesnook/core";
import { Multiselect } from "../common/multi-select"; import { Multiselect } from "../common/multi-select";
import { ResolvedItem } from "../components/list-container/resolved-item";
import { import {
VirtualizedTable, VirtualizedTable,
VirtualizedTableRowProps VirtualizedTableRowProps
} from "../components/virtualized-table"; } from "../components/virtualized-table";
import { FlexScrollContainer } from "../components/scroll-container"; import { FlexScrollContainer } from "../components/scroll-container";
import usePromise from "../hooks/use-promise";
type ToolbarAction = { type ToolbarAction = {
title: string; title: string;

View File

@@ -46,8 +46,7 @@ import {
TreeEnvironmentRef TreeEnvironmentRef
} from "react-complex-tree"; } from "react-complex-tree";
import { FlexScrollContainer } from "../components/scroll-container"; import { FlexScrollContainer } from "../components/scroll-container";
import { pluralize } from "@notesnook/common"; import { pluralize, usePromise } from "@notesnook/common";
import usePromise from "../hooks/use-promise";
type MoveDialogProps = { onClose: Perform; noteIds: string[] }; type MoveDialogProps = { onClose: Perform; noteIds: string[] };
type NotebookReference = { type NotebookReference = {

View File

@@ -25,8 +25,7 @@ import { Reminder } from "@notesnook/core/dist/types";
import IconTag from "../components/icon-tag"; import IconTag from "../components/icon-tag";
import { Clock, Refresh } from "../components/icons"; import { Clock, Refresh } from "../components/icons";
import Note from "../components/note"; import Note from "../components/note";
import { getFormattedReminderTime } from "@notesnook/common"; import { getFormattedReminderTime, usePromise } from "@notesnook/common";
import usePromise from "../hooks/use-promise";
export type ReminderPreviewDialogProps = { export type ReminderPreviewDialogProps = {
onClose: Perform; onClose: Perform;

View File

@@ -0,0 +1,20 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
export * from "./resolved-item";

View File

@@ -0,0 +1,39 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
import { ItemMap, ItemType } from "@notesnook/core";
import {
ResolvedItemOptions,
useResolvedItem
} from "../hooks/use-resolved-item";
type ResolvedItemProps<TItemType extends ItemType> =
ResolvedItemOptions<TItemType> & {
children: (item: {
item: ItemMap[TItemType];
data: unknown;
}) => React.ReactNode;
};
export function ResolvedItem<TItemType extends ItemType>(
props: ResolvedItemProps<TItemType>
) {
const { children } = props;
const result = useResolvedItem(props);
return result ? <>{children(result)}</> : null;
}

View File

@@ -18,3 +18,5 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
export * from "./use-time-ago"; export * from "./use-time-ago";
export * from "./use-promise";
export * from "./use-resolved-item";

View File

@@ -55,7 +55,7 @@ export type PromiseFactoryFn<T> = (signal: AbortSignal) => T | Promise<T>;
* @param factory Function that creates the promise. * @param factory Function that creates the promise.
* @param deps If present, promise will be recreated if the values in the list change. * @param deps If present, promise will be recreated if the values in the list change.
*/ */
export default function usePromise<T>( export function usePromise<T>(
factory: PromiseFactoryFn<T>, factory: PromiseFactoryFn<T>,
deps: DependencyList = [] deps: DependencyList = []
): PromiseResult<T> { ): PromiseResult<T> {

View File

@@ -0,0 +1,59 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
import { ItemMap, ItemType, VirtualizedGrouping } from "@notesnook/core";
import { usePromise } from "./use-promise";
import { resolveItems } from "../utils/resolve-items";
export type ResolvedItemOptions<TItemType extends ItemType> = {
type?: TItemType;
items: VirtualizedGrouping<ItemMap[TItemType]>;
index: number;
};
/**
* Fetches & resolves the item from VirtualizedGrouping
*/
export function useResolvedItem<TItemType extends ItemType>(
options: ResolvedItemOptions<TItemType>
) {
const { index, items, type } = options;
const result = usePromise(
() => items.item(index, resolveItems),
[index, items]
);
if (result.status === "rejected" || !result.value) return null;
if (type && result.value.item.type !== type) return null;
return result.value;
}
/**
* Fetches but does not resolve the item from VirtualizedGrouping
*/
export function useUnresolvedItem<TItemType extends ItemType>(
options: ResolvedItemOptions<TItemType>
) {
const { index, items, type } = options;
const result = usePromise(() => items.item(index), [index, items]);
if (result.status === "rejected" || !result.value) return null;
if (type && result.value.item.type !== type) return null;
return result.value;
}

View File

@@ -20,3 +20,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
export * from "./database"; export * from "./database";
export * from "./utils"; export * from "./utils";
export * from "./hooks"; export * from "./hooks";
export * from "./components";

View File

@@ -23,3 +23,4 @@ export * from "./time";
export * from "./debounce"; export * from "./debounce";
export * from "./random"; export * from "./random";
export * from "./string"; export * from "./string";
export * from "./resolve-items";

View File

@@ -16,10 +16,11 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Color, Item, Notebook, Reminder, Tag } from "@notesnook/core";
import { db } from "../common/database";
export type WithDateEdited<T> = { items: T[]; dateEdited: number }; import { Color, Item, Reminder, Notebook, Tag } from "@notesnook/core";
import { database as db } from "../database";
type WithDateEdited<T> = { items: T[]; dateEdited: number };
export type NotebooksWithDateEdited = WithDateEdited<Notebook>; export type NotebooksWithDateEdited = WithDateEdited<Notebook>;
export type TagsWithDateEdited = WithDateEdited<Tag>; export type TagsWithDateEdited = WithDateEdited<Tag>;
@@ -49,22 +50,25 @@ export async function resolveItems(ids: string[], items: Item[]) {
return []; return [];
} }
type NoteResolvedData = { export type NoteResolvedData = {
notebooks?: NotebooksWithDateEdited; notebooks?: NotebooksWithDateEdited;
reminder?: Reminder; reminder?: Reminder;
color?: Color; color?: Color;
tags?: TagsWithDateEdited; tags?: TagsWithDateEdited;
attachmentsCount?: number; attachments?: {
failed: number;
total: number;
};
}; };
async function resolveNotes(ids: string[]) { async function resolveNotes(ids: string[]) {
console.time("relations");
const relations = [ const relations = [
...(await db.relations ...(await db.relations
.to({ type: "note", ids }, ["notebook", "tag", "color"]) .to({ type: "note", ids }, ["notebook", "tag", "color"])
.get()), .get()),
...(await db.relations.from({ type: "note", ids }, "reminder").get()) ...(await db.relations.from({ type: "note", ids }, "reminder").get())
]; ];
console.timeEnd("relations");
const relationIds: { const relationIds: {
notebooks: Set<string>; notebooks: Set<string>;
colors: Set<string>; colors: Set<string>;
@@ -86,6 +90,7 @@ async function resolveNotes(ids: string[]) {
reminder?: string; reminder?: string;
} }
> = {}; > = {};
for (const relation of relations) { for (const relation of relations) {
const noteId = const noteId =
relation.toType === "reminder" ? relation.fromId : relation.toId; relation.toType === "reminder" ? relation.fromId : relation.toId;
@@ -110,7 +115,6 @@ async function resolveNotes(ids: string[]) {
grouped[noteId] = data; grouped[noteId] = data;
} }
console.time("resolve");
const resolved = { const resolved = {
notebooks: await db.notebooks.all.records( notebooks: await db.notebooks.all.records(
Array.from(relationIds.notebooks) Array.from(relationIds.notebooks)
@@ -119,7 +123,6 @@ async function resolveNotes(ids: string[]) {
colors: await db.colors.all.records(Array.from(relationIds.colors)), colors: await db.colors.all.records(Array.from(relationIds.colors)),
reminders: await db.reminders.all.records(Array.from(relationIds.reminders)) reminders: await db.reminders.all.records(Array.from(relationIds.reminders))
}; };
console.timeEnd("resolve");
const data: NoteResolvedData[] = []; const data: NoteResolvedData[] = [];
for (const noteId of ids) { for (const noteId of ids) {
@@ -129,6 +132,7 @@ async function resolveNotes(ids: string[]) {
continue; continue;
} }
const attachments = db.attachments?.ofNote(noteId, "all");
data.push({ data.push({
color: group.color ? resolved.colors[group.color] : undefined, color: group.color ? resolved.colors[group.color] : undefined,
reminder: group.reminder ? resolved.reminders[group.reminder] : undefined, reminder: group.reminder ? resolved.reminders[group.reminder] : undefined,
@@ -138,11 +142,14 @@ async function resolveNotes(ids: string[]) {
notebooks: withDateEdited( notebooks: withDateEdited(
group.notebooks.map((id) => resolved.notebooks[id]).filter(Boolean) group.notebooks.map((id) => resolved.notebooks[id]).filter(Boolean)
), ),
attachmentsCount: attachments: {
(await db.attachments?.ofNote(noteId, "all").ids())?.length || 0 total: await attachments.count(),
failed: await attachments
.where((eb) => eb("attachments.failed", "is not", eb.lit(null)))
.count()
}
}); });
} }
return data; return data;
} }