@rapidmx/web-client 0.10.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 (310) hide show
  1. package/README.md +183 -2
  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/components/admin/layout/AdminShell.tsx +22 -10
  21. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +107 -60
  22. package/apps/shared/components/admin/settings/BrandingForm.tsx +24 -0
  23. package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
  24. package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
  25. package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
  26. package/apps/shared/components/contacts/ContactForm.tsx +11 -5
  27. package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -3
  28. package/apps/shared/components/contacts/ContactsToolbar.tsx +25 -52
  29. package/apps/shared/components/contacts/layout/ContactsShell.tsx +10 -5
  30. package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
  31. package/apps/shared/components/layout/AppShell.tsx +98 -32
  32. package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
  33. package/apps/shared/components/layout/RailIcon.tsx +100 -0
  34. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
  35. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
  36. package/apps/shared/components/layout/UserMenu.tsx +75 -6
  37. package/apps/shared/components/mail/ConversationList.tsx +4 -2
  38. package/apps/shared/components/mail/ConversationThreadPane.tsx +487 -475
  39. package/apps/shared/components/mail/LazyReadingPane.tsx +14 -16
  40. package/apps/shared/components/mail/MailSelectionBar.tsx +2 -10
  41. package/apps/shared/components/mail/MessageDetailPane.tsx +366 -287
  42. package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
  43. package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
  44. package/apps/shared/components/mail/compose/ComposeContext.tsx +35 -2
  45. package/apps/shared/components/mail/compose/ComposeWindow.tsx +239 -437
  46. package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
  47. package/apps/shared/components/mail/layout/MailShell.tsx +39 -25
  48. package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
  49. package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
  50. package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
  51. package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
  52. package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
  53. package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
  54. package/apps/shared/components/mail/reading/color.ts +245 -0
  55. package/apps/shared/components/mail/reading/frameControl.ts +142 -0
  56. package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
  57. package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
  58. package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
  59. package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
  60. package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
  61. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
  62. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
  63. package/apps/shared/components/sharing/PrincipalPicker.tsx +145 -0
  64. package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
  65. package/apps/shared/components/tasks/layout/TasksShell.tsx +9 -5
  66. package/apps/shared/mail/folderCounts.ts +9 -3
  67. package/apps/shared/mail/folderTree.ts +143 -0
  68. package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
  69. package/apps/shared/mail/outbox/composeBridge.ts +69 -0
  70. package/apps/shared/mail/outbox/outboxState.ts +88 -0
  71. package/apps/shared/mail/outbox/pendingSends.ts +146 -0
  72. package/apps/shared/mail/outbox/sendDecision.ts +157 -0
  73. package/apps/shared/mail/outbox/sendJob.ts +427 -0
  74. package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
  75. package/apps/shared/mail/outbox/sendState.ts +35 -0
  76. package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
  77. package/apps/shared/mail/useMailConnection.ts +81 -28
  78. package/apps/shared/mail/useMailLiveUpdates.ts +55 -10
  79. package/apps/shared/mail/useNewMailNotifications.ts +47 -32
  80. package/apps/shared/navigation/AppRouter.tsx +2 -2
  81. package/apps/shared/notifications/NotificationCenter.tsx +215 -0
  82. package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
  83. package/apps/shared/notifications/apiErrors.ts +98 -0
  84. package/apps/shared/notifications/headerOffset.ts +35 -0
  85. package/apps/shared/notifications/pushStatus.ts +56 -0
  86. package/apps/shared/notifications/store.ts +549 -0
  87. package/apps/shared/notifications/systemErrors.ts +63 -0
  88. package/apps/shared/notifications/useNotifications.ts +41 -0
  89. package/apps/shared/signing/enrollmentStorage.ts +33 -0
  90. package/apps/shared/signing/enrollmentTracker.ts +385 -0
  91. package/apps/shared/signing/enrollmentView.ts +251 -0
  92. package/apps/shared/signing/signingInfo.ts +58 -0
  93. package/apps/shared/signing/useNow.ts +19 -0
  94. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
  95. package/apps/shared/styles/app.css +80 -0
  96. package/apps/www/_layout.tsx +8 -2
  97. package/apps/www/_routes.ts +1 -0
  98. package/apps/www/calendar/index.tsx +9 -14
  99. package/apps/www/contacts/[uid].tsx +5 -8
  100. package/apps/www/contacts/index.tsx +587 -584
  101. package/apps/www/index.tsx +38 -27
  102. package/apps/www/messages/[uid].tsx +3 -1
  103. package/apps/www/settings/appearance/index.tsx +21 -0
  104. package/apps/www/settings/auto-reply/index.tsx +3 -7
  105. package/apps/www/settings/encryption/index.tsx +151 -116
  106. package/apps/www/settings/filters/[uid].tsx +3 -1
  107. package/apps/www/settings/filters/new/index.tsx +3 -1
  108. package/apps/www/settings/privacy/index.tsx +3 -8
  109. package/apps/www/settings/read-receipts/index.tsx +3 -7
  110. package/apps/www/settings/sharing/index.tsx +44 -65
  111. package/apps/www/settings/signatures/[uid].tsx +3 -1
  112. package/apps/www/settings/signatures/new/index.tsx +3 -2
  113. package/apps/www/tasks/index.tsx +11 -16
  114. package/dist/apps/admin/_layout.d.ts +5 -1
  115. package/dist/apps/admin/_layout.js +3 -2
  116. package/dist/apps/admin/index.js +3 -2
  117. package/dist/apps/admin/ingest-queue/index.js +2 -1
  118. package/dist/apps/admin/mailboxes/[uid].js +9 -6
  119. package/dist/apps/admin/quarantine/index.js +2 -1
  120. package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
  121. package/dist/apps/admin/signing-certificates/index.js +161 -0
  122. package/dist/apps/escrow/_layout.d.ts +5 -1
  123. package/dist/apps/escrow/_layout.js +3 -2
  124. package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
  125. package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
  126. package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
  127. package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
  128. package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
  129. package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
  130. package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
  131. package/dist/apps/shared/appearance/ColorField.js +40 -0
  132. package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
  133. package/dist/apps/shared/appearance/appearanceCache.js +80 -0
  134. package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
  135. package/dist/apps/shared/appearance/appearanceContext.js +29 -0
  136. package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
  137. package/dist/apps/shared/appearance/bootScript.js +33 -0
  138. package/dist/apps/shared/appearance/color.d.ts +65 -0
  139. package/dist/apps/shared/appearance/color.js +132 -0
  140. package/dist/apps/shared/appearance/photo.d.ts +17 -0
  141. package/dist/apps/shared/appearance/photo.js +79 -0
  142. package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
  143. package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
  144. package/dist/apps/shared/appearance/theme.d.ts +111 -0
  145. package/dist/apps/shared/appearance/theme.js +251 -0
  146. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  147. package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
  148. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
  149. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
  150. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
  151. package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
  152. package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
  153. package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
  154. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
  155. package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
  156. package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
  157. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  158. package/dist/apps/shared/components/contacts/ContactsToolbar.js +24 -9
  159. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +4 -1
  160. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
  161. package/dist/apps/shared/components/layout/AppShell.d.ts +6 -1
  162. package/dist/apps/shared/components/layout/AppShell.js +59 -13
  163. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
  164. package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
  165. package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
  166. package/dist/apps/shared/components/layout/RailIcon.js +85 -0
  167. package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
  168. package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
  169. package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
  170. package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
  171. package/dist/apps/shared/components/layout/UserMenu.d.ts +7 -1
  172. package/dist/apps/shared/components/layout/UserMenu.js +41 -7
  173. package/dist/apps/shared/components/mail/ConversationList.js +3 -2
  174. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +2 -1
  175. package/dist/apps/shared/components/mail/ConversationThreadPane.js +42 -20
  176. package/dist/apps/shared/components/mail/LazyReadingPane.js +9 -8
  177. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +3 -4
  178. package/dist/apps/shared/components/mail/MailSelectionBar.js +2 -3
  179. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +18 -7
  180. package/dist/apps/shared/components/mail/MessageDetailPane.js +86 -41
  181. package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
  182. package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
  183. package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
  184. package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
  185. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +8 -0
  186. package/dist/apps/shared/components/mail/compose/ComposeContext.js +28 -3
  187. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +2 -1
  188. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +229 -328
  189. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
  190. package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
  191. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +2 -0
  192. package/dist/apps/shared/components/mail/layout/MailShell.js +20 -14
  193. package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
  194. package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
  195. package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
  196. package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
  197. package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
  198. package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
  199. package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
  200. package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
  201. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
  202. package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
  203. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
  204. package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
  205. package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
  206. package/dist/apps/shared/components/mail/reading/color.js +201 -0
  207. package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
  208. package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
  209. package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
  210. package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
  211. package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
  212. package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
  213. package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
  214. package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
  215. package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
  216. package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
  217. package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
  218. package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
  219. package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
  220. package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
  221. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
  222. package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +26 -0
  223. package/dist/apps/shared/components/sharing/PrincipalPicker.js +77 -0
  224. package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
  225. package/dist/apps/shared/components/tasks/layout/TasksShell.js +3 -1
  226. package/dist/apps/shared/mail/folderCounts.d.ts +1 -1
  227. package/dist/apps/shared/mail/folderCounts.js +9 -3
  228. package/dist/apps/shared/mail/folderTree.d.ts +55 -0
  229. package/dist/apps/shared/mail/folderTree.js +116 -0
  230. package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
  231. package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
  232. package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
  233. package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
  234. package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
  235. package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
  236. package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
  237. package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
  238. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
  239. package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
  240. package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
  241. package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
  242. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
  243. package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
  244. package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
  245. package/dist/apps/shared/mail/outbox/sendState.js +8 -0
  246. package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
  247. package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
  248. package/dist/apps/shared/mail/useMailConnection.d.ts +9 -8
  249. package/dist/apps/shared/mail/useMailConnection.js +64 -22
  250. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +17 -4
  251. package/dist/apps/shared/mail/useMailLiveUpdates.js +36 -10
  252. package/dist/apps/shared/mail/useNewMailNotifications.d.ts +4 -10
  253. package/dist/apps/shared/mail/useNewMailNotifications.js +43 -23
  254. package/dist/apps/shared/navigation/AppRouter.js +2 -2
  255. package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
  256. package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
  257. package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
  258. package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
  259. package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
  260. package/dist/apps/shared/notifications/apiErrors.js +86 -0
  261. package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
  262. package/dist/apps/shared/notifications/headerOffset.js +33 -0
  263. package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
  264. package/dist/apps/shared/notifications/pushStatus.js +54 -0
  265. package/dist/apps/shared/notifications/store.d.ts +136 -0
  266. package/dist/apps/shared/notifications/store.js +416 -0
  267. package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
  268. package/dist/apps/shared/notifications/systemErrors.js +58 -0
  269. package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
  270. package/dist/apps/shared/notifications/useNotifications.js +18 -0
  271. package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
  272. package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
  273. package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
  274. package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
  275. package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
  276. package/dist/apps/shared/signing/enrollmentView.js +214 -0
  277. package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
  278. package/dist/apps/shared/signing/signingInfo.js +48 -0
  279. package/dist/apps/shared/signing/useNow.d.ts +2 -0
  280. package/dist/apps/shared/signing/useNow.js +18 -0
  281. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
  282. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
  283. package/dist/apps/shared/styles/app.css +80 -0
  284. package/dist/apps/www/_layout.d.ts +5 -1
  285. package/dist/apps/www/_layout.js +3 -2
  286. package/dist/apps/www/_routes.js +1 -0
  287. package/dist/apps/www/calendar/index.js +3 -4
  288. package/dist/apps/www/contacts/[uid].js +5 -7
  289. package/dist/apps/www/contacts/index.js +14 -21
  290. package/dist/apps/www/index.js +37 -19
  291. package/dist/apps/www/messages/[uid].js +3 -1
  292. package/dist/apps/www/settings/appearance/index.d.ts +4 -0
  293. package/dist/apps/www/settings/appearance/index.js +13 -0
  294. package/dist/apps/www/settings/auto-reply/index.js +3 -6
  295. package/dist/apps/www/settings/encryption/index.js +103 -95
  296. package/dist/apps/www/settings/filters/[uid].js +3 -1
  297. package/dist/apps/www/settings/filters/new/index.js +3 -1
  298. package/dist/apps/www/settings/privacy/index.js +5 -8
  299. package/dist/apps/www/settings/read-receipts/index.js +3 -6
  300. package/dist/apps/www/settings/sharing/index.js +19 -33
  301. package/dist/apps/www/settings/signatures/[uid].js +3 -1
  302. package/dist/apps/www/settings/signatures/new/index.js +3 -2
  303. package/dist/apps/www/tasks/index.js +8 -14
  304. package/package.json +2 -2
  305. package/apps/shared/components/mail/NewMailToasts.tsx +0 -141
  306. package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
  307. package/dist/apps/shared/components/mail/NewMailToasts.d.ts +0 -19
  308. package/dist/apps/shared/components/mail/NewMailToasts.js +0 -55
  309. package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
  310. package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  import { useEffect, useState } from "react";
7
7
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
8
8
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
9
- import Skeleton from "@rapidmx/react-shared/components/feedback/Skeleton.js";
9
+ import { ReadingPaneSkeleton } from "./reading/MessageCard.js";
10
10
  /**
11
11
  * The reading pane (`MessageDetailPane`, and the thread pane that stacks it) is only rendered once a message is selected, and
12
12
  * it is the heaviest piece of the inbox after the compose window - message security, the body sanitizer, the labels and move
@@ -19,7 +19,7 @@ import Skeleton from "@rapidmx/react-shared/components/feedback/Skeleton.js";
19
19
  * code was already fetched. This reads the loaded component synchronously when there is one, so an already-loaded pane renders
20
20
  * on the click's own frame, and it can retry a failed download.
21
21
  */
22
- function lazyComponent(load) {
22
+ function lazyComponent(load, skeleton) {
23
23
  let loaded;
24
24
  let loading;
25
25
  /** Starts (once) loading the component's code. Rejects if it can't be downloaded - and then a later call tries again. */
@@ -47,20 +47,21 @@ function lazyComponent(load) {
47
47
  const Component = loaded;
48
48
  return _jsx(Component, { ...props });
49
49
  }
50
- return failed ? _jsx(ChunkFailed, {}) : _jsx(PaneSkeleton, {});
50
+ return failed ? _jsx(ChunkFailed, {}) : skeleton(props);
51
51
  }
52
52
  return { Lazy, start };
53
53
  }
54
- function PaneSkeleton() {
55
- return (_jsxs("div", { role: "status", "aria-busy": "true", className: "flex-1 p-6 flex flex-col gap-3", children: [_jsx("span", { className: "sr-only", children: "Loading the message" }), _jsx(Skeleton, { width: "w-2/3", height: "h-6" }), _jsx(Skeleton, { width: "w-1/3" }), _jsx(Skeleton, { height: "h-40" })] }));
56
- }
57
54
  /** A chunk that can't be downloaded (offline, or replaced by a deploy since this page loaded) is the one failure a reading
58
55
  * pane can't recover from by itself: say so, with the fix, rather than leaving a blank pane. */
59
56
  function ChunkFailed() {
60
57
  return (_jsx("div", { className: "flex-1 p-6", children: _jsxs(Alert, { children: [_jsx("p", { className: "mb-2", children: "This part of the page couldn\u2019t be loaded. Check your connection, then reload." }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => window.location.reload(), children: "Reload" })] }) }));
61
58
  }
62
- const messageDetailPane = lazyComponent(() => import("./MessageDetailPane.js"));
63
- const conversationThreadPane = lazyComponent(() => import("./ConversationThreadPane.js"));
59
+ // While the code loads the pane already draws its frame - the header card with the subject the list row knows, and a card of placeholder lines - so
60
+ // the click is answered on its own frame and the messages only fill in.
61
+ const messageDetailPane = lazyComponent(() => import("./MessageDetailPane.js"),
62
+ // `LazyMessageDetailPane` renders the empty state, not this, for no message.
63
+ (props) => _jsx(ReadingPaneSkeleton, { subject: props.message.subject }));
64
+ const conversationThreadPane = lazyComponent(() => import("./ConversationThreadPane.js"), (props) => _jsx(ReadingPaneSkeleton, { subject: props.conversation.subject, messageCount: props.conversation.messageCount }));
64
65
  /** Starts downloading the reading pane's code, for the page to call when it has nothing better to do. Never rejects. */
65
66
  export function prefetchReadingPane() {
66
67
  messageDetailPane.start().catch(() => undefined);
@@ -36,15 +36,14 @@ export interface MailSelectionBarProps {
36
36
  onArchive: () => void;
37
37
  /** Moves the selection into `folderUid`, resolving once the bulk update has settled. It resolves
38
38
  * whether or not that update was rejected: a bulk update is applied element by element, so a failure
39
- * is explained in this bar (along with the reload it triggers) rather than in the prompt, which would
40
- * be claiming the move simply didn't happen. */
39
+ * is explained by a pop-up (see `notifyApiError()`), raised by the page along with the reload it triggers, rather than in the
40
+ * prompt, which would be claiming the move simply didn't happen. */
41
41
  onMoveTo: (folderUid: string) => Promise<void>;
42
42
  onReportJunk: () => void;
43
43
  onDelete: () => void;
44
44
  /** A bulk action is in flight - every action is held until it settles, since the next one would send
45
45
  * `version`s the first has already superseded. */
46
46
  busy: boolean;
47
- error: string | null;
48
47
  /** The keyboard acts on this selection (Delete, Ctrl+Q, Ctrl+U, Insert - registered by the page): Mark read, Mark unread, Flag and
49
48
  * Delete name their shortcut in the tooltip and `aria-keyshortcuts`. */
50
49
  shortcuts?: boolean;
@@ -59,4 +58,4 @@ export interface MailSelectionBarProps {
59
58
  * target folder is the one already being viewed is disabled with a reason rather than hidden, so the set
60
59
  * of actions doesn't shift around between folders.
61
60
  */
62
- export default function MailSelectionBar({ selected, listed, totals, onSelectAll, onClearSelection, onCancel, folders, currentFolderUid, labels, mailboxUid, onLabelCreated, onFolderCreated, onApplyLabels, onSetRead, onSetFlagged, onArchive, onMoveTo, onReportJunk, onDelete, busy, error, shortcuts, }: MailSelectionBarProps): React.JSX.Element;
61
+ export default function MailSelectionBar({ selected, listed, totals, onSelectAll, onClearSelection, onCancel, folders, currentFolderUid, labels, mailboxUid, onLabelCreated, onFolderCreated, onApplyLabels, onSetRead, onSetFlagged, onArchive, onMoveTo, onReportJunk, onDelete, busy, shortcuts, }: MailSelectionBarProps): React.JSX.Element;
@@ -4,7 +4,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useState } from "react";
7
- import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
8
7
  import LabelMenuButton from "./labelMenu.js";
9
8
  import MoveToFolderDialog, { MOVE_TARGET_TYPES } from "./MoveToFolderDialog.js";
10
9
  import { ariaKeyShortcuts, withHint } from "../../keyboard/format.js";
@@ -23,7 +22,7 @@ function actionClassName() {
23
22
  * target folder is the one already being viewed is disabled with a reason rather than hidden, so the set
24
23
  * of actions doesn't shift around between folders.
25
24
  */
26
- export default function MailSelectionBar({ selected, listed, totals, onSelectAll, onClearSelection, onCancel, folders, currentFolderUid, labels, mailboxUid, onLabelCreated, onFolderCreated, onApplyLabels, onSetRead, onSetFlagged, onArchive, onMoveTo, onReportJunk, onDelete, busy, error, shortcuts, }) {
25
+ export default function MailSelectionBar({ selected, listed, totals, onSelectAll, onClearSelection, onCancel, folders, currentFolderUid, labels, mailboxUid, onLabelCreated, onFolderCreated, onApplyLabels, onSetRead, onSetFlagged, onArchive, onMoveTo, onReportJunk, onDelete, busy, shortcuts, }) {
27
26
  const env = useKeyEnvironment();
28
27
  /** `title` and `aria-keyshortcuts` for an action the keyboard also does; `reason` (why it is disabled) wins the tooltip. */
29
28
  const hint = (label, shortcut, reason) => shortcuts
@@ -52,5 +51,5 @@ export default function MailSelectionBar({ selected, listed, totals, onSelectAll
52
51
  const appliedToSome = labels.filter((label) => selected.some((m) => m.labelUids?.includes(label.uid)) && !appliedToAll.includes(label));
53
52
  return (_jsxs("div", { className: "border-b border-border", children: [_jsx(MoveToFolderDialog, { open: movePrompt, onClose: () => setMovePrompt(false), mailboxUid: mailboxUid, folders: folders, currentFolderUid: currentFolderUid, count: selected.length, onMove: onMoveTo, onFolderCreated: onFolderCreated }), _jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 bg-surface-alt", children: [_jsxs("span", { "aria-live": "polite", className: "text-sm font-semibold text-text", children: [selectedCount, totals ? ` ${totals.noun}${selectedCount === 1 ? "" : "s"}` : "", " selected"] }), _jsx("button", { type: "button", onClick: onSelectAll, disabled: allSelected || listedCount === 0, className: actionClassName(), children: "Select all" }), _jsx("button", { type: "button", onClick: onClearSelection, disabled: selectedCount === 0, className: actionClassName(), children: "Clear" }), _jsx("button", { type: "button", onClick: onCancel, className: `${actionClassName()} ml-auto`, children: "Cancel" })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-0.5 px-1.5 py-1", children: [_jsx("button", { type: "button", onClick: () => onSetRead(true), disabled: none || busy, className: actionClassName(), ...hint("Mark read", SHORTCUTS.mail.markRead), children: "Mark read" }), _jsx("button", { type: "button", onClick: () => onSetRead(false), disabled: none || busy, className: actionClassName(), ...hint("Mark unread", SHORTCUTS.mail.markUnread), children: "Mark unread" }), _jsx("button", { type: "button", onClick: () => onSetFlagged(true), disabled: none || busy, className: actionClassName(), ...hint("Flag", SHORTCUTS.mail.flag), children: "Flag" }), _jsx("button", { type: "button", onClick: () => onSetFlagged(false), disabled: none || busy, className: actionClassName(), children: "Unflag" }), _jsx("button", { type: "button", onClick: onArchive, disabled: none || busy || !!archiveReason, title: archiveReason, className: actionClassName(), children: "Archive" }), _jsx(LabelMenuButton, { "aria-label": "Apply label", label: "Apply label", className: "text-sm", labels: labels, mailboxUid: mailboxUid, onLabelCreated: onLabelCreated, applied: appliedToAll.map((label) => label.uid), partial: appliedToSome.map((label) => label.uid), onCommit: onApplyLabels, busy: busy, disabled: none || busy, note: selected.length === 1
54
53
  ? "Ticked labels are applied, unticked ones removed."
55
- : "Ticked labels are applied to every selected message and unticked ones removed from all of them; a dash means only some have that label, and leaving it alone keeps it that way.", emptyNote: "This mailbox has no labels yet.", commit: { label: "Apply" }, clear: { label: "Remove all labels" } }), _jsx("button", { type: "button", onClick: () => setMovePrompt(true), disabled: none || busy, className: actionClassName(), children: "Move to" }), _jsx("button", { type: "button", onClick: onReportJunk, disabled: none || busy || !!junkReason, title: junkReason, className: actionClassName(), children: "Report junk" }), _jsx("button", { type: "button", onClick: onDelete, disabled: none || busy || !!deleteReason, className: actionClassName(), ...hint("Delete", SHORTCUTS.mail.delete, deleteReason), children: "Delete" })] }), error && (_jsx("div", { className: "px-3 pb-2", children: _jsx(Alert, { children: error }) }))] }));
54
+ : "Ticked labels are applied to every selected message and unticked ones removed from all of them; a dash means only some have that label, and leaving it alone keeps it that way.", emptyNote: "This mailbox has no labels yet.", commit: { label: "Apply" }, clear: { label: "Remove all labels" } }), _jsx("button", { type: "button", onClick: () => setMovePrompt(true), disabled: none || busy, className: actionClassName(), children: "Move to" }), _jsx("button", { type: "button", onClick: onReportJunk, disabled: none || busy || !!junkReason, title: junkReason, className: actionClassName(), children: "Report junk" }), _jsx("button", { type: "button", onClick: onDelete, disabled: none || busy || !!deleteReason, className: actionClassName(), ...hint("Delete", SHORTCUTS.mail.delete, deleteReason), children: "Delete" })] })] }));
56
55
  }
@@ -24,9 +24,6 @@ export declare function trustSignerErrorMessage(err: unknown): string;
24
24
  /** Groups a fingerprint into 4-character blocks (`AB12 CD34 ...`), ignoring any separators it came with, so it can be
25
25
  * read out and compared with the sender over another channel. */
26
26
  export declare function formatFingerprint(fingerprint: string): string;
27
- /** Sanitizes a client-rendered body for `srcDoc` (DOMPurify plus every remote resource reference stripped - see
28
- * react-shared's `messageBodySanitizer.ts`), with the CSP meta as its very first element. */
29
- export declare function buildSecureSrcDoc(html: string): string;
30
27
  export interface MessageDetailPaneProps {
31
28
  message: Message | null;
32
29
  attachments: Attachment[];
@@ -86,10 +83,23 @@ export interface MessageDetailPaneProps {
86
83
  /** Handed a label created from the Labels menu, for the caller to add to `labels`. Without it the menu
87
84
  * offers no "New label" row, only the link to where labels are managed. */
88
85
  onLabelCreated?: (label: Label) => void;
89
- /** Rendered as one message of a thread (`ConversationThreadPane`) rather than as the pane itself: the
90
- * subject becomes a heading under the thread's own, since a document has one `h1` and the thread's is
91
- * the conversation. Everything else - the badges, the actions, the body - is identical. */
86
+ /** Rendered as one message of a thread (`ConversationThreadPane`) rather than as the pane itself: only its card is drawn - the thread
87
+ * owns the subject card, the scrolling and the `h1` - and the message's own subject appears (as an `h3`) only when it differs from the
88
+ * thread's (`threadSubject`). Everything else - the badges, the actions, the body - is identical. */
92
89
  inThread?: boolean;
90
+ /** The thread's subject, for a message of a thread to tell whether its own subject is worth showing. */
91
+ threadSubject?: string;
92
+ /** In a thread, what makes the sender line of this expanded card the button that collapses it: the id of the body it controls, whether the
93
+ * message is unread (which draws the accent bar and tint), what it does, and where to put its ref. */
94
+ threadHeader?: {
95
+ bodyId: string;
96
+ unread: boolean;
97
+ onToggle: () => void;
98
+ buttonRef: (node: HTMLButtonElement | null) => void;
99
+ };
100
+ /** Draw the Reply and Forward buttons at the foot of the card - by default for the single-message pane, and in a thread only where the
101
+ * caller asks (the newest expanded message). */
102
+ footer?: boolean;
93
103
  /**
94
104
  * Registers this pane's keyboard shortcuts - Reply, Reply all, Forward, Archive, Move to - so they act on this message. Only the message
95
105
  * the keyboard is acting on should set it: the one pane beside the list, or the opened message of a thread (each expanded message of a
@@ -98,7 +108,8 @@ export interface MessageDetailPaneProps {
98
108
  shortcuts?: boolean;
99
109
  }
100
110
  /**
101
- * A message's reading pane — header (subject/from/to/attachments) plus a sandboxed iframe for the body.
111
+ * A message's reading pane - a subject card, and under it the message as a card: header (sender, recipients, time, actions), notices, attachments,
112
+ * the body (`reading/MessageBody`: an isolated frame exactly as tall as its content, or text) and a footer. Rendered with `inThread`, only the card.
102
113
  * Shared by the desktop inline pane (`apps/www/index.tsx`, always visible alongside the message list),
103
114
  * and the mobile detail route (`apps/www/messages/[uid].tsx`, a full page on its own reached by tapping
104
115
  * a message row) — see each call site for how `message`/`attachments`/`isSentItems` are sourced. The
@@ -4,11 +4,10 @@ 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
13
  import { extractAddresses } from "@rapidmx/react-shared/crypto/mime.js";
@@ -23,7 +22,7 @@ import { prefetchComposeWindow, useCompose } from "./compose/ComposeContext.js";
23
22
  import { loadOriginalMessage, prefetchOriginalMessage } from "./compose/quotedBody.js";
24
23
  import { formatRecipient } from "./compose/recipients.js";
25
24
  import { formatMailAddress } from "@rapidmx/react-shared/mail/mailAddress.js";
26
- import { RecipientLine } from "./MailAddress.js";
25
+ import MailAddress, { RecipientLine } from "./MailAddress.js";
27
26
  import { useMailShell } from "./layout/MailShell.js";
28
27
  import { ariaKeyShortcuts, withHint } from "../../keyboard/format.js";
29
28
  import { SHORTCUTS } from "../../keyboard/keymap.js";
@@ -35,6 +34,12 @@ import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
35
34
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
36
35
  import LabelMenuButton from "./labelMenu.js";
37
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";
38
43
  /** Labels/styling for `specs/end-to-end_encryption.md`'s "Message Security Indicators" table - kept as
39
44
  * plain data (not JSX) so `SecurityIndicator` below stays a trivial lookup. "Signature failed" MUST NOT
40
45
  * read as a muted variant of "verified" (a failed signature is a stronger negative than no signature at
@@ -141,7 +146,9 @@ function IconAction({ icon, label, onClick, disabled, busy, onPrefetch, shortcut
141
146
  function SecurityIndicator({ security }) {
142
147
  const { label, className } = SECURITY_INDICATOR[security.state];
143
148
  if (security.state !== "verified_at_first_open") {
144
- 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] }));
145
152
  }
146
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] }));
147
154
  }
@@ -160,14 +167,13 @@ const GENERIC_SIGNATURE_FAILURE_MESSAGE = "This message's digital signature coul
160
167
  /** Fallback when the raw MIME for an encrypted message can't even be fetched/evaluated - an encrypted
161
168
  * message must never be mislabeled "Unprotected" just because loading failed. */
162
169
  const ENCRYPTED_LOAD_ERROR = "Couldn't load this message's encrypted content.";
163
- /** Content Security Policy prepended to every client-rendered (decrypted/verified) body's `srcDoc` - the
164
- * backstop behind the sanitizer below: nothing in the document may load any remote resource at all, only
165
- * inline styles and `data:`/`cid:` images. */
166
- const BODY_CSP_META = "<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; img-src data: cid:; style-src 'unsafe-inline'\">";
167
- /** Sanitizes a client-rendered body for `srcDoc` (DOMPurify plus every remote resource reference stripped - see
168
- * react-shared's `messageBodySanitizer.ts`), with the CSP meta as its very first element. */
169
- export function buildSecureSrcDoc(html) {
170
- 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);
171
177
  }
172
178
  function formatBytes(bytes) {
173
179
  if (bytes >= 1000000)
@@ -177,7 +183,8 @@ function formatBytes(bytes) {
177
183
  return `${bytes} B`;
178
184
  }
179
185
  /**
180
- * 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.
181
188
  * Shared by the desktop inline pane (`apps/www/index.tsx`, always visible alongside the message list),
182
189
  * and the mobile detail route (`apps/www/messages/[uid].tsx`, a full page on its own reached by tapping
183
190
  * a message row) — see each call site for how `message`/`attachments`/`isSentItems` are sourced. The
@@ -199,7 +206,7 @@ export default function MessageDetailPane(props) {
199
206
  function needsRawSecurityEvaluation(message) {
200
207
  return !!message.encrypted || message.hasAttachments;
201
208
  }
202
- function MessageDetailContent({ message: messageProp, attachments, backHref, isSentItems, onRecalled, isOutbox, draftsFolderUid, onScheduledSendCanceled, folders, onMoved, onFolderCreated, onReceiptHandled, onArchived, labels, onLabelsChanged, onLabelCreated, inThread, shortcuts, }) {
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, }) {
203
210
  // A copy re-read from the server after an Outbox action was refused (409/403) or a send lease ran out - see
204
211
  // `reloadMessage()`. A prop copy newer by `version` wins; an equal-version reload is kept, since claiming a send
205
212
  // lease need not bump `version`.
@@ -249,6 +256,14 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
249
256
  // effect could read reactively otherwise (getUnlockedKeys() is a plain module-level read, not React
250
257
  // state; see keySession.ts's own doc comment).
251
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);
252
267
  // The newest copy of this message known to the Labels popover - normally just the `message` prop,
253
268
  // but a successful label toggle's server response is held here too, so a follow-up toggle always
254
269
  // builds on the latest `labelUids`/`version` even before (or without) the caller patching its own
@@ -365,14 +380,32 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
365
380
  cancelled = true;
366
381
  };
367
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]);
368
394
  // Decrypted plaintext must not outlive the key session that produced it: the moment this mailbox's
369
395
  // keys are destroyed (logout, idle timeout, explicit lock), drop the recovered html/text and
370
- // 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.
371
398
  useEffect(() => subscribeKeySession((event) => {
372
- if (event.mailboxUid === message.mailboxUid && event.state === "locked") {
399
+ if (event.mailboxUid !== message.mailboxUid) {
400
+ return;
401
+ }
402
+ if (event.state === "locked") {
373
403
  setSecurity(null);
374
404
  setUnlockRefresh((n) => n + 1);
375
405
  }
406
+ else if (lockedRef.current) {
407
+ setUnlockRefresh((n) => n + 1);
408
+ }
376
409
  }), [message.mailboxUid]);
377
410
  // While restapi's ScheduledSendJob holds a send lease the message is being relayed: cancelling or moving it would be
378
411
  // refused (409/403), so the Outbox controls are replaced by "Sending...". When the lease runs out the message is
@@ -405,13 +438,19 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
405
438
  async function handleUnlockToView() {
406
439
  const mailboxUid = message.mailboxUid;
407
440
  const mailboxKeys = mailboxes.find((mb) => mb.uid === mailboxUid)?.keys ?? [];
441
+ setUnlocking(true);
408
442
  try {
409
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;
410
446
  setUnlockRefresh((n) => n + 1);
411
447
  }
412
448
  catch {
413
449
  // User dismissed the unlock dialog - security state stays exactly as it was.
414
450
  }
451
+ finally {
452
+ setUnlocking(false);
453
+ }
415
454
  }
416
455
  // Only ever invoked from the "Trust this signer" dialog, which only opens for an unverified-signer result carrying
417
456
  // `signerCertificate`. The pin goes on the contact in the mailbox the message belongs to; the evaluation is then
@@ -691,40 +730,46 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
691
730
  const recordedConflict = keyChanged && sameFingerprint(senderKeyState?.conflict?.observedKey.fingerprint, keyChanged.signerFingerprint)
692
731
  ? senderKeyState.conflict
693
732
  : undefined;
694
- const shownSubject = (protectedSubject ?? message.subject) || "(no subject)";
695
- /**
696
- * How tall the message body is - the one thing on this pane worth every pixel it can have.
697
- *
698
- * The same rule in both places: the pane is a full-height flex column and the body is its one growing
699
- * child, so it fills whatever the header leaves (`min-h-0` on both, or a tall body would stretch the
700
- * column past the pane instead of scrolling inside it). Inside a thread that height comes from the
701
- * message's own row, which `ConversationThreadPane` gives `min-h-full` of its scrolling list - so the
702
- * height is still the window's, resolved down the flex chain rather than written here as a `vh`
703
- * number, and it follows a resize with no breakpoint to cross.
704
- *
705
- * Why not size it to its content: that would need the document *inside* the frame to measure and
706
- * report itself, and these frames are `sandbox=""` - scripts off - because they render mail from
707
- * strangers. So a message longer than the pane keeps its own scrollbar, and everything shorter shows
708
- * whole with the frame reaching the bottom of the pane either way.
709
- */
710
- const bodyClassName = "flex-1 min-h-0 w-full";
711
- 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 &&
712
- (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: () => {
713
757
  setTrustError(null);
714
758
  setTrustConfirmOpen(true);
715
- }, 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: {
716
760
  fingerprint: keyChanged.signerFingerprint,
717
761
  emails: keyChanged.signerEmails,
718
762
  observedAt: recordedConflict?.observedAt,
719
763
  source: recordedConflict?.source,
720
- }, 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
721
765
  ? SIGNATURE_FAILURE_MESSAGE[security.signatureFailureReason]
722
- : 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", 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" } }))] }), 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
723
767
  .labelUids.map((uid) => labels.find((l) => l.uid === uid))
724
768
  .filter((l) => !!l)
725
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"]
726
770
  .filter((type) => (type === "delivery" ? message.deliveryReceiptPending : message.readReceiptPending))
727
- .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
728
- ? 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 ?? ""}`))) }))
729
- : 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" })] })] }) })] }));
730
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
+ }
@@ -1,5 +1,8 @@
1
1
  import React, { PropsWithChildren } from "react";
2
2
  import { DraftThreading } from "@rapidmx/react-shared/mail/mailApi.js";
3
+ import { ResumeCompose } from "../../../mail/outbox/composeBridge.js";
4
+ /** The CSS variable (on `<html>`) that holds how far down the screen the open compose windows start, in px. */
5
+ export declare const COMPOSE_TOP_VAR = "--rr-compose-top";
3
6
  /**
4
7
  * Starts downloading the compose window's code without opening one - on hover/focus of a Compose or Reply button and when
5
8
  * the browser is idle after the inbox has loaded - so that clicking it finds the chunk already in memory. Safe to call
@@ -32,6 +35,8 @@ export interface ComposeSession {
32
35
  quotePending?: boolean;
33
36
  /** What `OpenComposeInput.pending` resolved with, once it has - see `ComposeLateInput`. */
34
37
  late?: ComposeLateInput;
38
+ /** See `OpenComposeInput.resume`. */
39
+ resume?: ResumeCompose;
35
40
  minimized: boolean;
36
41
  }
37
42
  /** What a reply or forward could only work out after its window was already open (see `OpenComposeInput.pending`). */
@@ -83,6 +88,9 @@ export interface OpenComposeInput {
83
88
  * until it resolves.
84
89
  */
85
90
  pending?: Promise<ComposeLateInput | undefined>;
91
+ /** Re-opens a message that was already composed - a failed send's "Open draft": the window continues that server draft, with every field
92
+ * exactly as it was typed, instead of starting a new one. */
93
+ resume?: ResumeCompose;
86
94
  }
87
95
  export interface ComposeContextValue {
88
96
  /** Opens a new compose window, stacked alongside any already open (Gmail allows several at once). */