mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-05 17:14:59 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
290b4ccea9 | ||
|
|
ca1b892e11 | ||
|
|
e5d3e98ce3 |
No files matched your search
@@ -9,6 +9,7 @@ from urllib.parse import parse_qs, urlparse
|
||||
from django.utils import timezone
|
||||
|
||||
from chatballs.calls.event_handlers import CallInviteDeliveryError, handle_call_invite_send
|
||||
from chatballs.calls.lifecycle import transition_call
|
||||
from chatballs.calls.models import (
|
||||
CallEndedBy,
|
||||
CallInvite,
|
||||
@@ -16,7 +17,6 @@ from chatballs.calls.models import (
|
||||
CallStatus,
|
||||
InviteDeliveryStatus,
|
||||
)
|
||||
from chatballs.calls.lifecycle import transition_call
|
||||
from chatballs.calls.services import (
|
||||
decline_call_for_identity,
|
||||
open_call_for_identity,
|
||||
|
||||
@@ -18,6 +18,7 @@ from django.db.models import (
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
|
||||
from chatballs.conversations.contact_avatars import contact_avatar_url_in
|
||||
from chatballs.conversations.models import (
|
||||
ConnectionIdentity,
|
||||
Contact,
|
||||
@@ -26,7 +27,6 @@ from chatballs.conversations.models import (
|
||||
Conversation,
|
||||
LifecycleState,
|
||||
)
|
||||
from chatballs.conversations.contact_avatars import contact_avatar_url_in
|
||||
from chatballs.i18n import t
|
||||
from chatballs.identity.audit_catalog import (
|
||||
audit_action_label,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.db.models import Count, Max, Q
|
||||
|
||||
from chatballs.conversations.contact_avatars import contact_avatar_url_in
|
||||
from chatballs.conversations.models import (
|
||||
ConnectionIdentity,
|
||||
Conversation,
|
||||
@@ -8,7 +9,6 @@ from chatballs.conversations.models import (
|
||||
MessageKind,
|
||||
)
|
||||
from chatballs.i18n import t
|
||||
from chatballs.conversations.contact_avatars import contact_avatar_url_in
|
||||
from chatballs.identity.avatars import user_avatar_url_in
|
||||
from chatballs.integrations.features import features_payload
|
||||
from chatballs.integrations.models import IntegrationProvider
|
||||
|
||||
@@ -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