vg-x07df 1.10.6 → 1.11.1
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 +37 -0
- package/dist/channel/index.cjs +285 -283
- package/dist/channel/index.cjs.map +1 -1
- package/dist/channel/index.mjs +285 -283
- package/dist/channel/index.mjs.map +1 -1
- package/dist/index.cjs +579 -689
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +579 -689
- package/dist/index.mjs.map +1 -1
- package/dist/utils/index.cjs +962 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.cts +43 -1
- package/dist/utils/index.d.ts +43 -1
- package/dist/utils/index.mjs +952 -1
- package/dist/utils/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/channel/index.cjs
CHANGED
|
@@ -110,28 +110,6 @@ function createLogger(namespace, options) {
|
|
|
110
110
|
}
|
|
111
111
|
return new CallpadLoggerImpl(`callpad:${namespace}`, options);
|
|
112
112
|
}
|
|
113
|
-
var DataChannelContext = react.createContext(
|
|
114
|
-
null
|
|
115
|
-
);
|
|
116
|
-
function useDataChannelContext() {
|
|
117
|
-
const context = react.useContext(DataChannelContext);
|
|
118
|
-
if (!context) {
|
|
119
|
-
throw new Error(
|
|
120
|
-
"useDataChannelContext must be used within DataChannelProvider"
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
return context;
|
|
124
|
-
}
|
|
125
|
-
function useFeatureService(featureName) {
|
|
126
|
-
const { services } = useDataChannelContext();
|
|
127
|
-
const service = services.get(featureName);
|
|
128
|
-
if (!service) {
|
|
129
|
-
throw new Error(
|
|
130
|
-
`Feature service "${featureName}" not found. Make sure it's enabled in DataChannelProvider.`
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
return service;
|
|
134
|
-
}
|
|
135
113
|
|
|
136
114
|
// src/state/types.ts
|
|
137
115
|
var defaultState = {
|
|
@@ -820,6 +798,30 @@ var ChatService = class {
|
|
|
820
798
|
return sender;
|
|
821
799
|
}
|
|
822
800
|
};
|
|
801
|
+
var DataChannelContext = react.createContext(
|
|
802
|
+
null
|
|
803
|
+
);
|
|
804
|
+
function useDataChannelContext() {
|
|
805
|
+
const context = react.useContext(DataChannelContext);
|
|
806
|
+
if (!context) {
|
|
807
|
+
throw new Error(
|
|
808
|
+
"useDataChannelContext must be used within DataChannelProvider"
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
return context;
|
|
812
|
+
}
|
|
813
|
+
function useFeatureService(featureName) {
|
|
814
|
+
const { services } = useDataChannelContext();
|
|
815
|
+
const service = services.get(featureName);
|
|
816
|
+
if (!service) {
|
|
817
|
+
throw new Error(
|
|
818
|
+
`Feature service "${featureName}" not found. Make sure it's enabled in DataChannelProvider.`
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
return service;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/channel/chat/useChat.ts
|
|
823
825
|
function useChat() {
|
|
824
826
|
const service = useFeatureService("chat");
|
|
825
827
|
const byId = useChatStore((state) => state.byId);
|
|
@@ -1171,13 +1173,255 @@ function useRaiseHand() {
|
|
|
1171
1173
|
};
|
|
1172
1174
|
}
|
|
1173
1175
|
var defaultState3 = {
|
|
1176
|
+
spotlightedUser: null,
|
|
1177
|
+
isSpotlighted: false
|
|
1178
|
+
};
|
|
1179
|
+
var useSpotlightStore = zustand.create()(
|
|
1180
|
+
immer.immer((set, get) => ({
|
|
1181
|
+
...defaultState3,
|
|
1182
|
+
spotlight: (participantId, info) => set((state) => {
|
|
1183
|
+
state.spotlightedUser = {
|
|
1184
|
+
participantId,
|
|
1185
|
+
ts: Date.now(),
|
|
1186
|
+
...info && { info }
|
|
1187
|
+
};
|
|
1188
|
+
state.isSpotlighted = true;
|
|
1189
|
+
}),
|
|
1190
|
+
unspotlight: () => set((state) => {
|
|
1191
|
+
state.spotlightedUser = null;
|
|
1192
|
+
state.isSpotlighted = false;
|
|
1193
|
+
}),
|
|
1194
|
+
getSpotlightedUser: () => get().spotlightedUser,
|
|
1195
|
+
clear: () => set(() => ({
|
|
1196
|
+
spotlightedUser: null,
|
|
1197
|
+
isSpotlighted: false
|
|
1198
|
+
}))
|
|
1199
|
+
}))
|
|
1200
|
+
);
|
|
1201
|
+
function applyIncomingSpotlight(envelope) {
|
|
1202
|
+
const { spotlight, unspotlight } = useSpotlightStore.getState();
|
|
1203
|
+
switch (envelope.payload.action) {
|
|
1204
|
+
case "spotlight": {
|
|
1205
|
+
spotlight(envelope.payload.targetId, envelope.payload.info);
|
|
1206
|
+
break;
|
|
1207
|
+
}
|
|
1208
|
+
case "unspotlight": {
|
|
1209
|
+
unspotlight();
|
|
1210
|
+
break;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// src/channel/spotlight/service.ts
|
|
1216
|
+
var logger4 = createLogger("spotlight");
|
|
1217
|
+
var SpotlightService = class {
|
|
1218
|
+
constructor(room) {
|
|
1219
|
+
this.isSubscribed = false;
|
|
1220
|
+
this.room = room;
|
|
1221
|
+
}
|
|
1222
|
+
isRoomReady() {
|
|
1223
|
+
if (!this.room) {
|
|
1224
|
+
logger4.warn("Room not initialized");
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
if (this.room.state !== livekitClient.ConnectionState.Connected) {
|
|
1228
|
+
logger4.warn("Room not connected", { state: this.room.state });
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
if (!this.room.localParticipant) {
|
|
1232
|
+
logger4.warn("Local participant not available");
|
|
1233
|
+
return false;
|
|
1234
|
+
}
|
|
1235
|
+
return true;
|
|
1236
|
+
}
|
|
1237
|
+
subscribe() {
|
|
1238
|
+
if (this.isSubscribed) return;
|
|
1239
|
+
this.room.registerTextStreamHandler("spotlight:v1", async (reader) => {
|
|
1240
|
+
try {
|
|
1241
|
+
const text = await reader.readAll();
|
|
1242
|
+
this.handleIncoming(text);
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
logger4.error("Error reading spotlight stream", err);
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
this.isSubscribed = true;
|
|
1248
|
+
logger4.info("SpotlightService subscribed");
|
|
1249
|
+
}
|
|
1250
|
+
unsubscribe() {
|
|
1251
|
+
this.isSubscribed = false;
|
|
1252
|
+
logger4.info("SpotlightService unsubscribed");
|
|
1253
|
+
}
|
|
1254
|
+
getLocalParticipantId() {
|
|
1255
|
+
return this.room.localParticipant.identity;
|
|
1256
|
+
}
|
|
1257
|
+
async spotlight(targetId, info) {
|
|
1258
|
+
if (!this.isRoomReady()) {
|
|
1259
|
+
useRtcStore.getState().addError({
|
|
1260
|
+
code: "SPOTLIGHT_ROOM_NOT_READY",
|
|
1261
|
+
message: "Cannot spotlight: room not connected",
|
|
1262
|
+
timestamp: Date.now()
|
|
1263
|
+
});
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
const senderInfo = this.getSenderInfo();
|
|
1267
|
+
const previousSpotlighted = useSpotlightStore.getState().getSpotlightedUser();
|
|
1268
|
+
useSpotlightStore.getState().spotlight(targetId, info);
|
|
1269
|
+
try {
|
|
1270
|
+
const envelope = {
|
|
1271
|
+
v: 1,
|
|
1272
|
+
kind: "spotlight",
|
|
1273
|
+
roomId: this.room.name,
|
|
1274
|
+
ts: Date.now(),
|
|
1275
|
+
sender: senderInfo,
|
|
1276
|
+
payload: {
|
|
1277
|
+
action: "spotlight",
|
|
1278
|
+
targetId,
|
|
1279
|
+
info
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
await this.room.localParticipant.sendText(JSON.stringify(envelope), {
|
|
1283
|
+
topic: "spotlight:v1"
|
|
1284
|
+
});
|
|
1285
|
+
logger4.info("User spotlighted", { targetId });
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
logger4.error("Failed to spotlight user", error);
|
|
1288
|
+
if (previousSpotlighted) {
|
|
1289
|
+
useSpotlightStore.getState().spotlight(
|
|
1290
|
+
previousSpotlighted.participantId,
|
|
1291
|
+
previousSpotlighted.info
|
|
1292
|
+
);
|
|
1293
|
+
} else {
|
|
1294
|
+
useSpotlightStore.getState().unspotlight();
|
|
1295
|
+
}
|
|
1296
|
+
useRtcStore.getState().addError({
|
|
1297
|
+
code: "SPOTLIGHT_SEND_FAILED",
|
|
1298
|
+
message: error instanceof Error ? error.message : "Failed to spotlight user",
|
|
1299
|
+
timestamp: Date.now()
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
async unspotlight() {
|
|
1304
|
+
if (!this.isRoomReady()) {
|
|
1305
|
+
useRtcStore.getState().addError({
|
|
1306
|
+
code: "UNSPOTLIGHT_ROOM_NOT_READY",
|
|
1307
|
+
message: "Cannot unspotlight: room not connected",
|
|
1308
|
+
timestamp: Date.now()
|
|
1309
|
+
});
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
const senderInfo = this.getSenderInfo();
|
|
1313
|
+
const currentSpotlighted = useSpotlightStore.getState().getSpotlightedUser();
|
|
1314
|
+
if (!currentSpotlighted) {
|
|
1315
|
+
logger4.debug("No user is currently spotlighted");
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
const previousSpotlighted = { ...currentSpotlighted };
|
|
1319
|
+
useSpotlightStore.getState().unspotlight();
|
|
1320
|
+
try {
|
|
1321
|
+
const envelope = {
|
|
1322
|
+
v: 1,
|
|
1323
|
+
kind: "spotlight",
|
|
1324
|
+
roomId: this.room.name,
|
|
1325
|
+
ts: Date.now(),
|
|
1326
|
+
sender: senderInfo,
|
|
1327
|
+
payload: {
|
|
1328
|
+
action: "unspotlight",
|
|
1329
|
+
targetId: previousSpotlighted.participantId
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
await this.room.localParticipant.sendText(JSON.stringify(envelope), {
|
|
1333
|
+
topic: "spotlight:v1"
|
|
1334
|
+
});
|
|
1335
|
+
logger4.info("User unspotlighted");
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
logger4.error("Failed to unspotlight user", error);
|
|
1338
|
+
useSpotlightStore.getState().spotlight(previousSpotlighted.participantId, previousSpotlighted.info);
|
|
1339
|
+
useRtcStore.getState().addError({
|
|
1340
|
+
code: "UNSPOTLIGHT_SEND_FAILED",
|
|
1341
|
+
message: error instanceof Error ? error.message : "Failed to unspotlight user",
|
|
1342
|
+
timestamp: Date.now()
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
handleIncoming(text) {
|
|
1347
|
+
try {
|
|
1348
|
+
const parsed = JSON.parse(text);
|
|
1349
|
+
if (!this.isValidEnvelope(parsed)) {
|
|
1350
|
+
logger4.warn("Invalid spotlight envelope received", parsed);
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
if (parsed.sender.id === this.getLocalParticipantId()) {
|
|
1354
|
+
logger4.debug("Ignoring self-echo spotlight message");
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
applyIncomingSpotlight(parsed);
|
|
1358
|
+
logger4.debug("Spotlight message received", {
|
|
1359
|
+
action: parsed.payload.action,
|
|
1360
|
+
targetId: parsed.payload.targetId
|
|
1361
|
+
});
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
logger4.error("Error parsing incoming spotlight message", error);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
isValidEnvelope(e) {
|
|
1367
|
+
return e && e.v === 1 && e.kind === "spotlight" && typeof e.roomId === "string" && e.roomId === this.room.name && typeof e.ts === "number" && e.ts > 0 && typeof e.sender?.id === "string" && typeof e.payload?.action === "string" && ["spotlight", "unspotlight"].includes(e.payload.action) && typeof e.payload?.targetId === "string";
|
|
1368
|
+
}
|
|
1369
|
+
getSenderInfo() {
|
|
1370
|
+
const localParticipant = this.room.localParticipant;
|
|
1371
|
+
const sender = {
|
|
1372
|
+
id: localParticipant.identity
|
|
1373
|
+
};
|
|
1374
|
+
if (localParticipant.metadata) {
|
|
1375
|
+
try {
|
|
1376
|
+
sender.info = JSON.parse(
|
|
1377
|
+
localParticipant.metadata
|
|
1378
|
+
);
|
|
1379
|
+
} catch {
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return sender;
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
var logger5 = createLogger("spotlight:hook");
|
|
1386
|
+
function useSpotlight() {
|
|
1387
|
+
const service = useFeatureService("spotlight");
|
|
1388
|
+
const getSpotlightedUser = useSpotlightStore(
|
|
1389
|
+
(state) => state.getSpotlightedUser
|
|
1390
|
+
);
|
|
1391
|
+
const isSpotlighted = useSpotlightStore((state) => state.isSpotlighted);
|
|
1392
|
+
const spotlight = react.useCallback(
|
|
1393
|
+
async (targetId, info) => {
|
|
1394
|
+
if (!service) {
|
|
1395
|
+
logger5.error("Cannot spotlight: service not ready");
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
return service.spotlight(targetId, info);
|
|
1399
|
+
},
|
|
1400
|
+
[service]
|
|
1401
|
+
);
|
|
1402
|
+
const unspotlight = react.useCallback(async () => {
|
|
1403
|
+
if (!service) {
|
|
1404
|
+
logger5.error("Cannot unspotlight: service not ready");
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
return service.unspotlight();
|
|
1408
|
+
}, [service]);
|
|
1409
|
+
return {
|
|
1410
|
+
spotlight,
|
|
1411
|
+
unspotlight,
|
|
1412
|
+
getSpotlightedUser,
|
|
1413
|
+
isSpotlighted,
|
|
1414
|
+
isReady: !!service
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
var defaultState4 = {
|
|
1174
1418
|
reactions: /* @__PURE__ */ new Map(),
|
|
1175
1419
|
participantCache: {},
|
|
1176
1420
|
ttlMs: 4e3
|
|
1177
1421
|
};
|
|
1178
1422
|
var useReactionsStore = zustand.create()(
|
|
1179
1423
|
immer.immer((set) => ({
|
|
1180
|
-
...
|
|
1424
|
+
...defaultState4,
|
|
1181
1425
|
setReaction: (participantId, reaction) => set((state) => {
|
|
1182
1426
|
state.reactions.set(participantId, reaction);
|
|
1183
1427
|
}),
|
|
@@ -1198,7 +1442,7 @@ var useReactionsStore = zustand.create()(
|
|
|
1198
1442
|
clear: () => set(() => ({
|
|
1199
1443
|
reactions: /* @__PURE__ */ new Map(),
|
|
1200
1444
|
participantCache: {},
|
|
1201
|
-
ttlMs:
|
|
1445
|
+
ttlMs: defaultState4.ttlMs
|
|
1202
1446
|
}))
|
|
1203
1447
|
}))
|
|
1204
1448
|
);
|
|
@@ -1230,7 +1474,7 @@ function generateNonce() {
|
|
|
1230
1474
|
}
|
|
1231
1475
|
|
|
1232
1476
|
// src/channel/reactions/service.ts
|
|
1233
|
-
var
|
|
1477
|
+
var logger7 = createLogger("reactions");
|
|
1234
1478
|
var ReactionsService = class {
|
|
1235
1479
|
constructor(room) {
|
|
1236
1480
|
this.isSubscribed = false;
|
|
@@ -1241,15 +1485,15 @@ var ReactionsService = class {
|
|
|
1241
1485
|
}
|
|
1242
1486
|
isRoomReady() {
|
|
1243
1487
|
if (!this.room) {
|
|
1244
|
-
|
|
1488
|
+
logger7.warn("Room not initialized");
|
|
1245
1489
|
return false;
|
|
1246
1490
|
}
|
|
1247
1491
|
if (this.room.state !== livekitClient.ConnectionState.Connected) {
|
|
1248
|
-
|
|
1492
|
+
logger7.warn("Room not connected", { state: this.room.state });
|
|
1249
1493
|
return false;
|
|
1250
1494
|
}
|
|
1251
1495
|
if (!this.room.localParticipant) {
|
|
1252
|
-
|
|
1496
|
+
logger7.warn("Local participant not available");
|
|
1253
1497
|
return false;
|
|
1254
1498
|
}
|
|
1255
1499
|
return true;
|
|
@@ -1267,14 +1511,14 @@ var ReactionsService = class {
|
|
|
1267
1511
|
const text = await reader.readAll();
|
|
1268
1512
|
this.handleIncoming(text);
|
|
1269
1513
|
} catch (err) {
|
|
1270
|
-
|
|
1514
|
+
logger7.error("Error reading reactions stream", err);
|
|
1271
1515
|
}
|
|
1272
1516
|
});
|
|
1273
1517
|
this.pruneInterval = setInterval(() => {
|
|
1274
1518
|
useReactionsStore.getState().pruneExpired();
|
|
1275
1519
|
}, 1e3);
|
|
1276
1520
|
this.isSubscribed = true;
|
|
1277
|
-
|
|
1521
|
+
logger7.info("ReactionsService subscribed");
|
|
1278
1522
|
}
|
|
1279
1523
|
unsubscribe() {
|
|
1280
1524
|
this.isSubscribed = false;
|
|
@@ -1282,7 +1526,7 @@ var ReactionsService = class {
|
|
|
1282
1526
|
clearInterval(this.pruneInterval);
|
|
1283
1527
|
this.pruneInterval = void 0;
|
|
1284
1528
|
}
|
|
1285
|
-
|
|
1529
|
+
logger7.info("ReactionsService unsubscribed");
|
|
1286
1530
|
}
|
|
1287
1531
|
getLocalParticipantId() {
|
|
1288
1532
|
return this.room.localParticipant.identity;
|
|
@@ -1306,7 +1550,7 @@ var ReactionsService = class {
|
|
|
1306
1550
|
return;
|
|
1307
1551
|
}
|
|
1308
1552
|
if (!this.canSendNow()) {
|
|
1309
|
-
|
|
1553
|
+
logger7.debug("Rate limited, skipping send");
|
|
1310
1554
|
return;
|
|
1311
1555
|
}
|
|
1312
1556
|
const senderInfo = this.getSenderInfo();
|
|
@@ -1332,9 +1576,9 @@ var ReactionsService = class {
|
|
|
1332
1576
|
await this.room.localParticipant.sendText(JSON.stringify(envelope), {
|
|
1333
1577
|
topic: "reactions:v1"
|
|
1334
1578
|
});
|
|
1335
|
-
|
|
1579
|
+
logger7.debug("Reaction sent", { emoji });
|
|
1336
1580
|
} catch (error) {
|
|
1337
|
-
|
|
1581
|
+
logger7.error("Failed to send reaction", error);
|
|
1338
1582
|
useRtcStore.getState().addError({
|
|
1339
1583
|
code: "REACTIONS_SEND_FAILED",
|
|
1340
1584
|
message: error instanceof Error ? error.message : "Failed to send reaction",
|
|
@@ -1347,16 +1591,16 @@ var ReactionsService = class {
|
|
|
1347
1591
|
try {
|
|
1348
1592
|
const parsed = JSON.parse(text);
|
|
1349
1593
|
if (!this.isValidEnvelope(parsed)) {
|
|
1350
|
-
|
|
1594
|
+
logger7.warn("Invalid reaction envelope received", parsed);
|
|
1351
1595
|
return;
|
|
1352
1596
|
}
|
|
1353
1597
|
if (parsed.sender.id === this.getLocalParticipantId()) {
|
|
1354
|
-
|
|
1598
|
+
logger7.debug("Ignoring self-echo reaction");
|
|
1355
1599
|
return;
|
|
1356
1600
|
}
|
|
1357
1601
|
const lastTs = this.lastRemoteTs.get(parsed.sender.id) ?? Number.NEGATIVE_INFINITY;
|
|
1358
1602
|
if (parsed.ts < lastTs) {
|
|
1359
|
-
|
|
1603
|
+
logger7.debug("Ignoring out-of-order reaction", {
|
|
1360
1604
|
sender: parsed.sender.id,
|
|
1361
1605
|
ts: parsed.ts,
|
|
1362
1606
|
lastTs
|
|
@@ -1365,9 +1609,9 @@ var ReactionsService = class {
|
|
|
1365
1609
|
}
|
|
1366
1610
|
this.lastRemoteTs.set(parsed.sender.id, parsed.ts);
|
|
1367
1611
|
applyIncomingReaction(parsed);
|
|
1368
|
-
|
|
1612
|
+
logger7.debug("Reaction received", { emoji: parsed.payload.emoji });
|
|
1369
1613
|
} catch (error) {
|
|
1370
|
-
|
|
1614
|
+
logger7.error("Error parsing incoming reaction", error);
|
|
1371
1615
|
}
|
|
1372
1616
|
}
|
|
1373
1617
|
isValidEnvelope(e) {
|
|
@@ -1389,7 +1633,7 @@ var ReactionsService = class {
|
|
|
1389
1633
|
return sender;
|
|
1390
1634
|
}
|
|
1391
1635
|
};
|
|
1392
|
-
var
|
|
1636
|
+
var logger8 = createLogger("reactions:hook");
|
|
1393
1637
|
function useReactions() {
|
|
1394
1638
|
const service = useFeatureService("reactions");
|
|
1395
1639
|
const reactions = useReactionsStore((state) => state.reactions);
|
|
@@ -1408,7 +1652,7 @@ function useReactions() {
|
|
|
1408
1652
|
const sendReaction = react.useCallback(
|
|
1409
1653
|
async (emoji) => {
|
|
1410
1654
|
if (!service) {
|
|
1411
|
-
|
|
1655
|
+
logger8.error("Cannot send reaction: service not ready");
|
|
1412
1656
|
return;
|
|
1413
1657
|
}
|
|
1414
1658
|
return service.sendReaction(emoji);
|
|
@@ -1426,248 +1670,6 @@ function useReactions() {
|
|
|
1426
1670
|
isReady: !!service
|
|
1427
1671
|
};
|
|
1428
1672
|
}
|
|
1429
|
-
var defaultState4 = {
|
|
1430
|
-
spotlightedUser: null,
|
|
1431
|
-
isSpotlighted: false
|
|
1432
|
-
};
|
|
1433
|
-
var useSpotlightStore = zustand.create()(
|
|
1434
|
-
immer.immer((set, get) => ({
|
|
1435
|
-
...defaultState4,
|
|
1436
|
-
spotlight: (participantId, info) => set((state) => {
|
|
1437
|
-
state.spotlightedUser = {
|
|
1438
|
-
participantId,
|
|
1439
|
-
ts: Date.now(),
|
|
1440
|
-
...info && { info }
|
|
1441
|
-
};
|
|
1442
|
-
state.isSpotlighted = true;
|
|
1443
|
-
}),
|
|
1444
|
-
unspotlight: () => set((state) => {
|
|
1445
|
-
state.spotlightedUser = null;
|
|
1446
|
-
state.isSpotlighted = false;
|
|
1447
|
-
}),
|
|
1448
|
-
getSpotlightedUser: () => get().spotlightedUser,
|
|
1449
|
-
clear: () => set(() => ({
|
|
1450
|
-
spotlightedUser: null,
|
|
1451
|
-
isSpotlighted: false
|
|
1452
|
-
}))
|
|
1453
|
-
}))
|
|
1454
|
-
);
|
|
1455
|
-
function applyIncomingSpotlight(envelope) {
|
|
1456
|
-
const { spotlight, unspotlight } = useSpotlightStore.getState();
|
|
1457
|
-
switch (envelope.payload.action) {
|
|
1458
|
-
case "spotlight": {
|
|
1459
|
-
spotlight(envelope.payload.targetId, envelope.payload.info);
|
|
1460
|
-
break;
|
|
1461
|
-
}
|
|
1462
|
-
case "unspotlight": {
|
|
1463
|
-
unspotlight();
|
|
1464
|
-
break;
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
// src/channel/spotlight/service.ts
|
|
1470
|
-
var logger7 = createLogger("spotlight");
|
|
1471
|
-
var SpotlightService = class {
|
|
1472
|
-
constructor(room) {
|
|
1473
|
-
this.isSubscribed = false;
|
|
1474
|
-
this.room = room;
|
|
1475
|
-
}
|
|
1476
|
-
isRoomReady() {
|
|
1477
|
-
if (!this.room) {
|
|
1478
|
-
logger7.warn("Room not initialized");
|
|
1479
|
-
return false;
|
|
1480
|
-
}
|
|
1481
|
-
if (this.room.state !== livekitClient.ConnectionState.Connected) {
|
|
1482
|
-
logger7.warn("Room not connected", { state: this.room.state });
|
|
1483
|
-
return false;
|
|
1484
|
-
}
|
|
1485
|
-
if (!this.room.localParticipant) {
|
|
1486
|
-
logger7.warn("Local participant not available");
|
|
1487
|
-
return false;
|
|
1488
|
-
}
|
|
1489
|
-
return true;
|
|
1490
|
-
}
|
|
1491
|
-
subscribe() {
|
|
1492
|
-
if (this.isSubscribed) return;
|
|
1493
|
-
this.room.registerTextStreamHandler("spotlight:v1", async (reader) => {
|
|
1494
|
-
try {
|
|
1495
|
-
const text = await reader.readAll();
|
|
1496
|
-
this.handleIncoming(text);
|
|
1497
|
-
} catch (err) {
|
|
1498
|
-
logger7.error("Error reading spotlight stream", err);
|
|
1499
|
-
}
|
|
1500
|
-
});
|
|
1501
|
-
this.isSubscribed = true;
|
|
1502
|
-
logger7.info("SpotlightService subscribed");
|
|
1503
|
-
}
|
|
1504
|
-
unsubscribe() {
|
|
1505
|
-
this.isSubscribed = false;
|
|
1506
|
-
logger7.info("SpotlightService unsubscribed");
|
|
1507
|
-
}
|
|
1508
|
-
getLocalParticipantId() {
|
|
1509
|
-
return this.room.localParticipant.identity;
|
|
1510
|
-
}
|
|
1511
|
-
async spotlight(targetId, info) {
|
|
1512
|
-
if (!this.isRoomReady()) {
|
|
1513
|
-
useRtcStore.getState().addError({
|
|
1514
|
-
code: "SPOTLIGHT_ROOM_NOT_READY",
|
|
1515
|
-
message: "Cannot spotlight: room not connected",
|
|
1516
|
-
timestamp: Date.now()
|
|
1517
|
-
});
|
|
1518
|
-
return;
|
|
1519
|
-
}
|
|
1520
|
-
const senderInfo = this.getSenderInfo();
|
|
1521
|
-
const previousSpotlighted = useSpotlightStore.getState().getSpotlightedUser();
|
|
1522
|
-
useSpotlightStore.getState().spotlight(targetId, info);
|
|
1523
|
-
try {
|
|
1524
|
-
const envelope = {
|
|
1525
|
-
v: 1,
|
|
1526
|
-
kind: "spotlight",
|
|
1527
|
-
roomId: this.room.name,
|
|
1528
|
-
ts: Date.now(),
|
|
1529
|
-
sender: senderInfo,
|
|
1530
|
-
payload: {
|
|
1531
|
-
action: "spotlight",
|
|
1532
|
-
targetId,
|
|
1533
|
-
info
|
|
1534
|
-
}
|
|
1535
|
-
};
|
|
1536
|
-
await this.room.localParticipant.sendText(JSON.stringify(envelope), {
|
|
1537
|
-
topic: "spotlight:v1"
|
|
1538
|
-
});
|
|
1539
|
-
logger7.info("User spotlighted", { targetId });
|
|
1540
|
-
} catch (error) {
|
|
1541
|
-
logger7.error("Failed to spotlight user", error);
|
|
1542
|
-
if (previousSpotlighted) {
|
|
1543
|
-
useSpotlightStore.getState().spotlight(
|
|
1544
|
-
previousSpotlighted.participantId,
|
|
1545
|
-
previousSpotlighted.info
|
|
1546
|
-
);
|
|
1547
|
-
} else {
|
|
1548
|
-
useSpotlightStore.getState().unspotlight();
|
|
1549
|
-
}
|
|
1550
|
-
useRtcStore.getState().addError({
|
|
1551
|
-
code: "SPOTLIGHT_SEND_FAILED",
|
|
1552
|
-
message: error instanceof Error ? error.message : "Failed to spotlight user",
|
|
1553
|
-
timestamp: Date.now()
|
|
1554
|
-
});
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
async unspotlight() {
|
|
1558
|
-
if (!this.isRoomReady()) {
|
|
1559
|
-
useRtcStore.getState().addError({
|
|
1560
|
-
code: "UNSPOTLIGHT_ROOM_NOT_READY",
|
|
1561
|
-
message: "Cannot unspotlight: room not connected",
|
|
1562
|
-
timestamp: Date.now()
|
|
1563
|
-
});
|
|
1564
|
-
return;
|
|
1565
|
-
}
|
|
1566
|
-
const senderInfo = this.getSenderInfo();
|
|
1567
|
-
const currentSpotlighted = useSpotlightStore.getState().getSpotlightedUser();
|
|
1568
|
-
if (!currentSpotlighted) {
|
|
1569
|
-
logger7.debug("No user is currently spotlighted");
|
|
1570
|
-
return;
|
|
1571
|
-
}
|
|
1572
|
-
const previousSpotlighted = { ...currentSpotlighted };
|
|
1573
|
-
useSpotlightStore.getState().unspotlight();
|
|
1574
|
-
try {
|
|
1575
|
-
const envelope = {
|
|
1576
|
-
v: 1,
|
|
1577
|
-
kind: "spotlight",
|
|
1578
|
-
roomId: this.room.name,
|
|
1579
|
-
ts: Date.now(),
|
|
1580
|
-
sender: senderInfo,
|
|
1581
|
-
payload: {
|
|
1582
|
-
action: "unspotlight",
|
|
1583
|
-
targetId: previousSpotlighted.participantId
|
|
1584
|
-
}
|
|
1585
|
-
};
|
|
1586
|
-
await this.room.localParticipant.sendText(JSON.stringify(envelope), {
|
|
1587
|
-
topic: "spotlight:v1"
|
|
1588
|
-
});
|
|
1589
|
-
logger7.info("User unspotlighted");
|
|
1590
|
-
} catch (error) {
|
|
1591
|
-
logger7.error("Failed to unspotlight user", error);
|
|
1592
|
-
useSpotlightStore.getState().spotlight(previousSpotlighted.participantId, previousSpotlighted.info);
|
|
1593
|
-
useRtcStore.getState().addError({
|
|
1594
|
-
code: "UNSPOTLIGHT_SEND_FAILED",
|
|
1595
|
-
message: error instanceof Error ? error.message : "Failed to unspotlight user",
|
|
1596
|
-
timestamp: Date.now()
|
|
1597
|
-
});
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
handleIncoming(text) {
|
|
1601
|
-
try {
|
|
1602
|
-
const parsed = JSON.parse(text);
|
|
1603
|
-
if (!this.isValidEnvelope(parsed)) {
|
|
1604
|
-
logger7.warn("Invalid spotlight envelope received", parsed);
|
|
1605
|
-
return;
|
|
1606
|
-
}
|
|
1607
|
-
if (parsed.sender.id === this.getLocalParticipantId()) {
|
|
1608
|
-
logger7.debug("Ignoring self-echo spotlight message");
|
|
1609
|
-
return;
|
|
1610
|
-
}
|
|
1611
|
-
applyIncomingSpotlight(parsed);
|
|
1612
|
-
logger7.debug("Spotlight message received", {
|
|
1613
|
-
action: parsed.payload.action,
|
|
1614
|
-
targetId: parsed.payload.targetId
|
|
1615
|
-
});
|
|
1616
|
-
} catch (error) {
|
|
1617
|
-
logger7.error("Error parsing incoming spotlight message", error);
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
isValidEnvelope(e) {
|
|
1621
|
-
return e && e.v === 1 && e.kind === "spotlight" && typeof e.roomId === "string" && e.roomId === this.room.name && typeof e.ts === "number" && e.ts > 0 && typeof e.sender?.id === "string" && typeof e.payload?.action === "string" && ["spotlight", "unspotlight"].includes(e.payload.action) && typeof e.payload?.targetId === "string";
|
|
1622
|
-
}
|
|
1623
|
-
getSenderInfo() {
|
|
1624
|
-
const localParticipant = this.room.localParticipant;
|
|
1625
|
-
const sender = {
|
|
1626
|
-
id: localParticipant.identity
|
|
1627
|
-
};
|
|
1628
|
-
if (localParticipant.metadata) {
|
|
1629
|
-
try {
|
|
1630
|
-
sender.info = JSON.parse(
|
|
1631
|
-
localParticipant.metadata
|
|
1632
|
-
);
|
|
1633
|
-
} catch {
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
return sender;
|
|
1637
|
-
}
|
|
1638
|
-
};
|
|
1639
|
-
var logger8 = createLogger("spotlight:hook");
|
|
1640
|
-
function useSpotlight() {
|
|
1641
|
-
const service = useFeatureService("spotlight");
|
|
1642
|
-
const getSpotlightedUser = useSpotlightStore(
|
|
1643
|
-
(state) => state.getSpotlightedUser
|
|
1644
|
-
);
|
|
1645
|
-
const isSpotlighted = useSpotlightStore((state) => state.isSpotlighted);
|
|
1646
|
-
const spotlight = react.useCallback(
|
|
1647
|
-
async (targetId, info) => {
|
|
1648
|
-
if (!service) {
|
|
1649
|
-
logger8.error("Cannot spotlight: service not ready");
|
|
1650
|
-
return;
|
|
1651
|
-
}
|
|
1652
|
-
return service.spotlight(targetId, info);
|
|
1653
|
-
},
|
|
1654
|
-
[service]
|
|
1655
|
-
);
|
|
1656
|
-
const unspotlight = react.useCallback(async () => {
|
|
1657
|
-
if (!service) {
|
|
1658
|
-
logger8.error("Cannot unspotlight: service not ready");
|
|
1659
|
-
return;
|
|
1660
|
-
}
|
|
1661
|
-
return service.unspotlight();
|
|
1662
|
-
}, [service]);
|
|
1663
|
-
return {
|
|
1664
|
-
spotlight,
|
|
1665
|
-
unspotlight,
|
|
1666
|
-
getSpotlightedUser,
|
|
1667
|
-
isSpotlighted,
|
|
1668
|
-
isReady: !!service
|
|
1669
|
-
};
|
|
1670
|
-
}
|
|
1671
1673
|
|
|
1672
1674
|
// src/channel/registry.ts
|
|
1673
1675
|
var FEATURES = {
|