Skip to content

Commit

Permalink
[Calendar] Removes default reminders from shared calendars
Browse files Browse the repository at this point in the history
This commit removes the recently added feature that allows users to set multiple default reminders from not owned shared calendars.
These reminders are unique for each user's calendar and should not be available for shared calendars the user don't own.

CalendarInfo.userIsOwner was introduced, so we could keep using the CalendarInfo.shared field to preserve the current behavior(v235.240718.0) when dealing with selecting calendars.
This commit also rewrites some of the logic related to CalendarInfo.shared, which affects calendar sharing, calendar list(on the sidebar) and event invites,especially the capabilities a user has when editing an event.
  • Loading branch information
andrehgdias authored and domesticated-raptor committed Aug 15, 2024
1 parent bc19284 commit 4f48ea8
Show file tree
Hide file tree
Showing 15 changed files with 131 additions and 95 deletions.
3 changes: 2 additions & 1 deletion src/calendar-app/calendar/export/CalendarParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ import WindowsZones from "./WindowsZones"
import type { ParsedCalendarData } from "./CalendarImporter"
import { isMailAddress } from "../../../common/misc/FormatValidator"
import { CalendarAttendeeStatus, CalendarMethod, EndType, RepeatPeriod, reverse } from "../../../common/api/common/TutanotaConstants"
import { AlarmInterval, AlarmIntervalUnit, serializeAlarmInterval } from "../../../common/calendar/date/CalendarUtils.js"
import { AlarmInterval, AlarmIntervalUnit } from "../../../common/calendar/date/CalendarUtils.js"
import { AlarmInfoTemplate } from "../../../common/api/worker/facades/lazy/CalendarFacade.js"
import { serializeAlarmInterval } from "../../../common/api/common/utils/CommonCalendarUtils.js"

function parseDateString(dateString: string): {
year: number
Expand Down
8 changes: 8 additions & 0 deletions src/calendar-app/calendar/gui/CalendarGuiUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,14 @@ export function getEventType(
return EventType.SHARED_RO
}

/**
* if the event has a _ownerGroup, it means there is a calendar set to it
* so, if the user is the owner of said calendar they are free to manage the event however they want
**/
if ((isOrganizer || existingOrganizer === null) && calendarInfoForEvent.userIsOwner) {
return EventType.OWN
}

if (calendarInfoForEvent.shared) {
const canWrite = hasCapabilityOnGroup(user, calendarInfoForEvent.group, ShareCapability.Write)
if (canWrite) {
Expand Down
23 changes: 12 additions & 11 deletions src/calendar-app/calendar/gui/EditCalendarDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,18 @@ 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",
}),
!shared &&
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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { generateEventElementId } from "../../../../common/api/common/utils/CommonCalendarUtils.js"
import { generateEventElementId, serializeAlarmInterval } from "../../../../common/api/common/utils/CommonCalendarUtils.js"
import { noOp, remove } from "@tutao/tutanota-utils"
import { EventType } from "./CalendarEventModel.js"
import { DateProvider } from "../../../../common/api/common/DateProvider.js"
import { AlarmInterval, alarmIntervalToLuxonDurationLikeObject, serializeAlarmInterval } from "../../../../common/calendar/date/CalendarUtils.js"
import { AlarmInterval, alarmIntervalToLuxonDurationLikeObject } from "../../../../common/calendar/date/CalendarUtils.js"
import { Duration } from "luxon"
import { AlarmInfoTemplate } from "../../../../common/api/worker/facades/lazy/CalendarFacade.js"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,14 @@ export class CalendarEventWhoModel {
}

set selectedCalendar(v: CalendarInfo) {
if (v.shared && this._attendees.size > 0) {
/**
* when changing the calendar of an event, if the user is the organiser
* they can link any of their owned calendars(private or shared) to said event
* even if the event has guests
**/
if (!v.userIsOwner && v.shared && this._attendees.size > 0) {
throw new ProgrammingError("tried to select shared calendar while there are guests.")
} else if (v.shared && this.isNew && this._organizer != null) {
} else if (!v.userIsOwner && v.shared && this.isNew && this._organizer != null) {
// for new events, it's possible to have an organizer but no attendees if you only add yourself.
this._organizer = null
}
Expand Down Expand Up @@ -154,7 +159,11 @@ export class CalendarEventWhoModel {
* unable to send updates.
*/
get canModifyGuests(): boolean {
return !(this.selectedCalendar?.shared || this.eventType === EventType.INVITE || this.operation === CalendarOperation.EditThis)
/**
* if the user is the event's organiser and the owner of its linked calendar, the user can modify the guests freely
**/
const userIsOwner = this.eventType === EventType.OWN && this.selectedCalendar.userIsOwner
return userIsOwner || !(this.selectedCalendar?.shared || this.eventType === EventType.INVITE || this.operation === CalendarOperation.EditThis)
}

/**
Expand All @@ -168,7 +177,14 @@ export class CalendarEventWhoModel {
return [this.selectedCalendar]
} else if (this.isNew && this._attendees.size > 0) {
// if we added guests, we cannot select a shared calendar to create the event.
return calendarArray.filter((calendarInfo) => !calendarInfo.shared)
/**
* when changing the calendar of an event, if the user is the organiser
* they can link any of their owned calendars(private or shared) to said event
* even if the event has guests
**/
return calendarArray.filter((calendarInfo) => calendarInfo.userIsOwner || !calendarInfo.shared)
} else if (this._attendees.size > 0 && this.eventType === EventType.OWN) {
return calendarArray.filter((calendarInfo) => calendarInfo.userIsOwner)
} else if (this._attendees.size > 0 || this.eventType === EventType.INVITE) {
// We don't allow inviting in a shared calendar.
// If we have attendees, we cannot select a shared calendar.
Expand Down
25 changes: 15 additions & 10 deletions src/calendar-app/calendar/model/CalendarModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,24 @@ import {
import { IServiceExecutor } from "../../../common/api/common/ServiceRequest"
import { MembershipService } from "../../../common/api/entities/sys/Services"
import { FileController } from "../../../common/file/FileController"
import { findAttendeeInAddresses } from "../../../common/api/common/utils/CommonCalendarUtils.js"
import { findAttendeeInAddresses, serializeAlarmInterval } from "../../../common/api/common/utils/CommonCalendarUtils.js"
import { TutanotaError } from "@tutao/tutanota-error"
import { SessionKeyNotFoundError } from "../../../common/api/common/error/SessionKeyNotFoundError.js"
import Stream from "mithril/stream"
import { ObservableLazyLoaded } from "../../../common/api/common/utils/ObservableLazyLoaded.js"
import { UserController } from "../../../common/api/main/UserController.js"
import { formatDateWithWeekdayAndTime, formatTime } from "../../../common/misc/Formatter.js"
import { EntityUpdateData, isUpdateFor, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
import { AlarmInterval, parseAlarmInterval, serializeAlarmInterval } from "../../../common/calendar/date/CalendarUtils.js"
import { AlarmInterval } from "../../../common/calendar/date/CalendarUtils.js"
import { isSharedGroupOwner, loadGroupMembers } from "../../../common/sharing/GroupUtils.js"

const TAG = "[CalendarModel]"
export type CalendarInfo = {
groupRoot: CalendarGroupRoot
groupInfo: GroupInfo
group: Group
shared: boolean
userIsOwner: boolean
}

export class CalendarModel {
Expand Down Expand Up @@ -154,12 +156,11 @@ export class CalendarModel {

/** Load map from group/groupRoot ID to the calendar info */
private async loadCalendarInfos(progressMonitor: IProgressMonitor): Promise<ReadonlyMap<Id, CalendarInfo>> {
const { user, userSettingsGroupRoot } = this.logins.getUserController()
const userController = this.logins.getUserController()

const calendarMemberships = user.memberships.filter((m) => m.groupType === GroupType.Calendar)
const notFoundMemberships: GroupMembership[] = []
const groupInstances: Array<[CalendarGroupRoot, GroupInfo, Group]> = []
for (const membership of calendarMemberships) {
for (const membership of userController.getCalendarMemberships()) {
try {
const result = await Promise.all([
this.entityClient.load(CalendarGroupRootTypeRef, membership.group),
Expand All @@ -179,27 +180,31 @@ export class CalendarModel {

const calendarInfos: Map<Id, CalendarInfo> = new Map()
for (const [groupRoot, groupInfo, group] of groupInstances) {
const groupMembers = await loadGroupMembers(group, this.entityClient)
const shared = groupMembers.length > 1

calendarInfos.set(groupRoot._id, {
groupRoot,
groupInfo,
group: group,
shared: !isSameId(group.user, user._id),
shared,
userIsOwner: !shared || isSharedGroupOwner(group, userController.userId),
})
}

// cleanup inconsistent memberships
for (const mship of notFoundMemberships) {
for (const membership of notFoundMemberships) {
// noinspection ES6MissingAwait
this.serviceExecutor.delete(MembershipService, createMembershipRemoveData({ user: user._id, group: mship.group }))
this.serviceExecutor.delete(MembershipService, createMembershipRemoveData({ user: userController.userId, group: membership.group }))
}
return calendarInfos
}

private async loadOrCreateCalendarInfo(progressMonitor: IProgressMonitor): Promise<ReadonlyMap<Id, CalendarInfo>> {
const { findPrivateCalendar } = await import("../../../common/calendar/date/CalendarUtils.js")
const { findFirstPrivateCalendar } = await import("../../../common/calendar/date/CalendarUtils.js")
const calendarInfos = await this.loadCalendarInfos(progressMonitor)

if (!this.logins.isInternalUserLoggedIn() || findPrivateCalendar(calendarInfos)) {
if (!this.logins.isInternalUserLoggedIn() || findFirstPrivateCalendar(calendarInfos)) {
return calendarInfos
} else {
await this.createCalendar("", null, [])
Expand Down
4 changes: 2 additions & 2 deletions src/calendar-app/calendar/view/CalendarInvites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { CalendarEvent, CalendarEventAttendee, File as TutanotaFile, Mail,
import { locator } from "../../../common/api/main/CommonLocator.js"
import { CalendarAttendeeStatus, CalendarMethod, ConversationType, FeatureType, getAsEnumValue } from "../../../common/api/common/TutanotaConstants.js"
import { assert, assertNotNull, clone, filterInt, noOp, Require } from "@tutao/tutanota-utils"
import { findPrivateCalendar } from "../../../common/calendar/date/CalendarUtils.js"
import { findFirstPrivateCalendar } from "../../../common/calendar/date/CalendarUtils.js"
import { CalendarNotificationSender } from "./CalendarNotificationSender.js"
import { Dialog } from "../../../common/gui/base/Dialog.js"
import { UserError } from "../../../common/api/main/UserError.js"
Expand Down Expand Up @@ -183,7 +183,7 @@ export class CalendarInviteHandler {
// since there is no write permission. (Same issue can happen with locked, no write permission)
return ReplyResult.ReplySent
}
const calendar = findPrivateCalendar(calendars)
const calendar = findFirstPrivateCalendar(calendars)
if (calendar == null) return ReplyResult.ReplyNotSent
if (decision !== CalendarAttendeeStatus.DECLINED && eventClone.uid != null) {
const dbEvents = await this.calendarModel.getEventsByUid(eventClone.uid)
Expand Down
16 changes: 8 additions & 8 deletions src/calendar-app/calendar/view/CalendarView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
getTimeZone,
getWeekNumber,
parseAlarmInterval,
serializeAlarmInterval,
} from "../../../common/calendar/date/CalendarUtils"
import { ButtonColor } from "../../../common/gui/base/Button.js"
import { CalendarMonthView } from "./CalendarMonthView"
Expand Down Expand Up @@ -63,7 +62,7 @@ import { ButtonSize } from "../../../common/gui/base/ButtonSize.js"
import { DrawerMenuAttrs } from "../../../common/gui/nav/DrawerMenu.js"
import { BaseTopLevelView } from "../../../common/gui/BaseTopLevelView.js"
import { TopLevelAttrs, TopLevelView } from "../../../TopLevelView.js"
import { getEventWithDefaultTimes, getNextHalfHour } from "../../../common/api/common/utils/CommonCalendarUtils.js"
import { getEventWithDefaultTimes, getNextHalfHour, serializeAlarmInterval } from "../../../common/api/common/utils/CommonCalendarUtils.js"
import { BackgroundColumnLayout } from "../../../common/gui/BackgroundColumnLayout.js"
import { theme } from "../../../common/gui/theme.js"
import { CalendarMobileHeader } from "./CalendarMobileHeader.js"
Expand Down Expand Up @@ -638,12 +637,13 @@ export class CalendarView extends BaseTopLevelView implements TopLevelView<Calen

viewModel.setHiddenCalendars(newHiddenCalendars)
}

const calendarInfos = this.viewModel.calendarInfos
return Array.from(calendarInfos.values())
.filter((calendarInfo) => calendarInfo.shared === shared)
.map((calendarInfo) => {
return this.renderCalendarItem(calendarInfo, shared, setHidden)
})
const calendarInfosList = Array.from(calendarInfos.values())
const filtered = calendarInfosList.filter((calendarInfo) => calendarInfo.userIsOwner === !shared)
return filtered.map((calendarInfo) => {
return this.renderCalendarItem(calendarInfo, calendarInfo.shared, setHidden)
})
}

private renderCalendarItem(calendarInfo: CalendarInfo, shared: boolean, setHidden: (viewModel: CalendarViewModel, groupRootId: string) => void) {
Expand Down Expand Up @@ -747,7 +747,7 @@ export class CalendarView extends BaseTopLevelView implements TopLevelView<Calen
},
}
: null,
!sharedCalendar
calendarInfo.userIsOwner
? {
label: "delete_action",
icon: Icons.Trash,
Expand Down
8 changes: 8 additions & 0 deletions src/common/api/common/utils/CommonCalendarUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DAY_IN_MILLIS } from "@tutao/tutanota-utils"
import type { CalendarEvent } from "../../entities/tutanota/TypeRefs.js"
import { EncryptedMailAddress } from "../../entities/tutanota/TypeRefs.js"
import { stringToCustomId } from "./EntityUtils"
import type { AlarmInterval } from "../../../calendar/date/CalendarUtils.js"

export type CalendarEventTimes = Pick<CalendarEvent, "startTime" | "endTime">

Expand Down Expand Up @@ -154,3 +155,10 @@ export function getEventWithDefaultTimes(startDate: Date = getNextHalfHour()): C
endTime: new Date(endDate.setMinutes(endDate.getMinutes() + 30)),
}
}

/**
* Converts runtime representation of an alarm into a db one.
*/
export function serializeAlarmInterval(interval: AlarmInterval): string {
return `${interval.value}${interval.unit}`
}
12 changes: 2 additions & 10 deletions src/common/calendar/date/CalendarUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
downcast,
filterInt,
findAllAndRemove,
findAndRemove,
getFirstOrThrow,
getFromMap,
getStartOfDay,
Expand Down Expand Up @@ -799,9 +798,9 @@ export function incrementSequence(sequence: string): string {
return String(current + 1)
}

export function findPrivateCalendar(calendarInfo: ReadonlyMap<Id, CalendarInfo>): CalendarInfo | null {
export function findFirstPrivateCalendar(calendarInfo: ReadonlyMap<Id, CalendarInfo>): CalendarInfo | null {
for (const calendar of calendarInfo.values()) {
if (!calendar.shared) {
if (calendar.userIsOwner) {
return calendar
}
}
Expand Down Expand Up @@ -938,13 +937,6 @@ export type AlarmInterval = Readonly<{
value: number
}>

/**
* Converts runtime representation of an alarm into a db one.
*/
export function serializeAlarmInterval(interval: AlarmInterval): string {
return `${interval.value}${interval.unit}`
}

export function alarmIntervalToLuxonDurationLikeObject(alarmInterval: AlarmInterval): DurationLikeObject {
switch (alarmInterval.unit) {
case AlarmIntervalUnit.MINUTE:
Expand Down
2 changes: 1 addition & 1 deletion src/common/sharing/view/GroupInvitationFolderRow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import m, { Children, Component, Vnode } from "mithril"
import { size } from "../../gui/size"
import { getCapabilityText } from "../GroupUtils"
import { downcast } from "@tutao/tutanota-utils"
import { showGroupInvitationDialog } from "../../gui/ReceivedGroupInvitationDialog.js"
import { showGroupInvitationDialog } from "./ReceivedGroupInvitationDialog.js"
import { Icons } from "../../gui/base/icons/Icons"
import type { ReceivedGroupInvitation } from "../../api/entities/sys/TypeRefs.js"
import type { AllIcons } from "../../gui/base/Icon"
Expand Down
Loading

0 comments on commit 4f48ea8

Please sign in to comment.