@coventia/softphone 0.2.1 → 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-VKq1I8yP.d.cts → call-CaPDWVk3.d.cts} +44 -1
- package/dist/{call-VKq1I8yP.d.ts → call-CaPDWVk3.d.ts} +44 -1
- package/dist/{chunk-LMJXX4YM.js → chunk-UHNLEBBO.js} +4 -2
- package/dist/chunk-UHNLEBBO.js.map +1 -0
- package/dist/element/index.d.ts +68 -9
- package/dist/element/index.js +962 -114
- package/dist/element/index.js.map +1 -1
- package/dist/{engine-C0BOkIcW.d.ts → engine-BgBsIcEa.d.ts} +1 -1
- package/dist/{engine-BlQBo2Ys.d.cts → engine-CU2rEo3g.d.cts} +1 -1
- package/dist/index.cjs +120 -10
- 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 +69 -10
- 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-Cyq3hiIM.d.ts → softphone-DEt0TtQw.d.ts} +26 -10
- package/dist/{softphone-DlGV12Xp.d.cts → softphone-dZMkW8V1.d.cts} +26 -10
- package/dist/softphone.css +110 -12
- package/dist/{sources-IRKNQPWW.js → sources-M3GYMS6V.js} +54 -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 +885 -28
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +25 -3
- package/dist/ui/index.d.ts +25 -3
- package/dist/ui/index.js +885 -31
- package/dist/ui/index.js.map +1 -1
- package/dist/zadarma/index.d.cts +2 -2
- package/dist/zadarma/index.d.ts +2 -2
- package/package.json +1 -1
- package/dist/chunk-LMJXX4YM.js.map +0 -1
- package/dist/sources-IRKNQPWW.js.map +0 -1
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" }],
|
|
@@ -567,6 +595,59 @@ var KEYPAD_KEYS = [
|
|
|
567
595
|
["#", ""]
|
|
568
596
|
];
|
|
569
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
|
+
|
|
570
651
|
// src/ui/audio/audio.ts
|
|
571
652
|
var TONE_HZ = 425;
|
|
572
653
|
function createRemoteAudio() {
|
|
@@ -609,30 +690,159 @@ function primeAudio() {
|
|
|
609
690
|
if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
|
|
610
691
|
});
|
|
611
692
|
}
|
|
612
|
-
function playRingtone() {
|
|
693
|
+
function playRingtone(options = {}) {
|
|
613
694
|
const ctx = sharedContext();
|
|
614
695
|
if (!ctx) return { stop() {
|
|
615
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
|
+
}
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function fileVoice(ctx, bus, buffer, loop) {
|
|
708
|
+
const FILE_LEVEL = 1;
|
|
709
|
+
const RAMP = 0.02;
|
|
710
|
+
const LEAD = 0.08;
|
|
616
711
|
const gain = ctx.createGain();
|
|
617
712
|
gain.gain.value = 0;
|
|
618
|
-
gain.connect(
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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;
|
|
627
728
|
gain.gain.setValueAtTime(0, at);
|
|
628
|
-
gain.gain.linearRampToValueAtTime(
|
|
629
|
-
|
|
630
|
-
|
|
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 {
|
|
738
|
+
}
|
|
739
|
+
try {
|
|
740
|
+
gain.disconnect();
|
|
741
|
+
} catch {
|
|
742
|
+
}
|
|
743
|
+
}
|
|
631
744
|
};
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
745
|
+
}
|
|
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
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function safeDisconnect(node) {
|
|
778
|
+
try {
|
|
779
|
+
node.disconnect();
|
|
780
|
+
} catch {
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function teardown(el) {
|
|
784
|
+
el.pause();
|
|
785
|
+
el.srcObject = null;
|
|
786
|
+
el.remove();
|
|
787
|
+
}
|
|
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);
|
|
636
846
|
};
|
|
637
847
|
const LOOKAHEAD = 0.5;
|
|
638
848
|
const TICK_MS = 150;
|
|
@@ -641,7 +851,7 @@ function playRingtone() {
|
|
|
641
851
|
let timer = null;
|
|
642
852
|
let nextAt = 0;
|
|
643
853
|
const pump = () => {
|
|
644
|
-
while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt +=
|
|
854
|
+
while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleClassicCycle(ctx, input, nextAt, track);
|
|
645
855
|
};
|
|
646
856
|
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
647
857
|
}).finally(() => {
|
|
@@ -654,12 +864,15 @@ function playRingtone() {
|
|
|
654
864
|
stop() {
|
|
655
865
|
stopped = true;
|
|
656
866
|
if (timer) clearInterval(timer);
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
867
|
+
for (const o of scheduled) {
|
|
868
|
+
try {
|
|
869
|
+
o.stop();
|
|
870
|
+
} catch {
|
|
871
|
+
}
|
|
660
872
|
}
|
|
873
|
+
scheduled.clear();
|
|
661
874
|
try {
|
|
662
|
-
|
|
875
|
+
output.disconnect();
|
|
663
876
|
} catch {
|
|
664
877
|
}
|
|
665
878
|
}
|
|
@@ -2164,6 +2377,110 @@ function HistoryTab({ source }) {
|
|
|
2164
2377
|
selected ? /* @__PURE__ */ jsx(HistoryDetail, { entry: selected, onBack: () => setSelected(null) }) : null
|
|
2165
2378
|
] });
|
|
2166
2379
|
}
|
|
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
|
+
}
|
|
2167
2484
|
function SettingsSection({
|
|
2168
2485
|
title,
|
|
2169
2486
|
children
|
|
@@ -2173,11 +2490,527 @@ function SettingsSection({
|
|
|
2173
2490
|
/* @__PURE__ */ jsx("div", { className: "cv-settings-section-body", children })
|
|
2174
2491
|
] });
|
|
2175
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
|
+
}
|
|
2176
2920
|
function SettingsTab() {
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
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
|
+
] });
|
|
2181
3014
|
}
|
|
2182
3015
|
function useMicPermission() {
|
|
2183
3016
|
const [state, setState] = useState("checking");
|
|
@@ -2406,14 +3239,29 @@ function useCallAudio(enabled) {
|
|
|
2406
3239
|
const offDeviceChange = softphone.on("deviceChange" /* DeviceChange */, () => {
|
|
2407
3240
|
void remote.current?.setOutputDevice(softphone.audio.currentOutputId);
|
|
2408
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
|
+
});
|
|
2409
3251
|
const stopTone = () => {
|
|
2410
3252
|
tone.current?.stop();
|
|
2411
3253
|
tone.current = null;
|
|
2412
3254
|
};
|
|
2413
3255
|
const setTone = (next) => {
|
|
2414
3256
|
stopTone();
|
|
2415
|
-
if (next === "ringtone")
|
|
2416
|
-
|
|
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();
|
|
2417
3265
|
};
|
|
2418
3266
|
const perCall = [];
|
|
2419
3267
|
const followedCalls = /* @__PURE__ */ new Set();
|
|
@@ -2444,6 +3292,7 @@ function useCallAudio(enabled) {
|
|
|
2444
3292
|
stopTone();
|
|
2445
3293
|
remote.current?.detach();
|
|
2446
3294
|
if (wasConnected && (reason === "local" || reason === "remote")) playCallEndedTone();
|
|
3295
|
+
refreshRingtone();
|
|
2447
3296
|
})
|
|
2448
3297
|
);
|
|
2449
3298
|
};
|
|
@@ -2461,6 +3310,8 @@ function useCallAudio(enabled) {
|
|
|
2461
3310
|
offIncoming();
|
|
2462
3311
|
offActive();
|
|
2463
3312
|
offDeviceChange();
|
|
3313
|
+
offLeader();
|
|
3314
|
+
ringtones?.wantAudio(false);
|
|
2464
3315
|
for (const off of perCall) off();
|
|
2465
3316
|
stopTone();
|
|
2466
3317
|
remote.current?.destroy();
|
|
@@ -2635,6 +3486,7 @@ lucide-react/dist/esm/createLucideIcon.js:
|
|
|
2635
3486
|
lucide-react/dist/esm/icons/arrow-left-right.js:
|
|
2636
3487
|
lucide-react/dist/esm/icons/arrow-left.js:
|
|
2637
3488
|
lucide-react/dist/esm/icons/arrow-right.js:
|
|
3489
|
+
lucide-react/dist/esm/icons/bell-ring.js:
|
|
2638
3490
|
lucide-react/dist/esm/icons/calendar-clock.js:
|
|
2639
3491
|
lucide-react/dist/esm/icons/check.js:
|
|
2640
3492
|
lucide-react/dist/esm/icons/chevron-down.js:
|
|
@@ -2650,6 +3502,7 @@ lucide-react/dist/esm/icons/loader-circle.js:
|
|
|
2650
3502
|
lucide-react/dist/esm/icons/mic-off.js:
|
|
2651
3503
|
lucide-react/dist/esm/icons/mic.js:
|
|
2652
3504
|
lucide-react/dist/esm/icons/monitor-smartphone.js:
|
|
3505
|
+
lucide-react/dist/esm/icons/music.js:
|
|
2653
3506
|
lucide-react/dist/esm/icons/pause.js:
|
|
2654
3507
|
lucide-react/dist/esm/icons/phone-incoming.js:
|
|
2655
3508
|
lucide-react/dist/esm/icons/phone-missed.js:
|
|
@@ -2663,6 +3516,7 @@ lucide-react/dist/esm/icons/timer.js:
|
|
|
2663
3516
|
lucide-react/dist/esm/icons/triangle-alert.js:
|
|
2664
3517
|
lucide-react/dist/esm/icons/user.js:
|
|
2665
3518
|
lucide-react/dist/esm/icons/users.js:
|
|
3519
|
+
lucide-react/dist/esm/icons/volume-2.js:
|
|
2666
3520
|
lucide-react/dist/esm/icons/x.js:
|
|
2667
3521
|
lucide-react/dist/esm/lucide-react.js:
|
|
2668
3522
|
(**
|
|
@@ -2673,6 +3527,6 @@ lucide-react/dist/esm/lucide-react.js:
|
|
|
2673
3527
|
*)
|
|
2674
3528
|
*/
|
|
2675
3529
|
|
|
2676
|
-
export { CallScreen, ContactsTab, Dialpad, HistoryTab, MicPermissionGate, SoftphoneWidget };
|
|
3530
|
+
export { CallScreen, ContactsTab, Dialpad, HistoryTab, MicPermissionGate, SoftphoneWidget, previewRingtone };
|
|
2677
3531
|
//# sourceMappingURL=index.js.map
|
|
2678
3532
|
//# sourceMappingURL=index.js.map
|