@coventia/softphone 0.1.0 → 0.2.1
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-VKq1I8yP.d.cts} +158 -5
- package/dist/{call-BTYlT2SU.d.ts → call-VKq1I8yP.d.ts} +158 -5
- package/dist/{chunk-N2N2EQQJ.js → chunk-7R6LOOAI.js} +4 -2
- package/dist/chunk-7R6LOOAI.js.map +1 -0
- package/dist/{chunk-5BGEHMPP.js → chunk-EYEDPD7Y.js} +146 -20
- package/dist/chunk-EYEDPD7Y.js.map +1 -0
- package/dist/{chunk-3V2L6UTK.js → chunk-LMJXX4YM.js} +17 -3
- package/dist/chunk-LMJXX4YM.js.map +1 -0
- package/dist/element/index.d.ts +162 -7
- package/dist/element/index.js +866 -421
- package/dist/element/index.js.map +1 -1
- package/dist/{engine-D9nbeY08.d.cts → engine-BlQBo2Ys.d.cts} +1 -1
- package/dist/{engine-Doa6Brkm.d.ts → engine-C0BOkIcW.d.ts} +1 -1
- package/dist/index.cjs +586 -28
- 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 +335 -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-Cyq3hiIM.d.ts} +17 -2
- package/dist/{softphone-DHmhotbf.d.ts → softphone-DlGV12Xp.d.cts} +17 -2
- package/dist/softphone.css +477 -35
- package/dist/{sources-N4BOEDRL.js → sources-IRKNQPWW.js} +108 -5
- package/dist/sources-IRKNQPWW.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 +846 -410
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +17 -6
- package/dist/ui/index.d.ts +17 -6
- package/dist/ui/index.js +878 -456
- package/dist/ui/index.js.map +1 -1
- package/dist/zadarma/index.cjs +144 -18
- 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-L5DEFXUS.js +5 -0
- package/dist/{zadarma-engine-DN2SZSQ3.js.map → zadarma-engine-L5DEFXUS.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 */,
|
|
@@ -13707,21 +13721,263 @@ function caretAfterDigits(formatted, digitsBefore) {
|
|
|
13707
13721
|
function digitsBeforeCaret(text, caret) {
|
|
13708
13722
|
return (text.slice(0, caret).match(/\d/g) ?? []).length;
|
|
13709
13723
|
}
|
|
13710
|
-
|
|
13711
|
-
|
|
13712
|
-
|
|
13713
|
-
["
|
|
13714
|
-
["
|
|
13715
|
-
["
|
|
13716
|
-
["
|
|
13717
|
-
["
|
|
13718
|
-
["
|
|
13719
|
-
["
|
|
13720
|
-
["
|
|
13721
|
-
["
|
|
13722
|
-
["
|
|
13724
|
+
|
|
13725
|
+
// src/ui/dialing/keys.ts
|
|
13726
|
+
var KEYPAD_KEYS = [
|
|
13727
|
+
["1", ""],
|
|
13728
|
+
["2", "ABC"],
|
|
13729
|
+
["3", "DEF"],
|
|
13730
|
+
["4", "GHI"],
|
|
13731
|
+
["5", "JKL"],
|
|
13732
|
+
["6", "MNO"],
|
|
13733
|
+
["7", "PQRS"],
|
|
13734
|
+
["8", "TUV"],
|
|
13735
|
+
["9", "WXYZ"],
|
|
13736
|
+
["*", ""],
|
|
13737
|
+
["0", "+"],
|
|
13738
|
+
["#", ""]
|
|
13723
13739
|
];
|
|
13724
|
-
|
|
13740
|
+
|
|
13741
|
+
// src/ui/audio/audio.ts
|
|
13742
|
+
var TONE_HZ = 425;
|
|
13743
|
+
function createRemoteAudio() {
|
|
13744
|
+
const el = document.createElement("audio");
|
|
13745
|
+
el.autoplay = true;
|
|
13746
|
+
el.style.display = "none";
|
|
13747
|
+
document.body.appendChild(el);
|
|
13748
|
+
return {
|
|
13749
|
+
attach(stream) {
|
|
13750
|
+
el.srcObject = stream;
|
|
13751
|
+
void el.play().catch(() => {
|
|
13752
|
+
});
|
|
13753
|
+
},
|
|
13754
|
+
detach() {
|
|
13755
|
+
el.srcObject = null;
|
|
13756
|
+
},
|
|
13757
|
+
destroy() {
|
|
13758
|
+
el.srcObject = null;
|
|
13759
|
+
el.remove();
|
|
13760
|
+
},
|
|
13761
|
+
async setOutputDevice(deviceId) {
|
|
13762
|
+
if (typeof el.setSinkId !== "function") return;
|
|
13763
|
+
try {
|
|
13764
|
+
await el.setSinkId(deviceId ?? "");
|
|
13765
|
+
} catch {
|
|
13766
|
+
}
|
|
13767
|
+
}
|
|
13768
|
+
};
|
|
13769
|
+
}
|
|
13770
|
+
var shared = null;
|
|
13771
|
+
function sharedContext() {
|
|
13772
|
+
if (shared) return shared;
|
|
13773
|
+
const Ctor = typeof AudioContext !== "undefined" ? AudioContext : globalThis.webkitAudioContext;
|
|
13774
|
+
if (!Ctor) return null;
|
|
13775
|
+
shared = new Ctor();
|
|
13776
|
+
return shared;
|
|
13777
|
+
}
|
|
13778
|
+
function primeAudio() {
|
|
13779
|
+
const ctx = sharedContext();
|
|
13780
|
+
if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
|
|
13781
|
+
});
|
|
13782
|
+
}
|
|
13783
|
+
function playRingtone() {
|
|
13784
|
+
const ctx = sharedContext();
|
|
13785
|
+
if (!ctx) return { stop() {
|
|
13786
|
+
} };
|
|
13787
|
+
const gain = ctx.createGain();
|
|
13788
|
+
gain.gain.value = 0;
|
|
13789
|
+
gain.connect(ctx.destination);
|
|
13790
|
+
const osc = ctx.createOscillator();
|
|
13791
|
+
osc.type = "sine";
|
|
13792
|
+
osc.frequency.value = TONE_HZ;
|
|
13793
|
+
osc.connect(gain);
|
|
13794
|
+
osc.start();
|
|
13795
|
+
const LEVEL = 0.8;
|
|
13796
|
+
const RAMP = 0.02;
|
|
13797
|
+
const burst = (at, dur) => {
|
|
13798
|
+
gain.gain.setValueAtTime(0, at);
|
|
13799
|
+
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
13800
|
+
gain.gain.setValueAtTime(LEVEL, at + dur - RAMP);
|
|
13801
|
+
gain.gain.linearRampToValueAtTime(0, at + dur);
|
|
13802
|
+
};
|
|
13803
|
+
const scheduleCycle = (at) => {
|
|
13804
|
+
burst(at, 0.4);
|
|
13805
|
+
burst(at + 0.6, 0.4);
|
|
13806
|
+
return 3;
|
|
13807
|
+
};
|
|
13808
|
+
const LOOKAHEAD = 0.5;
|
|
13809
|
+
const TICK_MS = 150;
|
|
13810
|
+
const LEAD = 0.08;
|
|
13811
|
+
let stopped = false;
|
|
13812
|
+
let timer = null;
|
|
13813
|
+
let nextAt = 0;
|
|
13814
|
+
const pump = () => {
|
|
13815
|
+
while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleCycle(nextAt);
|
|
13816
|
+
};
|
|
13817
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
13818
|
+
}).finally(() => {
|
|
13819
|
+
if (stopped) return;
|
|
13820
|
+
nextAt = ctx.currentTime + LEAD;
|
|
13821
|
+
pump();
|
|
13822
|
+
timer = setInterval(pump, TICK_MS);
|
|
13823
|
+
});
|
|
13824
|
+
return {
|
|
13825
|
+
stop() {
|
|
13826
|
+
stopped = true;
|
|
13827
|
+
if (timer) clearInterval(timer);
|
|
13828
|
+
try {
|
|
13829
|
+
osc.stop();
|
|
13830
|
+
} catch {
|
|
13831
|
+
}
|
|
13832
|
+
try {
|
|
13833
|
+
gain.disconnect();
|
|
13834
|
+
} catch {
|
|
13835
|
+
}
|
|
13836
|
+
}
|
|
13837
|
+
};
|
|
13838
|
+
}
|
|
13839
|
+
function playCallEndedTone() {
|
|
13840
|
+
const ctx = sharedContext();
|
|
13841
|
+
if (!ctx) return;
|
|
13842
|
+
const gain = ctx.createGain();
|
|
13843
|
+
gain.gain.value = 0;
|
|
13844
|
+
gain.connect(ctx.destination);
|
|
13845
|
+
const osc = ctx.createOscillator();
|
|
13846
|
+
osc.type = "sine";
|
|
13847
|
+
osc.frequency.value = TONE_HZ;
|
|
13848
|
+
osc.connect(gain);
|
|
13849
|
+
osc.start();
|
|
13850
|
+
const LEVEL = 0.55;
|
|
13851
|
+
const RAMP = 8e-3;
|
|
13852
|
+
const BEEP = 0.25;
|
|
13853
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
13854
|
+
}).finally(() => {
|
|
13855
|
+
const at = ctx.currentTime + 0.02;
|
|
13856
|
+
gain.gain.setValueAtTime(0, at);
|
|
13857
|
+
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
13858
|
+
gain.gain.setValueAtTime(LEVEL, at + BEEP - RAMP);
|
|
13859
|
+
gain.gain.linearRampToValueAtTime(0, at + BEEP);
|
|
13860
|
+
const totalMs = (at + BEEP - ctx.currentTime) * 1e3;
|
|
13861
|
+
setTimeout(() => {
|
|
13862
|
+
try {
|
|
13863
|
+
osc.stop();
|
|
13864
|
+
} catch {
|
|
13865
|
+
}
|
|
13866
|
+
try {
|
|
13867
|
+
gain.disconnect();
|
|
13868
|
+
} catch {
|
|
13869
|
+
}
|
|
13870
|
+
}, totalMs);
|
|
13871
|
+
});
|
|
13872
|
+
}
|
|
13873
|
+
var DTMF_ROWS_HZ = [697, 770, 852, 941];
|
|
13874
|
+
var DTMF_COLS_HZ = [1209, 1336, 1477];
|
|
13875
|
+
var DTMF_LAYOUT = ["123", "456", "789", "*0#"];
|
|
13876
|
+
function dtmfFrequencies(key) {
|
|
13877
|
+
if (key.length !== 1) return null;
|
|
13878
|
+
for (let row = 0; row < DTMF_LAYOUT.length; row++) {
|
|
13879
|
+
const col = DTMF_LAYOUT[row].indexOf(key);
|
|
13880
|
+
if (col !== -1) return [DTMF_ROWS_HZ[row], DTMF_COLS_HZ[col]];
|
|
13881
|
+
}
|
|
13882
|
+
return null;
|
|
13883
|
+
}
|
|
13884
|
+
function playKeyTone(key) {
|
|
13885
|
+
const hz = dtmfFrequencies(key);
|
|
13886
|
+
if (!hz) return;
|
|
13887
|
+
const ctx = sharedContext();
|
|
13888
|
+
if (!ctx) return;
|
|
13889
|
+
const LEVEL = 0.2;
|
|
13890
|
+
const RAMP = 6e-3;
|
|
13891
|
+
const DURATION = 0.12;
|
|
13892
|
+
const LEAD = 0.01;
|
|
13893
|
+
const gain = ctx.createGain();
|
|
13894
|
+
gain.gain.value = 0;
|
|
13895
|
+
gain.connect(ctx.destination);
|
|
13896
|
+
const oscillators = hz.map((freq) => {
|
|
13897
|
+
const osc = ctx.createOscillator();
|
|
13898
|
+
osc.type = "sine";
|
|
13899
|
+
osc.frequency.value = freq;
|
|
13900
|
+
osc.connect(gain);
|
|
13901
|
+
osc.start();
|
|
13902
|
+
return osc;
|
|
13903
|
+
});
|
|
13904
|
+
const fire = () => {
|
|
13905
|
+
const at = ctx.currentTime + LEAD;
|
|
13906
|
+
gain.gain.setValueAtTime(0, at);
|
|
13907
|
+
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
13908
|
+
gain.gain.setValueAtTime(LEVEL, at + DURATION - RAMP);
|
|
13909
|
+
gain.gain.linearRampToValueAtTime(0, at + DURATION);
|
|
13910
|
+
for (const osc of oscillators) osc.stop(at + DURATION);
|
|
13911
|
+
oscillators[0].onended = () => gain.disconnect();
|
|
13912
|
+
};
|
|
13913
|
+
if (ctx.state === "running") fire();
|
|
13914
|
+
else void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
13915
|
+
}).finally(fire);
|
|
13916
|
+
}
|
|
13917
|
+
var ringbackUrl = null;
|
|
13918
|
+
function ringbackSource() {
|
|
13919
|
+
if (ringbackUrl) return ringbackUrl;
|
|
13920
|
+
const sampleRate = 48e3;
|
|
13921
|
+
const channels = 2;
|
|
13922
|
+
const bytesPerSample = 2;
|
|
13923
|
+
const amp = 0.5;
|
|
13924
|
+
const toneSec = 1.5;
|
|
13925
|
+
const silenceSec = 3;
|
|
13926
|
+
const totalFrames = Math.round((toneSec + silenceSec) * sampleRate);
|
|
13927
|
+
const toneFrames = Math.round(toneSec * sampleRate);
|
|
13928
|
+
const ramp = Math.round(0.02 * sampleRate);
|
|
13929
|
+
const blockAlign = channels * bytesPerSample;
|
|
13930
|
+
const dataSize = totalFrames * blockAlign;
|
|
13931
|
+
const buffer = new ArrayBuffer(44 + dataSize);
|
|
13932
|
+
const view = new DataView(buffer);
|
|
13933
|
+
const str = (off, s) => {
|
|
13934
|
+
for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
|
|
13935
|
+
};
|
|
13936
|
+
str(0, "RIFF");
|
|
13937
|
+
view.setUint32(4, 36 + dataSize, true);
|
|
13938
|
+
str(8, "WAVE");
|
|
13939
|
+
str(12, "fmt ");
|
|
13940
|
+
view.setUint32(16, 16, true);
|
|
13941
|
+
view.setUint16(20, 1, true);
|
|
13942
|
+
view.setUint16(22, channels, true);
|
|
13943
|
+
view.setUint32(24, sampleRate, true);
|
|
13944
|
+
view.setUint32(28, sampleRate * blockAlign, true);
|
|
13945
|
+
view.setUint16(32, blockAlign, true);
|
|
13946
|
+
view.setUint16(34, bytesPerSample * 8, true);
|
|
13947
|
+
str(36, "data");
|
|
13948
|
+
view.setUint32(40, dataSize, true);
|
|
13949
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
13950
|
+
let sample = 0;
|
|
13951
|
+
if (i < toneFrames) {
|
|
13952
|
+
let env = 1;
|
|
13953
|
+
if (i < ramp) env = i / ramp;
|
|
13954
|
+
else if (i > toneFrames - ramp) env = Math.max(0, (toneFrames - i) / ramp);
|
|
13955
|
+
sample = Math.sin(2 * Math.PI * TONE_HZ * (i / sampleRate)) * amp * env;
|
|
13956
|
+
}
|
|
13957
|
+
const s16 = Math.round(sample * 32767);
|
|
13958
|
+
const off = 44 + i * blockAlign;
|
|
13959
|
+
for (let c = 0; c < channels; c++) view.setInt16(off + c * bytesPerSample, s16, true);
|
|
13960
|
+
}
|
|
13961
|
+
ringbackUrl = URL.createObjectURL(new Blob([buffer], { type: "audio/wav" }));
|
|
13962
|
+
return ringbackUrl;
|
|
13963
|
+
}
|
|
13964
|
+
function playRingback() {
|
|
13965
|
+
const el = document.createElement("audio");
|
|
13966
|
+
el.src = ringbackSource();
|
|
13967
|
+
el.loop = true;
|
|
13968
|
+
el.style.display = "none";
|
|
13969
|
+
document.body.appendChild(el);
|
|
13970
|
+
void el.play().catch(() => {
|
|
13971
|
+
});
|
|
13972
|
+
return {
|
|
13973
|
+
stop() {
|
|
13974
|
+
el.pause();
|
|
13975
|
+
el.remove();
|
|
13976
|
+
}
|
|
13977
|
+
};
|
|
13978
|
+
}
|
|
13979
|
+
var NOT_DIALABLE = /* @__PURE__ */ new Set(["*", "#"]);
|
|
13980
|
+
function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
|
|
13725
13981
|
const softphone = useSoftphone();
|
|
13726
13982
|
const state = useSoftphoneState();
|
|
13727
13983
|
const [value, setValue] = React.useState("");
|
|
@@ -13742,6 +13998,7 @@ function Dialpad({ outcome = null, onIntent } = {}) {
|
|
|
13742
13998
|
const append = (key) => {
|
|
13743
13999
|
onIntent?.();
|
|
13744
14000
|
setValue((v) => v + key);
|
|
14001
|
+
if (keyTones) playKeyTone(key);
|
|
13745
14002
|
};
|
|
13746
14003
|
const backspace = () => {
|
|
13747
14004
|
onIntent?.();
|
|
@@ -13786,22 +14043,25 @@ function Dialpad({ outcome = null, onIntent } = {}) {
|
|
|
13786
14043
|
)
|
|
13787
14044
|
] }),
|
|
13788
14045
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-dial-outcome", role: "status", part: "error", "data-shown": outcome !== null, children: outcome?.text ?? "" }),
|
|
13789
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-keypad", children:
|
|
13790
|
-
|
|
13791
|
-
|
|
13792
|
-
|
|
13793
|
-
|
|
13794
|
-
|
|
13795
|
-
|
|
13796
|
-
|
|
13797
|
-
|
|
13798
|
-
|
|
13799
|
-
|
|
13800
|
-
|
|
13801
|
-
|
|
13802
|
-
|
|
13803
|
-
|
|
13804
|
-
|
|
14046
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-keypad", children: KEYPAD_KEYS.map(([key, letters]) => {
|
|
14047
|
+
const dialable = !NOT_DIALABLE.has(key);
|
|
14048
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
14049
|
+
"button",
|
|
14050
|
+
{
|
|
14051
|
+
type: "button",
|
|
14052
|
+
className: "cv-key",
|
|
14053
|
+
"data-dialable": dialable,
|
|
14054
|
+
onClick: () => dialable && append(key),
|
|
14055
|
+
"aria-disabled": !dialable,
|
|
14056
|
+
title: dialable ? void 0 : "Solo disponible durante una llamada",
|
|
14057
|
+
children: [
|
|
14058
|
+
/* @__PURE__ */ jsxRuntime.jsx("b", { children: key }),
|
|
14059
|
+
letters ? /* @__PURE__ */ jsxRuntime.jsx("small", { children: letters }) : null
|
|
14060
|
+
]
|
|
14061
|
+
},
|
|
14062
|
+
key
|
|
14063
|
+
);
|
|
14064
|
+
}) }),
|
|
13805
14065
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-dial-foot", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
13806
14066
|
"button",
|
|
13807
14067
|
{
|
|
@@ -13836,31 +14096,150 @@ function elapsedSeconds(since, now) {
|
|
|
13836
14096
|
if (!since) return 0;
|
|
13837
14097
|
return Math.floor((now - since.getTime()) / 1e3);
|
|
13838
14098
|
}
|
|
13839
|
-
function
|
|
13840
|
-
const
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
|
|
13844
|
-
|
|
13845
|
-
|
|
13846
|
-
}, [enabled, intervalMs]);
|
|
13847
|
-
return now;
|
|
13848
|
-
}
|
|
13849
|
-
function deviceLabel(device, index, kind) {
|
|
13850
|
-
if (device.label) return device.label;
|
|
13851
|
-
return kind === "input" ? `Micr\xF3fono ${index + 1}` : `Altavoz ${index + 1}`;
|
|
14099
|
+
function formatAge(totalSeconds) {
|
|
14100
|
+
const s = Math.max(0, Math.floor(totalSeconds));
|
|
14101
|
+
if (s < 60) return "justo ahora";
|
|
14102
|
+
const mm = Math.floor(s / 60);
|
|
14103
|
+
if (mm < 60) return `hace ${mm} min`;
|
|
14104
|
+
const hh = Math.floor(mm / 60);
|
|
14105
|
+
return `hace ${hh} h`;
|
|
13852
14106
|
}
|
|
13853
|
-
|
|
13854
|
-
|
|
13855
|
-
|
|
13856
|
-
|
|
13857
|
-
|
|
13858
|
-
|
|
13859
|
-
|
|
13860
|
-
|
|
13861
|
-
|
|
13862
|
-
}
|
|
13863
|
-
|
|
14107
|
+
|
|
14108
|
+
// src/ui/lib/error-text.ts
|
|
14109
|
+
var ERROR_TEXT = {
|
|
14110
|
+
// ── Engine lifecycle ────────────────────────────────────────────────────────
|
|
14111
|
+
// Integrator mistakes rather than user-facing failures, but they must still read as something
|
|
14112
|
+
// rather than as a blank panel if one ever reaches the UI.
|
|
14113
|
+
"engine/duplicate-instance": {
|
|
14114
|
+
title: "Tel\xE9fono duplicado",
|
|
14115
|
+
body: "Hay otro tel\xE9fono abierto en esta p\xE1gina. Cierra uno y vuelve a cargar."
|
|
14116
|
+
},
|
|
14117
|
+
"engine/not-started": { title: "El tel\xE9fono a\xFAn no est\xE1 listo" },
|
|
14118
|
+
"engine/destroyed": {
|
|
14119
|
+
title: "Tel\xE9fono cerrado",
|
|
14120
|
+
body: "Vuelve a cargar la p\xE1gina para usarlo de nuevo."
|
|
14121
|
+
},
|
|
14122
|
+
// ── Auth ────────────────────────────────────────────────────────────────────
|
|
14123
|
+
// Never "token": the user has no idea what one is. What they can act on is the session.
|
|
14124
|
+
"auth/token-fetch-failed": {
|
|
14125
|
+
title: "Sin conexi\xF3n con el servidor",
|
|
14126
|
+
body: "No se ha podido activar el tel\xE9fono. Reint\xE9ntalo en unos segundos."
|
|
14127
|
+
},
|
|
14128
|
+
"auth/token-rejected": {
|
|
14129
|
+
title: "Sesi\xF3n caducada",
|
|
14130
|
+
body: "Vuelve a iniciar sesi\xF3n para poder llamar."
|
|
14131
|
+
},
|
|
14132
|
+
// ── Media ───────────────────────────────────────────────────────────────────
|
|
14133
|
+
// Verbatim from MicPermissionGate, which already says these well and says them first.
|
|
14134
|
+
//
|
|
14135
|
+
// NEVER ACTUALLY SHOWN via this table — these four codes (through `media/playback-blocked`, below)
|
|
14136
|
+
// are never raised as a `SoftphoneError` at all. See the fuller comment on each in `core/errors.ts`
|
|
14137
|
+
// for why. Copy kept here anyway so the catalogue and this file stay the same shape as every other
|
|
14138
|
+
// code — `error-text.test.ts` would fail the build the moment they drifted.
|
|
14139
|
+
"media/permission-denied": {
|
|
14140
|
+
title: "Micr\xF3fono bloqueado",
|
|
14141
|
+
body: "Perm\xEDtelo en tu navegador para poder llamar."
|
|
14142
|
+
},
|
|
14143
|
+
"media/no-input-device": {
|
|
14144
|
+
title: "Sin micr\xF3fono",
|
|
14145
|
+
body: "Conecta un micr\xF3fono y vuelve a intentarlo."
|
|
14146
|
+
},
|
|
14147
|
+
"media/webrtc-unsupported": {
|
|
14148
|
+
title: "Llamadas no disponibles",
|
|
14149
|
+
body: "Este navegador no admite llamadas. Prueba con Chrome, Edge o Firefox."
|
|
14150
|
+
},
|
|
14151
|
+
"media/playback-blocked": { title: "Toca la pantalla para activar el sonido" },
|
|
14152
|
+
// ── Network / signalling ────────────────────────────────────────────────────
|
|
14153
|
+
// "Centralita" rather than "SIP" or "registro": the user knows what a phone system is.
|
|
14154
|
+
"net/registration-failed": {
|
|
14155
|
+
title: "No se pudo conectar",
|
|
14156
|
+
body: "No hemos podido conectar con la centralita. Reintenta en unos segundos."
|
|
14157
|
+
},
|
|
14158
|
+
"net/transport-failed": {
|
|
14159
|
+
title: "Conexi\xF3n perdida",
|
|
14160
|
+
body: "Se ha perdido la conexi\xF3n con la centralita."
|
|
14161
|
+
},
|
|
14162
|
+
// NEVER ACTUALLY SHOWN either — CallScreen's own "Reconectando…" already covers this, off the
|
|
14163
|
+
// call's `reconnecting` state rather than this channel. See the comment in `core/errors.ts`.
|
|
14164
|
+
"net/ice-failed": { title: "Recuperando el audio\u2026" },
|
|
14165
|
+
// ── Call ────────────────────────────────────────────────────────────────────
|
|
14166
|
+
/*
|
|
14167
|
+
THE VOICE RULE FOR THIS BLOCK: say what HAPPENED, not what it is called.
|
|
14168
|
+
|
|
14169
|
+
These were status labels — "Llamada perdida", "Llamada rechazada", "Sin respuesta". Each is a
|
|
14170
|
+
name for a category rather than an account of an event, which is why they read as a database enum
|
|
14171
|
+
someone forgot to translate into human. Worse, a label hides the ACTOR: "Llamada rechazada" never
|
|
14172
|
+
says who rejected what, so the user reading it after declining a call and the user reading it
|
|
14173
|
+
after being declined see the same three words.
|
|
14174
|
+
|
|
14175
|
+
So each one is a short sentence about what the other person, or the phone, did — the form
|
|
14176
|
+
`call/busy` already had, and the reason it was the only one here that read clearly.
|
|
14177
|
+
|
|
14178
|
+
Impersonal third-person plural ("contestaron", "colgaron") throughout: the user does not know who
|
|
14179
|
+
is at the other end, and it is what Spanish uses when the subject is unknown. Mixing it with the
|
|
14180
|
+
singular across neighbouring lines reads as carelessness.
|
|
14181
|
+
|
|
14182
|
+
LENGTH IS A HARD CONSTRAINT, not a preference. `.cv-dial-outcome` is one line at a fixed
|
|
14183
|
+
`1.15rem`, and the keypad below it is `flex: 1` — a title that wraps resizes every key and shifts
|
|
14184
|
+
the grid under a finger already reaching for it. `error-text.test.ts` caps this at 48 characters.
|
|
14185
|
+
*/
|
|
14186
|
+
"call/not-answerable": { title: "Esa llamada ya ha terminado" },
|
|
14187
|
+
/*
|
|
14188
|
+
NOT "Comunicando", which is what this said and what sent us looking. SIP 486 is both "the line is
|
|
14189
|
+
engaged" AND what almost every endpoint sends when a person presses decline — including our own
|
|
14190
|
+
`reject()`. One code, two events, no way to tell them apart, so the copy has to hold both rather
|
|
14191
|
+
than assert the less likely one. `call/rejected` can be definite: an endpoint sending 603 Decline
|
|
14192
|
+
means exactly that.
|
|
14193
|
+
*/
|
|
14194
|
+
"call/busy": { title: "No contestaron o la l\xEDnea estaba ocupada" },
|
|
14195
|
+
"call/no-answer": { title: "No contestaron" },
|
|
14196
|
+
"call/rejected": { title: "No aceptaron tu llamada" },
|
|
14197
|
+
/*
|
|
14198
|
+
The legacy CRM said "La persona que llamaba colgó antes de que pudieras contestar." — the right
|
|
14199
|
+
fact, at three times the width of the line that has to hold it. This keeps the fact and drops the
|
|
14200
|
+
words: the user needs to know the call was FOR them and that nobody is waiting on the line.
|
|
14201
|
+
"Llamada perdida" alone tested badly for exactly the label reason above — it names the category
|
|
14202
|
+
without saying anyone rang.
|
|
14203
|
+
*/
|
|
14204
|
+
"call/missed": { title: "Colgaron antes de que pudieras contestar" },
|
|
14205
|
+
// "No se pudo completar la llamada" was our own "something went wrong": true of every failure and
|
|
14206
|
+
// useful for none. The number is what the user can actually act on.
|
|
14207
|
+
"call/failed": { title: "No se pudo conectar con ese n\xFAmero" },
|
|
14208
|
+
// The load-bearing half is the SECOND sentence: the original call is still up, and a user who
|
|
14209
|
+
// thinks they dropped it will hang up on a client to "fix" it.
|
|
14210
|
+
"call/transfer-failed": { title: "No se pudo transferir. Sigues en la llamada" },
|
|
14211
|
+
"call/unsupported": { title: "Esta funci\xF3n no est\xE1 disponible" },
|
|
14212
|
+
// ── Provider ────────────────────────────────────────────────────────────────
|
|
14213
|
+
"provider/provision-failed": {
|
|
14214
|
+
title: "No se pudo activar el tel\xE9fono",
|
|
14215
|
+
body: "Si el problema contin\xFAa, contacta con soporte."
|
|
14216
|
+
}
|
|
14217
|
+
};
|
|
14218
|
+
function useTicker(enabled, intervalMs = 1e3) {
|
|
14219
|
+
const [now, setNow] = React.useState(() => Date.now());
|
|
14220
|
+
React.useEffect(() => {
|
|
14221
|
+
if (!enabled) return;
|
|
14222
|
+
setNow(Date.now());
|
|
14223
|
+
const id = setInterval(() => setNow(Date.now()), intervalMs);
|
|
14224
|
+
return () => clearInterval(id);
|
|
14225
|
+
}, [enabled, intervalMs]);
|
|
14226
|
+
return now;
|
|
14227
|
+
}
|
|
14228
|
+
function deviceLabel(device, index, kind) {
|
|
14229
|
+
if (device.label) return device.label;
|
|
14230
|
+
return kind === "input" ? `Micr\xF3fono ${index + 1}` : `Altavoz ${index + 1}`;
|
|
14231
|
+
}
|
|
14232
|
+
var DEFAULT_OPTION = "";
|
|
14233
|
+
function DevicePicker({
|
|
14234
|
+
icon: Icon3,
|
|
14235
|
+
label,
|
|
14236
|
+
kind,
|
|
14237
|
+
options,
|
|
14238
|
+
value,
|
|
14239
|
+
onChange,
|
|
14240
|
+
emptyText
|
|
14241
|
+
}) {
|
|
14242
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-settings-row", children: [
|
|
13864
14243
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-settings-row-icon", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx(Icon3, { size: 16 }) }),
|
|
13865
14244
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-settings-row-text", children: [
|
|
13866
14245
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-settings-row-label", id: `cv-settings-label-${kind}`, children: label }),
|
|
@@ -13922,7 +14301,9 @@ function Control({
|
|
|
13922
14301
|
label,
|
|
13923
14302
|
pressed = false,
|
|
13924
14303
|
tone,
|
|
13925
|
-
onClick
|
|
14304
|
+
onClick,
|
|
14305
|
+
disabled = false,
|
|
14306
|
+
title
|
|
13926
14307
|
}) {
|
|
13927
14308
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-ctrl-item", children: [
|
|
13928
14309
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -13934,6 +14315,8 @@ function Control({
|
|
|
13934
14315
|
"aria-pressed": pressed,
|
|
13935
14316
|
"aria-label": label,
|
|
13936
14317
|
onClick,
|
|
14318
|
+
disabled,
|
|
14319
|
+
title,
|
|
13937
14320
|
children: /* @__PURE__ */ jsxRuntime.jsx(Icon3, { size: 19 })
|
|
13938
14321
|
}
|
|
13939
14322
|
),
|
|
@@ -13948,34 +14331,227 @@ var STATUS_LABEL = {
|
|
|
13948
14331
|
reconnecting: "Reconectando\u2026",
|
|
13949
14332
|
held: "En espera"
|
|
13950
14333
|
};
|
|
14334
|
+
var CONSULT_FAIL_CODE = {
|
|
14335
|
+
busy: "call/busy",
|
|
14336
|
+
"no-answer": "call/no-answer",
|
|
14337
|
+
rejected: "call/rejected",
|
|
14338
|
+
failed: "call/failed"
|
|
14339
|
+
};
|
|
14340
|
+
function consultFailText(reason) {
|
|
14341
|
+
return ERROR_TEXT[CONSULT_FAIL_CODE[reason] ?? "call/transfer-failed"].title;
|
|
14342
|
+
}
|
|
14343
|
+
var MIN_CONSULT_HOLD_SECONDS = 10;
|
|
14344
|
+
function ConsultScreen({
|
|
14345
|
+
mainName,
|
|
14346
|
+
consultView,
|
|
14347
|
+
now,
|
|
14348
|
+
onReturn,
|
|
14349
|
+
onComplete,
|
|
14350
|
+
onHangupAll
|
|
14351
|
+
}) {
|
|
14352
|
+
const s = consultView.state;
|
|
14353
|
+
const answered = s === "active";
|
|
14354
|
+
const name = consultView.remote.displayName;
|
|
14355
|
+
const number = consultView.remote.number;
|
|
14356
|
+
const showNumber = number !== name;
|
|
14357
|
+
const heldSeconds = elapsedSeconds(consultView.answeredAt, now);
|
|
14358
|
+
const [rawM = "0", ss = "00"] = formatDuration(heldSeconds).split(":");
|
|
14359
|
+
const mm = rawM.padStart(2, "0");
|
|
14360
|
+
const canComplete = heldSeconds >= MIN_CONSULT_HOLD_SECONDS;
|
|
14361
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-view", children: [
|
|
14362
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-call", children: [
|
|
14363
|
+
/* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-consult-hold-banner", onClick: onReturn, children: [
|
|
14364
|
+
/* @__PURE__ */ jsxRuntime.jsx(Pause, { size: 13, "aria-hidden": true }),
|
|
14365
|
+
mainName,
|
|
14366
|
+
" en espera \xB7 Volver"
|
|
14367
|
+
] }),
|
|
14368
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-avatar-wrap", children: [
|
|
14369
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-avatar", "aria-hidden": true, children: initials(name) }),
|
|
14370
|
+
answered ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-avatar-dot", "aria-hidden": true }) : null
|
|
14371
|
+
] }),
|
|
14372
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call-name", children: name }),
|
|
14373
|
+
showNumber ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call-number", children: number }) : null,
|
|
14374
|
+
answered ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-timer", "aria-label": "Duraci\xF3n de la llamada", children: [
|
|
14375
|
+
/* @__PURE__ */ jsxRuntime.jsx("b", { children: mm }),
|
|
14376
|
+
/* @__PURE__ */ jsxRuntime.jsx("i", { children: ":" }),
|
|
14377
|
+
/* @__PURE__ */ jsxRuntime.jsx("b", { children: ss })
|
|
14378
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call-status", children: STATUS_LABEL[s] ?? "" })
|
|
14379
|
+
] }),
|
|
14380
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-controls", children: [
|
|
14381
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-transfer-actions", children: [
|
|
14382
|
+
answered ? /* @__PURE__ */ jsxRuntime.jsx("button", { className: "cv-btn cv-btn--accent", onClick: onComplete, disabled: !canComplete, children: "Completar" }) : null,
|
|
14383
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "cv-btn", onClick: onReturn, children: "Cancelar" })
|
|
14384
|
+
] }),
|
|
14385
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-controls-primary", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-ctrl-item", children: [
|
|
14386
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
14387
|
+
"button",
|
|
14388
|
+
{
|
|
14389
|
+
type: "button",
|
|
14390
|
+
className: "cv-round cv-round--hangup",
|
|
14391
|
+
onClick: onHangupAll,
|
|
14392
|
+
"aria-label": "Colgar todo",
|
|
14393
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(PhoneOff, { size: 26 })
|
|
14394
|
+
}
|
|
14395
|
+
),
|
|
14396
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Colgar todo" })
|
|
14397
|
+
] }) })
|
|
14398
|
+
] })
|
|
14399
|
+
] });
|
|
14400
|
+
}
|
|
14401
|
+
var PRESENCE_LABEL = {
|
|
14402
|
+
disponible: "Disponible",
|
|
14403
|
+
en_llamada: "En llamada",
|
|
14404
|
+
desconectado: "Desconectado"
|
|
14405
|
+
};
|
|
14406
|
+
var PRESENCE_RANK = {
|
|
14407
|
+
disponible: 0,
|
|
14408
|
+
en_llamada: 1,
|
|
14409
|
+
desconectado: 2
|
|
14410
|
+
};
|
|
14411
|
+
function filterAgents(agents, query) {
|
|
14412
|
+
const q = query.trim().toLowerCase();
|
|
14413
|
+
return agents.filter((a) => !q || (a.name ?? "").toLowerCase().includes(q) || a.extension.includes(q)).sort(
|
|
14414
|
+
(a, b) => PRESENCE_RANK[a.state] - PRESENCE_RANK[b.state] || (a.name ?? a.extension).localeCompare(b.name ?? b.extension)
|
|
14415
|
+
);
|
|
14416
|
+
}
|
|
14417
|
+
function truncateForButton(text, max = 16) {
|
|
14418
|
+
return text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
14419
|
+
}
|
|
14420
|
+
function TransferPicker({
|
|
14421
|
+
agents,
|
|
14422
|
+
myUserId,
|
|
14423
|
+
query,
|
|
14424
|
+
onSelect
|
|
14425
|
+
}) {
|
|
14426
|
+
const others = agents.filter((a) => a.userId !== myUserId);
|
|
14427
|
+
if (others.length === 0) return null;
|
|
14428
|
+
const filtered = filterAgents(others, query);
|
|
14429
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-presence", children: [
|
|
14430
|
+
/* @__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) => {
|
|
14431
|
+
const isBusy = agent.state === "en_llamada";
|
|
14432
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
14433
|
+
"button",
|
|
14434
|
+
{
|
|
14435
|
+
type: "button",
|
|
14436
|
+
role: "option",
|
|
14437
|
+
className: "cv-presence-item",
|
|
14438
|
+
disabled: isBusy,
|
|
14439
|
+
onClick: () => onSelect(agent.extension),
|
|
14440
|
+
children: [
|
|
14441
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-presence-dot", "data-state": agent.state, "aria-hidden": true }),
|
|
14442
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-presence-name", children: agent.name ?? agent.extension }),
|
|
14443
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-presence-meta", children: [
|
|
14444
|
+
agent.extension,
|
|
14445
|
+
" \xB7 ",
|
|
14446
|
+
PRESENCE_LABEL[agent.state]
|
|
14447
|
+
] })
|
|
14448
|
+
]
|
|
14449
|
+
},
|
|
14450
|
+
agent.userId
|
|
14451
|
+
);
|
|
14452
|
+
}) }),
|
|
14453
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-presence-legend", children: "Desconectado: sin conexi\xF3n en el softphone web. Puede atender desde otro dispositivo." })
|
|
14454
|
+
] });
|
|
14455
|
+
}
|
|
13951
14456
|
function CallScreen({
|
|
13952
14457
|
call,
|
|
13953
|
-
incomingCallAction
|
|
14458
|
+
incomingCallAction,
|
|
14459
|
+
keyTones = true
|
|
13954
14460
|
}) {
|
|
13955
14461
|
const view = useCall(call);
|
|
13956
|
-
const
|
|
14462
|
+
const softphone = useSoftphone();
|
|
14463
|
+
const [panel, setPanel] = React.useState(null);
|
|
14464
|
+
const showTransfer = panel === "transfer";
|
|
14465
|
+
const showKeypad = panel === "keypad";
|
|
14466
|
+
const showDevices = panel === "devices";
|
|
14467
|
+
const togglePanel = (p) => setPanel((cur) => cur === p ? null : p);
|
|
13957
14468
|
const [transferTo, setTransferTo] = React.useState("");
|
|
13958
|
-
const [
|
|
14469
|
+
const [consultCall, setConsultCall] = React.useState(null);
|
|
14470
|
+
const [consultOutcome, setConsultOutcome] = React.useState(null);
|
|
14471
|
+
const [myUserId, setMyUserId] = React.useState(null);
|
|
14472
|
+
const consultView = useCall(consultCall);
|
|
14473
|
+
const consultIntentRef = React.useRef(null);
|
|
13959
14474
|
const controlsRef = React.useRef(null);
|
|
13960
14475
|
const now = useTicker(view?.state === "active");
|
|
14476
|
+
const consultNow = useTicker(consultView?.state === "active");
|
|
13961
14477
|
const isConnected = view ? view.state === "active" || view.state === "held" || view.state === "reconnecting" : false;
|
|
14478
|
+
const transferPickerActive = isConnected && showTransfer && !consultCall;
|
|
14479
|
+
const presence = usePresence(transferPickerActive);
|
|
14480
|
+
React.useEffect(() => {
|
|
14481
|
+
const listAgents = softphone.sources?.listAgents;
|
|
14482
|
+
if (!listAgents) return;
|
|
14483
|
+
let cancelled = false;
|
|
14484
|
+
void listAgents().then((d) => !cancelled && setMyUserId(d.me?.userId ?? null)).catch(() => {
|
|
14485
|
+
});
|
|
14486
|
+
return () => {
|
|
14487
|
+
cancelled = true;
|
|
14488
|
+
};
|
|
14489
|
+
}, [softphone]);
|
|
14490
|
+
React.useEffect(() => {
|
|
14491
|
+
if (!consultCall) return;
|
|
14492
|
+
return consultCall.on("ended" /* Ended */, ({ reason }) => {
|
|
14493
|
+
const intent = consultIntentRef.current;
|
|
14494
|
+
consultIntentRef.current = null;
|
|
14495
|
+
setConsultCall(null);
|
|
14496
|
+
if (!intent) {
|
|
14497
|
+
setConsultOutcome(reason);
|
|
14498
|
+
if (call.state !== "ended") void call.unhold();
|
|
14499
|
+
}
|
|
14500
|
+
});
|
|
14501
|
+
}, [consultCall, call]);
|
|
14502
|
+
const startConsult = async (target) => {
|
|
14503
|
+
const t = (target ?? transferTo).trim();
|
|
14504
|
+
if (!t) return;
|
|
14505
|
+
setConsultOutcome(null);
|
|
14506
|
+
setConsultCall(await call.startConsult(t));
|
|
14507
|
+
};
|
|
14508
|
+
const selectPresenceAgent = (extension) => {
|
|
14509
|
+
setTransferTo(extension);
|
|
14510
|
+
void startConsult(extension);
|
|
14511
|
+
};
|
|
14512
|
+
const [quickTransfer, setQuickTransfer] = React.useState(false);
|
|
14513
|
+
const selectPresenceAgentAuto = (extension) => {
|
|
14514
|
+
void call.transfer(extension);
|
|
14515
|
+
};
|
|
14516
|
+
const completeTransfer = async () => {
|
|
14517
|
+
if (!consultCall) return;
|
|
14518
|
+
const target = transferTo.trim();
|
|
14519
|
+
consultIntentRef.current = "complete";
|
|
14520
|
+
await consultCall.hangup();
|
|
14521
|
+
await call.transfer(target);
|
|
14522
|
+
};
|
|
14523
|
+
const cancelConsult = async () => {
|
|
14524
|
+
consultIntentRef.current = "cancel";
|
|
14525
|
+
await call.cancelConsult();
|
|
14526
|
+
};
|
|
13962
14527
|
React.useEffect(() => {
|
|
13963
14528
|
if (!showDevices) return;
|
|
13964
14529
|
const onKey = (e) => {
|
|
13965
|
-
if (e.key === "Escape")
|
|
14530
|
+
if (e.key === "Escape") setPanel(null);
|
|
14531
|
+
};
|
|
14532
|
+
document.addEventListener("keydown", onKey);
|
|
14533
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
14534
|
+
}, [showDevices]);
|
|
14535
|
+
React.useEffect(() => {
|
|
14536
|
+
if (!showKeypad) return;
|
|
14537
|
+
const onKey = (e) => {
|
|
14538
|
+
if (e.key === "Escape") setPanel(null);
|
|
13966
14539
|
};
|
|
13967
14540
|
document.addEventListener("keydown", onKey);
|
|
13968
14541
|
if (!isConnected) return () => document.removeEventListener("keydown", onKey);
|
|
13969
14542
|
const root = controlsRef.current?.getRootNode() ?? document;
|
|
13970
14543
|
const onDown = (e) => {
|
|
13971
|
-
if (!controlsRef.current?.contains(e.target))
|
|
14544
|
+
if (!controlsRef.current?.contains(e.target)) setPanel(null);
|
|
13972
14545
|
};
|
|
13973
14546
|
root.addEventListener("pointerdown", onDown);
|
|
13974
14547
|
return () => {
|
|
13975
14548
|
document.removeEventListener("keydown", onKey);
|
|
13976
14549
|
root.removeEventListener("pointerdown", onDown);
|
|
13977
14550
|
};
|
|
13978
|
-
}, [
|
|
14551
|
+
}, [showKeypad, isConnected]);
|
|
14552
|
+
React.useEffect(() => {
|
|
14553
|
+
if (view && view.state !== "active") setPanel((cur) => cur === "keypad" ? null : cur);
|
|
14554
|
+
}, [view?.state]);
|
|
13979
14555
|
if (!view || view.state === "ended") return null;
|
|
13980
14556
|
const s = view.state;
|
|
13981
14557
|
const isRinging = s === "ringing";
|
|
@@ -13983,14 +14559,27 @@ function CallScreen({
|
|
|
13983
14559
|
const name = view.remote.displayName;
|
|
13984
14560
|
const number = view.remote.number;
|
|
13985
14561
|
const showNumber = number !== name;
|
|
14562
|
+
if (consultCall && consultView) {
|
|
14563
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
14564
|
+
ConsultScreen,
|
|
14565
|
+
{
|
|
14566
|
+
mainName: name,
|
|
14567
|
+
consultView,
|
|
14568
|
+
now: consultNow,
|
|
14569
|
+
onReturn: () => void cancelConsult(),
|
|
14570
|
+
onComplete: () => void completeTransfer(),
|
|
14571
|
+
onHangupAll: () => void call.hangup()
|
|
14572
|
+
}
|
|
14573
|
+
);
|
|
14574
|
+
}
|
|
13986
14575
|
const incomingAction = isRinging && view.remote.contact && incomingCallAction ? incomingCallAction : null;
|
|
13987
14576
|
const IncomingActionIcon = incomingAction?.icon ?? ExternalLink;
|
|
13988
14577
|
const [rawM = "0", ss = "00"] = formatDuration(elapsedSeconds(view.answeredAt, now)).split(":");
|
|
13989
14578
|
const mm = rawM.padStart(2, "0");
|
|
13990
14579
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-view", children: [
|
|
13991
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call", children:
|
|
14580
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call", children: showDevices ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-call-devices-inline", children: [
|
|
13992
14581
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-call-devices-header", children: [
|
|
13993
|
-
/* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () =>
|
|
14582
|
+
/* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setPanel(null), children: [
|
|
13994
14583
|
/* @__PURE__ */ jsxRuntime.jsx(ArrowLeft, { size: 13, "aria-hidden": true }),
|
|
13995
14584
|
"Volver"
|
|
13996
14585
|
] }),
|
|
@@ -13998,10 +14587,82 @@ function CallScreen({
|
|
|
13998
14587
|
] }),
|
|
13999
14588
|
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "cv-call-devices-context", children: [
|
|
14000
14589
|
showNumber ? `${number} \xB7 ` : "",
|
|
14001
|
-
STATUS_LABEL[s] ?? ""
|
|
14590
|
+
s === "active" ? `${mm}:${ss}` : STATUS_LABEL[s] ?? ""
|
|
14002
14591
|
] }),
|
|
14003
14592
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-settings-section-body", children: /* @__PURE__ */ jsxRuntime.jsx(DevicePickers, {}) })
|
|
14004
|
-
] }) :
|
|
14593
|
+
] }) : transferPickerActive ? (
|
|
14594
|
+
/*
|
|
14595
|
+
Found live (2026-09-29): the agent list a moment ago picked between showing ONE agent row
|
|
14596
|
+
or forcing a scroll — `.cv-presence-list`'s own `max-height` had already been trimmed twice
|
|
14597
|
+
(T7, then T3) chasing that, and both times the room it was chasing lived somewhere else.
|
|
14598
|
+
The full avatar/name/giant-timer block above stayed on screen, unconditionally, the WHOLE
|
|
14599
|
+
time the picker was open — exactly the anti-pattern `.cv-call-devices-inline` (above) was
|
|
14600
|
+
already built to avoid for the pre-connect device picker: "an inline swap, not an overlay,
|
|
14601
|
+
so it gets the full box, not a leftover sliver." The picker never got that same treatment.
|
|
14602
|
+
Measured live (a temporary harness, `playground/react/picker-harness.*`, real Chromium at
|
|
14603
|
+
this SDK's own real embed footprint, 310×520): the avatar+name+timer block alone cost
|
|
14604
|
+
~150px of `.cv-call`'s ~340px of usable height — more than the entire agent list, the mode
|
|
14605
|
+
toggle, and the free-text row combined. Reusing `.cv-call-devices-inline`'s OWN "who's
|
|
14606
|
+
calling doesn't disappear" compact context line (below) instead of the full block gives
|
|
14607
|
+
that ~150px back to the one thing this screen is actually for right now: the list.
|
|
14608
|
+
*/
|
|
14609
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-transfer", children: [
|
|
14610
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-transfer-context", children: [
|
|
14611
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-call-devices-title", children: name }),
|
|
14612
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "cv-call-devices-context", children: [
|
|
14613
|
+
showNumber ? `${number} \xB7 ` : "",
|
|
14614
|
+
s === "active" ? `${mm}:${ss}` : STATUS_LABEL[s] ?? ""
|
|
14615
|
+
] })
|
|
14616
|
+
] }),
|
|
14617
|
+
consultOutcome ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-dial-outcome", role: "status", "data-shown": "true", children: consultFailText(consultOutcome) }) : null,
|
|
14618
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-transfer-row", children: [
|
|
14619
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
14620
|
+
"input",
|
|
14621
|
+
{
|
|
14622
|
+
className: "cv-search",
|
|
14623
|
+
value: transferTo,
|
|
14624
|
+
onChange: (e) => setTransferTo(e.target.value),
|
|
14625
|
+
placeholder: "Nombre, ext. o n\xFAmero",
|
|
14626
|
+
"aria-label": "Transferir a",
|
|
14627
|
+
autoFocus: true
|
|
14628
|
+
}
|
|
14629
|
+
),
|
|
14630
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
14631
|
+
"button",
|
|
14632
|
+
{
|
|
14633
|
+
className: "cv-btn cv-btn--accent",
|
|
14634
|
+
disabled: !transferTo.trim(),
|
|
14635
|
+
onClick: () => void startConsult(),
|
|
14636
|
+
children: transferTo.trim() ? `Llamar a ${truncateForButton(transferTo.trim())}` : "Llamar"
|
|
14637
|
+
}
|
|
14638
|
+
)
|
|
14639
|
+
] }),
|
|
14640
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-segmented", role: "tablist", "aria-label": "Modo de transferencia", children: [
|
|
14641
|
+
{ key: "consult", label: "Hablar primero" },
|
|
14642
|
+
{ key: "auto", label: "Transferir directo" }
|
|
14643
|
+
].map(({ key, label }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
14644
|
+
"button",
|
|
14645
|
+
{
|
|
14646
|
+
type: "button",
|
|
14647
|
+
role: "tab",
|
|
14648
|
+
"aria-selected": quickTransfer === (key === "auto"),
|
|
14649
|
+
className: "cv-segmented-btn",
|
|
14650
|
+
onClick: () => setQuickTransfer(key === "auto"),
|
|
14651
|
+
children: label
|
|
14652
|
+
},
|
|
14653
|
+
key
|
|
14654
|
+
)) }),
|
|
14655
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
14656
|
+
TransferPicker,
|
|
14657
|
+
{
|
|
14658
|
+
agents: presence,
|
|
14659
|
+
myUserId,
|
|
14660
|
+
query: transferTo,
|
|
14661
|
+
onSelect: quickTransfer ? selectPresenceAgentAuto : selectPresenceAgent
|
|
14662
|
+
}
|
|
14663
|
+
)
|
|
14664
|
+
] })
|
|
14665
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
14005
14666
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-avatar-wrap", children: [
|
|
14006
14667
|
isRinging ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-avatar-ping", "aria-hidden": true }) : null,
|
|
14007
14668
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-avatar", "aria-hidden": true, children: initials(name) }),
|
|
@@ -14014,7 +14675,7 @@ function CallScreen({
|
|
|
14014
14675
|
/* @__PURE__ */ jsxRuntime.jsx("i", { children: ":" }),
|
|
14015
14676
|
/* @__PURE__ */ jsxRuntime.jsx("b", { children: ss })
|
|
14016
14677
|
] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call-status", children: s === "held" ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-badge-hold", children: STATUS_LABEL[s] }) : STATUS_LABEL[s] ?? "" }),
|
|
14017
|
-
|
|
14678
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-call-chips", children: [
|
|
14018
14679
|
incomingAction ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
14019
14680
|
"button",
|
|
14020
14681
|
{
|
|
@@ -14027,36 +14688,11 @@ function CallScreen({
|
|
|
14027
14688
|
]
|
|
14028
14689
|
}
|
|
14029
14690
|
) : null,
|
|
14030
|
-
/* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () =>
|
|
14691
|
+
/* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setPanel("devices"), children: [
|
|
14031
14692
|
/* @__PURE__ */ jsxRuntime.jsx(Headphones, { size: 13, "aria-hidden": true }),
|
|
14032
14693
|
"Dispositivos"
|
|
14033
14694
|
] })
|
|
14034
|
-
] })
|
|
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
|
-
] }) : null
|
|
14695
|
+
] })
|
|
14060
14696
|
] }) }),
|
|
14061
14697
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-controls", ref: controlsRef, children: isRinging ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-controls-primary", children: [
|
|
14062
14698
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-ctrl-item", children: [
|
|
@@ -14115,17 +14751,19 @@ function CallScreen({
|
|
|
14115
14751
|
label: "Transferir",
|
|
14116
14752
|
pressed: showTransfer,
|
|
14117
14753
|
tone: "accent",
|
|
14118
|
-
onClick: () =>
|
|
14754
|
+
onClick: () => togglePanel("transfer")
|
|
14119
14755
|
}
|
|
14120
14756
|
),
|
|
14121
14757
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
14122
14758
|
Control,
|
|
14123
14759
|
{
|
|
14124
|
-
icon:
|
|
14125
|
-
label: "
|
|
14126
|
-
pressed:
|
|
14760
|
+
icon: Grid3x3,
|
|
14761
|
+
label: "Teclado",
|
|
14762
|
+
pressed: showKeypad,
|
|
14127
14763
|
tone: "accent",
|
|
14128
|
-
|
|
14764
|
+
disabled: s !== "active",
|
|
14765
|
+
title: s === "held" ? "Reanuda la llamada para usar el teclado" : s === "reconnecting" ? "No disponible mientras se reconecta" : void 0,
|
|
14766
|
+
onClick: () => togglePanel("keypad")
|
|
14129
14767
|
}
|
|
14130
14768
|
)
|
|
14131
14769
|
] }) : null,
|
|
@@ -14143,7 +14781,22 @@ function CallScreen({
|
|
|
14143
14781
|
),
|
|
14144
14782
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Colgar" })
|
|
14145
14783
|
] }) }),
|
|
14146
|
-
inCall &&
|
|
14784
|
+
inCall && showKeypad ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-keypad-popover cv-keypad-popover--above", role: "dialog", "aria-label": "Teclado", children: KEYPAD_KEYS.map(([key, letters]) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
14785
|
+
"button",
|
|
14786
|
+
{
|
|
14787
|
+
type: "button",
|
|
14788
|
+
className: "cv-key",
|
|
14789
|
+
onClick: () => {
|
|
14790
|
+
call.sendDigits(key);
|
|
14791
|
+
if (keyTones) playKeyTone(key);
|
|
14792
|
+
},
|
|
14793
|
+
children: [
|
|
14794
|
+
/* @__PURE__ */ jsxRuntime.jsx("b", { children: key }),
|
|
14795
|
+
letters ? /* @__PURE__ */ jsxRuntime.jsx("small", { children: letters }) : null
|
|
14796
|
+
]
|
|
14797
|
+
},
|
|
14798
|
+
key
|
|
14799
|
+
)) }) : null
|
|
14147
14800
|
] }) })
|
|
14148
14801
|
] });
|
|
14149
14802
|
}
|
|
@@ -14862,117 +15515,6 @@ function MicPermissionGate({ children }) {
|
|
|
14862
15515
|
if (state === "ready") return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
|
|
14863
15516
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-softphone", children: /* @__PURE__ */ jsxRuntime.jsx(MicPrompt, { state, onRequest: () => void request() }) });
|
|
14864
15517
|
}
|
|
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
15518
|
function ErrorGate({
|
|
14977
15519
|
error,
|
|
14978
15520
|
onRetry
|
|
@@ -15029,203 +15571,6 @@ function useCallOutcome() {
|
|
|
15029
15571
|
clear
|
|
15030
15572
|
};
|
|
15031
15573
|
}
|
|
15032
|
-
|
|
15033
|
-
// src/ui/audio/audio.ts
|
|
15034
|
-
var TONE_HZ = 425;
|
|
15035
|
-
function createRemoteAudio() {
|
|
15036
|
-
const el = document.createElement("audio");
|
|
15037
|
-
el.autoplay = true;
|
|
15038
|
-
el.style.display = "none";
|
|
15039
|
-
document.body.appendChild(el);
|
|
15040
|
-
return {
|
|
15041
|
-
attach(stream) {
|
|
15042
|
-
el.srcObject = stream;
|
|
15043
|
-
void el.play().catch(() => {
|
|
15044
|
-
});
|
|
15045
|
-
},
|
|
15046
|
-
detach() {
|
|
15047
|
-
el.srcObject = null;
|
|
15048
|
-
},
|
|
15049
|
-
destroy() {
|
|
15050
|
-
el.srcObject = null;
|
|
15051
|
-
el.remove();
|
|
15052
|
-
},
|
|
15053
|
-
async setOutputDevice(deviceId) {
|
|
15054
|
-
if (typeof el.setSinkId !== "function") return;
|
|
15055
|
-
try {
|
|
15056
|
-
await el.setSinkId(deviceId ?? "");
|
|
15057
|
-
} catch {
|
|
15058
|
-
}
|
|
15059
|
-
}
|
|
15060
|
-
};
|
|
15061
|
-
}
|
|
15062
|
-
var shared = null;
|
|
15063
|
-
function sharedContext() {
|
|
15064
|
-
if (shared) return shared;
|
|
15065
|
-
const Ctor = typeof AudioContext !== "undefined" ? AudioContext : globalThis.webkitAudioContext;
|
|
15066
|
-
if (!Ctor) return null;
|
|
15067
|
-
shared = new Ctor();
|
|
15068
|
-
return shared;
|
|
15069
|
-
}
|
|
15070
|
-
function primeAudio() {
|
|
15071
|
-
const ctx = sharedContext();
|
|
15072
|
-
if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
|
|
15073
|
-
});
|
|
15074
|
-
}
|
|
15075
|
-
function playRingtone() {
|
|
15076
|
-
const ctx = sharedContext();
|
|
15077
|
-
if (!ctx) return { stop() {
|
|
15078
|
-
} };
|
|
15079
|
-
const gain = ctx.createGain();
|
|
15080
|
-
gain.gain.value = 0;
|
|
15081
|
-
gain.connect(ctx.destination);
|
|
15082
|
-
const osc = ctx.createOscillator();
|
|
15083
|
-
osc.type = "sine";
|
|
15084
|
-
osc.frequency.value = TONE_HZ;
|
|
15085
|
-
osc.connect(gain);
|
|
15086
|
-
osc.start();
|
|
15087
|
-
const LEVEL = 0.8;
|
|
15088
|
-
const RAMP = 0.02;
|
|
15089
|
-
const burst = (at, dur) => {
|
|
15090
|
-
gain.gain.setValueAtTime(0, at);
|
|
15091
|
-
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
15092
|
-
gain.gain.setValueAtTime(LEVEL, at + dur - RAMP);
|
|
15093
|
-
gain.gain.linearRampToValueAtTime(0, at + dur);
|
|
15094
|
-
};
|
|
15095
|
-
const scheduleCycle = (at) => {
|
|
15096
|
-
burst(at, 0.4);
|
|
15097
|
-
burst(at + 0.6, 0.4);
|
|
15098
|
-
return 3;
|
|
15099
|
-
};
|
|
15100
|
-
const LOOKAHEAD = 0.5;
|
|
15101
|
-
const TICK_MS = 150;
|
|
15102
|
-
const LEAD = 0.08;
|
|
15103
|
-
let stopped = false;
|
|
15104
|
-
let timer = null;
|
|
15105
|
-
let nextAt = 0;
|
|
15106
|
-
const pump = () => {
|
|
15107
|
-
while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleCycle(nextAt);
|
|
15108
|
-
};
|
|
15109
|
-
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
15110
|
-
}).finally(() => {
|
|
15111
|
-
if (stopped) return;
|
|
15112
|
-
nextAt = ctx.currentTime + LEAD;
|
|
15113
|
-
pump();
|
|
15114
|
-
timer = setInterval(pump, TICK_MS);
|
|
15115
|
-
});
|
|
15116
|
-
return {
|
|
15117
|
-
stop() {
|
|
15118
|
-
stopped = true;
|
|
15119
|
-
if (timer) clearInterval(timer);
|
|
15120
|
-
try {
|
|
15121
|
-
osc.stop();
|
|
15122
|
-
} catch {
|
|
15123
|
-
}
|
|
15124
|
-
try {
|
|
15125
|
-
gain.disconnect();
|
|
15126
|
-
} catch {
|
|
15127
|
-
}
|
|
15128
|
-
}
|
|
15129
|
-
};
|
|
15130
|
-
}
|
|
15131
|
-
function playCallEndedTone() {
|
|
15132
|
-
const ctx = sharedContext();
|
|
15133
|
-
if (!ctx) return;
|
|
15134
|
-
const gain = ctx.createGain();
|
|
15135
|
-
gain.gain.value = 0;
|
|
15136
|
-
gain.connect(ctx.destination);
|
|
15137
|
-
const osc = ctx.createOscillator();
|
|
15138
|
-
osc.type = "sine";
|
|
15139
|
-
osc.frequency.value = TONE_HZ;
|
|
15140
|
-
osc.connect(gain);
|
|
15141
|
-
osc.start();
|
|
15142
|
-
const LEVEL = 0.55;
|
|
15143
|
-
const RAMP = 8e-3;
|
|
15144
|
-
const BEEP = 0.25;
|
|
15145
|
-
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
15146
|
-
}).finally(() => {
|
|
15147
|
-
const at = ctx.currentTime + 0.02;
|
|
15148
|
-
gain.gain.setValueAtTime(0, at);
|
|
15149
|
-
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
15150
|
-
gain.gain.setValueAtTime(LEVEL, at + BEEP - RAMP);
|
|
15151
|
-
gain.gain.linearRampToValueAtTime(0, at + BEEP);
|
|
15152
|
-
const totalMs = (at + BEEP - ctx.currentTime) * 1e3;
|
|
15153
|
-
setTimeout(() => {
|
|
15154
|
-
try {
|
|
15155
|
-
osc.stop();
|
|
15156
|
-
} catch {
|
|
15157
|
-
}
|
|
15158
|
-
try {
|
|
15159
|
-
gain.disconnect();
|
|
15160
|
-
} catch {
|
|
15161
|
-
}
|
|
15162
|
-
}, totalMs);
|
|
15163
|
-
});
|
|
15164
|
-
}
|
|
15165
|
-
var ringbackUrl = null;
|
|
15166
|
-
function ringbackSource() {
|
|
15167
|
-
if (ringbackUrl) return ringbackUrl;
|
|
15168
|
-
const sampleRate = 48e3;
|
|
15169
|
-
const channels = 2;
|
|
15170
|
-
const bytesPerSample = 2;
|
|
15171
|
-
const amp = 0.5;
|
|
15172
|
-
const toneSec = 1.5;
|
|
15173
|
-
const silenceSec = 3;
|
|
15174
|
-
const totalFrames = Math.round((toneSec + silenceSec) * sampleRate);
|
|
15175
|
-
const toneFrames = Math.round(toneSec * sampleRate);
|
|
15176
|
-
const ramp = Math.round(0.02 * sampleRate);
|
|
15177
|
-
const blockAlign = channels * bytesPerSample;
|
|
15178
|
-
const dataSize = totalFrames * blockAlign;
|
|
15179
|
-
const buffer = new ArrayBuffer(44 + dataSize);
|
|
15180
|
-
const view = new DataView(buffer);
|
|
15181
|
-
const str = (off, s) => {
|
|
15182
|
-
for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
|
|
15183
|
-
};
|
|
15184
|
-
str(0, "RIFF");
|
|
15185
|
-
view.setUint32(4, 36 + dataSize, true);
|
|
15186
|
-
str(8, "WAVE");
|
|
15187
|
-
str(12, "fmt ");
|
|
15188
|
-
view.setUint32(16, 16, true);
|
|
15189
|
-
view.setUint16(20, 1, true);
|
|
15190
|
-
view.setUint16(22, channels, true);
|
|
15191
|
-
view.setUint32(24, sampleRate, true);
|
|
15192
|
-
view.setUint32(28, sampleRate * blockAlign, true);
|
|
15193
|
-
view.setUint16(32, blockAlign, true);
|
|
15194
|
-
view.setUint16(34, bytesPerSample * 8, true);
|
|
15195
|
-
str(36, "data");
|
|
15196
|
-
view.setUint32(40, dataSize, true);
|
|
15197
|
-
for (let i = 0; i < totalFrames; i++) {
|
|
15198
|
-
let sample = 0;
|
|
15199
|
-
if (i < toneFrames) {
|
|
15200
|
-
let env = 1;
|
|
15201
|
-
if (i < ramp) env = i / ramp;
|
|
15202
|
-
else if (i > toneFrames - ramp) env = Math.max(0, (toneFrames - i) / ramp);
|
|
15203
|
-
sample = Math.sin(2 * Math.PI * TONE_HZ * (i / sampleRate)) * amp * env;
|
|
15204
|
-
}
|
|
15205
|
-
const s16 = Math.round(sample * 32767);
|
|
15206
|
-
const off = 44 + i * blockAlign;
|
|
15207
|
-
for (let c = 0; c < channels; c++) view.setInt16(off + c * bytesPerSample, s16, true);
|
|
15208
|
-
}
|
|
15209
|
-
ringbackUrl = URL.createObjectURL(new Blob([buffer], { type: "audio/wav" }));
|
|
15210
|
-
return ringbackUrl;
|
|
15211
|
-
}
|
|
15212
|
-
function playRingback() {
|
|
15213
|
-
const el = document.createElement("audio");
|
|
15214
|
-
el.src = ringbackSource();
|
|
15215
|
-
el.loop = true;
|
|
15216
|
-
el.style.display = "none";
|
|
15217
|
-
document.body.appendChild(el);
|
|
15218
|
-
void el.play().catch(() => {
|
|
15219
|
-
});
|
|
15220
|
-
return {
|
|
15221
|
-
stop() {
|
|
15222
|
-
el.pause();
|
|
15223
|
-
el.remove();
|
|
15224
|
-
}
|
|
15225
|
-
};
|
|
15226
|
-
}
|
|
15227
|
-
|
|
15228
|
-
// src/ui/audio/useCallAudio.ts
|
|
15229
15574
|
function useCallAudio(enabled) {
|
|
15230
15575
|
const softphone = useSoftphone();
|
|
15231
15576
|
const remote = React.useRef(null);
|
|
@@ -15304,6 +15649,94 @@ function useCallAudio(enabled) {
|
|
|
15304
15649
|
};
|
|
15305
15650
|
}, [softphone, enabled]);
|
|
15306
15651
|
}
|
|
15652
|
+
function TransferNotices({
|
|
15653
|
+
notices,
|
|
15654
|
+
dismiss
|
|
15655
|
+
}) {
|
|
15656
|
+
const softphone = useSoftphone();
|
|
15657
|
+
const now = useTicker(notices.length > 0, 15e3);
|
|
15658
|
+
const callBack = (notice) => {
|
|
15659
|
+
dismiss(notice.id);
|
|
15660
|
+
if (notice.caller) void softphone.call(notice.caller).catch(() => {
|
|
15661
|
+
});
|
|
15662
|
+
};
|
|
15663
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
15664
|
+
"div",
|
|
15665
|
+
{
|
|
15666
|
+
className: "cv-transfer-notices",
|
|
15667
|
+
role: "status",
|
|
15668
|
+
part: "transfer-notices",
|
|
15669
|
+
"data-has-notices": notices.length > 0,
|
|
15670
|
+
children: [
|
|
15671
|
+
notices.length > 1 ? /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "cv-transfer-notices-count", children: [
|
|
15672
|
+
notices.length,
|
|
15673
|
+
" traspasos sin conectar"
|
|
15674
|
+
] }) : null,
|
|
15675
|
+
notices.map((notice) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-transfer-notice", part: "transfer-notice", children: [
|
|
15676
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-transfer-notice-mark", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx(PhoneMissed, { size: 16 }) }),
|
|
15677
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-transfer-notice-body", children: [
|
|
15678
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-transfer-notice-header", children: [
|
|
15679
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "cv-transfer-notice-title", children: [
|
|
15680
|
+
"El traspaso a ",
|
|
15681
|
+
notice.target,
|
|
15682
|
+
" no se conect\xF3"
|
|
15683
|
+
] }),
|
|
15684
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
15685
|
+
"button",
|
|
15686
|
+
{
|
|
15687
|
+
type: "button",
|
|
15688
|
+
className: "cv-transfer-notice-dismiss",
|
|
15689
|
+
onClick: () => dismiss(notice.id),
|
|
15690
|
+
"aria-label": "Descartar aviso",
|
|
15691
|
+
part: "transfer-notice-dismiss",
|
|
15692
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(X, { size: 14 })
|
|
15693
|
+
}
|
|
15694
|
+
)
|
|
15695
|
+
] }),
|
|
15696
|
+
notice.caller ? /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "cv-transfer-notice-subtitle", children: [
|
|
15697
|
+
notice.caller,
|
|
15698
|
+
" se ha desconectado."
|
|
15699
|
+
] }) : null,
|
|
15700
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-transfer-notice-footer", children: [
|
|
15701
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-transfer-notice-age", children: formatAge(elapsedSeconds(notice.at, now)) }),
|
|
15702
|
+
notice.caller ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
15703
|
+
"button",
|
|
15704
|
+
{
|
|
15705
|
+
type: "button",
|
|
15706
|
+
className: "cv-btn cv-btn--accent cv-transfer-notice-callback",
|
|
15707
|
+
onClick: () => callBack(notice),
|
|
15708
|
+
part: "transfer-notice-callback",
|
|
15709
|
+
children: [
|
|
15710
|
+
/* @__PURE__ */ jsxRuntime.jsx(Phone, { size: 14, "aria-hidden": true }),
|
|
15711
|
+
"Llamar de vuelta"
|
|
15712
|
+
]
|
|
15713
|
+
}
|
|
15714
|
+
) : null
|
|
15715
|
+
] })
|
|
15716
|
+
] })
|
|
15717
|
+
] }, notice.id))
|
|
15718
|
+
]
|
|
15719
|
+
}
|
|
15720
|
+
);
|
|
15721
|
+
}
|
|
15722
|
+
function useTransferNotices() {
|
|
15723
|
+
const softphone = useSoftphone();
|
|
15724
|
+
const [notices, setNotices] = React.useState([]);
|
|
15725
|
+
const nextId = React.useRef(0);
|
|
15726
|
+
const dismiss = React.useCallback((id) => {
|
|
15727
|
+
setNotices((prev) => prev.filter((n) => n.id !== id));
|
|
15728
|
+
}, []);
|
|
15729
|
+
React.useEffect(
|
|
15730
|
+
() => softphone.on("transferOutcome" /* TransferOutcome */, (payload) => {
|
|
15731
|
+
if (payload.status === "not-connected") {
|
|
15732
|
+
const id = nextId.current++;
|
|
15733
|
+
setNotices((prev) => [...prev, { id, target: payload.target, caller: payload.caller, at: payload.at }]);
|
|
15734
|
+
}
|
|
15735
|
+
}),
|
|
15736
|
+
[softphone]
|
|
15737
|
+
);
|
|
15738
|
+
return { notices, dismiss };
|
|
15739
|
+
}
|
|
15307
15740
|
function SoftphoneWidget(props) {
|
|
15308
15741
|
const {
|
|
15309
15742
|
contacts,
|
|
@@ -15328,7 +15761,9 @@ function SoftphoneWidget(props) {
|
|
|
15328
15761
|
useCallAudio(sounds);
|
|
15329
15762
|
const blocked = requireMic && mic.state !== "ready";
|
|
15330
15763
|
const fatal = useLastError();
|
|
15764
|
+
const screenIsFree = !call && !blocked && !fatal && !isFollower;
|
|
15331
15765
|
const { outcome, clear: clearOutcome } = useCallOutcome();
|
|
15766
|
+
const { notices: transferNotices, dismiss: dismissTransferNotice } = useTransferNotices();
|
|
15332
15767
|
const retry = () => {
|
|
15333
15768
|
void (async () => {
|
|
15334
15769
|
try {
|
|
@@ -15352,8 +15787,9 @@ function SoftphoneWidget(props) {
|
|
|
15352
15787
|
className: className ? `cv-softphone ${className}` : "cv-softphone",
|
|
15353
15788
|
...theme === "system" ? {} : { "data-cv-theme": theme },
|
|
15354
15789
|
children: [
|
|
15355
|
-
|
|
15356
|
-
|
|
15790
|
+
screenIsFree ? /* @__PURE__ */ jsxRuntime.jsx(TransferNotices, { notices: transferNotices, dismiss: dismissTransferNotice }) : null,
|
|
15791
|
+
/* @__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, keyTones: sounds, ...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, keyTones: sounds }) }),
|
|
15792
|
+
screenIsFree ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-tabs", role: "tablist", children: tabs.map(({ id, icon: Icon3, label }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
15357
15793
|
"button",
|
|
15358
15794
|
{
|
|
15359
15795
|
role: "tab",
|
|
@@ -15366,7 +15802,7 @@ function SoftphoneWidget(props) {
|
|
|
15366
15802
|
]
|
|
15367
15803
|
},
|
|
15368
15804
|
id
|
|
15369
|
-
)) })
|
|
15805
|
+
)) }) : null
|
|
15370
15806
|
]
|
|
15371
15807
|
}
|
|
15372
15808
|
);
|