@rapidmx/web-client 0.5.0 → 0.7.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 (224) hide show
  1. package/README.md +88 -0
  2. package/apps/shared/components/admin/layout/AdminShell.tsx +25 -8
  3. package/apps/shared/components/admin/settings/BrandingForm.tsx +398 -385
  4. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -112
  5. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +159 -152
  6. package/apps/shared/components/admin/settings/PluginsManager.tsx +1288 -1235
  7. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +9 -1
  8. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -290
  9. package/apps/shared/components/admin/setup/SetupWizard.tsx +443 -416
  10. package/apps/shared/components/calendar/layout/CalendarShell.tsx +194 -192
  11. package/apps/shared/components/contacts/layout/ContactsShell.tsx +199 -197
  12. package/apps/shared/components/layout/AppShell.tsx +288 -270
  13. package/apps/shared/components/mail/ConversationList.tsx +225 -33
  14. package/apps/shared/components/mail/ConversationThreadPane.tsx +434 -216
  15. package/apps/shared/components/mail/MailListToolbar.tsx +265 -0
  16. package/apps/shared/components/mail/MailSelectionBar.tsx +222 -0
  17. package/apps/shared/components/mail/MenuButton.tsx +384 -0
  18. package/apps/shared/components/mail/MessageDetailPane.tsx +280 -215
  19. package/apps/shared/components/mail/MoveToFolderDialog.tsx +263 -0
  20. package/apps/shared/components/mail/compose/ComposeContext.tsx +29 -2
  21. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1700 -1652
  22. package/apps/shared/components/mail/compose/RecipientInput.tsx +356 -0
  23. package/apps/shared/components/mail/compose/RichTextEditor.tsx +38 -3
  24. package/apps/shared/components/mail/compose/quotedBody.ts +100 -0
  25. package/apps/shared/components/mail/compose/recipients.ts +104 -0
  26. package/apps/shared/components/mail/labelMenu.tsx +278 -0
  27. package/apps/shared/components/mail/layout/MailShell.tsx +31 -4
  28. package/apps/shared/components/mail/listPreferences.ts +228 -0
  29. package/apps/shared/components/settings/layout/SettingsShell.tsx +23 -10
  30. package/apps/shared/components/tasks/layout/TasksShell.tsx +201 -199
  31. package/apps/shared/plugins/pluginNav.ts +73 -0
  32. package/apps/shared/search/localIndexRpcClient.ts +3 -1
  33. package/apps/shared/styles/app.css +28 -5
  34. package/apps/www/index.tsx +1854 -1314
  35. package/apps/www/messages/[uid].tsx +5 -4
  36. package/dist/apps/admin/_404.d.ts +2 -0
  37. package/dist/apps/admin/_500.d.ts +2 -0
  38. package/dist/apps/admin/_layout.d.ts +8 -0
  39. package/dist/apps/admin/audit-log/index.d.ts +11 -0
  40. package/dist/apps/admin/branding/index.d.ts +3 -0
  41. package/dist/apps/admin/data-requests/index.d.ts +3 -0
  42. package/dist/apps/admin/distribution-lists/[uid].d.ts +7 -0
  43. package/dist/apps/admin/distribution-lists/index.d.ts +3 -0
  44. package/dist/apps/admin/distribution-lists/new/index.d.ts +3 -0
  45. package/dist/apps/admin/domains/[uid].d.ts +7 -0
  46. package/dist/apps/admin/domains/index.d.ts +3 -0
  47. package/dist/apps/admin/domains/new/index.d.ts +3 -0
  48. package/dist/apps/admin/encryption-policy/index.d.ts +3 -0
  49. package/dist/apps/admin/escrow-scopes/[uid].d.ts +21 -0
  50. package/dist/apps/admin/escrow-scopes/index.d.ts +3 -0
  51. package/dist/apps/admin/escrow-scopes/new/index.d.ts +3 -0
  52. package/dist/apps/admin/index.d.ts +3 -0
  53. package/dist/apps/admin/ingest-queue/index.d.ts +4 -0
  54. package/dist/apps/admin/mailbox-policy/index.d.ts +3 -0
  55. package/dist/apps/admin/mailboxes/[uid].d.ts +7 -0
  56. package/dist/apps/admin/mailboxes/new/index.d.ts +3 -0
  57. package/dist/apps/admin/plugins/index.d.ts +3 -0
  58. package/dist/apps/admin/quarantine/index.d.ts +6 -0
  59. package/dist/apps/admin/retention-policy/index.d.ts +3 -0
  60. package/dist/apps/admin/setup/index.d.ts +3 -0
  61. package/dist/apps/admin/transport-rules/[uid].d.ts +7 -0
  62. package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +21 -0
  63. package/dist/apps/admin/transport-rules/index.d.ts +3 -0
  64. package/dist/apps/admin/transport-rules/new/index.d.ts +3 -0
  65. package/dist/apps/escrow/_layout.d.ts +10 -0
  66. package/dist/apps/escrow/audit-log/index.d.ts +9 -0
  67. package/dist/apps/escrow/index.d.ts +3 -0
  68. package/dist/apps/escrow/matters/[uid].d.ts +7 -0
  69. package/dist/apps/escrow/matters/new/index.d.ts +3 -0
  70. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.d.ts +17 -0
  71. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +32 -0
  72. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +32 -0
  73. package/dist/apps/shared/components/admin/layout/AdminShell.js +12 -5
  74. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +12 -0
  75. package/dist/apps/shared/components/admin/mailboxes/MailboxTable.d.ts +6 -0
  76. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.d.ts +19 -0
  77. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +12 -0
  78. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +13 -0
  79. package/dist/apps/shared/components/admin/settings/BrandingForm.js +6 -5
  80. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +12 -0
  81. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +15 -0
  82. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +2 -2
  83. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.d.ts +9 -0
  84. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.d.ts +19 -0
  85. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +13 -0
  86. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +2 -2
  87. package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +11 -0
  88. package/dist/apps/shared/components/admin/settings/PluginsManager.js +33 -14
  89. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +12 -0
  90. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +2 -2
  91. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.d.ts +14 -0
  92. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +1 -1
  93. package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +36 -0
  94. package/dist/apps/shared/components/admin/setup/SetupWizard.js +14 -10
  95. package/dist/apps/shared/components/admin/signOut.d.ts +23 -0
  96. package/dist/apps/shared/components/admin/usePagedList.d.ts +39 -0
  97. package/dist/apps/shared/components/calendar/CalendarListSidebar.d.ts +26 -0
  98. package/dist/apps/shared/components/calendar/EventModal.d.ts +46 -0
  99. package/dist/apps/shared/components/calendar/MonthView.d.ts +15 -0
  100. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +20 -0
  101. package/dist/apps/shared/components/calendar/ResourcePicker.d.ts +19 -0
  102. package/dist/apps/shared/components/calendar/SplitDayView.d.ts +28 -0
  103. package/dist/apps/shared/components/calendar/TimeGridView.d.ts +17 -0
  104. package/dist/apps/shared/components/calendar/allDay.d.ts +51 -0
  105. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +40 -0
  106. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +2 -2
  107. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +23 -0
  108. package/dist/apps/shared/components/contacts/ContactForm.d.ts +20 -0
  109. package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +40 -0
  110. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +25 -0
  111. package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +40 -0
  112. package/dist/apps/shared/components/contacts/contactKeys.d.ts +29 -0
  113. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +21 -0
  114. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +2 -2
  115. package/dist/apps/shared/components/escrow/layout/EscrowShell.d.ts +25 -0
  116. package/dist/apps/shared/components/forms/StringListField.d.ts +22 -0
  117. package/dist/apps/shared/components/layout/AppShell.d.ts +49 -0
  118. package/dist/apps/shared/components/layout/AppShell.js +15 -13
  119. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +18 -0
  120. package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +51 -0
  121. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +13 -0
  122. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +66 -0
  123. package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +45 -0
  124. package/dist/apps/shared/components/layout/UserMenu.d.ts +23 -0
  125. package/dist/apps/shared/components/mail/ConversationList.d.ts +47 -0
  126. package/dist/apps/shared/components/mail/ConversationList.js +84 -10
  127. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +62 -0
  128. package/dist/apps/shared/components/mail/ConversationThreadPane.js +217 -60
  129. package/dist/apps/shared/components/mail/MailListToolbar.d.ts +55 -0
  130. package/dist/apps/shared/components/mail/MailListToolbar.js +149 -0
  131. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +59 -0
  132. package/dist/apps/shared/components/mail/MailSelectionBar.js +48 -0
  133. package/dist/apps/shared/components/mail/MenuButton.d.ts +71 -0
  134. package/dist/apps/shared/components/mail/MenuButton.js +211 -0
  135. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +101 -0
  136. package/dist/apps/shared/components/mail/MessageDetailPane.js +126 -129
  137. package/dist/apps/shared/components/mail/MoveToFolderDialog.d.ts +57 -0
  138. package/dist/apps/shared/components/mail/MoveToFolderDialog.js +138 -0
  139. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +73 -0
  140. package/dist/apps/shared/components/mail/compose/ComposeContext.js +3 -1
  141. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +15 -0
  142. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +37 -0
  143. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +52 -20
  144. package/dist/apps/shared/components/mail/compose/EmojiPicker.d.ts +14 -0
  145. package/dist/apps/shared/components/mail/compose/GifPicker.d.ts +13 -0
  146. package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +36 -0
  147. package/dist/apps/shared/components/mail/compose/RecipientInput.js +203 -0
  148. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +43 -0
  149. package/dist/apps/shared/components/mail/compose/RichTextEditor.js +21 -3
  150. package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.d.ts +16 -0
  151. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +27 -0
  152. package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +37 -0
  153. package/dist/apps/shared/components/mail/compose/quotedBody.js +85 -0
  154. package/dist/apps/shared/components/mail/compose/recipients.d.ts +24 -0
  155. package/dist/apps/shared/components/mail/compose/recipients.js +87 -0
  156. package/dist/apps/shared/components/mail/labelMenu.d.ts +97 -0
  157. package/dist/apps/shared/components/mail/labelMenu.js +134 -0
  158. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +65 -0
  159. package/dist/apps/shared/components/mail/layout/MailShell.js +17 -5
  160. package/dist/apps/shared/components/mail/listPreferences.d.ts +101 -0
  161. package/dist/apps/shared/components/mail/listPreferences.js +186 -0
  162. package/dist/apps/shared/components/mail/pinnedSigners.d.ts +33 -0
  163. package/dist/apps/shared/components/mail/verificationSeals.d.ts +22 -0
  164. package/dist/apps/shared/components/mail/writableMailboxes.d.ts +15 -0
  165. package/dist/apps/shared/components/rules/RuleBuilder.d.ts +70 -0
  166. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +41 -0
  167. package/dist/apps/shared/components/settings/layout/SettingsShell.js +19 -10
  168. package/dist/apps/shared/components/tasks/TasksSidebar.d.ts +41 -0
  169. package/dist/apps/shared/components/tasks/TasksToolbar.d.ts +14 -0
  170. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +24 -0
  171. package/dist/apps/shared/components/tasks/layout/TasksShell.js +2 -2
  172. package/dist/apps/shared/mail/findWellKnownFolderUid.d.ts +5 -0
  173. package/dist/apps/shared/mail/listAllPages.d.ts +17 -0
  174. package/dist/apps/shared/plugins/pluginNav.d.ts +39 -0
  175. package/dist/apps/shared/plugins/pluginNav.js +33 -0
  176. package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +13 -0
  177. package/dist/apps/shared/search/localIndexBlockCipher.d.ts +36 -0
  178. package/dist/apps/shared/search/localIndexBuilder.d.ts +85 -0
  179. package/dist/apps/shared/search/localIndexKey.d.ts +4 -0
  180. package/dist/apps/shared/search/localIndexRpcClient.d.ts +71 -0
  181. package/dist/apps/shared/search/localIndexRpcClient.js +3 -1
  182. package/dist/apps/shared/search/localIndexSchema.d.ts +111 -0
  183. package/dist/apps/shared/search/localIndexSizePreference.d.ts +27 -0
  184. package/dist/apps/shared/search/localIndexStorage.d.ts +28 -0
  185. package/dist/apps/shared/search/localIndexVFS.d.ts +101 -0
  186. package/dist/apps/shared/search/localIndexWorker.d.ts +145 -0
  187. package/dist/apps/shared/search/searchTier2.d.ts +30 -0
  188. package/dist/apps/shared/styles/app.css +28 -5
  189. package/dist/apps/www/_404.d.ts +2 -0
  190. package/dist/apps/www/_500.d.ts +2 -0
  191. package/dist/apps/www/_layout.d.ts +14 -0
  192. package/dist/apps/www/calendar/index.d.ts +3 -0
  193. package/dist/apps/www/contacts/[uid].d.ts +13 -0
  194. package/dist/apps/www/contacts/index.d.ts +3 -0
  195. package/dist/apps/www/index.d.ts +3 -0
  196. package/dist/apps/www/index.js +506 -112
  197. package/dist/apps/www/messages/[uid].d.ts +11 -0
  198. package/dist/apps/www/messages/[uid].js +2 -3
  199. package/dist/apps/www/settings/auto-reply/index.d.ts +4 -0
  200. package/dist/apps/www/settings/encryption/index.d.ts +7 -0
  201. package/dist/apps/www/settings/filters/[uid].d.ts +8 -0
  202. package/dist/apps/www/settings/filters/_mailFilterRuleConfig.d.ts +11 -0
  203. package/dist/apps/www/settings/filters/index.d.ts +4 -0
  204. package/dist/apps/www/settings/filters/new/index.d.ts +4 -0
  205. package/dist/apps/www/settings/labels/index.d.ts +4 -0
  206. package/dist/apps/www/settings/privacy/index.d.ts +4 -0
  207. package/dist/apps/www/settings/read-receipts/index.d.ts +4 -0
  208. package/dist/apps/www/settings/sharing/index.d.ts +4 -0
  209. package/dist/apps/www/settings/signatures/[uid].d.ts +8 -0
  210. package/dist/apps/www/settings/signatures/index.d.ts +4 -0
  211. package/dist/apps/www/settings/signatures/new/index.d.ts +4 -0
  212. package/dist/apps/www/settings/signatures/signatureDefaults.d.ts +10 -0
  213. package/dist/apps/www/tasks/index.d.ts +3 -0
  214. package/package.json +3 -3
  215. package/apps/shared/components/booking/AvailabilityEditor.tsx +0 -118
  216. package/apps/www/settings/booking-types/[uid].tsx +0 -306
  217. package/apps/www/settings/booking-types/index.tsx +0 -98
  218. package/apps/www/settings/booking-types/new/index.tsx +0 -212
  219. package/apps/www/settings/focused-inbox/index.tsx +0 -150
  220. package/dist/apps/shared/components/booking/AvailabilityEditor.js +0 -45
  221. package/dist/apps/www/settings/booking-types/[uid].js +0 -134
  222. package/dist/apps/www/settings/booking-types/index.js +0 -29
  223. package/dist/apps/www/settings/booking-types/new/index.js +0 -86
  224. package/dist/apps/www/settings/focused-inbox/index.js +0 -71
@@ -2,57 +2,249 @@
2
2
  // Copyright (C) 2026 Jean-Philippe Steinmetz
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
- import React from "react";
6
- import { ConversationSummary } from "@rapidmx/react-shared/mail/conversationsApi.js";
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 { ConversationSummary, listConversationMessages } from "@rapidmx/react-shared/mail/conversationsApi.js";
7
10
 
8
11
  export interface ConversationListProps {
9
12
  conversations: ConversationSummary[];
10
- selectedId: string | null;
11
- onSelect: (conversation: ConversationSummary) => void;
13
+ /** The mailbox the conversations were listed from - `listConversationMessages()` is mailbox-scoped. */
14
+ mailboxUid: string;
15
+ /** The message currently open in the reading pane, so the row standing for it can be marked. */
16
+ selectedUid: string | null;
17
+ /**
18
+ * Opens a conversation in the reading pane, positioned at one of its messages: the one a child row
19
+ * stands for, or the conversation's own `latestMessageUid` for a parent row. The conversation goes
20
+ * with the uid because the pane shows the whole thread, not just that message.
21
+ */
22
+ onOpenMessage: (conversation: ConversationSummary, uid: string) => void;
23
+ /** Newer copies of messages this list already fetched - the reading pane marks the message it opens as
24
+ * read, which this list would otherwise keep showing as unread until the thread is collapsed and
25
+ * expanded again. */
26
+ messageOverrides?: Record<string, Message>;
27
+ /** Turns each parent row into a checkbox row: select mode here ticks whole conversations, since a
28
+ * conversation is what this list's rows are. A child row stays a plain "open this message" button - a
29
+ * mixed conversation/message selection has no sensible bulk semantics (see `MailListToolbar`). */
30
+ selectMode?: boolean;
31
+ /** The `conversationId`s currently ticked. */
32
+ selectedConversationIds?: Set<string>;
33
+ onToggleSelected?: (conversation: ConversationSummary) => void;
34
+ /**
35
+ * Whether a conversation's own messages read newest first, matching the order sense the rows themselves
36
+ * are in ("Newest on top"). `listConversationMessages()` always answers oldest first - the order a
37
+ * thread is read in - so this reverses that copy for display rather than asking for it differently.
38
+ */
39
+ newestFirst?: boolean;
12
40
  }
13
41
 
14
- /** The "By conversation" counterpart to the per-message list `apps/www/index.tsx` renders inline for
15
- * "By date" — conversations are mailbox-wide (see `conversationsApi.ts`), so there's no per-folder
16
- * empty/loading state to distinguish here beyond what the page itself already handles. */
17
- export default function ConversationList({ conversations, selectedId, onSelect }: ConversationListProps) {
42
+ function participantNames(conversation: ConversationSummary): string {
43
+ return conversation.participants.map((participant) => participant.displayName || participant.address).join(", ");
44
+ }
45
+
46
+ /**
47
+ * The nested, Outlook-style conversation list: one parent row per conversation - its participants, subject,
48
+ * message count, unread count, flag/attachment hints and the latest message's preview - expanding via its
49
+ * own chevron into the conversation's individual messages as child rows, fetched on first expand with
50
+ * `listConversationMessages()` (one request for the whole thread, across folders, oldest first).
51
+ *
52
+ * Opening a row opens the whole conversation in `ConversationThreadPane`, positioned at the message the row
53
+ * stands for: the latest one for a parent row, that one for a child row.
54
+ *
55
+ * The chevron is a button of its own beside the row's own button rather than inside it (a button can't
56
+ * nest inside a button), so expanding and opening are separately reachable by keyboard and each carries
57
+ * its own name.
58
+ */
59
+ export default function ConversationList({
60
+ conversations,
61
+ mailboxUid,
62
+ selectedUid,
63
+ onOpenMessage,
64
+ messageOverrides,
65
+ selectMode,
66
+ selectedConversationIds,
67
+ onToggleSelected,
68
+ newestFirst,
69
+ }: ConversationListProps) {
70
+ const [expanded, setExpanded] = useState<Set<string>>(new Set());
71
+ const [messagesById, setMessagesById] = useState<Record<string, Message[]>>({});
72
+ const [loadingIds, setLoadingIds] = useState<Set<string>>(new Set());
73
+ const [errorsById, setErrorsById] = useState<Record<string, string>>({});
74
+
75
+ function toggle(conversation: ConversationSummary) {
76
+ const id = conversation.conversationId;
77
+ setExpanded((prev) => {
78
+ const next = new Set(prev);
79
+ if (next.has(id)) {
80
+ next.delete(id);
81
+ } else {
82
+ next.add(id);
83
+ }
84
+ return next;
85
+ });
86
+ if (expanded.has(id) || messagesById[id] || loadingIds.has(id)) {
87
+ return;
88
+ }
89
+ setLoadingIds((prev) => new Set(prev).add(id));
90
+ setErrorsById((prev) => {
91
+ const next = { ...prev };
92
+ delete next[id];
93
+ return next;
94
+ });
95
+ listConversationMessages(mailboxUid, id)
96
+ .then((loaded) => setMessagesById((prev) => ({ ...prev, [id]: loaded })))
97
+ .catch((err) =>
98
+ setErrorsById((prev) => ({
99
+ ...prev,
100
+ [id]: err instanceof ApiRequestError ? err.message : "Could not load this conversation's messages.",
101
+ }))
102
+ )
103
+ .finally(() =>
104
+ setLoadingIds((prev) => {
105
+ const next = new Set(prev);
106
+ next.delete(id);
107
+ return next;
108
+ })
109
+ );
110
+ }
111
+
18
112
  if (conversations.length === 0) {
19
- return <p className="p-4 text-sm text-text-muted">No conversations in this mailbox.</p>;
113
+ return <p className="p-4 text-sm text-text-muted">No conversations in this folder.</p>;
20
114
  }
21
115
 
22
116
  return (
23
117
  <ul>
24
118
  {conversations.map((conversation) => {
119
+ const id = conversation.conversationId;
120
+ const isExpanded = expanded.has(id);
25
121
  const unread = conversation.unreadCount > 0;
122
+ const loaded = messagesById[id];
123
+ // `listConversationMessages()` answers oldest first; the rows read in whichever sense the
124
+ // list itself is arranged in. Copied before reversing - the fetched array is cached.
125
+ const children = loaded && newestFirst ? [...loaded].reverse() : loaded;
126
+ const panelId = `conversation-messages-${id}`;
127
+ const ticked = selectedConversationIds?.has(id) ?? false;
26
128
  return (
27
- <li key={conversation.conversationId}>
28
- <button
29
- type="button"
30
- onClick={() => onSelect(conversation)}
129
+ <li key={id}>
130
+ <div
31
131
  className={[
32
- "w-full text-left px-4 py-3 border-b border-border",
33
- conversation.conversationId === selectedId ? "bg-primary/10" : "hover:bg-surface-alt",
34
- unread ? "font-semibold" : "",
132
+ "flex items-stretch border-b border-border",
133
+ conversation.latestMessageUid === selectedUid || ticked
134
+ ? "bg-primary/10"
135
+ : "hover:bg-surface-alt",
35
136
  ].join(" ")}
36
137
  >
37
- <div className="flex items-center justify-between gap-2 text-sm">
38
- <span className="truncate">
39
- {conversation.participants.map((p) => p.displayName || p.address).join(", ")}
138
+ {selectMode && (
139
+ <span className="shrink-0 flex items-center pl-3">
140
+ <input
141
+ type="checkbox"
142
+ checked={ticked}
143
+ onChange={() => onToggleSelected?.(conversation)}
144
+ aria-label={`Select conversation: ${conversation.subject || "(no subject)"}`}
145
+ className="w-4 h-4 accent-primary"
146
+ />
40
147
  </span>
41
- <span className="text-xs text-text-muted shrink-0">
42
- {new Date(conversation.latestDate).toLocaleDateString()}
43
- </span>
44
- </div>
45
- <div className="text-sm truncate">{conversation.subject || "(no subject)"}</div>
46
- <div className="flex items-center gap-2 mt-1 text-xs text-text-muted font-normal">
47
- {conversation.messageCount > 1 && <span>{conversation.messageCount} messages</span>}
48
- {unread && (
49
- <span className="py-0.5 px-2 rounded-pill bg-primary/10 text-primary-dark font-semibold">
50
- {conversation.unreadCount} unread
51
- </span>
148
+ )}
149
+ <button
150
+ type="button"
151
+ onClick={() => toggle(conversation)}
152
+ aria-expanded={isExpanded}
153
+ aria-controls={panelId}
154
+ aria-label={`${isExpanded ? "Collapse" : "Expand"} conversation: ${conversation.subject || "(no subject)"}`}
155
+ className="shrink-0 px-2 text-text-muted hover:text-text"
156
+ >
157
+ {isExpanded ? (
158
+ <HiChevronDown size={16} aria-hidden="true" />
159
+ ) : (
160
+ <HiChevronRight size={16} aria-hidden="true" />
52
161
  )}
53
- {conversation.hasAttachments && <span aria-label="Has attachments">&#128206;</span>}
54
- </div>
55
- </button>
162
+ </button>
163
+ <button
164
+ type="button"
165
+ onClick={() => onOpenMessage(conversation, conversation.latestMessageUid)}
166
+ className={["flex-1 min-w-0 text-left pr-4 py-3", unread ? "font-semibold" : ""].join(
167
+ " "
168
+ )}
169
+ >
170
+ <div className="flex items-center justify-between gap-2 text-sm">
171
+ <span className="truncate">{participantNames(conversation)}</span>
172
+ <span className="text-xs text-text-muted shrink-0">
173
+ {new Date(conversation.latestDate).toLocaleDateString()}
174
+ </span>
175
+ </div>
176
+ <div className="text-sm truncate">{conversation.subject || "(no subject)"}</div>
177
+ <div className="text-xs text-text-muted truncate font-normal">
178
+ {conversation.latestPreview}
179
+ </div>
180
+ <div className="flex items-center gap-2 mt-1 text-xs text-text-muted font-normal">
181
+ {conversation.messageCount > 1 && <span>{conversation.messageCount} messages</span>}
182
+ {/* A one-message conversation's unread state is already the row's own
183
+ bolding - counting it "1 unread" on every such row is just noise. */}
184
+ {unread && conversation.messageCount > 1 && (
185
+ <span className="py-0.5 px-2 rounded-pill bg-primary/10 text-primary-dark font-semibold">
186
+ {conversation.unreadCount} unread
187
+ </span>
188
+ )}
189
+ {conversation.hasAttachments && (
190
+ <HiOutlinePaperClip size={13} aria-label="Has attachments" />
191
+ )}
192
+ {conversation.flagged && (
193
+ <HiOutlineFlag size={13} aria-label="Flagged" className="text-danger" />
194
+ )}
195
+ </div>
196
+ </button>
197
+ </div>
198
+ <ul id={panelId} hidden={!isExpanded}>
199
+ {errorsById[id] && (
200
+ <li role="alert" className="pl-8 pr-4 py-2 text-xs text-danger border-b border-border">
201
+ {errorsById[id]}
202
+ </li>
203
+ )}
204
+ {loadingIds.has(id) && (
205
+ <li className="pl-8 pr-4 py-2 text-xs text-text-muted border-b border-border">
206
+ Loading messages&hellip;
207
+ </li>
208
+ )}
209
+ {children?.map((fetched) => {
210
+ const message = messageOverrides?.[fetched.uid] ?? fetched;
211
+ return (
212
+ <li key={message.uid}>
213
+ <button
214
+ type="button"
215
+ onClick={() => onOpenMessage(conversation, message.uid)}
216
+ className={[
217
+ "w-full text-left pl-8 pr-4 py-2 border-b border-border",
218
+ message.uid === selectedUid ? "bg-primary/10" : "hover:bg-surface-alt",
219
+ message.flags.read ? "" : "font-semibold",
220
+ ].join(" ")}
221
+ >
222
+ <div className="flex items-center justify-between gap-2 text-sm">
223
+ <span className="truncate">
224
+ {message.from.displayName || message.from.address}
225
+ </span>
226
+ <span className="text-xs text-text-muted shrink-0">
227
+ {new Date(message.receivedDate).toLocaleDateString()}
228
+ </span>
229
+ </div>
230
+ <div className="flex items-center gap-2 text-xs text-text-muted font-normal">
231
+ <span className="truncate">{message.bodyPreview}</span>
232
+ {message.hasAttachments && (
233
+ <HiOutlinePaperClip size={12} aria-label="Has attachments" />
234
+ )}
235
+ {message.flags.flagged && (
236
+ <HiOutlineFlag
237
+ size={12}
238
+ aria-label="Flagged"
239
+ className="text-danger"
240
+ />
241
+ )}
242
+ </div>
243
+ </button>
244
+ </li>
245
+ );
246
+ })}
247
+ </ul>
56
248
  </li>
57
249
  );
58
250
  })}