@burdenoff/microfe-comradecircle 2026.1025.2 → 2026.1025.3
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/comradecircle/components/LiveStagePanel.d.ts +10 -0
- package/dist/comradecircle/components/LiveStagePanel.d.ts.map +1 -0
- package/dist/comradecircle/components/LiveStagePanel.js +263 -0
- package/dist/comradecircle/components/LiveStagePanel.js.map +1 -0
- package/dist/comradecircle/components/MediaTile.d.ts +16 -0
- package/dist/comradecircle/components/MediaTile.d.ts.map +1 -0
- package/dist/comradecircle/components/MediaTile.js +74 -0
- package/dist/comradecircle/components/MediaTile.js.map +1 -0
- package/dist/comradecircle/components/VoiceChannelPanel.d.ts +10 -0
- package/dist/comradecircle/components/VoiceChannelPanel.d.ts.map +1 -0
- package/dist/comradecircle/components/VoiceChannelPanel.js +123 -0
- package/dist/comradecircle/components/VoiceChannelPanel.js.map +1 -0
- package/dist/comradecircle/hooks/index.d.ts +3 -0
- package/dist/comradecircle/hooks/index.d.ts.map +1 -1
- package/dist/comradecircle/hooks/index.js +3 -0
- package/dist/comradecircle/hooks/useLiveStage.d.ts +20 -0
- package/dist/comradecircle/hooks/useLiveStage.d.ts.map +1 -0
- package/dist/comradecircle/hooks/useLiveStage.js +160 -0
- package/dist/comradecircle/hooks/useLiveStage.js.map +1 -0
- package/dist/comradecircle/hooks/useVoiceApi.d.ts +36 -0
- package/dist/comradecircle/hooks/useVoiceApi.d.ts.map +1 -0
- package/dist/comradecircle/hooks/useVoiceApi.js +113 -0
- package/dist/comradecircle/hooks/useVoiceApi.js.map +1 -0
- package/dist/comradecircle/hooks/useVoiceCall.d.ts +21 -0
- package/dist/comradecircle/hooks/useVoiceCall.d.ts.map +1 -0
- package/dist/comradecircle/hooks/useVoiceCall.js +154 -0
- package/dist/comradecircle/hooks/useVoiceCall.js.map +1 -0
- package/dist/comradecircle/lib/graphqlStream.d.ts +31 -0
- package/dist/comradecircle/lib/graphqlStream.d.ts.map +1 -0
- package/dist/comradecircle/lib/graphqlStream.js +112 -0
- package/dist/comradecircle/lib/graphqlStream.js.map +1 -0
- package/dist/comradecircle/lib/rtc/liveStage.d.ts +37 -0
- package/dist/comradecircle/lib/rtc/liveStage.d.ts.map +1 -0
- package/dist/comradecircle/lib/rtc/liveStage.js +97 -0
- package/dist/comradecircle/lib/rtc/liveStage.js.map +1 -0
- package/dist/comradecircle/lib/rtc/peerLink.d.ts +50 -0
- package/dist/comradecircle/lib/rtc/peerLink.d.ts.map +1 -0
- package/dist/comradecircle/lib/rtc/peerLink.js +110 -0
- package/dist/comradecircle/lib/rtc/peerLink.js.map +1 -0
- package/dist/comradecircle/lib/rtc/voiceMesh.d.ts +41 -0
- package/dist/comradecircle/lib/rtc/voiceMesh.d.ts.map +1 -0
- package/dist/comradecircle/lib/rtc/voiceMesh.js +101 -0
- package/dist/comradecircle/lib/rtc/voiceMesh.js.map +1 -0
- package/dist/comradecircle/pages/circles/CircleDetailPage.d.ts.map +1 -1
- package/dist/comradecircle/pages/circles/CircleDetailPage.js +299 -286
- package/dist/comradecircle/pages/circles/CircleDetailPage.js.map +1 -1
- package/dist/comradecircle/types/index.d.ts +51 -0
- package/dist/comradecircle/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LiveStagePanel.d.ts","sourceRoot":"","sources":["../../../src/comradecircle/components/LiveStagePanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAA+B,KAAK,EAAE,EAAkB,MAAM,OAAO,CAAC;AAqB7E,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;CACpB;AAiCD,eAAO,MAAM,cAAc,EAAE,EAAE,CAAC,mBAAmB,CAsNlD,CAAC"}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { useComradeCircle as e } from "../context/ComradeCircleContext.js";
|
|
2
|
+
import "../context/index.js";
|
|
3
|
+
import { useTr as t } from "../hooks/useTr.js";
|
|
4
|
+
import { useComLiveStreams as n, useLiveStreamActions as r } from "../hooks/useVoiceApi.js";
|
|
5
|
+
import { useLiveStage as i } from "../hooks/useLiveStage.js";
|
|
6
|
+
import "../hooks/index.js";
|
|
7
|
+
import { MemberIdentity as a } from "./MemberIdentity.js";
|
|
8
|
+
import { describeError as o, notify as s } from "../lib/toast.js";
|
|
9
|
+
import { LoadingView as c } from "./StateViews.js";
|
|
10
|
+
import { useEffect as l, useRef as u, useState as d } from "react";
|
|
11
|
+
import { Eye as f, LogOut as p, MonitorUp as m, Radio as h, Square as g, Video as _ } from "lucide-react";
|
|
12
|
+
import { jsx as v, jsxs as y } from "react/jsx-runtime";
|
|
13
|
+
import { Button as b, Input as x, Label as S, Select as C, SelectContent as w, SelectItem as T, SelectTrigger as E, SelectValue as D } from "@burdenoff/fe-libs/ui";
|
|
14
|
+
//#region src/comradecircle/components/LiveStagePanel.tsx
|
|
15
|
+
var O = ({ stream: e, local: t = !1, label: n }) => {
|
|
16
|
+
let r = u(null);
|
|
17
|
+
return l(() => {
|
|
18
|
+
r.current && (r.current.srcObject = e, e && r.current.play().catch(() => {}));
|
|
19
|
+
}, [e]), /* @__PURE__ */ v("video", {
|
|
20
|
+
ref: r,
|
|
21
|
+
autoPlay: !0,
|
|
22
|
+
playsInline: !0,
|
|
23
|
+
muted: t,
|
|
24
|
+
controls: !t,
|
|
25
|
+
"aria-label": n,
|
|
26
|
+
"data-testid": "live-stage-video",
|
|
27
|
+
className: "aspect-video w-full rounded-xl border border-border-seam bg-bg-sunken object-contain"
|
|
28
|
+
});
|
|
29
|
+
}, k = ({ label: e }) => /* @__PURE__ */ y("span", {
|
|
30
|
+
className: "inline-flex items-center gap-1 rounded-full bg-status-error-bg-subtle px-2 py-0.5 text-xs font-semibold uppercase tracking-wide text-status-error-text",
|
|
31
|
+
children: [
|
|
32
|
+
/* @__PURE__ */ v(h, { className: "h-3 w-3" }),
|
|
33
|
+
" ",
|
|
34
|
+
e
|
|
35
|
+
]
|
|
36
|
+
}), A = ({ circleId: l, isMember: u, canManage: h }) => {
|
|
37
|
+
let A = t(), { currentUser: j } = e(), M = j?.id ?? "", N = i(l), P = n(u ? l : void 0), F = r(), [I, L] = d(""), [R, z] = d("camera"), [B, V] = d(!1), H = P.data?.[0], U = (e) => A("comradecircle.live.viewers", "{{count}} watching", { count: e }), W = async (e) => {
|
|
38
|
+
V(!0);
|
|
39
|
+
try {
|
|
40
|
+
await F.end(e.id), s.success(A("comradecircle.live.ended", "Live stream ended")), await P.refetch();
|
|
41
|
+
} catch (e) {
|
|
42
|
+
s.error(A("comradecircle.live.endError", "Could not end the stream"), o(e));
|
|
43
|
+
} finally {
|
|
44
|
+
V(!1);
|
|
45
|
+
}
|
|
46
|
+
}, G = (e) => {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
let t = I.trim();
|
|
49
|
+
t && N.goLive(t, R);
|
|
50
|
+
}, K = N.error ? /* @__PURE__ */ v("p", {
|
|
51
|
+
role: "alert",
|
|
52
|
+
className: "rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text",
|
|
53
|
+
children: N.error
|
|
54
|
+
}) : null;
|
|
55
|
+
if (N.role === "host" && N.stream) return /* @__PURE__ */ y("section", {
|
|
56
|
+
className: "flex flex-col gap-4",
|
|
57
|
+
"data-testid": "live-stage-host",
|
|
58
|
+
children: [
|
|
59
|
+
/* @__PURE__ */ y("div", {
|
|
60
|
+
className: "flex flex-wrap items-center justify-between gap-3",
|
|
61
|
+
children: [/* @__PURE__ */ y("div", {
|
|
62
|
+
className: "flex min-w-0 items-center gap-2",
|
|
63
|
+
children: [/* @__PURE__ */ v(k, { label: A("comradecircle.live.liveNow", "Live") }), /* @__PURE__ */ v("h3", {
|
|
64
|
+
className: "truncate text-lg font-semibold text-text-primary",
|
|
65
|
+
children: N.stream.title
|
|
66
|
+
})]
|
|
67
|
+
}), /* @__PURE__ */ y("div", {
|
|
68
|
+
className: "flex items-center gap-3",
|
|
69
|
+
children: [/* @__PURE__ */ y("span", {
|
|
70
|
+
className: "inline-flex items-center gap-1 text-sm text-text-secondary",
|
|
71
|
+
children: [
|
|
72
|
+
/* @__PURE__ */ v(f, { className: "h-4 w-4" }),
|
|
73
|
+
" ",
|
|
74
|
+
U(N.stream.viewerCount)
|
|
75
|
+
]
|
|
76
|
+
}), /* @__PURE__ */ y(b, {
|
|
77
|
+
variant: "destructive",
|
|
78
|
+
size: "sm",
|
|
79
|
+
onClick: () => void N.stop(),
|
|
80
|
+
children: [
|
|
81
|
+
/* @__PURE__ */ v(g, { className: "mr-1.5 h-4 w-4" }),
|
|
82
|
+
" ",
|
|
83
|
+
A("comradecircle.live.end", "End stream")
|
|
84
|
+
]
|
|
85
|
+
})]
|
|
86
|
+
})]
|
|
87
|
+
}),
|
|
88
|
+
K,
|
|
89
|
+
/* @__PURE__ */ v(O, {
|
|
90
|
+
stream: N.media,
|
|
91
|
+
local: !0,
|
|
92
|
+
label: A("comradecircle.live.preview", "Your stream preview")
|
|
93
|
+
}),
|
|
94
|
+
/* @__PURE__ */ v("p", {
|
|
95
|
+
className: "text-xs text-text-tertiary",
|
|
96
|
+
children: A("comradecircle.live.hostHint", "Keep this tab open while you are live. Video goes from your browser directly to each viewer.")
|
|
97
|
+
})
|
|
98
|
+
]
|
|
99
|
+
});
|
|
100
|
+
if (N.role === "viewer" && N.stream) {
|
|
101
|
+
let e = N.connectionState === "connected";
|
|
102
|
+
return /* @__PURE__ */ y("section", {
|
|
103
|
+
className: "flex flex-col gap-4",
|
|
104
|
+
"data-testid": "live-stage-viewer",
|
|
105
|
+
children: [
|
|
106
|
+
/* @__PURE__ */ y("div", {
|
|
107
|
+
className: "flex flex-wrap items-center justify-between gap-3",
|
|
108
|
+
children: [/* @__PURE__ */ y("div", {
|
|
109
|
+
className: "flex min-w-0 items-center gap-2",
|
|
110
|
+
children: [/* @__PURE__ */ v(k, { label: A("comradecircle.live.liveNow", "Live") }), /* @__PURE__ */ v("h3", {
|
|
111
|
+
className: "truncate text-lg font-semibold text-text-primary",
|
|
112
|
+
children: N.stream.title
|
|
113
|
+
})]
|
|
114
|
+
}), /* @__PURE__ */ y("div", {
|
|
115
|
+
className: "flex items-center gap-3",
|
|
116
|
+
children: [/* @__PURE__ */ y("span", {
|
|
117
|
+
className: "inline-flex items-center gap-1 text-sm text-text-secondary",
|
|
118
|
+
children: [
|
|
119
|
+
/* @__PURE__ */ v(f, { className: "h-4 w-4" }),
|
|
120
|
+
" ",
|
|
121
|
+
U(N.stream.viewerCount)
|
|
122
|
+
]
|
|
123
|
+
}), /* @__PURE__ */ y(b, {
|
|
124
|
+
variant: "outline",
|
|
125
|
+
size: "sm",
|
|
126
|
+
onClick: () => void N.stop(),
|
|
127
|
+
children: [
|
|
128
|
+
/* @__PURE__ */ v(p, { className: "mr-1.5 h-4 w-4" }),
|
|
129
|
+
" ",
|
|
130
|
+
A("comradecircle.live.leave", "Leave")
|
|
131
|
+
]
|
|
132
|
+
})]
|
|
133
|
+
})]
|
|
134
|
+
}),
|
|
135
|
+
K,
|
|
136
|
+
/* @__PURE__ */ v(a, {
|
|
137
|
+
memberId: N.stream.startedBy,
|
|
138
|
+
size: "sm"
|
|
139
|
+
}),
|
|
140
|
+
/* @__PURE__ */ v(O, {
|
|
141
|
+
stream: N.media,
|
|
142
|
+
label: N.stream.title
|
|
143
|
+
}),
|
|
144
|
+
!e && /* @__PURE__ */ v("p", {
|
|
145
|
+
className: "text-sm text-text-secondary",
|
|
146
|
+
children: A("comradecircle.live.connecting", "Connecting to the host…")
|
|
147
|
+
})
|
|
148
|
+
]
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return u && P.isLoading ? /* @__PURE__ */ v(c, { label: A("comradecircle.live.loading", "Checking for live streams…") }) : /* @__PURE__ */ y("section", {
|
|
152
|
+
className: "flex flex-col gap-4",
|
|
153
|
+
"data-testid": "live-stage-lobby",
|
|
154
|
+
children: [
|
|
155
|
+
K,
|
|
156
|
+
H ? /* @__PURE__ */ y("div", {
|
|
157
|
+
className: "flex flex-col gap-3 rounded-2xl border border-border-seam p-4",
|
|
158
|
+
children: [
|
|
159
|
+
/* @__PURE__ */ y("div", {
|
|
160
|
+
className: "flex flex-wrap items-center justify-between gap-3",
|
|
161
|
+
children: [/* @__PURE__ */ y("div", {
|
|
162
|
+
className: "flex min-w-0 items-center gap-2",
|
|
163
|
+
children: [/* @__PURE__ */ v(k, { label: A("comradecircle.live.liveNow", "Live") }), /* @__PURE__ */ v("h3", {
|
|
164
|
+
className: "truncate text-base font-semibold text-text-primary",
|
|
165
|
+
children: H.title
|
|
166
|
+
})]
|
|
167
|
+
}), /* @__PURE__ */ y("span", {
|
|
168
|
+
className: "inline-flex items-center gap-1 text-sm text-text-secondary",
|
|
169
|
+
children: [
|
|
170
|
+
/* @__PURE__ */ v(f, { className: "h-4 w-4" }),
|
|
171
|
+
" ",
|
|
172
|
+
U(H.viewerCount)
|
|
173
|
+
]
|
|
174
|
+
})]
|
|
175
|
+
}),
|
|
176
|
+
/* @__PURE__ */ v(a, {
|
|
177
|
+
memberId: H.startedBy,
|
|
178
|
+
size: "sm"
|
|
179
|
+
}),
|
|
180
|
+
/* @__PURE__ */ y("div", {
|
|
181
|
+
className: "flex flex-wrap gap-2",
|
|
182
|
+
children: [u && H.startedBy !== M && /* @__PURE__ */ y(b, {
|
|
183
|
+
onClick: () => void N.watch(H),
|
|
184
|
+
disabled: N.busy,
|
|
185
|
+
children: [/* @__PURE__ */ v(f, { className: "mr-1.5 h-4 w-4" }), N.busy ? A("comradecircle.live.joining", "Joining…") : A("comradecircle.live.watch", "Watch")]
|
|
186
|
+
}), (H.startedBy === M || h) && /* @__PURE__ */ y(b, {
|
|
187
|
+
variant: "outline",
|
|
188
|
+
onClick: () => void W(H),
|
|
189
|
+
disabled: B,
|
|
190
|
+
children: [
|
|
191
|
+
/* @__PURE__ */ v(g, { className: "mr-1.5 h-4 w-4" }),
|
|
192
|
+
" ",
|
|
193
|
+
A("comradecircle.live.end", "End stream")
|
|
194
|
+
]
|
|
195
|
+
})]
|
|
196
|
+
})
|
|
197
|
+
]
|
|
198
|
+
}) : /* @__PURE__ */ v("p", {
|
|
199
|
+
className: "text-sm text-text-secondary",
|
|
200
|
+
children: A("comradecircle.live.none", "Nobody is live in this circle right now.")
|
|
201
|
+
}),
|
|
202
|
+
u && !H && /* @__PURE__ */ y("form", {
|
|
203
|
+
onSubmit: G,
|
|
204
|
+
className: "flex flex-col gap-3 rounded-2xl border border-border-seam p-4",
|
|
205
|
+
"aria-label": A("comradecircle.live.goLiveForm", "Go live"),
|
|
206
|
+
children: [
|
|
207
|
+
/* @__PURE__ */ v("h3", {
|
|
208
|
+
className: "text-base font-semibold text-text-primary",
|
|
209
|
+
children: A("comradecircle.live.goLiveTitle", "Go live to this circle")
|
|
210
|
+
}),
|
|
211
|
+
/* @__PURE__ */ y("div", {
|
|
212
|
+
className: "grid gap-3 sm:grid-cols-[1fr_200px]",
|
|
213
|
+
children: [/* @__PURE__ */ y("div", {
|
|
214
|
+
className: "flex flex-col gap-1.5",
|
|
215
|
+
children: [/* @__PURE__ */ v(S, {
|
|
216
|
+
htmlFor: "live-title",
|
|
217
|
+
children: A("comradecircle.live.titleLabel", "Title")
|
|
218
|
+
}), /* @__PURE__ */ v(x, {
|
|
219
|
+
id: "live-title",
|
|
220
|
+
value: I,
|
|
221
|
+
maxLength: 255,
|
|
222
|
+
onChange: (e) => L(e.target.value),
|
|
223
|
+
placeholder: A("comradecircle.live.titlePlaceholder", "What are you streaming?")
|
|
224
|
+
})]
|
|
225
|
+
}), /* @__PURE__ */ y("div", {
|
|
226
|
+
className: "flex flex-col gap-1.5",
|
|
227
|
+
children: [/* @__PURE__ */ v(S, {
|
|
228
|
+
htmlFor: "live-source",
|
|
229
|
+
children: A("comradecircle.live.sourceLabel", "Share")
|
|
230
|
+
}), /* @__PURE__ */ y(C, {
|
|
231
|
+
value: R,
|
|
232
|
+
onValueChange: (e) => z(e),
|
|
233
|
+
children: [/* @__PURE__ */ v(E, {
|
|
234
|
+
id: "live-source",
|
|
235
|
+
children: /* @__PURE__ */ v(D, {})
|
|
236
|
+
}), /* @__PURE__ */ y(w, { children: [/* @__PURE__ */ v(T, {
|
|
237
|
+
value: "camera",
|
|
238
|
+
children: A("comradecircle.live.sourceCamera", "Camera")
|
|
239
|
+
}), /* @__PURE__ */ v(T, {
|
|
240
|
+
value: "screen",
|
|
241
|
+
children: A("comradecircle.live.sourceScreen", "Screen")
|
|
242
|
+
})] })]
|
|
243
|
+
})]
|
|
244
|
+
})]
|
|
245
|
+
}),
|
|
246
|
+
/* @__PURE__ */ v("div", { children: /* @__PURE__ */ y(b, {
|
|
247
|
+
type: "submit",
|
|
248
|
+
disabled: !I.trim() || N.busy,
|
|
249
|
+
children: [v(R === "screen" ? m : _, { className: "mr-1.5 h-4 w-4" }), N.busy ? A("comradecircle.live.starting", "Starting…") : A("comradecircle.live.goLive", "Go live")]
|
|
250
|
+
}) })
|
|
251
|
+
]
|
|
252
|
+
}),
|
|
253
|
+
!u && /* @__PURE__ */ v("p", {
|
|
254
|
+
className: "text-sm text-text-secondary",
|
|
255
|
+
children: A("comradecircle.live.joinCircleFirst", "Join this circle to watch or host live streams.")
|
|
256
|
+
})
|
|
257
|
+
]
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
//#endregion
|
|
261
|
+
export { A as LiveStagePanel };
|
|
262
|
+
|
|
263
|
+
//# sourceMappingURL=LiveStagePanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LiveStagePanel.js","names":[],"sources":["../../../src/comradecircle/components/LiveStagePanel.tsx"],"sourcesContent":["/**\n * LiveStagePanel — a circle's live stage (BOFF-4450).\n *\n * Members can go live with their camera or screen; other members watch with a\n * receive-only connection. One live stage per circle at a time (enforced by\n * the server). The host or a circle manager can end a stream; a stream whose\n * host disappears is ended by the server after missed heartbeats.\n */\nimport { useEffect, useRef, useState, type FC, type FormEvent } from 'react';\nimport { Eye, MonitorUp, Radio, Square, Video, LogOut } from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\n\nimport { useComLiveStreams, useLiveStage, useLiveStreamActions, useTr } from '../hooks';\nimport type { StageSource } from '../hooks';\nimport { useComradeCircle } from '../context';\nimport { notify, describeError } from '../lib/toast';\nimport { MemberIdentity } from './MemberIdentity';\nimport { LoadingView } from './StateViews';\nimport type { ComLiveStream } from '../types';\n\ninterface LiveStagePanelProps {\n circleId: string;\n isMember: boolean;\n canManage: boolean;\n}\n\nconst StageVideo: FC<{ stream: MediaStream | null; local?: boolean; label: string }> = ({\n stream,\n local = false,\n label,\n}) => {\n const ref = useRef<HTMLVideoElement>(null);\n useEffect(() => {\n if (!ref.current) return;\n ref.current.srcObject = stream;\n if (stream) void ref.current.play().catch(() => {});\n }, [stream]);\n return (\n <video\n ref={ref}\n autoPlay\n playsInline\n muted={local}\n controls={!local}\n aria-label={label}\n data-testid=\"live-stage-video\"\n className=\"aspect-video w-full rounded-xl border border-border-seam bg-bg-sunken object-contain\"\n />\n );\n};\n\nconst LiveBadge: FC<{ label: string }> = ({ label }) => (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-error-bg-subtle px-2 py-0.5 text-xs font-semibold uppercase tracking-wide text-status-error-text\">\n <Radio className=\"h-3 w-3\" /> {label}\n </span>\n);\n\nexport const LiveStagePanel: FC<LiveStagePanelProps> = ({ circleId, isMember, canManage }) => {\n const tr = useTr();\n const { currentUser } = useComradeCircle();\n const selfId = currentUser?.id ?? '';\n const stage = useLiveStage(circleId);\n // Listing requires circle membership server-side.\n const streams = useComLiveStreams(isMember ? circleId : undefined);\n const actions = useLiveStreamActions();\n const [title, setTitle] = useState('');\n const [source, setSource] = useState<StageSource>('camera');\n const [ending, setEnding] = useState(false);\n\n const live: ComLiveStream | undefined = streams.data?.[0];\n const viewersLabel = (count: number) =>\n tr('comradecircle.live.viewers', '{{count}} watching', { count });\n\n const endOrphaned = async (stream: ComLiveStream) => {\n setEnding(true);\n try {\n await actions.end(stream.id);\n notify.success(tr('comradecircle.live.ended', 'Live stream ended'));\n await streams.refetch();\n } catch (e) {\n notify.error(tr('comradecircle.live.endError', 'Could not end the stream'), describeError(e));\n } finally {\n setEnding(false);\n }\n };\n\n const onGoLive = (e: FormEvent) => {\n e.preventDefault();\n const trimmed = title.trim();\n if (!trimmed) return;\n void stage.goLive(trimmed, source);\n };\n\n const errorBanner = stage.error ? (\n <p\n role=\"alert\"\n className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\"\n >\n {stage.error}\n </p>\n ) : null;\n\n if (stage.role === 'host' && stage.stream) {\n return (\n <section className=\"flex flex-col gap-4\" data-testid=\"live-stage-host\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <LiveBadge label={tr('comradecircle.live.liveNow', 'Live')} />\n <h3 className=\"truncate text-lg font-semibold text-text-primary\">\n {stage.stream.title}\n </h3>\n </div>\n <div className=\"flex items-center gap-3\">\n <span className=\"inline-flex items-center gap-1 text-sm text-text-secondary\">\n <Eye className=\"h-4 w-4\" /> {viewersLabel(stage.stream.viewerCount)}\n </span>\n <Button variant=\"destructive\" size=\"sm\" onClick={() => void stage.stop()}>\n <Square className=\"mr-1.5 h-4 w-4\" /> {tr('comradecircle.live.end', 'End stream')}\n </Button>\n </div>\n </div>\n {errorBanner}\n <StageVideo\n stream={stage.media}\n local\n label={tr('comradecircle.live.preview', 'Your stream preview')}\n />\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'comradecircle.live.hostHint',\n 'Keep this tab open while you are live. Video goes from your browser directly to each viewer.'\n )}\n </p>\n </section>\n );\n }\n\n if (stage.role === 'viewer' && stage.stream) {\n const connected = stage.connectionState === 'connected';\n return (\n <section className=\"flex flex-col gap-4\" data-testid=\"live-stage-viewer\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <LiveBadge label={tr('comradecircle.live.liveNow', 'Live')} />\n <h3 className=\"truncate text-lg font-semibold text-text-primary\">\n {stage.stream.title}\n </h3>\n </div>\n <div className=\"flex items-center gap-3\">\n <span className=\"inline-flex items-center gap-1 text-sm text-text-secondary\">\n <Eye className=\"h-4 w-4\" /> {viewersLabel(stage.stream.viewerCount)}\n </span>\n <Button variant=\"outline\" size=\"sm\" onClick={() => void stage.stop()}>\n <LogOut className=\"mr-1.5 h-4 w-4\" /> {tr('comradecircle.live.leave', 'Leave')}\n </Button>\n </div>\n </div>\n {errorBanner}\n <MemberIdentity memberId={stage.stream.startedBy} size=\"sm\" />\n <StageVideo stream={stage.media} label={stage.stream.title} />\n {!connected && (\n <p className=\"text-sm text-text-secondary\">\n {tr('comradecircle.live.connecting', 'Connecting to the host…')}\n </p>\n )}\n </section>\n );\n }\n\n if (isMember && streams.isLoading) {\n return <LoadingView label={tr('comradecircle.live.loading', 'Checking for live streams…')} />;\n }\n\n return (\n <section className=\"flex flex-col gap-4\" data-testid=\"live-stage-lobby\">\n {errorBanner}\n {live ? (\n <div className=\"flex flex-col gap-3 rounded-2xl border border-border-seam p-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <LiveBadge label={tr('comradecircle.live.liveNow', 'Live')} />\n <h3 className=\"truncate text-base font-semibold text-text-primary\">{live.title}</h3>\n </div>\n <span className=\"inline-flex items-center gap-1 text-sm text-text-secondary\">\n <Eye className=\"h-4 w-4\" /> {viewersLabel(live.viewerCount)}\n </span>\n </div>\n <MemberIdentity memberId={live.startedBy} size=\"sm\" />\n <div className=\"flex flex-wrap gap-2\">\n {isMember && live.startedBy !== selfId && (\n <Button onClick={() => void stage.watch(live)} disabled={stage.busy}>\n <Eye className=\"mr-1.5 h-4 w-4\" />\n {stage.busy\n ? tr('comradecircle.live.joining', 'Joining…')\n : tr('comradecircle.live.watch', 'Watch')}\n </Button>\n )}\n {(live.startedBy === selfId || canManage) && (\n <Button variant=\"outline\" onClick={() => void endOrphaned(live)} disabled={ending}>\n <Square className=\"mr-1.5 h-4 w-4\" /> {tr('comradecircle.live.end', 'End stream')}\n </Button>\n )}\n </div>\n </div>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {tr('comradecircle.live.none', 'Nobody is live in this circle right now.')}\n </p>\n )}\n\n {isMember && !live && (\n <form\n onSubmit={onGoLive}\n className=\"flex flex-col gap-3 rounded-2xl border border-border-seam p-4\"\n aria-label={tr('comradecircle.live.goLiveForm', 'Go live')}\n >\n <h3 className=\"text-base font-semibold text-text-primary\">\n {tr('comradecircle.live.goLiveTitle', 'Go live to this circle')}\n </h3>\n <div className=\"grid gap-3 sm:grid-cols-[1fr_200px]\">\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"live-title\">{tr('comradecircle.live.titleLabel', 'Title')}</Label>\n <Input\n id=\"live-title\"\n value={title}\n maxLength={255}\n onChange={(e) => setTitle(e.target.value)}\n placeholder={tr('comradecircle.live.titlePlaceholder', 'What are you streaming?')}\n />\n </div>\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"live-source\">{tr('comradecircle.live.sourceLabel', 'Share')}</Label>\n <Select value={source} onValueChange={(v) => setSource(v as StageSource)}>\n <SelectTrigger id=\"live-source\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"camera\">\n {tr('comradecircle.live.sourceCamera', 'Camera')}\n </SelectItem>\n <SelectItem value=\"screen\">\n {tr('comradecircle.live.sourceScreen', 'Screen')}\n </SelectItem>\n </SelectContent>\n </Select>\n </div>\n </div>\n <div>\n <Button type=\"submit\" disabled={!title.trim() || stage.busy}>\n {source === 'screen' ? (\n <MonitorUp className=\"mr-1.5 h-4 w-4\" />\n ) : (\n <Video className=\"mr-1.5 h-4 w-4\" />\n )}\n {stage.busy\n ? tr('comradecircle.live.starting', 'Starting…')\n : tr('comradecircle.live.goLive', 'Go live')}\n </Button>\n </div>\n </form>\n )}\n {!isMember && (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'comradecircle.live.joinCircleFirst',\n 'Join this circle to watch or host live streams.'\n )}\n </p>\n )}\n </section>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
|
|
3
|
+
interface MediaTileProps {
|
|
4
|
+
memberId: string;
|
|
5
|
+
stream: MediaStream | null;
|
|
6
|
+
isLocal?: boolean;
|
|
7
|
+
muted?: boolean;
|
|
8
|
+
/** Connection state to a remote peer; omitted for the local tile. */
|
|
9
|
+
connectionState?: RTCPeerConnectionState | null;
|
|
10
|
+
connectingLabel?: string;
|
|
11
|
+
mutedLabel?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const MediaTile: FC<MediaTileProps>;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=MediaTile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MediaTile.d.ts","sourceRoot":"","sources":["../../../src/comradecircle/components/MediaTile.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAA+B,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAM7D,UAAU,cAAc;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qEAAqE;IACrE,eAAe,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAChD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAQD,eAAO,MAAM,SAAS,EAAE,EAAE,CAAC,cAAc,CAyFxC,CAAC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { MemberIdentity as t } from "./MemberIdentity.js";
|
|
3
|
+
import { useEffect as n, useRef as r, useState as i } from "react";
|
|
4
|
+
import { MicOff as a } from "lucide-react";
|
|
5
|
+
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
6
|
+
//#region src/comradecircle/components/MediaTile.tsx
|
|
7
|
+
function c(e) {
|
|
8
|
+
return !!e?.getVideoTracks().some((e) => e.readyState === "live" && e.enabled && !e.muted);
|
|
9
|
+
}
|
|
10
|
+
var l = ({ memberId: l, stream: u, isLocal: d = !1, muted: f = !1, connectionState: p, connectingLabel: m = "Connecting…", mutedLabel: h = "Muted", className: g }) => {
|
|
11
|
+
let _ = r(null), v = r(null), [y, b] = i(c(u));
|
|
12
|
+
n(() => {
|
|
13
|
+
let e = () => b(c(u));
|
|
14
|
+
if (e(), !u) return;
|
|
15
|
+
let t = u.getVideoTracks();
|
|
16
|
+
return t.forEach((t) => {
|
|
17
|
+
t.addEventListener("mute", e), t.addEventListener("unmute", e), t.addEventListener("ended", e);
|
|
18
|
+
}), u.addEventListener("addtrack", e), u.addEventListener("removetrack", e), () => {
|
|
19
|
+
t.forEach((t) => {
|
|
20
|
+
t.removeEventListener("mute", e), t.removeEventListener("unmute", e), t.removeEventListener("ended", e);
|
|
21
|
+
}), u.removeEventListener("addtrack", e), u.removeEventListener("removetrack", e);
|
|
22
|
+
};
|
|
23
|
+
}, [u]), n(() => {
|
|
24
|
+
_.current && (_.current.srcObject = u), v.current && !d && (v.current.srcObject = u, v.current.play().catch(() => {}));
|
|
25
|
+
}, [
|
|
26
|
+
u,
|
|
27
|
+
d,
|
|
28
|
+
y
|
|
29
|
+
]);
|
|
30
|
+
let x = !d && p !== void 0 && p !== "connected";
|
|
31
|
+
return /* @__PURE__ */ s("div", {
|
|
32
|
+
className: e("relative flex aspect-video items-center justify-center overflow-hidden rounded-xl border border-border-seam bg-bg-sunken", g),
|
|
33
|
+
"data-testid": "voice-media-tile",
|
|
34
|
+
children: [
|
|
35
|
+
y ? /* @__PURE__ */ o("video", {
|
|
36
|
+
ref: _,
|
|
37
|
+
autoPlay: !0,
|
|
38
|
+
playsInline: !0,
|
|
39
|
+
muted: !0,
|
|
40
|
+
className: e("h-full w-full object-cover", d && "-scale-x-100")
|
|
41
|
+
}) : /* @__PURE__ */ o(t, {
|
|
42
|
+
memberId: l,
|
|
43
|
+
size: "lg",
|
|
44
|
+
className: "flex-col gap-2"
|
|
45
|
+
}),
|
|
46
|
+
!d && /* @__PURE__ */ o("audio", {
|
|
47
|
+
ref: v,
|
|
48
|
+
autoPlay: !0
|
|
49
|
+
}),
|
|
50
|
+
/* @__PURE__ */ s("div", {
|
|
51
|
+
className: "absolute bottom-2 left-2 flex items-center gap-1.5",
|
|
52
|
+
children: [f && /* @__PURE__ */ o("span", {
|
|
53
|
+
className: "inline-flex items-center gap-1 rounded-full bg-status-error-bg-subtle px-2 py-0.5 text-xs text-status-error-text",
|
|
54
|
+
"aria-label": h,
|
|
55
|
+
children: /* @__PURE__ */ o(a, { className: "h-3 w-3" })
|
|
56
|
+
}), x && /* @__PURE__ */ o("span", {
|
|
57
|
+
className: "rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary",
|
|
58
|
+
children: m
|
|
59
|
+
})]
|
|
60
|
+
}),
|
|
61
|
+
y && /* @__PURE__ */ o("div", {
|
|
62
|
+
className: "absolute left-2 top-2 rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-primary",
|
|
63
|
+
children: /* @__PURE__ */ o(t, {
|
|
64
|
+
memberId: l,
|
|
65
|
+
size: "xs"
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
]
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
//#endregion
|
|
72
|
+
export { l as MediaTile };
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=MediaTile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MediaTile.js","names":[],"sources":["../../../src/comradecircle/components/MediaTile.tsx"],"sourcesContent":["/**\n * MediaTile — renders one participant's live media (BOFF-4450).\n *\n * Shows the video when the stream carries a live, unmuted video track and\n * falls back to the member's avatar otherwise. Remote audio always plays\n * through a hidden <audio> element so audio-only calls work; the local tile\n * is muted to avoid hearing yourself.\n */\nimport { useEffect, useRef, useState, type FC } from 'react';\nimport { MicOff } from 'lucide-react';\n\nimport { cn } from '../utils/cn';\nimport { MemberIdentity } from './MemberIdentity';\n\ninterface MediaTileProps {\n memberId: string;\n stream: MediaStream | null;\n isLocal?: boolean;\n muted?: boolean;\n /** Connection state to a remote peer; omitted for the local tile. */\n connectionState?: RTCPeerConnectionState | null;\n connectingLabel?: string;\n mutedLabel?: string;\n className?: string;\n}\n\nfunction hasLiveVideo(stream: MediaStream | null): boolean {\n return Boolean(\n stream?.getVideoTracks().some((t) => t.readyState === 'live' && t.enabled && !t.muted)\n );\n}\n\nexport const MediaTile: FC<MediaTileProps> = ({\n memberId,\n stream,\n isLocal = false,\n muted = false,\n connectionState,\n connectingLabel = 'Connecting…',\n mutedLabel = 'Muted',\n className,\n}) => {\n const videoRef = useRef<HTMLVideoElement>(null);\n const audioRef = useRef<HTMLAudioElement>(null);\n const [showVideo, setShowVideo] = useState(hasLiveVideo(stream));\n\n useEffect(() => {\n const refresh = () => setShowVideo(hasLiveVideo(stream));\n refresh();\n if (!stream) return;\n const tracks = stream.getVideoTracks();\n tracks.forEach((t) => {\n t.addEventListener('mute', refresh);\n t.addEventListener('unmute', refresh);\n t.addEventListener('ended', refresh);\n });\n stream.addEventListener('addtrack', refresh);\n stream.addEventListener('removetrack', refresh);\n return () => {\n tracks.forEach((t) => {\n t.removeEventListener('mute', refresh);\n t.removeEventListener('unmute', refresh);\n t.removeEventListener('ended', refresh);\n });\n stream.removeEventListener('addtrack', refresh);\n stream.removeEventListener('removetrack', refresh);\n };\n }, [stream]);\n\n useEffect(() => {\n if (videoRef.current) videoRef.current.srcObject = stream;\n if (audioRef.current && !isLocal) {\n audioRef.current.srcObject = stream;\n void audioRef.current.play().catch(() => {});\n }\n }, [stream, isLocal, showVideo]);\n\n const connecting = !isLocal && connectionState !== undefined && connectionState !== 'connected';\n\n return (\n <div\n className={cn(\n 'relative flex aspect-video items-center justify-center overflow-hidden rounded-xl border border-border-seam bg-bg-sunken',\n className\n )}\n data-testid=\"voice-media-tile\"\n >\n {showVideo ? (\n <video\n ref={videoRef}\n autoPlay\n playsInline\n muted\n className={cn('h-full w-full object-cover', isLocal && '-scale-x-100')}\n />\n ) : (\n <MemberIdentity memberId={memberId} size=\"lg\" className=\"flex-col gap-2\" />\n )}\n {!isLocal && <audio ref={audioRef} autoPlay />}\n <div className=\"absolute bottom-2 left-2 flex items-center gap-1.5\">\n {muted && (\n <span\n className=\"inline-flex items-center gap-1 rounded-full bg-status-error-bg-subtle px-2 py-0.5 text-xs text-status-error-text\"\n aria-label={mutedLabel}\n >\n <MicOff className=\"h-3 w-3\" />\n </span>\n )}\n {connecting && (\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n {connectingLabel}\n </span>\n )}\n </div>\n {showVideo && (\n <div className=\"absolute left-2 top-2 rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-primary\">\n <MemberIdentity memberId={memberId} size=\"xs\" />\n </div>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;AA0BA,SAAS,EAAa,GAAqC;CACzD,OAAO,EACL,GAAQ,eAAe,EAAE,MAAM,MAAM,EAAE,eAAe,UAAU,EAAE,WAAW,CAAC,EAAE,KAAK;AAEzF;AAEA,IAAa,KAAiC,EAC5C,aACA,WACA,aAAU,IACV,WAAQ,IACR,oBACA,qBAAkB,eAClB,gBAAa,SACb,mBACI;CACJ,IAAM,IAAW,EAAyB,IAAI,GACxC,IAAW,EAAyB,IAAI,GACxC,CAAC,GAAW,KAAgB,EAAS,EAAa,CAAM,CAAC;CAyB/D,AAvBA,QAAgB;EACd,IAAM,UAAgB,EAAa,EAAa,CAAM,CAAC;EAEvD,IADA,EAAQ,GACJ,CAAC,GAAQ;EACb,IAAM,IAAS,EAAO,eAAe;EAQrC,OAPA,EAAO,SAAS,MAAM;GAGpB,AAFA,EAAE,iBAAiB,QAAQ,CAAO,GAClC,EAAE,iBAAiB,UAAU,CAAO,GACpC,EAAE,iBAAiB,SAAS,CAAO;EACrC,CAAC,GACD,EAAO,iBAAiB,YAAY,CAAO,GAC3C,EAAO,iBAAiB,eAAe,CAAO,SACjC;GAOX,AANA,EAAO,SAAS,MAAM;IAGpB,AAFA,EAAE,oBAAoB,QAAQ,CAAO,GACrC,EAAE,oBAAoB,UAAU,CAAO,GACvC,EAAE,oBAAoB,SAAS,CAAO;GACxC,CAAC,GACD,EAAO,oBAAoB,YAAY,CAAO,GAC9C,EAAO,oBAAoB,eAAe,CAAO;EACnD;CACF,GAAG,CAAC,CAAM,CAAC,GAEX,QAAgB;EAEd,AADI,EAAS,YAAS,EAAS,QAAQ,YAAY,IAC/C,EAAS,WAAW,CAAC,MACvB,EAAS,QAAQ,YAAY,GAC7B,EAAc,QAAQ,KAAK,EAAE,YAAY,CAAC,CAAC;CAE/C,GAAG;EAAC;EAAQ;EAAS;CAAS,CAAC;CAE/B,IAAM,IAAa,CAAC,KAAW,MAAoB,KAAA,KAAa,MAAoB;CAEpF,OACE,kBAAC,OAAD;EACE,WAAW,EACT,4HACA,CACF;EACA,eAAY;YALd;GAOG,IACC,kBAAC,SAAD;IACE,KAAK;IACL,UAAA;IACA,aAAA;IACA,OAAA;IACA,WAAW,EAAG,8BAA8B,KAAW,cAAc;GACtE,CAAA,IAED,kBAAC,GAAD;IAA0B;IAAU,MAAK;IAAK,WAAU;GAAkB,CAAA;GAE3E,CAAC,KAAW,kBAAC,SAAD;IAAO,KAAK;IAAU,UAAA;GAAU,CAAA;GAC7C,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,KACC,kBAAC,QAAD;KACE,WAAU;KACV,cAAY;eAEZ,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA;IACzB,CAAA,GAEP,KACC,kBAAC,QAAD;KAAM,WAAU;eACb;IACG,CAAA,CAEL;;GACJ,KACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAA0B;KAAU,MAAK;IAAM,CAAA;GAC5C,CAAA;EAEJ;;AAET"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { ComChannel } from '../types';
|
|
3
|
+
|
|
4
|
+
interface VoiceChannelPanelProps {
|
|
5
|
+
channel: ComChannel;
|
|
6
|
+
canJoin: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare const VoiceChannelPanel: FC<VoiceChannelPanelProps>;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=VoiceChannelPanel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoiceChannelPanel.d.ts","sourceRoot":"","sources":["../../../src/comradecircle/components/VoiceChannelPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAQhC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,UAAU,sBAAsB;IAC9B,OAAO,EAAE,UAAU,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,eAAO,MAAM,iBAAiB,EAAE,EAAE,CAAC,sBAAsB,CAkLxD,CAAC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useComradeCircle as e } from "../context/ComradeCircleContext.js";
|
|
2
|
+
import "../context/index.js";
|
|
3
|
+
import { useTr as t } from "../hooks/useTr.js";
|
|
4
|
+
import { useComRtcConfiguration as n, useComVoiceSession as r } from "../hooks/useVoiceApi.js";
|
|
5
|
+
import { useVoiceCall as i } from "../hooks/useVoiceCall.js";
|
|
6
|
+
import "../hooks/index.js";
|
|
7
|
+
import { MemberIdentity as a } from "./MemberIdentity.js";
|
|
8
|
+
import { MediaTile as o } from "./MediaTile.js";
|
|
9
|
+
import { Mic as s, MicOff as c, PhoneOff as l, Radio as u, Users as d, Video as f, VideoOff as p } from "lucide-react";
|
|
10
|
+
import { jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
11
|
+
import { Button as g } from "@burdenoff/fe-libs/ui";
|
|
12
|
+
//#region src/comradecircle/components/VoiceChannelPanel.tsx
|
|
13
|
+
var _ = ({ channel: _, canJoin: v }) => {
|
|
14
|
+
let y = t(), { currentUser: b } = e(), x = i(_.id), S = x.status === "connected" || x.status === "leaving", C = r(v ? _.id : void 0, S ? !1 : 15e3), w = n(v), T = (S ? x.session : C.data ?? null)?.participants.filter((e) => !e.leftAt) ?? [], E = w.data?.maxMeshParticipants, D = !S && E !== void 0 && T.length >= E, O = b?.id ?? "";
|
|
15
|
+
return /* @__PURE__ */ h("section", {
|
|
16
|
+
className: "flex flex-col gap-4 rounded-2xl border border-border-seam p-4",
|
|
17
|
+
"aria-label": y("comradecircle.voice.roomLabel", "Voice room"),
|
|
18
|
+
"data-testid": "voice-channel-panel",
|
|
19
|
+
children: [
|
|
20
|
+
/* @__PURE__ */ h("div", {
|
|
21
|
+
className: "flex flex-wrap items-center justify-between gap-3",
|
|
22
|
+
children: [/* @__PURE__ */ h("div", {
|
|
23
|
+
className: "flex items-center gap-2 text-sm text-text-secondary",
|
|
24
|
+
children: [
|
|
25
|
+
/* @__PURE__ */ m(u, { className: "h-4 w-4 text-accent" }),
|
|
26
|
+
T.length > 0 ? y("comradecircle.voice.inRoom", "{{count}} in the room", { count: T.length }) : y("comradecircle.voice.emptyRoom", "Nobody is here yet"),
|
|
27
|
+
E !== void 0 && /* @__PURE__ */ m("span", {
|
|
28
|
+
className: "text-text-tertiary",
|
|
29
|
+
children: y("comradecircle.voice.capacity", "(up to {{cap}} people)", { cap: E })
|
|
30
|
+
})
|
|
31
|
+
]
|
|
32
|
+
}), S ? /* @__PURE__ */ h("div", {
|
|
33
|
+
className: "flex items-center gap-2",
|
|
34
|
+
role: "group",
|
|
35
|
+
children: [
|
|
36
|
+
/* @__PURE__ */ h(g, {
|
|
37
|
+
variant: x.muted ? "destructive" : "outline",
|
|
38
|
+
size: "sm",
|
|
39
|
+
onClick: () => void x.toggleMute(),
|
|
40
|
+
"aria-pressed": x.muted,
|
|
41
|
+
children: [x.muted ? /* @__PURE__ */ m(c, { className: "mr-1.5 h-4 w-4" }) : /* @__PURE__ */ m(s, { className: "mr-1.5 h-4 w-4" }), x.muted ? y("comradecircle.voice.unmute", "Unmute") : y("comradecircle.voice.mute", "Mute")]
|
|
42
|
+
}),
|
|
43
|
+
/* @__PURE__ */ h(g, {
|
|
44
|
+
variant: "outline",
|
|
45
|
+
size: "sm",
|
|
46
|
+
onClick: () => void x.toggleVideo(),
|
|
47
|
+
"aria-pressed": x.videoOn,
|
|
48
|
+
children: [x.videoOn ? /* @__PURE__ */ m(p, { className: "mr-1.5 h-4 w-4" }) : /* @__PURE__ */ m(f, { className: "mr-1.5 h-4 w-4" }), x.videoOn ? y("comradecircle.voice.cameraOff", "Stop camera") : y("comradecircle.voice.cameraOn", "Start camera")]
|
|
49
|
+
}),
|
|
50
|
+
/* @__PURE__ */ h(g, {
|
|
51
|
+
variant: "destructive",
|
|
52
|
+
size: "sm",
|
|
53
|
+
onClick: () => void x.leave(),
|
|
54
|
+
disabled: x.status === "leaving",
|
|
55
|
+
children: [/* @__PURE__ */ m(l, { className: "mr-1.5 h-4 w-4" }), y("comradecircle.voice.leave", "Leave")]
|
|
56
|
+
})
|
|
57
|
+
]
|
|
58
|
+
}) : v ? /* @__PURE__ */ h("div", {
|
|
59
|
+
className: "flex items-center gap-2",
|
|
60
|
+
children: [/* @__PURE__ */ h(g, {
|
|
61
|
+
size: "sm",
|
|
62
|
+
onClick: () => void x.join(!1),
|
|
63
|
+
disabled: x.status === "joining" || D,
|
|
64
|
+
children: [/* @__PURE__ */ m(s, { className: "mr-1.5 h-4 w-4" }), x.status === "joining" ? y("comradecircle.voice.joining", "Joining…") : y("comradecircle.voice.join", "Join voice")]
|
|
65
|
+
}), /* @__PURE__ */ h(g, {
|
|
66
|
+
variant: "outline",
|
|
67
|
+
size: "sm",
|
|
68
|
+
onClick: () => void x.join(!0),
|
|
69
|
+
disabled: x.status === "joining" || D,
|
|
70
|
+
children: [/* @__PURE__ */ m(f, { className: "mr-1.5 h-4 w-4" }), y("comradecircle.voice.joinWithVideo", "Join with video")]
|
|
71
|
+
})]
|
|
72
|
+
}) : null]
|
|
73
|
+
}),
|
|
74
|
+
x.error && /* @__PURE__ */ m("p", {
|
|
75
|
+
role: "alert",
|
|
76
|
+
className: "rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text",
|
|
77
|
+
children: x.error
|
|
78
|
+
}),
|
|
79
|
+
D && /* @__PURE__ */ m("p", {
|
|
80
|
+
className: "text-sm text-text-secondary",
|
|
81
|
+
children: y("comradecircle.voice.full", "This room is full right now. Try again when someone leaves.")
|
|
82
|
+
}),
|
|
83
|
+
!v && !S && /* @__PURE__ */ m("p", {
|
|
84
|
+
className: "text-sm text-text-secondary",
|
|
85
|
+
children: y("comradecircle.voice.joinCircleFirst", "Join this circle to talk in its voice channels.")
|
|
86
|
+
}),
|
|
87
|
+
S ? /* @__PURE__ */ m("div", {
|
|
88
|
+
className: "grid gap-3 sm:grid-cols-2 xl:grid-cols-3",
|
|
89
|
+
children: T.map((e) => e.memberId === O ? /* @__PURE__ */ m(o, {
|
|
90
|
+
memberId: e.memberId,
|
|
91
|
+
stream: x.localStream,
|
|
92
|
+
isLocal: !0,
|
|
93
|
+
muted: x.muted,
|
|
94
|
+
mutedLabel: y("comradecircle.voice.muted", "Muted")
|
|
95
|
+
}, e.id) : /* @__PURE__ */ m(o, {
|
|
96
|
+
memberId: e.memberId,
|
|
97
|
+
stream: x.remoteStreams.get(e.memberId) ?? null,
|
|
98
|
+
muted: e.muted,
|
|
99
|
+
connectionState: x.peerStates.get(e.memberId) ?? "new",
|
|
100
|
+
connectingLabel: y("comradecircle.voice.connecting", "Connecting…"),
|
|
101
|
+
mutedLabel: y("comradecircle.voice.muted", "Muted")
|
|
102
|
+
}, e.id))
|
|
103
|
+
}) : T.length > 0 ? /* @__PURE__ */ m("ul", {
|
|
104
|
+
className: "flex flex-wrap gap-3",
|
|
105
|
+
"aria-label": y("comradecircle.voice.whoIsHere", "In the room"),
|
|
106
|
+
children: T.map((e) => /* @__PURE__ */ h("li", {
|
|
107
|
+
className: "flex items-center gap-1.5 rounded-full bg-bg-sunken px-3 py-1",
|
|
108
|
+
children: [/* @__PURE__ */ m(a, {
|
|
109
|
+
memberId: e.memberId,
|
|
110
|
+
size: "xs"
|
|
111
|
+
}), e.muted && /* @__PURE__ */ m(c, { className: "h-3 w-3 text-text-tertiary" })]
|
|
112
|
+
}, e.id))
|
|
113
|
+
}) : /* @__PURE__ */ h("div", {
|
|
114
|
+
className: "flex flex-col items-center gap-2 py-6 text-center text-sm text-text-secondary",
|
|
115
|
+
children: [/* @__PURE__ */ m(d, { className: "h-8 w-8 text-text-tertiary" }), y("comradecircle.voice.emptyHint", "Drop in to talk live. Audio and video go directly between participants.")]
|
|
116
|
+
})
|
|
117
|
+
]
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
//#endregion
|
|
121
|
+
export { _ as VoiceChannelPanel };
|
|
122
|
+
|
|
123
|
+
//# sourceMappingURL=VoiceChannelPanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoiceChannelPanel.js","names":[],"sources":["../../../src/comradecircle/components/VoiceChannelPanel.tsx"],"sourcesContent":["/**\n * VoiceChannelPanel — the live room for a VOICE channel (BOFF-4450).\n *\n * Before joining: who is in the room right now (polled) and Join buttons\n * (audio, or audio + camera). In the call: a tile per participant with\n * mute / camera / leave controls. Rooms are a bounded mesh — the server caps\n * participants (`maxMeshParticipants`) and the panel says so when full.\n */\nimport type { FC } from 'react';\nimport { Mic, MicOff, PhoneOff, Video, VideoOff, Users, Radio } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\n\nimport { useComRtcConfiguration, useComVoiceSession, useTr, useVoiceCall } from '../hooks';\nimport { useComradeCircle } from '../context';\nimport { MediaTile } from './MediaTile';\nimport { MemberIdentity } from './MemberIdentity';\nimport type { ComChannel } from '../types';\n\ninterface VoiceChannelPanelProps {\n channel: ComChannel;\n canJoin: boolean;\n}\n\nexport const VoiceChannelPanel: FC<VoiceChannelPanelProps> = ({ channel, canJoin }) => {\n const tr = useTr();\n const { currentUser } = useComradeCircle();\n const call = useVoiceCall(channel.id);\n const inCall = call.status === 'connected' || call.status === 'leaving';\n // Reading the room requires circle membership server-side.\n const preview = useComVoiceSession(canJoin ? channel.id : undefined, inCall ? false : 15_000);\n const rtc = useComRtcConfiguration(canJoin);\n const roomSession = inCall ? call.session : (preview.data ?? null);\n const participants = roomSession?.participants.filter((p) => !p.leftAt) ?? [];\n const cap = rtc.data?.maxMeshParticipants;\n const full = !inCall && cap !== undefined && participants.length >= cap;\n const selfId = currentUser?.id ?? '';\n\n return (\n <section\n className=\"flex flex-col gap-4 rounded-2xl border border-border-seam p-4\"\n aria-label={tr('comradecircle.voice.roomLabel', 'Voice room')}\n data-testid=\"voice-channel-panel\"\n >\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Radio className=\"h-4 w-4 text-accent\" />\n {participants.length > 0\n ? tr('comradecircle.voice.inRoom', '{{count}} in the room', {\n count: participants.length,\n })\n : tr('comradecircle.voice.emptyRoom', 'Nobody is here yet')}\n {cap !== undefined && (\n <span className=\"text-text-tertiary\">\n {tr('comradecircle.voice.capacity', '(up to {{cap}} people)', { cap })}\n </span>\n )}\n </div>\n\n {inCall ? (\n <div className=\"flex items-center gap-2\" role=\"group\">\n <Button\n variant={call.muted ? 'destructive' : 'outline'}\n size=\"sm\"\n onClick={() => void call.toggleMute()}\n aria-pressed={call.muted}\n >\n {call.muted ? (\n <MicOff className=\"mr-1.5 h-4 w-4\" />\n ) : (\n <Mic className=\"mr-1.5 h-4 w-4\" />\n )}\n {call.muted\n ? tr('comradecircle.voice.unmute', 'Unmute')\n : tr('comradecircle.voice.mute', 'Mute')}\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => void call.toggleVideo()}\n aria-pressed={call.videoOn}\n >\n {call.videoOn ? (\n <VideoOff className=\"mr-1.5 h-4 w-4\" />\n ) : (\n <Video className=\"mr-1.5 h-4 w-4\" />\n )}\n {call.videoOn\n ? tr('comradecircle.voice.cameraOff', 'Stop camera')\n : tr('comradecircle.voice.cameraOn', 'Start camera')}\n </Button>\n <Button\n variant=\"destructive\"\n size=\"sm\"\n onClick={() => void call.leave()}\n disabled={call.status === 'leaving'}\n >\n <PhoneOff className=\"mr-1.5 h-4 w-4\" />\n {tr('comradecircle.voice.leave', 'Leave')}\n </Button>\n </div>\n ) : canJoin ? (\n <div className=\"flex items-center gap-2\">\n <Button\n size=\"sm\"\n onClick={() => void call.join(false)}\n disabled={call.status === 'joining' || full}\n >\n <Mic className=\"mr-1.5 h-4 w-4\" />\n {call.status === 'joining'\n ? tr('comradecircle.voice.joining', 'Joining…')\n : tr('comradecircle.voice.join', 'Join voice')}\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => void call.join(true)}\n disabled={call.status === 'joining' || full}\n >\n <Video className=\"mr-1.5 h-4 w-4\" />\n {tr('comradecircle.voice.joinWithVideo', 'Join with video')}\n </Button>\n </div>\n ) : null}\n </div>\n\n {call.error && (\n <p\n role=\"alert\"\n className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\"\n >\n {call.error}\n </p>\n )}\n {full && (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'comradecircle.voice.full',\n 'This room is full right now. Try again when someone leaves.'\n )}\n </p>\n )}\n {!canJoin && !inCall && (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'comradecircle.voice.joinCircleFirst',\n 'Join this circle to talk in its voice channels.'\n )}\n </p>\n )}\n\n {inCall ? (\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {participants.map((p) =>\n p.memberId === selfId ? (\n <MediaTile\n key={p.id}\n memberId={p.memberId}\n stream={call.localStream}\n isLocal\n muted={call.muted}\n mutedLabel={tr('comradecircle.voice.muted', 'Muted')}\n />\n ) : (\n <MediaTile\n key={p.id}\n memberId={p.memberId}\n stream={call.remoteStreams.get(p.memberId) ?? null}\n muted={p.muted}\n connectionState={call.peerStates.get(p.memberId) ?? 'new'}\n connectingLabel={tr('comradecircle.voice.connecting', 'Connecting…')}\n mutedLabel={tr('comradecircle.voice.muted', 'Muted')}\n />\n )\n )}\n </div>\n ) : participants.length > 0 ? (\n <ul\n className=\"flex flex-wrap gap-3\"\n aria-label={tr('comradecircle.voice.whoIsHere', 'In the room')}\n >\n {participants.map((p) => (\n <li\n key={p.id}\n className=\"flex items-center gap-1.5 rounded-full bg-bg-sunken px-3 py-1\"\n >\n <MemberIdentity memberId={p.memberId} size=\"xs\" />\n {p.muted && <MicOff className=\"h-3 w-3 text-text-tertiary\" />}\n </li>\n ))}\n </ul>\n ) : (\n <div className=\"flex flex-col items-center gap-2 py-6 text-center text-sm text-text-secondary\">\n <Users className=\"h-8 w-8 text-text-tertiary\" />\n {tr(\n 'comradecircle.voice.emptyHint',\n 'Drop in to talk live. Audio and video go directly between participants.'\n )}\n </div>\n )}\n </section>\n );\n};\n"],"mappings":"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"}
|
|
@@ -7,4 +7,7 @@ export * from './useCursorPager';
|
|
|
7
7
|
export * from './useFiles';
|
|
8
8
|
export * from './useTr';
|
|
9
9
|
export * from './useModerationApi';
|
|
10
|
+
export * from './useVoiceApi';
|
|
11
|
+
export * from './useVoiceCall';
|
|
12
|
+
export * from './useLiveStage';
|
|
10
13
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/comradecircle/hooks/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AACxB,cAAc,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/comradecircle/hooks/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AACxB,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ComLiveStream } from '../types';
|
|
2
|
+
|
|
3
|
+
export type StageRole = 'none' | 'host' | 'viewer';
|
|
4
|
+
export type StageSource = 'camera' | 'screen';
|
|
5
|
+
export interface LiveStageState {
|
|
6
|
+
role: StageRole;
|
|
7
|
+
busy: boolean;
|
|
8
|
+
stream: ComLiveStream | null;
|
|
9
|
+
/** Host: own preview. Viewer: the host's media once connected. */
|
|
10
|
+
media: MediaStream | null;
|
|
11
|
+
connectionState: RTCPeerConnectionState | null;
|
|
12
|
+
connectedViewers: number;
|
|
13
|
+
error: string | null;
|
|
14
|
+
goLive: (title: string, source: StageSource) => Promise<void>;
|
|
15
|
+
watch: (stream: ComLiveStream) => Promise<void>;
|
|
16
|
+
stop: () => Promise<void>;
|
|
17
|
+
clearError: () => void;
|
|
18
|
+
}
|
|
19
|
+
export declare function useLiveStage(circleId: string): LiveStageState;
|
|
20
|
+
//# sourceMappingURL=useLiveStage.d.ts.map
|