From f60df67dfb3d924f34d4591a387fb97dde776db7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Romero?= Date: Fri, 6 Nov 2026 14:41:55 +0100 Subject: [PATCH] feat: move and copy mail to other accounts from the folder picker --- lang/flectar-mail.pot | 981 ++++++++++++++++--------------- src/mail.rs | 32 + src/mail_drop_tests.rs | 22 + src/mail_work.rs | 43 ++ src/main.rs | 37 +- src/move_picker.rs | 772 +++++++++++++++++------- src/startup.rs | 11 +- ui/app.slint | 9 + ui/dialogs/move-dialog.slint | 28 +- ui/dialogs/settings-dialog.slint | 1 + ui/shortcuts.slint | 4 +- ui/views/mail-list-pane.slint | 33 +- ui/views/mail-reading-pane.slint | 18 +- 13 files changed, 1295 insertions(+), 696 deletions(-) diff --git a/lang/flectar-mail.pot b/lang/flectar-mail.pot index 79aae9e8..09584dbd 100644 --- a/lang/flectar-mail.pot +++ b/lang/flectar-mail.pot @@ -25,17 +25,17 @@ msgstr "" msgid "Light" msgstr "" -#: ui/app-icon.slint:22 ui/views/mail-reading-pane.slint:2018 +#: ui/app-icon.slint:22 ui/views/mail-reading-pane.slint:2030 msgid "Blue" msgstr "" #: ui/app-icon.slint:23 ui/components/theme-editor.slint:623 -#: ui/app-icon.slint:23 ui/components/theme-editor.slint:623 ui/views/mail-reading-pane.slint:2023 +#: ui/app-icon.slint:23 ui/components/theme-editor.slint:623 ui/views/mail-reading-pane.slint:2035 msgid "Green" msgstr "" #: ui/app-icon.slint:24 ui/components/theme-editor.slint:624 -#: ui/app-icon.slint:24 ui/components/theme-editor.slint:624 ui/views/mail-reading-pane.slint:2019 +#: ui/app-icon.slint:24 ui/components/theme-editor.slint:624 ui/views/mail-reading-pane.slint:2031 msgid "Purple" msgstr "" @@ -285,7 +285,7 @@ msgctxt "time of day" msgid "AM" msgstr "" -#: ui/app.slint:155 ui/views/mail-list-pane.slint:625 +#: ui/app.slint:155 ui/views/mail-list-pane.slint:630 msgid "Yesterday" msgstr "" @@ -384,8 +384,8 @@ msgstr "" msgid "New address-book entry" msgstr "" -#: ui/app.slint:181 ui/app.slint:1045 ui/components/composer-fields.slint:66 -#: ui/app.slint:181 ui/app.slint:1045 ui/components/composer-fields.slint:66 ui/dialogs/compose-dialog.slint:37 +#: ui/app.slint:181 ui/app.slint:1054 ui/components/composer-fields.slint:66 +#: ui/app.slint:181 ui/app.slint:1054 ui/components/composer-fields.slint:66 ui/dialogs/compose-dialog.slint:37 msgid "No connected account" msgstr "" @@ -1381,438 +1381,484 @@ msgid "Could not move mail: {}" msgstr "" #: ui/app.slint:499 -msgid "Could not update label: {}" +msgid "No folder to copy this mail to." msgstr "" #: ui/app.slint:500 -msgid "Could not save label: {}" +msgid "Could not copy mail: {}" msgstr "" #: ui/app.slint:501 -msgid "Could not delete label: {}" +msgid "Moving mail to the other account…" msgstr "" #: ui/app.slint:502 -msgid "Folder created." +msgid "Copying mail to the other account…" msgstr "" #: ui/app.slint:503 -msgid "Folder renamed." -msgstr "" +msgid "Moved one message to the other account." +msgid_plural "Moved {0} messages to the other account." +msgstr[0] "" +msgstr[1] "" #: ui/app.slint:504 -msgid "Folder deleted." -msgstr "" +msgid "Copied one message to the other account." +msgid_plural "Copied {0} messages to the other account." +msgstr[0] "" +msgstr[1] "" #: ui/app.slint:505 -msgid "Could not create folder: {}" -msgstr "" +msgid "Moved or copied one message to the other account." +msgid_plural "Moved or copied {0} messages to the other account." +msgstr[0] "" +msgstr[1] "" #: ui/app.slint:506 -msgid "Could not rename folder: {}" -msgstr "" +msgid "The other account had that message already." +msgid_plural "The other account had those {0} messages already." +msgstr[0] "" +msgstr[1] "" #: ui/app.slint:507 -msgid "Could not delete folder: {}" -msgstr "" +msgid "Could not move or copy one message: {1}" +msgid_plural "Could not move or copy {0} messages: {1}" +msgstr[0] "" +msgstr[1] "" #: ui/app.slint:508 -msgid "Could not close preview: {}" +msgid "Could not update label: {}" msgstr "" #: ui/app.slint:509 -msgid "Opened the system print view." +msgid "Could not save label: {}" msgstr "" #: ui/app.slint:510 -msgid "Could not print message: {}" +msgid "Could not delete label: {}" msgstr "" #: ui/app.slint:511 -msgid "Opened the message in your browser." +msgid "Folder created." msgstr "" #: ui/app.slint:512 -msgid "Could not open message in browser: {}" +msgid "Folder renamed." msgstr "" #: ui/app.slint:513 -msgid "Connect an account before replying or forwarding." +msgid "Folder deleted." msgstr "" #: ui/app.slint:514 -msgid "Choose a message first." +msgid "Could not create folder: {}" msgstr "" #: ui/app.slint:515 -msgid "Only drafts can be reopened for editing." +msgid "Could not rename folder: {}" msgstr "" #: ui/app.slint:516 -msgid "Could not open draft: {}" +msgid "Could not delete folder: {}" msgstr "" #: ui/app.slint:517 -msgid "Could not open draft: invalid account identifier" +msgid "Could not close preview: {}" msgstr "" #: ui/app.slint:518 -msgid "Connected account" +msgid "Opened the system print view." msgstr "" #: ui/app.slint:519 -msgid "Could not open composer: {}" +msgid "Could not print message: {}" msgstr "" #: ui/app.slint:520 -msgid "Add an account to send mail." +msgid "Opened the message in your browser." msgstr "" -#: ui/app.slint:521 ui/components/sidebar.slint:520 -#: ui/app.slint:521 ui/components/sidebar.slint:520 -msgid "On this device" +#: ui/app.slint:521 +msgid "Could not open message in browser: {}" msgstr "" #: ui/app.slint:522 +msgid "Connect an account before replying or forwarding." +msgstr "" + +#: ui/app.slint:523 +msgid "Choose a message first." +msgstr "" + +#: ui/app.slint:524 +msgid "Only drafts can be reopened for editing." +msgstr "" + +#: ui/app.slint:525 +msgid "Could not open draft: {}" +msgstr "" + +#: ui/app.slint:526 +msgid "Could not open draft: invalid account identifier" +msgstr "" + +#: ui/app.slint:527 +msgid "Connected account" +msgstr "" + +#: ui/app.slint:528 +msgid "Could not open composer: {}" +msgstr "" + +#: ui/app.slint:529 +msgid "Add an account to send mail." +msgstr "" + +#: ui/app.slint:530 ui/components/sidebar.slint:520 +#: ui/app.slint:530 ui/components/sidebar.slint:520 ui/views/mail-list-pane.slint:887 +msgid "On this device" +msgstr "" + +#: ui/app.slint:531 msgid "Could not open composer: invalid account identifier" msgstr "" #. Reply and forward quotation headers, written into the draft. -#: ui/app.slint:528 +#: ui/app.slint:537 msgid "On {}, {} wrote:" msgstr "" -#: ui/app.slint:529 +#: ui/app.slint:538 msgid "{} wrote:" msgstr "" -#: ui/app.slint:530 +#: ui/app.slint:539 msgid "Forwarded message" msgstr "" -#: ui/app.slint:531 +#: ui/app.slint:540 msgctxt "forwarded message header" msgid "From: {}" msgstr "" -#: ui/app.slint:532 +#: ui/app.slint:541 msgctxt "forwarded message header" msgid "Date: {}" msgstr "" -#: ui/app.slint:533 +#: ui/app.slint:542 msgctxt "forwarded message header" msgid "Subject: {}" msgstr "" -#: ui/app.slint:534 +#: ui/app.slint:543 msgctxt "forwarded message header" msgid "To: {}" msgstr "" -#: ui/app.slint:535 +#: ui/app.slint:544 msgid "Attach files to this email" msgstr "" -#: ui/app.slint:536 +#: ui/app.slint:545 msgid "Could not add attachments: {}" msgstr "" -#: ui/app.slint:537 +#: ui/app.slint:546 msgid "Added 1 attachment." msgstr "" -#: ui/app.slint:538 +#: ui/app.slint:547 msgid "Added {} attachments." msgstr "" -#: ui/app.slint:539 +#: ui/app.slint:548 msgid "" "Attachments added: {}. Skipped: {} (duplicate, unreadable, or over the 25 MB " "total limit)." msgstr "" -#: ui/app.slint:540 +#: ui/app.slint:549 msgid "Attachment removed." msgstr "" -#: ui/app.slint:541 +#: ui/app.slint:550 msgid "Choose the account this message should be sent from." msgstr "" -#: ui/app.slint:542 +#: ui/app.slint:551 msgid "Choose a verified sender identity for this message." msgstr "" -#: ui/app.slint:543 +#: ui/app.slint:552 msgid "Connect an account before saving or sending mail." msgstr "" -#: ui/app.slint:544 +#: ui/app.slint:553 msgid "Message queued for delivery (action {})." msgstr "" -#: ui/app.slint:545 +#: ui/app.slint:554 msgid "Sending message…" msgstr "" -#: ui/app.slint:546 +#: ui/app.slint:555 msgid "Message sent." msgstr "" -#: ui/app.slint:547 +#: ui/app.slint:556 msgid "Message was not sent: {} Your draft is preserved." msgstr "" -#: ui/app.slint:548 +#: ui/app.slint:557 msgid "Draft saved locally (#{})." msgstr "" -#: ui/app.slint:549 +#: ui/app.slint:558 msgid "Compose failed: {}" msgstr "" -#: ui/app.slint:550 +#: ui/app.slint:559 msgid "Connect an account before managing email templates." msgstr "" -#: ui/app.slint:551 +#: ui/app.slint:560 msgid "Could not load email templates: {}" msgstr "" -#: ui/app.slint:552 +#: ui/app.slint:561 msgid "Enter a template name." msgstr "" -#: ui/app.slint:553 +#: ui/app.slint:562 msgid "Add a subject or message to the template." msgstr "" -#: ui/app.slint:554 +#: ui/app.slint:563 msgid "Email template saved." msgstr "" -#: ui/app.slint:555 +#: ui/app.slint:564 msgid "Could not save email template: {}" msgstr "" -#: ui/app.slint:556 +#: ui/app.slint:565 msgid "Email template deleted." msgstr "" -#: ui/app.slint:557 +#: ui/app.slint:566 msgid "Could not delete email template: {}" msgstr "" -#: ui/app.slint:558 +#: ui/app.slint:567 msgid "Email template is no longer available." msgstr "" -#: ui/app.slint:559 +#: ui/app.slint:568 msgid "Email template inserted." msgstr "" -#: ui/app.slint:560 +#: ui/app.slint:569 msgid "Email template inserted, but its usage could not be saved: {}" msgstr "" -#: ui/app.slint:561 +#: ui/app.slint:570 msgid "Choose a supported account type and connection security." msgstr "" -#: ui/app.slint:562 +#: ui/app.slint:571 msgid "IMAP port must be between 1 and 65535." msgstr "" -#: ui/app.slint:563 +#: ui/app.slint:572 msgid "POP3 port must be between 1 and 65535." msgstr "" -#: ui/app.slint:564 +#: ui/app.slint:573 msgid "SMTP port must be between 1 and 65535." msgstr "" -#: ui/app.slint:565 +#: ui/app.slint:574 msgid "Local mail data is unavailable. Retry startup." msgstr "" -#: ui/app.slint:566 +#: ui/app.slint:575 msgid "Discovering JMAP and checking capabilities…" msgstr "" -#: ui/app.slint:567 +#: ui/app.slint:576 msgid "Certificate imported. Connect to verify the server." msgstr "" -#: ui/app.slint:568 +#: ui/app.slint:577 msgid "Could not load account preferences: {}" msgstr "" -#: ui/app.slint:569 +#: ui/app.slint:578 msgid "" "Your edited signature was preserved. Choose No signature to keep it as " "message text before inserting another." msgstr "" -#: ui/app.slint:570 +#: ui/app.slint:579 msgid "Certificate import failed: {}" msgstr "" -#: ui/app.slint:571 +#: ui/app.slint:580 msgid "Testing IMAP and SMTP connections…" msgstr "" -#: ui/app.slint:572 +#: ui/app.slint:581 msgid "Testing POP3 and SMTP connections…" msgstr "" -#: ui/app.slint:577 +#: ui/app.slint:586 msgid "" "Another browser authorization is already in progress; finish it or choose " "Cancel." msgstr "" -#: ui/app.slint:578 ui/dialogs/settings-dialog.slint:1885 -#: ui/app.slint:578 ui/dialogs/settings-dialog.slint:1885 ui/dialogs/settings-dialog.slint:1928 -#: ui/app.slint:578 ui/dialogs/settings-dialog.slint:1885 ui/dialogs/settings-dialog.slint:1928 ui/views/onboarding-view.slint:161 -#: ui/app.slint:578 ui/dialogs/settings-dialog.slint:1885 ui/dialogs/settings-dialog.slint:1928 ui/views/onboarding-view.slint:161 +#: ui/app.slint:587 ui/dialogs/settings-dialog.slint:1885 +#: ui/app.slint:587 ui/dialogs/settings-dialog.slint:1885 ui/dialogs/settings-dialog.slint:1928 +#: ui/app.slint:587 ui/dialogs/settings-dialog.slint:1885 ui/dialogs/settings-dialog.slint:1928 ui/views/onboarding-view.slint:161 +#: ui/app.slint:587 ui/dialogs/settings-dialog.slint:1885 ui/dialogs/settings-dialog.slint:1928 ui/views/onboarding-view.slint:161 msgid "Add app keys in Sign-in settings to enable this provider." msgstr "" -#: ui/app.slint:579 +#: ui/app.slint:588 msgid "Choose Gmail or Outlook OAuth." msgstr "" -#: ui/app.slint:580 +#: ui/app.slint:589 msgid "Complete sign-in in your browser, or choose Cancel here." msgstr "" -#: ui/app.slint:581 +#: ui/app.slint:590 msgid "Cancelling browser sign-in…" msgstr "" -#: ui/app.slint:582 +#: ui/app.slint:591 msgid "Finishing browser sign-in…" msgstr "" -#: ui/app.slint:583 +#: ui/app.slint:592 msgid "Could not use that browser address: {}" msgstr "" -#: ui/app.slint:584 +#: ui/app.slint:593 msgid "Calendar core is unavailable." msgstr "" -#: ui/app.slint:585 +#: ui/app.slint:594 msgid "Account no longer exists." msgstr "" -#: ui/app.slint:586 +#: ui/app.slint:595 msgid "Authorize calendar access in your browser." msgstr "" -#: ui/app.slint:587 +#: ui/app.slint:596 msgid "OAuth app keys saved." msgstr "" -#: ui/app.slint:588 +#: ui/app.slint:597 msgid "{} OAuth settings saved." msgstr "" -#: ui/app.slint:589 +#: ui/app.slint:598 msgid "OAuth settings failed: {}" msgstr "" -#: ui/app.slint:590 +#: ui/app.slint:599 msgid "Mail refresh failed: {}" msgstr "" -#: ui/app.slint:591 +#: ui/app.slint:600 msgid "Mail filter failed: {}" msgstr "" -#: ui/app.slint:592 +#: ui/app.slint:601 msgid "Mail pagination failed: {}" msgstr "" -#: ui/app.slint:593 +#: ui/app.slint:602 msgid "Could not mark message as read: {}" msgstr "" -#: ui/app.slint:594 ui/views/mail-reading-pane.slint:197 -#: ui/app.slint:594 ui/views/mail-reading-pane.slint:197 ui/views/mail-reading-pane.slint:1296 +#: ui/app.slint:603 ui/views/mail-reading-pane.slint:197 +#: ui/app.slint:603 ui/views/mail-reading-pane.slint:197 ui/views/mail-reading-pane.slint:1308 msgid "Loading message…" msgstr "" -#: ui/app.slint:600 ui/components/workspace-navigation.slint:787 -#: ui/app.slint:600 ui/components/workspace-navigation.slint:787 ui/tray.slint:13 ui/tray.slint:14 -#: ui/app.slint:600 ui/components/workspace-navigation.slint:787 ui/tray.slint:13 ui/tray.slint:14 +#: ui/app.slint:609 ui/components/workspace-navigation.slint:787 +#: ui/app.slint:609 ui/components/workspace-navigation.slint:787 ui/tray.slint:13 ui/tray.slint:14 +#: ui/app.slint:609 ui/components/workspace-navigation.slint:787 ui/tray.slint:13 ui/tray.slint:14 msgid "Flectar Mail" msgstr "" -#: ui/app.slint:1278 ui/app.slint:1291 +#: ui/app.slint:1287 ui/app.slint:1300 msgid "Global Labels (local only)" msgstr "" -#: ui/app.slint:2232 +#: ui/app.slint:2241 msgid "Empty Trash?" msgstr "" -#: ui/app.slint:2232 ui/views/files-view.slint:578 +#: ui/app.slint:2241 ui/views/files-view.slint:578 msgid "Delete permanently?" msgstr "" -#: ui/app.slint:2236 +#: ui/app.slint:2245 msgid "Permanently delete all messages in Trash across every connected account?" msgstr "" -#: ui/app.slint:2237 +#: ui/app.slint:2246 msgid "Permanently delete all messages in this account’s Trash?" msgstr "" -#: ui/app.slint:2239 +#: ui/app.slint:2248 msgid "Permanently delete the selected conversations from Trash?" msgstr "" -#: ui/app.slint:2240 +#: ui/app.slint:2249 msgid "Permanently delete this conversation from Trash?" msgstr "" -#: ui/app.slint:2241 ui/components/calendar.slint:947 -#: ui/app.slint:2241 ui/components/calendar.slint:947 +#: ui/app.slint:2250 ui/components/calendar.slint:947 +#: ui/app.slint:2250 ui/components/calendar.slint:947 msgid "This action cannot be undone." msgstr "" -#: ui/app.slint:2242 +#: ui/app.slint:2251 msgid "Empty Trash" msgstr "" -#: ui/app.slint:2242 ui/views/files-view.slint:660 -#: ui/app.slint:2242 ui/views/files-view.slint:660 +#: ui/app.slint:2251 ui/views/files-view.slint:660 +#: ui/app.slint:2251 ui/views/files-view.slint:660 msgid "Delete permanently" msgstr "" -#: ui/app.slint:2425 +#: ui/app.slint:2434 msgid "Mailbox startup failed" msgstr "" -#: ui/app.slint:2431 +#: ui/app.slint:2440 msgid "Flectar Mail could not open local storage" msgstr "" -#: ui/app.slint:2444 +#: ui/app.slint:2453 msgid "Diagnostic ID: {}" msgstr "" -#: ui/app.slint:2447 +#: ui/app.slint:2456 msgid "Diagnostic ID {}" msgstr "" -#: ui/app.slint:2452 +#: ui/app.slint:2461 msgid "Retry" msgstr "" -#: ui/app.slint:2460 ui/components/copyable-diagnostic.slint:43 +#: ui/app.slint:2469 ui/components/copyable-diagnostic.slint:43 msgid "Copy diagnostics" msgstr "" @@ -1895,7 +1941,7 @@ msgstr "" #: ui/components/account-calendars.slint:264 #: ui/components/account-calendars.slint:264 -#: ui/components/account-calendars.slint:264 ui/dialogs/settings-dialog.slint:2787 +#: ui/components/account-calendars.slint:264 ui/dialogs/settings-dialog.slint:2788 msgid "Add CalDAV calendar" msgstr "" @@ -2020,9 +2066,9 @@ msgid "" msgstr "" #: ui/components/account-mail-preferences.slint:171 -#: ui/components/account-mail-preferences.slint:171 ui/dialogs/settings-dialog.slint:2957 -#: ui/components/account-mail-preferences.slint:171 ui/dialogs/settings-dialog.slint:2957 ui/views/onboarding-view.slint:267 -#: ui/components/account-mail-preferences.slint:171 ui/dialogs/settings-dialog.slint:2957 ui/views/onboarding-view.slint:267 +#: ui/components/account-mail-preferences.slint:171 ui/dialogs/settings-dialog.slint:2958 +#: ui/components/account-mail-preferences.slint:171 ui/dialogs/settings-dialog.slint:2958 ui/views/onboarding-view.slint:267 +#: ui/components/account-mail-preferences.slint:171 ui/dialogs/settings-dialog.slint:2958 ui/views/onboarding-view.slint:267 msgid "Email address" msgstr "" @@ -2061,10 +2107,10 @@ msgstr "" #: ui/components/account-mail-preferences.slint:178 ui/dialogs/folder-dialog.slint:128 #: ui/components/account-mail-preferences.slint:178 ui/dialogs/folder-dialog.slint:128 ui/dialogs/oauth-setup-dialog.slint:126 #: ui/components/account-mail-preferences.slint:178 ui/dialogs/folder-dialog.slint:128 ui/dialogs/oauth-setup-dialog.slint:126 ui/dialogs/settings-dialog.slint:2230 -#: ui/components/account-mail-preferences.slint:178 ui/dialogs/folder-dialog.slint:128 ui/dialogs/oauth-setup-dialog.slint:126 ui/dialogs/settings-dialog.slint:2230 ui/dialogs/settings-dialog.slint:3033 -#: ui/components/account-mail-preferences.slint:178 ui/dialogs/folder-dialog.slint:128 ui/dialogs/oauth-setup-dialog.slint:126 ui/dialogs/settings-dialog.slint:2230 ui/dialogs/settings-dialog.slint:3033 ui/views/contacts-view.slint:604 -#: ui/components/account-mail-preferences.slint:178 ui/dialogs/folder-dialog.slint:128 ui/dialogs/oauth-setup-dialog.slint:126 ui/dialogs/settings-dialog.slint:2230 ui/dialogs/settings-dialog.slint:3033 ui/views/contacts-view.slint:604 ui/views/files-view.slint:659 -#: ui/components/account-mail-preferences.slint:178 ui/dialogs/folder-dialog.slint:128 ui/dialogs/oauth-setup-dialog.slint:126 ui/dialogs/settings-dialog.slint:2230 ui/dialogs/settings-dialog.slint:3033 ui/views/contacts-view.slint:604 ui/views/files-view.slint:659 +#: ui/components/account-mail-preferences.slint:178 ui/dialogs/folder-dialog.slint:128 ui/dialogs/oauth-setup-dialog.slint:126 ui/dialogs/settings-dialog.slint:2230 ui/dialogs/settings-dialog.slint:3034 +#: ui/components/account-mail-preferences.slint:178 ui/dialogs/folder-dialog.slint:128 ui/dialogs/oauth-setup-dialog.slint:126 ui/dialogs/settings-dialog.slint:2230 ui/dialogs/settings-dialog.slint:3034 ui/views/contacts-view.slint:604 +#: ui/components/account-mail-preferences.slint:178 ui/dialogs/folder-dialog.slint:128 ui/dialogs/oauth-setup-dialog.slint:126 ui/dialogs/settings-dialog.slint:2230 ui/dialogs/settings-dialog.slint:3034 ui/views/contacts-view.slint:604 ui/views/files-view.slint:659 +#: ui/components/account-mail-preferences.slint:178 ui/dialogs/folder-dialog.slint:128 ui/dialogs/oauth-setup-dialog.slint:126 ui/dialogs/settings-dialog.slint:2230 ui/dialogs/settings-dialog.slint:3034 ui/views/contacts-view.slint:604 ui/views/files-view.slint:659 msgid "Cancel" msgstr "" @@ -2258,13 +2304,13 @@ msgstr "" #: ui/components/account-mail-preferences.slint:265 #: ui/components/account-mail-preferences.slint:265 #: ui/components/account-mail-preferences.slint:265 -#: ui/components/account-mail-preferences.slint:265 ui/dialogs/move-dialog.slint:204 -#: ui/components/account-mail-preferences.slint:265 ui/dialogs/move-dialog.slint:204 -#: ui/components/account-mail-preferences.slint:265 ui/dialogs/move-dialog.slint:204 ui/dialogs/settings-dialog.slint:2933 -#: ui/components/account-mail-preferences.slint:265 ui/dialogs/move-dialog.slint:204 ui/dialogs/settings-dialog.slint:2933 ui/views/files-view.slint:505 -#: ui/components/account-mail-preferences.slint:265 ui/dialogs/move-dialog.slint:204 ui/dialogs/settings-dialog.slint:2933 ui/views/files-view.slint:505 ui/views/files-view.slint:695 -#: ui/components/account-mail-preferences.slint:265 ui/dialogs/move-dialog.slint:204 ui/dialogs/settings-dialog.slint:2933 ui/views/files-view.slint:505 ui/views/files-view.slint:695 ui/window-chrome.slint:201 -#: ui/components/account-mail-preferences.slint:265 ui/dialogs/move-dialog.slint:204 ui/dialogs/settings-dialog.slint:2933 ui/views/files-view.slint:505 ui/views/files-view.slint:695 ui/window-chrome.slint:201 +#: ui/components/account-mail-preferences.slint:265 ui/dialogs/move-dialog.slint:211 +#: ui/components/account-mail-preferences.slint:265 ui/dialogs/move-dialog.slint:211 +#: ui/components/account-mail-preferences.slint:265 ui/dialogs/move-dialog.slint:211 ui/dialogs/settings-dialog.slint:2934 +#: ui/components/account-mail-preferences.slint:265 ui/dialogs/move-dialog.slint:211 ui/dialogs/settings-dialog.slint:2934 ui/views/files-view.slint:505 +#: ui/components/account-mail-preferences.slint:265 ui/dialogs/move-dialog.slint:211 ui/dialogs/settings-dialog.slint:2934 ui/views/files-view.slint:505 ui/views/files-view.slint:695 +#: ui/components/account-mail-preferences.slint:265 ui/dialogs/move-dialog.slint:211 ui/dialogs/settings-dialog.slint:2934 ui/views/files-view.slint:505 ui/views/files-view.slint:695 ui/window-chrome.slint:201 +#: ui/components/account-mail-preferences.slint:265 ui/dialogs/move-dialog.slint:211 ui/dialogs/settings-dialog.slint:2934 ui/views/files-view.slint:505 ui/views/files-view.slint:695 ui/window-chrome.slint:201 msgid "Close" msgstr "" @@ -2320,7 +2366,7 @@ msgid "Filters only affect this agenda." msgstr "" #: ui/components/calendar-agenda.slint:212 ui/components/calendar.slint:582 -#: ui/components/calendar-agenda.slint:212 ui/components/calendar.slint:582 ui/views/mail-list-pane.slint:624 +#: ui/components/calendar-agenda.slint:212 ui/components/calendar.slint:582 ui/views/mail-list-pane.slint:629 msgid "Today" msgstr "" @@ -2651,23 +2697,23 @@ msgid "Keep" msgstr "" #: ui/components/composer-fields.slint:39 ui/views/mail-reading-pane.slint:336 -#: ui/components/composer-fields.slint:39 ui/views/mail-reading-pane.slint:336 ui/views/mail-reading-pane.slint:374 -#: ui/components/composer-fields.slint:39 ui/views/mail-reading-pane.slint:336 ui/views/mail-reading-pane.slint:374 ui/views/mail-reading-pane.slint:749 -#: ui/components/composer-fields.slint:39 ui/views/mail-reading-pane.slint:336 ui/views/mail-reading-pane.slint:374 ui/views/mail-reading-pane.slint:749 +#: ui/components/composer-fields.slint:39 ui/views/mail-reading-pane.slint:336 ui/views/mail-reading-pane.slint:376 +#: ui/components/composer-fields.slint:39 ui/views/mail-reading-pane.slint:336 ui/views/mail-reading-pane.slint:376 ui/views/mail-reading-pane.slint:761 +#: ui/components/composer-fields.slint:39 ui/views/mail-reading-pane.slint:336 ui/views/mail-reading-pane.slint:376 ui/views/mail-reading-pane.slint:761 msgid "Reply" msgstr "" #: ui/components/composer-fields.slint:40 ui/views/mail-reading-pane.slint:337 -#: ui/components/composer-fields.slint:40 ui/views/mail-reading-pane.slint:337 ui/views/mail-reading-pane.slint:375 -#: ui/components/composer-fields.slint:40 ui/views/mail-reading-pane.slint:337 ui/views/mail-reading-pane.slint:375 ui/views/mail-reading-pane.slint:758 -#: ui/components/composer-fields.slint:40 ui/views/mail-reading-pane.slint:337 ui/views/mail-reading-pane.slint:375 ui/views/mail-reading-pane.slint:758 +#: ui/components/composer-fields.slint:40 ui/views/mail-reading-pane.slint:337 ui/views/mail-reading-pane.slint:377 +#: ui/components/composer-fields.slint:40 ui/views/mail-reading-pane.slint:337 ui/views/mail-reading-pane.slint:377 ui/views/mail-reading-pane.slint:770 +#: ui/components/composer-fields.slint:40 ui/views/mail-reading-pane.slint:337 ui/views/mail-reading-pane.slint:377 ui/views/mail-reading-pane.slint:770 msgid "Reply all" msgstr "" #: ui/components/composer-fields.slint:41 ui/views/mail-reading-pane.slint:338 -#: ui/components/composer-fields.slint:41 ui/views/mail-reading-pane.slint:338 ui/views/mail-reading-pane.slint:376 -#: ui/components/composer-fields.slint:41 ui/views/mail-reading-pane.slint:338 ui/views/mail-reading-pane.slint:376 ui/views/mail-reading-pane.slint:767 -#: ui/components/composer-fields.slint:41 ui/views/mail-reading-pane.slint:338 ui/views/mail-reading-pane.slint:376 ui/views/mail-reading-pane.slint:767 +#: ui/components/composer-fields.slint:41 ui/views/mail-reading-pane.slint:338 ui/views/mail-reading-pane.slint:378 +#: ui/components/composer-fields.slint:41 ui/views/mail-reading-pane.slint:338 ui/views/mail-reading-pane.slint:378 ui/views/mail-reading-pane.slint:779 +#: ui/components/composer-fields.slint:41 ui/views/mail-reading-pane.slint:338 ui/views/mail-reading-pane.slint:378 ui/views/mail-reading-pane.slint:779 msgid "Forward" msgstr "" @@ -2729,7 +2775,7 @@ msgstr "" msgid "Off" msgstr "" -#: ui/components/controls.slint:156 ui/views/mail-list-pane.slint:716 +#: ui/components/controls.slint:156 ui/views/mail-list-pane.slint:721 msgid "Clear selection" msgstr "" @@ -3084,7 +3130,8 @@ msgid "Done" msgstr "" #: ui/components/profile-switcher.slint:126 -#: ui/components/profile-switcher.slint:126 ui/views/files-view.slint:39 +#: ui/components/profile-switcher.slint:126 ui/dialogs/move-dialog.slint:51 +#: ui/components/profile-switcher.slint:126 ui/dialogs/move-dialog.slint:51 msgid "All accounts" msgstr "" @@ -3170,7 +3217,7 @@ msgstr "" msgid "Account settings" msgstr "" -#: ui/components/sidebar.slint:119 ui/views/mail-list-pane.slint:1466 +#: ui/components/sidebar.slint:119 ui/views/mail-list-pane.slint:1485 msgid "New email" msgstr "" @@ -3199,7 +3246,7 @@ msgstr "" msgid "All Inboxes" msgstr "" -#: ui/components/sidebar.slint:309 ui/dialogs/move-dialog.slint:39 +#: ui/components/sidebar.slint:309 ui/dialogs/move-dialog.slint:44 msgid "Inbox" msgstr "" @@ -3272,8 +3319,8 @@ msgstr "" #: ui/components/tab-overview.slint:69 #: ui/components/tab-overview.slint:69 #: ui/components/tab-overview.slint:69 ui/dialogs/settings-dialog.slint:1580 -#: ui/components/tab-overview.slint:69 ui/dialogs/settings-dialog.slint:1580 ui/dialogs/settings-dialog.slint:2649 -#: ui/components/tab-overview.slint:69 ui/dialogs/settings-dialog.slint:1580 ui/dialogs/settings-dialog.slint:2649 ui/views/contacts-view.slint:249 +#: ui/components/tab-overview.slint:69 ui/dialogs/settings-dialog.slint:1580 ui/dialogs/settings-dialog.slint:2650 +#: ui/components/tab-overview.slint:69 ui/dialogs/settings-dialog.slint:1580 ui/dialogs/settings-dialog.slint:2650 ui/views/contacts-view.slint:249 msgid "Contacts" msgstr "" @@ -3301,7 +3348,7 @@ msgstr "" #: ui/components/tab-overview.slint:198 ui/components/tab-overview.slint:222 #: ui/components/tab-overview.slint:198 ui/components/tab-overview.slint:222 -#: ui/components/tab-overview.slint:198 ui/components/tab-overview.slint:222 ui/dialogs/settings-dialog.slint:2415 +#: ui/components/tab-overview.slint:198 ui/components/tab-overview.slint:222 ui/dialogs/settings-dialog.slint:2416 msgid "Tabs" msgstr "" @@ -3477,7 +3524,7 @@ msgstr "" #: ui/components/theme-editor.slint:785 #: ui/components/theme-editor.slint:785 ui/dialogs/folder-dialog.slint:135 -#: ui/components/theme-editor.slint:785 ui/dialogs/folder-dialog.slint:135 ui/views/mail-reading-pane.slint:2044 +#: ui/components/theme-editor.slint:785 ui/dialogs/folder-dialog.slint:135 ui/views/mail-reading-pane.slint:2056 msgid "Save" msgstr "" @@ -3497,7 +3544,7 @@ msgstr "" msgid "Close tabs to the right" msgstr "" -#: ui/components/zoom-controls.slint:20 ui/dialogs/settings-dialog.slint:2411 +#: ui/components/zoom-controls.slint:20 ui/dialogs/settings-dialog.slint:2412 msgid "Zoom out" msgstr "" @@ -3509,7 +3556,7 @@ msgstr "" msgid "Zoom percentage" msgstr "" -#: ui/components/zoom-controls.slint:39 ui/dialogs/settings-dialog.slint:2410 +#: ui/components/zoom-controls.slint:39 ui/dialogs/settings-dialog.slint:2411 msgid "Zoom in" msgstr "" @@ -3714,7 +3761,7 @@ msgstr "" msgid "Inside “{}”" msgstr "" -#: ui/dialogs/folder-dialog.slint:133 ui/views/mail-reading-pane.slint:1993 +#: ui/dialogs/folder-dialog.slint:133 ui/views/mail-reading-pane.slint:2005 msgid "Create label" msgstr "" @@ -4249,41 +4296,45 @@ msgid "Dismiss" msgstr "" #. Local mail has no archive or spam. -#: ui/dialogs/move-dialog.slint:40 ui/dialogs/settings-dialog.slint:2397 -#: ui/dialogs/move-dialog.slint:40 ui/dialogs/settings-dialog.slint:2397 ui/views/mail-reading-pane.slint:349 -#: ui/dialogs/move-dialog.slint:40 ui/dialogs/settings-dialog.slint:2397 ui/views/mail-reading-pane.slint:349 +#: ui/dialogs/move-dialog.slint:45 ui/dialogs/settings-dialog.slint:2397 +#: ui/dialogs/move-dialog.slint:45 ui/dialogs/settings-dialog.slint:2397 ui/views/mail-reading-pane.slint:349 +#: ui/dialogs/move-dialog.slint:45 ui/dialogs/settings-dialog.slint:2397 ui/views/mail-reading-pane.slint:349 msgid "Archive" msgstr "" -#: ui/dialogs/move-dialog.slint:41 +#: ui/dialogs/move-dialog.slint:46 msgid "Spam" msgstr "" -#: ui/dialogs/move-dialog.slint:42 +#: ui/dialogs/move-dialog.slint:47 msgid "Trash" msgstr "" -#: ui/dialogs/move-dialog.slint:43 +#: ui/dialogs/move-dialog.slint:48 msgid "Recent" msgstr "" -#: ui/dialogs/move-dialog.slint:44 +#: ui/dialogs/move-dialog.slint:49 msgid "All folders" msgstr "" -#: ui/dialogs/move-dialog.slint:67 ui/dialogs/move-dialog.slint:191 +#: ui/dialogs/move-dialog.slint:74 ui/dialogs/move-dialog.slint:198 +msgid "Copy to" +msgstr "" + +#: ui/dialogs/move-dialog.slint:74 ui/dialogs/move-dialog.slint:198 msgid "Move to" msgstr "" -#: ui/dialogs/move-dialog.slint:212 ui/dialogs/move-dialog.slint:213 +#: ui/dialogs/move-dialog.slint:219 ui/dialogs/move-dialog.slint:220 msgid "Search folders" msgstr "" -#: ui/dialogs/move-dialog.slint:225 ui/views/files-view.slint:581 +#: ui/dialogs/move-dialog.slint:232 ui/views/files-view.slint:581 msgid "Folders" msgstr "" -#: ui/dialogs/move-dialog.slint:324 +#: ui/dialogs/move-dialog.slint:332 msgid "No matching folders" msgstr "" @@ -4396,8 +4447,8 @@ msgstr "" #: ui/dialogs/rendering-info-dialog.slint:25 #: ui/dialogs/rendering-info-dialog.slint:25 #: ui/dialogs/rendering-info-dialog.slint:25 ui/views/mail-reading-pane.slint:344 -#: ui/dialogs/rendering-info-dialog.slint:25 ui/views/mail-reading-pane.slint:344 ui/views/mail-reading-pane.slint:382 -#: ui/dialogs/rendering-info-dialog.slint:25 ui/views/mail-reading-pane.slint:344 ui/views/mail-reading-pane.slint:382 ui/views/mail-reading-pane.slint:1912 +#: ui/dialogs/rendering-info-dialog.slint:25 ui/views/mail-reading-pane.slint:344 ui/views/mail-reading-pane.slint:384 +#: ui/dialogs/rendering-info-dialog.slint:25 ui/views/mail-reading-pane.slint:344 ui/views/mail-reading-pane.slint:384 ui/views/mail-reading-pane.slint:1924 msgid "About email rendering" msgstr "" @@ -4422,7 +4473,7 @@ msgstr "" msgid "Report issue" msgstr "" -#: ui/dialogs/settings-dialog.slint:70 ui/dialogs/settings-dialog.slint:2466 +#: ui/dialogs/settings-dialog.slint:70 ui/dialogs/settings-dialog.slint:2467 msgid "Sync" msgstr "" @@ -4466,8 +4517,8 @@ msgstr "" msgid "Open Storage to calculate local usage." msgstr "" -#: ui/dialogs/settings-dialog.slint:269 ui/views/mail-reading-pane.slint:1266 -#: ui/dialogs/settings-dialog.slint:269 ui/views/mail-reading-pane.slint:1266 +#: ui/dialogs/settings-dialog.slint:269 ui/views/mail-reading-pane.slint:1278 +#: ui/dialogs/settings-dialog.slint:269 ui/views/mail-reading-pane.slint:1278 msgid "Attachments" msgstr "" @@ -4551,7 +4602,7 @@ msgstr "" msgid "1 year" msgstr "" -#: ui/dialogs/settings-dialog.slint:664 ui/views/mail-list-pane.slint:667 +#: ui/dialogs/settings-dialog.slint:664 ui/views/mail-list-pane.slint:672 msgid "All mail" msgstr "" @@ -4662,47 +4713,47 @@ msgstr "" msgid "about version credits contributors license flectar" msgstr "" -#: ui/dialogs/settings-dialog.slint:1021 ui/dialogs/settings-dialog.slint:2715 +#: ui/dialogs/settings-dialog.slint:1021 ui/dialogs/settings-dialog.slint:2716 msgid "SETTINGS" msgstr "" -#: ui/dialogs/settings-dialog.slint:1024 ui/dialogs/settings-dialog.slint:2717 +#: ui/dialogs/settings-dialog.slint:1024 ui/dialogs/settings-dialog.slint:2718 msgid "No settings found" msgstr "" #: ui/dialogs/settings-dialog.slint:1025 ui/dialogs/settings-dialog.slint:1055 -#: ui/dialogs/settings-dialog.slint:1025 ui/dialogs/settings-dialog.slint:1055 ui/dialogs/settings-dialog.slint:2718 +#: ui/dialogs/settings-dialog.slint:1025 ui/dialogs/settings-dialog.slint:1055 ui/dialogs/settings-dialog.slint:2719 msgid "General" msgstr "" #: ui/dialogs/settings-dialog.slint:1026 ui/dialogs/settings-dialog.slint:1730 -#: ui/dialogs/settings-dialog.slint:1026 ui/dialogs/settings-dialog.slint:1730 ui/dialogs/settings-dialog.slint:2719 -#: ui/dialogs/settings-dialog.slint:1026 ui/dialogs/settings-dialog.slint:1730 ui/dialogs/settings-dialog.slint:2719 ui/dialogs/settings-dialog.slint:3164 +#: ui/dialogs/settings-dialog.slint:1026 ui/dialogs/settings-dialog.slint:1730 ui/dialogs/settings-dialog.slint:2720 +#: ui/dialogs/settings-dialog.slint:1026 ui/dialogs/settings-dialog.slint:1730 ui/dialogs/settings-dialog.slint:2720 ui/dialogs/settings-dialog.slint:3165 msgid "Accounts" msgstr "" #: ui/dialogs/settings-dialog.slint:1028 ui/dialogs/settings-dialog.slint:2341 -#: ui/dialogs/settings-dialog.slint:1028 ui/dialogs/settings-dialog.slint:2341 ui/dialogs/settings-dialog.slint:2721 +#: ui/dialogs/settings-dialog.slint:1028 ui/dialogs/settings-dialog.slint:2341 ui/dialogs/settings-dialog.slint:2722 msgid "Notifications" msgstr "" -#: ui/dialogs/settings-dialog.slint:1029 ui/dialogs/settings-dialog.slint:2651 -#: ui/dialogs/settings-dialog.slint:1029 ui/dialogs/settings-dialog.slint:2651 +#: ui/dialogs/settings-dialog.slint:1029 ui/dialogs/settings-dialog.slint:2652 +#: ui/dialogs/settings-dialog.slint:1029 ui/dialogs/settings-dialog.slint:2652 msgid "Shortcuts" msgstr "" #: ui/dialogs/settings-dialog.slint:1030 ui/dialogs/settings-dialog.slint:2249 -#: ui/dialogs/settings-dialog.slint:1030 ui/dialogs/settings-dialog.slint:2249 ui/dialogs/settings-dialog.slint:2723 +#: ui/dialogs/settings-dialog.slint:1030 ui/dialogs/settings-dialog.slint:2249 ui/dialogs/settings-dialog.slint:2724 msgid "Storage" msgstr "" -#: ui/dialogs/settings-dialog.slint:1031 ui/dialogs/settings-dialog.slint:2441 -#: ui/dialogs/settings-dialog.slint:1031 ui/dialogs/settings-dialog.slint:2441 ui/dialogs/settings-dialog.slint:2724 +#: ui/dialogs/settings-dialog.slint:1031 ui/dialogs/settings-dialog.slint:2442 +#: ui/dialogs/settings-dialog.slint:1031 ui/dialogs/settings-dialog.slint:2442 ui/dialogs/settings-dialog.slint:2725 msgid "Services" msgstr "" -#: ui/dialogs/settings-dialog.slint:1032 ui/dialogs/settings-dialog.slint:2528 -#: ui/dialogs/settings-dialog.slint:1032 ui/dialogs/settings-dialog.slint:2528 ui/dialogs/settings-dialog.slint:2725 +#: ui/dialogs/settings-dialog.slint:1032 ui/dialogs/settings-dialog.slint:2529 +#: ui/dialogs/settings-dialog.slint:1032 ui/dialogs/settings-dialog.slint:2529 ui/dialogs/settings-dialog.slint:2726 msgid "About" msgstr "" @@ -5040,12 +5091,12 @@ msgid "" "you create one." msgstr "" -#: ui/dialogs/settings-dialog.slint:1856 ui/dialogs/settings-dialog.slint:3060 -#: ui/dialogs/settings-dialog.slint:1856 ui/dialogs/settings-dialog.slint:3060 +#: ui/dialogs/settings-dialog.slint:1856 ui/dialogs/settings-dialog.slint:3061 +#: ui/dialogs/settings-dialog.slint:1856 ui/dialogs/settings-dialog.slint:3061 msgid "Edit profile" msgstr "" -#: ui/dialogs/settings-dialog.slint:1866 ui/dialogs/settings-dialog.slint:3315 +#: ui/dialogs/settings-dialog.slint:1866 ui/dialogs/settings-dialog.slint:3316 msgid "Delete profile" msgstr "" @@ -5246,12 +5297,12 @@ msgstr "" msgid "https://contacts.example.com/" msgstr "" -#: ui/dialogs/settings-dialog.slint:2229 ui/dialogs/settings-dialog.slint:2828 -#: ui/dialogs/settings-dialog.slint:2229 ui/dialogs/settings-dialog.slint:2828 ui/dialogs/settings-dialog.slint:2958 +#: ui/dialogs/settings-dialog.slint:2229 ui/dialogs/settings-dialog.slint:2829 +#: ui/dialogs/settings-dialog.slint:2229 ui/dialogs/settings-dialog.slint:2829 ui/dialogs/settings-dialog.slint:2959 msgid "Username" msgstr "" -#: ui/dialogs/settings-dialog.slint:2229 ui/dialogs/settings-dialog.slint:2836 +#: ui/dialogs/settings-dialog.slint:2229 ui/dialogs/settings-dialog.slint:2837 msgid "App password" msgstr "" @@ -5259,8 +5310,8 @@ msgstr "" msgid "Disconnect" msgstr "" -#: ui/dialogs/settings-dialog.slint:2230 ui/dialogs/settings-dialog.slint:2863 -#: ui/dialogs/settings-dialog.slint:2230 ui/dialogs/settings-dialog.slint:2863 +#: ui/dialogs/settings-dialog.slint:2230 ui/dialogs/settings-dialog.slint:2864 +#: ui/dialogs/settings-dialog.slint:2230 ui/dialogs/settings-dialog.slint:2864 msgid "Connect" msgstr "" @@ -5278,14 +5329,14 @@ msgid "" "elsewhere." msgstr "" -#: ui/dialogs/settings-dialog.slint:2280 ui/dialogs/settings-dialog.slint:3264 +#: ui/dialogs/settings-dialog.slint:2280 ui/dialogs/settings-dialog.slint:3265 msgid "One message" msgid_plural "{} messages" msgstr[0] "" msgstr[1] "" -#: ui/dialogs/settings-dialog.slint:2288 ui/dialogs/settings-dialog.slint:3244 -#: ui/dialogs/settings-dialog.slint:2288 ui/dialogs/settings-dialog.slint:3244 ui/dialogs/settings-dialog.slint:3292 +#: ui/dialogs/settings-dialog.slint:2288 ui/dialogs/settings-dialog.slint:3245 +#: ui/dialogs/settings-dialog.slint:2288 ui/dialogs/settings-dialog.slint:3245 ui/dialogs/settings-dialog.slint:3293 msgid "Export mail…" msgstr "" @@ -5363,7 +5414,7 @@ msgstr "" msgid "This does not delete anything from Gmail, Outlook, or your IMAP server." msgstr "" -#: ui/dialogs/settings-dialog.slint:2328 ui/dialogs/settings-dialog.slint:3248 +#: ui/dialogs/settings-dialog.slint:2328 ui/dialogs/settings-dialog.slint:3249 msgid "Delete everything" msgstr "" @@ -5395,7 +5446,7 @@ msgstr "" msgid "Check every account for new mail" msgstr "" -#: ui/dialogs/settings-dialog.slint:2396 ui/views/mail-list-pane.slint:665 +#: ui/dialogs/settings-dialog.slint:2396 ui/views/mail-list-pane.slint:670 msgid "Search mail" msgstr "" @@ -5403,542 +5454,546 @@ msgstr "" msgid "Move to a folder" msgstr "" -#: ui/dialogs/settings-dialog.slint:2399 ui/views/mail-list-pane.slint:747 -#: ui/dialogs/settings-dialog.slint:2399 ui/views/mail-list-pane.slint:747 ui/views/mail-reading-pane.slint:351 -#: ui/dialogs/settings-dialog.slint:2399 ui/views/mail-list-pane.slint:747 ui/views/mail-reading-pane.slint:351 ui/views/mail-reading-pane.slint:809 +#: ui/dialogs/settings-dialog.slint:2399 +msgid "Copy to a folder" +msgstr "" + +#: ui/dialogs/settings-dialog.slint:2400 ui/views/mail-list-pane.slint:752 +#: ui/dialogs/settings-dialog.slint:2400 ui/views/mail-list-pane.slint:752 ui/views/mail-reading-pane.slint:352 +#: ui/dialogs/settings-dialog.slint:2400 ui/views/mail-list-pane.slint:752 ui/views/mail-reading-pane.slint:352 ui/views/mail-reading-pane.slint:821 msgid "Move to spam" msgstr "" -#: ui/dialogs/settings-dialog.slint:2400 ui/views/mail-list-pane.slint:756 -#: ui/dialogs/settings-dialog.slint:2400 ui/views/mail-list-pane.slint:756 ui/views/mail-reading-pane.slint:333 -#: ui/dialogs/settings-dialog.slint:2400 ui/views/mail-list-pane.slint:756 ui/views/mail-reading-pane.slint:333 ui/views/mail-reading-pane.slint:370 -#: ui/dialogs/settings-dialog.slint:2400 ui/views/mail-list-pane.slint:756 ui/views/mail-reading-pane.slint:333 ui/views/mail-reading-pane.slint:370 ui/views/mail-reading-pane.slint:404 -#: ui/dialogs/settings-dialog.slint:2400 ui/views/mail-list-pane.slint:756 ui/views/mail-reading-pane.slint:333 ui/views/mail-reading-pane.slint:370 ui/views/mail-reading-pane.slint:404 +#: ui/dialogs/settings-dialog.slint:2401 ui/views/mail-list-pane.slint:761 +#: ui/dialogs/settings-dialog.slint:2401 ui/views/mail-list-pane.slint:761 ui/views/mail-reading-pane.slint:333 +#: ui/dialogs/settings-dialog.slint:2401 ui/views/mail-list-pane.slint:761 ui/views/mail-reading-pane.slint:333 ui/views/mail-reading-pane.slint:372 +#: ui/dialogs/settings-dialog.slint:2401 ui/views/mail-list-pane.slint:761 ui/views/mail-reading-pane.slint:333 ui/views/mail-reading-pane.slint:372 ui/views/mail-reading-pane.slint:408 +#: ui/dialogs/settings-dialog.slint:2401 ui/views/mail-list-pane.slint:761 ui/views/mail-reading-pane.slint:333 ui/views/mail-reading-pane.slint:372 ui/views/mail-reading-pane.slint:408 msgid "Move to trash" msgstr "" -#: ui/dialogs/settings-dialog.slint:2402 +#: ui/dialogs/settings-dialog.slint:2403 msgid "Navigation" msgstr "" -#: ui/dialogs/settings-dialog.slint:2403 +#: ui/dialogs/settings-dialog.slint:2404 msgid "Move between messages" msgstr "" -#: ui/dialogs/settings-dialog.slint:2404 +#: ui/dialogs/settings-dialog.slint:2405 msgid "Open message" msgstr "" -#: ui/dialogs/settings-dialog.slint:2405 +#: ui/dialogs/settings-dialog.slint:2406 msgid "Select message" msgstr "" -#: ui/dialogs/settings-dialog.slint:2406 +#: ui/dialogs/settings-dialog.slint:2407 msgid "Switch between list and message" msgstr "" -#: ui/dialogs/settings-dialog.slint:2408 +#: ui/dialogs/settings-dialog.slint:2409 msgid "Reading" msgstr "" -#: ui/dialogs/settings-dialog.slint:2409 ui/views/mail-reading-pane.slint:1276 -#: ui/dialogs/settings-dialog.slint:2409 ui/views/mail-reading-pane.slint:1276 +#: ui/dialogs/settings-dialog.slint:2410 ui/views/mail-reading-pane.slint:1288 +#: ui/dialogs/settings-dialog.slint:2410 ui/views/mail-reading-pane.slint:1288 msgid "Find in message" msgstr "" -#: ui/dialogs/settings-dialog.slint:2412 +#: ui/dialogs/settings-dialog.slint:2413 msgid "Reset zoom" msgstr "" -#: ui/dialogs/settings-dialog.slint:2413 ui/views/mail-reading-pane.slint:1605 +#: ui/dialogs/settings-dialog.slint:2414 ui/views/mail-reading-pane.slint:1617 msgid "Caret browsing" msgstr "" -#: ui/dialogs/settings-dialog.slint:2419 +#: ui/dialogs/settings-dialog.slint:2420 msgid "Next tab" msgstr "" -#: ui/dialogs/settings-dialog.slint:2420 +#: ui/dialogs/settings-dialog.slint:2421 msgid "Previous tab" msgstr "" -#: ui/dialogs/settings-dialog.slint:2421 +#: ui/dialogs/settings-dialog.slint:2422 msgid "Go to tab 1–8" msgstr "" -#: ui/dialogs/settings-dialog.slint:2422 +#: ui/dialogs/settings-dialog.slint:2423 msgid "Go to last tab" msgstr "" -#: ui/dialogs/settings-dialog.slint:2442 +#: ui/dialogs/settings-dialog.slint:2443 msgid "" "Optional paid services from Flectar. Flectar Mail stays free and works " "without them." msgstr "" -#: ui/dialogs/settings-dialog.slint:2447 +#: ui/dialogs/settings-dialog.slint:2448 msgid "Flectar account" msgstr "" -#: ui/dialogs/settings-dialog.slint:2447 +#: ui/dialogs/settings-dialog.slint:2448 msgid "Not signed in" msgstr "" -#: ui/dialogs/settings-dialog.slint:2449 +#: ui/dialogs/settings-dialog.slint:2450 msgid "" "A Flectar account is only needed for these optional services. Learn more at " "flectar.com/mail." msgstr "" -#: ui/dialogs/settings-dialog.slint:2454 +#: ui/dialogs/settings-dialog.slint:2455 msgid "Sign in with Flectar" msgstr "" -#: ui/dialogs/settings-dialog.slint:2454 +#: ui/dialogs/settings-dialog.slint:2455 msgid "Sign-in coming soon" msgstr "" -#: ui/dialogs/settings-dialog.slint:2467 +#: ui/dialogs/settings-dialog.slint:2468 msgid "Your setup on every device" msgstr "" -#: ui/dialogs/settings-dialog.slint:2468 +#: ui/dialogs/settings-dialog.slint:2469 msgid "" "Sign in on a new phone or computer and your accounts, calendars, contacts, " "and preferences are ready without setting them up again." msgstr "" -#: ui/dialogs/settings-dialog.slint:2470 +#: ui/dialogs/settings-dialog.slint:2471 msgid "Mail accounts, profiles, and signatures" msgstr "" -#: ui/dialogs/settings-dialog.slint:2471 +#: ui/dialogs/settings-dialog.slint:2472 msgid "Calendar and contact connections" msgstr "" -#: ui/dialogs/settings-dialog.slint:2472 +#: ui/dialogs/settings-dialog.slint:2473 msgid "Themes, layout, and notification preferences" msgstr "" -#: ui/dialogs/settings-dialog.slint:2473 +#: ui/dialogs/settings-dialog.slint:2474 msgid "Account credentials, encrypted on this device before upload" msgstr "" -#: ui/dialogs/settings-dialog.slint:2475 +#: ui/dialogs/settings-dialog.slint:2476 msgid "" "Your messages stay with your mail provider. Sync only carries your Flectar " "Mail setup." msgstr "" -#: ui/dialogs/settings-dialog.slint:2490 +#: ui/dialogs/settings-dialog.slint:2491 msgid "Business email" msgstr "" -#: ui/dialogs/settings-dialog.slint:2491 +#: ui/dialogs/settings-dialog.slint:2492 msgid "Mailboxes on your own domain" msgstr "" -#: ui/dialogs/settings-dialog.slint:2492 +#: ui/dialogs/settings-dialog.slint:2493 msgid "Professional addresses such as you@yourcompany.com, hosted by Flectar." msgstr "" -#: ui/dialogs/settings-dialog.slint:2494 +#: ui/dialogs/settings-dialog.slint:2495 msgid "Connect a domain you already own" msgstr "" -#: ui/dialogs/settings-dialog.slint:2495 +#: ui/dialogs/settings-dialog.slint:2496 msgid "Create and manage mailboxes for your team" msgstr "" -#: ui/dialogs/settings-dialog.slint:2496 +#: ui/dialogs/settings-dialog.slint:2497 msgid "New mailboxes appear in Flectar Mail automatically" msgstr "" -#: ui/dialogs/settings-dialog.slint:2498 +#: ui/dialogs/settings-dialog.slint:2499 msgid "Billed per mailbox." msgstr "" -#: ui/dialogs/settings-dialog.slint:2510 +#: ui/dialogs/settings-dialog.slint:2511 msgid "" "These services are in development. Plans and availability will be announced " "on flectar.com/mail." msgstr "" -#: ui/dialogs/settings-dialog.slint:2529 +#: ui/dialogs/settings-dialog.slint:2530 msgid "Flectar Mail, its makers, and its community." msgstr "" -#: ui/dialogs/settings-dialog.slint:2545 ui/views/onboarding-view.slint:121 +#: ui/dialogs/settings-dialog.slint:2546 ui/views/onboarding-view.slint:121 msgid "Flectar Mail logo" msgstr "" -#: ui/dialogs/settings-dialog.slint:2565 +#: ui/dialogs/settings-dialog.slint:2566 msgid "A native mail client for a calm, private inbox." msgstr "" -#: ui/dialogs/settings-dialog.slint:2577 +#: ui/dialogs/settings-dialog.slint:2578 msgid "Version {}" msgstr "" -#: ui/dialogs/settings-dialog.slint:2588 +#: ui/dialogs/settings-dialog.slint:2589 msgid "Credits" msgstr "" -#: ui/dialogs/settings-dialog.slint:2588 +#: ui/dialogs/settings-dialog.slint:2589 msgid "Built by Flectar with help from the open-source community." msgstr "" -#: ui/dialogs/settings-dialog.slint:2598 +#: ui/dialogs/settings-dialog.slint:2599 msgid "Flectar" msgstr "" -#: ui/dialogs/settings-dialog.slint:2599 +#: ui/dialogs/settings-dialog.slint:2600 msgid "Company website" msgstr "" -#: ui/dialogs/settings-dialog.slint:2609 +#: ui/dialogs/settings-dialog.slint:2610 msgid "Open-source contributors" msgstr "" -#: ui/dialogs/settings-dialog.slint:2610 +#: ui/dialogs/settings-dialog.slint:2611 msgid "Meet the people who have contributed on GitHub" msgstr "" -#: ui/dialogs/settings-dialog.slint:2623 +#: ui/dialogs/settings-dialog.slint:2624 msgid "License" msgstr "" -#: ui/dialogs/settings-dialog.slint:2623 +#: ui/dialogs/settings-dialog.slint:2624 msgid "AGPL-3.0-only." msgstr "" -#: ui/dialogs/settings-dialog.slint:2623 +#: ui/dialogs/settings-dialog.slint:2624 msgid "" "Flectar Mail is released under the GNU Affero General Public License v3.0 " "only." msgstr "" -#: ui/dialogs/settings-dialog.slint:2629 +#: ui/dialogs/settings-dialog.slint:2630 msgid "Third-party components retain their own licenses and notices." msgstr "" -#: ui/dialogs/settings-dialog.slint:2656 +#: ui/dialogs/settings-dialog.slint:2657 msgid "Settings sections" msgstr "" -#: ui/dialogs/settings-dialog.slint:2668 +#: ui/dialogs/settings-dialog.slint:2669 msgid "Close settings" msgstr "" -#: ui/dialogs/settings-dialog.slint:2690 +#: ui/dialogs/settings-dialog.slint:2691 msgid "Close settings sections" msgstr "" -#: ui/dialogs/settings-dialog.slint:2738 ui/dialogs/settings-dialog.slint:2787 +#: ui/dialogs/settings-dialog.slint:2739 ui/dialogs/settings-dialog.slint:2788 msgid "CalDAV settings" msgstr "" -#: ui/dialogs/settings-dialog.slint:2788 +#: ui/dialogs/settings-dialog.slint:2789 msgid "Discover new calendars." msgstr "" -#: ui/dialogs/settings-dialog.slint:2788 +#: ui/dialogs/settings-dialog.slint:2789 msgid "" "Works with iCloud, Fastmail, Nextcloud, and standards-compliant CalDAV " "servers." msgstr "" -#: ui/dialogs/settings-dialog.slint:2813 +#: ui/dialogs/settings-dialog.slint:2814 msgid "Server address" msgstr "" -#: ui/dialogs/settings-dialog.slint:2815 +#: ui/dialogs/settings-dialog.slint:2816 msgid "https://calendar.example.com/" msgstr "" -#: ui/dialogs/settings-dialog.slint:2820 +#: ui/dialogs/settings-dialog.slint:2821 msgid "" "To sync one calendar, such as a shared mailbox's, enter that calendar's own " "address." msgstr "" -#: ui/dialogs/settings-dialog.slint:2830 ui/dialogs/settings-dialog.slint:2951 -#: ui/dialogs/settings-dialog.slint:2830 ui/dialogs/settings-dialog.slint:2951 +#: ui/dialogs/settings-dialog.slint:2831 ui/dialogs/settings-dialog.slint:2952 +#: ui/dialogs/settings-dialog.slint:2831 ui/dialogs/settings-dialog.slint:2952 msgid "Usually your email address" msgstr "" -#: ui/dialogs/settings-dialog.slint:2836 +#: ui/dialogs/settings-dialog.slint:2837 msgid "New app password · Optional" msgstr "" -#: ui/dialogs/settings-dialog.slint:2838 +#: ui/dialogs/settings-dialog.slint:2839 msgid "Leave blank to keep the current password" msgstr "" -#: ui/dialogs/settings-dialog.slint:2838 ui/dialogs/settings-dialog.slint:2960 +#: ui/dialogs/settings-dialog.slint:2839 ui/dialogs/settings-dialog.slint:2961 msgid "Stored securely in your system keyring" msgstr "" -#: ui/dialogs/settings-dialog.slint:2863 ui/dialogs/settings-dialog.slint:3039 -#: ui/dialogs/settings-dialog.slint:2863 ui/dialogs/settings-dialog.slint:3039 +#: ui/dialogs/settings-dialog.slint:2864 ui/dialogs/settings-dialog.slint:3040 +#: ui/dialogs/settings-dialog.slint:2864 ui/dialogs/settings-dialog.slint:3040 msgid "Connecting…" msgstr "" -#: ui/dialogs/settings-dialog.slint:2881 +#: ui/dialogs/settings-dialog.slint:2882 msgid "Edit mail account" msgstr "" -#: ui/dialogs/settings-dialog.slint:2881 ui/dialogs/settings-dialog.slint:2929 -#: ui/dialogs/settings-dialog.slint:2881 ui/dialogs/settings-dialog.slint:2929 +#: ui/dialogs/settings-dialog.slint:2882 ui/dialogs/settings-dialog.slint:2930 +#: ui/dialogs/settings-dialog.slint:2882 ui/dialogs/settings-dialog.slint:2930 msgid "Connect a mail account" msgstr "" -#: ui/dialogs/settings-dialog.slint:2929 +#: ui/dialogs/settings-dialog.slint:2930 msgid "Edit {}" msgstr "" -#: ui/dialogs/settings-dialog.slint:2930 +#: ui/dialogs/settings-dialog.slint:2931 msgid "Uses JMAP for mail sync and sending, with automatic discovery." msgstr "" -#: ui/dialogs/settings-dialog.slint:2930 +#: ui/dialogs/settings-dialog.slint:2931 msgid "Downloads mail from a POP3 server to this device and sends through SMTP." msgstr "" -#: ui/dialogs/settings-dialog.slint:2930 +#: ui/dialogs/settings-dialog.slint:2931 msgid "Uses separate IMAP and SMTP servers for mail sync and sending." msgstr "" -#: ui/dialogs/settings-dialog.slint:2950 ui/dialogs/settings-dialog.slint:2957 +#: ui/dialogs/settings-dialog.slint:2951 ui/dialogs/settings-dialog.slint:2958 msgid "name@example.com" msgstr "" -#: ui/dialogs/settings-dialog.slint:2960 ui/views/onboarding-view.slint:284 -#: ui/dialogs/settings-dialog.slint:2960 ui/views/onboarding-view.slint:284 +#: ui/dialogs/settings-dialog.slint:2961 ui/views/onboarding-view.slint:284 +#: ui/dialogs/settings-dialog.slint:2961 ui/views/onboarding-view.slint:284 msgid "Password, app password, or API token" msgstr "" -#: ui/dialogs/settings-dialog.slint:2960 ui/views/onboarding-view.slint:284 -#: ui/dialogs/settings-dialog.slint:2960 ui/views/onboarding-view.slint:284 +#: ui/dialogs/settings-dialog.slint:2961 ui/views/onboarding-view.slint:284 +#: ui/dialogs/settings-dialog.slint:2961 ui/views/onboarding-view.slint:284 msgid "Password or app password" msgstr "" -#: ui/dialogs/settings-dialog.slint:2967 ui/views/onboarding-view.slint:297 +#: ui/dialogs/settings-dialog.slint:2968 ui/views/onboarding-view.slint:297 msgid "IMAP" msgstr "" -#: ui/dialogs/settings-dialog.slint:2968 ui/views/onboarding-view.slint:305 +#: ui/dialogs/settings-dialog.slint:2969 ui/views/onboarding-view.slint:305 msgid "POP3" msgstr "" -#: ui/dialogs/settings-dialog.slint:2969 ui/views/onboarding-view.slint:313 +#: ui/dialogs/settings-dialog.slint:2970 ui/views/onboarding-view.slint:313 msgid "JMAP" msgstr "" -#: ui/dialogs/settings-dialog.slint:2973 +#: ui/dialogs/settings-dialog.slint:2974 msgid "Server address · Optional" msgstr "" -#: ui/dialogs/settings-dialog.slint:2975 +#: ui/dialogs/settings-dialog.slint:2976 msgid "Discovered from your email domain" msgstr "" -#: ui/dialogs/settings-dialog.slint:2982 ui/views/onboarding-view.slint:337 -#: ui/dialogs/settings-dialog.slint:2982 ui/views/onboarding-view.slint:337 +#: ui/dialogs/settings-dialog.slint:2983 ui/views/onboarding-view.slint:337 +#: ui/dialogs/settings-dialog.slint:2983 ui/views/onboarding-view.slint:337 msgid "POP3 server" msgstr "" -#: ui/dialogs/settings-dialog.slint:2982 ui/views/onboarding-view.slint:337 -#: ui/dialogs/settings-dialog.slint:2982 ui/views/onboarding-view.slint:337 +#: ui/dialogs/settings-dialog.slint:2983 ui/views/onboarding-view.slint:337 +#: ui/dialogs/settings-dialog.slint:2983 ui/views/onboarding-view.slint:337 msgid "IMAP server" msgstr "" -#: ui/dialogs/settings-dialog.slint:2982 +#: ui/dialogs/settings-dialog.slint:2983 msgid "pop.example.com" msgstr "" -#: ui/dialogs/settings-dialog.slint:2982 +#: ui/dialogs/settings-dialog.slint:2983 msgid "imap.example.com" msgstr "" -#: ui/dialogs/settings-dialog.slint:2983 ui/dialogs/settings-dialog.slint:2990 -#: ui/dialogs/settings-dialog.slint:2983 ui/dialogs/settings-dialog.slint:2990 ui/views/onboarding-view.slint:367 +#: ui/dialogs/settings-dialog.slint:2984 ui/dialogs/settings-dialog.slint:2991 +#: ui/dialogs/settings-dialog.slint:2984 ui/dialogs/settings-dialog.slint:2991 ui/views/onboarding-view.slint:367 msgid "Port" msgstr "" -#: ui/dialogs/settings-dialog.slint:2989 ui/views/onboarding-view.slint:359 -#: ui/dialogs/settings-dialog.slint:2989 ui/views/onboarding-view.slint:359 +#: ui/dialogs/settings-dialog.slint:2990 ui/views/onboarding-view.slint:359 +#: ui/dialogs/settings-dialog.slint:2990 ui/views/onboarding-view.slint:359 msgid "SMTP server" msgstr "" -#: ui/dialogs/settings-dialog.slint:2989 +#: ui/dialogs/settings-dialog.slint:2990 msgid "smtp.example.com" msgstr "" -#: ui/dialogs/settings-dialog.slint:2993 ui/views/onboarding-view.slint:375 +#: ui/dialogs/settings-dialog.slint:2994 ui/views/onboarding-view.slint:375 msgid "" "Proton Bridge: use the local servers, ports and password shown in Bridge, " "and match its encryption settings." msgstr "" -#: ui/dialogs/settings-dialog.slint:3039 +#: ui/dialogs/settings-dialog.slint:3040 msgid "Save account" msgstr "" -#: ui/dialogs/settings-dialog.slint:3039 ui/views/onboarding-view.slint:407 +#: ui/dialogs/settings-dialog.slint:3040 ui/views/onboarding-view.slint:407 msgid "Connect account" msgstr "" -#: ui/dialogs/settings-dialog.slint:3060 ui/dialogs/settings-dialog.slint:3116 -#: ui/dialogs/settings-dialog.slint:3060 ui/dialogs/settings-dialog.slint:3116 +#: ui/dialogs/settings-dialog.slint:3061 ui/dialogs/settings-dialog.slint:3117 +#: ui/dialogs/settings-dialog.slint:3061 ui/dialogs/settings-dialog.slint:3117 msgid "Create profile" msgstr "" -#: ui/dialogs/settings-dialog.slint:3117 +#: ui/dialogs/settings-dialog.slint:3118 msgid "" "Its color marks its accounts' mail, and choosing it shows only their mail." msgstr "" -#: ui/dialogs/settings-dialog.slint:3122 +#: ui/dialogs/settings-dialog.slint:3123 msgid "Profile name" msgstr "" -#: ui/dialogs/settings-dialog.slint:3124 +#: ui/dialogs/settings-dialog.slint:3125 msgid "Personal or Work" msgstr "" -#: ui/dialogs/settings-dialog.slint:3130 ui/views/mail-reading-pane.slint:2008 +#: ui/dialogs/settings-dialog.slint:3131 ui/views/mail-reading-pane.slint:2020 msgid "Color" msgstr "" -#: ui/dialogs/settings-dialog.slint:3139 +#: ui/dialogs/settings-dialog.slint:3140 msgid "Profile color {}" msgstr "" -#: ui/dialogs/settings-dialog.slint:3181 +#: ui/dialogs/settings-dialog.slint:3182 msgid "Save profile" msgstr "" -#: ui/dialogs/settings-dialog.slint:3189 +#: ui/dialogs/settings-dialog.slint:3190 msgid "Could not save this profile. Check the name and try again." msgstr "" -#: ui/dialogs/settings-dialog.slint:3207 +#: ui/dialogs/settings-dialog.slint:3208 msgid "Choose a profile for {}" msgstr "" -#: ui/dialogs/settings-dialog.slint:3237 +#: ui/dialogs/settings-dialog.slint:3238 msgid "Delete all local data?" msgstr "" -#: ui/dialogs/settings-dialog.slint:3238 +#: ui/dialogs/settings-dialog.slint:3239 msgid "" "This removes every account, downloaded message, calendar, contact, " "preference, and credential stored by Flectar Mail on this device." msgstr "" -#: ui/dialogs/settings-dialog.slint:3240 +#: ui/dialogs/settings-dialog.slint:3241 msgid "" "Imported mail and the mail of any POP3 account are stored only on this " "device and will be deleted. Mail and events on Gmail, Outlook, and IMAP " "servers will not be deleted." msgstr "" -#: ui/dialogs/settings-dialog.slint:3242 +#: ui/dialogs/settings-dialog.slint:3243 msgid "" "POP3 mail is stored only on this device and will be deleted. Mail and events " "on Gmail, Outlook, and IMAP servers will not be deleted." msgstr "" -#: ui/dialogs/settings-dialog.slint:3243 +#: ui/dialogs/settings-dialog.slint:3244 msgid "Mail and events on Gmail, Outlook, and IMAP servers will not be deleted." msgstr "" -#: ui/dialogs/settings-dialog.slint:3244 +#: ui/dialogs/settings-dialog.slint:3245 msgid "Export POP3 mail…" msgstr "" -#: ui/dialogs/settings-dialog.slint:3249 +#: ui/dialogs/settings-dialog.slint:3250 msgid "Keep my data" msgstr "" -#: ui/dialogs/settings-dialog.slint:3263 +#: ui/dialogs/settings-dialog.slint:3264 msgid "Delete mail on this device?" msgstr "" -#: ui/dialogs/settings-dialog.slint:3265 +#: ui/dialogs/settings-dialog.slint:3266 msgid "Delete the mail you imported and its folders from this device." msgstr "" -#: ui/dialogs/settings-dialog.slint:3266 +#: ui/dialogs/settings-dialog.slint:3267 msgid "It exists only here. Export it first to keep a copy." msgstr "" -#: ui/dialogs/settings-dialog.slint:3271 +#: ui/dialogs/settings-dialog.slint:3272 msgid "Delete mail" msgstr "" -#: ui/dialogs/settings-dialog.slint:3272 +#: ui/dialogs/settings-dialog.slint:3273 msgid "Keep mail" msgstr "" -#: ui/dialogs/settings-dialog.slint:3286 +#: ui/dialogs/settings-dialog.slint:3287 msgid "Remove account?" msgstr "" -#: ui/dialogs/settings-dialog.slint:3288 +#: ui/dialogs/settings-dialog.slint:3289 msgid "Remove this account and its downloaded mail from this device." msgstr "" -#: ui/dialogs/settings-dialog.slint:3290 +#: ui/dialogs/settings-dialog.slint:3291 msgid "" "This account's mail is stored only on this device. Export it first to keep a " "copy." msgstr "" -#: ui/dialogs/settings-dialog.slint:3291 +#: ui/dialogs/settings-dialog.slint:3292 msgid "Messages on the mail server will not be deleted." msgstr "" -#: ui/dialogs/settings-dialog.slint:3296 +#: ui/dialogs/settings-dialog.slint:3297 msgid "Remove account" msgstr "" -#: ui/dialogs/settings-dialog.slint:3297 +#: ui/dialogs/settings-dialog.slint:3298 msgid "Keep account" msgstr "" -#: ui/dialogs/settings-dialog.slint:3311 +#: ui/dialogs/settings-dialog.slint:3312 msgid "Delete profile?" msgstr "" -#: ui/dialogs/settings-dialog.slint:3313 +#: ui/dialogs/settings-dialog.slint:3314 msgid "" "Accounts assigned to this profile will return to their individual or " "generated account colors." msgstr "" -#: ui/dialogs/settings-dialog.slint:3314 +#: ui/dialogs/settings-dialog.slint:3315 msgid "Accounts and provider data are not changed." msgstr "" -#: ui/dialogs/settings-dialog.slint:3316 +#: ui/dialogs/settings-dialog.slint:3317 msgid "Keep profile" msgstr "" -#: ui/dialogs/settings-dialog.slint:3330 +#: ui/dialogs/settings-dialog.slint:3331 msgid "Remove calendar?" msgstr "" -#: ui/dialogs/settings-dialog.slint:3332 +#: ui/dialogs/settings-dialog.slint:3333 msgid "Stop syncing it and remove its events from this device." msgstr "" -#: ui/dialogs/settings-dialog.slint:3333 +#: ui/dialogs/settings-dialog.slint:3334 msgid "Events on the server will not be deleted." msgstr "" -#: ui/dialogs/settings-dialog.slint:3335 +#: ui/dialogs/settings-dialog.slint:3336 msgid "Keep calendar" msgstr "" @@ -6606,7 +6661,7 @@ msgstr "" msgid "Reviewed as completed" msgstr "" -#: ui/views/mail-list-pane.slint:92 ui/views/mail-list-pane.slint:619 +#: ui/views/mail-list-pane.slint:92 ui/views/mail-list-pane.slint:624 msgid "Copied verification code {}" msgstr "" @@ -6634,160 +6689,166 @@ msgid_plural "{n} messages" msgstr[0] "" msgstr[1] "" -#: ui/views/mail-list-pane.slint:626 +#: ui/views/mail-list-pane.slint:631 msgid "This week" msgstr "" -#: ui/views/mail-list-pane.slint:627 +#: ui/views/mail-list-pane.slint:632 msgid "Last week" msgstr "" -#: ui/views/mail-list-pane.slint:628 +#: ui/views/mail-list-pane.slint:633 msgid "This month" msgstr "" -#: ui/views/mail-list-pane.slint:629 +#: ui/views/mail-list-pane.slint:634 msgid "Last month" msgstr "" -#: ui/views/mail-list-pane.slint:630 +#: ui/views/mail-list-pane.slint:635 msgid "Earlier" msgstr "" -#: ui/views/mail-list-pane.slint:662 +#: ui/views/mail-list-pane.slint:667 msgid "Show folders" msgstr "" -#: ui/views/mail-list-pane.slint:662 +#: ui/views/mail-list-pane.slint:667 msgid "Hide folders" msgstr "" -#: ui/views/mail-list-pane.slint:668 +#: ui/views/mail-list-pane.slint:673 msgid "Unread" msgstr "" -#: ui/views/mail-list-pane.slint:669 +#: ui/views/mail-list-pane.slint:674 msgid "Starred" msgstr "" -#: ui/views/mail-list-pane.slint:670 ui/views/mail-list-pane.slint:924 +#: ui/views/mail-list-pane.slint:675 ui/views/mail-list-pane.slint:936 msgid "Has attachments" msgstr "" -#: ui/views/mail-list-pane.slint:673 +#: ui/views/mail-list-pane.slint:678 msgid "Filter mail" msgstr "" -#: ui/views/mail-list-pane.slint:676 +#: ui/views/mail-list-pane.slint:681 msgid "Synchronizing mail" msgstr "" -#: ui/views/mail-list-pane.slint:716 +#: ui/views/mail-list-pane.slint:721 msgid "Select all messages on this page" msgstr "" -#: ui/views/mail-list-pane.slint:738 +#: ui/views/mail-list-pane.slint:743 msgid "Archive selected message" msgstr "" -#: ui/views/mail-list-pane.slint:756 +#: ui/views/mail-list-pane.slint:761 msgid "Delete selected permanently" msgstr "" -#: ui/views/mail-list-pane.slint:769 ui/views/mail-reading-pane.slint:1030 +#: ui/views/mail-list-pane.slint:774 ui/views/mail-reading-pane.slint:1042 msgid "More message actions" msgstr "" -#: ui/views/mail-list-pane.slint:776 +#: ui/views/mail-list-pane.slint:781 msgid "Open in new tabs" msgstr "" -#: ui/views/mail-list-pane.slint:776 ui/views/mail-list-pane.slint:1372 +#: ui/views/mail-list-pane.slint:781 ui/views/mail-list-pane.slint:1384 msgid "Open in new tab" msgstr "" -#: ui/views/mail-list-pane.slint:781 ui/views/mail-list-pane.slint:1379 -#: ui/views/mail-list-pane.slint:781 ui/views/mail-list-pane.slint:1379 ui/views/mail-reading-pane.slint:368 -#: ui/views/mail-list-pane.slint:781 ui/views/mail-list-pane.slint:1379 ui/views/mail-reading-pane.slint:368 ui/views/mail-reading-pane.slint:403 +#: ui/views/mail-list-pane.slint:786 ui/views/mail-list-pane.slint:1391 +#: ui/views/mail-list-pane.slint:786 ui/views/mail-list-pane.slint:1391 ui/views/mail-reading-pane.slint:369 +#: ui/views/mail-list-pane.slint:786 ui/views/mail-list-pane.slint:1391 ui/views/mail-reading-pane.slint:369 ui/views/mail-reading-pane.slint:406 msgid "Move to…" msgstr "" -#: ui/views/mail-list-pane.slint:787 ui/views/mail-list-pane.slint:1375 -#: ui/views/mail-list-pane.slint:787 ui/views/mail-list-pane.slint:1375 ui/views/mail-reading-pane.slint:366 -#: ui/views/mail-list-pane.slint:787 ui/views/mail-list-pane.slint:1375 ui/views/mail-reading-pane.slint:366 ui/views/mail-reading-pane.slint:402 -#: ui/views/mail-list-pane.slint:787 ui/views/mail-list-pane.slint:1375 ui/views/mail-reading-pane.slint:366 ui/views/mail-reading-pane.slint:402 +#: ui/views/mail-list-pane.slint:793 ui/views/mail-list-pane.slint:1398 +#: ui/views/mail-list-pane.slint:793 ui/views/mail-list-pane.slint:1398 ui/views/mail-reading-pane.slint:370 +#: ui/views/mail-list-pane.slint:793 ui/views/mail-list-pane.slint:1398 ui/views/mail-reading-pane.slint:370 ui/views/mail-reading-pane.slint:407 +msgid "Copy to…" +msgstr "" + +#: ui/views/mail-list-pane.slint:799 ui/views/mail-list-pane.slint:1387 +#: ui/views/mail-list-pane.slint:799 ui/views/mail-list-pane.slint:1387 ui/views/mail-reading-pane.slint:367 +#: ui/views/mail-list-pane.slint:799 ui/views/mail-list-pane.slint:1387 ui/views/mail-reading-pane.slint:367 ui/views/mail-reading-pane.slint:405 +#: ui/views/mail-list-pane.slint:799 ui/views/mail-list-pane.slint:1387 ui/views/mail-reading-pane.slint:367 ui/views/mail-reading-pane.slint:405 msgid "Mark as read" msgstr "" -#: ui/views/mail-list-pane.slint:788 ui/views/mail-list-pane.slint:1375 -#: ui/views/mail-list-pane.slint:788 ui/views/mail-list-pane.slint:1375 ui/views/mail-reading-pane.slint:366 -#: ui/views/mail-list-pane.slint:788 ui/views/mail-list-pane.slint:1375 ui/views/mail-reading-pane.slint:366 ui/views/mail-reading-pane.slint:402 -#: ui/views/mail-list-pane.slint:788 ui/views/mail-list-pane.slint:1375 ui/views/mail-reading-pane.slint:366 ui/views/mail-reading-pane.slint:402 +#: ui/views/mail-list-pane.slint:800 ui/views/mail-list-pane.slint:1387 +#: ui/views/mail-list-pane.slint:800 ui/views/mail-list-pane.slint:1387 ui/views/mail-reading-pane.slint:367 +#: ui/views/mail-list-pane.slint:800 ui/views/mail-list-pane.slint:1387 ui/views/mail-reading-pane.slint:367 ui/views/mail-reading-pane.slint:405 +#: ui/views/mail-list-pane.slint:800 ui/views/mail-list-pane.slint:1387 ui/views/mail-reading-pane.slint:367 ui/views/mail-reading-pane.slint:405 msgid "Mark as unread" msgstr "" -#: ui/views/mail-list-pane.slint:789 ui/views/mail-list-pane.slint:1374 -#: ui/views/mail-list-pane.slint:789 ui/views/mail-list-pane.slint:1374 ui/views/mail-reading-pane.slint:358 -#: ui/views/mail-list-pane.slint:789 ui/views/mail-list-pane.slint:1374 ui/views/mail-reading-pane.slint:358 ui/views/mail-reading-pane.slint:394 -#: ui/views/mail-list-pane.slint:789 ui/views/mail-list-pane.slint:1374 ui/views/mail-reading-pane.slint:358 ui/views/mail-reading-pane.slint:394 +#: ui/views/mail-list-pane.slint:801 ui/views/mail-list-pane.slint:1386 +#: ui/views/mail-list-pane.slint:801 ui/views/mail-list-pane.slint:1386 ui/views/mail-reading-pane.slint:359 +#: ui/views/mail-list-pane.slint:801 ui/views/mail-list-pane.slint:1386 ui/views/mail-reading-pane.slint:359 ui/views/mail-reading-pane.slint:397 +#: ui/views/mail-list-pane.slint:801 ui/views/mail-list-pane.slint:1386 ui/views/mail-reading-pane.slint:359 ui/views/mail-reading-pane.slint:397 msgid "Star message" msgstr "" -#: ui/views/mail-list-pane.slint:790 ui/views/mail-list-pane.slint:1374 -#: ui/views/mail-list-pane.slint:790 ui/views/mail-list-pane.slint:1374 ui/views/mail-reading-pane.slint:358 -#: ui/views/mail-list-pane.slint:790 ui/views/mail-list-pane.slint:1374 ui/views/mail-reading-pane.slint:358 ui/views/mail-reading-pane.slint:394 -#: ui/views/mail-list-pane.slint:790 ui/views/mail-list-pane.slint:1374 ui/views/mail-reading-pane.slint:358 ui/views/mail-reading-pane.slint:394 +#: ui/views/mail-list-pane.slint:802 ui/views/mail-list-pane.slint:1386 +#: ui/views/mail-list-pane.slint:802 ui/views/mail-list-pane.slint:1386 ui/views/mail-reading-pane.slint:359 +#: ui/views/mail-list-pane.slint:802 ui/views/mail-list-pane.slint:1386 ui/views/mail-reading-pane.slint:359 ui/views/mail-reading-pane.slint:397 +#: ui/views/mail-list-pane.slint:802 ui/views/mail-list-pane.slint:1386 ui/views/mail-reading-pane.slint:359 ui/views/mail-reading-pane.slint:397 msgid "Remove star" msgstr "" -#: ui/views/mail-list-pane.slint:917 ui/views/mail-list-pane.slint:1176 -#: ui/views/mail-list-pane.slint:917 ui/views/mail-list-pane.slint:1176 +#: ui/views/mail-list-pane.slint:929 ui/views/mail-list-pane.slint:1188 +#: ui/views/mail-list-pane.slint:929 ui/views/mail-list-pane.slint:1188 msgid "(No subject)" msgstr "" -#: ui/views/mail-list-pane.slint:919 +#: ui/views/mail-list-pane.slint:931 msgid "Profile: {}" msgstr "" -#: ui/views/mail-list-pane.slint:920 +#: ui/views/mail-list-pane.slint:932 msgid "{} messages" msgstr "" -#: ui/views/mail-list-pane.slint:923 +#: ui/views/mail-list-pane.slint:935 msgid "Verification code {}" msgstr "" -#: ui/views/mail-list-pane.slint:925 +#: ui/views/mail-list-pane.slint:937 msgid "Replied" msgstr "" -#: ui/views/mail-list-pane.slint:1119 +#: ui/views/mail-list-pane.slint:1131 msgid "{} messages in this conversation" msgstr "" -#: ui/views/mail-list-pane.slint:1204 +#: ui/views/mail-list-pane.slint:1216 msgid "Account: {}" msgstr "" -#: ui/views/mail-list-pane.slint:1364 +#: ui/views/mail-list-pane.slint:1376 msgid "Deselect {}" msgstr "" -#: ui/views/mail-list-pane.slint:1386 ui/views/mail-reading-pane.slint:333 -#: ui/views/mail-list-pane.slint:1386 ui/views/mail-reading-pane.slint:333 ui/views/mail-reading-pane.slint:370 -#: ui/views/mail-list-pane.slint:1386 ui/views/mail-reading-pane.slint:333 ui/views/mail-reading-pane.slint:370 ui/views/mail-reading-pane.slint:404 +#: ui/views/mail-list-pane.slint:1405 ui/views/mail-reading-pane.slint:333 +#: ui/views/mail-list-pane.slint:1405 ui/views/mail-reading-pane.slint:333 ui/views/mail-reading-pane.slint:372 +#: ui/views/mail-list-pane.slint:1405 ui/views/mail-reading-pane.slint:333 ui/views/mail-reading-pane.slint:372 ui/views/mail-reading-pane.slint:408 msgid "Delete permanently…" msgstr "" -#: ui/views/mail-list-pane.slint:1443 ui/views/onboarding-view.slint:139 +#: ui/views/mail-list-pane.slint:1462 ui/views/onboarding-view.slint:139 msgid "Opening your mailbox…" msgstr "" -#: ui/views/mail-list-pane.slint:1443 +#: ui/views/mail-list-pane.slint:1462 msgid "Nothing here" msgstr "" -#: ui/views/mail-list-pane.slint:1444 +#: ui/views/mail-list-pane.slint:1463 msgid "Try another folder or search term." msgstr "" @@ -6823,229 +6884,229 @@ msgstr "" msgid "to {}" msgstr "" -#: ui/views/mail-reading-pane.slint:328 ui/views/mail-reading-pane.slint:1896 +#: ui/views/mail-reading-pane.slint:328 ui/views/mail-reading-pane.slint:1908 msgid "Continue editing" msgstr "" #: ui/views/mail-reading-pane.slint:330 ui/views/mail-reading-pane.slint:331 #: ui/views/mail-reading-pane.slint:330 ui/views/mail-reading-pane.slint:331 ui/views/mail-reading-pane.slint:346 -#: ui/views/mail-reading-pane.slint:330 ui/views/mail-reading-pane.slint:331 ui/views/mail-reading-pane.slint:346 ui/views/mail-reading-pane.slint:364 -#: ui/views/mail-reading-pane.slint:330 ui/views/mail-reading-pane.slint:331 ui/views/mail-reading-pane.slint:346 ui/views/mail-reading-pane.slint:364 ui/views/mail-reading-pane.slint:384 -#: ui/views/mail-reading-pane.slint:330 ui/views/mail-reading-pane.slint:331 ui/views/mail-reading-pane.slint:346 ui/views/mail-reading-pane.slint:364 ui/views/mail-reading-pane.slint:384 ui/views/mail-reading-pane.slint:400 +#: ui/views/mail-reading-pane.slint:330 ui/views/mail-reading-pane.slint:331 ui/views/mail-reading-pane.slint:346 ui/views/mail-reading-pane.slint:365 +#: ui/views/mail-reading-pane.slint:330 ui/views/mail-reading-pane.slint:331 ui/views/mail-reading-pane.slint:346 ui/views/mail-reading-pane.slint:365 ui/views/mail-reading-pane.slint:386 +#: ui/views/mail-reading-pane.slint:330 ui/views/mail-reading-pane.slint:331 ui/views/mail-reading-pane.slint:346 ui/views/mail-reading-pane.slint:365 ui/views/mail-reading-pane.slint:386 ui/views/mail-reading-pane.slint:403 msgid "Show rendered message" msgstr "" #: ui/views/mail-reading-pane.slint:330 ui/views/mail-reading-pane.slint:345 -#: ui/views/mail-reading-pane.slint:330 ui/views/mail-reading-pane.slint:345 ui/views/mail-reading-pane.slint:383 -#: ui/views/mail-reading-pane.slint:330 ui/views/mail-reading-pane.slint:345 ui/views/mail-reading-pane.slint:383 +#: ui/views/mail-reading-pane.slint:330 ui/views/mail-reading-pane.slint:345 ui/views/mail-reading-pane.slint:385 +#: ui/views/mail-reading-pane.slint:330 ui/views/mail-reading-pane.slint:345 ui/views/mail-reading-pane.slint:385 msgid "Show source" msgstr "" #: ui/views/mail-reading-pane.slint:331 ui/views/mail-reading-pane.slint:346 -#: ui/views/mail-reading-pane.slint:331 ui/views/mail-reading-pane.slint:346 ui/views/mail-reading-pane.slint:384 -#: ui/views/mail-reading-pane.slint:331 ui/views/mail-reading-pane.slint:346 ui/views/mail-reading-pane.slint:384 +#: ui/views/mail-reading-pane.slint:331 ui/views/mail-reading-pane.slint:346 ui/views/mail-reading-pane.slint:386 +#: ui/views/mail-reading-pane.slint:331 ui/views/mail-reading-pane.slint:346 ui/views/mail-reading-pane.slint:386 msgid "View plain text" msgstr "" #: ui/views/mail-reading-pane.slint:332 ui/views/mail-reading-pane.slint:347 -#: ui/views/mail-reading-pane.slint:332 ui/views/mail-reading-pane.slint:347 ui/views/mail-reading-pane.slint:385 -#: ui/views/mail-reading-pane.slint:332 ui/views/mail-reading-pane.slint:347 ui/views/mail-reading-pane.slint:385 ui/views/mail-reading-pane.slint:1277 +#: ui/views/mail-reading-pane.slint:332 ui/views/mail-reading-pane.slint:347 ui/views/mail-reading-pane.slint:387 +#: ui/views/mail-reading-pane.slint:332 ui/views/mail-reading-pane.slint:347 ui/views/mail-reading-pane.slint:387 ui/views/mail-reading-pane.slint:1289 msgid "Use message appearance" msgstr "" #: ui/views/mail-reading-pane.slint:332 ui/views/mail-reading-pane.slint:347 -#: ui/views/mail-reading-pane.slint:332 ui/views/mail-reading-pane.slint:347 ui/views/mail-reading-pane.slint:385 -#: ui/views/mail-reading-pane.slint:332 ui/views/mail-reading-pane.slint:347 ui/views/mail-reading-pane.slint:385 ui/views/mail-reading-pane.slint:1277 -#: ui/views/mail-reading-pane.slint:332 ui/views/mail-reading-pane.slint:347 ui/views/mail-reading-pane.slint:385 ui/views/mail-reading-pane.slint:1277 +#: ui/views/mail-reading-pane.slint:332 ui/views/mail-reading-pane.slint:347 ui/views/mail-reading-pane.slint:387 +#: ui/views/mail-reading-pane.slint:332 ui/views/mail-reading-pane.slint:347 ui/views/mail-reading-pane.slint:387 ui/views/mail-reading-pane.slint:1289 +#: ui/views/mail-reading-pane.slint:332 ui/views/mail-reading-pane.slint:347 ui/views/mail-reading-pane.slint:387 ui/views/mail-reading-pane.slint:1289 msgid "Show original colors" msgstr "" -#: ui/views/mail-reading-pane.slint:340 ui/views/mail-reading-pane.slint:359 -#: ui/views/mail-reading-pane.slint:340 ui/views/mail-reading-pane.slint:359 ui/views/mail-reading-pane.slint:395 -#: ui/views/mail-reading-pane.slint:340 ui/views/mail-reading-pane.slint:359 ui/views/mail-reading-pane.slint:395 +#: ui/views/mail-reading-pane.slint:340 ui/views/mail-reading-pane.slint:360 +#: ui/views/mail-reading-pane.slint:340 ui/views/mail-reading-pane.slint:360 ui/views/mail-reading-pane.slint:398 +#: ui/views/mail-reading-pane.slint:340 ui/views/mail-reading-pane.slint:360 ui/views/mail-reading-pane.slint:398 msgid "Manage labels" msgstr "" -#: ui/views/mail-reading-pane.slint:341 ui/views/mail-reading-pane.slint:360 -#: ui/views/mail-reading-pane.slint:341 ui/views/mail-reading-pane.slint:360 ui/views/mail-reading-pane.slint:396 +#: ui/views/mail-reading-pane.slint:341 ui/views/mail-reading-pane.slint:361 +#: ui/views/mail-reading-pane.slint:341 ui/views/mail-reading-pane.slint:361 ui/views/mail-reading-pane.slint:399 msgid "Decrypt / verify OpenPGP" msgstr "" -#: ui/views/mail-reading-pane.slint:342 ui/views/mail-reading-pane.slint:380 +#: ui/views/mail-reading-pane.slint:342 ui/views/mail-reading-pane.slint:382 msgid "Print message" msgstr "" -#: ui/views/mail-reading-pane.slint:343 ui/views/mail-reading-pane.slint:361 -#: ui/views/mail-reading-pane.slint:343 ui/views/mail-reading-pane.slint:361 ui/views/mail-reading-pane.slint:397 -#: ui/views/mail-reading-pane.slint:343 ui/views/mail-reading-pane.slint:361 ui/views/mail-reading-pane.slint:397 +#: ui/views/mail-reading-pane.slint:343 ui/views/mail-reading-pane.slint:362 +#: ui/views/mail-reading-pane.slint:343 ui/views/mail-reading-pane.slint:362 ui/views/mail-reading-pane.slint:400 +#: ui/views/mail-reading-pane.slint:343 ui/views/mail-reading-pane.slint:362 ui/views/mail-reading-pane.slint:400 msgid "Open in browser" msgstr "" -#: ui/views/mail-reading-pane.slint:718 +#: ui/views/mail-reading-pane.slint:730 msgid "Close conversation" msgstr "" -#: ui/views/mail-reading-pane.slint:728 +#: ui/views/mail-reading-pane.slint:740 msgid "Previous conversation" msgstr "" -#: ui/views/mail-reading-pane.slint:738 +#: ui/views/mail-reading-pane.slint:750 msgid "Next conversation" msgstr "" -#: ui/views/mail-reading-pane.slint:779 +#: ui/views/mail-reading-pane.slint:791 msgid "Archive message" msgstr "" -#: ui/views/mail-reading-pane.slint:882 +#: ui/views/mail-reading-pane.slint:894 msgid "Search labels" msgstr "" -#: ui/views/mail-reading-pane.slint:899 +#: ui/views/mail-reading-pane.slint:911 msgid "Available labels" msgstr "" -#: ui/views/mail-reading-pane.slint:938 +#: ui/views/mail-reading-pane.slint:950 msgid "Smart category" msgstr "" -#: ui/views/mail-reading-pane.slint:979 ui/views/mail-reading-pane.slint:1993 +#: ui/views/mail-reading-pane.slint:991 ui/views/mail-reading-pane.slint:2005 msgid "Edit label" msgstr "" -#: ui/views/mail-reading-pane.slint:996 +#: ui/views/mail-reading-pane.slint:1008 msgid "No matching labels" msgstr "" -#: ui/views/mail-reading-pane.slint:1008 +#: ui/views/mail-reading-pane.slint:1020 msgid "Create label…" msgstr "" -#: ui/views/mail-reading-pane.slint:1278 +#: ui/views/mail-reading-pane.slint:1290 msgid "Show original layout" msgstr "" -#: ui/views/mail-reading-pane.slint:1278 +#: ui/views/mail-reading-pane.slint:1290 msgid "Reader mode" msgstr "" -#: ui/views/mail-reading-pane.slint:1285 +#: ui/views/mail-reading-pane.slint:1297 msgid "Previous match" msgstr "" -#: ui/views/mail-reading-pane.slint:1286 +#: ui/views/mail-reading-pane.slint:1298 msgid "Next match" msgstr "" -#: ui/views/mail-reading-pane.slint:1287 +#: ui/views/mail-reading-pane.slint:1299 msgid "Close find" msgstr "" -#: ui/views/mail-reading-pane.slint:1291 +#: ui/views/mail-reading-pane.slint:1303 msgid "Some images could not be loaded." msgstr "" -#: ui/views/mail-reading-pane.slint:1292 ui/views/mail-reading-pane.slint:1603 +#: ui/views/mail-reading-pane.slint:1304 ui/views/mail-reading-pane.slint:1615 msgid "Retry images" msgstr "" -#: ui/views/mail-reading-pane.slint:1297 +#: ui/views/mail-reading-pane.slint:1309 msgid "Reload message" msgstr "" -#: ui/views/mail-reading-pane.slint:1359 +#: ui/views/mail-reading-pane.slint:1371 msgid "Remote images were blocked to protect your privacy." msgstr "" -#: ui/views/mail-reading-pane.slint:1368 +#: ui/views/mail-reading-pane.slint:1380 msgid "Load images" msgstr "" -#: ui/views/mail-reading-pane.slint:1406 +#: ui/views/mail-reading-pane.slint:1418 msgid "Loading image…" msgstr "" -#: ui/views/mail-reading-pane.slint:1406 +#: ui/views/mail-reading-pane.slint:1418 msgid "Blocked image" msgstr "" -#: ui/views/mail-reading-pane.slint:1406 +#: ui/views/mail-reading-pane.slint:1418 msgid "Image exceeds limits" msgstr "" -#: ui/views/mail-reading-pane.slint:1406 +#: ui/views/mail-reading-pane.slint:1418 msgid "Image unavailable" msgstr "" -#: ui/views/mail-reading-pane.slint:1552 +#: ui/views/mail-reading-pane.slint:1564 msgid "Show rendered email" msgstr "" -#: ui/views/mail-reading-pane.slint:1575 +#: ui/views/mail-reading-pane.slint:1587 msgid "Copy this HTML into your GitHub rendering issue." msgstr "" -#: ui/views/mail-reading-pane.slint:1592 +#: ui/views/mail-reading-pane.slint:1604 msgid "Show email" msgstr "" -#: ui/views/mail-reading-pane.slint:1602 +#: ui/views/mail-reading-pane.slint:1614 msgid "View original plain text" msgstr "" -#: ui/views/mail-reading-pane.slint:1604 +#: ui/views/mail-reading-pane.slint:1616 msgid "Copy formatted selection" msgstr "" -#: ui/views/mail-reading-pane.slint:1608 +#: ui/views/mail-reading-pane.slint:1620 msgid "Copy selected text" msgstr "" -#: ui/views/mail-reading-pane.slint:1618 +#: ui/views/mail-reading-pane.slint:1630 msgid "Copy all email text" msgstr "" -#: ui/views/mail-reading-pane.slint:1628 +#: ui/views/mail-reading-pane.slint:1640 msgid "Select all email text" msgstr "" -#: ui/views/mail-reading-pane.slint:1646 +#: ui/views/mail-reading-pane.slint:1658 msgid "Link: {}" msgstr "" -#: ui/views/mail-reading-pane.slint:1835 +#: ui/views/mail-reading-pane.slint:1847 msgid "Heading: {}" msgstr "" -#: ui/views/mail-reading-pane.slint:1957 +#: ui/views/mail-reading-pane.slint:1969 msgid "No message selected" msgstr "" -#: ui/views/mail-reading-pane.slint:1958 +#: ui/views/mail-reading-pane.slint:1970 msgid "Choose an email to read it here." msgstr "" -#: ui/views/mail-reading-pane.slint:2000 +#: ui/views/mail-reading-pane.slint:2012 msgid "Label name" msgstr "" -#: ui/views/mail-reading-pane.slint:2020 +#: ui/views/mail-reading-pane.slint:2032 msgid "Pink" msgstr "" -#: ui/views/mail-reading-pane.slint:2021 +#: ui/views/mail-reading-pane.slint:2033 msgid "Red" msgstr "" -#: ui/views/mail-reading-pane.slint:2022 +#: ui/views/mail-reading-pane.slint:2034 msgid "Amber" msgstr "" -#: ui/views/mail-reading-pane.slint:2025 +#: ui/views/mail-reading-pane.slint:2037 msgid "Slate" msgstr "" -#: ui/views/mail-reading-pane.slint:2030 +#: ui/views/mail-reading-pane.slint:2042 msgid "Use across all accounts (local only)" msgstr "" diff --git a/src/mail.rs b/src/mail.rs index a8320edd..e948ff5f 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -1784,6 +1784,38 @@ impl CoreMailSource { .map_err(|error| error.to_string()) } + /// Copies or moves mail to a folder of another account. The original of + /// a move goes once the copy is in place. + pub async fn transfer_to_folder( + &self, + target: MailTarget, + folder_id: i64, + is_move: bool, + ) -> Result<(), String> { + self.core + .transfer_mail(target.action_target(), folder_id, is_move) + .await + .map(|_| ()) + .map_err(|error| error.to_string()) + } + + /// Files mail in one more folder of its JMAP account. + pub async fn copy_to_folder(&self, target: MailTarget, folder_id: i64) -> Result<(), String> { + self.core + .perform_action(PerformActionArgs { + kind: ActionKind::Copy, + target: target.action_target(), + params: Some(ActionParams { + wake_at: None, + target_folder_id: Some(folder_id), + label_id: None, + }), + }) + .await + .map(|_| ()) + .map_err(|error| error.to_string()) + } + /// Inbox tabs route conversations, so a message's routes its conversation. pub async fn route_to_tab(&self, target: MailTarget, tab: String) -> Result<(), String> { self.core diff --git a/src/mail_drop_tests.rs b/src/mail_drop_tests.rs index 03432488..c5aac7ce 100644 --- a/src/mail_drop_tests.rs +++ b/src/mail_drop_tests.rs @@ -139,4 +139,26 @@ fn drops_resolve_by_scope_not_by_mailbox_name() { Ok(MailDropDestination::Action("trash")) ); assert!(drop(Scope::Folder(12), 1, 12).is_err()); + + // Another account's folder gets a copy, and the original goes once it + // is there; its Sent and Starred take no mail. + let other = |scope, folder_id, label| MailboxEntry { + account_id: 2, + ..mailbox(scope, folder_id, label) + }; + let other_sent = Scope::Account { + account_id: 2, + mailbox: StandardMailbox::Sent, + }; + state.mailboxes.push(other(Scope::Folder(30), 30, "Travel")); + state.mailboxes.push(other(other_sent, 31, "Sent")); + let drop = |scope, account_id, folder_id| { + resolve_single_mail_drop(&state, &item, scope, account_id, folder_id) + .map(|(_, destination)| destination) + }; + assert_eq!( + drop(Scope::Folder(30), 2, 30), + Ok(MailDropDestination::Transfer(30)) + ); + assert!(drop(other_sent, 2, 31).is_err()); } diff --git a/src/mail_work.rs b/src/mail_work.rs index a168e17f..451ba65e 100644 --- a/src/mail_work.rs +++ b/src/mail_work.rs @@ -44,8 +44,27 @@ pub(super) enum Operation { EmptyTrash(Option), Drop(MailDropDestination), Label(i64, bool), + /// Files mail in one more folder: another of its own account's, where + /// a message can be in several (JMAP), or a copy in another account's. + Copy { + folder_id: i64, + other_account: bool, + }, } impl Operation { + /// Whether it starts moving (`true`) or copying mail to another + /// account, which goes on after it returns. + fn starts_transfer(&self) -> Option { + match self { + Self::Drop(MailDropDestination::Transfer(_)) => Some(true), + Self::Copy { + other_account: true, + .. + } => Some(false), + _ => None, + } + } + fn queues_permanent_deletion(&self) -> bool { match self { Self::EmptyTrash(_) => true, @@ -75,6 +94,17 @@ impl Operation { core.perform_message_action(target, a).await } Self::Drop(MailDropDestination::Folder(id)) => core.move_to_folder(target, *id).await, + Self::Drop(MailDropDestination::Transfer(id)) => { + core.transfer_to_folder(target, *id, true).await + } + Self::Copy { + folder_id, + other_account: true, + } => core.transfer_to_folder(target, *folder_id, false).await, + Self::Copy { + folder_id, + other_account: false, + } => core.copy_to_folder(target, *folder_id).await, Self::Drop(MailDropDestination::Label(id)) => { core.perform_label_action(target, *id, true).await } @@ -96,6 +126,9 @@ struct ActionResult { moved: Vec, error: Option, deletion_queued: bool, + /// Mail started on its way to another account, moving (`true`) or + /// being copied. + transfer_started: Option, } pub(super) fn register( @@ -279,6 +312,13 @@ pub(super) fn register( Some(error) => { app.set_render_status(UiMessage::detail("Message action failed: {}", error)) } + // What became of it is told once the other account has it. + None if result.transfer_started == Some(true) => { + app.set_render_status(UiMessage::plain("Moving mail to the other account…")) + } + None if result.transfer_started == Some(false) => { + app.set_render_status(UiMessage::plain("Copying mail to the other account…")) + } None if result.deletion_queued => app.set_render_status(UiMessage::plain( "Permanent deletion queued. Mail will disappear after the server confirms.", )), @@ -447,14 +487,17 @@ where moved: vec![], error: None, deletion_queued: false, + transfer_started: None, }; for (id, target, operation) in operations { let deletion = operation.queues_permanent_deletion(); + let transfer = operation.starts_transfer(); result.ids.push(id); let moves = operation.moves(); match perform(target, operation).await { Ok(()) => { result.deletion_queued |= deletion; + result.transfer_started = result.transfer_started.or(transfer); result.completed.push(id); if moves { result.moved.push(id); diff --git a/src/main.rs b/src/main.rs index e07a1df4..486cb5ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -706,6 +706,8 @@ struct MailDragItem { enum MailDropDestination { Action(&'static str), Folder(i64), + /// A folder of another account. + Transfer(i64), Label(i64), Route(String), } @@ -832,15 +834,12 @@ fn resolve_single_mail_drop( return Err("this unified mailbox is not a drop destination".to_owned()); } Scope::Folder(_) | Scope::Account { .. } => { - if i64::from(target_account_id) != message.account_id { - return Err("messages cannot be moved between accounts".to_owned()); - } let target = state .mailboxes .iter() .find(|mailbox| { !mailbox.is_account - && mailbox.account_id == message.account_id + && mailbox.account_id == i64::from(target_account_id) && mailbox.scope == Some(target_scope) && mailbox.folder_id == i64::from(target_folder_id) }) @@ -848,17 +847,32 @@ fn resolve_single_mail_drop( if !target.is_selectable { return Err("this mailbox is a hierarchy container".to_owned()); } - // Folders are told apart by scope: another folder can have the - // name of the one shown. - if state.scope.shows_mail_in(target_scope) { - return Err("message is already in this destination".to_owned()); - } // Custom folders are plain moves even when named like a // standard mailbox. let standard = match target_scope { Scope::Account { mailbox, .. } => Some(mailbox), _ => None, }; + // Mail moves to another account as Thunderbird moves it: a copy + // goes there, and the original goes once the copy is in place. + if target.account_id != message.account_id { + return match standard { + Some( + StandardMailbox::Sent | StandardMailbox::Drafts | StandardMailbox::Starred, + ) => Err("this mailbox is not a drop destination".to_owned()), + _ if target.folder_id >= 0 => Ok(( + MailTarget::of(message) + .ok_or_else(|| "message thread is unavailable".to_owned())?, + MailDropDestination::Transfer(target.folder_id), + )), + _ => Err("mailbox destination is unavailable".to_owned()), + }; + } + // Folders are told apart by scope: another folder can have the + // name of the one shown. + if state.scope.shows_mail_in(target_scope) { + return Err("message is already in this destination".to_owned()); + } match standard { Some(StandardMailbox::Starred) if !message.starred => { MailDropDestination::Action("star") @@ -3869,6 +3883,11 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> app.set_render_status(message.clone()); app.set_sync_status(message); } + if let Some(report) = pending.transfers + && let Some(app) = mail_update_app.upgrade() + { + app.set_render_status(move_picker::transfer_message(&report)); + } if pending.calendar_changed && let Some(app) = mail_update_app.upgrade() && app.get_settings_open() diff --git a/src/move_picker.rs b/src/move_picker.rs index 04ea9a23..9a505928 100644 --- a/src/move_picker.rs +++ b/src/move_picker.rs @@ -1,16 +1,30 @@ -//! Move to: moving mail into a folder without dragging it (WCAG 2.5.7). +//! Move to and Copy to: moving or copying mail into a folder without +//! dragging it (WCAG 2.5.7). //! //! The picker lists the folders the mail could be dropped on in the sidebar, -//! and a choice is carried out as that drop, so both always agree. +//! and a move is carried out as that drop, so both always agree. A folder of +//! another account gets a copy there, and a move removes the original once +//! the copy is in place. A copy within an account is offered where a message +//! can be in several folders: JMAP. use super::*; +use flectar_mail_core::events::TransferReport; /// Folders listed under Recent, at most. const RECENT_TARGETS: usize = 5; +/// What the picker does with the mail. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub(crate) enum PickerMode { + #[default] + Move, + Copy, +} + /// A folder the picker offers: the sidebar row the mail would be dropped on. #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct MoveTarget { pub(crate) scope: Scope, + /// -1 for the unified mailboxes. pub(crate) account_id: i64, pub(crate) folder_id: i64, pub(crate) kind: &'static str, @@ -18,12 +32,16 @@ pub(crate) struct MoveTarget { /// The folders it is in, outermost first. pub(crate) path: String, pub(crate) depth: usize, + /// The name of its account; empty for the unified mailboxes. + pub(crate) account: String, } -/// One row of the picker: a target by its index, or a heading. +/// One row of the picker. #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) enum PickerRow { Heading(&'static str), + /// The account of the targets that follow, named by the first of them. + Account(usize), Target { index: usize, /// Whether the row shows where the folder is, rather than its @@ -32,75 +50,119 @@ pub(crate) enum PickerRow { }, } -/// Where mail of these accounts can go, in sidebar order: one account's -/// mailboxes and folders, or for several accounts the unified mailboxes. -/// These are the sidebar's drop targets that move mail, less the one the -/// list shows it in. +/// Where mail of `mail_accounts` can go, in sidebar order: for mail of +/// several accounts the unified mailboxes, then the mail's own account and +/// every other one. A copy within an account is offered only where +/// `copies_within` says a message can be in several folders. These are the +/// sidebar's drop targets, less the folder the list shows the mail in. pub(crate) fn move_targets( mailboxes: &[MailboxEntry], - account_ids: &[i64], + mail_accounts: &[i64], viewed: Scope, + mode: PickerMode, + copies_within: &dyn Fn(i64) -> bool, ) -> Vec { - let kind = |mailbox| match mailbox { + let mut targets = Vec::new(); + if mode == PickerMode::Move && mail_accounts.len() > 1 { + targets.extend( + StandardMailbox::ALL + .into_iter() + .filter_map(|mailbox| Some((Scope::Unified(mailbox), standard_kind(mailbox)?))) + .filter(|(scope, _)| !viewed.shows_mail_in(*scope)) + .map(|(scope, kind)| MoveTarget { + scope, + account_id: -1, + folder_id: -1, + kind, + label: String::new(), + path: String::new(), + depth: 0, + account: String::new(), + }), + ); + } + let mut accounts = Vec::new(); + for mailbox in mailboxes { + if mailbox.account_id >= 0 && !accounts.contains(&mailbox.account_id) { + accounts.push(mailbox.account_id); + } + } + // The mail's own account first. + if let [own] = mail_accounts + && let Some(position) = accounts.iter().position(|account| account == own) + { + accounts[..=position].rotate_right(1); + } + for account_id in accounts { + let own = mail_accounts.contains(&account_id); + if mode == PickerMode::Copy && own && !copies_within(account_id) { + continue; + } + targets.extend(account_targets( + mailboxes, + account_id, + own.then_some(viewed), + )); + } + targets +} + +/// What a standard mailbox the picker offers shows as. +fn standard_kind(mailbox: StandardMailbox) -> Option<&'static str> { + match mailbox { StandardMailbox::Inbox => Some("inbox"), StandardMailbox::Archive => Some("archive"), StandardMailbox::Spam => Some("spam"), StandardMailbox::Trash => Some("trash"), StandardMailbox::Starred | StandardMailbox::Sent | StandardMailbox::Drafts => None, - }; - if let [account_id] = account_ids { - let labels = mailboxes - .iter() - .filter(|mailbox| mailbox.account_id == *account_id && mailbox.folder_id >= 0) - .map(|mailbox| { - ( - mailbox.folder_id, - (mailbox.parent_folder_id, mailbox.label.as_str()), - ) - }) - .collect::>(); - return mailboxes - .iter() - .filter(|mailbox| { - mailbox.account_id == *account_id - && !mailbox.is_account - && mailbox.is_selectable - && mailbox.folder_id >= 0 - }) - .filter_map(|mailbox| { - let scope = mailbox.scope?; - let kind = match scope { - Scope::Account { mailbox, .. } => kind(mailbox)?, - Scope::Folder(_) => "folder", - _ => return None, - }; - if viewed.shows_mail_in(scope) { - return None; - } - Some(MoveTarget { - scope, - account_id: *account_id, - folder_id: mailbox.folder_id, - kind, - label: mailbox.label.clone(), - path: folder_path(&labels, mailbox.parent_folder_id), - depth: mailbox.depth, - }) - }) - .collect(); } - StandardMailbox::ALL - .into_iter() - .filter_map(|mailbox| Some((Scope::Unified(mailbox), kind(mailbox)?))) - .filter(|(scope, _)| !viewed.shows_mail_in(*scope)) - .map(|(scope, kind)| MoveTarget { - scope, - account_id: -1, - folder_id: -1, - kind, - label: String::new(), - path: String::new(), - depth: 0, +} + +/// One account's folders that can take mail, but the one the list shows +/// the mail in when it is the mail's own account (`viewed`). +fn account_targets( + mailboxes: &[MailboxEntry], + account_id: i64, + viewed: Option, +) -> Vec { + let labels = mailboxes + .iter() + .filter(|mailbox| mailbox.account_id == account_id && mailbox.folder_id >= 0) + .map(|mailbox| { + ( + mailbox.folder_id, + (mailbox.parent_folder_id, mailbox.label.as_str()), + ) + }) + .collect::>(); + mailboxes + .iter() + .filter(|mailbox| { + mailbox.account_id == account_id + && !mailbox.is_account + && mailbox.is_selectable + && mailbox.folder_id >= 0 + }) + .filter_map(|mailbox| { + let scope = mailbox.scope?; + let kind = match scope { + Scope::Account { mailbox, .. } => standard_kind(mailbox)?, + Scope::Folder(_) => "folder", + _ => return None, + }; + if viewed.is_some_and(|viewed| viewed.shows_mail_in(scope)) { + return None; + } + Some(MoveTarget { + scope, + account_id, + folder_id: mailbox.folder_id, + kind, + label: mailbox.label.clone(), + path: folder_path(&labels, mailbox.parent_folder_id), + depth: mailbox.depth, + account: mailbox.context.clone(), + }) }) .collect() } @@ -120,10 +182,21 @@ fn folder_path(labels: &HashMap, mut parent: i64) -> String { names.join(" / ") } +/// Whether rows name the targets' accounts: unless they are all of the one +/// account the mail is in. +pub(crate) fn names_accounts(targets: &[MoveTarget], mail_accounts: &[i64]) -> bool { + !matches!(mail_accounts, [own] if targets.iter().all(|target| target.account_id == *own)) +} + /// The picker's rows: with no search, the recent folders and then every -/// folder as a tree; with one, the folders whose name or location contains -/// it. -pub(crate) fn picker_rows(targets: &[MoveTarget], recent: &[i64], query: &str) -> Vec { +/// folder as a tree, under its account's name where rows are `named`; with +/// one, the folders whose name, location or account contains it. +pub(crate) fn picker_rows( + targets: &[MoveTarget], + recent: &[i64], + query: &str, + named: bool, +) -> Vec { let query = query.trim().to_lowercase(); if !query.is_empty() { return targets @@ -132,6 +205,7 @@ pub(crate) fn picker_rows(targets: &[MoveTarget], recent: &[i64], query: &str) - .filter(|(_, target)| { target.label.to_lowercase().contains(&query) || target.path.to_lowercase().contains(&query) + || (named && target.account.to_lowercase().contains(&query)) }) .map(|(index, _)| PickerRow::Target { index, @@ -148,23 +222,39 @@ pub(crate) fn picker_rows(targets: &[MoveTarget], recent: &[i64], query: &str) - }) .take(RECENT_TARGETS) .collect::>(); - let mut rows = Vec::with_capacity(targets.len() + recent.len() + 2); + let mut rows = Vec::with_capacity(targets.len() + recent.len() + 8); if !recent.is_empty() { rows.push(PickerRow::Heading("recent")); rows.extend(recent.into_iter().map(|index| PickerRow::Target { index, located: true, })); - rows.push(PickerRow::Heading("folders")); + if !named { + rows.push(PickerRow::Heading("folders")); + } + } + for (index, target) in targets.iter().enumerate() { + if named + && targets + .get(index.wrapping_sub(1)) + .is_none_or(|previous| previous.account_id != target.account_id) + { + rows.push(PickerRow::Account(index)); + } + rows.push(PickerRow::Target { + index, + located: false, + }); } - rows.extend((0..targets.len()).map(|index| PickerRow::Target { - index, - located: false, - })); rows } -fn slint_rows(targets: &[MoveTarget], rows: Vec) -> Vec { +fn slint_rows( + targets: &[MoveTarget], + rows: Vec, + local: Option, + named: bool, +) -> Vec { rows.into_iter() .filter_map(|row| match row { PickerRow::Heading(kind) => Some(MoveTargetRow { @@ -172,17 +262,46 @@ fn slint_rows(targets: &[MoveTarget], rows: Vec) -> Vec { + let target = targets.get(index)?; + Some(MoveTargetRow { + key: -1, + kind: if target.account_id < 0 { + "unified" + } else if Some(target.account_id) == local { + "device" + } else { + "account" + } + .into(), + label: target.account.clone().into(), + label_has_emoji: contains_emoji(&target.account), + ..Default::default() + }) + } PickerRow::Target { index, located } => { let target = targets.get(index)?; + let detail = if !located { + String::new() + } else if named && target.account_id >= 0 { + let account = if Some(target.account_id) == local { + String::new() + } else { + target.account.clone() + }; + [target.path.as_str(), account.as_str()] + .into_iter() + .filter(|part| !part.is_empty()) + .collect::>() + .join(" · ") + } else { + target.path.clone() + }; Some(MoveTargetRow { key: i32::try_from(index).ok()?, kind: target.kind.into(), label: target.label.clone().into(), - detail: if located { - target.path.clone().into() - } else { - Default::default() - }, + detail: detail.into(), depth: if located { 0 } else { @@ -195,16 +314,42 @@ fn slint_rows(targets: &[MoveTarget], rows: Vec) -> Vec UiMessage { + if report.failed > 0 { + return UiMessage::arguments( + "Could not move or copy {} messages: {}", + report.failed, + report.error.as_deref().unwrap_or_default(), + ); + } + match (report.moved, report.copied) { + (0, 0) if report.present > 0 => { + UiMessage::detail("The other account had {} messages already.", report.present) + } + (moved, 0) => UiMessage::detail("Moved {} messages to the other account.", moved), + (0, copied) => UiMessage::detail("Copied {} messages to the other account.", copied), + (moved, copied) => UiMessage::detail( + "Moved or copied {} messages to the other account.", + moved + copied, + ), + } +} + +/// The open picker: what it does, the mail it acts on and where to. #[derive(Default)] struct MovePicker { + mode: PickerMode, ids: Vec, targets: Vec, recent: Vec, + local: Option, + named: bool, } -/// The mail Move to acts on: a list row and the rows checked along with it, -/// the checked rows, or the open conversation. +/// The mail the picker acts on: a list row and the rows checked along with +/// it, the checked rows, or the open conversation. fn picker_ids(state: &InboxState, source: &str, id: i32) -> Vec { match source { "row" => mail_operation_ids(&state.messages, &state.checked_ids, id), @@ -231,11 +376,16 @@ pub(super) fn register( let request_state = state.clone(); let request_runtime = runtime.clone(); let request_picker = picker.clone(); - ui.on_request(move |source, id| { + ui.on_request(move |mode, source, id| { let Some(app) = weak.upgrade() else { return; }; - let (ids, targets, core, account_ids) = { + let mode = if mode == "copy" { + PickerMode::Copy + } else { + PickerMode::Move + }; + let (ids, targets, core, local, named) = { let state = request_state.borrow(); if !state.using_core { return; @@ -248,9 +398,35 @@ pub(super) fn register( .collect::>(); account_ids.sort_unstable(); account_ids.dedup(); - let targets = move_targets(&state.mailboxes, &account_ids, state.scope); - (ids, targets, state.core.clone(), account_ids) + if account_ids.is_empty() { + return; + } + let jmap = |account_id: i64| { + state.connected_accounts.iter().any(|account| { + account.id == account_id + && account.mail_protocol == flectar_mail_core::models::MailProtocol::Jmap + }) + }; + let targets = move_targets(&state.mailboxes, &account_ids, state.scope, mode, &jmap); + let named = names_accounts(&targets, &account_ids); + ( + ids, + targets, + state.core.clone(), + state.local_mail.map(|local| local.account_id), + named, + ) }; + let Some(core) = core else { + return; + }; + if targets.is_empty() { + app.set_render_status(match mode { + PickerMode::Move => UiMessage::plain("No other folder to move this mail to."), + PickerMode::Copy => UiMessage::plain("No folder to copy this mail to."), + }); + return; + } // Search matches the names shown, and standard mailboxes show // translated ones. let ui = app.global::(); @@ -263,29 +439,27 @@ pub(super) fn register( target }) .collect::>(); - let Some(core) = core else { - return; - }; - if ids.is_empty() || account_ids.is_empty() { - return; - } - if targets.is_empty() { - app.set_render_status(UiMessage::plain("No other folder to move this mail to.")); - return; - } - // Recent folders are those of the mail's one account. - let recent = match account_ids.as_slice() { - [_] => request_runtime - .block_on(core.recent_move_targets(4 * RECENT_TARGETS)) - .unwrap_or_default(), - _ => Vec::new(), - }; - let rows = slint_rows(&targets, picker_rows(&targets, &recent, "")); + let recent = request_runtime + .block_on(core.recent_move_targets(4 * RECENT_TARGETS)) + .unwrap_or_default(); + let rows = slint_rows( + &targets, + picker_rows(&targets, &recent, "", named), + local, + named, + ); *request_picker.borrow_mut() = MovePicker { + mode, ids, targets, recent, + local, + named, }; + ui.set_mode(match mode { + PickerMode::Move => "move".into(), + PickerMode::Copy => "copy".into(), + }); ui.set_query(Default::default()); ui.set_rows(ModelRc::new(VecModel::from(rows))); ui.set_open(true); @@ -300,7 +474,9 @@ pub(super) fn register( let picker = search_picker.borrow(); let rows = slint_rows( &picker.targets, - picker_rows(&picker.targets, &picker.recent, &query), + picker_rows(&picker.targets, &picker.recent, &query, picker.named), + picker.local, + picker.named, ); app.global::() .set_rows(ModelRc::new(VecModel::from(rows))); @@ -313,46 +489,104 @@ pub(super) fn register( let Some(app) = weak.upgrade() else { return; }; - let MovePicker { ids, targets, .. } = std::mem::take(&mut *picker.borrow_mut()); + let MovePicker { + mode, ids, targets, .. + } = std::mem::take(&mut *picker.borrow_mut()); let Some(target) = usize::try_from(key) .ok() .and_then(|index| targets.into_iter().nth(index)) else { return; }; - let items = { - let state = choose_state.borrow(); - ids.iter() - .filter_map(|id| state.messages.iter().find(|message| message.id == *id)) - .filter_map(|message| { - Some(MailDragItem { - message_id: message.id, - account_id: i32::try_from(message.account_id).ok()?, - }) - }) - .collect::>() + let result = match mode { + PickerMode::Move => { + let items = { + let state = choose_state.borrow(); + ids.iter() + .filter_map(|id| state.messages.iter().find(|message| message.id == *id)) + .filter_map(|message| { + Some(MailDragItem { + message_id: message.id, + account_id: i32::try_from(message.account_id).ok()?, + }) + }) + .collect::>() + }; + perform_mail_drop( + &app, + &choose_state, + &choose_runtime, + &MailDragPayload { items }, + target.scope, + i32::try_from(target.account_id).unwrap_or(-1), + i32::try_from(target.folder_id).unwrap_or(-1), + ) + } + PickerMode::Copy => copy_mail(&choose_state, &ids, &target), }; - if let Err(error) = perform_mail_drop( - &app, - &choose_state, - &choose_runtime, - &MailDragPayload { items }, - target.scope, - i32::try_from(target.account_id).unwrap_or(-1), - i32::try_from(target.folder_id).unwrap_or(-1), - ) { - app.set_render_status(UiMessage::detail("Could not move mail: {}", error)); + if let Err(error) = result { + app.set_render_status(match mode { + PickerMode::Move => UiMessage::detail("Could not move mail: {}", error), + PickerMode::Copy => UiMessage::detail("Could not copy mail: {}", error), + }); } }); } +/// Copies the mail to the target: to another folder of a JMAP account, or +/// another account. +fn copy_mail( + state: &Rc>, + ids: &[i32], + target: &MoveTarget, +) -> Result<(), String> { + let (core, operations) = { + let state = state.borrow(); + let operations = ids + .iter() + .map(|id| { + let message = state + .messages + .iter() + .find(|message| message.id == *id) + .ok_or_else(|| "message is no longer available".to_owned())?; + let mail = MailTarget::of(message) + .ok_or_else(|| "message thread is unavailable".to_owned())?; + Ok(( + *id, + mail, + mail_work::Operation::Copy { + folder_id: target.folder_id, + other_account: message.account_id != target.account_id, + }, + )) + }) + .collect::, String>>()?; + ( + state + .core + .clone() + .ok_or_else(|| "mail core is unavailable".to_owned())?, + operations, + ) + }; + mail_work::enqueue(state, core, operations) +} + #[cfg(test)] mod tests { use super::*; - fn entry(scope: Scope, folder_id: i64, parent: i64, depth: usize, label: &str) -> MailboxEntry { + fn entry( + account_id: i64, + scope: Scope, + folder_id: i64, + parent: i64, + depth: usize, + label: &str, + ) -> MailboxEntry { MailboxEntry { - account_id: 1, + account_id, folder_id, parent_folder_id: parent, depth, @@ -364,7 +598,7 @@ mod tests { can_delete: false, label: label.into(), scope: Some(scope), - context: String::new(), + context: if account_id == 1 { "Work" } else { "Home" }.into(), detail: String::new(), avatar: String::new(), is_account: false, @@ -372,84 +606,159 @@ mod tests { } } - fn account(mailbox: StandardMailbox) -> Scope { + fn account(account_id: i64, mailbox: StandardMailbox) -> Scope { Scope::Account { - account_id: 1, + account_id, mailbox, } } - /// An account's sidebar: standard mailboxes, then a folder tree with a - /// hierarchy container and two folders of the same name. + /// Two accounts' sidebar: Work's standard mailboxes and a folder tree + /// with a hierarchy container and two folders of the same name, and + /// Home's Inbox and one folder. fn sidebar() -> Vec { - let mut container = entry(Scope::Folder(20), 20, -1, 0, "Projects"); + let mut container = entry(1, Scope::Folder(20), 20, -1, 0, "Projects"); container.is_selectable = false; - let mut heading = entry(account(StandardMailbox::Inbox), -1, -1, 0, "me@example.com"); + let mut heading = entry(1, account(1, StandardMailbox::Inbox), -1, -1, 0, "Work"); heading.is_account = true; heading.scope = None; - let mut other_account = entry(Scope::Folder(40), 40, -1, 0, "Elsewhere"); - other_account.account_id = 2; vec![ heading, - entry(account(StandardMailbox::Inbox), 10, -1, 0, "Inbox"), - entry(account(StandardMailbox::Starred), -1, -1, 0, "Starred"), - entry(account(StandardMailbox::Sent), 11, -1, 0, "Sent"), - entry(account(StandardMailbox::Archive), 12, -1, 0, "Archive"), - entry(account(StandardMailbox::Spam), 13, -1, 0, "Spam"), - entry(account(StandardMailbox::Trash), 14, -1, 0, "Trash"), - entry(account(StandardMailbox::Drafts), 15, -1, 0, "Drafts"), + entry(1, account(1, StandardMailbox::Inbox), 10, -1, 0, "Inbox"), + entry( + 1, + account(1, StandardMailbox::Starred), + -1, + -1, + 0, + "Starred", + ), + entry(1, account(1, StandardMailbox::Sent), 11, -1, 0, "Sent"), + entry( + 1, + account(1, StandardMailbox::Archive), + 12, + -1, + 0, + "Archive", + ), + entry(1, account(1, StandardMailbox::Spam), 13, -1, 0, "Spam"), + entry(1, account(1, StandardMailbox::Trash), 14, -1, 0, "Trash"), + entry(1, account(1, StandardMailbox::Drafts), 15, -1, 0, "Drafts"), container, - entry(Scope::Folder(21), 21, 20, 1, "Work"), - entry(Scope::Folder(22), 22, 21, 2, "2024"), - entry(Scope::Folder(23), 23, 20, 1, "2024"), - other_account, + entry(1, Scope::Folder(21), 21, 20, 1, "Work"), + entry(1, Scope::Folder(22), 22, 21, 2, "2024"), + entry(1, Scope::Folder(23), 23, 20, 1, "2024"), + entry(2, account(2, StandardMailbox::Inbox), 40, -1, 0, "Inbox"), + entry(2, Scope::Folder(41), 41, -1, 0, "Travel"), ] } + /// Where each target is, as "Account: path / label". fn labels(targets: &[MoveTarget]) -> Vec { targets .iter() - .map(|target| match target.path.as_str() { - "" => target.label.clone(), - path => format!("{path} / {}", target.label), + .map(|target| { + let name = match target.path.as_str() { + "" => target.label.clone(), + path => format!("{path} / {}", target.label), + }; + match target.account.as_str() { + "" => name, + account => format!("{account}: {name}"), + } }) .collect() } + fn not_jmap(_: i64) -> bool { + false + } + #[test] - fn mail_moves_to_the_folders_it_can_be_dropped_on() { - let targets = move_targets(&sidebar(), &[1], account(StandardMailbox::Inbox)); + fn mail_moves_to_its_accounts_folders_and_then_to_the_others() { + let targets = move_targets( + &sidebar(), + &[1], + account(1, StandardMailbox::Inbox), + PickerMode::Move, + ¬_jmap, + ); assert_eq!( labels(&targets), [ - "Archive", - "Spam", - "Trash", - "Projects / Work", - "Projects / Work / 2024", - "Projects / 2024", + "Work: Archive", + "Work: Spam", + "Work: Trash", + "Work: Projects / Work", + "Work: Projects / Work / 2024", + "Work: Projects / 2024", + "Home: Inbox", + "Home: Travel", ], - "not the list's own mailbox, Starred, Sent, Drafts, a container or another account's folder" - ); - assert_eq!( - targets.iter().map(|target| target.kind).collect::>(), - ["archive", "spam", "trash", "folder", "folder", "folder"] + "not the list's own mailbox, Starred, Sent, Drafts or a container" ); assert_eq!(targets[4].scope, Scope::Folder(22)); assert_eq!(targets[4].depth, 2); // A folder named like the one shown is still another folder. - let from_folder = move_targets(&sidebar(), &[1], Scope::Folder(23)); + let from_folder = move_targets( + &sidebar(), + &[1], + Scope::Folder(23), + PickerMode::Move, + ¬_jmap, + ); assert!(from_folder.iter().any(|target| target.folder_id == 22)); assert!(!from_folder.iter().any(|target| target.folder_id == 23)); - assert!(from_folder.iter().any(|target| target.kind == "inbox")); + + // Home's mail lists Home first, and Work's Inbox: it isn't in it. + let home = move_targets( + &sidebar(), + &[2], + Scope::Unified(StandardMailbox::Inbox), + PickerMode::Move, + ¬_jmap, + ); + assert_eq!(labels(&home)[0], "Home: Travel"); + assert!(home.iter().any(|target| target.folder_id == 10)); + assert!(!home.iter().any(|target| target.folder_id == 40)); } #[test] - fn mail_of_several_accounts_moves_to_unified_mailboxes() { - let targets = move_targets(&sidebar(), &[1, 2], Scope::Unified(StandardMailbox::Inbox)); + fn copies_go_to_other_accounts_or_within_one_that_keeps_mail_in_several() { + let copies = move_targets( + &sidebar(), + &[1], + account(1, StandardMailbox::Inbox), + PickerMode::Copy, + ¬_jmap, + ); + assert_eq!(labels(&copies), ["Home: Inbox", "Home: Travel"]); + // The rows say whose folders they are. + assert!(names_accounts(&copies, &[1])); + let jmap = |account_id: i64| account_id == 1; + let within = move_targets( + &sidebar(), + &[1], + account(1, StandardMailbox::Inbox), + PickerMode::Copy, + &jmap, + ); + assert_eq!(within.len(), 8); + } + + #[test] + fn mail_of_several_accounts_moves_to_unified_mailboxes_too() { + let targets = move_targets( + &sidebar(), + &[1, 2], + Scope::Unified(StandardMailbox::Inbox), + PickerMode::Move, + ¬_jmap, + ); assert_eq!( - targets + targets[..3] .iter() .map(|target| target.scope) .collect::>(), @@ -459,70 +768,123 @@ mod tests { Scope::Unified(StandardMailbox::Trash), ] ); - // One account's mail in a unified list is in that account's mailbox. - let one = move_targets(&sidebar(), &[1], Scope::Unified(StandardMailbox::Archive)); - assert!(one.iter().any(|target| target.kind == "inbox")); - assert!(!one.iter().any(|target| target.kind == "archive")); - let important = move_targets(&sidebar(), &[1], Scope::Important); - assert!(!important.iter().any(|target| target.kind == "inbox")); + // Each account's mail is in its own Inbox. + assert!(!targets.iter().any(|target| target.kind == "inbox")); + let important = move_targets( + &sidebar(), + &[1], + Scope::Important, + PickerMode::Move, + ¬_jmap, + ); + assert!(!important.iter().any(|target| target.folder_id == 10)); } #[test] - fn recent_folders_come_first_and_search_finds_names_and_locations() { - let targets = move_targets(&sidebar(), &[1], account(StandardMailbox::Inbox)); + fn recent_folders_come_first_and_search_finds_names_locations_and_accounts() { + let targets = move_targets( + &sidebar(), + &[1], + account(1, StandardMailbox::Inbox), + PickerMode::Move, + ¬_jmap, + ); + let named = names_accounts(&targets, &[1]); + assert!(named, "Home's folders are listed too"); let shown = |rows: Vec| { - rows.into_iter() - .map(|row| match row { - PickerRow::Heading(kind) => format!("[{kind}]"), - PickerRow::Target { index, located } => { - let target = &targets[index]; - if located && !target.path.is_empty() { - format!("{} ({})", target.label, target.path) - } else { - format!("{}{}", " ".repeat(target.depth), target.label) - } - } + slint_rows(&targets, rows, None, named) + .into_iter() + .map(|row| match (row.key, row.detail.as_str()) { + (-1, _) => format!("[{} {}]", row.kind, row.label).replace(" ]", "]"), + (_, "") => format!("{}{}", " ".repeat(row.depth as usize), row.label), + (_, detail) => format!("{} ({detail})", row.label), }) .collect::>() }; assert_eq!( - shown(picker_rows(&targets, &[], "")), - ["Archive", "Spam", "Trash", " Work", " 2024", " 2024"] - ); - // Recent folders that are no destination now are left out: the - // inbox shown, a folder deleted since. - assert_eq!( - shown(picker_rows(&targets, &[23, 10, 99, 12], "")), + shown(picker_rows(&targets, &[], "", named)), [ - "[recent]", - "2024 (Projects)", - "Archive", - "[folders]", + "[account Work]", "Archive", "Spam", "Trash", " Work", " 2024", " 2024", + "[account Home]", + "Inbox", + "Travel", + ] + ); + // Recent folders that are no destination now are left out: the + // inbox shown, a folder deleted since. + assert_eq!( + shown(picker_rows(&targets, &[41, 23, 10, 99], "", named))[..4], + [ + "[recent]", + "Travel (Home)", + "2024 (Projects · Work)", + "[account Work]", ] ); let many = [12, 13, 14, 21, 22, 23]; assert_eq!( - picker_rows(&targets, &many, "") + picker_rows(&targets, &many, "", named) .iter() - .take_while(|row| **row != PickerRow::Heading("folders")) + .take_while(|row| !matches!(row, PickerRow::Account(_))) .count(), 1 + RECENT_TARGETS ); assert_eq!( - shown(picker_rows(&targets, &[23], " 2024 ")), - ["2024 (Projects / Work)", "2024 (Projects)"] + shown(picker_rows(&targets, &[], " 2024 ", named)), + ["2024 (Projects / Work · Work)", "2024 (Projects · Work)"] ); assert_eq!( - shown(picker_rows(&targets, &[], "work")), - ["Work (Projects)", "2024 (Projects / Work)"] + shown(picker_rows(&targets, &[], "home", named)), + ["Inbox (Home)", "Travel (Home)"] + ); + assert!(picker_rows(&targets, &[], "nothing like it", named).is_empty()); + + // One account's folders need no names, but a heading for the rest. + let one = move_targets( + &sidebar()[..12], + &[1], + account(1, StandardMailbox::Inbox), + PickerMode::Move, + ¬_jmap, + ); + assert!(!names_accounts(&one, &[1])); + let rows = slint_rows(&one, picker_rows(&one, &[23], "", false), None, false); + assert_eq!(rows[0].kind, "recent"); + assert_eq!(rows[1].detail, "Projects"); + assert_eq!(rows[2].kind, "folders"); + } + + #[test] + fn a_finished_transfer_says_what_became_of_it() { + let report = |moved, copied, present, failed| TransferReport { + moved, + copied, + present, + failed, + error: (failed > 0).then(|| "refused".to_owned()), + }; + assert_eq!( + transfer_message(&report(2, 0, 1, 0)).english(), + "Moved 2 messages to the other account." + ); + assert_eq!( + transfer_message(&report(0, 1, 0, 0)).english(), + "Copied 1 messages to the other account." + ); + assert_eq!( + transfer_message(&report(0, 0, 3, 0)).english(), + "The other account had 3 messages already." + ); + assert_eq!( + transfer_message(&report(1, 1, 0, 2)).english(), + "Could not move or copy 2 messages: refused" ); - assert!(picker_rows(&targets, &[], "nothing like it").is_empty()); } } diff --git a/src/startup.rs b/src/startup.rs index aacaa451..13c907be 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -18,7 +18,7 @@ use crate::{ }; use flectar_mail_core::{ config::Paths, - events::CoreEvent, + events::{CoreEvent, TransferReport}, models::{ Account, AccountConfig, CalendarConnection, CardDavConnection, MailGrouping, MessageCursor, Settings, ThreadCursor, normalized_workspace_list_pane_width, @@ -584,6 +584,8 @@ pub(crate) struct PendingCoreUpdates { pub(crate) all_mail_changed: bool, pub(crate) account_states: HashMap)>, pub(crate) action_error: Option, + /// What became of the latest mail copied or moved to other accounts. + pub(crate) transfers: Option, pub(crate) calendar_changed: bool, pub(crate) contacts_changed: bool, } @@ -738,6 +740,13 @@ pub(crate) fn spawn_core_event_listener( .action_error = Some(error); true } + Ok(CoreEvent::TransfersFinished(report)) => { + pending + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .transfers = Some(report); + true + } Ok(CoreEvent::CalendarUpdated { .. }) | Ok(CoreEvent::CalendarEventsAdded { .. }) => { pending diff --git a/ui/app.slint b/ui/app.slint index c83a8489..42a70860 100644 --- a/ui/app.slint +++ b/ui/app.slint @@ -496,6 +496,15 @@ export global I18n { if (key == "Label deleted.") { return @tr("Label deleted."); } if (key == "No other folder to move this mail to.") { return @tr("No other folder to move this mail to."); } if (key == "Could not move mail: {}") { return @tr("Could not move mail: {}", argument); } + if (key == "No folder to copy this mail to.") { return @tr("No folder to copy this mail to."); } + if (key == "Could not copy mail: {}") { return @tr("Could not copy mail: {}", argument); } + if (key == "Moving mail to the other account…") { return @tr("Moving mail to the other account…"); } + if (key == "Copying mail to the other account…") { return @tr("Copying mail to the other account…"); } + if (key == "Moved {} messages to the other account.") { return @tr("Moved one message to the other account." | "Moved {0} messages to the other account." % round(argument.to-float()), argument); } + if (key == "Copied {} messages to the other account.") { return @tr("Copied one message to the other account." | "Copied {0} messages to the other account." % round(argument.to-float()), argument); } + if (key == "Moved or copied {} messages to the other account.") { return @tr("Moved or copied one message to the other account." | "Moved or copied {0} messages to the other account." % round(argument.to-float()), argument); } + if (key == "The other account had {} messages already.") { return @tr("The other account had that message already." | "The other account had those {0} messages already." % round(argument.to-float()), argument); } + if (key == "Could not move or copy {} messages: {}") { return @tr("Could not move or copy one message: {1}" | "Could not move or copy {0} messages: {1}" % round(argument.to-float()), argument, second-argument); } if (key == "Could not update label: {}") { return @tr("Could not update label: {}", argument); } if (key == "Could not save label: {}") { return @tr("Could not save label: {}", argument); } if (key == "Could not delete label: {}") { return @tr("Could not delete label: {}", argument); } diff --git a/ui/dialogs/move-dialog.slint b/ui/dialogs/move-dialog.slint index edcde1cc..f961990c 100644 --- a/ui/dialogs/move-dialog.slint +++ b/ui/dialogs/move-dialog.slint @@ -7,7 +7,8 @@ import { LineEdit, ToolbarIconButton } from "../components/controls.slint"; export struct MoveTargetRow { // What `MovePickerUi.choose` takes; -1 on headings. key: int, - // inbox, archive, spam, trash or folder; on headings, recent or folders. + // inbox, archive, spam, trash or folder; on headings, recent, folders, + // or the account of the rows below: account, device or unified. kind: string, // The name shown, which for standard mailboxes is translated; empty on // headings, which their kind names. @@ -18,18 +19,22 @@ export struct MoveTargetRow { label_has_emoji: bool, } -// The non-drag way to move mail into a folder (WCAG 2.5.7). Rust lists the -// destinations when the picker opens and as the search changes; choosing -// one does what dropping the mail on that sidebar row does. +// The non-drag way to move mail into a folder (WCAG 2.5.7), and to copy it. +// Rust lists the destinations when the picker opens and as the search +// changes; choosing one for a move does what dropping the mail on that +// sidebar row does. export global MovePickerUi { in-out property open: false; + // move or copy. + in property mode: "move"; in property <[MoveTargetRow]> rows: []; in-out property query: ""; // Where focus goes back to once the picker closes: list or reader. in-out property return-to: "list"; - // Opens the picker for "row" (a list row, with the rows checked along - // with it), "checked" (the checked rows) or "open" (the open mail). - callback request(/* source */ string, /* email id */ int); + // Opens the picker to move or copy mail of "row" (a list row, with the + // rows checked along with it), "checked" (the checked rows) or "open" + // (the open mail). + callback request(/* mode */ string, /* source */ string, /* email id */ int); callback search(string); callback choose(/* key */ int); @@ -42,6 +47,8 @@ export global MovePickerUi { if kind == "trash" { return @tr("Trash"); } if kind == "recent" { return @tr("Recent"); } if kind == "folders" { return @tr("All folders"); } + if kind == "device" { return @tr("On this device"); } + if kind == "unified" { return @tr("All accounts"); } label } } @@ -64,7 +71,7 @@ export component MoveDialog inherits Rectangle { background: #00000066; forward-focus: search-field; accessible-role: groupbox; - accessible-label: @tr("Move to"); + accessible-label: MovePickerUi.mode == "copy" ? @tr("Copy to") : @tr("Move to"); // A heading is always followed by a destination, so one step past it // reaches one. @@ -188,7 +195,7 @@ export component MoveDialog inherits Rectangle { spacing: 8px; Text { - text: @tr("Move to"); + text: MovePickerUi.mode == "copy" ? @tr("Copy to") : @tr("Move to"); color: AppTheme.title; font-size: TypeScale.heading; font-weight: 700; @@ -243,10 +250,11 @@ export component MoveDialog inherits Rectangle { x: 10px; width: parent.width - 20px; height: parent.height - 4px; - text: MovePickerUi.display-name(row.kind, ""); + text: MovePickerUi.display-name(row.kind, row.label); color: AppTheme.muted; font-size: TypeScale.caption; font-weight: 600; + font-family: row.label_has_emoji ? "Noto Emoji" : ""; vertical-alignment: bottom; overflow: elide; accessible-role: none; diff --git a/ui/dialogs/settings-dialog.slint b/ui/dialogs/settings-dialog.slint index 80fb4d45..9366b499 100644 --- a/ui/dialogs/settings-dialog.slint +++ b/ui/dialogs/settings-dialog.slint @@ -2396,6 +2396,7 @@ export component SettingsDialog inherits Rectangle { ShortcutRow { title: @tr("Search mail"); keys: [Shortcuts.search, Shortcuts.search-alternate]; title-color: root.body-text; separator-color: root.faint-text; } ShortcutRow { title: @tr("Archive"); keys: [Shortcuts.archive]; title-color: root.body-text; separator-color: root.faint-text; } ShortcutRow { title: @tr("Move to a folder"); keys: [Shortcuts.move-to]; title-color: root.body-text; separator-color: root.faint-text; } + ShortcutRow { title: @tr("Copy to a folder"); keys: [Shortcuts.copy-to]; title-color: root.body-text; separator-color: root.faint-text; } ShortcutRow { title: @tr("Move to spam"); keys: [Shortcuts.spam]; title-color: root.body-text; separator-color: root.faint-text; } ShortcutRow { title: @tr("Move to trash"); keys: [Shortcuts.trash]; title-color: root.body-text; separator-color: root.faint-text; } } diff --git a/ui/shortcuts.slint b/ui/shortcuts.slint index 90e46d39..bbb6e86c 100644 --- a/ui/shortcuts.slint +++ b/ui/shortcuts.slint @@ -11,8 +11,10 @@ export global Shortcuts { out property spam: @keys(J); // Mac keyboards label Backspace "delete", and Apple Mail trashes with it. out property trash: KeyboardKeys.apple ? @keys(Backspace) : @keys(Delete); - // Outlook's and Evolution's key for moving mail to a folder. + // Outlook's and Evolution's keys for moving and copying mail to a + // folder. out property move-to: @keys(Control + Shift + V); + out property copy-to: @keys(Control + Shift + Y); // Thunderbird's keys: F5 checks the accounts in view, Shift+F5 every // account. Apple Mail checks every account with Shift-Command-N. diff --git a/ui/views/mail-list-pane.slint b/ui/views/mail-list-pane.slint index ed8e30ed..256f35f0 100644 --- a/ui/views/mail-list-pane.slint +++ b/ui/views/mail-list-pane.slint @@ -456,14 +456,19 @@ export component MailListPane inherits FocusScope { keys: Shortcuts.move-to; // Not while typing: text fields elsewhere paste plain text with it. enabled: !search-toolbar.search-focused; - activated => { root.move-messages(); } + activated => { root.pick-folder("move"); } + } + KeyBinding { + keys: Shortcuts.copy-to; + enabled: !search-toolbar.search-focused; + activated => { root.pick-folder("copy"); } } - // Move to opens for what the other shortcuts act on. - function move-messages() { + // Move to and Copy to open for what the other shortcuts act on. + function pick-folder(mode: string) { if root.selection-mode { MovePickerUi.return-to = "list"; - MovePickerUi.request("checked", -1); + MovePickerUi.request(mode, "checked", -1); return; } let target = MailListNavigation.target(root.list_entries, root.keyboard-email-id, "current", 1); @@ -471,7 +476,7 @@ export component MailListPane inherits FocusScope { let at-cursor = list-keyboard.has-focus && target.id == root.keyboard-email-id; if !at-cursor && !root.list_entries[target.entry_index].email.selected { return; } MovePickerUi.return-to = "list"; - MovePickerUi.request("row", target.id); + MovePickerUi.request(mode, "row", target.id); } function pointer-focus(id: int) { @@ -781,7 +786,14 @@ export component MailListPane inherits FocusScope { title: @tr("Move to…"); activated => { MovePickerUi.return-to = "list"; - MovePickerUi.request("checked", -1); + MovePickerUi.request("move", "checked", -1); + } + } + MenuItem { + title: @tr("Copy to…"); + activated => { + MovePickerUi.return-to = "list"; + MovePickerUi.request("copy", "checked", -1); } } MenuItem { title: @tr("Mark as read"); activated => { root.bulk_message_action("mark_read"); } } @@ -1379,7 +1391,14 @@ export component MailListPane inherits FocusScope { title: @tr("Move to…"); activated => { MovePickerUi.return-to = "list"; - MovePickerUi.request("row", mail-data.id); + MovePickerUi.request("move", "row", mail-data.id); + } + } + MenuItem { + title: @tr("Copy to…"); + activated => { + MovePickerUi.return-to = "list"; + MovePickerUi.request("copy", "row", mail-data.id); } } if !mail-row.local-mail : MenuItem { title: @tr("Move to spam"); activated => { root.message_action_for_email(mail-data.id, "spam"); } } diff --git a/ui/views/mail-reading-pane.slint b/ui/views/mail-reading-pane.slint index cb06dbd9..8b633fa1 100644 --- a/ui/views/mail-reading-pane.slint +++ b/ui/views/mail-reading-pane.slint @@ -348,6 +348,7 @@ export component MailReadingPane inherits FocusScope { { id: "read", title: root.selected_unread ? @tr("Mark as read") : @tr("Mark as unread"), icon: root.selected_unread ? @image-url("../icons/lucide/outline/mail-open.svg") : @image-url("../icons/lucide/outline/mail.svg") }, { id: "archive", title: @tr("Archive"), icon: @image-url("../icons/lucide/outline/archive.svg") }, { id: "move", title: @tr("Move to…"), icon: @image-url("../icons/lucide/outline/folder-move.svg") }, + { id: "copy", title: @tr("Copy to…"), icon: @image-url("../icons/lucide/outline/copy.svg") }, { id: "spam", title: @tr("Move to spam"), icon: @image-url("../icons/lucide/outline/alert.svg") }, { id: root.trash_view ? "delete_permanently" : "trash", title: root.trash_view ? @tr("Delete permanently…") : @tr("Move to trash"), icon: @image-url("../icons/lucide/outline/trash.svg"), destructive: true }, ]; @@ -366,6 +367,7 @@ export component MailReadingPane inherits FocusScope { { id: "read", title: root.selected_unread ? @tr("Mark as read") : @tr("Mark as unread"), icon: root.selected_unread ? @image-url("../icons/lucide/outline/mail-open.svg") : @image-url("../icons/lucide/outline/mail.svg") }, { id: "archive", title: @tr("Archive"), icon: @image-url("../icons/lucide/outline/archive.svg") }, { id: "move", title: @tr("Move to…"), icon: @image-url("../icons/lucide/outline/folder-move.svg") }, + { id: "copy", title: @tr("Copy to…"), icon: @image-url("../icons/lucide/outline/copy.svg") }, { id: "spam", title: @tr("Move to spam"), icon: @image-url("../icons/lucide/outline/alert.svg") }, { id: root.trash_view ? "delete_permanently" : "trash", title: root.trash_view ? @tr("Delete permanently…") : @tr("Move to trash"), icon: @image-url("../icons/lucide/outline/trash.svg"), destructive: true }, ]; @@ -385,6 +387,7 @@ export component MailReadingPane inherits FocusScope { { id: "original_colors", title: MessageColors.original ? @tr("Use message appearance") : @tr("Show original colors"), icon: @image-url("../icons/lucide/outline/contrast.svg") }, { id: "read", title: root.selected_unread ? @tr("Mark as read") : @tr("Mark as unread"), icon: root.selected_unread ? @image-url("../icons/lucide/outline/mail-open.svg") : @image-url("../icons/lucide/outline/mail.svg") }, { id: "move", title: @tr("Move to…"), icon: @image-url("../icons/lucide/outline/folder-move.svg") }, + { id: "copy", title: @tr("Copy to…"), icon: @image-url("../icons/lucide/outline/copy.svg") }, { id: root.trash_view ? "delete_permanently" : "trash", title: root.trash_view ? @tr("Delete permanently…") : @tr("Move to trash"), icon: @image-url("../icons/lucide/outline/trash.svg"), destructive: true }, ]; private property <[ActionMenuEntry]> local-action-menu-no-print: [ @@ -401,6 +404,7 @@ export component MailReadingPane inherits FocusScope { { id: "original_colors", title: MessageColors.original ? @tr("Use message appearance") : @tr("Show original colors"), icon: @image-url("../icons/lucide/outline/contrast.svg") }, { id: "read", title: root.selected_unread ? @tr("Mark as read") : @tr("Mark as unread"), icon: root.selected_unread ? @image-url("../icons/lucide/outline/mail-open.svg") : @image-url("../icons/lucide/outline/mail.svg") }, { id: "move", title: @tr("Move to…"), icon: @image-url("../icons/lucide/outline/folder-move.svg") }, + { id: "copy", title: @tr("Copy to…"), icon: @image-url("../icons/lucide/outline/copy.svg") }, { id: root.trash_view ? "delete_permanently" : "trash", title: root.trash_view ? @tr("Delete permanently…") : @tr("Move to trash"), icon: @image-url("../icons/lucide/outline/trash.svg"), destructive: true }, ]; // The reader toolbar drops actions that the More menu also offers when @@ -565,7 +569,15 @@ export component MailReadingPane inherits FocusScope { enabled: root.message-keys && !root.selected_is_draft && !EmailReader.find-open; activated => { MovePickerUi.return-to = "reader"; - MovePickerUi.request("open", -1); + MovePickerUi.request("move", "open", -1); + } + } + KeyBinding { + keys: Shortcuts.copy-to; + enabled: root.message-keys && !root.selected_is_draft && !EmailReader.find-open; + activated => { + MovePickerUi.return-to = "reader"; + MovePickerUi.request("copy", "open", -1); } } @@ -1064,9 +1076,9 @@ export component MailReadingPane inherits FocusScope { MessageColors.original = !MessageColors.original; } else if action == "read" { root.message_action(root.selected_unread ? "mark_read" : "mark_unread"); - } else if action == "move" { + } else if action == "move" || action == "copy" { MovePickerUi.return-to = "reader"; - MovePickerUi.request("open", -1); + MovePickerUi.request(action, "open", -1); } else { root.message_action(action); }