@coventia/softphone 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{call-DUVnDxsz.d.cts → call-CaPDWVk3.d.cts} +50 -1
- package/dist/{call-DUVnDxsz.d.ts → call-CaPDWVk3.d.ts} +50 -1
- package/dist/{chunk-YWI6EQYS.js → chunk-EYEDPD7Y.js} +108 -15
- package/dist/chunk-EYEDPD7Y.js.map +1 -0
- package/dist/{chunk-LMJXX4YM.js → chunk-UHNLEBBO.js} +4 -2
- package/dist/chunk-UHNLEBBO.js.map +1 -0
- package/dist/element/index.d.ts +76 -11
- package/dist/element/index.js +1411 -478
- package/dist/element/index.js.map +1 -1
- package/dist/{engine-EPCKf-Bl.d.ts → engine-BgBsIcEa.d.ts} +1 -1
- package/dist/{engine-CDjkrXHY.d.cts → engine-CU2rEo3g.d.cts} +1 -1
- package/dist/index.cjs +318 -32
- 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 +157 -19
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +2 -0
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +1 -1
- package/dist/{softphone-BtdgZWkm.d.ts → softphone-DEt0TtQw.d.ts} +26 -10
- package/dist/{softphone-DHG9MTNg.d.cts → softphone-dZMkW8V1.d.cts} +26 -10
- package/dist/softphone.css +192 -46
- package/dist/{sources-5L2R4HTV.js → sources-M3GYMS6V.js} +58 -3
- package/dist/sources-M3GYMS6V.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 +1308 -366
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +40 -7
- package/dist/ui/index.d.ts +40 -7
- package/dist/ui/index.js +1308 -369
- 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-LMJXX4YM.js.map +0 -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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isWebRtcSupported } from '../chunk-KAXEMJ7E.js';
|
|
2
|
-
import { useSoftphone, useSoftphoneState, useCall, usePresence, useActiveCall, useIsLeader, useLastError, useDevices } from '../chunk-
|
|
2
|
+
import { useSoftphone, useSoftphoneState, useCall, usePresence, useActiveCall, useIsLeader, useLastError, useDevices } from '../chunk-UHNLEBBO.js';
|
|
3
3
|
import '../chunk-7R6LOOAI.js';
|
|
4
|
-
import { forwardRef, createElement, useState, useRef, useLayoutEffect, useEffect, useCallback, useMemo } from 'react';
|
|
4
|
+
import { forwardRef, createElement, useState, useRef, useLayoutEffect, useEffect, useCallback, useMemo, useSyncExternalStore, useId } from 'react';
|
|
5
5
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
6
|
|
|
7
7
|
// ../../node_modules/lucide-react/dist/esm/shared/src/utils.js
|
|
@@ -89,6 +89,14 @@ var ArrowRight = createLucideIcon("ArrowRight", [
|
|
|
89
89
|
["path", { d: "m12 5 7 7-7 7", key: "xquz4c" }]
|
|
90
90
|
]);
|
|
91
91
|
|
|
92
|
+
// ../../node_modules/lucide-react/dist/esm/icons/bell-ring.js
|
|
93
|
+
var BellRing = createLucideIcon("BellRing", [
|
|
94
|
+
["path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9", key: "1qo2s2" }],
|
|
95
|
+
["path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0", key: "qgo35s" }],
|
|
96
|
+
["path", { d: "M4 2C2.8 3.7 2 5.7 2 8", key: "tap9e0" }],
|
|
97
|
+
["path", { d: "M22 8c0-2.3-.8-4.3-2-6", key: "5bb3ad" }]
|
|
98
|
+
]);
|
|
99
|
+
|
|
92
100
|
// ../../node_modules/lucide-react/dist/esm/icons/calendar-clock.js
|
|
93
101
|
var CalendarClock = createLucideIcon("CalendarClock", [
|
|
94
102
|
["path", { d: "M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5", key: "1osxxc" }],
|
|
@@ -203,6 +211,13 @@ var MonitorSmartphone = createLucideIcon("MonitorSmartphone", [
|
|
|
203
211
|
["rect", { width: "6", height: "10", x: "16", y: "12", rx: "2", key: "1egngj" }]
|
|
204
212
|
]);
|
|
205
213
|
|
|
214
|
+
// ../../node_modules/lucide-react/dist/esm/icons/music.js
|
|
215
|
+
var Music = createLucideIcon("Music", [
|
|
216
|
+
["path", { d: "M9 18V5l12-2v13", key: "1jmyc2" }],
|
|
217
|
+
["circle", { cx: "6", cy: "18", r: "3", key: "fqmcym" }],
|
|
218
|
+
["circle", { cx: "18", cy: "16", r: "3", key: "1hluhg" }]
|
|
219
|
+
]);
|
|
220
|
+
|
|
206
221
|
// ../../node_modules/lucide-react/dist/esm/icons/pause.js
|
|
207
222
|
var Pause = createLucideIcon("Pause", [
|
|
208
223
|
["rect", { x: "14", y: "4", width: "4", height: "16", rx: "1", key: "zuxfzm" }],
|
|
@@ -328,6 +343,19 @@ var Users = createLucideIcon("Users", [
|
|
|
328
343
|
["path", { d: "M16 3.13a4 4 0 0 1 0 7.75", key: "1da9ce" }]
|
|
329
344
|
]);
|
|
330
345
|
|
|
346
|
+
// ../../node_modules/lucide-react/dist/esm/icons/volume-2.js
|
|
347
|
+
var Volume2 = createLucideIcon("Volume2", [
|
|
348
|
+
[
|
|
349
|
+
"path",
|
|
350
|
+
{
|
|
351
|
+
d: "M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z",
|
|
352
|
+
key: "uqj9uw"
|
|
353
|
+
}
|
|
354
|
+
],
|
|
355
|
+
["path", { d: "M16 9a5 5 0 0 1 0 6", key: "1q6k2b" }],
|
|
356
|
+
["path", { d: "M19.364 18.364a9 9 0 0 0 0-12.728", key: "ijwkga" }]
|
|
357
|
+
]);
|
|
358
|
+
|
|
331
359
|
// ../../node_modules/lucide-react/dist/esm/icons/x.js
|
|
332
360
|
var X = createLucideIcon("X", [
|
|
333
361
|
["path", { d: "M18 6 6 18", key: "1bl5f8" }],
|
|
@@ -550,147 +578,578 @@ function caretAfterDigits(formatted, digitsBefore) {
|
|
|
550
578
|
function digitsBeforeCaret(text, caret) {
|
|
551
579
|
return (text.slice(0, caret).match(/\d/g) ?? []).length;
|
|
552
580
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
["
|
|
557
|
-
["
|
|
558
|
-
["
|
|
559
|
-
["
|
|
560
|
-
["
|
|
561
|
-
["
|
|
562
|
-
["
|
|
563
|
-
["
|
|
564
|
-
["
|
|
565
|
-
["
|
|
581
|
+
|
|
582
|
+
// src/ui/dialing/keys.ts
|
|
583
|
+
var KEYPAD_KEYS = [
|
|
584
|
+
["1", ""],
|
|
585
|
+
["2", "ABC"],
|
|
586
|
+
["3", "DEF"],
|
|
587
|
+
["4", "GHI"],
|
|
588
|
+
["5", "JKL"],
|
|
589
|
+
["6", "MNO"],
|
|
590
|
+
["7", "PQRS"],
|
|
591
|
+
["8", "TUV"],
|
|
592
|
+
["9", "WXYZ"],
|
|
593
|
+
["*", ""],
|
|
594
|
+
["0", "+"],
|
|
595
|
+
["#", ""]
|
|
566
596
|
];
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
};
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
597
|
+
|
|
598
|
+
// src/ui/audio/classic-ring.ts
|
|
599
|
+
var C6 = 1046.5;
|
|
600
|
+
var E6 = 1318.51;
|
|
601
|
+
var HARMONICITY = 4;
|
|
602
|
+
var INDEX = 1.8;
|
|
603
|
+
var CLASSIC_CYCLE_SECONDS = 3;
|
|
604
|
+
var CLASSIC_GAIN = 0.3841;
|
|
605
|
+
function classicRingGraph(ctx, dest) {
|
|
606
|
+
const output = ctx.createGain();
|
|
607
|
+
output.gain.value = CLASSIC_GAIN;
|
|
608
|
+
const chain = ["highpass", "highpass", "lowpass", "lowpass"].map((type) => {
|
|
609
|
+
const f = ctx.createBiquadFilter();
|
|
610
|
+
f.type = type;
|
|
611
|
+
f.frequency.value = type === "highpass" ? 250 : 7500;
|
|
612
|
+
f.Q.value = 0.707;
|
|
613
|
+
return f;
|
|
614
|
+
});
|
|
615
|
+
for (let i = 0; i < chain.length - 1; i++) chain[i].connect(chain[i + 1]);
|
|
616
|
+
chain[chain.length - 1].connect(output);
|
|
617
|
+
output.connect(dest);
|
|
618
|
+
return { input: chain[0], output };
|
|
619
|
+
}
|
|
620
|
+
function strike(ctx, input, t, freq, vel, track) {
|
|
621
|
+
const carrier = ctx.createOscillator();
|
|
622
|
+
carrier.frequency.value = freq;
|
|
623
|
+
const modulator = ctx.createOscillator();
|
|
624
|
+
modulator.frequency.value = freq * HARMONICITY;
|
|
625
|
+
const depth = ctx.createGain();
|
|
626
|
+
depth.gain.setValueAtTime(0, t);
|
|
627
|
+
depth.gain.linearRampToValueAtTime(INDEX * freq, t + 4e-3);
|
|
628
|
+
depth.gain.setTargetAtTime(0, t + 4e-3, 0.02);
|
|
629
|
+
modulator.connect(depth);
|
|
630
|
+
depth.connect(carrier.frequency);
|
|
631
|
+
const amp = ctx.createGain();
|
|
632
|
+
amp.gain.setValueAtTime(0, t);
|
|
633
|
+
amp.gain.linearRampToValueAtTime(vel, t + 5e-3);
|
|
634
|
+
amp.gain.setTargetAtTime(0, t + 5e-3, 0.15);
|
|
635
|
+
amp.gain.setTargetAtTime(0, t + 0.045, 0.08);
|
|
636
|
+
carrier.connect(amp);
|
|
637
|
+
amp.connect(input);
|
|
638
|
+
for (const o of [carrier, modulator]) {
|
|
639
|
+
o.start(t);
|
|
640
|
+
o.stop(t + 0.7);
|
|
641
|
+
track(o);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function scheduleClassicCycle(ctx, input, t0, track) {
|
|
645
|
+
for (const ring of [0, 0.6]) {
|
|
646
|
+
for (let i = 0; i < 8; i++) strike(ctx, input, t0 + ring + i * 0.05, i % 2 ? E6 : C6, 0.55 + 0.1 * (i % 2), track);
|
|
647
|
+
}
|
|
648
|
+
return CLASSIC_CYCLE_SECONDS;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/ui/audio/audio.ts
|
|
652
|
+
var TONE_HZ = 425;
|
|
653
|
+
function createRemoteAudio() {
|
|
654
|
+
const el = document.createElement("audio");
|
|
655
|
+
el.autoplay = true;
|
|
656
|
+
el.style.display = "none";
|
|
657
|
+
document.body.appendChild(el);
|
|
658
|
+
return {
|
|
659
|
+
attach(stream) {
|
|
660
|
+
el.srcObject = stream;
|
|
661
|
+
void el.play().catch(() => {
|
|
662
|
+
});
|
|
663
|
+
},
|
|
664
|
+
detach() {
|
|
665
|
+
el.srcObject = null;
|
|
666
|
+
},
|
|
667
|
+
destroy() {
|
|
668
|
+
el.srcObject = null;
|
|
669
|
+
el.remove();
|
|
670
|
+
},
|
|
671
|
+
async setOutputDevice(deviceId) {
|
|
672
|
+
if (typeof el.setSinkId !== "function") return;
|
|
673
|
+
try {
|
|
674
|
+
await el.setSinkId(deviceId ?? "");
|
|
675
|
+
} catch {
|
|
676
|
+
}
|
|
677
|
+
}
|
|
592
678
|
};
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
679
|
+
}
|
|
680
|
+
var shared = null;
|
|
681
|
+
function sharedContext() {
|
|
682
|
+
if (shared) return shared;
|
|
683
|
+
const Ctor = typeof AudioContext !== "undefined" ? AudioContext : globalThis.webkitAudioContext;
|
|
684
|
+
if (!Ctor) return null;
|
|
685
|
+
shared = new Ctor();
|
|
686
|
+
return shared;
|
|
687
|
+
}
|
|
688
|
+
function primeAudio() {
|
|
689
|
+
const ctx = sharedContext();
|
|
690
|
+
if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
function playRingtone(options = {}) {
|
|
694
|
+
const ctx = sharedContext();
|
|
695
|
+
if (!ctx) return { stop() {
|
|
696
|
+
} };
|
|
697
|
+
const bus = ctx.createGain();
|
|
698
|
+
const route = routeToDevice(ctx, bus, options.sinkId ?? null);
|
|
699
|
+
const voice = options.buffer ? fileVoice(ctx, bus, options.buffer, options.loop ?? null) : synthesisedVoice(ctx, bus);
|
|
700
|
+
return {
|
|
701
|
+
stop() {
|
|
702
|
+
voice.stop();
|
|
703
|
+
route.stop();
|
|
704
|
+
}
|
|
597
705
|
};
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
] }),
|
|
631
|
-
/* @__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: KEYS.map(([key, letters, dialable]) => /* @__PURE__ */ jsxs(
|
|
633
|
-
"button",
|
|
634
|
-
{
|
|
635
|
-
type: "button",
|
|
636
|
-
className: "cv-key",
|
|
637
|
-
"data-dialable": dialable,
|
|
638
|
-
onClick: () => dialable && append(key),
|
|
639
|
-
"aria-disabled": !dialable,
|
|
640
|
-
title: dialable ? void 0 : "Solo disponible durante una llamada",
|
|
641
|
-
children: [
|
|
642
|
-
/* @__PURE__ */ jsx("b", { children: key }),
|
|
643
|
-
letters ? /* @__PURE__ */ jsx("small", { children: letters }) : null
|
|
644
|
-
]
|
|
645
|
-
},
|
|
646
|
-
key
|
|
647
|
-
)) }),
|
|
648
|
-
/* @__PURE__ */ jsx("div", { className: "cv-dial-foot", children: /* @__PURE__ */ jsx(
|
|
649
|
-
"button",
|
|
650
|
-
{
|
|
651
|
-
type: "button",
|
|
652
|
-
className: "cv-round cv-round--answer",
|
|
653
|
-
onClick: call,
|
|
654
|
-
disabled: !canCall,
|
|
655
|
-
"aria-label": "Llamar",
|
|
656
|
-
part: "call-button",
|
|
657
|
-
children: /* @__PURE__ */ jsx(Phone, { size: 26 })
|
|
706
|
+
}
|
|
707
|
+
function fileVoice(ctx, bus, buffer, loop) {
|
|
708
|
+
const FILE_LEVEL = 1;
|
|
709
|
+
const RAMP = 0.02;
|
|
710
|
+
const LEAD = 0.08;
|
|
711
|
+
const gain = ctx.createGain();
|
|
712
|
+
gain.gain.value = 0;
|
|
713
|
+
gain.connect(bus);
|
|
714
|
+
const src = ctx.createBufferSource();
|
|
715
|
+
src.buffer = buffer;
|
|
716
|
+
src.loop = true;
|
|
717
|
+
const points = loop && loop.start >= 0 && loop.end > loop.start && loop.end <= buffer.duration + 1e-3 ? loop : null;
|
|
718
|
+
if (points) {
|
|
719
|
+
src.loopStart = points.start;
|
|
720
|
+
src.loopEnd = points.end;
|
|
721
|
+
}
|
|
722
|
+
src.connect(gain);
|
|
723
|
+
let stopped = false;
|
|
724
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
725
|
+
}).finally(() => {
|
|
726
|
+
if (stopped) return;
|
|
727
|
+
const at = ctx.currentTime + LEAD;
|
|
728
|
+
gain.gain.setValueAtTime(0, at);
|
|
729
|
+
gain.gain.linearRampToValueAtTime(FILE_LEVEL, at + RAMP);
|
|
730
|
+
src.start(at, points?.start ?? 0);
|
|
731
|
+
});
|
|
732
|
+
return {
|
|
733
|
+
stop() {
|
|
734
|
+
stopped = true;
|
|
735
|
+
try {
|
|
736
|
+
src.stop();
|
|
737
|
+
} catch {
|
|
658
738
|
}
|
|
659
|
-
|
|
660
|
-
|
|
739
|
+
try {
|
|
740
|
+
gain.disconnect();
|
|
741
|
+
} catch {
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
};
|
|
661
745
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
const
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
746
|
+
function routeToDevice(ctx, bus, sinkId) {
|
|
747
|
+
const toDefault = () => {
|
|
748
|
+
bus.connect(ctx.destination);
|
|
749
|
+
};
|
|
750
|
+
const el = sinkId && typeof document !== "undefined" ? document.createElement("audio") : null;
|
|
751
|
+
const dest = el && typeof ctx.createMediaStreamDestination === "function" ? ctx.createMediaStreamDestination() : null;
|
|
752
|
+
const canRoute = el && dest && typeof el.setSinkId === "function";
|
|
753
|
+
if (!canRoute || !sinkId) {
|
|
754
|
+
toDefault();
|
|
755
|
+
return { stop: () => safeDisconnect(bus) };
|
|
756
|
+
}
|
|
757
|
+
let stopped = false;
|
|
758
|
+
el.style.display = "none";
|
|
759
|
+
el.srcObject = dest.stream;
|
|
760
|
+
document.body.appendChild(el);
|
|
761
|
+
bus.connect(dest);
|
|
762
|
+
void el.setSinkId(sinkId).then(() => el.play()).catch((error) => {
|
|
763
|
+
if (stopped) return;
|
|
764
|
+
console.warn("[softphone] sound could not be sent to the chosen device; using the default output", error);
|
|
765
|
+
safeDisconnect(bus);
|
|
766
|
+
teardown(el);
|
|
767
|
+
toDefault();
|
|
768
|
+
});
|
|
769
|
+
return {
|
|
770
|
+
stop() {
|
|
771
|
+
stopped = true;
|
|
772
|
+
safeDisconnect(bus);
|
|
773
|
+
teardown(el);
|
|
774
|
+
}
|
|
775
|
+
};
|
|
671
776
|
}
|
|
672
|
-
function
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
777
|
+
function safeDisconnect(node) {
|
|
778
|
+
try {
|
|
779
|
+
node.disconnect();
|
|
780
|
+
} catch {
|
|
781
|
+
}
|
|
677
782
|
}
|
|
678
|
-
function
|
|
679
|
-
|
|
680
|
-
|
|
783
|
+
function teardown(el) {
|
|
784
|
+
el.pause();
|
|
785
|
+
el.srcObject = null;
|
|
786
|
+
el.remove();
|
|
681
787
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const
|
|
688
|
-
|
|
788
|
+
var TEST_TONE_MS = 900;
|
|
789
|
+
function playTestTone(sinkId) {
|
|
790
|
+
const ctx = sharedContext();
|
|
791
|
+
if (!ctx) return { stop() {
|
|
792
|
+
} };
|
|
793
|
+
const bus = ctx.createGain();
|
|
794
|
+
const route = routeToDevice(ctx, bus, sinkId);
|
|
795
|
+
const LEVEL = 0.5;
|
|
796
|
+
const NOTES = [
|
|
797
|
+
{ hz: 660, at: 0.05 },
|
|
798
|
+
{ hz: 880, at: 0.4 }
|
|
799
|
+
];
|
|
800
|
+
const oscs = [];
|
|
801
|
+
let stopped = false;
|
|
802
|
+
let timer = null;
|
|
803
|
+
const stop = () => {
|
|
804
|
+
if (stopped) return;
|
|
805
|
+
stopped = true;
|
|
806
|
+
if (timer) clearTimeout(timer);
|
|
807
|
+
for (const o of oscs) {
|
|
808
|
+
try {
|
|
809
|
+
o.stop();
|
|
810
|
+
} catch {
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
route.stop();
|
|
814
|
+
};
|
|
815
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
816
|
+
}).finally(() => {
|
|
817
|
+
if (stopped) return;
|
|
818
|
+
const t0 = ctx.currentTime;
|
|
819
|
+
for (const { hz, at } of NOTES) {
|
|
820
|
+
const gain = ctx.createGain();
|
|
821
|
+
gain.gain.value = 0;
|
|
822
|
+
gain.connect(bus);
|
|
823
|
+
const osc = ctx.createOscillator();
|
|
824
|
+
osc.type = "sine";
|
|
825
|
+
osc.frequency.value = hz;
|
|
826
|
+
osc.connect(gain);
|
|
827
|
+
const start = t0 + at;
|
|
828
|
+
gain.gain.setValueAtTime(0, start);
|
|
829
|
+
gain.gain.linearRampToValueAtTime(LEVEL, start + 0.015);
|
|
830
|
+
gain.gain.setValueAtTime(LEVEL, start + 0.25);
|
|
831
|
+
gain.gain.linearRampToValueAtTime(0, start + 0.32);
|
|
832
|
+
osc.start(start);
|
|
833
|
+
osc.stop(start + 0.34);
|
|
834
|
+
oscs.push(osc);
|
|
835
|
+
}
|
|
836
|
+
timer = setTimeout(stop, TEST_TONE_MS);
|
|
837
|
+
});
|
|
838
|
+
return { stop };
|
|
839
|
+
}
|
|
840
|
+
function synthesisedVoice(ctx, bus) {
|
|
841
|
+
const { input, output } = classicRingGraph(ctx, bus);
|
|
842
|
+
const scheduled = /* @__PURE__ */ new Set();
|
|
843
|
+
const track = (o) => {
|
|
844
|
+
scheduled.add(o);
|
|
845
|
+
o.onended = () => scheduled.delete(o);
|
|
846
|
+
};
|
|
847
|
+
const LOOKAHEAD = 0.5;
|
|
848
|
+
const TICK_MS = 150;
|
|
849
|
+
const LEAD = 0.08;
|
|
850
|
+
let stopped = false;
|
|
851
|
+
let timer = null;
|
|
852
|
+
let nextAt = 0;
|
|
853
|
+
const pump = () => {
|
|
854
|
+
while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleClassicCycle(ctx, input, nextAt, track);
|
|
855
|
+
};
|
|
856
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
857
|
+
}).finally(() => {
|
|
858
|
+
if (stopped) return;
|
|
859
|
+
nextAt = ctx.currentTime + LEAD;
|
|
860
|
+
pump();
|
|
861
|
+
timer = setInterval(pump, TICK_MS);
|
|
862
|
+
});
|
|
863
|
+
return {
|
|
864
|
+
stop() {
|
|
865
|
+
stopped = true;
|
|
866
|
+
if (timer) clearInterval(timer);
|
|
867
|
+
for (const o of scheduled) {
|
|
868
|
+
try {
|
|
869
|
+
o.stop();
|
|
870
|
+
} catch {
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
scheduled.clear();
|
|
874
|
+
try {
|
|
875
|
+
output.disconnect();
|
|
876
|
+
} catch {
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
};
|
|
689
880
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
881
|
+
function playCallEndedTone() {
|
|
882
|
+
const ctx = sharedContext();
|
|
883
|
+
if (!ctx) return;
|
|
884
|
+
const gain = ctx.createGain();
|
|
885
|
+
gain.gain.value = 0;
|
|
886
|
+
gain.connect(ctx.destination);
|
|
887
|
+
const osc = ctx.createOscillator();
|
|
888
|
+
osc.type = "sine";
|
|
889
|
+
osc.frequency.value = TONE_HZ;
|
|
890
|
+
osc.connect(gain);
|
|
891
|
+
osc.start();
|
|
892
|
+
const LEVEL = 0.55;
|
|
893
|
+
const RAMP = 8e-3;
|
|
894
|
+
const BEEP = 0.25;
|
|
895
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
896
|
+
}).finally(() => {
|
|
897
|
+
const at = ctx.currentTime + 0.02;
|
|
898
|
+
gain.gain.setValueAtTime(0, at);
|
|
899
|
+
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
900
|
+
gain.gain.setValueAtTime(LEVEL, at + BEEP - RAMP);
|
|
901
|
+
gain.gain.linearRampToValueAtTime(0, at + BEEP);
|
|
902
|
+
const totalMs = (at + BEEP - ctx.currentTime) * 1e3;
|
|
903
|
+
setTimeout(() => {
|
|
904
|
+
try {
|
|
905
|
+
osc.stop();
|
|
906
|
+
} catch {
|
|
907
|
+
}
|
|
908
|
+
try {
|
|
909
|
+
gain.disconnect();
|
|
910
|
+
} catch {
|
|
911
|
+
}
|
|
912
|
+
}, totalMs);
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
var DTMF_ROWS_HZ = [697, 770, 852, 941];
|
|
916
|
+
var DTMF_COLS_HZ = [1209, 1336, 1477];
|
|
917
|
+
var DTMF_LAYOUT = ["123", "456", "789", "*0#"];
|
|
918
|
+
function dtmfFrequencies(key) {
|
|
919
|
+
if (key.length !== 1) return null;
|
|
920
|
+
for (let row = 0; row < DTMF_LAYOUT.length; row++) {
|
|
921
|
+
const col = DTMF_LAYOUT[row].indexOf(key);
|
|
922
|
+
if (col !== -1) return [DTMF_ROWS_HZ[row], DTMF_COLS_HZ[col]];
|
|
923
|
+
}
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
function playKeyTone(key) {
|
|
927
|
+
const hz = dtmfFrequencies(key);
|
|
928
|
+
if (!hz) return;
|
|
929
|
+
const ctx = sharedContext();
|
|
930
|
+
if (!ctx) return;
|
|
931
|
+
const LEVEL = 0.2;
|
|
932
|
+
const RAMP = 6e-3;
|
|
933
|
+
const DURATION = 0.12;
|
|
934
|
+
const LEAD = 0.01;
|
|
935
|
+
const gain = ctx.createGain();
|
|
936
|
+
gain.gain.value = 0;
|
|
937
|
+
gain.connect(ctx.destination);
|
|
938
|
+
const oscillators = hz.map((freq) => {
|
|
939
|
+
const osc = ctx.createOscillator();
|
|
940
|
+
osc.type = "sine";
|
|
941
|
+
osc.frequency.value = freq;
|
|
942
|
+
osc.connect(gain);
|
|
943
|
+
osc.start();
|
|
944
|
+
return osc;
|
|
945
|
+
});
|
|
946
|
+
const fire = () => {
|
|
947
|
+
const at = ctx.currentTime + LEAD;
|
|
948
|
+
gain.gain.setValueAtTime(0, at);
|
|
949
|
+
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
950
|
+
gain.gain.setValueAtTime(LEVEL, at + DURATION - RAMP);
|
|
951
|
+
gain.gain.linearRampToValueAtTime(0, at + DURATION);
|
|
952
|
+
for (const osc of oscillators) osc.stop(at + DURATION);
|
|
953
|
+
oscillators[0].onended = () => gain.disconnect();
|
|
954
|
+
};
|
|
955
|
+
if (ctx.state === "running") fire();
|
|
956
|
+
else void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
957
|
+
}).finally(fire);
|
|
958
|
+
}
|
|
959
|
+
var ringbackUrl = null;
|
|
960
|
+
function ringbackSource() {
|
|
961
|
+
if (ringbackUrl) return ringbackUrl;
|
|
962
|
+
const sampleRate = 48e3;
|
|
963
|
+
const channels = 2;
|
|
964
|
+
const bytesPerSample = 2;
|
|
965
|
+
const amp = 0.5;
|
|
966
|
+
const toneSec = 1.5;
|
|
967
|
+
const silenceSec = 3;
|
|
968
|
+
const totalFrames = Math.round((toneSec + silenceSec) * sampleRate);
|
|
969
|
+
const toneFrames = Math.round(toneSec * sampleRate);
|
|
970
|
+
const ramp = Math.round(0.02 * sampleRate);
|
|
971
|
+
const blockAlign = channels * bytesPerSample;
|
|
972
|
+
const dataSize = totalFrames * blockAlign;
|
|
973
|
+
const buffer = new ArrayBuffer(44 + dataSize);
|
|
974
|
+
const view = new DataView(buffer);
|
|
975
|
+
const str = (off, s) => {
|
|
976
|
+
for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
|
|
977
|
+
};
|
|
978
|
+
str(0, "RIFF");
|
|
979
|
+
view.setUint32(4, 36 + dataSize, true);
|
|
980
|
+
str(8, "WAVE");
|
|
981
|
+
str(12, "fmt ");
|
|
982
|
+
view.setUint32(16, 16, true);
|
|
983
|
+
view.setUint16(20, 1, true);
|
|
984
|
+
view.setUint16(22, channels, true);
|
|
985
|
+
view.setUint32(24, sampleRate, true);
|
|
986
|
+
view.setUint32(28, sampleRate * blockAlign, true);
|
|
987
|
+
view.setUint16(32, blockAlign, true);
|
|
988
|
+
view.setUint16(34, bytesPerSample * 8, true);
|
|
989
|
+
str(36, "data");
|
|
990
|
+
view.setUint32(40, dataSize, true);
|
|
991
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
992
|
+
let sample = 0;
|
|
993
|
+
if (i < toneFrames) {
|
|
994
|
+
let env = 1;
|
|
995
|
+
if (i < ramp) env = i / ramp;
|
|
996
|
+
else if (i > toneFrames - ramp) env = Math.max(0, (toneFrames - i) / ramp);
|
|
997
|
+
sample = Math.sin(2 * Math.PI * TONE_HZ * (i / sampleRate)) * amp * env;
|
|
998
|
+
}
|
|
999
|
+
const s16 = Math.round(sample * 32767);
|
|
1000
|
+
const off = 44 + i * blockAlign;
|
|
1001
|
+
for (let c = 0; c < channels; c++) view.setInt16(off + c * bytesPerSample, s16, true);
|
|
1002
|
+
}
|
|
1003
|
+
ringbackUrl = URL.createObjectURL(new Blob([buffer], { type: "audio/wav" }));
|
|
1004
|
+
return ringbackUrl;
|
|
1005
|
+
}
|
|
1006
|
+
function playRingback() {
|
|
1007
|
+
const el = document.createElement("audio");
|
|
1008
|
+
el.src = ringbackSource();
|
|
1009
|
+
el.loop = true;
|
|
1010
|
+
el.style.display = "none";
|
|
1011
|
+
document.body.appendChild(el);
|
|
1012
|
+
void el.play().catch(() => {
|
|
1013
|
+
});
|
|
1014
|
+
return {
|
|
1015
|
+
stop() {
|
|
1016
|
+
el.pause();
|
|
1017
|
+
el.remove();
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
var NOT_DIALABLE = /* @__PURE__ */ new Set(["*", "#"]);
|
|
1022
|
+
function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
|
|
1023
|
+
const softphone = useSoftphone();
|
|
1024
|
+
const state = useSoftphoneState();
|
|
1025
|
+
const [value, setValue] = useState("");
|
|
1026
|
+
const [country, setCountry] = useState(DEFAULT_COUNTRY);
|
|
1027
|
+
const format = useNumberFormatter();
|
|
1028
|
+
const inputRef = useRef(null);
|
|
1029
|
+
const caretRef = useRef(null);
|
|
1030
|
+
const display = format(country, value);
|
|
1031
|
+
const target = toE164(country, value);
|
|
1032
|
+
const canCall = state === "online" && target.length > 1;
|
|
1033
|
+
useLayoutEffect(() => {
|
|
1034
|
+
const caret = caretRef.current;
|
|
1035
|
+
if (caret === null || !inputRef.current) return;
|
|
1036
|
+
caretRef.current = null;
|
|
1037
|
+
const pos = caretAfterDigits(display, caret);
|
|
1038
|
+
inputRef.current.setSelectionRange(pos, pos);
|
|
1039
|
+
}, [display]);
|
|
1040
|
+
const append = (key) => {
|
|
1041
|
+
onIntent?.();
|
|
1042
|
+
setValue((v) => v + key);
|
|
1043
|
+
if (keyTones) playKeyTone(key);
|
|
1044
|
+
};
|
|
1045
|
+
const backspace = () => {
|
|
1046
|
+
onIntent?.();
|
|
1047
|
+
setValue((v) => v.slice(0, -1));
|
|
1048
|
+
};
|
|
1049
|
+
const call = () => {
|
|
1050
|
+
if (!canCall) return;
|
|
1051
|
+
onIntent?.();
|
|
1052
|
+
void softphone.call(target);
|
|
1053
|
+
};
|
|
1054
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-view", children: [
|
|
1055
|
+
/* @__PURE__ */ jsx("div", { className: "cv-dial-prefix", children: /* @__PURE__ */ jsx(CountryPicker, { value: country, onChange: setCountry }) }),
|
|
1056
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-dial-head", children: [
|
|
1057
|
+
/* @__PURE__ */ jsx("span", { className: "cv-dial-side", "aria-hidden": true }),
|
|
1058
|
+
/* @__PURE__ */ jsx(
|
|
1059
|
+
"input",
|
|
1060
|
+
{
|
|
1061
|
+
ref: inputRef,
|
|
1062
|
+
className: "cv-display",
|
|
1063
|
+
value: display,
|
|
1064
|
+
onChange: (e) => {
|
|
1065
|
+
onIntent?.();
|
|
1066
|
+
caretRef.current = digitsBeforeCaret(e.target.value, e.target.selectionStart ?? 0);
|
|
1067
|
+
setValue(e.target.value.replace(/[^\d+]/g, ""));
|
|
1068
|
+
},
|
|
1069
|
+
onKeyDown: (e) => e.key === "Enter" && call(),
|
|
1070
|
+
placeholder: "N\xFAmero",
|
|
1071
|
+
"aria-label": "N\xFAmero a marcar",
|
|
1072
|
+
inputMode: "tel"
|
|
1073
|
+
}
|
|
1074
|
+
),
|
|
1075
|
+
/* @__PURE__ */ jsx(
|
|
1076
|
+
"button",
|
|
1077
|
+
{
|
|
1078
|
+
type: "button",
|
|
1079
|
+
className: "cv-dial-side",
|
|
1080
|
+
"data-hidden": value.length === 0,
|
|
1081
|
+
onClick: backspace,
|
|
1082
|
+
"aria-label": "Borrar",
|
|
1083
|
+
children: /* @__PURE__ */ jsx(Delete, { size: 19 })
|
|
1084
|
+
}
|
|
1085
|
+
)
|
|
1086
|
+
] }),
|
|
1087
|
+
/* @__PURE__ */ jsx("p", { className: "cv-dial-outcome", role: "status", part: "error", "data-shown": outcome !== null, children: outcome?.text ?? "" }),
|
|
1088
|
+
/* @__PURE__ */ jsx("div", { className: "cv-keypad", children: KEYPAD_KEYS.map(([key, letters]) => {
|
|
1089
|
+
const dialable = !NOT_DIALABLE.has(key);
|
|
1090
|
+
return /* @__PURE__ */ jsxs(
|
|
1091
|
+
"button",
|
|
1092
|
+
{
|
|
1093
|
+
type: "button",
|
|
1094
|
+
className: "cv-key",
|
|
1095
|
+
"data-dialable": dialable,
|
|
1096
|
+
onClick: () => dialable && append(key),
|
|
1097
|
+
"aria-disabled": !dialable,
|
|
1098
|
+
title: dialable ? void 0 : "Solo disponible durante una llamada",
|
|
1099
|
+
children: [
|
|
1100
|
+
/* @__PURE__ */ jsx("b", { children: key }),
|
|
1101
|
+
letters ? /* @__PURE__ */ jsx("small", { children: letters }) : null
|
|
1102
|
+
]
|
|
1103
|
+
},
|
|
1104
|
+
key
|
|
1105
|
+
);
|
|
1106
|
+
}) }),
|
|
1107
|
+
/* @__PURE__ */ jsx("div", { className: "cv-dial-foot", children: /* @__PURE__ */ jsx(
|
|
1108
|
+
"button",
|
|
1109
|
+
{
|
|
1110
|
+
type: "button",
|
|
1111
|
+
className: "cv-round cv-round--answer",
|
|
1112
|
+
onClick: call,
|
|
1113
|
+
disabled: !canCall,
|
|
1114
|
+
"aria-label": "Llamar",
|
|
1115
|
+
part: "call-button",
|
|
1116
|
+
children: /* @__PURE__ */ jsx(Phone, { size: 26 })
|
|
1117
|
+
}
|
|
1118
|
+
) })
|
|
1119
|
+
] });
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// src/ui/lib/format.ts
|
|
1123
|
+
function formatDuration(totalSeconds) {
|
|
1124
|
+
const s = Math.max(0, Math.floor(totalSeconds));
|
|
1125
|
+
const hh = Math.floor(s / 3600);
|
|
1126
|
+
const mm = Math.floor(s % 3600 / 60);
|
|
1127
|
+
const ss = s % 60;
|
|
1128
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1129
|
+
return hh > 0 ? `${hh}:${pad(mm)}:${pad(ss)}` : `${mm}:${pad(ss)}`;
|
|
1130
|
+
}
|
|
1131
|
+
function initials(name) {
|
|
1132
|
+
if (!name) return "#";
|
|
1133
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
1134
|
+
if (words.length === 0 || /^\+?\d/.test(words[0])) return "#";
|
|
1135
|
+
return words.slice(0, 2).map((w) => w[0].toUpperCase()).join("");
|
|
1136
|
+
}
|
|
1137
|
+
function elapsedSeconds(since, now) {
|
|
1138
|
+
if (!since) return 0;
|
|
1139
|
+
return Math.floor((now - since.getTime()) / 1e3);
|
|
1140
|
+
}
|
|
1141
|
+
function formatAge(totalSeconds) {
|
|
1142
|
+
const s = Math.max(0, Math.floor(totalSeconds));
|
|
1143
|
+
if (s < 60) return "justo ahora";
|
|
1144
|
+
const mm = Math.floor(s / 60);
|
|
1145
|
+
if (mm < 60) return `hace ${mm} min`;
|
|
1146
|
+
const hh = Math.floor(mm / 60);
|
|
1147
|
+
return `hace ${hh} h`;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// src/ui/lib/error-text.ts
|
|
1151
|
+
var ERROR_TEXT = {
|
|
1152
|
+
// ── Engine lifecycle ────────────────────────────────────────────────────────
|
|
694
1153
|
// Integrator mistakes rather than user-facing failures, but they must still read as something
|
|
695
1154
|
// rather than as a blank panel if one ever reaches the UI.
|
|
696
1155
|
"engine/duplicate-instance": {
|
|
@@ -884,7 +1343,9 @@ function Control({
|
|
|
884
1343
|
label,
|
|
885
1344
|
pressed = false,
|
|
886
1345
|
tone,
|
|
887
|
-
onClick
|
|
1346
|
+
onClick,
|
|
1347
|
+
disabled = false,
|
|
1348
|
+
title
|
|
888
1349
|
}) {
|
|
889
1350
|
return /* @__PURE__ */ jsxs("div", { className: "cv-ctrl-item", children: [
|
|
890
1351
|
/* @__PURE__ */ jsx(
|
|
@@ -896,6 +1357,8 @@ function Control({
|
|
|
896
1357
|
"aria-pressed": pressed,
|
|
897
1358
|
"aria-label": label,
|
|
898
1359
|
onClick,
|
|
1360
|
+
disabled,
|
|
1361
|
+
title,
|
|
899
1362
|
children: /* @__PURE__ */ jsx(Icon3, { size: 19 })
|
|
900
1363
|
}
|
|
901
1364
|
),
|
|
@@ -1034,18 +1497,22 @@ function TransferPicker({
|
|
|
1034
1497
|
}
|
|
1035
1498
|
function CallScreen({
|
|
1036
1499
|
call,
|
|
1037
|
-
incomingCallAction
|
|
1500
|
+
incomingCallAction,
|
|
1501
|
+
keyTones = true
|
|
1038
1502
|
}) {
|
|
1039
1503
|
const view = useCall(call);
|
|
1040
1504
|
const softphone = useSoftphone();
|
|
1041
|
-
const [
|
|
1505
|
+
const [panel, setPanel] = useState(null);
|
|
1506
|
+
const showTransfer = panel === "transfer";
|
|
1507
|
+
const showKeypad = panel === "keypad";
|
|
1508
|
+
const showDevices = panel === "devices";
|
|
1509
|
+
const togglePanel = (p) => setPanel((cur) => cur === p ? null : p);
|
|
1042
1510
|
const [transferTo, setTransferTo] = useState("");
|
|
1043
1511
|
const [consultCall, setConsultCall] = useState(null);
|
|
1044
1512
|
const [consultOutcome, setConsultOutcome] = useState(null);
|
|
1045
1513
|
const [myUserId, setMyUserId] = useState(null);
|
|
1046
1514
|
const consultView = useCall(consultCall);
|
|
1047
1515
|
const consultIntentRef = useRef(null);
|
|
1048
|
-
const [showDevices, setShowDevices] = useState(false);
|
|
1049
1516
|
const controlsRef = useRef(null);
|
|
1050
1517
|
const now = useTicker(view?.state === "active");
|
|
1051
1518
|
const consultNow = useTicker(consultView?.state === "active");
|
|
@@ -1102,20 +1569,31 @@ function CallScreen({
|
|
|
1102
1569
|
useEffect(() => {
|
|
1103
1570
|
if (!showDevices) return;
|
|
1104
1571
|
const onKey = (e) => {
|
|
1105
|
-
if (e.key === "Escape")
|
|
1572
|
+
if (e.key === "Escape") setPanel(null);
|
|
1573
|
+
};
|
|
1574
|
+
document.addEventListener("keydown", onKey);
|
|
1575
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
1576
|
+
}, [showDevices]);
|
|
1577
|
+
useEffect(() => {
|
|
1578
|
+
if (!showKeypad) return;
|
|
1579
|
+
const onKey = (e) => {
|
|
1580
|
+
if (e.key === "Escape") setPanel(null);
|
|
1106
1581
|
};
|
|
1107
1582
|
document.addEventListener("keydown", onKey);
|
|
1108
1583
|
if (!isConnected) return () => document.removeEventListener("keydown", onKey);
|
|
1109
1584
|
const root = controlsRef.current?.getRootNode() ?? document;
|
|
1110
1585
|
const onDown = (e) => {
|
|
1111
|
-
if (!controlsRef.current?.contains(e.target))
|
|
1586
|
+
if (!controlsRef.current?.contains(e.target)) setPanel(null);
|
|
1112
1587
|
};
|
|
1113
1588
|
root.addEventListener("pointerdown", onDown);
|
|
1114
1589
|
return () => {
|
|
1115
1590
|
document.removeEventListener("keydown", onKey);
|
|
1116
1591
|
root.removeEventListener("pointerdown", onDown);
|
|
1117
1592
|
};
|
|
1118
|
-
}, [
|
|
1593
|
+
}, [showKeypad, isConnected]);
|
|
1594
|
+
useEffect(() => {
|
|
1595
|
+
if (view && view.state !== "active") setPanel((cur) => cur === "keypad" ? null : cur);
|
|
1596
|
+
}, [view?.state]);
|
|
1119
1597
|
if (!view || view.state === "ended") return null;
|
|
1120
1598
|
const s = view.state;
|
|
1121
1599
|
const isRinging = s === "ringing";
|
|
@@ -1141,9 +1619,9 @@ function CallScreen({
|
|
|
1141
1619
|
const [rawM = "0", ss = "00"] = formatDuration(elapsedSeconds(view.answeredAt, now)).split(":");
|
|
1142
1620
|
const mm = rawM.padStart(2, "0");
|
|
1143
1621
|
return /* @__PURE__ */ jsxs("div", { className: "cv-view", children: [
|
|
1144
|
-
/* @__PURE__ */ jsx("div", { className: "cv-call", children:
|
|
1622
|
+
/* @__PURE__ */ jsx("div", { className: "cv-call", children: showDevices ? /* @__PURE__ */ jsxs("div", { className: "cv-call-devices-inline", children: [
|
|
1145
1623
|
/* @__PURE__ */ jsxs("div", { className: "cv-call-devices-header", children: [
|
|
1146
|
-
/* @__PURE__ */ jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () =>
|
|
1624
|
+
/* @__PURE__ */ jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setPanel(null), children: [
|
|
1147
1625
|
/* @__PURE__ */ jsx(ArrowLeft, { size: 13, "aria-hidden": true }),
|
|
1148
1626
|
"Volver"
|
|
1149
1627
|
] }),
|
|
@@ -1151,7 +1629,7 @@ function CallScreen({
|
|
|
1151
1629
|
] }),
|
|
1152
1630
|
/* @__PURE__ */ jsxs("p", { className: "cv-call-devices-context", children: [
|
|
1153
1631
|
showNumber ? `${number} \xB7 ` : "",
|
|
1154
|
-
STATUS_LABEL[s] ?? ""
|
|
1632
|
+
s === "active" ? `${mm}:${ss}` : STATUS_LABEL[s] ?? ""
|
|
1155
1633
|
] }),
|
|
1156
1634
|
/* @__PURE__ */ jsx("div", { className: "cv-settings-section-body", children: /* @__PURE__ */ jsx(DevicePickers, {}) })
|
|
1157
1635
|
] }) : transferPickerActive ? (
|
|
@@ -1239,7 +1717,7 @@ function CallScreen({
|
|
|
1239
1717
|
/* @__PURE__ */ jsx("i", { children: ":" }),
|
|
1240
1718
|
/* @__PURE__ */ jsx("b", { children: ss })
|
|
1241
1719
|
] }) : /* @__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
|
-
|
|
1720
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-call-chips", children: [
|
|
1243
1721
|
incomingAction ? /* @__PURE__ */ jsxs(
|
|
1244
1722
|
"button",
|
|
1245
1723
|
{
|
|
@@ -1252,11 +1730,11 @@ function CallScreen({
|
|
|
1252
1730
|
]
|
|
1253
1731
|
}
|
|
1254
1732
|
) : null,
|
|
1255
|
-
/* @__PURE__ */ jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () =>
|
|
1733
|
+
/* @__PURE__ */ jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setPanel("devices"), children: [
|
|
1256
1734
|
/* @__PURE__ */ jsx(Headphones, { size: 13, "aria-hidden": true }),
|
|
1257
1735
|
"Dispositivos"
|
|
1258
1736
|
] })
|
|
1259
|
-
] })
|
|
1737
|
+
] })
|
|
1260
1738
|
] }) }),
|
|
1261
1739
|
/* @__PURE__ */ jsx("div", { className: "cv-controls", ref: controlsRef, children: isRinging ? /* @__PURE__ */ jsxs("div", { className: "cv-controls-primary", children: [
|
|
1262
1740
|
/* @__PURE__ */ jsxs("div", { className: "cv-ctrl-item", children: [
|
|
@@ -1315,17 +1793,19 @@ function CallScreen({
|
|
|
1315
1793
|
label: "Transferir",
|
|
1316
1794
|
pressed: showTransfer,
|
|
1317
1795
|
tone: "accent",
|
|
1318
|
-
onClick: () =>
|
|
1796
|
+
onClick: () => togglePanel("transfer")
|
|
1319
1797
|
}
|
|
1320
1798
|
),
|
|
1321
1799
|
/* @__PURE__ */ jsx(
|
|
1322
1800
|
Control,
|
|
1323
1801
|
{
|
|
1324
|
-
icon:
|
|
1325
|
-
label: "
|
|
1326
|
-
pressed:
|
|
1802
|
+
icon: Grid3x3,
|
|
1803
|
+
label: "Teclado",
|
|
1804
|
+
pressed: showKeypad,
|
|
1327
1805
|
tone: "accent",
|
|
1328
|
-
|
|
1806
|
+
disabled: s !== "active",
|
|
1807
|
+
title: s === "held" ? "Reanuda la llamada para usar el teclado" : s === "reconnecting" ? "No disponible mientras se reconecta" : void 0,
|
|
1808
|
+
onClick: () => togglePanel("keypad")
|
|
1329
1809
|
}
|
|
1330
1810
|
)
|
|
1331
1811
|
] }) : null,
|
|
@@ -1343,7 +1823,22 @@ function CallScreen({
|
|
|
1343
1823
|
),
|
|
1344
1824
|
/* @__PURE__ */ jsx("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Colgar" })
|
|
1345
1825
|
] }) }),
|
|
1346
|
-
inCall &&
|
|
1826
|
+
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(
|
|
1827
|
+
"button",
|
|
1828
|
+
{
|
|
1829
|
+
type: "button",
|
|
1830
|
+
className: "cv-key",
|
|
1831
|
+
onClick: () => {
|
|
1832
|
+
call.sendDigits(key);
|
|
1833
|
+
if (keyTones) playKeyTone(key);
|
|
1834
|
+
},
|
|
1835
|
+
children: [
|
|
1836
|
+
/* @__PURE__ */ jsx("b", { children: key }),
|
|
1837
|
+
letters ? /* @__PURE__ */ jsx("small", { children: letters }) : null
|
|
1838
|
+
]
|
|
1839
|
+
},
|
|
1840
|
+
key
|
|
1841
|
+
)) }) : null
|
|
1347
1842
|
] }) })
|
|
1348
1843
|
] });
|
|
1349
1844
|
}
|
|
@@ -1882,21 +2377,641 @@ function HistoryTab({ source }) {
|
|
|
1882
2377
|
selected ? /* @__PURE__ */ jsx(HistoryDetail, { entry: selected, onBack: () => setSelected(null) }) : null
|
|
1883
2378
|
] });
|
|
1884
2379
|
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2380
|
+
|
|
2381
|
+
// src/ui/audio/ringtone-store.ts
|
|
2382
|
+
var stores = /* @__PURE__ */ new WeakMap();
|
|
2383
|
+
function ringtoneStore(softphone) {
|
|
2384
|
+
const source = softphone.sources?.ringtones;
|
|
2385
|
+
if (!source) return null;
|
|
2386
|
+
let store = stores.get(softphone);
|
|
2387
|
+
if (!store) {
|
|
2388
|
+
store = createRingtoneStore(source);
|
|
2389
|
+
stores.set(softphone, store);
|
|
2390
|
+
}
|
|
2391
|
+
return store;
|
|
2392
|
+
}
|
|
2393
|
+
function createRingtoneStore(source, decodeImpl = decodeRingtone) {
|
|
2394
|
+
let settings = null;
|
|
2395
|
+
let audioWanted = false;
|
|
2396
|
+
let loaded = null;
|
|
2397
|
+
let loading2 = null;
|
|
2398
|
+
let inFlight = null;
|
|
2399
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2400
|
+
const notify = () => {
|
|
2401
|
+
for (const l of listeners) l();
|
|
2402
|
+
};
|
|
2403
|
+
const effectiveUrl = () => {
|
|
2404
|
+
if (!settings) return null;
|
|
2405
|
+
return settings.catalogue.find((r) => r.id === settings.effective)?.url ?? null;
|
|
2406
|
+
};
|
|
2407
|
+
const loadEffective = async () => {
|
|
2408
|
+
const url = effectiveUrl();
|
|
2409
|
+
if (!audioWanted || !url || loaded?.url === url || loading2 === url) return;
|
|
2410
|
+
loading2 = url;
|
|
2411
|
+
try {
|
|
2412
|
+
const buffer = await decodeImpl(url);
|
|
2413
|
+
if (audioWanted && effectiveUrl() === url) {
|
|
2414
|
+
loaded = { url, buffer };
|
|
2415
|
+
notify();
|
|
2416
|
+
}
|
|
2417
|
+
} catch (error) {
|
|
2418
|
+
console.warn(`[softphone] ringtone could not be loaded, using the built-in tone: ${url}`, error);
|
|
2419
|
+
} finally {
|
|
2420
|
+
if (loading2 === url) loading2 = null;
|
|
2421
|
+
}
|
|
2422
|
+
};
|
|
2423
|
+
const apply = (next) => {
|
|
2424
|
+
settings = next;
|
|
2425
|
+
if (loaded && loaded.url !== effectiveUrl()) loaded = null;
|
|
2426
|
+
notify();
|
|
2427
|
+
};
|
|
2428
|
+
return {
|
|
2429
|
+
getSettings: () => settings,
|
|
2430
|
+
readyBuffer: () => loaded && loaded.url === effectiveUrl() ? loaded.buffer : null,
|
|
2431
|
+
readyLoop: () => {
|
|
2432
|
+
if (!loaded || loaded.url !== effectiveUrl() || !settings) return null;
|
|
2433
|
+
return settings.catalogue.find((r) => r.id === settings.effective)?.loop ?? null;
|
|
2434
|
+
},
|
|
2435
|
+
refresh() {
|
|
2436
|
+
inFlight ??= (async () => {
|
|
2437
|
+
if (!settings && source.cached) {
|
|
2438
|
+
const early = await source.cached().catch(() => null);
|
|
2439
|
+
if (early && !settings) {
|
|
2440
|
+
apply(early);
|
|
2441
|
+
void loadEffective();
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
try {
|
|
2445
|
+
apply(await source.get());
|
|
2446
|
+
} catch (error) {
|
|
2447
|
+
console.warn("[softphone] ringtone settings could not be loaded; keeping the previous ones", error);
|
|
2448
|
+
} finally {
|
|
2449
|
+
inFlight = null;
|
|
2450
|
+
}
|
|
2451
|
+
await loadEffective();
|
|
2452
|
+
})();
|
|
2453
|
+
return inFlight;
|
|
2454
|
+
},
|
|
2455
|
+
async setMine(id) {
|
|
2456
|
+
apply(await source.setMine(id));
|
|
2457
|
+
void loadEffective();
|
|
2458
|
+
},
|
|
2459
|
+
wantAudio(on) {
|
|
2460
|
+
if (audioWanted === on) return;
|
|
2461
|
+
audioWanted = on;
|
|
2462
|
+
if (!on) {
|
|
2463
|
+
loaded = null;
|
|
2464
|
+
notify();
|
|
2465
|
+
} else {
|
|
2466
|
+
void loadEffective();
|
|
2467
|
+
}
|
|
2468
|
+
},
|
|
2469
|
+
decode: (url) => decodeImpl(url),
|
|
2470
|
+
subscribe(listener) {
|
|
2471
|
+
listeners.add(listener);
|
|
2472
|
+
return () => listeners.delete(listener);
|
|
2473
|
+
}
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
async function decodeRingtone(url) {
|
|
2477
|
+
const Offline = typeof OfflineAudioContext !== "undefined" ? OfflineAudioContext : globalThis.webkitOfflineAudioContext;
|
|
2478
|
+
if (!Offline) throw new Error("this browser cannot decode audio files");
|
|
2479
|
+
const res = await fetch(url, { credentials: "omit" });
|
|
2480
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2481
|
+
const bytes = await res.arrayBuffer();
|
|
2482
|
+
return new Offline(1, 1, 48e3).decodeAudioData(bytes);
|
|
2483
|
+
}
|
|
2484
|
+
function SettingsSection({
|
|
2485
|
+
title,
|
|
2486
|
+
children
|
|
2487
|
+
}) {
|
|
2488
|
+
return /* @__PURE__ */ jsxs("section", { className: "cv-settings-section", children: [
|
|
2489
|
+
/* @__PURE__ */ jsx("h3", { className: "cv-settings-section-title", children: title }),
|
|
2490
|
+
/* @__PURE__ */ jsx("div", { className: "cv-settings-section-body", children })
|
|
2491
|
+
] });
|
|
2492
|
+
}
|
|
2493
|
+
function SettingsNavRow({
|
|
2494
|
+
icon: Icon3,
|
|
2495
|
+
title,
|
|
2496
|
+
value,
|
|
2497
|
+
onOpen,
|
|
2498
|
+
rowRef
|
|
2499
|
+
}) {
|
|
2500
|
+
const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2501
|
+
/* @__PURE__ */ jsx("span", { className: "cv-settings-row-icon", "aria-hidden": true, children: /* @__PURE__ */ jsx(Icon3, { size: 16 }) }),
|
|
2502
|
+
/* @__PURE__ */ jsxs("span", { className: "cv-settings-nav-text", children: [
|
|
2503
|
+
/* @__PURE__ */ jsx("span", { className: "cv-settings-nav-title", children: title }),
|
|
2504
|
+
/* @__PURE__ */ jsx("span", { className: "cv-settings-nav-value", children: value })
|
|
2505
|
+
] }),
|
|
2506
|
+
onOpen ? /* @__PURE__ */ jsx(ChevronRight, { size: 16, className: "cv-settings-nav-chevron", "aria-hidden": true }) : null
|
|
2507
|
+
] });
|
|
2508
|
+
return onOpen ? /* @__PURE__ */ jsx("button", { type: "button", className: "cv-settings-nav", onClick: onOpen, ref: rowRef, children: inner }) : /* @__PURE__ */ jsx("div", { className: "cv-settings-nav cv-settings-nav--static", children: inner });
|
|
2509
|
+
}
|
|
2510
|
+
function ChoiceList({
|
|
2511
|
+
label,
|
|
2512
|
+
choices,
|
|
2513
|
+
value,
|
|
2514
|
+
onSelect,
|
|
2515
|
+
onReselect
|
|
2516
|
+
}) {
|
|
2517
|
+
const name = useId();
|
|
2518
|
+
const pointer = useRef(false);
|
|
2519
|
+
return /* @__PURE__ */ jsx("div", { className: "cv-choices", role: "radiogroup", "aria-label": label, children: choices.map((c) => {
|
|
2520
|
+
const checked = c.value === value;
|
|
2521
|
+
return /* @__PURE__ */ jsxs(
|
|
2522
|
+
"label",
|
|
2523
|
+
{
|
|
2524
|
+
className: "cv-choice",
|
|
2525
|
+
"data-checked": checked || void 0,
|
|
2526
|
+
onPointerDown: () => {
|
|
2527
|
+
pointer.current = true;
|
|
2528
|
+
},
|
|
2529
|
+
children: [
|
|
2530
|
+
/* @__PURE__ */ jsx(
|
|
2531
|
+
"input",
|
|
2532
|
+
{
|
|
2533
|
+
type: "radio",
|
|
2534
|
+
className: "cv-choice-input",
|
|
2535
|
+
name,
|
|
2536
|
+
value: c.value,
|
|
2537
|
+
checked,
|
|
2538
|
+
onChange: () => {
|
|
2539
|
+
const via = pointer.current ? "pointer" : "keyboard";
|
|
2540
|
+
pointer.current = false;
|
|
2541
|
+
onSelect(c.value, via);
|
|
2542
|
+
},
|
|
2543
|
+
onClick: () => {
|
|
2544
|
+
if (c.value === value) {
|
|
2545
|
+
pointer.current = false;
|
|
2546
|
+
onReselect?.(c.value);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
),
|
|
2551
|
+
/* @__PURE__ */ jsxs("span", { className: "cv-choice-text", children: [
|
|
2552
|
+
/* @__PURE__ */ jsx("span", { className: "cv-choice-label", children: c.label }),
|
|
2553
|
+
c.sub ? /* @__PURE__ */ jsx("span", { className: "cv-choice-sub", children: c.sub }) : null
|
|
2554
|
+
] }),
|
|
2555
|
+
c.trailing,
|
|
2556
|
+
/* @__PURE__ */ jsx(Check, { size: 16, className: "cv-choice-check", "aria-hidden": true })
|
|
2557
|
+
]
|
|
2558
|
+
},
|
|
2559
|
+
c.value
|
|
2560
|
+
);
|
|
2561
|
+
}) });
|
|
2562
|
+
}
|
|
2563
|
+
function SettingsPage({
|
|
2564
|
+
title,
|
|
2565
|
+
intro,
|
|
2566
|
+
onBack,
|
|
2567
|
+
children
|
|
2568
|
+
}) {
|
|
2569
|
+
const titleId = useId();
|
|
2570
|
+
const heading = useRef(null);
|
|
2571
|
+
useEffect(() => heading.current?.focus(), []);
|
|
2572
|
+
return /* @__PURE__ */ jsxs(
|
|
2573
|
+
"div",
|
|
2574
|
+
{
|
|
2575
|
+
className: "cv-detail cv-settings-page",
|
|
2576
|
+
role: "region",
|
|
2577
|
+
"aria-labelledby": titleId,
|
|
2578
|
+
onKeyDown: (e) => {
|
|
2579
|
+
if (e.key === "Escape") {
|
|
2580
|
+
e.stopPropagation();
|
|
2581
|
+
onBack();
|
|
2582
|
+
}
|
|
2583
|
+
},
|
|
2584
|
+
children: [
|
|
2585
|
+
/* @__PURE__ */ jsx("div", { className: "cv-detail-head", children: /* @__PURE__ */ jsxs("button", { type: "button", className: "cv-detail-back", onClick: onBack, children: [
|
|
2586
|
+
/* @__PURE__ */ jsx(ArrowLeft, { size: 16, "aria-hidden": true }),
|
|
2587
|
+
"Ajustes"
|
|
2588
|
+
] }) }),
|
|
2589
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-settings-page-body", children: [
|
|
2590
|
+
/* @__PURE__ */ jsx("h2", { className: "cv-settings-page-title", id: titleId, tabIndex: -1, ref: heading, children: title }),
|
|
2591
|
+
intro ? /* @__PURE__ */ jsx("p", { className: "cv-settings-page-intro", children: intro }) : null,
|
|
2592
|
+
children
|
|
2593
|
+
] })
|
|
2594
|
+
]
|
|
2595
|
+
}
|
|
2596
|
+
);
|
|
2597
|
+
}
|
|
2598
|
+
var SYSTEM_DEFAULT = "";
|
|
2599
|
+
var PAGES = {
|
|
2600
|
+
input: {
|
|
2601
|
+
title: "Micr\xF3fono",
|
|
2602
|
+
intro: "El micr\xF3fono que usa el tel\xE9fono en las llamadas.",
|
|
2603
|
+
empty: "No se encontr\xF3 ning\xFAn micr\xF3fono."
|
|
2604
|
+
},
|
|
2605
|
+
output: {
|
|
2606
|
+
title: "Altavoces",
|
|
2607
|
+
intro: "D\xF3nde oyes a la otra persona durante una llamada.",
|
|
2608
|
+
empty: "No se encontr\xF3 ning\xFAn altavoz."
|
|
2609
|
+
},
|
|
2610
|
+
ringtone: {
|
|
2611
|
+
title: "Salida del tono",
|
|
2612
|
+
intro: "D\xF3nde suena el tono cuando entra una llamada. La llamada se oye por \xABAltavoces\xBB.",
|
|
2613
|
+
empty: "No se encontr\xF3 ning\xFAn altavoz."
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
function deviceSummary(kind, list, id) {
|
|
2617
|
+
const i = id ? list.findIndex((d) => d.deviceId === id) : -1;
|
|
2618
|
+
return i >= 0 ? deviceLabel(list[i], i, kind) : "Predeterminado del sistema";
|
|
2619
|
+
}
|
|
2620
|
+
function DevicePage({ kind, onBack }) {
|
|
2621
|
+
const devices = useDevices();
|
|
2622
|
+
const page = PAGES[kind];
|
|
2623
|
+
const list = kind === "input" ? devices.inputs : devices.outputs;
|
|
2624
|
+
const current = kind === "input" ? devices.currentInputId : kind === "output" ? devices.currentOutputId : devices.currentRingtoneOutputId;
|
|
2625
|
+
const apply = (id) => kind === "input" ? devices.setInput(id) : kind === "output" ? devices.setOutput(id) : devices.setRingtoneOutput(id);
|
|
2626
|
+
const choices = [
|
|
2627
|
+
{ value: SYSTEM_DEFAULT, label: "Predeterminado del sistema" },
|
|
2628
|
+
...list.map((d, i) => ({ value: d.deviceId, label: deviceLabel(d, i, kind) }))
|
|
2629
|
+
];
|
|
2630
|
+
return /* @__PURE__ */ jsxs(SettingsPage, { title: page.title, intro: page.intro, onBack, children: [
|
|
2631
|
+
list.length > 0 ? /* @__PURE__ */ jsx(
|
|
2632
|
+
ChoiceList,
|
|
2633
|
+
{
|
|
2634
|
+
label: page.title,
|
|
2635
|
+
choices,
|
|
2636
|
+
value: current ?? SYSTEM_DEFAULT,
|
|
2637
|
+
onSelect: (v) => void apply(v === SYSTEM_DEFAULT ? null : v)
|
|
2638
|
+
}
|
|
2639
|
+
) : /* @__PURE__ */ jsx("p", { className: "cv-settings-page-empty", children: page.empty }),
|
|
2640
|
+
kind === "output" ? /* @__PURE__ */ jsx(SpeakerTest, { sinkId: devices.currentOutputId }) : null
|
|
2641
|
+
] });
|
|
2642
|
+
}
|
|
2643
|
+
function SpeakerTest({ sinkId }) {
|
|
2644
|
+
const [testing, setTesting] = useState(false);
|
|
2645
|
+
const tone = useRef(null);
|
|
2646
|
+
const timer = useRef(null);
|
|
2647
|
+
useEffect(
|
|
2648
|
+
() => () => {
|
|
2649
|
+
tone.current?.stop();
|
|
2650
|
+
if (timer.current) clearTimeout(timer.current);
|
|
2651
|
+
},
|
|
2652
|
+
[]
|
|
2653
|
+
);
|
|
2654
|
+
const test = () => {
|
|
2655
|
+
primeAudio();
|
|
2656
|
+
tone.current?.stop();
|
|
2657
|
+
tone.current = playTestTone(sinkId);
|
|
2658
|
+
setTesting(true);
|
|
2659
|
+
if (timer.current) clearTimeout(timer.current);
|
|
2660
|
+
timer.current = setTimeout(() => setTesting(false), TEST_TONE_MS);
|
|
2661
|
+
};
|
|
2662
|
+
return /* @__PURE__ */ jsxs("button", { type: "button", className: "cv-btn cv-settings-test", onClick: test, disabled: testing, children: [
|
|
2663
|
+
/* @__PURE__ */ jsx(Volume2, { size: 16, "aria-hidden": true }),
|
|
2664
|
+
testing ? "Sonando\u2026" : "Probar altavoz"
|
|
2665
|
+
] });
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// src/ui/audio/preview.ts
|
|
2669
|
+
var PREVIEW_MAX_MS = 1e4;
|
|
2670
|
+
var BUILT_IN_PREVIEW_MS = 6e3;
|
|
2671
|
+
function previewDurationMs(buffer, loop) {
|
|
2672
|
+
if (!buffer) return BUILT_IN_PREVIEW_MS;
|
|
2673
|
+
const seconds = loop ? loop.end - loop.start : buffer.duration;
|
|
2674
|
+
return Math.min(seconds * 1e3, PREVIEW_MAX_MS);
|
|
2675
|
+
}
|
|
2676
|
+
function previewRingtone(ringtone, options = {}) {
|
|
2677
|
+
primeAudio();
|
|
2678
|
+
let tone = null;
|
|
2679
|
+
let timer = null;
|
|
2680
|
+
let over = false;
|
|
2681
|
+
let settle;
|
|
2682
|
+
let fail;
|
|
2683
|
+
const done = new Promise((resolve, reject) => {
|
|
2684
|
+
settle = resolve;
|
|
2685
|
+
fail = reject;
|
|
2686
|
+
});
|
|
2687
|
+
done.catch(() => {
|
|
2688
|
+
});
|
|
2689
|
+
const stop = () => {
|
|
2690
|
+
if (over) return;
|
|
2691
|
+
over = true;
|
|
2692
|
+
if (timer) clearTimeout(timer);
|
|
2693
|
+
tone?.stop();
|
|
2694
|
+
settle();
|
|
2695
|
+
};
|
|
2696
|
+
const play = (buffer) => {
|
|
2697
|
+
if (over) return;
|
|
2698
|
+
const loop = buffer ? ringtone.loop ?? null : null;
|
|
2699
|
+
tone = playRingtone({ buffer, loop, sinkId: options.sinkId ?? null });
|
|
2700
|
+
timer = setTimeout(stop, previewDurationMs(buffer, loop));
|
|
2701
|
+
};
|
|
2702
|
+
if (!ringtone.url) {
|
|
2703
|
+
play(null);
|
|
2704
|
+
} else {
|
|
2705
|
+
decodeRingtone(ringtone.url).then(play, (error) => {
|
|
2706
|
+
if (over) return;
|
|
2707
|
+
over = true;
|
|
2708
|
+
fail(error);
|
|
2709
|
+
});
|
|
2710
|
+
}
|
|
2711
|
+
return { stop, done };
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
// src/ui/views/settings/ringtone-state.ts
|
|
2715
|
+
var noSubscribe = () => () => {
|
|
2716
|
+
};
|
|
2717
|
+
var noSettings = () => null;
|
|
2718
|
+
function useRingtoneSettings(store) {
|
|
2719
|
+
const settings = useSyncExternalStore(store?.subscribe ?? noSubscribe, store?.getSettings ?? noSettings);
|
|
2720
|
+
const [failed, setFailed] = useState(false);
|
|
2721
|
+
const load = useCallback(() => {
|
|
2722
|
+
if (!store) return () => {
|
|
2723
|
+
};
|
|
2724
|
+
let live = true;
|
|
2725
|
+
setFailed(false);
|
|
2726
|
+
void store.refresh().then(() => {
|
|
2727
|
+
if (live) setFailed(store.getSettings() === null);
|
|
2728
|
+
});
|
|
2729
|
+
return () => {
|
|
2730
|
+
live = false;
|
|
2731
|
+
};
|
|
2732
|
+
}, [store]);
|
|
2733
|
+
useEffect(load, [load]);
|
|
2734
|
+
return { settings, failed, retry: load };
|
|
2735
|
+
}
|
|
2736
|
+
function ringtoneSummary(settings, failed) {
|
|
2737
|
+
if (!settings) return failed ? "No disponible" : "Cargando\u2026";
|
|
2738
|
+
const byId = (id) => id ? settings.catalogue.find((r) => r.id === id) : void 0;
|
|
2739
|
+
const mine = byId(settings.mine);
|
|
2740
|
+
if (mine) return mine.name;
|
|
2741
|
+
const org = byId(settings.orgDefault);
|
|
2742
|
+
if (org) return `${org.name} \xB7 organizaci\xF3n`;
|
|
2743
|
+
return settings.catalogue.find((r) => r.url === null)?.name ?? "Predeterminado";
|
|
2744
|
+
}
|
|
2745
|
+
function useRingtonePreview(store, sinkId) {
|
|
2746
|
+
const [playing, setPlaying] = useState(null);
|
|
2747
|
+
const [error, setError] = useState(null);
|
|
2748
|
+
const current = useRef(null);
|
|
2749
|
+
const generation = useRef(0);
|
|
2750
|
+
const stop = useCallback(() => {
|
|
2751
|
+
generation.current += 1;
|
|
2752
|
+
const c = current.current;
|
|
2753
|
+
current.current = null;
|
|
2754
|
+
if (c?.timer) clearTimeout(c.timer);
|
|
2755
|
+
c?.tone?.stop();
|
|
2756
|
+
setPlaying(null);
|
|
2757
|
+
}, []);
|
|
2758
|
+
useEffect(() => stop, [stop]);
|
|
2759
|
+
const start = (settings, choice) => {
|
|
2760
|
+
stop();
|
|
2761
|
+
primeAudio();
|
|
2762
|
+
setError(null);
|
|
2763
|
+
const gen = generation.current;
|
|
2764
|
+
const orgDefault = settings.catalogue.some((r) => r.id === settings.orgDefault) ? settings.orgDefault : null;
|
|
2765
|
+
const id = choice || orgDefault;
|
|
2766
|
+
const option = settings.catalogue.find((r) => r.id === id) ?? settings.catalogue.find((r) => r.url === null);
|
|
2767
|
+
setPlaying(choice);
|
|
2768
|
+
current.current = { tone: null, timer: null };
|
|
2769
|
+
const play = (buffer) => {
|
|
2770
|
+
if (gen !== generation.current) return;
|
|
2771
|
+
const loop = buffer ? option?.loop ?? null : null;
|
|
2772
|
+
const tone = playRingtone({ buffer, loop, sinkId });
|
|
2773
|
+
current.current = { tone, timer: setTimeout(stop, previewDurationMs(buffer, loop)) };
|
|
2774
|
+
};
|
|
2775
|
+
if (!option?.url) {
|
|
2776
|
+
play(null);
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
store.decode(option.url).then(play, () => {
|
|
2780
|
+
if (gen !== generation.current) return;
|
|
2781
|
+
stop();
|
|
2782
|
+
setError("No se pudo reproducir el tono.");
|
|
2783
|
+
});
|
|
2784
|
+
};
|
|
2785
|
+
return { playing, error, start, stop };
|
|
2786
|
+
}
|
|
2787
|
+
var ORG_DEFAULT = "";
|
|
2788
|
+
var KEYBOARD_SETTLE_MS = 300;
|
|
2789
|
+
function RingtonePage({
|
|
2790
|
+
store,
|
|
2791
|
+
settings,
|
|
2792
|
+
failed,
|
|
2793
|
+
onRetry,
|
|
2794
|
+
onBack
|
|
2795
|
+
}) {
|
|
2796
|
+
const softphone = useSoftphone();
|
|
2797
|
+
const devices = useDevices();
|
|
2798
|
+
const preview = useRingtonePreview(store, devices.canSelectRingtoneOutput ? devices.currentRingtoneOutputId : null);
|
|
2799
|
+
const [pending, setPending] = useState(void 0);
|
|
2800
|
+
const [error, setError] = useState(null);
|
|
2801
|
+
const save = useSerialSave(store, {
|
|
2802
|
+
onSettled: () => setPending(void 0),
|
|
2803
|
+
onFailed: () => {
|
|
2804
|
+
setPending(void 0);
|
|
2805
|
+
setError("No se pudo guardar el tono. Int\xE9ntalo de nuevo.");
|
|
2806
|
+
}
|
|
2807
|
+
});
|
|
2808
|
+
const latest = useRef(settings);
|
|
2809
|
+
latest.current = settings;
|
|
2810
|
+
const settle = useRef(null);
|
|
2811
|
+
const { stop: stopPreview } = preview;
|
|
2812
|
+
useEffect(() => softphone.on("incomingCall" /* IncomingCall */, stopPreview), [softphone, stopPreview]);
|
|
2813
|
+
useEffect(
|
|
2814
|
+
() => () => {
|
|
2815
|
+
if (settle.current) {
|
|
2816
|
+
clearTimeout(settle.current.timer);
|
|
2817
|
+
save(settle.current.value || null);
|
|
2818
|
+
}
|
|
2819
|
+
},
|
|
2820
|
+
[save]
|
|
2821
|
+
);
|
|
2822
|
+
const commit = (value) => {
|
|
2823
|
+
save(value || null);
|
|
2824
|
+
if (latest.current) preview.start(latest.current, value);
|
|
2825
|
+
};
|
|
2826
|
+
const onSelect = (value, via) => {
|
|
2827
|
+
setError(null);
|
|
2828
|
+
setPending(value);
|
|
2829
|
+
if (settle.current) clearTimeout(settle.current.timer);
|
|
2830
|
+
settle.current = null;
|
|
2831
|
+
if (via === "pointer") {
|
|
2832
|
+
commit(value);
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
preview.stop();
|
|
2836
|
+
settle.current = {
|
|
2837
|
+
value,
|
|
2838
|
+
timer: setTimeout(() => {
|
|
2839
|
+
settle.current = null;
|
|
2840
|
+
commit(value);
|
|
2841
|
+
}, KEYBOARD_SETTLE_MS)
|
|
2842
|
+
};
|
|
2843
|
+
};
|
|
2844
|
+
const onReselect = (value) => {
|
|
2845
|
+
if (!settings) return;
|
|
2846
|
+
if (preview.playing === value) preview.stop();
|
|
2847
|
+
else preview.start(settings, value);
|
|
2848
|
+
};
|
|
2849
|
+
return /* @__PURE__ */ jsx(
|
|
2850
|
+
SettingsPage,
|
|
2851
|
+
{
|
|
2852
|
+
title: "Tono de llamada",
|
|
2853
|
+
intro: "Lo que suena cuando entra una llamada. Se guarda en tu cuenta, as\xED que es el mismo en todos tus dispositivos.",
|
|
2854
|
+
onBack,
|
|
2855
|
+
children: settings ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2856
|
+
/* @__PURE__ */ jsx(
|
|
2857
|
+
ChoiceList,
|
|
2858
|
+
{
|
|
2859
|
+
label: "Tono de llamada",
|
|
2860
|
+
choices: choicesFor(settings, preview.playing),
|
|
2861
|
+
value: pending !== void 0 ? pending : currentValue(settings),
|
|
2862
|
+
onSelect,
|
|
2863
|
+
onReselect
|
|
2864
|
+
}
|
|
2865
|
+
),
|
|
2866
|
+
error ?? preview.error ? /* @__PURE__ */ jsx("p", { className: "cv-settings-error", role: "alert", children: error ?? preview.error }) : null
|
|
2867
|
+
] }) : failed ? /* @__PURE__ */ jsxs("div", { className: "cv-empty", children: [
|
|
2868
|
+
/* @__PURE__ */ jsx("p", { className: "cv-empty-title", children: "No se pudieron cargar los tonos." }),
|
|
2869
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "cv-btn cv-settings-retry", onClick: onRetry, children: "Reintentar" })
|
|
2870
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "cv-empty", role: "status", children: [
|
|
2871
|
+
/* @__PURE__ */ jsx(LoaderCircle, { className: "cv-empty-spinner", size: 18, "aria-hidden": true }),
|
|
2872
|
+
/* @__PURE__ */ jsx("span", { className: "cv-empty-title", children: "Cargando\u2026" })
|
|
2873
|
+
] })
|
|
2874
|
+
}
|
|
2875
|
+
);
|
|
2876
|
+
}
|
|
2877
|
+
function currentValue(settings) {
|
|
2878
|
+
return settings.mine && settings.catalogue.some((r) => r.id === settings.mine) ? settings.mine : ORG_DEFAULT;
|
|
2879
|
+
}
|
|
2880
|
+
function choicesFor(settings, playing) {
|
|
2881
|
+
const org = settings.orgDefault ? settings.catalogue.find((r) => r.id === settings.orgDefault) : void 0;
|
|
2882
|
+
const builtIn = settings.catalogue.find((r) => r.url === null);
|
|
2883
|
+
const mark = (value) => playing === value ? /* @__PURE__ */ jsxs("span", { className: "cv-choice-playing", children: [
|
|
2884
|
+
/* @__PURE__ */ jsx(Volume2, { size: 15, "aria-hidden": true }),
|
|
2885
|
+
/* @__PURE__ */ jsx("span", { className: "cv-sr-only", children: "Sonando" })
|
|
2886
|
+
] }) : void 0;
|
|
2887
|
+
const orgChoice = {
|
|
2888
|
+
value: ORG_DEFAULT,
|
|
2889
|
+
label: org ? "Predeterminado de la organizaci\xF3n" : "Predeterminado",
|
|
2890
|
+
sub: (org ?? builtIn)?.name ?? "",
|
|
2891
|
+
trailing: mark(ORG_DEFAULT)
|
|
2892
|
+
};
|
|
2893
|
+
return [orgChoice, ...settings.catalogue.map((r) => ({ value: r.id, label: r.name, trailing: mark(r.id) }))];
|
|
2894
|
+
}
|
|
2895
|
+
function useSerialSave(store, handlers) {
|
|
2896
|
+
const wanted = useRef(null);
|
|
2897
|
+
const busy = useRef(false);
|
|
2898
|
+
const on = useRef(handlers);
|
|
2899
|
+
on.current = handlers;
|
|
2900
|
+
const [save] = useState(() => (id) => {
|
|
2901
|
+
wanted.current = { id };
|
|
2902
|
+
if (busy.current) return;
|
|
2903
|
+
busy.current = true;
|
|
2904
|
+
void (async () => {
|
|
2905
|
+
while (wanted.current) {
|
|
2906
|
+
const { id: next } = wanted.current;
|
|
2907
|
+
wanted.current = null;
|
|
2908
|
+
try {
|
|
2909
|
+
await store.setMine(next);
|
|
2910
|
+
if (!wanted.current) on.current.onSettled();
|
|
2911
|
+
} catch {
|
|
2912
|
+
if (!wanted.current) on.current.onFailed();
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
busy.current = false;
|
|
2916
|
+
})();
|
|
2917
|
+
});
|
|
2918
|
+
return save;
|
|
2919
|
+
}
|
|
2920
|
+
function SettingsTab() {
|
|
2921
|
+
const softphone = useSoftphone();
|
|
2922
|
+
const devices = useDevices();
|
|
2923
|
+
const store = ringtoneStore(softphone);
|
|
2924
|
+
const ringtone = useRingtoneSettings(store);
|
|
2925
|
+
const [page, setPage] = useState(null);
|
|
2926
|
+
const list = useRef(null);
|
|
2927
|
+
const rows = useRef({});
|
|
2928
|
+
const returnTo = useRef(null);
|
|
2929
|
+
useEffect(() => {
|
|
2930
|
+
void softphone.audio.refresh();
|
|
2931
|
+
}, [softphone]);
|
|
2932
|
+
useEffect(() => {
|
|
2933
|
+
list.current?.toggleAttribute("inert", page !== null);
|
|
2934
|
+
if (page === null && returnTo.current) {
|
|
2935
|
+
rows.current[returnTo.current]?.focus();
|
|
2936
|
+
returnTo.current = null;
|
|
2937
|
+
}
|
|
2938
|
+
}, [page]);
|
|
2939
|
+
const open = (p) => () => {
|
|
2940
|
+
returnTo.current = p;
|
|
2941
|
+
setPage(p);
|
|
2942
|
+
};
|
|
2943
|
+
const back = () => setPage(null);
|
|
2944
|
+
const rowRef = (p) => (el) => {
|
|
2945
|
+
rows.current[p] = el;
|
|
2946
|
+
};
|
|
2947
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-view cv-settings", children: [
|
|
2948
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-settings-body", ref: list, children: [
|
|
2949
|
+
store ? /* @__PURE__ */ jsx(SettingsSection, { title: "Llamadas entrantes", children: /* @__PURE__ */ jsx(
|
|
2950
|
+
SettingsNavRow,
|
|
2951
|
+
{
|
|
2952
|
+
icon: Music,
|
|
2953
|
+
title: "Tono de llamada",
|
|
2954
|
+
value: ringtoneSummary(ringtone.settings, ringtone.failed),
|
|
2955
|
+
onOpen: open("tone"),
|
|
2956
|
+
rowRef: rowRef("tone")
|
|
2957
|
+
}
|
|
2958
|
+
) }) : null,
|
|
2959
|
+
/* @__PURE__ */ jsxs(SettingsSection, { title: "Dispositivos", children: [
|
|
2960
|
+
/* @__PURE__ */ jsx(
|
|
2961
|
+
SettingsNavRow,
|
|
2962
|
+
{
|
|
2963
|
+
icon: Mic,
|
|
2964
|
+
title: "Micr\xF3fono",
|
|
2965
|
+
value: deviceSummary("input", devices.inputs, devices.currentInputId),
|
|
2966
|
+
onOpen: open("input"),
|
|
2967
|
+
rowRef: rowRef("input")
|
|
2968
|
+
}
|
|
2969
|
+
),
|
|
2970
|
+
devices.canSelectOutput ? /* @__PURE__ */ jsx(
|
|
2971
|
+
SettingsNavRow,
|
|
2972
|
+
{
|
|
2973
|
+
icon: Headphones,
|
|
2974
|
+
title: "Altavoces",
|
|
2975
|
+
value: deviceSummary("output", devices.outputs, devices.currentOutputId),
|
|
2976
|
+
onOpen: open("output"),
|
|
2977
|
+
rowRef: rowRef("output")
|
|
2978
|
+
}
|
|
2979
|
+
) : (
|
|
2980
|
+
// Safari and most of Firefox have no `setSinkId`: offering a page whose choice silently does
|
|
2981
|
+
// nothing is worse than saying where the choice really lives.
|
|
2982
|
+
/* @__PURE__ */ jsx(
|
|
2983
|
+
SettingsNavRow,
|
|
2984
|
+
{
|
|
2985
|
+
icon: Headphones,
|
|
2986
|
+
title: "Altavoces",
|
|
2987
|
+
value: "Tu navegador no permite elegirlos desde aqu\xED \u2014 c\xE1mbialos en los ajustes del sistema."
|
|
2988
|
+
}
|
|
2989
|
+
)
|
|
2990
|
+
),
|
|
2991
|
+
devices.canSelectRingtoneOutput ? /* @__PURE__ */ jsx(
|
|
2992
|
+
SettingsNavRow,
|
|
2993
|
+
{
|
|
2994
|
+
icon: BellRing,
|
|
2995
|
+
title: "Salida del tono",
|
|
2996
|
+
value: deviceSummary("ringtone", devices.outputs, devices.currentRingtoneOutputId),
|
|
2997
|
+
onOpen: open("ringtone"),
|
|
2998
|
+
rowRef: rowRef("ringtone")
|
|
2999
|
+
}
|
|
3000
|
+
) : null
|
|
3001
|
+
] })
|
|
3002
|
+
] }),
|
|
3003
|
+
page === "tone" && store ? /* @__PURE__ */ jsx(
|
|
3004
|
+
RingtonePage,
|
|
3005
|
+
{
|
|
3006
|
+
store,
|
|
3007
|
+
settings: ringtone.settings,
|
|
3008
|
+
failed: ringtone.failed,
|
|
3009
|
+
onRetry: ringtone.retry,
|
|
3010
|
+
onBack: back
|
|
3011
|
+
}
|
|
3012
|
+
) : page !== null && page !== "tone" ? /* @__PURE__ */ jsx(DevicePage, { kind: page, onBack: back }) : null
|
|
3013
|
+
] });
|
|
3014
|
+
}
|
|
1900
3015
|
function useMicPermission() {
|
|
1901
3016
|
const [state, setState] = useState("checking");
|
|
1902
3017
|
const warm = useCallback(async () => {
|
|
@@ -2108,203 +3223,6 @@ function useCallOutcome() {
|
|
|
2108
3223
|
clear
|
|
2109
3224
|
};
|
|
2110
3225
|
}
|
|
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
3226
|
function useCallAudio(enabled) {
|
|
2309
3227
|
const softphone = useSoftphone();
|
|
2310
3228
|
const remote = useRef(null);
|
|
@@ -2321,14 +3239,29 @@ function useCallAudio(enabled) {
|
|
|
2321
3239
|
const offDeviceChange = softphone.on("deviceChange" /* DeviceChange */, () => {
|
|
2322
3240
|
void remote.current?.setOutputDevice(softphone.audio.currentOutputId);
|
|
2323
3241
|
});
|
|
3242
|
+
const ringtones = ringtoneStore(softphone);
|
|
3243
|
+
ringtones?.wantAudio(true);
|
|
3244
|
+
const refreshRingtone = () => {
|
|
3245
|
+
if (softphone.isLeader) void ringtones?.refresh();
|
|
3246
|
+
};
|
|
3247
|
+
refreshRingtone();
|
|
3248
|
+
const offLeader = softphone.on("leaderChanged" /* LeaderChanged */, ({ isLeader }) => {
|
|
3249
|
+
if (isLeader) refreshRingtone();
|
|
3250
|
+
});
|
|
2324
3251
|
const stopTone = () => {
|
|
2325
3252
|
tone.current?.stop();
|
|
2326
3253
|
tone.current = null;
|
|
2327
3254
|
};
|
|
2328
3255
|
const setTone = (next) => {
|
|
2329
3256
|
stopTone();
|
|
2330
|
-
if (next === "ringtone")
|
|
2331
|
-
|
|
3257
|
+
if (next === "ringtone") {
|
|
3258
|
+
const audio = softphone.audio;
|
|
3259
|
+
tone.current = playRingtone({
|
|
3260
|
+
buffer: ringtones?.readyBuffer() ?? null,
|
|
3261
|
+
loop: ringtones?.readyLoop() ?? null,
|
|
3262
|
+
sinkId: audio.canSelectRingtoneOutput ? audio.currentRingtoneOutputId : null
|
|
3263
|
+
});
|
|
3264
|
+
} else if (next === "ringback") tone.current = playRingback();
|
|
2332
3265
|
};
|
|
2333
3266
|
const perCall = [];
|
|
2334
3267
|
const followedCalls = /* @__PURE__ */ new Set();
|
|
@@ -2359,6 +3292,7 @@ function useCallAudio(enabled) {
|
|
|
2359
3292
|
stopTone();
|
|
2360
3293
|
remote.current?.detach();
|
|
2361
3294
|
if (wasConnected && (reason === "local" || reason === "remote")) playCallEndedTone();
|
|
3295
|
+
refreshRingtone();
|
|
2362
3296
|
})
|
|
2363
3297
|
);
|
|
2364
3298
|
};
|
|
@@ -2376,6 +3310,8 @@ function useCallAudio(enabled) {
|
|
|
2376
3310
|
offIncoming();
|
|
2377
3311
|
offActive();
|
|
2378
3312
|
offDeviceChange();
|
|
3313
|
+
offLeader();
|
|
3314
|
+
ringtones?.wantAudio(false);
|
|
2379
3315
|
for (const off of perCall) off();
|
|
2380
3316
|
stopTone();
|
|
2381
3317
|
remote.current?.destroy();
|
|
@@ -2522,7 +3458,7 @@ function SoftphoneWidget(props) {
|
|
|
2522
3458
|
...theme === "system" ? {} : { "data-cv-theme": theme },
|
|
2523
3459
|
children: [
|
|
2524
3460
|
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 }) }),
|
|
3461
|
+
/* @__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
3462
|
screenIsFree ? /* @__PURE__ */ jsx("div", { className: "cv-tabs", role: "tablist", children: tabs.map(({ id, icon: Icon3, label }) => /* @__PURE__ */ jsxs(
|
|
2527
3463
|
"button",
|
|
2528
3464
|
{
|
|
@@ -2550,6 +3486,7 @@ lucide-react/dist/esm/createLucideIcon.js:
|
|
|
2550
3486
|
lucide-react/dist/esm/icons/arrow-left-right.js:
|
|
2551
3487
|
lucide-react/dist/esm/icons/arrow-left.js:
|
|
2552
3488
|
lucide-react/dist/esm/icons/arrow-right.js:
|
|
3489
|
+
lucide-react/dist/esm/icons/bell-ring.js:
|
|
2553
3490
|
lucide-react/dist/esm/icons/calendar-clock.js:
|
|
2554
3491
|
lucide-react/dist/esm/icons/check.js:
|
|
2555
3492
|
lucide-react/dist/esm/icons/chevron-down.js:
|
|
@@ -2565,6 +3502,7 @@ lucide-react/dist/esm/icons/loader-circle.js:
|
|
|
2565
3502
|
lucide-react/dist/esm/icons/mic-off.js:
|
|
2566
3503
|
lucide-react/dist/esm/icons/mic.js:
|
|
2567
3504
|
lucide-react/dist/esm/icons/monitor-smartphone.js:
|
|
3505
|
+
lucide-react/dist/esm/icons/music.js:
|
|
2568
3506
|
lucide-react/dist/esm/icons/pause.js:
|
|
2569
3507
|
lucide-react/dist/esm/icons/phone-incoming.js:
|
|
2570
3508
|
lucide-react/dist/esm/icons/phone-missed.js:
|
|
@@ -2578,6 +3516,7 @@ lucide-react/dist/esm/icons/timer.js:
|
|
|
2578
3516
|
lucide-react/dist/esm/icons/triangle-alert.js:
|
|
2579
3517
|
lucide-react/dist/esm/icons/user.js:
|
|
2580
3518
|
lucide-react/dist/esm/icons/users.js:
|
|
3519
|
+
lucide-react/dist/esm/icons/volume-2.js:
|
|
2581
3520
|
lucide-react/dist/esm/icons/x.js:
|
|
2582
3521
|
lucide-react/dist/esm/lucide-react.js:
|
|
2583
3522
|
(**
|
|
@@ -2588,6 +3527,6 @@ lucide-react/dist/esm/lucide-react.js:
|
|
|
2588
3527
|
*)
|
|
2589
3528
|
*/
|
|
2590
3529
|
|
|
2591
|
-
export { CallScreen, ContactsTab, Dialpad, HistoryTab, MicPermissionGate, SoftphoneWidget };
|
|
3530
|
+
export { CallScreen, ContactsTab, Dialpad, HistoryTab, MicPermissionGate, SoftphoneWidget, previewRingtone };
|
|
2592
3531
|
//# sourceMappingURL=index.js.map
|
|
2593
3532
|
//# sourceMappingURL=index.js.map
|