@remit/web-client 0.0.321 → 0.0.323

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 (190) hide show
  1. package/package.json +4 -4
  2. package/src/auth/SignInAgain.tsx +18 -0
  3. package/src/auth/amplify-config.test.ts +1 -107
  4. package/src/auth/auth-interceptor.test.ts +0 -6
  5. package/src/auth/cognito/cognito-token.test.ts +2 -4
  6. package/src/components/calendar/AgendaStrip.tsx +1 -1
  7. package/src/components/calendar/CalendarWorkspace.tsx +1 -1
  8. package/src/components/calendar/ChangeCalendarSuggestion.tsx +1 -1
  9. package/src/components/calendar/OpenCalendarEvent.tsx +1 -18
  10. package/src/components/compose/ComposeForm.tsx +11 -6
  11. package/src/components/compose/FullCompose.tsx +1 -1
  12. package/src/components/compose/MobileComposeSheet.tsx +1 -2
  13. package/src/components/compose/compose.stories.tsx +18 -0
  14. package/src/components/compose/draft-discard-failure.render.test.ts +138 -0
  15. package/src/components/config-import/config-import.fixtures.ts +8 -9
  16. package/src/components/config-import/config-import.stories.tsx +2 -2
  17. package/src/components/layout/MailTopBar.tsx +3 -0
  18. package/src/components/mail/BriefPane.tsx +0 -3
  19. package/src/components/mail/ConversationView.tsx +1 -3
  20. package/src/components/mail/DailyBrief.tsx +5 -28
  21. package/src/components/mail/FlaggedList.tsx +3 -4
  22. package/src/components/mail/FlaggedPane.tsx +0 -3
  23. package/src/components/mail/IntelligenceDrawer.tsx +0 -3
  24. package/src/components/mail/IntelligencePane.render.test.ts +0 -1
  25. package/src/components/mail/IntelligencePane.test.ts +1 -95
  26. package/src/components/mail/IntelligencePane.tsx +3 -93
  27. package/src/components/mail/MailListHeader.tsx +12 -43
  28. package/src/components/mail/MailNav.tsx +6 -2
  29. package/src/components/mail/MailViewChrome.tsx +1 -11
  30. package/src/components/mail/MailboxPane.tsx +4 -34
  31. package/src/components/mail/MessageActionMenu.tsx +6 -6
  32. package/src/components/mail/MessageAttachments.tsx +35 -0
  33. package/src/components/mail/MessageBody.tsx +1 -0
  34. package/src/components/mail/MessageBodyErrorBanner.test.ts +0 -30
  35. package/src/components/mail/MessageCard.tsx +16 -4
  36. package/src/components/mail/MessageList.run-reporting.test.ts +2 -46
  37. package/src/components/mail/MessageList.tsx +23 -11
  38. package/src/components/mail/MessageRow.render.test.ts +0 -7
  39. package/src/components/mail/MoveToTrigger.tsx +1 -1
  40. package/src/components/mail/RawMessageView.tsx +1 -1
  41. package/src/components/mail/SelectionWizardHost.search-entry.test.ts +0 -1
  42. package/src/components/mail/SelectionWizardHost.tsx +16 -102
  43. package/src/components/mail/authenticity-warning.stories.tsx +0 -21
  44. package/src/components/mail/daily-brief.stories.tsx +31 -17
  45. package/src/components/mail/enter-cursor-late-open.render.test.ts +197 -0
  46. package/src/components/mail/intelligence-drawer.stories.tsx +0 -2
  47. package/src/components/mail/message-gone.stories.tsx +44 -0
  48. package/src/components/mail/offer-times-reply.render.test.ts +25 -1
  49. package/src/components/mail/reclassify-remove-override.render.test.ts +2 -6
  50. package/src/components/overlay-escape.render.test.ts +7 -3
  51. package/src/components/settings/AccountFormPanel.render.test.ts +26 -3
  52. package/src/components/settings/AccountFormPanel.test.ts +1 -36
  53. package/src/components/settings/AccountFormPanel.tsx +5 -25
  54. package/src/components/settings/AccountServices.tsx +1 -1
  55. package/src/components/settings/AdvancedNavIcon.tsx +3 -3
  56. package/src/components/settings/CalendarFeedCard.tsx +2 -1
  57. package/src/components/settings/CalendarSubscribeCard.tsx +1 -1
  58. package/src/components/settings/CalendarSubscriptionStatus.tsx +1 -2
  59. package/src/components/settings/FilterActionError.tsx +1 -1
  60. package/src/components/settings/FilterEditor.render.test.ts +3 -27
  61. package/src/components/settings/FilterEditor.tsx +4 -24
  62. package/src/components/settings/FilterEditorSurface.tsx +0 -3
  63. package/src/components/settings/FiltersList.render.test.ts +5 -24
  64. package/src/components/settings/FiltersList.stories.tsx +0 -1
  65. package/src/components/settings/FiltersList.tsx +10 -35
  66. package/src/components/settings/WarningsNavIcon.tsx +12 -0
  67. package/src/components/settings/{QuarantinePanel.delimiter.test.ts → WarningsPanel.dom.test.ts} +64 -5
  68. package/src/components/settings/WarningsPanel.render.test.ts +56 -0
  69. package/src/components/settings/WarningsPanel.tsx +244 -0
  70. package/src/components/settings/accounts.stories.tsx +18 -0
  71. package/src/components/settings/advanced.stories.tsx +3 -125
  72. package/src/components/settings/settings-filter.stories.tsx +2 -42
  73. package/src/components/settings/warnings.stories.tsx +290 -0
  74. package/src/components/ui/ErrorBannerProvider.tsx +5 -3
  75. package/src/components/ui/error-banners.test.ts +1 -22
  76. package/src/components/ui/error-banners.ts +9 -20
  77. package/src/hooks/calendar/useCalendarCollectionWrites.ts +2 -1
  78. package/src/hooks/calendar/useCalendarSuggestions.ts +2 -1
  79. package/src/hooks/calendar/useCalendarWrites.ts +2 -1
  80. package/src/hooks/intelligence-calendar.render.test.ts +0 -1
  81. package/src/hooks/useDeleteMessages.test.ts +1 -42
  82. package/src/hooks/useFilters.test.ts +0 -8
  83. package/src/hooks/useFollowFocusOpen.render.test.ts +1 -11
  84. package/src/hooks/useIntelligenceData.test.ts +9 -47
  85. package/src/hooks/useIntelligenceData.ts +7 -100
  86. package/src/hooks/useLayoutTier.test.ts +0 -48
  87. package/src/hooks/useReportSpam.ts +1 -1
  88. package/src/hooks/useRescueCandidates.test.ts +0 -23
  89. package/src/hooks/useRulePreview.ts +8 -7
  90. package/src/hooks/useSearchTokenContext.ts +2 -3
  91. package/src/hooks/useStaleAccountSync.test.ts +0 -17
  92. package/src/hooks/useStarredThreads.test.ts +0 -10
  93. package/src/hooks/useTriggerSync.test.ts +0 -6
  94. package/src/hooks/useVisualViewport.render.test.ts +63 -0
  95. package/src/hooks/useWarningsAttention.ts +37 -0
  96. package/src/lib/account-order.test.ts +0 -10
  97. package/src/lib/adjacent-message.test.ts +0 -9
  98. package/src/lib/app-info.ts +0 -3
  99. package/src/lib/autodiscovery.test.ts +0 -47
  100. package/src/lib/bug-report.test.ts +38 -0
  101. package/src/lib/bug-report.ts +10 -7
  102. package/src/lib/bulk-action-copy.ts +1 -1
  103. package/src/lib/calendar-route.test.ts +0 -5
  104. package/src/lib/config-import.test.ts +3 -39
  105. package/src/lib/dedupe-thread-messages.test.ts +0 -4
  106. package/src/lib/desktop-breakpoint.test.ts +0 -8
  107. package/src/lib/drafts.test.ts +0 -23
  108. package/src/lib/fatal-error.test.ts +0 -19
  109. package/src/lib/folder-roles.test.ts +0 -4
  110. package/src/lib/hot-sync-window.test.ts +0 -5
  111. package/src/lib/list-header-chrome.ts +1 -1
  112. package/src/lib/mail-context.ts +1 -1
  113. package/src/lib/message-attachments.test.ts +77 -0
  114. package/src/lib/message-attachments.ts +31 -15
  115. package/src/lib/message-body-source.test.ts +21 -4
  116. package/src/lib/message-body-source.ts +5 -2
  117. package/src/lib/onboarding-completion.test.ts +0 -16
  118. package/src/lib/organize/filter-edit-model.test.ts +2 -32
  119. package/src/lib/organize/filter-edit-model.ts +6 -16
  120. package/src/lib/organize/organize-model.test.ts +7 -40
  121. package/src/lib/organize/organize-model.ts +16 -18
  122. package/src/lib/organize/rule-model.test.ts +24 -202
  123. package/src/lib/organize/rule-model.ts +19 -166
  124. package/src/lib/organize/search-to-rule.test.ts +2 -36
  125. package/src/lib/organize/search-to-rule.ts +1 -16
  126. package/src/lib/provider-presets.test.ts +1 -77
  127. package/src/lib/query-error-handler.test.ts +0 -61
  128. package/src/lib/query-escalation.integration.test.ts +4 -4
  129. package/src/lib/result-count.ts +1 -1
  130. package/src/lib/result-folder.ts +1 -1
  131. package/src/lib/rum-adapter.test.ts +0 -104
  132. package/src/lib/search-query.test.ts +0 -11
  133. package/src/lib/search-result.test.ts +2 -142
  134. package/src/lib/search-result.ts +3 -73
  135. package/src/lib/search-scope.test.ts +1 -92
  136. package/src/lib/search-scope.ts +0 -29
  137. package/src/lib/search-surface.ts +2 -2
  138. package/src/lib/search-token-index.test.ts +0 -8
  139. package/src/lib/search-tokens.ts +15 -16
  140. package/src/lib/starred-rows.test.ts +0 -4
  141. package/src/lib/sync-warning-entries.ts +33 -0
  142. package/src/lib/wizard-selection.ts +1 -1
  143. package/src/mocks/story-frame/mail-handlers.ts +12 -8
  144. package/src/mocks/story-frame/mail-world.ts +5 -0
  145. package/src/routeTree.gen.ts +21 -0
  146. package/src/routes/calendar/$view.$date/index.tsx +1 -1
  147. package/src/routes/calendar.tsx +2 -1
  148. package/src/routes/mail/brief.tsx +1 -1
  149. package/src/routes/mail/flagged.tsx +1 -1
  150. package/src/routes/mail.tsx +5 -3
  151. package/src/routes/settings/-accounts.test.ts +0 -5
  152. package/src/routes/settings/-filters.test.ts +0 -1
  153. package/src/routes/settings/-shared/help-copy.tsx +15 -0
  154. package/src/routes/settings/accounts.tsx +1 -1
  155. package/src/routes/settings/advanced.tsx +1 -4
  156. package/src/routes/settings/filters.tsx +1 -2
  157. package/src/routes/settings/folders.tsx +1 -1
  158. package/src/routes/settings/labels.tsx +1 -1
  159. package/src/routes/settings/senders.tsx +1 -1
  160. package/src/routes/settings/warnings.tsx +36 -0
  161. package/src/routes/settings.tsx +7 -0
  162. package/src/routing/wizard-history.test.ts +1 -45
  163. package/src/runtime-config.ts +5 -0
  164. package/src/test-support/fixtures.ts +1 -1
  165. package/src/components/compose/draft-discard-banner.test.ts +0 -76
  166. package/src/components/mail/ReadingPaneEmpty.render.test.ts +0 -42
  167. package/src/components/mail/SpamRescue.render.test.ts +0 -35
  168. package/src/components/mail/intelligence-semantic-off.render.test.ts +0 -111
  169. package/src/components/mail/selection-surface-rollcall.test.ts +0 -270
  170. package/src/components/settings/QuarantinePanel.render.test.ts +0 -71
  171. package/src/components/settings/QuarantinePanel.tsx +0 -178
  172. package/src/components/ui/DropdownMenu.tsx +0 -99
  173. package/src/components/ui/EmptyState.tsx +0 -11
  174. package/src/components/ui/ErrorBanner.render.test.ts +0 -104
  175. package/src/components/ui/ErrorBanner.stories.tsx +0 -132
  176. package/src/components/ui/ErrorBanner.tsx +0 -106
  177. package/src/components/ui/ErrorBannerStack.tsx +0 -35
  178. package/src/components/ui/ErrorState.render.test.ts +0 -82
  179. package/src/components/ui/ErrorState.tsx +0 -80
  180. package/src/components/ui/KeyboardShortcutsModal.tsx +0 -113
  181. package/src/hooks/useMoveMessages.test.ts +0 -63
  182. package/src/hooks/useOrganizeWiden.test.ts +0 -217
  183. package/src/hooks/useOrganizeWiden.ts +0 -112
  184. package/src/hooks/useSemanticSearch.ts +0 -132
  185. package/src/hooks/useSemanticSearchEnabled.ts +0 -23
  186. package/src/hooks/useVisualViewport.test.ts +0 -126
  187. package/src/lib/organize/sender-fallback.test.ts +0 -52
  188. package/src/lib/organize/sender-fallback.ts +0 -38
  189. package/src/lib/rescue-telemetry.test.ts +0 -75
  190. package/src/router.test.ts +0 -81
@@ -13,7 +13,6 @@ import {
13
13
  SEARCH_SCOPE_CHIP_ID,
14
14
  scopeLabelForMailboxName,
15
15
  searchScopeForRoute,
16
- semanticMailboxScope,
17
16
  } from "./search-scope";
18
17
 
19
18
  const listFor: Record<string, BrowsedList["list"]> = {
@@ -117,97 +116,7 @@ describe("scopeLabelForMailboxName", () => {
117
116
  });
118
117
 
119
118
  // The invariant: no chip means global, a chip means every engine on that route
120
- // respects it. The semantic engine used to run unscoped inside a mailbox under
121
- // an "Everywhere" heading, contradicting the `in:` chip the same bar showed.
122
- describe("semanticMailboxScope — a chip binds every engine on the route", () => {
123
- const mailbox = (mailboxId: string): BrowsedList => ({
124
- list: "mailbox",
125
- mailboxId,
126
- });
127
-
128
- it("pins a mailbox route to its own mailbox", () => {
129
- assert.equal(
130
- semanticMailboxScope({ browsed: mailbox("mbx-spam") }),
131
- "mbx-spam",
132
- );
133
- });
134
-
135
- it("a mailbox route ignores a caller asking for somewhere else", () => {
136
- assert.equal(
137
- semanticMailboxScope({
138
- browsed: mailbox("mbx-spam"),
139
- callerMailboxId: "mbx-archive",
140
- }),
141
- "mbx-spam",
142
- );
143
- });
144
-
145
- it("a mailbox route ignores a typed in: term", () => {
146
- assert.equal(
147
- semanticMailboxScope({
148
- browsed: mailbox("mbx-spam"),
149
- inTokenMailboxId: "mbx-archive",
150
- }),
151
- "mbx-spam",
152
- );
153
- });
154
-
155
- it("a mailbox route never widens to global", () => {
156
- const scope = semanticMailboxScope({ browsed: mailbox("mbx-spam") });
157
- assert.notEqual(scope, undefined, "a scoped route must carry a scope");
158
- });
159
-
160
- // Flagged and outbox are scoped to a collection, which the mailbox-scoped
161
- // semantic API cannot express — so neither runs a semantic section, and
162
- // neither may fall back to a typed `in:` under its own chip.
163
- for (const routeId of [MAIL_FLAGGED_ROUTE_ID, MAIL_OUTBOX_ROUTE_ID]) {
164
- it(`${routeId} never honours a typed in: term`, () => {
165
- assert.equal(
166
- semanticMailboxScope({
167
- browsed: browsing(routeId),
168
- inTokenMailboxId: "mbx-archive",
169
- }),
170
- undefined,
171
- );
172
- });
173
- }
174
-
175
- it("the daily brief is global — no route scope, no chip", () => {
176
- assert.equal(
177
- semanticMailboxScope({ browsed: browsing(MAIL_BRIEF_ROUTE_ID) }),
178
- undefined,
179
- );
180
- });
181
-
182
- it("the daily brief is the one route a typed in: narrows", () => {
183
- assert.equal(
184
- semanticMailboxScope({
185
- browsed: browsing(MAIL_BRIEF_ROUTE_ID),
186
- inTokenMailboxId: "mbx-archive",
187
- }),
188
- "mbx-archive",
189
- );
190
- });
191
-
192
- it("every scoped route resolves a scope or refuses to widen", () => {
193
- // Pins the invariant across the route table rather than one case at a
194
- // time: on a scoped route the resolved scope is either the route's own
195
- // mailbox, or nothing that came from the typed query.
196
- const scoped = [
197
- { browsed: mailbox("mbx-spam"), expected: "mbx-spam" },
198
- { browsed: browsing(MAIL_FLAGGED_ROUTE_ID), expected: undefined },
199
- { browsed: browsing(MAIL_OUTBOX_ROUTE_ID), expected: undefined },
200
- ];
201
- for (const { browsed: m, expected } of scoped) {
202
- assert.equal(isScopedRoute(m), true);
203
- assert.equal(
204
- semanticMailboxScope({ browsed: m, inTokenMailboxId: "mbx-elsewhere" }),
205
- expected,
206
- );
207
- }
208
- });
209
- });
210
-
119
+ // respects it.
211
120
  describe("resultsScopeForRoute", () => {
212
121
  it("passes the global search through as global", () => {
213
122
  assert.deepEqual(
@@ -75,35 +75,6 @@ export function routeMailboxId(browsed: BrowsedList): string | undefined {
75
75
  return browsed.list === "mailbox" ? browsed.mailboxId : undefined;
76
76
  }
77
77
 
78
- /**
79
- * The mailbox every engine on the active route must be pinned to.
80
- *
81
- * No chip means global; a chip means nothing on the route reaches past it. The
82
- * semantic engine used to run unscoped inside a mailbox under an "Everywhere"
83
- * heading, which contradicted the `in:` chip the same bar was showing, so the
84
- * scope is now resolved from the route here rather than left to each caller:
85
- *
86
- * - A mailbox route pins to its own mailbox. It beats both the caller's
87
- * argument and a typed `in:`, so no call site can widen or redirect it.
88
- * - Any other scoped route (flagged, outbox) never falls back to a typed
89
- * `in:`. Its scope is a collection rather than a folder, which the semantic
90
- * API cannot express — which is why neither view runs a semantic section
91
- * today. Honouring `in:` there would be a search reaching past a chip that
92
- * promises otherwise.
93
- * - An unscoped route (the daily brief) is global unless a typed `in:` narrows
94
- * it, which is the one place `useSearchTokenContext` resolves that term.
95
- */
96
- export function semanticMailboxScope(args: {
97
- browsed: BrowsedList;
98
- callerMailboxId?: string;
99
- inTokenMailboxId?: string;
100
- }): string | undefined {
101
- const fromRoute = routeMailboxId(args.browsed);
102
- if (fromRoute) return fromRoute;
103
- if (isScopedRoute(args.browsed)) return args.callerMailboxId;
104
- return args.callerMailboxId ?? args.inTokenMailboxId;
105
- }
106
-
107
78
  /**
108
79
  * The scope of the active route.
109
80
  *
@@ -3,8 +3,8 @@ import type { LayoutTier } from "@/hooks/useLayoutTier";
3
3
  /**
4
4
  * Which surface a list view shows its search on. Two surfaces exist:
5
5
  *
6
- * - the read-only two-engine `SearchResults` panel (literal "Top matches" +
7
- * semantic "Related", cross-folder, spam offer), swapped into the list-pane
6
+ * - the read-only `SearchResults` panel ("Top matches", cross-folder, spam
7
+ * offer), swapped into the list-pane
8
8
  * body on tablet/desktop while searching; and
9
9
  * - the view's own body — for the mailbox route a selectable `MessageList` whose
10
10
  * threads filter to the committed search — which hosts multi-select and the
@@ -69,10 +69,6 @@ describe("buildAccountNameIndex", () => {
69
69
  ]);
70
70
  assert.equal(index.get("work"), "acct-1");
71
71
  });
72
-
73
- it("empty accounts yields an empty index", () => {
74
- assert.deepEqual(buildAccountNameIndex([]), new Map());
75
- });
76
72
  });
77
73
 
78
74
  describe("buildMailboxNameIndex", () => {
@@ -107,10 +103,6 @@ describe("buildMailboxNameIndex", () => {
107
103
  ]);
108
104
  assert.equal(index.size, 0);
109
105
  });
110
-
111
- it("empty input yields an empty index", () => {
112
- assert.deepEqual(buildMailboxNameIndex([]), new Map());
113
- });
114
106
  });
115
107
 
116
108
  describe("buildMailboxSuggestionValues", () => {
@@ -4,20 +4,20 @@
4
4
  * into removable chips. Only tokens the search APIs can actually honor are
5
5
  * recognized — everything else is left untouched as ordinary free-text words.
6
6
  *
7
- * The vocabulary is the search parameters `threadOperationsSearchThreads` and
8
- * `semanticSearchOperationsSemanticSearch` take, said in words:
7
+ * The vocabulary is the search parameters `threadOperationsSearchThreads`
8
+ * takes, said in words:
9
9
  *
10
- * | token | thread search | semantic search |
11
- * | ---------------------- | ------------------ | ---------------------- |
12
- * | `from:` | `from` | — |
13
- * | `subject:` | `subject` | — |
14
- * | `category:` | `category[]` | `category` |
15
- * | `is:unread` / `is:read`| `unread` | `isRead` |
16
- * | `is:starred` | `starred` | `hasStars` |
17
- * | `has:attachment` | `attachments` | `hasAttachment` |
18
- * | `before:` / `after:` | — | `sentDateTo` / `From` |
19
- * | `in:` | route scope | `mailboxId` |
20
- * | `account:` | — | — |
10
+ * | token | thread search |
11
+ * | ---------------------- | ------------------ |
12
+ * | `from:` | `from` |
13
+ * | `subject:` | `subject` |
14
+ * | `category:` | `category[]` |
15
+ * | `is:unread` / `is:read`| `unread` |
16
+ * | `is:starred` | `starred` |
17
+ * | `has:attachment` | `attachments` |
18
+ * | `before:` / `after:` | — |
19
+ * | `in:` | route scope |
20
+ * | `account:` | — |
21
21
  *
22
22
  * A token an engine has no parameter for is applied client-side over the rows
23
23
  * that engine returned (`matchesSearchTokens` in `lib/brief.ts`), never dropped.
@@ -35,9 +35,8 @@
35
35
  *
36
36
  * Pure functions only: no React, no fetch — the name index is injected, not
37
37
  * fetched here. `MailListHeader` renders the chips and the callers that issue
38
- * search requests (`MailboxPane`, `DailyBrief`, `FlaggedList`,
39
- * `useSemanticSearch`) apply the parsed tokens to the params each engine's API
40
- * actually supports.
38
+ * search requests (`MailboxPane`, `DailyBrief`, `FlaggedList`) apply the
39
+ * parsed tokens to the params each engine's API actually supports.
41
40
  */
42
41
 
43
42
  import type { RemitImapMessageCategory } from "@remit/api-http-client/types.gen.ts";
@@ -64,8 +64,4 @@ describe("dedupeByThread", () => {
64
64
  ["t1", "t2", "t3"],
65
65
  );
66
66
  });
67
-
68
- test("an empty list stays empty", () => {
69
- assert.deepEqual(dedupeByThread([]), []);
70
- });
71
67
  });
@@ -0,0 +1,33 @@
1
+ import type { RemitImapSyncWarningResponse } from "@remit/api-http-client/types.gen.ts";
2
+ import { syncWarningState } from "@remit/data-ports/sync-warning-state";
3
+ import {
4
+ canonicalRoleLabel,
5
+ folderLeaf,
6
+ type ResultFolder,
7
+ type SyncWarningEntry,
8
+ } from "@remit/ui";
9
+
10
+ export const UNLISTED_FOLDER_LABEL = "A folder no longer listed";
11
+
12
+ function folderLabel(folder: ResultFolder | undefined): string {
13
+ if (folder?.role) return canonicalRoleLabel(folder.role);
14
+ if (folder?.providerPath) {
15
+ return folderLeaf(folder.providerPath, folder.hierarchyDelimiter);
16
+ }
17
+ return UNLISTED_FOLDER_LABEL;
18
+ }
19
+
20
+ export function toSyncWarningEntry(
21
+ wire: RemitImapSyncWarningResponse,
22
+ folder: ResultFolder | undefined,
23
+ ): SyncWarningEntry {
24
+ return {
25
+ syncWarningId: wire.syncWarningId,
26
+ uid: wire.uid,
27
+ stage: wire.stage,
28
+ state: syncWarningState(wire.stage, wire.attempts),
29
+ attempts: wire.attempts,
30
+ createdAt: wire.createdAt,
31
+ folderLabel: folderLabel(folder),
32
+ };
33
+ }
@@ -2,7 +2,7 @@ import type { ThreadRowData, WizardMessage } from "@remit/ui";
2
2
 
3
3
  /** A ticked row, as the wizard's samples and its clause prefill read it. */
4
4
  export interface WizardSelectionMessage extends WizardMessage {
5
- /** Sender address — the widen's literal fallback and the prefill match on it. */
5
+ /** Sender address — the properties prefill matches on it. */
6
6
  email: string;
7
7
  /**
8
8
  * Owning account, which a bulk run splits its batches by (#872) — the bulk
@@ -27,7 +27,6 @@ import type {
27
27
  RemitImapThreadMessageResponse,
28
28
  RemitImapThreadSearchResponse,
29
29
  RemitImapUpdateOutboxMessageInput,
30
- SemanticSearchOperationsSemanticSearchResponse,
31
30
  SyncOperationsGetSyncStatusResponse,
32
31
  SyncOperationsTriggerSyncResponse,
33
32
  SystemOperationsGetSystemUpdateResponse,
@@ -46,6 +45,7 @@ export interface MailHandlerOptions {
46
45
  pageSize?: number;
47
46
  holdLaterPages?: boolean;
48
47
  draftSave?: "saves" | "refused";
48
+ draftDiscard?: "discards" | "refused";
49
49
  send?: "sends" | "holds";
50
50
  search?: "answers" | "holds";
51
51
  config?: "answers" | "holds";
@@ -64,6 +64,7 @@ const bodyPartsOf = (
64
64
  ? [
65
65
  {
66
66
  bodyPartId: `part-${row.messageId}`,
67
+ partPath: "1",
67
68
  mediaType: "APPLICATION",
68
69
  mediaSubtype: "pdf",
69
70
  sizeOctets: 48_000,
@@ -76,6 +77,7 @@ const bodyPartsOf = (
76
77
  : [
77
78
  {
78
79
  bodyPartId: `part-${row.messageId}`,
80
+ partPath: "1",
79
81
  mediaType: "TEXT",
80
82
  mediaSubtype: "plain",
81
83
  sizeOctets: (row.snippet ?? "").length,
@@ -215,6 +217,12 @@ export const mailHandlers = (
215
217
  { status: 409 },
216
218
  );
217
219
 
220
+ const refuseDiscard = () =>
221
+ HttpResponse.json(
222
+ { status: 409, message: "The draft is already gone." },
223
+ { status: 409 },
224
+ );
225
+
218
226
  return [
219
227
  http.get(`${API}/config`, () =>
220
228
  options.config === "holds"
@@ -485,6 +493,7 @@ export const mailHandlers = (
485
493
  return HttpResponse.json<RemitImapOutboxMessageResponse>(queued);
486
494
  }),
487
495
  http.delete(`${API}/outbox/:outboxMessageId`, ({ params }) => {
496
+ if (options.draftDiscard === "refused") return refuseDiscard();
488
497
  outbox.delete(String(params.outboxMessageId));
489
498
  return new HttpResponse(null, { status: 204 });
490
499
  }),
@@ -517,16 +526,11 @@ export const mailHandlers = (
517
526
  { items: [] },
518
527
  ),
519
528
  ),
520
- http.get(`${API}/search/semantic`, () =>
521
- options.search === "holds"
522
- ? pending()
523
- : HttpResponse.json<SemanticSearchOperationsSemanticSearchResponse>({
524
- items: [],
525
- }),
526
- ),
527
529
  http.get(`${API}/me/quarantine`, () =>
528
530
  HttpResponse.json<MeOperationsListQuarantineResponse>({
529
531
  entries: world.quarantine,
532
+ warnings: world.syncWarnings,
533
+ warningsTotal: world.syncWarningsTotal,
530
534
  }),
531
535
  ),
532
536
  http.get(`${API}/me/vip-suggestions`, () =>
@@ -10,6 +10,7 @@ import type {
10
10
  RemitImapOutboxMessageResponse,
11
11
  RemitImapOutboxMessageStatus,
12
12
  RemitImapQuarantineResponse,
13
+ RemitImapSyncWarningResponse,
13
14
  RemitImapSystemUpdateResponse,
14
15
  RemitImapThreadMessageResponse,
15
16
  RemitImapVipSuggestionEntry,
@@ -26,6 +27,8 @@ export interface MailWorld {
26
27
  threads: RemitImapThreadMessageResponse[];
27
28
  outbox: RemitImapOutboxMessageResponse[];
28
29
  quarantine: RemitImapQuarantineResponse[];
30
+ syncWarnings: RemitImapSyncWarningResponse[];
31
+ syncWarningsTotal: number;
29
32
  vipSuggestions: RemitImapVipSuggestionEntry[];
30
33
  addresses: RemitImapAddressResponse[];
31
34
  systemUpdate: RemitImapSystemUpdateResponse;
@@ -427,6 +430,8 @@ export const mailWorld = (overrides: Partial<MailWorld> = {}): MailWorld => ({
427
430
  threads: [...inboxThreads, ...serverDrafts],
428
431
  outbox: [...remitDrafts, ...outboxInEveryStatus],
429
432
  quarantine: [],
433
+ syncWarnings: [],
434
+ syncWarningsTotal: 0,
430
435
  vipSuggestions: [],
431
436
  addresses: [],
432
437
  systemUpdate: {
@@ -31,6 +31,7 @@ import { Route as SettingsFoldersRouteImport } from './routes/settings/folders'
31
31
  import { Route as SettingsLabelsRouteImport } from './routes/settings/labels'
32
32
  import { Route as SettingsSendersRouteImport } from './routes/settings/senders'
33
33
  import { Route as SettingsSuggestedVipsRouteImport } from './routes/settings/suggested-vips'
34
+ import { Route as SettingsWarningsRouteImport } from './routes/settings/warnings'
34
35
  import { Route as CalendarViewDateRouteImport } from './routes/calendar/$view.$date'
35
36
  import { Route as MailMailboxIdIndexRouteImport } from './routes/mail/$mailboxId/index'
36
37
  import { Route as MailMailboxIdThreadIdRouteImport } from './routes/mail/$mailboxId/$threadId'
@@ -170,6 +171,11 @@ const SettingsSuggestedVipsRoute = SettingsSuggestedVipsRouteImport.update({
170
171
  path: '/suggested-vips',
171
172
  getParentRoute: () => SettingsRoute,
172
173
  } as any)
174
+ const SettingsWarningsRoute = SettingsWarningsRouteImport.update({
175
+ id: '/warnings',
176
+ path: '/warnings',
177
+ getParentRoute: () => SettingsRoute,
178
+ } as any)
173
179
  const CalendarViewDateRoute = CalendarViewDateRouteImport.update({
174
180
  id: '/$view/$date',
175
181
  path: '/$view/$date',
@@ -354,6 +360,7 @@ export interface FileRoutesByFullPath {
354
360
  '/settings/labels': typeof SettingsLabelsRoute
355
361
  '/settings/senders': typeof SettingsSendersRoute
356
362
  '/settings/suggested-vips': typeof SettingsSuggestedVipsRoute
363
+ '/settings/warnings': typeof SettingsWarningsRoute
357
364
  '/calendar/': typeof CalendarIndexRoute
358
365
  '/mail/': typeof MailIndexRoute
359
366
  '/settings/': typeof SettingsIndexRoute
@@ -399,6 +406,7 @@ export interface FileRoutesByTo {
399
406
  '/settings/labels': typeof SettingsLabelsRoute
400
407
  '/settings/senders': typeof SettingsSendersRoute
401
408
  '/settings/suggested-vips': typeof SettingsSuggestedVipsRoute
409
+ '/settings/warnings': typeof SettingsWarningsRoute
402
410
  '/calendar': typeof CalendarIndexRoute
403
411
  '/mail': typeof MailIndexRoute
404
412
  '/settings': typeof SettingsIndexRoute
@@ -447,6 +455,7 @@ export interface FileRoutesById {
447
455
  '/settings/labels': typeof SettingsLabelsRoute
448
456
  '/settings/senders': typeof SettingsSendersRoute
449
457
  '/settings/suggested-vips': typeof SettingsSuggestedVipsRoute
458
+ '/settings/warnings': typeof SettingsWarningsRoute
450
459
  '/calendar/': typeof CalendarIndexRoute
451
460
  '/mail/': typeof MailIndexRoute
452
461
  '/settings/': typeof SettingsIndexRoute
@@ -501,6 +510,7 @@ export interface FileRouteTypes {
501
510
  | '/settings/labels'
502
511
  | '/settings/senders'
503
512
  | '/settings/suggested-vips'
513
+ | '/settings/warnings'
504
514
  | '/calendar/'
505
515
  | '/mail/'
506
516
  | '/settings/'
@@ -546,6 +556,7 @@ export interface FileRouteTypes {
546
556
  | '/settings/labels'
547
557
  | '/settings/senders'
548
558
  | '/settings/suggested-vips'
559
+ | '/settings/warnings'
549
560
  | '/calendar'
550
561
  | '/mail'
551
562
  | '/settings'
@@ -593,6 +604,7 @@ export interface FileRouteTypes {
593
604
  | '/settings/labels'
594
605
  | '/settings/senders'
595
606
  | '/settings/suggested-vips'
607
+ | '/settings/warnings'
596
608
  | '/calendar/'
597
609
  | '/mail/'
598
610
  | '/settings/'
@@ -791,6 +803,13 @@ declare module '@tanstack/react-router' {
791
803
  preLoaderRoute: typeof SettingsSuggestedVipsRouteImport
792
804
  parentRoute: typeof SettingsRoute
793
805
  }
806
+ '/settings/warnings': {
807
+ id: '/settings/warnings'
808
+ path: '/warnings'
809
+ fullPath: '/settings/warnings'
810
+ preLoaderRoute: typeof SettingsWarningsRouteImport
811
+ parentRoute: typeof SettingsRoute
812
+ }
794
813
  '/calendar/$view/$date': {
795
814
  id: '/calendar/$view/$date'
796
815
  path: '/$view/$date'
@@ -1225,6 +1244,7 @@ interface SettingsRouteChildren {
1225
1244
  SettingsLabelsRoute: typeof SettingsLabelsRoute
1226
1245
  SettingsSendersRoute: typeof SettingsSendersRoute
1227
1246
  SettingsSuggestedVipsRoute: typeof SettingsSuggestedVipsRoute
1247
+ SettingsWarningsRoute: typeof SettingsWarningsRoute
1228
1248
  SettingsIndexRoute: typeof SettingsIndexRoute
1229
1249
  }
1230
1250
 
@@ -1238,6 +1258,7 @@ const SettingsRouteChildren: SettingsRouteChildren = {
1238
1258
  SettingsLabelsRoute: SettingsLabelsRoute,
1239
1259
  SettingsSendersRoute: SettingsSendersRoute,
1240
1260
  SettingsSuggestedVipsRoute: SettingsSuggestedVipsRoute,
1261
+ SettingsWarningsRoute: SettingsWarningsRoute,
1241
1262
  SettingsIndexRoute: SettingsIndexRoute,
1242
1263
  }
1243
1264
 
@@ -5,9 +5,9 @@
5
5
  * as segments beside it.
6
6
  */
7
7
 
8
+ import { EmptyState } from "@remit/ui";
8
9
  import { createFileRoute } from "@tanstack/react-router";
9
10
  import { CalendarDays } from "lucide-react";
10
- import { EmptyState } from "@/components/ui/EmptyState";
11
11
 
12
12
  function CalendarReadingPane() {
13
13
  return (
@@ -10,13 +10,14 @@
10
10
  * through and takes it back to their mail — nothing calendar-specific joins the
11
11
  * union, because none of the calendar's own chrome is a panel.
12
12
  */
13
+
13
14
  import { configOperationsGetConfigOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
15
+ import { ErrorState } from "@remit/ui";
14
16
  import { useQuery } from "@tanstack/react-query";
15
17
  import { createFileRoute, Outlet, useNavigate } from "@tanstack/react-router";
16
18
  import { useCallback, useEffect } from "react";
17
19
  import { CalendarShellProvider } from "@/components/calendar/CalendarShell";
18
20
  import { MailNav } from "@/components/mail/MailNav";
19
- import { ErrorState } from "@/components/ui/ErrorState";
20
21
  import { isSinglePaneTier, useLayoutTier } from "@/hooks/useLayoutTier";
21
22
  import { panelsWithOverlay, useOpenPanels, useSetOpenPanels } from "@/routing";
22
23
  import "@/lib/client";
@@ -7,6 +7,7 @@
7
7
  * message inside it are the segments below, which is why nothing here reads a
8
8
  * selection out of the query.
9
9
  */
10
+ import { ErrorState } from "@remit/ui";
10
11
  import {
11
12
  createFileRoute,
12
13
  type ErrorComponentProps,
@@ -14,7 +15,6 @@ import {
14
15
  } from "@tanstack/react-router";
15
16
  import { MailShell } from "@/components/layout/MailShell";
16
17
  import { BriefPane } from "@/components/mail/BriefPane";
17
- import { ErrorState } from "@/components/ui/ErrorState";
18
18
  import { briefSearchSchema } from "@/lib/mail-search";
19
19
  import { useOpenThreadPath, useSearchMirror } from "@/routing";
20
20
 
@@ -6,6 +6,7 @@
6
6
  * route matched below this one, and the open thread and the message inside it
7
7
  * are the segments there rather than a selection read out of the query.
8
8
  */
9
+ import { ErrorState } from "@remit/ui";
9
10
  import {
10
11
  createFileRoute,
11
12
  type ErrorComponentProps,
@@ -13,7 +14,6 @@ import {
13
14
  } from "@tanstack/react-router";
14
15
  import { MailShell } from "@/components/layout/MailShell";
15
16
  import { FlaggedPane } from "@/components/mail/FlaggedPane";
16
- import { ErrorState } from "@/components/ui/ErrorState";
17
17
  import { flaggedSearchSchema } from "@/lib/mail-search";
18
18
  import { useOpenThreadPath, useSearchMirror } from "@/routing";
19
19
 
@@ -2,7 +2,11 @@ import {
2
2
  configOperationsGetConfigOptions,
3
3
  unifiedThreadOperationsListAllThreadsOptions,
4
4
  } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
5
- import { useTriageKeyboard } from "@remit/ui";
5
+ import {
6
+ ErrorState,
7
+ KeyboardShortcutsModal,
8
+ useTriageKeyboard,
9
+ } from "@remit/ui";
6
10
  import { useQuery } from "@tanstack/react-query";
7
11
  import {
8
12
  createFileRoute,
@@ -16,8 +20,6 @@ import { ComposeFab } from "@/components/layout/ComposeFab";
16
20
  import { MailShellProvider } from "@/components/layout/MailShell";
17
21
  import { MailTopBar } from "@/components/layout/MailTopBar";
18
22
  import { MailNav } from "@/components/mail/MailNav";
19
- import { ErrorState } from "@/components/ui/ErrorState";
20
- import { KeyboardShortcutsModal } from "@/components/ui/KeyboardShortcutsModal";
21
23
  import { useKeyboardNavigation } from "@/hooks/useKeyboardNavigation";
22
24
  import { isSinglePaneTier, useLayoutTier } from "@/hooks/useLayoutTier";
23
25
  import { useMailboxNameIndex } from "@/hooks/useMailboxNameIndex";
@@ -80,11 +80,6 @@ describe("mapOauthError", () => {
80
80
  `Expected code in message, got: ${result}`,
81
81
  );
82
82
  });
83
-
84
- test("empty string returns generic fallback", () => {
85
- const result = mapOauthError("");
86
- assert.ok(typeof result === "string" && result.length > 0);
87
- });
88
83
  });
89
84
 
90
85
  /**
@@ -65,7 +65,6 @@ const filter: RemitImapFilterResponse = {
65
65
  scope: "Standing",
66
66
  state: "Active",
67
67
  disabledReason: "None",
68
- hasAnchor: false,
69
68
  ruleChangedAt: 0,
70
69
  actionChangedAt: 0,
71
70
  matchOperator: "And",
@@ -66,11 +66,26 @@ export const advancedHelp = (
66
66
  <strong className="text-fg">Notification rules</strong> and data export
67
67
  are coming in a future release.
68
68
  </p>
69
+ </div>
70
+ );
71
+
72
+ export const warningsHelp = (
73
+ <div className="space-y-3">
74
+ <p>
75
+ A message that fails to sync three times shows here. A{" "}
76
+ <strong className="text-fg">retrying</strong> one is tried again every
77
+ sync; a body that <strong className="text-fg">gave up</strong> is not.
78
+ Either leaves the list once it syncs.
79
+ </p>
69
80
  <p>
70
81
  A message Remit cannot read is{" "}
71
82
  <strong className="text-fg">set aside</strong> rather than skipped, and
72
83
  the folder keeps syncing. Setting one aside is a defect in Remit, so every
73
84
  entry can be reported with the diagnostics already attached.
74
85
  </p>
86
+ <p>
87
+ A report carries no folder name, subject, address, message content or
88
+ error text.
89
+ </p>
75
90
  </div>
76
91
  );
@@ -12,6 +12,7 @@ import {
12
12
  Banner,
13
13
  Button,
14
14
  Dialog,
15
+ ErrorState,
15
16
  type RowAction,
16
17
  RowActions,
17
18
  SettingsShell,
@@ -28,7 +29,6 @@ import {
28
29
  import { AccountFormPanel } from "@/components/settings/AccountFormPanel";
29
30
  import { AccountServices } from "@/components/settings/AccountServices";
30
31
  import { DangerZone } from "@/components/settings/DangerZone";
31
- import { ErrorState } from "@/components/ui/ErrorState";
32
32
  import {
33
33
  REDIRECT_STALL_MESSAGE,
34
34
  useRedirectEnded,
@@ -1,13 +1,11 @@
1
1
  /**
2
2
  * Advanced settings. Updates live here — and only here — with a full pane in
3
- * Settings › Advanced, alongside the messages sync could not read. Notification
4
- * rules and export are future scope.
3
+ * Settings › Advanced. Notification rules and export are future scope.
5
4
  */
6
5
  import { SettingsShell } from "@remit/ui";
7
6
  import { createFileRoute, useNavigate } from "@tanstack/react-router";
8
7
  import { useState } from "react";
9
8
  import { ConfigTransferPanel } from "@/components/settings/ConfigTransferPanel";
10
- import { QuarantinePanel } from "@/components/settings/QuarantinePanel";
11
9
  import { SelfUpdatePanel } from "@/components/settings/SelfUpdatePanel";
12
10
  import { TlsRootCaDownload } from "@/components/settings/TlsRootCaDownload";
13
11
  import { AppVersion } from "@/components/ui/AppVersion";
@@ -40,7 +38,6 @@ function AdvancedSettings() {
40
38
  onBackToMail={() => void navigate({ to: "/mail" })}
41
39
  >
42
40
  <SelfUpdatePanel />
43
- <QuarantinePanel />
44
41
  <div className="mt-6 border-t border-line pt-4">
45
42
  <ConfigTransferPanel />
46
43
  </div>
@@ -3,14 +3,13 @@ import {
3
3
  mailboxOperationsListMailboxesOptions,
4
4
  } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
5
5
  import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
6
- import { type LabelOption, SettingsShell } from "@remit/ui";
6
+ import { ErrorState, type LabelOption, SettingsShell } from "@remit/ui";
7
7
  import { useQuery } from "@tanstack/react-query";
8
8
  import { createFileRoute, useNavigate } from "@tanstack/react-router";
9
9
  import { useCallback, useMemo, useState } from "react";
10
10
  import { FilterActionError } from "@/components/settings/FilterActionError";
11
11
  import { FilterEditorSurface } from "@/components/settings/FilterEditorSurface";
12
12
  import { FiltersList } from "@/components/settings/FiltersList";
13
- import { ErrorState } from "@/components/ui/ErrorState";
14
13
  import {
15
14
  useDeleteFilter,
16
15
  useFilterList,