feat: put pictures dropped on an open composer into the message text

This commit is contained in:
Linus Rath
2026-11-04 06:32:19 +01:00
parent a9064dd972
commit 749f2e938e
38 changed files with 104 additions and 6 deletions
+13
View File
@@ -2790,6 +2790,19 @@ export function EmailComposer({
useEffect(() => setComposerFileTarget(
(files) => void addFiles(files),
() => !!composerRootEl?.isConnected && composerRootEl.getClientRects().length > 0,
// The file drop overlay's "Insert into the message": the editor's own paste, so the
// upload, the place and the question for a description are the same as pasting.
{
available: () => !!editorRef.current && !editorRef.current.isDestroyed,
insert: (files) => {
const editor = editorRef.current;
if (!editor || editor.isDestroyed) return;
const transfer = new DataTransfer();
for (const file of files) transfer.items.add(file);
editor.commands.focus();
editor.view.dom.dispatchEvent(new ClipboardEvent('paste', { clipboardData: transfer, cancelable: true }));
},
},
), [addFiles, composerRootEl]);
// The id plugins know this composer by (registered below, with the handle).
const [pluginComposerId, setPluginComposerId] = useState<string | null>(null);
@@ -131,6 +131,21 @@ describe('a drag from outside', () => {
clear();
});
it('a picture dropped on "Insert into the message" goes into the text, as a paste would', async () => {
const take = vi.fn();
const insert = vi.fn();
const clear = setComposerFileTarget(take, () => true, { available: () => true, insert });
const picture = new File(['png'], 'chart.png', { type: 'image/png' });
render(<FileDropHost />);
dragIn([picture]);
expect(zone('insert-inline').textContent).toContain('zone_insert_inline');
fireEvent.drop(zone('insert-inline'), { dataTransfer: transfer([picture]) });
await flush();
expect(insert).toHaveBeenCalledWith([picture]);
expect(take).not.toHaveBeenCalled();
clear();
});
it('on the Files screen, "Upload to <folder>" hands the files to that folder', async () => {
const take = vi.fn();
const clear = setFilesDropTarget({ folder: 'Reports', take, visible: () => true });
+1
View File
@@ -68,6 +68,7 @@ function currentPlace(pathname: string, translate: (key: string) => string): { p
const folder = importFolder(pathname, translate);
const place: DropPlace = {
composer: !!visibleComposer(),
inlineImages: !!visibleComposer()?.images?.available(),
popout: isPopoutPath(window.location.pathname),
files: !!client?.supportsFiles?.() && features.isFeatureEnabled("filesEnabled"),
filesFolder: filesDropTarget()?.folder ?? null,
+3 -1
View File
@@ -2,12 +2,13 @@
import { forwardRef, type ComponentType, type ReactNode } from "react";
import { useTranslations } from "next-intl";
import { CalendarPlus, FolderInput, MailOpen, Paperclip, SquarePen, Upload, UserPlus } from "@/components/icons";
import { CalendarPlus, FolderInput, ImageIcon, MailOpen, Paperclip, SquarePen, Upload, UserPlus } from "@/components/icons";
import type { FileDropNote, FileDropZone, FileDropZoneId } from "@/lib/file-drop/zones";
import { cn, formatFileSize } from "@/lib/utils";
const ICONS: Record<FileDropZoneId, ComponentType<{ className?: string }>> = {
"attach-here": Paperclip,
"insert-inline": ImageIcon,
"new-message": SquarePen,
upload: Upload,
calendar: CalendarPlus,
@@ -21,6 +22,7 @@ export function useZoneText() {
const label = (zone: FileDropZone): string => {
switch (zone.id) {
case "attach-here": return t("zone_attach_here");
case "insert-inline": return t("zone_insert_inline");
case "new-message": return t("zone_new_message");
case "upload": return zone.folder ? t("zone_upload_folder", { folder: zone.folder }) : t("zone_upload_files");
case "calendar": return t("zone_calendar");
+9 -2
View File
@@ -4,10 +4,17 @@
* on is the app's own native one, which is no new file.
*/
/** Pictures put in the text where the cursor is, as pasting them does. Only while the rich text editor is there. */
export interface ComposerImages {
available: () => boolean;
insert: (files: File[]) => void;
}
interface ComposerTarget {
take: (files: File[]) => unknown;
/** The Pro shell keeps composers mounted on tabs out of sight. */
visible: () => boolean;
images?: ComposerImages;
}
const composerTargets: ComposerTarget[] = [];
@@ -15,8 +22,8 @@ const composerTargets: ComposerTarget[] = [];
let ownDrag = false;
/** A composer on screen takes files itself. */
export function setComposerFileTarget(take: ComposerTarget['take'], visible: ComposerTarget['visible']): () => void {
const target = { take, visible };
export function setComposerFileTarget(take: ComposerTarget['take'], visible: ComposerTarget['visible'], images?: ComposerImages): () => void {
const target = { take, visible, images };
composerTargets.push(target);
return () => {
const at = composerTargets.indexOf(target);
+11
View File
@@ -3,6 +3,7 @@ import { defaultZone, fileDropZones, filesWithinLimits, kindForType, MAX_ZONES,
const place = (patch: Partial<DropPlace> = {}): DropPlace => ({
composer: false,
inlineImages: false,
popout: false,
files: true,
filesFolder: null,
@@ -59,6 +60,16 @@ describe('the file drop zones', () => {
expect(zones).toEqual(['attach-here', 'open-eml', 'new-message', 'upload']);
});
it('pictures go into the text of an open rich text composer, beside attaching them', () => {
expect(ids(drag(['image', 'image']), place({ composer: true, inlineImages: true }))).toEqual(['attach-here', 'insert-inline', 'new-message', 'upload']);
// Plain text, a mix, or no composer: no such choice.
expect(ids(drag(['image']), place({ composer: true }))).toEqual(['attach-here', 'new-message', 'upload']);
expect(ids(drag(['image', 'other']), place({ composer: true, inlineImages: true }))).toEqual(['attach-here', 'new-message', 'upload']);
expect(ids(drag(['image']), place({ inlineImages: true }))).toEqual(['new-message', 'upload']);
expect(kindForType('image/png')).toBe('image');
expect(kindForType('image/svg+xml')).toBe('other');
});
it('in a pop-out, only its own composer', () => {
expect(ids(drag(['calendar']), place({ popout: true, composer: true }))).toEqual(['attach-here']);
expect(ids(drag(['other']), place({ popout: true }))).toEqual([]);
+7
View File
@@ -39,6 +39,13 @@ export async function runFileDropZone(zone: FileDropZoneId, files: File[], env:
else handOffDraft({ files });
return;
}
case 'insert-inline': {
const composer = visibleComposer();
if (composer?.images?.available()) composer.images.insert(files);
else if (composer) composer.take(files);
else handOffDraft({ files });
return;
}
case 'new-message':
handOffDraft({ files });
return;
+15 -3
View File
@@ -13,9 +13,9 @@
import { kindForFile, type HandoffKind } from '@/lib/launch-handoff';
export type FileDropZoneId = 'attach-here' | 'new-message' | 'upload' | 'calendar' | 'contacts' | 'open-eml' | 'import-eml';
export type FileDropZoneId = 'attach-here' | 'insert-inline' | 'new-message' | 'upload' | 'calendar' | 'contacts' | 'open-eml' | 'import-eml';
export type DroppedKind = Exclude<HandoffKind, 'compose'> | 'other';
export type DroppedKind = Exclude<HandoffKind, 'compose'> | 'image' | 'other';
export interface DropFiles {
/** Unknown during a drag in some browsers (Safari gives no items). */
@@ -29,6 +29,8 @@ export interface DropFiles {
export interface DropPlace {
/** A composer is on screen and takes files. */
composer: boolean;
/** Its rich text editor can take pictures into the text. */
inlineImages: boolean;
/** A pop-out window: only its own composer, nothing that needs the main window. */
popout: boolean;
/** Files is on for this account and its server has it. */
@@ -66,15 +68,23 @@ export interface FileDropZone {
export const MAX_ZONES = 4;
/** What a dragged item's MIME type says. Empty or anything else: no special choice. */
/** What a mail can show in its text. Not SVG: it can carry script, and mail clients drop it. */
const INLINE_IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
const INLINE_IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp']);
function dropped(kind: HandoffKind | null): DroppedKind {
return kind && kind !== 'compose' ? kind : 'other';
}
export function kindForType(type: string): DroppedKind {
if (INLINE_IMAGE_TYPES.has(type.toLowerCase())) return 'image';
return dropped(kindForFile({ name: '', type }));
}
export function kindForName(file: { name: string; type?: string }): DroppedKind {
const dot = file.name.lastIndexOf('.');
const extension = dot >= 0 ? file.name.slice(dot).toLowerCase() : '';
if (INLINE_IMAGE_TYPES.has((file.type ?? '').toLowerCase()) || INLINE_IMAGE_EXTENSIONS.has(extension)) return 'image';
return dropped(kindForFile(file));
}
@@ -109,6 +119,8 @@ export function fileDropZones(files: DropFiles, place: DropPlace): FileDropZone[
// What this kind of file is for, best first. Not in a pop-out: each opens on its own page.
const special: FileDropZone[] = [];
// Pictures into the text, as pasting them does: also in a pop-out, it's that composer's own.
if (kind === 'image' && place.composer && place.inlineImages) special.push({ id: 'insert-inline' });
if (!place.popout) {
if (kind === 'eml' && one) special.push({ id: 'open-eml' });
if (kind === 'eml' && place.importFolder) special.push(serverOnly(place, { id: 'import-eml', folder: place.importFolder }));
@@ -132,7 +144,7 @@ export function filesWithinLimits(
const perFile = place.maxUploadBytes;
const fitting = perFile > 0 ? files.filter((file) => file.size <= perFile) : [...files];
const tooLarge = files.filter((file) => !fitting.includes(file));
const mail = zone === 'attach-here' || zone === 'new-message';
const mail = zone === 'attach-here' || zone === 'new-message' || zone === 'insert-inline';
const total = fitting.reduce((sum, file) => sum + file.size, 0);
const overMessage = mail && place.maxMessageBytes > 0 && total > place.maxMessageBytes;
return { fitting: overMessage ? [] : fitting, tooLarge, overMessage };
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, zero {لا ملفات} one {ملف واحد} two {ملفان} few {# ملفات} many {# ملفًا} other {# ملف}}",
"files_summary_size": "{count, plural, zero {لا ملفات} one {ملف واحد} two {ملفان} few {# ملفات} many {# ملفًا} other {# ملف}}, {size}",
"zone_attach_here": "إرفاق بهذه الرسالة",
"zone_insert_inline": "إدراج في نص الرسالة",
"zone_new_message": "إرفاق برسالة جديدة",
"zone_upload_files": "رفع إلى الملفات",
"zone_upload_folder": "رفع إلى {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# fitxer} other {# fitxers}}",
"files_summary_size": "{count, plural, one {# fitxer} other {# fitxers}}, {size}",
"zone_attach_here": "Adjunta a aquest missatge",
"zone_insert_inline": "Insereix al missatge",
"zone_new_message": "Adjunta a un missatge nou",
"zone_upload_files": "Puja a Fitxers",
"zone_upload_folder": "Puja a {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# soubor} few {# soubory} many {# souboru} other {# souborů}}",
"files_summary_size": "{count, plural, one {# soubor} few {# soubory} many {# souboru} other {# souborů}}, {size}",
"zone_attach_here": "Připojit k této zprávě",
"zone_insert_inline": "Vložit do zprávy",
"zone_new_message": "Připojit k nové zprávě",
"zone_upload_files": "Nahrát do Souborů",
"zone_upload_folder": "Nahrát do {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# fil} other {# filer}}",
"files_summary_size": "{count, plural, one {# fil} other {# filer}}, {size}",
"zone_attach_here": "Vedhæft til denne besked",
"zone_insert_inline": "Indsæt i beskeden",
"zone_new_message": "Vedhæft til en ny besked",
"zone_upload_files": "Upload til Filer",
"zone_upload_folder": "Upload til {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# Datei} other {# Dateien}}",
"files_summary_size": "{count, plural, one {# Datei} other {# Dateien}}, {size}",
"zone_attach_here": "An diese Nachricht anhängen",
"zone_insert_inline": "In die Nachricht einfügen",
"zone_new_message": "An neue Nachricht anhängen",
"zone_upload_files": "In Dateien hochladen",
"zone_upload_folder": "In {folder} hochladen",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# file} other {# files}}",
"files_summary_size": "{count, plural, one {# file} other {# files}}, {size}",
"zone_attach_here": "Attach to this message",
"zone_insert_inline": "Insert into the message",
"zone_new_message": "Attach to a new message",
"zone_upload_files": "Upload to Files",
"zone_upload_folder": "Upload to {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# archivo} other {# archivos}}",
"files_summary_size": "{count, plural, one {# archivo} other {# archivos}}, {size}",
"zone_attach_here": "Adjuntar a este mensaje",
"zone_insert_inline": "Insertar en el mensaje",
"zone_new_message": "Adjuntar a un mensaje nuevo",
"zone_upload_files": "Subir a Archivos",
"zone_upload_folder": "Subir a {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# fail} other {# faili}}",
"files_summary_size": "{count, plural, one {# fail} other {# faili}}, {size}",
"zone_attach_here": "Lisa sellele kirjale",
"zone_insert_inline": "Lisa kirja sisse",
"zone_new_message": "Lisa uuele kirjale",
"zone_upload_files": "Laadi üles kausta Failid",
"zone_upload_folder": "Laadi üles kausta {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# فایل} other {# فایل}}",
"files_summary_size": "{count, plural, one {# فایل} other {# فایل}}, {size}",
"zone_attach_here": "پیوست به این پیام",
"zone_insert_inline": "درج در متن پیام",
"zone_new_message": "پیوست به پیام جدید",
"zone_upload_files": "بارگذاری در فایل‌ها",
"zone_upload_folder": "بارگذاری در {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# fichier} other {# fichiers}}",
"files_summary_size": "{count, plural, one {# fichier} other {# fichiers}}, {size}",
"zone_attach_here": "Joindre à ce message",
"zone_insert_inline": "Insérer dans le message",
"zone_new_message": "Joindre à un nouveau message",
"zone_upload_files": "Envoyer dans Fichiers",
"zone_upload_folder": "Envoyer dans {folder}",
+1
View File
@@ -7115,6 +7115,7 @@
"files_summary": "{count, plural, one {קובץ אחד} two {שני קבצים} other {# קבצים}}",
"files_summary_size": "{count, plural, one {קובץ אחד} two {שני קבצים} other {# קבצים}}, {size}",
"zone_attach_here": "צירוף להודעה הזו",
"zone_insert_inline": "הוספה לגוף ההודעה",
"zone_new_message": "צירוף להודעה חדשה",
"zone_upload_files": "העלאה לקבצים",
"zone_upload_folder": "העלאה אל {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# fájl} other {# fájl}}",
"files_summary_size": "{count, plural, one {# fájl} other {# fájl}}, {size}",
"zone_attach_here": "Csatolás ehhez az üzenethez",
"zone_insert_inline": "Beszúrás az üzenetbe",
"zone_new_message": "Csatolás új üzenethez",
"zone_upload_files": "Feltöltés a Fájlokba",
"zone_upload_folder": "Feltöltés ide: {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# file} other {# file}}",
"files_summary_size": "{count, plural, one {# file} other {# file}}, {size}",
"zone_attach_here": "Allega a questo messaggio",
"zone_insert_inline": "Inserisci nel messaggio",
"zone_new_message": "Allega a un nuovo messaggio",
"zone_upload_files": "Carica in File",
"zone_upload_folder": "Carica in {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, other {# 個のファイル}}",
"files_summary_size": "{count, plural, other {# 個のファイル}}, {size}",
"zone_attach_here": "このメッセージに添付",
"zone_insert_inline": "メッセージ本文に挿入",
"zone_new_message": "新規メッセージに添付",
"zone_upload_files": "ファイルにアップロード",
"zone_upload_folder": "{folder} にアップロード",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, other {파일 #개}}",
"files_summary_size": "{count, plural, other {파일 #개}}, {size}",
"zone_attach_here": "이 메시지에 첨부",
"zone_insert_inline": "메시지 본문에 삽입",
"zone_new_message": "새 메시지에 첨부",
"zone_upload_files": "파일에 업로드",
"zone_upload_folder": "{folder}에 업로드",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, zero {# failu} one {# fails} other {# faili}}",
"files_summary_size": "{count, plural, zero {# failu} one {# fails} other {# faili}}, {size}",
"zone_attach_here": "Pievienot šim ziņojumam",
"zone_insert_inline": "Ievietot ziņojumā",
"zone_new_message": "Pievienot jaunam ziņojumam",
"zone_upload_files": "Augšupielādēt failos",
"zone_upload_folder": "Augšupielādēt mapē {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# файл} other {# файл}}",
"files_summary_size": "{count, plural, one {# файл} other {# файл}}, {size}",
"zone_attach_here": "Энэ мессежид хавсаргах",
"zone_insert_inline": "Мессежийн дотор оруулах",
"zone_new_message": "Шинэ мессежид хавсаргах",
"zone_upload_files": "Файл руу байршуулах",
"zone_upload_folder": "{folder} руу байршуулах",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# fil} other {# filer}}",
"files_summary_size": "{count, plural, one {# fil} other {# filer}}, {size}",
"zone_attach_here": "Legg ved denne meldingen",
"zone_insert_inline": "Sett inn i meldingen",
"zone_new_message": "Legg ved en ny melding",
"zone_upload_files": "Last opp til Filer",
"zone_upload_folder": "Last opp til {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# bestand} other {# bestanden}}",
"files_summary_size": "{count, plural, one {# bestand} other {# bestanden}}, {size}",
"zone_attach_here": "Bij dit bericht voegen",
"zone_insert_inline": "In het bericht invoegen",
"zone_new_message": "Bij een nieuw bericht voegen",
"zone_upload_files": "Uploaden naar Bestanden",
"zone_upload_folder": "Uploaden naar {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# plik} few {# pliki} many {# plików} other {# pliku}}",
"files_summary_size": "{count, plural, one {# plik} few {# pliki} many {# plików} other {# pliku}}, {size}",
"zone_attach_here": "Załącz do tej wiadomości",
"zone_insert_inline": "Wstaw do wiadomości",
"zone_new_message": "Załącz do nowej wiadomości",
"zone_upload_files": "Prześlij do Plików",
"zone_upload_folder": "Prześlij do {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# arquivo} other {# arquivos}}",
"files_summary_size": "{count, plural, one {# arquivo} other {# arquivos}}, {size}",
"zone_attach_here": "Anexar a esta mensagem",
"zone_insert_inline": "Inserir na mensagem",
"zone_new_message": "Anexar a uma nova mensagem",
"zone_upload_files": "Enviar para Arquivos",
"zone_upload_folder": "Enviar para {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# fișier} few {# fișiere} other {# de fișiere}}",
"files_summary_size": "{count, plural, one {# fișier} few {# fișiere} other {# de fișiere}}, {size}",
"zone_attach_here": "Atașați la acest mesaj",
"zone_insert_inline": "Inserați în mesaj",
"zone_new_message": "Atașați la un mesaj nou",
"zone_upload_files": "Încărcați în Fișiere",
"zone_upload_folder": "Încărcați în {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# файл} few {# файла} many {# файлов} other {# файла}}",
"files_summary_size": "{count, plural, one {# файл} few {# файла} many {# файлов} other {# файла}}, {size}",
"zone_attach_here": "Прикрепить к этому письму",
"zone_insert_inline": "Вставить в текст письма",
"zone_new_message": "Прикрепить к новому письму",
"zone_upload_files": "Загрузить в Файлы",
"zone_upload_folder": "Загрузить в {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# súbor} few {# súbory} many {# súboru} other {# súborov}}",
"files_summary_size": "{count, plural, one {# súbor} few {# súbory} many {# súboru} other {# súborov}}, {size}",
"zone_attach_here": "Priložiť k tejto správe",
"zone_insert_inline": "Vložiť do správy",
"zone_new_message": "Priložiť k novej správe",
"zone_upload_files": "Nahrať do Súborov",
"zone_upload_folder": "Nahrať do {folder}",
+1
View File
@@ -7122,6 +7122,7 @@
"files_summary": "{count, plural, other {# ไฟล์}}",
"files_summary_size": "{count, plural, other {# ไฟล์}}, {size}",
"zone_attach_here": "แนบกับข้อความนี้",
"zone_insert_inline": "แทรกลงในข้อความ",
"zone_new_message": "แนบกับข้อความใหม่",
"zone_upload_files": "อัปโหลดไปยังไฟล์",
"zone_upload_folder": "อัปโหลดไปยัง {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# dosya} other {# dosya}}",
"files_summary_size": "{count, plural, one {# dosya} other {# dosya}}, {size}",
"zone_attach_here": "Bu iletiye ekle",
"zone_insert_inline": "İleti metnine ekle",
"zone_new_message": "Yeni bir iletiye ekle",
"zone_upload_files": "Dosyalar'a yükle",
"zone_upload_folder": "{folder} klasörüne yükle",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, one {# файл} few {# файли} many {# файлів} other {# файлу}}",
"files_summary_size": "{count, plural, one {# файл} few {# файли} many {# файлів} other {# файлу}}, {size}",
"zone_attach_here": "Прикріпити до цього повідомлення",
"zone_insert_inline": "Вставити в текст повідомлення",
"zone_new_message": "Прикріпити до нового повідомлення",
"zone_upload_files": "Завантажити у Файли",
"zone_upload_folder": "Завантажити в {folder}",
+1
View File
@@ -7126,6 +7126,7 @@
"files_summary": "{count, plural, other {# tệp}}",
"files_summary_size": "{count, plural, other {# tệp}}, {size}",
"zone_attach_here": "Đính kèm vào thư này",
"zone_insert_inline": "Chèn vào nội dung thư",
"zone_new_message": "Đính kèm vào thư mới",
"zone_upload_files": "Tải lên Tệp",
"zone_upload_folder": "Tải lên {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, other {# 個檔案}}",
"files_summary_size": "{count, plural, other {# 個檔案}}, {size}",
"zone_attach_here": "附加到此郵件",
"zone_insert_inline": "插入到郵件內文",
"zone_new_message": "附加到新郵件",
"zone_upload_files": "上傳到檔案",
"zone_upload_folder": "上傳到 {folder}",
+1
View File
@@ -7114,6 +7114,7 @@
"files_summary": "{count, plural, other {# 个文件}}",
"files_summary_size": "{count, plural, other {# 个文件}}, {size}",
"zone_attach_here": "附加到此邮件",
"zone_insert_inline": "插入到邮件正文",
"zone_new_message": "附加到新邮件",
"zone_upload_files": "上传到文件",
"zone_upload_folder": "上传到 {folder}",