feat: add JMAP Calendar integration with month/week/day/agenda views
Full calendar support via JMAP Calendars (RFC 8984): - Event create/edit/delete with recurrence rules and reminders - Multi-day event spanning, column-based overlap layout - Locale-aware date formatting, first day of week and time format settings - Real-time updates via push notifications - ARIA accessibility, input validation, color sanitization - Keyboard shortcuts, mobile touch targets, focus trap - ICU pluralization for all 8 supported languages
This commit is contained in:
+339
-28
@@ -1,4 +1,4 @@
|
||||
import type { Email, Mailbox, StateChange, AccountStates, Thread, Identity, EmailAddress, ContactCard, AddressBook, VacationResponse } from "./types";
|
||||
import type { Email, Mailbox, StateChange, AccountStates, Thread, Identity, EmailAddress, ContactCard, AddressBook, VacationResponse, Calendar, CalendarEvent, CalendarEventFilter } from "./types";
|
||||
|
||||
// JMAP protocol types - these are intentionally flexible due to server variations
|
||||
interface JMAPSession {
|
||||
@@ -1594,15 +1594,28 @@ export class JMAPClient {
|
||||
return this.hasCapability("urn:ietf:params:jmap:contacts");
|
||||
}
|
||||
|
||||
supportsCalendars(): boolean {
|
||||
return this.hasCapability("urn:ietf:params:jmap:calendars");
|
||||
}
|
||||
|
||||
getContactsAccountId(): string {
|
||||
const contactsAccount = this.session?.primaryAccounts?.["urn:ietf:params:jmap:contacts"];
|
||||
return contactsAccount || this.accountId;
|
||||
}
|
||||
|
||||
getCalendarsAccountId(): string {
|
||||
const calendarsAccount = this.session?.primaryAccounts?.["urn:ietf:params:jmap:calendars"];
|
||||
return calendarsAccount || this.accountId;
|
||||
}
|
||||
|
||||
private contactUsing(): string[] {
|
||||
return ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:contacts"];
|
||||
}
|
||||
|
||||
private calendarUsing(): string[] {
|
||||
return ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:calendars"];
|
||||
}
|
||||
|
||||
async getAddressBooks(): Promise<AddressBook[]> {
|
||||
try {
|
||||
const accountId = this.getContactsAccountId();
|
||||
@@ -1798,6 +1811,282 @@ export class JMAPClient {
|
||||
}
|
||||
}
|
||||
|
||||
async getCalendars(): Promise<Calendar[]> {
|
||||
try {
|
||||
const accountId = this.getCalendarsAccountId();
|
||||
const response = await this.request([
|
||||
["Calendar/get", { accountId }, "0"]
|
||||
], this.calendarUsing());
|
||||
|
||||
if (response.methodResponses?.[0]?.[0] === "Calendar/get") {
|
||||
return (response.methodResponses[0][1].list || []) as Calendar[];
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('Failed to get calendars:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async createCalendar(calendar: Partial<Calendar>): Promise<Calendar> {
|
||||
const accountId = this.getCalendarsAccountId();
|
||||
|
||||
const response = await this.request([
|
||||
["Calendar/set", {
|
||||
accountId,
|
||||
create: {
|
||||
"new-calendar": calendar
|
||||
}
|
||||
}, "0"]
|
||||
], this.calendarUsing());
|
||||
|
||||
if (response.methodResponses?.[0]?.[0] === "Calendar/set") {
|
||||
const result = response.methodResponses[0][1];
|
||||
|
||||
if (result.notCreated?.["new-calendar"]) {
|
||||
const error = result.notCreated["new-calendar"];
|
||||
throw new Error(error.description || "Failed to create calendar");
|
||||
}
|
||||
|
||||
const createdId = result.created?.["new-calendar"]?.id;
|
||||
if (createdId) {
|
||||
const calendars = await this.getCalendars();
|
||||
const created = calendars.find(c => c.id === createdId);
|
||||
if (created) return created;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Failed to create calendar");
|
||||
}
|
||||
|
||||
async updateCalendar(calendarId: string, updates: Partial<Calendar>): Promise<void> {
|
||||
const accountId = this.getCalendarsAccountId();
|
||||
|
||||
const response = await this.request([
|
||||
["Calendar/set", {
|
||||
accountId,
|
||||
update: {
|
||||
[calendarId]: updates
|
||||
}
|
||||
}, "0"]
|
||||
], this.calendarUsing());
|
||||
|
||||
if (response.methodResponses?.[0]?.[0] === "Calendar/set") {
|
||||
const result = response.methodResponses[0][1];
|
||||
|
||||
if (result.notUpdated?.[calendarId]) {
|
||||
const error = result.notUpdated[calendarId];
|
||||
throw new Error(error.description || "Failed to update calendar");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("Failed to update calendar");
|
||||
}
|
||||
|
||||
async deleteCalendar(calendarId: string): Promise<void> {
|
||||
const accountId = this.getCalendarsAccountId();
|
||||
|
||||
const response = await this.request([
|
||||
["Calendar/set", {
|
||||
accountId,
|
||||
destroy: [calendarId]
|
||||
}, "0"]
|
||||
], this.calendarUsing());
|
||||
|
||||
if (response.methodResponses?.[0]?.[0] === "Calendar/set") {
|
||||
const result = response.methodResponses[0][1];
|
||||
|
||||
if (result.notDestroyed?.[calendarId]) {
|
||||
const error = result.notDestroyed[calendarId];
|
||||
throw new Error(error.description || "Failed to delete calendar");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("Failed to delete calendar");
|
||||
}
|
||||
|
||||
async getCalendarEvents(calendarIds?: string[]): Promise<CalendarEvent[]> {
|
||||
try {
|
||||
const accountId = this.getCalendarsAccountId();
|
||||
|
||||
const queryArgs: Record<string, unknown> = { accountId, limit: 1000 };
|
||||
if (calendarIds && calendarIds.length > 0) {
|
||||
queryArgs.filter = { inCalendars: calendarIds };
|
||||
}
|
||||
|
||||
const response = await this.request([
|
||||
["CalendarEvent/query", queryArgs, "0"],
|
||||
["CalendarEvent/get", {
|
||||
accountId,
|
||||
"#ids": { resultOf: "0", name: "CalendarEvent/query", path: "/ids" },
|
||||
}, "1"]
|
||||
], this.calendarUsing());
|
||||
|
||||
if (response.methodResponses?.[1]?.[0] === "CalendarEvent/get") {
|
||||
return (response.methodResponses[1][1].list || []) as CalendarEvent[];
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('Failed to get calendar events:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async queryCalendarEvents(
|
||||
filter: CalendarEventFilter,
|
||||
sort?: Array<{ property: string; isAscending: boolean }>,
|
||||
limit?: number
|
||||
): Promise<CalendarEvent[]> {
|
||||
try {
|
||||
const accountId = this.getCalendarsAccountId();
|
||||
|
||||
const queryArgs: Record<string, unknown> = {
|
||||
accountId,
|
||||
filter,
|
||||
limit: limit || 100,
|
||||
};
|
||||
if (sort) {
|
||||
queryArgs.sort = sort;
|
||||
}
|
||||
|
||||
const response = await this.request([
|
||||
["CalendarEvent/query", queryArgs, "0"],
|
||||
["CalendarEvent/get", {
|
||||
accountId,
|
||||
"#ids": { resultOf: "0", name: "CalendarEvent/query", path: "/ids" },
|
||||
}, "1"]
|
||||
], this.calendarUsing());
|
||||
|
||||
if (response.methodResponses?.[1]?.[0] === "CalendarEvent/get") {
|
||||
return (response.methodResponses[1][1].list || []) as CalendarEvent[];
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('Failed to query calendar events:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getCalendarEvent(id: string): Promise<CalendarEvent | null> {
|
||||
try {
|
||||
const accountId = this.getCalendarsAccountId();
|
||||
const response = await this.request([
|
||||
["CalendarEvent/get", {
|
||||
accountId,
|
||||
ids: [id],
|
||||
}, "0"]
|
||||
], this.calendarUsing());
|
||||
|
||||
if (response.methodResponses?.[0]?.[0] === "CalendarEvent/get") {
|
||||
const list = response.methodResponses[0][1].list || [];
|
||||
return list[0] || null;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Failed to get calendar event:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async createCalendarEvent(event: Partial<CalendarEvent>, sendSchedulingMessages?: boolean): Promise<CalendarEvent> {
|
||||
const accountId = this.getCalendarsAccountId();
|
||||
|
||||
const setArgs: Record<string, unknown> = {
|
||||
accountId,
|
||||
create: {
|
||||
"new-event": event
|
||||
}
|
||||
};
|
||||
if (sendSchedulingMessages !== undefined) {
|
||||
setArgs.sendSchedulingMessages = sendSchedulingMessages;
|
||||
}
|
||||
|
||||
const response = await this.request([
|
||||
["CalendarEvent/set", setArgs, "0"]
|
||||
], this.calendarUsing());
|
||||
|
||||
if (response.methodResponses?.[0]?.[0] === "CalendarEvent/set") {
|
||||
const result = response.methodResponses[0][1];
|
||||
|
||||
if (result.notCreated?.["new-event"]) {
|
||||
const error = result.notCreated["new-event"];
|
||||
throw new Error(error.description || "Failed to create calendar event");
|
||||
}
|
||||
|
||||
const createdId = result.created?.["new-event"]?.id;
|
||||
if (createdId) {
|
||||
const created = await this.getCalendarEvent(createdId);
|
||||
if (created) return created;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Failed to create calendar event");
|
||||
}
|
||||
|
||||
async updateCalendarEvent(
|
||||
eventId: string,
|
||||
updates: Partial<CalendarEvent>,
|
||||
sendSchedulingMessages?: boolean
|
||||
): Promise<void> {
|
||||
const accountId = this.getCalendarsAccountId();
|
||||
|
||||
const setArgs: Record<string, unknown> = {
|
||||
accountId,
|
||||
update: {
|
||||
[eventId]: updates
|
||||
}
|
||||
};
|
||||
if (sendSchedulingMessages !== undefined) {
|
||||
setArgs.sendSchedulingMessages = sendSchedulingMessages;
|
||||
}
|
||||
|
||||
const response = await this.request([
|
||||
["CalendarEvent/set", setArgs, "0"]
|
||||
], this.calendarUsing());
|
||||
|
||||
if (response.methodResponses?.[0]?.[0] === "CalendarEvent/set") {
|
||||
const result = response.methodResponses[0][1];
|
||||
|
||||
if (result.notUpdated?.[eventId]) {
|
||||
const error = result.notUpdated[eventId];
|
||||
throw new Error(error.description || "Failed to update calendar event");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("Failed to update calendar event");
|
||||
}
|
||||
|
||||
async deleteCalendarEvent(eventId: string, sendSchedulingMessages?: boolean): Promise<void> {
|
||||
const accountId = this.getCalendarsAccountId();
|
||||
|
||||
const setArgs: Record<string, unknown> = {
|
||||
accountId,
|
||||
destroy: [eventId]
|
||||
};
|
||||
if (sendSchedulingMessages !== undefined) {
|
||||
setArgs.sendSchedulingMessages = sendSchedulingMessages;
|
||||
}
|
||||
|
||||
const response = await this.request([
|
||||
["CalendarEvent/set", setArgs, "0"]
|
||||
], this.calendarUsing());
|
||||
|
||||
if (response.methodResponses?.[0]?.[0] === "CalendarEvent/set") {
|
||||
const result = response.methodResponses[0][1];
|
||||
|
||||
if (result.notDestroyed?.[eventId]) {
|
||||
const error = result.notDestroyed[eventId];
|
||||
throw new Error(error.description || "Failed to delete calendar event");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("Failed to delete calendar event");
|
||||
}
|
||||
|
||||
async downloadBlob(blobId: string, name?: string, type?: string): Promise<void> {
|
||||
const url = this.getBlobDownloadUrl(blobId, name, type);
|
||||
|
||||
@@ -1851,25 +2140,32 @@ export class JMAPClient {
|
||||
|
||||
private async fetchCurrentStates(): Promise<void> {
|
||||
try {
|
||||
// Get current states from server using JMAP query
|
||||
const using = ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'];
|
||||
const methodCalls: JMAPMethodCall[] = [
|
||||
['Mailbox/get', { accountId: this.accountId, ids: null, properties: ['id'] }, 'a'],
|
||||
['Email/get', { accountId: this.accountId, ids: [], properties: ['id'] }, 'b'],
|
||||
];
|
||||
|
||||
if (this.supportsCalendars()) {
|
||||
using.push('urn:ietf:params:jmap:calendars');
|
||||
const calAccountId = this.getCalendarsAccountId();
|
||||
methodCalls.push(
|
||||
['Calendar/get', { accountId: calAccountId, ids: null, properties: ['id'] }, 'c'],
|
||||
['CalendarEvent/get', { accountId: calAccountId, ids: [], properties: ['id'] }, 'd'],
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(this.apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': this.authHeader,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'],
|
||||
methodCalls: [
|
||||
['Mailbox/get', { accountId: this.accountId, ids: null, properties: ['id'] }, 'a'],
|
||||
['Email/get', { accountId: this.accountId, ids: [], properties: ['id'] }, 'b'],
|
||||
],
|
||||
}),
|
||||
body: JSON.stringify({ using, methodCalls }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
// Extract states from response
|
||||
for (const [method, result] of data.methodResponses) {
|
||||
if (method === 'Mailbox/get' && result.state) {
|
||||
this.pollingStates['Mailbox'] = result.state;
|
||||
@@ -1877,6 +2173,12 @@ export class JMAPClient {
|
||||
if (method === 'Email/get' && result.state) {
|
||||
this.pollingStates['Email'] = result.state;
|
||||
}
|
||||
if (method === 'Calendar/get' && result.state) {
|
||||
this.pollingStates['Calendar'] = result.state;
|
||||
}
|
||||
if (method === 'CalendarEvent/get' && result.state) {
|
||||
this.pollingStates['CalendarEvent'] = result.state;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -1886,19 +2188,28 @@ export class JMAPClient {
|
||||
|
||||
private async checkForStateChanges(): Promise<void> {
|
||||
try {
|
||||
const using = ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'];
|
||||
const methodCalls: JMAPMethodCall[] = [
|
||||
['Mailbox/get', { accountId: this.accountId, ids: null, properties: ['id'] }, 'a'],
|
||||
['Email/get', { accountId: this.accountId, ids: [], properties: ['id'] }, 'b'],
|
||||
];
|
||||
|
||||
if (this.supportsCalendars()) {
|
||||
using.push('urn:ietf:params:jmap:calendars');
|
||||
const calAccountId = this.getCalendarsAccountId();
|
||||
methodCalls.push(
|
||||
['Calendar/get', { accountId: calAccountId, ids: null, properties: ['id'] }, 'c'],
|
||||
['CalendarEvent/get', { accountId: calAccountId, ids: [], properties: ['id'] }, 'd'],
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(this.apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': this.authHeader,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'],
|
||||
methodCalls: [
|
||||
['Mailbox/get', { accountId: this.accountId, ids: null, properties: ['id'] }, 'a'],
|
||||
['Email/get', { accountId: this.accountId, ids: [], properties: ['id'] }, 'b'],
|
||||
],
|
||||
}),
|
||||
body: JSON.stringify({ using, methodCalls }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -1907,19 +2218,19 @@ export class JMAPClient {
|
||||
let hasChanges = false;
|
||||
|
||||
for (const [method, result] of data.methodResponses) {
|
||||
if (method === 'Mailbox/get' && result.state) {
|
||||
if (this.pollingStates['Mailbox'] && this.pollingStates['Mailbox'] !== result.state) {
|
||||
changes['Mailbox'] = result.state;
|
||||
const typeMap: Record<string, string> = {
|
||||
'Mailbox/get': 'Mailbox',
|
||||
'Email/get': 'Email',
|
||||
'Calendar/get': 'Calendar',
|
||||
'CalendarEvent/get': 'CalendarEvent',
|
||||
};
|
||||
const stateKey = typeMap[method];
|
||||
if (stateKey && result.state) {
|
||||
if (this.pollingStates[stateKey] && this.pollingStates[stateKey] !== result.state) {
|
||||
changes[stateKey] = result.state;
|
||||
hasChanges = true;
|
||||
}
|
||||
this.pollingStates['Mailbox'] = result.state;
|
||||
}
|
||||
if (method === 'Email/get' && result.state) {
|
||||
if (this.pollingStates['Email'] && this.pollingStates['Email'] !== result.state) {
|
||||
changes['Email'] = result.state;
|
||||
hasChanges = true;
|
||||
}
|
||||
this.pollingStates['Email'] = result.state;
|
||||
this.pollingStates[stateKey] = result.state;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -266,6 +266,224 @@ export interface DeliveryStatus {
|
||||
displayed: "unknown" | "yes";
|
||||
}
|
||||
|
||||
// JMAP Calendar Types (RFC 8984 JSCalendar + RFC 9553 JMAP Calendars)
|
||||
|
||||
export interface Calendar {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
color: string | null;
|
||||
sortOrder: number;
|
||||
isSubscribed: boolean;
|
||||
isVisible: boolean;
|
||||
isDefault: boolean;
|
||||
includeInAvailability: 'all' | 'attending' | 'none';
|
||||
defaultAlertsWithTime: Record<string, CalendarEventAlert> | null;
|
||||
defaultAlertsWithoutTime: Record<string, CalendarEventAlert> | null;
|
||||
timeZone: string | null;
|
||||
shareWith: Record<string, CalendarRights> | null;
|
||||
myRights: CalendarRights;
|
||||
}
|
||||
|
||||
export interface CalendarRights {
|
||||
mayReadFreeBusy: boolean;
|
||||
mayReadItems: boolean;
|
||||
mayWriteAll: boolean;
|
||||
mayWriteOwn: boolean;
|
||||
mayUpdatePrivate: boolean;
|
||||
mayRSVP: boolean;
|
||||
mayAdmin: boolean;
|
||||
mayDelete: boolean;
|
||||
}
|
||||
|
||||
export interface CalendarEvent {
|
||||
id: string;
|
||||
calendarIds: Record<string, boolean>;
|
||||
isDraft: boolean;
|
||||
isOrigin: boolean;
|
||||
utcStart: string | null;
|
||||
utcEnd: string | null;
|
||||
'@type': 'Event';
|
||||
uid: string;
|
||||
title: string;
|
||||
description: string;
|
||||
descriptionContentType: string;
|
||||
created: string | null;
|
||||
updated: string;
|
||||
sequence: number;
|
||||
start: string;
|
||||
duration: string;
|
||||
timeZone: string | null;
|
||||
showWithoutTime: boolean;
|
||||
status: 'tentative' | 'confirmed' | 'cancelled';
|
||||
freeBusyStatus: 'free' | 'busy';
|
||||
privacy: 'public' | 'private' | 'secret';
|
||||
color: string | null;
|
||||
keywords: Record<string, boolean> | null;
|
||||
categories: Record<string, boolean> | null;
|
||||
locale: string | null;
|
||||
replyTo: Record<string, string> | null;
|
||||
participants: Record<string, CalendarParticipant> | null;
|
||||
mayInviteSelf: boolean;
|
||||
mayInviteOthers: boolean;
|
||||
hideAttendees: boolean;
|
||||
recurrenceId: string | null;
|
||||
recurrenceIdTimeZone: string | null;
|
||||
recurrenceRules: CalendarRecurrenceRule[] | null;
|
||||
recurrenceOverrides: Record<string, Partial<CalendarEvent>> | null;
|
||||
excludedRecurrenceRules: CalendarRecurrenceRule[] | null;
|
||||
useDefaultAlerts: boolean;
|
||||
alerts: Record<string, CalendarEventAlert> | null;
|
||||
locations: Record<string, CalendarLocation> | null;
|
||||
virtualLocations: Record<string, CalendarVirtualLocation> | null;
|
||||
links: Record<string, CalendarLink> | null;
|
||||
relatedTo: Record<string, CalendarRelation> | null;
|
||||
}
|
||||
|
||||
export interface CalendarParticipant {
|
||||
'@type': 'Participant';
|
||||
name: string;
|
||||
email: string;
|
||||
description: string | null;
|
||||
sendTo: Record<string, string> | null;
|
||||
kind: 'individual' | 'group' | 'location' | 'resource';
|
||||
roles: Record<string, boolean>;
|
||||
participationStatus: 'accepted' | 'declined' | 'tentative' | 'delegated' | 'needs-action';
|
||||
participationComment: string | null;
|
||||
expectReply: boolean;
|
||||
scheduleAgent: 'server' | 'client' | 'none';
|
||||
scheduleForceSend: boolean;
|
||||
scheduleId: string | null;
|
||||
scheduleSequence: number;
|
||||
scheduleStatus: string[] | null;
|
||||
scheduleUpdated: string | null;
|
||||
invitedBy: string | null;
|
||||
delegatedTo: Record<string, boolean> | null;
|
||||
delegatedFrom: Record<string, boolean> | null;
|
||||
memberOf: Record<string, boolean> | null;
|
||||
locationId: string | null;
|
||||
language: string | null;
|
||||
links: Record<string, CalendarLink> | null;
|
||||
}
|
||||
|
||||
export interface CalendarRecurrenceRule {
|
||||
'@type': 'RecurrenceRule';
|
||||
frequency: 'yearly' | 'monthly' | 'weekly' | 'daily' | 'hourly' | 'minutely' | 'secondly';
|
||||
interval: number;
|
||||
rscale: string;
|
||||
skip: 'omit' | 'backward' | 'forward';
|
||||
firstDayOfWeek: 'mo' | 'tu' | 'we' | 'th' | 'fr' | 'sa' | 'su';
|
||||
byDay: CalendarNDay[] | null;
|
||||
byMonthDay: number[] | null;
|
||||
byMonth: string[] | null;
|
||||
byYearDay: number[] | null;
|
||||
byWeekNo: number[] | null;
|
||||
byHour: number[] | null;
|
||||
byMinute: number[] | null;
|
||||
bySecond: number[] | null;
|
||||
bySetPosition: number[] | null;
|
||||
count: number | null;
|
||||
until: string | null;
|
||||
}
|
||||
|
||||
export interface CalendarNDay {
|
||||
day: string;
|
||||
nthOfPeriod?: number;
|
||||
}
|
||||
|
||||
export interface CalendarEventAlert {
|
||||
'@type': 'Alert';
|
||||
trigger: CalendarOffsetTrigger | CalendarAbsoluteTrigger;
|
||||
action: 'display' | 'email';
|
||||
acknowledged: string | null;
|
||||
relatedTo: Record<string, CalendarRelation> | null;
|
||||
}
|
||||
|
||||
export interface CalendarOffsetTrigger {
|
||||
'@type': 'OffsetTrigger';
|
||||
offset: string;
|
||||
relativeTo: 'start' | 'end';
|
||||
}
|
||||
|
||||
export interface CalendarAbsoluteTrigger {
|
||||
'@type': 'AbsoluteTrigger';
|
||||
when: string;
|
||||
}
|
||||
|
||||
export interface CalendarLocation {
|
||||
'@type': 'Location';
|
||||
name: string;
|
||||
description: string | null;
|
||||
locationTypes: Record<string, boolean> | null;
|
||||
coordinates: string | null;
|
||||
timeZone: string | null;
|
||||
links: Record<string, CalendarLink> | null;
|
||||
relativeTo: 'start' | 'end' | null;
|
||||
}
|
||||
|
||||
export interface CalendarVirtualLocation {
|
||||
'@type': 'VirtualLocation';
|
||||
name: string | null;
|
||||
description: string | null;
|
||||
uri: string;
|
||||
features: Record<string, boolean> | null;
|
||||
}
|
||||
|
||||
export interface CalendarLink {
|
||||
'@type': 'Link';
|
||||
href: string;
|
||||
cid: string | null;
|
||||
contentType: string | null;
|
||||
size: number | null;
|
||||
rel: string | null;
|
||||
display: string | null;
|
||||
title: string | null;
|
||||
}
|
||||
|
||||
export interface CalendarRelation {
|
||||
'@type': 'Relation';
|
||||
relation: Record<string, boolean> | null;
|
||||
}
|
||||
|
||||
export interface CalendarParticipantIdentity {
|
||||
id: string;
|
||||
name: string;
|
||||
scheduleId: string;
|
||||
sendTo: Record<string, string>;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
export interface CalendarEventNotification {
|
||||
id: string;
|
||||
created: string;
|
||||
changedBy: {
|
||||
name: string;
|
||||
email: string;
|
||||
principalId: string | null;
|
||||
scheduleId: string | null;
|
||||
};
|
||||
comment: string | null;
|
||||
type: 'created' | 'updated' | 'destroyed';
|
||||
calendarEventId: string;
|
||||
isDraft: boolean;
|
||||
event?: CalendarEvent;
|
||||
eventPatch?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CalendarEventFilter {
|
||||
inCalendars?: string[];
|
||||
after?: string;
|
||||
before?: string;
|
||||
text?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
location?: string;
|
||||
owner?: string;
|
||||
attendee?: string;
|
||||
participationStatus?: string;
|
||||
uid?: string;
|
||||
}
|
||||
|
||||
// JMAP Push Notification Types (RFC 8620 Section 7)
|
||||
|
||||
export interface StateChange {
|
||||
@@ -280,6 +498,8 @@ export interface StateChange {
|
||||
Identity?: string;
|
||||
ContactCard?: string;
|
||||
AddressBook?: string;
|
||||
Calendar?: string;
|
||||
CalendarEvent?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user