mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-03 08:06:59 +00:00
fix: never send an occurrence change to the next occurrence
A first change to one occurrence that keeps the attendees' answers goes out in two writes: the override as the occurrence stands, quietly, then the change as an update of it. The occurrence is looked up again in between, because the new override can give it another synthetic id. When that lookup failed, the change went to the old id. Up to Stalwart 0.16.20 synthetic ids are positions in the series' expansion (calendar_event/mod.rs), so after the new override the old id can name the next occurrence, which then got the change and its scheduling messages. 0.16.21 and 1.0 key the ids by recurrence id, and the id stays (checked live on 1.0 and 0.16.23). Now, when the occurrence is not found again, found twice, or the lookup fails, nothing more is sent and the editor says the change was not saved. The quiet override changes nothing that shows; the calendar is read again so a retry uses the ids the series has now, and goes out as an update of that override, which reaches every attendee. The message is translated into all 26 languages.
This commit is contained in:
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, zero {يبدأ بعد # ساعة} one {يبدأ بعد ساعة واحدة} two {يبدأ بعد ساعتين} few {يبدأ بعد # ساعات} many {يبدأ بعد # ساعة} other {يبدأ بعد # ساعة}}",
|
||||
"starts_in_minutes": "{count, plural, zero {يبدأ بعد # دقيقة} one {يبدأ بعد دقيقة واحدة} two {يبدأ بعد دقيقتين} few {يبدأ بعد # دقائق} many {يبدأ بعد # دقيقة} other {يبدأ بعد # دقيقة}}"
|
||||
},
|
||||
"occurrence_not_found_again": "لم يُحفظ التغيير: تعذّر العثور على هذا التكرار مرة أخرى على الخادم. حاول مرة أخرى.",
|
||||
"participants": {
|
||||
"limit_warning": "أكثر من {max, plural, zero {# مشارك} one {مشارك واحد} two {مشاركَين} few {# مشاركين} many {# مشاركًا} other {# مشارك}}، بمن فيهم المنظّم: قد يرفض الخادم حفظ هذا الحدث. يمكن للمسؤول رفع هذا الحد."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {Comença d'aquí a # hora} other {Comença d'aquí a # hores}}",
|
||||
"starts_in_minutes": "{count, plural, one {Comença d'aquí a # minut} other {Comença d'aquí a # minuts}}"
|
||||
},
|
||||
"occurrence_not_found_again": "No s'ha desat el canvi: no s'ha pogut tornar a trobar la repetició al servidor. Torneu-ho a provar.",
|
||||
"participants": {
|
||||
"limit_warning": "Més de {max, plural, one {# participant} other {# participants}}, inclòs l'organitzador: és possible que el servidor es negui a desar aquest esdeveniment. Un administrador pot augmentar el límit."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {Začíná za # hodinu} few {Začíná za # hodiny} many {Začíná za # hodiny} other {Začíná za # hodin}}",
|
||||
"starts_in_minutes": "{count, plural, one {Začíná za # minutu} few {Začíná za # minuty} many {Začíná za # minuty} other {Začíná za # minut}}"
|
||||
},
|
||||
"occurrence_not_found_again": "Změna nebyla uložena: opakování se na serveru nepodařilo znovu najít. Zkuste to znovu.",
|
||||
"participants": {
|
||||
"limit_warning": "Více než {max, plural, one {# účastník} few {# účastníci} many {# účastníka} other {# účastníků}} včetně organizátora: server může uložení této události odmítnout. Správce může limit zvýšit."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {Starter om # time} other {Starter om # timer}}",
|
||||
"starts_in_minutes": "{count, plural, one {Starter om # minut} other {Starter om # minutter}}"
|
||||
},
|
||||
"occurrence_not_found_again": "Ændringen blev ikke gemt: forekomsten kunne ikke findes igen på serveren. Prøv igen.",
|
||||
"participants": {
|
||||
"limit_warning": "Mere end {max, plural, one {# deltager} other {# deltagere}}, arrangøren medregnet: serveren kan afvise at gemme denne begivenhed. En administrator kan hæve grænsen."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {Beginnt in # Stunde} other {Beginnt in # Stunden}}",
|
||||
"starts_in_minutes": "{count, plural, one {Beginnt in # Minute} other {Beginnt in # Minuten}}"
|
||||
},
|
||||
"occurrence_not_found_again": "Die Änderung wurde nicht gespeichert: Der Termin konnte auf dem Server nicht wiedergefunden werden. Versuchen Sie es erneut.",
|
||||
"participants": {
|
||||
"limit_warning": "Mehr als {max, plural, one {# Teilnehmer} other {# Teilnehmer}}, den Organisator mitgezählt: Der Server lehnt das Speichern dieses Termins möglicherweise ab. Ihr Administrator kann das Limit erhöhen."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {Starts in # hour} other {Starts in # hours}}",
|
||||
"starts_in_minutes": "{count, plural, one {Starts in # minute} other {Starts in # minutes}}"
|
||||
},
|
||||
"occurrence_not_found_again": "The change was not saved: the occurrence could not be found again on the server. Please try again.",
|
||||
"participants": {
|
||||
"limit_warning": "More than {max, plural, one {# participant} other {# participants}}, including the organizer: the server may refuse to save this event. An administrator can raise the limit."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {Comienza en # hora} other {Comienza en # horas}}",
|
||||
"starts_in_minutes": "{count, plural, one {Comienza en # minuto} other {Comienza en # minutos}}"
|
||||
},
|
||||
"occurrence_not_found_again": "No se guardó el cambio: no se pudo volver a encontrar la repetición en el servidor. Inténtalo de nuevo.",
|
||||
"participants": {
|
||||
"limit_warning": "Más de {max, plural, one {# participante} other {# participantes}}, incluido el organizador: es posible que el servidor se niegue a guardar este evento. Un administrador puede aumentar el límite."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {# ساعت دیگر شروع میشود} other {# ساعت دیگر شروع میشود}}",
|
||||
"starts_in_minutes": "{count, plural, one {# دقیقه دیگر شروع میشود} other {# دقیقه دیگر شروع میشود}}"
|
||||
},
|
||||
"occurrence_not_found_again": "تغییر ذخیره نشد: این تکرار دوباره روی سرور پیدا نشد. دوباره تلاش کنید.",
|
||||
"participants": {
|
||||
"limit_warning": "بیش از {max, plural, one {# شرکتکننده} other {# شرکتکننده}}، با احتساب برگزارکننده: ممکن است سرور ذخیره این رویداد را رد کند. مدیر سیستم میتواند این محدودیت را افزایش دهد."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {Commence dans # heure} other {Commence dans # heures}}",
|
||||
"starts_in_minutes": "{count, plural, one {Commence dans # minute} other {Commence dans # minutes}}"
|
||||
},
|
||||
"occurrence_not_found_again": "La modification n'a pas été enregistrée : l'occurrence n'a pas pu être retrouvée sur le serveur. Réessayez.",
|
||||
"participants": {
|
||||
"limit_warning": "Plus de {max, plural, one {# participant} other {# participants}}, organisateur compris : le serveur risque de refuser d'enregistrer cet événement. Un administrateur peut relever la limite."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {מתחיל בעוד שעה} two {מתחיל בעוד שעתיים} other {מתחיל בעוד # שעות}}",
|
||||
"starts_in_minutes": "{count, plural, one {מתחיל בעוד דקה} two {מתחיל בעוד שתי דקות} other {מתחיל בעוד # דקות}}"
|
||||
},
|
||||
"occurrence_not_found_again": "השינוי לא נשמר: לא ניתן היה למצוא שוב את המופע בשרת. נסה שוב.",
|
||||
"participants": {
|
||||
"limit_warning": "יותר מ-{max, plural, one {# משתתף} other {# משתתפים}}, כולל המארגן: ייתכן שהשרת יסרב לשמור את האירוע. מנהל המערכת יכול להגדיל את המגבלה."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {# óra múlva kezdődik} other {# óra múlva kezdődik}}",
|
||||
"starts_in_minutes": "{count, plural, one {# perc múlva kezdődik} other {# perc múlva kezdődik}}"
|
||||
},
|
||||
"occurrence_not_found_again": "A módosítás nem lett mentve: az alkalmat nem sikerült újra megtalálni a szerveren. Próbáld újra.",
|
||||
"participants": {
|
||||
"limit_warning": "Több mint {max, plural, one {# résztvevő} other {# résztvevő}}, a szervezőt is beleértve: a szerver elutasíthatja az esemény mentését. A rendszergazda megemelheti a korlátot."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {Inizia tra # ora} other {Inizia tra # ore}}",
|
||||
"starts_in_minutes": "{count, plural, one {Inizia tra # minuto} other {Inizia tra # minuti}}"
|
||||
},
|
||||
"occurrence_not_found_again": "La modifica non è stata salvata: non è stato possibile ritrovare l'occorrenza sul server. Riprova.",
|
||||
"participants": {
|
||||
"limit_warning": "Più di {max, plural, one {# partecipante} other {# partecipanti}}, organizzatore incluso: il server potrebbe rifiutarsi di salvare questo evento. Un amministratore può aumentare il limite."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, other {#時間後に開始}}",
|
||||
"starts_in_minutes": "{count, plural, other {#分後に開始}}"
|
||||
},
|
||||
"occurrence_not_found_again": "変更は保存されませんでした:サーバーで対象の回をもう一度見つけられませんでした。もう一度お試しください。",
|
||||
"participants": {
|
||||
"limit_warning": "参加者が主催者を含めて{max, plural, other {#人}}を超えています。サーバーがこの予定の保存を拒否する可能性があります。管理者が上限を引き上げることができます。"
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, other {#시간 후 시작}}",
|
||||
"starts_in_minutes": "{count, plural, other {#분 후 시작}}"
|
||||
},
|
||||
"occurrence_not_found_again": "변경 사항이 저장되지 않았어요. 서버에서 해당 회차를 다시 찾을 수 없었어요. 다시 시도해 주세요.",
|
||||
"participants": {
|
||||
"limit_warning": "참석자가 주최자를 포함해 {max, plural, other {#명}}을 넘어요. 서버가 이 일정의 저장을 거부할 수 있어요. 관리자가 한도를 늘릴 수 있어요."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, zero {Sākas pēc # stundām} one {Sākas pēc # stundas} other {Sākas pēc # stundām}}",
|
||||
"starts_in_minutes": "{count, plural, zero {Sākas pēc # minūtēm} one {Sākas pēc # minūtes} other {Sākas pēc # minūtēm}}"
|
||||
},
|
||||
"occurrence_not_found_again": "Izmaiņas netika saglabātas: šo notikuma reizi serverī neizdevās atrast vēlreiz. Mēģiniet vēlreiz.",
|
||||
"participants": {
|
||||
"limit_warning": "Vairāk nekā {max, plural, one {# dalībnieks} other {# dalībnieki}}, ieskaitot organizatoru: serveris var atteikties saglabāt šo pasākumu. Administrators var palielināt ierobežojumu."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {# цагийн дараа эхэлнэ} other {# цагийн дараа эхэлнэ}}",
|
||||
"starts_in_minutes": "{count, plural, one {# минутын дараа эхэлнэ} other {# минутын дараа эхэлнэ}}"
|
||||
},
|
||||
"occurrence_not_found_again": "Өөрчлөлт хадгалагдсангүй: энэ удаагийн үйл явдлыг серверээс дахин олж чадсангүй. Дахин оролдоно уу.",
|
||||
"participants": {
|
||||
"limit_warning": "Зохион байгуулагчийг оролцуулаад {max, plural, other {#}}-аас олон оролцогч: сервер энэ үйл явдлыг хадгалахаас татгалзаж магадгүй. Администратор хязгаарыг нэмэгдүүлэх боломжтой."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {Starter om # time} other {Starter om # timer}}",
|
||||
"starts_in_minutes": "{count, plural, one {Starter om # minutt} other {Starter om # minutter}}"
|
||||
},
|
||||
"occurrence_not_found_again": "Endringen ble ikke lagret: forekomsten ble ikke funnet igjen på serveren. Prøv igjen.",
|
||||
"participants": {
|
||||
"limit_warning": "Mer enn {max, plural, one {# deltaker} other {# deltakere}}, arrangøren medregnet: serveren kan nekte å lagre denne hendelsen. En administrator kan øke grensen."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {Begint over # uur} other {Begint over # uur}}",
|
||||
"starts_in_minutes": "{count, plural, one {Begint over # minuut} other {Begint over # minuten}}"
|
||||
},
|
||||
"occurrence_not_found_again": "De wijziging is niet opgeslagen: de herhaling kon niet opnieuw op de server worden gevonden. Probeer het opnieuw.",
|
||||
"participants": {
|
||||
"limit_warning": "Meer dan {max, plural, one {# deelnemer} other {# deelnemers}}, de organisator meegeteld: de server kan weigeren dit evenement op te slaan. Een beheerder kan de limiet verhogen."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {Zaczyna się za # godzinę} few {Zaczyna się za # godziny} many {Zaczyna się za # godzin} other {Zaczyna się za # godziny}}",
|
||||
"starts_in_minutes": "{count, plural, one {Zaczyna się za # minutę} few {Zaczyna się za # minuty} many {Zaczyna się za # minut} other {Zaczyna się za # minuty}}"
|
||||
},
|
||||
"occurrence_not_found_again": "Zmiana nie została zapisana: nie udało się ponownie znaleźć wystąpienia na serwerze. Spróbuj ponownie.",
|
||||
"participants": {
|
||||
"limit_warning": "Ponad {max, plural, one {# uczestnika} few {# uczestników} many {# uczestników} other {# uczestnika}} łącznie z organizatorem: serwer może odmówić zapisania tego wydarzenia. Administrator może zwiększyć limit."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {Começa em # hora} other {Começa em # horas}}",
|
||||
"starts_in_minutes": "{count, plural, one {Começa em # minuto} other {Começa em # minutos}}"
|
||||
},
|
||||
"occurrence_not_found_again": "A alteração não foi salva: não foi possível encontrar a repetição novamente no servidor. Tente novamente.",
|
||||
"participants": {
|
||||
"limit_warning": "Mais de {max, plural, one {# participante} other {# participantes}}, incluindo o organizador: o servidor pode se recusar a salvar este evento. Um administrador pode aumentar o limite."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {Începe peste # oră} few {Începe peste # ore} other {Începe peste # de ore}}",
|
||||
"starts_in_minutes": "{count, plural, one {Începe peste # minut} few {Începe peste # minute} other {Începe peste # de minute}}"
|
||||
},
|
||||
"occurrence_not_found_again": "Modificarea nu a fost salvată: ocurența nu a mai putut fi găsită pe server. Încercați din nou.",
|
||||
"participants": {
|
||||
"limit_warning": "Mai mult de {max, plural, one {# participant} few {# participanți} other {# de participanți}}, inclusiv organizatorul: serverul poate refuza salvarea acestui eveniment. Un administrator poate mări limita."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {Начнётся через # час} few {Начнётся через # часа} many {Начнётся через # часов} other {Начнётся через # часа}}",
|
||||
"starts_in_minutes": "{count, plural, one {Начнётся через # минуту} few {Начнётся через # минуты} many {Начнётся через # минут} other {Начнётся через # минуты}}"
|
||||
},
|
||||
"occurrence_not_found_again": "Изменение не сохранено: не удалось снова найти это повторение на сервере. Попробуйте ещё раз.",
|
||||
"participants": {
|
||||
"limit_warning": "Больше {max, plural, one {# участника} few {# участников} many {# участников} other {# участника}}, включая организатора: сервер может отказаться сохранить это событие. Администратор может увеличить лимит."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {Začína o # hodinu} few {Začína o # hodiny} many {Začína o # hodiny} other {Začína o # hodín}}",
|
||||
"starts_in_minutes": "{count, plural, one {Začína o # minútu} few {Začína o # minúty} many {Začína o # minúty} other {Začína o # minút}}"
|
||||
},
|
||||
"occurrence_not_found_again": "Zmena sa neuložila: opakovanie sa na serveri nepodarilo znova nájsť. Skúste to znova.",
|
||||
"participants": {
|
||||
"limit_warning": "Viac ako {max, plural, one {# účastník} few {# účastníci} many {# účastníka} other {# účastníkov}} vrátane organizátora: server môže odmietnuť uloženie tejto udalosti. Správca môže limit zvýšiť."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {# saat sonra başlıyor} other {# saat sonra başlıyor}}",
|
||||
"starts_in_minutes": "{count, plural, one {# dakika sonra başlıyor} other {# dakika sonra başlıyor}}"
|
||||
},
|
||||
"occurrence_not_found_again": "Değişiklik kaydedilmedi: bu tekrar sunucuda yeniden bulunamadı. Tekrar deneyin.",
|
||||
"participants": {
|
||||
"limit_warning": "Organizatör dahil {max, plural, one {# katılımcıdan} other {# katılımcıdan}} fazla: sunucu bu etkinliği kaydetmeyi reddedebilir. Bir yönetici bu sınırı yükseltebilir."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, one {Почнеться через # годину} few {Почнеться через # години} many {Почнеться через # годин} other {Почнеться через # години}}",
|
||||
"starts_in_minutes": "{count, plural, one {Почнеться через # хвилину} few {Почнеться через # хвилини} many {Почнеться через # хвилин} other {Почнеться через # хвилини}}"
|
||||
},
|
||||
"occurrence_not_found_again": "Зміну не збережено: не вдалося знову знайти це повторення на сервері. Спробуйте ще раз.",
|
||||
"participants": {
|
||||
"limit_warning": "Понад {max, plural, one {# учасника} few {# учасників} many {# учасників} other {# учасника}}, включно з організатором: сервер може відмовитися зберегти цю подію. Адміністратор може збільшити ліміт."
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, other {# 小時後開始}}",
|
||||
"starts_in_minutes": "{count, plural, other {# 分鐘後開始}}"
|
||||
},
|
||||
"occurrence_not_found_again": "變更未儲存:無法在伺服器上再次找到這一次的活動。請再試一次。",
|
||||
"participants": {
|
||||
"limit_warning": "參與者(含主辦人)超過 {max, plural, other {# 位}}:伺服器可能會拒絕儲存此活動。管理員可以提高此限制。"
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"starts_in_hours": "{count, plural, other {# 小时后开始}}",
|
||||
"starts_in_minutes": "{count, plural, other {# 分钟后开始}}"
|
||||
},
|
||||
"occurrence_not_found_again": "更改未保存:无法在服务器上再次找到这一次的活动。请重试。",
|
||||
"participants": {
|
||||
"limit_warning": "参与者(含组织者)超过 {max, plural, other {# 位}}:服务器可能会拒绝保存此活动。管理员可以提高此限制。"
|
||||
},
|
||||
|
||||
@@ -92,7 +92,8 @@ beforeEach(() => {
|
||||
describe.each(['0.16.23', '1.0'])('a first change to one server occurrence on Stalwart %s', () => {
|
||||
it('writes the override as it stands, then sends the change to its re-read id', async () => {
|
||||
useCalendarStore.setState({ events: [serverOccurrence()] });
|
||||
// The new override gives the occurrence another synthetic id.
|
||||
// The new override gives the occurrence another synthetic id (positional
|
||||
// ids, up to 0.16.20; 0.16.21 and 1.0 keep it, see below).
|
||||
mockQueryExpanded.mockResolvedValue(['s9', 'other']);
|
||||
mockGetEvents.mockImplementation(async (ids: string[]) => (ids.includes('s9')
|
||||
? [
|
||||
@@ -113,6 +114,18 @@ describe.each(['0.16.23', '1.0'])('a first change to one server occurrence on St
|
||||
expect(mockGetEvents).toHaveBeenCalledWith(['s9', 'other'], undefined, { expanded: true });
|
||||
});
|
||||
|
||||
it('sends the change to the same id when the override keeps it (recurrence-keyed ids, live)', async () => {
|
||||
useCalendarStore.setState({ events: [serverOccurrence()] });
|
||||
mockQueryExpanded.mockResolvedValue(['s1']);
|
||||
mockGetEvents.mockResolvedValue([{ id: 's1', baseEventId: 'base', recurrenceId: RID, start: RID, duration: 'PT1H' } as CalendarEvent]);
|
||||
|
||||
await useCalendarStore.getState().updateEvent('s1', { title: 'Standup, room 2', participants } as Partial<CalendarEvent>, {
|
||||
sendSchedulingMessages: true,
|
||||
});
|
||||
|
||||
expect(mockUpdate.mock.calls.map(([id, , schedule]) => [id, schedule])).toEqual([['s1', false], ['s1', true]]);
|
||||
});
|
||||
|
||||
it('sends the change once when it asks the attendees again (a move)', async () => {
|
||||
useCalendarStore.setState({ events: [serverOccurrence()] });
|
||||
|
||||
@@ -142,15 +155,40 @@ describe.each(['0.16.23', '1.0'])('a first change to one server occurrence on St
|
||||
expect(mockUpdate.mock.calls.map(([id, , schedule]) => [id, schedule])).toEqual([['s1', false]]);
|
||||
});
|
||||
|
||||
it('sends the change to the old id when the occurrence cannot be found again', async () => {
|
||||
// Up to 0.16.20 Stalwart's synthetic ids are positions in the series'
|
||||
// expansion (calendar_event/mod.rs): after the new override "s1" can name
|
||||
// the next occurrence, which must not get the change and its scheduling
|
||||
// messages. 0.16.21 and 1.0 keep the id; the lookup finds it again there.
|
||||
it.each([
|
||||
['is not listed', () => mockQueryExpanded.mockResolvedValue(null)],
|
||||
['cannot be read', () => mockQueryExpanded.mockRejectedValue(new Error('Network request failed'))],
|
||||
['is listed twice', () => {
|
||||
mockQueryExpanded.mockResolvedValue(['s9', 's10']);
|
||||
mockGetEvents.mockResolvedValue([
|
||||
{ id: 's9', baseEventId: 'base', recurrenceId: RID, start: RID } as CalendarEvent,
|
||||
{ id: 's10', baseEventId: 'base', recurrenceId: RID, start: RID } as CalendarEvent,
|
||||
]);
|
||||
}],
|
||||
])('sends nothing more, and says so, when the occurrence %s after its quiet override', async (_what, arrange) => {
|
||||
useCalendarStore.setState({ events: [serverOccurrence()] });
|
||||
mockQueryExpanded.mockResolvedValue(null);
|
||||
arrange();
|
||||
const refresh = vi.fn(async () => undefined);
|
||||
const { refresh: storeRefresh } = useCalendarStore.getState();
|
||||
useCalendarStore.setState({ refresh });
|
||||
|
||||
await useCalendarStore.getState().updateEvent('s1', { title: 'Standup, room 2', participants } as Partial<CalendarEvent>, {
|
||||
sendSchedulingMessages: true,
|
||||
});
|
||||
try {
|
||||
await expect(useCalendarStore.getState().updateEvent('s1', { title: 'Standup, room 2', participants } as Partial<CalendarEvent>, {
|
||||
sendSchedulingMessages: true,
|
||||
})).rejects.toThrow('The change was not saved: the occurrence could not be found again on the server. Please try again.');
|
||||
} finally {
|
||||
useCalendarStore.setState({ refresh: storeRefresh });
|
||||
}
|
||||
|
||||
expect(mockUpdate.mock.calls.map(([id, , schedule]) => [id, schedule])).toEqual([['s1', false], ['s1', true]]);
|
||||
// Only the override as the occurrence stands, which reaches no one.
|
||||
expect(mockUpdate.mock.calls.map(([id, , schedule]) => [id, schedule])).toEqual([['s1', false]]);
|
||||
// The ids in view may name other occurrences now: read them again.
|
||||
expect(refresh).toHaveBeenCalled();
|
||||
expect(useCalendarStore.getState().events[0].title).toBe('Standup');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -519,7 +519,8 @@ async function updateOccurrence(
|
||||
|
||||
/**
|
||||
* The server occurrence of `instance`'s series at the same recurrence id, read
|
||||
* again: a new override can give an occurrence another synthetic id.
|
||||
* again: a new override can give an occurrence another synthetic id. Null
|
||||
* when the series lists none there, or more than one.
|
||||
*/
|
||||
async function findServerOccurrence(
|
||||
instance: CalendarEvent,
|
||||
@@ -531,7 +532,24 @@ async function findServerOccurrence(
|
||||
const ids = await queryExpandedEvents(new Date(at - DAY_MS).toISOString(), new Date(at + DAY_MS).toISOString(), accountId);
|
||||
if (!ids?.length) return null;
|
||||
const found = await fetchEvents(ids, accountId, { expanded: true });
|
||||
return found.find((e) => e.baseEventId === instance.baseEventId && e.recurrenceId === instance.recurrenceId) ?? null;
|
||||
const matches = found.filter((e) => e.baseEventId === instance.baseEventId && e.recurrenceId === instance.recurrenceId);
|
||||
return matches.length === 1 ? matches[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A first change to one occurrence could not be sent: after its quiet
|
||||
* override was written, the occurrence was not found again (see
|
||||
* updateOccurrenceInTwoSteps). That override changes nothing that shows, and
|
||||
* a retry updates it, which reaches every attendee.
|
||||
*/
|
||||
export class OccurrenceNotFoundAgainError extends Error {
|
||||
constructor() {
|
||||
super(t(
|
||||
'calendar.occurrence_not_found_again',
|
||||
'The change was not saved: the occurrence could not be found again on the server. Please try again.',
|
||||
));
|
||||
this.name = 'OccurrenceNotFoundAgainError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -541,7 +559,12 @@ async function findServerOccurrence(
|
||||
* change; an update of an existing override reaches everyone. So the override
|
||||
* is written first as the occurrence stands, without scheduling, and the
|
||||
* change then goes out as an update of it. The occurrence is looked up again
|
||||
* in between: the new override can give it another synthetic id.
|
||||
* in between: the new override can give it another synthetic id. When that
|
||||
* fails, the change is not sent to the old id: up to 0.16.20 Stalwart's
|
||||
* synthetic ids are positions in the series' expansion (calendar_event/
|
||||
* mod.rs), and after the new override the old one can name the next
|
||||
* occurrence, which would get the change and its scheduling messages.
|
||||
* 0.16.21 and 1.0 key them by recurrence id, and the id stays (live).
|
||||
*/
|
||||
async function updateOccurrenceInTwoSteps(
|
||||
instance: CalendarEvent,
|
||||
@@ -552,7 +575,8 @@ async function updateOccurrenceInTwoSteps(
|
||||
): Promise<void> {
|
||||
await updateOccurrence(instance, syntheticId, (options) => withNewOverrideDetails(instance, {}, options), false, accountId);
|
||||
const current = await findServerOccurrence(instance, accountId).catch(() => null);
|
||||
await updateOccurrence(current ?? instance, current?.id ?? syntheticId, () => patch, sendSchedulingMessages, accountId);
|
||||
if (!current) throw new OccurrenceNotFoundAgainError();
|
||||
await updateOccurrence(current, current.id, () => patch, sendSchedulingMessages, accountId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -945,7 +969,14 @@ export const useCalendarStore = create<CalendarState>()(
|
||||
await stopSeriesDefaultAlerts(storeEvent.baseEventId, accountId);
|
||||
}
|
||||
if (twoSteps) {
|
||||
await updateOccurrenceInTwoSteps(storeEvent, realId, patch as Partial<CalendarEvent>, schedule, accountId);
|
||||
try {
|
||||
await updateOccurrenceInTwoSteps(storeEvent, realId, patch as Partial<CalendarEvent>, schedule, accountId);
|
||||
} catch (err) {
|
||||
// Its quiet override is on the server, and the series' occurrences
|
||||
// may have other ids now: a retry must not go to the ones in view.
|
||||
if (err instanceof OccurrenceNotFoundAgainError) await get().refresh().catch(() => undefined);
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
await updateOccurrence(
|
||||
storeEvent,
|
||||
|
||||
Reference in New Issue
Block a user