Remember an added address book when the server will not
"You are not allowed to modify this address book." That is Stalwart's answer to a sharee subscribing to a book shared read-only, and it is a fair one: `isSubscribed` lives on the collection rather than on the reader, so adding one is a write to the *owner's* account. The identical write on a shared calendar is accepted. The difference is the server's. So the flag is still asked for first -- a preference the server holds is one every client agrees about -- and when it is refused the answer goes in the reader's own synced settings instead, as `addedShares`, keyed by account and collection. Either record counts as added, and the rule has a test of its own because three components ask the question and they must not drift apart. Two things about how this hid. The refusal arrives as a *successful* response with the id in `notUpdated`, so the version that ignored it saw nothing wrong and the button simply did nothing -- fixed a commit ago, and it is what turned "the + does nothing in Firefox" into a sentence from the server. And it cannot be seen from the owner's account at all, where the write succeeds: it took two browsers signed in as two accounts to find, which is why it survived every check made from one. The mock refuses the same write for the same reason. One that accepted it would have gone on agreeing with the belief that shipped. Verified against it: adding the shared book is refused by the server, recorded in settings, and the book moves to "Shared with me" with its contacts reaching the To field; removing undoes all three; and it survives a full page reload, which is the point of putting it where the settings live rather than in this tab.
This commit is contained in:
@@ -2,8 +2,7 @@ import { create } from "zustand";
|
||||
import { CAP, client, setErrorMessage } from "@/jmap/client";
|
||||
import type { BusyPeriod, Calendar, CalendarEvent, GetResponse, Id, JSCalendarParticipant, JSCalendarRecurrenceRule, ParticipantIdentity, QueryResponse, SetResponse } from "@/jmap/types";
|
||||
import { toUTCDate, toLocalDateTime, zonedToDate, parseDuration, DAY_MS, browserTimeZone } from "@/lib/dates";
|
||||
import { settings } from "./settings";
|
||||
import { toast } from "@/ui/toast";
|
||||
import { settings, useSettings } from "./settings";
|
||||
import { useSession } from "./session";
|
||||
|
||||
export interface EventInstance {
|
||||
@@ -150,13 +149,26 @@ export const useCalendar = create<CalendarState>((set, get) => ({
|
||||
// See the note in the contacts store: subscribing writes to another
|
||||
// account, so a refusal is an ordinary answer and arrives in `notUpdated`
|
||||
// rather than as a thrown error.
|
||||
/*
|
||||
* Server first, settings when it refuses -- the same arrangement the
|
||||
* contacts store explains. Stalwart takes this write on a shared calendar
|
||||
* where it will not on a shared address book, but the difference is the
|
||||
* server's to change and not worth relying on from here.
|
||||
*/
|
||||
let stored = false;
|
||||
try {
|
||||
const res = await client.call<SetResponse>("Calendar/set", { accountId, update: { [calendarId]: { isSubscribed: subscribed } } });
|
||||
const err = res.notUpdated?.[calendarId];
|
||||
if (err) throw new Error(setErrorMessage(err));
|
||||
} catch (err) {
|
||||
toast.error(`Could not ${subscribed ? "add" : "remove"} that calendar: ${(err as Error).message}`);
|
||||
return;
|
||||
stored = true;
|
||||
} catch {
|
||||
stored = false;
|
||||
}
|
||||
if (!stored) {
|
||||
const added = new Set(settings().addedShares);
|
||||
if (subscribed) added.add(sharedKey(accountId, calendarId));
|
||||
else added.delete(sharedKey(accountId, calendarId));
|
||||
useSettings.getState().update({ addedShares: [...added] });
|
||||
}
|
||||
set((s) => ({
|
||||
sharedCalendars: s.sharedCalendars.map((c) =>
|
||||
@@ -281,8 +293,13 @@ export const useCalendar = create<CalendarState>((set, get) => ({
|
||||
an account linked for its files offered its calendar too. `isSubscribed`
|
||||
is the only thing separating "shared with me" from "reachable", so
|
||||
nothing unsubscribed is drawn. */
|
||||
const added = new Set(settings().addedShares);
|
||||
const theirs: Record<Id, Calendar> = {};
|
||||
for (const c of sharedCalendars) if (c.accountId === accountId && c.calendar.isSubscribed) theirs[c.calendar.id] = c.calendar;
|
||||
for (const c of sharedCalendars) {
|
||||
if (c.accountId !== accountId) continue;
|
||||
if (!c.calendar.isSubscribed && !added.has(sharedKey(c.accountId, c.calendar.id))) continue;
|
||||
theirs[c.calendar.id] = c.calendar;
|
||||
}
|
||||
if (calId && !theirs[calId]) continue;
|
||||
const inst = toInstance(e, theirs);
|
||||
if (!inst) continue;
|
||||
|
||||
Reference in New Issue
Block a user