Improve internal model of ringing, expose ringing intent to call UI

I found our code's internal model of ringing a little overgrown (it had superfluous states like 'unknown') and difficult to extend with metadata or callbacks relating to ring attempts. By modeling ringing instead as a stream of ring attempts, where each attempt has an intent, a recipient, and an eventual outcome (accept/decline/timeout), I find it more natural to work with.

This makes room for a future 'try again' callback to allow ringing someone again after a timeout, and also forced me to look for a simpler solution to the duplicate leave sound effects. I exposed the intent of the ringing attempt to the call UI so I can later use it in the header.
This commit is contained in:
Robin
2026-06-10 15:49:00 +02:00
parent 3a824dfff0
commit 2ac6cdeb46
13 changed files with 468 additions and 577 deletions

View File

@@ -26,7 +26,6 @@ import {
createRingingMedia,
type RingingMediaViewModel,
} from "../state/media/RingingMediaViewModel";
import { type MuteStates } from "../state/MuteStates";
global.IntersectionObserver = class MockIntersectionObserver {
public observe(): void {}
@@ -93,10 +92,8 @@ test("GridTile displays ringing media", async () => {
>("ringing");
const vm = createRingingMedia({
pickupState$,
muteStates: {
video: { enabled$: constant(false) },
} as unknown as MuteStates,
id: "test",
intent: "audio",
userId: "@alice:example.org",
displayName$: constant("Alice"),
mxcAvatarUrl$: constant(undefined),

View File

@@ -77,7 +77,6 @@ const RingingMediaTile: FC<RingingMediaTileProps> = ({
}) => {
const { t } = useTranslation();
const pickupState = useBehavior(vm.pickupState$);
const videoEnabled = useBehavior(vm.videoEnabled$);
return (
<MediaView
@@ -89,11 +88,12 @@ const RingingMediaTile: FC<RingingMediaTileProps> = ({
pickupState === "ringing"
? {
text: t("video_tile.calling"),
Icon: videoEnabled ? VideoCallSolidIcon : VoiceCallSolidIcon,
Icon:
vm.intent === "video" ? VideoCallSolidIcon : VoiceCallSolidIcon,
}
: { text: t("video_tile.call_ended"), Icon: EndCallIcon }
}
videoEnabled={videoEnabled}
videoEnabled={false}
videoFit="cover"
mirror={false}
{...props}

View File

@@ -28,7 +28,6 @@ import {
createRingingMedia,
type RingingMediaViewModel,
} from "../state/media/RingingMediaViewModel";
import { type MuteStates } from "../state/MuteStates";
global.IntersectionObserver = class MockIntersectionObserver {
public observe(): void {}
@@ -156,10 +155,8 @@ test("SpotlightTile displays ringing media", async () => {
>("ringing");
const vm = createRingingMedia({
pickupState$,
muteStates: {
video: { enabled$: constant(false) },
} as unknown as MuteStates,
id: "test",
intent: "audio",
userId: "@alice:example.org",
displayName$: constant("Alice"),
mxcAvatarUrl$: constant(undefined),

View File

@@ -212,7 +212,6 @@ const SpotlightRingingMediaItem: FC<SpotlightRingingMediaItemProps> = ({
}) => {
const { t } = useTranslation();
const pickupState = useBehavior(vm.pickupState$);
const videoEnabled = useBehavior(vm.videoEnabled$);
return (
<MediaView
@@ -222,7 +221,8 @@ const SpotlightRingingMediaItem: FC<SpotlightRingingMediaItemProps> = ({
pickupState === "ringing"
? {
text: t("video_tile.calling"),
Icon: videoEnabled ? VideoCallSolidIcon : VoiceCallSolidIcon,
Icon:
vm.intent === "video" ? VideoCallSolidIcon : VoiceCallSolidIcon,
}
: { text: t("video_tile.call_ended"), Icon: EndCallIcon }
}