@remit/web-client 0.0.322 → 0.0.324

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 (198) 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/better-auth-config.ts +3 -10
  6. package/src/auth/cognito/cognito-token.test.ts +2 -4
  7. package/src/components/calendar/AgendaStrip.tsx +1 -1
  8. package/src/components/calendar/CalendarWorkspace.tsx +1 -1
  9. package/src/components/calendar/ChangeCalendarSuggestion.tsx +1 -1
  10. package/src/components/calendar/OpenCalendarEvent.tsx +1 -18
  11. package/src/components/compose/ComposeForm.tsx +11 -6
  12. package/src/components/compose/FullCompose.tsx +1 -1
  13. package/src/components/compose/MobileComposeSheet.tsx +1 -2
  14. package/src/components/compose/compose.stories.tsx +18 -0
  15. package/src/components/compose/draft-discard-failure.render.test.ts +138 -0
  16. package/src/components/config-import/config-import.fixtures.ts +8 -9
  17. package/src/components/config-import/config-import.stories.tsx +2 -2
  18. package/src/components/layout/MailTopBar.tsx +3 -0
  19. package/src/components/mail/BriefPane.tsx +0 -3
  20. package/src/components/mail/ConversationView.tsx +1 -3
  21. package/src/components/mail/DailyBrief.tsx +5 -28
  22. package/src/components/mail/FlaggedList.tsx +3 -4
  23. package/src/components/mail/FlaggedPane.tsx +0 -3
  24. package/src/components/mail/IntelligenceDrawer.tsx +0 -3
  25. package/src/components/mail/IntelligencePane.render.test.ts +0 -1
  26. package/src/components/mail/IntelligencePane.test.ts +1 -95
  27. package/src/components/mail/IntelligencePane.tsx +3 -93
  28. package/src/components/mail/MailListHeader.tsx +12 -43
  29. package/src/components/mail/MailNav.tsx +6 -2
  30. package/src/components/mail/MailViewChrome.tsx +1 -11
  31. package/src/components/mail/MailboxPane.tsx +4 -34
  32. package/src/components/mail/MessageActionMenu.tsx +6 -6
  33. package/src/components/mail/MessageAttachments.tsx +35 -0
  34. package/src/components/mail/MessageBody.tsx +1 -0
  35. package/src/components/mail/MessageBodyErrorBanner.test.ts +0 -30
  36. package/src/components/mail/MessageCard.tsx +8 -3
  37. package/src/components/mail/MessageList.run-reporting.test.ts +2 -46
  38. package/src/components/mail/MessageList.tsx +23 -11
  39. package/src/components/mail/MessageRow.render.test.ts +0 -7
  40. package/src/components/mail/MoveToTrigger.tsx +1 -1
  41. package/src/components/mail/RawMessageView.tsx +1 -1
  42. package/src/components/mail/SelectionWizardHost.search-entry.test.ts +0 -1
  43. package/src/components/mail/SelectionWizardHost.tsx +16 -102
  44. package/src/components/mail/authenticity-warning.stories.tsx +0 -21
  45. package/src/components/mail/daily-brief.stories.tsx +31 -17
  46. package/src/components/mail/enter-cursor-late-open.render.test.ts +197 -0
  47. package/src/components/mail/intelligence-drawer.stories.tsx +0 -2
  48. package/src/components/mail/message-gone.stories.tsx +3 -1
  49. package/src/components/mail/offer-times-reply.render.test.ts +25 -1
  50. package/src/components/mail/reclassify-remove-override.render.test.ts +2 -6
  51. package/src/components/overlay-escape.render.test.ts +7 -3
  52. package/src/components/settings/AccountFormPanel.render.test.ts +26 -3
  53. package/src/components/settings/AccountFormPanel.test.ts +1 -36
  54. package/src/components/settings/AccountFormPanel.tsx +5 -25
  55. package/src/components/settings/AccountServices.tsx +1 -1
  56. package/src/components/settings/AdvancedNavIcon.tsx +3 -3
  57. package/src/components/settings/CalendarFeedCard.tsx +2 -1
  58. package/src/components/settings/CalendarSubscribeCard.tsx +1 -1
  59. package/src/components/settings/CalendarSubscriptionStatus.tsx +1 -2
  60. package/src/components/settings/FilterActionError.tsx +1 -1
  61. package/src/components/settings/FilterEditor.render.test.ts +3 -27
  62. package/src/components/settings/FilterEditor.tsx +4 -24
  63. package/src/components/settings/FilterEditorSurface.tsx +0 -3
  64. package/src/components/settings/FiltersList.render.test.ts +5 -24
  65. package/src/components/settings/FiltersList.stories.tsx +0 -1
  66. package/src/components/settings/FiltersList.tsx +10 -35
  67. package/src/components/settings/WarningsNavIcon.tsx +12 -0
  68. package/src/components/settings/{QuarantinePanel.delimiter.test.ts → WarningsPanel.dom.test.ts} +64 -5
  69. package/src/components/settings/WarningsPanel.render.test.ts +56 -0
  70. package/src/components/settings/WarningsPanel.tsx +244 -0
  71. package/src/components/settings/accounts.stories.tsx +18 -0
  72. package/src/components/settings/advanced.stories.tsx +3 -125
  73. package/src/components/settings/self-update.stories.tsx +18 -0
  74. package/src/components/settings/settings-filter.stories.tsx +2 -42
  75. package/src/components/settings/warnings.stories.tsx +290 -0
  76. package/src/components/ui/ErrorBannerProvider.tsx +5 -3
  77. package/src/components/ui/error-banners.test.ts +1 -22
  78. package/src/components/ui/error-banners.ts +5 -20
  79. package/src/hooks/calendar/useCalendarCollectionWrites.ts +2 -1
  80. package/src/hooks/calendar/useCalendarSuggestions.ts +2 -1
  81. package/src/hooks/calendar/useCalendarWrites.ts +2 -1
  82. package/src/hooks/intelligence-calendar.render.test.ts +0 -1
  83. package/src/hooks/useDeleteMessages.test.ts +1 -42
  84. package/src/hooks/useFilters.test.ts +0 -8
  85. package/src/hooks/useFollowFocusOpen.render.test.ts +1 -11
  86. package/src/hooks/useIntelligenceData.test.ts +9 -47
  87. package/src/hooks/useIntelligenceData.ts +7 -100
  88. package/src/hooks/useLayoutTier.test.ts +0 -48
  89. package/src/hooks/useReportSpam.ts +1 -1
  90. package/src/hooks/useRescueCandidates.test.ts +0 -23
  91. package/src/hooks/useRulePreview.ts +8 -7
  92. package/src/hooks/useSearchTokenContext.ts +2 -3
  93. package/src/hooks/useStaleAccountSync.test.ts +0 -17
  94. package/src/hooks/useStarredThreads.test.ts +0 -10
  95. package/src/hooks/useTriggerSync.test.ts +0 -6
  96. package/src/hooks/useVisualViewport.render.test.ts +63 -0
  97. package/src/hooks/useWarningsAttention.ts +37 -0
  98. package/src/lib/account-order.test.ts +0 -10
  99. package/src/lib/adjacent-message.test.ts +0 -9
  100. package/src/lib/app-info.ts +0 -3
  101. package/src/lib/autodiscovery.test.ts +0 -47
  102. package/src/lib/bug-report.test.ts +38 -0
  103. package/src/lib/bug-report.ts +10 -7
  104. package/src/lib/bulk-action-copy.ts +1 -1
  105. package/src/lib/calendar-route.test.ts +0 -5
  106. package/src/lib/config-import.test.ts +3 -39
  107. package/src/lib/dedupe-thread-messages.test.ts +0 -4
  108. package/src/lib/desktop-breakpoint.test.ts +0 -8
  109. package/src/lib/drafts.test.ts +0 -23
  110. package/src/lib/fatal-error.test.ts +0 -19
  111. package/src/lib/folder-roles.test.ts +0 -4
  112. package/src/lib/hot-sync-window.test.ts +0 -5
  113. package/src/lib/list-header-chrome.ts +1 -1
  114. package/src/lib/mail-context.ts +1 -1
  115. package/src/lib/message-attachments.test.ts +77 -0
  116. package/src/lib/message-attachments.ts +31 -15
  117. package/src/lib/message-body-source.test.ts +21 -4
  118. package/src/lib/message-body-source.ts +5 -2
  119. package/src/lib/onboarding-completion.test.ts +0 -16
  120. package/src/lib/organize/filter-edit-model.test.ts +2 -32
  121. package/src/lib/organize/filter-edit-model.ts +6 -16
  122. package/src/lib/organize/organize-model.test.ts +7 -40
  123. package/src/lib/organize/organize-model.ts +16 -18
  124. package/src/lib/organize/rule-model.test.ts +24 -202
  125. package/src/lib/organize/rule-model.ts +19 -166
  126. package/src/lib/organize/search-to-rule.test.ts +2 -36
  127. package/src/lib/organize/search-to-rule.ts +1 -16
  128. package/src/lib/provider-presets.test.ts +1 -77
  129. package/src/lib/query-error-handler.test.ts +0 -61
  130. package/src/lib/query-escalation.integration.test.ts +4 -4
  131. package/src/lib/result-count.ts +1 -1
  132. package/src/lib/result-folder.ts +1 -1
  133. package/src/lib/rum-adapter.test.ts +0 -104
  134. package/src/lib/search-query.test.ts +0 -11
  135. package/src/lib/search-result.test.ts +2 -142
  136. package/src/lib/search-result.ts +3 -73
  137. package/src/lib/search-scope.test.ts +1 -92
  138. package/src/lib/search-scope.ts +0 -29
  139. package/src/lib/search-surface.ts +2 -2
  140. package/src/lib/search-token-index.test.ts +0 -8
  141. package/src/lib/search-tokens.ts +15 -16
  142. package/src/lib/session-storage.ts +7 -0
  143. package/src/lib/stale-build.test.ts +52 -0
  144. package/src/lib/stale-build.ts +36 -0
  145. package/src/lib/starred-rows.test.ts +0 -4
  146. package/src/lib/sync-warning-entries.ts +33 -0
  147. package/src/lib/wizard-selection.ts +1 -1
  148. package/src/mocks/story-frame/mail-handlers.ts +12 -8
  149. package/src/mocks/story-frame/mail-world.ts +5 -0
  150. package/src/routeTree.gen.ts +21 -0
  151. package/src/routes/__root.tsx +14 -4
  152. package/src/routes/calendar/$view.$date/index.tsx +1 -1
  153. package/src/routes/calendar.tsx +2 -1
  154. package/src/routes/mail/brief.tsx +1 -1
  155. package/src/routes/mail/flagged.tsx +1 -1
  156. package/src/routes/mail.tsx +5 -3
  157. package/src/routes/settings/-accounts.test.ts +0 -5
  158. package/src/routes/settings/-filters.test.ts +0 -1
  159. package/src/routes/settings/-shared/help-copy.tsx +15 -0
  160. package/src/routes/settings/accounts.tsx +1 -1
  161. package/src/routes/settings/advanced.tsx +3 -5
  162. package/src/routes/settings/filters.tsx +1 -2
  163. package/src/routes/settings/folders.tsx +1 -1
  164. package/src/routes/settings/labels.tsx +1 -1
  165. package/src/routes/settings/senders.tsx +1 -1
  166. package/src/routes/settings/warnings.tsx +36 -0
  167. package/src/routes/settings.tsx +7 -0
  168. package/src/routing/wizard-history.test.ts +1 -45
  169. package/src/runtime-config.test.ts +17 -0
  170. package/src/runtime-config.ts +18 -3
  171. package/src/shell/index.tsx +3 -0
  172. package/src/test-support/fixtures.ts +1 -1
  173. package/src/components/compose/draft-discard-banner.test.ts +0 -76
  174. package/src/components/mail/ReadingPaneEmpty.render.test.ts +0 -42
  175. package/src/components/mail/SpamRescue.render.test.ts +0 -35
  176. package/src/components/mail/intelligence-semantic-off.render.test.ts +0 -111
  177. package/src/components/mail/selection-surface-rollcall.test.ts +0 -270
  178. package/src/components/settings/QuarantinePanel.render.test.ts +0 -71
  179. package/src/components/settings/QuarantinePanel.tsx +0 -178
  180. package/src/components/ui/DropdownMenu.tsx +0 -99
  181. package/src/components/ui/EmptyState.tsx +0 -11
  182. package/src/components/ui/ErrorBanner.render.test.ts +0 -104
  183. package/src/components/ui/ErrorBanner.stories.tsx +0 -132
  184. package/src/components/ui/ErrorBanner.tsx +0 -106
  185. package/src/components/ui/ErrorBannerStack.tsx +0 -35
  186. package/src/components/ui/ErrorState.render.test.ts +0 -82
  187. package/src/components/ui/ErrorState.tsx +0 -80
  188. package/src/components/ui/KeyboardShortcutsModal.tsx +0 -113
  189. package/src/hooks/useMoveMessages.test.ts +0 -63
  190. package/src/hooks/useOrganizeWiden.test.ts +0 -217
  191. package/src/hooks/useOrganizeWiden.ts +0 -112
  192. package/src/hooks/useSemanticSearch.ts +0 -132
  193. package/src/hooks/useSemanticSearchEnabled.ts +0 -23
  194. package/src/hooks/useVisualViewport.test.ts +0 -126
  195. package/src/lib/organize/sender-fallback.test.ts +0 -52
  196. package/src/lib/organize/sender-fallback.ts +0 -38
  197. package/src/lib/rescue-telemetry.test.ts +0 -75
  198. package/src/router.test.ts +0 -81
@@ -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,17 +1,16 @@
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";
14
12
  import { SETTINGS_ID_TO_PATH, SETTINGS_NAV_ITEMS } from "@/routes/settings";
13
+ import { getRuntimeConfig } from "@/runtime-config";
15
14
  import { advancedHelp } from "./-shared/help-copy";
16
15
 
17
16
  export const Route = createFileRoute("/settings/advanced")({
@@ -39,8 +38,7 @@ function AdvancedSettings() {
39
38
  onSelect={handleSelectNav}
40
39
  onBackToMail={() => void navigate({ to: "/mail" })}
41
40
  >
42
- <SelfUpdatePanel />
43
- <QuarantinePanel />
41
+ {getRuntimeConfig().updateMode === "self" && <SelfUpdatePanel />}
44
42
  <div className="mt-6 border-t border-line pt-4">
45
43
  <ConfigTransferPanel />
46
44
  </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,
@@ -13,6 +13,7 @@ import type {
13
13
  import {
14
14
  Banner,
15
15
  type CandidateFolder,
16
+ ErrorState,
16
17
  FolderManager,
17
18
  FolderRenameDialog,
18
19
  type FolderRole,
@@ -26,7 +27,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
26
27
  import { createFileRoute, useNavigate } from "@tanstack/react-router";
27
28
  import { useMemo, useState } from "react";
28
29
  import { DeleteFolderDialog } from "@/components/settings/DeleteFolderDialog";
29
- import { ErrorState } from "@/components/ui/ErrorState";
30
30
  import { isMailboxNotSettledRefusal } from "@/components/ui/folder-role-refusal";
31
31
  import { useCreateMailbox } from "@/hooks/useCreateMailbox";
32
32
  import { useFolderLabelTranslator } from "@/hooks/useFolderLabelTranslator";
@@ -8,6 +8,7 @@ import {
8
8
  Banner,
9
9
  Button,
10
10
  ConfirmDialog,
11
+ ErrorState,
11
12
  Input,
12
13
  labelColorOptions,
13
14
  Select,
@@ -17,7 +18,6 @@ import { useQuery } from "@tanstack/react-query";
17
18
  import { createFileRoute, useNavigate } from "@tanstack/react-router";
18
19
  import { useState } from "react";
19
20
  import { LabelsList } from "@/components/settings/LabelsList";
20
- import { ErrorState } from "@/components/ui/ErrorState";
21
21
  import {
22
22
  useCreateLabel,
23
23
  useDeleteLabel,
@@ -20,6 +20,7 @@ import type {
20
20
  } from "@remit/api-http-client/types.gen.ts";
21
21
  import {
22
22
  Button,
23
+ ErrorState,
23
24
  Input,
24
25
  Kbd,
25
26
  SenderFlagRow,
@@ -30,7 +31,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
30
31
  import { createFileRoute, useNavigate } from "@tanstack/react-router";
31
32
  import { Search, Star, X } from "lucide-react";
32
33
  import { useCallback, useEffect, useState } from "react";
33
- import { ErrorState } from "@/components/ui/ErrorState";
34
34
  import { useDebouncedValue } from "@/hooks/useDebouncedValue";
35
35
  import { formatDate } from "@/lib/format";
36
36
  import { SETTINGS_ID_TO_PATH, SETTINGS_NAV_ITEMS } from "@/routes/settings";
@@ -0,0 +1,36 @@
1
+ import { SettingsShell } from "@remit/ui";
2
+ import { createFileRoute, useNavigate } from "@tanstack/react-router";
3
+ import { useState } from "react";
4
+ import { WarningsPanel } from "@/components/settings/WarningsPanel";
5
+ import { SETTINGS_ID_TO_PATH, SETTINGS_NAV_ITEMS } from "@/routes/settings";
6
+ import { warningsHelp } from "./-shared/help-copy";
7
+
8
+ export const Route = createFileRoute("/settings/warnings")({
9
+ component: WarningsSettings,
10
+ });
11
+
12
+ function WarningsSettings() {
13
+ const navigate = useNavigate();
14
+ const [helpOpen, setHelpOpen] = useState(true);
15
+
16
+ const handleSelectNav = (id: string) => {
17
+ const path = SETTINGS_ID_TO_PATH[id];
18
+ if (path) void navigate({ to: path });
19
+ };
20
+
21
+ return (
22
+ <SettingsShell
23
+ items={SETTINGS_NAV_ITEMS}
24
+ activeId="warnings"
25
+ title="Warnings"
26
+ description="Messages that failed to sync, and messages set aside."
27
+ help={warningsHelp}
28
+ helpOpen={helpOpen}
29
+ onToggleHelp={() => setHelpOpen((v) => !v)}
30
+ onSelect={handleSelectNav}
31
+ onBackToMail={() => void navigate({ to: "/mail" })}
32
+ >
33
+ <WarningsPanel />
34
+ </SettingsShell>
35
+ );
36
+ }
@@ -20,6 +20,7 @@ import {
20
20
  Users,
21
21
  } from "lucide-react";
22
22
  import { AdvancedNavIcon } from "@/components/settings/AdvancedNavIcon";
23
+ import { WarningsNavIcon } from "@/components/settings/WarningsNavIcon";
23
24
 
24
25
  /* ------------------------------------------------------------------ */
25
26
  /* Nav items — single source of truth shared by all settings pages */
@@ -57,6 +58,11 @@ export const SETTINGS_NAV_ITEMS: SettingsNavItem[] = [
57
58
  label: "Appearance",
58
59
  icon: <Palette className="size-4" />,
59
60
  },
61
+ {
62
+ id: "warnings",
63
+ label: "Warnings",
64
+ icon: <WarningsNavIcon />,
65
+ },
60
66
  { id: "advanced", label: "Advanced", icon: <AdvancedNavIcon /> },
61
67
  ];
62
68
 
@@ -68,6 +74,7 @@ export const SETTINGS_ID_TO_PATH: Record<string, string> = {
68
74
  labels: "/settings/labels",
69
75
  calendars: "/settings/calendars",
70
76
  appearance: "/settings/appearance",
77
+ warnings: "/settings/warnings",
71
78
  advanced: "/settings/advanced",
72
79
  };
73
80
 
@@ -1,8 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { readFileSync } from "node:fs";
3
- import { dirname, resolve } from "node:path";
4
2
  import { describe, it } from "node:test";
5
- import { fileURLToPath } from "node:url";
6
3
  import {
7
4
  backExits,
8
5
  type MatchMode,
@@ -28,7 +25,7 @@ const VERBS: readonly Verb[] = [
28
25
  "markRead",
29
26
  "organize",
30
27
  ];
31
- const MODES: readonly MatchMode[] = ["selected", "similar", "properties"];
28
+ const MODES: readonly MatchMode[] = ["selected", "properties"];
32
29
  const SCOPES: readonly (RuleScope | undefined)[] = [
33
30
  undefined,
34
31
  "once",
@@ -50,51 +47,10 @@ const answerSets = (): WizardAnswers[] => {
50
47
  return sets;
51
48
  };
52
49
 
53
- const here = dirname(fileURLToPath(import.meta.url));
54
- const source = readFileSync(resolve(here, "wizard-history.ts"), "utf8");
55
-
56
50
  /** The steps up to and including the one an answer is given on. */
57
51
  const prefixThrough = (steps: readonly StepId[], step: StepId): StepId[] =>
58
52
  steps.slice(0, steps.indexOf(step) + 1);
59
53
 
60
- /**
61
- * The re-root runs inside a router, so the rule is enforced by reading the
62
- * source — as `../components/mail/MessageList.selection.test.ts` does for its
63
- * own component-level rules. What it pins is where the decision comes from: a
64
- * step the app itself pushed must never be re-rooted, or the push that opens
65
- * the wizard from a verb (#483) duplicates the entry underneath it and the
66
- * first back after Cancel appears to do nothing.
67
- */
68
- describe("re-rooting a wizard that was loaded into", () => {
69
- it("decides from what the first render held, not from a flag", () => {
70
- assert.match(source, /useRef\(step !== undefined\)/);
71
- assert.match(
72
- source,
73
- /if \(!loadedHoldingStep\.current\) return;\s*\n\s*loadedHoldingStep\.current = false;/,
74
- );
75
- });
76
-
77
- // The root is the wizard-closed state, so it carries neither the step nor
78
- // the affordance that opened it; otherwise closing the wizard leaves the
79
- // entry marker behind in the address bar (#484).
80
- it("puts the wizard back on a root that names no entry", () => {
81
- assert.match(source, /wizard: undefined,\s*\n\s*wizardFrom: undefined,/);
82
- assert.match(
83
- source,
84
- /wizard: openingStep,\s*\n\s*wizardFrom: openingEntry,/,
85
- );
86
- });
87
-
88
- it("is never armed by a step the app pushed", () => {
89
- const goToStep = source.slice(source.indexOf("const goToStep"));
90
- assert.doesNotMatch(goToStep, /loadedHoldingStep/);
91
- });
92
-
93
- it("does not re-run when the step changes", () => {
94
- assert.doesNotMatch(source, /\}, \[step, openingStep, navigate\]\)/);
95
- });
96
- });
97
-
98
54
  describe("the wizard step in the URL", () => {
99
55
  it("round-trips every step the wizard can reach", () => {
100
56
  for (const answers of answerSets()) {
@@ -24,3 +24,20 @@ describe("getRuntimeConfig tlsMode", () => {
24
24
  assert.equal(getRuntimeConfig().tlsMode, "off");
25
25
  });
26
26
  });
27
+
28
+ describe("getRuntimeConfig updateMode", () => {
29
+ it("defaults to self when config.js sets nothing", () => {
30
+ globalThis.__REMIT_CONFIG__ = {};
31
+ assert.equal(getRuntimeConfig().updateMode, "self");
32
+ });
33
+
34
+ it("passes through managed", () => {
35
+ globalThis.__REMIT_CONFIG__ = { updateMode: "managed" };
36
+ assert.equal(getRuntimeConfig().updateMode, "managed");
37
+ });
38
+
39
+ it("falls back to self for a value config.js never declares", () => {
40
+ globalThis.__REMIT_CONFIG__ = { updateMode: "auto" };
41
+ assert.equal(getRuntimeConfig().updateMode, "self");
42
+ });
43
+ });
@@ -26,10 +26,19 @@ export interface RumConfig {
26
26
  */
27
27
  export type TlsMode = "internal" | "acme" | "tailscale" | "off";
28
28
 
29
+ export type UpdateMode = "self" | "managed";
30
+
31
+ const DEFAULT_ISSUES_URL = "https://github.com/remit-mail/reader/issues/new";
32
+
29
33
  const TLS_MODES: readonly TlsMode[] = ["internal", "acme", "tailscale", "off"];
30
34
 
31
- const isTlsMode = (value: unknown): value is TlsMode =>
32
- typeof value === "string" && (TLS_MODES as readonly string[]).includes(value);
35
+ const UPDATE_MODES: readonly UpdateMode[] = ["self", "managed"];
36
+
37
+ const oneOf = <T extends string>(
38
+ values: readonly T[],
39
+ raw: unknown,
40
+ fallback: T,
41
+ ): T => values.find((value) => value === raw) ?? fallback;
33
42
 
34
43
  export interface RemitRuntimeConfig {
35
44
  apiUrl: string;
@@ -40,6 +49,8 @@ export interface RemitRuntimeConfig {
40
49
  mailboxPollIntervalSeconds?: string;
41
50
  disableDevtools: boolean;
42
51
  tlsMode: TlsMode;
52
+ issuesUrl: string;
53
+ updateMode: UpdateMode;
43
54
  }
44
55
 
45
56
  interface RawRuntimeConfig {
@@ -51,6 +62,8 @@ interface RawRuntimeConfig {
51
62
  mailboxPollIntervalSeconds?: string | number;
52
63
  disableDevtools?: boolean;
53
64
  tlsMode?: string;
65
+ issuesUrl?: string;
66
+ updateMode?: string;
54
67
  }
55
68
 
56
69
  declare global {
@@ -84,6 +97,8 @@ export const getRuntimeConfig = (): RemitRuntimeConfig => {
84
97
  ? String(raw.mailboxPollIntervalSeconds)
85
98
  : undefined,
86
99
  disableDevtools: raw.disableDevtools ?? false,
87
- tlsMode: isTlsMode(raw.tlsMode) ? raw.tlsMode : "off",
100
+ tlsMode: oneOf(TLS_MODES, raw.tlsMode, "off"),
101
+ issuesUrl: raw.issuesUrl ?? DEFAULT_ISSUES_URL,
102
+ updateMode: oneOf(UPDATE_MODES, raw.updateMode, "self"),
88
103
  };
89
104
  };
@@ -17,6 +17,7 @@ import {
17
17
  handleQueryCacheError,
18
18
  } from "@/lib/query-error-handler";
19
19
  import { initRum } from "@/lib/rum-adapter";
20
+ import { installStaleBuildReload } from "@/lib/stale-build";
20
21
  import { TelemetryContext } from "@/lib/telemetry-context";
21
22
  import { installThemeSync } from "@/lib/theme";
22
23
  import { createAppRouter } from "@/router";
@@ -69,6 +70,8 @@ export const mountApp = ({
69
70
  },
70
71
  });
71
72
 
73
+ installStaleBuildReload();
74
+
72
75
  const router = createAppRouter(queryClient, telemetry);
73
76
  telemetry.recordPageView(window.location.pathname);
74
77
 
@@ -104,6 +104,6 @@ export const makeConfig = (
104
104
  updatedAt: 0,
105
105
  },
106
106
  accounts,
107
- semanticSearchEnabled: true,
107
+ buildCommit: "dev",
108
108
  ...overrides,
109
109
  });
@@ -1,76 +0,0 @@
1
- /**
2
- * Draft-discard must surface a banner on a soft failure.
3
- *
4
- * `ComposeForm.handleDiscard` closes the compose dialog optimistically, then
5
- * fires the delete mutation. Before #741 the delete had no `onError`, so a soft
6
- * 4xx (409/404 "the draft is already gone") closed the dialog as if it
7
- * succeeded — silent discard. The fix adds an `onError` that pushes a banner via
8
- * `buildMutationErrorBanner("Couldn't discard draft", …)`, mirroring the
9
- * outbox-delete pattern. A fatal 5xx still escalates globally; this pins the
10
- * non-fatal branch.
11
- *
12
- * Exercising the real mutation `onError` headlessly (no DOM) via the same
13
- * `MutationCache` wiring `ComposeForm` uses through `QueryClient`.
14
- */
15
-
16
- import assert from "node:assert/strict";
17
- import { describe, it } from "node:test";
18
- import { MutationCache, QueryClient } from "@tanstack/react-query";
19
- import { ApiError } from "../../lib/api.js";
20
- import {
21
- buildMutationErrorBanner,
22
- type PushErrorInput,
23
- } from "../ui/error-banners.js";
24
-
25
- describe("draft discard surfaces a banner on a soft failure (#741)", () => {
26
- const buildDiscardMutation = (
27
- pushError: (input: PushErrorInput) => void,
28
- fail: () => Promise<never>,
29
- ) => {
30
- const client = new QueryClient({
31
- mutationCache: new MutationCache(),
32
- });
33
- return client.getMutationCache().build(client, {
34
- mutationFn: fail,
35
- onError: (error: unknown) => {
36
- pushError(
37
- buildMutationErrorBanner(
38
- "Couldn't discard draft",
39
- "The draft wasn't discarded.",
40
- error,
41
- ),
42
- );
43
- },
44
- });
45
- };
46
-
47
- it("pushes a 'Couldn't discard draft' banner on a 409 already-gone", async () => {
48
- const pushed: PushErrorInput[] = [];
49
- const mutation = buildDiscardMutation(
50
- (input) => pushed.push(input),
51
- async () => {
52
- throw new ApiError("Draft already deleted", 409);
53
- },
54
- );
55
-
56
- await mutation.execute(undefined).catch(() => {});
57
-
58
- assert.equal(pushed.length, 1);
59
- assert.equal(pushed[0].title, "Couldn't discard draft");
60
- assert.equal(pushed[0].detail, "Draft already deleted");
61
- });
62
-
63
- it("falls back to a human detail when the error carries no message", async () => {
64
- const pushed: PushErrorInput[] = [];
65
- const mutation = buildDiscardMutation(
66
- (input) => pushed.push(input),
67
- async () => {
68
- throw {};
69
- },
70
- );
71
-
72
- await mutation.execute(undefined).catch(() => {});
73
-
74
- assert.equal(pushed[0].detail, "The draft wasn't discarded.");
75
- });
76
- });
@@ -1,42 +0,0 @@
1
- /**
2
- * Render invariants for the shared reading-pane zero-state and the keyboard
3
- * hint bar (#785). Both ship as remit-ui components consumed by the live mail
4
- * routes. Uses `react-dom/server` so no jsdom is needed.
5
- */
6
-
7
- import assert from "node:assert/strict";
8
- import { describe, it } from "node:test";
9
- import { KeyboardHintBar, ReadingPaneEmpty } from "@remit/ui";
10
- import React, { createElement } from "react";
11
- import { renderToString } from "react-dom/server";
12
-
13
- // See MessageToolbar.render.test.ts: the SSR test loader transpiles remit-ui
14
- // `.tsx` with the classic JSX runtime, which needs a global `React`.
15
- (globalThis as { React?: typeof React }).React = React;
16
-
17
- describe("ReadingPaneEmpty (#785)", () => {
18
- it("renders the prompt and the j/k/Enter Kbd hints by default", () => {
19
- const html = renderToString(createElement(ReadingPaneEmpty) as never);
20
- assert.match(html, /Select a thread to read/);
21
- assert.match(html, /<kbd[^>]*>j<\/kbd>/);
22
- assert.match(html, /<kbd[^>]*>k<\/kbd>/);
23
- assert.match(html, /<kbd[^>]*>Enter<\/kbd>/);
24
- });
25
-
26
- it("can hide the hint line on touch surfaces", () => {
27
- const html = renderToString(
28
- createElement(ReadingPaneEmpty, { showHints: false }) as never,
29
- );
30
- assert.equal(/<kbd/.test(html), false);
31
- });
32
- });
33
-
34
- describe("KeyboardHintBar (#785)", () => {
35
- it("renders the default hint set", () => {
36
- const html = renderToString(createElement(KeyboardHintBar) as never);
37
- assert.match(html, /navigate/);
38
- assert.match(html, /mute/);
39
- assert.match(html, /all shortcuts/);
40
- assert.match(html, /<kbd[^>]*>j<\/kbd>/);
41
- });
42
- });
@@ -1,35 +0,0 @@
1
- /**
2
- * The rescue banner is rendered by SpamRescue above the spam list. These tests
3
- * cover the banner's rendering with a count sourced from the backend (via
4
- * useRescueCandidates), exercising the component's output in isolation without
5
- * app providers.
6
- */
7
-
8
- import assert from "node:assert/strict";
9
- import { describe, it } from "node:test";
10
- import { RescueBanner } from "@remit/ui";
11
- import React, { createElement } from "react";
12
- import { renderToString } from "react-dom/server";
13
-
14
- (globalThis as { React?: typeof React }).React = React;
15
-
16
- const noop = () => {};
17
-
18
- const renderBanner = (count: number): string =>
19
- renderToString(createElement(RescueBanner, { count, onReview: noop }));
20
-
21
- describe("RescueBanner rendering", () => {
22
- it("shows the candidate count for multiple messages", () => {
23
- const html = renderBanner(2);
24
- assert.match(html, /2 messages here/);
25
- });
26
-
27
- it("shows singular phrasing for a single message", () => {
28
- const html = renderBanner(1);
29
- assert.ok(html.includes("1"), "count should appear");
30
- });
31
-
32
- it("renders without throwing for zero count", () => {
33
- assert.doesNotThrow(() => renderBanner(0));
34
- });
35
- });