feat(calls): EC iframe load watchdog + recovery UI; avatar decorations on call tiles

- CallEmbed: 25s load watchdog that fails fast on iframe error / preparing-error /
  timeout instead of hanging on a permanent spinner; additive onLoadError API,
  cleared on ready/capabilities/joined.
- CallView: user-visible "call failed to load" overlay with Retry/Leave (folds +
  tokens) via a new useCallLoadError hook.
- CallMemberCard: wrap the participant avatar in AvatarDecoration so decorations
  render in the call roster (the tile rendered UserAvatar bare while member lists
  already wrapped it).

Addresses LOTUS_BUGS item 3 (avatar decorations in calls) and EC iframe failure monitoring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 08:22:01 -04:00
parent d2946c00ce
commit 0394fce929
4 changed files with 163 additions and 13 deletions
+49 -5
View File
@@ -1,6 +1,13 @@
import React, { RefObject, useRef } from 'react';
import { Badge, Box, color, Header, Scroll, Text, toRem } from 'folds';
import { useCallEmbed, useCallJoined, useCallEmbedPlacementSync } from '../../hooks/useCallEmbed';
import { useSetAtom } from 'jotai';
import { Badge, Box, Button, color, Header, Icon, Icons, Scroll, Text, toRem } from 'folds';
import {
useCallEmbed,
useCallJoined,
useCallEmbedPlacementSync,
useCallLoadError,
} from '../../hooks/useCallEmbed';
import { callEmbedAtom } from '../../state/callEmbed';
import { ContainerColor } from '../../styles/ContainerColor.css';
import { PrescreenControls } from './PrescreenControls';
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
@@ -153,6 +160,37 @@ function CallPrescreen() {
);
}
function CallLoadErrorMessage() {
const setCallEmbed = useSetAtom(callEmbedAtom);
// Disposing the embed tears down the hung iframe and returns the user to the
// prescreen, from which they can join again ("Retry") or simply walk away.
const dismiss = () => setCallEmbed(undefined);
return (
<Box
className={css.CallViewContent}
direction="Column"
alignItems="Center"
justifyContent="Center"
gap="300"
>
<Icon src={Icons.Warning} size="400" style={{ color: color.Critical.Main }} />
<Text style={{ color: color.Critical.Main }} size="L400" align="Center">
The call failed to load. Check your connection and try again.
</Text>
<Box gap="200" alignItems="Center">
<Button variant="Primary" size="300" radii="400" onClick={dismiss}>
<Text size="B400">Retry</Text>
</Button>
<Button variant="Secondary" fill="Soft" size="300" radii="400" onClick={dismiss}>
<Text size="B400">Leave</Text>
</Button>
</Box>
</Box>
);
}
type CallJoinedProps = {
containerRef: RefObject<HTMLDivElement>;
joined: boolean;
@@ -175,8 +213,13 @@ export function CallView() {
const callEmbed = useCallEmbed();
const callJoined = useCallJoined(callEmbed);
const loadError = useCallLoadError(callEmbed);
const currentJoined = callEmbed?.roomId === room.roomId && callJoined;
const isCurrentRoom = callEmbed?.roomId === room.roomId;
const currentJoined = isCurrentRoom && callJoined;
// Show the recovery UI when this room's embed failed to load and we never
// made it into the call (a hung iframe / blank spinner otherwise).
const showLoadError = isCurrentRoom && !currentJoined && Boolean(loadError);
return (
<Box
@@ -184,8 +227,9 @@ export function CallView() {
style={{ minWidth: toRem(280) }}
grow="Yes"
>
{!currentJoined && <CallPrescreen />}
<CallJoined joined={currentJoined} containerRef={callContainerRef} />
{showLoadError && <CallLoadErrorMessage />}
{!currentJoined && !showLoadError && <CallPrescreen />}
{!showLoadError && <CallJoined joined={currentJoined} containerRef={callContainerRef} />}
</Box>
);
}