@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.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isWebRtcSupported } from '../chunk-KAXEMJ7E.js';
|
|
2
|
-
import { useSoftphone, useSoftphoneState, useCall, useActiveCall, useIsLeader, useLastError, useDevices } from '../chunk-
|
|
3
|
-
import '../chunk-
|
|
2
|
+
import { useSoftphone, useSoftphoneState, useCall, usePresence, useActiveCall, useIsLeader, useLastError, useDevices } from '../chunk-LMJXX4YM.js';
|
|
3
|
+
import '../chunk-7R6LOOAI.js';
|
|
4
4
|
import { forwardRef, createElement, useState, useRef, useLayoutEffect, useEffect, useCallback, useMemo } from 'react';
|
|
5
5
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
6
|
|
|
@@ -550,21 +550,263 @@ function caretAfterDigits(formatted, digitsBefore) {
|
|
|
550
550
|
function digitsBeforeCaret(text, caret) {
|
|
551
551
|
return (text.slice(0, caret).match(/\d/g) ?? []).length;
|
|
552
552
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
["
|
|
557
|
-
["
|
|
558
|
-
["
|
|
559
|
-
["
|
|
560
|
-
["
|
|
561
|
-
["
|
|
562
|
-
["
|
|
563
|
-
["
|
|
564
|
-
["
|
|
565
|
-
["
|
|
553
|
+
|
|
554
|
+
// src/ui/dialing/keys.ts
|
|
555
|
+
var KEYPAD_KEYS = [
|
|
556
|
+
["1", ""],
|
|
557
|
+
["2", "ABC"],
|
|
558
|
+
["3", "DEF"],
|
|
559
|
+
["4", "GHI"],
|
|
560
|
+
["5", "JKL"],
|
|
561
|
+
["6", "MNO"],
|
|
562
|
+
["7", "PQRS"],
|
|
563
|
+
["8", "TUV"],
|
|
564
|
+
["9", "WXYZ"],
|
|
565
|
+
["*", ""],
|
|
566
|
+
["0", "+"],
|
|
567
|
+
["#", ""]
|
|
566
568
|
];
|
|
567
|
-
|
|
569
|
+
|
|
570
|
+
// src/ui/audio/audio.ts
|
|
571
|
+
var TONE_HZ = 425;
|
|
572
|
+
function createRemoteAudio() {
|
|
573
|
+
const el = document.createElement("audio");
|
|
574
|
+
el.autoplay = true;
|
|
575
|
+
el.style.display = "none";
|
|
576
|
+
document.body.appendChild(el);
|
|
577
|
+
return {
|
|
578
|
+
attach(stream) {
|
|
579
|
+
el.srcObject = stream;
|
|
580
|
+
void el.play().catch(() => {
|
|
581
|
+
});
|
|
582
|
+
},
|
|
583
|
+
detach() {
|
|
584
|
+
el.srcObject = null;
|
|
585
|
+
},
|
|
586
|
+
destroy() {
|
|
587
|
+
el.srcObject = null;
|
|
588
|
+
el.remove();
|
|
589
|
+
},
|
|
590
|
+
async setOutputDevice(deviceId) {
|
|
591
|
+
if (typeof el.setSinkId !== "function") return;
|
|
592
|
+
try {
|
|
593
|
+
await el.setSinkId(deviceId ?? "");
|
|
594
|
+
} catch {
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
var shared = null;
|
|
600
|
+
function sharedContext() {
|
|
601
|
+
if (shared) return shared;
|
|
602
|
+
const Ctor = typeof AudioContext !== "undefined" ? AudioContext : globalThis.webkitAudioContext;
|
|
603
|
+
if (!Ctor) return null;
|
|
604
|
+
shared = new Ctor();
|
|
605
|
+
return shared;
|
|
606
|
+
}
|
|
607
|
+
function primeAudio() {
|
|
608
|
+
const ctx = sharedContext();
|
|
609
|
+
if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
function playRingtone() {
|
|
613
|
+
const ctx = sharedContext();
|
|
614
|
+
if (!ctx) return { stop() {
|
|
615
|
+
} };
|
|
616
|
+
const gain = ctx.createGain();
|
|
617
|
+
gain.gain.value = 0;
|
|
618
|
+
gain.connect(ctx.destination);
|
|
619
|
+
const osc = ctx.createOscillator();
|
|
620
|
+
osc.type = "sine";
|
|
621
|
+
osc.frequency.value = TONE_HZ;
|
|
622
|
+
osc.connect(gain);
|
|
623
|
+
osc.start();
|
|
624
|
+
const LEVEL = 0.8;
|
|
625
|
+
const RAMP = 0.02;
|
|
626
|
+
const burst = (at, dur) => {
|
|
627
|
+
gain.gain.setValueAtTime(0, at);
|
|
628
|
+
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
629
|
+
gain.gain.setValueAtTime(LEVEL, at + dur - RAMP);
|
|
630
|
+
gain.gain.linearRampToValueAtTime(0, at + dur);
|
|
631
|
+
};
|
|
632
|
+
const scheduleCycle = (at) => {
|
|
633
|
+
burst(at, 0.4);
|
|
634
|
+
burst(at + 0.6, 0.4);
|
|
635
|
+
return 3;
|
|
636
|
+
};
|
|
637
|
+
const LOOKAHEAD = 0.5;
|
|
638
|
+
const TICK_MS = 150;
|
|
639
|
+
const LEAD = 0.08;
|
|
640
|
+
let stopped = false;
|
|
641
|
+
let timer = null;
|
|
642
|
+
let nextAt = 0;
|
|
643
|
+
const pump = () => {
|
|
644
|
+
while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleCycle(nextAt);
|
|
645
|
+
};
|
|
646
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
647
|
+
}).finally(() => {
|
|
648
|
+
if (stopped) return;
|
|
649
|
+
nextAt = ctx.currentTime + LEAD;
|
|
650
|
+
pump();
|
|
651
|
+
timer = setInterval(pump, TICK_MS);
|
|
652
|
+
});
|
|
653
|
+
return {
|
|
654
|
+
stop() {
|
|
655
|
+
stopped = true;
|
|
656
|
+
if (timer) clearInterval(timer);
|
|
657
|
+
try {
|
|
658
|
+
osc.stop();
|
|
659
|
+
} catch {
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
gain.disconnect();
|
|
663
|
+
} catch {
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function playCallEndedTone() {
|
|
669
|
+
const ctx = sharedContext();
|
|
670
|
+
if (!ctx) return;
|
|
671
|
+
const gain = ctx.createGain();
|
|
672
|
+
gain.gain.value = 0;
|
|
673
|
+
gain.connect(ctx.destination);
|
|
674
|
+
const osc = ctx.createOscillator();
|
|
675
|
+
osc.type = "sine";
|
|
676
|
+
osc.frequency.value = TONE_HZ;
|
|
677
|
+
osc.connect(gain);
|
|
678
|
+
osc.start();
|
|
679
|
+
const LEVEL = 0.55;
|
|
680
|
+
const RAMP = 8e-3;
|
|
681
|
+
const BEEP = 0.25;
|
|
682
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
683
|
+
}).finally(() => {
|
|
684
|
+
const at = ctx.currentTime + 0.02;
|
|
685
|
+
gain.gain.setValueAtTime(0, at);
|
|
686
|
+
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
687
|
+
gain.gain.setValueAtTime(LEVEL, at + BEEP - RAMP);
|
|
688
|
+
gain.gain.linearRampToValueAtTime(0, at + BEEP);
|
|
689
|
+
const totalMs = (at + BEEP - ctx.currentTime) * 1e3;
|
|
690
|
+
setTimeout(() => {
|
|
691
|
+
try {
|
|
692
|
+
osc.stop();
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
695
|
+
try {
|
|
696
|
+
gain.disconnect();
|
|
697
|
+
} catch {
|
|
698
|
+
}
|
|
699
|
+
}, totalMs);
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
var DTMF_ROWS_HZ = [697, 770, 852, 941];
|
|
703
|
+
var DTMF_COLS_HZ = [1209, 1336, 1477];
|
|
704
|
+
var DTMF_LAYOUT = ["123", "456", "789", "*0#"];
|
|
705
|
+
function dtmfFrequencies(key) {
|
|
706
|
+
if (key.length !== 1) return null;
|
|
707
|
+
for (let row = 0; row < DTMF_LAYOUT.length; row++) {
|
|
708
|
+
const col = DTMF_LAYOUT[row].indexOf(key);
|
|
709
|
+
if (col !== -1) return [DTMF_ROWS_HZ[row], DTMF_COLS_HZ[col]];
|
|
710
|
+
}
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
function playKeyTone(key) {
|
|
714
|
+
const hz = dtmfFrequencies(key);
|
|
715
|
+
if (!hz) return;
|
|
716
|
+
const ctx = sharedContext();
|
|
717
|
+
if (!ctx) return;
|
|
718
|
+
const LEVEL = 0.2;
|
|
719
|
+
const RAMP = 6e-3;
|
|
720
|
+
const DURATION = 0.12;
|
|
721
|
+
const LEAD = 0.01;
|
|
722
|
+
const gain = ctx.createGain();
|
|
723
|
+
gain.gain.value = 0;
|
|
724
|
+
gain.connect(ctx.destination);
|
|
725
|
+
const oscillators = hz.map((freq) => {
|
|
726
|
+
const osc = ctx.createOscillator();
|
|
727
|
+
osc.type = "sine";
|
|
728
|
+
osc.frequency.value = freq;
|
|
729
|
+
osc.connect(gain);
|
|
730
|
+
osc.start();
|
|
731
|
+
return osc;
|
|
732
|
+
});
|
|
733
|
+
const fire = () => {
|
|
734
|
+
const at = ctx.currentTime + LEAD;
|
|
735
|
+
gain.gain.setValueAtTime(0, at);
|
|
736
|
+
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
737
|
+
gain.gain.setValueAtTime(LEVEL, at + DURATION - RAMP);
|
|
738
|
+
gain.gain.linearRampToValueAtTime(0, at + DURATION);
|
|
739
|
+
for (const osc of oscillators) osc.stop(at + DURATION);
|
|
740
|
+
oscillators[0].onended = () => gain.disconnect();
|
|
741
|
+
};
|
|
742
|
+
if (ctx.state === "running") fire();
|
|
743
|
+
else void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
744
|
+
}).finally(fire);
|
|
745
|
+
}
|
|
746
|
+
var ringbackUrl = null;
|
|
747
|
+
function ringbackSource() {
|
|
748
|
+
if (ringbackUrl) return ringbackUrl;
|
|
749
|
+
const sampleRate = 48e3;
|
|
750
|
+
const channels = 2;
|
|
751
|
+
const bytesPerSample = 2;
|
|
752
|
+
const amp = 0.5;
|
|
753
|
+
const toneSec = 1.5;
|
|
754
|
+
const silenceSec = 3;
|
|
755
|
+
const totalFrames = Math.round((toneSec + silenceSec) * sampleRate);
|
|
756
|
+
const toneFrames = Math.round(toneSec * sampleRate);
|
|
757
|
+
const ramp = Math.round(0.02 * sampleRate);
|
|
758
|
+
const blockAlign = channels * bytesPerSample;
|
|
759
|
+
const dataSize = totalFrames * blockAlign;
|
|
760
|
+
const buffer = new ArrayBuffer(44 + dataSize);
|
|
761
|
+
const view = new DataView(buffer);
|
|
762
|
+
const str = (off, s) => {
|
|
763
|
+
for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
|
|
764
|
+
};
|
|
765
|
+
str(0, "RIFF");
|
|
766
|
+
view.setUint32(4, 36 + dataSize, true);
|
|
767
|
+
str(8, "WAVE");
|
|
768
|
+
str(12, "fmt ");
|
|
769
|
+
view.setUint32(16, 16, true);
|
|
770
|
+
view.setUint16(20, 1, true);
|
|
771
|
+
view.setUint16(22, channels, true);
|
|
772
|
+
view.setUint32(24, sampleRate, true);
|
|
773
|
+
view.setUint32(28, sampleRate * blockAlign, true);
|
|
774
|
+
view.setUint16(32, blockAlign, true);
|
|
775
|
+
view.setUint16(34, bytesPerSample * 8, true);
|
|
776
|
+
str(36, "data");
|
|
777
|
+
view.setUint32(40, dataSize, true);
|
|
778
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
779
|
+
let sample = 0;
|
|
780
|
+
if (i < toneFrames) {
|
|
781
|
+
let env = 1;
|
|
782
|
+
if (i < ramp) env = i / ramp;
|
|
783
|
+
else if (i > toneFrames - ramp) env = Math.max(0, (toneFrames - i) / ramp);
|
|
784
|
+
sample = Math.sin(2 * Math.PI * TONE_HZ * (i / sampleRate)) * amp * env;
|
|
785
|
+
}
|
|
786
|
+
const s16 = Math.round(sample * 32767);
|
|
787
|
+
const off = 44 + i * blockAlign;
|
|
788
|
+
for (let c = 0; c < channels; c++) view.setInt16(off + c * bytesPerSample, s16, true);
|
|
789
|
+
}
|
|
790
|
+
ringbackUrl = URL.createObjectURL(new Blob([buffer], { type: "audio/wav" }));
|
|
791
|
+
return ringbackUrl;
|
|
792
|
+
}
|
|
793
|
+
function playRingback() {
|
|
794
|
+
const el = document.createElement("audio");
|
|
795
|
+
el.src = ringbackSource();
|
|
796
|
+
el.loop = true;
|
|
797
|
+
el.style.display = "none";
|
|
798
|
+
document.body.appendChild(el);
|
|
799
|
+
void el.play().catch(() => {
|
|
800
|
+
});
|
|
801
|
+
return {
|
|
802
|
+
stop() {
|
|
803
|
+
el.pause();
|
|
804
|
+
el.remove();
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
var NOT_DIALABLE = /* @__PURE__ */ new Set(["*", "#"]);
|
|
809
|
+
function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
|
|
568
810
|
const softphone = useSoftphone();
|
|
569
811
|
const state = useSoftphoneState();
|
|
570
812
|
const [value, setValue] = useState("");
|
|
@@ -585,6 +827,7 @@ function Dialpad({ outcome = null, onIntent } = {}) {
|
|
|
585
827
|
const append = (key) => {
|
|
586
828
|
onIntent?.();
|
|
587
829
|
setValue((v) => v + key);
|
|
830
|
+
if (keyTones) playKeyTone(key);
|
|
588
831
|
};
|
|
589
832
|
const backspace = () => {
|
|
590
833
|
onIntent?.();
|
|
@@ -629,22 +872,25 @@ function Dialpad({ outcome = null, onIntent } = {}) {
|
|
|
629
872
|
)
|
|
630
873
|
] }),
|
|
631
874
|
/* @__PURE__ */ jsx("p", { className: "cv-dial-outcome", role: "status", part: "error", "data-shown": outcome !== null, children: outcome?.text ?? "" }),
|
|
632
|
-
/* @__PURE__ */ jsx("div", { className: "cv-keypad", children:
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
875
|
+
/* @__PURE__ */ jsx("div", { className: "cv-keypad", children: KEYPAD_KEYS.map(([key, letters]) => {
|
|
876
|
+
const dialable = !NOT_DIALABLE.has(key);
|
|
877
|
+
return /* @__PURE__ */ jsxs(
|
|
878
|
+
"button",
|
|
879
|
+
{
|
|
880
|
+
type: "button",
|
|
881
|
+
className: "cv-key",
|
|
882
|
+
"data-dialable": dialable,
|
|
883
|
+
onClick: () => dialable && append(key),
|
|
884
|
+
"aria-disabled": !dialable,
|
|
885
|
+
title: dialable ? void 0 : "Solo disponible durante una llamada",
|
|
886
|
+
children: [
|
|
887
|
+
/* @__PURE__ */ jsx("b", { children: key }),
|
|
888
|
+
letters ? /* @__PURE__ */ jsx("small", { children: letters }) : null
|
|
889
|
+
]
|
|
890
|
+
},
|
|
891
|
+
key
|
|
892
|
+
);
|
|
893
|
+
}) }),
|
|
648
894
|
/* @__PURE__ */ jsx("div", { className: "cv-dial-foot", children: /* @__PURE__ */ jsx(
|
|
649
895
|
"button",
|
|
650
896
|
{
|
|
@@ -679,6 +925,125 @@ function elapsedSeconds(since, now) {
|
|
|
679
925
|
if (!since) return 0;
|
|
680
926
|
return Math.floor((now - since.getTime()) / 1e3);
|
|
681
927
|
}
|
|
928
|
+
function formatAge(totalSeconds) {
|
|
929
|
+
const s = Math.max(0, Math.floor(totalSeconds));
|
|
930
|
+
if (s < 60) return "justo ahora";
|
|
931
|
+
const mm = Math.floor(s / 60);
|
|
932
|
+
if (mm < 60) return `hace ${mm} min`;
|
|
933
|
+
const hh = Math.floor(mm / 60);
|
|
934
|
+
return `hace ${hh} h`;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/ui/lib/error-text.ts
|
|
938
|
+
var ERROR_TEXT = {
|
|
939
|
+
// ── Engine lifecycle ────────────────────────────────────────────────────────
|
|
940
|
+
// Integrator mistakes rather than user-facing failures, but they must still read as something
|
|
941
|
+
// rather than as a blank panel if one ever reaches the UI.
|
|
942
|
+
"engine/duplicate-instance": {
|
|
943
|
+
title: "Tel\xE9fono duplicado",
|
|
944
|
+
body: "Hay otro tel\xE9fono abierto en esta p\xE1gina. Cierra uno y vuelve a cargar."
|
|
945
|
+
},
|
|
946
|
+
"engine/not-started": { title: "El tel\xE9fono a\xFAn no est\xE1 listo" },
|
|
947
|
+
"engine/destroyed": {
|
|
948
|
+
title: "Tel\xE9fono cerrado",
|
|
949
|
+
body: "Vuelve a cargar la p\xE1gina para usarlo de nuevo."
|
|
950
|
+
},
|
|
951
|
+
// ── Auth ────────────────────────────────────────────────────────────────────
|
|
952
|
+
// Never "token": the user has no idea what one is. What they can act on is the session.
|
|
953
|
+
"auth/token-fetch-failed": {
|
|
954
|
+
title: "Sin conexi\xF3n con el servidor",
|
|
955
|
+
body: "No se ha podido activar el tel\xE9fono. Reint\xE9ntalo en unos segundos."
|
|
956
|
+
},
|
|
957
|
+
"auth/token-rejected": {
|
|
958
|
+
title: "Sesi\xF3n caducada",
|
|
959
|
+
body: "Vuelve a iniciar sesi\xF3n para poder llamar."
|
|
960
|
+
},
|
|
961
|
+
// ── Media ───────────────────────────────────────────────────────────────────
|
|
962
|
+
// Verbatim from MicPermissionGate, which already says these well and says them first.
|
|
963
|
+
//
|
|
964
|
+
// NEVER ACTUALLY SHOWN via this table — these four codes (through `media/playback-blocked`, below)
|
|
965
|
+
// are never raised as a `SoftphoneError` at all. See the fuller comment on each in `core/errors.ts`
|
|
966
|
+
// for why. Copy kept here anyway so the catalogue and this file stay the same shape as every other
|
|
967
|
+
// code — `error-text.test.ts` would fail the build the moment they drifted.
|
|
968
|
+
"media/permission-denied": {
|
|
969
|
+
title: "Micr\xF3fono bloqueado",
|
|
970
|
+
body: "Perm\xEDtelo en tu navegador para poder llamar."
|
|
971
|
+
},
|
|
972
|
+
"media/no-input-device": {
|
|
973
|
+
title: "Sin micr\xF3fono",
|
|
974
|
+
body: "Conecta un micr\xF3fono y vuelve a intentarlo."
|
|
975
|
+
},
|
|
976
|
+
"media/webrtc-unsupported": {
|
|
977
|
+
title: "Llamadas no disponibles",
|
|
978
|
+
body: "Este navegador no admite llamadas. Prueba con Chrome, Edge o Firefox."
|
|
979
|
+
},
|
|
980
|
+
"media/playback-blocked": { title: "Toca la pantalla para activar el sonido" },
|
|
981
|
+
// ── Network / signalling ────────────────────────────────────────────────────
|
|
982
|
+
// "Centralita" rather than "SIP" or "registro": the user knows what a phone system is.
|
|
983
|
+
"net/registration-failed": {
|
|
984
|
+
title: "No se pudo conectar",
|
|
985
|
+
body: "No hemos podido conectar con la centralita. Reintenta en unos segundos."
|
|
986
|
+
},
|
|
987
|
+
"net/transport-failed": {
|
|
988
|
+
title: "Conexi\xF3n perdida",
|
|
989
|
+
body: "Se ha perdido la conexi\xF3n con la centralita."
|
|
990
|
+
},
|
|
991
|
+
// NEVER ACTUALLY SHOWN either — CallScreen's own "Reconectando…" already covers this, off the
|
|
992
|
+
// call's `reconnecting` state rather than this channel. See the comment in `core/errors.ts`.
|
|
993
|
+
"net/ice-failed": { title: "Recuperando el audio\u2026" },
|
|
994
|
+
// ── Call ────────────────────────────────────────────────────────────────────
|
|
995
|
+
/*
|
|
996
|
+
THE VOICE RULE FOR THIS BLOCK: say what HAPPENED, not what it is called.
|
|
997
|
+
|
|
998
|
+
These were status labels — "Llamada perdida", "Llamada rechazada", "Sin respuesta". Each is a
|
|
999
|
+
name for a category rather than an account of an event, which is why they read as a database enum
|
|
1000
|
+
someone forgot to translate into human. Worse, a label hides the ACTOR: "Llamada rechazada" never
|
|
1001
|
+
says who rejected what, so the user reading it after declining a call and the user reading it
|
|
1002
|
+
after being declined see the same three words.
|
|
1003
|
+
|
|
1004
|
+
So each one is a short sentence about what the other person, or the phone, did — the form
|
|
1005
|
+
`call/busy` already had, and the reason it was the only one here that read clearly.
|
|
1006
|
+
|
|
1007
|
+
Impersonal third-person plural ("contestaron", "colgaron") throughout: the user does not know who
|
|
1008
|
+
is at the other end, and it is what Spanish uses when the subject is unknown. Mixing it with the
|
|
1009
|
+
singular across neighbouring lines reads as carelessness.
|
|
1010
|
+
|
|
1011
|
+
LENGTH IS A HARD CONSTRAINT, not a preference. `.cv-dial-outcome` is one line at a fixed
|
|
1012
|
+
`1.15rem`, and the keypad below it is `flex: 1` — a title that wraps resizes every key and shifts
|
|
1013
|
+
the grid under a finger already reaching for it. `error-text.test.ts` caps this at 48 characters.
|
|
1014
|
+
*/
|
|
1015
|
+
"call/not-answerable": { title: "Esa llamada ya ha terminado" },
|
|
1016
|
+
/*
|
|
1017
|
+
NOT "Comunicando", which is what this said and what sent us looking. SIP 486 is both "the line is
|
|
1018
|
+
engaged" AND what almost every endpoint sends when a person presses decline — including our own
|
|
1019
|
+
`reject()`. One code, two events, no way to tell them apart, so the copy has to hold both rather
|
|
1020
|
+
than assert the less likely one. `call/rejected` can be definite: an endpoint sending 603 Decline
|
|
1021
|
+
means exactly that.
|
|
1022
|
+
*/
|
|
1023
|
+
"call/busy": { title: "No contestaron o la l\xEDnea estaba ocupada" },
|
|
1024
|
+
"call/no-answer": { title: "No contestaron" },
|
|
1025
|
+
"call/rejected": { title: "No aceptaron tu llamada" },
|
|
1026
|
+
/*
|
|
1027
|
+
The legacy CRM said "La persona que llamaba colgó antes de que pudieras contestar." — the right
|
|
1028
|
+
fact, at three times the width of the line that has to hold it. This keeps the fact and drops the
|
|
1029
|
+
words: the user needs to know the call was FOR them and that nobody is waiting on the line.
|
|
1030
|
+
"Llamada perdida" alone tested badly for exactly the label reason above — it names the category
|
|
1031
|
+
without saying anyone rang.
|
|
1032
|
+
*/
|
|
1033
|
+
"call/missed": { title: "Colgaron antes de que pudieras contestar" },
|
|
1034
|
+
// "No se pudo completar la llamada" was our own "something went wrong": true of every failure and
|
|
1035
|
+
// useful for none. The number is what the user can actually act on.
|
|
1036
|
+
"call/failed": { title: "No se pudo conectar con ese n\xFAmero" },
|
|
1037
|
+
// The load-bearing half is the SECOND sentence: the original call is still up, and a user who
|
|
1038
|
+
// thinks they dropped it will hang up on a client to "fix" it.
|
|
1039
|
+
"call/transfer-failed": { title: "No se pudo transferir. Sigues en la llamada" },
|
|
1040
|
+
"call/unsupported": { title: "Esta funci\xF3n no est\xE1 disponible" },
|
|
1041
|
+
// ── Provider ────────────────────────────────────────────────────────────────
|
|
1042
|
+
"provider/provision-failed": {
|
|
1043
|
+
title: "No se pudo activar el tel\xE9fono",
|
|
1044
|
+
body: "Si el problema contin\xFAa, contacta con soporte."
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
682
1047
|
function useTicker(enabled, intervalMs = 1e3) {
|
|
683
1048
|
const [now, setNow] = useState(() => Date.now());
|
|
684
1049
|
useEffect(() => {
|
|
@@ -760,65 +1125,262 @@ function DevicePickers() {
|
|
|
760
1125
|
)
|
|
761
1126
|
] });
|
|
762
1127
|
}
|
|
763
|
-
function Control({
|
|
764
|
-
icon: Icon3,
|
|
765
|
-
label,
|
|
766
|
-
pressed = false,
|
|
767
|
-
tone,
|
|
768
|
-
onClick
|
|
1128
|
+
function Control({
|
|
1129
|
+
icon: Icon3,
|
|
1130
|
+
label,
|
|
1131
|
+
pressed = false,
|
|
1132
|
+
tone,
|
|
1133
|
+
onClick,
|
|
1134
|
+
disabled = false,
|
|
1135
|
+
title
|
|
1136
|
+
}) {
|
|
1137
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-ctrl-item", children: [
|
|
1138
|
+
/* @__PURE__ */ jsx(
|
|
1139
|
+
"button",
|
|
1140
|
+
{
|
|
1141
|
+
type: "button",
|
|
1142
|
+
className: "cv-ctrl",
|
|
1143
|
+
"data-tone": pressed ? tone : void 0,
|
|
1144
|
+
"aria-pressed": pressed,
|
|
1145
|
+
"aria-label": label,
|
|
1146
|
+
onClick,
|
|
1147
|
+
disabled,
|
|
1148
|
+
title,
|
|
1149
|
+
children: /* @__PURE__ */ jsx(Icon3, { size: 19 })
|
|
1150
|
+
}
|
|
1151
|
+
),
|
|
1152
|
+
/* @__PURE__ */ jsx("span", { className: "cv-ctrl-label", "aria-hidden": true, children: label })
|
|
1153
|
+
] });
|
|
1154
|
+
}
|
|
1155
|
+
var STATUS_LABEL = {
|
|
1156
|
+
ringing: "Llamada entrante",
|
|
1157
|
+
dialing: "Llamando\u2026",
|
|
1158
|
+
early: "Llamando\u2026",
|
|
1159
|
+
answering: "Conectando\u2026",
|
|
1160
|
+
reconnecting: "Reconectando\u2026",
|
|
1161
|
+
held: "En espera"
|
|
1162
|
+
};
|
|
1163
|
+
var CONSULT_FAIL_CODE = {
|
|
1164
|
+
busy: "call/busy",
|
|
1165
|
+
"no-answer": "call/no-answer",
|
|
1166
|
+
rejected: "call/rejected",
|
|
1167
|
+
failed: "call/failed"
|
|
1168
|
+
};
|
|
1169
|
+
function consultFailText(reason) {
|
|
1170
|
+
return ERROR_TEXT[CONSULT_FAIL_CODE[reason] ?? "call/transfer-failed"].title;
|
|
1171
|
+
}
|
|
1172
|
+
var MIN_CONSULT_HOLD_SECONDS = 10;
|
|
1173
|
+
function ConsultScreen({
|
|
1174
|
+
mainName,
|
|
1175
|
+
consultView,
|
|
1176
|
+
now,
|
|
1177
|
+
onReturn,
|
|
1178
|
+
onComplete,
|
|
1179
|
+
onHangupAll
|
|
1180
|
+
}) {
|
|
1181
|
+
const s = consultView.state;
|
|
1182
|
+
const answered = s === "active";
|
|
1183
|
+
const name = consultView.remote.displayName;
|
|
1184
|
+
const number = consultView.remote.number;
|
|
1185
|
+
const showNumber = number !== name;
|
|
1186
|
+
const heldSeconds = elapsedSeconds(consultView.answeredAt, now);
|
|
1187
|
+
const [rawM = "0", ss = "00"] = formatDuration(heldSeconds).split(":");
|
|
1188
|
+
const mm = rawM.padStart(2, "0");
|
|
1189
|
+
const canComplete = heldSeconds >= MIN_CONSULT_HOLD_SECONDS;
|
|
1190
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-view", children: [
|
|
1191
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-call", children: [
|
|
1192
|
+
/* @__PURE__ */ jsxs("button", { type: "button", className: "cv-consult-hold-banner", onClick: onReturn, children: [
|
|
1193
|
+
/* @__PURE__ */ jsx(Pause, { size: 13, "aria-hidden": true }),
|
|
1194
|
+
mainName,
|
|
1195
|
+
" en espera \xB7 Volver"
|
|
1196
|
+
] }),
|
|
1197
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-avatar-wrap", children: [
|
|
1198
|
+
/* @__PURE__ */ jsx("div", { className: "cv-avatar", "aria-hidden": true, children: initials(name) }),
|
|
1199
|
+
answered ? /* @__PURE__ */ jsx("span", { className: "cv-avatar-dot", "aria-hidden": true }) : null
|
|
1200
|
+
] }),
|
|
1201
|
+
/* @__PURE__ */ jsx("div", { className: "cv-call-name", children: name }),
|
|
1202
|
+
showNumber ? /* @__PURE__ */ jsx("div", { className: "cv-call-number", children: number }) : null,
|
|
1203
|
+
answered ? /* @__PURE__ */ jsxs("div", { className: "cv-timer", "aria-label": "Duraci\xF3n de la llamada", children: [
|
|
1204
|
+
/* @__PURE__ */ jsx("b", { children: mm }),
|
|
1205
|
+
/* @__PURE__ */ jsx("i", { children: ":" }),
|
|
1206
|
+
/* @__PURE__ */ jsx("b", { children: ss })
|
|
1207
|
+
] }) : /* @__PURE__ */ jsx("div", { className: "cv-call-status", children: STATUS_LABEL[s] ?? "" })
|
|
1208
|
+
] }),
|
|
1209
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-controls", children: [
|
|
1210
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-transfer-actions", children: [
|
|
1211
|
+
answered ? /* @__PURE__ */ jsx("button", { className: "cv-btn cv-btn--accent", onClick: onComplete, disabled: !canComplete, children: "Completar" }) : null,
|
|
1212
|
+
/* @__PURE__ */ jsx("button", { className: "cv-btn", onClick: onReturn, children: "Cancelar" })
|
|
1213
|
+
] }),
|
|
1214
|
+
/* @__PURE__ */ jsx("div", { className: "cv-controls-primary", children: /* @__PURE__ */ jsxs("div", { className: "cv-ctrl-item", children: [
|
|
1215
|
+
/* @__PURE__ */ jsx(
|
|
1216
|
+
"button",
|
|
1217
|
+
{
|
|
1218
|
+
type: "button",
|
|
1219
|
+
className: "cv-round cv-round--hangup",
|
|
1220
|
+
onClick: onHangupAll,
|
|
1221
|
+
"aria-label": "Colgar todo",
|
|
1222
|
+
children: /* @__PURE__ */ jsx(PhoneOff, { size: 26 })
|
|
1223
|
+
}
|
|
1224
|
+
),
|
|
1225
|
+
/* @__PURE__ */ jsx("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Colgar todo" })
|
|
1226
|
+
] }) })
|
|
1227
|
+
] })
|
|
1228
|
+
] });
|
|
1229
|
+
}
|
|
1230
|
+
var PRESENCE_LABEL = {
|
|
1231
|
+
disponible: "Disponible",
|
|
1232
|
+
en_llamada: "En llamada",
|
|
1233
|
+
desconectado: "Desconectado"
|
|
1234
|
+
};
|
|
1235
|
+
var PRESENCE_RANK = {
|
|
1236
|
+
disponible: 0,
|
|
1237
|
+
en_llamada: 1,
|
|
1238
|
+
desconectado: 2
|
|
1239
|
+
};
|
|
1240
|
+
function filterAgents(agents, query) {
|
|
1241
|
+
const q = query.trim().toLowerCase();
|
|
1242
|
+
return agents.filter((a) => !q || (a.name ?? "").toLowerCase().includes(q) || a.extension.includes(q)).sort(
|
|
1243
|
+
(a, b) => PRESENCE_RANK[a.state] - PRESENCE_RANK[b.state] || (a.name ?? a.extension).localeCompare(b.name ?? b.extension)
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
function truncateForButton(text, max = 16) {
|
|
1247
|
+
return text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
1248
|
+
}
|
|
1249
|
+
function TransferPicker({
|
|
1250
|
+
agents,
|
|
1251
|
+
myUserId,
|
|
1252
|
+
query,
|
|
1253
|
+
onSelect
|
|
769
1254
|
}) {
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
"
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1255
|
+
const others = agents.filter((a) => a.userId !== myUserId);
|
|
1256
|
+
if (others.length === 0) return null;
|
|
1257
|
+
const filtered = filterAgents(others, query);
|
|
1258
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-presence", children: [
|
|
1259
|
+
/* @__PURE__ */ jsx("div", { className: "cv-presence-list", role: "listbox", "aria-label": "Compa\xF1eros", children: filtered.length === 0 ? /* @__PURE__ */ jsx("p", { className: "cv-presence-empty", children: "Sin coincidencias" }) : filtered.map((agent) => {
|
|
1260
|
+
const isBusy = agent.state === "en_llamada";
|
|
1261
|
+
return /* @__PURE__ */ jsxs(
|
|
1262
|
+
"button",
|
|
1263
|
+
{
|
|
1264
|
+
type: "button",
|
|
1265
|
+
role: "option",
|
|
1266
|
+
className: "cv-presence-item",
|
|
1267
|
+
disabled: isBusy,
|
|
1268
|
+
onClick: () => onSelect(agent.extension),
|
|
1269
|
+
children: [
|
|
1270
|
+
/* @__PURE__ */ jsx("span", { className: "cv-presence-dot", "data-state": agent.state, "aria-hidden": true }),
|
|
1271
|
+
/* @__PURE__ */ jsx("span", { className: "cv-presence-name", children: agent.name ?? agent.extension }),
|
|
1272
|
+
/* @__PURE__ */ jsxs("span", { className: "cv-presence-meta", children: [
|
|
1273
|
+
agent.extension,
|
|
1274
|
+
" \xB7 ",
|
|
1275
|
+
PRESENCE_LABEL[agent.state]
|
|
1276
|
+
] })
|
|
1277
|
+
]
|
|
1278
|
+
},
|
|
1279
|
+
agent.userId
|
|
1280
|
+
);
|
|
1281
|
+
}) }),
|
|
1282
|
+
/* @__PURE__ */ jsx("p", { className: "cv-presence-legend", children: "Desconectado: sin conexi\xF3n en el softphone web. Puede atender desde otro dispositivo." })
|
|
784
1283
|
] });
|
|
785
1284
|
}
|
|
786
|
-
var STATUS_LABEL = {
|
|
787
|
-
ringing: "Llamada entrante",
|
|
788
|
-
dialing: "Llamando\u2026",
|
|
789
|
-
early: "Llamando\u2026",
|
|
790
|
-
answering: "Conectando\u2026",
|
|
791
|
-
reconnecting: "Reconectando\u2026",
|
|
792
|
-
held: "En espera"
|
|
793
|
-
};
|
|
794
1285
|
function CallScreen({
|
|
795
1286
|
call,
|
|
796
|
-
incomingCallAction
|
|
1287
|
+
incomingCallAction,
|
|
1288
|
+
keyTones = true
|
|
797
1289
|
}) {
|
|
798
1290
|
const view = useCall(call);
|
|
799
|
-
const
|
|
1291
|
+
const softphone = useSoftphone();
|
|
1292
|
+
const [panel, setPanel] = useState(null);
|
|
1293
|
+
const showTransfer = panel === "transfer";
|
|
1294
|
+
const showKeypad = panel === "keypad";
|
|
1295
|
+
const showDevices = panel === "devices";
|
|
1296
|
+
const togglePanel = (p) => setPanel((cur) => cur === p ? null : p);
|
|
800
1297
|
const [transferTo, setTransferTo] = useState("");
|
|
801
|
-
const [
|
|
1298
|
+
const [consultCall, setConsultCall] = useState(null);
|
|
1299
|
+
const [consultOutcome, setConsultOutcome] = useState(null);
|
|
1300
|
+
const [myUserId, setMyUserId] = useState(null);
|
|
1301
|
+
const consultView = useCall(consultCall);
|
|
1302
|
+
const consultIntentRef = useRef(null);
|
|
802
1303
|
const controlsRef = useRef(null);
|
|
803
1304
|
const now = useTicker(view?.state === "active");
|
|
1305
|
+
const consultNow = useTicker(consultView?.state === "active");
|
|
804
1306
|
const isConnected = view ? view.state === "active" || view.state === "held" || view.state === "reconnecting" : false;
|
|
1307
|
+
const transferPickerActive = isConnected && showTransfer && !consultCall;
|
|
1308
|
+
const presence = usePresence(transferPickerActive);
|
|
1309
|
+
useEffect(() => {
|
|
1310
|
+
const listAgents = softphone.sources?.listAgents;
|
|
1311
|
+
if (!listAgents) return;
|
|
1312
|
+
let cancelled = false;
|
|
1313
|
+
void listAgents().then((d) => !cancelled && setMyUserId(d.me?.userId ?? null)).catch(() => {
|
|
1314
|
+
});
|
|
1315
|
+
return () => {
|
|
1316
|
+
cancelled = true;
|
|
1317
|
+
};
|
|
1318
|
+
}, [softphone]);
|
|
1319
|
+
useEffect(() => {
|
|
1320
|
+
if (!consultCall) return;
|
|
1321
|
+
return consultCall.on("ended" /* Ended */, ({ reason }) => {
|
|
1322
|
+
const intent = consultIntentRef.current;
|
|
1323
|
+
consultIntentRef.current = null;
|
|
1324
|
+
setConsultCall(null);
|
|
1325
|
+
if (!intent) {
|
|
1326
|
+
setConsultOutcome(reason);
|
|
1327
|
+
if (call.state !== "ended") void call.unhold();
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
}, [consultCall, call]);
|
|
1331
|
+
const startConsult = async (target) => {
|
|
1332
|
+
const t = (target ?? transferTo).trim();
|
|
1333
|
+
if (!t) return;
|
|
1334
|
+
setConsultOutcome(null);
|
|
1335
|
+
setConsultCall(await call.startConsult(t));
|
|
1336
|
+
};
|
|
1337
|
+
const selectPresenceAgent = (extension) => {
|
|
1338
|
+
setTransferTo(extension);
|
|
1339
|
+
void startConsult(extension);
|
|
1340
|
+
};
|
|
1341
|
+
const [quickTransfer, setQuickTransfer] = useState(false);
|
|
1342
|
+
const selectPresenceAgentAuto = (extension) => {
|
|
1343
|
+
void call.transfer(extension);
|
|
1344
|
+
};
|
|
1345
|
+
const completeTransfer = async () => {
|
|
1346
|
+
if (!consultCall) return;
|
|
1347
|
+
const target = transferTo.trim();
|
|
1348
|
+
consultIntentRef.current = "complete";
|
|
1349
|
+
await consultCall.hangup();
|
|
1350
|
+
await call.transfer(target);
|
|
1351
|
+
};
|
|
1352
|
+
const cancelConsult = async () => {
|
|
1353
|
+
consultIntentRef.current = "cancel";
|
|
1354
|
+
await call.cancelConsult();
|
|
1355
|
+
};
|
|
805
1356
|
useEffect(() => {
|
|
806
1357
|
if (!showDevices) return;
|
|
807
1358
|
const onKey = (e) => {
|
|
808
|
-
if (e.key === "Escape")
|
|
1359
|
+
if (e.key === "Escape") setPanel(null);
|
|
1360
|
+
};
|
|
1361
|
+
document.addEventListener("keydown", onKey);
|
|
1362
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
1363
|
+
}, [showDevices]);
|
|
1364
|
+
useEffect(() => {
|
|
1365
|
+
if (!showKeypad) return;
|
|
1366
|
+
const onKey = (e) => {
|
|
1367
|
+
if (e.key === "Escape") setPanel(null);
|
|
809
1368
|
};
|
|
810
1369
|
document.addEventListener("keydown", onKey);
|
|
811
1370
|
if (!isConnected) return () => document.removeEventListener("keydown", onKey);
|
|
812
1371
|
const root = controlsRef.current?.getRootNode() ?? document;
|
|
813
1372
|
const onDown = (e) => {
|
|
814
|
-
if (!controlsRef.current?.contains(e.target))
|
|
1373
|
+
if (!controlsRef.current?.contains(e.target)) setPanel(null);
|
|
815
1374
|
};
|
|
816
1375
|
root.addEventListener("pointerdown", onDown);
|
|
817
1376
|
return () => {
|
|
818
1377
|
document.removeEventListener("keydown", onKey);
|
|
819
1378
|
root.removeEventListener("pointerdown", onDown);
|
|
820
1379
|
};
|
|
821
|
-
}, [
|
|
1380
|
+
}, [showKeypad, isConnected]);
|
|
1381
|
+
useEffect(() => {
|
|
1382
|
+
if (view && view.state !== "active") setPanel((cur) => cur === "keypad" ? null : cur);
|
|
1383
|
+
}, [view?.state]);
|
|
822
1384
|
if (!view || view.state === "ended") return null;
|
|
823
1385
|
const s = view.state;
|
|
824
1386
|
const isRinging = s === "ringing";
|
|
@@ -826,14 +1388,27 @@ function CallScreen({
|
|
|
826
1388
|
const name = view.remote.displayName;
|
|
827
1389
|
const number = view.remote.number;
|
|
828
1390
|
const showNumber = number !== name;
|
|
1391
|
+
if (consultCall && consultView) {
|
|
1392
|
+
return /* @__PURE__ */ jsx(
|
|
1393
|
+
ConsultScreen,
|
|
1394
|
+
{
|
|
1395
|
+
mainName: name,
|
|
1396
|
+
consultView,
|
|
1397
|
+
now: consultNow,
|
|
1398
|
+
onReturn: () => void cancelConsult(),
|
|
1399
|
+
onComplete: () => void completeTransfer(),
|
|
1400
|
+
onHangupAll: () => void call.hangup()
|
|
1401
|
+
}
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
829
1404
|
const incomingAction = isRinging && view.remote.contact && incomingCallAction ? incomingCallAction : null;
|
|
830
1405
|
const IncomingActionIcon = incomingAction?.icon ?? ExternalLink;
|
|
831
1406
|
const [rawM = "0", ss = "00"] = formatDuration(elapsedSeconds(view.answeredAt, now)).split(":");
|
|
832
1407
|
const mm = rawM.padStart(2, "0");
|
|
833
1408
|
return /* @__PURE__ */ jsxs("div", { className: "cv-view", children: [
|
|
834
|
-
/* @__PURE__ */ jsx("div", { className: "cv-call", children:
|
|
1409
|
+
/* @__PURE__ */ jsx("div", { className: "cv-call", children: showDevices ? /* @__PURE__ */ jsxs("div", { className: "cv-call-devices-inline", children: [
|
|
835
1410
|
/* @__PURE__ */ jsxs("div", { className: "cv-call-devices-header", children: [
|
|
836
|
-
/* @__PURE__ */ jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () =>
|
|
1411
|
+
/* @__PURE__ */ jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setPanel(null), children: [
|
|
837
1412
|
/* @__PURE__ */ jsx(ArrowLeft, { size: 13, "aria-hidden": true }),
|
|
838
1413
|
"Volver"
|
|
839
1414
|
] }),
|
|
@@ -841,10 +1416,82 @@ function CallScreen({
|
|
|
841
1416
|
] }),
|
|
842
1417
|
/* @__PURE__ */ jsxs("p", { className: "cv-call-devices-context", children: [
|
|
843
1418
|
showNumber ? `${number} \xB7 ` : "",
|
|
844
|
-
STATUS_LABEL[s] ?? ""
|
|
1419
|
+
s === "active" ? `${mm}:${ss}` : STATUS_LABEL[s] ?? ""
|
|
845
1420
|
] }),
|
|
846
1421
|
/* @__PURE__ */ jsx("div", { className: "cv-settings-section-body", children: /* @__PURE__ */ jsx(DevicePickers, {}) })
|
|
847
|
-
] }) :
|
|
1422
|
+
] }) : transferPickerActive ? (
|
|
1423
|
+
/*
|
|
1424
|
+
Found live (2026-09-29): the agent list a moment ago picked between showing ONE agent row
|
|
1425
|
+
or forcing a scroll — `.cv-presence-list`'s own `max-height` had already been trimmed twice
|
|
1426
|
+
(T7, then T3) chasing that, and both times the room it was chasing lived somewhere else.
|
|
1427
|
+
The full avatar/name/giant-timer block above stayed on screen, unconditionally, the WHOLE
|
|
1428
|
+
time the picker was open — exactly the anti-pattern `.cv-call-devices-inline` (above) was
|
|
1429
|
+
already built to avoid for the pre-connect device picker: "an inline swap, not an overlay,
|
|
1430
|
+
so it gets the full box, not a leftover sliver." The picker never got that same treatment.
|
|
1431
|
+
Measured live (a temporary harness, `playground/react/picker-harness.*`, real Chromium at
|
|
1432
|
+
this SDK's own real embed footprint, 310×520): the avatar+name+timer block alone cost
|
|
1433
|
+
~150px of `.cv-call`'s ~340px of usable height — more than the entire agent list, the mode
|
|
1434
|
+
toggle, and the free-text row combined. Reusing `.cv-call-devices-inline`'s OWN "who's
|
|
1435
|
+
calling doesn't disappear" compact context line (below) instead of the full block gives
|
|
1436
|
+
that ~150px back to the one thing this screen is actually for right now: the list.
|
|
1437
|
+
*/
|
|
1438
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-transfer", children: [
|
|
1439
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-transfer-context", children: [
|
|
1440
|
+
/* @__PURE__ */ jsx("span", { className: "cv-call-devices-title", children: name }),
|
|
1441
|
+
/* @__PURE__ */ jsxs("p", { className: "cv-call-devices-context", children: [
|
|
1442
|
+
showNumber ? `${number} \xB7 ` : "",
|
|
1443
|
+
s === "active" ? `${mm}:${ss}` : STATUS_LABEL[s] ?? ""
|
|
1444
|
+
] })
|
|
1445
|
+
] }),
|
|
1446
|
+
consultOutcome ? /* @__PURE__ */ jsx("p", { className: "cv-dial-outcome", role: "status", "data-shown": "true", children: consultFailText(consultOutcome) }) : null,
|
|
1447
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-transfer-row", children: [
|
|
1448
|
+
/* @__PURE__ */ jsx(
|
|
1449
|
+
"input",
|
|
1450
|
+
{
|
|
1451
|
+
className: "cv-search",
|
|
1452
|
+
value: transferTo,
|
|
1453
|
+
onChange: (e) => setTransferTo(e.target.value),
|
|
1454
|
+
placeholder: "Nombre, ext. o n\xFAmero",
|
|
1455
|
+
"aria-label": "Transferir a",
|
|
1456
|
+
autoFocus: true
|
|
1457
|
+
}
|
|
1458
|
+
),
|
|
1459
|
+
/* @__PURE__ */ jsx(
|
|
1460
|
+
"button",
|
|
1461
|
+
{
|
|
1462
|
+
className: "cv-btn cv-btn--accent",
|
|
1463
|
+
disabled: !transferTo.trim(),
|
|
1464
|
+
onClick: () => void startConsult(),
|
|
1465
|
+
children: transferTo.trim() ? `Llamar a ${truncateForButton(transferTo.trim())}` : "Llamar"
|
|
1466
|
+
}
|
|
1467
|
+
)
|
|
1468
|
+
] }),
|
|
1469
|
+
/* @__PURE__ */ jsx("div", { className: "cv-segmented", role: "tablist", "aria-label": "Modo de transferencia", children: [
|
|
1470
|
+
{ key: "consult", label: "Hablar primero" },
|
|
1471
|
+
{ key: "auto", label: "Transferir directo" }
|
|
1472
|
+
].map(({ key, label }) => /* @__PURE__ */ jsx(
|
|
1473
|
+
"button",
|
|
1474
|
+
{
|
|
1475
|
+
type: "button",
|
|
1476
|
+
role: "tab",
|
|
1477
|
+
"aria-selected": quickTransfer === (key === "auto"),
|
|
1478
|
+
className: "cv-segmented-btn",
|
|
1479
|
+
onClick: () => setQuickTransfer(key === "auto"),
|
|
1480
|
+
children: label
|
|
1481
|
+
},
|
|
1482
|
+
key
|
|
1483
|
+
)) }),
|
|
1484
|
+
/* @__PURE__ */ jsx(
|
|
1485
|
+
TransferPicker,
|
|
1486
|
+
{
|
|
1487
|
+
agents: presence,
|
|
1488
|
+
myUserId,
|
|
1489
|
+
query: transferTo,
|
|
1490
|
+
onSelect: quickTransfer ? selectPresenceAgentAuto : selectPresenceAgent
|
|
1491
|
+
}
|
|
1492
|
+
)
|
|
1493
|
+
] })
|
|
1494
|
+
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
848
1495
|
/* @__PURE__ */ jsxs("div", { className: "cv-avatar-wrap", children: [
|
|
849
1496
|
isRinging ? /* @__PURE__ */ jsx("span", { className: "cv-avatar-ping", "aria-hidden": true }) : null,
|
|
850
1497
|
/* @__PURE__ */ jsx("div", { className: "cv-avatar", "aria-hidden": true, children: initials(name) }),
|
|
@@ -857,7 +1504,7 @@ function CallScreen({
|
|
|
857
1504
|
/* @__PURE__ */ jsx("i", { children: ":" }),
|
|
858
1505
|
/* @__PURE__ */ jsx("b", { children: ss })
|
|
859
1506
|
] }) : /* @__PURE__ */ jsx("div", { className: "cv-call-status", children: s === "held" ? /* @__PURE__ */ jsx("span", { className: "cv-badge-hold", children: STATUS_LABEL[s] }) : STATUS_LABEL[s] ?? "" }),
|
|
860
|
-
|
|
1507
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-call-chips", children: [
|
|
861
1508
|
incomingAction ? /* @__PURE__ */ jsxs(
|
|
862
1509
|
"button",
|
|
863
1510
|
{
|
|
@@ -870,36 +1517,11 @@ function CallScreen({
|
|
|
870
1517
|
]
|
|
871
1518
|
}
|
|
872
1519
|
) : null,
|
|
873
|
-
/* @__PURE__ */ jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () =>
|
|
1520
|
+
/* @__PURE__ */ jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setPanel("devices"), children: [
|
|
874
1521
|
/* @__PURE__ */ jsx(Headphones, { size: 13, "aria-hidden": true }),
|
|
875
1522
|
"Dispositivos"
|
|
876
1523
|
] })
|
|
877
|
-
] })
|
|
878
|
-
inCall && showTransfer ? /* @__PURE__ */ jsxs("div", { className: "cv-transfer", children: [
|
|
879
|
-
/* @__PURE__ */ jsx(
|
|
880
|
-
"input",
|
|
881
|
-
{
|
|
882
|
-
className: "cv-search",
|
|
883
|
-
value: transferTo,
|
|
884
|
-
onChange: (e) => setTransferTo(e.target.value),
|
|
885
|
-
placeholder: "Extensi\xF3n o n\xFAmero",
|
|
886
|
-
"aria-label": "Transferir a",
|
|
887
|
-
autoFocus: true
|
|
888
|
-
}
|
|
889
|
-
),
|
|
890
|
-
/* @__PURE__ */ jsx(
|
|
891
|
-
"button",
|
|
892
|
-
{
|
|
893
|
-
className: "cv-btn cv-btn--accent",
|
|
894
|
-
disabled: !transferTo.trim(),
|
|
895
|
-
onClick: () => {
|
|
896
|
-
void call.transfer(transferTo.trim());
|
|
897
|
-
setShowTransfer(false);
|
|
898
|
-
},
|
|
899
|
-
children: "Transferir"
|
|
900
|
-
}
|
|
901
|
-
)
|
|
902
|
-
] }) : null
|
|
1524
|
+
] })
|
|
903
1525
|
] }) }),
|
|
904
1526
|
/* @__PURE__ */ jsx("div", { className: "cv-controls", ref: controlsRef, children: isRinging ? /* @__PURE__ */ jsxs("div", { className: "cv-controls-primary", children: [
|
|
905
1527
|
/* @__PURE__ */ jsxs("div", { className: "cv-ctrl-item", children: [
|
|
@@ -958,17 +1580,19 @@ function CallScreen({
|
|
|
958
1580
|
label: "Transferir",
|
|
959
1581
|
pressed: showTransfer,
|
|
960
1582
|
tone: "accent",
|
|
961
|
-
onClick: () =>
|
|
1583
|
+
onClick: () => togglePanel("transfer")
|
|
962
1584
|
}
|
|
963
1585
|
),
|
|
964
1586
|
/* @__PURE__ */ jsx(
|
|
965
1587
|
Control,
|
|
966
1588
|
{
|
|
967
|
-
icon:
|
|
968
|
-
label: "
|
|
969
|
-
pressed:
|
|
1589
|
+
icon: Grid3x3,
|
|
1590
|
+
label: "Teclado",
|
|
1591
|
+
pressed: showKeypad,
|
|
970
1592
|
tone: "accent",
|
|
971
|
-
|
|
1593
|
+
disabled: s !== "active",
|
|
1594
|
+
title: s === "held" ? "Reanuda la llamada para usar el teclado" : s === "reconnecting" ? "No disponible mientras se reconecta" : void 0,
|
|
1595
|
+
onClick: () => togglePanel("keypad")
|
|
972
1596
|
}
|
|
973
1597
|
)
|
|
974
1598
|
] }) : null,
|
|
@@ -986,7 +1610,22 @@ function CallScreen({
|
|
|
986
1610
|
),
|
|
987
1611
|
/* @__PURE__ */ jsx("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Colgar" })
|
|
988
1612
|
] }) }),
|
|
989
|
-
inCall &&
|
|
1613
|
+
inCall && showKeypad ? /* @__PURE__ */ jsx("div", { className: "cv-keypad-popover cv-keypad-popover--above", role: "dialog", "aria-label": "Teclado", children: KEYPAD_KEYS.map(([key, letters]) => /* @__PURE__ */ jsxs(
|
|
1614
|
+
"button",
|
|
1615
|
+
{
|
|
1616
|
+
type: "button",
|
|
1617
|
+
className: "cv-key",
|
|
1618
|
+
onClick: () => {
|
|
1619
|
+
call.sendDigits(key);
|
|
1620
|
+
if (keyTones) playKeyTone(key);
|
|
1621
|
+
},
|
|
1622
|
+
children: [
|
|
1623
|
+
/* @__PURE__ */ jsx("b", { children: key }),
|
|
1624
|
+
letters ? /* @__PURE__ */ jsx("small", { children: letters }) : null
|
|
1625
|
+
]
|
|
1626
|
+
},
|
|
1627
|
+
key
|
|
1628
|
+
)) }) : null
|
|
990
1629
|
] }) })
|
|
991
1630
|
] });
|
|
992
1631
|
}
|
|
@@ -1653,159 +2292,48 @@ function MicPrompt({
|
|
|
1653
2292
|
}
|
|
1654
2293
|
if (state === "denied") {
|
|
1655
2294
|
const guide = unblockGuide();
|
|
1656
|
-
return /* @__PURE__ */ jsxs("div", { className: "cv-gate", "data-state": state, "data-tone": "danger", children: [
|
|
1657
|
-
/* @__PURE__ */ jsx("span", { className: "cv-gate-mark", children: /* @__PURE__ */ jsx(MicOff, { size: 22 }) }),
|
|
1658
|
-
/* @__PURE__ */ jsx("h3", { className: "cv-gate-title", children: "Micr\xF3fono bloqueado" }),
|
|
1659
|
-
/* @__PURE__ */ jsx("p", { children: "Perm\xEDtelo en tu navegador para poder llamar." }),
|
|
1660
|
-
/* @__PURE__ */ jsxs("details", { className: "cv-help", children: [
|
|
1661
|
-
/* @__PURE__ */ jsxs("summary", { children: [
|
|
1662
|
-
/* @__PURE__ */ jsx(CircleHelp, { size: 13, "aria-hidden": true }),
|
|
1663
|
-
"C\xF3mo permitirlo"
|
|
1664
|
-
] }),
|
|
1665
|
-
/* @__PURE__ */ jsx("ol", { children: guide.steps.map((step) => /* @__PURE__ */ jsx("li", { children: step }, step)) }),
|
|
1666
|
-
guide.url ? /* @__PURE__ */ jsxs("a", { href: guide.url, target: "_blank", rel: "noreferrer noopener", children: [
|
|
1667
|
-
"Gu\xEDa del navegador",
|
|
1668
|
-
/* @__PURE__ */ jsx(ExternalLink, { size: 11, "aria-hidden": true })
|
|
1669
|
-
] }) : null
|
|
1670
|
-
] })
|
|
1671
|
-
] });
|
|
1672
|
-
}
|
|
1673
|
-
if (state === "warming") {
|
|
1674
|
-
return /* @__PURE__ */ jsxs("div", { className: "cv-gate", "data-state": state, children: [
|
|
1675
|
-
/* @__PURE__ */ jsx("span", { className: "cv-gate-mark", children: /* @__PURE__ */ jsx(Mic, { size: 22 }) }),
|
|
1676
|
-
/* @__PURE__ */ jsx("h3", { className: "cv-gate-title", children: "Permite el micr\xF3fono" }),
|
|
1677
|
-
/* @__PURE__ */ jsx("p", { children: "Acepta el aviso que muestra el navegador para continuar." })
|
|
1678
|
-
] });
|
|
1679
|
-
}
|
|
1680
|
-
if (state === "checking") {
|
|
1681
|
-
return /* @__PURE__ */ jsxs("div", { className: "cv-gate", "data-state": state, children: [
|
|
1682
|
-
/* @__PURE__ */ jsx("span", { className: "cv-gate-mark", children: /* @__PURE__ */ jsx(Mic, { size: 22 }) }),
|
|
1683
|
-
/* @__PURE__ */ jsx("p", { children: "Preparando el micr\xF3fono\u2026" })
|
|
1684
|
-
] });
|
|
1685
|
-
}
|
|
1686
|
-
return /* @__PURE__ */ jsxs("div", { className: "cv-gate", "data-state": state, children: [
|
|
1687
|
-
/* @__PURE__ */ jsx("span", { className: "cv-gate-mark", children: /* @__PURE__ */ jsx(Mic, { size: 22 }) }),
|
|
1688
|
-
/* @__PURE__ */ jsx("h3", { className: "cv-gate-title", children: "Permite el micr\xF3fono" }),
|
|
1689
|
-
/* @__PURE__ */ jsx("p", { children: "Lo necesitamos para que puedas hablar durante las llamadas." }),
|
|
1690
|
-
/* @__PURE__ */ jsx("button", { className: "cv-btn cv-btn--accent", onClick: onRequest, children: "Permitir micr\xF3fono" })
|
|
1691
|
-
] });
|
|
1692
|
-
}
|
|
1693
|
-
function MicPermissionGate({ children }) {
|
|
1694
|
-
const { state, request } = useMicPermission();
|
|
1695
|
-
if (state === "ready") return /* @__PURE__ */ jsx(Fragment, { children });
|
|
1696
|
-
return /* @__PURE__ */ jsx("div", { className: "cv-softphone", children: /* @__PURE__ */ jsx(MicPrompt, { state, onRequest: () => void request() }) });
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
// src/ui/lib/error-text.ts
|
|
1700
|
-
var ERROR_TEXT = {
|
|
1701
|
-
// ── Engine lifecycle ────────────────────────────────────────────────────────
|
|
1702
|
-
// Integrator mistakes rather than user-facing failures, but they must still read as something
|
|
1703
|
-
// rather than as a blank panel if one ever reaches the UI.
|
|
1704
|
-
"engine/duplicate-instance": {
|
|
1705
|
-
title: "Tel\xE9fono duplicado",
|
|
1706
|
-
body: "Hay otro tel\xE9fono abierto en esta p\xE1gina. Cierra uno y vuelve a cargar."
|
|
1707
|
-
},
|
|
1708
|
-
"engine/not-started": { title: "El tel\xE9fono a\xFAn no est\xE1 listo" },
|
|
1709
|
-
"engine/destroyed": {
|
|
1710
|
-
title: "Tel\xE9fono cerrado",
|
|
1711
|
-
body: "Vuelve a cargar la p\xE1gina para usarlo de nuevo."
|
|
1712
|
-
},
|
|
1713
|
-
// ── Auth ────────────────────────────────────────────────────────────────────
|
|
1714
|
-
// Never "token": the user has no idea what one is. What they can act on is the session.
|
|
1715
|
-
"auth/token-fetch-failed": {
|
|
1716
|
-
title: "Sin conexi\xF3n con el servidor",
|
|
1717
|
-
body: "No se ha podido activar el tel\xE9fono. Reint\xE9ntalo en unos segundos."
|
|
1718
|
-
},
|
|
1719
|
-
"auth/token-rejected": {
|
|
1720
|
-
title: "Sesi\xF3n caducada",
|
|
1721
|
-
body: "Vuelve a iniciar sesi\xF3n para poder llamar."
|
|
1722
|
-
},
|
|
1723
|
-
// ── Media ───────────────────────────────────────────────────────────────────
|
|
1724
|
-
// Verbatim from MicPermissionGate, which already says these well and says them first.
|
|
1725
|
-
//
|
|
1726
|
-
// NEVER ACTUALLY SHOWN via this table — these four codes (through `media/playback-blocked`, below)
|
|
1727
|
-
// are never raised as a `SoftphoneError` at all. See the fuller comment on each in `core/errors.ts`
|
|
1728
|
-
// for why. Copy kept here anyway so the catalogue and this file stay the same shape as every other
|
|
1729
|
-
// code — `error-text.test.ts` would fail the build the moment they drifted.
|
|
1730
|
-
"media/permission-denied": {
|
|
1731
|
-
title: "Micr\xF3fono bloqueado",
|
|
1732
|
-
body: "Perm\xEDtelo en tu navegador para poder llamar."
|
|
1733
|
-
},
|
|
1734
|
-
"media/no-input-device": {
|
|
1735
|
-
title: "Sin micr\xF3fono",
|
|
1736
|
-
body: "Conecta un micr\xF3fono y vuelve a intentarlo."
|
|
1737
|
-
},
|
|
1738
|
-
"media/webrtc-unsupported": {
|
|
1739
|
-
title: "Llamadas no disponibles",
|
|
1740
|
-
body: "Este navegador no admite llamadas. Prueba con Chrome, Edge o Firefox."
|
|
1741
|
-
},
|
|
1742
|
-
"media/playback-blocked": { title: "Toca la pantalla para activar el sonido" },
|
|
1743
|
-
// ── Network / signalling ────────────────────────────────────────────────────
|
|
1744
|
-
// "Centralita" rather than "SIP" or "registro": the user knows what a phone system is.
|
|
1745
|
-
"net/registration-failed": {
|
|
1746
|
-
title: "No se pudo conectar",
|
|
1747
|
-
body: "No hemos podido conectar con la centralita. Reintenta en unos segundos."
|
|
1748
|
-
},
|
|
1749
|
-
"net/transport-failed": {
|
|
1750
|
-
title: "Conexi\xF3n perdida",
|
|
1751
|
-
body: "Se ha perdido la conexi\xF3n con la centralita."
|
|
1752
|
-
},
|
|
1753
|
-
// NEVER ACTUALLY SHOWN either — CallScreen's own "Reconectando…" already covers this, off the
|
|
1754
|
-
// call's `reconnecting` state rather than this channel. See the comment in `core/errors.ts`.
|
|
1755
|
-
"net/ice-failed": { title: "Recuperando el audio\u2026" },
|
|
1756
|
-
// ── Call ────────────────────────────────────────────────────────────────────
|
|
1757
|
-
/*
|
|
1758
|
-
THE VOICE RULE FOR THIS BLOCK: say what HAPPENED, not what it is called.
|
|
1759
|
-
|
|
1760
|
-
These were status labels — "Llamada perdida", "Llamada rechazada", "Sin respuesta". Each is a
|
|
1761
|
-
name for a category rather than an account of an event, which is why they read as a database enum
|
|
1762
|
-
someone forgot to translate into human. Worse, a label hides the ACTOR: "Llamada rechazada" never
|
|
1763
|
-
says who rejected what, so the user reading it after declining a call and the user reading it
|
|
1764
|
-
after being declined see the same three words.
|
|
1765
|
-
|
|
1766
|
-
So each one is a short sentence about what the other person, or the phone, did — the form
|
|
1767
|
-
`call/busy` already had, and the reason it was the only one here that read clearly.
|
|
1768
|
-
|
|
1769
|
-
Impersonal third-person plural ("contestaron", "colgaron") throughout: the user does not know who
|
|
1770
|
-
is at the other end, and it is what Spanish uses when the subject is unknown. Mixing it with the
|
|
1771
|
-
singular across neighbouring lines reads as carelessness.
|
|
1772
|
-
|
|
1773
|
-
LENGTH IS A HARD CONSTRAINT, not a preference. `.cv-dial-outcome` is one line at a fixed
|
|
1774
|
-
`1.15rem`, and the keypad below it is `flex: 1` — a title that wraps resizes every key and shifts
|
|
1775
|
-
the grid under a finger already reaching for it. `error-text.test.ts` caps this at 48 characters.
|
|
1776
|
-
*/
|
|
1777
|
-
"call/not-answerable": { title: "Esa llamada ya ha terminado" },
|
|
1778
|
-
/*
|
|
1779
|
-
NOT "Comunicando", which is what this said and what sent us looking. SIP 486 is both "the line is
|
|
1780
|
-
engaged" AND what almost every endpoint sends when a person presses decline — including our own
|
|
1781
|
-
`reject()`. One code, two events, no way to tell them apart, so the copy has to hold both rather
|
|
1782
|
-
than assert the less likely one. `call/rejected` can be definite: an endpoint sending 603 Decline
|
|
1783
|
-
means exactly that.
|
|
1784
|
-
*/
|
|
1785
|
-
"call/busy": { title: "No contestaron o la l\xEDnea estaba ocupada" },
|
|
1786
|
-
"call/no-answer": { title: "No contestaron" },
|
|
1787
|
-
"call/rejected": { title: "No aceptaron tu llamada" },
|
|
1788
|
-
/*
|
|
1789
|
-
The legacy CRM said "La persona que llamaba colgó antes de que pudieras contestar." — the right
|
|
1790
|
-
fact, at three times the width of the line that has to hold it. This keeps the fact and drops the
|
|
1791
|
-
words: the user needs to know the call was FOR them and that nobody is waiting on the line.
|
|
1792
|
-
"Llamada perdida" alone tested badly for exactly the label reason above — it names the category
|
|
1793
|
-
without saying anyone rang.
|
|
1794
|
-
*/
|
|
1795
|
-
"call/missed": { title: "Colgaron antes de que pudieras contestar" },
|
|
1796
|
-
// "No se pudo completar la llamada" was our own "something went wrong": true of every failure and
|
|
1797
|
-
// useful for none. The number is what the user can actually act on.
|
|
1798
|
-
"call/failed": { title: "No se pudo conectar con ese n\xFAmero" },
|
|
1799
|
-
// The load-bearing half is the SECOND sentence: the original call is still up, and a user who
|
|
1800
|
-
// thinks they dropped it will hang up on a client to "fix" it.
|
|
1801
|
-
"call/transfer-failed": { title: "No se pudo transferir. Sigues en la llamada" },
|
|
1802
|
-
"call/unsupported": { title: "Esta funci\xF3n no est\xE1 disponible" },
|
|
1803
|
-
// ── Provider ────────────────────────────────────────────────────────────────
|
|
1804
|
-
"provider/provision-failed": {
|
|
1805
|
-
title: "No se pudo activar el tel\xE9fono",
|
|
1806
|
-
body: "Si el problema contin\xFAa, contacta con soporte."
|
|
2295
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-gate", "data-state": state, "data-tone": "danger", children: [
|
|
2296
|
+
/* @__PURE__ */ jsx("span", { className: "cv-gate-mark", children: /* @__PURE__ */ jsx(MicOff, { size: 22 }) }),
|
|
2297
|
+
/* @__PURE__ */ jsx("h3", { className: "cv-gate-title", children: "Micr\xF3fono bloqueado" }),
|
|
2298
|
+
/* @__PURE__ */ jsx("p", { children: "Perm\xEDtelo en tu navegador para poder llamar." }),
|
|
2299
|
+
/* @__PURE__ */ jsxs("details", { className: "cv-help", children: [
|
|
2300
|
+
/* @__PURE__ */ jsxs("summary", { children: [
|
|
2301
|
+
/* @__PURE__ */ jsx(CircleHelp, { size: 13, "aria-hidden": true }),
|
|
2302
|
+
"C\xF3mo permitirlo"
|
|
2303
|
+
] }),
|
|
2304
|
+
/* @__PURE__ */ jsx("ol", { children: guide.steps.map((step) => /* @__PURE__ */ jsx("li", { children: step }, step)) }),
|
|
2305
|
+
guide.url ? /* @__PURE__ */ jsxs("a", { href: guide.url, target: "_blank", rel: "noreferrer noopener", children: [
|
|
2306
|
+
"Gu\xEDa del navegador",
|
|
2307
|
+
/* @__PURE__ */ jsx(ExternalLink, { size: 11, "aria-hidden": true })
|
|
2308
|
+
] }) : null
|
|
2309
|
+
] })
|
|
2310
|
+
] });
|
|
1807
2311
|
}
|
|
1808
|
-
|
|
2312
|
+
if (state === "warming") {
|
|
2313
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-gate", "data-state": state, children: [
|
|
2314
|
+
/* @__PURE__ */ jsx("span", { className: "cv-gate-mark", children: /* @__PURE__ */ jsx(Mic, { size: 22 }) }),
|
|
2315
|
+
/* @__PURE__ */ jsx("h3", { className: "cv-gate-title", children: "Permite el micr\xF3fono" }),
|
|
2316
|
+
/* @__PURE__ */ jsx("p", { children: "Acepta el aviso que muestra el navegador para continuar." })
|
|
2317
|
+
] });
|
|
2318
|
+
}
|
|
2319
|
+
if (state === "checking") {
|
|
2320
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-gate", "data-state": state, children: [
|
|
2321
|
+
/* @__PURE__ */ jsx("span", { className: "cv-gate-mark", children: /* @__PURE__ */ jsx(Mic, { size: 22 }) }),
|
|
2322
|
+
/* @__PURE__ */ jsx("p", { children: "Preparando el micr\xF3fono\u2026" })
|
|
2323
|
+
] });
|
|
2324
|
+
}
|
|
2325
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-gate", "data-state": state, children: [
|
|
2326
|
+
/* @__PURE__ */ jsx("span", { className: "cv-gate-mark", children: /* @__PURE__ */ jsx(Mic, { size: 22 }) }),
|
|
2327
|
+
/* @__PURE__ */ jsx("h3", { className: "cv-gate-title", children: "Permite el micr\xF3fono" }),
|
|
2328
|
+
/* @__PURE__ */ jsx("p", { children: "Lo necesitamos para que puedas hablar durante las llamadas." }),
|
|
2329
|
+
/* @__PURE__ */ jsx("button", { className: "cv-btn cv-btn--accent", onClick: onRequest, children: "Permitir micr\xF3fono" })
|
|
2330
|
+
] });
|
|
2331
|
+
}
|
|
2332
|
+
function MicPermissionGate({ children }) {
|
|
2333
|
+
const { state, request } = useMicPermission();
|
|
2334
|
+
if (state === "ready") return /* @__PURE__ */ jsx(Fragment, { children });
|
|
2335
|
+
return /* @__PURE__ */ jsx("div", { className: "cv-softphone", children: /* @__PURE__ */ jsx(MicPrompt, { state, onRequest: () => void request() }) });
|
|
2336
|
+
}
|
|
1809
2337
|
function ErrorGate({
|
|
1810
2338
|
error,
|
|
1811
2339
|
onRetry
|
|
@@ -1862,203 +2390,6 @@ function useCallOutcome() {
|
|
|
1862
2390
|
clear
|
|
1863
2391
|
};
|
|
1864
2392
|
}
|
|
1865
|
-
|
|
1866
|
-
// src/ui/audio/audio.ts
|
|
1867
|
-
var TONE_HZ = 425;
|
|
1868
|
-
function createRemoteAudio() {
|
|
1869
|
-
const el = document.createElement("audio");
|
|
1870
|
-
el.autoplay = true;
|
|
1871
|
-
el.style.display = "none";
|
|
1872
|
-
document.body.appendChild(el);
|
|
1873
|
-
return {
|
|
1874
|
-
attach(stream) {
|
|
1875
|
-
el.srcObject = stream;
|
|
1876
|
-
void el.play().catch(() => {
|
|
1877
|
-
});
|
|
1878
|
-
},
|
|
1879
|
-
detach() {
|
|
1880
|
-
el.srcObject = null;
|
|
1881
|
-
},
|
|
1882
|
-
destroy() {
|
|
1883
|
-
el.srcObject = null;
|
|
1884
|
-
el.remove();
|
|
1885
|
-
},
|
|
1886
|
-
async setOutputDevice(deviceId) {
|
|
1887
|
-
if (typeof el.setSinkId !== "function") return;
|
|
1888
|
-
try {
|
|
1889
|
-
await el.setSinkId(deviceId ?? "");
|
|
1890
|
-
} catch {
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
};
|
|
1894
|
-
}
|
|
1895
|
-
var shared = null;
|
|
1896
|
-
function sharedContext() {
|
|
1897
|
-
if (shared) return shared;
|
|
1898
|
-
const Ctor = typeof AudioContext !== "undefined" ? AudioContext : globalThis.webkitAudioContext;
|
|
1899
|
-
if (!Ctor) return null;
|
|
1900
|
-
shared = new Ctor();
|
|
1901
|
-
return shared;
|
|
1902
|
-
}
|
|
1903
|
-
function primeAudio() {
|
|
1904
|
-
const ctx = sharedContext();
|
|
1905
|
-
if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
|
|
1906
|
-
});
|
|
1907
|
-
}
|
|
1908
|
-
function playRingtone() {
|
|
1909
|
-
const ctx = sharedContext();
|
|
1910
|
-
if (!ctx) return { stop() {
|
|
1911
|
-
} };
|
|
1912
|
-
const gain = ctx.createGain();
|
|
1913
|
-
gain.gain.value = 0;
|
|
1914
|
-
gain.connect(ctx.destination);
|
|
1915
|
-
const osc = ctx.createOscillator();
|
|
1916
|
-
osc.type = "sine";
|
|
1917
|
-
osc.frequency.value = TONE_HZ;
|
|
1918
|
-
osc.connect(gain);
|
|
1919
|
-
osc.start();
|
|
1920
|
-
const LEVEL = 0.8;
|
|
1921
|
-
const RAMP = 0.02;
|
|
1922
|
-
const burst = (at, dur) => {
|
|
1923
|
-
gain.gain.setValueAtTime(0, at);
|
|
1924
|
-
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
1925
|
-
gain.gain.setValueAtTime(LEVEL, at + dur - RAMP);
|
|
1926
|
-
gain.gain.linearRampToValueAtTime(0, at + dur);
|
|
1927
|
-
};
|
|
1928
|
-
const scheduleCycle = (at) => {
|
|
1929
|
-
burst(at, 0.4);
|
|
1930
|
-
burst(at + 0.6, 0.4);
|
|
1931
|
-
return 3;
|
|
1932
|
-
};
|
|
1933
|
-
const LOOKAHEAD = 0.5;
|
|
1934
|
-
const TICK_MS = 150;
|
|
1935
|
-
const LEAD = 0.08;
|
|
1936
|
-
let stopped = false;
|
|
1937
|
-
let timer = null;
|
|
1938
|
-
let nextAt = 0;
|
|
1939
|
-
const pump = () => {
|
|
1940
|
-
while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleCycle(nextAt);
|
|
1941
|
-
};
|
|
1942
|
-
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
1943
|
-
}).finally(() => {
|
|
1944
|
-
if (stopped) return;
|
|
1945
|
-
nextAt = ctx.currentTime + LEAD;
|
|
1946
|
-
pump();
|
|
1947
|
-
timer = setInterval(pump, TICK_MS);
|
|
1948
|
-
});
|
|
1949
|
-
return {
|
|
1950
|
-
stop() {
|
|
1951
|
-
stopped = true;
|
|
1952
|
-
if (timer) clearInterval(timer);
|
|
1953
|
-
try {
|
|
1954
|
-
osc.stop();
|
|
1955
|
-
} catch {
|
|
1956
|
-
}
|
|
1957
|
-
try {
|
|
1958
|
-
gain.disconnect();
|
|
1959
|
-
} catch {
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
};
|
|
1963
|
-
}
|
|
1964
|
-
function playCallEndedTone() {
|
|
1965
|
-
const ctx = sharedContext();
|
|
1966
|
-
if (!ctx) return;
|
|
1967
|
-
const gain = ctx.createGain();
|
|
1968
|
-
gain.gain.value = 0;
|
|
1969
|
-
gain.connect(ctx.destination);
|
|
1970
|
-
const osc = ctx.createOscillator();
|
|
1971
|
-
osc.type = "sine";
|
|
1972
|
-
osc.frequency.value = TONE_HZ;
|
|
1973
|
-
osc.connect(gain);
|
|
1974
|
-
osc.start();
|
|
1975
|
-
const LEVEL = 0.55;
|
|
1976
|
-
const RAMP = 8e-3;
|
|
1977
|
-
const BEEP = 0.25;
|
|
1978
|
-
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
1979
|
-
}).finally(() => {
|
|
1980
|
-
const at = ctx.currentTime + 0.02;
|
|
1981
|
-
gain.gain.setValueAtTime(0, at);
|
|
1982
|
-
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
1983
|
-
gain.gain.setValueAtTime(LEVEL, at + BEEP - RAMP);
|
|
1984
|
-
gain.gain.linearRampToValueAtTime(0, at + BEEP);
|
|
1985
|
-
const totalMs = (at + BEEP - ctx.currentTime) * 1e3;
|
|
1986
|
-
setTimeout(() => {
|
|
1987
|
-
try {
|
|
1988
|
-
osc.stop();
|
|
1989
|
-
} catch {
|
|
1990
|
-
}
|
|
1991
|
-
try {
|
|
1992
|
-
gain.disconnect();
|
|
1993
|
-
} catch {
|
|
1994
|
-
}
|
|
1995
|
-
}, totalMs);
|
|
1996
|
-
});
|
|
1997
|
-
}
|
|
1998
|
-
var ringbackUrl = null;
|
|
1999
|
-
function ringbackSource() {
|
|
2000
|
-
if (ringbackUrl) return ringbackUrl;
|
|
2001
|
-
const sampleRate = 48e3;
|
|
2002
|
-
const channels = 2;
|
|
2003
|
-
const bytesPerSample = 2;
|
|
2004
|
-
const amp = 0.5;
|
|
2005
|
-
const toneSec = 1.5;
|
|
2006
|
-
const silenceSec = 3;
|
|
2007
|
-
const totalFrames = Math.round((toneSec + silenceSec) * sampleRate);
|
|
2008
|
-
const toneFrames = Math.round(toneSec * sampleRate);
|
|
2009
|
-
const ramp = Math.round(0.02 * sampleRate);
|
|
2010
|
-
const blockAlign = channels * bytesPerSample;
|
|
2011
|
-
const dataSize = totalFrames * blockAlign;
|
|
2012
|
-
const buffer = new ArrayBuffer(44 + dataSize);
|
|
2013
|
-
const view = new DataView(buffer);
|
|
2014
|
-
const str = (off, s) => {
|
|
2015
|
-
for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
|
|
2016
|
-
};
|
|
2017
|
-
str(0, "RIFF");
|
|
2018
|
-
view.setUint32(4, 36 + dataSize, true);
|
|
2019
|
-
str(8, "WAVE");
|
|
2020
|
-
str(12, "fmt ");
|
|
2021
|
-
view.setUint32(16, 16, true);
|
|
2022
|
-
view.setUint16(20, 1, true);
|
|
2023
|
-
view.setUint16(22, channels, true);
|
|
2024
|
-
view.setUint32(24, sampleRate, true);
|
|
2025
|
-
view.setUint32(28, sampleRate * blockAlign, true);
|
|
2026
|
-
view.setUint16(32, blockAlign, true);
|
|
2027
|
-
view.setUint16(34, bytesPerSample * 8, true);
|
|
2028
|
-
str(36, "data");
|
|
2029
|
-
view.setUint32(40, dataSize, true);
|
|
2030
|
-
for (let i = 0; i < totalFrames; i++) {
|
|
2031
|
-
let sample = 0;
|
|
2032
|
-
if (i < toneFrames) {
|
|
2033
|
-
let env = 1;
|
|
2034
|
-
if (i < ramp) env = i / ramp;
|
|
2035
|
-
else if (i > toneFrames - ramp) env = Math.max(0, (toneFrames - i) / ramp);
|
|
2036
|
-
sample = Math.sin(2 * Math.PI * TONE_HZ * (i / sampleRate)) * amp * env;
|
|
2037
|
-
}
|
|
2038
|
-
const s16 = Math.round(sample * 32767);
|
|
2039
|
-
const off = 44 + i * blockAlign;
|
|
2040
|
-
for (let c = 0; c < channels; c++) view.setInt16(off + c * bytesPerSample, s16, true);
|
|
2041
|
-
}
|
|
2042
|
-
ringbackUrl = URL.createObjectURL(new Blob([buffer], { type: "audio/wav" }));
|
|
2043
|
-
return ringbackUrl;
|
|
2044
|
-
}
|
|
2045
|
-
function playRingback() {
|
|
2046
|
-
const el = document.createElement("audio");
|
|
2047
|
-
el.src = ringbackSource();
|
|
2048
|
-
el.loop = true;
|
|
2049
|
-
el.style.display = "none";
|
|
2050
|
-
document.body.appendChild(el);
|
|
2051
|
-
void el.play().catch(() => {
|
|
2052
|
-
});
|
|
2053
|
-
return {
|
|
2054
|
-
stop() {
|
|
2055
|
-
el.pause();
|
|
2056
|
-
el.remove();
|
|
2057
|
-
}
|
|
2058
|
-
};
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
// src/ui/audio/useCallAudio.ts
|
|
2062
2393
|
function useCallAudio(enabled) {
|
|
2063
2394
|
const softphone = useSoftphone();
|
|
2064
2395
|
const remote = useRef(null);
|
|
@@ -2137,6 +2468,94 @@ function useCallAudio(enabled) {
|
|
|
2137
2468
|
};
|
|
2138
2469
|
}, [softphone, enabled]);
|
|
2139
2470
|
}
|
|
2471
|
+
function TransferNotices({
|
|
2472
|
+
notices,
|
|
2473
|
+
dismiss
|
|
2474
|
+
}) {
|
|
2475
|
+
const softphone = useSoftphone();
|
|
2476
|
+
const now = useTicker(notices.length > 0, 15e3);
|
|
2477
|
+
const callBack = (notice) => {
|
|
2478
|
+
dismiss(notice.id);
|
|
2479
|
+
if (notice.caller) void softphone.call(notice.caller).catch(() => {
|
|
2480
|
+
});
|
|
2481
|
+
};
|
|
2482
|
+
return /* @__PURE__ */ jsxs(
|
|
2483
|
+
"div",
|
|
2484
|
+
{
|
|
2485
|
+
className: "cv-transfer-notices",
|
|
2486
|
+
role: "status",
|
|
2487
|
+
part: "transfer-notices",
|
|
2488
|
+
"data-has-notices": notices.length > 0,
|
|
2489
|
+
children: [
|
|
2490
|
+
notices.length > 1 ? /* @__PURE__ */ jsxs("p", { className: "cv-transfer-notices-count", children: [
|
|
2491
|
+
notices.length,
|
|
2492
|
+
" traspasos sin conectar"
|
|
2493
|
+
] }) : null,
|
|
2494
|
+
notices.map((notice) => /* @__PURE__ */ jsxs("div", { className: "cv-transfer-notice", part: "transfer-notice", children: [
|
|
2495
|
+
/* @__PURE__ */ jsx("span", { className: "cv-transfer-notice-mark", "aria-hidden": true, children: /* @__PURE__ */ jsx(PhoneMissed, { size: 16 }) }),
|
|
2496
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-transfer-notice-body", children: [
|
|
2497
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-transfer-notice-header", children: [
|
|
2498
|
+
/* @__PURE__ */ jsxs("p", { className: "cv-transfer-notice-title", children: [
|
|
2499
|
+
"El traspaso a ",
|
|
2500
|
+
notice.target,
|
|
2501
|
+
" no se conect\xF3"
|
|
2502
|
+
] }),
|
|
2503
|
+
/* @__PURE__ */ jsx(
|
|
2504
|
+
"button",
|
|
2505
|
+
{
|
|
2506
|
+
type: "button",
|
|
2507
|
+
className: "cv-transfer-notice-dismiss",
|
|
2508
|
+
onClick: () => dismiss(notice.id),
|
|
2509
|
+
"aria-label": "Descartar aviso",
|
|
2510
|
+
part: "transfer-notice-dismiss",
|
|
2511
|
+
children: /* @__PURE__ */ jsx(X, { size: 14 })
|
|
2512
|
+
}
|
|
2513
|
+
)
|
|
2514
|
+
] }),
|
|
2515
|
+
notice.caller ? /* @__PURE__ */ jsxs("p", { className: "cv-transfer-notice-subtitle", children: [
|
|
2516
|
+
notice.caller,
|
|
2517
|
+
" se ha desconectado."
|
|
2518
|
+
] }) : null,
|
|
2519
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-transfer-notice-footer", children: [
|
|
2520
|
+
/* @__PURE__ */ jsx("p", { className: "cv-transfer-notice-age", children: formatAge(elapsedSeconds(notice.at, now)) }),
|
|
2521
|
+
notice.caller ? /* @__PURE__ */ jsxs(
|
|
2522
|
+
"button",
|
|
2523
|
+
{
|
|
2524
|
+
type: "button",
|
|
2525
|
+
className: "cv-btn cv-btn--accent cv-transfer-notice-callback",
|
|
2526
|
+
onClick: () => callBack(notice),
|
|
2527
|
+
part: "transfer-notice-callback",
|
|
2528
|
+
children: [
|
|
2529
|
+
/* @__PURE__ */ jsx(Phone, { size: 14, "aria-hidden": true }),
|
|
2530
|
+
"Llamar de vuelta"
|
|
2531
|
+
]
|
|
2532
|
+
}
|
|
2533
|
+
) : null
|
|
2534
|
+
] })
|
|
2535
|
+
] })
|
|
2536
|
+
] }, notice.id))
|
|
2537
|
+
]
|
|
2538
|
+
}
|
|
2539
|
+
);
|
|
2540
|
+
}
|
|
2541
|
+
function useTransferNotices() {
|
|
2542
|
+
const softphone = useSoftphone();
|
|
2543
|
+
const [notices, setNotices] = useState([]);
|
|
2544
|
+
const nextId = useRef(0);
|
|
2545
|
+
const dismiss = useCallback((id) => {
|
|
2546
|
+
setNotices((prev) => prev.filter((n) => n.id !== id));
|
|
2547
|
+
}, []);
|
|
2548
|
+
useEffect(
|
|
2549
|
+
() => softphone.on("transferOutcome" /* TransferOutcome */, (payload) => {
|
|
2550
|
+
if (payload.status === "not-connected") {
|
|
2551
|
+
const id = nextId.current++;
|
|
2552
|
+
setNotices((prev) => [...prev, { id, target: payload.target, caller: payload.caller, at: payload.at }]);
|
|
2553
|
+
}
|
|
2554
|
+
}),
|
|
2555
|
+
[softphone]
|
|
2556
|
+
);
|
|
2557
|
+
return { notices, dismiss };
|
|
2558
|
+
}
|
|
2140
2559
|
function SoftphoneWidget(props) {
|
|
2141
2560
|
const {
|
|
2142
2561
|
contacts,
|
|
@@ -2161,7 +2580,9 @@ function SoftphoneWidget(props) {
|
|
|
2161
2580
|
useCallAudio(sounds);
|
|
2162
2581
|
const blocked = requireMic && mic.state !== "ready";
|
|
2163
2582
|
const fatal = useLastError();
|
|
2583
|
+
const screenIsFree = !call && !blocked && !fatal && !isFollower;
|
|
2164
2584
|
const { outcome, clear: clearOutcome } = useCallOutcome();
|
|
2585
|
+
const { notices: transferNotices, dismiss: dismissTransferNotice } = useTransferNotices();
|
|
2165
2586
|
const retry = () => {
|
|
2166
2587
|
void (async () => {
|
|
2167
2588
|
try {
|
|
@@ -2185,8 +2606,9 @@ function SoftphoneWidget(props) {
|
|
|
2185
2606
|
className: className ? `cv-softphone ${className}` : "cv-softphone",
|
|
2186
2607
|
...theme === "system" ? {} : { "data-cv-theme": theme },
|
|
2187
2608
|
children: [
|
|
2188
|
-
|
|
2189
|
-
|
|
2609
|
+
screenIsFree ? /* @__PURE__ */ jsx(TransferNotices, { notices: transferNotices, dismiss: dismissTransferNotice }) : null,
|
|
2610
|
+
/* @__PURE__ */ jsx("div", { className: "cv-body", children: isFollower ? /* @__PURE__ */ jsx(LeaderGate, {}) : blocked ? /* @__PURE__ */ jsx(MicPrompt, { state: mic.state, onRequest: () => void mic.request() }) : call ? /* @__PURE__ */ jsx(CallScreen, { call, keyTones: sounds, ...incomingCallAction ? { incomingCallAction } : {} }) : fatal ? /* @__PURE__ */ jsx(ErrorGate, { error: fatal, onRetry: retry }) : tab === "contacts" && contactsSource ? /* @__PURE__ */ jsx(ContactsTab, { source: contactsSource, ...contactAction ? { contactAction } : {} }) : tab === "history" && historySource ? /* @__PURE__ */ jsx(HistoryTab, { source: historySource }) : tab === "settings" ? /* @__PURE__ */ jsx(SettingsTab, {}) : /* @__PURE__ */ jsx(Dialpad, { outcome, onIntent: clearOutcome, keyTones: sounds }) }),
|
|
2611
|
+
screenIsFree ? /* @__PURE__ */ jsx("div", { className: "cv-tabs", role: "tablist", children: tabs.map(({ id, icon: Icon3, label }) => /* @__PURE__ */ jsxs(
|
|
2190
2612
|
"button",
|
|
2191
2613
|
{
|
|
2192
2614
|
role: "tab",
|
|
@@ -2199,7 +2621,7 @@ function SoftphoneWidget(props) {
|
|
|
2199
2621
|
]
|
|
2200
2622
|
},
|
|
2201
2623
|
id
|
|
2202
|
-
)) })
|
|
2624
|
+
)) }) : null
|
|
2203
2625
|
]
|
|
2204
2626
|
}
|
|
2205
2627
|
);
|