@ai-matrx/meet 0.7.2 → 0.7.5
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 +88 -0
- package/dist/index.cjs +1002 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +484 -26
- package/dist/index.d.ts +484 -26
- package/dist/index.js +1002 -69
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +2385 -244
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +136 -12
- package/dist/react.d.ts +136 -12
- package/dist/react.js +2386 -245
- package/dist/react.js.map +1 -1
- package/dist/styles.css +522 -0
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ var MeetError = class extends Error {
|
|
|
25
25
|
};
|
|
26
26
|
var FORBIDDEN_CODES = /* @__PURE__ */ new Set(["42501", "PGRST301", "PGRST302"]);
|
|
27
27
|
var NOT_FOUND_CODES = /* @__PURE__ */ new Set(["PGRST116", "PGRST205"]);
|
|
28
|
-
var CONFLICT_CODES = /* @__PURE__ */ new Set(["23505"]);
|
|
28
|
+
var CONFLICT_CODES = /* @__PURE__ */ new Set(["23505", "40001", "55000"]);
|
|
29
29
|
var SESSION_MARKERS = [
|
|
30
30
|
"auth session missing",
|
|
31
31
|
"jwt expired",
|
|
@@ -183,8 +183,28 @@ var MEET_ROUTES = {
|
|
|
183
183
|
*/
|
|
184
184
|
record: "/api/v1/meet/record",
|
|
185
185
|
/** A host chooses what guests may read of ONE meeting (MRF-1). */
|
|
186
|
-
recordAccess: "/api/v1/meet/record/access"
|
|
186
|
+
recordAccess: "/api/v1/meet/record/access",
|
|
187
|
+
/**
|
|
188
|
+
* Meet wave 1 (0.8): bring every invitee up to date after a write — sends the
|
|
189
|
+
* invitation / update / cancellation emails with their `.ics` and re-queues
|
|
190
|
+
* reminders. Idempotent. `{meeting_id}` is substituted by `meetingPath`.
|
|
191
|
+
*/
|
|
192
|
+
meetingAnnounce: "/api/v1/meet/meetings/{meeting_id}/announce",
|
|
193
|
+
/** Put a one-off meeting on the caller's own Google Calendar (gated by the server). */
|
|
194
|
+
meetingGoogleCalendar: "/api/v1/meet/meetings/{meeting_id}/google-calendar",
|
|
195
|
+
/**
|
|
196
|
+
* Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
|
|
197
|
+
* unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
|
|
198
|
+
* Decided by the server from the meeting itself; a guest's pass rides
|
|
199
|
+
* `x-meet-room-token`.
|
|
200
|
+
*/
|
|
201
|
+
moderate: "/api/v1/meet/moderate",
|
|
202
|
+
/** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */
|
|
203
|
+
chat: "/api/v1/meet/chat"
|
|
187
204
|
};
|
|
205
|
+
function meetingPath(template, meetingId) {
|
|
206
|
+
return template.replace("{meeting_id}", encodeURIComponent(meetingId));
|
|
207
|
+
}
|
|
188
208
|
var ROOM_TOKEN_HEADER = "x-meet-room-token";
|
|
189
209
|
var REFRESH_RATIO = 0.8;
|
|
190
210
|
function readString(source, key) {
|
|
@@ -314,6 +334,19 @@ function createTokenClient(options) {
|
|
|
314
334
|
"The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry."
|
|
315
335
|
);
|
|
316
336
|
}
|
|
337
|
+
if (response.status === 410) {
|
|
338
|
+
let reason = "This meeting was cancelled.";
|
|
339
|
+
try {
|
|
340
|
+
const body = await response.json();
|
|
341
|
+
if (typeof body?.detail?.message === "string") reason = body.detail.message;
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
throw new MeetError(
|
|
345
|
+
"cancelled",
|
|
346
|
+
reason,
|
|
347
|
+
"Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one."
|
|
348
|
+
);
|
|
349
|
+
}
|
|
317
350
|
if (response.status === 423) {
|
|
318
351
|
throw new MeetError(
|
|
319
352
|
"room-locked",
|
|
@@ -515,12 +548,12 @@ function createMeetAi(options) {
|
|
|
515
548
|
async function dispatch(route, body, operation) {
|
|
516
549
|
const response = await send({ path: route, body, operation, requireAuth: true });
|
|
517
550
|
if (!response.ok) {
|
|
518
|
-
const
|
|
519
|
-
options.store.patch({ noteTakerRefusal:
|
|
551
|
+
const refusal2 = await refusalFor(response, operation);
|
|
552
|
+
options.store.patch({ noteTakerRefusal: refusal2 });
|
|
520
553
|
throw new MeetError(
|
|
521
554
|
response.status === 403 ? "forbidden" : "transport",
|
|
522
|
-
|
|
523
|
-
|
|
555
|
+
refusal2.reason,
|
|
556
|
+
refusal2.remedy
|
|
524
557
|
);
|
|
525
558
|
}
|
|
526
559
|
options.store.patch({ noteTakerRefusal: null });
|
|
@@ -558,11 +591,11 @@ function createMeetAi(options) {
|
|
|
558
591
|
...signal !== void 0 ? { signal } : {}
|
|
559
592
|
});
|
|
560
593
|
if (!response.ok) {
|
|
561
|
-
const
|
|
594
|
+
const refusal2 = await refusalFor(response, "meet.ai.ask");
|
|
562
595
|
throw new MeetError(
|
|
563
596
|
response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : "transport",
|
|
564
|
-
|
|
565
|
-
|
|
597
|
+
refusal2.reason,
|
|
598
|
+
refusal2.remedy
|
|
566
599
|
);
|
|
567
600
|
}
|
|
568
601
|
const body = response.body;
|
|
@@ -671,14 +704,14 @@ function noteTakerStatus(args) {
|
|
|
671
704
|
const row = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
|
|
672
705
|
const recorded = readState(row["state"]);
|
|
673
706
|
const detail = typeof row["reason"] === "string" && row["reason"].length > 0 ? row["reason"] : null;
|
|
674
|
-
const
|
|
707
|
+
const refusal2 = args.refusal ?? null;
|
|
675
708
|
if (recorded === null) {
|
|
676
|
-
return { state: "absent", detail, present, refusal };
|
|
709
|
+
return { state: "absent", detail, present, refusal: refusal2 };
|
|
677
710
|
}
|
|
678
711
|
if (recorded === "transcribing" && !present) {
|
|
679
|
-
return { state: "handoff_pending", detail, present, refusal };
|
|
712
|
+
return { state: "handoff_pending", detail, present, refusal: refusal2 };
|
|
680
713
|
}
|
|
681
|
-
return { state: recorded, detail, present, refusal };
|
|
714
|
+
return { state: recorded, detail, present, refusal: refusal2 };
|
|
682
715
|
}
|
|
683
716
|
function noteTakerLabel(status) {
|
|
684
717
|
switch (status.state) {
|
|
@@ -1091,7 +1124,11 @@ var TABLES = {
|
|
|
1091
1124
|
invites: "meet_call_invites",
|
|
1092
1125
|
recordings: "meet_recordings",
|
|
1093
1126
|
transcripts: "meet_transcript_segments",
|
|
1094
|
-
notes: "meet_notes"
|
|
1127
|
+
notes: "meet_notes",
|
|
1128
|
+
/** Wave 1 (0.8): who a meeting was sent to, and what they answered. */
|
|
1129
|
+
invitees: "meet_invitees",
|
|
1130
|
+
/** Wave 1 (0.8): one occurrence of a series cancelled or moved. */
|
|
1131
|
+
occurrenceOverrides: "meet_occurrence_overrides"
|
|
1095
1132
|
};
|
|
1096
1133
|
var RPCS = {
|
|
1097
1134
|
/** Atomic: create the invite row and stamp its expiry in one statement. */
|
|
@@ -1107,10 +1144,41 @@ var RPCS = {
|
|
|
1107
1144
|
admit: "meet_admit_participant",
|
|
1108
1145
|
setLock: "meet_set_lock",
|
|
1109
1146
|
endMeeting: "meet_end_meeting",
|
|
1110
|
-
recordConsent: "meet_record_consent"
|
|
1147
|
+
recordConsent: "meet_record_consent",
|
|
1148
|
+
/*
|
|
1149
|
+
* ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
|
|
1150
|
+
* authorized by the meeting's host or a real iam grant (editor to change,
|
|
1151
|
+
* admin = co-host to archive or name a co-host) — never an organization
|
|
1152
|
+
* match. After any of these writes, call `announceMeeting` so the server
|
|
1153
|
+
* sends invitations / updates / cancellations and re-queues reminders.
|
|
1154
|
+
*/
|
|
1155
|
+
/** Schedule with every setting at create; unset settings come from the knobs. */
|
|
1156
|
+
scheduleMeeting: "meet_schedule_meeting",
|
|
1157
|
+
/** Edit / reschedule — a JSON patch with a version compare-and-swap. */
|
|
1158
|
+
updateMeeting: "meet_update_meeting",
|
|
1159
|
+
/** Cancel is a state (`cancelled_at`); the link keeps resolving. */
|
|
1160
|
+
cancelMeeting: "meet_cancel_meeting",
|
|
1161
|
+
/** Archive (soft delete) — the invitations and exceptions cascade. */
|
|
1162
|
+
archiveMeeting: "meet_archive_meeting",
|
|
1163
|
+
restoreMeeting: "meet_restore_meeting",
|
|
1164
|
+
/** Cancel, move or restore ONE occurrence of a series by its original start. */
|
|
1165
|
+
setOccurrence: "meet_set_occurrence",
|
|
1166
|
+
addInvitees: "meet_add_invitees",
|
|
1167
|
+
setInviteeRole: "meet_set_invitee_role",
|
|
1168
|
+
removeInvitee: "meet_remove_invitee",
|
|
1169
|
+
/** A signed-in invitee answers for themselves. */
|
|
1170
|
+
respond: "meet_respond",
|
|
1171
|
+
/** The emailed RSVP link (no account): read and answer. */
|
|
1172
|
+
invitationByToken: "meet_invitation_by_token",
|
|
1173
|
+
respondByToken: "meet_respond_by_token",
|
|
1174
|
+
/** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */
|
|
1175
|
+
meetingOccurrences: "meet_meeting_occurrences",
|
|
1176
|
+
/** Every occurrence the reader can see between two instants — the Upcoming list. */
|
|
1177
|
+
occurrencesBetween: "meet_occurrences_between"
|
|
1111
1178
|
};
|
|
1112
1179
|
var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
|
|
1113
|
-
var MEETING_COLUMNS = "id,organization_id,room_name,slug,title,kind,host_user_id,scheduled_for,scheduled_duration_minutes,started_at,ended_at,locked,lobby_enabled,recording_policy,ai_enabled,metadata";
|
|
1180
|
+
var MEETING_COLUMNS = "id,organization_id,room_name,slug,title,kind,host_user_id,scheduled_for,scheduled_duration_minutes,started_at,ended_at,locked,lobby_enabled,recording_policy,ai_enabled,metadata,time_zone,agenda,recurrence_rule,join_before_host,cancelled_at,cancellation_reason,deleted_at,calendar_sequence,version";
|
|
1181
|
+
var INVITEE_COLUMNS = "id,meeting_id,invitee_user_id,email,display_name,role,rsvp_state,responded_at,rsvp_note,last_sent_sequence,last_sent_at";
|
|
1114
1182
|
var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
|
|
1115
1183
|
var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
|
|
1116
1184
|
var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
|
|
@@ -1208,6 +1276,138 @@ function projectNote(row) {
|
|
|
1208
1276
|
source: "record"
|
|
1209
1277
|
};
|
|
1210
1278
|
}
|
|
1279
|
+
var RSVP_STATES = ["needs_action", "accepted", "declined", "tentative"];
|
|
1280
|
+
var OCCURRENCE_STATES = ["scheduled", "moved", "cancelled"];
|
|
1281
|
+
function rsvpState(value) {
|
|
1282
|
+
return RSVP_STATES.includes(value) ? value : "needs_action";
|
|
1283
|
+
}
|
|
1284
|
+
function num(row, key) {
|
|
1285
|
+
const value = row[key];
|
|
1286
|
+
return typeof value === "number" ? value : null;
|
|
1287
|
+
}
|
|
1288
|
+
function when(row, key) {
|
|
1289
|
+
const value = row[key];
|
|
1290
|
+
if (value instanceof Date) return value.toISOString();
|
|
1291
|
+
return text(row, key);
|
|
1292
|
+
}
|
|
1293
|
+
function projectInvitee(row) {
|
|
1294
|
+
const operation = "meet.projectInvitee";
|
|
1295
|
+
const userId = text(row, "invitee_user_id");
|
|
1296
|
+
return {
|
|
1297
|
+
id: requireText(row, "id", operation),
|
|
1298
|
+
meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
|
|
1299
|
+
userId: userId === null ? null : asUserId(userId),
|
|
1300
|
+
email: text(row, "email"),
|
|
1301
|
+
displayName: text(row, "display_name"),
|
|
1302
|
+
role: text(row, "role") === "cohost" ? "cohost" : "invitee",
|
|
1303
|
+
rsvpState: rsvpState(text(row, "rsvp_state")),
|
|
1304
|
+
respondedAt: when(row, "responded_at"),
|
|
1305
|
+
rsvpNote: text(row, "rsvp_note"),
|
|
1306
|
+
lastSentSequence: num(row, "last_sent_sequence"),
|
|
1307
|
+
lastSentAt: when(row, "last_sent_at")
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
function projectOccurrence(row) {
|
|
1311
|
+
const operation = "meet.projectOccurrence";
|
|
1312
|
+
const state = text(row, "state");
|
|
1313
|
+
return {
|
|
1314
|
+
meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
|
|
1315
|
+
originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
|
|
1316
|
+
occurrenceStart: when(row, "occurrence_start") ?? requireText(row, "occurrence_start", operation),
|
|
1317
|
+
durationMinutes: num(row, "duration_minutes"),
|
|
1318
|
+
// An unknown state is shown as scheduled, never dropped: the row is real.
|
|
1319
|
+
state: OCCURRENCE_STATES.includes(state) ? state : "scheduled",
|
|
1320
|
+
sequence: num(row, "sequence") ?? 0
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
function projectUpcoming(row) {
|
|
1324
|
+
const operation = "meet.projectUpcoming";
|
|
1325
|
+
const kind = text(row, "kind");
|
|
1326
|
+
const myRole = text(row, "my_role");
|
|
1327
|
+
const myRsvp = text(row, "my_rsvp");
|
|
1328
|
+
return {
|
|
1329
|
+
...projectOccurrence(row),
|
|
1330
|
+
organizationId: asOrganizationId(requireText(row, "organization_id", operation)),
|
|
1331
|
+
title: text(row, "title") ?? "Meeting",
|
|
1332
|
+
slug: requireText(row, "slug", operation),
|
|
1333
|
+
kind: kind === "scheduled" || kind === "recurring" ? kind : "instant",
|
|
1334
|
+
hostUserId: asUserId(requireText(row, "host_user_id", operation)),
|
|
1335
|
+
timeZone: text(row, "time_zone") ?? "UTC",
|
|
1336
|
+
meetingCancelled: bool(row, "meeting_cancelled", false),
|
|
1337
|
+
myRsvp: myRsvp === null ? null : rsvpState(myRsvp),
|
|
1338
|
+
myRole: myRole === "host" || myRole === "cohost" || myRole === "invitee" ? myRole : null
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
function projectOverride(row) {
|
|
1342
|
+
const operation = "meet.projectOverride";
|
|
1343
|
+
return {
|
|
1344
|
+
id: requireText(row, "id", operation),
|
|
1345
|
+
meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
|
|
1346
|
+
originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
|
|
1347
|
+
state: text(row, "state") === "moved" ? "moved" : "cancelled",
|
|
1348
|
+
newStart: when(row, "new_start"),
|
|
1349
|
+
newDurationMinutes: num(row, "new_duration_minutes"),
|
|
1350
|
+
reason: text(row, "reason"),
|
|
1351
|
+
calendarSequence: num(row, "calendar_sequence") ?? 0,
|
|
1352
|
+
deletedAt: when(row, "deleted_at")
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
function projectRsvpInvitation(body) {
|
|
1356
|
+
const operation = "meet.projectRsvpInvitation";
|
|
1357
|
+
if (typeof body !== "object" || body === null) {
|
|
1358
|
+
throw invalidResponse(operation, "the RSVP door returned no object");
|
|
1359
|
+
}
|
|
1360
|
+
const row = body;
|
|
1361
|
+
if (row["ok"] !== true) {
|
|
1362
|
+
return {
|
|
1363
|
+
ok: false,
|
|
1364
|
+
message: text(row, "message") ?? "This invitation link is no longer valid \u2014 ask the host to send it again."
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
const meeting = row["meeting"] ?? {};
|
|
1368
|
+
const invitee = row["invitee"] ?? {};
|
|
1369
|
+
return {
|
|
1370
|
+
ok: true,
|
|
1371
|
+
meeting: {
|
|
1372
|
+
title: text(meeting, "title") ?? "Meeting",
|
|
1373
|
+
slug: requireText(meeting, "slug", operation),
|
|
1374
|
+
scheduledFor: when(meeting, "scheduled_for"),
|
|
1375
|
+
durationMinutes: num(meeting, "duration_minutes"),
|
|
1376
|
+
timeZone: text(meeting, "time_zone") ?? "UTC",
|
|
1377
|
+
agenda: text(meeting, "agenda"),
|
|
1378
|
+
recurrenceRule: text(meeting, "recurrence_rule"),
|
|
1379
|
+
cancelledAt: when(meeting, "cancelled_at"),
|
|
1380
|
+
cancellationReason: text(meeting, "cancellation_reason"),
|
|
1381
|
+
endedAt: when(meeting, "ended_at")
|
|
1382
|
+
},
|
|
1383
|
+
invitee: {
|
|
1384
|
+
displayName: text(invitee, "display_name"),
|
|
1385
|
+
email: text(invitee, "email"),
|
|
1386
|
+
rsvpState: rsvpState(text(invitee, "rsvp_state")),
|
|
1387
|
+
respondedAt: when(invitee, "responded_at")
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
function settingsPatch(settings) {
|
|
1392
|
+
const patch = {};
|
|
1393
|
+
if (settings.lobbyEnabled !== void 0) patch["lobby_enabled"] = settings.lobbyEnabled;
|
|
1394
|
+
if (settings.recordingPolicy !== void 0) patch["recording_policy"] = settings.recordingPolicy;
|
|
1395
|
+
if (settings.aiEnabled !== void 0) patch["ai_enabled"] = settings.aiEnabled;
|
|
1396
|
+
if (settings.joinBeforeHost !== void 0) patch["join_before_host"] = settings.joinBeforeHost;
|
|
1397
|
+
return patch;
|
|
1398
|
+
}
|
|
1399
|
+
function changesPatch(changes) {
|
|
1400
|
+
const patch = settingsPatch(changes);
|
|
1401
|
+
if (changes.title !== void 0) patch["title"] = changes.title;
|
|
1402
|
+
if (changes.agenda !== void 0) patch["agenda"] = changes.agenda;
|
|
1403
|
+
if (changes.timeZone !== void 0) patch["time_zone"] = changes.timeZone;
|
|
1404
|
+
if (changes.scheduledFor !== void 0) patch["scheduled_for"] = changes.scheduledFor;
|
|
1405
|
+
if (changes.scheduledDurationMinutes !== void 0) {
|
|
1406
|
+
patch["scheduled_duration_minutes"] = changes.scheduledDurationMinutes;
|
|
1407
|
+
}
|
|
1408
|
+
if (changes.recurrenceRule !== void 0) patch["recurrence_rule"] = changes.recurrenceRule;
|
|
1409
|
+
return patch;
|
|
1410
|
+
}
|
|
1211
1411
|
function createMeetRepository(options) {
|
|
1212
1412
|
const db = options.client.schema(MEET_SCHEMA);
|
|
1213
1413
|
async function guardVoid(operation, run) {
|
|
@@ -1277,7 +1477,16 @@ function createMeetRepository(options) {
|
|
|
1277
1477
|
lobbyEnabled: bool(row, "lobby_enabled", true),
|
|
1278
1478
|
recordingPolicy: policy === "disabled" || policy === "always-on" ? policy : "host-controlled",
|
|
1279
1479
|
aiEnabled: bool(row, "ai_enabled", true),
|
|
1280
|
-
metadata: typeof metadata === "object" && metadata !== null && !Array.isArray(metadata) ? metadata : {}
|
|
1480
|
+
metadata: typeof metadata === "object" && metadata !== null && !Array.isArray(metadata) ? metadata : {},
|
|
1481
|
+
timeZone: text(row, "time_zone") ?? "UTC",
|
|
1482
|
+
agenda: text(row, "agenda"),
|
|
1483
|
+
recurrenceRule: text(row, "recurrence_rule"),
|
|
1484
|
+
joinBeforeHost: bool(row, "join_before_host", true),
|
|
1485
|
+
cancelledAt: text(row, "cancelled_at"),
|
|
1486
|
+
cancellationReason: text(row, "cancellation_reason"),
|
|
1487
|
+
deletedAt: text(row, "deleted_at"),
|
|
1488
|
+
calendarSequence: typeof row["calendar_sequence"] === "number" ? row["calendar_sequence"] : 0,
|
|
1489
|
+
version: typeof row["version"] === "number" ? row["version"] : 1
|
|
1281
1490
|
};
|
|
1282
1491
|
}
|
|
1283
1492
|
function projectRecording(row) {
|
|
@@ -1364,6 +1573,170 @@ function createMeetRepository(options) {
|
|
|
1364
1573
|
projectNote,
|
|
1365
1574
|
projectRecording,
|
|
1366
1575
|
projectAttendee,
|
|
1576
|
+
projectInvitee,
|
|
1577
|
+
async scheduleMeeting(input) {
|
|
1578
|
+
requireOrg(input.organizationId, "meet.scheduleMeeting");
|
|
1579
|
+
const row = await guard(
|
|
1580
|
+
"meet.scheduleMeeting",
|
|
1581
|
+
() => db.rpc(RPCS.scheduleMeeting, {
|
|
1582
|
+
p_organization_id: input.organizationId,
|
|
1583
|
+
p_host_user_id: input.hostUserId,
|
|
1584
|
+
p_title: input.title,
|
|
1585
|
+
p_scheduled_for: input.scheduledFor,
|
|
1586
|
+
p_time_zone: input.timeZone,
|
|
1587
|
+
p_duration_minutes: input.durationMinutes ?? null,
|
|
1588
|
+
p_agenda: input.agenda ?? null,
|
|
1589
|
+
p_recurrence_rule: input.recurrenceRule ?? null,
|
|
1590
|
+
p_settings: settingsPatch(input.settings ?? {})
|
|
1591
|
+
})
|
|
1592
|
+
);
|
|
1593
|
+
return projectMeeting(row);
|
|
1594
|
+
},
|
|
1595
|
+
async updateMeeting(meetingId, changes, options2 = {}) {
|
|
1596
|
+
const row = await guard(
|
|
1597
|
+
"meet.updateMeeting",
|
|
1598
|
+
() => db.rpc(RPCS.updateMeeting, {
|
|
1599
|
+
p_meeting_id: meetingId,
|
|
1600
|
+
p_changes: changesPatch(changes),
|
|
1601
|
+
p_expected_version: options2.expectedVersion ?? null,
|
|
1602
|
+
p_by_user_id: options2.byUserId ?? null
|
|
1603
|
+
})
|
|
1604
|
+
);
|
|
1605
|
+
return projectMeeting(row);
|
|
1606
|
+
},
|
|
1607
|
+
async cancelMeeting(meetingId, reason, byUserId = null) {
|
|
1608
|
+
const row = await guard(
|
|
1609
|
+
"meet.cancelMeeting",
|
|
1610
|
+
() => db.rpc(RPCS.cancelMeeting, {
|
|
1611
|
+
p_meeting_id: meetingId,
|
|
1612
|
+
p_reason: reason,
|
|
1613
|
+
p_by_user_id: byUserId
|
|
1614
|
+
})
|
|
1615
|
+
);
|
|
1616
|
+
return projectMeeting(row);
|
|
1617
|
+
},
|
|
1618
|
+
async archiveMeeting(meetingId, byUserId = null) {
|
|
1619
|
+
const row = await guard(
|
|
1620
|
+
"meet.archiveMeeting",
|
|
1621
|
+
() => db.rpc(RPCS.archiveMeeting, { p_meeting_id: meetingId, p_by_user_id: byUserId })
|
|
1622
|
+
);
|
|
1623
|
+
return projectMeeting(row);
|
|
1624
|
+
},
|
|
1625
|
+
async restoreMeeting(meetingId, byUserId = null) {
|
|
1626
|
+
const row = await guard(
|
|
1627
|
+
"meet.restoreMeeting",
|
|
1628
|
+
() => db.rpc(RPCS.restoreMeeting, { p_meeting_id: meetingId, p_by_user_id: byUserId })
|
|
1629
|
+
);
|
|
1630
|
+
return projectMeeting(row);
|
|
1631
|
+
},
|
|
1632
|
+
async setOccurrence(args) {
|
|
1633
|
+
const row = await guard(
|
|
1634
|
+
"meet.setOccurrence",
|
|
1635
|
+
() => db.rpc(RPCS.setOccurrence, {
|
|
1636
|
+
p_meeting_id: args.meetingId,
|
|
1637
|
+
p_original_start: args.originalStart,
|
|
1638
|
+
p_action: args.action,
|
|
1639
|
+
p_new_start: args.newStart ?? null,
|
|
1640
|
+
p_new_duration_minutes: args.newDurationMinutes ?? null,
|
|
1641
|
+
p_reason: args.reason ?? null,
|
|
1642
|
+
p_by_user_id: args.byUserId ?? null
|
|
1643
|
+
})
|
|
1644
|
+
);
|
|
1645
|
+
return projectOverride(row);
|
|
1646
|
+
},
|
|
1647
|
+
async meetingOccurrences(meetingId, { from, to, limit = 200 }) {
|
|
1648
|
+
const rows = await guard(
|
|
1649
|
+
"meet.meetingOccurrences",
|
|
1650
|
+
() => db.rpc(RPCS.meetingOccurrences, {
|
|
1651
|
+
p_meeting_id: meetingId,
|
|
1652
|
+
p_from: from,
|
|
1653
|
+
p_to: to,
|
|
1654
|
+
p_limit: limit
|
|
1655
|
+
})
|
|
1656
|
+
);
|
|
1657
|
+
return rows.map(projectOccurrence);
|
|
1658
|
+
},
|
|
1659
|
+
async occurrencesBetween({ from, to, organizationId = null, limit = 500 }) {
|
|
1660
|
+
const rows = await guard(
|
|
1661
|
+
"meet.occurrencesBetween",
|
|
1662
|
+
() => db.rpc(RPCS.occurrencesBetween, {
|
|
1663
|
+
p_from: from,
|
|
1664
|
+
p_to: to,
|
|
1665
|
+
p_organization_id: organizationId,
|
|
1666
|
+
p_limit: limit
|
|
1667
|
+
})
|
|
1668
|
+
);
|
|
1669
|
+
return rows.map(projectUpcoming);
|
|
1670
|
+
},
|
|
1671
|
+
async invitees(meetingId) {
|
|
1672
|
+
const response = await db.from(TABLES.invitees).select(INVITEE_COLUMNS).eq("meeting_id", meetingId).is("deleted_at", null).order("created_at", { ascending: true }).order("id", { ascending: true });
|
|
1673
|
+
if (response.error !== null) throw normalizeMeetError(response.error, "meet.invitees");
|
|
1674
|
+
return (response.data ?? []).map(projectInvitee);
|
|
1675
|
+
},
|
|
1676
|
+
async addInvitees(meetingId, people, byUserId = null) {
|
|
1677
|
+
const rows = await guard(
|
|
1678
|
+
"meet.addInvitees",
|
|
1679
|
+
() => db.rpc(RPCS.addInvitees, {
|
|
1680
|
+
p_meeting_id: meetingId,
|
|
1681
|
+
p_invitees: people.map((person) => ({
|
|
1682
|
+
...person.userId !== void 0 ? { user_id: person.userId } : {},
|
|
1683
|
+
...person.email !== void 0 ? { email: person.email } : {},
|
|
1684
|
+
...person.displayName !== void 0 ? { display_name: person.displayName } : {},
|
|
1685
|
+
...person.role !== void 0 ? { role: person.role } : {}
|
|
1686
|
+
})),
|
|
1687
|
+
p_by_user_id: byUserId
|
|
1688
|
+
})
|
|
1689
|
+
);
|
|
1690
|
+
return rows.map(projectInvitee);
|
|
1691
|
+
},
|
|
1692
|
+
async setInviteeRole(inviteeId, role, byUserId = null) {
|
|
1693
|
+
const row = await guard(
|
|
1694
|
+
"meet.setInviteeRole",
|
|
1695
|
+
() => db.rpc(RPCS.setInviteeRole, {
|
|
1696
|
+
p_invitee_id: inviteeId,
|
|
1697
|
+
p_role: role,
|
|
1698
|
+
p_by_user_id: byUserId
|
|
1699
|
+
})
|
|
1700
|
+
);
|
|
1701
|
+
return projectInvitee(row);
|
|
1702
|
+
},
|
|
1703
|
+
async removeInvitee(inviteeId, byUserId = null) {
|
|
1704
|
+
const row = await guard(
|
|
1705
|
+
"meet.removeInvitee",
|
|
1706
|
+
() => db.rpc(RPCS.removeInvitee, { p_invitee_id: inviteeId, p_by_user_id: byUserId })
|
|
1707
|
+
);
|
|
1708
|
+
return projectInvitee(row);
|
|
1709
|
+
},
|
|
1710
|
+
async respond(meetingId, answer, note = null) {
|
|
1711
|
+
const row = await guard(
|
|
1712
|
+
"meet.respond",
|
|
1713
|
+
() => db.rpc(RPCS.respond, {
|
|
1714
|
+
p_meeting_id: meetingId,
|
|
1715
|
+
p_response: answer,
|
|
1716
|
+
p_note: note,
|
|
1717
|
+
p_by_user_id: null
|
|
1718
|
+
})
|
|
1719
|
+
);
|
|
1720
|
+
return projectInvitee(row);
|
|
1721
|
+
},
|
|
1722
|
+
async invitationByToken(secret) {
|
|
1723
|
+
const body = await guard(
|
|
1724
|
+
"meet.invitationByToken",
|
|
1725
|
+
() => db.rpc(RPCS.invitationByToken, { p_secret: secret })
|
|
1726
|
+
);
|
|
1727
|
+
return projectRsvpInvitation(body);
|
|
1728
|
+
},
|
|
1729
|
+
async respondByToken(secret, answer, note = null) {
|
|
1730
|
+
const body = await guard(
|
|
1731
|
+
"meet.respondByToken",
|
|
1732
|
+
() => db.rpc(RPCS.respondByToken, {
|
|
1733
|
+
p_secret: secret,
|
|
1734
|
+
p_response: answer,
|
|
1735
|
+
p_note: note
|
|
1736
|
+
})
|
|
1737
|
+
);
|
|
1738
|
+
return projectRsvpInvitation(body);
|
|
1739
|
+
},
|
|
1367
1740
|
async attendees(meetingId) {
|
|
1368
1741
|
const response = await db.from(TABLES.participants).select(ATTENDEE_COLUMNS).eq("meeting_id", meetingId).order("joined_at", { ascending: true }).order("id", { ascending: true });
|
|
1369
1742
|
if (response.error !== null) throw normalizeMeetError(response.error, "meet.attendees");
|
|
@@ -1545,7 +1918,19 @@ var ROOM_EVENTS = {
|
|
|
1545
1918
|
* production 2026-09-09 the guest lane showed no indicator at all while the
|
|
1546
1919
|
* host recorded (MRI-C8).
|
|
1547
1920
|
*/
|
|
1548
|
-
recording: "matrx.meet.recording"
|
|
1921
|
+
recording: "matrx.meet.recording",
|
|
1922
|
+
/**
|
|
1923
|
+
* A chat message to everyone was SAVED (Meet wave 2). Payload
|
|
1924
|
+
* `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
|
|
1925
|
+
*/
|
|
1926
|
+
chat: "matrx.meet.chat",
|
|
1927
|
+
/**
|
|
1928
|
+
* A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`:
|
|
1929
|
+
* `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the
|
|
1930
|
+
* person concerned; `lower-hands {identities: [...] | "*"}` · `role {identity,
|
|
1931
|
+
* role}` — to everyone.
|
|
1932
|
+
*/
|
|
1933
|
+
moderation: "matrx.meet.moderation"
|
|
1549
1934
|
};
|
|
1550
1935
|
function isRoomEventTopic(topic) {
|
|
1551
1936
|
return topic.startsWith(ROOM_EVENT_PREFIX);
|
|
@@ -1581,6 +1966,10 @@ function applyRoomEvent(args) {
|
|
|
1581
1966
|
return applyEnded(store, payload);
|
|
1582
1967
|
case ROOM_EVENTS.recording:
|
|
1583
1968
|
return applyRecording(store, payload);
|
|
1969
|
+
case ROOM_EVENTS.chat:
|
|
1970
|
+
return applyChat(store, payload);
|
|
1971
|
+
case ROOM_EVENTS.moderation:
|
|
1972
|
+
return applyModeration(store, payload, now);
|
|
1584
1973
|
default:
|
|
1585
1974
|
return ignored(`${topic} is not known to this version of @ai-matrx/meet`);
|
|
1586
1975
|
}
|
|
@@ -1672,6 +2061,101 @@ function applyEnded(store, payload) {
|
|
|
1672
2061
|
store.patch({ meeting: { ...meeting, endedAt } });
|
|
1673
2062
|
return HANDLED;
|
|
1674
2063
|
}
|
|
2064
|
+
function chatFromRow(row) {
|
|
2065
|
+
const id = text2(row["id"]);
|
|
2066
|
+
const identity = text2(row["sender_identity"]);
|
|
2067
|
+
const body = text2(row["body"]);
|
|
2068
|
+
if (id === null || identity === null || body === null) return null;
|
|
2069
|
+
return {
|
|
2070
|
+
id,
|
|
2071
|
+
identity: asParticipantIdentity(identity),
|
|
2072
|
+
displayName: text2(row["sender_name"]) ?? "Participant",
|
|
2073
|
+
text: body,
|
|
2074
|
+
at: text2(row["created_at"]) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2075
|
+
// `false` only from a server whose chat table has not landed: the room heard
|
|
2076
|
+
// it, nobody will see it later — and the line says so.
|
|
2077
|
+
saved: row["saved"] !== false
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
function applyChat(store, payload) {
|
|
2081
|
+
const row = object(payload["message"]);
|
|
2082
|
+
if (row === null) return ignored("a chat event with no `message` row");
|
|
2083
|
+
const meeting = store.snapshot().meeting;
|
|
2084
|
+
if (meeting !== null && text2(row["meeting_id"]) !== null && row["meeting_id"] !== meeting.id) {
|
|
2085
|
+
return ignored("a chat message for a different meeting");
|
|
2086
|
+
}
|
|
2087
|
+
const message = chatFromRow(row);
|
|
2088
|
+
if (message === null) return ignored("a chat row with no id, sender or body");
|
|
2089
|
+
store.addChat(message);
|
|
2090
|
+
return HANDLED;
|
|
2091
|
+
}
|
|
2092
|
+
function moderationText(kind, payload) {
|
|
2093
|
+
const by = text2(payload["by"]) ?? "The host";
|
|
2094
|
+
switch (kind) {
|
|
2095
|
+
case "muted": {
|
|
2096
|
+
const source = text2(payload["source"]) ?? "microphone";
|
|
2097
|
+
return source === "microphone" ? `${by} muted you.` : source === "camera" ? `${by} turned off your camera.` : `${by} stopped your screen share.`;
|
|
2098
|
+
}
|
|
2099
|
+
case "ask-unmute":
|
|
2100
|
+
return `${by} is asking you to unmute.`;
|
|
2101
|
+
case "removed":
|
|
2102
|
+
return `${by} removed you from this meeting.`;
|
|
2103
|
+
default:
|
|
2104
|
+
return null;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
function applyModeration(store, payload, now) {
|
|
2108
|
+
const snapshot = store.snapshot();
|
|
2109
|
+
const meetingId = text2(payload["meeting_id"]);
|
|
2110
|
+
if (snapshot.meeting !== null && meetingId !== null && meetingId !== snapshot.meeting.id) {
|
|
2111
|
+
return ignored("a moderation event for a different meeting");
|
|
2112
|
+
}
|
|
2113
|
+
const kind = text2(payload["kind"]);
|
|
2114
|
+
switch (kind) {
|
|
2115
|
+
case "lower-hands": {
|
|
2116
|
+
const raw = payload["identities"];
|
|
2117
|
+
const everyone = raw === "*";
|
|
2118
|
+
const named = new Set(
|
|
2119
|
+
Array.isArray(raw) ? raw.filter((value) => typeof value === "string") : []
|
|
2120
|
+
);
|
|
2121
|
+
for (const participant of snapshot.participants) {
|
|
2122
|
+
if (participant.handRaised && (everyone || named.has(participant.identity))) {
|
|
2123
|
+
store.setHand(participant.identity, false);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
return HANDLED;
|
|
2127
|
+
}
|
|
2128
|
+
case "role": {
|
|
2129
|
+
const identity = text2(payload["identity"]);
|
|
2130
|
+
const role = text2(payload["role"]);
|
|
2131
|
+
if (identity === null || role === null || !ROLES.includes(role)) {
|
|
2132
|
+
return ignored("a role event with no identity or an unknown role");
|
|
2133
|
+
}
|
|
2134
|
+
if (identity === snapshot.localIdentity) {
|
|
2135
|
+
store.patch({
|
|
2136
|
+
grants: { ...snapshot.grants, role },
|
|
2137
|
+
moderationNotice: {
|
|
2138
|
+
kind: "role",
|
|
2139
|
+
text: role === "host" ? "You are now the host." : role === "cohost" ? "You are now a co-host." : "You are no longer a co-host.",
|
|
2140
|
+
at: now()
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
return HANDLED;
|
|
2145
|
+
}
|
|
2146
|
+
case "muted":
|
|
2147
|
+
case "ask-unmute":
|
|
2148
|
+
case "removed": {
|
|
2149
|
+
const sentence = moderationText(kind, payload);
|
|
2150
|
+
if (sentence === null) return ignored("a moderation event with no sentence");
|
|
2151
|
+
store.patch({ moderationNotice: { kind, text: sentence, at: now() } });
|
|
2152
|
+
return HANDLED;
|
|
2153
|
+
}
|
|
2154
|
+
default:
|
|
2155
|
+
return ignored(`a moderation kind this version does not know: ${String(kind)}`);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
var ROLES = ["host", "cohost", "participant", "guest"];
|
|
1675
2159
|
|
|
1676
2160
|
// src/core/devices.ts
|
|
1677
2161
|
var KINDS = ["audioinput", "videoinput", "audiooutput"];
|
|
@@ -1873,8 +2357,8 @@ function callStateLabel(state, outgoing) {
|
|
|
1873
2357
|
return "Call";
|
|
1874
2358
|
}
|
|
1875
2359
|
}
|
|
1876
|
-
function countLabel(
|
|
1877
|
-
return `${
|
|
2360
|
+
function countLabel(count2, singular, plural = `${singular}s`) {
|
|
2361
|
+
return `${count2} ${count2 === 1 ? singular : plural}`;
|
|
1878
2362
|
}
|
|
1879
2363
|
|
|
1880
2364
|
// src/core/invitations.ts
|
|
@@ -2004,7 +2488,8 @@ function computeStage(args) {
|
|
|
2004
2488
|
const ordered = orderParticipants(args.participants, args.activeSpeakers);
|
|
2005
2489
|
const sharer = ordered.find((participant) => participant.screenSharing) ?? null;
|
|
2006
2490
|
const pinnedParticipant = args.pinned === null ? null : ordered.find((participant) => participant.identity === args.pinned) ?? null;
|
|
2007
|
-
const
|
|
2491
|
+
const spotlit = args.spotlight == null ? null : ordered.find((participant) => participant.identity === args.spotlight) ?? null;
|
|
2492
|
+
const feature = sharer !== null ? sharer : pinnedParticipant !== null && args.layout !== "grid" ? pinnedParticipant : spotlit !== null ? spotlit : args.layout === "grid" ? null : ordered[0] ?? null;
|
|
2008
2493
|
const rest = feature === null ? ordered : ordered.filter((participant) => participant.identity !== feature.identity);
|
|
2009
2494
|
const pageCount = Math.max(1, Math.ceil(rest.length / pageSize));
|
|
2010
2495
|
const page = Math.min(Math.max(0, args.page ?? 0), pageCount - 1);
|
|
@@ -2018,11 +2503,11 @@ function computeStage(args) {
|
|
|
2018
2503
|
presenting: sharer !== null
|
|
2019
2504
|
};
|
|
2020
2505
|
}
|
|
2021
|
-
function gridColumns(
|
|
2022
|
-
if (
|
|
2023
|
-
if (
|
|
2024
|
-
if (
|
|
2025
|
-
if (
|
|
2506
|
+
function gridColumns(count2) {
|
|
2507
|
+
if (count2 <= 1) return 1;
|
|
2508
|
+
if (count2 <= 4) return 2;
|
|
2509
|
+
if (count2 <= 9) return 3;
|
|
2510
|
+
if (count2 <= 16) return 4;
|
|
2026
2511
|
return 5;
|
|
2027
2512
|
}
|
|
2028
2513
|
|
|
@@ -2030,8 +2515,7 @@ function gridColumns(count) {
|
|
|
2030
2515
|
var DATA_TOPICS = {
|
|
2031
2516
|
hand: "mx.hand",
|
|
2032
2517
|
reaction: "mx.reaction",
|
|
2033
|
-
chat: "mx.chat"
|
|
2034
|
-
hostAction: "mx.host"
|
|
2518
|
+
chat: "mx.chat"
|
|
2035
2519
|
};
|
|
2036
2520
|
var TRANSCRIPTION_TOPIC = "lk.transcription";
|
|
2037
2521
|
var TRANSCRIPTION_ATTRIBUTES = {
|
|
@@ -2092,11 +2576,48 @@ function snapshotParticipant(participant, isLocal) {
|
|
|
2092
2576
|
screenSharing: participant.isScreenShareEnabled
|
|
2093
2577
|
};
|
|
2094
2578
|
}
|
|
2579
|
+
var DEFAULT_AUDIO_PROCESSING = {
|
|
2580
|
+
noiseSuppression: true,
|
|
2581
|
+
echoCancellation: true,
|
|
2582
|
+
autoGainControl: true
|
|
2583
|
+
};
|
|
2584
|
+
function videoEffectsSupported() {
|
|
2585
|
+
const scope = globalThis;
|
|
2586
|
+
if (typeof scope.OffscreenCanvas === "undefined" || scope.document === void 0) return false;
|
|
2587
|
+
try {
|
|
2588
|
+
return scope.document.createElement("canvas").getContext("webgl2") !== null;
|
|
2589
|
+
} catch {
|
|
2590
|
+
return false;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
async function applyVideoEffect(target, effect) {
|
|
2594
|
+
const { Track } = await import("livekit-client");
|
|
2595
|
+
const track = target.localParticipant.getTrackPublication(Track.Source.Camera)?.track;
|
|
2596
|
+
if (track === void 0) return;
|
|
2597
|
+
try {
|
|
2598
|
+
if (effect.kind === "none") {
|
|
2599
|
+
await track.stopProcessor();
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
const processors = await import("@livekit/track-processors");
|
|
2603
|
+
const processor = effect.kind === "blur" ? processors.BackgroundBlur(effect.radius ?? 12) : processors.VirtualBackground(effect.imageUrl);
|
|
2604
|
+
await track.setProcessor(processor);
|
|
2605
|
+
} catch (error) {
|
|
2606
|
+
throw new MeetError(
|
|
2607
|
+
"media-unsupported",
|
|
2608
|
+
"The background effect could not start on this camera",
|
|
2609
|
+
"Background effects need a browser with WebGL2 (current Chrome, Edge, Firefox or Safari). Your video continues without the effect.",
|
|
2610
|
+
error
|
|
2611
|
+
);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2095
2614
|
var encoder = new TextEncoder();
|
|
2096
2615
|
var decoder = new TextDecoder();
|
|
2097
2616
|
function createLiveKitDriver(handlers, options = {}) {
|
|
2098
2617
|
let room = null;
|
|
2099
2618
|
let projectRoster = null;
|
|
2619
|
+
let audioCapture = DEFAULT_AUDIO_PROCESSING;
|
|
2620
|
+
let videoEffect = { kind: "none" };
|
|
2100
2621
|
async function load() {
|
|
2101
2622
|
if (room !== null) return room;
|
|
2102
2623
|
let module;
|
|
@@ -2125,7 +2646,9 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
2125
2646
|
handlers.participants([local, ...remotes]);
|
|
2126
2647
|
};
|
|
2127
2648
|
projectRoster = roster;
|
|
2128
|
-
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.
|
|
2649
|
+
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) => {
|
|
2650
|
+
handlers.roomMetadata?.(metadata ?? "");
|
|
2651
|
+
}).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
|
|
2129
2652
|
handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
|
|
2130
2653
|
}).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
|
|
2131
2654
|
handlers.quality(participant.identity, mapQuality(quality));
|
|
@@ -2202,6 +2725,7 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
2202
2725
|
throw normalizeDeviceError(error, "meet.connect");
|
|
2203
2726
|
}
|
|
2204
2727
|
projectRoster?.();
|
|
2728
|
+
handlers.roomMetadata?.(target.metadata ?? "");
|
|
2205
2729
|
},
|
|
2206
2730
|
async disconnect() {
|
|
2207
2731
|
if (room === null) return;
|
|
@@ -2216,14 +2740,19 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
2216
2740
|
},
|
|
2217
2741
|
async setMicrophoneEnabled(enabled) {
|
|
2218
2742
|
try {
|
|
2219
|
-
await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(
|
|
2743
|
+
await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(
|
|
2744
|
+
enabled,
|
|
2745
|
+
audioCapture
|
|
2746
|
+
);
|
|
2220
2747
|
} catch (error) {
|
|
2221
2748
|
throw normalizeDeviceError(error, "meet.setMicrophoneEnabled");
|
|
2222
2749
|
}
|
|
2223
2750
|
},
|
|
2224
2751
|
async setCameraEnabled(enabled) {
|
|
2225
2752
|
try {
|
|
2226
|
-
|
|
2753
|
+
const target = requireRoom("setCameraEnabled");
|
|
2754
|
+
await target.localParticipant.setCameraEnabled(enabled);
|
|
2755
|
+
if (enabled && videoEffect.kind !== "none") await applyVideoEffect(target, videoEffect);
|
|
2227
2756
|
} catch (error) {
|
|
2228
2757
|
throw normalizeDeviceError(error, "meet.setCameraEnabled");
|
|
2229
2758
|
}
|
|
@@ -2235,14 +2764,40 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
2235
2764
|
throw normalizeDeviceError(error, "meet.setScreenShareEnabled");
|
|
2236
2765
|
}
|
|
2237
2766
|
},
|
|
2238
|
-
async publishData(topic, payload) {
|
|
2767
|
+
async publishData(topic, payload, to) {
|
|
2239
2768
|
await requireRoom("publishData").localParticipant.publishData(
|
|
2240
2769
|
// `reliable` is not optional for a control plane: a dropped
|
|
2241
2770
|
// "hand lowered" leaves a hand up on every other screen forever.
|
|
2242
2771
|
encoder.encode(payload),
|
|
2243
|
-
{ reliable: true, topic }
|
|
2772
|
+
to === void 0 || to.length === 0 ? { reliable: true, topic } : { reliable: true, topic, destinationIdentities: [...to] }
|
|
2244
2773
|
);
|
|
2245
2774
|
},
|
|
2775
|
+
async setAudioProcessing(processing) {
|
|
2776
|
+
audioCapture = { ...audioCapture, ...processing };
|
|
2777
|
+
const target = room;
|
|
2778
|
+
if (target === null) return;
|
|
2779
|
+
const { Track } = await import("livekit-client");
|
|
2780
|
+
const publication = target.localParticipant.getTrackPublication(Track.Source.Microphone);
|
|
2781
|
+
const track = publication?.track;
|
|
2782
|
+
if (track === void 0 || publication?.isMuted === true) return;
|
|
2783
|
+
try {
|
|
2784
|
+
await track.restartTrack({
|
|
2785
|
+
...audioCapture,
|
|
2786
|
+
...target.getActiveDevice("audioinput") !== void 0 ? { deviceId: target.getActiveDevice("audioinput") } : {}
|
|
2787
|
+
});
|
|
2788
|
+
} catch (error) {
|
|
2789
|
+
throw normalizeDeviceError(error, "meet.setAudioProcessing");
|
|
2790
|
+
}
|
|
2791
|
+
},
|
|
2792
|
+
async setVideoEffect(effect) {
|
|
2793
|
+
videoEffect = effect;
|
|
2794
|
+
const target = room;
|
|
2795
|
+
if (target === null) return;
|
|
2796
|
+
await applyVideoEffect(target, effect);
|
|
2797
|
+
},
|
|
2798
|
+
supportsVideoEffects() {
|
|
2799
|
+
return videoEffectsSupported();
|
|
2800
|
+
},
|
|
2246
2801
|
attach(identity, kind, element) {
|
|
2247
2802
|
const target = requireRoom("attach");
|
|
2248
2803
|
const source = sources?.[kind];
|
|
@@ -2465,6 +3020,11 @@ function listenOnlyNotice(media) {
|
|
|
2465
3020
|
}
|
|
2466
3021
|
|
|
2467
3022
|
// src/core/store.ts
|
|
3023
|
+
var DEFAULT_EFFECTS = {
|
|
3024
|
+
noiseSuppression: true,
|
|
3025
|
+
background: "none",
|
|
3026
|
+
backgroundImage: null
|
|
3027
|
+
};
|
|
2468
3028
|
var NO_GRANTS = {
|
|
2469
3029
|
role: "participant",
|
|
2470
3030
|
canRecord: false,
|
|
@@ -2487,7 +3047,12 @@ var EMPTY = {
|
|
|
2487
3047
|
recordingSource: "local",
|
|
2488
3048
|
recordingId: null,
|
|
2489
3049
|
chat: [],
|
|
3050
|
+
chatLoaded: false,
|
|
2490
3051
|
reactions: [],
|
|
3052
|
+
spotlight: null,
|
|
3053
|
+
moderationNotice: null,
|
|
3054
|
+
actionNotice: null,
|
|
3055
|
+
effects: DEFAULT_EFFECTS,
|
|
2491
3056
|
transcript: [],
|
|
2492
3057
|
notes: [],
|
|
2493
3058
|
names: {},
|
|
@@ -2526,6 +3091,7 @@ function createMeetStore(initial = {}) {
|
|
|
2526
3091
|
setParticipants(participants) {
|
|
2527
3092
|
const names = learnNames(state.names, participants);
|
|
2528
3093
|
const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
|
|
3094
|
+
const handTimes = new Map(state.participants.map((p) => [p.identity, p.handRaisedAt ?? null]));
|
|
2529
3095
|
const quality = new Map(state.participants.map((p) => [p.identity, p.quality]));
|
|
2530
3096
|
commit({
|
|
2531
3097
|
...state,
|
|
@@ -2539,6 +3105,7 @@ function createMeetStore(initial = {}) {
|
|
|
2539
3105
|
// name book. `roster-names.test.ts` fails if a token ever gets past it.
|
|
2540
3106
|
displayName: displayNameFor(names, participant.identity, participant.displayName),
|
|
2541
3107
|
handRaised: hands.get(participant.identity) ?? participant.handRaised,
|
|
3108
|
+
handRaisedAt: handTimes.get(participant.identity) ?? participant.handRaisedAt ?? null,
|
|
2542
3109
|
quality: participant.quality === "unknown" ? quality.get(participant.identity) ?? "unknown" : participant.quality
|
|
2543
3110
|
}))
|
|
2544
3111
|
});
|
|
@@ -2561,11 +3128,17 @@ function createMeetStore(initial = {}) {
|
|
|
2561
3128
|
)
|
|
2562
3129
|
});
|
|
2563
3130
|
},
|
|
2564
|
-
setHand(identity, raised) {
|
|
3131
|
+
setHand(identity, raised, at) {
|
|
2565
3132
|
commit({
|
|
2566
3133
|
...state,
|
|
2567
3134
|
participants: state.participants.map(
|
|
2568
|
-
(participant) => participant.identity === identity ? {
|
|
3135
|
+
(participant) => participant.identity === identity ? {
|
|
3136
|
+
...participant,
|
|
3137
|
+
handRaised: raised,
|
|
3138
|
+
// A hand that is ALREADY up keeps its place in the queue when
|
|
3139
|
+
// it is re-announced (to a late joiner): the first time wins.
|
|
3140
|
+
handRaisedAt: raised ? participant.handRaised && participant.handRaisedAt != null ? participant.handRaisedAt : at ?? Date.now() : null
|
|
3141
|
+
} : participant
|
|
2569
3142
|
)
|
|
2570
3143
|
});
|
|
2571
3144
|
},
|
|
@@ -2575,6 +3148,31 @@ function createMeetStore(initial = {}) {
|
|
|
2575
3148
|
const names = learnName(state.names, message.identity, message.displayName);
|
|
2576
3149
|
commit({ ...state, chat: chat.slice(-CHAT_LIMIT), names });
|
|
2577
3150
|
},
|
|
3151
|
+
loadChat(messages) {
|
|
3152
|
+
const known = new Set(state.chat.map((message) => message.id));
|
|
3153
|
+
const merged = [...messages.filter((message) => !known.has(message.id)), ...state.chat].sort(
|
|
3154
|
+
(a, b) => a.at < b.at ? -1 : a.at > b.at ? 1 : 0
|
|
3155
|
+
);
|
|
3156
|
+
const names = learnNames(
|
|
3157
|
+
state.names,
|
|
3158
|
+
messages.map((message) => ({ identity: message.identity, displayName: message.displayName }))
|
|
3159
|
+
);
|
|
3160
|
+
commit({ ...state, chat: merged.slice(-CHAT_LIMIT), chatLoaded: true, names });
|
|
3161
|
+
},
|
|
3162
|
+
settleChat(localId, next) {
|
|
3163
|
+
const index = state.chat.findIndex((message) => message.id === localId);
|
|
3164
|
+
if (index === -1) return;
|
|
3165
|
+
const current = state.chat[index];
|
|
3166
|
+
let replacement;
|
|
3167
|
+
if ("failed" in next && typeof next.failed === "string" && !("id" in next)) {
|
|
3168
|
+
replacement = { ...current, failed: next.failed };
|
|
3169
|
+
} else {
|
|
3170
|
+
replacement = next;
|
|
3171
|
+
}
|
|
3172
|
+
const duplicate = replacement.id !== localId && state.chat.some((message) => message.id === replacement.id);
|
|
3173
|
+
const chat = duplicate ? state.chat.filter((message) => message.id !== localId) : state.chat.map((message, position) => position === index ? replacement : message);
|
|
3174
|
+
commit({ ...state, chat });
|
|
3175
|
+
},
|
|
2578
3176
|
addReaction(reaction) {
|
|
2579
3177
|
commit({ ...state, reactions: [...state.reactions, reaction] });
|
|
2580
3178
|
},
|
|
@@ -2665,7 +3263,7 @@ function createMeetStore(initial = {}) {
|
|
|
2665
3263
|
commit({ ...state, media: { ...state.media, [kind]: next } });
|
|
2666
3264
|
},
|
|
2667
3265
|
reset() {
|
|
2668
|
-
commit({ ...EMPTY });
|
|
3266
|
+
commit({ ...EMPTY, effects: state.effects });
|
|
2669
3267
|
}
|
|
2670
3268
|
};
|
|
2671
3269
|
}
|
|
@@ -2995,11 +3593,11 @@ function createMeetingSession(options) {
|
|
|
2995
3593
|
} catch {
|
|
2996
3594
|
body = null;
|
|
2997
3595
|
}
|
|
2998
|
-
const
|
|
3596
|
+
const refusal2 = readServerRefusal(body);
|
|
2999
3597
|
throw new MeetError(
|
|
3000
3598
|
response.status === 403 ? "forbidden" : "transport",
|
|
3001
|
-
|
|
3002
|
-
|
|
3599
|
+
refusal2?.reason ?? `meet.endForEveryone: the server answered ${response.status}`,
|
|
3600
|
+
refusal2?.remedy !== void 0 && refusal2.remedy.length > 0 ? refusal2.remedy : "The meeting is still running. Only a host may end it, and the server re-checks that \u2014 being a host on this screen is not the decision."
|
|
3003
3601
|
);
|
|
3004
3602
|
}
|
|
3005
3603
|
let payload = null;
|
|
@@ -3127,6 +3725,57 @@ function fileDownloadPath(fileId) {
|
|
|
3127
3725
|
return `/files/${encodeURIComponent(fileId)}/download?inline=1`;
|
|
3128
3726
|
}
|
|
3129
3727
|
|
|
3728
|
+
// src/core/management.ts
|
|
3729
|
+
async function refusal(response, operation) {
|
|
3730
|
+
let body = null;
|
|
3731
|
+
try {
|
|
3732
|
+
body = await response.json();
|
|
3733
|
+
} catch {
|
|
3734
|
+
body = null;
|
|
3735
|
+
}
|
|
3736
|
+
const said = readServerRefusal(body);
|
|
3737
|
+
return new MeetError(
|
|
3738
|
+
response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
|
|
3739
|
+
`${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
|
|
3740
|
+
said?.remedy || "The meeting itself is saved \u2014 only the telling failed. Try again; the call is idempotent."
|
|
3741
|
+
);
|
|
3742
|
+
}
|
|
3743
|
+
function count(body, key) {
|
|
3744
|
+
const value = body[key];
|
|
3745
|
+
return typeof value === "number" ? value : 0;
|
|
3746
|
+
}
|
|
3747
|
+
async function announceMeeting(api, meetingId) {
|
|
3748
|
+
const operation = "meet.announceMeeting";
|
|
3749
|
+
const response = await api.request({
|
|
3750
|
+
path: meetingPath(MEET_ROUTES.meetingAnnounce, meetingId),
|
|
3751
|
+
method: "POST",
|
|
3752
|
+
requireAuth: true,
|
|
3753
|
+
operation
|
|
3754
|
+
});
|
|
3755
|
+
if (!response.ok) throw await refusal(response, operation);
|
|
3756
|
+
const body = await response.json();
|
|
3757
|
+
return {
|
|
3758
|
+
invitations: count(body, "invitations"),
|
|
3759
|
+
updates: count(body, "updates"),
|
|
3760
|
+
cancellations: count(body, "cancellations"),
|
|
3761
|
+
alreadyCurrent: count(body, "already_current"),
|
|
3762
|
+
remindersQueued: count(body, "reminders_queued"),
|
|
3763
|
+
remindersCancelled: count(body, "reminders_cancelled")
|
|
3764
|
+
};
|
|
3765
|
+
}
|
|
3766
|
+
async function pushMeetingToGoogleCalendar(api, meetingId, target) {
|
|
3767
|
+
const operation = "meet.pushMeetingToGoogleCalendar";
|
|
3768
|
+
const response = await api.request({
|
|
3769
|
+
path: meetingPath(MEET_ROUTES.meetingGoogleCalendar, meetingId),
|
|
3770
|
+
method: "POST",
|
|
3771
|
+
requireAuth: true,
|
|
3772
|
+
operation,
|
|
3773
|
+
body: { connection_id: target.connectionId, calendar_id: target.calendarId ?? "primary" }
|
|
3774
|
+
});
|
|
3775
|
+
if (!response.ok) throw await refusal(response, operation);
|
|
3776
|
+
return await response.json();
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3130
3779
|
// src/core/room.ts
|
|
3131
3780
|
var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
|
|
3132
3781
|
var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
|
|
@@ -3246,7 +3895,37 @@ function createRoomEngine(options) {
|
|
|
3246
3895
|
const handlers = {
|
|
3247
3896
|
participants(list) {
|
|
3248
3897
|
const joinedAt = new Date(now()).toISOString();
|
|
3898
|
+
const before = new Set(store.snapshot().participants.map((p) => p.identity));
|
|
3249
3899
|
store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
|
|
3900
|
+
const snapshot = store.snapshot();
|
|
3901
|
+
const newcomer = snapshot.participants.some(
|
|
3902
|
+
(participant) => !participant.isLocal && !before.has(participant.identity)
|
|
3903
|
+
);
|
|
3904
|
+
const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
|
|
3905
|
+
if (newcomer && before.size > 0 && me?.handRaised === true) {
|
|
3906
|
+
void publish(DATA_TOPICS.hand, { raised: true, raisedAt: me.handRaisedAt ?? now() }).catch(
|
|
3907
|
+
() => void 0
|
|
3908
|
+
);
|
|
3909
|
+
}
|
|
3910
|
+
},
|
|
3911
|
+
roomMetadata(metadata) {
|
|
3912
|
+
let spotlight = null;
|
|
3913
|
+
if (metadata.length > 0) {
|
|
3914
|
+
try {
|
|
3915
|
+
const parsed = JSON.parse(metadata);
|
|
3916
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
3917
|
+
const value = parsed.spotlight;
|
|
3918
|
+
if (typeof value === "string" && value.length > 0) spotlight = asParticipantIdentity(value);
|
|
3919
|
+
}
|
|
3920
|
+
} catch {
|
|
3921
|
+
report(
|
|
3922
|
+
"warn",
|
|
3923
|
+
"meet: the room's metadata is not JSON, so no spotlight was read from it",
|
|
3924
|
+
"Only aidream writes room metadata; check services/meet/moderation.py."
|
|
3925
|
+
);
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
|
|
3250
3929
|
},
|
|
3251
3930
|
activeSpeakers(identities) {
|
|
3252
3931
|
store.setSpeaking(identities.map(asParticipantIdentity));
|
|
@@ -3350,7 +4029,11 @@ function createRoomEngine(options) {
|
|
|
3350
4029
|
const sender = asParticipantIdentity(from ?? "");
|
|
3351
4030
|
switch (topic) {
|
|
3352
4031
|
case DATA_TOPICS.hand:
|
|
3353
|
-
store.setHand(
|
|
4032
|
+
store.setHand(
|
|
4033
|
+
sender,
|
|
4034
|
+
envelope.raised === true,
|
|
4035
|
+
typeof envelope.raisedAt === "number" ? envelope.raisedAt : now()
|
|
4036
|
+
);
|
|
3354
4037
|
return;
|
|
3355
4038
|
case DATA_TOPICS.reaction:
|
|
3356
4039
|
if (typeof envelope.emoji === "string") {
|
|
@@ -3364,12 +4047,14 @@ function createRoomEngine(options) {
|
|
|
3364
4047
|
return;
|
|
3365
4048
|
case DATA_TOPICS.chat:
|
|
3366
4049
|
if (typeof envelope.text === "string") {
|
|
4050
|
+
const local = store.snapshot().localIdentity;
|
|
3367
4051
|
store.addChat({
|
|
3368
4052
|
id: envelope.id ?? `${sender}-${now()}`,
|
|
3369
4053
|
identity: sender,
|
|
3370
4054
|
displayName: envelope.name ?? sender,
|
|
3371
4055
|
text: envelope.text,
|
|
3372
|
-
at: envelope.at ?? new Date(now()).toISOString()
|
|
4056
|
+
at: envelope.at ?? new Date(now()).toISOString(),
|
|
4057
|
+
...typeof envelope.to === "string" && local !== null ? { to: local } : {}
|
|
3373
4058
|
});
|
|
3374
4059
|
}
|
|
3375
4060
|
return;
|
|
@@ -3415,6 +4100,24 @@ function createRoomEngine(options) {
|
|
|
3415
4100
|
store.patch({ phase: "connected", failure: null });
|
|
3416
4101
|
store.setLocalTrack("microphone", IDLE_TRACK);
|
|
3417
4102
|
store.setLocalTrack("camera", IDLE_TRACK);
|
|
4103
|
+
const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
|
|
4104
|
+
store.patch({ effects });
|
|
4105
|
+
if (!effects.noiseSuppression || effects.background !== "none") {
|
|
4106
|
+
try {
|
|
4107
|
+
await applyEffects(created, effects);
|
|
4108
|
+
} catch {
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
for (const kind of ["audioinput", "videoinput", "audiooutput"]) {
|
|
4112
|
+
const chosen = options.resolveDevice?.(kind) ?? null;
|
|
4113
|
+
if (chosen === null || chosen === "default") continue;
|
|
4114
|
+
try {
|
|
4115
|
+
await created.switchDevice(kind, chosen);
|
|
4116
|
+
} catch (error) {
|
|
4117
|
+
const normalized = normalizeDeviceError(error, `meet.switchDevice(${kind})`);
|
|
4118
|
+
report("warn", normalized.message, normalized.remedy);
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
3418
4121
|
if (request.mode === "video") {
|
|
3419
4122
|
await enableTrack(created, "camera", true);
|
|
3420
4123
|
}
|
|
@@ -3437,9 +4140,30 @@ function createRoomEngine(options) {
|
|
|
3437
4140
|
return state;
|
|
3438
4141
|
}
|
|
3439
4142
|
}
|
|
3440
|
-
async function publish(topic, body) {
|
|
4143
|
+
async function publish(topic, body, to) {
|
|
3441
4144
|
if (driver === null) return;
|
|
3442
|
-
await driver.publishData(topic, JSON.stringify(body));
|
|
4145
|
+
await driver.publishData(topic, JSON.stringify(body), to);
|
|
4146
|
+
}
|
|
4147
|
+
async function applyEffects(target, effects) {
|
|
4148
|
+
try {
|
|
4149
|
+
await target.setAudioProcessing?.({
|
|
4150
|
+
noiseSuppression: effects.noiseSuppression,
|
|
4151
|
+
echoCancellation: true,
|
|
4152
|
+
autoGainControl: true
|
|
4153
|
+
});
|
|
4154
|
+
} catch (error) {
|
|
4155
|
+
report("warn", `meet: noise suppression could not be applied: ${String(error)}`, "The microphone keeps working without it.");
|
|
4156
|
+
}
|
|
4157
|
+
if (target.setVideoEffect === void 0) return;
|
|
4158
|
+
const effect = effects.background === "blur" ? { kind: "blur" } : effects.background === "image" && effects.backgroundImage !== null ? { kind: "image", imageUrl: effects.backgroundImage } : { kind: "none" };
|
|
4159
|
+
if (effect.kind !== "none" && target.supportsVideoEffects?.() === false) return;
|
|
4160
|
+
try {
|
|
4161
|
+
await target.setVideoEffect(effect);
|
|
4162
|
+
} catch (error) {
|
|
4163
|
+
const normalized = normalizeMeetError(error, "meet.setVideoEffect");
|
|
4164
|
+
report("warn", normalized.message, normalized.remedy);
|
|
4165
|
+
throw normalized;
|
|
4166
|
+
}
|
|
3443
4167
|
}
|
|
3444
4168
|
return {
|
|
3445
4169
|
join(request) {
|
|
@@ -3495,7 +4219,7 @@ function createRoomEngine(options) {
|
|
|
3495
4219
|
async raiseHand(raised) {
|
|
3496
4220
|
const local = store.snapshot().localIdentity;
|
|
3497
4221
|
if (local !== null) store.setHand(local, raised);
|
|
3498
|
-
await publish(DATA_TOPICS.hand, { raised });
|
|
4222
|
+
await publish(DATA_TOPICS.hand, { raised, raisedAt: now() });
|
|
3499
4223
|
},
|
|
3500
4224
|
async react(emoji) {
|
|
3501
4225
|
const local = store.snapshot().localIdentity;
|
|
@@ -3505,32 +4229,41 @@ function createRoomEngine(options) {
|
|
|
3505
4229
|
}
|
|
3506
4230
|
await publish(DATA_TOPICS.reaction, { id, emoji });
|
|
3507
4231
|
},
|
|
3508
|
-
async
|
|
4232
|
+
async sendPrivateChat(to, text3) {
|
|
3509
4233
|
const trimmed = text3.trim();
|
|
3510
4234
|
if (trimmed.length === 0) return;
|
|
3511
4235
|
const local = store.snapshot().localIdentity;
|
|
3512
4236
|
const id = `${local ?? "local"}-${now()}`;
|
|
3513
4237
|
const at = new Date(now()).toISOString();
|
|
3514
4238
|
if (local !== null) {
|
|
3515
|
-
store.addChat({
|
|
3516
|
-
id,
|
|
3517
|
-
identity: local,
|
|
3518
|
-
displayName: identity.displayName,
|
|
3519
|
-
text: trimmed,
|
|
3520
|
-
at
|
|
3521
|
-
});
|
|
4239
|
+
store.addChat({ id, identity: local, displayName: identity.displayName, text: trimmed, at, to });
|
|
3522
4240
|
}
|
|
3523
|
-
await publish(
|
|
4241
|
+
await publish(
|
|
4242
|
+
DATA_TOPICS.chat,
|
|
4243
|
+
{ id, text: trimmed, at, name: identity.displayName, to },
|
|
4244
|
+
[to]
|
|
4245
|
+
);
|
|
3524
4246
|
},
|
|
3525
|
-
async
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
4247
|
+
async setEffects(next) {
|
|
4248
|
+
const previous = store.snapshot().effects;
|
|
4249
|
+
const merged = { ...previous, ...next };
|
|
4250
|
+
store.patch({ effects: merged });
|
|
4251
|
+
options.effectsStorage?.save(merged);
|
|
4252
|
+
if (driver === null) return;
|
|
4253
|
+
try {
|
|
4254
|
+
await applyEffects(driver, merged);
|
|
4255
|
+
} catch {
|
|
4256
|
+
const reverted = { ...merged, background: previous.background, backgroundImage: previous.backgroundImage };
|
|
4257
|
+
store.patch({ effects: reverted });
|
|
4258
|
+
options.effectsStorage?.save(reverted);
|
|
3532
4259
|
}
|
|
3533
|
-
|
|
4260
|
+
},
|
|
4261
|
+
supportsVideoEffects() {
|
|
4262
|
+
return driver?.supportsVideoEffects?.() ?? false;
|
|
4263
|
+
},
|
|
4264
|
+
async switchDevice(kind, deviceId) {
|
|
4265
|
+
if (driver === null) return;
|
|
4266
|
+
await driver.switchDevice(kind, deviceId);
|
|
3534
4267
|
},
|
|
3535
4268
|
pin(who) {
|
|
3536
4269
|
store.patch({ pinned: who, layout: who === null ? "grid" : "speaker" });
|
|
@@ -3561,6 +4294,192 @@ function hostUserIds(participants) {
|
|
|
3561
4294
|
return participants.filter((participant) => participant.role === "host" && participant.userId !== null).map((participant) => participant.userId);
|
|
3562
4295
|
}
|
|
3563
4296
|
|
|
4297
|
+
// src/core/effects.ts
|
|
4298
|
+
function coerce(value) {
|
|
4299
|
+
if (typeof value !== "object" || value === null) return DEFAULT_EFFECTS;
|
|
4300
|
+
const row = value;
|
|
4301
|
+
const background = row["background"] === "blur" || row["background"] === "image" ? row["background"] : "none";
|
|
4302
|
+
const backgroundImage = typeof row["backgroundImage"] === "string" ? row["backgroundImage"] : null;
|
|
4303
|
+
return {
|
|
4304
|
+
noiseSuppression: row["noiseSuppression"] !== false,
|
|
4305
|
+
background: background === "image" && backgroundImage === null ? "none" : background,
|
|
4306
|
+
backgroundImage
|
|
4307
|
+
};
|
|
4308
|
+
}
|
|
4309
|
+
function createWebEffectsStorage(key = "mx.meet.effects") {
|
|
4310
|
+
let memory = DEFAULT_EFFECTS;
|
|
4311
|
+
const storage = globalThis.localStorage;
|
|
4312
|
+
return {
|
|
4313
|
+
load() {
|
|
4314
|
+
if (storage === void 0) return memory;
|
|
4315
|
+
try {
|
|
4316
|
+
const raw = storage.getItem(key);
|
|
4317
|
+
return raw === null ? memory : coerce(JSON.parse(raw));
|
|
4318
|
+
} catch {
|
|
4319
|
+
return memory;
|
|
4320
|
+
}
|
|
4321
|
+
},
|
|
4322
|
+
save(effects) {
|
|
4323
|
+
memory = effects;
|
|
4324
|
+
try {
|
|
4325
|
+
storage?.setItem(key, JSON.stringify(effects));
|
|
4326
|
+
} catch {
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
};
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
// src/core/in-room.ts
|
|
4333
|
+
var MODERATION_ACTIONS = [
|
|
4334
|
+
"mute",
|
|
4335
|
+
"stop_video",
|
|
4336
|
+
"stop_screen",
|
|
4337
|
+
"mute_all",
|
|
4338
|
+
"ask_unmute",
|
|
4339
|
+
"ask_all_unmute",
|
|
4340
|
+
"spotlight",
|
|
4341
|
+
"lower_hand",
|
|
4342
|
+
"lower_all_hands",
|
|
4343
|
+
"make_cohost",
|
|
4344
|
+
"revoke_cohost",
|
|
4345
|
+
"transfer_host",
|
|
4346
|
+
"rename",
|
|
4347
|
+
"remove",
|
|
4348
|
+
"report"
|
|
4349
|
+
];
|
|
4350
|
+
function createInRoomClient(options) {
|
|
4351
|
+
const { api, store } = options;
|
|
4352
|
+
const now = options.now ?? (() => Date.now());
|
|
4353
|
+
function meetingId(operation) {
|
|
4354
|
+
const meeting = store.snapshot().meeting;
|
|
4355
|
+
if (meeting === null) {
|
|
4356
|
+
throw new MeetError(
|
|
4357
|
+
"not-found",
|
|
4358
|
+
`${operation}: this room has no meeting record`,
|
|
4359
|
+
"Moderation and saved chat belong to a meeting. An ad-hoc call has neither."
|
|
4360
|
+
);
|
|
4361
|
+
}
|
|
4362
|
+
return meeting.id;
|
|
4363
|
+
}
|
|
4364
|
+
function headers() {
|
|
4365
|
+
const token = options.resolveRoomToken();
|
|
4366
|
+
return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
|
|
4367
|
+
}
|
|
4368
|
+
async function refusal2(response, operation, remedy) {
|
|
4369
|
+
let body = null;
|
|
4370
|
+
try {
|
|
4371
|
+
body = await response.json();
|
|
4372
|
+
} catch {
|
|
4373
|
+
body = null;
|
|
4374
|
+
}
|
|
4375
|
+
const said = readServerRefusal(body);
|
|
4376
|
+
return new MeetError(
|
|
4377
|
+
response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
|
|
4378
|
+
said?.reason ?? `${operation}: the server answered ${response.status}`,
|
|
4379
|
+
said?.remedy !== void 0 && said.remedy.length > 0 ? said.remedy : remedy
|
|
4380
|
+
);
|
|
4381
|
+
}
|
|
4382
|
+
return {
|
|
4383
|
+
async moderate(request) {
|
|
4384
|
+
const operation = `meet.moderate(${request.action})`;
|
|
4385
|
+
const hdrs = headers();
|
|
4386
|
+
const response = await api.request({
|
|
4387
|
+
path: MEET_ROUTES.moderate,
|
|
4388
|
+
method: "POST",
|
|
4389
|
+
operation,
|
|
4390
|
+
body: {
|
|
4391
|
+
meeting_id: meetingId(operation),
|
|
4392
|
+
action: request.action,
|
|
4393
|
+
...request.identity != null ? { identity: request.identity } : {},
|
|
4394
|
+
...request.name !== void 0 ? { name: request.name } : {},
|
|
4395
|
+
...request.reason !== void 0 ? { reason: request.reason } : {}
|
|
4396
|
+
},
|
|
4397
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
4398
|
+
});
|
|
4399
|
+
if (!response.ok) {
|
|
4400
|
+
throw await refusal2(
|
|
4401
|
+
response,
|
|
4402
|
+
operation,
|
|
4403
|
+
"Nothing was changed. The server decides host powers from the meeting itself."
|
|
4404
|
+
);
|
|
4405
|
+
}
|
|
4406
|
+
const payload = await response.json();
|
|
4407
|
+
return {
|
|
4408
|
+
action: request.action,
|
|
4409
|
+
affected: Array.isArray(payload["affected"]) ? payload["affected"].filter((value) => typeof value === "string") : [],
|
|
4410
|
+
message: typeof payload["message"] === "string" ? payload["message"] : "Done."
|
|
4411
|
+
};
|
|
4412
|
+
},
|
|
4413
|
+
async sendChat(text3) {
|
|
4414
|
+
const body = text3.trim();
|
|
4415
|
+
if (body.length === 0) return;
|
|
4416
|
+
const operation = "meet.sendChat";
|
|
4417
|
+
const id = meetingId(operation);
|
|
4418
|
+
const snapshot = store.snapshot();
|
|
4419
|
+
const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
|
|
4420
|
+
const localId = `pending-${now()}`;
|
|
4421
|
+
const pending = {
|
|
4422
|
+
id: localId,
|
|
4423
|
+
identity: snapshot.localIdentity ?? "local",
|
|
4424
|
+
displayName: me?.displayName ?? "You",
|
|
4425
|
+
text: body,
|
|
4426
|
+
at: new Date(now()).toISOString(),
|
|
4427
|
+
saved: false
|
|
4428
|
+
};
|
|
4429
|
+
store.addChat(pending);
|
|
4430
|
+
const hdrs = headers();
|
|
4431
|
+
let response;
|
|
4432
|
+
try {
|
|
4433
|
+
response = await api.request({
|
|
4434
|
+
path: MEET_ROUTES.chat,
|
|
4435
|
+
method: "POST",
|
|
4436
|
+
operation,
|
|
4437
|
+
body: { meeting_id: id, body },
|
|
4438
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
4439
|
+
});
|
|
4440
|
+
} catch (error) {
|
|
4441
|
+
store.settleChat(localId, { failed: "Not sent \u2014 the server could not be reached." });
|
|
4442
|
+
throw error;
|
|
4443
|
+
}
|
|
4444
|
+
if (!response.ok) {
|
|
4445
|
+
const error = await refusal2(response, operation, "Your message was not sent.");
|
|
4446
|
+
store.settleChat(localId, { failed: error.message });
|
|
4447
|
+
throw error;
|
|
4448
|
+
}
|
|
4449
|
+
const saved = chatFromRow(await response.json());
|
|
4450
|
+
if (saved === null) {
|
|
4451
|
+
store.settleChat(localId, { failed: "The server's answer could not be read." });
|
|
4452
|
+
return;
|
|
4453
|
+
}
|
|
4454
|
+
store.settleChat(localId, saved);
|
|
4455
|
+
},
|
|
4456
|
+
async loadChat() {
|
|
4457
|
+
const operation = "meet.loadChat";
|
|
4458
|
+
const id = meetingId(operation);
|
|
4459
|
+
const hdrs = headers();
|
|
4460
|
+
const response = await api.request({
|
|
4461
|
+
path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(id)}`,
|
|
4462
|
+
method: "GET",
|
|
4463
|
+
operation,
|
|
4464
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
4465
|
+
});
|
|
4466
|
+
if (!response.ok) {
|
|
4467
|
+
throw await refusal2(
|
|
4468
|
+
response,
|
|
4469
|
+
operation,
|
|
4470
|
+
"Messages sent before you joined could not be read. New messages still arrive."
|
|
4471
|
+
);
|
|
4472
|
+
}
|
|
4473
|
+
const payload = await response.json();
|
|
4474
|
+
const rows = Array.isArray(payload.messages) ? payload.messages : [];
|
|
4475
|
+
const messages = rows.map(
|
|
4476
|
+
(row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
|
|
4477
|
+
).filter((message) => message !== null);
|
|
4478
|
+
store.loadChat(messages);
|
|
4479
|
+
}
|
|
4480
|
+
};
|
|
4481
|
+
}
|
|
4482
|
+
|
|
3564
4483
|
// src/core/record.ts
|
|
3565
4484
|
import { formatDurationMs } from "@ai-matrx/kit/format";
|
|
3566
4485
|
function activeWrapUpSummary(notes) {
|
|
@@ -3701,7 +4620,7 @@ function meetingWhen(meeting, locale) {
|
|
|
3701
4620
|
if (start === null) return "Time not recorded";
|
|
3702
4621
|
const startedAt = new Date(start);
|
|
3703
4622
|
if (Number.isNaN(startedAt.getTime())) return "Time not recorded";
|
|
3704
|
-
const
|
|
4623
|
+
const when2 = startedAt.toLocaleString(locale, {
|
|
3705
4624
|
weekday: "long",
|
|
3706
4625
|
year: "numeric",
|
|
3707
4626
|
month: "long",
|
|
@@ -3709,11 +4628,11 @@ function meetingWhen(meeting, locale) {
|
|
|
3709
4628
|
hour: "numeric",
|
|
3710
4629
|
minute: "2-digit"
|
|
3711
4630
|
});
|
|
3712
|
-
if (meeting.endedAt === null || meeting.startedAt === null) return
|
|
4631
|
+
if (meeting.endedAt === null || meeting.startedAt === null) return when2;
|
|
3713
4632
|
const endedAt = new Date(meeting.endedAt);
|
|
3714
|
-
if (Number.isNaN(endedAt.getTime())) return
|
|
4633
|
+
if (Number.isNaN(endedAt.getTime())) return when2;
|
|
3715
4634
|
const elapsedMs = endedAt.getTime() - startedAt.getTime();
|
|
3716
|
-
return `${
|
|
4635
|
+
return `${when2} \xB7 ${formatDurationMs(elapsedMs, { style: "long" })}`;
|
|
3717
4636
|
}
|
|
3718
4637
|
var GUEST_RECORD_ACCESS_LEVELS = [
|
|
3719
4638
|
"none",
|
|
@@ -3789,8 +4708,8 @@ async function loadGuestMeetingRecord(args) {
|
|
|
3789
4708
|
);
|
|
3790
4709
|
}
|
|
3791
4710
|
if (!response.ok) {
|
|
3792
|
-
const
|
|
3793
|
-
return refusedBundle(meeting,
|
|
4711
|
+
const refusal2 = await readRefusal(response);
|
|
4712
|
+
return refusedBundle(meeting, refusal2.message, refusal2.remedy);
|
|
3794
4713
|
}
|
|
3795
4714
|
const body = await response.json();
|
|
3796
4715
|
const payload = typeof body["data"] === "object" && body["data"] !== null ? body["data"] : body;
|
|
@@ -3881,8 +4800,8 @@ async function setGuestRecordAccess(args) {
|
|
|
3881
4800
|
}
|
|
3882
4801
|
});
|
|
3883
4802
|
if (!response.ok) {
|
|
3884
|
-
const
|
|
3885
|
-
throw new MeetError("forbidden",
|
|
4803
|
+
const refusal2 = await readRefusal(response);
|
|
4804
|
+
throw new MeetError("forbidden", refusal2.message, refusal2.remedy);
|
|
3886
4805
|
}
|
|
3887
4806
|
const body = await response.json();
|
|
3888
4807
|
const payload = typeof body["data"] === "object" && body["data"] !== null ? body["data"] : body;
|
|
@@ -3897,6 +4816,7 @@ export {
|
|
|
3897
4816
|
CALL_INVITE_VERSION,
|
|
3898
4817
|
CONSENT_NOTICE_ATTRIBUTE,
|
|
3899
4818
|
DATA_TOPICS,
|
|
4819
|
+
DEFAULT_EFFECTS,
|
|
3900
4820
|
DEFAULT_PAGE_SIZE,
|
|
3901
4821
|
END_MEETING_NOT_SERVED,
|
|
3902
4822
|
FILE_SESSION_PATH,
|
|
@@ -3914,6 +4834,7 @@ export {
|
|
|
3914
4834
|
MEET_EVENTS,
|
|
3915
4835
|
MEET_ROUTES,
|
|
3916
4836
|
MEET_SCHEMA,
|
|
4837
|
+
MODERATION_ACTIONS,
|
|
3917
4838
|
MeetError,
|
|
3918
4839
|
NOTE_TAKER_IDENTITY_PREFIX,
|
|
3919
4840
|
NOTE_TAKER_METADATA_KEY,
|
|
@@ -3932,6 +4853,7 @@ export {
|
|
|
3932
4853
|
TRANSCRIPTION_ATTRIBUTES,
|
|
3933
4854
|
TRANSCRIPTION_TOPIC,
|
|
3934
4855
|
absenceSentence,
|
|
4856
|
+
announceMeeting,
|
|
3935
4857
|
applyRoomEvent,
|
|
3936
4858
|
asCallId,
|
|
3937
4859
|
asMeetingId,
|
|
@@ -3946,11 +4868,13 @@ export {
|
|
|
3946
4868
|
canModerateParticipant,
|
|
3947
4869
|
canTransition,
|
|
3948
4870
|
captionSegment,
|
|
4871
|
+
changesPatch,
|
|
3949
4872
|
computeStage,
|
|
3950
4873
|
countLabel,
|
|
3951
4874
|
createCallCenter,
|
|
3952
4875
|
createCallInviteHandler,
|
|
3953
4876
|
createDeviceManager,
|
|
4877
|
+
createInRoomClient,
|
|
3954
4878
|
createLiveKitDriver,
|
|
3955
4879
|
createMeetAi,
|
|
3956
4880
|
createMeetApi,
|
|
@@ -3963,6 +4887,7 @@ export {
|
|
|
3963
4887
|
createRoomEngine,
|
|
3964
4888
|
createTokenClient,
|
|
3965
4889
|
createWebDeviceStorage,
|
|
4890
|
+
createWebEffectsStorage,
|
|
3966
4891
|
createWebRoomTokenStorage,
|
|
3967
4892
|
displayNameFor,
|
|
3968
4893
|
fileDownloadPath,
|
|
@@ -3990,6 +4915,7 @@ export {
|
|
|
3990
4915
|
mapQuality,
|
|
3991
4916
|
meetingInviteAction,
|
|
3992
4917
|
meetingLink,
|
|
4918
|
+
meetingPath,
|
|
3993
4919
|
meetingSlug,
|
|
3994
4920
|
meetingTopic,
|
|
3995
4921
|
meetingWhen,
|
|
@@ -4006,9 +4932,15 @@ export {
|
|
|
4006
4932
|
parseRoomToken,
|
|
4007
4933
|
participantSummary,
|
|
4008
4934
|
playableRecordingFileId,
|
|
4935
|
+
projectInvitee,
|
|
4009
4936
|
projectNote,
|
|
4937
|
+
projectOccurrence,
|
|
4938
|
+
projectOverride,
|
|
4010
4939
|
projectParticipant,
|
|
4940
|
+
projectRsvpInvitation,
|
|
4011
4941
|
projectSegment,
|
|
4942
|
+
projectUpcoming,
|
|
4943
|
+
pushMeetingToGoogleCalendar,
|
|
4012
4944
|
readServerRefusal,
|
|
4013
4945
|
readableTranscript,
|
|
4014
4946
|
recordingLabel,
|
|
@@ -4016,6 +4948,7 @@ export {
|
|
|
4016
4948
|
roleOf,
|
|
4017
4949
|
roomTopic,
|
|
4018
4950
|
setGuestRecordAccess,
|
|
4951
|
+
settingsPatch,
|
|
4019
4952
|
snapshotParticipant,
|
|
4020
4953
|
unavailableFrom,
|
|
4021
4954
|
userTopic,
|