fix(web): code review round 2 -- critical session/connection races

- RequireAuth: a page reload lost the session even with a valid token
  sitting in sessionStorage -- myUserId is never persisted (by design)
  but nothing ever re-derived it from the reusable login token. Added
  connection.ts's restoreSession(), which also fixes the race the first
  attempt at this ran into live: React 19 StrictMode double-invokes the
  restore effect in dev, and Tinode.loginToken() isn't idempotent (second
  call on an already-authenticated connection gets 409'd), so both the
  connect() and the login itself needed module-level in-flight dedup,
  not just a per-effect cancelled flag
- topics.ts: same dedup pattern for openConversation/closeConversation --
  StrictMode's mount->cleanup->remount always overlapped an open with
  itself, and a leave() landing before its own subscribe() resolved was
  silently no-op'ing instead of detaching
- LOGGING_ENABLED was unconditionally on, including prod builds --
  Tinode's verbose logging prints the raw login secret and full message
  content to the console
- imPost/imPatch/imPut/imDelete could silently drop the Idempotency-Key
  header for a caller that passes its own headers (object-spread order
  bug, dormant today since no call site does this yet)
- DetailColumn's getFriends()/getBlocks() only ever read page 1 despite
  fetchAllPages existing specifically to prevent this bug class
- availableActions() now defers to the server's message_policy.
  supported_actions when present instead of only a client-side time
  window, and exposes recallRemainingSeconds for the context menu's
  countdown label

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-08 04:48:28 +08:00
co-authored by Claude Sonnet 5
parent 1bd8d7f804
commit d3a0eb0dbc
12 changed files with 181 additions and 65 deletions
@@ -1,29 +0,0 @@
import { useEffect } from 'react';
import { Icon } from '@/app/Icon';
import { TOAST_DURATION } from '@/lib/config';
interface ToastProps {
message: string;
onDone: () => void;
}
export function Toast({ message, onDone }: ToastProps) {
useEffect(() => {
if (!message) return undefined;
const timer = window.setTimeout(onDone, TOAST_DURATION);
return () => window.clearTimeout(timer);
}, [message, onDone]);
if (!message) return null;
return (
<div
role="status"
className="fixed bottom-8 left-1/2 flex -translate-x-1/2 items-center gap-2 rounded-lg bg-[#1f2329] px-4 py-3 text-sm text-white shadow-modal"
>
<Icon name="check_circle" className="text-[18px] text-success" />
{message}
</div>
);
}