feat(core): calendar integration setting (#11882)

close AF-2503
This commit is contained in:
CatsJuice
2025-04-25 02:37:52 +00:00
parent f5ac0aee97
commit 4c5e3a875e
11 changed files with 223 additions and 95 deletions
@@ -7,6 +7,7 @@ import {
onComplete,
onStart,
} from '@toeverything/infra';
import dayjs from 'dayjs';
import ICAL from 'ical.js';
import { EMPTY, mergeMap, switchMap, throttleTime } from 'rxjs';
@@ -14,7 +15,9 @@ import type {
CalendarStore,
CalendarSubscriptionConfig,
} from '../store/calendar';
import type { CalendarEvent, EventsByDateMap } from '../type';
import { parseCalendarUrl } from '../utils/calendar-url-parser';
import { isAllDay } from '../utils/is-all-day';
export class CalendarSubscription extends Entity<{ url: string }> {
constructor(private readonly store: CalendarStore) {
@@ -39,6 +42,65 @@ export class CalendarSubscription extends Entity<{ url: string }> {
return '';
}
});
eventsByDateMap$ = LiveData.computed(get => {
const content = get(this.content$);
const config = get(this.config$);
const map: EventsByDateMap = new Map();
if (!content || !config?.showEvents) return map;
const jCal = ICAL.parse(content);
const vCalendar = new ICAL.Component(jCal);
const vEvents = vCalendar.getAllSubcomponents('vevent');
for (const vEvent of vEvents) {
const event = new ICAL.Event(vEvent);
const calendarEvent: CalendarEvent = {
id: event.uid,
url: this.url,
title: event.summary,
startAt: event.startDate,
endAt: event.endDate,
};
// create index for each day of the event
if (event.startDate && event.endDate) {
const start = dayjs(event.startDate.toJSDate());
const end = dayjs(event.endDate.toJSDate());
let current = start;
while (current.isBefore(end) || current.isSame(end, 'day')) {
if (
current.isSame(end, 'day') &&
end.hour() === 0 &&
end.minute() === 0
) {
break;
}
const todayEvent: CalendarEvent = { ...calendarEvent };
const dateKey = current.format('YYYY-MM-DD');
if (!map.has(dateKey)) {
map.set(dateKey, []);
}
todayEvent.allDay = isAllDay(current, start, end);
todayEvent.date = current;
todayEvent.id = `${event.uid}-${dateKey}`;
if (
config.showAllDayEvents ||
(!config.showAllDayEvents && !todayEvent.allDay)
) {
map.get(dateKey)?.push(todayEvent);
}
current = current.add(1, 'day');
}
} else {
console.warn("event's start or end date is missing", event);
}
}
return map;
});
url = this.props.url;
loading$ = new LiveData(false);
@@ -1,5 +1,5 @@
import { Entity, LiveData, ObjectPool } from '@toeverything/infra';
import dayjs, { type Dayjs } from 'dayjs';
import { type Dayjs } from 'dayjs';
import ICAL from 'ical.js';
import { Observable, switchMap } from 'rxjs';
@@ -7,33 +7,10 @@ import type {
CalendarStore,
CalendarSubscriptionConfig,
} from '../store/calendar';
import type { CalendarEvent } from '../type';
import { parseCalendarUrl } from '../utils/calendar-url-parser';
import { CalendarSubscription } from './calendar-subscription';
export type CalendarEvent = {
id: string;
url: string;
title: string;
startAt?: ICAL.Time;
endAt?: ICAL.Time;
allDay?: boolean;
date?: Dayjs;
};
type EventsByDateMap = Map<string, CalendarEvent[]>;
const isAllDay = (current: Dayjs, start: Dayjs, end: Dayjs): boolean => {
if (current.isSame(start, 'day')) {
return (
start.hour() === 0 && start.minute() === 0 && !current.isSame(end, 'day')
);
} else if (current.isSame(end, 'day')) {
return false;
} else {
return true;
}
};
export class CalendarIntegration extends Entity {
constructor(private readonly store: CalendarStore) {
super();
@@ -76,65 +53,20 @@ export class CalendarIntegration extends Entity {
subscriptions.find(sub => sub.url === url)
);
}
contents$ = LiveData.computed(get => {
const subscriptions = get(this.subscriptions$);
return subscriptions.map(sub => ({
url: sub.url,
content: get(sub.content$),
}));
});
eventsByDateMap$ = LiveData.computed(get => {
const contents = get(this.contents$);
const eventsByDate: EventsByDateMap = new Map();
for (const { content, url } of contents) {
if (!content) continue;
const jCal = ICAL.parse(content);
const vCalendar = new ICAL.Component(jCal);
const vEvents = vCalendar.getAllSubcomponents('vevent');
for (const vEvent of vEvents) {
const event = new ICAL.Event(vEvent);
const calendarEvent: CalendarEvent = {
id: event.uid,
url,
title: event.summary,
startAt: event.startDate,
endAt: event.endDate,
};
// create index for each day of the event
if (event.startDate && event.endDate) {
const start = dayjs(event.startDate.toJSDate());
const end = dayjs(event.endDate.toJSDate());
let current = start;
while (current.isBefore(end) || current.isSame(end, 'day')) {
if (
current.isSame(end, 'day') &&
end.hour() === 0 &&
end.minute() === 0
) {
break;
}
const todayEvent: CalendarEvent = { ...calendarEvent };
const dateKey = current.format('YYYY-MM-DD');
if (!eventsByDate.has(dateKey)) {
eventsByDate.set(dateKey, []);
}
todayEvent.allDay = isAllDay(current, start, end);
todayEvent.date = current;
todayEvent.id = `${event.uid}-${dateKey}`;
eventsByDate.get(dateKey)?.push(todayEvent);
current = current.add(1, 'day');
}
} else {
console.warn("event's start or end date is missing", event);
return get(this.subscriptions$)
.map(sub => get(sub.eventsByDateMap$))
.reduce((acc, map) => {
for (const [date, events] of map) {
acc.set(
date,
acc.has(date) ? [...(acc.get(date) ?? []), ...events] : [...events]
);
}
}
}
return eventsByDate;
return acc;
}, new Map<string, CalendarEvent[]>());
});
eventsByDate$(date: Dayjs) {
return this.eventsByDateMap$.map(eventsByDateMap => {
const dateKey = date.format('YYYY-MM-DD');
@@ -18,9 +18,9 @@ import { IntegrationRefStore } from './store/integration-ref';
import { ReadwiseStore } from './store/readwise';
export { IntegrationService };
export type { CalendarEvent } from './entities/calendar';
export { CalendarIntegration } from './entities/calendar';
export { CalendarSubscription } from './entities/calendar-subscription';
export type { CalendarEvent } from './type';
export { IntegrationTypeIcon } from './views/icon';
export { DocIntegrationPropertiesTable } from './views/properties-table';
@@ -1,4 +1,6 @@
import type { I18nString } from '@affine/i18n';
import type { Dayjs } from 'dayjs';
import type ICAL from 'ical.js';
import type { ComponentType, SVGProps } from 'react';
import type { DocIntegrationRef } from '../db/schema/schema';
@@ -96,3 +98,18 @@ export interface ReadwiseConfig {
// Zotero
// ===============================
// TODO
// ===============================
// Calendar
// ===============================
export type CalendarEvent = {
id: string;
url: string;
title: string;
startAt?: ICAL.Time;
endAt?: ICAL.Time;
allDay?: boolean;
date?: Dayjs;
};
export type EventsByDateMap = Map<string, CalendarEvent[]>;
@@ -0,0 +1,13 @@
import type { Dayjs } from 'dayjs';
export const isAllDay = (current: Dayjs, start: Dayjs, end: Dayjs): boolean => {
if (current.isSame(start, 'day')) {
return (
start.hour() === 0 && start.minute() === 0 && !current.isSame(end, 'day')
);
} else if (current.isSame(end, 'day')) {
return false;
} else {
return true;
}
};