From a071d0581c4449921baf181cccd5b7700be3095f Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:03:24 +0200 Subject: [PATCH] feat: refetch filters and vacation on push and fan state changes out to screens --- App.tsx | 12 ++++++++++++ src/lib/state-change-bus.ts | 38 +++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/lib/state-change-bus.ts diff --git a/App.tsx b/App.tsx index a3c2e13e..c014aac2 100644 --- a/App.tsx +++ b/App.tsx @@ -10,6 +10,9 @@ import { Mail, Calendar, BookUser, HardDrive, Settings } from 'lucide-react-nati import { startLiveUpdates, type LiveUpdatesHandle } from './src/api/push-stream'; import { jmapClient } from './src/api/jmap-client'; import type { StateChange } from './src/api/types'; +import { dispatchStateChange } from './src/lib/state-change-bus'; +import { useFilterStore } from './src/stores/filter-store'; +import { useVacationStore } from './src/stores/vacation-store'; import { addMessageListener, addNotificationTapListener, @@ -406,10 +409,19 @@ export default function App() { let appActive = AppState.currentState !== 'background' && AppState.currentState !== 'inactive'; const onStateChange = async (change: StateChange) => { + // Filters / vacation edited elsewhere (webmail, another device): refetch + // so the settings panes don't save stale state over the newer script. + const primary = (() => { try { return jmapClient.accountId; } catch { return null; } })(); + const own = primary ? change.changed?.[primary] : undefined; + const extra: Promise[] = []; + if (own?.SieveScript) extra.push(useFilterStore.getState().fetchFilters().catch(() => undefined)); + if (own?.VacationResponse) extra.push(useVacationStore.getState().fetch().catch(() => undefined)); + dispatchStateChange(change); await Promise.all([ useEmailStore.getState().handleStateChange(change), useContactsStore.getState().handleStateChange(change), useCalendarStore.getState().handleStateChange(change), + ...extra, ]); }; diff --git a/src/lib/state-change-bus.ts b/src/lib/state-change-bus.ts new file mode 100644 index 00000000..87a2b7b2 --- /dev/null +++ b/src/lib/state-change-bus.ts @@ -0,0 +1,38 @@ +// Fan-out of JMAP StateChange events to screens that have no store of their +// own (Scheduled list, Files). The mail/contacts/calendar stores subscribe +// directly in App.tsx; anything else listens here for the type it cares about. + +import type { StateChange } from '../api/types'; + +type Listener = (accountId: string, state: string, change: StateChange) => void; + +const listeners = new Map>(); + +/** Subscribe to changes of one JMAP type (e.g. `EmailSubmission`, `FileNode`). */ +export function onStateChangeType(type: string, listener: Listener): () => void { + let set = listeners.get(type); + if (!set) { + set = new Set(); + listeners.set(type, set); + } + set.add(listener); + return () => { + set?.delete(listener); + }; +} + +export function dispatchStateChange(change: StateChange): void { + for (const [accountId, types] of Object.entries(change.changed ?? {})) { + for (const [type, state] of Object.entries(types ?? {})) { + const set = listeners.get(type); + if (!set) continue; + for (const l of set) { + try { + l(accountId, state, change); + } catch { + // a broken listener must not break the others + } + } + } + } +}