diff --git a/web/src/app/Avatar.tsx b/web/src/app/Avatar.tsx
index 75ad4aa8..06294838 100644
--- a/web/src/app/Avatar.tsx
+++ b/web/src/app/Avatar.tsx
@@ -20,12 +20,18 @@ interface AvatarProps {
* through every profile-update API in this app but never actually RENDERED anywhere -- every avatar
* call site across the whole codebase drew only the letter-tile fallback, so uploading a custom
* avatar produced zero visible change anywhere, including on the very page the upload happened.
- * `avatar_file_id` is a bare file id (e.g. "abc123"), not a ref path -- reconstruct the
- * `/v0/file/s/{id}` shape `useAuthedBlobUrl` (same authenticated-fetch-to-blob-URL mechanism already
- * used for chat attachments) expects.
+ * `avatar_file_id` is USUALLY a bare file id (e.g. "abc123"), needing the `/v0/file/s/{id}` shape
+ * `useAuthedBlobUrl` (same authenticated-fetch-to-blob-URL mechanism already used for chat
+ * attachments) reconstructed -- but live-confirmed (2026-09-18, real console error): default-avatar
+ * placeholders come back as an ALREADY-COMPLETE OSS URL in this same field (e.g.
+ * "https://im-oss-t1.oss-cn-shenzhen.aliyuncs.com/im-assets/default-avatars/male/2.png"), which this
+ * used to unconditionally re-wrap into "/v0/file/s/https://...", a malformed request that 404s.
+ * `useAuthedBlobUrl`/`fileDownloadUrl` already accept either shape (see hooks.ts's own comment) --
+ * only needed to stop force-prefixing a value that's already a full URL.
*/
export function Avatar({ id, name, avatarFileId, className = '', fallbackBgClass }: AvatarProps) {
- const remote = useAuthedBlobUrl(avatarFileId ? `/v0/file/s/${avatarFileId}` : undefined);
+ const isAbsoluteUrl = !!avatarFileId && /^https?:\/\//.test(avatarFileId);
+ const remote = useAuthedBlobUrl(avatarFileId ? (isAbsoluteUrl ? avatarFileId : `/v0/file/s/${avatarFileId}`) : undefined);
if (remote.url) {
return ;
}
diff --git a/web/src/features/calls/CallRoot.tsx b/web/src/features/calls/CallRoot.tsx
index a8443f56..00ea4fad 100644
--- a/web/src/features/calls/CallRoot.tsx
+++ b/web/src/features/calls/CallRoot.tsx
@@ -127,7 +127,7 @@ function MobileIncomingCall() {
* minimizing is pure local UI state, matching the old app's own real behavior (CallPanel is never
* unmounted between full-screen and minimized). */
function MinimizedCallBar() {
- const { topic, peerName, connectedAt, muted, toggleMinimize, toggleMute, hangUp } = useCallStore();
+ const { topic, peerName, connectedAt, mediaJoined, muted, toggleMinimize, toggleMute, hangUp } = useCallStore();
const elapsed = useElapsedSeconds(connectedAt);
return (
// `bottom-[106px] sm:bottom-6`: same viewport-vs-Outlet-box fixed-positioning issue as Toast.tsx
@@ -141,7 +141,10 @@ function MinimizedCallBar() {
{peerName}
- {connectedAt ? `通话中 · ${formatElapsed(elapsed)}` : '正在连接…'}
+ {/* `mediaJoined`, not bare `connectedAt` -- signaling accept flips `connectedAt` before
+ media has actually joined (see stores/calls.ts's own comment on this field), so this
+ used to claim "通话中" for a call whose mic never actually attached. */}
+ {connectedAt && mediaJoined ? `通话中 · ${formatElapsed(elapsed)}` : '正在连接…'}