@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
@@ -4,14 +4,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useRef, useState } from "react";
7
- import { HiOutlineArchiveBox, HiOutlineArrowUturnLeft, HiOutlineArrowUturnRight, HiOutlineCheck, HiOutlineExclamationTriangle, HiOutlineFolderArrowDown, HiOutlineLockClosed, } from "react-icons/hi2";
7
+ import { HiOutlineArchiveBox, HiOutlineArrowUturnLeft, HiOutlineArrowUturnRight, HiOutlineCheck, HiOutlineExclamationTriangle, HiOutlineFolderArrowDown, HiOutlineLockClosed, HiOutlineMoon, HiOutlineSun, } from "react-icons/hi2";
8
8
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
9
9
  import { approveReceipt, archiveMessage, attachmentContentUrl, cancelScheduledSend, declineReceipt, getMailbox, getMessage, getMessageRawContent, moveMessage, recallMessage, setMessageLabels, } from "@rapidmx/react-shared/mail/mailApi.js";
10
10
  import { buildForwardQuote, buildReplyQuote, buildReplyRecipients, buildReplyThreading, forwardSubject, replySubject, } from "@rapidmx/react-shared/mail/compose/composeQuoting.js";
11
- import { sanitizeMessageBodyHtml } from "@rapidmx/react-shared/mail/messageBodySanitizer.js";
12
11
  import MoveToFolderDialog from "./MoveToFolderDialog.js";
13
12
  import { getUnlockedKeys, subscribeKeySession } from "@rapidmx/react-shared/crypto/keySession.js";
14
- import { evaluateMessageSecurity, evaluateMessageSecurityWithSeal, } from "@rapidmx/react-shared/crypto/messageSecurity.js";
15
13
  import { extractAddresses } from "@rapidmx/react-shared/crypto/mime.js";
16
14
  import { SignerKeyConflictError, signingKeyFingerprints, trustSigner } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
17
15
  import { isLikelyMailingList } from "@rapidmx/react-shared/crypto/composeSecurity.js";
@@ -20,18 +18,28 @@ import { currentVerificationSeal, getVaultGeneration, sendVerificationSeal } fro
20
18
  import { getMyMailboxAccess } from "@rapidmx/react-shared/mail/mailboxAccessApi.js";
21
19
  import KeyChangeReview from "../contacts/KeyChangeReview.js";
22
20
  import { KEY_CHANGE_STALE_MESSAGE, sameFingerprint } from "../contacts/contactKeys.js";
23
- import { useCompose } from "./compose/ComposeContext.js";
24
- import { loadOriginalMessage } from "./compose/quotedBody.js";
21
+ import { prefetchComposeWindow, useCompose } from "./compose/ComposeContext.js";
22
+ import { loadOriginalMessage, prefetchOriginalMessage } from "./compose/quotedBody.js";
25
23
  import { formatRecipient } from "./compose/recipients.js";
26
24
  import { formatMailAddress } from "@rapidmx/react-shared/mail/mailAddress.js";
27
- import { RecipientLine } from "./MailAddress.js";
25
+ import MailAddress, { RecipientLine } from "./MailAddress.js";
28
26
  import { useMailShell } from "./layout/MailShell.js";
27
+ import { ariaKeyShortcuts, withHint } from "../../keyboard/format.js";
28
+ import { SHORTCUTS } from "../../keyboard/keymap.js";
29
+ import { useKeyEnvironment } from "../../keyboard/ShortcutProvider.js";
30
+ import { useShortcut } from "../../keyboard/useShortcut.js";
29
31
  import { useUnlockPrompt } from "../layout/UnlockPromptProvider.js";
30
32
  import { moveLocalEntity } from "../../search/localIndexRpcClient.js";
31
33
  import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
32
34
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
33
35
  import LabelMenuButton from "./labelMenu.js";
34
36
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
37
+ import EncryptedBody from "./reading/EncryptedBody.js";
38
+ import { displaySubject } from "./reading/EncryptedPreview.js";
39
+ import MessageBody, { BodySkeleton } from "./reading/MessageBody.js";
40
+ import { BODY_FONT_STYLE, CardShell, SenderAvatar, SubjectCard } from "./reading/MessageCard.js";
41
+ import { useViewOriginal } from "./reading/viewOriginal.js";
42
+ import { ROW_FOCUS_CLASS, UnreadLabel, dateClass, senderClass } from "./unreadStyle.js";
35
43
  /** Labels/styling for `specs/end-to-end_encryption.md`'s "Message Security Indicators" table - kept as
36
44
  * plain data (not JSX) so `SecurityIndicator` below stays a trivial lookup. "Signature failed" MUST NOT
37
45
  * read as a muted variant of "verified" (a failed signature is a stronger negative than no signature at
@@ -131,13 +139,16 @@ function downloadMimeAttachment(attachment) {
131
139
  * A plain `<button>` rather than react-shared's `Button`, whose padding and minimum width are sized for a
132
140
  * text label. Ordinary DOM order, so the keyboard reaches these in the order they are read.
133
141
  */
134
- function IconAction({ icon, label, onClick, disabled, busy, }) {
135
- return (_jsx("button", { type: "button", "aria-label": label, title: label, "aria-busy": busy || undefined, disabled: disabled, onClick: onClick, className: "inline-flex items-center justify-center p-1.5 rounded-md border border-border text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent", children: icon }));
142
+ function IconAction({ icon, label, onClick, disabled, busy, onPrefetch, shortcut, }) {
143
+ const env = useKeyEnvironment();
144
+ return (_jsx("button", { type: "button", "aria-label": label, title: shortcut ? withHint(label, shortcut, env) : label, "aria-keyshortcuts": shortcut ? ariaKeyShortcuts(shortcut, env) : undefined, "aria-busy": busy || undefined, disabled: disabled, onClick: onClick, onPointerEnter: onPrefetch, onFocus: onPrefetch, className: "inline-flex items-center justify-center p-1.5 rounded-md border border-border text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent", children: icon }));
136
145
  }
137
146
  function SecurityIndicator({ security }) {
138
147
  const { label, className } = SECURITY_INDICATOR[security.state];
139
148
  if (security.state !== "verified_at_first_open") {
140
- return _jsx("span", { className: `text-xs font-medium shrink-0 py-1 px-2.5 rounded-pill ${className}`, children: label });
149
+ // Every encrypted state carries a small lock, so the badge reads at a glance; the words are unchanged.
150
+ const encrypted = security.state.startsWith("encrypted");
151
+ return (_jsxs("span", { className: `inline-flex items-center gap-1 text-xs font-medium shrink-0 py-1 px-2.5 rounded-pill ${className}`, children: [encrypted && _jsx(HiOutlineLockClosed, { size: 12, "aria-hidden": "true" }), label] }));
141
152
  }
142
153
  return (_jsxs("span", { className: `inline-flex items-center gap-1 text-xs font-medium shrink-0 py-1 px-2.5 rounded-pill ${security.laterCompromised ? LATER_COMPROMISED_INDICATOR_CLASS : className}`, children: [security.laterCompromised ? (_jsx(HiOutlineExclamationTriangle, { size: 12, "aria-hidden": "true" })) : (_jsx(HiOutlineCheck, { size: 12, "aria-hidden": "true" })), label] }));
143
154
  }
@@ -156,14 +167,13 @@ const GENERIC_SIGNATURE_FAILURE_MESSAGE = "This message's digital signature coul
156
167
  /** Fallback when the raw MIME for an encrypted message can't even be fetched/evaluated - an encrypted
157
168
  * message must never be mislabeled "Unprotected" just because loading failed. */
158
169
  const ENCRYPTED_LOAD_ERROR = "Couldn't load this message's encrypted content.";
159
- /** Content Security Policy prepended to every client-rendered (decrypted/verified) body's `srcDoc` - the
160
- * backstop behind the sanitizer below: nothing in the document may load any remote resource at all, only
161
- * inline styles and `data:`/`cid:` images. */
162
- const BODY_CSP_META = "<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; img-src data: cid:; style-src 'unsafe-inline'\">";
163
- /** Sanitizes a client-rendered body for `srcDoc` (DOMPurify plus every remote resource reference stripped - see
164
- * react-shared's `messageBodySanitizer.ts`), with the CSP meta as its very first element. */
165
- export function buildSecureSrcDoc(html) {
166
- return BODY_CSP_META + sanitizeMessageBodyHtml(html);
170
+ /** A subject without its \`Re:\`/\`Fwd:\` prefixes, lower-cased - what makes two messages of one thread the same subject. */
171
+ function subjectCore(subject) {
172
+ return (subject ?? "").replace(/^(\s*(re|fw|fwd|aw|sv)\s*:)+/i, "").trim().toLowerCase();
173
+ }
174
+ /** Whether a message's subject says nothing its thread's subject doesn't. */
175
+ function sameSubject(subject, threadSubject) {
176
+ return subjectCore(subject) === subjectCore(threadSubject);
167
177
  }
168
178
  function formatBytes(bytes) {
169
179
  if (bytes >= 1000000)
@@ -173,7 +183,8 @@ function formatBytes(bytes) {
173
183
  return `${bytes} B`;
174
184
  }
175
185
  /**
176
- * A message's reading pane — header (subject/from/to/attachments) plus a sandboxed iframe for the body.
186
+ * A message's reading pane - a subject card, and under it the message as a card: header (sender, recipients, time, actions), notices, attachments,
187
+ * the body (`reading/MessageBody`: an isolated frame exactly as tall as its content, or text) and a footer. Rendered with `inThread`, only the card.
177
188
  * Shared by the desktop inline pane (`apps/www/index.tsx`, always visible alongside the message list),
178
189
  * and the mobile detail route (`apps/www/messages/[uid].tsx`, a full page on its own reached by tapping
179
190
  * a message row) — see each call site for how `message`/`attachments`/`isSentItems` are sourced. The
@@ -195,7 +206,7 @@ export default function MessageDetailPane(props) {
195
206
  function needsRawSecurityEvaluation(message) {
196
207
  return !!message.encrypted || message.hasAttachments;
197
208
  }
198
- function MessageDetailContent({ message: messageProp, attachments, backHref, isSentItems, onRecalled, isOutbox, draftsFolderUid, onScheduledSendCanceled, folders, onMoved, onFolderCreated, onReceiptHandled, onArchived, labels, onLabelsChanged, onLabelCreated, inThread, }) {
209
+ function MessageDetailContent({ message: messageProp, attachments, backHref, isSentItems, onRecalled, isOutbox, draftsFolderUid, onScheduledSendCanceled, folders, onMoved, onFolderCreated, onReceiptHandled, onArchived, labels, onLabelsChanged, onLabelCreated, inThread, threadSubject, threadHeader, footer, shortcuts, }) {
199
210
  // A copy re-read from the server after an Outbox action was refused (409/403) or a send lease ran out - see
200
211
  // `reloadMessage()`. A prop copy newer by `version` wins; an equal-version reload is kept, since claiming a send
201
212
  // lease need not bump `version`.
@@ -239,12 +250,20 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
239
250
  const [keyChangeNotice, setKeyChangeNotice] = useState(null);
240
251
  // "Now", for deciding whether a scheduled send's lease is still live - advanced when the lease runs out.
241
252
  const [nowMs, setNowMs] = useState(() => Date.now());
242
- const { mailboxes } = useMailShell();
253
+ const { mailboxes, trackMessageChange } = useMailShell();
243
254
  const { requestUnlock } = useUnlockPrompt();
244
255
  // Bumped after a successful on-demand unlock to re-run the effect below - it's not a dependency the
245
256
  // effect could read reactively otherwise (getUnlockedKeys() is a plain module-level read, not React
246
257
  // state; see keySession.ts's own doc comment).
247
258
  const [unlockRefresh, setUnlockRefresh] = useState(0);
259
+ // "View original": this message exactly as its author wrote it instead of adapted to the theme, remembered for the session. Offered only
260
+ // once the body says adapting would change something (`MessageBody`'s `onAdaptable`), or when the reader already chose it.
261
+ const [viewOriginal, setViewOriginal] = useViewOriginal(messageProp.uid);
262
+ const [adaptable, setAdaptable] = useState(viewOriginal);
263
+ // The locked body's Unlock button is waiting on the unlock prompt; and, once it has answered, the focus is to move to the body region.
264
+ const [unlocking, setUnlocking] = useState(false);
265
+ const focusBodyRef = useRef(false);
266
+ const bodyRegionRef = useRef(null);
248
267
  // The newest copy of this message known to the Labels popover - normally just the `message` prop,
249
268
  // but a successful label toggle's server response is held here too, so a follow-up toggle always
250
269
  // builds on the latest `labelUids`/`version` even before (or without) the caller patching its own
@@ -293,6 +312,9 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
293
312
  if (cancelled) {
294
313
  return;
295
314
  }
315
+ // The S/MIME code (PKI.js and the ASN.1/X.509 libraries) loads here, only for a message that is signed or
316
+ // encrypted - it is over half a megabyte, and the pane opens plain messages without it.
317
+ const messageSecurity = await import("@rapidmx/react-shared/crypto/messageSecurity.js");
296
318
  const [primaryAddress, ...aliasAddresses] = readerAddressesKey ? readerAddressesKey.split(" ") : [];
297
319
  const ownAddresses = readerAddressesKey.toLowerCase().split(" ");
298
320
  const ownKeys = ownAddresses.includes(senderAddress.toLowerCase()) ? (readerMailbox.keys ?? []) : [];
@@ -310,7 +332,7 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
310
332
  ...(sealContext.keyState?.previous ?? []),
311
333
  ...ownKeys,
312
334
  ];
313
- result = await evaluateMessageSecurityWithSeal(rawMime, unlocked, pinned, primaryAddress, {
335
+ result = await messageSecurity.evaluateMessageSecurityWithSeal(rawMime, unlocked, pinned, primaryAddress, {
314
336
  mailboxUid: message.mailboxUid,
315
337
  messageUid: message.uid,
316
338
  ...currentVerificationSeal(message),
@@ -318,14 +340,14 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
318
340
  signerKeys,
319
341
  }).catch(() =>
320
342
  // Only a lock while sealing throws: evaluate as locked (the lock itself re-evaluates too).
321
- evaluateMessageSecurity(rawMime, undefined, pinned, primaryAddress));
343
+ messageSecurity.evaluateMessageSecurity(rawMime, undefined, pinned, primaryAddress));
322
344
  if (result.sealToWrite) {
323
345
  // Best effort and in the background: never awaited, never shown.
324
346
  void sendVerificationSeal(message.uid, result.sealToWrite);
325
347
  }
326
348
  }
327
349
  else {
328
- result = await evaluateMessageSecurity(rawMime, unlocked, pinned, primaryAddress);
350
+ result = await messageSecurity.evaluateMessageSecurity(rawMime, unlocked, pinned, primaryAddress);
329
351
  }
330
352
  // Only one reader address can be checked per evaluation: a message sent to one of this mailbox's
331
353
  // aliases isn't "not addressed to you", so each alias is tried before saying so.
@@ -333,7 +355,7 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
333
355
  if (!result.notAddressedToReader) {
334
356
  break;
335
357
  }
336
- const viaAlias = await evaluateMessageSecurity(rawMime, unlocked, pinned, alias);
358
+ const viaAlias = await messageSecurity.evaluateMessageSecurity(rawMime, unlocked, pinned, alias);
337
359
  result = { ...result, notAddressedToReader: viaAlias.notAddressedToReader };
338
360
  }
339
361
  const unpinned = contactPins.loaded && pins.length === 0;
@@ -358,14 +380,32 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
358
380
  cancelled = true;
359
381
  };
360
382
  }, [message.uid, message.mailboxUid, message.from.address, message.encrypted, rawEvaluationNeeded, unlockRefresh, readerAddressesKey]);
383
+ // Whether the body is the locked state right now (an encrypted message this device holds no unlocked keys for), for the subscription below.
384
+ const lockedRef = useRef(false);
385
+ lockedRef.current = security?.decryptError !== undefined && !getUnlockedKeys(message.mailboxUid);
386
+ // After an unlock from the button, the message (or the reason it can't be read) is where the reader's attention is: the focus moves to it once it is
387
+ // there, since the button that had it is gone.
388
+ useEffect(() => {
389
+ if (focusBodyRef.current && security !== null) {
390
+ focusBodyRef.current = false;
391
+ bodyRegionRef.current?.focus({ preventScroll: true });
392
+ }
393
+ }, [security]);
361
394
  // Decrypted plaintext must not outlive the key session that produced it: the moment this mailbox's
362
395
  // keys are destroyed (logout, idle timeout, explicit lock), drop the recovered html/text and
363
- // re-evaluate - an encrypted message then falls back to its "Unlock to view" state.
396
+ // re-evaluate - an encrypted message then falls back to its locked state. And the other way round: keys unlocked
397
+ // from anywhere (compose, another message, the unlock prompt) open a message that was waiting on them, in place.
364
398
  useEffect(() => subscribeKeySession((event) => {
365
- if (event.mailboxUid === message.mailboxUid && event.state === "locked") {
399
+ if (event.mailboxUid !== message.mailboxUid) {
400
+ return;
401
+ }
402
+ if (event.state === "locked") {
366
403
  setSecurity(null);
367
404
  setUnlockRefresh((n) => n + 1);
368
405
  }
406
+ else if (lockedRef.current) {
407
+ setUnlockRefresh((n) => n + 1);
408
+ }
369
409
  }), [message.mailboxUid]);
370
410
  // While restapi's ScheduledSendJob holds a send lease the message is being relayed: cancelling or moving it would be
371
411
  // refused (409/403), so the Outbox controls are replaced by "Sending...". When the lease runs out the message is
@@ -398,13 +438,19 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
398
438
  async function handleUnlockToView() {
399
439
  const mailboxUid = message.mailboxUid;
400
440
  const mailboxKeys = mailboxes.find((mb) => mb.uid === mailboxUid)?.keys ?? [];
441
+ setUnlocking(true);
401
442
  try {
402
443
  await requestUnlock(mailboxUid, mailboxKeys);
444
+ // The Unlock button is about to be replaced by the message (or by why it can't be read): the focus goes there, once it has been evaluated.
445
+ focusBodyRef.current = true;
403
446
  setUnlockRefresh((n) => n + 1);
404
447
  }
405
448
  catch {
406
449
  // User dismissed the unlock dialog - security state stays exactly as it was.
407
450
  }
451
+ finally {
452
+ setUnlocking(false);
453
+ }
408
454
  }
409
455
  // Only ever invoked from the "Trust this signer" dialog, which only opens for an unverified-signer result carrying
410
456
  // `signerCertificate`. The pin goes on the contact in the mailbox the message belongs to; the evaluation is then
@@ -442,46 +488,72 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
442
488
  const mailbox = readerMailbox ?? (await getMailbox(message.mailboxUid).catch(() => undefined));
443
489
  return mailbox ? [mailbox.primarySmtpAddress, ...(mailbox.aliasAddresses ?? [])] : [];
444
490
  }
491
+ /** Starts fetching what Reply/Reply All/Forward will need (the compose window's code, the original's body) as the
492
+ * pointer or keyboard reaches one of their buttons, so the click itself finds it already on its way or here. */
493
+ function prefetchReply() {
494
+ prefetchComposeWindow();
495
+ prefetchOriginalMessage(message);
496
+ }
497
+ /**
498
+ * Opens the compose window at once from what is already in memory - the message's own subject and sender, and the
499
+ * mailbox it was read in - and fetches what needs the network meanwhile: the original's body to quote, and for Reply All
500
+ * the recipients its headers name. Both reach the window through `OpenComposeInput.pending` when they arrive, so the
501
+ * window is on screen on the click's own frame rather than after two or three sequential requests.
502
+ */
445
503
  async function handleReplyOrForward(kind) {
446
504
  setPreparingCompose(true);
447
505
  try {
448
- const [original, own] = await Promise.all([
449
- loadOriginalMessage(message, security, { recipients: kind === "replyAll" }),
450
- kind === "forward" ? [] : ownAddresses(),
451
- ]);
452
506
  const encrypt = !!message.encrypted;
453
507
  // What makes the new message part of this one's thread rather than a conversation of its own:
454
508
  // the server composes the MIME from the recipients, subject and HTML alone, so nothing else
455
509
  // recovers what is being replied to. A forward carries it for the same reason - it continues
456
510
  // the thread it came from, which is where its recipient will file the reply to it.
457
511
  const threading = buildReplyThreading(message);
512
+ const format = (list) => list.map((r) => formatRecipient(r)).join(", ");
513
+ // Known without a request when the shell has already listed the mailbox; otherwise fetched with the body, and
514
+ // the window's recipients are corrected by `late` below if that changes who they are.
515
+ const knownOwn = readerAddressesKey ? readerAddressesKey.split(" ") : undefined;
516
+ // Never rejects: loadOriginalMessage() and ownAddresses() don't, and the rest is pure.
517
+ const late = (async () => {
518
+ const [original, own] = await Promise.all([
519
+ loadOriginalMessage(message, security, { recipients: kind === "replyAll" }),
520
+ kind === "forward" ? [] : (knownOwn ?? ownAddresses()),
521
+ ]);
522
+ if (kind === "forward") {
523
+ return { quotedHtml: buildForwardQuote(message, original.body) };
524
+ }
525
+ // The message's own recipients are only the envelope recipient of a delivered message, so Reply All
526
+ // answers whoever its headers actually name, with anything the record holds that they don't.
527
+ const known = new Set((original.recipients ?? []).map((r) => r.address.trim().toLowerCase()));
528
+ const recipients = buildReplyRecipients({ ...message, recipients: [...(original.recipients ?? []), ...message.recipients.filter((r) => !known.has(r.address.trim().toLowerCase()))] }, own, kind === "replyAll");
529
+ return { quotedHtml: buildReplyQuote(message, original.body), to: format(recipients.to), cc: format(recipients.cc) };
530
+ })();
458
531
  if (kind === "forward") {
459
532
  openCompose({
460
533
  mailboxUid: message.mailboxUid,
461
534
  subject: forwardSubject(message.subject),
462
- quotedHtml: buildForwardQuote(message, original.body),
463
535
  signatureContext: "reply_forward",
464
536
  encrypt,
465
537
  threading,
538
+ pending: late,
466
539
  });
467
- return;
468
540
  }
469
- // The message's own recipients are only the envelope recipient of a delivered message, so Reply All
470
- // answers whoever its headers actually name, with anything the record holds that they don't.
471
- const known = new Set((original.recipients ?? []).map((r) => r.address.trim().toLowerCase()));
472
- const recipients = buildReplyRecipients({ ...message, recipients: [...(original.recipients ?? []), ...message.recipients.filter((r) => !known.has(r.address.trim().toLowerCase()))] }, own, kind === "replyAll");
473
- const format = (list) => list.map((r) => formatRecipient(r)).join(", ");
474
- openCompose({
475
- mailboxUid: message.mailboxUid,
476
- to: format(recipients.to),
477
- cc: recipients.cc.length > 0 ? format(recipients.cc) : undefined,
478
- subject: replySubject(message.subject),
479
- quotedHtml: buildReplyQuote(message, original.body),
480
- signatureContext: "reply_forward",
481
- suppressSigning: isLikelyMailingList({ listUnsubscribe: message.listUnsubscribeHeader }),
482
- encrypt,
483
- threading,
484
- });
541
+ else {
542
+ const first = buildReplyRecipients(message, knownOwn ?? [], kind === "replyAll");
543
+ openCompose({
544
+ mailboxUid: message.mailboxUid,
545
+ to: format(first.to),
546
+ cc: first.cc.length > 0 ? format(first.cc) : undefined,
547
+ subject: replySubject(message.subject),
548
+ signatureContext: "reply_forward",
549
+ suppressSigning: isLikelyMailingList({ listUnsubscribe: message.listUnsubscribeHeader }),
550
+ encrypt,
551
+ threading,
552
+ pending: late,
553
+ });
554
+ }
555
+ // Held until the quote is in, so a second click can't open a second window for the same message meanwhile.
556
+ await late;
485
557
  }
486
558
  finally {
487
559
  setPreparingCompose(false);
@@ -517,6 +589,8 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
517
589
  const updated = await cancelScheduledSend(message, draftsFolderUid);
518
590
  // Keeps a `folder:`-scoped Tier 2 local search from still finding it in its old folder.
519
591
  void moveLocalEntity(updated.mailboxUid, updated.uid, updated.folderUid);
592
+ // Out of Outbox, into Drafts: both folders' badges.
593
+ trackMessageChange(message, updated).settle();
520
594
  onScheduledSendCanceled?.(updated);
521
595
  }
522
596
  catch (err) {
@@ -541,6 +615,7 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
541
615
  try {
542
616
  const updated = await archiveMessage(message.uid);
543
617
  void moveLocalEntity(updated.mailboxUid, updated.uid, updated.folderUid);
618
+ trackMessageChange(message, updated).settle();
544
619
  onArchived?.(updated);
545
620
  }
546
621
  catch (err) {
@@ -585,8 +660,21 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
585
660
  */
586
661
  async function handleMove(folderUid) {
587
662
  const updated = await moveMessage(message, folderUid);
663
+ // Move to, Delete and Report junk are all this: the source folder's badge goes down, the target's up.
664
+ trackMessageChange(message, updated).settle();
588
665
  onMoved?.(updated);
589
666
  }
667
+ // Keyboard shortcuts for the message this pane shows. They call the same handlers as the buttons below, and exist only where the
668
+ // button does (Archive not for Drafts/Outbox, Move to only with folders to move to). A shortcut pressed while its action is already
669
+ // running is consumed and does nothing, exactly as the disabled button would: a browser must not act on Ctrl+R meanwhile.
670
+ const archivable = !inOutbox && message.folderUid !== draftsFolderUid;
671
+ const movable = !!folders && folders.length > 0;
672
+ const keyboard = !!shortcuts;
673
+ useShortcut(SHORTCUTS.mail.reply, () => void (!preparingCompose && handleReplyOrForward("reply")), { enabled: keyboard });
674
+ useShortcut(SHORTCUTS.mail.replyAll, () => void (!preparingCompose && handleReplyOrForward("replyAll")), { enabled: keyboard });
675
+ useShortcut(SHORTCUTS.mail.forward, () => void (!preparingCompose && handleReplyOrForward("forward")), { enabled: keyboard });
676
+ useShortcut(SHORTCUTS.mail.archive, () => void (!archiving && handleArchive()), { enabled: keyboard && archivable });
677
+ useShortcut(SHORTCUTS.mail.move, () => setMovePrompt(true), { enabled: keyboard && movable });
590
678
  // Only ever invoked from the pending-receipt banner below, which itself only renders once `message`
591
679
  // is loaded — same real invariant as every other handler above.
592
680
  async function handleReceipt(type, action) {
@@ -642,40 +730,46 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
642
730
  const recordedConflict = keyChanged && sameFingerprint(senderKeyState?.conflict?.observedKey.fingerprint, keyChanged.signerFingerprint)
643
731
  ? senderKeyState.conflict
644
732
  : undefined;
645
- const shownSubject = (protectedSubject ?? message.subject) || "(no subject)";
646
- /**
647
- * How tall the message body is - the one thing on this pane worth every pixel it can have.
648
- *
649
- * The same rule in both places: the pane is a full-height flex column and the body is its one growing
650
- * child, so it fills whatever the header leaves (`min-h-0` on both, or a tall body would stretch the
651
- * column past the pane instead of scrolling inside it). Inside a thread that height comes from the
652
- * message's own row, which `ConversationThreadPane` gives `min-h-full` of its scrolling list - so the
653
- * height is still the window's, resolved down the flex chain rather than written here as a `vh`
654
- * number, and it follows a resize with no breakpoint to cross.
655
- *
656
- * Why not size it to its content: that would need the document *inside* the frame to measure and
657
- * report itself, and these frames are `sandbox=""` - scripts off - because they render mail from
658
- * strangers. So a message longer than the pane keeps its own scrollbar, and everything shorter shows
659
- * whole with the frame reaching the bottom of the pane either way.
660
- */
661
- const bodyClassName = "flex-1 min-h-0 w-full";
662
- return (_jsxs("div", { className: `flex-1 min-w-0 min-h-0 flex flex-col${inThread ? "" : " h-full"}`, children: [_jsxs("div", { className: "border-b border-border p-4", children: [backHref && (_jsx("a", { href: backHref, className: "text-sm text-primary-dark hover:underline block mb-2", children: "\u2190 Back to messages" })), _jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [inThread ? (_jsx("h3", { className: "text-sm font-semibold truncate", children: shownSubject })) : (_jsx("h1", { className: "text-lg font-bold tracking-tight truncate", children: shownSubject })), security && _jsx(SecurityIndicator, { security: security })] }), sendInProgress && (_jsx("span", { className: "text-xs font-medium text-text-muted shrink-0 py-1 px-2.5 rounded-pill bg-surface-alt", children: "Sending\u2026" })), isSentItems &&
663
- (message.recallRequestedAt ? (_jsx("span", { className: "text-xs font-medium text-text-muted shrink-0 py-1 px-2.5 rounded-pill bg-surface-alt", children: "Recall requested" })) : (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", onClick: () => setConfirming(true), children: "Recall this message" }))), inOutbox && !sendInProgress && message.scheduledSendTime && (_jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [_jsxs("span", { className: "text-xs font-medium text-text-muted py-1 px-2.5 rounded-pill bg-surface-alt", children: ["Scheduled for ", new Date(message.scheduledSendTime).toLocaleString()] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: canceling, disabled: canceling, onClick: handleCancelScheduledSend, children: "Cancel" })] })), inOutbox && !sendInProgress && !message.scheduledSendTime && draftsFolderUid && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", loading: canceling, disabled: canceling, onClick: handleCancelScheduledSend, children: "Move to Drafts" }))] }), inOutbox && message.scheduledSendError && (_jsx("div", { className: "mt-2", children: _jsxs(Alert, { children: ["This message wasn\u2019t sent: ", message.scheduledSendError] }) })), cancelError && (_jsx("div", { className: "mt-2", children: _jsx(Alert, { children: cancelError }) })), keyChangeNotice && (_jsx("p", { role: "status", className: "mt-2 py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: keyChangeNotice })), security && UNVERIFIED_SIGNER_STATES.has(security.state) && (_jsxs("p", { role: "status", className: "mt-2 py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: ["Signed, but the signer isn\u2019t a trusted contact key, so the sender isn\u2019t verified.", security.signerEmails && security.signerEmails.length > 0 && _jsxs(_Fragment, { children: [" Certificate for ", security.signerEmails.join(", "), "."] }), security.signerFingerprint && (_jsxs(_Fragment, { children: [" ", "Fingerprint ", _jsx("span", { className: "font-mono text-xs break-all", children: security.signerFingerprint }), "."] }))] })), pendingConflict && (_jsxs("p", { role: "status", className: "mt-2 py-2 px-3 rounded-sm text-sm bg-warning/15 text-text", children: ["This sender has a signing key change waiting for your review, so this signer can\u2019t be trusted from here.", " ", _jsx("a", { href: `/contacts/${encodeURIComponent(senderKeyState.contactUid)}`, className: "font-medium text-primary-dark hover:underline", children: "Review it in Contacts" })] })), trustableCertificate && (_jsx("div", { className: "mt-1.5", children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: trusting, onClick: () => {
733
+ const shownSubject = (protectedSubject ?? displaySubject(message.subject)) || "(no subject)";
734
+ // What the body area shows. Text or HTML the client recovered from a signed or encrypted message is shown as it is (its own sanitizing
735
+ // happens in `MessageBody`, like every body's); anything else is the server's own `/content`. An encrypted message has nothing worth
736
+ // fetching until it has been decrypted (the server only has the ciphertext), so it shows the skeleton meanwhile.
737
+ const bodyContent = security?.text !== undefined
738
+ ? { kind: "text", text: security.text }
739
+ : security?.html !== undefined
740
+ ? { kind: "html", html: security.html }
741
+ : undefined;
742
+ const bodyTitle = message.subject || "Message content";
743
+ const answered = message.flags.answered === true;
744
+ const forwarded = message.flags.forwarded === true;
745
+ const cardUnread = threadHeader?.unread ?? false;
746
+ const showFooter = footer ?? !inThread;
747
+ const senderRecipient = { displayName: senderName, address: senderAddress };
748
+ const sendingStatus = (_jsxs(_Fragment, { children: [sendInProgress && (_jsx("span", { className: "text-xs font-medium text-text-muted shrink-0 py-1 px-2.5 rounded-pill bg-surface-alt", children: "Sending\u2026" })), isSentItems &&
749
+ (message.recallRequestedAt ? (_jsx("span", { className: "text-xs font-medium text-text-muted shrink-0 py-1 px-2.5 rounded-pill bg-surface-alt", children: "Recall requested" })) : (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", onClick: () => setConfirming(true), children: "Recall this message" }))), inOutbox && !sendInProgress && message.scheduledSendTime && (_jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [_jsxs("span", { className: "text-xs font-medium text-text-muted py-1 px-2.5 rounded-pill bg-surface-alt", children: ["Scheduled for ", new Date(message.scheduledSendTime).toLocaleString()] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: canceling, disabled: canceling, onClick: handleCancelScheduledSend, children: "Cancel" })] })), inOutbox && !sendInProgress && !message.scheduledSendTime && draftsFolderUid && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", loading: canceling, disabled: canceling, onClick: handleCancelScheduledSend, children: "Move to Drafts" }))] }));
750
+ const card = (_jsxs(CardShell, { unread: cardUnread, children: [_jsxs("div", { className: ["flex flex-wrap items-start gap-x-3 gap-y-1 px-4 pt-3 pb-2", cardUnread ? "bg-primary/[0.07]" : ""].join(" "), children: [_jsx(SenderAvatar, { from: senderRecipient }), _jsxs("div", { className: "flex-1 min-w-[12rem]", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1", children: [threadHeader ? (_jsx("h2", { className: "min-w-0 flex-1", style: BODY_FONT_STYLE, children: _jsxs("button", { type: "button", ref: threadHeader.buttonRef, onClick: threadHeader.onToggle, "aria-expanded": true, "aria-controls": threadHeader.bodyId, className: [
751
+ "w-full text-left rounded-sm text-sm",
752
+ ROW_FOCUS_CLASS,
753
+ ].join(" "), children: [_jsx(UnreadLabel, { unread: cardUnread }), _jsx(MailAddress, { recipient: senderRecipient, className: senderClass(cardUnread) })] }) })) : (_jsxs("p", { className: "text-sm break-words min-w-0", children: ["From ", _jsx("span", { className: "font-semibold text-text", children: senderLabel })] })), security && _jsx(SecurityIndicator, { security: security })] }), senderNameCheck.misleading && (_jsxs("p", { role: "status", className: "mt-2 py-2 px-3 rounded-sm text-sm bg-warning/15 text-text", children: ["The sender\u2019s name \u201C", senderName, "\u201D looks like an email address, but this message was sent from ", _jsx("span", { className: "font-medium", children: senderAddress }), ". Don\u2019t trust it based on the name."] })), _jsx(RecipientLine, { label: "To", recipients: message.recipients.filter((r) => r.type !== "cc" && r.type !== "bcc") }), _jsx(RecipientLine, { label: "Cc", recipients: message.recipients.filter((r) => r.type === "cc") }), _jsx(RecipientLine, { label: "Bcc", recipients: message.recipients.filter((r) => r.type === "bcc") })] }), _jsxs("div", { className: "ml-auto flex flex-wrap items-center justify-end gap-1", children: [_jsx("time", { dateTime: message.receivedDate, className: ["text-xs mr-1", dateClass(cardUnread)].join(" "), children: new Date(message.receivedDate).toLocaleString() }), adaptable && (_jsx("button", { type: "button", "aria-label": "View original", "aria-pressed": viewOriginal, title: viewOriginal ? "Follow the theme" : "View original", onClick: () => setViewOriginal(!viewOriginal), className: [
754
+ "inline-flex items-center justify-center p-1.5 rounded-md border border-border text-sm text-text hover:bg-surface-alt",
755
+ viewOriginal ? "bg-primary/10" : "",
756
+ ].join(" "), children: viewOriginal ? _jsx(HiOutlineMoon, { size: 16, "aria-hidden": "true" }) : _jsx(HiOutlineSun, { size: 16, "aria-hidden": "true" }) })), _jsx(IconAction, { icon: _jsx(HiOutlineArrowUturnLeft, { size: 16, "aria-hidden": "true" }), label: "Reply", shortcut: keyboard ? SHORTCUTS.mail.reply : undefined, disabled: preparingCompose, onPrefetch: prefetchReply, onClick: () => void handleReplyOrForward("reply") }), _jsx(IconAction, { icon: _jsxs("span", { className: "inline-flex items-center", "aria-hidden": "true", children: [_jsx(HiOutlineArrowUturnLeft, { size: 16 }), _jsx(HiOutlineArrowUturnLeft, { size: 16, className: "-ml-2.5" })] }), label: "Reply All", shortcut: keyboard ? SHORTCUTS.mail.replyAll : undefined, disabled: preparingCompose, onPrefetch: prefetchReply, onClick: () => void handleReplyOrForward("replyAll") }), _jsx(IconAction, { icon: _jsx(HiOutlineArrowUturnRight, { size: 16, "aria-hidden": "true" }), label: "Forward", shortcut: keyboard ? SHORTCUTS.mail.forward : undefined, disabled: preparingCompose, onPrefetch: prefetchReply, onClick: () => void handleReplyOrForward("forward") }), archivable && (_jsx(IconAction, { icon: _jsx(HiOutlineArchiveBox, { size: 16, "aria-hidden": "true" }), label: "Archive", shortcut: keyboard ? SHORTCUTS.mail.archive : undefined, busy: archiving, disabled: archiving, onClick: handleArchive })), movable && (_jsx(IconAction, { icon: _jsx(HiOutlineFolderArrowDown, { size: 16, "aria-hidden": "true" }), label: "Move to", shortcut: keyboard ? SHORTCUTS.mail.move : undefined, onClick: () => setMovePrompt(true) })), labels && labels.length > 0 && (_jsx(LabelMenuButton, { "aria-label": "Labels", label: "Labels", className: "border border-border py-1.5", labels: labels, mailboxUid: message.mailboxUid, onLabelCreated: onLabelCreated, applied: currentLabelsMessage.labelUids ?? [], onCommit: (labelUids) => void handleApplyLabels(labelUids), busy: savingLabels, note: "Ticked labels are applied to this message and unticked ones removed.", emptyNote: "This mailbox has no labels yet.", commit: { label: "Apply" }, clear: { label: "Remove all labels" } }))] })] }), (answered || forwarded) && (_jsx("p", { className: "px-4 py-1.5 text-xs text-text-muted bg-surface-alt border-y border-border", children: answered && forwarded ? "You replied to and forwarded this message." : answered ? "You replied to this message." : "You forwarded this message." })), _jsxs("div", { id: threadHeader?.bodyId, className: "px-4 pb-3 flex flex-col gap-2", children: [inThread && (subjectDiffers || !sameSubject(shownSubject, threadSubject)) && (_jsx("h3", { className: "text-sm font-semibold break-words", children: shownSubject })), _jsx("div", { className: "flex flex-wrap items-center gap-2 empty:hidden", children: sendingStatus }), inOutbox && message.scheduledSendError && _jsxs(Alert, { children: ["This message wasn\u2019t sent: ", message.scheduledSendError] }), cancelError && _jsx(Alert, { children: cancelError }), keyChangeNotice && (_jsx("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: keyChangeNotice })), security && UNVERIFIED_SIGNER_STATES.has(security.state) && (_jsxs("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: ["Signed, but the signer isn\u2019t a trusted contact key, so the sender isn\u2019t verified.", security.signerEmails && security.signerEmails.length > 0 && _jsxs(_Fragment, { children: [" Certificate for ", security.signerEmails.join(", "), "."] }), security.signerFingerprint && (_jsxs(_Fragment, { children: [" ", "Fingerprint ", _jsx("span", { className: "font-mono text-xs break-all", children: security.signerFingerprint }), "."] }))] })), pendingConflict && (_jsxs("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-warning/15 text-text", children: ["This sender has a signing key change waiting for your review, so this signer can\u2019t be trusted from here.", " ", _jsx("a", { href: `/contacts/${encodeURIComponent(senderKeyState.contactUid)}`, className: "font-medium text-primary-dark hover:underline", children: "Review it in Contacts" })] })), trustableCertificate && (_jsx("div", { children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: trusting, onClick: () => {
664
757
  setTrustError(null);
665
758
  setTrustConfirmOpen(true);
666
- }, children: "Trust this signer" }) })), subjectDiffers && (_jsxs("p", { role: "status", className: "mt-2 py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: ["The subject shown above is the one the sender signed. It differs from the subject this message was delivered with (\u201C", message.subject, "\u201D), which may have been changed on the way, e.g. by a mailing list."] })), security?.headerTamperDetected && (_jsx("div", { className: "mt-2", children: _jsx(Alert, { children: "This message's visible From/To/Cc/Date/Subject don't match what the sender actually signed or encrypted \u2014 an intermediary may have altered them after sending. Treat the fields shown above with caution." }) })), security?.state === "verified_at_first_open" && (_jsx("p", { role: "status", className: `mt-2 py-2 px-3 rounded-sm text-sm text-text ${security.laterCompromised ? "bg-warning/15" : "bg-surface-alt"}`, children: verifiedAtFirstOpenMessage(security) })), keyChanged && (_jsxs("section", { "aria-label": "Signing key changed", className: "mt-2 py-3 px-3 rounded-sm text-sm bg-warning/15 text-text flex flex-col gap-2", children: [_jsx("h2", { className: "font-semibold", children: "This sender\u2019s signing key changed" }), _jsxs("p", { children: ["The signature on this message is valid and its certificate names ", senderAddress, ", but it was made with a different key than the one you trust for this sender", keyChanged.state === "verified_at_first_open" ? "." : _jsx(_Fragment, { children: ", so it isn\u2019t verified." })] }), _jsx(KeyChangeReview, { mailboxUid: message.mailboxUid, address: senderAddress, useType: "sign", ownerName: "the sender", current: pinnedSignerKey && { fingerprint: pinnedSignerKey.fingerprint, since: senderKeyState.pinnedSince }, proposed: {
759
+ }, children: "Trust this signer" }) })), subjectDiffers && (_jsxs("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: ["The subject shown above is the one the sender signed. It differs from the subject this message was delivered with (\u201C", message.subject, "\u201D), which may have been changed on the way, e.g. by a mailing list."] })), security?.headerTamperDetected && (_jsx(Alert, { children: "This message's visible From/To/Cc/Date/Subject don't match what the sender actually signed or encrypted \u2014 an intermediary may have altered them after sending. Treat the fields shown above with caution." })), security?.state === "verified_at_first_open" && (_jsx("p", { role: "status", className: `py-2 px-3 rounded-sm text-sm text-text ${security.laterCompromised ? "bg-warning/15" : "bg-surface-alt"}`, children: verifiedAtFirstOpenMessage(security) })), keyChanged && (_jsxs("section", { "aria-label": "Signing key changed", className: "py-3 px-3 rounded-sm text-sm bg-warning/15 text-text flex flex-col gap-2", children: [_jsx("h2", { className: "font-semibold", children: "This sender\u2019s signing key changed" }), _jsxs("p", { children: ["The signature on this message is valid and its certificate names ", senderAddress, ", but it was made with a different key than the one you trust for this sender", keyChanged.state === "verified_at_first_open" ? "." : _jsx(_Fragment, { children: ", so it isn\u2019t verified." })] }), _jsx(KeyChangeReview, { mailboxUid: message.mailboxUid, address: senderAddress, useType: "sign", ownerName: "the sender", current: pinnedSignerKey && { fingerprint: pinnedSignerKey.fingerprint, since: senderKeyState.pinnedSince }, proposed: {
667
760
  fingerprint: keyChanged.signerFingerprint,
668
761
  emails: keyChanged.signerEmails,
669
762
  observedAt: recordedConflict?.observedAt,
670
763
  source: recordedConflict?.source,
671
- }, certificate: keyChanged.signerCertificate, canReject: recordedConflict !== undefined, canResolve: canUpdateMailbox, onResolved: (action) => refreshAfterKeyChange(action === "reject" ? KEPT_CURRENT_SIGNING_KEY_MESSAGE : null), onPinnedKeyChanged: () => refreshAfterKeyChange(KEY_CHANGE_STALE_MESSAGE) })] })), security?.state === "signature_failed" && !keyChanged && (_jsx("p", { role: "status", className: "mt-2 py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: security.signatureFailureReason
764
+ }, certificate: keyChanged.signerCertificate, canReject: recordedConflict !== undefined, canResolve: canUpdateMailbox, onResolved: (action) => refreshAfterKeyChange(action === "reject" ? KEPT_CURRENT_SIGNING_KEY_MESSAGE : null), onPinnedKeyChanged: () => refreshAfterKeyChange(KEY_CHANGE_STALE_MESSAGE) })] })), security?.state === "signature_failed" && !keyChanged && (_jsx("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: security.signatureFailureReason
672
765
  ? SIGNATURE_FAILURE_MESSAGE[security.signatureFailureReason]
673
- : GENERIC_SIGNATURE_FAILURE_MESSAGE })), verifiedOrSealed && !security.protectedHeaders && (_jsx("p", { role: "status", className: "mt-2 py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: "The signature covers this message\u2019s content and attachments only. Its Subject, To and Cc weren\u2019t signed, so they could have been changed after it was sent." })), security?.notAddressedToReader && (_jsx("p", { role: "status", className: "mt-2 py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: "The recipients this message was signed or encrypted for don\u2019t include this mailbox - it may have been forwarded or re-sent to you unchanged, or you were Bcc\u2019d." })), _jsxs("p", { className: "text-sm text-text-muted mt-1 break-words", children: ["From ", _jsx("span", { className: "font-medium text-text", children: senderLabel }), " \u00B7", " ", new Date(message.receivedDate).toLocaleString()] }), senderNameCheck.misleading && (_jsxs("p", { role: "status", className: "mt-2 py-2 px-3 rounded-sm text-sm bg-warning/15 text-text", children: ["The sender\u2019s name \u201C", senderName, "\u201D looks like an email address, but this message was sent from ", _jsx("span", { className: "font-medium", children: senderAddress }), ". Don\u2019t trust it based on the name."] })), _jsx(RecipientLine, { label: "To", recipients: message.recipients.filter((r) => r.type !== "cc" && r.type !== "bcc") }), _jsx(RecipientLine, { label: "Cc", recipients: message.recipients.filter((r) => r.type === "cc") }), _jsx(RecipientLine, { label: "Bcc", recipients: message.recipients.filter((r) => r.type === "bcc") }), _jsxs("div", { className: "flex flex-wrap gap-1 mt-3", children: [_jsx(IconAction, { icon: _jsx(HiOutlineArrowUturnLeft, { size: 16, "aria-hidden": "true" }), label: "Reply", disabled: preparingCompose, onClick: () => void handleReplyOrForward("reply") }), _jsx(IconAction, { icon: _jsxs("span", { className: "inline-flex items-center", "aria-hidden": "true", children: [_jsx(HiOutlineArrowUturnLeft, { size: 16 }), _jsx(HiOutlineArrowUturnLeft, { size: 16, className: "-ml-2.5" })] }), label: "Reply All", disabled: preparingCompose, onClick: () => void handleReplyOrForward("replyAll") }), _jsx(IconAction, { icon: _jsx(HiOutlineArrowUturnRight, { size: 16, "aria-hidden": "true" }), label: "Forward", disabled: preparingCompose, onClick: () => void handleReplyOrForward("forward") }), !inOutbox && message.folderUid !== draftsFolderUid && (_jsx(IconAction, { icon: _jsx(HiOutlineArchiveBox, { size: 16, "aria-hidden": "true" }), label: "Archive", busy: archiving, disabled: archiving, onClick: handleArchive })), folders && folders.length > 0 && (_jsx(IconAction, { icon: _jsx(HiOutlineFolderArrowDown, { size: 16, "aria-hidden": "true" }), label: "Move to", onClick: () => setMovePrompt(true) })), labels && labels.length > 0 && (_jsx(LabelMenuButton, { "aria-label": "Labels", label: "Labels", className: "border border-border py-1.5", labels: labels, mailboxUid: message.mailboxUid, onLabelCreated: onLabelCreated, applied: currentLabelsMessage.labelUids ?? [], onCommit: (labelUids) => void handleApplyLabels(labelUids), busy: savingLabels, note: "Ticked labels are applied to this message and unticked ones removed.", emptyNote: "This mailbox has no labels yet.", commit: { label: "Apply" }, clear: { label: "Remove all labels" } }))] }), archiveError && (_jsx("div", { className: "mt-2", children: _jsx(Alert, { children: archiveError }) })), labelsError && (_jsx("div", { className: "mt-2", children: _jsx(Alert, { children: labelsError }) })), labels && (currentLabelsMessage.labelUids?.length ?? 0) > 0 && (_jsx("div", { className: "flex flex-wrap items-center gap-1.5 mt-2", children: currentLabelsMessage
766
+ : GENERIC_SIGNATURE_FAILURE_MESSAGE })), verifiedOrSealed && !security.protectedHeaders && (_jsx("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: "The signature covers this message\u2019s content and attachments only. Its Subject, To and Cc weren\u2019t signed, so they could have been changed after it was sent." })), security?.notAddressedToReader && !isSentItems && (_jsx("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: "The recipients this message was signed or encrypted for don\u2019t include this mailbox - it may have been forwarded or re-sent to you unchanged, or you were Bcc\u2019d." })), archiveError && _jsx(Alert, { children: archiveError }), labelsError && _jsx(Alert, { children: labelsError }), labels && (currentLabelsMessage.labelUids?.length ?? 0) > 0 && (_jsx("div", { className: "flex flex-wrap items-center gap-1.5", children: currentLabelsMessage
674
767
  .labelUids.map((uid) => labels.find((l) => l.uid === uid))
675
768
  .filter((l) => !!l)
676
769
  .map((l) => (_jsxs("span", { className: "inline-flex items-center gap-1.5 text-xs font-medium py-1 px-2.5 rounded-pill bg-surface-alt", children: [_jsx("span", { className: "w-2 h-2 rounded-full shrink-0", style: { backgroundColor: l.color ?? "#6366f1" } }), l.name] }, l.uid))) })), ["delivery", "read"]
677
770
  .filter((type) => (type === "delivery" ? message.deliveryReceiptPending : message.readReceiptPending))
678
- .map((type) => (_jsxs("div", { className: "flex flex-wrap items-center gap-2 mt-2 py-2 px-3 rounded-sm bg-surface-alt text-sm", children: [_jsxs("span", { children: [senderLabel, " requested a ", type, " receipt for this message."] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: receiptBusy === type, disabled: receiptBusy !== null, onClick: () => handleReceipt(type, "approve"), children: "Send receipt" }), _jsx(Button, { type: "button", variant: "text", disabled: receiptBusy !== null, onClick: () => handleReceipt(type, "decline"), children: "Decline" })] }, type))), receiptError && (_jsx("div", { className: "mt-2", children: _jsx(Alert, { children: receiptError }) })), security?.state === "signature_failed" && innerAttachments && innerAttachments.length > 0 && (_jsx("p", { role: "status", className: "mt-3 py-2 px-3 rounded-sm text-sm bg-warning/15 text-text", children: "These attachments come from a message whose signature couldn\u2019t be verified. Open them only if you trust the sender." })), innerAttachments
679
- ? innerAttachments.length > 0 && (_jsx("ul", { className: "flex flex-wrap gap-2 mt-3", children: innerAttachments.map((attachment, index) => (_jsx("li", { children: _jsx("button", { type: "button", onClick: () => downloadMimeAttachment(attachment), className: "text-xs font-medium py-1 px-2.5 rounded-pill bg-surface-alt text-text-muted hover:text-primary-dark", children: attachment.filename ?? `Unnamed ${attachment.contentType} attachment` }) }, `${index}:${attachment.filename ?? ""}`))) }))
680
- : attachments.length > 0 && (_jsx("ul", { className: "flex flex-wrap gap-2 mt-3", children: attachments.map((attachment) => (_jsx("li", { children: _jsxs("a", { href: attachmentContentUrl(attachment.uid), className: "text-xs font-medium py-1 px-2.5 rounded-pill bg-surface-alt text-text-muted hover:text-primary-dark", children: [attachment.filename, " (", formatBytes(attachment.sizeBytes), ")"] }) }, attachment.uid))) }))] }), security?.decryptError && (_jsxs("div", { className: "px-4 pt-2", children: [_jsx(Alert, { children: security.decryptError }), !getUnlockedKeys(message.mailboxUid) && (_jsxs("button", { type: "button", onClick: handleUnlockToView, className: "mt-1.5 inline-flex items-center gap-1 text-xs font-medium text-primary-dark hover:underline", children: [_jsx(HiOutlineLockClosed, { size: 12, "aria-hidden": "true" }), "Unlock to view this message"] }))] })), security?.text !== undefined ? (_jsx("pre", { "aria-label": message.subject || "Message content", className: `${bodyClassName} overflow-auto p-4 m-0 text-sm font-sans whitespace-pre-wrap break-words`, children: security.text })) : security?.html !== undefined ? (_jsx("iframe", { title: message.subject || "Message content", srcDoc: buildSecureSrcDoc(security.html), sandbox: "", className: `${bodyClassName} border-0` }, message.uid)) : (_jsx("iframe", { title: message.subject || "Message content", src: `/api/mail/messages/${encodeURIComponent(message.uid)}/content`, sandbox: "", className: `${bodyClassName} border-0` }, message.uid)), _jsx(MoveToFolderDialog, { open: movePrompt, onClose: () => setMovePrompt(false), mailboxUid: message.mailboxUid, folders: folders ?? [], currentFolderUid: message.folderUid, onMove: handleMove, onFolderCreated: onFolderCreated }), _jsxs(Modal, { open: confirming, onClose: () => setConfirming(false), title: "Recall this message?", children: [_jsx("p", { className: "text-sm text-text-muted mb-4", children: "This asks every original recipient's mail system to delete their copy, but only if it's still unread there \u2014 there's no way to guarantee it, and no confirmation once it either succeeds or fails. Recipients who already read the message will keep it." }), error && _jsx(Alert, { children: error }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "button", loading: recalling, disabled: recalling, onClick: handleRecall, className: "!w-auto", children: "Recall message" }), _jsx(Button, { type: "button", variant: "secondary", disabled: recalling, onClick: () => setConfirming(false), className: "!w-auto", children: "Cancel" })] })] }), _jsx(Modal, { open: trustConfirmOpen && !!trustableCertificate, onClose: () => !trusting && setTrustConfirmOpen(false), title: "Trust this signer?", children: _jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsxs("p", { children: ["Mail from ", _jsx("span", { className: "font-medium", children: senderAddress }), " signed with this certificate will show as verified."] }), _jsxs("dl", { className: "flex flex-col gap-1", children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Certificate issued to" }), _jsx("dd", { children: security?.signerEmails?.length ? security.signerEmails.join(", ") : "No email address" }), _jsx("dt", { className: "text-xs text-text-muted", children: "Fingerprint" }), _jsx("dd", { className: "font-mono text-xs break-all", children: security?.signerFingerprint ? formatFingerprint(security.signerFingerprint) : "Unknown" })] }), _jsx("p", { className: "text-text-muted", children: "Anyone can create a certificate naming any address. Before trusting it, confirm this fingerprint with the sender through another channel, such as a phone call." }), trustError && _jsx(Alert, { children: trustError }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "button", className: "!w-auto", loading: trusting, disabled: trusting, onClick: () => handleTrustSigner(senderAddress, trustableCertificate), children: "Trust" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: trusting, onClick: () => setTrustConfirmOpen(false), children: "Cancel" })] })] }) })] }));
771
+ .map((type) => (_jsxs("div", { className: "flex flex-wrap items-center gap-2 py-2 px-3 rounded-sm bg-surface-alt text-sm", children: [_jsxs("span", { children: [senderLabel, " requested a ", type, " receipt for this message."] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: receiptBusy === type, disabled: receiptBusy !== null, onClick: () => handleReceipt(type, "approve"), children: "Send receipt" }), _jsx(Button, { type: "button", variant: "text", disabled: receiptBusy !== null, onClick: () => handleReceipt(type, "decline"), children: "Decline" })] }, type))), receiptError && _jsx(Alert, { children: receiptError }), security?.state === "signature_failed" && innerAttachments && innerAttachments.length > 0 && (_jsx("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-warning/15 text-text", children: "These attachments come from a message whose signature couldn\u2019t be verified. Open them only if you trust the sender." })), innerAttachments
772
+ ? innerAttachments.length > 0 && (_jsx("ul", { className: "flex flex-wrap gap-2", children: innerAttachments.map((attachment, index) => (_jsx("li", { children: _jsx("button", { type: "button", onClick: () => downloadMimeAttachment(attachment), className: "text-xs font-medium py-1 px-2.5 rounded-pill bg-surface-alt text-text-muted hover:text-primary-dark", children: attachment.filename ?? `Unnamed ${attachment.contentType} attachment` }) }, `${index}:${attachment.filename ?? ""}`))) }))
773
+ : attachments.length > 0 && (_jsx("ul", { className: "flex flex-wrap gap-2", children: attachments.map((attachment) => (_jsx("li", { children: _jsxs("a", { href: attachmentContentUrl(attachment.uid), className: "text-xs font-medium py-1 px-2.5 rounded-pill bg-surface-alt text-text-muted hover:text-primary-dark", children: [attachment.filename, " (", formatBytes(attachment.sizeBytes), ")"] }) }, attachment.uid))) })), _jsx("div", { ref: bodyRegionRef, tabIndex: -1, className: "outline-none", children: message.encrypted && security === null ? (_jsx(BodySkeleton, {})) : security?.decryptError !== undefined ? (_jsx(EncryptedBody, { locked: lockedRef.current, reason: security.decryptError, unlocking: unlocking, onUnlock: () => void handleUnlockToView() })) : (_jsx(MessageBody, { messageUid: message.uid, messageVersion: message.version, title: bodyTitle, content: bodyContent, attachments: attachments, inlineParts: innerAttachments, original: viewOriginal, onAdaptable: setAdaptable })) })] }), showFooter && (_jsxs("div", { className: "flex flex-wrap gap-2 px-4 py-3 border-t border-border print:hidden", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": "Reply to this message", disabled: preparingCompose, onClick: () => void handleReplyOrForward("reply"), children: "Reply" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": "Forward this message", disabled: preparingCompose, onClick: () => void handleReplyOrForward("forward"), children: "Forward" })] }))] }));
774
+ return (_jsxs("div", { className: inThread ? "min-w-0" : "flex-1 min-w-0 min-h-0 flex flex-col h-full", children: [inThread ? (card) : (_jsxs(_Fragment, { children: [backHref && (_jsx("a", { href: backHref, className: "text-sm text-primary-dark hover:underline block px-4 pt-3", children: "\u2190 Back to messages" })), _jsx(SubjectCard, { subject: shownSubject }), _jsx("div", { className: "flex-1 min-h-0 overflow-y-auto p-2 sm:p-3 flex flex-col gap-3", children: card })] })), _jsx(MoveToFolderDialog, { open: movePrompt, onClose: () => setMovePrompt(false), mailboxUid: message.mailboxUid, folders: folders ?? [], currentFolderUid: message.folderUid, onMove: handleMove, onFolderCreated: onFolderCreated }), _jsxs(Modal, { open: confirming, onClose: () => setConfirming(false), title: "Recall this message?", children: [_jsx("p", { className: "text-sm text-text-muted mb-4", children: "This asks every original recipient's mail system to delete their copy, but only if it's still unread there \u2014 there's no way to guarantee it, and no confirmation once it either succeeds or fails. Recipients who already read the message will keep it." }), error && _jsx(Alert, { children: error }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "button", loading: recalling, disabled: recalling, onClick: handleRecall, className: "!w-auto", children: "Recall message" }), _jsx(Button, { type: "button", variant: "secondary", disabled: recalling, onClick: () => setConfirming(false), className: "!w-auto", children: "Cancel" })] })] }), _jsx(Modal, { open: trustConfirmOpen && !!trustableCertificate, onClose: () => !trusting && setTrustConfirmOpen(false), title: "Trust this signer?", children: _jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsxs("p", { children: ["Mail from ", _jsx("span", { className: "font-medium", children: senderAddress }), " signed with this certificate will show as verified."] }), _jsxs("dl", { className: "flex flex-col gap-1", children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Certificate issued to" }), _jsx("dd", { children: security?.signerEmails?.length ? security.signerEmails.join(", ") : "No email address" }), _jsx("dt", { className: "text-xs text-text-muted", children: "Fingerprint" }), _jsx("dd", { className: "font-mono text-xs break-all", children: security?.signerFingerprint ? formatFingerprint(security.signerFingerprint) : "Unknown" })] }), _jsx("p", { className: "text-text-muted", children: "Anyone can create a certificate naming any address. Before trusting it, confirm this fingerprint with the sender through another channel, such as a phone call." }), trustError && _jsx(Alert, { children: trustError }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "button", className: "!w-auto", loading: trusting, disabled: trusting, onClick: () => handleTrustSigner(senderAddress, trustableCertificate), children: "Trust" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: trusting, onClick: () => setTrustConfirmOpen(false), children: "Cancel" })] })] }) })] }));
681
775
  }
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+ import { OutboxFolderStatus } from "../../mail/outbox/outboxState.js";
3
+ export interface OutboxBadgeProps {
4
+ /** How many messages the Outbox holds: the folder's own count, which already includes what this tab has just sent. */
5
+ total: number;
6
+ /** What they are doing, when known (`useOutboxStatus()`). */
7
+ status?: OutboxFolderStatus;
8
+ /** How many of this mailbox's messages this tab is still handing to the server. */
9
+ pendingHere: number;
10
+ }
11
+ /**
12
+ * The Outbox's count, next to its name: a small pill. While anything is on its way it says so - an animated dot and an accent colour (the dot's
13
+ * animation is CSS, off under `prefers-reduced-motion`); when a message failed and is waiting for the user it is red; otherwise it is the plain
14
+ * count every "how many are in here" folder shows. Screen readers get the same as text ("2 messages sending"), in a polite live region so a
15
+ * change - a message going out, a failure - is announced.
16
+ */
17
+ export default function OutboxBadge({ total, status, pendingHere }: OutboxBadgeProps): React.JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { outboxLabel } from "../../mail/outbox/outboxState.js";
3
+ /**
4
+ * The Outbox's count, next to its name: a small pill. While anything is on its way it says so - an animated dot and an accent colour (the dot's
5
+ * animation is CSS, off under `prefers-reduced-motion`); when a message failed and is waiting for the user it is red; otherwise it is the plain
6
+ * count every "how many are in here" folder shows. Screen readers get the same as text ("2 messages sending"), in a polite live region so a
7
+ * change - a message going out, a failure - is announced.
8
+ */
9
+ export default function OutboxBadge({ total, status, pendingHere }) {
10
+ const failed = status?.failed ?? 0;
11
+ const inFlight = (status ? status.sending + status.retrying : 0) + pendingHere;
12
+ const state = failed > 0 ? "failed" : inFlight > 0 ? "sending" : "idle";
13
+ const label = outboxLabel(total, status, pendingHere);
14
+ return (_jsxs("span", { "data-testid": "outbox-badge", "data-state": state, title: label, "aria-live": "polite", className: [
15
+ "inline-flex items-center gap-1 rounded-pill px-1.5 py-0.5 text-xs",
16
+ state === "failed" ? "bg-danger font-bold text-white" : state === "sending" ? "bg-primary/15 font-bold text-primary-dark" : "font-medium text-text-muted",
17
+ ].join(" "), children: [state === "sending" && _jsx("span", { "aria-hidden": "true", className: "rr-sending-dot h-1.5 w-1.5 rounded-full bg-primary" }), _jsx("span", { "aria-hidden": "true", children: total }), _jsxs("span", { className: "sr-only", children: [" ", label] })] }));
18
+ }
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import type { Message } from "@rapidmx/react-shared/mail/mailApi.js";
3
+ /**
4
+ * The state line an Outbox list row carries under its subject: "Sending...", "Retrying (attempt 2, at 10:15)", "Not sent: <reason>" or
5
+ * "Scheduled for ...". Read from the message's own `scheduledSend*` fields (see `outboxItemStatus()`), so it follows the list as the server's
6
+ * events refresh it. A failed one is the one that needs the user: red, and its reason is spelled out.
7
+ */
8
+ export default function OutboxRowStatus({ message }: {
9
+ message: Message;
10
+ }): React.JSX.Element;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { outboxItemStatus } from "../../mail/outbox/outboxState.js";
3
+ /**
4
+ * The state line an Outbox list row carries under its subject: "Sending...", "Retrying (attempt 2, at 10:15)", "Not sent: <reason>" or
5
+ * "Scheduled for ...". Read from the message's own `scheduledSend*` fields (see `outboxItemStatus()`), so it follows the list as the server's
6
+ * events refresh it. A failed one is the one that needs the user: red, and its reason is spelled out.
7
+ */
8
+ export default function OutboxRowStatus({ message }) {
9
+ const status = outboxItemStatus(message);
10
+ const at = status.at === undefined ? "" : new Date(status.at).toLocaleString();
11
+ const text = status.state === "failed"
12
+ ? `Not sent: ${status.error}`
13
+ : status.state === "retrying"
14
+ ? `Retrying (attempt ${status.attempts + 1}${at ? `, ${at}` : ""})`
15
+ : status.state === "scheduled"
16
+ ? `Scheduled for ${at}`
17
+ : "Sending…";
18
+ return (_jsxs("div", { "data-testid": "outbox-row-status", "data-state": status.state, className: ["flex items-center gap-1.5 text-xs", status.state === "failed" ? "font-semibold text-danger" : "text-text-muted"].join(" "), children: [status.state === "sending" && _jsx("span", { "aria-hidden": "true", className: "rr-sending-dot h-1.5 w-1.5 shrink-0 rounded-full bg-primary" }), _jsx("span", { className: "truncate", children: text })] }));
19
+ }