@rapidmx/web-client 0.15.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. package/README.md +381 -373
  2. package/apps/admin/branding/index.tsx +39 -39
  3. package/apps/admin/data-requests/index.tsx +482 -480
  4. package/apps/admin/distribution-lists/new/index.tsx +3 -1
  5. package/apps/admin/domains/[uid].tsx +166 -164
  6. package/apps/admin/domains/new/index.tsx +3 -1
  7. package/apps/admin/escrow-scopes/[uid].tsx +350 -348
  8. package/apps/admin/escrow-scopes/new/index.tsx +3 -1
  9. package/apps/admin/index.tsx +129 -124
  10. package/apps/admin/mailboxes/[uid].tsx +271 -224
  11. package/apps/admin/mailboxes/new/index.tsx +30 -28
  12. package/apps/admin/plugins/index.tsx +15 -15
  13. package/apps/admin/retention-policy/index.tsx +39 -39
  14. package/apps/admin/signing-certificates/index.tsx +343 -343
  15. package/apps/admin/transport-rules/new/index.tsx +3 -1
  16. package/apps/escrow/audit-log/index.tsx +196 -196
  17. package/apps/escrow/matters/[uid].tsx +621 -621
  18. package/apps/escrow/matters/new/index.tsx +3 -1
  19. package/apps/shared/auth/adminAccess.ts +99 -99
  20. package/apps/shared/components/admin/layout/AdminShell.tsx +381 -378
  21. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
  22. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
  23. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
  24. package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
  25. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +50 -3
  27. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
  28. package/apps/shared/components/admin/settings/PluginsManager.tsx +1545 -1545
  29. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
  30. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
  31. package/apps/shared/components/admin/setup/SetupWizard.tsx +446 -444
  32. package/apps/shared/components/admin/usePagedList.tsx +129 -129
  33. package/apps/shared/components/calendar/EventEditor.tsx +5 -10
  34. package/apps/shared/components/calendar/EventFormParts.tsx +16 -47
  35. package/apps/shared/components/calendar/EventModal.tsx +206 -206
  36. package/apps/shared/components/calendar/GuestInput.tsx +100 -0
  37. package/apps/shared/components/calendar/MonthView.tsx +185 -185
  38. package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
  39. package/apps/shared/components/calendar/RequestChangeForm.tsx +23 -54
  40. package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
  41. package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
  42. package/apps/shared/components/calendar/allDay.ts +124 -124
  43. package/apps/shared/components/calendar/eventForm.ts +2 -2
  44. package/apps/shared/components/calendar/eventFormat.ts +49 -22
  45. package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
  46. package/apps/shared/components/contacts/ContactForm.tsx +383 -383
  47. package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
  48. package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
  49. package/apps/shared/components/contacts/layout/ContactsShell.tsx +218 -216
  50. package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
  51. package/apps/shared/components/layout/AppShell.tsx +461 -480
  52. package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
  53. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
  54. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
  55. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
  56. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
  57. package/apps/shared/components/layout/UserMenu.tsx +439 -424
  58. package/apps/shared/components/mail/ConversationList.tsx +323 -323
  59. package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -491
  60. package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
  61. package/apps/shared/components/mail/MailSelectionBar.tsx +240 -227
  62. package/apps/shared/components/mail/MenuButton.tsx +31 -11
  63. package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1559
  64. package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
  65. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
  66. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
  67. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1677
  68. package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
  69. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
  70. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
  71. package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
  72. package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
  73. package/apps/shared/components/mail/layout/MailShell.tsx +161 -134
  74. package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
  75. package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
  76. package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
  77. package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
  78. package/apps/shared/components/mail/reading/messageExport.ts +59 -0
  79. package/apps/shared/components/mail/reading/printMessage.ts +99 -0
  80. package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
  81. package/apps/shared/components/mail/verificationSeals.ts +125 -125
  82. package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
  83. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
  84. package/apps/shared/components/settings/layout/SettingsShell.tsx +257 -254
  85. package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
  86. package/apps/shared/components/tasks/layout/TasksShell.tsx +219 -217
  87. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
  88. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
  89. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
  90. package/apps/shared/keyboard/dispatch.ts +124 -124
  91. package/apps/shared/keyboard/format.ts +89 -89
  92. package/apps/shared/keyboard/keymap.ts +114 -114
  93. package/apps/shared/keyboard/registry.ts +65 -65
  94. package/apps/shared/keyboard/targets.ts +79 -79
  95. package/apps/shared/mail/folderCounts.ts +17 -0
  96. package/apps/shared/mail/folderOfType.ts +49 -0
  97. package/apps/shared/mail/folderTree.ts +143 -143
  98. package/apps/shared/mail/listAllPages.ts +39 -39
  99. package/apps/shared/mail/newMailNotifications.ts +171 -171
  100. package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
  101. package/apps/shared/mail/outbox/sendJob.ts +19 -1
  102. package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
  103. package/apps/shared/mail/permanentDelete.ts +160 -0
  104. package/apps/shared/mail/primaryMailbox.ts +71 -0
  105. package/apps/shared/mail/reportNotices.ts +66 -0
  106. package/apps/shared/mail/senderBlocking.ts +141 -0
  107. package/apps/shared/mail/useCollapsedSections.ts +146 -0
  108. package/apps/shared/mail/useMailConnection.ts +205 -203
  109. package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
  110. package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
  111. package/apps/shared/mail/useMarkMessageRead.ts +47 -47
  112. package/apps/shared/mail/useNewMailNotifications.ts +178 -178
  113. package/apps/shared/mail/usePermanentDelete.tsx +160 -0
  114. package/apps/shared/navigation/frameContext.tsx +1 -1
  115. package/apps/shared/navigation/index.ts +43 -0
  116. package/apps/shared/navigation/pageTitle.ts +19 -0
  117. package/apps/shared/notifications/store.ts +560 -560
  118. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
  119. package/apps/shared/search/crossMailboxSearch.ts +169 -0
  120. package/apps/shared/search/localIndexBuilder.ts +481 -481
  121. package/apps/shared/signing/enrollmentStorage.ts +33 -33
  122. package/apps/shared/signing/enrollmentTracker.ts +385 -385
  123. package/apps/shared/signing/enrollmentView.ts +251 -251
  124. package/apps/shared/signing/useNow.ts +19 -19
  125. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
  126. package/apps/shared/styles/app.css +396 -386
  127. package/apps/www/_shell.tsx +82 -0
  128. package/apps/www/calendar/index.tsx +581 -571
  129. package/apps/www/contacts/[uid].tsx +112 -109
  130. package/apps/www/contacts/index.tsx +14 -3
  131. package/apps/www/index.tsx +2962 -2479
  132. package/apps/www/messages/[uid].tsx +139 -135
  133. package/apps/www/settings/appearance/index.tsx +5 -2
  134. package/apps/www/settings/auto-reply/index.tsx +136 -133
  135. package/apps/www/settings/blocked-senders/index.tsx +303 -0
  136. package/apps/www/settings/encryption/index.tsx +1290 -1287
  137. package/apps/www/settings/filters/[uid].tsx +179 -176
  138. package/apps/www/settings/filters/index.tsx +105 -102
  139. package/apps/www/settings/filters/new/index.tsx +165 -140
  140. package/apps/www/settings/labels/index.tsx +207 -204
  141. package/apps/www/settings/privacy/index.tsx +495 -492
  142. package/apps/www/settings/profile/index.tsx +251 -248
  143. package/apps/www/settings/read-receipts/index.tsx +150 -147
  144. package/apps/www/settings/sharing/index.tsx +259 -256
  145. package/apps/www/settings/signatures/[uid].tsx +175 -172
  146. package/apps/www/settings/signatures/index.tsx +91 -88
  147. package/apps/www/settings/signatures/new/index.tsx +138 -135
  148. package/apps/www/tasks/index.tsx +654 -649
  149. package/dist/apps/admin/data-requests/index.js +1 -1
  150. package/dist/apps/admin/distribution-lists/new/index.js +3 -1
  151. package/dist/apps/admin/domains/[uid].js +3 -1
  152. package/dist/apps/admin/domains/new/index.js +3 -1
  153. package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
  154. package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
  155. package/dist/apps/admin/index.js +5 -2
  156. package/dist/apps/admin/mailboxes/[uid].js +15 -3
  157. package/dist/apps/admin/mailboxes/new/index.js +3 -1
  158. package/dist/apps/admin/transport-rules/new/index.js +3 -1
  159. package/dist/apps/escrow/matters/new/index.js +3 -1
  160. package/dist/apps/shared/components/admin/layout/AdminShell.js +6 -3
  161. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
  162. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
  163. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
  164. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
  165. package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
  166. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +25 -3
  167. package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
  168. package/dist/apps/shared/components/calendar/EventEditor.js +5 -10
  169. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -2
  170. package/dist/apps/shared/components/calendar/EventFormParts.js +5 -16
  171. package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
  172. package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
  173. package/dist/apps/shared/components/calendar/RequestChangeForm.js +11 -22
  174. package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -2
  175. package/dist/apps/shared/components/calendar/eventFormat.d.ts +21 -8
  176. package/dist/apps/shared/components/calendar/eventFormat.js +50 -23
  177. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +5 -4
  178. package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
  179. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  180. package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
  181. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +7 -5
  182. package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
  183. package/dist/apps/shared/components/layout/AppShell.js +24 -33
  184. package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
  185. package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
  186. package/dist/apps/shared/components/layout/UserMenu.d.ts +4 -1
  187. package/dist/apps/shared/components/layout/UserMenu.js +6 -5
  188. package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
  189. package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
  190. package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
  191. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +14 -4
  192. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -13
  193. package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
  194. package/dist/apps/shared/components/mail/MenuButton.js +15 -11
  195. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
  196. package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
  197. package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
  198. package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
  199. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +8 -4
  200. package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
  201. package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
  202. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
  203. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
  204. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +16 -7
  205. package/dist/apps/shared/components/mail/layout/MailShell.js +78 -65
  206. package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
  207. package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
  208. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
  209. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
  210. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
  211. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
  212. package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
  213. package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
  214. package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
  215. package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
  216. package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
  217. package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
  218. package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
  219. package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
  220. package/dist/apps/shared/components/settings/layout/SettingsShell.js +10 -7
  221. package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
  222. package/dist/apps/shared/components/tasks/layout/TasksShell.js +7 -5
  223. package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
  224. package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
  225. package/dist/apps/shared/mail/folderCounts.js +12 -0
  226. package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
  227. package/dist/apps/shared/mail/folderOfType.js +40 -0
  228. package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
  229. package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
  230. package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
  231. package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
  232. package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
  233. package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
  234. package/dist/apps/shared/mail/permanentDelete.js +127 -0
  235. package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
  236. package/dist/apps/shared/mail/primaryMailbox.js +61 -0
  237. package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
  238. package/dist/apps/shared/mail/reportNotices.js +47 -0
  239. package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
  240. package/dist/apps/shared/mail/senderBlocking.js +92 -0
  241. package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
  242. package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
  243. package/dist/apps/shared/mail/useMailConnection.js +3 -1
  244. package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
  245. package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
  246. package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
  247. package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
  248. package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
  249. package/dist/apps/shared/navigation/index.d.ts +16 -0
  250. package/dist/apps/shared/navigation/index.js +34 -0
  251. package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
  252. package/dist/apps/shared/navigation/pageTitle.js +12 -0
  253. package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
  254. package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
  255. package/dist/apps/shared/styles/app.css +396 -386
  256. package/dist/apps/www/_shell.d.ts +26 -0
  257. package/dist/apps/www/_shell.js +31 -0
  258. package/dist/apps/www/calendar/index.d.ts +7 -2
  259. package/dist/apps/www/calendar/index.js +7 -4
  260. package/dist/apps/www/contacts/[uid].d.ts +14 -3
  261. package/dist/apps/www/contacts/[uid].js +5 -3
  262. package/dist/apps/www/contacts/index.d.ts +7 -2
  263. package/dist/apps/www/contacts/index.js +8 -4
  264. package/dist/apps/www/index.d.ts +7 -2
  265. package/dist/apps/www/index.js +542 -207
  266. package/dist/apps/www/messages/[uid].d.ts +14 -3
  267. package/dist/apps/www/messages/[uid].js +7 -5
  268. package/dist/apps/www/settings/appearance/index.d.ts +8 -2
  269. package/dist/apps/www/settings/appearance/index.js +4 -2
  270. package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
  271. package/dist/apps/www/settings/auto-reply/index.js +4 -2
  272. package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
  273. package/dist/apps/www/settings/blocked-senders/index.js +134 -0
  274. package/dist/apps/www/settings/encryption/index.d.ts +7 -2
  275. package/dist/apps/www/settings/encryption/index.js +4 -2
  276. package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
  277. package/dist/apps/www/settings/filters/[uid].js +4 -2
  278. package/dist/apps/www/settings/filters/index.d.ts +7 -2
  279. package/dist/apps/www/settings/filters/index.js +4 -2
  280. package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
  281. package/dist/apps/www/settings/filters/new/index.js +28 -5
  282. package/dist/apps/www/settings/labels/index.d.ts +7 -2
  283. package/dist/apps/www/settings/labels/index.js +4 -2
  284. package/dist/apps/www/settings/privacy/index.d.ts +7 -2
  285. package/dist/apps/www/settings/privacy/index.js +4 -2
  286. package/dist/apps/www/settings/profile/index.d.ts +7 -2
  287. package/dist/apps/www/settings/profile/index.js +4 -2
  288. package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
  289. package/dist/apps/www/settings/read-receipts/index.js +4 -2
  290. package/dist/apps/www/settings/sharing/index.d.ts +7 -2
  291. package/dist/apps/www/settings/sharing/index.js +4 -2
  292. package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
  293. package/dist/apps/www/settings/signatures/[uid].js +4 -2
  294. package/dist/apps/www/settings/signatures/index.d.ts +7 -2
  295. package/dist/apps/www/settings/signatures/index.js +4 -2
  296. package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
  297. package/dist/apps/www/settings/signatures/new/index.js +5 -3
  298. package/dist/apps/www/tasks/index.d.ts +7 -2
  299. package/dist/apps/www/tasks/index.js +5 -3
  300. package/package.json +4 -2
  301. package/apps/shared/navigation/AppRouter.tsx +0 -300
  302. package/apps/shared/navigation/idle.ts +0 -45
  303. package/apps/shared/navigation/routerContext.tsx +0 -83
  304. package/apps/shared/navigation/routes.ts +0 -72
  305. package/apps/www/_routedPage.tsx +0 -24
  306. package/apps/www/_routes.ts +0 -37
  307. package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
  308. package/dist/apps/shared/navigation/AppRouter.js +0 -242
  309. package/dist/apps/shared/navigation/idle.d.ts +0 -14
  310. package/dist/apps/shared/navigation/idle.js +0 -43
  311. package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
  312. package/dist/apps/shared/navigation/routerContext.js +0 -56
  313. package/dist/apps/shared/navigation/routes.d.ts +0 -32
  314. package/dist/apps/shared/navigation/routes.js +0 -37
  315. package/dist/apps/www/_routedPage.d.ts +0 -12
  316. package/dist/apps/www/_routedPage.js +0 -19
  317. package/dist/apps/www/_routes.d.ts +0 -11
  318. package/dist/apps/www/_routes.js +0 -31
@@ -1,323 +1,323 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React, { useState } from "react";
6
- import { HiChevronDown, HiChevronRight, HiOutlineFlag, HiOutlinePaperClip } from "react-icons/hi2";
7
- import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
- import { Message } from "@rapidmx/react-shared/mail/mailApi.js";
9
- import { formatMailAddress } from "@rapidmx/react-shared/mail/mailAddress.js";
10
- import MailAddress from "./MailAddress.js";
11
- import { EncryptedPreview, conversationLooksEncrypted } from "./reading/EncryptedPreview.js";
12
- import { ConversationSummary, listConversationMessages } from "@rapidmx/react-shared/mail/conversationsApi.js";
13
- import { ROW_FOCUS_CLASS, UnreadBar, UnreadLabel, dateClass, isUnread, rowClass, senderClass, subjectClass } from "./unreadStyle.js";
14
- import SwipeRow from "./SwipeRow.js";
15
- import InviteRowChip from "./invite/InviteRowChip.js";
16
-
17
- export interface ConversationListProps {
18
- conversations: ConversationSummary[];
19
- /** The mailbox the conversations were listed from - `listConversationMessages()` is mailbox-scoped. */
20
- mailboxUid: string;
21
- /** The message currently open in the reading pane, so the row standing for it can be marked. */
22
- selectedUid: string | null;
23
- /**
24
- * Opens a conversation in the reading pane, positioned at one of its messages: the one a child row
25
- * stands for, or the conversation's own `latestMessageUid` for a parent row. The conversation goes
26
- * with the uid because the pane shows the whole thread, not just that message.
27
- */
28
- onOpenMessage: (conversation: ConversationSummary, uid: string) => void;
29
- /** Newer copies of messages this list already fetched - the reading pane marks the message it opens as
30
- * read, which this list would otherwise keep showing as unread until the thread is collapsed and
31
- * expanded again. */
32
- messageOverrides?: Record<string, Message>;
33
- /** Called with one of a conversation's own messages carrying the answer the reader just gave to its meeting request (from that row's RSVP chip), so the caller can keep it. An answer given from the conversation's own (parent) row is kept by this list itself. */
34
- onMeetingResponded?: (updated: Message) => void;
35
- /** Turns each parent row into a checkbox row: select mode here ticks whole conversations, since a
36
- * conversation is what this list's rows are. A child row stays a plain "open this message" button - a
37
- * mixed conversation/message selection has no sensible bulk semantics (see `MailListToolbar`). */
38
- selectMode?: boolean;
39
- /** The `conversationId`s currently ticked. */
40
- selectedConversationIds?: Set<string>;
41
- onToggleSelected?: (conversation: ConversationSummary) => void;
42
- /**
43
- * What swiping a parent row does on a phone: right to left archives the conversation (resolving whether it went through), left
44
- * to right asks where to move it. Absent, or `enabled: false` (a desktop), rows don't follow a finger.
45
- */
46
- swipe?: {
47
- enabled: boolean;
48
- onArchive: (conversation: ConversationSummary) => Promise<boolean>;
49
- onMove: (conversation: ConversationSummary) => void;
50
- };
51
- /**
52
- * Whether a conversation's own messages read newest first, matching the order sense the rows themselves
53
- * are in ("Newest on top"). `listConversationMessages()` always answers oldest first - the order a
54
- * thread is read in - so this reverses that copy for display rather than asking for it differently.
55
- */
56
- newestFirst?: boolean;
57
- }
58
-
59
- /** Every participant as `Name <address>`, for the tooltip and the accessible text of the "+N" a crowded row folds them into. */
60
- function participantList(conversation: ConversationSummary): string {
61
- return conversation.participants.map((participant) => formatMailAddress(participant)).join(", ");
62
- }
63
-
64
- /**
65
- * The nested, Outlook-style conversation list: one parent row per conversation - its participants, subject,
66
- * message count, unread count, flag/attachment hints and the latest message's preview - expanding via its
67
- * own chevron into the conversation's individual messages as child rows, fetched on first expand with
68
- * `listConversationMessages()` (one request for the whole thread, across folders, oldest first).
69
- *
70
- * Opening a row opens the whole conversation in `ConversationThreadPane`, positioned at the message the row
71
- * stands for: the latest one for a parent row, that one for a child row.
72
- *
73
- * The chevron is a button of its own beside the row's own button rather than inside it (a button can't
74
- * nest inside a button), so expanding and opening are separately reachable by keyboard and each carries
75
- * its own name.
76
- */
77
- export default function ConversationList({
78
- conversations,
79
- mailboxUid,
80
- selectedUid,
81
- onOpenMessage,
82
- messageOverrides,
83
- onMeetingResponded,
84
- selectMode,
85
- selectedConversationIds,
86
- onToggleSelected,
87
- swipe,
88
- newestFirst,
89
- }: ConversationListProps) {
90
- const [expanded, setExpanded] = useState<Set<string>>(new Set());
91
- const [messagesById, setMessagesById] = useState<Record<string, Message[]>>({});
92
- const [loadingIds, setLoadingIds] = useState<Set<string>>(new Set());
93
- const [errorsById, setErrorsById] = useState<Record<string, string>>({});
94
- // The answers given from a row's RSVP chip, by message uid: a row draws them at once, whichever row (the conversation's, or one of its messages) they were given from.
95
- const [answers, setAnswers] = useState<Record<string, Message["meetingResponse"]>>({});
96
-
97
- function answered(updated: Message) {
98
- setAnswers((prev) => ({ ...prev, [updated.uid]: updated.meetingResponse }));
99
- }
100
-
101
- function toggle(conversation: ConversationSummary) {
102
- const id = conversation.conversationId;
103
- setExpanded((prev) => {
104
- const next = new Set(prev);
105
- if (next.has(id)) {
106
- next.delete(id);
107
- } else {
108
- next.add(id);
109
- }
110
- return next;
111
- });
112
- if (expanded.has(id) || messagesById[id] || loadingIds.has(id)) {
113
- return;
114
- }
115
- setLoadingIds((prev) => new Set(prev).add(id));
116
- setErrorsById((prev) => {
117
- const next = { ...prev };
118
- delete next[id];
119
- return next;
120
- });
121
- listConversationMessages(mailboxUid, id)
122
- .then((loaded) => setMessagesById((prev) => ({ ...prev, [id]: loaded })))
123
- .catch((err) =>
124
- setErrorsById((prev) => ({
125
- ...prev,
126
- [id]: err instanceof ApiRequestError ? err.message : "Could not load this conversation's messages.",
127
- }))
128
- )
129
- .finally(() =>
130
- setLoadingIds((prev) => {
131
- const next = new Set(prev);
132
- next.delete(id);
133
- return next;
134
- })
135
- );
136
- }
137
-
138
- if (conversations.length === 0) {
139
- return <p className="p-4 text-sm text-text-muted">No conversations in this folder.</p>;
140
- }
141
-
142
- return (
143
- // `overflow-x-clip`: the panels a swiped row drags along stand outside its edges (see `SwipeRow`).
144
- <ul className={swipe?.enabled ? "overflow-x-clip" : undefined}>
145
- {conversations.map((conversation) => {
146
- const id = conversation.conversationId;
147
- const isExpanded = expanded.has(id);
148
- const unread = conversation.unreadCount > 0;
149
- const loaded = messagesById[id];
150
- // `listConversationMessages()` answers oldest first; the rows read in whichever sense the
151
- // list itself is arranged in. Copied before reversing - the fetched array is cached.
152
- const children = loaded && newestFirst ? [...loaded].reverse() : loaded;
153
- const panelId = `conversation-messages-${id}`;
154
- const ticked = selectedConversationIds?.has(id) ?? false;
155
- return (
156
- <li key={id}>
157
- <SwipeRow
158
- as="div"
159
- enabled={!!swipe?.enabled && !selectMode}
160
- onArchive={() => swipe!.onArchive(conversation)}
161
- onMove={() => swipe!.onMove(conversation)}
162
- data-message-uid={conversation.latestMessageUid}
163
- data-unread={unread ? "true" : undefined}
164
- className={rowClass({ unread, selected: conversation.latestMessageUid === selectedUid || ticked }, "flex items-stretch")}
165
- >
166
- <UnreadBar unread={unread} />
167
- {selectMode && (
168
- <span className="shrink-0 flex items-center pl-3">
169
- <input
170
- type="checkbox"
171
- checked={ticked}
172
- onChange={() => onToggleSelected?.(conversation)}
173
- aria-label={`Select conversation: ${conversation.subject || "(no subject)"}`}
174
- className="w-4 h-4 accent-primary"
175
- />
176
- </span>
177
- )}
178
- <button
179
- type="button"
180
- onClick={() => toggle(conversation)}
181
- aria-expanded={isExpanded}
182
- aria-controls={panelId}
183
- aria-label={`${isExpanded ? "Collapse" : "Expand"} conversation: ${conversation.subject || "(no subject)"}`}
184
- className={["shrink-0 px-2 text-text-muted hover:text-text", ROW_FOCUS_CLASS].join(" ")}
185
- >
186
- {isExpanded ? (
187
- <HiChevronDown size={16} aria-hidden="true" />
188
- ) : (
189
- <HiChevronRight size={16} aria-hidden="true" />
190
- )}
191
- </button>
192
- {/* The open button and, for a meeting request, its RSVP chip: a button can't hold a button, so the chip is the button's sibling. */}
193
- <div className="flex-1 min-w-0 flex flex-col">
194
- <button
195
- type="button"
196
- data-row-open
197
- onClick={() => onOpenMessage(conversation, conversation.latestMessageUid)}
198
- className={["w-full text-left pr-4 py-3", ROW_FOCUS_CLASS].join(" ")}
199
- >
200
- <UnreadLabel unread={unread} />
201
- <div className="flex items-center justify-between gap-2 text-sm">
202
- {/* The first participant in full - name and address - and the rest as a count, with everyone's
203
- address in its tooltip; the latest sender when the summary lists none. */}
204
- <MailAddress
205
- recipient={conversation.participants[0] ?? conversation.latestFrom}
206
- className={["flex-1", senderClass(unread)].join(" ")}
207
- />
208
- {conversation.participants.length > 1 && (
209
- <span className="text-xs text-text-muted shrink-0 font-normal" title={participantList(conversation)}>
210
- +{conversation.participants.length - 1}
211
- <span className="sr-only"> more: {participantList(conversation)}</span>
212
- </span>
213
- )}
214
- <span className={["text-xs shrink-0", dateClass(unread)].join(" ")}>
215
- {new Date(conversation.latestDate).toLocaleDateString()}
216
- </span>
217
- </div>
218
- <div className={["text-sm truncate", subjectClass(unread)].join(" ")}>{conversation.subject || "(no subject)"}</div>
219
- <div className="text-xs text-text-muted truncate font-normal">
220
- {/* An encrypted latest message has no preview (the server never had its plaintext): say so, with a lock, not nothing. */}
221
- {conversationLooksEncrypted(conversation) ? <EncryptedPreview /> : conversation.latestPreview}
222
- </div>
223
- <div className="flex items-center gap-2 mt-1 text-xs text-text-muted font-normal">
224
- {conversation.messageCount > 1 && <span>{conversation.messageCount} messages</span>}
225
- {/* A one-message conversation's unread state is already the row's own
226
- bolding - counting it "1 unread" on every such row is just noise. */}
227
- {unread && conversation.messageCount > 1 && (
228
- <span className="py-0.5 px-2 rounded-pill bg-primary/15 text-primary-dark font-bold">
229
- {conversation.unreadCount} unread
230
- </span>
231
- )}
232
- {conversation.hasAttachments && (
233
- <HiOutlinePaperClip size={13} aria-label="Has attachments" />
234
- )}
235
- {conversation.flagged && (
236
- <HiOutlineFlag size={13} aria-label="Flagged" className="text-danger" />
237
- )}
238
- </div>
239
- </button>
240
- <InviteRowChip
241
- message={
242
- {
243
- uid: conversation.latestMessageUid,
244
- meetingMethod: conversation.latestMeetingMethod ?? undefined,
245
- meetingResponse:
246
- answers[conversation.latestMessageUid] ??
247
- messageOverrides?.[conversation.latestMessageUid]?.meetingResponse ??
248
- conversation.latestMeetingResponse ??
249
- undefined,
250
- } as Message
251
- }
252
- onResponded={answered}
253
- className="mr-4"
254
- />
255
- </div>
256
- </SwipeRow>
257
- <ul id={panelId} hidden={!isExpanded}>
258
- {errorsById[id] && (
259
- <li role="alert" className="pl-8 pr-4 py-2 text-xs text-danger border-b border-border">
260
- {errorsById[id]}
261
- </li>
262
- )}
263
- {loadingIds.has(id) && (
264
- <li className="pl-8 pr-4 py-2 text-xs text-text-muted border-b border-border">
265
- Loading messages&hellip;
266
- </li>
267
- )}
268
- {children?.map((fetched) => {
269
- const message = messageOverrides?.[fetched.uid] ?? fetched;
270
- const messageUnread = isUnread(message);
271
- return (
272
- <li
273
- key={message.uid}
274
- data-message-uid={message.uid}
275
- data-unread={messageUnread ? "true" : undefined}
276
- className={rowClass({ unread: messageUnread, selected: message.uid === selectedUid })}
277
- >
278
- <UnreadBar unread={messageUnread} />
279
- <button
280
- type="button"
281
- data-row-open
282
- onClick={() => onOpenMessage(conversation, message.uid)}
283
- className={["w-full text-left pl-8 pr-4 py-2", ROW_FOCUS_CLASS].join(" ")}
284
- >
285
- <UnreadLabel unread={messageUnread} />
286
- <div className="flex items-center justify-between gap-2 text-sm">
287
- <MailAddress recipient={message.from} className={senderClass(messageUnread)} />
288
- <span className={["text-xs shrink-0", dateClass(messageUnread)].join(" ")}>
289
- {new Date(message.receivedDate).toLocaleDateString()}
290
- </span>
291
- </div>
292
- <div className="flex items-center gap-2 text-xs text-text-muted font-normal">
293
- <span className="truncate">{message.bodyPreview || (message.encrypted ? <EncryptedPreview /> : null)}</span>
294
- {message.hasAttachments && (
295
- <HiOutlinePaperClip size={12} aria-label="Has attachments" />
296
- )}
297
- {message.flags.flagged && (
298
- <HiOutlineFlag
299
- size={12}
300
- aria-label="Flagged"
301
- className="text-danger"
302
- />
303
- )}
304
- </div>
305
- </button>
306
- <InviteRowChip
307
- message={{ ...message, meetingResponse: answers[message.uid] ?? message.meetingResponse }}
308
- onResponded={(updated) => {
309
- answered(updated);
310
- onMeetingResponded?.(updated);
311
- }}
312
- className="ml-8 mr-4"
313
- />
314
- </li>
315
- );
316
- })}
317
- </ul>
318
- </li>
319
- );
320
- })}
321
- </ul>
322
- );
323
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { useState } from "react";
6
+ import { HiChevronDown, HiChevronRight, HiOutlineFlag, HiOutlinePaperClip } from "react-icons/hi2";
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import { Message } from "@rapidmx/react-shared/mail/mailApi.js";
9
+ import { formatMailAddress } from "@rapidmx/react-shared/mail/mailAddress.js";
10
+ import MailAddress from "./MailAddress.js";
11
+ import { EncryptedPreview, conversationLooksEncrypted } from "./reading/EncryptedPreview.js";
12
+ import { ConversationSummary, listConversationMessages } from "@rapidmx/react-shared/mail/conversationsApi.js";
13
+ import { ROW_FOCUS_CLASS, UnreadBar, UnreadLabel, dateClass, isUnread, rowClass, senderClass, subjectClass } from "./unreadStyle.js";
14
+ import SwipeRow from "./SwipeRow.js";
15
+ import InviteRowChip from "./invite/InviteRowChip.js";
16
+
17
+ export interface ConversationListProps {
18
+ conversations: ConversationSummary[];
19
+ /** The mailbox the conversations were listed from - `listConversationMessages()` is mailbox-scoped. */
20
+ mailboxUid: string;
21
+ /** The message currently open in the reading pane, so the row standing for it can be marked. */
22
+ selectedUid: string | null;
23
+ /**
24
+ * Opens a conversation in the reading pane, positioned at one of its messages: the one a child row
25
+ * stands for, or the conversation's own `latestMessageUid` for a parent row. The conversation goes
26
+ * with the uid because the pane shows the whole thread, not just that message.
27
+ */
28
+ onOpenMessage: (conversation: ConversationSummary, uid: string) => void;
29
+ /** Newer copies of messages this list already fetched - the reading pane marks the message it opens as
30
+ * read, which this list would otherwise keep showing as unread until the thread is collapsed and
31
+ * expanded again. */
32
+ messageOverrides?: Record<string, Message>;
33
+ /** Called with one of a conversation's own messages carrying the answer the reader just gave to its meeting request (from that row's RSVP chip), so the caller can keep it. An answer given from the conversation's own (parent) row is kept by this list itself. */
34
+ onMeetingResponded?: (updated: Message) => void;
35
+ /** Turns each parent row into a checkbox row: select mode here ticks whole conversations, since a
36
+ * conversation is what this list's rows are. A child row stays a plain "open this message" button - a
37
+ * mixed conversation/message selection has no sensible bulk semantics (see `MailListToolbar`). */
38
+ selectMode?: boolean;
39
+ /** The `conversationId`s currently ticked. */
40
+ selectedConversationIds?: Set<string>;
41
+ onToggleSelected?: (conversation: ConversationSummary) => void;
42
+ /**
43
+ * What swiping a parent row does on a phone: right to left archives the conversation (resolving whether it went through), left
44
+ * to right asks where to move it. Absent, or `enabled: false` (a desktop), rows don't follow a finger.
45
+ */
46
+ swipe?: {
47
+ enabled: boolean;
48
+ onArchive: (conversation: ConversationSummary) => Promise<boolean>;
49
+ onMove: (conversation: ConversationSummary) => void;
50
+ };
51
+ /**
52
+ * Whether a conversation's own messages read newest first, matching the order sense the rows themselves
53
+ * are in ("Newest on top"). `listConversationMessages()` always answers oldest first - the order a
54
+ * thread is read in - so this reverses that copy for display rather than asking for it differently.
55
+ */
56
+ newestFirst?: boolean;
57
+ }
58
+
59
+ /** Every participant as `Name <address>`, for the tooltip and the accessible text of the "+N" a crowded row folds them into. */
60
+ function participantList(conversation: ConversationSummary): string {
61
+ return conversation.participants.map((participant) => formatMailAddress(participant)).join(", ");
62
+ }
63
+
64
+ /**
65
+ * The nested, Outlook-style conversation list: one parent row per conversation - its participants, subject,
66
+ * message count, unread count, flag/attachment hints and the latest message's preview - expanding via its
67
+ * own chevron into the conversation's individual messages as child rows, fetched on first expand with
68
+ * `listConversationMessages()` (one request for the whole thread, across folders, oldest first).
69
+ *
70
+ * Opening a row opens the whole conversation in `ConversationThreadPane`, positioned at the message the row
71
+ * stands for: the latest one for a parent row, that one for a child row.
72
+ *
73
+ * The chevron is a button of its own beside the row's own button rather than inside it (a button can't
74
+ * nest inside a button), so expanding and opening are separately reachable by keyboard and each carries
75
+ * its own name.
76
+ */
77
+ export default function ConversationList({
78
+ conversations,
79
+ mailboxUid,
80
+ selectedUid,
81
+ onOpenMessage,
82
+ messageOverrides,
83
+ onMeetingResponded,
84
+ selectMode,
85
+ selectedConversationIds,
86
+ onToggleSelected,
87
+ swipe,
88
+ newestFirst,
89
+ }: ConversationListProps) {
90
+ const [expanded, setExpanded] = useState<Set<string>>(new Set());
91
+ const [messagesById, setMessagesById] = useState<Record<string, Message[]>>({});
92
+ const [loadingIds, setLoadingIds] = useState<Set<string>>(new Set());
93
+ const [errorsById, setErrorsById] = useState<Record<string, string>>({});
94
+ // The answers given from a row's RSVP chip, by message uid: a row draws them at once, whichever row (the conversation's, or one of its messages) they were given from.
95
+ const [answers, setAnswers] = useState<Record<string, Message["meetingResponse"]>>({});
96
+
97
+ function answered(updated: Message) {
98
+ setAnswers((prev) => ({ ...prev, [updated.uid]: updated.meetingResponse }));
99
+ }
100
+
101
+ function toggle(conversation: ConversationSummary) {
102
+ const id = conversation.conversationId;
103
+ setExpanded((prev) => {
104
+ const next = new Set(prev);
105
+ if (next.has(id)) {
106
+ next.delete(id);
107
+ } else {
108
+ next.add(id);
109
+ }
110
+ return next;
111
+ });
112
+ if (expanded.has(id) || messagesById[id] || loadingIds.has(id)) {
113
+ return;
114
+ }
115
+ setLoadingIds((prev) => new Set(prev).add(id));
116
+ setErrorsById((prev) => {
117
+ const next = { ...prev };
118
+ delete next[id];
119
+ return next;
120
+ });
121
+ listConversationMessages(mailboxUid, id)
122
+ .then((loaded) => setMessagesById((prev) => ({ ...prev, [id]: loaded })))
123
+ .catch((err) =>
124
+ setErrorsById((prev) => ({
125
+ ...prev,
126
+ [id]: err instanceof ApiRequestError ? err.message : "Could not load this conversation's messages.",
127
+ }))
128
+ )
129
+ .finally(() =>
130
+ setLoadingIds((prev) => {
131
+ const next = new Set(prev);
132
+ next.delete(id);
133
+ return next;
134
+ })
135
+ );
136
+ }
137
+
138
+ if (conversations.length === 0) {
139
+ return <p className="p-4 text-sm text-text-muted">No conversations in this folder.</p>;
140
+ }
141
+
142
+ return (
143
+ // `overflow-x-clip`: the panels a swiped row drags along stand outside its edges (see `SwipeRow`).
144
+ <ul className={swipe?.enabled ? "overflow-x-clip" : undefined}>
145
+ {conversations.map((conversation) => {
146
+ const id = conversation.conversationId;
147
+ const isExpanded = expanded.has(id);
148
+ const unread = conversation.unreadCount > 0;
149
+ const loaded = messagesById[id];
150
+ // `listConversationMessages()` answers oldest first; the rows read in whichever sense the
151
+ // list itself is arranged in. Copied before reversing - the fetched array is cached.
152
+ const children = loaded && newestFirst ? [...loaded].reverse() : loaded;
153
+ const panelId = `conversation-messages-${id}`;
154
+ const ticked = selectedConversationIds?.has(id) ?? false;
155
+ return (
156
+ <li key={id}>
157
+ <SwipeRow
158
+ as="div"
159
+ enabled={!!swipe?.enabled && !selectMode}
160
+ onArchive={() => swipe!.onArchive(conversation)}
161
+ onMove={() => swipe!.onMove(conversation)}
162
+ data-message-uid={conversation.latestMessageUid}
163
+ data-unread={unread ? "true" : undefined}
164
+ className={rowClass({ unread, selected: conversation.latestMessageUid === selectedUid || ticked }, "flex items-stretch")}
165
+ >
166
+ <UnreadBar unread={unread} />
167
+ {selectMode && (
168
+ <span className="shrink-0 flex items-center pl-3">
169
+ <input
170
+ type="checkbox"
171
+ checked={ticked}
172
+ onChange={() => onToggleSelected?.(conversation)}
173
+ aria-label={`Select conversation: ${conversation.subject || "(no subject)"}`}
174
+ className="w-4 h-4 accent-primary"
175
+ />
176
+ </span>
177
+ )}
178
+ <button
179
+ type="button"
180
+ onClick={() => toggle(conversation)}
181
+ aria-expanded={isExpanded}
182
+ aria-controls={panelId}
183
+ aria-label={`${isExpanded ? "Collapse" : "Expand"} conversation: ${conversation.subject || "(no subject)"}`}
184
+ className={["shrink-0 px-2 text-text-muted hover:text-text", ROW_FOCUS_CLASS].join(" ")}
185
+ >
186
+ {isExpanded ? (
187
+ <HiChevronDown size={16} aria-hidden="true" />
188
+ ) : (
189
+ <HiChevronRight size={16} aria-hidden="true" />
190
+ )}
191
+ </button>
192
+ {/* The open button and, for a meeting request, its RSVP chip: a button can't hold a button, so the chip is the button's sibling. */}
193
+ <div className="flex-1 min-w-0 flex flex-col">
194
+ <button
195
+ type="button"
196
+ data-row-open
197
+ onClick={() => onOpenMessage(conversation, conversation.latestMessageUid)}
198
+ className={["w-full text-left pr-4 py-3", ROW_FOCUS_CLASS].join(" ")}
199
+ >
200
+ <UnreadLabel unread={unread} />
201
+ <div className="flex items-center justify-between gap-2 text-sm">
202
+ {/* The first participant in full - name and address - and the rest as a count, with everyone's
203
+ address in its tooltip; the latest sender when the summary lists none. */}
204
+ <MailAddress
205
+ recipient={conversation.participants[0] ?? conversation.latestFrom}
206
+ className={["flex-1", senderClass(unread)].join(" ")}
207
+ />
208
+ {conversation.participants.length > 1 && (
209
+ <span className="text-xs text-text-muted shrink-0 font-normal" title={participantList(conversation)}>
210
+ +{conversation.participants.length - 1}
211
+ <span className="sr-only"> more: {participantList(conversation)}</span>
212
+ </span>
213
+ )}
214
+ <span className={["text-xs shrink-0", dateClass(unread)].join(" ")}>
215
+ {new Date(conversation.latestDate).toLocaleDateString()}
216
+ </span>
217
+ </div>
218
+ <div className={["text-sm truncate", subjectClass(unread)].join(" ")}>{conversation.subject || "(no subject)"}</div>
219
+ <div className="text-xs text-text-muted truncate font-normal">
220
+ {/* An encrypted latest message has no preview (the server never had its plaintext): say so, with a lock, not nothing. */}
221
+ {conversationLooksEncrypted(conversation) ? <EncryptedPreview /> : conversation.latestPreview}
222
+ </div>
223
+ <div className="flex items-center gap-2 mt-1 text-xs text-text-muted font-normal">
224
+ {conversation.messageCount > 1 && <span>{conversation.messageCount} messages</span>}
225
+ {/* A one-message conversation's unread state is already the row's own
226
+ bolding - counting it "1 unread" on every such row is just noise. */}
227
+ {unread && conversation.messageCount > 1 && (
228
+ <span className="py-0.5 px-2 rounded-pill bg-primary/15 text-primary-dark font-bold">
229
+ {conversation.unreadCount} unread
230
+ </span>
231
+ )}
232
+ {conversation.hasAttachments && (
233
+ <HiOutlinePaperClip size={13} aria-label="Has attachments" />
234
+ )}
235
+ {conversation.flagged && (
236
+ <HiOutlineFlag size={13} aria-label="Flagged" className="text-danger" />
237
+ )}
238
+ </div>
239
+ </button>
240
+ <InviteRowChip
241
+ message={
242
+ {
243
+ uid: conversation.latestMessageUid,
244
+ meetingMethod: conversation.latestMeetingMethod ?? undefined,
245
+ meetingResponse:
246
+ answers[conversation.latestMessageUid] ??
247
+ messageOverrides?.[conversation.latestMessageUid]?.meetingResponse ??
248
+ conversation.latestMeetingResponse ??
249
+ undefined,
250
+ } as Message
251
+ }
252
+ onResponded={answered}
253
+ className="mr-4"
254
+ />
255
+ </div>
256
+ </SwipeRow>
257
+ <ul id={panelId} hidden={!isExpanded}>
258
+ {errorsById[id] && (
259
+ <li role="alert" className="pl-8 pr-4 py-2 text-xs text-danger border-b border-border">
260
+ {errorsById[id]}
261
+ </li>
262
+ )}
263
+ {loadingIds.has(id) && (
264
+ <li className="pl-8 pr-4 py-2 text-xs text-text-muted border-b border-border">
265
+ Loading messages&hellip;
266
+ </li>
267
+ )}
268
+ {children?.map((fetched) => {
269
+ const message = messageOverrides?.[fetched.uid] ?? fetched;
270
+ const messageUnread = isUnread(message);
271
+ return (
272
+ <li
273
+ key={message.uid}
274
+ data-message-uid={message.uid}
275
+ data-unread={messageUnread ? "true" : undefined}
276
+ className={rowClass({ unread: messageUnread, selected: message.uid === selectedUid })}
277
+ >
278
+ <UnreadBar unread={messageUnread} />
279
+ <button
280
+ type="button"
281
+ data-row-open
282
+ onClick={() => onOpenMessage(conversation, message.uid)}
283
+ className={["w-full text-left pl-8 pr-4 py-2", ROW_FOCUS_CLASS].join(" ")}
284
+ >
285
+ <UnreadLabel unread={messageUnread} />
286
+ <div className="flex items-center justify-between gap-2 text-sm">
287
+ <MailAddress recipient={message.from} className={senderClass(messageUnread)} />
288
+ <span className={["text-xs shrink-0", dateClass(messageUnread)].join(" ")}>
289
+ {new Date(message.receivedDate).toLocaleDateString()}
290
+ </span>
291
+ </div>
292
+ <div className="flex items-center gap-2 text-xs text-text-muted font-normal">
293
+ <span className="truncate">{message.bodyPreview || (message.encrypted ? <EncryptedPreview /> : null)}</span>
294
+ {message.hasAttachments && (
295
+ <HiOutlinePaperClip size={12} aria-label="Has attachments" />
296
+ )}
297
+ {message.flags.flagged && (
298
+ <HiOutlineFlag
299
+ size={12}
300
+ aria-label="Flagged"
301
+ className="text-danger"
302
+ />
303
+ )}
304
+ </div>
305
+ </button>
306
+ <InviteRowChip
307
+ message={{ ...message, meetingResponse: answers[message.uid] ?? message.meetingResponse }}
308
+ onResponded={(updated) => {
309
+ answered(updated);
310
+ onMeetingResponded?.(updated);
311
+ }}
312
+ className="ml-8 mr-4"
313
+ />
314
+ </li>
315
+ );
316
+ })}
317
+ </ul>
318
+ </li>
319
+ );
320
+ })}
321
+ </ul>
322
+ );
323
+ }