@coventia/softphone 0.1.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/3x2-2HQTJDKZ.js +6583 -0
- package/dist/3x2-2HQTJDKZ.js.map +1 -0
- package/dist/3x2-INKWPNLA.js +6585 -0
- package/dist/3x2-INKWPNLA.js.map +1 -0
- package/dist/call-BTYlT2SU.d.cts +680 -0
- package/dist/call-BTYlT2SU.d.ts +680 -0
- package/dist/chunk-2ZSOGGPO.js +169 -0
- package/dist/chunk-2ZSOGGPO.js.map +1 -0
- package/dist/chunk-3V2L6UTK.js +129 -0
- package/dist/chunk-3V2L6UTK.js.map +1 -0
- package/dist/chunk-5BGEHMPP.js +945 -0
- package/dist/chunk-5BGEHMPP.js.map +1 -0
- package/dist/chunk-5WRI5ZAA.js +29 -0
- package/dist/chunk-5WRI5ZAA.js.map +1 -0
- package/dist/chunk-KAXEMJ7E.js +11 -0
- package/dist/chunk-KAXEMJ7E.js.map +1 -0
- package/dist/chunk-MHOJHG5E.js +39 -0
- package/dist/chunk-MHOJHG5E.js.map +1 -0
- package/dist/chunk-MP7CSA24.js +299 -0
- package/dist/chunk-MP7CSA24.js.map +1 -0
- package/dist/chunk-N2N2EQQJ.js +29 -0
- package/dist/chunk-N2N2EQQJ.js.map +1 -0
- package/dist/element/index.d.ts +853 -0
- package/dist/element/index.js +9635 -0
- package/dist/element/index.js.map +1 -0
- package/dist/engine-D9nbeY08.d.cts +115 -0
- package/dist/engine-Doa6Brkm.d.ts +115 -0
- package/dist/index.cjs +2133 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +62 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +630 -0
- package/dist/index.js.map +1 -0
- package/dist/min-64GIMWWL.js +5456 -0
- package/dist/min-64GIMWWL.js.map +1 -0
- package/dist/min-ATXYR23R.js +5458 -0
- package/dist/min-ATXYR23R.js.map +1 -0
- package/dist/react/index.cjs +138 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +68 -0
- package/dist/react/index.d.ts +68 -0
- package/dist/react/index.js +4 -0
- package/dist/react/index.js.map +1 -0
- package/dist/softphone-DHmhotbf.d.ts +128 -0
- package/dist/softphone-DhuhcKCy.d.cts +128 -0
- package/dist/softphone.css +1923 -0
- package/dist/sources-N4BOEDRL.js +226 -0
- package/dist/sources-N4BOEDRL.js.map +1 -0
- package/dist/testing/index.cjs +345 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +66 -0
- package/dist/testing/index.d.ts +66 -0
- package/dist/testing/index.js +186 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/ui/index.cjs +15428 -0
- package/dist/ui/index.cjs.map +1 -0
- package/dist/ui/index.d.cts +188 -0
- package/dist/ui/index.d.ts +188 -0
- package/dist/ui/index.js +2256 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/zadarma/index.cjs +1144 -0
- package/dist/zadarma/index.cjs.map +1 -0
- package/dist/zadarma/index.d.cts +86 -0
- package/dist/zadarma/index.d.ts +86 -0
- package/dist/zadarma/index.js +5 -0
- package/dist/zadarma/index.js.map +1 -0
- package/dist/zadarma-engine-DN2SZSQ3.js +5 -0
- package/dist/zadarma-engine-DN2SZSQ3.js.map +1 -0
- package/package.json +123 -0
package/dist/ui/index.js
ADDED
|
@@ -0,0 +1,2256 @@
|
|
|
1
|
+
import { isWebRtcSupported } from '../chunk-KAXEMJ7E.js';
|
|
2
|
+
import { useSoftphone, useSoftphoneState, useCall, useActiveCall, useIsLeader, useLastError, useDevices } from '../chunk-3V2L6UTK.js';
|
|
3
|
+
import '../chunk-N2N2EQQJ.js';
|
|
4
|
+
import { forwardRef, createElement, useState, useRef, useLayoutEffect, useEffect, useCallback, useMemo } from 'react';
|
|
5
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
// ../../node_modules/lucide-react/dist/esm/shared/src/utils.js
|
|
8
|
+
var toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
9
|
+
var mergeClasses = (...classes) => classes.filter((className, index, array) => {
|
|
10
|
+
return Boolean(className) && array.indexOf(className) === index;
|
|
11
|
+
}).join(" ");
|
|
12
|
+
|
|
13
|
+
// ../../node_modules/lucide-react/dist/esm/defaultAttributes.js
|
|
14
|
+
var defaultAttributes = {
|
|
15
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
16
|
+
width: 24,
|
|
17
|
+
height: 24,
|
|
18
|
+
viewBox: "0 0 24 24",
|
|
19
|
+
fill: "none",
|
|
20
|
+
stroke: "currentColor",
|
|
21
|
+
strokeWidth: 2,
|
|
22
|
+
strokeLinecap: "round",
|
|
23
|
+
strokeLinejoin: "round"
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// ../../node_modules/lucide-react/dist/esm/Icon.js
|
|
27
|
+
var Icon = forwardRef(
|
|
28
|
+
({
|
|
29
|
+
color = "currentColor",
|
|
30
|
+
size = 24,
|
|
31
|
+
strokeWidth = 2,
|
|
32
|
+
absoluteStrokeWidth,
|
|
33
|
+
className = "",
|
|
34
|
+
children,
|
|
35
|
+
iconNode,
|
|
36
|
+
...rest
|
|
37
|
+
}, ref) => {
|
|
38
|
+
return createElement(
|
|
39
|
+
"svg",
|
|
40
|
+
{
|
|
41
|
+
ref,
|
|
42
|
+
...defaultAttributes,
|
|
43
|
+
width: size,
|
|
44
|
+
height: size,
|
|
45
|
+
stroke: color,
|
|
46
|
+
strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth,
|
|
47
|
+
className: mergeClasses("lucide", className),
|
|
48
|
+
...rest
|
|
49
|
+
},
|
|
50
|
+
[
|
|
51
|
+
...iconNode.map(([tag, attrs]) => createElement(tag, attrs)),
|
|
52
|
+
...Array.isArray(children) ? children : [children]
|
|
53
|
+
]
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// ../../node_modules/lucide-react/dist/esm/createLucideIcon.js
|
|
59
|
+
var createLucideIcon = (iconName, iconNode) => {
|
|
60
|
+
const Component = forwardRef(
|
|
61
|
+
({ className, ...props }, ref) => createElement(Icon, {
|
|
62
|
+
ref,
|
|
63
|
+
iconNode,
|
|
64
|
+
className: mergeClasses(`lucide-${toKebabCase(iconName)}`, className),
|
|
65
|
+
...props
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
Component.displayName = `${iconName}`;
|
|
69
|
+
return Component;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ../../node_modules/lucide-react/dist/esm/icons/arrow-left-right.js
|
|
73
|
+
var ArrowLeftRight = createLucideIcon("ArrowLeftRight", [
|
|
74
|
+
["path", { d: "M8 3 4 7l4 4", key: "9rb6wj" }],
|
|
75
|
+
["path", { d: "M4 7h16", key: "6tx8e3" }],
|
|
76
|
+
["path", { d: "m16 21 4-4-4-4", key: "siv7j2" }],
|
|
77
|
+
["path", { d: "M20 17H4", key: "h6l3hr" }]
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
// ../../node_modules/lucide-react/dist/esm/icons/arrow-left.js
|
|
81
|
+
var ArrowLeft = createLucideIcon("ArrowLeft", [
|
|
82
|
+
["path", { d: "m12 19-7-7 7-7", key: "1l729n" }],
|
|
83
|
+
["path", { d: "M19 12H5", key: "x3x0zl" }]
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
// ../../node_modules/lucide-react/dist/esm/icons/arrow-right.js
|
|
87
|
+
var ArrowRight = createLucideIcon("ArrowRight", [
|
|
88
|
+
["path", { d: "M5 12h14", key: "1ays0h" }],
|
|
89
|
+
["path", { d: "m12 5 7 7-7 7", key: "xquz4c" }]
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
// ../../node_modules/lucide-react/dist/esm/icons/calendar-clock.js
|
|
93
|
+
var CalendarClock = createLucideIcon("CalendarClock", [
|
|
94
|
+
["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" }],
|
|
95
|
+
["path", { d: "M16 2v4", key: "4m81vk" }],
|
|
96
|
+
["path", { d: "M8 2v4", key: "1cmpym" }],
|
|
97
|
+
["path", { d: "M3 10h5", key: "r794hk" }],
|
|
98
|
+
["path", { d: "M17.5 17.5 16 16.3V14", key: "akvzfd" }],
|
|
99
|
+
["circle", { cx: "16", cy: "16", r: "6", key: "qoo3c4" }]
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
// ../../node_modules/lucide-react/dist/esm/icons/check.js
|
|
103
|
+
var Check = createLucideIcon("Check", [["path", { d: "M20 6 9 17l-5-5", key: "1gmf2c" }]]);
|
|
104
|
+
|
|
105
|
+
// ../../node_modules/lucide-react/dist/esm/icons/chevron-down.js
|
|
106
|
+
var ChevronDown = createLucideIcon("ChevronDown", [
|
|
107
|
+
["path", { d: "m6 9 6 6 6-6", key: "qrunsl" }]
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
// ../../node_modules/lucide-react/dist/esm/icons/chevron-right.js
|
|
111
|
+
var ChevronRight = createLucideIcon("ChevronRight", [
|
|
112
|
+
["path", { d: "m9 18 6-6-6-6", key: "mthhwq" }]
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
// ../../node_modules/lucide-react/dist/esm/icons/circle-help.js
|
|
116
|
+
var CircleHelp = createLucideIcon("CircleHelp", [
|
|
117
|
+
["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }],
|
|
118
|
+
["path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3", key: "1u773s" }],
|
|
119
|
+
["path", { d: "M12 17h.01", key: "p32p05" }]
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
// ../../node_modules/lucide-react/dist/esm/icons/clock.js
|
|
123
|
+
var Clock = createLucideIcon("Clock", [
|
|
124
|
+
["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }],
|
|
125
|
+
["polyline", { points: "12 6 12 12 16 14", key: "68esgv" }]
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
// ../../node_modules/lucide-react/dist/esm/icons/delete.js
|
|
129
|
+
var Delete = createLucideIcon("Delete", [
|
|
130
|
+
[
|
|
131
|
+
"path",
|
|
132
|
+
{
|
|
133
|
+
d: "M10 5a2 2 0 0 0-1.344.519l-6.328 5.74a1 1 0 0 0 0 1.481l6.328 5.741A2 2 0 0 0 10 19h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2z",
|
|
134
|
+
key: "1yo7s0"
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
["path", { d: "m12 9 6 6", key: "anjzzh" }],
|
|
138
|
+
["path", { d: "m18 9-6 6", key: "1fp51s" }]
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
// ../../node_modules/lucide-react/dist/esm/icons/external-link.js
|
|
142
|
+
var ExternalLink = createLucideIcon("ExternalLink", [
|
|
143
|
+
["path", { d: "M15 3h6v6", key: "1q9fwt" }],
|
|
144
|
+
["path", { d: "M10 14 21 3", key: "gplh6r" }],
|
|
145
|
+
["path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6", key: "a6xqqp" }]
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
// ../../node_modules/lucide-react/dist/esm/icons/grid-3x3.js
|
|
149
|
+
var Grid3x3 = createLucideIcon("Grid3x3", [
|
|
150
|
+
["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", key: "afitv7" }],
|
|
151
|
+
["path", { d: "M3 9h18", key: "1pudct" }],
|
|
152
|
+
["path", { d: "M3 15h18", key: "5xshup" }],
|
|
153
|
+
["path", { d: "M9 3v18", key: "fh3hqa" }],
|
|
154
|
+
["path", { d: "M15 3v18", key: "14nvp0" }]
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
// ../../node_modules/lucide-react/dist/esm/icons/hash.js
|
|
158
|
+
var Hash = createLucideIcon("Hash", [
|
|
159
|
+
["line", { x1: "4", x2: "20", y1: "9", y2: "9", key: "4lhtct" }],
|
|
160
|
+
["line", { x1: "4", x2: "20", y1: "15", y2: "15", key: "vyu0kd" }],
|
|
161
|
+
["line", { x1: "10", x2: "8", y1: "3", y2: "21", key: "1ggp8o" }],
|
|
162
|
+
["line", { x1: "16", x2: "14", y1: "3", y2: "21", key: "weycgp" }]
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
// ../../node_modules/lucide-react/dist/esm/icons/headphones.js
|
|
166
|
+
var Headphones = createLucideIcon("Headphones", [
|
|
167
|
+
[
|
|
168
|
+
"path",
|
|
169
|
+
{
|
|
170
|
+
d: "M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a9 9 0 0 1 18 0v7a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3",
|
|
171
|
+
key: "1xhozi"
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
// ../../node_modules/lucide-react/dist/esm/icons/loader-circle.js
|
|
177
|
+
var LoaderCircle = createLucideIcon("LoaderCircle", [
|
|
178
|
+
["path", { d: "M21 12a9 9 0 1 1-6.219-8.56", key: "13zald" }]
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
// ../../node_modules/lucide-react/dist/esm/icons/mic-off.js
|
|
182
|
+
var MicOff = createLucideIcon("MicOff", [
|
|
183
|
+
["line", { x1: "2", x2: "22", y1: "2", y2: "22", key: "a6p6uj" }],
|
|
184
|
+
["path", { d: "M18.89 13.23A7.12 7.12 0 0 0 19 12v-2", key: "80xlxr" }],
|
|
185
|
+
["path", { d: "M5 10v2a7 7 0 0 0 12 5", key: "p2k8kg" }],
|
|
186
|
+
["path", { d: "M15 9.34V5a3 3 0 0 0-5.68-1.33", key: "1gzdoj" }],
|
|
187
|
+
["path", { d: "M9 9v3a3 3 0 0 0 5.12 2.12", key: "r2i35w" }],
|
|
188
|
+
["line", { x1: "12", x2: "12", y1: "19", y2: "22", key: "x3vr5v" }]
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
// ../../node_modules/lucide-react/dist/esm/icons/mic.js
|
|
192
|
+
var Mic = createLucideIcon("Mic", [
|
|
193
|
+
["path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z", key: "131961" }],
|
|
194
|
+
["path", { d: "M19 10v2a7 7 0 0 1-14 0v-2", key: "1vc78b" }],
|
|
195
|
+
["line", { x1: "12", x2: "12", y1: "19", y2: "22", key: "x3vr5v" }]
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
// ../../node_modules/lucide-react/dist/esm/icons/monitor-smartphone.js
|
|
199
|
+
var MonitorSmartphone = createLucideIcon("MonitorSmartphone", [
|
|
200
|
+
["path", { d: "M18 8V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h8", key: "10dyio" }],
|
|
201
|
+
["path", { d: "M10 19v-3.96 3.15", key: "1irgej" }],
|
|
202
|
+
["path", { d: "M7 19h5", key: "qswx4l" }],
|
|
203
|
+
["rect", { width: "6", height: "10", x: "16", y: "12", rx: "2", key: "1egngj" }]
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
// ../../node_modules/lucide-react/dist/esm/icons/pause.js
|
|
207
|
+
var Pause = createLucideIcon("Pause", [
|
|
208
|
+
["rect", { x: "14", y: "4", width: "4", height: "16", rx: "1", key: "zuxfzm" }],
|
|
209
|
+
["rect", { x: "6", y: "4", width: "4", height: "16", rx: "1", key: "1okwgv" }]
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
// ../../node_modules/lucide-react/dist/esm/icons/phone-incoming.js
|
|
213
|
+
var PhoneIncoming = createLucideIcon("PhoneIncoming", [
|
|
214
|
+
["polyline", { points: "16 2 16 8 22 8", key: "1ygljm" }],
|
|
215
|
+
["line", { x1: "22", x2: "16", y1: "2", y2: "8", key: "1xzwqn" }],
|
|
216
|
+
[
|
|
217
|
+
"path",
|
|
218
|
+
{
|
|
219
|
+
d: "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z",
|
|
220
|
+
key: "foiqr5"
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
// ../../node_modules/lucide-react/dist/esm/icons/phone-missed.js
|
|
226
|
+
var PhoneMissed = createLucideIcon("PhoneMissed", [
|
|
227
|
+
["line", { x1: "22", x2: "16", y1: "2", y2: "8", key: "1xzwqn" }],
|
|
228
|
+
["line", { x1: "16", x2: "22", y1: "2", y2: "8", key: "13zxdn" }],
|
|
229
|
+
[
|
|
230
|
+
"path",
|
|
231
|
+
{
|
|
232
|
+
d: "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z",
|
|
233
|
+
key: "foiqr5"
|
|
234
|
+
}
|
|
235
|
+
]
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
// ../../node_modules/lucide-react/dist/esm/icons/phone-off.js
|
|
239
|
+
var PhoneOff = createLucideIcon("PhoneOff", [
|
|
240
|
+
[
|
|
241
|
+
"path",
|
|
242
|
+
{
|
|
243
|
+
d: "M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91",
|
|
244
|
+
key: "z86iuo"
|
|
245
|
+
}
|
|
246
|
+
],
|
|
247
|
+
["line", { x1: "22", x2: "2", y1: "2", y2: "22", key: "11kh81" }]
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
// ../../node_modules/lucide-react/dist/esm/icons/phone-outgoing.js
|
|
251
|
+
var PhoneOutgoing = createLucideIcon("PhoneOutgoing", [
|
|
252
|
+
["polyline", { points: "22 8 22 2 16 2", key: "1g204g" }],
|
|
253
|
+
["line", { x1: "16", x2: "22", y1: "8", y2: "2", key: "1ggias" }],
|
|
254
|
+
[
|
|
255
|
+
"path",
|
|
256
|
+
{
|
|
257
|
+
d: "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z",
|
|
258
|
+
key: "foiqr5"
|
|
259
|
+
}
|
|
260
|
+
]
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
// ../../node_modules/lucide-react/dist/esm/icons/phone.js
|
|
264
|
+
var Phone = createLucideIcon("Phone", [
|
|
265
|
+
[
|
|
266
|
+
"path",
|
|
267
|
+
{
|
|
268
|
+
d: "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z",
|
|
269
|
+
key: "foiqr5"
|
|
270
|
+
}
|
|
271
|
+
]
|
|
272
|
+
]);
|
|
273
|
+
|
|
274
|
+
// ../../node_modules/lucide-react/dist/esm/icons/play.js
|
|
275
|
+
var Play = createLucideIcon("Play", [
|
|
276
|
+
["polygon", { points: "6 3 20 12 6 21 6 3", key: "1oa8hb" }]
|
|
277
|
+
]);
|
|
278
|
+
|
|
279
|
+
// ../../node_modules/lucide-react/dist/esm/icons/search.js
|
|
280
|
+
var Search = createLucideIcon("Search", [
|
|
281
|
+
["circle", { cx: "11", cy: "11", r: "8", key: "4ej97u" }],
|
|
282
|
+
["path", { d: "m21 21-4.3-4.3", key: "1qie3q" }]
|
|
283
|
+
]);
|
|
284
|
+
|
|
285
|
+
// ../../node_modules/lucide-react/dist/esm/icons/settings.js
|
|
286
|
+
var Settings = createLucideIcon("Settings", [
|
|
287
|
+
[
|
|
288
|
+
"path",
|
|
289
|
+
{
|
|
290
|
+
d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z",
|
|
291
|
+
key: "1qme2f"
|
|
292
|
+
}
|
|
293
|
+
],
|
|
294
|
+
["circle", { cx: "12", cy: "12", r: "3", key: "1v7zrd" }]
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
// ../../node_modules/lucide-react/dist/esm/icons/timer.js
|
|
298
|
+
var Timer = createLucideIcon("Timer", [
|
|
299
|
+
["line", { x1: "10", x2: "14", y1: "2", y2: "2", key: "14vaq8" }],
|
|
300
|
+
["line", { x1: "12", x2: "15", y1: "14", y2: "11", key: "17fdiu" }],
|
|
301
|
+
["circle", { cx: "12", cy: "14", r: "8", key: "1e1u0o" }]
|
|
302
|
+
]);
|
|
303
|
+
|
|
304
|
+
// ../../node_modules/lucide-react/dist/esm/icons/triangle-alert.js
|
|
305
|
+
var TriangleAlert = createLucideIcon("TriangleAlert", [
|
|
306
|
+
[
|
|
307
|
+
"path",
|
|
308
|
+
{
|
|
309
|
+
d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",
|
|
310
|
+
key: "wmoenq"
|
|
311
|
+
}
|
|
312
|
+
],
|
|
313
|
+
["path", { d: "M12 9v4", key: "juzpu7" }],
|
|
314
|
+
["path", { d: "M12 17h.01", key: "p32p05" }]
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
// ../../node_modules/lucide-react/dist/esm/icons/user.js
|
|
318
|
+
var User = createLucideIcon("User", [
|
|
319
|
+
["path", { d: "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2", key: "975kel" }],
|
|
320
|
+
["circle", { cx: "12", cy: "7", r: "4", key: "17ys0d" }]
|
|
321
|
+
]);
|
|
322
|
+
|
|
323
|
+
// ../../node_modules/lucide-react/dist/esm/icons/users.js
|
|
324
|
+
var Users = createLucideIcon("Users", [
|
|
325
|
+
["path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2", key: "1yyitq" }],
|
|
326
|
+
["circle", { cx: "9", cy: "7", r: "4", key: "nufk8" }],
|
|
327
|
+
["path", { d: "M22 21v-2a4 4 0 0 0-3-3.87", key: "kshegd" }],
|
|
328
|
+
["path", { d: "M16 3.13a4 4 0 0 1 0 7.75", key: "1da9ce" }]
|
|
329
|
+
]);
|
|
330
|
+
|
|
331
|
+
// ../../node_modules/lucide-react/dist/esm/icons/x.js
|
|
332
|
+
var X = createLucideIcon("X", [
|
|
333
|
+
["path", { d: "M18 6 6 18", key: "1bl5f8" }],
|
|
334
|
+
["path", { d: "m6 6 12 12", key: "d8bk6v" }]
|
|
335
|
+
]);
|
|
336
|
+
|
|
337
|
+
// src/ui/dialing/countries.ts
|
|
338
|
+
var PACKED = "AC247 AD376 AE971 AF93 AG1 AI1 AL355 AM374 AO244 AR54 AS1 AT43 AU61 AW297 AX358 AZ994 BA387 BB1 BD880 BE32 BF226 BG359 BH973 BI257 BJ229 BL590 BM1 BN673 BO591 BQ599 BR55 BS1 BT975 BW267 BY375 BZ501 CA1 CC61 CD243 CF236 CG242 CH41 CI225 CK682 CL56 CM237 CN86 CO57 CR506 CU53 CV238 CW599 CX61 CY357 CZ420 DE49 DJ253 DK45 DM1 DO1 DZ213 EC593 EE372 EG20 EH212 ER291 ES34 ET251 FI358 FJ679 FK500 FM691 FO298 FR33 GA241 GB44 GD1 GE995 GF594 GG44 GH233 GI350 GL299 GM220 GN224 GP590 GQ240 GR30 GT502 GU1 GW245 GY592 HK852 HN504 HR385 HT509 HU36 ID62 IE353 IL972 IM44 IN91 IO246 IQ964 IR98 IS354 IT39 JE44 JM1 JO962 JP81 KE254 KG996 KH855 KI686 KM269 KN1 KP850 KR82 KW965 KY1 KZ7 LA856 LB961 LC1 LI423 LK94 LR231 LS266 LT370 LU352 LV371 LY218 MA212 MC377 MD373 ME382 MF590 MG261 MH692 MK389 ML223 MM95 MN976 MO853 MP1 MQ596 MR222 MS1 MT356 MU230 MV960 MW265 MX52 MY60 MZ258 NA264 NC687 NE227 NF672 NG234 NI505 NL31 NO47 NP977 NR674 NU683 NZ64 OM968 PA507 PE51 PF689 PG675 PH63 PK92 PL48 PM508 PR1 PS970 PT351 PW680 PY595 QA974 RE262 RO40 RS381 RU7 RW250 SA966 SB677 SC248 SD249 SE46 SG65 SH290 SI386 SJ47 SK421 SL232 SM378 SN221 SO252 SR597 SS211 ST239 SV503 SX1 SY963 SZ268 TA290 TC1 TD235 TG228 TH66 TJ992 TK690 TL670 TM993 TN216 TO676 TR90 TT1 TV688 TW886 TZ255 UA380 UG256 US1 UY598 UZ998 VA39 VC1 VE58 VG1 VI1 VN84 VU678 WF681 WS685 XK383 YE967 YT262 ZA27 ZM260 ZW263";
|
|
339
|
+
var DIAL_CODES = new Map(
|
|
340
|
+
PACKED.split(" ").map((token) => [token.slice(0, 2), Number(token.slice(2))])
|
|
341
|
+
);
|
|
342
|
+
var DEFAULT_COUNTRY = "ES";
|
|
343
|
+
var REGION_NAMES = (() => {
|
|
344
|
+
try {
|
|
345
|
+
return new Intl.DisplayNames(["es"], { type: "region" });
|
|
346
|
+
} catch {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
})();
|
|
350
|
+
function countryName(country) {
|
|
351
|
+
try {
|
|
352
|
+
return REGION_NAMES?.of(country) ?? country;
|
|
353
|
+
} catch {
|
|
354
|
+
return country;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
var COUNTRIES = [...DIAL_CODES.entries()].map(([code, dial]) => ({ code, name: countryName(code), dial })).sort((a, b) => a.name.localeCompare(b.name, "es"));
|
|
358
|
+
function toE164(country, national) {
|
|
359
|
+
const typed = String(national ?? "").trim();
|
|
360
|
+
if (typed.startsWith("+")) return "+" + typed.slice(1).replace(/\D/g, "");
|
|
361
|
+
const digits = typed.replace(/\D/g, "").replace(/^0+/, "");
|
|
362
|
+
if (!digits) return "";
|
|
363
|
+
const dial = DIAL_CODES.get(country);
|
|
364
|
+
return dial === void 0 ? `+${digits}` : `+${dial}${digits}`;
|
|
365
|
+
}
|
|
366
|
+
var FLAGS = null;
|
|
367
|
+
var flagsPromise = null;
|
|
368
|
+
function loadFlags() {
|
|
369
|
+
flagsPromise ??= import('../3x2-2HQTJDKZ.js').then((m) => FLAGS = m).catch(() => FLAGS = {});
|
|
370
|
+
return flagsPromise;
|
|
371
|
+
}
|
|
372
|
+
function useFlags() {
|
|
373
|
+
const [flags, setFlags] = useState(FLAGS);
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
if (flags) return;
|
|
376
|
+
let alive = true;
|
|
377
|
+
void loadFlags().then((f) => {
|
|
378
|
+
if (alive) setFlags(f);
|
|
379
|
+
});
|
|
380
|
+
return () => {
|
|
381
|
+
alive = false;
|
|
382
|
+
};
|
|
383
|
+
}, [flags]);
|
|
384
|
+
return flags;
|
|
385
|
+
}
|
|
386
|
+
function CountryMark({ code }) {
|
|
387
|
+
const flags = useFlags();
|
|
388
|
+
const Flag = flags?.[code];
|
|
389
|
+
return /* @__PURE__ */ jsx("span", { className: "cv-mark", "data-plate": !Flag, "aria-hidden": true, part: "country-mark", children: Flag ? /* @__PURE__ */ jsx(Flag, { className: "cv-mark-svg" }) : code });
|
|
390
|
+
}
|
|
391
|
+
function CountryPicker({
|
|
392
|
+
value,
|
|
393
|
+
onChange
|
|
394
|
+
}) {
|
|
395
|
+
const [open, setOpen] = useState(false);
|
|
396
|
+
const [query, setQuery] = useState("");
|
|
397
|
+
const rootRef = useRef(null);
|
|
398
|
+
const inputRef = useRef(null);
|
|
399
|
+
const results = useMemo(() => {
|
|
400
|
+
const q = query.trim().toLowerCase().replace(/^\+/, "");
|
|
401
|
+
if (!q) return COUNTRIES;
|
|
402
|
+
return COUNTRIES.filter(
|
|
403
|
+
(c) => c.name.toLowerCase().includes(q) || c.code.toLowerCase().includes(q) || String(c.dial).startsWith(q)
|
|
404
|
+
);
|
|
405
|
+
}, [query]);
|
|
406
|
+
useEffect(() => {
|
|
407
|
+
if (!open) return;
|
|
408
|
+
const root = rootRef.current?.getRootNode() ?? document;
|
|
409
|
+
const onDown = (e) => {
|
|
410
|
+
if (!rootRef.current?.contains(e.target)) setOpen(false);
|
|
411
|
+
};
|
|
412
|
+
const onKey = (e) => {
|
|
413
|
+
if (e.key === "Escape") setOpen(false);
|
|
414
|
+
};
|
|
415
|
+
root.addEventListener("pointerdown", onDown);
|
|
416
|
+
document.addEventListener("keydown", onKey);
|
|
417
|
+
return () => {
|
|
418
|
+
root.removeEventListener("pointerdown", onDown);
|
|
419
|
+
document.removeEventListener("keydown", onKey);
|
|
420
|
+
};
|
|
421
|
+
}, [open]);
|
|
422
|
+
useEffect(() => {
|
|
423
|
+
if (open) inputRef.current?.focus();
|
|
424
|
+
else setQuery("");
|
|
425
|
+
}, [open]);
|
|
426
|
+
const dial = DIAL_CODES.get(value);
|
|
427
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-cp", ref: rootRef, children: [
|
|
428
|
+
/* @__PURE__ */ jsxs(
|
|
429
|
+
"button",
|
|
430
|
+
{
|
|
431
|
+
type: "button",
|
|
432
|
+
className: "cv-cp-chip",
|
|
433
|
+
onClick: () => setOpen((o) => !o),
|
|
434
|
+
"aria-haspopup": "listbox",
|
|
435
|
+
"aria-expanded": open,
|
|
436
|
+
"aria-label": `Prefijo: ${countryName(value)} (+${dial ?? "?"}). Cambiar pa\xEDs`,
|
|
437
|
+
part: "country-chip",
|
|
438
|
+
children: [
|
|
439
|
+
/* @__PURE__ */ jsx(CountryMark, { code: value }),
|
|
440
|
+
/* @__PURE__ */ jsxs("span", { className: "cv-cp-dial", children: [
|
|
441
|
+
"+",
|
|
442
|
+
dial ?? "?"
|
|
443
|
+
] }),
|
|
444
|
+
/* @__PURE__ */ jsx(ChevronDown, { size: 13, className: "cv-cp-caret", "data-open": open })
|
|
445
|
+
]
|
|
446
|
+
}
|
|
447
|
+
),
|
|
448
|
+
open ? /* @__PURE__ */ jsxs("div", { className: "cv-cp-pop", role: "dialog", part: "country-list", children: [
|
|
449
|
+
/* @__PURE__ */ jsx(
|
|
450
|
+
"input",
|
|
451
|
+
{
|
|
452
|
+
ref: inputRef,
|
|
453
|
+
className: "cv-cp-search",
|
|
454
|
+
value: query,
|
|
455
|
+
onChange: (e) => setQuery(e.target.value),
|
|
456
|
+
placeholder: "Buscar pa\xEDs\u2026",
|
|
457
|
+
"aria-label": "Buscar pa\xEDs"
|
|
458
|
+
}
|
|
459
|
+
),
|
|
460
|
+
/* @__PURE__ */ jsx("div", { className: "cv-cp-list", role: "listbox", children: results.length === 0 ? /* @__PURE__ */ jsx("p", { className: "cv-cp-empty", children: "No se encontr\xF3 ning\xFAn pa\xEDs." }) : results.map((c) => /* @__PURE__ */ jsxs(
|
|
461
|
+
"button",
|
|
462
|
+
{
|
|
463
|
+
type: "button",
|
|
464
|
+
role: "option",
|
|
465
|
+
"aria-selected": c.code === value,
|
|
466
|
+
className: "cv-cp-item",
|
|
467
|
+
onClick: () => {
|
|
468
|
+
onChange(c.code);
|
|
469
|
+
setOpen(false);
|
|
470
|
+
},
|
|
471
|
+
children: [
|
|
472
|
+
/* @__PURE__ */ jsx(CountryMark, { code: c.code }),
|
|
473
|
+
/* @__PURE__ */ jsx("span", { className: "cv-cp-name", children: c.name }),
|
|
474
|
+
/* @__PURE__ */ jsxs("span", { className: "cv-cp-dial", children: [
|
|
475
|
+
"+",
|
|
476
|
+
c.dial
|
|
477
|
+
] }),
|
|
478
|
+
/* @__PURE__ */ jsx(Check, { size: 14, className: "cv-cp-check", "data-on": c.code === value })
|
|
479
|
+
]
|
|
480
|
+
},
|
|
481
|
+
c.code
|
|
482
|
+
)) })
|
|
483
|
+
] }) : null
|
|
484
|
+
] });
|
|
485
|
+
}
|
|
486
|
+
var AsYouType = null;
|
|
487
|
+
var parseIntl = null;
|
|
488
|
+
var loading = null;
|
|
489
|
+
function loadNumberFormatter() {
|
|
490
|
+
loading ??= import('../min-64GIMWWL.js').then((m) => {
|
|
491
|
+
AsYouType = m.AsYouType;
|
|
492
|
+
parseIntl = m.parsePhoneNumberFromString;
|
|
493
|
+
}).catch(() => {
|
|
494
|
+
});
|
|
495
|
+
return loading;
|
|
496
|
+
}
|
|
497
|
+
function formatIntl(e164) {
|
|
498
|
+
if (!parseIntl) return e164;
|
|
499
|
+
try {
|
|
500
|
+
return parseIntl(e164)?.formatInternational() ?? e164;
|
|
501
|
+
} catch {
|
|
502
|
+
return e164;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function useIntlFormatter() {
|
|
506
|
+
const [, bump] = useState(0);
|
|
507
|
+
useEffect(() => {
|
|
508
|
+
if (parseIntl) return;
|
|
509
|
+
let alive = true;
|
|
510
|
+
void loadNumberFormatter().then(() => {
|
|
511
|
+
if (alive) bump((n) => n + 1);
|
|
512
|
+
});
|
|
513
|
+
return () => {
|
|
514
|
+
alive = false;
|
|
515
|
+
};
|
|
516
|
+
}, []);
|
|
517
|
+
return formatIntl;
|
|
518
|
+
}
|
|
519
|
+
function formatNational(country, digits) {
|
|
520
|
+
if (!AsYouType || !digits) return digits;
|
|
521
|
+
try {
|
|
522
|
+
return new AsYouType(country).input(digits);
|
|
523
|
+
} catch {
|
|
524
|
+
return digits;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function useNumberFormatter() {
|
|
528
|
+
const [, bump] = useState(0);
|
|
529
|
+
useEffect(() => {
|
|
530
|
+
if (AsYouType) return;
|
|
531
|
+
let alive = true;
|
|
532
|
+
void loadNumberFormatter().then(() => {
|
|
533
|
+
if (alive) bump((n) => n + 1);
|
|
534
|
+
});
|
|
535
|
+
return () => {
|
|
536
|
+
alive = false;
|
|
537
|
+
};
|
|
538
|
+
}, []);
|
|
539
|
+
return formatNational;
|
|
540
|
+
}
|
|
541
|
+
function caretAfterDigits(formatted, digitsBefore) {
|
|
542
|
+
if (digitsBefore <= 0) return 0;
|
|
543
|
+
let seen = 0;
|
|
544
|
+
for (let i = 0; i < formatted.length; i++) {
|
|
545
|
+
if (/\d/.test(formatted[i])) seen++;
|
|
546
|
+
if (seen === digitsBefore) return i + 1;
|
|
547
|
+
}
|
|
548
|
+
return formatted.length;
|
|
549
|
+
}
|
|
550
|
+
function digitsBeforeCaret(text, caret) {
|
|
551
|
+
return (text.slice(0, caret).match(/\d/g) ?? []).length;
|
|
552
|
+
}
|
|
553
|
+
var KEYS = [
|
|
554
|
+
["1", "", true],
|
|
555
|
+
["2", "ABC", true],
|
|
556
|
+
["3", "DEF", true],
|
|
557
|
+
["4", "GHI", true],
|
|
558
|
+
["5", "JKL", true],
|
|
559
|
+
["6", "MNO", true],
|
|
560
|
+
["7", "PQRS", true],
|
|
561
|
+
["8", "TUV", true],
|
|
562
|
+
["9", "WXYZ", true],
|
|
563
|
+
["*", "", false],
|
|
564
|
+
["0", "+", true],
|
|
565
|
+
["#", "", false]
|
|
566
|
+
];
|
|
567
|
+
function Dialpad({ outcome = null, onIntent } = {}) {
|
|
568
|
+
const softphone = useSoftphone();
|
|
569
|
+
const state = useSoftphoneState();
|
|
570
|
+
const [value, setValue] = useState("");
|
|
571
|
+
const [country, setCountry] = useState(DEFAULT_COUNTRY);
|
|
572
|
+
const format = useNumberFormatter();
|
|
573
|
+
const inputRef = useRef(null);
|
|
574
|
+
const caretRef = useRef(null);
|
|
575
|
+
const display = format(country, value);
|
|
576
|
+
const target = toE164(country, value);
|
|
577
|
+
const canCall = state === "online" && target.length > 1;
|
|
578
|
+
useLayoutEffect(() => {
|
|
579
|
+
const caret = caretRef.current;
|
|
580
|
+
if (caret === null || !inputRef.current) return;
|
|
581
|
+
caretRef.current = null;
|
|
582
|
+
const pos = caretAfterDigits(display, caret);
|
|
583
|
+
inputRef.current.setSelectionRange(pos, pos);
|
|
584
|
+
}, [display]);
|
|
585
|
+
const append = (key) => {
|
|
586
|
+
onIntent?.();
|
|
587
|
+
setValue((v) => v + key);
|
|
588
|
+
};
|
|
589
|
+
const backspace = () => {
|
|
590
|
+
onIntent?.();
|
|
591
|
+
setValue((v) => v.slice(0, -1));
|
|
592
|
+
};
|
|
593
|
+
const call = () => {
|
|
594
|
+
if (!canCall) return;
|
|
595
|
+
onIntent?.();
|
|
596
|
+
void softphone.call(target);
|
|
597
|
+
};
|
|
598
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-view", children: [
|
|
599
|
+
/* @__PURE__ */ jsx("div", { className: "cv-dial-prefix", children: /* @__PURE__ */ jsx(CountryPicker, { value: country, onChange: setCountry }) }),
|
|
600
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-dial-head", children: [
|
|
601
|
+
/* @__PURE__ */ jsx("span", { className: "cv-dial-side", "aria-hidden": true }),
|
|
602
|
+
/* @__PURE__ */ jsx(
|
|
603
|
+
"input",
|
|
604
|
+
{
|
|
605
|
+
ref: inputRef,
|
|
606
|
+
className: "cv-display",
|
|
607
|
+
value: display,
|
|
608
|
+
onChange: (e) => {
|
|
609
|
+
onIntent?.();
|
|
610
|
+
caretRef.current = digitsBeforeCaret(e.target.value, e.target.selectionStart ?? 0);
|
|
611
|
+
setValue(e.target.value.replace(/[^\d+]/g, ""));
|
|
612
|
+
},
|
|
613
|
+
onKeyDown: (e) => e.key === "Enter" && call(),
|
|
614
|
+
placeholder: "N\xFAmero",
|
|
615
|
+
"aria-label": "N\xFAmero a marcar",
|
|
616
|
+
inputMode: "tel"
|
|
617
|
+
}
|
|
618
|
+
),
|
|
619
|
+
/* @__PURE__ */ jsx(
|
|
620
|
+
"button",
|
|
621
|
+
{
|
|
622
|
+
type: "button",
|
|
623
|
+
className: "cv-dial-side",
|
|
624
|
+
"data-hidden": value.length === 0,
|
|
625
|
+
onClick: backspace,
|
|
626
|
+
"aria-label": "Borrar",
|
|
627
|
+
children: /* @__PURE__ */ jsx(Delete, { size: 19 })
|
|
628
|
+
}
|
|
629
|
+
)
|
|
630
|
+
] }),
|
|
631
|
+
/* @__PURE__ */ jsx("p", { className: "cv-dial-outcome", role: "status", part: "error", "data-shown": outcome !== null, children: outcome?.text ?? "" }),
|
|
632
|
+
/* @__PURE__ */ jsx("div", { className: "cv-keypad", children: KEYS.map(([key, letters, dialable]) => /* @__PURE__ */ jsxs(
|
|
633
|
+
"button",
|
|
634
|
+
{
|
|
635
|
+
type: "button",
|
|
636
|
+
className: "cv-key",
|
|
637
|
+
"data-dialable": dialable,
|
|
638
|
+
onClick: () => dialable && append(key),
|
|
639
|
+
"aria-disabled": !dialable,
|
|
640
|
+
title: dialable ? void 0 : "Solo disponible durante una llamada",
|
|
641
|
+
children: [
|
|
642
|
+
/* @__PURE__ */ jsx("b", { children: key }),
|
|
643
|
+
letters ? /* @__PURE__ */ jsx("small", { children: letters }) : null
|
|
644
|
+
]
|
|
645
|
+
},
|
|
646
|
+
key
|
|
647
|
+
)) }),
|
|
648
|
+
/* @__PURE__ */ jsx("div", { className: "cv-dial-foot", children: /* @__PURE__ */ jsx(
|
|
649
|
+
"button",
|
|
650
|
+
{
|
|
651
|
+
type: "button",
|
|
652
|
+
className: "cv-round cv-round--answer",
|
|
653
|
+
onClick: call,
|
|
654
|
+
disabled: !canCall,
|
|
655
|
+
"aria-label": "Llamar",
|
|
656
|
+
part: "call-button",
|
|
657
|
+
children: /* @__PURE__ */ jsx(Phone, { size: 26 })
|
|
658
|
+
}
|
|
659
|
+
) })
|
|
660
|
+
] });
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// src/ui/lib/format.ts
|
|
664
|
+
function formatDuration(totalSeconds) {
|
|
665
|
+
const s = Math.max(0, Math.floor(totalSeconds));
|
|
666
|
+
const hh = Math.floor(s / 3600);
|
|
667
|
+
const mm = Math.floor(s % 3600 / 60);
|
|
668
|
+
const ss = s % 60;
|
|
669
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
670
|
+
return hh > 0 ? `${hh}:${pad(mm)}:${pad(ss)}` : `${mm}:${pad(ss)}`;
|
|
671
|
+
}
|
|
672
|
+
function initials(name) {
|
|
673
|
+
if (!name) return "#";
|
|
674
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
675
|
+
if (words.length === 0 || /^\+?\d/.test(words[0])) return "#";
|
|
676
|
+
return words.slice(0, 2).map((w) => w[0].toUpperCase()).join("");
|
|
677
|
+
}
|
|
678
|
+
function elapsedSeconds(since, now) {
|
|
679
|
+
if (!since) return 0;
|
|
680
|
+
return Math.floor((now - since.getTime()) / 1e3);
|
|
681
|
+
}
|
|
682
|
+
function useTicker(enabled, intervalMs = 1e3) {
|
|
683
|
+
const [now, setNow] = useState(() => Date.now());
|
|
684
|
+
useEffect(() => {
|
|
685
|
+
if (!enabled) return;
|
|
686
|
+
setNow(Date.now());
|
|
687
|
+
const id = setInterval(() => setNow(Date.now()), intervalMs);
|
|
688
|
+
return () => clearInterval(id);
|
|
689
|
+
}, [enabled, intervalMs]);
|
|
690
|
+
return now;
|
|
691
|
+
}
|
|
692
|
+
function deviceLabel(device, index, kind) {
|
|
693
|
+
if (device.label) return device.label;
|
|
694
|
+
return kind === "input" ? `Micr\xF3fono ${index + 1}` : `Altavoz ${index + 1}`;
|
|
695
|
+
}
|
|
696
|
+
var DEFAULT_OPTION = "";
|
|
697
|
+
function DevicePicker({
|
|
698
|
+
icon: Icon3,
|
|
699
|
+
label,
|
|
700
|
+
kind,
|
|
701
|
+
options,
|
|
702
|
+
value,
|
|
703
|
+
onChange,
|
|
704
|
+
emptyText
|
|
705
|
+
}) {
|
|
706
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-settings-row", children: [
|
|
707
|
+
/* @__PURE__ */ jsx("span", { className: "cv-settings-row-icon", "aria-hidden": true, children: /* @__PURE__ */ jsx(Icon3, { size: 16 }) }),
|
|
708
|
+
/* @__PURE__ */ jsxs("span", { className: "cv-settings-row-text", children: [
|
|
709
|
+
/* @__PURE__ */ jsx("span", { className: "cv-settings-row-label", id: `cv-settings-label-${kind}`, children: label }),
|
|
710
|
+
options.length > 0 ? /* @__PURE__ */ jsxs(
|
|
711
|
+
"select",
|
|
712
|
+
{
|
|
713
|
+
className: "cv-settings-select",
|
|
714
|
+
"aria-labelledby": `cv-settings-label-${kind}`,
|
|
715
|
+
value: value ?? DEFAULT_OPTION,
|
|
716
|
+
onChange: (e) => onChange(e.target.value === DEFAULT_OPTION ? null : e.target.value),
|
|
717
|
+
children: [
|
|
718
|
+
/* @__PURE__ */ jsx("option", { value: DEFAULT_OPTION, children: "Predeterminado del sistema" }),
|
|
719
|
+
options.map((d, i) => /* @__PURE__ */ jsx("option", { value: d.deviceId, children: deviceLabel(d, i, kind) }, d.deviceId))
|
|
720
|
+
]
|
|
721
|
+
}
|
|
722
|
+
) : /* @__PURE__ */ jsx("span", { className: "cv-settings-empty", children: emptyText })
|
|
723
|
+
] })
|
|
724
|
+
] });
|
|
725
|
+
}
|
|
726
|
+
function DevicePickers() {
|
|
727
|
+
const softphone = useSoftphone();
|
|
728
|
+
const devices = useDevices();
|
|
729
|
+
useEffect(() => {
|
|
730
|
+
void softphone.audio.refresh();
|
|
731
|
+
}, [softphone]);
|
|
732
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
733
|
+
/* @__PURE__ */ jsx(
|
|
734
|
+
DevicePicker,
|
|
735
|
+
{
|
|
736
|
+
icon: Mic,
|
|
737
|
+
label: "Micr\xF3fono",
|
|
738
|
+
kind: "input",
|
|
739
|
+
options: devices.inputs,
|
|
740
|
+
value: devices.currentInputId,
|
|
741
|
+
onChange: (id) => void devices.setInput(id),
|
|
742
|
+
emptyText: "No se encontr\xF3 ning\xFAn micr\xF3fono"
|
|
743
|
+
}
|
|
744
|
+
),
|
|
745
|
+
devices.canSelectOutput ? /* @__PURE__ */ jsx(
|
|
746
|
+
DevicePicker,
|
|
747
|
+
{
|
|
748
|
+
icon: Headphones,
|
|
749
|
+
label: "Altavoces",
|
|
750
|
+
kind: "output",
|
|
751
|
+
options: devices.outputs,
|
|
752
|
+
value: devices.currentOutputId,
|
|
753
|
+
onChange: (id) => void devices.setOutput(id),
|
|
754
|
+
emptyText: "No se encontr\xF3 ning\xFAn altavoz"
|
|
755
|
+
}
|
|
756
|
+
) : (
|
|
757
|
+
// Safari and most of Firefox have no `setSinkId` at all — nothing here would do anything,
|
|
758
|
+
// and offering a picker that silently fails is worse than not offering one.
|
|
759
|
+
/* @__PURE__ */ jsx("p", { className: "cv-settings-note", children: "Tu navegador no permite elegir el altavoz desde aqu\xED \u2014 c\xE1mbialo en los ajustes del sistema." })
|
|
760
|
+
)
|
|
761
|
+
] });
|
|
762
|
+
}
|
|
763
|
+
function Control({
|
|
764
|
+
icon: Icon3,
|
|
765
|
+
label,
|
|
766
|
+
pressed = false,
|
|
767
|
+
tone,
|
|
768
|
+
onClick
|
|
769
|
+
}) {
|
|
770
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-ctrl-item", children: [
|
|
771
|
+
/* @__PURE__ */ jsx(
|
|
772
|
+
"button",
|
|
773
|
+
{
|
|
774
|
+
type: "button",
|
|
775
|
+
className: "cv-ctrl",
|
|
776
|
+
"data-tone": pressed ? tone : void 0,
|
|
777
|
+
"aria-pressed": pressed,
|
|
778
|
+
"aria-label": label,
|
|
779
|
+
onClick,
|
|
780
|
+
children: /* @__PURE__ */ jsx(Icon3, { size: 19 })
|
|
781
|
+
}
|
|
782
|
+
),
|
|
783
|
+
/* @__PURE__ */ jsx("span", { className: "cv-ctrl-label", "aria-hidden": true, children: label })
|
|
784
|
+
] });
|
|
785
|
+
}
|
|
786
|
+
var STATUS_LABEL = {
|
|
787
|
+
ringing: "Llamada entrante",
|
|
788
|
+
dialing: "Llamando\u2026",
|
|
789
|
+
early: "Llamando\u2026",
|
|
790
|
+
answering: "Conectando\u2026",
|
|
791
|
+
reconnecting: "Reconectando\u2026",
|
|
792
|
+
held: "En espera"
|
|
793
|
+
};
|
|
794
|
+
function CallScreen({
|
|
795
|
+
call,
|
|
796
|
+
incomingCallAction
|
|
797
|
+
}) {
|
|
798
|
+
const view = useCall(call);
|
|
799
|
+
const [showTransfer, setShowTransfer] = useState(false);
|
|
800
|
+
const [transferTo, setTransferTo] = useState("");
|
|
801
|
+
const [showDevices, setShowDevices] = useState(false);
|
|
802
|
+
const controlsRef = useRef(null);
|
|
803
|
+
const now = useTicker(view?.state === "active");
|
|
804
|
+
const isConnected = view ? view.state === "active" || view.state === "held" || view.state === "reconnecting" : false;
|
|
805
|
+
useEffect(() => {
|
|
806
|
+
if (!showDevices) return;
|
|
807
|
+
const onKey = (e) => {
|
|
808
|
+
if (e.key === "Escape") setShowDevices(false);
|
|
809
|
+
};
|
|
810
|
+
document.addEventListener("keydown", onKey);
|
|
811
|
+
if (!isConnected) return () => document.removeEventListener("keydown", onKey);
|
|
812
|
+
const root = controlsRef.current?.getRootNode() ?? document;
|
|
813
|
+
const onDown = (e) => {
|
|
814
|
+
if (!controlsRef.current?.contains(e.target)) setShowDevices(false);
|
|
815
|
+
};
|
|
816
|
+
root.addEventListener("pointerdown", onDown);
|
|
817
|
+
return () => {
|
|
818
|
+
document.removeEventListener("keydown", onKey);
|
|
819
|
+
root.removeEventListener("pointerdown", onDown);
|
|
820
|
+
};
|
|
821
|
+
}, [showDevices, isConnected]);
|
|
822
|
+
if (!view || view.state === "ended") return null;
|
|
823
|
+
const s = view.state;
|
|
824
|
+
const isRinging = s === "ringing";
|
|
825
|
+
const inCall = isConnected;
|
|
826
|
+
const name = view.remote.displayName;
|
|
827
|
+
const number = view.remote.number;
|
|
828
|
+
const showNumber = number !== name;
|
|
829
|
+
const incomingAction = isRinging && view.remote.contact && incomingCallAction ? incomingCallAction : null;
|
|
830
|
+
const IncomingActionIcon = incomingAction?.icon ?? ExternalLink;
|
|
831
|
+
const [rawM = "0", ss = "00"] = formatDuration(elapsedSeconds(view.answeredAt, now)).split(":");
|
|
832
|
+
const mm = rawM.padStart(2, "0");
|
|
833
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-view", children: [
|
|
834
|
+
/* @__PURE__ */ jsx("div", { className: "cv-call", children: !inCall && showDevices ? /* @__PURE__ */ jsxs("div", { className: "cv-call-devices-inline", children: [
|
|
835
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-call-devices-header", children: [
|
|
836
|
+
/* @__PURE__ */ jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setShowDevices(false), children: [
|
|
837
|
+
/* @__PURE__ */ jsx(ArrowLeft, { size: 13, "aria-hidden": true }),
|
|
838
|
+
"Volver"
|
|
839
|
+
] }),
|
|
840
|
+
/* @__PURE__ */ jsx("span", { className: "cv-call-devices-title", children: "Dispositivos" })
|
|
841
|
+
] }),
|
|
842
|
+
/* @__PURE__ */ jsxs("p", { className: "cv-call-devices-context", children: [
|
|
843
|
+
showNumber ? `${number} \xB7 ` : "",
|
|
844
|
+
STATUS_LABEL[s] ?? ""
|
|
845
|
+
] }),
|
|
846
|
+
/* @__PURE__ */ jsx("div", { className: "cv-settings-section-body", children: /* @__PURE__ */ jsx(DevicePickers, {}) })
|
|
847
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
848
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-avatar-wrap", children: [
|
|
849
|
+
isRinging ? /* @__PURE__ */ jsx("span", { className: "cv-avatar-ping", "aria-hidden": true }) : null,
|
|
850
|
+
/* @__PURE__ */ jsx("div", { className: "cv-avatar", "aria-hidden": true, children: initials(name) }),
|
|
851
|
+
s === "active" ? /* @__PURE__ */ jsx("span", { className: "cv-avatar-dot", "aria-hidden": true }) : null
|
|
852
|
+
] }),
|
|
853
|
+
/* @__PURE__ */ jsx("div", { className: "cv-call-name", children: name }),
|
|
854
|
+
showNumber ? /* @__PURE__ */ jsx("div", { className: "cv-call-number", children: number }) : null,
|
|
855
|
+
s === "active" ? /* @__PURE__ */ jsxs("div", { className: "cv-timer", "aria-label": "Duraci\xF3n de la llamada", children: [
|
|
856
|
+
/* @__PURE__ */ jsx("b", { children: mm }),
|
|
857
|
+
/* @__PURE__ */ jsx("i", { children: ":" }),
|
|
858
|
+
/* @__PURE__ */ jsx("b", { children: ss })
|
|
859
|
+
] }) : /* @__PURE__ */ jsx("div", { className: "cv-call-status", children: s === "held" ? /* @__PURE__ */ jsx("span", { className: "cv-badge-hold", children: STATUS_LABEL[s] }) : STATUS_LABEL[s] ?? "" }),
|
|
860
|
+
!inCall ? /* @__PURE__ */ jsxs("div", { className: "cv-call-chips", children: [
|
|
861
|
+
incomingAction ? /* @__PURE__ */ jsxs(
|
|
862
|
+
"button",
|
|
863
|
+
{
|
|
864
|
+
type: "button",
|
|
865
|
+
className: "cv-call-devices-chip",
|
|
866
|
+
onClick: () => incomingAction.onClick(view.remote.contact),
|
|
867
|
+
children: [
|
|
868
|
+
/* @__PURE__ */ jsx(IncomingActionIcon, { size: 13, "aria-hidden": true }),
|
|
869
|
+
incomingAction.label
|
|
870
|
+
]
|
|
871
|
+
}
|
|
872
|
+
) : null,
|
|
873
|
+
/* @__PURE__ */ jsxs("button", { type: "button", className: "cv-call-devices-chip", onClick: () => setShowDevices(true), children: [
|
|
874
|
+
/* @__PURE__ */ jsx(Headphones, { size: 13, "aria-hidden": true }),
|
|
875
|
+
"Dispositivos"
|
|
876
|
+
] })
|
|
877
|
+
] }) : null,
|
|
878
|
+
inCall && showTransfer ? /* @__PURE__ */ jsxs("div", { className: "cv-transfer", children: [
|
|
879
|
+
/* @__PURE__ */ jsx(
|
|
880
|
+
"input",
|
|
881
|
+
{
|
|
882
|
+
className: "cv-search",
|
|
883
|
+
value: transferTo,
|
|
884
|
+
onChange: (e) => setTransferTo(e.target.value),
|
|
885
|
+
placeholder: "Extensi\xF3n o n\xFAmero",
|
|
886
|
+
"aria-label": "Transferir a",
|
|
887
|
+
autoFocus: true
|
|
888
|
+
}
|
|
889
|
+
),
|
|
890
|
+
/* @__PURE__ */ jsx(
|
|
891
|
+
"button",
|
|
892
|
+
{
|
|
893
|
+
className: "cv-btn cv-btn--accent",
|
|
894
|
+
disabled: !transferTo.trim(),
|
|
895
|
+
onClick: () => {
|
|
896
|
+
void call.transfer(transferTo.trim());
|
|
897
|
+
setShowTransfer(false);
|
|
898
|
+
},
|
|
899
|
+
children: "Transferir"
|
|
900
|
+
}
|
|
901
|
+
)
|
|
902
|
+
] }) : null
|
|
903
|
+
] }) }),
|
|
904
|
+
/* @__PURE__ */ jsx("div", { className: "cv-controls", ref: controlsRef, children: isRinging ? /* @__PURE__ */ jsxs("div", { className: "cv-controls-primary", children: [
|
|
905
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-ctrl-item", children: [
|
|
906
|
+
/* @__PURE__ */ jsx(
|
|
907
|
+
"button",
|
|
908
|
+
{
|
|
909
|
+
type: "button",
|
|
910
|
+
className: "cv-round cv-round--hangup",
|
|
911
|
+
onClick: () => void call.reject(),
|
|
912
|
+
"aria-label": "Rechazar",
|
|
913
|
+
children: /* @__PURE__ */ jsx(PhoneOff, { size: 26 })
|
|
914
|
+
}
|
|
915
|
+
),
|
|
916
|
+
/* @__PURE__ */ jsx("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Rechazar" })
|
|
917
|
+
] }),
|
|
918
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-ctrl-item", children: [
|
|
919
|
+
/* @__PURE__ */ jsx(
|
|
920
|
+
"button",
|
|
921
|
+
{
|
|
922
|
+
type: "button",
|
|
923
|
+
className: "cv-round cv-round--answer",
|
|
924
|
+
onClick: () => void call.answer(),
|
|
925
|
+
"aria-label": "Contestar",
|
|
926
|
+
part: "answer-button",
|
|
927
|
+
children: /* @__PURE__ */ jsx(Phone, { size: 26 })
|
|
928
|
+
}
|
|
929
|
+
),
|
|
930
|
+
/* @__PURE__ */ jsx("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Contestar" })
|
|
931
|
+
] })
|
|
932
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
933
|
+
inCall ? /* @__PURE__ */ jsxs("div", { className: "cv-controls-row", children: [
|
|
934
|
+
/* @__PURE__ */ jsx(
|
|
935
|
+
Control,
|
|
936
|
+
{
|
|
937
|
+
icon: view.isMuted ? MicOff : Mic,
|
|
938
|
+
label: view.isMuted ? "Activar" : "Silenciar",
|
|
939
|
+
pressed: view.isMuted,
|
|
940
|
+
tone: "danger",
|
|
941
|
+
onClick: () => call.mute(!view.isMuted)
|
|
942
|
+
}
|
|
943
|
+
),
|
|
944
|
+
/* @__PURE__ */ jsx(
|
|
945
|
+
Control,
|
|
946
|
+
{
|
|
947
|
+
icon: view.isOnHold ? Play : Pause,
|
|
948
|
+
label: view.isOnHold ? "Reanudar" : "Espera",
|
|
949
|
+
pressed: view.isOnHold,
|
|
950
|
+
tone: "hold",
|
|
951
|
+
onClick: () => view.isOnHold ? void call.unhold() : void call.hold()
|
|
952
|
+
}
|
|
953
|
+
),
|
|
954
|
+
/* @__PURE__ */ jsx(
|
|
955
|
+
Control,
|
|
956
|
+
{
|
|
957
|
+
icon: ArrowLeftRight,
|
|
958
|
+
label: "Transferir",
|
|
959
|
+
pressed: showTransfer,
|
|
960
|
+
tone: "accent",
|
|
961
|
+
onClick: () => setShowTransfer((v) => !v)
|
|
962
|
+
}
|
|
963
|
+
),
|
|
964
|
+
/* @__PURE__ */ jsx(
|
|
965
|
+
Control,
|
|
966
|
+
{
|
|
967
|
+
icon: Headphones,
|
|
968
|
+
label: "Dispositivos",
|
|
969
|
+
pressed: showDevices,
|
|
970
|
+
tone: "accent",
|
|
971
|
+
onClick: () => setShowDevices((v) => !v)
|
|
972
|
+
}
|
|
973
|
+
)
|
|
974
|
+
] }) : null,
|
|
975
|
+
/* @__PURE__ */ jsx("div", { className: "cv-controls-primary", children: /* @__PURE__ */ jsxs("div", { className: "cv-ctrl-item", children: [
|
|
976
|
+
/* @__PURE__ */ jsx(
|
|
977
|
+
"button",
|
|
978
|
+
{
|
|
979
|
+
type: "button",
|
|
980
|
+
className: "cv-round cv-round--hangup",
|
|
981
|
+
onClick: () => void call.hangup(),
|
|
982
|
+
"aria-label": "Colgar",
|
|
983
|
+
part: "hangup-button",
|
|
984
|
+
children: /* @__PURE__ */ jsx(PhoneOff, { size: 26 })
|
|
985
|
+
}
|
|
986
|
+
),
|
|
987
|
+
/* @__PURE__ */ jsx("span", { className: "cv-ctrl-label", "aria-hidden": true, children: "Colgar" })
|
|
988
|
+
] }) }),
|
|
989
|
+
inCall && showDevices ? /* @__PURE__ */ jsx("div", { className: "cv-device-popover cv-device-popover--above", role: "dialog", "aria-label": "Dispositivos de audio", children: /* @__PURE__ */ jsx(DevicePickers, {}) }) : null
|
|
990
|
+
] }) })
|
|
991
|
+
] });
|
|
992
|
+
}
|
|
993
|
+
function DetailRow({
|
|
994
|
+
icon: Icon3,
|
|
995
|
+
eyebrow,
|
|
996
|
+
text,
|
|
997
|
+
onClick
|
|
998
|
+
}) {
|
|
999
|
+
const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1000
|
+
/* @__PURE__ */ jsx("span", { className: "cv-detail-row-icon", "aria-hidden": true, children: /* @__PURE__ */ jsx(Icon3, { size: 16 }) }),
|
|
1001
|
+
/* @__PURE__ */ jsxs("span", { className: "cv-detail-row-text", children: [
|
|
1002
|
+
eyebrow ? /* @__PURE__ */ jsx("span", { className: "cv-detail-row-eyebrow", children: eyebrow }) : null,
|
|
1003
|
+
/* @__PURE__ */ jsx("span", { className: "cv-detail-row-value", children: text })
|
|
1004
|
+
] }),
|
|
1005
|
+
onClick ? /* @__PURE__ */ jsx(ChevronRight, { size: 16, className: "cv-detail-row-chevron", "aria-hidden": true }) : null
|
|
1006
|
+
] });
|
|
1007
|
+
return onClick ? /* @__PURE__ */ jsx("button", { type: "button", className: "cv-detail-row cv-detail-row--action", onClick, children: inner }) : /* @__PURE__ */ jsx("div", { className: "cv-detail-row", children: inner });
|
|
1008
|
+
}
|
|
1009
|
+
function ContactDetail({
|
|
1010
|
+
contact,
|
|
1011
|
+
onBack,
|
|
1012
|
+
contactAction
|
|
1013
|
+
}) {
|
|
1014
|
+
const softphone = useSoftphone();
|
|
1015
|
+
const formatIntl2 = useIntlFormatter();
|
|
1016
|
+
const formatted = formatIntl2(contact.phone);
|
|
1017
|
+
const name = contact.name ?? formatted;
|
|
1018
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-detail", role: "region", "aria-label": name, children: [
|
|
1019
|
+
/* @__PURE__ */ jsx("div", { className: "cv-detail-head", children: /* @__PURE__ */ jsxs("button", { type: "button", className: "cv-detail-back", onClick: onBack, children: [
|
|
1020
|
+
/* @__PURE__ */ jsx(ArrowLeft, { size: 16, "aria-hidden": true }),
|
|
1021
|
+
"Contactos"
|
|
1022
|
+
] }) }),
|
|
1023
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-detail-body", children: [
|
|
1024
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-detail-identity", children: [
|
|
1025
|
+
/* @__PURE__ */ jsx("span", { className: "cv-detail-avatar", "aria-hidden": true, children: initials(contact.name) }),
|
|
1026
|
+
/* @__PURE__ */ jsx("h2", { className: "cv-detail-name", children: name })
|
|
1027
|
+
] }),
|
|
1028
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-detail-rows", children: [
|
|
1029
|
+
/* @__PURE__ */ jsx(DetailRow, { icon: Phone, eyebrow: "Tel\xE9fono", text: formatted }),
|
|
1030
|
+
contactAction ? /* @__PURE__ */ jsx(
|
|
1031
|
+
DetailRow,
|
|
1032
|
+
{
|
|
1033
|
+
icon: contactAction.icon ?? ExternalLink,
|
|
1034
|
+
text: contactAction.label,
|
|
1035
|
+
onClick: () => contactAction.onClick(contact)
|
|
1036
|
+
}
|
|
1037
|
+
) : null
|
|
1038
|
+
] })
|
|
1039
|
+
] }),
|
|
1040
|
+
/* @__PURE__ */ jsx("div", { className: "cv-detail-foot", children: /* @__PURE__ */ jsxs(
|
|
1041
|
+
"button",
|
|
1042
|
+
{
|
|
1043
|
+
type: "button",
|
|
1044
|
+
className: "cv-btn cv-btn--accent cv-detail-call",
|
|
1045
|
+
onClick: () => void softphone.call(contact.phone),
|
|
1046
|
+
children: [
|
|
1047
|
+
/* @__PURE__ */ jsx(Phone, { size: 16, "aria-hidden": true }),
|
|
1048
|
+
"Llamar"
|
|
1049
|
+
]
|
|
1050
|
+
}
|
|
1051
|
+
) })
|
|
1052
|
+
] });
|
|
1053
|
+
}
|
|
1054
|
+
function useInfiniteList(loadPage) {
|
|
1055
|
+
const [items, setItems] = useState([]);
|
|
1056
|
+
const [loading2, setLoading] = useState(true);
|
|
1057
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
1058
|
+
const [hasMore, setHasMore] = useState(true);
|
|
1059
|
+
const [error, setError] = useState(false);
|
|
1060
|
+
const seqRef = useRef(0);
|
|
1061
|
+
const itemsRef = useRef(items);
|
|
1062
|
+
itemsRef.current = items;
|
|
1063
|
+
const loadingMoreRef = useRef(false);
|
|
1064
|
+
const hasMoreRef = useRef(true);
|
|
1065
|
+
useEffect(() => {
|
|
1066
|
+
const seq = ++seqRef.current;
|
|
1067
|
+
loadingMoreRef.current = false;
|
|
1068
|
+
hasMoreRef.current = true;
|
|
1069
|
+
setItems([]);
|
|
1070
|
+
setHasMore(true);
|
|
1071
|
+
setError(false);
|
|
1072
|
+
setLoading(true);
|
|
1073
|
+
void loadPage(0).then((page) => {
|
|
1074
|
+
if (seq !== seqRef.current) return;
|
|
1075
|
+
setItems(page.items);
|
|
1076
|
+
setHasMore(page.hasMore);
|
|
1077
|
+
hasMoreRef.current = page.hasMore;
|
|
1078
|
+
}).catch(() => {
|
|
1079
|
+
if (seq !== seqRef.current) return;
|
|
1080
|
+
setItems([]);
|
|
1081
|
+
setHasMore(false);
|
|
1082
|
+
hasMoreRef.current = false;
|
|
1083
|
+
setError(true);
|
|
1084
|
+
}).finally(() => {
|
|
1085
|
+
if (seq === seqRef.current) setLoading(false);
|
|
1086
|
+
});
|
|
1087
|
+
}, [loadPage]);
|
|
1088
|
+
const loadMore = useCallback(() => {
|
|
1089
|
+
if (loadingMoreRef.current || !hasMoreRef.current) return;
|
|
1090
|
+
const seq = seqRef.current;
|
|
1091
|
+
loadingMoreRef.current = true;
|
|
1092
|
+
setLoadingMore(true);
|
|
1093
|
+
void loadPage(itemsRef.current.length).then((page) => {
|
|
1094
|
+
if (seq !== seqRef.current) return;
|
|
1095
|
+
setItems((prev) => [...prev, ...page.items]);
|
|
1096
|
+
setHasMore(page.hasMore);
|
|
1097
|
+
hasMoreRef.current = page.hasMore;
|
|
1098
|
+
}).catch(() => {
|
|
1099
|
+
}).finally(() => {
|
|
1100
|
+
loadingMoreRef.current = false;
|
|
1101
|
+
if (seq === seqRef.current) setLoadingMore(false);
|
|
1102
|
+
});
|
|
1103
|
+
}, [loadPage]);
|
|
1104
|
+
const [containerEl, setContainerEl] = useState(null);
|
|
1105
|
+
const [sentinelEl, setSentinelEl] = useState(null);
|
|
1106
|
+
const loadMoreRef = useRef(loadMore);
|
|
1107
|
+
loadMoreRef.current = loadMore;
|
|
1108
|
+
useEffect(() => {
|
|
1109
|
+
if (!containerEl || !sentinelEl) return;
|
|
1110
|
+
const observer = new IntersectionObserver(
|
|
1111
|
+
([entry]) => {
|
|
1112
|
+
if (entry?.isIntersecting) loadMoreRef.current();
|
|
1113
|
+
},
|
|
1114
|
+
{ root: containerEl, rootMargin: "160px" }
|
|
1115
|
+
);
|
|
1116
|
+
observer.observe(sentinelEl);
|
|
1117
|
+
return () => observer.disconnect();
|
|
1118
|
+
}, [containerEl, sentinelEl]);
|
|
1119
|
+
return {
|
|
1120
|
+
items,
|
|
1121
|
+
loading: loading2,
|
|
1122
|
+
loadingMore,
|
|
1123
|
+
hasMore,
|
|
1124
|
+
error,
|
|
1125
|
+
containerRef: setContainerEl,
|
|
1126
|
+
sentinelRef: setSentinelEl
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
var PAGE = 50;
|
|
1130
|
+
function ContactsTab({
|
|
1131
|
+
source,
|
|
1132
|
+
contactAction
|
|
1133
|
+
}) {
|
|
1134
|
+
const softphone = useSoftphone();
|
|
1135
|
+
const [search, setSearch] = useState("");
|
|
1136
|
+
const [debouncedSearch, setDebouncedSearch] = useState("");
|
|
1137
|
+
const [selected, setSelected] = useState(null);
|
|
1138
|
+
useEffect(() => {
|
|
1139
|
+
const t = setTimeout(() => setDebouncedSearch(search), 200);
|
|
1140
|
+
return () => clearTimeout(t);
|
|
1141
|
+
}, [search]);
|
|
1142
|
+
const loadPage = useCallback(
|
|
1143
|
+
(skip) => source({ search: debouncedSearch, skip, limit: PAGE }).then((page) => ({
|
|
1144
|
+
items: page.items,
|
|
1145
|
+
hasMore: skip + page.items.length < page.total
|
|
1146
|
+
})),
|
|
1147
|
+
[source, debouncedSearch]
|
|
1148
|
+
);
|
|
1149
|
+
const {
|
|
1150
|
+
items,
|
|
1151
|
+
loading: loadingPage,
|
|
1152
|
+
loadingMore,
|
|
1153
|
+
hasMore,
|
|
1154
|
+
containerRef,
|
|
1155
|
+
sentinelRef
|
|
1156
|
+
} = useInfiniteList(loadPage);
|
|
1157
|
+
const loading2 = loadingPage || search !== debouncedSearch;
|
|
1158
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-view", children: [
|
|
1159
|
+
/* @__PURE__ */ jsx("div", { className: "cv-list-head", children: /* @__PURE__ */ jsxs("div", { className: "cv-search-field", children: [
|
|
1160
|
+
/* @__PURE__ */ jsx(Search, { className: "cv-search-icon", size: 15, "aria-hidden": true }),
|
|
1161
|
+
/* @__PURE__ */ jsx(
|
|
1162
|
+
"input",
|
|
1163
|
+
{
|
|
1164
|
+
className: "cv-search cv-search--icon",
|
|
1165
|
+
value: search,
|
|
1166
|
+
onChange: (e) => setSearch(e.target.value),
|
|
1167
|
+
placeholder: "Buscar contactos",
|
|
1168
|
+
"aria-label": "Buscar contactos"
|
|
1169
|
+
}
|
|
1170
|
+
),
|
|
1171
|
+
search ? /* @__PURE__ */ jsx(
|
|
1172
|
+
"button",
|
|
1173
|
+
{
|
|
1174
|
+
type: "button",
|
|
1175
|
+
className: "cv-search-clear",
|
|
1176
|
+
onClick: () => setSearch(""),
|
|
1177
|
+
"aria-label": "Limpiar b\xFAsqueda",
|
|
1178
|
+
children: /* @__PURE__ */ jsx(X, { size: 14 })
|
|
1179
|
+
}
|
|
1180
|
+
) : null
|
|
1181
|
+
] }) }),
|
|
1182
|
+
/* @__PURE__ */ jsx("div", { className: "cv-list", ref: containerRef, children: loading2 ? (
|
|
1183
|
+
// The spinner alone was silent to a screen reader — nothing else on this state carries
|
|
1184
|
+
// the word "loading" for it to announce. `role="status"` + real text fixes both the
|
|
1185
|
+
// accessibility gap and, incidentally, gives the icon something to be decorative NEXT TO
|
|
1186
|
+
// rather than being the sole carrier of the information itself.
|
|
1187
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-empty", role: "status", children: [
|
|
1188
|
+
/* @__PURE__ */ jsx(LoaderCircle, { className: "cv-empty-spinner", size: 18, "aria-hidden": true }),
|
|
1189
|
+
/* @__PURE__ */ jsx("span", { className: "cv-empty-title", children: "Cargando\u2026" })
|
|
1190
|
+
] })
|
|
1191
|
+
) : items.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "cv-empty", children: [
|
|
1192
|
+
/* @__PURE__ */ jsx("span", { className: "cv-empty-icon", "aria-hidden": true, children: /* @__PURE__ */ jsx(Users, { size: 20 }) }),
|
|
1193
|
+
/* @__PURE__ */ jsx("p", { className: "cv-empty-title", children: search ? "Sin resultados" : "No tienes contactos" }),
|
|
1194
|
+
/* @__PURE__ */ jsx("p", { className: "cv-empty-sub", children: search ? `Nada coincide con \xAB${search}\xBB.` : "Tus contactos con tel\xE9fono aparecer\xE1n aqu\xED." })
|
|
1195
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1196
|
+
items.map((c) => (
|
|
1197
|
+
// A row, not a button: the name area and the call action are separate controls, so
|
|
1198
|
+
// neither is nested inside the other (nested interactives are invalid + inaccessible).
|
|
1199
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-item", children: [
|
|
1200
|
+
/* @__PURE__ */ jsx("span", { className: "cv-item-avatar", "aria-hidden": true, children: initials(c.name) === "#" ? /* @__PURE__ */ jsx(User, { size: 17 }) : initials(c.name) }),
|
|
1201
|
+
/* @__PURE__ */ jsxs("button", { type: "button", className: "cv-item-main", onClick: () => setSelected(c), children: [
|
|
1202
|
+
/* @__PURE__ */ jsx("span", { className: "cv-item-name", children: c.name ?? c.phone }),
|
|
1203
|
+
/* @__PURE__ */ jsx("span", { className: "cv-item-sub", children: c.phone })
|
|
1204
|
+
] }),
|
|
1205
|
+
/* @__PURE__ */ jsx(
|
|
1206
|
+
"button",
|
|
1207
|
+
{
|
|
1208
|
+
type: "button",
|
|
1209
|
+
className: "cv-item-action",
|
|
1210
|
+
"data-tone": "accent",
|
|
1211
|
+
"aria-label": `Llamar a ${c.name ?? c.phone}`,
|
|
1212
|
+
onClick: () => void softphone.call(c.phone),
|
|
1213
|
+
children: /* @__PURE__ */ jsx(Phone, { size: 16 })
|
|
1214
|
+
}
|
|
1215
|
+
)
|
|
1216
|
+
] }, c.id)
|
|
1217
|
+
)),
|
|
1218
|
+
hasMore ? /* @__PURE__ */ jsx("div", { ref: sentinelRef, className: "cv-list-sentinel", "aria-hidden": true }) : null,
|
|
1219
|
+
loadingMore ? /* @__PURE__ */ jsx("div", { className: "cv-list-loading-more", role: "status", "aria-label": "Cargando m\xE1s contactos", children: /* @__PURE__ */ jsx(LoaderCircle, { className: "cv-list-loading-more-spinner", size: 14, "aria-hidden": true }) }) : null
|
|
1220
|
+
] }) }),
|
|
1221
|
+
selected ? /* @__PURE__ */ jsx(
|
|
1222
|
+
ContactDetail,
|
|
1223
|
+
{
|
|
1224
|
+
contact: selected,
|
|
1225
|
+
onBack: () => setSelected(null),
|
|
1226
|
+
...contactAction ? { contactAction } : {}
|
|
1227
|
+
}
|
|
1228
|
+
) : null
|
|
1229
|
+
] });
|
|
1230
|
+
}
|
|
1231
|
+
var OUTCOME_LABELS = {
|
|
1232
|
+
"no-answer": "No contestada",
|
|
1233
|
+
busy: "Ocupada",
|
|
1234
|
+
cancelled: "Cancelada",
|
|
1235
|
+
failed: "Fallida",
|
|
1236
|
+
unavailable: "No disponible"
|
|
1237
|
+
};
|
|
1238
|
+
function timeLabel(at) {
|
|
1239
|
+
return at.toLocaleTimeString("es-ES", { hour: "2-digit", minute: "2-digit" });
|
|
1240
|
+
}
|
|
1241
|
+
function fullDateLabel(at) {
|
|
1242
|
+
return at.toLocaleString("es-ES", {
|
|
1243
|
+
day: "2-digit",
|
|
1244
|
+
month: "long",
|
|
1245
|
+
year: "numeric",
|
|
1246
|
+
hour: "2-digit",
|
|
1247
|
+
minute: "2-digit"
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
function Icon2({ entry }) {
|
|
1251
|
+
if (entry.missed) return /* @__PURE__ */ jsx(PhoneMissed, { size: 16 });
|
|
1252
|
+
return entry.direction === "inbound" ? /* @__PURE__ */ jsx(PhoneIncoming, { size: 16 }) : /* @__PURE__ */ jsx(PhoneOutgoing, { size: 16 });
|
|
1253
|
+
}
|
|
1254
|
+
function statusIcon(entry) {
|
|
1255
|
+
if (entry.missed) return PhoneMissed;
|
|
1256
|
+
return entry.direction === "inbound" ? PhoneIncoming : PhoneOutgoing;
|
|
1257
|
+
}
|
|
1258
|
+
function statusTone(entry) {
|
|
1259
|
+
if (entry.missed) return "danger";
|
|
1260
|
+
return entry.direction === "inbound" ? "accent" : "info";
|
|
1261
|
+
}
|
|
1262
|
+
function HandlerChain({ handlers }) {
|
|
1263
|
+
return /* @__PURE__ */ jsx("span", { className: "cv-detail-handlers", children: handlers.map((h, i) => {
|
|
1264
|
+
const isLast = i === handlers.length - 1;
|
|
1265
|
+
const label = h.name ?? `Ext. ${h.extension}`;
|
|
1266
|
+
return /* @__PURE__ */ jsxs("span", { className: "cv-detail-handler-hop", children: [
|
|
1267
|
+
i > 0 ? /* @__PURE__ */ jsx(ArrowRight, { size: 12, className: "cv-detail-handler-arrow", "aria-hidden": true }) : null,
|
|
1268
|
+
/* @__PURE__ */ jsxs("span", { className: "cv-detail-handler-chip", "data-tone": isLast ? "accent" : void 0, children: [
|
|
1269
|
+
label,
|
|
1270
|
+
h.name ? /* @__PURE__ */ jsxs("span", { className: "cv-detail-handler-ext", children: [
|
|
1271
|
+
"\xB7 ",
|
|
1272
|
+
h.extension
|
|
1273
|
+
] }) : null
|
|
1274
|
+
] })
|
|
1275
|
+
] }, `${h.extension}-${i}`);
|
|
1276
|
+
}) });
|
|
1277
|
+
}
|
|
1278
|
+
function HistoryDetail({
|
|
1279
|
+
entry,
|
|
1280
|
+
onBack
|
|
1281
|
+
}) {
|
|
1282
|
+
const softphone = useSoftphone();
|
|
1283
|
+
const formatIntl2 = useIntlFormatter();
|
|
1284
|
+
const [detail, setDetail] = useState(null);
|
|
1285
|
+
const [loadingDetail, setLoadingDetail] = useState(false);
|
|
1286
|
+
useEffect(() => {
|
|
1287
|
+
const load = entry.loadDetail;
|
|
1288
|
+
if (!load) return;
|
|
1289
|
+
let cancelled = false;
|
|
1290
|
+
setLoadingDetail(true);
|
|
1291
|
+
void load().then((d) => !cancelled && setDetail(d)).catch(() => {
|
|
1292
|
+
}).finally(() => !cancelled && setLoadingDetail(false));
|
|
1293
|
+
return () => {
|
|
1294
|
+
cancelled = true;
|
|
1295
|
+
};
|
|
1296
|
+
}, [entry]);
|
|
1297
|
+
const formatted = formatIntl2(entry.number);
|
|
1298
|
+
const name = entry.name ?? formatted;
|
|
1299
|
+
const handledBy = detail?.handledBy ?? entry.handledBy;
|
|
1300
|
+
const recordingUrls = detail?.recordingUrls ?? [];
|
|
1301
|
+
const loadingRecording = entry.recorded && loadingDetail && recordingUrls.length === 0;
|
|
1302
|
+
const statusText = entry.missed ? entry.outcome ? OUTCOME_LABELS[entry.outcome] : "Perdida" : "Contestada";
|
|
1303
|
+
const handlerLabel = handledBy.length > 1 ? "Ruta de la llamada" : entry.direction === "outbound" ? "Realizada por" : "Atendida por";
|
|
1304
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-detail", role: "region", "aria-label": name, children: [
|
|
1305
|
+
/* @__PURE__ */ jsx("div", { className: "cv-detail-head", children: /* @__PURE__ */ jsxs("button", { type: "button", className: "cv-detail-back", onClick: onBack, children: [
|
|
1306
|
+
/* @__PURE__ */ jsx(ArrowLeft, { size: 16, "aria-hidden": true }),
|
|
1307
|
+
"Historial"
|
|
1308
|
+
] }) }),
|
|
1309
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-detail-body", children: [
|
|
1310
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-detail-identity", children: [
|
|
1311
|
+
/* @__PURE__ */ jsx("span", { className: "cv-detail-avatar", "data-tone": statusTone(entry), "aria-hidden": true, children: /* @__PURE__ */ jsx(Icon2, { entry }) }),
|
|
1312
|
+
/* @__PURE__ */ jsx("h2", { className: "cv-detail-name", "data-tone": entry.missed ? "danger" : void 0, children: name }),
|
|
1313
|
+
/* @__PURE__ */ jsxs("p", { className: "cv-detail-meta", children: [
|
|
1314
|
+
statusText,
|
|
1315
|
+
" \xB7 ",
|
|
1316
|
+
timeLabel(entry.at)
|
|
1317
|
+
] })
|
|
1318
|
+
] }),
|
|
1319
|
+
entry.recorded ? /* @__PURE__ */ jsx("div", { className: "cv-detail-recording", children: loadingRecording ? /* @__PURE__ */ jsxs("div", { className: "cv-detail-recording-status", children: [
|
|
1320
|
+
/* @__PURE__ */ jsx(LoaderCircle, { size: 14, className: "cv-detail-recording-spinner", "aria-hidden": true }),
|
|
1321
|
+
"Cargando grabaci\xF3n\u2026"
|
|
1322
|
+
] }) : recordingUrls.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1323
|
+
/* @__PURE__ */ jsx("p", { className: "cv-detail-recording-label", children: recordingUrls.length === 1 ? "Grabaci\xF3n" : "Grabaciones" }),
|
|
1324
|
+
recordingUrls.map((url) => /* @__PURE__ */ jsx("audio", { controls: true, src: url, className: "cv-detail-audio", preload: "metadata" }, url))
|
|
1325
|
+
] }) : /* @__PURE__ */ jsx("p", { className: "cv-detail-recording-status cv-detail-recording-status--warn", children: "No se pudo cargar la grabaci\xF3n ahora. Int\xE9ntalo de nuevo m\xE1s tarde." }) }) : /* @__PURE__ */ jsxs("p", { className: "cv-detail-recording-status", children: [
|
|
1326
|
+
/* @__PURE__ */ jsx(Mic, { size: 14, "aria-hidden": true }),
|
|
1327
|
+
"Esta llamada no se grab\xF3."
|
|
1328
|
+
] }),
|
|
1329
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-detail-rows", children: [
|
|
1330
|
+
/* @__PURE__ */ jsx(DetailRow, { icon: statusIcon(entry), eyebrow: "Estado", text: statusText }),
|
|
1331
|
+
/* @__PURE__ */ jsx(DetailRow, { icon: CalendarClock, eyebrow: "Fecha y hora", text: fullDateLabel(entry.at) }),
|
|
1332
|
+
entry.durationSec > 0 ? /* @__PURE__ */ jsx(DetailRow, { icon: Timer, eyebrow: "Duraci\xF3n", text: formatDuration(entry.durationSec) }) : null,
|
|
1333
|
+
/* @__PURE__ */ jsx(DetailRow, { icon: Phone, eyebrow: "N\xFAmero", text: formatted }),
|
|
1334
|
+
handledBy.length > 0 ? /* @__PURE__ */ jsx(DetailRow, { icon: User, eyebrow: handlerLabel, text: /* @__PURE__ */ jsx(HandlerChain, { handlers: handledBy }) }) : null,
|
|
1335
|
+
entry.direction === "inbound" && entry.did ? /* @__PURE__ */ jsx(DetailRow, { icon: Hash, eyebrow: "N\xFAmero marcado (DID)", text: formatIntl2(entry.did) }) : null
|
|
1336
|
+
] })
|
|
1337
|
+
] }),
|
|
1338
|
+
/* @__PURE__ */ jsx("div", { className: "cv-detail-foot", children: /* @__PURE__ */ jsxs(
|
|
1339
|
+
"button",
|
|
1340
|
+
{
|
|
1341
|
+
type: "button",
|
|
1342
|
+
className: "cv-btn cv-btn--accent cv-detail-call",
|
|
1343
|
+
onClick: () => void softphone.call(entry.number),
|
|
1344
|
+
children: [
|
|
1345
|
+
/* @__PURE__ */ jsx(Phone, { size: 16, "aria-hidden": true }),
|
|
1346
|
+
"Llamar"
|
|
1347
|
+
]
|
|
1348
|
+
}
|
|
1349
|
+
) })
|
|
1350
|
+
] });
|
|
1351
|
+
}
|
|
1352
|
+
var PAGE2 = 30;
|
|
1353
|
+
function startOfDay(at) {
|
|
1354
|
+
return new Date(at.getFullYear(), at.getMonth(), at.getDate()).getTime();
|
|
1355
|
+
}
|
|
1356
|
+
function dayLabel(at, now) {
|
|
1357
|
+
const diffDays = Math.round((startOfDay(new Date(now)) - startOfDay(at)) / 864e5);
|
|
1358
|
+
if (diffDays === 0) return "Hoy";
|
|
1359
|
+
if (diffDays === 1) return "Ayer";
|
|
1360
|
+
return at.toLocaleDateString("es-ES", { day: "numeric", month: "long" });
|
|
1361
|
+
}
|
|
1362
|
+
function groupByDay(entries, now) {
|
|
1363
|
+
const groups = [];
|
|
1364
|
+
for (const e of entries) {
|
|
1365
|
+
const label = dayLabel(e.at, now);
|
|
1366
|
+
const last = groups.at(-1);
|
|
1367
|
+
if (last && last.label === label) last.rows.push(e);
|
|
1368
|
+
else groups.push({ label, rows: [e] });
|
|
1369
|
+
}
|
|
1370
|
+
return groups;
|
|
1371
|
+
}
|
|
1372
|
+
function PersonFilter({
|
|
1373
|
+
value,
|
|
1374
|
+
directory,
|
|
1375
|
+
onChange
|
|
1376
|
+
}) {
|
|
1377
|
+
const [open, setOpen] = useState(false);
|
|
1378
|
+
const rootRef = useRef(null);
|
|
1379
|
+
useEffect(() => {
|
|
1380
|
+
if (!open) return;
|
|
1381
|
+
const root = rootRef.current?.getRootNode() ?? document;
|
|
1382
|
+
const onDown = (e) => {
|
|
1383
|
+
if (!rootRef.current?.contains(e.target)) setOpen(false);
|
|
1384
|
+
};
|
|
1385
|
+
const onKey = (e) => {
|
|
1386
|
+
if (e.key === "Escape") setOpen(false);
|
|
1387
|
+
};
|
|
1388
|
+
root.addEventListener("pointerdown", onDown);
|
|
1389
|
+
document.addEventListener("keydown", onKey);
|
|
1390
|
+
return () => {
|
|
1391
|
+
root.removeEventListener("pointerdown", onDown);
|
|
1392
|
+
document.removeEventListener("keydown", onKey);
|
|
1393
|
+
};
|
|
1394
|
+
}, [open]);
|
|
1395
|
+
const options = [
|
|
1396
|
+
{ v: "", label: "Todas las personas" },
|
|
1397
|
+
...directory.me ? [{ v: directory.me.userId, label: `Mis llamadas \xB7 ${directory.me.name ?? "Yo"}` }] : [],
|
|
1398
|
+
...directory.people.filter((p) => !directory.me || p.userId !== directory.me.userId).map((p) => ({ v: p.userId, label: p.name ?? "Sin nombre" }))
|
|
1399
|
+
];
|
|
1400
|
+
const label = options.find((o) => o.v === value)?.label ?? "Todas las personas";
|
|
1401
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-person-filter", ref: rootRef, children: [
|
|
1402
|
+
/* @__PURE__ */ jsxs(
|
|
1403
|
+
"button",
|
|
1404
|
+
{
|
|
1405
|
+
type: "button",
|
|
1406
|
+
className: "cv-person-filter-chip",
|
|
1407
|
+
onClick: () => setOpen((o) => !o),
|
|
1408
|
+
"aria-haspopup": "listbox",
|
|
1409
|
+
"aria-expanded": open,
|
|
1410
|
+
children: [
|
|
1411
|
+
/* @__PURE__ */ jsx(Users, { size: 13, "aria-hidden": true }),
|
|
1412
|
+
/* @__PURE__ */ jsx("span", { className: "cv-person-filter-label", children: label }),
|
|
1413
|
+
/* @__PURE__ */ jsx(ChevronDown, { size: 13, className: "cv-person-filter-caret", "data-open": open, "aria-hidden": true })
|
|
1414
|
+
]
|
|
1415
|
+
}
|
|
1416
|
+
),
|
|
1417
|
+
open ? /* @__PURE__ */ jsx("div", { className: "cv-person-filter-pop", role: "listbox", children: options.map((opt) => /* @__PURE__ */ jsxs(
|
|
1418
|
+
"button",
|
|
1419
|
+
{
|
|
1420
|
+
type: "button",
|
|
1421
|
+
role: "option",
|
|
1422
|
+
"aria-selected": opt.v === value,
|
|
1423
|
+
className: "cv-person-filter-item",
|
|
1424
|
+
onClick: () => {
|
|
1425
|
+
onChange(opt.v);
|
|
1426
|
+
setOpen(false);
|
|
1427
|
+
},
|
|
1428
|
+
children: [
|
|
1429
|
+
/* @__PURE__ */ jsx("span", { className: "cv-person-filter-name", children: opt.label }),
|
|
1430
|
+
/* @__PURE__ */ jsx(Check, { size: 14, className: "cv-person-filter-check", "data-on": opt.v === value, "aria-hidden": true })
|
|
1431
|
+
]
|
|
1432
|
+
},
|
|
1433
|
+
opt.v || "all"
|
|
1434
|
+
)) }) : null
|
|
1435
|
+
] });
|
|
1436
|
+
}
|
|
1437
|
+
function HistoryTab({ source }) {
|
|
1438
|
+
const softphone = useSoftphone();
|
|
1439
|
+
const formatIntl2 = useIntlFormatter();
|
|
1440
|
+
const now = Date.now();
|
|
1441
|
+
const [status, setStatus] = useState("all");
|
|
1442
|
+
const [agentId, setAgentId] = useState("");
|
|
1443
|
+
const [directory, setDirectory] = useState(null);
|
|
1444
|
+
const [selected, setSelected] = useState(null);
|
|
1445
|
+
useEffect(() => {
|
|
1446
|
+
const listAgents = softphone.sources?.listAgents;
|
|
1447
|
+
if (!listAgents) return;
|
|
1448
|
+
let cancelled = false;
|
|
1449
|
+
void listAgents().then((d) => !cancelled && setDirectory(d)).catch(() => {
|
|
1450
|
+
});
|
|
1451
|
+
return () => {
|
|
1452
|
+
cancelled = true;
|
|
1453
|
+
};
|
|
1454
|
+
}, [softphone]);
|
|
1455
|
+
const loadPage = useCallback(
|
|
1456
|
+
(skip) => source({
|
|
1457
|
+
skip,
|
|
1458
|
+
limit: PAGE2,
|
|
1459
|
+
missed: status === "missed",
|
|
1460
|
+
...agentId ? { userId: agentId } : {}
|
|
1461
|
+
}).then((rows) => ({ items: rows, hasMore: rows.length === PAGE2 })),
|
|
1462
|
+
[source, status, agentId]
|
|
1463
|
+
);
|
|
1464
|
+
const { items: entries, loading: loading2, loadingMore, hasMore, containerRef, sentinelRef } = useInfiniteList(loadPage);
|
|
1465
|
+
const groups = groupByDay(entries, now);
|
|
1466
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-view", children: [
|
|
1467
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-list-head", children: [
|
|
1468
|
+
/* @__PURE__ */ jsx("div", { className: "cv-segmented", role: "tablist", "aria-label": "Filtrar por estado", children: ["all", "missed"].map((key) => /* @__PURE__ */ jsx(
|
|
1469
|
+
"button",
|
|
1470
|
+
{
|
|
1471
|
+
type: "button",
|
|
1472
|
+
role: "tab",
|
|
1473
|
+
"aria-selected": status === key,
|
|
1474
|
+
className: "cv-segmented-btn",
|
|
1475
|
+
onClick: () => setStatus(key),
|
|
1476
|
+
children: key === "all" ? "Todas" : "Perdidas"
|
|
1477
|
+
},
|
|
1478
|
+
key
|
|
1479
|
+
)) }),
|
|
1480
|
+
directory ? /* @__PURE__ */ jsx(PersonFilter, { value: agentId, directory, onChange: setAgentId }) : null
|
|
1481
|
+
] }),
|
|
1482
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-list", ref: containerRef, children: [
|
|
1483
|
+
loading2 && /* @__PURE__ */ jsx("div", { className: "cv-empty", children: "Cargando\u2026" }),
|
|
1484
|
+
!loading2 && entries.length === 0 && /* @__PURE__ */ jsx("div", { className: "cv-empty", children: status === "missed" ? "Sin llamadas perdidas" : "Sin llamadas recientes" }),
|
|
1485
|
+
!loading2 && groups.map((group) => /* @__PURE__ */ jsxs("div", { children: [
|
|
1486
|
+
/* @__PURE__ */ jsx("div", { className: "cv-history-day", children: group.label }),
|
|
1487
|
+
group.rows.map((e) => /* @__PURE__ */ jsxs("div", { className: "cv-item", children: [
|
|
1488
|
+
/* @__PURE__ */ jsx("span", { className: "cv-item-avatar", "data-tone": statusTone(e), "aria-hidden": true, children: /* @__PURE__ */ jsx(Icon2, { entry: e }) }),
|
|
1489
|
+
/* @__PURE__ */ jsxs(
|
|
1490
|
+
"button",
|
|
1491
|
+
{
|
|
1492
|
+
type: "button",
|
|
1493
|
+
className: "cv-item-main",
|
|
1494
|
+
onClick: () => setSelected(e),
|
|
1495
|
+
children: [
|
|
1496
|
+
/* @__PURE__ */ jsx("span", { className: "cv-item-name", "data-tone": e.missed ? "danger" : void 0, children: e.name ?? formatIntl2(e.number) }),
|
|
1497
|
+
/* @__PURE__ */ jsxs("span", { className: "cv-item-sub", children: [
|
|
1498
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums", children: timeLabel(e.at) }),
|
|
1499
|
+
e.missed ? /* @__PURE__ */ jsxs("span", { className: "cv-item-sub-outcome", children: [
|
|
1500
|
+
" ",
|
|
1501
|
+
"\xB7 ",
|
|
1502
|
+
e.outcome ? OUTCOME_LABELS[e.outcome] : "Perdida"
|
|
1503
|
+
] }) : e.durationSec > 0 && ` \xB7 ${formatDuration(e.durationSec)}`,
|
|
1504
|
+
e.recorded ? /* @__PURE__ */ jsx(Mic, { size: 11, className: "cv-item-sub-rec", "aria-label": "Grabada" }) : null
|
|
1505
|
+
] })
|
|
1506
|
+
]
|
|
1507
|
+
}
|
|
1508
|
+
),
|
|
1509
|
+
/* @__PURE__ */ jsx(
|
|
1510
|
+
"button",
|
|
1511
|
+
{
|
|
1512
|
+
type: "button",
|
|
1513
|
+
className: "cv-item-action",
|
|
1514
|
+
"data-tone": "accent",
|
|
1515
|
+
"aria-label": `Llamar a ${e.name ?? e.number}`,
|
|
1516
|
+
onClick: () => void softphone.call(e.number),
|
|
1517
|
+
children: /* @__PURE__ */ jsx(Phone, { size: 16 })
|
|
1518
|
+
}
|
|
1519
|
+
)
|
|
1520
|
+
] }, e.id))
|
|
1521
|
+
] }, group.label)),
|
|
1522
|
+
!loading2 && hasMore ? /* @__PURE__ */ jsx("div", { ref: sentinelRef, className: "cv-list-sentinel", "aria-hidden": true }) : null,
|
|
1523
|
+
loadingMore ? /* @__PURE__ */ jsx("div", { className: "cv-list-loading-more", role: "status", "aria-label": "Cargando m\xE1s llamadas", children: /* @__PURE__ */ jsx(LoaderCircle, { className: "cv-list-loading-more-spinner", size: 14, "aria-hidden": true }) }) : null
|
|
1524
|
+
] }),
|
|
1525
|
+
selected ? /* @__PURE__ */ jsx(HistoryDetail, { entry: selected, onBack: () => setSelected(null) }) : null
|
|
1526
|
+
] });
|
|
1527
|
+
}
|
|
1528
|
+
function SettingsSection({
|
|
1529
|
+
title,
|
|
1530
|
+
children
|
|
1531
|
+
}) {
|
|
1532
|
+
return /* @__PURE__ */ jsxs("section", { className: "cv-settings-section", children: [
|
|
1533
|
+
/* @__PURE__ */ jsx("h3", { className: "cv-settings-section-title", children: title }),
|
|
1534
|
+
/* @__PURE__ */ jsx("div", { className: "cv-settings-section-body", children })
|
|
1535
|
+
] });
|
|
1536
|
+
}
|
|
1537
|
+
function SettingsTab() {
|
|
1538
|
+
return /* @__PURE__ */ jsx("div", { className: "cv-view cv-settings", children: /* @__PURE__ */ jsxs("div", { className: "cv-settings-body", children: [
|
|
1539
|
+
/* @__PURE__ */ jsx(SettingsSection, { title: "Dispositivos", children: /* @__PURE__ */ jsx(DevicePickers, {}) }),
|
|
1540
|
+
/* @__PURE__ */ jsx("p", { className: "cv-settings-more", children: "M\xE1s ajustes se ir\xE1n a\xF1adiendo pr\xF3ximamente." })
|
|
1541
|
+
] }) });
|
|
1542
|
+
}
|
|
1543
|
+
function useMicPermission() {
|
|
1544
|
+
const [state, setState] = useState("checking");
|
|
1545
|
+
const warm = useCallback(async () => {
|
|
1546
|
+
try {
|
|
1547
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
1548
|
+
for (const track of stream.getTracks()) track.stop();
|
|
1549
|
+
setState("ready");
|
|
1550
|
+
} catch {
|
|
1551
|
+
setState("denied");
|
|
1552
|
+
}
|
|
1553
|
+
}, []);
|
|
1554
|
+
useEffect(() => {
|
|
1555
|
+
if (!isWebRtcSupported()) {
|
|
1556
|
+
setState("unsupported");
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
let cancelled = false;
|
|
1560
|
+
let status = null;
|
|
1561
|
+
const apply = (permission) => {
|
|
1562
|
+
if (cancelled) return;
|
|
1563
|
+
if (permission === "denied") {
|
|
1564
|
+
setState("denied");
|
|
1565
|
+
} else if (permission === "granted") {
|
|
1566
|
+
setState("warming");
|
|
1567
|
+
void warm();
|
|
1568
|
+
} else {
|
|
1569
|
+
setState("prompting");
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
void (async () => {
|
|
1573
|
+
try {
|
|
1574
|
+
status = await navigator.permissions.query({ name: "microphone" });
|
|
1575
|
+
if (cancelled) return;
|
|
1576
|
+
apply(status.state);
|
|
1577
|
+
status.onchange = () => status && apply(status.state);
|
|
1578
|
+
} catch {
|
|
1579
|
+
if (!cancelled) setState("prompting");
|
|
1580
|
+
}
|
|
1581
|
+
})();
|
|
1582
|
+
return () => {
|
|
1583
|
+
cancelled = true;
|
|
1584
|
+
if (status) status.onchange = null;
|
|
1585
|
+
};
|
|
1586
|
+
}, [warm]);
|
|
1587
|
+
return { state, request: warm };
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// src/ui/permission/unblock-mic.ts
|
|
1591
|
+
function detectBrowser(ua) {
|
|
1592
|
+
if (/firefox|fxios/i.test(ua)) return "firefox";
|
|
1593
|
+
if (/edg\//i.test(ua)) return "edge";
|
|
1594
|
+
if (/chrome|chromium|crios/i.test(ua)) return "chromium";
|
|
1595
|
+
if (/safari/i.test(ua)) return "safari";
|
|
1596
|
+
return "unknown";
|
|
1597
|
+
}
|
|
1598
|
+
var CHROME_HELP = "https://support.google.com/chrome/answer/2693767?hl=es";
|
|
1599
|
+
var GUIDES = {
|
|
1600
|
+
chromium: {
|
|
1601
|
+
steps: [
|
|
1602
|
+
"Haz clic en el icono \u{1F512} a la izquierda de la direcci\xF3n web.",
|
|
1603
|
+
"Busca \xABMicr\xF3fono\xBB y c\xE1mbialo a \xABPermitir\xBB.",
|
|
1604
|
+
"Recarga la p\xE1gina."
|
|
1605
|
+
],
|
|
1606
|
+
url: CHROME_HELP
|
|
1607
|
+
},
|
|
1608
|
+
// Edge is Chromium: same padlock, same panel. The article is Chrome's, so it is offered but not
|
|
1609
|
+
// led with — the steps above match what an Edge user actually sees.
|
|
1610
|
+
edge: {
|
|
1611
|
+
steps: [
|
|
1612
|
+
"Haz clic en el icono \u{1F512} a la izquierda de la direcci\xF3n web.",
|
|
1613
|
+
"Busca \xABMicr\xF3fono\xBB y c\xE1mbialo a \xABPermitir\xBB.",
|
|
1614
|
+
"Recarga la p\xE1gina."
|
|
1615
|
+
]
|
|
1616
|
+
},
|
|
1617
|
+
firefox: {
|
|
1618
|
+
steps: [
|
|
1619
|
+
"Haz clic en el icono de micr\xF3fono tachado en la barra de direcciones.",
|
|
1620
|
+
"Pulsa la \xAB\u2715\xBB junto a \xABBloqueado\xBB para quitar el bloqueo.",
|
|
1621
|
+
"Recarga la p\xE1gina."
|
|
1622
|
+
]
|
|
1623
|
+
},
|
|
1624
|
+
safari: {
|
|
1625
|
+
steps: [
|
|
1626
|
+
"Abre \xABSafari\xBB \u2192 \xABAjustes para este sitio web\xBB.",
|
|
1627
|
+
"En \xABMicr\xF3fono\xBB, elige \xABPermitir\xBB.",
|
|
1628
|
+
"Recarga la p\xE1gina."
|
|
1629
|
+
]
|
|
1630
|
+
},
|
|
1631
|
+
unknown: {
|
|
1632
|
+
steps: [
|
|
1633
|
+
"Abre los permisos de este sitio en tu navegador.",
|
|
1634
|
+
"Cambia el micr\xF3fono a \xABPermitir\xBB.",
|
|
1635
|
+
"Recarga la p\xE1gina."
|
|
1636
|
+
]
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
function unblockGuide() {
|
|
1640
|
+
const ua = typeof navigator === "undefined" ? "" : navigator.userAgent;
|
|
1641
|
+
return GUIDES[detectBrowser(ua)];
|
|
1642
|
+
}
|
|
1643
|
+
function MicPrompt({
|
|
1644
|
+
state,
|
|
1645
|
+
onRequest
|
|
1646
|
+
}) {
|
|
1647
|
+
if (state === "unsupported") {
|
|
1648
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-gate", "data-state": state, "data-tone": "danger", children: [
|
|
1649
|
+
/* @__PURE__ */ jsx("span", { className: "cv-gate-mark", children: /* @__PURE__ */ jsx(MicOff, { size: 22 }) }),
|
|
1650
|
+
/* @__PURE__ */ jsx("h3", { className: "cv-gate-title", children: "Llamadas no disponibles" }),
|
|
1651
|
+
/* @__PURE__ */ jsx("p", { children: "Este navegador no admite llamadas. Prueba con Chrome, Edge o Firefox." })
|
|
1652
|
+
] });
|
|
1653
|
+
}
|
|
1654
|
+
if (state === "denied") {
|
|
1655
|
+
const guide = unblockGuide();
|
|
1656
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-gate", "data-state": state, "data-tone": "danger", children: [
|
|
1657
|
+
/* @__PURE__ */ jsx("span", { className: "cv-gate-mark", children: /* @__PURE__ */ jsx(MicOff, { size: 22 }) }),
|
|
1658
|
+
/* @__PURE__ */ jsx("h3", { className: "cv-gate-title", children: "Micr\xF3fono bloqueado" }),
|
|
1659
|
+
/* @__PURE__ */ jsx("p", { children: "Perm\xEDtelo en tu navegador para poder llamar." }),
|
|
1660
|
+
/* @__PURE__ */ jsxs("details", { className: "cv-help", children: [
|
|
1661
|
+
/* @__PURE__ */ jsxs("summary", { children: [
|
|
1662
|
+
/* @__PURE__ */ jsx(CircleHelp, { size: 13, "aria-hidden": true }),
|
|
1663
|
+
"C\xF3mo permitirlo"
|
|
1664
|
+
] }),
|
|
1665
|
+
/* @__PURE__ */ jsx("ol", { children: guide.steps.map((step) => /* @__PURE__ */ jsx("li", { children: step }, step)) }),
|
|
1666
|
+
guide.url ? /* @__PURE__ */ jsxs("a", { href: guide.url, target: "_blank", rel: "noreferrer noopener", children: [
|
|
1667
|
+
"Gu\xEDa del navegador",
|
|
1668
|
+
/* @__PURE__ */ jsx(ExternalLink, { size: 11, "aria-hidden": true })
|
|
1669
|
+
] }) : null
|
|
1670
|
+
] })
|
|
1671
|
+
] });
|
|
1672
|
+
}
|
|
1673
|
+
if (state === "warming") {
|
|
1674
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-gate", "data-state": state, children: [
|
|
1675
|
+
/* @__PURE__ */ jsx("span", { className: "cv-gate-mark", children: /* @__PURE__ */ jsx(Mic, { size: 22 }) }),
|
|
1676
|
+
/* @__PURE__ */ jsx("h3", { className: "cv-gate-title", children: "Permite el micr\xF3fono" }),
|
|
1677
|
+
/* @__PURE__ */ jsx("p", { children: "Acepta el aviso que muestra el navegador para continuar." })
|
|
1678
|
+
] });
|
|
1679
|
+
}
|
|
1680
|
+
if (state === "checking") {
|
|
1681
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-gate", "data-state": state, children: [
|
|
1682
|
+
/* @__PURE__ */ jsx("span", { className: "cv-gate-mark", children: /* @__PURE__ */ jsx(Mic, { size: 22 }) }),
|
|
1683
|
+
/* @__PURE__ */ jsx("p", { children: "Preparando el micr\xF3fono\u2026" })
|
|
1684
|
+
] });
|
|
1685
|
+
}
|
|
1686
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-gate", "data-state": state, children: [
|
|
1687
|
+
/* @__PURE__ */ jsx("span", { className: "cv-gate-mark", children: /* @__PURE__ */ jsx(Mic, { size: 22 }) }),
|
|
1688
|
+
/* @__PURE__ */ jsx("h3", { className: "cv-gate-title", children: "Permite el micr\xF3fono" }),
|
|
1689
|
+
/* @__PURE__ */ jsx("p", { children: "Lo necesitamos para que puedas hablar durante las llamadas." }),
|
|
1690
|
+
/* @__PURE__ */ jsx("button", { className: "cv-btn cv-btn--accent", onClick: onRequest, children: "Permitir micr\xF3fono" })
|
|
1691
|
+
] });
|
|
1692
|
+
}
|
|
1693
|
+
function MicPermissionGate({ children }) {
|
|
1694
|
+
const { state, request } = useMicPermission();
|
|
1695
|
+
if (state === "ready") return /* @__PURE__ */ jsx(Fragment, { children });
|
|
1696
|
+
return /* @__PURE__ */ jsx("div", { className: "cv-softphone", children: /* @__PURE__ */ jsx(MicPrompt, { state, onRequest: () => void request() }) });
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// src/ui/lib/error-text.ts
|
|
1700
|
+
var ERROR_TEXT = {
|
|
1701
|
+
// ── Engine lifecycle ────────────────────────────────────────────────────────
|
|
1702
|
+
// Integrator mistakes rather than user-facing failures, but they must still read as something
|
|
1703
|
+
// rather than as a blank panel if one ever reaches the UI.
|
|
1704
|
+
"engine/duplicate-instance": {
|
|
1705
|
+
title: "Tel\xE9fono duplicado",
|
|
1706
|
+
body: "Hay otro tel\xE9fono abierto en esta p\xE1gina. Cierra uno y vuelve a cargar."
|
|
1707
|
+
},
|
|
1708
|
+
"engine/not-started": { title: "El tel\xE9fono a\xFAn no est\xE1 listo" },
|
|
1709
|
+
"engine/destroyed": {
|
|
1710
|
+
title: "Tel\xE9fono cerrado",
|
|
1711
|
+
body: "Vuelve a cargar la p\xE1gina para usarlo de nuevo."
|
|
1712
|
+
},
|
|
1713
|
+
// ── Auth ────────────────────────────────────────────────────────────────────
|
|
1714
|
+
// Never "token": the user has no idea what one is. What they can act on is the session.
|
|
1715
|
+
"auth/token-fetch-failed": {
|
|
1716
|
+
title: "Sin conexi\xF3n con el servidor",
|
|
1717
|
+
body: "No se ha podido activar el tel\xE9fono. Reint\xE9ntalo en unos segundos."
|
|
1718
|
+
},
|
|
1719
|
+
"auth/token-rejected": {
|
|
1720
|
+
title: "Sesi\xF3n caducada",
|
|
1721
|
+
body: "Vuelve a iniciar sesi\xF3n para poder llamar."
|
|
1722
|
+
},
|
|
1723
|
+
// ── Media ───────────────────────────────────────────────────────────────────
|
|
1724
|
+
// Verbatim from MicPermissionGate, which already says these well and says them first.
|
|
1725
|
+
//
|
|
1726
|
+
// NEVER ACTUALLY SHOWN via this table — these four codes (through `media/playback-blocked`, below)
|
|
1727
|
+
// are never raised as a `SoftphoneError` at all. See the fuller comment on each in `core/errors.ts`
|
|
1728
|
+
// for why. Copy kept here anyway so the catalogue and this file stay the same shape as every other
|
|
1729
|
+
// code — `error-text.test.ts` would fail the build the moment they drifted.
|
|
1730
|
+
"media/permission-denied": {
|
|
1731
|
+
title: "Micr\xF3fono bloqueado",
|
|
1732
|
+
body: "Perm\xEDtelo en tu navegador para poder llamar."
|
|
1733
|
+
},
|
|
1734
|
+
"media/no-input-device": {
|
|
1735
|
+
title: "Sin micr\xF3fono",
|
|
1736
|
+
body: "Conecta un micr\xF3fono y vuelve a intentarlo."
|
|
1737
|
+
},
|
|
1738
|
+
"media/webrtc-unsupported": {
|
|
1739
|
+
title: "Llamadas no disponibles",
|
|
1740
|
+
body: "Este navegador no admite llamadas. Prueba con Chrome, Edge o Firefox."
|
|
1741
|
+
},
|
|
1742
|
+
"media/playback-blocked": { title: "Toca la pantalla para activar el sonido" },
|
|
1743
|
+
// ── Network / signalling ────────────────────────────────────────────────────
|
|
1744
|
+
// "Centralita" rather than "SIP" or "registro": the user knows what a phone system is.
|
|
1745
|
+
"net/registration-failed": {
|
|
1746
|
+
title: "No se pudo conectar",
|
|
1747
|
+
body: "No hemos podido conectar con la centralita. Reintenta en unos segundos."
|
|
1748
|
+
},
|
|
1749
|
+
"net/transport-failed": {
|
|
1750
|
+
title: "Conexi\xF3n perdida",
|
|
1751
|
+
body: "Se ha perdido la conexi\xF3n con la centralita."
|
|
1752
|
+
},
|
|
1753
|
+
// NEVER ACTUALLY SHOWN either — CallScreen's own "Reconectando…" already covers this, off the
|
|
1754
|
+
// call's `reconnecting` state rather than this channel. See the comment in `core/errors.ts`.
|
|
1755
|
+
"net/ice-failed": { title: "Recuperando el audio\u2026" },
|
|
1756
|
+
// ── Call ────────────────────────────────────────────────────────────────────
|
|
1757
|
+
/*
|
|
1758
|
+
THE VOICE RULE FOR THIS BLOCK: say what HAPPENED, not what it is called.
|
|
1759
|
+
|
|
1760
|
+
These were status labels — "Llamada perdida", "Llamada rechazada", "Sin respuesta". Each is a
|
|
1761
|
+
name for a category rather than an account of an event, which is why they read as a database enum
|
|
1762
|
+
someone forgot to translate into human. Worse, a label hides the ACTOR: "Llamada rechazada" never
|
|
1763
|
+
says who rejected what, so the user reading it after declining a call and the user reading it
|
|
1764
|
+
after being declined see the same three words.
|
|
1765
|
+
|
|
1766
|
+
So each one is a short sentence about what the other person, or the phone, did — the form
|
|
1767
|
+
`call/busy` already had, and the reason it was the only one here that read clearly.
|
|
1768
|
+
|
|
1769
|
+
Impersonal third-person plural ("contestaron", "colgaron") throughout: the user does not know who
|
|
1770
|
+
is at the other end, and it is what Spanish uses when the subject is unknown. Mixing it with the
|
|
1771
|
+
singular across neighbouring lines reads as carelessness.
|
|
1772
|
+
|
|
1773
|
+
LENGTH IS A HARD CONSTRAINT, not a preference. `.cv-dial-outcome` is one line at a fixed
|
|
1774
|
+
`1.15rem`, and the keypad below it is `flex: 1` — a title that wraps resizes every key and shifts
|
|
1775
|
+
the grid under a finger already reaching for it. `error-text.test.ts` caps this at 48 characters.
|
|
1776
|
+
*/
|
|
1777
|
+
"call/not-answerable": { title: "Esa llamada ya ha terminado" },
|
|
1778
|
+
/*
|
|
1779
|
+
NOT "Comunicando", which is what this said and what sent us looking. SIP 486 is both "the line is
|
|
1780
|
+
engaged" AND what almost every endpoint sends when a person presses decline — including our own
|
|
1781
|
+
`reject()`. One code, two events, no way to tell them apart, so the copy has to hold both rather
|
|
1782
|
+
than assert the less likely one. `call/rejected` can be definite: an endpoint sending 603 Decline
|
|
1783
|
+
means exactly that.
|
|
1784
|
+
*/
|
|
1785
|
+
"call/busy": { title: "No contestaron o la l\xEDnea estaba ocupada" },
|
|
1786
|
+
"call/no-answer": { title: "No contestaron" },
|
|
1787
|
+
"call/rejected": { title: "No aceptaron tu llamada" },
|
|
1788
|
+
/*
|
|
1789
|
+
The legacy CRM said "La persona que llamaba colgó antes de que pudieras contestar." — the right
|
|
1790
|
+
fact, at three times the width of the line that has to hold it. This keeps the fact and drops the
|
|
1791
|
+
words: the user needs to know the call was FOR them and that nobody is waiting on the line.
|
|
1792
|
+
"Llamada perdida" alone tested badly for exactly the label reason above — it names the category
|
|
1793
|
+
without saying anyone rang.
|
|
1794
|
+
*/
|
|
1795
|
+
"call/missed": { title: "Colgaron antes de que pudieras contestar" },
|
|
1796
|
+
// "No se pudo completar la llamada" was our own "something went wrong": true of every failure and
|
|
1797
|
+
// useful for none. The number is what the user can actually act on.
|
|
1798
|
+
"call/failed": { title: "No se pudo conectar con ese n\xFAmero" },
|
|
1799
|
+
// The load-bearing half is the SECOND sentence: the original call is still up, and a user who
|
|
1800
|
+
// thinks they dropped it will hang up on a client to "fix" it.
|
|
1801
|
+
"call/transfer-failed": { title: "No se pudo transferir. Sigues en la llamada" },
|
|
1802
|
+
"call/unsupported": { title: "Esta funci\xF3n no est\xE1 disponible" },
|
|
1803
|
+
// ── Provider ────────────────────────────────────────────────────────────────
|
|
1804
|
+
"provider/provision-failed": {
|
|
1805
|
+
title: "No se pudo activar el tel\xE9fono",
|
|
1806
|
+
body: "Si el problema contin\xFAa, contacta con soporte."
|
|
1807
|
+
}
|
|
1808
|
+
};
|
|
1809
|
+
function ErrorGate({
|
|
1810
|
+
error,
|
|
1811
|
+
onRetry
|
|
1812
|
+
}) {
|
|
1813
|
+
const text = ERROR_TEXT[error.code];
|
|
1814
|
+
return (
|
|
1815
|
+
/*
|
|
1816
|
+
`part` on all four, because this is the most intrusive thing the SDK draws — it replaces the
|
|
1817
|
+
whole dialpad — and inside `<coventia-softphone>` a shadow root makes it otherwise unreachable
|
|
1818
|
+
from the host's CSS. Custom properties alone only let a host recolour what we chose to draw.
|
|
1819
|
+
|
|
1820
|
+
`data-code` is NOT a substitute: it is inside the shadow root, so `[data-code=…]` selectors
|
|
1821
|
+
cannot cross the boundary either. It stays for the light-DOM `/ui` path and for debugging.
|
|
1822
|
+
*/
|
|
1823
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-gate", "data-tone": "danger", "data-code": error.code, part: "error-gate", children: [
|
|
1824
|
+
/* @__PURE__ */ jsx("span", { className: "cv-gate-mark", children: /* @__PURE__ */ jsx(TriangleAlert, { size: 22 }) }),
|
|
1825
|
+
/* @__PURE__ */ jsx("h3", { className: "cv-gate-title", part: "error-title", children: text.title }),
|
|
1826
|
+
text.body === void 0 ? null : /* @__PURE__ */ jsx("p", { part: "error-body", children: text.body }),
|
|
1827
|
+
/* @__PURE__ */ jsx("button", { className: "cv-btn cv-btn--accent", onClick: onRetry, part: "error-retry", children: "Reintentar" })
|
|
1828
|
+
] })
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
function LeaderGate() {
|
|
1832
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-gate", part: "leader-gate", children: [
|
|
1833
|
+
/* @__PURE__ */ jsx("span", { className: "cv-gate-mark", children: /* @__PURE__ */ jsx(MonitorSmartphone, { size: 22 }) }),
|
|
1834
|
+
/* @__PURE__ */ jsx("h3", { className: "cv-gate-title", part: "leader-title", children: "Tel\xE9fono activo en otra pesta\xF1a" }),
|
|
1835
|
+
/* @__PURE__ */ jsx("p", { part: "leader-body", children: "Solo una pesta\xF1a puede usar el tel\xE9fono a la vez. Cierra la otra para activar esta." })
|
|
1836
|
+
] });
|
|
1837
|
+
}
|
|
1838
|
+
var BACKSTOP_MS = 6e3;
|
|
1839
|
+
function useCallOutcome() {
|
|
1840
|
+
const softphone = useSoftphone();
|
|
1841
|
+
const [code, setCode] = useState(null);
|
|
1842
|
+
const timer = useRef(void 0);
|
|
1843
|
+
const clear = useCallback(() => {
|
|
1844
|
+
clearTimeout(timer.current);
|
|
1845
|
+
setCode(null);
|
|
1846
|
+
}, []);
|
|
1847
|
+
useEffect(() => {
|
|
1848
|
+
const off = softphone.on("error" /* Error */, (error) => {
|
|
1849
|
+
if (error.fatal) return;
|
|
1850
|
+
clearTimeout(timer.current);
|
|
1851
|
+
setCode(error.code);
|
|
1852
|
+
timer.current = setTimeout(() => setCode(null), BACKSTOP_MS);
|
|
1853
|
+
});
|
|
1854
|
+
return () => {
|
|
1855
|
+
off();
|
|
1856
|
+
clearTimeout(timer.current);
|
|
1857
|
+
};
|
|
1858
|
+
}, [softphone]);
|
|
1859
|
+
useEffect(() => softphone.on("incomingCall" /* IncomingCall */, () => clear()), [softphone, clear]);
|
|
1860
|
+
return {
|
|
1861
|
+
outcome: code === null ? null : { code, text: ERROR_TEXT[code].title },
|
|
1862
|
+
clear
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// src/ui/audio/audio.ts
|
|
1867
|
+
var TONE_HZ = 425;
|
|
1868
|
+
function createRemoteAudio() {
|
|
1869
|
+
const el = document.createElement("audio");
|
|
1870
|
+
el.autoplay = true;
|
|
1871
|
+
el.style.display = "none";
|
|
1872
|
+
document.body.appendChild(el);
|
|
1873
|
+
return {
|
|
1874
|
+
attach(stream) {
|
|
1875
|
+
el.srcObject = stream;
|
|
1876
|
+
void el.play().catch(() => {
|
|
1877
|
+
});
|
|
1878
|
+
},
|
|
1879
|
+
detach() {
|
|
1880
|
+
el.srcObject = null;
|
|
1881
|
+
},
|
|
1882
|
+
destroy() {
|
|
1883
|
+
el.srcObject = null;
|
|
1884
|
+
el.remove();
|
|
1885
|
+
},
|
|
1886
|
+
async setOutputDevice(deviceId) {
|
|
1887
|
+
if (typeof el.setSinkId !== "function") return;
|
|
1888
|
+
try {
|
|
1889
|
+
await el.setSinkId(deviceId ?? "");
|
|
1890
|
+
} catch {
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
var shared = null;
|
|
1896
|
+
function sharedContext() {
|
|
1897
|
+
if (shared) return shared;
|
|
1898
|
+
const Ctor = typeof AudioContext !== "undefined" ? AudioContext : globalThis.webkitAudioContext;
|
|
1899
|
+
if (!Ctor) return null;
|
|
1900
|
+
shared = new Ctor();
|
|
1901
|
+
return shared;
|
|
1902
|
+
}
|
|
1903
|
+
function primeAudio() {
|
|
1904
|
+
const ctx = sharedContext();
|
|
1905
|
+
if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
function playRingtone() {
|
|
1909
|
+
const ctx = sharedContext();
|
|
1910
|
+
if (!ctx) return { stop() {
|
|
1911
|
+
} };
|
|
1912
|
+
const gain = ctx.createGain();
|
|
1913
|
+
gain.gain.value = 0;
|
|
1914
|
+
gain.connect(ctx.destination);
|
|
1915
|
+
const osc = ctx.createOscillator();
|
|
1916
|
+
osc.type = "sine";
|
|
1917
|
+
osc.frequency.value = TONE_HZ;
|
|
1918
|
+
osc.connect(gain);
|
|
1919
|
+
osc.start();
|
|
1920
|
+
const LEVEL = 0.8;
|
|
1921
|
+
const RAMP = 0.02;
|
|
1922
|
+
const burst = (at, dur) => {
|
|
1923
|
+
gain.gain.setValueAtTime(0, at);
|
|
1924
|
+
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
1925
|
+
gain.gain.setValueAtTime(LEVEL, at + dur - RAMP);
|
|
1926
|
+
gain.gain.linearRampToValueAtTime(0, at + dur);
|
|
1927
|
+
};
|
|
1928
|
+
const scheduleCycle = (at) => {
|
|
1929
|
+
burst(at, 0.4);
|
|
1930
|
+
burst(at + 0.6, 0.4);
|
|
1931
|
+
return 3;
|
|
1932
|
+
};
|
|
1933
|
+
const LOOKAHEAD = 0.5;
|
|
1934
|
+
const TICK_MS = 150;
|
|
1935
|
+
const LEAD = 0.08;
|
|
1936
|
+
let stopped = false;
|
|
1937
|
+
let timer = null;
|
|
1938
|
+
let nextAt = 0;
|
|
1939
|
+
const pump = () => {
|
|
1940
|
+
while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleCycle(nextAt);
|
|
1941
|
+
};
|
|
1942
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
1943
|
+
}).finally(() => {
|
|
1944
|
+
if (stopped) return;
|
|
1945
|
+
nextAt = ctx.currentTime + LEAD;
|
|
1946
|
+
pump();
|
|
1947
|
+
timer = setInterval(pump, TICK_MS);
|
|
1948
|
+
});
|
|
1949
|
+
return {
|
|
1950
|
+
stop() {
|
|
1951
|
+
stopped = true;
|
|
1952
|
+
if (timer) clearInterval(timer);
|
|
1953
|
+
try {
|
|
1954
|
+
osc.stop();
|
|
1955
|
+
} catch {
|
|
1956
|
+
}
|
|
1957
|
+
try {
|
|
1958
|
+
gain.disconnect();
|
|
1959
|
+
} catch {
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
function playCallEndedTone() {
|
|
1965
|
+
const ctx = sharedContext();
|
|
1966
|
+
if (!ctx) return;
|
|
1967
|
+
const gain = ctx.createGain();
|
|
1968
|
+
gain.gain.value = 0;
|
|
1969
|
+
gain.connect(ctx.destination);
|
|
1970
|
+
const osc = ctx.createOscillator();
|
|
1971
|
+
osc.type = "sine";
|
|
1972
|
+
osc.frequency.value = TONE_HZ;
|
|
1973
|
+
osc.connect(gain);
|
|
1974
|
+
osc.start();
|
|
1975
|
+
const LEVEL = 0.55;
|
|
1976
|
+
const RAMP = 8e-3;
|
|
1977
|
+
const BEEP = 0.25;
|
|
1978
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
1979
|
+
}).finally(() => {
|
|
1980
|
+
const at = ctx.currentTime + 0.02;
|
|
1981
|
+
gain.gain.setValueAtTime(0, at);
|
|
1982
|
+
gain.gain.linearRampToValueAtTime(LEVEL, at + RAMP);
|
|
1983
|
+
gain.gain.setValueAtTime(LEVEL, at + BEEP - RAMP);
|
|
1984
|
+
gain.gain.linearRampToValueAtTime(0, at + BEEP);
|
|
1985
|
+
const totalMs = (at + BEEP - ctx.currentTime) * 1e3;
|
|
1986
|
+
setTimeout(() => {
|
|
1987
|
+
try {
|
|
1988
|
+
osc.stop();
|
|
1989
|
+
} catch {
|
|
1990
|
+
}
|
|
1991
|
+
try {
|
|
1992
|
+
gain.disconnect();
|
|
1993
|
+
} catch {
|
|
1994
|
+
}
|
|
1995
|
+
}, totalMs);
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
var ringbackUrl = null;
|
|
1999
|
+
function ringbackSource() {
|
|
2000
|
+
if (ringbackUrl) return ringbackUrl;
|
|
2001
|
+
const sampleRate = 48e3;
|
|
2002
|
+
const channels = 2;
|
|
2003
|
+
const bytesPerSample = 2;
|
|
2004
|
+
const amp = 0.5;
|
|
2005
|
+
const toneSec = 1.5;
|
|
2006
|
+
const silenceSec = 3;
|
|
2007
|
+
const totalFrames = Math.round((toneSec + silenceSec) * sampleRate);
|
|
2008
|
+
const toneFrames = Math.round(toneSec * sampleRate);
|
|
2009
|
+
const ramp = Math.round(0.02 * sampleRate);
|
|
2010
|
+
const blockAlign = channels * bytesPerSample;
|
|
2011
|
+
const dataSize = totalFrames * blockAlign;
|
|
2012
|
+
const buffer = new ArrayBuffer(44 + dataSize);
|
|
2013
|
+
const view = new DataView(buffer);
|
|
2014
|
+
const str = (off, s) => {
|
|
2015
|
+
for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
|
|
2016
|
+
};
|
|
2017
|
+
str(0, "RIFF");
|
|
2018
|
+
view.setUint32(4, 36 + dataSize, true);
|
|
2019
|
+
str(8, "WAVE");
|
|
2020
|
+
str(12, "fmt ");
|
|
2021
|
+
view.setUint32(16, 16, true);
|
|
2022
|
+
view.setUint16(20, 1, true);
|
|
2023
|
+
view.setUint16(22, channels, true);
|
|
2024
|
+
view.setUint32(24, sampleRate, true);
|
|
2025
|
+
view.setUint32(28, sampleRate * blockAlign, true);
|
|
2026
|
+
view.setUint16(32, blockAlign, true);
|
|
2027
|
+
view.setUint16(34, bytesPerSample * 8, true);
|
|
2028
|
+
str(36, "data");
|
|
2029
|
+
view.setUint32(40, dataSize, true);
|
|
2030
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
2031
|
+
let sample = 0;
|
|
2032
|
+
if (i < toneFrames) {
|
|
2033
|
+
let env = 1;
|
|
2034
|
+
if (i < ramp) env = i / ramp;
|
|
2035
|
+
else if (i > toneFrames - ramp) env = Math.max(0, (toneFrames - i) / ramp);
|
|
2036
|
+
sample = Math.sin(2 * Math.PI * TONE_HZ * (i / sampleRate)) * amp * env;
|
|
2037
|
+
}
|
|
2038
|
+
const s16 = Math.round(sample * 32767);
|
|
2039
|
+
const off = 44 + i * blockAlign;
|
|
2040
|
+
for (let c = 0; c < channels; c++) view.setInt16(off + c * bytesPerSample, s16, true);
|
|
2041
|
+
}
|
|
2042
|
+
ringbackUrl = URL.createObjectURL(new Blob([buffer], { type: "audio/wav" }));
|
|
2043
|
+
return ringbackUrl;
|
|
2044
|
+
}
|
|
2045
|
+
function playRingback() {
|
|
2046
|
+
const el = document.createElement("audio");
|
|
2047
|
+
el.src = ringbackSource();
|
|
2048
|
+
el.loop = true;
|
|
2049
|
+
el.style.display = "none";
|
|
2050
|
+
document.body.appendChild(el);
|
|
2051
|
+
void el.play().catch(() => {
|
|
2052
|
+
});
|
|
2053
|
+
return {
|
|
2054
|
+
stop() {
|
|
2055
|
+
el.pause();
|
|
2056
|
+
el.remove();
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// src/ui/audio/useCallAudio.ts
|
|
2062
|
+
function useCallAudio(enabled) {
|
|
2063
|
+
const softphone = useSoftphone();
|
|
2064
|
+
const remote = useRef(null);
|
|
2065
|
+
const tone = useRef(null);
|
|
2066
|
+
useEffect(() => {
|
|
2067
|
+
if (!enabled || typeof document === "undefined") return;
|
|
2068
|
+
const prime = () => primeAudio();
|
|
2069
|
+
const gestures = ["pointerdown", "keydown"];
|
|
2070
|
+
for (const type of gestures) {
|
|
2071
|
+
document.addEventListener(type, prime, { once: true, capture: true });
|
|
2072
|
+
}
|
|
2073
|
+
remote.current = createRemoteAudio();
|
|
2074
|
+
void remote.current.setOutputDevice(softphone.audio.currentOutputId);
|
|
2075
|
+
const offDeviceChange = softphone.on("deviceChange" /* DeviceChange */, () => {
|
|
2076
|
+
void remote.current?.setOutputDevice(softphone.audio.currentOutputId);
|
|
2077
|
+
});
|
|
2078
|
+
const stopTone = () => {
|
|
2079
|
+
tone.current?.stop();
|
|
2080
|
+
tone.current = null;
|
|
2081
|
+
};
|
|
2082
|
+
const setTone = (next) => {
|
|
2083
|
+
stopTone();
|
|
2084
|
+
if (next === "ringtone") tone.current = playRingtone();
|
|
2085
|
+
else if (next === "ringback") tone.current = playRingback();
|
|
2086
|
+
};
|
|
2087
|
+
const perCall = [];
|
|
2088
|
+
const followedCalls = /* @__PURE__ */ new Set();
|
|
2089
|
+
const follow = (call) => {
|
|
2090
|
+
if (followedCalls.has(call)) return;
|
|
2091
|
+
followedCalls.add(call);
|
|
2092
|
+
let wasConnected = call.state === "active" || call.state === "held";
|
|
2093
|
+
const apply = (state) => {
|
|
2094
|
+
if (state === "active" || state === "held") wasConnected = true;
|
|
2095
|
+
if (state === "ringing") setTone("ringtone");
|
|
2096
|
+
else if (state === "dialing" || state === "early") setTone("ringback");
|
|
2097
|
+
else setTone(null);
|
|
2098
|
+
};
|
|
2099
|
+
apply(call.state);
|
|
2100
|
+
perCall.push(
|
|
2101
|
+
call.on("stateChanged" /* StateChanged */, ({ state }) => apply(state)),
|
|
2102
|
+
// Attach on `active` only — see the note above. `remoteStream` is usually already set by
|
|
2103
|
+
// then, since media arrives before the 200 OK.
|
|
2104
|
+
call.on("accepted" /* Accepted */, () => {
|
|
2105
|
+
if (call.remoteStream) remote.current?.attach(call.remoteStream);
|
|
2106
|
+
}),
|
|
2107
|
+
// …but if it arrives late, take it then — as long as we are past ringing.
|
|
2108
|
+
call.on("remoteStream" /* RemoteStream */, ({ stream }) => {
|
|
2109
|
+
if (call.state === "active" || call.state === "held") remote.current?.attach(stream);
|
|
2110
|
+
}),
|
|
2111
|
+
call.on("ended" /* Ended */, ({ reason }) => {
|
|
2112
|
+
followedCalls.delete(call);
|
|
2113
|
+
stopTone();
|
|
2114
|
+
remote.current?.detach();
|
|
2115
|
+
if (wasConnected && (reason === "local" || reason === "remote")) playCallEndedTone();
|
|
2116
|
+
})
|
|
2117
|
+
);
|
|
2118
|
+
};
|
|
2119
|
+
const offIncoming = softphone.on("incomingCall" /* IncomingCall */, follow);
|
|
2120
|
+
const offActive = softphone.on("activeCallChanged" /* ActiveCallChanged */, ({ call }) => {
|
|
2121
|
+
if (call) follow(call);
|
|
2122
|
+
else {
|
|
2123
|
+
stopTone();
|
|
2124
|
+
remote.current?.detach();
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
if (softphone.activeCall) follow(softphone.activeCall);
|
|
2128
|
+
return () => {
|
|
2129
|
+
for (const type of gestures) document.removeEventListener(type, prime, { capture: true });
|
|
2130
|
+
offIncoming();
|
|
2131
|
+
offActive();
|
|
2132
|
+
offDeviceChange();
|
|
2133
|
+
for (const off of perCall) off();
|
|
2134
|
+
stopTone();
|
|
2135
|
+
remote.current?.destroy();
|
|
2136
|
+
remote.current = null;
|
|
2137
|
+
};
|
|
2138
|
+
}, [softphone, enabled]);
|
|
2139
|
+
}
|
|
2140
|
+
function SoftphoneWidget(props) {
|
|
2141
|
+
const {
|
|
2142
|
+
contacts,
|
|
2143
|
+
history,
|
|
2144
|
+
contactAction,
|
|
2145
|
+
incomingCallAction,
|
|
2146
|
+
className,
|
|
2147
|
+
theme = "system",
|
|
2148
|
+
requireMic = true,
|
|
2149
|
+
sounds = true
|
|
2150
|
+
} = props;
|
|
2151
|
+
const softphone = useSoftphone();
|
|
2152
|
+
const fallback = softphone.sources;
|
|
2153
|
+
const contactsSource = contacts ?? fallback?.contacts;
|
|
2154
|
+
const historySource = history ?? fallback?.history;
|
|
2155
|
+
const [tab, setTab] = useState("dialer");
|
|
2156
|
+
const call = useActiveCall();
|
|
2157
|
+
const isLeader = useIsLeader();
|
|
2158
|
+
const softphoneState = useSoftphoneState();
|
|
2159
|
+
const isFollower = !isLeader && softphoneState === "starting";
|
|
2160
|
+
const mic = useMicPermission();
|
|
2161
|
+
useCallAudio(sounds);
|
|
2162
|
+
const blocked = requireMic && mic.state !== "ready";
|
|
2163
|
+
const fatal = useLastError();
|
|
2164
|
+
const { outcome, clear: clearOutcome } = useCallOutcome();
|
|
2165
|
+
const retry = () => {
|
|
2166
|
+
void (async () => {
|
|
2167
|
+
try {
|
|
2168
|
+
await softphone.stop();
|
|
2169
|
+
await softphone.start();
|
|
2170
|
+
} catch {
|
|
2171
|
+
}
|
|
2172
|
+
})();
|
|
2173
|
+
};
|
|
2174
|
+
const tabs = [
|
|
2175
|
+
{ id: "dialer", icon: Grid3x3, label: "Teclado", show: true },
|
|
2176
|
+
{ id: "contacts", icon: Users, label: "Contactos", show: !!contactsSource },
|
|
2177
|
+
{ id: "history", icon: Clock, label: "Historial", show: !!historySource },
|
|
2178
|
+
// Unlike Contactos/Historial, needs no host-supplied source — a microphone and a speaker are not
|
|
2179
|
+
// optional parts of a phone, so this is never conditionally hidden.
|
|
2180
|
+
{ id: "settings", icon: Settings, label: "Ajustes", show: true }
|
|
2181
|
+
].filter((t) => t.show);
|
|
2182
|
+
return /* @__PURE__ */ jsxs(
|
|
2183
|
+
"div",
|
|
2184
|
+
{
|
|
2185
|
+
className: className ? `cv-softphone ${className}` : "cv-softphone",
|
|
2186
|
+
...theme === "system" ? {} : { "data-cv-theme": theme },
|
|
2187
|
+
children: [
|
|
2188
|
+
/* @__PURE__ */ jsx("div", { className: "cv-body", children: isFollower ? /* @__PURE__ */ jsx(LeaderGate, {}) : blocked ? /* @__PURE__ */ jsx(MicPrompt, { state: mic.state, onRequest: () => void mic.request() }) : call ? /* @__PURE__ */ jsx(CallScreen, { call, ...incomingCallAction ? { incomingCallAction } : {} }) : fatal ? /* @__PURE__ */ jsx(ErrorGate, { error: fatal, onRetry: retry }) : tab === "contacts" && contactsSource ? /* @__PURE__ */ jsx(ContactsTab, { source: contactsSource, ...contactAction ? { contactAction } : {} }) : tab === "history" && historySource ? /* @__PURE__ */ jsx(HistoryTab, { source: historySource }) : tab === "settings" ? /* @__PURE__ */ jsx(SettingsTab, {}) : /* @__PURE__ */ jsx(Dialpad, { outcome, onIntent: clearOutcome }) }),
|
|
2189
|
+
call || blocked || fatal || isFollower ? null : /* @__PURE__ */ jsx("div", { className: "cv-tabs", role: "tablist", children: tabs.map(({ id, icon: Icon3, label }) => /* @__PURE__ */ jsxs(
|
|
2190
|
+
"button",
|
|
2191
|
+
{
|
|
2192
|
+
role: "tab",
|
|
2193
|
+
"aria-selected": tab === id,
|
|
2194
|
+
className: "cv-tab",
|
|
2195
|
+
onClick: () => setTab(id),
|
|
2196
|
+
children: [
|
|
2197
|
+
/* @__PURE__ */ jsx(Icon3, { size: 20 }),
|
|
2198
|
+
/* @__PURE__ */ jsx("span", { children: label })
|
|
2199
|
+
]
|
|
2200
|
+
},
|
|
2201
|
+
id
|
|
2202
|
+
)) })
|
|
2203
|
+
]
|
|
2204
|
+
}
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
/*! Bundled license information:
|
|
2208
|
+
|
|
2209
|
+
lucide-react/dist/esm/shared/src/utils.js:
|
|
2210
|
+
lucide-react/dist/esm/defaultAttributes.js:
|
|
2211
|
+
lucide-react/dist/esm/Icon.js:
|
|
2212
|
+
lucide-react/dist/esm/createLucideIcon.js:
|
|
2213
|
+
lucide-react/dist/esm/icons/arrow-left-right.js:
|
|
2214
|
+
lucide-react/dist/esm/icons/arrow-left.js:
|
|
2215
|
+
lucide-react/dist/esm/icons/arrow-right.js:
|
|
2216
|
+
lucide-react/dist/esm/icons/calendar-clock.js:
|
|
2217
|
+
lucide-react/dist/esm/icons/check.js:
|
|
2218
|
+
lucide-react/dist/esm/icons/chevron-down.js:
|
|
2219
|
+
lucide-react/dist/esm/icons/chevron-right.js:
|
|
2220
|
+
lucide-react/dist/esm/icons/circle-help.js:
|
|
2221
|
+
lucide-react/dist/esm/icons/clock.js:
|
|
2222
|
+
lucide-react/dist/esm/icons/delete.js:
|
|
2223
|
+
lucide-react/dist/esm/icons/external-link.js:
|
|
2224
|
+
lucide-react/dist/esm/icons/grid-3x3.js:
|
|
2225
|
+
lucide-react/dist/esm/icons/hash.js:
|
|
2226
|
+
lucide-react/dist/esm/icons/headphones.js:
|
|
2227
|
+
lucide-react/dist/esm/icons/loader-circle.js:
|
|
2228
|
+
lucide-react/dist/esm/icons/mic-off.js:
|
|
2229
|
+
lucide-react/dist/esm/icons/mic.js:
|
|
2230
|
+
lucide-react/dist/esm/icons/monitor-smartphone.js:
|
|
2231
|
+
lucide-react/dist/esm/icons/pause.js:
|
|
2232
|
+
lucide-react/dist/esm/icons/phone-incoming.js:
|
|
2233
|
+
lucide-react/dist/esm/icons/phone-missed.js:
|
|
2234
|
+
lucide-react/dist/esm/icons/phone-off.js:
|
|
2235
|
+
lucide-react/dist/esm/icons/phone-outgoing.js:
|
|
2236
|
+
lucide-react/dist/esm/icons/phone.js:
|
|
2237
|
+
lucide-react/dist/esm/icons/play.js:
|
|
2238
|
+
lucide-react/dist/esm/icons/search.js:
|
|
2239
|
+
lucide-react/dist/esm/icons/settings.js:
|
|
2240
|
+
lucide-react/dist/esm/icons/timer.js:
|
|
2241
|
+
lucide-react/dist/esm/icons/triangle-alert.js:
|
|
2242
|
+
lucide-react/dist/esm/icons/user.js:
|
|
2243
|
+
lucide-react/dist/esm/icons/users.js:
|
|
2244
|
+
lucide-react/dist/esm/icons/x.js:
|
|
2245
|
+
lucide-react/dist/esm/lucide-react.js:
|
|
2246
|
+
(**
|
|
2247
|
+
* @license lucide-react v0.436.0 - ISC
|
|
2248
|
+
*
|
|
2249
|
+
* This source code is licensed under the ISC license.
|
|
2250
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
2251
|
+
*)
|
|
2252
|
+
*/
|
|
2253
|
+
|
|
2254
|
+
export { CallScreen, ContactsTab, Dialpad, HistoryTab, MicPermissionGate, SoftphoneWidget };
|
|
2255
|
+
//# sourceMappingURL=index.js.map
|
|
2256
|
+
//# sourceMappingURL=index.js.map
|