Skip to content

Commit

Permalink
Show which calendar the event belongs to at event preview
Browse files Browse the repository at this point in the history
Add calendar info, color, name, and type, at event popup and event
details (search and agenda view)
Changes icon alignment to always align to the top, except when the
data displayed fits in a single line, in this case it's centered.
  • Loading branch information
andrehgdias authored and mup committed Feb 6, 2025
1 parent dbbf01a commit ae24e11
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export class CalendarEventPopup implements ModalComponent {
// use the version of the event the popup was opened with, which means the next
// click uses an outdated version.
participation: this.model.getParticipationSetterAndThen(() => this.close()),
calendarEventPreviewModel: this.model,
} satisfies EventPreviewViewAttrs),
]),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,9 @@ export class CalendarEventPreviewViewModel {
getSanitizedDescription() {
return this.sanitizedDescription
}

getCalendarRenderInfo() {
if (!this.calendarEvent._ownerGroup) return null
return this.calendarModel.getCalendarRenderInfo(this.calendarEvent._ownerGroup)
}
}
53 changes: 41 additions & 12 deletions src/calendar-app/calendar/gui/eventpopup/EventPreviewView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AllIcons, Icon, IconSize } from "../../../../common/gui/base/Icon.js"
import { theme } from "../../../../common/gui/theme.js"
import { BootIcons } from "../../../../common/gui/base/icons/BootIcons.js"
import { Icons } from "../../../../common/gui/base/icons/Icons.js"
import { getRepeatEndTimeForDisplay, getTimeZone } from "../../../../common/calendar/date/CalendarUtils.js"
import { getRepeatEndTimeForDisplay, getTimeZone, RENDER_TYPE_TRANSLATION_MAP, RenderType } from "../../../../common/calendar/date/CalendarUtils.js"
import { CalendarAttendeeStatus, EndType, getAttendeeStatus, RepeatPeriod } from "../../../../common/api/common/TutanotaConstants.js"
import { downcast, memoized } from "@tutao/tutanota-utils"
import { lang, TranslationKey } from "../../../../common/misc/LanguageViewModel.js"
Expand All @@ -21,8 +21,10 @@ import { ExternalLink } from "../../../../common/gui/base/ExternalLink.js"

import { createRepeatRuleFrequencyValues, formatEventDuration, getDisplayEventTitle, iconForAttendeeStatus } from "../CalendarGuiUtils.js"
import { hasError } from "../../../../common/api/common/utils/ErrorUtils.js"
import { px, size } from "../../../../common/gui/size.js"

export type EventPreviewViewAttrs = {
calendarEventPreviewModel: CalendarEventPreviewViewModel
event: Omit<CalendarEvent, "description">
sanitizedDescription: string | null
participation?: ReturnType<typeof CalendarEventPreviewViewModel.prototype.getParticipationSetterAndThen>
Expand Down Expand Up @@ -82,28 +84,32 @@ export class EventPreviewView implements Component<EventPreviewViewAttrs> {
}

view(vnode: Vnode<EventPreviewViewAttrs>): Children {
const { event, sanitizedDescription, participation } = vnode.attrs
const { event, sanitizedDescription, participation, calendarEventPreviewModel } = vnode.attrs
const attendees = prepareAttendees(event.attendees, event.organizer)
const eventTitle = getDisplayEventTitle(event.summary)

const renderInfo = calendarEventPreviewModel.getCalendarRenderInfo()

return m(".flex.col.smaller.scroll.visible-scrollbar", [
this.renderRow(BootIcons.Calendar, [m("span.h3", eventTitle)]),
this.renderRow(Icons.Time, [formatEventDuration(event, getTimeZone(), false), this.renderRepeatRule(event.repeatRule, isAllDayEvent(event))]),
this.renderRow(BootIcons.Calendar, [m("span.h3", eventTitle)], true, true),
this.renderCalendar(renderInfo?.name, renderInfo?.color, RENDER_TYPE_TRANSLATION_MAP.get(renderInfo?.renderType ?? RenderType.Private)),
this.renderRow(
Icons.Time,
[formatEventDuration(event, getTimeZone(), false), m("small.text-fade", this.renderRepeatRule(event.repeatRule, isAllDayEvent(event)))],
true,
),
this.renderLocation(event.location),
this.renderAttendeesSection(attendees, participation),
this.renderAttendanceSection(event, attendees, participation),
this.renderDescription(sanitizedDescription),
])
}

private renderRow(headerIcon: AllIcons, children: Children, isAlignedLeft?: boolean): Children {
return m(
".flex.pb-s",
{
class: isAlignedLeft ? "items-start" : "items-center",
},
[this.renderSectionIndicator(headerIcon, isAlignedLeft ? { marginTop: "2px" } : undefined), m(".selectable.text-break.full-width", children)],
)
private renderRow(headerIcon: AllIcons, children: Children, isAlignedLeft: boolean = false, isEventTitle: boolean = false): Children {
return m(".flex.pb-s", [
this.renderSectionIndicator(headerIcon, isAlignedLeft ? { marginTop: isEventTitle ? "6px" : "2px" } : undefined),
m(".selectable.text-break.full-width.align-self-center", children),
])
}

private renderSectionIndicator(icon: AllIcons, style: Record<string, any> = {}): Children {
Expand Down Expand Up @@ -202,6 +208,29 @@ export class EventPreviewView implements Component<EventPreviewViewAttrs> {
if (sanitizedDescription == null || sanitizedDescription.length === 0) return null
return this.renderRow(Icons.AlignLeft, [m.trust(sanitizedDescription)], true)
}

private renderCalendar(calendarName: string | undefined, calendarColor: string | undefined, calendarRenderType: TranslationKey | undefined) {
return m(".flex.pb-s", [
m(
".flex.items-center.justify-center.mr",
{
style: {
width: "24px",
height: "24px",
},
},
m("", {
style: {
borderRadius: "50%",
width: px(size.hpad_large),
height: px(size.hpad_large),
backgroundColor: calendarColor,
},
}),
),
m(".flex.col", [calendarName, m("small.text-fade", lang.get(calendarRenderType!))]),
])
}
}

/**
Expand Down
26 changes: 24 additions & 2 deletions src/calendar-app/calendar/model/CalendarModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
Require,
symmetricDifference,
} from "@tutao/tutanota-utils"
import { CalendarMethod, EXTERNAL_CALENDAR_SYNC_INTERVAL, FeatureType, OperationType } from "../../../common/api/common/TutanotaConstants"
import { CalendarMethod, defaultCalendarColor, EXTERNAL_CALENDAR_SYNC_INTERVAL, FeatureType, OperationType } from "../../../common/api/common/TutanotaConstants"
import { EventController } from "../../../common/api/main/EventController"
import {
createDateWrapper,
Expand Down Expand Up @@ -76,10 +76,12 @@ import {
assignEventId,
CalendarEventValidity,
checkEventValidity,
getCalendarRenderType,
getTimeZone,
hasSourceUrl,
RenderType,
} from "../../../common/calendar/date/CalendarUtils.js"
import { isSharedGroupOwner, loadGroupMembers } from "../../../common/sharing/GroupUtils.js"
import { getSharedGroupName, isSharedGroupOwner, loadGroupMembers } from "../../../common/sharing/GroupUtils.js"
import { ExternalCalendarFacade } from "../../../common/native/common/generatedipc/ExternalCalendarFacade.js"
import { DeviceConfig } from "../../../common/misc/DeviceConfig.js"
import { locator } from "../../../common/api/main/CommonLocator.js"
Expand All @@ -98,6 +100,12 @@ export type CalendarInfo = {
isExternal: boolean
}

export type CalendarRenderInfo = {
name: string
color: string
renderType: RenderType
}

export function assertEventValidity(event: CalendarEvent) {
switch (checkEventValidity(event)) {
case CalendarEventValidity.InvalidContainsInvalidDate:
Expand Down Expand Up @@ -170,6 +178,20 @@ export class CalendarModel {
return this.calendarInfos.stream
}

getCalendarRenderInfo(calendarId: Id, existingGroupSettings?: GroupSettings | null): CalendarRenderInfo | null {
const calendarInfo = this.calendarInfos.stream().get(calendarId)
if (!calendarInfo) return null
let groupSettings = existingGroupSettings
if (!groupSettings) {
const { userSettingsGroupRoot } = this.logins.getUserController()
groupSettings = userSettingsGroupRoot.groupSettings.find((gc) => gc.group === calendarInfo.groupInfo.group) ?? undefined
}
const color = "#" + (groupSettings?.color ?? defaultCalendarColor)
const name = getSharedGroupName(calendarInfo.groupInfo, locator.logins.getUserController(), calendarInfo.shared)
const renderType = getCalendarRenderType(calendarInfo)
return { name, color, renderType }
}

async createEvent(event: CalendarEvent, alarmInfos: ReadonlyArray<AlarmInfoTemplate>, zone: string, groupRoot: CalendarGroupRoot): Promise<void> {
await this.doCreate(event, zone, groupRoot, alarmInfos)
}
Expand Down
47 changes: 17 additions & 30 deletions src/calendar-app/calendar/view/CalendarView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ import {
hasSourceUrl,
isBirthdayEvent,
isClientOnlyCalendar,
isExternalRenderType,
isPrivateRenderType,
isSharedRenderType,
parseAlarmInterval,
RenderType,
} from "../../../common/calendar/date/CalendarUtils"
import { ButtonColor } from "../../../common/gui/base/Button.js"
import { CalendarMonthView } from "./CalendarMonthView"
Expand Down Expand Up @@ -111,13 +115,6 @@ export interface CalendarViewAttrs extends TopLevelAttrs {

const CalendarViewTypeByValue = reverse(CalendarViewType)

enum RenderType {
Private,
Shared,
External,
ClientOnly,
}

export class CalendarView extends BaseTopLevelView implements TopLevelView<CalendarViewAttrs> {
private readonly sidebarColumn: ViewColumn
private readonly contentColumn: ViewColumn
Expand Down Expand Up @@ -765,28 +762,18 @@ export class CalendarView extends BaseTopLevelView implements TopLevelView<Calen
const calendarInfos = [...this.viewModel.calendarInfos, ...(includeLocalCalendars ? this.viewModel.clientOnlyCalendars : [])]

const filteredCalendarInfos = calendarInfos.filter(([_, calendarInfo]) => {
const renderTypeToCondition: ReadonlyMap<RenderType, (calendarInfo: CalendarInfo) => boolean> = new Map([
[RenderType.ClientOnly, (calendarInfo: CalendarInfo) => isClientOnlyCalendar(calendarInfo.group._id)],
[RenderType.Private, isPrivateRenderType],
[RenderType.Shared, isSharedRenderType],
[RenderType.External, isExternalRenderType],
])
/**
* Dinamically filter calendarInfoList according to the renderTypes
* Dynamically filters calendarInfoList according to the renderTypes
*/
const conditions: Array<(calendarInfo: CalendarInfo) => boolean> = []
for (const renderType of renderTypes) {
switch (renderType) {
case RenderType.ClientOnly:
conditions.push((calendarInfo: CalendarInfo) => isClientOnlyCalendar(calendarInfo.group._id))
break
case RenderType.Private:
conditions.push(
(calendarInfo: CalendarInfo) =>
calendarInfo.userIsOwner && !calendarInfo.isExternal && !isClientOnlyCalendar(calendarInfo.group._id),
)
break
case RenderType.Shared:
conditions.push((calendarInfo: CalendarInfo) => !calendarInfo.userIsOwner)
break
case RenderType.External:
conditions.push((calendarInfo: CalendarInfo) => calendarInfo.userIsOwner && calendarInfo.isExternal)
break
}
conditions.push(renderTypeToCondition.get(renderType)!)
}
return conditions.reduce((result, condition) => result || condition(calendarInfo), false)
})
Expand All @@ -805,10 +792,9 @@ export class CalendarView extends BaseTopLevelView implements TopLevelView<Calen
const { userSettingsGroupRoot } = locator.logins.getUserController()
const existingGroupSettings = userSettingsGroupRoot.groupSettings.find((gc) => gc.group === calendarInfo.groupInfo.group) ?? null

const groupRootId = calendarInfo.groupRoot._id

let colorValue = "#" + (existingGroupSettings ? existingGroupSettings.color : defaultCalendarColor)
let groupName = getSharedGroupName(calendarInfo.groupInfo, locator.logins.getUserController(), shared)
const renderInfo = this.viewModel.getCalendarModel().getCalendarRenderInfo(calendarInfo.groupInfo.group, existingGroupSettings)
let colorValue = renderInfo?.color
let groupName = renderInfo?.name
if (isClientOnlyCalendar(calendarInfo.group._id)) {
const clientOnlyId = calendarInfo.group._id.match(/#(.*)/)?.[1]!
const clientOnlyCalendarConfig = deviceConfig.getClientOnlyCalendars().get(calendarInfo.group._id)
Expand All @@ -822,6 +808,7 @@ export class CalendarView extends BaseTopLevelView implements TopLevelView<Calen
? lang.get("lastSync_label", { "{date}": `${formatDate(lastSyncDate)} at ${formatTime(lastSyncDate)}` })
: lang.get("iCalNotSync_msg")

const groupRootId = calendarInfo.groupRoot._id
const handleToggleCalendar = () => {
if (!isClientOnlyCalendar(groupRootId) || this.viewModel.isNewPaidPlan) toggleHidden(this.viewModel, groupRootId)
else showPlanUpgradeRequiredDialog(NewPaidPlans)
Expand Down Expand Up @@ -871,7 +858,7 @@ export class CalendarView extends BaseTopLevelView implements TopLevelView<Calen
},
})
: null,
this.createCalendarActionDropdown(calendarInfo, colorValue, existingGroupSettings, userSettingsGroupRoot, shared),
this.createCalendarActionDropdown(calendarInfo, colorValue ?? defaultCalendarColor, existingGroupSettings, userSettingsGroupRoot, shared),
])
}

Expand Down
1 change: 1 addition & 0 deletions src/calendar-app/calendar/view/EventDetailsView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class EventDetailsView implements Component<EventDetailsViewAttrs> {
event: this.model.calendarEvent,
sanitizedDescription: this.model.getSanitizedDescription(),
participation: this.model.getParticipationSetterAndThen(() => null),
calendarEventPreviewModel: this.model,
}),
),
m(".flex.mt-xs", [this.renderSendUpdateButton(), this.renderEditButton(), this.renderDeleteButton()]),
Expand Down
36 changes: 36 additions & 0 deletions src/common/calendar/date/CalendarUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
downcast,
filterInt,
findAllAndRemove,
freezeMap,
getFirstOrThrow,
getFromMap,
getStartOfDay,
Expand Down Expand Up @@ -48,6 +49,7 @@ import { CalendarEventUidIndexEntry } from "../../api/worker/facades/lazy/Calend
import { ParserError } from "../../misc/parsing/ParserCombinator.js"
import { LoginController } from "../../api/main/LoginController.js"
import { BirthdayEventRegistry } from "./CalendarEventsRepository.js"
import type { TranslationKey } from "../../misc/LanguageViewModel.js"

export type CalendarTimeRange = {
start: number
Expand Down Expand Up @@ -984,6 +986,40 @@ export enum CalendarType {
CLIENT_ONLY,
}

export enum RenderType {
Private,
Shared,
External,
ClientOnly,
}

export const RENDER_TYPE_TRANSLATION_MAP: ReadonlyMap<RenderType, TranslationKey> = freezeMap(
new Map([
[RenderType.Private, "yourCalendars_label"],
[RenderType.External, "calendarSubscriptions_label"],
[RenderType.Shared, "calendarShared_label"],
]),
)

export function isPrivateRenderType(calendarInfo: CalendarInfo) {
return calendarInfo.userIsOwner && !calendarInfo.isExternal && !isClientOnlyCalendar(calendarInfo.group._id)
}

export function isSharedRenderType(calendarInfo: CalendarInfo) {
return !calendarInfo.userIsOwner
}

export function isExternalRenderType(calendarInfo: CalendarInfo) {
return calendarInfo.userIsOwner && calendarInfo.isExternal
}

export function getCalendarRenderType(calendarInfo: CalendarInfo): RenderType {
if (isPrivateRenderType(calendarInfo)) return RenderType.Private
if (isSharedRenderType(calendarInfo)) return RenderType.Shared
if (isExternalRenderType(calendarInfo)) return RenderType.External
throw new Error("Unknown calendar Render Type")
}

export function isClientOnlyCalendar(calendarId: Id) {
const clientOnlyId = calendarId.match(/#(.*)/)?.[1]!
return CLIENT_ONLY_CALENDARS.has(clientOnlyId)
Expand Down

0 comments on commit ae24e11

Please sign in to comment.