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
+28
View File
@@ -49,6 +49,14 @@ export interface LotusCallParticipant {
speakingWhileMuted?: boolean;
}
/** [Gitea #143] The fork's one-shot io.lotus.call_summary payload. */
export interface LotusCallSummary {
durationMs: number;
reconnects: number;
poorMs: number;
verdict: 'good' | 'fair' | 'poor' | 'unknown';
}
export class CallEmbed {
private mx: MatrixClient;
@@ -60,6 +68,12 @@ export class CallEmbed {
public joined = false;
/** [Gitea #143] When the first JoinCall landed, for the hangup readout. */
public joinedAt: number | undefined;
/** [Gitea #143] The fork's end-of-call summary, once it arrives. */
public lastSummary: LotusCallSummary | undefined;
// C-M4: set once dispose() has run so the hangup fallback timer can tell
// whether the embed was already torn down by the normal Close/Hangup echo.
public disposed = false;
@@ -390,6 +404,19 @@ export class CallEmbed {
this.forkStateRequestListeners.forEach((l) => l());
}),
);
this.disposables.push(
this.listenAction('io.lotus.call_summary', (evt) => {
const data = (evt.detail as { data?: Partial<LotusCallSummary> } | undefined)?.data;
if (data && typeof data.durationMs === 'number') {
this.lastSummary = {
durationMs: data.durationMs,
reconnects: data.reconnects ?? 0,
poorMs: data.poorMs ?? 0,
verdict: data.verdict ?? 'unknown',
};
}
}),
);
this.disposables.push(
this.listenAction('io.lotus.call_state', (evt) => {
const data = (evt.detail as { data?: { participants?: unknown } } | undefined)?.data;
@@ -537,6 +564,7 @@ export class CallEmbed {
return;
}
this.joined = true;
this.joinedAt = Date.now();
// EC ignores io.element.device_mute before join; re-apply desired state now that EC is live
this.control.forceState(this.initialState);
}