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..9beb1dfb9 --- /dev/null +++ b/apps/mobile/app/screens/settings/failed-inbox-items.tsx @@ -0,0 +1,396 @@ +/* +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 ( + +
0 + ? { + name: "delete", + color: colors.primary.icon, + onPress: async () => { + 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; + } + }); + } + } + : undefined + } + /> + + {result.status === "pending" ? ( + + + + {strings.loading()} + + + ) : null} + + {result.status === "rejected" ? ( + + + {strings.failed()} + + + )} + + ); +} + +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); + showToast("success", strings.itemDeleted()); + if (result.status !== "pending") { + result.refresh(); + } + } + + async function deleteAll() { + await db.inboxItemsHistory.deleteFailed(); + showToast("success", strings.allItemsDeleted()); + if (result.status !== "pending") { + result.refresh(); + } + } + + return ( + 0 && ( + + ) + } + onClose={() => 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 ( + + + + {getFormattedDate(item.dateSynced)} + + + + + + + + {(description as string) ?? "—"} + + + + {hasExtra ? ( + + ) : ( + + — + + )} + + + + + + ); + })} + + + + + )} + + ); + } +); 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/dialogs/settings/inbox-settings.ts b/apps/web/src/dialogs/settings/inbox-settings.ts index bdec99565..de0281c3c 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 "../inbox-history-dialog"; 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/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/inbox-public.asc b/inbox-public.asc new file mode 100644 index 000000000..bdaf88521 --- /dev/null +++ b/inbox-public.asc @@ -0,0 +1,33 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: openpgp-mobile + +xsBNBGoNaK8BCAD0B33KK4LRAvN1lZLpJhQMyk/+Srss56PjFphMH1MmqMgIRRBP +3RykX+7+ibha+5WIFYpgBEaPM9osZz22XUGhVrUzUxJMScjhLWR6xyuv0qs0Dctg +ePdcupPbJND3j9W4OnOBXwv+Ko/fX5K+enJfPp6fxyWbf3X/1BnAYlHyLngBLz8P +mt5qj0qax5V4ujUVU8ByNFvQkjcA+ip0vol2xQlmKa5UXJ1KYfM7LQWa4gQqdaY8 +8FOl6CaaWsXO/vCnIF47JClWGJfJ0rAaREI/Kj+MV+i98BPAcZG8Kj523QmgIHo6 +Puahi1q9wqX6Vwe6hjhiTUJSWvNjZXGi9A4PABEBAAHNDU5OIDxOTkBOTi5OTj7C +wLsEEwEIAG8FgmoNaK8CCwcJkC34qZJdxA5jNRQAAAAAABwAEHNhbHRAbm90YXRp +b25zLm9wZW5wZ3Bqcy5vcmfLP2eEUAk99foAqkNQSyW9AhUIAhYAAhkBApsDAh4B +FiEEjXaqZcVUExRCuEhoLfipkl3EDmMAAKW7CACxh26CVTjRLjeq/GNueceWRJoT +qF/OMQY/3mnuLMbaEMuYSc03ml6jiMdqZy6qeKgjH36qvpfu68HxPYEOn3WQ/k9V +2Iug4xDFlqFw0IN2Gqkgur+FYbFCjG1mkqsrlKsN2QHqG8sf+5EXegpvNibI+43Q +HZp+Q5B8HftnBANvZngFJz3t0hCCpUgVOTK4vwK8IKDK7zbMqfnT4k5vNbfpxd30 +5xfZkQXrltJLaHb5pzgVMeoM00TaHd8WBDFYn8BF1OI/TbcChYpRPkW7WoKKuZ/1 +tIBfv/O5h3ZxgSc4NdL1FuLZ0nTscPoN+uBE29XQat8IOiYiA4DvKLbIQe89zsBN +BGoNaK8BCACppIfZHotgC8zKvCkj1sWAny5Qq/AkYdIJh/b//7NhnWyUgiSDnoEV +1yik33CiQgoORR42YfVCZCMH2ZydzeKdaNKd/fFLeMsog0ddp4cW68drDDVvkGso +vSMwvpSS/J4JJ2KXqIbvscJXGzAaFZ61BoY3kvRKHymHGe2oLs2bPscNug7mm8pm +18+IhNeOtuS6MZzdXr9rmfuTtY9zUIbIaOgY3EiiaRkvcQLPcBTwsoM9b9zNMK+1 +ivA65KrtgN+T7axcIRT+QFFoNOw7mjHNQybb6qRABWS8AWouot4B2g9tNHndlhNV +aimI+P1RHaP9VOxJFneWkmvviXqznv4TABEBAAHCwKwEGAEIAGAFgmoNaK8JkC34 +qZJdxA5jNRQAAAAAABwAEHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmeywJ1O +IyGqDaRT2EGH4ZZpApsMFiEEjXaqZcVUExRCuEhoLfipkl3EDmMAABm8B/9rKv4l +PNwYXLVQlhyGlF/MvdYvT4Fj2CxtO32Fo++dUlEYJqX++GihXr0HyjdE200Ttb1Q +IpZto9rx5X0QHKGEqVGM+kJ6STOWK5jRlADES6GKE3dZde4Z+QS+BBdEdveZtGwR +GsLArPKjhnbkiBXTrMUkoQa4eGanuXA452io5NsiIeNlMUsC6IAXxuZp8+iBtN9K +65iSrvmasdKgwttbqp0qiI5VudiEAQjrkHDMlGftMqSllZWkavlbpYPIN/Omzh8G +kVbBMAvQtDsxSFACnZCGQ1l3r9qDJWUQyn57qQgrQwYXx+sHIlOcdAMGiPbE49KP +kUYOKopIqix0i2/F +=rfLM +-----END PGP PUBLIC KEY BLOCK----- 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..bc5441d88 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.path.join(".")}: ${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..9d72c3b5a --- /dev/null +++ b/packages/core/src/collections/inbox-items-history.ts @@ -0,0 +1,82 @@ +/* +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]); + } + + 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/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/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"] diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po index daa93beaf..8d937f8f4 100644 --- a/packages/intl/locale/en.po +++ b/packages/intl/locale/en.po @@ -1,6 +1,7 @@ msgid "" msgstr "" -"POT-Creation-Date: 2026-05-20 09:27+0500\n" +"POT-Creation-Date: 2026-05-21 08:29+0500\n" +"POT-Creation-Date: 2026-05-21 08:29+0500\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -820,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." @@ -975,6 +980,10 @@ msgstr "Are you sure you want to clear all logs from {key}?" msgid "Are you sure you want to clear trash?" msgstr "Are you sure you want to clear trash?" +#: src/strings.ts:2757 +msgid "Are you sure you want to delete all failed inbox items?" +msgstr "Are you sure you want to delete all failed inbox items?" + #: src/strings.ts:2734 msgid "Are you sure you want to delete this attachment?" msgstr "Are you sure you want to delete this attachment?" @@ -2154,6 +2163,10 @@ msgstr "Date format" msgid "Date modified" msgstr "Date modified" +#: src/strings.ts:2749 +msgid "Date synced" +msgstr "Date synced" + #: src/strings.ts:2044 msgid "Date uploaded" msgstr "Date uploaded" @@ -2239,6 +2252,10 @@ msgstr "Delete" msgid "Delete account" msgstr "Delete account" +#: src/strings.ts:2755 +msgid "Delete all" +msgstr "Delete all" + #: src/strings.ts:2732 msgid "Delete attachment" msgstr "Delete attachment" @@ -2307,6 +2324,10 @@ msgstr "Desktop app" msgid "Desktop integration" msgstr "Desktop integration" +#: src/strings.ts:2748 +msgid "Details" +msgstr "Details" + #: src/strings.ts:871 msgid "Did you save recovery key?" msgstr "Did you save recovery key?" @@ -2911,6 +2932,10 @@ msgstr "Faced an issue or have a suggestion? Click here to create a bug report" msgid "Failed" msgstr "Failed" +#: src/strings.ts:2750 +msgid "Failed inbox items" +msgstr "Failed inbox items" + #: src/strings.ts:2653 msgid "Failed to attach file" msgstr "Failed to attach file" @@ -3664,6 +3689,10 @@ msgstr "item" msgid "Item" msgstr "Item" +#: src/strings.ts:2754 +msgid "Item deleted" +msgstr "Item deleted" + #: src/strings.ts:311 #: src/strings.ts:1705 msgid "items" @@ -4360,6 +4389,10 @@ msgstr "No downloads in progress." msgid "No encryption key found" msgstr "No encryption key found" +#: src/strings.ts:2752 +msgid "No failed inbox items" +msgstr "No failed inbox items" + #: src/strings.ts:1667 msgid "No headings found" msgstr "No headings found" @@ -6281,6 +6314,10 @@ msgstr "shortcuts" msgid "Shortcuts" msgstr "Shortcuts" +#: src/strings.ts:2753 +msgid "Show" +msgstr "Show" + #: src/strings.ts:96 msgid "Sign up" msgstr "Sign up" @@ -7405,6 +7442,10 @@ msgstr "View and manage inbox API keys" msgid "View and share debug logs" msgstr "View and share debug logs" +#: src/strings.ts:2751 +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..737e6d398 100644 --- a/packages/intl/locale/pseudo-LOCALE.po +++ b/packages/intl/locale/pseudo-LOCALE.po @@ -1,6 +1,7 @@ msgid "" msgstr "" -"POT-Creation-Date: 2026-05-20 09:27+0500\n" +"POT-Creation-Date: 2026-05-21 08:29+0500\n" +"POT-Creation-Date: 2026-05-21 08:29+0500\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -820,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 "" @@ -973,7 +978,11 @@ msgstr "" #: src/strings.ts:1326 msgid "Are you sure you want to clear trash?" -msgstr "" +msgstr "<<<<<<< HEAD=======" + +#: src/strings.ts:2757 +msgid "Are you sure you want to delete all failed inbox items?" +msgstr ">>>>>>> 101851125 (mobile: add failed inbox item history)" #: src/strings.ts:2734 msgid "Are you sure you want to delete this attachment?" @@ -2143,6 +2152,10 @@ msgstr "" msgid "Date modified" msgstr "" +#: src/strings.ts:2749 +msgid "Date synced" +msgstr "" + #: src/strings.ts:2044 msgid "Date uploaded" msgstr "" @@ -2226,7 +2239,11 @@ msgstr "" #: src/strings.ts:1078 msgid "Delete account" -msgstr "" +msgstr "<<<<<<< HEAD=======" + +#: src/strings.ts:2755 +msgid "Delete all" +msgstr ">>>>>>> 101851125 (mobile: add failed inbox item history)" #: src/strings.ts:2732 msgid "Delete attachment" @@ -2296,6 +2313,10 @@ msgstr "" msgid "Desktop integration" msgstr "" +#: src/strings.ts:2748 +msgid "Details" +msgstr "" + #: src/strings.ts:871 msgid "Did you save recovery key?" msgstr "" @@ -2900,6 +2921,10 @@ msgstr "" msgid "Failed" msgstr "" +#: src/strings.ts:2750 +msgid "Failed inbox items" +msgstr "" + #: src/strings.ts:2653 msgid "Failed to attach file" msgstr "" @@ -3644,6 +3669,10 @@ msgstr "" msgid "Item" msgstr "" +#: src/strings.ts:2754 +msgid "Item deleted" +msgstr "" + #: src/strings.ts:311 #: src/strings.ts:1705 msgid "items" @@ -4340,6 +4369,10 @@ msgstr "" msgid "No encryption key found" msgstr "" +#: src/strings.ts:2752 +msgid "No failed inbox items" +msgstr "" + #: src/strings.ts:1667 msgid "No headings found" msgstr "" @@ -5151,11 +5184,11 @@ msgstr "" #: src/strings.ts:2193 msgid "Proxy" -msgstr "" +msgstr "<<<<<<< HEAD" #: src/strings.ts:2746 msgid "Public key required" -msgstr "" +msgstr "=======>>>>>>> 101851125 (mobile: add failed inbox item history)" #: src/strings.ts:2712 msgid "Public Key:" @@ -6247,6 +6280,10 @@ msgstr "" msgid "Shortcuts" msgstr "" +#: src/strings.ts:2753 +msgid "Show" +msgstr "" + #: src/strings.ts:96 msgid "Sign up" msgstr "" @@ -7355,6 +7392,10 @@ msgstr "" msgid "View and share debug logs" msgstr "" +#: src/strings.ts:2751 +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..b5bce3913 100644 --- a/packages/intl/src/strings.ts +++ b/packages/intl/src/strings.ts @@ -2744,5 +2744,16 @@ 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`, + itemDeleted: () => t`Item deleted`, + deleteAll: () => t`Delete all`, + deleteAllFailedItemsDesc: () => + t`Are you sure you want to delete all failed inbox items?`, + allItemsDeleted: () => t`All items deleted` };