mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-05 09:14:58 +03:00
🐛 fix(calls): отбой клиента закрывает окно звонка у оператора
Клиент кладёт трубку, сервер переводит звонок в терминал, а peer connection у оператора узнаёт об этом позже — или не узнаёт вовсе. Проверка терминального статуса в аудиозвонке стояла последней, после состояния соединения, поэтому у оператора оставался экран активного разговора с кнопкой «Завершить», которой нечего было завершать: она молча выходила по той же проверке. Теперь терминальный статус решает первым (как в видеозвонке), а «Завершить» на уже завершённом звонке просто закрывает окно. Проверено: четыре теста разбора фазы аудиозвонка. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e5d3e98ce3
commit
ca1b892e11
3 files changed
+53
-4
No files matched your search
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { resolveAudioMode } from "./AudioCallOverlay";
|
||||
import type { ApiCall } from "./model";
|
||||
|
||||
// Экран аудиозвонка у оператора: что он показывает в каждой фазе.
|
||||
|
||||
function call(status: ApiCall["status"]): ApiCall {
|
||||
return { id: "c1", conversationId: 1, status, kind: "AUDIO", connectedAt: null, endedBy: null, durationSeconds: null } as ApiCall;
|
||||
}
|
||||
|
||||
describe("resolveAudioMode", () => {
|
||||
it("показывает разговор, пока звонок идёт", () => {
|
||||
expect(resolveAudioMode(call("ACTIVE"), "", "connected", "none")).toBe("active");
|
||||
});
|
||||
|
||||
it("считает звонок завершённым, даже когда соединение ещё считает себя живым", () => {
|
||||
// Клиент положил трубку: сервер уже в терминале, а peer connection узнаёт
|
||||
// об этом позже — или не узнаёт вовсе, если сокет мёртв.
|
||||
expect(resolveAudioMode(call("ENDED"), "", "connected", "none")).toBe("status");
|
||||
expect(resolveAudioMode(call("DECLINED"), "", "connected", "none")).toBe("status");
|
||||
expect(resolveAudioMode(call("FAILED"), "", "connected", "none")).toBe("status");
|
||||
});
|
||||
|
||||
it("до ответа клиента звонок исходящий", () => {
|
||||
expect(resolveAudioMode(call("RINGING"), "", "idle", "none")).toBe("ringing");
|
||||
});
|
||||
|
||||
it("принятый звонок без соединения — соединение", () => {
|
||||
expect(resolveAudioMode(call("ACCEPTED"), "", "idle", "none")).toBe("connecting");
|
||||
});
|
||||
});
|
||||
@@ -67,7 +67,13 @@ export function AudioCallOverlay(props: Props) {
|
||||
// состоянии (карточку звонка могли открыть из списка), а сам запрос может
|
||||
// упасть — тогда звонок заканчивается вторым путём, по сессии оператора.
|
||||
const finish = async () => {
|
||||
if (!call || isTerminalCallStatus(call.status)) return;
|
||||
if (!call) return;
|
||||
// Звонок уже закончил кто-то другой — завершать нечего, окно закрывается.
|
||||
if (isTerminalCallStatus(call.status)) {
|
||||
rtc.stop();
|
||||
props.onClose();
|
||||
return;
|
||||
}
|
||||
const token = props.access?.accessToken;
|
||||
try {
|
||||
if (token) props.onCallChange(await endCallByAccess(token));
|
||||
@@ -123,16 +129,21 @@ export function AudioCallOverlay(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAudioMode(call: ApiCall | null, errorText: string, connection: string, mediaIssue: string): AudioCallMode {
|
||||
export function resolveAudioMode(call: ApiCall | null, errorText: string, connection: string, mediaIssue: string): AudioCallMode {
|
||||
if (errorText || !call) return "status";
|
||||
if (mediaIssue === "devices" || mediaIssue === "unsupported") return "status";
|
||||
// Завершённый звонок завершён, чем бы ни было занято RTC-соединение. Клиент
|
||||
// кладёт трубку, сервер переводит звонок в терминал, а peer connection у
|
||||
// оператора об этом узнаёт позже — а если сокет уже мёртв, то и не узнаёт
|
||||
// вовсе. Пока эта проверка стояла последней, у оператора оставался экран
|
||||
// активного разговора с кнопкой «Завершить», которой нечего было завершать.
|
||||
if (isTerminalCallStatus(call.status)) return "status";
|
||||
// Звонок всегда инициирует оператор, поэтому до ответа клиента это исходящий.
|
||||
if (call.status === "REQUESTED" || call.status === "RINGING") return "ringing";
|
||||
if (call.status === "ACCEPTED") return connection === "connected" ? "active" : "connecting";
|
||||
if (connection === "reconnecting") return "reconnecting";
|
||||
if (connection === "failed") return "status";
|
||||
if (call.status === "ACTIVE" || connection === "connected") return "active";
|
||||
if (isTerminalCallStatus(call.status)) return "status";
|
||||
return "connecting";
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,13 @@ export function VideoCallOverlay(props: Props) {
|
||||
// «Завершить» обязано сработать всегда: нет токена доступа или запрос не
|
||||
// прошёл — звонок заканчивается вторым путём, по сессии оператора.
|
||||
const finish = async () => {
|
||||
if (!call || TERMINAL[call.status]) return;
|
||||
if (!call) return;
|
||||
// Звонок уже закончил кто-то другой — завершать нечего, окно закрывается.
|
||||
if (TERMINAL[call.status]) {
|
||||
rtc.stop();
|
||||
props.onClose();
|
||||
return;
|
||||
}
|
||||
const token = props.access?.accessToken;
|
||||
try {
|
||||
if (token) props.onCallChange(await endCallByAccess(token));
|
||||
|
||||
Reference in new issue
Block a user