@remit/web-client 0.0.19 → 0.0.21
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.
|
|
3
|
+
"version": "0.0.21",
|
|
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": {
|
|
@@ -161,9 +161,10 @@ export const ConversationView = ({
|
|
|
161
161
|
error,
|
|
162
162
|
refetch,
|
|
163
163
|
} = useQuery({
|
|
164
|
+
// The API reads a conversation oldest first (#81) and the pane renders it
|
|
165
|
+
// in that order, so no `order` is asked for and none is re-sorted here.
|
|
164
166
|
...threadDetailOperationsListThreadMessagesOptions({
|
|
165
167
|
path: { threadId },
|
|
166
|
-
query: { order: "desc" },
|
|
167
168
|
}),
|
|
168
169
|
});
|
|
169
170
|
|
|
@@ -171,21 +172,24 @@ export const ConversationView = ({
|
|
|
171
172
|
() => messagesResponse?.items ?? [],
|
|
172
173
|
[messagesResponse?.items],
|
|
173
174
|
);
|
|
175
|
+
const latestMessage = messages[messages.length - 1];
|
|
174
176
|
const messageRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
|
175
177
|
|
|
176
178
|
// Track which messages are expanded
|
|
177
|
-
// By default, the
|
|
179
|
+
// By default, the latest message is expanded
|
|
178
180
|
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
|
179
181
|
const [currentThreadId, setCurrentThreadId] = useState<string | null>(null);
|
|
180
182
|
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
181
183
|
|
|
182
|
-
// Reset and expand
|
|
184
|
+
// Reset and expand the latest message when thread changes or messages load.
|
|
183
185
|
// Also expand the selected message (the row the user clicked in the list)
|
|
184
186
|
// so that an older unread message is visible and gets marked as read.
|
|
185
187
|
useEffect(() => {
|
|
186
188
|
if (messages.length > 0 && threadId !== currentThreadId) {
|
|
187
189
|
setCurrentThreadId(threadId);
|
|
188
|
-
const initialExpanded = new Set([
|
|
190
|
+
const initialExpanded = new Set([
|
|
191
|
+
messages[messages.length - 1].threadMessageId,
|
|
192
|
+
]);
|
|
189
193
|
if (selectedMessageId) {
|
|
190
194
|
const selected = messages.find(
|
|
191
195
|
(m) => m.messageId === selectedMessageId,
|
|
@@ -193,7 +197,7 @@ export const ConversationView = ({
|
|
|
193
197
|
if (selected) initialExpanded.add(selected.threadMessageId);
|
|
194
198
|
}
|
|
195
199
|
setExpandedIds(initialExpanded);
|
|
196
|
-
setFocusedIndex(
|
|
200
|
+
setFocusedIndex(messages.length - 1);
|
|
197
201
|
}
|
|
198
202
|
}, [threadId, messages, currentThreadId, selectedMessageId]);
|
|
199
203
|
|
|
@@ -283,12 +287,13 @@ export const ConversationView = ({
|
|
|
283
287
|
}
|
|
284
288
|
}, [composeRequest, onComposeClose]);
|
|
285
289
|
|
|
286
|
-
|
|
287
|
-
|
|
290
|
+
// Reply and forward act on the latest turn of the conversation, which is the
|
|
291
|
+
// last message now that the thread reads oldest first.
|
|
292
|
+
const { data: latestMessageData } = useQuery({
|
|
288
293
|
...messageOperationsDescribeMessageOptions({
|
|
289
|
-
path: { messageId:
|
|
294
|
+
path: { messageId: latestMessage?.messageId ?? "" },
|
|
290
295
|
}),
|
|
291
|
-
enabled: !!
|
|
296
|
+
enabled: !!latestMessage && composeMode !== null,
|
|
292
297
|
});
|
|
293
298
|
|
|
294
299
|
const handleReply = useCallback(() => {
|
|
@@ -432,7 +437,7 @@ export const ConversationView = ({
|
|
|
432
437
|
<InlineCompose
|
|
433
438
|
mode={composeMode}
|
|
434
439
|
account={activeAccount}
|
|
435
|
-
sourceMessage={
|
|
440
|
+
sourceMessage={latestMessageData}
|
|
436
441
|
onClose={handleCloseCompose}
|
|
437
442
|
/>
|
|
438
443
|
)}
|
|
@@ -454,7 +459,7 @@ export const ConversationView = ({
|
|
|
454
459
|
<InlineCompose
|
|
455
460
|
mode={composeMode}
|
|
456
461
|
account={activeAccount}
|
|
457
|
-
sourceMessage={
|
|
462
|
+
sourceMessage={latestMessageData}
|
|
458
463
|
onClose={handleCloseCompose}
|
|
459
464
|
/>
|
|
460
465
|
)}
|
|
@@ -232,6 +232,12 @@ export const MessageList = ({
|
|
|
232
232
|
// leave this null, so the list never yanks focus out of the reading pane.
|
|
233
233
|
const pendingDomFocusRef = useRef<string | null>(null);
|
|
234
234
|
|
|
235
|
+
// The row the cursor was on when the delete confirmation opened. The dialog
|
|
236
|
+
// takes DOM focus for as long as it is up, so dismissing it has to give that
|
|
237
|
+
// focus back or the list is left with no cursor and the next shortcut acts on
|
|
238
|
+
// nothing (#80).
|
|
239
|
+
const focusBeforeConfirmRef = useRef<string | null>(null);
|
|
240
|
+
|
|
235
241
|
// Whether the list can serve keyboard commands at all. It stays true while
|
|
236
242
|
// the delete confirmation is open — withdrawing the commands there would let
|
|
237
243
|
// the route fall through to its own unconfirmed delete on a second Delete
|
|
@@ -341,9 +347,10 @@ export const MessageList = ({
|
|
|
341
347
|
const requestDelete = useCallback(
|
|
342
348
|
(ids: string[]) => {
|
|
343
349
|
if (!onDeleteMessages || ids.length === 0) return;
|
|
350
|
+
focusBeforeConfirmRef.current = focusedMessageId ?? null;
|
|
344
351
|
setPendingDeleteIds(ids);
|
|
345
352
|
},
|
|
346
|
-
[onDeleteMessages],
|
|
353
|
+
[onDeleteMessages, focusedMessageId],
|
|
347
354
|
);
|
|
348
355
|
|
|
349
356
|
// Keyboard shift-arrow range extend: move focus one row in `direction` and
|
|
@@ -452,9 +459,14 @@ export const MessageList = ({
|
|
|
452
459
|
|
|
453
460
|
onDeleteMessages?.(pendingDeleteIds);
|
|
454
461
|
clearSelection();
|
|
462
|
+
focusBeforeConfirmRef.current = null;
|
|
455
463
|
setPendingDeleteIds(null);
|
|
456
464
|
|
|
457
465
|
if (nextFocus !== undefined) {
|
|
466
|
+
// Same hand-back as cancelling, aimed at the surviving neighbour
|
|
467
|
+
// instead: confirming also closes a dialog that held DOM focus.
|
|
468
|
+
pendingDomFocusRef.current = nextFocus;
|
|
469
|
+
setFocusedMessageId(nextFocus);
|
|
458
470
|
navigate({
|
|
459
471
|
to: "/mail/$mailboxId",
|
|
460
472
|
params: { mailboxId },
|
|
@@ -471,8 +483,17 @@ export const MessageList = ({
|
|
|
471
483
|
mailboxId,
|
|
472
484
|
]);
|
|
473
485
|
|
|
486
|
+
// Every way out of the confirmation that isn't the delete — Escape, Cancel,
|
|
487
|
+
// the backdrop — arrives here, so this is the one place the keyboard has to
|
|
488
|
+
// be handed back. Restoring the cursor also puts DOM focus back on that row,
|
|
489
|
+
// via the same pending-focus channel j/k use.
|
|
474
490
|
const handleCancelDelete = useCallback(() => {
|
|
491
|
+
const restoreTo = focusBeforeConfirmRef.current;
|
|
492
|
+
focusBeforeConfirmRef.current = null;
|
|
475
493
|
setPendingDeleteIds(null);
|
|
494
|
+
if (restoreTo === null) return;
|
|
495
|
+
pendingDomFocusRef.current = restoreTo;
|
|
496
|
+
setFocusedMessageId(restoreTo);
|
|
476
497
|
}, []);
|
|
477
498
|
|
|
478
499
|
// Handle mark as read
|