@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
@@ -2,16 +2,16 @@
2
2
  // Copyright (C) 2026 Jean-Philippe Steinmetz
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
- import React, { useEffect, useRef, useState } from "react";
5
+ import React, { useEffect, useState } from "react";
6
6
  import ContactAvatar from "../../../../lib/components/avatar/ContactAvatar.js";
7
- import Button from "../../../../lib/components/buttons/Button.js";
8
- import { CONTACT_PHOTO_MAX_BYTES, CONTACT_PHOTO_TYPES } from "../../../../lib/contacts/contactsApi.js";
7
+ import { ContactPhotoError, prepareContactPhoto } from "../../../../lib/contacts/preparePhoto.js";
8
+ import ImageEditBadge from "../../../../lib/components/forms/ImageEditBadge.js";
9
9
 
10
10
  export interface ContactPhotoFieldProps {
11
11
  displayName: string;
12
12
  /** The contact's first address, for the Gravatar shown while they have no picture of their own. */
13
13
  email?: string;
14
- /** The picture the contact has now (`contactPhotoUrl()`), if any. */
14
+ /** The picture the contact has now (`useContactPhotoSrc()`), if any. */
15
15
  currentUrl?: string;
16
16
  /** The picture chosen to replace it, not uploaded until the contact is saved; `null` for none. */
17
17
  picked: File | null;
@@ -21,15 +21,18 @@ export interface ContactPhotoFieldProps {
21
21
  onRemove: () => void;
22
22
  /** Called with the reason a chosen file was turned down. */
23
23
  onReject: (message: string) => void;
24
+ /** Called with `true` while a chosen file is being made ready, so the form is not saved before it is, and with `false` after. */
25
+ onBusyChange: (busy: boolean) => void;
24
26
  }
25
27
 
26
28
  /**
27
- * 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
28
- * 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.
29
+ * 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
30
+ * 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,
31
+ * in any orientation, included): it is made small enough here by `prepareContactPhoto()` and only uploaded when the form is saved.
29
32
  */
30
- export default function ContactPhotoField({ displayName, email, currentUrl, picked, removed, onPick, onRemove, onReject }: ContactPhotoFieldProps) {
31
- const input = useRef<HTMLInputElement>(null);
33
+ export default function ContactPhotoField({ displayName, email, currentUrl, picked, removed, onPick, onRemove, onReject, onBusyChange }: ContactPhotoFieldProps) {
32
34
  const [preview, setPreview] = useState<string | undefined>();
35
+ const [preparing, setPreparing] = useState(false);
33
36
 
34
37
  useEffect(() => {
35
38
  if (!picked) {
@@ -41,16 +44,16 @@ export default function ContactPhotoField({ displayName, email, currentUrl, pick
41
44
  return () => URL.revokeObjectURL(url);
42
45
  }, [picked]);
43
46
 
44
- function handleChange(file: File | undefined) {
45
- if (!file) {
46
- return;
47
- }
48
- if (!CONTACT_PHOTO_TYPES.includes(file.type)) {
49
- onReject("Choose a JPEG, PNG, GIF or WebP image.");
50
- } else if (file.size > CONTACT_PHOTO_MAX_BYTES) {
51
- onReject("Choose an image no larger than 1 MB.");
52
- } else {
53
- onPick(file);
47
+ async function handleChange(file: File) {
48
+ setPreparing(true);
49
+ onBusyChange(true);
50
+ try {
51
+ onPick(await prepareContactPhoto(file));
52
+ } catch (err) {
53
+ onReject(err instanceof ContactPhotoError ? err.message : "This picture could not be read - choose another one.");
54
+ } finally {
55
+ setPreparing(false);
56
+ onBusyChange(false);
54
57
  }
55
58
  }
56
59
 
@@ -58,29 +61,23 @@ export default function ContactPhotoField({ displayName, email, currentUrl, pick
58
61
  const hasOwn = !!picked || (!!currentUrl && !removed);
59
62
  return (
60
63
  <div className="flex items-center gap-4 mb-3">
61
- <ContactAvatar displayName={displayName || "?"} size={72} photoUrl={shown} email={picked || (currentUrl && !removed) ? undefined : email} />
62
- <div className="flex flex-wrap gap-2">
63
- <input
64
- ref={input}
65
- type="file"
66
- accept={CONTACT_PHOTO_TYPES.join(",")}
67
- aria-label="Contact photo file"
68
- className="sr-only"
69
- tabIndex={-1}
70
- onChange={(e) => {
71
- handleChange(e.target.files?.[0]);
72
- e.target.value = "";
73
- }}
64
+ <div className="relative inline-block shrink-0">
65
+ <ContactAvatar displayName={displayName || "?"} size={72} photoUrl={shown} email={picked || (currentUrl && !removed) ? undefined : email} />
66
+ <ImageEditBadge
67
+ position="bottom-right"
68
+ label="Change contact photo"
69
+ fileInputLabel="Contact photo file"
70
+ hasImage={hasOwn}
71
+ busy={preparing}
72
+ onFile={(file) => void handleChange(file)}
73
+ onRemove={onRemove}
74
74
  />
75
- <Button type="button" variant="secondary" className="!w-auto !py-1 !px-3 text-xs" onClick={() => input.current?.click()}>
76
- {hasOwn ? "Change photo" : "Add photo"}
77
- </Button>
78
- {hasOwn && (
79
- <Button type="button" variant="secondary" className="!w-auto !py-1 !px-3 text-xs" onClick={onRemove}>
80
- Remove photo
81
- </Button>
82
- )}
83
75
  </div>
76
+ {preparing && (
77
+ <span role="status" className="text-xs text-text-muted">
78
+ Preparing picture…
79
+ </span>
80
+ )}
84
81
  </div>
85
82
  );
86
83
  }
@@ -113,7 +113,7 @@ export default function ContactsShell({
113
113
  // A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else
114
114
  // for a mailbox-less caller to do here yet, so the icon rail/header don't render at all.
115
115
  if (userUid && status === "ready" && !mailboxUid) {
116
- return <MailboxProvisioning />;
116
+ return <MailboxProvisioning impersonating={impersonating} impersonationBaseUrl={impersonationBaseUrl} />;
117
117
  }
118
118
 
119
119
  let inner: ReactNode = null;
@@ -128,15 +128,32 @@ export function contactInputFromCard(card: ParsedVCardContact, scope: { mailboxU
128
128
  return { ...card, ...scope, ...names, emails: card.emails ?? [], phones: card.phones ?? [], addresses: card.addresses ?? [] };
129
129
  }
130
130
 
131
- /** 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. */
131
+ /** What a card with no email is told apart by: its name and its first phone number (digits only). */
132
+ function identityKey(person: { displayName: string; phones?: readonly { phoneNumber: string }[] }): string {
133
+ return `${person.displayName.trim().toLowerCase()}|${(person.phones?.[0]?.phoneNumber ?? "").replace(/\D/g, "")}`;
134
+ }
135
+
136
+ /**
137
+ * 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,
138
+ * 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).
139
+ */
132
140
  export function withoutDuplicates(
133
141
  cards: readonly ParsedVCardContact[],
134
142
  existing: readonly Contact[],
135
143
  ): { fresh: ParsedVCardContact[]; skipped: number } {
136
144
  const known = new Set(existing.flatMap((contact) => contact.emails.map((email) => normalizeAddress(email.address))));
145
+ const knownIdentities = new Set(existing.map(identityKey));
137
146
  const fresh: ParsedVCardContact[] = [];
138
147
  for (const card of cards) {
139
148
  const addresses = (card.emails ?? []).map((email) => normalizeAddress(email.address));
149
+ if (addresses.length === 0) {
150
+ const identity = identityKey(card);
151
+ if (!knownIdentities.has(identity)) {
152
+ knownIdentities.add(identity);
153
+ fresh.push(card);
154
+ }
155
+ continue;
156
+ }
140
157
  if (addresses.some((address) => known.has(address))) {
141
158
  continue;
142
159
  }
@@ -34,6 +34,8 @@ import { SIGN_OUT_CHANNEL, destroyAllLocalIndexes } from "../../search/localInde
34
34
  import { authApiFetch, setApiUnauthorizedObserver } from "../../../../lib/util/api.js";
35
35
  import { destroyUnlockedKeys } from "../../../../lib/crypto/keySession.js";
36
36
  import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
37
+ import { resetNotifications } from "../../notifications/store.js";
38
+ import { clearContactPhotoCache } from "../../../../lib/contacts/useContactPhotoSrc.js";
37
39
  import { pluginIcon } from "../../plugins/pluginIcons.js";
38
40
  import { mergePluginNavItems, PluginNav, PluginNavProps } from "../../plugins/pluginNav.js";
39
41
  import { type ResolvedRailLinks, useResolvedRailItems } from "../../plugins/useResolvedRailItems.js";
@@ -266,6 +268,9 @@ export function AppChrome({
266
268
  destroyUnlockedKeys();
267
269
  clearPinnedSignerCache();
268
270
  clearAppearanceCache();
271
+ clearContactPhotoCache();
272
+ // The notification history (meeting titles, error details) is this session's, and the tab's storage outlives it.
273
+ resetNotifications();
269
274
  // Bounded by its own timeout and never rejects - awaited so navigating doesn't kill the Worker mid-delete.
270
275
  void destroyAllLocalIndexes().then(() => {
271
276
  window.location.href = authServerUrl ?? "/";
@@ -284,6 +289,8 @@ export function AppChrome({
284
289
  // Trusted signer pins read from contacts don't outlive the session either.
285
290
  clearPinnedSignerCache();
286
291
  clearAppearanceCache();
292
+ clearContactPhotoCache();
293
+ resetNotifications();
287
294
  // Open compose windows save edits still waiting on their autosave debounce while the session is still
288
295
  // valid - logout invalidates it. Bounded the same way as logout itself, and never rejects.
289
296
  await flushComposeDrafts(LOGOUT_TIMEOUT_MS);
@@ -306,6 +313,8 @@ export function AppChrome({
306
313
  // they were, so there's nothing else useful to show — matching this app's other network-error
307
314
  // handling, which surfaces via a full reload rather than an inline retry affordance.
308
315
  } finally {
316
+ // What the impersonated user's session raised (their meetings, their errors) must not be left for the administrator to read.
317
+ resetNotifications();
309
318
  window.location.href = "/admin";
310
319
  }
311
320
  }
@@ -10,7 +10,13 @@ import FormField from "../../../../lib/components/forms/FormField.js";
10
10
  import { enrollKey, getKeyVault, PublicKey, VaultAlreadyInitializedError } from "../../../../lib/crypto/keyvaultApi.js";
11
11
  import { UnopenableKeysNotice, unlockErrorMessage } from "./UnlockPromptProvider.js";
12
12
  import { FrameTakeover } from "../../navigation/frameContext.js";
13
- import { RecoveryFollowUp, RecoveryFollowUpModal, UnlockModeToggle, startRecoveryUnlock } from "./RecoveryCodeUnlock.js";
13
+ import {
14
+ MIN_PASSWORD_LENGTH,
15
+ RecoveryFollowUp,
16
+ RecoveryFollowUpModal,
17
+ UnlockModeToggle,
18
+ startRecoveryUnlock,
19
+ } from "./RecoveryCodeUnlock.js";
14
20
  import {
15
21
  ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE,
16
22
  getUnlockedKeys,
@@ -23,8 +29,6 @@ import { exportPrivateKeyPkcs8, generateKeyPairWithCsr } from "../../../../lib/c
23
29
 
24
30
  type Status = "checking" | "setup_password" | "enrolling" | "already_set_up" | "show_recovery_codes" | "unlock" | "unlocking" | "ready";
25
31
 
26
- const MIN_PASSWORD_LENGTH = 8;
27
-
28
32
  async function provisionEncryptionKey(
29
33
  mailboxUid: string,
30
34
  mailboxAddress: string,
@@ -299,7 +303,8 @@ export default function KeyEnrollmentGate({
299
303
  <h1 className="text-lg font-bold mb-2">Protect your mailbox</h1>
300
304
  <p className="text-sm text-text-muted mb-5">
301
305
  Choose a password to protect your encryption keys. This is separate from your sign-in
302
- password and is never sent to the server.
306
+ password and is never sent to the server. Make it {MIN_PASSWORD_LENGTH} or more characters: a
307
+ passphrase of several unrelated words is long enough and easy to remember.
303
308
  </p>
304
309
  {error && <Alert>{error}</Alert>}
305
310
  <form onSubmit={handleSetPassword}>
@@ -4,13 +4,13 @@
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { useEffect, useState } from "react";
6
6
  import { ApiRequestError } from "../../../../lib/util/api.js";
7
- import { MailboxAutoProvisionAliasOption, autoProvisionMailbox } from "../../../../lib/mail/mailApi.js";
7
+ import { MailboxAutoProvisionAliasOption, autoProvisionMailbox, stopImpersonating } from "../../../../lib/mail/mailApi.js";
8
8
  import { useApiClient } from "../../../../lib/util/apiClientContext.js";
9
9
  import Alert from "../../../../lib/components/feedback/Alert.js";
10
10
  import Button from "../../../../lib/components/buttons/Button.js";
11
11
  import { FrameTakeover } from "../../navigation/frameContext.js";
12
12
 
13
- type Status = "checking" | "needs_selection" | "creating" | "unavailable" | "retryable";
13
+ type Status = "checking" | "needs_selection" | "creating" | "unavailable" | "retryable" | "impersonating";
14
14
 
15
15
  /** What the "Retry" screen says: the server couldn't read its provisioning policy (503) ... */
16
16
  const RETRY_POLICY_TEXT = "We couldn\u2019t check whether a mailbox can be set up for you. Please try again.";
@@ -48,7 +48,7 @@ function unavailableReason(err: unknown): string | null {
48
48
  * refused deliberately (a 4xx, e.g. "Automatic mailbox provisioning is not enabled.") so the caller and the
49
49
  * administrator they ask can tell why - see `unavailableReason()`.
50
50
  */
51
- export default function MailboxProvisioning() {
51
+ export default function MailboxProvisioning({ impersonating, impersonationBaseUrl }: { impersonating?: boolean; impersonationBaseUrl?: string }) {
52
52
  const client = useApiClient();
53
53
  const [status, setStatus] = useState<Status>("checking");
54
54
  const [options, setOptions] = useState<MailboxAutoProvisionAliasOption[]>([]);
@@ -62,6 +62,12 @@ export default function MailboxProvisioning() {
62
62
  const [attempt, setAttempt] = useState(0);
63
63
 
64
64
  useEffect(() => {
65
+ if (impersonating) {
66
+ // 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
67
+ // (this takeover hides the frame's own "Return to admin" bar).
68
+ setStatus("impersonating");
69
+ return;
70
+ }
65
71
  setStatus("checking");
66
72
  autoProvisionMailbox(undefined, client)
67
73
  .then((result) => {
@@ -93,6 +99,18 @@ export default function MailboxProvisioning() {
93
99
  });
94
100
  }, [attempt]);
95
101
 
102
+ const [returning, setReturning] = useState(false);
103
+ async function handleReturnToAdmin() {
104
+ setReturning(true);
105
+ try {
106
+ await stopImpersonating(impersonationBaseUrl ?? "");
107
+ } catch {
108
+ // Navigate either way, as the frame's own banner does: a failed call leaves the session as it was.
109
+ } finally {
110
+ window.location.href = "/admin";
111
+ }
112
+ }
113
+
96
114
  async function handleConfirm() {
97
115
  // `selected` only ever takes a value from `options` itself (the initial default, or the
98
116
  // <select>'s own onChange, which can only report one of its own rendered <option> values), so
@@ -136,6 +154,16 @@ export default function MailboxProvisioning() {
136
154
  </Button>
137
155
  </div>
138
156
  );
157
+ } else if (status === "impersonating") {
158
+ content = (
159
+ <>
160
+ <h1 className="text-lg font-bold uppercase tracking-wide">No mailbox</h1>
161
+ <p className="text-sm text-text-muted">The user you are viewing as has no mailbox, and none is created for them while you are viewing as them.</p>
162
+ <Button type="button" onClick={() => void handleReturnToAdmin()} loading={returning} disabled={returning} className="!w-auto self-center">
163
+ Return to admin
164
+ </Button>
165
+ </>
166
+ );
139
167
  } else if (status === "retryable") {
140
168
  content = (
141
169
  <>
@@ -26,8 +26,10 @@ import Alert from "../../../../lib/components/feedback/Alert.js";
26
26
  import Button from "../../../../lib/components/buttons/Button.js";
27
27
  import FormField from "../../../../lib/components/forms/FormField.js";
28
28
 
29
- /** The same minimum every other "set an encryption password" form in this app uses. */
30
- export const MIN_PASSWORD_LENGTH = 8;
29
+ /** The same minimum every other "set an encryption password" form in this app uses. The password is the only thing
30
+ * between an offline guesser holding the server's wrapped master key and the mailbox's private keys, so it is
31
+ * long: a passphrase of a few words clears it easily. */
32
+ export const MIN_PASSWORD_LENGTH = 12;
31
33
 
32
34
  /** Where recovery codes are regenerated. */
33
35
  export const ENCRYPTION_SETTINGS_HREF = "/settings/encryption";
@@ -2,14 +2,14 @@
2
2
  // Copyright (C) 2026 Jean-Philippe Steinmetz
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
- import React, { useEffect, useState } from "react";
5
+ import React, { useEffect, useRef, useState } from "react";
6
6
  import { HiChevronDown, HiChevronRight, HiOutlineFlag, HiOutlinePaperClip } from "react-icons/hi2";
7
7
  import { ApiRequestError } from "../../../../lib/util/api.js";
8
8
  import { Message } from "../../../../lib/mail/mailApi.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 { ConversationSummary, 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";
@@ -120,17 +120,24 @@ export default function ConversationList({
120
120
  const unlockEpoch = useUnlockEpoch();
121
121
  // An encrypted conversation's row stands for its latest message, whose subject only decrypting can tell. Rows are decrypted as they are listed
122
122
  // whenever the mailbox is unlocked (and again once it is), the same way the message list does.
123
+ // A few at a time (the list can hold hundreds), and none more once the list has been replaced by a newer one.
123
124
  useEffect(() => {
124
- for (const conversation of conversations) {
125
- if (conversationLooksEncrypted(conversation)) {
126
- void decryptForDisplay(conversation.latestMessageUid, conversation.mailboxUid ?? mailboxUid);
125
+ let stale = false;
126
+ void mapWithConcurrency(conversations.filter(conversationLooksEncrypted), 4, async (conversation) => {
127
+ if (!stale) {
128
+ await decryptForDisplay(conversation.latestMessageUid, conversation.mailboxUid ?? mailboxUid);
127
129
  }
128
- }
130
+ });
131
+ return () => {
132
+ stale = true;
133
+ };
129
134
  }, [conversations, mailboxUid, unlockEpoch]);
130
135
  const [expanded, setExpanded] = useState<Set<string>>(new Set());
131
136
  const [messagesById, setMessagesById] = useState<Record<string, Message[]>>({});
132
137
  const [loadingIds, setLoadingIds] = useState<Set<string>>(new Set());
133
138
  const [errorsById, setErrorsById] = useState<Record<string, string>>({});
139
+ // The messages each cached thread was fetched for (its `messageUids`): a list that now says otherwise has the thread fetched again.
140
+ const fetchedForRef = useRef<Record<string, string>>({});
134
141
  // 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.
135
142
  const [answers, setAnswers] = useState<Record<string, Message["meetingResponse"]>>({});
136
143
 
@@ -138,6 +145,41 @@ export default function ConversationList({
138
145
  setAnswers((prev) => ({ ...prev, [updated.uid]: updated.meetingResponse }));
139
146
  }
140
147
 
148
+ // 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
149
+ // 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
150
+ // that left the list is forgotten too.
151
+ useEffect(() => {
152
+ const listed = new Set<string>();
153
+ const stale: string[] = [];
154
+ for (const conversation of conversations) {
155
+ const id = conversationRowKey(conversation);
156
+ listed.add(id);
157
+ const fetchedFor = fetchedForRef.current[id];
158
+ if (fetchedFor === undefined || fetchedFor === conversation.messageUids.join(",")) {
159
+ continue;
160
+ }
161
+ if (expanded.has(id)) {
162
+ fetchedForRef.current[id] = conversation.messageUids.join(",");
163
+ listConversationMessages(conversation.mailboxUid ?? mailboxUid, conversation.conversationId, {}, client)
164
+ .then((loaded) => setMessagesById((prev) => ({ ...prev, [id]: loaded })))
165
+ .catch(() => undefined);
166
+ } else {
167
+ stale.push(id);
168
+ }
169
+ }
170
+ for (const id of Object.keys(fetchedForRef.current)) {
171
+ if (!listed.has(id)) {
172
+ stale.push(id);
173
+ }
174
+ }
175
+ if (stale.length > 0) {
176
+ for (const id of stale) {
177
+ delete fetchedForRef.current[id];
178
+ }
179
+ setMessagesById((prev) => Object.fromEntries(Object.entries(prev).filter(([id]) => !stale.includes(id))));
180
+ }
181
+ }, [conversations]);
182
+
141
183
  function toggle(conversation: ListedConversation) {
142
184
  const id = conversationRowKey(conversation);
143
185
  setExpanded((prev) => {
@@ -158,6 +200,7 @@ export default function ConversationList({
158
200
  delete next[id];
159
201
  return next;
160
202
  });
203
+ fetchedForRef.current[id] = conversation.messageUids.join(",");
161
204
  listConversationMessages(conversation.mailboxUid ?? mailboxUid, conversation.conversationId, {}, client)
162
205
  .then((loaded) => setMessagesById((prev) => ({ ...prev, [id]: loaded })))
163
206
  .catch((err) =>
@@ -380,7 +380,7 @@ export default function ConversationThreadPane({
380
380
  }
381
381
  },
382
382
  track: trackMessageChange,
383
- });
383
+ }, client);
384
384
  }
385
385
  }
386
386
  }, [expandedUids, messages, client]);