@coventia/softphone 0.2.1 → 0.4.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-C6YpT6uR.d.cts} +107 -1
- package/dist/{call-VKq1I8yP.d.ts → call-C6YpT6uR.d.ts} +107 -1
- package/dist/chunk-5E5OEXS2.js +14 -0
- package/dist/chunk-5E5OEXS2.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 +124 -9
- package/dist/element/index.js +1837 -413
- package/dist/element/index.js.map +1 -1
- package/dist/{engine-BlQBo2Ys.d.cts → engine-CDdaSojx.d.cts} +1 -1
- package/dist/{engine-C0BOkIcW.d.ts → engine-DPft99Wj.d.ts} +1 -1
- package/dist/index.cjs +213 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -6
- package/dist/index.d.ts +16 -6
- package/dist/index.js +75 -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-DlGV12Xp.d.cts → softphone-CY5d2WWY.d.cts} +28 -10
- package/dist/{softphone-Cyq3hiIM.d.ts → softphone-DXgmTyrX.d.ts} +28 -10
- package/dist/softphone.css +364 -35
- package/dist/{sources-IRKNQPWW.js → sources-ONYQW733.js} +125 -6
- package/dist/sources-ONYQW733.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 +1450 -33
- 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 +1445 -36
- 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,8 @@
|
|
|
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 {
|
|
4
|
+
import { isOutboundNumberRefused } from '../chunk-5E5OEXS2.js';
|
|
5
|
+
import { forwardRef, createElement, useState, useRef, useLayoutEffect, useEffect, useCallback, useMemo, useSyncExternalStore, useId } from 'react';
|
|
5
6
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
7
|
|
|
7
8
|
// ../../node_modules/lucide-react/dist/esm/shared/src/utils.js
|
|
@@ -89,6 +90,14 @@ var ArrowRight = createLucideIcon("ArrowRight", [
|
|
|
89
90
|
["path", { d: "m12 5 7 7-7 7", key: "xquz4c" }]
|
|
90
91
|
]);
|
|
91
92
|
|
|
93
|
+
// ../../node_modules/lucide-react/dist/esm/icons/bell-ring.js
|
|
94
|
+
var BellRing = createLucideIcon("BellRing", [
|
|
95
|
+
["path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9", key: "1qo2s2" }],
|
|
96
|
+
["path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0", key: "qgo35s" }],
|
|
97
|
+
["path", { d: "M4 2C2.8 3.7 2 5.7 2 8", key: "tap9e0" }],
|
|
98
|
+
["path", { d: "M22 8c0-2.3-.8-4.3-2-6", key: "5bb3ad" }]
|
|
99
|
+
]);
|
|
100
|
+
|
|
92
101
|
// ../../node_modules/lucide-react/dist/esm/icons/calendar-clock.js
|
|
93
102
|
var CalendarClock = createLucideIcon("CalendarClock", [
|
|
94
103
|
["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 +212,13 @@ var MonitorSmartphone = createLucideIcon("MonitorSmartphone", [
|
|
|
203
212
|
["rect", { width: "6", height: "10", x: "16", y: "12", rx: "2", key: "1egngj" }]
|
|
204
213
|
]);
|
|
205
214
|
|
|
215
|
+
// ../../node_modules/lucide-react/dist/esm/icons/music.js
|
|
216
|
+
var Music = createLucideIcon("Music", [
|
|
217
|
+
["path", { d: "M9 18V5l12-2v13", key: "1jmyc2" }],
|
|
218
|
+
["circle", { cx: "6", cy: "18", r: "3", key: "fqmcym" }],
|
|
219
|
+
["circle", { cx: "18", cy: "16", r: "3", key: "1hluhg" }]
|
|
220
|
+
]);
|
|
221
|
+
|
|
206
222
|
// ../../node_modules/lucide-react/dist/esm/icons/pause.js
|
|
207
223
|
var Pause = createLucideIcon("Pause", [
|
|
208
224
|
["rect", { x: "14", y: "4", width: "4", height: "16", rx: "1", key: "zuxfzm" }],
|
|
@@ -328,6 +344,19 @@ var Users = createLucideIcon("Users", [
|
|
|
328
344
|
["path", { d: "M16 3.13a4 4 0 0 1 0 7.75", key: "1da9ce" }]
|
|
329
345
|
]);
|
|
330
346
|
|
|
347
|
+
// ../../node_modules/lucide-react/dist/esm/icons/volume-2.js
|
|
348
|
+
var Volume2 = createLucideIcon("Volume2", [
|
|
349
|
+
[
|
|
350
|
+
"path",
|
|
351
|
+
{
|
|
352
|
+
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",
|
|
353
|
+
key: "uqj9uw"
|
|
354
|
+
}
|
|
355
|
+
],
|
|
356
|
+
["path", { d: "M16 9a5 5 0 0 1 0 6", key: "1q6k2b" }],
|
|
357
|
+
["path", { d: "M19.364 18.364a9 9 0 0 0 0-12.728", key: "ijwkga" }]
|
|
358
|
+
]);
|
|
359
|
+
|
|
331
360
|
// ../../node_modules/lucide-react/dist/esm/icons/x.js
|
|
332
361
|
var X = createLucideIcon("X", [
|
|
333
362
|
["path", { d: "M18 6 6 18", key: "1bl5f8" }],
|
|
@@ -339,6 +368,36 @@ var PACKED = "AC247 AD376 AE971 AF93 AG1 AI1 AL355 AM374 AO244 AR54 AS1 AT43 AU6
|
|
|
339
368
|
var DIAL_CODES = new Map(
|
|
340
369
|
PACKED.split(" ").map((token) => [token.slice(0, 2), Number(token.slice(2))])
|
|
341
370
|
);
|
|
371
|
+
var REGIONS_BY_CODE = (() => {
|
|
372
|
+
const map = /* @__PURE__ */ new Map();
|
|
373
|
+
for (const [region, code] of DIAL_CODES) map.set(code, [...map.get(code) ?? [], region]);
|
|
374
|
+
return map;
|
|
375
|
+
})();
|
|
376
|
+
var MAIN_REGION = {
|
|
377
|
+
1: "US",
|
|
378
|
+
7: "RU",
|
|
379
|
+
39: "IT",
|
|
380
|
+
44: "GB",
|
|
381
|
+
47: "NO",
|
|
382
|
+
61: "AU",
|
|
383
|
+
212: "MA",
|
|
384
|
+
262: "RE",
|
|
385
|
+
290: "SH",
|
|
386
|
+
358: "FI",
|
|
387
|
+
590: "GP",
|
|
388
|
+
599: "CW"
|
|
389
|
+
};
|
|
390
|
+
function regionOfInternational(typed) {
|
|
391
|
+
const text = String(typed ?? "").trim();
|
|
392
|
+
if (!text.startsWith("+")) return null;
|
|
393
|
+
const digits = text.slice(1).replace(/\D/g, "");
|
|
394
|
+
for (let length = 1; length <= Math.min(3, digits.length); length++) {
|
|
395
|
+
const code = Number(digits.slice(0, length));
|
|
396
|
+
const regions = REGIONS_BY_CODE.get(code);
|
|
397
|
+
if (regions) return MAIN_REGION[code] ?? regions[0] ?? null;
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
342
401
|
var DEFAULT_COUNTRY = "ES";
|
|
343
402
|
var REGION_NAMES = (() => {
|
|
344
403
|
try {
|
|
@@ -390,12 +449,16 @@ function CountryMark({ code }) {
|
|
|
390
449
|
}
|
|
391
450
|
function CountryPicker({
|
|
392
451
|
value,
|
|
393
|
-
onChange
|
|
452
|
+
onChange,
|
|
453
|
+
fromNumber = false
|
|
394
454
|
}) {
|
|
395
455
|
const [open, setOpen] = useState(false);
|
|
396
456
|
const [query, setQuery] = useState("");
|
|
397
457
|
const rootRef = useRef(null);
|
|
398
458
|
const inputRef = useRef(null);
|
|
459
|
+
useEffect(() => {
|
|
460
|
+
if (fromNumber) setOpen(false);
|
|
461
|
+
}, [fromNumber]);
|
|
399
462
|
const results = useMemo(() => {
|
|
400
463
|
const q = query.trim().toLowerCase().replace(/^\+/, "");
|
|
401
464
|
if (!q) return COUNTRIES;
|
|
@@ -424,6 +487,12 @@ function CountryPicker({
|
|
|
424
487
|
else setQuery("");
|
|
425
488
|
}, [open]);
|
|
426
489
|
const dial = DIAL_CODES.get(value);
|
|
490
|
+
if (fromNumber) {
|
|
491
|
+
return /* @__PURE__ */ jsx("div", { className: "cv-cp", ref: rootRef, children: /* @__PURE__ */ jsxs("span", { className: "cv-cp-chip", "data-from-number": true, title: "El prefijo viene del n\xFAmero marcado", part: "country-chip", children: [
|
|
492
|
+
/* @__PURE__ */ jsx(CountryMark, { code: value || "+" }),
|
|
493
|
+
/* @__PURE__ */ jsx("span", { className: "cv-sr-only", children: dial ? `Prefijo del n\xFAmero marcado: ${countryName(value)} (+${dial})` : "Prefijo del n\xFAmero marcado" })
|
|
494
|
+
] }) });
|
|
495
|
+
}
|
|
427
496
|
return /* @__PURE__ */ jsxs("div", { className: "cv-cp", ref: rootRef, children: [
|
|
428
497
|
/* @__PURE__ */ jsxs(
|
|
429
498
|
"button",
|
|
@@ -567,6 +636,59 @@ var KEYPAD_KEYS = [
|
|
|
567
636
|
["#", ""]
|
|
568
637
|
];
|
|
569
638
|
|
|
639
|
+
// src/ui/audio/classic-ring.ts
|
|
640
|
+
var C6 = 1046.5;
|
|
641
|
+
var E6 = 1318.51;
|
|
642
|
+
var HARMONICITY = 4;
|
|
643
|
+
var INDEX = 1.8;
|
|
644
|
+
var CLASSIC_CYCLE_SECONDS = 3;
|
|
645
|
+
var CLASSIC_GAIN = 0.3841;
|
|
646
|
+
function classicRingGraph(ctx, dest) {
|
|
647
|
+
const output = ctx.createGain();
|
|
648
|
+
output.gain.value = CLASSIC_GAIN;
|
|
649
|
+
const chain = ["highpass", "highpass", "lowpass", "lowpass"].map((type) => {
|
|
650
|
+
const f = ctx.createBiquadFilter();
|
|
651
|
+
f.type = type;
|
|
652
|
+
f.frequency.value = type === "highpass" ? 250 : 7500;
|
|
653
|
+
f.Q.value = 0.707;
|
|
654
|
+
return f;
|
|
655
|
+
});
|
|
656
|
+
for (let i = 0; i < chain.length - 1; i++) chain[i].connect(chain[i + 1]);
|
|
657
|
+
chain[chain.length - 1].connect(output);
|
|
658
|
+
output.connect(dest);
|
|
659
|
+
return { input: chain[0], output };
|
|
660
|
+
}
|
|
661
|
+
function strike(ctx, input, t, freq, vel, track) {
|
|
662
|
+
const carrier = ctx.createOscillator();
|
|
663
|
+
carrier.frequency.value = freq;
|
|
664
|
+
const modulator = ctx.createOscillator();
|
|
665
|
+
modulator.frequency.value = freq * HARMONICITY;
|
|
666
|
+
const depth = ctx.createGain();
|
|
667
|
+
depth.gain.setValueAtTime(0, t);
|
|
668
|
+
depth.gain.linearRampToValueAtTime(INDEX * freq, t + 4e-3);
|
|
669
|
+
depth.gain.setTargetAtTime(0, t + 4e-3, 0.02);
|
|
670
|
+
modulator.connect(depth);
|
|
671
|
+
depth.connect(carrier.frequency);
|
|
672
|
+
const amp = ctx.createGain();
|
|
673
|
+
amp.gain.setValueAtTime(0, t);
|
|
674
|
+
amp.gain.linearRampToValueAtTime(vel, t + 5e-3);
|
|
675
|
+
amp.gain.setTargetAtTime(0, t + 5e-3, 0.15);
|
|
676
|
+
amp.gain.setTargetAtTime(0, t + 0.045, 0.08);
|
|
677
|
+
carrier.connect(amp);
|
|
678
|
+
amp.connect(input);
|
|
679
|
+
for (const o of [carrier, modulator]) {
|
|
680
|
+
o.start(t);
|
|
681
|
+
o.stop(t + 0.7);
|
|
682
|
+
track(o);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function scheduleClassicCycle(ctx, input, t0, track) {
|
|
686
|
+
for (const ring of [0, 0.6]) {
|
|
687
|
+
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);
|
|
688
|
+
}
|
|
689
|
+
return CLASSIC_CYCLE_SECONDS;
|
|
690
|
+
}
|
|
691
|
+
|
|
570
692
|
// src/ui/audio/audio.ts
|
|
571
693
|
var TONE_HZ = 425;
|
|
572
694
|
function createRemoteAudio() {
|
|
@@ -609,30 +731,159 @@ function primeAudio() {
|
|
|
609
731
|
if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
|
|
610
732
|
});
|
|
611
733
|
}
|
|
612
|
-
function playRingtone() {
|
|
734
|
+
function playRingtone(options = {}) {
|
|
613
735
|
const ctx = sharedContext();
|
|
614
736
|
if (!ctx) return { stop() {
|
|
615
737
|
} };
|
|
738
|
+
const bus = ctx.createGain();
|
|
739
|
+
const route = routeToDevice(ctx, bus, options.sinkId ?? null);
|
|
740
|
+
const voice = options.buffer ? fileVoice(ctx, bus, options.buffer, options.loop ?? null) : synthesisedVoice(ctx, bus);
|
|
741
|
+
return {
|
|
742
|
+
stop() {
|
|
743
|
+
voice.stop();
|
|
744
|
+
route.stop();
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
function fileVoice(ctx, bus, buffer, loop) {
|
|
749
|
+
const FILE_LEVEL = 1;
|
|
750
|
+
const RAMP = 0.02;
|
|
751
|
+
const LEAD = 0.08;
|
|
616
752
|
const gain = ctx.createGain();
|
|
617
753
|
gain.gain.value = 0;
|
|
618
|
-
gain.connect(
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
754
|
+
gain.connect(bus);
|
|
755
|
+
const src = ctx.createBufferSource();
|
|
756
|
+
src.buffer = buffer;
|
|
757
|
+
src.loop = true;
|
|
758
|
+
const points = loop && loop.start >= 0 && loop.end > loop.start && loop.end <= buffer.duration + 1e-3 ? loop : null;
|
|
759
|
+
if (points) {
|
|
760
|
+
src.loopStart = points.start;
|
|
761
|
+
src.loopEnd = points.end;
|
|
762
|
+
}
|
|
763
|
+
src.connect(gain);
|
|
764
|
+
let stopped = false;
|
|
765
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
766
|
+
}).finally(() => {
|
|
767
|
+
if (stopped) return;
|
|
768
|
+
const at = ctx.currentTime + LEAD;
|
|
627
769
|
gain.gain.setValueAtTime(0, at);
|
|
628
|
-
gain.gain.linearRampToValueAtTime(
|
|
629
|
-
|
|
630
|
-
|
|
770
|
+
gain.gain.linearRampToValueAtTime(FILE_LEVEL, at + RAMP);
|
|
771
|
+
src.start(at, points?.start ?? 0);
|
|
772
|
+
});
|
|
773
|
+
return {
|
|
774
|
+
stop() {
|
|
775
|
+
stopped = true;
|
|
776
|
+
try {
|
|
777
|
+
src.stop();
|
|
778
|
+
} catch {
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
gain.disconnect();
|
|
782
|
+
} catch {
|
|
783
|
+
}
|
|
784
|
+
}
|
|
631
785
|
};
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
786
|
+
}
|
|
787
|
+
function routeToDevice(ctx, bus, sinkId) {
|
|
788
|
+
const toDefault = () => {
|
|
789
|
+
bus.connect(ctx.destination);
|
|
790
|
+
};
|
|
791
|
+
const el = sinkId && typeof document !== "undefined" ? document.createElement("audio") : null;
|
|
792
|
+
const dest = el && typeof ctx.createMediaStreamDestination === "function" ? ctx.createMediaStreamDestination() : null;
|
|
793
|
+
const canRoute = el && dest && typeof el.setSinkId === "function";
|
|
794
|
+
if (!canRoute || !sinkId) {
|
|
795
|
+
toDefault();
|
|
796
|
+
return { stop: () => safeDisconnect(bus) };
|
|
797
|
+
}
|
|
798
|
+
let stopped = false;
|
|
799
|
+
el.style.display = "none";
|
|
800
|
+
el.srcObject = dest.stream;
|
|
801
|
+
document.body.appendChild(el);
|
|
802
|
+
bus.connect(dest);
|
|
803
|
+
void el.setSinkId(sinkId).then(() => el.play()).catch((error) => {
|
|
804
|
+
if (stopped) return;
|
|
805
|
+
console.warn("[softphone] sound could not be sent to the chosen device; using the default output", error);
|
|
806
|
+
safeDisconnect(bus);
|
|
807
|
+
teardown(el);
|
|
808
|
+
toDefault();
|
|
809
|
+
});
|
|
810
|
+
return {
|
|
811
|
+
stop() {
|
|
812
|
+
stopped = true;
|
|
813
|
+
safeDisconnect(bus);
|
|
814
|
+
teardown(el);
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
function safeDisconnect(node) {
|
|
819
|
+
try {
|
|
820
|
+
node.disconnect();
|
|
821
|
+
} catch {
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function teardown(el) {
|
|
825
|
+
el.pause();
|
|
826
|
+
el.srcObject = null;
|
|
827
|
+
el.remove();
|
|
828
|
+
}
|
|
829
|
+
var TEST_TONE_MS = 900;
|
|
830
|
+
function playTestTone(sinkId) {
|
|
831
|
+
const ctx = sharedContext();
|
|
832
|
+
if (!ctx) return { stop() {
|
|
833
|
+
} };
|
|
834
|
+
const bus = ctx.createGain();
|
|
835
|
+
const route = routeToDevice(ctx, bus, sinkId);
|
|
836
|
+
const LEVEL = 0.5;
|
|
837
|
+
const NOTES = [
|
|
838
|
+
{ hz: 660, at: 0.05 },
|
|
839
|
+
{ hz: 880, at: 0.4 }
|
|
840
|
+
];
|
|
841
|
+
const oscs = [];
|
|
842
|
+
let stopped = false;
|
|
843
|
+
let timer = null;
|
|
844
|
+
const stop = () => {
|
|
845
|
+
if (stopped) return;
|
|
846
|
+
stopped = true;
|
|
847
|
+
if (timer) clearTimeout(timer);
|
|
848
|
+
for (const o of oscs) {
|
|
849
|
+
try {
|
|
850
|
+
o.stop();
|
|
851
|
+
} catch {
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
route.stop();
|
|
855
|
+
};
|
|
856
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
857
|
+
}).finally(() => {
|
|
858
|
+
if (stopped) return;
|
|
859
|
+
const t0 = ctx.currentTime;
|
|
860
|
+
for (const { hz, at } of NOTES) {
|
|
861
|
+
const gain = ctx.createGain();
|
|
862
|
+
gain.gain.value = 0;
|
|
863
|
+
gain.connect(bus);
|
|
864
|
+
const osc = ctx.createOscillator();
|
|
865
|
+
osc.type = "sine";
|
|
866
|
+
osc.frequency.value = hz;
|
|
867
|
+
osc.connect(gain);
|
|
868
|
+
const start = t0 + at;
|
|
869
|
+
gain.gain.setValueAtTime(0, start);
|
|
870
|
+
gain.gain.linearRampToValueAtTime(LEVEL, start + 0.015);
|
|
871
|
+
gain.gain.setValueAtTime(LEVEL, start + 0.25);
|
|
872
|
+
gain.gain.linearRampToValueAtTime(0, start + 0.32);
|
|
873
|
+
osc.start(start);
|
|
874
|
+
osc.stop(start + 0.34);
|
|
875
|
+
oscs.push(osc);
|
|
876
|
+
}
|
|
877
|
+
timer = setTimeout(stop, TEST_TONE_MS);
|
|
878
|
+
});
|
|
879
|
+
return { stop };
|
|
880
|
+
}
|
|
881
|
+
function synthesisedVoice(ctx, bus) {
|
|
882
|
+
const { input, output } = classicRingGraph(ctx, bus);
|
|
883
|
+
const scheduled = /* @__PURE__ */ new Set();
|
|
884
|
+
const track = (o) => {
|
|
885
|
+
scheduled.add(o);
|
|
886
|
+
o.onended = () => scheduled.delete(o);
|
|
636
887
|
};
|
|
637
888
|
const LOOKAHEAD = 0.5;
|
|
638
889
|
const TICK_MS = 150;
|
|
@@ -641,7 +892,7 @@ function playRingtone() {
|
|
|
641
892
|
let timer = null;
|
|
642
893
|
let nextAt = 0;
|
|
643
894
|
const pump = () => {
|
|
644
|
-
while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt +=
|
|
895
|
+
while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleClassicCycle(ctx, input, nextAt, track);
|
|
645
896
|
};
|
|
646
897
|
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
647
898
|
}).finally(() => {
|
|
@@ -654,12 +905,15 @@ function playRingtone() {
|
|
|
654
905
|
stop() {
|
|
655
906
|
stopped = true;
|
|
656
907
|
if (timer) clearInterval(timer);
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
908
|
+
for (const o of scheduled) {
|
|
909
|
+
try {
|
|
910
|
+
o.stop();
|
|
911
|
+
} catch {
|
|
912
|
+
}
|
|
660
913
|
}
|
|
914
|
+
scheduled.clear();
|
|
661
915
|
try {
|
|
662
|
-
|
|
916
|
+
output.disconnect();
|
|
663
917
|
} catch {
|
|
664
918
|
}
|
|
665
919
|
}
|
|
@@ -805,18 +1059,378 @@ function playRingback() {
|
|
|
805
1059
|
}
|
|
806
1060
|
};
|
|
807
1061
|
}
|
|
1062
|
+
var stores = /* @__PURE__ */ new WeakMap();
|
|
1063
|
+
function outboundNumberStore(softphone) {
|
|
1064
|
+
const source = softphone.sources?.outboundNumbers;
|
|
1065
|
+
if (!source) return null;
|
|
1066
|
+
let store = stores.get(softphone);
|
|
1067
|
+
if (!store) {
|
|
1068
|
+
store = createOutboundNumberStore(source);
|
|
1069
|
+
stores.set(softphone, store);
|
|
1070
|
+
}
|
|
1071
|
+
return store;
|
|
1072
|
+
}
|
|
1073
|
+
function createOutboundNumberStore(source) {
|
|
1074
|
+
let settings = null;
|
|
1075
|
+
let inFlight = null;
|
|
1076
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1077
|
+
const apply = (next) => {
|
|
1078
|
+
settings = next;
|
|
1079
|
+
for (const l of listeners) l();
|
|
1080
|
+
};
|
|
1081
|
+
return {
|
|
1082
|
+
getSettings: () => settings,
|
|
1083
|
+
refresh() {
|
|
1084
|
+
inFlight ??= (async () => {
|
|
1085
|
+
if (!settings && source.cached) {
|
|
1086
|
+
const early = await source.cached().catch(() => null);
|
|
1087
|
+
if (early && !settings) apply(early);
|
|
1088
|
+
}
|
|
1089
|
+
try {
|
|
1090
|
+
apply(await source.get());
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
console.warn("[softphone] outbound numbers could not be loaded; keeping the previous ones", error);
|
|
1093
|
+
} finally {
|
|
1094
|
+
inFlight = null;
|
|
1095
|
+
}
|
|
1096
|
+
})();
|
|
1097
|
+
return inFlight;
|
|
1098
|
+
},
|
|
1099
|
+
async setMine(id) {
|
|
1100
|
+
apply(await source.setMine(id));
|
|
1101
|
+
},
|
|
1102
|
+
subscribe(listener) {
|
|
1103
|
+
listeners.add(listener);
|
|
1104
|
+
return () => listeners.delete(listener);
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
var noSubscribe = () => () => {
|
|
1109
|
+
};
|
|
1110
|
+
var noSettings = () => null;
|
|
1111
|
+
function useOutboundNumbers(store) {
|
|
1112
|
+
const settings = useSyncExternalStore(store?.subscribe ?? noSubscribe, store?.getSettings ?? noSettings);
|
|
1113
|
+
const [failed, setFailed] = useState(false);
|
|
1114
|
+
const load = useCallback(() => {
|
|
1115
|
+
if (!store) return () => {
|
|
1116
|
+
};
|
|
1117
|
+
let live = true;
|
|
1118
|
+
setFailed(false);
|
|
1119
|
+
void store.refresh().then(() => {
|
|
1120
|
+
if (live) setFailed(store.getSettings() === null);
|
|
1121
|
+
});
|
|
1122
|
+
return () => {
|
|
1123
|
+
live = false;
|
|
1124
|
+
};
|
|
1125
|
+
}, [store]);
|
|
1126
|
+
useEffect(load, [load]);
|
|
1127
|
+
return { settings, failed, retry: load };
|
|
1128
|
+
}
|
|
1129
|
+
function useKnownOutboundNumbers(store) {
|
|
1130
|
+
return useSyncExternalStore(store?.subscribe ?? noSubscribe, store?.getSettings ?? noSettings);
|
|
1131
|
+
}
|
|
1132
|
+
function useCountdown(until) {
|
|
1133
|
+
const [, setTick] = useState(0);
|
|
1134
|
+
useEffect(() => {
|
|
1135
|
+
if (!until) return void 0;
|
|
1136
|
+
const timer = setInterval(() => {
|
|
1137
|
+
setTick((n) => n + 1);
|
|
1138
|
+
if (secondsLeft(until) === 0) clearInterval(timer);
|
|
1139
|
+
}, 1e3);
|
|
1140
|
+
return () => clearInterval(timer);
|
|
1141
|
+
}, [until]);
|
|
1142
|
+
return secondsLeft(until);
|
|
1143
|
+
}
|
|
1144
|
+
function secondsLeft(until) {
|
|
1145
|
+
return until ? Math.max(0, Math.ceil((until - Date.now()) / 1e3)) : 0;
|
|
1146
|
+
}
|
|
1147
|
+
function numberName(n, format = formatIntl) {
|
|
1148
|
+
if (!n) return "N\xFAmero de la organizaci\xF3n";
|
|
1149
|
+
return n.label || format(`+${n.number}`);
|
|
1150
|
+
}
|
|
1151
|
+
function effectiveNumber(settings) {
|
|
1152
|
+
return settings?.numbers.find((n) => n.id === settings.effective);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/ui/outbound/outbound-change.ts
|
|
1156
|
+
var KIND_TAG = {
|
|
1157
|
+
mobile: { tag: "M\xF3vil", hint: "No usar para atenci\xF3n al cliente ni para llamadas comerciales no solicitadas." },
|
|
1158
|
+
commercial: { tag: "Comercial (400)", hint: "Solo para llamadas comerciales. No recibe llamadas." }
|
|
1159
|
+
};
|
|
1160
|
+
function noticeText(situation, seconds, current) {
|
|
1161
|
+
switch (situation) {
|
|
1162
|
+
case "platform":
|
|
1163
|
+
return `El cambio de n\xFAmero de salida no est\xE1 disponible temporalmente. Vuelve a intentarlo en ${seconds} s. Hasta entonces, tus llamadas seguir\xE1n saliendo desde ${current}.`;
|
|
1164
|
+
case "user":
|
|
1165
|
+
return `Acabas de cambiar el n\xFAmero de salida. Podr\xE1s cambiarlo de nuevo en ${seconds} s. Hasta entonces, tus llamadas saldr\xE1n desde ${current}.`;
|
|
1166
|
+
case "wait":
|
|
1167
|
+
return `Podr\xE1s cambiar el n\xFAmero de salida en ${seconds} s.`;
|
|
1168
|
+
case "pending":
|
|
1169
|
+
return seconds > 0 ? `Tu n\xFAmero de salida todav\xEDa no se ha aplicado. Podr\xE1s volver a elegirlo en ${seconds} s.` : "Tu n\xFAmero de salida todav\xEDa no se ha aplicado. Vuelve a elegirlo para aplicarlo.";
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
function useOutboundNumberChange(store, settings, current) {
|
|
1173
|
+
const [busy, setBusy] = useState(false);
|
|
1174
|
+
const [notice, setNotice] = useState(null);
|
|
1175
|
+
const [waitUntil, setWaitUntil] = useState(null);
|
|
1176
|
+
useEffect(() => {
|
|
1177
|
+
const s = settings?.changeAvailableInSeconds ?? 0;
|
|
1178
|
+
setWaitUntil(s > 0 ? Date.now() + s * 1e3 : null);
|
|
1179
|
+
}, [settings]);
|
|
1180
|
+
const refusedUntil = notice?.kind === "refused" ? notice.until : null;
|
|
1181
|
+
const blockedFor = useCountdown(refusedUntil ?? waitUntil);
|
|
1182
|
+
useEffect(() => {
|
|
1183
|
+
if (notice?.kind === "refused" && blockedFor === 0) setNotice(null);
|
|
1184
|
+
}, [notice, blockedFor]);
|
|
1185
|
+
const locked = busy || blockedFor > 0;
|
|
1186
|
+
const choose = async (id) => {
|
|
1187
|
+
if (!store || !settings || locked) return false;
|
|
1188
|
+
if (id === settings.mine) return true;
|
|
1189
|
+
setBusy(true);
|
|
1190
|
+
setNotice(null);
|
|
1191
|
+
try {
|
|
1192
|
+
await store.setMine(id);
|
|
1193
|
+
return true;
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
setNotice(
|
|
1196
|
+
isOutboundNumberRefused(error) ? { kind: "refused", reason: error.refusal.reason, until: Date.now() + error.refusal.retryAfterSeconds * 1e3 } : { kind: "error", text: errorText(error) }
|
|
1197
|
+
);
|
|
1198
|
+
return false;
|
|
1199
|
+
} finally {
|
|
1200
|
+
setBusy(false);
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
return {
|
|
1204
|
+
busy,
|
|
1205
|
+
locked,
|
|
1206
|
+
choose,
|
|
1207
|
+
/** What to say about the choice right now, if anything. */
|
|
1208
|
+
message: settings ? currentMessage(notice, settings.state, blockedFor, current) : null,
|
|
1209
|
+
/** The message is an error (the change failed), not information (wait, pending). */
|
|
1210
|
+
isError: notice?.kind === "error"
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
function currentMessage(notice, state, blockedFor, current) {
|
|
1214
|
+
if (notice?.kind === "refused") return blockedFor > 0 ? noticeText(notice.reason, blockedFor, current) : null;
|
|
1215
|
+
if (notice?.kind === "error") return notice.text;
|
|
1216
|
+
if (state === "pending" || state === "failed") return noticeText("pending", blockedFor, current);
|
|
1217
|
+
return blockedFor > 0 ? noticeText("wait", blockedFor, current) : null;
|
|
1218
|
+
}
|
|
1219
|
+
function errorText(error) {
|
|
1220
|
+
const message = error instanceof Error ? error.message : "";
|
|
1221
|
+
return message && !/HTTP \d+/.test(message) ? message : "No se ha podido cambiar el n\xFAmero de salida. Int\xE9ntalo de nuevo.";
|
|
1222
|
+
}
|
|
1223
|
+
var CALLOUT_MS = 6e3;
|
|
1224
|
+
function OutboundNumberPicker({ onBusyChange }) {
|
|
1225
|
+
const softphone = useSoftphone();
|
|
1226
|
+
const store = outboundNumberStore(softphone);
|
|
1227
|
+
const { settings } = useOutboundNumbers(store);
|
|
1228
|
+
const formatIntl2 = useIntlFormatter();
|
|
1229
|
+
const current = settings ? numberName(effectiveNumber(settings), formatIntl2) : "";
|
|
1230
|
+
const { busy, locked, choose: apply, message, isError } = useOutboundNumberChange(store, settings, current);
|
|
1231
|
+
const [open, setOpen] = useState(false);
|
|
1232
|
+
const [applying, setApplying] = useState(void 0);
|
|
1233
|
+
const [callout, setCallout] = useState(false);
|
|
1234
|
+
const rootRef = useRef(null);
|
|
1235
|
+
const chipRef = useRef(null);
|
|
1236
|
+
const keys = useListKeyboard(open);
|
|
1237
|
+
useEffect(() => onBusyChange?.(busy), [busy, onBusyChange]);
|
|
1238
|
+
useEffect(() => {
|
|
1239
|
+
if (!message) setCallout(false);
|
|
1240
|
+
}, [message]);
|
|
1241
|
+
useEffect(() => {
|
|
1242
|
+
if (!callout) return void 0;
|
|
1243
|
+
const timer = setTimeout(() => setCallout(false), CALLOUT_MS);
|
|
1244
|
+
return () => clearTimeout(timer);
|
|
1245
|
+
}, [callout]);
|
|
1246
|
+
const floating = open || callout;
|
|
1247
|
+
useEffect(() => {
|
|
1248
|
+
if (!floating) return;
|
|
1249
|
+
const close = () => {
|
|
1250
|
+
setOpen(false);
|
|
1251
|
+
setCallout(false);
|
|
1252
|
+
};
|
|
1253
|
+
const root = rootRef.current?.getRootNode() ?? document;
|
|
1254
|
+
const onDown = (e) => {
|
|
1255
|
+
if (!rootRef.current?.contains(e.target)) close();
|
|
1256
|
+
};
|
|
1257
|
+
const onKey = (e) => {
|
|
1258
|
+
if (e.key !== "Escape") return;
|
|
1259
|
+
if (rootRef.current?.contains(e.target)) chipRef.current?.focus();
|
|
1260
|
+
close();
|
|
1261
|
+
};
|
|
1262
|
+
root.addEventListener("pointerdown", onDown);
|
|
1263
|
+
document.addEventListener("keydown", onKey);
|
|
1264
|
+
return () => {
|
|
1265
|
+
root.removeEventListener("pointerdown", onDown);
|
|
1266
|
+
document.removeEventListener("keydown", onKey);
|
|
1267
|
+
};
|
|
1268
|
+
}, [floating]);
|
|
1269
|
+
if (!store || !settings || settings.numbers.length < 2) return null;
|
|
1270
|
+
const orgDefault = settings.numbers.find((n) => n.id === settings.orgDefault);
|
|
1271
|
+
const applyingNumber = applying === null ? orgDefault : settings.numbers.find((n) => n.id === applying);
|
|
1272
|
+
const shown = applying === void 0 ? current : numberName(applyingNumber, formatIntl2);
|
|
1273
|
+
const choose = async (id) => {
|
|
1274
|
+
if (locked) return;
|
|
1275
|
+
setOpen(false);
|
|
1276
|
+
chipRef.current?.focus();
|
|
1277
|
+
if (id === settings.mine) return;
|
|
1278
|
+
setCallout(false);
|
|
1279
|
+
setApplying(id);
|
|
1280
|
+
const applied = await apply(id);
|
|
1281
|
+
setApplying(void 0);
|
|
1282
|
+
if (!applied) setCallout(true);
|
|
1283
|
+
};
|
|
1284
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-on", ref: rootRef, children: [
|
|
1285
|
+
/* @__PURE__ */ jsxs(
|
|
1286
|
+
"button",
|
|
1287
|
+
{
|
|
1288
|
+
ref: chipRef,
|
|
1289
|
+
type: "button",
|
|
1290
|
+
className: "cv-on-chip",
|
|
1291
|
+
onClick: () => {
|
|
1292
|
+
setCallout(false);
|
|
1293
|
+
setOpen((o) => !o);
|
|
1294
|
+
},
|
|
1295
|
+
"aria-haspopup": "listbox",
|
|
1296
|
+
"aria-expanded": open,
|
|
1297
|
+
"aria-label": `Llamar desde: ${shown}${busy ? " (aplicando)" : ""}. Cambiar n\xFAmero de salida`,
|
|
1298
|
+
"aria-busy": busy,
|
|
1299
|
+
part: "outbound-number-chip",
|
|
1300
|
+
children: [
|
|
1301
|
+
/* @__PURE__ */ jsx("span", { className: "cv-on-label", children: "Llamar desde:" }),
|
|
1302
|
+
busy ? /* @__PURE__ */ jsx(LoaderCircle, { size: 13, className: "cv-on-icon cv-on-spin", "aria-hidden": true }) : null,
|
|
1303
|
+
/* @__PURE__ */ jsx("span", { className: "cv-on-current", children: shown }),
|
|
1304
|
+
/* @__PURE__ */ jsx(ChevronDown, { size: 13, className: "cv-cp-caret", "data-open": open, "aria-hidden": true })
|
|
1305
|
+
]
|
|
1306
|
+
}
|
|
1307
|
+
),
|
|
1308
|
+
open ? /* @__PURE__ */ jsxs(
|
|
1309
|
+
"div",
|
|
1310
|
+
{
|
|
1311
|
+
className: "cv-cp-pop cv-on-pop",
|
|
1312
|
+
role: "dialog",
|
|
1313
|
+
"aria-label": "Llamar desde",
|
|
1314
|
+
part: "outbound-number-list",
|
|
1315
|
+
onBlur: (e) => {
|
|
1316
|
+
if (!e.currentTarget.contains(e.relatedTarget)) setOpen(false);
|
|
1317
|
+
},
|
|
1318
|
+
children: [
|
|
1319
|
+
/* @__PURE__ */ jsx("p", { className: "cv-on-title", children: "Llamar desde" }),
|
|
1320
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-cp-list", role: "listbox", "aria-disabled": locked, ref: keys.listRef, onKeyDown: keys.onKeyDown, children: [
|
|
1321
|
+
/* @__PURE__ */ jsx(
|
|
1322
|
+
OptionRow,
|
|
1323
|
+
{
|
|
1324
|
+
selected: settings.mine === null,
|
|
1325
|
+
disabled: locked,
|
|
1326
|
+
title: "Por defecto de la organizaci\xF3n",
|
|
1327
|
+
sub: orgDefault ? numberName(orgDefault, formatIntl2) : "Sin n\xFAmero por defecto",
|
|
1328
|
+
onSelect: () => void choose(null)
|
|
1329
|
+
}
|
|
1330
|
+
),
|
|
1331
|
+
settings.numbers.map((n) => /* @__PURE__ */ jsx(
|
|
1332
|
+
OptionRow,
|
|
1333
|
+
{
|
|
1334
|
+
selected: settings.mine === n.id,
|
|
1335
|
+
disabled: locked,
|
|
1336
|
+
title: numberName(n, formatIntl2),
|
|
1337
|
+
sub: n.label ? formatIntl2(`+${n.number}`) : null,
|
|
1338
|
+
tag: KIND_TAG[n.kind],
|
|
1339
|
+
onSelect: () => void choose(n.id)
|
|
1340
|
+
},
|
|
1341
|
+
n.id
|
|
1342
|
+
))
|
|
1343
|
+
] }),
|
|
1344
|
+
message ? /* @__PURE__ */ jsx("p", { className: "cv-on-notice", role: "status", "data-kind": isError ? "error" : "info", children: message }) : null
|
|
1345
|
+
]
|
|
1346
|
+
}
|
|
1347
|
+
) : null,
|
|
1348
|
+
callout && !open && message ? /* @__PURE__ */ jsx("p", { className: "cv-on-callout", role: "status", "data-kind": isError ? "error" : "info", part: "outbound-number-notice", children: message }) : null
|
|
1349
|
+
] });
|
|
1350
|
+
}
|
|
1351
|
+
function OptionRow({
|
|
1352
|
+
selected,
|
|
1353
|
+
disabled,
|
|
1354
|
+
title,
|
|
1355
|
+
sub,
|
|
1356
|
+
tag,
|
|
1357
|
+
onSelect
|
|
1358
|
+
}) {
|
|
1359
|
+
return /* @__PURE__ */ jsxs(
|
|
1360
|
+
"button",
|
|
1361
|
+
{
|
|
1362
|
+
type: "button",
|
|
1363
|
+
role: "option",
|
|
1364
|
+
"aria-selected": selected,
|
|
1365
|
+
"aria-disabled": disabled,
|
|
1366
|
+
className: "cv-cp-item cv-on-item",
|
|
1367
|
+
tabIndex: -1,
|
|
1368
|
+
onClick: onSelect,
|
|
1369
|
+
children: [
|
|
1370
|
+
/* @__PURE__ */ jsxs("span", { className: "cv-on-text", children: [
|
|
1371
|
+
/* @__PURE__ */ jsxs("span", { className: "cv-on-line", children: [
|
|
1372
|
+
/* @__PURE__ */ jsx("span", { className: "cv-cp-name", children: title }),
|
|
1373
|
+
tag ? /* @__PURE__ */ jsx("span", { className: "cv-on-tag", children: tag.tag }) : null
|
|
1374
|
+
] }),
|
|
1375
|
+
sub ? /* @__PURE__ */ jsx("span", { className: "cv-on-sub", children: sub }) : null,
|
|
1376
|
+
tag ? /* @__PURE__ */ jsx("span", { className: "cv-on-hint", children: tag.hint }) : null
|
|
1377
|
+
] }),
|
|
1378
|
+
/* @__PURE__ */ jsx(Check, { size: 14, className: "cv-cp-check", "data-on": selected, "aria-hidden": true })
|
|
1379
|
+
]
|
|
1380
|
+
}
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
var optionsOf = (list) => list ? [...list.querySelectorAll('[role="option"]')] : [];
|
|
1384
|
+
var fold = (text) => text.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
|
|
1385
|
+
var TYPEAHEAD_MS = 700;
|
|
1386
|
+
function useListKeyboard(open) {
|
|
1387
|
+
const listRef = useRef(null);
|
|
1388
|
+
const typed = useRef({ text: "", at: 0 });
|
|
1389
|
+
useEffect(() => {
|
|
1390
|
+
if (!open) return;
|
|
1391
|
+
const options = optionsOf(listRef.current);
|
|
1392
|
+
(options.find((o) => o.getAttribute("aria-selected") === "true") ?? options[0])?.focus();
|
|
1393
|
+
}, [open]);
|
|
1394
|
+
const onKeyDown = (e) => {
|
|
1395
|
+
const options = optionsOf(listRef.current);
|
|
1396
|
+
const at = options.indexOf(e.target);
|
|
1397
|
+
const move = {
|
|
1398
|
+
ArrowDown: Math.min(at + 1, options.length - 1),
|
|
1399
|
+
ArrowUp: Math.max(at - 1, 0),
|
|
1400
|
+
Home: 0,
|
|
1401
|
+
End: options.length - 1
|
|
1402
|
+
};
|
|
1403
|
+
let next = move[e.key];
|
|
1404
|
+
if (next === void 0 && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
1405
|
+
const now = Date.now();
|
|
1406
|
+
const text = now - typed.current.at < TYPEAHEAD_MS ? typed.current.text + e.key : e.key;
|
|
1407
|
+
typed.current = { text, at: now };
|
|
1408
|
+
const wanted = fold(text);
|
|
1409
|
+
const match = options.findIndex((o) => fold(o.querySelector(".cv-cp-name")?.textContent ?? "").startsWith(wanted));
|
|
1410
|
+
next = match >= 0 ? match : void 0;
|
|
1411
|
+
}
|
|
1412
|
+
if (next === void 0) return;
|
|
1413
|
+
e.preventDefault();
|
|
1414
|
+
options[next]?.focus();
|
|
1415
|
+
};
|
|
1416
|
+
return { listRef, onKeyDown };
|
|
1417
|
+
}
|
|
808
1418
|
var NOT_DIALABLE = /* @__PURE__ */ new Set(["*", "#"]);
|
|
1419
|
+
var DISPLAY_SIZES = ["1.375rem", "1.25rem", "1.125rem", "1rem"];
|
|
809
1420
|
function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
|
|
810
1421
|
const softphone = useSoftphone();
|
|
811
1422
|
const state = useSoftphoneState();
|
|
812
1423
|
const [value, setValue] = useState("");
|
|
813
1424
|
const [country, setCountry] = useState(DEFAULT_COUNTRY);
|
|
1425
|
+
const [changingNumber, setChangingNumber] = useState(false);
|
|
814
1426
|
const format = useNumberFormatter();
|
|
815
1427
|
const inputRef = useRef(null);
|
|
816
1428
|
const caretRef = useRef(null);
|
|
817
1429
|
const display = format(country, value);
|
|
818
1430
|
const target = toE164(country, value);
|
|
819
|
-
const canCall = state === "online" && target.length > 1;
|
|
1431
|
+
const canCall = state === "online" && target.length > 1 && !changingNumber;
|
|
1432
|
+
const typedInternational = value.trim().startsWith("+");
|
|
1433
|
+
const numberRegion = regionOfInternational(value);
|
|
820
1434
|
useLayoutEffect(() => {
|
|
821
1435
|
const caret = caretRef.current;
|
|
822
1436
|
if (caret === null || !inputRef.current) return;
|
|
@@ -824,6 +1438,14 @@ function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
|
|
|
824
1438
|
const pos = caretAfterDigits(display, caret);
|
|
825
1439
|
inputRef.current.setSelectionRange(pos, pos);
|
|
826
1440
|
}, [display]);
|
|
1441
|
+
useLayoutEffect(() => {
|
|
1442
|
+
const input = inputRef.current;
|
|
1443
|
+
if (!input) return;
|
|
1444
|
+
for (const size of DISPLAY_SIZES) {
|
|
1445
|
+
input.style.fontSize = size;
|
|
1446
|
+
if (input.scrollWidth <= input.clientWidth) break;
|
|
1447
|
+
}
|
|
1448
|
+
}, [display]);
|
|
827
1449
|
const append = (key) => {
|
|
828
1450
|
onIntent?.();
|
|
829
1451
|
setValue((v) => v + key);
|
|
@@ -839,9 +1461,16 @@ function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
|
|
|
839
1461
|
void softphone.call(target);
|
|
840
1462
|
};
|
|
841
1463
|
return /* @__PURE__ */ jsxs("div", { className: "cv-view", children: [
|
|
842
|
-
/* @__PURE__ */ jsx(
|
|
1464
|
+
/* @__PURE__ */ jsx(OutboundNumberPicker, { onBusyChange: setChangingNumber }),
|
|
843
1465
|
/* @__PURE__ */ jsxs("div", { className: "cv-dial-head", children: [
|
|
844
|
-
/* @__PURE__ */ jsx(
|
|
1466
|
+
/* @__PURE__ */ jsx(
|
|
1467
|
+
CountryPicker,
|
|
1468
|
+
{
|
|
1469
|
+
value: numberRegion ?? country,
|
|
1470
|
+
onChange: setCountry,
|
|
1471
|
+
fromNumber: typedInternational
|
|
1472
|
+
}
|
|
1473
|
+
),
|
|
845
1474
|
/* @__PURE__ */ jsx(
|
|
846
1475
|
"input",
|
|
847
1476
|
{
|
|
@@ -865,6 +1494,8 @@ function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
|
|
|
865
1494
|
type: "button",
|
|
866
1495
|
className: "cv-dial-side",
|
|
867
1496
|
"data-hidden": value.length === 0,
|
|
1497
|
+
tabIndex: value.length === 0 ? -1 : void 0,
|
|
1498
|
+
"aria-hidden": value.length === 0 || void 0,
|
|
868
1499
|
onClick: backspace,
|
|
869
1500
|
"aria-label": "Borrar",
|
|
870
1501
|
children: /* @__PURE__ */ jsx(Delete, { size: 19 })
|
|
@@ -1289,6 +1920,9 @@ function CallScreen({
|
|
|
1289
1920
|
}) {
|
|
1290
1921
|
const view = useCall(call);
|
|
1291
1922
|
const softphone = useSoftphone();
|
|
1923
|
+
const outbound = useOutboundNumbers(call.direction === "outbound" ? outboundNumberStore(softphone) : null).settings;
|
|
1924
|
+
const formatIntl2 = useIntlFormatter();
|
|
1925
|
+
const fromName = outbound && outbound.numbers.length > 1 ? numberName(effectiveNumber(outbound), formatIntl2) : null;
|
|
1292
1926
|
const [panel, setPanel] = useState(null);
|
|
1293
1927
|
const showTransfer = panel === "transfer";
|
|
1294
1928
|
const showKeypad = panel === "keypad";
|
|
@@ -1499,6 +2133,13 @@ function CallScreen({
|
|
|
1499
2133
|
] }),
|
|
1500
2134
|
/* @__PURE__ */ jsx("div", { className: "cv-call-name", children: name }),
|
|
1501
2135
|
showNumber ? /* @__PURE__ */ jsx("div", { className: "cv-call-number", children: number }) : null,
|
|
2136
|
+
fromName ? /* @__PURE__ */ jsxs("div", { className: "cv-call-from", children: [
|
|
2137
|
+
/* @__PURE__ */ jsx(PhoneOutgoing, { size: 11, "aria-hidden": true }),
|
|
2138
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
2139
|
+
"Desde ",
|
|
2140
|
+
fromName
|
|
2141
|
+
] })
|
|
2142
|
+
] }) : null,
|
|
1502
2143
|
s === "active" ? /* @__PURE__ */ jsxs("div", { className: "cv-timer", "aria-label": "Duraci\xF3n de la llamada", children: [
|
|
1503
2144
|
/* @__PURE__ */ jsx("b", { children: mm }),
|
|
1504
2145
|
/* @__PURE__ */ jsx("i", { children: ":" }),
|
|
@@ -1914,12 +2555,23 @@ function HandlerChain({ handlers }) {
|
|
|
1914
2555
|
] }, `${h.extension}-${i}`);
|
|
1915
2556
|
}) });
|
|
1916
2557
|
}
|
|
2558
|
+
function outboundFrom(entry, known, formatIntl2) {
|
|
2559
|
+
if (entry.direction !== "outbound" || !entry.from) return null;
|
|
2560
|
+
const number = formatIntl2(`+${entry.from}`);
|
|
2561
|
+
const label = known?.numbers.find((n) => n.number === entry.from)?.label;
|
|
2562
|
+
if (!label) return number;
|
|
2563
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2564
|
+
label,
|
|
2565
|
+
/* @__PURE__ */ jsx("span", { className: "cv-detail-row-sub", children: number })
|
|
2566
|
+
] });
|
|
2567
|
+
}
|
|
1917
2568
|
function HistoryDetail({
|
|
1918
2569
|
entry,
|
|
1919
2570
|
onBack
|
|
1920
2571
|
}) {
|
|
1921
2572
|
const softphone = useSoftphone();
|
|
1922
2573
|
const formatIntl2 = useIntlFormatter();
|
|
2574
|
+
const knownNumbers = useKnownOutboundNumbers(outboundNumberStore(softphone));
|
|
1923
2575
|
const [detail, setDetail] = useState(null);
|
|
1924
2576
|
const [loadingDetail, setLoadingDetail] = useState(false);
|
|
1925
2577
|
useEffect(() => {
|
|
@@ -1935,6 +2587,7 @@ function HistoryDetail({
|
|
|
1935
2587
|
}, [entry]);
|
|
1936
2588
|
const formatted = formatIntl2(entry.number);
|
|
1937
2589
|
const name = entry.name ?? formatted;
|
|
2590
|
+
const from = outboundFrom(entry, knownNumbers, formatIntl2);
|
|
1938
2591
|
const handledBy = detail?.handledBy ?? entry.handledBy;
|
|
1939
2592
|
const recordingUrls = detail?.recordingUrls ?? [];
|
|
1940
2593
|
const loadingRecording = entry.recorded && loadingDetail && recordingUrls.length === 0;
|
|
@@ -1971,7 +2624,8 @@ function HistoryDetail({
|
|
|
1971
2624
|
entry.durationSec > 0 ? /* @__PURE__ */ jsx(DetailRow, { icon: Timer, eyebrow: "Duraci\xF3n", text: formatDuration(entry.durationSec) }) : null,
|
|
1972
2625
|
/* @__PURE__ */ jsx(DetailRow, { icon: Phone, eyebrow: "N\xFAmero", text: formatted }),
|
|
1973
2626
|
handledBy.length > 0 ? /* @__PURE__ */ jsx(DetailRow, { icon: User, eyebrow: handlerLabel, text: /* @__PURE__ */ jsx(HandlerChain, { handlers: handledBy }) }) : null,
|
|
1974
|
-
entry.direction === "inbound" && entry.did ? /* @__PURE__ */ jsx(DetailRow, { icon: Hash, eyebrow: "N\xFAmero marcado (DID)", text: formatIntl2(entry.did) }) : null
|
|
2627
|
+
entry.direction === "inbound" && entry.did ? /* @__PURE__ */ jsx(DetailRow, { icon: Hash, eyebrow: "N\xFAmero marcado (DID)", text: formatIntl2(entry.did) }) : null,
|
|
2628
|
+
from ? /* @__PURE__ */ jsx(DetailRow, { icon: PhoneOutgoing, eyebrow: "Llamada desde", text: from }) : null
|
|
1975
2629
|
] })
|
|
1976
2630
|
] }),
|
|
1977
2631
|
/* @__PURE__ */ jsx("div", { className: "cv-detail-foot", children: /* @__PURE__ */ jsxs(
|
|
@@ -2164,6 +2818,110 @@ function HistoryTab({ source }) {
|
|
|
2164
2818
|
selected ? /* @__PURE__ */ jsx(HistoryDetail, { entry: selected, onBack: () => setSelected(null) }) : null
|
|
2165
2819
|
] });
|
|
2166
2820
|
}
|
|
2821
|
+
|
|
2822
|
+
// src/ui/audio/ringtone-store.ts
|
|
2823
|
+
var stores2 = /* @__PURE__ */ new WeakMap();
|
|
2824
|
+
function ringtoneStore(softphone) {
|
|
2825
|
+
const source = softphone.sources?.ringtones;
|
|
2826
|
+
if (!source) return null;
|
|
2827
|
+
let store = stores2.get(softphone);
|
|
2828
|
+
if (!store) {
|
|
2829
|
+
store = createRingtoneStore(source);
|
|
2830
|
+
stores2.set(softphone, store);
|
|
2831
|
+
}
|
|
2832
|
+
return store;
|
|
2833
|
+
}
|
|
2834
|
+
function createRingtoneStore(source, decodeImpl = decodeRingtone) {
|
|
2835
|
+
let settings = null;
|
|
2836
|
+
let audioWanted = false;
|
|
2837
|
+
let loaded = null;
|
|
2838
|
+
let loading2 = null;
|
|
2839
|
+
let inFlight = null;
|
|
2840
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2841
|
+
const notify = () => {
|
|
2842
|
+
for (const l of listeners) l();
|
|
2843
|
+
};
|
|
2844
|
+
const effectiveUrl = () => {
|
|
2845
|
+
if (!settings) return null;
|
|
2846
|
+
return settings.catalogue.find((r) => r.id === settings.effective)?.url ?? null;
|
|
2847
|
+
};
|
|
2848
|
+
const loadEffective = async () => {
|
|
2849
|
+
const url = effectiveUrl();
|
|
2850
|
+
if (!audioWanted || !url || loaded?.url === url || loading2 === url) return;
|
|
2851
|
+
loading2 = url;
|
|
2852
|
+
try {
|
|
2853
|
+
const buffer = await decodeImpl(url);
|
|
2854
|
+
if (audioWanted && effectiveUrl() === url) {
|
|
2855
|
+
loaded = { url, buffer };
|
|
2856
|
+
notify();
|
|
2857
|
+
}
|
|
2858
|
+
} catch (error) {
|
|
2859
|
+
console.warn(`[softphone] ringtone could not be loaded, using the built-in tone: ${url}`, error);
|
|
2860
|
+
} finally {
|
|
2861
|
+
if (loading2 === url) loading2 = null;
|
|
2862
|
+
}
|
|
2863
|
+
};
|
|
2864
|
+
const apply = (next) => {
|
|
2865
|
+
settings = next;
|
|
2866
|
+
if (loaded && loaded.url !== effectiveUrl()) loaded = null;
|
|
2867
|
+
notify();
|
|
2868
|
+
};
|
|
2869
|
+
return {
|
|
2870
|
+
getSettings: () => settings,
|
|
2871
|
+
readyBuffer: () => loaded && loaded.url === effectiveUrl() ? loaded.buffer : null,
|
|
2872
|
+
readyLoop: () => {
|
|
2873
|
+
if (!loaded || loaded.url !== effectiveUrl() || !settings) return null;
|
|
2874
|
+
return settings.catalogue.find((r) => r.id === settings.effective)?.loop ?? null;
|
|
2875
|
+
},
|
|
2876
|
+
refresh() {
|
|
2877
|
+
inFlight ??= (async () => {
|
|
2878
|
+
if (!settings && source.cached) {
|
|
2879
|
+
const early = await source.cached().catch(() => null);
|
|
2880
|
+
if (early && !settings) {
|
|
2881
|
+
apply(early);
|
|
2882
|
+
void loadEffective();
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
try {
|
|
2886
|
+
apply(await source.get());
|
|
2887
|
+
} catch (error) {
|
|
2888
|
+
console.warn("[softphone] ringtone settings could not be loaded; keeping the previous ones", error);
|
|
2889
|
+
} finally {
|
|
2890
|
+
inFlight = null;
|
|
2891
|
+
}
|
|
2892
|
+
await loadEffective();
|
|
2893
|
+
})();
|
|
2894
|
+
return inFlight;
|
|
2895
|
+
},
|
|
2896
|
+
async setMine(id) {
|
|
2897
|
+
apply(await source.setMine(id));
|
|
2898
|
+
void loadEffective();
|
|
2899
|
+
},
|
|
2900
|
+
wantAudio(on) {
|
|
2901
|
+
if (audioWanted === on) return;
|
|
2902
|
+
audioWanted = on;
|
|
2903
|
+
if (!on) {
|
|
2904
|
+
loaded = null;
|
|
2905
|
+
notify();
|
|
2906
|
+
} else {
|
|
2907
|
+
void loadEffective();
|
|
2908
|
+
}
|
|
2909
|
+
},
|
|
2910
|
+
decode: (url) => decodeImpl(url),
|
|
2911
|
+
subscribe(listener) {
|
|
2912
|
+
listeners.add(listener);
|
|
2913
|
+
return () => listeners.delete(listener);
|
|
2914
|
+
}
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
async function decodeRingtone(url) {
|
|
2918
|
+
const Offline = typeof OfflineAudioContext !== "undefined" ? OfflineAudioContext : globalThis.webkitOfflineAudioContext;
|
|
2919
|
+
if (!Offline) throw new Error("this browser cannot decode audio files");
|
|
2920
|
+
const res = await fetch(url, { credentials: "omit" });
|
|
2921
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2922
|
+
const bytes = await res.arrayBuffer();
|
|
2923
|
+
return new Offline(1, 1, 48e3).decodeAudioData(bytes);
|
|
2924
|
+
}
|
|
2167
2925
|
function SettingsSection({
|
|
2168
2926
|
title,
|
|
2169
2927
|
children
|
|
@@ -2173,11 +2931,641 @@ function SettingsSection({
|
|
|
2173
2931
|
/* @__PURE__ */ jsx("div", { className: "cv-settings-section-body", children })
|
|
2174
2932
|
] });
|
|
2175
2933
|
}
|
|
2934
|
+
function SettingsNavRow({
|
|
2935
|
+
icon: Icon3,
|
|
2936
|
+
title,
|
|
2937
|
+
value,
|
|
2938
|
+
onOpen,
|
|
2939
|
+
rowRef
|
|
2940
|
+
}) {
|
|
2941
|
+
const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2942
|
+
/* @__PURE__ */ jsx("span", { className: "cv-settings-row-icon", "aria-hidden": true, children: /* @__PURE__ */ jsx(Icon3, { size: 16 }) }),
|
|
2943
|
+
/* @__PURE__ */ jsxs("span", { className: "cv-settings-nav-text", children: [
|
|
2944
|
+
/* @__PURE__ */ jsx("span", { className: "cv-settings-nav-title", children: title }),
|
|
2945
|
+
/* @__PURE__ */ jsx("span", { className: "cv-settings-nav-value", children: value })
|
|
2946
|
+
] }),
|
|
2947
|
+
onOpen ? /* @__PURE__ */ jsx(ChevronRight, { size: 16, className: "cv-settings-nav-chevron", "aria-hidden": true }) : null
|
|
2948
|
+
] });
|
|
2949
|
+
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 });
|
|
2950
|
+
}
|
|
2951
|
+
function ChoiceList({
|
|
2952
|
+
label,
|
|
2953
|
+
choices,
|
|
2954
|
+
value,
|
|
2955
|
+
onSelect,
|
|
2956
|
+
onReselect,
|
|
2957
|
+
disabled = false
|
|
2958
|
+
}) {
|
|
2959
|
+
const name = useId();
|
|
2960
|
+
const pointer = useRef(false);
|
|
2961
|
+
return /* @__PURE__ */ jsx("div", { className: "cv-choices", role: "radiogroup", "aria-label": label, "aria-disabled": disabled || void 0, children: choices.map((c) => {
|
|
2962
|
+
const checked = c.value === value;
|
|
2963
|
+
return /* @__PURE__ */ jsxs(
|
|
2964
|
+
"label",
|
|
2965
|
+
{
|
|
2966
|
+
className: "cv-choice",
|
|
2967
|
+
"data-checked": checked || void 0,
|
|
2968
|
+
"data-disabled": disabled || void 0,
|
|
2969
|
+
onPointerDown: () => {
|
|
2970
|
+
pointer.current = true;
|
|
2971
|
+
},
|
|
2972
|
+
children: [
|
|
2973
|
+
/* @__PURE__ */ jsx(
|
|
2974
|
+
"input",
|
|
2975
|
+
{
|
|
2976
|
+
type: "radio",
|
|
2977
|
+
className: "cv-choice-input",
|
|
2978
|
+
name,
|
|
2979
|
+
value: c.value,
|
|
2980
|
+
checked,
|
|
2981
|
+
onChange: () => {
|
|
2982
|
+
const via = pointer.current ? "pointer" : "keyboard";
|
|
2983
|
+
pointer.current = false;
|
|
2984
|
+
if (!disabled) onSelect(c.value, via);
|
|
2985
|
+
},
|
|
2986
|
+
onClick: () => {
|
|
2987
|
+
if (c.value === value) {
|
|
2988
|
+
pointer.current = false;
|
|
2989
|
+
if (!disabled) onReselect?.(c.value);
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
),
|
|
2994
|
+
/* @__PURE__ */ jsxs("span", { className: "cv-choice-text", children: [
|
|
2995
|
+
c.tag ? /* @__PURE__ */ jsxs("span", { className: "cv-choice-line", children: [
|
|
2996
|
+
/* @__PURE__ */ jsx("span", { className: "cv-choice-label", children: c.label }),
|
|
2997
|
+
/* @__PURE__ */ jsx("span", { className: "cv-choice-tag", children: c.tag })
|
|
2998
|
+
] }) : /* @__PURE__ */ jsx("span", { className: "cv-choice-label", children: c.label }),
|
|
2999
|
+
c.sub ? /* @__PURE__ */ jsx("span", { className: "cv-choice-sub", children: c.sub }) : null,
|
|
3000
|
+
c.hint ? /* @__PURE__ */ jsx("span", { className: "cv-choice-hint", children: c.hint }) : null
|
|
3001
|
+
] }),
|
|
3002
|
+
c.trailing,
|
|
3003
|
+
/* @__PURE__ */ jsx(Check, { size: 16, className: "cv-choice-check", "aria-hidden": true })
|
|
3004
|
+
]
|
|
3005
|
+
},
|
|
3006
|
+
c.value
|
|
3007
|
+
);
|
|
3008
|
+
}) });
|
|
3009
|
+
}
|
|
3010
|
+
function SettingsPage({
|
|
3011
|
+
title,
|
|
3012
|
+
intro,
|
|
3013
|
+
onBack,
|
|
3014
|
+
children
|
|
3015
|
+
}) {
|
|
3016
|
+
const titleId = useId();
|
|
3017
|
+
const heading = useRef(null);
|
|
3018
|
+
useEffect(() => heading.current?.focus(), []);
|
|
3019
|
+
return /* @__PURE__ */ jsxs(
|
|
3020
|
+
"div",
|
|
3021
|
+
{
|
|
3022
|
+
className: "cv-detail cv-settings-page",
|
|
3023
|
+
role: "region",
|
|
3024
|
+
"aria-labelledby": titleId,
|
|
3025
|
+
onKeyDown: (e) => {
|
|
3026
|
+
if (e.key === "Escape") {
|
|
3027
|
+
e.stopPropagation();
|
|
3028
|
+
onBack();
|
|
3029
|
+
}
|
|
3030
|
+
},
|
|
3031
|
+
children: [
|
|
3032
|
+
/* @__PURE__ */ jsx("div", { className: "cv-detail-head", children: /* @__PURE__ */ jsxs("button", { type: "button", className: "cv-detail-back", onClick: onBack, children: [
|
|
3033
|
+
/* @__PURE__ */ jsx(ArrowLeft, { size: 16, "aria-hidden": true }),
|
|
3034
|
+
"Ajustes"
|
|
3035
|
+
] }) }),
|
|
3036
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-settings-page-body", children: [
|
|
3037
|
+
/* @__PURE__ */ jsx("h2", { className: "cv-settings-page-title", id: titleId, tabIndex: -1, ref: heading, children: title }),
|
|
3038
|
+
intro ? /* @__PURE__ */ jsx("p", { className: "cv-settings-page-intro", children: intro }) : null,
|
|
3039
|
+
children
|
|
3040
|
+
] })
|
|
3041
|
+
]
|
|
3042
|
+
}
|
|
3043
|
+
);
|
|
3044
|
+
}
|
|
3045
|
+
var SYSTEM_DEFAULT = "";
|
|
3046
|
+
var PAGES = {
|
|
3047
|
+
input: {
|
|
3048
|
+
title: "Micr\xF3fono",
|
|
3049
|
+
intro: "El micr\xF3fono que usa el tel\xE9fono en las llamadas.",
|
|
3050
|
+
empty: "No se encontr\xF3 ning\xFAn micr\xF3fono."
|
|
3051
|
+
},
|
|
3052
|
+
output: {
|
|
3053
|
+
title: "Altavoces",
|
|
3054
|
+
intro: "D\xF3nde oyes a la otra persona durante una llamada.",
|
|
3055
|
+
empty: "No se encontr\xF3 ning\xFAn altavoz."
|
|
3056
|
+
},
|
|
3057
|
+
ringtone: {
|
|
3058
|
+
title: "Salida del tono",
|
|
3059
|
+
intro: "D\xF3nde suena el tono cuando entra una llamada. La llamada se oye por \xABAltavoces\xBB.",
|
|
3060
|
+
empty: "No se encontr\xF3 ning\xFAn altavoz."
|
|
3061
|
+
}
|
|
3062
|
+
};
|
|
3063
|
+
function deviceSummary(kind, list, id) {
|
|
3064
|
+
const i = id ? list.findIndex((d) => d.deviceId === id) : -1;
|
|
3065
|
+
return i >= 0 ? deviceLabel(list[i], i, kind) : "Predeterminado del sistema";
|
|
3066
|
+
}
|
|
3067
|
+
function DevicePage({ kind, onBack }) {
|
|
3068
|
+
const devices = useDevices();
|
|
3069
|
+
const page = PAGES[kind];
|
|
3070
|
+
const list = kind === "input" ? devices.inputs : devices.outputs;
|
|
3071
|
+
const current = kind === "input" ? devices.currentInputId : kind === "output" ? devices.currentOutputId : devices.currentRingtoneOutputId;
|
|
3072
|
+
const apply = (id) => kind === "input" ? devices.setInput(id) : kind === "output" ? devices.setOutput(id) : devices.setRingtoneOutput(id);
|
|
3073
|
+
const choices = [
|
|
3074
|
+
{ value: SYSTEM_DEFAULT, label: "Predeterminado del sistema" },
|
|
3075
|
+
...list.map((d, i) => ({ value: d.deviceId, label: deviceLabel(d, i, kind) }))
|
|
3076
|
+
];
|
|
3077
|
+
return /* @__PURE__ */ jsxs(SettingsPage, { title: page.title, intro: page.intro, onBack, children: [
|
|
3078
|
+
list.length > 0 ? /* @__PURE__ */ jsx(
|
|
3079
|
+
ChoiceList,
|
|
3080
|
+
{
|
|
3081
|
+
label: page.title,
|
|
3082
|
+
choices,
|
|
3083
|
+
value: current ?? SYSTEM_DEFAULT,
|
|
3084
|
+
onSelect: (v) => void apply(v === SYSTEM_DEFAULT ? null : v)
|
|
3085
|
+
}
|
|
3086
|
+
) : /* @__PURE__ */ jsx("p", { className: "cv-settings-page-empty", children: page.empty }),
|
|
3087
|
+
kind === "output" ? /* @__PURE__ */ jsx(SpeakerTest, { sinkId: devices.currentOutputId }) : null
|
|
3088
|
+
] });
|
|
3089
|
+
}
|
|
3090
|
+
function SpeakerTest({ sinkId }) {
|
|
3091
|
+
const [testing, setTesting] = useState(false);
|
|
3092
|
+
const tone = useRef(null);
|
|
3093
|
+
const timer = useRef(null);
|
|
3094
|
+
useEffect(
|
|
3095
|
+
() => () => {
|
|
3096
|
+
tone.current?.stop();
|
|
3097
|
+
if (timer.current) clearTimeout(timer.current);
|
|
3098
|
+
},
|
|
3099
|
+
[]
|
|
3100
|
+
);
|
|
3101
|
+
const test = () => {
|
|
3102
|
+
primeAudio();
|
|
3103
|
+
tone.current?.stop();
|
|
3104
|
+
tone.current = playTestTone(sinkId);
|
|
3105
|
+
setTesting(true);
|
|
3106
|
+
if (timer.current) clearTimeout(timer.current);
|
|
3107
|
+
timer.current = setTimeout(() => setTesting(false), TEST_TONE_MS);
|
|
3108
|
+
};
|
|
3109
|
+
return /* @__PURE__ */ jsxs("button", { type: "button", className: "cv-btn cv-settings-test", onClick: test, disabled: testing, children: [
|
|
3110
|
+
/* @__PURE__ */ jsx(Volume2, { size: 16, "aria-hidden": true }),
|
|
3111
|
+
testing ? "Sonando\u2026" : "Probar altavoz"
|
|
3112
|
+
] });
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
// src/ui/audio/preview.ts
|
|
3116
|
+
var PREVIEW_MAX_MS = 1e4;
|
|
3117
|
+
var BUILT_IN_PREVIEW_MS = 6e3;
|
|
3118
|
+
function previewDurationMs(buffer, loop) {
|
|
3119
|
+
if (!buffer) return BUILT_IN_PREVIEW_MS;
|
|
3120
|
+
const seconds = loop ? loop.end - loop.start : buffer.duration;
|
|
3121
|
+
return Math.min(seconds * 1e3, PREVIEW_MAX_MS);
|
|
3122
|
+
}
|
|
3123
|
+
function previewRingtone(ringtone, options = {}) {
|
|
3124
|
+
primeAudio();
|
|
3125
|
+
let tone = null;
|
|
3126
|
+
let timer = null;
|
|
3127
|
+
let over = false;
|
|
3128
|
+
let settle;
|
|
3129
|
+
let fail;
|
|
3130
|
+
const done = new Promise((resolve, reject) => {
|
|
3131
|
+
settle = resolve;
|
|
3132
|
+
fail = reject;
|
|
3133
|
+
});
|
|
3134
|
+
done.catch(() => {
|
|
3135
|
+
});
|
|
3136
|
+
const stop = () => {
|
|
3137
|
+
if (over) return;
|
|
3138
|
+
over = true;
|
|
3139
|
+
if (timer) clearTimeout(timer);
|
|
3140
|
+
tone?.stop();
|
|
3141
|
+
settle();
|
|
3142
|
+
};
|
|
3143
|
+
const play = (buffer) => {
|
|
3144
|
+
if (over) return;
|
|
3145
|
+
const loop = buffer ? ringtone.loop ?? null : null;
|
|
3146
|
+
tone = playRingtone({ buffer, loop, sinkId: options.sinkId ?? null });
|
|
3147
|
+
timer = setTimeout(stop, previewDurationMs(buffer, loop));
|
|
3148
|
+
};
|
|
3149
|
+
if (!ringtone.url) {
|
|
3150
|
+
play(null);
|
|
3151
|
+
} else {
|
|
3152
|
+
decodeRingtone(ringtone.url).then(play, (error) => {
|
|
3153
|
+
if (over) return;
|
|
3154
|
+
over = true;
|
|
3155
|
+
fail(error);
|
|
3156
|
+
});
|
|
3157
|
+
}
|
|
3158
|
+
return { stop, done };
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
// src/ui/views/settings/ringtone-state.ts
|
|
3162
|
+
var noSubscribe2 = () => () => {
|
|
3163
|
+
};
|
|
3164
|
+
var noSettings2 = () => null;
|
|
3165
|
+
function useRingtoneSettings(store) {
|
|
3166
|
+
const settings = useSyncExternalStore(store?.subscribe ?? noSubscribe2, store?.getSettings ?? noSettings2);
|
|
3167
|
+
const [failed, setFailed] = useState(false);
|
|
3168
|
+
const load = useCallback(() => {
|
|
3169
|
+
if (!store) return () => {
|
|
3170
|
+
};
|
|
3171
|
+
let live = true;
|
|
3172
|
+
setFailed(false);
|
|
3173
|
+
void store.refresh().then(() => {
|
|
3174
|
+
if (live) setFailed(store.getSettings() === null);
|
|
3175
|
+
});
|
|
3176
|
+
return () => {
|
|
3177
|
+
live = false;
|
|
3178
|
+
};
|
|
3179
|
+
}, [store]);
|
|
3180
|
+
useEffect(load, [load]);
|
|
3181
|
+
return { settings, failed, retry: load };
|
|
3182
|
+
}
|
|
3183
|
+
function ringtoneSummary(settings, failed) {
|
|
3184
|
+
if (!settings) return failed ? "No disponible" : "Cargando\u2026";
|
|
3185
|
+
const byId = (id) => id ? settings.catalogue.find((r) => r.id === id) : void 0;
|
|
3186
|
+
const mine = byId(settings.mine);
|
|
3187
|
+
if (mine) return mine.name;
|
|
3188
|
+
const org = byId(settings.orgDefault);
|
|
3189
|
+
if (org) return `${org.name} \xB7 organizaci\xF3n`;
|
|
3190
|
+
return settings.catalogue.find((r) => r.url === null)?.name ?? "Predeterminado";
|
|
3191
|
+
}
|
|
3192
|
+
function useRingtonePreview(store, sinkId) {
|
|
3193
|
+
const [playing, setPlaying] = useState(null);
|
|
3194
|
+
const [error, setError] = useState(null);
|
|
3195
|
+
const current = useRef(null);
|
|
3196
|
+
const generation = useRef(0);
|
|
3197
|
+
const stop = useCallback(() => {
|
|
3198
|
+
generation.current += 1;
|
|
3199
|
+
const c = current.current;
|
|
3200
|
+
current.current = null;
|
|
3201
|
+
if (c?.timer) clearTimeout(c.timer);
|
|
3202
|
+
c?.tone?.stop();
|
|
3203
|
+
setPlaying(null);
|
|
3204
|
+
}, []);
|
|
3205
|
+
useEffect(() => stop, [stop]);
|
|
3206
|
+
const start = (settings, choice) => {
|
|
3207
|
+
stop();
|
|
3208
|
+
primeAudio();
|
|
3209
|
+
setError(null);
|
|
3210
|
+
const gen = generation.current;
|
|
3211
|
+
const orgDefault = settings.catalogue.some((r) => r.id === settings.orgDefault) ? settings.orgDefault : null;
|
|
3212
|
+
const id = choice || orgDefault;
|
|
3213
|
+
const option = settings.catalogue.find((r) => r.id === id) ?? settings.catalogue.find((r) => r.url === null);
|
|
3214
|
+
setPlaying(choice);
|
|
3215
|
+
current.current = { tone: null, timer: null };
|
|
3216
|
+
const play = (buffer) => {
|
|
3217
|
+
if (gen !== generation.current) return;
|
|
3218
|
+
const loop = buffer ? option?.loop ?? null : null;
|
|
3219
|
+
const tone = playRingtone({ buffer, loop, sinkId });
|
|
3220
|
+
current.current = { tone, timer: setTimeout(stop, previewDurationMs(buffer, loop)) };
|
|
3221
|
+
};
|
|
3222
|
+
if (!option?.url) {
|
|
3223
|
+
play(null);
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
store.decode(option.url).then(play, () => {
|
|
3227
|
+
if (gen !== generation.current) return;
|
|
3228
|
+
stop();
|
|
3229
|
+
setError("No se pudo reproducir el tono.");
|
|
3230
|
+
});
|
|
3231
|
+
};
|
|
3232
|
+
return { playing, error, start, stop };
|
|
3233
|
+
}
|
|
3234
|
+
var ORG_DEFAULT = "";
|
|
3235
|
+
var KEYBOARD_SETTLE_MS = 300;
|
|
3236
|
+
function RingtonePage({
|
|
3237
|
+
store,
|
|
3238
|
+
settings,
|
|
3239
|
+
failed,
|
|
3240
|
+
onRetry,
|
|
3241
|
+
onBack
|
|
3242
|
+
}) {
|
|
3243
|
+
const softphone = useSoftphone();
|
|
3244
|
+
const devices = useDevices();
|
|
3245
|
+
const preview = useRingtonePreview(store, devices.canSelectRingtoneOutput ? devices.currentRingtoneOutputId : null);
|
|
3246
|
+
const [pending, setPending] = useState(void 0);
|
|
3247
|
+
const [error, setError] = useState(null);
|
|
3248
|
+
const save = useSerialSave(store, {
|
|
3249
|
+
onSettled: () => setPending(void 0),
|
|
3250
|
+
onFailed: () => {
|
|
3251
|
+
setPending(void 0);
|
|
3252
|
+
setError("No se pudo guardar el tono. Int\xE9ntalo de nuevo.");
|
|
3253
|
+
}
|
|
3254
|
+
});
|
|
3255
|
+
const latest = useRef(settings);
|
|
3256
|
+
latest.current = settings;
|
|
3257
|
+
const settle = useRef(null);
|
|
3258
|
+
const { stop: stopPreview } = preview;
|
|
3259
|
+
useEffect(() => softphone.on("incomingCall" /* IncomingCall */, stopPreview), [softphone, stopPreview]);
|
|
3260
|
+
useEffect(
|
|
3261
|
+
() => () => {
|
|
3262
|
+
if (settle.current) {
|
|
3263
|
+
clearTimeout(settle.current.timer);
|
|
3264
|
+
save(settle.current.value || null);
|
|
3265
|
+
}
|
|
3266
|
+
},
|
|
3267
|
+
[save]
|
|
3268
|
+
);
|
|
3269
|
+
const commit = (value) => {
|
|
3270
|
+
save(value || null);
|
|
3271
|
+
if (latest.current) preview.start(latest.current, value);
|
|
3272
|
+
};
|
|
3273
|
+
const onSelect = (value, via) => {
|
|
3274
|
+
setError(null);
|
|
3275
|
+
setPending(value);
|
|
3276
|
+
if (settle.current) clearTimeout(settle.current.timer);
|
|
3277
|
+
settle.current = null;
|
|
3278
|
+
if (via === "pointer") {
|
|
3279
|
+
commit(value);
|
|
3280
|
+
return;
|
|
3281
|
+
}
|
|
3282
|
+
preview.stop();
|
|
3283
|
+
settle.current = {
|
|
3284
|
+
value,
|
|
3285
|
+
timer: setTimeout(() => {
|
|
3286
|
+
settle.current = null;
|
|
3287
|
+
commit(value);
|
|
3288
|
+
}, KEYBOARD_SETTLE_MS)
|
|
3289
|
+
};
|
|
3290
|
+
};
|
|
3291
|
+
const onReselect = (value) => {
|
|
3292
|
+
if (!settings) return;
|
|
3293
|
+
if (preview.playing === value) preview.stop();
|
|
3294
|
+
else preview.start(settings, value);
|
|
3295
|
+
};
|
|
3296
|
+
return /* @__PURE__ */ jsx(
|
|
3297
|
+
SettingsPage,
|
|
3298
|
+
{
|
|
3299
|
+
title: "Tono de llamada",
|
|
3300
|
+
intro: "Lo que suena cuando entra una llamada. Se guarda en tu cuenta, as\xED que es el mismo en todos tus dispositivos.",
|
|
3301
|
+
onBack,
|
|
3302
|
+
children: settings ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3303
|
+
/* @__PURE__ */ jsx(
|
|
3304
|
+
ChoiceList,
|
|
3305
|
+
{
|
|
3306
|
+
label: "Tono de llamada",
|
|
3307
|
+
choices: choicesFor(settings, preview.playing),
|
|
3308
|
+
value: pending !== void 0 ? pending : currentValue(settings),
|
|
3309
|
+
onSelect,
|
|
3310
|
+
onReselect
|
|
3311
|
+
}
|
|
3312
|
+
),
|
|
3313
|
+
error ?? preview.error ? /* @__PURE__ */ jsx("p", { className: "cv-settings-error", role: "alert", children: error ?? preview.error }) : null
|
|
3314
|
+
] }) : failed ? /* @__PURE__ */ jsxs("div", { className: "cv-empty", children: [
|
|
3315
|
+
/* @__PURE__ */ jsx("p", { className: "cv-empty-title", children: "No se pudieron cargar los tonos." }),
|
|
3316
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "cv-btn cv-settings-retry", onClick: onRetry, children: "Reintentar" })
|
|
3317
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "cv-empty", role: "status", children: [
|
|
3318
|
+
/* @__PURE__ */ jsx(LoaderCircle, { className: "cv-empty-spinner", size: 18, "aria-hidden": true }),
|
|
3319
|
+
/* @__PURE__ */ jsx("span", { className: "cv-empty-title", children: "Cargando\u2026" })
|
|
3320
|
+
] })
|
|
3321
|
+
}
|
|
3322
|
+
);
|
|
3323
|
+
}
|
|
3324
|
+
function currentValue(settings) {
|
|
3325
|
+
return settings.mine && settings.catalogue.some((r) => r.id === settings.mine) ? settings.mine : ORG_DEFAULT;
|
|
3326
|
+
}
|
|
3327
|
+
function choicesFor(settings, playing) {
|
|
3328
|
+
const org = settings.orgDefault ? settings.catalogue.find((r) => r.id === settings.orgDefault) : void 0;
|
|
3329
|
+
const builtIn = settings.catalogue.find((r) => r.url === null);
|
|
3330
|
+
const mark = (value) => playing === value ? /* @__PURE__ */ jsxs("span", { className: "cv-choice-playing", children: [
|
|
3331
|
+
/* @__PURE__ */ jsx(Volume2, { size: 15, "aria-hidden": true }),
|
|
3332
|
+
/* @__PURE__ */ jsx("span", { className: "cv-sr-only", children: "Sonando" })
|
|
3333
|
+
] }) : void 0;
|
|
3334
|
+
const orgChoice = {
|
|
3335
|
+
value: ORG_DEFAULT,
|
|
3336
|
+
label: org ? "Predeterminado de la organizaci\xF3n" : "Predeterminado",
|
|
3337
|
+
sub: (org ?? builtIn)?.name ?? "",
|
|
3338
|
+
trailing: mark(ORG_DEFAULT)
|
|
3339
|
+
};
|
|
3340
|
+
return [orgChoice, ...settings.catalogue.map((r) => ({ value: r.id, label: r.name, trailing: mark(r.id) }))];
|
|
3341
|
+
}
|
|
3342
|
+
function useSerialSave(store, handlers) {
|
|
3343
|
+
const wanted = useRef(null);
|
|
3344
|
+
const busy = useRef(false);
|
|
3345
|
+
const on = useRef(handlers);
|
|
3346
|
+
on.current = handlers;
|
|
3347
|
+
const [save] = useState(() => (id) => {
|
|
3348
|
+
wanted.current = { id };
|
|
3349
|
+
if (busy.current) return;
|
|
3350
|
+
busy.current = true;
|
|
3351
|
+
void (async () => {
|
|
3352
|
+
while (wanted.current) {
|
|
3353
|
+
const { id: next } = wanted.current;
|
|
3354
|
+
wanted.current = null;
|
|
3355
|
+
try {
|
|
3356
|
+
await store.setMine(next);
|
|
3357
|
+
if (!wanted.current) on.current.onSettled();
|
|
3358
|
+
} catch {
|
|
3359
|
+
if (!wanted.current) on.current.onFailed();
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
busy.current = false;
|
|
3363
|
+
})();
|
|
3364
|
+
});
|
|
3365
|
+
return save;
|
|
3366
|
+
}
|
|
3367
|
+
var ORG_DEFAULT2 = "";
|
|
3368
|
+
var KEYBOARD_SETTLE_MS2 = 800;
|
|
3369
|
+
function OutboundNumberPage({
|
|
3370
|
+
store,
|
|
3371
|
+
settings,
|
|
3372
|
+
onBack
|
|
3373
|
+
}) {
|
|
3374
|
+
const formatIntl2 = useIntlFormatter();
|
|
3375
|
+
const current = numberName(effectiveNumber(settings), formatIntl2);
|
|
3376
|
+
const change = useOutboundNumberChange(store, settings, current);
|
|
3377
|
+
const [pending, setPending] = useState(void 0);
|
|
3378
|
+
const settle = useRef(null);
|
|
3379
|
+
const choose = useRef(change.choose);
|
|
3380
|
+
choose.current = change.choose;
|
|
3381
|
+
useEffect(
|
|
3382
|
+
() => () => {
|
|
3383
|
+
if (settle.current) clearTimeout(settle.current);
|
|
3384
|
+
},
|
|
3385
|
+
[]
|
|
3386
|
+
);
|
|
3387
|
+
const notice = useRef(null);
|
|
3388
|
+
const apply = async (value) => {
|
|
3389
|
+
setPending(value);
|
|
3390
|
+
const applied = await choose.current(value || null);
|
|
3391
|
+
setPending(void 0);
|
|
3392
|
+
if (!applied) requestAnimationFrame(() => notice.current?.scrollIntoView?.({ block: "nearest" }));
|
|
3393
|
+
};
|
|
3394
|
+
const onSelect = (value, via) => {
|
|
3395
|
+
if (settle.current) clearTimeout(settle.current);
|
|
3396
|
+
settle.current = null;
|
|
3397
|
+
if (via === "pointer") {
|
|
3398
|
+
void apply(value);
|
|
3399
|
+
return;
|
|
3400
|
+
}
|
|
3401
|
+
setPending(value);
|
|
3402
|
+
settle.current = setTimeout(() => {
|
|
3403
|
+
settle.current = null;
|
|
3404
|
+
void apply(value);
|
|
3405
|
+
}, KEYBOARD_SETTLE_MS2);
|
|
3406
|
+
};
|
|
3407
|
+
return /* @__PURE__ */ jsxs(
|
|
3408
|
+
SettingsPage,
|
|
3409
|
+
{
|
|
3410
|
+
title: "N\xFAmero de salida",
|
|
3411
|
+
intro: "El n\xFAmero que ve la persona a la que llamas. Es el mismo en todos tus dispositivos y tambi\xE9n puedes cambiarlo desde el teclado.",
|
|
3412
|
+
onBack,
|
|
3413
|
+
children: [
|
|
3414
|
+
change.message ? /* @__PURE__ */ jsx("p", { className: "cv-settings-notice", role: "status", "data-kind": change.isError ? "error" : "info", ref: notice, children: change.message }) : null,
|
|
3415
|
+
/* @__PURE__ */ jsx(
|
|
3416
|
+
ChoiceList,
|
|
3417
|
+
{
|
|
3418
|
+
label: "N\xFAmero de salida",
|
|
3419
|
+
choices: choicesFor2(settings, formatIntl2, change.busy ? pending : void 0),
|
|
3420
|
+
value: pending ?? currentValue2(settings),
|
|
3421
|
+
onSelect,
|
|
3422
|
+
disabled: change.locked
|
|
3423
|
+
}
|
|
3424
|
+
)
|
|
3425
|
+
]
|
|
3426
|
+
}
|
|
3427
|
+
);
|
|
3428
|
+
}
|
|
3429
|
+
function currentValue2(settings) {
|
|
3430
|
+
return settings.mine && settings.numbers.some((n) => n.id === settings.mine) ? settings.mine : ORG_DEFAULT2;
|
|
3431
|
+
}
|
|
3432
|
+
function choicesFor2(settings, formatIntl2, applying) {
|
|
3433
|
+
const orgDefault = settings.numbers.find((n) => n.id === settings.orgDefault);
|
|
3434
|
+
const spinner = (value) => applying === value ? /* @__PURE__ */ jsxs("span", { className: "cv-choice-applying", children: [
|
|
3435
|
+
/* @__PURE__ */ jsx(LoaderCircle, { size: 15, className: "cv-choice-applying-icon", "aria-hidden": true }),
|
|
3436
|
+
/* @__PURE__ */ jsx("span", { className: "cv-sr-only", children: "Aplicando" })
|
|
3437
|
+
] }) : void 0;
|
|
3438
|
+
return [
|
|
3439
|
+
{
|
|
3440
|
+
value: ORG_DEFAULT2,
|
|
3441
|
+
label: "Por defecto de la organizaci\xF3n",
|
|
3442
|
+
sub: orgDefault ? numberName(orgDefault, formatIntl2) : "Sin n\xFAmero por defecto",
|
|
3443
|
+
trailing: spinner(ORG_DEFAULT2)
|
|
3444
|
+
},
|
|
3445
|
+
...settings.numbers.map((n) => {
|
|
3446
|
+
const kind = KIND_TAG[n.kind];
|
|
3447
|
+
return {
|
|
3448
|
+
value: n.id,
|
|
3449
|
+
label: numberName(n, formatIntl2),
|
|
3450
|
+
...n.label ? { sub: formatIntl2(`+${n.number}`) } : {},
|
|
3451
|
+
...kind ? { tag: kind.tag, hint: kind.hint } : {},
|
|
3452
|
+
trailing: spinner(n.id)
|
|
3453
|
+
};
|
|
3454
|
+
})
|
|
3455
|
+
];
|
|
3456
|
+
}
|
|
3457
|
+
function outboundSummary(settings, formatIntl2) {
|
|
3458
|
+
const effective = effectiveNumber(settings);
|
|
3459
|
+
const name = numberName(effective, formatIntl2);
|
|
3460
|
+
return settings.mine || !effective ? name : `${name} \xB7 organizaci\xF3n`;
|
|
3461
|
+
}
|
|
2176
3462
|
function SettingsTab() {
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
3463
|
+
const softphone = useSoftphone();
|
|
3464
|
+
const devices = useDevices();
|
|
3465
|
+
const store = ringtoneStore(softphone);
|
|
3466
|
+
const ringtone = useRingtoneSettings(store);
|
|
3467
|
+
const outStore = outboundNumberStore(softphone);
|
|
3468
|
+
const outbound = useOutboundNumbers(outStore).settings;
|
|
3469
|
+
const formatIntl2 = useIntlFormatter();
|
|
3470
|
+
const [page, setPage] = useState(null);
|
|
3471
|
+
const list = useRef(null);
|
|
3472
|
+
const rows = useRef({});
|
|
3473
|
+
const returnTo = useRef(null);
|
|
3474
|
+
useEffect(() => {
|
|
3475
|
+
void softphone.audio.refresh();
|
|
3476
|
+
}, [softphone]);
|
|
3477
|
+
useEffect(() => {
|
|
3478
|
+
list.current?.toggleAttribute("inert", page !== null);
|
|
3479
|
+
if (page === null && returnTo.current) {
|
|
3480
|
+
rows.current[returnTo.current]?.focus();
|
|
3481
|
+
returnTo.current = null;
|
|
3482
|
+
}
|
|
3483
|
+
}, [page]);
|
|
3484
|
+
const open = (p) => () => {
|
|
3485
|
+
returnTo.current = p;
|
|
3486
|
+
setPage(p);
|
|
3487
|
+
};
|
|
3488
|
+
const back = () => setPage(null);
|
|
3489
|
+
const rowRef = (p) => (el) => {
|
|
3490
|
+
rows.current[p] = el;
|
|
3491
|
+
};
|
|
3492
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-view cv-settings", children: [
|
|
3493
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-settings-body", ref: list, children: [
|
|
3494
|
+
store ? /* @__PURE__ */ jsx(SettingsSection, { title: "Llamadas entrantes", children: /* @__PURE__ */ jsx(
|
|
3495
|
+
SettingsNavRow,
|
|
3496
|
+
{
|
|
3497
|
+
icon: Music,
|
|
3498
|
+
title: "Tono de llamada",
|
|
3499
|
+
value: ringtoneSummary(ringtone.settings, ringtone.failed),
|
|
3500
|
+
onOpen: open("tone"),
|
|
3501
|
+
rowRef: rowRef("tone")
|
|
3502
|
+
}
|
|
3503
|
+
) }) : null,
|
|
3504
|
+
outStore && outbound && outbound.numbers.length > 1 ? /* @__PURE__ */ jsx(SettingsSection, { title: "Llamadas salientes", children: /* @__PURE__ */ jsx(
|
|
3505
|
+
SettingsNavRow,
|
|
3506
|
+
{
|
|
3507
|
+
icon: PhoneOutgoing,
|
|
3508
|
+
title: "N\xFAmero de salida",
|
|
3509
|
+
value: outboundSummary(outbound, formatIntl2),
|
|
3510
|
+
onOpen: open("outbound"),
|
|
3511
|
+
rowRef: rowRef("outbound")
|
|
3512
|
+
}
|
|
3513
|
+
) }) : null,
|
|
3514
|
+
/* @__PURE__ */ jsxs(SettingsSection, { title: "Dispositivos", children: [
|
|
3515
|
+
/* @__PURE__ */ jsx(
|
|
3516
|
+
SettingsNavRow,
|
|
3517
|
+
{
|
|
3518
|
+
icon: Mic,
|
|
3519
|
+
title: "Micr\xF3fono",
|
|
3520
|
+
value: deviceSummary("input", devices.inputs, devices.currentInputId),
|
|
3521
|
+
onOpen: open("input"),
|
|
3522
|
+
rowRef: rowRef("input")
|
|
3523
|
+
}
|
|
3524
|
+
),
|
|
3525
|
+
devices.canSelectOutput ? /* @__PURE__ */ jsx(
|
|
3526
|
+
SettingsNavRow,
|
|
3527
|
+
{
|
|
3528
|
+
icon: Headphones,
|
|
3529
|
+
title: "Altavoces",
|
|
3530
|
+
value: deviceSummary("output", devices.outputs, devices.currentOutputId),
|
|
3531
|
+
onOpen: open("output"),
|
|
3532
|
+
rowRef: rowRef("output")
|
|
3533
|
+
}
|
|
3534
|
+
) : (
|
|
3535
|
+
// Safari and most of Firefox have no `setSinkId`: offering a page whose choice silently does
|
|
3536
|
+
// nothing is worse than saying where the choice really lives.
|
|
3537
|
+
/* @__PURE__ */ jsx(
|
|
3538
|
+
SettingsNavRow,
|
|
3539
|
+
{
|
|
3540
|
+
icon: Headphones,
|
|
3541
|
+
title: "Altavoces",
|
|
3542
|
+
value: "Tu navegador no permite elegirlos desde aqu\xED \u2014 c\xE1mbialos en los ajustes del sistema."
|
|
3543
|
+
}
|
|
3544
|
+
)
|
|
3545
|
+
),
|
|
3546
|
+
devices.canSelectRingtoneOutput ? /* @__PURE__ */ jsx(
|
|
3547
|
+
SettingsNavRow,
|
|
3548
|
+
{
|
|
3549
|
+
icon: BellRing,
|
|
3550
|
+
title: "Salida del tono",
|
|
3551
|
+
value: deviceSummary("ringtone", devices.outputs, devices.currentRingtoneOutputId),
|
|
3552
|
+
onOpen: open("ringtone"),
|
|
3553
|
+
rowRef: rowRef("ringtone")
|
|
3554
|
+
}
|
|
3555
|
+
) : null
|
|
3556
|
+
] })
|
|
3557
|
+
] }),
|
|
3558
|
+
page === "tone" && store ? /* @__PURE__ */ jsx(
|
|
3559
|
+
RingtonePage,
|
|
3560
|
+
{
|
|
3561
|
+
store,
|
|
3562
|
+
settings: ringtone.settings,
|
|
3563
|
+
failed: ringtone.failed,
|
|
3564
|
+
onRetry: ringtone.retry,
|
|
3565
|
+
onBack: back
|
|
3566
|
+
}
|
|
3567
|
+
) : page === "outbound" && outStore && outbound ? /* @__PURE__ */ jsx(OutboundNumberPage, { store: outStore, settings: outbound, onBack: back }) : page !== null && page !== "tone" && page !== "outbound" ? /* @__PURE__ */ jsx(DevicePage, { kind: page, onBack: back }) : null
|
|
3568
|
+
] });
|
|
2181
3569
|
}
|
|
2182
3570
|
function useMicPermission() {
|
|
2183
3571
|
const [state, setState] = useState("checking");
|
|
@@ -2406,14 +3794,29 @@ function useCallAudio(enabled) {
|
|
|
2406
3794
|
const offDeviceChange = softphone.on("deviceChange" /* DeviceChange */, () => {
|
|
2407
3795
|
void remote.current?.setOutputDevice(softphone.audio.currentOutputId);
|
|
2408
3796
|
});
|
|
3797
|
+
const ringtones = ringtoneStore(softphone);
|
|
3798
|
+
ringtones?.wantAudio(true);
|
|
3799
|
+
const refreshRingtone = () => {
|
|
3800
|
+
if (softphone.isLeader) void ringtones?.refresh();
|
|
3801
|
+
};
|
|
3802
|
+
refreshRingtone();
|
|
3803
|
+
const offLeader = softphone.on("leaderChanged" /* LeaderChanged */, ({ isLeader }) => {
|
|
3804
|
+
if (isLeader) refreshRingtone();
|
|
3805
|
+
});
|
|
2409
3806
|
const stopTone = () => {
|
|
2410
3807
|
tone.current?.stop();
|
|
2411
3808
|
tone.current = null;
|
|
2412
3809
|
};
|
|
2413
3810
|
const setTone = (next) => {
|
|
2414
3811
|
stopTone();
|
|
2415
|
-
if (next === "ringtone")
|
|
2416
|
-
|
|
3812
|
+
if (next === "ringtone") {
|
|
3813
|
+
const audio = softphone.audio;
|
|
3814
|
+
tone.current = playRingtone({
|
|
3815
|
+
buffer: ringtones?.readyBuffer() ?? null,
|
|
3816
|
+
loop: ringtones?.readyLoop() ?? null,
|
|
3817
|
+
sinkId: audio.canSelectRingtoneOutput ? audio.currentRingtoneOutputId : null
|
|
3818
|
+
});
|
|
3819
|
+
} else if (next === "ringback") tone.current = playRingback();
|
|
2417
3820
|
};
|
|
2418
3821
|
const perCall = [];
|
|
2419
3822
|
const followedCalls = /* @__PURE__ */ new Set();
|
|
@@ -2444,6 +3847,7 @@ function useCallAudio(enabled) {
|
|
|
2444
3847
|
stopTone();
|
|
2445
3848
|
remote.current?.detach();
|
|
2446
3849
|
if (wasConnected && (reason === "local" || reason === "remote")) playCallEndedTone();
|
|
3850
|
+
refreshRingtone();
|
|
2447
3851
|
})
|
|
2448
3852
|
);
|
|
2449
3853
|
};
|
|
@@ -2461,6 +3865,8 @@ function useCallAudio(enabled) {
|
|
|
2461
3865
|
offIncoming();
|
|
2462
3866
|
offActive();
|
|
2463
3867
|
offDeviceChange();
|
|
3868
|
+
offLeader();
|
|
3869
|
+
ringtones?.wantAudio(false);
|
|
2464
3870
|
for (const off of perCall) off();
|
|
2465
3871
|
stopTone();
|
|
2466
3872
|
remote.current?.destroy();
|
|
@@ -2635,6 +4041,7 @@ lucide-react/dist/esm/createLucideIcon.js:
|
|
|
2635
4041
|
lucide-react/dist/esm/icons/arrow-left-right.js:
|
|
2636
4042
|
lucide-react/dist/esm/icons/arrow-left.js:
|
|
2637
4043
|
lucide-react/dist/esm/icons/arrow-right.js:
|
|
4044
|
+
lucide-react/dist/esm/icons/bell-ring.js:
|
|
2638
4045
|
lucide-react/dist/esm/icons/calendar-clock.js:
|
|
2639
4046
|
lucide-react/dist/esm/icons/check.js:
|
|
2640
4047
|
lucide-react/dist/esm/icons/chevron-down.js:
|
|
@@ -2650,6 +4057,7 @@ lucide-react/dist/esm/icons/loader-circle.js:
|
|
|
2650
4057
|
lucide-react/dist/esm/icons/mic-off.js:
|
|
2651
4058
|
lucide-react/dist/esm/icons/mic.js:
|
|
2652
4059
|
lucide-react/dist/esm/icons/monitor-smartphone.js:
|
|
4060
|
+
lucide-react/dist/esm/icons/music.js:
|
|
2653
4061
|
lucide-react/dist/esm/icons/pause.js:
|
|
2654
4062
|
lucide-react/dist/esm/icons/phone-incoming.js:
|
|
2655
4063
|
lucide-react/dist/esm/icons/phone-missed.js:
|
|
@@ -2663,6 +4071,7 @@ lucide-react/dist/esm/icons/timer.js:
|
|
|
2663
4071
|
lucide-react/dist/esm/icons/triangle-alert.js:
|
|
2664
4072
|
lucide-react/dist/esm/icons/user.js:
|
|
2665
4073
|
lucide-react/dist/esm/icons/users.js:
|
|
4074
|
+
lucide-react/dist/esm/icons/volume-2.js:
|
|
2666
4075
|
lucide-react/dist/esm/icons/x.js:
|
|
2667
4076
|
lucide-react/dist/esm/lucide-react.js:
|
|
2668
4077
|
(**
|
|
@@ -2673,6 +4082,6 @@ lucide-react/dist/esm/lucide-react.js:
|
|
|
2673
4082
|
*)
|
|
2674
4083
|
*/
|
|
2675
4084
|
|
|
2676
|
-
export { CallScreen, ContactsTab, Dialpad, HistoryTab, MicPermissionGate, SoftphoneWidget };
|
|
4085
|
+
export { CallScreen, ContactsTab, Dialpad, HistoryTab, MicPermissionGate, SoftphoneWidget, previewRingtone };
|
|
2677
4086
|
//# sourceMappingURL=index.js.map
|
|
2678
4087
|
//# sourceMappingURL=index.js.map
|