Skip to content

Commit

Permalink
[Calendar] Adds default reminders to calendars
Browse files Browse the repository at this point in the history
This commit adds a new feature that allows users to set multiple default reminders at each calendar, either shared or private.
These reminders are unique for each user's calendar.

Co-authored-by: rih <[email protected]>
Co-authored-by: Murilo Rocha Pereira <[email protected]>
  • Loading branch information
domesticated-raptor and murilopereirame committed Aug 15, 2024
1 parent b61febb commit bc19284
Show file tree
Hide file tree
Showing 21 changed files with 691 additions and 474 deletions.
10 changes: 10 additions & 0 deletions schemas/tutanota.json
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,16 @@
"info": "RemoveValue Contact/autoTransmitPassword/78."
}
]
},
{
"version": 74,
"changes": [
{
"name": "AddAssociation",
"sourceType": "GroupSettings",
"info": "AddAssociation GroupSettings/defaultAlarmsList/AGGREGATION/1446."
}
]
}
]
}
20 changes: 18 additions & 2 deletions src/calendar-app/calendar/gui/EditCalendarDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import stream from "mithril/stream"
import { TextField } from "../../../common/gui/base/TextField.js"
import { lang } from "../../../common/misc/LanguageViewModel.js"
import type { TranslationKeyType } from "../../../common/misc/TranslationKey.js"
import { downcast } from "@tutao/tutanota-utils"
import { deepEqual, downcast } from "@tutao/tutanota-utils"
import { AlarmInterval } from "../../../common/calendar/date/CalendarUtils.js"
import { RemindersEditor } from "./RemindersEditor.js"

type CalendarProperties = {
name: string
color: string
alarms: AlarmInterval[]
}

export function showEditCalendarDialog(
{ name, color }: CalendarProperties,
{ name, color, alarms }: CalendarProperties,
titleTextId: TranslationKeyType,
shared: boolean,
okAction: (arg0: Dialog, arg1: CalendarProperties) => unknown,
Expand All @@ -22,6 +25,7 @@ export function showEditCalendarDialog(
const nameStream = stream(name)
let colorPickerDom: HTMLInputElement | null
const colorStream = stream("#" + color)

Dialog.showActionDialog({
title: () => lang.get(titleTextId),
allowOkWithReturn: true,
Expand All @@ -44,13 +48,25 @@ export function showEditCalendarDialog(
colorStream(target.value)
},
}),
m(RemindersEditor, {
alarms,
addAlarm: (alarm: AlarmInterval) => {
alarms?.push(alarm)
},
removeAlarm: (alarm: AlarmInterval) => {
const index = alarms?.findIndex((a: AlarmInterval) => deepEqual(a, alarm))
if (index !== -1) alarms?.splice(index, 1)
},
label: "calendarDefaultReminder_label",
}),
]),
},
okActionTextId: okTextId,
okAction: (dialog: Dialog) => {
okAction(dialog, {
name: nameStream(),
color: colorStream().substring(1),
alarms,
})
},
})
Expand Down
122 changes: 122 additions & 0 deletions src/calendar-app/calendar/gui/RemindersEditor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import m, { Children, Component, Vnode } from "mithril"
import { TextField, TextFieldAttrs, TextFieldType } from "../../../common/gui/base/TextField.js"
import { createAlarmIntervalItems, createCustomRepeatRuleUnitValues, humanDescriptionForAlarmInterval } from "./CalendarGuiUtils.js"
import { lang, TranslationKey } from "../../../common/misc/LanguageViewModel.js"
import { IconButton } from "../../../common/gui/base/IconButton.js"
import { Icons } from "../../../common/gui/base/icons/Icons.js"
import { attachDropdown } from "../../../common/gui/base/Dropdown.js"
import { AlarmInterval, AlarmIntervalUnit } from "../../../common/calendar/date/CalendarUtils.js"
import { Dialog } from "../../../common/gui/base/Dialog.js"
import { DropDownSelector } from "../../../common/gui/base/DropDownSelector.js"
import { deepEqual } from "@tutao/tutanota-utils"

export type RemindersEditorAttrs = {
addAlarm: (alarm: AlarmInterval) => unknown
removeAlarm: (alarm: AlarmInterval) => unknown
alarms: readonly AlarmInterval[]
label: TranslationKey
}

export class RemindersEditor implements Component<RemindersEditorAttrs> {
view(vnode: Vnode<RemindersEditorAttrs>): Children {
const { addAlarm, removeAlarm, alarms } = vnode.attrs
const addNewAlarm = (newAlarm: AlarmInterval) => {
const hasAlarm = alarms.find((alarm) => deepEqual(alarm, newAlarm))
if (hasAlarm) return
addAlarm(newAlarm)
}
const textFieldAttrs: Array<TextFieldAttrs> = alarms.map((a) => ({
value: humanDescriptionForAlarmInterval(a, lang.languageTag),
label: "emptyString_msg",
isReadOnly: true,
injectionsRight: () =>
m(IconButton, {
title: "delete_action",
icon: Icons.Cancel,
click: () => removeAlarm(a),
}),
}))

textFieldAttrs.push({
value: lang.get("add_action"),
label: "emptyString_msg",
isReadOnly: true,
injectionsRight: () =>
m(
IconButton,
attachDropdown({
mainButtonAttrs: {
title: "add_action",
icon: Icons.Add,
},
childAttrs: () => [
...createAlarmIntervalItems(lang.languageTag).map((i) => ({
label: () => i.name,
click: () => addNewAlarm(i.value),
})),
{
label: () => lang.get("calendarReminderIntervalDropdownCustomItem_label"),
click: () => {
this.showCustomReminderIntervalDialog((value, unit) => {
addNewAlarm({
value,
unit,
})
})
},
},
],
}),
),
})

textFieldAttrs[0].label = vnode.attrs.label

return m(
".flex.col.flex-half.pl-s",
textFieldAttrs.map((a) => m(TextField, a)),
)
}

private showCustomReminderIntervalDialog(onAddAction: (value: number, unit: AlarmIntervalUnit) => void) {
let timeReminderValue = 0
let timeReminderUnit: AlarmIntervalUnit = AlarmIntervalUnit.MINUTE

Dialog.showActionDialog({
title: () => lang.get("calendarReminderIntervalCustomDialog_title"),
allowOkWithReturn: true,
child: {
view: () => {
const unitItems = createCustomRepeatRuleUnitValues() ?? []
return m(".flex full-width pt-s", [
m(TextField, {
type: TextFieldType.Number,
min: 0,
label: "calendarReminderIntervalValue_label",
value: timeReminderValue.toString(),
oninput: (v) => {
const time = Number.parseInt(v)
const isEmpty = v === ""
if (!Number.isNaN(time) || isEmpty) timeReminderValue = isEmpty ? 0 : Math.abs(time)
},
class: "flex-half no-appearance", //Removes the up/down arrow from input number. Pressing arrow up/down key still working
}),
m(DropDownSelector, {
label: "emptyString_msg",
selectedValue: timeReminderUnit,
items: unitItems,
class: "flex-half pl-s",
selectionChangedHandler: (selectedValue: AlarmIntervalUnit) => (timeReminderUnit = selectedValue as AlarmIntervalUnit),
disabled: false,
}),
])
},
},
okActionTextId: "add_action",
okAction: (dialog: Dialog) => {
onAddAction(timeReminderValue, timeReminderUnit)
dialog.close()
},
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ export class CalendarEventAlarmModel {
this.uiUpdateCallback()
}

removeAll() {
this._alarms.splice(0)
}

addAll(alarmIntervalList: AlarmInterval[]) {
this._alarms.push(...alarmIntervalList)
}

get alarms(): ReadonlyArray<AlarmInterval> {
return this._alarms
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Dialog } from "../../../../common/gui/base/Dialog.js"
import { lang } from "../../../../common/misc/LanguageViewModel.js"
import { ButtonType } from "../../../../common/gui/base/Button.js"
import { Keys } from "../../../../common/api/common/TutanotaConstants.js"
import { getStartOfTheWeekOffsetForUser, getTimeFormatForUser } from "../../../../common/calendar/date/CalendarUtils.js"
import { AlarmInterval, getStartOfTheWeekOffsetForUser, getTimeFormatForUser, parseAlarmInterval } from "../../../../common/calendar/date/CalendarUtils.js"
import { client } from "../../../../common/misc/ClientDetector.js"
import type { DialogHeaderBarAttrs } from "../../../../common/gui/base/DialogHeaderBar.js"
import { assertNotNull, noOp, Thunk } from "@tutao/tutanota-utils"
Expand Down Expand Up @@ -42,10 +42,21 @@ type EditDialogOkHandler = (posRect: PosRect, finish: Thunk) => Promise<unknown>
async function showCalendarEventEditDialog(model: CalendarEventModel, responseMail: Mail | null, handler: EditDialogOkHandler): Promise<void> {
const recipientsSearch = await locator.recipientsSearchModel()
const { HtmlEditor } = await import("../../../../common/gui/editor/HtmlEditor.js")
const groupColors: Map<Id, string> = locator.logins.getUserController().userSettingsGroupRoot.groupSettings.reduce((acc, gc) => {
const groupSettings = locator.logins.getUserController().userSettingsGroupRoot.groupSettings

const groupColors: Map<Id, string> = groupSettings.reduce((acc, gc) => {
acc.set(gc.group, gc.color)
return acc
}, new Map())

const defaultAlarms: Map<Id, AlarmInterval[]> = groupSettings.reduce((acc, gc) => {
acc.set(
gc.group,
gc.defaultAlarmsList.map((alarm) => parseAlarmInterval(alarm.trigger)),
)
return acc
}, new Map())

const descriptionText = convertTextToHtml(model.editModels.description.content)
const descriptionEditor: HtmlEditor = new HtmlEditor("description_label")
.setMinHeight(400)
Expand Down Expand Up @@ -89,13 +100,15 @@ async function showCalendarEventEditDialog(model: CalendarEventModel, responseMa
headerDom = dom
},
}

const dialog: Dialog = Dialog.editDialog(dialogHeaderBarAttrs, CalendarEventEditView, {
model,
recipientsSearch,
descriptionEditor,
startOfTheWeekOffset: getStartOfTheWeekOffsetForUser(locator.logins.getUserController().userSettingsGroupRoot),
timeFormat: getTimeFormatForUser(locator.logins.getUserController().userSettingsGroupRoot),
groupColors,
defaultAlarms,
})
.addShortcut({
key: Keys.ESC,
Expand All @@ -113,6 +126,7 @@ async function showCalendarEventEditDialog(model: CalendarEventModel, responseMa
// Prevent focusing text field automatically on mobile. It opens keyboard and you don't see all details.
dialog.setFocusOnLoadFunction(noOp)
}

dialog.show()
}

Expand Down
Loading

0 comments on commit bc19284

Please sign in to comment.