mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-07-10 04:21:21 +02:00
web: handle & show failed inbox items
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
267
apps/web/src/dialogs/InboxHistoryDialog.tsx
Normal file
267
apps/web/src/dialogs/InboxHistoryDialog.tsx
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<boolean>;
|
||||
|
||||
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 (
|
||||
<Text variant="body" sx={{ color: "paragraph-secondary" }}>
|
||||
—
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const style = ERROR_CHIP_STYLES[message] ?? {
|
||||
bg: "background-error",
|
||||
color: "accent-error"
|
||||
};
|
||||
return (
|
||||
<Text
|
||||
sx={{
|
||||
bg: style.bg,
|
||||
color: style.color,
|
||||
borderRadius: "default",
|
||||
px: "6px",
|
||||
py: "2px",
|
||||
fontSize: "0.65em",
|
||||
fontWeight: 600
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Box
|
||||
sx={{ position: "relative" }}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
<ScrollContainer suppressScrollX style={{ maxHeight: 100 }}>
|
||||
<Box
|
||||
as="pre"
|
||||
sx={{
|
||||
m: 0,
|
||||
fontSize: "0.72em",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-all"
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</Box>
|
||||
</ScrollContainer>
|
||||
{(hovered || copied) && (
|
||||
<Button
|
||||
variant="icon"
|
||||
title="Copy"
|
||||
onClick={handleCopy}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
p: "2px",
|
||||
bg: "background",
|
||||
color: copied ? "accent" : undefined
|
||||
}}
|
||||
>
|
||||
{copied ? <Check size={14} /> : <Copy size={14} />}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={strings.failedInboxItems()}
|
||||
onClose={() => props.onClose(false)}
|
||||
negativeButton={{
|
||||
text: strings.close(),
|
||||
onClick: () => props.onClose(false)
|
||||
}}
|
||||
noScroll
|
||||
width="80%"
|
||||
>
|
||||
{result.status === "pending" ? (
|
||||
<Text sx={{ p: 3 }}>{strings.loading()}</Text>
|
||||
) : result.status === "rejected" ? (
|
||||
<Text sx={{ p: 3 }}>{strings.failed()}</Text>
|
||||
) : result.value.length === 0 ? (
|
||||
<Text sx={{ p: 3 }}>{strings.noFailedInboxItems()}</Text>
|
||||
) : (
|
||||
<FlexScrollContainer style={{ maxHeight: "70vh" }}>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box
|
||||
as="table"
|
||||
sx={{
|
||||
width: "100%",
|
||||
borderCollapse: "collapse",
|
||||
tableLayout: "fixed",
|
||||
"th, td": {
|
||||
px: 2,
|
||||
py: 1,
|
||||
textAlign: "left",
|
||||
verticalAlign: "top",
|
||||
borderBottom: "1px solid var(--separator)"
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box as="thead">
|
||||
<Box as="tr">
|
||||
{COLUMNS.map((col) => (
|
||||
<Box
|
||||
key={col.title}
|
||||
as="th"
|
||||
sx={{ width: col.width, whiteSpace: "nowrap" }}
|
||||
>
|
||||
<Text variant="subtitle">{col.title}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box as="tbody">
|
||||
{result.value.map((item) => {
|
||||
const ctx = parseErrorContext(item.errorContext);
|
||||
const { message, description, ...rest } =
|
||||
ctx ??
|
||||
({} as Partial<InboxItemsHistoryErrorContext> &
|
||||
Record<string, unknown>);
|
||||
const hasExtra = Object.keys(rest).length > 0;
|
||||
return (
|
||||
<Box as="tr" key={item.id}>
|
||||
<Box as="td">
|
||||
<Text variant="body" sx={{ whiteSpace: "nowrap" }}>
|
||||
{formatDate(item.dateSynced)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box as="td">
|
||||
<ErrorBadge message={message} />
|
||||
</Box>
|
||||
<Box as="td">
|
||||
<Text variant="body">
|
||||
{(description as string) ?? "—"}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box as="td">
|
||||
{hasExtra ? (
|
||||
<DetailsCell
|
||||
value={JSON.stringify(rest, null, 2)}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{ color: "paragraph-secondary" }}
|
||||
>
|
||||
—
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box as="td" sx={{ textAlign: "center" }}>
|
||||
<Button
|
||||
variant="icon"
|
||||
title={strings.delete()}
|
||||
onClick={() => deleteItem(item.id)}
|
||||
sx={{ color: "accent-error", p: "2px" }}
|
||||
>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</FlexScrollContainer>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
77
packages/core/src/collections/inbox-items-history.ts
Normal file
77
packages/core/src/collections/inbox-items-history.ts
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<InboxItemHistory>(
|
||||
(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);
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
Color,
|
||||
ContentItem,
|
||||
HistorySession,
|
||||
InboxItemHistory,
|
||||
ItemReference,
|
||||
ItemReferences,
|
||||
ItemType,
|
||||
@@ -92,6 +93,7 @@ export interface DatabaseSchema {
|
||||
shortcuts: SQLiteItem<Shortcut>;
|
||||
vaults: SQLiteItem<Vault>;
|
||||
monographs: SQLiteItem<Monograph>;
|
||||
inboxitemshistory: SQLiteItem<InboxItemHistory>;
|
||||
}
|
||||
|
||||
export type RawDatabaseSchema = DatabaseSchema & {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -702,7 +702,8 @@ const VALID_SORT_OPTIONS: Record<
|
||||
settings: [],
|
||||
shortcuts: [],
|
||||
vaults: [],
|
||||
monographs: []
|
||||
monographs: [],
|
||||
inboxitemshistory: []
|
||||
};
|
||||
|
||||
function sanitizeSortOptions(type: keyof DatabaseSchema, options: SortOptions) {
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
});
|
||||
|
||||
export interface InboxItemHistory extends BaseItem<"inboxitemhistory"> {
|
||||
dateSynced: number;
|
||||
status: "failed" | "success";
|
||||
source?: string;
|
||||
errorContext?: string;
|
||||
}
|
||||
|
||||
export type Match = {
|
||||
prefix: string;
|
||||
match: string;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user