@remit/web-client 0.0.20 → 0.0.22

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.20",
3
+ "version": "0.0.22",
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 first (newest) message is expanded
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 first message when thread changes or messages load.
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([messages[0].threadMessageId]);
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(0);
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
- const lastMessage = messages[0];
287
- const { data: lastMessageData } = useQuery({
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: lastMessage?.messageId ?? "" },
294
+ path: { messageId: latestMessage?.messageId ?? "" },
290
295
  }),
291
- enabled: !!lastMessage && composeMode !== null,
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={lastMessageData}
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={lastMessageData}
462
+ sourceMessage={latestMessageData}
458
463
  onClose={handleCloseCompose}
459
464
  />
460
465
  )}
@@ -232,6 +232,16 @@ 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
+ // Whether the cursor's last move came from a row taking DOM focus — a click,
236
+ // or the browser restoring focus — rather than from a command this list ran.
237
+ // The list scrolls the cursor into view only for its own moves. Scrolling for
238
+ // a click moves the row out from under the pointer between mousedown and the
239
+ // click event, so the click lands on the empty space the row left behind and
240
+ // nothing opens (#85). Only rows below the fold could hit it: the pointer has
241
+ // to have scrolled to reach them, and the list then scrolled again on top of
242
+ // that. Every command below resets this to false before moving the cursor.
243
+ const cursorMovedByPointerRef = useRef(false);
244
+
235
245
  // The row the cursor was on when the delete confirmation opened. The dialog
236
246
  // takes DOM focus for as long as it is up, so dismissing it has to give that
237
247
  // focus back or the list is left with no cursor and the next shortcut acts on
@@ -281,6 +291,7 @@ export const MessageList = ({
281
291
  return;
282
292
  }
283
293
  pendingDomFocusRef.current = thread.messageId;
294
+ cursorMovedByPointerRef.current = false;
284
295
  setFocusedMessageId(thread.messageId);
285
296
  },
286
297
  [threads, isMultiSelectMode, toggleCheck],
@@ -367,6 +378,7 @@ export const MessageList = ({
367
378
  // the selection from the anchor — the keyboard equivalent of
368
379
  // shift-click.
369
380
  pendingDomFocusRef.current = target;
381
+ cursorMovedByPointerRef.current = false;
370
382
  setFocusedMessageId(target);
371
383
  },
372
384
  [orderedIds, focusedMessageId, selectRange],
@@ -466,6 +478,7 @@ export const MessageList = ({
466
478
  // Same hand-back as cancelling, aimed at the surviving neighbour
467
479
  // instead: confirming also closes a dialog that held DOM focus.
468
480
  pendingDomFocusRef.current = nextFocus;
481
+ cursorMovedByPointerRef.current = false;
469
482
  setFocusedMessageId(nextFocus);
470
483
  navigate({
471
484
  to: "/mail/$mailboxId",
@@ -493,6 +506,7 @@ export const MessageList = ({
493
506
  setPendingDeleteIds(null);
494
507
  if (restoreTo === null) return;
495
508
  pendingDomFocusRef.current = restoreTo;
509
+ cursorMovedByPointerRef.current = false;
496
510
  setFocusedMessageId(restoreTo);
497
511
  }, []);
498
512
 
@@ -572,6 +586,10 @@ export const MessageList = ({
572
586
  // Scroll the roving focus cursor into view as it moves (j/k). Falls back to
573
587
  // the open thread when nothing is focused yet.
574
588
  useEffect(() => {
589
+ // A row that took focus from the pointer is already where the user aimed,
590
+ // and scrolling it now would move it out from under the click still in
591
+ // flight (#85).
592
+ if (cursorMovedByPointerRef.current) return;
575
593
  // On single-pane tiers, opening a thread swaps this list out for the
576
594
  // conversation. Scrolling the list as it unmounts is both pointless (it's
577
595
  // no longer visible) and unsafe: @tanstack/react-virtual's scrollToIndex
@@ -590,6 +608,9 @@ export const MessageList = ({
590
608
  // sync on open while remaining independent during scanning.
591
609
  useEffect(() => {
592
610
  if (selectedMessageId) {
611
+ // A thread going open is the list's own move, whatever opened it — a
612
+ // deep link arrives with the row far down and has to be scrolled to.
613
+ cursorMovedByPointerRef.current = false;
593
614
  setFocusedMessageId(selectedMessageId);
594
615
  }
595
616
  }, [selectedMessageId]);
@@ -818,6 +839,7 @@ export const MessageList = ({
818
839
  // A row focused by Tab or click becomes the cursor, so the keys act on what
819
840
  // the browser says is focused.
820
841
  const handleRowFocus = useCallback((messageId: string) => {
842
+ cursorMovedByPointerRef.current = true;
821
843
  setFocusedMessageId(messageId);
822
844
  }, []);
823
845