@rapidmx/web-client 0.15.1 → 0.17.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 (318) hide show
  1. package/README.md +381 -373
  2. package/apps/admin/branding/index.tsx +39 -39
  3. package/apps/admin/data-requests/index.tsx +482 -480
  4. package/apps/admin/distribution-lists/new/index.tsx +3 -1
  5. package/apps/admin/domains/[uid].tsx +166 -164
  6. package/apps/admin/domains/new/index.tsx +3 -1
  7. package/apps/admin/escrow-scopes/[uid].tsx +350 -348
  8. package/apps/admin/escrow-scopes/new/index.tsx +3 -1
  9. package/apps/admin/index.tsx +129 -124
  10. package/apps/admin/mailboxes/[uid].tsx +271 -224
  11. package/apps/admin/mailboxes/new/index.tsx +30 -28
  12. package/apps/admin/plugins/index.tsx +15 -15
  13. package/apps/admin/retention-policy/index.tsx +39 -39
  14. package/apps/admin/signing-certificates/index.tsx +343 -343
  15. package/apps/admin/transport-rules/new/index.tsx +3 -1
  16. package/apps/escrow/audit-log/index.tsx +196 -196
  17. package/apps/escrow/matters/[uid].tsx +621 -621
  18. package/apps/escrow/matters/new/index.tsx +3 -1
  19. package/apps/shared/auth/adminAccess.ts +99 -99
  20. package/apps/shared/components/admin/layout/AdminShell.tsx +381 -378
  21. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
  22. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
  23. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
  24. package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
  25. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +50 -3
  27. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
  28. package/apps/shared/components/admin/settings/PluginsManager.tsx +1545 -1545
  29. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
  30. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
  31. package/apps/shared/components/admin/setup/SetupWizard.tsx +446 -444
  32. package/apps/shared/components/admin/usePagedList.tsx +129 -129
  33. package/apps/shared/components/calendar/EventEditor.tsx +5 -10
  34. package/apps/shared/components/calendar/EventFormParts.tsx +16 -47
  35. package/apps/shared/components/calendar/EventModal.tsx +206 -206
  36. package/apps/shared/components/calendar/GuestInput.tsx +100 -0
  37. package/apps/shared/components/calendar/MonthView.tsx +185 -185
  38. package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
  39. package/apps/shared/components/calendar/RequestChangeForm.tsx +23 -54
  40. package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
  41. package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
  42. package/apps/shared/components/calendar/allDay.ts +124 -124
  43. package/apps/shared/components/calendar/eventForm.ts +2 -2
  44. package/apps/shared/components/calendar/eventFormat.ts +49 -22
  45. package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
  46. package/apps/shared/components/contacts/ContactForm.tsx +383 -383
  47. package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
  48. package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
  49. package/apps/shared/components/contacts/layout/ContactsShell.tsx +218 -216
  50. package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
  51. package/apps/shared/components/layout/AppShell.tsx +461 -480
  52. package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
  53. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
  54. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
  55. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
  56. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
  57. package/apps/shared/components/layout/UserMenu.tsx +439 -424
  58. package/apps/shared/components/mail/ConversationList.tsx +323 -323
  59. package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -491
  60. package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
  61. package/apps/shared/components/mail/MailSelectionBar.tsx +240 -227
  62. package/apps/shared/components/mail/MenuButton.tsx +31 -11
  63. package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1559
  64. package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
  65. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
  66. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
  67. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1677
  68. package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
  69. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
  70. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
  71. package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
  72. package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
  73. package/apps/shared/components/mail/layout/MailShell.tsx +161 -134
  74. package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
  75. package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
  76. package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
  77. package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
  78. package/apps/shared/components/mail/reading/messageExport.ts +59 -0
  79. package/apps/shared/components/mail/reading/printMessage.ts +99 -0
  80. package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
  81. package/apps/shared/components/mail/verificationSeals.ts +125 -125
  82. package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
  83. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
  84. package/apps/shared/components/settings/layout/SettingsShell.tsx +257 -254
  85. package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
  86. package/apps/shared/components/tasks/layout/TasksShell.tsx +219 -217
  87. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
  88. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
  89. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
  90. package/apps/shared/keyboard/dispatch.ts +124 -124
  91. package/apps/shared/keyboard/format.ts +89 -89
  92. package/apps/shared/keyboard/keymap.ts +114 -114
  93. package/apps/shared/keyboard/registry.ts +65 -65
  94. package/apps/shared/keyboard/targets.ts +79 -79
  95. package/apps/shared/mail/folderCounts.ts +17 -0
  96. package/apps/shared/mail/folderOfType.ts +49 -0
  97. package/apps/shared/mail/folderTree.ts +143 -143
  98. package/apps/shared/mail/listAllPages.ts +39 -39
  99. package/apps/shared/mail/newMailNotifications.ts +171 -171
  100. package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
  101. package/apps/shared/mail/outbox/sendJob.ts +19 -1
  102. package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
  103. package/apps/shared/mail/permanentDelete.ts +160 -0
  104. package/apps/shared/mail/primaryMailbox.ts +71 -0
  105. package/apps/shared/mail/reportNotices.ts +66 -0
  106. package/apps/shared/mail/senderBlocking.ts +141 -0
  107. package/apps/shared/mail/useCollapsedSections.ts +146 -0
  108. package/apps/shared/mail/useMailConnection.ts +205 -203
  109. package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
  110. package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
  111. package/apps/shared/mail/useMarkMessageRead.ts +47 -47
  112. package/apps/shared/mail/useNewMailNotifications.ts +178 -178
  113. package/apps/shared/mail/usePermanentDelete.tsx +160 -0
  114. package/apps/shared/navigation/frameContext.tsx +1 -1
  115. package/apps/shared/navigation/index.ts +43 -0
  116. package/apps/shared/navigation/pageTitle.ts +19 -0
  117. package/apps/shared/notifications/store.ts +560 -560
  118. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
  119. package/apps/shared/search/crossMailboxSearch.ts +169 -0
  120. package/apps/shared/search/localIndexBuilder.ts +481 -481
  121. package/apps/shared/signing/enrollmentStorage.ts +33 -33
  122. package/apps/shared/signing/enrollmentTracker.ts +385 -385
  123. package/apps/shared/signing/enrollmentView.ts +251 -251
  124. package/apps/shared/signing/useNow.ts +19 -19
  125. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
  126. package/apps/shared/styles/app.css +396 -386
  127. package/apps/www/_shell.tsx +82 -0
  128. package/apps/www/calendar/index.tsx +581 -571
  129. package/apps/www/contacts/[uid].tsx +112 -109
  130. package/apps/www/contacts/index.tsx +14 -3
  131. package/apps/www/index.tsx +2962 -2479
  132. package/apps/www/messages/[uid].tsx +139 -135
  133. package/apps/www/settings/appearance/index.tsx +5 -2
  134. package/apps/www/settings/auto-reply/index.tsx +136 -133
  135. package/apps/www/settings/blocked-senders/index.tsx +303 -0
  136. package/apps/www/settings/encryption/index.tsx +1290 -1287
  137. package/apps/www/settings/filters/[uid].tsx +179 -176
  138. package/apps/www/settings/filters/index.tsx +105 -102
  139. package/apps/www/settings/filters/new/index.tsx +165 -140
  140. package/apps/www/settings/labels/index.tsx +207 -204
  141. package/apps/www/settings/privacy/index.tsx +495 -492
  142. package/apps/www/settings/profile/index.tsx +251 -248
  143. package/apps/www/settings/read-receipts/index.tsx +150 -147
  144. package/apps/www/settings/sharing/index.tsx +259 -256
  145. package/apps/www/settings/signatures/[uid].tsx +175 -172
  146. package/apps/www/settings/signatures/index.tsx +91 -88
  147. package/apps/www/settings/signatures/new/index.tsx +138 -135
  148. package/apps/www/tasks/index.tsx +654 -649
  149. package/dist/apps/admin/data-requests/index.js +1 -1
  150. package/dist/apps/admin/distribution-lists/new/index.js +3 -1
  151. package/dist/apps/admin/domains/[uid].js +3 -1
  152. package/dist/apps/admin/domains/new/index.js +3 -1
  153. package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
  154. package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
  155. package/dist/apps/admin/index.js +5 -2
  156. package/dist/apps/admin/mailboxes/[uid].js +15 -3
  157. package/dist/apps/admin/mailboxes/new/index.js +3 -1
  158. package/dist/apps/admin/transport-rules/new/index.js +3 -1
  159. package/dist/apps/escrow/matters/new/index.js +3 -1
  160. package/dist/apps/shared/components/admin/layout/AdminShell.js +6 -3
  161. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
  162. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
  163. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
  164. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
  165. package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
  166. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +25 -3
  167. package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
  168. package/dist/apps/shared/components/calendar/EventEditor.js +5 -10
  169. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -2
  170. package/dist/apps/shared/components/calendar/EventFormParts.js +5 -16
  171. package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
  172. package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
  173. package/dist/apps/shared/components/calendar/RequestChangeForm.js +11 -22
  174. package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -2
  175. package/dist/apps/shared/components/calendar/eventFormat.d.ts +21 -8
  176. package/dist/apps/shared/components/calendar/eventFormat.js +50 -23
  177. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +5 -4
  178. package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
  179. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  180. package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
  181. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +7 -5
  182. package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
  183. package/dist/apps/shared/components/layout/AppShell.js +24 -33
  184. package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
  185. package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
  186. package/dist/apps/shared/components/layout/UserMenu.d.ts +4 -1
  187. package/dist/apps/shared/components/layout/UserMenu.js +6 -5
  188. package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
  189. package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
  190. package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
  191. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +14 -4
  192. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -13
  193. package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
  194. package/dist/apps/shared/components/mail/MenuButton.js +15 -11
  195. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
  196. package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
  197. package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
  198. package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
  199. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +8 -4
  200. package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
  201. package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
  202. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
  203. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
  204. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +16 -7
  205. package/dist/apps/shared/components/mail/layout/MailShell.js +78 -65
  206. package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
  207. package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
  208. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
  209. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
  210. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
  211. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
  212. package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
  213. package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
  214. package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
  215. package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
  216. package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
  217. package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
  218. package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
  219. package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
  220. package/dist/apps/shared/components/settings/layout/SettingsShell.js +10 -7
  221. package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
  222. package/dist/apps/shared/components/tasks/layout/TasksShell.js +7 -5
  223. package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
  224. package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
  225. package/dist/apps/shared/mail/folderCounts.js +12 -0
  226. package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
  227. package/dist/apps/shared/mail/folderOfType.js +40 -0
  228. package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
  229. package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
  230. package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
  231. package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
  232. package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
  233. package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
  234. package/dist/apps/shared/mail/permanentDelete.js +127 -0
  235. package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
  236. package/dist/apps/shared/mail/primaryMailbox.js +61 -0
  237. package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
  238. package/dist/apps/shared/mail/reportNotices.js +47 -0
  239. package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
  240. package/dist/apps/shared/mail/senderBlocking.js +92 -0
  241. package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
  242. package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
  243. package/dist/apps/shared/mail/useMailConnection.js +3 -1
  244. package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
  245. package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
  246. package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
  247. package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
  248. package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
  249. package/dist/apps/shared/navigation/index.d.ts +16 -0
  250. package/dist/apps/shared/navigation/index.js +34 -0
  251. package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
  252. package/dist/apps/shared/navigation/pageTitle.js +12 -0
  253. package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
  254. package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
  255. package/dist/apps/shared/styles/app.css +396 -386
  256. package/dist/apps/www/_shell.d.ts +26 -0
  257. package/dist/apps/www/_shell.js +31 -0
  258. package/dist/apps/www/calendar/index.d.ts +7 -2
  259. package/dist/apps/www/calendar/index.js +7 -4
  260. package/dist/apps/www/contacts/[uid].d.ts +14 -3
  261. package/dist/apps/www/contacts/[uid].js +5 -3
  262. package/dist/apps/www/contacts/index.d.ts +7 -2
  263. package/dist/apps/www/contacts/index.js +8 -4
  264. package/dist/apps/www/index.d.ts +7 -2
  265. package/dist/apps/www/index.js +542 -207
  266. package/dist/apps/www/messages/[uid].d.ts +14 -3
  267. package/dist/apps/www/messages/[uid].js +7 -5
  268. package/dist/apps/www/settings/appearance/index.d.ts +8 -2
  269. package/dist/apps/www/settings/appearance/index.js +4 -2
  270. package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
  271. package/dist/apps/www/settings/auto-reply/index.js +4 -2
  272. package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
  273. package/dist/apps/www/settings/blocked-senders/index.js +134 -0
  274. package/dist/apps/www/settings/encryption/index.d.ts +7 -2
  275. package/dist/apps/www/settings/encryption/index.js +4 -2
  276. package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
  277. package/dist/apps/www/settings/filters/[uid].js +4 -2
  278. package/dist/apps/www/settings/filters/index.d.ts +7 -2
  279. package/dist/apps/www/settings/filters/index.js +4 -2
  280. package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
  281. package/dist/apps/www/settings/filters/new/index.js +28 -5
  282. package/dist/apps/www/settings/labels/index.d.ts +7 -2
  283. package/dist/apps/www/settings/labels/index.js +4 -2
  284. package/dist/apps/www/settings/privacy/index.d.ts +7 -2
  285. package/dist/apps/www/settings/privacy/index.js +4 -2
  286. package/dist/apps/www/settings/profile/index.d.ts +7 -2
  287. package/dist/apps/www/settings/profile/index.js +4 -2
  288. package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
  289. package/dist/apps/www/settings/read-receipts/index.js +4 -2
  290. package/dist/apps/www/settings/sharing/index.d.ts +7 -2
  291. package/dist/apps/www/settings/sharing/index.js +4 -2
  292. package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
  293. package/dist/apps/www/settings/signatures/[uid].js +4 -2
  294. package/dist/apps/www/settings/signatures/index.d.ts +7 -2
  295. package/dist/apps/www/settings/signatures/index.js +4 -2
  296. package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
  297. package/dist/apps/www/settings/signatures/new/index.js +5 -3
  298. package/dist/apps/www/tasks/index.d.ts +7 -2
  299. package/dist/apps/www/tasks/index.js +5 -3
  300. package/package.json +4 -2
  301. package/apps/shared/navigation/AppRouter.tsx +0 -300
  302. package/apps/shared/navigation/idle.ts +0 -45
  303. package/apps/shared/navigation/routerContext.tsx +0 -83
  304. package/apps/shared/navigation/routes.ts +0 -72
  305. package/apps/www/_routedPage.tsx +0 -24
  306. package/apps/www/_routes.ts +0 -37
  307. package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
  308. package/dist/apps/shared/navigation/AppRouter.js +0 -242
  309. package/dist/apps/shared/navigation/idle.d.ts +0 -14
  310. package/dist/apps/shared/navigation/idle.js +0 -43
  311. package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
  312. package/dist/apps/shared/navigation/routerContext.js +0 -56
  313. package/dist/apps/shared/navigation/routes.d.ts +0 -32
  314. package/dist/apps/shared/navigation/routes.js +0 -37
  315. package/dist/apps/www/_routedPage.d.ts +0 -12
  316. package/dist/apps/www/_routedPage.js +0 -19
  317. package/dist/apps/www/_routes.d.ts +0 -11
  318. package/dist/apps/www/_routes.js +0 -31
@@ -4,6 +4,7 @@ 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, useLayoutEffect, useRef, useState } from "react";
7
+ import { flushSync } from "react-dom";
7
8
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
9
  import { listAttachments } from "@rapidmx/react-shared/mail/mailApi.js";
9
10
  import { listConversationMessages } from "@rapidmx/react-shared/mail/conversationsApi.js";
@@ -11,8 +12,10 @@ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
12
  import MessageDetailPane from "./MessageDetailPane.js";
12
13
  import { EncryptedPreview, displaySubject } from "./reading/EncryptedPreview.js";
13
14
  import { CollapsedCard, SkeletonCards, SubjectCard } from "./reading/MessageCard.js";
15
+ import PendingMessageCard from "./reading/PendingMessageCard.js";
14
16
  import { useMailShell } from "./layout/MailShell.js";
15
17
  import { setReadState } from "../../mail/messageReadState.js";
18
+ import { adoptedOutgoing, belongsToThread, settleOutgoing, useOutgoingReplies } from "../../mail/outbox/outgoingReplies.js";
16
19
  import { dateClass, isUnread, senderClass } from "./unreadStyle.js";
17
20
  /** One request's worth of the thread. The server's own default for `listConversationMessages()`. */
18
21
  export const THREAD_PAGE_SIZE = 100;
@@ -77,6 +80,20 @@ async function loadThread(mailboxUid, conversationId) {
77
80
  messages.reverse();
78
81
  return { messages, truncated: more };
79
82
  }
83
+ /**
84
+ * `next` - the thread as it was just read again - for the pane to show in place of `previous`. A message the pane holds a newer copy of (one that
85
+ * was read, flagged or labelled here a moment ago, whose change the read may have started before) keeps that copy, and a read that changed
86
+ * nothing gives back `previous` itself, so the pane is not drawn again.
87
+ */
88
+ function mergeThread(previous, next) {
89
+ const held = new Map(previous.map((message) => [message.uid, message]));
90
+ const merged = next.map((message) => {
91
+ const own = held.get(message.uid);
92
+ return own && own.version > message.version ? own : message;
93
+ });
94
+ const unchanged = merged.length === previous.length && merged.every((message, index) => message.uid === previous[index].uid && message.version === previous[index].version);
95
+ return unchanged ? previous : merged;
96
+ }
80
97
  /** Whether the browser is showing a focus ring on `element` - it was focused from the keyboard (or by a key press's handler), not by a click. */
81
98
  function hasFocusRing(element) {
82
99
  try {
@@ -105,7 +122,9 @@ function hasFocusRing(element) {
105
122
  * that - mounting a body iframe per message up front would be wasteful in a long thread.
106
123
  */
107
124
  export default function ConversationThreadPane({ conversation, mailboxUid, selectedUid, folders, labels, onMessagePatched, onMessageRemoved, onLabelCreated, onFolderCreated, shortcuts, }) {
108
- const { trackMessageChange } = useMailShell();
125
+ const { trackMessageChange, live } = useMailShell();
126
+ // The replies and forwards this tab has sent, drawn at the top of the thread they continue until the server's own copy is in it.
127
+ const outgoing = useOutgoingReplies();
109
128
  const [messages, setMessages] = useState([]);
110
129
  const [attachmentsByUid, setAttachmentsByUid] = useState({});
111
130
  const [expandedUids, setExpandedUids] = useState(new Set());
@@ -134,11 +153,19 @@ export default function ConversationThreadPane({ conversation, mailboxUid, selec
134
153
  /** The message whose header button had the focus when it was toggled: expanding or collapsing swaps that button for the other state's, so the
135
154
  * focus is put back on the new one. */
136
155
  const refocusRef = useRef(null);
156
+ /** The pending cards that have already been scrolled to, so that only a message that has just been sent takes the view and the focus. */
157
+ const announcedRef = useRef(new Set());
158
+ /** Messages that left the thread here (archived, moved, a scheduled send taken back): a read of the thread again still lists them, since a conversation spans folders. */
159
+ const removedRef = useRef(new Set());
160
+ /** The last live update this pane has answered - one already in the shell's hands when the pane opened is not news. */
161
+ const seenLiveRef = useRef(live);
137
162
  const conversationId = conversation?.conversationId;
138
163
  useEffect(() => {
139
164
  const generation = ++generationRef.current;
140
165
  markReadRequestedRef.current = new Set();
141
166
  attachmentsRequestedRef.current = new Set();
167
+ announcedRef.current = new Set();
168
+ removedRef.current = new Set();
142
169
  appliedSelectionRef.current = null;
143
170
  loadedIdRef.current = null;
144
171
  setMessages([]);
@@ -159,6 +186,8 @@ export default function ConversationThreadPane({ conversation, mailboxUid, selec
159
186
  if (generation !== generationRef.current)
160
187
  return;
161
188
  loadedIdRef.current = conversationId;
189
+ // A message sent from here whose Sent Items copy is already in the thread is drawn as that copy, not as a pending card.
190
+ settleOutgoing(mailboxUid, loaded.messages);
162
191
  setMessages(loaded.messages);
163
192
  setTruncated(loaded.truncated);
164
193
  })
@@ -283,6 +312,77 @@ export default function ConversationThreadPane({ conversation, mailboxUid, selec
283
312
  }
284
313
  }
285
314
  }, [expandedUids, messages]);
315
+ /**
316
+ * Reads the thread again, quietly: what is on screen stays until the answer is here (a read that fails changes nothing), a message that arrived
317
+ * is added at the top, and the reader's expanded and collapsed cards are left as they are. A message this tab sent - which the server has
318
+ * now filed in Sent Items under the same `uid` - takes the place of its pending card, expanded as that card was, with the focus if the
319
+ * card had it. A thread that has not finished its first load is not read again: that load is the fresh read.
320
+ */
321
+ function refreshThread() {
322
+ const id = loadedIdRef.current;
323
+ if (!id) {
324
+ return;
325
+ }
326
+ const generation = generationRef.current;
327
+ loadThread(mailboxUid, id).then((loaded) => {
328
+ if (generation !== generationRef.current)
329
+ return;
330
+ const current = loaded.messages.filter((message) => !removedRef.current.has(message.uid));
331
+ const adopted = adoptedOutgoing(mailboxUid, current);
332
+ const refocus = adopted.find((uid) => document.activeElement === headerRefs.current[uid]);
333
+ // Drawn first, and only then is the pending card forgotten: there is never a frame with neither it nor the real message.
334
+ flushSync(() => {
335
+ setMessages((previous) => mergeThread(previous, current));
336
+ setTruncated(loaded.truncated);
337
+ if (adopted.length > 0) {
338
+ setExpandedUids((previous) => new Set([...previous, ...adopted]));
339
+ }
340
+ if (refocus) {
341
+ setPendingFocusUid(refocus);
342
+ }
343
+ });
344
+ settleOutgoing(mailboxUid, current);
345
+ },
346
+ // Quietly: the next live update reads it again, and what is shown is still right.
347
+ () => undefined);
348
+ }
349
+ // Another message of this conversation may have arrived - a recipient's reply, one sent from another tab or device, or this tab's own reply
350
+ // filed in Sent Items - whenever the shell announces a live update that touched one of this mailbox's folders (or does not say which).
351
+ useEffect(() => {
352
+ if (live === seenLiveRef.current)
353
+ return;
354
+ seenLiveRef.current = live;
355
+ if (live.folderUids === null || folders.some((folder) => live.folderUids.has(folder.uid))) {
356
+ refreshThread();
357
+ }
358
+ }, [live]);
359
+ // The server has relayed a message this tab sent: its Sent Items copy is read for at once, without waiting for the live update that follows.
360
+ const sentKey = outgoing
361
+ .filter((reply) => reply.state === "sent" && reply.mailboxUid === mailboxUid)
362
+ .map((reply) => reply.uid)
363
+ .join(",");
364
+ useEffect(() => {
365
+ if (sentKey) {
366
+ refreshThread();
367
+ }
368
+ }, [sentKey]);
369
+ // The messages sent from here that continue this thread and whose real copy it does not hold yet, newest first (the pane's order). A message
370
+ // that replies to nothing here - a new message, or a reply to another conversation - is not in this list, and never is.
371
+ const pendingCards = outgoing
372
+ .filter((reply) => reply.mailboxUid === mailboxUid && !messages.some((message) => message.uid === reply.uid) && belongsToThread(reply, messages))
373
+ .reverse();
374
+ // A message that has just been sent is scrolled into view and takes the focus - the compose window it was sent from has just closed, and the focus
375
+ // with it - once. A failed one, or one that was already there when the thread was opened, is left where it is.
376
+ const pendingKey = pendingCards.map((reply) => reply.uid).join(",");
377
+ useEffect(() => {
378
+ const fresh = pendingCards.filter((reply) => reply.state === "sending" && !announcedRef.current.has(reply.uid));
379
+ if (fresh.length === 0)
380
+ return;
381
+ for (const reply of fresh) {
382
+ announcedRef.current.add(reply.uid);
383
+ }
384
+ setPendingFocusUid(fresh[0].uid);
385
+ }, [pendingKey]);
286
386
  /** A newer copy of one of the thread's messages, kept here and handed to the list - with the copy it
287
387
  * replaces where the caller was given one, so a conversation row can tell what actually changed. */
288
388
  function patchMessage(updated, previous) {
@@ -291,6 +391,7 @@ export default function ConversationThreadPane({ conversation, mailboxUid, selec
291
391
  }
292
392
  /** A message that left the folder being listed - it leaves the thread too, as it left the list. */
293
393
  function removeMessage(updated) {
394
+ removedRef.current.add(updated.uid);
294
395
  setMessages((prev) => prev.filter((message) => message.uid !== updated.uid));
295
396
  onMessageRemoved(updated);
296
397
  }
@@ -329,26 +430,30 @@ export default function ConversationThreadPane({ conversation, mailboxUid, selec
329
430
  : undefined
330
431
  : error
331
432
  ? undefined
332
- : `${messages.length} message${messages.length === 1 ? "" : "s"}`, children: truncated && !loading && !error && (_jsxs("p", { className: "text-xs text-text-muted mt-1", children: ["Only the oldest ", THREAD_MESSAGE_LIMIT, " messages of this conversation are shown here. The rest are still in the message list."] })) }), loading ? (_jsx(SkeletonCards, { messageCount: conversation.messageCount })) : error ? (_jsx("div", { className: "p-2 sm:p-3", children: _jsx(Alert, { children: error }) })) : (_jsx("ul", { className: "flex-1 min-h-0 overflow-y-auto p-2 sm:p-3 flex flex-col gap-3", children: messages.map((message) => {
333
- const uid = message.uid;
334
- const expanded = expandedUids.has(uid);
335
- const bodyId = `thread-message-${uid}`;
336
- const messageUnread = isUnread(message);
337
- return (_jsx("li", { ref: (node) => {
338
- rowRefs.current[uid] = node;
339
- }, "data-unread": messageUnread ? "true" : undefined, children: expanded ? (_jsx(MessageDetailPane, { inThread: true, threadSubject: subject, threadHeader: {
340
- bodyId,
341
- unread: messageUnread,
342
- onToggle: () => toggleExpanded(uid),
343
- buttonRef: (node) => {
344
- headerRefs.current[uid] = node;
345
- },
346
- }, footer: uid === footerUid, shortcuts: !!shortcuts && uid === keyboardUid, message: message, attachments: attachmentsByUid[uid] ?? [], isSentItems: folderTypeOf(message) === "sent_items", isOutbox: folderTypeOf(message) === "outbox", draftsFolderUid: folders.find((folder) => folder.type === "drafts")?.uid, folders: folders, onMoved: removeMessage, onFolderCreated: onFolderCreated, onRecalled: patchMessage, onReceiptHandled: patchMessage, onScheduledSendCanceled: removeMessage, onArchived: removeMessage, labels: labels, onLabelsChanged: patchMessage, onLabelCreated: onLabelCreated })) : (_jsxs(_Fragment, { children: [_jsx(CollapsedCard, { from: message.from, date: new Date(message.receivedDate).toLocaleString(), preview: message.bodyPreview || (message.encrypted ? _jsx(EncryptedPreview, {}) : ""), unread: messageUnread, senderClassName: senderClass(messageUnread), dateClassName: dateClass(messageUnread), buttonRef: (node) => {
433
+ : `${messages.length + pendingCards.length} message${messages.length + pendingCards.length === 1 ? "" : "s"}`, children: truncated && !loading && !error && (_jsxs("p", { className: "text-xs text-text-muted mt-1", children: ["Only the oldest ", THREAD_MESSAGE_LIMIT, " messages of this conversation are shown here. The rest are still in the message list."] })) }), loading ? (_jsx(SkeletonCards, { messageCount: conversation.messageCount })) : error ? (_jsx("div", { className: "p-2 sm:p-3", children: _jsx(Alert, { children: error }) })) : (_jsxs("ul", { className: "flex-1 min-h-0 overflow-y-auto p-2 sm:p-3 flex flex-col gap-3", children: [pendingCards.map((reply) => (_jsx("li", { ref: (node) => {
434
+ rowRefs.current[reply.uid] = node;
435
+ }, "data-outgoing": "true", children: _jsx(PendingMessageCard, { reply: reply, headerRef: (node) => {
436
+ headerRefs.current[reply.uid] = node;
437
+ } }) }, reply.uid))), messages.map((message) => {
438
+ const uid = message.uid;
439
+ const expanded = expandedUids.has(uid);
440
+ const bodyId = `thread-message-${uid}`;
441
+ const messageUnread = isUnread(message);
442
+ return (_jsx("li", { ref: (node) => {
443
+ rowRefs.current[uid] = node;
444
+ }, "data-unread": messageUnread ? "true" : undefined, children: expanded ? (_jsx(MessageDetailPane, { inThread: true, threadSubject: subject, threadHeader: {
445
+ bodyId,
446
+ unread: messageUnread,
447
+ onToggle: () => toggleExpanded(uid),
448
+ buttonRef: (node) => {
347
449
  headerRefs.current[uid] = node;
348
- }, buttonProps: {
349
- onClick: () => toggleExpanded(uid),
350
- "aria-expanded": false,
351
- "aria-controls": bodyId,
352
- } }), _jsx("div", { id: bodyId, hidden: true })] })) }, uid));
353
- }) }))] }));
450
+ },
451
+ }, footer: uid === footerUid, shortcuts: !!shortcuts && uid === keyboardUid, message: message, attachments: attachmentsByUid[uid] ?? [], isSentItems: folderTypeOf(message) === "sent_items", isOutbox: folderTypeOf(message) === "outbox", draftsFolderUid: folders.find((folder) => folder.type === "drafts")?.uid, folders: folders, onMoved: removeMessage, onFolderCreated: onFolderCreated, onRecalled: patchMessage, onReceiptHandled: patchMessage, onScheduledSendCanceled: removeMessage, onArchived: removeMessage, onChanged: patchMessage, labels: labels, onLabelsChanged: patchMessage, onLabelCreated: onLabelCreated })) : (_jsxs(_Fragment, { children: [_jsx(CollapsedCard, { from: message.from, date: new Date(message.receivedDate).toLocaleString(), preview: message.bodyPreview || (message.encrypted ? _jsx(EncryptedPreview, {}) : ""), unread: messageUnread, senderClassName: senderClass(messageUnread), dateClassName: dateClass(messageUnread), buttonRef: (node) => {
452
+ headerRefs.current[uid] = node;
453
+ }, buttonProps: {
454
+ onClick: () => toggleExpanded(uid),
455
+ "aria-expanded": false,
456
+ "aria-controls": bodyId,
457
+ } }), _jsx("div", { id: bodyId, hidden: true })] })) }, uid));
458
+ })] }))] }));
354
459
  }
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+ export interface EmptyFolderBarProps {
3
+ /** The folder the list is showing ("Deleted Items", "Junk Email"). */
4
+ folderName: string;
5
+ /** How many messages it holds, when that is known. */
6
+ count?: number;
7
+ /** Nothing to empty, or not allowed to. */
8
+ disabled: boolean;
9
+ /** Why, shown as the button's tooltip while it is disabled. */
10
+ disabledReason?: string;
11
+ onEmpty: () => void;
12
+ }
13
+ /**
14
+ * Outlook's "Empty folder", at the top of the list of Deleted Items and of Junk Email: the whole folder is permanently deleted, after the
15
+ * page has asked (this only reports the click). A row of its own under the toolbar rather than a toolbar button, so the toolbar keeps the
16
+ * width a phone gives it and the action is never taken for one of the list's arrangement controls.
17
+ */
18
+ export default function EmptyFolderBar({ folderName, count, disabled, disabledReason, onEmpty }: EmptyFolderBarProps): React.JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { HiOutlineTrash } from "react-icons/hi2";
3
+ /**
4
+ * Outlook's "Empty folder", at the top of the list of Deleted Items and of Junk Email: the whole folder is permanently deleted, after the
5
+ * page has asked (this only reports the click). A row of its own under the toolbar rather than a toolbar button, so the toolbar keeps the
6
+ * width a phone gives it and the action is never taken for one of the list's arrangement controls.
7
+ */
8
+ export default function EmptyFolderBar({ folderName, count, disabled, disabledReason, onEmpty }) {
9
+ return (_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 py-1 border-b border-border text-xs text-text-muted", children: [_jsx("span", { children: count === undefined ? "" : `${count} ${count === 1 ? "item" : "items"}` }), _jsxs("button", { type: "button", onClick: onEmpty, disabled: disabled, title: disabled ? disabledReason : `Permanently delete everything in ${folderName}`, className: "inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-sm text-danger hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent", children: [_jsx(HiOutlineTrash, { size: 16, "aria-hidden": "true", className: "shrink-0" }), `Empty ${folderName}`] })] }));
10
+ }
@@ -40,22 +40,32 @@ export interface MailSelectionBarProps {
40
40
  * prompt, which would be claiming the move simply didn't happen. */
41
41
  onMoveTo: (folderUid: string) => Promise<void>;
42
42
  onReportJunk: () => void;
43
+ /** Delete: moves the selection to Deleted Items - or, when `deletesPermanently`, permanently deletes it (the page asks first). */
43
44
  onDelete: () => void;
45
+ /** The selection is in Deleted Items, so Delete is the permanent one: the button says "Delete permanently" and is styled as the destructive
46
+ * action it is. The page decides (a search over several mailboxes lists messages from more than one folder, so the folder being viewed is not
47
+ * always the answer). */
48
+ deletesPermanently?: boolean;
44
49
  /** A bulk action is in flight - every action is held until it settles, since the next one would send
45
50
  * `version`s the first has already superseded. */
46
51
  busy: boolean;
47
52
  /** The keyboard acts on this selection (Delete, Ctrl+Q, Ctrl+U, Insert - registered by the page): Mark read, Mark unread, Flag and
48
53
  * Delete name their shortcut in the tooltip and `aria-keyshortcuts`. */
49
54
  shortcuts?: boolean;
55
+ /** Why Move to is unavailable, when the selection is spread over mailboxes and a folder can only belong to one (search results over several
56
+ * mailboxes). Shown as the button's tooltip. */
57
+ moveDisabledReason?: string;
58
+ /** The same for Apply label: a label belongs to one mailbox, and `labels` are one mailbox's. */
59
+ labelsDisabledReason?: string;
50
60
  }
51
61
  /**
52
62
  * The header that replaces the list's own toolbar while select mode is on: how many rows are selected,
53
63
  * select-all/clear, and the bulk actions Outlook offers for a multi-selection - mark read/unread, flag,
54
64
  * archive, move, report junk and delete.
55
65
  *
56
- * "Delete" means *move to Deleted Items*, never a permanent erase: there is no bulk permanent-delete API,
57
- * and the collection-level DELETE truncates the folder rather than removing a selection. An action whose
58
- * target folder is the one already being viewed is disabled with a reason rather than hidden, so the set
66
+ * "Delete" means *move to Deleted Items* - except on messages that are already in Deleted Items (`deletesPermanently`),
67
+ * where it is "Delete permanently": the page confirms, then erases them for good, as Outlook's Delete does there. An
68
+ * action whose target folder is the one already being viewed is disabled with a reason rather than hidden, so the set
59
69
  * of actions doesn't shift around between folders.
60
70
  */
61
- export default function MailSelectionBar({ selected, listed, totals, onSelectAll, onClearSelection, onCancel, folders, currentFolderUid, labels, mailboxUid, onLabelCreated, onFolderCreated, onApplyLabels, onSetRead, onSetFlagged, onArchive, onMoveTo, onReportJunk, onDelete, busy, shortcuts, }: MailSelectionBarProps): React.JSX.Element;
71
+ export default function MailSelectionBar({ selected, listed, totals, onSelectAll, onClearSelection, onCancel, folders, currentFolderUid, labels, mailboxUid, onLabelCreated, onFolderCreated, onApplyLabels, onSetRead, onSetFlagged, onArchive, onMoveTo, onReportJunk, onDelete, deletesPermanently, busy, shortcuts, moveDisabledReason, labelsDisabledReason, }: MailSelectionBarProps): React.JSX.Element;
@@ -9,25 +9,23 @@ import MoveToFolderDialog, { MOVE_TARGET_TYPES } from "./MoveToFolderDialog.js";
9
9
  import { ariaKeyShortcuts, withHint } from "../../keyboard/format.js";
10
10
  import { SHORTCUTS } from "../../keyboard/keymap.js";
11
11
  import { useKeyEnvironment } from "../../keyboard/ShortcutProvider.js";
12
- function actionClassName() {
13
- return "px-2 py-1 rounded-md text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent";
12
+ function actionClassName(destructive = false) {
13
+ return `px-2 py-1 rounded-md text-sm ${destructive ? "text-danger font-semibold" : "text-text"} hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent`;
14
14
  }
15
15
  /**
16
16
  * The header that replaces the list's own toolbar while select mode is on: how many rows are selected,
17
17
  * select-all/clear, and the bulk actions Outlook offers for a multi-selection - mark read/unread, flag,
18
18
  * archive, move, report junk and delete.
19
19
  *
20
- * "Delete" means *move to Deleted Items*, never a permanent erase: there is no bulk permanent-delete API,
21
- * and the collection-level DELETE truncates the folder rather than removing a selection. An action whose
22
- * target folder is the one already being viewed is disabled with a reason rather than hidden, so the set
20
+ * "Delete" means *move to Deleted Items* - except on messages that are already in Deleted Items (`deletesPermanently`),
21
+ * where it is "Delete permanently": the page confirms, then erases them for good, as Outlook's Delete does there. An
22
+ * action whose target folder is the one already being viewed is disabled with a reason rather than hidden, so the set
23
23
  * of actions doesn't shift around between folders.
24
24
  */
25
- export default function MailSelectionBar({ selected, listed, totals, onSelectAll, onClearSelection, onCancel, folders, currentFolderUid, labels, mailboxUid, onLabelCreated, onFolderCreated, onApplyLabels, onSetRead, onSetFlagged, onArchive, onMoveTo, onReportJunk, onDelete, busy, shortcuts, }) {
25
+ export default function MailSelectionBar({ selected, listed, totals, onSelectAll, onClearSelection, onCancel, folders, currentFolderUid, labels, mailboxUid, onLabelCreated, onFolderCreated, onApplyLabels, onSetRead, onSetFlagged, onArchive, onMoveTo, onReportJunk, onDelete, deletesPermanently = false, busy, shortcuts, moveDisabledReason, labelsDisabledReason, }) {
26
26
  const env = useKeyEnvironment();
27
- /** `title` and `aria-keyshortcuts` for an action the keyboard also does; `reason` (why it is disabled) wins the tooltip. */
28
- const hint = (label, shortcut, reason) => shortcuts
29
- ? { title: reason ?? withHint(label, shortcut, env), "aria-keyshortcuts": ariaKeyShortcuts(shortcut, env) }
30
- : { title: reason };
27
+ /** `title` and `aria-keyshortcuts` for an action the keyboard also does. */
28
+ const hint = (label, shortcut) => shortcuts ? { title: withHint(label, shortcut, env), "aria-keyshortcuts": ariaKeyShortcuts(shortcut, env) } : {};
31
29
  const selectedCount = totals?.selected ?? selected.length;
32
30
  const listedCount = totals?.listed ?? listed.length;
33
31
  // Counted on the rows that were ticked, but *emptied* on the messages: a ticked conversation whose
@@ -44,12 +42,12 @@ export default function MailSelectionBar({ selected, listed, totals, onSelectAll
44
42
  // them is already being in the folder they would move to.
45
43
  const archiveReason = currentType === "archive" ? "These messages are already in Archive" : undefined;
46
44
  const junkReason = currentType === "junk" ? "These messages are already in Junk" : undefined;
47
- const deleteReason = currentType === "deleted_items" ? "These messages are already in Deleted Items" : undefined;
45
+ const deleteLabel = deletesPermanently ? "Delete permanently" : "Delete";
48
46
  // A label every selected message already carries starts ticked; one only some of them carry starts
49
47
  // partially applied, and is left exactly as it is unless the reader touches that row.
50
48
  const appliedToAll = labels.filter((label) => !none && selected.every((m) => m.labelUids?.includes(label.uid)));
51
49
  const appliedToSome = labels.filter((label) => selected.some((m) => m.labelUids?.includes(label.uid)) && !appliedToAll.includes(label));
52
- return (_jsxs("div", { className: "border-b border-border", children: [_jsx(MoveToFolderDialog, { open: movePrompt, onClose: () => setMovePrompt(false), mailboxUid: mailboxUid, folders: folders, currentFolderUid: currentFolderUid, count: selected.length, onMove: onMoveTo, onFolderCreated: onFolderCreated }), _jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 bg-surface-alt", children: [_jsxs("span", { "aria-live": "polite", className: "text-sm font-semibold text-text", children: [selectedCount, totals ? ` ${totals.noun}${selectedCount === 1 ? "" : "s"}` : "", " selected"] }), _jsx("button", { type: "button", onClick: onSelectAll, disabled: allSelected || listedCount === 0, className: actionClassName(), children: "Select all" }), _jsx("button", { type: "button", onClick: onClearSelection, disabled: selectedCount === 0, className: actionClassName(), children: "Clear" }), _jsx("button", { type: "button", onClick: onCancel, className: `${actionClassName()} ml-auto`, children: "Cancel" })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-0.5 px-1.5 py-1", children: [_jsx("button", { type: "button", onClick: () => onSetRead(true), disabled: none || busy, className: actionClassName(), ...hint("Mark read", SHORTCUTS.mail.markRead), children: "Mark read" }), _jsx("button", { type: "button", onClick: () => onSetRead(false), disabled: none || busy, className: actionClassName(), ...hint("Mark unread", SHORTCUTS.mail.markUnread), children: "Mark unread" }), _jsx("button", { type: "button", onClick: () => onSetFlagged(true), disabled: none || busy, className: actionClassName(), ...hint("Flag", SHORTCUTS.mail.flag), children: "Flag" }), _jsx("button", { type: "button", onClick: () => onSetFlagged(false), disabled: none || busy, className: actionClassName(), children: "Unflag" }), _jsx("button", { type: "button", onClick: onArchive, disabled: none || busy || !!archiveReason, title: archiveReason, className: actionClassName(), children: "Archive" }), _jsx(LabelMenuButton, { "aria-label": "Apply label", label: "Apply label", className: "text-sm", labels: labels, mailboxUid: mailboxUid, onLabelCreated: onLabelCreated, applied: appliedToAll.map((label) => label.uid), partial: appliedToSome.map((label) => label.uid), onCommit: onApplyLabels, busy: busy, disabled: none || busy, note: selected.length === 1
50
+ return (_jsxs("div", { className: "border-b border-border", children: [_jsx(MoveToFolderDialog, { open: movePrompt, onClose: () => setMovePrompt(false), mailboxUid: mailboxUid, folders: folders, currentFolderUid: currentFolderUid, count: selected.length, onMove: onMoveTo, onFolderCreated: onFolderCreated }), _jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 bg-surface-alt", children: [_jsxs("span", { "aria-live": "polite", className: "text-sm font-semibold text-text", children: [selectedCount, totals ? ` ${totals.noun}${selectedCount === 1 ? "" : "s"}` : "", " selected"] }), _jsx("button", { type: "button", onClick: onSelectAll, disabled: allSelected || listedCount === 0, className: actionClassName(), children: "Select all" }), _jsx("button", { type: "button", onClick: onClearSelection, disabled: selectedCount === 0, className: actionClassName(), children: "Clear" }), _jsx("button", { type: "button", onClick: onCancel, className: `${actionClassName()} ml-auto`, children: "Cancel" })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-0.5 px-1.5 py-1", children: [_jsx("button", { type: "button", onClick: () => onSetRead(true), disabled: none || busy, className: actionClassName(), ...hint("Mark read", SHORTCUTS.mail.markRead), children: "Mark read" }), _jsx("button", { type: "button", onClick: () => onSetRead(false), disabled: none || busy, className: actionClassName(), ...hint("Mark unread", SHORTCUTS.mail.markUnread), children: "Mark unread" }), _jsx("button", { type: "button", onClick: () => onSetFlagged(true), disabled: none || busy, className: actionClassName(), ...hint("Flag", SHORTCUTS.mail.flag), children: "Flag" }), _jsx("button", { type: "button", onClick: () => onSetFlagged(false), disabled: none || busy, className: actionClassName(), children: "Unflag" }), _jsx("button", { type: "button", onClick: onArchive, disabled: none || busy || !!archiveReason, title: archiveReason, className: actionClassName(), children: "Archive" }), _jsx(LabelMenuButton, { "aria-label": "Apply label", label: "Apply label", className: "text-sm", labels: labels, mailboxUid: mailboxUid, onLabelCreated: onLabelCreated, applied: appliedToAll.map((label) => label.uid), partial: appliedToSome.map((label) => label.uid), onCommit: onApplyLabels, busy: busy, disabled: none || busy || !!labelsDisabledReason, title: labelsDisabledReason, note: selected.length === 1
53
51
  ? "Ticked labels are applied, unticked ones removed."
54
- : "Ticked labels are applied to every selected message and unticked ones removed from all of them; a dash means only some have that label, and leaving it alone keeps it that way.", emptyNote: "This mailbox has no labels yet.", commit: { label: "Apply" }, clear: { label: "Remove all labels" } }), _jsx("button", { type: "button", onClick: () => setMovePrompt(true), disabled: none || busy, className: actionClassName(), children: "Move to" }), _jsx("button", { type: "button", onClick: onReportJunk, disabled: none || busy || !!junkReason, title: junkReason, className: actionClassName(), children: "Report junk" }), _jsx("button", { type: "button", onClick: onDelete, disabled: none || busy || !!deleteReason, className: actionClassName(), ...hint("Delete", SHORTCUTS.mail.delete, deleteReason), children: "Delete" })] })] }));
52
+ : "Ticked labels are applied to every selected message and unticked ones removed from all of them; a dash means only some have that label, and leaving it alone keeps it that way.", emptyNote: "This mailbox has no labels yet.", commit: { label: "Apply" }, clear: { label: "Remove all labels" } }), _jsx("button", { type: "button", onClick: () => setMovePrompt(true), disabled: none || busy || !!moveDisabledReason, title: moveDisabledReason, className: actionClassName(), children: "Move to" }), _jsx("button", { type: "button", onClick: onReportJunk, disabled: none || busy || !!junkReason, title: junkReason, className: actionClassName(), children: "Report junk" }), _jsx("button", { type: "button", onClick: onDelete, disabled: none || busy, className: actionClassName(deletesPermanently), ...hint(deleteLabel, SHORTCUTS.mail.delete), children: deleteLabel })] })] }));
55
53
  }
@@ -12,6 +12,10 @@ interface MenuItemBase {
12
12
  checked?: boolean | "mixed";
13
13
  /** A colour swatch before the label - a label's own colour. */
14
14
  swatchColor?: string;
15
+ /** An icon before the label, for a menu whose rows are commands (the reading pane's "More actions"). */
16
+ icon?: ReactNode;
17
+ /** A tooltip for the row - the full text of a label the row truncates. */
18
+ title?: string;
15
19
  disabled?: boolean;
16
20
  /** Leaves the menu open after choosing this row, for a multi-select list where several rows are ticked
17
21
  * before one command commits them all. */
@@ -40,8 +44,11 @@ export interface MenuSectionSpec {
40
44
  /** The height `PopoverPortal` is asked for - exact for a short menu, capped (the list scrolls) for a long one. */
41
45
  export declare function menuHeight(sections: MenuSectionSpec[], width?: number): number;
42
46
  export interface MenuButtonProps {
43
- /** The trigger's visible label. */
47
+ /** The trigger's visible label. Not drawn (nor is the chevron) with `iconOnly`, where `aria-label` is all the trigger says. */
44
48
  label: ReactNode;
49
+ /** The trigger is just its `icon` - a round icon button like the reading pane's own command row, whose `className` it takes over
50
+ * completely. `aria-label` (and `title`) name it. */
51
+ iconOnly?: boolean;
45
52
  /** The trigger's accessible name, which also names the menu itself - includes the current selection
46
53
  * where there is one ("Filter: Unread"), so the button says what it's set to, not just what it does. */
47
54
  "aria-label": string;
@@ -67,5 +74,5 @@ export interface MenuButtonProps {
67
74
  * comment). The portal supplies the positioning, the outside-click dismissal and the box; the menu
68
75
  * semantics, roving focus and keyboard handling are this component's own.
69
76
  */
70
- export default function MenuButton({ label, icon, disabled, title, sections, width, className, "aria-label": ariaLabel, onOpenChange, }: MenuButtonProps): React.JSX.Element;
77
+ export default function MenuButton({ label, iconOnly, icon, disabled, title, sections, width, className, "aria-label": ariaLabel, onOpenChange, }: MenuButtonProps): React.JSX.Element;
71
78
  export {};
@@ -61,7 +61,7 @@ export function menuHeight(sections, width = DEFAULT_MENU_WIDTH) {
61
61
  * comment). The portal supplies the positioning, the outside-click dismissal and the box; the menu
62
62
  * semantics, roving focus and keyboard handling are this component's own.
63
63
  */
64
- export default function MenuButton({ label, icon, disabled, title, sections, width = DEFAULT_MENU_WIDTH, className = "", "aria-label": ariaLabel, onOpenChange, }) {
64
+ export default function MenuButton({ label, iconOnly = false, icon, disabled, title, sections, width = DEFAULT_MENU_WIDTH, className = "", "aria-label": ariaLabel, onOpenChange, }) {
65
65
  const [open, setOpen] = useState(false);
66
66
  const [activeIndex, setActiveIndex] = useState(0);
67
67
  // The key of the item whose submenu is showing, or `null` at the top level. Held as a key, not as the
@@ -111,8 +111,10 @@ export default function MenuButton({ label, icon, disabled, title, sections, wid
111
111
  }
112
112
  function enterSubmenu(item) {
113
113
  setSubmenuKey(item.key);
114
- // Past the Back row, onto the first row of the submenu itself.
115
- setActiveIndex(1 + initialActiveIndex(item.submenu.flatMap((section) => section.items)));
114
+ const rows = item.submenu.flatMap((section) => section.items);
115
+ // Past the Back row, onto the first row of the submenu itself - or, when none of them can take focus (every row is unavailable right now), onto
116
+ // the Back row, so the keyboard still has a place to be.
117
+ setActiveIndex(rows.some((row) => !row.disabled) ? 1 + initialActiveIndex(rows) : 0);
116
118
  }
117
119
  function leaveSubmenu() {
118
120
  // Back on the row the submenu was opened from.
@@ -193,19 +195,21 @@ export default function MenuButton({ label, icon, disabled, title, sections, wid
193
195
  }
194
196
  }
195
197
  let itemIndex = -1;
196
- return (_jsxs(_Fragment, { children: [_jsxs("button", { ref: triggerRef, type: "button", "aria-haspopup": "menu", "aria-expanded": open, "aria-label": ariaLabel, title: title, disabled: disabled, onClick: () => (open ? closeMenu(false) : openMenu()), onKeyDown: handleTriggerKeyDown, className: [
197
- // `min-w-0` so a long label ("Filter: Has attachments") truncates instead of pushing
198
- // whatever sits beside it in the toolbar off the edge of a 384px list column.
199
- "inline-flex min-w-0 items-center gap-1 px-2 py-1 rounded-md text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent",
200
- className,
201
- ].join(" "), children: [icon, _jsx("span", { className: "truncate", children: label }), _jsx(HiChevronDown, { size: 14, "aria-hidden": "true", className: "shrink-0 text-text-muted" })] }), open && (_jsx(PopoverPortal, { anchorRef: triggerRef, onClose: () => closeMenu(false), width: width, height: menuHeight(shownSections, width), "aria-label": ariaLabel, children: _jsx("div", { ref: setMenuNode, role: "menu", tabIndex: -1, "aria-label": ariaLabel, onKeyDown: handleMenuKeyDown, className: "flex-1 overflow-y-auto py-1", children: shownSections.map((section, sectionIndex) => (_jsxs("div", { role: "group", "aria-label": section.label, className: sectionIndex > 0 ? "border-t border-border mt-1 pt-1" : "", children: [section.label && (_jsx("div", { "aria-hidden": "true", className: "h-6 flex items-center px-3 text-xs font-semibold uppercase tracking-wide text-text-muted", children: section.label })), section.items.map((item) => {
198
+ return (_jsxs(_Fragment, { children: [_jsxs("button", { ref: triggerRef, type: "button", "aria-haspopup": "menu", "aria-expanded": open, "aria-label": ariaLabel, title: title, disabled: disabled, onClick: () => (open ? closeMenu(false) : openMenu()), onKeyDown: handleTriggerKeyDown, className: iconOnly
199
+ ? className
200
+ : [
201
+ // `min-w-0` so a long label ("Filter: Has attachments") truncates instead of pushing
202
+ // whatever sits beside it in the toolbar off the edge of a 384px list column.
203
+ "inline-flex min-w-0 items-center gap-1 px-2 py-1 rounded-md text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent",
204
+ className,
205
+ ].join(" "), children: [icon, !iconOnly && _jsx("span", { className: "truncate", children: label }), !iconOnly && _jsx(HiChevronDown, { size: 14, "aria-hidden": "true", className: "shrink-0 text-text-muted" })] }), open && (_jsx(PopoverPortal, { anchorRef: triggerRef, onClose: () => closeMenu(false), width: width, height: menuHeight(shownSections, width), "aria-label": ariaLabel, children: _jsx("div", { ref: setMenuNode, role: "menu", tabIndex: -1, "aria-label": ariaLabel, onKeyDown: handleMenuKeyDown, className: "flex-1 overflow-y-auto py-1", children: shownSections.map((section, sectionIndex) => (_jsxs("div", { role: "group", "aria-label": section.label, className: sectionIndex > 0 ? "border-t border-border mt-1 pt-1" : "", children: [section.label && (_jsx("div", { "aria-hidden": "true", className: "h-6 flex items-center px-3 text-xs font-semibold uppercase tracking-wide text-text-muted", children: section.label })), section.items.map((item) => {
202
206
  itemIndex += 1;
203
207
  const index = itemIndex;
204
208
  const role = item.role ?? "menuitem";
205
209
  return (_jsxs("button", { ref: (node) => {
206
210
  itemRefs.current[index] = node;
207
- }, type: "button", role: role, disabled: item.disabled, tabIndex: index === activeIndex ? 0 : -1, ...(role === "menuitem"
211
+ }, type: "button", role: role, disabled: item.disabled, title: item.title, tabIndex: index === activeIndex ? 0 : -1, ...(role === "menuitem"
208
212
  ? {}
209
- : { "aria-checked": item.checked === "mixed" ? "mixed" : !!item.checked }), ...(item.submenu ? { "aria-haspopup": "menu", "aria-expanded": false } : {}), onClick: () => selectItem(item), className: "w-full flex items-start gap-2 px-3 py-2 text-left text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent", children: [_jsxs("span", { className: "w-4 shrink-0 flex justify-center pt-0.5", children: [item.key === "__back" && (_jsx(HiChevronLeft, { size: 14, "aria-hidden": "true", className: "text-text-muted" })), item.checked === "mixed" ? (_jsx(HiMinus, { size: 14, "aria-hidden": "true", className: "text-primary-dark" })) : (item.checked && _jsx(HiCheck, { size: 14, "aria-hidden": "true", className: "text-primary-dark" }))] }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "flex h-5 items-center gap-1.5", children: [item.swatchColor && (_jsx("span", { "aria-hidden": "true", className: "w-2.5 h-2.5 rounded-full shrink-0", style: { backgroundColor: item.swatchColor } })), _jsx("span", { className: "truncate", children: item.label })] }), item.description && (_jsx("span", { className: "block h-4 text-xs leading-4 text-text-muted truncate font-normal", children: item.description }))] }), item.submenu && (_jsx(HiChevronRight, { size: 14, "aria-hidden": "true", className: "shrink-0 mt-0.5 text-text-muted" }))] }, item.key));
213
+ : { "aria-checked": item.checked === "mixed" ? "mixed" : !!item.checked }), ...(item.submenu ? { "aria-haspopup": "menu", "aria-expanded": false } : {}), onClick: () => selectItem(item), className: "w-full flex items-start gap-2 px-3 py-2 text-left text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent", children: [_jsxs("span", { className: "w-4 shrink-0 flex justify-center pt-0.5", children: [item.key === "__back" && (_jsx(HiChevronLeft, { size: 14, "aria-hidden": "true", className: "text-text-muted" })), item.checked === "mixed" ? (_jsx(HiMinus, { size: 14, "aria-hidden": "true", className: "text-primary-dark" })) : (item.checked && _jsx(HiCheck, { size: 14, "aria-hidden": "true", className: "text-primary-dark" }))] }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "flex h-5 items-center gap-1.5", children: [item.icon && (_jsx("span", { "aria-hidden": "true", className: "shrink-0 text-text-muted", children: item.icon })), item.swatchColor && (_jsx("span", { "aria-hidden": "true", className: "w-2.5 h-2.5 rounded-full shrink-0", style: { backgroundColor: item.swatchColor } })), _jsx("span", { className: "truncate", children: item.label })] }), item.description && (_jsx("span", { className: "block h-4 text-xs leading-4 text-text-muted truncate font-normal", children: item.description }))] }), item.submenu && (_jsx(HiChevronRight, { size: 14, "aria-hidden": "true", className: "shrink-0 mt-0.5 text-text-muted" }))] }, item.key));
210
214
  }), section.note && _jsx("p", { className: "px-3 py-1 text-xs leading-4 text-text-muted", children: section.note })] }, section.key))) }) }))] }));
211
215
  }
@@ -72,6 +72,11 @@ export interface MessageDetailPaneProps {
72
72
  * server-side 400 guard); Drafts is detected via `draftsFolderUid` (already passed by every caller
73
73
  * for the Outbox-cancel flow) rather than a new prop. */
74
74
  onArchived?: (updated: Message) => void;
75
+ /** Called with the server's updated copy (and the copy it replaces) after the card's More actions menu marked the message read or unread, or
76
+ * flagged or unflagged it - it stays where it is, so a caller patches it in place, as it does for `onLabelsChanged`. The menu's Delete, Report
77
+ * junk and Block move the message out of its folder and are reported through `onMoved`, which is what makes a thread's card leave it and the
78
+ * pane advance, exactly as after Archive. */
79
+ onChanged?: (updated: Message, previous?: Message) => void;
75
80
  /** Every label defined in this message's mailbox, for the label-assignment popover below — each
76
81
  * caller fetches its own mailbox's labels the same way it already resolves `draftsFolderUid`
77
82
  * (`listLabels()`). Absent/empty simply hides the Labels control - there's nothing to assign. */