From b615dfcd87c413ff537119a931a5f298b497af95 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Thu, 7 May 2026 16:01:43 +0500 Subject: [PATCH 1/6] web: handle & show failed inbox items Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- apps/web/src/dialogs/InboxHistoryDialog.tsx | 267 ++++++++++++++++++ .../src/dialogs/settings/inbox-settings.ts | 20 ++ packages/core/src/api/index.ts | 4 + packages/core/src/api/sync/merger.ts | 79 +++++- packages/core/src/api/sync/types.ts | 3 +- .../src/collections/inbox-items-history.ts | 77 +++++ packages/core/src/database/index.ts | 2 + packages/core/src/database/migrations.ts | 13 + packages/core/src/database/sql-collection.ts | 3 +- packages/core/src/types.ts | 28 ++ packages/intl/locale/en.po | 24 ++ packages/intl/locale/pseudo-LOCALE.po | 24 ++ packages/intl/src/strings.ts | 8 +- 13 files changed, 540 insertions(+), 12 deletions(-) create mode 100644 apps/web/src/dialogs/InboxHistoryDialog.tsx create mode 100644 packages/core/src/collections/inbox-items-history.ts diff --git a/apps/web/src/dialogs/InboxHistoryDialog.tsx b/apps/web/src/dialogs/InboxHistoryDialog.tsx new file mode 100644 index 000000000..f2fc7a780 --- /dev/null +++ b/apps/web/src/dialogs/InboxHistoryDialog.tsx @@ -0,0 +1,267 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { useState } from "react"; +import { usePromise } from "@notesnook/common"; +import { formatDate, InboxItemsHistoryErrorContext } from "@notesnook/core"; +import { Box, Button, Text } from "@theme-ui/components"; +import { db } from "../common/db"; +import { BaseDialogProps, DialogManager } from "../common/dialog-manager"; +import Dialog from "../components/dialog"; +import { Check, Copy, Trash } from "../components/icons"; +import ScrollContainer, { + FlexScrollContainer +} from "../components/scroll-container"; +import { strings } from "@notesnook/intl"; +import { writeText } from "clipboard-polyfill"; + +type InboxHistoryDialogProps = BaseDialogProps; + +const COLUMNS = [ + { title: strings.dateSynced(), width: "160px" }, + { title: strings.error(), width: "120px" }, + { title: strings.description(), width: "1fr" }, + { title: strings.details(), width: "1fr" }, + { title: "", width: "40px" } +]; + +const ERROR_CHIP_STYLES: Record< + InboxItemsHistoryErrorContext["message"], + { bg: string; color: string } +> = { + "Decryption failed": { bg: "background-error", color: "accent-error" }, + "Invalid JSON": { bg: "rgba(255, 152, 0, 0.15)", color: "#e65100" }, + "Validation failed": { bg: "rgba(255, 193, 7, 0.15)", color: "#8a6000" } +}; + +function ErrorBadge({ + message +}: { + message: InboxItemsHistoryErrorContext["message"] | undefined; +}) { + if (!message) { + return ( + + — + + ); + } + + const style = ERROR_CHIP_STYLES[message] ?? { + bg: "background-error", + color: "accent-error" + }; + return ( + + {message} + + ); +} + +function DetailsCell({ value }: { value: string }) { + const [hovered, setHovered] = useState(false); + const [copied, setCopied] = useState(false); + + function handleCopy() { + writeText(value); + setCopied(true); + setTimeout(() => setCopied(false), 1000); + } + + return ( + setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + + + {value} + + + {(hovered || copied) && ( + + )} + + ); +} + +function parseErrorContext( + raw: string | undefined +): InboxItemsHistoryErrorContext | null { + if (!raw) return null; + + try { + return JSON.parse(raw) as InboxItemsHistoryErrorContext; + } catch { + return null; + } +} + +export const InboxHistoryDialog = DialogManager.register( + function InboxHistoryDialog(props: InboxHistoryDialogProps) { + const result = usePromise(() => db.inboxItemsHistory.failed.items()); + + async function deleteItem(id: string) { + await db.inboxItemsHistory.delete(id); + if (result.status !== "pending") { + result.refresh(); + } + } + + return ( + props.onClose(false)} + negativeButton={{ + text: strings.close(), + onClick: () => props.onClose(false) + }} + noScroll + width="80%" + > + {result.status === "pending" ? ( + {strings.loading()} + ) : result.status === "rejected" ? ( + {strings.failed()} + ) : result.value.length === 0 ? ( + {strings.noFailedInboxItems()} + ) : ( + + + + + + {COLUMNS.map((col) => ( + + {col.title} + + ))} + + + + {result.value.map((item) => { + const ctx = parseErrorContext(item.errorContext); + const { message, description, ...rest } = + ctx ?? + ({} as Partial & + Record); + const hasExtra = Object.keys(rest).length > 0; + return ( + + + + {formatDate(item.dateSynced)} + + + + + + + + {(description as string) ?? "—"} + + + + {hasExtra ? ( + + ) : ( + + — + + )} + + + + + + ); + })} + + + + + )} + + ); + } +); diff --git a/apps/web/src/dialogs/settings/inbox-settings.ts b/apps/web/src/dialogs/settings/inbox-settings.ts index bdec99565..34288e302 100644 --- a/apps/web/src/dialogs/settings/inbox-settings.ts +++ b/apps/web/src/dialogs/settings/inbox-settings.ts @@ -24,6 +24,7 @@ import { InboxPGPKeysDialog } from "../inbox-pgp-keys-dialog"; import { db } from "../../common/db"; import { showPasswordDialog } from "../password-dialog"; import { strings } from "@notesnook/intl"; +import { InboxHistoryDialog } from "../InboxHistoryDialog"; export const InboxSettings: SettingsGroup[] = [ { @@ -81,6 +82,25 @@ export const InboxSettings: SettingsGroup[] = [ } ] }, + { + key: "failed-inbox-items", + title: strings.failedInboxItems(), + description: strings.failedInboxItemsDesc(), + keywords: ["inbox", "failed", "items"], + onStateChange: (listener) => + useSettingStore.subscribe((s) => s.isInboxEnabled, listener), + isHidden: () => !useSettingStore.getState().isInboxEnabled, + components: [ + { + type: "button", + title: strings.show(), + variant: "secondary", + action: () => { + InboxHistoryDialog.show({}); + } + } + ] + }, { key: "inbox-api-keys", title: "", diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts index 979a2f0ee..fea4fd6f6 100644 --- a/packages/core/src/api/index.ts +++ b/packages/core/src/api/index.ts @@ -50,6 +50,7 @@ import { Shortcuts } from "../collections/shortcuts.js"; import { Reminders } from "../collections/reminders.js"; import { Relations } from "../collections/relations.js"; import Subscriptions from "./subscriptions.js"; +import { InboxItemsHistory } from "../collections/inbox-items-history.js"; import { CompressorAccessor, ConfigStorageAccessor, @@ -228,6 +229,7 @@ class Database { settings = new Settings(this); inboxApiKeys = new InboxApiKeys(this, this.tokenManager); + inboxItemsHistory = new InboxItemsHistory(this); wrapped = new Wrapped(this); @@ -350,6 +352,8 @@ class Database { await this.vaults.init(); await this.monographsCollection.init(); + await this.inboxItemsHistory.init(); + await this.trash.init(); // legacy collections diff --git a/packages/core/src/api/sync/merger.ts b/packages/core/src/api/sync/merger.ts index 16be28c63..60cbb4366 100644 --- a/packages/core/src/api/sync/merger.ts +++ b/packages/core/src/api/sync/merger.ts @@ -29,6 +29,7 @@ import { isDeleted } from "../../types.js"; import { SyncInboxItem } from "./types.js"; +import { InboxItemsHistoryErrorContext } from "../../types.js"; import { z } from "zod"; import { sanitizeHtml } from "../../utils/html-parser.js"; @@ -201,24 +202,76 @@ export async function handleInboxItems( for (const item of inboxItems) { try { - if (await db.notes.exists(item.id)) { - logger.info("Inbox item already exists, skipping.", { + if (await db.inboxItemsHistory.exists(item.id)) { + logger.info("Inbox item already processed, skipping.", { inboxItemId: item.id }); continue; } - const decryptedItem = await db - .storage() - .decryptPGPMessage(inboxKeys.privateKey, item.cipher); - const validation = RawInboxItemSchema.safeParse( - JSON.parse(decryptedItem) - ); + let decryptedItem: string; + try { + decryptedItem = await db + .storage() + .decryptPGPMessage(inboxKeys.privateKey, item.cipher); + } catch (e) { + logger.error(e, "Failed to decrypt inbox item.", { + inboxItemId: item.id + }); + await db.inboxItemsHistory.add({ + id: item.id, + status: "failed", + errorContext: JSON.stringify({ + message: "Decryption failed", + description: (e as Error).message, + inboxItem: { id: item.id, v: item.v, alg: item.alg } + } satisfies InboxItemsHistoryErrorContext) + }); + continue; + } + + let parsed: unknown; + try { + parsed = JSON.parse(decryptedItem); + } catch (e) { + logger.error(e, "Failed to parse inbox item JSON.", { + inboxItemId: item.id + }); + await db.inboxItemsHistory.add({ + id: item.id, + status: "failed", + errorContext: JSON.stringify({ + message: "Invalid JSON", + description: (e as Error).message, + inboxItem: { id: item.id, v: item.v, alg: item.alg }, + decryptedItem + } satisfies InboxItemsHistoryErrorContext) + }); + continue; + } + + const validation = RawInboxItemSchema.safeParse(parsed); if (!validation.success) { logger.warn("Failed to validate inbox item.", { inboxItem: item, errors: validation.error.issues }); + const { content: _content, ...parsedWithoutContent } = parsed as Record< + string, + unknown + >; + await db.inboxItemsHistory.add({ + id: item.id, + status: "failed", + errorContext: JSON.stringify({ + message: "Validation failed", + description: validation.error.issues + .map((i) => i.message) + .join("; "), + inboxItem: { id: item.id, v: item.v, alg: item.alg }, + parsedItem: parsedWithoutContent + } satisfies InboxItemsHistoryErrorContext) + }); continue; } @@ -248,11 +301,19 @@ export async function handleInboxItems( { type: "note", id: item.id } ); } + await db.inboxItemsHistory.add({ + id: item.id, + status: "success", + source: data.source + }); + await db.relations.add( + { type: "inboxitemhistory", id: item.id }, + { type: "note", id: item.id } + ); } catch (e) { logger.error(e, "Failed to process inbox item.", { inboxItem: item }); - continue; } } } diff --git a/packages/core/src/api/sync/types.ts b/packages/core/src/api/sync/types.ts index efba6eb92..e8dde136f 100644 --- a/packages/core/src/api/sync/types.ts +++ b/packages/core/src/api/sync/types.ts @@ -45,7 +45,8 @@ export const SYNC_COLLECTIONS_MAP = { tag: "tags", color: "colors", note: "notes", - vault: "vaults" + vault: "vaults", + inboxitemhistory: "inboxItemsHistory" } as const; export const SYNC_ITEM_TYPES = Object.keys( diff --git a/packages/core/src/collections/inbox-items-history.ts b/packages/core/src/collections/inbox-items-history.ts new file mode 100644 index 000000000..c93cc5b3a --- /dev/null +++ b/packages/core/src/collections/inbox-items-history.ts @@ -0,0 +1,77 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { InboxItemHistory } from "../types.js"; +import Database from "../api/index.js"; +import { ICollection } from "./collection.js"; +import { SQLCollection } from "../database/sql-collection.js"; +import { isFalse } from "../database/index.js"; + +export class InboxItemsHistory implements ICollection { + name = "inboxitemshistory"; + readonly collection: SQLCollection<"inboxitemshistory", InboxItemHistory>; + constructor(private readonly db: Database) { + this.collection = new SQLCollection( + db.sql, + db.transaction, + "inboxitemshistory", + db.eventManager, + db.sanitizer + ); + } + + init() { + return this.collection.init(); + } + + async add(item: { + id: string; + status: "failed" | "success"; + source?: string; + errorContext?: string; + }) { + const now = Date.now(); + await this.collection.upsert({ + id: item.id, + type: "inboxitemhistory", + dateCreated: now, + dateModified: now, + dateSynced: now, + status: item.status, + source: item.source, + errorContext: item.errorContext + }); + return item.id; + } + + get failed() { + return this.collection.createFilter( + (qb) => qb.where(isFalse("deleted")).where("status", "==", "failed"), + this.db.options?.batchSize + ); + } + + async delete(id: string) { + await this.collection.softDelete([id]); + } + + exists(id: string) { + return this.collection.exists(id); + } +} diff --git a/packages/core/src/database/index.ts b/packages/core/src/database/index.ts index f9e03c45a..550bc2bab 100644 --- a/packages/core/src/database/index.ts +++ b/packages/core/src/database/index.ts @@ -42,6 +42,7 @@ import { Color, ContentItem, HistorySession, + InboxItemHistory, ItemReference, ItemReferences, ItemType, @@ -92,6 +93,7 @@ export interface DatabaseSchema { shortcuts: SQLiteItem; vaults: SQLiteItem; monographs: SQLiteItem; + inboxitemshistory: SQLiteItem; } export type RawDatabaseSchema = DatabaseSchema & { diff --git a/packages/core/src/database/migrations.ts b/packages/core/src/database/migrations.ts index e1fc4e5c3..6fa22a5c5 100644 --- a/packages/core/src/database/migrations.ts +++ b/packages/core/src/database/migrations.ts @@ -441,6 +441,19 @@ export class NNMigrationProvider implements MigrationProvider { .addColumn("spellcheck", "boolean", (c) => c.defaultTo(true)) .execute(); } + }, + "a-2026-05-07": { + async up(db) { + await db.schema + .createTable("inboxitemshistory") + .modifyEnd(sql`without rowid`) + .$call(addBaseColumns) + .addColumn("dateSynced", "integer") + .addColumn("status", "text") + .addColumn("source", "text") + .addColumn("errorContext", "text") + .execute(); + } } }; } diff --git a/packages/core/src/database/sql-collection.ts b/packages/core/src/database/sql-collection.ts index b13f0f6a6..4c3e11da1 100644 --- a/packages/core/src/database/sql-collection.ts +++ b/packages/core/src/database/sql-collection.ts @@ -702,7 +702,8 @@ const VALID_SORT_OPTIONS: Record< settings: [], shortcuts: [], vaults: [], - monographs: [] + monographs: [], + inboxitemshistory: [] }; function sanitizeSortOptions(type: keyof DatabaseSchema, options: SortOptions) { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index eba45d003..4f988823f 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -83,6 +83,7 @@ export type Collections = { settingsv2: "settingitem"; vaults: "vault"; monographs: "monograph"; + inboxitemshistory: "inboxitemhistory"; /** * @deprecated only kept here for migration purposes @@ -114,6 +115,7 @@ export type GroupableItem = ValueOf< | "settingitem" | "vault" | "monograph" + | "inboxitemhistory" > >; @@ -136,6 +138,7 @@ export type ItemMap = { vault: Vault; searchResult: HighlightedResult; monograph: Monograph; + inboxitemhistory: InboxItemHistory; /** * @deprecated only kept here for migration purposes @@ -512,6 +515,31 @@ export interface Monograph extends BaseItem<"monograph"> { publishUrl?: string; } +type InboxItemHistoryErrorContextBase = { + description: string; + inboxItem: { id: string; v: number; alg: string }; +}; + +export type InboxItemsHistoryErrorContext = + | (InboxItemHistoryErrorContextBase & { + message: "Decryption failed"; + }) + | (InboxItemHistoryErrorContextBase & { + message: "Invalid JSON"; + decryptedItem: string; + }) + | (InboxItemHistoryErrorContextBase & { + message: "Validation failed"; + parsedItem: Record; + }); + +export interface InboxItemHistory extends BaseItem<"inboxitemhistory"> { + dateSynced: number; + status: "failed" | "success"; + source?: string; + errorContext?: string; +} + export type Match = { prefix: string; match: string; diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po index daa93beaf..3dd7a2fb3 100644 --- a/packages/intl/locale/en.po +++ b/packages/intl/locale/en.po @@ -2154,6 +2154,10 @@ msgstr "Date format" msgid "Date modified" msgstr "Date modified" +#: src/strings.ts:2747 +msgid "Date synced" +msgstr "Date synced" + #: src/strings.ts:2044 msgid "Date uploaded" msgstr "Date uploaded" @@ -2307,6 +2311,10 @@ msgstr "Desktop app" msgid "Desktop integration" msgstr "Desktop integration" +#: src/strings.ts:2746 +msgid "Details" +msgstr "Details" + #: src/strings.ts:871 msgid "Did you save recovery key?" msgstr "Did you save recovery key?" @@ -2911,6 +2919,10 @@ msgstr "Faced an issue or have a suggestion? Click here to create a bug report" msgid "Failed" msgstr "Failed" +#: src/strings.ts:2748 +msgid "Failed inbox items" +msgstr "Failed inbox items" + #: src/strings.ts:2653 msgid "Failed to attach file" msgstr "Failed to attach file" @@ -4360,6 +4372,10 @@ msgstr "No downloads in progress." msgid "No encryption key found" msgstr "No encryption key found" +#: src/strings.ts:2750 +msgid "No failed inbox items" +msgstr "No failed inbox items" + #: src/strings.ts:1667 msgid "No headings found" msgstr "No headings found" @@ -6281,6 +6297,10 @@ msgstr "shortcuts" msgid "Shortcuts" msgstr "Shortcuts" +#: src/strings.ts:2751 +msgid "Show" +msgstr "Show" + #: src/strings.ts:96 msgid "Sign up" msgstr "Sign up" @@ -7405,6 +7425,10 @@ msgstr "View and manage inbox API keys" msgid "View and share debug logs" msgstr "View and share debug logs" +#: src/strings.ts:2749 +msgid "View failed inbox items and error contexts" +msgstr "View failed inbox items and error contexts" + #: src/strings.ts:1749 msgid "View receipt" msgstr "View receipt" diff --git a/packages/intl/locale/pseudo-LOCALE.po b/packages/intl/locale/pseudo-LOCALE.po index aba1e728f..ae4c03c68 100644 --- a/packages/intl/locale/pseudo-LOCALE.po +++ b/packages/intl/locale/pseudo-LOCALE.po @@ -2143,6 +2143,10 @@ msgstr "" msgid "Date modified" msgstr "" +#: src/strings.ts:2747 +msgid "Date synced" +msgstr "" + #: src/strings.ts:2044 msgid "Date uploaded" msgstr "" @@ -2296,6 +2300,10 @@ msgstr "" msgid "Desktop integration" msgstr "" +#: src/strings.ts:2746 +msgid "Details" +msgstr "" + #: src/strings.ts:871 msgid "Did you save recovery key?" msgstr "" @@ -2900,6 +2908,10 @@ msgstr "" msgid "Failed" msgstr "" +#: src/strings.ts:2748 +msgid "Failed inbox items" +msgstr "" + #: src/strings.ts:2653 msgid "Failed to attach file" msgstr "" @@ -4340,6 +4352,10 @@ msgstr "" msgid "No encryption key found" msgstr "" +#: src/strings.ts:2750 +msgid "No failed inbox items" +msgstr "" + #: src/strings.ts:1667 msgid "No headings found" msgstr "" @@ -6247,6 +6263,10 @@ msgstr "" msgid "Shortcuts" msgstr "" +#: src/strings.ts:2751 +msgid "Show" +msgstr "" + #: src/strings.ts:96 msgid "Sign up" msgstr "" @@ -7355,6 +7375,10 @@ msgstr "" msgid "View and share debug logs" msgstr "" +#: src/strings.ts:2749 +msgid "View failed inbox items and error contexts" +msgstr "" + #: src/strings.ts:1749 msgid "View receipt" msgstr "" diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts index 67722ff2b..eced6fba2 100644 --- a/packages/intl/src/strings.ts +++ b/packages/intl/src/strings.ts @@ -2744,5 +2744,11 @@ Continue without attachments?`, pleaseLoginToDownloadAttachments: () => t`Please login to download attachments.`, publicKeyRequired: () => t`Public key required`, - privateKeyRequired: () => t`Private key required` + privateKeyRequired: () => t`Private key required`, + details: () => t`Details`, + dateSynced: () => t`Date synced`, + failedInboxItems: () => t`Failed inbox items`, + failedInboxItemsDesc: () => t`View failed inbox items and error contexts`, + noFailedInboxItems: () => t`No failed inbox items`, + show: () => t`Show` }; From 7deb095aca99794120d97678623bbf3a0beea862 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Fri, 15 May 2026 14:50:09 +0500 Subject: [PATCH 2/6] web: inbox UI fixes * use user format preferences for dates in inbox module * delete all failed inbox items when inbox is disabled * improve error message for failed validation case Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- apps/web/src/dialogs/InboxHistoryDialog.tsx | 8 +++++--- .../src/dialogs/settings/components/inbox-api-keys.tsx | 10 +++++----- apps/web/src/stores/setting-store.ts | 1 + packages/core/src/api/sync/merger.ts | 2 +- packages/core/src/collections/inbox-items-history.ts | 5 +++++ packages/intl/locale/en.po | 4 ++++ packages/intl/locale/pseudo-LOCALE.po | 4 ++++ packages/intl/src/strings.ts | 3 ++- 8 files changed, 27 insertions(+), 10 deletions(-) diff --git a/apps/web/src/dialogs/InboxHistoryDialog.tsx b/apps/web/src/dialogs/InboxHistoryDialog.tsx index f2fc7a780..5093361b5 100644 --- a/apps/web/src/dialogs/InboxHistoryDialog.tsx +++ b/apps/web/src/dialogs/InboxHistoryDialog.tsx @@ -18,8 +18,8 @@ along with this program. If not, see . */ import { useState } from "react"; -import { usePromise } from "@notesnook/common"; -import { formatDate, InboxItemsHistoryErrorContext } from "@notesnook/core"; +import { getFormattedDate, usePromise } from "@notesnook/common"; +import { InboxItemsHistoryErrorContext } from "@notesnook/core"; import { Box, Button, Text } from "@theme-ui/components"; import { db } from "../common/db"; import { BaseDialogProps, DialogManager } from "../common/dialog-manager"; @@ -30,6 +30,7 @@ import ScrollContainer, { } from "../components/scroll-container"; import { strings } from "@notesnook/intl"; import { writeText } from "clipboard-polyfill"; +import { showToast } from "../utils/toast"; type InboxHistoryDialogProps = BaseDialogProps; @@ -152,6 +153,7 @@ export const InboxHistoryDialog = DialogManager.register( async function deleteItem(id: string) { await db.inboxItemsHistory.delete(id); + showToast("success", strings.itemDeleted()); if (result.status !== "pending") { result.refresh(); } @@ -218,7 +220,7 @@ export const InboxHistoryDialog = DialogManager.register( - {formatDate(item.dateSynced)} + {getFormattedDate(item.dateSynced)} diff --git a/apps/web/src/dialogs/settings/components/inbox-api-keys.tsx b/apps/web/src/dialogs/settings/components/inbox-api-keys.tsx index 3206a83ec..2a770aea8 100644 --- a/apps/web/src/dialogs/settings/components/inbox-api-keys.tsx +++ b/apps/web/src/dialogs/settings/components/inbox-api-keys.tsx @@ -19,7 +19,7 @@ along with this program. If not, see . import { useRef, useState, useEffect } from "react"; import { Box, Button, Flex, Input, Text, Select } from "@theme-ui/components"; -import { formatDate, InboxApiKey } from "@notesnook/core"; +import { InboxApiKey } from "@notesnook/core"; import { db } from "../../../common/db"; import { showToast } from "../../../utils/toast"; import { @@ -32,7 +32,7 @@ import { import Field from "../../../components/field"; import { BaseDialogProps, DialogManager } from "../../../common/dialog-manager"; import Dialog from "../../../components/dialog"; -import { usePromise } from "@notesnook/common"; +import { getFormattedDate, usePromise } from "@notesnook/common"; import { ConfirmDialog } from "../../confirm"; import { showPasswordDialog } from "../../password-dialog"; import { strings } from "@notesnook/intl"; @@ -233,17 +233,17 @@ function ApiKeyItem({ apiKey, onRevoke, isAtEnd }: ApiKeyItemProps) { {apiKey.lastUsedAt - ? `Last used on ${formatDate(apiKey.lastUsedAt)}` + ? `Last used on ${getFormattedDate(apiKey.lastUsedAt)}` : "Never used"} - Created on {formatDate(apiKey.dateCreated)} + Created on {getFormattedDate(apiKey.dateCreated)} {apiKey.expiryDate === -1 ? "Never expires" : `${isApiKeyExpired ? "Expired" : "Expires"} on - ${formatDate(apiKey.expiryDate)}`} + ${getFormattedDate(apiKey.expiryDate)}`} diff --git a/apps/web/src/stores/setting-store.ts b/apps/web/src/stores/setting-store.ts index c7230a524..b0d0fa10d 100644 --- a/apps/web/src/stores/setting-store.ts +++ b/apps/web/src/stores/setting-store.ts @@ -305,6 +305,7 @@ class SettingStore extends BaseStore { }); if (!ok) return; + await db.inboxItemsHistory.deleteFailed(); await db.user.discardInboxKeys(); this.set({ isInboxEnabled: false }); diff --git a/packages/core/src/api/sync/merger.ts b/packages/core/src/api/sync/merger.ts index 60cbb4366..bc5441d88 100644 --- a/packages/core/src/api/sync/merger.ts +++ b/packages/core/src/api/sync/merger.ts @@ -266,7 +266,7 @@ export async function handleInboxItems( errorContext: JSON.stringify({ message: "Validation failed", description: validation.error.issues - .map((i) => i.message) + .map((i) => `${i.path.join(".")}: ${i.message}`) .join("; "), inboxItem: { id: item.id, v: item.v, alg: item.alg }, parsedItem: parsedWithoutContent diff --git a/packages/core/src/collections/inbox-items-history.ts b/packages/core/src/collections/inbox-items-history.ts index c93cc5b3a..9d72c3b5a 100644 --- a/packages/core/src/collections/inbox-items-history.ts +++ b/packages/core/src/collections/inbox-items-history.ts @@ -71,6 +71,11 @@ export class InboxItemsHistory implements ICollection { await this.collection.softDelete([id]); } + async deleteFailed() { + const ids = await this.failed.ids(); + await this.collection.softDelete(ids); + } + exists(id: string) { return this.collection.exists(id); } diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po index 3dd7a2fb3..36a4620d6 100644 --- a/packages/intl/locale/en.po +++ b/packages/intl/locale/en.po @@ -3676,6 +3676,10 @@ msgstr "item" msgid "Item" msgstr "Item" +#: src/strings.ts:2734 +msgid "Item deleted" +msgstr "Item deleted" + #: src/strings.ts:311 #: src/strings.ts:1705 msgid "items" diff --git a/packages/intl/locale/pseudo-LOCALE.po b/packages/intl/locale/pseudo-LOCALE.po index ae4c03c68..c0ac36fc2 100644 --- a/packages/intl/locale/pseudo-LOCALE.po +++ b/packages/intl/locale/pseudo-LOCALE.po @@ -3656,6 +3656,10 @@ msgstr "" msgid "Item" msgstr "" +#: src/strings.ts:2734 +msgid "Item deleted" +msgstr "" + #: src/strings.ts:311 #: src/strings.ts:1705 msgid "items" diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts index eced6fba2..960f68e57 100644 --- a/packages/intl/src/strings.ts +++ b/packages/intl/src/strings.ts @@ -2750,5 +2750,6 @@ Continue without attachments?`, failedInboxItems: () => t`Failed inbox items`, failedInboxItemsDesc: () => t`View failed inbox items and error contexts`, noFailedInboxItems: () => t`No failed inbox items`, - show: () => t`Show` + show: () => t`Show`, + itemDeleted: () => t`Item deleted` }; From 51b226226eebe69453a37a57e6069836e688fb70 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Wed, 20 May 2026 14:08:28 +0500 Subject: [PATCH 3/6] core: fix invalid html results in empty note When html is invalid we want to return it as-is in a code-block so it gets rendered correctly --- packages/core/src/utils/html-parser.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core/src/utils/html-parser.ts b/packages/core/src/utils/html-parser.ts index 4ce7e83f5..d3774f522 100644 --- a/packages/core/src/utils/html-parser.ts +++ b/packages/core/src/utils/html-parser.ts @@ -30,7 +30,12 @@ export const parseHTML = (input: string) => : null; export const sanitizeHtml = (html: string): string => { + if (!isHtmlValid(html)) { + return wrapInCodeBlock(html); + } + const inputHtml = normalizeToHtmlBody(html); + return getDomPurify().sanitize(inputHtml, { RETURN_DOM: false, ADD_TAGS: ["iframe"] From 436c63588906f6c6486b8145428dae1d45df386e Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Thu, 21 May 2026 09:27:15 +0500 Subject: [PATCH 4/6] mobile: add failed inbox item history --- .../app/screens/settings/components.tsx | 4 +- .../screens/settings/failed-inbox-items.tsx | 392 ++++++++++++++++++ .../screens/settings/manage-inbox-keys.tsx | 35 +- .../app/screens/settings/settings-data.tsx | 8 + inbox-public.asc | 33 ++ packages/intl/locale/en.po | 25 +- packages/intl/locale/pseudo-LOCALE.po | 33 +- packages/intl/src/strings.ts | 5 +- 8 files changed, 506 insertions(+), 29 deletions(-) create mode 100644 apps/mobile/app/screens/settings/failed-inbox-items.tsx create mode 100644 inbox-public.asc diff --git a/apps/mobile/app/screens/settings/components.tsx b/apps/mobile/app/screens/settings/components.tsx index 4413648bf..f18de97aa 100644 --- a/apps/mobile/app/screens/settings/components.tsx +++ b/apps/mobile/app/screens/settings/components.tsx @@ -49,6 +49,7 @@ import ThemeSelector from "./theme-selector"; import { TitleFormat } from "./title-format"; import { NotesnookCircle } from "./notesnook-circle"; import { ManageInboxKeys, InboxKeysList } from "./manage-inbox-keys"; +import { FailedInboxItems } from "./failed-inbox-items"; export const components: { [name: string]: ReactElement } = { homeselector: , @@ -82,5 +83,6 @@ export const components: { [name: string]: ReactElement } = { "change-email": , "notesnook-circle": , "manage-inbox-keys": , - "inbox-keys": + "inbox-keys": , + "failed-inbox-items": }; diff --git a/apps/mobile/app/screens/settings/failed-inbox-items.tsx b/apps/mobile/app/screens/settings/failed-inbox-items.tsx new file mode 100644 index 000000000..cb09c13f4 --- /dev/null +++ b/apps/mobile/app/screens/settings/failed-inbox-items.tsx @@ -0,0 +1,392 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +import { getFormattedDate, usePromise } from "@notesnook/common"; +import { InboxItemsHistoryErrorContext } from "@notesnook/core"; +import { strings } from "@notesnook/intl"; +import { useThemeColors } from "@notesnook/theme"; +import Clipboard from "@react-native-clipboard/clipboard"; +import React, { useEffect, useState } from "react"; +import { ActivityIndicator, ScrollView, View } from "react-native"; +import { db } from "../../common/database"; +import { presentDialog } from "../../components/dialog/functions"; +import { Button } from "../../components/ui/button"; +import { IconButton } from "../../components/ui/icon-button"; +import Paragraph from "../../components/ui/typography/paragraph"; +import { ToastManager } from "../../services/event-manager"; +import { AppFontSize, defaultBorderRadius } from "../../utils/size"; +import { DefaultAppStyles } from "../../utils/styles"; +import { Header } from "../../components/header"; + +type FailedInboxItem = { + id: string; + dateSynced: number; + errorContext?: string; +}; + +function parseErrorContext( + raw: string | undefined +): InboxItemsHistoryErrorContext | null { + if (!raw) return null; + + try { + return JSON.parse(raw) as InboxItemsHistoryErrorContext; + } catch { + return null; + } +} + +function ErrorBadge({ + message +}: { + message: InboxItemsHistoryErrorContext["message"] | undefined; +}) { + const { colors } = useThemeColors(); + + if (!message) { + return ( + + N/A + + ); + } + + const palette = + message === "Invalid JSON" + ? { background: "rgba(255, 152, 0, 0.15)", paragraph: "#e65100" } + : message === "Validation failed" + ? { background: "rgba(255, 193, 7, 0.15)", paragraph: "#8a6000" } + : colors.error; + + return ( + + + {message} + + + ); +} + +function DetailsBlock({ value }: { value: string }) { + const { colors } = useThemeColors(); + const [copied, setCopied] = useState(false); + + useEffect(() => { + if (!copied) return; + const timeout = setTimeout(() => setCopied(false), 1000); + return () => clearTimeout(timeout); + }, [copied]); + + return ( + + + { + try { + Clipboard.setString(value); + setCopied(true); + ToastManager.show({ + message: strings.copied(), + type: "success" + }); + } catch { + ToastManager.show({ + message: strings.failedToCopyToClipboard(), + type: "error" + }); + } + }} + /> + + + + + {value} + + + + ); +} + +export const FailedInboxItems = () => { + const { colors } = useThemeColors(); + const result = usePromise(() => db.inboxItemsHistory.failed.items()); + + async function deleteItem(id: string) { + await db.inboxItemsHistory.delete(id); + ToastManager.show({ + message: strings.itemDeleted(), + type: "success" + }); + + if (result.status !== "pending") { + result.refresh(); + } + } + + const items = ( + result.status === "fulfilled" ? result.value : [] + ) as FailedInboxItem[]; + + return ( + +
{ + presentDialog({ + title: strings.deleteAll(), + paragraph: strings.deleteAllFailedItemsDesc(), + positiveText: strings.delete(), + positiveType: "errorShade", + positivePress: async () => { + if (result.status !== "pending") { + await db.inboxItemsHistory.deleteFailed(); + result.refresh(); + } + + return true; + } + }); + } + }} + /> + + {result.status === "pending" ? ( + + + + {strings.loading()} + + + ) : null} + + {result.status === "rejected" ? ( + + + {strings.failed()} + + + ) + } onClose={() => props.onClose(false)} negativeButton={{ text: strings.close(), diff --git a/apps/web/src/dialogs/settings/inbox-settings.ts b/apps/web/src/dialogs/settings/inbox-settings.ts index 34288e302..de0281c3c 100644 --- a/apps/web/src/dialogs/settings/inbox-settings.ts +++ b/apps/web/src/dialogs/settings/inbox-settings.ts @@ -24,7 +24,7 @@ import { InboxPGPKeysDialog } from "../inbox-pgp-keys-dialog"; import { db } from "../../common/db"; import { showPasswordDialog } from "../password-dialog"; import { strings } from "@notesnook/intl"; -import { InboxHistoryDialog } from "../InboxHistoryDialog"; +import { InboxHistoryDialog } from "../inbox-history-dialog"; export const InboxSettings: SettingsGroup[] = [ { diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po index 03ffd2a8e..8d937f8f4 100644 --- a/packages/intl/locale/en.po +++ b/packages/intl/locale/en.po @@ -821,6 +821,10 @@ msgstr "All fields are required" msgid "All files" msgstr "All files" +#: src/strings.ts:2756 +msgid "All items deleted" +msgstr "All items deleted" + #: src/strings.ts:1176 msgid "All locked notes will be re-encrypted with the new password." msgstr "All locked notes will be re-encrypted with the new password." diff --git a/packages/intl/locale/pseudo-LOCALE.po b/packages/intl/locale/pseudo-LOCALE.po index 59210397a..737e6d398 100644 --- a/packages/intl/locale/pseudo-LOCALE.po +++ b/packages/intl/locale/pseudo-LOCALE.po @@ -821,6 +821,10 @@ msgstr "" msgid "All files" msgstr "" +#: src/strings.ts:2756 +msgid "All items deleted" +msgstr "" + #: src/strings.ts:1176 msgid "All locked notes will be re-encrypted with the new password." msgstr "" diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts index 7cb632b72..b5bce3913 100644 --- a/packages/intl/src/strings.ts +++ b/packages/intl/src/strings.ts @@ -2754,5 +2754,6 @@ Continue without attachments?`, itemDeleted: () => t`Item deleted`, deleteAll: () => t`Delete all`, deleteAllFailedItemsDesc: () => - t`Are you sure you want to delete all failed inbox items?` + t`Are you sure you want to delete all failed inbox items?`, + allItemsDeleted: () => t`All items deleted` };