@rapidmx/web-client 0.20.0 → 0.22.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 (906) hide show
  1. package/README.md +415 -381
  2. package/apps/admin/_layout.tsx +2 -2
  3. package/apps/admin/audit-log/index.tsx +4 -4
  4. package/apps/admin/branding/index.tsx +39 -39
  5. package/apps/admin/data-requests/index.tsx +482 -482
  6. package/apps/admin/distribution-lists/[uid].tsx +3 -3
  7. package/apps/admin/distribution-lists/index.tsx +4 -4
  8. package/apps/admin/distribution-lists/new/index.tsx +5 -5
  9. package/apps/admin/domains/[uid].tsx +166 -166
  10. package/apps/admin/domains/index.tsx +4 -4
  11. package/apps/admin/domains/new/index.tsx +5 -5
  12. package/apps/admin/encryption-policy/index.tsx +1 -1
  13. package/apps/admin/escrow-scopes/[uid].tsx +350 -350
  14. package/apps/admin/escrow-scopes/index.tsx +4 -4
  15. package/apps/admin/escrow-scopes/new/index.tsx +5 -5
  16. package/apps/admin/index.tsx +129 -129
  17. package/apps/admin/ingest-queue/index.tsx +4 -4
  18. package/apps/admin/mailbox-policy/index.tsx +1 -1
  19. package/apps/admin/mailboxes/[uid].tsx +271 -271
  20. package/apps/admin/mailboxes/new/index.tsx +30 -30
  21. package/apps/admin/plugins/index.tsx +15 -15
  22. package/apps/admin/quarantine/index.tsx +5 -5
  23. package/apps/admin/retention-policy/index.tsx +39 -39
  24. package/apps/admin/signing-certificates/index.tsx +343 -343
  25. package/apps/admin/transport-rules/[uid].tsx +5 -5
  26. package/apps/admin/transport-rules/_transportRuleConfig.tsx +3 -3
  27. package/apps/admin/transport-rules/index.tsx +4 -4
  28. package/apps/admin/transport-rules/new/index.tsx +5 -5
  29. package/apps/escrow/_layout.tsx +1 -1
  30. package/apps/escrow/audit-log/index.tsx +196 -196
  31. package/apps/escrow/index.tsx +4 -4
  32. package/apps/escrow/matters/[uid].tsx +621 -621
  33. package/apps/escrow/matters/new/index.tsx +6 -6
  34. package/apps/shared/appearance/AppearanceForm.tsx +3 -3
  35. package/apps/shared/appearance/AppearanceHead.tsx +1 -1
  36. package/apps/shared/appearance/AppearanceProvider.tsx +3 -3
  37. package/apps/shared/appearance/appearanceCache.ts +1 -1
  38. package/apps/shared/appearance/appearanceContext.ts +1 -1
  39. package/apps/shared/appearance/theme.ts +1 -1
  40. package/apps/shared/auth/adminAccess.ts +99 -99
  41. package/apps/shared/calendar/bookingPlugin.ts +1 -1
  42. package/apps/shared/calendar/calendarLiveUpdates.ts +1 -1
  43. package/apps/shared/calendar/calendarReminders.ts +1 -1
  44. package/apps/shared/calendar/useCalendarReminders.ts +1 -1
  45. package/apps/shared/components/admin/diagnostics/DiagnosticsManager.tsx +1 -1
  46. package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +2 -2
  47. package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +2 -2
  48. package/apps/shared/components/admin/diagnostics/PackagesTable.tsx +1 -1
  49. package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +1 -1
  50. package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +2 -2
  51. package/apps/shared/components/admin/diagnostics/VersionsPanel.tsx +1 -1
  52. package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +1 -1
  53. package/apps/shared/components/admin/diagnostics/format.ts +1 -1
  54. package/apps/shared/components/admin/diagnostics/logClient.ts +1 -1
  55. package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
  56. package/apps/shared/components/admin/elevation.ts +1 -1
  57. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +3 -3
  58. package/apps/shared/components/admin/layout/AdminShell.tsx +384 -384
  59. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -238
  60. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +154 -152
  61. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -194
  62. package/apps/shared/components/admin/mailboxes/MailboxTable.tsx +1 -1
  63. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +19 -14
  64. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +5 -5
  65. package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
  66. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +5 -5
  67. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
  68. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +2 -2
  69. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +32 -27
  70. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
  71. package/apps/shared/components/admin/settings/PluginsManager.tsx +2093 -2093
  72. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
  73. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
  74. package/apps/shared/components/admin/setup/SetupWizard.tsx +450 -446
  75. package/apps/shared/components/admin/signOut.ts +1 -1
  76. package/apps/shared/components/admin/usePagedList.tsx +129 -129
  77. package/apps/shared/components/calendar/AppointmentQuickForm.tsx +3 -3
  78. package/apps/shared/components/calendar/CalendarListSidebar.tsx +3 -3
  79. package/apps/shared/components/calendar/DescriptionEditor.tsx +1 -1
  80. package/apps/shared/components/calendar/EventDescriptionView.tsx +1 -1
  81. package/apps/shared/components/calendar/EventDetails.tsx +16 -14
  82. package/apps/shared/components/calendar/EventEditor.tsx +54 -39
  83. package/apps/shared/components/calendar/EventExpandedForm.tsx +3 -3
  84. package/apps/shared/components/calendar/EventFormParts.tsx +2 -2
  85. package/apps/shared/components/calendar/EventModal.tsx +206 -206
  86. package/apps/shared/components/calendar/EventQuickForm.tsx +3 -3
  87. package/apps/shared/components/calendar/EventShell.tsx +1 -1
  88. package/apps/shared/components/calendar/FindATime.tsx +6 -4
  89. package/apps/shared/components/calendar/GuestInput.tsx +2 -2
  90. package/apps/shared/components/calendar/MonthView.tsx +185 -185
  91. package/apps/shared/components/calendar/QuickCreateFaces.tsx +1 -1
  92. package/apps/shared/components/calendar/QuickFaceFrame.tsx +1 -1
  93. package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
  94. package/apps/shared/components/calendar/RequestChangeForm.tsx +8 -6
  95. package/apps/shared/components/calendar/ResourcePicker.tsx +7 -5
  96. package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
  97. package/apps/shared/components/calendar/TaskQuickForm.tsx +23 -18
  98. package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
  99. package/apps/shared/components/calendar/allDay.ts +124 -124
  100. package/apps/shared/components/calendar/eventDialogFields.ts +3 -3
  101. package/apps/shared/components/calendar/eventForm.ts +3 -3
  102. package/apps/shared/components/calendar/eventFormat.ts +3 -3
  103. package/apps/shared/components/calendar/findTimeGrid.ts +2 -2
  104. package/apps/shared/components/calendar/layout/CalendarShell.tsx +11 -9
  105. package/apps/shared/components/contacts/ContactDetailPane.tsx +4 -4
  106. package/apps/shared/components/contacts/ContactForm.tsx +393 -383
  107. package/apps/shared/components/contacts/ContactsSidebar.tsx +7 -5
  108. package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -103
  109. package/apps/shared/components/contacts/KeyChangeReview.tsx +5 -5
  110. package/apps/shared/components/contacts/contactKeys.ts +3 -3
  111. package/apps/shared/components/contacts/layout/ContactsShell.tsx +9 -7
  112. package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
  113. package/apps/shared/components/forms/StringListField.tsx +1 -1
  114. package/apps/shared/components/layout/AppShell.tsx +465 -461
  115. package/apps/shared/components/layout/BrandingChrome.tsx +1 -1
  116. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
  117. package/apps/shared/components/layout/MailboxProvisioning.tsx +170 -168
  118. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +9 -9
  119. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
  120. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
  121. package/apps/shared/components/layout/UnlockPromptProvider.tsx +6 -6
  122. package/apps/shared/components/layout/UserMenu.tsx +441 -439
  123. package/apps/shared/components/mail/ConversationList.tsx +334 -332
  124. package/apps/shared/components/mail/ConversationThreadPane.tsx +624 -613
  125. package/apps/shared/components/mail/InviteCard.tsx +11 -9
  126. package/apps/shared/components/mail/LazyReadingPane.tsx +2 -2
  127. package/apps/shared/components/mail/MailAddress.tsx +1 -1
  128. package/apps/shared/components/mail/MailListToolbar.tsx +2 -2
  129. package/apps/shared/components/mail/MailSelectionBar.tsx +240 -240
  130. package/apps/shared/components/mail/MenuButton.tsx +404 -404
  131. package/apps/shared/components/mail/MessageDetailPane.tsx +1759 -1757
  132. package/apps/shared/components/mail/MoveToFolderDialog.tsx +8 -6
  133. package/apps/shared/components/mail/OutboxRowStatus.tsx +1 -1
  134. package/apps/shared/components/mail/PermanentDeleteDialog.tsx +2 -2
  135. package/apps/shared/components/mail/compose/ComposeContext.tsx +323 -312
  136. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
  137. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1709 -1681
  138. package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +4 -4
  139. package/apps/shared/components/mail/compose/EmojiPicker.tsx +2 -2
  140. package/apps/shared/components/mail/compose/GifPicker.tsx +3 -3
  141. package/apps/shared/components/mail/compose/RecipientInput.tsx +1 -1
  142. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
  143. package/apps/shared/components/mail/compose/ScheduleSendPicker.tsx +1 -1
  144. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
  145. package/apps/shared/components/mail/compose/encryptionRequirement.ts +2 -2
  146. package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
  147. package/apps/shared/components/mail/compose/recipients.ts +1 -1
  148. package/apps/shared/components/mail/invite/InviteRowChip.tsx +2 -2
  149. package/apps/shared/components/mail/invite/InviteRsvpPopover.tsx +9 -7
  150. package/apps/shared/components/mail/invite/InviteTimeline.tsx +1 -1
  151. package/apps/shared/components/mail/invite/ProposeTimeForm.tsx +2 -2
  152. package/apps/shared/components/mail/invite/inviteFormat.ts +2 -2
  153. package/apps/shared/components/mail/invite/inviteStore.ts +14 -6
  154. package/apps/shared/components/mail/labelMenu.tsx +9 -7
  155. package/apps/shared/components/mail/layout/MailShell.tsx +4 -4
  156. package/apps/shared/components/mail/listPreferences.ts +2 -2
  157. package/apps/shared/components/mail/pinnedSigners.ts +45 -19
  158. package/apps/shared/components/mail/reading/EncryptedBody.tsx +2 -2
  159. package/apps/shared/components/mail/reading/MessageBody.tsx +6 -6
  160. package/apps/shared/components/mail/reading/MessageCard.tsx +3 -3
  161. package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -258
  162. package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -79
  163. package/apps/shared/components/mail/reading/PendingMessageCard.tsx +1 -1
  164. package/apps/shared/components/mail/reading/bodyContent.ts +3 -3
  165. package/apps/shared/components/mail/reading/bodyHtml.ts +1 -1
  166. package/apps/shared/components/mail/reading/messageExport.ts +59 -59
  167. package/apps/shared/components/mail/reading/printMessage.ts +99 -99
  168. package/apps/shared/components/mail/reading/useMessageActions.ts +450 -443
  169. package/apps/shared/components/mail/unreadStyle.tsx +1 -1
  170. package/apps/shared/components/mail/verificationSeals.ts +129 -125
  171. package/apps/shared/components/mail/writableMailboxes.ts +14 -7
  172. package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
  173. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
  174. package/apps/shared/components/settings/layout/SettingsShell.tsx +8 -6
  175. package/apps/shared/components/sharing/PrincipalListField.tsx +1 -1
  176. package/apps/shared/components/sharing/PrincipalPicker.tsx +1 -1
  177. package/apps/shared/components/sharing/PrincipalResolver.tsx +4 -4
  178. package/apps/shared/components/tasks/TasksSidebar.tsx +7 -5
  179. package/apps/shared/components/tasks/layout/TasksShell.tsx +9 -7
  180. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
  181. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
  182. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
  183. package/apps/shared/keyboard/dispatch.ts +124 -124
  184. package/apps/shared/keyboard/format.ts +89 -89
  185. package/apps/shared/keyboard/keymap.ts +114 -114
  186. package/apps/shared/keyboard/registry.ts +65 -65
  187. package/apps/shared/keyboard/targets.ts +79 -79
  188. package/apps/shared/mail/findWellKnownFolderUid.ts +6 -4
  189. package/apps/shared/mail/folderCounts.ts +8 -4
  190. package/apps/shared/mail/folderOfType.ts +51 -49
  191. package/apps/shared/mail/folderTree.ts +143 -143
  192. package/apps/shared/mail/listAllPages.ts +39 -39
  193. package/apps/shared/mail/listSnapshots.ts +2 -2
  194. package/apps/shared/mail/messageReadState.ts +6 -5
  195. package/apps/shared/mail/newMailNotifications.ts +171 -171
  196. package/apps/shared/mail/outbox/composeBridge.ts +2 -2
  197. package/apps/shared/mail/outbox/outboxState.ts +1 -1
  198. package/apps/shared/mail/outbox/outgoingReplies.ts +2 -2
  199. package/apps/shared/mail/outbox/sendDecision.ts +2 -2
  200. package/apps/shared/mail/outbox/sendJob.ts +450 -445
  201. package/apps/shared/mail/outbox/sendOutcomes.ts +160 -155
  202. package/apps/shared/mail/outbox/useOutboxStatus.ts +5 -3
  203. package/apps/shared/mail/permanentDelete.ts +8 -7
  204. package/apps/shared/mail/primaryMailbox.ts +1 -1
  205. package/apps/shared/mail/reportNotices.ts +66 -66
  206. package/apps/shared/mail/senderBlocking.ts +157 -141
  207. package/apps/shared/mail/useMailConnection.ts +207 -205
  208. package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
  209. package/apps/shared/mail/useMailboxUpdateAccess.ts +84 -60
  210. package/apps/shared/mail/useMarkMessageRead.ts +54 -47
  211. package/apps/shared/mail/useNewMailNotifications.ts +178 -178
  212. package/apps/shared/mail/usePermanentDelete.tsx +5 -3
  213. package/apps/shared/navigation/pageTitle.ts +1 -1
  214. package/apps/shared/notifications/NotificationCenter.tsx +1 -1
  215. package/apps/shared/notifications/NotificationHistoryDialog.tsx +2 -2
  216. package/apps/shared/notifications/apiErrors.ts +2 -2
  217. package/apps/shared/notifications/pushStatus.ts +1 -1
  218. package/apps/shared/notifications/store.ts +560 -560
  219. package/apps/shared/notifications/systemErrors.ts +1 -1
  220. package/apps/shared/search/LocalIndexLifecycle.tsx +120 -114
  221. package/apps/shared/search/crossMailboxSearch.ts +3 -3
  222. package/apps/shared/search/localIndexBuilder.ts +493 -481
  223. package/apps/shared/search/localIndexKey.ts +1 -1
  224. package/apps/shared/search/localIndexRpcClient.ts +150 -30
  225. package/apps/shared/search/localIndexTransport.ts +84 -0
  226. package/apps/shared/search/localIndexWorker.ts +1 -1
  227. package/apps/shared/search/searchTier2.ts +3 -3
  228. package/apps/shared/signing/enrollmentStorage.ts +33 -33
  229. package/apps/shared/signing/enrollmentTracker.ts +385 -385
  230. package/apps/shared/signing/enrollmentView.ts +251 -251
  231. package/apps/shared/signing/signingInfo.ts +1 -1
  232. package/apps/shared/signing/useNow.ts +19 -19
  233. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +92 -90
  234. package/apps/shared/styles/app.css +387 -396
  235. package/apps/www/_layout.tsx +2 -2
  236. package/apps/www/calendar/index.tsx +583 -581
  237. package/apps/www/contacts/[uid].tsx +114 -112
  238. package/apps/www/contacts/index.tsx +19 -17
  239. package/apps/www/index.tsx +3021 -3012
  240. package/apps/www/messages/[uid].tsx +141 -139
  241. package/apps/www/settings/auto-reply/index.tsx +138 -136
  242. package/apps/www/settings/blocked-senders/index.tsx +305 -303
  243. package/apps/www/settings/encryption/index.tsx +1296 -1290
  244. package/apps/www/settings/filters/[uid].tsx +184 -179
  245. package/apps/www/settings/filters/_mailFilterRuleConfig.tsx +2 -2
  246. package/apps/www/settings/filters/index.tsx +105 -105
  247. package/apps/www/settings/filters/new/index.tsx +167 -165
  248. package/apps/www/settings/labels/index.tsx +209 -207
  249. package/apps/www/settings/privacy/index.tsx +499 -495
  250. package/apps/www/settings/profile/index.tsx +256 -251
  251. package/apps/www/settings/read-receipts/index.tsx +155 -150
  252. package/apps/www/settings/sharing/index.tsx +259 -259
  253. package/apps/www/settings/signatures/[uid].tsx +175 -175
  254. package/apps/www/settings/signatures/index.tsx +91 -91
  255. package/apps/www/settings/signatures/new/index.tsx +138 -138
  256. package/apps/www/settings/signatures/signatureDefaults.ts +1 -1
  257. package/apps/www/tasks/index.tsx +659 -654
  258. package/dist/apps/admin/_layout.d.ts +1 -1
  259. package/dist/apps/admin/_layout.js +1 -1
  260. package/dist/apps/admin/audit-log/index.d.ts +1 -1
  261. package/dist/apps/admin/audit-log/index.js +4 -4
  262. package/dist/apps/admin/branding/index.js +3 -3
  263. package/dist/apps/admin/data-requests/index.js +8 -8
  264. package/dist/apps/admin/distribution-lists/[uid].js +3 -3
  265. package/dist/apps/admin/distribution-lists/index.js +4 -4
  266. package/dist/apps/admin/distribution-lists/new/index.js +5 -5
  267. package/dist/apps/admin/domains/[uid].js +6 -6
  268. package/dist/apps/admin/domains/index.js +4 -4
  269. package/dist/apps/admin/domains/new/index.js +5 -5
  270. package/dist/apps/admin/encryption-policy/index.js +1 -1
  271. package/dist/apps/admin/escrow-scopes/[uid].d.ts +1 -1
  272. package/dist/apps/admin/escrow-scopes/[uid].js +7 -7
  273. package/dist/apps/admin/escrow-scopes/index.js +4 -4
  274. package/dist/apps/admin/escrow-scopes/new/index.js +5 -5
  275. package/dist/apps/admin/index.js +6 -6
  276. package/dist/apps/admin/ingest-queue/index.js +4 -4
  277. package/dist/apps/admin/mailbox-policy/index.js +1 -1
  278. package/dist/apps/admin/mailboxes/[uid].js +5 -5
  279. package/dist/apps/admin/quarantine/index.js +5 -5
  280. package/dist/apps/admin/retention-policy/index.js +3 -3
  281. package/dist/apps/admin/signing-certificates/index.js +5 -5
  282. package/dist/apps/admin/transport-rules/[uid].js +5 -5
  283. package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +1 -1
  284. package/dist/apps/admin/transport-rules/_transportRuleConfig.js +2 -2
  285. package/dist/apps/admin/transport-rules/index.js +4 -4
  286. package/dist/apps/admin/transport-rules/new/index.js +5 -5
  287. package/dist/apps/escrow/_layout.d.ts +1 -1
  288. package/dist/apps/escrow/audit-log/index.d.ts +1 -1
  289. package/dist/apps/escrow/audit-log/index.js +4 -4
  290. package/dist/apps/escrow/index.js +4 -4
  291. package/dist/apps/escrow/matters/[uid].js +10 -10
  292. package/dist/apps/escrow/matters/new/index.js +6 -6
  293. package/dist/apps/shared/appearance/AppearanceForm.js +3 -3
  294. package/dist/apps/shared/appearance/AppearanceHead.js +1 -1
  295. package/dist/apps/shared/appearance/AppearanceProvider.d.ts +1 -1
  296. package/dist/apps/shared/appearance/AppearanceProvider.js +3 -3
  297. package/dist/apps/shared/appearance/appearanceCache.d.ts +1 -1
  298. package/dist/apps/shared/appearance/appearanceCache.js +1 -1
  299. package/dist/apps/shared/appearance/appearanceContext.d.ts +1 -1
  300. package/dist/apps/shared/appearance/theme.d.ts +1 -1
  301. package/dist/apps/shared/auth/adminAccess.js +1 -1
  302. package/dist/apps/shared/calendar/bookingPlugin.js +1 -1
  303. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +1 -1
  304. package/dist/apps/shared/calendar/calendarReminders.d.ts +1 -1
  305. package/dist/apps/shared/calendar/useCalendarReminders.js +1 -1
  306. package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.js +1 -1
  307. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.d.ts +1 -1
  308. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +2 -2
  309. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +2 -2
  310. package/dist/apps/shared/components/admin/diagnostics/PackagesTable.js +1 -1
  311. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
  312. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +2 -2
  313. package/dist/apps/shared/components/admin/diagnostics/VersionsPanel.js +1 -1
  314. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.js +1 -1
  315. package/dist/apps/shared/components/admin/diagnostics/format.js +1 -1
  316. package/dist/apps/shared/components/admin/diagnostics/logClient.js +1 -1
  317. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.d.ts +1 -1
  318. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
  319. package/dist/apps/shared/components/admin/elevation.js +1 -1
  320. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +3 -3
  321. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  322. package/dist/apps/shared/components/admin/layout/AdminShell.js +8 -8
  323. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +1 -1
  324. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +6 -6
  325. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +1 -1
  326. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +10 -8
  327. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
  328. package/dist/apps/shared/components/admin/mailboxes/MailboxTable.d.ts +1 -1
  329. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.d.ts +1 -1
  330. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +7 -5
  331. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +5 -5
  332. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +1 -1
  333. package/dist/apps/shared/components/admin/settings/BrandingForm.js +8 -8
  334. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +1 -1
  335. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +5 -5
  336. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +1 -1
  337. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +4 -4
  338. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +2 -2
  339. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.d.ts +1 -1
  340. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +14 -12
  341. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +1 -1
  342. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +4 -4
  343. package/dist/apps/shared/components/admin/settings/PluginsManager.js +5 -5
  344. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +1 -1
  345. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +5 -5
  346. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +7 -7
  347. package/dist/apps/shared/components/admin/setup/SetupWizard.js +26 -22
  348. package/dist/apps/shared/components/admin/signOut.js +1 -1
  349. package/dist/apps/shared/components/admin/usePagedList.js +2 -2
  350. package/dist/apps/shared/components/calendar/AppointmentQuickForm.js +3 -3
  351. package/dist/apps/shared/components/calendar/CalendarListSidebar.d.ts +1 -1
  352. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +2 -2
  353. package/dist/apps/shared/components/calendar/DescriptionEditor.js +1 -1
  354. package/dist/apps/shared/components/calendar/EventDescriptionView.js +1 -1
  355. package/dist/apps/shared/components/calendar/EventDetails.d.ts +2 -2
  356. package/dist/apps/shared/components/calendar/EventDetails.js +16 -14
  357. package/dist/apps/shared/components/calendar/EventEditor.d.ts +2 -2
  358. package/dist/apps/shared/components/calendar/EventEditor.js +22 -20
  359. package/dist/apps/shared/components/calendar/EventExpandedForm.js +2 -2
  360. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +1 -1
  361. package/dist/apps/shared/components/calendar/EventFormParts.js +1 -1
  362. package/dist/apps/shared/components/calendar/EventModal.d.ts +2 -2
  363. package/dist/apps/shared/components/calendar/EventModal.js +3 -3
  364. package/dist/apps/shared/components/calendar/EventQuickForm.js +3 -3
  365. package/dist/apps/shared/components/calendar/EventShell.js +1 -1
  366. package/dist/apps/shared/components/calendar/FindATime.js +6 -4
  367. package/dist/apps/shared/components/calendar/GuestInput.d.ts +2 -2
  368. package/dist/apps/shared/components/calendar/MonthView.d.ts +1 -1
  369. package/dist/apps/shared/components/calendar/MonthView.js +1 -1
  370. package/dist/apps/shared/components/calendar/QuickCreateFaces.js +1 -1
  371. package/dist/apps/shared/components/calendar/QuickFaceFrame.js +1 -1
  372. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +1 -1
  373. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +1 -1
  374. package/dist/apps/shared/components/calendar/RequestChangeForm.js +8 -6
  375. package/dist/apps/shared/components/calendar/ResourcePicker.d.ts +1 -1
  376. package/dist/apps/shared/components/calendar/ResourcePicker.js +7 -5
  377. package/dist/apps/shared/components/calendar/SplitDayView.d.ts +1 -1
  378. package/dist/apps/shared/components/calendar/TaskQuickForm.d.ts +1 -1
  379. package/dist/apps/shared/components/calendar/TaskQuickForm.js +9 -7
  380. package/dist/apps/shared/components/calendar/TimeGridView.d.ts +1 -1
  381. package/dist/apps/shared/components/calendar/TimeGridView.js +1 -1
  382. package/dist/apps/shared/components/calendar/allDay.d.ts +2 -2
  383. package/dist/apps/shared/components/calendar/allDay.js +1 -1
  384. package/dist/apps/shared/components/calendar/eventDialogFields.d.ts +2 -2
  385. package/dist/apps/shared/components/calendar/eventDialogFields.js +2 -2
  386. package/dist/apps/shared/components/calendar/eventForm.d.ts +3 -3
  387. package/dist/apps/shared/components/calendar/eventFormat.d.ts +1 -1
  388. package/dist/apps/shared/components/calendar/eventFormat.js +2 -2
  389. package/dist/apps/shared/components/calendar/findTimeGrid.d.ts +1 -1
  390. package/dist/apps/shared/components/calendar/findTimeGrid.js +1 -1
  391. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +1 -1
  392. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +11 -9
  393. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +1 -1
  394. package/dist/apps/shared/components/contacts/ContactDetailPane.js +2 -2
  395. package/dist/apps/shared/components/contacts/ContactForm.d.ts +2 -2
  396. package/dist/apps/shared/components/contacts/ContactForm.js +12 -8
  397. package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +1 -1
  398. package/dist/apps/shared/components/contacts/ContactsSidebar.js +7 -5
  399. package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +1 -1
  400. package/dist/apps/shared/components/contacts/KeyChangeReview.js +5 -5
  401. package/dist/apps/shared/components/contacts/contactKeys.d.ts +2 -2
  402. package/dist/apps/shared/components/contacts/contactKeys.js +2 -2
  403. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +1 -1
  404. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +9 -7
  405. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -7
  406. package/dist/apps/shared/components/forms/StringListField.js +1 -1
  407. package/dist/apps/shared/components/layout/AppShell.d.ts +2 -2
  408. package/dist/apps/shared/components/layout/AppShell.js +14 -10
  409. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +1 -1
  410. package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +1 -1
  411. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +9 -9
  412. package/dist/apps/shared/components/layout/MailboxProvisioning.js +8 -6
  413. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +1 -1
  414. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +9 -9
  415. package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +2 -2
  416. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +5 -5
  417. package/dist/apps/shared/components/layout/UserMenu.js +6 -4
  418. package/dist/apps/shared/components/mail/ConversationList.d.ts +2 -2
  419. package/dist/apps/shared/components/mail/ConversationList.js +6 -4
  420. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +3 -3
  421. package/dist/apps/shared/components/mail/ConversationThreadPane.js +13 -11
  422. package/dist/apps/shared/components/mail/InviteCard.js +10 -8
  423. package/dist/apps/shared/components/mail/LazyReadingPane.js +2 -2
  424. package/dist/apps/shared/components/mail/MailAddress.d.ts +1 -1
  425. package/dist/apps/shared/components/mail/MailAddress.js +1 -1
  426. package/dist/apps/shared/components/mail/MailListToolbar.d.ts +2 -2
  427. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +2 -2
  428. package/dist/apps/shared/components/mail/MenuButton.js +1 -1
  429. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +3 -3
  430. package/dist/apps/shared/components/mail/MessageDetailPane.js +30 -28
  431. package/dist/apps/shared/components/mail/MoveToFolderDialog.d.ts +1 -1
  432. package/dist/apps/shared/components/mail/MoveToFolderDialog.js +8 -6
  433. package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +1 -1
  434. package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +2 -2
  435. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +10 -1
  436. package/dist/apps/shared/components/mail/compose/ComposeContext.js +4 -2
  437. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +7 -1
  438. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +54 -31
  439. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +4 -4
  440. package/dist/apps/shared/components/mail/compose/EmojiPicker.js +2 -2
  441. package/dist/apps/shared/components/mail/compose/GifPicker.js +3 -3
  442. package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +1 -1
  443. package/dist/apps/shared/components/mail/compose/RecipientInput.js +1 -1
  444. package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.js +1 -1
  445. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +2 -2
  446. package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +1 -1
  447. package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +3 -3
  448. package/dist/apps/shared/components/mail/compose/quotedBody.js +3 -3
  449. package/dist/apps/shared/components/mail/compose/recipients.d.ts +1 -1
  450. package/dist/apps/shared/components/mail/invite/InviteRowChip.d.ts +1 -1
  451. package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.d.ts +1 -1
  452. package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.js +9 -7
  453. package/dist/apps/shared/components/mail/invite/InviteTimeline.d.ts +1 -1
  454. package/dist/apps/shared/components/mail/invite/ProposeTimeForm.d.ts +1 -1
  455. package/dist/apps/shared/components/mail/invite/ProposeTimeForm.js +1 -1
  456. package/dist/apps/shared/components/mail/invite/inviteFormat.d.ts +2 -2
  457. package/dist/apps/shared/components/mail/invite/inviteStore.d.ts +9 -3
  458. package/dist/apps/shared/components/mail/invite/inviteStore.js +13 -6
  459. package/dist/apps/shared/components/mail/labelMenu.d.ts +1 -1
  460. package/dist/apps/shared/components/mail/labelMenu.js +9 -7
  461. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +1 -1
  462. package/dist/apps/shared/components/mail/layout/MailShell.js +3 -3
  463. package/dist/apps/shared/components/mail/listPreferences.d.ts +2 -2
  464. package/dist/apps/shared/components/mail/listPreferences.js +1 -1
  465. package/dist/apps/shared/components/mail/pinnedSigners.d.ts +12 -5
  466. package/dist/apps/shared/components/mail/pinnedSigners.js +37 -18
  467. package/dist/apps/shared/components/mail/reading/EncryptedBody.js +2 -2
  468. package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +2 -2
  469. package/dist/apps/shared/components/mail/reading/MessageBody.js +4 -4
  470. package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +1 -1
  471. package/dist/apps/shared/components/mail/reading/MessageCard.js +2 -2
  472. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +3 -3
  473. package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +1 -1
  474. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +2 -2
  475. package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -2
  476. package/dist/apps/shared/components/mail/reading/bodyHtml.js +1 -1
  477. package/dist/apps/shared/components/mail/reading/messageExport.js +1 -1
  478. package/dist/apps/shared/components/mail/reading/printMessage.d.ts +2 -2
  479. package/dist/apps/shared/components/mail/reading/printMessage.js +11 -11
  480. package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +1 -1
  481. package/dist/apps/shared/components/mail/reading/useMessageActions.js +18 -16
  482. package/dist/apps/shared/components/mail/unreadStyle.d.ts +1 -1
  483. package/dist/apps/shared/components/mail/verificationSeals.d.ts +27 -6
  484. package/dist/apps/shared/components/mail/verificationSeals.js +15 -11
  485. package/dist/apps/shared/components/mail/writableMailboxes.d.ts +8 -3
  486. package/dist/apps/shared/components/mail/writableMailboxes.js +12 -6
  487. package/dist/apps/shared/components/rules/RuleBuilder.js +1 -1
  488. package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +1 -1
  489. package/dist/apps/shared/components/settings/SigningCertificateCard.js +3 -3
  490. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +1 -1
  491. package/dist/apps/shared/components/settings/layout/SettingsShell.js +8 -6
  492. package/dist/apps/shared/components/sharing/PrincipalListField.d.ts +1 -1
  493. package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +1 -1
  494. package/dist/apps/shared/components/sharing/PrincipalPicker.js +1 -1
  495. package/dist/apps/shared/components/sharing/PrincipalResolver.d.ts +1 -1
  496. package/dist/apps/shared/components/sharing/PrincipalResolver.js +3 -3
  497. package/dist/apps/shared/components/tasks/TasksSidebar.d.ts +1 -1
  498. package/dist/apps/shared/components/tasks/TasksSidebar.js +7 -5
  499. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +1 -1
  500. package/dist/apps/shared/components/tasks/layout/TasksShell.js +9 -7
  501. package/dist/apps/shared/keyboard/ShortcutsDialog.js +1 -1
  502. package/dist/apps/shared/mail/findWellKnownFolderUid.d.ts +5 -3
  503. package/dist/apps/shared/mail/findWellKnownFolderUid.js +5 -4
  504. package/dist/apps/shared/mail/folderCounts.d.ts +1 -1
  505. package/dist/apps/shared/mail/folderCounts.js +6 -4
  506. package/dist/apps/shared/mail/folderOfType.d.ts +3 -2
  507. package/dist/apps/shared/mail/folderOfType.js +4 -4
  508. package/dist/apps/shared/mail/folderTree.d.ts +1 -1
  509. package/dist/apps/shared/mail/listSnapshots.d.ts +2 -2
  510. package/dist/apps/shared/mail/messageReadState.d.ts +4 -3
  511. package/dist/apps/shared/mail/messageReadState.js +5 -5
  512. package/dist/apps/shared/mail/newMailNotifications.d.ts +1 -1
  513. package/dist/apps/shared/mail/newMailNotifications.js +1 -1
  514. package/dist/apps/shared/mail/outbox/composeBridge.d.ts +2 -2
  515. package/dist/apps/shared/mail/outbox/outboxState.d.ts +1 -1
  516. package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +2 -2
  517. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +2 -2
  518. package/dist/apps/shared/mail/outbox/sendDecision.js +1 -1
  519. package/dist/apps/shared/mail/outbox/sendJob.d.ts +10 -6
  520. package/dist/apps/shared/mail/outbox/sendJob.js +39 -35
  521. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +6 -3
  522. package/dist/apps/shared/mail/outbox/sendOutcomes.js +19 -15
  523. package/dist/apps/shared/mail/outbox/useOutboxStatus.js +5 -3
  524. package/dist/apps/shared/mail/permanentDelete.d.ts +4 -3
  525. package/dist/apps/shared/mail/permanentDelete.js +8 -8
  526. package/dist/apps/shared/mail/primaryMailbox.d.ts +1 -1
  527. package/dist/apps/shared/mail/primaryMailbox.js +1 -1
  528. package/dist/apps/shared/mail/reportNotices.d.ts +1 -1
  529. package/dist/apps/shared/mail/senderBlocking.d.ts +5 -4
  530. package/dist/apps/shared/mail/senderBlocking.js +19 -16
  531. package/dist/apps/shared/mail/useMailConnection.d.ts +1 -1
  532. package/dist/apps/shared/mail/useMailConnection.js +12 -10
  533. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +2 -2
  534. package/dist/apps/shared/mail/useMailLiveUpdates.js +2 -2
  535. package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +3 -2
  536. package/dist/apps/shared/mail/useMailboxUpdateAccess.js +31 -10
  537. package/dist/apps/shared/mail/useMarkMessageRead.d.ts +1 -1
  538. package/dist/apps/shared/mail/useMarkMessageRead.js +5 -3
  539. package/dist/apps/shared/mail/useNewMailNotifications.d.ts +1 -1
  540. package/dist/apps/shared/mail/usePermanentDelete.d.ts +1 -1
  541. package/dist/apps/shared/mail/usePermanentDelete.js +4 -2
  542. package/dist/apps/shared/navigation/pageTitle.d.ts +1 -1
  543. package/dist/apps/shared/notifications/NotificationCenter.js +1 -1
  544. package/dist/apps/shared/notifications/NotificationHistoryDialog.js +2 -2
  545. package/dist/apps/shared/notifications/apiErrors.js +2 -2
  546. package/dist/apps/shared/notifications/pushStatus.js +1 -1
  547. package/dist/apps/shared/notifications/systemErrors.js +1 -1
  548. package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +2 -2
  549. package/dist/apps/shared/search/LocalIndexLifecycle.js +15 -10
  550. package/dist/apps/shared/search/crossMailboxSearch.d.ts +1 -1
  551. package/dist/apps/shared/search/crossMailboxSearch.js +2 -2
  552. package/dist/apps/shared/search/localIndexBuilder.d.ts +4 -3
  553. package/dist/apps/shared/search/localIndexBuilder.js +15 -12
  554. package/dist/apps/shared/search/localIndexKey.js +1 -1
  555. package/dist/apps/shared/search/localIndexRpcClient.d.ts +43 -31
  556. package/dist/apps/shared/search/localIndexRpcClient.js +113 -20
  557. package/dist/apps/shared/search/localIndexTransport.d.ts +77 -0
  558. package/dist/apps/shared/search/localIndexTransport.js +1 -0
  559. package/dist/apps/shared/search/localIndexWorker.d.ts +1 -1
  560. package/dist/apps/shared/search/searchTier2.d.ts +3 -3
  561. package/dist/apps/shared/signing/enrollmentTracker.d.ts +1 -1
  562. package/dist/apps/shared/signing/enrollmentTracker.js +3 -3
  563. package/dist/apps/shared/signing/enrollmentView.d.ts +1 -1
  564. package/dist/apps/shared/signing/signingInfo.d.ts +1 -1
  565. package/dist/apps/shared/signing/signingInfo.js +1 -1
  566. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +1 -1
  567. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +5 -3
  568. package/dist/apps/shared/styles/app.css +387 -396
  569. package/dist/apps/www/_layout.d.ts +1 -1
  570. package/dist/apps/www/_layout.js +1 -1
  571. package/dist/apps/www/calendar/index.d.ts +1 -1
  572. package/dist/apps/www/calendar/index.js +20 -18
  573. package/dist/apps/www/contacts/[uid].d.ts +1 -1
  574. package/dist/apps/www/contacts/[uid].js +8 -6
  575. package/dist/apps/www/contacts/index.d.ts +1 -1
  576. package/dist/apps/www/contacts/index.js +19 -17
  577. package/dist/apps/www/index.d.ts +1 -1
  578. package/dist/apps/www/index.js +54 -50
  579. package/dist/apps/www/messages/[uid].d.ts +1 -1
  580. package/dist/apps/www/messages/[uid].js +11 -9
  581. package/dist/apps/www/settings/appearance/index.d.ts +1 -1
  582. package/dist/apps/www/settings/auto-reply/index.d.ts +1 -1
  583. package/dist/apps/www/settings/auto-reply/index.js +6 -4
  584. package/dist/apps/www/settings/blocked-senders/index.d.ts +1 -1
  585. package/dist/apps/www/settings/blocked-senders/index.js +6 -4
  586. package/dist/apps/www/settings/encryption/index.d.ts +1 -1
  587. package/dist/apps/www/settings/encryption/index.js +31 -29
  588. package/dist/apps/www/settings/filters/[uid].d.ts +1 -1
  589. package/dist/apps/www/settings/filters/[uid].js +11 -9
  590. package/dist/apps/www/settings/filters/_mailFilterRuleConfig.d.ts +2 -2
  591. package/dist/apps/www/settings/filters/index.d.ts +1 -1
  592. package/dist/apps/www/settings/filters/index.js +4 -4
  593. package/dist/apps/www/settings/filters/new/index.d.ts +1 -1
  594. package/dist/apps/www/settings/filters/new/index.js +10 -8
  595. package/dist/apps/www/settings/labels/index.d.ts +1 -1
  596. package/dist/apps/www/settings/labels/index.js +12 -10
  597. package/dist/apps/www/settings/privacy/index.d.ts +1 -1
  598. package/dist/apps/www/settings/privacy/index.js +18 -14
  599. package/dist/apps/www/settings/profile/index.d.ts +1 -1
  600. package/dist/apps/www/settings/profile/index.js +7 -5
  601. package/dist/apps/www/settings/read-receipts/index.d.ts +1 -1
  602. package/dist/apps/www/settings/read-receipts/index.js +5 -3
  603. package/dist/apps/www/settings/sharing/index.d.ts +1 -1
  604. package/dist/apps/www/settings/sharing/index.js +6 -6
  605. package/dist/apps/www/settings/signatures/[uid].d.ts +1 -1
  606. package/dist/apps/www/settings/signatures/[uid].js +5 -5
  607. package/dist/apps/www/settings/signatures/index.d.ts +1 -1
  608. package/dist/apps/www/settings/signatures/index.js +4 -4
  609. package/dist/apps/www/settings/signatures/new/index.d.ts +1 -1
  610. package/dist/apps/www/settings/signatures/new/index.js +4 -4
  611. package/dist/apps/www/settings/signatures/signatureDefaults.d.ts +1 -1
  612. package/dist/apps/www/settings/signatures/signatureDefaults.js +1 -1
  613. package/dist/apps/www/tasks/index.d.ts +1 -1
  614. package/dist/apps/www/tasks/index.js +17 -15
  615. package/dist/lib/admin/auditLogApi.d.ts +32 -0
  616. package/dist/lib/admin/auditLogApi.js +27 -0
  617. package/dist/lib/admin/distributionListsApi.d.ts +47 -0
  618. package/dist/lib/admin/distributionListsApi.js +34 -0
  619. package/dist/lib/admin/domainsApi.d.ts +75 -0
  620. package/dist/lib/admin/domainsApi.js +44 -0
  621. package/dist/lib/admin/escrowAccessRequestsApi.d.ts +79 -0
  622. package/dist/lib/admin/escrowAccessRequestsApi.js +46 -0
  623. package/dist/lib/admin/escrowAuditLogApi.d.ts +66 -0
  624. package/dist/lib/admin/escrowAuditLogApi.js +31 -0
  625. package/dist/lib/admin/escrowScopesApi.d.ts +78 -0
  626. package/dist/lib/admin/escrowScopesApi.js +50 -0
  627. package/dist/lib/admin/leftoverMailboxApi.d.ts +73 -0
  628. package/dist/lib/admin/leftoverMailboxApi.js +67 -0
  629. package/dist/lib/admin/mailboxPolicyApi.d.ts +27 -0
  630. package/dist/lib/admin/mailboxPolicyApi.js +21 -0
  631. package/dist/lib/admin/matterExportApi.d.ts +40 -0
  632. package/dist/lib/admin/matterExportApi.js +39 -0
  633. package/dist/lib/admin/matterSearchApi.d.ts +20 -0
  634. package/dist/lib/admin/matterSearchApi.js +27 -0
  635. package/dist/lib/admin/mattersApi.d.ts +56 -0
  636. package/dist/lib/admin/mattersApi.js +40 -0
  637. package/dist/lib/admin/pluginsApi.d.ts +265 -0
  638. package/dist/lib/admin/pluginsApi.js +102 -0
  639. package/dist/lib/admin/retentionPolicyApi.d.ts +32 -0
  640. package/dist/lib/admin/retentionPolicyApi.js +29 -0
  641. package/dist/lib/admin/setupApi.d.ts +23 -0
  642. package/dist/lib/admin/setupApi.js +26 -0
  643. package/dist/lib/admin/transportRulesApi.d.ts +66 -0
  644. package/dist/lib/admin/transportRulesApi.js +43 -0
  645. package/dist/lib/appearance/preferencesApi.d.ts +128 -0
  646. package/dist/lib/appearance/preferencesApi.js +257 -0
  647. package/dist/lib/auth/profileApi.d.ts +33 -0
  648. package/dist/lib/auth/profileApi.js +52 -0
  649. package/dist/lib/auth/session.d.ts +70 -0
  650. package/dist/lib/auth/session.js +210 -0
  651. package/dist/lib/branding/brandingApi.d.ts +47 -0
  652. package/dist/lib/branding/brandingApi.js +61 -0
  653. package/dist/lib/branding/useBranding.d.ts +23 -0
  654. package/dist/lib/branding/useBranding.js +65 -0
  655. package/dist/lib/calendar/calendarApi.d.ts +230 -0
  656. package/dist/lib/calendar/calendarApi.js +106 -0
  657. package/dist/lib/calendar/calendarColors.d.ts +18 -0
  658. package/dist/lib/calendar/calendarColors.js +39 -0
  659. package/dist/lib/calendar/calendarDragIds.d.ts +42 -0
  660. package/dist/lib/calendar/calendarDragIds.js +92 -0
  661. package/dist/lib/calendar/calendarMutations.d.ts +68 -0
  662. package/dist/lib/calendar/calendarMutations.js +190 -0
  663. package/dist/lib/calendar/eventDescription.d.ts +43 -0
  664. package/dist/lib/calendar/eventDescription.js +195 -0
  665. package/dist/lib/calendar/freeBusyApi.d.ts +100 -0
  666. package/dist/lib/calendar/freeBusyApi.js +119 -0
  667. package/dist/lib/calendar/inviteApi.d.ts +121 -0
  668. package/dist/lib/calendar/inviteApi.js +54 -0
  669. package/dist/lib/calendar/recurrence.d.ts +60 -0
  670. package/dist/lib/calendar/recurrence.js +225 -0
  671. package/dist/lib/components/avatar/ContactAvatar.d.ts +8 -0
  672. package/dist/lib/components/avatar/ContactAvatar.js +26 -0
  673. package/dist/lib/components/buttons/Button.d.ts +9 -0
  674. package/dist/lib/components/buttons/Button.js +14 -0
  675. package/dist/lib/components/buttons/CopyButton.d.ts +18 -0
  676. package/dist/lib/components/buttons/CopyButton.js +14 -0
  677. package/dist/lib/components/feedback/Alert.d.ts +4 -0
  678. package/dist/lib/components/feedback/Alert.js +4 -0
  679. package/dist/lib/components/feedback/Skeleton.d.ts +21 -0
  680. package/dist/lib/components/feedback/Skeleton.js +14 -0
  681. package/dist/lib/components/forms/FormField.d.ts +8 -0
  682. package/dist/lib/components/forms/FormField.js +5 -0
  683. package/dist/lib/components/navigation/BottomTabBar.d.ts +27 -0
  684. package/dist/lib/components/navigation/BottomTabBar.js +32 -0
  685. package/dist/lib/components/overlays/Drawer.d.ts +20 -0
  686. package/dist/lib/components/overlays/Drawer.js +27 -0
  687. package/dist/lib/components/overlays/Modal.d.ts +17 -0
  688. package/dist/lib/components/overlays/Modal.js +25 -0
  689. package/dist/lib/components/overlays/PopoverPortal.d.ts +23 -0
  690. package/dist/lib/components/overlays/PopoverPortal.js +53 -0
  691. package/dist/lib/components/overlays/overlayStack.d.ts +31 -0
  692. package/dist/lib/components/overlays/overlayStack.js +143 -0
  693. package/dist/lib/components/pickers/MiniDatePicker.d.ts +13 -0
  694. package/dist/lib/components/pickers/MiniDatePicker.js +42 -0
  695. package/dist/lib/contacts/contactsApi.d.ts +174 -0
  696. package/dist/lib/contacts/contactsApi.js +139 -0
  697. package/dist/lib/contacts/vcard.d.ts +23 -0
  698. package/dist/lib/contacts/vcard.js +179 -0
  699. package/dist/lib/crypto/composeSecurity.d.ts +67 -0
  700. package/dist/lib/crypto/composeSecurity.js +93 -0
  701. package/dist/lib/crypto/encoding.d.ts +9 -0
  702. package/dist/lib/crypto/encoding.js +32 -0
  703. package/dist/lib/crypto/escrowKeys.d.ts +25 -0
  704. package/dist/lib/crypto/escrowKeys.js +59 -0
  705. package/dist/lib/crypto/idleTimeout.d.ts +16 -0
  706. package/dist/lib/crypto/idleTimeout.js +47 -0
  707. package/dist/lib/crypto/keySession.d.ts +159 -0
  708. package/dist/lib/crypto/keySession.js +367 -0
  709. package/dist/lib/crypto/keys.d.ts +50 -0
  710. package/dist/lib/crypto/keys.js +89 -0
  711. package/dist/lib/crypto/keyvaultApi.d.ts +394 -0
  712. package/dist/lib/crypto/keyvaultApi.js +350 -0
  713. package/dist/lib/crypto/masterKey.d.ts +59 -0
  714. package/dist/lib/crypto/masterKey.js +104 -0
  715. package/dist/lib/crypto/masterKeyWraps.d.ts +108 -0
  716. package/dist/lib/crypto/masterKeyWraps.js +228 -0
  717. package/dist/lib/crypto/messageSecurity.d.ts +248 -0
  718. package/dist/lib/crypto/messageSecurity.js +367 -0
  719. package/dist/lib/crypto/mime.d.ts +143 -0
  720. package/dist/lib/crypto/mime.js +504 -0
  721. package/dist/lib/crypto/passkeyUnlock.d.ts +20 -0
  722. package/dist/lib/crypto/passkeyUnlock.js +79 -0
  723. package/dist/lib/crypto/passwordUnlock.d.ts +39 -0
  724. package/dist/lib/crypto/passwordUnlock.js +73 -0
  725. package/dist/lib/crypto/recoveryCode.d.ts +28 -0
  726. package/dist/lib/crypto/recoveryCode.js +86 -0
  727. package/dist/lib/crypto/signingProviderApi.d.ts +95 -0
  728. package/dist/lib/crypto/signingProviderApi.js +60 -0
  729. package/dist/lib/crypto/smime.d.ts +128 -0
  730. package/dist/lib/crypto/smime.js +402 -0
  731. package/dist/lib/crypto/smimeMessage.d.ts +132 -0
  732. package/dist/lib/crypto/smimeMessage.js +341 -0
  733. package/dist/lib/crypto/useIdleKeyTimeout.d.ts +10 -0
  734. package/dist/lib/crypto/useIdleKeyTimeout.js +163 -0
  735. package/dist/lib/crypto/verificationSeal.d.ts +56 -0
  736. package/dist/lib/crypto/verificationSeal.js +220 -0
  737. package/dist/lib/mail/compose/composeQuoting.d.ts +88 -0
  738. package/dist/lib/mail/compose/composeQuoting.js +204 -0
  739. package/dist/lib/mail/compose/emojiData.d.ts +9 -0
  740. package/dist/lib/mail/compose/emojiData.js +18 -0
  741. package/dist/lib/mail/compose/giphyApi.d.ts +17 -0
  742. package/dist/lib/mail/compose/giphyApi.js +20 -0
  743. package/dist/lib/mail/conversationsApi.d.ts +80 -0
  744. package/dist/lib/mail/conversationsApi.js +44 -0
  745. package/dist/lib/mail/dataExportApi.d.ts +50 -0
  746. package/dist/lib/mail/dataExportApi.js +40 -0
  747. package/dist/lib/mail/directoryApi.d.ts +49 -0
  748. package/dist/lib/mail/directoryApi.js +101 -0
  749. package/dist/lib/mail/erasureRequestApi.d.ts +45 -0
  750. package/dist/lib/mail/erasureRequestApi.js +43 -0
  751. package/dist/lib/mail/flaggedMessages.d.ts +22 -0
  752. package/dist/lib/mail/flaggedMessages.js +73 -0
  753. package/dist/lib/mail/focusedInboxOverridesApi.d.ts +37 -0
  754. package/dist/lib/mail/focusedInboxOverridesApi.js +34 -0
  755. package/dist/lib/mail/labelsApi.d.ts +47 -0
  756. package/dist/lib/mail/labelsApi.js +38 -0
  757. package/dist/lib/mail/mailAddress.d.ts +27 -0
  758. package/dist/lib/mail/mailAddress.js +54 -0
  759. package/dist/lib/mail/mailApi.d.ts +850 -0
  760. package/dist/lib/mail/mailApi.js +602 -0
  761. package/dist/lib/mail/mailDetailHooks.d.ts +10 -0
  762. package/dist/lib/mail/mailDetailHooks.js +53 -0
  763. package/dist/lib/mail/mailFilterRulesApi.d.ts +76 -0
  764. package/dist/lib/mail/mailFilterRulesApi.js +45 -0
  765. package/dist/lib/mail/mailSignaturesApi.d.ts +49 -0
  766. package/dist/lib/mail/mailSignaturesApi.js +42 -0
  767. package/dist/lib/mail/mailboxAccessApi.d.ts +74 -0
  768. package/dist/lib/mail/mailboxAccessApi.js +56 -0
  769. package/dist/lib/mail/mailboxImportApi.d.ts +53 -0
  770. package/dist/lib/mail/mailboxImportApi.js +51 -0
  771. package/dist/lib/mail/messageBodySanitizer.d.ts +16 -0
  772. package/dist/lib/mail/messageBodySanitizer.js +118 -0
  773. package/dist/lib/mail/pushClient.d.ts +106 -0
  774. package/dist/lib/mail/pushClient.js +317 -0
  775. package/dist/lib/mail/sendEvents.d.ts +33 -0
  776. package/dist/lib/mail/sendEvents.js +44 -0
  777. package/dist/lib/mail/sendFailure.d.ts +16 -0
  778. package/dist/lib/mail/sendFailure.js +86 -0
  779. package/dist/lib/mail/senderListsApi.d.ts +69 -0
  780. package/dist/lib/mail/senderListsApi.js +126 -0
  781. package/dist/lib/search/queryGrammar.d.ts +51 -0
  782. package/dist/lib/search/queryGrammar.js +111 -0
  783. package/dist/lib/search/searchApi.d.ts +100 -0
  784. package/dist/lib/search/searchApi.js +107 -0
  785. package/dist/lib/search/searchScoring.d.ts +34 -0
  786. package/dist/lib/search/searchScoring.js +44 -0
  787. package/dist/lib/search/searchTier3.d.ts +64 -0
  788. package/dist/lib/search/searchTier3.js +335 -0
  789. package/dist/lib/tasks/tasksApi.d.ts +85 -0
  790. package/dist/lib/tasks/tasksApi.js +55 -0
  791. package/dist/lib/util/api.d.ts +160 -0
  792. package/dist/lib/util/api.js +251 -0
  793. package/dist/lib/util/apiClientContext.d.ts +7 -0
  794. package/dist/lib/util/apiClientContext.js +32 -0
  795. package/dist/lib/util/apiQuery.d.ts +32 -0
  796. package/dist/lib/util/apiQuery.js +42 -0
  797. package/dist/lib/util/clipboard.d.ts +7 -0
  798. package/dist/lib/util/clipboard.js +56 -0
  799. package/dist/lib/util/dateInput.d.ts +6 -0
  800. package/dist/lib/util/dateInput.js +14 -0
  801. package/dist/lib/util/timeZone.d.ts +12 -0
  802. package/dist/lib/util/timeZone.js +32 -0
  803. package/dist/lib/util/useCopyToClipboard.d.ts +12 -0
  804. package/dist/lib/util/useCopyToClipboard.js +34 -0
  805. package/dist/lib/util/useIsMobile.d.ts +10 -0
  806. package/dist/lib/util/useIsMobile.js +29 -0
  807. package/dist/lib/videoconf/videoMeetingsApi.d.ts +116 -0
  808. package/dist/lib/videoconf/videoMeetingsApi.js +49 -0
  809. package/lib/admin/auditLogApi.ts +48 -0
  810. package/lib/admin/distributionListsApi.ts +77 -0
  811. package/lib/admin/domainsApi.ts +113 -0
  812. package/lib/admin/escrowAccessRequestsApi.ts +110 -0
  813. package/lib/admin/escrowAuditLogApi.ts +101 -0
  814. package/lib/admin/escrowScopesApi.ts +112 -0
  815. package/lib/admin/leftoverMailboxApi.ts +120 -0
  816. package/lib/admin/mailboxPolicyApi.ts +38 -0
  817. package/lib/admin/matterExportApi.ts +62 -0
  818. package/lib/admin/matterSearchApi.ts +35 -0
  819. package/lib/admin/mattersApi.ts +86 -0
  820. package/lib/admin/pluginsApi.ts +339 -0
  821. package/lib/admin/retentionPolicyApi.ts +46 -0
  822. package/lib/admin/setupApi.ts +40 -0
  823. package/lib/admin/transportRulesApi.ts +106 -0
  824. package/lib/appearance/preferencesApi.ts +335 -0
  825. package/lib/auth/profileApi.ts +77 -0
  826. package/lib/auth/session.ts +238 -0
  827. package/lib/branding/brandingApi.ts +97 -0
  828. package/lib/branding/useBranding.ts +81 -0
  829. package/lib/calendar/calendarApi.ts +309 -0
  830. package/lib/calendar/calendarColors.ts +45 -0
  831. package/lib/calendar/calendarDragIds.ts +112 -0
  832. package/lib/calendar/calendarMutations.ts +236 -0
  833. package/lib/calendar/eventDescription.ts +211 -0
  834. package/lib/calendar/freeBusyApi.ts +197 -0
  835. package/lib/calendar/inviteApi.ts +154 -0
  836. package/lib/calendar/recurrence.ts +285 -0
  837. package/lib/components/avatar/ContactAvatar.tsx +47 -0
  838. package/lib/components/buttons/Button.tsx +40 -0
  839. package/lib/components/buttons/CopyButton.tsx +45 -0
  840. package/lib/components/feedback/Alert.tsx +13 -0
  841. package/lib/components/feedback/Skeleton.tsx +38 -0
  842. package/lib/components/forms/FormField.tsx +23 -0
  843. package/lib/components/navigation/BottomTabBar.tsx +66 -0
  844. package/lib/components/overlays/Drawer.tsx +72 -0
  845. package/lib/components/overlays/Modal.tsx +64 -0
  846. package/lib/components/overlays/PopoverPortal.tsx +74 -0
  847. package/lib/components/overlays/overlayStack.ts +165 -0
  848. package/lib/components/pickers/MiniDatePicker.tsx +90 -0
  849. package/lib/contacts/contactsApi.ts +289 -0
  850. package/lib/contacts/vcard.ts +197 -0
  851. package/lib/crypto/composeSecurity.ts +140 -0
  852. package/lib/crypto/encoding.ts +36 -0
  853. package/lib/crypto/escrowKeys.ts +74 -0
  854. package/lib/crypto/idleTimeout.ts +49 -0
  855. package/lib/crypto/keySession.ts +483 -0
  856. package/lib/crypto/keys.ts +105 -0
  857. package/lib/crypto/keyvaultApi.ts +674 -0
  858. package/lib/crypto/masterKey.ts +134 -0
  859. package/lib/crypto/masterKeyWraps.ts +269 -0
  860. package/lib/crypto/messageSecurity.ts +600 -0
  861. package/lib/crypto/mime.ts +568 -0
  862. package/lib/crypto/passkeyUnlock.ts +103 -0
  863. package/lib/crypto/passwordUnlock.ts +98 -0
  864. package/lib/crypto/recoveryCode.ts +93 -0
  865. package/lib/crypto/signingProviderApi.ts +135 -0
  866. package/lib/crypto/smime.ts +459 -0
  867. package/lib/crypto/smimeMessage.ts +491 -0
  868. package/lib/crypto/useIdleKeyTimeout.ts +176 -0
  869. package/lib/crypto/verificationSeal.ts +288 -0
  870. package/lib/mail/compose/composeQuoting.ts +243 -0
  871. package/lib/mail/compose/emojiData.ts +34 -0
  872. package/lib/mail/compose/giphyApi.ts +31 -0
  873. package/lib/mail/conversationsApi.ts +123 -0
  874. package/lib/mail/dataExportApi.ts +73 -0
  875. package/lib/mail/directoryApi.ts +161 -0
  876. package/lib/mail/erasureRequestApi.ts +71 -0
  877. package/lib/mail/flaggedMessages.ts +80 -0
  878. package/lib/mail/focusedInboxOverridesApi.ts +64 -0
  879. package/lib/mail/labelsApi.ts +71 -0
  880. package/lib/mail/mailAddress.ts +76 -0
  881. package/lib/mail/mailApi.ts +1286 -0
  882. package/lib/mail/mailDetailHooks.ts +57 -0
  883. package/lib/mail/mailFilterRulesApi.ts +116 -0
  884. package/lib/mail/mailSignaturesApi.ts +84 -0
  885. package/lib/mail/mailboxAccessApi.ts +106 -0
  886. package/lib/mail/mailboxImportApi.ts +87 -0
  887. package/lib/mail/messageBodySanitizer.ts +129 -0
  888. package/lib/mail/pushClient.ts +375 -0
  889. package/lib/mail/sendEvents.ts +72 -0
  890. package/lib/mail/sendFailure.ts +98 -0
  891. package/lib/mail/senderListsApi.ts +161 -0
  892. package/lib/search/queryGrammar.ts +179 -0
  893. package/lib/search/searchApi.ts +199 -0
  894. package/lib/search/searchScoring.ts +46 -0
  895. package/lib/search/searchTier3.ts +406 -0
  896. package/lib/tasks/tasksApi.ts +128 -0
  897. package/lib/util/api.ts +318 -0
  898. package/lib/util/apiClientContext.ts +35 -0
  899. package/lib/util/apiQuery.ts +61 -0
  900. package/lib/util/clipboard.ts +55 -0
  901. package/lib/util/dateInput.ts +15 -0
  902. package/lib/util/timeZone.ts +33 -0
  903. package/lib/util/useCopyToClipboard.ts +48 -0
  904. package/lib/util/useIsMobile.ts +34 -0
  905. package/lib/videoconf/videoMeetingsApi.ts +144 -0
  906. package/package.json +10 -4
@@ -1,613 +1,624 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
6
- import { flushSync } from "react-dom";
7
- import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
- import { Attachment, Folder, Message, listAttachments } from "@rapidmx/react-shared/mail/mailApi.js";
9
- import { ConversationSummary, listConversationMessages } from "@rapidmx/react-shared/mail/conversationsApi.js";
10
- import { Label } from "@rapidmx/react-shared/mail/labelsApi.js";
11
- import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
12
- import MessageDetailPane from "./MessageDetailPane.js";
13
- import { EncryptedPreview, displaySubject } from "./reading/EncryptedPreview.js";
14
- import { CollapsedCard, SkeletonCards, SubjectCard } from "./reading/MessageCard.js";
15
- import PendingMessageCard from "./reading/PendingMessageCard.js";
16
- import { useMailShell } from "./layout/MailShell.js";
17
- import { setReadState } from "../../mail/messageReadState.js";
18
- import { adoptedOutgoing, belongsToThread, settleOutgoing, useOutgoingReplies } from "../../mail/outbox/outgoingReplies.js";
19
- import { dateClass, isUnread, senderClass } from "./unreadStyle.js";
20
-
21
- /** One request's worth of the thread. The server's own default for `listConversationMessages()`. */
22
- export const THREAD_PAGE_SIZE = 100;
23
- /**
24
- * How many of a conversation's messages this pane will load. The server reads at most
25
- * `CONVERSATION_SCAN_LIMIT` (500) messages when it groups conversations, so a `ConversationSummary` never
26
- * describes more than this many anyway; the cap is here so a thread that somehow reports more can't turn
27
- * into an unbounded run of requests. Past it the pane says so and the rest stay readable from the list.
28
- */
29
- export const THREAD_MESSAGE_LIMIT = 500;
30
-
31
- /** What the pane needs to know of a conversation up front: the thread itself is loaded by `conversationId`. The subject and
32
- * count fill the header while it loads, so a caller that only has a message (the mobile route) gives its subject and a count of 1. */
33
- export type ConversationThreadHead = Pick<ConversationSummary, "conversationId" | "subject" | "messageCount">;
34
-
35
- export interface ConversationThreadPaneProps {
36
- conversation: ConversationThreadHead | null;
37
- /** The mailbox the conversation was listed from - `listConversationMessages()` is mailbox-scoped. */
38
- mailboxUid: string;
39
- /**
40
- * The message the reader opened: the thread is scrolled to it, it takes focus, and it is the *oldest*
41
- * message left expanded (see `expandedFrom()`). `null`, or a uid this thread doesn't hold, falls back
42
- * to the newest message - which is also what a parent row means by "open the conversation".
43
- */
44
- selectedUid: string | null;
45
- /** The mailbox's full folder list. A conversation spans folders (an Inbox message and the Sent Items
46
- * copy of its reply), so each message's own folder type is looked up against its own `folderUid`. */
47
- folders: Folder[];
48
- /** The mailbox's labels, passed through to every expanded message's own `MessageDetailPane`. */
49
- labels?: Label[];
50
- /**
51
- * A newer copy of one of the thread's messages - read, flagged, labelled, classified, recalled - for
52
- * the caller's own list to stay in step with what was done in here.
53
- *
54
- * `previous` is the copy this pane held before the change, where it has one: the conversation row this
55
- * thread came from is a *summary* (a message count, an unread count), so a list showing those rows has
56
- * no way to tell "this message has just been read" from "this already-read message was relabelled"
57
- * without it - which is what left a row reporting "2 unread" after both had been read.
58
- */
59
- onMessagePatched: (updated: Message, previous?: Message) => void;
60
- /** A message that left the folder being listed (archived, or a scheduled send sent back to Drafts). */
61
- onMessageRemoved: (updated: Message) => void;
62
- onLabelCreated?: (label: Label) => void;
63
- /** A folder created from a message's own Move to prompt, for the folder sidebar to pick up. */
64
- onFolderCreated?: (folder: Folder) => void;
65
- /** Registers the keyboard shortcuts (Reply, Reply all, Forward, Archive, Move to) of the message that was opened - see `MessageDetailPane`'s
66
- * `shortcuts`. The caller turns it off while it is doing something else with the keyboard (select mode). */
67
- shortcuts?: boolean;
68
- }
69
-
70
- /**
71
- * Every message from `selectedUid` through to the newest - the run the reader is reading. Anything older
72
- * stays collapsed to its one-line summary. Selecting the newest message therefore expands just that one.
73
- *
74
- * The pane lists newest first (see `loadThread()`), so that run is the opened message together with
75
- * everything *above* it, and the opened message is the set's **last** entry rather than its first.
76
- */
77
- function expandedFrom(messages: Message[], selectedUid: string | null): Set<string> {
78
- const index = messages.findIndex((message) => message.uid === selectedUid);
79
- // `messages` is never empty here (the callers below check), so -1 means "not in this thread" and the
80
- // newest message - the first entry - is the anchor, exactly as a parent row's own click means.
81
- const anchor = index === -1 ? 0 : index;
82
- return new Set(messages.slice(0, anchor + 1).map((message) => message.uid));
83
- }
84
-
85
- /**
86
- * The element a message of this thread actually scrolls inside. The pane is not itself the scroll
87
- * container in the mail shell - `MailShell`'s own `<main>` is - so an adjustment has to be applied where
88
- * the scrolling really happens, which is whichever ancestor is both scrollable and overflowing.
89
- */
90
- function scrollingAncestor(node: HTMLElement): HTMLElement {
91
- for (let el = node.parentElement; el; el = el.parentElement) {
92
- const overflowY = getComputedStyle(el).overflowY;
93
- if ((overflowY === "auto" || overflowY === "scroll") && el.scrollHeight > el.clientHeight) {
94
- return el;
95
- }
96
- }
97
- // Nothing between the row and the root scrolls, so the page itself does - which in standards mode is
98
- // `documentElement`, the same element `document.scrollingElement` names there.
99
- return document.documentElement;
100
- }
101
-
102
- /**
103
- * The thread's messages, **newest first**, in pages of `THREAD_PAGE_SIZE` up to `THREAD_MESSAGE_LIMIT`.
104
- *
105
- * `listConversationMessages()` pages oldest first and has no order of its own to ask for, so the pages are
106
- * collected in that order - which is also the order the `THREAD_MESSAGE_LIMIT` cap has to apply in, since it
107
- * is the oldest messages that are dropped when a thread is too long to load - and reversed once at the end.
108
- * The pane reads newest first always, whatever the *list* is sorted by: it is the reading order for mail,
109
- * and it means the message a conversation row stands for is the entry at the top.
110
- */
111
- async function loadThread(mailboxUid: string, conversationId: string): Promise<{ messages: Message[]; truncated: boolean }> {
112
- const messages: Message[] = [];
113
- let more = true;
114
- while (more && messages.length < THREAD_MESSAGE_LIMIT) {
115
- const page = await listConversationMessages(mailboxUid, conversationId, {
116
- page: messages.length / THREAD_PAGE_SIZE,
117
- limit: THREAD_PAGE_SIZE,
118
- });
119
- messages.push(...page);
120
- // A short page is the last one; a full page means asking for another.
121
- more = page.length === THREAD_PAGE_SIZE;
122
- }
123
- messages.reverse();
124
- return { messages, truncated: more };
125
- }
126
-
127
- /**
128
- * `next` - the thread as it was just read again - for the pane to show in place of `previous`. A message the pane holds a newer copy of (one that
129
- * was read, flagged or labelled here a moment ago, whose change the read may have started before) keeps that copy, and a read that changed
130
- * nothing gives back `previous` itself, so the pane is not drawn again.
131
- */
132
- function mergeThread(previous: Message[], next: Message[]): Message[] {
133
- const held = new Map(previous.map((message) => [message.uid, message]));
134
- const merged = next.map((message) => {
135
- const own = held.get(message.uid);
136
- return own && own.version > message.version ? own : message;
137
- });
138
- const unchanged = merged.length === previous.length && merged.every((message, index) => message.uid === previous[index].uid && message.version === previous[index].version);
139
- return unchanged ? previous : merged;
140
- }
141
-
142
- /** Whether the browser is showing a focus ring on `element` - it was focused from the keyboard (or by a key press's handler), not by a click. */
143
- function hasFocusRing(element: HTMLElement): boolean {
144
- try {
145
- return element.matches(":focus-visible");
146
- } catch {
147
- return false;
148
- }
149
- }
150
-
151
- /**
152
- * The reading pane for the conversation list: the conversation's subject in a card of its own, pinned at the top, and under it the whole thread as
153
- * a stack of message cards (each exactly as tall as its message; the list scrolls as a whole), **newest at the top**, opened at the message
154
- * the reader picked. Every message from that one through to the newest is expanded - which in this order is
155
- * the opened message and the entries above it - and the older ones, below it, are collapsed to a one-line
156
- * summary (sender, date, preview) that expands on click or Enter. So opening the newest message shows just
157
- * the top entry expanded, and opening 5 of 10 expands 10 down to 5.
158
- *
159
- * Newest first regardless of how the *list* is sorted: the list's own order arranges rows to pick from,
160
- * while this is one conversation being read, and the entry a conversation row stands for - its latest
161
- * message - is the one that should be at the top of the pane every time it is opened.
162
- *
163
- * Each *expanded* message is a `MessageDetailPane` of its own rather than a reimplementation of it, so the
164
- * signature and verification badges, the verification-seal and decryption behaviour, the labels chips and
165
- * menu, the attachments and Reply/Reply All/Forward/Archive all behave exactly as they do in the
166
- * single-message pane, and each acts on the message it belongs to. A collapsed message mounts none of
167
- * that - mounting a body iframe per message up front would be wasteful in a long thread.
168
- */
169
- export default function ConversationThreadPane({
170
- conversation,
171
- mailboxUid,
172
- selectedUid,
173
- folders,
174
- labels,
175
- onMessagePatched,
176
- onMessageRemoved,
177
- onLabelCreated,
178
- onFolderCreated,
179
- shortcuts,
180
- }: ConversationThreadPaneProps) {
181
- const { trackMessageChange, live } = useMailShell();
182
- // The replies and forwards this tab has sent, drawn at the top of the thread they continue until the server's own copy is in it.
183
- const outgoing = useOutgoingReplies();
184
- const [messages, setMessages] = useState<Message[]>([]);
185
- const [attachmentsByUid, setAttachmentsByUid] = useState<Record<string, Attachment[]>>({});
186
- const [expandedUids, setExpandedUids] = useState<Set<string>>(new Set());
187
- const [truncated, setTruncated] = useState(false);
188
- const [loading, setLoading] = useState(false);
189
- const [error, setError] = useState<string | null>(null);
190
- /** The message to scroll to and focus once it has rendered, or `null` once that has happened. */
191
- const [pendingFocusUid, setPendingFocusUid] = useState<string | null>(null);
192
-
193
- // Bumped on every conversation switch - an in-flight load/attachments/mark-read response carrying an
194
- // older generation belongs to a superseded conversation and is dropped rather than applied.
195
- const generationRef = useRef(0);
196
- const markReadRequestedRef = useRef<Set<string>>(new Set());
197
- const attachmentsRequestedRef = useRef<Set<string>>(new Set());
198
- /** The `conversationId:selectedUid` the expansion run below has already been applied for, so patching
199
- * a message (which changes `messages`) doesn't re-expand what the reader has since collapsed. */
200
- const appliedSelectionRef = useRef<string | null>(null);
201
- /** Which conversation the messages currently in state belong to. A render with a new conversation and
202
- * the previous one's messages still in state happens before the load effect has cleared them, and the
203
- * expansion run below must sit that render out rather than anchor on a message from another thread. */
204
- const loadedIdRef = useRef<string | null>(null);
205
- const rowRefs = useRef<Record<string, HTMLLIElement | null>>({});
206
- const headerRefs = useRef<Record<string, HTMLElement | null>>({});
207
- /** Where the toggled message's header sat in the viewport before it expanded, so the run below can put
208
- * it back there - expanding a message above the one being read must not shove that one off-screen. */
209
- const anchorRef = useRef<{ uid: string; top: number } | null>(null);
210
- /** The message whose header button had the focus when it was toggled: expanding or collapsing swaps that button for the other state's, so the
211
- * focus is put back on the new one. */
212
- const refocusRef = useRef<string | null>(null);
213
- /** The pending cards that have already been scrolled to, so that only a message that has just been sent takes the view and the focus. */
214
- const announcedRef = useRef<Set<string>>(new Set());
215
- /** Messages that left the thread here (archived, moved, a scheduled send taken back): a read of the thread again still lists them, since a conversation spans folders. */
216
- const removedRef = useRef<Set<string>>(new Set());
217
- /** The last live update this pane has answered - one already in the shell's hands when the pane opened is not news. */
218
- const seenLiveRef = useRef(live);
219
-
220
- const conversationId = conversation?.conversationId;
221
-
222
- useEffect(() => {
223
- const generation = ++generationRef.current;
224
- markReadRequestedRef.current = new Set();
225
- attachmentsRequestedRef.current = new Set();
226
- announcedRef.current = new Set();
227
- removedRef.current = new Set();
228
- appliedSelectionRef.current = null;
229
- loadedIdRef.current = null;
230
- setMessages([]);
231
- setAttachmentsByUid({});
232
- setExpandedUids(new Set());
233
- setTruncated(false);
234
- setError(null);
235
- // The message the previous conversation was to be scrolled to went with it; leaving it set would
236
- // point the run below at a row that is no longer rendered.
237
- setPendingFocusUid(null);
238
- if (!conversationId) {
239
- setLoading(false);
240
- return;
241
- }
242
- setLoading(true);
243
- loadThread(mailboxUid, conversationId)
244
- .then((loaded) => {
245
- if (generation !== generationRef.current) return;
246
- loadedIdRef.current = conversationId;
247
- // A message sent from here whose Sent Items copy is already in the thread is drawn as that copy, not as a pending card.
248
- settleOutgoing(mailboxUid, loaded.messages);
249
- setMessages(loaded.messages);
250
- setTruncated(loaded.truncated);
251
- })
252
- .catch((err) => {
253
- if (generation !== generationRef.current) return;
254
- setError(err instanceof ApiRequestError ? err.message : "Could not load this conversation.");
255
- })
256
- .finally(() => {
257
- if (generation === generationRef.current) setLoading(false);
258
- });
259
- }, [conversationId, mailboxUid]);
260
-
261
- // Opening the thread, and opening a different message of the same thread, both set the run of expanded
262
- // messages and ask for that message to be scrolled to. `messages` is a dependency because the thread's
263
- // messages arrive after the click that selected one of them; the ref guard keeps a later change to
264
- // `messages` (a patched copy) from re-running it.
265
- useEffect(() => {
266
- if (messages.length === 0 || loadedIdRef.current !== conversationId) return;
267
- const key = `${conversationId}:${selectedUid}`;
268
- if (appliedSelectionRef.current === key) return;
269
- appliedSelectionRef.current = key;
270
- const expanded = expandedFrom(messages, selectedUid);
271
- setExpandedUids(expanded);
272
- // The oldest expanded message is the one that was opened - `expandedFrom()`'s own anchor - which in
273
- // this newest-first order is the *last* of the run, not the first.
274
- setPendingFocusUid([...expanded][expanded.size - 1]);
275
- }, [conversationId, selectedUid, messages]);
276
-
277
- useLayoutEffect(() => {
278
- if (!pendingFocusUid) return;
279
- // The row is always rendered by now: this runs after the DOM update that added the message it
280
- // names, and that message came out of `messages` in the first place.
281
- //
282
- // Scrolled inside the thread's own list and nowhere else. `scrollIntoView()` scrolls *every*
283
- // scrollable ancestor, the window included, which with a run of full-height messages expanded
284
- // took the app header and the folder rail off the screen - so the adjustment goes on whichever
285
- // ancestor actually scrolls, exactly as the toggle below already does it. When that is the page
286
- // itself, nothing inside the pane scrolls and the row is already in view, so it is left alone.
287
- const row = rowRefs.current[pendingFocusUid]!;
288
- const scroller = scrollingAncestor(row);
289
- if (scroller !== document.documentElement) {
290
- const rect = row.getBoundingClientRect();
291
- const top = rect.top - scroller.getBoundingClientRect().top;
292
- // "nearest": the least that brings it into view, and nothing at all when it is already there.
293
- if (top < 0 || top + rect.height > scroller.clientHeight) {
294
- scroller.scrollTop += top;
295
- }
296
- }
297
- // A key press (j, k, the arrows) that opened this thread left the focus on its list row on purpose, so Enter goes on to open the
298
- // message's page; taking the focus here would make Enter toggle this header instead. A click leaves no focus ring, and hands
299
- // the focus to the thread as before.
300
- const active = document.activeElement;
301
- if (!(active instanceof HTMLElement && active.hasAttribute("data-row-open") && hasFocusRing(active))) {
302
- // `preventScroll` so focusing doesn't scroll it somewhere else again.
303
- // (A card's header button is drawn by the message pane, which registers it; a stand-in pane that doesn't has none to focus.)
304
- headerRefs.current[pendingFocusUid]?.focus({ preventScroll: true });
305
- }
306
- setPendingFocusUid(null);
307
- }, [pendingFocusUid, expandedUids]);
308
-
309
- // Keeps the message whose header was just clicked where it was on screen. Expanding one above the
310
- // message being read otherwise pushes everything below it down by however tall the new body is.
311
- useLayoutEffect(() => {
312
- const anchor = anchorRef.current;
313
- if (!anchor) return;
314
- anchorRef.current = null;
315
- // The row is still there: the anchor was taken from a rendered row, and toggling never removes one.
316
- const row = rowRefs.current[anchor.uid]!;
317
- scrollingAncestor(row).scrollTop += row.getBoundingClientRect().top - anchor.top;
318
- // The button the reader had focused is gone (a collapsed card and an expanded one each draw their own): keep them where they were.
319
- if (refocusRef.current === anchor.uid) {
320
- headerRefs.current[anchor.uid]?.focus({ preventScroll: true });
321
- }
322
- refocusRef.current = null;
323
- }, [expandedUids]);
324
-
325
- // Attachments and mark-as-read, for expanded messages only - mirrors `mailDetailHooks.ts`'s
326
- // `useMessageAttachments`/`useMarkMessageRead`, reimplemented here (rather than called in a loop, which
327
- // the rules of hooks don't allow) because a thread expands several messages at once.
328
- useEffect(() => {
329
- const generation = generationRef.current;
330
- for (const message of messages) {
331
- const uid = message.uid;
332
- if (!expandedUids.has(uid)) continue;
333
- if (message.hasAttachments && !attachmentsRequestedRef.current.has(uid)) {
334
- attachmentsRequestedRef.current.add(uid);
335
- listAttachments(message.folderUid, uid)
336
- .then((loaded) => {
337
- if (generation === generationRef.current) {
338
- setAttachmentsByUid((prev) => ({ ...prev, [uid]: loaded }));
339
- }
340
- })
341
- .catch(() => {
342
- // Best-effort, as in `useMessageAttachments`: no attachments render meanwhile, and
343
- // forgetting the request lets a later re-expand retry it.
344
- attachmentsRequestedRef.current.delete(uid);
345
- });
346
- }
347
- if (!markReadRequestedRef.current.has(uid)) {
348
- // Asked once per opened conversation and per expanding of the message: a failure is not retried from here, because
349
- // undoing the optimistic change changes `messages`, which would run this effect again and ask again, for ever. A message
350
- // that is already read is asked about too (there is nothing to send), so marking it unread while it is open - the
351
- // keyboard's Ctrl+U - doesn't make this run again and read it straight back.
352
- markReadRequestedRef.current.add(uid);
353
- if (message.flags.read === true) {
354
- continue;
355
- }
356
- // `message` is this render's copy, so the request carries its current `version`. The row, the conversation row's
357
- // unread count and the folder badge all change at once; `previous` is what tells the list's conversation row
358
- // that its count moved (see `setReadState()`).
359
- void setReadState(message, true, {
360
- patch: (updated, previous) => {
361
- if (generation === generationRef.current) {
362
- patchMessage(updated, previous);
363
- }
364
- },
365
- track: trackMessageChange,
366
- });
367
- }
368
- }
369
- }, [expandedUids, messages]);
370
-
371
- /**
372
- * Reads the thread again, quietly: what is on screen stays until the answer is here (a read that fails changes nothing), a message that arrived
373
- * is added at the top, and the reader's expanded and collapsed cards are left as they are. A message this tab sent - which the server has
374
- * now filed in Sent Items under the same `uid` - takes the place of its pending card, expanded as that card was, with the focus if the
375
- * card had it. A thread that has not finished its first load is not read again: that load is the fresh read.
376
- */
377
- function refreshThread() {
378
- const id = loadedIdRef.current;
379
- if (!id) {
380
- return;
381
- }
382
- const generation = generationRef.current;
383
- loadThread(mailboxUid, id).then(
384
- (loaded) => {
385
- if (generation !== generationRef.current) return;
386
- const current = loaded.messages.filter((message) => !removedRef.current.has(message.uid));
387
- const adopted = adoptedOutgoing(mailboxUid, current);
388
- const refocus = adopted.find((uid) => document.activeElement === headerRefs.current[uid]);
389
- // Drawn first, and only then is the pending card forgotten: there is never a frame with neither it nor the real message.
390
- flushSync(() => {
391
- setMessages((previous) => mergeThread(previous, current));
392
- setTruncated(loaded.truncated);
393
- if (adopted.length > 0) {
394
- setExpandedUids((previous) => new Set([...previous, ...adopted]));
395
- }
396
- if (refocus) {
397
- setPendingFocusUid(refocus);
398
- }
399
- });
400
- settleOutgoing(mailboxUid, current);
401
- },
402
- // Quietly: the next live update reads it again, and what is shown is still right.
403
- () => undefined,
404
- );
405
- }
406
-
407
- // Another message of this conversation may have arrived - a recipient's reply, one sent from another tab or device, or this tab's own reply
408
- // filed in Sent Items - whenever the shell announces a live update that touched one of this mailbox's folders (or does not say which).
409
- useEffect(() => {
410
- if (live === seenLiveRef.current) return;
411
- seenLiveRef.current = live;
412
- if (live.folderUids === null || folders.some((folder) => live.folderUids!.has(folder.uid))) {
413
- refreshThread();
414
- }
415
- }, [live]);
416
-
417
- // The server has relayed a message this tab sent: its Sent Items copy is read for at once, without waiting for the live update that follows.
418
- const sentKey = outgoing
419
- .filter((reply) => reply.state === "sent" && reply.mailboxUid === mailboxUid)
420
- .map((reply) => reply.uid)
421
- .join(",");
422
- useEffect(() => {
423
- if (sentKey) {
424
- refreshThread();
425
- }
426
- }, [sentKey]);
427
-
428
- // The messages sent from here that continue this thread and whose real copy it does not hold yet, newest first (the pane's order). A message
429
- // that replies to nothing here - a new message, or a reply to another conversation - is not in this list, and never is.
430
- const pendingCards = outgoing
431
- .filter((reply) => reply.mailboxUid === mailboxUid && !messages.some((message) => message.uid === reply.uid) && belongsToThread(reply, messages))
432
- .reverse();
433
- // A message that has just been sent is scrolled into view and takes the focus - the compose window it was sent from has just closed, and the focus
434
- // with it - once. A failed one, or one that was already there when the thread was opened, is left where it is.
435
- const pendingKey = pendingCards.map((reply) => reply.uid).join(",");
436
- useEffect(() => {
437
- const fresh = pendingCards.filter((reply) => reply.state === "sending" && !announcedRef.current.has(reply.uid));
438
- if (fresh.length === 0) return;
439
- for (const reply of fresh) {
440
- announcedRef.current.add(reply.uid);
441
- }
442
- setPendingFocusUid(fresh[0].uid);
443
- }, [pendingKey]);
444
-
445
- /** A newer copy of one of the thread's messages, kept here and handed to the list - with the copy it
446
- * replaces where the caller was given one, so a conversation row can tell what actually changed. */
447
- function patchMessage(updated: Message, previous?: Message) {
448
- setMessages((prev) => prev.map((message) => (message.uid === updated.uid ? updated : message)));
449
- onMessagePatched(updated, previous);
450
- }
451
-
452
- /** A message that left the folder being listed - it leaves the thread too, as it left the list. */
453
- function removeMessage(updated: Message) {
454
- removedRef.current.add(updated.uid);
455
- setMessages((prev) => prev.filter((message) => message.uid !== updated.uid));
456
- onMessageRemoved(updated);
457
- }
458
-
459
- function toggleExpanded(uid: string) {
460
- // Opening a message again asks to mark it read again - that is how one whose request failed is retried.
461
- markReadRequestedRef.current.delete(uid);
462
- // The row this button lives in has rendered, so its ref is set.
463
- anchorRef.current = { uid, top: rowRefs.current[uid]!.getBoundingClientRect().top };
464
- refocusRef.current = document.activeElement === headerRefs.current[uid] ? uid : null;
465
- setExpandedUids((prev) => {
466
- const next = new Set(prev);
467
- if (next.has(uid)) {
468
- next.delete(uid);
469
- } else {
470
- next.add(uid);
471
- }
472
- return next;
473
- });
474
- }
475
-
476
- // The keyboard acts on the message that was opened - or the newest, when the opened one isn't in this thread - never on all the expanded
477
- // ones at once (each is a `MessageDetailPane`, and two of them must not both answer Ctrl+R).
478
- const keyboardUid = messages.some((message) => message.uid === selectedUid) ? selectedUid : messages[0]?.uid;
479
-
480
- function folderTypeOf(message: Message): string | undefined {
481
- return folders.find((folder) => folder.uid === message.folderUid)?.type;
482
- }
483
-
484
- if (!conversation) {
485
- return <p className="p-8 text-sm text-text-muted">Select a conversation to read it.</p>;
486
- }
487
- const subject = displaySubject(conversation.subject) || "(no subject)";
488
- // The newest message that is open carries the Reply / Forward buttons at the foot of its card.
489
- const footerUid = messages.find((message) => expandedUids.has(message.uid))?.uid;
490
-
491
- return (
492
- // The pane is the window's height, not the thread's: a full-height flex column whose header card is fixed and whose list of
493
- // message cards is the one scrolling, growing child (`min-h-0`, or the list would stretch the column past the pane instead of
494
- // scrolling inside it). Each card is exactly as tall as its message.
495
- // The subject card is the same element while the messages load and once they are here (the subject and the count are known from the
496
- // list's row), so nothing shifts or is drawn again when they arrive: a skeleton card per message (up to three) stands where they will be.
497
- <div className="flex-1 min-w-0 min-h-0 flex flex-col">
498
- <SubjectCard
499
- subject={subject}
500
- meta={
501
- loading
502
- ? conversation.messageCount > 1
503
- ? `${conversation.messageCount} messages`
504
- : undefined
505
- : error
506
- ? undefined
507
- : `${messages.length + pendingCards.length} message${messages.length + pendingCards.length === 1 ? "" : "s"}`
508
- }
509
- >
510
- {truncated && !loading && !error && (
511
- <p className="text-xs text-text-muted mt-1">
512
- Only the oldest {THREAD_MESSAGE_LIMIT} messages of this conversation are shown here. The rest
513
- are still in the message list.
514
- </p>
515
- )}
516
- </SubjectCard>
517
- {loading ? (
518
- <SkeletonCards messageCount={conversation.messageCount} />
519
- ) : error ? (
520
- <div className="p-2 sm:p-3">
521
- <Alert>{error}</Alert>
522
- </div>
523
- ) : (
524
- <ul className="flex-1 min-h-0 overflow-y-auto p-2 sm:p-3 flex flex-col gap-3">
525
- {pendingCards.map((reply) => (
526
- <li
527
- key={reply.uid}
528
- ref={(node) => {
529
- rowRefs.current[reply.uid] = node;
530
- }}
531
- data-outgoing="true"
532
- >
533
- <PendingMessageCard
534
- reply={reply}
535
- headerRef={(node) => {
536
- headerRefs.current[reply.uid] = node;
537
- }}
538
- />
539
- </li>
540
- ))}
541
- {messages.map((message) => {
542
- const uid = message.uid;
543
- const expanded = expandedUids.has(uid);
544
- const bodyId = `thread-message-${uid}`;
545
- const messageUnread = isUnread(message);
546
- return (
547
- <li
548
- key={uid}
549
- ref={(node) => {
550
- rowRefs.current[uid] = node;
551
- }}
552
- data-unread={messageUnread ? "true" : undefined}
553
- >
554
- {expanded ? (
555
- <MessageDetailPane
556
- inThread
557
- threadSubject={subject}
558
- threadHeader={{
559
- bodyId,
560
- unread: messageUnread,
561
- onToggle: () => toggleExpanded(uid),
562
- buttonRef: (node) => {
563
- headerRefs.current[uid] = node;
564
- },
565
- }}
566
- footer={uid === footerUid}
567
- shortcuts={!!shortcuts && uid === keyboardUid}
568
- message={message}
569
- attachments={attachmentsByUid[uid] ?? []}
570
- isSentItems={folderTypeOf(message) === "sent_items"}
571
- isOutbox={folderTypeOf(message) === "outbox"}
572
- draftsFolderUid={folders.find((folder) => folder.type === "drafts")?.uid}
573
- folders={folders}
574
- onMoved={removeMessage}
575
- onFolderCreated={onFolderCreated}
576
- onRecalled={patchMessage}
577
- onReceiptHandled={patchMessage}
578
- onScheduledSendCanceled={removeMessage}
579
- onArchived={removeMessage}
580
- onChanged={patchMessage}
581
- labels={labels}
582
- onLabelsChanged={patchMessage}
583
- onLabelCreated={onLabelCreated}
584
- />
585
- ) : (
586
- <>
587
- <CollapsedCard
588
- from={message.from}
589
- date={new Date(message.receivedDate).toLocaleString()}
590
- preview={message.bodyPreview || (message.encrypted ? <EncryptedPreview /> : "")}
591
- unread={messageUnread}
592
- senderClassName={senderClass(messageUnread)}
593
- dateClassName={dateClass(messageUnread)}
594
- buttonRef={(node) => {
595
- headerRefs.current[uid] = node;
596
- }}
597
- buttonProps={{
598
- onClick: () => toggleExpanded(uid),
599
- "aria-expanded": false,
600
- "aria-controls": bodyId,
601
- }}
602
- />
603
- <div id={bodyId} hidden />
604
- </>
605
- )}
606
- </li>
607
- );
608
- })}
609
- </ul>
610
- )}
611
- </div>
612
- );
613
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
6
+ import { flushSync } from "react-dom";
7
+ import { ApiClient, ApiRequestError } from "../../../../lib/util/api.js";
8
+ import { Attachment, Folder, Message, listAttachments } from "../../../../lib/mail/mailApi.js";
9
+ import { ConversationSummary, listConversationMessages } from "../../../../lib/mail/conversationsApi.js";
10
+ import { Label } from "../../../../lib/mail/labelsApi.js";
11
+ import { useApiClient } from "../../../../lib/util/apiClientContext.js";
12
+ import Alert from "../../../../lib/components/feedback/Alert.js";
13
+ import MessageDetailPane from "./MessageDetailPane.js";
14
+ import { EncryptedPreview, displaySubject } from "./reading/EncryptedPreview.js";
15
+ import { CollapsedCard, SkeletonCards, SubjectCard } from "./reading/MessageCard.js";
16
+ import PendingMessageCard from "./reading/PendingMessageCard.js";
17
+ import { useMailShell } from "./layout/MailShell.js";
18
+ import { setReadState } from "../../mail/messageReadState.js";
19
+ import { adoptedOutgoing, belongsToThread, settleOutgoing, useOutgoingReplies } from "../../mail/outbox/outgoingReplies.js";
20
+ import { dateClass, isUnread, senderClass } from "./unreadStyle.js";
21
+
22
+ /** One request's worth of the thread. The server's own default for `listConversationMessages()`. */
23
+ export const THREAD_PAGE_SIZE = 100;
24
+ /**
25
+ * How many of a conversation's messages this pane will load. The server reads at most
26
+ * `CONVERSATION_SCAN_LIMIT` (500) messages when it groups conversations, so a `ConversationSummary` never
27
+ * describes more than this many anyway; the cap is here so a thread that somehow reports more can't turn
28
+ * into an unbounded run of requests. Past it the pane says so and the rest stay readable from the list.
29
+ */
30
+ export const THREAD_MESSAGE_LIMIT = 500;
31
+
32
+ /** What the pane needs to know of a conversation up front: the thread itself is loaded by `conversationId`. The subject and
33
+ * count fill the header while it loads, so a caller that only has a message (the mobile route) gives its subject and a count of 1. */
34
+ export type ConversationThreadHead = Pick<ConversationSummary, "conversationId" | "subject" | "messageCount">;
35
+
36
+ export interface ConversationThreadPaneProps {
37
+ conversation: ConversationThreadHead | null;
38
+ /** The mailbox the conversation was listed from - `listConversationMessages()` is mailbox-scoped. */
39
+ mailboxUid: string;
40
+ /**
41
+ * The message the reader opened: the thread is scrolled to it, it takes focus, and it is the *oldest*
42
+ * message left expanded (see `expandedFrom()`). `null`, or a uid this thread doesn't hold, falls back
43
+ * to the newest message - which is also what a parent row means by "open the conversation".
44
+ */
45
+ selectedUid: string | null;
46
+ /** The mailbox's full folder list. A conversation spans folders (an Inbox message and the Sent Items
47
+ * copy of its reply), so each message's own folder type is looked up against its own `folderUid`. */
48
+ folders: Folder[];
49
+ /** The mailbox's labels, passed through to every expanded message's own `MessageDetailPane`. */
50
+ labels?: Label[];
51
+ /**
52
+ * A newer copy of one of the thread's messages - read, flagged, labelled, classified, recalled - for
53
+ * the caller's own list to stay in step with what was done in here.
54
+ *
55
+ * `previous` is the copy this pane held before the change, where it has one: the conversation row this
56
+ * thread came from is a *summary* (a message count, an unread count), so a list showing those rows has
57
+ * no way to tell "this message has just been read" from "this already-read message was relabelled"
58
+ * without it - which is what left a row reporting "2 unread" after both had been read.
59
+ */
60
+ onMessagePatched: (updated: Message, previous?: Message) => void;
61
+ /** A message that left the folder being listed (archived, or a scheduled send sent back to Drafts). */
62
+ onMessageRemoved: (updated: Message) => void;
63
+ onLabelCreated?: (label: Label) => void;
64
+ /** A folder created from a message's own Move to prompt, for the folder sidebar to pick up. */
65
+ onFolderCreated?: (folder: Folder) => void;
66
+ /** Registers the keyboard shortcuts (Reply, Reply all, Forward, Archive, Move to) of the message that was opened - see `MessageDetailPane`'s
67
+ * `shortcuts`. The caller turns it off while it is doing something else with the keyboard (select mode). */
68
+ shortcuts?: boolean;
69
+ }
70
+
71
+ /**
72
+ * Every message from `selectedUid` through to the newest - the run the reader is reading. Anything older
73
+ * stays collapsed to its one-line summary. Selecting the newest message therefore expands just that one.
74
+ *
75
+ * The pane lists newest first (see `loadThread()`), so that run is the opened message together with
76
+ * everything *above* it, and the opened message is the set's **last** entry rather than its first.
77
+ */
78
+ function expandedFrom(messages: Message[], selectedUid: string | null): Set<string> {
79
+ const index = messages.findIndex((message) => message.uid === selectedUid);
80
+ // `messages` is never empty here (the callers below check), so -1 means "not in this thread" and the
81
+ // newest message - the first entry - is the anchor, exactly as a parent row's own click means.
82
+ const anchor = index === -1 ? 0 : index;
83
+ return new Set(messages.slice(0, anchor + 1).map((message) => message.uid));
84
+ }
85
+
86
+ /**
87
+ * The element a message of this thread actually scrolls inside. The pane is not itself the scroll
88
+ * container in the mail shell - `MailShell`'s own `<main>` is - so an adjustment has to be applied where
89
+ * the scrolling really happens, which is whichever ancestor is both scrollable and overflowing.
90
+ */
91
+ function scrollingAncestor(node: HTMLElement): HTMLElement {
92
+ for (let el = node.parentElement; el; el = el.parentElement) {
93
+ const overflowY = getComputedStyle(el).overflowY;
94
+ if ((overflowY === "auto" || overflowY === "scroll") && el.scrollHeight > el.clientHeight) {
95
+ return el;
96
+ }
97
+ }
98
+ // Nothing between the row and the root scrolls, so the page itself does - which in standards mode is
99
+ // `documentElement`, the same element `document.scrollingElement` names there.
100
+ return document.documentElement;
101
+ }
102
+
103
+ /**
104
+ * The thread's messages, **newest first**, in pages of `THREAD_PAGE_SIZE` up to `THREAD_MESSAGE_LIMIT`.
105
+ *
106
+ * `listConversationMessages()` pages oldest first and has no order of its own to ask for, so the pages are
107
+ * collected in that order - which is also the order the `THREAD_MESSAGE_LIMIT` cap has to apply in, since it
108
+ * is the oldest messages that are dropped when a thread is too long to load - and reversed once at the end.
109
+ * The pane reads newest first always, whatever the *list* is sorted by: it is the reading order for mail,
110
+ * and it means the message a conversation row stands for is the entry at the top.
111
+ */
112
+ async function loadThread(
113
+ mailboxUid: string,
114
+ conversationId: string,
115
+ client?: ApiClient,
116
+ ): Promise<{ messages: Message[]; truncated: boolean }> {
117
+ const messages: Message[] = [];
118
+ let more = true;
119
+ while (more && messages.length < THREAD_MESSAGE_LIMIT) {
120
+ const page = await listConversationMessages(
121
+ mailboxUid,
122
+ conversationId,
123
+ {
124
+ page: messages.length / THREAD_PAGE_SIZE,
125
+ limit: THREAD_PAGE_SIZE,
126
+ },
127
+ client,
128
+ );
129
+ messages.push(...page);
130
+ // A short page is the last one; a full page means asking for another.
131
+ more = page.length === THREAD_PAGE_SIZE;
132
+ }
133
+ messages.reverse();
134
+ return { messages, truncated: more };
135
+ }
136
+
137
+ /**
138
+ * `next` - the thread as it was just read again - for the pane to show in place of `previous`. A message the pane holds a newer copy of (one that
139
+ * was read, flagged or labelled here a moment ago, whose change the read may have started before) keeps that copy, and a read that changed
140
+ * nothing gives back `previous` itself, so the pane is not drawn again.
141
+ */
142
+ function mergeThread(previous: Message[], next: Message[]): Message[] {
143
+ const held = new Map(previous.map((message) => [message.uid, message]));
144
+ const merged = next.map((message) => {
145
+ const own = held.get(message.uid);
146
+ return own && own.version > message.version ? own : message;
147
+ });
148
+ const unchanged = merged.length === previous.length && merged.every((message, index) => message.uid === previous[index].uid && message.version === previous[index].version);
149
+ return unchanged ? previous : merged;
150
+ }
151
+
152
+ /** Whether the browser is showing a focus ring on `element` - it was focused from the keyboard (or by a key press's handler), not by a click. */
153
+ function hasFocusRing(element: HTMLElement): boolean {
154
+ try {
155
+ return element.matches(":focus-visible");
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * The reading pane for the conversation list: the conversation's subject in a card of its own, pinned at the top, and under it the whole thread as
163
+ * a stack of message cards (each exactly as tall as its message; the list scrolls as a whole), **newest at the top**, opened at the message
164
+ * the reader picked. Every message from that one through to the newest is expanded - which in this order is
165
+ * the opened message and the entries above it - and the older ones, below it, are collapsed to a one-line
166
+ * summary (sender, date, preview) that expands on click or Enter. So opening the newest message shows just
167
+ * the top entry expanded, and opening 5 of 10 expands 10 down to 5.
168
+ *
169
+ * Newest first regardless of how the *list* is sorted: the list's own order arranges rows to pick from,
170
+ * while this is one conversation being read, and the entry a conversation row stands for - its latest
171
+ * message - is the one that should be at the top of the pane every time it is opened.
172
+ *
173
+ * Each *expanded* message is a `MessageDetailPane` of its own rather than a reimplementation of it, so the
174
+ * signature and verification badges, the verification-seal and decryption behaviour, the labels chips and
175
+ * menu, the attachments and Reply/Reply All/Forward/Archive all behave exactly as they do in the
176
+ * single-message pane, and each acts on the message it belongs to. A collapsed message mounts none of
177
+ * that - mounting a body iframe per message up front would be wasteful in a long thread.
178
+ */
179
+ export default function ConversationThreadPane({
180
+ conversation,
181
+ mailboxUid,
182
+ selectedUid,
183
+ folders,
184
+ labels,
185
+ onMessagePatched,
186
+ onMessageRemoved,
187
+ onLabelCreated,
188
+ onFolderCreated,
189
+ shortcuts,
190
+ }: ConversationThreadPaneProps) {
191
+ const { trackMessageChange, live } = useMailShell();
192
+ const client = useApiClient();
193
+ // The replies and forwards this tab has sent, drawn at the top of the thread they continue until the server's own copy is in it.
194
+ const outgoing = useOutgoingReplies();
195
+ const [messages, setMessages] = useState<Message[]>([]);
196
+ const [attachmentsByUid, setAttachmentsByUid] = useState<Record<string, Attachment[]>>({});
197
+ const [expandedUids, setExpandedUids] = useState<Set<string>>(new Set());
198
+ const [truncated, setTruncated] = useState(false);
199
+ const [loading, setLoading] = useState(false);
200
+ const [error, setError] = useState<string | null>(null);
201
+ /** The message to scroll to and focus once it has rendered, or `null` once that has happened. */
202
+ const [pendingFocusUid, setPendingFocusUid] = useState<string | null>(null);
203
+
204
+ // Bumped on every conversation switch - an in-flight load/attachments/mark-read response carrying an
205
+ // older generation belongs to a superseded conversation and is dropped rather than applied.
206
+ const generationRef = useRef(0);
207
+ const markReadRequestedRef = useRef<Set<string>>(new Set());
208
+ const attachmentsRequestedRef = useRef<Set<string>>(new Set());
209
+ /** The `conversationId:selectedUid` the expansion run below has already been applied for, so patching
210
+ * a message (which changes `messages`) doesn't re-expand what the reader has since collapsed. */
211
+ const appliedSelectionRef = useRef<string | null>(null);
212
+ /** Which conversation the messages currently in state belong to. A render with a new conversation and
213
+ * the previous one's messages still in state happens before the load effect has cleared them, and the
214
+ * expansion run below must sit that render out rather than anchor on a message from another thread. */
215
+ const loadedIdRef = useRef<string | null>(null);
216
+ const rowRefs = useRef<Record<string, HTMLLIElement | null>>({});
217
+ const headerRefs = useRef<Record<string, HTMLElement | null>>({});
218
+ /** Where the toggled message's header sat in the viewport before it expanded, so the run below can put
219
+ * it back there - expanding a message above the one being read must not shove that one off-screen. */
220
+ const anchorRef = useRef<{ uid: string; top: number } | null>(null);
221
+ /** The message whose header button had the focus when it was toggled: expanding or collapsing swaps that button for the other state's, so the
222
+ * focus is put back on the new one. */
223
+ const refocusRef = useRef<string | null>(null);
224
+ /** The pending cards that have already been scrolled to, so that only a message that has just been sent takes the view and the focus. */
225
+ const announcedRef = useRef<Set<string>>(new Set());
226
+ /** Messages that left the thread here (archived, moved, a scheduled send taken back): a read of the thread again still lists them, since a conversation spans folders. */
227
+ const removedRef = useRef<Set<string>>(new Set());
228
+ /** The last live update this pane has answered - one already in the shell's hands when the pane opened is not news. */
229
+ const seenLiveRef = useRef(live);
230
+
231
+ const conversationId = conversation?.conversationId;
232
+
233
+ useEffect(() => {
234
+ const generation = ++generationRef.current;
235
+ markReadRequestedRef.current = new Set();
236
+ attachmentsRequestedRef.current = new Set();
237
+ announcedRef.current = new Set();
238
+ removedRef.current = new Set();
239
+ appliedSelectionRef.current = null;
240
+ loadedIdRef.current = null;
241
+ setMessages([]);
242
+ setAttachmentsByUid({});
243
+ setExpandedUids(new Set());
244
+ setTruncated(false);
245
+ setError(null);
246
+ // The message the previous conversation was to be scrolled to went with it; leaving it set would
247
+ // point the run below at a row that is no longer rendered.
248
+ setPendingFocusUid(null);
249
+ if (!conversationId) {
250
+ setLoading(false);
251
+ return;
252
+ }
253
+ setLoading(true);
254
+ loadThread(mailboxUid, conversationId, client)
255
+ .then((loaded) => {
256
+ if (generation !== generationRef.current) return;
257
+ loadedIdRef.current = conversationId;
258
+ // A message sent from here whose Sent Items copy is already in the thread is drawn as that copy, not as a pending card.
259
+ settleOutgoing(mailboxUid, loaded.messages);
260
+ setMessages(loaded.messages);
261
+ setTruncated(loaded.truncated);
262
+ })
263
+ .catch((err) => {
264
+ if (generation !== generationRef.current) return;
265
+ setError(err instanceof ApiRequestError ? err.message : "Could not load this conversation.");
266
+ })
267
+ .finally(() => {
268
+ if (generation === generationRef.current) setLoading(false);
269
+ });
270
+ }, [conversationId, mailboxUid, client]);
271
+
272
+ // Opening the thread, and opening a different message of the same thread, both set the run of expanded
273
+ // messages and ask for that message to be scrolled to. `messages` is a dependency because the thread's
274
+ // messages arrive after the click that selected one of them; the ref guard keeps a later change to
275
+ // `messages` (a patched copy) from re-running it.
276
+ useEffect(() => {
277
+ if (messages.length === 0 || loadedIdRef.current !== conversationId) return;
278
+ const key = `${conversationId}:${selectedUid}`;
279
+ if (appliedSelectionRef.current === key) return;
280
+ appliedSelectionRef.current = key;
281
+ const expanded = expandedFrom(messages, selectedUid);
282
+ setExpandedUids(expanded);
283
+ // The oldest expanded message is the one that was opened - `expandedFrom()`'s own anchor - which in
284
+ // this newest-first order is the *last* of the run, not the first.
285
+ setPendingFocusUid([...expanded][expanded.size - 1]);
286
+ }, [conversationId, selectedUid, messages]);
287
+
288
+ useLayoutEffect(() => {
289
+ if (!pendingFocusUid) return;
290
+ // The row is always rendered by now: this runs after the DOM update that added the message it
291
+ // names, and that message came out of `messages` in the first place.
292
+ //
293
+ // Scrolled inside the thread's own list and nowhere else. `scrollIntoView()` scrolls *every*
294
+ // scrollable ancestor, the window included, which with a run of full-height messages expanded
295
+ // took the app header and the folder rail off the screen - so the adjustment goes on whichever
296
+ // ancestor actually scrolls, exactly as the toggle below already does it. When that is the page
297
+ // itself, nothing inside the pane scrolls and the row is already in view, so it is left alone.
298
+ const row = rowRefs.current[pendingFocusUid]!;
299
+ const scroller = scrollingAncestor(row);
300
+ if (scroller !== document.documentElement) {
301
+ const rect = row.getBoundingClientRect();
302
+ const top = rect.top - scroller.getBoundingClientRect().top;
303
+ // "nearest": the least that brings it into view, and nothing at all when it is already there.
304
+ if (top < 0 || top + rect.height > scroller.clientHeight) {
305
+ scroller.scrollTop += top;
306
+ }
307
+ }
308
+ // A key press (j, k, the arrows) that opened this thread left the focus on its list row on purpose, so Enter goes on to open the
309
+ // message's page; taking the focus here would make Enter toggle this header instead. A click leaves no focus ring, and hands
310
+ // the focus to the thread as before.
311
+ const active = document.activeElement;
312
+ if (!(active instanceof HTMLElement && active.hasAttribute("data-row-open") && hasFocusRing(active))) {
313
+ // `preventScroll` so focusing doesn't scroll it somewhere else again.
314
+ // (A card's header button is drawn by the message pane, which registers it; a stand-in pane that doesn't has none to focus.)
315
+ headerRefs.current[pendingFocusUid]?.focus({ preventScroll: true });
316
+ }
317
+ setPendingFocusUid(null);
318
+ }, [pendingFocusUid, expandedUids]);
319
+
320
+ // Keeps the message whose header was just clicked where it was on screen. Expanding one above the
321
+ // message being read otherwise pushes everything below it down by however tall the new body is.
322
+ useLayoutEffect(() => {
323
+ const anchor = anchorRef.current;
324
+ if (!anchor) return;
325
+ anchorRef.current = null;
326
+ // The row is still there: the anchor was taken from a rendered row, and toggling never removes one.
327
+ const row = rowRefs.current[anchor.uid]!;
328
+ scrollingAncestor(row).scrollTop += row.getBoundingClientRect().top - anchor.top;
329
+ // The button the reader had focused is gone (a collapsed card and an expanded one each draw their own): keep them where they were.
330
+ if (refocusRef.current === anchor.uid) {
331
+ headerRefs.current[anchor.uid]?.focus({ preventScroll: true });
332
+ }
333
+ refocusRef.current = null;
334
+ }, [expandedUids]);
335
+
336
+ // Attachments and mark-as-read, for expanded messages only - mirrors `mailDetailHooks.ts`'s
337
+ // `useMessageAttachments`/`useMarkMessageRead`, reimplemented here (rather than called in a loop, which
338
+ // the rules of hooks don't allow) because a thread expands several messages at once.
339
+ useEffect(() => {
340
+ const generation = generationRef.current;
341
+ for (const message of messages) {
342
+ const uid = message.uid;
343
+ if (!expandedUids.has(uid)) continue;
344
+ if (message.hasAttachments && !attachmentsRequestedRef.current.has(uid)) {
345
+ attachmentsRequestedRef.current.add(uid);
346
+ listAttachments(message.folderUid, uid, client)
347
+ .then((loaded) => {
348
+ if (generation === generationRef.current) {
349
+ setAttachmentsByUid((prev) => ({ ...prev, [uid]: loaded }));
350
+ }
351
+ })
352
+ .catch(() => {
353
+ // Best-effort, as in `useMessageAttachments`: no attachments render meanwhile, and
354
+ // forgetting the request lets a later re-expand retry it.
355
+ attachmentsRequestedRef.current.delete(uid);
356
+ });
357
+ }
358
+ if (!markReadRequestedRef.current.has(uid)) {
359
+ // Asked once per opened conversation and per expanding of the message: a failure is not retried from here, because
360
+ // undoing the optimistic change changes `messages`, which would run this effect again and ask again, for ever. A message
361
+ // that is already read is asked about too (there is nothing to send), so marking it unread while it is open - the
362
+ // keyboard's Ctrl+U - doesn't make this run again and read it straight back.
363
+ markReadRequestedRef.current.add(uid);
364
+ if (message.flags.read === true) {
365
+ continue;
366
+ }
367
+ // `message` is this render's copy, so the request carries its current `version`. The row, the conversation row's
368
+ // unread count and the folder badge all change at once; `previous` is what tells the list's conversation row
369
+ // that its count moved (see `setReadState()`).
370
+ void setReadState(message, true, {
371
+ patch: (updated, previous) => {
372
+ if (generation === generationRef.current) {
373
+ patchMessage(updated, previous);
374
+ }
375
+ },
376
+ track: trackMessageChange,
377
+ });
378
+ }
379
+ }
380
+ }, [expandedUids, messages, client]);
381
+
382
+ /**
383
+ * Reads the thread again, quietly: what is on screen stays until the answer is here (a read that fails changes nothing), a message that arrived
384
+ * is added at the top, and the reader's expanded and collapsed cards are left as they are. A message this tab sent - which the server has
385
+ * now filed in Sent Items under the same `uid` - takes the place of its pending card, expanded as that card was, with the focus if the
386
+ * card had it. A thread that has not finished its first load is not read again: that load is the fresh read.
387
+ */
388
+ function refreshThread() {
389
+ const id = loadedIdRef.current;
390
+ if (!id) {
391
+ return;
392
+ }
393
+ const generation = generationRef.current;
394
+ loadThread(mailboxUid, id, client).then(
395
+ (loaded) => {
396
+ if (generation !== generationRef.current) return;
397
+ const current = loaded.messages.filter((message) => !removedRef.current.has(message.uid));
398
+ const adopted = adoptedOutgoing(mailboxUid, current);
399
+ const refocus = adopted.find((uid) => document.activeElement === headerRefs.current[uid]);
400
+ // Drawn first, and only then is the pending card forgotten: there is never a frame with neither it nor the real message.
401
+ flushSync(() => {
402
+ setMessages((previous) => mergeThread(previous, current));
403
+ setTruncated(loaded.truncated);
404
+ if (adopted.length > 0) {
405
+ setExpandedUids((previous) => new Set([...previous, ...adopted]));
406
+ }
407
+ if (refocus) {
408
+ setPendingFocusUid(refocus);
409
+ }
410
+ });
411
+ settleOutgoing(mailboxUid, current);
412
+ },
413
+ // Quietly: the next live update reads it again, and what is shown is still right.
414
+ () => undefined,
415
+ );
416
+ }
417
+
418
+ // Another message of this conversation may have arrived - a recipient's reply, one sent from another tab or device, or this tab's own reply
419
+ // filed in Sent Items - whenever the shell announces a live update that touched one of this mailbox's folders (or does not say which).
420
+ useEffect(() => {
421
+ if (live === seenLiveRef.current) return;
422
+ seenLiveRef.current = live;
423
+ if (live.folderUids === null || folders.some((folder) => live.folderUids!.has(folder.uid))) {
424
+ refreshThread();
425
+ }
426
+ }, [live]);
427
+
428
+ // The server has relayed a message this tab sent: its Sent Items copy is read for at once, without waiting for the live update that follows.
429
+ const sentKey = outgoing
430
+ .filter((reply) => reply.state === "sent" && reply.mailboxUid === mailboxUid)
431
+ .map((reply) => reply.uid)
432
+ .join(",");
433
+ useEffect(() => {
434
+ if (sentKey) {
435
+ refreshThread();
436
+ }
437
+ }, [sentKey]);
438
+
439
+ // The messages sent from here that continue this thread and whose real copy it does not hold yet, newest first (the pane's order). A message
440
+ // that replies to nothing here - a new message, or a reply to another conversation - is not in this list, and never is.
441
+ const pendingCards = outgoing
442
+ .filter((reply) => reply.mailboxUid === mailboxUid && !messages.some((message) => message.uid === reply.uid) && belongsToThread(reply, messages))
443
+ .reverse();
444
+ // A message that has just been sent is scrolled into view and takes the focus - the compose window it was sent from has just closed, and the focus
445
+ // with it - once. A failed one, or one that was already there when the thread was opened, is left where it is.
446
+ const pendingKey = pendingCards.map((reply) => reply.uid).join(",");
447
+ useEffect(() => {
448
+ const fresh = pendingCards.filter((reply) => reply.state === "sending" && !announcedRef.current.has(reply.uid));
449
+ if (fresh.length === 0) return;
450
+ for (const reply of fresh) {
451
+ announcedRef.current.add(reply.uid);
452
+ }
453
+ setPendingFocusUid(fresh[0].uid);
454
+ }, [pendingKey]);
455
+
456
+ /** A newer copy of one of the thread's messages, kept here and handed to the list - with the copy it
457
+ * replaces where the caller was given one, so a conversation row can tell what actually changed. */
458
+ function patchMessage(updated: Message, previous?: Message) {
459
+ setMessages((prev) => prev.map((message) => (message.uid === updated.uid ? updated : message)));
460
+ onMessagePatched(updated, previous);
461
+ }
462
+
463
+ /** A message that left the folder being listed - it leaves the thread too, as it left the list. */
464
+ function removeMessage(updated: Message) {
465
+ removedRef.current.add(updated.uid);
466
+ setMessages((prev) => prev.filter((message) => message.uid !== updated.uid));
467
+ onMessageRemoved(updated);
468
+ }
469
+
470
+ function toggleExpanded(uid: string) {
471
+ // Opening a message again asks to mark it read again - that is how one whose request failed is retried.
472
+ markReadRequestedRef.current.delete(uid);
473
+ // The row this button lives in has rendered, so its ref is set.
474
+ anchorRef.current = { uid, top: rowRefs.current[uid]!.getBoundingClientRect().top };
475
+ refocusRef.current = document.activeElement === headerRefs.current[uid] ? uid : null;
476
+ setExpandedUids((prev) => {
477
+ const next = new Set(prev);
478
+ if (next.has(uid)) {
479
+ next.delete(uid);
480
+ } else {
481
+ next.add(uid);
482
+ }
483
+ return next;
484
+ });
485
+ }
486
+
487
+ // The keyboard acts on the message that was opened - or the newest, when the opened one isn't in this thread - never on all the expanded
488
+ // ones at once (each is a `MessageDetailPane`, and two of them must not both answer Ctrl+R).
489
+ const keyboardUid = messages.some((message) => message.uid === selectedUid) ? selectedUid : messages[0]?.uid;
490
+
491
+ function folderTypeOf(message: Message): string | undefined {
492
+ return folders.find((folder) => folder.uid === message.folderUid)?.type;
493
+ }
494
+
495
+ if (!conversation) {
496
+ return <p className="p-8 text-sm text-text-muted">Select a conversation to read it.</p>;
497
+ }
498
+ const subject = displaySubject(conversation.subject) || "(no subject)";
499
+ // The newest message that is open carries the Reply / Forward buttons at the foot of its card.
500
+ const footerUid = messages.find((message) => expandedUids.has(message.uid))?.uid;
501
+
502
+ return (
503
+ // The pane is the window's height, not the thread's: a full-height flex column whose header card is fixed and whose list of
504
+ // message cards is the one scrolling, growing child (`min-h-0`, or the list would stretch the column past the pane instead of
505
+ // scrolling inside it). Each card is exactly as tall as its message.
506
+ // The subject card is the same element while the messages load and once they are here (the subject and the count are known from the
507
+ // list's row), so nothing shifts or is drawn again when they arrive: a skeleton card per message (up to three) stands where they will be.
508
+ <div className="flex-1 min-w-0 min-h-0 flex flex-col">
509
+ <SubjectCard
510
+ subject={subject}
511
+ meta={
512
+ loading
513
+ ? conversation.messageCount > 1
514
+ ? `${conversation.messageCount} messages`
515
+ : undefined
516
+ : error
517
+ ? undefined
518
+ : `${messages.length + pendingCards.length} message${messages.length + pendingCards.length === 1 ? "" : "s"}`
519
+ }
520
+ >
521
+ {truncated && !loading && !error && (
522
+ <p className="text-xs text-text-muted mt-1">
523
+ Only the oldest {THREAD_MESSAGE_LIMIT} messages of this conversation are shown here. The rest
524
+ are still in the message list.
525
+ </p>
526
+ )}
527
+ </SubjectCard>
528
+ {loading ? (
529
+ <SkeletonCards messageCount={conversation.messageCount} />
530
+ ) : error ? (
531
+ <div className="p-2 sm:p-3">
532
+ <Alert>{error}</Alert>
533
+ </div>
534
+ ) : (
535
+ <ul className="flex-1 min-h-0 overflow-y-auto p-2 sm:p-3 flex flex-col gap-3">
536
+ {pendingCards.map((reply) => (
537
+ <li
538
+ key={reply.uid}
539
+ ref={(node) => {
540
+ rowRefs.current[reply.uid] = node;
541
+ }}
542
+ data-outgoing="true"
543
+ >
544
+ <PendingMessageCard
545
+ reply={reply}
546
+ headerRef={(node) => {
547
+ headerRefs.current[reply.uid] = node;
548
+ }}
549
+ />
550
+ </li>
551
+ ))}
552
+ {messages.map((message) => {
553
+ const uid = message.uid;
554
+ const expanded = expandedUids.has(uid);
555
+ const bodyId = `thread-message-${uid}`;
556
+ const messageUnread = isUnread(message);
557
+ return (
558
+ <li
559
+ key={uid}
560
+ ref={(node) => {
561
+ rowRefs.current[uid] = node;
562
+ }}
563
+ data-unread={messageUnread ? "true" : undefined}
564
+ >
565
+ {expanded ? (
566
+ <MessageDetailPane
567
+ inThread
568
+ threadSubject={subject}
569
+ threadHeader={{
570
+ bodyId,
571
+ unread: messageUnread,
572
+ onToggle: () => toggleExpanded(uid),
573
+ buttonRef: (node) => {
574
+ headerRefs.current[uid] = node;
575
+ },
576
+ }}
577
+ footer={uid === footerUid}
578
+ shortcuts={!!shortcuts && uid === keyboardUid}
579
+ message={message}
580
+ attachments={attachmentsByUid[uid] ?? []}
581
+ isSentItems={folderTypeOf(message) === "sent_items"}
582
+ isOutbox={folderTypeOf(message) === "outbox"}
583
+ draftsFolderUid={folders.find((folder) => folder.type === "drafts")?.uid}
584
+ folders={folders}
585
+ onMoved={removeMessage}
586
+ onFolderCreated={onFolderCreated}
587
+ onRecalled={patchMessage}
588
+ onReceiptHandled={patchMessage}
589
+ onScheduledSendCanceled={removeMessage}
590
+ onArchived={removeMessage}
591
+ onChanged={patchMessage}
592
+ labels={labels}
593
+ onLabelsChanged={patchMessage}
594
+ onLabelCreated={onLabelCreated}
595
+ />
596
+ ) : (
597
+ <>
598
+ <CollapsedCard
599
+ from={message.from}
600
+ date={new Date(message.receivedDate).toLocaleString()}
601
+ preview={message.bodyPreview || (message.encrypted ? <EncryptedPreview /> : "")}
602
+ unread={messageUnread}
603
+ senderClassName={senderClass(messageUnread)}
604
+ dateClassName={dateClass(messageUnread)}
605
+ buttonRef={(node) => {
606
+ headerRefs.current[uid] = node;
607
+ }}
608
+ buttonProps={{
609
+ onClick: () => toggleExpanded(uid),
610
+ "aria-expanded": false,
611
+ "aria-controls": bodyId,
612
+ }}
613
+ />
614
+ <div id={bodyId} hidden />
615
+ </>
616
+ )}
617
+ </li>
618
+ );
619
+ })}
620
+ </ul>
621
+ )}
622
+ </div>
623
+ );
624
+ }