@coventia/softphone 0.2.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-DUVnDxsz.d.cts → call-VKq1I8yP.d.cts} +6 -0
- package/dist/{call-DUVnDxsz.d.ts → call-VKq1I8yP.d.ts} +6 -0
- package/dist/{chunk-YWI6EQYS.js → chunk-EYEDPD7Y.js} +108 -15
- package/dist/chunk-EYEDPD7Y.js.map +1 -0
- package/dist/element/index.d.ts +8 -2
- package/dist/element/index.js +334 -249
- package/dist/element/index.js.map +1 -1
- package/dist/{engine-CDjkrXHY.d.cts → engine-BlQBo2Ys.d.cts} +1 -1
- package/dist/{engine-EPCKf-Bl.d.ts → engine-C0BOkIcW.d.ts} +1 -1
- package/dist/index.cjs +199 -23
- 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 +90 -11
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.cts +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/{softphone-BtdgZWkm.d.ts → softphone-Cyq3hiIM.d.ts} +1 -1
- package/dist/{softphone-DHG9MTNg.d.cts → softphone-DlGV12Xp.d.cts} +1 -1
- package/dist/softphone.css +82 -34
- package/dist/{sources-5L2R4HTV.js → sources-IRKNQPWW.js} +6 -2
- package/dist/sources-IRKNQPWW.js.map +1 -0
- package/dist/testing/index.d.cts +2 -2
- package/dist/testing/index.d.ts +2 -2
- package/dist/ui/index.cjs +332 -247
- 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 +332 -247
- package/dist/ui/index.js.map +1 -1
- package/dist/zadarma/index.cjs +106 -13
- 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-4VOPIBXG.js.map → zadarma-engine-L5DEFXUS.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-YWI6EQYS.js.map +0 -1
- package/dist/sources-5L2R4HTV.js.map +0 -1
- package/dist/zadarma-engine-4VOPIBXG.js +0 -5
package/dist/ui/index.js
CHANGED
|
@@ -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
|
{
|
|
@@ -884,7 +1130,9 @@ function Control({
|
|
|
884
1130
|
label,
|
|
885
1131
|
pressed = false,
|
|
886
1132
|
tone,
|
|
887
|
-
onClick
|
|
1133
|
+
onClick,
|
|
1134
|
+
disabled = false,
|
|
1135
|
+
title
|
|
888
1136
|
}) {
|
|
889
1137
|
return /* @__PURE__ */ jsxs("div", { className: "cv-ctrl-item", children: [
|
|
890
1138
|
/* @__PURE__ */ jsx(
|
|
@@ -896,6 +1144,8 @@ function Control({
|
|
|
896
1144
|
"aria-pressed": pressed,
|
|
897
1145
|
"aria-label": label,
|
|
898
1146
|
onClick,
|
|
1147
|
+
disabled,
|
|
1148
|
+
title,
|
|
899
1149
|
children: /* @__PURE__ */ jsx(Icon3, { size: 19 })
|
|
900
1150
|
}
|
|
901
1151
|
),
|
|
@@ -1034,18 +1284,22 @@ function TransferPicker({
|
|
|
1034
1284
|
}
|
|
1035
1285
|
function CallScreen({
|
|
1036
1286
|
call,
|
|
1037
|
-
incomingCallAction
|
|
1287
|
+
incomingCallAction,
|
|
1288
|
+
keyTones = true
|
|
1038
1289
|
}) {
|
|
1039
1290
|
const view = useCall(call);
|
|
1040
1291
|
const softphone = useSoftphone();
|
|
1041
|
-
const [
|
|
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);
|
|
1042
1297
|
const [transferTo, setTransferTo] = useState("");
|
|
1043
1298
|
const [consultCall, setConsultCall] = useState(null);
|
|
1044
1299
|
const [consultOutcome, setConsultOutcome] = useState(null);
|
|
1045
1300
|
const [myUserId, setMyUserId] = useState(null);
|
|
1046
1301
|
const consultView = useCall(consultCall);
|
|
1047
1302
|
const consultIntentRef = useRef(null);
|
|
1048
|
-
const [showDevices, setShowDevices] = useState(false);
|
|
1049
1303
|
const controlsRef = useRef(null);
|
|
1050
1304
|
const now = useTicker(view?.state === "active");
|
|
1051
1305
|
const consultNow = useTicker(consultView?.state === "active");
|
|
@@ -1102,20 +1356,31 @@ function CallScreen({
|
|
|
1102
1356
|
useEffect(() => {
|
|
1103
1357
|
if (!showDevices) return;
|
|
1104
1358
|
const onKey = (e) => {
|
|
1105
|
-
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);
|
|
1106
1368
|
};
|
|
1107
1369
|
document.addEventListener("keydown", onKey);
|
|
1108
1370
|
if (!isConnected) return () => document.removeEventListener("keydown", onKey);
|
|
1109
1371
|
const root = controlsRef.current?.getRootNode() ?? document;
|
|
1110
1372
|
const onDown = (e) => {
|
|
1111
|
-
if (!controlsRef.current?.contains(e.target))
|
|
1373
|
+
if (!controlsRef.current?.contains(e.target)) setPanel(null);
|
|
1112
1374
|
};
|
|
1113
1375
|
root.addEventListener("pointerdown", onDown);
|
|
1114
1376
|
return () => {
|
|
1115
1377
|
document.removeEventListener("keydown", onKey);
|
|
1116
1378
|
root.removeEventListener("pointerdown", onDown);
|
|
1117
1379
|
};
|
|
1118
|
-
}, [
|
|
1380
|
+
}, [showKeypad, isConnected]);
|
|
1381
|
+
useEffect(() => {
|
|
1382
|
+
if (view && view.state !== "active") setPanel((cur) => cur === "keypad" ? null : cur);
|
|
1383
|
+
}, [view?.state]);
|
|
1119
1384
|
if (!view || view.state === "ended") return null;
|
|
1120
1385
|
const s = view.state;
|
|
1121
1386
|
const isRinging = s === "ringing";
|
|
@@ -1141,9 +1406,9 @@ function CallScreen({
|
|
|
1141
1406
|
const [rawM = "0", ss = "00"] = formatDuration(elapsedSeconds(view.answeredAt, now)).split(":");
|
|
1142
1407
|
const mm = rawM.padStart(2, "0");
|
|
1143
1408
|
return /* @__PURE__ */ jsxs("div", { className: "cv-view", children: [
|
|
1144
|
-
/* @__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: [
|
|
1145
1410
|
/* @__PURE__ */ jsxs("div", { className: "cv-call-devices-header", children: [
|
|
1146
|
-
/* @__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: [
|
|
1147
1412
|
/* @__PURE__ */ jsx(ArrowLeft, { size: 13, "aria-hidden": true }),
|
|
1148
1413
|
"Volver"
|
|
1149
1414
|
] }),
|
|
@@ -1151,7 +1416,7 @@ function CallScreen({
|
|
|
1151
1416
|
] }),
|
|
1152
1417
|
/* @__PURE__ */ jsxs("p", { className: "cv-call-devices-context", children: [
|
|
1153
1418
|
showNumber ? `${number} \xB7 ` : "",
|
|
1154
|
-
STATUS_LABEL[s] ?? ""
|
|
1419
|
+
s === "active" ? `${mm}:${ss}` : STATUS_LABEL[s] ?? ""
|
|
1155
1420
|
] }),
|
|
1156
1421
|
/* @__PURE__ */ jsx("div", { className: "cv-settings-section-body", children: /* @__PURE__ */ jsx(DevicePickers, {}) })
|
|
1157
1422
|
] }) : transferPickerActive ? (
|
|
@@ -1239,7 +1504,7 @@ function CallScreen({
|
|
|
1239
1504
|
/* @__PURE__ */ jsx("i", { children: ":" }),
|
|
1240
1505
|
/* @__PURE__ */ jsx("b", { children: ss })
|
|
1241
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] ?? "" }),
|
|
1242
|
-
|
|
1507
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-call-chips", children: [
|
|
1243
1508
|
incomingAction ? /* @__PURE__ */ jsxs(
|
|
1244
1509
|
"button",
|
|
1245
1510
|
{
|
|
@@ -1252,11 +1517,11 @@ function CallScreen({
|
|
|
1252
1517
|
]
|
|
1253
1518
|
}
|
|
1254
1519
|
) : null,
|
|
1255
|
-
/* @__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: [
|
|
1256
1521
|
/* @__PURE__ */ jsx(Headphones, { size: 13, "aria-hidden": true }),
|
|
1257
1522
|
"Dispositivos"
|
|
1258
1523
|
] })
|
|
1259
|
-
] })
|
|
1524
|
+
] })
|
|
1260
1525
|
] }) }),
|
|
1261
1526
|
/* @__PURE__ */ jsx("div", { className: "cv-controls", ref: controlsRef, children: isRinging ? /* @__PURE__ */ jsxs("div", { className: "cv-controls-primary", children: [
|
|
1262
1527
|
/* @__PURE__ */ jsxs("div", { className: "cv-ctrl-item", children: [
|
|
@@ -1315,17 +1580,19 @@ function CallScreen({
|
|
|
1315
1580
|
label: "Transferir",
|
|
1316
1581
|
pressed: showTransfer,
|
|
1317
1582
|
tone: "accent",
|
|
1318
|
-
onClick: () =>
|
|
1583
|
+
onClick: () => togglePanel("transfer")
|
|
1319
1584
|
}
|
|
1320
1585
|
),
|
|
1321
1586
|
/* @__PURE__ */ jsx(
|
|
1322
1587
|
Control,
|
|
1323
1588
|
{
|
|
1324
|
-
icon:
|
|
1325
|
-
label: "
|
|
1326
|
-
pressed:
|
|
1589
|
+
icon: Grid3x3,
|
|
1590
|
+
label: "Teclado",
|
|
1591
|
+
pressed: showKeypad,
|
|
1327
1592
|
tone: "accent",
|
|
1328
|
-
|
|
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")
|
|
1329
1596
|
}
|
|
1330
1597
|
)
|
|
1331
1598
|
] }) : null,
|
|
@@ -1343,7 +1610,22 @@ function CallScreen({
|
|
|
1343
1610
|
),
|
|
1344
1611
|
/* @__PURE__ */ jsx("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Colgar" })
|
|
1345
1612
|
] }) }),
|
|
1346
|
-
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
|
|
1347
1629
|
] }) })
|
|
1348
1630
|
] });
|
|
1349
1631
|
}
|
|
@@ -2108,203 +2390,6 @@ function useCallOutcome() {
|
|
|
2108
2390
|
clear
|
|
2109
2391
|
};
|
|
2110
2392
|
}
|
|
2111
|
-
|
|
2112
|
-
// src/ui/audio/audio.ts
|
|
2113
|
-
var TONE_HZ = 425;
|
|
2114
|
-
function createRemoteAudio() {
|
|
2115
|
-
const el = document.createElement("audio");
|
|
2116
|
-
el.autoplay = true;
|
|
2117
|
-
el.style.display = "none";
|
|
2118
|
-
document.body.appendChild(el);
|
|
2119
|
-
return {
|
|
2120
|
-
attach(stream) {
|
|
2121
|
-
el.srcObject = stream;
|
|
2122
|
-
void el.play().catch(() => {
|
|
2123
|
-
});
|
|
2124
|
-
},
|
|
2125
|
-
detach() {
|
|
2126
|
-
el.srcObject = null;
|
|
2127
|
-
},
|
|
2128
|
-
destroy() {
|
|
2129
|
-
el.srcObject = null;
|
|
2130
|
-
el.remove();
|
|
2131
|
-
},
|
|
2132
|
-
async setOutputDevice(deviceId) {
|
|
2133
|
-
if (typeof el.setSinkId !== "function") return;
|
|
2134
|
-
try {
|
|
2135
|
-
await el.setSinkId(deviceId ?? "");
|
|
2136
|
-
} catch {
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
};
|
|
2140
|
-
}
|
|
2141
|
-
var shared = null;
|
|
2142
|
-
function sharedContext() {
|
|
2143
|
-
if (shared) return shared;
|
|
2144
|
-
const Ctor = typeof AudioContext !== "undefined" ? AudioContext : globalThis.webkitAudioContext;
|
|
2145
|
-
if (!Ctor) return null;
|
|
2146
|
-
shared = new Ctor();
|
|
2147
|
-
return shared;
|
|
2148
|
-
}
|
|
2149
|
-
function primeAudio() {
|
|
2150
|
-
const ctx = sharedContext();
|
|
2151
|
-
if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
|
|
2152
|
-
});
|
|
2153
|
-
}
|
|
2154
|
-
function playRingtone() {
|
|
2155
|
-
const ctx = sharedContext();
|
|
2156
|
-
if (!ctx) return { stop() {
|
|
2157
|
-
} };
|
|
2158
|
-
const gain = ctx.createGain();
|
|
2159
|
-
gain.gain.value = 0;
|
|
2160
|
-
gain.connect(ctx.destination);
|
|
2161
|
-
const osc = ctx.createOscillator();
|
|
2162
|
-
osc.type = "sine";
|
|
2163
|
-
osc.frequency.value = TONE_HZ;
|
|
2164
|
-
osc.connect(gain);
|
|
2165
|
-
osc.start();
|
|
2166
|
-
const LEVEL = 0.8;
|
|
2167
|
-
const RAMP = 0.02;
|
|
2168
|
-
const burst = (at, dur) => {
|
|
2169
|
-
gain.gain.setValueAtTime(0, at);
|
|
2170
|
-
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
2171
|
-
gain.gain.setValueAtTime(LEVEL, at + dur - RAMP);
|
|
2172
|
-
gain.gain.linearRampToValueAtTime(0, at + dur);
|
|
2173
|
-
};
|
|
2174
|
-
const scheduleCycle = (at) => {
|
|
2175
|
-
burst(at, 0.4);
|
|
2176
|
-
burst(at + 0.6, 0.4);
|
|
2177
|
-
return 3;
|
|
2178
|
-
};
|
|
2179
|
-
const LOOKAHEAD = 0.5;
|
|
2180
|
-
const TICK_MS = 150;
|
|
2181
|
-
const LEAD = 0.08;
|
|
2182
|
-
let stopped = false;
|
|
2183
|
-
let timer = null;
|
|
2184
|
-
let nextAt = 0;
|
|
2185
|
-
const pump = () => {
|
|
2186
|
-
while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleCycle(nextAt);
|
|
2187
|
-
};
|
|
2188
|
-
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
2189
|
-
}).finally(() => {
|
|
2190
|
-
if (stopped) return;
|
|
2191
|
-
nextAt = ctx.currentTime + LEAD;
|
|
2192
|
-
pump();
|
|
2193
|
-
timer = setInterval(pump, TICK_MS);
|
|
2194
|
-
});
|
|
2195
|
-
return {
|
|
2196
|
-
stop() {
|
|
2197
|
-
stopped = true;
|
|
2198
|
-
if (timer) clearInterval(timer);
|
|
2199
|
-
try {
|
|
2200
|
-
osc.stop();
|
|
2201
|
-
} catch {
|
|
2202
|
-
}
|
|
2203
|
-
try {
|
|
2204
|
-
gain.disconnect();
|
|
2205
|
-
} catch {
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
};
|
|
2209
|
-
}
|
|
2210
|
-
function playCallEndedTone() {
|
|
2211
|
-
const ctx = sharedContext();
|
|
2212
|
-
if (!ctx) return;
|
|
2213
|
-
const gain = ctx.createGain();
|
|
2214
|
-
gain.gain.value = 0;
|
|
2215
|
-
gain.connect(ctx.destination);
|
|
2216
|
-
const osc = ctx.createOscillator();
|
|
2217
|
-
osc.type = "sine";
|
|
2218
|
-
osc.frequency.value = TONE_HZ;
|
|
2219
|
-
osc.connect(gain);
|
|
2220
|
-
osc.start();
|
|
2221
|
-
const LEVEL = 0.55;
|
|
2222
|
-
const RAMP = 8e-3;
|
|
2223
|
-
const BEEP = 0.25;
|
|
2224
|
-
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
2225
|
-
}).finally(() => {
|
|
2226
|
-
const at = ctx.currentTime + 0.02;
|
|
2227
|
-
gain.gain.setValueAtTime(0, at);
|
|
2228
|
-
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
2229
|
-
gain.gain.setValueAtTime(LEVEL, at + BEEP - RAMP);
|
|
2230
|
-
gain.gain.linearRampToValueAtTime(0, at + BEEP);
|
|
2231
|
-
const totalMs = (at + BEEP - ctx.currentTime) * 1e3;
|
|
2232
|
-
setTimeout(() => {
|
|
2233
|
-
try {
|
|
2234
|
-
osc.stop();
|
|
2235
|
-
} catch {
|
|
2236
|
-
}
|
|
2237
|
-
try {
|
|
2238
|
-
gain.disconnect();
|
|
2239
|
-
} catch {
|
|
2240
|
-
}
|
|
2241
|
-
}, totalMs);
|
|
2242
|
-
});
|
|
2243
|
-
}
|
|
2244
|
-
var ringbackUrl = null;
|
|
2245
|
-
function ringbackSource() {
|
|
2246
|
-
if (ringbackUrl) return ringbackUrl;
|
|
2247
|
-
const sampleRate = 48e3;
|
|
2248
|
-
const channels = 2;
|
|
2249
|
-
const bytesPerSample = 2;
|
|
2250
|
-
const amp = 0.5;
|
|
2251
|
-
const toneSec = 1.5;
|
|
2252
|
-
const silenceSec = 3;
|
|
2253
|
-
const totalFrames = Math.round((toneSec + silenceSec) * sampleRate);
|
|
2254
|
-
const toneFrames = Math.round(toneSec * sampleRate);
|
|
2255
|
-
const ramp = Math.round(0.02 * sampleRate);
|
|
2256
|
-
const blockAlign = channels * bytesPerSample;
|
|
2257
|
-
const dataSize = totalFrames * blockAlign;
|
|
2258
|
-
const buffer = new ArrayBuffer(44 + dataSize);
|
|
2259
|
-
const view = new DataView(buffer);
|
|
2260
|
-
const str = (off, s) => {
|
|
2261
|
-
for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
|
|
2262
|
-
};
|
|
2263
|
-
str(0, "RIFF");
|
|
2264
|
-
view.setUint32(4, 36 + dataSize, true);
|
|
2265
|
-
str(8, "WAVE");
|
|
2266
|
-
str(12, "fmt ");
|
|
2267
|
-
view.setUint32(16, 16, true);
|
|
2268
|
-
view.setUint16(20, 1, true);
|
|
2269
|
-
view.setUint16(22, channels, true);
|
|
2270
|
-
view.setUint32(24, sampleRate, true);
|
|
2271
|
-
view.setUint32(28, sampleRate * blockAlign, true);
|
|
2272
|
-
view.setUint16(32, blockAlign, true);
|
|
2273
|
-
view.setUint16(34, bytesPerSample * 8, true);
|
|
2274
|
-
str(36, "data");
|
|
2275
|
-
view.setUint32(40, dataSize, true);
|
|
2276
|
-
for (let i = 0; i < totalFrames; i++) {
|
|
2277
|
-
let sample = 0;
|
|
2278
|
-
if (i < toneFrames) {
|
|
2279
|
-
let env = 1;
|
|
2280
|
-
if (i < ramp) env = i / ramp;
|
|
2281
|
-
else if (i > toneFrames - ramp) env = Math.max(0, (toneFrames - i) / ramp);
|
|
2282
|
-
sample = Math.sin(2 * Math.PI * TONE_HZ * (i / sampleRate)) * amp * env;
|
|
2283
|
-
}
|
|
2284
|
-
const s16 = Math.round(sample * 32767);
|
|
2285
|
-
const off = 44 + i * blockAlign;
|
|
2286
|
-
for (let c = 0; c < channels; c++) view.setInt16(off + c * bytesPerSample, s16, true);
|
|
2287
|
-
}
|
|
2288
|
-
ringbackUrl = URL.createObjectURL(new Blob([buffer], { type: "audio/wav" }));
|
|
2289
|
-
return ringbackUrl;
|
|
2290
|
-
}
|
|
2291
|
-
function playRingback() {
|
|
2292
|
-
const el = document.createElement("audio");
|
|
2293
|
-
el.src = ringbackSource();
|
|
2294
|
-
el.loop = true;
|
|
2295
|
-
el.style.display = "none";
|
|
2296
|
-
document.body.appendChild(el);
|
|
2297
|
-
void el.play().catch(() => {
|
|
2298
|
-
});
|
|
2299
|
-
return {
|
|
2300
|
-
stop() {
|
|
2301
|
-
el.pause();
|
|
2302
|
-
el.remove();
|
|
2303
|
-
}
|
|
2304
|
-
};
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
// src/ui/audio/useCallAudio.ts
|
|
2308
2393
|
function useCallAudio(enabled) {
|
|
2309
2394
|
const softphone = useSoftphone();
|
|
2310
2395
|
const remote = useRef(null);
|
|
@@ -2522,7 +2607,7 @@ function SoftphoneWidget(props) {
|
|
|
2522
2607
|
...theme === "system" ? {} : { "data-cv-theme": theme },
|
|
2523
2608
|
children: [
|
|
2524
2609
|
screenIsFree ? /* @__PURE__ */ jsx(TransferNotices, { notices: transferNotices, dismiss: dismissTransferNotice }) : null,
|
|
2525
|
-
/* @__PURE__ */ jsx("div", { className: "cv-body", children: isFollower ? /* @__PURE__ */ jsx(LeaderGate, {}) : blocked ? /* @__PURE__ */ jsx(MicPrompt, { state: mic.state, onRequest: () => void mic.request() }) : call ? /* @__PURE__ */ jsx(CallScreen, { call, ...incomingCallAction ? { incomingCallAction } : {} }) : fatal ? /* @__PURE__ */ jsx(ErrorGate, { error: fatal, onRetry: retry }) : tab === "contacts" && contactsSource ? /* @__PURE__ */ jsx(ContactsTab, { source: contactsSource, ...contactAction ? { contactAction } : {} }) : tab === "history" && historySource ? /* @__PURE__ */ jsx(HistoryTab, { source: historySource }) : tab === "settings" ? /* @__PURE__ */ jsx(SettingsTab, {}) : /* @__PURE__ */ jsx(Dialpad, { outcome, onIntent: clearOutcome }) }),
|
|
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 }) }),
|
|
2526
2611
|
screenIsFree ? /* @__PURE__ */ jsx("div", { className: "cv-tabs", role: "tablist", children: tabs.map(({ id, icon: Icon3, label }) => /* @__PURE__ */ jsxs(
|
|
2527
2612
|
"button",
|
|
2528
2613
|
{
|