@coventia/softphone 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{call-BTYlT2SU.d.cts → call-DUVnDxsz.d.cts} +152 -5
- package/dist/{call-BTYlT2SU.d.ts → call-DUVnDxsz.d.ts} +152 -5
- package/dist/{chunk-N2N2EQQJ.js → chunk-7R6LOOAI.js} +4 -2
- package/dist/chunk-7R6LOOAI.js.map +1 -0
- package/dist/{chunk-3V2L6UTK.js → chunk-LMJXX4YM.js} +17 -3
- package/dist/chunk-LMJXX4YM.js.map +1 -0
- package/dist/{chunk-5BGEHMPP.js → chunk-YWI6EQYS.js} +40 -7
- package/dist/chunk-YWI6EQYS.js.map +1 -0
- package/dist/element/index.d.ts +154 -5
- package/dist/element/index.js +512 -152
- package/dist/element/index.js.map +1 -1
- package/dist/{engine-D9nbeY08.d.cts → engine-CDjkrXHY.d.cts} +1 -1
- package/dist/{engine-Doa6Brkm.d.ts → engine-EPCKf-Bl.d.ts} +1 -1
- package/dist/index.cjs +396 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +256 -8
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +15 -0
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +14 -3
- package/dist/react/index.d.ts +14 -3
- package/dist/react/index.js +2 -2
- package/dist/{softphone-DhuhcKCy.d.cts → softphone-BtdgZWkm.d.ts} +17 -2
- package/dist/{softphone-DHmhotbf.d.ts → softphone-DHG9MTNg.d.cts} +17 -2
- package/dist/softphone.css +395 -1
- package/dist/{sources-N4BOEDRL.js → sources-5L2R4HTV.js} +104 -5
- package/dist/sources-5L2R4HTV.js.map +1 -0
- package/dist/testing/index.cjs +41 -5
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +8 -2
- package/dist/testing/index.d.ts +8 -2
- package/dist/testing/index.js +41 -5
- package/dist/testing/index.js.map +1 -1
- package/dist/ui/index.cjs +490 -139
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +2 -2
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +478 -141
- package/dist/ui/index.js.map +1 -1
- package/dist/zadarma/index.cjs +38 -5
- package/dist/zadarma/index.cjs.map +1 -1
- package/dist/zadarma/index.d.cts +2 -2
- package/dist/zadarma/index.d.ts +2 -2
- package/dist/zadarma/index.js +1 -1
- package/dist/zadarma-engine-4VOPIBXG.js +5 -0
- package/dist/{zadarma-engine-DN2SZSQ3.js.map → zadarma-engine-4VOPIBXG.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-3V2L6UTK.js.map +0 -1
- package/dist/chunk-5BGEHMPP.js.map +0 -1
- package/dist/chunk-N2N2EQQJ.js.map +0 -1
- package/dist/sources-N4BOEDRL.js.map +0 -1
- package/dist/zadarma-engine-DN2SZSQ3.js +0 -5
package/dist/ui/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isWebRtcSupported } from '../chunk-KAXEMJ7E.js';
|
|
2
|
-
import { useSoftphone, useSoftphoneState, useCall, useActiveCall, useIsLeader, useLastError, useDevices } from '../chunk-
|
|
3
|
-
import '../chunk-
|
|
2
|
+
import { useSoftphone, useSoftphoneState, useCall, usePresence, useActiveCall, useIsLeader, useLastError, useDevices } from '../chunk-LMJXX4YM.js';
|
|
3
|
+
import '../chunk-7R6LOOAI.js';
|
|
4
4
|
import { forwardRef, createElement, useState, useRef, useLayoutEffect, useEffect, useCallback, useMemo } from 'react';
|
|
5
5
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
6
|
|
|
@@ -679,6 +679,125 @@ function elapsedSeconds(since, now) {
|
|
|
679
679
|
if (!since) return 0;
|
|
680
680
|
return Math.floor((now - since.getTime()) / 1e3);
|
|
681
681
|
}
|
|
682
|
+
function formatAge(totalSeconds) {
|
|
683
|
+
const s = Math.max(0, Math.floor(totalSeconds));
|
|
684
|
+
if (s < 60) return "justo ahora";
|
|
685
|
+
const mm = Math.floor(s / 60);
|
|
686
|
+
if (mm < 60) return `hace ${mm} min`;
|
|
687
|
+
const hh = Math.floor(mm / 60);
|
|
688
|
+
return `hace ${hh} h`;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/ui/lib/error-text.ts
|
|
692
|
+
var ERROR_TEXT = {
|
|
693
|
+
// ── Engine lifecycle ────────────────────────────────────────────────────────
|
|
694
|
+
// Integrator mistakes rather than user-facing failures, but they must still read as something
|
|
695
|
+
// rather than as a blank panel if one ever reaches the UI.
|
|
696
|
+
"engine/duplicate-instance": {
|
|
697
|
+
title: "Tel\xE9fono duplicado",
|
|
698
|
+
body: "Hay otro tel\xE9fono abierto en esta p\xE1gina. Cierra uno y vuelve a cargar."
|
|
699
|
+
},
|
|
700
|
+
"engine/not-started": { title: "El tel\xE9fono a\xFAn no est\xE1 listo" },
|
|
701
|
+
"engine/destroyed": {
|
|
702
|
+
title: "Tel\xE9fono cerrado",
|
|
703
|
+
body: "Vuelve a cargar la p\xE1gina para usarlo de nuevo."
|
|
704
|
+
},
|
|
705
|
+
// ── Auth ────────────────────────────────────────────────────────────────────
|
|
706
|
+
// Never "token": the user has no idea what one is. What they can act on is the session.
|
|
707
|
+
"auth/token-fetch-failed": {
|
|
708
|
+
title: "Sin conexi\xF3n con el servidor",
|
|
709
|
+
body: "No se ha podido activar el tel\xE9fono. Reint\xE9ntalo en unos segundos."
|
|
710
|
+
},
|
|
711
|
+
"auth/token-rejected": {
|
|
712
|
+
title: "Sesi\xF3n caducada",
|
|
713
|
+
body: "Vuelve a iniciar sesi\xF3n para poder llamar."
|
|
714
|
+
},
|
|
715
|
+
// ── Media ───────────────────────────────────────────────────────────────────
|
|
716
|
+
// Verbatim from MicPermissionGate, which already says these well and says them first.
|
|
717
|
+
//
|
|
718
|
+
// NEVER ACTUALLY SHOWN via this table — these four codes (through `media/playback-blocked`, below)
|
|
719
|
+
// are never raised as a `SoftphoneError` at all. See the fuller comment on each in `core/errors.ts`
|
|
720
|
+
// for why. Copy kept here anyway so the catalogue and this file stay the same shape as every other
|
|
721
|
+
// code — `error-text.test.ts` would fail the build the moment they drifted.
|
|
722
|
+
"media/permission-denied": {
|
|
723
|
+
title: "Micr\xF3fono bloqueado",
|
|
724
|
+
body: "Perm\xEDtelo en tu navegador para poder llamar."
|
|
725
|
+
},
|
|
726
|
+
"media/no-input-device": {
|
|
727
|
+
title: "Sin micr\xF3fono",
|
|
728
|
+
body: "Conecta un micr\xF3fono y vuelve a intentarlo."
|
|
729
|
+
},
|
|
730
|
+
"media/webrtc-unsupported": {
|
|
731
|
+
title: "Llamadas no disponibles",
|
|
732
|
+
body: "Este navegador no admite llamadas. Prueba con Chrome, Edge o Firefox."
|
|
733
|
+
},
|
|
734
|
+
"media/playback-blocked": { title: "Toca la pantalla para activar el sonido" },
|
|
735
|
+
// ── Network / signalling ────────────────────────────────────────────────────
|
|
736
|
+
// "Centralita" rather than "SIP" or "registro": the user knows what a phone system is.
|
|
737
|
+
"net/registration-failed": {
|
|
738
|
+
title: "No se pudo conectar",
|
|
739
|
+
body: "No hemos podido conectar con la centralita. Reintenta en unos segundos."
|
|
740
|
+
},
|
|
741
|
+
"net/transport-failed": {
|
|
742
|
+
title: "Conexi\xF3n perdida",
|
|
743
|
+
body: "Se ha perdido la conexi\xF3n con la centralita."
|
|
744
|
+
},
|
|
745
|
+
// NEVER ACTUALLY SHOWN either — CallScreen's own "Reconectando…" already covers this, off the
|
|
746
|
+
// call's `reconnecting` state rather than this channel. See the comment in `core/errors.ts`.
|
|
747
|
+
"net/ice-failed": { title: "Recuperando el audio\u2026" },
|
|
748
|
+
// ── Call ────────────────────────────────────────────────────────────────────
|
|
749
|
+
/*
|
|
750
|
+
THE VOICE RULE FOR THIS BLOCK: say what HAPPENED, not what it is called.
|
|
751
|
+
|
|
752
|
+
These were status labels — "Llamada perdida", "Llamada rechazada", "Sin respuesta". Each is a
|
|
753
|
+
name for a category rather than an account of an event, which is why they read as a database enum
|
|
754
|
+
someone forgot to translate into human. Worse, a label hides the ACTOR: "Llamada rechazada" never
|
|
755
|
+
says who rejected what, so the user reading it after declining a call and the user reading it
|
|
756
|
+
after being declined see the same three words.
|
|
757
|
+
|
|
758
|
+
So each one is a short sentence about what the other person, or the phone, did — the form
|
|
759
|
+
`call/busy` already had, and the reason it was the only one here that read clearly.
|
|
760
|
+
|
|
761
|
+
Impersonal third-person plural ("contestaron", "colgaron") throughout: the user does not know who
|
|
762
|
+
is at the other end, and it is what Spanish uses when the subject is unknown. Mixing it with the
|
|
763
|
+
singular across neighbouring lines reads as carelessness.
|
|
764
|
+
|
|
765
|
+
LENGTH IS A HARD CONSTRAINT, not a preference. `.cv-dial-outcome` is one line at a fixed
|
|
766
|
+
`1.15rem`, and the keypad below it is `flex: 1` — a title that wraps resizes every key and shifts
|
|
767
|
+
the grid under a finger already reaching for it. `error-text.test.ts` caps this at 48 characters.
|
|
768
|
+
*/
|
|
769
|
+
"call/not-answerable": { title: "Esa llamada ya ha terminado" },
|
|
770
|
+
/*
|
|
771
|
+
NOT "Comunicando", which is what this said and what sent us looking. SIP 486 is both "the line is
|
|
772
|
+
engaged" AND what almost every endpoint sends when a person presses decline — including our own
|
|
773
|
+
`reject()`. One code, two events, no way to tell them apart, so the copy has to hold both rather
|
|
774
|
+
than assert the less likely one. `call/rejected` can be definite: an endpoint sending 603 Decline
|
|
775
|
+
means exactly that.
|
|
776
|
+
*/
|
|
777
|
+
"call/busy": { title: "No contestaron o la l\xEDnea estaba ocupada" },
|
|
778
|
+
"call/no-answer": { title: "No contestaron" },
|
|
779
|
+
"call/rejected": { title: "No aceptaron tu llamada" },
|
|
780
|
+
/*
|
|
781
|
+
The legacy CRM said "La persona que llamaba colgó antes de que pudieras contestar." — the right
|
|
782
|
+
fact, at three times the width of the line that has to hold it. This keeps the fact and drops the
|
|
783
|
+
words: the user needs to know the call was FOR them and that nobody is waiting on the line.
|
|
784
|
+
"Llamada perdida" alone tested badly for exactly the label reason above — it names the category
|
|
785
|
+
without saying anyone rang.
|
|
786
|
+
*/
|
|
787
|
+
"call/missed": { title: "Colgaron antes de que pudieras contestar" },
|
|
788
|
+
// "No se pudo completar la llamada" was our own "something went wrong": true of every failure and
|
|
789
|
+
// useful for none. The number is what the user can actually act on.
|
|
790
|
+
"call/failed": { title: "No se pudo conectar con ese n\xFAmero" },
|
|
791
|
+
// The load-bearing half is the SECOND sentence: the original call is still up, and a user who
|
|
792
|
+
// thinks they dropped it will hang up on a client to "fix" it.
|
|
793
|
+
"call/transfer-failed": { title: "No se pudo transferir. Sigues en la llamada" },
|
|
794
|
+
"call/unsupported": { title: "Esta funci\xF3n no est\xE1 disponible" },
|
|
795
|
+
// ── Provider ────────────────────────────────────────────────────────────────
|
|
796
|
+
"provider/provision-failed": {
|
|
797
|
+
title: "No se pudo activar el tel\xE9fono",
|
|
798
|
+
body: "Si el problema contin\xFAa, contacta con soporte."
|
|
799
|
+
}
|
|
800
|
+
};
|
|
682
801
|
function useTicker(enabled, intervalMs = 1e3) {
|
|
683
802
|
const [now, setNow] = useState(() => Date.now());
|
|
684
803
|
useEffect(() => {
|
|
@@ -791,17 +910,195 @@ var STATUS_LABEL = {
|
|
|
791
910
|
reconnecting: "Reconectando\u2026",
|
|
792
911
|
held: "En espera"
|
|
793
912
|
};
|
|
913
|
+
var CONSULT_FAIL_CODE = {
|
|
914
|
+
busy: "call/busy",
|
|
915
|
+
"no-answer": "call/no-answer",
|
|
916
|
+
rejected: "call/rejected",
|
|
917
|
+
failed: "call/failed"
|
|
918
|
+
};
|
|
919
|
+
function consultFailText(reason) {
|
|
920
|
+
return ERROR_TEXT[CONSULT_FAIL_CODE[reason] ?? "call/transfer-failed"].title;
|
|
921
|
+
}
|
|
922
|
+
var MIN_CONSULT_HOLD_SECONDS = 10;
|
|
923
|
+
function ConsultScreen({
|
|
924
|
+
mainName,
|
|
925
|
+
consultView,
|
|
926
|
+
now,
|
|
927
|
+
onReturn,
|
|
928
|
+
onComplete,
|
|
929
|
+
onHangupAll
|
|
930
|
+
}) {
|
|
931
|
+
const s = consultView.state;
|
|
932
|
+
const answered = s === "active";
|
|
933
|
+
const name = consultView.remote.displayName;
|
|
934
|
+
const number = consultView.remote.number;
|
|
935
|
+
const showNumber = number !== name;
|
|
936
|
+
const heldSeconds = elapsedSeconds(consultView.answeredAt, now);
|
|
937
|
+
const [rawM = "0", ss = "00"] = formatDuration(heldSeconds).split(":");
|
|
938
|
+
const mm = rawM.padStart(2, "0");
|
|
939
|
+
const canComplete = heldSeconds >= MIN_CONSULT_HOLD_SECONDS;
|
|
940
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-view", children: [
|
|
941
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-call", children: [
|
|
942
|
+
/* @__PURE__ */ jsxs("button", { type: "button", className: "cv-consult-hold-banner", onClick: onReturn, children: [
|
|
943
|
+
/* @__PURE__ */ jsx(Pause, { size: 13, "aria-hidden": true }),
|
|
944
|
+
mainName,
|
|
945
|
+
" en espera \xB7 Volver"
|
|
946
|
+
] }),
|
|
947
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-avatar-wrap", children: [
|
|
948
|
+
/* @__PURE__ */ jsx("div", { className: "cv-avatar", "aria-hidden": true, children: initials(name) }),
|
|
949
|
+
answered ? /* @__PURE__ */ jsx("span", { className: "cv-avatar-dot", "aria-hidden": true }) : null
|
|
950
|
+
] }),
|
|
951
|
+
/* @__PURE__ */ jsx("div", { className: "cv-call-name", children: name }),
|
|
952
|
+
showNumber ? /* @__PURE__ */ jsx("div", { className: "cv-call-number", children: number }) : null,
|
|
953
|
+
answered ? /* @__PURE__ */ jsxs("div", { className: "cv-timer", "aria-label": "Duraci\xF3n de la llamada", children: [
|
|
954
|
+
/* @__PURE__ */ jsx("b", { children: mm }),
|
|
955
|
+
/* @__PURE__ */ jsx("i", { children: ":" }),
|
|
956
|
+
/* @__PURE__ */ jsx("b", { children: ss })
|
|
957
|
+
] }) : /* @__PURE__ */ jsx("div", { className: "cv-call-status", children: STATUS_LABEL[s] ?? "" })
|
|
958
|
+
] }),
|
|
959
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-controls", children: [
|
|
960
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-transfer-actions", children: [
|
|
961
|
+
answered ? /* @__PURE__ */ jsx("button", { className: "cv-btn cv-btn--accent", onClick: onComplete, disabled: !canComplete, children: "Completar" }) : null,
|
|
962
|
+
/* @__PURE__ */ jsx("button", { className: "cv-btn", onClick: onReturn, children: "Cancelar" })
|
|
963
|
+
] }),
|
|
964
|
+
/* @__PURE__ */ jsx("div", { className: "cv-controls-primary", children: /* @__PURE__ */ jsxs("div", { className: "cv-ctrl-item", children: [
|
|
965
|
+
/* @__PURE__ */ jsx(
|
|
966
|
+
"button",
|
|
967
|
+
{
|
|
968
|
+
type: "button",
|
|
969
|
+
className: "cv-round cv-round--hangup",
|
|
970
|
+
onClick: onHangupAll,
|
|
971
|
+
"aria-label": "Colgar todo",
|
|
972
|
+
children: /* @__PURE__ */ jsx(PhoneOff, { size: 26 })
|
|
973
|
+
}
|
|
974
|
+
),
|
|
975
|
+
/* @__PURE__ */ jsx("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Colgar todo" })
|
|
976
|
+
] }) })
|
|
977
|
+
] })
|
|
978
|
+
] });
|
|
979
|
+
}
|
|
980
|
+
var PRESENCE_LABEL = {
|
|
981
|
+
disponible: "Disponible",
|
|
982
|
+
en_llamada: "En llamada",
|
|
983
|
+
desconectado: "Desconectado"
|
|
984
|
+
};
|
|
985
|
+
var PRESENCE_RANK = {
|
|
986
|
+
disponible: 0,
|
|
987
|
+
en_llamada: 1,
|
|
988
|
+
desconectado: 2
|
|
989
|
+
};
|
|
990
|
+
function filterAgents(agents, query) {
|
|
991
|
+
const q = query.trim().toLowerCase();
|
|
992
|
+
return agents.filter((a) => !q || (a.name ?? "").toLowerCase().includes(q) || a.extension.includes(q)).sort(
|
|
993
|
+
(a, b) => PRESENCE_RANK[a.state] - PRESENCE_RANK[b.state] || (a.name ?? a.extension).localeCompare(b.name ?? b.extension)
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
function truncateForButton(text, max = 16) {
|
|
997
|
+
return text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
998
|
+
}
|
|
999
|
+
function TransferPicker({
|
|
1000
|
+
agents,
|
|
1001
|
+
myUserId,
|
|
1002
|
+
query,
|
|
1003
|
+
onSelect
|
|
1004
|
+
}) {
|
|
1005
|
+
const others = agents.filter((a) => a.userId !== myUserId);
|
|
1006
|
+
if (others.length === 0) return null;
|
|
1007
|
+
const filtered = filterAgents(others, query);
|
|
1008
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-presence", children: [
|
|
1009
|
+
/* @__PURE__ */ jsx("div", { className: "cv-presence-list", role: "listbox", "aria-label": "Compa\xF1eros", children: filtered.length === 0 ? /* @__PURE__ */ jsx("p", { className: "cv-presence-empty", children: "Sin coincidencias" }) : filtered.map((agent) => {
|
|
1010
|
+
const isBusy = agent.state === "en_llamada";
|
|
1011
|
+
return /* @__PURE__ */ jsxs(
|
|
1012
|
+
"button",
|
|
1013
|
+
{
|
|
1014
|
+
type: "button",
|
|
1015
|
+
role: "option",
|
|
1016
|
+
className: "cv-presence-item",
|
|
1017
|
+
disabled: isBusy,
|
|
1018
|
+
onClick: () => onSelect(agent.extension),
|
|
1019
|
+
children: [
|
|
1020
|
+
/* @__PURE__ */ jsx("span", { className: "cv-presence-dot", "data-state": agent.state, "aria-hidden": true }),
|
|
1021
|
+
/* @__PURE__ */ jsx("span", { className: "cv-presence-name", children: agent.name ?? agent.extension }),
|
|
1022
|
+
/* @__PURE__ */ jsxs("span", { className: "cv-presence-meta", children: [
|
|
1023
|
+
agent.extension,
|
|
1024
|
+
" \xB7 ",
|
|
1025
|
+
PRESENCE_LABEL[agent.state]
|
|
1026
|
+
] })
|
|
1027
|
+
]
|
|
1028
|
+
},
|
|
1029
|
+
agent.userId
|
|
1030
|
+
);
|
|
1031
|
+
}) }),
|
|
1032
|
+
/* @__PURE__ */ jsx("p", { className: "cv-presence-legend", children: "Desconectado: sin conexi\xF3n en el softphone web. Puede atender desde otro dispositivo." })
|
|
1033
|
+
] });
|
|
1034
|
+
}
|
|
794
1035
|
function CallScreen({
|
|
795
1036
|
call,
|
|
796
1037
|
incomingCallAction
|
|
797
1038
|
}) {
|
|
798
1039
|
const view = useCall(call);
|
|
1040
|
+
const softphone = useSoftphone();
|
|
799
1041
|
const [showTransfer, setShowTransfer] = useState(false);
|
|
800
1042
|
const [transferTo, setTransferTo] = useState("");
|
|
1043
|
+
const [consultCall, setConsultCall] = useState(null);
|
|
1044
|
+
const [consultOutcome, setConsultOutcome] = useState(null);
|
|
1045
|
+
const [myUserId, setMyUserId] = useState(null);
|
|
1046
|
+
const consultView = useCall(consultCall);
|
|
1047
|
+
const consultIntentRef = useRef(null);
|
|
801
1048
|
const [showDevices, setShowDevices] = useState(false);
|
|
802
1049
|
const controlsRef = useRef(null);
|
|
803
1050
|
const now = useTicker(view?.state === "active");
|
|
1051
|
+
const consultNow = useTicker(consultView?.state === "active");
|
|
804
1052
|
const isConnected = view ? view.state === "active" || view.state === "held" || view.state === "reconnecting" : false;
|
|
1053
|
+
const transferPickerActive = isConnected && showTransfer && !consultCall;
|
|
1054
|
+
const presence = usePresence(transferPickerActive);
|
|
1055
|
+
useEffect(() => {
|
|
1056
|
+
const listAgents = softphone.sources?.listAgents;
|
|
1057
|
+
if (!listAgents) return;
|
|
1058
|
+
let cancelled = false;
|
|
1059
|
+
void listAgents().then((d) => !cancelled && setMyUserId(d.me?.userId ?? null)).catch(() => {
|
|
1060
|
+
});
|
|
1061
|
+
return () => {
|
|
1062
|
+
cancelled = true;
|
|
1063
|
+
};
|
|
1064
|
+
}, [softphone]);
|
|
1065
|
+
useEffect(() => {
|
|
1066
|
+
if (!consultCall) return;
|
|
1067
|
+
return consultCall.on("ended" /* Ended */, ({ reason }) => {
|
|
1068
|
+
const intent = consultIntentRef.current;
|
|
1069
|
+
consultIntentRef.current = null;
|
|
1070
|
+
setConsultCall(null);
|
|
1071
|
+
if (!intent) {
|
|
1072
|
+
setConsultOutcome(reason);
|
|
1073
|
+
if (call.state !== "ended") void call.unhold();
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
}, [consultCall, call]);
|
|
1077
|
+
const startConsult = async (target) => {
|
|
1078
|
+
const t = (target ?? transferTo).trim();
|
|
1079
|
+
if (!t) return;
|
|
1080
|
+
setConsultOutcome(null);
|
|
1081
|
+
setConsultCall(await call.startConsult(t));
|
|
1082
|
+
};
|
|
1083
|
+
const selectPresenceAgent = (extension) => {
|
|
1084
|
+
setTransferTo(extension);
|
|
1085
|
+
void startConsult(extension);
|
|
1086
|
+
};
|
|
1087
|
+
const [quickTransfer, setQuickTransfer] = useState(false);
|
|
1088
|
+
const selectPresenceAgentAuto = (extension) => {
|
|
1089
|
+
void call.transfer(extension);
|
|
1090
|
+
};
|
|
1091
|
+
const completeTransfer = async () => {
|
|
1092
|
+
if (!consultCall) return;
|
|
1093
|
+
const target = transferTo.trim();
|
|
1094
|
+
consultIntentRef.current = "complete";
|
|
1095
|
+
await consultCall.hangup();
|
|
1096
|
+
await call.transfer(target);
|
|
1097
|
+
};
|
|
1098
|
+
const cancelConsult = async () => {
|
|
1099
|
+
consultIntentRef.current = "cancel";
|
|
1100
|
+
await call.cancelConsult();
|
|
1101
|
+
};
|
|
805
1102
|
useEffect(() => {
|
|
806
1103
|
if (!showDevices) return;
|
|
807
1104
|
const onKey = (e) => {
|
|
@@ -826,6 +1123,19 @@ function CallScreen({
|
|
|
826
1123
|
const name = view.remote.displayName;
|
|
827
1124
|
const number = view.remote.number;
|
|
828
1125
|
const showNumber = number !== name;
|
|
1126
|
+
if (consultCall && consultView) {
|
|
1127
|
+
return /* @__PURE__ */ jsx(
|
|
1128
|
+
ConsultScreen,
|
|
1129
|
+
{
|
|
1130
|
+
mainName: name,
|
|
1131
|
+
consultView,
|
|
1132
|
+
now: consultNow,
|
|
1133
|
+
onReturn: () => void cancelConsult(),
|
|
1134
|
+
onComplete: () => void completeTransfer(),
|
|
1135
|
+
onHangupAll: () => void call.hangup()
|
|
1136
|
+
}
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
829
1139
|
const incomingAction = isRinging && view.remote.contact && incomingCallAction ? incomingCallAction : null;
|
|
830
1140
|
const IncomingActionIcon = incomingAction?.icon ?? ExternalLink;
|
|
831
1141
|
const [rawM = "0", ss = "00"] = formatDuration(elapsedSeconds(view.answeredAt, now)).split(":");
|
|
@@ -844,7 +1154,79 @@ function CallScreen({
|
|
|
844
1154
|
STATUS_LABEL[s] ?? ""
|
|
845
1155
|
] }),
|
|
846
1156
|
/* @__PURE__ */ jsx("div", { className: "cv-settings-section-body", children: /* @__PURE__ */ jsx(DevicePickers, {}) })
|
|
847
|
-
] }) :
|
|
1157
|
+
] }) : transferPickerActive ? (
|
|
1158
|
+
/*
|
|
1159
|
+
Found live (2026-09-29): the agent list a moment ago picked between showing ONE agent row
|
|
1160
|
+
or forcing a scroll — `.cv-presence-list`'s own `max-height` had already been trimmed twice
|
|
1161
|
+
(T7, then T3) chasing that, and both times the room it was chasing lived somewhere else.
|
|
1162
|
+
The full avatar/name/giant-timer block above stayed on screen, unconditionally, the WHOLE
|
|
1163
|
+
time the picker was open — exactly the anti-pattern `.cv-call-devices-inline` (above) was
|
|
1164
|
+
already built to avoid for the pre-connect device picker: "an inline swap, not an overlay,
|
|
1165
|
+
so it gets the full box, not a leftover sliver." The picker never got that same treatment.
|
|
1166
|
+
Measured live (a temporary harness, `playground/react/picker-harness.*`, real Chromium at
|
|
1167
|
+
this SDK's own real embed footprint, 310×520): the avatar+name+timer block alone cost
|
|
1168
|
+
~150px of `.cv-call`'s ~340px of usable height — more than the entire agent list, the mode
|
|
1169
|
+
toggle, and the free-text row combined. Reusing `.cv-call-devices-inline`'s OWN "who's
|
|
1170
|
+
calling doesn't disappear" compact context line (below) instead of the full block gives
|
|
1171
|
+
that ~150px back to the one thing this screen is actually for right now: the list.
|
|
1172
|
+
*/
|
|
1173
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-transfer", children: [
|
|
1174
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-transfer-context", children: [
|
|
1175
|
+
/* @__PURE__ */ jsx("span", { className: "cv-call-devices-title", children: name }),
|
|
1176
|
+
/* @__PURE__ */ jsxs("p", { className: "cv-call-devices-context", children: [
|
|
1177
|
+
showNumber ? `${number} \xB7 ` : "",
|
|
1178
|
+
s === "active" ? `${mm}:${ss}` : STATUS_LABEL[s] ?? ""
|
|
1179
|
+
] })
|
|
1180
|
+
] }),
|
|
1181
|
+
consultOutcome ? /* @__PURE__ */ jsx("p", { className: "cv-dial-outcome", role: "status", "data-shown": "true", children: consultFailText(consultOutcome) }) : null,
|
|
1182
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-transfer-row", children: [
|
|
1183
|
+
/* @__PURE__ */ jsx(
|
|
1184
|
+
"input",
|
|
1185
|
+
{
|
|
1186
|
+
className: "cv-search",
|
|
1187
|
+
value: transferTo,
|
|
1188
|
+
onChange: (e) => setTransferTo(e.target.value),
|
|
1189
|
+
placeholder: "Nombre, ext. o n\xFAmero",
|
|
1190
|
+
"aria-label": "Transferir a",
|
|
1191
|
+
autoFocus: true
|
|
1192
|
+
}
|
|
1193
|
+
),
|
|
1194
|
+
/* @__PURE__ */ jsx(
|
|
1195
|
+
"button",
|
|
1196
|
+
{
|
|
1197
|
+
className: "cv-btn cv-btn--accent",
|
|
1198
|
+
disabled: !transferTo.trim(),
|
|
1199
|
+
onClick: () => void startConsult(),
|
|
1200
|
+
children: transferTo.trim() ? `Llamar a ${truncateForButton(transferTo.trim())}` : "Llamar"
|
|
1201
|
+
}
|
|
1202
|
+
)
|
|
1203
|
+
] }),
|
|
1204
|
+
/* @__PURE__ */ jsx("div", { className: "cv-segmented", role: "tablist", "aria-label": "Modo de transferencia", children: [
|
|
1205
|
+
{ key: "consult", label: "Hablar primero" },
|
|
1206
|
+
{ key: "auto", label: "Transferir directo" }
|
|
1207
|
+
].map(({ key, label }) => /* @__PURE__ */ jsx(
|
|
1208
|
+
"button",
|
|
1209
|
+
{
|
|
1210
|
+
type: "button",
|
|
1211
|
+
role: "tab",
|
|
1212
|
+
"aria-selected": quickTransfer === (key === "auto"),
|
|
1213
|
+
className: "cv-segmented-btn",
|
|
1214
|
+
onClick: () => setQuickTransfer(key === "auto"),
|
|
1215
|
+
children: label
|
|
1216
|
+
},
|
|
1217
|
+
key
|
|
1218
|
+
)) }),
|
|
1219
|
+
/* @__PURE__ */ jsx(
|
|
1220
|
+
TransferPicker,
|
|
1221
|
+
{
|
|
1222
|
+
agents: presence,
|
|
1223
|
+
myUserId,
|
|
1224
|
+
query: transferTo,
|
|
1225
|
+
onSelect: quickTransfer ? selectPresenceAgentAuto : selectPresenceAgent
|
|
1226
|
+
}
|
|
1227
|
+
)
|
|
1228
|
+
] })
|
|
1229
|
+
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
848
1230
|
/* @__PURE__ */ jsxs("div", { className: "cv-avatar-wrap", children: [
|
|
849
1231
|
isRinging ? /* @__PURE__ */ jsx("span", { className: "cv-avatar-ping", "aria-hidden": true }) : null,
|
|
850
1232
|
/* @__PURE__ */ jsx("div", { className: "cv-avatar", "aria-hidden": true, children: initials(name) }),
|
|
@@ -874,31 +1256,6 @@ function CallScreen({
|
|
|
874
1256
|
/* @__PURE__ */ jsx(Headphones, { size: 13, "aria-hidden": true }),
|
|
875
1257
|
"Dispositivos"
|
|
876
1258
|
] })
|
|
877
|
-
] }) : null,
|
|
878
|
-
inCall && showTransfer ? /* @__PURE__ */ jsxs("div", { className: "cv-transfer", children: [
|
|
879
|
-
/* @__PURE__ */ jsx(
|
|
880
|
-
"input",
|
|
881
|
-
{
|
|
882
|
-
className: "cv-search",
|
|
883
|
-
value: transferTo,
|
|
884
|
-
onChange: (e) => setTransferTo(e.target.value),
|
|
885
|
-
placeholder: "Extensi\xF3n o n\xFAmero",
|
|
886
|
-
"aria-label": "Transferir a",
|
|
887
|
-
autoFocus: true
|
|
888
|
-
}
|
|
889
|
-
),
|
|
890
|
-
/* @__PURE__ */ jsx(
|
|
891
|
-
"button",
|
|
892
|
-
{
|
|
893
|
-
className: "cv-btn cv-btn--accent",
|
|
894
|
-
disabled: !transferTo.trim(),
|
|
895
|
-
onClick: () => {
|
|
896
|
-
void call.transfer(transferTo.trim());
|
|
897
|
-
setShowTransfer(false);
|
|
898
|
-
},
|
|
899
|
-
children: "Transferir"
|
|
900
|
-
}
|
|
901
|
-
)
|
|
902
1259
|
] }) : null
|
|
903
1260
|
] }) }),
|
|
904
1261
|
/* @__PURE__ */ jsx("div", { className: "cv-controls", ref: controlsRef, children: isRinging ? /* @__PURE__ */ jsxs("div", { className: "cv-controls-primary", children: [
|
|
@@ -1695,117 +2052,6 @@ function MicPermissionGate({ children }) {
|
|
|
1695
2052
|
if (state === "ready") return /* @__PURE__ */ jsx(Fragment, { children });
|
|
1696
2053
|
return /* @__PURE__ */ jsx("div", { className: "cv-softphone", children: /* @__PURE__ */ jsx(MicPrompt, { state, onRequest: () => void request() }) });
|
|
1697
2054
|
}
|
|
1698
|
-
|
|
1699
|
-
// src/ui/lib/error-text.ts
|
|
1700
|
-
var ERROR_TEXT = {
|
|
1701
|
-
// ── Engine lifecycle ────────────────────────────────────────────────────────
|
|
1702
|
-
// Integrator mistakes rather than user-facing failures, but they must still read as something
|
|
1703
|
-
// rather than as a blank panel if one ever reaches the UI.
|
|
1704
|
-
"engine/duplicate-instance": {
|
|
1705
|
-
title: "Tel\xE9fono duplicado",
|
|
1706
|
-
body: "Hay otro tel\xE9fono abierto en esta p\xE1gina. Cierra uno y vuelve a cargar."
|
|
1707
|
-
},
|
|
1708
|
-
"engine/not-started": { title: "El tel\xE9fono a\xFAn no est\xE1 listo" },
|
|
1709
|
-
"engine/destroyed": {
|
|
1710
|
-
title: "Tel\xE9fono cerrado",
|
|
1711
|
-
body: "Vuelve a cargar la p\xE1gina para usarlo de nuevo."
|
|
1712
|
-
},
|
|
1713
|
-
// ── Auth ────────────────────────────────────────────────────────────────────
|
|
1714
|
-
// Never "token": the user has no idea what one is. What they can act on is the session.
|
|
1715
|
-
"auth/token-fetch-failed": {
|
|
1716
|
-
title: "Sin conexi\xF3n con el servidor",
|
|
1717
|
-
body: "No se ha podido activar el tel\xE9fono. Reint\xE9ntalo en unos segundos."
|
|
1718
|
-
},
|
|
1719
|
-
"auth/token-rejected": {
|
|
1720
|
-
title: "Sesi\xF3n caducada",
|
|
1721
|
-
body: "Vuelve a iniciar sesi\xF3n para poder llamar."
|
|
1722
|
-
},
|
|
1723
|
-
// ── Media ───────────────────────────────────────────────────────────────────
|
|
1724
|
-
// Verbatim from MicPermissionGate, which already says these well and says them first.
|
|
1725
|
-
//
|
|
1726
|
-
// NEVER ACTUALLY SHOWN via this table — these four codes (through `media/playback-blocked`, below)
|
|
1727
|
-
// are never raised as a `SoftphoneError` at all. See the fuller comment on each in `core/errors.ts`
|
|
1728
|
-
// for why. Copy kept here anyway so the catalogue and this file stay the same shape as every other
|
|
1729
|
-
// code — `error-text.test.ts` would fail the build the moment they drifted.
|
|
1730
|
-
"media/permission-denied": {
|
|
1731
|
-
title: "Micr\xF3fono bloqueado",
|
|
1732
|
-
body: "Perm\xEDtelo en tu navegador para poder llamar."
|
|
1733
|
-
},
|
|
1734
|
-
"media/no-input-device": {
|
|
1735
|
-
title: "Sin micr\xF3fono",
|
|
1736
|
-
body: "Conecta un micr\xF3fono y vuelve a intentarlo."
|
|
1737
|
-
},
|
|
1738
|
-
"media/webrtc-unsupported": {
|
|
1739
|
-
title: "Llamadas no disponibles",
|
|
1740
|
-
body: "Este navegador no admite llamadas. Prueba con Chrome, Edge o Firefox."
|
|
1741
|
-
},
|
|
1742
|
-
"media/playback-blocked": { title: "Toca la pantalla para activar el sonido" },
|
|
1743
|
-
// ── Network / signalling ────────────────────────────────────────────────────
|
|
1744
|
-
// "Centralita" rather than "SIP" or "registro": the user knows what a phone system is.
|
|
1745
|
-
"net/registration-failed": {
|
|
1746
|
-
title: "No se pudo conectar",
|
|
1747
|
-
body: "No hemos podido conectar con la centralita. Reintenta en unos segundos."
|
|
1748
|
-
},
|
|
1749
|
-
"net/transport-failed": {
|
|
1750
|
-
title: "Conexi\xF3n perdida",
|
|
1751
|
-
body: "Se ha perdido la conexi\xF3n con la centralita."
|
|
1752
|
-
},
|
|
1753
|
-
// NEVER ACTUALLY SHOWN either — CallScreen's own "Reconectando…" already covers this, off the
|
|
1754
|
-
// call's `reconnecting` state rather than this channel. See the comment in `core/errors.ts`.
|
|
1755
|
-
"net/ice-failed": { title: "Recuperando el audio\u2026" },
|
|
1756
|
-
// ── Call ────────────────────────────────────────────────────────────────────
|
|
1757
|
-
/*
|
|
1758
|
-
THE VOICE RULE FOR THIS BLOCK: say what HAPPENED, not what it is called.
|
|
1759
|
-
|
|
1760
|
-
These were status labels — "Llamada perdida", "Llamada rechazada", "Sin respuesta". Each is a
|
|
1761
|
-
name for a category rather than an account of an event, which is why they read as a database enum
|
|
1762
|
-
someone forgot to translate into human. Worse, a label hides the ACTOR: "Llamada rechazada" never
|
|
1763
|
-
says who rejected what, so the user reading it after declining a call and the user reading it
|
|
1764
|
-
after being declined see the same three words.
|
|
1765
|
-
|
|
1766
|
-
So each one is a short sentence about what the other person, or the phone, did — the form
|
|
1767
|
-
`call/busy` already had, and the reason it was the only one here that read clearly.
|
|
1768
|
-
|
|
1769
|
-
Impersonal third-person plural ("contestaron", "colgaron") throughout: the user does not know who
|
|
1770
|
-
is at the other end, and it is what Spanish uses when the subject is unknown. Mixing it with the
|
|
1771
|
-
singular across neighbouring lines reads as carelessness.
|
|
1772
|
-
|
|
1773
|
-
LENGTH IS A HARD CONSTRAINT, not a preference. `.cv-dial-outcome` is one line at a fixed
|
|
1774
|
-
`1.15rem`, and the keypad below it is `flex: 1` — a title that wraps resizes every key and shifts
|
|
1775
|
-
the grid under a finger already reaching for it. `error-text.test.ts` caps this at 48 characters.
|
|
1776
|
-
*/
|
|
1777
|
-
"call/not-answerable": { title: "Esa llamada ya ha terminado" },
|
|
1778
|
-
/*
|
|
1779
|
-
NOT "Comunicando", which is what this said and what sent us looking. SIP 486 is both "the line is
|
|
1780
|
-
engaged" AND what almost every endpoint sends when a person presses decline — including our own
|
|
1781
|
-
`reject()`. One code, two events, no way to tell them apart, so the copy has to hold both rather
|
|
1782
|
-
than assert the less likely one. `call/rejected` can be definite: an endpoint sending 603 Decline
|
|
1783
|
-
means exactly that.
|
|
1784
|
-
*/
|
|
1785
|
-
"call/busy": { title: "No contestaron o la l\xEDnea estaba ocupada" },
|
|
1786
|
-
"call/no-answer": { title: "No contestaron" },
|
|
1787
|
-
"call/rejected": { title: "No aceptaron tu llamada" },
|
|
1788
|
-
/*
|
|
1789
|
-
The legacy CRM said "La persona que llamaba colgó antes de que pudieras contestar." — the right
|
|
1790
|
-
fact, at three times the width of the line that has to hold it. This keeps the fact and drops the
|
|
1791
|
-
words: the user needs to know the call was FOR them and that nobody is waiting on the line.
|
|
1792
|
-
"Llamada perdida" alone tested badly for exactly the label reason above — it names the category
|
|
1793
|
-
without saying anyone rang.
|
|
1794
|
-
*/
|
|
1795
|
-
"call/missed": { title: "Colgaron antes de que pudieras contestar" },
|
|
1796
|
-
// "No se pudo completar la llamada" was our own "something went wrong": true of every failure and
|
|
1797
|
-
// useful for none. The number is what the user can actually act on.
|
|
1798
|
-
"call/failed": { title: "No se pudo conectar con ese n\xFAmero" },
|
|
1799
|
-
// The load-bearing half is the SECOND sentence: the original call is still up, and a user who
|
|
1800
|
-
// thinks they dropped it will hang up on a client to "fix" it.
|
|
1801
|
-
"call/transfer-failed": { title: "No se pudo transferir. Sigues en la llamada" },
|
|
1802
|
-
"call/unsupported": { title: "Esta funci\xF3n no est\xE1 disponible" },
|
|
1803
|
-
// ── Provider ────────────────────────────────────────────────────────────────
|
|
1804
|
-
"provider/provision-failed": {
|
|
1805
|
-
title: "No se pudo activar el tel\xE9fono",
|
|
1806
|
-
body: "Si el problema contin\xFAa, contacta con soporte."
|
|
1807
|
-
}
|
|
1808
|
-
};
|
|
1809
2055
|
function ErrorGate({
|
|
1810
2056
|
error,
|
|
1811
2057
|
onRetry
|
|
@@ -2137,6 +2383,94 @@ function useCallAudio(enabled) {
|
|
|
2137
2383
|
};
|
|
2138
2384
|
}, [softphone, enabled]);
|
|
2139
2385
|
}
|
|
2386
|
+
function TransferNotices({
|
|
2387
|
+
notices,
|
|
2388
|
+
dismiss
|
|
2389
|
+
}) {
|
|
2390
|
+
const softphone = useSoftphone();
|
|
2391
|
+
const now = useTicker(notices.length > 0, 15e3);
|
|
2392
|
+
const callBack = (notice) => {
|
|
2393
|
+
dismiss(notice.id);
|
|
2394
|
+
if (notice.caller) void softphone.call(notice.caller).catch(() => {
|
|
2395
|
+
});
|
|
2396
|
+
};
|
|
2397
|
+
return /* @__PURE__ */ jsxs(
|
|
2398
|
+
"div",
|
|
2399
|
+
{
|
|
2400
|
+
className: "cv-transfer-notices",
|
|
2401
|
+
role: "status",
|
|
2402
|
+
part: "transfer-notices",
|
|
2403
|
+
"data-has-notices": notices.length > 0,
|
|
2404
|
+
children: [
|
|
2405
|
+
notices.length > 1 ? /* @__PURE__ */ jsxs("p", { className: "cv-transfer-notices-count", children: [
|
|
2406
|
+
notices.length,
|
|
2407
|
+
" traspasos sin conectar"
|
|
2408
|
+
] }) : null,
|
|
2409
|
+
notices.map((notice) => /* @__PURE__ */ jsxs("div", { className: "cv-transfer-notice", part: "transfer-notice", children: [
|
|
2410
|
+
/* @__PURE__ */ jsx("span", { className: "cv-transfer-notice-mark", "aria-hidden": true, children: /* @__PURE__ */ jsx(PhoneMissed, { size: 16 }) }),
|
|
2411
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-transfer-notice-body", children: [
|
|
2412
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-transfer-notice-header", children: [
|
|
2413
|
+
/* @__PURE__ */ jsxs("p", { className: "cv-transfer-notice-title", children: [
|
|
2414
|
+
"El traspaso a ",
|
|
2415
|
+
notice.target,
|
|
2416
|
+
" no se conect\xF3"
|
|
2417
|
+
] }),
|
|
2418
|
+
/* @__PURE__ */ jsx(
|
|
2419
|
+
"button",
|
|
2420
|
+
{
|
|
2421
|
+
type: "button",
|
|
2422
|
+
className: "cv-transfer-notice-dismiss",
|
|
2423
|
+
onClick: () => dismiss(notice.id),
|
|
2424
|
+
"aria-label": "Descartar aviso",
|
|
2425
|
+
part: "transfer-notice-dismiss",
|
|
2426
|
+
children: /* @__PURE__ */ jsx(X, { size: 14 })
|
|
2427
|
+
}
|
|
2428
|
+
)
|
|
2429
|
+
] }),
|
|
2430
|
+
notice.caller ? /* @__PURE__ */ jsxs("p", { className: "cv-transfer-notice-subtitle", children: [
|
|
2431
|
+
notice.caller,
|
|
2432
|
+
" se ha desconectado."
|
|
2433
|
+
] }) : null,
|
|
2434
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-transfer-notice-footer", children: [
|
|
2435
|
+
/* @__PURE__ */ jsx("p", { className: "cv-transfer-notice-age", children: formatAge(elapsedSeconds(notice.at, now)) }),
|
|
2436
|
+
notice.caller ? /* @__PURE__ */ jsxs(
|
|
2437
|
+
"button",
|
|
2438
|
+
{
|
|
2439
|
+
type: "button",
|
|
2440
|
+
className: "cv-btn cv-btn--accent cv-transfer-notice-callback",
|
|
2441
|
+
onClick: () => callBack(notice),
|
|
2442
|
+
part: "transfer-notice-callback",
|
|
2443
|
+
children: [
|
|
2444
|
+
/* @__PURE__ */ jsx(Phone, { size: 14, "aria-hidden": true }),
|
|
2445
|
+
"Llamar de vuelta"
|
|
2446
|
+
]
|
|
2447
|
+
}
|
|
2448
|
+
) : null
|
|
2449
|
+
] })
|
|
2450
|
+
] })
|
|
2451
|
+
] }, notice.id))
|
|
2452
|
+
]
|
|
2453
|
+
}
|
|
2454
|
+
);
|
|
2455
|
+
}
|
|
2456
|
+
function useTransferNotices() {
|
|
2457
|
+
const softphone = useSoftphone();
|
|
2458
|
+
const [notices, setNotices] = useState([]);
|
|
2459
|
+
const nextId = useRef(0);
|
|
2460
|
+
const dismiss = useCallback((id) => {
|
|
2461
|
+
setNotices((prev) => prev.filter((n) => n.id !== id));
|
|
2462
|
+
}, []);
|
|
2463
|
+
useEffect(
|
|
2464
|
+
() => softphone.on("transferOutcome" /* TransferOutcome */, (payload) => {
|
|
2465
|
+
if (payload.status === "not-connected") {
|
|
2466
|
+
const id = nextId.current++;
|
|
2467
|
+
setNotices((prev) => [...prev, { id, target: payload.target, caller: payload.caller, at: payload.at }]);
|
|
2468
|
+
}
|
|
2469
|
+
}),
|
|
2470
|
+
[softphone]
|
|
2471
|
+
);
|
|
2472
|
+
return { notices, dismiss };
|
|
2473
|
+
}
|
|
2140
2474
|
function SoftphoneWidget(props) {
|
|
2141
2475
|
const {
|
|
2142
2476
|
contacts,
|
|
@@ -2161,7 +2495,9 @@ function SoftphoneWidget(props) {
|
|
|
2161
2495
|
useCallAudio(sounds);
|
|
2162
2496
|
const blocked = requireMic && mic.state !== "ready";
|
|
2163
2497
|
const fatal = useLastError();
|
|
2498
|
+
const screenIsFree = !call && !blocked && !fatal && !isFollower;
|
|
2164
2499
|
const { outcome, clear: clearOutcome } = useCallOutcome();
|
|
2500
|
+
const { notices: transferNotices, dismiss: dismissTransferNotice } = useTransferNotices();
|
|
2165
2501
|
const retry = () => {
|
|
2166
2502
|
void (async () => {
|
|
2167
2503
|
try {
|
|
@@ -2185,8 +2521,9 @@ function SoftphoneWidget(props) {
|
|
|
2185
2521
|
className: className ? `cv-softphone ${className}` : "cv-softphone",
|
|
2186
2522
|
...theme === "system" ? {} : { "data-cv-theme": theme },
|
|
2187
2523
|
children: [
|
|
2524
|
+
screenIsFree ? /* @__PURE__ */ jsx(TransferNotices, { notices: transferNotices, dismiss: dismissTransferNotice }) : null,
|
|
2188
2525
|
/* @__PURE__ */ jsx("div", { className: "cv-body", children: isFollower ? /* @__PURE__ */ jsx(LeaderGate, {}) : blocked ? /* @__PURE__ */ jsx(MicPrompt, { state: mic.state, onRequest: () => void mic.request() }) : call ? /* @__PURE__ */ jsx(CallScreen, { call, ...incomingCallAction ? { incomingCallAction } : {} }) : fatal ? /* @__PURE__ */ jsx(ErrorGate, { error: fatal, onRetry: retry }) : tab === "contacts" && contactsSource ? /* @__PURE__ */ jsx(ContactsTab, { source: contactsSource, ...contactAction ? { contactAction } : {} }) : tab === "history" && historySource ? /* @__PURE__ */ jsx(HistoryTab, { source: historySource }) : tab === "settings" ? /* @__PURE__ */ jsx(SettingsTab, {}) : /* @__PURE__ */ jsx(Dialpad, { outcome, onIntent: clearOutcome }) }),
|
|
2189
|
-
|
|
2526
|
+
screenIsFree ? /* @__PURE__ */ jsx("div", { className: "cv-tabs", role: "tablist", children: tabs.map(({ id, icon: Icon3, label }) => /* @__PURE__ */ jsxs(
|
|
2190
2527
|
"button",
|
|
2191
2528
|
{
|
|
2192
2529
|
role: "tab",
|
|
@@ -2199,7 +2536,7 @@ function SoftphoneWidget(props) {
|
|
|
2199
2536
|
]
|
|
2200
2537
|
},
|
|
2201
2538
|
id
|
|
2202
|
-
)) })
|
|
2539
|
+
)) }) : null
|
|
2203
2540
|
]
|
|
2204
2541
|
}
|
|
2205
2542
|
);
|