@jgengine/react 0.6.0 → 0.8.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/CHANGELOG.md +214 -2
- package/dist/chat.d.ts +52 -0
- package/dist/chat.js +68 -0
- package/dist/components.d.ts +52 -2
- package/dist/components.js +58 -2
- package/dist/display.d.ts +13 -0
- package/dist/display.js +44 -0
- package/dist/dragLayer.d.ts +67 -0
- package/dist/dragLayer.js +143 -0
- package/dist/gameIcons.d.ts +12 -0
- package/dist/gameIcons.js +282 -0
- package/dist/hooks.d.ts +70 -1
- package/dist/hooks.js +146 -1
- package/dist/identity.d.ts +65 -0
- package/dist/identity.js +94 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/map.d.ts +68 -0
- package/dist/map.js +210 -0
- package/dist/social.d.ts +108 -0
- package/dist/social.js +119 -0
- package/dist/voice.d.ts +58 -0
- package/dist/voice.js +130 -0
- package/llms.txt +1726 -0
- package/package.json +4 -3
package/dist/map.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useSyncExternalStore } from "react";
|
|
3
|
+
import { DEFAULT_MARKER_KINDS, markerKindStyle, } from "@jgengine/core/world/markers";
|
|
4
|
+
import { bearingToCardinal, clampToMinimapEdge, compassBearing, projectToMinimap, relativeBearing, } from "@jgengine/core/world/minimap";
|
|
5
|
+
export function useMarkers(markers) {
|
|
6
|
+
return useSyncExternalStore(markers.subscribe, markers.snapshot, markers.snapshot);
|
|
7
|
+
}
|
|
8
|
+
export function useFog(fog) {
|
|
9
|
+
return useSyncExternalStore(fog.subscribe, fog.cells, fog.cells);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Framed circular minimap: optional baked terrain background, reveal-on-event
|
|
13
|
+
* fog overlay, categorized marker icons, and a facing arrow. Reads a core
|
|
14
|
+
* `MarkerSet` / `FogField`; supply your own `kindStyles` palette to reskin.
|
|
15
|
+
*/
|
|
16
|
+
export function Minimap({ markers, center, worldRadius, fog, size = 176, heading = 0, rotate = false, kindStyles = DEFAULT_MARKER_KINDS, background, mapBounds, className, title = "Map", children, }) {
|
|
17
|
+
const markerList = useMarkers(markers);
|
|
18
|
+
const fogCells = useSyncExternalStore(fog?.subscribe ?? NO_SUBSCRIBE, fog?.cells ?? NULL_CELLS, fog?.cells ?? NULL_CELLS);
|
|
19
|
+
const view = {
|
|
20
|
+
center,
|
|
21
|
+
worldRadius,
|
|
22
|
+
size,
|
|
23
|
+
...(rotate ? { rotate: -heading } : {}),
|
|
24
|
+
};
|
|
25
|
+
const half = size / 2;
|
|
26
|
+
const clipId = `mm-clip-${size}`;
|
|
27
|
+
const sorted = [...markerList].sort((a, b) => markerKindStyle(a.kind, kindStyles).priority - markerKindStyle(b.kind, kindStyles).priority);
|
|
28
|
+
const fogRects = [];
|
|
29
|
+
if (fogCells !== null) {
|
|
30
|
+
const cellPx = (fogCells.cellSize / worldRadius) * half;
|
|
31
|
+
for (let row = 0; row < fogCells.rows; row += 1) {
|
|
32
|
+
for (let col = 0; col < fogCells.cols; col += 1) {
|
|
33
|
+
if (fogCells.revealed[row * fogCells.cols + col])
|
|
34
|
+
continue;
|
|
35
|
+
const worldX = fogCells.minX + (col + 0.5) * fogCells.cellSize;
|
|
36
|
+
const worldZ = fogCells.minZ + (row + 0.5) * fogCells.cellSize;
|
|
37
|
+
const projected = projectToMinimap([worldX, worldZ], view);
|
|
38
|
+
if (projected.distance > worldRadius + fogCells.cellSize)
|
|
39
|
+
continue;
|
|
40
|
+
fogRects.push(_jsx("rect", { x: projected.x - cellPx / 2 - 0.5, y: projected.y - cellPx / 2 - 0.5, width: cellPx + 1, height: cellPx + 1, fill: "#0b0f14", opacity: 0.82 }, `fog-${col}-${row}`));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return (_jsxs("div", { className: className, "data-minimap": true, style: {
|
|
45
|
+
width: size,
|
|
46
|
+
borderRadius: 14,
|
|
47
|
+
padding: 8,
|
|
48
|
+
background: "linear-gradient(160deg, rgba(24,28,36,0.94), rgba(12,15,20,0.94))",
|
|
49
|
+
border: "1px solid rgba(148,163,184,0.28)",
|
|
50
|
+
boxShadow: "0 10px 30px rgba(0,0,0,0.45)",
|
|
51
|
+
color: "#e2e8f0",
|
|
52
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
53
|
+
}, children: [_jsxs("div", { style: {
|
|
54
|
+
display: "flex",
|
|
55
|
+
justifyContent: "space-between",
|
|
56
|
+
alignItems: "center",
|
|
57
|
+
marginBottom: 6,
|
|
58
|
+
fontSize: 10,
|
|
59
|
+
letterSpacing: 1.4,
|
|
60
|
+
textTransform: "uppercase",
|
|
61
|
+
color: "rgba(203,213,225,0.75)",
|
|
62
|
+
}, children: [_jsx("span", { children: title }), _jsx("span", { style: { color: "rgba(148,163,184,0.6)" }, children: bearingToCardinal(heading) })] }), _jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, "data-minimap-canvas": true, children: [_jsx("defs", { children: _jsx("clipPath", { id: clipId, children: _jsx("circle", { cx: half, cy: half, r: half - 1 }) }) }), _jsxs("g", { clipPath: `url(#${clipId})`, children: [_jsx("circle", { cx: half, cy: half, r: half - 1, fill: "#1c2733" }), background !== undefined
|
|
63
|
+
? (() => {
|
|
64
|
+
if (mapBounds === undefined) {
|
|
65
|
+
return (_jsx("image", { href: background, x: 0, y: 0, width: size, height: size, preserveAspectRatio: "xMidYMid slice", opacity: 0.9 }));
|
|
66
|
+
}
|
|
67
|
+
const pxPerWorld = half / worldRadius;
|
|
68
|
+
return (_jsx("image", { href: background, x: half + (mapBounds.minX - center[0]) * pxPerWorld, y: half + (mapBounds.minZ - center[1]) * pxPerWorld, width: (mapBounds.maxX - mapBounds.minX) * pxPerWorld, height: (mapBounds.maxZ - mapBounds.minZ) * pxPerWorld, preserveAspectRatio: "none", opacity: 0.92 }));
|
|
69
|
+
})()
|
|
70
|
+
: null, fogRects, _jsx("circle", { cx: half, cy: half, r: half - 1, fill: "none", stroke: "rgba(148,163,184,0.12)" }), sorted.map((marker) => {
|
|
71
|
+
const style = markerKindStyle(marker.kind, kindStyles);
|
|
72
|
+
const projected = projectToMinimap(marker.position, view);
|
|
73
|
+
const at = projected.inside ? projected : clampToMinimapEdge(projected, size);
|
|
74
|
+
return (_jsxs("g", { "data-marker-kind": marker.kind, children: [_jsx("circle", { cx: at.x, cy: at.y, r: 7, fill: "rgba(2,6,12,0.55)" }), _jsx("text", { x: at.x, y: at.y + 3.5, textAnchor: "middle", fontSize: 10, fill: style.color, style: { fontWeight: 700 }, children: style.glyph })] }, marker.id));
|
|
75
|
+
}), _jsx("g", { transform: `translate(${half} ${half}) rotate(${(rotate ? 0 : heading) * (180 / Math.PI)})`, children: _jsx("path", { d: "M0,-9 L6,7 L0,3 L-6,7 Z", fill: "#4ade80", stroke: "#052e16", strokeWidth: 0.75 }) })] }), _jsx("circle", { cx: half, cy: half, r: half - 1, fill: "none", stroke: "rgba(148,163,184,0.4)", strokeWidth: 1.5 }), _jsx("text", { x: half, y: 13, textAnchor: "middle", fontSize: 11, fill: "rgba(226,232,240,0.9)", style: { fontWeight: 700 }, children: "N" })] }), children] }));
|
|
76
|
+
}
|
|
77
|
+
const NO_SUBSCRIBE = () => () => undefined;
|
|
78
|
+
const NULL_CELLS = () => null;
|
|
79
|
+
const EMPTY_MARKERS_VALUE = [];
|
|
80
|
+
const EMPTY_MARKERS = () => EMPTY_MARKERS_VALUE;
|
|
81
|
+
const COMPASS_TICKS = [
|
|
82
|
+
{ cardinal: "N", bearing: 0 },
|
|
83
|
+
{ cardinal: "NE", bearing: Math.PI / 4 },
|
|
84
|
+
{ cardinal: "E", bearing: Math.PI / 2 },
|
|
85
|
+
{ cardinal: "SE", bearing: (3 * Math.PI) / 4 },
|
|
86
|
+
{ cardinal: "S", bearing: Math.PI },
|
|
87
|
+
{ cardinal: "SW", bearing: (5 * Math.PI) / 4 },
|
|
88
|
+
{ cardinal: "W", bearing: (3 * Math.PI) / 2 },
|
|
89
|
+
{ cardinal: "NW", bearing: (7 * Math.PI) / 4 },
|
|
90
|
+
];
|
|
91
|
+
/**
|
|
92
|
+
* Horizontal compass strip centered on the player's facing bearing, with the
|
|
93
|
+
* eight cardinals and optional marker pips (bearing to each `MarkerSet` entry).
|
|
94
|
+
*/
|
|
95
|
+
export function Compass({ heading, center, markers, width = 340, fov = (Math.PI * 2) / 3, kindStyles = DEFAULT_MARKER_KINDS, className, }) {
|
|
96
|
+
const markerList = useSyncExternalStore(markers?.subscribe ?? NO_SUBSCRIBE, markers?.snapshot ?? EMPTY_MARKERS, markers?.snapshot ?? EMPTY_MARKERS);
|
|
97
|
+
const half = fov / 2;
|
|
98
|
+
const toX = (bearing) => {
|
|
99
|
+
const delta = relativeBearing(bearing, heading);
|
|
100
|
+
if (Math.abs(delta) > half)
|
|
101
|
+
return null;
|
|
102
|
+
return width / 2 + (delta / half) * (width / 2);
|
|
103
|
+
};
|
|
104
|
+
return (_jsxs("div", { className: className, "data-compass": true, style: {
|
|
105
|
+
width,
|
|
106
|
+
height: 34,
|
|
107
|
+
position: "relative",
|
|
108
|
+
overflow: "hidden",
|
|
109
|
+
borderRadius: 8,
|
|
110
|
+
border: "1px solid rgba(148,163,184,0.28)",
|
|
111
|
+
background: "linear-gradient(180deg, rgba(18,22,30,0.92), rgba(10,13,18,0.92))",
|
|
112
|
+
color: "#e2e8f0",
|
|
113
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
114
|
+
}, children: [_jsx("div", { style: {
|
|
115
|
+
position: "absolute",
|
|
116
|
+
left: "50%",
|
|
117
|
+
top: 0,
|
|
118
|
+
bottom: 0,
|
|
119
|
+
width: 1,
|
|
120
|
+
background: "rgba(74,222,128,0.85)",
|
|
121
|
+
} }), COMPASS_TICKS.map((tick) => {
|
|
122
|
+
const x = toX(tick.bearing);
|
|
123
|
+
if (x === null)
|
|
124
|
+
return null;
|
|
125
|
+
const major = tick.cardinal.length === 1;
|
|
126
|
+
return (_jsx("span", { style: {
|
|
127
|
+
position: "absolute",
|
|
128
|
+
left: x,
|
|
129
|
+
top: major ? 7 : 11,
|
|
130
|
+
transform: "translateX(-50%)",
|
|
131
|
+
fontSize: major ? 13 : 9,
|
|
132
|
+
fontWeight: major ? 700 : 500,
|
|
133
|
+
color: major ? "#f8fafc" : "rgba(148,163,184,0.75)",
|
|
134
|
+
}, children: tick.cardinal }, tick.cardinal));
|
|
135
|
+
}), center !== undefined
|
|
136
|
+
? markerList.map((marker) => {
|
|
137
|
+
const bearing = compassBearing(center, [marker.position[0], marker.position[2]]);
|
|
138
|
+
const x = toX(bearing);
|
|
139
|
+
if (x === null)
|
|
140
|
+
return null;
|
|
141
|
+
const style = markerKindStyle(marker.kind, kindStyles);
|
|
142
|
+
return (_jsx("span", { "data-compass-marker": marker.kind, style: {
|
|
143
|
+
position: "absolute",
|
|
144
|
+
left: x,
|
|
145
|
+
bottom: 2,
|
|
146
|
+
transform: "translateX(-50%)",
|
|
147
|
+
fontSize: 11,
|
|
148
|
+
color: style.color,
|
|
149
|
+
fontWeight: 700,
|
|
150
|
+
}, children: style.glyph }, marker.id));
|
|
151
|
+
})
|
|
152
|
+
: null] }));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Full-bounds top-down world map (the "press M" overlay): baked terrain
|
|
156
|
+
* background, reveal-on-event fog, all markers with labels, and the player.
|
|
157
|
+
* Rectangular linear projection over the supplied world `bounds`.
|
|
158
|
+
*/
|
|
159
|
+
export function WorldMap({ markers, bounds, player, heading = 0, fog, background, width = 520, height, kindStyles = DEFAULT_MARKER_KINDS, className, title = "World Map", onClose, }) {
|
|
160
|
+
const markerList = useMarkers(markers);
|
|
161
|
+
const fogCells = useSyncExternalStore(fog?.subscribe ?? NO_SUBSCRIBE, fog?.cells ?? NULL_CELLS, fog?.cells ?? NULL_CELLS);
|
|
162
|
+
const worldW = bounds.maxX - bounds.minX;
|
|
163
|
+
const worldD = bounds.maxZ - bounds.minZ;
|
|
164
|
+
const mapH = height ?? Math.round((width * worldD) / worldW);
|
|
165
|
+
const project = (x, z) => ({
|
|
166
|
+
x: ((x - bounds.minX) / worldW) * width,
|
|
167
|
+
y: ((z - bounds.minZ) / worldD) * mapH,
|
|
168
|
+
});
|
|
169
|
+
return (_jsxs("div", { className: className, "data-world-map": true, style: {
|
|
170
|
+
borderRadius: 16,
|
|
171
|
+
padding: 14,
|
|
172
|
+
background: "linear-gradient(160deg, rgba(20,24,32,0.97), rgba(10,13,18,0.97))",
|
|
173
|
+
border: "1px solid rgba(148,163,184,0.35)",
|
|
174
|
+
boxShadow: "0 24px 60px rgba(0,0,0,0.6)",
|
|
175
|
+
color: "#e2e8f0",
|
|
176
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
177
|
+
}, children: [_jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }, children: [_jsx("span", { style: { fontSize: 13, fontWeight: 700, letterSpacing: 1.5, textTransform: "uppercase" }, children: title }), onClose !== undefined ? (_jsx("button", { type: "button", onClick: onClose, style: {
|
|
178
|
+
border: "1px solid rgba(148,163,184,0.4)",
|
|
179
|
+
borderRadius: 6,
|
|
180
|
+
background: "transparent",
|
|
181
|
+
color: "rgba(226,232,240,0.85)",
|
|
182
|
+
fontSize: 11,
|
|
183
|
+
padding: "3px 9px",
|
|
184
|
+
cursor: "pointer",
|
|
185
|
+
}, children: "Close" })) : null] }), _jsxs("svg", { width: width, height: mapH, viewBox: `0 0 ${width} ${mapH}`, "data-world-map-canvas": true, style: { borderRadius: 10 }, children: [_jsx("rect", { x: 0, y: 0, width: width, height: mapH, fill: "#16202b" }), background !== undefined ? (_jsx("image", { href: background, x: 0, y: 0, width: width, height: mapH, preserveAspectRatio: "none", opacity: 0.95 })) : null, fogCells !== null
|
|
186
|
+
? (() => {
|
|
187
|
+
const cellW = (fogCells.cellSize / worldW) * width;
|
|
188
|
+
const cellH = (fogCells.cellSize / worldD) * mapH;
|
|
189
|
+
const rects = [];
|
|
190
|
+
for (let row = 0; row < fogCells.rows; row += 1) {
|
|
191
|
+
for (let col = 0; col < fogCells.cols; col += 1) {
|
|
192
|
+
if (fogCells.revealed[row * fogCells.cols + col])
|
|
193
|
+
continue;
|
|
194
|
+
const at = project(fogCells.minX + col * fogCells.cellSize, fogCells.minZ + row * fogCells.cellSize);
|
|
195
|
+
rects.push(_jsx("rect", { x: at.x, y: at.y, width: cellW + 0.5, height: cellH + 0.5, fill: "#0b0f14", opacity: 0.86 }, `wfog-${col}-${row}`));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return rects;
|
|
199
|
+
})()
|
|
200
|
+
: null, markerList.map((marker) => {
|
|
201
|
+
const at = project(marker.position[0], marker.position[2]);
|
|
202
|
+
const style = markerKindStyle(marker.kind, kindStyles);
|
|
203
|
+
return (_jsxs("g", { "data-world-marker": marker.kind, children: [_jsx("circle", { cx: at.x, cy: at.y, r: 9, fill: "rgba(2,6,12,0.6)", stroke: style.color, strokeWidth: 1.2 }), _jsx("text", { x: at.x, y: at.y + 4, textAnchor: "middle", fontSize: 12, fill: style.color, style: { fontWeight: 700 }, children: style.glyph }), marker.label !== undefined ? (_jsx("text", { x: at.x + 12, y: at.y + 4, fontSize: 11, fill: "rgba(226,232,240,0.85)", children: marker.label })) : null] }, marker.id));
|
|
204
|
+
}), player !== undefined
|
|
205
|
+
? (() => {
|
|
206
|
+
const at = project(player[0], player[1]);
|
|
207
|
+
return (_jsx("g", { transform: `translate(${at.x} ${at.y}) rotate(${heading * (180 / Math.PI)})`, children: _jsx("path", { d: "M0,-11 L7,9 L0,4 L-7,9 Z", fill: "#4ade80", stroke: "#052e16", strokeWidth: 1 }) }));
|
|
208
|
+
})()
|
|
209
|
+
: null] })] }));
|
|
210
|
+
}
|
package/dist/social.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { FriendEntry, FriendRequestEntry, PartyInviteEntry, PartyMemberEntry, WorldInvite, WorldInviteTarget } from "@jgengine/core/game/social";
|
|
3
|
+
import { type MatchFilter, type SessionListing } from "@jgengine/core/multiplayer/matchmaking";
|
|
4
|
+
export declare function PresenceDot({ userId, className }: {
|
|
5
|
+
userId: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
8
|
+
export declare function FriendRow({ friend, className, dotClassName, children, }: {
|
|
9
|
+
friend: FriendEntry;
|
|
10
|
+
className?: string;
|
|
11
|
+
dotClassName?: string;
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
14
|
+
export declare function FriendsList({ className, rowClassName, dotClassName, emptyState, renderFriend, }: {
|
|
15
|
+
className?: string;
|
|
16
|
+
rowClassName?: string;
|
|
17
|
+
dotClassName?: string;
|
|
18
|
+
emptyState?: ReactNode;
|
|
19
|
+
renderFriend?: (friend: FriendEntry) => ReactNode;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
export declare function AddFriendButton({ toUserId, className, children, onRequested, onRejected, }: {
|
|
22
|
+
toUserId: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
children?: ReactNode;
|
|
25
|
+
onRequested?: (requestId: string) => void;
|
|
26
|
+
onRejected?: (reason: string) => void;
|
|
27
|
+
}): import("react").JSX.Element;
|
|
28
|
+
export declare function FriendRequestsList({ className, rowClassName, acceptClassName, declineClassName, emptyState, renderRequest, }: {
|
|
29
|
+
className?: string;
|
|
30
|
+
rowClassName?: string;
|
|
31
|
+
acceptClassName?: string;
|
|
32
|
+
declineClassName?: string;
|
|
33
|
+
emptyState?: ReactNode;
|
|
34
|
+
renderRequest?: (request: FriendRequestEntry) => ReactNode;
|
|
35
|
+
}): import("react").JSX.Element;
|
|
36
|
+
export declare function PartyMemberRow({ member, className, dotClassName, children, }: {
|
|
37
|
+
member: PartyMemberEntry;
|
|
38
|
+
className?: string;
|
|
39
|
+
dotClassName?: string;
|
|
40
|
+
children?: ReactNode;
|
|
41
|
+
}): import("react").JSX.Element;
|
|
42
|
+
export declare function PartyFrame({ className, rowClassName, dotClassName, emptyState, renderMember, }: {
|
|
43
|
+
className?: string;
|
|
44
|
+
rowClassName?: string;
|
|
45
|
+
dotClassName?: string;
|
|
46
|
+
emptyState?: ReactNode;
|
|
47
|
+
renderMember?: (member: PartyMemberEntry) => ReactNode;
|
|
48
|
+
}): import("react").JSX.Element;
|
|
49
|
+
export declare function PartyInviteToast({ className, acceptClassName, declineClassName, renderInvite, }: {
|
|
50
|
+
className?: string;
|
|
51
|
+
acceptClassName?: string;
|
|
52
|
+
declineClassName?: string;
|
|
53
|
+
renderInvite?: (invite: PartyInviteEntry) => ReactNode;
|
|
54
|
+
}): import("react").JSX.Element | null;
|
|
55
|
+
export declare function LeavePartyButton({ className, children, }: {
|
|
56
|
+
className?: string;
|
|
57
|
+
children?: ReactNode;
|
|
58
|
+
}): import("react").JSX.Element | null;
|
|
59
|
+
export declare function WorldInviteToast({ className, acceptClassName, declineClassName, onAccepted, renderInvite, }: {
|
|
60
|
+
className?: string;
|
|
61
|
+
acceptClassName?: string;
|
|
62
|
+
declineClassName?: string;
|
|
63
|
+
onAccepted: (target: WorldInviteTarget) => void;
|
|
64
|
+
renderInvite?: (invite: WorldInvite) => ReactNode;
|
|
65
|
+
}): import("react").JSX.Element | null;
|
|
66
|
+
export declare function InviteToWorldButton({ toUserId, target, className, children, onInvited, onRejected, }: {
|
|
67
|
+
toUserId: string;
|
|
68
|
+
target: WorldInviteTarget;
|
|
69
|
+
className?: string;
|
|
70
|
+
children?: ReactNode;
|
|
71
|
+
onInvited?: (inviteId: string) => void;
|
|
72
|
+
onRejected?: (reason: string) => void;
|
|
73
|
+
}): import("react").JSX.Element;
|
|
74
|
+
export declare function WorldBrowser({ listings, onJoin, className, rowClassName, joinClassName, emptyState, renderListing, }: {
|
|
75
|
+
listings: readonly SessionListing[];
|
|
76
|
+
onJoin: (listing: SessionListing) => void;
|
|
77
|
+
className?: string;
|
|
78
|
+
rowClassName?: string;
|
|
79
|
+
joinClassName?: string;
|
|
80
|
+
emptyState?: ReactNode;
|
|
81
|
+
renderListing?: (listing: SessionListing) => ReactNode;
|
|
82
|
+
}): import("react").JSX.Element;
|
|
83
|
+
export declare function JoinByCode({ onJoin, className, inputClassName, buttonClassName, placeholder, children, }: {
|
|
84
|
+
onJoin: (code: string) => void;
|
|
85
|
+
className?: string;
|
|
86
|
+
inputClassName?: string;
|
|
87
|
+
buttonClassName?: string;
|
|
88
|
+
placeholder?: string;
|
|
89
|
+
children?: ReactNode;
|
|
90
|
+
}): import("react").JSX.Element;
|
|
91
|
+
export declare function QuickMatchButton({ listings, onJoin, onNoMatch, filter, className, children, }: {
|
|
92
|
+
listings: readonly SessionListing[];
|
|
93
|
+
onJoin: (listing: SessionListing) => void;
|
|
94
|
+
onNoMatch?: () => void;
|
|
95
|
+
filter?: MatchFilter;
|
|
96
|
+
className?: string;
|
|
97
|
+
children?: ReactNode;
|
|
98
|
+
}): import("react").JSX.Element;
|
|
99
|
+
export declare function EmoteWheel({ emotes, radius, open, className, emoteClassName, onPlayed, onRejected, renderEmote, }: {
|
|
100
|
+
emotes: readonly string[];
|
|
101
|
+
radius?: number;
|
|
102
|
+
open?: boolean;
|
|
103
|
+
className?: string;
|
|
104
|
+
emoteClassName?: string;
|
|
105
|
+
onPlayed?: (emoteId: string) => void;
|
|
106
|
+
onRejected?: (reason: string) => void;
|
|
107
|
+
renderEmote?: (emoteId: string) => ReactNode;
|
|
108
|
+
}): import("react").JSX.Element | null;
|
package/dist/social.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { normalizeJoinCode, quickMatch, } from "@jgengine/core/multiplayer/matchmaking";
|
|
4
|
+
import { useGameContext } from "./provider.js";
|
|
5
|
+
import { useFriendRequests, useFriends, useParty, usePartyInvites, usePresence, useWorldInvites, } from "./hooks.js";
|
|
6
|
+
export function PresenceDot({ userId, className }) {
|
|
7
|
+
const presence = usePresence(userId);
|
|
8
|
+
return (_jsx("span", { className: className, "data-presence-dot": true, "data-user": userId, "data-online": presence.online, "data-server": presence.serverId }));
|
|
9
|
+
}
|
|
10
|
+
export function FriendRow({ friend, className, dotClassName, children, }) {
|
|
11
|
+
return (_jsxs("div", { className: className, "data-friend": friend.userId, "data-online": friend.online, children: [_jsx(PresenceDot, { userId: friend.userId, className: dotClassName }), _jsx("span", { "data-friend-name": true, children: friend.userId }), children] }));
|
|
12
|
+
}
|
|
13
|
+
export function FriendsList({ className, rowClassName, dotClassName, emptyState, renderFriend, }) {
|
|
14
|
+
const friends = useFriends();
|
|
15
|
+
return (_jsx("div", { className: className, "data-friends-list": true, children: friends.length === 0
|
|
16
|
+
? emptyState ?? null
|
|
17
|
+
: friends.map((friend) => renderFriend !== undefined ? (renderFriend(friend)) : (_jsx(FriendRow, { friend: friend, className: rowClassName, dotClassName: dotClassName }, friend.userId))) }));
|
|
18
|
+
}
|
|
19
|
+
export function AddFriendButton({ toUserId, className, children, onRequested, onRejected, }) {
|
|
20
|
+
const ctx = useGameContext();
|
|
21
|
+
const denied = ctx.game.social.friends.canRequest(ctx.player.userId, toUserId);
|
|
22
|
+
return (_jsx("button", { type: "button", className: className, "data-add-friend": toUserId, disabled: denied !== null, title: denied?.reason, onClick: () => {
|
|
23
|
+
const result = ctx.game.social.friends.request(ctx.player.userId, toUserId);
|
|
24
|
+
if ("reason" in result)
|
|
25
|
+
onRejected?.(result.reason);
|
|
26
|
+
else
|
|
27
|
+
onRequested?.(result.requestId);
|
|
28
|
+
}, children: children ?? "Add friend" }));
|
|
29
|
+
}
|
|
30
|
+
export function FriendRequestsList({ className, rowClassName, acceptClassName, declineClassName, emptyState, renderRequest, }) {
|
|
31
|
+
const ctx = useGameContext();
|
|
32
|
+
const requests = useFriendRequests();
|
|
33
|
+
return (_jsx("div", { className: className, "data-friend-requests": true, children: requests.length === 0
|
|
34
|
+
? emptyState ?? null
|
|
35
|
+
: requests.map((request) => renderRequest !== undefined ? (renderRequest(request)) : (_jsxs("div", { className: rowClassName, "data-friend-request": request.requestId, children: [_jsx("span", { "data-request-from": true, children: request.fromUserId }), _jsx("button", { type: "button", className: acceptClassName, "data-accept": true, onClick: () => ctx.game.social.friends.accept(ctx.player.userId, request.requestId), children: "Accept" }), _jsx("button", { type: "button", className: declineClassName, "data-decline": true, onClick: () => ctx.game.social.friends.decline(ctx.player.userId, request.requestId), children: "Decline" })] }, request.requestId))) }));
|
|
36
|
+
}
|
|
37
|
+
export function PartyMemberRow({ member, className, dotClassName, children, }) {
|
|
38
|
+
return (_jsxs("div", { className: className, "data-party-member": member.userId, "data-role": member.role, children: [_jsx(PresenceDot, { userId: member.userId, className: dotClassName }), _jsx("span", { "data-member-name": true, children: member.userId }), children] }));
|
|
39
|
+
}
|
|
40
|
+
export function PartyFrame({ className, rowClassName, dotClassName, emptyState, renderMember, }) {
|
|
41
|
+
const members = useParty();
|
|
42
|
+
return (_jsx("div", { className: className, "data-party-frame": true, "data-party-size": members.length, children: members.length === 0
|
|
43
|
+
? emptyState ?? null
|
|
44
|
+
: members.map((member) => renderMember !== undefined ? (renderMember(member)) : (_jsx(PartyMemberRow, { member: member, className: rowClassName, dotClassName: dotClassName }, member.userId))) }));
|
|
45
|
+
}
|
|
46
|
+
export function PartyInviteToast({ className, acceptClassName, declineClassName, renderInvite, }) {
|
|
47
|
+
const ctx = useGameContext();
|
|
48
|
+
const invites = usePartyInvites();
|
|
49
|
+
if (invites.length === 0)
|
|
50
|
+
return null;
|
|
51
|
+
return (_jsx("div", { className: className, "data-party-invites": true, children: invites.map((invite) => renderInvite !== undefined ? (renderInvite(invite)) : (_jsxs("div", { "data-party-invite": invite.inviteId, children: [_jsx("span", { "data-invite-from": true, children: invite.fromUserId }), _jsx("button", { type: "button", className: acceptClassName, "data-accept": true, onClick: () => ctx.game.social.party.accept(ctx.player.userId, invite.inviteId), children: "Join" }), _jsx("button", { type: "button", className: declineClassName, "data-decline": true, onClick: () => ctx.game.social.party.decline(ctx.player.userId, invite.inviteId), children: "Decline" })] }, invite.inviteId))) }));
|
|
52
|
+
}
|
|
53
|
+
export function LeavePartyButton({ className, children, }) {
|
|
54
|
+
const ctx = useGameContext();
|
|
55
|
+
const members = useParty();
|
|
56
|
+
if (members.length === 0)
|
|
57
|
+
return null;
|
|
58
|
+
return (_jsx("button", { type: "button", className: className, "data-leave-party": true, onClick: () => ctx.game.social.party.leave(ctx.player.userId), children: children ?? "Leave party" }));
|
|
59
|
+
}
|
|
60
|
+
export function WorldInviteToast({ className, acceptClassName, declineClassName, onAccepted, renderInvite, }) {
|
|
61
|
+
const ctx = useGameContext();
|
|
62
|
+
const invites = useWorldInvites();
|
|
63
|
+
if (invites.length === 0)
|
|
64
|
+
return null;
|
|
65
|
+
return (_jsx("div", { className: className, "data-world-invites": true, children: invites.map((invite) => renderInvite !== undefined ? (renderInvite(invite)) : (_jsxs("div", { "data-world-invite": invite.id, children: [_jsx("span", { "data-invite-from": true, children: invite.fromUserId }), _jsx("span", { "data-invite-server": true, children: invite.serverId }), _jsx("button", { type: "button", className: acceptClassName, "data-accept": true, onClick: () => {
|
|
66
|
+
const result = ctx.game.social.worldInvites.accept(ctx.player.userId, invite.id);
|
|
67
|
+
if ("target" in result)
|
|
68
|
+
onAccepted(result.target);
|
|
69
|
+
}, children: "Join" }), _jsx("button", { type: "button", className: declineClassName, "data-decline": true, onClick: () => ctx.game.social.worldInvites.decline(ctx.player.userId, invite.id), children: "Decline" })] }, invite.id))) }));
|
|
70
|
+
}
|
|
71
|
+
export function InviteToWorldButton({ toUserId, target, className, children, onInvited, onRejected, }) {
|
|
72
|
+
const ctx = useGameContext();
|
|
73
|
+
const denied = ctx.game.social.worldInvites.canInvite(ctx.player.userId, toUserId);
|
|
74
|
+
return (_jsx("button", { type: "button", className: className, "data-invite-to-world": toUserId, disabled: denied !== null, title: denied?.reason, onClick: () => {
|
|
75
|
+
const result = ctx.game.social.worldInvites.invite(ctx.player.userId, toUserId, target);
|
|
76
|
+
if ("reason" in result)
|
|
77
|
+
onRejected?.(result.reason);
|
|
78
|
+
else
|
|
79
|
+
onInvited?.(result.inviteId);
|
|
80
|
+
}, children: children ?? "Invite to world" }));
|
|
81
|
+
}
|
|
82
|
+
export function WorldBrowser({ listings, onJoin, className, rowClassName, joinClassName, emptyState, renderListing, }) {
|
|
83
|
+
return (_jsx("div", { className: className, "data-world-browser": true, children: listings.length === 0
|
|
84
|
+
? emptyState ?? null
|
|
85
|
+
: listings.map((listing) => renderListing !== undefined ? (renderListing(listing)) : (_jsxs("div", { className: rowClassName, "data-world": listing.serverId, "data-mode": listing.mode, "data-full": listing.memberCount >= listing.slotsPerServer, children: [_jsx("span", { "data-world-label": true, children: listing.label ?? listing.serverId }), _jsxs("span", { "data-world-count": true, children: [listing.memberCount, "/", listing.slotsPerServer] }), _jsx("button", { type: "button", className: joinClassName, "data-join": true, disabled: listing.memberCount >= listing.slotsPerServer, onClick: () => onJoin(listing), children: "Join" })] }, listing.serverId))) }));
|
|
86
|
+
}
|
|
87
|
+
export function JoinByCode({ onJoin, className, inputClassName, buttonClassName, placeholder, children, }) {
|
|
88
|
+
const [code, setCode] = useState("");
|
|
89
|
+
function submit(event) {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
const normalized = normalizeJoinCode(code);
|
|
92
|
+
if (normalized.length === 0)
|
|
93
|
+
return;
|
|
94
|
+
onJoin(normalized);
|
|
95
|
+
setCode("");
|
|
96
|
+
}
|
|
97
|
+
return (_jsxs("form", { className: className, "data-join-by-code": true, onSubmit: submit, children: [_jsx("input", { className: inputClassName, type: "text", value: code, placeholder: placeholder ?? "Join code", onChange: (event) => setCode(event.target.value) }), _jsx("button", { type: "submit", className: buttonClassName, "data-join": true, children: children ?? "Join" })] }));
|
|
98
|
+
}
|
|
99
|
+
export function QuickMatchButton({ listings, onJoin, onNoMatch, filter, className, children, }) {
|
|
100
|
+
return (_jsx("button", { type: "button", className: className, "data-quick-match": true, onClick: () => {
|
|
101
|
+
const match = quickMatch(listings, filter);
|
|
102
|
+
if (match === null)
|
|
103
|
+
onNoMatch?.();
|
|
104
|
+
else
|
|
105
|
+
onJoin(match);
|
|
106
|
+
}, children: children ?? "Quick match" }));
|
|
107
|
+
}
|
|
108
|
+
export function EmoteWheel({ emotes, radius, open = true, className, emoteClassName, onPlayed, onRejected, renderEmote, }) {
|
|
109
|
+
const ctx = useGameContext();
|
|
110
|
+
if (!open)
|
|
111
|
+
return null;
|
|
112
|
+
return (_jsx("div", { className: className, role: "menu", "data-emote-wheel": true, "data-emote-count": emotes.length, children: emotes.map((emoteId, index) => (_jsx("button", { type: "button", role: "menuitem", className: emoteClassName, "data-emote": emoteId, "data-emote-index": index, onClick: () => {
|
|
113
|
+
const result = ctx.game.social.emotes.play(ctx.player.userId, emoteId, radius);
|
|
114
|
+
if ("reason" in result)
|
|
115
|
+
onRejected?.(result.reason);
|
|
116
|
+
else
|
|
117
|
+
onPlayed?.(emoteId);
|
|
118
|
+
}, children: renderEmote !== undefined ? renderEmote(emoteId) : emoteId }, emoteId))) }));
|
|
119
|
+
}
|
package/dist/voice.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type PushToTalkMode, type PushToTalkStatus, type VoiceParticipant, type VoiceRoute, type VoiceTransport } from "@jgengine/core/multiplayer/voiceContract";
|
|
3
|
+
export interface UseVoiceOptions {
|
|
4
|
+
transport?: VoiceTransport;
|
|
5
|
+
channelId?: string;
|
|
6
|
+
mode?: PushToTalkMode;
|
|
7
|
+
resolveRoutes?: () => readonly VoiceRoute[];
|
|
8
|
+
getUserMedia?: (constraints: MediaStreamConstraints) => Promise<MediaStream>;
|
|
9
|
+
}
|
|
10
|
+
export interface VoiceState {
|
|
11
|
+
supported: boolean;
|
|
12
|
+
micStream: MediaStream | null;
|
|
13
|
+
micError: string | null;
|
|
14
|
+
requestMic(): Promise<boolean>;
|
|
15
|
+
transmitting: boolean;
|
|
16
|
+
status: PushToTalkStatus;
|
|
17
|
+
mode: PushToTalkMode;
|
|
18
|
+
setMode(mode: PushToTalkMode): void;
|
|
19
|
+
muted: boolean;
|
|
20
|
+
setMuted(muted: boolean): void;
|
|
21
|
+
keyDown(): void;
|
|
22
|
+
keyUp(): void;
|
|
23
|
+
participants: readonly VoiceParticipant[];
|
|
24
|
+
routes: readonly VoiceRoute[];
|
|
25
|
+
gainFor(userId: string): number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Mic capture + push-to-talk + channel roster over the VoiceTransport
|
|
29
|
+
* signaling seam. Transmission gates the captured tracks' `enabled` flag; the
|
|
30
|
+
* media plane that actually moves audio bytes (WebRTC/SFU) stays behind the
|
|
31
|
+
* transport, host-supplied. Call once per voice channel and hand the returned
|
|
32
|
+
* state to the voice components.
|
|
33
|
+
*/
|
|
34
|
+
export declare function useVoice(options?: UseVoiceOptions): VoiceState;
|
|
35
|
+
export declare function PushToTalkButton({ voice, className, children, }: {
|
|
36
|
+
voice: VoiceState;
|
|
37
|
+
className?: string;
|
|
38
|
+
children?: ReactNode;
|
|
39
|
+
}): import("react").JSX.Element;
|
|
40
|
+
export declare function MicToggle({ voice, className, mutedLabel, unmutedLabel, }: {
|
|
41
|
+
voice: VoiceState;
|
|
42
|
+
className?: string;
|
|
43
|
+
mutedLabel?: ReactNode;
|
|
44
|
+
unmutedLabel?: ReactNode;
|
|
45
|
+
}): import("react").JSX.Element;
|
|
46
|
+
export declare function SpeakingIndicator({ voice, userId, className, threshold, children, }: {
|
|
47
|
+
voice: VoiceState;
|
|
48
|
+
userId: string;
|
|
49
|
+
className?: string;
|
|
50
|
+
threshold?: number;
|
|
51
|
+
children?: ReactNode;
|
|
52
|
+
}): import("react").JSX.Element;
|
|
53
|
+
export declare function VoiceRoster({ voice, className, participantClassName, renderParticipant, }: {
|
|
54
|
+
voice: VoiceState;
|
|
55
|
+
className?: string;
|
|
56
|
+
participantClassName?: string;
|
|
57
|
+
renderParticipant?: (participant: VoiceParticipant, gain: number) => ReactNode;
|
|
58
|
+
}): import("react").JSX.Element;
|
package/dist/voice.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { createPushToTalk, } from "@jgengine/core/multiplayer/voiceContract";
|
|
4
|
+
/**
|
|
5
|
+
* Mic capture + push-to-talk + channel roster over the VoiceTransport
|
|
6
|
+
* signaling seam. Transmission gates the captured tracks' `enabled` flag; the
|
|
7
|
+
* media plane that actually moves audio bytes (WebRTC/SFU) stays behind the
|
|
8
|
+
* transport, host-supplied. Call once per voice channel and hand the returned
|
|
9
|
+
* state to the voice components.
|
|
10
|
+
*/
|
|
11
|
+
export function useVoice(options) {
|
|
12
|
+
const transport = options?.transport;
|
|
13
|
+
const channelId = options?.channelId ?? "voice";
|
|
14
|
+
const resolveRoutes = options?.resolveRoutes;
|
|
15
|
+
const requestedMode = options?.mode ?? "hold";
|
|
16
|
+
const [transmitting, setTransmitting] = useState(false);
|
|
17
|
+
const [version, setVersion] = useState(0);
|
|
18
|
+
const bump = useCallback(() => setVersion((current) => current + 1), []);
|
|
19
|
+
void version;
|
|
20
|
+
const pttRef = useRef(null);
|
|
21
|
+
if (pttRef.current === null) {
|
|
22
|
+
pttRef.current = createPushToTalk({ mode: requestedMode, onChange: setTransmitting });
|
|
23
|
+
}
|
|
24
|
+
const ptt = pttRef.current;
|
|
25
|
+
const [micStream, setMicStream] = useState(null);
|
|
26
|
+
const [micError, setMicError] = useState(null);
|
|
27
|
+
const micRef = useRef(null);
|
|
28
|
+
const getUserMedia = options?.getUserMedia ??
|
|
29
|
+
(typeof navigator !== "undefined" && navigator.mediaDevices !== undefined
|
|
30
|
+
? (constraints) => navigator.mediaDevices.getUserMedia(constraints)
|
|
31
|
+
: undefined);
|
|
32
|
+
const supported = getUserMedia !== undefined;
|
|
33
|
+
const requestMic = useCallback(async () => {
|
|
34
|
+
if (getUserMedia === undefined) {
|
|
35
|
+
setMicError("microphone capture not supported");
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const stream = await getUserMedia({ audio: true });
|
|
40
|
+
micRef.current = stream;
|
|
41
|
+
setMicStream(stream);
|
|
42
|
+
setMicError(null);
|
|
43
|
+
void transport?.publish(channelId, stream.id);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
setMicError(error instanceof Error ? error.message : "microphone permission denied");
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}, [getUserMedia, transport, channelId]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const stream = micRef.current;
|
|
53
|
+
if (stream === null)
|
|
54
|
+
return;
|
|
55
|
+
for (const track of stream.getAudioTracks())
|
|
56
|
+
track.enabled = transmitting;
|
|
57
|
+
}, [transmitting, micStream]);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
return () => {
|
|
60
|
+
const stream = micRef.current;
|
|
61
|
+
if (stream !== null)
|
|
62
|
+
for (const track of stream.getTracks())
|
|
63
|
+
track.stop();
|
|
64
|
+
};
|
|
65
|
+
}, []);
|
|
66
|
+
const [participants, setParticipants] = useState([]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (transport === undefined)
|
|
69
|
+
return undefined;
|
|
70
|
+
void transport.join(channelId, micRef.current?.id);
|
|
71
|
+
const unsubscribe = transport.subscribers(channelId, setParticipants);
|
|
72
|
+
return () => {
|
|
73
|
+
unsubscribe();
|
|
74
|
+
void transport.leave(channelId);
|
|
75
|
+
};
|
|
76
|
+
}, [transport, channelId]);
|
|
77
|
+
const routes = resolveRoutes?.() ?? [];
|
|
78
|
+
return useMemo(() => ({
|
|
79
|
+
supported,
|
|
80
|
+
micStream,
|
|
81
|
+
micError,
|
|
82
|
+
requestMic,
|
|
83
|
+
transmitting,
|
|
84
|
+
status: ptt.status(),
|
|
85
|
+
mode: ptt.mode(),
|
|
86
|
+
setMode(mode) {
|
|
87
|
+
ptt.setMode(mode);
|
|
88
|
+
bump();
|
|
89
|
+
},
|
|
90
|
+
muted: ptt.muted(),
|
|
91
|
+
setMuted(muted) {
|
|
92
|
+
ptt.setMuted(muted);
|
|
93
|
+
bump();
|
|
94
|
+
},
|
|
95
|
+
keyDown() {
|
|
96
|
+
ptt.keyDown();
|
|
97
|
+
bump();
|
|
98
|
+
},
|
|
99
|
+
keyUp() {
|
|
100
|
+
ptt.keyUp();
|
|
101
|
+
bump();
|
|
102
|
+
},
|
|
103
|
+
participants,
|
|
104
|
+
routes,
|
|
105
|
+
gainFor(userId) {
|
|
106
|
+
let gain = 0;
|
|
107
|
+
for (const route of routes) {
|
|
108
|
+
if (route.fromUserId === userId && route.gain > gain)
|
|
109
|
+
gain = route.gain;
|
|
110
|
+
}
|
|
111
|
+
return gain;
|
|
112
|
+
},
|
|
113
|
+
}), [supported, micStream, micError, requestMic, transmitting, participants, routes, ptt, bump]);
|
|
114
|
+
}
|
|
115
|
+
export function PushToTalkButton({ voice, className, children, }) {
|
|
116
|
+
return (_jsx("button", { type: "button", className: className, "data-push-to-talk": true, "data-transmitting": voice.transmitting, "data-status": voice.status, onPointerDown: () => voice.keyDown(), onPointerUp: () => voice.keyUp(), onPointerLeave: () => voice.keyUp(), children: children ?? (voice.transmitting ? "Transmitting" : "Push to talk") }));
|
|
117
|
+
}
|
|
118
|
+
export function MicToggle({ voice, className, mutedLabel, unmutedLabel, }) {
|
|
119
|
+
return (_jsx("button", { type: "button", className: className, "data-mic-toggle": true, "data-muted": voice.muted, "aria-pressed": voice.muted, onClick: () => voice.setMuted(!voice.muted), children: voice.muted ? (mutedLabel ?? "Unmute") : (unmutedLabel ?? "Mute") }));
|
|
120
|
+
}
|
|
121
|
+
export function SpeakingIndicator({ voice, userId, className, threshold = 0.01, children, }) {
|
|
122
|
+
const gain = voice.gainFor(userId);
|
|
123
|
+
return (_jsx("span", { className: className, "data-speaking": gain > threshold, "data-gain": gain.toFixed(3), "data-user": userId, children: children }));
|
|
124
|
+
}
|
|
125
|
+
export function VoiceRoster({ voice, className, participantClassName, renderParticipant, }) {
|
|
126
|
+
return (_jsx("div", { className: className, "data-voice-roster": true, children: voice.participants.map((participant) => {
|
|
127
|
+
const gain = voice.gainFor(participant.userId);
|
|
128
|
+
return (_jsx("div", { className: participantClassName, "data-voice-participant": participant.userId, "data-published": participant.streamId !== undefined, "data-speaking": gain > 0.01, children: renderParticipant !== undefined ? renderParticipant(participant, gain) : participant.userId }, participant.userId));
|
|
129
|
+
}) }));
|
|
130
|
+
}
|