@rapidmx/web-client 0.30.0 → 0.31.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 (400) hide show
  1. package/apps/admin/audit-log/index.tsx +3 -3
  2. package/apps/admin/branding/index.tsx +3 -3
  3. package/apps/admin/data-requests/index.tsx +18 -7
  4. package/apps/admin/distribution-lists/[uid].tsx +3 -3
  5. package/apps/admin/distribution-lists/index.tsx +3 -3
  6. package/apps/admin/distribution-lists/new/index.tsx +3 -3
  7. package/apps/admin/domains/[uid].tsx +6 -5
  8. package/apps/admin/domains/index.tsx +3 -3
  9. package/apps/admin/domains/new/index.tsx +3 -3
  10. package/apps/admin/escrow-scopes/[uid].tsx +11 -5
  11. package/apps/admin/escrow-scopes/index.tsx +3 -3
  12. package/apps/admin/escrow-scopes/new/index.tsx +9 -3
  13. package/apps/admin/index.tsx +4 -4
  14. package/apps/admin/ingest-queue/index.tsx +3 -3
  15. package/apps/admin/mailboxes/[uid].tsx +15 -7
  16. package/apps/admin/quarantine/index.tsx +4 -4
  17. package/apps/admin/retention-policy/index.tsx +3 -3
  18. package/apps/admin/signing-certificates/index.tsx +3 -3
  19. package/apps/admin/transport-rules/[uid].tsx +4 -4
  20. package/apps/admin/transport-rules/index.tsx +3 -3
  21. package/apps/admin/transport-rules/new/index.tsx +3 -3
  22. package/apps/escrow/matters/[uid].tsx +26 -2
  23. package/apps/escrow/matters/new/index.tsx +4 -0
  24. package/apps/shared/calendar/calendarLiveUpdates.ts +11 -10
  25. package/apps/shared/calendar/useCalendarReminders.ts +2 -0
  26. package/apps/shared/components/admin/ActionAlert.tsx +32 -0
  27. package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +1 -1
  28. package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +1 -1
  29. package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +1 -1
  30. package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +1 -1
  31. package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +1 -1
  32. package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
  33. package/apps/shared/components/admin/elevation.ts +19 -0
  34. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
  35. package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
  36. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
  37. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
  38. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
  39. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  40. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
  41. package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
  42. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
  43. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
  44. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
  45. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
  46. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
  47. package/apps/shared/components/admin/settings/PluginsManager.tsx +39 -17
  48. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
  49. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
  50. package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
  51. package/apps/shared/components/admin/signOut.ts +3 -0
  52. package/apps/shared/components/admin/usePagedList.tsx +2 -2
  53. package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
  54. package/apps/shared/components/calendar/EventDetails.tsx +34 -8
  55. package/apps/shared/components/calendar/EventEditor.tsx +90 -40
  56. package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
  57. package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
  58. package/apps/shared/components/calendar/EventModal.tsx +25 -1
  59. package/apps/shared/components/calendar/FindATime.tsx +10 -2
  60. package/apps/shared/components/calendar/ListView.tsx +99 -25
  61. package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
  62. package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
  63. package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
  64. package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
  65. package/apps/shared/components/calendar/gridLayout.ts +94 -0
  66. package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
  67. package/apps/shared/components/calendar/timePicker.ts +5 -2
  68. package/apps/shared/components/contacts/ContactCard.tsx +15 -4
  69. package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
  70. package/apps/shared/components/contacts/ContactForm.tsx +61 -17
  71. package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
  72. package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
  73. package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
  74. package/apps/shared/components/contacts/participantDetails.ts +18 -1
  75. package/apps/shared/components/layout/AppShell.tsx +9 -0
  76. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
  77. package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
  78. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
  79. package/apps/shared/components/mail/ConversationList.tsx +49 -6
  80. package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
  81. package/apps/shared/components/mail/MenuButton.tsx +3 -400
  82. package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
  83. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
  84. package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
  85. package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
  86. package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
  87. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
  88. package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
  89. package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
  90. package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
  91. package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
  92. package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
  93. package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
  94. package/apps/shared/components/mail/writableMailboxes.ts +2 -0
  95. package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
  96. package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
  97. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
  98. package/apps/shared/components/tasks/dueDate.ts +29 -0
  99. package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
  100. package/apps/shared/mail/decryptedMessages.ts +51 -2
  101. package/apps/shared/mail/listSnapshots.ts +2 -0
  102. package/apps/shared/mail/outbox/sendDecision.ts +14 -3
  103. package/apps/shared/mail/outbox/sendJob.ts +5 -0
  104. package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
  105. package/apps/shared/mail/usePermanentDelete.tsx +3 -1
  106. package/apps/shared/notifications/apiErrors.ts +2 -0
  107. package/apps/shared/notifications/preferences.ts +1 -1
  108. package/apps/shared/notifications/store.ts +3 -1
  109. package/apps/shared/plugins/pluginNav.ts +4 -2
  110. package/apps/www/calendar/index.tsx +24 -12
  111. package/apps/www/contacts/[uid].tsx +6 -1
  112. package/apps/www/contacts/index.tsx +66 -12
  113. package/apps/www/index.tsx +91 -28
  114. package/apps/www/messages/[uid].tsx +8 -3
  115. package/apps/www/settings/auto-reply/index.tsx +13 -0
  116. package/apps/www/settings/blocked-senders/index.tsx +4 -0
  117. package/apps/www/settings/encryption/index.tsx +66 -4
  118. package/apps/www/settings/privacy/index.tsx +4 -1
  119. package/apps/www/settings/profile/index.tsx +12 -1
  120. package/apps/www/settings/sharing/index.tsx +11 -1
  121. package/apps/www/settings/signatures/[uid].tsx +6 -4
  122. package/apps/www/tasks/index.tsx +97 -29
  123. package/dist/apps/admin/audit-log/index.js +3 -3
  124. package/dist/apps/admin/branding/index.js +3 -3
  125. package/dist/apps/admin/data-requests/index.js +17 -7
  126. package/dist/apps/admin/distribution-lists/[uid].js +3 -3
  127. package/dist/apps/admin/distribution-lists/index.js +3 -3
  128. package/dist/apps/admin/distribution-lists/new/index.js +3 -3
  129. package/dist/apps/admin/domains/[uid].js +6 -5
  130. package/dist/apps/admin/domains/index.js +3 -3
  131. package/dist/apps/admin/domains/new/index.js +3 -3
  132. package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
  133. package/dist/apps/admin/escrow-scopes/index.js +3 -3
  134. package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
  135. package/dist/apps/admin/index.js +4 -4
  136. package/dist/apps/admin/ingest-queue/index.js +3 -3
  137. package/dist/apps/admin/mailboxes/[uid].js +15 -7
  138. package/dist/apps/admin/quarantine/index.js +4 -4
  139. package/dist/apps/admin/retention-policy/index.js +3 -3
  140. package/dist/apps/admin/signing-certificates/index.js +3 -3
  141. package/dist/apps/admin/transport-rules/[uid].js +4 -4
  142. package/dist/apps/admin/transport-rules/index.js +3 -3
  143. package/dist/apps/admin/transport-rules/new/index.js +3 -3
  144. package/dist/apps/escrow/matters/[uid].js +8 -3
  145. package/dist/apps/escrow/matters/new/index.js +4 -0
  146. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
  147. package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
  148. package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
  149. package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
  150. package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
  151. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +1 -1
  152. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
  153. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
  154. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
  155. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
  156. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
  157. package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
  158. package/dist/apps/shared/components/admin/elevation.js +16 -0
  159. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
  160. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
  161. package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
  162. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
  163. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
  164. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
  165. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  166. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
  167. package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
  168. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
  169. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
  170. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
  171. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
  172. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
  173. package/dist/apps/shared/components/admin/settings/PluginsManager.js +26 -12
  174. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
  175. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
  176. package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
  177. package/dist/apps/shared/components/admin/signOut.js +3 -0
  178. package/dist/apps/shared/components/admin/usePagedList.js +2 -2
  179. package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
  180. package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
  181. package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
  182. package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
  183. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
  184. package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
  185. package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
  186. package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
  187. package/dist/apps/shared/components/calendar/EventModal.js +10 -2
  188. package/dist/apps/shared/components/calendar/FindATime.js +4 -3
  189. package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
  190. package/dist/apps/shared/components/calendar/ListView.js +92 -27
  191. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
  192. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
  193. package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
  194. package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
  195. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
  196. package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
  197. package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
  198. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
  199. package/dist/apps/shared/components/calendar/timePicker.js +5 -2
  200. package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
  201. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
  202. package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
  203. package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
  204. package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
  205. package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
  206. package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
  207. package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
  208. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
  209. package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
  210. package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
  211. package/dist/apps/shared/components/layout/AppShell.js +9 -0
  212. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
  213. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
  214. package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
  215. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
  216. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
  217. package/dist/apps/shared/components/mail/ConversationList.js +49 -6
  218. package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
  219. package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
  220. package/dist/apps/shared/components/mail/MenuButton.js +2 -211
  221. package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
  222. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
  223. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
  224. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
  225. package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
  226. package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
  227. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
  228. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
  229. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
  230. package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
  231. package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
  232. package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
  233. package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
  234. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
  235. package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
  236. package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
  237. package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
  238. package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
  239. package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
  240. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
  241. package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
  242. package/dist/apps/shared/components/tasks/dueDate.js +26 -0
  243. package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
  244. package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
  245. package/dist/apps/shared/mail/decryptedMessages.js +46 -2
  246. package/dist/apps/shared/mail/listSnapshots.js +2 -0
  247. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
  248. package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
  249. package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
  250. package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
  251. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
  252. package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
  253. package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
  254. package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
  255. package/dist/apps/shared/notifications/apiErrors.js +2 -0
  256. package/dist/apps/shared/notifications/preferences.d.ts +1 -1
  257. package/dist/apps/shared/notifications/preferences.js +1 -1
  258. package/dist/apps/shared/notifications/store.js +3 -1
  259. package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
  260. package/dist/apps/shared/plugins/pluginNav.js +4 -2
  261. package/dist/apps/www/calendar/index.js +24 -12
  262. package/dist/apps/www/contacts/[uid].js +6 -2
  263. package/dist/apps/www/contacts/index.js +62 -11
  264. package/dist/apps/www/index.js +85 -30
  265. package/dist/apps/www/messages/[uid].js +8 -3
  266. package/dist/apps/www/settings/auto-reply/index.js +13 -1
  267. package/dist/apps/www/settings/blocked-senders/index.js +4 -0
  268. package/dist/apps/www/settings/encryption/index.js +29 -5
  269. package/dist/apps/www/settings/privacy/index.js +3 -2
  270. package/dist/apps/www/settings/profile/index.js +4 -1
  271. package/dist/apps/www/settings/sharing/index.js +10 -1
  272. package/dist/apps/www/settings/signatures/[uid].js +6 -4
  273. package/dist/apps/www/tasks/index.js +94 -32
  274. package/dist/lib/admin/auditLogApi.js +2 -2
  275. package/dist/lib/admin/distributionListsApi.js +2 -2
  276. package/dist/lib/admin/domainsApi.js +2 -2
  277. package/dist/lib/admin/escrowAuditLogApi.js +2 -2
  278. package/dist/lib/admin/escrowScopesApi.js +2 -2
  279. package/dist/lib/admin/pluginsApi.d.ts +13 -2
  280. package/dist/lib/admin/pluginsApi.js +8 -0
  281. package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
  282. package/dist/lib/admin/retentionPolicyApi.js +3 -0
  283. package/dist/lib/admin/transportRulesApi.js +2 -2
  284. package/dist/lib/appearance/preferencesApi.d.ts +1 -1
  285. package/dist/lib/appearance/preferencesApi.js +3 -14
  286. package/dist/lib/auth/session.d.ts +51 -12
  287. package/dist/lib/auth/session.js +214 -38
  288. package/dist/lib/branding/brandingApi.d.ts +3 -3
  289. package/dist/lib/branding/brandingApi.js +9 -21
  290. package/dist/lib/calendar/calendarApi.js +3 -1
  291. package/dist/lib/calendar/eventSearch.js +43 -6
  292. package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
  293. package/dist/lib/calendar/freeBusyApi.js +13 -5
  294. package/dist/lib/calendar/recurrence.js +51 -3
  295. package/dist/lib/calendar/useDayKey.d.ts +7 -0
  296. package/dist/lib/calendar/useDayKey.js +43 -0
  297. package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
  298. package/dist/lib/components/avatar/ContactAvatar.js +37 -8
  299. package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
  300. package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
  301. package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
  302. package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
  303. package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
  304. package/dist/lib/components/forms/ImageEditBadge.js +62 -0
  305. package/dist/lib/components/menus/MenuButton.d.ts +78 -0
  306. package/dist/lib/components/menus/MenuButton.js +219 -0
  307. package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
  308. package/dist/lib/components/navigation/BottomTabBar.js +22 -3
  309. package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
  310. package/dist/lib/components/overlays/PopoverPortal.js +3 -2
  311. package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
  312. package/dist/lib/contacts/contactsApi.d.ts +5 -2
  313. package/dist/lib/contacts/contactsApi.js +16 -19
  314. package/dist/lib/contacts/gravatar.d.ts +14 -3
  315. package/dist/lib/contacts/gravatar.js +42 -5
  316. package/dist/lib/contacts/preparePhoto.d.ts +24 -0
  317. package/dist/lib/contacts/preparePhoto.js +250 -0
  318. package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
  319. package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
  320. package/dist/lib/contacts/vcard.d.ts +3 -0
  321. package/dist/lib/contacts/vcard.js +59 -7
  322. package/dist/lib/crypto/escrowKeys.d.ts +8 -0
  323. package/dist/lib/crypto/escrowKeys.js +20 -1
  324. package/dist/lib/crypto/messageSecurity.d.ts +7 -4
  325. package/dist/lib/crypto/messageSecurity.js +23 -5
  326. package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
  327. package/dist/lib/crypto/passwordUnlock.js +13 -2
  328. package/dist/lib/crypto/smime.js +12 -2
  329. package/dist/lib/mail/compose/composeQuoting.js +3 -2
  330. package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
  331. package/dist/lib/mail/compose/quotedImages.js +35 -9
  332. package/dist/lib/mail/labelsApi.js +2 -2
  333. package/dist/lib/mail/mailApi.d.ts +8 -5
  334. package/dist/lib/mail/mailApi.js +73 -39
  335. package/dist/lib/mail/mailDetailHooks.js +11 -3
  336. package/dist/lib/mail/mailFilterRulesApi.js +2 -2
  337. package/dist/lib/mail/mailSignaturesApi.js +2 -2
  338. package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
  339. package/dist/lib/mail/mailboxImportApi.js +3 -15
  340. package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
  341. package/dist/lib/mail/messageBodySanitizer.js +44 -1
  342. package/dist/lib/search/queryGrammar.js +12 -4
  343. package/dist/lib/search/searchTier3.d.ts +6 -1
  344. package/dist/lib/search/searchTier3.js +54 -25
  345. package/dist/lib/tasks/tasksApi.js +3 -3
  346. package/dist/lib/util/api.d.ts +26 -4
  347. package/dist/lib/util/api.js +73 -16
  348. package/dist/lib/util/apiQuery.d.ts +11 -0
  349. package/dist/lib/util/apiQuery.js +11 -0
  350. package/dist/lib/util/settleWithLimit.d.ts +5 -0
  351. package/dist/lib/util/settleWithLimit.js +25 -0
  352. package/lib/admin/auditLogApi.ts +2 -2
  353. package/lib/admin/distributionListsApi.ts +2 -2
  354. package/lib/admin/domainsApi.ts +2 -2
  355. package/lib/admin/escrowAuditLogApi.ts +2 -2
  356. package/lib/admin/escrowScopesApi.ts +2 -2
  357. package/lib/admin/pluginsApi.ts +21 -2
  358. package/lib/admin/retentionPolicyApi.ts +4 -0
  359. package/lib/admin/transportRulesApi.ts +2 -2
  360. package/lib/appearance/preferencesApi.ts +3 -14
  361. package/lib/auth/session.ts +239 -39
  362. package/lib/branding/brandingApi.ts +9 -21
  363. package/lib/calendar/calendarApi.ts +3 -1
  364. package/lib/calendar/eventSearch.ts +46 -6
  365. package/lib/calendar/freeBusyApi.ts +14 -6
  366. package/lib/calendar/recurrence.ts +59 -8
  367. package/lib/calendar/useDayKey.ts +45 -0
  368. package/lib/components/avatar/ContactAvatar.tsx +46 -8
  369. package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
  370. package/lib/components/forms/CameraCaptureModal.tsx +133 -0
  371. package/lib/components/forms/ImageEditBadge.tsx +121 -0
  372. package/lib/components/menus/MenuButton.tsx +408 -0
  373. package/lib/components/navigation/BottomTabBar.tsx +30 -8
  374. package/lib/components/overlays/PopoverPortal.tsx +3 -2
  375. package/lib/components/pickers/TimeZonePicker.tsx +17 -1
  376. package/lib/contacts/contactsApi.ts +18 -19
  377. package/lib/contacts/gravatar.ts +47 -5
  378. package/lib/contacts/preparePhoto.ts +273 -0
  379. package/lib/contacts/useContactPhotoSrc.ts +177 -0
  380. package/lib/contacts/vcard.ts +60 -7
  381. package/lib/crypto/escrowKeys.ts +21 -1
  382. package/lib/crypto/messageSecurity.ts +33 -9
  383. package/lib/crypto/passwordUnlock.ts +14 -2
  384. package/lib/crypto/smime.ts +13 -2
  385. package/lib/mail/compose/composeQuoting.ts +3 -2
  386. package/lib/mail/compose/quotedImages.ts +39 -9
  387. package/lib/mail/labelsApi.ts +2 -2
  388. package/lib/mail/mailApi.ts +96 -39
  389. package/lib/mail/mailDetailHooks.ts +11 -3
  390. package/lib/mail/mailFilterRulesApi.ts +2 -2
  391. package/lib/mail/mailSignaturesApi.ts +2 -2
  392. package/lib/mail/mailboxImportApi.ts +9 -15
  393. package/lib/mail/messageBodySanitizer.ts +47 -1
  394. package/lib/search/queryGrammar.ts +13 -4
  395. package/lib/search/searchTier3.ts +65 -32
  396. package/lib/tasks/tasksApi.ts +3 -3
  397. package/lib/util/api.ts +99 -16
  398. package/lib/util/apiQuery.ts +13 -0
  399. package/lib/util/settleWithLimit.ts +25 -0
  400. package/package.json +1 -1
@@ -3,17 +3,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
- import { useEffect, useRef, useState } from "react";
6
+ import { useEffect, useState } from "react";
7
7
  import ContactAvatar from "../../../../lib/components/avatar/ContactAvatar.js";
8
- import Button from "../../../../lib/components/buttons/Button.js";
9
- import { CONTACT_PHOTO_MAX_BYTES, CONTACT_PHOTO_TYPES } from "../../../../lib/contacts/contactsApi.js";
8
+ import { ContactPhotoError, prepareContactPhoto } from "../../../../lib/contacts/preparePhoto.js";
9
+ import ImageEditBadge from "../../../../lib/components/forms/ImageEditBadge.js";
10
10
  /**
11
- * The picture of a contact in the edit form: the avatar as it is (their picture, else Gravatar, else initials) with "Change photo" and, when
12
- * there is a picture of their own, "Remove photo". A file is checked here (type and size) and only uploaded when the form is saved.
11
+ * The picture of a contact in the edit form: the avatar as it is (their picture, else Gravatar, else initials) with a camera badge at its lower right that opens a
12
+ * menu (`ImageEditBadge`) - "Upload file", "Take photo" and, when there is a picture of their own, "Remove photo". Any picture is taken (a phone's camera photo of several megabytes,
13
+ * in any orientation, included): it is made small enough here by `prepareContactPhoto()` and only uploaded when the form is saved.
13
14
  */
14
- export default function ContactPhotoField({ displayName, email, currentUrl, picked, removed, onPick, onRemove, onReject }) {
15
- const input = useRef(null);
15
+ export default function ContactPhotoField({ displayName, email, currentUrl, picked, removed, onPick, onRemove, onReject, onBusyChange }) {
16
16
  const [preview, setPreview] = useState();
17
+ const [preparing, setPreparing] = useState(false);
17
18
  useEffect(() => {
18
19
  if (!picked) {
19
20
  setPreview(undefined);
@@ -23,24 +24,21 @@ export default function ContactPhotoField({ displayName, email, currentUrl, pick
23
24
  setPreview(url);
24
25
  return () => URL.revokeObjectURL(url);
25
26
  }, [picked]);
26
- function handleChange(file) {
27
- if (!file) {
28
- return;
29
- }
30
- if (!CONTACT_PHOTO_TYPES.includes(file.type)) {
31
- onReject("Choose a JPEG, PNG, GIF or WebP image.");
27
+ async function handleChange(file) {
28
+ setPreparing(true);
29
+ onBusyChange(true);
30
+ try {
31
+ onPick(await prepareContactPhoto(file));
32
32
  }
33
- else if (file.size > CONTACT_PHOTO_MAX_BYTES) {
34
- onReject("Choose an image no larger than 1 MB.");
33
+ catch (err) {
34
+ onReject(err instanceof ContactPhotoError ? err.message : "This picture could not be read - choose another one.");
35
35
  }
36
- else {
37
- onPick(file);
36
+ finally {
37
+ setPreparing(false);
38
+ onBusyChange(false);
38
39
  }
39
40
  }
40
41
  const shown = picked ? preview : removed ? undefined : currentUrl;
41
42
  const hasOwn = !!picked || (!!currentUrl && !removed);
42
- return (_jsxs("div", { className: "flex items-center gap-4 mb-3", children: [_jsx(ContactAvatar, { displayName: displayName || "?", size: 72, photoUrl: shown, email: picked || (currentUrl && !removed) ? undefined : email }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx("input", { ref: input, type: "file", accept: CONTACT_PHOTO_TYPES.join(","), "aria-label": "Contact photo file", className: "sr-only", tabIndex: -1, onChange: (e) => {
43
- handleChange(e.target.files?.[0]);
44
- e.target.value = "";
45
- } }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto !py-1 !px-3 text-xs", onClick: () => input.current?.click(), children: hasOwn ? "Change photo" : "Add photo" }), hasOwn && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto !py-1 !px-3 text-xs", onClick: onRemove, children: "Remove photo" }))] })] }));
43
+ return (_jsxs("div", { className: "flex items-center gap-4 mb-3", children: [_jsxs("div", { className: "relative inline-block shrink-0", children: [_jsx(ContactAvatar, { displayName: displayName || "?", size: 72, photoUrl: shown, email: picked || (currentUrl && !removed) ? undefined : email }), _jsx(ImageEditBadge, { position: "bottom-right", label: "Change contact photo", fileInputLabel: "Contact photo file", hasImage: hasOwn, busy: preparing, onFile: (file) => void handleChange(file), onRemove: onRemove })] }), preparing && (_jsx("span", { role: "status", className: "text-xs text-text-muted", children: "Preparing picture\u2026" }))] }));
46
44
  }
@@ -79,7 +79,7 @@ export default function ContactsShell({ userUid, authServerUrl, impersonating, i
79
79
  // A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else
80
80
  // for a mailbox-less caller to do here yet, so the icon rail/header don't render at all.
81
81
  if (userUid && status === "ready" && !mailboxUid) {
82
- return _jsx(MailboxProvisioning, {});
82
+ return _jsx(MailboxProvisioning, { impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl });
83
83
  }
84
84
  let inner = null;
85
85
  if (userUid && status === "checking") {
@@ -52,7 +52,10 @@ export declare function contactInputFromCard(card: ParsedVCardContact, scope: {
52
52
  mailboxUid: string;
53
53
  folderUid: string;
54
54
  }): KnownPerson;
55
- /** The cards that are not yet in the address book, and how many were: a card is a duplicate when one of its emails is a stored contact's or an earlier card's. */
55
+ /**
56
+ * The cards that are not yet in the address book, and how many were: a card is a duplicate when one of its emails is a stored contact's or an earlier card's,
57
+ * and a card with no email at all when its name and first phone number are a stored contact's or an earlier card's (so importing the same file twice adds nothing).
58
+ */
56
59
  export declare function withoutDuplicates(cards: readonly ParsedVCardContact[], existing: readonly Contact[]): {
57
60
  fresh: ParsedVCardContact[];
58
61
  skipped: number;
@@ -92,12 +92,28 @@ export function contactInputFromCard(card, scope) {
92
92
  const names = card.givenName || card.surname ? { givenName: card.givenName, surname: card.surname } : splitPersonName(card.displayName);
93
93
  return { ...card, ...scope, ...names, emails: card.emails ?? [], phones: card.phones ?? [], addresses: card.addresses ?? [] };
94
94
  }
95
- /** The cards that are not yet in the address book, and how many were: a card is a duplicate when one of its emails is a stored contact's or an earlier card's. */
95
+ /** What a card with no email is told apart by: its name and its first phone number (digits only). */
96
+ function identityKey(person) {
97
+ return `${person.displayName.trim().toLowerCase()}|${(person.phones?.[0]?.phoneNumber ?? "").replace(/\D/g, "")}`;
98
+ }
99
+ /**
100
+ * The cards that are not yet in the address book, and how many were: a card is a duplicate when one of its emails is a stored contact's or an earlier card's,
101
+ * and a card with no email at all when its name and first phone number are a stored contact's or an earlier card's (so importing the same file twice adds nothing).
102
+ */
96
103
  export function withoutDuplicates(cards, existing) {
97
104
  const known = new Set(existing.flatMap((contact) => contact.emails.map((email) => normalizeAddress(email.address))));
105
+ const knownIdentities = new Set(existing.map(identityKey));
98
106
  const fresh = [];
99
107
  for (const card of cards) {
100
108
  const addresses = (card.emails ?? []).map((email) => normalizeAddress(email.address));
109
+ if (addresses.length === 0) {
110
+ const identity = identityKey(card);
111
+ if (!knownIdentities.has(identity)) {
112
+ knownIdentities.add(identity);
113
+ fresh.push(card);
114
+ }
115
+ continue;
116
+ }
101
117
  if (addresses.some((address) => known.has(address))) {
102
118
  continue;
103
119
  }
@@ -28,6 +28,8 @@ import { SIGN_OUT_CHANNEL, destroyAllLocalIndexes } from "../../search/localInde
28
28
  import { authApiFetch, setApiUnauthorizedObserver } from "../../../../lib/util/api.js";
29
29
  import { destroyUnlockedKeys } from "../../../../lib/crypto/keySession.js";
30
30
  import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
31
+ import { resetNotifications } from "../../notifications/store.js";
32
+ import { clearContactPhotoCache } from "../../../../lib/contacts/useContactPhotoSrc.js";
31
33
  import { pluginIcon } from "../../plugins/pluginIcons.js";
32
34
  import { mergePluginNavItems } from "../../plugins/pluginNav.js";
33
35
  import { useResolvedRailItems } from "../../plugins/useResolvedRailItems.js";
@@ -181,6 +183,9 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
181
183
  destroyUnlockedKeys();
182
184
  clearPinnedSignerCache();
183
185
  clearAppearanceCache();
186
+ clearContactPhotoCache();
187
+ // The notification history (meeting titles, error details) is this session's, and the tab's storage outlives it.
188
+ resetNotifications();
184
189
  // Bounded by its own timeout and never rejects - awaited so navigating doesn't kill the Worker mid-delete.
185
190
  void destroyAllLocalIndexes().then(() => {
186
191
  window.location.href = authServerUrl ?? "/";
@@ -198,6 +203,8 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
198
203
  // Trusted signer pins read from contacts don't outlive the session either.
199
204
  clearPinnedSignerCache();
200
205
  clearAppearanceCache();
206
+ clearContactPhotoCache();
207
+ resetNotifications();
201
208
  // Open compose windows save edits still waiting on their autosave debounce while the session is still
202
209
  // valid - logout invalidates it. Bounded the same way as logout itself, and never rejects.
203
210
  await flushComposeDrafts(LOGOUT_TIMEOUT_MS);
@@ -221,6 +228,8 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
221
228
  // handling, which surfaces via a full reload rather than an inline retry affordance.
222
229
  }
223
230
  finally {
231
+ // What the impersonated user's session raised (their meetings, their errors) must not be left for the administrator to read.
232
+ resetNotifications();
224
233
  window.location.href = "/admin";
225
234
  }
226
235
  }
@@ -11,12 +11,11 @@ import FormField from "../../../../lib/components/forms/FormField.js";
11
11
  import { enrollKey, getKeyVault, VaultAlreadyInitializedError } from "../../../../lib/crypto/keyvaultApi.js";
12
12
  import { UnopenableKeysNotice, unlockErrorMessage } from "./UnlockPromptProvider.js";
13
13
  import { FrameTakeover } from "../../navigation/frameContext.js";
14
- import { RecoveryFollowUpModal, UnlockModeToggle, startRecoveryUnlock } from "./RecoveryCodeUnlock.js";
14
+ import { MIN_PASSWORD_LENGTH, RecoveryFollowUpModal, UnlockModeToggle, startRecoveryUnlock, } from "./RecoveryCodeUnlock.js";
15
15
  import { ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE, getUnlockedKeys, unlockWithPassword, } from "../../../../lib/crypto/keySession.js";
16
16
  import { buildAad, generateMasterKey, sealWithKey } from "../../../../lib/crypto/masterKey.js";
17
17
  import { buildPasswordWrap, buildRecoveryWraps } from "../../../../lib/crypto/masterKeyWraps.js";
18
18
  import { exportPrivateKeyPkcs8, generateKeyPairWithCsr } from "../../../../lib/crypto/keys.js";
19
- const MIN_PASSWORD_LENGTH = 8;
20
19
  async function provisionEncryptionKey(mailboxUid, mailboxAddress, password) {
21
20
  const mk = generateMasterKey();
22
21
  const passwordWrap = await buildPasswordWrap(mailboxUid, mk, password);
@@ -180,7 +179,7 @@ export default function KeyEnrollmentGate({ mailboxUid, mailboxAddress, mailboxK
180
179
  }
181
180
  if (status === "setup_password" || status === "enrolling") {
182
181
  const enrolling = status === "enrolling";
183
- return (_jsx(FrameTakeover, { children: _jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Protect your mailbox" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Choose a password to protect your encryption keys. This is separate from your sign-in password and is never sent to the server." }), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleSetPassword, children: [_jsx(FormField, { label: "Encryption password", htmlFor: "key-enrollment-password", children: _jsx("input", { id: "key-enrollment-password", type: "password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: password, onChange: (e) => setPassword(e.target.value), disabled: enrolling, autoComplete: "new-password" }) }), _jsx(FormField, { label: "Confirm password", htmlFor: "key-enrollment-password-confirm", children: _jsx("input", { id: "key-enrollment-password-confirm", type: "password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), disabled: enrolling, autoComplete: "new-password" }) }), _jsx(Button, { type: "submit", loading: enrolling, disabled: enrolling, children: "Continue" })] })] }) }) }));
182
+ return (_jsx(FrameTakeover, { children: _jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Protect your mailbox" }), _jsxs("p", { className: "text-sm text-text-muted mb-5", children: ["Choose a password to protect your encryption keys. This is separate from your sign-in password and is never sent to the server. Make it ", MIN_PASSWORD_LENGTH, " or more characters: a passphrase of several unrelated words is long enough and easy to remember."] }), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleSetPassword, children: [_jsx(FormField, { label: "Encryption password", htmlFor: "key-enrollment-password", children: _jsx("input", { id: "key-enrollment-password", type: "password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: password, onChange: (e) => setPassword(e.target.value), disabled: enrolling, autoComplete: "new-password" }) }), _jsx(FormField, { label: "Confirm password", htmlFor: "key-enrollment-password-confirm", children: _jsx("input", { id: "key-enrollment-password-confirm", type: "password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), disabled: enrolling, autoComplete: "new-password" }) }), _jsx(Button, { type: "submit", loading: enrolling, disabled: enrolling, children: "Continue" })] })] }) }) }));
184
183
  }
185
184
  if (status === "already_set_up") {
186
185
  return (_jsx(FrameTakeover, { children: _jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Encryption is already set up" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "This mailbox\u2019s encryption was set up in another tab or on another device while this page was open, so nothing was changed here. Reload the page and unlock with the password chosen there." }), _jsx(Button, { type: "button", onClick: () => window.location.reload(), children: "Reload" })] }) }) }));
@@ -13,4 +13,7 @@ import React from "react";
13
13
  * refused deliberately (a 4xx, e.g. "Automatic mailbox provisioning is not enabled.") so the caller and the
14
14
  * administrator they ask can tell why - see `unavailableReason()`.
15
15
  */
16
- export default function MailboxProvisioning(): React.JSX.Element;
16
+ export default function MailboxProvisioning({ impersonating, impersonationBaseUrl }: {
17
+ impersonating?: boolean;
18
+ impersonationBaseUrl?: string;
19
+ }): React.JSX.Element;
@@ -5,7 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useState } from "react";
7
7
  import { ApiRequestError } from "../../../../lib/util/api.js";
8
- import { autoProvisionMailbox } from "../../../../lib/mail/mailApi.js";
8
+ import { autoProvisionMailbox, stopImpersonating } from "../../../../lib/mail/mailApi.js";
9
9
  import { useApiClient } from "../../../../lib/util/apiClientContext.js";
10
10
  import Alert from "../../../../lib/components/feedback/Alert.js";
11
11
  import Button from "../../../../lib/components/buttons/Button.js";
@@ -43,7 +43,7 @@ function unavailableReason(err) {
43
43
  * refused deliberately (a 4xx, e.g. "Automatic mailbox provisioning is not enabled.") so the caller and the
44
44
  * administrator they ask can tell why - see `unavailableReason()`.
45
45
  */
46
- export default function MailboxProvisioning() {
46
+ export default function MailboxProvisioning({ impersonating, impersonationBaseUrl }) {
47
47
  const client = useApiClient();
48
48
  const [status, setStatus] = useState("checking");
49
49
  const [options, setOptions] = useState([]);
@@ -55,6 +55,12 @@ export default function MailboxProvisioning() {
55
55
  // Bumped by "Retry" to re-run the check below.
56
56
  const [attempt, setAttempt] = useState(0);
57
57
  useEffect(() => {
58
+ if (impersonating) {
59
+ // An administrator viewing as a user must never create a mailbox for them just by looking: nothing is provisioned, and the way back is shown
60
+ // (this takeover hides the frame's own "Return to admin" bar).
61
+ setStatus("impersonating");
62
+ return;
63
+ }
58
64
  setStatus("checking");
59
65
  autoProvisionMailbox(undefined, client)
60
66
  .then((result) => {
@@ -87,6 +93,19 @@ export default function MailboxProvisioning() {
87
93
  }
88
94
  });
89
95
  }, [attempt]);
96
+ const [returning, setReturning] = useState(false);
97
+ async function handleReturnToAdmin() {
98
+ setReturning(true);
99
+ try {
100
+ await stopImpersonating(impersonationBaseUrl ?? "");
101
+ }
102
+ catch {
103
+ // Navigate either way, as the frame's own banner does: a failed call leaves the session as it was.
104
+ }
105
+ finally {
106
+ window.location.href = "/admin";
107
+ }
108
+ }
90
109
  async function handleConfirm() {
91
110
  // `selected` only ever takes a value from `options` itself (the initial default, or the
92
111
  // <select>'s own onChange, which can only report one of its own rendered <option> values), so
@@ -111,6 +130,9 @@ export default function MailboxProvisioning() {
111
130
  const creating = status === "creating";
112
131
  content = (_jsxs("div", { className: "w-full flex flex-col gap-3", children: [_jsx("p", { className: "text-sm text-text-muted text-center", children: "Choose your mailbox address to get started:" }), error && _jsx(Alert, { children: error }), _jsx("select", { "aria-label": "Mailbox address", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: selected, onChange: (e) => setSelected(e.target.value), disabled: creating, children: options.map((option) => (_jsx("option", { value: option.primarySmtpAddress, children: option.primarySmtpAddress }, option.primarySmtpAddress))) }), _jsx(Button, { type: "button", onClick: handleConfirm, loading: creating, disabled: creating, className: "!w-auto self-center", children: "Continue" })] }));
113
132
  }
133
+ else if (status === "impersonating") {
134
+ content = (_jsxs(_Fragment, { children: [_jsx("h1", { className: "text-lg font-bold uppercase tracking-wide", children: "No mailbox" }), _jsx("p", { className: "text-sm text-text-muted", children: "The user you are viewing as has no mailbox, and none is created for them while you are viewing as them." }), _jsx(Button, { type: "button", onClick: () => void handleReturnToAdmin(), loading: returning, disabled: returning, className: "!w-auto self-center", children: "Return to admin" })] }));
135
+ }
114
136
  else if (status === "retryable") {
115
137
  content = (_jsxs(_Fragment, { children: [_jsx("h1", { className: "text-lg font-bold uppercase tracking-wide", children: "Couldn\u2019t check right now" }), _jsx("p", { className: "text-sm text-text-muted", children: retryText }), _jsx(Button, { type: "button", onClick: () => setAttempt((n) => n + 1), className: "!w-auto self-center", children: "Retry" })] }));
116
138
  }
@@ -9,8 +9,10 @@
9
9
  */
10
10
  import React from "react";
11
11
  import { type PublicKey } from "../../../../lib/crypto/keyvaultApi.js";
12
- /** The same minimum every other "set an encryption password" form in this app uses. */
13
- export declare const MIN_PASSWORD_LENGTH = 8;
12
+ /** The same minimum every other "set an encryption password" form in this app uses. The password is the only thing
13
+ * between an offline guesser holding the server's wrapped master key and the mailbox's private keys, so it is
14
+ * long: a passphrase of a few words clears it easily. */
15
+ export declare const MIN_PASSWORD_LENGTH = 12;
14
16
  /** Where recovery codes are regenerated. */
15
17
  export declare const ENCRYPTION_SETTINGS_HREF = "/settings/encryption";
16
18
  /** Deliberately generic: a wrong code and a vault with no recovery codes fail the same way. */
@@ -22,8 +22,10 @@ import Modal from "../../../../lib/components/overlays/Modal.js";
22
22
  import Alert from "../../../../lib/components/feedback/Alert.js";
23
23
  import Button from "../../../../lib/components/buttons/Button.js";
24
24
  import FormField from "../../../../lib/components/forms/FormField.js";
25
- /** The same minimum every other "set an encryption password" form in this app uses. */
26
- export const MIN_PASSWORD_LENGTH = 8;
25
+ /** The same minimum every other "set an encryption password" form in this app uses. The password is the only thing
26
+ * between an offline guesser holding the server's wrapped master key and the mailbox's private keys, so it is
27
+ * long: a passphrase of a few words clears it easily. */
28
+ export const MIN_PASSWORD_LENGTH = 12;
27
29
  /** Where recovery codes are regenerated. */
28
30
  export const ENCRYPTION_SETTINGS_HREF = "/settings/encryption";
29
31
  /** Deliberately generic: a wrong code and a vault with no recovery codes fail the same way. */
@@ -3,13 +3,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
- import { useEffect, useState } from "react";
6
+ import { useEffect, useRef, useState } from "react";
7
7
  import { HiChevronDown, HiChevronRight, HiOutlineFlag, HiOutlinePaperClip } from "react-icons/hi2";
8
8
  import { ApiRequestError } from "../../../../lib/util/api.js";
9
9
  import { formatMailAddress } from "../../../../lib/mail/mailAddress.js";
10
10
  import MailAddress from "./MailAddress.js";
11
11
  import { EncryptedPreview, conversationLooksEncrypted, displaySubject } from "./reading/EncryptedPreview.js";
12
- import { decryptForDisplay, useDecryptedMessages, useUnlockEpoch } from "../../mail/decryptedMessages.js";
12
+ import { decryptForDisplay, mapWithConcurrency, useDecryptedMessages, useUnlockEpoch } from "../../mail/decryptedMessages.js";
13
13
  import { listConversationMessages } from "../../../../lib/mail/conversationsApi.js";
14
14
  import { useApiClient } from "../../../../lib/util/apiClientContext.js";
15
15
  import { ROW_FOCUS_CLASS, UnreadBar, UnreadLabel, dateClass, isUnread, rowClass, senderClass, subjectClass } from "./unreadStyle.js";
@@ -44,22 +44,64 @@ export default function ConversationList({ conversations, mailboxUid, selectedUi
44
44
  const unlockEpoch = useUnlockEpoch();
45
45
  // An encrypted conversation's row stands for its latest message, whose subject only decrypting can tell. Rows are decrypted as they are listed
46
46
  // whenever the mailbox is unlocked (and again once it is), the same way the message list does.
47
+ // A few at a time (the list can hold hundreds), and none more once the list has been replaced by a newer one.
47
48
  useEffect(() => {
48
- for (const conversation of conversations) {
49
- if (conversationLooksEncrypted(conversation)) {
50
- void decryptForDisplay(conversation.latestMessageUid, conversation.mailboxUid ?? mailboxUid);
49
+ let stale = false;
50
+ void mapWithConcurrency(conversations.filter(conversationLooksEncrypted), 4, async (conversation) => {
51
+ if (!stale) {
52
+ await decryptForDisplay(conversation.latestMessageUid, conversation.mailboxUid ?? mailboxUid);
51
53
  }
52
- }
54
+ });
55
+ return () => {
56
+ stale = true;
57
+ };
53
58
  }, [conversations, mailboxUid, unlockEpoch]);
54
59
  const [expanded, setExpanded] = useState(new Set());
55
60
  const [messagesById, setMessagesById] = useState({});
56
61
  const [loadingIds, setLoadingIds] = useState(new Set());
57
62
  const [errorsById, setErrorsById] = useState({});
63
+ // The messages each cached thread was fetched for (its `messageUids`): a list that now says otherwise has the thread fetched again.
64
+ const fetchedForRef = useRef({});
58
65
  // The answers given from a row's RSVP chip, by message uid: a row draws them at once, whichever row (the conversation's, or one of its messages) they were given from.
59
66
  const [answers, setAnswers] = useState({});
60
67
  function answered(updated) {
61
68
  setAnswers((prev) => ({ ...prev, [updated.uid]: updated.meetingResponse }));
62
69
  }
70
+ // New mail, a delete or a move changes a conversation's messages: what was fetched of it is out of date. An expanded thread is fetched again
71
+ // in place (the rows it shows stay until the new ones arrive); a collapsed one is forgotten, to be fetched when next opened. A conversation
72
+ // that left the list is forgotten too.
73
+ useEffect(() => {
74
+ const listed = new Set();
75
+ const stale = [];
76
+ for (const conversation of conversations) {
77
+ const id = conversationRowKey(conversation);
78
+ listed.add(id);
79
+ const fetchedFor = fetchedForRef.current[id];
80
+ if (fetchedFor === undefined || fetchedFor === conversation.messageUids.join(",")) {
81
+ continue;
82
+ }
83
+ if (expanded.has(id)) {
84
+ fetchedForRef.current[id] = conversation.messageUids.join(",");
85
+ listConversationMessages(conversation.mailboxUid ?? mailboxUid, conversation.conversationId, {}, client)
86
+ .then((loaded) => setMessagesById((prev) => ({ ...prev, [id]: loaded })))
87
+ .catch(() => undefined);
88
+ }
89
+ else {
90
+ stale.push(id);
91
+ }
92
+ }
93
+ for (const id of Object.keys(fetchedForRef.current)) {
94
+ if (!listed.has(id)) {
95
+ stale.push(id);
96
+ }
97
+ }
98
+ if (stale.length > 0) {
99
+ for (const id of stale) {
100
+ delete fetchedForRef.current[id];
101
+ }
102
+ setMessagesById((prev) => Object.fromEntries(Object.entries(prev).filter(([id]) => !stale.includes(id))));
103
+ }
104
+ }, [conversations]);
63
105
  function toggle(conversation) {
64
106
  const id = conversationRowKey(conversation);
65
107
  setExpanded((prev) => {
@@ -81,6 +123,7 @@ export default function ConversationList({ conversations, mailboxUid, selectedUi
81
123
  delete next[id];
82
124
  return next;
83
125
  });
126
+ fetchedForRef.current[id] = conversation.messageUids.join(",");
84
127
  listConversationMessages(conversation.mailboxUid ?? mailboxUid, conversation.conversationId, {}, client)
85
128
  .then((loaded) => setMessagesById((prev) => ({ ...prev, [id]: loaded })))
86
129
  .catch((err) => setErrorsById((prev) => ({
@@ -316,7 +316,7 @@ export default function ConversationThreadPane({ conversation, mailboxUid, selec
316
316
  }
317
317
  },
318
318
  track: trackMessageChange,
319
- });
319
+ }, client);
320
320
  }
321
321
  }
322
322
  }, [expandedUids, messages, client]);
@@ -1,78 +1,2 @@
1
- import React, { ReactNode } from "react";
2
- /** What every row has. `checked` turns it into a radio/checkbox item (the caller says which via `role`) and
3
- * draws the checkmark column; without it the row is a plain command. */
4
- interface MenuItemBase {
5
- key: string;
6
- label: string;
7
- /** A one-line explanation under the label, for an item whose effect isn't obvious from its name. */
8
- description?: string;
9
- role?: "menuitem" | "menuitemradio" | "menuitemcheckbox";
10
- /** `"mixed"` is ARIA's third checkbox state - some of the things this row acts on have it and some
11
- * don't, which is what a label applied to only part of a multi-message selection looks like. */
12
- checked?: boolean | "mixed";
13
- /** A colour swatch before the label - a label's own colour. */
14
- swatchColor?: string;
15
- /** An icon before the label, for a menu whose rows are commands (the reading pane's "More actions"). */
16
- icon?: ReactNode;
17
- /** A tooltip for the row - the full text of a label the row truncates. */
18
- title?: string;
19
- disabled?: boolean;
20
- /** Leaves the menu open after choosing this row, for a multi-select list where several rows are ticked
21
- * before one command commits them all. */
22
- keepOpen?: boolean;
23
- }
24
- /** A row that does something when chosen. */
25
- export interface MenuCommandSpec extends MenuItemBase {
26
- onSelect: () => void;
27
- submenu?: never;
28
- }
29
- /** A row that opens a submenu instead: choosing it replaces the menu's contents with these sections, under
30
- * a Back row. One level deep, which is all any menu here needs - so it has nothing of its own to do. */
31
- export interface MenuSubmenuSpec extends MenuItemBase {
32
- submenu: MenuSectionSpec[];
33
- onSelect?: never;
34
- }
35
- export type MenuItemSpec = MenuCommandSpec | MenuSubmenuSpec;
36
- /** A labelled group of items. Groups after the first are drawn with a separator above them. */
37
- export interface MenuSectionSpec {
38
- key: string;
39
- label?: string;
40
- /** A short note under the group's items - e.g. why some of them are unavailable right now. */
41
- note?: string;
42
- items: MenuItemSpec[];
43
- }
44
- /** The height `PopoverPortal` is asked for - exact for a short menu, capped (the list scrolls) for a long one. */
45
- export declare function menuHeight(sections: MenuSectionSpec[], width?: number): number;
46
- export interface MenuButtonProps {
47
- /** The trigger's visible label. Not drawn (nor is the chevron) with `iconOnly`, where `aria-label` is all the trigger says. */
48
- label: ReactNode;
49
- /** The trigger is just its `icon` - a round icon button like the reading pane's own command row, whose `className` it takes over
50
- * completely. `aria-label` (and `title`) name it. */
51
- iconOnly?: boolean;
52
- /** The trigger's accessible name, which also names the menu itself - includes the current selection
53
- * where there is one ("Filter: Unread"), so the button says what it's set to, not just what it does. */
54
- "aria-label": string;
55
- icon?: ReactNode;
56
- disabled?: boolean;
57
- title?: string;
58
- sections: MenuSectionSpec[];
59
- width?: number;
60
- /** Extra classes for the trigger, on top of the shared toolbar-button styling. */
61
- className?: string;
62
- /** Told whenever the menu opens or closes - how a caller resets a draft it keeps for the menu's own
63
- * multi-select rows (see `useLabelDraft()`). */
64
- onOpenChange?: (open: boolean) => void;
65
- }
66
- /**
67
- * A toolbar button that opens an ARIA menu: `role="menu"` rows with checkmarks for the current choice,
68
- * full keyboard support (Enter/Space or Arrow Down to open, arrows/Home/End to move with wrap, Enter/Space
69
- * to choose, Escape or Tab to close with focus returning to the trigger) and a click outside to dismiss.
70
- *
71
- * Rendered through `PopoverPortal` rather than as an absolutely-positioned child: the mail list toolbar
72
- * lives inside the list's own `overflow-y-auto` scroll container, which clips an absolute popup however
73
- * high its `z-index` - the same reason Compose's own pickers portal out (see `PopoverPortal`'s own doc
74
- * comment). The portal supplies the positioning, the outside-click dismissal and the box; the menu
75
- * semantics, roving focus and keyboard handling are this component's own.
76
- */
77
- export default function MenuButton({ label, iconOnly, icon, disabled, title, sections, width, className, "aria-label": ariaLabel, onOpenChange, }: MenuButtonProps): React.JSX.Element;
78
- export {};
1
+ export { default, menuHeight } from "../../../../lib/components/menus/MenuButton.js";
2
+ export type { MenuCommandSpec, MenuSubmenuSpec, MenuItemSpec, MenuSectionSpec, MenuButtonProps } from "../../../../lib/components/menus/MenuButton.js";