Skip to content

Commit

Permalink
Adds import button to Calendar attachments
Browse files Browse the repository at this point in the history
This commit adds the import action to attachments with the ics mime.
  • Loading branch information
mup committed Aug 19, 2024
1 parent 388085b commit 11f4ce3
Show file tree
Hide file tree
Showing 9 changed files with 84 additions and 44 deletions.
52 changes: 37 additions & 15 deletions src/calendar-app/calendar/export/CalendarImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -109,25 +110,46 @@ 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<CalendarEvent, KindaCalendarRow>),
),
]),
],
}).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<Id, string> = 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,
Expand Down Expand Up @@ -176,7 +198,7 @@ export function calendarSelectionDialog(
icon: BootIcons.Expand,
disabled: availableCalendars.length < 2,
helpLabel: () => renderCalendarColor(selectedCalendar, groupColors),
} satisfies DropDownSelectorAttrs<CalendarInfo>)
} satisfies DropDownSelectorAttrs<CalendarInfo>),
]),
],
}).show()
Expand Down
5 changes: 2 additions & 3 deletions src/calendar-app/calendar/export/CalendarImporterDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,8 @@ export async function showCalendarImportDialog(calendarGroupRoot: CalendarGroupR

async function selectAndParseIcalFile(): Promise<ParsedEvent[]> {
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) {
Expand Down
17 changes: 2 additions & 15 deletions src/calendar-app/calendarLocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Id, string> = 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
Expand All @@ -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)
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/common/calendar/date/CalendarUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/common/file/FileController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ export async function showNativeFilePicker(fileTypes?: Array<string>): Promise<R
Dialog.message("couldNotAttachFile_msg")
}

console.log("Failed to import calendar files", err)
console.log("Failed read files", err)
}
}

Expand Down
7 changes: 5 additions & 2 deletions src/common/gui/AttachmentBubble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { focusNext, focusPrevious, Shortcut } from "../misc/KeyManager.js"
import { PosRect } from "./base/Dropdown.js"
import { Keys } from "../api/common/TutanotaConstants.js"
import { px } from "./size.js"
import { Icon } from "./base/Icon.js"
import { AllIcons, Icon } from "./base/Icon.js"
import { theme } from "./theme.js"
import { animations, height, opacity, transform, TransformEnum, width } from "./animation/Animations.js"
import { ease } from "./animation/Easing.js"
Expand All @@ -18,6 +18,7 @@ import { getSafeAreaInsetBottom } from "./HtmlUtils.js"
import { hasError } from "../api/common/utils/ErrorUtils.js"
import { BubbleButton, bubbleButtonHeight, bubbleButtonPadding } from "./base/buttons/BubbleButton.js"
import { CALENDAR_MIME_TYPE, VCARD_MIME_TYPES } from "../file/FileController.js"
import { BootIcons } from "./base/icons/BootIcons.js"

export enum AttachmentType {
GENERIC,
Expand Down Expand Up @@ -76,10 +77,12 @@ async function showAttachmentDetailsPopup(dom: HTMLElement, attrs: AttachmentBub
return panel.deferAfterClose
}

function getAttachmentIcon(type: AttachmentType): Icons {
function getAttachmentIcon(type: AttachmentType): AllIcons {
switch (type) {
case AttachmentType.CONTACT:
return Icons.People
case AttachmentType.CALENDAR:
return BootIcons.Calendar
default:
return Icons.Attachment
}
Expand Down
38 changes: 31 additions & 7 deletions src/mail-app/mail/view/MailViewerViewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,20 @@ import { AttachmentType, getAttachmentType } from "../../../common/gui/Attachmen
import type { ContactImporter } from "../../contacts/ContactImporter.js"
import { InlineImages, revokeInlineImages } from "../../../common/mailFunctionality/inlineImagesUtils.js"
import {
getPathToFolderString,
assertSystemFolderOfType,
getDefaultSender,
getEnabledMailAddressesWithUser,
getMailboxName,
getFolderName,
getMailboxName,
getPathToFolderString,
isNoReplyTeamAddress,
isSystemNotification,
isTutanotaTeamMail,
loadMailDetails,
loadMailHeaders,
getDefaultSender,
assertSystemFolderOfType,
} from "../../../common/mailFunctionality/SharedMailUtils.js"
import { isSystemNotification, isTutanotaTeamMail, isNoReplyTeamAddress } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { getDisplayedSender, getMailBodyText, MailAddressAndName } from "../../../common/api/common/CommonMailUtils.js"
import { CalendarModel } from "../../../calendar-app/calendar/model/CalendarModel.js"

export const enum ContentBlockingStatus {
Block = "0",
Expand Down Expand Up @@ -146,6 +149,7 @@ export class MailViewerViewModel {
private readonly mailFacade: MailFacade,
private readonly cryptoFacade: CryptoFacade,
private readonly contactImporter: lazyAsync<ContactImporter>,
private readonly calendarModel: lazyAsync<CalendarModel>,
) {
this.folderMailboxText = null
if (showFolder) {
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/mail-app/mailLocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ class MailLocator {
this.mailFacade,
this.cryptoFacade,
() => this.contactImporter(),
() => this.calendarModel(),
)
}

Expand Down
4 changes: 4 additions & 0 deletions test/tests/mail/view/MailViewerViewModelTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -45,6 +46,7 @@ o.spec("MailViewerViewModel", function () {
let sendMailModelFactory: (mailboxDetails: MailboxDetail) => Promise<SendMailModel> = () => Promise.resolve(sendMailModel)
let cryptoFacade: CryptoFacade
let contactImporter: ContactImporter
let calendarModel: CalendarModel

function makeViewModelWithHeaders(headers: string) {
entityClient = object()
Expand All @@ -60,6 +62,7 @@ o.spec("MailViewerViewModel", function () {
mailFacade = object()
cryptoFacade = object()
contactImporter = object()
calendarModel = object()
mail = prepareMailWithHeaders(mailFacade, headers)

return new MailViewerViewModel(
Expand All @@ -78,6 +81,7 @@ o.spec("MailViewerViewModel", function () {
mailFacade,
cryptoFacade,
async () => contactImporter,
async () => calendarModel,
)
}

Expand Down

0 comments on commit 11f4ce3

Please sign in to comment.