feat(calls): "Call ended · 41 min · connection was good" toast (#143)

One line in the existing toast style when a call you were in ends: the
duration from our own join clock, plus the fork's io.lotus.call_summary
readout (fork ≥ 0.25.0-lotus.10) when it arrives — "connection was
good", "3 reconnects", "connection was poor for 4 min". Nothing is
stored or sent; the summary is one postMessage at hangup. Without the
fork summary the toast still shows the duration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-20 13:48:14 -04:00
co-authored by Claude Opus 5
parent 96a97a2f86
commit af1c0ee184
5 changed files with 171 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
import { useEffect, useRef } from 'react';
import { useSetAtom } from 'jotai';
import { CallEmbed } from '../plugins/call';
import { useCallHangupEvent } from './useCallEmbed';
import { toastQueueAtom } from '../state/toast';
import { describeCallEnd } from '../utils/callSummary';
// The fork sends io.lotus.call_summary on SFU disconnect, which normally lands
// before Element Call's hangup echo; give a late one this long before toasting.
const SUMMARY_GRACE_MS = 400;
/**
* [Gitea #143] "Call ended · 41 min · connection was good" in the existing
* toast style when a call you were in ends. Duration comes from our own
* join clock; the quality readout from the fork, if it arrived.
*/
export function useCallEndedToast(embed: CallEmbed): void {
const setToast = useSetAtom(toastQueueAtom);
const fired = useRef(false);
useEffect(() => {
fired.current = false;
}, [embed]);
useCallHangupEvent(embed, () => {
if (fired.current || !embed.joined || embed.joinedAt === undefined) return;
fired.current = true;
const durationMs = Date.now() - embed.joinedAt;
const toast = () =>
setToast({
id: `call-ended-${Date.now()}`,
displayName: 'Lotus Chat',
body: describeCallEnd(durationMs, embed.lastSummary),
roomName: embed.room.name ?? 'Voice call',
roomId: embed.roomId,
});
if (embed.lastSummary) toast();
else setTimeout(toast, SUMMARY_GRACE_MS);
});
}