@rapidmx/web-client 0.4.0 → 0.6.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 (320) hide show
  1. package/README.md +88 -0
  2. package/apps/admin/audit-log/index.tsx +35 -8
  3. package/apps/admin/data-requests/index.tsx +480 -485
  4. package/apps/admin/distribution-lists/[uid].tsx +19 -1
  5. package/apps/admin/escrow-scopes/[uid].tsx +348 -237
  6. package/apps/admin/escrow-scopes/new/index.tsx +8 -2
  7. package/apps/admin/index.tsx +19 -1
  8. package/apps/admin/ingest-queue/index.tsx +44 -5
  9. package/apps/admin/mailboxes/[uid].tsx +211 -184
  10. package/apps/admin/quarantine/index.tsx +46 -14
  11. package/apps/admin/transport-rules/[uid].tsx +30 -1
  12. package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
  13. package/apps/admin/transport-rules/index.tsx +24 -6
  14. package/apps/admin/transport-rules/new/index.tsx +30 -1
  15. package/apps/escrow/_layout.tsx +2 -3
  16. package/apps/escrow/audit-log/index.tsx +196 -168
  17. package/apps/escrow/matters/[uid].tsx +621 -521
  18. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
  19. package/apps/shared/components/admin/layout/AdminShell.tsx +28 -9
  20. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
  21. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  22. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
  23. package/apps/shared/components/admin/settings/BrandingForm.tsx +15 -4
  24. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +18 -7
  25. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +10 -2
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +6 -0
  27. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +45 -12
  28. package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -595
  29. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -98
  30. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +49 -13
  31. package/apps/shared/components/admin/setup/SetupWizard.tsx +150 -34
  32. package/apps/shared/components/admin/signOut.ts +79 -0
  33. package/apps/shared/components/admin/usePagedList.tsx +129 -0
  34. package/apps/shared/components/calendar/EventModal.tsx +717 -549
  35. package/apps/shared/components/calendar/MonthView.tsx +173 -139
  36. package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
  37. package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
  38. package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
  39. package/apps/shared/components/calendar/allDay.ts +124 -0
  40. package/apps/shared/components/calendar/layout/CalendarShell.tsx +194 -192
  41. package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
  42. package/apps/shared/components/contacts/ContactForm.tsx +377 -348
  43. package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
  44. package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
  45. package/apps/shared/components/contacts/contactKeys.ts +78 -0
  46. package/apps/shared/components/contacts/layout/ContactsShell.tsx +199 -197
  47. package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
  48. package/apps/shared/components/layout/AppShell.tsx +123 -33
  49. package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
  50. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +389 -298
  51. package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
  52. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
  53. package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -128
  54. package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
  55. package/apps/shared/components/mail/MessageDetailPane.tsx +1318 -630
  56. package/apps/shared/components/mail/compose/ComposeContext.tsx +20 -15
  57. package/apps/shared/components/mail/compose/ComposeWindow.tsx +854 -132
  58. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
  59. package/apps/shared/components/mail/layout/MailShell.tsx +420 -408
  60. package/apps/shared/components/mail/pinnedSigners.ts +124 -0
  61. package/apps/shared/components/mail/verificationSeals.ts +125 -0
  62. package/apps/shared/components/mail/writableMailboxes.ts +101 -0
  63. package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
  64. package/apps/shared/components/settings/layout/SettingsShell.tsx +23 -9
  65. package/apps/shared/components/tasks/layout/TasksShell.tsx +201 -199
  66. package/apps/shared/mail/listAllPages.ts +39 -0
  67. package/apps/shared/plugins/pluginNav.ts +73 -0
  68. package/apps/shared/search/LocalIndexLifecycle.tsx +64 -55
  69. package/apps/shared/search/localIndexBuilder.ts +481 -179
  70. package/apps/shared/search/localIndexRpcClient.ts +231 -29
  71. package/apps/shared/search/localIndexSchema.ts +15 -6
  72. package/apps/shared/search/localIndexStorage.ts +98 -0
  73. package/apps/shared/search/localIndexVFS.ts +32 -1
  74. package/apps/shared/search/localIndexWorker.ts +648 -186
  75. package/apps/www/calendar/index.tsx +438 -417
  76. package/apps/www/contacts/[uid].tsx +21 -2
  77. package/apps/www/contacts/index.tsx +116 -12
  78. package/apps/www/index.tsx +1314 -1041
  79. package/apps/www/messages/[uid].tsx +18 -6
  80. package/apps/www/settings/auto-reply/index.tsx +134 -129
  81. package/apps/www/settings/encryption/index.tsx +739 -216
  82. package/apps/www/settings/filters/[uid].tsx +171 -155
  83. package/apps/www/settings/filters/new/index.tsx +8 -1
  84. package/apps/www/settings/focused-inbox/index.tsx +150 -148
  85. package/apps/www/settings/privacy/index.tsx +494 -436
  86. package/apps/www/settings/read-receipts/index.tsx +148 -125
  87. package/apps/www/settings/sharing/index.tsx +17 -14
  88. package/apps/www/tasks/index.tsx +638 -579
  89. package/dist/apps/admin/_404.d.ts +2 -0
  90. package/dist/apps/admin/_500.d.ts +2 -0
  91. package/dist/apps/admin/_layout.d.ts +8 -0
  92. package/dist/apps/admin/audit-log/index.d.ts +11 -0
  93. package/dist/apps/admin/audit-log/index.js +38 -8
  94. package/dist/apps/admin/branding/index.d.ts +3 -0
  95. package/dist/apps/admin/data-requests/index.d.ts +3 -0
  96. package/dist/apps/admin/data-requests/index.js +52 -72
  97. package/dist/apps/admin/distribution-lists/[uid].d.ts +7 -0
  98. package/dist/apps/admin/distribution-lists/[uid].js +1 -1
  99. package/dist/apps/admin/distribution-lists/index.d.ts +3 -0
  100. package/dist/apps/admin/distribution-lists/new/index.d.ts +3 -0
  101. package/dist/apps/admin/domains/[uid].d.ts +7 -0
  102. package/dist/apps/admin/domains/index.d.ts +3 -0
  103. package/dist/apps/admin/domains/new/index.d.ts +3 -0
  104. package/dist/apps/admin/encryption-policy/index.d.ts +3 -0
  105. package/dist/apps/admin/escrow-scopes/[uid].d.ts +21 -0
  106. package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
  107. package/dist/apps/admin/escrow-scopes/index.d.ts +3 -0
  108. package/dist/apps/admin/escrow-scopes/new/index.d.ts +3 -0
  109. package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
  110. package/dist/apps/admin/index.d.ts +3 -0
  111. package/dist/apps/admin/index.js +4 -1
  112. package/dist/apps/admin/ingest-queue/index.d.ts +4 -0
  113. package/dist/apps/admin/ingest-queue/index.js +24 -6
  114. package/dist/apps/admin/mailbox-policy/index.d.ts +3 -0
  115. package/dist/apps/admin/mailboxes/[uid].d.ts +7 -0
  116. package/dist/apps/admin/mailboxes/[uid].js +20 -4
  117. package/dist/apps/admin/mailboxes/new/index.d.ts +3 -0
  118. package/dist/apps/admin/plugins/index.d.ts +3 -0
  119. package/dist/apps/admin/quarantine/index.d.ts +6 -0
  120. package/dist/apps/admin/quarantine/index.js +20 -11
  121. package/dist/apps/admin/retention-policy/index.d.ts +3 -0
  122. package/dist/apps/admin/setup/index.d.ts +3 -0
  123. package/dist/apps/admin/transport-rules/[uid].d.ts +7 -0
  124. package/dist/apps/admin/transport-rules/[uid].js +18 -2
  125. package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +21 -0
  126. package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
  127. package/dist/apps/admin/transport-rules/index.d.ts +3 -0
  128. package/dist/apps/admin/transport-rules/index.js +22 -6
  129. package/dist/apps/admin/transport-rules/new/index.d.ts +3 -0
  130. package/dist/apps/admin/transport-rules/new/index.js +18 -2
  131. package/dist/apps/escrow/_layout.d.ts +10 -0
  132. package/dist/apps/escrow/_layout.js +3 -3
  133. package/dist/apps/escrow/audit-log/index.d.ts +9 -0
  134. package/dist/apps/escrow/audit-log/index.js +26 -1
  135. package/dist/apps/escrow/index.d.ts +3 -0
  136. package/dist/apps/escrow/matters/[uid].d.ts +7 -0
  137. package/dist/apps/escrow/matters/[uid].js +77 -42
  138. package/dist/apps/escrow/matters/new/index.d.ts +3 -0
  139. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.d.ts +17 -0
  140. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +32 -0
  141. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
  142. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +32 -0
  143. package/dist/apps/shared/components/admin/layout/AdminShell.js +15 -6
  144. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +12 -0
  145. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
  146. package/dist/apps/shared/components/admin/mailboxes/MailboxTable.d.ts +6 -0
  147. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.d.ts +19 -0
  148. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  149. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +12 -0
  150. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
  151. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +9 -0
  152. package/dist/apps/shared/components/admin/settings/BrandingForm.js +15 -4
  153. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +12 -0
  154. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +13 -7
  155. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +12 -0
  156. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +7 -2
  157. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.d.ts +9 -0
  158. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.d.ts +19 -0
  159. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +6 -0
  160. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +10 -0
  161. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +34 -12
  162. package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +4 -0
  163. package/dist/apps/shared/components/admin/settings/PluginsManager.js +390 -78
  164. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +9 -0
  165. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +40 -11
  166. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.d.ts +14 -0
  167. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +29 -13
  168. package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +36 -0
  169. package/dist/apps/shared/components/admin/setup/SetupWizard.js +87 -21
  170. package/dist/apps/shared/components/admin/signOut.d.ts +23 -0
  171. package/dist/apps/shared/components/admin/signOut.js +75 -0
  172. package/dist/apps/shared/components/admin/usePagedList.d.ts +39 -0
  173. package/dist/apps/shared/components/admin/usePagedList.js +99 -0
  174. package/dist/apps/shared/components/calendar/CalendarListSidebar.d.ts +26 -0
  175. package/dist/apps/shared/components/calendar/EventModal.d.ts +46 -0
  176. package/dist/apps/shared/components/calendar/EventModal.js +142 -21
  177. package/dist/apps/shared/components/calendar/MonthView.d.ts +15 -0
  178. package/dist/apps/shared/components/calendar/MonthView.js +12 -5
  179. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +20 -0
  180. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
  181. package/dist/apps/shared/components/calendar/ResourcePicker.d.ts +19 -0
  182. package/dist/apps/shared/components/calendar/SplitDayView.d.ts +28 -0
  183. package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
  184. package/dist/apps/shared/components/calendar/TimeGridView.d.ts +17 -0
  185. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
  186. package/dist/apps/shared/components/calendar/allDay.d.ts +51 -0
  187. package/dist/apps/shared/components/calendar/allDay.js +111 -0
  188. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +40 -0
  189. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +3 -3
  190. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +23 -0
  191. package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
  192. package/dist/apps/shared/components/contacts/ContactForm.d.ts +20 -0
  193. package/dist/apps/shared/components/contacts/ContactForm.js +32 -10
  194. package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +40 -0
  195. package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
  196. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +25 -0
  197. package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +40 -0
  198. package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
  199. package/dist/apps/shared/components/contacts/contactKeys.d.ts +29 -0
  200. package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
  201. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +21 -0
  202. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +2 -2
  203. package/dist/apps/shared/components/escrow/layout/EscrowShell.d.ts +25 -0
  204. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
  205. package/dist/apps/shared/components/forms/StringListField.d.ts +22 -0
  206. package/dist/apps/shared/components/layout/AppShell.d.ts +49 -0
  207. package/dist/apps/shared/components/layout/AppShell.js +97 -26
  208. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +18 -0
  209. package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
  210. package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +51 -0
  211. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +49 -12
  212. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +13 -0
  213. package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
  214. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +66 -0
  215. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
  216. package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +45 -0
  217. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +73 -14
  218. package/dist/apps/shared/components/layout/UserMenu.d.ts +23 -0
  219. package/dist/apps/shared/components/mail/ConversationList.d.ts +11 -0
  220. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +23 -0
  221. package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
  222. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +93 -0
  223. package/dist/apps/shared/components/mail/MessageDetailPane.js +441 -36
  224. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +58 -0
  225. package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
  226. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +15 -0
  227. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +37 -0
  228. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +699 -128
  229. package/dist/apps/shared/components/mail/compose/EmojiPicker.d.ts +14 -0
  230. package/dist/apps/shared/components/mail/compose/GifPicker.d.ts +13 -0
  231. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +34 -0
  232. package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.d.ts +16 -0
  233. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +27 -0
  234. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
  235. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +57 -0
  236. package/dist/apps/shared/components/mail/layout/MailShell.js +13 -4
  237. package/dist/apps/shared/components/mail/pinnedSigners.d.ts +33 -0
  238. package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
  239. package/dist/apps/shared/components/mail/verificationSeals.d.ts +22 -0
  240. package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
  241. package/dist/apps/shared/components/mail/writableMailboxes.d.ts +15 -0
  242. package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
  243. package/dist/apps/shared/components/rules/RuleBuilder.d.ts +70 -0
  244. package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
  245. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +41 -0
  246. package/dist/apps/shared/components/settings/layout/SettingsShell.js +19 -9
  247. package/dist/apps/shared/components/tasks/TasksSidebar.d.ts +41 -0
  248. package/dist/apps/shared/components/tasks/TasksToolbar.d.ts +14 -0
  249. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +24 -0
  250. package/dist/apps/shared/components/tasks/layout/TasksShell.js +2 -2
  251. package/dist/apps/shared/mail/findWellKnownFolderUid.d.ts +5 -0
  252. package/dist/apps/shared/mail/listAllPages.d.ts +17 -0
  253. package/dist/apps/shared/mail/listAllPages.js +25 -0
  254. package/dist/apps/shared/plugins/pluginNav.d.ts +39 -0
  255. package/dist/apps/shared/plugins/pluginNav.js +33 -0
  256. package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +13 -0
  257. package/dist/apps/shared/search/LocalIndexLifecycle.js +58 -54
  258. package/dist/apps/shared/search/localIndexBlockCipher.d.ts +36 -0
  259. package/dist/apps/shared/search/localIndexBuilder.d.ts +85 -0
  260. package/dist/apps/shared/search/localIndexBuilder.js +292 -37
  261. package/dist/apps/shared/search/localIndexKey.d.ts +4 -0
  262. package/dist/apps/shared/search/localIndexRpcClient.d.ts +71 -0
  263. package/dist/apps/shared/search/localIndexRpcClient.js +191 -26
  264. package/dist/apps/shared/search/localIndexSchema.d.ts +111 -0
  265. package/dist/apps/shared/search/localIndexSchema.js +12 -6
  266. package/dist/apps/shared/search/localIndexSizePreference.d.ts +27 -0
  267. package/dist/apps/shared/search/localIndexStorage.d.ts +28 -0
  268. package/dist/apps/shared/search/localIndexStorage.js +86 -0
  269. package/dist/apps/shared/search/localIndexVFS.d.ts +101 -0
  270. package/dist/apps/shared/search/localIndexVFS.js +68 -40
  271. package/dist/apps/shared/search/localIndexWorker.d.ts +145 -0
  272. package/dist/apps/shared/search/localIndexWorker.js +519 -173
  273. package/dist/apps/shared/search/searchTier2.d.ts +30 -0
  274. package/dist/apps/www/_404.d.ts +2 -0
  275. package/dist/apps/www/_500.d.ts +2 -0
  276. package/dist/apps/www/_layout.d.ts +14 -0
  277. package/dist/apps/www/calendar/index.d.ts +3 -0
  278. package/dist/apps/www/calendar/index.js +28 -10
  279. package/dist/apps/www/contacts/[uid].d.ts +13 -0
  280. package/dist/apps/www/contacts/[uid].js +13 -2
  281. package/dist/apps/www/contacts/index.d.ts +3 -0
  282. package/dist/apps/www/contacts/index.js +80 -10
  283. package/dist/apps/www/index.d.ts +3 -0
  284. package/dist/apps/www/index.js +339 -108
  285. package/dist/apps/www/messages/[uid].d.ts +11 -0
  286. package/dist/apps/www/messages/[uid].js +18 -6
  287. package/dist/apps/www/settings/auto-reply/index.d.ts +4 -0
  288. package/dist/apps/www/settings/auto-reply/index.js +9 -4
  289. package/dist/apps/www/settings/encryption/index.d.ts +7 -0
  290. package/dist/apps/www/settings/encryption/index.js +514 -97
  291. package/dist/apps/www/settings/filters/[uid].d.ts +8 -0
  292. package/dist/apps/www/settings/filters/[uid].js +22 -7
  293. package/dist/apps/www/settings/filters/_mailFilterRuleConfig.d.ts +11 -0
  294. package/dist/apps/www/settings/filters/index.d.ts +4 -0
  295. package/dist/apps/www/settings/filters/new/index.d.ts +4 -0
  296. package/dist/apps/www/settings/filters/new/index.js +8 -2
  297. package/dist/apps/www/settings/focused-inbox/index.d.ts +4 -0
  298. package/dist/apps/www/settings/focused-inbox/index.js +3 -1
  299. package/dist/apps/www/settings/labels/index.d.ts +4 -0
  300. package/dist/apps/www/settings/privacy/index.d.ts +4 -0
  301. package/dist/apps/www/settings/privacy/index.js +57 -22
  302. package/dist/apps/www/settings/read-receipts/index.d.ts +4 -0
  303. package/dist/apps/www/settings/read-receipts/index.js +10 -3
  304. package/dist/apps/www/settings/sharing/index.d.ts +4 -0
  305. package/dist/apps/www/settings/sharing/index.js +7 -11
  306. package/dist/apps/www/settings/signatures/[uid].d.ts +8 -0
  307. package/dist/apps/www/settings/signatures/index.d.ts +4 -0
  308. package/dist/apps/www/settings/signatures/new/index.d.ts +4 -0
  309. package/dist/apps/www/settings/signatures/signatureDefaults.d.ts +10 -0
  310. package/dist/apps/www/tasks/index.d.ts +3 -0
  311. package/dist/apps/www/tasks/index.js +50 -10
  312. package/package.json +3 -3
  313. package/apps/shared/components/booking/AvailabilityEditor.tsx +0 -118
  314. package/apps/www/settings/booking-types/[uid].tsx +0 -300
  315. package/apps/www/settings/booking-types/index.tsx +0 -98
  316. package/apps/www/settings/booking-types/new/index.tsx +0 -212
  317. package/dist/apps/shared/components/booking/AvailabilityEditor.js +0 -45
  318. package/dist/apps/www/settings/booking-types/[uid].js +0 -129
  319. package/dist/apps/www/settings/booking-types/index.js +0 -29
  320. package/dist/apps/www/settings/booking-types/new/index.js +0 -86
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Tier 2 (`specs/search.md` §6) - the local encrypted index's public entry point, mirroring
3
+ * `@rapidmx/react-shared`'s `searchTier3.ts#searchEncryptedCandidates()` in shape: given a parsed query
4
+ * and this session's unlocked keys, return already-scored, already-sourced results plus how much of the
5
+ * mailbox the local index actually covers right now.
6
+ *
7
+ * Returns `{ results: [], coverage: undefined }` - never throws, never rejects - both when `unlocked` is
8
+ * absent (nothing is indexed without a master key to derive the index key from, the same "silently
9
+ * contributes nothing" degradation `searchTier3.ts`'s own doc comment describes for its identical case)
10
+ * and when anything about the local index itself fails (Worker/WASM/OPFS unsupported or unavailable in
11
+ * this browser, a corrupted index mid-rebuild, a query timeout). This tier's own storage engine is
12
+ * strictly best-effort infrastructure Tier 1 doesn't depend on - a caller `Promise.all()`-ing this
13
+ * alongside Tier 1/Tier 3 (`apps/www/index.tsx`'s own search orchestration) must never see the *whole
14
+ * search* fail just because this one, most novel piece had a bad moment.
15
+ */
16
+ import type { UnlockedKeys } from "@rapidmx/react-shared/crypto/keySession.js";
17
+ import type { ParsedSearchQuery } from "@rapidmx/react-shared/search/queryGrammar.js";
18
+ import type { SearchResult } from "@rapidmx/react-shared/search/searchApi.js";
19
+ import type { Coverage } from "./localIndexWorker.js";
20
+ export interface Tier2SearchOutcome {
21
+ results: SearchResult[];
22
+ /** `undefined` when `unlocked` was absent - there's no local index to report coverage for at all in
23
+ * that case, distinct from a real, empty (just-built) index. */
24
+ coverage?: Coverage;
25
+ /** `true` when at least one more match exists beyond `results` - §8's composite pagination cursor's
26
+ * own per-tier continuation signal. Always `false` alongside the `unlocked`-absent/error
27
+ * degradations below - there is nothing more to page through either way. */
28
+ hasMore: boolean;
29
+ }
30
+ export declare function searchLocalIndex(mailboxUid: string, parsed: ParsedSearchQuery, unlocked: UnlockedKeys | undefined, limit?: number, offset?: number): Promise<Tier2SearchOutcome>;
@@ -0,0 +1,2 @@
1
+ import React from "react";
2
+ export default function NotFoundPage(): React.JSX.Element;
@@ -0,0 +1,2 @@
1
+ import React from "react";
2
+ export default function ErrorPage(): React.JSX.Element;
@@ -0,0 +1,14 @@
1
+ import React, { PropsWithChildren } from "react";
2
+ import { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
3
+ export interface LayoutProps {
4
+ /**
5
+ * Supplied by `WwwRoute`'s `fetchProps()` override (`src/mongo/routes/wwwRoute.ts`/
6
+ * `src/sql/routes/wwwRoute.ts`) - `@rapidrest/react` spreads the same merged page props onto this
7
+ * layout as onto the page it wraps, so it renders correctly server-side on the very first byte of the
8
+ * response: no client-side flash from the stock defaults below, and a crawler reading the raw HTML
9
+ * sees the real branding too. `undefined` only on the framework's unwrapped `_500` fallback path,
10
+ * which doesn't receive props at all - falling back to the stock defaults there is acceptable.
11
+ */
12
+ branding?: Branding;
13
+ }
14
+ export default function Layout({ children, branding }: PropsWithChildren<LayoutProps>): React.JSX.Element;
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import { CalendarShellProps } from "../../shared/components/calendar/layout/CalendarShell.js";
3
+ export default function CalendarPage(props: CalendarShellProps): React.JSX.Element;
@@ -3,10 +3,10 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
- import { useEffect, useMemo, useState } from "react";
6
+ import { useEffect, useMemo, useRef, useState } from "react";
7
7
  import { DndContext, MouseSensor, TouchSensor, useSensor, useSensors } from "@dnd-kit/core";
8
8
  import { HiOutlineBars3 } from "react-icons/hi2";
9
- import { addDays, addMonths, addWeeks, eachDayOfInterval, endOfDay, endOfMonth, endOfWeek, format, startOfDay, startOfMonth, startOfWeek, } from "date-fns";
9
+ import { addDays, addMonths, addWeeks, eachDayOfInterval, endOfDay, endOfMonth, endOfWeek, format, parseISO, startOfDay, startOfMonth, startOfWeek, } from "date-fns";
10
10
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
11
11
  import { listCalendarEvents } from "@rapidmx/react-shared/calendar/calendarApi.js";
12
12
  import { moveOccurrence, resizeOccurrenceEnd } from "@rapidmx/react-shared/calendar/calendarMutations.js";
@@ -16,6 +16,7 @@ import { expandAllOccurrences } from "@rapidmx/react-shared/calendar/recurrence.
16
16
  import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
17
17
  import CalendarShell, { useCalendarShell } from "../../shared/components/calendar/layout/CalendarShell.js";
18
18
  import CalendarListSidebar from "../../shared/components/calendar/CalendarListSidebar.js";
19
+ import { useWritableMailboxes } from "../../shared/components/mail/writableMailboxes.js";
19
20
  import EventModal from "../../shared/components/calendar/EventModal.js";
20
21
  import MiniDatePicker from "@rapidmx/react-shared/components/pickers/MiniDatePicker.js";
21
22
  import MonthView from "../../shared/components/calendar/MonthView.js";
@@ -24,11 +25,11 @@ import TimeGridView from "../../shared/components/calendar/TimeGridView.js";
24
25
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
25
26
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
26
27
  export default function CalendarPage(props) {
27
- return (_jsx(CalendarShell, { ...props, children: _jsx(CalendarContent, {}) }));
28
+ return (_jsx(CalendarShell, { ...props, children: _jsx(CalendarContent, { userUid: props.userUid }) }));
28
29
  }
29
30
  const VIEW_TYPES = ["month", "week", "workWeek", "day", "split"];
30
31
  const VIEW_LABELS = { month: "Month", week: "Week", workWeek: "Work Week", day: "Day", split: "Split" };
31
- function CalendarContent() {
32
+ function CalendarContent({ userUid }) {
32
33
  const { mailboxUid, folderUid, calendarFolders, mailboxCalendars, mailboxes, reloadFolders, colorFor } = useCalendarShell();
33
34
  // `PointerSensor` alone activates a drag on the very first touch-move, indistinguishable from a
34
35
  // scroll gesture on a touch device. `MouseSensor` (a small `distance` — desktop drags still start
@@ -55,7 +56,9 @@ function CalendarContent() {
55
56
  setView(requestedView);
56
57
  }
57
58
  const requestedDate = params.get("date");
58
- const parsedDate = requestedDate ? new Date(requestedDate) : null;
59
+ // `parseISO()`, not `new Date()` - `?date=2026-06-16` is a local calendar day, but `new Date()` reads a
60
+ // date-only string as UTC midnight, i.e. the previous day anywhere west of UTC.
61
+ const parsedDate = requestedDate ? parseISO(requestedDate) : null;
59
62
  if (parsedDate && !isNaN(parsedDate.getTime())) {
60
63
  setViewDate(startOfDay(parsedDate));
61
64
  }
@@ -100,7 +103,13 @@ function CalendarContent() {
100
103
  const end = endOfDay(viewDate);
101
104
  return { rangeStart: start, rangeEnd: end, days: [start] };
102
105
  }, [view, viewDate]);
106
+ // Only the most recent reload() may apply its results - toggling calendars (or saving/dragging an event)
107
+ // while an earlier, slower fetch is still in flight must not let that stale response overwrite newer
108
+ // events. (No date range is passed: `listCalendarEvents()` deliberately fetches the whole calendar and
109
+ // filters client-side - see its own doc comment on the server-side range query it works around.)
110
+ const reloadSeqRef = useRef(0);
103
111
  function reload() {
112
+ const seq = ++reloadSeqRef.current;
104
113
  const targets = calendarFolders.filter((f) => checkedFolderUids.has(f.uid));
105
114
  if (targets.length === 0) {
106
115
  setEvents([]);
@@ -111,6 +120,9 @@ function CalendarContent() {
111
120
  setError(null);
112
121
  void Promise.allSettled(targets.map((f) => listCalendarEvents(f.uid)))
113
122
  .then((results) => {
123
+ if (seq !== reloadSeqRef.current) {
124
+ return;
125
+ }
114
126
  const loaded = [];
115
127
  const failures = [];
116
128
  results.forEach((result, i) => {
@@ -137,7 +149,11 @@ function CalendarContent() {
137
149
  setError(`Could not load events for: ${failures.map((f) => f.name).join(", ")}.`);
138
150
  }
139
151
  })
140
- .finally(() => setLoading(false));
152
+ .finally(() => {
153
+ if (seq === reloadSeqRef.current) {
154
+ setLoading(false);
155
+ }
156
+ });
141
157
  }
142
158
  // Keyed on stable string summaries (not the array/set objects themselves, which are new references
143
159
  // every render) so this only re-fires when the actual set of folders to fetch changes.
@@ -215,10 +231,12 @@ function CalendarContent() {
215
231
  const modalFolderUid = modal ? (modal.targetFolderUid ?? modal.occurrence?.folderUid ?? folderUid) : undefined;
216
232
  const modalMailboxUid = calendarFolders.find((f) => f.uid === modalFolderUid)?.mailboxUid ?? mailboxUid;
217
233
  const modalMailbox = mailboxes.find((mb) => mb.uid === modalMailboxUid);
218
- // Mailboxes a new event can be created in - only those with at least one calendar.
234
+ // Mailboxes a new event can be created in - only those with at least one calendar that the caller can
235
+ // write to (a view-only share is left out; see writableMailboxes.ts).
236
+ const writableMailboxes = useWritableMailboxes(mailboxes, userUid, modalMailboxUid);
219
237
  const mailboxOptions = useMemo(() => mailboxCalendars
220
- .filter((mc) => mc.calendarFolders.length > 0)
221
- .map((mc) => ({ mailbox: mc.mailbox, calendars: mc.calendarFolders.map((f) => ({ uid: f.uid, name: f.name })) })), [mailboxCalendars]);
238
+ .filter((mc) => mc.calendarFolders.length > 0 && writableMailboxes.some((mb) => mb.uid === mc.mailbox.uid))
239
+ .map((mc) => ({ mailbox: mc.mailbox, calendars: mc.calendarFolders.map((f) => ({ uid: f.uid, name: f.name })) })), [mailboxCalendars, writableMailboxes]);
222
240
  const sidebarContent = (_jsxs(_Fragment, { children: [_jsx(MiniDatePicker, { selected: viewDate, onSelect: (date) => setViewDate(startOfDay(date)) }), _jsx(CalendarListSidebar, { mailboxCalendars: mailboxCalendars, checkedFolderUids: checkedFolderUids, onToggle: toggleCalendar, onAddCalendar: handleAddCalendar, colorFor: colorFor })] }));
223
241
  return (_jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsx("div", { className: "hidden md:flex w-56 shrink-0 border-r border-border flex-col overflow-y-auto", children: sidebarContent }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Calendars", children: _jsx("div", { className: "flex flex-col", children: sidebarContent }) }), _jsxs("div", { className: "flex-1 flex flex-col min-h-0", children: [_jsxs("div", { className: "flex items-center gap-3 p-3 border-b border-border shrink-0", children: [_jsx("button", { type: "button", className: "md:hidden w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text shrink-0", "aria-label": "Open calendars", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(Button, { type: "button", onClick: () => openNewEvent(), className: "!w-auto shrink-0", children: "+ New event" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => shiftView(-1), "aria-label": "Previous", className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u2039" }), _jsx("button", { type: "button", onClick: goToday, className: "px-2 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "Today" }), _jsx("button", { type: "button", onClick: () => shiftView(1), "aria-label": "Next", className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u203A" })] }), _jsx("h1", { className: "hidden md:block text-lg font-bold tracking-tight", children: title }), _jsx("div", { className: "ml-auto flex gap-1 overflow-x-auto", children: VIEW_TYPES.map((v) => (_jsx("button", { type: "button", onClick: () => setView(v), className: [
224
242
  "px-3 h-7 text-sm rounded-sm whitespace-nowrap",
@@ -226,5 +244,5 @@ function CalendarContent() {
226
244
  (v === "workWeek" || v === "split") && "hidden md:inline-block",
227
245
  ]
228
246
  .filter(Boolean)
229
- .join(" "), children: VIEW_LABELS[v] }, v))) })] }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), actionError && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: actionError }) })), loading ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : (_jsx(DndContext, { sensors: sensors, onDragEnd: handleDragEnd, children: view === "month" ? (_jsx(MonthView, { viewDate: viewDate, occurrences: occurrences, folderColors: folderColors, onSelectDay: handleSelectDay, onSelectEvent: openEvent })) : view === "split" ? (_jsx(SplitDayView, { day: viewDate, columns: splitColumns, occurrences: occurrences, onSelectEvent: openEvent, onSelectSlot: (start, end, targetFolderUid) => openNewEvent(start, end, targetFolderUid) })) : (_jsx(TimeGridView, { days: days, occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, onSelectSlot: openNewEvent })) })), modal && modalMailbox && (_jsx(EventModal, { open: true, onClose: closeModal, mailboxUid: modalMailbox.uid, folderUid: modalFolderUid, calendars: calendarFolders.filter((f) => f.mailboxUid === modalMailbox.uid).map((f) => ({ uid: f.uid, name: f.name })), mailboxOptions: mailboxOptions, organizerAddress: modalMailbox.primarySmtpAddress, occurrence: modal.occurrence, initialStart: modal.initialStart, initialEnd: modal.initialEnd, onSaved: handleSaved, onDeleted: handleDeleted }))] })] }));
247
+ .join(" "), children: VIEW_LABELS[v] }, v))) })] }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), actionError && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: actionError }) })), loading ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : (_jsx(DndContext, { sensors: sensors, onDragEnd: handleDragEnd, children: view === "month" ? (_jsx(MonthView, { viewDate: viewDate, occurrences: occurrences, folderColors: folderColors, onSelectDay: handleSelectDay, onSelectEvent: openEvent })) : view === "split" ? (_jsx(SplitDayView, { day: viewDate, columns: splitColumns, occurrences: occurrences, onSelectEvent: openEvent, onSelectSlot: (start, end, targetFolderUid) => openNewEvent(start, end, targetFolderUid) })) : (_jsx(TimeGridView, { days: days, occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, onSelectSlot: openNewEvent })) })), modal && modalMailbox && (_jsx(EventModal, { open: true, onClose: closeModal, mailboxUid: modalMailbox.uid, folderUid: modalFolderUid, calendars: calendarFolders.filter((f) => f.mailboxUid === modalMailbox.uid).map((f) => ({ uid: f.uid, name: f.name })), mailboxOptions: mailboxOptions, organizerAddress: modalMailbox.primarySmtpAddress, organizerAliases: modalMailbox.aliasAddresses, occurrence: modal.occurrence, initialStart: modal.initialStart, initialEnd: modal.initialEnd, onSaved: handleSaved, onDeleted: handleDeleted }))] })] }));
230
248
  }
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import { ContactsShellProps } from "../../shared/components/contacts/layout/ContactsShell.js";
3
+ /**
4
+ * Only reached on mobile (below the `md` breakpoint) — desktop's `apps/www/contacts/index.tsx` keeps its
5
+ * existing inline detail pane and never navigates here; see that file's `handleSelectRow`. There is no
6
+ * equivalent "new contact" route — an unsaved contact has no uid to route on, so creation stays an
7
+ * in-place mode-switch on every device (see `apps/www/contacts/index.tsx`'s `handleNew`).
8
+ */
9
+ export default function ContactDetailPage(props: ContactsShellProps & {
10
+ params: {
11
+ uid: string;
12
+ };
13
+ }): React.JSX.Element;
@@ -10,6 +10,7 @@ import ContactsShell from "../../shared/components/contacts/layout/ContactsShell
10
10
  import ContactDetailPane from "../../shared/components/contacts/ContactDetailPane.js";
11
11
  import ContactForm from "../../shared/components/contacts/ContactForm.js";
12
12
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
13
+ import { clearPinnedSignerCache } from "../../shared/components/mail/pinnedSigners.js";
13
14
  /**
14
15
  * Only reached on mobile (below the `md` breakpoint) — desktop's `apps/www/contacts/index.tsx` keeps its
15
16
  * existing inline detail pane and never navigates here; see that file's `handleSelectRow`. There is no
@@ -23,6 +24,9 @@ function ContactDetailContent({ uid }) {
23
24
  const [contact, setContact] = useState(null);
24
25
  const [loading, setLoading] = useState(true);
25
26
  const [error, setError] = useState(null);
27
+ // Kept apart from the load `error` - a failed delete must leave the contact on screen (with the
28
+ // reason) rather than replacing the whole page with an error as if it had never loaded.
29
+ const [deleteError, setDeleteError] = useState(null);
26
30
  const [mode, setMode] = useState("view");
27
31
  useEffect(() => {
28
32
  setLoading(true);
@@ -37,14 +41,21 @@ function ContactDetailContent({ uid }) {
37
41
  // rendered once `contact` is already resolved (see the `error || !contact` guard above it), so this
38
42
  // never runs with a stale/absent contact — no redundant null check needed here.
39
43
  async function handleDelete(contact) {
44
+ setDeleteError(null);
40
45
  try {
41
46
  await deleteContact(contact.uid, contact.version);
47
+ // A deleted contact's pinned signing keys must stop vouching for signatures.
48
+ clearPinnedSignerCache();
42
49
  window.location.href = "/contacts";
43
50
  }
44
51
  catch (err) {
45
- setError(err instanceof ApiRequestError ? err.message : "Could not delete this contact.");
52
+ setDeleteError(err instanceof ApiRequestError ? err.message : "Could not delete this contact.");
46
53
  }
47
54
  }
55
+ // After a key change was resolved (or found stale): re-read the contact, keeping the one shown if that fails.
56
+ function handleKeysChanged() {
57
+ getContact(uid).then(setContact, () => undefined);
58
+ }
48
59
  if (loading) {
49
60
  return _jsx("p", { className: "p-8 text-sm text-text-muted", children: "Loading\u2026" });
50
61
  }
@@ -58,5 +69,5 @@ function ContactDetailContent({ uid }) {
58
69
  setMode("view");
59
70
  }, onCancel: () => setMode("view") })] }));
60
71
  }
61
- return (_jsx("div", { className: "p-6", children: _jsx(ContactDetailPane, { contact: contact, onEdit: () => setMode("edit"), onDelete: () => handleDelete(contact), backHref: backHref }) }));
72
+ return (_jsxs("div", { className: "p-6", children: [deleteError && _jsx(Alert, { children: deleteError }), _jsx(ContactDetailPane, { contact: contact, onEdit: () => setMode("edit"), onDelete: () => handleDelete(contact), backHref: backHref, onKeysChanged: handleKeysChanged })] }));
62
73
  }
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import { ContactsShellProps } from "../../shared/components/contacts/layout/ContactsShell.js";
3
+ export default function ContactsPage(props: ContactsShellProps): React.JSX.Element;
@@ -15,25 +15,40 @@ import ContactsToolbar from "../../shared/components/contacts/ContactsToolbar.js
15
15
  import ContactAvatar from "@rapidmx/react-shared/components/avatar/ContactAvatar.js";
16
16
  import ContactDetailPane from "../../shared/components/contacts/ContactDetailPane.js";
17
17
  import ContactForm from "../../shared/components/contacts/ContactForm.js";
18
+ import { useWritableMailboxes } from "../../shared/components/mail/writableMailboxes.js";
19
+ import { getMyMailboxAccess } from "@rapidmx/react-shared/mail/mailboxAccessApi.js";
18
20
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
21
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
22
+ import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
23
+ import { LIST_PAGE_SIZE, MAX_LIST_PAGES, listAllPages } from "../../shared/mail/listAllPages.js";
24
+ import { clearPinnedSignerCache } from "../../shared/components/mail/pinnedSigners.js";
19
25
  const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
20
26
  export default function ContactsPage(props) {
21
- return (_jsx(ContactsShell, { ...props, children: _jsx(ContactsContent, {}) }));
27
+ return (_jsx(ContactsShell, { ...props, children: _jsx(ContactsContent, { userUid: props.userUid }) }));
22
28
  }
29
+ /** Shown in the Deleted view when the server answered `?deleted=true` with live contacts (see `canViewDeleted`). */
30
+ const DELETED_NOT_PERMITTED_MESSAGE = "You don't have permission to view deleted contacts in this folder.";
23
31
  function primaryInfo(contact) {
24
32
  return contact.emails[0]?.address ?? contact.phones[0]?.phoneNumber ?? "";
25
33
  }
34
+ /** How long an export's object URL is kept alive after the click - some browsers are still reading the
35
+ * blob after `click()` returns, so revoking it synchronously can cancel the download. */
36
+ const VCARD_DOWNLOAD_URL_LIFETIME_MS = 60000;
26
37
  function downloadTextFile(filename, content) {
27
38
  const blob = new Blob([content], { type: "text/vcard" });
28
39
  const url = URL.createObjectURL(blob);
29
40
  const a = document.createElement("a");
30
41
  a.href = url;
31
42
  a.download = filename;
43
+ document.body.appendChild(a);
32
44
  a.click();
33
- URL.revokeObjectURL(url);
45
+ a.remove();
46
+ setTimeout(() => URL.revokeObjectURL(url), VCARD_DOWNLOAD_URL_LIFETIME_MS);
34
47
  }
35
- function ContactsContent() {
48
+ function ContactsContent({ userUid }) {
36
49
  const { folderUid, mailboxUid, mailboxes } = useContactsShell();
50
+ // The new-contact form's Mailbox choices - view-only shares are left out (see writableMailboxes.ts).
51
+ const writableMailboxes = useWritableMailboxes(mailboxes, userUid, mailboxUid);
37
52
  const { openCompose } = useCompose();
38
53
  const isMobile = useIsMobile();
39
54
  const [contacts, setContacts] = useState([]);
@@ -42,6 +57,9 @@ function ContactsContent() {
42
57
  const [deletedError, setDeletedError] = useState(null);
43
58
  const [loading, setLoading] = useState(true);
44
59
  const [error, setError] = useState(null);
60
+ // Set when `listAllPages()` stopped at its page cap - the list shown isn't every contact.
61
+ const [truncated, setTruncated] = useState(false);
62
+ const [deletedTruncated, setDeletedTruncated] = useState(false);
45
63
  const [query, setQuery] = useState("");
46
64
  const [view, setView] = useState({ type: "all" });
47
65
  const [sortColumn, setSortColumn] = useState("name");
@@ -51,6 +69,7 @@ function ContactsContent() {
51
69
  const [mode, setMode] = useState("view");
52
70
  // Set after a contact is created in a different mailbox than the one this list shows.
53
71
  const [savedElsewhere, setSavedElsewhere] = useState(null);
72
+ const [confirmingBulkDelete, setConfirmingBulkDelete] = useState(false);
54
73
  /** Returns a promise so bulk actions (below) can wait for the refreshed list before re-asserting their
55
74
  * own error message — this always clears `error` first (a legitimate reset for a fresh fetch attempt),
56
75
  * which would otherwise silently wipe out a bulk action's own just-set failure message before the user
@@ -58,27 +77,69 @@ function ContactsContent() {
58
77
  function reload() {
59
78
  if (!folderUid) {
60
79
  setContacts([]);
80
+ setTruncated(false);
61
81
  setLoading(false);
62
82
  return Promise.resolve();
63
83
  }
64
84
  setLoading(true);
65
85
  setError(null);
66
- return listContacts(folderUid, { limit: 500 })
67
- .then(setContacts)
86
+ return listAllPages((page) => listContacts(folderUid, { limit: LIST_PAGE_SIZE, page }))
87
+ .then((result) => {
88
+ setContacts(result.items);
89
+ setTruncated(result.truncated);
90
+ })
68
91
  .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load contacts."))
69
92
  .finally(() => setLoading(false));
70
93
  }
71
94
  useEffect(() => {
72
95
  void reload();
73
96
  }, [folderUid]);
97
+ // restapi only honors `?deleted=true` for a caller with both delete and update rights on the contacts *folder*
98
+ // (its ACL, which inherits the mailbox's but can carry its own records) - anyone else silently gets the *live*
99
+ // contacts back, which the Deleted view would then present as deleted. There is no client API for the caller's
100
+ // access to a folder, so the mailbox-level check below is only a first gate (an owner always passes; anyone else
101
+ // asks the server; unknown - still loading, or the check failed - hides the view), and the Deleted view's own
102
+ // load below detects the server ignoring the filter. `ContactsShell` only renders this with a resolved mailbox,
103
+ // and switching mailboxes is a full page load, so `mailboxUid` never changes under this component.
104
+ const ownsMailbox = mailboxes.some((mb) => mb.uid === mailboxUid && mb.ownerUserUid !== undefined && mb.ownerUserUid === userUid);
105
+ const [delegateCanViewDeleted, setDelegateCanViewDeleted] = useState(false);
106
+ // Resolving a contact's key change needs update rights on the mailbox - `undefined` while unknown.
107
+ const [delegateCanUpdate, setDelegateCanUpdate] = useState(undefined);
108
+ useEffect(() => {
109
+ if (ownsMailbox) {
110
+ return;
111
+ }
112
+ let cancelled = false;
113
+ getMyMailboxAccess(mailboxUid).then((access) => {
114
+ if (!cancelled) {
115
+ setDelegateCanViewDeleted(access.canDelete && access.canUpdate);
116
+ setDelegateCanUpdate(access.canUpdate);
117
+ }
118
+ }, () => undefined);
119
+ return () => {
120
+ cancelled = true;
121
+ };
122
+ }, [ownsMailbox]);
123
+ const canViewDeleted = ownsMailbox || delegateCanViewDeleted;
74
124
  useEffect(() => {
75
125
  if (view.type !== "deleted" || !folderUid) {
76
126
  return;
77
127
  }
78
128
  setDeletedLoading(true);
79
129
  setDeletedError(null);
80
- listDeletedContacts(folderUid, { limit: 500 })
81
- .then(setDeletedContacts)
130
+ listAllPages((page) => listDeletedContacts(folderUid, { limit: LIST_PAGE_SIZE, page }))
131
+ .then((result) => {
132
+ // A server that dropped the `deleted` filter (no delete+update right on this folder) answers with live
133
+ // contacts, which must never be listed as deleted.
134
+ if (result.items.some((c) => c.deleted !== true)) {
135
+ setDeletedContacts([]);
136
+ setDeletedTruncated(false);
137
+ setDeletedError(DELETED_NOT_PERMITTED_MESSAGE);
138
+ return;
139
+ }
140
+ setDeletedContacts(result.items);
141
+ setDeletedTruncated(result.truncated);
142
+ })
82
143
  .catch((err) => setDeletedError(err instanceof ApiRequestError ? err.message : "Could not load deleted contacts."))
83
144
  .finally(() => setDeletedLoading(false));
84
145
  // Re-fetches every time the Deleted view is (re-)selected, matching this page's own `reload()`
@@ -175,6 +236,8 @@ function ContactsContent() {
175
236
  async function handleDelete(contact) {
176
237
  try {
177
238
  await deleteContact(contact.uid, contact.version);
239
+ // Trusted signer pins come from contacts - a deleted contact's keys must stop vouching for signatures.
240
+ clearPinnedSignerCache();
178
241
  setSelectedUid(null);
179
242
  setMode("view");
180
243
  void reload();
@@ -184,6 +247,7 @@ function ContactsContent() {
184
247
  }
185
248
  }
186
249
  async function handleBulkDelete() {
250
+ setConfirmingBulkDelete(false);
187
251
  let bulkError = null;
188
252
  for (const contact of checkedContacts) {
189
253
  try {
@@ -193,6 +257,7 @@ function ContactsContent() {
193
257
  bulkError = err instanceof ApiRequestError ? err.message : "Could not delete one or more contacts.";
194
258
  }
195
259
  }
260
+ clearPinnedSignerCache();
196
261
  setCheckedUids(new Set());
197
262
  await reload();
198
263
  setError(bulkError);
@@ -222,6 +287,7 @@ function ContactsContent() {
222
287
  bulkError = err instanceof ApiRequestError ? err.message : "Could not update one or more contacts.";
223
288
  }
224
289
  }
290
+ clearPinnedSignerCache();
225
291
  await reload();
226
292
  setError(bulkError);
227
293
  }
@@ -234,12 +300,15 @@ function ContactsContent() {
234
300
  for (const contact of checkedContacts) {
235
301
  const categories = Array.from(new Set([...(contact.categories ?? []), category.trim()]));
236
302
  try {
237
- await updateContact({ ...contact, categories });
303
+ // Only the changed field - sending the whole fetched contact back includes server-managed
304
+ // fields restapi rejects, and would overwrite any concurrent edit to the other fields.
305
+ await updateContact({ uid: contact.uid, version: contact.version, categories });
238
306
  }
239
307
  catch (err) {
240
308
  bulkError = err instanceof ApiRequestError ? err.message : "Could not update one or more contacts.";
241
309
  }
242
310
  }
311
+ clearPinnedSignerCache();
243
312
  await reload();
244
313
  setError(bulkError);
245
314
  }
@@ -261,11 +330,12 @@ function ContactsContent() {
261
330
  bulkError = err instanceof ApiRequestError ? err.message : "Could not import one or more contacts.";
262
331
  }
263
332
  }
333
+ clearPinnedSignerCache();
264
334
  await reload();
265
335
  setError(bulkError);
266
336
  }
267
- return (_jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsx(ContactsSidebar, { mailboxUid: mailboxUid, contacts: contacts, active: view, onSelect: handleSelectView }), _jsxs("div", { className: [
337
+ return (_jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsx(ContactsSidebar, { mailboxUid: mailboxUid, contacts: contacts, active: view, onSelect: handleSelectView, showDeleted: canViewDeleted }), _jsxs("div", { className: [
268
338
  "w-full md:w-96 shrink-0 md:border-r border-border md:flex flex-col min-h-0",
269
339
  mode === "new" ? "hidden md:flex" : "flex",
270
- ].join(" "), children: [_jsx(ContactsToolbar, { selectedCount: checkedContacts.length, allSelectedFavorited: checkedContacts.length > 0 && checkedContacts.every((c) => c.favorite), onNewContact: handleNew, onEdit: handleToolbarEdit, onDelete: handleBulkDelete, onEmail: handleEmail, onToggleFavorite: handleToggleFavorite, onAddCategory: handleAddCategory, onExportVCard: handleExportVCard, onImportFile: handleImportFile }), _jsx("div", { className: "p-3 border-b border-border", children: _jsx("input", { type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search contacts", "aria-label": "Search contacts", className: INPUT_CLASS }) }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), deletedError && isDeletedView && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: deletedError }) })), (isDeletedView ? deletedLoading : loading) ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : sorted.length === 0 ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "No contacts found." })) : (_jsx("div", { className: "flex-1 overflow-auto", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [!isDeletedView && (_jsx("th", { className: "w-8 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all contacts", checked: allChecked, onChange: toggleCheckAll }) })), _jsx("th", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("name"), className: "text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Name", sortColumn === "name" ? (sortDesc ? " ▾" : " ▴") : ""] }) }), _jsx("th", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("info"), className: "text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Contact info", sortColumn === "info" ? (sortDesc ? " ▾" : " ▴") : ""] }) })] }) }), _jsx("tbody", { children: sorted.map((contact) => (_jsxs("tr", { className: ["border-b border-border", contact.uid === selectedUid ? "bg-primary/10" : "hover:bg-surface-alt"].join(" "), children: [!isDeletedView && (_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${contact.displayName}`, checked: checkedUids.has(contact.uid), onChange: () => toggleChecked(contact.uid) }) })), _jsx("td", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSelectRow(contact), className: "flex items-center gap-2 text-left", children: [_jsx(ContactAvatar, { displayName: contact.displayName, size: 28 }), _jsxs("span", { className: "truncate font-medium", children: [contact.displayName, contact.favorite && _jsx("span", { "aria-label": "Favorite", children: " \u2605" })] })] }) }), _jsx("td", { className: "px-3 py-2 text-text-muted truncate", children: primaryInfo(contact) })] }, contact.uid))) })] }) }))] }), _jsxs("div", { className: ["flex-1 min-w-0 overflow-y-auto p-6 md:block", mode === "new" ? "block" : "hidden"].join(" "), children: [savedElsewhere && mode !== "new" && (_jsxs("p", { role: "status", className: "mb-4 text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: [savedElsewhere.displayName, " was added to", " ", mailboxes.find((mb) => mb.uid === savedElsewhere.mailboxUid)?.displayName ?? "another mailbox", ".", " ", _jsx("a", { href: `/contacts?mailboxUid=${encodeURIComponent(savedElsewhere.mailboxUid)}`, className: "font-medium text-primary-dark hover:underline", children: "View that mailbox\u2019s contacts" })] })), mode === "new" && mailboxUid && folderUid ? (_jsx(ContactForm, { mailboxUid: mailboxUid, folderUid: folderUid, mailboxes: mailboxes, onSaved: handleSaved, onCancel: handleCancel })) : mode === "edit" && selected ? (_jsx(ContactForm, { contact: selected, onSaved: handleSaved, onCancel: handleCancel })) : selected ? (_jsx(ContactDetailPane, { contact: selected, onEdit: () => setMode("edit"), onDelete: () => handleDelete(selected) })) : (_jsx("p", { className: "text-sm text-text-muted", children: "Select a contact, or create a new one." }))] })] }));
340
+ ].join(" "), children: [_jsx(ContactsToolbar, { selectedCount: checkedContacts.length, allSelectedFavorited: checkedContacts.length > 0 && checkedContacts.every((c) => c.favorite), onNewContact: handleNew, onEdit: handleToolbarEdit, onDelete: () => setConfirmingBulkDelete(true), onEmail: handleEmail, onToggleFavorite: handleToggleFavorite, onAddCategory: handleAddCategory, onExportVCard: handleExportVCard, onImportFile: handleImportFile }), _jsx("div", { className: "p-3 border-b border-border", children: _jsx("input", { type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search contacts", "aria-label": "Search contacts", className: INPUT_CLASS }) }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), (isDeletedView ? deletedTruncated : truncated) && (_jsx("div", { className: "p-3", children: _jsxs(Alert, { children: ["This folder has more contacts than can be shown at once - only the first ", LIST_PAGE_SIZE * MAX_LIST_PAGES, " are listed."] }) })), deletedError && isDeletedView && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: deletedError }) })), (isDeletedView ? deletedLoading : loading) ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : sorted.length === 0 ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "No contacts found." })) : (_jsx("div", { className: "flex-1 overflow-auto", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [!isDeletedView && (_jsx("th", { className: "w-8 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all contacts", checked: allChecked, onChange: toggleCheckAll }) })), _jsx("th", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("name"), className: "text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Name", sortColumn === "name" ? (sortDesc ? " ▾" : " ▴") : ""] }) }), _jsx("th", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("info"), className: "text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Contact info", sortColumn === "info" ? (sortDesc ? " ▾" : " ▴") : ""] }) })] }) }), _jsx("tbody", { children: sorted.map((contact) => (_jsxs("tr", { className: ["border-b border-border", contact.uid === selectedUid ? "bg-primary/10" : "hover:bg-surface-alt"].join(" "), children: [!isDeletedView && (_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${contact.displayName}`, checked: checkedUids.has(contact.uid), onChange: () => toggleChecked(contact.uid) }) })), _jsx("td", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSelectRow(contact), className: "flex items-center gap-2 text-left", children: [_jsx(ContactAvatar, { displayName: contact.displayName, size: 28 }), _jsxs("span", { className: "truncate font-medium", children: [contact.displayName, contact.favorite && _jsx("span", { "aria-label": "Favorite", children: " \u2605" })] })] }) }), _jsx("td", { className: "px-3 py-2 text-text-muted truncate", children: primaryInfo(contact) })] }, contact.uid))) })] }) }))] }), _jsxs("div", { className: ["flex-1 min-w-0 overflow-y-auto p-6 md:block", mode === "new" ? "block" : "hidden"].join(" "), children: [savedElsewhere && mode !== "new" && (_jsxs("p", { role: "status", className: "mb-4 text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: [savedElsewhere.displayName, " was added to", " ", mailboxes.find((mb) => mb.uid === savedElsewhere.mailboxUid)?.displayName ?? "another mailbox", ".", " ", _jsx("a", { href: `/contacts?mailboxUid=${encodeURIComponent(savedElsewhere.mailboxUid)}`, className: "font-medium text-primary-dark hover:underline", children: "View that mailbox\u2019s contacts" })] })), mode === "new" && mailboxUid && folderUid ? (_jsx(ContactForm, { mailboxUid: mailboxUid, folderUid: folderUid, mailboxes: writableMailboxes, onSaved: handleSaved, onCancel: handleCancel })) : mode === "edit" && selected ? (_jsx(ContactForm, { contact: selected, onSaved: handleSaved, onCancel: handleCancel })) : selected ? (_jsx(ContactDetailPane, { contact: selected, onEdit: () => setMode("edit"), onDelete: () => handleDelete(selected), onKeysChanged: () => void reload(), canResolveKeys: ownsMailbox || delegateCanUpdate })) : (_jsx("p", { className: "text-sm text-text-muted", children: "Select a contact, or create a new one." }))] }), _jsxs(Modal, { open: confirmingBulkDelete, onClose: () => setConfirmingBulkDelete(false), title: "Delete contacts", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Delete ", checkedContacts.length, " selected ", checkedContacts.length === 1 ? "contact" : "contacts", "? They move to Deleted contacts."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingBulkDelete(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: handleBulkDelete, children: "Delete" })] })] })] }));
271
341
  }
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import { MailShellProps } from "../shared/components/mail/layout/MailShell.js";
3
+ export default function InboxPage(props: MailShellProps): React.JSX.Element;