@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.
- package/package.json +4 -4
- package/src/auth/SignInAgain.tsx +18 -0
- package/src/auth/amplify-config.test.ts +1 -107
- package/src/auth/auth-interceptor.test.ts +0 -6
- package/src/auth/cognito/cognito-token.test.ts +2 -4
- package/src/components/calendar/AgendaStrip.tsx +1 -1
- package/src/components/calendar/CalendarWorkspace.tsx +1 -1
- package/src/components/calendar/ChangeCalendarSuggestion.tsx +1 -1
- package/src/components/calendar/OpenCalendarEvent.tsx +1 -18
- package/src/components/compose/ComposeForm.tsx +11 -6
- package/src/components/compose/FullCompose.tsx +1 -1
- package/src/components/compose/MobileComposeSheet.tsx +1 -2
- package/src/components/compose/compose.stories.tsx +18 -0
- package/src/components/compose/draft-discard-failure.render.test.ts +138 -0
- package/src/components/config-import/config-import.fixtures.ts +8 -9
- package/src/components/config-import/config-import.stories.tsx +2 -2
- package/src/components/layout/MailTopBar.tsx +3 -0
- package/src/components/mail/BriefPane.tsx +0 -3
- package/src/components/mail/ConversationView.tsx +1 -3
- package/src/components/mail/DailyBrief.tsx +5 -28
- package/src/components/mail/FlaggedList.tsx +3 -4
- package/src/components/mail/FlaggedPane.tsx +0 -3
- package/src/components/mail/IntelligenceDrawer.tsx +0 -3
- package/src/components/mail/IntelligencePane.render.test.ts +0 -1
- package/src/components/mail/IntelligencePane.test.ts +1 -95
- package/src/components/mail/IntelligencePane.tsx +3 -93
- package/src/components/mail/MailListHeader.tsx +12 -43
- package/src/components/mail/MailNav.tsx +6 -2
- package/src/components/mail/MailViewChrome.tsx +1 -11
- package/src/components/mail/MailboxPane.tsx +4 -34
- package/src/components/mail/MessageActionMenu.tsx +6 -6
- package/src/components/mail/MessageAttachments.tsx +35 -0
- package/src/components/mail/MessageBody.tsx +1 -0
- package/src/components/mail/MessageBodyErrorBanner.test.ts +0 -30
- package/src/components/mail/MessageCard.tsx +16 -4
- package/src/components/mail/MessageList.run-reporting.test.ts +2 -46
- package/src/components/mail/MessageList.tsx +23 -11
- package/src/components/mail/MessageRow.render.test.ts +0 -7
- package/src/components/mail/MoveToTrigger.tsx +1 -1
- package/src/components/mail/RawMessageView.tsx +1 -1
- package/src/components/mail/SelectionWizardHost.search-entry.test.ts +0 -1
- package/src/components/mail/SelectionWizardHost.tsx +16 -102
- package/src/components/mail/authenticity-warning.stories.tsx +0 -21
- package/src/components/mail/daily-brief.stories.tsx +31 -17
- package/src/components/mail/enter-cursor-late-open.render.test.ts +197 -0
- package/src/components/mail/intelligence-drawer.stories.tsx +0 -2
- package/src/components/mail/message-gone.stories.tsx +44 -0
- package/src/components/mail/offer-times-reply.render.test.ts +25 -1
- package/src/components/mail/reclassify-remove-override.render.test.ts +2 -6
- package/src/components/overlay-escape.render.test.ts +7 -3
- package/src/components/settings/AccountFormPanel.render.test.ts +26 -3
- package/src/components/settings/AccountFormPanel.test.ts +1 -36
- package/src/components/settings/AccountFormPanel.tsx +5 -25
- package/src/components/settings/AccountServices.tsx +1 -1
- package/src/components/settings/AdvancedNavIcon.tsx +3 -3
- package/src/components/settings/CalendarFeedCard.tsx +2 -1
- package/src/components/settings/CalendarSubscribeCard.tsx +1 -1
- package/src/components/settings/CalendarSubscriptionStatus.tsx +1 -2
- package/src/components/settings/FilterActionError.tsx +1 -1
- package/src/components/settings/FilterEditor.render.test.ts +3 -27
- package/src/components/settings/FilterEditor.tsx +4 -24
- package/src/components/settings/FilterEditorSurface.tsx +0 -3
- package/src/components/settings/FiltersList.render.test.ts +5 -24
- package/src/components/settings/FiltersList.stories.tsx +0 -1
- package/src/components/settings/FiltersList.tsx +10 -35
- package/src/components/settings/WarningsNavIcon.tsx +12 -0
- package/src/components/settings/{QuarantinePanel.delimiter.test.ts → WarningsPanel.dom.test.ts} +64 -5
- package/src/components/settings/WarningsPanel.render.test.ts +56 -0
- package/src/components/settings/WarningsPanel.tsx +244 -0
- package/src/components/settings/accounts.stories.tsx +18 -0
- package/src/components/settings/advanced.stories.tsx +3 -125
- package/src/components/settings/settings-filter.stories.tsx +2 -42
- package/src/components/settings/warnings.stories.tsx +290 -0
- package/src/components/ui/ErrorBannerProvider.tsx +5 -3
- package/src/components/ui/error-banners.test.ts +1 -22
- package/src/components/ui/error-banners.ts +9 -20
- package/src/hooks/calendar/useCalendarCollectionWrites.ts +2 -1
- package/src/hooks/calendar/useCalendarSuggestions.ts +2 -1
- package/src/hooks/calendar/useCalendarWrites.ts +2 -1
- package/src/hooks/intelligence-calendar.render.test.ts +0 -1
- package/src/hooks/useDeleteMessages.test.ts +1 -42
- package/src/hooks/useFilters.test.ts +0 -8
- package/src/hooks/useFollowFocusOpen.render.test.ts +1 -11
- package/src/hooks/useIntelligenceData.test.ts +9 -47
- package/src/hooks/useIntelligenceData.ts +7 -100
- package/src/hooks/useLayoutTier.test.ts +0 -48
- package/src/hooks/useReportSpam.ts +1 -1
- package/src/hooks/useRescueCandidates.test.ts +0 -23
- package/src/hooks/useRulePreview.ts +8 -7
- package/src/hooks/useSearchTokenContext.ts +2 -3
- package/src/hooks/useStaleAccountSync.test.ts +0 -17
- package/src/hooks/useStarredThreads.test.ts +0 -10
- package/src/hooks/useTriggerSync.test.ts +0 -6
- package/src/hooks/useVisualViewport.render.test.ts +63 -0
- package/src/hooks/useWarningsAttention.ts +37 -0
- package/src/lib/account-order.test.ts +0 -10
- package/src/lib/adjacent-message.test.ts +0 -9
- package/src/lib/app-info.ts +0 -3
- package/src/lib/autodiscovery.test.ts +0 -47
- package/src/lib/bug-report.test.ts +38 -0
- package/src/lib/bug-report.ts +10 -7
- package/src/lib/bulk-action-copy.ts +1 -1
- package/src/lib/calendar-route.test.ts +0 -5
- package/src/lib/config-import.test.ts +3 -39
- package/src/lib/dedupe-thread-messages.test.ts +0 -4
- package/src/lib/desktop-breakpoint.test.ts +0 -8
- package/src/lib/drafts.test.ts +0 -23
- package/src/lib/fatal-error.test.ts +0 -19
- package/src/lib/folder-roles.test.ts +0 -4
- package/src/lib/hot-sync-window.test.ts +0 -5
- package/src/lib/list-header-chrome.ts +1 -1
- package/src/lib/mail-context.ts +1 -1
- package/src/lib/message-attachments.test.ts +77 -0
- package/src/lib/message-attachments.ts +31 -15
- package/src/lib/message-body-source.test.ts +21 -4
- package/src/lib/message-body-source.ts +5 -2
- package/src/lib/onboarding-completion.test.ts +0 -16
- package/src/lib/organize/filter-edit-model.test.ts +2 -32
- package/src/lib/organize/filter-edit-model.ts +6 -16
- package/src/lib/organize/organize-model.test.ts +7 -40
- package/src/lib/organize/organize-model.ts +16 -18
- package/src/lib/organize/rule-model.test.ts +24 -202
- package/src/lib/organize/rule-model.ts +19 -166
- package/src/lib/organize/search-to-rule.test.ts +2 -36
- package/src/lib/organize/search-to-rule.ts +1 -16
- package/src/lib/provider-presets.test.ts +1 -77
- package/src/lib/query-error-handler.test.ts +0 -61
- package/src/lib/query-escalation.integration.test.ts +4 -4
- package/src/lib/result-count.ts +1 -1
- package/src/lib/result-folder.ts +1 -1
- package/src/lib/rum-adapter.test.ts +0 -104
- package/src/lib/search-query.test.ts +0 -11
- package/src/lib/search-result.test.ts +2 -142
- package/src/lib/search-result.ts +3 -73
- package/src/lib/search-scope.test.ts +1 -92
- package/src/lib/search-scope.ts +0 -29
- package/src/lib/search-surface.ts +2 -2
- package/src/lib/search-token-index.test.ts +0 -8
- package/src/lib/search-tokens.ts +15 -16
- package/src/lib/starred-rows.test.ts +0 -4
- package/src/lib/sync-warning-entries.ts +33 -0
- package/src/lib/wizard-selection.ts +1 -1
- package/src/mocks/story-frame/mail-handlers.ts +12 -8
- package/src/mocks/story-frame/mail-world.ts +5 -0
- package/src/routeTree.gen.ts +21 -0
- package/src/routes/calendar/$view.$date/index.tsx +1 -1
- package/src/routes/calendar.tsx +2 -1
- package/src/routes/mail/brief.tsx +1 -1
- package/src/routes/mail/flagged.tsx +1 -1
- package/src/routes/mail.tsx +5 -3
- package/src/routes/settings/-accounts.test.ts +0 -5
- package/src/routes/settings/-filters.test.ts +0 -1
- package/src/routes/settings/-shared/help-copy.tsx +15 -0
- package/src/routes/settings/accounts.tsx +1 -1
- package/src/routes/settings/advanced.tsx +1 -4
- package/src/routes/settings/filters.tsx +1 -2
- package/src/routes/settings/folders.tsx +1 -1
- package/src/routes/settings/labels.tsx +1 -1
- package/src/routes/settings/senders.tsx +1 -1
- package/src/routes/settings/warnings.tsx +36 -0
- package/src/routes/settings.tsx +7 -0
- package/src/routing/wizard-history.test.ts +1 -45
- package/src/runtime-config.ts +5 -0
- package/src/test-support/fixtures.ts +1 -1
- package/src/components/compose/draft-discard-banner.test.ts +0 -76
- package/src/components/mail/ReadingPaneEmpty.render.test.ts +0 -42
- package/src/components/mail/SpamRescue.render.test.ts +0 -35
- package/src/components/mail/intelligence-semantic-off.render.test.ts +0 -111
- package/src/components/mail/selection-surface-rollcall.test.ts +0 -270
- package/src/components/settings/QuarantinePanel.render.test.ts +0 -71
- package/src/components/settings/QuarantinePanel.tsx +0 -178
- package/src/components/ui/DropdownMenu.tsx +0 -99
- package/src/components/ui/EmptyState.tsx +0 -11
- package/src/components/ui/ErrorBanner.render.test.ts +0 -104
- package/src/components/ui/ErrorBanner.stories.tsx +0 -132
- package/src/components/ui/ErrorBanner.tsx +0 -106
- package/src/components/ui/ErrorBannerStack.tsx +0 -35
- package/src/components/ui/ErrorState.render.test.ts +0 -82
- package/src/components/ui/ErrorState.tsx +0 -80
- package/src/components/ui/KeyboardShortcutsModal.tsx +0 -113
- package/src/hooks/useMoveMessages.test.ts +0 -63
- package/src/hooks/useOrganizeWiden.test.ts +0 -217
- package/src/hooks/useOrganizeWiden.ts +0 -112
- package/src/hooks/useSemanticSearch.ts +0 -132
- package/src/hooks/useSemanticSearchEnabled.ts +0 -23
- package/src/hooks/useVisualViewport.test.ts +0 -126
- package/src/lib/organize/sender-fallback.test.ts +0 -52
- package/src/lib/organize/sender-fallback.ts +0 -38
- package/src/lib/rescue-telemetry.test.ts +0 -75
- package/src/router.test.ts +0 -81
|
@@ -91,7 +91,6 @@ import { useRefreshControl } from "@/hooks/useRefreshControl";
|
|
|
91
91
|
import { useRescueCandidates } from "@/hooks/useRescueCandidates";
|
|
92
92
|
import { useResultCount } from "@/hooks/useResultCount";
|
|
93
93
|
import { useSearchTokenContext } from "@/hooks/useSearchTokenContext";
|
|
94
|
-
import { useSemanticSearch } from "@/hooks/useSemanticSearch";
|
|
95
94
|
import { useThreadActions } from "@/hooks/useThreadActions";
|
|
96
95
|
import { useThreadMessageIds } from "@/hooks/useThreadMessageIds";
|
|
97
96
|
import { useThreadRow } from "@/hooks/useThreadRow";
|
|
@@ -116,10 +115,7 @@ import { isRescueCandidate } from "@/lib/rescue-candidates";
|
|
|
116
115
|
import { recordRescueSentToJunk } from "@/lib/rescue-telemetry";
|
|
117
116
|
import { shouldRequestResultCount } from "@/lib/result-count";
|
|
118
117
|
import { normalizeSearchQuery } from "@/lib/search-query";
|
|
119
|
-
import {
|
|
120
|
-
relatedSearchResults,
|
|
121
|
-
threadToSearchResult,
|
|
122
|
-
} from "@/lib/search-result";
|
|
118
|
+
import { threadToSearchResult } from "@/lib/search-result";
|
|
123
119
|
import { parseSearchTokens } from "@/lib/search-tokens";
|
|
124
120
|
import { useTelemetry } from "@/lib/telemetry-context";
|
|
125
121
|
import {
|
|
@@ -961,33 +957,14 @@ function MailboxList() {
|
|
|
961
957
|
threads.map((thread) => threadToSearchResult(thread, resultFolderIndex)),
|
|
962
958
|
[threads, resultFolderIndex],
|
|
963
959
|
);
|
|
964
|
-
// The
|
|
965
|
-
// here respects it: the literal engine searches this mailbox, and the
|
|
966
|
-
// semantic engine takes the same `mailboxId`. No chip means global; a chip
|
|
967
|
-
// means nothing on the route reaches past it. Results are deduped by thread,
|
|
968
|
-
// so a thread never shows in both sections.
|
|
969
|
-
const { hits: semanticHits, isLoading: relatedLoading } = useSemanticSearch({
|
|
970
|
-
mailboxId,
|
|
971
|
-
filterCategory,
|
|
972
|
-
});
|
|
973
|
-
const relatedResults = useMemo(
|
|
974
|
-
() =>
|
|
975
|
-
relatedSearchResults(
|
|
976
|
-
semanticHits,
|
|
977
|
-
threads.map((t) => t.threadId),
|
|
978
|
-
resultFolderIndex,
|
|
979
|
-
),
|
|
980
|
-
[semanticHits, threads, resultFolderIndex],
|
|
981
|
-
);
|
|
982
|
-
// The two-engine results panel, whose semantic hits are in no list at all —
|
|
983
|
-
// each one carries the thread it belongs to.
|
|
960
|
+
// The results panel; each result carries the thread it belongs to.
|
|
984
961
|
const handleSelectSearchResult = useCallback(
|
|
985
962
|
(result: SearchResult) => {
|
|
986
963
|
const threadId =
|
|
987
964
|
result.threadId ??
|
|
988
965
|
threads.find((thread) => thread.messageId === result.id)?.threadId;
|
|
989
966
|
if (!threadId) return;
|
|
990
|
-
//
|
|
967
|
+
// The search is scoped to this mailbox, so a result's own mailbox is
|
|
991
968
|
// normally this one; keep reading it off the result so a row can never
|
|
992
969
|
// open under a mailbox it does not belong to.
|
|
993
970
|
openThread(
|
|
@@ -1096,13 +1073,11 @@ function MailboxList() {
|
|
|
1096
1073
|
onClearFilters={onClearFilters}
|
|
1097
1074
|
searchResults={searchResults}
|
|
1098
1075
|
searchLoading={isLoading}
|
|
1099
|
-
relatedResults={relatedResults}
|
|
1100
|
-
relatedLoading={relatedLoading}
|
|
1101
1076
|
onSelectSearchResult={handleSelectSearchResult}
|
|
1102
1077
|
// A committed mailbox search renders in the body's own `MessageList`
|
|
1103
1078
|
// (its threads filter to the results), so the multi-select toolbar and
|
|
1104
1079
|
// the "Select all N matching" escalation are reachable on desktop (#212).
|
|
1105
|
-
// The typing/uncommitted state still shows the
|
|
1080
|
+
// The typing/uncommitted state still shows the results panel.
|
|
1106
1081
|
searchResultsInBody
|
|
1107
1082
|
refreshControl={refreshControl}
|
|
1108
1083
|
>
|
|
@@ -1204,7 +1179,6 @@ function MailboxReading() {
|
|
|
1204
1179
|
isOpen={intelligence.drawerOpen}
|
|
1205
1180
|
onClose={intelligence.closeDrawer}
|
|
1206
1181
|
thread={selectedThread}
|
|
1207
|
-
mailboxId={mailboxId}
|
|
1208
1182
|
accountId={mailboxAccountId}
|
|
1209
1183
|
onAfterOptimisticRemove={handleDeselectIfRemoved}
|
|
1210
1184
|
onReplyWithText={replyWithText}
|
|
@@ -1221,7 +1195,6 @@ function MailboxReading() {
|
|
|
1221
1195
|
function MailboxIntelligence() {
|
|
1222
1196
|
const replyWithText = useReplyWithText();
|
|
1223
1197
|
const {
|
|
1224
|
-
mailboxId,
|
|
1225
1198
|
mailboxAccountId,
|
|
1226
1199
|
selectedThread,
|
|
1227
1200
|
onToggleIntelligence,
|
|
@@ -1233,7 +1206,6 @@ function MailboxIntelligence() {
|
|
|
1233
1206
|
<IntelligencePane
|
|
1234
1207
|
onClose={onToggleIntelligence}
|
|
1235
1208
|
thread={selectedThread}
|
|
1236
|
-
mailboxId={mailboxId}
|
|
1237
1209
|
accountId={mailboxAccountId}
|
|
1238
1210
|
onAfterOptimisticRemove={handleDeselectIfRemoved}
|
|
1239
1211
|
onReplyWithText={replyWithText}
|
|
@@ -1249,7 +1221,6 @@ function MailboxIntelligence() {
|
|
|
1249
1221
|
function MailboxPhone() {
|
|
1250
1222
|
const replyWithText = useReplyWithText();
|
|
1251
1223
|
const {
|
|
1252
|
-
mailboxId,
|
|
1253
1224
|
mailboxAccountId,
|
|
1254
1225
|
selectedThread,
|
|
1255
1226
|
conversation,
|
|
@@ -1288,7 +1259,6 @@ function MailboxPhone() {
|
|
|
1288
1259
|
isOpen={drawer.isOpen}
|
|
1289
1260
|
onClose={drawer.close}
|
|
1290
1261
|
thread={selectedThread}
|
|
1291
|
-
mailboxId={mailboxId}
|
|
1292
1262
|
accountId={mailboxAccountId}
|
|
1293
1263
|
onAfterOptimisticRemove={handleDeselectIfRemoved}
|
|
1294
1264
|
onReplyWithText={replyWithText}
|
|
@@ -2,6 +2,12 @@ import {
|
|
|
2
2
|
messageBulkOperationsUpdateFlagsMutation,
|
|
3
3
|
threadDetailOperationsListThreadMessagesQueryKey,
|
|
4
4
|
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
5
|
+
import {
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuSeparator,
|
|
9
|
+
ErrorState,
|
|
10
|
+
} from "@remit/ui";
|
|
5
11
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
6
12
|
import {
|
|
7
13
|
BadgeCheck,
|
|
@@ -12,13 +18,7 @@ import {
|
|
|
12
18
|
Trash2,
|
|
13
19
|
} from "lucide-react";
|
|
14
20
|
import { useCallback } from "react";
|
|
15
|
-
import {
|
|
16
|
-
DropdownMenu,
|
|
17
|
-
DropdownMenuItem,
|
|
18
|
-
DropdownMenuSeparator,
|
|
19
|
-
} from "@/components/ui/DropdownMenu";
|
|
20
21
|
import { useErrorBanners } from "@/components/ui/ErrorBannerProvider";
|
|
21
|
-
import { ErrorState } from "@/components/ui/ErrorState";
|
|
22
22
|
import { formatErrorDetail } from "@/components/ui/error-banners";
|
|
23
23
|
import { useDeleteMessages } from "@/hooks/useDeleteMessages";
|
|
24
24
|
import { setReadOnItems } from "@/hooks/useMarkAsRead";
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
RemitImapBodyPartResponse,
|
|
4
4
|
RemitImapDescribeMessageResponse,
|
|
5
5
|
} from "@remit/api-http-client/types.gen.ts";
|
|
6
|
+
import { encapsulatedParts } from "@remit/data-ports/message-parts";
|
|
6
7
|
import { type AttachmentDownloadState, AttachmentList } from "@remit/ui";
|
|
7
8
|
import { useQueryClient } from "@tanstack/react-query";
|
|
8
9
|
import { useMemo, useState } from "react";
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
type MessageAttachment,
|
|
21
22
|
selectMessageAttachments,
|
|
22
23
|
} from "@/lib/message-attachments";
|
|
24
|
+
import { MessageBody } from "./MessageBody";
|
|
23
25
|
|
|
24
26
|
interface MessageAttachmentsProps {
|
|
25
27
|
messageId: string;
|
|
@@ -38,6 +40,9 @@ const IDLE: AttachmentDownloadState = { status: "idle" };
|
|
|
38
40
|
* Download state is per row and lives here rather than in a query: a download is
|
|
39
41
|
* a one-shot user action with no cached result to hold, and the bytes are handed
|
|
40
42
|
* to the browser rather than rendered.
|
|
43
|
+
*
|
|
44
|
+
* A forwarded message also opens in place: its own parts render through the
|
|
45
|
+
* same body and attachment components as the message carrying it.
|
|
41
46
|
*/
|
|
42
47
|
export const MessageAttachments = ({
|
|
43
48
|
messageId,
|
|
@@ -50,6 +55,7 @@ export const MessageAttachments = ({
|
|
|
50
55
|
const [downloads, setDownloads] = useState<
|
|
51
56
|
Record<string, AttachmentDownloadState>
|
|
52
57
|
>({});
|
|
58
|
+
const [openedId, setOpenedId] = useState<string | null>(null);
|
|
53
59
|
|
|
54
60
|
const attachments = useMemo(
|
|
55
61
|
() => selectMessageAttachments(bodyParts ?? []),
|
|
@@ -136,11 +142,20 @@ export const MessageAttachments = ({
|
|
|
136
142
|
const hasUnlistedAttachment =
|
|
137
143
|
hasAttachment && bodyParts !== undefined && attachments.length === 0;
|
|
138
144
|
|
|
145
|
+
const openedMessage = attachments.find(
|
|
146
|
+
(attachment) =>
|
|
147
|
+
attachment.kind === "message" && attachment.bodyPartId === openedId,
|
|
148
|
+
);
|
|
149
|
+
const openedParts = openedMessage
|
|
150
|
+
? encapsulatedParts(bodyParts ?? [], openedMessage.partPath)
|
|
151
|
+
: [];
|
|
152
|
+
|
|
139
153
|
return (
|
|
140
154
|
<AttachmentList
|
|
141
155
|
className={className}
|
|
142
156
|
attachments={attachments.map((attachment) => ({
|
|
143
157
|
attachmentId: attachment.bodyPartId,
|
|
158
|
+
kind: attachment.kind,
|
|
144
159
|
filename: attachment.filename,
|
|
145
160
|
typeLabel: attachment.typeLabel,
|
|
146
161
|
sizeOctets: attachment.sizeOctets,
|
|
@@ -148,6 +163,26 @@ export const MessageAttachments = ({
|
|
|
148
163
|
}))}
|
|
149
164
|
onDownload={download}
|
|
150
165
|
hasUnlistedAttachment={hasUnlistedAttachment}
|
|
166
|
+
onToggleOpen={(bodyPartId) =>
|
|
167
|
+
setOpenedId((current) => (current === bodyPartId ? null : bodyPartId))
|
|
168
|
+
}
|
|
169
|
+
opened={
|
|
170
|
+
openedMessage
|
|
171
|
+
? {
|
|
172
|
+
attachmentId: openedMessage.bodyPartId,
|
|
173
|
+
content: (
|
|
174
|
+
<>
|
|
175
|
+
<MessageBody bodyParts={openedParts} messageId={messageId} />
|
|
176
|
+
<MessageAttachments
|
|
177
|
+
messageId={messageId}
|
|
178
|
+
bodyParts={openedParts}
|
|
179
|
+
className="mt-4"
|
|
180
|
+
/>
|
|
181
|
+
</>
|
|
182
|
+
),
|
|
183
|
+
}
|
|
184
|
+
: undefined
|
|
185
|
+
}
|
|
151
186
|
/>
|
|
152
187
|
);
|
|
153
188
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
|
-
import { BodyFetchError } from "@/hooks/useMessageBodyContent";
|
|
4
3
|
import {
|
|
5
4
|
contentForReason,
|
|
6
5
|
extractFallbackDetail,
|
|
@@ -8,18 +7,6 @@ import {
|
|
|
8
7
|
} from "./message-body-error-banner-content";
|
|
9
8
|
|
|
10
9
|
describe("contentForReason — diagnostic banner copy per failure mode (#401)", () => {
|
|
11
|
-
it("renders a session-expired title for auth failures (Lambda@Edge 401/403 with x-remit-403-reason header)", () => {
|
|
12
|
-
const content = contentForReason("auth", "ignored fallback");
|
|
13
|
-
assert.match(content.title, /session/i);
|
|
14
|
-
assert.match(content.detail, /sign in/i);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("renders a body-missing-in-storage title for bare S3 403/404 (no edge header — OAC blocks list so missing keys 403)", () => {
|
|
18
|
-
const content = contentForReason("body-missing", "ignored fallback");
|
|
19
|
-
assert.match(content.title, /missing in storage/i);
|
|
20
|
-
assert.match(content.detail, /reconcile|sync|support/i);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
10
|
it("renders a generic title for other 4xx/5xx and falls back to the error's own message in the detail", () => {
|
|
24
11
|
const content = contentForReason("generic", "Network connection lost");
|
|
25
12
|
assert.match(content.title, /couldn.?t load/i);
|
|
@@ -37,26 +24,9 @@ describe("contentForReason — diagnostic banner copy per failure mode (#401)",
|
|
|
37
24
|
assert.match(content.title, /couldn.?t load/i);
|
|
38
25
|
assert.match(content.detail, /shell|infrastructure|report/i);
|
|
39
26
|
});
|
|
40
|
-
|
|
41
|
-
it("never reuses the auth copy for body-missing — these must be visibly distinct so the user knows which failure they're facing (#401 acceptance)", () => {
|
|
42
|
-
const auth = contentForReason("auth", "x");
|
|
43
|
-
const missing = contentForReason("body-missing", "x");
|
|
44
|
-
assert.notEqual(auth.title, missing.title);
|
|
45
|
-
assert.notEqual(auth.detail, missing.detail);
|
|
46
|
-
});
|
|
47
27
|
});
|
|
48
28
|
|
|
49
29
|
describe("extractReason — pulls the discriminated reason off a BodyFetchError, falls back to generic", () => {
|
|
50
|
-
it("returns the reason from a BodyFetchError instance", () => {
|
|
51
|
-
const err = new BodyFetchError("body-missing", "missing", 403);
|
|
52
|
-
assert.equal(extractReason(err), "body-missing");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("returns the reason from a BodyFetchError(auth)", () => {
|
|
56
|
-
const err = new BodyFetchError("auth", "Invalid id_token", 401);
|
|
57
|
-
assert.equal(extractReason(err), "auth");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
30
|
it("falls back to generic for a plain Error (e.g. a network error before fetch returned)", () => {
|
|
61
31
|
assert.equal(extractReason(new Error("offline")), "generic");
|
|
62
32
|
});
|
|
@@ -4,13 +4,21 @@ import type {
|
|
|
4
4
|
RemitImapThreadMessageResponse,
|
|
5
5
|
} from "@remit/api-http-client/types.gen.ts";
|
|
6
6
|
import type { ThreadMessageData } from "@remit/ui";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
AddressList,
|
|
9
|
+
CollapsedMessage,
|
|
10
|
+
cn,
|
|
11
|
+
EmptyState,
|
|
12
|
+
ErrorState,
|
|
13
|
+
ExpandedMessage,
|
|
14
|
+
} from "@remit/ui";
|
|
8
15
|
import { useQuery } from "@tanstack/react-query";
|
|
9
16
|
import { BadgeCheck, Paperclip, Star } from "lucide-react";
|
|
10
17
|
import { useState } from "react";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
import {
|
|
19
|
+
isMessageGoneFromServerError,
|
|
20
|
+
isMessageNotFoundError,
|
|
21
|
+
} from "@/components/ui/error-banners";
|
|
14
22
|
import { toDisplayCategory } from "@/lib/display-category";
|
|
15
23
|
import { formatDatePreset } from "@/lib/format";
|
|
16
24
|
import { AutoMovedIndicator } from "./AutoMovedIndicator";
|
|
@@ -310,6 +318,10 @@ const ExpandedCard = ({
|
|
|
310
318
|
<div className="h-4 bg-surface-sunken rounded w-3/4" />
|
|
311
319
|
<div className="h-4 bg-surface-sunken rounded w-1/2" />
|
|
312
320
|
</div>
|
|
321
|
+
) : isError && isMessageGoneFromServerError(error) ? (
|
|
322
|
+
<div className="mt-3">
|
|
323
|
+
<EmptyState message="This message is no longer on the mail server. It was moved or deleted in another mail app." />
|
|
324
|
+
</div>
|
|
313
325
|
) : isError && isMessageNotFoundError(error) ? (
|
|
314
326
|
<div className="mt-3">
|
|
315
327
|
<EmptyState message="This message has been deleted" />
|
|
@@ -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 { runEndingBanner } from "@/lib/bulk-action-copy";
|
|
7
4
|
import type { BulkRunOutcome } from "@/lib/bulk-actions";
|
|
8
5
|
import { type DeleteOutcome, deleteExpunges } from "@/lib/format";
|
|
@@ -13,22 +10,10 @@ import { type DeleteOutcome, deleteExpunges } from "@/lib/format";
|
|
|
13
10
|
* the run with no screen of its own — so the list says what it reached, whether
|
|
14
11
|
* that was everything or a hundred out of three thousand.
|
|
15
12
|
*
|
|
16
|
-
* Which ending gets which banner is `runEndingBanner`,
|
|
17
|
-
*
|
|
18
|
-
* `reportRunOutcome`, which meant the rule held only as long as nobody moved
|
|
19
|
-
* those lines — adding an argument to the completion call broke all three
|
|
20
|
-
* cases while the behaviour they protect was intact. The one fact that still
|
|
21
|
-
* cannot be read off a result is the wiring: that the wizard is handed
|
|
22
|
-
* somewhere to report an ending after it has closed. That stays a source read,
|
|
23
|
-
* because reaching it otherwise means mounting the list's virtualizer, router
|
|
24
|
-
* and data hooks around a run that has already finished.
|
|
25
|
-
*
|
|
26
|
-
* Which runs reach this seam at all is `SelectionWizardHost.run-exit.test.ts`.
|
|
13
|
+
* Which ending gets which banner is `runEndingBanner`, asserted here by its
|
|
14
|
+
* result.
|
|
27
15
|
*/
|
|
28
16
|
|
|
29
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
30
|
-
const source = readFileSync(resolve(here, "MessageList.tsx"), "utf8");
|
|
31
|
-
|
|
32
17
|
const expunging: DeleteOutcome[] = ["permanent", "unconfirmed"];
|
|
33
18
|
|
|
34
19
|
const ended = (over: Partial<BulkRunOutcome> = {}): BulkRunOutcome => ({
|
|
@@ -40,21 +25,6 @@ const ended = (over: Partial<BulkRunOutcome> = {}): BulkRunOutcome => ({
|
|
|
40
25
|
});
|
|
41
26
|
|
|
42
27
|
describe("reporting how a run ended", () => {
|
|
43
|
-
it("hands the wizard somewhere to report an ending it can no longer show", () => {
|
|
44
|
-
const host = source.match(/<SelectionWizardHost[\s\S]*?\/>/)?.[0] ?? "";
|
|
45
|
-
assert.match(host, /onRunEnded=\{reportRunOutcome\}/);
|
|
46
|
-
assert.match(
|
|
47
|
-
source,
|
|
48
|
-
/const reportRunOutcome = useCallback\(/,
|
|
49
|
-
"the list reports no run outcome",
|
|
50
|
-
);
|
|
51
|
-
assert.match(
|
|
52
|
-
source,
|
|
53
|
-
/runEndingBanner\(kind, matched, outcome, deleteOutcome\)/,
|
|
54
|
-
"and the ending it reports is the one runEndingBanner decides",
|
|
55
|
-
);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
28
|
it("names both endings: what it covered, and what it stopped short of", () => {
|
|
59
29
|
const stopped = runEndingBanner(
|
|
60
30
|
"delete",
|
|
@@ -115,17 +85,3 @@ describe("reporting how a run ended", () => {
|
|
|
115
85
|
});
|
|
116
86
|
}
|
|
117
87
|
});
|
|
118
|
-
|
|
119
|
-
describe("the run the wizard leaves behind", () => {
|
|
120
|
-
it("is stoppable from the wizard, which is not the same as closing it", () => {
|
|
121
|
-
assert.match(source, /stop: escalation\.stop,/);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("keeps reporting on the bar from the moment it starts", () => {
|
|
125
|
-
assert.match(
|
|
126
|
-
source,
|
|
127
|
-
/escalation\.progress\?\.total \?\? selectionCount/,
|
|
128
|
-
"a run with no batch finished yet has no denominator to show",
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
});
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
cn,
|
|
5
5
|
type Density,
|
|
6
6
|
deriveIsMultiSelectMode,
|
|
7
|
+
formatErrorMessage,
|
|
7
8
|
ListResultHeader,
|
|
8
9
|
type MessageListFilter,
|
|
9
10
|
MessageListLoadingMore,
|
|
@@ -20,7 +21,6 @@ import { useVirtualizer } from "@tanstack/react-virtual";
|
|
|
20
21
|
import type { RefObject } from "react";
|
|
21
22
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
22
23
|
import { useErrorBanners } from "@/components/ui/ErrorBannerProvider";
|
|
23
|
-
import { formatErrorMessage } from "@/components/ui/ErrorState";
|
|
24
24
|
import { useJunkMailbox } from "@/hooks/useArchiveMailbox";
|
|
25
25
|
import { useDeleteOutcome } from "@/hooks/useDeleteOutcome";
|
|
26
26
|
import {
|
|
@@ -501,9 +501,8 @@ export const MessageList = ({
|
|
|
501
501
|
const orderedIds = useMemo(() => threads.map((t) => t.messageId), [threads]);
|
|
502
502
|
|
|
503
503
|
// The checked rows as the wizard reads them: the sample it shows under every
|
|
504
|
-
// screen that names a match, and the senders its
|
|
505
|
-
//
|
|
506
|
-
// so no extra round-trip.
|
|
504
|
+
// screen that names a match, and the senders its properties prefill reads.
|
|
505
|
+
// Read off the already-loaded thread rows, so no extra round-trip.
|
|
507
506
|
const wizardSelection = useMemo<WizardSelectionMessage[]>(() => {
|
|
508
507
|
const rows: WizardSelectionMessage[] = [];
|
|
509
508
|
for (const thread of threads) {
|
|
@@ -673,8 +672,16 @@ export const MessageList = ({
|
|
|
673
672
|
// and the selection toolbar — not a message — is what the user is looking at.
|
|
674
673
|
// Suspended off desktop too, where the reading pane replaces the list
|
|
675
674
|
// entirely, so following the cursor would throw the user off the list.
|
|
675
|
+
//
|
|
676
|
+
// The open lands a navigation later, and the cursor may have moved on by
|
|
677
|
+
// then. The open records which row it was for, so its arrival does not drag
|
|
678
|
+
// the cursor back off the row the user has since moved to.
|
|
679
|
+
const followOpenedRef = useRef<string | undefined>(undefined);
|
|
676
680
|
const followFocusOpen = useCallback(
|
|
677
|
-
(messageId: string) =>
|
|
681
|
+
(messageId: string) => {
|
|
682
|
+
followOpenedRef.current = messageId;
|
|
683
|
+
openRow(messageId, { replace: true });
|
|
684
|
+
},
|
|
678
685
|
[openRow],
|
|
679
686
|
);
|
|
680
687
|
useFollowFocusOpen({
|
|
@@ -877,13 +884,18 @@ export const MessageList = ({
|
|
|
877
884
|
// Opening a thread (click or Enter, anywhere) seeds the focus cursor onto it
|
|
878
885
|
// so subsequent j/k continue from the open row — focus and open stay in
|
|
879
886
|
// sync on open while remaining independent during scanning.
|
|
887
|
+
// A row the cursor itself opened is the exception: the cursor was already on
|
|
888
|
+
// it, and if the user has pressed j again since, Enter must act on the row
|
|
889
|
+
// they are on now rather than the one the reading pane caught up to.
|
|
880
890
|
useEffect(() => {
|
|
881
|
-
if (selectedMessageId)
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
891
|
+
if (!selectedMessageId) return;
|
|
892
|
+
const openedByCursor = followOpenedRef.current === selectedMessageId;
|
|
893
|
+
followOpenedRef.current = undefined;
|
|
894
|
+
if (openedByCursor) return;
|
|
895
|
+
// A thread going open is the list's own move, whatever opened it — a
|
|
896
|
+
// deep link arrives with the row far down and has to be scrolled to.
|
|
897
|
+
cursorMovedByPointerRef.current = false;
|
|
898
|
+
setFocusedMessageId(selectedMessageId);
|
|
887
899
|
}, [selectedMessageId, setFocusedMessageId]);
|
|
888
900
|
|
|
889
901
|
// Keep the focus cursor valid as the thread list changes (after delete /
|
|
@@ -51,11 +51,4 @@ describe("MessageRow semantics", () => {
|
|
|
51
51
|
assert.match(html, /role="option"/);
|
|
52
52
|
assert.match(html, /aria-selected/);
|
|
53
53
|
});
|
|
54
|
-
|
|
55
|
-
it("renders the row's sender, subject and snippet", () => {
|
|
56
|
-
const html = render({ thread });
|
|
57
|
-
assert.match(html, /Alex Rivera/);
|
|
58
|
-
assert.match(html, /Quarterly numbers are in/);
|
|
59
|
-
assert.match(html, /shared drive/);
|
|
60
|
-
});
|
|
61
54
|
});
|
|
@@ -2,6 +2,7 @@ import { mailboxOperationsListMailboxesOptions } from "@remit/api-http-client/@t
|
|
|
2
2
|
import {
|
|
3
3
|
Button,
|
|
4
4
|
cn,
|
|
5
|
+
ErrorState,
|
|
5
6
|
type FolderTreeNode,
|
|
6
7
|
FolderTreePicker,
|
|
7
8
|
PopoverMenuPortal,
|
|
@@ -18,7 +19,6 @@ import {
|
|
|
18
19
|
useState,
|
|
19
20
|
} from "react";
|
|
20
21
|
import { Drawer } from "vaul";
|
|
21
|
-
import { ErrorState } from "@/components/ui/ErrorState";
|
|
22
22
|
import { useFolderAppointments } from "@/hooks/useArchiveMailbox";
|
|
23
23
|
import { useCreateMailbox } from "@/hooks/useCreateMailbox";
|
|
24
24
|
import { useFolderLabelTranslator } from "@/hooks/useFolderLabelTranslator";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { messageOperationsGetRawMessageOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
2
|
+
import { ErrorState } from "@remit/ui";
|
|
2
3
|
import { useQuery } from "@tanstack/react-query";
|
|
3
|
-
import { ErrorState } from "@/components/ui/ErrorState";
|
|
4
4
|
|
|
5
5
|
interface RawMessageViewProps {
|
|
6
6
|
/** The message whose raw RFC822/MIME source to fetch and display. */
|