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:
co-authored by
Claude Sonnet 5
parent
1bd8d7f804
commit
d3a0eb0dbc
@@ -9,6 +9,21 @@ const LOCAL_SEQID = 0xfffffff;
|
||||
|
||||
export type MessageAction = 'copy' | 'quote' | 'favorite' | 'forward' | 'recall' | 'delete' | 'select';
|
||||
|
||||
/**
|
||||
* Seconds left in which `msg` can still be recalled, or `null` if it can't be recalled at all
|
||||
* (not the sender, not yet server-confirmed, no window configured, or the window already lapsed).
|
||||
* Split out from availableActions() so the context menu can also render the "1:42 内" countdown
|
||||
* README §3.5 calls for, not just decide whether to show the action.
|
||||
*/
|
||||
export function recallRemainingSeconds(msg: StoredMessage, myUserId: string | null, messagePolicy: MessagePolicy | undefined): number | null {
|
||||
const mine = !!myUserId && msg.from === myUserId;
|
||||
const isConfirmed = typeof msg.seq === 'number' && msg.seq < LOCAL_SEQID;
|
||||
const recallWindow = messagePolicy?.recall_window_seconds ?? 0;
|
||||
if (!mine || !isConfirmed || recallWindow <= 0 || !msg.ts) return null;
|
||||
const remaining = recallWindow - Math.floor((Date.now() - msg.ts.getTime()) / 1000);
|
||||
return remaining > 0 ? remaining : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which context-menu actions apply to one message. Scoped to P2P for now (no group-admin recall,
|
||||
* no self-topic hard-delete-always -- those need group role/topic-type knowledge Phase 3 will add).
|
||||
@@ -19,10 +34,11 @@ export type MessageAction = 'copy' | 'quote' | 'favorite' | 'forward' | 'recall'
|
||||
export function availableActions(msg: StoredMessage, myUserId: string | null, messagePolicy: MessagePolicy | undefined): MessageAction[] {
|
||||
if (isServerDeleted(msg) || isRecalled(msg)) return [];
|
||||
const actions: MessageAction[] = ['quote', 'copy', 'favorite', 'forward'];
|
||||
const mine = !!myUserId && msg.from === myUserId;
|
||||
const isConfirmed = typeof msg.seq === 'number' && msg.seq < LOCAL_SEQID;
|
||||
const recallWindow = messagePolicy?.recall_window_seconds ?? 0;
|
||||
if (mine && isConfirmed && recallWindow > 0 && msg.ts && msg.ts.getTime() > Date.now() - recallWindow * 1000) {
|
||||
// Defer to the server's own supported_actions list when it's present, so a server-side policy
|
||||
// change (recall disabled tenant-wide, say) doesn't leave the client offering an action that
|
||||
// will just 400 when tapped -- the time-window check below still applies on top of this.
|
||||
const serverAllowsRecall = !messagePolicy?.supported_actions || messagePolicy.supported_actions.includes('recall');
|
||||
if (serverAllowsRecall && recallRemainingSeconds(msg, myUserId, messagePolicy) !== null) {
|
||||
actions.push('recall');
|
||||
}
|
||||
actions.push('delete', 'select');
|
||||
|
||||
Reference in New Issue
Block a user