@remit/web-client 0.0.6 → 0.0.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/web-client",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
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": {
@@ -137,7 +137,7 @@ export function FlaggedList({
137
137
 
138
138
  return (
139
139
  <MailViewChrome
140
- title="Flagged"
140
+ title="Starred"
141
141
  unreadCount={unreadCount}
142
142
  preset={preset}
143
143
  selectedCategory={selectedCategory}
@@ -150,7 +150,7 @@ export function FlaggedList({
150
150
  onSelectSearchResult={(result) => onSelectMessage?.(result.id)}
151
151
  >
152
152
  <MessageListPane
153
- listTitle="Flagged"
153
+ listTitle="Starred"
154
154
  sections={[{ id: "flagged", threads: rows }]}
155
155
  flatList
156
156
  hideHeader
@@ -110,8 +110,10 @@ import { useTelemetry } from "@/lib/telemetry-context";
110
110
  import { MailViewChrome } from "./MailViewChrome";
111
111
 
112
112
  /* ------------------------------------------------------------------ */
113
- /* Inbox filter predicates — the inbox preset offers Unread / Flagged /
114
- Has attachment (never accounts; an inbox is one account already). */
113
+ /* Inbox filter predicates — the inbox preset offers Unread / Starred /
114
+ Has attachment (never accounts; an inbox is one account already).
115
+ The `flagged` id is the wire name for IMAP \Flagged; the label is
116
+ "Starred". */
115
117
  /* ------------------------------------------------------------------ */
116
118
 
117
119
  const INBOX_FILTER_PREDICATES: Record<
@@ -119,7 +121,7 @@ const INBOX_FILTER_PREDICATES: Record<
119
121
  (t: RemitImapThreadMessageResponse) => boolean
120
122
  > = {
121
123
  unread: (t) => !t.isRead,
122
- flagged: (t) => t.star != null && t.star !== "none" && t.hasStars === true,
124
+ flagged: (t) => t.hasStars === true,
123
125
  attachment: (t) => Boolean(t.hasAttachment),
124
126
  };
125
127
 
@@ -80,8 +80,7 @@ const toThreadRowData = (
80
80
  timeLabel: formatEmailDate(thread.sentDate),
81
81
  isRead: thread.isRead,
82
82
  hasAttachment: thread.hasAttachment,
83
- starred:
84
- thread.star != null && thread.star !== "none" && thread.hasStars === true,
83
+ starred: thread.hasStars === true,
85
84
  trust: thread.senderTrust as SenderTrustLevel,
86
85
  category: toDisplayCategory(thread.category),
87
86
  messageCount,
@@ -50,7 +50,7 @@ describe("MailActionToolbar never disables its action buttons (#799)", () => {
50
50
  it("keeps the action buttons present (reply/flag) so they stay pressable", () => {
51
51
  const html = render({ hasThread: false });
52
52
  assert.match(html, /aria-label="Reply"/);
53
- assert.match(html, /aria-label="Flag"/);
53
+ assert.match(html, /aria-label="Star"/);
54
54
  // No archive verb — Remit is IMAP-backed (move-to-folder is the equivalent).
55
55
  assert.doesNotMatch(html, /aria-label="Archive"/);
56
56
  });
@@ -102,7 +102,7 @@ export const MessageToolbar = ({
102
102
  replyAllTitle={`Reply all ${tooltipForAction("replyAll")}`}
103
103
  forwardTitle={`Forward ${tooltipForAction("forward")}`}
104
104
  deleteTitle={`Move to Trash ${tooltipForAction("delete")}`}
105
- flagTitle={`Flag ${tooltipForAction("toggleStar")}`}
105
+ flagTitle={`Star ${tooltipForAction("toggleStar")}`}
106
106
  onReply={onReply}
107
107
  onReplyAll={onReplyAll}
108
108
  onForward={onForward}
@@ -0,0 +1,57 @@
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 { toggleStarsInItems } from "./useToggleStar.js";
5
+
6
+ const make = (
7
+ messageId: string,
8
+ hasStars: boolean,
9
+ ): RemitImapThreadMessageResponse => ({
10
+ senderTrust: "unknown",
11
+ threadId: "t1",
12
+ threadMessageId: `tm-${messageId}`,
13
+ messageId,
14
+ accountConfigId: "cfg_1",
15
+ mailboxId: "mb1",
16
+ subject: "s",
17
+ fromName: "n",
18
+ fromEmail: "e",
19
+ sentDate: 1767225600,
20
+ snippet: "",
21
+ isRead: false,
22
+ isDeleted: false,
23
+ hasAttachment: false,
24
+ star: "none",
25
+ hasStars,
26
+ createdAt: 0,
27
+ updatedAt: 0,
28
+ });
29
+
30
+ describe("toggleStarsInItems", () => {
31
+ test("stars only the target message", () => {
32
+ const items = [make("m1", false), make("m2", false)];
33
+ const got = toggleStarsInItems(items, "m1", true);
34
+ assert.deepStrictEqual(
35
+ got.map((i) => [i.messageId, i.hasStars]),
36
+ [
37
+ ["m1", true],
38
+ ["m2", false],
39
+ ],
40
+ );
41
+ });
42
+
43
+ test("unstars the target message", () => {
44
+ const items = [make("m1", true)];
45
+ const got = toggleStarsInItems(items, "m1", false);
46
+ assert.strictEqual(got[0]?.hasStars, false);
47
+ });
48
+
49
+ test("leaves the list untouched when the message is absent", () => {
50
+ const items = [make("m1", false)];
51
+ const got = toggleStarsInItems(items, "missing", true);
52
+ assert.deepStrictEqual(
53
+ got.map((i) => i.hasStars),
54
+ [false],
55
+ );
56
+ });
57
+ });
@@ -3,6 +3,7 @@ import {
3
3
  threadDetailOperationsListThreadMessagesQueryKey,
4
4
  threadOperationsListThreadsQueryKey,
5
5
  threadOperationsSearchThreadsQueryKey,
6
+ unifiedThreadOperationsListAllThreadsQueryKey,
6
7
  } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
7
8
  import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
8
9
  import { useMutation, useQueryClient } from "@tanstack/react-query";
@@ -38,11 +39,13 @@ interface ToggleStarContext {
38
39
  threadMessagesPrefix: readonly unknown[];
39
40
  threadsListPrefix: readonly unknown[];
40
41
  threadsSearchPrefix: readonly unknown[];
42
+ unifiedThreadsPrefix: readonly unknown[];
41
43
  previousThreadMessages: SnapshotEntry<ThreadMessagesData>[];
42
44
  previousThreadsList: SnapshotEntry<ThreadsListData>[];
45
+ previousUnifiedThreads: SnapshotEntry<ThreadMessagesData>[];
43
46
  }
44
47
 
45
- const toggleStarsInItems = (
48
+ export const toggleStarsInItems = (
46
49
  items: RemitImapThreadMessageResponse[],
47
50
  messageId: string,
48
51
  nextStarred: boolean,
@@ -78,11 +81,18 @@ export const useToggleStar = ({
78
81
  const threadsSearchPrefix = threadOperationsSearchThreadsQueryKey({
79
82
  path: { mailboxId },
80
83
  });
84
+ // The unified cross-account listing backs the daily brief and the
85
+ // Starred mailbox, so a star toggled from an inbox has to land there
86
+ // too — without this the starred view keeps serving a stale page for
87
+ // its whole staleTime and the message never appears.
88
+ const unifiedThreadsPrefix =
89
+ unifiedThreadOperationsListAllThreadsQueryKey();
81
90
 
82
91
  await Promise.all([
83
92
  queryClient.cancelQueries({ queryKey: threadMessagesPrefix }),
84
93
  queryClient.cancelQueries({ queryKey: threadsListPrefix }),
85
94
  queryClient.cancelQueries({ queryKey: threadsSearchPrefix }),
95
+ queryClient.cancelQueries({ queryKey: unifiedThreadsPrefix }),
86
96
  ]);
87
97
 
88
98
  const previousThreadMessages = queryClient
@@ -106,15 +116,29 @@ export const useToggleStar = ({
106
116
  )
107
117
  .map(([queryKey, data]) => ({ queryKey, data }));
108
118
 
119
+ const previousUnifiedThreads = queryClient
120
+ .getQueriesData<ThreadMessagesData>({ queryKey: unifiedThreadsPrefix })
121
+ .filter(
122
+ (entry): entry is [readonly unknown[], ThreadMessagesData] =>
123
+ entry[1] !== undefined,
124
+ )
125
+ .map(([queryKey, data]) => ({ queryKey, data }));
126
+
127
+ const patchItemsData = (old: ThreadMessagesData | undefined) => {
128
+ if (!old) return old;
129
+ return {
130
+ ...old,
131
+ items: toggleStarsInItems(old.items, messageId, nextStarred),
132
+ };
133
+ };
134
+
109
135
  queryClient.setQueriesData<ThreadMessagesData>(
110
136
  { queryKey: threadMessagesPrefix },
111
- (old) => {
112
- if (!old) return old;
113
- return {
114
- ...old,
115
- items: toggleStarsInItems(old.items, messageId, nextStarred),
116
- };
117
- },
137
+ patchItemsData,
138
+ );
139
+ queryClient.setQueriesData<ThreadMessagesData>(
140
+ { queryKey: unifiedThreadsPrefix },
141
+ patchItemsData,
118
142
  );
119
143
 
120
144
  const patchListData = (old: ThreadsListData | undefined) => {
@@ -141,8 +165,10 @@ export const useToggleStar = ({
141
165
  threadMessagesPrefix,
142
166
  threadsListPrefix,
143
167
  threadsSearchPrefix,
168
+ unifiedThreadsPrefix,
144
169
  previousThreadMessages,
145
170
  previousThreadsList,
171
+ previousUnifiedThreads,
146
172
  };
147
173
  },
148
174
  onError: (err, vars, context) => {
@@ -153,6 +179,9 @@ export const useToggleStar = ({
153
179
  for (const entry of context.previousThreadsList) {
154
180
  queryClient.setQueryData(entry.queryKey, entry.data);
155
181
  }
182
+ for (const entry of context.previousUnifiedThreads) {
183
+ queryClient.setQueryData(entry.queryKey, entry.data);
184
+ }
156
185
  }
157
186
  const nextStarred = vars.body.isStarred ?? false;
158
187
  pushError({
@@ -167,6 +196,7 @@ export const useToggleStar = ({
167
196
  queryClient.invalidateQueries({ queryKey: context.threadMessagesPrefix });
168
197
  queryClient.invalidateQueries({ queryKey: context.threadsListPrefix });
169
198
  queryClient.invalidateQueries({ queryKey: context.threadsSearchPrefix });
199
+ queryClient.invalidateQueries({ queryKey: context.unifiedThreadsPrefix });
170
200
  },
171
201
  });
172
202
 
@@ -1,13 +1,41 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
+ import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
3
4
  import type { ThreadRowData } from "@remit/ui";
4
5
  import {
5
6
  groupBriefSections,
6
7
  matchesBriefSearch,
7
8
  matchesSearchTokens,
9
+ toThreadRowData,
8
10
  } from "./brief.js";
9
11
  import type { SearchToken } from "./search-tokens.js";
10
12
 
13
+ function threadResponse(
14
+ overrides: Partial<RemitImapThreadMessageResponse> = {},
15
+ ): RemitImapThreadMessageResponse {
16
+ return {
17
+ threadId: "t1",
18
+ threadMessageId: "tm1",
19
+ messageId: "m1",
20
+ accountConfigId: "cfg_1",
21
+ mailboxId: "mb1",
22
+ fromName: "Sender",
23
+ fromEmail: "sender@example.com",
24
+ subject: "Subject",
25
+ snippet: "Snippet",
26
+ sentDate: 1767225600,
27
+ isRead: false,
28
+ isDeleted: false,
29
+ hasAttachment: false,
30
+ hasStars: false,
31
+ star: "none",
32
+ senderTrust: "unknown",
33
+ createdAt: 0,
34
+ updatedAt: 0,
35
+ ...overrides,
36
+ };
37
+ }
38
+
11
39
  function row(
12
40
  overrides: Partial<ThreadRowData> & Pick<ThreadRowData, "id">,
13
41
  ): ThreadRowData {
@@ -25,6 +53,32 @@ function row(
25
53
  };
26
54
  }
27
55
 
56
+ describe("toThreadRowData", () => {
57
+ // `star` is a colour, and it defaults to the "none" sentinel — starring a
58
+ // message only flips `hasStars`. Reading the colour to decide starredness
59
+ // made every row unstarred and left the Starred mailbox permanently empty.
60
+ test("a message with hasStars and the default star colour is starred", () => {
61
+ const row = toThreadRowData(
62
+ threadResponse({ hasStars: true, star: "none" }),
63
+ );
64
+ assert.strictEqual(row.starred, true);
65
+ });
66
+
67
+ test("a message with a star colour is starred", () => {
68
+ const row = toThreadRowData(
69
+ threadResponse({ hasStars: true, star: "yellow" }),
70
+ );
71
+ assert.strictEqual(row.starred, true);
72
+ });
73
+
74
+ test("a message without hasStars is not starred", () => {
75
+ const row = toThreadRowData(
76
+ threadResponse({ hasStars: false, star: "yellow" }),
77
+ );
78
+ assert.strictEqual(row.starred, false);
79
+ });
80
+ });
81
+
28
82
  describe("groupBriefSections", () => {
29
83
  test("returns empty array when no rows", () => {
30
84
  const sections = groupBriefSections([]);
package/src/lib/brief.ts CHANGED
@@ -57,8 +57,7 @@ export function toThreadRowData(
57
57
  sentDate: thread.sentDate,
58
58
  isRead: thread.isRead,
59
59
  hasAttachment: thread.hasAttachment,
60
- starred:
61
- thread.star != null && thread.star !== "none" && thread.hasStars === true,
60
+ starred: thread.hasStars === true,
62
61
  trust: thread.senderTrust as SenderTrustLevel,
63
62
  category: toDisplayCategory(thread.category),
64
63
  suspicious,
package/src/lib/keymap.ts CHANGED
@@ -130,7 +130,7 @@ export const KEY_HINT_GROUPS: KeyHintGroup[] = [
130
130
  { action: "goBrief", keys: ["g", "b"], description: "Daily brief" },
131
131
  { action: "goInbox", keys: ["g", "i"], description: "Inbox" },
132
132
  { action: "goSent", keys: ["g", "s"], description: "Sent" },
133
- { action: "goFlagged", keys: ["g", "f"], description: "Flagged" },
133
+ { action: "goFlagged", keys: ["g", "f"], description: "Starred" },
134
134
  { action: "goSettings", keys: ["g", ","], description: "Settings" },
135
135
  ],
136
136
  },
@@ -15,7 +15,7 @@ import { ErrorState } from "@/components/ui/ErrorState";
15
15
  const FlaggedError = ({ error, reset }: ErrorComponentProps) => (
16
16
  <div className="flex h-full items-center justify-center bg-canvas p-4">
17
17
  <ErrorState
18
- title="Couldn't load your flagged mail"
18
+ title="Couldn't load your starred mail"
19
19
  error={error}
20
20
  onRetry={reset}
21
21
  />