@remit/web-client 0.0.1

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 (298) hide show
  1. package/harness/build.mjs +84 -0
  2. package/harness/index.html +37 -0
  3. package/harness/vite-preset.ts +29 -0
  4. package/index.html +47 -0
  5. package/package.json +147 -0
  6. package/public/config.js +5 -0
  7. package/public/locales/de/common.json +1 -0
  8. package/public/locales/de/errors.json +1 -0
  9. package/public/locales/de/mail.json +13 -0
  10. package/public/locales/de/settings.json +1 -0
  11. package/public/locales/en/common.json +26 -0
  12. package/public/locales/en/errors.json +1 -0
  13. package/public/locales/en/mail.json +69 -0
  14. package/public/locales/en/settings.json +1 -0
  15. package/public/locales/fr/common.json +1 -0
  16. package/public/locales/fr/errors.json +1 -0
  17. package/public/locales/fr/mail.json +13 -0
  18. package/public/locales/fr/settings.json +1 -0
  19. package/public/locales/nl/common.json +1 -0
  20. package/public/locales/nl/errors.json +1 -0
  21. package/public/locales/nl/mail.json +13 -0
  22. package/public/locales/nl/settings.json +1 -0
  23. package/src/auth/AccountMenu.tsx +58 -0
  24. package/src/auth/AccountSession.tsx +19 -0
  25. package/src/auth/BetterAuthShell.tsx +198 -0
  26. package/src/auth/account-menu-mode.test.ts +64 -0
  27. package/src/auth/account-menu-mode.ts +28 -0
  28. package/src/auth/amplify-config.test.ts +158 -0
  29. package/src/auth/amplify-config.ts +60 -0
  30. package/src/auth/auth-error.test.ts +109 -0
  31. package/src/auth/auth-error.ts +107 -0
  32. package/src/auth/auth-interceptor.test.ts +86 -0
  33. package/src/auth/auth-interceptor.ts +23 -0
  34. package/src/auth/auth.css +334 -0
  35. package/src/auth/better-auth/BetterAuthAccount.tsx +25 -0
  36. package/src/auth/better-auth-config.ts +85 -0
  37. package/src/auth/better-auth-provider.tsx +20 -0
  38. package/src/auth/cognito/CognitoAccount.tsx +48 -0
  39. package/src/auth/cognito/CognitoShell.tsx +266 -0
  40. package/src/auth/cognito/cognito-token.test.ts +110 -0
  41. package/src/auth/cognito/cognito-token.ts +47 -0
  42. package/src/auth/cognito/cognito.css +21 -0
  43. package/src/auth/cognito-provider.tsx +22 -0
  44. package/src/auth/combined-provider.tsx +42 -0
  45. package/src/auth/composition.test.ts +136 -0
  46. package/src/auth/provider.tsx +63 -0
  47. package/src/auth/sign-out-visibility.test.ts +40 -0
  48. package/src/auth/sign-out-visibility.ts +10 -0
  49. package/src/auth/sign-up-disabled.test.ts +23 -0
  50. package/src/components/compose/AddressField.tsx +237 -0
  51. package/src/components/compose/ComposeBody.tsx +36 -0
  52. package/src/components/compose/ComposeForm.tsx +676 -0
  53. package/src/components/compose/ComposeProvider.tsx +133 -0
  54. package/src/components/compose/ComposeSmtpMissingBanner.tsx +49 -0
  55. package/src/components/compose/FromSelector.tsx +67 -0
  56. package/src/components/compose/FullCompose.tsx +82 -0
  57. package/src/components/compose/InlineCompose.tsx +29 -0
  58. package/src/components/compose/MobileComposeSheet.tsx +152 -0
  59. package/src/components/compose/PlateEditor.tsx +86 -0
  60. package/src/components/compose/PlateToolbar.tsx +110 -0
  61. package/src/components/compose/SubjectField.tsx +19 -0
  62. package/src/components/compose/draft-discard-banner.test.ts +76 -0
  63. package/src/components/compose/index.ts +6 -0
  64. package/src/components/compose/sanitize-quote-html.ts +23 -0
  65. package/src/components/layout/AppShellSkeleton.tsx +53 -0
  66. package/src/components/layout/ComposeFab.tsx +86 -0
  67. package/src/components/layout/Drawer.tsx +105 -0
  68. package/src/components/mail/AutoMovedIndicator.tsx +48 -0
  69. package/src/components/mail/BlockedImagesNotice.render.test.ts +65 -0
  70. package/src/components/mail/BlockedImagesNotice.tsx +41 -0
  71. package/src/components/mail/BriefPane.tsx +289 -0
  72. package/src/components/mail/ConversationView.tsx +462 -0
  73. package/src/components/mail/DailyBrief.tsx +451 -0
  74. package/src/components/mail/DraftsView.tsx +279 -0
  75. package/src/components/mail/FlaggedList.tsx +170 -0
  76. package/src/components/mail/FlaggedPane.tsx +217 -0
  77. package/src/components/mail/IntelligencePane.test.ts +123 -0
  78. package/src/components/mail/IntelligencePane.tsx +441 -0
  79. package/src/components/mail/MailListHeader.tsx +187 -0
  80. package/src/components/mail/MailNav.tsx +70 -0
  81. package/src/components/mail/MailSidebarAdapter.tsx +351 -0
  82. package/src/components/mail/MailViewChrome.tsx +95 -0
  83. package/src/components/mail/MailboxPane.tsx +1219 -0
  84. package/src/components/mail/MessageActionMenu.tsx +368 -0
  85. package/src/components/mail/MessageBody.tsx +207 -0
  86. package/src/components/mail/MessageBodyErrorBanner.render.test.ts +85 -0
  87. package/src/components/mail/MessageBodyErrorBanner.test.ts +86 -0
  88. package/src/components/mail/MessageBodyErrorBanner.tsx +90 -0
  89. package/src/components/mail/MessageCard.tsx +401 -0
  90. package/src/components/mail/MessageDetail.tsx +153 -0
  91. package/src/components/mail/MessageList.tsx +804 -0
  92. package/src/components/mail/MessageListItem.tsx +267 -0
  93. package/src/components/mail/MessageToolbar.render.test.ts +57 -0
  94. package/src/components/mail/MessageToolbar.tsx +166 -0
  95. package/src/components/mail/MobileMessageBar.tsx +142 -0
  96. package/src/components/mail/MoveToTrigger.tsx +250 -0
  97. package/src/components/mail/NavMenuButton.tsx +25 -0
  98. package/src/components/mail/OutboxPane.tsx +531 -0
  99. package/src/components/mail/PullToRefresh.tsx +24 -0
  100. package/src/components/mail/RawMessageView.tsx +84 -0
  101. package/src/components/mail/ReadingPaneEmpty.render.test.ts +42 -0
  102. package/src/components/mail/SelectionToolbar.tsx +139 -0
  103. package/src/components/mail/SpamRescue.render.test.ts +35 -0
  104. package/src/components/mail/SpamRescue.tsx +138 -0
  105. package/src/components/mail/SwipeableMessageRow.tsx +148 -0
  106. package/src/components/mail/message-body-error-banner-content.ts +87 -0
  107. package/src/components/mail/organize/OrganizeDialog.render.test.ts +37 -0
  108. package/src/components/mail/organize/OrganizeDialog.tsx +83 -0
  109. package/src/components/mail/organize/OrganizePanel.render.test.ts +102 -0
  110. package/src/components/mail/organize/OrganizePanel.tsx +344 -0
  111. package/src/components/onboarding/OnboardingWizard.tsx +1547 -0
  112. package/src/components/settings/AccountFormPanel.test.ts +72 -0
  113. package/src/components/settings/AccountFormPanel.tsx +942 -0
  114. package/src/components/settings/DangerZone.tsx +30 -0
  115. package/src/components/settings/DeleteAccountDialog.render.test.ts +47 -0
  116. package/src/components/settings/DeleteAccountDialog.tsx +224 -0
  117. package/src/components/settings/FiltersList.render.test.ts +71 -0
  118. package/src/components/settings/FiltersList.tsx +93 -0
  119. package/src/components/settings/account-form-helpers.test.ts +172 -0
  120. package/src/components/settings/account-form-helpers.ts +105 -0
  121. package/src/components/ui/AppVersion.tsx +50 -0
  122. package/src/components/ui/BugReportButton.tsx +45 -0
  123. package/src/components/ui/ConfirmDialog.tsx +125 -0
  124. package/src/components/ui/DropdownMenu.tsx +105 -0
  125. package/src/components/ui/EmptyState.tsx +11 -0
  126. package/src/components/ui/ErrorBanner.tsx +88 -0
  127. package/src/components/ui/ErrorBannerProvider.tsx +77 -0
  128. package/src/components/ui/ErrorBannerStack.tsx +34 -0
  129. package/src/components/ui/ErrorState.tsx +80 -0
  130. package/src/components/ui/FatalErrorOverlay.render.test.ts +66 -0
  131. package/src/components/ui/FatalErrorOverlay.tsx +151 -0
  132. package/src/components/ui/KeyboardShortcutsModal.tsx +120 -0
  133. package/src/components/ui/SlidePanel.tsx +72 -0
  134. package/src/components/ui/error-banners.test.ts +207 -0
  135. package/src/components/ui/error-banners.ts +111 -0
  136. package/src/hooks/queries/keys.ts +38 -0
  137. package/src/hooks/useArchiveMailbox.ts +104 -0
  138. package/src/hooks/useAutoMovedBadge.ts +77 -0
  139. package/src/hooks/useCurrentMailboxName.ts +99 -0
  140. package/src/hooks/useDebouncedValue.ts +12 -0
  141. package/src/hooks/useDeleteMessages.test.ts +103 -0
  142. package/src/hooks/useDeleteMessages.ts +226 -0
  143. package/src/hooks/useFilters.ts +112 -0
  144. package/src/hooks/useIntelligenceData.test.ts +269 -0
  145. package/src/hooks/useIntelligenceData.ts +284 -0
  146. package/src/hooks/useIsDark.ts +27 -0
  147. package/src/hooks/useKeyboardNavigation.ts +192 -0
  148. package/src/hooks/useLayoutTier.test.ts +47 -0
  149. package/src/hooks/useLayoutTier.ts +44 -0
  150. package/src/hooks/useLongPress.test.ts +291 -0
  151. package/src/hooks/useLongPress.ts +87 -0
  152. package/src/hooks/useMailboxAccount.ts +62 -0
  153. package/src/hooks/useMailboxNameIndex.ts +32 -0
  154. package/src/hooks/useMarkAsRead.test.ts +165 -0
  155. package/src/hooks/useMarkAsRead.ts +319 -0
  156. package/src/hooks/useMediaQuery.ts +39 -0
  157. package/src/hooks/useMessageBodyContent.test.ts +370 -0
  158. package/src/hooks/useMessageBodyContent.ts +380 -0
  159. package/src/hooks/useMoveMessages.test.ts +63 -0
  160. package/src/hooks/useMoveMessages.ts +221 -0
  161. package/src/hooks/useOrganizeJob.ts +90 -0
  162. package/src/hooks/useOrganizePreview.ts +40 -0
  163. package/src/hooks/useRescueCandidates.test.ts +217 -0
  164. package/src/hooks/useRescueCandidates.ts +48 -0
  165. package/src/hooks/useSaveDraft.ts +118 -0
  166. package/src/hooks/useSelection.test.ts +75 -0
  167. package/src/hooks/useSelection.ts +211 -0
  168. package/src/hooks/useSemanticSearch.ts +94 -0
  169. package/src/hooks/useSignature.ts +69 -0
  170. package/src/hooks/useStaleAccountSync.test.ts +211 -0
  171. package/src/hooks/useStaleAccountSync.ts +246 -0
  172. package/src/hooks/useSwipeNavigation.test.ts +35 -0
  173. package/src/hooks/useSwipeNavigation.ts +99 -0
  174. package/src/hooks/useToggleStar.ts +187 -0
  175. package/src/hooks/useToggleTrusted.test.ts +102 -0
  176. package/src/hooks/useToggleTrusted.ts +161 -0
  177. package/src/hooks/useTriageKeyboard.ts +103 -0
  178. package/src/hooks/useTriggerSync.test.ts +24 -0
  179. package/src/hooks/useTriggerSync.ts +58 -0
  180. package/src/hooks/useUpdateAddressFlags.ts +125 -0
  181. package/src/hooks/useVisualViewport.test.ts +127 -0
  182. package/src/hooks/useVisualViewport.ts +51 -0
  183. package/src/index.css +19 -0
  184. package/src/lib/account-order.test.ts +84 -0
  185. package/src/lib/account-order.ts +13 -0
  186. package/src/lib/adjacent-message.test.ts +43 -0
  187. package/src/lib/adjacent-message.ts +20 -0
  188. package/src/lib/api.ts +82 -0
  189. package/src/lib/app-info.ts +15 -0
  190. package/src/lib/auto-moved.test.ts +140 -0
  191. package/src/lib/auto-moved.ts +68 -0
  192. package/src/lib/autodiscovery.test.ts +94 -0
  193. package/src/lib/autodiscovery.ts +354 -0
  194. package/src/lib/brief.test.ts +366 -0
  195. package/src/lib/brief.ts +180 -0
  196. package/src/lib/bug-report.test.ts +213 -0
  197. package/src/lib/bug-report.ts +176 -0
  198. package/src/lib/client.ts +36 -0
  199. package/src/lib/console-errors.test.ts +64 -0
  200. package/src/lib/console-errors.ts +92 -0
  201. package/src/lib/conversation-target.test.ts +68 -0
  202. package/src/lib/conversation-target.ts +59 -0
  203. package/src/lib/display-category.ts +20 -0
  204. package/src/lib/drafts.test.ts +241 -0
  205. package/src/lib/drafts.ts +121 -0
  206. package/src/lib/error-classifier.test.ts +133 -0
  207. package/src/lib/error-classifier.ts +96 -0
  208. package/src/lib/fatal-error.test.ts +97 -0
  209. package/src/lib/fatal-error.ts +165 -0
  210. package/src/lib/folder-roles.test.ts +113 -0
  211. package/src/lib/folder-roles.ts +112 -0
  212. package/src/lib/format.test.ts +115 -0
  213. package/src/lib/format.ts +198 -0
  214. package/src/lib/i18n.ts +36 -0
  215. package/src/lib/intelligence-pref.test.ts +57 -0
  216. package/src/lib/intelligence-pref.ts +25 -0
  217. package/src/lib/keymap-dispatch.test.ts +178 -0
  218. package/src/lib/keymap-dispatch.ts +187 -0
  219. package/src/lib/keymap.test.ts +49 -0
  220. package/src/lib/keymap.ts +178 -0
  221. package/src/lib/mail-context.ts +64 -0
  222. package/src/lib/mail-route.test.ts +80 -0
  223. package/src/lib/mail-route.ts +44 -0
  224. package/src/lib/message-body-source.test.ts +98 -0
  225. package/src/lib/message-body-source.ts +48 -0
  226. package/src/lib/move-targets.test.ts +195 -0
  227. package/src/lib/move-targets.ts +74 -0
  228. package/src/lib/onboarding-completion.test.ts +64 -0
  229. package/src/lib/onboarding-completion.ts +23 -0
  230. package/src/lib/organize/filter-status.test.ts +99 -0
  231. package/src/lib/organize/filter-status.ts +86 -0
  232. package/src/lib/organize/organize-copy.test.ts +93 -0
  233. package/src/lib/organize/organize-copy.ts +61 -0
  234. package/src/lib/organize/organize-model.test.ts +104 -0
  235. package/src/lib/organize/organize-model.ts +105 -0
  236. package/src/lib/organize/organize-poll.test.ts +37 -0
  237. package/src/lib/organize/organize-poll.ts +28 -0
  238. package/src/lib/outbox-status.test.ts +64 -0
  239. package/src/lib/outbox-status.ts +75 -0
  240. package/src/lib/plate-serializer.ts +164 -0
  241. package/src/lib/provider-presets.test.ts +96 -0
  242. package/src/lib/provider-presets.ts +81 -0
  243. package/src/lib/query-error-handler.test.ts +144 -0
  244. package/src/lib/query-error-handler.ts +34 -0
  245. package/src/lib/query-escalation.integration.test.ts +138 -0
  246. package/src/lib/recent-searches.test.ts +53 -0
  247. package/src/lib/recent-searches.ts +34 -0
  248. package/src/lib/rescue-candidates.test.ts +94 -0
  249. package/src/lib/rescue-candidates.ts +35 -0
  250. package/src/lib/rescue-telemetry.test.ts +75 -0
  251. package/src/lib/rescue-telemetry.ts +55 -0
  252. package/src/lib/rum-adapter.test.ts +299 -0
  253. package/src/lib/rum-adapter.ts +107 -0
  254. package/src/lib/saved-searches.test.ts +80 -0
  255. package/src/lib/saved-searches.ts +52 -0
  256. package/src/lib/search-pending.test.ts +61 -0
  257. package/src/lib/search-pending.ts +24 -0
  258. package/src/lib/search-query.test.ts +35 -0
  259. package/src/lib/search-query.ts +11 -0
  260. package/src/lib/search-result.test.ts +160 -0
  261. package/src/lib/search-result.ts +102 -0
  262. package/src/lib/search-schemas.ts +34 -0
  263. package/src/lib/search-token-index.test.ts +112 -0
  264. package/src/lib/search-token-index.ts +56 -0
  265. package/src/lib/search-tokens.test.ts +218 -0
  266. package/src/lib/search-tokens.ts +133 -0
  267. package/src/lib/telemetry-context.tsx +9 -0
  268. package/src/lib/telemetry.ts +17 -0
  269. package/src/lib/theme-preference.ts +46 -0
  270. package/src/lib/theme.ts +33 -0
  271. package/src/lib/utils.ts +4 -0
  272. package/src/main.tsx +10 -0
  273. package/src/routeTree.gen.ts +392 -0
  274. package/src/router.test.ts +81 -0
  275. package/src/router.tsx +31 -0
  276. package/src/routes/__root.tsx +69 -0
  277. package/src/routes/index.tsx +7 -0
  278. package/src/routes/mail/$mailboxId.tsx +27 -0
  279. package/src/routes/mail/flagged.tsx +37 -0
  280. package/src/routes/mail/index.tsx +41 -0
  281. package/src/routes/mail/outbox.tsx +13 -0
  282. package/src/routes/mail.tsx +413 -0
  283. package/src/routes/onboarding.tsx +35 -0
  284. package/src/routes/settings/-accounts.test.ts +65 -0
  285. package/src/routes/settings/accounts.tsx +542 -0
  286. package/src/routes/settings/advanced.tsx +55 -0
  287. package/src/routes/settings/appearance.tsx +140 -0
  288. package/src/routes/settings/filters.tsx +141 -0
  289. package/src/routes/settings/folders.tsx +216 -0
  290. package/src/routes/settings/index.tsx +5 -0
  291. package/src/routes/settings/senders.tsx +466 -0
  292. package/src/routes/settings/suggested-vips.tsx +10 -0
  293. package/src/routes/settings.tsx +66 -0
  294. package/src/runtime-config.ts +75 -0
  295. package/src/shell/index.tsx +98 -0
  296. package/src/types/index.ts +73 -0
  297. package/src/vite-env.d.ts +4 -0
  298. package/vite.base.ts +43 -0
@@ -0,0 +1,279 @@
1
+ /**
2
+ * DraftsView — segmented Drafts folder view.
3
+ *
4
+ * Renders two labeled sections when the open mailbox is an account's IMAP
5
+ * \Drafts special-use folder (issue #505):
6
+ *
7
+ * 1. "Not yet sent (Remit)" — outbox rows with status === "draft" belonging
8
+ * to the account that owns the open \Drafts mailbox. Clicking a row opens
9
+ * compose pre-filled via openCompose({ mode: "new", outboxMessageId }).
10
+ *
11
+ * 2. "On the server" — IMAP \Drafts thread rows already loaded for the
12
+ * mailbox. Clicking a row opens the normal reading pane (read-only;
13
+ * editing IMAP drafts in compose is a known follow-up gap).
14
+ *
15
+ * Reuses the remit-ui ThreadSection / sectioned-list machinery that the Daily
16
+ * Brief also uses — sticky section headers, ComfortableRow / CompactRow with
17
+ * density. No new sectioned-list code is introduced here.
18
+ *
19
+ * Dedup note: Remit draft rows and IMAP \Drafts messages have no shared key
20
+ * today (no IMAP APPEND-as-draft path exists). Both sections render without
21
+ * client-side dedup; if appending Remit drafts into IMAP \Drafts ships in the
22
+ * future the dedup key would be the RFC822 Message-ID header (issue #505).
23
+ */
24
+ import {
25
+ outboxDetailOperationsDeleteOutboxMessageMutation,
26
+ outboxOperationsListOutboxMessagesOptions,
27
+ outboxOperationsListOutboxMessagesQueryKey,
28
+ } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
29
+ import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
30
+ import {
31
+ Avatar,
32
+ ComfortableRowTextContent,
33
+ cn,
34
+ comfortableRowClass,
35
+ type ThreadRowData,
36
+ type ThreadSection,
37
+ } from "@remit/ui";
38
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
39
+ import { useNavigate } from "@tanstack/react-router";
40
+ import { FileText, Inbox, Trash2 } from "lucide-react";
41
+ import { useMemo } from "react";
42
+ import { useCompose } from "@/components/compose/ComposeProvider";
43
+ import { groupDraftSections } from "@/lib/drafts";
44
+ import { NavMenuButton } from "./NavMenuButton";
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Section header (mirrors DailyBrief SectionHeader)
48
+ // ---------------------------------------------------------------------------
49
+
50
+ const SectionHeader = ({ label, count }: { label: string; count: number }) => (
51
+ <div className="sticky top-0 z-10 flex items-baseline justify-between border-b border-line bg-surface-sunken px-row-inset py-1">
52
+ <span className="text-2xs font-semibold uppercase tracking-wider text-fg-subtle">
53
+ {label}
54
+ </span>
55
+ <span className="text-2xs text-fg-subtle tabular-nums">{count}</span>
56
+ </div>
57
+ );
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Remit-draft row — opens compose for editing
61
+ // ---------------------------------------------------------------------------
62
+
63
+ interface RemitDraftRowProps {
64
+ row: ThreadRowData;
65
+ isSelected: boolean;
66
+ onOpen: (outboxMessageId: string) => void;
67
+ onDelete: (outboxMessageId: string) => void;
68
+ isDeleting: boolean;
69
+ }
70
+
71
+ const RemitDraftRow = ({
72
+ row,
73
+ isSelected,
74
+ onOpen,
75
+ onDelete,
76
+ isDeleting,
77
+ }: RemitDraftRowProps) => (
78
+ <button
79
+ type="button"
80
+ onClick={() => onOpen(row.id)}
81
+ className={cn("group", comfortableRowClass({ active: isSelected }))}
82
+ >
83
+ <FileText className="size-7 shrink-0 text-fg-muted mt-0.5" />
84
+ <ComfortableRowTextContent thread={row} />
85
+ <div className="flex items-center gap-1 shrink-0">
86
+ <button
87
+ type="button"
88
+ onClick={(e) => {
89
+ e.stopPropagation();
90
+ onDelete(row.id);
91
+ }}
92
+ disabled={isDeleting}
93
+ className="p-1.5 rounded-md text-fg-muted hover:text-danger hover:bg-surface-raised transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100"
94
+ title="Delete draft"
95
+ >
96
+ <Trash2 className="size-3.5" />
97
+ </button>
98
+ </div>
99
+ </button>
100
+ );
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // IMAP-draft row — opens reading pane (read-only)
104
+ // ---------------------------------------------------------------------------
105
+
106
+ interface ImapDraftRowProps {
107
+ row: ThreadRowData;
108
+ isSelected: boolean;
109
+ onOpen: (messageId: string) => void;
110
+ }
111
+
112
+ const ImapDraftRow = ({ row, isSelected, onOpen }: ImapDraftRowProps) => {
113
+ const unread = !row.isRead;
114
+ return (
115
+ <button
116
+ type="button"
117
+ onClick={() => onOpen(row.id)}
118
+ className={cn("group", comfortableRowClass({ active: isSelected }))}
119
+ >
120
+ {unread && (
121
+ <span className="absolute left-1.5 top-1/2 size-1.5 -translate-y-1/2 rounded-full bg-accent" />
122
+ )}
123
+ <Avatar name={row.fromName} email={row.fromEmail} size="sm" />
124
+ <ComfortableRowTextContent thread={row} />
125
+ </button>
126
+ );
127
+ };
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Main component
131
+ // ---------------------------------------------------------------------------
132
+
133
+ interface DraftsViewProps {
134
+ /** The mailbox id for the \Drafts mailbox being viewed. */
135
+ mailboxId: string;
136
+ /** The accountId that owns this \Drafts mailbox. */
137
+ accountId: string;
138
+ /** The currently selected message id (reading pane). */
139
+ selectedMessageId?: string;
140
+ /** IMAP thread rows already loaded for the \Drafts mailbox. */
141
+ imapThreads: RemitImapThreadMessageResponse[];
142
+ /** Mailbox display name (e.g. "Drafts"). */
143
+ title: string;
144
+ /** Total unread count for the header. */
145
+ unreadCount?: number;
146
+ }
147
+
148
+ export function DraftsView({
149
+ mailboxId,
150
+ accountId,
151
+ selectedMessageId,
152
+ imapThreads,
153
+ title,
154
+ unreadCount,
155
+ }: DraftsViewProps) {
156
+ const { openCompose, state: composeState } = useCompose();
157
+ const navigate = useNavigate();
158
+ const queryClient = useQueryClient();
159
+
160
+ // Fetch the full outbox list — both sources are already fetched by the
161
+ // client; this is a pure client-side composition (issue #505).
162
+ const { data: outboxResponse } = useQuery(
163
+ outboxOperationsListOutboxMessagesOptions(),
164
+ );
165
+
166
+ const sections = useMemo<ThreadSection[]>(() => {
167
+ return groupDraftSections({
168
+ outboxMessages: outboxResponse?.items ?? [],
169
+ accountId,
170
+ imapThreads,
171
+ });
172
+ }, [outboxResponse?.items, accountId, imapThreads]);
173
+
174
+ const deleteMutation = useMutation({
175
+ ...outboxDetailOperationsDeleteOutboxMessageMutation(),
176
+ onSuccess: () => {
177
+ queryClient.invalidateQueries({
178
+ queryKey: outboxOperationsListOutboxMessagesQueryKey(),
179
+ });
180
+ },
181
+ });
182
+
183
+ const handleRemitDraftDelete = (outboxMessageId: string) => {
184
+ deleteMutation.mutate({ path: { outboxMessageId } });
185
+ };
186
+
187
+ const handleRemitDraftOpen = (outboxMessageId: string) => {
188
+ // Clear any open IMAP draft first. The route's detailPane only renders
189
+ // FullCompose when `composeState.isOpen && !selectedThread`; if an IMAP
190
+ // draft is open (selectedMessageId set) the reading pane would keep
191
+ // showing ConversationView and compose would never surface (#505).
192
+ if (selectedMessageId) {
193
+ navigate({
194
+ to: "/mail/$mailboxId",
195
+ params: { mailboxId },
196
+ search: { selectedMessageId: undefined },
197
+ });
198
+ }
199
+ openCompose({ mode: "new", outboxMessageId });
200
+ };
201
+
202
+ const handleImapDraftOpen = (messageId: string) => {
203
+ navigate({
204
+ to: "/mail/$mailboxId",
205
+ params: { mailboxId },
206
+ search: { selectedMessageId: messageId },
207
+ });
208
+ };
209
+
210
+ const isEmpty = sections.length === 0;
211
+
212
+ return (
213
+ <section className="flex h-full w-full flex-col bg-surface">
214
+ {/* List datum bar */}
215
+ <header className="flex h-pane-header shrink-0 items-center gap-2 border-b border-line px-row-inset">
216
+ <NavMenuButton />
217
+ <h1 className="min-w-0 flex-1 truncate text-sm font-semibold text-fg">
218
+ {title}
219
+ </h1>
220
+ {(unreadCount ?? 0) > 0 && (
221
+ <span className="shrink-0 text-2xs text-fg-subtle tabular-nums">
222
+ {unreadCount} unread
223
+ </span>
224
+ )}
225
+ </header>
226
+
227
+ {/* Scrollable body */}
228
+ <div className="flex-1 overflow-y-auto">
229
+ {isEmpty ? (
230
+ <div className="flex flex-col items-center justify-center h-full gap-2 text-center px-4">
231
+ <Inbox className="size-8 text-fg-subtle" />
232
+ <p className="text-sm font-medium text-fg">No drafts</p>
233
+ <p className="text-xs text-fg-subtle">
234
+ Messages you start composing will appear here.
235
+ </p>
236
+ </div>
237
+ ) : (
238
+ sections.map((section) => (
239
+ <div key={section.id}>
240
+ {section.label && (
241
+ <SectionHeader
242
+ label={section.label}
243
+ count={section.threads.length}
244
+ />
245
+ )}
246
+ <div className="divide-y divide-line">
247
+ {section.threads.map((thread) => {
248
+ if (section.id === "remit-drafts") {
249
+ return (
250
+ <RemitDraftRow
251
+ key={thread.id}
252
+ row={thread}
253
+ isSelected={
254
+ composeState.isOpen &&
255
+ thread.id === composeState.outboxMessageId
256
+ }
257
+ onOpen={handleRemitDraftOpen}
258
+ onDelete={handleRemitDraftDelete}
259
+ isDeleting={deleteMutation.isPending}
260
+ />
261
+ );
262
+ }
263
+ return (
264
+ <ImapDraftRow
265
+ key={thread.id}
266
+ row={thread}
267
+ isSelected={thread.id === selectedMessageId}
268
+ onOpen={handleImapDraftOpen}
269
+ />
270
+ );
271
+ })}
272
+ </div>
273
+ </div>
274
+ ))
275
+ )}
276
+ </div>
277
+ </section>
278
+ );
279
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * FlaggedList — a FLAT, cross-account inbox of starred mail.
3
+ *
4
+ * Reads the same unified GET /threads source as the daily brief, keeps only
5
+ * starred rows (`toThreadRowData(...).starred`), and renders them as one
6
+ * continuous list (no category sections). The shared `MailViewChrome` owns the
7
+ * `MailHeader` + filter expando; the kit `MessageListPane` (flat, no
8
+ * `briefFilters`) owns the loading / empty / error chrome and keyboard hints,
9
+ * with a consumer-supplied `listBody` so the real rows render at every width.
10
+ */
11
+ import { unifiedThreadOperationsListAllThreadsOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
12
+ import {
13
+ ComfortableRow,
14
+ flaggedFilterConfig,
15
+ MessageListPane,
16
+ type ThreadRowData,
17
+ } from "@remit/ui";
18
+ import { useQuery } from "@tanstack/react-query";
19
+ import { useCallback, useMemo, useState } from "react";
20
+ import { formatErrorMessage } from "@/components/ui/ErrorState";
21
+ import { useIsDesktop } from "@/hooks/useMediaQuery";
22
+ import {
23
+ matchesBriefSearch,
24
+ matchesSearchTokens,
25
+ toThreadRowData,
26
+ } from "@/lib/brief";
27
+ import { buildBugReportContext, buildGitHubIssueUrl } from "@/lib/bug-report";
28
+ import { useMailContext } from "@/lib/mail-context";
29
+ import { rowToSearchResult } from "@/lib/search-result";
30
+ import { parseSearchTokens } from "@/lib/search-tokens";
31
+ import { MailViewChrome } from "./MailViewChrome";
32
+
33
+ const FILTER_PREDICATES: Record<string, (t: ThreadRowData) => boolean> = {
34
+ unread: (t) => !t.isRead,
35
+ attachment: (t) => t.hasAttachment === true,
36
+ };
37
+
38
+ interface FlaggedListProps {
39
+ selectedMessageId?: string;
40
+ onSelectMessage?: (id: string) => void;
41
+ }
42
+
43
+ export function FlaggedList({
44
+ selectedMessageId,
45
+ onSelectMessage,
46
+ }: FlaggedListProps) {
47
+ const { searchQuery, mailboxNameIndex, accountNameIndex } = useMailContext();
48
+ const isDesktop = useIsDesktop();
49
+
50
+ const [selectedCategory, setSelectedCategory] = useState("all");
51
+ const [activeFilters, setActiveFilters] = useState<ReadonlySet<string>>(
52
+ new Set(),
53
+ );
54
+
55
+ const toggleFilter = useCallback((id: string) => {
56
+ setActiveFilters((prev) => {
57
+ const next = new Set(prev);
58
+ if (next.has(id)) next.delete(id);
59
+ else next.add(id);
60
+ return next;
61
+ });
62
+ }, []);
63
+
64
+ const clearFilters = useCallback(() => {
65
+ setSelectedCategory("all");
66
+ setActiveFilters(new Set());
67
+ }, []);
68
+
69
+ const {
70
+ data: threadsData,
71
+ isLoading,
72
+ isError,
73
+ error,
74
+ refetch,
75
+ } = useQuery({
76
+ ...unifiedThreadOperationsListAllThreadsOptions(),
77
+ staleTime: 60_000,
78
+ });
79
+
80
+ const { freeText: sq, tokens: queryTokens } = parseSearchTokens(
81
+ searchQuery.trim().toLowerCase(),
82
+ { mailboxesByName: mailboxNameIndex, accountsByName: accountNameIndex },
83
+ );
84
+
85
+ const rows = useMemo<ThreadRowData[]>(() => {
86
+ const predicates = Array.from(activeFilters)
87
+ .map((id) => FILTER_PREDICATES[id])
88
+ .filter((p): p is (t: ThreadRowData) => boolean => p != null);
89
+ return (threadsData?.items ?? [])
90
+ .map(toThreadRowData)
91
+ .filter((t) => t.starred === true)
92
+ .filter(
93
+ (t) =>
94
+ (selectedCategory === "all" || t.category === selectedCategory) &&
95
+ predicates.every((p) => p(t)) &&
96
+ (!sq || matchesBriefSearch(t, sq)) &&
97
+ matchesSearchTokens(t, queryTokens),
98
+ );
99
+ }, [threadsData, selectedCategory, activeFilters, sq, queryTokens]);
100
+
101
+ const preset = useMemo(() => flaggedFilterConfig(), []);
102
+
103
+ const searchResults = useMemo(() => rows.map(rowToSearchResult), [rows]);
104
+
105
+ const unreadCount = useMemo(
106
+ () => rows.filter((t) => !t.isRead).length,
107
+ [rows],
108
+ );
109
+
110
+ const listState = isLoading
111
+ ? "loading"
112
+ : isError
113
+ ? "error"
114
+ : rows.length === 0
115
+ ? "empty"
116
+ : "ready";
117
+
118
+ const handleReportError = useCallback(() => {
119
+ const url = buildGitHubIssueUrl(buildBugReportContext());
120
+ window.open(url, "_blank", "noopener,noreferrer");
121
+ }, []);
122
+
123
+ const listBody = (
124
+ <div className="flex-1 overflow-y-auto">
125
+ <div className="divide-y divide-line">
126
+ {rows.map((thread) => (
127
+ <ComfortableRow
128
+ key={thread.id}
129
+ thread={thread}
130
+ active={thread.id === selectedMessageId}
131
+ onClick={() => onSelectMessage?.(thread.id)}
132
+ />
133
+ ))}
134
+ </div>
135
+ </div>
136
+ );
137
+
138
+ return (
139
+ <MailViewChrome
140
+ title="Flagged"
141
+ unreadCount={unreadCount}
142
+ preset={preset}
143
+ selectedCategory={selectedCategory}
144
+ activeFilters={activeFilters}
145
+ onSelectCategory={setSelectedCategory}
146
+ onToggleFilter={toggleFilter}
147
+ onClearFilters={clearFilters}
148
+ searchResults={searchResults}
149
+ searchLoading={isLoading}
150
+ onSelectSearchResult={(result) => onSelectMessage?.(result.id)}
151
+ >
152
+ <MessageListPane
153
+ listTitle="Flagged"
154
+ sections={[{ id: "flagged", threads: rows }]}
155
+ flatList
156
+ hideHeader
157
+ listState={listState}
158
+ searchQuery={sq ? searchQuery : undefined}
159
+ errorMessage={isError ? formatErrorMessage(error) : undefined}
160
+ onRetry={() => refetch()}
161
+ onReportError={handleReportError}
162
+ selectedThreadId={selectedMessageId}
163
+ onSelectThread={onSelectMessage}
164
+ onSelectBriefCategory={() => undefined}
165
+ isDesktop={isDesktop}
166
+ listBody={listState === "ready" ? listBody : undefined}
167
+ />
168
+ </MailViewChrome>
169
+ );
170
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * FlaggedPane — compound component for the Flagged virtual mailbox
3
+ * (/mail/flagged route).
4
+ *
5
+ * Mirrors BriefPane: it reads the unified cross-account thread list to resolve
6
+ * the open thread and owns the list / reading / phone slots. The list itself is
7
+ * a FLAT inbox of starred mail (see `FlaggedList`), not the sectioned brief.
8
+ *
9
+ * <FlaggedPane selectedMessageId={...}>
10
+ * <AppShellSlotted
11
+ * list={<FlaggedPane.List />}
12
+ * reading={<FlaggedPane.Reading />}
13
+ * />
14
+ * </FlaggedPane>
15
+ *
16
+ * On phone, use `<FlaggedPane.Phone />` instead.
17
+ */
18
+ import { unifiedThreadOperationsListAllThreadsOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
19
+ import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
20
+ import { ReadingPaneEmpty } from "@remit/ui";
21
+ import { useQuery } from "@tanstack/react-query";
22
+ import { useNavigate } from "@tanstack/react-router";
23
+ import {
24
+ createContext,
25
+ type ReactNode,
26
+ useCallback,
27
+ useContext,
28
+ useMemo,
29
+ } from "react";
30
+ import { Drawer } from "@/components/layout/Drawer";
31
+ import { ConversationView } from "@/components/mail/ConversationView";
32
+ import { FlaggedList } from "@/components/mail/FlaggedList";
33
+ import { IntelligencePane } from "@/components/mail/IntelligencePane";
34
+ import { MessageToolbar } from "@/components/mail/MessageToolbar";
35
+ import { useMailContext } from "@/lib/mail-context";
36
+
37
+ /* ------------------------------------------------------------------ */
38
+ /* Context */
39
+ /* ------------------------------------------------------------------ */
40
+
41
+ interface FlaggedPaneContextValue {
42
+ selectedMessageId: string | undefined;
43
+ selectedThread: RemitImapThreadMessageResponse | undefined;
44
+ onSelectMessage: (id: string) => void;
45
+ onCloseThread: () => void;
46
+ }
47
+
48
+ const FlaggedPaneCtx = createContext<FlaggedPaneContextValue | null>(null);
49
+
50
+ function useFlaggedPane(): FlaggedPaneContextValue {
51
+ const ctx = useContext(FlaggedPaneCtx);
52
+ if (!ctx) throw new Error("FlaggedPane.* must be used inside <FlaggedPane>");
53
+ return ctx;
54
+ }
55
+
56
+ /* ------------------------------------------------------------------ */
57
+ /* Provider */
58
+ /* ------------------------------------------------------------------ */
59
+
60
+ interface FlaggedPaneProps {
61
+ selectedMessageId: string | undefined;
62
+ children: ReactNode;
63
+ }
64
+
65
+ function FlaggedPaneProvider({
66
+ selectedMessageId,
67
+ children,
68
+ }: FlaggedPaneProps) {
69
+ const navigate = useNavigate();
70
+
71
+ const { data: threadsData } = useQuery({
72
+ ...unifiedThreadOperationsListAllThreadsOptions(),
73
+ staleTime: 60_000,
74
+ });
75
+
76
+ const selectedThread = useMemo(() => {
77
+ if (!selectedMessageId) return undefined;
78
+ return threadsData?.items.find((t) => t.messageId === selectedMessageId);
79
+ }, [threadsData, selectedMessageId]);
80
+
81
+ const handleSelectMessage = useCallback(
82
+ (id: string) => {
83
+ navigate({
84
+ to: "/mail/flagged",
85
+ search: (prev) => ({ ...prev, selectedMessageId: id }),
86
+ });
87
+ },
88
+ [navigate],
89
+ );
90
+
91
+ const handleCloseThread = useCallback(() => {
92
+ navigate({
93
+ to: "/mail/flagged",
94
+ search: (prev) => ({ ...prev, selectedMessageId: undefined }),
95
+ });
96
+ }, [navigate]);
97
+
98
+ const ctx: FlaggedPaneContextValue = {
99
+ selectedMessageId,
100
+ selectedThread,
101
+ onSelectMessage: handleSelectMessage,
102
+ onCloseThread: handleCloseThread,
103
+ };
104
+
105
+ return (
106
+ <FlaggedPaneCtx.Provider value={ctx}>{children}</FlaggedPaneCtx.Provider>
107
+ );
108
+ }
109
+
110
+ /* ------------------------------------------------------------------ */
111
+ /* Sub-views */
112
+ /* ------------------------------------------------------------------ */
113
+
114
+ /** Flat starred list. Mount in the `list` slot of `AppShellSlotted`. */
115
+ function FlaggedListSlot() {
116
+ const { selectedMessageId, onSelectMessage } = useFlaggedPane();
117
+ return (
118
+ <FlaggedList
119
+ selectedMessageId={selectedMessageId}
120
+ onSelectMessage={onSelectMessage}
121
+ />
122
+ );
123
+ }
124
+
125
+ /**
126
+ * Reading pane: toolbar + ConversationView.
127
+ * Mount in the `reading` slot of `AppShellSlotted`. Only rendered ≥ 1024px.
128
+ */
129
+ function FlaggedReading() {
130
+ const { selectedThread } = useFlaggedPane();
131
+ const {
132
+ onToggleIntelligence,
133
+ searchInput,
134
+ onSearchChange,
135
+ onSearchClear,
136
+ onSearchClearQuery,
137
+ } = useMailContext();
138
+
139
+ return (
140
+ <section className="flex h-full w-full min-w-0 flex-col bg-canvas">
141
+ <MessageToolbar
142
+ hasThread={Boolean(selectedThread)}
143
+ onCompose={() => undefined}
144
+ intelligenceOpen={false}
145
+ showIntelligenceToggle={false}
146
+ onToggleIntelligence={onToggleIntelligence}
147
+ searchValue={searchInput}
148
+ onSearchChange={onSearchChange}
149
+ onSearchClear={onSearchClear}
150
+ onSearchClearQuery={onSearchClearQuery}
151
+ />
152
+ <div className="min-h-0 flex-1 overflow-hidden">
153
+ {selectedThread ? (
154
+ <ConversationView
155
+ threadId={selectedThread.threadId}
156
+ mailboxId={selectedThread.mailboxId}
157
+ subject={selectedThread.subject}
158
+ authenticity={selectedThread.authenticity}
159
+ />
160
+ ) : (
161
+ <ReadingPaneEmpty />
162
+ )}
163
+ </div>
164
+ </section>
165
+ );
166
+ }
167
+
168
+ /** Phone view: ConversationView when a thread is open, else the flat list. */
169
+ function FlaggedPhone() {
170
+ const { selectedThread, onCloseThread } = useFlaggedPane();
171
+ const { intelligenceOpen, onToggleIntelligence } = useMailContext();
172
+
173
+ if (selectedThread) {
174
+ return (
175
+ <>
176
+ <ConversationView
177
+ threadId={selectedThread.threadId}
178
+ mailboxId={selectedThread.mailboxId}
179
+ subject={selectedThread.subject}
180
+ authenticity={selectedThread.authenticity}
181
+ onBack={onCloseThread}
182
+ onOpenIntelligence={onToggleIntelligence}
183
+ />
184
+ <Drawer
185
+ isOpen={intelligenceOpen}
186
+ onClose={onToggleIntelligence}
187
+ ariaLabel="Message details"
188
+ side="right"
189
+ >
190
+ <IntelligencePane
191
+ onClose={onToggleIntelligence}
192
+ thread={selectedThread}
193
+ hideCloseButton
194
+ />
195
+ </Drawer>
196
+ </>
197
+ );
198
+ }
199
+
200
+ return (
201
+ <div className="h-full">
202
+ <FlaggedListSlot />
203
+ </div>
204
+ );
205
+ }
206
+
207
+ /* ------------------------------------------------------------------ */
208
+ /* Compound component assembly */
209
+ /* ------------------------------------------------------------------ */
210
+
211
+ const FlaggedPane = Object.assign(FlaggedPaneProvider, {
212
+ List: FlaggedListSlot,
213
+ Reading: FlaggedReading,
214
+ Phone: FlaggedPhone,
215
+ });
216
+
217
+ export { FlaggedPane };