@ai-matrx/meet 0.7.9 → 0.7.11
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 +51 -0
- package/dist/index.cjs +1917 -1311
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +292 -1
- package/dist/index.d.ts +292 -1
- package/dist/index.js +1917 -1311
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +2360 -669
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +94 -3
- package/dist/react.d.ts +94 -3
- package/dist/react.js +2361 -670
- package/dist/react.js.map +1 -1
- package/dist/styles.css +293 -0
- package/dist/tokens.css +2 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -33,10 +33,12 @@ __export(src_exports, {
|
|
|
33
33
|
ATTENDANCE_CSV_HEADER: () => ATTENDANCE_CSV_HEADER,
|
|
34
34
|
CALL_INVITE_KIND: () => CALL_INVITE_KIND,
|
|
35
35
|
CALL_INVITE_VERSION: () => CALL_INVITE_VERSION,
|
|
36
|
+
CAPTION_LANGUAGES: () => CAPTION_LANGUAGES,
|
|
36
37
|
CONSENT_NOTICE_ATTRIBUTE: () => CONSENT_NOTICE_ATTRIBUTE,
|
|
37
38
|
DATA_TOPICS: () => DATA_TOPICS,
|
|
38
39
|
DEFAULT_EFFECTS: () => DEFAULT_EFFECTS,
|
|
39
40
|
DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
|
|
41
|
+
EMPTY_COLLAB: () => EMPTY_COLLAB,
|
|
40
42
|
END_MEETING_NOT_SERVED: () => END_MEETING_NOT_SERVED,
|
|
41
43
|
FILE_SESSION_PATH: () => FILE_SESSION_PATH,
|
|
42
44
|
GUEST_RECORD_ACCESS_CHOICES: () => GUEST_RECORD_ACCESS_CHOICES,
|
|
@@ -74,6 +76,7 @@ __export(src_exports, {
|
|
|
74
76
|
absenceSentence: () => absenceSentence,
|
|
75
77
|
activeWrapUpSummary: () => activeWrapUpSummary,
|
|
76
78
|
announceMeeting: () => announceMeeting,
|
|
79
|
+
applyCollabPacket: () => applyCollabPacket,
|
|
77
80
|
applyRoomEvent: () => applyRoomEvent,
|
|
78
81
|
asCallId: () => asCallId,
|
|
79
82
|
asMeetingId: () => asMeetingId,
|
|
@@ -81,10 +84,13 @@ __export(src_exports, {
|
|
|
81
84
|
asParticipantIdentity: () => asParticipantIdentity,
|
|
82
85
|
asRoomName: () => asRoomName,
|
|
83
86
|
asUserId: () => asUserId,
|
|
87
|
+
assignedRoom: () => assignedRoom,
|
|
84
88
|
attendanceCsv: () => attendanceCsv,
|
|
85
89
|
attendanceReport: () => attendanceReport,
|
|
86
90
|
attendanceTotals: () => attendanceTotals,
|
|
87
91
|
attendeeKey: () => attendeeKey,
|
|
92
|
+
autoAssign: () => autoAssign,
|
|
93
|
+
breakoutRemainingMs: () => breakoutRemainingMs,
|
|
88
94
|
callInviteAction: () => callInviteAction,
|
|
89
95
|
callInviteText: () => callInviteText,
|
|
90
96
|
callStateLabel: () => callStateLabel,
|
|
@@ -94,6 +100,7 @@ __export(src_exports, {
|
|
|
94
100
|
captionSegment: () => captionSegment,
|
|
95
101
|
changesPatch: () => changesPatch,
|
|
96
102
|
clockPosition: () => clockPosition,
|
|
103
|
+
collabFromState: () => collabFromState,
|
|
97
104
|
computeStage: () => computeStage,
|
|
98
105
|
countLabel: () => countLabel,
|
|
99
106
|
createCallCenter: () => createCallCenter,
|
|
@@ -114,6 +121,7 @@ __export(src_exports, {
|
|
|
114
121
|
createWebDeviceStorage: () => createWebDeviceStorage,
|
|
115
122
|
createWebEffectsStorage: () => createWebEffectsStorage,
|
|
116
123
|
createWebRoomTokenStorage: () => createWebRoomTokenStorage,
|
|
124
|
+
defaultCaptionLanguage: () => defaultCaptionLanguage,
|
|
117
125
|
displayNameFor: () => displayNameFor,
|
|
118
126
|
fileDownloadPath: () => fileDownloadPath,
|
|
119
127
|
gridColumns: () => gridColumns,
|
|
@@ -136,6 +144,7 @@ __export(src_exports, {
|
|
|
136
144
|
listenOnlyNotice: () => listenOnlyNotice,
|
|
137
145
|
loadFollowUpDraft: () => loadFollowUpDraft,
|
|
138
146
|
loadGuestMeetingRecord: () => loadGuestMeetingRecord,
|
|
147
|
+
loadMeetingActivities: () => loadMeetingActivities,
|
|
139
148
|
loadMeetingChat: () => loadMeetingChat,
|
|
140
149
|
loadMeetingRecord: () => loadMeetingRecord,
|
|
141
150
|
mapConnectionState: () => mapConnectionState,
|
|
@@ -191,6 +200,7 @@ __export(src_exports, {
|
|
|
191
200
|
uniqueAttendees: () => uniqueAttendees,
|
|
192
201
|
userTopic: () => userTopic,
|
|
193
202
|
wasRefused: () => wasRefused,
|
|
203
|
+
withItem: () => withItem,
|
|
194
204
|
wrapUpStatus: () => wrapUpStatus
|
|
195
205
|
});
|
|
196
206
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -418,7 +428,14 @@ var MEET_ROUTES = {
|
|
|
418
428
|
*/
|
|
419
429
|
moderate: "/api/v1/meet/moderate",
|
|
420
430
|
/** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */
|
|
421
|
-
chat: "/api/v1/meet/chat"
|
|
431
|
+
chat: "/api/v1/meet/chat",
|
|
432
|
+
/**
|
|
433
|
+
* Meet wave 5: polls, Q&A, the shared whiteboard and breakout rooms. `POST` acts
|
|
434
|
+
* (`{meeting_id, action, …}`), `GET ?meeting_id=` reads what this viewer may see.
|
|
435
|
+
*/
|
|
436
|
+
collab: "/api/v1/meet/collab",
|
|
437
|
+
/** Meet wave 5: translate finished caption lines into this viewer's language. */
|
|
438
|
+
captionsTranslate: "/api/v1/meet/captions/translate"
|
|
422
439
|
};
|
|
423
440
|
function meetingPath(template, meetingId) {
|
|
424
441
|
return template.replace("{meeting_id}", encodeURIComponent(meetingId));
|
|
@@ -477,7 +494,8 @@ function parseRoomToken(body, roomName) {
|
|
|
477
494
|
// server that has not landed MRI-A11 — and never invents a host from
|
|
478
495
|
// silence, because both fallbacks below grant nothing.
|
|
479
496
|
role: readRole(payload, "role") ?? (isHost ? "host" : identity.startsWith("guest:") ? "guest" : "participant"),
|
|
480
|
-
lobby: readBoolean(payload, "lobby", false)
|
|
497
|
+
lobby: readBoolean(payload, "lobby", false),
|
|
498
|
+
breakoutRoomId: readString(payload, "breakout_room_id") ?? null
|
|
481
499
|
};
|
|
482
500
|
}
|
|
483
501
|
function createTokenClient(options) {
|
|
@@ -525,6 +543,7 @@ function createTokenClient(options) {
|
|
|
525
543
|
room_name: input.roomName,
|
|
526
544
|
...input.meetingId !== void 0 ? { meeting_id: input.meetingId } : {},
|
|
527
545
|
...input.slug !== void 0 ? { slug: input.slug } : {},
|
|
546
|
+
...input.breakoutRoomId != null ? { breakout_room_id: input.breakoutRoomId } : {},
|
|
528
547
|
organization_id: input.identity.organizationId,
|
|
529
548
|
display_name: input.identity.displayName,
|
|
530
549
|
device_id: input.identity.deviceId,
|
|
@@ -588,7 +607,8 @@ function createTokenClient(options) {
|
|
|
588
607
|
return {
|
|
589
608
|
get(input) {
|
|
590
609
|
const entry = cache.get(input.roomName);
|
|
591
|
-
|
|
610
|
+
const sameRoom = entry !== void 0 && (entry.token.breakoutRoomId ?? null) === (input.breakoutRoomId ?? null);
|
|
611
|
+
if (entry !== void 0 && sameRoom) {
|
|
592
612
|
if (entry.inFlight !== null) return entry.inFlight;
|
|
593
613
|
if (!expired(entry)) return Promise.resolve(entry.token);
|
|
594
614
|
}
|
|
@@ -634,7 +654,8 @@ function placeholder(roomName) {
|
|
|
634
654
|
canRecord: false,
|
|
635
655
|
isHost: false,
|
|
636
656
|
role: "participant",
|
|
637
|
-
lobby: false
|
|
657
|
+
lobby: false,
|
|
658
|
+
breakoutRoomId: null
|
|
638
659
|
};
|
|
639
660
|
}
|
|
640
661
|
async function serverSentence(response) {
|
|
@@ -1363,6 +1384,339 @@ function createCallCenter(options) {
|
|
|
1363
1384
|
};
|
|
1364
1385
|
}
|
|
1365
1386
|
|
|
1387
|
+
// src/core/collab.ts
|
|
1388
|
+
var EMPTY_COLLAB = {
|
|
1389
|
+
loaded: false,
|
|
1390
|
+
error: null,
|
|
1391
|
+
settings: null,
|
|
1392
|
+
polls: [],
|
|
1393
|
+
questions: [],
|
|
1394
|
+
strokes: [],
|
|
1395
|
+
breakout: null,
|
|
1396
|
+
broadcast: null,
|
|
1397
|
+
moveRequest: null,
|
|
1398
|
+
panelRequest: null,
|
|
1399
|
+
captionLanguage: null,
|
|
1400
|
+
captionTranslations: {},
|
|
1401
|
+
captionTranslationError: null
|
|
1402
|
+
};
|
|
1403
|
+
function obj(value) {
|
|
1404
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
1405
|
+
}
|
|
1406
|
+
function str(value) {
|
|
1407
|
+
return typeof value === "string" ? value : null;
|
|
1408
|
+
}
|
|
1409
|
+
function num(value) {
|
|
1410
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1411
|
+
}
|
|
1412
|
+
function strings(value) {
|
|
1413
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
1414
|
+
}
|
|
1415
|
+
function pollFromRow(row) {
|
|
1416
|
+
const id = str(row["id"]);
|
|
1417
|
+
if (id === null) return null;
|
|
1418
|
+
const options = [];
|
|
1419
|
+
for (const raw of Array.isArray(row["options"]) ? row["options"] : []) {
|
|
1420
|
+
const option = obj(raw);
|
|
1421
|
+
const optionId = option === null ? null : str(option["id"]);
|
|
1422
|
+
if (option === null || optionId === null) continue;
|
|
1423
|
+
const votes = num(option["votes"]);
|
|
1424
|
+
options.push({
|
|
1425
|
+
id: optionId,
|
|
1426
|
+
text: str(option["text"]) ?? "",
|
|
1427
|
+
...votes !== null ? { votes } : {},
|
|
1428
|
+
...Array.isArray(option["voters"]) ? { voters: strings(option["voters"]) } : {}
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
const state = str(row["state"]);
|
|
1432
|
+
return {
|
|
1433
|
+
id,
|
|
1434
|
+
question: str(row["question"]) ?? "",
|
|
1435
|
+
options,
|
|
1436
|
+
multiple: row["multiple"] === true,
|
|
1437
|
+
anonymous: row["anonymous"] === true,
|
|
1438
|
+
state: state === "live" || state === "closed" ? state : "draft",
|
|
1439
|
+
resultsShared: row["results_shared"] === true,
|
|
1440
|
+
voterCount: num(row["voter_count"]),
|
|
1441
|
+
myVote: strings(row["my_vote"]),
|
|
1442
|
+
createdByName: str(row["created_by_name"]) ?? "",
|
|
1443
|
+
createdAt: str(row["created_at"])
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
function questionFromRow(row) {
|
|
1447
|
+
const id = str(row["id"]);
|
|
1448
|
+
if (id === null) return null;
|
|
1449
|
+
const state = str(row["state"]);
|
|
1450
|
+
return {
|
|
1451
|
+
id,
|
|
1452
|
+
text: str(row["text"]) ?? "",
|
|
1453
|
+
askerName: str(row["asker_name"]) ?? "",
|
|
1454
|
+
anonymous: row["anonymous"] === true,
|
|
1455
|
+
state: state === "answered" || state === "dismissed" ? state : "open",
|
|
1456
|
+
answer: str(row["answer"]),
|
|
1457
|
+
answeredByName: str(row["answered_by_name"]),
|
|
1458
|
+
upvotes: num(row["upvotes"]) ?? 0,
|
|
1459
|
+
upvotedByMe: row["upvoted_by_me"] === true,
|
|
1460
|
+
mine: row["mine"] === true,
|
|
1461
|
+
createdAt: str(row["created_at"])
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
function strokeFromRow(row) {
|
|
1465
|
+
const id = str(row["id"]);
|
|
1466
|
+
if (id === null || !Array.isArray(row["points"])) return null;
|
|
1467
|
+
const points = [];
|
|
1468
|
+
for (const raw of row["points"]) {
|
|
1469
|
+
if (Array.isArray(raw) && typeof raw[0] === "number" && typeof raw[1] === "number") {
|
|
1470
|
+
points.push([raw[0], raw[1]]);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return {
|
|
1474
|
+
id,
|
|
1475
|
+
points,
|
|
1476
|
+
color: str(row["color"]) ?? "#111827",
|
|
1477
|
+
width: num(row["width"]) ?? 3,
|
|
1478
|
+
tool: row["tool"] === "highlighter" ? "highlighter" : "pen",
|
|
1479
|
+
by: str(row["by"]) ?? "",
|
|
1480
|
+
byName: str(row["by_name"]) ?? ""
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
function broadcastFrom(value) {
|
|
1484
|
+
const row = obj(value);
|
|
1485
|
+
const text3 = row === null ? null : str(row["text"]);
|
|
1486
|
+
if (row === null || text3 === null) return null;
|
|
1487
|
+
return { text: text3, by: str(row["by"]) ?? "", at: str(row["at"]) ?? "" };
|
|
1488
|
+
}
|
|
1489
|
+
function planFromRow(row) {
|
|
1490
|
+
const id = str(row["id"]);
|
|
1491
|
+
if (id === null) return null;
|
|
1492
|
+
const rooms = [];
|
|
1493
|
+
for (const raw of Array.isArray(row["rooms"]) ? row["rooms"] : []) {
|
|
1494
|
+
const room = obj(raw);
|
|
1495
|
+
const roomId = room === null ? null : str(room["id"]);
|
|
1496
|
+
if (room === null || roomId === null) continue;
|
|
1497
|
+
rooms.push({ id: roomId, name: str(room["name"]) ?? roomId, identities: strings(room["identities"]) });
|
|
1498
|
+
}
|
|
1499
|
+
return {
|
|
1500
|
+
id,
|
|
1501
|
+
state: row["state"] === "open" ? "open" : "closed",
|
|
1502
|
+
rooms,
|
|
1503
|
+
allowChoose: row["allow_choose"] === true,
|
|
1504
|
+
minutes: num(row["minutes"]) ?? 0,
|
|
1505
|
+
openedAt: str(row["opened_at"]),
|
|
1506
|
+
endsAt: str(row["ends_at"]),
|
|
1507
|
+
closedAt: str(row["closed_at"]),
|
|
1508
|
+
broadcasts: (Array.isArray(row["broadcasts"]) ? row["broadcasts"] : []).map(broadcastFrom).filter((b) => b !== null)
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
function settingsFromRow(row) {
|
|
1512
|
+
if (row === null) return null;
|
|
1513
|
+
return {
|
|
1514
|
+
pollsEnabled: row["polls_enabled"] === true,
|
|
1515
|
+
pollMaxOptions: num(row["poll_max_options"]) ?? 10,
|
|
1516
|
+
qaEnabled: row["qa_enabled"] === true,
|
|
1517
|
+
qaAnonymousAllowed: row["qa_anonymous_allowed"] === true,
|
|
1518
|
+
whiteboardEnabled: row["whiteboard_enabled"] === true,
|
|
1519
|
+
breakoutsEnabled: row["breakouts_enabled"] === true,
|
|
1520
|
+
breakoutMaxRooms: num(row["breakout_max_rooms"]) ?? 20,
|
|
1521
|
+
breakoutDefaultMinutes: num(row["breakout_default_minutes"]) ?? 15,
|
|
1522
|
+
captionTranslationEnabled: row["caption_translation_enabled"] === true,
|
|
1523
|
+
captionTranslationLanguage: str(row["caption_translation_language"]) ?? ""
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
function collabFromState(body, previous) {
|
|
1527
|
+
const list = (key, parse) => (Array.isArray(body[key]) ? body[key] : []).map((raw) => {
|
|
1528
|
+
const row = obj(raw);
|
|
1529
|
+
return row === null ? null : parse(row);
|
|
1530
|
+
}).filter((item) => item !== null);
|
|
1531
|
+
const breakoutRow = obj(body["breakout"]);
|
|
1532
|
+
return {
|
|
1533
|
+
...previous,
|
|
1534
|
+
loaded: true,
|
|
1535
|
+
error: null,
|
|
1536
|
+
settings: settingsFromRow(obj(body["settings"])),
|
|
1537
|
+
polls: list("polls", pollFromRow),
|
|
1538
|
+
questions: list("questions", questionFromRow),
|
|
1539
|
+
strokes: list("strokes", strokeFromRow),
|
|
1540
|
+
breakout: breakoutRow === null ? null : planFromRow(breakoutRow)
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
function upsert(items, next) {
|
|
1544
|
+
const index = items.findIndex((item) => item.id === next.id);
|
|
1545
|
+
if (index === -1) return [...items, next];
|
|
1546
|
+
const copy = items.slice();
|
|
1547
|
+
copy[index] = next;
|
|
1548
|
+
return copy;
|
|
1549
|
+
}
|
|
1550
|
+
function withItem(collab, row, fromRoom = false) {
|
|
1551
|
+
const kind = str(row["kind"]);
|
|
1552
|
+
if (kind === "poll") {
|
|
1553
|
+
const poll = pollFromRow(row);
|
|
1554
|
+
if (poll === null) return collab;
|
|
1555
|
+
const known = collab.polls.find((p) => p.id === poll.id);
|
|
1556
|
+
const merged = fromRoom && known !== void 0 ? { ...poll, myVote: known.myVote } : poll;
|
|
1557
|
+
return { ...collab, polls: upsert(collab.polls, merged) };
|
|
1558
|
+
}
|
|
1559
|
+
if (kind === "question") {
|
|
1560
|
+
const question = questionFromRow(row);
|
|
1561
|
+
if (question === null) return collab;
|
|
1562
|
+
const known = collab.questions.find((q) => q.id === question.id);
|
|
1563
|
+
const merged = fromRoom && known !== void 0 ? { ...question, mine: known.mine, upvotedByMe: known.upvotedByMe } : question;
|
|
1564
|
+
return { ...collab, questions: upsert(collab.questions, merged) };
|
|
1565
|
+
}
|
|
1566
|
+
if (kind === "stroke") {
|
|
1567
|
+
const stroke = strokeFromRow(row);
|
|
1568
|
+
if (stroke === null) return collab;
|
|
1569
|
+
return { ...collab, strokes: upsert(collab.strokes, stroke) };
|
|
1570
|
+
}
|
|
1571
|
+
if (kind === "breakout") {
|
|
1572
|
+
const plan = planFromRow(row);
|
|
1573
|
+
if (plan === null) return collab;
|
|
1574
|
+
return { ...collab, breakout: plan };
|
|
1575
|
+
}
|
|
1576
|
+
return collab;
|
|
1577
|
+
}
|
|
1578
|
+
function without(collab, ids) {
|
|
1579
|
+
const gone = new Set(ids);
|
|
1580
|
+
return {
|
|
1581
|
+
...collab,
|
|
1582
|
+
polls: collab.polls.filter((p) => !gone.has(p.id)),
|
|
1583
|
+
questions: collab.questions.filter((q) => !gone.has(q.id)),
|
|
1584
|
+
strokes: collab.strokes.filter((s) => !gone.has(s.id))
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
function applyCollabPacket(collab, payload, context) {
|
|
1588
|
+
const item = obj(payload["item"]);
|
|
1589
|
+
const audience = str(payload["audience"]);
|
|
1590
|
+
if (item !== null) {
|
|
1591
|
+
const kind = str(item["kind"]);
|
|
1592
|
+
const hostOnlyView = kind === "poll" || kind === "question";
|
|
1593
|
+
if (hostOnlyView && context.isHost && audience !== "hosts") {
|
|
1594
|
+
return { next: null, dropped: "the room copy of an item a host receives in full" };
|
|
1595
|
+
}
|
|
1596
|
+
let next = withItem(collab, item, true);
|
|
1597
|
+
if (kind === "poll" && str(item["state"]) === "live") {
|
|
1598
|
+
const before = collab.polls.find((p) => p.id === str(item["id"]));
|
|
1599
|
+
if (before === void 0 || before.state !== "live") {
|
|
1600
|
+
next = { ...next, panelRequest: { panel: "polls", at: context.now } };
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
if (kind === "breakout") {
|
|
1604
|
+
const plan = planFromRow(item);
|
|
1605
|
+
if (plan !== null && plan.state === "closed") {
|
|
1606
|
+
next = { ...next, moveRequest: { roomId: null, at: context.now } };
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
const move = obj(payload["move"]);
|
|
1610
|
+
if (move !== null && str(move["identity"]) === context.localIdentity) {
|
|
1611
|
+
next = { ...next, moveRequest: { roomId: str(move["room_id"]), at: context.now } };
|
|
1612
|
+
}
|
|
1613
|
+
return { next };
|
|
1614
|
+
}
|
|
1615
|
+
if (Array.isArray(payload["removed"])) {
|
|
1616
|
+
return { next: without(collab, strings(payload["removed"])) };
|
|
1617
|
+
}
|
|
1618
|
+
if (Array.isArray(payload["hidden"])) {
|
|
1619
|
+
return context.isHost ? { next: null, dropped: "hosts keep dismissed questions" } : { next: without(collab, strings(payload["hidden"])) };
|
|
1620
|
+
}
|
|
1621
|
+
if (payload["cleared"] === "whiteboard") {
|
|
1622
|
+
return { next: { ...collab, strokes: [] } };
|
|
1623
|
+
}
|
|
1624
|
+
const broadcast = broadcastFrom(payload["broadcast"]);
|
|
1625
|
+
if (broadcast !== null) {
|
|
1626
|
+
return { next: { ...collab, broadcast } };
|
|
1627
|
+
}
|
|
1628
|
+
return { next: null, dropped: "a collab packet with nothing this version understands" };
|
|
1629
|
+
}
|
|
1630
|
+
function assignedRoom(plan, identity) {
|
|
1631
|
+
if (plan === null || plan.state !== "open" || identity === null) return null;
|
|
1632
|
+
return plan.rooms.find((room) => room.identities.includes(identity)) ?? null;
|
|
1633
|
+
}
|
|
1634
|
+
function autoAssign(identities, rooms, exclude = []) {
|
|
1635
|
+
const count2 = Math.max(1, Math.floor(rooms));
|
|
1636
|
+
const out = Array.from({ length: count2 }, () => []);
|
|
1637
|
+
const skip = new Set(exclude);
|
|
1638
|
+
let index = 0;
|
|
1639
|
+
for (const identity of identities) {
|
|
1640
|
+
if (skip.has(identity)) continue;
|
|
1641
|
+
out[index % count2].push(identity);
|
|
1642
|
+
index += 1;
|
|
1643
|
+
}
|
|
1644
|
+
return out;
|
|
1645
|
+
}
|
|
1646
|
+
function breakoutRemainingMs(plan, now) {
|
|
1647
|
+
if (plan === null || plan.state !== "open" || plan.endsAt === null) return null;
|
|
1648
|
+
const end = Date.parse(plan.endsAt);
|
|
1649
|
+
return Number.isNaN(end) ? null : Math.max(0, end - now);
|
|
1650
|
+
}
|
|
1651
|
+
function defaultCaptionLanguage(settings, viewerLanguage) {
|
|
1652
|
+
const configured = settings?.captionTranslationLanguage.trim() ?? "";
|
|
1653
|
+
if (configured.length > 0) return configured;
|
|
1654
|
+
const own = (viewerLanguage ?? "").trim();
|
|
1655
|
+
return own.length > 0 ? own : "en";
|
|
1656
|
+
}
|
|
1657
|
+
var CAPTION_LANGUAGES = [
|
|
1658
|
+
{ tag: "en", name: "English" },
|
|
1659
|
+
{ tag: "es", name: "Spanish" },
|
|
1660
|
+
{ tag: "fr", name: "French" },
|
|
1661
|
+
{ tag: "de", name: "German" },
|
|
1662
|
+
{ tag: "it", name: "Italian" },
|
|
1663
|
+
{ tag: "pt", name: "Portuguese" },
|
|
1664
|
+
{ tag: "nl", name: "Dutch" },
|
|
1665
|
+
{ tag: "pl", name: "Polish" },
|
|
1666
|
+
{ tag: "ru", name: "Russian" },
|
|
1667
|
+
{ tag: "uk", name: "Ukrainian" },
|
|
1668
|
+
{ tag: "tr", name: "Turkish" },
|
|
1669
|
+
{ tag: "ar", name: "Arabic" },
|
|
1670
|
+
{ tag: "he", name: "Hebrew" },
|
|
1671
|
+
{ tag: "fa", name: "Persian" },
|
|
1672
|
+
{ tag: "hi", name: "Hindi" },
|
|
1673
|
+
{ tag: "bn", name: "Bengali" },
|
|
1674
|
+
{ tag: "zh", name: "Chinese" },
|
|
1675
|
+
{ tag: "ja", name: "Japanese" },
|
|
1676
|
+
{ tag: "ko", name: "Korean" },
|
|
1677
|
+
{ tag: "vi", name: "Vietnamese" },
|
|
1678
|
+
{ tag: "th", name: "Thai" },
|
|
1679
|
+
{ tag: "id", name: "Indonesian" },
|
|
1680
|
+
{ tag: "sv", name: "Swedish" }
|
|
1681
|
+
];
|
|
1682
|
+
async function loadMeetingActivities(api, meetingId, roomToken = null) {
|
|
1683
|
+
let response;
|
|
1684
|
+
try {
|
|
1685
|
+
response = await api.request({
|
|
1686
|
+
path: `${MEET_ROUTES.collab}?meeting_id=${encodeURIComponent(meetingId)}`,
|
|
1687
|
+
method: "GET",
|
|
1688
|
+
operation: "meet.loadMeetingActivities",
|
|
1689
|
+
requireAuth: roomToken === null,
|
|
1690
|
+
...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
|
|
1691
|
+
});
|
|
1692
|
+
} catch (error) {
|
|
1693
|
+
return {
|
|
1694
|
+
collab: null,
|
|
1695
|
+
refusal: {
|
|
1696
|
+
message: `Polls, questions and the whiteboard could not be read (${error.message}).`,
|
|
1697
|
+
remedy: error.remedy ?? "Reload the page."
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
if (!response.ok) {
|
|
1702
|
+
let body = null;
|
|
1703
|
+
try {
|
|
1704
|
+
body = await response.json();
|
|
1705
|
+
} catch {
|
|
1706
|
+
body = null;
|
|
1707
|
+
}
|
|
1708
|
+
const said = readServerRefusal(body);
|
|
1709
|
+
return {
|
|
1710
|
+
collab: null,
|
|
1711
|
+
refusal: {
|
|
1712
|
+
message: said?.reason ?? `Polls and questions could not be read (the server answered ${response.status}).`,
|
|
1713
|
+
remedy: said?.remedy || "Reload the page."
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
return { collab: collabFromState(await response.json(), EMPTY_COLLAB), refusal: null };
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1366
1720
|
// src/core/repository.ts
|
|
1367
1721
|
var MEET_SCHEMA = "communication";
|
|
1368
1722
|
var TABLES = {
|
|
@@ -1438,7 +1792,19 @@ var INVITEE_COLUMNS = "id,meeting_id,invitee_user_id,email,display_name,role,rsv
|
|
|
1438
1792
|
var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
|
|
1439
1793
|
var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
|
|
1440
1794
|
var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
|
|
1441
|
-
var ATTENDEE_COLUMNS = "id,meeting_id,identity,participant_user_id,display_name,avatar_url,role,admission_state,is_agent,joined_at,left_at";
|
|
1795
|
+
var ATTENDEE_COLUMNS = "id,meeting_id,identity,participant_user_id,display_name,avatar_url,role,admission_state,is_agent,joined_at,left_at,sessions:metadata->sessions";
|
|
1796
|
+
function projectStays(value) {
|
|
1797
|
+
if (!Array.isArray(value)) return [];
|
|
1798
|
+
const stays = [];
|
|
1799
|
+
for (const item of value) {
|
|
1800
|
+
if (item === null || typeof item !== "object") continue;
|
|
1801
|
+
const joined = item["joined_at"];
|
|
1802
|
+
const left = item["left_at"];
|
|
1803
|
+
if (typeof joined !== "string" || joined.length === 0) continue;
|
|
1804
|
+
stays.push({ joinedAt: joined, leftAt: typeof left === "string" && left.length > 0 ? left : null });
|
|
1805
|
+
}
|
|
1806
|
+
return stays;
|
|
1807
|
+
}
|
|
1442
1808
|
function text(row, key) {
|
|
1443
1809
|
const value = row[key];
|
|
1444
1810
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
@@ -1537,7 +1903,7 @@ var OCCURRENCE_STATES = ["scheduled", "moved", "cancelled"];
|
|
|
1537
1903
|
function rsvpState(value) {
|
|
1538
1904
|
return RSVP_STATES.includes(value) ? value : "needs_action";
|
|
1539
1905
|
}
|
|
1540
|
-
function
|
|
1906
|
+
function num2(row, key) {
|
|
1541
1907
|
const value = row[key];
|
|
1542
1908
|
return typeof value === "number" ? value : null;
|
|
1543
1909
|
}
|
|
@@ -1559,7 +1925,7 @@ function projectInvitee(row) {
|
|
|
1559
1925
|
rsvpState: rsvpState(text(row, "rsvp_state")),
|
|
1560
1926
|
respondedAt: when(row, "responded_at"),
|
|
1561
1927
|
rsvpNote: text(row, "rsvp_note"),
|
|
1562
|
-
lastSentSequence:
|
|
1928
|
+
lastSentSequence: num2(row, "last_sent_sequence"),
|
|
1563
1929
|
lastSentAt: when(row, "last_sent_at")
|
|
1564
1930
|
};
|
|
1565
1931
|
}
|
|
@@ -1570,10 +1936,10 @@ function projectOccurrence(row) {
|
|
|
1570
1936
|
meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
|
|
1571
1937
|
originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
|
|
1572
1938
|
occurrenceStart: when(row, "occurrence_start") ?? requireText(row, "occurrence_start", operation),
|
|
1573
|
-
durationMinutes:
|
|
1939
|
+
durationMinutes: num2(row, "duration_minutes"),
|
|
1574
1940
|
// An unknown state is shown as scheduled, never dropped: the row is real.
|
|
1575
1941
|
state: OCCURRENCE_STATES.includes(state) ? state : "scheduled",
|
|
1576
|
-
sequence:
|
|
1942
|
+
sequence: num2(row, "sequence") ?? 0
|
|
1577
1943
|
};
|
|
1578
1944
|
}
|
|
1579
1945
|
function projectUpcoming(row) {
|
|
@@ -1602,9 +1968,9 @@ function projectOverride(row) {
|
|
|
1602
1968
|
originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
|
|
1603
1969
|
state: text(row, "state") === "moved" ? "moved" : "cancelled",
|
|
1604
1970
|
newStart: when(row, "new_start"),
|
|
1605
|
-
newDurationMinutes:
|
|
1971
|
+
newDurationMinutes: num2(row, "new_duration_minutes"),
|
|
1606
1972
|
reason: text(row, "reason"),
|
|
1607
|
-
calendarSequence:
|
|
1973
|
+
calendarSequence: num2(row, "calendar_sequence") ?? 0,
|
|
1608
1974
|
deletedAt: when(row, "deleted_at")
|
|
1609
1975
|
};
|
|
1610
1976
|
}
|
|
@@ -1628,7 +1994,7 @@ function projectRsvpInvitation(body) {
|
|
|
1628
1994
|
title: text(meeting, "title") ?? "Meeting",
|
|
1629
1995
|
slug: requireText(meeting, "slug", operation),
|
|
1630
1996
|
scheduledFor: when(meeting, "scheduled_for"),
|
|
1631
|
-
durationMinutes:
|
|
1997
|
+
durationMinutes: num2(meeting, "duration_minutes"),
|
|
1632
1998
|
timeZone: text(meeting, "time_zone") ?? "UTC",
|
|
1633
1999
|
agenda: text(meeting, "agenda"),
|
|
1634
2000
|
recurrenceRule: text(meeting, "recurrence_rule"),
|
|
@@ -1662,6 +2028,7 @@ function changesPatch(changes) {
|
|
|
1662
2028
|
patch["scheduled_duration_minutes"] = changes.scheduledDurationMinutes;
|
|
1663
2029
|
}
|
|
1664
2030
|
if (changes.recurrenceRule !== void 0) patch["recurrence_rule"] = changes.recurrenceRule;
|
|
2031
|
+
if (changes.keepExceptions !== void 0) patch["keep_exceptions"] = changes.keepExceptions;
|
|
1665
2032
|
return patch;
|
|
1666
2033
|
}
|
|
1667
2034
|
function createMeetRepository(options) {
|
|
@@ -1808,7 +2175,8 @@ function createMeetRepository(options) {
|
|
|
1808
2175
|
admissionState: states.includes(admission) ? admission : "admitted",
|
|
1809
2176
|
isAgent: bool(row, "is_agent", false),
|
|
1810
2177
|
joinedAt: text(row, "joined_at"),
|
|
1811
|
-
leftAt: text(row, "left_at")
|
|
2178
|
+
leftAt: text(row, "left_at"),
|
|
2179
|
+
sessions: projectStays(row["sessions"])
|
|
1812
2180
|
};
|
|
1813
2181
|
}
|
|
1814
2182
|
function requireOrg(organizationId, operation) {
|
|
@@ -2202,8 +2570,8 @@ function projectLibraryRecording(row) {
|
|
|
2202
2570
|
fileId: text(row, "file_id"),
|
|
2203
2571
|
startedAt: text(row, "started_at"),
|
|
2204
2572
|
endedAt: text(row, "ended_at"),
|
|
2205
|
-
durationMs:
|
|
2206
|
-
sizeBytes:
|
|
2573
|
+
durationMs: num2(row, "duration_ms"),
|
|
2574
|
+
sizeBytes: num2(row, "size_bytes"),
|
|
2207
2575
|
failureReason: text(row, "failure_reason"),
|
|
2208
2576
|
archivedAt: text(row, "deleted_at"),
|
|
2209
2577
|
organizationId: asOrganizationId(requireText(row, "organization_id", operation))
|
|
@@ -2230,7 +2598,7 @@ function projectSearchHit(row) {
|
|
|
2230
2598
|
speaker: text(row, "speaker"),
|
|
2231
2599
|
snippet: text(row, "snippet") ?? "",
|
|
2232
2600
|
at: text(row, "said_at"),
|
|
2233
|
-
rank:
|
|
2601
|
+
rank: num2(row, "rank") ?? 0
|
|
2234
2602
|
};
|
|
2235
2603
|
}
|
|
2236
2604
|
function snippetRuns(snippet) {
|
|
@@ -2247,1386 +2615,1460 @@ function snippetRuns(snippet) {
|
|
|
2247
2615
|
return runs;
|
|
2248
2616
|
}
|
|
2249
2617
|
|
|
2250
|
-
// src/core/
|
|
2251
|
-
|
|
2252
|
-
"
|
|
2253
|
-
"starting",
|
|
2254
|
-
"recording",
|
|
2255
|
-
"stopping",
|
|
2256
|
-
"processing",
|
|
2257
|
-
"available",
|
|
2258
|
-
"expired",
|
|
2259
|
-
"failed"
|
|
2260
|
-
];
|
|
2261
|
-
var ROOM_EVENT_PREFIX = "matrx.meet.";
|
|
2262
|
-
var ROOM_EVENTS = {
|
|
2263
|
-
/** One durable `meet_notes` row was written. Payload `{ note: <row> }`. */
|
|
2264
|
-
note: "matrx.meet.note",
|
|
2265
|
-
/** `meet_meetings.metadata.note_taker` changed. Payload `{ meeting_id, note_taker }`. */
|
|
2266
|
-
noteTaker: "matrx.meet.note_taker",
|
|
2267
|
-
/** A wrap-up pass finished. Payload `{ meeting_id, note_ids, segment_count }`. */
|
|
2268
|
-
wrapUp: "matrx.meet.wrap_up",
|
|
2269
|
-
/** The meeting ended for everyone. Payload `{ meeting_id, ended_at, trigger }`. */
|
|
2270
|
-
ended: "matrx.meet.ended",
|
|
2271
|
-
/**
|
|
2272
|
-
* A `meet_recordings` row changed state. Payload
|
|
2273
|
-
* `{ meeting_id, recording: { id, state, started_by } }`.
|
|
2274
|
-
*
|
|
2275
|
-
* 🚨 THIS IS THE ONLY WAY A GUEST LEARNS A RECORDING IS RUNNING. PreJoin
|
|
2276
|
-
* promises every joiner an indicator, and `anon` holds no SELECT grant on
|
|
2277
|
-
* `meet_recordings` (MRI-A10 settled that widening it is forbidden), so
|
|
2278
|
-
* without this packet the promise was a sentence with nothing behind it: on
|
|
2279
|
-
* production 2026-09-09 the guest lane showed no indicator at all while the
|
|
2280
|
-
* host recorded (MRI-C8).
|
|
2281
|
-
*/
|
|
2282
|
-
recording: "matrx.meet.recording",
|
|
2283
|
-
/**
|
|
2284
|
-
* A chat message to everyone was SAVED (Meet wave 2). Payload
|
|
2285
|
-
* `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
|
|
2286
|
-
*/
|
|
2287
|
-
chat: "matrx.meet.chat",
|
|
2288
|
-
/**
|
|
2289
|
-
* A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`:
|
|
2290
|
-
* `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the
|
|
2291
|
-
* person concerned; `lower-hands {identities: [...] | "*"}` · `role {identity,
|
|
2292
|
-
* role}` — to everyone.
|
|
2293
|
-
*/
|
|
2294
|
-
moderation: "matrx.meet.moderation"
|
|
2295
|
-
};
|
|
2296
|
-
function isRoomEventTopic(topic) {
|
|
2297
|
-
return topic.startsWith(ROOM_EVENT_PREFIX);
|
|
2298
|
-
}
|
|
2299
|
-
function object(value) {
|
|
2300
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
2618
|
+
// src/core/names.ts
|
|
2619
|
+
function isGuestIdentity(identity) {
|
|
2620
|
+
return String(identity).startsWith("guest:");
|
|
2301
2621
|
}
|
|
2302
|
-
function
|
|
2303
|
-
return
|
|
2622
|
+
function isAgentIdentity(identity) {
|
|
2623
|
+
return String(identity).startsWith("agent:");
|
|
2304
2624
|
}
|
|
2305
|
-
var
|
|
2306
|
-
|
|
2307
|
-
|
|
2625
|
+
var NOTE_TAKER_PRODUCT_NAME = "Matrx note-taker";
|
|
2626
|
+
var AGENT_PRODUCT_NAME = "Matrx agent";
|
|
2627
|
+
var UNNAMED_GUEST = "Unnamed guest";
|
|
2628
|
+
var UNNAMED_PARTICIPANT = "Unnamed participant";
|
|
2629
|
+
var PLACEHOLDER_NAMES = [
|
|
2630
|
+
NOTE_TAKER_PRODUCT_NAME,
|
|
2631
|
+
AGENT_PRODUCT_NAME,
|
|
2632
|
+
UNNAMED_GUEST,
|
|
2633
|
+
UNNAMED_PARTICIPANT
|
|
2634
|
+
];
|
|
2635
|
+
function isPlaceholderName(name) {
|
|
2636
|
+
return typeof name === "string" && PLACEHOLDER_NAMES.includes(name.trim());
|
|
2308
2637
|
}
|
|
2309
|
-
function
|
|
2310
|
-
const
|
|
2311
|
-
if (
|
|
2312
|
-
return
|
|
2313
|
-
}
|
|
2314
|
-
const payload = object(args.payload);
|
|
2315
|
-
if (payload === null) {
|
|
2316
|
-
return ignored(`${topic} carried no object payload`);
|
|
2317
|
-
}
|
|
2318
|
-
const now = args.now ?? (() => Date.now());
|
|
2319
|
-
switch (topic) {
|
|
2320
|
-
case ROOM_EVENTS.note:
|
|
2321
|
-
return applyNote(store, payload);
|
|
2322
|
-
case ROOM_EVENTS.noteTaker:
|
|
2323
|
-
return applyNoteTaker(store, payload);
|
|
2324
|
-
case ROOM_EVENTS.wrapUp:
|
|
2325
|
-
return applyWrapUp(store, payload, now);
|
|
2326
|
-
case ROOM_EVENTS.ended:
|
|
2327
|
-
return applyEnded(store, payload);
|
|
2328
|
-
case ROOM_EVENTS.recording:
|
|
2329
|
-
return applyRecording(store, payload);
|
|
2330
|
-
case ROOM_EVENTS.chat:
|
|
2331
|
-
return applyChat(store, payload);
|
|
2332
|
-
case ROOM_EVENTS.moderation:
|
|
2333
|
-
return applyModeration(store, payload, now);
|
|
2334
|
-
default:
|
|
2335
|
-
return ignored(`${topic} is not known to this version of @ai-matrx/meet`);
|
|
2638
|
+
function placeholderNameFor(identity) {
|
|
2639
|
+
const key = String(identity);
|
|
2640
|
+
if (isAgentIdentity(key)) {
|
|
2641
|
+
return key.startsWith("agent:note_taker:") || key.startsWith("agent:note-taker") ? NOTE_TAKER_PRODUCT_NAME : AGENT_PRODUCT_NAME;
|
|
2336
2642
|
}
|
|
2643
|
+
return isGuestIdentity(key) ? UNNAMED_GUEST : UNNAMED_PARTICIPANT;
|
|
2337
2644
|
}
|
|
2338
|
-
function
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
if (
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
if (meeting !== null && row["meeting_id"] !== meeting.id) {
|
|
2346
|
-
return ignored("a note for a different meeting than this room is showing");
|
|
2347
|
-
}
|
|
2348
|
-
try {
|
|
2349
|
-
store.upsertNote({ ...projectNote(row), source: "room" });
|
|
2350
|
-
} catch (error) {
|
|
2351
|
-
return ignored(`a note row this client could not read: ${String(error)}`);
|
|
2352
|
-
}
|
|
2353
|
-
return HANDLED;
|
|
2645
|
+
function isRealName(identity, name) {
|
|
2646
|
+
if (typeof name !== "string") return false;
|
|
2647
|
+
const trimmed = name.trim();
|
|
2648
|
+
if (trimmed.length === 0) return false;
|
|
2649
|
+
if (trimmed === String(identity)) return false;
|
|
2650
|
+
if (isPlaceholderName(trimmed)) return false;
|
|
2651
|
+
return true;
|
|
2354
2652
|
}
|
|
2355
|
-
function
|
|
2356
|
-
const
|
|
2357
|
-
if (
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
}
|
|
2362
|
-
const meetingId = text2(payload["meeting_id"]);
|
|
2363
|
-
if (meetingId !== null && meetingId !== meeting.id) {
|
|
2364
|
-
return ignored("a note-taker state for a different meeting");
|
|
2365
|
-
}
|
|
2366
|
-
const next = {
|
|
2367
|
-
...meeting,
|
|
2368
|
-
metadata: { ...meeting.metadata, note_taker: noteTaker }
|
|
2369
|
-
};
|
|
2370
|
-
store.patch({ meeting: next });
|
|
2371
|
-
return HANDLED;
|
|
2653
|
+
function learnName(book, identity, name) {
|
|
2654
|
+
const key = String(identity);
|
|
2655
|
+
if (key.length === 0) return book;
|
|
2656
|
+
if (!isRealName(key, name)) return book;
|
|
2657
|
+
const learned = name.trim();
|
|
2658
|
+
if (book[key] === learned) return book;
|
|
2659
|
+
return { ...book, [key]: learned };
|
|
2372
2660
|
}
|
|
2373
|
-
function
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
if (meeting !== null && meetingId !== meeting.id) {
|
|
2378
|
-
return ignored("a wrap-up for a different meeting");
|
|
2379
|
-
}
|
|
2380
|
-
const rawIds = payload["note_ids"];
|
|
2381
|
-
const noteIds = Array.isArray(rawIds) ? rawIds.filter((value) => typeof value === "string") : [];
|
|
2382
|
-
const segmentCount = payload["segment_count"];
|
|
2383
|
-
const announcement = {
|
|
2384
|
-
meetingId: asMeetingId(meetingId),
|
|
2385
|
-
noteIds,
|
|
2386
|
-
segmentCount: typeof segmentCount === "number" ? segmentCount : 0,
|
|
2387
|
-
at: new Date(now()).toISOString()
|
|
2388
|
-
};
|
|
2389
|
-
store.announceWrapUp(announcement);
|
|
2390
|
-
return HANDLED;
|
|
2391
|
-
}
|
|
2392
|
-
function applyRecording(store, payload) {
|
|
2393
|
-
const row = object(payload["recording"]);
|
|
2394
|
-
if (row === null) return ignored("a recording event with no `recording` row");
|
|
2395
|
-
const meetingId = text2(payload["meeting_id"]);
|
|
2396
|
-
const meeting = store.snapshot().meeting;
|
|
2397
|
-
if (meeting !== null && meetingId !== null && meetingId !== meeting.id) {
|
|
2398
|
-
return ignored("a recording state for a different meeting than this room is showing");
|
|
2399
|
-
}
|
|
2400
|
-
const state = text2(row["state"]);
|
|
2401
|
-
if (state === null || !RECORDING_STATES.includes(state)) {
|
|
2402
|
-
return ignored(`a recording state this version does not know: ${String(row["state"])}`);
|
|
2403
|
-
}
|
|
2404
|
-
const by = text2(row["started_by"]);
|
|
2405
|
-
store.setRecording(
|
|
2406
|
-
state,
|
|
2407
|
-
by === null ? null : by,
|
|
2408
|
-
"room",
|
|
2409
|
-
text2(row["id"])
|
|
2410
|
-
);
|
|
2411
|
-
return HANDLED;
|
|
2412
|
-
}
|
|
2413
|
-
function applyEnded(store, payload) {
|
|
2414
|
-
const meetingId = text2(payload["meeting_id"]);
|
|
2415
|
-
const meeting = store.snapshot().meeting;
|
|
2416
|
-
if (meeting === null) return ignored("an ended event with no meeting record");
|
|
2417
|
-
if (meetingId !== null && meetingId !== meeting.id) {
|
|
2418
|
-
return ignored("an ended event for a different meeting");
|
|
2419
|
-
}
|
|
2420
|
-
const endedAt = text2(payload["ended_at"]) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2421
|
-
if (meeting.endedAt === endedAt) return HANDLED;
|
|
2422
|
-
store.patch({ meeting: { ...meeting, endedAt } });
|
|
2423
|
-
return HANDLED;
|
|
2424
|
-
}
|
|
2425
|
-
function chatFromRow(row) {
|
|
2426
|
-
const id = text2(row["id"]);
|
|
2427
|
-
const identity = text2(row["sender_identity"]);
|
|
2428
|
-
const body = text2(row["body"]);
|
|
2429
|
-
if (id === null || identity === null || body === null) return null;
|
|
2430
|
-
return {
|
|
2431
|
-
id,
|
|
2432
|
-
identity: asParticipantIdentity(identity),
|
|
2433
|
-
displayName: text2(row["sender_name"]) ?? "Participant",
|
|
2434
|
-
text: body,
|
|
2435
|
-
at: text2(row["created_at"]) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2436
|
-
// `false` only from a server whose chat table has not landed: the room heard
|
|
2437
|
-
// it, nobody will see it later — and the line says so.
|
|
2438
|
-
saved: row["saved"] !== false
|
|
2439
|
-
};
|
|
2440
|
-
}
|
|
2441
|
-
function applyChat(store, payload) {
|
|
2442
|
-
const row = object(payload["message"]);
|
|
2443
|
-
if (row === null) return ignored("a chat event with no `message` row");
|
|
2444
|
-
const meeting = store.snapshot().meeting;
|
|
2445
|
-
if (meeting !== null && text2(row["meeting_id"]) !== null && row["meeting_id"] !== meeting.id) {
|
|
2446
|
-
return ignored("a chat message for a different meeting");
|
|
2447
|
-
}
|
|
2448
|
-
const message = chatFromRow(row);
|
|
2449
|
-
if (message === null) return ignored("a chat row with no id, sender or body");
|
|
2450
|
-
store.addChat(message);
|
|
2451
|
-
return HANDLED;
|
|
2661
|
+
function learnNames(book, entries) {
|
|
2662
|
+
let next = book;
|
|
2663
|
+
for (const entry of entries) next = learnName(next, entry.identity, entry.displayName);
|
|
2664
|
+
return next;
|
|
2452
2665
|
}
|
|
2453
|
-
function
|
|
2454
|
-
const
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
}
|
|
2460
|
-
case "ask-unmute":
|
|
2461
|
-
return `${by} is asking you to unmute.`;
|
|
2462
|
-
case "removed":
|
|
2463
|
-
return `${by} removed you from this meeting.`;
|
|
2464
|
-
default:
|
|
2465
|
-
return null;
|
|
2466
|
-
}
|
|
2666
|
+
function displayNameFor(book, identity, preferred) {
|
|
2667
|
+
const key = String(identity);
|
|
2668
|
+
if (isRealName(key, preferred)) return preferred.trim();
|
|
2669
|
+
const known = book[key];
|
|
2670
|
+
if (known !== void 0) return known;
|
|
2671
|
+
return placeholderNameFor(key);
|
|
2467
2672
|
}
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2673
|
+
|
|
2674
|
+
// src/core/recording.ts
|
|
2675
|
+
function createRecordingController(options) {
|
|
2676
|
+
async function post(route, body) {
|
|
2677
|
+
let response;
|
|
2678
|
+
try {
|
|
2679
|
+
response = await options.api.request({
|
|
2680
|
+
path: route,
|
|
2681
|
+
method: "POST",
|
|
2682
|
+
// A GUEST CAN NEVER START OR STOP A RECORDING (R5) — refused here,
|
|
2683
|
+
// before the wire, with the same sentence the hidden control implies.
|
|
2684
|
+
requireAuth: true,
|
|
2685
|
+
operation: "meet.recording",
|
|
2686
|
+
body
|
|
2687
|
+
});
|
|
2688
|
+
} catch (error) {
|
|
2689
|
+
if (error.code === "session-unavailable") {
|
|
2690
|
+
throw new MeetError(
|
|
2691
|
+
"forbidden",
|
|
2692
|
+
"meet.recording: recording requires an authenticated host",
|
|
2693
|
+
"A guest can never start or stop a recording (R5). The control is hidden for guests."
|
|
2694
|
+
);
|
|
2486
2695
|
}
|
|
2487
|
-
|
|
2696
|
+
throw error;
|
|
2488
2697
|
}
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
if (identity === snapshot.localIdentity) {
|
|
2496
|
-
store.patch({
|
|
2497
|
-
grants: { ...snapshot.grants, role },
|
|
2498
|
-
moderationNotice: {
|
|
2499
|
-
kind: "role",
|
|
2500
|
-
text: role === "host" ? "You are now the host." : role === "cohost" ? "You are now a co-host." : "You are no longer a co-host.",
|
|
2501
|
-
at: now()
|
|
2502
|
-
}
|
|
2503
|
-
});
|
|
2504
|
-
}
|
|
2505
|
-
return HANDLED;
|
|
2698
|
+
if (response.status === 403) {
|
|
2699
|
+
throw new MeetError(
|
|
2700
|
+
"forbidden",
|
|
2701
|
+
"meet.recording: the server refused this recording action",
|
|
2702
|
+
"The actor is not a host of this meeting, or the org policy is `disabled`. The server is the authority; do not re-decide it on the client."
|
|
2703
|
+
);
|
|
2506
2704
|
}
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
return HANDLED;
|
|
2705
|
+
if (!response.ok) {
|
|
2706
|
+
throw new MeetError(
|
|
2707
|
+
"transport",
|
|
2708
|
+
`meet.recording: the server returned ${response.status}`,
|
|
2709
|
+
"Egress could not be controlled. The recording state stays where it was rather than showing a recording indicator for a recording that is not running."
|
|
2710
|
+
);
|
|
2514
2711
|
}
|
|
2515
|
-
default:
|
|
2516
|
-
return ignored(`a moderation kind this version does not know: ${String(kind)}`);
|
|
2517
2712
|
}
|
|
2518
|
-
}
|
|
2519
|
-
var ROLES = ["host", "cohost", "participant", "guest"];
|
|
2520
|
-
|
|
2521
|
-
// src/core/devices.ts
|
|
2522
|
-
var KINDS = ["audioinput", "videoinput", "audiooutput"];
|
|
2523
|
-
function isDeviceKind(value) {
|
|
2524
|
-
return value === "audioinput" || value === "videoinput" || value === "audiooutput";
|
|
2525
|
-
}
|
|
2526
|
-
function createWebDeviceStorage(key = "mx.meet.devices") {
|
|
2527
|
-
const store = globalThis.localStorage;
|
|
2528
|
-
if (store === void 0) return createMemoryDeviceStorage();
|
|
2529
2713
|
return {
|
|
2530
|
-
|
|
2531
|
-
|
|
2714
|
+
canStart(meeting, isHost) {
|
|
2715
|
+
if (meeting === null) return false;
|
|
2716
|
+
if (meeting.recordingPolicy === "disabled") return false;
|
|
2717
|
+
if (meeting.recordingPolicy === "always-on") return false;
|
|
2718
|
+
return isHost;
|
|
2719
|
+
},
|
|
2720
|
+
async start({ meetingId, roomName, by }) {
|
|
2721
|
+
options.store.setRecording("starting", by, "local");
|
|
2532
2722
|
try {
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
} catch {
|
|
2538
|
-
|
|
2723
|
+
await post(MEET_ROUTES.recordingStart, {
|
|
2724
|
+
meeting_id: meetingId,
|
|
2725
|
+
room_name: roomName
|
|
2726
|
+
});
|
|
2727
|
+
} catch (error) {
|
|
2728
|
+
options.store.setRecording("idle", null, "local");
|
|
2729
|
+
throw error;
|
|
2539
2730
|
}
|
|
2731
|
+
options.announce?.("starting", by);
|
|
2540
2732
|
},
|
|
2541
|
-
|
|
2733
|
+
async stop({ meetingId, by }) {
|
|
2734
|
+
options.store.setRecording("stopping", options.store.snapshot().recordingStartedBy, "local");
|
|
2542
2735
|
try {
|
|
2543
|
-
|
|
2544
|
-
} catch {
|
|
2736
|
+
await post(MEET_ROUTES.recordingStop, { meeting_id: meetingId });
|
|
2737
|
+
} catch (error) {
|
|
2738
|
+
options.store.setRecording(
|
|
2739
|
+
"recording",
|
|
2740
|
+
options.store.snapshot().recordingStartedBy,
|
|
2741
|
+
"local"
|
|
2742
|
+
);
|
|
2743
|
+
throw error;
|
|
2744
|
+
}
|
|
2745
|
+
options.announce?.("stopping", by);
|
|
2746
|
+
},
|
|
2747
|
+
consentNotice(meeting) {
|
|
2748
|
+
if (meeting === null || meeting.recordingPolicy === "disabled") return null;
|
|
2749
|
+
if (meeting.recordingPolicy === "always-on") {
|
|
2750
|
+
return "This meeting is recorded and transcribed automatically. By joining, you consent to being recorded.";
|
|
2545
2751
|
}
|
|
2752
|
+
return "The host can record and transcribe this meeting. You will see a recording indicator whenever it is running.";
|
|
2753
|
+
},
|
|
2754
|
+
async acknowledge(meetingId) {
|
|
2755
|
+
await options.acknowledgeConsent?.(meetingId);
|
|
2546
2756
|
}
|
|
2547
2757
|
};
|
|
2548
2758
|
}
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
}
|
|
2557
|
-
};
|
|
2759
|
+
var RECORDING_IN_FLIGHT = [
|
|
2760
|
+
"starting",
|
|
2761
|
+
"recording",
|
|
2762
|
+
"stopping"
|
|
2763
|
+
];
|
|
2764
|
+
function isRecordingInFlight(state) {
|
|
2765
|
+
return RECORDING_IN_FLIGHT.includes(state);
|
|
2558
2766
|
}
|
|
2559
|
-
function
|
|
2560
|
-
const storage = options.storage ?? createMemoryDeviceStorage();
|
|
2561
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
2562
|
-
const stored = storage.read();
|
|
2563
|
-
let devices = {
|
|
2564
|
-
audioinput: [],
|
|
2565
|
-
videoinput: [],
|
|
2566
|
-
audiooutput: []
|
|
2567
|
-
};
|
|
2568
|
-
let selected = {
|
|
2569
|
-
audioinput: stored["audioinput"] ?? null,
|
|
2570
|
-
videoinput: stored["videoinput"] ?? null,
|
|
2571
|
-
audiooutput: stored["audiooutput"] ?? null
|
|
2572
|
-
};
|
|
2573
|
-
let labelsHidden = true;
|
|
2574
|
-
let permission = "unknown";
|
|
2575
|
-
let fallbackNotice = null;
|
|
2576
|
-
let current = {
|
|
2577
|
-
devices,
|
|
2578
|
-
selected,
|
|
2579
|
-
labelsHidden,
|
|
2580
|
-
permission,
|
|
2581
|
-
fallbackNotice
|
|
2582
|
-
};
|
|
2583
|
-
function snapshot() {
|
|
2584
|
-
return current;
|
|
2585
|
-
}
|
|
2586
|
-
function commit() {
|
|
2587
|
-
current = { devices, selected, labelsHidden, permission, fallbackNotice };
|
|
2588
|
-
return current;
|
|
2589
|
-
}
|
|
2590
|
-
function emit() {
|
|
2591
|
-
const next = commit();
|
|
2592
|
-
options.onChange?.(next);
|
|
2593
|
-
for (const listener of listeners) listener(next);
|
|
2594
|
-
return next;
|
|
2595
|
-
}
|
|
2596
|
-
async function enumerate() {
|
|
2597
|
-
let raw;
|
|
2598
|
-
try {
|
|
2599
|
-
raw = await options.media.enumerateDevices();
|
|
2600
|
-
} catch (error) {
|
|
2601
|
-
throw normalizeDeviceError(error, "meet.enumerateDevices");
|
|
2602
|
-
}
|
|
2603
|
-
const grouped = {
|
|
2604
|
-
audioinput: [],
|
|
2605
|
-
videoinput: [],
|
|
2606
|
-
audiooutput: []
|
|
2607
|
-
};
|
|
2608
|
-
let anyLabel = false;
|
|
2609
|
-
for (const device of raw) {
|
|
2610
|
-
if (!isDeviceKind(device.kind)) continue;
|
|
2611
|
-
if (device.label.length > 0) anyLabel = true;
|
|
2612
|
-
grouped[device.kind].push({
|
|
2613
|
-
deviceId: device.deviceId,
|
|
2614
|
-
kind: device.kind,
|
|
2615
|
-
label: device.label
|
|
2616
|
-
});
|
|
2617
|
-
}
|
|
2618
|
-
devices = grouped;
|
|
2619
|
-
labelsHidden = !anyLabel && raw.length > 0;
|
|
2620
|
-
const lost = [];
|
|
2621
|
-
for (const kind of KINDS) {
|
|
2622
|
-
const current2 = selected[kind];
|
|
2623
|
-
if (current2 === null) continue;
|
|
2624
|
-
if (!grouped[kind].some((device) => device.deviceId === current2)) {
|
|
2625
|
-
selected = { ...selected, [kind]: null };
|
|
2626
|
-
lost.push(kind);
|
|
2627
|
-
}
|
|
2628
|
-
}
|
|
2629
|
-
fallbackNotice = lost.length === 0 ? null : `A remembered ${lost.join(" and ")} device is no longer connected \u2014 using the system default. Pick another in the device menu.`;
|
|
2630
|
-
return emit();
|
|
2631
|
-
}
|
|
2632
|
-
const onDeviceChange = () => {
|
|
2633
|
-
void enumerate();
|
|
2634
|
-
};
|
|
2635
|
-
options.media.addEventListener?.("devicechange", onDeviceChange);
|
|
2636
|
-
return {
|
|
2637
|
-
snapshot,
|
|
2638
|
-
refresh: enumerate,
|
|
2639
|
-
async requestPermission(kinds) {
|
|
2640
|
-
if (!kinds.audio && !kinds.video) {
|
|
2641
|
-
throw new MeetError(
|
|
2642
|
-
"misconfigured",
|
|
2643
|
-
"meet.requestPermission: asked for neither audio nor video",
|
|
2644
|
-
"Request at least one kind. Prompting for nothing silently resolves and leaves labels hidden, which looks like a broken picker."
|
|
2645
|
-
);
|
|
2646
|
-
}
|
|
2647
|
-
try {
|
|
2648
|
-
const stream = await options.media.getUserMedia({
|
|
2649
|
-
...kinds.audio ? { audio: true } : {},
|
|
2650
|
-
...kinds.video ? { video: true } : {}
|
|
2651
|
-
});
|
|
2652
|
-
for (const track of stream.getTracks()) track.stop();
|
|
2653
|
-
permission = "granted";
|
|
2654
|
-
} catch (error) {
|
|
2655
|
-
permission = "denied";
|
|
2656
|
-
emit();
|
|
2657
|
-
throw normalizeDeviceError(error, "meet.requestPermission");
|
|
2658
|
-
}
|
|
2659
|
-
return enumerate();
|
|
2660
|
-
},
|
|
2661
|
-
select(kind, deviceId) {
|
|
2662
|
-
selected = { ...selected, [kind]: deviceId };
|
|
2663
|
-
fallbackNotice = null;
|
|
2664
|
-
storage.write({
|
|
2665
|
-
...storage.read(),
|
|
2666
|
-
[kind]: deviceId
|
|
2667
|
-
});
|
|
2668
|
-
return emit();
|
|
2669
|
-
},
|
|
2670
|
-
resolve(kind) {
|
|
2671
|
-
const current2 = selected[kind];
|
|
2672
|
-
if (current2 !== null && devices[kind].some((device) => device.deviceId === current2)) {
|
|
2673
|
-
return current2;
|
|
2674
|
-
}
|
|
2675
|
-
return devices[kind][0]?.deviceId ?? null;
|
|
2676
|
-
},
|
|
2677
|
-
subscribe(listener) {
|
|
2678
|
-
listeners.add(listener);
|
|
2679
|
-
return () => listeners.delete(listener);
|
|
2680
|
-
},
|
|
2681
|
-
dispose() {
|
|
2682
|
-
options.media.removeEventListener?.("devicechange", onDeviceChange);
|
|
2683
|
-
listeners.clear();
|
|
2684
|
-
}
|
|
2685
|
-
};
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
// src/core/format.ts
|
|
2689
|
-
function participantSummary(participants) {
|
|
2690
|
-
const names = participants.filter((p) => !p.isLocal).map((p) => p.displayName);
|
|
2691
|
-
if (names.length === 0) return "Just you";
|
|
2692
|
-
if (names.length === 1) return names[0] ?? "";
|
|
2693
|
-
if (names.length === 2) return `${names[0]} and ${names[1]}`;
|
|
2694
|
-
return `${names[0]}, ${names[1]} and ${names.length - 2} ${names.length - 2 === 1 ? "other" : "others"}`;
|
|
2695
|
-
}
|
|
2696
|
-
function callStateLabel(state, outgoing) {
|
|
2767
|
+
function recordingLabel(state) {
|
|
2697
2768
|
switch (state) {
|
|
2698
|
-
case "
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
return outgoing ? "On another call" : "You were busy";
|
|
2711
|
-
case "cancelled":
|
|
2712
|
-
return outgoing ? "Cancelled" : "Cancelled by caller";
|
|
2713
|
-
case "ended":
|
|
2714
|
-
return "Call ended";
|
|
2769
|
+
case "starting":
|
|
2770
|
+
return "Starting recording\u2026";
|
|
2771
|
+
case "recording":
|
|
2772
|
+
return "Recording";
|
|
2773
|
+
case "stopping":
|
|
2774
|
+
return "Stopping recording\u2026";
|
|
2775
|
+
case "processing":
|
|
2776
|
+
return "Processing recording\u2026";
|
|
2777
|
+
case "available":
|
|
2778
|
+
return "Recording ready";
|
|
2779
|
+
case "expired":
|
|
2780
|
+
return "Recording expired";
|
|
2715
2781
|
case "failed":
|
|
2716
|
-
return "
|
|
2782
|
+
return "Recording failed";
|
|
2717
2783
|
default:
|
|
2718
|
-
return
|
|
2784
|
+
return null;
|
|
2719
2785
|
}
|
|
2720
2786
|
}
|
|
2721
|
-
function countLabel(count2, singular, plural = `${singular}s`) {
|
|
2722
|
-
return `${count2} ${count2 === 1 ? singular : plural}`;
|
|
2723
|
-
}
|
|
2724
2787
|
|
|
2725
|
-
// src/core/
|
|
2726
|
-
var
|
|
2727
|
-
var
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
invite_id: invite.id,
|
|
2734
|
-
room_name: invite.roomName,
|
|
2735
|
-
mode: invite.mode,
|
|
2736
|
-
caller_name: invite.callerName,
|
|
2737
|
-
expires_at: invite.expiresAt
|
|
2738
|
-
}
|
|
2739
|
-
};
|
|
2740
|
-
}
|
|
2741
|
-
function callInviteText(invite) {
|
|
2742
|
-
return invite.mode === "video" ? `${invite.callerName} started a video call` : `${invite.callerName} started an audio call`;
|
|
2788
|
+
// src/core/attendance.ts
|
|
2789
|
+
var IDLE_TRACK = { enabled: false, unavailable: null };
|
|
2790
|
+
var IDLE_MEDIA = {
|
|
2791
|
+
microphone: IDLE_TRACK,
|
|
2792
|
+
camera: IDLE_TRACK
|
|
2793
|
+
};
|
|
2794
|
+
function unavailableFrom(error) {
|
|
2795
|
+
return { code: error.code, message: error.message, remedy: error.remedy };
|
|
2743
2796
|
}
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
return
|
|
2747
|
-
kind: CALL_INVITE_KIND,
|
|
2748
|
-
versions: [CALL_INVITE_VERSION],
|
|
2749
|
-
choices: (payload) => {
|
|
2750
|
-
if (Date.parse(payload.expires_at) < now()) return [];
|
|
2751
|
-
return [
|
|
2752
|
-
{ id: "join", label: payload.mode === "video" ? "Join video" : "Join call", tone: "primary" },
|
|
2753
|
-
{ id: "decline", label: "Decline", tone: "neutral" }
|
|
2754
|
-
];
|
|
2755
|
-
},
|
|
2756
|
-
summarize: (payload) => payload.mode === "video" ? `${payload.caller_name} started a video call` : `${payload.caller_name} started an audio call`,
|
|
2757
|
-
execute: async (payload, context) => {
|
|
2758
|
-
const settledAt = new Date(now()).toISOString();
|
|
2759
|
-
const inviteId = asCallId(payload.invite_id);
|
|
2760
|
-
const pending = await options.repository.pendingInvites(asUserId(String(context.actorId)));
|
|
2761
|
-
const live = pending.find((invite) => invite.id === inviteId) ?? null;
|
|
2762
|
-
if (live === null) {
|
|
2763
|
-
return {
|
|
2764
|
-
kind: CALL_INVITE_KIND,
|
|
2765
|
-
messageId: context.messageId,
|
|
2766
|
-
actorId: context.actorId,
|
|
2767
|
-
outcome: "unavailable",
|
|
2768
|
-
label: "This call has ended",
|
|
2769
|
-
settledAt,
|
|
2770
|
-
detail: "The invite is no longer ringing \u2014 it was answered elsewhere, cancelled, or it timed out. The chips are replaced by the outcome rather than left clickable."
|
|
2771
|
-
};
|
|
2772
|
-
}
|
|
2773
|
-
if (context.choice === "decline") {
|
|
2774
|
-
await options.calls.decline(inviteId);
|
|
2775
|
-
return {
|
|
2776
|
-
kind: CALL_INVITE_KIND,
|
|
2777
|
-
messageId: context.messageId,
|
|
2778
|
-
actorId: context.actorId,
|
|
2779
|
-
outcome: "declined",
|
|
2780
|
-
label: "Declined",
|
|
2781
|
-
settledAt
|
|
2782
|
-
};
|
|
2783
|
-
}
|
|
2784
|
-
const accepted = await options.calls.accept(inviteId, live.mode);
|
|
2785
|
-
await options.onJoin(accepted);
|
|
2786
|
-
return {
|
|
2787
|
-
kind: CALL_INVITE_KIND,
|
|
2788
|
-
messageId: context.messageId,
|
|
2789
|
-
actorId: context.actorId,
|
|
2790
|
-
outcome: "applied",
|
|
2791
|
-
label: "Joined",
|
|
2792
|
-
settledAt
|
|
2793
|
-
};
|
|
2794
|
-
}
|
|
2795
|
-
};
|
|
2797
|
+
var LISTEN_ONLY_NOTICE = "You're attending without a microphone \u2014 others can't hear you";
|
|
2798
|
+
function isListenOnly(media) {
|
|
2799
|
+
return !media.microphone.enabled;
|
|
2796
2800
|
}
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
function meetingInviteAction(payload) {
|
|
2800
|
-
return { kind: MEETING_INVITE_KIND, version: MEETING_INVITE_VERSION, payload };
|
|
2801
|
-
}
|
|
2802
|
-
function createMeetingInviteHandler(options) {
|
|
2803
|
-
const now = options.now ?? (() => Date.now());
|
|
2804
|
-
return {
|
|
2805
|
-
kind: MEETING_INVITE_KIND,
|
|
2806
|
-
versions: [MEETING_INVITE_VERSION],
|
|
2807
|
-
// A DURABLE LINK NEVER EXPIRES (R3) — the chip stays useful after the
|
|
2808
|
-
// meeting too, because that is when people go looking for the notes.
|
|
2809
|
-
choices: () => [{ id: "open", label: "Open meeting", tone: "primary" }],
|
|
2810
|
-
summarize: (payload) => payload.scheduled_for === null ? payload.title : `${payload.title} \u2014 ${payload.scheduled_for}`,
|
|
2811
|
-
execute: async (payload, context) => {
|
|
2812
|
-
if (payload.slug.length === 0) {
|
|
2813
|
-
throw new MeetError(
|
|
2814
|
-
"invalid-response",
|
|
2815
|
-
"meet.meeting-invite: the payload carries no slug",
|
|
2816
|
-
"A meeting invitation without a durable slug cannot be opened. Fix the sender; never synthesize a link on the receiving side."
|
|
2817
|
-
);
|
|
2818
|
-
}
|
|
2819
|
-
await options.onOpen(payload);
|
|
2820
|
-
return {
|
|
2821
|
-
kind: MEETING_INVITE_KIND,
|
|
2822
|
-
messageId: context.messageId,
|
|
2823
|
-
actorId: context.actorId,
|
|
2824
|
-
outcome: "applied",
|
|
2825
|
-
label: "Opened",
|
|
2826
|
-
settledAt: new Date(now()).toISOString()
|
|
2827
|
-
};
|
|
2828
|
-
}
|
|
2829
|
-
};
|
|
2830
|
-
}
|
|
2831
|
-
|
|
2832
|
-
// src/core/layout.ts
|
|
2833
|
-
var DEFAULT_PAGE_SIZE = 12;
|
|
2834
|
-
function orderParticipants(participants, activeSpeakers) {
|
|
2835
|
-
const speakerRank = /* @__PURE__ */ new Map();
|
|
2836
|
-
activeSpeakers.forEach((identity, index) => speakerRank.set(identity, index));
|
|
2837
|
-
return [...participants].sort((a, b) => {
|
|
2838
|
-
const speakerA = speakerRank.get(a.identity) ?? Number.MAX_SAFE_INTEGER;
|
|
2839
|
-
const speakerB = speakerRank.get(b.identity) ?? Number.MAX_SAFE_INTEGER;
|
|
2840
|
-
if (speakerA !== speakerB) return speakerA - speakerB;
|
|
2841
|
-
if (a.handRaised !== b.handRaised) return a.handRaised ? -1 : 1;
|
|
2842
|
-
if (a.cameraEnabled !== b.cameraEnabled) return a.cameraEnabled ? -1 : 1;
|
|
2843
|
-
if (a.joinedAt !== b.joinedAt) return a.joinedAt < b.joinedAt ? -1 : 1;
|
|
2844
|
-
return a.identity < b.identity ? -1 : 1;
|
|
2845
|
-
});
|
|
2846
|
-
}
|
|
2847
|
-
function computeStage(args) {
|
|
2848
|
-
const pageSize = Math.max(1, args.pageSize ?? DEFAULT_PAGE_SIZE);
|
|
2849
|
-
const ordered = orderParticipants(args.participants, args.activeSpeakers);
|
|
2850
|
-
const sharer = ordered.find((participant) => participant.screenSharing) ?? null;
|
|
2851
|
-
const pinnedParticipant = args.pinned === null ? null : ordered.find((participant) => participant.identity === args.pinned) ?? null;
|
|
2852
|
-
const spotlit = args.spotlight == null ? null : ordered.find((participant) => participant.identity === args.spotlight) ?? null;
|
|
2853
|
-
const feature = sharer !== null ? sharer : pinnedParticipant !== null && args.layout !== "grid" ? pinnedParticipant : spotlit !== null ? spotlit : args.layout === "grid" ? null : ordered[0] ?? null;
|
|
2854
|
-
const rest = feature === null ? ordered : ordered.filter((participant) => participant.identity !== feature.identity);
|
|
2855
|
-
const pageCount = Math.max(1, Math.ceil(rest.length / pageSize));
|
|
2856
|
-
const page = Math.min(Math.max(0, args.page ?? 0), pageCount - 1);
|
|
2857
|
-
const tiles = rest.slice(page * pageSize, page * pageSize + pageSize);
|
|
2858
|
-
return {
|
|
2859
|
-
feature,
|
|
2860
|
-
tiles,
|
|
2861
|
-
overflow: rest.length - tiles.length,
|
|
2862
|
-
page,
|
|
2863
|
-
pageCount,
|
|
2864
|
-
presenting: sharer !== null
|
|
2865
|
-
};
|
|
2866
|
-
}
|
|
2867
|
-
function gridColumns(count2) {
|
|
2868
|
-
if (count2 <= 1) return 1;
|
|
2869
|
-
if (count2 <= 4) return 2;
|
|
2870
|
-
if (count2 <= 9) return 3;
|
|
2871
|
-
if (count2 <= 16) return 4;
|
|
2872
|
-
return 5;
|
|
2801
|
+
function listenOnlyNotice(media) {
|
|
2802
|
+
return isListenOnly(media) ? LISTEN_ONLY_NOTICE : null;
|
|
2873
2803
|
}
|
|
2874
2804
|
|
|
2875
|
-
// src/core/
|
|
2876
|
-
var
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2805
|
+
// src/core/store.ts
|
|
2806
|
+
var DEFAULT_EFFECTS = {
|
|
2807
|
+
noiseSuppression: true,
|
|
2808
|
+
background: "none",
|
|
2809
|
+
backgroundImage: null
|
|
2880
2810
|
};
|
|
2881
|
-
var
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
/**
|
|
2887
|
-
* WHO IS BEING TRANSCRIBED. The LiveKit JS client reports the PUBLISHER of a
|
|
2888
|
-
* text stream, and the publisher is always the note-taker — so a caption
|
|
2889
|
-
* carries the speaker in this attribute and falls back to the publisher only
|
|
2890
|
-
* when it is absent (which attributes the line to the agent, visibly, rather
|
|
2891
|
-
* than to the wrong person, silently).
|
|
2892
|
-
*/
|
|
2893
|
-
speakerIdentity: "lk.transcribed_participant_identity"
|
|
2811
|
+
var NO_GRANTS = {
|
|
2812
|
+
role: "participant",
|
|
2813
|
+
canRecord: false,
|
|
2814
|
+
canPublish: false,
|
|
2815
|
+
canSubscribe: false
|
|
2894
2816
|
};
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
name: name ?? "",
|
|
2929
|
-
metadata: participant.metadata ?? null,
|
|
2930
|
-
// ATTRIBUTES ARE PART OF THE SNAPSHOT, not read ad hoc later: the note-taker
|
|
2931
|
-
// announces the consent notice here (D10) and the roster is the only place
|
|
2932
|
-
// this package looks at a participant.
|
|
2933
|
-
attributes: participant.attributes ?? {},
|
|
2934
|
-
isLocal,
|
|
2935
|
-
micEnabled: participant.isMicrophoneEnabled,
|
|
2936
|
-
cameraEnabled: participant.isCameraEnabled,
|
|
2937
|
-
screenSharing: participant.isScreenShareEnabled
|
|
2938
|
-
};
|
|
2939
|
-
}
|
|
2940
|
-
var DEFAULT_AUDIO_PROCESSING = {
|
|
2941
|
-
noiseSuppression: true,
|
|
2942
|
-
echoCancellation: true,
|
|
2943
|
-
autoGainControl: true
|
|
2817
|
+
var EMPTY = {
|
|
2818
|
+
phase: "idle",
|
|
2819
|
+
meeting: null,
|
|
2820
|
+
localIdentity: null,
|
|
2821
|
+
grants: NO_GRANTS,
|
|
2822
|
+
participants: [],
|
|
2823
|
+
activeSpeakers: [],
|
|
2824
|
+
pinned: null,
|
|
2825
|
+
layout: "grid",
|
|
2826
|
+
lobby: [],
|
|
2827
|
+
locked: false,
|
|
2828
|
+
recording: "idle",
|
|
2829
|
+
recordingStartedBy: null,
|
|
2830
|
+
recordingSource: "local",
|
|
2831
|
+
recordingId: null,
|
|
2832
|
+
chat: [],
|
|
2833
|
+
chatLoaded: false,
|
|
2834
|
+
chatHistoryError: null,
|
|
2835
|
+
reactions: [],
|
|
2836
|
+
spotlight: null,
|
|
2837
|
+
moderationNotice: null,
|
|
2838
|
+
actionNotice: null,
|
|
2839
|
+
privateChatRequest: null,
|
|
2840
|
+
effects: DEFAULT_EFFECTS,
|
|
2841
|
+
transcript: [],
|
|
2842
|
+
notes: [],
|
|
2843
|
+
names: {},
|
|
2844
|
+
wrapUp: null,
|
|
2845
|
+
noteTakerRefusal: null,
|
|
2846
|
+
media: IDLE_MEDIA,
|
|
2847
|
+
failure: null,
|
|
2848
|
+
collab: EMPTY_COLLAB,
|
|
2849
|
+
breakoutRoomId: null
|
|
2944
2850
|
};
|
|
2945
|
-
function
|
|
2946
|
-
const
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
}
|
|
2851
|
+
function effectiveRole(snapshot) {
|
|
2852
|
+
const me = snapshot.participants.find(
|
|
2853
|
+
(participant) => participant.identity === snapshot.localIdentity
|
|
2854
|
+
);
|
|
2855
|
+
const fromRoster = me?.role ?? "participant";
|
|
2856
|
+
const fromToken = snapshot.grants.role;
|
|
2857
|
+
return ROLE_RANK[fromRoster] >= ROLE_RANK[fromToken] ? fromRoster : fromToken;
|
|
2953
2858
|
}
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
const processors = await import("@livekit/track-processors");
|
|
2964
|
-
const processor = effect.kind === "blur" ? processors.BackgroundBlur(effect.radius ?? 12) : processors.VirtualBackground(effect.imageUrl);
|
|
2965
|
-
await track.setProcessor(processor);
|
|
2966
|
-
} catch (error) {
|
|
2967
|
-
throw new MeetError(
|
|
2968
|
-
"media-unsupported",
|
|
2969
|
-
"The background effect could not start on this camera",
|
|
2970
|
-
"Background effects need a browser with WebGL2 (current Chrome, Edge, Firefox or Safari). Your video continues without the effect.",
|
|
2971
|
-
error
|
|
2972
|
-
);
|
|
2973
|
-
}
|
|
2974
|
-
}
|
|
2975
|
-
var encoder = new TextEncoder();
|
|
2976
|
-
var decoder = new TextDecoder();
|
|
2977
|
-
function createLiveKitDriver(handlers, options = {}) {
|
|
2978
|
-
let room = null;
|
|
2979
|
-
let projectRoster = null;
|
|
2980
|
-
let audioCapture = DEFAULT_AUDIO_PROCESSING;
|
|
2981
|
-
let videoEffect = { kind: "none" };
|
|
2982
|
-
async function load() {
|
|
2983
|
-
if (room !== null) return room;
|
|
2984
|
-
let module2;
|
|
2985
|
-
try {
|
|
2986
|
-
module2 = await import("livekit-client");
|
|
2987
|
-
} catch (error) {
|
|
2988
|
-
throw new MeetError(
|
|
2989
|
-
"misconfigured",
|
|
2990
|
-
"livekit-client could not be loaded",
|
|
2991
|
-
"`livekit-client` is a dependency of @ai-matrx/meet and is imported lazily at connect time. If this failed, the bundler excluded it \u2014 remove it from any `external`/`serverExternalPackages` list, or pass your own driver factory.",
|
|
2992
|
-
error
|
|
2993
|
-
);
|
|
2994
|
-
}
|
|
2995
|
-
const { Room: LiveKitRoom, RoomEvent, Track } = module2;
|
|
2996
|
-
const created = new LiveKitRoom({
|
|
2997
|
-
// R5: large meetings degrade gracefully instead of melting a laptop.
|
|
2998
|
-
adaptiveStream: true,
|
|
2999
|
-
dynacast: true,
|
|
3000
|
-
...options.roomOptions
|
|
3001
|
-
});
|
|
3002
|
-
const roster = () => {
|
|
3003
|
-
const local = snapshotParticipant(created.localParticipant, true);
|
|
3004
|
-
const remotes = [...created.remoteParticipants.values()].map(
|
|
3005
|
-
(participant) => snapshotParticipant(participant, false)
|
|
3006
|
-
);
|
|
3007
|
-
handlers.participants([local, ...remotes]);
|
|
3008
|
-
};
|
|
3009
|
-
projectRoster = roster;
|
|
3010
|
-
created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, roster).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, roster).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ParticipantNameChanged, roster).on(RoomEvent.ParticipantAttributesChanged, roster).on(RoomEvent.ParticipantMetadataChanged, roster).on(RoomEvent.RoomMetadataChanged, (metadata) => {
|
|
3011
|
-
handlers.roomMetadata?.(metadata ?? "");
|
|
3012
|
-
}).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
|
|
3013
|
-
handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
|
|
3014
|
-
}).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
|
|
3015
|
-
handlers.quality(participant.identity, mapQuality(quality));
|
|
3016
|
-
}).on(RoomEvent.ConnectionStateChanged, (state) => {
|
|
3017
|
-
handlers.connectionState(mapConnectionState(state));
|
|
3018
|
-
}).on(RoomEvent.DataReceived, (payload, participant, _kind, topic) => {
|
|
3019
|
-
handlers.data(
|
|
3020
|
-
topic ?? "",
|
|
3021
|
-
decoder.decode(payload),
|
|
3022
|
-
participant?.identity ?? null
|
|
3023
|
-
);
|
|
3024
|
-
}).on(RoomEvent.Disconnected, (reason) => {
|
|
3025
|
-
handlers.disconnected(reason === void 0 ? null : String(reason));
|
|
3026
|
-
});
|
|
3027
|
-
try {
|
|
3028
|
-
created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
|
|
3029
|
-
const attributes = { ...reader.info.attributes ?? {} };
|
|
3030
|
-
const segmentId = attributes[TRANSCRIPTION_ATTRIBUTES.segmentId] ?? reader.info.id;
|
|
3031
|
-
const trackId = attributes[TRANSCRIPTION_ATTRIBUTES.trackId] ?? null;
|
|
3032
|
-
const final = attributes[TRANSCRIPTION_ATTRIBUTES.final] === "true";
|
|
3033
|
-
const speakerIdentity = attributes[TRANSCRIPTION_ATTRIBUTES.speakerIdentity] ?? participantInfo.identity;
|
|
3034
|
-
const at = new Date(reader.info.timestamp || Date.now()).toISOString();
|
|
3035
|
-
void (async () => {
|
|
3036
|
-
let text3 = "";
|
|
3037
|
-
try {
|
|
3038
|
-
for await (const chunk of reader) {
|
|
3039
|
-
text3 += chunk;
|
|
3040
|
-
handlers.transcription({
|
|
3041
|
-
segmentId,
|
|
3042
|
-
speakerIdentity,
|
|
3043
|
-
publisherIdentity: participantInfo.identity,
|
|
3044
|
-
text: text3,
|
|
3045
|
-
final,
|
|
3046
|
-
trackId,
|
|
3047
|
-
at
|
|
3048
|
-
});
|
|
3049
|
-
}
|
|
3050
|
-
} catch (error) {
|
|
3051
|
-
handlers.data(
|
|
3052
|
-
TRANSCRIPTION_TOPIC,
|
|
3053
|
-
JSON.stringify({ error: String(error?.message ?? error) }),
|
|
3054
|
-
participantInfo.identity
|
|
3055
|
-
);
|
|
3056
|
-
}
|
|
3057
|
-
})();
|
|
3058
|
-
});
|
|
3059
|
-
} catch {
|
|
3060
|
-
}
|
|
3061
|
-
sources = {
|
|
3062
|
-
camera: Track.Source.Camera,
|
|
3063
|
-
microphone: Track.Source.Microphone,
|
|
3064
|
-
screen: Track.Source.ScreenShare
|
|
3065
|
-
};
|
|
3066
|
-
room = created;
|
|
3067
|
-
return created;
|
|
3068
|
-
}
|
|
3069
|
-
let sources = null;
|
|
3070
|
-
function requireRoom(operation) {
|
|
3071
|
-
if (room === null) {
|
|
3072
|
-
throw new MeetError(
|
|
3073
|
-
"transport",
|
|
3074
|
-
`${operation}: the room is not connected`,
|
|
3075
|
-
"Call connect() first. The engine never reaches this \u2014 it gates every media call on its own phase \u2014 so seeing it means a consumer drove the driver directly."
|
|
3076
|
-
);
|
|
3077
|
-
}
|
|
3078
|
-
return room;
|
|
2859
|
+
var REACTION_TTL_MS = 5e3;
|
|
2860
|
+
var CHAT_LIMIT = 200;
|
|
2861
|
+
function createMeetStore(initial = {}) {
|
|
2862
|
+
let state = { ...EMPTY, ...initial };
|
|
2863
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2864
|
+
function commit(next) {
|
|
2865
|
+
if (next === state) return;
|
|
2866
|
+
state = next;
|
|
2867
|
+
for (const listener of listeners) listener();
|
|
3079
2868
|
}
|
|
3080
2869
|
return {
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
} catch (error) {
|
|
3086
|
-
throw normalizeDeviceError(error, "meet.connect");
|
|
3087
|
-
}
|
|
3088
|
-
projectRoster?.();
|
|
3089
|
-
handlers.roomMetadata?.(target.metadata ?? "");
|
|
2870
|
+
snapshot: () => state,
|
|
2871
|
+
subscribe(listener) {
|
|
2872
|
+
listeners.add(listener);
|
|
2873
|
+
return () => listeners.delete(listener);
|
|
3090
2874
|
},
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
const target = room;
|
|
3094
|
-
room = null;
|
|
3095
|
-
try {
|
|
3096
|
-
target.unregisterTextStreamHandler(TRANSCRIPTION_TOPIC);
|
|
3097
|
-
} catch {
|
|
3098
|
-
}
|
|
3099
|
-
projectRoster = null;
|
|
3100
|
-
await target.disconnect();
|
|
2875
|
+
patch(next) {
|
|
2876
|
+
commit({ ...state, ...next });
|
|
3101
2877
|
},
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
2878
|
+
setParticipants(participants) {
|
|
2879
|
+
const names = learnNames(state.names, participants);
|
|
2880
|
+
const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
|
|
2881
|
+
const handTimes = new Map(state.participants.map((p) => [p.identity, p.handRaisedAt ?? null]));
|
|
2882
|
+
const quality = new Map(state.participants.map((p) => [p.identity, p.quality]));
|
|
2883
|
+
commit({
|
|
2884
|
+
...state,
|
|
2885
|
+
names,
|
|
2886
|
+
participants: participants.map((participant) => ({
|
|
2887
|
+
...participant,
|
|
2888
|
+
// 🚨 THE ONE ROSTER RESOLUTION. Every surface that renders a person —
|
|
2889
|
+
// the tile, the People row, the header summary, every Pin/Mute label —
|
|
2890
|
+
// reads `participant.displayName`, so this is the single place a raw
|
|
2891
|
+
// identity could reach a screen, and it is the place that asks the
|
|
2892
|
+
// name book. `roster-names.test.ts` fails if a token ever gets past it.
|
|
2893
|
+
displayName: displayNameFor(names, participant.identity, participant.displayName),
|
|
2894
|
+
handRaised: hands.get(participant.identity) ?? participant.handRaised,
|
|
2895
|
+
handRaisedAt: handTimes.get(participant.identity) ?? participant.handRaisedAt ?? null,
|
|
2896
|
+
quality: participant.quality === "unknown" ? quality.get(participant.identity) ?? "unknown" : participant.quality
|
|
2897
|
+
}))
|
|
2898
|
+
});
|
|
3111
2899
|
},
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
2900
|
+
setSpeaking(identities) {
|
|
2901
|
+
const speaking = new Set(identities);
|
|
2902
|
+
commit({
|
|
2903
|
+
...state,
|
|
2904
|
+
activeSpeakers: identities,
|
|
2905
|
+
participants: state.participants.map(
|
|
2906
|
+
(participant) => participant.speaking === speaking.has(participant.identity) ? participant : { ...participant, speaking: speaking.has(participant.identity) }
|
|
2907
|
+
)
|
|
2908
|
+
});
|
|
3120
2909
|
},
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
2910
|
+
setQuality(identity, quality) {
|
|
2911
|
+
commit({
|
|
2912
|
+
...state,
|
|
2913
|
+
participants: state.participants.map(
|
|
2914
|
+
(participant) => participant.identity === identity ? { ...participant, quality } : participant
|
|
2915
|
+
)
|
|
2916
|
+
});
|
|
3127
2917
|
},
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
2918
|
+
setHand(identity, raised, at) {
|
|
2919
|
+
commit({
|
|
2920
|
+
...state,
|
|
2921
|
+
participants: state.participants.map(
|
|
2922
|
+
(participant) => participant.identity === identity ? {
|
|
2923
|
+
...participant,
|
|
2924
|
+
handRaised: raised,
|
|
2925
|
+
// A hand that is ALREADY up keeps its place in the queue when
|
|
2926
|
+
// it is re-announced (to a late joiner): the first time wins.
|
|
2927
|
+
handRaisedAt: raised ? participant.handRaised && participant.handRaisedAt != null ? participant.handRaisedAt : at ?? Date.now() : null
|
|
2928
|
+
} : participant
|
|
2929
|
+
)
|
|
2930
|
+
});
|
|
2931
|
+
},
|
|
2932
|
+
addChat(message) {
|
|
2933
|
+
if (state.chat.some((existing) => existing.id === message.id)) return;
|
|
2934
|
+
const chat = [...state.chat, message];
|
|
2935
|
+
const names = learnName(state.names, message.identity, message.displayName);
|
|
2936
|
+
commit({ ...state, chat: chat.slice(-CHAT_LIMIT), names });
|
|
2937
|
+
},
|
|
2938
|
+
loadChat(messages) {
|
|
2939
|
+
const known = new Set(state.chat.map((message) => message.id));
|
|
2940
|
+
const merged = [...messages.filter((message) => !known.has(message.id)), ...state.chat].sort(
|
|
2941
|
+
(a, b) => a.at < b.at ? -1 : a.at > b.at ? 1 : 0
|
|
2942
|
+
);
|
|
2943
|
+
const names = learnNames(
|
|
2944
|
+
state.names,
|
|
2945
|
+
messages.map((message) => ({ identity: message.identity, displayName: message.displayName }))
|
|
3134
2946
|
);
|
|
2947
|
+
commit({ ...state, chat: merged.slice(-CHAT_LIMIT), chatLoaded: true, chatHistoryError: null, names });
|
|
3135
2948
|
},
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
await track.restartTrack({
|
|
3146
|
-
...audioCapture,
|
|
3147
|
-
...target.getActiveDevice("audioinput") !== void 0 ? { deviceId: target.getActiveDevice("audioinput") } : {}
|
|
3148
|
-
});
|
|
3149
|
-
} catch (error) {
|
|
3150
|
-
throw normalizeDeviceError(error, "meet.setAudioProcessing");
|
|
2949
|
+
settleChat(localId, next) {
|
|
2950
|
+
const index = state.chat.findIndex((message) => message.id === localId);
|
|
2951
|
+
if (index === -1) return;
|
|
2952
|
+
const current = state.chat[index];
|
|
2953
|
+
let replacement;
|
|
2954
|
+
if ("failed" in next && typeof next.failed === "string" && !("id" in next)) {
|
|
2955
|
+
replacement = { ...current, failed: next.failed };
|
|
2956
|
+
} else {
|
|
2957
|
+
replacement = next;
|
|
3151
2958
|
}
|
|
2959
|
+
const duplicate = replacement.id !== localId && state.chat.some((message) => message.id === replacement.id);
|
|
2960
|
+
const chat = duplicate ? state.chat.filter((message) => message.id !== localId) : state.chat.map((message, position) => position === index ? replacement : message);
|
|
2961
|
+
commit({ ...state, chat });
|
|
3152
2962
|
},
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
const target = room;
|
|
3156
|
-
if (target === null) return;
|
|
3157
|
-
await applyVideoEffect(target, effect);
|
|
3158
|
-
},
|
|
3159
|
-
supportsVideoEffects() {
|
|
3160
|
-
return videoEffectsSupported();
|
|
2963
|
+
addReaction(reaction) {
|
|
2964
|
+
commit({ ...state, reactions: [...state.reactions, reaction] });
|
|
3161
2965
|
},
|
|
3162
|
-
|
|
3163
|
-
const
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
if (participant === void 0 || source === void 0) return () => void 0;
|
|
3167
|
-
const publication = participant.getTrackPublication(source);
|
|
3168
|
-
const track = publication?.track;
|
|
3169
|
-
if (track === void 0) return () => void 0;
|
|
3170
|
-
track.attach(element);
|
|
3171
|
-
return () => {
|
|
3172
|
-
track.detach(element);
|
|
3173
|
-
};
|
|
2966
|
+
expireReactions(now) {
|
|
2967
|
+
const live = state.reactions.filter((reaction) => now - reaction.at < REACTION_TTL_MS);
|
|
2968
|
+
if (live.length === state.reactions.length) return;
|
|
2969
|
+
commit({ ...state, reactions: live });
|
|
3174
2970
|
},
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
2971
|
+
setRecording(next, by, source, recordingId = null) {
|
|
2972
|
+
if (state.recordingSource === "record" && source !== "record") {
|
|
2973
|
+
const aboutTheSameRecording = source === "room" && (recordingId === null || state.recordingId === null || recordingId === state.recordingId);
|
|
2974
|
+
if (aboutTheSameRecording || isRecordingInFlight(state.recording)) return;
|
|
3179
2975
|
}
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
videoinput: target.getActiveDevice("videoinput") ?? null,
|
|
3183
|
-
audiooutput: target.getActiveDevice("audiooutput") ?? null
|
|
3184
|
-
};
|
|
3185
|
-
},
|
|
3186
|
-
async switchDevice(kind, deviceId) {
|
|
3187
|
-
try {
|
|
3188
|
-
await requireRoom("switchDevice").switchActiveDevice(kind, deviceId);
|
|
3189
|
-
} catch (error) {
|
|
3190
|
-
throw normalizeDeviceError(error, "meet.switchDevice");
|
|
2976
|
+
if (state.recording === next && state.recordingStartedBy === by && state.recordingSource === source && state.recordingId === (recordingId ?? state.recordingId)) {
|
|
2977
|
+
return;
|
|
3191
2978
|
}
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
return String(identity).startsWith("guest:");
|
|
3199
|
-
}
|
|
3200
|
-
function isAgentIdentity(identity) {
|
|
3201
|
-
return String(identity).startsWith("agent:");
|
|
3202
|
-
}
|
|
3203
|
-
var NOTE_TAKER_PRODUCT_NAME = "Matrx note-taker";
|
|
3204
|
-
var AGENT_PRODUCT_NAME = "Matrx agent";
|
|
3205
|
-
var UNNAMED_GUEST = "Unnamed guest";
|
|
3206
|
-
var UNNAMED_PARTICIPANT = "Unnamed participant";
|
|
3207
|
-
var PLACEHOLDER_NAMES = [
|
|
3208
|
-
NOTE_TAKER_PRODUCT_NAME,
|
|
3209
|
-
AGENT_PRODUCT_NAME,
|
|
3210
|
-
UNNAMED_GUEST,
|
|
3211
|
-
UNNAMED_PARTICIPANT
|
|
3212
|
-
];
|
|
3213
|
-
function isPlaceholderName(name) {
|
|
3214
|
-
return typeof name === "string" && PLACEHOLDER_NAMES.includes(name.trim());
|
|
3215
|
-
}
|
|
3216
|
-
function placeholderNameFor(identity) {
|
|
3217
|
-
const key = String(identity);
|
|
3218
|
-
if (isAgentIdentity(key)) {
|
|
3219
|
-
return key.startsWith("agent:note_taker:") || key.startsWith("agent:note-taker") ? NOTE_TAKER_PRODUCT_NAME : AGENT_PRODUCT_NAME;
|
|
3220
|
-
}
|
|
3221
|
-
return isGuestIdentity(key) ? UNNAMED_GUEST : UNNAMED_PARTICIPANT;
|
|
3222
|
-
}
|
|
3223
|
-
function isRealName(identity, name) {
|
|
3224
|
-
if (typeof name !== "string") return false;
|
|
3225
|
-
const trimmed = name.trim();
|
|
3226
|
-
if (trimmed.length === 0) return false;
|
|
3227
|
-
if (trimmed === String(identity)) return false;
|
|
3228
|
-
if (isPlaceholderName(trimmed)) return false;
|
|
3229
|
-
return true;
|
|
3230
|
-
}
|
|
3231
|
-
function learnName(book, identity, name) {
|
|
3232
|
-
const key = String(identity);
|
|
3233
|
-
if (key.length === 0) return book;
|
|
3234
|
-
if (!isRealName(key, name)) return book;
|
|
3235
|
-
const learned = name.trim();
|
|
3236
|
-
if (book[key] === learned) return book;
|
|
3237
|
-
return { ...book, [key]: learned };
|
|
3238
|
-
}
|
|
3239
|
-
function learnNames(book, entries) {
|
|
3240
|
-
let next = book;
|
|
3241
|
-
for (const entry of entries) next = learnName(next, entry.identity, entry.displayName);
|
|
3242
|
-
return next;
|
|
3243
|
-
}
|
|
3244
|
-
function displayNameFor(book, identity, preferred) {
|
|
3245
|
-
const key = String(identity);
|
|
3246
|
-
if (isRealName(key, preferred)) return preferred.trim();
|
|
3247
|
-
const known = book[key];
|
|
3248
|
-
if (known !== void 0) return known;
|
|
3249
|
-
return placeholderNameFor(key);
|
|
3250
|
-
}
|
|
3251
|
-
|
|
3252
|
-
// src/core/recording.ts
|
|
3253
|
-
function createRecordingController(options) {
|
|
3254
|
-
async function post(route, body) {
|
|
3255
|
-
let response;
|
|
3256
|
-
try {
|
|
3257
|
-
response = await options.api.request({
|
|
3258
|
-
path: route,
|
|
3259
|
-
method: "POST",
|
|
3260
|
-
// A GUEST CAN NEVER START OR STOP A RECORDING (R5) — refused here,
|
|
3261
|
-
// before the wire, with the same sentence the hidden control implies.
|
|
3262
|
-
requireAuth: true,
|
|
3263
|
-
operation: "meet.recording",
|
|
3264
|
-
body
|
|
2979
|
+
commit({
|
|
2980
|
+
...state,
|
|
2981
|
+
recording: next,
|
|
2982
|
+
recordingStartedBy: by,
|
|
2983
|
+
recordingSource: source,
|
|
2984
|
+
recordingId: recordingId ?? state.recordingId
|
|
3265
2985
|
});
|
|
3266
|
-
} catch (error) {
|
|
3267
|
-
if (error.code === "session-unavailable") {
|
|
3268
|
-
throw new MeetError(
|
|
3269
|
-
"forbidden",
|
|
3270
|
-
"meet.recording: recording requires an authenticated host",
|
|
3271
|
-
"A guest can never start or stop a recording (R5). The control is hidden for guests."
|
|
3272
|
-
);
|
|
3273
|
-
}
|
|
3274
|
-
throw error;
|
|
3275
|
-
}
|
|
3276
|
-
if (response.status === 403) {
|
|
3277
|
-
throw new MeetError(
|
|
3278
|
-
"forbidden",
|
|
3279
|
-
"meet.recording: the server refused this recording action",
|
|
3280
|
-
"The actor is not a host of this meeting, or the org policy is `disabled`. The server is the authority; do not re-decide it on the client."
|
|
3281
|
-
);
|
|
3282
|
-
}
|
|
3283
|
-
if (!response.ok) {
|
|
3284
|
-
throw new MeetError(
|
|
3285
|
-
"transport",
|
|
3286
|
-
`meet.recording: the server returned ${response.status}`,
|
|
3287
|
-
"Egress could not be controlled. The recording state stays where it was rather than showing a recording indicator for a recording that is not running."
|
|
3288
|
-
);
|
|
3289
|
-
}
|
|
3290
|
-
}
|
|
3291
|
-
return {
|
|
3292
|
-
canStart(meeting, isHost) {
|
|
3293
|
-
if (meeting === null) return false;
|
|
3294
|
-
if (meeting.recordingPolicy === "disabled") return false;
|
|
3295
|
-
if (meeting.recordingPolicy === "always-on") return false;
|
|
3296
|
-
return isHost;
|
|
3297
2986
|
},
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
meeting_id: meetingId,
|
|
3303
|
-
room_name: roomName
|
|
3304
|
-
});
|
|
3305
|
-
} catch (error) {
|
|
3306
|
-
options.store.setRecording("idle", null, "local");
|
|
3307
|
-
throw error;
|
|
3308
|
-
}
|
|
3309
|
-
options.announce?.("starting", by);
|
|
2987
|
+
upsertLobby(entry) {
|
|
2988
|
+
const index = state.lobby.findIndex((existing) => existing.identity === entry.identity);
|
|
2989
|
+
const lobby = index === -1 ? [...state.lobby, entry] : state.lobby.map((existing, position) => position === index ? entry : existing);
|
|
2990
|
+
commit({ ...state, lobby });
|
|
3310
2991
|
},
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
2992
|
+
setAdmission(identity, admissionState, by) {
|
|
2993
|
+
commit({
|
|
2994
|
+
...state,
|
|
2995
|
+
lobby: state.lobby.map(
|
|
2996
|
+
(entry) => entry.identity === identity ? {
|
|
2997
|
+
...entry,
|
|
2998
|
+
state: admissionState,
|
|
2999
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3000
|
+
decidedBy: by
|
|
3001
|
+
} : entry
|
|
3002
|
+
)
|
|
3003
|
+
});
|
|
3004
|
+
},
|
|
3005
|
+
learn(entries) {
|
|
3006
|
+
const names = learnNames(state.names, entries);
|
|
3007
|
+
if (names === state.names) return;
|
|
3008
|
+
commit({ ...state, names });
|
|
3009
|
+
},
|
|
3010
|
+
ingestCaption(segment) {
|
|
3011
|
+
const names = learnName(state.names, segment.identity, segment.speaker);
|
|
3012
|
+
const transcript = mergeCaption(state.transcript, segment);
|
|
3013
|
+
if (transcript === state.transcript && names === state.names) return;
|
|
3014
|
+
commit({ ...state, transcript, names });
|
|
3015
|
+
},
|
|
3016
|
+
ingestRecord(segment) {
|
|
3017
|
+
const names = learnName(state.names, segment.identity, segment.speaker);
|
|
3018
|
+
const transcript = mergeRecord(state.transcript, segment);
|
|
3019
|
+
if (transcript === state.transcript && names === state.names) return;
|
|
3020
|
+
commit({ ...state, transcript, names });
|
|
3021
|
+
},
|
|
3022
|
+
upsertNote(note) {
|
|
3023
|
+
const index = state.notes.findIndex((existing2) => existing2.id === note.id);
|
|
3024
|
+
if (index === -1) {
|
|
3025
|
+
commit({ ...state, notes: [...state.notes, note] });
|
|
3026
|
+
return;
|
|
3322
3027
|
}
|
|
3323
|
-
|
|
3028
|
+
const existing = state.notes[index];
|
|
3029
|
+
if (existing !== void 0 && existing.text === note.text && existing.kind === note.kind && existing.supersededBy === note.supersededBy && existing.source === note.source) {
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
if (existing !== void 0 && existing.source === "record" && note.source === "room") {
|
|
3033
|
+
return;
|
|
3034
|
+
}
|
|
3035
|
+
commit({
|
|
3036
|
+
...state,
|
|
3037
|
+
notes: state.notes.map((entry, position) => position === index ? note : entry)
|
|
3038
|
+
});
|
|
3324
3039
|
},
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
if (
|
|
3328
|
-
|
|
3040
|
+
announceWrapUp(announcement) {
|
|
3041
|
+
const current = state.wrapUp;
|
|
3042
|
+
if (current !== null && current.at === announcement.at) return;
|
|
3043
|
+
commit({ ...state, wrapUp: announcement });
|
|
3044
|
+
},
|
|
3045
|
+
setLocalTrack(kind, next) {
|
|
3046
|
+
const current = state.media[kind];
|
|
3047
|
+
if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.message === next.unavailable?.message) {
|
|
3048
|
+
return;
|
|
3329
3049
|
}
|
|
3330
|
-
|
|
3050
|
+
commit({ ...state, media: { ...state.media, [kind]: next } });
|
|
3331
3051
|
},
|
|
3332
|
-
|
|
3333
|
-
|
|
3052
|
+
reset() {
|
|
3053
|
+
commit({ ...EMPTY, effects: state.effects });
|
|
3334
3054
|
}
|
|
3335
3055
|
};
|
|
3336
3056
|
}
|
|
3337
|
-
|
|
3057
|
+
|
|
3058
|
+
// src/core/room-events.ts
|
|
3059
|
+
var RECORDING_STATES = [
|
|
3060
|
+
"idle",
|
|
3338
3061
|
"starting",
|
|
3339
3062
|
"recording",
|
|
3340
|
-
"stopping"
|
|
3063
|
+
"stopping",
|
|
3064
|
+
"processing",
|
|
3065
|
+
"available",
|
|
3066
|
+
"expired",
|
|
3067
|
+
"failed"
|
|
3341
3068
|
];
|
|
3342
|
-
|
|
3343
|
-
|
|
3069
|
+
var ROOM_EVENT_PREFIX = "matrx.meet.";
|
|
3070
|
+
var ROOM_EVENTS = {
|
|
3071
|
+
/** One durable `meet_notes` row was written. Payload `{ note: <row> }`. */
|
|
3072
|
+
note: "matrx.meet.note",
|
|
3073
|
+
/** `meet_meetings.metadata.note_taker` changed. Payload `{ meeting_id, note_taker }`. */
|
|
3074
|
+
noteTaker: "matrx.meet.note_taker",
|
|
3075
|
+
/** A wrap-up pass finished. Payload `{ meeting_id, note_ids, segment_count }`. */
|
|
3076
|
+
wrapUp: "matrx.meet.wrap_up",
|
|
3077
|
+
/** The meeting ended for everyone. Payload `{ meeting_id, ended_at, trigger }`. */
|
|
3078
|
+
ended: "matrx.meet.ended",
|
|
3079
|
+
/**
|
|
3080
|
+
* A `meet_recordings` row changed state. Payload
|
|
3081
|
+
* `{ meeting_id, recording: { id, state, started_by } }`.
|
|
3082
|
+
*
|
|
3083
|
+
* 🚨 THIS IS THE ONLY WAY A GUEST LEARNS A RECORDING IS RUNNING. PreJoin
|
|
3084
|
+
* promises every joiner an indicator, and `anon` holds no SELECT grant on
|
|
3085
|
+
* `meet_recordings` (MRI-A10 settled that widening it is forbidden), so
|
|
3086
|
+
* without this packet the promise was a sentence with nothing behind it: on
|
|
3087
|
+
* production 2026-09-09 the guest lane showed no indicator at all while the
|
|
3088
|
+
* host recorded (MRI-C8).
|
|
3089
|
+
*/
|
|
3090
|
+
recording: "matrx.meet.recording",
|
|
3091
|
+
/**
|
|
3092
|
+
* A chat message to everyone was SAVED (Meet wave 2). Payload
|
|
3093
|
+
* `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
|
|
3094
|
+
*/
|
|
3095
|
+
chat: "matrx.meet.chat",
|
|
3096
|
+
/**
|
|
3097
|
+
* A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`:
|
|
3098
|
+
* `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the
|
|
3099
|
+
* person concerned; `lower-hands {identities: [...] | "*"}` · `role {identity,
|
|
3100
|
+
* role}` — to everyone.
|
|
3101
|
+
*/
|
|
3102
|
+
moderation: "matrx.meet.moderation",
|
|
3103
|
+
/**
|
|
3104
|
+
* A meeting ROOM ITEM changed (Meet wave 5): a poll, a Q&A question, a whiteboard
|
|
3105
|
+
* stroke or the breakout plan. Payload `{ meeting_id, item }` · `{ removed: [ids] }` ·
|
|
3106
|
+
* `{ hidden: [ids] }` · `{ cleared: "whiteboard" }` · `{ broadcast }` · `{ item, move }`.
|
|
3107
|
+
* A host's fuller copy of a poll or question carries `audience: "hosts"`.
|
|
3108
|
+
*/
|
|
3109
|
+
collab: "matrx.meet.collab"
|
|
3110
|
+
};
|
|
3111
|
+
function isRoomEventTopic(topic) {
|
|
3112
|
+
return topic.startsWith(ROOM_EVENT_PREFIX);
|
|
3344
3113
|
}
|
|
3345
|
-
function
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3114
|
+
function object(value) {
|
|
3115
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
3116
|
+
}
|
|
3117
|
+
function text2(value) {
|
|
3118
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
3119
|
+
}
|
|
3120
|
+
var HANDLED = { handled: true };
|
|
3121
|
+
function ignored(reason) {
|
|
3122
|
+
return { handled: false, ignored: reason };
|
|
3123
|
+
}
|
|
3124
|
+
function applyRoomEvent(args) {
|
|
3125
|
+
const { store, topic } = args;
|
|
3126
|
+
if (!isRoomEventTopic(topic)) {
|
|
3127
|
+
return ignored(`${topic} is not a Meet room event`);
|
|
3128
|
+
}
|
|
3129
|
+
const payload = object(args.payload);
|
|
3130
|
+
if (payload === null) {
|
|
3131
|
+
return ignored(`${topic} carried no object payload`);
|
|
3132
|
+
}
|
|
3133
|
+
const now = args.now ?? (() => Date.now());
|
|
3134
|
+
switch (topic) {
|
|
3135
|
+
case ROOM_EVENTS.note:
|
|
3136
|
+
return applyNote(store, payload);
|
|
3137
|
+
case ROOM_EVENTS.noteTaker:
|
|
3138
|
+
return applyNoteTaker(store, payload);
|
|
3139
|
+
case ROOM_EVENTS.wrapUp:
|
|
3140
|
+
return applyWrapUp(store, payload, now);
|
|
3141
|
+
case ROOM_EVENTS.ended:
|
|
3142
|
+
return applyEnded(store, payload);
|
|
3143
|
+
case ROOM_EVENTS.recording:
|
|
3144
|
+
return applyRecording(store, payload);
|
|
3145
|
+
case ROOM_EVENTS.chat:
|
|
3146
|
+
return applyChat(store, payload);
|
|
3147
|
+
case ROOM_EVENTS.moderation:
|
|
3148
|
+
return applyModeration(store, payload, now);
|
|
3149
|
+
case ROOM_EVENTS.collab:
|
|
3150
|
+
return applyCollab(store, payload, now);
|
|
3361
3151
|
default:
|
|
3362
|
-
return
|
|
3152
|
+
return ignored(`${topic} is not known to this version of @ai-matrx/meet`);
|
|
3363
3153
|
}
|
|
3364
3154
|
}
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3155
|
+
function applyCollab(store, payload, now) {
|
|
3156
|
+
const snapshot = store.snapshot();
|
|
3157
|
+
const meeting = snapshot.meeting;
|
|
3158
|
+
if (meeting !== null && text2(payload["meeting_id"]) !== null && payload["meeting_id"] !== meeting.id) {
|
|
3159
|
+
return ignored("a collab event for a different meeting than this room is showing");
|
|
3160
|
+
}
|
|
3161
|
+
const result = applyCollabPacket(snapshot.collab, payload, {
|
|
3162
|
+
localIdentity: snapshot.localIdentity,
|
|
3163
|
+
isHost: ROLE_RANK[effectiveRole(snapshot)] >= ROLE_RANK.cohost,
|
|
3164
|
+
now: now()
|
|
3165
|
+
});
|
|
3166
|
+
if (result.next === null) return ignored(result.dropped ?? "a collab packet that changed nothing");
|
|
3167
|
+
store.patch({ collab: result.next });
|
|
3168
|
+
return HANDLED;
|
|
3374
3169
|
}
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
return
|
|
3170
|
+
function applyNote(store, payload) {
|
|
3171
|
+
const row = object(payload["note"]);
|
|
3172
|
+
if (row === null) return ignored("a note event with no `note` row");
|
|
3173
|
+
if (text2(row["id"]) === null || text2(row["meeting_id"]) === null) {
|
|
3174
|
+
return ignored("a note row with no id or meeting_id");
|
|
3175
|
+
}
|
|
3176
|
+
const meeting = store.snapshot().meeting;
|
|
3177
|
+
if (meeting !== null && row["meeting_id"] !== meeting.id) {
|
|
3178
|
+
return ignored("a note for a different meeting than this room is showing");
|
|
3179
|
+
}
|
|
3180
|
+
try {
|
|
3181
|
+
store.upsertNote({ ...projectNote(row), source: "room" });
|
|
3182
|
+
} catch (error) {
|
|
3183
|
+
return ignored(`a note row this client could not read: ${String(error)}`);
|
|
3184
|
+
}
|
|
3185
|
+
return HANDLED;
|
|
3378
3186
|
}
|
|
3379
|
-
function
|
|
3380
|
-
|
|
3187
|
+
function applyNoteTaker(store, payload) {
|
|
3188
|
+
const noteTaker = object(payload["note_taker"]);
|
|
3189
|
+
if (noteTaker === null) return ignored("a note-taker event with no state");
|
|
3190
|
+
const meeting = store.snapshot().meeting;
|
|
3191
|
+
if (meeting === null) {
|
|
3192
|
+
return ignored("a note-taker state with no meeting record to attach it to");
|
|
3193
|
+
}
|
|
3194
|
+
const meetingId = text2(payload["meeting_id"]);
|
|
3195
|
+
if (meetingId !== null && meetingId !== meeting.id) {
|
|
3196
|
+
return ignored("a note-taker state for a different meeting");
|
|
3197
|
+
}
|
|
3198
|
+
const next = {
|
|
3199
|
+
...meeting,
|
|
3200
|
+
metadata: { ...meeting.metadata, note_taker: noteTaker }
|
|
3201
|
+
};
|
|
3202
|
+
store.patch({ meeting: next });
|
|
3203
|
+
return HANDLED;
|
|
3381
3204
|
}
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
}
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
transcript: [],
|
|
3420
|
-
notes: [],
|
|
3421
|
-
names: {},
|
|
3422
|
-
wrapUp: null,
|
|
3423
|
-
noteTakerRefusal: null,
|
|
3424
|
-
media: IDLE_MEDIA,
|
|
3425
|
-
failure: null
|
|
3426
|
-
};
|
|
3427
|
-
function effectiveRole(snapshot) {
|
|
3428
|
-
const me = snapshot.participants.find(
|
|
3429
|
-
(participant) => participant.identity === snapshot.localIdentity
|
|
3205
|
+
function applyWrapUp(store, payload, now) {
|
|
3206
|
+
const meetingId = text2(payload["meeting_id"]);
|
|
3207
|
+
if (meetingId === null) return ignored("a wrap-up event with no meeting id");
|
|
3208
|
+
const meeting = store.snapshot().meeting;
|
|
3209
|
+
if (meeting !== null && meetingId !== meeting.id) {
|
|
3210
|
+
return ignored("a wrap-up for a different meeting");
|
|
3211
|
+
}
|
|
3212
|
+
const rawIds = payload["note_ids"];
|
|
3213
|
+
const noteIds = Array.isArray(rawIds) ? rawIds.filter((value) => typeof value === "string") : [];
|
|
3214
|
+
const segmentCount = payload["segment_count"];
|
|
3215
|
+
const announcement = {
|
|
3216
|
+
meetingId: asMeetingId(meetingId),
|
|
3217
|
+
noteIds,
|
|
3218
|
+
segmentCount: typeof segmentCount === "number" ? segmentCount : 0,
|
|
3219
|
+
at: new Date(now()).toISOString()
|
|
3220
|
+
};
|
|
3221
|
+
store.announceWrapUp(announcement);
|
|
3222
|
+
return HANDLED;
|
|
3223
|
+
}
|
|
3224
|
+
function applyRecording(store, payload) {
|
|
3225
|
+
const row = object(payload["recording"]);
|
|
3226
|
+
if (row === null) return ignored("a recording event with no `recording` row");
|
|
3227
|
+
const meetingId = text2(payload["meeting_id"]);
|
|
3228
|
+
const meeting = store.snapshot().meeting;
|
|
3229
|
+
if (meeting !== null && meetingId !== null && meetingId !== meeting.id) {
|
|
3230
|
+
return ignored("a recording state for a different meeting than this room is showing");
|
|
3231
|
+
}
|
|
3232
|
+
const state = text2(row["state"]);
|
|
3233
|
+
if (state === null || !RECORDING_STATES.includes(state)) {
|
|
3234
|
+
return ignored(`a recording state this version does not know: ${String(row["state"])}`);
|
|
3235
|
+
}
|
|
3236
|
+
const by = text2(row["started_by"]);
|
|
3237
|
+
store.setRecording(
|
|
3238
|
+
state,
|
|
3239
|
+
by === null ? null : by,
|
|
3240
|
+
"room",
|
|
3241
|
+
text2(row["id"])
|
|
3430
3242
|
);
|
|
3431
|
-
|
|
3432
|
-
const fromToken = snapshot.grants.role;
|
|
3433
|
-
return ROLE_RANK[fromRoster] >= ROLE_RANK[fromToken] ? fromRoster : fromToken;
|
|
3243
|
+
return HANDLED;
|
|
3434
3244
|
}
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
if (next === state) return;
|
|
3442
|
-
state = next;
|
|
3443
|
-
for (const listener of listeners) listener();
|
|
3245
|
+
function applyEnded(store, payload) {
|
|
3246
|
+
const meetingId = text2(payload["meeting_id"]);
|
|
3247
|
+
const meeting = store.snapshot().meeting;
|
|
3248
|
+
if (meeting === null) return ignored("an ended event with no meeting record");
|
|
3249
|
+
if (meetingId !== null && meetingId !== meeting.id) {
|
|
3250
|
+
return ignored("an ended event for a different meeting");
|
|
3444
3251
|
}
|
|
3252
|
+
const endedAt = text2(payload["ended_at"]) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3253
|
+
if (meeting.endedAt === endedAt) return HANDLED;
|
|
3254
|
+
store.patch({ meeting: { ...meeting, endedAt } });
|
|
3255
|
+
return HANDLED;
|
|
3256
|
+
}
|
|
3257
|
+
function chatFromRow(row) {
|
|
3258
|
+
const id = text2(row["id"]);
|
|
3259
|
+
const identity = text2(row["sender_identity"]);
|
|
3260
|
+
const body = text2(row["body"]);
|
|
3261
|
+
if (id === null || identity === null || body === null) return null;
|
|
3445
3262
|
return {
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3263
|
+
id,
|
|
3264
|
+
identity: asParticipantIdentity(identity),
|
|
3265
|
+
displayName: text2(row["sender_name"]) ?? "Participant",
|
|
3266
|
+
text: body,
|
|
3267
|
+
at: text2(row["created_at"]) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3268
|
+
// `false` only from a server whose chat table has not landed: the room heard
|
|
3269
|
+
// it, nobody will see it later — and the line says so.
|
|
3270
|
+
saved: row["saved"] !== false
|
|
3271
|
+
};
|
|
3272
|
+
}
|
|
3273
|
+
function applyChat(store, payload) {
|
|
3274
|
+
const row = object(payload["message"]);
|
|
3275
|
+
if (row === null) return ignored("a chat event with no `message` row");
|
|
3276
|
+
const meeting = store.snapshot().meeting;
|
|
3277
|
+
if (meeting !== null && text2(row["meeting_id"]) !== null && row["meeting_id"] !== meeting.id) {
|
|
3278
|
+
return ignored("a chat message for a different meeting");
|
|
3279
|
+
}
|
|
3280
|
+
const message = chatFromRow(row);
|
|
3281
|
+
if (message === null) return ignored("a chat row with no id, sender or body");
|
|
3282
|
+
store.addChat(message);
|
|
3283
|
+
return HANDLED;
|
|
3284
|
+
}
|
|
3285
|
+
function moderationText(kind, payload) {
|
|
3286
|
+
const by = text2(payload["by"]) ?? "The host";
|
|
3287
|
+
switch (kind) {
|
|
3288
|
+
case "muted": {
|
|
3289
|
+
const source = text2(payload["source"]) ?? "microphone";
|
|
3290
|
+
return source === "microphone" ? `${by} muted you.` : source === "camera" ? `${by} turned off your camera.` : `${by} stopped your screen share.`;
|
|
3291
|
+
}
|
|
3292
|
+
case "ask-unmute":
|
|
3293
|
+
return `${by} is asking you to unmute.`;
|
|
3294
|
+
case "removed":
|
|
3295
|
+
return `${by} removed you from this meeting.`;
|
|
3296
|
+
default:
|
|
3297
|
+
return null;
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
function applyModeration(store, payload, now) {
|
|
3301
|
+
const snapshot = store.snapshot();
|
|
3302
|
+
const meetingId = text2(payload["meeting_id"]);
|
|
3303
|
+
if (snapshot.meeting !== null && meetingId !== null && meetingId !== snapshot.meeting.id) {
|
|
3304
|
+
return ignored("a moderation event for a different meeting");
|
|
3305
|
+
}
|
|
3306
|
+
const kind = text2(payload["kind"]);
|
|
3307
|
+
switch (kind) {
|
|
3308
|
+
case "lower-hands": {
|
|
3309
|
+
const raw = payload["identities"];
|
|
3310
|
+
const everyone = raw === "*";
|
|
3311
|
+
const named = new Set(
|
|
3312
|
+
Array.isArray(raw) ? raw.filter((value) => typeof value === "string") : []
|
|
3313
|
+
);
|
|
3314
|
+
for (const participant of snapshot.participants) {
|
|
3315
|
+
if (participant.handRaised && (everyone || named.has(participant.identity))) {
|
|
3316
|
+
store.setHand(participant.identity, false);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
return HANDLED;
|
|
3320
|
+
}
|
|
3321
|
+
case "role": {
|
|
3322
|
+
const identity = text2(payload["identity"]);
|
|
3323
|
+
const role = text2(payload["role"]);
|
|
3324
|
+
if (identity === null || role === null || !ROLES.includes(role)) {
|
|
3325
|
+
return ignored("a role event with no identity or an unknown role");
|
|
3326
|
+
}
|
|
3327
|
+
if (identity === snapshot.localIdentity) {
|
|
3328
|
+
store.patch({
|
|
3329
|
+
grants: { ...snapshot.grants, role },
|
|
3330
|
+
moderationNotice: {
|
|
3331
|
+
kind: "role",
|
|
3332
|
+
text: role === "host" ? "You are now the host." : role === "cohost" ? "You are now a co-host." : "You are no longer a co-host.",
|
|
3333
|
+
at: now()
|
|
3334
|
+
}
|
|
3335
|
+
});
|
|
3336
|
+
}
|
|
3337
|
+
return HANDLED;
|
|
3338
|
+
}
|
|
3339
|
+
case "muted":
|
|
3340
|
+
case "ask-unmute":
|
|
3341
|
+
case "removed": {
|
|
3342
|
+
const sentence = moderationText(kind, payload);
|
|
3343
|
+
if (sentence === null) return ignored("a moderation event with no sentence");
|
|
3344
|
+
store.patch({ moderationNotice: { kind, text: sentence, at: now() } });
|
|
3345
|
+
return HANDLED;
|
|
3346
|
+
}
|
|
3347
|
+
default:
|
|
3348
|
+
return ignored(`a moderation kind this version does not know: ${String(kind)}`);
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
var ROLES = ["host", "cohost", "participant", "guest"];
|
|
3352
|
+
|
|
3353
|
+
// src/core/devices.ts
|
|
3354
|
+
var KINDS = ["audioinput", "videoinput", "audiooutput"];
|
|
3355
|
+
function isDeviceKind(value) {
|
|
3356
|
+
return value === "audioinput" || value === "videoinput" || value === "audiooutput";
|
|
3357
|
+
}
|
|
3358
|
+
function createWebDeviceStorage(key = "mx.meet.devices") {
|
|
3359
|
+
const store = globalThis.localStorage;
|
|
3360
|
+
if (store === void 0) return createMemoryDeviceStorage();
|
|
3361
|
+
return {
|
|
3362
|
+
durable: true,
|
|
3363
|
+
read() {
|
|
3364
|
+
try {
|
|
3365
|
+
const raw = store.getItem(key);
|
|
3366
|
+
if (raw === null) return {};
|
|
3367
|
+
const parsed = JSON.parse(raw);
|
|
3368
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
3369
|
+
} catch {
|
|
3370
|
+
return {};
|
|
3371
|
+
}
|
|
3493
3372
|
},
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3373
|
+
write(value) {
|
|
3374
|
+
try {
|
|
3375
|
+
store.setItem(key, JSON.stringify(value));
|
|
3376
|
+
} catch {
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
};
|
|
3380
|
+
}
|
|
3381
|
+
function createMemoryDeviceStorage() {
|
|
3382
|
+
let value = {};
|
|
3383
|
+
return {
|
|
3384
|
+
durable: false,
|
|
3385
|
+
read: () => value,
|
|
3386
|
+
write: (next) => {
|
|
3387
|
+
value = next;
|
|
3388
|
+
}
|
|
3389
|
+
};
|
|
3390
|
+
}
|
|
3391
|
+
function createDeviceManager(options) {
|
|
3392
|
+
const storage = options.storage ?? createMemoryDeviceStorage();
|
|
3393
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
3394
|
+
const stored = storage.read();
|
|
3395
|
+
let devices = {
|
|
3396
|
+
audioinput: [],
|
|
3397
|
+
videoinput: [],
|
|
3398
|
+
audiooutput: []
|
|
3399
|
+
};
|
|
3400
|
+
let selected = {
|
|
3401
|
+
audioinput: stored["audioinput"] ?? null,
|
|
3402
|
+
videoinput: stored["videoinput"] ?? null,
|
|
3403
|
+
audiooutput: stored["audiooutput"] ?? null
|
|
3404
|
+
};
|
|
3405
|
+
let labelsHidden = true;
|
|
3406
|
+
let permission = "unknown";
|
|
3407
|
+
let fallbackNotice = null;
|
|
3408
|
+
let current = {
|
|
3409
|
+
devices,
|
|
3410
|
+
selected,
|
|
3411
|
+
labelsHidden,
|
|
3412
|
+
permission,
|
|
3413
|
+
fallbackNotice
|
|
3414
|
+
};
|
|
3415
|
+
function snapshot() {
|
|
3416
|
+
return current;
|
|
3417
|
+
}
|
|
3418
|
+
function commit() {
|
|
3419
|
+
current = { devices, selected, labelsHidden, permission, fallbackNotice };
|
|
3420
|
+
return current;
|
|
3421
|
+
}
|
|
3422
|
+
function emit() {
|
|
3423
|
+
const next = commit();
|
|
3424
|
+
options.onChange?.(next);
|
|
3425
|
+
for (const listener of listeners) listener(next);
|
|
3426
|
+
return next;
|
|
3427
|
+
}
|
|
3428
|
+
async function enumerate() {
|
|
3429
|
+
let raw;
|
|
3430
|
+
try {
|
|
3431
|
+
raw = await options.media.enumerateDevices();
|
|
3432
|
+
} catch (error) {
|
|
3433
|
+
throw normalizeDeviceError(error, "meet.enumerateDevices");
|
|
3434
|
+
}
|
|
3435
|
+
const grouped = {
|
|
3436
|
+
audioinput: [],
|
|
3437
|
+
videoinput: [],
|
|
3438
|
+
audiooutput: []
|
|
3439
|
+
};
|
|
3440
|
+
let anyLabel = false;
|
|
3441
|
+
for (const device of raw) {
|
|
3442
|
+
if (!isDeviceKind(device.kind)) continue;
|
|
3443
|
+
if (device.label.length > 0) anyLabel = true;
|
|
3444
|
+
grouped[device.kind].push({
|
|
3445
|
+
deviceId: device.deviceId,
|
|
3446
|
+
kind: device.kind,
|
|
3447
|
+
label: device.label
|
|
3506
3448
|
});
|
|
3449
|
+
}
|
|
3450
|
+
devices = grouped;
|
|
3451
|
+
labelsHidden = !anyLabel && raw.length > 0;
|
|
3452
|
+
const lost = [];
|
|
3453
|
+
for (const kind of KINDS) {
|
|
3454
|
+
const current2 = selected[kind];
|
|
3455
|
+
if (current2 === null) continue;
|
|
3456
|
+
if (!grouped[kind].some((device) => device.deviceId === current2)) {
|
|
3457
|
+
selected = { ...selected, [kind]: null };
|
|
3458
|
+
lost.push(kind);
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
fallbackNotice = lost.length === 0 ? null : `A remembered ${lost.join(" and ")} device is no longer connected \u2014 using the system default. Pick another in the device menu.`;
|
|
3462
|
+
return emit();
|
|
3463
|
+
}
|
|
3464
|
+
const onDeviceChange = () => {
|
|
3465
|
+
void enumerate();
|
|
3466
|
+
};
|
|
3467
|
+
options.media.addEventListener?.("devicechange", onDeviceChange);
|
|
3468
|
+
return {
|
|
3469
|
+
snapshot,
|
|
3470
|
+
refresh: enumerate,
|
|
3471
|
+
async requestPermission(kinds) {
|
|
3472
|
+
if (!kinds.audio && !kinds.video) {
|
|
3473
|
+
throw new MeetError(
|
|
3474
|
+
"misconfigured",
|
|
3475
|
+
"meet.requestPermission: asked for neither audio nor video",
|
|
3476
|
+
"Request at least one kind. Prompting for nothing silently resolves and leaves labels hidden, which looks like a broken picker."
|
|
3477
|
+
);
|
|
3478
|
+
}
|
|
3479
|
+
try {
|
|
3480
|
+
const stream = await options.media.getUserMedia({
|
|
3481
|
+
...kinds.audio ? { audio: true } : {},
|
|
3482
|
+
...kinds.video ? { video: true } : {}
|
|
3483
|
+
});
|
|
3484
|
+
for (const track of stream.getTracks()) track.stop();
|
|
3485
|
+
permission = "granted";
|
|
3486
|
+
} catch (error) {
|
|
3487
|
+
permission = "denied";
|
|
3488
|
+
emit();
|
|
3489
|
+
throw normalizeDeviceError(error, "meet.requestPermission");
|
|
3490
|
+
}
|
|
3491
|
+
return enumerate();
|
|
3507
3492
|
},
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3493
|
+
select(kind, deviceId) {
|
|
3494
|
+
selected = { ...selected, [kind]: deviceId };
|
|
3495
|
+
fallbackNotice = null;
|
|
3496
|
+
storage.write({
|
|
3497
|
+
...storage.read(),
|
|
3498
|
+
[kind]: deviceId
|
|
3499
|
+
});
|
|
3500
|
+
return emit();
|
|
3513
3501
|
},
|
|
3514
|
-
|
|
3515
|
-
const
|
|
3516
|
-
|
|
3517
|
-
|
|
3502
|
+
resolve(kind) {
|
|
3503
|
+
const current2 = selected[kind];
|
|
3504
|
+
if (current2 !== null && devices[kind].some((device) => device.deviceId === current2)) {
|
|
3505
|
+
return current2;
|
|
3506
|
+
}
|
|
3507
|
+
return devices[kind][0]?.deviceId ?? null;
|
|
3508
|
+
},
|
|
3509
|
+
subscribe(listener) {
|
|
3510
|
+
listeners.add(listener);
|
|
3511
|
+
return () => listeners.delete(listener);
|
|
3512
|
+
},
|
|
3513
|
+
dispose() {
|
|
3514
|
+
options.media.removeEventListener?.("devicechange", onDeviceChange);
|
|
3515
|
+
listeners.clear();
|
|
3516
|
+
}
|
|
3517
|
+
};
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
// src/core/format.ts
|
|
3521
|
+
function participantSummary(participants) {
|
|
3522
|
+
const names = participants.filter((p) => !p.isLocal).map((p) => p.displayName);
|
|
3523
|
+
if (names.length === 0) return "Just you";
|
|
3524
|
+
if (names.length === 1) return names[0] ?? "";
|
|
3525
|
+
if (names.length === 2) return `${names[0]} and ${names[1]}`;
|
|
3526
|
+
return `${names[0]}, ${names[1]} and ${names.length - 2} ${names.length - 2 === 1 ? "other" : "others"}`;
|
|
3527
|
+
}
|
|
3528
|
+
function callStateLabel(state, outgoing) {
|
|
3529
|
+
switch (state) {
|
|
3530
|
+
case "dialing":
|
|
3531
|
+
case "ringing":
|
|
3532
|
+
return outgoing ? "Calling\u2026" : "Incoming call";
|
|
3533
|
+
case "connecting":
|
|
3534
|
+
return "Connecting\u2026";
|
|
3535
|
+
case "active":
|
|
3536
|
+
return "In call";
|
|
3537
|
+
case "declined":
|
|
3538
|
+
return outgoing ? "Declined" : "You declined";
|
|
3539
|
+
case "missed":
|
|
3540
|
+
return outgoing ? "No answer" : "Missed call";
|
|
3541
|
+
case "busy":
|
|
3542
|
+
return outgoing ? "On another call" : "You were busy";
|
|
3543
|
+
case "cancelled":
|
|
3544
|
+
return outgoing ? "Cancelled" : "Cancelled by caller";
|
|
3545
|
+
case "ended":
|
|
3546
|
+
return "Call ended";
|
|
3547
|
+
case "failed":
|
|
3548
|
+
return "Call failed";
|
|
3549
|
+
default:
|
|
3550
|
+
return "Call";
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
function countLabel(count2, singular, plural = `${singular}s`) {
|
|
3554
|
+
return `${count2} ${count2 === 1 ? singular : plural}`;
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
// src/core/invitations.ts
|
|
3558
|
+
var CALL_INVITE_KIND = "meet.call-invite";
|
|
3559
|
+
var CALL_INVITE_VERSION = 1;
|
|
3560
|
+
function callInviteAction(invite) {
|
|
3561
|
+
return {
|
|
3562
|
+
kind: CALL_INVITE_KIND,
|
|
3563
|
+
version: CALL_INVITE_VERSION,
|
|
3564
|
+
payload: {
|
|
3565
|
+
invite_id: invite.id,
|
|
3566
|
+
room_name: invite.roomName,
|
|
3567
|
+
mode: invite.mode,
|
|
3568
|
+
caller_name: invite.callerName,
|
|
3569
|
+
expires_at: invite.expiresAt
|
|
3570
|
+
}
|
|
3571
|
+
};
|
|
3572
|
+
}
|
|
3573
|
+
function callInviteText(invite) {
|
|
3574
|
+
return invite.mode === "video" ? `${invite.callerName} started a video call` : `${invite.callerName} started an audio call`;
|
|
3575
|
+
}
|
|
3576
|
+
function createCallInviteHandler(options) {
|
|
3577
|
+
const now = options.now ?? (() => Date.now());
|
|
3578
|
+
return {
|
|
3579
|
+
kind: CALL_INVITE_KIND,
|
|
3580
|
+
versions: [CALL_INVITE_VERSION],
|
|
3581
|
+
choices: (payload) => {
|
|
3582
|
+
if (Date.parse(payload.expires_at) < now()) return [];
|
|
3583
|
+
return [
|
|
3584
|
+
{ id: "join", label: payload.mode === "video" ? "Join video" : "Join call", tone: "primary" },
|
|
3585
|
+
{ id: "decline", label: "Decline", tone: "neutral" }
|
|
3586
|
+
];
|
|
3587
|
+
},
|
|
3588
|
+
summarize: (payload) => payload.mode === "video" ? `${payload.caller_name} started a video call` : `${payload.caller_name} started an audio call`,
|
|
3589
|
+
execute: async (payload, context) => {
|
|
3590
|
+
const settledAt = new Date(now()).toISOString();
|
|
3591
|
+
const inviteId = asCallId(payload.invite_id);
|
|
3592
|
+
const pending = await options.repository.pendingInvites(asUserId(String(context.actorId)));
|
|
3593
|
+
const live = pending.find((invite) => invite.id === inviteId) ?? null;
|
|
3594
|
+
if (live === null) {
|
|
3595
|
+
return {
|
|
3596
|
+
kind: CALL_INVITE_KIND,
|
|
3597
|
+
messageId: context.messageId,
|
|
3598
|
+
actorId: context.actorId,
|
|
3599
|
+
outcome: "unavailable",
|
|
3600
|
+
label: "This call has ended",
|
|
3601
|
+
settledAt,
|
|
3602
|
+
detail: "The invite is no longer ringing \u2014 it was answered elsewhere, cancelled, or it timed out. The chips are replaced by the outcome rather than left clickable."
|
|
3603
|
+
};
|
|
3604
|
+
}
|
|
3605
|
+
if (context.choice === "decline") {
|
|
3606
|
+
await options.calls.decline(inviteId);
|
|
3607
|
+
return {
|
|
3608
|
+
kind: CALL_INVITE_KIND,
|
|
3609
|
+
messageId: context.messageId,
|
|
3610
|
+
actorId: context.actorId,
|
|
3611
|
+
outcome: "declined",
|
|
3612
|
+
label: "Declined",
|
|
3613
|
+
settledAt
|
|
3614
|
+
};
|
|
3615
|
+
}
|
|
3616
|
+
const accepted = await options.calls.accept(inviteId, live.mode);
|
|
3617
|
+
await options.onJoin(accepted);
|
|
3618
|
+
return {
|
|
3619
|
+
kind: CALL_INVITE_KIND,
|
|
3620
|
+
messageId: context.messageId,
|
|
3621
|
+
actorId: context.actorId,
|
|
3622
|
+
outcome: "applied",
|
|
3623
|
+
label: "Joined",
|
|
3624
|
+
settledAt
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
3627
|
+
};
|
|
3628
|
+
}
|
|
3629
|
+
var MEETING_INVITE_KIND = "meet.meeting-invite";
|
|
3630
|
+
var MEETING_INVITE_VERSION = 1;
|
|
3631
|
+
function meetingInviteAction(payload) {
|
|
3632
|
+
return { kind: MEETING_INVITE_KIND, version: MEETING_INVITE_VERSION, payload };
|
|
3633
|
+
}
|
|
3634
|
+
function createMeetingInviteHandler(options) {
|
|
3635
|
+
const now = options.now ?? (() => Date.now());
|
|
3636
|
+
return {
|
|
3637
|
+
kind: MEETING_INVITE_KIND,
|
|
3638
|
+
versions: [MEETING_INVITE_VERSION],
|
|
3639
|
+
// A DURABLE LINK NEVER EXPIRES (R3) — the chip stays useful after the
|
|
3640
|
+
// meeting too, because that is when people go looking for the notes.
|
|
3641
|
+
choices: () => [{ id: "open", label: "Open meeting", tone: "primary" }],
|
|
3642
|
+
summarize: (payload) => payload.scheduled_for === null ? payload.title : `${payload.title} \u2014 ${payload.scheduled_for}`,
|
|
3643
|
+
execute: async (payload, context) => {
|
|
3644
|
+
if (payload.slug.length === 0) {
|
|
3645
|
+
throw new MeetError(
|
|
3646
|
+
"invalid-response",
|
|
3647
|
+
"meet.meeting-invite: the payload carries no slug",
|
|
3648
|
+
"A meeting invitation without a durable slug cannot be opened. Fix the sender; never synthesize a link on the receiving side."
|
|
3649
|
+
);
|
|
3650
|
+
}
|
|
3651
|
+
await options.onOpen(payload);
|
|
3652
|
+
return {
|
|
3653
|
+
kind: MEETING_INVITE_KIND,
|
|
3654
|
+
messageId: context.messageId,
|
|
3655
|
+
actorId: context.actorId,
|
|
3656
|
+
outcome: "applied",
|
|
3657
|
+
label: "Opened",
|
|
3658
|
+
settledAt: new Date(now()).toISOString()
|
|
3659
|
+
};
|
|
3660
|
+
}
|
|
3661
|
+
};
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
// src/core/layout.ts
|
|
3665
|
+
var DEFAULT_PAGE_SIZE = 12;
|
|
3666
|
+
function orderParticipants(participants, activeSpeakers) {
|
|
3667
|
+
const speakerRank = /* @__PURE__ */ new Map();
|
|
3668
|
+
activeSpeakers.forEach((identity, index) => speakerRank.set(identity, index));
|
|
3669
|
+
return [...participants].sort((a, b) => {
|
|
3670
|
+
const speakerA = speakerRank.get(a.identity) ?? Number.MAX_SAFE_INTEGER;
|
|
3671
|
+
const speakerB = speakerRank.get(b.identity) ?? Number.MAX_SAFE_INTEGER;
|
|
3672
|
+
if (speakerA !== speakerB) return speakerA - speakerB;
|
|
3673
|
+
if (a.handRaised !== b.handRaised) return a.handRaised ? -1 : 1;
|
|
3674
|
+
if (a.cameraEnabled !== b.cameraEnabled) return a.cameraEnabled ? -1 : 1;
|
|
3675
|
+
if (a.joinedAt !== b.joinedAt) return a.joinedAt < b.joinedAt ? -1 : 1;
|
|
3676
|
+
return a.identity < b.identity ? -1 : 1;
|
|
3677
|
+
});
|
|
3678
|
+
}
|
|
3679
|
+
function computeStage(args) {
|
|
3680
|
+
const pageSize = Math.max(1, args.pageSize ?? DEFAULT_PAGE_SIZE);
|
|
3681
|
+
const ordered = orderParticipants(args.participants, args.activeSpeakers);
|
|
3682
|
+
const sharer = ordered.find((participant) => participant.screenSharing) ?? null;
|
|
3683
|
+
const pinnedParticipant = args.pinned === null ? null : ordered.find((participant) => participant.identity === args.pinned) ?? null;
|
|
3684
|
+
const spotlit = args.spotlight == null ? null : ordered.find((participant) => participant.identity === args.spotlight) ?? null;
|
|
3685
|
+
const feature = sharer !== null ? sharer : pinnedParticipant !== null && args.layout !== "grid" ? pinnedParticipant : spotlit !== null ? spotlit : args.layout === "grid" ? null : ordered[0] ?? null;
|
|
3686
|
+
const rest = feature === null ? ordered : ordered.filter((participant) => participant.identity !== feature.identity);
|
|
3687
|
+
const pageCount = Math.max(1, Math.ceil(rest.length / pageSize));
|
|
3688
|
+
const page = Math.min(Math.max(0, args.page ?? 0), pageCount - 1);
|
|
3689
|
+
const tiles = rest.slice(page * pageSize, page * pageSize + pageSize);
|
|
3690
|
+
return {
|
|
3691
|
+
feature,
|
|
3692
|
+
tiles,
|
|
3693
|
+
overflow: rest.length - tiles.length,
|
|
3694
|
+
page,
|
|
3695
|
+
pageCount,
|
|
3696
|
+
presenting: sharer !== null
|
|
3697
|
+
};
|
|
3698
|
+
}
|
|
3699
|
+
function gridColumns(count2) {
|
|
3700
|
+
if (count2 <= 1) return 1;
|
|
3701
|
+
if (count2 <= 4) return 2;
|
|
3702
|
+
if (count2 <= 9) return 3;
|
|
3703
|
+
if (count2 <= 16) return 4;
|
|
3704
|
+
return 5;
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
// src/core/room-driver.ts
|
|
3708
|
+
var DATA_TOPICS = {
|
|
3709
|
+
hand: "mx.hand",
|
|
3710
|
+
reaction: "mx.reaction",
|
|
3711
|
+
chat: "mx.chat"
|
|
3712
|
+
};
|
|
3713
|
+
var TRANSCRIPTION_TOPIC = "lk.transcription";
|
|
3714
|
+
var TRANSCRIPTION_ATTRIBUTES = {
|
|
3715
|
+
segmentId: "lk.segment_id",
|
|
3716
|
+
trackId: "lk.transcribed_track_id",
|
|
3717
|
+
final: "lk.transcription_final",
|
|
3718
|
+
/**
|
|
3719
|
+
* WHO IS BEING TRANSCRIBED. The LiveKit JS client reports the PUBLISHER of a
|
|
3720
|
+
* text stream, and the publisher is always the note-taker — so a caption
|
|
3721
|
+
* carries the speaker in this attribute and falls back to the publisher only
|
|
3722
|
+
* when it is absent (which attributes the line to the agent, visibly, rather
|
|
3723
|
+
* than to the wrong person, silently).
|
|
3724
|
+
*/
|
|
3725
|
+
speakerIdentity: "lk.transcribed_participant_identity"
|
|
3726
|
+
};
|
|
3727
|
+
|
|
3728
|
+
// src/core/livekit-driver.ts
|
|
3729
|
+
function mapQuality(quality) {
|
|
3730
|
+
switch (quality) {
|
|
3731
|
+
case "excellent":
|
|
3732
|
+
return "excellent";
|
|
3733
|
+
case "good":
|
|
3734
|
+
return "good";
|
|
3735
|
+
case "poor":
|
|
3736
|
+
return "poor";
|
|
3737
|
+
case "lost":
|
|
3738
|
+
return "lost";
|
|
3739
|
+
default:
|
|
3740
|
+
return "unknown";
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
function mapConnectionState(state) {
|
|
3744
|
+
switch (state) {
|
|
3745
|
+
case "connected":
|
|
3746
|
+
return "connected";
|
|
3747
|
+
case "connecting":
|
|
3748
|
+
return "connecting";
|
|
3749
|
+
case "reconnecting":
|
|
3750
|
+
case "signalReconnecting":
|
|
3751
|
+
return "reconnecting";
|
|
3752
|
+
default:
|
|
3753
|
+
return "disconnected";
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
function snapshotParticipant(participant, isLocal) {
|
|
3757
|
+
const name = participant.name;
|
|
3758
|
+
return {
|
|
3759
|
+
identity: participant.identity,
|
|
3760
|
+
name: name ?? "",
|
|
3761
|
+
metadata: participant.metadata ?? null,
|
|
3762
|
+
// ATTRIBUTES ARE PART OF THE SNAPSHOT, not read ad hoc later: the note-taker
|
|
3763
|
+
// announces the consent notice here (D10) and the roster is the only place
|
|
3764
|
+
// this package looks at a participant.
|
|
3765
|
+
attributes: participant.attributes ?? {},
|
|
3766
|
+
isLocal,
|
|
3767
|
+
micEnabled: participant.isMicrophoneEnabled,
|
|
3768
|
+
cameraEnabled: participant.isCameraEnabled,
|
|
3769
|
+
screenSharing: participant.isScreenShareEnabled
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3772
|
+
var DEFAULT_AUDIO_PROCESSING = {
|
|
3773
|
+
noiseSuppression: true,
|
|
3774
|
+
echoCancellation: true,
|
|
3775
|
+
autoGainControl: true
|
|
3776
|
+
};
|
|
3777
|
+
function videoEffectsSupported() {
|
|
3778
|
+
const scope = globalThis;
|
|
3779
|
+
if (typeof scope.OffscreenCanvas === "undefined" || scope.document === void 0) return false;
|
|
3780
|
+
try {
|
|
3781
|
+
return scope.document.createElement("canvas").getContext("webgl2") !== null;
|
|
3782
|
+
} catch {
|
|
3783
|
+
return false;
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
async function applyVideoEffect(target, effect) {
|
|
3787
|
+
const { Track } = await import("livekit-client");
|
|
3788
|
+
const track = target.localParticipant.getTrackPublication(Track.Source.Camera)?.track;
|
|
3789
|
+
if (track === void 0) return;
|
|
3790
|
+
try {
|
|
3791
|
+
if (effect.kind === "none") {
|
|
3792
|
+
await track.stopProcessor();
|
|
3793
|
+
return;
|
|
3794
|
+
}
|
|
3795
|
+
const processors = await import("@livekit/track-processors");
|
|
3796
|
+
const processor = effect.kind === "blur" ? processors.BackgroundBlur(effect.radius ?? 12) : processors.VirtualBackground(effect.imageUrl);
|
|
3797
|
+
await track.setProcessor(processor);
|
|
3798
|
+
} catch (error) {
|
|
3799
|
+
throw new MeetError(
|
|
3800
|
+
"media-unsupported",
|
|
3801
|
+
"The background effect could not start on this camera",
|
|
3802
|
+
"Background effects need a browser with WebGL2 (current Chrome, Edge, Firefox or Safari). Your video continues without the effect.",
|
|
3803
|
+
error
|
|
3804
|
+
);
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
var encoder = new TextEncoder();
|
|
3808
|
+
var decoder = new TextDecoder();
|
|
3809
|
+
function createLiveKitDriver(handlers, options = {}) {
|
|
3810
|
+
let room = null;
|
|
3811
|
+
let projectRoster = null;
|
|
3812
|
+
let audioCapture = DEFAULT_AUDIO_PROCESSING;
|
|
3813
|
+
let videoEffect = { kind: "none" };
|
|
3814
|
+
async function load() {
|
|
3815
|
+
if (room !== null) return room;
|
|
3816
|
+
let module2;
|
|
3817
|
+
try {
|
|
3818
|
+
module2 = await import("livekit-client");
|
|
3819
|
+
} catch (error) {
|
|
3820
|
+
throw new MeetError(
|
|
3821
|
+
"misconfigured",
|
|
3822
|
+
"livekit-client could not be loaded",
|
|
3823
|
+
"`livekit-client` is a dependency of @ai-matrx/meet and is imported lazily at connect time. If this failed, the bundler excluded it \u2014 remove it from any `external`/`serverExternalPackages` list, or pass your own driver factory.",
|
|
3824
|
+
error
|
|
3518
3825
|
);
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3826
|
+
}
|
|
3827
|
+
const { Room: LiveKitRoom, RoomEvent, Track } = module2;
|
|
3828
|
+
const created = new LiveKitRoom({
|
|
3829
|
+
// R5: large meetings degrade gracefully instead of melting a laptop.
|
|
3830
|
+
adaptiveStream: true,
|
|
3831
|
+
dynacast: true,
|
|
3832
|
+
...options.roomOptions
|
|
3833
|
+
});
|
|
3834
|
+
const roster = () => {
|
|
3835
|
+
const local = snapshotParticipant(created.localParticipant, true);
|
|
3836
|
+
const remotes = [...created.remoteParticipants.values()].map(
|
|
3837
|
+
(participant) => snapshotParticipant(participant, false)
|
|
3522
3838
|
);
|
|
3523
|
-
|
|
3524
|
-
}
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
if (
|
|
3528
|
-
const
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3839
|
+
handlers.participants([local, ...remotes]);
|
|
3840
|
+
};
|
|
3841
|
+
projectRoster = roster;
|
|
3842
|
+
created.on(RoomEvent.TrackSubscribed, (track) => {
|
|
3843
|
+
if (track.kind !== Track.Kind.Audio || typeof document === "undefined") return;
|
|
3844
|
+
const element = track.attach();
|
|
3845
|
+
element.setAttribute("data-mx-meet-audio", "");
|
|
3846
|
+
audioSink().appendChild(element);
|
|
3847
|
+
}).on(RoomEvent.TrackUnsubscribed, (track) => {
|
|
3848
|
+
if (track.kind !== Track.Kind.Audio) return;
|
|
3849
|
+
for (const element of track.detach()) element.remove();
|
|
3850
|
+
});
|
|
3851
|
+
created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, roster).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, roster).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ParticipantNameChanged, roster).on(RoomEvent.ParticipantAttributesChanged, roster).on(RoomEvent.ParticipantMetadataChanged, roster).on(RoomEvent.RoomMetadataChanged, (metadata) => {
|
|
3852
|
+
handlers.roomMetadata?.(metadata ?? "");
|
|
3853
|
+
}).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
|
|
3854
|
+
handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
|
|
3855
|
+
}).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
|
|
3856
|
+
handlers.quality(participant.identity, mapQuality(quality));
|
|
3857
|
+
}).on(RoomEvent.ConnectionStateChanged, (state) => {
|
|
3858
|
+
handlers.connectionState(mapConnectionState(state));
|
|
3859
|
+
}).on(RoomEvent.DataReceived, (payload, participant, _kind, topic) => {
|
|
3860
|
+
handlers.data(
|
|
3861
|
+
topic ?? "",
|
|
3862
|
+
decoder.decode(payload),
|
|
3863
|
+
participant?.identity ?? null
|
|
3864
|
+
);
|
|
3865
|
+
}).on(RoomEvent.Disconnected, (reason) => {
|
|
3866
|
+
audioContainer?.remove();
|
|
3867
|
+
audioContainer = null;
|
|
3868
|
+
handlers.disconnected(reason === void 0 ? null : String(reason));
|
|
3869
|
+
});
|
|
3870
|
+
try {
|
|
3871
|
+
created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
|
|
3872
|
+
const attributes = { ...reader.info.attributes ?? {} };
|
|
3873
|
+
const segmentId = attributes[TRANSCRIPTION_ATTRIBUTES.segmentId] ?? reader.info.id;
|
|
3874
|
+
const trackId = attributes[TRANSCRIPTION_ATTRIBUTES.trackId] ?? null;
|
|
3875
|
+
const final = attributes[TRANSCRIPTION_ATTRIBUTES.final] === "true";
|
|
3876
|
+
const speakerIdentity = attributes[TRANSCRIPTION_ATTRIBUTES.speakerIdentity] ?? participantInfo.identity;
|
|
3877
|
+
const at = new Date(reader.info.timestamp || Date.now()).toISOString();
|
|
3878
|
+
void (async () => {
|
|
3879
|
+
let text3 = "";
|
|
3880
|
+
try {
|
|
3881
|
+
for await (const chunk of reader) {
|
|
3882
|
+
text3 += chunk;
|
|
3883
|
+
handlers.transcription({
|
|
3884
|
+
segmentId,
|
|
3885
|
+
speakerIdentity,
|
|
3886
|
+
publisherIdentity: participantInfo.identity,
|
|
3887
|
+
text: text3,
|
|
3888
|
+
final,
|
|
3889
|
+
trackId,
|
|
3890
|
+
at
|
|
3891
|
+
});
|
|
3892
|
+
}
|
|
3893
|
+
} catch (error) {
|
|
3894
|
+
handlers.data(
|
|
3895
|
+
TRANSCRIPTION_TOPIC,
|
|
3896
|
+
JSON.stringify({ error: String(error?.message ?? error) }),
|
|
3897
|
+
participantInfo.identity
|
|
3898
|
+
);
|
|
3899
|
+
}
|
|
3900
|
+
})();
|
|
3901
|
+
});
|
|
3902
|
+
} catch {
|
|
3903
|
+
}
|
|
3904
|
+
sources = {
|
|
3905
|
+
camera: Track.Source.Camera,
|
|
3906
|
+
microphone: Track.Source.Microphone,
|
|
3907
|
+
screen: Track.Source.ScreenShare
|
|
3908
|
+
};
|
|
3909
|
+
trackEvents = {
|
|
3910
|
+
subscribed: RoomEvent.TrackSubscribed,
|
|
3911
|
+
localPublished: RoomEvent.LocalTrackPublished
|
|
3912
|
+
};
|
|
3913
|
+
room = created;
|
|
3914
|
+
return created;
|
|
3915
|
+
}
|
|
3916
|
+
let sources = null;
|
|
3917
|
+
let trackEvents = null;
|
|
3918
|
+
let audioContainer = null;
|
|
3919
|
+
function audioSink() {
|
|
3920
|
+
if (audioContainer !== null && audioContainer.isConnected) return audioContainer;
|
|
3921
|
+
const container = document.createElement("div");
|
|
3922
|
+
container.setAttribute("data-mx-meet-audio-sink", "");
|
|
3923
|
+
container.setAttribute("aria-hidden", "true");
|
|
3924
|
+
container.style.display = "none";
|
|
3925
|
+
document.body.appendChild(container);
|
|
3926
|
+
audioContainer = container;
|
|
3927
|
+
return container;
|
|
3928
|
+
}
|
|
3929
|
+
function requireRoom(operation) {
|
|
3930
|
+
if (room === null) {
|
|
3931
|
+
throw new MeetError(
|
|
3932
|
+
"transport",
|
|
3933
|
+
`${operation}: the room is not connected`,
|
|
3934
|
+
"Call connect() first. The engine never reaches this \u2014 it gates every media call on its own phase \u2014 so seeing it means a consumer drove the driver directly."
|
|
3935
|
+
);
|
|
3936
|
+
}
|
|
3937
|
+
return room;
|
|
3938
|
+
}
|
|
3939
|
+
return {
|
|
3940
|
+
async connect(serverUrl, token) {
|
|
3941
|
+
const target = await load();
|
|
3942
|
+
try {
|
|
3943
|
+
await target.connect(serverUrl, token);
|
|
3944
|
+
} catch (error) {
|
|
3945
|
+
throw normalizeDeviceError(error, "meet.connect");
|
|
3534
3946
|
}
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
commit({ ...state, chat });
|
|
3538
|
-
},
|
|
3539
|
-
addReaction(reaction) {
|
|
3540
|
-
commit({ ...state, reactions: [...state.reactions, reaction] });
|
|
3947
|
+
projectRoster?.();
|
|
3948
|
+
handlers.roomMetadata?.(target.metadata ?? "");
|
|
3541
3949
|
},
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3950
|
+
async disconnect() {
|
|
3951
|
+
if (room === null) return;
|
|
3952
|
+
const target = room;
|
|
3953
|
+
room = null;
|
|
3954
|
+
try {
|
|
3955
|
+
target.unregisterTextStreamHandler(TRANSCRIPTION_TOPIC);
|
|
3956
|
+
} catch {
|
|
3957
|
+
}
|
|
3958
|
+
projectRoster = null;
|
|
3959
|
+
audioContainer?.remove();
|
|
3960
|
+
audioContainer = null;
|
|
3961
|
+
await target.disconnect();
|
|
3546
3962
|
},
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3963
|
+
async setMicrophoneEnabled(enabled) {
|
|
3964
|
+
try {
|
|
3965
|
+
await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(
|
|
3966
|
+
enabled,
|
|
3967
|
+
audioCapture
|
|
3968
|
+
);
|
|
3969
|
+
} catch (error) {
|
|
3970
|
+
throw normalizeDeviceError(error, "meet.setMicrophoneEnabled");
|
|
3551
3971
|
}
|
|
3552
|
-
|
|
3553
|
-
|
|
3972
|
+
},
|
|
3973
|
+
async setCameraEnabled(enabled) {
|
|
3974
|
+
try {
|
|
3975
|
+
const target = requireRoom("setCameraEnabled");
|
|
3976
|
+
await target.localParticipant.setCameraEnabled(enabled);
|
|
3977
|
+
if (enabled && videoEffect.kind !== "none") await applyVideoEffect(target, videoEffect);
|
|
3978
|
+
} catch (error) {
|
|
3979
|
+
throw normalizeDeviceError(error, "meet.setCameraEnabled");
|
|
3554
3980
|
}
|
|
3555
|
-
commit({
|
|
3556
|
-
...state,
|
|
3557
|
-
recording: next,
|
|
3558
|
-
recordingStartedBy: by,
|
|
3559
|
-
recordingSource: source,
|
|
3560
|
-
recordingId: recordingId ?? state.recordingId
|
|
3561
|
-
});
|
|
3562
3981
|
},
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3982
|
+
async setScreenShareEnabled(enabled) {
|
|
3983
|
+
try {
|
|
3984
|
+
await requireRoom("setScreenShareEnabled").localParticipant.setScreenShareEnabled(enabled);
|
|
3985
|
+
} catch (error) {
|
|
3986
|
+
throw normalizeDeviceError(error, "meet.setScreenShareEnabled");
|
|
3987
|
+
}
|
|
3567
3988
|
},
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3576
|
-
decidedBy: by
|
|
3577
|
-
} : entry
|
|
3578
|
-
)
|
|
3579
|
-
});
|
|
3989
|
+
async publishData(topic, payload, to) {
|
|
3990
|
+
await requireRoom("publishData").localParticipant.publishData(
|
|
3991
|
+
// `reliable` is not optional for a control plane: a dropped
|
|
3992
|
+
// "hand lowered" leaves a hand up on every other screen forever.
|
|
3993
|
+
encoder.encode(payload),
|
|
3994
|
+
to === void 0 || to.length === 0 ? { reliable: true, topic } : { reliable: true, topic, destinationIdentities: [...to] }
|
|
3995
|
+
);
|
|
3580
3996
|
},
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3997
|
+
async setAudioProcessing(processing) {
|
|
3998
|
+
audioCapture = { ...audioCapture, ...processing };
|
|
3999
|
+
const target = room;
|
|
4000
|
+
if (target === null) return;
|
|
4001
|
+
const { Track } = await import("livekit-client");
|
|
4002
|
+
const publication = target.localParticipant.getTrackPublication(Track.Source.Microphone);
|
|
4003
|
+
const track = publication?.track;
|
|
4004
|
+
if (track === void 0 || publication?.isMuted === true) return;
|
|
4005
|
+
try {
|
|
4006
|
+
await track.restartTrack({
|
|
4007
|
+
...audioCapture,
|
|
4008
|
+
...target.getActiveDevice("audioinput") !== void 0 ? { deviceId: target.getActiveDevice("audioinput") } : {}
|
|
4009
|
+
});
|
|
4010
|
+
} catch (error) {
|
|
4011
|
+
throw normalizeDeviceError(error, "meet.setAudioProcessing");
|
|
4012
|
+
}
|
|
3585
4013
|
},
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
const
|
|
3589
|
-
if (
|
|
3590
|
-
|
|
4014
|
+
async setVideoEffect(effect) {
|
|
4015
|
+
videoEffect = effect;
|
|
4016
|
+
const target = room;
|
|
4017
|
+
if (target === null) return;
|
|
4018
|
+
await applyVideoEffect(target, effect);
|
|
3591
4019
|
},
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
const transcript = mergeRecord(state.transcript, segment);
|
|
3595
|
-
if (transcript === state.transcript && names === state.names) return;
|
|
3596
|
-
commit({ ...state, transcript, names });
|
|
4020
|
+
supportsVideoEffects() {
|
|
4021
|
+
return videoEffectsSupported();
|
|
3597
4022
|
},
|
|
3598
|
-
|
|
3599
|
-
const
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
return;
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
4023
|
+
attach(identity, kind, element) {
|
|
4024
|
+
const target = requireRoom("attach");
|
|
4025
|
+
const source = sources?.[kind];
|
|
4026
|
+
if (source === void 0) return () => void 0;
|
|
4027
|
+
let current = null;
|
|
4028
|
+
const sync = () => {
|
|
4029
|
+
const participant = target.localParticipant.identity === identity ? target.localParticipant : target.remoteParticipants.get(identity);
|
|
4030
|
+
const track = participant?.getTrackPublication(source)?.track;
|
|
4031
|
+
if (track === void 0 || track === current) return;
|
|
4032
|
+
current?.detach(element);
|
|
4033
|
+
track.attach(element);
|
|
4034
|
+
current = track;
|
|
4035
|
+
};
|
|
4036
|
+
const onTrack = (...args) => {
|
|
4037
|
+
const who = args[args.length - 1];
|
|
4038
|
+
if (who?.identity === identity) sync();
|
|
4039
|
+
};
|
|
4040
|
+
sync();
|
|
4041
|
+
const events = trackEvents;
|
|
4042
|
+
if (events !== null) {
|
|
4043
|
+
target.on(events.subscribed, onTrack);
|
|
4044
|
+
target.on(events.localPublished, onTrack);
|
|
3610
4045
|
}
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
commit({ ...state, wrapUp: announcement });
|
|
4046
|
+
return () => {
|
|
4047
|
+
if (events !== null) {
|
|
4048
|
+
target.off(events.subscribed, onTrack);
|
|
4049
|
+
target.off(events.localPublished, onTrack);
|
|
4050
|
+
}
|
|
4051
|
+
current?.detach(element);
|
|
4052
|
+
current = null;
|
|
4053
|
+
};
|
|
3620
4054
|
},
|
|
3621
|
-
|
|
3622
|
-
const
|
|
3623
|
-
if (
|
|
3624
|
-
return;
|
|
4055
|
+
activeDevices() {
|
|
4056
|
+
const target = room;
|
|
4057
|
+
if (target === null) {
|
|
4058
|
+
return { audioinput: null, videoinput: null, audiooutput: null };
|
|
3625
4059
|
}
|
|
3626
|
-
|
|
4060
|
+
return {
|
|
4061
|
+
audioinput: target.getActiveDevice("audioinput") ?? null,
|
|
4062
|
+
videoinput: target.getActiveDevice("videoinput") ?? null,
|
|
4063
|
+
audiooutput: target.getActiveDevice("audiooutput") ?? null
|
|
4064
|
+
};
|
|
3627
4065
|
},
|
|
3628
|
-
|
|
3629
|
-
|
|
4066
|
+
async switchDevice(kind, deviceId) {
|
|
4067
|
+
try {
|
|
4068
|
+
await requireRoom("switchDevice").switchActiveDevice(kind, deviceId);
|
|
4069
|
+
} catch (error) {
|
|
4070
|
+
throw normalizeDeviceError(error, "meet.switchDevice");
|
|
4071
|
+
}
|
|
3630
4072
|
}
|
|
3631
4073
|
};
|
|
3632
4074
|
}
|
|
@@ -4098,6 +4540,7 @@ function kindOf(person) {
|
|
|
4098
4540
|
if (person.role === "host") return "host";
|
|
4099
4541
|
if (person.role === "cohost") return "cohost";
|
|
4100
4542
|
if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
|
|
4543
|
+
if (person.userId === null) return "guest";
|
|
4101
4544
|
return "member";
|
|
4102
4545
|
}
|
|
4103
4546
|
function ms(iso) {
|
|
@@ -4153,12 +4596,19 @@ function coveredMs(intervals) {
|
|
|
4153
4596
|
}
|
|
4154
4597
|
function attendanceReport(meeting, attendees, names = {}) {
|
|
4155
4598
|
const end = ms(meeting.endedAt);
|
|
4599
|
+
const measuredOf = /* @__PURE__ */ new Map();
|
|
4156
4600
|
const perJoin = attendees.map((person) => {
|
|
4157
|
-
const
|
|
4158
|
-
|
|
4159
|
-
const
|
|
4160
|
-
const
|
|
4161
|
-
|
|
4601
|
+
const stays = person.sessions !== void 0 && person.sessions.length > 0 ? person.sessions.map((stay) => [ms(stay.joinedAt), ms(stay.leftAt)]) : [[ms(person.joinedAt), ms(person.leftAt)]];
|
|
4602
|
+
let toEnd = false;
|
|
4603
|
+
const measured = [];
|
|
4604
|
+
for (const [joined, left] of stays) {
|
|
4605
|
+
const until = left ?? end;
|
|
4606
|
+
if (joined === null || until === null || until < joined) continue;
|
|
4607
|
+
if (left === null) toEnd = true;
|
|
4608
|
+
measured.push([joined, until]);
|
|
4609
|
+
}
|
|
4610
|
+
const duration = measured.length === 0 ? null : coveredMs(measured);
|
|
4611
|
+
const row = {
|
|
4162
4612
|
id: person.id,
|
|
4163
4613
|
identity: person.identity,
|
|
4164
4614
|
name: displayNameFor(names, person.identity, person.displayName),
|
|
@@ -4167,10 +4617,12 @@ function attendanceReport(meeting, attendees, names = {}) {
|
|
|
4167
4617
|
joinedAt: person.joinedAt,
|
|
4168
4618
|
leftAt: person.leftAt,
|
|
4169
4619
|
durationMs: duration,
|
|
4170
|
-
durationToEnd: duration !== null &&
|
|
4620
|
+
durationToEnd: duration !== null && toEnd,
|
|
4171
4621
|
admission: person.admissionState,
|
|
4172
|
-
joins: 1
|
|
4622
|
+
joins: Math.max(1, person.sessions?.length ?? 0)
|
|
4173
4623
|
};
|
|
4624
|
+
measuredOf.set(row, measured);
|
|
4625
|
+
return row;
|
|
4174
4626
|
});
|
|
4175
4627
|
const groups = /* @__PURE__ */ new Map();
|
|
4176
4628
|
attendees.forEach((person, index) => {
|
|
@@ -4193,17 +4645,14 @@ function attendanceReport(meeting, attendees, names = {}) {
|
|
|
4193
4645
|
const left = ms(row.leftAt);
|
|
4194
4646
|
return left !== null && (latest === null || left > latest) ? left : latest;
|
|
4195
4647
|
}, null);
|
|
4196
|
-
const intervals = came.flatMap((row) =>
|
|
4197
|
-
const joined = ms(row.joinedAt);
|
|
4198
|
-
return joined !== null && row.durationMs !== null ? [[joined, joined + row.durationMs]] : [];
|
|
4199
|
-
});
|
|
4648
|
+
const intervals = came.flatMap((row) => measuredOf.get(row) ?? []);
|
|
4200
4649
|
return {
|
|
4201
4650
|
...first,
|
|
4202
4651
|
kind,
|
|
4203
4652
|
leftAt: open ? null : lastLeave === null ? null : new Date(lastLeave).toISOString(),
|
|
4204
4653
|
durationMs: intervals.length === 0 ? null : coveredMs(intervals),
|
|
4205
4654
|
durationToEnd: came.some((row) => row.durationToEnd),
|
|
4206
|
-
joins: came.length === 0 ? joins.length : came.
|
|
4655
|
+
joins: came.length === 0 ? joins.length : came.reduce((total, row) => total + row.joins, 0)
|
|
4207
4656
|
};
|
|
4208
4657
|
});
|
|
4209
4658
|
const rank = KIND_RANK;
|
|
@@ -4594,6 +5043,7 @@ function createRoomEngine(options) {
|
|
|
4594
5043
|
let joining = null;
|
|
4595
5044
|
let lastRequest = null;
|
|
4596
5045
|
let disposed = false;
|
|
5046
|
+
let moving = false;
|
|
4597
5047
|
const confirmingEnds = /* @__PURE__ */ new WeakSet();
|
|
4598
5048
|
async function leaveRoom() {
|
|
4599
5049
|
const target = driver;
|
|
@@ -4823,6 +5273,7 @@ function createRoomEngine(options) {
|
|
|
4823
5273
|
}
|
|
4824
5274
|
},
|
|
4825
5275
|
disconnected(reason) {
|
|
5276
|
+
if (moving) return;
|
|
4826
5277
|
if (current !== null) {
|
|
4827
5278
|
tokens.invalidate(current.roomName);
|
|
4828
5279
|
current = null;
|
|
@@ -4837,11 +5288,13 @@ function createRoomEngine(options) {
|
|
|
4837
5288
|
roomName: request.roomName,
|
|
4838
5289
|
identity,
|
|
4839
5290
|
...request.meetingId !== void 0 ? { meetingId: request.meetingId } : {},
|
|
4840
|
-
...request.slug !== void 0 ? { slug: request.slug } : {}
|
|
5291
|
+
...request.slug !== void 0 ? { slug: request.slug } : {},
|
|
5292
|
+
...request.breakoutRoomId != null ? { breakoutRoomId: request.breakoutRoomId } : {}
|
|
4841
5293
|
});
|
|
4842
5294
|
current = token;
|
|
4843
5295
|
store.patch({
|
|
4844
5296
|
localIdentity: token.identity,
|
|
5297
|
+
breakoutRoomId: token.breakoutRoomId ?? null,
|
|
4845
5298
|
grants: {
|
|
4846
5299
|
role: token.role,
|
|
4847
5300
|
canRecord: token.canRecord,
|
|
@@ -4960,6 +5413,56 @@ function createRoomEngine(options) {
|
|
|
4960
5413
|
});
|
|
4961
5414
|
return joining;
|
|
4962
5415
|
},
|
|
5416
|
+
async moveToBreakout(roomId) {
|
|
5417
|
+
const request = lastRequest;
|
|
5418
|
+
if (request === null || driver === null) {
|
|
5419
|
+
report(
|
|
5420
|
+
"warn",
|
|
5421
|
+
"meet.moveToBreakout: this engine is not in a meeting room",
|
|
5422
|
+
"Join the meeting first; a breakout is a room OF a meeting."
|
|
5423
|
+
);
|
|
5424
|
+
return;
|
|
5425
|
+
}
|
|
5426
|
+
if ((store.snapshot().breakoutRoomId ?? null) === roomId) return;
|
|
5427
|
+
if (joining !== null) await joining.catch(() => void 0);
|
|
5428
|
+
const previous = driver;
|
|
5429
|
+
const next = {
|
|
5430
|
+
...request,
|
|
5431
|
+
breakoutRoomId: roomId,
|
|
5432
|
+
// Arrive as you were: camera and microphone as they are now.
|
|
5433
|
+
mode: store.snapshot().media.camera.enabled ? "video" : "audio",
|
|
5434
|
+
microphone: store.snapshot().media.microphone.enabled
|
|
5435
|
+
};
|
|
5436
|
+
moving = true;
|
|
5437
|
+
try {
|
|
5438
|
+
driver = null;
|
|
5439
|
+
tokens.invalidate(request.roomName);
|
|
5440
|
+
current = null;
|
|
5441
|
+
await previous.disconnect();
|
|
5442
|
+
lastRequest = next;
|
|
5443
|
+
joining = connect(next).finally(() => {
|
|
5444
|
+
joining = null;
|
|
5445
|
+
});
|
|
5446
|
+
await joining;
|
|
5447
|
+
} catch (error) {
|
|
5448
|
+
const normalized = normalizeMeetError(error, "meet.moveToBreakout");
|
|
5449
|
+
report("warn", normalized.message, normalized.remedy);
|
|
5450
|
+
if (roomId !== null) {
|
|
5451
|
+
lastRequest = { ...next, breakoutRoomId: null };
|
|
5452
|
+
joining = connect(lastRequest).finally(() => {
|
|
5453
|
+
joining = null;
|
|
5454
|
+
});
|
|
5455
|
+
await joining.catch((again) => {
|
|
5456
|
+
throw fail(again, "meet.moveToBreakout");
|
|
5457
|
+
});
|
|
5458
|
+
} else {
|
|
5459
|
+
throw fail(error, "meet.moveToBreakout");
|
|
5460
|
+
}
|
|
5461
|
+
throw normalized;
|
|
5462
|
+
} finally {
|
|
5463
|
+
moving = false;
|
|
5464
|
+
}
|
|
5465
|
+
},
|
|
4963
5466
|
leave: leaveRoom,
|
|
4964
5467
|
// Turning a device on mid-meeting is the SAME operation as turning it on
|
|
4965
5468
|
// during the join, so it runs the same function. A failure here used to
|
|
@@ -5229,6 +5732,109 @@ function createInRoomClient(options) {
|
|
|
5229
5732
|
(row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
|
|
5230
5733
|
).filter((message) => message !== null);
|
|
5231
5734
|
store.loadChat(messages);
|
|
5735
|
+
},
|
|
5736
|
+
async loadCollab() {
|
|
5737
|
+
const operation = "meet.loadCollab";
|
|
5738
|
+
const id = meetingId(operation);
|
|
5739
|
+
const hdrs = headers();
|
|
5740
|
+
const response = await api.request({
|
|
5741
|
+
path: `${MEET_ROUTES.collab}?meeting_id=${encodeURIComponent(id)}`,
|
|
5742
|
+
method: "GET",
|
|
5743
|
+
operation,
|
|
5744
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
5745
|
+
});
|
|
5746
|
+
if (!response.ok) {
|
|
5747
|
+
const error = await refusal2(
|
|
5748
|
+
response,
|
|
5749
|
+
operation,
|
|
5750
|
+
"Polls, questions and the whiteboard could not be read. New ones still arrive."
|
|
5751
|
+
);
|
|
5752
|
+
store.patch({ collab: { ...store.snapshot().collab, loaded: true, error: error.message } });
|
|
5753
|
+
throw error;
|
|
5754
|
+
}
|
|
5755
|
+
const body = await response.json();
|
|
5756
|
+
store.patch({ collab: collabFromState(body, store.snapshot().collab) });
|
|
5757
|
+
},
|
|
5758
|
+
async collab(request) {
|
|
5759
|
+
const operation = `meet.collab(${request.action})`;
|
|
5760
|
+
const hdrs = headers();
|
|
5761
|
+
const response = await api.request({
|
|
5762
|
+
path: MEET_ROUTES.collab,
|
|
5763
|
+
method: "POST",
|
|
5764
|
+
operation,
|
|
5765
|
+
body: {
|
|
5766
|
+
meeting_id: meetingId(operation),
|
|
5767
|
+
action: request.action,
|
|
5768
|
+
...request.itemId !== void 0 ? { item_id: request.itemId } : {},
|
|
5769
|
+
...request.question !== void 0 ? { question: request.question } : {},
|
|
5770
|
+
...request.options !== void 0 ? { options: request.options } : {},
|
|
5771
|
+
...request.multiple !== void 0 ? { multiple: request.multiple } : {},
|
|
5772
|
+
...request.anonymous !== void 0 ? { anonymous: request.anonymous } : {},
|
|
5773
|
+
...request.optionIds !== void 0 ? { option_ids: request.optionIds } : {},
|
|
5774
|
+
...request.text !== void 0 ? { text: request.text } : {},
|
|
5775
|
+
...request.stroke !== void 0 ? { stroke: request.stroke } : {},
|
|
5776
|
+
...request.rooms !== void 0 ? { rooms: request.rooms } : {},
|
|
5777
|
+
...request.allowChoose !== void 0 ? { allow_choose: request.allowChoose } : {},
|
|
5778
|
+
...request.minutes !== void 0 ? { minutes: request.minutes } : {},
|
|
5779
|
+
...request.identity !== void 0 ? { identity: request.identity } : {},
|
|
5780
|
+
...request.roomId !== void 0 ? { room_id: request.roomId } : {}
|
|
5781
|
+
},
|
|
5782
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
5783
|
+
});
|
|
5784
|
+
if (!response.ok) {
|
|
5785
|
+
throw await refusal2(response, operation, "Nothing was changed.");
|
|
5786
|
+
}
|
|
5787
|
+
const payload = await response.json();
|
|
5788
|
+
const removed = Array.isArray(payload["removed"]) ? payload["removed"].filter((v) => typeof v === "string") : [];
|
|
5789
|
+
let next = store.snapshot().collab;
|
|
5790
|
+
const item = payload["item"];
|
|
5791
|
+
if (typeof item === "object" && item !== null) next = withItem(next, item);
|
|
5792
|
+
if (removed.length > 0) {
|
|
5793
|
+
const gone = new Set(removed);
|
|
5794
|
+
next = {
|
|
5795
|
+
...next,
|
|
5796
|
+
polls: next.polls.filter((p) => !gone.has(p.id)),
|
|
5797
|
+
questions: next.questions.filter((q) => !gone.has(q.id)),
|
|
5798
|
+
strokes: next.strokes.filter((st) => !gone.has(st.id))
|
|
5799
|
+
};
|
|
5800
|
+
}
|
|
5801
|
+
store.patch({ collab: next });
|
|
5802
|
+
const itemId = typeof item === "object" && item !== null && typeof item["id"] === "string" ? item["id"] : null;
|
|
5803
|
+
return {
|
|
5804
|
+
action: request.action,
|
|
5805
|
+
message: typeof payload["message"] === "string" ? payload["message"] : "Done.",
|
|
5806
|
+
removed,
|
|
5807
|
+
itemId
|
|
5808
|
+
};
|
|
5809
|
+
},
|
|
5810
|
+
async translateCaptions(language, lines) {
|
|
5811
|
+
if (lines.length === 0) return;
|
|
5812
|
+
const operation = "meet.translateCaptions";
|
|
5813
|
+
const hdrs = headers();
|
|
5814
|
+
const response = await api.request({
|
|
5815
|
+
path: MEET_ROUTES.captionsTranslate,
|
|
5816
|
+
method: "POST",
|
|
5817
|
+
operation,
|
|
5818
|
+
body: { meeting_id: meetingId(operation), target_language: language, lines },
|
|
5819
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
5820
|
+
});
|
|
5821
|
+
if (!response.ok) {
|
|
5822
|
+
const error = await refusal2(response, operation, "Captions stay in the language they were spoken.");
|
|
5823
|
+
store.patch({
|
|
5824
|
+
collab: { ...store.snapshot().collab, captionTranslationError: error.message }
|
|
5825
|
+
});
|
|
5826
|
+
throw error;
|
|
5827
|
+
}
|
|
5828
|
+
const payload = await response.json();
|
|
5829
|
+
const current = store.snapshot().collab;
|
|
5830
|
+
if (current.captionLanguage !== language) return;
|
|
5831
|
+
const merged = { ...current.captionTranslations };
|
|
5832
|
+
for (const [id, value] of Object.entries(payload.translations ?? {})) {
|
|
5833
|
+
if (typeof value === "string") merged[id] = value;
|
|
5834
|
+
}
|
|
5835
|
+
store.patch({
|
|
5836
|
+
collab: { ...current, captionTranslations: merged, captionTranslationError: null }
|
|
5837
|
+
});
|
|
5232
5838
|
}
|
|
5233
5839
|
};
|
|
5234
5840
|
}
|