feat: organize invitations as the default ParticipantIdentity

This commit is contained in:
Linus Rath
2026-08-29 18:07:47 +02:00
parent e8c594001d
commit 14cdb2f6ff
34 changed files with 232 additions and 8 deletions
+11 -2
View File
@@ -137,9 +137,18 @@ export function CalendarApp({ linkSegments }: CalendarAppProps = {}) {
void fetchPrincipal();
}, [accountEmails, fetchPrincipal]);
// The default ParticipantIdentity (draft-ietf-jmap-calendars §6) is the
// address new invitations are organised from, so it goes first: the event
// modal takes currentUserEmails[0] as organizer.
const participantIdentities = useCalendarStore((s) => s.participantIdentities);
const currentUserEmails = useMemo(
() => collectUserCalendarAddresses(identities.map(id => id.email), accountEmails),
[identities, accountEmails]
() => collectUserCalendarAddresses(
participantIdentities.filter(i => i.isDefault).map(i => i.calendarAddress.replace(/^mailto:/i, '')),
participantIdentities.map(i => i.calendarAddress.replace(/^mailto:/i, '')),
identities.map(id => id.email),
accountEmails,
),
[participantIdentities, identities, accountEmails]
);
const [showEventModal, setShowEventModal] = useState(false);
+35 -1
View File
@@ -1,16 +1,30 @@
"use client";
import { useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { useCalendarStore, CalendarViewMode } from '@/stores/calendar-store';
import { useSettingsStore } from '@/stores/settings-store';
import { usePolicyStore } from '@/stores/policy-store';
import { useAuthStore } from '@/stores/auth-store';
import { toast } from '@/stores/toast-store';
import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section';
export function CalendarSettings() {
const t = useTranslations('calendar.settings');
const tViews = useTranslations('calendar.views');
const { viewMode, setViewMode } = useCalendarStore();
const { viewMode, setViewMode, participantIdentities, fetchParticipantIdentities, setDefaultParticipantIdentity } = useCalendarStore();
const client = useAuthStore((s) => s.client);
// ParticipantIdentity list (draft-ietf-jmap-calendars §6): which of the
// user's addresses organises new invitations. Loaded here because the
// settings page can open before the calendar app ever did.
useEffect(() => {
if (client && participantIdentities.length === 0) {
void fetchParticipantIdentities(client);
}
}, [client, participantIdentities.length, fetchParticipantIdentities]);
const defaultIdentityId = participantIdentities.find((i) => i.isDefault)?.id ?? participantIdentities[0]?.id ?? '';
const {
showTimeInMonthView,
showWeekNumbers,
@@ -37,6 +51,26 @@ export function CalendarSettings() {
/>
</SettingItem>
{client && participantIdentities.length > 1 && (
<SettingItem
label={t('organizer_identity')}
description={t('organizer_identity_desc')}
>
<Select
value={defaultIdentityId}
onChange={(value) => {
setDefaultParticipantIdentity(client, value).catch((err) => {
toast.error(err instanceof Error ? err.message : t('organizer_identity_failed'));
});
}}
options={participantIdentities.map((i) => {
const address = i.calendarAddress.replace(/^mailto:/i, '');
return { value: i.id, label: i.name && i.name !== address ? `${i.name} <${address}>` : address };
})}
/>
</SettingItem>
)}
<SettingItem
label={t('show_time_in_month_view')}
description={t('show_time_in_month_view_desc')}
@@ -188,6 +188,32 @@ describe('JMAP client extensions', () => {
});
});
describe('participant identities', () => {
it('maps ParticipantIdentity/get and sets the default via onSuccessSetIsDefault', async () => {
await setup({ 'urn:ietf:params:jmap:calendars': {} });
answer((body) => {
const [method] = body.methodCalls[0];
if (method === 'ParticipantIdentity/get') {
return { methodResponses: [['ParticipantIdentity/get', { list: [
{ id: 'i1', name: 'Me', calendarAddress: 'mailto:me@example.com', isDefault: true },
{ id: 'i2', name: '', calendarAddress: 'mailto:alias@example.com', isDefault: false },
] }, '0']] };
}
return { methodResponses: [['ParticipantIdentity/set', { updated: null }, '0']] };
});
const identities = await client.getParticipantIdentities();
expect(requests[0].using).toContain('urn:ietf:params:jmap:calendars');
expect(identities).toEqual([
{ id: 'i1', name: 'Me', calendarAddress: 'mailto:me@example.com', isDefault: true },
{ id: 'i2', name: '', calendarAddress: 'mailto:alias@example.com', isDefault: false },
]);
await client.setDefaultParticipantIdentity('i2');
expect(requests[1].methodCalls[0]).toEqual(['ParticipantIdentity/set', { accountId: 'acct-1', onSuccessSetIsDefault: 'i2' }, '0']);
});
});
describe('free/busy', () => {
it('needs both the principals and the availability capability', async () => {
await setup();
+5 -1
View File
@@ -1,4 +1,4 @@
import type { Email, Mailbox, MailboxRights, StateChange, AccountStates, CollectionChanges, ShareNotification, BusyPeriod, Thread, Identity, EmailAddress, ContactCard, AddressBook, AddressBookRights, VacationResponse, Calendar, CreateCalendarOptions, CalendarRights, CalendarEvent, CalendarEventFilter, CalendarTask, FileNode, FileNodeRights, Principal, PushSubscription, EmailPushConfig, ScheduledEmail, SendEmailResult, SharedAccount } from "./types";
import type { Email, Mailbox, MailboxRights, StateChange, AccountStates, CollectionChanges, ShareNotification, BusyPeriod, CalendarParticipantIdentity, Thread, Identity, EmailAddress, ContactCard, AddressBook, AddressBookRights, VacationResponse, Calendar, CreateCalendarOptions, CalendarRights, CalendarEvent, CalendarEventFilter, CalendarTask, FileNode, FileNodeRights, Principal, PushSubscription, EmailPushConfig, ScheduledEmail, SendEmailResult, SharedAccount } from "./types";
import type { SieveScript, SieveCapabilities } from "./sieve-types";
import type { SortLevel } from "@/lib/message-list-order";
@@ -126,6 +126,10 @@ export interface IJMAPClient {
createMailbox(name: string, parentId?: string, accountId?: string): Promise<Mailbox>;
updateMailbox(mailboxId: string, changes: { name?: string; parentId?: string | null; role?: string | null; sortOrder?: number }, accountId?: string): Promise<void>;
deleteMailbox(mailboxId: string, accountId?: string): Promise<void>;
// ParticipantIdentity (draft-ietf-jmap-calendars §6): the addresses the
// user organises events as. Optional.
getParticipantIdentities?(): Promise<CalendarParticipantIdentity[]>;
setDefaultParticipantIdentity?(id: string): Promise<void>;
// Free/busy (Principal/getAvailability, principals:availability). Optional.
supportsAvailability?(): boolean;
getPrincipalAvailability?(principalId: string, utcStart: Date, utcEnd: Date): Promise<BusyPeriod[]>;
+37 -1
View File
@@ -1,5 +1,5 @@
import { generateUUID } from '@/lib/utils';
import type { Email, Mailbox, MailboxRights, StateChange, AccountStates, CollectionChanges, ShareNotification, BusyPeriod, Thread, Identity, EmailAddress, ContactCard, AddressBook, AddressBookRights, VacationResponse, Calendar, CalendarComponentType, CalendarRights, CalendarEvent, CalendarEventFilter, CalendarTask, CreateCalendarOptions, FileNode, FileNodeFilter, FileNodeRights, Principal, PushSubscription, EmailPushConfig, EmailSubmission, ScheduledEmail, SendEmailResult, SharedAccount } from "./types";
import type { Email, Mailbox, MailboxRights, StateChange, AccountStates, CollectionChanges, ShareNotification, BusyPeriod, CalendarParticipantIdentity, Thread, Identity, EmailAddress, ContactCard, AddressBook, AddressBookRights, VacationResponse, Calendar, CalendarComponentType, CalendarRights, CalendarEvent, CalendarEventFilter, CalendarTask, CreateCalendarOptions, FileNode, FileNodeFilter, FileNodeRights, Principal, PushSubscription, EmailPushConfig, EmailSubmission, ScheduledEmail, SendEmailResult, SharedAccount } from "./types";
import type { SieveScript, SieveCapabilities } from "./sieve-types";
import type { IJMAPClient, KeywordDiscoveryResult, KeywordInfo, KeywordMigration } from "./client-interface";
import { toWildcardQuery } from "./search-utils";
@@ -5071,6 +5071,42 @@ export class JMAPClient implements IJMAPClient {
}
}
// ── ParticipantIdentity (draft-ietf-jmap-calendars §6) ─────────
/**
* The calendar addresses this user can organise events as, with the
* server's default flagged. Empty when the server has no calendars.
*/
async getParticipantIdentities(): Promise<CalendarParticipantIdentity[]> {
if (!this.supportsCalendars()) return [];
const accountId = this.getCalendarsAccountId();
const response = await this.request([
["ParticipantIdentity/get", { accountId }, "0"],
], this.calendarUsing());
const [method, result] = response.methodResponses?.[0] ?? [];
if (method !== "ParticipantIdentity/get") {
throw new Error((result as { description?: string })?.description || "Failed to load participant identities");
}
return ((result as { list?: Array<Partial<CalendarParticipantIdentity> & { id: string }> }).list ?? []).map((i) => ({
id: i.id,
name: i.name ?? "",
calendarAddress: i.calendarAddress ?? "",
isDefault: i.isDefault === true,
}));
}
/** Makes `id` the default identity (ParticipantIdentity/set onSuccessSetIsDefault). */
async setDefaultParticipantIdentity(id: string): Promise<void> {
const accountId = this.getCalendarsAccountId();
const response = await this.request([
["ParticipantIdentity/set", { accountId, onSuccessSetIsDefault: id }, "0"],
], this.calendarUsing());
const [method, result] = response.methodResponses?.[0] ?? [];
if (method !== "ParticipantIdentity/set") {
throw new Error((result as { description?: string })?.description || "Failed to set the default participant identity");
}
}
// ── Principal/getAvailability (free/busy) ──────────────────────
static readonly PRINCIPALS_AVAILABILITY_CAPABILITY = 'urn:ietf:params:jmap:principals:availability';
+8 -2
View File
@@ -805,11 +805,17 @@ export interface CalendarTask {
relatedTo: Record<string, CalendarRelation> | null;
}
/**
* A ParticipantIdentity (draft-ietf-jmap-calendars §6): one of the calendar
* addresses the user may act as when organising or replying to events. The
* server derives them from the account's addresses; the default one is what
* new invitations are sent from.
*/
export interface CalendarParticipantIdentity {
id: string;
name: string;
scheduleId: string;
sendTo: Record<string, string>;
/** `mailto:` URI of the scheduling address. */
calendarAddress: string;
isDefault: boolean;
}
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "تأخير ثانية واحدة",
"hover_preview_delay_2s": "تأخير ثانيتين",
"hover_preview_off": "معطّل",
"organizer_identity": "تنظيم الدعوات باسم",
"organizer_identity_desc": "عنوان التقويم المستخدم كمنظّم عند دعوة الآخرين إلى الأحداث.",
"organizer_identity_failed": "فشل تغيير عنوان المنظّم",
"show_birthday_calendar": "تقويم أعياد ميلاد جهات الاتصال",
"show_birthday_calendar_desc": "إظهار تقويم افتراضي بأعياد ميلاد جهات اتصالك"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "Retard d'1 segon",
"hover_preview_delay_2s": "Retard de 2 segons",
"hover_preview_off": "Desactivada",
"organizer_identity": "Organitza les invitacions com a",
"organizer_identity_desc": "L'adreça de calendari usada com a organitzador quan convides gent als esdeveniments.",
"organizer_identity_failed": "No s'ha pogut canviar l'adreça de l'organitzador",
"show_birthday_calendar": "Calendari d'aniversaris dels contactes",
"show_birthday_calendar_desc": "Mostra un calendari virtual amb els aniversaris dels vostres contactes"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "Zpoždění 1 sekunda",
"hover_preview_delay_2s": "Zpoždění 2 sekundy",
"hover_preview_off": "Vypnuto",
"organizer_identity": "Organizovat pozvánky jako",
"organizer_identity_desc": "Adresa kalendáře použitá jako organizátor, když zvete lidi na události.",
"organizer_identity_failed": "Nepodařilo se změnit adresu organizátora",
"show_birthday_calendar": "Kalendář narozenin",
"show_birthday_calendar_desc": "Zobrazovat virtuální kalendář s narozeninami z vašich kontaktů"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "1 sekunds forsinkelse",
"hover_preview_delay_2s": "2 sekunders forsinkelse",
"hover_preview_off": "Deaktiveret",
"organizer_identity": "Organisér invitationer som",
"organizer_identity_desc": "Kalenderadressen, der bruges som arrangør, når du inviterer folk til begivenheder.",
"organizer_identity_failed": "Kunne ikke ændre arrangøradressen",
"show_birthday_calendar": "Kontaktfødselsdagskalender",
"show_birthday_calendar_desc": "Vis en virtuel kalender med fødselsdage fra dine kontakter"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "1 Sekunde Verzögerung",
"hover_preview_delay_2s": "2 Sekunden Verzögerung",
"hover_preview_off": "Deaktiviert",
"organizer_identity": "Einladungen organisieren als",
"organizer_identity_desc": "Die Kalenderadresse, die als Organisator verwendet wird, wenn du Personen zu Terminen einlädst.",
"organizer_identity_failed": "Organisatoradresse konnte nicht geändert werden",
"show_birthday_calendar": "Geburtstags-Kalender",
"show_birthday_calendar_desc": "Zeigt einen virtuellen Kalender mit Geburtstagen aus deinen Kontakten"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "1-second delay",
"hover_preview_delay_2s": "2-second delay",
"hover_preview_off": "Disabled",
"organizer_identity": "Organize invitations as",
"organizer_identity_desc": "The calendar address used as organizer when you invite people to events.",
"organizer_identity_failed": "Failed to change the organizer address",
"show_birthday_calendar": "Contact birthday calendar",
"show_birthday_calendar_desc": "Show a virtual calendar with birthdays from your contacts"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "Retraso de 1 segundo",
"hover_preview_delay_2s": "Retraso de 2 segundos",
"hover_preview_off": "Desactivado",
"organizer_identity": "Organizar invitaciones como",
"organizer_identity_desc": "La dirección de calendario usada como organizador al invitar a personas a eventos.",
"organizer_identity_failed": "No se pudo cambiar la dirección del organizador",
"show_birthday_calendar": "Calendario de cumpleaños",
"show_birthday_calendar_desc": "Mostrar un calendario virtual con los cumpleaños de tus contactos"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "تأخیر ۱ ثانیه",
"hover_preview_delay_2s": "تأخیر ۲ ثانیه",
"hover_preview_off": "غیرفعال",
"organizer_identity": "سازمان‌دهی دعوت‌ها به‌عنوان",
"organizer_identity_desc": "آدرس تقویمی که هنگام دعوت افراد به رویدادها به‌عنوان برگزارکننده استفاده می‌شود.",
"organizer_identity_failed": "تغییر آدرس برگزارکننده ناموفق بود",
"show_birthday_calendar": "تقویم تولد مخاطبین",
"show_birthday_calendar_desc": "نمایش تقویم مجازی با تولدهای مخاطبین"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "Délai de 1 seconde",
"hover_preview_delay_2s": "Délai de 2 secondes",
"hover_preview_off": "Désactivé",
"organizer_identity": "Organiser les invitations en tant que",
"organizer_identity_desc": "L'adresse d'agenda utilisée comme organisateur lorsque vous invitez des personnes à des événements.",
"organizer_identity_failed": "Impossible de modifier l'adresse de l'organisateur",
"show_birthday_calendar": "Calendrier des anniversaires",
"show_birthday_calendar_desc": "Afficher un calendrier virtuel avec les anniversaires de vos contacts"
},
+3
View File
@@ -2852,6 +2852,9 @@
"hover_preview_delay_1s": "עיכוב של שנייה",
"hover_preview_delay_2s": "עיכוב של 2 שניות",
"hover_preview_off": "מושבת",
"organizer_identity": "ארגון הזמנות בתור",
"organizer_identity_desc": "כתובת היומן שתשמש כמארגן כשמזמינים אנשים לאירועים.",
"organizer_identity_failed": "שינוי כתובת המארגן נכשל",
"show_birthday_calendar": "צור קשר עם יומן יום הולדת",
"show_birthday_calendar_desc": "הצג לוח שנה וירטואלי עם ימי הולדת מאנשי הקשר שלך"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "1 másodperc késleltetés",
"hover_preview_delay_2s": "2 másodperc késleltetés",
"hover_preview_off": "Letiltva",
"organizer_identity": "Meghívók szervezése mint",
"organizer_identity_desc": "A szervezőként használt naptárcím, amikor embereket hívsz meg eseményekre.",
"organizer_identity_failed": "Nem sikerült módosítani a szervező címét",
"show_birthday_calendar": "Névjegy születésnap naptár",
"show_birthday_calendar_desc": "Virtuális naptár mutatása a névjegyeid születésnapjaival"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "Ritardo di 1 secondo",
"hover_preview_delay_2s": "Ritardo di 2 secondi",
"hover_preview_off": "Disabilitato",
"organizer_identity": "Organizza gli inviti come",
"organizer_identity_desc": "L'indirizzo del calendario usato come organizzatore quando inviti persone agli eventi.",
"organizer_identity_failed": "Impossibile cambiare l'indirizzo dell'organizzatore",
"show_birthday_calendar": "Calendario compleanni",
"show_birthday_calendar_desc": "Mostra un calendario virtuale con i compleanni dei tuoi contatti"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "1秒遅延",
"hover_preview_delay_2s": "2秒遅延",
"hover_preview_off": "無効",
"organizer_identity": "招待の主催者アドレス",
"organizer_identity_desc": "イベントに人を招待する際に主催者として使用するカレンダーアドレスです。",
"organizer_identity_failed": "主催者アドレスを変更できませんでした",
"show_birthday_calendar": "誕生日カレンダー",
"show_birthday_calendar_desc": "連絡先の誕生日を表示する仮想カレンダーを表示します"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "1초 지연",
"hover_preview_delay_2s": "2초 지연",
"hover_preview_off": "사용 안 함",
"organizer_identity": "초대 주최자 주소",
"organizer_identity_desc": "이벤트에 사람들을 초대할 때 주최자로 사용되는 캘린더 주소입니다.",
"organizer_identity_failed": "주최자 주소를 변경하지 못했습니다",
"show_birthday_calendar": "연락처 생일 캘린더",
"show_birthday_calendar_desc": "연락처에 등록된 생일을 가상 캘린더로 보여줘요"
},
+3
View File
@@ -2924,6 +2924,9 @@
"hover_preview_delay_1s": "1 sekundes aizture",
"hover_preview_delay_2s": "2 sekunžu aizture",
"hover_preview_off": "Izslēgts",
"organizer_identity": "Organizēt ielūgumus kā",
"organizer_identity_desc": "Kalendāra adrese, ko izmanto kā organizatoru, uzaicinot cilvēkus uz notikumiem.",
"organizer_identity_failed": "Neizdevās mainīt organizatora adresi",
"show_birthday_calendar": "Kontaktu dzimšanas dienu kalendārs",
"show_birthday_calendar_desc": "Rādīt virtuālu kalendāru ar kontaktu dzimšanas dienām"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "1 секундын саатал",
"hover_preview_delay_2s": "2 секундын саатал",
"hover_preview_off": "Хөгжлийн бэрхшээлтэй",
"organizer_identity": "Урилгыг зохион байгуулагч",
"organizer_identity_desc": "Хүмүүсийг арга хэмжээнд урихад зохион байгуулагчаар ашиглах хуанлийн хаяг.",
"organizer_identity_failed": "Зохион байгуулагчийн хаягийг өөрчилж чадсангүй",
"show_birthday_calendar": "Холбоо барих төрсөн өдрийн хуанли",
"show_birthday_calendar_desc": "Харилцагчдаас төрсөн өдөр бүхий виртуал хуанли харуулах"
},
+3
View File
@@ -2923,6 +2923,9 @@
"hover_preview_delay_1s": "Forsinkelse på 1 sekund",
"hover_preview_delay_2s": "Forsinkelse på 2 sekunder",
"hover_preview_off": "Deaktivert",
"organizer_identity": "Organiser invitasjoner som",
"organizer_identity_desc": "Kalenderadressen som brukes som arrangør når du inviterer folk til hendelser.",
"organizer_identity_failed": "Kunne ikke endre arrangøradressen",
"show_birthday_calendar": "Fødselsdager fra kontakter",
"show_birthday_calendar_desc": "Vis en virtuell kalender med bursdager fra kontaktene dine"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "1 seconde vertraging",
"hover_preview_delay_2s": "2 seconden vertraging",
"hover_preview_off": "Uitgeschakeld",
"organizer_identity": "Uitnodigingen organiseren als",
"organizer_identity_desc": "Het agenda-adres dat als organisator wordt gebruikt wanneer je mensen uitnodigt voor afspraken.",
"organizer_identity_failed": "Kon het organisatoradres niet wijzigen",
"show_birthday_calendar": "Verjaardagskalender",
"show_birthday_calendar_desc": "Toon een virtuele kalender met verjaardagen van je contacten"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "Opóźnienie 1 sekundy",
"hover_preview_delay_2s": "Opóźnienie 2 sekund",
"hover_preview_off": "Wyłączono",
"organizer_identity": "Organizuj zaproszenia jako",
"organizer_identity_desc": "Adres kalendarza używany jako organizator, gdy zapraszasz osoby na wydarzenia.",
"organizer_identity_failed": "Nie udało się zmienić adresu organizatora",
"show_birthday_calendar": "Kalendarz urodzin kontaktów",
"show_birthday_calendar_desc": "Pokaż wirtualny kalendarz z urodzinami z kontaktów"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "Atraso de 1 segundo",
"hover_preview_delay_2s": "Atraso de 2 segundos",
"hover_preview_off": "Desativado",
"organizer_identity": "Organizar convites como",
"organizer_identity_desc": "O endereço de calendário usado como organizador quando convida pessoas para eventos.",
"organizer_identity_failed": "Não foi possível alterar o endereço do organizador",
"show_birthday_calendar": "Calendário de aniversários",
"show_birthday_calendar_desc": "Mostrar um calendário virtual com os aniversários dos seus contatos"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "Întârziere de 1 secundă",
"hover_preview_delay_2s": "Întârziere de 2 secunde",
"hover_preview_off": "Dezactivat",
"organizer_identity": "Organizează invitațiile ca",
"organizer_identity_desc": "Adresa de calendar folosită ca organizator când inviți persoane la evenimente.",
"organizer_identity_failed": "Nu s-a putut schimba adresa organizatorului",
"show_birthday_calendar": "Calendarul zilelor de naștere ale contactelor",
"show_birthday_calendar_desc": "Afișați un calendar virtual cu zilele de naștere ale contactelor dvs."
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "Задержка 1 секунда",
"hover_preview_delay_2s": "Задержка 2 секунды",
"hover_preview_off": "Отключено",
"organizer_identity": "Организовывать приглашения как",
"organizer_identity_desc": "Адрес календаря, используемый как организатор при приглашении людей на события.",
"organizer_identity_failed": "Не удалось изменить адрес организатора",
"show_birthday_calendar": "Календарь дней рождения",
"show_birthday_calendar_desc": "Показать виртуальный календарь с днями рождения из ваших контактов"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "Oneskorenie 1 sekunda",
"hover_preview_delay_2s": "Oneskorenie 2 sekundy",
"hover_preview_off": "Vypnuté",
"organizer_identity": "Organizovať pozvánky ako",
"organizer_identity_desc": "Adresa kalendára použitá ako organizátor, keď pozývate ľudí na udalosti.",
"organizer_identity_failed": "Nepodarilo sa zmeniť adresu organizátora",
"show_birthday_calendar": "Kalendár narodenín",
"show_birthday_calendar_desc": "Zobrazovať virtuálny kalendár s narodeninami z vašich kontaktov"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "1 saniyelik gecikme",
"hover_preview_delay_2s": "2 saniyelik gecikme",
"hover_preview_off": "Devre dışı",
"organizer_identity": "Davetleri şu kimlikle düzenle",
"organizer_identity_desc": "Kişileri etkinliklere davet ederken düzenleyici olarak kullanılan takvim adresi.",
"organizer_identity_failed": "Düzenleyici adresi değiştirilemedi",
"show_birthday_calendar": "Kişi doğum günü takvimi",
"show_birthday_calendar_desc": "Kişilerinizdeki doğum günlerini gösteren sanal takvim göster"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "1-секундна затримка",
"hover_preview_delay_2s": "2-секундна затримка",
"hover_preview_off": "Вимкнено",
"organizer_identity": "Організовувати запрошення як",
"organizer_identity_desc": "Адреса календаря, що використовується як організатор, коли ви запрошуєте людей на події.",
"organizer_identity_failed": "Не вдалося змінити адресу організатора",
"show_birthday_calendar": "Контактний календар днів народження",
"show_birthday_calendar_desc": "Показати віртуальний календар із днями народження ваших контактів"
},
+3
View File
@@ -2918,6 +2918,9 @@
"hover_preview_delay_1s": "延遲 1 秒",
"hover_preview_delay_2s": "延遲 2 秒",
"hover_preview_off": "停用",
"organizer_identity": "以此身分發送邀請",
"organizer_identity_desc": "邀請他人參加活動時用作主辦者的行事曆地址。",
"organizer_identity_failed": "無法變更主辦者地址",
"show_birthday_calendar": "聯絡人生日行事曆",
"show_birthday_calendar_desc": "顯示包含聯絡人生日的虛擬行事曆"
},
+3
View File
@@ -2925,6 +2925,9 @@
"hover_preview_delay_1s": "1秒延迟",
"hover_preview_delay_2s": "2秒延迟",
"hover_preview_off": "关闭",
"organizer_identity": "以此身份组织邀请",
"organizer_identity_desc": "邀请他人参加活动时用作组织者的日历地址。",
"organizer_identity_failed": "无法更改组织者地址",
"show_birthday_calendar": "联系人生日日历",
"show_birthday_calendar_desc": "显示包含联系人生日的虚拟日历"
},
+29 -1
View File
@@ -1,7 +1,7 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { IJMAPClient } from '@/lib/jmap/client-interface';
import type { Calendar, CalendarEvent, CalendarParticipant, CalendarRights, CreateCalendarOptions } from '@/lib/jmap/types';
import type { Calendar, CalendarEvent, CalendarParticipant, CalendarParticipantIdentity, CalendarRights, CreateCalendarOptions } from '@/lib/jmap/types';
import { debug } from '@/lib/debug';
import { normalizeAllDayDuration } from '@/lib/calendar-utils';
import { displayNow } from '@/lib/timezone';
@@ -427,6 +427,11 @@ interface CalendarStore {
supportsCalendar: boolean;
error: string | null;
dateRange: { start: string; end: string } | null;
// The user's ParticipantIdentity list (calendar addresses to organise
// as); the default one is used as organizer of new invitations.
participantIdentities: CalendarParticipantIdentity[];
fetchParticipantIdentities: (client: IJMAPClient) => Promise<void>;
setDefaultParticipantIdentity: (client: IJMAPClient, id: string) => Promise<void>;
setSupported: (supported: boolean) => void;
fetchCalendars: (client: IJMAPClient) => Promise<void>;
@@ -469,6 +474,7 @@ const initialState = {
isLoading: false,
isLoadingEvents: false,
supportsCalendar: false,
participantIdentities: [],
error: null as string | null,
dateRange: null as { start: string; end: string } | null,
icalSubscriptions: [] as ICalSubscription[],
@@ -486,8 +492,30 @@ export const useCalendarStore = create<CalendarStore>()(
setSupported: (supported) => set({ supportsCalendar: supported }),
fetchParticipantIdentities: async (client) => {
if (!client.getParticipantIdentities) return;
try {
set({ participantIdentities: await client.getParticipantIdentities() });
} catch (error) {
// Servers without ParticipantIdentity (pre-draft) reject the method;
// the organizer then falls back to the login addresses.
debug.log('calendar', 'ParticipantIdentity/get unavailable:', error);
}
},
setDefaultParticipantIdentity: async (client, id) => {
if (!client.setDefaultParticipantIdentity) return;
await client.setDefaultParticipantIdentity(id);
set((state) => ({
participantIdentities: state.participantIdentities.map((i) => ({ ...i, isDefault: i.id === id })),
}));
},
fetchCalendars: async (client) => {
set({ isLoading: true, error: null });
// Identities are independent of the calendar list; load them
// alongside without blocking the grid.
void get().fetchParticipantIdentities(client);
try {
const calendars = await client.getAllCalendars();
const { selectedCalendarIds } = get();