From 11f4ce3aab7d74324cae62d9079f0ed9a39b3091 Mon Sep 17 00:00:00 2001 From: mup Date: Mon, 19 Aug 2024 13:39:31 +0200 Subject: [PATCH] Adds import button to Calendar attachments This commit adds the import action to attachments with the ics mime. --- .../calendar/export/CalendarImporter.ts | 52 +++++++++++++------ .../calendar/export/CalendarImporterDialog.ts | 5 +- src/calendar-app/calendarLocator.ts | 17 +----- src/common/calendar/date/CalendarUtils.ts | 2 +- src/common/file/FileController.ts | 2 +- src/common/gui/AttachmentBubble.ts | 7 ++- src/mail-app/mail/view/MailViewerViewModel.ts | 38 +++++++++++--- src/mail-app/mailLocator.ts | 1 + .../mail/view/MailViewerViewModelTest.ts | 4 ++ 9 files changed, 84 insertions(+), 44 deletions(-) diff --git a/src/calendar-app/calendar/export/CalendarImporter.ts b/src/calendar-app/calendar/export/CalendarImporter.ts index ff127c775651..bc86f1c899ad 100644 --- a/src/calendar-app/calendar/export/CalendarImporter.ts +++ b/src/calendar-app/calendar/export/CalendarImporter.ts @@ -16,11 +16,12 @@ import m from "mithril" import { DropDownSelector, DropDownSelectorAttrs } from "../../../common/gui/base/DropDownSelector.js" import { getSharedGroupName, hasCapabilityOnGroup } from "../../../common/sharing/GroupUtils.js" import { BootIcons } from "../../../common/gui/base/icons/BootIcons.js" -import { CalendarInfo } from "../model/CalendarModel.js" +import { CalendarInfo, CalendarModel } from "../model/CalendarModel.js" import { UserController } from "../../../common/api/main/UserController.js" import { ShareCapability } from "../../../common/api/common/TutanotaConstants.js" import { renderCalendarColor } from "../gui/CalendarGuiUtils.js" import { GroupColors } from "../view/CalendarView.js" +import { showCalendarImportDialog } from "./CalendarImporterDialog.js" export type ParsedEvent = { event: Require<"uid", CalendarEvent> @@ -52,7 +53,7 @@ export function parseCalendarStringData(value: string, zone: string): ParsedCale } /** - * Show a dialog with a preview of a given list of events + * Shows a dialog with a preview of a given list of events * @param events The event list to be previewed * @param okAction The action to be executed when the user press the ok or continue button * @param title @@ -109,18 +110,12 @@ export function showEventsImportDialog(events: CalendarEvent[], okAction: (dialo activeIndex: null, selectedItems: new Set(), }, - onLoadMore() { - }, - onRangeSelectionTowards(item: CalendarEvent) { - }, - onRetryLoading() { - }, - onSingleSelection(item: CalendarEvent) { - }, - onSingleTogglingMultiselection(item: CalendarEvent) { - }, - onStopLoading() { - }, + onLoadMore() {}, + onRangeSelectionTowards(item: CalendarEvent) {}, + onRetryLoading() {}, + onSingleSelection(item: CalendarEvent) {}, + onSingleTogglingMultiselection(item: CalendarEvent) {}, + onStopLoading() {}, } satisfies ListAttrs), ), ]), @@ -128,6 +123,33 @@ export function showEventsImportDialog(events: CalendarEvent[], okAction: (dialo }).show() } +/** + * Handle the import of calendar events with preview of events to be imported + * @param calendarModel + * @param userController + * @param events The event list to be previewed and imported + */ +export async function importCalendarFile(calendarModel: CalendarModel, userController: UserController, events: ParsedEvent[]) { + const groupSettings = userController.userSettingsGroupRoot.groupSettings + const calendarInfos = await calendarModel.getCalendarInfos() + const groupColors: Map = groupSettings.reduce((acc, gc) => { + acc.set(gc.group, gc.color) + return acc + }, new Map()) + + calendarSelectionDialog(Array.from(calendarInfos.values()), userController, groupColors, (dialog, selectedCalendar) => { + dialog.close() + showCalendarImportDialog(selectedCalendar.groupRoot, events) + }) +} + +/** + * Shows a dialog with user's calendars that are able to receive new events + * @param calendars List of user's calendars + * @param userController + * @param groupColors List of calendar's colors + * @param okAction + */ export function calendarSelectionDialog( calendars: CalendarInfo[], userController: UserController, @@ -176,7 +198,7 @@ export function calendarSelectionDialog( icon: BootIcons.Expand, disabled: availableCalendars.length < 2, helpLabel: () => renderCalendarColor(selectedCalendar, groupColors), - } satisfies DropDownSelectorAttrs) + } satisfies DropDownSelectorAttrs), ]), ], }).show() diff --git a/src/calendar-app/calendar/export/CalendarImporterDialog.ts b/src/calendar-app/calendar/export/CalendarImporterDialog.ts index bc5aaf724e45..4fd3d01fd494 100644 --- a/src/calendar-app/calendar/export/CalendarImporterDialog.ts +++ b/src/calendar-app/calendar/export/CalendarImporterDialog.ts @@ -75,9 +75,8 @@ export async function showCalendarImportDialog(calendarGroupRoot: CalendarGroupR async function selectAndParseIcalFile(): Promise { try { - const dataFiles = isApp() - ? await showNativeFilePicker(["ical", "ics", "ifb", "icalendar"]) - : await showFileChooser(true, ["ical", "ics", "ifb", "icalendar"]) + const allowedExtensions = ["ical", "ics", "ifb", "icalendar"] + const dataFiles = isApp() ? await showNativeFilePicker(allowedExtensions) : await showFileChooser(true, allowedExtensions) const contents = dataFiles.map((file) => parseCalendarFile(file).contents) return contents.flat() } catch (e) { diff --git a/src/calendar-app/calendarLocator.ts b/src/calendar-app/calendarLocator.ts index bf01e8ccbb27..553b5b2b73f0 100644 --- a/src/calendar-app/calendarLocator.ts +++ b/src/calendar-app/calendarLocator.ts @@ -730,19 +730,9 @@ class CalendarLocator { const files = await this.fileApp.getFilesMetaData(filesUris) const areAllICSFiles = files.every((file) => file.mimeType === CALENDAR_MIME_TYPE) if (areAllICSFiles) { - const calendarModel = await this.calendarModel() - const groupSettings = this.logins.getUserController().userSettingsGroupRoot.groupSettings - const calendarInfos = await calendarModel.getCalendarInfos() - const groupColors: Map = groupSettings.reduce((acc, gc) => { - acc.set(gc.group, gc.color) - return acc - }, new Map()) - - const { calendarSelectionDialog, parseCalendarFile } = await import("../calendar-app/calendar/export/CalendarImporter.js") - const { showCalendarImportDialog } = await import("../calendar-app/calendar/export/CalendarImporterDialog.js") + const { importCalendarFile, parseCalendarFile } = await import("../calendar-app/calendar/export/CalendarImporter.js") let parsedEvents: ParsedEvent[] = [] - for (const fileRef of files) { const dataFile = await this.fileApp.readDataFile(fileRef.location) if (dataFile == null) continue @@ -751,10 +741,7 @@ class CalendarLocator { parsedEvents.push(...data.contents) } - calendarSelectionDialog(Array.from(calendarInfos.values()), this.logins.getUserController(), groupColors, (dialog, selectedCalendar) => { - dialog.close() - showCalendarImportDialog(selectedCalendar.groupRoot, parsedEvents) - }) + await importCalendarFile(await this.calendarModel(), this.logins.getUserController(), parsedEvents) } } diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 4d835e9881d2..f1cf0445881b 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -30,7 +30,7 @@ import type { RepeatRule } from "../../api/entities/sys/TypeRefs.js" import { createDateWrapper, DateWrapper, User } from "../../api/entities/sys/TypeRefs.js" import { isSameId } from "../../api/common/utils/EntityUtils" import type { Time } from "./Time.js" -import type { CalendarInfo } from "../../../calendar-app/calendar/model/CalendarModel" +import { CalendarInfo } from "../../../calendar-app/calendar/model/CalendarModel" import { DateProvider } from "../../api/common/DateProvider" import { EntityClient } from "../../api/common/EntityClient.js" import { CalendarEventUidIndexEntry } from "../../api/worker/facades/lazy/CalendarFacade.js" diff --git a/src/common/file/FileController.ts b/src/common/file/FileController.ts index c31d824589a0..9d6e96b88071 100644 --- a/src/common/file/FileController.ts +++ b/src/common/file/FileController.ts @@ -352,7 +352,7 @@ export async function showNativeFilePicker(fileTypes?: Array): Promise, + private readonly calendarModel: lazyAsync, ) { this.folderMailboxText = null if (showFolder) { @@ -1022,8 +1026,11 @@ export class MailViewerViewModel { } async importAttachment(file: TutanotaFile) { - if (getAttachmentType(file.mimeType ?? "") === AttachmentType.CONTACT) { + const attachmentType = getAttachmentType(file.mimeType ?? "") + if (attachmentType === AttachmentType.CONTACT) { await this.importContacts(file) + } else if (attachmentType === AttachmentType.CALENDAR) { + await this.importCalendar(file) } } @@ -1042,8 +1049,25 @@ export class MailViewerViewModel { } } + private async importCalendar(file: TutanotaFile) { + file = (await this.cryptoFacade.enforceSessionKeyUpdateIfNeeded(this._mail, [file]))[0] + try { + const { importCalendarFile, parseCalendarFile } = await import("../../../calendar-app/calendar/export/CalendarImporter.js") + const dataFile = await this.fileController.getAsDataFile(file) + const data = parseCalendarFile(dataFile) + await importCalendarFile(await this.calendarModel(), this.logins.getUserController(), data.contents) + } catch (e) { + console.log(e) + throw new UserError("errorDuringFileOpen_msg") + } + } + canImportFile(file: TutanotaFile): boolean { - return this.logins.isInternalUserLoggedIn() && file.mimeType != null && getAttachmentType(file.mimeType) === AttachmentType.CONTACT + if (!this.logins.isInternalUserLoggedIn() || file.mimeType == null) { + return false + } + const attachmentType = getAttachmentType(file.mimeType) + return attachmentType === AttachmentType.CONTACT || attachmentType === AttachmentType.CALENDAR } canReplyAll(): boolean { diff --git a/src/mail-app/mailLocator.ts b/src/mail-app/mailLocator.ts index d081c6473cf9..fa354f779f28 100644 --- a/src/mail-app/mailLocator.ts +++ b/src/mail-app/mailLocator.ts @@ -451,6 +451,7 @@ class MailLocator { this.mailFacade, this.cryptoFacade, () => this.contactImporter(), + () => this.calendarModel(), ) } diff --git a/test/tests/mail/view/MailViewerViewModelTest.ts b/test/tests/mail/view/MailViewerViewModelTest.ts index 7113689f08c0..9cef6bbf19e2 100644 --- a/test/tests/mail/view/MailViewerViewModelTest.ts +++ b/test/tests/mail/view/MailViewerViewModelTest.ts @@ -26,6 +26,7 @@ import { ContactImporter } from "../../../../src/mail-app/contacts/ContactImport import { MailboxDetail, MailModel } from "../../../../src/common/mailFunctionality/MailModel.js" import { ContactModel } from "../../../../src/common/contactsFunctionality/ContactModel.js" import { SendMailModel } from "../../../../src/common/mailFunctionality/SendMailModel.js" +import { CalendarModel } from "../../../../src/calendar-app/calendar/model/CalendarModel.js" o.spec("MailViewerViewModel", function () { let mail: Mail @@ -45,6 +46,7 @@ o.spec("MailViewerViewModel", function () { let sendMailModelFactory: (mailboxDetails: MailboxDetail) => Promise = () => Promise.resolve(sendMailModel) let cryptoFacade: CryptoFacade let contactImporter: ContactImporter + let calendarModel: CalendarModel function makeViewModelWithHeaders(headers: string) { entityClient = object() @@ -60,6 +62,7 @@ o.spec("MailViewerViewModel", function () { mailFacade = object() cryptoFacade = object() contactImporter = object() + calendarModel = object() mail = prepareMailWithHeaders(mailFacade, headers) return new MailViewerViewModel( @@ -78,6 +81,7 @@ o.spec("MailViewerViewModel", function () { mailFacade, cryptoFacade, async () => contactImporter, + async () => calendarModel, ) }