Files
SRCmail/lib/calendar-participants.ts
T
Matthieu MALVACHEandMatthieu MALVACHE b65b2f26df feat: add participant scheduling with iTIP invitations and inline calendar invitation banner
Add organizer/attendee UI with RSVP, contact autocomplete for participants,
scheduling messages, and inline calendar invitation banner in email viewer
with auto-detect .ics attachments, RSVP/import to calendar, and cancellation display.
2026-02-17 01:09:57 +01:00

107 lines
3.2 KiB
TypeScript

import type { CalendarEvent, CalendarParticipant } from '@/lib/jmap/types';
export interface ParticipantInfo {
id: string;
name: string;
email: string;
status: CalendarParticipant['participationStatus'];
isOrganizer: boolean;
}
export interface StatusCounts {
accepted: number;
declined: number;
tentative: number;
'needs-action': number;
}
export function isOrganizer(event: CalendarEvent, userEmails: string[]): boolean {
if (!event.participants) return false;
const lower = userEmails.map(e => e.toLowerCase());
return Object.values(event.participants).some(p =>
p.roles?.owner && lower.includes(p.email?.toLowerCase())
);
}
export function getUserParticipantId(event: CalendarEvent, userEmails: string[]): string | null {
if (!event.participants) return null;
const lower = userEmails.map(e => e.toLowerCase());
for (const [id, p] of Object.entries(event.participants)) {
if (lower.includes(p.email?.toLowerCase())) return id;
}
return null;
}
export function getUserStatus(
event: CalendarEvent,
userEmails: string[]
): CalendarParticipant['participationStatus'] | null {
if (!event.participants) return null;
const lower = userEmails.map(e => e.toLowerCase());
for (const p of Object.values(event.participants)) {
if (lower.includes(p.email?.toLowerCase())) return p.participationStatus;
}
return null;
}
export function getParticipantList(event: CalendarEvent): ParticipantInfo[] {
if (!event.participants) return [];
return Object.entries(event.participants).map(([id, p]) => ({
id,
name: p.name || '',
email: p.email || '',
status: p.participationStatus || 'needs-action',
isOrganizer: !!p.roles?.owner,
}));
}
export function getStatusCounts(event: CalendarEvent): StatusCounts {
const counts: StatusCounts = { accepted: 0, declined: 0, tentative: 0, 'needs-action': 0 };
if (!event.participants) return counts;
for (const p of Object.values(event.participants)) {
const s = p.participationStatus || 'needs-action';
if (s in counts) counts[s as keyof StatusCounts]++;
}
return counts;
}
export function getParticipantCount(event: CalendarEvent): number {
if (!event.participants) return 0;
return Object.keys(event.participants).length;
}
export function buildParticipantMap(
organizer: { name: string; email: string },
attendees: { name: string; email: string }[]
): Record<string, Partial<CalendarParticipant>> {
const participants: Record<string, Partial<CalendarParticipant>> = {};
participants['organizer'] = {
'@type': 'Participant',
name: organizer.name,
email: organizer.email,
roles: { owner: true, attendee: true },
participationStatus: 'accepted',
scheduleAgent: 'server',
sendTo: { imip: `mailto:${organizer.email}` },
expectReply: false,
kind: 'individual',
};
attendees.forEach((a, i) => {
participants[`attendee-${i}`] = {
'@type': 'Participant',
name: a.name,
email: a.email,
roles: { attendee: true },
participationStatus: 'needs-action',
scheduleAgent: 'server',
sendTo: { imip: `mailto:${a.email}` },
expectReply: true,
kind: 'individual',
};
});
return participants;
}