fix: call media-connect state, blocklist banner staleness, group rename, avatar URL

- stores/calls.ts, CallRoot.tsx: distinguish signaling-connected from
  actual media-joined (mediaJoined flag), so a call whose mic never
  attaches no longer shows a live timer/"网络良好"; propagate a
  disconnected reason on post-accept media failure so both sides agree
  on the outcome instead of one side seeing a normal "已结束".
- DetailColumn.tsx: invalidate conversation-context alongside blocks on
  block/unblock, so the composer's blocked-notice banner updates
  immediately instead of only after a window-focus refetch.
- GroupDetailColumn.tsx: add an owner-only group rename control
  (updateGroup was already implemented but never wired to any UI).
- Avatar.tsx: stop re-wrapping an already-absolute avatar_file_id (seen
  on default-avatar placeholders) into a malformed
  /v0/file/s/https://... request.
This commit is contained in:
Your Name
2026-09-18 15:01:08 +08:00
parent 0c5b329179
commit 7fff1a7b3a
5 changed files with 133 additions and 22 deletions
+27 -6
View File
@@ -35,6 +35,13 @@ interface CallState {
// dynamic Agora SDK import -> client.join() -> createCameraVideoTrack()) hadn't resolved yet, and
// never re-fire once it did, leaving the local preview box permanently blank for the whole call.
hasLocalVideo: boolean;
// Set once `joinCall()` actually resolves (mic track created and published, or for video, mic+
// camera) -- distinct from `phase==='connected'`, which flips the instant the SIGNALING accept
// arrives, before any media has actually joined. `CallWindow`'s status line previously showed a
// ticking duration + "网络良好" from the moment `phase` became 'connected', which live-confirmed
// stays true even when the mic never actually attaches at all (permission denied/no device) -- a
// real user reported this as "看起来接通了" for a call where no audio ever worked on either side.
mediaJoined: boolean;
// `'busy'` is never a real PERSISTED `head.webrtc` value (confirmed live, see dispatch.ts's own
// comment) so it's deliberately NOT part of `CallWebrtcState` -- but it IS a real value this app's
// own `sendCallHangup(..., 'busy')` puts on the LIVE, ephemeral `{info hang-up}` signal a second
@@ -74,6 +81,7 @@ const IDLE = {
connectedAt: null,
hasRemoteVideo: false,
hasLocalVideo: false,
mediaJoined: false,
endReason: null,
wasCaller: false,
mediaError: null,
@@ -123,7 +131,7 @@ export const useCallStore = create<CallState>((set, get) => ({
const join = await joinRtcCall(topic, callId, seq, callType, 'callee');
if (get().callId !== callId) return;
await joinCall(join, callType, (v) => get().setHasRemoteVideo(v));
if (get().callId === callId) set({ hasLocalVideo: callType === 'video' });
if (get().callId === callId) set({ hasLocalVideo: callType === 'video', mediaJoined: true });
} catch {
// Same guard the structurally-identical caller-side accept handler below already has, for the
// identical reason: `callId` here is the ORIGINAL id this whole acceptCall() call was for,
@@ -138,7 +146,14 @@ export const useCallStore = create<CallState>((set, get) => ({
// just only when THIS call is still the live one.
if (get().callId !== callId) return;
const { topic: currentTopic, seq: currentSeq } = get();
if (currentTopic && currentSeq != null) sendCallHangup(getTinode().getTopic(currentTopic), currentSeq, callId);
// `reason: 'disconnected'` (not the default no-reason hang-up) so the OTHER side's own
// `info.event==='hang-up'` handler can tell this apart from a normal hang-up too -- without it,
// the peer's `connectedAt` is already truthy (set the moment accept/accept-event landed, same
// as this side's), so its own reason-mapping fell through to 'finished' and both the live
// overlay AND the permanent chat-history record showed a normal "通话时长" for a call where
// media never actually worked on either end. Live-confirmed: both sides recorded "通话时长
// 00:03" for a call that failed getUserMedia immediately after accept.
if (currentTopic && currentSeq != null) sendCallHangup(getTinode().getTopic(currentTopic), currentSeq, callId, 'disconnected');
leaveCall();
get()._endWithReason('disconnected');
}
@@ -321,7 +336,7 @@ export function initCallSignaling() {
.then(async (join) => {
if (useCallStore.getState().callId !== callId) return;
await joinCall(join, callType, (v) => useCallStore.getState().setHasRemoteVideo(v));
if (useCallStore.getState().callId === callId) useCallStore.setState({ hasLocalVideo: callType === 'video' });
if (useCallStore.getState().callId === callId) useCallStore.setState({ hasLocalVideo: callType === 'video', mediaJoined: true });
})
.catch(() => {
// Same reasoning as acceptCall()'s own catch branch: `connectedAt` was already set (right
@@ -330,7 +345,9 @@ export function initCallSignaling() {
// for a call that never actually connected media at all -- go straight to the honest
// 'disconnected' outcome instead, and still tell the peer so their UI doesn't hang.
if (useCallStore.getState().callId !== callId) return;
if (joinTopic && seq != null) sendCallHangup(getTinode().getTopic(joinTopic), seq, callId);
// `reason: 'disconnected'` -- same fix as acceptCall()'s structurally identical catch,
// so the callee's own hang-up handler maps this to 'disconnected' instead of 'finished'.
if (joinTopic && seq != null) sendCallHangup(getTinode().getTopic(joinTopic), seq, callId, 'disconnected');
leaveCall();
useCallStore.getState()._endWithReason('disconnected');
});
@@ -346,8 +363,12 @@ export function initCallSignaling() {
// already been sending a real `reason:'declined'` all along, just never read back out here),
// and a caller who cancelled before pickup made the CALLEE's overlay wrongly show "通话已结束"
// instead of "未接听" -- mirrors the SAME connectedAt-based finished-vs-missed distinction the
// local hangUp() above already makes for its own no-reason-given case.
useCallStore.getState()._endWithReason(reason === 'busy' ? 'busy' : reason === 'declined' ? 'declined' : connectedAt ? 'finished' : 'missed');
// local hangUp() above already makes for its own no-reason-given case. `'disconnected'` is the
// same fix, for the media-join-failed case: acceptCall()/the accept-handler's own catch above
// now send this explicit reason so the OTHER side shows "连接异常" too, not a fake "通话已结束".
useCallStore
.getState()
._endWithReason(reason === 'busy' ? 'busy' : reason === 'declined' ? 'declined' : reason === 'disconnected' ? 'disconnected' : connectedAt ? 'finished' : 'missed');
}
});
}