Initialize calendars in the CalendarModel and implement CalendarInfoBase

- This commit introduces CalendarInfoBase as a base type to hold common fields between CalendarInfo and BirthdayCalendarInfo
- It also makes CalendarModel provide the available calendars.
- SearchViewModel dependency on the locator is removed and CalendarModel is now injected.
This commit is contained in:
and 2025-09-24 13:49:05 +02:00 committed by mup
parent e2804db800
commit 34d4614900
15 changed files with 265 additions and 363 deletions

View file

@ -80,7 +80,6 @@ import { EventsOnDays } from "../view/CalendarViewModel.js"
import { CalendarEventPreviewViewModel } from "./eventpopup/CalendarEventPreviewViewModel.js"
import { createAsyncDropdown } from "../../../common/gui/base/Dropdown.js"
import { UserController } from "../../../common/api/main/UserController.js"
import { ClientOnlyCalendarsInfo } from "../../../common/misc/DeviceConfig.js"
import { SelectOption } from "../../../common/gui/base/Select.js"
import { RadioGroupOption } from "../../../common/gui/base/RadioGroup.js"
import { ColorPickerModel } from "../../../common/gui/base/colorPicker/ColorPickerModel.js"
@ -927,16 +926,6 @@ export const getClientOnlyColors = (userId: Id) => {
return new Map([[calendarId, DEFAULT_BIRTHDAY_CALENDAR_COLOR]])
}
export const getClientOnlyCalendars = (
userId: Id,
): (ClientOnlyCalendarsInfo & {
id: string
name: string
})[] => {
const calendarId = `${userId}#${BIRTHDAY_CALENDAR_BASE_ID}`
return [{ id: calendarId, name: lang.get("birthdayCalendar_label"), color: DEFAULT_BIRTHDAY_CALENDAR_COLOR }]
}
/**
* find out how we ended up with this event, which determines the capabilities we have with it.
* for shared events in calendar where we have read-write access, we can still only view events that have
@ -1108,7 +1097,11 @@ export function renderCalendarColor(selectedCalendar: CalendarInfo | null, group
* // Handle macOS modifier logic
* }
*/
export function extractCalendarEventModifierKey<T extends MouseEvent | KeyboardEvent>(event: T & { redraw?: boolean }): Key | undefined {
export function extractCalendarEventModifierKey<T extends MouseEvent | KeyboardEvent>(
event: T & {
redraw?: boolean
},
): Key | undefined {
let key
if (event.metaKey && isAppleDevice()) {
key = Keys.META

View file

@ -58,7 +58,7 @@ export class CalendarSidebarRow implements Component<CalendarSidebarRowAttrs> {
size: IconSize.Medium,
class: "pr-s",
style: {
fill: theme.content_button,
fill: theme.on_surface_variant,
},
})
: null,

View file

@ -7,18 +7,17 @@ import {
} from "../../../../common/calendar/date/CalendarUtils.js"
import { CalendarEventModel, CalendarOperation, EventSaveResult, EventType, getNonOrganizerAttendees } from "../eventeditor-model/CalendarEventModel.js"
import { NotFoundError } from "../../../../common/api/common/error/RestError.js"
import { CalendarModel } from "../../model/CalendarModel.js"
import { CalendarInfoBase, CalendarModel } from "../../model/CalendarModel.js"
import { ProgrammingError } from "../../../../common/api/common/error/ProgrammingError.js"
import { CalendarAttendeeStatus, EndType } from "../../../../common/api/common/TutanotaConstants.js"
import m from "mithril"
import { clone, deepEqual, incrementDate, Thunk } from "@tutao/tutanota-utils"
import { clone, deepEqual, incrementDate, LazyLoaded, Thunk } from "@tutao/tutanota-utils"
import { CalendarEventUidIndexEntry } from "../../../../common/api/worker/facades/lazy/CalendarFacade.js"
import { EventEditorDialog } from "../eventeditor-view/CalendarEventEditDialog.js"
import { convertTextToHtml } from "../../../../common/misc/Formatter.js"
import { prepareCalendarDescription } from "../../../../common/api/common/utils/CommonCalendarUtils.js"
import { SearchToken } from "../../../../common/api/common/utils/QueryTokenUtils"
import { lang } from "../../../../common/misc/LanguageViewModel.js"
import { CalendarRenderInfo } from "../../view/CalendarViewModel"
/**
* makes decisions about which operations are available from the popup and knows how to implement them depending on the event's type.
@ -50,6 +49,13 @@ export class CalendarEventPreviewViewModel {
*/
comment: string = ""
private readonly calendar: LazyLoaded<CalendarInfoBase | undefined> = new LazyLoaded<CalendarInfoBase | undefined>(async () => {
if (!this.calendarEvent?._ownerGroup) {
return undefined
}
return this.calendarModel.getCalendarInfo(this.calendarEvent._ownerGroup)
})
/**
*
* @param calendarEvent the event to display in the popup
@ -93,6 +99,8 @@ export class CalendarEventPreviewViewModel {
this.isRepeatingForEditing =
(calendarEvent.repeatRule != null || calendarEvent.recurrenceId != null) && (eventType === EventType.OWN || eventType === EventType.SHARED_RW)
this.calendar.getAsync().then(m.redraw)
}
/** for deleting, an event that has only one non-deleted instance behaves as if it wasn't repeating
@ -370,8 +378,8 @@ export class CalendarEventPreviewViewModel {
}
// Returns null if there is no ownerGroup, which might be the case if an event invitation is being viewed
getCalendarRenderInfo(): CalendarRenderInfo | null {
if (!this.calendarEvent._ownerGroup) return null
return this.calendarModel.getCalendarRenderInfo(this.calendarEvent._ownerGroup)
getCalendarInfoBase(): CalendarInfoBase | undefined {
if (!this.calendarEvent._ownerGroup) return undefined
return this.calendar.getLoaded()
}
}

View file

@ -100,7 +100,7 @@ export class EventPreviewView implements Component<EventPreviewViewAttrs> {
const attendees = prepareAttendees(event.attendees, event.organizer)
const eventTitle = getDisplayEventTitle(event.summary)
const renderInfo = calendarEventPreviewModel.getCalendarRenderInfo()
const renderInfo = calendarEventPreviewModel.getCalendarInfoBase()
return m(".flex.col.smaller", [
this.renderRow(

View file

@ -12,6 +12,7 @@ import {
getFromMap,
isNotEmpty,
isSameDay,
LazyLoaded,
Require,
splitInChunks,
symmetricDifference,
@ -19,6 +20,7 @@ import {
import {
BIRTHDAY_CALENDAR_BASE_ID,
CalendarMethod,
DEFAULT_BIRTHDAY_CALENDAR_COLOR,
defaultCalendarColor,
EXTERNAL_CALENDAR_SYNC_INTERVAL,
FeatureType,
@ -98,6 +100,8 @@ import {
getCalendarRenderType,
getTimeZone,
hasSourceUrl,
isBirthdayCalendar,
RenderType,
} from "../../../common/calendar/date/CalendarUtils.js"
import { getSharedGroupName, isSharedGroupOwner, loadGroupMembers } from "../../../common/sharing/GroupUtils.js"
import { ExternalCalendarFacade } from "../../../common/native/common/generatedipc/ExternalCalendarFacade.js"
@ -117,13 +121,19 @@ import { lang } from "../../../common/misc/LanguageViewModel.js"
import { NativePushServiceApp } from "../../../common/native/main/NativePushServiceApp.js"
import { SyncTracker } from "../../../common/api/main/SyncTracker.js"
import { CacheMode } from "../../../common/api/worker/rest/EntityRestClient"
import { CalendarRenderInfo } from "../view/CalendarViewModel"
const TAG = "[CalendarModel]"
const EXTERNAL_CALENDAR_RETRY_LIMIT = 3
const EXTERNAL_CALENDAR_RETRY_DELAY_MS = 1000
export type CalendarInfo = {
export type CalendarInfoBase = {
id: string
name: string
color: string
renderType: RenderType // FIXME should we still use renderType - maybe merge with CalendarType?
}
export type CalendarInfo = CalendarInfoBase & {
groupRoot: CalendarGroupRoot
groupInfo: GroupInfo
group: Group
@ -132,11 +142,18 @@ export type CalendarInfo = {
isExternal: boolean
}
export type BirthdayCalendarInfo = {
id: string
export type BirthdayCalendarInfo = CalendarInfoBase & {
contactGroupId: Id
}
export function isBirthdayCalendarInfo(calendarInfoBase: CalendarInfoBase): calendarInfoBase is BirthdayCalendarInfo {
return calendarInfoBase.renderType === RenderType.ClientOnly
}
export function isCalendarInfo(calendarInfoBase: CalendarInfoBase): calendarInfoBase is CalendarInfo {
return calendarInfoBase.renderType !== RenderType.ClientOnly
}
type ExternalCalendarQueueItem = {
url: string
group: string
@ -187,6 +204,10 @@ export class CalendarModel {
return calendarInfoPromise
}, new Map())
private readonly userHasNewPaidPlan: LazyLoaded<boolean> = new LazyLoaded<boolean>(async () => {
return await this.logins.getUserController().isNewPaidPlan()
})
/**
* Stores the queued calendars to be synchronized
*/
@ -224,11 +245,15 @@ export class CalendarModel {
}
})
this.birthdayCalendarInfo = this.createBirthdayCalendarInfo()
this.userHasNewPaidPlan.getAsync().then(m.redraw)
}
private createBirthdayCalendarInfo(): BirthdayCalendarInfo {
return {
id: `${this.logins.getUserController().userId}#${BIRTHDAY_CALENDAR_BASE_ID}`,
name: lang.get("birthdayCalendar_label"),
color: DEFAULT_BIRTHDAY_CALENDAR_COLOR, // FIXME
renderType: RenderType.ClientOnly,
contactGroupId: getFirstOrThrow(this.logins.getUserController().getContactGroupMemberships()).group,
}
}
@ -249,18 +274,26 @@ export class CalendarModel {
return this.calendarInfos.stream
}
getCalendarRenderInfo(calendarId: Id, existingGroupSettings?: GroupSettings | null): CalendarRenderInfo {
const calendarInfo = this.calendarInfos.stream().get(calendarId)
if (!calendarInfo) throw new Error("Calendar infos not loaded")
let groupSettings = existingGroupSettings
if (!groupSettings) {
const { userSettingsGroupRoot } = this.logins.getUserController()
groupSettings = userSettingsGroupRoot.groupSettings.find((gc) => gc.group === calendarInfo.groupInfo.group) ?? undefined
getAvailableCalendars(includesBirthday: boolean = false): Array<CalendarInfoBase> {
if (this.userHasNewPaidPlan.isLoaded() && this.calendarInfos.isLoaded()) {
// Load user's calendar list
const calendarInfos: Array<CalendarInfoBase> = Array.from(this.calendarInfos.getLoaded().values())
if (includesBirthday && this.userHasNewPaidPlan.getLoaded()) {
const birthdayCalendarInfo = this.getBirthdayCalendarInfo()
calendarInfos.push(birthdayCalendarInfo)
}
return calendarInfos
} else {
return []
}
const color = "#" + (groupSettings?.color ?? defaultCalendarColor)
const name = getSharedGroupName(calendarInfo.groupInfo, locator.logins.getUserController().userSettingsGroupRoot, calendarInfo.shared)
const renderType = getCalendarRenderType(calendarInfo)
return { name, color, renderType }
}
async getCalendarInfo(calendarId: Id): Promise<CalendarInfoBase | undefined> {
if (isBirthdayCalendar(calendarId)) {
return this.birthdayCalendarInfo
}
const calendars = await this.getCalendarInfos()
return calendars.get(calendarId)
}
async createEvent(event: CalendarEvent, alarmInfos: ReadonlyArray<AlarmInfoTemplate>, zone: string, groupRoot: CalendarGroupRoot): Promise<void> {
@ -328,14 +361,28 @@ export class CalendarModel {
}
const calendarInfos: Map<Id, CalendarInfo> = new Map()
const groupSettings = userController.userSettingsGroupRoot.groupSettings
const groupSettingsList = userController.userSettingsGroupRoot.groupSettings
for (const [groupRoot, groupInfo, group] of groupInstances) {
try {
const groupMembers = await loadGroupMembers(group, this.entityClient)
const shared = groupMembers.length > 1
const userIsOwner = !shared || isSharedGroupOwner(group, userController.userId)
const isExternal = hasSourceUrl(groupSettings.find((groupSettings) => groupSettings.group === group._id))
const groupSettings = groupSettingsList.find((groupSettings) => groupSettings.group === group._id)
const isExternal = hasSourceUrl(groupSettings)
const calendarId = groupRoot._id
const color = groupSettings?.color ?? defaultCalendarColor
const sharedGroupName = getSharedGroupName(groupInfo, userController.userSettingsGroupRoot, shared)
const calendarType = getCalendarRenderType({
calendarId: calendarId,
isExternalCalendar: isExternal,
isUserOwner: userIsOwner,
})
calendarInfos.set(groupRoot._id, {
id: groupRoot._id,
name: sharedGroupName,
color: color,
renderType: calendarType,
groupRoot,
groupInfo,
group: group,
@ -1233,6 +1280,7 @@ export class CalendarModel {
// and user might have subscribed to a new calendar, so we must reload
// calendar infos to make sure that the calendar has been put in the correct section
this.calendarInfos.reload()
this.userHasNewPaidPlan.reload()
}
}

View file

@ -58,6 +58,8 @@ import { formatDate } from "../../../../common/misc/Formatter"
import { createDropdown } from "../../../../common/gui/base/Dropdown"
import { ProgrammingError } from "../../../../common/api/common/error/ProgrammingError"
import { showDateRangeSelectionDialog } from "../../gui/pickers/DatePickerDialog"
import { isSameId } from "../../../../common/api/common/utils/EntityUtils"
import { CalendarInfo } from "../../model/CalendarModel"
assertMainOrNode()
@ -77,8 +79,9 @@ export class CalendarSearchView extends BaseTopLevelView implements TopLevelView
private getSanitizedPreviewData: (event: CalendarEvent) => LazyLoaded<CalendarEventPreviewViewModel> = memoized((event: CalendarEvent) =>
new LazyLoaded(async () => {
const calendars = await this.searchViewModel.getLazyCalendarInfos().getAsync()
const eventPreviewModel = await calendarLocator.calendarEventPreviewModel(event, calendars, [])
const calendars = await this.searchViewModel.getAvailableCalendars(false)
const calendarInfosMap = new Map(calendars.map((calendarInfo) => [calendarInfo.id, calendarInfo as CalendarInfo]))
const eventPreviewModel = await calendarLocator.calendarEventPreviewModel(event, calendarInfosMap, [])
eventPreviewModel.sanitizeDescription().then(() => m.redraw())
return eventPreviewModel
}).load(),
@ -431,11 +434,9 @@ export class CalendarSearchView extends BaseTopLevelView implements TopLevelView
const dateToUse = this.searchViewModel.startDate ? setNextHalfHour(new Date(this.searchViewModel.startDate)) : setNextHalfHour(new Date())
// Disallow creation of events when there is no existing calendar
const lazyCalendarInfo = this.searchViewModel.getLazyCalendarInfos()
const calendarInfos = lazyCalendarInfo.isLoaded() ? lazyCalendarInfo.getSync() : lazyCalendarInfo.getAsync()
if (calendarInfos instanceof Promise) {
await showProgressDialog("pleaseWait_msg", calendarInfos)
const calendarInfos = this.searchViewModel.getAvailableCalendars(false)
if (!calendarInfos.length) {
await showProgressDialog("pleaseWait_msg", this.searchViewModel.loadCalendarInfos())
}
const mailboxDetails = await calendarLocator.mailboxModel.getUserMailboxDetails()
@ -515,7 +516,7 @@ export class CalendarSearchView extends BaseTopLevelView implements TopLevelView
}
private renderCalendarFilterChips() {
const availableCalendars = this.searchViewModel.getAvailableCalendars()
const availableCalendars = this.searchViewModel.getAvailableCalendars(true)
const selectedCalendar = this.searchViewModel.selectedCalendar
return [
m(FilterChip, {
@ -533,7 +534,10 @@ export class CalendarSearchView extends BaseTopLevelView implements TopLevelView
}),
m(FilterChip, {
label: selectedCalendar
? lang.makeTranslation("calendar_label", availableCalendars.find((f) => f.info === this.searchViewModel.selectedCalendar)?.name ?? "")
? lang.makeTranslation(
"calendar_label",
availableCalendars.find((calendarInfo) => isSameId(calendarInfo.id, selectedCalendar.id))?.name ?? "",
)
: lang.getTranslation("calendar_label"),
selected: selectedCalendar != null,
chevron: true,
@ -543,9 +547,9 @@ export class CalendarSearchView extends BaseTopLevelView implements TopLevelView
label: lang.getTranslation("all_label"),
click: () => this.searchViewModel.selectCalendar(null),
},
...availableCalendars.map((f) => ({
label: lang.makeTranslation(f.name, f.name),
click: () => this.searchViewModel.selectCalendar(f.info),
...availableCalendars.map((calendarInfo) => ({
label: lang.makeTranslation(calendarInfo.name, calendarInfo.name),
click: () => this.searchViewModel.selectCalendar(calendarInfo),
})),
],
}),

View file

@ -12,7 +12,6 @@ import {
incrementMonth,
isSameDayOfDate,
isSameTypeRef,
LazyLoaded,
lazyMemoized,
neverNull,
ofClass,
@ -25,24 +24,19 @@ import { NotFoundError } from "../../../../common/api/common/error/RestError.js"
import { createRestriction, decodeCalendarSearchKey, encodeCalendarSearchKey, getRestriction } from "../model/SearchUtils.js"
import Stream from "mithril/stream"
import stream from "mithril/stream"
import { generateCalendarInstancesInRange, retrieveBirthdayEventsForUser } from "../../../../common/calendar/date/CalendarUtils.js"
import { generateCalendarInstancesInRange, isBirthdayCalendar, retrieveBirthdayEventsForUser } from "../../../../common/calendar/date/CalendarUtils.js"
import { LoginController } from "../../../../common/api/main/LoginController.js"
import { EntityClient } from "../../../../common/api/common/EntityClient.js"
import { EntityUpdateData, isUpdateForTypeRef } from "../../../../common/api/common/utils/EntityUpdateUtils.js"
import { CalendarInfo } from "../../model/CalendarModel.js"
import m from "mithril"
import { CalendarInfoBase, CalendarModel, isBirthdayCalendarInfo, isCalendarInfo } from "../../model/CalendarModel.js"
import { CalendarFacade } from "../../../../common/api/worker/facades/lazy/CalendarFacade.js"
import { ProgressTracker } from "../../../../common/api/main/ProgressTracker.js"
import { ListAutoSelectBehavior } from "../../../../common/misc/DeviceConfig.js"
import { ProgrammingError } from "../../../../common/api/common/error/ProgrammingError.js"
import { SearchRouter } from "../../../../common/search/view/SearchRouter.js"
import { locator } from "../../../../common/api/main/CommonLocator.js"
import { CalendarEventsRepository } from "../../../../common/calendar/date/CalendarEventsRepository"
import { getClientOnlyCalendars } from "../../gui/CalendarGuiUtils"
import { ListElementListModel } from "../../../../common/misc/ListElementListModel"
import { getStartOfTheWeekOffsetForUser } from "../../../../common/misc/weekOffset"
import { getSharedGroupName } from "../../../../common/sharing/GroupUtils"
import { BIRTHDAY_CALENDAR_BASE_ID } from "../../../../common/api/common/TutanotaConstants"
const SEARCH_PAGE_SIZE = 100
@ -93,20 +87,20 @@ export class CalendarSearchViewModel {
}
// isn't an IdTuple because it is two list ids
private _selectedCalendar: readonly [Id, Id] | string | null = null
get selectedCalendar(): CalendarInfo | string | null {
const calendars = this.getAvailableCalendars()
return (
calendars.find((calendar) => {
if (typeof calendar.info === "string") {
return calendar.info === this._selectedCalendar
private _selectedCalendar: readonly [Id, Id] | Id | null = null // [longListId, shorListId] || birthDay_calendar_id | null
get selectedCalendar(): CalendarInfoBase | null {
const calendars = this.getAvailableCalendars(true)
const selectedCalendar =
calendars.find((calendarInfo) => {
if (isBirthdayCalendarInfo(calendarInfo)) {
return calendarInfo.id === this._selectedCalendar
}
// It isn't a string, so it can be only a Calendar Info
const calendarValue = calendar.info
return isSameId([calendarValue.groupRoot.longEvents, calendarValue.groupRoot.shortEvents], this._selectedCalendar)
})?.info ?? null
)
if (isCalendarInfo(calendarInfo)) {
const groupRoot = calendarInfo.groupRoot
return isSameId([groupRoot.longEvents, groupRoot.shortEvents], this._selectedCalendar)
}
}) ?? null
return selectedCalendar
}
// Contains load more results even when searchModel doesn't.
@ -117,22 +111,12 @@ export class CalendarSearchViewModel {
private listStateSubscription: Stream<unknown> | null = null
loadingAllForSearchResult: SearchResult | null = null
private readonly lazyCalendarInfos: LazyLoaded<ReadonlyMap<string, CalendarInfo>> = new LazyLoaded(async () => {
const calendarModel = await locator.calendarModel()
const calendarInfos = await calendarModel.getCalendarInfos()
m.redraw()
return calendarInfos
})
private readonly userHasNewPaidPlan: LazyLoaded<boolean> = new LazyLoaded<boolean>(async () => {
return await this.logins.getUserController().isNewPaidPlan()
})
currentQuery: string = ""
constructor(
readonly router: SearchRouter,
private readonly search: CalendarSearchModel,
private readonly calendarModel: CalendarModel,
private readonly logins: LoginController,
private readonly entityClient: EntityClient,
private readonly eventController: EventController,
@ -145,40 +129,6 @@ export class CalendarSearchViewModel {
this._listModel = this.createList()
}
getLazyCalendarInfos() {
return this.lazyCalendarInfos
}
getAvailableCalendars(): Array<{ info: CalendarInfo | string; name: string }> {
if (this.getLazyCalendarInfos().isLoaded() && this.getUserHasNewPaidPlan().isLoaded()) {
// Load user's calendar list
const items: {
info: CalendarInfo | string
name: string
}[] = Array.from(this.getLazyCalendarInfos().getLoaded().values()).map((ci) => ({
info: ci,
name: getSharedGroupName(ci.groupInfo, locator.logins.getUserController().userSettingsGroupRoot, true),
}))
if (this.getUserHasNewPaidPlan().getSync()) {
const localCalendars = this.getLocalCalendars().map((cal) => ({
info: cal.id,
name: cal.name,
}))
items.push(...localCalendars)
}
return items
} else {
return []
}
}
getUserHasNewPaidPlan() {
return this.userHasNewPaidPlan
}
readonly init = lazyMemoized(() => {
this.resultSubscription = this.search.result.map((result) => {
if (this.searchResult == null || result == null || !areResultsForTheSameQuery(result, this.searchResult)) {
@ -257,28 +207,22 @@ export class CalendarSearchViewModel {
this._startDate = restriction.start ? new Date(restriction.start) : null
this._endDate = restriction.end ? new Date(restriction.end) : null
// Check if user is trying to search in a client only calendar while using a free account
const selectedCalendar = this.extractCalendarListIds(restriction.folderIds)
if (!selectedCalendar || Array.isArray(selectedCalendar)) {
this._selectedCalendar = selectedCalendar
} else if (selectedCalendar.toString().includes(BIRTHDAY_CALENDAR_BASE_ID)) {
this.getUserHasNewPaidPlan()
.getAsync()
.then((isNewPaidPlan) => {
if (!isNewPaidPlan) {
return (this._selectedCalendar = null)
}
this._selectedCalendar = selectedCalendar
})
}
this._includeRepeatingEvents = restriction.eventSeries ?? true
this.lazyCalendarInfos.load()
this.userHasNewPaidPlan.load()
this.latestCalendarRestriction = restriction
// Check if user is trying to search in a birthday calendar while using a free account
const listIdsOrBirthdayCalendarId = this.extractCalendarListIds(restriction.folderIds)
if (!listIdsOrBirthdayCalendarId || Array.isArray(listIdsOrBirthdayCalendarId)) {
this._selectedCalendar = listIdsOrBirthdayCalendarId
} else if (isBirthdayCalendar(listIdsOrBirthdayCalendarId.toString())) {
const availableCalendars = this.getAvailableCalendars(true)
if (availableCalendars.some(isBirthdayCalendarInfo)) {
this._selectedCalendar = listIdsOrBirthdayCalendarId
}
this._selectedCalendar = null
return
}
if (args.id != null) {
try {
const { start, id } = decodeCalendarSearchKey(args.id)
@ -378,10 +322,12 @@ export class CalendarSearchViewModel {
return null
}
selectCalendar(calendarInfo: CalendarInfo | string | null) {
if (typeof calendarInfo === "string" || calendarInfo == null) {
this._selectedCalendar = calendarInfo
} else {
selectCalendar(calendarInfo: CalendarInfoBase | null) {
if (!calendarInfo) {
this._selectedCalendar = null
} else if (isBirthdayCalendarInfo(calendarInfo)) {
this._selectedCalendar = calendarInfo.id
} else if (isCalendarInfo(calendarInfo)) {
this._selectedCalendar = [calendarInfo.groupRoot.longEvents, calendarInfo.groupRoot.shortEvents]
}
this.searchAgain()
@ -443,13 +389,14 @@ export class CalendarSearchViewModel {
}
private getCalendarLists(): string[] {
if (typeof this.selectedCalendar === "string") {
return [this.selectedCalendar]
} else if (this.selectedCalendar != null) {
const calendarInfo = this.selectedCalendar
return [calendarInfo.groupRoot.longEvents, calendarInfo.groupRoot.shortEvents]
const selectedCalendar = this.selectedCalendar
if (!selectedCalendar) {
return []
} else if (isBirthdayCalendarInfo(selectedCalendar)) {
return [this.selectedCalendar.id]
} else if (isCalendarInfo(selectedCalendar)) {
return [selectedCalendar.groupRoot.longEvents, selectedCalendar.groupRoot.shortEvents]
}
return []
}
@ -649,12 +596,16 @@ export class CalendarSearchViewModel {
return generateCalendarInstancesInRange(eventList, { start, end })
}
sendStopLoadingSignal() {
this.search.sendCancelSignal()
getAvailableCalendars(includesBirthday: boolean): Array<CalendarInfoBase> {
return this.calendarModel.getAvailableCalendars(includesBirthday)
}
getLocalCalendars() {
return getClientOnlyCalendars(this.logins.getUserController().userId)
loadCalendarInfos() {
return this.calendarModel.getCalendarInfos()
}
sendStopLoadingSignal() {
this.search.sendCancelSignal()
}
dispose() {

View file

@ -46,7 +46,6 @@ import {
hasSourceUrl,
isBirthdayCalendar,
isBirthdayEvent,
isCalendarInfoOfRenderType,
parseAlarmInterval,
RenderType,
} from "../../../common/calendar/date/CalendarUtils"
@ -262,7 +261,6 @@ export class CalendarView extends BaseTopLevelView implements TopLevelView<Calen
this.contentColumn = new ViewColumn(
{
view: () => {
this.viewModel.loadCalendarColors()
switch (this.currentViewType) {
case CalendarViewType.MONTH:
return m(BackgroundColumnLayout, {
@ -995,11 +993,11 @@ export class CalendarView extends BaseTopLevelView implements TopLevelView<Calen
private renderCalendar(renderType: RenderType): Children {
const calendarInfos = Array.from(this.viewModel.calendarInfos.entries())
const filteredCalendarInfos = calendarInfos.filter(([_, calendarInfo]) => {
return isCalendarInfoOfRenderType(calendarInfo, renderType)
return calendarInfo.renderType === renderType
})
return filteredCalendarInfos.map(([calendarId, calendarInfo]) => {
const { name, color, renderType } = this.viewModel.getCalendarRenderInfo(calendarInfo)
const { name, color, renderType } = calendarInfo
const rightIconData = this.viewModel.getIcon(renderType, calendarId)
return m(CalendarSidebarRow, {
id: calendarId,
@ -1018,7 +1016,7 @@ export class CalendarView extends BaseTopLevelView implements TopLevelView<Calen
return m(CalendarSidebarRow, {
id: id,
name: lang.get("birthdayCalendar_label"),
color: `#${DEFAULT_BIRTHDAY_CALENDAR_COLOR}`, // FIXME get new persisted color
color: DEFAULT_BIRTHDAY_CALENDAR_COLOR, // FIXME get new persisted color
isHidden: this.viewModel.hiddenCalendars.has(id),
toggleHiddenCalendar: this.viewModel.toggleHiddenCalendar,
} satisfies CalendarSidebarRowAttrs)
@ -1182,7 +1180,7 @@ export class CalendarView extends BaseTopLevelView implements TopLevelView<Calen
const clientOnlyCalendar = isBirthdayCalendar(groupInfo.group)
if (clientOnlyCalendar) {
this.viewModel.handleClientOnlyUpdate(groupInfo, { name: properties.nameData.name, color: properties.color })
this.viewModel.handleClientOnlyUpdate(groupInfo, properties.color)
dialog.close()
return this.viewModel.redraw(undefined)
} else {

View file

@ -14,7 +14,6 @@ import {
} from "@tutao/tutanota-utils"
import { CalendarEvent, CalendarEventTypeRef, Contact, ContactTypeRef, GroupSettings } from "../../../common/api/entities/tutanota/TypeRefs.js"
import {
defaultCalendarColor,
EndType,
EXTERNAL_CALENDAR_SYNC_INTERVAL,
getWeekStart,
@ -34,7 +33,6 @@ import {
addDaysForRecurringEvent,
CalendarTimeRange,
extractContactIdFromEvent,
getCalendarRenderType,
getDiffIn60mIntervals,
getMonthRange,
getStartOfDayWithZone,
@ -44,13 +42,13 @@ import {
} from "../../../common/calendar/date/CalendarUtils"
import { isAllDayEvent } from "../../../common/api/common/utils/CommonCalendarUtils"
import { CalendarEventModel, CalendarOperation, EventSaveResult, EventType, getNonOrganizerAttendees } from "../gui/eventeditor-model/CalendarEventModel.js"
import { askIfShouldSendCalendarUpdatesToAttendees, getClientOnlyColors, getEventType, shouldDisplayEvent } from "../gui/CalendarGuiUtils.js"
import { askIfShouldSendCalendarUpdatesToAttendees, getEventType, shouldDisplayEvent } from "../gui/CalendarGuiUtils.js"
import { ReceivedGroupInvitationsModel } from "../../../common/sharing/model/ReceivedGroupInvitationsModel"
import type { CalendarInfo, CalendarModel } from "../model/CalendarModel"
import { EventController } from "../../../common/api/main/EventController"
import { EntityClient } from "../../../common/api/common/EntityClient"
import { ProgressTracker } from "../../../common/api/main/ProgressTracker"
import { ClientOnlyCalendarsInfo, deviceConfig, DeviceConfig } from "../../../common/misc/DeviceConfig"
import { deviceConfig, DeviceConfig } from "../../../common/misc/DeviceConfig"
import type { EventDragHandlerCallbacks } from "./EventDragHandler"
import { ProgrammingError } from "../../../common/api/common/error/ProgrammingError.js"
import { Time } from "../../../common/calendar/date/Time.js"
@ -60,20 +58,17 @@ import { EntityUpdateData, isUpdateFor, isUpdateForTypeRef } from "../../../comm
import { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import { getEnabledMailAddressesWithUser } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { ContactModel } from "../../../common/contactsFunctionality/ContactModel.js"
import type { GroupColors } from "./CalendarView.js"
import { lang } from "../../../common/misc/LanguageViewModel.js"
import { CalendarContactPreviewViewModel } from "../gui/eventpopup/CalendarContactPreviewViewModel.js"
import { Dialog } from "../../../common/gui/base/Dialog.js"
import { SearchToken } from "../../../common/api/common/utils/QueryTokenUtils"
import { getGroupColors } from "../../../common/misc/GroupColors"
import { GroupNameData, GroupSettingsModel } from "../../../common/sharing/model/GroupSettingsModel"
import { EventEditorDialog } from "../gui/eventeditor-view/CalendarEventEditDialog.js"
import { showPlanUpgradeRequiredDialog } from "../../../common/misc/SubscriptionDialogs"
import { formatDate, formatTime } from "../../../common/misc/Formatter"
import { CalendarSidebarIconData } from "../gui/CalendarSidebarRow"
import { Icons } from "../../../common/gui/base/icons/Icons"
import { getSharedGroupName } from "../../../common/sharing/GroupUtils"
import { locator } from "../../../common/api/main/CommonLocator"
import { SyncStatus } from "../../../common/calendar/gui/ImportExportUtils"
export type EventsOnDays = {
days: Array<Date>
@ -102,13 +97,6 @@ export type CalendarEventPreviewModelFactory = (
export type CalendarContactPreviewModelFactory = (event: CalendarEvent, contact: Contact, canEdit: boolean) => Promise<CalendarContactPreviewViewModel>
export type CalendarPreviewModels = CalendarEventPreviewViewModel | CalendarContactPreviewViewModel
export type CalendarRenderInfo = {
// FIXME later use this tupe only to extract the sidebar calendar row
name: string
color: string
renderType: RenderType
}
export class CalendarViewModel implements EventDragHandlerCallbacks {
// Should not be changed directly but only through the URL
readonly selectedDate: Stream<Date> = stream(getStartOfDay(new Date()))
@ -140,7 +128,6 @@ export class CalendarViewModel implements EventDragHandlerCallbacks {
private viewSize: number | null = null
private _isNewPaidPlan: boolean = false
private _calendarColors: GroupColors = new Map()
isCreatingExternalCalendar: boolean = false
private cancelSignal: Stream<boolean> = stream(false)
@ -199,8 +186,6 @@ export class CalendarViewModel implements EventDragHandlerCallbacks {
this.doRedraw()
})
this.loadCalendarColors()
// disable birthday calendars by default if the user is not on a new paid plan.
logins
.getUserController()
@ -246,16 +231,12 @@ export class CalendarViewModel implements EventDragHandlerCallbacks {
this.deviceConfig.setCalendarDaySelectorExpanded(expanded)
}
loadCalendarColors() {
const clientOnlyColors = getClientOnlyColors(this.logins.getUserController().userId)
const groupColors = getGroupColors(this.logins.getUserController().userSettingsGroupRoot)
for (let [calendarId, color] of clientOnlyColors.entries()) {
groupColors.set(calendarId, color)
}
if (!deepEqual(this._calendarColors, groupColors)) {
this._calendarColors = new Map(groupColors)
get calendarColors() {
const calendarColors = new Map()
for (let calendarInfo of this.calendarModel.getAvailableCalendars(true)) {
calendarColors.set(calendarInfo.id, calendarInfo.color)
}
return calendarColors
}
async getCalendarNameData(groupInfo: GroupInfo): Promise<GroupNameData> {
@ -317,10 +298,6 @@ export class CalendarViewModel implements EventDragHandlerCallbacks {
return this.calendarInvitationsModel.invitations
}
get calendarColors(): GroupColors {
return this._calendarColors
}
get calendarInfos(): ReadonlyMap<Id, CalendarInfo> {
return this.calendarModel.getCalendarInfosStream()()
}
@ -778,7 +755,7 @@ export class CalendarViewModel implements EventDragHandlerCallbacks {
return this.calendarModel
}
handleClientOnlyUpdate(groupInfo: GroupInfo, newGroupSettings: ClientOnlyCalendarsInfo) {
handleClientOnlyUpdate(groupInfo: GroupInfo, newBirthdayColor: string) {
console.log("Update to handle new birthday colors at userSettings") // FIXME
}
@ -805,24 +782,14 @@ export class CalendarViewModel implements EventDragHandlerCallbacks {
this.setHiddenCalendars(newHiddenCalendars)
}
getCalendarRenderInfo(calendarInfo: CalendarInfo): CalendarRenderInfo {
const { userSettingsGroupRoot } = this.logins.getUserController()
const groupSettings = userSettingsGroupRoot.groupSettings.find((gc) => gc.group === calendarInfo.groupInfo.group) ?? undefined
const sharedGroupName = getSharedGroupName(calendarInfo.groupInfo, locator.logins.getUserController().userSettingsGroupRoot, calendarInfo.shared)
const color = "#" + (groupSettings?.color ?? defaultCalendarColor)
const calendarType = getCalendarRenderType(calendarInfo)
return {
name: sharedGroupName,
color,
renderType: calendarType,
}
}
getIcon(renderType: RenderType, calendarId: string): CalendarSidebarIconData | undefined {
switch (renderType) {
case RenderType.External: {
const lastSyncEntry = deviceConfig.getLastExternalCalendarSync().get(calendarId)
if (!lastSyncEntry || lastSyncEntry.lastSyncStatus === SyncStatus.Success) {
// lastSyncEntry won't exist in the webClient
return
}
const lastSyncDate = lastSyncEntry?.lastSuccessfulSync ? new Date(lastSyncEntry.lastSuccessfulSync) : null
const lastSyncStr = lastSyncDate
? lang.get("lastSync_label", { "{date}": `${formatDate(lastSyncDate)} at ${formatTime(lastSyncDate)}` })

View file

@ -227,30 +227,12 @@ class CalendarLocator implements CommonLocator {
const redraw = await this.redraw()
const searchRouter = await this.scopedSearchRouter()
const calendarEventsRepository = await this.calendarEventsRepository()
const calendarModel = await this.calendarModel()
return () => {
return new CalendarSearchViewModel(
searchRouter,
this.search,
this.logins,
this.entityClient,
this.eventController,
this.calendarFacade,
this.progressTracker,
calendarEventsRepository,
redraw,
)
}
}
async calendarSearchViewModelFactory(): Promise<() => CalendarSearchViewModel> {
const { CalendarSearchViewModel } = await import("./calendar/search/view/CalendarSearchViewModel.js")
const redraw = await this.redraw()
const searchRouter = await this.scopedSearchRouter()
const calendarEventsRepository = await this.calendarEventsRepository()
return () => {
return new CalendarSearchViewModel(
searchRouter,
this.search,
calendarModel,
this.logins,
this.entityClient,
this.eventController,

View file

@ -1704,35 +1704,29 @@ export const RENDER_TYPE_TRANSLATION_MAP: ReadonlyMap<RenderType, TranslationKey
]),
)
export function isCalendarInfoOfRenderType(calendarInfo: CalendarInfo, renderType: RenderType) {
switch (renderType) {
case RenderType.Private:
return isPrivateRenderType(calendarInfo)
case RenderType.Shared:
return isSharedRenderType(calendarInfo)
case RenderType.External:
return isExternalRenderType(calendarInfo)
default:
return false
}
export function isPrivateRenderType(renderTypeInfo: RenderTypeInfo) {
return renderTypeInfo.isUserOwner && !renderTypeInfo.isExternalCalendar && !isBirthdayCalendar(renderTypeInfo.calendarId)
}
export function isPrivateRenderType(calendarInfo: CalendarInfo) {
return calendarInfo.userIsOwner && !calendarInfo.isExternal && !isBirthdayCalendar(calendarInfo.group._id)
export function isSharedRenderType(renderTypeInfo: RenderTypeInfo) {
return !renderTypeInfo.isUserOwner
}
export function isSharedRenderType(calendarInfo: CalendarInfo) {
return !calendarInfo.userIsOwner
export function isExternalRenderType(renderTypeInfo: RenderTypeInfo) {
return renderTypeInfo.isUserOwner && renderTypeInfo.isExternalCalendar
}
export function isExternalRenderType(calendarInfo: CalendarInfo) {
return calendarInfo.userIsOwner && calendarInfo.isExternal
export type RenderTypeInfo = {
calendarId: string
isExternalCalendar: boolean
isUserOwner: boolean
}
export function getCalendarRenderType(calendarInfo: CalendarInfo): RenderType {
if (isPrivateRenderType(calendarInfo)) return RenderType.Private
if (isSharedRenderType(calendarInfo)) return RenderType.Shared
if (isExternalRenderType(calendarInfo)) return RenderType.External
export function getCalendarRenderType(renderTypeInfo: RenderTypeInfo): RenderType {
if (isBirthdayCalendar(renderTypeInfo.calendarId)) return RenderType.ClientOnly
if (isPrivateRenderType(renderTypeInfo)) return RenderType.Private
if (isSharedRenderType(renderTypeInfo)) return RenderType.Shared
if (isExternalRenderType(renderTypeInfo)) return RenderType.External
throw new Error("Unknown calendar Render Type")
}
@ -1775,16 +1769,22 @@ export function extractYearFromBirthday(birthday: string | null): number | null
return Number.parseInt(dateParts[0])
}
export async function retrieveBirthdayEventsForUser(logins: LoginController, events: IdTuple[], localEvents: Map<number, BirthdayEventRegistry[]>) {
export async function retrieveBirthdayEventsForUser(
logins: LoginController,
searchResultEventIds: IdTuple[],
birthdayEventsByMonth: Map<number, BirthdayEventRegistry[]>,
) {
if (!(await logins.getUserController().isNewPaidPlan())) {
return []
}
const clientOnlyEvents = events.filter(([calendarId, _]) => isBirthdayCalendar(calendarId)).flatMap((event) => event.join("/"))
const birthdayEventsFromSearchResult = searchResultEventIds.filter(([calendarId, _]) => isBirthdayCalendar(calendarId))
const birthdayEventIdsString = birthdayEventsFromSearchResult.flatMap((eventId) => eventId.join("/"))
const retrievedEvents: CalendarEvent[] = []
for (const event of Array.from(localEvents.values()).flat()) {
if (clientOnlyEvents.includes(event.event._id.join("/"))) {
const allBirthdayEvents = Array.from(birthdayEventsByMonth.values()).flat()
for (const event of allBirthdayEvents) {
if (birthdayEventIdsString.includes(event.event._id.join("/"))) {
retrievedEvents.push(event.event)
}
}

View file

@ -12,7 +12,6 @@ import { CalendarViewType } from "../api/common/utils/CommonCalendarUtils.js"
import { SyncStatus } from "../calendar/gui/ImportExportUtils.js"
import Stream from "mithril/stream"
import stream from "mithril/stream"
import type { GroupSettings } from "../api/entities/tutanota/TypeRefs.js"
assertMainOrNodeBoot()
export const defaultThemePreference: ThemePreference = "auto:light|dark"
@ -33,8 +32,6 @@ export type LastExternalCalendarSyncEntry = {
lastSyncStatus: SyncStatus
}
export type ClientOnlyCalendarsInfo = Pick<GroupSettings, "name" | "color">
/**
* Definition of the config object that will be saved to local storage
*/

View file

@ -304,6 +304,7 @@ class MailLocator implements CommonLocator {
const searchRouter = await this.scopedSearchRouter()
const calendarEventsRepository = await this.calendarEventsRepository()
const offlineStorageSettings = await this.offlineStorageSettingsModel()
const calendarModel = await this.calendarModel()
return () => {
return new SearchViewModel(
searchRouter,
@ -319,6 +320,7 @@ class MailLocator implements CommonLocator {
this.progressTracker,
conversationViewModelFactory,
calendarEventsRepository,
calendarModel,
redraw,
deviceConfig.getMailAutoSelectBehavior(),
offlineStorageSettings,

View file

@ -119,6 +119,7 @@ import { showDateRangeSelectionDialog } from "../../../calendar-app/calendar/gui
import { ProgrammingError } from "../../../common/api/common/error/ProgrammingError"
import { UndoModel } from "../../UndoModel"
import { deviceConfig } from "../../../common/misc/DeviceConfig"
import { CalendarInfo } from "../../../calendar-app/calendar/model/CalendarModel"
assertMainOrNode()
@ -142,8 +143,9 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
private getSanitizedPreviewData: (event: CalendarEvent) => LazyLoaded<CalendarEventPreviewViewModel> = memoized((event: CalendarEvent) =>
new LazyLoaded(async () => {
const calendars = await this.searchViewModel.getLazyCalendarInfos().getAsync()
const eventPreviewModel = await locator.calendarEventPreviewModel(event, calendars, this.searchViewModel.getHighlightedStrings())
const calendars = await this.searchViewModel.getAvailableCalendars(false)
const calendarInfosMap = new Map(calendars.map((calendarInfo) => [calendarInfo.id, calendarInfo as CalendarInfo]))
const eventPreviewModel = await locator.calendarEventPreviewModel(event, calendarInfosMap, this.searchViewModel.getHighlightedStrings())
eventPreviewModel.sanitizeDescription().then(() => m.redraw())
return eventPreviewModel
}).load(),
@ -1260,11 +1262,9 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
const dateToUse = this.searchViewModel.startDate ? setNextHalfHour(new Date(this.searchViewModel.startDate)) : setNextHalfHour(new Date())
// Disallow creation of events when there is no existing calendar
const lazyCalendarInfo = this.searchViewModel.getLazyCalendarInfos()
const calendarInfos = lazyCalendarInfo.isLoaded() ? lazyCalendarInfo.getSync() : lazyCalendarInfo.getAsync()
if (calendarInfos instanceof Promise) {
await showProgressDialog("pleaseWait_msg", calendarInfos)
const calendarInfos = this.searchViewModel.getAvailableCalendars(false)
if (!calendarInfos.length) {
await showProgressDialog("pleaseWait_msg", this.searchViewModel.loadCalendarInfos())
}
const mailboxDetails = await locator.mailboxModel.getUserMailboxDetails()
@ -1375,7 +1375,8 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
}
private renderCalendarFilterChips() {
const availableCalendars = this.searchViewModel.getAvailableCalendars()
const availableCalendars = this.searchViewModel.getAvailableCalendars(true)
const selectedCalendar = this.searchViewModel.selectedCalendar
return [
this.renderCategoryChip("calendar_label", BootIcons.Calendar),
m(FilterChip, {
@ -1392,10 +1393,13 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
onClick: (_) => this.onCalendarDateRangeSelect(),
}),
m(FilterChip, {
label: this.searchViewModel.selectedCalendar
? lang.makeTranslation("calendar_label", availableCalendars.find((f) => f.info === this.searchViewModel.selectedCalendar)?.name ?? "")
label: selectedCalendar
? lang.makeTranslation(
"calendar_label",
availableCalendars.find((calendarInfo) => isSameId(calendarInfo.id, selectedCalendar?.id))?.name ?? "",
)
: lang.getTranslation("calendar_label"),
selected: this.searchViewModel.selectedCalendar != null,
selected: selectedCalendar != null,
chevron: true,
onClick: createDropdown({
lazyButtons: () => [
@ -1403,9 +1407,9 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
label: lang.getTranslation("all_label"),
click: () => this.searchViewModel.selectCalendar(null),
},
...availableCalendars.map((f) => ({
label: lang.makeTranslation(f.name, f.name),
click: () => this.searchViewModel.selectCalendar(f.info),
...availableCalendars.map((calendarInfo) => ({
label: lang.makeTranslation(calendarInfo.name, calendarInfo.name),
click: () => this.searchViewModel.selectCalendar(calendarInfo),
})),
],
}),

View file

@ -4,13 +4,7 @@ import { SearchRestriction, SearchResult } from "../../../common/api/worker/sear
import { EntityEventsListener, EventController } from "../../../common/api/main/EventController.js"
import { CalendarEvent, CalendarEventTypeRef, Contact, ContactTypeRef, Mail, MailFolder, MailTypeRef } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { ListElementEntity } from "../../../common/api/common/EntityTypes.js"
import {
BIRTHDAY_CALENDAR_BASE_ID,
FULL_INDEXED_TIMESTAMP,
MailSetKind,
NOTHING_INDEXED_TIMESTAMP,
OperationType,
} from "../../../common/api/common/TutanotaConstants.js"
import { FULL_INDEXED_TIMESTAMP, MailSetKind, NOTHING_INDEXED_TIMESTAMP, OperationType } from "../../../common/api/common/TutanotaConstants.js"
import {
assertIsEntity,
assertIsEntity2,
@ -33,7 +27,6 @@ import {
incrementMonth,
isSameDayOfDate,
isSameTypeRef,
LazyLoaded,
memoizedWithHiddenArgument,
neverNull,
ofClass,
@ -61,18 +54,15 @@ import { EntityClient, loadMultipleFromLists } from "../../../common/api/common/
import { SearchRouter } from "../../../common/search/view/SearchRouter.js"
import { MailOpenedListener } from "../../mail/view/MailViewModel.js"
import { EntityUpdateData, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
import { CalendarInfo } from "../../../calendar-app/calendar/model/CalendarModel.js"
import { locator } from "../../../common/api/main/CommonLocator.js"
import m from "mithril"
import { CalendarInfoBase, CalendarModel, isBirthdayCalendarInfo, isCalendarInfo } from "../../../calendar-app/calendar/model/CalendarModel.js"
import { CalendarFacade } from "../../../common/api/worker/facades/lazy/CalendarFacade.js"
import { ProgrammingError } from "../../../common/api/common/error/ProgrammingError.js"
import { ProgressTracker } from "../../../common/api/main/ProgressTracker.js"
import { ListAutoSelectBehavior } from "../../../common/misc/DeviceConfig.js"
import { generateCalendarInstancesInRange, retrieveBirthdayEventsForUser } from "../../../common/calendar/date/CalendarUtils.js"
import { generateCalendarInstancesInRange, isBirthdayCalendar, retrieveBirthdayEventsForUser } from "../../../common/calendar/date/CalendarUtils.js"
import { mailLocator } from "../../mailLocator.js"
import { getMailFilterForType, MailFilterType } from "../../mail/view/MailViewerUtils.js"
import { CalendarEventsRepository } from "../../../common/calendar/date/CalendarEventsRepository.js"
import { getClientOnlyCalendars } from "../../../calendar-app/calendar/gui/CalendarGuiUtils.js"
import { ListFilter } from "../../../common/misc/ListModel"
import { client } from "../../../common/misc/ClientDetector"
import { OfflineStorageSettingsModel } from "../../../common/offline/OfflineStorageSettingsModel"
@ -82,7 +72,6 @@ import { SearchFacade } from "../../workerUtils/index/SearchFacade"
import { compareMails } from "../../mail/model/MailUtils"
import { isOfflineStorageAvailable } from "../../../common/api/common/Env"
import { SearchToken } from "../../../common/api/common/utils/QueryTokenUtils"
import { getSharedGroupName } from "../../../common/sharing/GroupUtils"
const SEARCH_PAGE_SIZE = 100
@ -159,20 +148,20 @@ export class SearchViewModel {
}
// isn't an IdTuple because it is two list ids
private _selectedCalendar: readonly [Id, Id] | string | null = null
get selectedCalendar(): CalendarInfo | string | null {
const calendars = this.getAvailableCalendars()
return (
calendars.find((calendar) => {
if (typeof calendar.info === "string") {
return calendar.info === this._selectedCalendar
private _selectedCalendar: readonly [Id, Id] | Id | null = null // [longListId, shorListId] || birthDay_calendar_id | null
get selectedCalendar(): CalendarInfoBase | null {
const calendars = this.getAvailableCalendars(true)
const selectedCalendar =
calendars.find((calendarInfo) => {
if (isBirthdayCalendarInfo(calendarInfo)) {
return calendarInfo.id === this._selectedCalendar
}
// It isn't a string, so it can be only a Calendar Info
const calendarValue = calendar.info
return isSameId([calendarValue.groupRoot.longEvents, calendarValue.groupRoot.shortEvents], this._selectedCalendar)
})?.info ?? null
)
if (isCalendarInfo(calendarInfo)) {
const groupRoot = calendarInfo.groupRoot
return isSameId([groupRoot.longEvents, groupRoot.shortEvents], this._selectedCalendar)
}
}) ?? null
return selectedCalendar
}
private _mailboxes: MailboxDetail[] = []
@ -195,16 +184,6 @@ export class SearchViewModel {
private resultSubscription: Stream<void> | null = null
private listStateSubscription: Stream<unknown> | null = null
loadingAllForSearchResult: SearchResult | null = null
private readonly lazyCalendarInfos: LazyLoaded<ReadonlyMap<string, CalendarInfo>> = new LazyLoaded(async () => {
const calendarModel = await locator.calendarModel()
const calendarInfos = await calendarModel.getCalendarInfos()
m.redraw()
return calendarInfos
})
private readonly userHasNewPaidPlan: LazyLoaded<boolean> = new LazyLoaded<boolean>(async () => {
return await this.logins.getUserController().isNewPaidPlan()
})
private currentQuery: string = ""
@ -225,6 +204,7 @@ export class SearchViewModel {
private readonly progressTracker: ProgressTracker,
private readonly conversationViewModelFactory: ConversationViewModelFactory | null,
private readonly eventsRepository: CalendarEventsRepository,
private readonly calendarModel: CalendarModel,
private readonly updateUi: () => unknown,
private readonly selectionBehavior: ListAutoSelectBehavior,
private readonly offlineStorageSettings: OfflineStorageSettingsModel | null,
@ -233,40 +213,6 @@ export class SearchViewModel {
this._listModel = this.createList()
}
getLazyCalendarInfos() {
return this.lazyCalendarInfos
}
getAvailableCalendars(): Array<{ info: CalendarInfo | string; name: string }> {
if (this.getLazyCalendarInfos().isLoaded() && this.getUserHasNewPaidPlan().isLoaded()) {
// Load user's calendar list
const items: {
info: CalendarInfo | string
name: string
}[] = Array.from(this.getLazyCalendarInfos().getLoaded().values()).map((ci) => ({
info: ci,
name: getSharedGroupName(ci.groupInfo, locator.logins.getUserController().userSettingsGroupRoot, true),
}))
if (this.getUserHasNewPaidPlan().getSync()) {
const localCalendars = this.getLocalCalendars().map((cal) => ({
info: cal.id,
name: cal.name,
}))
items.push(...localCalendars)
}
return items
} else {
return []
}
}
getUserHasNewPaidPlan() {
return this.userHasNewPaidPlan
}
async init(extendIndexConfirmationCallback: SearchViewModel["extendIndexConfirmationCallback"]) {
if (this.extendIndexConfirmationCallback) {
return
@ -380,24 +326,19 @@ export class SearchViewModel {
this._startDate = restriction.start ? new Date(restriction.start) : null
this._endDate = restriction.end ? new Date(restriction.end) : null
this._includeRepeatingEvents = restriction.eventSeries ?? true
this.lazyCalendarInfos.load()
this.userHasNewPaidPlan.load()
this.latestCalendarRestriction = restriction
// Check if user is trying to search in a client only calendar while using a free account
const selectedCalendar = this.extractCalendarListIds(restriction.folderIds)
if (!selectedCalendar || Array.isArray(selectedCalendar)) {
this._selectedCalendar = selectedCalendar
} else if (selectedCalendar.toString().includes(BIRTHDAY_CALENDAR_BASE_ID)) {
this.getUserHasNewPaidPlan()
.getAsync()
.then((isNewPaidPlan) => {
if (!isNewPaidPlan) {
return (this._selectedCalendar = null)
}
this._selectedCalendar = selectedCalendar
})
// Check if user is trying to search in a birthday calendar while using a free account
const listIdsOrBirthdayCalendarId = this.extractCalendarListIds(restriction.folderIds)
if (!listIdsOrBirthdayCalendarId || Array.isArray(listIdsOrBirthdayCalendarId)) {
this._selectedCalendar = listIdsOrBirthdayCalendarId
} else if (isBirthdayCalendar(listIdsOrBirthdayCalendarId.toString())) {
const availableCalendars = this.getAvailableCalendars(true)
if (availableCalendars.some(isBirthdayCalendarInfo)) {
this._selectedCalendar = listIdsOrBirthdayCalendarId
}
this._selectedCalendar = null
return
}
if (args.id != null) {
@ -566,10 +507,12 @@ export class SearchViewModel {
return PaidFunctionResult.Success
}
selectCalendar(calendarInfo: CalendarInfo | string | null) {
if (typeof calendarInfo === "string" || calendarInfo == null) {
this._selectedCalendar = calendarInfo
} else {
selectCalendar(calendarInfo: CalendarInfoBase | null) {
if (!calendarInfo) {
this._selectedCalendar = null
} else if (isBirthdayCalendarInfo(calendarInfo)) {
this._selectedCalendar = calendarInfo.id
} else if (isCalendarInfo(calendarInfo)) {
this._selectedCalendar = [calendarInfo.groupRoot.longEvents, calendarInfo.groupRoot.shortEvents]
}
this.searchAgain()
@ -693,13 +636,14 @@ export class SearchViewModel {
}
private getCalendarLists(): string[] {
if (typeof this.selectedCalendar === "string") {
return [this.selectedCalendar]
} else if (this.selectedCalendar != null) {
const calendarInfo = this.selectedCalendar
return [calendarInfo.groupRoot.longEvents, calendarInfo.groupRoot.shortEvents]
const selectedCalendar = this.selectedCalendar
if (!selectedCalendar) {
return []
} else if (isBirthdayCalendarInfo(selectedCalendar)) {
return [this.selectedCalendar.id]
} else if (isCalendarInfo(selectedCalendar)) {
return [selectedCalendar.groupRoot.longEvents, selectedCalendar.groupRoot.shortEvents]
}
return []
}
@ -1074,6 +1018,14 @@ export class SearchViewModel {
return generateCalendarInstancesInRange(eventList, { start, end })
}
getAvailableCalendars(includesBirthday: boolean): Array<CalendarInfoBase> {
return this.calendarModel.getAvailableCalendars(includesBirthday)
}
loadCalendarInfos() {
return this.calendarModel.getCalendarInfos()
}
/**
* take a list of IDs and load them by list, filtering out the ones that could not be loaded.
* updates the passed currentResult.result list to not include the failed IDs anymore
@ -1113,10 +1065,6 @@ export class SearchViewModel {
this.search.sendCancelSignal()
}
getLocalCalendars() {
return getClientOnlyCalendars(this.logins.getUserController().userId)
}
dispose() {
this.stopLoadAll()
this.extendIndexConfirmationCallback = null