Skip to content

Commit

Permalink
Export individual mails works on conversation groups
Browse files Browse the repository at this point in the history
  • Loading branch information
wrdhub committed Feb 4, 2025
1 parent e53a34a commit d756690
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 152 deletions.
2 changes: 1 addition & 1 deletion src/mail-app/mail/export/Exporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export async function getMailExportMode(): Promise<MailExportMode> {
* was instructed to open the new zip File containing the exported files
*/
export async function exportMails(
mails: Array<Mail>,
mails: ReadonlyArray<Mail>,
mailFacade: MailFacade,
entityClient: EntityClient,
fileController: FileController,
Expand Down
23 changes: 13 additions & 10 deletions src/mail-app/mail/view/MailViewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,9 @@ export class MailViewModel {
}

/*
If ConversationInListView is active, all mails in the conversation are returned (so they can be processed in a group)
If not, only the primary mail is returned, since that is the one being looked at/interacted with.
*/
If ConversationInListView is active, all mails in the conversation are returned (so they can be processed in a group)
If not, only the primary mail is returned, since that is the one being looked at/interacted with.
*/
async getActionableMails(mails: Mail[]): Promise<ReadonlyArray<Mail>> {
if (this.conversationPrefProvider.getMailListDisplayMode() === MailListDisplayMode.CONVERSATIONS) {
return this.loadConversationsForAllMails(mails)
Expand All @@ -345,19 +345,21 @@ export class MailViewModel {
conversationEntries.push(...entries)
},
async (e) => {
if (e instanceof NotAuthorizedError) {
// Most likely the conversation entry list does not exist anymore. The server does not distinguish between the case when the
// list does not exist and when we have no permission on it (and for good reasons, it prevents enumeration).
// Most often it happens when we are not fully synced with the server yet and the primary mail does not even exist.
//FIXME: not sure what to do here
//return this.conversationItemsForSelectedMailOnly()
} else {
// Most likely the conversation entry list does not exist anymore. The server does not distinguish between the case when the
// list does not exist and when we have no permission on it (and for good reasons, it prevents enumeration).
// Most often it happens when we are not fully synced with the server yet and the primary mail does not even exist.
if (!(e instanceof NotAuthorizedError)) {
throw e
}
},
)
}

// If there are no conversationEntries (somehow they didn't load), just return the mails back
if (conversationEntries.length < 0) {
return mails
}

const byList = groupBy(conversationEntries, (c) => c.mail && listIdPart(c.mail))
const allMails: Mail[] = []
for (const [listId, conversations] of byList.entries()) {
Expand All @@ -370,6 +372,7 @@ export class MailViewModel {

allMails.push(...loaded)
}

return allMails
}

Expand Down
4 changes: 2 additions & 2 deletions src/mail-app/mail/view/MailViewerHeader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { isNotNull, noOp, resolveMaybeLazy } from "@tutao/tutanota-utils"
import { IconButton } from "../../../common/gui/base/IconButton.js"
import { getConfidentialIcon, getFolderIconByType, isTutanotaTeamMail, promptAndDeleteMails, showMoveMailsDropdown } from "./MailGuiUtils.js"
import { BootIcons } from "../../../common/gui/base/icons/BootIcons.js"
import { editDraft, mailViewerMoreActions } from "./MailViewerUtils.js"
import { editDraft, singleMailViewerMoreActions } from "./MailViewerUtils.js"
import { liveDataAttrs } from "../../../common/gui/AriaUtils.js"
import { isKeyPressed } from "../../../common/misc/KeyManager.js"
import { AttachmentBubble, getAttachmentType } from "../../../common/gui/AttachmentBubble.js"
Expand Down Expand Up @@ -796,7 +796,7 @@ export class MailViewerHeader implements Component<MailViewerHeaderAttrs> {
icon: Icons.Trash,
})

actionButtons.push(...mailViewerMoreActions(viewModel))
actionButtons.push(...singleMailViewerMoreActions(viewModel))
}

return actionButtons
Expand Down
109 changes: 10 additions & 99 deletions src/mail-app/mail/view/MailViewerToolbar.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,20 @@
import m, { Children, Component, Vnode } from "mithril"
import { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import { ConversationEntryTypeRef, Mail } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { Mail } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { IconButton } from "../../../common/gui/base/IconButton.js"
import { promptAndDeleteMails, showMoveMailsDropdown } from "./MailGuiUtils.js"
import { noOp, ofClass } from "@tutao/tutanota-utils"
import { assertNotNull, noOp, ofClass } from "@tutao/tutanota-utils"
import { Icons } from "../../../common/gui/base/icons/Icons.js"
import { MailViewerViewModel } from "./MailViewerViewModel.js"
import { UserError } from "../../../common/api/main/UserError.js"
import { showUserError } from "../../../common/misc/ErrorHandlerImpl.js"
import { createDropdown, DropdownButtonAttrs } from "../../../common/gui/base/Dropdown.js"
import { editDraft, mailViewerMoreActions } from "./MailViewerUtils.js"
import { ButtonType } from "../../../common/gui/base/Button.js"
import { editDraft, exportAction, multipleMailViewerMoreActions } from "./MailViewerUtils.js"
import { isApp } from "../../../common/api/common/Env.js"
import { locator } from "../../../common/api/main/CommonLocator.js"
import { showProgressDialog } from "../../../common/gui/dialogs/ProgressDialog.js"
import { lang } from "../../../common/misc/LanguageViewModel.js"
import { DialogHeaderBarAttrs } from "../../../common/gui/base/DialogHeaderBar.js"
import { Dialog, DialogType } from "../../../common/gui/base/Dialog.js"
import { ColumnWidth, Table } from "../../../common/gui/base/Table.js"
import { ExpanderButton, ExpanderPanel } from "../../../common/gui/base/Expander.js"
import stream from "mithril/stream"
import { exportMails } from "../export/Exporter.js"
import { MailModel } from "../model/MailModel.js"
import { LabelsPopup } from "./LabelsPopup.js"
import { allInSameMailbox } from "../model/MailUtils"
import { styles } from "../../../common/gui/styles"
import { MailItem } from "./ConversationViewModel"
import { listIdPart } from "../../../common/api/common/utils/EntityUtils"

/*
note that mailViewerViewModel has a mailModel, so you do not need to pass both if you pass a mailViewerViewModel
Expand All @@ -47,7 +35,7 @@ export class MailViewerActions implements Component<MailViewerToolbarAttrs> {
this.renderSingleMailActions(vnode.attrs),
vnode.attrs.mailViewerViewModel ? m(".nav-bar-spacer") : null,
this.renderActions(vnode.attrs),
this.renderMoreButton(vnode.attrs.mailViewerViewModel),
this.renderMoreButton(vnode.attrs.mailViewerViewModel, vnode.attrs.actionApplyMails),
])
}

Expand Down Expand Up @@ -159,93 +147,16 @@ export class MailViewerActions implements Component<MailViewerToolbarAttrs> {

private renderExportButton(attrs: MailViewerToolbarAttrs) {
if (!isApp() && attrs.mailModel.isExportingMailsAllowed()) {
const operation = locator.operationProgressTracker.startNewOperation()
const ac = new AbortController()
const headerBarAttrs: DialogHeaderBarAttrs = {
left: [
{
label: "cancel_action",
click: () => ac.abort(),
type: ButtonType.Secondary,
},
],
middle: "emptyString_msg",
}

const exportAttrs = exportAction(attrs.actionApplyMails)
return m(IconButton, {
title: "export_action",
click: () =>
showProgressDialog(
lang.getTranslation("mailExportProgress_msg", {
"{current}": Math.round((operation.progress() / 100) * attrs.mails.length).toFixed(0),
"{total}": attrs.mails.length,
}),
exportMails(
attrs.mails,
locator.mailFacade,
locator.entityClient,
locator.fileController,
locator.cryptoFacade,
operation.id,
ac.signal,
)
.then((result) => this.handleExportEmailsResult(result.failed))
.finally(operation.done),
operation.progress,
true,
headerBarAttrs,
),
title: exportAttrs.label,
icon: Icons.Export,
// we know where we got this from, and we know it has the click attribute
click: assertNotNull(exportAttrs.click),
})
}
}

private handleExportEmailsResult(mailList: Mail[]) {
if (mailList && mailList.length > 0) {
const lines = mailList.map((mail) => ({
cells: [mail.sender.address, mail.subject],
actionButtonAttrs: null,
}))

const expanded = stream<boolean>(false)
const dialog = Dialog.createActionDialog({
title: "failedToExport_title",
child: () =>
m("", [
m(".pt-m", lang.get("failedToExport_msg")),
m(".flex-start.items-center", [
m(ExpanderButton, {
label: lang.makeTranslation(
"hide_show",
`${lang.get(expanded() ? "hide_action" : "show_action")} ${lang.get("failedToExport_label", { "{0}": mailList.length })}`,
),
expanded: expanded(),
onExpandedChange: expanded,
}),
]),
m(
ExpanderPanel,
{
expanded: expanded(),
},
m(Table, {
columnHeading: ["email_label", "subject_label"],
columnWidths: [ColumnWidth.Largest, ColumnWidth.Largest],
showActionButtonColumn: false,
lines,
}),
),
]),
okAction: () => dialog.close(),
allowCancel: false,
okActionTextId: "ok_action",
type: DialogType.EditMedium,
})

dialog.show()
}
}

private renderReplyButton(viewModel: MailViewerViewModel) {
const actions: Children = []
actions.push(
Expand Down Expand Up @@ -276,11 +187,11 @@ export class MailViewerActions implements Component<MailViewerToolbarAttrs> {
})
}

private renderMoreButton(viewModel: MailViewerViewModel | undefined): Children {
private renderMoreButton(viewModel: MailViewerViewModel | undefined, actionApplyMails: () => Promise<readonly Mail[]>): Children {
let actions: DropdownButtonAttrs[] = []

if (viewModel) {
actions = mailViewerMoreActions(viewModel, false)
actions = multipleMailViewerMoreActions(viewModel, actionApplyMails)
}

return actions.length > 0
Expand Down
Loading

0 comments on commit d756690

Please sign in to comment.