@coventia/softphone 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/{call-VKq1I8yP.d.cts → call-CaPDWVk3.d.cts} +44 -1
  2. package/dist/{call-VKq1I8yP.d.ts → call-CaPDWVk3.d.ts} +44 -1
  3. package/dist/{chunk-LMJXX4YM.js → chunk-UHNLEBBO.js} +4 -2
  4. package/dist/chunk-UHNLEBBO.js.map +1 -0
  5. package/dist/element/index.d.ts +68 -9
  6. package/dist/element/index.js +962 -114
  7. package/dist/element/index.js.map +1 -1
  8. package/dist/{engine-C0BOkIcW.d.ts → engine-BgBsIcEa.d.ts} +1 -1
  9. package/dist/{engine-BlQBo2Ys.d.cts → engine-CU2rEo3g.d.cts} +1 -1
  10. package/dist/index.cjs +120 -10
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +5 -5
  13. package/dist/index.d.ts +5 -5
  14. package/dist/index.js +69 -10
  15. package/dist/index.js.map +1 -1
  16. package/dist/react/index.cjs +2 -0
  17. package/dist/react/index.cjs.map +1 -1
  18. package/dist/react/index.d.cts +2 -2
  19. package/dist/react/index.d.ts +2 -2
  20. package/dist/react/index.js +1 -1
  21. package/dist/{softphone-Cyq3hiIM.d.ts → softphone-DEt0TtQw.d.ts} +26 -10
  22. package/dist/{softphone-DlGV12Xp.d.cts → softphone-dZMkW8V1.d.cts} +26 -10
  23. package/dist/softphone.css +110 -12
  24. package/dist/{sources-IRKNQPWW.js → sources-M3GYMS6V.js} +54 -3
  25. package/dist/sources-M3GYMS6V.js.map +1 -0
  26. package/dist/testing/index.d.cts +2 -2
  27. package/dist/testing/index.d.ts +2 -2
  28. package/dist/ui/index.cjs +885 -28
  29. package/dist/ui/index.cjs.map +1 -1
  30. package/dist/ui/index.d.cts +25 -3
  31. package/dist/ui/index.d.ts +25 -3
  32. package/dist/ui/index.js +885 -31
  33. package/dist/ui/index.js.map +1 -1
  34. package/dist/zadarma/index.d.cts +2 -2
  35. package/dist/zadarma/index.d.ts +2 -2
  36. package/package.json +1 -1
  37. package/dist/chunk-LMJXX4YM.js.map +0 -1
  38. package/dist/sources-IRKNQPWW.js.map +0 -1
package/dist/ui/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { isWebRtcSupported } from '../chunk-KAXEMJ7E.js';
2
- import { useSoftphone, useSoftphoneState, useCall, usePresence, useActiveCall, useIsLeader, useLastError, useDevices } from '../chunk-LMJXX4YM.js';
2
+ import { useSoftphone, useSoftphoneState, useCall, usePresence, useActiveCall, useIsLeader, useLastError, useDevices } from '../chunk-UHNLEBBO.js';
3
3
  import '../chunk-7R6LOOAI.js';
4
- import { forwardRef, createElement, useState, useRef, useLayoutEffect, useEffect, useCallback, useMemo } from 'react';
4
+ import { forwardRef, createElement, useState, useRef, useLayoutEffect, useEffect, useCallback, useMemo, useSyncExternalStore, useId } from 'react';
5
5
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
6
 
7
7
  // ../../node_modules/lucide-react/dist/esm/shared/src/utils.js
@@ -89,6 +89,14 @@ var ArrowRight = createLucideIcon("ArrowRight", [
89
89
  ["path", { d: "m12 5 7 7-7 7", key: "xquz4c" }]
90
90
  ]);
91
91
 
92
+ // ../../node_modules/lucide-react/dist/esm/icons/bell-ring.js
93
+ var BellRing = createLucideIcon("BellRing", [
94
+ ["path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9", key: "1qo2s2" }],
95
+ ["path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0", key: "qgo35s" }],
96
+ ["path", { d: "M4 2C2.8 3.7 2 5.7 2 8", key: "tap9e0" }],
97
+ ["path", { d: "M22 8c0-2.3-.8-4.3-2-6", key: "5bb3ad" }]
98
+ ]);
99
+
92
100
  // ../../node_modules/lucide-react/dist/esm/icons/calendar-clock.js
93
101
  var CalendarClock = createLucideIcon("CalendarClock", [
94
102
  ["path", { d: "M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5", key: "1osxxc" }],
@@ -203,6 +211,13 @@ var MonitorSmartphone = createLucideIcon("MonitorSmartphone", [
203
211
  ["rect", { width: "6", height: "10", x: "16", y: "12", rx: "2", key: "1egngj" }]
204
212
  ]);
205
213
 
214
+ // ../../node_modules/lucide-react/dist/esm/icons/music.js
215
+ var Music = createLucideIcon("Music", [
216
+ ["path", { d: "M9 18V5l12-2v13", key: "1jmyc2" }],
217
+ ["circle", { cx: "6", cy: "18", r: "3", key: "fqmcym" }],
218
+ ["circle", { cx: "18", cy: "16", r: "3", key: "1hluhg" }]
219
+ ]);
220
+
206
221
  // ../../node_modules/lucide-react/dist/esm/icons/pause.js
207
222
  var Pause = createLucideIcon("Pause", [
208
223
  ["rect", { x: "14", y: "4", width: "4", height: "16", rx: "1", key: "zuxfzm" }],
@@ -328,6 +343,19 @@ var Users = createLucideIcon("Users", [
328
343
  ["path", { d: "M16 3.13a4 4 0 0 1 0 7.75", key: "1da9ce" }]
329
344
  ]);
330
345
 
346
+ // ../../node_modules/lucide-react/dist/esm/icons/volume-2.js
347
+ var Volume2 = createLucideIcon("Volume2", [
348
+ [
349
+ "path",
350
+ {
351
+ d: "M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z",
352
+ key: "uqj9uw"
353
+ }
354
+ ],
355
+ ["path", { d: "M16 9a5 5 0 0 1 0 6", key: "1q6k2b" }],
356
+ ["path", { d: "M19.364 18.364a9 9 0 0 0 0-12.728", key: "ijwkga" }]
357
+ ]);
358
+
331
359
  // ../../node_modules/lucide-react/dist/esm/icons/x.js
332
360
  var X = createLucideIcon("X", [
333
361
  ["path", { d: "M18 6 6 18", key: "1bl5f8" }],
@@ -567,6 +595,59 @@ var KEYPAD_KEYS = [
567
595
  ["#", ""]
568
596
  ];
569
597
 
598
+ // src/ui/audio/classic-ring.ts
599
+ var C6 = 1046.5;
600
+ var E6 = 1318.51;
601
+ var HARMONICITY = 4;
602
+ var INDEX = 1.8;
603
+ var CLASSIC_CYCLE_SECONDS = 3;
604
+ var CLASSIC_GAIN = 0.3841;
605
+ function classicRingGraph(ctx, dest) {
606
+ const output = ctx.createGain();
607
+ output.gain.value = CLASSIC_GAIN;
608
+ const chain = ["highpass", "highpass", "lowpass", "lowpass"].map((type) => {
609
+ const f = ctx.createBiquadFilter();
610
+ f.type = type;
611
+ f.frequency.value = type === "highpass" ? 250 : 7500;
612
+ f.Q.value = 0.707;
613
+ return f;
614
+ });
615
+ for (let i = 0; i < chain.length - 1; i++) chain[i].connect(chain[i + 1]);
616
+ chain[chain.length - 1].connect(output);
617
+ output.connect(dest);
618
+ return { input: chain[0], output };
619
+ }
620
+ function strike(ctx, input, t, freq, vel, track) {
621
+ const carrier = ctx.createOscillator();
622
+ carrier.frequency.value = freq;
623
+ const modulator = ctx.createOscillator();
624
+ modulator.frequency.value = freq * HARMONICITY;
625
+ const depth = ctx.createGain();
626
+ depth.gain.setValueAtTime(0, t);
627
+ depth.gain.linearRampToValueAtTime(INDEX * freq, t + 4e-3);
628
+ depth.gain.setTargetAtTime(0, t + 4e-3, 0.02);
629
+ modulator.connect(depth);
630
+ depth.connect(carrier.frequency);
631
+ const amp = ctx.createGain();
632
+ amp.gain.setValueAtTime(0, t);
633
+ amp.gain.linearRampToValueAtTime(vel, t + 5e-3);
634
+ amp.gain.setTargetAtTime(0, t + 5e-3, 0.15);
635
+ amp.gain.setTargetAtTime(0, t + 0.045, 0.08);
636
+ carrier.connect(amp);
637
+ amp.connect(input);
638
+ for (const o of [carrier, modulator]) {
639
+ o.start(t);
640
+ o.stop(t + 0.7);
641
+ track(o);
642
+ }
643
+ }
644
+ function scheduleClassicCycle(ctx, input, t0, track) {
645
+ for (const ring of [0, 0.6]) {
646
+ for (let i = 0; i < 8; i++) strike(ctx, input, t0 + ring + i * 0.05, i % 2 ? E6 : C6, 0.55 + 0.1 * (i % 2), track);
647
+ }
648
+ return CLASSIC_CYCLE_SECONDS;
649
+ }
650
+
570
651
  // src/ui/audio/audio.ts
571
652
  var TONE_HZ = 425;
572
653
  function createRemoteAudio() {
@@ -609,30 +690,159 @@ function primeAudio() {
609
690
  if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
610
691
  });
611
692
  }
612
- function playRingtone() {
693
+ function playRingtone(options = {}) {
613
694
  const ctx = sharedContext();
614
695
  if (!ctx) return { stop() {
615
696
  } };
697
+ const bus = ctx.createGain();
698
+ const route = routeToDevice(ctx, bus, options.sinkId ?? null);
699
+ const voice = options.buffer ? fileVoice(ctx, bus, options.buffer, options.loop ?? null) : synthesisedVoice(ctx, bus);
700
+ return {
701
+ stop() {
702
+ voice.stop();
703
+ route.stop();
704
+ }
705
+ };
706
+ }
707
+ function fileVoice(ctx, bus, buffer, loop) {
708
+ const FILE_LEVEL = 1;
709
+ const RAMP = 0.02;
710
+ const LEAD = 0.08;
616
711
  const gain = ctx.createGain();
617
712
  gain.gain.value = 0;
618
- gain.connect(ctx.destination);
619
- const osc = ctx.createOscillator();
620
- osc.type = "sine";
621
- osc.frequency.value = TONE_HZ;
622
- osc.connect(gain);
623
- osc.start();
624
- const LEVEL = 0.8;
625
- const RAMP = 0.02;
626
- const burst = (at, dur) => {
713
+ gain.connect(bus);
714
+ const src = ctx.createBufferSource();
715
+ src.buffer = buffer;
716
+ src.loop = true;
717
+ const points = loop && loop.start >= 0 && loop.end > loop.start && loop.end <= buffer.duration + 1e-3 ? loop : null;
718
+ if (points) {
719
+ src.loopStart = points.start;
720
+ src.loopEnd = points.end;
721
+ }
722
+ src.connect(gain);
723
+ let stopped = false;
724
+ void Promise.resolve(ctx.resume?.()).catch(() => {
725
+ }).finally(() => {
726
+ if (stopped) return;
727
+ const at = ctx.currentTime + LEAD;
627
728
  gain.gain.setValueAtTime(0, at);
628
- gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
629
- gain.gain.setValueAtTime(LEVEL, at + dur - RAMP);
630
- gain.gain.linearRampToValueAtTime(0, at + dur);
729
+ gain.gain.linearRampToValueAtTime(FILE_LEVEL, at + RAMP);
730
+ src.start(at, points?.start ?? 0);
731
+ });
732
+ return {
733
+ stop() {
734
+ stopped = true;
735
+ try {
736
+ src.stop();
737
+ } catch {
738
+ }
739
+ try {
740
+ gain.disconnect();
741
+ } catch {
742
+ }
743
+ }
631
744
  };
632
- const scheduleCycle = (at) => {
633
- burst(at, 0.4);
634
- burst(at + 0.6, 0.4);
635
- return 3;
745
+ }
746
+ function routeToDevice(ctx, bus, sinkId) {
747
+ const toDefault = () => {
748
+ bus.connect(ctx.destination);
749
+ };
750
+ const el = sinkId && typeof document !== "undefined" ? document.createElement("audio") : null;
751
+ const dest = el && typeof ctx.createMediaStreamDestination === "function" ? ctx.createMediaStreamDestination() : null;
752
+ const canRoute = el && dest && typeof el.setSinkId === "function";
753
+ if (!canRoute || !sinkId) {
754
+ toDefault();
755
+ return { stop: () => safeDisconnect(bus) };
756
+ }
757
+ let stopped = false;
758
+ el.style.display = "none";
759
+ el.srcObject = dest.stream;
760
+ document.body.appendChild(el);
761
+ bus.connect(dest);
762
+ void el.setSinkId(sinkId).then(() => el.play()).catch((error) => {
763
+ if (stopped) return;
764
+ console.warn("[softphone] sound could not be sent to the chosen device; using the default output", error);
765
+ safeDisconnect(bus);
766
+ teardown(el);
767
+ toDefault();
768
+ });
769
+ return {
770
+ stop() {
771
+ stopped = true;
772
+ safeDisconnect(bus);
773
+ teardown(el);
774
+ }
775
+ };
776
+ }
777
+ function safeDisconnect(node) {
778
+ try {
779
+ node.disconnect();
780
+ } catch {
781
+ }
782
+ }
783
+ function teardown(el) {
784
+ el.pause();
785
+ el.srcObject = null;
786
+ el.remove();
787
+ }
788
+ var TEST_TONE_MS = 900;
789
+ function playTestTone(sinkId) {
790
+ const ctx = sharedContext();
791
+ if (!ctx) return { stop() {
792
+ } };
793
+ const bus = ctx.createGain();
794
+ const route = routeToDevice(ctx, bus, sinkId);
795
+ const LEVEL = 0.5;
796
+ const NOTES = [
797
+ { hz: 660, at: 0.05 },
798
+ { hz: 880, at: 0.4 }
799
+ ];
800
+ const oscs = [];
801
+ let stopped = false;
802
+ let timer = null;
803
+ const stop = () => {
804
+ if (stopped) return;
805
+ stopped = true;
806
+ if (timer) clearTimeout(timer);
807
+ for (const o of oscs) {
808
+ try {
809
+ o.stop();
810
+ } catch {
811
+ }
812
+ }
813
+ route.stop();
814
+ };
815
+ void Promise.resolve(ctx.resume?.()).catch(() => {
816
+ }).finally(() => {
817
+ if (stopped) return;
818
+ const t0 = ctx.currentTime;
819
+ for (const { hz, at } of NOTES) {
820
+ const gain = ctx.createGain();
821
+ gain.gain.value = 0;
822
+ gain.connect(bus);
823
+ const osc = ctx.createOscillator();
824
+ osc.type = "sine";
825
+ osc.frequency.value = hz;
826
+ osc.connect(gain);
827
+ const start = t0 + at;
828
+ gain.gain.setValueAtTime(0, start);
829
+ gain.gain.linearRampToValueAtTime(LEVEL, start + 0.015);
830
+ gain.gain.setValueAtTime(LEVEL, start + 0.25);
831
+ gain.gain.linearRampToValueAtTime(0, start + 0.32);
832
+ osc.start(start);
833
+ osc.stop(start + 0.34);
834
+ oscs.push(osc);
835
+ }
836
+ timer = setTimeout(stop, TEST_TONE_MS);
837
+ });
838
+ return { stop };
839
+ }
840
+ function synthesisedVoice(ctx, bus) {
841
+ const { input, output } = classicRingGraph(ctx, bus);
842
+ const scheduled = /* @__PURE__ */ new Set();
843
+ const track = (o) => {
844
+ scheduled.add(o);
845
+ o.onended = () => scheduled.delete(o);
636
846
  };
637
847
  const LOOKAHEAD = 0.5;
638
848
  const TICK_MS = 150;
@@ -641,7 +851,7 @@ function playRingtone() {
641
851
  let timer = null;
642
852
  let nextAt = 0;
643
853
  const pump = () => {
644
- while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleCycle(nextAt);
854
+ while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleClassicCycle(ctx, input, nextAt, track);
645
855
  };
646
856
  void Promise.resolve(ctx.resume?.()).catch(() => {
647
857
  }).finally(() => {
@@ -654,12 +864,15 @@ function playRingtone() {
654
864
  stop() {
655
865
  stopped = true;
656
866
  if (timer) clearInterval(timer);
657
- try {
658
- osc.stop();
659
- } catch {
867
+ for (const o of scheduled) {
868
+ try {
869
+ o.stop();
870
+ } catch {
871
+ }
660
872
  }
873
+ scheduled.clear();
661
874
  try {
662
- gain.disconnect();
875
+ output.disconnect();
663
876
  } catch {
664
877
  }
665
878
  }
@@ -2164,6 +2377,110 @@ function HistoryTab({ source }) {
2164
2377
  selected ? /* @__PURE__ */ jsx(HistoryDetail, { entry: selected, onBack: () => setSelected(null) }) : null
2165
2378
  ] });
2166
2379
  }
2380
+
2381
+ // src/ui/audio/ringtone-store.ts
2382
+ var stores = /* @__PURE__ */ new WeakMap();
2383
+ function ringtoneStore(softphone) {
2384
+ const source = softphone.sources?.ringtones;
2385
+ if (!source) return null;
2386
+ let store = stores.get(softphone);
2387
+ if (!store) {
2388
+ store = createRingtoneStore(source);
2389
+ stores.set(softphone, store);
2390
+ }
2391
+ return store;
2392
+ }
2393
+ function createRingtoneStore(source, decodeImpl = decodeRingtone) {
2394
+ let settings = null;
2395
+ let audioWanted = false;
2396
+ let loaded = null;
2397
+ let loading2 = null;
2398
+ let inFlight = null;
2399
+ const listeners = /* @__PURE__ */ new Set();
2400
+ const notify = () => {
2401
+ for (const l of listeners) l();
2402
+ };
2403
+ const effectiveUrl = () => {
2404
+ if (!settings) return null;
2405
+ return settings.catalogue.find((r) => r.id === settings.effective)?.url ?? null;
2406
+ };
2407
+ const loadEffective = async () => {
2408
+ const url = effectiveUrl();
2409
+ if (!audioWanted || !url || loaded?.url === url || loading2 === url) return;
2410
+ loading2 = url;
2411
+ try {
2412
+ const buffer = await decodeImpl(url);
2413
+ if (audioWanted && effectiveUrl() === url) {
2414
+ loaded = { url, buffer };
2415
+ notify();
2416
+ }
2417
+ } catch (error) {
2418
+ console.warn(`[softphone] ringtone could not be loaded, using the built-in tone: ${url}`, error);
2419
+ } finally {
2420
+ if (loading2 === url) loading2 = null;
2421
+ }
2422
+ };
2423
+ const apply = (next) => {
2424
+ settings = next;
2425
+ if (loaded && loaded.url !== effectiveUrl()) loaded = null;
2426
+ notify();
2427
+ };
2428
+ return {
2429
+ getSettings: () => settings,
2430
+ readyBuffer: () => loaded && loaded.url === effectiveUrl() ? loaded.buffer : null,
2431
+ readyLoop: () => {
2432
+ if (!loaded || loaded.url !== effectiveUrl() || !settings) return null;
2433
+ return settings.catalogue.find((r) => r.id === settings.effective)?.loop ?? null;
2434
+ },
2435
+ refresh() {
2436
+ inFlight ??= (async () => {
2437
+ if (!settings && source.cached) {
2438
+ const early = await source.cached().catch(() => null);
2439
+ if (early && !settings) {
2440
+ apply(early);
2441
+ void loadEffective();
2442
+ }
2443
+ }
2444
+ try {
2445
+ apply(await source.get());
2446
+ } catch (error) {
2447
+ console.warn("[softphone] ringtone settings could not be loaded; keeping the previous ones", error);
2448
+ } finally {
2449
+ inFlight = null;
2450
+ }
2451
+ await loadEffective();
2452
+ })();
2453
+ return inFlight;
2454
+ },
2455
+ async setMine(id) {
2456
+ apply(await source.setMine(id));
2457
+ void loadEffective();
2458
+ },
2459
+ wantAudio(on) {
2460
+ if (audioWanted === on) return;
2461
+ audioWanted = on;
2462
+ if (!on) {
2463
+ loaded = null;
2464
+ notify();
2465
+ } else {
2466
+ void loadEffective();
2467
+ }
2468
+ },
2469
+ decode: (url) => decodeImpl(url),
2470
+ subscribe(listener) {
2471
+ listeners.add(listener);
2472
+ return () => listeners.delete(listener);
2473
+ }
2474
+ };
2475
+ }
2476
+ async function decodeRingtone(url) {
2477
+ const Offline = typeof OfflineAudioContext !== "undefined" ? OfflineAudioContext : globalThis.webkitOfflineAudioContext;
2478
+ if (!Offline) throw new Error("this browser cannot decode audio files");
2479
+ const res = await fetch(url, { credentials: "omit" });
2480
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2481
+ const bytes = await res.arrayBuffer();
2482
+ return new Offline(1, 1, 48e3).decodeAudioData(bytes);
2483
+ }
2167
2484
  function SettingsSection({
2168
2485
  title,
2169
2486
  children
@@ -2173,11 +2490,527 @@ function SettingsSection({
2173
2490
  /* @__PURE__ */ jsx("div", { className: "cv-settings-section-body", children })
2174
2491
  ] });
2175
2492
  }
2493
+ function SettingsNavRow({
2494
+ icon: Icon3,
2495
+ title,
2496
+ value,
2497
+ onOpen,
2498
+ rowRef
2499
+ }) {
2500
+ const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
2501
+ /* @__PURE__ */ jsx("span", { className: "cv-settings-row-icon", "aria-hidden": true, children: /* @__PURE__ */ jsx(Icon3, { size: 16 }) }),
2502
+ /* @__PURE__ */ jsxs("span", { className: "cv-settings-nav-text", children: [
2503
+ /* @__PURE__ */ jsx("span", { className: "cv-settings-nav-title", children: title }),
2504
+ /* @__PURE__ */ jsx("span", { className: "cv-settings-nav-value", children: value })
2505
+ ] }),
2506
+ onOpen ? /* @__PURE__ */ jsx(ChevronRight, { size: 16, className: "cv-settings-nav-chevron", "aria-hidden": true }) : null
2507
+ ] });
2508
+ return onOpen ? /* @__PURE__ */ jsx("button", { type: "button", className: "cv-settings-nav", onClick: onOpen, ref: rowRef, children: inner }) : /* @__PURE__ */ jsx("div", { className: "cv-settings-nav cv-settings-nav--static", children: inner });
2509
+ }
2510
+ function ChoiceList({
2511
+ label,
2512
+ choices,
2513
+ value,
2514
+ onSelect,
2515
+ onReselect
2516
+ }) {
2517
+ const name = useId();
2518
+ const pointer = useRef(false);
2519
+ return /* @__PURE__ */ jsx("div", { className: "cv-choices", role: "radiogroup", "aria-label": label, children: choices.map((c) => {
2520
+ const checked = c.value === value;
2521
+ return /* @__PURE__ */ jsxs(
2522
+ "label",
2523
+ {
2524
+ className: "cv-choice",
2525
+ "data-checked": checked || void 0,
2526
+ onPointerDown: () => {
2527
+ pointer.current = true;
2528
+ },
2529
+ children: [
2530
+ /* @__PURE__ */ jsx(
2531
+ "input",
2532
+ {
2533
+ type: "radio",
2534
+ className: "cv-choice-input",
2535
+ name,
2536
+ value: c.value,
2537
+ checked,
2538
+ onChange: () => {
2539
+ const via = pointer.current ? "pointer" : "keyboard";
2540
+ pointer.current = false;
2541
+ onSelect(c.value, via);
2542
+ },
2543
+ onClick: () => {
2544
+ if (c.value === value) {
2545
+ pointer.current = false;
2546
+ onReselect?.(c.value);
2547
+ }
2548
+ }
2549
+ }
2550
+ ),
2551
+ /* @__PURE__ */ jsxs("span", { className: "cv-choice-text", children: [
2552
+ /* @__PURE__ */ jsx("span", { className: "cv-choice-label", children: c.label }),
2553
+ c.sub ? /* @__PURE__ */ jsx("span", { className: "cv-choice-sub", children: c.sub }) : null
2554
+ ] }),
2555
+ c.trailing,
2556
+ /* @__PURE__ */ jsx(Check, { size: 16, className: "cv-choice-check", "aria-hidden": true })
2557
+ ]
2558
+ },
2559
+ c.value
2560
+ );
2561
+ }) });
2562
+ }
2563
+ function SettingsPage({
2564
+ title,
2565
+ intro,
2566
+ onBack,
2567
+ children
2568
+ }) {
2569
+ const titleId = useId();
2570
+ const heading = useRef(null);
2571
+ useEffect(() => heading.current?.focus(), []);
2572
+ return /* @__PURE__ */ jsxs(
2573
+ "div",
2574
+ {
2575
+ className: "cv-detail cv-settings-page",
2576
+ role: "region",
2577
+ "aria-labelledby": titleId,
2578
+ onKeyDown: (e) => {
2579
+ if (e.key === "Escape") {
2580
+ e.stopPropagation();
2581
+ onBack();
2582
+ }
2583
+ },
2584
+ children: [
2585
+ /* @__PURE__ */ jsx("div", { className: "cv-detail-head", children: /* @__PURE__ */ jsxs("button", { type: "button", className: "cv-detail-back", onClick: onBack, children: [
2586
+ /* @__PURE__ */ jsx(ArrowLeft, { size: 16, "aria-hidden": true }),
2587
+ "Ajustes"
2588
+ ] }) }),
2589
+ /* @__PURE__ */ jsxs("div", { className: "cv-settings-page-body", children: [
2590
+ /* @__PURE__ */ jsx("h2", { className: "cv-settings-page-title", id: titleId, tabIndex: -1, ref: heading, children: title }),
2591
+ intro ? /* @__PURE__ */ jsx("p", { className: "cv-settings-page-intro", children: intro }) : null,
2592
+ children
2593
+ ] })
2594
+ ]
2595
+ }
2596
+ );
2597
+ }
2598
+ var SYSTEM_DEFAULT = "";
2599
+ var PAGES = {
2600
+ input: {
2601
+ title: "Micr\xF3fono",
2602
+ intro: "El micr\xF3fono que usa el tel\xE9fono en las llamadas.",
2603
+ empty: "No se encontr\xF3 ning\xFAn micr\xF3fono."
2604
+ },
2605
+ output: {
2606
+ title: "Altavoces",
2607
+ intro: "D\xF3nde oyes a la otra persona durante una llamada.",
2608
+ empty: "No se encontr\xF3 ning\xFAn altavoz."
2609
+ },
2610
+ ringtone: {
2611
+ title: "Salida del tono",
2612
+ intro: "D\xF3nde suena el tono cuando entra una llamada. La llamada se oye por \xABAltavoces\xBB.",
2613
+ empty: "No se encontr\xF3 ning\xFAn altavoz."
2614
+ }
2615
+ };
2616
+ function deviceSummary(kind, list, id) {
2617
+ const i = id ? list.findIndex((d) => d.deviceId === id) : -1;
2618
+ return i >= 0 ? deviceLabel(list[i], i, kind) : "Predeterminado del sistema";
2619
+ }
2620
+ function DevicePage({ kind, onBack }) {
2621
+ const devices = useDevices();
2622
+ const page = PAGES[kind];
2623
+ const list = kind === "input" ? devices.inputs : devices.outputs;
2624
+ const current = kind === "input" ? devices.currentInputId : kind === "output" ? devices.currentOutputId : devices.currentRingtoneOutputId;
2625
+ const apply = (id) => kind === "input" ? devices.setInput(id) : kind === "output" ? devices.setOutput(id) : devices.setRingtoneOutput(id);
2626
+ const choices = [
2627
+ { value: SYSTEM_DEFAULT, label: "Predeterminado del sistema" },
2628
+ ...list.map((d, i) => ({ value: d.deviceId, label: deviceLabel(d, i, kind) }))
2629
+ ];
2630
+ return /* @__PURE__ */ jsxs(SettingsPage, { title: page.title, intro: page.intro, onBack, children: [
2631
+ list.length > 0 ? /* @__PURE__ */ jsx(
2632
+ ChoiceList,
2633
+ {
2634
+ label: page.title,
2635
+ choices,
2636
+ value: current ?? SYSTEM_DEFAULT,
2637
+ onSelect: (v) => void apply(v === SYSTEM_DEFAULT ? null : v)
2638
+ }
2639
+ ) : /* @__PURE__ */ jsx("p", { className: "cv-settings-page-empty", children: page.empty }),
2640
+ kind === "output" ? /* @__PURE__ */ jsx(SpeakerTest, { sinkId: devices.currentOutputId }) : null
2641
+ ] });
2642
+ }
2643
+ function SpeakerTest({ sinkId }) {
2644
+ const [testing, setTesting] = useState(false);
2645
+ const tone = useRef(null);
2646
+ const timer = useRef(null);
2647
+ useEffect(
2648
+ () => () => {
2649
+ tone.current?.stop();
2650
+ if (timer.current) clearTimeout(timer.current);
2651
+ },
2652
+ []
2653
+ );
2654
+ const test = () => {
2655
+ primeAudio();
2656
+ tone.current?.stop();
2657
+ tone.current = playTestTone(sinkId);
2658
+ setTesting(true);
2659
+ if (timer.current) clearTimeout(timer.current);
2660
+ timer.current = setTimeout(() => setTesting(false), TEST_TONE_MS);
2661
+ };
2662
+ return /* @__PURE__ */ jsxs("button", { type: "button", className: "cv-btn cv-settings-test", onClick: test, disabled: testing, children: [
2663
+ /* @__PURE__ */ jsx(Volume2, { size: 16, "aria-hidden": true }),
2664
+ testing ? "Sonando\u2026" : "Probar altavoz"
2665
+ ] });
2666
+ }
2667
+
2668
+ // src/ui/audio/preview.ts
2669
+ var PREVIEW_MAX_MS = 1e4;
2670
+ var BUILT_IN_PREVIEW_MS = 6e3;
2671
+ function previewDurationMs(buffer, loop) {
2672
+ if (!buffer) return BUILT_IN_PREVIEW_MS;
2673
+ const seconds = loop ? loop.end - loop.start : buffer.duration;
2674
+ return Math.min(seconds * 1e3, PREVIEW_MAX_MS);
2675
+ }
2676
+ function previewRingtone(ringtone, options = {}) {
2677
+ primeAudio();
2678
+ let tone = null;
2679
+ let timer = null;
2680
+ let over = false;
2681
+ let settle;
2682
+ let fail;
2683
+ const done = new Promise((resolve, reject) => {
2684
+ settle = resolve;
2685
+ fail = reject;
2686
+ });
2687
+ done.catch(() => {
2688
+ });
2689
+ const stop = () => {
2690
+ if (over) return;
2691
+ over = true;
2692
+ if (timer) clearTimeout(timer);
2693
+ tone?.stop();
2694
+ settle();
2695
+ };
2696
+ const play = (buffer) => {
2697
+ if (over) return;
2698
+ const loop = buffer ? ringtone.loop ?? null : null;
2699
+ tone = playRingtone({ buffer, loop, sinkId: options.sinkId ?? null });
2700
+ timer = setTimeout(stop, previewDurationMs(buffer, loop));
2701
+ };
2702
+ if (!ringtone.url) {
2703
+ play(null);
2704
+ } else {
2705
+ decodeRingtone(ringtone.url).then(play, (error) => {
2706
+ if (over) return;
2707
+ over = true;
2708
+ fail(error);
2709
+ });
2710
+ }
2711
+ return { stop, done };
2712
+ }
2713
+
2714
+ // src/ui/views/settings/ringtone-state.ts
2715
+ var noSubscribe = () => () => {
2716
+ };
2717
+ var noSettings = () => null;
2718
+ function useRingtoneSettings(store) {
2719
+ const settings = useSyncExternalStore(store?.subscribe ?? noSubscribe, store?.getSettings ?? noSettings);
2720
+ const [failed, setFailed] = useState(false);
2721
+ const load = useCallback(() => {
2722
+ if (!store) return () => {
2723
+ };
2724
+ let live = true;
2725
+ setFailed(false);
2726
+ void store.refresh().then(() => {
2727
+ if (live) setFailed(store.getSettings() === null);
2728
+ });
2729
+ return () => {
2730
+ live = false;
2731
+ };
2732
+ }, [store]);
2733
+ useEffect(load, [load]);
2734
+ return { settings, failed, retry: load };
2735
+ }
2736
+ function ringtoneSummary(settings, failed) {
2737
+ if (!settings) return failed ? "No disponible" : "Cargando\u2026";
2738
+ const byId = (id) => id ? settings.catalogue.find((r) => r.id === id) : void 0;
2739
+ const mine = byId(settings.mine);
2740
+ if (mine) return mine.name;
2741
+ const org = byId(settings.orgDefault);
2742
+ if (org) return `${org.name} \xB7 organizaci\xF3n`;
2743
+ return settings.catalogue.find((r) => r.url === null)?.name ?? "Predeterminado";
2744
+ }
2745
+ function useRingtonePreview(store, sinkId) {
2746
+ const [playing, setPlaying] = useState(null);
2747
+ const [error, setError] = useState(null);
2748
+ const current = useRef(null);
2749
+ const generation = useRef(0);
2750
+ const stop = useCallback(() => {
2751
+ generation.current += 1;
2752
+ const c = current.current;
2753
+ current.current = null;
2754
+ if (c?.timer) clearTimeout(c.timer);
2755
+ c?.tone?.stop();
2756
+ setPlaying(null);
2757
+ }, []);
2758
+ useEffect(() => stop, [stop]);
2759
+ const start = (settings, choice) => {
2760
+ stop();
2761
+ primeAudio();
2762
+ setError(null);
2763
+ const gen = generation.current;
2764
+ const orgDefault = settings.catalogue.some((r) => r.id === settings.orgDefault) ? settings.orgDefault : null;
2765
+ const id = choice || orgDefault;
2766
+ const option = settings.catalogue.find((r) => r.id === id) ?? settings.catalogue.find((r) => r.url === null);
2767
+ setPlaying(choice);
2768
+ current.current = { tone: null, timer: null };
2769
+ const play = (buffer) => {
2770
+ if (gen !== generation.current) return;
2771
+ const loop = buffer ? option?.loop ?? null : null;
2772
+ const tone = playRingtone({ buffer, loop, sinkId });
2773
+ current.current = { tone, timer: setTimeout(stop, previewDurationMs(buffer, loop)) };
2774
+ };
2775
+ if (!option?.url) {
2776
+ play(null);
2777
+ return;
2778
+ }
2779
+ store.decode(option.url).then(play, () => {
2780
+ if (gen !== generation.current) return;
2781
+ stop();
2782
+ setError("No se pudo reproducir el tono.");
2783
+ });
2784
+ };
2785
+ return { playing, error, start, stop };
2786
+ }
2787
+ var ORG_DEFAULT = "";
2788
+ var KEYBOARD_SETTLE_MS = 300;
2789
+ function RingtonePage({
2790
+ store,
2791
+ settings,
2792
+ failed,
2793
+ onRetry,
2794
+ onBack
2795
+ }) {
2796
+ const softphone = useSoftphone();
2797
+ const devices = useDevices();
2798
+ const preview = useRingtonePreview(store, devices.canSelectRingtoneOutput ? devices.currentRingtoneOutputId : null);
2799
+ const [pending, setPending] = useState(void 0);
2800
+ const [error, setError] = useState(null);
2801
+ const save = useSerialSave(store, {
2802
+ onSettled: () => setPending(void 0),
2803
+ onFailed: () => {
2804
+ setPending(void 0);
2805
+ setError("No se pudo guardar el tono. Int\xE9ntalo de nuevo.");
2806
+ }
2807
+ });
2808
+ const latest = useRef(settings);
2809
+ latest.current = settings;
2810
+ const settle = useRef(null);
2811
+ const { stop: stopPreview } = preview;
2812
+ useEffect(() => softphone.on("incomingCall" /* IncomingCall */, stopPreview), [softphone, stopPreview]);
2813
+ useEffect(
2814
+ () => () => {
2815
+ if (settle.current) {
2816
+ clearTimeout(settle.current.timer);
2817
+ save(settle.current.value || null);
2818
+ }
2819
+ },
2820
+ [save]
2821
+ );
2822
+ const commit = (value) => {
2823
+ save(value || null);
2824
+ if (latest.current) preview.start(latest.current, value);
2825
+ };
2826
+ const onSelect = (value, via) => {
2827
+ setError(null);
2828
+ setPending(value);
2829
+ if (settle.current) clearTimeout(settle.current.timer);
2830
+ settle.current = null;
2831
+ if (via === "pointer") {
2832
+ commit(value);
2833
+ return;
2834
+ }
2835
+ preview.stop();
2836
+ settle.current = {
2837
+ value,
2838
+ timer: setTimeout(() => {
2839
+ settle.current = null;
2840
+ commit(value);
2841
+ }, KEYBOARD_SETTLE_MS)
2842
+ };
2843
+ };
2844
+ const onReselect = (value) => {
2845
+ if (!settings) return;
2846
+ if (preview.playing === value) preview.stop();
2847
+ else preview.start(settings, value);
2848
+ };
2849
+ return /* @__PURE__ */ jsx(
2850
+ SettingsPage,
2851
+ {
2852
+ title: "Tono de llamada",
2853
+ intro: "Lo que suena cuando entra una llamada. Se guarda en tu cuenta, as\xED que es el mismo en todos tus dispositivos.",
2854
+ onBack,
2855
+ children: settings ? /* @__PURE__ */ jsxs(Fragment, { children: [
2856
+ /* @__PURE__ */ jsx(
2857
+ ChoiceList,
2858
+ {
2859
+ label: "Tono de llamada",
2860
+ choices: choicesFor(settings, preview.playing),
2861
+ value: pending !== void 0 ? pending : currentValue(settings),
2862
+ onSelect,
2863
+ onReselect
2864
+ }
2865
+ ),
2866
+ error ?? preview.error ? /* @__PURE__ */ jsx("p", { className: "cv-settings-error", role: "alert", children: error ?? preview.error }) : null
2867
+ ] }) : failed ? /* @__PURE__ */ jsxs("div", { className: "cv-empty", children: [
2868
+ /* @__PURE__ */ jsx("p", { className: "cv-empty-title", children: "No se pudieron cargar los tonos." }),
2869
+ /* @__PURE__ */ jsx("button", { type: "button", className: "cv-btn cv-settings-retry", onClick: onRetry, children: "Reintentar" })
2870
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "cv-empty", role: "status", children: [
2871
+ /* @__PURE__ */ jsx(LoaderCircle, { className: "cv-empty-spinner", size: 18, "aria-hidden": true }),
2872
+ /* @__PURE__ */ jsx("span", { className: "cv-empty-title", children: "Cargando\u2026" })
2873
+ ] })
2874
+ }
2875
+ );
2876
+ }
2877
+ function currentValue(settings) {
2878
+ return settings.mine && settings.catalogue.some((r) => r.id === settings.mine) ? settings.mine : ORG_DEFAULT;
2879
+ }
2880
+ function choicesFor(settings, playing) {
2881
+ const org = settings.orgDefault ? settings.catalogue.find((r) => r.id === settings.orgDefault) : void 0;
2882
+ const builtIn = settings.catalogue.find((r) => r.url === null);
2883
+ const mark = (value) => playing === value ? /* @__PURE__ */ jsxs("span", { className: "cv-choice-playing", children: [
2884
+ /* @__PURE__ */ jsx(Volume2, { size: 15, "aria-hidden": true }),
2885
+ /* @__PURE__ */ jsx("span", { className: "cv-sr-only", children: "Sonando" })
2886
+ ] }) : void 0;
2887
+ const orgChoice = {
2888
+ value: ORG_DEFAULT,
2889
+ label: org ? "Predeterminado de la organizaci\xF3n" : "Predeterminado",
2890
+ sub: (org ?? builtIn)?.name ?? "",
2891
+ trailing: mark(ORG_DEFAULT)
2892
+ };
2893
+ return [orgChoice, ...settings.catalogue.map((r) => ({ value: r.id, label: r.name, trailing: mark(r.id) }))];
2894
+ }
2895
+ function useSerialSave(store, handlers) {
2896
+ const wanted = useRef(null);
2897
+ const busy = useRef(false);
2898
+ const on = useRef(handlers);
2899
+ on.current = handlers;
2900
+ const [save] = useState(() => (id) => {
2901
+ wanted.current = { id };
2902
+ if (busy.current) return;
2903
+ busy.current = true;
2904
+ void (async () => {
2905
+ while (wanted.current) {
2906
+ const { id: next } = wanted.current;
2907
+ wanted.current = null;
2908
+ try {
2909
+ await store.setMine(next);
2910
+ if (!wanted.current) on.current.onSettled();
2911
+ } catch {
2912
+ if (!wanted.current) on.current.onFailed();
2913
+ }
2914
+ }
2915
+ busy.current = false;
2916
+ })();
2917
+ });
2918
+ return save;
2919
+ }
2176
2920
  function SettingsTab() {
2177
- return /* @__PURE__ */ jsx("div", { className: "cv-view cv-settings", children: /* @__PURE__ */ jsxs("div", { className: "cv-settings-body", children: [
2178
- /* @__PURE__ */ jsx(SettingsSection, { title: "Dispositivos", children: /* @__PURE__ */ jsx(DevicePickers, {}) }),
2179
- /* @__PURE__ */ jsx("p", { className: "cv-settings-more", children: "M\xE1s ajustes se ir\xE1n a\xF1adiendo pr\xF3ximamente." })
2180
- ] }) });
2921
+ const softphone = useSoftphone();
2922
+ const devices = useDevices();
2923
+ const store = ringtoneStore(softphone);
2924
+ const ringtone = useRingtoneSettings(store);
2925
+ const [page, setPage] = useState(null);
2926
+ const list = useRef(null);
2927
+ const rows = useRef({});
2928
+ const returnTo = useRef(null);
2929
+ useEffect(() => {
2930
+ void softphone.audio.refresh();
2931
+ }, [softphone]);
2932
+ useEffect(() => {
2933
+ list.current?.toggleAttribute("inert", page !== null);
2934
+ if (page === null && returnTo.current) {
2935
+ rows.current[returnTo.current]?.focus();
2936
+ returnTo.current = null;
2937
+ }
2938
+ }, [page]);
2939
+ const open = (p) => () => {
2940
+ returnTo.current = p;
2941
+ setPage(p);
2942
+ };
2943
+ const back = () => setPage(null);
2944
+ const rowRef = (p) => (el) => {
2945
+ rows.current[p] = el;
2946
+ };
2947
+ return /* @__PURE__ */ jsxs("div", { className: "cv-view cv-settings", children: [
2948
+ /* @__PURE__ */ jsxs("div", { className: "cv-settings-body", ref: list, children: [
2949
+ store ? /* @__PURE__ */ jsx(SettingsSection, { title: "Llamadas entrantes", children: /* @__PURE__ */ jsx(
2950
+ SettingsNavRow,
2951
+ {
2952
+ icon: Music,
2953
+ title: "Tono de llamada",
2954
+ value: ringtoneSummary(ringtone.settings, ringtone.failed),
2955
+ onOpen: open("tone"),
2956
+ rowRef: rowRef("tone")
2957
+ }
2958
+ ) }) : null,
2959
+ /* @__PURE__ */ jsxs(SettingsSection, { title: "Dispositivos", children: [
2960
+ /* @__PURE__ */ jsx(
2961
+ SettingsNavRow,
2962
+ {
2963
+ icon: Mic,
2964
+ title: "Micr\xF3fono",
2965
+ value: deviceSummary("input", devices.inputs, devices.currentInputId),
2966
+ onOpen: open("input"),
2967
+ rowRef: rowRef("input")
2968
+ }
2969
+ ),
2970
+ devices.canSelectOutput ? /* @__PURE__ */ jsx(
2971
+ SettingsNavRow,
2972
+ {
2973
+ icon: Headphones,
2974
+ title: "Altavoces",
2975
+ value: deviceSummary("output", devices.outputs, devices.currentOutputId),
2976
+ onOpen: open("output"),
2977
+ rowRef: rowRef("output")
2978
+ }
2979
+ ) : (
2980
+ // Safari and most of Firefox have no `setSinkId`: offering a page whose choice silently does
2981
+ // nothing is worse than saying where the choice really lives.
2982
+ /* @__PURE__ */ jsx(
2983
+ SettingsNavRow,
2984
+ {
2985
+ icon: Headphones,
2986
+ title: "Altavoces",
2987
+ value: "Tu navegador no permite elegirlos desde aqu\xED \u2014 c\xE1mbialos en los ajustes del sistema."
2988
+ }
2989
+ )
2990
+ ),
2991
+ devices.canSelectRingtoneOutput ? /* @__PURE__ */ jsx(
2992
+ SettingsNavRow,
2993
+ {
2994
+ icon: BellRing,
2995
+ title: "Salida del tono",
2996
+ value: deviceSummary("ringtone", devices.outputs, devices.currentRingtoneOutputId),
2997
+ onOpen: open("ringtone"),
2998
+ rowRef: rowRef("ringtone")
2999
+ }
3000
+ ) : null
3001
+ ] })
3002
+ ] }),
3003
+ page === "tone" && store ? /* @__PURE__ */ jsx(
3004
+ RingtonePage,
3005
+ {
3006
+ store,
3007
+ settings: ringtone.settings,
3008
+ failed: ringtone.failed,
3009
+ onRetry: ringtone.retry,
3010
+ onBack: back
3011
+ }
3012
+ ) : page !== null && page !== "tone" ? /* @__PURE__ */ jsx(DevicePage, { kind: page, onBack: back }) : null
3013
+ ] });
2181
3014
  }
2182
3015
  function useMicPermission() {
2183
3016
  const [state, setState] = useState("checking");
@@ -2406,14 +3239,29 @@ function useCallAudio(enabled) {
2406
3239
  const offDeviceChange = softphone.on("deviceChange" /* DeviceChange */, () => {
2407
3240
  void remote.current?.setOutputDevice(softphone.audio.currentOutputId);
2408
3241
  });
3242
+ const ringtones = ringtoneStore(softphone);
3243
+ ringtones?.wantAudio(true);
3244
+ const refreshRingtone = () => {
3245
+ if (softphone.isLeader) void ringtones?.refresh();
3246
+ };
3247
+ refreshRingtone();
3248
+ const offLeader = softphone.on("leaderChanged" /* LeaderChanged */, ({ isLeader }) => {
3249
+ if (isLeader) refreshRingtone();
3250
+ });
2409
3251
  const stopTone = () => {
2410
3252
  tone.current?.stop();
2411
3253
  tone.current = null;
2412
3254
  };
2413
3255
  const setTone = (next) => {
2414
3256
  stopTone();
2415
- if (next === "ringtone") tone.current = playRingtone();
2416
- else if (next === "ringback") tone.current = playRingback();
3257
+ if (next === "ringtone") {
3258
+ const audio = softphone.audio;
3259
+ tone.current = playRingtone({
3260
+ buffer: ringtones?.readyBuffer() ?? null,
3261
+ loop: ringtones?.readyLoop() ?? null,
3262
+ sinkId: audio.canSelectRingtoneOutput ? audio.currentRingtoneOutputId : null
3263
+ });
3264
+ } else if (next === "ringback") tone.current = playRingback();
2417
3265
  };
2418
3266
  const perCall = [];
2419
3267
  const followedCalls = /* @__PURE__ */ new Set();
@@ -2444,6 +3292,7 @@ function useCallAudio(enabled) {
2444
3292
  stopTone();
2445
3293
  remote.current?.detach();
2446
3294
  if (wasConnected && (reason === "local" || reason === "remote")) playCallEndedTone();
3295
+ refreshRingtone();
2447
3296
  })
2448
3297
  );
2449
3298
  };
@@ -2461,6 +3310,8 @@ function useCallAudio(enabled) {
2461
3310
  offIncoming();
2462
3311
  offActive();
2463
3312
  offDeviceChange();
3313
+ offLeader();
3314
+ ringtones?.wantAudio(false);
2464
3315
  for (const off of perCall) off();
2465
3316
  stopTone();
2466
3317
  remote.current?.destroy();
@@ -2635,6 +3486,7 @@ lucide-react/dist/esm/createLucideIcon.js:
2635
3486
  lucide-react/dist/esm/icons/arrow-left-right.js:
2636
3487
  lucide-react/dist/esm/icons/arrow-left.js:
2637
3488
  lucide-react/dist/esm/icons/arrow-right.js:
3489
+ lucide-react/dist/esm/icons/bell-ring.js:
2638
3490
  lucide-react/dist/esm/icons/calendar-clock.js:
2639
3491
  lucide-react/dist/esm/icons/check.js:
2640
3492
  lucide-react/dist/esm/icons/chevron-down.js:
@@ -2650,6 +3502,7 @@ lucide-react/dist/esm/icons/loader-circle.js:
2650
3502
  lucide-react/dist/esm/icons/mic-off.js:
2651
3503
  lucide-react/dist/esm/icons/mic.js:
2652
3504
  lucide-react/dist/esm/icons/monitor-smartphone.js:
3505
+ lucide-react/dist/esm/icons/music.js:
2653
3506
  lucide-react/dist/esm/icons/pause.js:
2654
3507
  lucide-react/dist/esm/icons/phone-incoming.js:
2655
3508
  lucide-react/dist/esm/icons/phone-missed.js:
@@ -2663,6 +3516,7 @@ lucide-react/dist/esm/icons/timer.js:
2663
3516
  lucide-react/dist/esm/icons/triangle-alert.js:
2664
3517
  lucide-react/dist/esm/icons/user.js:
2665
3518
  lucide-react/dist/esm/icons/users.js:
3519
+ lucide-react/dist/esm/icons/volume-2.js:
2666
3520
  lucide-react/dist/esm/icons/x.js:
2667
3521
  lucide-react/dist/esm/lucide-react.js:
2668
3522
  (**
@@ -2673,6 +3527,6 @@ lucide-react/dist/esm/lucide-react.js:
2673
3527
  *)
2674
3528
  */
2675
3529
 
2676
- export { CallScreen, ContactsTab, Dialpad, HistoryTab, MicPermissionGate, SoftphoneWidget };
3530
+ export { CallScreen, ContactsTab, Dialpad, HistoryTab, MicPermissionGate, SoftphoneWidget, previewRingtone };
2677
3531
  //# sourceMappingURL=index.js.map
2678
3532
  //# sourceMappingURL=index.js.map