@rapidmx/web-client 0.30.0 → 0.32.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 (402) 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 +3 -3
  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/diagnostics/diagnosticsApi.ts +2 -2
  33. package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
  34. package/apps/shared/components/admin/elevation.ts +19 -0
  35. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
  36. package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
  37. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
  38. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
  39. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
  40. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  41. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
  42. package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
  43. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
  44. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
  45. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
  46. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
  47. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
  48. package/apps/shared/components/admin/settings/PluginsManager.tsx +195 -46
  49. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
  50. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
  51. package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
  52. package/apps/shared/components/admin/signOut.ts +3 -0
  53. package/apps/shared/components/admin/usePagedList.tsx +2 -2
  54. package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
  55. package/apps/shared/components/calendar/EventDetails.tsx +34 -8
  56. package/apps/shared/components/calendar/EventEditor.tsx +90 -40
  57. package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
  58. package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
  59. package/apps/shared/components/calendar/EventModal.tsx +25 -1
  60. package/apps/shared/components/calendar/FindATime.tsx +10 -2
  61. package/apps/shared/components/calendar/ListView.tsx +99 -25
  62. package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
  63. package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
  64. package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
  65. package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
  66. package/apps/shared/components/calendar/gridLayout.ts +94 -0
  67. package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
  68. package/apps/shared/components/calendar/timePicker.ts +5 -2
  69. package/apps/shared/components/contacts/ContactCard.tsx +15 -4
  70. package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
  71. package/apps/shared/components/contacts/ContactForm.tsx +61 -17
  72. package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
  73. package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
  74. package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
  75. package/apps/shared/components/contacts/participantDetails.ts +18 -1
  76. package/apps/shared/components/layout/AppShell.tsx +9 -0
  77. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
  78. package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
  79. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
  80. package/apps/shared/components/mail/ConversationList.tsx +49 -6
  81. package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
  82. package/apps/shared/components/mail/MenuButton.tsx +3 -400
  83. package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
  84. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
  85. package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
  86. package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
  87. package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
  88. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
  89. package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
  90. package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
  91. package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
  92. package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
  93. package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
  94. package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
  95. package/apps/shared/components/mail/writableMailboxes.ts +2 -0
  96. package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
  97. package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
  98. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
  99. package/apps/shared/components/tasks/dueDate.ts +29 -0
  100. package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
  101. package/apps/shared/mail/decryptedMessages.ts +51 -2
  102. package/apps/shared/mail/listSnapshots.ts +2 -0
  103. package/apps/shared/mail/outbox/sendDecision.ts +14 -3
  104. package/apps/shared/mail/outbox/sendJob.ts +5 -0
  105. package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
  106. package/apps/shared/mail/usePermanentDelete.tsx +3 -1
  107. package/apps/shared/notifications/apiErrors.ts +2 -0
  108. package/apps/shared/notifications/preferences.ts +1 -1
  109. package/apps/shared/notifications/store.ts +3 -1
  110. package/apps/shared/plugins/pluginNav.ts +4 -2
  111. package/apps/www/calendar/index.tsx +24 -12
  112. package/apps/www/contacts/[uid].tsx +6 -1
  113. package/apps/www/contacts/index.tsx +66 -12
  114. package/apps/www/index.tsx +91 -28
  115. package/apps/www/messages/[uid].tsx +8 -3
  116. package/apps/www/settings/auto-reply/index.tsx +13 -0
  117. package/apps/www/settings/blocked-senders/index.tsx +4 -0
  118. package/apps/www/settings/encryption/index.tsx +66 -4
  119. package/apps/www/settings/privacy/index.tsx +4 -1
  120. package/apps/www/settings/profile/index.tsx +12 -1
  121. package/apps/www/settings/sharing/index.tsx +11 -1
  122. package/apps/www/settings/signatures/[uid].tsx +6 -4
  123. package/apps/www/tasks/index.tsx +97 -29
  124. package/dist/apps/admin/audit-log/index.js +3 -3
  125. package/dist/apps/admin/branding/index.js +3 -3
  126. package/dist/apps/admin/data-requests/index.js +17 -7
  127. package/dist/apps/admin/distribution-lists/[uid].js +3 -3
  128. package/dist/apps/admin/distribution-lists/index.js +3 -3
  129. package/dist/apps/admin/distribution-lists/new/index.js +3 -3
  130. package/dist/apps/admin/domains/[uid].js +6 -5
  131. package/dist/apps/admin/domains/index.js +3 -3
  132. package/dist/apps/admin/domains/new/index.js +3 -3
  133. package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
  134. package/dist/apps/admin/escrow-scopes/index.js +3 -3
  135. package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
  136. package/dist/apps/admin/index.js +4 -4
  137. package/dist/apps/admin/ingest-queue/index.js +3 -3
  138. package/dist/apps/admin/mailboxes/[uid].js +15 -7
  139. package/dist/apps/admin/quarantine/index.js +4 -4
  140. package/dist/apps/admin/retention-policy/index.js +3 -3
  141. package/dist/apps/admin/signing-certificates/index.js +3 -3
  142. package/dist/apps/admin/transport-rules/[uid].js +4 -4
  143. package/dist/apps/admin/transport-rules/index.js +3 -3
  144. package/dist/apps/admin/transport-rules/new/index.js +3 -3
  145. package/dist/apps/escrow/matters/[uid].js +8 -3
  146. package/dist/apps/escrow/matters/new/index.js +4 -0
  147. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
  148. package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
  149. package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
  150. package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
  151. package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
  152. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +2 -2
  153. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
  154. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
  155. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
  156. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
  157. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +2 -2
  158. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
  159. package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
  160. package/dist/apps/shared/components/admin/elevation.js +16 -0
  161. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
  162. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
  163. package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
  164. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
  165. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
  166. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
  167. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  168. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
  169. package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
  170. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
  171. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
  172. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
  173. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
  174. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
  175. package/dist/apps/shared/components/admin/settings/PluginsManager.js +103 -18
  176. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
  177. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
  178. package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
  179. package/dist/apps/shared/components/admin/signOut.js +3 -0
  180. package/dist/apps/shared/components/admin/usePagedList.js +2 -2
  181. package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
  182. package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
  183. package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
  184. package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
  185. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
  186. package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
  187. package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
  188. package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
  189. package/dist/apps/shared/components/calendar/EventModal.js +10 -2
  190. package/dist/apps/shared/components/calendar/FindATime.js +4 -3
  191. package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
  192. package/dist/apps/shared/components/calendar/ListView.js +92 -27
  193. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
  194. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
  195. package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
  196. package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
  197. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
  198. package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
  199. package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
  200. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
  201. package/dist/apps/shared/components/calendar/timePicker.js +5 -2
  202. package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
  203. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
  204. package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
  205. package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
  206. package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
  207. package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
  208. package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
  209. package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
  210. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
  211. package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
  212. package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
  213. package/dist/apps/shared/components/layout/AppShell.js +9 -0
  214. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
  215. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
  216. package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
  217. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
  218. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
  219. package/dist/apps/shared/components/mail/ConversationList.js +49 -6
  220. package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
  221. package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
  222. package/dist/apps/shared/components/mail/MenuButton.js +2 -211
  223. package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
  224. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
  225. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
  226. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
  227. package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
  228. package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
  229. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
  230. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
  231. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
  232. package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
  233. package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
  234. package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
  235. package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
  236. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
  237. package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
  238. package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
  239. package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
  240. package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
  241. package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
  242. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
  243. package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
  244. package/dist/apps/shared/components/tasks/dueDate.js +26 -0
  245. package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
  246. package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
  247. package/dist/apps/shared/mail/decryptedMessages.js +46 -2
  248. package/dist/apps/shared/mail/listSnapshots.js +2 -0
  249. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
  250. package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
  251. package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
  252. package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
  253. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
  254. package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
  255. package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
  256. package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
  257. package/dist/apps/shared/notifications/apiErrors.js +2 -0
  258. package/dist/apps/shared/notifications/preferences.d.ts +1 -1
  259. package/dist/apps/shared/notifications/preferences.js +1 -1
  260. package/dist/apps/shared/notifications/store.js +3 -1
  261. package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
  262. package/dist/apps/shared/plugins/pluginNav.js +4 -2
  263. package/dist/apps/www/calendar/index.js +24 -12
  264. package/dist/apps/www/contacts/[uid].js +6 -2
  265. package/dist/apps/www/contacts/index.js +62 -11
  266. package/dist/apps/www/index.js +85 -30
  267. package/dist/apps/www/messages/[uid].js +8 -3
  268. package/dist/apps/www/settings/auto-reply/index.js +13 -1
  269. package/dist/apps/www/settings/blocked-senders/index.js +4 -0
  270. package/dist/apps/www/settings/encryption/index.js +29 -5
  271. package/dist/apps/www/settings/privacy/index.js +3 -2
  272. package/dist/apps/www/settings/profile/index.js +4 -1
  273. package/dist/apps/www/settings/sharing/index.js +10 -1
  274. package/dist/apps/www/settings/signatures/[uid].js +6 -4
  275. package/dist/apps/www/tasks/index.js +94 -32
  276. package/dist/lib/admin/auditLogApi.js +2 -2
  277. package/dist/lib/admin/distributionListsApi.js +2 -2
  278. package/dist/lib/admin/domainsApi.js +2 -2
  279. package/dist/lib/admin/escrowAuditLogApi.js +2 -2
  280. package/dist/lib/admin/escrowScopesApi.js +2 -2
  281. package/dist/lib/admin/pluginsApi.d.ts +31 -2
  282. package/dist/lib/admin/pluginsApi.js +23 -1
  283. package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
  284. package/dist/lib/admin/retentionPolicyApi.js +3 -0
  285. package/dist/lib/admin/transportRulesApi.js +2 -2
  286. package/dist/lib/appearance/preferencesApi.d.ts +1 -1
  287. package/dist/lib/appearance/preferencesApi.js +3 -14
  288. package/dist/lib/auth/session.d.ts +51 -12
  289. package/dist/lib/auth/session.js +214 -38
  290. package/dist/lib/branding/brandingApi.d.ts +3 -3
  291. package/dist/lib/branding/brandingApi.js +9 -21
  292. package/dist/lib/calendar/calendarApi.js +3 -1
  293. package/dist/lib/calendar/eventSearch.js +43 -6
  294. package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
  295. package/dist/lib/calendar/freeBusyApi.js +13 -5
  296. package/dist/lib/calendar/recurrence.js +51 -3
  297. package/dist/lib/calendar/useDayKey.d.ts +7 -0
  298. package/dist/lib/calendar/useDayKey.js +43 -0
  299. package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
  300. package/dist/lib/components/avatar/ContactAvatar.js +37 -8
  301. package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
  302. package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
  303. package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
  304. package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
  305. package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
  306. package/dist/lib/components/forms/ImageEditBadge.js +62 -0
  307. package/dist/lib/components/menus/MenuButton.d.ts +78 -0
  308. package/dist/lib/components/menus/MenuButton.js +219 -0
  309. package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
  310. package/dist/lib/components/navigation/BottomTabBar.js +22 -3
  311. package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
  312. package/dist/lib/components/overlays/PopoverPortal.js +3 -2
  313. package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
  314. package/dist/lib/contacts/contactsApi.d.ts +5 -2
  315. package/dist/lib/contacts/contactsApi.js +16 -19
  316. package/dist/lib/contacts/gravatar.d.ts +14 -3
  317. package/dist/lib/contacts/gravatar.js +42 -5
  318. package/dist/lib/contacts/preparePhoto.d.ts +24 -0
  319. package/dist/lib/contacts/preparePhoto.js +250 -0
  320. package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
  321. package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
  322. package/dist/lib/contacts/vcard.d.ts +3 -0
  323. package/dist/lib/contacts/vcard.js +59 -7
  324. package/dist/lib/crypto/escrowKeys.d.ts +8 -0
  325. package/dist/lib/crypto/escrowKeys.js +20 -1
  326. package/dist/lib/crypto/messageSecurity.d.ts +7 -4
  327. package/dist/lib/crypto/messageSecurity.js +23 -5
  328. package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
  329. package/dist/lib/crypto/passwordUnlock.js +13 -2
  330. package/dist/lib/crypto/smime.js +12 -2
  331. package/dist/lib/mail/compose/composeQuoting.js +3 -2
  332. package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
  333. package/dist/lib/mail/compose/quotedImages.js +35 -9
  334. package/dist/lib/mail/labelsApi.js +2 -2
  335. package/dist/lib/mail/mailApi.d.ts +8 -5
  336. package/dist/lib/mail/mailApi.js +73 -39
  337. package/dist/lib/mail/mailDetailHooks.js +11 -3
  338. package/dist/lib/mail/mailFilterRulesApi.js +2 -2
  339. package/dist/lib/mail/mailSignaturesApi.js +2 -2
  340. package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
  341. package/dist/lib/mail/mailboxImportApi.js +3 -15
  342. package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
  343. package/dist/lib/mail/messageBodySanitizer.js +44 -1
  344. package/dist/lib/search/queryGrammar.js +12 -4
  345. package/dist/lib/search/searchTier3.d.ts +6 -1
  346. package/dist/lib/search/searchTier3.js +54 -25
  347. package/dist/lib/tasks/tasksApi.js +3 -3
  348. package/dist/lib/util/api.d.ts +26 -4
  349. package/dist/lib/util/api.js +73 -16
  350. package/dist/lib/util/apiQuery.d.ts +11 -0
  351. package/dist/lib/util/apiQuery.js +11 -0
  352. package/dist/lib/util/settleWithLimit.d.ts +5 -0
  353. package/dist/lib/util/settleWithLimit.js +25 -0
  354. package/lib/admin/auditLogApi.ts +2 -2
  355. package/lib/admin/distributionListsApi.ts +2 -2
  356. package/lib/admin/domainsApi.ts +2 -2
  357. package/lib/admin/escrowAuditLogApi.ts +2 -2
  358. package/lib/admin/escrowScopesApi.ts +2 -2
  359. package/lib/admin/pluginsApi.ts +45 -3
  360. package/lib/admin/retentionPolicyApi.ts +4 -0
  361. package/lib/admin/transportRulesApi.ts +2 -2
  362. package/lib/appearance/preferencesApi.ts +3 -14
  363. package/lib/auth/session.ts +239 -39
  364. package/lib/branding/brandingApi.ts +9 -21
  365. package/lib/calendar/calendarApi.ts +3 -1
  366. package/lib/calendar/eventSearch.ts +46 -6
  367. package/lib/calendar/freeBusyApi.ts +14 -6
  368. package/lib/calendar/recurrence.ts +59 -8
  369. package/lib/calendar/useDayKey.ts +45 -0
  370. package/lib/components/avatar/ContactAvatar.tsx +46 -8
  371. package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
  372. package/lib/components/forms/CameraCaptureModal.tsx +133 -0
  373. package/lib/components/forms/ImageEditBadge.tsx +121 -0
  374. package/lib/components/menus/MenuButton.tsx +408 -0
  375. package/lib/components/navigation/BottomTabBar.tsx +30 -8
  376. package/lib/components/overlays/PopoverPortal.tsx +3 -2
  377. package/lib/components/pickers/TimeZonePicker.tsx +17 -1
  378. package/lib/contacts/contactsApi.ts +18 -19
  379. package/lib/contacts/gravatar.ts +47 -5
  380. package/lib/contacts/preparePhoto.ts +273 -0
  381. package/lib/contacts/useContactPhotoSrc.ts +177 -0
  382. package/lib/contacts/vcard.ts +60 -7
  383. package/lib/crypto/escrowKeys.ts +21 -1
  384. package/lib/crypto/messageSecurity.ts +33 -9
  385. package/lib/crypto/passwordUnlock.ts +14 -2
  386. package/lib/crypto/smime.ts +13 -2
  387. package/lib/mail/compose/composeQuoting.ts +3 -2
  388. package/lib/mail/compose/quotedImages.ts +39 -9
  389. package/lib/mail/labelsApi.ts +2 -2
  390. package/lib/mail/mailApi.ts +96 -39
  391. package/lib/mail/mailDetailHooks.ts +11 -3
  392. package/lib/mail/mailFilterRulesApi.ts +2 -2
  393. package/lib/mail/mailSignaturesApi.ts +2 -2
  394. package/lib/mail/mailboxImportApi.ts +9 -15
  395. package/lib/mail/messageBodySanitizer.ts +47 -1
  396. package/lib/search/queryGrammar.ts +13 -4
  397. package/lib/search/searchTier3.ts +65 -32
  398. package/lib/tasks/tasksApi.ts +3 -3
  399. package/lib/util/api.ts +99 -16
  400. package/lib/util/apiQuery.ts +13 -0
  401. package/lib/util/settleWithLimit.ts +25 -0
  402. package/package.json +1 -1
@@ -9,12 +9,13 @@ import { useEffect, useState } from "react";
9
9
  import { ApiRequestError } from "../../../lib/util/api.js";
10
10
  import { deleteContact, getContact } from "../../../lib/contacts/contactsApi.js";
11
11
  import { useApiClient } from "../../../lib/util/apiClientContext.js";
12
- import ContactsShell from "../../shared/components/contacts/layout/ContactsShell.js";
12
+ import ContactsShell, { useContactsShell } from "../../shared/components/contacts/layout/ContactsShell.js";
13
13
  import ContactDetailPane from "../../shared/components/contacts/ContactDetailPane.js";
14
14
  import ContactForm from "../../shared/components/contacts/ContactForm.js";
15
15
  import Alert from "../../../lib/components/feedback/Alert.js";
16
16
  import { clearPinnedSignerCache } from "../../shared/components/mail/pinnedSigners.js";
17
17
  import { notifyApiError } from "../../shared/notifications/apiErrors.js";
18
+ import { useMailboxUpdateAccess } from "../../shared/mail/useMailboxUpdateAccess.js";
18
19
  /**
19
20
  * Only reached on mobile (below the `md` breakpoint) — desktop's `apps/www/contacts/index.tsx` keeps its
20
21
  * existing inline detail pane and never navigates here; see that file's `handleSelectRow`. There is no
@@ -31,6 +32,9 @@ function ContactDetailContent({ uid }) {
31
32
  const [loading, setLoading] = useState(true);
32
33
  const [error, setError] = useState(null);
33
34
  const [mode, setMode] = useState("view");
35
+ const { mailboxes } = useContactsShell();
36
+ // A contact in a mailbox shared with the reader view-only can be read, not changed.
37
+ const writable = useMailboxUpdateAccess(mailboxes.find((mailbox) => mailbox.uid === contact?.mailboxUid));
34
38
  useEffect(() => {
35
39
  setLoading(true);
36
40
  setError(null);
@@ -72,7 +76,7 @@ function ContactDetailContent({ uid }) {
72
76
  setMode("view");
73
77
  }, onCancel: () => setMode("view") })] }));
74
78
  }
75
- return (_jsx("div", { className: "p-6", children: _jsx(ContactDetailPane, { contact: contact, onEdit: () => setMode("edit"), onDelete: () => handleDelete(contact), backHref: backHref, onKeysChanged: handleKeysChanged, onChanged: setContact }) }));
79
+ return (_jsx("div", { className: "p-6", children: _jsx(ContactDetailPane, { contact: contact, onEdit: () => setMode("edit"), onDelete: () => handleDelete(contact), canWrite: writable, backHref: backHref, onKeysChanged: handleKeysChanged, onChanged: setContact }) }));
76
80
  }
77
81
  export default ContactDetailPage;
78
82
  /** The tab's title: `Brand: Contacts` (see `pageTitle()`). */
@@ -7,8 +7,9 @@ import { pageTitle } from "../../shared/navigation/pageTitle.js";
7
7
  import { useNavigate } from "../../shared/navigation/index.js";
8
8
  import { useEffect, useMemo, useRef, useState } from "react";
9
9
  import { ApiRequestError } from "../../../lib/util/api.js";
10
- import { createContact, deleteContact, listContacts, listDeletedContacts, setContactFavorite, contactPhotoUrl, updateContact, } from "../../../lib/contacts/contactsApi.js";
11
- import { contactsToVCardFile, contactToVCard, parseVCards } from "../../../lib/contacts/vcard.js";
10
+ import { createContact, deleteContact, listContacts, listContactsInFolders, listDeletedContacts, setContactFavorite, updateContact, } from "../../../lib/contacts/contactsApi.js";
11
+ import { VCARD_MAX_CHARS, VCARD_MAX_CONTACTS, contactsToVCardFile, contactToVCard, parseVCards } from "../../../lib/contacts/vcard.js";
12
+ import { withoutDuplicates } from "../../shared/components/contacts/participantDetails.js";
12
13
  import useIsMobile from "../../../lib/util/useIsMobile.js";
13
14
  import { useApiClient } from "../../../lib/util/apiClientContext.js";
14
15
  import { useCompose } from "../../shared/components/mail/compose/ComposeContext.js";
@@ -17,11 +18,12 @@ import ContactsSidebar from "../../shared/components/contacts/ContactsSidebar.js
17
18
  import ContactsToolbar from "../../shared/components/contacts/ContactsToolbar.js";
18
19
  import FloatingActionButton from "../../shared/components/layout/FloatingActionButton.js";
19
20
  import { HiOutlineUserPlus } from "react-icons/hi2";
20
- import ContactAvatar from "../../../lib/components/avatar/ContactAvatar.js";
21
+ import ContactPhotoAvatar from "../../../lib/components/avatar/ContactPhotoAvatar.js";
21
22
  import ContactDetailPane from "../../shared/components/contacts/ContactDetailPane.js";
22
23
  import ContactForm from "../../shared/components/contacts/ContactForm.js";
23
24
  import { useWritableMailboxes } from "../../shared/components/mail/writableMailboxes.js";
24
25
  import { getMyMailboxAccess } from "../../../lib/mail/mailboxAccessApi.js";
26
+ import { useMailboxUpdateAccess } from "../../shared/mail/useMailboxUpdateAccess.js";
25
27
  import Alert from "../../../lib/components/feedback/Alert.js";
26
28
  import Button from "../../../lib/components/buttons/Button.js";
27
29
  import Modal from "../../../lib/components/overlays/Modal.js";
@@ -30,6 +32,7 @@ import { clearPinnedSignerCache } from "../../shared/components/mail/pinnedSigne
30
32
  import { SHORTCUTS } from "../../shared/keyboard/keymap.js";
31
33
  import { useShortcut } from "../../shared/keyboard/useShortcut.js";
32
34
  import { notifyApiError } from "../../shared/notifications/apiErrors.js";
35
+ import { notify } from "../../shared/notifications/store.js";
33
36
  const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
34
37
  function ContactsPage(props) {
35
38
  return (_jsx(ContactsShell, { ...props, children: _jsx(ContactsContent, { userUid: props.userUid }) }));
@@ -57,6 +60,8 @@ function ContactsContent({ userUid }) {
57
60
  const { folderUid, mailboxUid, mailboxes } = useContactsShell();
58
61
  // The new-contact form's Mailbox choices - view-only shares are left out (see writableMailboxes.ts).
59
62
  const writableMailboxes = useWritableMailboxes(mailboxes, userUid, mailboxUid);
63
+ // A mailbox shared with the reader view-only: its contacts can be read, not changed.
64
+ const mailboxWritable = useMailboxUpdateAccess(mailboxes.find((mb) => mb.uid === mailboxUid));
60
65
  const { openCompose } = useCompose();
61
66
  const isMobile = useIsMobile();
62
67
  const navigate = useNavigate();
@@ -263,6 +268,22 @@ function ContactsContent({ userUid }) {
263
268
  }
264
269
  }
265
270
  // The bulk handlers below try every contact and report each failure: the same failure repeated is one pop-up with a count.
271
+ // 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.
272
+ const bulkRunning = useRef(false);
273
+ function singleFlight(run) {
274
+ return async () => {
275
+ if (bulkRunning.current) {
276
+ return;
277
+ }
278
+ bulkRunning.current = true;
279
+ try {
280
+ await run();
281
+ }
282
+ finally {
283
+ bulkRunning.current = false;
284
+ }
285
+ };
286
+ }
266
287
  async function handleBulkDelete() {
267
288
  setConfirmingBulkDelete(false);
268
289
  for (const contact of checkedContacts) {
@@ -291,7 +312,7 @@ function ContactsContent({ userUid }) {
291
312
  // contacts happen to be open - the compose window's From field can still change it.
292
313
  openCompose({ to: addresses.join(", ") });
293
314
  }
294
- async function handleToggleFavorite() {
315
+ const handleToggleFavorite = singleFlight(async () => {
295
316
  const allFavorited = checkedContacts.every((c) => c.favorite);
296
317
  for (const contact of checkedContacts) {
297
318
  try {
@@ -303,8 +324,8 @@ function ContactsContent({ userUid }) {
303
324
  }
304
325
  clearPinnedSignerCache();
305
326
  await reload();
306
- }
307
- async function handleAddCategory() {
327
+ });
328
+ const handleAddCategory = singleFlight(async () => {
308
329
  const category = window.prompt("Category name");
309
330
  if (!category?.trim()) {
310
331
  return;
@@ -322,7 +343,7 @@ function ContactsContent({ userUid }) {
322
343
  }
323
344
  clearPinnedSignerCache();
324
345
  await reload();
325
- }
346
+ });
326
347
  function handleExportVCard() {
327
348
  downloadTextFile(checkedContacts.length === 1 ? `${checkedContacts[0].displayName}.vcf` : "contacts.vcf", checkedContacts.length === 1 ? contactToVCard(checkedContacts[0]) : contactsToVCardFile(checkedContacts));
328
349
  }
@@ -330,9 +351,37 @@ function ContactsContent({ userUid }) {
330
351
  if (!mailboxUid || !folderUid) {
331
352
  return;
332
353
  }
333
- const text = await file.text();
334
- const parsed = parseVCards(text);
335
- for (const input of parsed) {
354
+ // 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
355
+ // lock the page for minutes and flood the server. Whatever goes wrong while reading it is said, not left as an unhandled rejection.
356
+ if (file.size > VCARD_MAX_CHARS) {
357
+ notify({ kind: "error", title: "Couldn't import contacts", message: `That file is larger than ${Math.round(VCARD_MAX_CHARS / 1000000)} MB. Split it into smaller files.` });
358
+ return;
359
+ }
360
+ let parsed;
361
+ try {
362
+ parsed = parseVCards(await file.text());
363
+ }
364
+ catch (err) {
365
+ notifyApiError(err, "Couldn't read that contacts file");
366
+ return;
367
+ }
368
+ if (parsed.length >= VCARD_MAX_CONTACTS) {
369
+ 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.` });
370
+ }
371
+ // 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
372
+ // 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.
373
+ let fresh;
374
+ try {
375
+ fresh = withoutDuplicates(parsed, await listContactsInFolders([folderUid], client)).fresh;
376
+ }
377
+ catch (err) {
378
+ notifyApiError(err, "Couldn't check the file against your contacts");
379
+ return;
380
+ }
381
+ if (fresh.length < parsed.length) {
382
+ notify({ kind: "info", title: "Some contacts were skipped", message: `${parsed.length - fresh.length} of the contacts in that file are already in your contacts.` });
383
+ }
384
+ for (const input of fresh) {
336
385
  try {
337
386
  await createContact({ mailboxUid, folderUid, ...input }, client);
338
387
  }
@@ -346,7 +395,9 @@ function ContactsContent({ userUid }) {
346
395
  return (_jsxs("div", { className: "flex-1 min-w-0 min-h-0 flex flex-col lg:flex-row md:flex-none md:h-[calc(100dvh_-_var(--rr-header-h,4rem))] md:overflow-hidden", children: [_jsx(ContactsSidebar, { mailboxUid: mailboxUid, contacts: contacts, active: view, onSelect: handleSelectView, showDeleted: canViewDeleted }), _jsxs("div", { className: "flex-1 min-w-0 min-h-0 flex", children: [_jsxs("div", { className: [
347
396
  "w-full md:w-[clamp(16rem,32vw,26rem)] min-w-0 md:shrink-0 md:border-r border-border md:flex flex-col min-h-0",
348
397
  mode === "new" ? "hidden md:flex" : "flex",
349
- ].join(" "), children: [_jsx(ContactsToolbar, { selectedCount: checkedContacts.length, allSelectedFavorited: checkedContacts.length > 0 && checkedContacts.every((c) => c.favorite), onNewContact: handleNew, onEdit: handleToolbarEdit, onDelete: () => setConfirmingBulkDelete(true), onEmail: handleEmail, onToggleFavorite: handleToggleFavorite, onAddCategory: handleAddCategory, onExportVCard: handleExportVCard, onImportFile: handleImportFile, shortcuts: true, hideNew: isMobile }), _jsx("div", { className: "p-3 border-b border-border", children: _jsx("input", { ref: searchInputRef, type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search contacts", "aria-label": "Search contacts", className: INPUT_CLASS }) }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), (isDeletedView ? deletedTruncated : truncated) && (_jsx("div", { className: "p-3", children: _jsxs(Alert, { children: ["This folder has more contacts than can be shown at once - only the first ", LIST_PAGE_SIZE * MAX_LIST_PAGES, " are listed."] }) })), deletedError && isDeletedView && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: deletedError }) })), (isDeletedView ? deletedLoading : loading) ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : sorted.length === 0 ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "No contacts found." })) : (_jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: _jsxs("table", { className: "w-full table-fixed text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [!isDeletedView && (_jsx("th", { className: "w-10 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all contacts", checked: allChecked, onChange: toggleCheckAll }) })), _jsx("th", { className: "w-[58%] px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("name"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Name", sortColumn === "name" ? (sortDesc ? " ▾" : " ▴") : ""] }) }), _jsx("th", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("info"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Contact info", sortColumn === "info" ? (sortDesc ? " ▾" : " ▴") : ""] }) })] }) }), _jsx("tbody", { children: sorted.map((contact) => (_jsxs("tr", { className: ["border-b border-border", contact.uid === selectedUid ? "bg-primary/10" : "hover:bg-surface-alt"].join(" "), children: [!isDeletedView && (_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${contact.displayName}`, checked: checkedUids.has(contact.uid), onChange: () => toggleChecked(contact.uid) }) })), _jsx("td", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSelectRow(contact), className: "flex w-full min-w-0 items-center gap-2 text-left", children: [_jsx(ContactAvatar, { displayName: contact.displayName, size: 28, photoUrl: contactPhotoUrl(contact), email: contact.emails[0]?.address }), _jsxs("span", { className: "min-w-0 truncate font-medium", children: [contact.displayName, contact.favorite && _jsx("span", { "aria-label": "Favorite", children: " \u2605" })] })] }) }), _jsx("td", { className: "max-w-0 truncate px-3 py-2 text-text-muted", children: primaryInfo(contact) })] }, contact.uid))) })] }) }))] }), _jsxs("div", { className: ["flex-1 min-w-0 min-h-0 flex-col md:flex", mode === "new" ? "flex" : "hidden"].join(" "), children: [savedElsewhere && mode !== "new" && (_jsxs("p", { role: "status", className: "m-6 mb-0 text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: [savedElsewhere.displayName, " was added to", " ", mailboxes.find((mb) => mb.uid === savedElsewhere.mailboxUid)?.displayName ?? "another mailbox", ".", " ", _jsx("a", { href: `/contacts?mailboxUid=${encodeURIComponent(savedElsewhere.mailboxUid)}`, "data-router-shallow": "", "data-router-prefetch": "false", className: "font-medium text-primary-dark hover:underline", children: "View that mailbox\u2019s contacts" })] })), mode === "new" && mailboxUid && folderUid ? (_jsx(ContactForm, { mailboxUid: mailboxUid, folderUid: folderUid, mailboxes: writableMailboxes, onSaved: handleSaved, onCancel: handleCancel })) : mode === "edit" && selected ? (_jsx(ContactForm, { contact: selected, onSaved: handleSaved, onCancel: handleCancel })) : selected ? (_jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-6", children: _jsx(ContactDetailPane, { contact: selected, onEdit: () => setMode("edit"), onDelete: () => handleDelete(selected), onKeysChanged: () => void reload(), onChanged: () => void reload(), canResolveKeys: ownsMailbox || delegateCanUpdate }) })) : (_jsx("div", { className: "flex flex-1 items-center justify-center p-6 text-center", children: _jsx("p", { className: "text-sm text-text-muted", children: "Select a contact, or create a new one." }) }))] })] }), _jsxs(Modal, { open: confirmingBulkDelete, onClose: () => setConfirmingBulkDelete(false), title: "Delete contacts", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Delete ", checkedContacts.length, " selected ", checkedContacts.length === 1 ? "contact" : "contacts", "? They move to Deleted contacts."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingBulkDelete(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: handleBulkDelete, children: "Delete" })] })] }), isMobile && mode !== "new" && _jsx(FloatingActionButton, { label: "New contact", icon: HiOutlineUserPlus, onClick: handleNew })] }));
398
+ ].join(" "), children: [_jsx(ContactsToolbar, { selectedCount: checkedContacts.length, allSelectedFavorited: checkedContacts.length > 0 && checkedContacts.every((c) => c.favorite), onNewContact: handleNew, onEdit: handleToolbarEdit, onDelete: () => setConfirmingBulkDelete(true), onEmail: handleEmail, onToggleFavorite: handleToggleFavorite, onAddCategory: handleAddCategory, onExportVCard: handleExportVCard, onImportFile: handleImportFile, shortcuts: true, hideNew: isMobile }), _jsx("div", { className: "p-3 border-b border-border", children: _jsx("input", { ref: searchInputRef, type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search contacts", "aria-label": "Search contacts", className: INPUT_CLASS }) }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), (isDeletedView ? deletedTruncated : truncated) && (_jsx("div", { className: "p-3", children: _jsxs(Alert, { children: ["This folder has more contacts than can be shown at once - only the first ", LIST_PAGE_SIZE * MAX_LIST_PAGES, " are listed."] }) })), deletedError && isDeletedView && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: deletedError }) })), (isDeletedView ? deletedLoading : loading) ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : sorted.length === 0 ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "No contacts found." })) : (_jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: _jsxs("table", { className: "w-full table-fixed text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [!isDeletedView && (_jsx("th", { className: "w-10 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all contacts", checked: allChecked, onChange: toggleCheckAll }) })), _jsx("th", { className: "w-[58%] px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("name"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Name", sortColumn === "name" ? (sortDesc ? " ▾" : " ▴") : ""] }) }), _jsx("th", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("info"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Contact info", sortColumn === "info" ? (sortDesc ? " ▾" : " ▴") : ""] }) })] }) }), _jsx("tbody", { children: sorted.map((contact) => (_jsxs("tr", { className: ["border-b border-border", contact.uid === selectedUid ? "bg-primary/10" : "hover:bg-surface-alt"].join(" "), children: [!isDeletedView && (_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${contact.displayName}`, checked: checkedUids.has(contact.uid), onChange: () => toggleChecked(contact.uid) }) })), _jsx("td", { className: "px-3 py-2", children: _jsxs("button", { type: "button",
399
+ // A deleted contact has no pane to show (it can't be restored or opened), and its route would answer 404.
400
+ disabled: isDeletedView, onClick: () => handleSelectRow(contact), className: "flex w-full min-w-0 items-center gap-2 text-left disabled:cursor-default", children: [_jsx(ContactPhotoAvatar, { lazy: true, displayName: contact.displayName, size: 28, contact: contact, email: contact.emails[0]?.address }), _jsxs("span", { className: "min-w-0 truncate font-medium", children: [contact.displayName, contact.favorite && _jsx("span", { "aria-label": "Favorite", children: " \u2605" })] })] }) }), _jsx("td", { className: "max-w-0 truncate px-3 py-2 text-text-muted", children: primaryInfo(contact) })] }, contact.uid))) })] }) }))] }), _jsxs("div", { className: ["flex-1 min-w-0 min-h-0 flex-col md:flex", mode === "new" ? "flex" : "hidden"].join(" "), children: [savedElsewhere && mode !== "new" && (_jsxs("p", { role: "status", className: "m-6 mb-0 text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: [savedElsewhere.displayName, " was added to", " ", mailboxes.find((mb) => mb.uid === savedElsewhere.mailboxUid)?.displayName ?? "another mailbox", ".", " ", _jsx("a", { href: `/contacts?mailboxUid=${encodeURIComponent(savedElsewhere.mailboxUid)}`, "data-router-shallow": "", "data-router-prefetch": "false", className: "font-medium text-primary-dark hover:underline", children: "View that mailbox\u2019s contacts" })] })), mode === "new" && mailboxUid && folderUid ? (_jsx(ContactForm, { mailboxUid: mailboxUid, folderUid: folderUid, mailboxes: writableMailboxes, onSaved: handleSaved, onCancel: handleCancel })) : mode === "edit" && selected ? (_jsx(ContactForm, { contact: selected, onSaved: handleSaved, onCancel: handleCancel })) : selected ? (_jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-6", children: _jsx(ContactDetailPane, { contact: selected, onEdit: () => setMode("edit"), onDelete: () => handleDelete(selected), canWrite: mailboxWritable, onKeysChanged: () => void reload(), onChanged: () => void reload(), canResolveKeys: ownsMailbox || delegateCanUpdate }) })) : (_jsx("div", { className: "flex flex-1 items-center justify-center p-6 text-center", children: _jsx("p", { className: "text-sm text-text-muted", children: "Select a contact, or create a new one." }) }))] })] }), _jsxs(Modal, { open: confirmingBulkDelete, onClose: () => setConfirmingBulkDelete(false), title: "Delete contacts", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Delete ", checkedContacts.length, " selected ", checkedContacts.length === 1 ? "contact" : "contacts", "? They move to Deleted contacts."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingBulkDelete(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: handleBulkDelete, children: "Delete" })] })] }), isMobile && mode !== "new" && _jsx(FloatingActionButton, { label: "New contact", icon: HiOutlineUserPlus, onClick: handleNew })] }));
350
401
  }
351
402
  export default ContactsPage;
352
403
  /** The tab's title: `Brand: Contacts` (see `pageTitle()`). */
@@ -21,6 +21,7 @@ import { searchLocalIndex } from "../shared/search/searchTier2.js";
21
21
  import { SEARCH_LIMITS, classifyFailure, describeFailures, hitKey, mergeSearchResults, runLimited, searchPageSize, tagHits, withTimeout, } from "../shared/search/crossMailboxSearch.js";
22
22
  import { getUnlockedKeys, subscribeKeySession } from "../../lib/crypto/keySession.js";
23
23
  import { useMessageAttachments } from "../../lib/mail/mailDetailHooks.js";
24
+ import { settleWithLimit } from "../../lib/util/settleWithLimit.js";
24
25
  import useIsMobile from "../../lib/util/useIsMobile.js";
25
26
  import MailShell, { useMailShell, } from "../shared/components/mail/layout/MailShell.js";
26
27
  import MailAddress from "../shared/components/mail/MailAddress.js";
@@ -34,7 +35,7 @@ import { setReadStateMany } from "../shared/mail/messageReadState.js";
34
35
  import { useMarkMessageRead } from "../shared/mail/useMarkMessageRead.js";
35
36
  import { ROW_FOCUS_CLASS, UnreadBar, UnreadLabel, dateClass, isUnread, rowClass, senderClass, subjectClass } from "../shared/components/mail/unreadStyle.js";
36
37
  import { LazyConversationThreadPane, LazyMessageDetailPane, prefetchReadingPane } from "../shared/components/mail/LazyReadingPane.js";
37
- import { rememberDecrypted } from "../shared/mail/decryptedMessages.js";
38
+ import { decryptFailedBefore, mapWithConcurrency, markDecryptFailed, rememberDecrypted } from "../shared/mail/decryptedMessages.js";
38
39
  import ConversationList, { conversationRowKey } from "../shared/components/mail/ConversationList.js";
39
40
  import SwipeRow from "../shared/components/mail/SwipeRow.js";
40
41
  import InviteRowChip from "../shared/components/mail/invite/InviteRowChip.js";
@@ -57,6 +58,8 @@ import { notifyApiError } from "../shared/notifications/apiErrors.js";
57
58
  const MESSAGE_PAGE_SIZE = 50;
58
59
  const SEARCH_DEBOUNCE_MS = 300;
59
60
  const LIST_PREVIEW_MAX_LENGTH = 160;
61
+ /** How many encrypted rows are fetched and decrypted at once. */
62
+ const DECRYPT_CONCURRENCY = 4;
60
63
  /** The literal outer-envelope `Subject` every encrypted message carries server-side - RFC 9788's
61
64
  * `hcp_baseline` policy obscures it to this exact string (see `smimeMessage.ts`'s
62
65
  * `applyBaselineOuterHeaders()`), which is also all `Message.subject` ever shows for one of these until
@@ -90,10 +93,12 @@ function stripHtmlToText(html) {
90
93
  * and `handleUnlockList()`), and a single row's fetch/decrypt failure never blocks the rest.
91
94
  */
92
95
  async function decryptEncryptedRows(messages, unlocked) {
93
- const encrypted = messages.filter((m) => m.subject === ENCRYPTED_SUBJECT_PLACEHOLDER);
96
+ // A row that already came to nothing since the last unlock is not downloaded again by every refresh of the list.
97
+ const encrypted = messages.filter((m) => m.subject === ENCRYPTED_SUBJECT_PLACEHOLDER && !decryptFailedBefore(m.uid));
94
98
  // Imported once for all the rows, by whichever needs it first.
95
99
  let securityModule;
96
- const entries = await Promise.all(encrypted.map(async (message) => {
100
+ // A few at a time: each is a download and a public-key decryption.
101
+ const entries = await mapWithConcurrency(encrypted, DECRYPT_CONCURRENCY, async (message) => {
97
102
  try {
98
103
  const rawMime = await getMessageRawContent(message.uid);
99
104
  // Loaded here, on first use: the S/MIME code (PKI.js, ASN.1, X.509) is over half a megabyte and an inbox with
@@ -101,6 +106,7 @@ async function decryptEncryptedRows(messages, unlocked) {
101
106
  const { evaluateMessageSecurity } = await (securityModule ?? (securityModule = import("../../lib/crypto/messageSecurity.js")));
102
107
  const security = await evaluateMessageSecurity(rawMime, unlocked);
103
108
  if (!security.subject && !security.html) {
109
+ markDecryptFailed(message.uid);
104
110
  return null;
105
111
  }
106
112
  const preview = security.html ? stripHtmlToText(security.html).slice(0, LIST_PREVIEW_MAX_LENGTH) : undefined;
@@ -108,9 +114,10 @@ async function decryptEncryptedRows(messages, unlocked) {
108
114
  return [message.uid, { subject: security.subject, preview }];
109
115
  }
110
116
  catch {
117
+ markDecryptFailed(message.uid);
111
118
  return null;
112
119
  }
113
- }));
120
+ });
114
121
  const result = {};
115
122
  for (const entry of entries) {
116
123
  if (entry) {
@@ -319,6 +326,10 @@ function appendUnseenRows(shown, more, idOf, cap) {
319
326
  }
320
327
  const messageUid = (message) => message.uid;
321
328
  const conversationKey = conversationRowKey;
329
+ /** How many conversations' messages are requested at once when many are ticked together (Select all on a long list). */
330
+ const RESOLVE_CONVERSATIONS_AT_ONCE = 5;
331
+ /** 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. */
332
+ const conversationSignature = (row) => [row.messageUids.join(","), row.unreadCount, row.flagged, row.folderUids.join(","), row.latestFolderUid].join("|");
322
333
  /** Flattens and sorts a per-mailbox fetch into one merged, newest-first list - the aggregate ("All
323
334
  * Inboxes" etc.) equivalent of `mergeSearchResults()` above, but simpler: an aggregated message has no
324
335
  * natural relevance score to normalize, so this only ever sorts by `receivedDate`. */
@@ -330,7 +341,9 @@ function mergeInboxMessages(perMailbox) {
330
341
  /**
331
342
  * Fans out one `listMessages()` call per accessible mailbox that has a folder of `type`, merges the
332
343
  * results newest-first. A mailbox with no matching folder, or whose fetch fails, simply contributes
333
- * nothing - one mailbox's absence/failure must not blank out every other mailbox's messages.
344
+ * nothing - one mailbox's absence/failure must not blank out every other mailbox's messages. `listed`, the
345
+ * rows on screen when this is a refresh of them, makes a mailbox whose fetch fails keep the rows it has there
346
+ * instead of contributing none (a timeout must not make its mail vanish until the next success).
334
347
  *
335
348
  * **Pagination scope trim (deliberate, matching this file's own documented Tier 2/3 tradeoffs)**: there is
336
349
  * no composite cursor across an arbitrary number of independently-paginated mailboxes in this pass - this
@@ -339,7 +352,7 @@ function mergeInboxMessages(perMailbox) {
339
352
  * composite-cursor "load more" per mailbox is a natural v2 if usage shows people scrolling past the first
340
353
  * page in aggregate view often.
341
354
  */
342
- async function fetchAggregateMessages(mailboxFolders, type, filter, client) {
355
+ async function fetchAggregateMessages(mailboxFolders, type, filter, client, listed) {
343
356
  const perMailbox = await Promise.all(mailboxFolders.map(async ({ mailbox, folders }) => {
344
357
  const folder = folders.find((f) => f.type === type);
345
358
  if (!folder) {
@@ -349,7 +362,7 @@ async function fetchAggregateMessages(mailboxFolders, type, filter, client) {
349
362
  // this function's own pagination scope trim) - each mailbox contributes its own newest page and
350
363
  // they're merged newest-first, which a different sort key couldn't be made honest across an
351
364
  // arbitrary number of independently-paged folders.
352
- const messages = await listMessages(folder.uid, { limit: MESSAGE_PAGE_SIZE, filter }, client).catch(() => []);
365
+ const messages = await listMessages(folder.uid, { limit: MESSAGE_PAGE_SIZE, filter }, client).catch(() => (listed ?? []).filter((m) => m.mailboxUid === mailbox.uid));
353
366
  return { mailbox, messages };
354
367
  }));
355
368
  return mergeInboxMessages(perMailbox);
@@ -358,16 +371,17 @@ async function fetchAggregateMessages(mailboxFolders, type, filter, client) {
358
371
  * The conversation-list counterpart of `fetchAggregateMessages()`: one `listConversations()` per accessible mailbox that has a folder of
359
372
  * `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
360
373
  * grouping pulls in) and each row tagged with its mailbox, then merged newest activity first. Same first-page-only scope, and the same
361
- * rule that a mailbox that has no such folder or fails to answer contributes nothing rather than blanking the rest.
374
+ * 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
375
+ * screen, for a refresh of them), keeps the rows it has there.
362
376
  */
363
- async function fetchAggregateConversations(mailboxFolders, type, params, client) {
377
+ async function fetchAggregateConversations(mailboxFolders, type, params, client, listed) {
364
378
  const perMailbox = await Promise.all(mailboxFolders.map(async ({ mailbox, folders }) => {
365
379
  const folder = folders.find((f) => f.type === type);
366
380
  if (!folder) {
367
381
  return [];
368
382
  }
369
- const rows = await listConversations(mailbox.uid, { ...params, folderUid: folder.uid, page: 0 }, client).catch(() => []);
370
- return rows.map((row) => ({ ...row, mailboxUid: mailbox.uid }));
383
+ const rows = await listConversations(mailbox.uid, { ...params, folderUid: folder.uid, page: 0 }, client).catch(() => null);
384
+ return rows ? rows.map((row) => ({ ...row, mailboxUid: mailbox.uid })) : (listed ?? []).filter((row) => row.mailboxUid === mailbox.uid);
371
385
  }));
372
386
  return perMailbox.flat().sort((a, b) => Date.parse(b.latestDate) - Date.parse(a.latestDate));
373
387
  }
@@ -621,6 +635,8 @@ function InboxContent({ userUid }) {
621
635
  messagesRef.current = messages;
622
636
  const conversationsRef = useRef(conversations);
623
637
  conversationsRef.current = conversations;
638
+ const selectedConversationIdsRef = useRef(selectedConversationIds);
639
+ selectedConversationIdsRef.current = selectedConversationIds;
624
640
  // Reset to a fresh Map at the start of every new search pass (see the search effect below) - see
625
641
  // resolveHitsToMessages()'s own doc comment on why this needs to persist *within* one pass but not
626
642
  // across passes (a stale `null` for a uid that's since become resolvable elsewhere must not stick).
@@ -1256,8 +1272,9 @@ function InboxContent({ userUid }) {
1256
1272
  try {
1257
1273
  if (asConversations && aggregateFolderType) {
1258
1274
  // No paging here either: the fresh merged first pages are the list.
1259
- const fresh = await fetchAggregateConversations(mailboxFolders, aggregateFolderType, conversationParams(0), client);
1275
+ const fresh = await fetchAggregateConversations(mailboxFolders, aggregateFolderType, conversationParams(0), client, conversationsRef.current);
1260
1276
  if (isCurrent()) {
1277
+ refreshConversationMessages(conversationsRef.current, fresh);
1261
1278
  setConversations(fresh);
1262
1279
  }
1263
1280
  }
@@ -1267,13 +1284,14 @@ function InboxContent({ userUid }) {
1267
1284
  return;
1268
1285
  }
1269
1286
  const merged = mergeFirstPage(conversationsRef.current, fresh, conversationKey, MESSAGE_PAGE_SIZE);
1287
+ refreshConversationMessages(conversationsRef.current, merged.rows);
1270
1288
  setConversations(merged.rows);
1271
1289
  listedOffsetRef.current = merged.complete ? fresh.length : listedOffsetRef.current + merged.added;
1272
1290
  setHasMore(!merged.complete);
1273
1291
  }
1274
1292
  else if (aggregateFolderType) {
1275
1293
  // No paging here (see `fetchAggregateMessages()`): the fresh merged first pages are the list.
1276
- const fresh = await fetchAggregateMessages(mailboxFolders, aggregateFolderType, effectiveFilter, client);
1294
+ const fresh = await fetchAggregateMessages(mailboxFolders, aggregateFolderType, effectiveFilter, client, messagesRef.current);
1277
1295
  if (isCurrent()) {
1278
1296
  setMessages(fresh);
1279
1297
  }
@@ -1685,35 +1703,66 @@ function InboxContent({ userUid }) {
1685
1703
  return next;
1686
1704
  });
1687
1705
  }
1688
- /** Loads (once) the messages behind each of `ids`, so a ticked conversation resolves to the messages
1689
- * every bulk action below acts on. If any of them fails to load, none of that batch stays ticked and
1690
- * a pop-up says why - better than acting on the part of a selection that happened to arrive. */
1691
- async function resolveConversations(rows) {
1692
- const missing = rows.filter((row) => !conversationMessagesById[conversationKey(row)]);
1706
+ /** Loads (once, or again with `refetch`) the messages behind each of `rows`, so a ticked conversation resolves to the messages
1707
+ * every bulk action below acts on - a few requests at a time (Select all on a long list is hundreds of rows). A conversation
1708
+ * 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.
1709
+ * What arrives after the list was loaded again is dropped: those rows are gone. */
1710
+ async function resolveConversations(rows, refetch = false) {
1711
+ const missing = rows.filter((row) => refetch || !conversationMessagesById[conversationKey(row)]);
1693
1712
  if (missing.length === 0) {
1694
1713
  return;
1695
1714
  }
1715
+ const listRun = searchRunIdRef.current;
1696
1716
  setResolvingSelection((n) => n + 1);
1697
1717
  try {
1698
- const loaded = await Promise.all(
1718
+ const outcomes = await settleWithLimit(missing, RESOLVE_CONVERSATIONS_AT_ONCE, (row) =>
1699
1719
  // A row of the "All mailboxes" view names its own mailbox; any other list is the open mailbox's.
1700
- missing.map(async (row) => [conversationKey(row), await listConversationMessages(row.mailboxUid ?? activeMailboxUid, row.conversationId, {}, client)]));
1701
- setConversationMessagesById((prev) => ({ ...prev, ...Object.fromEntries(loaded) }));
1702
- }
1703
- catch (err) {
1704
- notifyApiError(err, "Couldn't load the messages in one of those conversations");
1705
- setSelectedConversationIds((prev) => {
1706
- const next = new Set(prev);
1707
- for (const row of missing) {
1708
- next.delete(conversationKey(row));
1720
+ searchRunIdRef.current === listRun
1721
+ ? listConversationMessages(row.mailboxUid ?? activeMailboxUid, row.conversationId, {}, client)
1722
+ : Promise.reject(new Error("The list was loaded again")));
1723
+ if (searchRunIdRef.current !== listRun) {
1724
+ return;
1725
+ }
1726
+ const loaded = {};
1727
+ const failed = new Set();
1728
+ let firstFailure;
1729
+ outcomes.forEach((outcome, index) => {
1730
+ const key = conversationKey(missing[index]);
1731
+ if (outcome.status === "fulfilled") {
1732
+ loaded[key] = outcome.value;
1733
+ }
1734
+ else {
1735
+ failed.add(key);
1736
+ firstFailure ?? (firstFailure = outcome.reason);
1709
1737
  }
1710
- return next;
1711
1738
  });
1739
+ setConversationMessagesById((prev) => ({ ...prev, ...loaded }));
1740
+ if (failed.size > 0) {
1741
+ notifyApiError(firstFailure, "Couldn't load the messages in one of those conversations");
1742
+ setSelectedConversationIds((prev) => new Set([...prev].filter((key) => !failed.has(key))));
1743
+ }
1712
1744
  }
1713
1745
  finally {
1714
1746
  setResolvingSelection((n) => n - 1);
1715
1747
  }
1716
1748
  }
1749
+ /**
1750
+ * 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
1751
+ * 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
1752
+ * 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
1753
+ * until they arrive.
1754
+ */
1755
+ function refreshConversationMessages(before, after) {
1756
+ const was = new Map(before.map((row) => [conversationKey(row), conversationSignature(row)]));
1757
+ const changed = after.filter((row) => was.has(conversationKey(row)) && was.get(conversationKey(row)) !== conversationSignature(row));
1758
+ if (changed.length === 0) {
1759
+ return;
1760
+ }
1761
+ const ticked = changed.filter((row) => selectedConversationIdsRef.current.has(conversationKey(row)));
1762
+ const stale = new Set(changed.filter((row) => !ticked.includes(row)).flatMap((row) => [conversationKey(row), row.conversationId]));
1763
+ setConversationMessagesById((prev) => Object.fromEntries(Object.entries(prev).filter(([key]) => !stale.has(key))));
1764
+ void resolveConversations(ticked, true);
1765
+ }
1717
1766
  function toggleConversationSelected(conversation) {
1718
1767
  const id = conversationKey(conversation);
1719
1768
  const ticking = !selectedConversationIds.has(id);
@@ -1955,7 +2004,7 @@ function InboxContent({ userUid }) {
1955
2004
  }
1956
2005
  /** Empty folder: permanently deletes everything in the folder being listed (Deleted Items or Junk Email) after the reader has confirmed. */
1957
2006
  async function emptyListedFolder(folder, count) {
1958
- const outcome = await permanent.requestEmptyFolder({ uid: folder.uid, name: folder.name }, count);
2007
+ const outcome = await permanent.requestEmptyFolder(folder, count);
1959
2008
  if (!outcome) {
1960
2009
  return;
1961
2010
  }
@@ -2325,6 +2374,12 @@ function InboxContent({ userUid }) {
2325
2374
  uid: emptiableFolders[0].uid,
2326
2375
  name: aggregateFolderType ? (emptiableFolders[0].type === "junk" ? "Junk Email" : "Deleted Items") : emptiableFolders[0].name,
2327
2376
  uids: aggregateFolderType ? emptiableFolders.map((f) => f.uid) : undefined,
2377
+ // The mailboxes whose folders these are, for the confirmation to name (it empties each of them, a share included).
2378
+ scope: aggregateFolderType
2379
+ ? mailboxFolders
2380
+ .filter((entry) => entry.folders.some((f) => f.type === aggregateFolderType))
2381
+ .map(({ mailbox }) => `${mailbox.displayName}${mailbox.ownerUserUid ? "" : " (shared)"}`)
2382
+ : undefined,
2328
2383
  };
2329
2384
  // 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.
2330
2385
  const emptiableTotal = emptiableFolders.reduce((sum, f) => sum + folderCountOf(f).total, 0);
@@ -34,12 +34,17 @@ function MessageDetailContent({ uid }) {
34
34
  const [labels, setLabels] = useState([]);
35
35
  const conversationId = useSearchParams()[0].get("conversation");
36
36
  useEffect(() => {
37
+ // An answer for a message since replaced (the uid changed without the page remounting) must not be shown as the current one.
38
+ let cancelled = false;
37
39
  setLoading(true);
38
40
  setError(null);
39
41
  getMessage(uid, client)
40
- .then(setMessage)
41
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this message."))
42
- .finally(() => setLoading(false));
42
+ .then((loaded) => !cancelled && setMessage(loaded))
43
+ .catch((err) => !cancelled && setError(err instanceof ApiRequestError ? err.message : "Could not load this message."))
44
+ .finally(() => !cancelled && setLoading(false));
45
+ return () => {
46
+ cancelled = true;
47
+ };
43
48
  }, [uid, client]);
44
49
  // See `apps/www/index.tsx`'s identical effect's own doc comment - a failure here just hides the
45
50
  // Labels control rather than blocking the rest of the page. Keyed on the message's own mailbox, not the
@@ -10,6 +10,7 @@ import { updateMailbox } from "../../../../lib/mail/mailApi.js";
10
10
  import { useApiClient } from "../../../../lib/util/apiClientContext.js";
11
11
  import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
12
12
  import Button from "../../../../lib/components/buttons/Button.js";
13
+ import Alert from "../../../../lib/components/feedback/Alert.js";
13
14
  import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
14
15
  const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
15
16
  function SettingsAutoReplyPage(props) {
@@ -31,9 +32,20 @@ function AutoReplyContent() {
31
32
  const [version, setVersion] = useState(mailbox.version);
32
33
  const [saving, setSaving] = useState(false);
33
34
  const [saved, setSaved] = useState(false);
35
+ const [error, setError] = useState(null);
34
36
  async function handleSubmit(e) {
35
37
  e.preventDefault();
36
38
  setSaved(false);
39
+ // 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.
40
+ if (oofEnabled && !oofMessage.trim()) {
41
+ setError("Write the message to send while automatic replies are on.");
42
+ return;
43
+ }
44
+ if (oofEnabled && oofStartTime && oofEndTime && new Date(oofEndTime) <= new Date(oofStartTime)) {
45
+ setError("The end must be after the start.");
46
+ return;
47
+ }
48
+ setError(null);
37
49
  setSaving(true);
38
50
  try {
39
51
  const updated = await updateMailbox({
@@ -55,7 +67,7 @@ function AutoReplyContent() {
55
67
  setSaving(false);
56
68
  }
57
69
  }
58
- return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Automatic Replies" }), _jsxs("p", { className: "text-sm text-text-muted mb-4", children: ["Send an automatic reply to anyone who emails ", mailbox.displayName, " while you\u2019re away. A calendar event with its own automatic reply enabled takes over for its own start/end window."] }), saved && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm font-medium", children: [_jsx("input", { type: "checkbox", checked: oofEnabled, onChange: (e) => setOofEnabled(e.target.checked) }), "Automatic replies are on"] }), oofEnabled && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Message" }), _jsx("textarea", { "aria-label": "Automatic reply message", className: INPUT_CLASS, rows: 5, value: oofMessage, onChange: (e) => setOofMessage(e.target.value) })] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Start (optional)" }), _jsx("input", { type: "datetime-local", "aria-label": "Automatic reply start", className: INPUT_CLASS, value: oofStartTime, onChange: (e) => setOofStartTime(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "End (optional)" }), _jsx("input", { type: "datetime-local", "aria-label": "Automatic reply end", className: INPUT_CLASS, value: oofEndTime, onChange: (e) => setOofEndTime(e.target.value) })] })] }), _jsx("p", { className: "text-xs text-text-muted -mt-2", children: "Leave both blank to stay on indefinitely until turned off." })] })), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }) }));
70
+ return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Automatic Replies" }), _jsxs("p", { className: "text-sm text-text-muted mb-4", children: ["Send an automatic reply to anyone who emails ", mailbox.displayName, " while you\u2019re away. A calendar event with its own automatic reply enabled takes over for its own start/end window."] }), saved && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm font-medium", children: [_jsx("input", { type: "checkbox", checked: oofEnabled, onChange: (e) => setOofEnabled(e.target.checked) }), "Automatic replies are on"] }), oofEnabled && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Message" }), _jsx("textarea", { "aria-label": "Automatic reply message", className: INPUT_CLASS, rows: 5, value: oofMessage, onChange: (e) => setOofMessage(e.target.value) })] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Start (optional)" }), _jsx("input", { type: "datetime-local", "aria-label": "Automatic reply start", className: INPUT_CLASS, value: oofStartTime, onChange: (e) => setOofStartTime(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "End (optional)" }), _jsx("input", { type: "datetime-local", "aria-label": "Automatic reply end", className: INPUT_CLASS, value: oofEndTime, onChange: (e) => setOofEndTime(e.target.value) })] })] }), _jsx("p", { className: "text-xs text-text-muted -mt-2", children: "Leave both blank to stay on indefinitely until turned off." })] })), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }) }));
59
71
  }
60
72
  export default SettingsAutoReplyPage;
61
73
  /** The tab's title: `Brand: Settings` (see `pageTitle()`). */
@@ -61,6 +61,10 @@ function ListSection({ kind, entries, otherEntries, ownAddresses, editable, onAd
61
61
  setError("That is one of your own addresses, so it cannot be blocked.");
62
62
  return;
63
63
  }
64
+ if (kind === "blocked" && checked.entry.startsWith("@") && ownAddresses.some((address) => address.toLowerCase().endsWith(checked.entry))) {
65
+ setError("That is your own domain: blocking it would send the mail of everyone at it - yours included - to Junk.");
66
+ return;
67
+ }
64
68
  setError(null);
65
69
  setBusy(true);
66
70
  try {