fix: update the sync settings' collection count after the chooser listed new ones #34

The account row in the sync settings counts the synced address books
or calendars from its own copy of the list, loaded when the row
appears. The chooser loads a fresh list when it opens, so a calendar
created meanwhile showed there but not in the count ("4 of 5 sync"
stayed after a sixth appeared, and turning it on or off changed
nothing) until the settings page was opened again.

Every fresh list now reaches every view of that account's collections,
so the count follows the chooser.
This commit is contained in:
Linus Rath
2026-09-27 11:23:44 +02:00
parent e09640cfc2
commit 157ea398b8
3 changed files with 39 additions and 0 deletions
@@ -7,6 +7,7 @@ import {
cachedSyncCollections,
CollectionsError,
listSyncCollections,
onSyncCollectionsLoaded,
type CollectionsErrorKind,
type SyncCollection,
} from '../../../device-sync/app/collections';
@@ -56,6 +57,14 @@ export function useSyncCollections(registryId: string, authority: Authority, act
return () => { cancelled = true; };
}, [registryId, authority, active, attempt, feeds]);
// The chooser loads its own, fresher list: the row's counts follow it.
React.useEffect(
() => onSyncCollectionsLoaded((id, auth, collections) => {
if (id === registryId && auth === authority) setState({ kind: 'loaded', collections });
}),
[registryId, authority],
);
const reload = React.useCallback(() => setAttempt((n) => n + 1), []);
return { state, reload };
}
@@ -3,6 +3,7 @@ import {
CollectionsError,
forgetSyncCollections,
listSyncCollections,
onSyncCollectionsLoaded,
otherAccountsSyncing,
type CollectionsClient,
} from '../../app/collections';
@@ -78,6 +79,19 @@ describe('listSyncCollections', () => {
expect(calls[0][1].properties).toEqual(expect.arrayContaining(['id', 'name', 'myRights']));
});
it('tells every view of the account when a fresh list arrived', async () => {
const { client } = fakeClient({
'AddressBook/get c': () => ({ list: [{ id: 'b1', name: 'Personal' }] }),
'AddressBook/get team': () => ({ list: [] }),
});
const seen: string[][] = [];
const stop = onSyncCollectionsLoaded((id, authority, list) => seen.push([id, authority, ...list.map((c) => c.key)]));
await listSyncCollections('alice@x', CONTACTS_AUTHORITY, { client });
stop();
await listSyncCollections('alice@x', CONTACTS_AUTHORITY, { client, force: true });
expect(seen).toEqual([['alice@x', CONTACTS_AUTHORITY, 'c/b1']]);
});
it('remembers the personal JMAP account for the push routes', async () => {
const { client } = fakeClient({ 'AddressBook/get': () => ({ list: [] }) });
await listSyncCollections('alice@x', CONTACTS_AUTHORITY, { client });
+16
View File
@@ -92,8 +92,23 @@ const CALENDAR_PROPERTIES = ['id', 'name', 'color', 'isDefault', 'sortOrder', 'i
const cache = new Map<string, { at: number; collections: SyncCollection[] }>();
const inFlight = new Map<string, Promise<SyncCollection[]>>();
type LoadedListener = (registryId: string, authority: Authority, collections: SyncCollection[]) => void;
const loadedListeners = new Set<LoadedListener>();
const cacheKey = (registryId: string, authority: Authority) => `${registryId}\n${authority}`;
/**
* Calls `listener` with every list loaded from the server, so each view of an
* account's collections (the settings row's counts, the chooser) shows the
* freshest one. Returns the unsubscribe.
*/
export function onSyncCollectionsLoaded(listener: LoadedListener): () => void {
loadedListeners.add(listener);
return () => {
loadedListeners.delete(listener);
};
}
/** The last list loaded for an account, if it is still fresh. */
export function cachedSyncCollections(registryId: string, authority: Authority): SyncCollection[] | null {
const hit = cache.get(cacheKey(registryId, authority));
@@ -122,6 +137,7 @@ export function listSyncCollections(
}
const run = load(registryId, authority, options).then((collections) => {
cache.set(key, { at: Date.now(), collections });
for (const listener of [...loadedListeners]) listener(registryId, authority, collections);
return collections;
});
if (!options.client) {