From 310ccc0c6b6491b9b34b9dd6661bcbe23504a809 Mon Sep 17 00:00:00 2001 From: bir Date: Thu, 21 Nov 2024 15:43:00 +0100 Subject: [PATCH] UI for Export mailbox in settings close: #7952 Co-authored-by: ivk --- src/common/misc/TranslationKey.ts | 9 +- .../mail/model/MailExportController.ts | 46 +++++++ src/mail-app/mailLocator.ts | 5 + src/mail-app/settings/MailExportSettings.ts | 124 ++++++++++++++++++ src/mail-app/settings/MailSettingsViewer.ts | 15 +++ src/mail-app/translations/de.ts | 7 +- src/mail-app/translations/de_sie.ts | 9 +- src/mail-app/translations/en.ts | 7 +- 8 files changed, 214 insertions(+), 8 deletions(-) create mode 100644 src/mail-app/mail/model/MailExportController.ts create mode 100644 src/mail-app/settings/MailExportSettings.ts diff --git a/src/common/misc/TranslationKey.ts b/src/common/misc/TranslationKey.ts index 4792146aca3b..1c672ba204ba 100644 --- a/src/common/misc/TranslationKey.ts +++ b/src/common/misc/TranslationKey.ts @@ -1800,7 +1800,8 @@ export type TranslationKeyType = | "yourMessage_label" | "you_label" | "emptyString_msg" - | "editLabel_action" - | "importantLabel_label" - | "labelLimitExceeded_msg" - | "confirmDeleteLabel_msg" + | "exportMailbox_label" + | "mailboxToExport_label" + | "lastExportTime_Label" + | "exportingEmails_label" + | "mailsExported_label" diff --git a/src/mail-app/mail/model/MailExportController.ts b/src/mail-app/mail/model/MailExportController.ts new file mode 100644 index 000000000000..47b6bebd5106 --- /dev/null +++ b/src/mail-app/mail/model/MailExportController.ts @@ -0,0 +1,46 @@ +import { MailboxDetail } from "../../../common/mailFunctionality/MailboxModel" +import Stream from "mithril/stream" +import stream from "mithril/stream" + +export type MailExportState = + | { type: "idle"; lastExport: Date | null } + | { type: "exporting"; mailbox: MailboxDetail; progress: number; exportedMails: number } + | { + type: "finished" + mailbox: MailboxDetail + exportedMails: number + } + +export class MailExportController { + private _state: Stream = stream({ type: "idle", lastExport: new Date() }) + private progressTimeout: number | null = null + + get state(): Stream { + return this._state + } + + startExport(mailbox: MailboxDetail) { + this._state({ type: "exporting", mailbox: mailbox, progress: 0, exportedMails: 0 }) + this.progressTimeout = window.setInterval(() => { + const oldState = this._state() + if (oldState.type === "exporting") { + if (oldState.progress >= 0.9) { + this._state({ type: "finished", mailbox: mailbox, exportedMails: oldState.exportedMails }) + } else { + this._state({ + ...oldState, + progress: oldState.progress + 0.1, + exportedMails: oldState.exportedMails + 100, + }) + } + } + }, 1000) + } + + cancelExport() { + if (this.progressTimeout) { + clearInterval(this.progressTimeout) + this.progressTimeout = null + } + } +} diff --git a/src/mail-app/mailLocator.ts b/src/mail-app/mailLocator.ts index f95cb50f3fc6..b17be52b027b 100644 --- a/src/mail-app/mailLocator.ts +++ b/src/mail-app/mailLocator.ts @@ -139,6 +139,7 @@ import type { CalendarContactPreviewViewModel } from "../calendar-app/calendar/g import { KeyLoaderFacade } from "../common/api/worker/facades/KeyLoaderFacade.js" import { MobileContactSuggestionProvider } from "../common/native/main/MobileContactSuggestionProvider" import { ContactSuggestion } from "../common/native/common/generatedipc/ContactSuggestion" +import { MailExportController } from "./mail/model/MailExportController" assertMainOrNode() @@ -1167,6 +1168,10 @@ class MailLocator { return this.loginFacade.migrateKdfType(KdfType.Bcrypt, passphrase, currentUser) } + mailExportController: () => Promise = lazyMemoized(async () => { + return new MailExportController() + }) + /** * Factory method for credentials provider that will return an instance injected with the implementations appropriate for the platform. */ diff --git a/src/mail-app/settings/MailExportSettings.ts b/src/mail-app/settings/MailExportSettings.ts new file mode 100644 index 000000000000..7b014d8e40e5 --- /dev/null +++ b/src/mail-app/settings/MailExportSettings.ts @@ -0,0 +1,124 @@ +import m, { Children, Component, Vnode } from "mithril" +import { lang } from "../../common/misc/LanguageViewModel" +import { theme } from "../../common/gui/theme" +import { px } from "../../common/gui/size" +import { ProgressBar } from "../../common/gui/base/ProgressBar" +import { DropDownSelector, type DropDownSelectorAttrs } from "../../common/gui/base/DropDownSelector" +import { MailboxDetail } from "../../common/mailFunctionality/MailboxModel" +import { getMailboxName } from "../../common/mailFunctionality/SharedMailUtils" +import { mailLocator } from "../mailLocator" +import { first } from "@tutao/tutanota-utils" +import { LoginController } from "../../common/api/main/LoginController" +import { Button, ButtonType } from "../../common/gui/base/Button" +import { MailExportController } from "../mail/model/MailExportController" +import { formatDate } from "../../common/misc/Formatter" +import Stream from "mithril/stream" + +interface MailExportSettingsAttrs { + mailboxDetails: MailboxDetail[] + logins: LoginController + mailExportController: MailExportController +} + +export class MailExportSettings implements Component { + private selectedMailbox: MailboxDetail | null = null + private controllerSubscription: Stream | null = null + + oncreate(vnode: Vnode) { + this.controllerSubscription = vnode.attrs.mailExportController.state.map(m.redraw) + } + + onremove() { + if (this.controllerSubscription) { + this.controllerSubscription.end() + this.controllerSubscription = null + } + } + + view(vnode: Vnode): Children { + const { mailboxDetails } = vnode.attrs + this.selectedMailbox = this.selectedMailbox ?? first(mailboxDetails) + const state = vnode.attrs.mailExportController.state() + return [ + m(DropDownSelector, { + label: "mailboxToExport_label", + items: mailboxDetails.map((mailboxDetail) => { + return { name: getMailboxName(mailLocator.logins, mailboxDetail), value: mailboxDetail } + }), + selectedValue: this.selectedMailbox, + selectionChangedHandler: (selectedMailbox) => { + this.selectedMailbox = selectedMailbox + }, + dropdownWidth: 300, + disabled: state.type === "exporting", + } satisfies DropDownSelectorAttrs), + state.type === "exporting" + ? [ + m(".flex-space-between.items-center.mt.mb-s", [ + m(".flex-grow.mr", [ + m( + "small.noselect", + lang.get("exportingEmails_label", { + "{count}": state.exportedMails, + }), + ), + m( + ".rel.full-width.mt-s", + { + style: { + "background-color": theme.content_border, + height: px(2), + }, + }, + m(ProgressBar, { progress: state.progress }), + ), + ]), + m(Button, { + label: "cancel_action", + type: ButtonType.Secondary, + click: () => { + vnode.attrs.mailExportController.cancelExport() + }, + }), + ]), + ] + : state.type === "idle" + ? [ + m(".flex-space-between.items-center.mt.mb-s", [ + m( + "small.noselect", + state.lastExport + ? lang.get("lastExportTime_Label", { + "{date}": formatDate(state.lastExport), + }) + : null, + ), + m(Button, { + label: "export_action", + click: () => { + if (this.selectedMailbox) { + vnode.attrs.mailExportController.startExport(this.selectedMailbox) + } + }, + type: ButtonType.Secondary, + }), + ]), + ] + : [ + m(".flex-space-between.items-center.mt.mb-s", [ + m( + "small.noselect", + lang.get("mailsExported_label", { + "{numbers}": state.exportedMails, + }), + ), + m(Button, { + label: "open_action", + click: () => {}, + type: ButtonType.Secondary, + }), + ]), + ], + ] + } +} diff --git a/src/mail-app/settings/MailSettingsViewer.ts b/src/mail-app/settings/MailSettingsViewer.ts index e8550be66630..68a72e02cd3c 100644 --- a/src/mail-app/settings/MailSettingsViewer.ts +++ b/src/mail-app/settings/MailSettingsViewer.ts @@ -49,6 +49,8 @@ import { UpdatableSettingsViewer } from "../../common/settings/Interfaces.js" import { mailLocator } from "../mailLocator.js" import { getDefaultSenderFromUser, getFolderName } from "../mail/model/MailUtils.js" import { elementIdPart } from "../../common/api/common/utils/EntityUtils.js" +import { MailExportSettings } from "./MailExportSettings" +import { MailExportController } from "../mail/model/MailExportController" assertMainOrNode() @@ -70,6 +72,7 @@ export class MailSettingsViewer implements UpdatableSettingsViewer { private customerInfo: CustomerInfo | null private mailAddressTableModel: MailAddressTableModel | null = null private mailAddressTableExpanded: boolean + private mailExportController: MailExportController | null = null private offlineStorageSettings = new OfflineStorageSettingsModel(mailLocator.logins.getUserController(), deviceConfig) @@ -87,6 +90,10 @@ export class MailSettingsViewer implements UpdatableSettingsViewer { this._inboxRulesTableLines = stream>([]) this._outOfOfficeStatus = stream(lang.get("deactivated_label")) this._indexStateWatch = null + mailLocator.mailExportController().then((controller) => { + this.mailExportController = controller + m.redraw() + }) // normally we would maybe like to get it as an argument but these viewers are created in an odd way mailLocator.mailAddressTableModelForOwnMailbox().then((model) => { this.mailAddressTableModel = model @@ -382,6 +389,14 @@ export class MailSettingsViewer implements UpdatableSettingsViewer { }), ), ], + m(".h4.mt-l", lang.get("exportMailbox_label")), + this.mailExportController + ? m(MailExportSettings, { + mailboxDetails: mailLocator.mailboxModel.mailboxDetails(), + logins: mailLocator.logins, + mailExportController: this.mailExportController, + }) + : null, ], ), ] diff --git a/src/mail-app/translations/de.ts b/src/mail-app/translations/de.ts index c9a23004a4bd..958090e6984d 100644 --- a/src/mail-app/translations/de.ts +++ b/src/mail-app/translations/de.ts @@ -1820,6 +1820,11 @@ export default { "yourFolders_action": "DEINE ORDNER", "yourMessage_label": "Deine Nachricht", "you_label": "Du", - "confirmDeleteLabel_msg": "Möchtest du wirklich das Label \"{1}\" löschen?" + "confirmDeleteLabel_msg": "Möchtest du wirklich das Label \"{1}\" löschen?", + "exportMailbox_label": "", + "mailboxToExport_label": "", + "lastExportTime_Label": "", + "exportingEmails_label": "", + "mailsExported_label": "", } } diff --git a/src/mail-app/translations/de_sie.ts b/src/mail-app/translations/de_sie.ts index de4e39dec383..7fba7ca6f635 100644 --- a/src/mail-app/translations/de_sie.ts +++ b/src/mail-app/translations/de_sie.ts @@ -1819,7 +1819,12 @@ export default { "yourCalendars_label": "Deine Kalender", "yourFolders_action": "Ihre ORDNER", "yourMessage_label": "Ihre Nachricht", - "you_label": "Sie", - "confirmDeleteLabel_msg": "Möchten Sie wirklich das Label \"{1}\" löschen?" + "you_label": "Sie", + "confirmDeleteLabel_msg": "Möchten Sie wirklich das Label \"{1}\" löschen?", + "exportMailbox_label": "", + "mailboxToExport_label": "", + "lastExportTime_Label": "", + "exportingEmails_label": "", + "mailsExported_label": "", } } diff --git a/src/mail-app/translations/en.ts b/src/mail-app/translations/en.ts index 7c3814b646a6..f4918d638aab 100644 --- a/src/mail-app/translations/en.ts +++ b/src/mail-app/translations/en.ts @@ -1816,6 +1816,11 @@ export default { "yourFolders_action": "YOUR FOLDERS", "yourMessage_label": "Your message", "you_label": "You", - "confirmDeleteLabel_msg": "Are you sure that you want to delete the label \"{1}\"?" + "confirmDeleteLabel_msg": "Are you sure that you want to delete the label \"{1}\"?", + "exportMailbox_label": "Export Mailbox", + "mailboxToExport_label": "Mailbox to Export", + "lastExportTime_Label": "Last export: {date}", + "exportingEmails_label": "Exporting emails: {count}", + "mailsExported_label": "Emails exported: {numbers}", } }