@rapidmx/web-client 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/apps/admin/audit-log/index.tsx +35 -8
  2. package/apps/admin/data-requests/index.tsx +480 -485
  3. package/apps/admin/distribution-lists/[uid].tsx +19 -1
  4. package/apps/admin/escrow-scopes/[uid].tsx +348 -237
  5. package/apps/admin/escrow-scopes/new/index.tsx +8 -2
  6. package/apps/admin/index.tsx +19 -1
  7. package/apps/admin/ingest-queue/index.tsx +44 -5
  8. package/apps/admin/mailboxes/[uid].tsx +211 -184
  9. package/apps/admin/quarantine/index.tsx +46 -14
  10. package/apps/admin/transport-rules/[uid].tsx +30 -1
  11. package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
  12. package/apps/admin/transport-rules/index.tsx +24 -6
  13. package/apps/admin/transport-rules/new/index.tsx +30 -1
  14. package/apps/escrow/_layout.tsx +2 -3
  15. package/apps/escrow/audit-log/index.tsx +196 -168
  16. package/apps/escrow/matters/[uid].tsx +621 -521
  17. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
  18. package/apps/shared/components/admin/layout/AdminShell.tsx +3 -1
  19. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
  20. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  21. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
  22. package/apps/shared/components/admin/settings/BrandingForm.tsx +15 -4
  23. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +18 -7
  24. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +10 -2
  25. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +6 -0
  26. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +45 -12
  27. package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -595
  28. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -98
  29. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +49 -13
  30. package/apps/shared/components/admin/setup/SetupWizard.tsx +150 -34
  31. package/apps/shared/components/admin/signOut.ts +79 -0
  32. package/apps/shared/components/admin/usePagedList.tsx +129 -0
  33. package/apps/shared/components/calendar/EventModal.tsx +717 -549
  34. package/apps/shared/components/calendar/MonthView.tsx +173 -139
  35. package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
  36. package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
  37. package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
  38. package/apps/shared/components/calendar/allDay.ts +124 -0
  39. package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
  40. package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
  41. package/apps/shared/components/contacts/ContactForm.tsx +377 -348
  42. package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
  43. package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
  44. package/apps/shared/components/contacts/contactKeys.ts +78 -0
  45. package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
  46. package/apps/shared/components/layout/AppShell.tsx +87 -15
  47. package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
  48. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +389 -298
  49. package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
  50. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
  51. package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -128
  52. package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
  53. package/apps/shared/components/mail/MessageDetailPane.tsx +1318 -630
  54. package/apps/shared/components/mail/compose/ComposeContext.tsx +20 -15
  55. package/apps/shared/components/mail/compose/ComposeWindow.tsx +854 -132
  56. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
  57. package/apps/shared/components/mail/layout/MailShell.tsx +12 -2
  58. package/apps/shared/components/mail/pinnedSigners.ts +124 -0
  59. package/apps/shared/components/mail/verificationSeals.ts +125 -0
  60. package/apps/shared/components/mail/writableMailboxes.ts +101 -0
  61. package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
  62. package/apps/shared/mail/listAllPages.ts +39 -0
  63. package/apps/shared/search/LocalIndexLifecycle.tsx +64 -55
  64. package/apps/shared/search/localIndexBuilder.ts +481 -179
  65. package/apps/shared/search/localIndexRpcClient.ts +228 -28
  66. package/apps/shared/search/localIndexSchema.ts +15 -6
  67. package/apps/shared/search/localIndexStorage.ts +98 -0
  68. package/apps/shared/search/localIndexVFS.ts +32 -1
  69. package/apps/shared/search/localIndexWorker.ts +648 -186
  70. package/apps/www/calendar/index.tsx +438 -417
  71. package/apps/www/contacts/[uid].tsx +21 -2
  72. package/apps/www/contacts/index.tsx +116 -12
  73. package/apps/www/index.tsx +1314 -1041
  74. package/apps/www/messages/[uid].tsx +18 -6
  75. package/apps/www/settings/auto-reply/index.tsx +134 -129
  76. package/apps/www/settings/booking-types/[uid].tsx +306 -300
  77. package/apps/www/settings/encryption/index.tsx +739 -216
  78. package/apps/www/settings/filters/[uid].tsx +171 -155
  79. package/apps/www/settings/filters/new/index.tsx +8 -1
  80. package/apps/www/settings/focused-inbox/index.tsx +150 -148
  81. package/apps/www/settings/privacy/index.tsx +494 -436
  82. package/apps/www/settings/read-receipts/index.tsx +148 -125
  83. package/apps/www/settings/sharing/index.tsx +17 -14
  84. package/apps/www/tasks/index.tsx +638 -579
  85. package/dist/apps/admin/audit-log/index.js +38 -8
  86. package/dist/apps/admin/data-requests/index.js +52 -72
  87. package/dist/apps/admin/distribution-lists/[uid].js +1 -1
  88. package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
  89. package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
  90. package/dist/apps/admin/index.js +4 -1
  91. package/dist/apps/admin/ingest-queue/index.js +24 -6
  92. package/dist/apps/admin/mailboxes/[uid].js +20 -4
  93. package/dist/apps/admin/quarantine/index.js +20 -11
  94. package/dist/apps/admin/transport-rules/[uid].js +18 -2
  95. package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
  96. package/dist/apps/admin/transport-rules/index.js +22 -6
  97. package/dist/apps/admin/transport-rules/new/index.js +18 -2
  98. package/dist/apps/escrow/_layout.js +3 -3
  99. package/dist/apps/escrow/audit-log/index.js +26 -1
  100. package/dist/apps/escrow/matters/[uid].js +77 -42
  101. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
  102. package/dist/apps/shared/components/admin/layout/AdminShell.js +3 -1
  103. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
  104. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  105. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
  106. package/dist/apps/shared/components/admin/settings/BrandingForm.js +15 -4
  107. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +13 -7
  108. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +7 -2
  109. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +6 -0
  110. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +34 -12
  111. package/dist/apps/shared/components/admin/settings/PluginsManager.js +390 -78
  112. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +40 -11
  113. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +29 -13
  114. package/dist/apps/shared/components/admin/setup/SetupWizard.js +87 -21
  115. package/dist/apps/shared/components/admin/signOut.js +75 -0
  116. package/dist/apps/shared/components/admin/usePagedList.js +99 -0
  117. package/dist/apps/shared/components/calendar/EventModal.js +142 -21
  118. package/dist/apps/shared/components/calendar/MonthView.js +12 -5
  119. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
  120. package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
  121. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
  122. package/dist/apps/shared/components/calendar/allDay.js +111 -0
  123. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
  124. package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
  125. package/dist/apps/shared/components/contacts/ContactForm.js +32 -10
  126. package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
  127. package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
  128. package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
  129. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
  130. package/dist/apps/shared/components/layout/AppShell.js +83 -14
  131. package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
  132. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +49 -12
  133. package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
  134. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
  135. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +73 -14
  136. package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
  137. package/dist/apps/shared/components/mail/MessageDetailPane.js +441 -36
  138. package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
  139. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +699 -128
  140. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
  141. package/dist/apps/shared/components/mail/layout/MailShell.js +12 -3
  142. package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
  143. package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
  144. package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
  145. package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
  146. package/dist/apps/shared/mail/listAllPages.js +25 -0
  147. package/dist/apps/shared/search/LocalIndexLifecycle.js +58 -54
  148. package/dist/apps/shared/search/localIndexBuilder.js +292 -37
  149. package/dist/apps/shared/search/localIndexRpcClient.js +188 -25
  150. package/dist/apps/shared/search/localIndexSchema.js +12 -6
  151. package/dist/apps/shared/search/localIndexStorage.js +86 -0
  152. package/dist/apps/shared/search/localIndexVFS.js +68 -40
  153. package/dist/apps/shared/search/localIndexWorker.js +519 -173
  154. package/dist/apps/www/calendar/index.js +28 -10
  155. package/dist/apps/www/contacts/[uid].js +13 -2
  156. package/dist/apps/www/contacts/index.js +80 -10
  157. package/dist/apps/www/index.js +339 -108
  158. package/dist/apps/www/messages/[uid].js +18 -6
  159. package/dist/apps/www/settings/auto-reply/index.js +9 -4
  160. package/dist/apps/www/settings/booking-types/[uid].js +17 -12
  161. package/dist/apps/www/settings/encryption/index.js +514 -97
  162. package/dist/apps/www/settings/filters/[uid].js +22 -7
  163. package/dist/apps/www/settings/filters/new/index.js +8 -2
  164. package/dist/apps/www/settings/focused-inbox/index.js +3 -1
  165. package/dist/apps/www/settings/privacy/index.js +57 -22
  166. package/dist/apps/www/settings/read-receipts/index.js +10 -3
  167. package/dist/apps/www/settings/sharing/index.js +7 -11
  168. package/dist/apps/www/tasks/index.js +50 -10
  169. package/package.json +2 -2
@@ -0,0 +1,39 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+
6
+ /** Page size the list pages (Contacts, Tasks) request per call - restapi caps `limit` at 500. */
7
+ export const LIST_PAGE_SIZE = 500;
8
+
9
+ /** Safety cap on pages fetched - stops a server that ignores `page` (and keeps returning a full first
10
+ * page) from looping forever. 40 pages x 500 = 20,000 items. */
11
+ export const MAX_LIST_PAGES = 40;
12
+
13
+ export interface ListAllPagesResult<T> {
14
+ items: T[];
15
+ /** `true` when fetching stopped at `maxPages` with the last page still full - there may be more items
16
+ * the caller isn't showing, so it should say so rather than presenting `items` as the whole list. */
17
+ truncated: boolean;
18
+ }
19
+
20
+ /**
21
+ * Fetches every page of a zero-based `limit`/`page` list endpoint, stopping at the first short page
22
+ * (fewer than `pageSize` items) - so a folder with more items than one page holds is shown in full
23
+ * rather than silently cut off at the first page. Stops at `maxPages` regardless, reporting `truncated`.
24
+ */
25
+ export async function listAllPages<T>(
26
+ fetchPage: (page: number) => Promise<T[]>,
27
+ pageSize: number = LIST_PAGE_SIZE,
28
+ maxPages: number = MAX_LIST_PAGES,
29
+ ): Promise<ListAllPagesResult<T>> {
30
+ const items: T[] = [];
31
+ for (let page = 0; page < maxPages; page++) {
32
+ const batch = await fetchPage(page);
33
+ items.push(...batch);
34
+ if (batch.length < pageSize) {
35
+ return { items, truncated: false };
36
+ }
37
+ }
38
+ return { items, truncated: true };
39
+ }
@@ -3,87 +3,96 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  /**
6
- * Mounts the Tier 2 local index's lifecycle for one mailbox: starts the background build once it's
7
- * unlocked, and destroys the index on the same events that destroy the unlocked keys themselves (spec
8
- * §11 "MUST be destroyed on the same events that destroy private keys: explicit logout, session
9
- * revocation, and the configurable idle timeout").
6
+ * Mounts the Tier 2 local index's lifecycle: starts the background build for the active mailbox once it's
7
+ * unlocked, and destroys an index on the same events that destroy that mailbox's unlocked keys (spec §11
8
+ * "MUST be destroyed on the same events that destroy private keys: explicit logout, session revocation,
9
+ * and the configurable idle timeout").
10
10
  *
11
11
  * **Why polling, not hooking the existing destroy call sites directly**: `destroyUnlockedKeys()`
12
- * (`react-shared/src/crypto/keySession.ts`) has no observer/callback hook, and its own two real call
13
- * sites are `useIdleKeyTimeout.ts` (in `react-shared`, not this repo) and the manual "Destroy keys now"
14
- * button in Settings - editing the former would mean a react-shared source change that (per this
15
- * session's own earlier finding) doesn't reach `web-client` without a package republish + patch refresh
16
- * cycle, which is out of scope for wiring up a destroy signal. Watching `getUnlockedKeys()` for a
17
- * present-to-absent transition instead catches *every* destroy path uniformly - idle timeout, the manual
12
+ * (`react-shared/src/crypto/keySession.ts`) has no observer/callback hook. Watching `getUnlockedKeys()`
13
+ * for a present-to-absent transition catches *every* destroy path uniformly - idle timeout, the manual
18
14
  * button, or anything else that calls it - without needing to know which one fired.
19
15
  *
20
- * **Explicit logout is the one gap this polling doesn't reliably cover**: `AppShell.tsx`'s sign-out
21
- * navigates away immediately, which may not leave time for a poll tick to fire first. That case is
22
- * handled separately, directly in `AppShell.tsx`, via `destroyAllLocalIndexes()` - see that call site's
23
- * own comment.
16
+ * Every mailbox this component has seen unlocked is tracked for the life of the mount - not just whichever
17
+ * is active right now - so switching mailboxes (or a folders-list refresh re-running the build effect)
18
+ * can never lose a pending unlocked-to-locked transition for a mailbox whose index was already built.
19
+ *
20
+ * **Explicit logout** navigates away immediately, which may not leave time for a poll tick - `AppShell.tsx`
21
+ * handles that case itself via `destroyAllLocalIndexes()`.
24
22
  */
25
23
  import { useEffect, useRef } from "react";
26
24
  import { getUnlockedKeys } from "@rapidmx/react-shared/crypto/keySession.js";
27
25
  import type { PublicKey } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
28
26
  import type { Folder } from "@rapidmx/react-shared/mail/mailApi.js";
29
- import { buildLocalIndex } from "./localIndexBuilder.js";
30
- import { destroyLocalIndex } from "./localIndexRpcClient.js";
27
+ import { buildLocalIndex, cancelLocalIndexBuild } from "./localIndexBuilder.js";
28
+ import { destroyLocalIndex, pruneInaccessibleLocalIndexes } from "./localIndexRpcClient.js";
31
29
 
32
- const POLL_INTERVAL_MS = 5_000;
30
+ export const POLL_INTERVAL_MS = 5_000;
33
31
 
34
32
  export interface LocalIndexLifecycleProps {
35
33
  mailboxUid?: string;
36
34
  mailboxKeys?: PublicKey[];
37
35
  folders: Folder[];
36
+ /** Every mailbox the signed-in user can currently access. Once known, indexes on this device for any
37
+ * other mailbox (e.g. left behind by a different user who never signed out) are removed. */
38
+ accessibleMailboxUids?: string[];
38
39
  }
39
40
 
40
- /** Renders nothing - pure side-effect component, mounted by `MailShell.tsx` (which is where a
41
- * `mailboxUid` is actually known; `AppShell.tsx` itself is mailbox-agnostic, shared by every app). Each
42
- * `buildLocalIndex()` call below omits its `windowConfig` argument deliberately - that leaves the byte
43
- * budget to its own default, which already resolves per-device (Web vs. Electron) and per-user
44
- * preference (Settings > Encryption) on its own; see `localIndexBuilder.ts`/`localIndexSizePreference.ts`
45
- * for how. */
46
- export default function LocalIndexLifecycle({ mailboxUid, folders }: LocalIndexLifecycleProps) {
47
- // Guards against re-triggering a build every time this component re-renders (e.g. on an unrelated
48
- // folders-list refresh) for a mailbox already built/building this session.
49
- const buildStartedForRef = useRef<string | undefined>(undefined);
50
- const wasUnlockedRef = useRef(false);
41
+ /** Renders nothing - pure side-effect component, mounted by `MailShell.tsx`. */
42
+ export default function LocalIndexLifecycle({ mailboxUid, folders, accessibleMailboxUids }: LocalIndexLifecycleProps) {
43
+ // mailboxUid -> whether it was unlocked at the last check. An entry exists for every mailbox whose
44
+ // build this component started; only ever changed by a real observed transition, never reset by an
45
+ // effect re-run.
46
+ const trackedRef = useRef(new Map<string, boolean>());
47
+ // The latest props, for the long-lived polling interval below.
48
+ const latestRef = useRef({ mailboxUid, folders });
49
+ latestRef.current = { mailboxUid, folders };
51
50
 
52
- useEffect(() => {
53
- if (!mailboxUid || folders.length === 0) {
51
+ function startBuildIfUnlocked(targetMailboxUid: string, targetFolders: Folder[]) {
52
+ const unlocked = getUnlockedKeys(targetMailboxUid);
53
+ if (!unlocked || targetFolders.length === 0 || trackedRef.current.get(targetMailboxUid)) {
54
54
  return;
55
55
  }
56
- const unlocked = getUnlockedKeys(mailboxUid);
57
- wasUnlockedRef.current = !!unlocked;
58
- if (unlocked && buildStartedForRef.current !== mailboxUid) {
59
- buildStartedForRef.current = mailboxUid;
60
- // Never an unhandled rejection - a broken local index (Worker/WASM/OPFS unsupported or
61
- // unavailable, a corrupted store) is best-effort infrastructure, not a build the rest of the
62
- // app depends on. searchTier2.ts's own callers already degrade gracefully independent of
63
- // whether a build ever completed at all.
64
- buildLocalIndex(mailboxUid, unlocked, folders).catch(() => undefined);
56
+ trackedRef.current.set(targetMailboxUid, true);
57
+ // Never an unhandled rejection - a broken local index is best-effort infrastructure, not a build
58
+ // the rest of the app depends on.
59
+ buildLocalIndex(targetMailboxUid, unlocked, targetFolders).catch(() => undefined);
60
+ }
61
+
62
+ useEffect(() => {
63
+ if (mailboxUid) {
64
+ startBuildIfUnlocked(mailboxUid, folders);
65
65
  }
66
+ }, [mailboxUid, folders]);
66
67
 
68
+ useEffect(() => {
67
69
  const interval = setInterval(() => {
68
- const stillUnlocked = !!getUnlockedKeys(mailboxUid);
69
- if (wasUnlockedRef.current && !stillUnlocked) {
70
- // A present-to-absent transition: something just destroyed this mailbox's unlocked keys
71
- // (idle timeout or the manual "Destroy keys now" button - see this module's own doc
72
- // comment on why this is observed rather than hooked directly).
73
- void destroyLocalIndex(mailboxUid);
74
- buildStartedForRef.current = undefined;
75
- } else if (!wasUnlockedRef.current && stillUnlocked && buildStartedForRef.current !== mailboxUid) {
76
- // The mailbox was re-unlocked this session (e.g. via ComposeWindow's or
77
- // MessageDetailPane's own on-demand unlock prompt) - start the build it missed the first
78
- // time around.
79
- buildStartedForRef.current = mailboxUid;
80
- buildLocalIndex(mailboxUid, getUnlockedKeys(mailboxUid)!, folders).catch(() => undefined);
70
+ for (const [trackedUid, wasUnlocked] of trackedRef.current) {
71
+ if (wasUnlocked && !getUnlockedKeys(trackedUid)) {
72
+ // A present-to-absent transition: something just destroyed this mailbox's keys.
73
+ trackedRef.current.set(trackedUid, false);
74
+ // Stop the pass still running with the now-destroyed keys; the destroy itself also makes
75
+ // the Worker reject anything that pass still sends.
76
+ void cancelLocalIndexBuild(trackedUid);
77
+ void destroyLocalIndex(trackedUid);
78
+ }
79
+ }
80
+ // The active mailbox was (re-)unlocked since the last check, e.g. via an on-demand unlock prompt.
81
+ const { mailboxUid: activeUid, folders: activeFolders } = latestRef.current;
82
+ if (activeUid) {
83
+ startBuildIfUnlocked(activeUid, activeFolders);
81
84
  }
82
- wasUnlockedRef.current = stillUnlocked;
83
85
  }, POLL_INTERVAL_MS);
84
-
85
86
  return () => clearInterval(interval);
86
- }, [mailboxUid, folders]);
87
+ }, []);
88
+
89
+ // A stable string key so a re-render with an equal-but-new array doesn't re-scan OPFS.
90
+ const accessibleKey = accessibleMailboxUids && accessibleMailboxUids.length > 0 ? JSON.stringify([...accessibleMailboxUids].sort()) : "";
91
+ useEffect(() => {
92
+ if (accessibleKey) {
93
+ void pruneInaccessibleLocalIndexes(JSON.parse(accessibleKey) as string[]);
94
+ }
95
+ }, [accessibleKey]);
87
96
 
88
97
  return null;
89
98
  }