@remit/web-client 0.0.309 → 0.0.311

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.309",
3
+ "version": "0.0.311",
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": {
@@ -327,57 +327,67 @@ const WiredComposeHeader = ({
327
327
  // the keyboard down with it and started the cycle over.
328
328
  const [expandedFor, setExpandedFor] = useState<number | undefined>(undefined);
329
329
  const expanded = expandedFor === documentGeneration;
330
+ const [focusWithin, setFocusWithin] = useState(false);
330
331
 
331
332
  return (
332
- <ComposeHeader
333
- collapsed={!isDesktop && isKeyboardOpen && !expanded}
334
- onExpand={() => setExpandedFor(documentGeneration)}
335
- summary={composeHeaderSummary({
336
- to: toAddresses,
337
- cc: ccAddresses,
338
- bcc: bccAddresses,
339
- subject,
340
- })}
341
- from={
342
- <FromSelector
343
- selectedAccountId={selectedAccountId}
344
- onSelect={onAccountChange}
345
- />
346
- }
347
- to={
348
- <AddressField
349
- label="To"
350
- addresses={toAddresses}
351
- onChange={setToAddresses}
352
- placeholder="Recipients"
353
- onPendingChange={onToPendingChange}
354
- ref={fieldHandles.to}
355
- />
356
- }
357
- cc={
358
- showCc ? (
359
- <AddressField
360
- label="Cc"
361
- addresses={ccAddresses}
362
- onChange={setCcAddresses}
363
- ref={fieldHandles.cc}
333
+ <div
334
+ className="contents"
335
+ onFocusCapture={() => setFocusWithin(true)}
336
+ onBlurCapture={(event) => {
337
+ if (event.currentTarget.contains(event.relatedTarget)) return;
338
+ setFocusWithin(false);
339
+ }}
340
+ >
341
+ <ComposeHeader
342
+ collapsed={!isDesktop && isKeyboardOpen && !expanded && !focusWithin}
343
+ onExpand={() => setExpandedFor(documentGeneration)}
344
+ summary={composeHeaderSummary({
345
+ to: toAddresses,
346
+ cc: ccAddresses,
347
+ bcc: bccAddresses,
348
+ subject,
349
+ })}
350
+ from={
351
+ <FromSelector
352
+ selectedAccountId={selectedAccountId}
353
+ onSelect={onAccountChange}
364
354
  />
365
- ) : undefined
366
- }
367
- bcc={
368
- showBcc ? (
355
+ }
356
+ to={
369
357
  <AddressField
370
- label="Bcc"
371
- addresses={bccAddresses}
372
- onChange={setBccAddresses}
373
- ref={fieldHandles.bcc}
358
+ label="To"
359
+ addresses={toAddresses}
360
+ onChange={setToAddresses}
361
+ placeholder="Recipients"
362
+ onPendingChange={onToPendingChange}
363
+ ref={fieldHandles.to}
374
364
  />
375
- ) : undefined
376
- }
377
- subject={<ComposeSubjectField value={subject} onChange={setSubject} />}
378
- onShowCc={() => setShowCc(true)}
379
- onShowBcc={() => setShowBcc(true)}
380
- />
365
+ }
366
+ cc={
367
+ showCc ? (
368
+ <AddressField
369
+ label="Cc"
370
+ addresses={ccAddresses}
371
+ onChange={setCcAddresses}
372
+ ref={fieldHandles.cc}
373
+ />
374
+ ) : undefined
375
+ }
376
+ bcc={
377
+ showBcc ? (
378
+ <AddressField
379
+ label="Bcc"
380
+ addresses={bccAddresses}
381
+ onChange={setBccAddresses}
382
+ ref={fieldHandles.bcc}
383
+ />
384
+ ) : undefined
385
+ }
386
+ subject={<ComposeSubjectField value={subject} onChange={setSubject} />}
387
+ onShowCc={() => setShowCc(true)}
388
+ onShowBcc={() => setShowBcc(true)}
389
+ />
390
+ </div>
381
391
  );
382
392
  };
383
393
 
@@ -14,7 +14,10 @@
14
14
 
15
15
  import assert from "node:assert/strict";
16
16
  import { afterEach, describe, it } from "node:test";
17
- import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
17
+ import type {
18
+ RemitImapAccountResponse,
19
+ RemitImapDescribeMessageResponse,
20
+ } from "@remit/api-http-client/types.gen.ts";
18
21
  import {
19
22
  type AnyRouter,
20
23
  createMemoryHistory,
@@ -44,6 +47,20 @@ const account = {
44
47
  smtpEnabled: true,
45
48
  } as unknown as RemitImapAccountResponse;
46
49
 
50
+ const sourceMessage = {
51
+ message: { messageId: "msg-1" },
52
+ envelope: {
53
+ subject: "Lunch",
54
+ messageIdValue: "<m1@example.com>",
55
+ from: [{ normalizedEmail: "them@example.com", displayName: "Them" }],
56
+ replyTo: [],
57
+ to: [],
58
+ cc: [],
59
+ },
60
+ references: [],
61
+ bodyParts: [],
62
+ } as unknown as RemitImapDescribeMessageResponse;
63
+
47
64
  interface VisualViewportStub {
48
65
  height: number;
49
66
  addEventListener: (type: string, listener: () => void) => void;
@@ -97,15 +114,22 @@ const testRouter = (): AnyRouter =>
97
114
  history: createMemoryHistory({ initialEntries: ["/mail/mbx-1"] }),
98
115
  }) as unknown as AnyRouter;
99
116
 
100
- const Opened = () =>
117
+ const Opened = ({ mode }: { mode: "new" | "reply" }) =>
101
118
  createElement(ComposeForm, {
102
- mode: "new",
119
+ mode,
103
120
  account,
121
+ sourceMessage: mode === "reply" ? sourceMessage : undefined,
104
122
  onDraftCreated: () => {},
105
123
  onClose: () => {},
106
124
  });
107
125
 
108
- const mount = async (): Promise<void> => {
126
+ const mount = async ({
127
+ mode = "new",
128
+ keyboardOpen = true,
129
+ }: {
130
+ mode?: "new" | "reply";
131
+ keyboardOpen?: boolean;
132
+ } = {}): Promise<void> => {
109
133
  http = mockFetch(async (call) => {
110
134
  if (call.path.endsWith("/config")) return { accounts: [account] };
111
135
  return { items: [] };
@@ -115,7 +139,9 @@ const mount = async (): Promise<void> => {
115
139
  configurable: true,
116
140
  value: viewport,
117
141
  });
118
- viewport.height = globalThis.window.innerHeight - KEYBOARD_HEIGHT;
142
+ viewport.height = keyboardOpen
143
+ ? globalThis.window.innerHeight - KEYBOARD_HEIGHT
144
+ : globalThis.window.innerHeight;
119
145
 
120
146
  harness = createDomHarness(PHONE);
121
147
  harness.renderApp(
@@ -125,7 +151,7 @@ const mount = async (): Promise<void> => {
125
151
  children: createElement(
126
152
  ComposeProvider,
127
153
  null,
128
- createElement(Opened, null),
154
+ createElement(Opened, { mode }),
129
155
  ),
130
156
  }),
131
157
  );
@@ -171,4 +197,33 @@ describe("the phone compose header and the software keyboard", () => {
171
197
  "the header did not collapse over the field being typed into",
172
198
  );
173
199
  });
200
+
201
+ it("keeps To mounted when a reply brings the keyboard up over it", async () => {
202
+ await mount({ mode: "reply", keyboardOpen: false });
203
+
204
+ const input = recipientInput();
205
+ assert.ok(input, "the rows are there while the keyboard is down");
206
+
207
+ act(() => {
208
+ input.focus();
209
+ });
210
+ setKeyboard(true);
211
+ await harness?.flush();
212
+
213
+ assert.equal(
214
+ recipientInput(),
215
+ input,
216
+ "the recipient field stayed mounted under the keyboard",
217
+ );
218
+ assert.equal(
219
+ globalThis.document.activeElement,
220
+ input,
221
+ "the recipient field kept focus",
222
+ );
223
+ assert.equal(
224
+ collapsedBar(),
225
+ null,
226
+ "the header did not collapse over the field being typed into",
227
+ );
228
+ });
174
229
  });
@@ -105,6 +105,10 @@ export interface CalendarSuggestionAnswers {
105
105
  suggestionId: string,
106
106
  calendarId: string,
107
107
  ) => Promise<SuggestionAnswer>;
108
+ tentative: (
109
+ suggestionId: string,
110
+ calendarId: string,
111
+ ) => Promise<SuggestionAnswer>;
108
112
  decline: (suggestionId: string) => Promise<SuggestionAnswer>;
109
113
  dismiss: (
110
114
  suggestionId: string,
@@ -144,6 +148,17 @@ export function useCalendarSuggestionAnswers(): CalendarSuggestionAnswers {
144
148
  .catch(refused),
145
149
  [accept],
146
150
  );
151
+ const tentativeSuggestion = useCallback(
152
+ (suggestionId: string, calendarId: string) =>
153
+ accept
154
+ .mutateAsync({
155
+ path: { suggestionId },
156
+ body: { calendarId, answer: "Tentative" },
157
+ })
158
+ .then(() => ANSWERED)
159
+ .catch(refused),
160
+ [accept],
161
+ );
147
162
  const declineSuggestion = useCallback(
148
163
  (suggestionId: string) =>
149
164
  decline
@@ -164,12 +179,14 @@ export function useCalendarSuggestionAnswers(): CalendarSuggestionAnswers {
164
179
  return useMemo(
165
180
  () => ({
166
181
  accept: acceptSuggestion,
182
+ tentative: tentativeSuggestion,
167
183
  decline: declineSuggestion,
168
184
  dismiss: dismissSuggestion,
169
185
  isAnswering: accept.isPending || decline.isPending || dismiss.isPending,
170
186
  }),
171
187
  [
172
188
  acceptSuggestion,
189
+ tentativeSuggestion,
173
190
  declineSuggestion,
174
191
  dismissSuggestion,
175
192
  accept.isPending,
@@ -245,6 +245,12 @@ export function useIntelligenceCalendar(
245
245
  answer(invitationId, "add this to your calendar", () =>
246
246
  answers.accept(invitationId, defaultCalendarId),
247
247
  ),
248
+ onTentativeInvite: cancellation
249
+ ? undefined
250
+ : () =>
251
+ answer(invitationId, "add this as a maybe", () =>
252
+ answers.tentative(invitationId, defaultCalendarId),
253
+ ),
248
254
  onDeclineInvite: () =>
249
255
  answer(invitationId, "decline this", () => answers.decline(invitationId)),
250
256
  onMuteInvite: () =>