From 4556e4d230ee8836be71b4bed40f0bc903841e72 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Fri, 10 Nov 2023 15:33:14 +0500 Subject: [PATCH] web: use lazy loading to load attachments --- apps/web/src/common/multi-select.ts | 56 +-- apps/web/src/components/attachment/index.tsx | 3 +- apps/web/src/dialogs/attachments-dialog.tsx | 424 +++++++++--------- ...ttachment-store.js => attachment-store.ts} | 112 ++--- .../src/utils/streams/attachment-stream.ts | 22 +- packages/core/src/collections/attachments.ts | 46 +- packages/core/src/database/sql-collection.ts | 12 +- packages/core/src/types.ts | 17 +- .../core/src/utils/virtualized-grouping.ts | 4 + 9 files changed, 379 insertions(+), 317 deletions(-) rename apps/web/src/stores/{attachment-store.js => attachment-store.ts} (52%) diff --git a/apps/web/src/common/multi-select.ts b/apps/web/src/common/multi-select.ts index 358abb09a..c2eb8fcdc 100644 --- a/apps/web/src/common/multi-select.ts +++ b/apps/web/src/common/multi-select.ts @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { showMultiDeleteConfirmation } from "./dialog-controller"; +import { confirm, showMultiDeleteConfirmation } from "./dialog-controller"; import { store as noteStore } from "../stores/note-store"; import { store as notebookStore } from "../stores/notebook-store"; import { store as attachmentStore } from "../stores/attachment-store"; @@ -27,12 +27,7 @@ import { showToast } from "../utils/toast"; import Vault from "./vault"; import { TaskManager } from "./task-manager"; import { pluralize } from "@notesnook/common"; - -type Item = { - id: string; - locked?: boolean; - metadata?: Record; -}; +import { Reminder } from "@notesnook/core"; async function moveNotesToTrash(ids: string[], confirm = true) { if (confirm && !(await showMultiDeleteConfirmation(ids.length))) return; @@ -77,27 +72,13 @@ async function moveNotebooksToTrash(ids: string[]) { showToast("success", `${pluralize(ids.length, "notebook")} moved to trash`); } -async function deleteTopics(notebookId: string, topics: Item[]) { - await TaskManager.startTask({ - type: "status", - id: "deleteTopics", - action: async (report) => { - report({ - text: `Deleting ${pluralize(topics.length, "topic")}...` - }); - await db.notebooks.topics(notebookId).delete(...topics.map((t) => t.id)); - notebookStore.setSelectedNotebook(notebookId); - noteStore.refresh(); - } - }); - showToast("success", `${pluralize(topics.length, "topic")} deleted`); -} - -async function deleteAttachments(attachments: Item[]) { +async function deleteAttachments(ids: string[]) { if ( - !window.confirm( - "Are you sure you want to permanently delete these attachments? This action is IRREVERSIBLE." - ) + !(await confirm({ + title: "Are you sure?", + message: + "Are you sure you want to permanently delete these attachments? This action is IRREVERSIBLE." + })) ) return; @@ -105,24 +86,24 @@ async function deleteAttachments(attachments: Item[]) { type: "status", id: "deleteAttachments", action: async (report) => { - for (let i = 0; i < attachments.length; ++i) { - const attachment = attachments[i]; + for (let i = 0; i < ids.length; ++i) { + const id = ids[i]; + const attachment = await attachmentStore.get().attachments?.item(id); + if (!attachment) continue; + report({ - text: `Deleting ${pluralize(attachments.length, "attachment")}...`, + text: `Deleting ${pluralize(ids.length, "attachment")}...`, current: i, - total: attachments.length + total: ids.length }); - await attachmentStore.permanentDelete(attachment.metadata?.hash); + await attachmentStore.permanentDelete(attachment); } } }); - showToast( - "success", - `${pluralize(attachments.length, "attachment")} deleted` - ); + showToast("success", `${pluralize(ids.length, "attachment")} deleted`); } -async function moveRemindersToTrash(reminders: Item[]) { +async function moveRemindersToTrash(reminders: Reminder[]) { const isMultiselect = reminders.length > 1; if (isMultiselect) { if (!(await showMultiDeleteConfirmation(reminders.length))) return; @@ -146,6 +127,5 @@ export const Multiselect = { moveRemindersToTrash, moveNotebooksToTrash, moveNotesToTrash, - deleteTopics, deleteAttachments }; diff --git a/apps/web/src/components/attachment/index.tsx b/apps/web/src/components/attachment/index.tsx index af047908a..526af57b9 100644 --- a/apps/web/src/components/attachment/index.tsx +++ b/apps/web/src/components/attachment/index.tsx @@ -59,6 +59,7 @@ import { useEffect, useState } from "react"; import { AppEventManager, AppEvents } from "../../common/app-events"; import { getFormattedDate } from "@notesnook/common"; import { MenuItem } from "@notesnook/ui"; +import { Attachment } from "@notesnook/core"; const FILE_ICONS: Record = { "image/": FileImage, @@ -85,7 +86,7 @@ type AttachmentProgressStatus = { }; type AttachmentProps = { - attachment: any; + attachment: Attachment; isSelected?: boolean; onSelected?: () => void; compact?: boolean; diff --git a/apps/web/src/dialogs/attachments-dialog.tsx b/apps/web/src/dialogs/attachments-dialog.tsx index 666ad18ae..81a7e319b 100644 --- a/apps/web/src/dialogs/attachments-dialog.tsx +++ b/apps/web/src/dialogs/attachments-dialog.tsx @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { useEffect, useState, memo, useMemo, useRef } from "react"; +import { useEffect, useState, memo, useRef } from "react"; import { Box, Button, @@ -28,11 +28,10 @@ import { Label, Text } from "@theme-ui/components"; -import { getTotalSize } from "../common/attachments"; -import { useStore, store } from "../stores/attachment-store"; +import { store, useStore } from "../stores/attachment-store"; import { formatBytes } from "@notesnook/common"; import Dialog from "../components/dialog"; -import { TableVirtuoso } from "react-virtuoso"; +import { ItemProps, TableVirtuoso } from "react-virtuoso"; import { ChevronDown, ChevronUp, @@ -52,21 +51,20 @@ import NavigationItem from "../components/navigation-menu/navigation-item"; import { pluralize } from "@notesnook/common"; import { db } from "../common/db"; import { Perform } from "../common/dialog-controller"; -import { Multiselect } from "../common/multi-select"; import { CustomScrollbarsVirtualList } from "../components/list-container"; import { Attachment } from "../components/attachment"; -import { - isDocument, - isImage, - isVideo -} from "@notesnook/core/dist/utils/filename"; -import { alpha } from "@theme-ui/color"; import { ScopedThemeProvider } from "../components/theme-provider"; +import { + Attachment as AttachmentType, + VirtualizedGrouping +} from "@notesnook/core"; +import usePromise from "../hooks/use-promise"; +import { Multiselect } from "../common/multi-select"; type ToolbarAction = { title: string; icon: Icon; - onClick: ({ selected }: { selected: any[] }) => void; + onClick: ({ selected }: { selected: string[] }) => void; }; const TOOLBAR_ACTIONS: ToolbarAction[] = [ @@ -80,9 +78,7 @@ const TOOLBAR_ACTIONS: ToolbarAction[] = [ { title: "Recheck", icon: DoubleCheckmark, - onClick: async ({ selected }) => { - await store.recheck(selected.map((a) => a.metadata.hash)); - } + onClick: async ({ selected }) => await store.recheck(selected) }, { title: "Delete", @@ -91,53 +87,65 @@ const TOOLBAR_ACTIONS: ToolbarAction[] = [ } ]; +const COLUMNS = [ + { id: "filename" as const, title: "Name", width: "65%" }, + { id: "status" as const, width: "24px" }, + { id: "size" as const, title: "Size", width: "15%" }, + { id: "dateUploaded" as const, title: "Date uploaded", width: "20%" } +]; + +type SortOptions = { + id: "filename" | "size" | "dateUploaded"; + direction: "asc" | "desc"; +}; type AttachmentsDialogProps = { onClose: Perform }; function AttachmentsDialog({ onClose }: AttachmentsDialogProps) { const allAttachments = useStore((store) => store.attachments); - const [attachments, setAttachments] = useState(allAttachments); + const [attachments, setAttachments] = + useState>(); + const [counts, setCounts] = useState>({ + all: 0, + documents: 0, + images: 0, + orphaned: 0, + uploads: 0, + videos: 0 + }); const [selected, setSelected] = useState([]); - const [sortBy, setSortBy] = useState({ id: "name", direction: "asc" }); + const [sortBy, setSortBy] = useState({ + id: "filename", + direction: "asc" + }); const currentRoute = useRef("all"); const refresh = useStore((store) => store.refresh); + const download = useStore((store) => store.download); useEffect(() => { refresh(); }, [refresh]); useEffect(() => { - setAttachments(filterAttachments(currentRoute.current, allAttachments)); - }, [allAttachments]); + (async function () { + setAttachments( + await filterAttachments(currentRoute.current).sorted({ + sortBy: sortBy.id, + sortDirection: sortBy.direction + }) + ); + })(); + }, [sortBy, allAttachments]); useEffect(() => { - setAttachments((a) => { - const attachments = a.slice(); - if (sortBy.id === "name") { - attachments.sort( - sortBy.direction === "asc" - ? (a, b) => a.metadata.filename.localeCompare(b.metadata.filename) - : (a, b) => b.metadata.filename.localeCompare(a.metadata.filename) - ); - } else if (sortBy.id === "size") { - attachments.sort( - sortBy.direction === "asc" - ? (a, b) => a.length - b.length - : (a, b) => b.length - a.length - ); - } else if (sortBy.id === "dateUploaded") { - attachments.sort( - sortBy.direction === "asc" - ? (a, b) => a.dateUploaded - b.dateUploaded - : (a, b) => b.dateUploaded - a.dateUploaded - ); - } - return attachments; - }); - }, [sortBy]); + (async function () { + setCounts(await getCounts()); + })(); + }, [allAttachments]); - const totalSize = useMemo( - () => getTotalSize(allAttachments), - [allAttachments] - ); + const totalSize = 0; + // useMemo( + // () => getTotalSize(allAttachments), + // [allAttachments] + // ); return ( download(allAttachments?.ungrouped || [])} filter={(query) => { - setAttachments( - db.lookup?.attachments(db.attachments.all || [], query) || [] - ); + // setAttachments( + // db.lookup?.attachments(db.attachments.all || [], query) || [] + // ); }} - counts={getCounts(allAttachments)} - onRouteChange={(route) => { + counts={counts} + onRouteChange={async (route) => { currentRoute.current = route; setSelected([]); - setAttachments(filterAttachments(route, allAttachments)); + setAttachments( + await filterAttachments(currentRoute.current).sorted({ + sortBy: sortBy.id, + sortDirection: sortBy.direction + }) + ); }} /> tool.onClick({ - selected: attachments.filter( - (a) => selected.indexOf(a.id) > -1 - ) + selected + // : attachments.filter( + // (a) => selected.indexOf(a.id) > -1 + // ) }) } disabled={!selected.length} @@ -218,123 +233,113 @@ function AttachmentsDialog({ onClose }: AttachmentsDialogProps) { */} - { - const attachment = attachments[props["data-item-index"]]; - return ( - -1} - onSelected={() => { - setSelected((s) => { - const copy = s.slice(); - const index = copy.indexOf(attachment.id); - if (index > -1) copy.splice(index, 1); - else copy.push(attachment.id); - return copy; - }); - }} - /> - ); - } - }} - style={{ height: "100%" }} - data={attachments} - fixedItemHeight={30} - defaultItemHeight={30} - fixedHeaderContent={() => ( - - attachments.getKey(index)} + data={attachments.ungrouped} + context={{ + isSelected: (id: string) => selected.indexOf(id) > -1, + select: (id: string) => { + setSelected((s) => { + const copy = s.slice(); + const index = copy.indexOf(id); + if (index > -1) copy.splice(index, 1); + else copy.push(id); + return copy; + }); + }, + attachments + }} + fixedItemHeight={30} + defaultItemHeight={30} + fixedHeaderContent={() => ( + - - - {[ - { id: "name", title: "Name", width: "65%" }, - { id: "status", width: "24px" }, - { id: "size", title: "Size", width: "15%" }, - { id: "dateUploaded", title: "Date uploaded", width: "20%" } - ].map((column) => - !column.title ? ( - - ) : ( - { - setSortBy((sortBy) => ({ - direction: - sortBy.id === column.id && - sortBy.direction === "asc" - ? "desc" - : "asc", - id: column.id - })); - }} - > - + + + {COLUMNS.map((column) => + !column.title ? ( + + ) : ( + { + setSortBy((sortBy) => ({ + direction: + sortBy.id === column.id && + sortBy.direction === "asc" + ? "desc" + : "asc", + id: column.id + })); }} > - - {column.title} - - {sortBy.id === column.id ? ( - sortBy.direction === "asc" ? ( - - ) : ( - - ) - ) : null} - - - ) - )} - - )} - itemContent={() => <>} - /> + + {column.title} + + {sortBy.id === column.id ? ( + sortBy.direction === "asc" ? ( + + ) : ( + + ) + ) : null} + + + ) + )} + + )} + itemContent={() => <>} + /> + )} @@ -379,6 +384,7 @@ const routes: { id: Route; icon: Icon; title: string }[] = [ ]; type SidebarProps = { + onDownloadAll: () => void; onRouteChange: (route: Route) => void; filter: (query: string) => void; counts: Record; @@ -386,11 +392,10 @@ type SidebarProps = { }; const Sidebar = memo( function Sidebar(props: SidebarProps) { - const { onRouteChange, filter, counts, totalSize } = props; + const { onRouteChange, filter, counts, totalSize, onDownloadAll } = props; const [route, setRoute] = useState("all"); const downloadStatus = useStore((store) => store.status); const cancelDownload = useStore((store) => store.cancel); - const download = useStore((store) => store.download); return ( @@ -446,7 +451,7 @@ const Sidebar = memo( if (downloadStatus) { await cancelDownload(); } else { - await download(db.attachments.all); + onDownloadAll(); } }} > @@ -482,38 +487,57 @@ const Sidebar = memo( prev.counts.orphaned === next.counts.orphaned ); -function getCounts(attachments: any[]): Record { - const counts: Record = { - all: 0, - documents: 0, - images: 0, - videos: 0, - uploads: 0, - orphaned: 0 - }; - for (const attachment of attachments) { - counts.all++; - - if (isDocument(attachment.metadata.type)) counts.documents++; - else if (isImage(attachment.metadata.type)) counts.images++; - else if (isVideo(attachment.metadata.type)) counts.videos++; - - if (!attachment.dateUploaded) counts.uploads++; - if (!attachment.noteIds.length) counts.orphaned++; +function TableRow( + props: ItemProps & { + context?: { + isSelected: (id: string) => boolean; + select: (id: string) => void; + attachments: VirtualizedGrouping; + }; } - return counts; +) { + const { context, item, ...restProps } = props; + const result = usePromise( + () => context?.attachments.item(item), + [item, context] + ); + + if (result.status !== "fulfilled" || !result.value) + return
; + return ( + { + context?.select(item); + }} + /> + ); } -function filterAttachments(route: Route, attachments: any[]): any[] { - return route === "all" - ? attachments - : route === "images" - ? attachments.filter((a) => a.metadata.type.startsWith("image/")) - : route === "videos" - ? attachments.filter((a) => a.metadata.type.startsWith("video/")) - : route === "documents" - ? attachments.filter((a) => isDocument(a.metadata.type)) - : route === "orphaned" - ? attachments.filter((a) => !a.noteIds.length) - : attachments.filter((a) => !a.dateUploaded); +async function getCounts(): Promise> { + return { + all: await db.attachments.all.count(), + documents: await db.attachments.documents.count(), + images: await db.attachments.images.count(), + videos: await db.attachments.videos.count(), + uploads: await db.attachments.pending.count(), + orphaned: await db.attachments.orphaned.count() + }; +} + +function filterAttachments(route: Route) { + return route === "all" + ? db.attachments.all + : route === "images" + ? db.attachments.images + : route === "videos" + ? db.attachments.videos + : route === "documents" + ? db.attachments.documents + : route === "orphaned" + ? db.attachments.orphaned + : db.attachments.pending; } diff --git a/apps/web/src/stores/attachment-store.js b/apps/web/src/stores/attachment-store.ts similarity index 52% rename from apps/web/src/stores/attachment-store.js rename to apps/web/src/stores/attachment-store.ts index 11b856009..6a4290be1 100644 --- a/apps/web/src/stores/attachment-store.js +++ b/apps/web/src/stores/attachment-store.ts @@ -26,44 +26,45 @@ import { showToast } from "../utils/toast"; import { AttachmentStream } from "../utils/streams/attachment-stream"; import { createZipStream } from "../utils/streams/zip-stream"; import { createWriteStream } from "../utils/stream-saver"; +import { Attachment, VirtualizedGrouping } from "@notesnook/core"; -let abortController = undefined; -/** - * @extends {BaseStore} - */ -class AttachmentStore extends BaseStore { - attachments = []; - /** - * @type {{current: number, total: number}} - */ - status = undefined; +let abortController: AbortController | undefined = undefined; +class AttachmentStore extends BaseStore { + attachments?: VirtualizedGrouping; + status?: { current: number; total: number }; + processing: Record< + string, + { failed?: string; working?: "delete" | "recheck" } + > = {}; - refresh = () => { - this.set((state) => (state.attachments = db.attachments.all)); + refresh = async () => { + this.set({ + attachments: await db.attachments.all.sorted({ + sortBy: "dateCreated", + sortDirection: "desc" + }) + }); }; init = () => { this.refresh(); }; - download = async (attachments) => { + download = async (ids: string[]) => { if (this.get().status) throw new Error( "Please wait for the previous download to finish or cancel it." ); - this.set( - (state) => (state.status = { current: 0, total: attachments.length }) - ); + this.set({ status: { current: 0, total: ids.length } }); abortController = new AbortController(); const attachmentStream = new AttachmentStream( - attachments, + ids, + (id) => this.attachments?.item(id), abortController.signal, (current) => { - this.set( - (state) => (state.status = { current, total: attachments.length }) - ); + this.set({ status: { current, total: ids.length } }); } ); await attachmentStream @@ -78,71 +79,74 @@ class AttachmentStore extends BaseStore { cancel = async () => { if (abortController) { - await abortController.abort(); + abortController.abort(); abortController = undefined; this.set((state) => (state.status = undefined)); } }; - recheck = async (hashes) => { - const attachments = this.get().attachments; - for (let hash of hashes) { - const index = attachments.findIndex((a) => a.metadata.hash === hash); + recheck = async (ids: string[]) => { + for (const id of ids) { + const attachment = await this.attachments?.item(id); + if (!attachment) continue; try { - this._changeWorkingStatus(index, "recheck", undefined); + this._changeWorkingStatus(attachment.hash, "recheck"); - const { failed, success } = await checkAttachment(hash); - this._changeWorkingStatus(index, false, success ? null : failed); + const { failed, success } = await checkAttachment(attachment.hash); + this._changeWorkingStatus( + attachment.hash, + undefined, + success ? undefined : failed + ); } catch (e) { console.error(e); - this._changeWorkingStatus(index, false, false); - showToast("error", `Rechecking failed: ${e.message}`); + this._changeWorkingStatus(attachment.hash); + if (e instanceof Error) + showToast("error", `Rechecking failed: ${e.message}`); } } }; - rename = async (hash, newName) => { + rename = async (hash: string, newName: string) => { await db.attachments.add({ hash, filename: newName }); - this.get().refresh(); + await this.get().refresh(); }; - permanentDelete = async (hash) => { - const index = this.get().attachments.findIndex( - (a) => a.metadata.hash === hash - ); - if (index <= -1) return; - const noteIds = this.get().attachments[index].noteIds.slice(); - + permanentDelete = async (attachment: Attachment) => { try { - this._changeWorkingStatus(index, "delete", undefined); - if (await db.attachments.remove(hash, false)) { - this.get().refresh(); + this._changeWorkingStatus(attachment.hash, "delete"); + if (await db.attachments.remove(attachment.hash, false)) { + await this.get().refresh(); - if (noteIds.includes(editorStore.get().session.id)) { + const sessionId = editorStore.get().session.id; + if ( + sessionId && + (await db.relations + .to({ id: attachment.id, type: "attachment" }, "note") + .has(sessionId)) + ) { await editorStore.clearSession(); } } } catch (e) { console.error(e); - this._changeWorkingStatus(index, false, false); - showToast("error", `Failed to delete: ${e.message}`); + this._changeWorkingStatus(attachment.hash); + if (e instanceof Error) + showToast("error", `Failed to delete: ${e.message}`); throw e; } }; - /** - * - * @param {*} index - * @param {"delete"|"recheck"} workType - * @param {*} failed - */ - _changeWorkingStatus = (index, workType, failed) => { + private _changeWorkingStatus = ( + hash: string, + working?: "delete" | "recheck", + failed?: string + ) => { this.set((state) => { - state.attachments[index].failed = failed; - state.attachments[index].working = workType; + state.processing[hash] = { failed, working }; }); }; } diff --git a/apps/web/src/utils/streams/attachment-stream.ts b/apps/web/src/utils/streams/attachment-stream.ts index 81a48a16c..43bfdfd45 100644 --- a/apps/web/src/utils/streams/attachment-stream.ts +++ b/apps/web/src/utils/streams/attachment-stream.ts @@ -21,12 +21,14 @@ import { db } from "../../common/db"; import { lazify } from "../lazify"; import { makeUniqueFilename } from "./utils"; import { ZipFile } from "./zip-stream"; +import { Attachment } from "@notesnook/core"; export const METADATA_FILENAME = "metadata.json"; const GROUP_ID = "all-attachments"; export class AttachmentStream extends ReadableStream { constructor( - attachments: Array, + ids: string[], + resolve: (id: string) => Promise | undefined, signal?: AbortSignal, onProgress?: (current: number) => void ) { @@ -47,16 +49,12 @@ export class AttachmentStream extends ReadableStream { } onProgress && onProgress(index); - const attachment = attachments[index++]; + const attachment = await resolve(ids[index++]); + if (!attachment) return; await db .fs() - .downloadFile( - GROUP_ID, - attachment.metadata.hash, - attachment.chunkSize, - attachment.metadata - ); + .downloadFile(GROUP_ID, attachment.hash, attachment.chunkSize); const key = await db.attachments.decryptKey(attachment.key); if (!key) return; @@ -67,14 +65,14 @@ export class AttachmentStream extends ReadableStream { decryptFile(attachment.metadata.hash, { key, iv: attachment.iv, - name: attachment.metadata.filename, - type: attachment.metadata.type, + name: attachment.filename, + type: attachment.mimeType, isUploaded: !!attachment.dateUploaded }) ); if (file) { - const filePath: string = attachment.metadata.filename; + const filePath: string = attachment.filename; controller.enqueue({ path: makeUniqueFilename(filePath, counters), data: new Uint8Array(await file.arrayBuffer()) @@ -86,7 +84,7 @@ export class AttachmentStream extends ReadableStream { console.error(e); controller.error(e); } finally { - if (index === attachments.length) { + if (index === ids.length) { controller.close(); } } diff --git a/packages/core/src/collections/attachments.ts b/packages/core/src/collections/attachments.ts index 0ae995289..50beb15b9 100644 --- a/packages/core/src/collections/attachments.ts +++ b/packages/core/src/collections/attachments.ts @@ -24,6 +24,7 @@ import { EV, EVENTS } from "../common"; import dataurl from "../utils/dataurl"; import dayjs from "dayjs"; import { + DocumentMimeTypes, getFileNameWithExtension, isImage, isWebClip @@ -390,14 +391,47 @@ export class Attachments implements ICollection { ); } - // get images() { - // return this.all.filter((attachment) => isImage(attachment.metadata.type)); - // } + get images() { + return this.collection.createFilter( + (qb) => qb.where("mimeType", "like", `image/%`), + this.db.options?.batchSize + ); + } - // get webclips() { - // return this.all.filter((attachment) => isWebClip(attachment.metadata.type)); - // } + get videos() { + return this.collection.createFilter( + (qb) => qb.where("mimeType", "like", `video/%`), + this.db.options?.batchSize + ); + } + get documents() { + return this.collection.createFilter( + (qb) => qb.where("mimeType", "in", DocumentMimeTypes), + this.db.options?.batchSize + ); + } + + get webclips() { + return this.collection.createFilter( + (qb) => qb.where("mimeType", "==", `application/vnd.notesnook.web-clip`), + this.db.options?.batchSize + ); + } + + get orphaned() { + return this.collection.createFilter( + (qb) => + qb.where("id", "not in", (eb) => + eb + .selectFrom("relations") + .where("toType", "==", "attachment") + .select("toId as id") + .$narrowType<{ id: string }>() + ), + this.db.options?.batchSize + ); + } // get media() { // return this.all.filter( // (attachment) => diff --git a/packages/core/src/database/sql-collection.ts b/packages/core/src/database/sql-collection.ts index 68484feda..1f7a4ebf4 100644 --- a/packages/core/src/database/sql-collection.ts +++ b/packages/core/src/database/sql-collection.ts @@ -18,7 +18,13 @@ along with this program. If not, see . */ import { EVENTS } from "../common"; -import { GroupOptions, Item, MaybeDeletedItem, isDeleted } from "../types"; +import { + GroupOptions, + Item, + MaybeDeletedItem, + SortOptions, + isDeleted +} from "../types"; import EventManager from "../utils/event-manager"; import { DatabaseAccessor, @@ -359,7 +365,7 @@ export class FilteredSelector { ); } - async sorted(options: GroupOptions) { + async sorted(options: SortOptions) { const items = await this.filter .$call(this.buildSortExpression(options)) .select("id") @@ -378,7 +384,7 @@ export class FilteredSelector { }); } - private buildSortExpression(options: GroupOptions) { + private buildSortExpression(options: SortOptions) { return ( qb: SelectQueryBuilder ) => { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 757f6fc88..0d4a63154 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -20,12 +20,23 @@ along with this program. If not, see . import { Cipher } from "@notesnook/crypto"; import { TimeFormat } from "./utils/date"; -export type GroupOptions = { - groupBy: "none" | "abc" | "year" | "month" | "week" | "default"; - sortBy: "dateCreated" | "dateDeleted" | "dateEdited" | "title" | "dueDate"; +export type SortOptions = { + sortBy: + | "dateCreated" + | "dateDeleted" + | "dateEdited" + | "title" + | "filename" + | "size" + | "dateUploaded" + | "dueDate"; sortDirection: "desc" | "asc"; }; +export type GroupOptions = SortOptions & { + groupBy: "none" | "abc" | "year" | "month" | "week" | "default"; +}; + export type GroupedItems = (T | GroupHeader)[]; export type GroupingKey = diff --git a/packages/core/src/utils/virtualized-grouping.ts b/packages/core/src/utils/virtualized-grouping.ts index bc8a2212b..86a90b9ba 100644 --- a/packages/core/src/utils/virtualized-grouping.ts +++ b/packages/core/src/utils/virtualized-grouping.ts @@ -44,6 +44,10 @@ export class VirtualizedGrouping { return item; } + get ungrouped() { + return this.ids.filter((i) => !isGroupHeader(i)) as string[]; + } + /** * Get item from cache or request the appropriate batch for caching * and load it from there.