@rapidmx/web-client 0.9.0 → 0.11.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 (443) hide show
  1. package/README.md +373 -88
  2. package/apps/admin/_layout.tsx +8 -2
  3. package/apps/admin/index.tsx +7 -1
  4. package/apps/admin/ingest-queue/index.tsx +2 -1
  5. package/apps/admin/mailboxes/[uid].tsx +23 -10
  6. package/apps/admin/quarantine/index.tsx +2 -1
  7. package/apps/admin/signing-certificates/index.tsx +343 -0
  8. package/apps/escrow/_layout.tsx +8 -2
  9. package/apps/shared/appearance/AppearanceForm.tsx +392 -0
  10. package/apps/shared/appearance/AppearanceHead.tsx +49 -0
  11. package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
  12. package/apps/shared/appearance/ColorField.tsx +107 -0
  13. package/apps/shared/appearance/appearanceCache.ts +106 -0
  14. package/apps/shared/appearance/appearanceContext.ts +77 -0
  15. package/apps/shared/appearance/bootScript.ts +35 -0
  16. package/apps/shared/appearance/color.ts +169 -0
  17. package/apps/shared/appearance/photo.ts +88 -0
  18. package/apps/shared/appearance/resolvedTheme.ts +67 -0
  19. package/apps/shared/appearance/theme.ts +355 -0
  20. package/apps/shared/auth/accountUrl.ts +9 -0
  21. package/apps/shared/auth/adminAccess.ts +99 -0
  22. package/apps/shared/components/admin/layout/AdminShell.tsx +22 -10
  23. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +107 -60
  24. package/apps/shared/components/admin/settings/BrandingForm.tsx +24 -0
  25. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +2 -1
  26. package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
  27. package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
  28. package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
  29. package/apps/shared/components/calendar/layout/CalendarShell.tsx +8 -2
  30. package/apps/shared/components/contacts/ContactForm.tsx +11 -5
  31. package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -3
  32. package/apps/shared/components/contacts/ContactsToolbar.tsx +98 -115
  33. package/apps/shared/components/contacts/layout/ContactsShell.tsx +25 -8
  34. package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
  35. package/apps/shared/components/layout/AppShell.tsx +477 -288
  36. package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
  37. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -389
  38. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -164
  39. package/apps/shared/components/layout/RailIcon.tsx +100 -0
  40. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
  41. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
  42. package/apps/shared/components/layout/UserMenu.tsx +372 -185
  43. package/apps/shared/components/mail/ConversationList.tsx +271 -262
  44. package/apps/shared/components/mail/ConversationThreadPane.tsx +147 -94
  45. package/apps/shared/components/mail/LazyReadingPane.tsx +113 -0
  46. package/apps/shared/components/mail/MailSelectionBar.tsx +227 -222
  47. package/apps/shared/components/mail/MessageDetailPane.tsx +1537 -1387
  48. package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
  49. package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
  50. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -160
  51. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -418
  52. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1644 -1736
  53. package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +111 -0
  54. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -118
  55. package/apps/shared/components/mail/compose/composePerf.ts +46 -0
  56. package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
  57. package/apps/shared/components/mail/compose/quotedBody.ts +161 -100
  58. package/apps/shared/components/mail/layout/MailShell.tsx +490 -472
  59. package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
  60. package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
  61. package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
  62. package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
  63. package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
  64. package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
  65. package/apps/shared/components/mail/reading/color.ts +245 -0
  66. package/apps/shared/components/mail/reading/frameControl.ts +142 -0
  67. package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
  68. package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
  69. package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
  70. package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
  71. package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
  72. package/apps/shared/components/mail/unreadStyle.tsx +75 -0
  73. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
  74. package/apps/shared/components/settings/layout/SettingsShell.tsx +253 -240
  75. package/apps/shared/components/sharing/PrincipalPicker.tsx +145 -0
  76. package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
  77. package/apps/shared/components/tasks/layout/TasksShell.tsx +24 -8
  78. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -0
  79. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -0
  80. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -0
  81. package/apps/shared/keyboard/dispatch.ts +124 -0
  82. package/apps/shared/keyboard/format.ts +89 -0
  83. package/apps/shared/keyboard/keymap.ts +114 -0
  84. package/apps/shared/keyboard/match.ts +44 -0
  85. package/apps/shared/keyboard/parse.ts +136 -0
  86. package/apps/shared/keyboard/platform.ts +34 -0
  87. package/apps/shared/keyboard/registry.ts +65 -0
  88. package/apps/shared/keyboard/targets.ts +79 -0
  89. package/apps/shared/keyboard/useShortcut.ts +50 -0
  90. package/apps/shared/keyboard/useShortcutProps.ts +17 -0
  91. package/apps/shared/mail/folderCounts.ts +308 -0
  92. package/apps/shared/mail/folderTree.ts +143 -0
  93. package/apps/shared/mail/listSnapshots.ts +87 -0
  94. package/apps/shared/mail/messageReadState.ts +85 -0
  95. package/apps/shared/mail/newMailNotifications.ts +183 -0
  96. package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
  97. package/apps/shared/mail/outbox/composeBridge.ts +69 -0
  98. package/apps/shared/mail/outbox/outboxState.ts +88 -0
  99. package/apps/shared/mail/outbox/pendingSends.ts +146 -0
  100. package/apps/shared/mail/outbox/sendDecision.ts +157 -0
  101. package/apps/shared/mail/outbox/sendJob.ts +427 -0
  102. package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
  103. package/apps/shared/mail/outbox/sendState.ts +35 -0
  104. package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
  105. package/apps/shared/mail/useMailConnection.ts +203 -0
  106. package/apps/shared/mail/useMailLiveUpdates.ts +277 -224
  107. package/apps/shared/mail/useMarkMessageRead.ts +47 -0
  108. package/apps/shared/mail/useNewMailNotifications.ts +178 -0
  109. package/apps/shared/mail/useUnreadTitle.ts +42 -0
  110. package/apps/shared/navigation/AppRouter.tsx +300 -0
  111. package/apps/shared/navigation/appHrefs.ts +23 -0
  112. package/apps/shared/navigation/frameContext.tsx +35 -0
  113. package/apps/shared/navigation/idle.ts +45 -0
  114. package/apps/shared/navigation/routerContext.tsx +83 -0
  115. package/apps/shared/navigation/routes.ts +72 -0
  116. package/apps/shared/notifications/NotificationCenter.tsx +215 -0
  117. package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
  118. package/apps/shared/notifications/apiErrors.ts +98 -0
  119. package/apps/shared/notifications/headerOffset.ts +35 -0
  120. package/apps/shared/notifications/pushStatus.ts +56 -0
  121. package/apps/shared/notifications/store.ts +549 -0
  122. package/apps/shared/notifications/systemErrors.ts +63 -0
  123. package/apps/shared/notifications/useNotifications.ts +41 -0
  124. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -98
  125. package/apps/shared/signing/enrollmentStorage.ts +33 -0
  126. package/apps/shared/signing/enrollmentTracker.ts +385 -0
  127. package/apps/shared/signing/enrollmentView.ts +251 -0
  128. package/apps/shared/signing/signingInfo.ts +58 -0
  129. package/apps/shared/signing/useNow.ts +19 -0
  130. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
  131. package/apps/shared/styles/app.css +108 -10
  132. package/apps/www/_layout.tsx +8 -2
  133. package/apps/www/_routedPage.tsx +24 -0
  134. package/apps/www/_routes.ts +36 -0
  135. package/apps/www/calendar/index.tsx +64 -33
  136. package/apps/www/contacts/[uid].tsx +109 -107
  137. package/apps/www/contacts/index.tsx +62 -42
  138. package/apps/www/index.tsx +2291 -1917
  139. package/apps/www/messages/[uid].tsx +108 -101
  140. package/apps/www/settings/appearance/index.tsx +21 -0
  141. package/apps/www/settings/auto-reply/index.tsx +7 -8
  142. package/apps/www/settings/encryption/index.tsx +1287 -1249
  143. package/apps/www/settings/filters/[uid].tsx +7 -2
  144. package/apps/www/settings/filters/index.tsx +102 -99
  145. package/apps/www/settings/filters/new/index.tsx +140 -133
  146. package/apps/www/settings/labels/index.tsx +204 -201
  147. package/apps/www/settings/privacy/index.tsx +7 -9
  148. package/apps/www/settings/read-receipts/index.tsx +7 -8
  149. package/apps/www/settings/sharing/index.tsx +256 -274
  150. package/apps/www/settings/signatures/[uid].tsx +172 -167
  151. package/apps/www/settings/signatures/index.tsx +88 -85
  152. package/apps/www/settings/signatures/new/index.tsx +135 -129
  153. package/apps/www/tasks/index.tsx +29 -18
  154. package/dist/apps/admin/_layout.d.ts +5 -1
  155. package/dist/apps/admin/_layout.js +3 -2
  156. package/dist/apps/admin/index.js +3 -2
  157. package/dist/apps/admin/ingest-queue/index.js +2 -1
  158. package/dist/apps/admin/mailboxes/[uid].js +9 -6
  159. package/dist/apps/admin/quarantine/index.js +2 -1
  160. package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
  161. package/dist/apps/admin/signing-certificates/index.js +161 -0
  162. package/dist/apps/escrow/_layout.d.ts +5 -1
  163. package/dist/apps/escrow/_layout.js +3 -2
  164. package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
  165. package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
  166. package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
  167. package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
  168. package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
  169. package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
  170. package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
  171. package/dist/apps/shared/appearance/ColorField.js +40 -0
  172. package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
  173. package/dist/apps/shared/appearance/appearanceCache.js +80 -0
  174. package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
  175. package/dist/apps/shared/appearance/appearanceContext.js +29 -0
  176. package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
  177. package/dist/apps/shared/appearance/bootScript.js +33 -0
  178. package/dist/apps/shared/appearance/color.d.ts +65 -0
  179. package/dist/apps/shared/appearance/color.js +132 -0
  180. package/dist/apps/shared/appearance/photo.d.ts +17 -0
  181. package/dist/apps/shared/appearance/photo.js +79 -0
  182. package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
  183. package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
  184. package/dist/apps/shared/appearance/theme.d.ts +111 -0
  185. package/dist/apps/shared/appearance/theme.js +251 -0
  186. package/dist/apps/shared/auth/accountUrl.d.ts +2 -0
  187. package/dist/apps/shared/auth/accountUrl.js +8 -0
  188. package/dist/apps/shared/auth/adminAccess.d.ts +30 -0
  189. package/dist/apps/shared/auth/adminAccess.js +89 -0
  190. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  191. package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
  192. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
  193. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
  194. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
  195. package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
  196. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +1 -1
  197. package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
  198. package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
  199. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
  200. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +1 -1
  201. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +8 -4
  202. package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
  203. package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
  204. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +6 -2
  205. package/dist/apps/shared/components/contacts/ContactsToolbar.js +28 -10
  206. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +1 -1
  207. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +19 -6
  208. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
  209. package/dist/apps/shared/components/layout/AppShell.d.ts +34 -4
  210. package/dist/apps/shared/components/layout/AppShell.js +122 -17
  211. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
  212. package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
  213. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -9
  214. package/dist/apps/shared/components/layout/MailboxProvisioning.js +3 -1
  215. package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
  216. package/dist/apps/shared/components/layout/RailIcon.js +85 -0
  217. package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
  218. package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
  219. package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
  220. package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
  221. package/dist/apps/shared/components/layout/UserMenu.d.ts +25 -3
  222. package/dist/apps/shared/components/layout/UserMenu.js +88 -7
  223. package/dist/apps/shared/components/mail/ConversationList.js +5 -11
  224. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +6 -2
  225. package/dist/apps/shared/components/mail/ConversationThreadPane.js +87 -40
  226. package/dist/apps/shared/components/mail/LazyReadingPane.d.ts +9 -0
  227. package/dist/apps/shared/components/mail/LazyReadingPane.js +83 -0
  228. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +6 -4
  229. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -4
  230. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +25 -8
  231. package/dist/apps/shared/components/mail/MessageDetailPane.js +167 -73
  232. package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
  233. package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
  234. package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
  235. package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
  236. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +38 -0
  237. package/dist/apps/shared/components/mail/compose/ComposeContext.js +97 -6
  238. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +4 -3
  239. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +3 -2
  240. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +316 -339
  241. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +19 -0
  242. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +33 -0
  243. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +9 -1
  244. package/dist/apps/shared/components/mail/compose/RichTextEditor.js +20 -2
  245. package/dist/apps/shared/components/mail/compose/composePerf.d.ts +16 -0
  246. package/dist/apps/shared/components/mail/compose/composePerf.js +41 -0
  247. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
  248. package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
  249. package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +13 -0
  250. package/dist/apps/shared/components/mail/compose/quotedBody.js +67 -11
  251. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +14 -4
  252. package/dist/apps/shared/components/mail/layout/MailShell.js +90 -104
  253. package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
  254. package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
  255. package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
  256. package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
  257. package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
  258. package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
  259. package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
  260. package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
  261. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
  262. package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
  263. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
  264. package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
  265. package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
  266. package/dist/apps/shared/components/mail/reading/color.js +201 -0
  267. package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
  268. package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
  269. package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
  270. package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
  271. package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
  272. package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
  273. package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
  274. package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
  275. package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
  276. package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
  277. package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
  278. package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
  279. package/dist/apps/shared/components/mail/unreadStyle.d.ts +45 -0
  280. package/dist/apps/shared/components/mail/unreadStyle.js +61 -0
  281. package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
  282. package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
  283. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +1 -1
  284. package/dist/apps/shared/components/settings/layout/SettingsShell.js +16 -5
  285. package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +26 -0
  286. package/dist/apps/shared/components/sharing/PrincipalPicker.js +77 -0
  287. package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
  288. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +1 -1
  289. package/dist/apps/shared/components/tasks/layout/TasksShell.js +18 -6
  290. package/dist/apps/shared/keyboard/GlobalShortcuts.d.ts +14 -0
  291. package/dist/apps/shared/keyboard/GlobalShortcuts.js +37 -0
  292. package/dist/apps/shared/keyboard/ShortcutProvider.d.ts +20 -0
  293. package/dist/apps/shared/keyboard/ShortcutProvider.js +52 -0
  294. package/dist/apps/shared/keyboard/ShortcutsDialog.d.ts +14 -0
  295. package/dist/apps/shared/keyboard/ShortcutsDialog.js +42 -0
  296. package/dist/apps/shared/keyboard/dispatch.d.ts +16 -0
  297. package/dist/apps/shared/keyboard/dispatch.js +109 -0
  298. package/dist/apps/shared/keyboard/format.d.ts +12 -0
  299. package/dist/apps/shared/keyboard/format.js +74 -0
  300. package/dist/apps/shared/keyboard/keymap.d.ts +294 -0
  301. package/dist/apps/shared/keyboard/keymap.js +84 -0
  302. package/dist/apps/shared/keyboard/match.d.ts +20 -0
  303. package/dist/apps/shared/keyboard/match.js +31 -0
  304. package/dist/apps/shared/keyboard/parse.d.ts +31 -0
  305. package/dist/apps/shared/keyboard/parse.js +107 -0
  306. package/dist/apps/shared/keyboard/platform.d.ts +15 -0
  307. package/dist/apps/shared/keyboard/platform.js +22 -0
  308. package/dist/apps/shared/keyboard/registry.d.ts +39 -0
  309. package/dist/apps/shared/keyboard/registry.js +33 -0
  310. package/dist/apps/shared/keyboard/targets.d.ts +14 -0
  311. package/dist/apps/shared/keyboard/targets.js +67 -0
  312. package/dist/apps/shared/keyboard/useShortcut.d.ts +19 -0
  313. package/dist/apps/shared/keyboard/useShortcut.js +35 -0
  314. package/dist/apps/shared/keyboard/useShortcutProps.d.ts +10 -0
  315. package/dist/apps/shared/keyboard/useShortcutProps.js +15 -0
  316. package/dist/apps/shared/mail/folderCounts.d.ts +78 -0
  317. package/dist/apps/shared/mail/folderCounts.js +218 -0
  318. package/dist/apps/shared/mail/folderTree.d.ts +55 -0
  319. package/dist/apps/shared/mail/folderTree.js +116 -0
  320. package/dist/apps/shared/mail/listSnapshots.d.ts +46 -0
  321. package/dist/apps/shared/mail/listSnapshots.js +43 -0
  322. package/dist/apps/shared/mail/messageReadState.d.ts +32 -0
  323. package/dist/apps/shared/mail/messageReadState.js +63 -0
  324. package/dist/apps/shared/mail/newMailNotifications.d.ts +62 -0
  325. package/dist/apps/shared/mail/newMailNotifications.js +138 -0
  326. package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
  327. package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
  328. package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
  329. package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
  330. package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
  331. package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
  332. package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
  333. package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
  334. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
  335. package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
  336. package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
  337. package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
  338. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
  339. package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
  340. package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
  341. package/dist/apps/shared/mail/outbox/sendState.js +8 -0
  342. package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
  343. package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
  344. package/dist/apps/shared/mail/useMailConnection.d.ts +56 -0
  345. package/dist/apps/shared/mail/useMailConnection.js +138 -0
  346. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +29 -9
  347. package/dist/apps/shared/mail/useMailLiveUpdates.js +60 -32
  348. package/dist/apps/shared/mail/useMarkMessageRead.d.ts +12 -0
  349. package/dist/apps/shared/mail/useMarkMessageRead.js +44 -0
  350. package/dist/apps/shared/mail/useNewMailNotifications.d.ts +35 -0
  351. package/dist/apps/shared/mail/useNewMailNotifications.js +130 -0
  352. package/dist/apps/shared/mail/useUnreadTitle.d.ts +18 -0
  353. package/dist/apps/shared/mail/useUnreadTitle.js +31 -0
  354. package/dist/apps/shared/navigation/AppRouter.d.ts +52 -0
  355. package/dist/apps/shared/navigation/AppRouter.js +242 -0
  356. package/dist/apps/shared/navigation/appHrefs.d.ts +14 -0
  357. package/dist/apps/shared/navigation/appHrefs.js +20 -0
  358. package/dist/apps/shared/navigation/frameContext.d.ts +19 -0
  359. package/dist/apps/shared/navigation/frameContext.js +24 -0
  360. package/dist/apps/shared/navigation/idle.d.ts +14 -0
  361. package/dist/apps/shared/navigation/idle.js +43 -0
  362. package/dist/apps/shared/navigation/routerContext.d.ts +37 -0
  363. package/dist/apps/shared/navigation/routerContext.js +56 -0
  364. package/dist/apps/shared/navigation/routes.d.ts +32 -0
  365. package/dist/apps/shared/navigation/routes.js +37 -0
  366. package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
  367. package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
  368. package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
  369. package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
  370. package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
  371. package/dist/apps/shared/notifications/apiErrors.js +86 -0
  372. package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
  373. package/dist/apps/shared/notifications/headerOffset.js +33 -0
  374. package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
  375. package/dist/apps/shared/notifications/pushStatus.js +54 -0
  376. package/dist/apps/shared/notifications/store.d.ts +136 -0
  377. package/dist/apps/shared/notifications/store.js +416 -0
  378. package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
  379. package/dist/apps/shared/notifications/systemErrors.js +58 -0
  380. package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
  381. package/dist/apps/shared/notifications/useNotifications.js +18 -0
  382. package/dist/apps/shared/search/LocalIndexLifecycle.js +17 -3
  383. package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
  384. package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
  385. package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
  386. package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
  387. package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
  388. package/dist/apps/shared/signing/enrollmentView.js +214 -0
  389. package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
  390. package/dist/apps/shared/signing/signingInfo.js +48 -0
  391. package/dist/apps/shared/signing/useNow.d.ts +2 -0
  392. package/dist/apps/shared/signing/useNow.js +18 -0
  393. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
  394. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
  395. package/dist/apps/shared/styles/app.css +108 -10
  396. package/dist/apps/www/_layout.d.ts +5 -1
  397. package/dist/apps/www/_layout.js +3 -2
  398. package/dist/apps/www/_routedPage.d.ts +12 -0
  399. package/dist/apps/www/_routedPage.js +19 -0
  400. package/dist/apps/www/_routes.d.ts +11 -0
  401. package/dist/apps/www/_routes.js +30 -0
  402. package/dist/apps/www/calendar/index.d.ts +2 -2
  403. package/dist/apps/www/calendar/index.js +41 -11
  404. package/dist/apps/www/contacts/[uid].d.ts +3 -9
  405. package/dist/apps/www/contacts/[uid].js +11 -9
  406. package/dist/apps/www/contacts/index.d.ts +2 -2
  407. package/dist/apps/www/contacts/index.js +30 -24
  408. package/dist/apps/www/index.d.ts +2 -2
  409. package/dist/apps/www/index.js +332 -48
  410. package/dist/apps/www/messages/[uid].d.ts +3 -7
  411. package/dist/apps/www/messages/[uid].js +10 -5
  412. package/dist/apps/www/settings/appearance/index.d.ts +4 -0
  413. package/dist/apps/www/settings/appearance/index.js +13 -0
  414. package/dist/apps/www/settings/auto-reply/index.d.ts +2 -2
  415. package/dist/apps/www/settings/auto-reply/index.js +6 -7
  416. package/dist/apps/www/settings/encryption/index.d.ts +2 -2
  417. package/dist/apps/www/settings/encryption/index.js +106 -96
  418. package/dist/apps/www/settings/filters/[uid].d.ts +2 -2
  419. package/dist/apps/www/settings/filters/[uid].js +6 -2
  420. package/dist/apps/www/settings/filters/index.d.ts +2 -2
  421. package/dist/apps/www/settings/filters/index.js +3 -1
  422. package/dist/apps/www/settings/filters/new/index.d.ts +2 -2
  423. package/dist/apps/www/settings/filters/new/index.js +9 -3
  424. package/dist/apps/www/settings/labels/index.d.ts +2 -2
  425. package/dist/apps/www/settings/labels/index.js +3 -1
  426. package/dist/apps/www/settings/privacy/index.d.ts +2 -2
  427. package/dist/apps/www/settings/privacy/index.js +8 -9
  428. package/dist/apps/www/settings/read-receipts/index.d.ts +2 -2
  429. package/dist/apps/www/settings/read-receipts/index.js +6 -7
  430. package/dist/apps/www/settings/sharing/index.d.ts +2 -2
  431. package/dist/apps/www/settings/sharing/index.js +22 -34
  432. package/dist/apps/www/settings/signatures/[uid].d.ts +2 -2
  433. package/dist/apps/www/settings/signatures/[uid].js +6 -2
  434. package/dist/apps/www/settings/signatures/index.d.ts +2 -2
  435. package/dist/apps/www/settings/signatures/index.js +3 -1
  436. package/dist/apps/www/settings/signatures/new/index.d.ts +2 -2
  437. package/dist/apps/www/settings/signatures/new/index.js +9 -4
  438. package/dist/apps/www/tasks/index.d.ts +2 -2
  439. package/dist/apps/www/tasks/index.js +22 -16
  440. package/package.json +2 -2
  441. package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
  442. package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
  443. package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
@@ -3,19 +3,28 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
- import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
6
+ import { createContext, useContext, useEffect, useMemo, useState } from "react";
7
7
  import { HiOutlineBars3 } from "react-icons/hi2";
8
- import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
9
8
  import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
10
- import { listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
11
9
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
12
10
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
13
11
  import AppShell from "../../layout/AppShell.js";
12
+ import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
14
13
  import KeyEnrollmentGate from "../../layout/KeyEnrollmentGate.js";
15
14
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
16
- import { useCompose } from "../compose/ComposeContext.js";
15
+ import { prefetchComposeWindow, useCompose } from "../compose/ComposeContext.js";
17
16
  import LocalIndexLifecycle from "../../../search/LocalIndexLifecycle.js";
18
- import { NO_LIVE_UPDATES, useMailLiveUpdates } from "../../../mail/useMailLiveUpdates.js";
17
+ import { NO_LIVE_UPDATES } from "../../../mail/useMailLiveUpdates.js";
18
+ import { MAILBOX_LIST_LIMIT, MailConnectionContext, useMailConnection } from "../../../mail/useMailConnection.js";
19
+ import { folderRows } from "../../../mail/folderTree.js";
20
+ import { badgeFor, badgeLabel, countOfFolder, inboxUnreadTotal, } from "../../../mail/folderCounts.js";
21
+ import { useUnreadTitle } from "../../../mail/useUnreadTitle.js";
22
+ import { pendingCountFor, usePendingSends } from "../../../mail/outbox/pendingSends.js";
23
+ import OutboxBadge from "../OutboxBadge.js";
24
+ import { ariaKeyShortcuts, withHint } from "../../../keyboard/format.js";
25
+ import { SHORTCUTS } from "../../../keyboard/keymap.js";
26
+ import { useKeyEnvironment } from "../../../keyboard/ShortcutProvider.js";
27
+ import { useShortcut } from "../../../keyboard/useShortcut.js";
19
28
  /** The well-known mail folder types an "aggregate" pseudo-folder can merge across every accessible
20
29
  * mailbox - a fixed, small set: `outbox` is deliberately excluded (transient per-mailbox send-queue
21
30
  * state, not a "merge across mailboxes" concept), and there's no aggregate concept for `calendar`/
@@ -25,11 +34,14 @@ export const AGGREGATE_FOLDER_TYPES = ["inbox", "sent_items", "drafts", "deleted
25
34
  function isAggregateFolderType(value) {
26
35
  return !!value && AGGREGATE_FOLDER_TYPES.includes(value);
27
36
  }
37
+ const NO_TRACKER = { settle: () => undefined, revert: () => undefined };
28
38
  const MailShellContext = createContext({
29
39
  mailboxes: [],
30
40
  mailboxFolders: [],
31
41
  onFolderCreated: () => undefined,
42
+ noteFolderUids: () => undefined,
32
43
  live: NO_LIVE_UPDATES,
44
+ trackMessageChange: () => NO_TRACKER,
33
45
  });
34
46
  /** Reads the mailbox/folder a page is currently showing, as resolved by the enclosing `MailShell`. */
35
47
  export function useMailShell() {
@@ -44,34 +56,23 @@ const FOLDER_LABELS = {
44
56
  archive: "Archive",
45
57
  deleted_items: "Deleted Items",
46
58
  };
47
- /** Well-known folders sort first, in Gmail/Outlook's conventional order; anything else (incl. `user`) sorts after, alphabetically. */
48
- const FOLDER_ORDER = ["inbox", "drafts", "outbox", "sent_items", "junk", "archive", "deleted_items"];
49
- /**
50
- * A mailbox's `calendar`/`contacts`/`tasks`/`notes` folders back their own dedicated apps (see
51
- * `CalendarShell`/`ContactsShell`/`TasksShell`), not Mail — `listFolders()` returns every well-known
52
- * folder for the mailbox regardless of which app owns it, so Mail's own folder tree must filter down
53
- * to just the mail ones itself, or those other apps' folders leak into this sidebar.
54
- */
55
- const MAIL_FOLDER_TYPES = new Set([...FOLDER_ORDER, "user"]);
56
- function folderSortKey(folder) {
57
- const idx = FOLDER_ORDER.indexOf(folder.type);
58
- return idx === -1 ? FOLDER_ORDER.length : idx;
59
- }
60
- function sortedFoldersOf(folders) {
61
- return [...folders].sort((a, b) => folderSortKey(a) - folderSortKey(b) || a.name.localeCompare(b.name));
62
- }
63
- /** A folder's unread count: the refreshed one when live updates have read it since load, else the one it was listed with. */
64
- function unreadOf(folder, unreadCounts) {
65
- return unreadCounts[folder.uid] ?? folder.unreadCount;
66
- }
67
- function aggregateUnreadCount(mailboxFolders, type, unreadCounts) {
68
- return mailboxFolders.reduce((total, mf) => {
59
+ /** The badge an "All Mailboxes" entry shows: the folders of that type, summed across every mailbox, under the same rules as a single folder's. */
60
+ function aggregateBadge(mailboxFolders, type, counts) {
61
+ const sum = mailboxFolders.reduce((total, mf) => {
69
62
  const folder = mf.folders.find((f) => f.type === type);
70
- return total + (folder ? unreadOf(folder, unreadCounts) : 0);
71
- }, 0);
63
+ const count = folder ? countOfFolder(folder, counts) : { unread: 0, total: 0 };
64
+ return { unread: total.unread + count.unread, total: total.total + count.total };
65
+ }, { unread: 0, total: 0 });
66
+ return badgeFor(type, sum);
67
+ }
68
+ /** A folder's badge: an accent pill with the unread count, or - for the folders that show how many they hold - plain muted text. */
69
+ function FolderBadgeChip({ badge }) {
70
+ return (_jsxs("span", { className: [
71
+ "text-xs rounded-pill py-0.5 px-1.5",
72
+ badge.kind === "unread" ? "font-bold bg-primary/15 text-primary-dark" : "font-medium text-text-muted",
73
+ ].join(" "), children: [_jsx("span", { "aria-hidden": "true", children: badge.value }), _jsxs("span", { className: "sr-only", children: [" ", badgeLabel(badge)] })] }));
72
74
  }
73
- /** Page size of the one `listMailboxes()` call this shell makes. */
74
- export const MAILBOX_LIST_LIMIT = 100;
75
+ export { MAILBOX_LIST_LIMIT };
75
76
  /**
76
77
  * A separate component (not inlined into `MailShell`'s own render) so `useCompose()` resolves against
77
78
  * `ComposeProvider` correctly: that provider is rendered *inside* the `AppShell` that `MailShell` itself
@@ -80,12 +81,25 @@ export const MAILBOX_LIST_LIMIT = 100;
80
81
  * its own descendants creates. This button, rendered as part of `AppShell`'s `children`, sits correctly
81
82
  * inside that subtree.
82
83
  *
83
- * Opens with no mailbox: a fresh message defaults to the caller's own mailbox, and the compose window's
84
- * own From field is where the sender is chosen.
84
+ * Opens with the caller's own mailbox (the shell has already listed them), so the window can start on its
85
+ * signature, draft and encryption lookups straight away instead of first asking which mailbox that is; the
86
+ * compose window's own From field is where the sender is chosen. Fetches the window's code as the pointer or
87
+ * keyboard reaches the button, so the click finds it already here.
85
88
  */
86
- function ComposeButton() {
89
+ function ComposeButton({ mailboxUid }) {
87
90
  const { openCompose } = useCompose();
88
- return (_jsx("button", { type: "button", onClick: () => openCompose({}), className: "block text-center w-full py-2.5 px-4 rounded-sm font-semibold text-sm bg-primary text-white hover:bg-primary-dark", children: "Compose" }));
91
+ const env = useKeyEnvironment();
92
+ return (_jsx("button", { type: "button", title: withHint("Compose", SHORTCUTS.mail.create, env), "aria-keyshortcuts": ariaKeyShortcuts(SHORTCUTS.mail.create, env), onClick: () => openCompose({ mailboxUid }), onPointerEnter: prefetchComposeWindow, onFocus: prefetchComposeWindow, className: "block text-center w-full py-2.5 px-4 rounded-sm font-semibold text-sm bg-primary text-white hover:bg-primary-dark", children: "Compose" }));
93
+ }
94
+ /**
95
+ * Mail's keyboard shortcuts that don't belong to a list or a message: "New message". A component of its own for the reason `ComposeButton`
96
+ * is - `useCompose()` only resolves below `ComposeProvider`, i.e. inside the `AppShell` this shell renders - and mounted once, whatever number
97
+ * of Compose buttons (the sidebar, the folders drawer) are on screen. Opens the same window, for the same mailbox, as the button.
98
+ */
99
+ function MailShortcuts({ mailboxUid }) {
100
+ const { openCompose } = useCompose();
101
+ useShortcut(SHORTCUTS.mail.create, () => openCompose({ mailboxUid }));
102
+ return null;
89
103
  }
90
104
  /**
91
105
  * Mail's own contextual sidebar (every accessible mailbox's own folder tree, plus a merged "All Mailboxes"
@@ -103,36 +117,35 @@ function ComposeButton() {
103
117
  * aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
104
118
  * of folders" requirement.
105
119
  */
106
- export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }) {
107
- const [status, setStatus] = useState("checking");
108
- const [error, setError] = useState(null);
109
- const [mailboxes, setMailboxes] = useState([]);
110
- const [mailboxFolders, setMailboxFolders] = useState([]);
111
- const [foldersLoading, setFoldersLoading] = useState(true);
120
+ export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }) {
121
+ // The mailboxes, their folders and the live connection come from the persistent app frame when there is one (`AppChrome` owns them, so
122
+ // they survive Mail -> Calendar -> Mail and the push socket, pop-ups, counters and tab title work in every app). A page rendered
123
+ // outside the frame - a test, a plugin page - has no frame to ask, so this shell runs the same hook for itself, and renders the pop-ups.
124
+ const navigate = useNavigate();
125
+ const hosted = useContext(MailConnectionContext);
126
+ const own = useMailConnection({ userUid, enabled: !hosted, open: navigate });
127
+ const { status, error, mailboxes, mailboxFolders, foldersLoading, onFolderCreated, noteFolderUids, live, folderCounts, outbox: outboxStatuses } = hosted ?? own;
128
+ // The messages this tab is still handing to the server: the Outbox pill counts them at once.
129
+ const pendingSends = usePendingSends();
112
130
  const [requestedMailboxUid, setRequestedMailboxUid] = useState(null);
113
131
  const [requestedFolderUid, setRequestedFolderUid] = useState(null);
114
132
  const [requestedAggregateType, setRequestedAggregateType] = useState(null);
115
133
  const [drawerOpen, setDrawerOpen] = useState(false);
134
+ // The selection lives in the URL, and the router (`AppRouter`) changes the URL without a page load when a folder link is
135
+ // clicked - so it is read from the router's location, which updates, rather than once from `window.location`. Still read
136
+ // in an effect, never during the first render, so the server render and the hydrating render agree.
137
+ const search = useLocationSearch();
116
138
  useEffect(() => {
117
- const params = new URLSearchParams(window.location.search);
139
+ const params = new URLSearchParams(search);
118
140
  setRequestedMailboxUid(params.get("mailboxUid"));
119
141
  setRequestedFolderUid(params.get("folderUid"));
120
142
  setRequestedAggregateType(params.get("aggregate"));
121
- }, []);
143
+ }, [search]);
144
+ // Choosing a folder (or mailbox) changes the URL without a page load now, so the drawer that held the choice - which used to go
145
+ // with the page - is closed here.
122
146
  useEffect(() => {
123
- if (!userUid) {
124
- return;
125
- }
126
- listMailboxes({ limit: MAILBOX_LIST_LIMIT })
127
- .then((result) => {
128
- setMailboxes(result);
129
- setStatus("ready");
130
- })
131
- .catch((err) => {
132
- setError(err instanceof ApiRequestError ? err.message : "Could not load your mailboxes.");
133
- setStatus("error");
134
- });
135
- }, [userUid]);
147
+ setDrawerOpen(false);
148
+ }, [search]);
136
149
  const aggregateFolderType = isAggregateFolderType(requestedAggregateType)
137
150
  ? requestedAggregateType
138
151
  : undefined;
@@ -140,29 +153,6 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
140
153
  ? undefined
141
154
  : (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
142
155
  mailboxes[0]?.uid;
143
- // Fans out one listFolders() call per accessible mailbox in parallel - each call catches its own
144
- // failure into an MailboxFolders.error rather than letting Promise.all reject, so one mailbox's fetch
145
- // failure renders that section's own inline Alert instead of blanking out every other mailbox's
146
- // folder tree.
147
- useEffect(() => {
148
- if (mailboxes.length === 0) {
149
- // Deliberately leaves foldersLoading as-is: this also runs once before listMailboxes() has
150
- // resolved, and clearing it here would briefly render an empty sidebar the moment mailboxes
151
- // arrive, before their folders do. A genuinely mailbox-less caller gets MailboxProvisioning.
152
- setMailboxFolders([]);
153
- return;
154
- }
155
- setFoldersLoading(true);
156
- void Promise.all(mailboxes.map((mailbox) => listFolders(mailbox.uid)
157
- .then((result) => ({ mailbox, folders: result.filter((f) => MAIL_FOLDER_TYPES.has(f.type)) }))
158
- .catch((err) => ({
159
- mailbox,
160
- folders: [],
161
- error: err instanceof ApiRequestError ? err.message : "Could not load folders.",
162
- }))))
163
- .then(setMailboxFolders)
164
- .finally(() => setFoldersLoading(false));
165
- }, [mailboxes]);
166
156
  const selectedMailboxFolders = mailboxFolders.find((mf) => mf.mailbox.uid === mailboxUid)?.folders ?? [];
167
157
  const folderUid = aggregateFolderType
168
158
  ? undefined
@@ -175,23 +165,11 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
175
165
  // mailbox may still need that mailbox's own folder view opened directly to unlock/decrypt it.
176
166
  const defaultMailboxUid = mailboxes.find((mb) => mb.ownerUserUid === userUid)?.uid ?? mailboxes[0]?.uid;
177
167
  const activeMailboxUid = mailboxUid ?? defaultMailboxUid;
178
- /** Files a newly created folder under its own mailbox, leaving every other mailbox's list untouched -
179
- * and ignoring a type this sidebar doesn't list at all, the same filter the fetch above applies. */
180
- const onFolderCreated = useCallback((folder) => {
181
- if (!MAIL_FOLDER_TYPES.has(folder.type)) {
182
- return;
183
- }
184
- setMailboxFolders((prev) => prev.map((entry) =>
185
- // Not one already there: another client's create event can arrive after (or twice, or alongside) our own.
186
- entry.mailbox.uid === folder.mailboxUid && !entry.folders.some((existing) => existing.uid === folder.uid)
187
- ? { ...entry, folders: [...entry.folders, folder] }
188
- : entry));
189
- }, []);
190
- // New mail without a reload: push events for every folder, plus a safety-net poll. Held in state beside `mailboxFolders`
191
- // rather than written into it, so a refreshed unread count never looks like a change of folders to the list on screen
192
- // (which reloads, and forgets its selection, whenever the folders do).
193
- const { live, unreadCounts } = useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolderCreated });
194
- const contextValue = useMemo(() => ({ mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, live }), [mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, live]);
168
+ const counts = folderCounts.counts;
169
+ const trackMessageChange = folderCounts.track;
170
+ // `(3) Acme: Mail` in the tab strip while there is unread mail in an Inbox - kept by the frame when there is one.
171
+ useUnreadTitle(inboxUnreadTotal(mailboxFolders, counts), { enabled: !hosted });
172
+ const contextValue = useMemo(() => ({ mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange }), [mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange]);
195
173
  // A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else for a
196
174
  // mailbox-less caller to do here yet, so the icon rail/header/folder tree don't render at all. Checks
197
175
  // `mailboxes.length` directly (not `!mailboxUid`) since `mailboxUid` is legitimately undefined in
@@ -212,24 +190,32 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
212
190
  // A function, not a plain JSX constant — it's rendered twice (desktop `<aside>` + mobile
213
191
  // `Drawer`), possibly *simultaneously* mounted (the aside is only CSS-hidden below `md`, not
214
192
  // unmounted).
215
- const sidebarContent = () => (_jsxs(_Fragment, { children: [_jsx("div", { className: "p-3", children: _jsx(ComposeButton, {}) }), foldersLoading ? (_jsx("div", { className: "flex-1 overflow-y-auto px-3 pb-3", children: _jsx(SkeletonList, { count: 6, className: "pt-1" }) })) : (_jsxs("nav", { className: "flex-1 overflow-y-auto px-3 pb-3 flex flex-col gap-3", children: [mailboxes.length > 1 && (_jsxs("div", { children: [_jsx("div", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5", children: "All Mailboxes" }), _jsx("div", { className: "flex flex-col gap-0.5", children: AGGREGATE_FOLDER_TYPES.map((type) => {
216
- const unread = aggregateUnreadCount(mailboxFolders, type, unreadCounts);
193
+ const sidebarContent = () => (_jsxs(_Fragment, { children: [_jsx("div", { className: "p-3", children: _jsx(ComposeButton, { mailboxUid: defaultMailboxUid }) }), foldersLoading ? (_jsx("div", { className: "flex-1 overflow-y-auto px-3 pb-3", children: _jsx(SkeletonList, { count: 6, className: "pt-1" }) })) : (_jsxs("nav", { className: "flex-1 overflow-y-auto px-3 pb-3 flex flex-col gap-3", children: [mailboxes.length > 1 && (_jsxs("div", { children: [_jsx("div", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5", children: "All Mailboxes" }), _jsx("div", { className: "flex flex-col gap-0.5", children: AGGREGATE_FOLDER_TYPES.map((type) => {
194
+ const badge = aggregateBadge(mailboxFolders, type, counts);
217
195
  return (_jsxs("a", { href: `/?aggregate=${encodeURIComponent(type)}`, className: [
218
196
  "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
219
197
  aggregateFolderType === type
220
198
  ? "bg-primary/10 text-primary-dark font-semibold"
221
199
  : "text-text hover:bg-surface-alt",
222
- ].join(" "), children: [_jsx("span", { children: FOLDER_LABELS[type] }), unread > 0 && (_jsx("span", { className: "text-xs font-bold rounded-pill py-0.5 px-1.5 bg-surface-alt text-text-muted", children: unread }))] }, type));
223
- }) })] })), mailboxFolders.map(({ mailbox, folders, error: mailboxError }) => (_jsxs("div", { children: [_jsxs("div", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5 truncate", children: [mailbox.displayName, mailbox.ownerUserUid ? "" : " (shared)"] }), mailboxError && (_jsx("div", { className: "px-2.5 pb-1", children: _jsx(Alert, { children: mailboxError }) })), _jsx("div", { className: "flex flex-col gap-0.5", children: sortedFoldersOf(folders).map((folder) => {
224
- const unread = unreadOf(folder, unreadCounts);
200
+ ].join(" "), children: [_jsx("span", { className: badge?.kind === "unread" ? "font-semibold" : undefined, children: FOLDER_LABELS[type] }), badge && _jsx(FolderBadgeChip, { badge: badge })] }, type));
201
+ }) })] })), mailboxFolders.map(({ mailbox, folders, error: mailboxError }) => (_jsxs("div", { children: [_jsxs("div", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5 truncate", children: [mailbox.displayName, mailbox.ownerUserUid ? "" : " (shared)"] }), mailboxError && (_jsx("div", { className: "px-2.5 pb-1", children: _jsx(Alert, { children: mailboxError }) })), _jsx("div", { className: "flex flex-col gap-0.5", children: folderRows(folders, pendingCountFor(mailbox.uid, pendingSends)).map((row) => {
202
+ if (row.kind === "placeholder") {
203
+ // A folder the server has not made (or told us about) yet - the Outbox and Sent Items are made with the first message
204
+ // sent - shown the moment a message is on its way. Nothing to open; the real folder replaces it when it arrives.
205
+ const sending = pendingCountFor(mailbox.uid, pendingSends);
206
+ return (_jsxs("div", { "data-folder-placeholder": row.type, className: "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5 text-text-muted", children: [_jsx("span", { children: FOLDER_LABELS[row.type] }), row.type === "outbox" && _jsx(OutboxBadge, { total: sending, pendingHere: sending })] }, `placeholder-${row.type}`));
207
+ }
208
+ const folder = row.folder;
209
+ const badge = badgeFor(folder.type, countOfFolder(folder, counts));
225
210
  return (_jsxs("a", { href: `/?mailboxUid=${encodeURIComponent(mailbox.uid)}&folderUid=${encodeURIComponent(folder.uid)}`, className: [
226
211
  "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
227
212
  folder.uid === folderUid
228
213
  ? "bg-primary/10 text-primary-dark font-semibold"
229
214
  : "text-text hover:bg-surface-alt",
230
- ].join(" "), children: [_jsx("span", { children: FOLDER_LABELS[folder.type] ?? folder.name }), unread > 0 && (_jsx("span", { className: "text-xs font-bold rounded-pill py-0.5 px-1.5 bg-surface-alt text-text-muted", children: unread }))] }, folder.uid));
215
+ ].join(" "), children: [_jsx("span", { className: badge?.kind === "unread" ? "font-semibold" : undefined, children: FOLDER_LABELS[folder.type] ?? folder.name }), badge &&
216
+ (folder.type === "outbox" ? (_jsx(OutboxBadge, { total: badge.value, status: outboxStatuses[folder.uid], pendingHere: pendingCountFor(mailbox.uid, pendingSends) })) : (_jsx(FolderBadgeChip, { badge: badge })))] }, folder.uid));
231
217
  }) })] }, mailbox.uid)))] }))] }));
232
- inner = (_jsxs(_Fragment, { children: [_jsx("aside", { className: "hidden md:flex w-64 shrink-0 bg-surface border-r border-border flex-col", children: sidebarContent() }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Folders", children: _jsx("div", { className: "flex flex-col", children: sidebarContent() }) }), _jsxs("main", { className: "flex-1 min-w-0 overflow-y-auto", children: [_jsx("button", { type: "button", className: "md: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 folders", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(MailShellContext.Provider, { value: contextValue, children: children })] })] }));
218
+ inner = (_jsxs(_Fragment, { children: [_jsx(MailShortcuts, { mailboxUid: defaultMailboxUid }), _jsx("aside", { className: "hidden md:flex w-64 shrink-0 bg-surface border-r border-border flex-col", children: sidebarContent() }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Folders", children: _jsx("div", { className: "flex flex-col", children: sidebarContent() }) }), _jsxs("main", { className: "flex-1 min-w-0 overflow-y-auto", children: [_jsx("button", { type: "button", className: "md: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 folders", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(MailShellContext.Provider, { value: contextValue, children: children })] })] }));
233
219
  }
234
220
  // Wraps AppShell unconditionally, at a stable tree position regardless of whether mailboxUid has
235
221
  // resolved yet - KeyEnrollmentGate itself passes `children` through untouched until a real
@@ -251,5 +237,5 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
251
237
  canProvision: !impersonating && activeMailbox?.ownerUserUid === userUid, children: [_jsx(LocalIndexLifecycle, { mailboxUid: activeMailboxUid, mailboxKeys: activeMailbox?.keys, folders: mailboxFolders.find((mf) => mf.mailbox.uid === activeMailboxUid)?.folders ?? [],
252
238
  // A full page may be truncated - pruning against it would delete indexes of accessible mailboxes
253
239
  // beyond the limit, so it's only trusted as the complete list when it came back short.
254
- accessibleMailboxUids: status === "ready" && mailboxes.length < MAILBOX_LIST_LIMIT ? mailboxes.map((mb) => mb.uid) : undefined }), _jsx(AppShell, { active: "mail", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, pluginNav: pluginNav, children: inner })] }));
240
+ accessibleMailboxUids: status === "ready" && mailboxes.length < MAILBOX_LIST_LIMIT ? mailboxes.map((mb) => mb.uid) : undefined }), _jsx(AppShell, { active: "mail", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner })] }));
255
241
  }
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ export interface EncryptedBodyProps {
3
+ /** This device has no unlocked key session for the message's mailbox: unlocking could help. */
4
+ locked: boolean;
5
+ /** Why the message couldn't be decrypted when the keys were available (shown for the failed state). */
6
+ reason?: string;
7
+ /** Opens the app's unlock prompt (`useUnlockPrompt().requestUnlock`). Only used - and only offered - when `locked`. */
8
+ onUnlock: () => void;
9
+ /** An unlock is being asked for: the button waits. */
10
+ unlocking?: boolean;
11
+ }
12
+ /**
13
+ * What an encrypted message's body is until (and unless) it can be read, drawn as card content in the app's own tokens: on the theme's opaque surface, so
14
+ * it stays legible over a translucent card and a background photo.
15
+ *
16
+ * **Locked** - the keys are not unlocked: a lock, "This message is encrypted", a hint and a primary Unlock button, which opens the same prompt compose's
17
+ * "Unlock to sign or encrypt" does. When it succeeds the pane decrypts and shows the message in place.
18
+ * **Cannot be decrypted** - the keys are unlocked and it still can't be opened (no key for this mailbox, not a recipient, damaged, unsupported): the reason,
19
+ * and no button - unlocking again wouldn't change it.
20
+ */
21
+ export default function EncryptedBody({ locked, reason, onUnlock, unlocking }: EncryptedBodyProps): React.JSX.Element;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { HiOutlineExclamationTriangle, HiOutlineLockClosed } from "react-icons/hi2";
3
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
4
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
5
+ import { useResolvedTheme } from "../../../appearance/resolvedTheme.js";
6
+ import { formatColour } from "./color.js";
7
+ import { useThemeSurface } from "./themeSurface.js";
8
+ /**
9
+ * What an encrypted message's body is until (and unless) it can be read, drawn as card content in the app's own tokens: on the theme's opaque surface, so
10
+ * it stays legible over a translucent card and a background photo.
11
+ *
12
+ * **Locked** - the keys are not unlocked: a lock, "This message is encrypted", a hint and a primary Unlock button, which opens the same prompt compose's
13
+ * "Unlock to sign or encrypt" does. When it succeeds the pane decrypts and shows the message in place.
14
+ * **Cannot be decrypted** - the keys are unlocked and it still can't be opened (no key for this mailbox, not a recipient, damaged, unsupported): the reason,
15
+ * and no button - unlocking again wouldn't change it.
16
+ */
17
+ export default function EncryptedBody({ locked, reason, onUnlock, unlocking = false }) {
18
+ const theme = useResolvedTheme();
19
+ const surface = useThemeSurface(theme);
20
+ const Icon = locked ? HiOutlineLockClosed : HiOutlineExclamationTriangle;
21
+ return (_jsxs("div", { className: "flex flex-col items-center gap-2 rounded-md border border-border px-4 py-8 text-center", style: { backgroundColor: formatColour(surface.background) }, children: [_jsx("span", { "aria-hidden": "true", className: ["inline-flex h-12 w-12 items-center justify-center rounded-full", locked ? "bg-primary/10 text-primary-dark" : "bg-warning/15 text-text"].join(" "), children: _jsx(Icon, { size: 24 }) }), _jsx("p", { className: "text-base font-semibold text-text", children: locked ? "This message is encrypted" : "This message can’t be decrypted" }), locked ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm text-text-muted", children: "Unlock your keys to read it" }), _jsx(Button, { type: "button", className: "!w-auto mt-1", "aria-label": "Unlock to view this message", loading: unlocking, disabled: unlocking, onClick: onUnlock, children: "Unlock" })] })) : (reason && (_jsx("div", { className: "mt-1 max-w-prose text-left", children: _jsx(Alert, { children: reason }) })))] }));
22
+ }
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+ /** The subject an encrypted message carries on the server (RFC 9788's placeholder): the real one is inside the ciphertext. */
3
+ export declare const ENCRYPTED_SUBJECT_PLACEHOLDER = "[...]";
4
+ /**
5
+ * What a list row's preview line says for an encrypted message nothing has been decrypted of yet: a small lock and "Encrypted message". The server never
6
+ * derives a preview for encrypted mail (it has no plaintext), so the line would otherwise be empty.
7
+ */
8
+ export declare function EncryptedPreview(): React.JSX.Element;
9
+ /**
10
+ * Whether a conversation row stands for an encrypted latest message. A conversation summary carries no flag for it, but an encrypted message has no preview
11
+ * and the placeholder subject - which a plaintext message with an empty body does not.
12
+ */
13
+ export declare function conversationLooksEncrypted(conversation: {
14
+ subject: string;
15
+ latestPreview: string;
16
+ }): boolean;
17
+ /** The subject to show for a message's own: what an encrypted message says is the placeholder, which nobody should have to read as a title. */
18
+ export declare function displaySubject(subject: string): string;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { HiOutlineLockClosed } from "react-icons/hi2";
3
+ /** The subject an encrypted message carries on the server (RFC 9788's placeholder): the real one is inside the ciphertext. */
4
+ export const ENCRYPTED_SUBJECT_PLACEHOLDER = "[...]";
5
+ /**
6
+ * What a list row's preview line says for an encrypted message nothing has been decrypted of yet: a small lock and "Encrypted message". The server never
7
+ * derives a preview for encrypted mail (it has no plaintext), so the line would otherwise be empty.
8
+ */
9
+ export function EncryptedPreview() {
10
+ return (_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx(HiOutlineLockClosed, { size: 12, "aria-hidden": "true", className: "shrink-0" }), "Encrypted message"] }));
11
+ }
12
+ /**
13
+ * Whether a conversation row stands for an encrypted latest message. A conversation summary carries no flag for it, but an encrypted message has no preview
14
+ * and the placeholder subject - which a plaintext message with an empty body does not.
15
+ */
16
+ export function conversationLooksEncrypted(conversation) {
17
+ return conversation.latestPreview === "" && conversation.subject === ENCRYPTED_SUBJECT_PLACEHOLDER;
18
+ }
19
+ /** The subject to show for a message's own: what an encrypted message says is the placeholder, which nobody should have to read as a title. */
20
+ export function displaySubject(subject) {
21
+ return subject === ENCRYPTED_SUBJECT_PLACEHOLDER ? "Encrypted message" : subject;
22
+ }
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+ import type { Attachment } from "@rapidmx/react-shared/mail/mailApi.js";
3
+ import type { MimeAttachment } from "@rapidmx/react-shared/crypto/mime.js";
4
+ import { BodyContent } from "./bodyContent.js";
5
+ export interface MessageBodyProps {
6
+ messageUid: string;
7
+ /** Part of what a remembered body is keyed on, so an edited message is fetched again. */
8
+ messageVersion: number;
9
+ /** The accessible name of the body. */
10
+ title: string;
11
+ /** A body the caller already has - decrypted or verified content. Shown instead of asking the server. */
12
+ content?: BodyContent;
13
+ /** The message's server-side attachments, for resolving its inline `cid:` images. */
14
+ attachments?: Attachment[];
15
+ /** The parts inside a signed or encrypted message, for resolving its inline images from the decrypted content. */
16
+ inlineParts?: MimeAttachment[];
17
+ /** Show the message as its author wrote it: nothing adapted to the theme. */
18
+ original?: boolean;
19
+ /** Says whether "view original" would change anything, once that is known: never for plain text, a message with its own dark styles, or a
20
+ * message the theme leaves as it is. */
21
+ onAdaptable?: (adaptable: boolean) => void;
22
+ }
23
+ /** Placeholder lines shown where the body will be, in the body's own place, until it has arrived and been laid out. */
24
+ export declare function BodySkeleton(): React.JSX.Element;
25
+ /**
26
+ * The body of one message, in the card's own place: the sanitized HTML in a frame that is exactly as tall as its content (following it as
27
+ * images load and the width changes), or plain text. See `frameDocument.ts` for the isolation and `themeAdaptation.ts` for how a message
28
+ * takes the app's theme.
29
+ *
30
+ * Nothing waits on the network to lay out: the skeleton is here at once, and the frame's height is only ever the height of what is in it.
31
+ */
32
+ export default function MessageBody({ messageUid, messageVersion, title, content, attachments, inlineParts, original, onAdaptable }: MessageBodyProps): React.JSX.Element;
@@ -0,0 +1,136 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useMemo, useRef, useState } from "react";
7
+ import { ApiRequestError, apiUrl } from "@rapidmx/react-shared/util/api.js";
8
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
9
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
10
+ import Skeleton from "@rapidmx/react-shared/components/feedback/Skeleton.js";
11
+ import { useResolvedTheme } from "../../../appearance/resolvedTheme.js";
12
+ import { attachmentSource, cachedBodyContent, fetchBodyContent, makeCidResolver } from "./bodyContent.js";
13
+ import { prepareBodyHtml } from "./bodyHtml.js";
14
+ import { formatColour } from "./color.js";
15
+ import { controlFrame } from "./frameControl.js";
16
+ import { FRAME_SANDBOX, buildFrameDocument } from "./frameDocument.js";
17
+ import { surfaceKey, useThemeSurface } from "./themeSurface.js";
18
+ /** Placeholder lines shown where the body will be, in the body's own place, until it has arrived and been laid out. */
19
+ export function BodySkeleton() {
20
+ return (_jsxs("div", { role: "status", "aria-busy": "true", className: "flex flex-col gap-2.5 py-1", children: [_jsx("span", { className: "sr-only", children: "Loading the message" }), _jsx(Skeleton, { width: "w-11/12" }), _jsx(Skeleton, { width: "w-4/5" }), _jsx(Skeleton, { width: "w-9/12" }), _jsx(Skeleton, { width: "w-1/2" })] }));
21
+ }
22
+ /** A short, stable fingerprint of a document's text (djb2), to tell one loaded document from the next. */
23
+ function fingerprint(text) {
24
+ let hash = 5381;
25
+ for (let i = 0; i < text.length; i++) {
26
+ hash = ((hash << 5) + hash + text.charCodeAt(i)) | 0;
27
+ }
28
+ return (hash >>> 0).toString(36);
29
+ }
30
+ /** The background of a message shown as authored: white, the canvas an HTML message is written for. */
31
+ const AUTHORED_CANVAS = "rgb(255, 255, 255)";
32
+ /**
33
+ * The body of one message, in the card's own place: the sanitized HTML in a frame that is exactly as tall as its content (following it as
34
+ * images load and the width changes), or plain text. See `frameDocument.ts` for the isolation and `themeAdaptation.ts` for how a message
35
+ * takes the app's theme.
36
+ *
37
+ * Nothing waits on the network to lay out: the skeleton is here at once, and the frame's height is only ever the height of what is in it.
38
+ */
39
+ export default function MessageBody({ messageUid, messageVersion, title, content, attachments, inlineParts, original = false, onAdaptable }) {
40
+ const [remote, setRemote] = useState(() => {
41
+ const cached = cachedBodyContent(messageUid, messageVersion);
42
+ return cached ? { status: "ready", content: cached } : { status: "loading" };
43
+ });
44
+ const [attempt, setAttempt] = useState(0);
45
+ const supplied = content !== undefined;
46
+ useEffect(() => {
47
+ if (supplied) {
48
+ return;
49
+ }
50
+ const cached = cachedBodyContent(messageUid, messageVersion);
51
+ if (cached) {
52
+ setRemote({ status: "ready", content: cached });
53
+ return;
54
+ }
55
+ const controller = new AbortController();
56
+ setRemote({ status: "loading" });
57
+ fetchBodyContent(messageUid, messageVersion, controller.signal).then((loaded) => setRemote({ status: "ready", content: loaded }), (err) => {
58
+ if (!controller.signal.aborted) {
59
+ setRemote({ status: "error", message: err instanceof ApiRequestError ? err.message : "Could not load this message." });
60
+ }
61
+ });
62
+ return () => controller.abort();
63
+ }, [messageUid, messageVersion, supplied, attempt]);
64
+ const shown = content ? { status: "ready", content } : remote;
65
+ if (shown.status === "loading") {
66
+ return _jsx(BodySkeleton, {});
67
+ }
68
+ if (shown.status === "error") {
69
+ return (_jsxs("div", { className: "flex flex-col items-start gap-2", children: [_jsx(Alert, { children: shown.message }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setAttempt((n) => n + 1), children: "Try again" })] }));
70
+ }
71
+ if (shown.content.kind === "text") {
72
+ // Rendered as text - React escapes it - never as markup, in the theme's own colours.
73
+ return _jsx("pre", { "aria-label": title, className: "m-0 text-sm font-sans whitespace-pre-wrap break-words text-text", children: shown.content.text });
74
+ }
75
+ return (_jsx(HtmlBody, { messageUid: messageUid, title: title, html: shown.content.html, remoteAvailable: !supplied, attachments: attachments, inlineParts: inlineParts, original: original, onAdaptable: onAdaptable }, messageUid));
76
+ }
77
+ function HtmlBody({ messageUid, title, html, remoteAvailable, attachments, inlineParts, original, onAdaptable, }) {
78
+ const theme = useResolvedTheme();
79
+ const surface = useThemeSurface(theme);
80
+ // Keyed on what the resolver reads rather than on the arrays, which a caller may rebuild on every render.
81
+ const inlineKey = [...(attachments ?? []).map((a) => `${a.uid}:${a.contentId ?? ""}`), inlineParts ? `parts:${inlineParts.length}` : ""].join("|");
82
+ const resolveCid = useMemo(() => makeCidResolver(attachments, inlineParts), [inlineKey]);
83
+ const prepared = useMemo(() => prepareBodyHtml(html, { resolveCid }), [html, resolveCid]);
84
+ const mode = prepared.declaresDarkSupport ? "native" : original ? "original" : "adapt";
85
+ const surfaceCss = formatColour(surface.background);
86
+ const srcDoc = useMemo(() => {
87
+ // The frame may load images from this server's attachment URLs only when an inline image was actually resolved to one.
88
+ const source = prepared.inlineImages > 0 ? attachmentSource() : undefined;
89
+ return buildFrameDocument(prepared.html, {
90
+ mode,
91
+ scheme: theme,
92
+ surface: surfaceCss,
93
+ text: formatColour(surface.text),
94
+ attachmentSource: source && prepared.html.includes(source) ? source : undefined,
95
+ });
96
+ }, [prepared, mode, theme, surfaceCss, surface.text]);
97
+ // A different body, surface or mode is a different document: the adaptation pass edits the document it ran on, so it is run on a fresh
98
+ // one, and the old one's readiness is not the new one's.
99
+ const frameKey = useMemo(() => `${mode}:${surfaceKey(surface)}:${fingerprint(srcDoc)}`, [mode, surface, srcDoc]);
100
+ const [height, setHeight] = useState(undefined);
101
+ const [readyFor, setReadyFor] = useState(null);
102
+ const [canvasAuthored, setCanvasAuthored] = useState(false);
103
+ const dispose = useRef(undefined);
104
+ useEffect(() => () => dispose.current?.(), []);
105
+ const reportAdaptable = useRef(onAdaptable);
106
+ reportAdaptable.current = onAdaptable;
107
+ useEffect(() => {
108
+ if (mode === "native") {
109
+ reportAdaptable.current?.(false);
110
+ }
111
+ }, [mode]);
112
+ if (prepared.status === "too_large") {
113
+ return (_jsxs(Alert, { children: ["This message is too large to show here.", " ", remoteAvailable && (_jsx("a", { href: apiUrl(`/mail/messages/${encodeURIComponent(messageUid)}/content`), target: "_blank", rel: "noopener noreferrer", className: "font-medium underline", children: "Open it in its own tab" }))] }));
114
+ }
115
+ const ready = readyFor === frameKey;
116
+ const canvas = mode === "original" ? AUTHORED_CANVAS : surfaceCss;
117
+ const framed = mode !== "adapt" || canvasAuthored;
118
+ return (_jsxs("div", { className: "relative rounded-md overflow-hidden", style: { backgroundColor: canvas }, children: [!ready && _jsx(BodySkeleton, {}), _jsx("iframe", { title: title, srcDoc: srcDoc, sandbox: FRAME_SANDBOX, scrolling: "no", onLoad: (event) => {
119
+ dispose.current?.();
120
+ dispose.current = controlFrame(event.currentTarget, {
121
+ surface: mode === "adapt" ? surface : undefined,
122
+ onHeight: setHeight,
123
+ onReady: (adaptation) => {
124
+ setReadyFor(frameKey);
125
+ if (adaptation) {
126
+ setCanvasAuthored(adaptation.authoredCanvas);
127
+ reportAdaptable.current?.(adaptation.material);
128
+ }
129
+ },
130
+ });
131
+ }, className: [
132
+ "block w-full border-0 rounded-md",
133
+ ready ? "" : "absolute inset-x-0 top-0 invisible",
134
+ framed && ready ? "ring-1 ring-border" : "",
135
+ ].join(" "), style: { height: height ?? 0 } }, frameKey)] }));
136
+ }