@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
@@ -5,6 +5,7 @@
5
5
  import { format } from "date-fns";
6
6
  import { fromEventWallClock, toEventWallClock } from "@rapidmx/react-shared/calendar/recurrence.js";
7
7
  import { toDatetimeLocal } from "@rapidmx/react-shared/util/dateInput.js";
8
+ import { formatRecipient, isValidRecipientAddress, parseRecipient, splitRecipientList } from "../mail/compose/recipients.js";
8
9
  import { addDaysToKey, allDayDateKey } from "./allDay.js";
9
10
  /** What the event form and the read-only event view both need to say about a busy status / an answer. */
10
11
  export const BUSY_STATUSES = ["busy", "free", "tentative", "oof"];
@@ -70,17 +71,52 @@ export function describeReminder(minutes) {
70
71
  const amount = minutes / unit.factor;
71
72
  return `Notify ${amount} ${amount === 1 ? unit.singular : unit.unit} before`;
72
73
  }
73
- /** Splits what was typed into the guests field into its addresses: commas, semicolons and white space separate them. */
74
- export function splitGuestInput(text) {
75
- return text.split(/[,;\s]+/).filter(Boolean);
74
+ /** An attendee as the guests field adds one. */
75
+ export function newGuest(address, displayName) {
76
+ return { address, ...(displayName ? { displayName } : {}), role: "required", responseStatus: "needsAction", isOrganizer: false };
76
77
  }
77
- /** The loosest address check that is still worth making before an invitation is sent: something, an @, something. */
78
- export function isValidGuestAddress(address) {
79
- return /^[^\s@<>,;]+@[^\s@<>,;]+$/.test(address);
78
+ /** A guest as the guests field shows it: `Name <address>`, or the bare address for one without a name (the form compose's recipient fields use). */
79
+ export function guestChip(guest) {
80
+ return formatRecipient({ address: guest.address, displayName: guest.displayName });
80
81
  }
81
- /** An attendee as the guests field adds one. */
82
- export function newGuest(address) {
83
- return { address, role: "required", responseStatus: "needsAction", isOrganizer: false };
82
+ /**
83
+ * The guests the guests field's chips name, given the guests `current`ly on the list: a chip for someone already there keeps that guest as
84
+ * they are (their role and answer, whatever the case of the address), a chip for a new address adds a required guest with the chip's name, and
85
+ * an address twice (or one of `skipAddresses` - the organizer's, or the people already invited) is added once. The chips are the field's whole
86
+ * list, so a guest without a chip is removed. What is not an address comes back as `invalid`, to stay in the field flagged; several addresses
87
+ * separated by white space in one chip ("a@x.com b@x.com", as a paste can be) are taken as several.
88
+ */
89
+ export function applyGuestChips(current, chips, skipAddresses = []) {
90
+ const skip = new Set(skipAddresses.map((address) => address.toLowerCase()));
91
+ const attendees = [];
92
+ const invalid = [];
93
+ const known = (address) => current.find((guest) => guest.address.toLowerCase() === address.toLowerCase());
94
+ const listed = (address) => attendees.some((guest) => guest.address.toLowerCase() === address.toLowerCase());
95
+ function add(address, displayName) {
96
+ const existing = known(address);
97
+ if (existing) {
98
+ if (!listed(address)) {
99
+ attendees.push(existing);
100
+ }
101
+ }
102
+ else if (!skip.has(address.toLowerCase()) && !listed(address)) {
103
+ attendees.push(newGuest(address, displayName));
104
+ }
105
+ }
106
+ for (const chip of chips) {
107
+ const { address, displayName } = parseRecipient(chip);
108
+ const parts = chip.trim().split(/\s+/);
109
+ if (isValidRecipientAddress(address) || known(address)) {
110
+ add(address, displayName);
111
+ }
112
+ else if (parts.length > 1 && parts.every(isValidRecipientAddress)) {
113
+ parts.forEach((part) => add(part));
114
+ }
115
+ else if (!invalid.includes(chip.trim())) {
116
+ invalid.push(chip.trim());
117
+ }
118
+ }
119
+ return { attendees, invalid };
84
120
  }
85
121
  /** The wall-clock string a form input holds ("YYYY-MM-DDTHH:mm") read as `zone`'s clock, as an instant. `deviceZone` is the zone the browser
86
122
  * itself parses such a string in, which needs no conversion. */
@@ -133,20 +169,11 @@ export function formatStoredWhen(startDate, endDate, allDay, now = new Date()) {
133
169
  }
134
170
  return formatWhen(new Date(startDate), new Date(endDate), false, now);
135
171
  }
136
- /** Adds the addresses typed into the guests field to `current`: an address already on the list (in any case) is skipped, and what is not an
137
- * address comes back as `invalid` for the field to keep. */
138
- export function mergeGuests(current, text) {
139
- const attendees = [...current];
140
- const invalid = [];
141
- for (const token of splitGuestInput(text)) {
142
- if (!isValidGuestAddress(token)) {
143
- invalid.push(token);
144
- }
145
- else if (!attendees.some((a) => a.address.toLowerCase() === token.toLowerCase())) {
146
- attendees.push(newGuest(token));
147
- }
148
- }
149
- return { attendees, invalid };
172
+ /** Adds the guests typed or pasted into the guests field to `current` - recipients separated by commas or semicolons, each an address or
173
+ * `Name <address>` (see `applyGuestChips()`, which keeps who is already there): an address already on the list (in any case) or in
174
+ * `skipAddresses` is skipped, and what is not an address comes back as `invalid` for the field to keep. */
175
+ export function mergeGuests(current, text, skipAddresses = []) {
176
+ return applyGuestChips(current, [...current.map(guestChip), ...splitRecipientList(text)], skipAddresses);
150
177
  }
151
178
  /** `formatWhen()` for the form's own wall-clock strings (an all-day event's `end` is its inclusive last day); a message while either is empty. */
152
179
  export function formatFormWhen(start, end, allDay, now = new Date()) {
@@ -10,8 +10,9 @@ import { listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.j
10
10
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
11
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
12
12
  import AppShell from "../../layout/AppShell.js";
13
- import { useLocationSearch } from "../../../navigation/AppRouter.js";
13
+ import { useLocation } from "@rapidrest/react/client";
14
14
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
15
+ import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
15
16
  const CalendarShellContext = createContext({
16
17
  calendarFolders: [],
17
18
  mailboxCalendars: [],
@@ -39,7 +40,7 @@ export default function CalendarShell({ userUid, authServerUrl, impersonating, i
39
40
  const [folderRefreshToken, setFolderRefreshToken] = useState(0);
40
41
  // Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
41
42
  // effect, not during render, so the server render and the hydrating render agree.
42
- const search = useLocationSearch();
43
+ const { search } = useLocation();
43
44
  useEffect(() => {
44
45
  setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
45
46
  }, [search]);
@@ -49,7 +50,7 @@ export default function CalendarShell({ userUid, authServerUrl, impersonating, i
49
50
  }
50
51
  listMailboxes({ limit: 100 })
51
52
  .then((result) => {
52
- setMailboxes(result);
53
+ setMailboxes(orderMailboxes(result, userUid));
53
54
  setStatus("ready");
54
55
  })
55
56
  .catch((err) => {
@@ -57,7 +58,7 @@ export default function CalendarShell({ userUid, authServerUrl, impersonating, i
57
58
  setStatus("error");
58
59
  });
59
60
  }, [userUid]);
60
- const ownMailboxUid = mailboxes.find((mb) => mb.ownerUserUid === userUid)?.uid ?? mailboxes[0]?.uid;
61
+ const ownMailboxUid = primaryMailboxUid(mailboxes, userUid);
61
62
  const mailboxUid = (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
62
63
  ownMailboxUid;
63
64
  useEffect(() => {
@@ -103,5 +103,5 @@ export default function ContactsSidebar({ mailboxUid, contacts, active, onSelect
103
103
  // `Drawer` (see MailShell/ContactsShell for the identical pattern and the duplicate-id reasoning
104
104
  // behind the `idPrefix`).
105
105
  const navContent = (idPrefix) => (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx(NavItem, { label: "Your contacts", count: contacts.length, active: active.type === "all", onClick: () => handleSelect({ type: "all" }) }), _jsx(NavItem, { label: "Favorites", count: favoriteCount, active: active.type === "favorites", onClick: () => handleSelect({ type: "favorites" }) }), showDeleted && _jsx(NavItem, { label: "Deleted", active: active.type === "deleted", onClick: () => handleSelect({ type: "deleted" }) })] }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between px-2.5 mb-1", children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted", children: "Your contact lists" }), _jsx("button", { type: "button", onClick: () => setAddingList(true), "aria-label": "New contact list", className: "text-xs font-bold text-primary-dark hover:underline", children: "+" })] }), listsError && _jsx("p", { className: "px-2.5 text-xs text-danger", children: listsError }), _jsx("div", { className: "flex flex-col gap-0.5", children: lists.map((list) => (_jsx(NavItem, { label: list.name, count: listCounts.get(list.uid) ?? 0, active: active.type === "list" && active.uid === list.uid, onClick: () => handleSelect({ type: "list", uid: list.uid, name: list.name }) }, list.uid))) }), addingList && (_jsxs("form", { onSubmit: handleAddList, className: "flex items-center gap-1 px-2.5 mt-1", children: [_jsx("input", { type: "text", autoFocus: true, "aria-label": "New list name", id: `${idPrefix}-new-list-name`, value: newListName, onChange: (e) => setNewListName(e.target.value), className: "flex-1 text-xs py-1 px-1.5 border border-border rounded-sm bg-surface" }), _jsx("button", { type: "submit", className: "text-xs font-semibold text-primary-dark", children: "Add" })] }))] }), categories.length > 0 && (_jsxs("div", { children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted px-2.5 mb-1", children: "Categories" }), _jsx("div", { className: "flex flex-col gap-0.5", children: categories.map((name) => (_jsx(NavItem, { label: name, swatch: categoryColor(name), active: active.type === "category" && active.name === name, onClick: () => handleSelect({ type: "category", name }) }, name))) })] }))] }));
106
- return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "lg:hidden m-3 mb-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open contacts menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("nav", { "aria-label": "Contacts", className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col gap-4 p-3 overflow-y-auto", children: navContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Contacts", children: _jsx("div", { className: "flex flex-col gap-4", children: navContent("mobile") }) })] }));
106
+ return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "lg:hidden m-3 mb-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open contacts menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("nav", { "aria-label": "Contacts", className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col gap-4 p-3 overflow-y-auto", children: navContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Contacts", fullScreen: true, children: _jsx("div", { className: "flex flex-col gap-4", children: navContent("mobile") }) })] }));
107
107
  }
@@ -16,6 +16,8 @@ export interface ContactsToolbarProps {
16
16
  onImportFile: (file: File) => void;
17
17
  /** The keyboard's "New contact" is registered by the page: the button names its shortcut in its tooltip and `aria-keyshortcuts`. */
18
18
  shortcuts?: boolean;
19
+ /** Leaves the New contact button out - the phone layout offers it as a floating button instead. */
20
+ hideNew?: boolean;
19
21
  }
20
22
  /**
21
23
  * Contacts' Outlook-style ribbon toolbar, fitted to its column (`ResponsiveToolbar`): captions under the icons while there is room, then icons
@@ -26,4 +28,4 @@ export interface ContactsToolbarProps {
26
28
  * needs. "New contact list" isn't duplicated here — `ContactsSidebar` already has its own inline
27
29
  * "+ New list" affordance next to the "Your contact lists" heading.
28
30
  */
29
- export default function ContactsToolbar({ selectedCount, allSelectedFavorited, onNewContact, onEdit, onDelete, onEmail, onToggleFavorite, onAddCategory, onExportVCard, onImportFile, shortcuts, }: ContactsToolbarProps): React.JSX.Element;
31
+ export default function ContactsToolbar({ selectedCount, allSelectedFavorited, onNewContact, onEdit, onDelete, onEmail, onToggleFavorite, onAddCategory, onExportVCard, onImportFile, shortcuts, hideNew, }: ContactsToolbarProps): React.JSX.Element;
@@ -17,7 +17,7 @@ import ResponsiveToolbar from "../layout/ResponsiveToolbar.js";
17
17
  * needs. "New contact list" isn't duplicated here — `ContactsSidebar` already has its own inline
18
18
  * "+ New list" affordance next to the "Your contact lists" heading.
19
19
  */
20
- export default function ContactsToolbar({ selectedCount, allSelectedFavorited, onNewContact, onEdit, onDelete, onEmail, onToggleFavorite, onAddCategory, onExportVCard, onImportFile, shortcuts, }) {
20
+ export default function ContactsToolbar({ selectedCount, allSelectedFavorited, onNewContact, onEdit, onDelete, onEmail, onToggleFavorite, onAddCategory, onExportVCard, onImportFile, shortcuts, hideNew, }) {
21
21
  const newContactHint = useShortcutProps("New contact", SHORTCUTS.contacts.create, !!shortcuts);
22
22
  const importInputRef = useRef(null);
23
23
  const hasSelection = selectedCount > 0;
@@ -29,7 +29,9 @@ export default function ContactsToolbar({ selectedCount, allSelectedFavorited, o
29
29
  }
30
30
  }
31
31
  const actions = [
32
- { id: "new", label: "New contact", icon: HiOutlineUserPlus, onClick: onNewContact, hint: shortcuts ? newContactHint : undefined, group: 0, rank: 99, essential: true },
32
+ ...(hideNew
33
+ ? []
34
+ : [{ id: "new", label: "New contact", icon: HiOutlineUserPlus, onClick: onNewContact, hint: shortcuts ? newContactHint : undefined, group: 0, rank: 99, essential: true }]),
33
35
  { id: "edit", label: "Edit", icon: HiOutlinePencil, onClick: onEdit, disabled: selectedCount !== 1, group: 1, rank: 6 },
34
36
  { id: "delete", label: "Delete", icon: HiOutlineTrash, onClick: onDelete, disabled: !hasSelection, group: 1, rank: 5 },
35
37
  { id: "email", label: "Email", icon: HiOutlineEnvelope, onClick: onEmail, disabled: !hasSelection, group: 2, rank: 4 },
@@ -11,8 +11,10 @@ import { listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.j
11
11
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
12
12
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
13
13
  import AppShell from "../../layout/AppShell.js";
14
- import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
14
+ import { useLocation } from "@rapidrest/react/client";
15
+ import { useNavigate } from "../../../navigation/index.js";
15
16
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
17
+ import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
16
18
  const ContactsShellContext = createContext({ mailboxes: [] });
17
19
  /** Reads the mailbox/folder Contacts is currently showing, as resolved by the enclosing `ContactsShell`. */
18
20
  export function useContactsShell() {
@@ -35,7 +37,7 @@ export default function ContactsShell({ userUid, authServerUrl, impersonating, i
35
37
  const [drawerOpen, setDrawerOpen] = useState(false);
36
38
  // Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
37
39
  // effect, not during render, so the server render and the hydrating render agree.
38
- const search = useLocationSearch();
40
+ const { search } = useLocation();
39
41
  const navigate = useNavigate();
40
42
  useEffect(() => {
41
43
  setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
@@ -51,7 +53,7 @@ export default function ContactsShell({ userUid, authServerUrl, impersonating, i
51
53
  }
52
54
  listMailboxes({ limit: 100 })
53
55
  .then((result) => {
54
- setMailboxes(result);
56
+ setMailboxes(orderMailboxes(result, userUid));
55
57
  setStatus("ready");
56
58
  })
57
59
  .catch((err) => {
@@ -60,7 +62,7 @@ export default function ContactsShell({ userUid, authServerUrl, impersonating, i
60
62
  });
61
63
  }, [userUid]);
62
64
  const mailboxUid = (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
63
- mailboxes[0]?.uid;
65
+ primaryMailboxUid(mailboxes, userUid);
64
66
  useEffect(() => {
65
67
  if (!mailboxUid) {
66
68
  setFolders([]);
@@ -100,7 +102,7 @@ export default function ContactsShell({ userUid, authServerUrl, impersonating, i
100
102
  // The mailbox switcher (or an error) is all this column holds: with a single mailbox and no error there is nothing to put in it, and an empty
101
103
  // 224 px column beside the contacts' own menu is wasted room.
102
104
  const hasSidebar = mailboxes.length > 1 || !!folderError;
103
- inner = (_jsxs(_Fragment, { children: [hasSidebar && (_jsx("aside", { className: "hidden lg:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") })), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Mailbox", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [hasSidebar && (_jsx("button", { type: "button", className: "lg:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open mailbox switcher", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) })), _jsx(ContactsShellContext.Provider, { value: contextValue, children: children })] })] }));
105
+ inner = (_jsxs(_Fragment, { children: [hasSidebar && (_jsx("aside", { className: "hidden lg:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") })), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Mailbox", fullScreen: true, children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [hasSidebar && (_jsx("button", { type: "button", className: "lg:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open mailbox switcher", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) })), _jsx(ContactsShellContext.Provider, { value: contextValue, children: children })] })] }));
104
106
  }
105
107
  return (_jsx(AppShell, { active: "contacts", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner }));
106
108
  }
@@ -49,12 +49,9 @@ export interface AppDef {
49
49
  export declare const APPS: AppDef[];
50
50
  /** `APPS` followed by the plugins' `appRail` items (generic icon), core ids winning - see `mergePluginNavItems`. */
51
51
  export declare function appRailItems(pluginNav?: PluginNav): NavItem[];
52
- /** What only the persistent frame (`AppRouter`) passes to the chrome it keeps mounted. */
52
+ /** What only the persistent frame (`apps/www/_shell.tsx`) passes to the chrome it keeps mounted. */
53
53
  export interface AppChromeProps extends AppShellProps {
54
- /** Changes whenever the router shows a different page: the chrome then moves focus to the content, scrolls to its top,
55
- * sets the document title and announces the page. Absent outside the router, when nothing does. */
56
- routeKey?: string;
57
- /** The router is loading the next page's code - the content is `aria-busy`. */
54
+ /** The router is loading the next page - the content is `aria-busy`. */
58
55
  busy?: boolean;
59
56
  /** A screen that takes over the window (`FrameTakeover`) is showing: the rail, header, banner and footer are hidden - not
60
57
  * unmounted, so what is inside them (and the page in the content region) keeps its state. */
@@ -66,14 +63,14 @@ export interface AppChromeProps extends AppShellProps {
66
63
  * Each app's own shell (e.g. `MailShell`) renders its own contextual sidebar + content as `children`, inside
67
64
  * the area to the right of the icon rail and below the header.
68
65
  *
69
- * Rendered by `AppRouter`, once, for the life of the page, so that moving between apps replaces only `children` - see
70
- * `AppShell` below for what a page's own shell renders instead.
66
+ * Rendered by the webmail's app shell (`apps/www/_shell.tsx`, the router's persistent client layout), once, for the life of the
67
+ * page, so that moving between apps replaces only `children` - see `AppShell` below for what a page's own shell renders instead.
71
68
  */
72
- export declare function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, branding: initialBranding, appearance, pluginNav, routeKey, busy, hideChrome, children, }: PropsWithChildren<AppChromeProps>): React.JSX.Element;
69
+ export declare function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, branding: initialBranding, appearance, pluginNav, busy, hideChrome, children, }: PropsWithChildren<AppChromeProps>): React.JSX.Element;
73
70
  /**
74
71
  * What a page's own shell (`MailShell`, `CalendarShell`, ...) renders around its content. Outside the client-side router it
75
- * is the whole `AppChrome`, as it always was. Inside it (`AppRouter`) the chrome is already mounted above the page - and stays
76
- * mounted as the page is replaced - so this is only its children; everything it would have been given comes from the router
77
- * frame instead (the props are the same for every page, and `active` is the route's).
72
+ * is the whole `AppChrome`, as it always was. Inside it (the router's app shell, `apps/www/_shell.tsx`) the chrome is already mounted above the
73
+ * page - and stays mounted as the page is replaced - so this is only its children; everything it would have been given comes from
74
+ * the shell instead (the props are the same for every page, and `active` is the route's).
78
75
  */
79
76
  export default function AppShell(props: PropsWithChildren<AppShellProps>): React.JSX.Element;
@@ -4,8 +4,9 @@ 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 "../../styles/app.css";
7
- import { useEffect, useRef, useState } from "react";
7
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
8
8
  import { HiOutlineCalendarDays, HiOutlineClipboardDocumentList, HiOutlineEnvelope, HiOutlinePuzzlePiece, HiOutlineUsers, } from "react-icons/hi2";
9
+ import { useRouter } from "@rapidrest/react/client";
9
10
  import { useRedirectIfUnauthenticated } from "@rapidmx/react-shared/auth/session.js";
10
11
  import { getSetupStatus } from "@rapidmx/react-shared/admin/setupApi.js";
11
12
  import { stopImpersonating } from "@rapidmx/react-shared/mail/mailApi.js";
@@ -27,7 +28,7 @@ import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
27
28
  import { mergePluginNavItems } from "../../plugins/pluginNav.js";
28
29
  import { useInAppFrame } from "../../navigation/frameContext.js";
29
30
  import { APP_HREFS } from "../../navigation/appHrefs.js";
30
- import { useNavigate } from "../../navigation/routerContext.js";
31
+ import { useNavigate } from "../../navigation/index.js";
31
32
  import { GlobalShortcuts } from "../../keyboard/GlobalShortcuts.js";
32
33
  import { ShortcutProvider } from "../../keyboard/ShortcutProvider.js";
33
34
  import ShortcutsDialog from "../../keyboard/ShortcutsDialog.js";
@@ -84,10 +85,10 @@ export function appRailItems(pluginNav) {
84
85
  * Each app's own shell (e.g. `MailShell`) renders its own contextual sidebar + content as `children`, inside
85
86
  * the area to the right of the icon rail and below the header.
86
87
  *
87
- * Rendered by `AppRouter`, once, for the life of the page, so that moving between apps replaces only `children` - see
88
- * `AppShell` below for what a page's own shell renders instead.
88
+ * Rendered by the webmail's app shell (`apps/www/_shell.tsx`, the router's persistent client layout), once, for the life of the
89
+ * page, so that moving between apps replaces only `children` - see `AppShell` below for what a page's own shell renders instead.
89
90
  */
90
- export function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, branding: initialBranding, appearance, pluginNav, routeKey, busy, hideChrome, children, }) {
91
+ export function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, branding: initialBranding, appearance, pluginNav, busy, hideChrome, children, }) {
91
92
  const [stoppingImpersonation, setStoppingImpersonation] = useState(false);
92
93
  // The keyboard shortcuts dialog: opened by `?`/Ctrl+/ (`GlobalShortcuts`) and by the account menu's item.
93
94
  const [shortcutsOpen, setShortcutsOpen] = useState(false);
@@ -111,6 +112,7 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
111
112
  // the router (`AppShell` rendered as a page's own chrome: tests, plugin pages) it stays off, and a Mail shell owns its connection itself.
112
113
  const inFrame = useInAppFrame();
113
114
  const navigate = useNavigate();
115
+ const { pathname } = useRouter();
114
116
  const mail = useMailConnection({ userUid, enabled: inFrame, open: navigate });
115
117
  // A signing certificate the user asked for is announced when it is issued (or fails), on whichever page they are - see the hook.
116
118
  useSigningEnrollmentWatcher({ userUid, mailboxes: mail.mailboxes, enabled: inFrame });
@@ -210,32 +212,21 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
210
212
  window.location.href = "/admin";
211
213
  }
212
214
  }
213
- // What the persistent frame (`AppRouter`) needs on top of a page that never changes: the document title follows the page, and
214
- // after the router replaces the page the keyboard focus, the scroll position and a screen reader are told about it - a
215
- // page load did all of that by itself. Nothing here runs for a page shown outside the router (`routeKey` is undefined).
216
- const contentRef = useRef(null);
217
- const [announcement, setAnnouncement] = useState("");
218
- const firstRouteKeyRef = useRef(routeKey);
219
- const pageLabel = active === "settings" ? "Settings" : appRailItems(pluginNav).find((app) => app.id === active)?.label;
215
+ // The tab's title in the frame is each page's own (its `title` export: rendered by the server, set again by the router on every navigation),
216
+ // but `useBranding()` above sets it to the branding's title once its fetch answers - a title for a page that has none. The page's is put back:
217
+ // the layout effect reads it before that hook's effect runs, this one (declared after it) writes it back.
218
+ const pageTitleRef = useRef("");
219
+ useLayoutEffect(() => {
220
+ pageTitleRef.current = document.title;
221
+ }, [fetchedBranding?.title]);
220
222
  useEffect(() => {
221
- if (routeKey !== undefined) {
222
- const brand = branding?.title || branding?.companyName || "RapidMX";
223
- document.title = pageLabel ? `${brand}: ${pageLabel}` : brand;
223
+ if (inFrame) {
224
+ document.title = pageTitleRef.current;
224
225
  }
225
- }, [routeKey, pageLabel, branding]);
226
- // After the title effect above, so a page change (which rewrites the title) is followed by the unread count going back in front of it.
227
- useUnreadTitle(inboxUnreadTotal(mail.mailboxFolders, mail.folderCounts.counts), {
228
- enabled: inFrame,
229
- resetKey: `${routeKey}|${pageLabel}|${branding?.title}|${branding?.companyName}`,
230
- });
231
- useEffect(() => {
232
- if (routeKey === undefined || routeKey === firstRouteKeyRef.current) {
233
- return;
234
- }
235
- setAnnouncement(pageLabel ?? "");
236
- contentRef.current?.focus({ preventScroll: true });
237
- window.scrollTo(0, 0);
238
- }, [routeKey]);
226
+ }, [fetchedBranding?.title, inFrame]);
227
+ // The router sets the document's title to the page's own whenever the page changes, which drops the unread count this puts in front
228
+ // of it, so a new pathname is what puts the count back. (A folder change is shallow and keeps the title.)
229
+ useUnreadTitle(inboxUnreadTotal(mail.mailboxFolders, mail.folderCounts.counts), { enabled: inFrame, resetKey: pathname });
239
230
  if (!userUid) {
240
231
  return _jsx("div", { className: "min-h-screen" });
241
232
  }
@@ -260,13 +251,13 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
260
251
  id === active
261
252
  ? "bg-primary/10 text-primary-dark"
262
253
  : "text-text-muted hover:bg-surface-alt hover:text-text",
263
- ].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] })), !hideChrome && _jsx(BottomTabBar, { apps: apps, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [!hideChrome && !customHeader && (_jsxs("header", { ref: headerRef, className: "rr-solid sticky top-0 z-30 h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeLabel }), renderUserMenu("down")] })), _jsx(NotificationCenter, {}), _jsx("div", { ref: contentRef, id: "app-content", tabIndex: -1, "aria-busy": busy || undefined, className: ["flex-1 flex min-h-0 outline-none", hideChrome ? "" : "pb-14 md:pb-0"].join(" "), children: children })] })] }), routeKey !== undefined && (_jsx("div", { role: "status", "aria-live": "polite", className: "sr-only", children: announcement })), !hideChrome && _jsx(FrameBrandingFooter, { parsed: footer, userMenu: menuInFooter ? renderUserMenu("up") : undefined, appTitle: activeLabel })] }) })] }), _jsx(NotificationHistoryDialog, { open: historyOpen, onClose: () => setHistoryOpen(false) })] }) }) }));
254
+ ].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] })), !hideChrome && _jsx(BottomTabBar, { apps: apps, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [!hideChrome && !customHeader && (_jsxs("header", { ref: headerRef, className: "rr-solid sticky top-0 z-30 h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeLabel }), renderUserMenu("down")] })), _jsx(NotificationCenter, {}), _jsx("div", { id: "app-content", tabIndex: -1, "aria-busy": busy || undefined, className: ["flex-1 flex min-h-0 outline-none", hideChrome ? "" : "pb-14 md:pb-0"].join(" "), children: children })] })] }), !hideChrome && _jsx(FrameBrandingFooter, { parsed: footer, userMenu: menuInFooter ? renderUserMenu("up") : undefined, appTitle: activeLabel })] }) })] }), _jsx(NotificationHistoryDialog, { open: historyOpen, onClose: () => setHistoryOpen(false) })] }) }) }));
264
255
  }
265
256
  /**
266
257
  * What a page's own shell (`MailShell`, `CalendarShell`, ...) renders around its content. Outside the client-side router it
267
- * is the whole `AppChrome`, as it always was. Inside it (`AppRouter`) the chrome is already mounted above the page - and stays
268
- * mounted as the page is replaced - so this is only its children; everything it would have been given comes from the router
269
- * frame instead (the props are the same for every page, and `active` is the route's).
258
+ * is the whole `AppChrome`, as it always was. Inside it (the router's app shell, `apps/www/_shell.tsx`) the chrome is already mounted above the
259
+ * page - and stays mounted as the page is replaced - so this is only its children; everything it would have been given comes from
260
+ * the shell instead (the props are the same for every page, and `active` is the route's).
270
261
  */
271
262
  export default function AppShell(props) {
272
263
  const inFrame = useInAppFrame();
@@ -0,0 +1,13 @@
1
+ import React, { ButtonHTMLAttributes } from "react";
2
+ import type { IconType } from "react-icons";
3
+ export interface FloatingActionButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "aria-label" | "children" | "className" | "type"> {
4
+ /** The button's accessible name (it shows only the icon). */
5
+ label: string;
6
+ icon: IconType;
7
+ }
8
+ /**
9
+ * The phone layout's primary action - "New message" in Mail, "New event" in Calendar, "New contact" in Contacts - as a small round accent button
10
+ * floating at the bottom right, just above the bottom tab bar (which is `h-14`, fixed). A real `<button>` named by `label`; hidden from `md` up,
11
+ * where each page keeps its own toolbar button. Any other button attribute (`onPointerEnter`, `onFocus`, ...) is passed through.
12
+ */
13
+ export default function FloatingActionButton({ label, icon: Icon, ...rest }: FloatingActionButtonProps): React.JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * The phone layout's primary action - "New message" in Mail, "New event" in Calendar, "New contact" in Contacts - as a small round accent button
4
+ * floating at the bottom right, just above the bottom tab bar (which is `h-14`, fixed). A real `<button>` named by `label`; hidden from `md` up,
5
+ * where each page keeps its own toolbar button. Any other button attribute (`onPointerEnter`, `onFocus`, ...) is passed through.
6
+ */
7
+ export default function FloatingActionButton({ label, icon: Icon, ...rest }) {
8
+ return (_jsx("button", { ...rest, type: "button", "aria-label": label, className: "md:hidden fixed right-4 bottom-[4.5rem] z-30 w-12 h-12 flex items-center justify-center rounded-full bg-primary text-white shadow-lg hover:bg-primary-dark", children: _jsx(Icon, { size: 22, "aria-hidden": "true" }) }));
9
+ }
@@ -32,6 +32,9 @@ export interface UserMenuProps {
32
32
  placement?: "down" | "up";
33
33
  /** Shows a "Recent notifications" item that calls this - opening the history of the last pop-ups (`AppChrome` only). */
34
34
  onShowNotifications?: () => void;
35
+ /** Shows a "Back to mail" item, first in the menu, linking to the main application (`/`, on this same host). For the consoles
36
+ * (`AdminShell`), which are a step away from the app; the app itself has no need of it. */
37
+ showMailLink?: boolean;
35
38
  /** Errors in that history nobody has looked at yet: a count beside the item, and a dot on the menu button. */
36
39
  unseenErrors?: number;
37
40
  }
@@ -47,4 +50,4 @@ export interface UserMenuProps {
47
50
  * (auth-server's CORS list not including this origin, or no profile document at all - a 404 for some accounts) and none
48
51
  * ever surfaces as an error. The avatar image comes from the profile alone.
49
52
  */
50
- export default function UserMenu({ userUid, authServerUrl, onSignOut, showAdminLink, detectAdmin, trustedRoles, showSettingsLink, showNotificationSettings, onShowShortcuts, placement, onShowNotifications, unseenErrors, }: UserMenuProps): React.JSX.Element;
53
+ export default function UserMenu({ userUid, authServerUrl, onSignOut, showAdminLink, detectAdmin, trustedRoles, showSettingsLink, showNotificationSettings, onShowShortcuts, placement, onShowNotifications, showMailLink, unseenErrors, }: UserMenuProps): React.JSX.Element;
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  import { useContext, useEffect, useLayoutEffect, useRef, useState } from "react";
7
7
  import { createPortal } from "react-dom";
8
8
  import useIsMobile from "@rapidmx/react-shared/util/useIsMobile.js";
9
- import { HiOutlineShieldCheck } from "react-icons/hi2";
9
+ import { HiOutlineArrowUturnLeft, HiOutlineShieldCheck } from "react-icons/hi2";
10
10
  import { formatProfileName, getMyProfile, getMyUsername, profileInitials } from "@rapidmx/react-shared/auth/profileApi.js";
11
11
  import { listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
12
12
  import { accountUrlOf } from "../../auth/accountUrl.js";
@@ -14,8 +14,9 @@ import { DEFAULT_TRUSTED_ROLES, lookUpAdminAccess } from "../../auth/adminAccess
14
14
  import { ariaKeyShortcuts } from "../../keyboard/format.js";
15
15
  import { SHORTCUTS } from "../../keyboard/keymap.js";
16
16
  import { useKeyEnvironment } from "../../keyboard/ShortcutProvider.js";
17
- import { SETTINGS_HREF } from "../../navigation/appHrefs.js";
17
+ import { APP_HREFS, SETTINGS_HREF } from "../../navigation/appHrefs.js";
18
18
  import { MailConnectionContext } from "../../mail/useMailConnection.js";
19
+ import { ownMailboxes } from "../../mail/primaryMailbox.js";
19
20
  import ThemeSwitch from "./ThemeSwitch.js";
20
21
  import { desktopPermission, requestDesktopPermission, setDesktopOfferDismissed, } from "../../mail/newMailNotifications.js";
21
22
  import { getNotificationsEnabled, setNotificationsEnabled } from "../../notifications/preferences.js";
@@ -39,7 +40,7 @@ function Avatar({ profile, initials, large }) {
39
40
  * (auth-server's CORS list not including this origin, or no profile document at all - a 404 for some accounts) and none
40
41
  * ever surfaces as an error. The avatar image comes from the profile alone.
41
42
  */
42
- export default function UserMenu({ userUid, authServerUrl, onSignOut, showAdminLink, detectAdmin, trustedRoles = DEFAULT_TRUSTED_ROLES, showSettingsLink, showNotificationSettings, onShowShortcuts, placement = "down", onShowNotifications, unseenErrors = 0, }) {
43
+ export default function UserMenu({ userUid, authServerUrl, onSignOut, showAdminLink, detectAdmin, trustedRoles = DEFAULT_TRUSTED_ROLES, showSettingsLink, showNotificationSettings, onShowShortcuts, placement = "down", onShowNotifications, showMailLink, unseenErrors = 0, }) {
43
44
  const env = useKeyEnvironment();
44
45
  // A phone has no keyboard to speak of, so the shortcuts list (and the dialog it opens) has nothing to offer there.
45
46
  const isMobile = useIsMobile();
@@ -101,7 +102,7 @@ export default function UserMenu({ userUid, authServerUrl, onSignOut, showAdminL
101
102
  const listed = framed ? (connection.status === "checking" ? null : connection.mailboxes) : ownListing;
102
103
  const mailboxesKnown = listed !== null;
103
104
  // Only a mailbox the caller owns counts: a shared or delegated one carries somebody else's name.
104
- const mailboxName = needsFallbackName ? listed?.find((mb) => mb.ownerUserUid === userUid)?.displayName?.trim() || undefined : undefined;
105
+ const mailboxName = needsFallbackName ? ownMailboxes(listed ?? [], userUid)[0]?.displayName?.trim() || undefined : undefined;
105
106
  useEffect(() => {
106
107
  if (!authServerUrl || !needsFallbackName || !mailboxesKnown || mailboxName) {
107
108
  return;
@@ -203,7 +204,7 @@ export default function UserMenu({ userUid, authServerUrl, onSignOut, showAdminL
203
204
  const accountUrl = accountUrlOf(authServerUrl);
204
205
  return (_jsxs("div", { className: "relative", ref: containerRef, children: [_jsxs("button", { type: "button", onClick: () => setOpen((o) => !o), "aria-haspopup": "true", "aria-expanded": open, "aria-label": "Account menu", className: "relative flex items-center gap-1.5 rounded-pill py-1 pl-1 pr-2 hover:bg-surface-alt", children: [_jsx(Avatar, { profile: profile, initials: initials }), unseenErrors > 0 && (_jsx("span", { "data-testid": "unseen-errors-dot", "aria-hidden": "true", className: "absolute right-0.5 top-0.5 h-2.5 w-2.5 rounded-full border-2 border-surface bg-danger" })), _jsx("svg", { width: "14", height: "14", viewBox: "0 0 20 20", fill: "none", className: `text-text-muted transition-transform ${open ? "rotate-180" : ""}`, "aria-hidden": "true", children: _jsx("path", { d: "M5 7.5L10 12.5L15 7.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) })] }), open &&
205
206
  anchor &&
206
- createPortal(_jsxs("div", { ref: menuRef, role: "menu", style: { position: "fixed", right: anchor.right, top: anchor.top, bottom: anchor.bottom }, className: "w-60 max-w-[calc(100vw-1rem)] max-h-[calc(100vh-5rem)] overflow-y-auto bg-surface border border-border rounded-md shadow-modal py-1.5 z-[70]", children: [_jsxs("div", { className: "flex items-center gap-3 px-3.5 py-2.5 border-b border-border", children: [_jsx(Avatar, { profile: profile, initials: initials, large: true }), _jsx("span", { className: "text-sm font-semibold text-text truncate", children: name })] }), _jsx(ThemeSwitch, {}), accountUrl && (_jsx("a", { role: "menuitem", href: accountUrl, className: "block px-3.5 py-2 text-sm text-text hover:bg-surface-alt", children: "Account" })), showSettingsLink && (_jsx("a", { role: "menuitem", href: SETTINGS_HREF, className: "block px-3.5 py-2 text-sm text-text hover:bg-surface-alt", children: "Settings" })), showNotificationSettings && (_jsxs("button", { role: "menuitemcheckbox", "aria-checked": popups, type: "button", onClick: togglePopups, className: "flex w-full items-center justify-between gap-2 px-3.5 py-2 text-left text-sm text-text hover:bg-surface-alt", children: [_jsx("span", { children: "Notifications" }), _jsx("span", { className: "text-xs font-semibold text-text-muted", children: popups ? "On" : "Off" })] })), showNotificationSettings && permission === "default" && (_jsx("button", { role: "menuitem", type: "button", onClick: () => void enableDesktopNotifications(), className: "block w-full px-3.5 py-2 text-left text-sm text-text hover:bg-surface-alt", children: "Turn on desktop notifications" })), onShowNotifications && (_jsxs("button", { role: "menuitem", type: "button", onClick: () => {
207
+ createPortal(_jsxs("div", { ref: menuRef, role: "menu", style: { position: "fixed", right: anchor.right, top: anchor.top, bottom: anchor.bottom }, className: "w-60 max-w-[calc(100vw-1rem)] max-h-[calc(100vh-5rem)] overflow-y-auto bg-surface border border-border rounded-md shadow-modal py-1.5 z-[70]", children: [_jsxs("div", { className: "flex items-center gap-3 px-3.5 py-2.5 border-b border-border", children: [_jsx(Avatar, { profile: profile, initials: initials, large: true }), _jsx("span", { className: "text-sm font-semibold text-text truncate", children: name })] }), showMailLink && (_jsxs("a", { role: "menuitem", href: APP_HREFS.mail, className: "flex items-center gap-2 px-3.5 py-2 text-sm text-text hover:bg-surface-alt", children: [_jsx(HiOutlineArrowUturnLeft, { size: 16, "aria-hidden": "true", className: "shrink-0 text-text-muted" }), "Back to mail"] })), _jsx(ThemeSwitch, {}), accountUrl && (_jsx("a", { role: "menuitem", href: accountUrl, className: "block px-3.5 py-2 text-sm text-text hover:bg-surface-alt", children: "Account" })), showSettingsLink && (_jsx("a", { role: "menuitem", href: SETTINGS_HREF, className: "block px-3.5 py-2 text-sm text-text hover:bg-surface-alt", children: "Settings" })), showNotificationSettings && (_jsxs("button", { role: "menuitemcheckbox", "aria-checked": popups, type: "button", onClick: togglePopups, className: "flex w-full items-center justify-between gap-2 px-3.5 py-2 text-left text-sm text-text hover:bg-surface-alt", children: [_jsx("span", { children: "Notifications" }), _jsx("span", { className: "text-xs font-semibold text-text-muted", children: popups ? "On" : "Off" })] })), showNotificationSettings && permission === "default" && (_jsx("button", { role: "menuitem", type: "button", onClick: () => void enableDesktopNotifications(), className: "block w-full px-3.5 py-2 text-left text-sm text-text hover:bg-surface-alt", children: "Turn on desktop notifications" })), onShowNotifications && (_jsxs("button", { role: "menuitem", type: "button", onClick: () => {
207
208
  setOpen(false);
208
209
  onShowNotifications();
209
210
  }, className: "flex w-full items-center justify-between gap-2 px-3.5 py-2 text-left text-sm text-text hover:bg-surface-alt", children: [_jsx("span", { children: "Recent notifications" }), unseenErrors > 0 && (_jsxs("span", { className: "rounded-pill bg-danger px-1.5 text-xs font-bold text-white", children: [_jsx("span", { "aria-hidden": "true", children: unseenErrors }), _jsxs("span", { className: "sr-only", children: [unseenErrors, " unseen ", unseenErrors === 1 ? "error" : "errors"] })] }))] })), onShowShortcuts && !isMobile && (_jsx("button", { role: "menuitem", type: "button", "aria-keyshortcuts": ariaKeyShortcuts(SHORTCUTS.global.help, env), onClick: () => {