@remit/web-client 0.0.322 → 0.0.324
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/better-auth-config.ts +3 -10
- 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 +8 -3
- 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 +3 -1
- 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/self-update.stories.tsx +18 -0
- 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 +5 -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/session-storage.ts +7 -0
- package/src/lib/stale-build.test.ts +52 -0
- package/src/lib/stale-build.ts +36 -0
- 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/__root.tsx +14 -4
- 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 +3 -5
- 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.test.ts +17 -0
- package/src/runtime-config.ts +18 -3
- package/src/shell/index.tsx +3 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.324",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
|
|
6
6
|
"exports": {
|
|
@@ -86,10 +86,10 @@
|
|
|
86
86
|
"aws-amplify": "^6",
|
|
87
87
|
"@aws-amplify/ui-react": "^6",
|
|
88
88
|
"aws-rum-web": "^3",
|
|
89
|
-
"better-auth": "^1.
|
|
89
|
+
"better-auth": "^1.7.12",
|
|
90
90
|
"i18next": "^24",
|
|
91
91
|
"i18next-browser-languagedetector": "^8",
|
|
92
|
-
"i18next-http-backend": "^
|
|
92
|
+
"i18next-http-backend": "^4.0.2",
|
|
93
93
|
"lucide-react": "^0.468",
|
|
94
94
|
"p-map": "*",
|
|
95
95
|
"react-hook-form": "*",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"react-dom": "^19.2.4",
|
|
116
116
|
"tailwindcss": "^4",
|
|
117
117
|
"typescript": "*",
|
|
118
|
-
"vite": "^7.3.
|
|
118
|
+
"vite": "^7.3.7"
|
|
119
119
|
},
|
|
120
120
|
"license": "MIT",
|
|
121
121
|
"publishConfig": {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useAuthProvider } from "@/auth/provider";
|
|
2
|
+
|
|
3
|
+
export function SignInAgain() {
|
|
4
|
+
const { Account } = useAuthProvider();
|
|
5
|
+
return (
|
|
6
|
+
<Account>
|
|
7
|
+
{({ signOut }) => (
|
|
8
|
+
<button
|
|
9
|
+
type="button"
|
|
10
|
+
onClick={() => signOut()}
|
|
11
|
+
className="rounded-md border border-line px-2.5 py-1 text-sm font-medium text-fg outline-none hover:bg-surface-sunken focus-visible:ring-2 focus-visible:ring-ring"
|
|
12
|
+
>
|
|
13
|
+
Sign in again
|
|
14
|
+
</button>
|
|
15
|
+
)}
|
|
16
|
+
</Account>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
|
-
import {
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
3
|
|
|
4
4
|
type TestConfig = {
|
|
5
5
|
cognito?: {
|
|
@@ -10,36 +10,11 @@ type TestConfig = {
|
|
|
10
10
|
};
|
|
11
11
|
appOrigin?: string;
|
|
12
12
|
};
|
|
13
|
-
type AmplifyCall = {
|
|
14
|
-
Auth?: {
|
|
15
|
-
Cognito?: {
|
|
16
|
-
userPoolId?: string;
|
|
17
|
-
userPoolClientId?: string;
|
|
18
|
-
loginWith?: {
|
|
19
|
-
oauth?: {
|
|
20
|
-
domain?: string;
|
|
21
|
-
redirectSignIn?: string[];
|
|
22
|
-
redirectSignOut?: string[];
|
|
23
|
-
responseType?: string;
|
|
24
|
-
scopes?: string[];
|
|
25
|
-
};
|
|
26
|
-
};
|
|
27
|
-
};
|
|
28
|
-
};
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
declare global {
|
|
32
|
-
var __AMPLIFY_MOCKS__: { configureCalls: AmplifyCall[] } | undefined;
|
|
33
|
-
var __CACHE_BUST__: number | undefined;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
13
|
let cacheBust = 0;
|
|
37
14
|
|
|
38
15
|
const loadModule = async (config: TestConfig) => {
|
|
39
16
|
cacheBust += 1;
|
|
40
|
-
globalThis.__CACHE_BUST__ = cacheBust;
|
|
41
17
|
globalThis.__REMIT_CONFIG__ = config;
|
|
42
|
-
globalThis.__AMPLIFY_MOCKS__ = { configureCalls: [] };
|
|
43
18
|
return import(`./amplify-config.ts?v=${cacheBust}`);
|
|
44
19
|
};
|
|
45
20
|
|
|
@@ -50,7 +25,6 @@ const fullConfig: TestConfig = {
|
|
|
50
25
|
domain: "example.auth.us-east-1.amazoncognito.com",
|
|
51
26
|
region: "us-east-1",
|
|
52
27
|
},
|
|
53
|
-
appOrigin: "https://app.example.com",
|
|
54
28
|
};
|
|
55
29
|
|
|
56
30
|
describe("isCognitoConfigured", () => {
|
|
@@ -74,83 +48,3 @@ describe("isCognitoConfigured", () => {
|
|
|
74
48
|
assert.equal(mod.isCognitoConfigured(), false);
|
|
75
49
|
});
|
|
76
50
|
});
|
|
77
|
-
|
|
78
|
-
describe("configureAmplify", () => {
|
|
79
|
-
let originalWarn: typeof console.warn;
|
|
80
|
-
let warnings: unknown[][];
|
|
81
|
-
|
|
82
|
-
beforeEach(() => {
|
|
83
|
-
originalWarn = console.warn;
|
|
84
|
-
warnings = [];
|
|
85
|
-
console.warn = (...args: unknown[]) => {
|
|
86
|
-
warnings.push(args);
|
|
87
|
-
};
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("warns and does not call Amplify.configure when unconfigured", async () => {
|
|
91
|
-
const mod = await loadModule({});
|
|
92
|
-
mod.configureAmplify();
|
|
93
|
-
console.warn = originalWarn;
|
|
94
|
-
assert.equal(globalThis.__AMPLIFY_MOCKS__?.configureCalls.length, 0);
|
|
95
|
-
assert.equal(warnings.length, 1);
|
|
96
|
-
const message = String(warnings[0][0]);
|
|
97
|
-
assert.ok(
|
|
98
|
-
message.includes("Cognito"),
|
|
99
|
-
`expected warning to mention Cognito, got: ${message}`,
|
|
100
|
-
);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("passes userPoolId and clientId to Amplify.configure", async () => {
|
|
104
|
-
const mod = await loadModule(fullConfig);
|
|
105
|
-
mod.configureAmplify();
|
|
106
|
-
console.warn = originalWarn;
|
|
107
|
-
const calls = globalThis.__AMPLIFY_MOCKS__?.configureCalls ?? [];
|
|
108
|
-
assert.equal(calls.length, 1);
|
|
109
|
-
const cognito = calls[0]?.Auth?.Cognito;
|
|
110
|
-
assert.equal(cognito?.userPoolId, "us-east-1_pool");
|
|
111
|
-
assert.equal(cognito?.userPoolClientId, "client-id");
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("wires OAuth block with appOrigin in redirect arrays when domain set", async () => {
|
|
115
|
-
const mod = await loadModule(fullConfig);
|
|
116
|
-
mod.configureAmplify();
|
|
117
|
-
console.warn = originalWarn;
|
|
118
|
-
const oauth =
|
|
119
|
-
globalThis.__AMPLIFY_MOCKS__?.configureCalls[0]?.Auth?.Cognito?.loginWith
|
|
120
|
-
?.oauth;
|
|
121
|
-
assert.ok(oauth, "expected oauth block to be present");
|
|
122
|
-
assert.equal(oauth?.domain, "example.auth.us-east-1.amazoncognito.com");
|
|
123
|
-
assert.deepEqual(oauth?.redirectSignIn, ["https://app.example.com"]);
|
|
124
|
-
assert.deepEqual(oauth?.redirectSignOut, ["https://app.example.com"]);
|
|
125
|
-
assert.equal(oauth?.responseType, "code");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("omits loginWith block when no domain configured", async () => {
|
|
129
|
-
const mod = await loadModule({
|
|
130
|
-
cognito: { userPoolId: "pool", clientId: "client" },
|
|
131
|
-
});
|
|
132
|
-
mod.configureAmplify();
|
|
133
|
-
console.warn = originalWarn;
|
|
134
|
-
const cognito =
|
|
135
|
-
globalThis.__AMPLIFY_MOCKS__?.configureCalls[0]?.Auth?.Cognito;
|
|
136
|
-
assert.equal(cognito?.loginWith, undefined);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("uses empty redirect arrays when domain present but appOrigin missing", async () => {
|
|
140
|
-
const mod = await loadModule({
|
|
141
|
-
cognito: {
|
|
142
|
-
userPoolId: "pool",
|
|
143
|
-
clientId: "client",
|
|
144
|
-
domain: "example.auth.us-east-1.amazoncognito.com",
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
mod.configureAmplify();
|
|
148
|
-
console.warn = originalWarn;
|
|
149
|
-
const oauth =
|
|
150
|
-
globalThis.__AMPLIFY_MOCKS__?.configureCalls[0]?.Auth?.Cognito?.loginWith
|
|
151
|
-
?.oauth;
|
|
152
|
-
assert.ok(oauth);
|
|
153
|
-
assert.deepEqual(oauth?.redirectSignIn, []);
|
|
154
|
-
assert.deepEqual(oauth?.redirectSignOut, []);
|
|
155
|
-
});
|
|
156
|
-
});
|
|
@@ -41,12 +41,6 @@ const sentHeaders = (): Record<string, string> => {
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
describe("installAuthInterceptor", () => {
|
|
44
|
-
test("registers exactly one request interceptor", async () => {
|
|
45
|
-
const mod = await loadInterceptor();
|
|
46
|
-
mod.installAuthInterceptor(providerWithToken(async () => null));
|
|
47
|
-
assert.equal(client.interceptors.request.fns.length, 1);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
44
|
test("is idempotent — subsequent calls do not register additional interceptors", async () => {
|
|
51
45
|
const mod = await loadInterceptor();
|
|
52
46
|
const provider = providerWithToken(async () => null);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createAuthClient } from "better-auth/react";
|
|
2
2
|
import { NetworkError, taggedFetch } from "../lib/network-error";
|
|
3
|
+
import { sessionStore } from "../lib/session-storage";
|
|
3
4
|
import { getRuntimeConfig } from "../runtime-config";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -46,16 +47,8 @@ interface CachedToken {
|
|
|
46
47
|
expiresAt: number;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
const tokenStore = (): Storage | null => {
|
|
50
|
-
try {
|
|
51
|
-
return globalThis.sessionStorage ?? null;
|
|
52
|
-
} catch {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
|
|
57
50
|
const readStore = (): CachedToken | null => {
|
|
58
|
-
const raw =
|
|
51
|
+
const raw = sessionStore()?.getItem(STORAGE_KEY);
|
|
59
52
|
if (!raw) return null;
|
|
60
53
|
try {
|
|
61
54
|
const parsed: unknown = JSON.parse(raw);
|
|
@@ -77,7 +70,7 @@ const readStore = (): CachedToken | null => {
|
|
|
77
70
|
};
|
|
78
71
|
|
|
79
72
|
const writeStore = (token: CachedToken | null): void => {
|
|
80
|
-
const store =
|
|
73
|
+
const store = sessionStore();
|
|
81
74
|
if (!store) return;
|
|
82
75
|
try {
|
|
83
76
|
if (!token) {
|
|
@@ -9,9 +9,7 @@ type AmplifyAuthMocks = {
|
|
|
9
9
|
|
|
10
10
|
declare global {
|
|
11
11
|
var __AMPLIFY_AUTH_MOCKS__: AmplifyAuthMocks | undefined;
|
|
12
|
-
var __AMPLIFY_CONFIG_MOCKS__:
|
|
13
|
-
| { configured: boolean; configureCalls: number }
|
|
14
|
-
| undefined;
|
|
12
|
+
var __AMPLIFY_CONFIG_MOCKS__: { configured: boolean } | undefined;
|
|
15
13
|
}
|
|
16
14
|
|
|
17
15
|
let cacheBust = 2000;
|
|
@@ -32,7 +30,7 @@ const loadCognitoToken = async (
|
|
|
32
30
|
) => {
|
|
33
31
|
cacheBust += 1;
|
|
34
32
|
globalThis.__REMIT_CONFIG__ = {};
|
|
35
|
-
globalThis.__AMPLIFY_CONFIG_MOCKS__ = { configured
|
|
33
|
+
globalThis.__AMPLIFY_CONFIG_MOCKS__ = { configured };
|
|
36
34
|
globalThis.__AMPLIFY_AUTH_MOCKS__ = {
|
|
37
35
|
session: { tokens: {} },
|
|
38
36
|
fetchCalls: 0,
|
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
type CalendarDescriptor,
|
|
6
6
|
type CalendarSlotPick,
|
|
7
7
|
type Density,
|
|
8
|
+
ErrorState,
|
|
8
9
|
NextUpCard,
|
|
9
10
|
readNextUp,
|
|
10
11
|
} from "@remit/ui";
|
|
11
12
|
import { useMemo } from "react";
|
|
12
|
-
import { ErrorState } from "@/components/ui/ErrorState";
|
|
13
13
|
import type { FreeLookup } from "@/hooks/calendar";
|
|
14
14
|
import { agendaDensityOf } from "@/lib/calendar-density";
|
|
15
15
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type CalendarViewId,
|
|
8
8
|
CalendarViewSwitch,
|
|
9
9
|
type Density,
|
|
10
|
+
ErrorState,
|
|
10
11
|
Segmented,
|
|
11
12
|
type SegmentOption,
|
|
12
13
|
} from "@remit/ui";
|
|
@@ -14,7 +15,6 @@ import { Loader2 } from "lucide-react";
|
|
|
14
15
|
import { type ReactNode, useState } from "react";
|
|
15
16
|
import { CalendarViewPlaceholder } from "@/components/calendar/CalendarViewPlaceholder";
|
|
16
17
|
import { NavMenuButton } from "@/components/mail/NavMenuButton";
|
|
17
|
-
import { ErrorState } from "@/components/ui/ErrorState";
|
|
18
18
|
import {
|
|
19
19
|
calendarViewMountsAgenda,
|
|
20
20
|
calendarViewMountsGrid,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { RemitImapCalendarSuggestionResponse } from "@remit/api-http-client/types.gen.ts";
|
|
2
2
|
import {
|
|
3
3
|
CalendarFailureNote,
|
|
4
|
+
EmptyState,
|
|
4
5
|
type EventDraft,
|
|
5
6
|
EventEditorPane,
|
|
6
7
|
} from "@remit/ui";
|
|
7
8
|
import { useEffect, useState } from "react";
|
|
8
9
|
import { CalendarComposePane } from "@/components/calendar/CalendarComposePane";
|
|
9
|
-
import { EmptyState } from "@/components/ui/EmptyState";
|
|
10
10
|
import {
|
|
11
11
|
acceptEditedInputFromDrafts,
|
|
12
12
|
anchorZoneFor,
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "@remit/ui";
|
|
6
6
|
import { Navigate } from "@tanstack/react-router";
|
|
7
7
|
import { useState } from "react";
|
|
8
|
-
import {
|
|
8
|
+
import { SignInAgain } from "@/auth/SignInAgain";
|
|
9
9
|
import { CalendarComposePane } from "@/components/calendar/CalendarComposePane";
|
|
10
10
|
import {
|
|
11
11
|
type CalendarEventAbsence,
|
|
@@ -66,23 +66,6 @@ const CONFLICT =
|
|
|
66
66
|
|
|
67
67
|
type Intent = "edit" | "delete";
|
|
68
68
|
|
|
69
|
-
function SignInAgain() {
|
|
70
|
-
const { Account } = useAuthProvider();
|
|
71
|
-
return (
|
|
72
|
-
<Account>
|
|
73
|
-
{({ signOut }) => (
|
|
74
|
-
<button
|
|
75
|
-
type="button"
|
|
76
|
-
onClick={() => signOut()}
|
|
77
|
-
className="rounded-md border border-line px-2.5 py-1 text-sm font-medium text-fg outline-none hover:bg-surface-sunken focus-visible:ring-2 focus-visible:ring-ring"
|
|
78
|
-
>
|
|
79
|
-
Sign in again
|
|
80
|
-
</button>
|
|
81
|
-
)}
|
|
82
|
-
</Account>
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
69
|
const absenceOf = (
|
|
87
70
|
search: CalendarJumpSearch,
|
|
88
71
|
loading: boolean,
|
|
@@ -185,12 +185,17 @@ const getReferences = (
|
|
|
185
185
|
msg: RemitImapDescribeMessageResponse,
|
|
186
186
|
): { inReplyTo?: string; references: string[] } => {
|
|
187
187
|
const messageIdValue = msg.envelope.messageIdValue;
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
.
|
|
192
|
-
|
|
193
|
-
|
|
188
|
+
const ofType = (
|
|
189
|
+
type: RemitImapDescribeMessageResponse["references"][number]["referenceType"],
|
|
190
|
+
): string[] =>
|
|
191
|
+
msg.references
|
|
192
|
+
.filter((r) => r.referenceType === type)
|
|
193
|
+
.sort((a, b) => a.referenceOrder - b.referenceOrder)
|
|
194
|
+
.map((r) => r.messageIdValue);
|
|
195
|
+
const existingRefs = ofType("references");
|
|
196
|
+
|
|
197
|
+
const references =
|
|
198
|
+
existingRefs.length > 0 ? [...existingRefs] : ofType("in_reply_to");
|
|
194
199
|
if (messageIdValue && !references.includes(messageIdValue)) {
|
|
195
200
|
references.push(messageIdValue);
|
|
196
201
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { configOperationsGetConfigOptions } 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
4
|
import { X } from "lucide-react";
|
|
4
|
-
import { ErrorState } from "@/components/ui/ErrorState";
|
|
5
5
|
import { useKeyboardNavigation } from "@/hooks/useKeyboardNavigation";
|
|
6
6
|
import { useIsDesktop } from "@/hooks/useMediaQuery";
|
|
7
7
|
import {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { configOperationsGetConfigOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
2
|
-
import { ConfirmDialog, useOverlayScope } from "@remit/ui";
|
|
2
|
+
import { ConfirmDialog, ErrorState, useOverlayScope } from "@remit/ui";
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
4
|
import { useCallback, useRef, useState } from "react";
|
|
5
5
|
import { Drawer } from "vaul";
|
|
6
|
-
import { ErrorState } from "@/components/ui/ErrorState";
|
|
7
6
|
import {
|
|
8
7
|
useAdoptComposeDraft,
|
|
9
8
|
useCloseCompose,
|
|
@@ -310,6 +310,24 @@ export const SaveFailed: Story = {
|
|
|
310
310
|
},
|
|
311
311
|
};
|
|
312
312
|
|
|
313
|
+
export const DiscardFailed: Story = {
|
|
314
|
+
args: { url: `/mail/${INBOX}/compose/out-plain` },
|
|
315
|
+
parameters: {
|
|
316
|
+
msw: { handlers: mailHandlers(world, { draftDiscard: "refused" }) },
|
|
317
|
+
},
|
|
318
|
+
play: async () => {
|
|
319
|
+
await userEvent.click(
|
|
320
|
+
await page().findByRole("button", { name: "Discard" }),
|
|
321
|
+
);
|
|
322
|
+
await expect(
|
|
323
|
+
await page().findByText("Couldn't discard draft"),
|
|
324
|
+
).toBeVisible();
|
|
325
|
+
await expect(
|
|
326
|
+
await page().findByText("The draft is already gone."),
|
|
327
|
+
).toBeVisible();
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
|
|
313
331
|
export const Sending: Story = {
|
|
314
332
|
args: { url: `/mail/${INBOX}/compose/out-misspelt` },
|
|
315
333
|
parameters: {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { afterEach, it } from "node:test";
|
|
3
|
+
import type {
|
|
4
|
+
RemitImapAccountResponse,
|
|
5
|
+
RemitImapDescribeMessageResponse,
|
|
6
|
+
} from "@remit/api-http-client/types.gen.ts";
|
|
7
|
+
import { MutationCache, QueryCache, QueryClient } from "@tanstack/react-query";
|
|
8
|
+
import {
|
|
9
|
+
type AnyRouter,
|
|
10
|
+
createMemoryHistory,
|
|
11
|
+
createRootRoute,
|
|
12
|
+
createRoute,
|
|
13
|
+
createRouter,
|
|
14
|
+
RouterContextProvider,
|
|
15
|
+
} from "@tanstack/react-router";
|
|
16
|
+
import { createElement } from "react";
|
|
17
|
+
import { __resetFatalError } from "../../lib/fatal-error";
|
|
18
|
+
import {
|
|
19
|
+
handleMutationCacheError,
|
|
20
|
+
handleQueryCacheError,
|
|
21
|
+
} from "../../lib/query-error-handler";
|
|
22
|
+
import { createDomHarness, type DomHarness } from "../../test-support/dom";
|
|
23
|
+
import { type HttpMock, httpError, mockFetch } from "../../test-support/http";
|
|
24
|
+
import { ComposeForm } from "./ComposeForm";
|
|
25
|
+
import { ComposeProvider } from "./ComposeProvider";
|
|
26
|
+
|
|
27
|
+
const OUTBOX_MESSAGE_ID = "ob-1";
|
|
28
|
+
|
|
29
|
+
const account = {
|
|
30
|
+
accountId: "acc-1",
|
|
31
|
+
email: "me@example.com",
|
|
32
|
+
smtpEnabled: true,
|
|
33
|
+
} as unknown as RemitImapAccountResponse;
|
|
34
|
+
|
|
35
|
+
const sourceMessage = {
|
|
36
|
+
message: { messageId: "msg-1" },
|
|
37
|
+
envelope: {
|
|
38
|
+
subject: "Lunch",
|
|
39
|
+
messageIdValue: "<m1@example.com>",
|
|
40
|
+
from: [{ normalizedEmail: "them@example.com", displayName: "Them" }],
|
|
41
|
+
replyTo: [],
|
|
42
|
+
to: [],
|
|
43
|
+
cc: [],
|
|
44
|
+
},
|
|
45
|
+
references: [],
|
|
46
|
+
bodyParts: [],
|
|
47
|
+
} as unknown as RemitImapDescribeMessageResponse;
|
|
48
|
+
|
|
49
|
+
const outboxEntry = {
|
|
50
|
+
outboxMessageId: OUTBOX_MESSAGE_ID,
|
|
51
|
+
accountId: account.accountId,
|
|
52
|
+
fromAddress: account.email,
|
|
53
|
+
toAddresses: ["them@example.com"],
|
|
54
|
+
attachments: [],
|
|
55
|
+
ccAddresses: [],
|
|
56
|
+
bccAddresses: [],
|
|
57
|
+
references: [],
|
|
58
|
+
subject: "Fwd: Lunch",
|
|
59
|
+
textBody: "here you go",
|
|
60
|
+
status: "draft",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let harness: DomHarness | undefined;
|
|
64
|
+
let http: HttpMock | undefined;
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
harness?.close();
|
|
68
|
+
harness = undefined;
|
|
69
|
+
http?.restore();
|
|
70
|
+
http = undefined;
|
|
71
|
+
__resetFatalError();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
(globalThis as { self?: typeof globalThis }).self ??= globalThis;
|
|
75
|
+
|
|
76
|
+
const rootRoute = createRootRoute();
|
|
77
|
+
const mailboxRoute = createRoute({
|
|
78
|
+
getParentRoute: () => rootRoute,
|
|
79
|
+
path: "/mail/$mailboxId",
|
|
80
|
+
validateSearch: (search: Record<string, unknown>) => search,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const testRouter = (): AnyRouter =>
|
|
84
|
+
createRouter({
|
|
85
|
+
routeTree: rootRoute.addChildren([mailboxRoute]),
|
|
86
|
+
history: createMemoryHistory({ initialEntries: ["/mail/mbx-1"] }),
|
|
87
|
+
}) as unknown as AnyRouter;
|
|
88
|
+
|
|
89
|
+
it("banners a refused draft discard instead of closing as if it worked", async () => {
|
|
90
|
+
http = mockFetch(async (call) => {
|
|
91
|
+
if (call.path.endsWith("/config")) return { accounts: [account] };
|
|
92
|
+
if (call.method === "DELETE") {
|
|
93
|
+
return httpError(409, "the draft is already gone");
|
|
94
|
+
}
|
|
95
|
+
return outboxEntry;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const queryClient = new QueryClient({
|
|
99
|
+
queryCache: new QueryCache({ onError: handleQueryCacheError }),
|
|
100
|
+
mutationCache: new MutationCache({ onError: handleMutationCacheError }),
|
|
101
|
+
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
harness = createDomHarness({ queryClient });
|
|
105
|
+
harness.renderApp(
|
|
106
|
+
createElement(RouterContextProvider, {
|
|
107
|
+
router: testRouter(),
|
|
108
|
+
// biome-ignore lint/correctness/noChildrenProp: RouterContextProvider types `children` as a required prop, which createElement's rest-argument form does not satisfy
|
|
109
|
+
children: createElement(
|
|
110
|
+
ComposeProvider,
|
|
111
|
+
null,
|
|
112
|
+
createElement(ComposeForm, {
|
|
113
|
+
mode: "forward",
|
|
114
|
+
account,
|
|
115
|
+
sourceMessage,
|
|
116
|
+
outboxMessageId: OUTBOX_MESSAGE_ID,
|
|
117
|
+
onDraftCreated: () => {},
|
|
118
|
+
onClose: () => {},
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
await harness.flush();
|
|
124
|
+
await harness.wait(50);
|
|
125
|
+
|
|
126
|
+
const discard = harness.query('[aria-label="Discard"]');
|
|
127
|
+
if (!discard) throw new Error("the discard button is not mounted");
|
|
128
|
+
harness.click(discard);
|
|
129
|
+
await harness.flush();
|
|
130
|
+
await harness.wait(100);
|
|
131
|
+
|
|
132
|
+
const alerts = harness.queryAll(
|
|
133
|
+
'[aria-label="Notifications"] [role="alert"]',
|
|
134
|
+
);
|
|
135
|
+
assert.equal(alerts.length, 1, "one banner reports the failure");
|
|
136
|
+
assert.match(alerts[0]?.textContent ?? "", /Couldn.t discard draft/);
|
|
137
|
+
assert.match(alerts[0]?.textContent ?? "", /the draft is already gone/);
|
|
138
|
+
});
|
|
@@ -174,7 +174,7 @@ export const partialImportReport: RemitImapConfigImportReport = report({
|
|
|
174
174
|
{
|
|
175
175
|
code: "import_write_failed",
|
|
176
176
|
message:
|
|
177
|
-
"the filter store refused the write The items above it were written and remain; the
|
|
177
|
+
"the filter store refused the write The items above it were written and remain; import the same file again with merge to finish.",
|
|
178
178
|
details: { section: "filters", key: "Bonnetjes" },
|
|
179
179
|
},
|
|
180
180
|
],
|
|
@@ -182,26 +182,25 @@ export const partialImportReport: RemitImapConfigImportReport = report({
|
|
|
182
182
|
});
|
|
183
183
|
|
|
184
184
|
/**
|
|
185
|
-
* The same stop with no section written:
|
|
186
|
-
*
|
|
187
|
-
* and `applied` is false. Warnings go with them.
|
|
185
|
+
* The same stop with no section written: the first write was the one that
|
|
186
|
+
* failed, so the report carries no landed item and `applied` is false.
|
|
188
187
|
*/
|
|
189
188
|
export const nothingLandedImportReport: RemitImapConfigImportReport = report({
|
|
190
189
|
applied: false,
|
|
191
190
|
items: [
|
|
192
191
|
item(
|
|
193
|
-
"
|
|
194
|
-
"
|
|
192
|
+
"accounts",
|
|
193
|
+
"matthijs@ischen.nl",
|
|
195
194
|
"rejected",
|
|
196
|
-
"the
|
|
195
|
+
"the account store refused the write",
|
|
197
196
|
),
|
|
198
197
|
],
|
|
199
198
|
errors: [
|
|
200
199
|
{
|
|
201
200
|
code: "import_write_failed",
|
|
202
201
|
message:
|
|
203
|
-
"the
|
|
204
|
-
details: { section: "
|
|
202
|
+
"the account store refused the write The items above it were written and remain; import the same file again with merge to finish.",
|
|
203
|
+
details: { section: "accounts", key: "matthijs@ischen.nl" },
|
|
205
204
|
},
|
|
206
205
|
],
|
|
207
206
|
warnings: [],
|
|
@@ -436,8 +436,8 @@ export const PartiallyLandedImport: Story = {
|
|
|
436
436
|
};
|
|
437
437
|
|
|
438
438
|
/**
|
|
439
|
-
* The same stop with no section written —
|
|
440
|
-
*
|
|
439
|
+
* The same stop with no section written — the first write was the one that
|
|
440
|
+
* failed. No section may carry a
|
|
441
441
|
* tick, and the screen counts nothing as landed.
|
|
442
442
|
*/
|
|
443
443
|
export const NothingLandedImport: Story = {
|
|
@@ -13,6 +13,7 @@ import { useMemo } from "react";
|
|
|
13
13
|
import { AccountMenu } from "@/auth/AccountMenu";
|
|
14
14
|
import { useRefreshControl } from "@/hooks/useRefreshControl";
|
|
15
15
|
import { useSearchScope } from "@/hooks/useSearchScope";
|
|
16
|
+
import { useWarningsAttention } from "@/hooks/useWarningsAttention";
|
|
16
17
|
import { openBugReport } from "@/lib/bug-report";
|
|
17
18
|
import { useMailContext } from "@/lib/mail-context";
|
|
18
19
|
import { useMailFreshness } from "@/lib/mail-freshness";
|
|
@@ -38,6 +39,7 @@ export function MailTopBar({ accounts }: MailTopBarProps) {
|
|
|
38
39
|
const goToSection = useGoToSection();
|
|
39
40
|
const compose = useOpenCompose();
|
|
40
41
|
const { scope, clearScope } = useSearchScope(accounts);
|
|
42
|
+
const settingsAttention = useWarningsAttention();
|
|
41
43
|
const chips =
|
|
42
44
|
scope.kind === "scoped"
|
|
43
45
|
? [{ id: scope.chip.id, label: scope.chip.label, tone: "scope" as const }]
|
|
@@ -80,6 +82,7 @@ export function MailTopBar({ accounts }: MailTopBarProps) {
|
|
|
80
82
|
onCompose={compose}
|
|
81
83
|
onReportBug={openBugReport}
|
|
82
84
|
onOpenSettings={() => goToSection("accounts")}
|
|
85
|
+
settingsAttention={settingsAttention}
|
|
83
86
|
composeShortcut={shortcutHintForAction("compose")}
|
|
84
87
|
refreshControl={
|
|
85
88
|
<RefreshButton
|
|
@@ -374,7 +374,6 @@ function BriefReading() {
|
|
|
374
374
|
isOpen={intelligence.drawerOpen}
|
|
375
375
|
onClose={intelligence.closeDrawer}
|
|
376
376
|
thread={selectedThread}
|
|
377
|
-
mailboxId={selectedThread?.mailboxId}
|
|
378
377
|
accountId={selectedThread?.accountId}
|
|
379
378
|
onAfterOptimisticRemove={handleDeselectIfRemoved}
|
|
380
379
|
onReplyWithText={replyWithText}
|
|
@@ -398,7 +397,6 @@ function BriefIntelligence() {
|
|
|
398
397
|
<IntelligencePane
|
|
399
398
|
onClose={onToggleIntelligence}
|
|
400
399
|
thread={selectedThread}
|
|
401
|
-
mailboxId={selectedThread?.mailboxId}
|
|
402
400
|
accountId={selectedThread?.accountId}
|
|
403
401
|
onAfterOptimisticRemove={handleDeselectIfRemoved}
|
|
404
402
|
onReplyWithText={replyWithText}
|
|
@@ -444,7 +442,6 @@ function BriefPhone() {
|
|
|
444
442
|
isOpen={drawer.isOpen}
|
|
445
443
|
onClose={drawer.close}
|
|
446
444
|
thread={selectedThread}
|
|
447
|
-
mailboxId={selectedThread?.mailboxId}
|
|
448
445
|
accountId={selectedThread?.accountId}
|
|
449
446
|
onAfterOptimisticRemove={handleDeselectIfRemoved}
|
|
450
447
|
onReplyWithText={replyWithText}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { threadDetailOperationsListThreadMessagesOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
2
2
|
import type { RemitImapMessageAuthenticity } from "@remit/api-http-client/types.gen.ts";
|
|
3
|
-
import { MobileReadingPane } from "@remit/ui";
|
|
3
|
+
import { EmptyState, ErrorState, MobileReadingPane } from "@remit/ui";
|
|
4
4
|
import { useQuery } from "@tanstack/react-query";
|
|
5
5
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
6
|
import { ConversationCompose } from "@/components/compose/ConversationCompose";
|
|
7
|
-
import { EmptyState } from "@/components/ui/EmptyState";
|
|
8
|
-
import { ErrorState } from "@/components/ui/ErrorState";
|
|
9
7
|
import { useKeyboardNavigation } from "@/hooks/useKeyboardNavigation";
|
|
10
8
|
import { useMailboxAccount } from "@/hooks/useMailboxAccount";
|
|
11
9
|
import { useMarkAsRead } from "@/hooks/useMarkAsRead";
|