@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
@@ -11,12 +11,13 @@ import {
11
11
  createContact,
12
12
  deleteContact,
13
13
  listContacts,
14
+ listContactsInFolders,
14
15
  listDeletedContacts,
15
16
  setContactFavorite,
16
- contactPhotoUrl,
17
17
  updateContact,
18
18
  } from "../../../lib/contacts/contactsApi.js";
19
- import { contactsToVCardFile, contactToVCard, parseVCards } from "../../../lib/contacts/vcard.js";
19
+ import { VCARD_MAX_CHARS, VCARD_MAX_CONTACTS, ParsedVCardContact, contactsToVCardFile, contactToVCard, parseVCards } from "../../../lib/contacts/vcard.js";
20
+ import { withoutDuplicates } from "../../shared/components/contacts/participantDetails.js";
20
21
  import useIsMobile from "../../../lib/util/useIsMobile.js";
21
22
  import { useApiClient } from "../../../lib/util/apiClientContext.js";
22
23
  import { useCompose } from "../../shared/components/mail/compose/ComposeContext.js";
@@ -25,11 +26,12 @@ import ContactsSidebar, { ContactsView } from "../../shared/components/contacts/
25
26
  import ContactsToolbar from "../../shared/components/contacts/ContactsToolbar.js";
26
27
  import FloatingActionButton from "../../shared/components/layout/FloatingActionButton.js";
27
28
  import { HiOutlineUserPlus } from "react-icons/hi2";
28
- import ContactAvatar from "../../../lib/components/avatar/ContactAvatar.js";
29
+ import ContactPhotoAvatar from "../../../lib/components/avatar/ContactPhotoAvatar.js";
29
30
  import ContactDetailPane from "../../shared/components/contacts/ContactDetailPane.js";
30
31
  import ContactForm from "../../shared/components/contacts/ContactForm.js";
31
32
  import { useWritableMailboxes } from "../../shared/components/mail/writableMailboxes.js";
32
33
  import { getMyMailboxAccess } from "../../../lib/mail/mailboxAccessApi.js";
34
+ import { useMailboxUpdateAccess } from "../../shared/mail/useMailboxUpdateAccess.js";
33
35
  import Alert from "../../../lib/components/feedback/Alert.js";
34
36
  import Button from "../../../lib/components/buttons/Button.js";
35
37
  import Modal from "../../../lib/components/overlays/Modal.js";
@@ -38,6 +40,7 @@ import { clearPinnedSignerCache } from "../../shared/components/mail/pinnedSigne
38
40
  import { SHORTCUTS } from "../../shared/keyboard/keymap.js";
39
41
  import { useShortcut } from "../../shared/keyboard/useShortcut.js";
40
42
  import { notifyApiError } from "../../shared/notifications/apiErrors.js";
43
+ import { notify } from "../../shared/notifications/store.js";
41
44
 
42
45
  const INPUT_CLASS =
43
46
  "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
@@ -80,6 +83,8 @@ function ContactsContent({ userUid }: { userUid?: string }) {
80
83
  const { folderUid, mailboxUid, mailboxes } = useContactsShell();
81
84
  // The new-contact form's Mailbox choices - view-only shares are left out (see writableMailboxes.ts).
82
85
  const writableMailboxes = useWritableMailboxes(mailboxes, userUid, mailboxUid);
86
+ // A mailbox shared with the reader view-only: its contacts can be read, not changed.
87
+ const mailboxWritable = useMailboxUpdateAccess(mailboxes.find((mb) => mb.uid === mailboxUid));
83
88
  const { openCompose } = useCompose();
84
89
  const isMobile = useIsMobile();
85
90
  const navigate = useNavigate();
@@ -309,6 +314,22 @@ function ContactsContent({ userUid }: { userUid?: string }) {
309
314
  }
310
315
 
311
316
  // The bulk handlers below try every contact and report each failure: the same failure repeated is one pop-up with a count.
317
+
318
+ // A bulk action sends every checked row's own version: pressed again before it finishes, it would send stale ones and raise a stream of 409s.
319
+ const bulkRunning = useRef(false);
320
+ function singleFlight(run: () => Promise<void>): () => Promise<void> {
321
+ return async () => {
322
+ if (bulkRunning.current) {
323
+ return;
324
+ }
325
+ bulkRunning.current = true;
326
+ try {
327
+ await run();
328
+ } finally {
329
+ bulkRunning.current = false;
330
+ }
331
+ };
332
+ }
312
333
  async function handleBulkDelete() {
313
334
  setConfirmingBulkDelete(false);
314
335
  for (const contact of checkedContacts) {
@@ -339,7 +360,7 @@ function ContactsContent({ userUid }: { userUid?: string }) {
339
360
  openCompose({ to: addresses.join(", ") });
340
361
  }
341
362
 
342
- async function handleToggleFavorite() {
363
+ const handleToggleFavorite = singleFlight(async () => {
343
364
  const allFavorited = checkedContacts.every((c) => c.favorite);
344
365
  for (const contact of checkedContacts) {
345
366
  try {
@@ -350,9 +371,9 @@ function ContactsContent({ userUid }: { userUid?: string }) {
350
371
  }
351
372
  clearPinnedSignerCache();
352
373
  await reload();
353
- }
374
+ });
354
375
 
355
- async function handleAddCategory() {
376
+ const handleAddCategory = singleFlight(async () => {
356
377
  const category = window.prompt("Category name");
357
378
  if (!category?.trim()) {
358
379
  return;
@@ -369,7 +390,7 @@ function ContactsContent({ userUid }: { userUid?: string }) {
369
390
  }
370
391
  clearPinnedSignerCache();
371
392
  await reload();
372
- }
393
+ });
373
394
 
374
395
  function handleExportVCard() {
375
396
  downloadTextFile(
@@ -382,9 +403,35 @@ function ContactsContent({ userUid }: { userUid?: string }) {
382
403
  if (!mailboxUid || !folderUid) {
383
404
  return;
384
405
  }
385
- const text = await file.text();
386
- const parsed = parseVCards(text);
387
- for (const input of parsed) {
406
+ // A file is only read, and only so many records taken from it (see `parseVCards()`): one contact is one request, and a huge export would
407
+ // lock the page for minutes and flood the server. Whatever goes wrong while reading it is said, not left as an unhandled rejection.
408
+ if (file.size > VCARD_MAX_CHARS) {
409
+ notify({ kind: "error", title: "Couldn't import contacts", message: `That file is larger than ${Math.round(VCARD_MAX_CHARS / 1_000_000)} MB. Split it into smaller files.` });
410
+ return;
411
+ }
412
+ let parsed: ReturnType<typeof parseVCards>;
413
+ try {
414
+ parsed = parseVCards(await file.text());
415
+ } catch (err) {
416
+ notifyApiError(err, "Couldn't read that contacts file");
417
+ return;
418
+ }
419
+ if (parsed.length >= VCARD_MAX_CONTACTS) {
420
+ notify({ kind: "warning", title: "Only part of the file was imported", message: `A file is imported up to its first ${VCARD_MAX_CONTACTS} contacts - import the rest from another file.` });
421
+ }
422
+ // Importing a file twice must not double every contact: one whose address (or, with no address, name and phone) is already a contact's - or an
423
+ // earlier card's in the file - is left alone. The folder is read in full for that, not just the page of it the list holds.
424
+ let fresh: ParsedVCardContact[];
425
+ try {
426
+ fresh = withoutDuplicates(parsed, await listContactsInFolders([folderUid], client)).fresh;
427
+ } catch (err) {
428
+ notifyApiError(err, "Couldn't check the file against your contacts");
429
+ return;
430
+ }
431
+ if (fresh.length < parsed.length) {
432
+ notify({ kind: "info", title: "Some contacts were skipped", message: `${parsed.length - fresh.length} of the contacts in that file are already in your contacts.` });
433
+ }
434
+ for (const input of fresh) {
388
435
  try {
389
436
  await createContact({ mailboxUid, folderUid, ...input }, client);
390
437
  } catch (err) {
@@ -513,8 +560,14 @@ function ContactsContent({ userUid }: { userUid?: string }) {
513
560
  </td>
514
561
  )}
515
562
  <td className="px-3 py-2">
516
- <button type="button" onClick={() => handleSelectRow(contact)} className="flex w-full min-w-0 items-center gap-2 text-left">
517
- <ContactAvatar displayName={contact.displayName} size={28} photoUrl={contactPhotoUrl(contact)} email={contact.emails[0]?.address} />
563
+ <button
564
+ type="button"
565
+ // A deleted contact has no pane to show (it can't be restored or opened), and its route would answer 404.
566
+ disabled={isDeletedView}
567
+ onClick={() => handleSelectRow(contact)}
568
+ className="flex w-full min-w-0 items-center gap-2 text-left disabled:cursor-default"
569
+ >
570
+ <ContactPhotoAvatar lazy displayName={contact.displayName} size={28} contact={contact} email={contact.emails[0]?.address} />
518
571
  <span className="min-w-0 truncate font-medium">
519
572
  {contact.displayName}
520
573
  {contact.favorite && <span aria-label="Favorite"> ★</span>}
@@ -563,6 +616,7 @@ function ContactsContent({ userUid }: { userUid?: string }) {
563
616
  contact={selected}
564
617
  onEdit={() => setMode("edit")}
565
618
  onDelete={() => handleDelete(selected)}
619
+ canWrite={mailboxWritable}
566
620
  onKeysChanged={() => void reload()}
567
621
  onChanged={() => void reload()}
568
622
  canResolveKeys={ownsMailbox || delegateCanUpdate}
@@ -53,6 +53,7 @@ import {
53
53
  import type { Coverage } from "../shared/search/localIndexWorker.js";
54
54
  import { getUnlockedKeys, subscribeKeySession, UnlockedKeys } from "../../lib/crypto/keySession.js";
55
55
  import { useMessageAttachments } from "../../lib/mail/mailDetailHooks.js";
56
+ import { settleWithLimit } from "../../lib/util/settleWithLimit.js";
56
57
  import useIsMobile from "../../lib/util/useIsMobile.js";
57
58
  import MailShell, {
58
59
  AggregateFolderType,
@@ -71,7 +72,7 @@ import { setReadStateMany } from "../shared/mail/messageReadState.js";
71
72
  import { useMarkMessageRead } from "../shared/mail/useMarkMessageRead.js";
72
73
  import { ROW_FOCUS_CLASS, UnreadBar, UnreadLabel, dateClass, isUnread, rowClass, senderClass, subjectClass } from "../shared/components/mail/unreadStyle.js";
73
74
  import { LazyConversationThreadPane, LazyMessageDetailPane, prefetchReadingPane } from "../shared/components/mail/LazyReadingPane.js";
74
- import { rememberDecrypted } from "../shared/mail/decryptedMessages.js";
75
+ import { decryptFailedBefore, mapWithConcurrency, markDecryptFailed, rememberDecrypted } from "../shared/mail/decryptedMessages.js";
75
76
  import ConversationList, { ListedConversation, conversationRowKey } from "../shared/components/mail/ConversationList.js";
76
77
  import type { ConversationThreadHead } from "../shared/components/mail/ConversationThreadPane.js";
77
78
  import SwipeRow from "../shared/components/mail/SwipeRow.js";
@@ -104,6 +105,8 @@ import { notifyApiError } from "../shared/notifications/apiErrors.js";
104
105
  const MESSAGE_PAGE_SIZE = 50;
105
106
  const SEARCH_DEBOUNCE_MS = 300;
106
107
  const LIST_PREVIEW_MAX_LENGTH = 160;
108
+ /** How many encrypted rows are fetched and decrypted at once. */
109
+ const DECRYPT_CONCURRENCY = 4;
107
110
 
108
111
  /** The literal outer-envelope `Subject` every encrypted message carries server-side - RFC 9788's
109
112
  * `hcp_baseline` policy obscures it to this exact string (see `smimeMessage.ts`'s
@@ -147,11 +150,15 @@ function stripHtmlToText(html: string): string {
147
150
  * and `handleUnlockList()`), and a single row's fetch/decrypt failure never blocks the rest.
148
151
  */
149
152
  async function decryptEncryptedRows(messages: Message[], unlocked: UnlockedKeys): Promise<Record<string, DecryptedRow>> {
150
- const encrypted = messages.filter((m) => m.subject === ENCRYPTED_SUBJECT_PLACEHOLDER);
153
+ // A row that already came to nothing since the last unlock is not downloaded again by every refresh of the list.
154
+ const encrypted = messages.filter((m) => m.subject === ENCRYPTED_SUBJECT_PLACEHOLDER && !decryptFailedBefore(m.uid));
151
155
  // Imported once for all the rows, by whichever needs it first.
152
156
  let securityModule: Promise<typeof import("../../lib/crypto/messageSecurity.js")> | undefined;
153
- const entries = await Promise.all(
154
- encrypted.map(async (message): Promise<[string, DecryptedRow] | null> => {
157
+ // A few at a time: each is a download and a public-key decryption.
158
+ const entries = await mapWithConcurrency(
159
+ encrypted,
160
+ DECRYPT_CONCURRENCY,
161
+ async (message): Promise<[string, DecryptedRow] | null> => {
155
162
  try {
156
163
  const rawMime = await getMessageRawContent(message.uid);
157
164
  // Loaded here, on first use: the S/MIME code (PKI.js, ASN.1, X.509) is over half a megabyte and an inbox with
@@ -159,15 +166,17 @@ async function decryptEncryptedRows(messages: Message[], unlocked: UnlockedKeys)
159
166
  const { evaluateMessageSecurity } = await (securityModule ??= import("../../lib/crypto/messageSecurity.js"));
160
167
  const security = await evaluateMessageSecurity(rawMime, unlocked);
161
168
  if (!security.subject && !security.html) {
169
+ markDecryptFailed(message.uid);
162
170
  return null;
163
171
  }
164
172
  const preview = security.html ? stripHtmlToText(security.html).slice(0, LIST_PREVIEW_MAX_LENGTH) : undefined;
165
173
  rememberDecrypted(message.uid, message.mailboxUid, { subject: security.subject, preview });
166
174
  return [message.uid, { subject: security.subject, preview }];
167
175
  } catch {
176
+ markDecryptFailed(message.uid);
168
177
  return null;
169
178
  }
170
- }),
179
+ },
171
180
  );
172
181
  const result: Record<string, DecryptedRow> = {};
173
182
  for (const entry of entries) {
@@ -470,6 +479,13 @@ function appendUnseenRows<T>(shown: T[], more: T[], idOf: (row: T) => string, ca
470
479
  const messageUid = (message: Message) => message.uid;
471
480
  const conversationKey = conversationRowKey;
472
481
 
482
+ /** How many conversations' messages are requested at once when many are ticked together (Select all on a long list). */
483
+ const RESOLVE_CONVERSATIONS_AT_ONCE = 5;
484
+
485
+ /** What of a conversation's summary changes when one of its messages does: a different one means the messages fetched for it are out of date. */
486
+ const conversationSignature = (row: ListedConversation) =>
487
+ [row.messageUids.join(","), row.unreadCount, row.flagged, row.folderUids.join(","), row.latestFolderUid].join("|");
488
+
473
489
  /** Flattens and sorts a per-mailbox fetch into one merged, newest-first list - the aggregate ("All
474
490
  * Inboxes" etc.) equivalent of `mergeSearchResults()` above, but simpler: an aggregated message has no
475
491
  * natural relevance score to normalize, so this only ever sorts by `receivedDate`. */
@@ -482,7 +498,9 @@ function mergeInboxMessages(perMailbox: { mailbox: Mailbox; messages: Message[]
482
498
  /**
483
499
  * Fans out one `listMessages()` call per accessible mailbox that has a folder of `type`, merges the
484
500
  * results newest-first. A mailbox with no matching folder, or whose fetch fails, simply contributes
485
- * nothing - one mailbox's absence/failure must not blank out every other mailbox's messages.
501
+ * nothing - one mailbox's absence/failure must not blank out every other mailbox's messages. `listed`, the
502
+ * rows on screen when this is a refresh of them, makes a mailbox whose fetch fails keep the rows it has there
503
+ * instead of contributing none (a timeout must not make its mail vanish until the next success).
486
504
  *
487
505
  * **Pagination scope trim (deliberate, matching this file's own documented Tier 2/3 tradeoffs)**: there is
488
506
  * no composite cursor across an arbitrary number of independently-paginated mailboxes in this pass - this
@@ -496,6 +514,7 @@ async function fetchAggregateMessages(
496
514
  type: AggregateFolderType,
497
515
  filter: MessageListFilter,
498
516
  client?: ApiClient,
517
+ listed?: Message[],
499
518
  ): Promise<Message[]> {
500
519
  const perMailbox = await Promise.all(
501
520
  mailboxFolders.map(async ({ mailbox, folders }) => {
@@ -507,7 +526,7 @@ async function fetchAggregateMessages(
507
526
  // this function's own pagination scope trim) - each mailbox contributes its own newest page and
508
527
  // they're merged newest-first, which a different sort key couldn't be made honest across an
509
528
  // arbitrary number of independently-paged folders.
510
- const messages = await listMessages(folder.uid, { limit: MESSAGE_PAGE_SIZE, filter }, client).catch(() => [] as Message[]);
529
+ const messages = await listMessages(folder.uid, { limit: MESSAGE_PAGE_SIZE, filter }, client).catch(() => (listed ?? []).filter((m) => m.mailboxUid === mailbox.uid));
511
530
  return { mailbox, messages };
512
531
  }),
513
532
  );
@@ -518,13 +537,15 @@ async function fetchAggregateMessages(
518
537
  * The conversation-list counterpart of `fetchAggregateMessages()`: one `listConversations()` per accessible mailbox that has a folder of
519
538
  * `type`, each scoped to *that folder* (so "All Inboxes" groups only Inbox mail - not the Sent Items copy of a reply, which a mailbox-wide
520
539
  * grouping pulls in) and each row tagged with its mailbox, then merged newest activity first. Same first-page-only scope, and the same
521
- * rule that a mailbox that has no such folder or fails to answer contributes nothing rather than blanking the rest.
540
+ * rule that a mailbox that has no such folder or fails to answer contributes nothing rather than blanking the rest - or, with `listed` (the rows on
541
+ * screen, for a refresh of them), keeps the rows it has there.
522
542
  */
523
543
  async function fetchAggregateConversations(
524
544
  mailboxFolders: MailboxFolders[],
525
545
  type: AggregateFolderType,
526
546
  params: ConversationListParams,
527
547
  client?: ApiClient,
548
+ listed?: ListedConversation[],
528
549
  ): Promise<ListedConversation[]> {
529
550
  const perMailbox = await Promise.all(
530
551
  mailboxFolders.map(async ({ mailbox, folders }): Promise<ListedConversation[]> => {
@@ -532,8 +553,8 @@ async function fetchAggregateConversations(
532
553
  if (!folder) {
533
554
  return [];
534
555
  }
535
- const rows = await listConversations(mailbox.uid, { ...params, folderUid: folder.uid, page: 0 }, client).catch(() => [] as ConversationSummary[]);
536
- return rows.map((row) => ({ ...row, mailboxUid: mailbox.uid }));
556
+ const rows = await listConversations(mailbox.uid, { ...params, folderUid: folder.uid, page: 0 }, client).catch(() => null);
557
+ return rows ? rows.map((row) => ({ ...row, mailboxUid: mailbox.uid })) : (listed ?? []).filter((row) => row.mailboxUid === mailbox.uid);
537
558
  }),
538
559
  );
539
560
  return perMailbox.flat().sort((a, b) => Date.parse(b.latestDate) - Date.parse(a.latestDate));
@@ -805,6 +826,8 @@ function InboxContent({ userUid }: { userUid?: string }) {
805
826
  messagesRef.current = messages;
806
827
  const conversationsRef = useRef(conversations);
807
828
  conversationsRef.current = conversations;
829
+ const selectedConversationIdsRef = useRef(selectedConversationIds);
830
+ selectedConversationIdsRef.current = selectedConversationIds;
808
831
  // Reset to a fresh Map at the start of every new search pass (see the search effect below) - see
809
832
  // resolveHitsToMessages()'s own doc comment on why this needs to persist *within* one pass but not
810
833
  // across passes (a stale `null` for a uid that's since become resolvable elsewhere must not stick).
@@ -1485,8 +1508,9 @@ function InboxContent({ userUid }: { userUid?: string }) {
1485
1508
  try {
1486
1509
  if (asConversations && aggregateFolderType) {
1487
1510
  // No paging here either: the fresh merged first pages are the list.
1488
- const fresh = await fetchAggregateConversations(mailboxFolders, aggregateFolderType, conversationParams(0), client);
1511
+ const fresh = await fetchAggregateConversations(mailboxFolders, aggregateFolderType, conversationParams(0), client, conversationsRef.current);
1489
1512
  if (isCurrent()) {
1513
+ refreshConversationMessages(conversationsRef.current, fresh);
1490
1514
  setConversations(fresh);
1491
1515
  }
1492
1516
  } else if (asConversations) {
@@ -1495,12 +1519,13 @@ function InboxContent({ userUid }: { userUid?: string }) {
1495
1519
  return;
1496
1520
  }
1497
1521
  const merged = mergeFirstPage(conversationsRef.current, fresh, conversationKey, MESSAGE_PAGE_SIZE);
1522
+ refreshConversationMessages(conversationsRef.current, merged.rows);
1498
1523
  setConversations(merged.rows);
1499
1524
  listedOffsetRef.current = merged.complete ? fresh.length : listedOffsetRef.current + merged.added;
1500
1525
  setHasMore(!merged.complete);
1501
1526
  } else if (aggregateFolderType) {
1502
1527
  // No paging here (see `fetchAggregateMessages()`): the fresh merged first pages are the list.
1503
- const fresh = await fetchAggregateMessages(mailboxFolders, aggregateFolderType, effectiveFilter, client);
1528
+ const fresh = await fetchAggregateMessages(mailboxFolders, aggregateFolderType, effectiveFilter, client, messagesRef.current);
1504
1529
  if (isCurrent()) {
1505
1530
  setMessages(fresh);
1506
1531
  }
@@ -1954,35 +1979,67 @@ function InboxContent({ userUid }: { userUid?: string }) {
1954
1979
  });
1955
1980
  }
1956
1981
 
1957
- /** Loads (once) the messages behind each of `ids`, so a ticked conversation resolves to the messages
1958
- * every bulk action below acts on. If any of them fails to load, none of that batch stays ticked and
1959
- * a pop-up says why - better than acting on the part of a selection that happened to arrive. */
1960
- async function resolveConversations(rows: ListedConversation[]) {
1961
- const missing = rows.filter((row) => !conversationMessagesById[conversationKey(row)]);
1982
+ /** Loads (once, or again with `refetch`) the messages behind each of `rows`, so a ticked conversation resolves to the messages
1983
+ * every bulk action below acts on - a few requests at a time (Select all on a long list is hundreds of rows). A conversation
1984
+ * that fails to load is unticked, with one pop-up saying why - better than acting on a part of it - and the rest stay ticked.
1985
+ * What arrives after the list was loaded again is dropped: those rows are gone. */
1986
+ async function resolveConversations(rows: ListedConversation[], refetch = false) {
1987
+ const missing = rows.filter((row) => refetch || !conversationMessagesById[conversationKey(row)]);
1962
1988
  if (missing.length === 0) {
1963
1989
  return;
1964
1990
  }
1991
+ const listRun = searchRunIdRef.current;
1965
1992
  setResolvingSelection((n) => n + 1);
1966
1993
  try {
1967
- const loaded = await Promise.all(
1994
+ const outcomes = await settleWithLimit(missing, RESOLVE_CONVERSATIONS_AT_ONCE, (row) =>
1968
1995
  // A row of the "All mailboxes" view names its own mailbox; any other list is the open mailbox's.
1969
- missing.map(async (row) => [conversationKey(row), await listConversationMessages(row.mailboxUid ?? activeMailboxUid, row.conversationId, {}, client)] as const),
1996
+ searchRunIdRef.current === listRun
1997
+ ? listConversationMessages(row.mailboxUid ?? activeMailboxUid, row.conversationId, {}, client)
1998
+ : Promise.reject(new Error("The list was loaded again")),
1970
1999
  );
1971
- setConversationMessagesById((prev) => ({ ...prev, ...Object.fromEntries(loaded) }));
1972
- } catch (err) {
1973
- notifyApiError(err, "Couldn't load the messages in one of those conversations");
1974
- setSelectedConversationIds((prev) => {
1975
- const next = new Set(prev);
1976
- for (const row of missing) {
1977
- next.delete(conversationKey(row));
2000
+ if (searchRunIdRef.current !== listRun) {
2001
+ return;
2002
+ }
2003
+ const loaded: Record<string, Message[]> = {};
2004
+ const failed = new Set<string>();
2005
+ let firstFailure: unknown;
2006
+ outcomes.forEach((outcome, index) => {
2007
+ const key = conversationKey(missing[index]);
2008
+ if (outcome.status === "fulfilled") {
2009
+ loaded[key] = outcome.value;
2010
+ } else {
2011
+ failed.add(key);
2012
+ firstFailure ??= outcome.reason;
1978
2013
  }
1979
- return next;
1980
2014
  });
2015
+ setConversationMessagesById((prev) => ({ ...prev, ...loaded }));
2016
+ if (failed.size > 0) {
2017
+ notifyApiError(firstFailure, "Couldn't load the messages in one of those conversations");
2018
+ setSelectedConversationIds((prev) => new Set([...prev].filter((key) => !failed.has(key))));
2019
+ }
1981
2020
  } finally {
1982
2021
  setResolvingSelection((n) => n - 1);
1983
2022
  }
1984
2023
  }
1985
2024
 
2025
+ /**
2026
+ * Called as a refresh of the list swaps in `after` for `before`: the messages kept for a conversation whose summary changed (a reply arrived, a
2027
+ * message was read or moved) are out of date - the next action on it would act on a list missing the new message, or send an old `version`. The
2028
+ * cached messages of one that is not ticked are dropped, to be fetched when next wanted; a ticked one's are fetched again now, the old ones standing
2029
+ * until they arrive.
2030
+ */
2031
+ function refreshConversationMessages(before: ListedConversation[], after: ListedConversation[]) {
2032
+ const was = new Map(before.map((row) => [conversationKey(row), conversationSignature(row)]));
2033
+ const changed = after.filter((row) => was.has(conversationKey(row)) && was.get(conversationKey(row)) !== conversationSignature(row));
2034
+ if (changed.length === 0) {
2035
+ return;
2036
+ }
2037
+ const ticked = changed.filter((row) => selectedConversationIdsRef.current.has(conversationKey(row)));
2038
+ const stale = new Set(changed.filter((row) => !ticked.includes(row)).flatMap((row) => [conversationKey(row), row.conversationId]));
2039
+ setConversationMessagesById((prev) => Object.fromEntries(Object.entries(prev).filter(([key]) => !stale.has(key))));
2040
+ void resolveConversations(ticked, true);
2041
+ }
2042
+
1986
2043
  function toggleConversationSelected(conversation: ListedConversation) {
1987
2044
  const id = conversationKey(conversation);
1988
2045
  const ticking = !selectedConversationIds.has(id);
@@ -2251,7 +2308,7 @@ function InboxContent({ userUid }: { userUid?: string }) {
2251
2308
 
2252
2309
  /** Empty folder: permanently deletes everything in the folder being listed (Deleted Items or Junk Email) after the reader has confirmed. */
2253
2310
  async function emptyListedFolder(folder: EmptiableFolder, count: number | undefined) {
2254
- const outcome = await permanent.requestEmptyFolder({ uid: folder.uid, name: folder.name }, count);
2311
+ const outcome = await permanent.requestEmptyFolder(folder, count);
2255
2312
  if (!outcome) {
2256
2313
  return;
2257
2314
  }
@@ -2799,6 +2856,12 @@ function InboxContent({ userUid }: { userUid?: string }) {
2799
2856
  uid: emptiableFolders[0].uid,
2800
2857
  name: aggregateFolderType ? (emptiableFolders[0].type === "junk" ? "Junk Email" : "Deleted Items") : emptiableFolders[0].name,
2801
2858
  uids: aggregateFolderType ? emptiableFolders.map((f) => f.uid) : undefined,
2859
+ // The mailboxes whose folders these are, for the confirmation to name (it empties each of them, a share included).
2860
+ scope: aggregateFolderType
2861
+ ? mailboxFolders
2862
+ .filter((entry) => entry.folders.some((f) => f.type === aggregateFolderType))
2863
+ .map(({ mailbox }) => `${mailbox.displayName}${mailbox.ownerUserUid ? "" : " (shared)"}`)
2864
+ : undefined,
2802
2865
  };
2803
2866
  // How many it holds as the sidebar's counts have it (kept right as messages leave), unless that says none while the list shows some.
2804
2867
  const emptiableTotal = emptiableFolders.reduce((sum, f) => sum + folderCountOf(f).total, 0);
@@ -40,12 +40,17 @@ function MessageDetailContent({ uid }: { uid: string }) {
40
40
  const conversationId: string | null = useSearchParams()[0].get("conversation");
41
41
 
42
42
  useEffect(() => {
43
+ // An answer for a message since replaced (the uid changed without the page remounting) must not be shown as the current one.
44
+ let cancelled = false;
43
45
  setLoading(true);
44
46
  setError(null);
45
47
  getMessage(uid, client)
46
- .then(setMessage)
47
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this message."))
48
- .finally(() => setLoading(false));
48
+ .then((loaded) => !cancelled && setMessage(loaded))
49
+ .catch((err) => !cancelled && setError(err instanceof ApiRequestError ? err.message : "Could not load this message."))
50
+ .finally(() => !cancelled && setLoading(false));
51
+ return () => {
52
+ cancelled = true;
53
+ };
49
54
  }, [uid, client]);
50
55
 
51
56
  // See `apps/www/index.tsx`'s identical effect's own doc comment - a failure here just hides the
@@ -9,6 +9,7 @@ import { updateMailbox } from "../../../../lib/mail/mailApi.js";
9
9
  import { useApiClient } from "../../../../lib/util/apiClientContext.js";
10
10
  import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
11
11
  import Button from "../../../../lib/components/buttons/Button.js";
12
+ import Alert from "../../../../lib/components/feedback/Alert.js";
12
13
  import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
13
14
 
14
15
  const INPUT_CLASS =
@@ -41,10 +42,21 @@ function AutoReplyContent() {
41
42
  const [version, setVersion] = useState(mailbox.version);
42
43
  const [saving, setSaving] = useState(false);
43
44
  const [saved, setSaved] = useState(false);
45
+ const [error, setError] = useState<string | null>(null);
44
46
 
45
47
  async function handleSubmit(e: FormEvent) {
46
48
  e.preventDefault();
47
49
  setSaved(false);
50
+ // Turned on with nothing to send, or over a window that ends before it starts, would answer people with an empty message or never at all.
51
+ if (oofEnabled && !oofMessage.trim()) {
52
+ setError("Write the message to send while automatic replies are on.");
53
+ return;
54
+ }
55
+ if (oofEnabled && oofStartTime && oofEndTime && new Date(oofEndTime) <= new Date(oofStartTime)) {
56
+ setError("The end must be after the start.");
57
+ return;
58
+ }
59
+ setError(null);
48
60
  setSaving(true);
49
61
  try {
50
62
  const updated = await updateMailbox({
@@ -75,6 +87,7 @@ function AutoReplyContent() {
75
87
  </p>
76
88
 
77
89
  {saved && <div className="mb-4 text-sm text-success font-medium">Saved.</div>}
90
+ {error && <Alert>{error}</Alert>}
78
91
 
79
92
  <form onSubmit={handleSubmit} className="flex flex-col gap-4">
80
93
  <label className="flex items-center gap-2 text-sm font-medium">
@@ -93,6 +93,10 @@ function ListSection({ kind, entries, otherEntries, ownAddresses, editable, onAd
93
93
  setError("That is one of your own addresses, so it cannot be blocked.");
94
94
  return;
95
95
  }
96
+ if (kind === "blocked" && checked.entry.startsWith("@") && ownAddresses.some((address) => address.toLowerCase().endsWith(checked.entry))) {
97
+ setError("That is your own domain: blocking it would send the mail of everyone at it - yours included - to Junk.");
98
+ return;
99
+ }
96
100
  setError(null);
97
101
  setBusy(true);
98
102
  try {