@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.cjs
CHANGED
|
@@ -13418,6 +13418,20 @@ function useActiveCall() {
|
|
|
13418
13418
|
React.useCallback(() => phone.activeCall, [phone])
|
|
13419
13419
|
);
|
|
13420
13420
|
}
|
|
13421
|
+
function usePresence(active) {
|
|
13422
|
+
const phone = useSoftphone();
|
|
13423
|
+
return React.useSyncExternalStore(
|
|
13424
|
+
React.useCallback(
|
|
13425
|
+
(cb) => {
|
|
13426
|
+
if (!active) return () => {
|
|
13427
|
+
};
|
|
13428
|
+
return phone.on("presenceChanged" /* PresenceChanged */, () => cb());
|
|
13429
|
+
},
|
|
13430
|
+
[phone, active]
|
|
13431
|
+
),
|
|
13432
|
+
React.useCallback(() => phone.presence, [phone])
|
|
13433
|
+
);
|
|
13434
|
+
}
|
|
13421
13435
|
var CALL_EVENTS = [
|
|
13422
13436
|
"stateChanged" /* StateChanged */,
|
|
13423
13437
|
"muted" /* Muted */,
|
|
@@ -13836,6 +13850,125 @@ function elapsedSeconds(since, now) {
|
|
|
13836
13850
|
if (!since) return 0;
|
|
13837
13851
|
return Math.floor((now - since.getTime()) / 1e3);
|
|
13838
13852
|
}
|
|
13853
|
+
function formatAge(totalSeconds) {
|
|
13854
|
+
const s = Math.max(0, Math.floor(totalSeconds));
|
|
13855
|
+
if (s < 60) return "justo ahora";
|
|
13856
|
+
const mm = Math.floor(s / 60);
|
|
13857
|
+
if (mm < 60) return `hace ${mm} min`;
|
|
13858
|
+
const hh = Math.floor(mm / 60);
|
|
13859
|
+
return `hace ${hh} h`;
|
|
13860
|
+
}
|
|
13861
|
+
|
|
13862
|
+
// src/ui/lib/error-text.ts
|
|
13863
|
+
var ERROR_TEXT = {
|
|
13864
|
+
// ── Engine lifecycle ────────────────────────────────────────────────────────
|
|
13865
|
+
// Integrator mistakes rather than user-facing failures, but they must still read as something
|
|
13866
|
+
// rather than as a blank panel if one ever reaches the UI.
|
|
13867
|
+
"engine/duplicate-instance": {
|
|
13868
|
+
title: "Tel\xE9fono duplicado",
|
|
13869
|
+
body: "Hay otro tel\xE9fono abierto en esta p\xE1gina. Cierra uno y vuelve a cargar."
|
|
13870
|
+
},
|
|
13871
|
+
"engine/not-started": { title: "El tel\xE9fono a\xFAn no est\xE1 listo" },
|
|
13872
|
+
"engine/destroyed": {
|
|
13873
|
+
title: "Tel\xE9fono cerrado",
|
|
13874
|
+
body: "Vuelve a cargar la p\xE1gina para usarlo de nuevo."
|
|
13875
|
+
},
|
|
13876
|
+
// ── Auth ────────────────────────────────────────────────────────────────────
|
|
13877
|
+
// Never "token": the user has no idea what one is. What they can act on is the session.
|
|
13878
|
+
"auth/token-fetch-failed": {
|
|
13879
|
+
title: "Sin conexi\xF3n con el servidor",
|
|
13880
|
+
body: "No se ha podido activar el tel\xE9fono. Reint\xE9ntalo en unos segundos."
|
|
13881
|
+
},
|
|
13882
|
+
"auth/token-rejected": {
|
|
13883
|
+
title: "Sesi\xF3n caducada",
|
|
13884
|
+
body: "Vuelve a iniciar sesi\xF3n para poder llamar."
|
|
13885
|
+
},
|
|
13886
|
+
// ── Media ───────────────────────────────────────────────────────────────────
|
|
13887
|
+
// Verbatim from MicPermissionGate, which already says these well and says them first.
|
|
13888
|
+
//
|
|
13889
|
+
// NEVER ACTUALLY SHOWN via this table — these four codes (through `media/playback-blocked`, below)
|
|
13890
|
+
// are never raised as a `SoftphoneError` at all. See the fuller comment on each in `core/errors.ts`
|
|
13891
|
+
// for why. Copy kept here anyway so the catalogue and this file stay the same shape as every other
|
|
13892
|
+
// code — `error-text.test.ts` would fail the build the moment they drifted.
|
|
13893
|
+
"media/permission-denied": {
|
|
13894
|
+
title: "Micr\xF3fono bloqueado",
|
|
13895
|
+
body: "Perm\xEDtelo en tu navegador para poder llamar."
|
|
13896
|
+
},
|
|
13897
|
+
"media/no-input-device": {
|
|
13898
|
+
title: "Sin micr\xF3fono",
|
|
13899
|
+
body: "Conecta un micr\xF3fono y vuelve a intentarlo."
|
|
13900
|
+
},
|
|
13901
|
+
"media/webrtc-unsupported": {
|
|
13902
|
+
title: "Llamadas no disponibles",
|
|
13903
|
+
body: "Este navegador no admite llamadas. Prueba con Chrome, Edge o Firefox."
|
|
13904
|
+
},
|
|
13905
|
+
"media/playback-blocked": { title: "Toca la pantalla para activar el sonido" },
|
|
13906
|
+
// ── Network / signalling ────────────────────────────────────────────────────
|
|
13907
|
+
// "Centralita" rather than "SIP" or "registro": the user knows what a phone system is.
|
|
13908
|
+
"net/registration-failed": {
|
|
13909
|
+
title: "No se pudo conectar",
|
|
13910
|
+
body: "No hemos podido conectar con la centralita. Reintenta en unos segundos."
|
|
13911
|
+
},
|
|
13912
|
+
"net/transport-failed": {
|
|
13913
|
+
title: "Conexi\xF3n perdida",
|
|
13914
|
+
body: "Se ha perdido la conexi\xF3n con la centralita."
|
|
13915
|
+
},
|
|
13916
|
+
// NEVER ACTUALLY SHOWN either — CallScreen's own "Reconectando…" already covers this, off the
|
|
13917
|
+
// call's `reconnecting` state rather than this channel. See the comment in `core/errors.ts`.
|
|
13918
|
+
"net/ice-failed": { title: "Recuperando el audio\u2026" },
|
|
13919
|
+
// ── Call ────────────────────────────────────────────────────────────────────
|
|
13920
|
+
/*
|
|
13921
|
+
THE VOICE RULE FOR THIS BLOCK: say what HAPPENED, not what it is called.
|
|
13922
|
+
|
|
13923
|
+
These were status labels — "Llamada perdida", "Llamada rechazada", "Sin respuesta". Each is a
|
|
13924
|
+
name for a category rather than an account of an event, which is why they read as a database enum
|
|
13925
|
+
someone forgot to translate into human. Worse, a label hides the ACTOR: "Llamada rechazada" never
|
|
13926
|
+
says who rejected what, so the user reading it after declining a call and the user reading it
|
|
13927
|
+
after being declined see the same three words.
|
|
13928
|
+
|
|
13929
|
+
So each one is a short sentence about what the other person, or the phone, did — the form
|
|
13930
|
+
`call/busy` already had, and the reason it was the only one here that read clearly.
|
|
13931
|
+
|
|
13932
|
+
Impersonal third-person plural ("contestaron", "colgaron") throughout: the user does not know who
|
|
13933
|
+
is at the other end, and it is what Spanish uses when the subject is unknown. Mixing it with the
|
|
13934
|
+
singular across neighbouring lines reads as carelessness.
|
|
13935
|
+
|
|
13936
|
+
LENGTH IS A HARD CONSTRAINT, not a preference. `.cv-dial-outcome` is one line at a fixed
|
|
13937
|
+
`1.15rem`, and the keypad below it is `flex: 1` — a title that wraps resizes every key and shifts
|
|
13938
|
+
the grid under a finger already reaching for it. `error-text.test.ts` caps this at 48 characters.
|
|
13939
|
+
*/
|
|
13940
|
+
"call/not-answerable": { title: "Esa llamada ya ha terminado" },
|
|
13941
|
+
/*
|
|
13942
|
+
NOT "Comunicando", which is what this said and what sent us looking. SIP 486 is both "the line is
|
|
13943
|
+
engaged" AND what almost every endpoint sends when a person presses decline — including our own
|
|
13944
|
+
`reject()`. One code, two events, no way to tell them apart, so the copy has to hold both rather
|
|
13945
|
+
than assert the less likely one. `call/rejected` can be definite: an endpoint sending 603 Decline
|
|
13946
|
+
means exactly that.
|
|
13947
|
+
*/
|
|
13948
|
+
"call/busy": { title: "No contestaron o la l\xEDnea estaba ocupada" },
|
|
13949
|
+
"call/no-answer": { title: "No contestaron" },
|
|
13950
|
+
"call/rejected": { title: "No aceptaron tu llamada" },
|
|
13951
|
+
/*
|
|
13952
|
+
The legacy CRM said "La persona que llamaba colgó antes de que pudieras contestar." — the right
|
|
13953
|
+
fact, at three times the width of the line that has to hold it. This keeps the fact and drops the
|
|
13954
|
+
words: the user needs to know the call was FOR them and that nobody is waiting on the line.
|
|
13955
|
+
"Llamada perdida" alone tested badly for exactly the label reason above — it names the category
|
|
13956
|
+
without saying anyone rang.
|
|
13957
|
+
*/
|
|
13958
|
+
"call/missed": { title: "Colgaron antes de que pudieras contestar" },
|
|
13959
|
+
// "No se pudo completar la llamada" was our own "something went wrong": true of every failure and
|
|
13960
|
+
// useful for none. The number is what the user can actually act on.
|
|
13961
|
+
"call/failed": { title: "No se pudo conectar con ese n\xFAmero" },
|
|
13962
|
+
// The load-bearing half is the SECOND sentence: the original call is still up, and a user who
|
|
13963
|
+
// thinks they dropped it will hang up on a client to "fix" it.
|
|
13964
|
+
"call/transfer-failed": { title: "No se pudo transferir. Sigues en la llamada" },
|
|
13965
|
+
"call/unsupported": { title: "Esta funci\xF3n no est\xE1 disponible" },
|
|
13966
|
+
// ── Provider ────────────────────────────────────────────────────────────────
|
|
13967
|
+
"provider/provision-failed": {
|
|
13968
|
+
title: "No se pudo activar el tel\xE9fono",
|
|
13969
|
+
body: "Si el problema contin\xFAa, contacta con soporte."
|
|
13970
|
+
}
|
|
13971
|
+
};
|
|
13839
13972
|
function useTicker(enabled, intervalMs = 1e3) {
|
|
13840
13973
|
const [now, setNow] = React.useState(() => Date.now());
|
|
13841
13974
|
React.useEffect(() => {
|
|
@@ -13948,17 +14081,195 @@ var STATUS_LABEL = {
|
|
|
13948
14081
|
reconnecting: "Reconectando\u2026",
|
|
13949
14082
|
held: "En espera"
|
|
13950
14083
|
};
|
|
14084
|
+
var CONSULT_FAIL_CODE = {
|
|
14085
|
+
busy: "call/busy",
|
|
14086
|
+
"no-answer": "call/no-answer",
|
|
14087
|
+
rejected: "call/rejected",
|
|
14088
|
+
failed: "call/failed"
|
|
14089
|
+
};
|
|
14090
|
+
function consultFailText(reason) {
|
|
14091
|
+
return ERROR_TEXT[CONSULT_FAIL_CODE[reason] ?? "call/transfer-failed"].title;
|
|
14092
|
+
}
|
|
14093
|
+
var MIN_CONSULT_HOLD_SECONDS = 10;
|
|
14094
|
+
function ConsultScreen({
|
|
14095
|
+
mainName,
|
|
14096
|
+
consultView,
|
|
14097
|
+
now,
|
|
14098
|
+
onReturn,
|
|
14099
|
+
onComplete,
|
|
14100
|
+
onHangupAll
|
|
14101
|
+
}) {
|
|
14102
|
+
const s = consultView.state;
|
|
14103
|
+
const answered = s === "active";
|
|
14104
|
+
const name = consultView.remote.displayName;
|
|
14105
|
+
const number = consultView.remote.number;
|
|
14106
|
+
const showNumber = number !== name;
|
|
14107
|
+
const heldSeconds = elapsedSeconds(consultView.answeredAt, now);
|
|
14108
|
+
const [rawM = "0", ss = "00"] = formatDuration(heldSeconds).split(":");
|
|
14109
|
+
const mm = rawM.padStart(2, "0");
|
|
14110
|
+
const canComplete = heldSeconds >= MIN_CONSULT_HOLD_SECONDS;
|
|
14111
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-view", children: [
|
|
14112
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-call", children: [
|
|
14113
|
+
/* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-consult-hold-banner", onClick: onReturn, children: [
|
|
14114
|
+
/* @__PURE__ */ jsxRuntime.jsx(Pause, { size: 13, "aria-hidden": true }),
|
|
14115
|
+
mainName,
|
|
14116
|
+
" en espera \xB7 Volver"
|
|
14117
|
+
] }),
|
|
14118
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-avatar-wrap", children: [
|
|
14119
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-avatar", "aria-hidden": true, children: initials(name) }),
|
|
14120
|
+
answered ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-avatar-dot", "aria-hidden": true }) : null
|
|
14121
|
+
] }),
|
|
14122
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call-name", children: name }),
|
|
14123
|
+
showNumber ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call-number", children: number }) : null,
|
|
14124
|
+
answered ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-timer", "aria-label": "Duraci\xF3n de la llamada", children: [
|
|
14125
|
+
/* @__PURE__ */ jsxRuntime.jsx("b", { children: mm }),
|
|
14126
|
+
/* @__PURE__ */ jsxRuntime.jsx("i", { children: ":" }),
|
|
14127
|
+
/* @__PURE__ */ jsxRuntime.jsx("b", { children: ss })
|
|
14128
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call-status", children: STATUS_LABEL[s] ?? "" })
|
|
14129
|
+
] }),
|
|
14130
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-controls", children: [
|
|
14131
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-transfer-actions", children: [
|
|
14132
|
+
answered ? /* @__PURE__ */ jsxRuntime.jsx("button", { className: "cv-btn cv-btn--accent", onClick: onComplete, disabled: !canComplete, children: "Completar" }) : null,
|
|
14133
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "cv-btn", onClick: onReturn, children: "Cancelar" })
|
|
14134
|
+
] }),
|
|
14135
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-controls-primary", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-ctrl-item", children: [
|
|
14136
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
14137
|
+
"button",
|
|
14138
|
+
{
|
|
14139
|
+
type: "button",
|
|
14140
|
+
className: "cv-round cv-round--hangup",
|
|
14141
|
+
onClick: onHangupAll,
|
|
14142
|
+
"aria-label": "Colgar todo",
|
|
14143
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(PhoneOff, { size: 26 })
|
|
14144
|
+
}
|
|
14145
|
+
),
|
|
14146
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Colgar todo" })
|
|
14147
|
+
] }) })
|
|
14148
|
+
] })
|
|
14149
|
+
] });
|
|
14150
|
+
}
|
|
14151
|
+
var PRESENCE_LABEL = {
|
|
14152
|
+
disponible: "Disponible",
|
|
14153
|
+
en_llamada: "En llamada",
|
|
14154
|
+
desconectado: "Desconectado"
|
|
14155
|
+
};
|
|
14156
|
+
var PRESENCE_RANK = {
|
|
14157
|
+
disponible: 0,
|
|
14158
|
+
en_llamada: 1,
|
|
14159
|
+
desconectado: 2
|
|
14160
|
+
};
|
|
14161
|
+
function filterAgents(agents, query) {
|
|
14162
|
+
const q = query.trim().toLowerCase();
|
|
14163
|
+
return agents.filter((a) => !q || (a.name ?? "").toLowerCase().includes(q) || a.extension.includes(q)).sort(
|
|
14164
|
+
(a, b) => PRESENCE_RANK[a.state] - PRESENCE_RANK[b.state] || (a.name ?? a.extension).localeCompare(b.name ?? b.extension)
|
|
14165
|
+
);
|
|
14166
|
+
}
|
|
14167
|
+
function truncateForButton(text, max = 16) {
|
|
14168
|
+
return text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
14169
|
+
}
|
|
14170
|
+
function TransferPicker({
|
|
14171
|
+
agents,
|
|
14172
|
+
myUserId,
|
|
14173
|
+
query,
|
|
14174
|
+
onSelect
|
|
14175
|
+
}) {
|
|
14176
|
+
const others = agents.filter((a) => a.userId !== myUserId);
|
|
14177
|
+
if (others.length === 0) return null;
|
|
14178
|
+
const filtered = filterAgents(others, query);
|
|
14179
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-presence", children: [
|
|
14180
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-presence-list", role: "listbox", "aria-label": "Compa\xF1eros", children: filtered.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-presence-empty", children: "Sin coincidencias" }) : filtered.map((agent) => {
|
|
14181
|
+
const isBusy = agent.state === "en_llamada";
|
|
14182
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
14183
|
+
"button",
|
|
14184
|
+
{
|
|
14185
|
+
type: "button",
|
|
14186
|
+
role: "option",
|
|
14187
|
+
className: "cv-presence-item",
|
|
14188
|
+
disabled: isBusy,
|
|
14189
|
+
onClick: () => onSelect(agent.extension),
|
|
14190
|
+
children: [
|
|
14191
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-presence-dot", "data-state": agent.state, "aria-hidden": true }),
|
|
14192
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-presence-name", children: agent.name ?? agent.extension }),
|
|
14193
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-presence-meta", children: [
|
|
14194
|
+
agent.extension,
|
|
14195
|
+
" \xB7 ",
|
|
14196
|
+
PRESENCE_LABEL[agent.state]
|
|
14197
|
+
] })
|
|
14198
|
+
]
|
|
14199
|
+
},
|
|
14200
|
+
agent.userId
|
|
14201
|
+
);
|
|
14202
|
+
}) }),
|
|
14203
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-presence-legend", children: "Desconectado: sin conexi\xF3n en el softphone web. Puede atender desde otro dispositivo." })
|
|
14204
|
+
] });
|
|
14205
|
+
}
|
|
13951
14206
|
function CallScreen({
|
|
13952
14207
|
call,
|
|
13953
14208
|
incomingCallAction
|
|
13954
14209
|
}) {
|
|
13955
14210
|
const view = useCall(call);
|
|
14211
|
+
const softphone = useSoftphone();
|
|
13956
14212
|
const [showTransfer, setShowTransfer] = React.useState(false);
|
|
13957
14213
|
const [transferTo, setTransferTo] = React.useState("");
|
|
14214
|
+
const [consultCall, setConsultCall] = React.useState(null);
|
|
14215
|
+
const [consultOutcome, setConsultOutcome] = React.useState(null);
|
|
14216
|
+
const [myUserId, setMyUserId] = React.useState(null);
|
|
14217
|
+
const consultView = useCall(consultCall);
|
|
14218
|
+
const consultIntentRef = React.useRef(null);
|
|
13958
14219
|
const [showDevices, setShowDevices] = React.useState(false);
|
|
13959
14220
|
const controlsRef = React.useRef(null);
|
|
13960
14221
|
const now = useTicker(view?.state === "active");
|
|
14222
|
+
const consultNow = useTicker(consultView?.state === "active");
|
|
13961
14223
|
const isConnected = view ? view.state === "active" || view.state === "held" || view.state === "reconnecting" : false;
|
|
14224
|
+
const transferPickerActive = isConnected && showTransfer && !consultCall;
|
|
14225
|
+
const presence = usePresence(transferPickerActive);
|
|
14226
|
+
React.useEffect(() => {
|
|
14227
|
+
const listAgents = softphone.sources?.listAgents;
|
|
14228
|
+
if (!listAgents) return;
|
|
14229
|
+
let cancelled = false;
|
|
14230
|
+
void listAgents().then((d) => !cancelled && setMyUserId(d.me?.userId ?? null)).catch(() => {
|
|
14231
|
+
});
|
|
14232
|
+
return () => {
|
|
14233
|
+
cancelled = true;
|
|
14234
|
+
};
|
|
14235
|
+
}, [softphone]);
|
|
14236
|
+
React.useEffect(() => {
|
|
14237
|
+
if (!consultCall) return;
|
|
14238
|
+
return consultCall.on("ended" /* Ended */, ({ reason }) => {
|
|
14239
|
+
const intent = consultIntentRef.current;
|
|
14240
|
+
consultIntentRef.current = null;
|
|
14241
|
+
setConsultCall(null);
|
|
14242
|
+
if (!intent) {
|
|
14243
|
+
setConsultOutcome(reason);
|
|
14244
|
+
if (call.state !== "ended") void call.unhold();
|
|
14245
|
+
}
|
|
14246
|
+
});
|
|
14247
|
+
}, [consultCall, call]);
|
|
14248
|
+
const startConsult = async (target) => {
|
|
14249
|
+
const t = (target ?? transferTo).trim();
|
|
14250
|
+
if (!t) return;
|
|
14251
|
+
setConsultOutcome(null);
|
|
14252
|
+
setConsultCall(await call.startConsult(t));
|
|
14253
|
+
};
|
|
14254
|
+
const selectPresenceAgent = (extension) => {
|
|
14255
|
+
setTransferTo(extension);
|
|
14256
|
+
void startConsult(extension);
|
|
14257
|
+
};
|
|
14258
|
+
const [quickTransfer, setQuickTransfer] = React.useState(false);
|
|
14259
|
+
const selectPresenceAgentAuto = (extension) => {
|
|
14260
|
+
void call.transfer(extension);
|
|
14261
|
+
};
|
|
14262
|
+
const completeTransfer = async () => {
|
|
14263
|
+
if (!consultCall) return;
|
|
14264
|
+
const target = transferTo.trim();
|
|
14265
|
+
consultIntentRef.current = "complete";
|
|
14266
|
+
await consultCall.hangup();
|
|
14267
|
+
await call.transfer(target);
|
|
14268
|
+
};
|
|
14269
|
+
const cancelConsult = async () => {
|
|
14270
|
+
consultIntentRef.current = "cancel";
|
|
14271
|
+
await call.cancelConsult();
|
|
14272
|
+
};
|
|
13962
14273
|
React.useEffect(() => {
|
|
13963
14274
|
if (!showDevices) return;
|
|
13964
14275
|
const onKey = (e) => {
|
|
@@ -13983,6 +14294,19 @@ function CallScreen({
|
|
|
13983
14294
|
const name = view.remote.displayName;
|
|
13984
14295
|
const number = view.remote.number;
|
|
13985
14296
|
const showNumber = number !== name;
|
|
14297
|
+
if (consultCall && consultView) {
|
|
14298
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
14299
|
+
ConsultScreen,
|
|
14300
|
+
{
|
|
14301
|
+
mainName: name,
|
|
14302
|
+
consultView,
|
|
14303
|
+
now: consultNow,
|
|
14304
|
+
onReturn: () => void cancelConsult(),
|
|
14305
|
+
onComplete: () => void completeTransfer(),
|
|
14306
|
+
onHangupAll: () => void call.hangup()
|
|
14307
|
+
}
|
|
14308
|
+
);
|
|
14309
|
+
}
|
|
13986
14310
|
const incomingAction = isRinging && view.remote.contact && incomingCallAction ? incomingCallAction : null;
|
|
13987
14311
|
const IncomingActionIcon = incomingAction?.icon ?? ExternalLink;
|
|
13988
14312
|
const [rawM = "0", ss = "00"] = formatDuration(elapsedSeconds(view.answeredAt, now)).split(":");
|
|
@@ -14001,7 +14325,79 @@ function CallScreen({
|
|
|
14001
14325
|
STATUS_LABEL[s] ?? ""
|
|
14002
14326
|
] }),
|
|
14003
14327
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-settings-section-body", children: /* @__PURE__ */ jsxRuntime.jsx(DevicePickers, {}) })
|
|
14004
|
-
] }) :
|
|
14328
|
+
] }) : transferPickerActive ? (
|
|
14329
|
+
/*
|
|
14330
|
+
Found live (2026-09-29): the agent list a moment ago picked between showing ONE agent row
|
|
14331
|
+
or forcing a scroll — `.cv-presence-list`'s own `max-height` had already been trimmed twice
|
|
14332
|
+
(T7, then T3) chasing that, and both times the room it was chasing lived somewhere else.
|
|
14333
|
+
The full avatar/name/giant-timer block above stayed on screen, unconditionally, the WHOLE
|
|
14334
|
+
time the picker was open — exactly the anti-pattern `.cv-call-devices-inline` (above) was
|
|
14335
|
+
already built to avoid for the pre-connect device picker: "an inline swap, not an overlay,
|
|
14336
|
+
so it gets the full box, not a leftover sliver." The picker never got that same treatment.
|
|
14337
|
+
Measured live (a temporary harness, `playground/react/picker-harness.*`, real Chromium at
|
|
14338
|
+
this SDK's own real embed footprint, 310×520): the avatar+name+timer block alone cost
|
|
14339
|
+
~150px of `.cv-call`'s ~340px of usable height — more than the entire agent list, the mode
|
|
14340
|
+
toggle, and the free-text row combined. Reusing `.cv-call-devices-inline`'s OWN "who's
|
|
14341
|
+
calling doesn't disappear" compact context line (below) instead of the full block gives
|
|
14342
|
+
that ~150px back to the one thing this screen is actually for right now: the list.
|
|
14343
|
+
*/
|
|
14344
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-transfer", children: [
|
|
14345
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-transfer-context", children: [
|
|
14346
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-call-devices-title", children: name }),
|
|
14347
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "cv-call-devices-context", children: [
|
|
14348
|
+
showNumber ? `${number} \xB7 ` : "",
|
|
14349
|
+
s === "active" ? `${mm}:${ss}` : STATUS_LABEL[s] ?? ""
|
|
14350
|
+
] })
|
|
14351
|
+
] }),
|
|
14352
|
+
consultOutcome ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-dial-outcome", role: "status", "data-shown": "true", children: consultFailText(consultOutcome) }) : null,
|
|
14353
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-transfer-row", children: [
|
|
14354
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
14355
|
+
"input",
|
|
14356
|
+
{
|
|
14357
|
+
className: "cv-search",
|
|
14358
|
+
value: transferTo,
|
|
14359
|
+
onChange: (e) => setTransferTo(e.target.value),
|
|
14360
|
+
placeholder: "Nombre, ext. o n\xFAmero",
|
|
14361
|
+
"aria-label": "Transferir a",
|
|
14362
|
+
autoFocus: true
|
|
14363
|
+
}
|
|
14364
|
+
),
|
|
14365
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
14366
|
+
"button",
|
|
14367
|
+
{
|
|
14368
|
+
className: "cv-btn cv-btn--accent",
|
|
14369
|
+
disabled: !transferTo.trim(),
|
|
14370
|
+
onClick: () => void startConsult(),
|
|
14371
|
+
children: transferTo.trim() ? `Llamar a ${truncateForButton(transferTo.trim())}` : "Llamar"
|
|
14372
|
+
}
|
|
14373
|
+
)
|
|
14374
|
+
] }),
|
|
14375
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-segmented", role: "tablist", "aria-label": "Modo de transferencia", children: [
|
|
14376
|
+
{ key: "consult", label: "Hablar primero" },
|
|
14377
|
+
{ key: "auto", label: "Transferir directo" }
|
|
14378
|
+
].map(({ key, label }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
14379
|
+
"button",
|
|
14380
|
+
{
|
|
14381
|
+
type: "button",
|
|
14382
|
+
role: "tab",
|
|
14383
|
+
"aria-selected": quickTransfer === (key === "auto"),
|
|
14384
|
+
className: "cv-segmented-btn",
|
|
14385
|
+
onClick: () => setQuickTransfer(key === "auto"),
|
|
14386
|
+
children: label
|
|
14387
|
+
},
|
|
14388
|
+
key
|
|
14389
|
+
)) }),
|
|
14390
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
14391
|
+
TransferPicker,
|
|
14392
|
+
{
|
|
14393
|
+
agents: presence,
|
|
14394
|
+
myUserId,
|
|
14395
|
+
query: transferTo,
|
|
14396
|
+
onSelect: quickTransfer ? selectPresenceAgentAuto : selectPresenceAgent
|
|
14397
|
+
}
|
|
14398
|
+
)
|
|
14399
|
+
] })
|
|
14400
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
14005
14401
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-avatar-wrap", children: [
|
|
14006
14402
|
isRinging ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-avatar-ping", "aria-hidden": true }) : null,
|
|
14007
14403
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-avatar", "aria-hidden": true, children: initials(name) }),
|
|
@@ -14031,31 +14427,6 @@ function CallScreen({
|
|
|
14031
14427
|
/* @__PURE__ */ jsxRuntime.jsx(Headphones, { size: 13, "aria-hidden": true }),
|
|
14032
14428
|
"Dispositivos"
|
|
14033
14429
|
] })
|
|
14034
|
-
] }) : null,
|
|
14035
|
-
inCall && showTransfer ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-transfer", children: [
|
|
14036
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
14037
|
-
"input",
|
|
14038
|
-
{
|
|
14039
|
-
className: "cv-search",
|
|
14040
|
-
value: transferTo,
|
|
14041
|
-
onChange: (e) => setTransferTo(e.target.value),
|
|
14042
|
-
placeholder: "Extensi\xF3n o n\xFAmero",
|
|
14043
|
-
"aria-label": "Transferir a",
|
|
14044
|
-
autoFocus: true
|
|
14045
|
-
}
|
|
14046
|
-
),
|
|
14047
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
14048
|
-
"button",
|
|
14049
|
-
{
|
|
14050
|
-
className: "cv-btn cv-btn--accent",
|
|
14051
|
-
disabled: !transferTo.trim(),
|
|
14052
|
-
onClick: () => {
|
|
14053
|
-
void call.transfer(transferTo.trim());
|
|
14054
|
-
setShowTransfer(false);
|
|
14055
|
-
},
|
|
14056
|
-
children: "Transferir"
|
|
14057
|
-
}
|
|
14058
|
-
)
|
|
14059
14430
|
] }) : null
|
|
14060
14431
|
] }) }),
|
|
14061
14432
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-controls", ref: controlsRef, children: isRinging ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-controls-primary", children: [
|
|
@@ -14862,117 +15233,6 @@ function MicPermissionGate({ children }) {
|
|
|
14862
15233
|
if (state === "ready") return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
|
|
14863
15234
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-softphone", children: /* @__PURE__ */ jsxRuntime.jsx(MicPrompt, { state, onRequest: () => void request() }) });
|
|
14864
15235
|
}
|
|
14865
|
-
|
|
14866
|
-
// src/ui/lib/error-text.ts
|
|
14867
|
-
var ERROR_TEXT = {
|
|
14868
|
-
// ── Engine lifecycle ────────────────────────────────────────────────────────
|
|
14869
|
-
// Integrator mistakes rather than user-facing failures, but they must still read as something
|
|
14870
|
-
// rather than as a blank panel if one ever reaches the UI.
|
|
14871
|
-
"engine/duplicate-instance": {
|
|
14872
|
-
title: "Tel\xE9fono duplicado",
|
|
14873
|
-
body: "Hay otro tel\xE9fono abierto en esta p\xE1gina. Cierra uno y vuelve a cargar."
|
|
14874
|
-
},
|
|
14875
|
-
"engine/not-started": { title: "El tel\xE9fono a\xFAn no est\xE1 listo" },
|
|
14876
|
-
"engine/destroyed": {
|
|
14877
|
-
title: "Tel\xE9fono cerrado",
|
|
14878
|
-
body: "Vuelve a cargar la p\xE1gina para usarlo de nuevo."
|
|
14879
|
-
},
|
|
14880
|
-
// ── Auth ────────────────────────────────────────────────────────────────────
|
|
14881
|
-
// Never "token": the user has no idea what one is. What they can act on is the session.
|
|
14882
|
-
"auth/token-fetch-failed": {
|
|
14883
|
-
title: "Sin conexi\xF3n con el servidor",
|
|
14884
|
-
body: "No se ha podido activar el tel\xE9fono. Reint\xE9ntalo en unos segundos."
|
|
14885
|
-
},
|
|
14886
|
-
"auth/token-rejected": {
|
|
14887
|
-
title: "Sesi\xF3n caducada",
|
|
14888
|
-
body: "Vuelve a iniciar sesi\xF3n para poder llamar."
|
|
14889
|
-
},
|
|
14890
|
-
// ── Media ───────────────────────────────────────────────────────────────────
|
|
14891
|
-
// Verbatim from MicPermissionGate, which already says these well and says them first.
|
|
14892
|
-
//
|
|
14893
|
-
// NEVER ACTUALLY SHOWN via this table — these four codes (through `media/playback-blocked`, below)
|
|
14894
|
-
// are never raised as a `SoftphoneError` at all. See the fuller comment on each in `core/errors.ts`
|
|
14895
|
-
// for why. Copy kept here anyway so the catalogue and this file stay the same shape as every other
|
|
14896
|
-
// code — `error-text.test.ts` would fail the build the moment they drifted.
|
|
14897
|
-
"media/permission-denied": {
|
|
14898
|
-
title: "Micr\xF3fono bloqueado",
|
|
14899
|
-
body: "Perm\xEDtelo en tu navegador para poder llamar."
|
|
14900
|
-
},
|
|
14901
|
-
"media/no-input-device": {
|
|
14902
|
-
title: "Sin micr\xF3fono",
|
|
14903
|
-
body: "Conecta un micr\xF3fono y vuelve a intentarlo."
|
|
14904
|
-
},
|
|
14905
|
-
"media/webrtc-unsupported": {
|
|
14906
|
-
title: "Llamadas no disponibles",
|
|
14907
|
-
body: "Este navegador no admite llamadas. Prueba con Chrome, Edge o Firefox."
|
|
14908
|
-
},
|
|
14909
|
-
"media/playback-blocked": { title: "Toca la pantalla para activar el sonido" },
|
|
14910
|
-
// ── Network / signalling ────────────────────────────────────────────────────
|
|
14911
|
-
// "Centralita" rather than "SIP" or "registro": the user knows what a phone system is.
|
|
14912
|
-
"net/registration-failed": {
|
|
14913
|
-
title: "No se pudo conectar",
|
|
14914
|
-
body: "No hemos podido conectar con la centralita. Reintenta en unos segundos."
|
|
14915
|
-
},
|
|
14916
|
-
"net/transport-failed": {
|
|
14917
|
-
title: "Conexi\xF3n perdida",
|
|
14918
|
-
body: "Se ha perdido la conexi\xF3n con la centralita."
|
|
14919
|
-
},
|
|
14920
|
-
// NEVER ACTUALLY SHOWN either — CallScreen's own "Reconectando…" already covers this, off the
|
|
14921
|
-
// call's `reconnecting` state rather than this channel. See the comment in `core/errors.ts`.
|
|
14922
|
-
"net/ice-failed": { title: "Recuperando el audio\u2026" },
|
|
14923
|
-
// ── Call ────────────────────────────────────────────────────────────────────
|
|
14924
|
-
/*
|
|
14925
|
-
THE VOICE RULE FOR THIS BLOCK: say what HAPPENED, not what it is called.
|
|
14926
|
-
|
|
14927
|
-
These were status labels — "Llamada perdida", "Llamada rechazada", "Sin respuesta". Each is a
|
|
14928
|
-
name for a category rather than an account of an event, which is why they read as a database enum
|
|
14929
|
-
someone forgot to translate into human. Worse, a label hides the ACTOR: "Llamada rechazada" never
|
|
14930
|
-
says who rejected what, so the user reading it after declining a call and the user reading it
|
|
14931
|
-
after being declined see the same three words.
|
|
14932
|
-
|
|
14933
|
-
So each one is a short sentence about what the other person, or the phone, did — the form
|
|
14934
|
-
`call/busy` already had, and the reason it was the only one here that read clearly.
|
|
14935
|
-
|
|
14936
|
-
Impersonal third-person plural ("contestaron", "colgaron") throughout: the user does not know who
|
|
14937
|
-
is at the other end, and it is what Spanish uses when the subject is unknown. Mixing it with the
|
|
14938
|
-
singular across neighbouring lines reads as carelessness.
|
|
14939
|
-
|
|
14940
|
-
LENGTH IS A HARD CONSTRAINT, not a preference. `.cv-dial-outcome` is one line at a fixed
|
|
14941
|
-
`1.15rem`, and the keypad below it is `flex: 1` — a title that wraps resizes every key and shifts
|
|
14942
|
-
the grid under a finger already reaching for it. `error-text.test.ts` caps this at 48 characters.
|
|
14943
|
-
*/
|
|
14944
|
-
"call/not-answerable": { title: "Esa llamada ya ha terminado" },
|
|
14945
|
-
/*
|
|
14946
|
-
NOT "Comunicando", which is what this said and what sent us looking. SIP 486 is both "the line is
|
|
14947
|
-
engaged" AND what almost every endpoint sends when a person presses decline — including our own
|
|
14948
|
-
`reject()`. One code, two events, no way to tell them apart, so the copy has to hold both rather
|
|
14949
|
-
than assert the less likely one. `call/rejected` can be definite: an endpoint sending 603 Decline
|
|
14950
|
-
means exactly that.
|
|
14951
|
-
*/
|
|
14952
|
-
"call/busy": { title: "No contestaron o la l\xEDnea estaba ocupada" },
|
|
14953
|
-
"call/no-answer": { title: "No contestaron" },
|
|
14954
|
-
"call/rejected": { title: "No aceptaron tu llamada" },
|
|
14955
|
-
/*
|
|
14956
|
-
The legacy CRM said "La persona que llamaba colgó antes de que pudieras contestar." — the right
|
|
14957
|
-
fact, at three times the width of the line that has to hold it. This keeps the fact and drops the
|
|
14958
|
-
words: the user needs to know the call was FOR them and that nobody is waiting on the line.
|
|
14959
|
-
"Llamada perdida" alone tested badly for exactly the label reason above — it names the category
|
|
14960
|
-
without saying anyone rang.
|
|
14961
|
-
*/
|
|
14962
|
-
"call/missed": { title: "Colgaron antes de que pudieras contestar" },
|
|
14963
|
-
// "No se pudo completar la llamada" was our own "something went wrong": true of every failure and
|
|
14964
|
-
// useful for none. The number is what the user can actually act on.
|
|
14965
|
-
"call/failed": { title: "No se pudo conectar con ese n\xFAmero" },
|
|
14966
|
-
// The load-bearing half is the SECOND sentence: the original call is still up, and a user who
|
|
14967
|
-
// thinks they dropped it will hang up on a client to "fix" it.
|
|
14968
|
-
"call/transfer-failed": { title: "No se pudo transferir. Sigues en la llamada" },
|
|
14969
|
-
"call/unsupported": { title: "Esta funci\xF3n no est\xE1 disponible" },
|
|
14970
|
-
// ── Provider ────────────────────────────────────────────────────────────────
|
|
14971
|
-
"provider/provision-failed": {
|
|
14972
|
-
title: "No se pudo activar el tel\xE9fono",
|
|
14973
|
-
body: "Si el problema contin\xFAa, contacta con soporte."
|
|
14974
|
-
}
|
|
14975
|
-
};
|
|
14976
15236
|
function ErrorGate({
|
|
14977
15237
|
error,
|
|
14978
15238
|
onRetry
|
|
@@ -15304,6 +15564,94 @@ function useCallAudio(enabled) {
|
|
|
15304
15564
|
};
|
|
15305
15565
|
}, [softphone, enabled]);
|
|
15306
15566
|
}
|
|
15567
|
+
function TransferNotices({
|
|
15568
|
+
notices,
|
|
15569
|
+
dismiss
|
|
15570
|
+
}) {
|
|
15571
|
+
const softphone = useSoftphone();
|
|
15572
|
+
const now = useTicker(notices.length > 0, 15e3);
|
|
15573
|
+
const callBack = (notice) => {
|
|
15574
|
+
dismiss(notice.id);
|
|
15575
|
+
if (notice.caller) void softphone.call(notice.caller).catch(() => {
|
|
15576
|
+
});
|
|
15577
|
+
};
|
|
15578
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
15579
|
+
"div",
|
|
15580
|
+
{
|
|
15581
|
+
className: "cv-transfer-notices",
|
|
15582
|
+
role: "status",
|
|
15583
|
+
part: "transfer-notices",
|
|
15584
|
+
"data-has-notices": notices.length > 0,
|
|
15585
|
+
children: [
|
|
15586
|
+
notices.length > 1 ? /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "cv-transfer-notices-count", children: [
|
|
15587
|
+
notices.length,
|
|
15588
|
+
" traspasos sin conectar"
|
|
15589
|
+
] }) : null,
|
|
15590
|
+
notices.map((notice) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-transfer-notice", part: "transfer-notice", children: [
|
|
15591
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-transfer-notice-mark", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx(PhoneMissed, { size: 16 }) }),
|
|
15592
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-transfer-notice-body", children: [
|
|
15593
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-transfer-notice-header", children: [
|
|
15594
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "cv-transfer-notice-title", children: [
|
|
15595
|
+
"El traspaso a ",
|
|
15596
|
+
notice.target,
|
|
15597
|
+
" no se conect\xF3"
|
|
15598
|
+
] }),
|
|
15599
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
15600
|
+
"button",
|
|
15601
|
+
{
|
|
15602
|
+
type: "button",
|
|
15603
|
+
className: "cv-transfer-notice-dismiss",
|
|
15604
|
+
onClick: () => dismiss(notice.id),
|
|
15605
|
+
"aria-label": "Descartar aviso",
|
|
15606
|
+
part: "transfer-notice-dismiss",
|
|
15607
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(X, { size: 14 })
|
|
15608
|
+
}
|
|
15609
|
+
)
|
|
15610
|
+
] }),
|
|
15611
|
+
notice.caller ? /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "cv-transfer-notice-subtitle", children: [
|
|
15612
|
+
notice.caller,
|
|
15613
|
+
" se ha desconectado."
|
|
15614
|
+
] }) : null,
|
|
15615
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-transfer-notice-footer", children: [
|
|
15616
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-transfer-notice-age", children: formatAge(elapsedSeconds(notice.at, now)) }),
|
|
15617
|
+
notice.caller ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
15618
|
+
"button",
|
|
15619
|
+
{
|
|
15620
|
+
type: "button",
|
|
15621
|
+
className: "cv-btn cv-btn--accent cv-transfer-notice-callback",
|
|
15622
|
+
onClick: () => callBack(notice),
|
|
15623
|
+
part: "transfer-notice-callback",
|
|
15624
|
+
children: [
|
|
15625
|
+
/* @__PURE__ */ jsxRuntime.jsx(Phone, { size: 14, "aria-hidden": true }),
|
|
15626
|
+
"Llamar de vuelta"
|
|
15627
|
+
]
|
|
15628
|
+
}
|
|
15629
|
+
) : null
|
|
15630
|
+
] })
|
|
15631
|
+
] })
|
|
15632
|
+
] }, notice.id))
|
|
15633
|
+
]
|
|
15634
|
+
}
|
|
15635
|
+
);
|
|
15636
|
+
}
|
|
15637
|
+
function useTransferNotices() {
|
|
15638
|
+
const softphone = useSoftphone();
|
|
15639
|
+
const [notices, setNotices] = React.useState([]);
|
|
15640
|
+
const nextId = React.useRef(0);
|
|
15641
|
+
const dismiss = React.useCallback((id) => {
|
|
15642
|
+
setNotices((prev) => prev.filter((n) => n.id !== id));
|
|
15643
|
+
}, []);
|
|
15644
|
+
React.useEffect(
|
|
15645
|
+
() => softphone.on("transferOutcome" /* TransferOutcome */, (payload) => {
|
|
15646
|
+
if (payload.status === "not-connected") {
|
|
15647
|
+
const id = nextId.current++;
|
|
15648
|
+
setNotices((prev) => [...prev, { id, target: payload.target, caller: payload.caller, at: payload.at }]);
|
|
15649
|
+
}
|
|
15650
|
+
}),
|
|
15651
|
+
[softphone]
|
|
15652
|
+
);
|
|
15653
|
+
return { notices, dismiss };
|
|
15654
|
+
}
|
|
15307
15655
|
function SoftphoneWidget(props) {
|
|
15308
15656
|
const {
|
|
15309
15657
|
contacts,
|
|
@@ -15328,7 +15676,9 @@ function SoftphoneWidget(props) {
|
|
|
15328
15676
|
useCallAudio(sounds);
|
|
15329
15677
|
const blocked = requireMic && mic.state !== "ready";
|
|
15330
15678
|
const fatal = useLastError();
|
|
15679
|
+
const screenIsFree = !call && !blocked && !fatal && !isFollower;
|
|
15331
15680
|
const { outcome, clear: clearOutcome } = useCallOutcome();
|
|
15681
|
+
const { notices: transferNotices, dismiss: dismissTransferNotice } = useTransferNotices();
|
|
15332
15682
|
const retry = () => {
|
|
15333
15683
|
void (async () => {
|
|
15334
15684
|
try {
|
|
@@ -15352,8 +15702,9 @@ function SoftphoneWidget(props) {
|
|
|
15352
15702
|
className: className ? `cv-softphone ${className}` : "cv-softphone",
|
|
15353
15703
|
...theme === "system" ? {} : { "data-cv-theme": theme },
|
|
15354
15704
|
children: [
|
|
15705
|
+
screenIsFree ? /* @__PURE__ */ jsxRuntime.jsx(TransferNotices, { notices: transferNotices, dismiss: dismissTransferNotice }) : null,
|
|
15355
15706
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-body", children: isFollower ? /* @__PURE__ */ jsxRuntime.jsx(LeaderGate, {}) : blocked ? /* @__PURE__ */ jsxRuntime.jsx(MicPrompt, { state: mic.state, onRequest: () => void mic.request() }) : call ? /* @__PURE__ */ jsxRuntime.jsx(CallScreen, { call, ...incomingCallAction ? { incomingCallAction } : {} }) : fatal ? /* @__PURE__ */ jsxRuntime.jsx(ErrorGate, { error: fatal, onRetry: retry }) : tab === "contacts" && contactsSource ? /* @__PURE__ */ jsxRuntime.jsx(ContactsTab, { source: contactsSource, ...contactAction ? { contactAction } : {} }) : tab === "history" && historySource ? /* @__PURE__ */ jsxRuntime.jsx(HistoryTab, { source: historySource }) : tab === "settings" ? /* @__PURE__ */ jsxRuntime.jsx(SettingsTab, {}) : /* @__PURE__ */ jsxRuntime.jsx(Dialpad, { outcome, onIntent: clearOutcome }) }),
|
|
15356
|
-
|
|
15707
|
+
screenIsFree ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-tabs", role: "tablist", children: tabs.map(({ id, icon: Icon3, label }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
15357
15708
|
"button",
|
|
15358
15709
|
{
|
|
15359
15710
|
role: "tab",
|
|
@@ -15366,7 +15717,7 @@ function SoftphoneWidget(props) {
|
|
|
15366
15717
|
]
|
|
15367
15718
|
},
|
|
15368
15719
|
id
|
|
15369
|
-
)) })
|
|
15720
|
+
)) }) : null
|
|
15370
15721
|
]
|
|
15371
15722
|
}
|
|
15372
15723
|
);
|