@coventia/softphone 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{call-DUVnDxsz.d.cts → call-CaPDWVk3.d.cts} +50 -1
- package/dist/{call-DUVnDxsz.d.ts → call-CaPDWVk3.d.ts} +50 -1
- package/dist/{chunk-YWI6EQYS.js → chunk-EYEDPD7Y.js} +108 -15
- package/dist/chunk-EYEDPD7Y.js.map +1 -0
- package/dist/{chunk-LMJXX4YM.js → chunk-UHNLEBBO.js} +4 -2
- package/dist/chunk-UHNLEBBO.js.map +1 -0
- package/dist/element/index.d.ts +76 -11
- package/dist/element/index.js +1411 -478
- package/dist/element/index.js.map +1 -1
- package/dist/{engine-EPCKf-Bl.d.ts → engine-BgBsIcEa.d.ts} +1 -1
- package/dist/{engine-CDjkrXHY.d.cts → engine-CU2rEo3g.d.cts} +1 -1
- package/dist/index.cjs +318 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +157 -19
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +2 -0
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +1 -1
- package/dist/{softphone-BtdgZWkm.d.ts → softphone-DEt0TtQw.d.ts} +26 -10
- package/dist/{softphone-DHG9MTNg.d.cts → softphone-dZMkW8V1.d.cts} +26 -10
- package/dist/softphone.css +192 -46
- package/dist/{sources-5L2R4HTV.js → sources-M3GYMS6V.js} +58 -3
- package/dist/sources-M3GYMS6V.js.map +1 -0
- package/dist/testing/index.d.cts +2 -2
- package/dist/testing/index.d.ts +2 -2
- package/dist/ui/index.cjs +1308 -366
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +40 -7
- package/dist/ui/index.d.ts +40 -7
- package/dist/ui/index.js +1308 -369
- package/dist/ui/index.js.map +1 -1
- package/dist/zadarma/index.cjs +106 -13
- package/dist/zadarma/index.cjs.map +1 -1
- package/dist/zadarma/index.d.cts +2 -2
- package/dist/zadarma/index.d.ts +2 -2
- package/dist/zadarma/index.js +1 -1
- package/dist/zadarma-engine-L5DEFXUS.js +5 -0
- package/dist/{zadarma-engine-4VOPIBXG.js.map → zadarma-engine-L5DEFXUS.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-LMJXX4YM.js.map +0 -1
- package/dist/chunk-YWI6EQYS.js.map +0 -1
- package/dist/sources-5L2R4HTV.js.map +0 -1
- package/dist/zadarma-engine-4VOPIBXG.js +0 -5
package/dist/ui/index.cjs
CHANGED
|
@@ -13128,6 +13128,14 @@ var ArrowRight = createLucideIcon("ArrowRight", [
|
|
|
13128
13128
|
["path", { d: "m12 5 7 7-7 7", key: "xquz4c" }]
|
|
13129
13129
|
]);
|
|
13130
13130
|
|
|
13131
|
+
// ../../node_modules/lucide-react/dist/esm/icons/bell-ring.js
|
|
13132
|
+
var BellRing = createLucideIcon("BellRing", [
|
|
13133
|
+
["path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9", key: "1qo2s2" }],
|
|
13134
|
+
["path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0", key: "qgo35s" }],
|
|
13135
|
+
["path", { d: "M4 2C2.8 3.7 2 5.7 2 8", key: "tap9e0" }],
|
|
13136
|
+
["path", { d: "M22 8c0-2.3-.8-4.3-2-6", key: "5bb3ad" }]
|
|
13137
|
+
]);
|
|
13138
|
+
|
|
13131
13139
|
// ../../node_modules/lucide-react/dist/esm/icons/calendar-clock.js
|
|
13132
13140
|
var CalendarClock = createLucideIcon("CalendarClock", [
|
|
13133
13141
|
["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" }],
|
|
@@ -13242,6 +13250,13 @@ var MonitorSmartphone = createLucideIcon("MonitorSmartphone", [
|
|
|
13242
13250
|
["rect", { width: "6", height: "10", x: "16", y: "12", rx: "2", key: "1egngj" }]
|
|
13243
13251
|
]);
|
|
13244
13252
|
|
|
13253
|
+
// ../../node_modules/lucide-react/dist/esm/icons/music.js
|
|
13254
|
+
var Music = createLucideIcon("Music", [
|
|
13255
|
+
["path", { d: "M9 18V5l12-2v13", key: "1jmyc2" }],
|
|
13256
|
+
["circle", { cx: "6", cy: "18", r: "3", key: "fqmcym" }],
|
|
13257
|
+
["circle", { cx: "18", cy: "16", r: "3", key: "1hluhg" }]
|
|
13258
|
+
]);
|
|
13259
|
+
|
|
13245
13260
|
// ../../node_modules/lucide-react/dist/esm/icons/pause.js
|
|
13246
13261
|
var Pause = createLucideIcon("Pause", [
|
|
13247
13262
|
["rect", { x: "14", y: "4", width: "4", height: "16", rx: "1", key: "zuxfzm" }],
|
|
@@ -13367,6 +13382,19 @@ var Users = createLucideIcon("Users", [
|
|
|
13367
13382
|
["path", { d: "M16 3.13a4 4 0 0 1 0 7.75", key: "1da9ce" }]
|
|
13368
13383
|
]);
|
|
13369
13384
|
|
|
13385
|
+
// ../../node_modules/lucide-react/dist/esm/icons/volume-2.js
|
|
13386
|
+
var Volume2 = createLucideIcon("Volume2", [
|
|
13387
|
+
[
|
|
13388
|
+
"path",
|
|
13389
|
+
{
|
|
13390
|
+
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",
|
|
13391
|
+
key: "uqj9uw"
|
|
13392
|
+
}
|
|
13393
|
+
],
|
|
13394
|
+
["path", { d: "M16 9a5 5 0 0 1 0 6", key: "1q6k2b" }],
|
|
13395
|
+
["path", { d: "M19.364 18.364a9 9 0 0 0 0-12.728", key: "ijwkga" }]
|
|
13396
|
+
]);
|
|
13397
|
+
|
|
13370
13398
|
// ../../node_modules/lucide-react/dist/esm/icons/x.js
|
|
13371
13399
|
var X = createLucideIcon("X", [
|
|
13372
13400
|
["path", { d: "M18 6 6 18", key: "1bl5f8" }],
|
|
@@ -13493,7 +13521,9 @@ function useDevices() {
|
|
|
13493
13521
|
outputs: audio.outputs,
|
|
13494
13522
|
currentInputId: audio.currentInputId,
|
|
13495
13523
|
currentOutputId: audio.currentOutputId,
|
|
13524
|
+
currentRingtoneOutputId: audio.currentRingtoneOutputId,
|
|
13496
13525
|
canSelectOutput: audio.canSelectOutput,
|
|
13526
|
+
canSelectRingtoneOutput: audio.canSelectRingtoneOutput,
|
|
13497
13527
|
setInput: (deviceId) => audio.setInput(deviceId),
|
|
13498
13528
|
setOutput: (deviceId) => audio.setOutput(deviceId),
|
|
13499
13529
|
setRingtoneOutput: (deviceId) => audio.setRingtoneOutput(deviceId),
|
|
@@ -13721,151 +13751,582 @@ function caretAfterDigits(formatted, digitsBefore) {
|
|
|
13721
13751
|
function digitsBeforeCaret(text, caret) {
|
|
13722
13752
|
return (text.slice(0, caret).match(/\d/g) ?? []).length;
|
|
13723
13753
|
}
|
|
13724
|
-
|
|
13725
|
-
|
|
13726
|
-
|
|
13727
|
-
["
|
|
13728
|
-
["
|
|
13729
|
-
["
|
|
13730
|
-
["
|
|
13731
|
-
["
|
|
13732
|
-
["
|
|
13733
|
-
["
|
|
13734
|
-
["
|
|
13735
|
-
["
|
|
13736
|
-
["
|
|
13754
|
+
|
|
13755
|
+
// src/ui/dialing/keys.ts
|
|
13756
|
+
var KEYPAD_KEYS = [
|
|
13757
|
+
["1", ""],
|
|
13758
|
+
["2", "ABC"],
|
|
13759
|
+
["3", "DEF"],
|
|
13760
|
+
["4", "GHI"],
|
|
13761
|
+
["5", "JKL"],
|
|
13762
|
+
["6", "MNO"],
|
|
13763
|
+
["7", "PQRS"],
|
|
13764
|
+
["8", "TUV"],
|
|
13765
|
+
["9", "WXYZ"],
|
|
13766
|
+
["*", ""],
|
|
13767
|
+
["0", "+"],
|
|
13768
|
+
["#", ""]
|
|
13737
13769
|
];
|
|
13738
|
-
|
|
13739
|
-
|
|
13740
|
-
|
|
13741
|
-
|
|
13742
|
-
|
|
13743
|
-
|
|
13744
|
-
|
|
13745
|
-
|
|
13746
|
-
|
|
13747
|
-
const
|
|
13748
|
-
|
|
13749
|
-
|
|
13750
|
-
const
|
|
13751
|
-
|
|
13752
|
-
|
|
13753
|
-
|
|
13754
|
-
|
|
13755
|
-
}
|
|
13756
|
-
|
|
13757
|
-
|
|
13758
|
-
|
|
13759
|
-
};
|
|
13760
|
-
|
|
13761
|
-
|
|
13762
|
-
|
|
13770
|
+
|
|
13771
|
+
// src/ui/audio/classic-ring.ts
|
|
13772
|
+
var C6 = 1046.5;
|
|
13773
|
+
var E6 = 1318.51;
|
|
13774
|
+
var HARMONICITY = 4;
|
|
13775
|
+
var INDEX = 1.8;
|
|
13776
|
+
var CLASSIC_CYCLE_SECONDS = 3;
|
|
13777
|
+
var CLASSIC_GAIN = 0.3841;
|
|
13778
|
+
function classicRingGraph(ctx, dest) {
|
|
13779
|
+
const output = ctx.createGain();
|
|
13780
|
+
output.gain.value = CLASSIC_GAIN;
|
|
13781
|
+
const chain = ["highpass", "highpass", "lowpass", "lowpass"].map((type) => {
|
|
13782
|
+
const f = ctx.createBiquadFilter();
|
|
13783
|
+
f.type = type;
|
|
13784
|
+
f.frequency.value = type === "highpass" ? 250 : 7500;
|
|
13785
|
+
f.Q.value = 0.707;
|
|
13786
|
+
return f;
|
|
13787
|
+
});
|
|
13788
|
+
for (let i = 0; i < chain.length - 1; i++) chain[i].connect(chain[i + 1]);
|
|
13789
|
+
chain[chain.length - 1].connect(output);
|
|
13790
|
+
output.connect(dest);
|
|
13791
|
+
return { input: chain[0], output };
|
|
13792
|
+
}
|
|
13793
|
+
function strike(ctx, input, t, freq, vel, track) {
|
|
13794
|
+
const carrier = ctx.createOscillator();
|
|
13795
|
+
carrier.frequency.value = freq;
|
|
13796
|
+
const modulator = ctx.createOscillator();
|
|
13797
|
+
modulator.frequency.value = freq * HARMONICITY;
|
|
13798
|
+
const depth = ctx.createGain();
|
|
13799
|
+
depth.gain.setValueAtTime(0, t);
|
|
13800
|
+
depth.gain.linearRampToValueAtTime(INDEX * freq, t + 4e-3);
|
|
13801
|
+
depth.gain.setTargetAtTime(0, t + 4e-3, 0.02);
|
|
13802
|
+
modulator.connect(depth);
|
|
13803
|
+
depth.connect(carrier.frequency);
|
|
13804
|
+
const amp = ctx.createGain();
|
|
13805
|
+
amp.gain.setValueAtTime(0, t);
|
|
13806
|
+
amp.gain.linearRampToValueAtTime(vel, t + 5e-3);
|
|
13807
|
+
amp.gain.setTargetAtTime(0, t + 5e-3, 0.15);
|
|
13808
|
+
amp.gain.setTargetAtTime(0, t + 0.045, 0.08);
|
|
13809
|
+
carrier.connect(amp);
|
|
13810
|
+
amp.connect(input);
|
|
13811
|
+
for (const o of [carrier, modulator]) {
|
|
13812
|
+
o.start(t);
|
|
13813
|
+
o.stop(t + 0.7);
|
|
13814
|
+
track(o);
|
|
13815
|
+
}
|
|
13816
|
+
}
|
|
13817
|
+
function scheduleClassicCycle(ctx, input, t0, track) {
|
|
13818
|
+
for (const ring of [0, 0.6]) {
|
|
13819
|
+
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);
|
|
13820
|
+
}
|
|
13821
|
+
return CLASSIC_CYCLE_SECONDS;
|
|
13822
|
+
}
|
|
13823
|
+
|
|
13824
|
+
// src/ui/audio/audio.ts
|
|
13825
|
+
var TONE_HZ = 425;
|
|
13826
|
+
function createRemoteAudio() {
|
|
13827
|
+
const el = document.createElement("audio");
|
|
13828
|
+
el.autoplay = true;
|
|
13829
|
+
el.style.display = "none";
|
|
13830
|
+
document.body.appendChild(el);
|
|
13831
|
+
return {
|
|
13832
|
+
attach(stream) {
|
|
13833
|
+
el.srcObject = stream;
|
|
13834
|
+
void el.play().catch(() => {
|
|
13835
|
+
});
|
|
13836
|
+
},
|
|
13837
|
+
detach() {
|
|
13838
|
+
el.srcObject = null;
|
|
13839
|
+
},
|
|
13840
|
+
destroy() {
|
|
13841
|
+
el.srcObject = null;
|
|
13842
|
+
el.remove();
|
|
13843
|
+
},
|
|
13844
|
+
async setOutputDevice(deviceId) {
|
|
13845
|
+
if (typeof el.setSinkId !== "function") return;
|
|
13846
|
+
try {
|
|
13847
|
+
await el.setSinkId(deviceId ?? "");
|
|
13848
|
+
} catch {
|
|
13849
|
+
}
|
|
13850
|
+
}
|
|
13763
13851
|
};
|
|
13764
|
-
|
|
13765
|
-
|
|
13766
|
-
|
|
13767
|
-
|
|
13852
|
+
}
|
|
13853
|
+
var shared = null;
|
|
13854
|
+
function sharedContext() {
|
|
13855
|
+
if (shared) return shared;
|
|
13856
|
+
const Ctor = typeof AudioContext !== "undefined" ? AudioContext : globalThis.webkitAudioContext;
|
|
13857
|
+
if (!Ctor) return null;
|
|
13858
|
+
shared = new Ctor();
|
|
13859
|
+
return shared;
|
|
13860
|
+
}
|
|
13861
|
+
function primeAudio() {
|
|
13862
|
+
const ctx = sharedContext();
|
|
13863
|
+
if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
|
|
13864
|
+
});
|
|
13865
|
+
}
|
|
13866
|
+
function playRingtone(options = {}) {
|
|
13867
|
+
const ctx = sharedContext();
|
|
13868
|
+
if (!ctx) return { stop() {
|
|
13869
|
+
} };
|
|
13870
|
+
const bus = ctx.createGain();
|
|
13871
|
+
const route = routeToDevice(ctx, bus, options.sinkId ?? null);
|
|
13872
|
+
const voice = options.buffer ? fileVoice(ctx, bus, options.buffer, options.loop ?? null) : synthesisedVoice(ctx, bus);
|
|
13873
|
+
return {
|
|
13874
|
+
stop() {
|
|
13875
|
+
voice.stop();
|
|
13876
|
+
route.stop();
|
|
13877
|
+
}
|
|
13768
13878
|
};
|
|
13769
|
-
|
|
13770
|
-
|
|
13771
|
-
|
|
13772
|
-
|
|
13773
|
-
|
|
13774
|
-
|
|
13775
|
-
|
|
13776
|
-
|
|
13777
|
-
|
|
13778
|
-
|
|
13779
|
-
|
|
13780
|
-
|
|
13781
|
-
|
|
13782
|
-
|
|
13783
|
-
|
|
13784
|
-
|
|
13785
|
-
|
|
13786
|
-
|
|
13787
|
-
|
|
13788
|
-
|
|
13789
|
-
|
|
13790
|
-
|
|
13791
|
-
|
|
13792
|
-
|
|
13793
|
-
|
|
13794
|
-
|
|
13795
|
-
|
|
13796
|
-
|
|
13797
|
-
|
|
13798
|
-
|
|
13799
|
-
|
|
13800
|
-
|
|
13801
|
-
] }),
|
|
13802
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-dial-outcome", role: "status", part: "error", "data-shown": outcome !== null, children: outcome?.text ?? "" }),
|
|
13803
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-keypad", children: KEYS.map(([key, letters, dialable]) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13804
|
-
"button",
|
|
13805
|
-
{
|
|
13806
|
-
type: "button",
|
|
13807
|
-
className: "cv-key",
|
|
13808
|
-
"data-dialable": dialable,
|
|
13809
|
-
onClick: () => dialable && append(key),
|
|
13810
|
-
"aria-disabled": !dialable,
|
|
13811
|
-
title: dialable ? void 0 : "Solo disponible durante una llamada",
|
|
13812
|
-
children: [
|
|
13813
|
-
/* @__PURE__ */ jsxRuntime.jsx("b", { children: key }),
|
|
13814
|
-
letters ? /* @__PURE__ */ jsxRuntime.jsx("small", { children: letters }) : null
|
|
13815
|
-
]
|
|
13816
|
-
},
|
|
13817
|
-
key
|
|
13818
|
-
)) }),
|
|
13819
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-dial-foot", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
13820
|
-
"button",
|
|
13821
|
-
{
|
|
13822
|
-
type: "button",
|
|
13823
|
-
className: "cv-round cv-round--answer",
|
|
13824
|
-
onClick: call,
|
|
13825
|
-
disabled: !canCall,
|
|
13826
|
-
"aria-label": "Llamar",
|
|
13827
|
-
part: "call-button",
|
|
13828
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(Phone, { size: 26 })
|
|
13879
|
+
}
|
|
13880
|
+
function fileVoice(ctx, bus, buffer, loop) {
|
|
13881
|
+
const FILE_LEVEL = 1;
|
|
13882
|
+
const RAMP = 0.02;
|
|
13883
|
+
const LEAD = 0.08;
|
|
13884
|
+
const gain = ctx.createGain();
|
|
13885
|
+
gain.gain.value = 0;
|
|
13886
|
+
gain.connect(bus);
|
|
13887
|
+
const src = ctx.createBufferSource();
|
|
13888
|
+
src.buffer = buffer;
|
|
13889
|
+
src.loop = true;
|
|
13890
|
+
const points = loop && loop.start >= 0 && loop.end > loop.start && loop.end <= buffer.duration + 1e-3 ? loop : null;
|
|
13891
|
+
if (points) {
|
|
13892
|
+
src.loopStart = points.start;
|
|
13893
|
+
src.loopEnd = points.end;
|
|
13894
|
+
}
|
|
13895
|
+
src.connect(gain);
|
|
13896
|
+
let stopped = false;
|
|
13897
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
13898
|
+
}).finally(() => {
|
|
13899
|
+
if (stopped) return;
|
|
13900
|
+
const at = ctx.currentTime + LEAD;
|
|
13901
|
+
gain.gain.setValueAtTime(0, at);
|
|
13902
|
+
gain.gain.linearRampToValueAtTime(FILE_LEVEL, at + RAMP);
|
|
13903
|
+
src.start(at, points?.start ?? 0);
|
|
13904
|
+
});
|
|
13905
|
+
return {
|
|
13906
|
+
stop() {
|
|
13907
|
+
stopped = true;
|
|
13908
|
+
try {
|
|
13909
|
+
src.stop();
|
|
13910
|
+
} catch {
|
|
13829
13911
|
}
|
|
13830
|
-
|
|
13831
|
-
|
|
13912
|
+
try {
|
|
13913
|
+
gain.disconnect();
|
|
13914
|
+
} catch {
|
|
13915
|
+
}
|
|
13916
|
+
}
|
|
13917
|
+
};
|
|
13832
13918
|
}
|
|
13833
|
-
|
|
13834
|
-
|
|
13835
|
-
|
|
13836
|
-
|
|
13837
|
-
const
|
|
13838
|
-
const
|
|
13839
|
-
const
|
|
13840
|
-
|
|
13841
|
-
|
|
13919
|
+
function routeToDevice(ctx, bus, sinkId) {
|
|
13920
|
+
const toDefault = () => {
|
|
13921
|
+
bus.connect(ctx.destination);
|
|
13922
|
+
};
|
|
13923
|
+
const el = sinkId && typeof document !== "undefined" ? document.createElement("audio") : null;
|
|
13924
|
+
const dest = el && typeof ctx.createMediaStreamDestination === "function" ? ctx.createMediaStreamDestination() : null;
|
|
13925
|
+
const canRoute = el && dest && typeof el.setSinkId === "function";
|
|
13926
|
+
if (!canRoute || !sinkId) {
|
|
13927
|
+
toDefault();
|
|
13928
|
+
return { stop: () => safeDisconnect(bus) };
|
|
13929
|
+
}
|
|
13930
|
+
let stopped = false;
|
|
13931
|
+
el.style.display = "none";
|
|
13932
|
+
el.srcObject = dest.stream;
|
|
13933
|
+
document.body.appendChild(el);
|
|
13934
|
+
bus.connect(dest);
|
|
13935
|
+
void el.setSinkId(sinkId).then(() => el.play()).catch((error) => {
|
|
13936
|
+
if (stopped) return;
|
|
13937
|
+
console.warn("[softphone] sound could not be sent to the chosen device; using the default output", error);
|
|
13938
|
+
safeDisconnect(bus);
|
|
13939
|
+
teardown(el);
|
|
13940
|
+
toDefault();
|
|
13941
|
+
});
|
|
13942
|
+
return {
|
|
13943
|
+
stop() {
|
|
13944
|
+
stopped = true;
|
|
13945
|
+
safeDisconnect(bus);
|
|
13946
|
+
teardown(el);
|
|
13947
|
+
}
|
|
13948
|
+
};
|
|
13842
13949
|
}
|
|
13843
|
-
function
|
|
13844
|
-
|
|
13845
|
-
|
|
13846
|
-
|
|
13847
|
-
|
|
13950
|
+
function safeDisconnect(node) {
|
|
13951
|
+
try {
|
|
13952
|
+
node.disconnect();
|
|
13953
|
+
} catch {
|
|
13954
|
+
}
|
|
13848
13955
|
}
|
|
13849
|
-
function
|
|
13850
|
-
|
|
13851
|
-
|
|
13956
|
+
function teardown(el) {
|
|
13957
|
+
el.pause();
|
|
13958
|
+
el.srcObject = null;
|
|
13959
|
+
el.remove();
|
|
13852
13960
|
}
|
|
13853
|
-
|
|
13854
|
-
|
|
13855
|
-
|
|
13856
|
-
|
|
13857
|
-
|
|
13858
|
-
const
|
|
13859
|
-
|
|
13961
|
+
var TEST_TONE_MS = 900;
|
|
13962
|
+
function playTestTone(sinkId) {
|
|
13963
|
+
const ctx = sharedContext();
|
|
13964
|
+
if (!ctx) return { stop() {
|
|
13965
|
+
} };
|
|
13966
|
+
const bus = ctx.createGain();
|
|
13967
|
+
const route = routeToDevice(ctx, bus, sinkId);
|
|
13968
|
+
const LEVEL = 0.5;
|
|
13969
|
+
const NOTES = [
|
|
13970
|
+
{ hz: 660, at: 0.05 },
|
|
13971
|
+
{ hz: 880, at: 0.4 }
|
|
13972
|
+
];
|
|
13973
|
+
const oscs = [];
|
|
13974
|
+
let stopped = false;
|
|
13975
|
+
let timer = null;
|
|
13976
|
+
const stop = () => {
|
|
13977
|
+
if (stopped) return;
|
|
13978
|
+
stopped = true;
|
|
13979
|
+
if (timer) clearTimeout(timer);
|
|
13980
|
+
for (const o of oscs) {
|
|
13981
|
+
try {
|
|
13982
|
+
o.stop();
|
|
13983
|
+
} catch {
|
|
13984
|
+
}
|
|
13985
|
+
}
|
|
13986
|
+
route.stop();
|
|
13987
|
+
};
|
|
13988
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
13989
|
+
}).finally(() => {
|
|
13990
|
+
if (stopped) return;
|
|
13991
|
+
const t0 = ctx.currentTime;
|
|
13992
|
+
for (const { hz, at } of NOTES) {
|
|
13993
|
+
const gain = ctx.createGain();
|
|
13994
|
+
gain.gain.value = 0;
|
|
13995
|
+
gain.connect(bus);
|
|
13996
|
+
const osc = ctx.createOscillator();
|
|
13997
|
+
osc.type = "sine";
|
|
13998
|
+
osc.frequency.value = hz;
|
|
13999
|
+
osc.connect(gain);
|
|
14000
|
+
const start = t0 + at;
|
|
14001
|
+
gain.gain.setValueAtTime(0, start);
|
|
14002
|
+
gain.gain.linearRampToValueAtTime(LEVEL, start + 0.015);
|
|
14003
|
+
gain.gain.setValueAtTime(LEVEL, start + 0.25);
|
|
14004
|
+
gain.gain.linearRampToValueAtTime(0, start + 0.32);
|
|
14005
|
+
osc.start(start);
|
|
14006
|
+
osc.stop(start + 0.34);
|
|
14007
|
+
oscs.push(osc);
|
|
14008
|
+
}
|
|
14009
|
+
timer = setTimeout(stop, TEST_TONE_MS);
|
|
14010
|
+
});
|
|
14011
|
+
return { stop };
|
|
14012
|
+
}
|
|
14013
|
+
function synthesisedVoice(ctx, bus) {
|
|
14014
|
+
const { input, output } = classicRingGraph(ctx, bus);
|
|
14015
|
+
const scheduled = /* @__PURE__ */ new Set();
|
|
14016
|
+
const track = (o) => {
|
|
14017
|
+
scheduled.add(o);
|
|
14018
|
+
o.onended = () => scheduled.delete(o);
|
|
14019
|
+
};
|
|
14020
|
+
const LOOKAHEAD = 0.5;
|
|
14021
|
+
const TICK_MS = 150;
|
|
14022
|
+
const LEAD = 0.08;
|
|
14023
|
+
let stopped = false;
|
|
14024
|
+
let timer = null;
|
|
14025
|
+
let nextAt = 0;
|
|
14026
|
+
const pump = () => {
|
|
14027
|
+
while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleClassicCycle(ctx, input, nextAt, track);
|
|
14028
|
+
};
|
|
14029
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
14030
|
+
}).finally(() => {
|
|
14031
|
+
if (stopped) return;
|
|
14032
|
+
nextAt = ctx.currentTime + LEAD;
|
|
14033
|
+
pump();
|
|
14034
|
+
timer = setInterval(pump, TICK_MS);
|
|
14035
|
+
});
|
|
14036
|
+
return {
|
|
14037
|
+
stop() {
|
|
14038
|
+
stopped = true;
|
|
14039
|
+
if (timer) clearInterval(timer);
|
|
14040
|
+
for (const o of scheduled) {
|
|
14041
|
+
try {
|
|
14042
|
+
o.stop();
|
|
14043
|
+
} catch {
|
|
14044
|
+
}
|
|
14045
|
+
}
|
|
14046
|
+
scheduled.clear();
|
|
14047
|
+
try {
|
|
14048
|
+
output.disconnect();
|
|
14049
|
+
} catch {
|
|
14050
|
+
}
|
|
14051
|
+
}
|
|
14052
|
+
};
|
|
13860
14053
|
}
|
|
13861
|
-
|
|
13862
|
-
|
|
13863
|
-
|
|
13864
|
-
|
|
13865
|
-
|
|
13866
|
-
|
|
13867
|
-
|
|
13868
|
-
|
|
14054
|
+
function playCallEndedTone() {
|
|
14055
|
+
const ctx = sharedContext();
|
|
14056
|
+
if (!ctx) return;
|
|
14057
|
+
const gain = ctx.createGain();
|
|
14058
|
+
gain.gain.value = 0;
|
|
14059
|
+
gain.connect(ctx.destination);
|
|
14060
|
+
const osc = ctx.createOscillator();
|
|
14061
|
+
osc.type = "sine";
|
|
14062
|
+
osc.frequency.value = TONE_HZ;
|
|
14063
|
+
osc.connect(gain);
|
|
14064
|
+
osc.start();
|
|
14065
|
+
const LEVEL = 0.55;
|
|
14066
|
+
const RAMP = 8e-3;
|
|
14067
|
+
const BEEP = 0.25;
|
|
14068
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
14069
|
+
}).finally(() => {
|
|
14070
|
+
const at = ctx.currentTime + 0.02;
|
|
14071
|
+
gain.gain.setValueAtTime(0, at);
|
|
14072
|
+
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
14073
|
+
gain.gain.setValueAtTime(LEVEL, at + BEEP - RAMP);
|
|
14074
|
+
gain.gain.linearRampToValueAtTime(0, at + BEEP);
|
|
14075
|
+
const totalMs = (at + BEEP - ctx.currentTime) * 1e3;
|
|
14076
|
+
setTimeout(() => {
|
|
14077
|
+
try {
|
|
14078
|
+
osc.stop();
|
|
14079
|
+
} catch {
|
|
14080
|
+
}
|
|
14081
|
+
try {
|
|
14082
|
+
gain.disconnect();
|
|
14083
|
+
} catch {
|
|
14084
|
+
}
|
|
14085
|
+
}, totalMs);
|
|
14086
|
+
});
|
|
14087
|
+
}
|
|
14088
|
+
var DTMF_ROWS_HZ = [697, 770, 852, 941];
|
|
14089
|
+
var DTMF_COLS_HZ = [1209, 1336, 1477];
|
|
14090
|
+
var DTMF_LAYOUT = ["123", "456", "789", "*0#"];
|
|
14091
|
+
function dtmfFrequencies(key) {
|
|
14092
|
+
if (key.length !== 1) return null;
|
|
14093
|
+
for (let row = 0; row < DTMF_LAYOUT.length; row++) {
|
|
14094
|
+
const col = DTMF_LAYOUT[row].indexOf(key);
|
|
14095
|
+
if (col !== -1) return [DTMF_ROWS_HZ[row], DTMF_COLS_HZ[col]];
|
|
14096
|
+
}
|
|
14097
|
+
return null;
|
|
14098
|
+
}
|
|
14099
|
+
function playKeyTone(key) {
|
|
14100
|
+
const hz = dtmfFrequencies(key);
|
|
14101
|
+
if (!hz) return;
|
|
14102
|
+
const ctx = sharedContext();
|
|
14103
|
+
if (!ctx) return;
|
|
14104
|
+
const LEVEL = 0.2;
|
|
14105
|
+
const RAMP = 6e-3;
|
|
14106
|
+
const DURATION = 0.12;
|
|
14107
|
+
const LEAD = 0.01;
|
|
14108
|
+
const gain = ctx.createGain();
|
|
14109
|
+
gain.gain.value = 0;
|
|
14110
|
+
gain.connect(ctx.destination);
|
|
14111
|
+
const oscillators = hz.map((freq) => {
|
|
14112
|
+
const osc = ctx.createOscillator();
|
|
14113
|
+
osc.type = "sine";
|
|
14114
|
+
osc.frequency.value = freq;
|
|
14115
|
+
osc.connect(gain);
|
|
14116
|
+
osc.start();
|
|
14117
|
+
return osc;
|
|
14118
|
+
});
|
|
14119
|
+
const fire = () => {
|
|
14120
|
+
const at = ctx.currentTime + LEAD;
|
|
14121
|
+
gain.gain.setValueAtTime(0, at);
|
|
14122
|
+
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
14123
|
+
gain.gain.setValueAtTime(LEVEL, at + DURATION - RAMP);
|
|
14124
|
+
gain.gain.linearRampToValueAtTime(0, at + DURATION);
|
|
14125
|
+
for (const osc of oscillators) osc.stop(at + DURATION);
|
|
14126
|
+
oscillators[0].onended = () => gain.disconnect();
|
|
14127
|
+
};
|
|
14128
|
+
if (ctx.state === "running") fire();
|
|
14129
|
+
else void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
14130
|
+
}).finally(fire);
|
|
14131
|
+
}
|
|
14132
|
+
var ringbackUrl = null;
|
|
14133
|
+
function ringbackSource() {
|
|
14134
|
+
if (ringbackUrl) return ringbackUrl;
|
|
14135
|
+
const sampleRate = 48e3;
|
|
14136
|
+
const channels = 2;
|
|
14137
|
+
const bytesPerSample = 2;
|
|
14138
|
+
const amp = 0.5;
|
|
14139
|
+
const toneSec = 1.5;
|
|
14140
|
+
const silenceSec = 3;
|
|
14141
|
+
const totalFrames = Math.round((toneSec + silenceSec) * sampleRate);
|
|
14142
|
+
const toneFrames = Math.round(toneSec * sampleRate);
|
|
14143
|
+
const ramp = Math.round(0.02 * sampleRate);
|
|
14144
|
+
const blockAlign = channels * bytesPerSample;
|
|
14145
|
+
const dataSize = totalFrames * blockAlign;
|
|
14146
|
+
const buffer = new ArrayBuffer(44 + dataSize);
|
|
14147
|
+
const view = new DataView(buffer);
|
|
14148
|
+
const str = (off, s) => {
|
|
14149
|
+
for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
|
|
14150
|
+
};
|
|
14151
|
+
str(0, "RIFF");
|
|
14152
|
+
view.setUint32(4, 36 + dataSize, true);
|
|
14153
|
+
str(8, "WAVE");
|
|
14154
|
+
str(12, "fmt ");
|
|
14155
|
+
view.setUint32(16, 16, true);
|
|
14156
|
+
view.setUint16(20, 1, true);
|
|
14157
|
+
view.setUint16(22, channels, true);
|
|
14158
|
+
view.setUint32(24, sampleRate, true);
|
|
14159
|
+
view.setUint32(28, sampleRate * blockAlign, true);
|
|
14160
|
+
view.setUint16(32, blockAlign, true);
|
|
14161
|
+
view.setUint16(34, bytesPerSample * 8, true);
|
|
14162
|
+
str(36, "data");
|
|
14163
|
+
view.setUint32(40, dataSize, true);
|
|
14164
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
14165
|
+
let sample = 0;
|
|
14166
|
+
if (i < toneFrames) {
|
|
14167
|
+
let env = 1;
|
|
14168
|
+
if (i < ramp) env = i / ramp;
|
|
14169
|
+
else if (i > toneFrames - ramp) env = Math.max(0, (toneFrames - i) / ramp);
|
|
14170
|
+
sample = Math.sin(2 * Math.PI * TONE_HZ * (i / sampleRate)) * amp * env;
|
|
14171
|
+
}
|
|
14172
|
+
const s16 = Math.round(sample * 32767);
|
|
14173
|
+
const off = 44 + i * blockAlign;
|
|
14174
|
+
for (let c = 0; c < channels; c++) view.setInt16(off + c * bytesPerSample, s16, true);
|
|
14175
|
+
}
|
|
14176
|
+
ringbackUrl = URL.createObjectURL(new Blob([buffer], { type: "audio/wav" }));
|
|
14177
|
+
return ringbackUrl;
|
|
14178
|
+
}
|
|
14179
|
+
function playRingback() {
|
|
14180
|
+
const el = document.createElement("audio");
|
|
14181
|
+
el.src = ringbackSource();
|
|
14182
|
+
el.loop = true;
|
|
14183
|
+
el.style.display = "none";
|
|
14184
|
+
document.body.appendChild(el);
|
|
14185
|
+
void el.play().catch(() => {
|
|
14186
|
+
});
|
|
14187
|
+
return {
|
|
14188
|
+
stop() {
|
|
14189
|
+
el.pause();
|
|
14190
|
+
el.remove();
|
|
14191
|
+
}
|
|
14192
|
+
};
|
|
14193
|
+
}
|
|
14194
|
+
var NOT_DIALABLE = /* @__PURE__ */ new Set(["*", "#"]);
|
|
14195
|
+
function Dialpad({ outcome = null, onIntent, keyTones = true } = {}) {
|
|
14196
|
+
const softphone = useSoftphone();
|
|
14197
|
+
const state = useSoftphoneState();
|
|
14198
|
+
const [value, setValue] = React.useState("");
|
|
14199
|
+
const [country2, setCountry] = React.useState(DEFAULT_COUNTRY);
|
|
14200
|
+
const format = useNumberFormatter();
|
|
14201
|
+
const inputRef = React.useRef(null);
|
|
14202
|
+
const caretRef = React.useRef(null);
|
|
14203
|
+
const display = format(country2, value);
|
|
14204
|
+
const target = toE164(country2, value);
|
|
14205
|
+
const canCall = state === "online" && target.length > 1;
|
|
14206
|
+
React.useLayoutEffect(() => {
|
|
14207
|
+
const caret = caretRef.current;
|
|
14208
|
+
if (caret === null || !inputRef.current) return;
|
|
14209
|
+
caretRef.current = null;
|
|
14210
|
+
const pos = caretAfterDigits(display, caret);
|
|
14211
|
+
inputRef.current.setSelectionRange(pos, pos);
|
|
14212
|
+
}, [display]);
|
|
14213
|
+
const append = (key) => {
|
|
14214
|
+
onIntent?.();
|
|
14215
|
+
setValue((v) => v + key);
|
|
14216
|
+
if (keyTones) playKeyTone(key);
|
|
14217
|
+
};
|
|
14218
|
+
const backspace = () => {
|
|
14219
|
+
onIntent?.();
|
|
14220
|
+
setValue((v) => v.slice(0, -1));
|
|
14221
|
+
};
|
|
14222
|
+
const call = () => {
|
|
14223
|
+
if (!canCall) return;
|
|
14224
|
+
onIntent?.();
|
|
14225
|
+
void softphone.call(target);
|
|
14226
|
+
};
|
|
14227
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-view", children: [
|
|
14228
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-dial-prefix", children: /* @__PURE__ */ jsxRuntime.jsx(CountryPicker, { value: country2, onChange: setCountry }) }),
|
|
14229
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-dial-head", children: [
|
|
14230
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-dial-side", "aria-hidden": true }),
|
|
14231
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
14232
|
+
"input",
|
|
14233
|
+
{
|
|
14234
|
+
ref: inputRef,
|
|
14235
|
+
className: "cv-display",
|
|
14236
|
+
value: display,
|
|
14237
|
+
onChange: (e) => {
|
|
14238
|
+
onIntent?.();
|
|
14239
|
+
caretRef.current = digitsBeforeCaret(e.target.value, e.target.selectionStart ?? 0);
|
|
14240
|
+
setValue(e.target.value.replace(/[^\d+]/g, ""));
|
|
14241
|
+
},
|
|
14242
|
+
onKeyDown: (e) => e.key === "Enter" && call(),
|
|
14243
|
+
placeholder: "N\xFAmero",
|
|
14244
|
+
"aria-label": "N\xFAmero a marcar",
|
|
14245
|
+
inputMode: "tel"
|
|
14246
|
+
}
|
|
14247
|
+
),
|
|
14248
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
14249
|
+
"button",
|
|
14250
|
+
{
|
|
14251
|
+
type: "button",
|
|
14252
|
+
className: "cv-dial-side",
|
|
14253
|
+
"data-hidden": value.length === 0,
|
|
14254
|
+
onClick: backspace,
|
|
14255
|
+
"aria-label": "Borrar",
|
|
14256
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Delete, { size: 19 })
|
|
14257
|
+
}
|
|
14258
|
+
)
|
|
14259
|
+
] }),
|
|
14260
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-dial-outcome", role: "status", part: "error", "data-shown": outcome !== null, children: outcome?.text ?? "" }),
|
|
14261
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-keypad", children: KEYPAD_KEYS.map(([key, letters]) => {
|
|
14262
|
+
const dialable = !NOT_DIALABLE.has(key);
|
|
14263
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
14264
|
+
"button",
|
|
14265
|
+
{
|
|
14266
|
+
type: "button",
|
|
14267
|
+
className: "cv-key",
|
|
14268
|
+
"data-dialable": dialable,
|
|
14269
|
+
onClick: () => dialable && append(key),
|
|
14270
|
+
"aria-disabled": !dialable,
|
|
14271
|
+
title: dialable ? void 0 : "Solo disponible durante una llamada",
|
|
14272
|
+
children: [
|
|
14273
|
+
/* @__PURE__ */ jsxRuntime.jsx("b", { children: key }),
|
|
14274
|
+
letters ? /* @__PURE__ */ jsxRuntime.jsx("small", { children: letters }) : null
|
|
14275
|
+
]
|
|
14276
|
+
},
|
|
14277
|
+
key
|
|
14278
|
+
);
|
|
14279
|
+
}) }),
|
|
14280
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-dial-foot", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
14281
|
+
"button",
|
|
14282
|
+
{
|
|
14283
|
+
type: "button",
|
|
14284
|
+
className: "cv-round cv-round--answer",
|
|
14285
|
+
onClick: call,
|
|
14286
|
+
disabled: !canCall,
|
|
14287
|
+
"aria-label": "Llamar",
|
|
14288
|
+
part: "call-button",
|
|
14289
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Phone, { size: 26 })
|
|
14290
|
+
}
|
|
14291
|
+
) })
|
|
14292
|
+
] });
|
|
14293
|
+
}
|
|
14294
|
+
|
|
14295
|
+
// src/ui/lib/format.ts
|
|
14296
|
+
function formatDuration(totalSeconds) {
|
|
14297
|
+
const s = Math.max(0, Math.floor(totalSeconds));
|
|
14298
|
+
const hh = Math.floor(s / 3600);
|
|
14299
|
+
const mm = Math.floor(s % 3600 / 60);
|
|
14300
|
+
const ss = s % 60;
|
|
14301
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
14302
|
+
return hh > 0 ? `${hh}:${pad(mm)}:${pad(ss)}` : `${mm}:${pad(ss)}`;
|
|
14303
|
+
}
|
|
14304
|
+
function initials(name) {
|
|
14305
|
+
if (!name) return "#";
|
|
14306
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
14307
|
+
if (words.length === 0 || /^\+?\d/.test(words[0])) return "#";
|
|
14308
|
+
return words.slice(0, 2).map((w) => w[0].toUpperCase()).join("");
|
|
14309
|
+
}
|
|
14310
|
+
function elapsedSeconds(since, now) {
|
|
14311
|
+
if (!since) return 0;
|
|
14312
|
+
return Math.floor((now - since.getTime()) / 1e3);
|
|
14313
|
+
}
|
|
14314
|
+
function formatAge(totalSeconds) {
|
|
14315
|
+
const s = Math.max(0, Math.floor(totalSeconds));
|
|
14316
|
+
if (s < 60) return "justo ahora";
|
|
14317
|
+
const mm = Math.floor(s / 60);
|
|
14318
|
+
if (mm < 60) return `hace ${mm} min`;
|
|
14319
|
+
const hh = Math.floor(mm / 60);
|
|
14320
|
+
return `hace ${hh} h`;
|
|
14321
|
+
}
|
|
14322
|
+
|
|
14323
|
+
// src/ui/lib/error-text.ts
|
|
14324
|
+
var ERROR_TEXT = {
|
|
14325
|
+
// ── Engine lifecycle ────────────────────────────────────────────────────────
|
|
14326
|
+
// Integrator mistakes rather than user-facing failures, but they must still read as something
|
|
14327
|
+
// rather than as a blank panel if one ever reaches the UI.
|
|
14328
|
+
"engine/duplicate-instance": {
|
|
14329
|
+
title: "Tel\xE9fono duplicado",
|
|
13869
14330
|
body: "Hay otro tel\xE9fono abierto en esta p\xE1gina. Cierra uno y vuelve a cargar."
|
|
13870
14331
|
},
|
|
13871
14332
|
"engine/not-started": { title: "El tel\xE9fono a\xFAn no est\xE1 listo" },
|
|
@@ -14055,7 +14516,9 @@ function Control({
|
|
|
14055
14516
|
label,
|
|
14056
14517
|
pressed = false,
|
|
14057
14518
|
tone,
|
|
14058
|
-
onClick
|
|
14519
|
+
onClick,
|
|
14520
|
+
disabled = false,
|
|
14521
|
+
title
|
|
14059
14522
|
}) {
|
|
14060
14523
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-ctrl-item", children: [
|
|
14061
14524
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -14067,6 +14530,8 @@ function Control({
|
|
|
14067
14530
|
"aria-pressed": pressed,
|
|
14068
14531
|
"aria-label": label,
|
|
14069
14532
|
onClick,
|
|
14533
|
+
disabled,
|
|
14534
|
+
title,
|
|
14070
14535
|
children: /* @__PURE__ */ jsxRuntime.jsx(Icon3, { size: 19 })
|
|
14071
14536
|
}
|
|
14072
14537
|
),
|
|
@@ -14205,18 +14670,22 @@ function TransferPicker({
|
|
|
14205
14670
|
}
|
|
14206
14671
|
function CallScreen({
|
|
14207
14672
|
call,
|
|
14208
|
-
incomingCallAction
|
|
14673
|
+
incomingCallAction,
|
|
14674
|
+
keyTones = true
|
|
14209
14675
|
}) {
|
|
14210
14676
|
const view = useCall(call);
|
|
14211
14677
|
const softphone = useSoftphone();
|
|
14212
|
-
const [
|
|
14678
|
+
const [panel, setPanel] = React.useState(null);
|
|
14679
|
+
const showTransfer = panel === "transfer";
|
|
14680
|
+
const showKeypad = panel === "keypad";
|
|
14681
|
+
const showDevices = panel === "devices";
|
|
14682
|
+
const togglePanel = (p) => setPanel((cur) => cur === p ? null : p);
|
|
14213
14683
|
const [transferTo, setTransferTo] = React.useState("");
|
|
14214
14684
|
const [consultCall, setConsultCall] = React.useState(null);
|
|
14215
14685
|
const [consultOutcome, setConsultOutcome] = React.useState(null);
|
|
14216
14686
|
const [myUserId, setMyUserId] = React.useState(null);
|
|
14217
14687
|
const consultView = useCall(consultCall);
|
|
14218
14688
|
const consultIntentRef = React.useRef(null);
|
|
14219
|
-
const [showDevices, setShowDevices] = React.useState(false);
|
|
14220
14689
|
const controlsRef = React.useRef(null);
|
|
14221
14690
|
const now = useTicker(view?.state === "active");
|
|
14222
14691
|
const consultNow = useTicker(consultView?.state === "active");
|
|
@@ -14273,20 +14742,31 @@ function CallScreen({
|
|
|
14273
14742
|
React.useEffect(() => {
|
|
14274
14743
|
if (!showDevices) return;
|
|
14275
14744
|
const onKey = (e) => {
|
|
14276
|
-
if (e.key === "Escape")
|
|
14745
|
+
if (e.key === "Escape") setPanel(null);
|
|
14746
|
+
};
|
|
14747
|
+
document.addEventListener("keydown", onKey);
|
|
14748
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
14749
|
+
}, [showDevices]);
|
|
14750
|
+
React.useEffect(() => {
|
|
14751
|
+
if (!showKeypad) return;
|
|
14752
|
+
const onKey = (e) => {
|
|
14753
|
+
if (e.key === "Escape") setPanel(null);
|
|
14277
14754
|
};
|
|
14278
14755
|
document.addEventListener("keydown", onKey);
|
|
14279
14756
|
if (!isConnected) return () => document.removeEventListener("keydown", onKey);
|
|
14280
14757
|
const root = controlsRef.current?.getRootNode() ?? document;
|
|
14281
14758
|
const onDown = (e) => {
|
|
14282
|
-
if (!controlsRef.current?.contains(e.target))
|
|
14759
|
+
if (!controlsRef.current?.contains(e.target)) setPanel(null);
|
|
14283
14760
|
};
|
|
14284
14761
|
root.addEventListener("pointerdown", onDown);
|
|
14285
14762
|
return () => {
|
|
14286
14763
|
document.removeEventListener("keydown", onKey);
|
|
14287
14764
|
root.removeEventListener("pointerdown", onDown);
|
|
14288
14765
|
};
|
|
14289
|
-
}, [
|
|
14766
|
+
}, [showKeypad, isConnected]);
|
|
14767
|
+
React.useEffect(() => {
|
|
14768
|
+
if (view && view.state !== "active") setPanel((cur) => cur === "keypad" ? null : cur);
|
|
14769
|
+
}, [view?.state]);
|
|
14290
14770
|
if (!view || view.state === "ended") return null;
|
|
14291
14771
|
const s = view.state;
|
|
14292
14772
|
const isRinging = s === "ringing";
|
|
@@ -14312,9 +14792,9 @@ function CallScreen({
|
|
|
14312
14792
|
const [rawM = "0", ss = "00"] = formatDuration(elapsedSeconds(view.answeredAt, now)).split(":");
|
|
14313
14793
|
const mm = rawM.padStart(2, "0");
|
|
14314
14794
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-view", children: [
|
|
14315
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call", children:
|
|
14795
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call", children: showDevices ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-call-devices-inline", children: [
|
|
14316
14796
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-call-devices-header", children: [
|
|
14317
|
-
/* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () =>
|
|
14797
|
+
/* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setPanel(null), children: [
|
|
14318
14798
|
/* @__PURE__ */ jsxRuntime.jsx(ArrowLeft, { size: 13, "aria-hidden": true }),
|
|
14319
14799
|
"Volver"
|
|
14320
14800
|
] }),
|
|
@@ -14322,7 +14802,7 @@ function CallScreen({
|
|
|
14322
14802
|
] }),
|
|
14323
14803
|
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "cv-call-devices-context", children: [
|
|
14324
14804
|
showNumber ? `${number} \xB7 ` : "",
|
|
14325
|
-
STATUS_LABEL[s] ?? ""
|
|
14805
|
+
s === "active" ? `${mm}:${ss}` : STATUS_LABEL[s] ?? ""
|
|
14326
14806
|
] }),
|
|
14327
14807
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-settings-section-body", children: /* @__PURE__ */ jsxRuntime.jsx(DevicePickers, {}) })
|
|
14328
14808
|
] }) : transferPickerActive ? (
|
|
@@ -14410,7 +14890,7 @@ function CallScreen({
|
|
|
14410
14890
|
/* @__PURE__ */ jsxRuntime.jsx("i", { children: ":" }),
|
|
14411
14891
|
/* @__PURE__ */ jsxRuntime.jsx("b", { children: ss })
|
|
14412
14892
|
] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-call-status", children: s === "held" ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-badge-hold", children: STATUS_LABEL[s] }) : STATUS_LABEL[s] ?? "" }),
|
|
14413
|
-
|
|
14893
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-call-chips", children: [
|
|
14414
14894
|
incomingAction ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
14415
14895
|
"button",
|
|
14416
14896
|
{
|
|
@@ -14423,11 +14903,11 @@ function CallScreen({
|
|
|
14423
14903
|
]
|
|
14424
14904
|
}
|
|
14425
14905
|
) : null,
|
|
14426
|
-
/* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () =>
|
|
14906
|
+
/* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setPanel("devices"), children: [
|
|
14427
14907
|
/* @__PURE__ */ jsxRuntime.jsx(Headphones, { size: 13, "aria-hidden": true }),
|
|
14428
14908
|
"Dispositivos"
|
|
14429
14909
|
] })
|
|
14430
|
-
] })
|
|
14910
|
+
] })
|
|
14431
14911
|
] }) }),
|
|
14432
14912
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-controls", ref: controlsRef, children: isRinging ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-controls-primary", children: [
|
|
14433
14913
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-ctrl-item", children: [
|
|
@@ -14486,17 +14966,19 @@ function CallScreen({
|
|
|
14486
14966
|
label: "Transferir",
|
|
14487
14967
|
pressed: showTransfer,
|
|
14488
14968
|
tone: "accent",
|
|
14489
|
-
onClick: () =>
|
|
14969
|
+
onClick: () => togglePanel("transfer")
|
|
14490
14970
|
}
|
|
14491
14971
|
),
|
|
14492
14972
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
14493
14973
|
Control,
|
|
14494
14974
|
{
|
|
14495
|
-
icon:
|
|
14496
|
-
label: "
|
|
14497
|
-
pressed:
|
|
14975
|
+
icon: Grid3x3,
|
|
14976
|
+
label: "Teclado",
|
|
14977
|
+
pressed: showKeypad,
|
|
14498
14978
|
tone: "accent",
|
|
14499
|
-
|
|
14979
|
+
disabled: s !== "active",
|
|
14980
|
+
title: s === "held" ? "Reanuda la llamada para usar el teclado" : s === "reconnecting" ? "No disponible mientras se reconecta" : void 0,
|
|
14981
|
+
onClick: () => togglePanel("keypad")
|
|
14500
14982
|
}
|
|
14501
14983
|
)
|
|
14502
14984
|
] }) : null,
|
|
@@ -14514,7 +14996,22 @@ function CallScreen({
|
|
|
14514
14996
|
),
|
|
14515
14997
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Colgar" })
|
|
14516
14998
|
] }) }),
|
|
14517
|
-
inCall &&
|
|
14999
|
+
inCall && showKeypad ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-keypad-popover cv-keypad-popover--above", role: "dialog", "aria-label": "Teclado", children: KEYPAD_KEYS.map(([key, letters]) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
15000
|
+
"button",
|
|
15001
|
+
{
|
|
15002
|
+
type: "button",
|
|
15003
|
+
className: "cv-key",
|
|
15004
|
+
onClick: () => {
|
|
15005
|
+
call.sendDigits(key);
|
|
15006
|
+
if (keyTones) playKeyTone(key);
|
|
15007
|
+
},
|
|
15008
|
+
children: [
|
|
15009
|
+
/* @__PURE__ */ jsxRuntime.jsx("b", { children: key }),
|
|
15010
|
+
letters ? /* @__PURE__ */ jsxRuntime.jsx("small", { children: letters }) : null
|
|
15011
|
+
]
|
|
15012
|
+
},
|
|
15013
|
+
key
|
|
15014
|
+
)) }) : null
|
|
14518
15015
|
] }) })
|
|
14519
15016
|
] });
|
|
14520
15017
|
}
|
|
@@ -15053,20 +15550,640 @@ function HistoryTab({ source }) {
|
|
|
15053
15550
|
selected ? /* @__PURE__ */ jsxRuntime.jsx(HistoryDetail, { entry: selected, onBack: () => setSelected(null) }) : null
|
|
15054
15551
|
] });
|
|
15055
15552
|
}
|
|
15056
|
-
|
|
15057
|
-
|
|
15058
|
-
|
|
15059
|
-
|
|
15060
|
-
|
|
15061
|
-
|
|
15062
|
-
|
|
15553
|
+
|
|
15554
|
+
// src/ui/audio/ringtone-store.ts
|
|
15555
|
+
var stores = /* @__PURE__ */ new WeakMap();
|
|
15556
|
+
function ringtoneStore(softphone) {
|
|
15557
|
+
const source = softphone.sources?.ringtones;
|
|
15558
|
+
if (!source) return null;
|
|
15559
|
+
let store = stores.get(softphone);
|
|
15560
|
+
if (!store) {
|
|
15561
|
+
store = createRingtoneStore(source);
|
|
15562
|
+
stores.set(softphone, store);
|
|
15563
|
+
}
|
|
15564
|
+
return store;
|
|
15565
|
+
}
|
|
15566
|
+
function createRingtoneStore(source, decodeImpl = decodeRingtone) {
|
|
15567
|
+
let settings = null;
|
|
15568
|
+
let audioWanted = false;
|
|
15569
|
+
let loaded = null;
|
|
15570
|
+
let loading2 = null;
|
|
15571
|
+
let inFlight = null;
|
|
15572
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
15573
|
+
const notify = () => {
|
|
15574
|
+
for (const l of listeners) l();
|
|
15575
|
+
};
|
|
15576
|
+
const effectiveUrl = () => {
|
|
15577
|
+
if (!settings) return null;
|
|
15578
|
+
return settings.catalogue.find((r) => r.id === settings.effective)?.url ?? null;
|
|
15579
|
+
};
|
|
15580
|
+
const loadEffective = async () => {
|
|
15581
|
+
const url = effectiveUrl();
|
|
15582
|
+
if (!audioWanted || !url || loaded?.url === url || loading2 === url) return;
|
|
15583
|
+
loading2 = url;
|
|
15584
|
+
try {
|
|
15585
|
+
const buffer = await decodeImpl(url);
|
|
15586
|
+
if (audioWanted && effectiveUrl() === url) {
|
|
15587
|
+
loaded = { url, buffer };
|
|
15588
|
+
notify();
|
|
15589
|
+
}
|
|
15590
|
+
} catch (error) {
|
|
15591
|
+
console.warn(`[softphone] ringtone could not be loaded, using the built-in tone: ${url}`, error);
|
|
15592
|
+
} finally {
|
|
15593
|
+
if (loading2 === url) loading2 = null;
|
|
15594
|
+
}
|
|
15595
|
+
};
|
|
15596
|
+
const apply = (next) => {
|
|
15597
|
+
settings = next;
|
|
15598
|
+
if (loaded && loaded.url !== effectiveUrl()) loaded = null;
|
|
15599
|
+
notify();
|
|
15600
|
+
};
|
|
15601
|
+
return {
|
|
15602
|
+
getSettings: () => settings,
|
|
15603
|
+
readyBuffer: () => loaded && loaded.url === effectiveUrl() ? loaded.buffer : null,
|
|
15604
|
+
readyLoop: () => {
|
|
15605
|
+
if (!loaded || loaded.url !== effectiveUrl() || !settings) return null;
|
|
15606
|
+
return settings.catalogue.find((r) => r.id === settings.effective)?.loop ?? null;
|
|
15607
|
+
},
|
|
15608
|
+
refresh() {
|
|
15609
|
+
inFlight ??= (async () => {
|
|
15610
|
+
if (!settings && source.cached) {
|
|
15611
|
+
const early = await source.cached().catch(() => null);
|
|
15612
|
+
if (early && !settings) {
|
|
15613
|
+
apply(early);
|
|
15614
|
+
void loadEffective();
|
|
15615
|
+
}
|
|
15616
|
+
}
|
|
15617
|
+
try {
|
|
15618
|
+
apply(await source.get());
|
|
15619
|
+
} catch (error) {
|
|
15620
|
+
console.warn("[softphone] ringtone settings could not be loaded; keeping the previous ones", error);
|
|
15621
|
+
} finally {
|
|
15622
|
+
inFlight = null;
|
|
15623
|
+
}
|
|
15624
|
+
await loadEffective();
|
|
15625
|
+
})();
|
|
15626
|
+
return inFlight;
|
|
15627
|
+
},
|
|
15628
|
+
async setMine(id) {
|
|
15629
|
+
apply(await source.setMine(id));
|
|
15630
|
+
void loadEffective();
|
|
15631
|
+
},
|
|
15632
|
+
wantAudio(on) {
|
|
15633
|
+
if (audioWanted === on) return;
|
|
15634
|
+
audioWanted = on;
|
|
15635
|
+
if (!on) {
|
|
15636
|
+
loaded = null;
|
|
15637
|
+
notify();
|
|
15638
|
+
} else {
|
|
15639
|
+
void loadEffective();
|
|
15640
|
+
}
|
|
15641
|
+
},
|
|
15642
|
+
decode: (url) => decodeImpl(url),
|
|
15643
|
+
subscribe(listener) {
|
|
15644
|
+
listeners.add(listener);
|
|
15645
|
+
return () => listeners.delete(listener);
|
|
15646
|
+
}
|
|
15647
|
+
};
|
|
15648
|
+
}
|
|
15649
|
+
async function decodeRingtone(url) {
|
|
15650
|
+
const Offline = typeof OfflineAudioContext !== "undefined" ? OfflineAudioContext : globalThis.webkitOfflineAudioContext;
|
|
15651
|
+
if (!Offline) throw new Error("this browser cannot decode audio files");
|
|
15652
|
+
const res = await fetch(url, { credentials: "omit" });
|
|
15653
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
15654
|
+
const bytes = await res.arrayBuffer();
|
|
15655
|
+
return new Offline(1, 1, 48e3).decodeAudioData(bytes);
|
|
15656
|
+
}
|
|
15657
|
+
function SettingsSection({
|
|
15658
|
+
title,
|
|
15659
|
+
children
|
|
15660
|
+
}) {
|
|
15661
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "cv-settings-section", children: [
|
|
15662
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "cv-settings-section-title", children: title }),
|
|
15663
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-settings-section-body", children })
|
|
15664
|
+
] });
|
|
15665
|
+
}
|
|
15666
|
+
function SettingsNavRow({
|
|
15667
|
+
icon: Icon3,
|
|
15668
|
+
title,
|
|
15669
|
+
value,
|
|
15670
|
+
onOpen,
|
|
15671
|
+
rowRef
|
|
15672
|
+
}) {
|
|
15673
|
+
const inner = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
15674
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-settings-row-icon", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx(Icon3, { size: 16 }) }),
|
|
15675
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-settings-nav-text", children: [
|
|
15676
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-settings-nav-title", children: title }),
|
|
15677
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-settings-nav-value", children: value })
|
|
15678
|
+
] }),
|
|
15679
|
+
onOpen ? /* @__PURE__ */ jsxRuntime.jsx(ChevronRight, { size: 16, className: "cv-settings-nav-chevron", "aria-hidden": true }) : null
|
|
15680
|
+
] });
|
|
15681
|
+
return onOpen ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "cv-settings-nav", onClick: onOpen, ref: rowRef, children: inner }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-settings-nav cv-settings-nav--static", children: inner });
|
|
15682
|
+
}
|
|
15683
|
+
function ChoiceList({
|
|
15684
|
+
label,
|
|
15685
|
+
choices,
|
|
15686
|
+
value,
|
|
15687
|
+
onSelect,
|
|
15688
|
+
onReselect
|
|
15689
|
+
}) {
|
|
15690
|
+
const name = React.useId();
|
|
15691
|
+
const pointer = React.useRef(false);
|
|
15692
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-choices", role: "radiogroup", "aria-label": label, children: choices.map((c) => {
|
|
15693
|
+
const checked = c.value === value;
|
|
15694
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
15695
|
+
"label",
|
|
15696
|
+
{
|
|
15697
|
+
className: "cv-choice",
|
|
15698
|
+
"data-checked": checked || void 0,
|
|
15699
|
+
onPointerDown: () => {
|
|
15700
|
+
pointer.current = true;
|
|
15701
|
+
},
|
|
15702
|
+
children: [
|
|
15703
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
15704
|
+
"input",
|
|
15705
|
+
{
|
|
15706
|
+
type: "radio",
|
|
15707
|
+
className: "cv-choice-input",
|
|
15708
|
+
name,
|
|
15709
|
+
value: c.value,
|
|
15710
|
+
checked,
|
|
15711
|
+
onChange: () => {
|
|
15712
|
+
const via = pointer.current ? "pointer" : "keyboard";
|
|
15713
|
+
pointer.current = false;
|
|
15714
|
+
onSelect(c.value, via);
|
|
15715
|
+
},
|
|
15716
|
+
onClick: () => {
|
|
15717
|
+
if (c.value === value) {
|
|
15718
|
+
pointer.current = false;
|
|
15719
|
+
onReselect?.(c.value);
|
|
15720
|
+
}
|
|
15721
|
+
}
|
|
15722
|
+
}
|
|
15723
|
+
),
|
|
15724
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-choice-text", children: [
|
|
15725
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-choice-label", children: c.label }),
|
|
15726
|
+
c.sub ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-choice-sub", children: c.sub }) : null
|
|
15727
|
+
] }),
|
|
15728
|
+
c.trailing,
|
|
15729
|
+
/* @__PURE__ */ jsxRuntime.jsx(Check, { size: 16, className: "cv-choice-check", "aria-hidden": true })
|
|
15730
|
+
]
|
|
15731
|
+
},
|
|
15732
|
+
c.value
|
|
15733
|
+
);
|
|
15734
|
+
}) });
|
|
15735
|
+
}
|
|
15736
|
+
function SettingsPage({
|
|
15737
|
+
title,
|
|
15738
|
+
intro,
|
|
15739
|
+
onBack,
|
|
15740
|
+
children
|
|
15741
|
+
}) {
|
|
15742
|
+
const titleId = React.useId();
|
|
15743
|
+
const heading = React.useRef(null);
|
|
15744
|
+
React.useEffect(() => heading.current?.focus(), []);
|
|
15745
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
15746
|
+
"div",
|
|
15747
|
+
{
|
|
15748
|
+
className: "cv-detail cv-settings-page",
|
|
15749
|
+
role: "region",
|
|
15750
|
+
"aria-labelledby": titleId,
|
|
15751
|
+
onKeyDown: (e) => {
|
|
15752
|
+
if (e.key === "Escape") {
|
|
15753
|
+
e.stopPropagation();
|
|
15754
|
+
onBack();
|
|
15755
|
+
}
|
|
15756
|
+
},
|
|
15757
|
+
children: [
|
|
15758
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-detail-head", children: /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-detail-back", onClick: onBack, children: [
|
|
15759
|
+
/* @__PURE__ */ jsxRuntime.jsx(ArrowLeft, { size: 16, "aria-hidden": true }),
|
|
15760
|
+
"Ajustes"
|
|
15761
|
+
] }) }),
|
|
15762
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-settings-page-body", children: [
|
|
15763
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "cv-settings-page-title", id: titleId, tabIndex: -1, ref: heading, children: title }),
|
|
15764
|
+
intro ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-settings-page-intro", children: intro }) : null,
|
|
15765
|
+
children
|
|
15766
|
+
] })
|
|
15767
|
+
]
|
|
15768
|
+
}
|
|
15769
|
+
);
|
|
15770
|
+
}
|
|
15771
|
+
var SYSTEM_DEFAULT = "";
|
|
15772
|
+
var PAGES = {
|
|
15773
|
+
input: {
|
|
15774
|
+
title: "Micr\xF3fono",
|
|
15775
|
+
intro: "El micr\xF3fono que usa el tel\xE9fono en las llamadas.",
|
|
15776
|
+
empty: "No se encontr\xF3 ning\xFAn micr\xF3fono."
|
|
15777
|
+
},
|
|
15778
|
+
output: {
|
|
15779
|
+
title: "Altavoces",
|
|
15780
|
+
intro: "D\xF3nde oyes a la otra persona durante una llamada.",
|
|
15781
|
+
empty: "No se encontr\xF3 ning\xFAn altavoz."
|
|
15782
|
+
},
|
|
15783
|
+
ringtone: {
|
|
15784
|
+
title: "Salida del tono",
|
|
15785
|
+
intro: "D\xF3nde suena el tono cuando entra una llamada. La llamada se oye por \xABAltavoces\xBB.",
|
|
15786
|
+
empty: "No se encontr\xF3 ning\xFAn altavoz."
|
|
15787
|
+
}
|
|
15788
|
+
};
|
|
15789
|
+
function deviceSummary(kind, list, id) {
|
|
15790
|
+
const i = id ? list.findIndex((d) => d.deviceId === id) : -1;
|
|
15791
|
+
return i >= 0 ? deviceLabel(list[i], i, kind) : "Predeterminado del sistema";
|
|
15792
|
+
}
|
|
15793
|
+
function DevicePage({ kind, onBack }) {
|
|
15794
|
+
const devices = useDevices();
|
|
15795
|
+
const page = PAGES[kind];
|
|
15796
|
+
const list = kind === "input" ? devices.inputs : devices.outputs;
|
|
15797
|
+
const current = kind === "input" ? devices.currentInputId : kind === "output" ? devices.currentOutputId : devices.currentRingtoneOutputId;
|
|
15798
|
+
const apply = (id) => kind === "input" ? devices.setInput(id) : kind === "output" ? devices.setOutput(id) : devices.setRingtoneOutput(id);
|
|
15799
|
+
const choices = [
|
|
15800
|
+
{ value: SYSTEM_DEFAULT, label: "Predeterminado del sistema" },
|
|
15801
|
+
...list.map((d, i) => ({ value: d.deviceId, label: deviceLabel(d, i, kind) }))
|
|
15802
|
+
];
|
|
15803
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(SettingsPage, { title: page.title, intro: page.intro, onBack, children: [
|
|
15804
|
+
list.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
15805
|
+
ChoiceList,
|
|
15806
|
+
{
|
|
15807
|
+
label: page.title,
|
|
15808
|
+
choices,
|
|
15809
|
+
value: current ?? SYSTEM_DEFAULT,
|
|
15810
|
+
onSelect: (v) => void apply(v === SYSTEM_DEFAULT ? null : v)
|
|
15811
|
+
}
|
|
15812
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-settings-page-empty", children: page.empty }),
|
|
15813
|
+
kind === "output" ? /* @__PURE__ */ jsxRuntime.jsx(SpeakerTest, { sinkId: devices.currentOutputId }) : null
|
|
15814
|
+
] });
|
|
15815
|
+
}
|
|
15816
|
+
function SpeakerTest({ sinkId }) {
|
|
15817
|
+
const [testing, setTesting] = React.useState(false);
|
|
15818
|
+
const tone = React.useRef(null);
|
|
15819
|
+
const timer = React.useRef(null);
|
|
15820
|
+
React.useEffect(
|
|
15821
|
+
() => () => {
|
|
15822
|
+
tone.current?.stop();
|
|
15823
|
+
if (timer.current) clearTimeout(timer.current);
|
|
15824
|
+
},
|
|
15825
|
+
[]
|
|
15826
|
+
);
|
|
15827
|
+
const test = () => {
|
|
15828
|
+
primeAudio();
|
|
15829
|
+
tone.current?.stop();
|
|
15830
|
+
tone.current = playTestTone(sinkId);
|
|
15831
|
+
setTesting(true);
|
|
15832
|
+
if (timer.current) clearTimeout(timer.current);
|
|
15833
|
+
timer.current = setTimeout(() => setTesting(false), TEST_TONE_MS);
|
|
15834
|
+
};
|
|
15835
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-btn cv-settings-test", onClick: test, disabled: testing, children: [
|
|
15836
|
+
/* @__PURE__ */ jsxRuntime.jsx(Volume2, { size: 16, "aria-hidden": true }),
|
|
15837
|
+
testing ? "Sonando\u2026" : "Probar altavoz"
|
|
15063
15838
|
] });
|
|
15064
15839
|
}
|
|
15840
|
+
|
|
15841
|
+
// src/ui/audio/preview.ts
|
|
15842
|
+
var PREVIEW_MAX_MS = 1e4;
|
|
15843
|
+
var BUILT_IN_PREVIEW_MS = 6e3;
|
|
15844
|
+
function previewDurationMs(buffer, loop) {
|
|
15845
|
+
if (!buffer) return BUILT_IN_PREVIEW_MS;
|
|
15846
|
+
const seconds = loop ? loop.end - loop.start : buffer.duration;
|
|
15847
|
+
return Math.min(seconds * 1e3, PREVIEW_MAX_MS);
|
|
15848
|
+
}
|
|
15849
|
+
function previewRingtone(ringtone, options = {}) {
|
|
15850
|
+
primeAudio();
|
|
15851
|
+
let tone = null;
|
|
15852
|
+
let timer = null;
|
|
15853
|
+
let over = false;
|
|
15854
|
+
let settle;
|
|
15855
|
+
let fail;
|
|
15856
|
+
const done = new Promise((resolve, reject) => {
|
|
15857
|
+
settle = resolve;
|
|
15858
|
+
fail = reject;
|
|
15859
|
+
});
|
|
15860
|
+
done.catch(() => {
|
|
15861
|
+
});
|
|
15862
|
+
const stop = () => {
|
|
15863
|
+
if (over) return;
|
|
15864
|
+
over = true;
|
|
15865
|
+
if (timer) clearTimeout(timer);
|
|
15866
|
+
tone?.stop();
|
|
15867
|
+
settle();
|
|
15868
|
+
};
|
|
15869
|
+
const play = (buffer) => {
|
|
15870
|
+
if (over) return;
|
|
15871
|
+
const loop = buffer ? ringtone.loop ?? null : null;
|
|
15872
|
+
tone = playRingtone({ buffer, loop, sinkId: options.sinkId ?? null });
|
|
15873
|
+
timer = setTimeout(stop, previewDurationMs(buffer, loop));
|
|
15874
|
+
};
|
|
15875
|
+
if (!ringtone.url) {
|
|
15876
|
+
play(null);
|
|
15877
|
+
} else {
|
|
15878
|
+
decodeRingtone(ringtone.url).then(play, (error) => {
|
|
15879
|
+
if (over) return;
|
|
15880
|
+
over = true;
|
|
15881
|
+
fail(error);
|
|
15882
|
+
});
|
|
15883
|
+
}
|
|
15884
|
+
return { stop, done };
|
|
15885
|
+
}
|
|
15886
|
+
|
|
15887
|
+
// src/ui/views/settings/ringtone-state.ts
|
|
15888
|
+
var noSubscribe = () => () => {
|
|
15889
|
+
};
|
|
15890
|
+
var noSettings = () => null;
|
|
15891
|
+
function useRingtoneSettings(store) {
|
|
15892
|
+
const settings = React.useSyncExternalStore(store?.subscribe ?? noSubscribe, store?.getSettings ?? noSettings);
|
|
15893
|
+
const [failed, setFailed] = React.useState(false);
|
|
15894
|
+
const load = React.useCallback(() => {
|
|
15895
|
+
if (!store) return () => {
|
|
15896
|
+
};
|
|
15897
|
+
let live = true;
|
|
15898
|
+
setFailed(false);
|
|
15899
|
+
void store.refresh().then(() => {
|
|
15900
|
+
if (live) setFailed(store.getSettings() === null);
|
|
15901
|
+
});
|
|
15902
|
+
return () => {
|
|
15903
|
+
live = false;
|
|
15904
|
+
};
|
|
15905
|
+
}, [store]);
|
|
15906
|
+
React.useEffect(load, [load]);
|
|
15907
|
+
return { settings, failed, retry: load };
|
|
15908
|
+
}
|
|
15909
|
+
function ringtoneSummary(settings, failed) {
|
|
15910
|
+
if (!settings) return failed ? "No disponible" : "Cargando\u2026";
|
|
15911
|
+
const byId = (id) => id ? settings.catalogue.find((r) => r.id === id) : void 0;
|
|
15912
|
+
const mine = byId(settings.mine);
|
|
15913
|
+
if (mine) return mine.name;
|
|
15914
|
+
const org = byId(settings.orgDefault);
|
|
15915
|
+
if (org) return `${org.name} \xB7 organizaci\xF3n`;
|
|
15916
|
+
return settings.catalogue.find((r) => r.url === null)?.name ?? "Predeterminado";
|
|
15917
|
+
}
|
|
15918
|
+
function useRingtonePreview(store, sinkId) {
|
|
15919
|
+
const [playing, setPlaying] = React.useState(null);
|
|
15920
|
+
const [error, setError] = React.useState(null);
|
|
15921
|
+
const current = React.useRef(null);
|
|
15922
|
+
const generation = React.useRef(0);
|
|
15923
|
+
const stop = React.useCallback(() => {
|
|
15924
|
+
generation.current += 1;
|
|
15925
|
+
const c = current.current;
|
|
15926
|
+
current.current = null;
|
|
15927
|
+
if (c?.timer) clearTimeout(c.timer);
|
|
15928
|
+
c?.tone?.stop();
|
|
15929
|
+
setPlaying(null);
|
|
15930
|
+
}, []);
|
|
15931
|
+
React.useEffect(() => stop, [stop]);
|
|
15932
|
+
const start = (settings, choice) => {
|
|
15933
|
+
stop();
|
|
15934
|
+
primeAudio();
|
|
15935
|
+
setError(null);
|
|
15936
|
+
const gen = generation.current;
|
|
15937
|
+
const orgDefault = settings.catalogue.some((r) => r.id === settings.orgDefault) ? settings.orgDefault : null;
|
|
15938
|
+
const id = choice || orgDefault;
|
|
15939
|
+
const option = settings.catalogue.find((r) => r.id === id) ?? settings.catalogue.find((r) => r.url === null);
|
|
15940
|
+
setPlaying(choice);
|
|
15941
|
+
current.current = { tone: null, timer: null };
|
|
15942
|
+
const play = (buffer) => {
|
|
15943
|
+
if (gen !== generation.current) return;
|
|
15944
|
+
const loop = buffer ? option?.loop ?? null : null;
|
|
15945
|
+
const tone = playRingtone({ buffer, loop, sinkId });
|
|
15946
|
+
current.current = { tone, timer: setTimeout(stop, previewDurationMs(buffer, loop)) };
|
|
15947
|
+
};
|
|
15948
|
+
if (!option?.url) {
|
|
15949
|
+
play(null);
|
|
15950
|
+
return;
|
|
15951
|
+
}
|
|
15952
|
+
store.decode(option.url).then(play, () => {
|
|
15953
|
+
if (gen !== generation.current) return;
|
|
15954
|
+
stop();
|
|
15955
|
+
setError("No se pudo reproducir el tono.");
|
|
15956
|
+
});
|
|
15957
|
+
};
|
|
15958
|
+
return { playing, error, start, stop };
|
|
15959
|
+
}
|
|
15960
|
+
var ORG_DEFAULT = "";
|
|
15961
|
+
var KEYBOARD_SETTLE_MS = 300;
|
|
15962
|
+
function RingtonePage({
|
|
15963
|
+
store,
|
|
15964
|
+
settings,
|
|
15965
|
+
failed,
|
|
15966
|
+
onRetry,
|
|
15967
|
+
onBack
|
|
15968
|
+
}) {
|
|
15969
|
+
const softphone = useSoftphone();
|
|
15970
|
+
const devices = useDevices();
|
|
15971
|
+
const preview = useRingtonePreview(store, devices.canSelectRingtoneOutput ? devices.currentRingtoneOutputId : null);
|
|
15972
|
+
const [pending, setPending] = React.useState(void 0);
|
|
15973
|
+
const [error, setError] = React.useState(null);
|
|
15974
|
+
const save = useSerialSave(store, {
|
|
15975
|
+
onSettled: () => setPending(void 0),
|
|
15976
|
+
onFailed: () => {
|
|
15977
|
+
setPending(void 0);
|
|
15978
|
+
setError("No se pudo guardar el tono. Int\xE9ntalo de nuevo.");
|
|
15979
|
+
}
|
|
15980
|
+
});
|
|
15981
|
+
const latest = React.useRef(settings);
|
|
15982
|
+
latest.current = settings;
|
|
15983
|
+
const settle = React.useRef(null);
|
|
15984
|
+
const { stop: stopPreview } = preview;
|
|
15985
|
+
React.useEffect(() => softphone.on("incomingCall" /* IncomingCall */, stopPreview), [softphone, stopPreview]);
|
|
15986
|
+
React.useEffect(
|
|
15987
|
+
() => () => {
|
|
15988
|
+
if (settle.current) {
|
|
15989
|
+
clearTimeout(settle.current.timer);
|
|
15990
|
+
save(settle.current.value || null);
|
|
15991
|
+
}
|
|
15992
|
+
},
|
|
15993
|
+
[save]
|
|
15994
|
+
);
|
|
15995
|
+
const commit = (value) => {
|
|
15996
|
+
save(value || null);
|
|
15997
|
+
if (latest.current) preview.start(latest.current, value);
|
|
15998
|
+
};
|
|
15999
|
+
const onSelect = (value, via) => {
|
|
16000
|
+
setError(null);
|
|
16001
|
+
setPending(value);
|
|
16002
|
+
if (settle.current) clearTimeout(settle.current.timer);
|
|
16003
|
+
settle.current = null;
|
|
16004
|
+
if (via === "pointer") {
|
|
16005
|
+
commit(value);
|
|
16006
|
+
return;
|
|
16007
|
+
}
|
|
16008
|
+
preview.stop();
|
|
16009
|
+
settle.current = {
|
|
16010
|
+
value,
|
|
16011
|
+
timer: setTimeout(() => {
|
|
16012
|
+
settle.current = null;
|
|
16013
|
+
commit(value);
|
|
16014
|
+
}, KEYBOARD_SETTLE_MS)
|
|
16015
|
+
};
|
|
16016
|
+
};
|
|
16017
|
+
const onReselect = (value) => {
|
|
16018
|
+
if (!settings) return;
|
|
16019
|
+
if (preview.playing === value) preview.stop();
|
|
16020
|
+
else preview.start(settings, value);
|
|
16021
|
+
};
|
|
16022
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
16023
|
+
SettingsPage,
|
|
16024
|
+
{
|
|
16025
|
+
title: "Tono de llamada",
|
|
16026
|
+
intro: "Lo que suena cuando entra una llamada. Se guarda en tu cuenta, as\xED que es el mismo en todos tus dispositivos.",
|
|
16027
|
+
onBack,
|
|
16028
|
+
children: settings ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
16029
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
16030
|
+
ChoiceList,
|
|
16031
|
+
{
|
|
16032
|
+
label: "Tono de llamada",
|
|
16033
|
+
choices: choicesFor(settings, preview.playing),
|
|
16034
|
+
value: pending !== void 0 ? pending : currentValue(settings),
|
|
16035
|
+
onSelect,
|
|
16036
|
+
onReselect
|
|
16037
|
+
}
|
|
16038
|
+
),
|
|
16039
|
+
error ?? preview.error ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-settings-error", role: "alert", children: error ?? preview.error }) : null
|
|
16040
|
+
] }) : failed ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-empty", children: [
|
|
16041
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-empty-title", children: "No se pudieron cargar los tonos." }),
|
|
16042
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "cv-btn cv-settings-retry", onClick: onRetry, children: "Reintentar" })
|
|
16043
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-empty", role: "status", children: [
|
|
16044
|
+
/* @__PURE__ */ jsxRuntime.jsx(LoaderCircle, { className: "cv-empty-spinner", size: 18, "aria-hidden": true }),
|
|
16045
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-empty-title", children: "Cargando\u2026" })
|
|
16046
|
+
] })
|
|
16047
|
+
}
|
|
16048
|
+
);
|
|
16049
|
+
}
|
|
16050
|
+
function currentValue(settings) {
|
|
16051
|
+
return settings.mine && settings.catalogue.some((r) => r.id === settings.mine) ? settings.mine : ORG_DEFAULT;
|
|
16052
|
+
}
|
|
16053
|
+
function choicesFor(settings, playing) {
|
|
16054
|
+
const org = settings.orgDefault ? settings.catalogue.find((r) => r.id === settings.orgDefault) : void 0;
|
|
16055
|
+
const builtIn = settings.catalogue.find((r) => r.url === null);
|
|
16056
|
+
const mark = (value) => playing === value ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-choice-playing", children: [
|
|
16057
|
+
/* @__PURE__ */ jsxRuntime.jsx(Volume2, { size: 15, "aria-hidden": true }),
|
|
16058
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-sr-only", children: "Sonando" })
|
|
16059
|
+
] }) : void 0;
|
|
16060
|
+
const orgChoice = {
|
|
16061
|
+
value: ORG_DEFAULT,
|
|
16062
|
+
label: org ? "Predeterminado de la organizaci\xF3n" : "Predeterminado",
|
|
16063
|
+
sub: (org ?? builtIn)?.name ?? "",
|
|
16064
|
+
trailing: mark(ORG_DEFAULT)
|
|
16065
|
+
};
|
|
16066
|
+
return [orgChoice, ...settings.catalogue.map((r) => ({ value: r.id, label: r.name, trailing: mark(r.id) }))];
|
|
16067
|
+
}
|
|
16068
|
+
function useSerialSave(store, handlers) {
|
|
16069
|
+
const wanted = React.useRef(null);
|
|
16070
|
+
const busy = React.useRef(false);
|
|
16071
|
+
const on = React.useRef(handlers);
|
|
16072
|
+
on.current = handlers;
|
|
16073
|
+
const [save] = React.useState(() => (id) => {
|
|
16074
|
+
wanted.current = { id };
|
|
16075
|
+
if (busy.current) return;
|
|
16076
|
+
busy.current = true;
|
|
16077
|
+
void (async () => {
|
|
16078
|
+
while (wanted.current) {
|
|
16079
|
+
const { id: next } = wanted.current;
|
|
16080
|
+
wanted.current = null;
|
|
16081
|
+
try {
|
|
16082
|
+
await store.setMine(next);
|
|
16083
|
+
if (!wanted.current) on.current.onSettled();
|
|
16084
|
+
} catch {
|
|
16085
|
+
if (!wanted.current) on.current.onFailed();
|
|
16086
|
+
}
|
|
16087
|
+
}
|
|
16088
|
+
busy.current = false;
|
|
16089
|
+
})();
|
|
16090
|
+
});
|
|
16091
|
+
return save;
|
|
16092
|
+
}
|
|
15065
16093
|
function SettingsTab() {
|
|
15066
|
-
|
|
15067
|
-
|
|
15068
|
-
|
|
15069
|
-
|
|
16094
|
+
const softphone = useSoftphone();
|
|
16095
|
+
const devices = useDevices();
|
|
16096
|
+
const store = ringtoneStore(softphone);
|
|
16097
|
+
const ringtone = useRingtoneSettings(store);
|
|
16098
|
+
const [page, setPage] = React.useState(null);
|
|
16099
|
+
const list = React.useRef(null);
|
|
16100
|
+
const rows = React.useRef({});
|
|
16101
|
+
const returnTo = React.useRef(null);
|
|
16102
|
+
React.useEffect(() => {
|
|
16103
|
+
void softphone.audio.refresh();
|
|
16104
|
+
}, [softphone]);
|
|
16105
|
+
React.useEffect(() => {
|
|
16106
|
+
list.current?.toggleAttribute("inert", page !== null);
|
|
16107
|
+
if (page === null && returnTo.current) {
|
|
16108
|
+
rows.current[returnTo.current]?.focus();
|
|
16109
|
+
returnTo.current = null;
|
|
16110
|
+
}
|
|
16111
|
+
}, [page]);
|
|
16112
|
+
const open = (p) => () => {
|
|
16113
|
+
returnTo.current = p;
|
|
16114
|
+
setPage(p);
|
|
16115
|
+
};
|
|
16116
|
+
const back = () => setPage(null);
|
|
16117
|
+
const rowRef = (p) => (el) => {
|
|
16118
|
+
rows.current[p] = el;
|
|
16119
|
+
};
|
|
16120
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-view cv-settings", children: [
|
|
16121
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-settings-body", ref: list, children: [
|
|
16122
|
+
store ? /* @__PURE__ */ jsxRuntime.jsx(SettingsSection, { title: "Llamadas entrantes", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
16123
|
+
SettingsNavRow,
|
|
16124
|
+
{
|
|
16125
|
+
icon: Music,
|
|
16126
|
+
title: "Tono de llamada",
|
|
16127
|
+
value: ringtoneSummary(ringtone.settings, ringtone.failed),
|
|
16128
|
+
onOpen: open("tone"),
|
|
16129
|
+
rowRef: rowRef("tone")
|
|
16130
|
+
}
|
|
16131
|
+
) }) : null,
|
|
16132
|
+
/* @__PURE__ */ jsxRuntime.jsxs(SettingsSection, { title: "Dispositivos", children: [
|
|
16133
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
16134
|
+
SettingsNavRow,
|
|
16135
|
+
{
|
|
16136
|
+
icon: Mic,
|
|
16137
|
+
title: "Micr\xF3fono",
|
|
16138
|
+
value: deviceSummary("input", devices.inputs, devices.currentInputId),
|
|
16139
|
+
onOpen: open("input"),
|
|
16140
|
+
rowRef: rowRef("input")
|
|
16141
|
+
}
|
|
16142
|
+
),
|
|
16143
|
+
devices.canSelectOutput ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
16144
|
+
SettingsNavRow,
|
|
16145
|
+
{
|
|
16146
|
+
icon: Headphones,
|
|
16147
|
+
title: "Altavoces",
|
|
16148
|
+
value: deviceSummary("output", devices.outputs, devices.currentOutputId),
|
|
16149
|
+
onOpen: open("output"),
|
|
16150
|
+
rowRef: rowRef("output")
|
|
16151
|
+
}
|
|
16152
|
+
) : (
|
|
16153
|
+
// Safari and most of Firefox have no `setSinkId`: offering a page whose choice silently does
|
|
16154
|
+
// nothing is worse than saying where the choice really lives.
|
|
16155
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
16156
|
+
SettingsNavRow,
|
|
16157
|
+
{
|
|
16158
|
+
icon: Headphones,
|
|
16159
|
+
title: "Altavoces",
|
|
16160
|
+
value: "Tu navegador no permite elegirlos desde aqu\xED \u2014 c\xE1mbialos en los ajustes del sistema."
|
|
16161
|
+
}
|
|
16162
|
+
)
|
|
16163
|
+
),
|
|
16164
|
+
devices.canSelectRingtoneOutput ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
16165
|
+
SettingsNavRow,
|
|
16166
|
+
{
|
|
16167
|
+
icon: BellRing,
|
|
16168
|
+
title: "Salida del tono",
|
|
16169
|
+
value: deviceSummary("ringtone", devices.outputs, devices.currentRingtoneOutputId),
|
|
16170
|
+
onOpen: open("ringtone"),
|
|
16171
|
+
rowRef: rowRef("ringtone")
|
|
16172
|
+
}
|
|
16173
|
+
) : null
|
|
16174
|
+
] })
|
|
16175
|
+
] }),
|
|
16176
|
+
page === "tone" && store ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
16177
|
+
RingtonePage,
|
|
16178
|
+
{
|
|
16179
|
+
store,
|
|
16180
|
+
settings: ringtone.settings,
|
|
16181
|
+
failed: ringtone.failed,
|
|
16182
|
+
onRetry: ringtone.retry,
|
|
16183
|
+
onBack: back
|
|
16184
|
+
}
|
|
16185
|
+
) : page !== null && page !== "tone" ? /* @__PURE__ */ jsxRuntime.jsx(DevicePage, { kind: page, onBack: back }) : null
|
|
16186
|
+
] });
|
|
15070
16187
|
}
|
|
15071
16188
|
|
|
15072
16189
|
// src/core/env.ts
|
|
@@ -15289,203 +16406,6 @@ function useCallOutcome() {
|
|
|
15289
16406
|
clear
|
|
15290
16407
|
};
|
|
15291
16408
|
}
|
|
15292
|
-
|
|
15293
|
-
// src/ui/audio/audio.ts
|
|
15294
|
-
var TONE_HZ = 425;
|
|
15295
|
-
function createRemoteAudio() {
|
|
15296
|
-
const el = document.createElement("audio");
|
|
15297
|
-
el.autoplay = true;
|
|
15298
|
-
el.style.display = "none";
|
|
15299
|
-
document.body.appendChild(el);
|
|
15300
|
-
return {
|
|
15301
|
-
attach(stream) {
|
|
15302
|
-
el.srcObject = stream;
|
|
15303
|
-
void el.play().catch(() => {
|
|
15304
|
-
});
|
|
15305
|
-
},
|
|
15306
|
-
detach() {
|
|
15307
|
-
el.srcObject = null;
|
|
15308
|
-
},
|
|
15309
|
-
destroy() {
|
|
15310
|
-
el.srcObject = null;
|
|
15311
|
-
el.remove();
|
|
15312
|
-
},
|
|
15313
|
-
async setOutputDevice(deviceId) {
|
|
15314
|
-
if (typeof el.setSinkId !== "function") return;
|
|
15315
|
-
try {
|
|
15316
|
-
await el.setSinkId(deviceId ?? "");
|
|
15317
|
-
} catch {
|
|
15318
|
-
}
|
|
15319
|
-
}
|
|
15320
|
-
};
|
|
15321
|
-
}
|
|
15322
|
-
var shared = null;
|
|
15323
|
-
function sharedContext() {
|
|
15324
|
-
if (shared) return shared;
|
|
15325
|
-
const Ctor = typeof AudioContext !== "undefined" ? AudioContext : globalThis.webkitAudioContext;
|
|
15326
|
-
if (!Ctor) return null;
|
|
15327
|
-
shared = new Ctor();
|
|
15328
|
-
return shared;
|
|
15329
|
-
}
|
|
15330
|
-
function primeAudio() {
|
|
15331
|
-
const ctx = sharedContext();
|
|
15332
|
-
if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
|
|
15333
|
-
});
|
|
15334
|
-
}
|
|
15335
|
-
function playRingtone() {
|
|
15336
|
-
const ctx = sharedContext();
|
|
15337
|
-
if (!ctx) return { stop() {
|
|
15338
|
-
} };
|
|
15339
|
-
const gain = ctx.createGain();
|
|
15340
|
-
gain.gain.value = 0;
|
|
15341
|
-
gain.connect(ctx.destination);
|
|
15342
|
-
const osc = ctx.createOscillator();
|
|
15343
|
-
osc.type = "sine";
|
|
15344
|
-
osc.frequency.value = TONE_HZ;
|
|
15345
|
-
osc.connect(gain);
|
|
15346
|
-
osc.start();
|
|
15347
|
-
const LEVEL = 0.8;
|
|
15348
|
-
const RAMP = 0.02;
|
|
15349
|
-
const burst = (at, dur) => {
|
|
15350
|
-
gain.gain.setValueAtTime(0, at);
|
|
15351
|
-
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
15352
|
-
gain.gain.setValueAtTime(LEVEL, at + dur - RAMP);
|
|
15353
|
-
gain.gain.linearRampToValueAtTime(0, at + dur);
|
|
15354
|
-
};
|
|
15355
|
-
const scheduleCycle = (at) => {
|
|
15356
|
-
burst(at, 0.4);
|
|
15357
|
-
burst(at + 0.6, 0.4);
|
|
15358
|
-
return 3;
|
|
15359
|
-
};
|
|
15360
|
-
const LOOKAHEAD = 0.5;
|
|
15361
|
-
const TICK_MS = 150;
|
|
15362
|
-
const LEAD = 0.08;
|
|
15363
|
-
let stopped = false;
|
|
15364
|
-
let timer = null;
|
|
15365
|
-
let nextAt = 0;
|
|
15366
|
-
const pump = () => {
|
|
15367
|
-
while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleCycle(nextAt);
|
|
15368
|
-
};
|
|
15369
|
-
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
15370
|
-
}).finally(() => {
|
|
15371
|
-
if (stopped) return;
|
|
15372
|
-
nextAt = ctx.currentTime + LEAD;
|
|
15373
|
-
pump();
|
|
15374
|
-
timer = setInterval(pump, TICK_MS);
|
|
15375
|
-
});
|
|
15376
|
-
return {
|
|
15377
|
-
stop() {
|
|
15378
|
-
stopped = true;
|
|
15379
|
-
if (timer) clearInterval(timer);
|
|
15380
|
-
try {
|
|
15381
|
-
osc.stop();
|
|
15382
|
-
} catch {
|
|
15383
|
-
}
|
|
15384
|
-
try {
|
|
15385
|
-
gain.disconnect();
|
|
15386
|
-
} catch {
|
|
15387
|
-
}
|
|
15388
|
-
}
|
|
15389
|
-
};
|
|
15390
|
-
}
|
|
15391
|
-
function playCallEndedTone() {
|
|
15392
|
-
const ctx = sharedContext();
|
|
15393
|
-
if (!ctx) return;
|
|
15394
|
-
const gain = ctx.createGain();
|
|
15395
|
-
gain.gain.value = 0;
|
|
15396
|
-
gain.connect(ctx.destination);
|
|
15397
|
-
const osc = ctx.createOscillator();
|
|
15398
|
-
osc.type = "sine";
|
|
15399
|
-
osc.frequency.value = TONE_HZ;
|
|
15400
|
-
osc.connect(gain);
|
|
15401
|
-
osc.start();
|
|
15402
|
-
const LEVEL = 0.55;
|
|
15403
|
-
const RAMP = 8e-3;
|
|
15404
|
-
const BEEP = 0.25;
|
|
15405
|
-
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
15406
|
-
}).finally(() => {
|
|
15407
|
-
const at = ctx.currentTime + 0.02;
|
|
15408
|
-
gain.gain.setValueAtTime(0, at);
|
|
15409
|
-
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
15410
|
-
gain.gain.setValueAtTime(LEVEL, at + BEEP - RAMP);
|
|
15411
|
-
gain.gain.linearRampToValueAtTime(0, at + BEEP);
|
|
15412
|
-
const totalMs = (at + BEEP - ctx.currentTime) * 1e3;
|
|
15413
|
-
setTimeout(() => {
|
|
15414
|
-
try {
|
|
15415
|
-
osc.stop();
|
|
15416
|
-
} catch {
|
|
15417
|
-
}
|
|
15418
|
-
try {
|
|
15419
|
-
gain.disconnect();
|
|
15420
|
-
} catch {
|
|
15421
|
-
}
|
|
15422
|
-
}, totalMs);
|
|
15423
|
-
});
|
|
15424
|
-
}
|
|
15425
|
-
var ringbackUrl = null;
|
|
15426
|
-
function ringbackSource() {
|
|
15427
|
-
if (ringbackUrl) return ringbackUrl;
|
|
15428
|
-
const sampleRate = 48e3;
|
|
15429
|
-
const channels = 2;
|
|
15430
|
-
const bytesPerSample = 2;
|
|
15431
|
-
const amp = 0.5;
|
|
15432
|
-
const toneSec = 1.5;
|
|
15433
|
-
const silenceSec = 3;
|
|
15434
|
-
const totalFrames = Math.round((toneSec + silenceSec) * sampleRate);
|
|
15435
|
-
const toneFrames = Math.round(toneSec * sampleRate);
|
|
15436
|
-
const ramp = Math.round(0.02 * sampleRate);
|
|
15437
|
-
const blockAlign = channels * bytesPerSample;
|
|
15438
|
-
const dataSize = totalFrames * blockAlign;
|
|
15439
|
-
const buffer = new ArrayBuffer(44 + dataSize);
|
|
15440
|
-
const view = new DataView(buffer);
|
|
15441
|
-
const str = (off, s) => {
|
|
15442
|
-
for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
|
|
15443
|
-
};
|
|
15444
|
-
str(0, "RIFF");
|
|
15445
|
-
view.setUint32(4, 36 + dataSize, true);
|
|
15446
|
-
str(8, "WAVE");
|
|
15447
|
-
str(12, "fmt ");
|
|
15448
|
-
view.setUint32(16, 16, true);
|
|
15449
|
-
view.setUint16(20, 1, true);
|
|
15450
|
-
view.setUint16(22, channels, true);
|
|
15451
|
-
view.setUint32(24, sampleRate, true);
|
|
15452
|
-
view.setUint32(28, sampleRate * blockAlign, true);
|
|
15453
|
-
view.setUint16(32, blockAlign, true);
|
|
15454
|
-
view.setUint16(34, bytesPerSample * 8, true);
|
|
15455
|
-
str(36, "data");
|
|
15456
|
-
view.setUint32(40, dataSize, true);
|
|
15457
|
-
for (let i = 0; i < totalFrames; i++) {
|
|
15458
|
-
let sample = 0;
|
|
15459
|
-
if (i < toneFrames) {
|
|
15460
|
-
let env = 1;
|
|
15461
|
-
if (i < ramp) env = i / ramp;
|
|
15462
|
-
else if (i > toneFrames - ramp) env = Math.max(0, (toneFrames - i) / ramp);
|
|
15463
|
-
sample = Math.sin(2 * Math.PI * TONE_HZ * (i / sampleRate)) * amp * env;
|
|
15464
|
-
}
|
|
15465
|
-
const s16 = Math.round(sample * 32767);
|
|
15466
|
-
const off = 44 + i * blockAlign;
|
|
15467
|
-
for (let c = 0; c < channels; c++) view.setInt16(off + c * bytesPerSample, s16, true);
|
|
15468
|
-
}
|
|
15469
|
-
ringbackUrl = URL.createObjectURL(new Blob([buffer], { type: "audio/wav" }));
|
|
15470
|
-
return ringbackUrl;
|
|
15471
|
-
}
|
|
15472
|
-
function playRingback() {
|
|
15473
|
-
const el = document.createElement("audio");
|
|
15474
|
-
el.src = ringbackSource();
|
|
15475
|
-
el.loop = true;
|
|
15476
|
-
el.style.display = "none";
|
|
15477
|
-
document.body.appendChild(el);
|
|
15478
|
-
void el.play().catch(() => {
|
|
15479
|
-
});
|
|
15480
|
-
return {
|
|
15481
|
-
stop() {
|
|
15482
|
-
el.pause();
|
|
15483
|
-
el.remove();
|
|
15484
|
-
}
|
|
15485
|
-
};
|
|
15486
|
-
}
|
|
15487
|
-
|
|
15488
|
-
// src/ui/audio/useCallAudio.ts
|
|
15489
16409
|
function useCallAudio(enabled) {
|
|
15490
16410
|
const softphone = useSoftphone();
|
|
15491
16411
|
const remote = React.useRef(null);
|
|
@@ -15502,14 +16422,29 @@ function useCallAudio(enabled) {
|
|
|
15502
16422
|
const offDeviceChange = softphone.on("deviceChange" /* DeviceChange */, () => {
|
|
15503
16423
|
void remote.current?.setOutputDevice(softphone.audio.currentOutputId);
|
|
15504
16424
|
});
|
|
16425
|
+
const ringtones = ringtoneStore(softphone);
|
|
16426
|
+
ringtones?.wantAudio(true);
|
|
16427
|
+
const refreshRingtone = () => {
|
|
16428
|
+
if (softphone.isLeader) void ringtones?.refresh();
|
|
16429
|
+
};
|
|
16430
|
+
refreshRingtone();
|
|
16431
|
+
const offLeader = softphone.on("leaderChanged" /* LeaderChanged */, ({ isLeader }) => {
|
|
16432
|
+
if (isLeader) refreshRingtone();
|
|
16433
|
+
});
|
|
15505
16434
|
const stopTone = () => {
|
|
15506
16435
|
tone.current?.stop();
|
|
15507
16436
|
tone.current = null;
|
|
15508
16437
|
};
|
|
15509
16438
|
const setTone = (next) => {
|
|
15510
16439
|
stopTone();
|
|
15511
|
-
if (next === "ringtone")
|
|
15512
|
-
|
|
16440
|
+
if (next === "ringtone") {
|
|
16441
|
+
const audio = softphone.audio;
|
|
16442
|
+
tone.current = playRingtone({
|
|
16443
|
+
buffer: ringtones?.readyBuffer() ?? null,
|
|
16444
|
+
loop: ringtones?.readyLoop() ?? null,
|
|
16445
|
+
sinkId: audio.canSelectRingtoneOutput ? audio.currentRingtoneOutputId : null
|
|
16446
|
+
});
|
|
16447
|
+
} else if (next === "ringback") tone.current = playRingback();
|
|
15513
16448
|
};
|
|
15514
16449
|
const perCall = [];
|
|
15515
16450
|
const followedCalls = /* @__PURE__ */ new Set();
|
|
@@ -15540,6 +16475,7 @@ function useCallAudio(enabled) {
|
|
|
15540
16475
|
stopTone();
|
|
15541
16476
|
remote.current?.detach();
|
|
15542
16477
|
if (wasConnected && (reason === "local" || reason === "remote")) playCallEndedTone();
|
|
16478
|
+
refreshRingtone();
|
|
15543
16479
|
})
|
|
15544
16480
|
);
|
|
15545
16481
|
};
|
|
@@ -15557,6 +16493,8 @@ function useCallAudio(enabled) {
|
|
|
15557
16493
|
offIncoming();
|
|
15558
16494
|
offActive();
|
|
15559
16495
|
offDeviceChange();
|
|
16496
|
+
offLeader();
|
|
16497
|
+
ringtones?.wantAudio(false);
|
|
15560
16498
|
for (const off of perCall) off();
|
|
15561
16499
|
stopTone();
|
|
15562
16500
|
remote.current?.destroy();
|
|
@@ -15703,7 +16641,7 @@ function SoftphoneWidget(props) {
|
|
|
15703
16641
|
...theme === "system" ? {} : { "data-cv-theme": theme },
|
|
15704
16642
|
children: [
|
|
15705
16643
|
screenIsFree ? /* @__PURE__ */ jsxRuntime.jsx(TransferNotices, { notices: transferNotices, dismiss: dismissTransferNotice }) : null,
|
|
15706
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-body", children: isFollower ? /* @__PURE__ */ jsxRuntime.jsx(LeaderGate, {}) : blocked ? /* @__PURE__ */ jsxRuntime.jsx(MicPrompt, { state: mic.state, onRequest: () => void mic.request() }) : call ? /* @__PURE__ */ jsxRuntime.jsx(CallScreen, { call, ...incomingCallAction ? { incomingCallAction } : {} }) : fatal ? /* @__PURE__ */ jsxRuntime.jsx(ErrorGate, { error: fatal, onRetry: retry }) : tab === "contacts" && contactsSource ? /* @__PURE__ */ jsxRuntime.jsx(ContactsTab, { source: contactsSource, ...contactAction ? { contactAction } : {} }) : tab === "history" && historySource ? /* @__PURE__ */ jsxRuntime.jsx(HistoryTab, { source: historySource }) : tab === "settings" ? /* @__PURE__ */ jsxRuntime.jsx(SettingsTab, {}) : /* @__PURE__ */ jsxRuntime.jsx(Dialpad, { outcome, onIntent: clearOutcome }) }),
|
|
16644
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-body", children: isFollower ? /* @__PURE__ */ jsxRuntime.jsx(LeaderGate, {}) : blocked ? /* @__PURE__ */ jsxRuntime.jsx(MicPrompt, { state: mic.state, onRequest: () => void mic.request() }) : call ? /* @__PURE__ */ jsxRuntime.jsx(CallScreen, { call, keyTones: sounds, ...incomingCallAction ? { incomingCallAction } : {} }) : fatal ? /* @__PURE__ */ jsxRuntime.jsx(ErrorGate, { error: fatal, onRetry: retry }) : tab === "contacts" && contactsSource ? /* @__PURE__ */ jsxRuntime.jsx(ContactsTab, { source: contactsSource, ...contactAction ? { contactAction } : {} }) : tab === "history" && historySource ? /* @__PURE__ */ jsxRuntime.jsx(HistoryTab, { source: historySource }) : tab === "settings" ? /* @__PURE__ */ jsxRuntime.jsx(SettingsTab, {}) : /* @__PURE__ */ jsxRuntime.jsx(Dialpad, { outcome, onIntent: clearOutcome, keyTones: sounds }) }),
|
|
15707
16645
|
screenIsFree ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-tabs", role: "tablist", children: tabs.map(({ id, icon: Icon3, label }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
15708
16646
|
"button",
|
|
15709
16647
|
{
|
|
@@ -15731,6 +16669,7 @@ lucide-react/dist/esm/createLucideIcon.js:
|
|
|
15731
16669
|
lucide-react/dist/esm/icons/arrow-left-right.js:
|
|
15732
16670
|
lucide-react/dist/esm/icons/arrow-left.js:
|
|
15733
16671
|
lucide-react/dist/esm/icons/arrow-right.js:
|
|
16672
|
+
lucide-react/dist/esm/icons/bell-ring.js:
|
|
15734
16673
|
lucide-react/dist/esm/icons/calendar-clock.js:
|
|
15735
16674
|
lucide-react/dist/esm/icons/check.js:
|
|
15736
16675
|
lucide-react/dist/esm/icons/chevron-down.js:
|
|
@@ -15746,6 +16685,7 @@ lucide-react/dist/esm/icons/loader-circle.js:
|
|
|
15746
16685
|
lucide-react/dist/esm/icons/mic-off.js:
|
|
15747
16686
|
lucide-react/dist/esm/icons/mic.js:
|
|
15748
16687
|
lucide-react/dist/esm/icons/monitor-smartphone.js:
|
|
16688
|
+
lucide-react/dist/esm/icons/music.js:
|
|
15749
16689
|
lucide-react/dist/esm/icons/pause.js:
|
|
15750
16690
|
lucide-react/dist/esm/icons/phone-incoming.js:
|
|
15751
16691
|
lucide-react/dist/esm/icons/phone-missed.js:
|
|
@@ -15759,6 +16699,7 @@ lucide-react/dist/esm/icons/timer.js:
|
|
|
15759
16699
|
lucide-react/dist/esm/icons/triangle-alert.js:
|
|
15760
16700
|
lucide-react/dist/esm/icons/user.js:
|
|
15761
16701
|
lucide-react/dist/esm/icons/users.js:
|
|
16702
|
+
lucide-react/dist/esm/icons/volume-2.js:
|
|
15762
16703
|
lucide-react/dist/esm/icons/x.js:
|
|
15763
16704
|
lucide-react/dist/esm/lucide-react.js:
|
|
15764
16705
|
(**
|
|
@@ -15775,5 +16716,6 @@ exports.Dialpad = Dialpad;
|
|
|
15775
16716
|
exports.HistoryTab = HistoryTab;
|
|
15776
16717
|
exports.MicPermissionGate = MicPermissionGate;
|
|
15777
16718
|
exports.SoftphoneWidget = SoftphoneWidget;
|
|
16719
|
+
exports.previewRingtone = previewRingtone;
|
|
15778
16720
|
//# sourceMappingURL=index.cjs.map
|
|
15779
16721
|
//# sourceMappingURL=index.cjs.map
|