@remit/web-client 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/harness/build.mjs +84 -0
- package/harness/index.html +37 -0
- package/harness/vite-preset.ts +29 -0
- package/index.html +47 -0
- package/package.json +147 -0
- package/public/config.js +5 -0
- package/public/locales/de/common.json +1 -0
- package/public/locales/de/errors.json +1 -0
- package/public/locales/de/mail.json +13 -0
- package/public/locales/de/settings.json +1 -0
- package/public/locales/en/common.json +26 -0
- package/public/locales/en/errors.json +1 -0
- package/public/locales/en/mail.json +69 -0
- package/public/locales/en/settings.json +1 -0
- package/public/locales/fr/common.json +1 -0
- package/public/locales/fr/errors.json +1 -0
- package/public/locales/fr/mail.json +13 -0
- package/public/locales/fr/settings.json +1 -0
- package/public/locales/nl/common.json +1 -0
- package/public/locales/nl/errors.json +1 -0
- package/public/locales/nl/mail.json +13 -0
- package/public/locales/nl/settings.json +1 -0
- package/src/auth/AccountMenu.tsx +58 -0
- package/src/auth/AccountSession.tsx +19 -0
- package/src/auth/BetterAuthShell.tsx +198 -0
- package/src/auth/account-menu-mode.test.ts +64 -0
- package/src/auth/account-menu-mode.ts +28 -0
- package/src/auth/amplify-config.test.ts +158 -0
- package/src/auth/amplify-config.ts +60 -0
- package/src/auth/auth-error.test.ts +109 -0
- package/src/auth/auth-error.ts +107 -0
- package/src/auth/auth-interceptor.test.ts +86 -0
- package/src/auth/auth-interceptor.ts +23 -0
- package/src/auth/auth.css +334 -0
- package/src/auth/better-auth/BetterAuthAccount.tsx +25 -0
- package/src/auth/better-auth-config.ts +85 -0
- package/src/auth/better-auth-provider.tsx +20 -0
- package/src/auth/cognito/CognitoAccount.tsx +48 -0
- package/src/auth/cognito/CognitoShell.tsx +266 -0
- package/src/auth/cognito/cognito-token.test.ts +110 -0
- package/src/auth/cognito/cognito-token.ts +47 -0
- package/src/auth/cognito/cognito.css +21 -0
- package/src/auth/cognito-provider.tsx +22 -0
- package/src/auth/combined-provider.tsx +42 -0
- package/src/auth/composition.test.ts +136 -0
- package/src/auth/provider.tsx +63 -0
- package/src/auth/sign-out-visibility.test.ts +40 -0
- package/src/auth/sign-out-visibility.ts +10 -0
- package/src/auth/sign-up-disabled.test.ts +23 -0
- package/src/components/compose/AddressField.tsx +237 -0
- package/src/components/compose/ComposeBody.tsx +36 -0
- package/src/components/compose/ComposeForm.tsx +676 -0
- package/src/components/compose/ComposeProvider.tsx +133 -0
- package/src/components/compose/ComposeSmtpMissingBanner.tsx +49 -0
- package/src/components/compose/FromSelector.tsx +67 -0
- package/src/components/compose/FullCompose.tsx +82 -0
- package/src/components/compose/InlineCompose.tsx +29 -0
- package/src/components/compose/MobileComposeSheet.tsx +152 -0
- package/src/components/compose/PlateEditor.tsx +86 -0
- package/src/components/compose/PlateToolbar.tsx +110 -0
- package/src/components/compose/SubjectField.tsx +19 -0
- package/src/components/compose/draft-discard-banner.test.ts +76 -0
- package/src/components/compose/index.ts +6 -0
- package/src/components/compose/sanitize-quote-html.ts +23 -0
- package/src/components/layout/AppShellSkeleton.tsx +53 -0
- package/src/components/layout/ComposeFab.tsx +86 -0
- package/src/components/layout/Drawer.tsx +105 -0
- package/src/components/mail/AutoMovedIndicator.tsx +48 -0
- package/src/components/mail/BlockedImagesNotice.render.test.ts +65 -0
- package/src/components/mail/BlockedImagesNotice.tsx +41 -0
- package/src/components/mail/BriefPane.tsx +289 -0
- package/src/components/mail/ConversationView.tsx +462 -0
- package/src/components/mail/DailyBrief.tsx +451 -0
- package/src/components/mail/DraftsView.tsx +279 -0
- package/src/components/mail/FlaggedList.tsx +170 -0
- package/src/components/mail/FlaggedPane.tsx +217 -0
- package/src/components/mail/IntelligencePane.test.ts +123 -0
- package/src/components/mail/IntelligencePane.tsx +441 -0
- package/src/components/mail/MailListHeader.tsx +187 -0
- package/src/components/mail/MailNav.tsx +70 -0
- package/src/components/mail/MailSidebarAdapter.tsx +351 -0
- package/src/components/mail/MailViewChrome.tsx +95 -0
- package/src/components/mail/MailboxPane.tsx +1219 -0
- package/src/components/mail/MessageActionMenu.tsx +368 -0
- package/src/components/mail/MessageBody.tsx +207 -0
- package/src/components/mail/MessageBodyErrorBanner.render.test.ts +85 -0
- package/src/components/mail/MessageBodyErrorBanner.test.ts +86 -0
- package/src/components/mail/MessageBodyErrorBanner.tsx +90 -0
- package/src/components/mail/MessageCard.tsx +401 -0
- package/src/components/mail/MessageDetail.tsx +153 -0
- package/src/components/mail/MessageList.tsx +804 -0
- package/src/components/mail/MessageListItem.tsx +267 -0
- package/src/components/mail/MessageToolbar.render.test.ts +57 -0
- package/src/components/mail/MessageToolbar.tsx +166 -0
- package/src/components/mail/MobileMessageBar.tsx +142 -0
- package/src/components/mail/MoveToTrigger.tsx +250 -0
- package/src/components/mail/NavMenuButton.tsx +25 -0
- package/src/components/mail/OutboxPane.tsx +531 -0
- package/src/components/mail/PullToRefresh.tsx +24 -0
- package/src/components/mail/RawMessageView.tsx +84 -0
- package/src/components/mail/ReadingPaneEmpty.render.test.ts +42 -0
- package/src/components/mail/SelectionToolbar.tsx +139 -0
- package/src/components/mail/SpamRescue.render.test.ts +35 -0
- package/src/components/mail/SpamRescue.tsx +138 -0
- package/src/components/mail/SwipeableMessageRow.tsx +148 -0
- package/src/components/mail/message-body-error-banner-content.ts +87 -0
- package/src/components/mail/organize/OrganizeDialog.render.test.ts +37 -0
- package/src/components/mail/organize/OrganizeDialog.tsx +83 -0
- package/src/components/mail/organize/OrganizePanel.render.test.ts +102 -0
- package/src/components/mail/organize/OrganizePanel.tsx +344 -0
- package/src/components/onboarding/OnboardingWizard.tsx +1547 -0
- package/src/components/settings/AccountFormPanel.test.ts +72 -0
- package/src/components/settings/AccountFormPanel.tsx +942 -0
- package/src/components/settings/DangerZone.tsx +30 -0
- package/src/components/settings/DeleteAccountDialog.render.test.ts +47 -0
- package/src/components/settings/DeleteAccountDialog.tsx +224 -0
- package/src/components/settings/FiltersList.render.test.ts +71 -0
- package/src/components/settings/FiltersList.tsx +93 -0
- package/src/components/settings/account-form-helpers.test.ts +172 -0
- package/src/components/settings/account-form-helpers.ts +105 -0
- package/src/components/ui/AppVersion.tsx +50 -0
- package/src/components/ui/BugReportButton.tsx +45 -0
- package/src/components/ui/ConfirmDialog.tsx +125 -0
- package/src/components/ui/DropdownMenu.tsx +105 -0
- package/src/components/ui/EmptyState.tsx +11 -0
- package/src/components/ui/ErrorBanner.tsx +88 -0
- package/src/components/ui/ErrorBannerProvider.tsx +77 -0
- package/src/components/ui/ErrorBannerStack.tsx +34 -0
- package/src/components/ui/ErrorState.tsx +80 -0
- package/src/components/ui/FatalErrorOverlay.render.test.ts +66 -0
- package/src/components/ui/FatalErrorOverlay.tsx +151 -0
- package/src/components/ui/KeyboardShortcutsModal.tsx +120 -0
- package/src/components/ui/SlidePanel.tsx +72 -0
- package/src/components/ui/error-banners.test.ts +207 -0
- package/src/components/ui/error-banners.ts +111 -0
- package/src/hooks/queries/keys.ts +38 -0
- package/src/hooks/useArchiveMailbox.ts +104 -0
- package/src/hooks/useAutoMovedBadge.ts +77 -0
- package/src/hooks/useCurrentMailboxName.ts +99 -0
- package/src/hooks/useDebouncedValue.ts +12 -0
- package/src/hooks/useDeleteMessages.test.ts +103 -0
- package/src/hooks/useDeleteMessages.ts +226 -0
- package/src/hooks/useFilters.ts +112 -0
- package/src/hooks/useIntelligenceData.test.ts +269 -0
- package/src/hooks/useIntelligenceData.ts +284 -0
- package/src/hooks/useIsDark.ts +27 -0
- package/src/hooks/useKeyboardNavigation.ts +192 -0
- package/src/hooks/useLayoutTier.test.ts +47 -0
- package/src/hooks/useLayoutTier.ts +44 -0
- package/src/hooks/useLongPress.test.ts +291 -0
- package/src/hooks/useLongPress.ts +87 -0
- package/src/hooks/useMailboxAccount.ts +62 -0
- package/src/hooks/useMailboxNameIndex.ts +32 -0
- package/src/hooks/useMarkAsRead.test.ts +165 -0
- package/src/hooks/useMarkAsRead.ts +319 -0
- package/src/hooks/useMediaQuery.ts +39 -0
- package/src/hooks/useMessageBodyContent.test.ts +370 -0
- package/src/hooks/useMessageBodyContent.ts +380 -0
- package/src/hooks/useMoveMessages.test.ts +63 -0
- package/src/hooks/useMoveMessages.ts +221 -0
- package/src/hooks/useOrganizeJob.ts +90 -0
- package/src/hooks/useOrganizePreview.ts +40 -0
- package/src/hooks/useRescueCandidates.test.ts +217 -0
- package/src/hooks/useRescueCandidates.ts +48 -0
- package/src/hooks/useSaveDraft.ts +118 -0
- package/src/hooks/useSelection.test.ts +75 -0
- package/src/hooks/useSelection.ts +211 -0
- package/src/hooks/useSemanticSearch.ts +94 -0
- package/src/hooks/useSignature.ts +69 -0
- package/src/hooks/useStaleAccountSync.test.ts +211 -0
- package/src/hooks/useStaleAccountSync.ts +246 -0
- package/src/hooks/useSwipeNavigation.test.ts +35 -0
- package/src/hooks/useSwipeNavigation.ts +99 -0
- package/src/hooks/useToggleStar.ts +187 -0
- package/src/hooks/useToggleTrusted.test.ts +102 -0
- package/src/hooks/useToggleTrusted.ts +161 -0
- package/src/hooks/useTriageKeyboard.ts +103 -0
- package/src/hooks/useTriggerSync.test.ts +24 -0
- package/src/hooks/useTriggerSync.ts +58 -0
- package/src/hooks/useUpdateAddressFlags.ts +125 -0
- package/src/hooks/useVisualViewport.test.ts +127 -0
- package/src/hooks/useVisualViewport.ts +51 -0
- package/src/index.css +19 -0
- package/src/lib/account-order.test.ts +84 -0
- package/src/lib/account-order.ts +13 -0
- package/src/lib/adjacent-message.test.ts +43 -0
- package/src/lib/adjacent-message.ts +20 -0
- package/src/lib/api.ts +82 -0
- package/src/lib/app-info.ts +15 -0
- package/src/lib/auto-moved.test.ts +140 -0
- package/src/lib/auto-moved.ts +68 -0
- package/src/lib/autodiscovery.test.ts +94 -0
- package/src/lib/autodiscovery.ts +354 -0
- package/src/lib/brief.test.ts +366 -0
- package/src/lib/brief.ts +180 -0
- package/src/lib/bug-report.test.ts +213 -0
- package/src/lib/bug-report.ts +176 -0
- package/src/lib/client.ts +36 -0
- package/src/lib/console-errors.test.ts +64 -0
- package/src/lib/console-errors.ts +92 -0
- package/src/lib/conversation-target.test.ts +68 -0
- package/src/lib/conversation-target.ts +59 -0
- package/src/lib/display-category.ts +20 -0
- package/src/lib/drafts.test.ts +241 -0
- package/src/lib/drafts.ts +121 -0
- package/src/lib/error-classifier.test.ts +133 -0
- package/src/lib/error-classifier.ts +96 -0
- package/src/lib/fatal-error.test.ts +97 -0
- package/src/lib/fatal-error.ts +165 -0
- package/src/lib/folder-roles.test.ts +113 -0
- package/src/lib/folder-roles.ts +112 -0
- package/src/lib/format.test.ts +115 -0
- package/src/lib/format.ts +198 -0
- package/src/lib/i18n.ts +36 -0
- package/src/lib/intelligence-pref.test.ts +57 -0
- package/src/lib/intelligence-pref.ts +25 -0
- package/src/lib/keymap-dispatch.test.ts +178 -0
- package/src/lib/keymap-dispatch.ts +187 -0
- package/src/lib/keymap.test.ts +49 -0
- package/src/lib/keymap.ts +178 -0
- package/src/lib/mail-context.ts +64 -0
- package/src/lib/mail-route.test.ts +80 -0
- package/src/lib/mail-route.ts +44 -0
- package/src/lib/message-body-source.test.ts +98 -0
- package/src/lib/message-body-source.ts +48 -0
- package/src/lib/move-targets.test.ts +195 -0
- package/src/lib/move-targets.ts +74 -0
- package/src/lib/onboarding-completion.test.ts +64 -0
- package/src/lib/onboarding-completion.ts +23 -0
- package/src/lib/organize/filter-status.test.ts +99 -0
- package/src/lib/organize/filter-status.ts +86 -0
- package/src/lib/organize/organize-copy.test.ts +93 -0
- package/src/lib/organize/organize-copy.ts +61 -0
- package/src/lib/organize/organize-model.test.ts +104 -0
- package/src/lib/organize/organize-model.ts +105 -0
- package/src/lib/organize/organize-poll.test.ts +37 -0
- package/src/lib/organize/organize-poll.ts +28 -0
- package/src/lib/outbox-status.test.ts +64 -0
- package/src/lib/outbox-status.ts +75 -0
- package/src/lib/plate-serializer.ts +164 -0
- package/src/lib/provider-presets.test.ts +96 -0
- package/src/lib/provider-presets.ts +81 -0
- package/src/lib/query-error-handler.test.ts +144 -0
- package/src/lib/query-error-handler.ts +34 -0
- package/src/lib/query-escalation.integration.test.ts +138 -0
- package/src/lib/recent-searches.test.ts +53 -0
- package/src/lib/recent-searches.ts +34 -0
- package/src/lib/rescue-candidates.test.ts +94 -0
- package/src/lib/rescue-candidates.ts +35 -0
- package/src/lib/rescue-telemetry.test.ts +75 -0
- package/src/lib/rescue-telemetry.ts +55 -0
- package/src/lib/rum-adapter.test.ts +299 -0
- package/src/lib/rum-adapter.ts +107 -0
- package/src/lib/saved-searches.test.ts +80 -0
- package/src/lib/saved-searches.ts +52 -0
- package/src/lib/search-pending.test.ts +61 -0
- package/src/lib/search-pending.ts +24 -0
- package/src/lib/search-query.test.ts +35 -0
- package/src/lib/search-query.ts +11 -0
- package/src/lib/search-result.test.ts +160 -0
- package/src/lib/search-result.ts +102 -0
- package/src/lib/search-schemas.ts +34 -0
- package/src/lib/search-token-index.test.ts +112 -0
- package/src/lib/search-token-index.ts +56 -0
- package/src/lib/search-tokens.test.ts +218 -0
- package/src/lib/search-tokens.ts +133 -0
- package/src/lib/telemetry-context.tsx +9 -0
- package/src/lib/telemetry.ts +17 -0
- package/src/lib/theme-preference.ts +46 -0
- package/src/lib/theme.ts +33 -0
- package/src/lib/utils.ts +4 -0
- package/src/main.tsx +10 -0
- package/src/routeTree.gen.ts +392 -0
- package/src/router.test.ts +81 -0
- package/src/router.tsx +31 -0
- package/src/routes/__root.tsx +69 -0
- package/src/routes/index.tsx +7 -0
- package/src/routes/mail/$mailboxId.tsx +27 -0
- package/src/routes/mail/flagged.tsx +37 -0
- package/src/routes/mail/index.tsx +41 -0
- package/src/routes/mail/outbox.tsx +13 -0
- package/src/routes/mail.tsx +413 -0
- package/src/routes/onboarding.tsx +35 -0
- package/src/routes/settings/-accounts.test.ts +65 -0
- package/src/routes/settings/accounts.tsx +542 -0
- package/src/routes/settings/advanced.tsx +55 -0
- package/src/routes/settings/appearance.tsx +140 -0
- package/src/routes/settings/filters.tsx +141 -0
- package/src/routes/settings/folders.tsx +216 -0
- package/src/routes/settings/index.tsx +5 -0
- package/src/routes/settings/senders.tsx +466 -0
- package/src/routes/settings/suggested-vips.tsx +10 -0
- package/src/routes/settings.tsx +66 -0
- package/src/runtime-config.ts +75 -0
- package/src/shell/index.tsx +98 -0
- package/src/types/index.ts +73 -0
- package/src/vite-env.d.ts +4 -0
- package/vite.base.ts +43 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { beforeEach, describe, mock, test } from "node:test";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Test harness for useLongPress. Since we can't use React Testing Library,
|
|
6
|
+
* we test the handler logic directly by simulating pointer events and timers.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface MockPointerEvent {
|
|
10
|
+
clientX: number;
|
|
11
|
+
clientY: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const createPointerEvent = (x: number, y: number): MockPointerEvent => ({
|
|
15
|
+
clientX: x,
|
|
16
|
+
clientY: y,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Minimal implementation of useLongPress logic for testing.
|
|
21
|
+
* Matches the hook's behavior without React dependencies.
|
|
22
|
+
*/
|
|
23
|
+
class LongPressSimulator {
|
|
24
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
25
|
+
private startPos: { x: number; y: number } | null = null;
|
|
26
|
+
private readonly MOVEMENT_THRESHOLD = 8;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private readonly onLongPress: () => void,
|
|
30
|
+
private readonly delayMs: number = 500,
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
onPointerDown(e: MockPointerEvent): void {
|
|
34
|
+
this.clearTimer();
|
|
35
|
+
this.startPos = { x: e.clientX, y: e.clientY };
|
|
36
|
+
this.timer = setTimeout(() => {
|
|
37
|
+
this.onLongPress();
|
|
38
|
+
this.clearTimer();
|
|
39
|
+
}, this.delayMs);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onPointerMove(e: MockPointerEvent): void {
|
|
43
|
+
if (!this.startPos) return;
|
|
44
|
+
|
|
45
|
+
const dx = e.clientX - this.startPos.x;
|
|
46
|
+
const dy = e.clientY - this.startPos.y;
|
|
47
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
48
|
+
|
|
49
|
+
if (distance > this.MOVEMENT_THRESHOLD) {
|
|
50
|
+
this.clearTimer();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
onPointerUp(): void {
|
|
55
|
+
this.clearTimer();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
onPointerCancel(): void {
|
|
59
|
+
this.clearTimer();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private clearTimer(): void {
|
|
63
|
+
if (this.timer) {
|
|
64
|
+
clearTimeout(this.timer);
|
|
65
|
+
this.timer = null;
|
|
66
|
+
}
|
|
67
|
+
this.startPos = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Test helper: check if timer is active */
|
|
71
|
+
hasActiveTimer(): boolean {
|
|
72
|
+
return this.timer !== null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Test helper: cleanup */
|
|
76
|
+
cleanup(): void {
|
|
77
|
+
this.clearTimer();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("useLongPress", () => {
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
// Reset any timers between tests
|
|
84
|
+
mock.restoreAll();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("fires callback after delay with no movement", async () => {
|
|
88
|
+
const onLongPress = mock.fn();
|
|
89
|
+
const sim = new LongPressSimulator(onLongPress, 500);
|
|
90
|
+
|
|
91
|
+
sim.onPointerDown(createPointerEvent(100, 100));
|
|
92
|
+
|
|
93
|
+
// Wait for the delay
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, 550));
|
|
95
|
+
|
|
96
|
+
assert.strictEqual(onLongPress.mock.calls.length, 1);
|
|
97
|
+
sim.cleanup();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("does not fire if pointer moves more than 8px", async () => {
|
|
101
|
+
const onLongPress = mock.fn();
|
|
102
|
+
const sim = new LongPressSimulator(onLongPress, 500);
|
|
103
|
+
|
|
104
|
+
sim.onPointerDown(createPointerEvent(100, 100));
|
|
105
|
+
|
|
106
|
+
// Move 10 pixels (more than threshold of 8)
|
|
107
|
+
sim.onPointerMove(createPointerEvent(110, 100));
|
|
108
|
+
|
|
109
|
+
// Wait beyond the delay
|
|
110
|
+
await new Promise((resolve) => setTimeout(resolve, 550));
|
|
111
|
+
|
|
112
|
+
assert.strictEqual(
|
|
113
|
+
onLongPress.mock.calls.length,
|
|
114
|
+
0,
|
|
115
|
+
"callback should not fire after moving > threshold",
|
|
116
|
+
);
|
|
117
|
+
sim.cleanup();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("does not fire on early pointerup", async () => {
|
|
121
|
+
const onLongPress = mock.fn();
|
|
122
|
+
const sim = new LongPressSimulator(onLongPress, 500);
|
|
123
|
+
|
|
124
|
+
sim.onPointerDown(createPointerEvent(100, 100));
|
|
125
|
+
|
|
126
|
+
// Release before delay completes
|
|
127
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
128
|
+
sim.onPointerUp();
|
|
129
|
+
|
|
130
|
+
// Wait beyond the original delay
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
132
|
+
|
|
133
|
+
assert.strictEqual(
|
|
134
|
+
onLongPress.mock.calls.length,
|
|
135
|
+
0,
|
|
136
|
+
"callback should not fire after early pointerup",
|
|
137
|
+
);
|
|
138
|
+
sim.cleanup();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("does not fire on pointercancel", async () => {
|
|
142
|
+
const onLongPress = mock.fn();
|
|
143
|
+
const sim = new LongPressSimulator(onLongPress, 500);
|
|
144
|
+
|
|
145
|
+
sim.onPointerDown(createPointerEvent(100, 100));
|
|
146
|
+
|
|
147
|
+
// Cancel before delay completes
|
|
148
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
149
|
+
sim.onPointerCancel();
|
|
150
|
+
|
|
151
|
+
// Wait beyond the original delay
|
|
152
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
153
|
+
|
|
154
|
+
assert.strictEqual(
|
|
155
|
+
onLongPress.mock.calls.length,
|
|
156
|
+
0,
|
|
157
|
+
"callback should not fire after pointercancel",
|
|
158
|
+
);
|
|
159
|
+
sim.cleanup();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("respects custom delayMs", async () => {
|
|
163
|
+
const onLongPress = mock.fn();
|
|
164
|
+
const customDelay = 200;
|
|
165
|
+
const sim = new LongPressSimulator(onLongPress, customDelay);
|
|
166
|
+
|
|
167
|
+
sim.onPointerDown(createPointerEvent(100, 100));
|
|
168
|
+
|
|
169
|
+
// Wait less than custom delay
|
|
170
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
171
|
+
assert.strictEqual(
|
|
172
|
+
onLongPress.mock.calls.length,
|
|
173
|
+
0,
|
|
174
|
+
"should not fire before custom delay",
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Wait past custom delay
|
|
178
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
179
|
+
assert.strictEqual(
|
|
180
|
+
onLongPress.mock.calls.length,
|
|
181
|
+
1,
|
|
182
|
+
"should fire after custom delay",
|
|
183
|
+
);
|
|
184
|
+
sim.cleanup();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("allows movement within 8px threshold", async () => {
|
|
188
|
+
const onLongPress = mock.fn();
|
|
189
|
+
const sim = new LongPressSimulator(onLongPress, 500);
|
|
190
|
+
|
|
191
|
+
sim.onPointerDown(createPointerEvent(100, 100));
|
|
192
|
+
|
|
193
|
+
// Move 7 pixels (within threshold)
|
|
194
|
+
sim.onPointerMove(createPointerEvent(107, 100));
|
|
195
|
+
|
|
196
|
+
// Wait for the delay
|
|
197
|
+
await new Promise((resolve) => setTimeout(resolve, 550));
|
|
198
|
+
|
|
199
|
+
assert.strictEqual(
|
|
200
|
+
onLongPress.mock.calls.length,
|
|
201
|
+
1,
|
|
202
|
+
"callback should fire when movement is within threshold",
|
|
203
|
+
);
|
|
204
|
+
sim.cleanup();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("calculates diagonal movement correctly", async () => {
|
|
208
|
+
const onLongPress = mock.fn();
|
|
209
|
+
const sim = new LongPressSimulator(onLongPress, 500);
|
|
210
|
+
|
|
211
|
+
sim.onPointerDown(createPointerEvent(100, 100));
|
|
212
|
+
|
|
213
|
+
// Move 6px in both x and y (diagonal distance ~8.48px > 8px threshold)
|
|
214
|
+
sim.onPointerMove(createPointerEvent(106, 106));
|
|
215
|
+
|
|
216
|
+
// Wait beyond the delay
|
|
217
|
+
await new Promise((resolve) => setTimeout(resolve, 550));
|
|
218
|
+
|
|
219
|
+
assert.strictEqual(
|
|
220
|
+
onLongPress.mock.calls.length,
|
|
221
|
+
0,
|
|
222
|
+
"callback should not fire when diagonal movement exceeds threshold",
|
|
223
|
+
);
|
|
224
|
+
sim.cleanup();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("ignores pointer move before pointer down", async () => {
|
|
228
|
+
const onLongPress = mock.fn();
|
|
229
|
+
const sim = new LongPressSimulator(onLongPress, 500);
|
|
230
|
+
|
|
231
|
+
// Move without pressing down first
|
|
232
|
+
sim.onPointerMove(createPointerEvent(200, 200));
|
|
233
|
+
|
|
234
|
+
sim.onPointerDown(createPointerEvent(100, 100));
|
|
235
|
+
|
|
236
|
+
// Wait for the delay
|
|
237
|
+
await new Promise((resolve) => setTimeout(resolve, 550));
|
|
238
|
+
|
|
239
|
+
assert.strictEqual(
|
|
240
|
+
onLongPress.mock.calls.length,
|
|
241
|
+
1,
|
|
242
|
+
"callback should fire - earlier move should be ignored",
|
|
243
|
+
);
|
|
244
|
+
sim.cleanup();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("resets on new pointer down", async () => {
|
|
248
|
+
const onLongPress = mock.fn();
|
|
249
|
+
const sim = new LongPressSimulator(onLongPress, 500);
|
|
250
|
+
|
|
251
|
+
// First press
|
|
252
|
+
sim.onPointerDown(createPointerEvent(100, 100));
|
|
253
|
+
|
|
254
|
+
// Wait a bit
|
|
255
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
256
|
+
|
|
257
|
+
// New press at different location (should reset timer)
|
|
258
|
+
sim.onPointerDown(createPointerEvent(200, 200));
|
|
259
|
+
|
|
260
|
+
// Wait for delay from second press
|
|
261
|
+
await new Promise((resolve) => setTimeout(resolve, 550));
|
|
262
|
+
|
|
263
|
+
// Should only fire once (from second press)
|
|
264
|
+
assert.strictEqual(
|
|
265
|
+
onLongPress.mock.calls.length,
|
|
266
|
+
1,
|
|
267
|
+
"callback should fire once from second press",
|
|
268
|
+
);
|
|
269
|
+
sim.cleanup();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("exact 8px movement does not cancel", async () => {
|
|
273
|
+
const onLongPress = mock.fn();
|
|
274
|
+
const sim = new LongPressSimulator(onLongPress, 500);
|
|
275
|
+
|
|
276
|
+
sim.onPointerDown(createPointerEvent(100, 100));
|
|
277
|
+
|
|
278
|
+
// Move exactly 8 pixels
|
|
279
|
+
sim.onPointerMove(createPointerEvent(108, 100));
|
|
280
|
+
|
|
281
|
+
// Wait for the delay
|
|
282
|
+
await new Promise((resolve) => setTimeout(resolve, 550));
|
|
283
|
+
|
|
284
|
+
assert.strictEqual(
|
|
285
|
+
onLongPress.mock.calls.length,
|
|
286
|
+
1,
|
|
287
|
+
"callback should fire - movement exactly at threshold is allowed",
|
|
288
|
+
);
|
|
289
|
+
sim.cleanup();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
interface UseLongPressOptions {
|
|
4
|
+
onLongPress: () => void;
|
|
5
|
+
delayMs?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface LongPressHandlers {
|
|
9
|
+
onPointerDown: (e: PointerEvent | React.PointerEvent) => void;
|
|
10
|
+
onPointerMove: (e: PointerEvent | React.PointerEvent) => void;
|
|
11
|
+
onPointerUp: () => void;
|
|
12
|
+
onPointerCancel: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const MOVEMENT_THRESHOLD = 8; // pixels
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Hook for detecting long-press gestures on touch/pointer devices.
|
|
19
|
+
*
|
|
20
|
+
* @param onLongPress - Callback fired after the delay if the pointer hasn't moved
|
|
21
|
+
* @param delayMs - How long to wait before firing (default 500ms)
|
|
22
|
+
*
|
|
23
|
+
* @returns Pointer event handlers to spread onto your element
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* const { handlers } = useLongPress({ onLongPress: () => console.log('long press!') });
|
|
27
|
+
* return <div {...handlers}>Press and hold me</div>;
|
|
28
|
+
*/
|
|
29
|
+
export const useLongPress = ({
|
|
30
|
+
onLongPress,
|
|
31
|
+
delayMs = 500,
|
|
32
|
+
}: UseLongPressOptions): { handlers: LongPressHandlers } => {
|
|
33
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
34
|
+
const startPosRef = useRef<{ x: number; y: number } | null>(null);
|
|
35
|
+
|
|
36
|
+
const clearTimer = useCallback(() => {
|
|
37
|
+
if (timerRef.current) {
|
|
38
|
+
clearTimeout(timerRef.current);
|
|
39
|
+
timerRef.current = null;
|
|
40
|
+
}
|
|
41
|
+
startPosRef.current = null;
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const onPointerDown = useCallback(
|
|
45
|
+
(e: PointerEvent | React.PointerEvent) => {
|
|
46
|
+
clearTimer();
|
|
47
|
+
startPosRef.current = { x: e.clientX, y: e.clientY };
|
|
48
|
+
timerRef.current = setTimeout(() => {
|
|
49
|
+
onLongPress();
|
|
50
|
+
clearTimer();
|
|
51
|
+
}, delayMs);
|
|
52
|
+
},
|
|
53
|
+
[onLongPress, delayMs, clearTimer],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const onPointerMove = useCallback(
|
|
57
|
+
(e: PointerEvent | React.PointerEvent) => {
|
|
58
|
+
if (!startPosRef.current) return;
|
|
59
|
+
|
|
60
|
+
const dx = e.clientX - startPosRef.current.x;
|
|
61
|
+
const dy = e.clientY - startPosRef.current.y;
|
|
62
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
63
|
+
|
|
64
|
+
if (distance > MOVEMENT_THRESHOLD) {
|
|
65
|
+
clearTimer();
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
[clearTimer],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const onPointerUp = useCallback(() => {
|
|
72
|
+
clearTimer();
|
|
73
|
+
}, [clearTimer]);
|
|
74
|
+
|
|
75
|
+
const onPointerCancel = useCallback(() => {
|
|
76
|
+
clearTimer();
|
|
77
|
+
}, [clearTimer]);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
handlers: {
|
|
81
|
+
onPointerDown,
|
|
82
|
+
onPointerMove,
|
|
83
|
+
onPointerUp,
|
|
84
|
+
onPointerCancel,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
configOperationsGetConfigOptions,
|
|
3
|
+
mailboxOperationsListMailboxesOptions,
|
|
4
|
+
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
5
|
+
import { useQueries, useQuery } from "@tanstack/react-query";
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
|
|
8
|
+
interface MailboxAccountResolution {
|
|
9
|
+
accountId: string | undefined;
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the owning account for a single mailbox by id.
|
|
15
|
+
*
|
|
16
|
+
* The mailbox-list query is keyed by accountId, so we fan out to every
|
|
17
|
+
* configured account's mailbox list (cached forever, see sidebar) and find
|
|
18
|
+
* the one containing this mailboxId. Returns `accountId: undefined` while
|
|
19
|
+
* the lookup is still loading or if the mailbox isn't found in any account.
|
|
20
|
+
*
|
|
21
|
+
* Used by per-message Move actions where the `MessageActionMenu` only knows
|
|
22
|
+
* the mailboxId — the picker must scope to the correct account or the
|
|
23
|
+
* destination list would mix folders from unrelated accounts.
|
|
24
|
+
*/
|
|
25
|
+
export const useMailboxAccount = (
|
|
26
|
+
mailboxId: string | undefined,
|
|
27
|
+
): MailboxAccountResolution => {
|
|
28
|
+
const { data: config, isLoading: isConfigLoading } = useQuery({
|
|
29
|
+
...configOperationsGetConfigOptions(),
|
|
30
|
+
staleTime: Infinity,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const accountIds = useMemo(
|
|
34
|
+
() => (config?.accounts ?? []).map((account) => account.accountId),
|
|
35
|
+
[config?.accounts],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const mailboxQueries = useQueries({
|
|
39
|
+
queries: accountIds.map((accountId) => ({
|
|
40
|
+
...mailboxOperationsListMailboxesOptions({ path: { accountId } }),
|
|
41
|
+
staleTime: Infinity,
|
|
42
|
+
enabled: !!mailboxId,
|
|
43
|
+
})),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const isLoading =
|
|
47
|
+
isConfigLoading || mailboxQueries.some((query) => query.isLoading);
|
|
48
|
+
|
|
49
|
+
const accountId = useMemo(() => {
|
|
50
|
+
if (!mailboxId) return undefined;
|
|
51
|
+
for (let i = 0; i < accountIds.length; i++) {
|
|
52
|
+
const result = mailboxQueries[i];
|
|
53
|
+
const items = result.data?.items ?? [];
|
|
54
|
+
if (items.some((mailbox) => mailbox.mailboxId === mailboxId)) {
|
|
55
|
+
return accountIds[i];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}, [mailboxId, accountIds, mailboxQueries]);
|
|
60
|
+
|
|
61
|
+
return { accountId, isLoading };
|
|
62
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fans out the per-account mailbox-list query (mirrors `MailSidebarAdapter` /
|
|
3
|
+
* `useMailboxAccount` — cached forever, react-query dedupes the identical
|
|
4
|
+
* query key across call sites) and reduces it to the name index `in:` tokens
|
|
5
|
+
* resolve against (#428 follow-up, see doc/design/flows/06-search.md).
|
|
6
|
+
*/
|
|
7
|
+
import { mailboxOperationsListMailboxesOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
8
|
+
import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
|
|
9
|
+
import { useQueries } from "@tanstack/react-query";
|
|
10
|
+
import { useMemo } from "react";
|
|
11
|
+
import { buildMailboxNameIndex } from "@/lib/search-token-index";
|
|
12
|
+
|
|
13
|
+
export function useMailboxNameIndex(
|
|
14
|
+
accounts: RemitImapAccountResponse[],
|
|
15
|
+
): ReadonlyMap<string, string> {
|
|
16
|
+
const mailboxQueries = useQueries({
|
|
17
|
+
queries: accounts.map((account) => ({
|
|
18
|
+
...mailboxOperationsListMailboxesOptions({
|
|
19
|
+
path: { accountId: account.accountId },
|
|
20
|
+
}),
|
|
21
|
+
staleTime: Infinity,
|
|
22
|
+
})),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return useMemo(
|
|
26
|
+
() =>
|
|
27
|
+
buildMailboxNameIndex(
|
|
28
|
+
mailboxQueries.map((query) => query.data?.items ?? []),
|
|
29
|
+
),
|
|
30
|
+
[mailboxQueries],
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
|
|
4
|
+
import { selectMessagesToMarkRead } from "./useMarkAsRead.js";
|
|
5
|
+
|
|
6
|
+
const make = (
|
|
7
|
+
overrides: Partial<RemitImapThreadMessageResponse> & {
|
|
8
|
+
messageId: string;
|
|
9
|
+
threadMessageId: string;
|
|
10
|
+
isRead: boolean;
|
|
11
|
+
},
|
|
12
|
+
): RemitImapThreadMessageResponse =>
|
|
13
|
+
({
|
|
14
|
+
threadId: "t1",
|
|
15
|
+
mailboxId: "mb1",
|
|
16
|
+
subject: "s",
|
|
17
|
+
fromName: "n",
|
|
18
|
+
fromEmail: "e",
|
|
19
|
+
sentDate: "2025-01-01T00:00:00Z",
|
|
20
|
+
snippet: "",
|
|
21
|
+
hasAttachment: false,
|
|
22
|
+
hasStars: false,
|
|
23
|
+
...overrides,
|
|
24
|
+
}) as RemitImapThreadMessageResponse;
|
|
25
|
+
|
|
26
|
+
describe("selectMessagesToMarkRead", () => {
|
|
27
|
+
test("returns the unread expanded messages", () => {
|
|
28
|
+
const messages = [
|
|
29
|
+
make({ messageId: "m1", threadMessageId: "tm1", isRead: false }),
|
|
30
|
+
make({ messageId: "m2", threadMessageId: "tm2", isRead: true }),
|
|
31
|
+
make({ messageId: "m3", threadMessageId: "tm3", isRead: false }),
|
|
32
|
+
];
|
|
33
|
+
const expanded = new Set(["tm1", "tm3"]);
|
|
34
|
+
const got = selectMessagesToMarkRead(
|
|
35
|
+
messages,
|
|
36
|
+
expanded,
|
|
37
|
+
new Set(),
|
|
38
|
+
new Set(),
|
|
39
|
+
);
|
|
40
|
+
assert.deepStrictEqual(got, ["m1", "m3"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("skips already-read messages even when expanded", () => {
|
|
44
|
+
const messages = [
|
|
45
|
+
make({ messageId: "m1", threadMessageId: "tm1", isRead: true }),
|
|
46
|
+
];
|
|
47
|
+
const expanded = new Set(["tm1"]);
|
|
48
|
+
const got = selectMessagesToMarkRead(
|
|
49
|
+
messages,
|
|
50
|
+
expanded,
|
|
51
|
+
new Set(),
|
|
52
|
+
new Set(),
|
|
53
|
+
);
|
|
54
|
+
assert.deepStrictEqual(got, []);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("skips collapsed messages even when unread", () => {
|
|
58
|
+
const messages = [
|
|
59
|
+
make({ messageId: "m1", threadMessageId: "tm1", isRead: false }),
|
|
60
|
+
make({ messageId: "m2", threadMessageId: "tm2", isRead: false }),
|
|
61
|
+
];
|
|
62
|
+
const expanded = new Set(["tm1"]);
|
|
63
|
+
const got = selectMessagesToMarkRead(
|
|
64
|
+
messages,
|
|
65
|
+
expanded,
|
|
66
|
+
new Set(),
|
|
67
|
+
new Set(),
|
|
68
|
+
);
|
|
69
|
+
assert.deepStrictEqual(got, ["m1"]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("skips messages already marked in this session", () => {
|
|
73
|
+
const messages = [
|
|
74
|
+
make({ messageId: "m1", threadMessageId: "tm1", isRead: false }),
|
|
75
|
+
];
|
|
76
|
+
const expanded = new Set(["tm1"]);
|
|
77
|
+
const got = selectMessagesToMarkRead(
|
|
78
|
+
messages,
|
|
79
|
+
expanded,
|
|
80
|
+
new Set(["m1"]),
|
|
81
|
+
new Set(),
|
|
82
|
+
);
|
|
83
|
+
assert.deepStrictEqual(got, []);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("skips messages with an in-flight mutation", () => {
|
|
87
|
+
const messages = [
|
|
88
|
+
make({ messageId: "m1", threadMessageId: "tm1", isRead: false }),
|
|
89
|
+
];
|
|
90
|
+
const expanded = new Set(["tm1"]);
|
|
91
|
+
const got = selectMessagesToMarkRead(
|
|
92
|
+
messages,
|
|
93
|
+
expanded,
|
|
94
|
+
new Set(),
|
|
95
|
+
new Set(["m1"]),
|
|
96
|
+
);
|
|
97
|
+
assert.deepStrictEqual(got, []);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("returns empty list when nothing is expanded", () => {
|
|
101
|
+
const messages = [
|
|
102
|
+
make({ messageId: "m1", threadMessageId: "tm1", isRead: false }),
|
|
103
|
+
make({ messageId: "m2", threadMessageId: "tm2", isRead: false }),
|
|
104
|
+
];
|
|
105
|
+
const got = selectMessagesToMarkRead(
|
|
106
|
+
messages,
|
|
107
|
+
new Set(),
|
|
108
|
+
new Set(),
|
|
109
|
+
new Set(),
|
|
110
|
+
);
|
|
111
|
+
assert.deepStrictEqual(got, []);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("does not require all unread messages to be expanded", () => {
|
|
115
|
+
// Regression: the previous policy gated firing on every unread
|
|
116
|
+
// message being expanded, which meant most opens silently never
|
|
117
|
+
// produced a mutation. The newest message being expanded must be
|
|
118
|
+
// enough to mark it read.
|
|
119
|
+
const messages = [
|
|
120
|
+
make({ messageId: "m1", threadMessageId: "tm1", isRead: false }),
|
|
121
|
+
make({ messageId: "m2", threadMessageId: "tm2", isRead: false }),
|
|
122
|
+
make({ messageId: "m3", threadMessageId: "tm3", isRead: false }),
|
|
123
|
+
];
|
|
124
|
+
const expanded = new Set(["tm1"]);
|
|
125
|
+
const got = selectMessagesToMarkRead(
|
|
126
|
+
messages,
|
|
127
|
+
expanded,
|
|
128
|
+
new Set(),
|
|
129
|
+
new Set(),
|
|
130
|
+
);
|
|
131
|
+
assert.deepStrictEqual(got, ["m1"]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("marks an older selected message when newest is already read (#683)", () => {
|
|
135
|
+
// Regression: ConversationView opens with order:"desc" so messages[0] is
|
|
136
|
+
// the newest. When the user clicks a thread row whose representative
|
|
137
|
+
// message is an older unread message (not the newest), only that older
|
|
138
|
+
// message must be marked. The newest is already read and must be skipped.
|
|
139
|
+
const messages = [
|
|
140
|
+
// Newest — already read (messages[0] in desc order)
|
|
141
|
+
make({
|
|
142
|
+
messageId: "m-newest",
|
|
143
|
+
threadMessageId: "tm-newest",
|
|
144
|
+
isRead: true,
|
|
145
|
+
}),
|
|
146
|
+
// Older — unread, the one the user clicked in the thread list
|
|
147
|
+
make({
|
|
148
|
+
messageId: "m-older",
|
|
149
|
+
threadMessageId: "tm-older",
|
|
150
|
+
isRead: false,
|
|
151
|
+
}),
|
|
152
|
+
];
|
|
153
|
+
// ConversationView now expands both messages[0] AND the selected message.
|
|
154
|
+
// Here the selected message is the older one, so both are expanded.
|
|
155
|
+
const expanded = new Set(["tm-newest", "tm-older"]);
|
|
156
|
+
const got = selectMessagesToMarkRead(
|
|
157
|
+
messages,
|
|
158
|
+
expanded,
|
|
159
|
+
new Set(),
|
|
160
|
+
new Set(),
|
|
161
|
+
);
|
|
162
|
+
// Only the unread older message should be marked — the newest is already read.
|
|
163
|
+
assert.deepStrictEqual(got, ["m-older"]);
|
|
164
|
+
});
|
|
165
|
+
});
|