diff --git a/apps/web/src/components/editor/index.tsx b/apps/web/src/components/editor/index.tsx index b2d8824ba..2bdf51dac 100644 --- a/apps/web/src/components/editor/index.tsx +++ b/apps/web/src/components/editor/index.tsx @@ -50,7 +50,8 @@ import { FlexScrollContainer } from "../scroll-container"; import Tiptap, { OnChangeHandler } from "./tiptap"; import Header from "./header"; import { Attachment } from "../icons"; -import { attachFiles, AttachmentProgress, insertAttachments } from "./picker"; +import { AttachmentProgress, insertAttachments } from "./picker"; +import { AttachFilesDialog } from "../../dialogs/attach-files-dialog"; import { useEditorManager } from "./manager"; import { saveAttachment, @@ -617,13 +618,11 @@ export function Editor(props: EditorProps) { } const mime = type === "file" ? "*/*" : "image/*"; - const attachments = await insertAttachments(mime); - const editor = useEditorManager.getState().getEditor(id)?.editor; - - if (!attachments) return; - for (const attachment of attachments) { - editor?.attachFile(attachment); - } + await insertAttachments(mime, (attachments) => { + const editor = useEditorManager.getState().getEditor(id)?.editor; + if (!editor) return; + attachments.forEach((a) => editor?.attachFile(a)); + }); }} onGetAttachmentData={async (attachment) => { logger.debug("Getting attachment data", { @@ -646,10 +645,14 @@ export function Editor(props: EditorProps) { return result; }} onAttachFiles={async (files) => { - const editor = useEditorManager.getState().getEditor(id)?.editor; - const result = await attachFiles(files); - if (!result) return; - result.forEach((attachment) => editor?.attachFile(attachment)); + await AttachFilesDialog.show({ + files, + onDone: (attachments) => { + const editor = useEditorManager.getState().getEditor(id)?.editor; + if (!editor) return; + attachments.forEach((a) => editor?.attachFile(a)); + } + }); }} onInsertInternalLink={async (attributes) => { const link = await NoteLinkingDialog.show({ attributes }); @@ -799,17 +802,20 @@ function DropZone(props: DropZoneProps) { }} onDrop={async (e) => { try { - const { activeEditorId, getEditor } = useEditorManager.getState(); - const editor = getEditor(activeEditorId || "")?.editor; - if (!e.dataTransfer.files?.length || !editor) return; + const { activeEditorId } = useEditorManager.getState(); + if (!e.dataTransfer.files?.length || !activeEditorId) return; e.preventDefault(); - const attachments = await attachFiles( - Array.from(e.dataTransfer.files) - ); - for (const attachment of attachments || []) { - editor.attachFile(attachment); - } + await AttachFilesDialog.show({ + files: Array.from(e.dataTransfer.files), + onDone: (attachments) => { + const editor = useEditorManager + .getState() + .getEditor(activeEditorId)?.editor; + if (!editor) return; + attachments.forEach((a) => editor?.attachFile(a)); + } + }); } catch (e) { logger.error(e as Error, "Failed to attach file from drag and drop"); showToast("error", strings.failedToAttachFile()); diff --git a/apps/web/src/components/editor/picker.ts b/apps/web/src/components/editor/picker.ts index c318930f0..1a04bc4da 100644 --- a/apps/web/src/components/editor/picker.ts +++ b/apps/web/src/components/editor/picker.ts @@ -18,86 +18,35 @@ along with this program. If not, see . */ import { SerializedKey } from "@notesnook/crypto"; -import { AppEventManager, AppEvents } from "../../common/app-events"; import { db } from "../../common/db"; -import { TaskManager } from "../../common/task-manager"; import { showToast } from "../../utils/toast"; import { showFilePicker } from "../../utils/file-picker"; import { Attachment } from "@notesnook/editor"; -import { ImagePickerDialog } from "../../dialogs/image-picker-dialog"; -import { strings } from "@notesnook/intl"; import { getUploadedFileSize, hashStream, writeEncryptedFile } from "../../interfaces/fs"; -import Config from "../../utils/config"; import { compressImage, FileWithURI } from "../../utils/image-compressor"; -import { ImageCompressionOptions } from "../../stores/setting-store"; import { checkFeature } from "../../common"; +import { AttachFilesDialog } from "../../dialogs/attach-files-dialog"; +import { strings } from "@notesnook/intl"; -export async function insertAttachments(type = "*/*") { +export async function insertAttachments( + type: string, + onDone: (attachments: Attachment[]) => void +): Promise { const files = await showFilePicker({ acceptedFileTypes: type || "*/*", multiple: true }); - if (!files) return; - return await attachFiles(files, type === "*/*"); -} + if (!files || files.length === 0) return; -export async function attachFiles( - files: File[], - skipSpecialImageHandling = false -) { - let images = files.filter((f) => f.type.startsWith("image/")); - const imageCompressionConfig = Config.get( - "imageCompression", - ImageCompressionOptions.ASK_EVERY_TIME - ); - - switch (imageCompressionConfig) { - case ImageCompressionOptions.ENABLE: { - const compressedImages: FileWithURI[] = []; - for (const image of images) { - const compressed = await compressImage(image, { - maxWidth: (naturalWidth) => Math.min(1920, naturalWidth * 0.7), - width: (naturalWidth) => naturalWidth, - height: (_, naturalHeight) => naturalHeight, - resize: "contain", - quality: 0.7 - }); - compressedImages.push( - new FileWithURI([compressed], image.name, { - lastModified: image.lastModified, - type: image.type - }) - ); - } - images = compressedImages; - break; - } - case ImageCompressionOptions.DISABLE: - break; - default: - images = - images.length > 0 - ? (await ImagePickerDialog.show({ - images - })) || [] - : []; - } - - const documents = files.filter((f) => !f.type.startsWith("image/")); - const attachments: Attachment[] = []; - for (const file of [...images, ...documents]) { - const attachment = - !skipSpecialImageHandling && file.type.startsWith("image/") - ? await pickImage(file) - : await pickFile(file); - if (!attachment) continue; - attachments.push(attachment); - } - return attachments; + await AttachFilesDialog.show({ + files, + skipSpecialImageHandling: type === "*/*", + onDone + }); } export async function reuploadAttachment( @@ -109,9 +58,16 @@ export async function reuploadAttachment( }); if (!selectedFile) return; + if ( + !(await checkFeature("fileSize", { + value: selectedFile.size + })) + ) { + return; + } + const options: AddAttachmentOptions = { expectedFileHash, - showProgress: false, forceWrite: true }; @@ -124,6 +80,72 @@ export async function reuploadAttachment( } } +type AttachFilesMessage = + | { type: "compressing"; index: number } + | { type: "encrypting"; index: number } + | { + type: "done"; + index: number; + attachment: Attachment | undefined; + } + | { type: "error"; index: number; error: string }; + +export async function* attachFiles( + files: File[], + shouldCompress: boolean[], + skipSpecialImageHandling = false +): AsyncGenerator { + for (let i = 0; i < files.length; i++) { + let file = files[i]; + const shouldCompressFile = shouldCompress[i]; + + if (shouldCompressFile) { + yield { type: "compressing", index: i }; + try { + const compressed = await compressImage(file, { + maxWidth: (naturalWidth) => Math.min(1920, naturalWidth * 0.7), + width: (naturalWidth) => naturalWidth, + height: (_, naturalHeight) => naturalHeight, + resize: "contain", + quality: 0.7 + }); + file = new FileWithURI([compressed], file.name, { + lastModified: file.lastModified, + type: file.type + }); + } catch (e) { + yield { + type: "error", + index: i, + error: (e as Error).message || strings.compressionFailed() + }; + continue; + } + } + + yield { type: "encrypting", index: i }; + + try { + const allowed = await checkFeature("fileSize", { + value: file.size, + type: "toast" + }); + if (!allowed) { + throw new Error(strings.fileSizeLimitExceededPleaseUpgrade()); + } + + const attachment = + !skipSpecialImageHandling && file.type.startsWith("image/") + ? await pickImage(file) + : await pickFile(file); + + yield { type: "done", index: i, attachment: attachment || undefined }; + } catch (e) { + yield { type: "error", index: i, error: (e as Error).message }; + } + } +} + /** * @param {File} file * @returns @@ -133,8 +155,6 @@ async function pickFile( options?: AddAttachmentOptions ): Promise { try { - if (!(await checkFeature("fileSize", { value: file.size }))) return; - const hash = await addAttachment(file, options); return { type: "file", @@ -158,8 +178,6 @@ async function pickImage( options?: AddAttachmentOptions ): Promise { try { - if (!(await checkFeature("fileSize", { value: file.size }))) return; - const hash = await addAttachment(file, options); const dimensions = await getImageDimensions(file); return { @@ -171,6 +189,7 @@ async function pickImage( ...dimensions }; } catch (e) { + console.error(e); showToast("error", (e as Error).message); } } @@ -190,7 +209,6 @@ export type AttachmentProgress = { type AddAttachmentOptions = { expectedFileHash?: string; - showProgress?: boolean; forceWrite?: boolean; }; @@ -198,80 +216,46 @@ async function addAttachment( file: File, options: AddAttachmentOptions = {} ): Promise { - const { expectedFileHash, showProgress = true } = options; + const { expectedFileHash } = options; let forceWrite = options.forceWrite; - const action = async () => { - const reader = file.stream().getReader(); - const { hash, type: hashType } = await hashStream(reader); - reader.releaseLock(); + const reader = file.stream().getReader(); + const { hash, type: hashType } = await hashStream(reader); + reader.releaseLock(); - if (expectedFileHash && hash !== expectedFileHash) - throw new Error( - `Please select the same file for reuploading. Expected hash ${expectedFileHash} but got ${hash}.` - ); + if (expectedFileHash && hash !== expectedFileHash) + throw new Error( + `Please select the same file for reuploading. Expected hash ${expectedFileHash} but got ${hash}.` + ); - const exists = await db.attachments.attachment(hash); - if (!forceWrite && exists) { - forceWrite = (await getUploadedFileSize(hash)) === 0; + const exists = await db.attachments.attachment(hash); + if (!forceWrite && exists) { + forceWrite = (await getUploadedFileSize(hash)) === 0; + } + + if (forceWrite || !exists) { + if (forceWrite && exists) { + if (!(await db.fs().deleteFile(hash, false))) + throw new Error("Failed to delete attachment from server."); + await db.attachments.reset(exists.id); } - if (forceWrite || !exists) { - if (forceWrite && exists) { - if (!(await db.fs().deleteFile(hash, false))) - throw new Error("Failed to delete attachment from server."); - await db.attachments.reset(exists.id); - } + const key: SerializedKey = await getEncryptionKey(); - const key: SerializedKey = await getEncryptionKey(); + const output = await writeEncryptedFile(file, key, hash); + if (!output) throw new Error("Could not encrypt file."); - const output = await writeEncryptedFile(file, key, hash); - if (!output) throw new Error("Could not encrypt file."); + await db.attachments.add({ + ...output, + hash, + hashType, + filename: exists?.filename || file.name, + mimeType: exists?.type || file.type, + key + }); + } - await db.attachments.add({ - ...output, - hash, - hashType, - filename: exists?.filename || file.name, - mimeType: exists?.type || file.type, - key - }); - } - - return hash; - }; - - const result = showProgress - ? await withProgress(file, action) - : await action(); - - if (result instanceof Error) throw result; - return result; -} - -function withProgress( - file: File, - action: () => Promise -): Promise { - return TaskManager.startTask({ - type: "modal", - title: strings.encryptingAttachment(), - subtitle: strings.encryptingAttachmentDesc(), - action: (report) => { - const event = AppEventManager.subscribe( - AppEvents.UPDATE_ATTACHMENT_PROGRESS, - ({ type, total, loaded }: AttachmentProgress) => { - if (type !== "encrypt") return; - report({ - current: Math.round((loaded / total) * 100), - total: 100, - text: file.name - }); - } - ); - return action().finally(() => event.unsubscribe()); - } - }); + return hash; } function getImageDimensions(file: File) { diff --git a/apps/web/src/components/icons/index.tsx b/apps/web/src/components/icons/index.tsx index 745eb98be..fcc5077a2 100644 --- a/apps/web/src/components/icons/index.tsx +++ b/apps/web/src/components/icons/index.tsx @@ -229,7 +229,8 @@ import { mdiArrowUp, mdiInbox, mdiConsoleLine, - mdiDeleteSweepOutline + mdiDeleteSweepOutline, + mdiCloseCircle } from "@mdi/js"; import { useTheme } from "@emotion/react"; import { Theme } from "@notesnook/theme"; @@ -586,3 +587,4 @@ export const HamburgerMenu = createIcon(mdiMenu); export const ArrowUp = createIcon(mdiArrowUp); export const Inbox = createIcon(mdiInbox); export const ClearTrash = createIcon(mdiDeleteSweepOutline); +export const CloseCircle = createIcon(mdiCloseCircle); diff --git a/apps/web/src/dialogs/attach-files-dialog.tsx b/apps/web/src/dialogs/attach-files-dialog.tsx new file mode 100644 index 000000000..d8d58646c --- /dev/null +++ b/apps/web/src/dialogs/attach-files-dialog.tsx @@ -0,0 +1,372 @@ +/* +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 { useEffect, useMemo, useRef, useState } from "react"; +import Dialog from "../components/dialog"; +import { ScrollContainer } from "@notesnook/ui"; +import { Box, Flex, Image, Switch, Text } from "@theme-ui/components"; +import { formatBytes } from "@notesnook/common"; +import { BaseDialogProps, DialogManager } from "../common/dialog-manager"; +import { strings } from "@notesnook/intl"; +import { checkFeature } from "../common"; +import { AppEventManager, AppEvents } from "../common/app-events"; +import { attachFiles, AttachmentProgress } from "../components/editor/picker"; +import Config from "../utils/config"; +import { ImageCompressionOptions } from "../stores/setting-store"; +import { Attachment } from "@notesnook/editor"; +import { + CheckCircle, + Loading, + File as FileIcon, + CloseCircle +} from "../components/icons"; + +type FileStatus = "pending" | "compressing" | "encrypting" | "done" | "error"; + +type FileState = { + file: File; + status: FileStatus; + progress: number; + error?: string; + compress: boolean; +}; + +type AttachFilesDialogProps = BaseDialogProps & { + files: File[]; + skipSpecialImageHandling?: boolean; + onDone: (attachments: Attachment[]) => void; +}; + +export const AttachFilesDialog = DialogManager.register( + function AttachFilesDialog({ + files, + skipSpecialImageHandling, + onDone, + onClose + }: AttachFilesDialogProps) { + const hasImages = files.some((f) => f.type.startsWith("image/")); + const imageCompressionConfig = Config.get( + "imageCompression", + ImageCompressionOptions.ASK_EVERY_TIME + ); + const [fileStates, setFileStates] = useState(() => + files.map((file) => ({ + file, + status: "pending", + progress: 0, + compress: file.type.startsWith("image/") + ? imageCompressionConfig !== ImageCompressionOptions.DISABLE + : false + })) + ); + const [showCompressionPrompt, setShowCompressionPrompt] = useState( + hasImages && + imageCompressionConfig === ImageCompressionOptions.ASK_EVERY_TIME + ); + const processingRef = useRef(false); + + useEffect(() => { + const event = AppEventManager.subscribe( + AppEvents.UPDATE_ATTACHMENT_PROGRESS, + ({ type, total, loaded }: AttachmentProgress) => { + if (type !== "encrypt") return; + + setFileStates((prev) => + prev.map((s) => { + /** + * only one file is encrypted at a time, so we can just update progress of the state with "encrypting" status + */ + if (s.status !== "encrypting") return s; + + return { + ...s, + progress: Math.round((loaded / total) * 100) + }; + }) + ); + } + ); + return () => { + event.unsubscribe(); + }; + }, []); + + useEffect(() => { + if (showCompressionPrompt || processingRef.current) return; + + processingRef.current = true; + + const shouldCompress: boolean[] = fileStates.map((s) => !!s.compress); + + (async () => { + const attachments: Attachment[] = []; + let hasError = false; + + for await (const message of attachFiles( + files, + shouldCompress, + skipSpecialImageHandling + )) { + const { index } = message; + switch (message.type) { + case "compressing": + setFileStates((prev) => + prev.map((s, i) => + i === index + ? { ...s, status: "compressing" as FileStatus } + : s + ) + ); + break; + case "encrypting": + setFileStates((prev) => + prev.map((s, i) => + i === index + ? { ...s, status: "encrypting" as FileStatus, progress: 0 } + : s + ) + ); + break; + case "done": + if (message.attachment) attachments.push(message.attachment); + setFileStates((prev) => + prev.map((s, i) => + i === index + ? { + ...s, + status: "done" as FileStatus + } + : s + ) + ); + break; + case "error": + hasError = true; + setFileStates((prev) => + prev.map((s, i) => + i === index + ? { + ...s, + status: "error" as FileStatus, + error: message.error + } + : s + ) + ); + break; + } + } + + onDone(attachments); + if (files.length === 1 && !hasError) onClose(false); + })(); + }, [showCompressionPrompt]); + + return ( + onClose(false)} + width={500} + positiveButton={ + showCompressionPrompt + ? { + text: strings.done(), + onClick: () => setShowCompressionPrompt(false) + } + : undefined + } + negativeButton={{ + text: strings.close(), + onClick: () => onClose(false) + }} + > + + {fileStates.map((state, index) => ( + 1} + showCompressionToggle={showCompressionPrompt} + onToggleCompress={async () => { + if ( + !(await checkFeature("fullQualityImages", { type: "toast" })) + ) { + return; + } + + setFileStates((prev) => + prev.map((s, idx) => + idx === index ? { ...s, compress: !s.compress } : s + ) + ); + }} + /> + ))} + + + ); + } +); + +function FileRow({ + state, + showDivider, + showCompressionToggle, + onToggleCompress +}: { + state: FileState; + showDivider?: boolean; + showCompressionToggle?: boolean; + onToggleCompress?: () => void; +}) { + const { file, status, progress, error, compress } = state; + const isImage = file.type.startsWith("image/"); + const thumbnail = useMemo( + () => (isImage ? URL.createObjectURL(file) : undefined), + [file, isImage] + ); + + useEffect(() => { + return () => { + if (thumbnail) URL.revokeObjectURL(thumbnail); + }; + }, [thumbnail]); + + return ( + + {thumbnail ? ( + + ) : ( + + + + )} + + + + {file.name} + + + {formatBytes(file.size)} + {status === "compressing" + ? ` — ${strings.compressing()}...` + : status === "encrypting" + ? ` — ${strings.encrypting()} ${progress}%` + : status === "error" + ? ` — ${error}` + : ""} + + + + + {showCompressionToggle && isImage ? ( + + ) : showCompressionToggle && !isImage ? ( + + N/A + + ) : status === "done" ? ( + + ) : status === "error" ? ( + + ) : status === "encrypting" || status === "compressing" ? ( + + + + + + ) : ( + + )} + + + ); +} diff --git a/apps/web/src/dialogs/image-picker-dialog.tsx b/apps/web/src/dialogs/image-picker-dialog.tsx deleted file mode 100644 index c4530f30b..000000000 --- a/apps/web/src/dialogs/image-picker-dialog.tsx +++ /dev/null @@ -1,147 +0,0 @@ -/* -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 { useEffect, useState } from "react"; -import Dialog from "../components/dialog"; -import { ScrollContainer } from "@notesnook/ui"; -import { Flex, Image, Label, Text } from "@theme-ui/components"; -import { formatBytes } from "@notesnook/common"; -import { compressImage, FileWithURI } from "../utils/image-compressor"; -import { BaseDialogProps, DialogManager } from "../common/dialog-manager"; -import { strings } from "@notesnook/intl"; -import { checkFeature } from "../common"; - -export type ImagePickerDialogProps = BaseDialogProps & { - images: File[]; -}; - -export const ImagePickerDialog = DialogManager.register( - function ImagePickerDialog(props: ImagePickerDialogProps) { - const [images, setImages] = useState([]); - const [selectedIndex, setSelectedIndex] = useState(0); - const [compress, setCompress] = useState(true); - const selectedImage = images[selectedIndex]; - - useEffect(() => { - (async function () { - const images: FileWithURI[] = []; - for (const image of props.images) { - const compressed = compress - ? await compressImage(image, { - maxWidth: (naturalWidth) => Math.min(1920, naturalWidth * 0.7), - width: (naturalWidth) => naturalWidth, - height: (_, naturalHeight) => naturalHeight, - resize: "contain", - quality: 0.7 - }) - : image; - images.push( - new FileWithURI([compressed], image.name, { - lastModified: image.lastModified, - type: image.type - }) - ); - } - setImages(images); - })(); - }, [props.images, compress]); - - useEffect(() => { - return () => { - images.forEach((i) => URL.revokeObjectURL(i.uri)); - }; - }, [images]); - - return ( - props.onClose(false)} - positiveButton={{ - text: strings.insert(), - onClick: () => props.onClose(images) - }} - negativeButton={{ - text: strings.cancel(), - onClick: () => props.onClose(false) - }} - > - {selectedImage && ( - - - - {selectedImage.name} ({formatBytes(selectedImage.size)}) - - - )} - {images.length > 1 ? ( - - {images.map((image, index) => ( - setSelectedIndex(index)} - /> - ))} - - ) : null} - - - - ); - } -); diff --git a/apps/web/src/utils/file-picker.ts b/apps/web/src/utils/file-picker.ts index 275624bf5..2c8affe35 100644 --- a/apps/web/src/utils/file-picker.ts +++ b/apps/web/src/utils/file-picker.ts @@ -18,28 +18,37 @@ along with this program. If not, see . */ import { PAGE_VISIBILITY_CHANGE } from "./page-visibility"; +import { strings } from "@notesnook/intl"; +import { TaskManager } from "../common/task-manager"; type FilePickerOptions = { acceptedFileTypes: string; multiple?: boolean }; -export function showFilePicker({ +export async function showFilePicker({ acceptedFileTypes, multiple }: FilePickerOptions): Promise { - return new Promise((resolve) => { - PAGE_VISIBILITY_CHANGE.ignore = true; - const input = document.createElement("input"); - input.setAttribute("type", "file"); - input.setAttribute("multiple", `${multiple || false}`); - input.setAttribute("accept", acceptedFileTypes); - input.dispatchEvent(new MouseEvent("click")); - input.oncancel = async function () { - resolve([]); - }; - input.onchange = async function () { - if (!input.files) return resolve([]); - resolve(Array.from(input.files)); - }; + PAGE_VISIBILITY_CHANGE.ignore = true; + const input = document.createElement("input"); + input.setAttribute("type", "file"); + input.setAttribute("multiple", `${multiple || false}`); + input.setAttribute("accept", acceptedFileTypes); + input.dispatchEvent(new MouseEvent("click")); + const result = await TaskManager.startTask({ + type: "modal", + title: strings.processing(), + subtitle: strings.pleaseWait(), + action: () => + new Promise((resolve) => { + input.oncancel = async function () { + resolve([]); + }; + input.onchange = async function () { + if (!input.files) return resolve([]); + resolve(Array.from(input.files)); + }; + }) }); + return result instanceof Error ? [] : result; } export async function readFile(file: File): Promise { diff --git a/apps/web/src/utils/web-extension-server.ts b/apps/web/src/utils/web-extension-server.ts index d5cd96fc4..3ef523d86 100644 --- a/apps/web/src/utils/web-extension-server.ts +++ b/apps/web/src/utils/web-extension-server.ts @@ -88,7 +88,11 @@ export class WebExtensionServer implements Server { } ); - const attachment = (await attachFiles([clippedFile]))?.at(0); + let attachment; + for await (const message of attachFiles([clippedFile], [false])) { + if (message.type === "done") attachment = message.attachment; + else if (message.type === "error") return; + } if (!attachment) return; clipContent += h("iframe", [], { diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po index b8a10442e..8a3570685 100644 --- a/packages/intl/locale/en.po +++ b/packages/intl/locale/en.po @@ -984,6 +984,10 @@ msgstr "Attach image from URL" msgid "Attached files" msgstr "Attached files" +#: src/strings.ts:2678 +msgid "Attaching files" +msgstr "Attaching files" + #: src/strings.ts:290 msgid "attachment" msgstr "attachment" @@ -1646,6 +1650,10 @@ msgstr "Click to update" msgid "Close" msgstr "Close" +#: src/strings.ts:2679 +msgid "Close ({seconds})" +msgstr "Close ({seconds})" + #: src/strings.ts:2019 msgid "Close all" msgstr "Close all" @@ -1755,6 +1763,14 @@ msgstr "Compress images before uploading" msgid "Compressed images are uploaded in Full HD resolution and usually are good enough for most use cases." msgstr "Compressed images are uploaded in Full HD resolution and usually are good enough for most use cases." +#: src/strings.ts:2680 +msgid "Compressing" +msgstr "Compressing" + +#: src/strings.ts:2684 +msgid "Compression failed" +msgstr "Compression failed" + #: src/strings.ts:2262 msgid "Configure" msgstr "Configure" @@ -2547,6 +2563,10 @@ msgstr "Encrypted backup" msgid "Encrypted, private, secure." msgstr "Encrypted, private, secure." +#: src/strings.ts:2681 +msgid "Encrypting" +msgstr "Encrypting" + #: src/strings.ts:940 msgid "Encrypting attachment" msgstr "Encrypting attachment" @@ -2917,6 +2937,10 @@ msgstr "File length mismatch. Expected {expectedSize} but got {currentSize} byte msgid "File mismatch" msgstr "File mismatch" +#: src/strings.ts:2683 +msgid "File size limit exceeded. Please upgrade your plan." +msgstr "File size limit exceeded. Please upgrade your plan." + #: src/strings.ts:945 msgid "File size should be less than {sizeInMB}" msgstr "File size should be less than {sizeInMB}" diff --git a/packages/intl/locale/pseudo-LOCALE.po b/packages/intl/locale/pseudo-LOCALE.po index 718e2c9d3..5af418f71 100644 --- a/packages/intl/locale/pseudo-LOCALE.po +++ b/packages/intl/locale/pseudo-LOCALE.po @@ -984,6 +984,10 @@ msgstr "" msgid "Attached files" msgstr "" +#: src/strings.ts:2678 +msgid "Attaching files" +msgstr "" + #: src/strings.ts:290 msgid "attachment" msgstr "" @@ -1635,6 +1639,10 @@ msgstr "" msgid "Close" msgstr "" +#: src/strings.ts:2679 +msgid "Close ({seconds})" +msgstr "" + #: src/strings.ts:2019 msgid "Close all" msgstr "" @@ -1742,6 +1750,14 @@ msgstr "" #: src/strings.ts:144 msgid "Compressed images are uploaded in Full HD resolution and usually are good enough for most use cases." +msgstr "<<<<<<< HEAD" + +#: src/strings.ts:2680 +msgid "Compressing" +msgstr "<<<<<<< HEAD" + +#: src/strings.ts:2684 +msgid "Compression failed" msgstr "" #: src/strings.ts:2262 @@ -2536,6 +2552,10 @@ msgstr "" msgid "Encrypted, private, secure." msgstr "" +#: src/strings.ts:2681 +msgid "Encrypting" +msgstr "" + #: src/strings.ts:940 msgid "Encrypting attachment" msgstr "" @@ -2906,6 +2926,10 @@ msgstr "" msgid "File mismatch" msgstr "" +#: src/strings.ts:2683 +msgid "File size limit exceeded. Please upgrade your plan." +msgstr "" + #: src/strings.ts:945 msgid "File size should be less than {sizeInMB}" msgstr "" diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts index e318c1a76..42ecbee89 100644 --- a/packages/intl/src/strings.ts +++ b/packages/intl/src/strings.ts @@ -2674,5 +2674,12 @@ Use this if changes from other devices are not appearing on this device. This wi Continue without attachments?`, pleaseLoginToDownloadAttachments: () => - t`Please login to download attachments.` + t`Please login to download attachments.`, + attachingFiles: () => t`Attaching files`, + closeCountdown: (seconds: number) => t`Close (${seconds})`, + compressing: () => t`Compressing`, + encrypting: () => t`Encrypting`, + fileSizeLimitExceededPleaseUpgrade: () => + t`File size limit exceeded. Please upgrade your plan.`, + compressionFailed: () => t`Compression failed` };