@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
@@ -6,6 +6,7 @@ import { pageTitle } from "../../../shared/navigation/pageTitle.js";
6
6
  import React, { FormEvent, useEffect, useState } from "react";
7
7
  import { ApiRequestError } from "../../../../lib/util/api.js";
8
8
  import {
9
+ EscrowInfo,
9
10
  KeyVault,
10
11
  MasterKeyWrap,
11
12
  PublicKey,
@@ -43,6 +44,7 @@ import { getMailbox } from "../../../../lib/mail/mailApi.js";
43
44
  import { useApiClient } from "../../../../lib/util/apiClientContext.js";
44
45
  import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
45
46
  import KeyEnrollmentGate from "../../../shared/components/layout/KeyEnrollmentGate.js";
47
+ import { MIN_PASSWORD_LENGTH } from "../../../shared/components/layout/RecoveryCodeUnlock.js";
46
48
  import { useUnlockPrompt } from "../../../shared/components/layout/UnlockPromptProvider.js";
47
49
  import { destroyLocalIndex } from "../../../shared/search/localIndexRpcClient.js";
48
50
  import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
@@ -62,8 +64,6 @@ import {
62
64
  import { STALE_REQUEST_TEXT, beforeRequestText, healthWarning, isInstalling, timeOf } from "../../../shared/signing/enrollmentView.js";
63
65
  import { useSigningEnrollmentInfo } from "../../../shared/signing/signingInfo.js";
64
66
 
65
- const MIN_PASSWORD_LENGTH = 8;
66
-
67
67
  /** Mirrors restapi's `BaseKeyVaultRoute` `MAX_MASTER_KEY_WRAPS` - `addMasterKeyWrap()` refuses a vault
68
68
  * already holding this many wraps (escrow included). */
69
69
  export const MAX_MASTER_KEY_WRAPS = 20;
@@ -253,6 +253,7 @@ function EncryptionContent({ canManageKeys }: { canManageKeys: boolean }) {
253
253
  // Distinguishes the "Save your new recovery codes" screen's copy for the two different actions that
254
254
  // land on it - a plain regeneration only invalidates old recovery codes, while a rotation also
255
255
  // invalidates every other unlock method, which the copy needs to say plainly.
256
+ const [confirmingRotation, setConfirmingRotation] = useState(false);
256
257
  const [recoveryCodesReason, setRecoveryCodesReason] = useState<"regenerate" | "rotate">("regenerate");
257
258
  const [codesSaved, setCodesSaved] = useState(false);
258
259
  const [codesCopied, setCodesCopied] = useState(false);
@@ -304,6 +305,7 @@ function EncryptionContent({ canManageKeys }: { canManageKeys: boolean }) {
304
305
  const hasEscrowWrap = vault?.masterKeyWraps.some((w) => w.method === "escrow") ?? false;
305
306
  const [wrappingEscrow, setWrappingEscrow] = useState(false);
306
307
  const [escrowError, setEscrowError] = useState<string | null>(null);
308
+ const [pendingEscrow, setPendingEscrow] = useState<EscrowInfo | null>(null);
307
309
 
308
310
  /**
309
311
  * This mailbox's unlocked keys *right now* - never a copy read at render time, which a lock (idle
@@ -351,13 +353,28 @@ function EncryptionContent({ canManageKeys }: { canManageKeys: boolean }) {
351
353
  return err instanceof ApiRequestError ? err.message : fallback;
352
354
  }
353
355
 
356
+ /** Looks up the certificate the server says this mailbox's escrow scope holds, and asks the owner to confirm it
357
+ * before the master key is wrapped to it: nothing else vouches for that certificate, and whoever holds its
358
+ * private key can read this mailbox's encrypted mail. */
354
359
  async function handleWrapEscrow() {
355
360
  setEscrowError(null);
356
361
  setWrappingEscrow(true);
357
362
  try {
358
363
  // Only reachable when mailbox.escrowScopeId is set (see the render guard below).
364
+ await verifiedUnlockedKeys();
365
+ setPendingEscrow(await getEscrowInfo(mailboxUid!, client));
366
+ } catch (err) {
367
+ setEscrowError(errorMessage(err, "Could not add escrow protection for this mailbox."));
368
+ } finally {
369
+ setWrappingEscrow(false);
370
+ }
371
+ }
372
+
373
+ async function handleConfirmEscrow(escrowInfo: EscrowInfo) {
374
+ setPendingEscrow(null);
375
+ setWrappingEscrow(true);
376
+ try {
359
377
  const { current } = await verifiedUnlockedKeys();
360
- const escrowInfo = await getEscrowInfo(mailboxUid!, client);
361
378
  const wrap = await buildEscrowWrap(current.masterKey, escrowInfo.escrowScopeId, fromBase64(escrowInfo.publicKey.publicKey));
362
379
  await addMasterKeyWrap(mailboxUid!, wrap, undefined, client);
363
380
  await loadVault();
@@ -729,7 +746,7 @@ function EncryptionContent({ canManageKeys }: { canManageKeys: boolean }) {
729
746
  * working the instant this succeeds, since `rekey()` replaces `masterKeyWraps` wholesale - the whole
730
747
  * point, for a captured-wrap scenario where it's unclear which method was compromised.
731
748
  */
732
- async function handleRotateKeys(e: FormEvent) {
749
+ function handleRotateKeys(e: FormEvent) {
733
750
  e.preventDefault();
734
751
  if (rotationBlockedBySigning) {
735
752
  // The form's controls are disabled in this state too; this also covers an implicit submit.
@@ -744,6 +761,13 @@ function EncryptionContent({ canManageKeys }: { canManageKeys: boolean }) {
744
761
  return;
745
762
  }
746
763
  setActionError(null);
764
+ // One press must not end every other unlock method and every other device's password: say so, and wait for a yes.
765
+ setConfirmingRotation(true);
766
+ }
767
+
768
+ async function rotateKeys() {
769
+ setConfirmingRotation(false);
770
+ setActionError(null);
747
771
  setEscrowError(null);
748
772
  setRotating(true);
749
773
  let keys: PublicKey[];
@@ -1038,6 +1062,30 @@ function EncryptionContent({ canManageKeys }: { canManageKeys: boolean }) {
1038
1062
  )}
1039
1063
  </div>
1040
1064
  )}
1065
+ <Modal open={pendingEscrow !== null} onClose={() => setPendingEscrow(null)} title="Confirm escrow certificate">
1066
+ <p className="text-sm mb-3">
1067
+ Anyone holding the private key for this certificate will be able to recover this mailbox&rsquo;s
1068
+ encrypted mail. Confirm it is the escrow certificate your organization expects before continuing.
1069
+ </p>
1070
+ <dl className="text-sm mb-5 flex flex-col gap-1">
1071
+ <div>
1072
+ <dt className="text-xs text-text-muted">Escrow scope</dt>
1073
+ <dd className="break-all">{pendingEscrow?.escrowScopeId}</dd>
1074
+ </div>
1075
+ <div>
1076
+ <dt className="text-xs text-text-muted">Certificate fingerprint</dt>
1077
+ <dd className="font-mono break-all">{pendingEscrow?.publicKey.fingerprint}</dd>
1078
+ </div>
1079
+ </dl>
1080
+ <div className="flex gap-3 justify-end">
1081
+ <Button type="button" variant="secondary" className="!w-auto" onClick={() => setPendingEscrow(null)}>
1082
+ Cancel
1083
+ </Button>
1084
+ <Button type="button" className="!w-auto" onClick={() => void handleConfirmEscrow(pendingEscrow!)}>
1085
+ Trust this certificate
1086
+ </Button>
1087
+ </div>
1088
+ </Modal>
1041
1089
  </div>
1042
1090
  )}
1043
1091
 
@@ -1160,6 +1208,20 @@ function EncryptionContent({ canManageKeys }: { canManageKeys: boolean }) {
1160
1208
  </Button>
1161
1209
  </div>
1162
1210
 
1211
+ <Modal open={confirmingRotation} onClose={() => setConfirmingRotation(false)} title="Rotate your keys?">
1212
+ <p className="text-sm mb-3">
1213
+ Every other way of unlocking this mailbox - its password, its recovery codes, and every other device&rsquo;s saved password - stops working
1214
+ the moment this finishes, and cannot be brought back. You will be shown new recovery codes once.
1215
+ </p>
1216
+ <div className="flex gap-3 justify-end">
1217
+ <Button type="button" variant="secondary" className="!w-auto" onClick={() => setConfirmingRotation(false)}>
1218
+ Cancel
1219
+ </Button>
1220
+ <Button type="button" className="!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger" onClick={() => void rotateKeys()}>
1221
+ Yes, rotate my keys
1222
+ </Button>
1223
+ </div>
1224
+ </Modal>
1163
1225
  <form onSubmit={handleRotateKeys} className="flex flex-col gap-2 border-t border-border pt-6">
1164
1226
  <h2 className="text-sm font-semibold">Rotate keys</h2>
1165
1227
  <p className="text-xs text-text-muted mb-1">
@@ -30,6 +30,7 @@ import Alert from "../../../../lib/components/feedback/Alert.js";
30
30
  import Button from "../../../../lib/components/buttons/Button.js";
31
31
  import Modal from "../../../../lib/components/overlays/Modal.js";
32
32
  import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
33
+ import GravatarSetting from "../../../shared/components/settings/GravatarSetting.js";
33
34
 
34
35
  const NON_MAIL_FOLDER_TYPES = new Set(["calendar", "contacts", "tasks", "notes"]);
35
36
 
@@ -83,6 +84,8 @@ function PrivacyContent({ userUid }: { userUid?: string }) {
83
84
  <p className="text-sm text-text-muted">Export or manage the data associated with this mailbox.</p>
84
85
  </div>
85
86
 
87
+ <GravatarSetting />
88
+
86
89
  {selectedIsOwn ? (
87
90
  <>
88
91
  <ExportSection mailboxUid={mailboxUid} mailboxLabel={`${selected.displayName} (${selected.primarySmtpAddress})`} />
@@ -288,7 +291,7 @@ function ImportSection({ mailboxUid }: { mailboxUid?: string }) {
288
291
  return;
289
292
  }
290
293
  setUploading(true);
291
- uploadMailboxImport(file, { format: importFormatFromFilename(file.name), targetFolderUid })
294
+ uploadMailboxImport(file, { format: importFormatFromFilename(file.name), targetFolderUid, mailboxUid: mailboxUid! }, client)
292
295
  .then(() => loadRequests())
293
296
  .catch((err) => notifyApiError(err, "Couldn't upload the file"))
294
297
  .finally(() => setUploading(false));
@@ -197,7 +197,18 @@ function ProfileContent() {
197
197
  </div>
198
198
  {zoneWasPreselected && (
199
199
  <p className="mt-1 text-xs text-text-muted">
200
- Your mailbox has no time zone chosen yet; this device&rsquo;s is preselected. Save to keep it.
200
+ Your mailbox has no time zone chosen yet (it uses UTC); this device&rsquo;s is preselected, and Save sets it - whatever else you change.{" "}
201
+ <Button
202
+ type="button"
203
+ variant="text"
204
+ className="!w-auto !inline !p-0 !text-xs"
205
+ onClick={() => {
206
+ setTimezone(DEFAULT_TIME_ZONE);
207
+ setSaved(false);
208
+ }}
209
+ >
210
+ Keep UTC
211
+ </Button>
201
212
  </p>
202
213
  )}
203
214
  <p id="profile-time-zone-help" className="mt-1 text-xs text-text-muted">
@@ -3,7 +3,7 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import { pageTitle } from "../../../shared/navigation/pageTitle.js";
6
- import React, { useEffect, useState } from "react";
6
+ import React, { useEffect, useRef, useState } from "react";
7
7
  import { ApiRequestError } from "../../../../lib/util/api.js";
8
8
  import {
9
9
  MailboxAccessMember,
@@ -60,15 +60,25 @@ function SharingContent() {
60
60
  // `SettingsShell` only ever renders its children once `mailboxUid` has resolved (a mailbox-less caller
61
61
  // gets `<MailboxProvisioning />` instead) - same established precedent as the Labels/Focused Inbox
62
62
  // settings pages, so none of the handlers below need their own `!mailboxUid` guard.
63
+ // Only the latest request's answer is shown (a reload after a change can overtake a slower earlier one).
64
+ const latestLoad = useRef(0);
65
+
63
66
  function reload() {
67
+ const load = ++latestLoad.current;
64
68
  setStatus("loading");
65
69
  setError(null);
66
70
  listMailboxAccess(mailboxUid!)
67
71
  .then((result) => {
72
+ if (load !== latestLoad.current) {
73
+ return;
74
+ }
68
75
  setMembers(result);
69
76
  setStatus("ready");
70
77
  })
71
78
  .catch((err) => {
79
+ if (load !== latestLoad.current) {
80
+ return;
81
+ }
72
82
  // A 403 here is the live, server-verified "can this caller manage this mailbox's sharing"
73
83
  // signal - the same permission threshold (ACLAction.UPDATE) this route itself requires, so
74
84
  // no separate probe call is needed to decide which state to render.
@@ -77,10 +77,7 @@ function SignatureDetailContent({ uid }: { uid: string }) {
77
77
  setSaving(true);
78
78
  setSaved(false);
79
79
  try {
80
- if (isDefaultForNewMessages || isDefaultForReplyForward) {
81
- const existing = await listMailSignatures(mailboxUid!, { limit: 100 });
82
- await clearPreviousDefaults(existing, original!.uid, isDefaultForNewMessages, isDefaultForReplyForward);
83
- }
80
+ // This signature is saved first: were it refused (a 409, a 400) after the others had been cleared, the mailbox would be left with no default at all.
84
81
  const updated = await updateMailSignature({
85
82
  uid: original!.uid,
86
83
  version: original!.version,
@@ -90,6 +87,11 @@ function SignatureDetailContent({ uid }: { uid: string }) {
90
87
  isDefaultForReplyForward,
91
88
  });
92
89
  setOriginal(updated);
90
+ if (isDefaultForNewMessages || isDefaultForReplyForward) {
91
+ // The signature's own mailbox, not the one the page happens to have selected.
92
+ const existing = await listMailSignatures(original!.mailboxUid, { limit: 100 });
93
+ await clearPreviousDefaults(existing, original!.uid, isDefaultForNewMessages, isDefaultForReplyForward);
94
+ }
93
95
  setSaved(true);
94
96
  } catch (err) {
95
97
  // A pop-up: `error` is only this form's own validation (and the load failure above).
@@ -4,7 +4,7 @@
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import { pageTitle } from "../../shared/navigation/pageTitle.js";
6
6
  import React, { FormEvent, useEffect, useMemo, useRef, useState } from "react";
7
- import { endOfWeek, isAfter, isBefore, isToday, parseISO, startOfDay } from "date-fns";
7
+ import { endOfWeek, isAfter, isBefore, isToday, startOfDay } from "date-fns";
8
8
  import { ApiRequestError } from "../../../lib/util/api.js";
9
9
  import {
10
10
  Task,
@@ -19,6 +19,8 @@ import {
19
19
  import { listFlaggedMessages } from "../../../lib/mail/flaggedMessages.js";
20
20
  import { Message } from "../../../lib/mail/mailApi.js";
21
21
  import { useApiClient } from "../../../lib/util/apiClientContext.js";
22
+ import { useDayKey } from "../../../lib/calendar/useDayKey.js";
23
+ import { dueDateInstant, parseDueDate } from "../../shared/components/tasks/dueDate.js";
22
24
  import TasksShell, { TasksShellProps, useTasksShell } from "../../shared/components/tasks/layout/TasksShell.js";
23
25
  import TasksSidebar, { TasksView } from "../../shared/components/tasks/TasksSidebar.js";
24
26
  import TasksToolbar, { TasksViewMode } from "../../shared/components/tasks/TasksToolbar.js";
@@ -44,15 +46,6 @@ function TasksPage(props: TasksShellProps) {
44
46
  );
45
47
  }
46
48
 
47
- /**
48
- * Parses a task's `dueDate` - `parseISO()` rather than `new Date()`, since `new Date("2026-06-16")` reads a
49
- * date-only value as UTC midnight, which is the *previous* local day anywhere west of UTC. A full
50
- * timestamp parses identically either way.
51
- */
52
- function parseDueDate(value: string): Date {
53
- return parseISO(value);
54
- }
55
-
56
49
  type Bucket = "Overdue" | "Today" | "This Week" | "Later" | "No due date";
57
50
  const BUCKET_ORDER: Bucket[] = ["Overdue", "Today", "This Week", "Later", "No due date"];
58
51
 
@@ -118,7 +111,10 @@ function TasksContent() {
118
111
  useShortcut(SHORTCUTS.tasks.create, () => addTaskRef.current!.focus(), { enabled: canAddTask });
119
112
  const addTaskHint = useShortcutProps("Add a task", SHORTCUTS.tasks.create, canAddTask);
120
113
 
114
+ // Which load is the latest: a slower, older one (another folder's, from before the mailbox changed) must not replace the list a newer one set.
115
+ const loadRun = useRef(0);
121
116
  function reload(): Promise<void> {
117
+ const run = ++loadRun.current;
122
118
  if (!folderUid) {
123
119
  setTasks([]);
124
120
  setTruncated(false);
@@ -129,11 +125,21 @@ function TasksContent() {
129
125
  setError(null);
130
126
  return listAllPages((page) => listTasks(folderUid, { limit: LIST_PAGE_SIZE, page }, client))
131
127
  .then((result) => {
132
- setTasks(result.items);
133
- setTruncated(result.truncated);
128
+ if (run === loadRun.current) {
129
+ setTasks(result.items);
130
+ setTruncated(result.truncated);
131
+ }
134
132
  })
135
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load tasks."))
136
- .finally(() => setLoading(false));
133
+ .catch((err) => {
134
+ if (run === loadRun.current) {
135
+ setError(err instanceof ApiRequestError ? err.message : "Could not load tasks.");
136
+ }
137
+ })
138
+ .finally(() => {
139
+ if (run === loadRun.current) {
140
+ setLoading(false);
141
+ }
142
+ });
137
143
  }
138
144
 
139
145
  useEffect(() => {
@@ -149,12 +155,29 @@ function TasksContent() {
149
155
  if (view.type !== "flagged" || !mailboxUid) {
150
156
  return;
151
157
  }
158
+ // Set once the view or mailbox this load was for is no longer the one shown, so its answer is dropped.
159
+ let stale = false;
152
160
  setFlaggedLoading(true);
153
161
  setFlaggedError(null);
154
162
  listFlaggedMessages(mailboxUid, client)
155
- .then(setFlaggedMessages)
156
- .catch((err) => setFlaggedError(err instanceof ApiRequestError ? err.message : "Could not load flagged email."))
157
- .finally(() => setFlaggedLoading(false));
163
+ .then((messages) => {
164
+ if (!stale) {
165
+ setFlaggedMessages(messages);
166
+ }
167
+ })
168
+ .catch((err) => {
169
+ if (!stale) {
170
+ setFlaggedError(err instanceof ApiRequestError ? err.message : "Could not load flagged email.");
171
+ }
172
+ })
173
+ .finally(() => {
174
+ if (!stale) {
175
+ setFlaggedLoading(false);
176
+ }
177
+ });
178
+ return () => {
179
+ stale = true;
180
+ };
158
181
  }, [view, mailboxUid]);
159
182
 
160
183
  async function handleCreate(e: FormEvent) {
@@ -180,8 +203,8 @@ function TasksContent() {
180
203
  mailboxUid: target,
181
204
  folderUid: targetFolderUid,
182
205
  title: title.trim(),
183
- // The date input's "yyyy-MM-dd" is the user's *local* day - see `parseDueDate()`.
184
- dueDate: dueDate ? parseDueDate(dueDate).toISOString() : undefined,
206
+ // The date input's "yyyy-MM-dd" is stored as that day's UTC midnight - see `dueDateInstant()`.
207
+ dueDate: dueDate ? dueDateInstant(dueDate) : undefined,
185
208
  priority,
186
209
  },
187
210
  client,
@@ -203,7 +226,24 @@ function TasksContent() {
203
226
  }
204
227
  }
205
228
 
229
+ // The tasks whose completion is being saved. The control is disabled meanwhile, and the ref (current at once, unlike state) turns away a second click that
230
+ // lands before the page has re-rendered: it would send the same stale `version` and be refused, rolling the task back to a state the server has left.
231
+ const togglingRef = useRef(new Set<string>());
232
+ const [toggling, setToggling] = useState<Set<string>>(new Set());
233
+ function setTogglePending(uid: string, pending: boolean) {
234
+ if (pending) {
235
+ togglingRef.current.add(uid);
236
+ } else {
237
+ togglingRef.current.delete(uid);
238
+ }
239
+ setToggling(new Set(togglingRef.current));
240
+ }
241
+
206
242
  async function handleToggle(task: Task) {
243
+ if (togglingRef.current.has(task.uid)) {
244
+ return;
245
+ }
246
+ setTogglePending(task.uid, true);
207
247
  const nextCompleted = !task.completed;
208
248
  setTasks((prev) => prev.map((t) => (t.uid === task.uid ? { ...t, completed: nextCompleted } : t)));
209
249
  try {
@@ -212,6 +252,8 @@ function TasksContent() {
212
252
  } catch (err) {
213
253
  setTasks((prev) => prev.map((t) => (t.uid === task.uid ? task : t)));
214
254
  notifyApiError(err, "Couldn't update the task");
255
+ } finally {
256
+ setTogglePending(task.uid, false);
215
257
  }
216
258
  }
217
259
 
@@ -262,8 +304,24 @@ function TasksContent() {
262
304
 
263
305
  const checkedTasks = viewFiltered.filter((t) => checkedUids.has(t.uid));
264
306
 
307
+ // 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.
308
+ const bulkRunning = useRef(false);
309
+ function singleFlight(run: () => Promise<void>): () => Promise<void> {
310
+ return async () => {
311
+ if (bulkRunning.current) {
312
+ return;
313
+ }
314
+ bulkRunning.current = true;
315
+ try {
316
+ await run();
317
+ } finally {
318
+ bulkRunning.current = false;
319
+ }
320
+ };
321
+ }
322
+
265
323
  // The bulk handlers try every task and report each failure: the same failure repeated is one pop-up with a count.
266
- async function handleBulkComplete() {
324
+ const handleBulkComplete = singleFlight(async () => {
267
325
  for (const task of checkedTasks) {
268
326
  try {
269
327
  await setTaskCompleted(task, true, client);
@@ -273,9 +331,9 @@ function TasksContent() {
273
331
  }
274
332
  setCheckedUids(new Set());
275
333
  await reload();
276
- }
334
+ });
277
335
 
278
- async function handleBulkAddToMyDay() {
336
+ const handleBulkAddToMyDay = singleFlight(async () => {
279
337
  for (const task of checkedTasks) {
280
338
  try {
281
339
  await setTaskMyDay(task, true, client);
@@ -285,7 +343,7 @@ function TasksContent() {
285
343
  }
286
344
  setCheckedUids(new Set());
287
345
  await reload();
288
- }
346
+ });
289
347
 
290
348
  async function handleBulkDelete() {
291
349
  for (const task of checkedTasks) {
@@ -313,6 +371,8 @@ function TasksContent() {
313
371
  }
314
372
  }
315
373
 
374
+ // Re-worked when the day changes, so a page left open past midnight moves Today's tasks to Overdue.
375
+ const dayKey = useDayKey();
316
376
  const grouped = useMemo(() => {
317
377
  const now = new Date();
318
378
  const map = new Map<Bucket, Task[]>(BUCKET_ORDER.map((b) => [b, []]));
@@ -323,7 +383,7 @@ function TasksContent() {
323
383
  map.get(bucketFor(task, now))!.push(task);
324
384
  }
325
385
  return map;
326
- }, [viewFiltered]);
386
+ }, [viewFiltered, dayKey]);
327
387
 
328
388
  const completedTasks = viewFiltered.filter((t) => t.completed);
329
389
 
@@ -428,6 +488,7 @@ function TasksContent() {
428
488
  checkedUids={checkedUids}
429
489
  onToggleChecked={toggleChecked}
430
490
  onToggle={handleToggle}
491
+ toggling={toggling}
431
492
  onDelete={requestDelete}
432
493
  />
433
494
  ) : (
@@ -445,6 +506,7 @@ function TasksContent() {
445
506
  checkedUids={checkedUids}
446
507
  onToggleChecked={toggleChecked}
447
508
  onToggle={handleToggle}
509
+ toggling={toggling}
448
510
  onDelete={requestDelete}
449
511
  />
450
512
  );
@@ -456,6 +518,7 @@ function TasksContent() {
456
518
  checkedUids={checkedUids}
457
519
  onToggleChecked={toggleChecked}
458
520
  onToggle={handleToggle}
521
+ toggling={toggling}
459
522
  onDelete={requestDelete}
460
523
  />
461
524
  )}
@@ -509,12 +572,13 @@ function FlaggedEmailList({ loading, error, messages }: { loading: boolean; erro
509
572
 
510
573
  /** A circular completion-toggle control matching Outlook's visual language (not a native checkbox), while
511
574
  * staying a real accessible toggle. */
512
- function CompletionToggle({ task, onToggle }: { task: Task; onToggle: (task: Task) => void }) {
575
+ function CompletionToggle({ task, onToggle, pending }: { task: Task; onToggle: (task: Task) => void; pending: boolean }) {
513
576
  return (
514
577
  <button
515
578
  type="button"
516
579
  role="checkbox"
517
580
  aria-checked={task.completed}
581
+ disabled={pending}
518
582
  aria-label={`Mark "${task.title}" as ${task.completed ? "not complete" : "complete"}`}
519
583
  onClick={() => onToggle(task)}
520
584
  className={[
@@ -533,10 +597,12 @@ interface TaskGroupProps {
533
597
  checkedUids: Set<string>;
534
598
  onToggleChecked: (uid: string) => void;
535
599
  onToggle: (task: Task) => void;
600
+ /** The uids of the tasks whose completion is being saved. */
601
+ toggling: Set<string>;
536
602
  onDelete: (task: Task) => void;
537
603
  }
538
604
 
539
- function TaskGroup({ label, tasks, checkedUids, onToggleChecked, onToggle, onDelete }: TaskGroupProps) {
605
+ function TaskGroup({ label, tasks, checkedUids, onToggleChecked, onToggle, toggling, onDelete }: TaskGroupProps) {
540
606
  return (
541
607
  <div>
542
608
  <h2 className="text-xs font-bold uppercase tracking-wide text-text-muted mb-2">{label}</h2>
@@ -549,7 +615,7 @@ function TaskGroup({ label, tasks, checkedUids, onToggleChecked, onToggle, onDel
549
615
  checked={checkedUids.has(task.uid)}
550
616
  onChange={() => onToggleChecked(task.uid)}
551
617
  />
552
- <CompletionToggle task={task} onToggle={onToggle} />
618
+ <CompletionToggle task={task} onToggle={onToggle} pending={toggling.has(task.uid)} />
553
619
  <span className={["flex-1 text-sm", task.completed ? "line-through text-text-muted" : ""].join(" ")}>
554
620
  {task.title}
555
621
  </span>
@@ -581,10 +647,12 @@ interface TaskTableProps {
581
647
  checkedUids: Set<string>;
582
648
  onToggleChecked: (uid: string) => void;
583
649
  onToggle: (task: Task) => void;
650
+ /** The uids of the tasks whose completion is being saved. */
651
+ toggling: Set<string>;
584
652
  onDelete: (task: Task) => void;
585
653
  }
586
654
 
587
- function TaskTable({ tasks, checkedUids, onToggleChecked, onToggle, onDelete }: TaskTableProps) {
655
+ function TaskTable({ tasks, checkedUids, onToggleChecked, onToggle, toggling, onDelete }: TaskTableProps) {
588
656
  const allChecked = tasks.length > 0 && tasks.every((t) => checkedUids.has(t.uid));
589
657
 
590
658
  function toggleAll() {
@@ -630,7 +698,7 @@ function TaskTable({ tasks, checkedUids, onToggleChecked, onToggle, onDelete }:
630
698
  />
631
699
  </td>
632
700
  <td className="px-3 py-2">
633
- <CompletionToggle task={task} onToggle={onToggle} />
701
+ <CompletionToggle task={task} onToggle={onToggle} pending={toggling.has(task.uid)} />
634
702
  </td>
635
703
  <td className={["px-3 py-2", task.completed ? "line-through text-text-muted" : ""].join(" ")}>{task.title}</td>
636
704
  <td className="px-3 py-2 text-text-muted">{task.dueDate ? parseDueDate(task.dueDate).toLocaleDateString() : ""}</td>
@@ -4,11 +4,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useRef, useState } from "react";
7
- import { ApiRequestError } from "../../../lib/util/api.js";
8
7
  import { listAuditLog } from "../../../lib/admin/auditLogApi.js";
9
8
  import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
10
- import Alert from "../../../lib/components/feedback/Alert.js";
9
+ import Alert from "../../shared/components/admin/ActionAlert.js";
11
10
  import Button from "../../../lib/components/buttons/Button.js";
11
+ import { actionErrorMessage } from "../../shared/components/admin/elevation.js";
12
12
  const PAGE_SIZE = 25;
13
13
  /** How long typing in a filter box has to pause before the log is re-fetched. */
14
14
  export const FILTER_DEBOUNCE_MS = 300;
@@ -64,7 +64,7 @@ function AuditLogContent() {
64
64
  })
65
65
  .catch((err) => {
66
66
  if (!cancelled)
67
- setError(err instanceof ApiRequestError ? err.message : "Could not load the audit log.");
67
+ setError(actionErrorMessage(err, "Could not load the audit log."));
68
68
  })
69
69
  .finally(() => {
70
70
  if (!cancelled)
@@ -4,11 +4,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useState } from "react";
7
- import { ApiRequestError } from "../../../lib/util/api.js";
8
7
  import { getBranding } from "../../../lib/branding/brandingApi.js";
9
8
  import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
10
9
  import BrandingForm from "../../shared/components/admin/settings/BrandingForm.js";
11
- import Alert from "../../../lib/components/feedback/Alert.js";
10
+ import Alert from "../../shared/components/admin/ActionAlert.js";
11
+ import { actionErrorMessage } from "../../shared/components/admin/elevation.js";
12
12
  export default function BrandingPage(props) {
13
13
  return (_jsx(AdminShell, { ...props, active: "branding", children: _jsx(BrandingContent, {}) }));
14
14
  }
@@ -19,7 +19,7 @@ function BrandingContent() {
19
19
  useEffect(() => {
20
20
  getBranding()
21
21
  .then(setBranding)
22
- .catch((err) => setLoadError(err instanceof ApiRequestError ? err.message : "Could not load branding."))
22
+ .catch((err) => setLoadError(actionErrorMessage(err, "Could not load branding.")))
23
23
  .finally(() => setLoading(false));
24
24
  }, []);
25
25
  if (loading) {