@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,267 @@
|
|
|
1
|
+
import { messageOperationsDescribeMessageOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
2
|
+
import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
|
|
3
|
+
import {
|
|
4
|
+
Avatar,
|
|
5
|
+
ComfortableRowTextContent,
|
|
6
|
+
CompactRowBody,
|
|
7
|
+
comfortableRowClass,
|
|
8
|
+
compactRowClass,
|
|
9
|
+
type Density,
|
|
10
|
+
type SenderTrustLevel,
|
|
11
|
+
type ThreadRowData,
|
|
12
|
+
} from "@remit/ui";
|
|
13
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
14
|
+
import { Link } from "@tanstack/react-router";
|
|
15
|
+
import { Check } from "lucide-react";
|
|
16
|
+
import { type MouseEvent, memo, useCallback } from "react";
|
|
17
|
+
import { useLongPress } from "@/hooks/useLongPress";
|
|
18
|
+
import type { SelectionModifiers } from "@/hooks/useSelection";
|
|
19
|
+
import { toDisplayCategory } from "@/lib/display-category";
|
|
20
|
+
import { formatEmailDate } from "@/lib/format";
|
|
21
|
+
import { cn } from "@/lib/utils";
|
|
22
|
+
import { AutoMovedIndicator } from "./AutoMovedIndicator";
|
|
23
|
+
|
|
24
|
+
interface MailboxLinkSearch {
|
|
25
|
+
selectedMessageId?: string;
|
|
26
|
+
q?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface MessageListItemProps {
|
|
30
|
+
thread: RemitImapThreadMessageResponse;
|
|
31
|
+
mailboxId: string;
|
|
32
|
+
/** Owning account, used to resolve the Inbox/Junk mailboxes for the auto-moved badge's undo action. */
|
|
33
|
+
accountId?: string;
|
|
34
|
+
isSelected: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Roving keyboard focus cursor (#429). Distinct from `isSelected` (the open
|
|
37
|
+
* thread): a focused-but-not-open row shows the left accent rail; the open
|
|
38
|
+
* row shows the full highlight. Both can be true (the open row stays focused).
|
|
39
|
+
*/
|
|
40
|
+
isFocused?: boolean;
|
|
41
|
+
isChecked: boolean;
|
|
42
|
+
onToggleCheck: (id: string) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Desktop mouse selection. Called from the row's onClick with the click
|
|
45
|
+
* modifiers. Returns true when selection consumed the click (the row should
|
|
46
|
+
* not navigate); false for a plain click (navigation proceeds).
|
|
47
|
+
*/
|
|
48
|
+
onRowSelect: (messageId: string, modifiers: SelectionModifiers) => boolean;
|
|
49
|
+
messageCount?: number;
|
|
50
|
+
/** When true, the checkbox is always visible (e.g. mobile multi-select mode). */
|
|
51
|
+
isMultiSelectMode?: boolean;
|
|
52
|
+
/** Called on long press (mobile only). Receives the row's messageId. */
|
|
53
|
+
onLongPress?: (messageId: string) => void;
|
|
54
|
+
/** Whether the current viewport is desktop size. */
|
|
55
|
+
isDesktop?: boolean;
|
|
56
|
+
/** Row density — comfortable (default) or compact (mutt mode). */
|
|
57
|
+
density?: Density;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Map a RemitImapThreadMessageResponse to the ThreadRowData shape used by
|
|
62
|
+
* remit-ui row body components.
|
|
63
|
+
*/
|
|
64
|
+
const toThreadRowData = (
|
|
65
|
+
thread: RemitImapThreadMessageResponse,
|
|
66
|
+
messageCount: number | undefined,
|
|
67
|
+
): ThreadRowData => {
|
|
68
|
+
// Use the backend's authoritative DKIM-alignment verdict rather than
|
|
69
|
+
// re-deriving it in the view: dkimMismatch already accounts for the
|
|
70
|
+
// multi-signature / alignment semantics a single string compare misses.
|
|
71
|
+
const suspicious = thread.authenticity?.dkimMismatch === true;
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
id: thread.messageId,
|
|
75
|
+
accountId: thread.accountConfigId,
|
|
76
|
+
fromName: thread.fromName ?? thread.fromEmail ?? "Unknown",
|
|
77
|
+
fromEmail: thread.fromEmail ?? "",
|
|
78
|
+
subject: thread.subject ?? "(No subject)",
|
|
79
|
+
snippet: thread.snippet ?? "",
|
|
80
|
+
timeLabel: formatEmailDate(thread.sentDate),
|
|
81
|
+
isRead: thread.isRead,
|
|
82
|
+
hasAttachment: thread.hasAttachment,
|
|
83
|
+
starred:
|
|
84
|
+
thread.star != null && thread.star !== "none" && thread.hasStars === true,
|
|
85
|
+
trust: thread.senderTrust as SenderTrustLevel,
|
|
86
|
+
category: toDisplayCategory(thread.category),
|
|
87
|
+
messageCount,
|
|
88
|
+
suspicious,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const MessageListItemComponent = ({
|
|
93
|
+
thread,
|
|
94
|
+
mailboxId,
|
|
95
|
+
accountId,
|
|
96
|
+
isSelected,
|
|
97
|
+
isFocused = false,
|
|
98
|
+
isChecked,
|
|
99
|
+
onToggleCheck,
|
|
100
|
+
onRowSelect,
|
|
101
|
+
messageCount,
|
|
102
|
+
isMultiSelectMode = false,
|
|
103
|
+
onLongPress,
|
|
104
|
+
isDesktop = true,
|
|
105
|
+
density = "comfortable",
|
|
106
|
+
}: MessageListItemProps) => {
|
|
107
|
+
const queryClient = useQueryClient();
|
|
108
|
+
const messageId = thread.messageId;
|
|
109
|
+
const rowData = toThreadRowData(thread, messageCount);
|
|
110
|
+
|
|
111
|
+
const handleCheckboxClick = (e: MouseEvent) => {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
e.stopPropagation();
|
|
114
|
+
onToggleCheck(thread.messageId);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Desktop mouse selection semantics. Plain click falls through to the Link's
|
|
118
|
+
// navigation; shift / cmd / ctrl click is routed to selection and the
|
|
119
|
+
// navigation is suppressed. On mobile this is a no-op so taps still open.
|
|
120
|
+
const handleRowClick = useCallback(
|
|
121
|
+
(e: MouseEvent) => {
|
|
122
|
+
if (!isDesktop) return;
|
|
123
|
+
const handled = onRowSelect(messageId, {
|
|
124
|
+
shiftKey: e.shiftKey,
|
|
125
|
+
metaKey: e.metaKey,
|
|
126
|
+
ctrlKey: e.ctrlKey,
|
|
127
|
+
});
|
|
128
|
+
if (handled) {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
e.stopPropagation();
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
[isDesktop, onRowSelect, messageId],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const handleLongPress = useCallback(() => {
|
|
137
|
+
onLongPress?.(messageId);
|
|
138
|
+
}, [onLongPress, messageId]);
|
|
139
|
+
|
|
140
|
+
const longPressHandlers = useLongPress({
|
|
141
|
+
onLongPress: handleLongPress,
|
|
142
|
+
delayMs: 500,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Intent-based prefetch: by the time the user clicks, the body is in
|
|
146
|
+
// React Query's cache and the detail pane renders without a spinner.
|
|
147
|
+
const prefetchMessage = useCallback(() => {
|
|
148
|
+
queryClient.prefetchQuery(
|
|
149
|
+
messageOperationsDescribeMessageOptions({
|
|
150
|
+
path: { messageId: thread.messageId },
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
}, [queryClient, thread.messageId]);
|
|
154
|
+
|
|
155
|
+
const unread = !thread.isRead;
|
|
156
|
+
|
|
157
|
+
if (density === "compact") {
|
|
158
|
+
return (
|
|
159
|
+
<Link
|
|
160
|
+
to="/mail/$mailboxId"
|
|
161
|
+
params={{ mailboxId }}
|
|
162
|
+
search={(prev: MailboxLinkSearch) => ({
|
|
163
|
+
...prev,
|
|
164
|
+
selectedMessageId: thread.messageId,
|
|
165
|
+
})}
|
|
166
|
+
data-message-row
|
|
167
|
+
onClick={handleRowClick}
|
|
168
|
+
onMouseEnter={prefetchMessage}
|
|
169
|
+
onFocus={prefetchMessage}
|
|
170
|
+
{...(!isDesktop && longPressHandlers.handlers)}
|
|
171
|
+
className={cn(
|
|
172
|
+
compactRowClass({ active: isSelected, focused: isFocused }),
|
|
173
|
+
isChecked && "bg-accent-soft",
|
|
174
|
+
!isDesktop && "min-h-11",
|
|
175
|
+
)}
|
|
176
|
+
>
|
|
177
|
+
<CompactRowBody thread={rowData} />
|
|
178
|
+
</Link>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Comfortable density: avatar/checkbox leading slot + text content.
|
|
183
|
+
// The slot is a fixed 36px so the row never reflows when state changes.
|
|
184
|
+
// Unread dot is positioned absolute (left-1.5, vertically centered).
|
|
185
|
+
return (
|
|
186
|
+
<Link
|
|
187
|
+
to="/mail/$mailboxId"
|
|
188
|
+
params={{ mailboxId }}
|
|
189
|
+
search={(prev: MailboxLinkSearch) => ({
|
|
190
|
+
...prev,
|
|
191
|
+
selectedMessageId: thread.messageId,
|
|
192
|
+
})}
|
|
193
|
+
data-message-row
|
|
194
|
+
onClick={handleRowClick}
|
|
195
|
+
onMouseEnter={prefetchMessage}
|
|
196
|
+
onFocus={prefetchMessage}
|
|
197
|
+
{...(!isDesktop && longPressHandlers.handlers)}
|
|
198
|
+
className={cn(
|
|
199
|
+
"group",
|
|
200
|
+
comfortableRowClass({ active: isSelected, focused: isFocused }),
|
|
201
|
+
isChecked && "bg-accent-soft",
|
|
202
|
+
!isDesktop && "min-h-11",
|
|
203
|
+
)}
|
|
204
|
+
>
|
|
205
|
+
{/* Absolute unread dot — 6px gutter from the left pane hairline */}
|
|
206
|
+
{unread && (
|
|
207
|
+
<span className="absolute left-1.5 top-1/2 size-1.5 -translate-y-1/2 rounded-full bg-accent" />
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{/* Leading slot: avatar by default, checkbox on hover (desktop) or
|
|
211
|
+
while checked / in multi-select mode. Fixed 28px slot (size-7,
|
|
212
|
+
matching Avatar size="sm" in remit-ui ComfortableRow). */}
|
|
213
|
+
<div className="relative size-7 shrink-0">
|
|
214
|
+
<Avatar
|
|
215
|
+
name={thread.fromName ?? thread.fromEmail ?? "?"}
|
|
216
|
+
email={thread.fromEmail ?? undefined}
|
|
217
|
+
size="sm"
|
|
218
|
+
className={cn(
|
|
219
|
+
"absolute inset-0",
|
|
220
|
+
"sm:group-hover:opacity-0 transition-opacity",
|
|
221
|
+
(isChecked || isMultiSelectMode) && "opacity-0",
|
|
222
|
+
)}
|
|
223
|
+
/>
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
onClick={handleCheckboxClick}
|
|
227
|
+
className={cn(
|
|
228
|
+
"absolute inset-0 size-7 rounded-full border items-center justify-center transition-opacity",
|
|
229
|
+
isMultiSelectMode ? "flex" : "hidden sm:flex",
|
|
230
|
+
isChecked
|
|
231
|
+
? "bg-accent border-accent text-accent-fg opacity-100"
|
|
232
|
+
: isMultiSelectMode
|
|
233
|
+
? "border-fg-subtle/40 opacity-100 bg-canvas"
|
|
234
|
+
: "border-fg-subtle/40 opacity-0 group-hover:opacity-100 bg-canvas",
|
|
235
|
+
)}
|
|
236
|
+
aria-label={isChecked ? "Deselect message" : "Select message"}
|
|
237
|
+
>
|
|
238
|
+
{isChecked && <Check className="size-3" />}
|
|
239
|
+
</button>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
{/* Text/glyph content block */}
|
|
243
|
+
<ComfortableRowTextContent
|
|
244
|
+
thread={rowData}
|
|
245
|
+
badge={
|
|
246
|
+
thread.autoMoved && (
|
|
247
|
+
<AutoMovedIndicator
|
|
248
|
+
accountId={accountId}
|
|
249
|
+
messageId={thread.messageId}
|
|
250
|
+
threadId={thread.threadId}
|
|
251
|
+
mailboxId={thread.mailboxId}
|
|
252
|
+
autoMoved={thread.autoMoved}
|
|
253
|
+
size="sm"
|
|
254
|
+
/>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
/>
|
|
258
|
+
</Link>
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Wrapped in React.memo so virtualized rows don't re-render on every parent
|
|
263
|
+
// state change. The `search` callback prop on Link is inline, but it gets a
|
|
264
|
+
// stable reference because the parent itself is stable across re-renders
|
|
265
|
+
// (mailboxId is a string from route params). React.memo with default shallow
|
|
266
|
+
// equality is appropriate here since props are primitives + stable callback.
|
|
267
|
+
export const MessageListItem = memo(MessageListItemComponent);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render invariants for the reading-pane toolbar (#799): the never-disable
|
|
3
|
+
* tenet (`doc/rules/ux.md`). The toolbar's action buttons must stay pressable
|
|
4
|
+
* even with no thread open — they no-op and explain rather than greying out.
|
|
5
|
+
*
|
|
6
|
+
* Asserted against the shared `MailActionToolbar` (remit-ui), which the live
|
|
7
|
+
* `MessageToolbar` composes. Uses `react-dom/server` so no jsdom is needed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { describe, it } from "node:test";
|
|
12
|
+
import { MailActionToolbar } from "@remit/ui";
|
|
13
|
+
import React, { createElement } from "react";
|
|
14
|
+
import { renderToString } from "react-dom/server";
|
|
15
|
+
|
|
16
|
+
// The node test loader transpiles remit-ui's `.tsx` (resolved through
|
|
17
|
+
// node_modules) with the classic JSX runtime, which references a global
|
|
18
|
+
// `React`. Vite/Storybook use the automatic runtime, so this shim only exists
|
|
19
|
+
// for the SSR test harness.
|
|
20
|
+
(globalThis as { React?: typeof React }).React = React;
|
|
21
|
+
|
|
22
|
+
const render = (props: Parameters<typeof MailActionToolbar>[0]): string =>
|
|
23
|
+
renderToString(createElement(MailActionToolbar, props) as never);
|
|
24
|
+
|
|
25
|
+
describe("MailActionToolbar never disables its action buttons (#799)", () => {
|
|
26
|
+
it("renders no disabled button with no thread open", () => {
|
|
27
|
+
const html = render({ hasThread: false });
|
|
28
|
+
assert.equal(
|
|
29
|
+
/\sdisabled(=""|\s|>)/.test(html),
|
|
30
|
+
false,
|
|
31
|
+
"no toolbar button may carry a `disabled` attribute",
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("renders no disabled button with a thread open", () => {
|
|
36
|
+
const html = render({ hasThread: true });
|
|
37
|
+
assert.equal(/\sdisabled(=""|\s|>)/.test(html), false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("renders the inline explanation when supplied with no thread", () => {
|
|
41
|
+
const html = render({
|
|
42
|
+
hasThread: false,
|
|
43
|
+
unavailableHint: "Open a message first",
|
|
44
|
+
});
|
|
45
|
+
assert.match(html, /Open a message first/);
|
|
46
|
+
// Inline status, not a toast.
|
|
47
|
+
assert.match(html, /role="status"/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("keeps the action buttons present (reply/flag) so they stay pressable", () => {
|
|
51
|
+
const html = render({ hasThread: false });
|
|
52
|
+
assert.match(html, /aria-label="Reply"/);
|
|
53
|
+
assert.match(html, /aria-label="Flag"/);
|
|
54
|
+
// No archive verb — Remit is IMAP-backed (move-to-folder is the equivalent).
|
|
55
|
+
assert.doesNotMatch(html, /aria-label="Archive"/);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
type MailAction,
|
|
4
|
+
MailActionToolbar,
|
|
5
|
+
SearchBar,
|
|
6
|
+
} from "@remit/ui";
|
|
7
|
+
import { Info, SquarePen } from "lucide-react";
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import { AccountMenu } from "@/auth/AccountMenu";
|
|
10
|
+
import { BugReportButton } from "@/components/ui/BugReportButton";
|
|
11
|
+
import { tooltipForAction } from "@/lib/keymap";
|
|
12
|
+
import { MoveToTrigger } from "./MoveToTrigger";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Message action toolbar on the pane-header datum (40px, the shared
|
|
16
|
+
* `--spacing-pane-header`). The reading pane's verbs — reply / reply-all /
|
|
17
|
+
* forward, then delete / move / flag — Apple-Mail-style ghost icon buttons,
|
|
18
|
+
* then the search field top-right (still filters the current list), compose
|
|
19
|
+
* (✎) and the intelligence toggle (#422).
|
|
20
|
+
*
|
|
21
|
+
* Buttons are always pressable (never `disabled`): with no thread open a press
|
|
22
|
+
* is a no-op that surfaces a one-line inline explanation rather than greying
|
|
23
|
+
* out (`doc/rules/ux.md`).
|
|
24
|
+
*/
|
|
25
|
+
export interface MessageToolbarProps {
|
|
26
|
+
hasThread: boolean;
|
|
27
|
+
onCompose: () => void;
|
|
28
|
+
intelligenceOpen: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Whether the intelligence toggle is shown at all. The rail is contextual
|
|
31
|
+
* to an open message, so the toggle only appears once a thread is selected
|
|
32
|
+
* (matches the remit-ui AppShell reference) — it never opens an empty rail.
|
|
33
|
+
*/
|
|
34
|
+
showIntelligenceToggle: boolean;
|
|
35
|
+
onToggleIntelligence: () => void;
|
|
36
|
+
searchValue: string;
|
|
37
|
+
onSearchChange: (value: string) => void;
|
|
38
|
+
/** Full clear (X button): drops the query and any selected thread (#538). */
|
|
39
|
+
onSearchClear: () => void;
|
|
40
|
+
/** Query-only clear (Esc): drops the query, keeps the thread open (#489). */
|
|
41
|
+
onSearchClearQuery?: () => void;
|
|
42
|
+
|
|
43
|
+
/* ---- wired action callbacks (omit to keep the no-op-explain behaviour) ---- */
|
|
44
|
+
onReply?: () => void;
|
|
45
|
+
onReplyAll?: () => void;
|
|
46
|
+
onForward?: () => void;
|
|
47
|
+
/** Delete all messages in the open thread. */
|
|
48
|
+
onDelete?: () => void;
|
|
49
|
+
/**
|
|
50
|
+
* Override the enabled state of the delete button. Defaults to `hasThread`.
|
|
51
|
+
* Pass `true` when a non-thread item (e.g. a Remit draft) is active so the
|
|
52
|
+
* trash icon acts without acting on the other thread-scoped verbs.
|
|
53
|
+
*/
|
|
54
|
+
canDelete?: boolean;
|
|
55
|
+
/** Toggle the star on the most-recent message in the thread. */
|
|
56
|
+
onToggleStar?: () => void;
|
|
57
|
+
/** Whether the most-recent message is starred. */
|
|
58
|
+
isStarred?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Move-to-mailbox trigger context. When present the FolderInput button
|
|
61
|
+
* is replaced by the full `MoveToTrigger` popover.
|
|
62
|
+
*/
|
|
63
|
+
moveContext?: {
|
|
64
|
+
accountId: string;
|
|
65
|
+
currentMailboxId: string;
|
|
66
|
+
onMove: (destinationMailboxId: string) => void;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const OPEN_FIRST = "Open a message first";
|
|
71
|
+
|
|
72
|
+
export const MessageToolbar = ({
|
|
73
|
+
hasThread,
|
|
74
|
+
onCompose,
|
|
75
|
+
intelligenceOpen,
|
|
76
|
+
showIntelligenceToggle,
|
|
77
|
+
onToggleIntelligence,
|
|
78
|
+
searchValue,
|
|
79
|
+
onSearchChange,
|
|
80
|
+
onSearchClear,
|
|
81
|
+
onSearchClearQuery,
|
|
82
|
+
onReply,
|
|
83
|
+
onReplyAll,
|
|
84
|
+
onForward,
|
|
85
|
+
onDelete,
|
|
86
|
+
canDelete,
|
|
87
|
+
onToggleStar,
|
|
88
|
+
isStarred,
|
|
89
|
+
moveContext,
|
|
90
|
+
}: MessageToolbarProps) => {
|
|
91
|
+
const [hint, setHint] = useState<string | null>(null);
|
|
92
|
+
const canDeleteResolved = canDelete ?? hasThread;
|
|
93
|
+
const explain = (message: string) => () => setHint(message);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<MailActionToolbar
|
|
97
|
+
hasThread={hasThread}
|
|
98
|
+
isStarred={isStarred}
|
|
99
|
+
onUnavailable={(_action: MailAction) => setHint(OPEN_FIRST)}
|
|
100
|
+
unavailableHint={hint}
|
|
101
|
+
replyTitle={`Reply ${tooltipForAction("reply")}`}
|
|
102
|
+
replyAllTitle={`Reply all ${tooltipForAction("replyAll")}`}
|
|
103
|
+
forwardTitle={`Forward ${tooltipForAction("forward")}`}
|
|
104
|
+
deleteTitle={`Move to Trash ${tooltipForAction("delete")}`}
|
|
105
|
+
flagTitle={`Flag ${tooltipForAction("toggleStar")}`}
|
|
106
|
+
onReply={onReply}
|
|
107
|
+
onReplyAll={onReplyAll}
|
|
108
|
+
onForward={onForward}
|
|
109
|
+
onDelete={canDeleteResolved ? onDelete : explain(OPEN_FIRST)}
|
|
110
|
+
onToggleStar={onToggleStar}
|
|
111
|
+
onMove={explain(OPEN_FIRST)}
|
|
112
|
+
moveSlot={
|
|
113
|
+
moveContext ? (
|
|
114
|
+
<MoveToTrigger
|
|
115
|
+
accountId={moveContext.accountId}
|
|
116
|
+
currentMailboxId={moveContext.currentMailboxId}
|
|
117
|
+
onMove={moveContext.onMove}
|
|
118
|
+
label="Move to mailbox"
|
|
119
|
+
/>
|
|
120
|
+
) : undefined
|
|
121
|
+
}
|
|
122
|
+
>
|
|
123
|
+
{/* Apple Mail geometry: search sits top-right over the message area
|
|
124
|
+
but still filters the current list. Reuses the wired SearchBar
|
|
125
|
+
(the global "/" focus shortcut + Escape handling live in it). */}
|
|
126
|
+
<div className="w-64 min-w-40 shrink">
|
|
127
|
+
<SearchBar
|
|
128
|
+
value={searchValue}
|
|
129
|
+
onChange={onSearchChange}
|
|
130
|
+
onClear={onSearchClear}
|
|
131
|
+
onClearQuery={onSearchClearQuery}
|
|
132
|
+
placeholder="Search mail"
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
<span className="mx-1 h-4 w-px bg-line" aria-hidden />
|
|
136
|
+
<Button
|
|
137
|
+
variant="ghost"
|
|
138
|
+
size="sm"
|
|
139
|
+
icon={<SquarePen className="size-4" />}
|
|
140
|
+
title={`Compose ${tooltipForAction("compose")}`}
|
|
141
|
+
aria-label="Compose"
|
|
142
|
+
onClick={onCompose}
|
|
143
|
+
/>
|
|
144
|
+
{showIntelligenceToggle && (
|
|
145
|
+
<Button
|
|
146
|
+
variant="ghost"
|
|
147
|
+
size="sm"
|
|
148
|
+
icon={<Info className="size-4" />}
|
|
149
|
+
title="Intelligence"
|
|
150
|
+
aria-label={
|
|
151
|
+
intelligenceOpen
|
|
152
|
+
? "Hide intelligence sidebar"
|
|
153
|
+
: "Show intelligence sidebar"
|
|
154
|
+
}
|
|
155
|
+
aria-pressed={intelligenceOpen}
|
|
156
|
+
onClick={onToggleIntelligence}
|
|
157
|
+
className={
|
|
158
|
+
intelligenceOpen ? "bg-accent-2-soft text-accent-2" : undefined
|
|
159
|
+
}
|
|
160
|
+
/>
|
|
161
|
+
)}
|
|
162
|
+
<BugReportButton />
|
|
163
|
+
<AccountMenu />
|
|
164
|
+
</MailActionToolbar>
|
|
165
|
+
);
|
|
166
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
messageBulkOperationsUpdateFlagsMutation,
|
|
3
|
+
threadDetailOperationsListThreadMessagesQueryKey,
|
|
4
|
+
threadOperationsListThreadsQueryKey,
|
|
5
|
+
threadOperationsSearchThreadsQueryKey,
|
|
6
|
+
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
7
|
+
import { MobileMessageActionBar, type PopoverMenuItem } from "@remit/ui";
|
|
8
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
9
|
+
import { BadgeCheck, Check, Code } from "lucide-react";
|
|
10
|
+
import { useCallback } from "react";
|
|
11
|
+
import { useDeleteMessages } from "@/hooks/useDeleteMessages";
|
|
12
|
+
import { useMoveMessages } from "@/hooks/useMoveMessages";
|
|
13
|
+
import { useToggleTrusted } from "@/hooks/useToggleTrusted";
|
|
14
|
+
import { MoveToTrigger } from "./MoveToTrigger";
|
|
15
|
+
|
|
16
|
+
interface MobileMessageBarProps {
|
|
17
|
+
messageId: string;
|
|
18
|
+
threadId: string;
|
|
19
|
+
mailboxId: string;
|
|
20
|
+
accountId?: string;
|
|
21
|
+
isRead: boolean;
|
|
22
|
+
isStarred: boolean;
|
|
23
|
+
onToggleStar: () => void;
|
|
24
|
+
/** From-address id; enables the "Trusted sender" overflow item. */
|
|
25
|
+
fromAddressId?: string;
|
|
26
|
+
isTrusted: boolean;
|
|
27
|
+
showRaw: boolean;
|
|
28
|
+
onToggleRaw?: () => void;
|
|
29
|
+
onReply?: () => void;
|
|
30
|
+
onReplyAll?: () => void;
|
|
31
|
+
onForward?: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The live per-message action bar for the mobile single-pane reading view.
|
|
36
|
+
* Wraps the kit `MobileMessageActionBar` and owns this message's mutations:
|
|
37
|
+
* star (host), move-to-folder, delete, mark read/unread, plus the trusted /
|
|
38
|
+
* raw toggles in the overflow. Reply verbs come from the conversation.
|
|
39
|
+
*/
|
|
40
|
+
export const MobileMessageBar = ({
|
|
41
|
+
messageId,
|
|
42
|
+
threadId,
|
|
43
|
+
mailboxId,
|
|
44
|
+
accountId,
|
|
45
|
+
isRead,
|
|
46
|
+
isStarred,
|
|
47
|
+
onToggleStar,
|
|
48
|
+
fromAddressId,
|
|
49
|
+
isTrusted,
|
|
50
|
+
showRaw,
|
|
51
|
+
onToggleRaw,
|
|
52
|
+
onReply,
|
|
53
|
+
onReplyAll,
|
|
54
|
+
onForward,
|
|
55
|
+
}: MobileMessageBarProps) => {
|
|
56
|
+
const queryClient = useQueryClient();
|
|
57
|
+
|
|
58
|
+
const { deleteMessages } = useDeleteMessages({ mailboxId, threadId });
|
|
59
|
+
const { moveMessages } = useMoveMessages({ mailboxId, threadId, accountId });
|
|
60
|
+
const { toggleTrusted } = useToggleTrusted({ messageId });
|
|
61
|
+
|
|
62
|
+
const { mutate: updateFlags } = useMutation({
|
|
63
|
+
...messageBulkOperationsUpdateFlagsMutation(),
|
|
64
|
+
onSettled: () => {
|
|
65
|
+
queryClient.invalidateQueries({
|
|
66
|
+
queryKey: threadDetailOperationsListThreadMessagesQueryKey({
|
|
67
|
+
path: { threadId },
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
queryClient.invalidateQueries({
|
|
71
|
+
queryKey: threadOperationsListThreadsQueryKey({ path: { mailboxId } }),
|
|
72
|
+
});
|
|
73
|
+
queryClient.invalidateQueries({
|
|
74
|
+
queryKey: threadOperationsSearchThreadsQueryKey({
|
|
75
|
+
path: { mailboxId },
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const handleMove = useCallback(
|
|
82
|
+
(destinationMailboxId: string) => {
|
|
83
|
+
moveMessages([messageId], destinationMailboxId);
|
|
84
|
+
},
|
|
85
|
+
[moveMessages, messageId],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const handleDelete = useCallback(() => {
|
|
89
|
+
deleteMessages([messageId]);
|
|
90
|
+
}, [deleteMessages, messageId]);
|
|
91
|
+
|
|
92
|
+
const handleToggleRead = useCallback(() => {
|
|
93
|
+
updateFlags({ body: { messageIds: [messageId], isRead: !isRead } });
|
|
94
|
+
}, [updateFlags, messageId, isRead]);
|
|
95
|
+
|
|
96
|
+
const overflowItems: PopoverMenuItem[] = [];
|
|
97
|
+
if (fromAddressId) {
|
|
98
|
+
overflowItems.push({
|
|
99
|
+
key: "trusted",
|
|
100
|
+
label: isTrusted ? "Trusted sender" : "Mark sender trusted",
|
|
101
|
+
icon: isTrusted ? (
|
|
102
|
+
<BadgeCheck className="size-4 text-positive" />
|
|
103
|
+
) : (
|
|
104
|
+
<Check className="size-4" />
|
|
105
|
+
),
|
|
106
|
+
onSelect: () => toggleTrusted(fromAddressId, isTrusted),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (onToggleRaw) {
|
|
110
|
+
overflowItems.push({
|
|
111
|
+
key: "raw",
|
|
112
|
+
label: showRaw ? "Show formatted" : "Show raw email",
|
|
113
|
+
icon: <Code className="size-4" />,
|
|
114
|
+
onSelect: onToggleRaw,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<MobileMessageActionBar
|
|
120
|
+
hasThread
|
|
121
|
+
onReply={onReply}
|
|
122
|
+
onReplyAll={onReplyAll}
|
|
123
|
+
onForward={onForward}
|
|
124
|
+
isStarred={isStarred}
|
|
125
|
+
onToggleStar={onToggleStar}
|
|
126
|
+
onDelete={handleDelete}
|
|
127
|
+
isRead={isRead}
|
|
128
|
+
onToggleRead={handleToggleRead}
|
|
129
|
+
overflowItems={overflowItems}
|
|
130
|
+
moveSlot={
|
|
131
|
+
accountId ? (
|
|
132
|
+
<MoveToTrigger
|
|
133
|
+
accountId={accountId}
|
|
134
|
+
currentMailboxId={mailboxId}
|
|
135
|
+
onMove={handleMove}
|
|
136
|
+
label="Move this message"
|
|
137
|
+
/>
|
|
138
|
+
) : undefined
|
|
139
|
+
}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
};
|