mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 19:49:54 +02:00
Merge pull request #9829 from streetwriters/web/failed-inbox-table
web: handle & show failed inbox items
This commit is contained in:
@@ -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: <HomePicker />,
|
||||
@@ -82,5 +83,6 @@ export const components: { [name: string]: ReactElement } = {
|
||||
"change-email": <ChangeEmail />,
|
||||
"notesnook-circle": <NotesnookCircle />,
|
||||
"manage-inbox-keys": <ManageInboxKeys />,
|
||||
"inbox-keys": <InboxKeysList />
|
||||
"inbox-keys": <InboxKeysList />,
|
||||
"failed-inbox-items": <FailedInboxItems />
|
||||
};
|
||||
|
||||
396
apps/mobile/app/screens/settings/failed-inbox-items.tsx
Normal file
396
apps/mobile/app/screens/settings/failed-inbox-items.tsx
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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 (
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
N/A
|
||||
</Paragraph>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
backgroundColor: palette.background,
|
||||
borderRadius: defaultBorderRadius,
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 8
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
color={palette.paragraph}
|
||||
style={{ fontWeight: "700" }}
|
||||
>
|
||||
{message}
|
||||
</Paragraph>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<View
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.secondary.border,
|
||||
borderRadius: defaultBorderRadius,
|
||||
backgroundColor: colors.secondary.background,
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.secondary.border
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
name={copied ? "check" : "content-copy"}
|
||||
color={colors.primary.icon}
|
||||
size={AppFontSize.lg}
|
||||
onPress={() => {
|
||||
try {
|
||||
Clipboard.setString(value);
|
||||
setCopied(true);
|
||||
ToastManager.show({
|
||||
message: strings.copied(),
|
||||
type: "success"
|
||||
});
|
||||
} catch {
|
||||
ToastManager.show({
|
||||
message: strings.failedToCopyToClipboard(),
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
horizontal
|
||||
style={{ maxHeight: 120 }}
|
||||
contentContainerStyle={{
|
||||
padding: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
style={{
|
||||
fontFamily: "monospace"
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</Paragraph>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<View
|
||||
style={{
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
<Header
|
||||
renderedInRoute="Settings"
|
||||
title={strings.failedInboxItems()}
|
||||
canGoBack={true}
|
||||
id="Settings"
|
||||
rightButton={
|
||||
items?.length > 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" ? (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator size="small" color={colors.primary.accent} />
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{strings.loading()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{result.status === "rejected" ? (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.error.paragraph}>
|
||||
{strings.failed()}
|
||||
</Paragraph>
|
||||
<Button
|
||||
title={strings.retry()}
|
||||
type="accent"
|
||||
onPress={result.refresh}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{items.length === 0 ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
flex: 1,
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
padding: DefaultAppStyles.GAP,
|
||||
borderRadius: defaultBorderRadius,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{strings.noFailedInboxItems()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{result.status === "fulfilled" && items.length > 0 ? (
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingBottom: 50
|
||||
}}
|
||||
>
|
||||
{items.map((item) => {
|
||||
const context = parseErrorContext(item.errorContext);
|
||||
const { message, description, ...rest } =
|
||||
context ??
|
||||
({} as Partial<InboxItemsHistoryErrorContext> &
|
||||
Record<string, unknown>);
|
||||
const details =
|
||||
Object.keys(rest).length > 0
|
||||
? JSON.stringify(rest, null, 2)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<View
|
||||
key={item.id}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.secondary.border,
|
||||
borderRadius: defaultBorderRadius,
|
||||
backgroundColor: colors.primary.background,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{getFormattedDate(item.dateSynced, "date-time")}
|
||||
</Paragraph>
|
||||
|
||||
<ErrorBadge message={message} />
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.error.background,
|
||||
padding: 3,
|
||||
paddingHorizontal: 6,
|
||||
borderRadius: 4
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.error.paragraph}>Error</Paragraph>
|
||||
</View>
|
||||
<Paragraph> {(description as string) || "N/A"}</Paragraph>
|
||||
</View>
|
||||
|
||||
{details ? (
|
||||
<DetailsBlock value={details} />
|
||||
) : (
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
N/A
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
<View style={{ alignItems: "flex-end" }}>
|
||||
<Button
|
||||
title={strings.delete()}
|
||||
type="error"
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={() => {
|
||||
presentDialog({
|
||||
title: strings.delete(),
|
||||
paragraph: strings.areYouSure(),
|
||||
positiveText: strings.delete(),
|
||||
positiveType: "error",
|
||||
negativeText: strings.cancel(),
|
||||
positivePress: async () => {
|
||||
try {
|
||||
await deleteItem(item.id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
ToastManager.error(error as Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -195,6 +195,8 @@ const InboxKeysList = () => {
|
||||
|
||||
const apiKeys = apiKeysPromise.value || [];
|
||||
|
||||
console.log(apiKeys);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
@@ -202,24 +204,43 @@ const InboxKeysList = () => {
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingBottom: 50
|
||||
paddingBottom: 50,
|
||||
minHeight: "100%"
|
||||
}}
|
||||
>
|
||||
{apiKeys.length === 0 ? (
|
||||
<View
|
||||
style={{
|
||||
padding: DefaultAppStyles.GAP * 2,
|
||||
borderWidth: 1,
|
||||
borderStyle: "dashed",
|
||||
borderColor: colors.secondary.border,
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
alignItems: "center"
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
flex: 1,
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{strings.createFirstApiKey()}
|
||||
</Paragraph>
|
||||
|
||||
<Button
|
||||
title={strings.createKey()}
|
||||
type="accent"
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (apiKeys.length >= 10) {
|
||||
presentDialog({
|
||||
title: strings.apiKeysLimitReached(),
|
||||
paragraph: strings.apiKeysLimitReachedMessage(),
|
||||
positiveText: strings.ok()
|
||||
});
|
||||
} else {
|
||||
AddApiKeySheet.present(() => apiKeysPromise.refresh());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View style={{ gap: 0 }}>
|
||||
|
||||
@@ -762,6 +762,14 @@ export const settingsGroups: SettingSection[] = [
|
||||
description: strings.viewAPIKeysDesc(),
|
||||
type: "screen",
|
||||
component: "inbox-keys"
|
||||
},
|
||||
{
|
||||
id: "failed-inbox-items",
|
||||
name: strings.failedInboxItems(),
|
||||
description: strings.failedInboxItemsDesc(),
|
||||
type: "screen",
|
||||
component: "failed-inbox-items",
|
||||
hideHeader: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ type DialogProps = SxProp & {
|
||||
textAlignment?: "left" | "right" | "center";
|
||||
buttonsAlignment?: "start" | "center" | "end";
|
||||
title?: string;
|
||||
titleAction?: React.ReactNode;
|
||||
description?: string;
|
||||
positiveButton?: DialogButtonProps | null;
|
||||
negativeButton?: DialogButtonProps | null;
|
||||
@@ -140,19 +141,24 @@ function BaseDialog(props: React.PropsWithChildren<DialogProps>) {
|
||||
{props.title || props.description ? (
|
||||
<Flex sx={{ flexDirection: "column" }} p={4} pb={0}>
|
||||
{props.title && (
|
||||
<Text
|
||||
variant="heading"
|
||||
data-test-id="dialog-title"
|
||||
sx={{
|
||||
fontSize: "subheading",
|
||||
textAlign: props.textAlignment || "left",
|
||||
color: "paragraph",
|
||||
overflowWrap: "anywhere",
|
||||
wordSpacing: "wrap"
|
||||
}}
|
||||
<Flex
|
||||
sx={{ alignItems: "center", justifyContent: "space-between" }}
|
||||
>
|
||||
{props.title}
|
||||
</Text>
|
||||
<Text
|
||||
variant="heading"
|
||||
data-test-id="dialog-title"
|
||||
sx={{
|
||||
fontSize: "subheading",
|
||||
textAlign: props.textAlignment || "left",
|
||||
color: "paragraph",
|
||||
overflowWrap: "anywhere",
|
||||
wordSpacing: "wrap"
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
</Text>
|
||||
{props.titleAction}
|
||||
</Flex>
|
||||
)}
|
||||
{props.description && (
|
||||
<Text
|
||||
|
||||
285
apps/web/src/dialogs/inbox-history-dialog.tsx
Normal file
285
apps/web/src/dialogs/inbox-history-dialog.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
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 { 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";
|
||||
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";
|
||||
import { showToast } from "../utils/toast";
|
||||
|
||||
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);
|
||||
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 (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={strings.failedInboxItems()}
|
||||
titleAction={
|
||||
result.status === "fulfilled" &&
|
||||
result.value.length > 0 && (
|
||||
<Button variant="errorSecondary" onClick={deleteAll}>
|
||||
{strings.deleteAll()}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
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" }}>
|
||||
{getFormattedDate(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>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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) {
|
||||
<Flex sx={{ mb: 1, flexDirection: "column" }}>
|
||||
<Text variant="subBody" sx={{ color: "paragraph-secondary" }}>
|
||||
{apiKey.lastUsedAt
|
||||
? `Last used on ${formatDate(apiKey.lastUsedAt)}`
|
||||
? `Last used on ${getFormattedDate(apiKey.lastUsedAt)}`
|
||||
: "Never used"}
|
||||
</Text>
|
||||
<Text variant="subBody" sx={{ color: "paragraph-secondary" }}>
|
||||
Created on {formatDate(apiKey.dateCreated)}
|
||||
Created on {getFormattedDate(apiKey.dateCreated)}
|
||||
</Text>
|
||||
<Text variant="subBody" sx={{ color: "paragraph-secondary" }}>
|
||||
{apiKey.expiryDate === -1
|
||||
? "Never expires"
|
||||
: `${isApiKeyExpired ? "Expired" : "Expires"} on
|
||||
${formatDate(apiKey.expiryDate)}`}
|
||||
${getFormattedDate(apiKey.expiryDate)}`}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
@@ -305,6 +305,7 @@ class SettingStore extends BaseStore<SettingStore> {
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
await db.inboxItemsHistory.deleteFailed();
|
||||
await db.user.discardInboxKeys();
|
||||
this.set({ isInboxEnabled: false });
|
||||
|
||||
|
||||
33
inbox-public.asc
Normal file
33
inbox-public.asc
Normal file
@@ -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-----
|
||||
@@ -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.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
82
packages/core/src/collections/inbox-items-history.ts
Normal file
82
packages/core/src/collections/inbox-items-history.ts
Normal file
@@ -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 <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]);
|
||||
}
|
||||
|
||||
async deleteFailed() {
|
||||
const ids = await this.failed.ids();
|
||||
await this.collection.softDelete(ids);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user