@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.cjs
CHANGED
|
@@ -34,6 +34,7 @@ __export(src_exports, {
|
|
|
34
34
|
CALL_INVITE_VERSION: () => CALL_INVITE_VERSION,
|
|
35
35
|
CONSENT_NOTICE_ATTRIBUTE: () => CONSENT_NOTICE_ATTRIBUTE,
|
|
36
36
|
DATA_TOPICS: () => DATA_TOPICS,
|
|
37
|
+
DEFAULT_EFFECTS: () => DEFAULT_EFFECTS,
|
|
37
38
|
DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
|
|
38
39
|
END_MEETING_NOT_SERVED: () => END_MEETING_NOT_SERVED,
|
|
39
40
|
FILE_SESSION_PATH: () => FILE_SESSION_PATH,
|
|
@@ -51,6 +52,7 @@ __export(src_exports, {
|
|
|
51
52
|
MEET_EVENTS: () => MEET_EVENTS,
|
|
52
53
|
MEET_ROUTES: () => MEET_ROUTES,
|
|
53
54
|
MEET_SCHEMA: () => MEET_SCHEMA,
|
|
55
|
+
MODERATION_ACTIONS: () => MODERATION_ACTIONS,
|
|
54
56
|
MeetError: () => MeetError,
|
|
55
57
|
NOTE_TAKER_IDENTITY_PREFIX: () => NOTE_TAKER_IDENTITY_PREFIX,
|
|
56
58
|
NOTE_TAKER_METADATA_KEY: () => NOTE_TAKER_METADATA_KEY,
|
|
@@ -69,6 +71,7 @@ __export(src_exports, {
|
|
|
69
71
|
TRANSCRIPTION_ATTRIBUTES: () => TRANSCRIPTION_ATTRIBUTES,
|
|
70
72
|
TRANSCRIPTION_TOPIC: () => TRANSCRIPTION_TOPIC,
|
|
71
73
|
absenceSentence: () => absenceSentence,
|
|
74
|
+
announceMeeting: () => announceMeeting,
|
|
72
75
|
applyRoomEvent: () => applyRoomEvent,
|
|
73
76
|
asCallId: () => asCallId,
|
|
74
77
|
asMeetingId: () => asMeetingId,
|
|
@@ -83,11 +86,13 @@ __export(src_exports, {
|
|
|
83
86
|
canModerateParticipant: () => canModerateParticipant,
|
|
84
87
|
canTransition: () => canTransition,
|
|
85
88
|
captionSegment: () => captionSegment,
|
|
89
|
+
changesPatch: () => changesPatch,
|
|
86
90
|
computeStage: () => computeStage,
|
|
87
91
|
countLabel: () => countLabel,
|
|
88
92
|
createCallCenter: () => createCallCenter,
|
|
89
93
|
createCallInviteHandler: () => createCallInviteHandler,
|
|
90
94
|
createDeviceManager: () => createDeviceManager,
|
|
95
|
+
createInRoomClient: () => createInRoomClient,
|
|
91
96
|
createLiveKitDriver: () => createLiveKitDriver,
|
|
92
97
|
createMeetAi: () => createMeetAi,
|
|
93
98
|
createMeetApi: () => createMeetApi,
|
|
@@ -100,6 +105,7 @@ __export(src_exports, {
|
|
|
100
105
|
createRoomEngine: () => createRoomEngine,
|
|
101
106
|
createTokenClient: () => createTokenClient,
|
|
102
107
|
createWebDeviceStorage: () => createWebDeviceStorage,
|
|
108
|
+
createWebEffectsStorage: () => createWebEffectsStorage,
|
|
103
109
|
createWebRoomTokenStorage: () => createWebRoomTokenStorage,
|
|
104
110
|
displayNameFor: () => displayNameFor,
|
|
105
111
|
fileDownloadPath: () => fileDownloadPath,
|
|
@@ -127,6 +133,7 @@ __export(src_exports, {
|
|
|
127
133
|
mapQuality: () => mapQuality,
|
|
128
134
|
meetingInviteAction: () => meetingInviteAction,
|
|
129
135
|
meetingLink: () => meetingLink,
|
|
136
|
+
meetingPath: () => meetingPath,
|
|
130
137
|
meetingSlug: () => meetingSlug,
|
|
131
138
|
meetingTopic: () => meetingTopic,
|
|
132
139
|
meetingWhen: () => meetingWhen,
|
|
@@ -143,9 +150,15 @@ __export(src_exports, {
|
|
|
143
150
|
parseRoomToken: () => parseRoomToken,
|
|
144
151
|
participantSummary: () => participantSummary,
|
|
145
152
|
playableRecordingFileId: () => playableRecordingFileId,
|
|
153
|
+
projectInvitee: () => projectInvitee,
|
|
146
154
|
projectNote: () => projectNote,
|
|
155
|
+
projectOccurrence: () => projectOccurrence,
|
|
156
|
+
projectOverride: () => projectOverride,
|
|
147
157
|
projectParticipant: () => projectParticipant,
|
|
158
|
+
projectRsvpInvitation: () => projectRsvpInvitation,
|
|
148
159
|
projectSegment: () => projectSegment,
|
|
160
|
+
projectUpcoming: () => projectUpcoming,
|
|
161
|
+
pushMeetingToGoogleCalendar: () => pushMeetingToGoogleCalendar,
|
|
149
162
|
readServerRefusal: () => readServerRefusal,
|
|
150
163
|
readableTranscript: () => readableTranscript,
|
|
151
164
|
recordingLabel: () => recordingLabel,
|
|
@@ -153,6 +166,7 @@ __export(src_exports, {
|
|
|
153
166
|
roleOf: () => roleOf,
|
|
154
167
|
roomTopic: () => roomTopic,
|
|
155
168
|
setGuestRecordAccess: () => setGuestRecordAccess,
|
|
169
|
+
settingsPatch: () => settingsPatch,
|
|
156
170
|
snapshotParticipant: () => snapshotParticipant,
|
|
157
171
|
unavailableFrom: () => unavailableFrom,
|
|
158
172
|
userTopic: () => userTopic,
|
|
@@ -187,7 +201,7 @@ var MeetError = class extends Error {
|
|
|
187
201
|
};
|
|
188
202
|
var FORBIDDEN_CODES = /* @__PURE__ */ new Set(["42501", "PGRST301", "PGRST302"]);
|
|
189
203
|
var NOT_FOUND_CODES = /* @__PURE__ */ new Set(["PGRST116", "PGRST205"]);
|
|
190
|
-
var CONFLICT_CODES = /* @__PURE__ */ new Set(["23505"]);
|
|
204
|
+
var CONFLICT_CODES = /* @__PURE__ */ new Set(["23505", "40001", "55000"]);
|
|
191
205
|
var SESSION_MARKERS = [
|
|
192
206
|
"auth session missing",
|
|
193
207
|
"jwt expired",
|
|
@@ -345,8 +359,28 @@ var MEET_ROUTES = {
|
|
|
345
359
|
*/
|
|
346
360
|
record: "/api/v1/meet/record",
|
|
347
361
|
/** A host chooses what guests may read of ONE meeting (MRF-1). */
|
|
348
|
-
recordAccess: "/api/v1/meet/record/access"
|
|
362
|
+
recordAccess: "/api/v1/meet/record/access",
|
|
363
|
+
/**
|
|
364
|
+
* Meet wave 1 (0.8): bring every invitee up to date after a write — sends the
|
|
365
|
+
* invitation / update / cancellation emails with their `.ics` and re-queues
|
|
366
|
+
* reminders. Idempotent. `{meeting_id}` is substituted by `meetingPath`.
|
|
367
|
+
*/
|
|
368
|
+
meetingAnnounce: "/api/v1/meet/meetings/{meeting_id}/announce",
|
|
369
|
+
/** Put a one-off meeting on the caller's own Google Calendar (gated by the server). */
|
|
370
|
+
meetingGoogleCalendar: "/api/v1/meet/meetings/{meeting_id}/google-calendar",
|
|
371
|
+
/**
|
|
372
|
+
* Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
|
|
373
|
+
* unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
|
|
374
|
+
* Decided by the server from the meeting itself; a guest's pass rides
|
|
375
|
+
* `x-meet-room-token`.
|
|
376
|
+
*/
|
|
377
|
+
moderate: "/api/v1/meet/moderate",
|
|
378
|
+
/** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */
|
|
379
|
+
chat: "/api/v1/meet/chat"
|
|
349
380
|
};
|
|
381
|
+
function meetingPath(template, meetingId) {
|
|
382
|
+
return template.replace("{meeting_id}", encodeURIComponent(meetingId));
|
|
383
|
+
}
|
|
350
384
|
var ROOM_TOKEN_HEADER = "x-meet-room-token";
|
|
351
385
|
var REFRESH_RATIO = 0.8;
|
|
352
386
|
function readString(source, key) {
|
|
@@ -476,6 +510,19 @@ function createTokenClient(options) {
|
|
|
476
510
|
"The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry."
|
|
477
511
|
);
|
|
478
512
|
}
|
|
513
|
+
if (response.status === 410) {
|
|
514
|
+
let reason = "This meeting was cancelled.";
|
|
515
|
+
try {
|
|
516
|
+
const body = await response.json();
|
|
517
|
+
if (typeof body?.detail?.message === "string") reason = body.detail.message;
|
|
518
|
+
} catch {
|
|
519
|
+
}
|
|
520
|
+
throw new MeetError(
|
|
521
|
+
"cancelled",
|
|
522
|
+
reason,
|
|
523
|
+
"Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one."
|
|
524
|
+
);
|
|
525
|
+
}
|
|
479
526
|
if (response.status === 423) {
|
|
480
527
|
throw new MeetError(
|
|
481
528
|
"room-locked",
|
|
@@ -677,12 +724,12 @@ function createMeetAi(options) {
|
|
|
677
724
|
async function dispatch(route, body, operation) {
|
|
678
725
|
const response = await send({ path: route, body, operation, requireAuth: true });
|
|
679
726
|
if (!response.ok) {
|
|
680
|
-
const
|
|
681
|
-
options.store.patch({ noteTakerRefusal:
|
|
727
|
+
const refusal2 = await refusalFor(response, operation);
|
|
728
|
+
options.store.patch({ noteTakerRefusal: refusal2 });
|
|
682
729
|
throw new MeetError(
|
|
683
730
|
response.status === 403 ? "forbidden" : "transport",
|
|
684
|
-
|
|
685
|
-
|
|
731
|
+
refusal2.reason,
|
|
732
|
+
refusal2.remedy
|
|
686
733
|
);
|
|
687
734
|
}
|
|
688
735
|
options.store.patch({ noteTakerRefusal: null });
|
|
@@ -720,11 +767,11 @@ function createMeetAi(options) {
|
|
|
720
767
|
...signal !== void 0 ? { signal } : {}
|
|
721
768
|
});
|
|
722
769
|
if (!response.ok) {
|
|
723
|
-
const
|
|
770
|
+
const refusal2 = await refusalFor(response, "meet.ai.ask");
|
|
724
771
|
throw new MeetError(
|
|
725
772
|
response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : "transport",
|
|
726
|
-
|
|
727
|
-
|
|
773
|
+
refusal2.reason,
|
|
774
|
+
refusal2.remedy
|
|
728
775
|
);
|
|
729
776
|
}
|
|
730
777
|
const body = response.body;
|
|
@@ -833,14 +880,14 @@ function noteTakerStatus(args) {
|
|
|
833
880
|
const row = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
|
|
834
881
|
const recorded = readState(row["state"]);
|
|
835
882
|
const detail = typeof row["reason"] === "string" && row["reason"].length > 0 ? row["reason"] : null;
|
|
836
|
-
const
|
|
883
|
+
const refusal2 = args.refusal ?? null;
|
|
837
884
|
if (recorded === null) {
|
|
838
|
-
return { state: "absent", detail, present, refusal };
|
|
885
|
+
return { state: "absent", detail, present, refusal: refusal2 };
|
|
839
886
|
}
|
|
840
887
|
if (recorded === "transcribing" && !present) {
|
|
841
|
-
return { state: "handoff_pending", detail, present, refusal };
|
|
888
|
+
return { state: "handoff_pending", detail, present, refusal: refusal2 };
|
|
842
889
|
}
|
|
843
|
-
return { state: recorded, detail, present, refusal };
|
|
890
|
+
return { state: recorded, detail, present, refusal: refusal2 };
|
|
844
891
|
}
|
|
845
892
|
function noteTakerLabel(status) {
|
|
846
893
|
switch (status.state) {
|
|
@@ -1253,7 +1300,11 @@ var TABLES = {
|
|
|
1253
1300
|
invites: "meet_call_invites",
|
|
1254
1301
|
recordings: "meet_recordings",
|
|
1255
1302
|
transcripts: "meet_transcript_segments",
|
|
1256
|
-
notes: "meet_notes"
|
|
1303
|
+
notes: "meet_notes",
|
|
1304
|
+
/** Wave 1 (0.8): who a meeting was sent to, and what they answered. */
|
|
1305
|
+
invitees: "meet_invitees",
|
|
1306
|
+
/** Wave 1 (0.8): one occurrence of a series cancelled or moved. */
|
|
1307
|
+
occurrenceOverrides: "meet_occurrence_overrides"
|
|
1257
1308
|
};
|
|
1258
1309
|
var RPCS = {
|
|
1259
1310
|
/** Atomic: create the invite row and stamp its expiry in one statement. */
|
|
@@ -1269,10 +1320,41 @@ var RPCS = {
|
|
|
1269
1320
|
admit: "meet_admit_participant",
|
|
1270
1321
|
setLock: "meet_set_lock",
|
|
1271
1322
|
endMeeting: "meet_end_meeting",
|
|
1272
|
-
recordConsent: "meet_record_consent"
|
|
1323
|
+
recordConsent: "meet_record_consent",
|
|
1324
|
+
/*
|
|
1325
|
+
* ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
|
|
1326
|
+
* authorized by the meeting's host or a real iam grant (editor to change,
|
|
1327
|
+
* admin = co-host to archive or name a co-host) — never an organization
|
|
1328
|
+
* match. After any of these writes, call `announceMeeting` so the server
|
|
1329
|
+
* sends invitations / updates / cancellations and re-queues reminders.
|
|
1330
|
+
*/
|
|
1331
|
+
/** Schedule with every setting at create; unset settings come from the knobs. */
|
|
1332
|
+
scheduleMeeting: "meet_schedule_meeting",
|
|
1333
|
+
/** Edit / reschedule — a JSON patch with a version compare-and-swap. */
|
|
1334
|
+
updateMeeting: "meet_update_meeting",
|
|
1335
|
+
/** Cancel is a state (`cancelled_at`); the link keeps resolving. */
|
|
1336
|
+
cancelMeeting: "meet_cancel_meeting",
|
|
1337
|
+
/** Archive (soft delete) — the invitations and exceptions cascade. */
|
|
1338
|
+
archiveMeeting: "meet_archive_meeting",
|
|
1339
|
+
restoreMeeting: "meet_restore_meeting",
|
|
1340
|
+
/** Cancel, move or restore ONE occurrence of a series by its original start. */
|
|
1341
|
+
setOccurrence: "meet_set_occurrence",
|
|
1342
|
+
addInvitees: "meet_add_invitees",
|
|
1343
|
+
setInviteeRole: "meet_set_invitee_role",
|
|
1344
|
+
removeInvitee: "meet_remove_invitee",
|
|
1345
|
+
/** A signed-in invitee answers for themselves. */
|
|
1346
|
+
respond: "meet_respond",
|
|
1347
|
+
/** The emailed RSVP link (no account): read and answer. */
|
|
1348
|
+
invitationByToken: "meet_invitation_by_token",
|
|
1349
|
+
respondByToken: "meet_respond_by_token",
|
|
1350
|
+
/** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */
|
|
1351
|
+
meetingOccurrences: "meet_meeting_occurrences",
|
|
1352
|
+
/** Every occurrence the reader can see between two instants — the Upcoming list. */
|
|
1353
|
+
occurrencesBetween: "meet_occurrences_between"
|
|
1273
1354
|
};
|
|
1274
1355
|
var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
|
|
1275
|
-
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";
|
|
1356
|
+
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";
|
|
1357
|
+
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";
|
|
1276
1358
|
var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
|
|
1277
1359
|
var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
|
|
1278
1360
|
var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
|
|
@@ -1370,6 +1452,138 @@ function projectNote(row) {
|
|
|
1370
1452
|
source: "record"
|
|
1371
1453
|
};
|
|
1372
1454
|
}
|
|
1455
|
+
var RSVP_STATES = ["needs_action", "accepted", "declined", "tentative"];
|
|
1456
|
+
var OCCURRENCE_STATES = ["scheduled", "moved", "cancelled"];
|
|
1457
|
+
function rsvpState(value) {
|
|
1458
|
+
return RSVP_STATES.includes(value) ? value : "needs_action";
|
|
1459
|
+
}
|
|
1460
|
+
function num(row, key) {
|
|
1461
|
+
const value = row[key];
|
|
1462
|
+
return typeof value === "number" ? value : null;
|
|
1463
|
+
}
|
|
1464
|
+
function when(row, key) {
|
|
1465
|
+
const value = row[key];
|
|
1466
|
+
if (value instanceof Date) return value.toISOString();
|
|
1467
|
+
return text(row, key);
|
|
1468
|
+
}
|
|
1469
|
+
function projectInvitee(row) {
|
|
1470
|
+
const operation = "meet.projectInvitee";
|
|
1471
|
+
const userId = text(row, "invitee_user_id");
|
|
1472
|
+
return {
|
|
1473
|
+
id: requireText(row, "id", operation),
|
|
1474
|
+
meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
|
|
1475
|
+
userId: userId === null ? null : asUserId(userId),
|
|
1476
|
+
email: text(row, "email"),
|
|
1477
|
+
displayName: text(row, "display_name"),
|
|
1478
|
+
role: text(row, "role") === "cohost" ? "cohost" : "invitee",
|
|
1479
|
+
rsvpState: rsvpState(text(row, "rsvp_state")),
|
|
1480
|
+
respondedAt: when(row, "responded_at"),
|
|
1481
|
+
rsvpNote: text(row, "rsvp_note"),
|
|
1482
|
+
lastSentSequence: num(row, "last_sent_sequence"),
|
|
1483
|
+
lastSentAt: when(row, "last_sent_at")
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
function projectOccurrence(row) {
|
|
1487
|
+
const operation = "meet.projectOccurrence";
|
|
1488
|
+
const state = text(row, "state");
|
|
1489
|
+
return {
|
|
1490
|
+
meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
|
|
1491
|
+
originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
|
|
1492
|
+
occurrenceStart: when(row, "occurrence_start") ?? requireText(row, "occurrence_start", operation),
|
|
1493
|
+
durationMinutes: num(row, "duration_minutes"),
|
|
1494
|
+
// An unknown state is shown as scheduled, never dropped: the row is real.
|
|
1495
|
+
state: OCCURRENCE_STATES.includes(state) ? state : "scheduled",
|
|
1496
|
+
sequence: num(row, "sequence") ?? 0
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
function projectUpcoming(row) {
|
|
1500
|
+
const operation = "meet.projectUpcoming";
|
|
1501
|
+
const kind = text(row, "kind");
|
|
1502
|
+
const myRole = text(row, "my_role");
|
|
1503
|
+
const myRsvp = text(row, "my_rsvp");
|
|
1504
|
+
return {
|
|
1505
|
+
...projectOccurrence(row),
|
|
1506
|
+
organizationId: asOrganizationId(requireText(row, "organization_id", operation)),
|
|
1507
|
+
title: text(row, "title") ?? "Meeting",
|
|
1508
|
+
slug: requireText(row, "slug", operation),
|
|
1509
|
+
kind: kind === "scheduled" || kind === "recurring" ? kind : "instant",
|
|
1510
|
+
hostUserId: asUserId(requireText(row, "host_user_id", operation)),
|
|
1511
|
+
timeZone: text(row, "time_zone") ?? "UTC",
|
|
1512
|
+
meetingCancelled: bool(row, "meeting_cancelled", false),
|
|
1513
|
+
myRsvp: myRsvp === null ? null : rsvpState(myRsvp),
|
|
1514
|
+
myRole: myRole === "host" || myRole === "cohost" || myRole === "invitee" ? myRole : null
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
function projectOverride(row) {
|
|
1518
|
+
const operation = "meet.projectOverride";
|
|
1519
|
+
return {
|
|
1520
|
+
id: requireText(row, "id", operation),
|
|
1521
|
+
meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
|
|
1522
|
+
originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
|
|
1523
|
+
state: text(row, "state") === "moved" ? "moved" : "cancelled",
|
|
1524
|
+
newStart: when(row, "new_start"),
|
|
1525
|
+
newDurationMinutes: num(row, "new_duration_minutes"),
|
|
1526
|
+
reason: text(row, "reason"),
|
|
1527
|
+
calendarSequence: num(row, "calendar_sequence") ?? 0,
|
|
1528
|
+
deletedAt: when(row, "deleted_at")
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
function projectRsvpInvitation(body) {
|
|
1532
|
+
const operation = "meet.projectRsvpInvitation";
|
|
1533
|
+
if (typeof body !== "object" || body === null) {
|
|
1534
|
+
throw invalidResponse(operation, "the RSVP door returned no object");
|
|
1535
|
+
}
|
|
1536
|
+
const row = body;
|
|
1537
|
+
if (row["ok"] !== true) {
|
|
1538
|
+
return {
|
|
1539
|
+
ok: false,
|
|
1540
|
+
message: text(row, "message") ?? "This invitation link is no longer valid \u2014 ask the host to send it again."
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
const meeting = row["meeting"] ?? {};
|
|
1544
|
+
const invitee = row["invitee"] ?? {};
|
|
1545
|
+
return {
|
|
1546
|
+
ok: true,
|
|
1547
|
+
meeting: {
|
|
1548
|
+
title: text(meeting, "title") ?? "Meeting",
|
|
1549
|
+
slug: requireText(meeting, "slug", operation),
|
|
1550
|
+
scheduledFor: when(meeting, "scheduled_for"),
|
|
1551
|
+
durationMinutes: num(meeting, "duration_minutes"),
|
|
1552
|
+
timeZone: text(meeting, "time_zone") ?? "UTC",
|
|
1553
|
+
agenda: text(meeting, "agenda"),
|
|
1554
|
+
recurrenceRule: text(meeting, "recurrence_rule"),
|
|
1555
|
+
cancelledAt: when(meeting, "cancelled_at"),
|
|
1556
|
+
cancellationReason: text(meeting, "cancellation_reason"),
|
|
1557
|
+
endedAt: when(meeting, "ended_at")
|
|
1558
|
+
},
|
|
1559
|
+
invitee: {
|
|
1560
|
+
displayName: text(invitee, "display_name"),
|
|
1561
|
+
email: text(invitee, "email"),
|
|
1562
|
+
rsvpState: rsvpState(text(invitee, "rsvp_state")),
|
|
1563
|
+
respondedAt: when(invitee, "responded_at")
|
|
1564
|
+
}
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
function settingsPatch(settings) {
|
|
1568
|
+
const patch = {};
|
|
1569
|
+
if (settings.lobbyEnabled !== void 0) patch["lobby_enabled"] = settings.lobbyEnabled;
|
|
1570
|
+
if (settings.recordingPolicy !== void 0) patch["recording_policy"] = settings.recordingPolicy;
|
|
1571
|
+
if (settings.aiEnabled !== void 0) patch["ai_enabled"] = settings.aiEnabled;
|
|
1572
|
+
if (settings.joinBeforeHost !== void 0) patch["join_before_host"] = settings.joinBeforeHost;
|
|
1573
|
+
return patch;
|
|
1574
|
+
}
|
|
1575
|
+
function changesPatch(changes) {
|
|
1576
|
+
const patch = settingsPatch(changes);
|
|
1577
|
+
if (changes.title !== void 0) patch["title"] = changes.title;
|
|
1578
|
+
if (changes.agenda !== void 0) patch["agenda"] = changes.agenda;
|
|
1579
|
+
if (changes.timeZone !== void 0) patch["time_zone"] = changes.timeZone;
|
|
1580
|
+
if (changes.scheduledFor !== void 0) patch["scheduled_for"] = changes.scheduledFor;
|
|
1581
|
+
if (changes.scheduledDurationMinutes !== void 0) {
|
|
1582
|
+
patch["scheduled_duration_minutes"] = changes.scheduledDurationMinutes;
|
|
1583
|
+
}
|
|
1584
|
+
if (changes.recurrenceRule !== void 0) patch["recurrence_rule"] = changes.recurrenceRule;
|
|
1585
|
+
return patch;
|
|
1586
|
+
}
|
|
1373
1587
|
function createMeetRepository(options) {
|
|
1374
1588
|
const db = options.client.schema(MEET_SCHEMA);
|
|
1375
1589
|
async function guardVoid(operation, run) {
|
|
@@ -1439,7 +1653,16 @@ function createMeetRepository(options) {
|
|
|
1439
1653
|
lobbyEnabled: bool(row, "lobby_enabled", true),
|
|
1440
1654
|
recordingPolicy: policy === "disabled" || policy === "always-on" ? policy : "host-controlled",
|
|
1441
1655
|
aiEnabled: bool(row, "ai_enabled", true),
|
|
1442
|
-
metadata: typeof metadata === "object" && metadata !== null && !Array.isArray(metadata) ? metadata : {}
|
|
1656
|
+
metadata: typeof metadata === "object" && metadata !== null && !Array.isArray(metadata) ? metadata : {},
|
|
1657
|
+
timeZone: text(row, "time_zone") ?? "UTC",
|
|
1658
|
+
agenda: text(row, "agenda"),
|
|
1659
|
+
recurrenceRule: text(row, "recurrence_rule"),
|
|
1660
|
+
joinBeforeHost: bool(row, "join_before_host", true),
|
|
1661
|
+
cancelledAt: text(row, "cancelled_at"),
|
|
1662
|
+
cancellationReason: text(row, "cancellation_reason"),
|
|
1663
|
+
deletedAt: text(row, "deleted_at"),
|
|
1664
|
+
calendarSequence: typeof row["calendar_sequence"] === "number" ? row["calendar_sequence"] : 0,
|
|
1665
|
+
version: typeof row["version"] === "number" ? row["version"] : 1
|
|
1443
1666
|
};
|
|
1444
1667
|
}
|
|
1445
1668
|
function projectRecording(row) {
|
|
@@ -1526,6 +1749,170 @@ function createMeetRepository(options) {
|
|
|
1526
1749
|
projectNote,
|
|
1527
1750
|
projectRecording,
|
|
1528
1751
|
projectAttendee,
|
|
1752
|
+
projectInvitee,
|
|
1753
|
+
async scheduleMeeting(input) {
|
|
1754
|
+
requireOrg(input.organizationId, "meet.scheduleMeeting");
|
|
1755
|
+
const row = await guard(
|
|
1756
|
+
"meet.scheduleMeeting",
|
|
1757
|
+
() => db.rpc(RPCS.scheduleMeeting, {
|
|
1758
|
+
p_organization_id: input.organizationId,
|
|
1759
|
+
p_host_user_id: input.hostUserId,
|
|
1760
|
+
p_title: input.title,
|
|
1761
|
+
p_scheduled_for: input.scheduledFor,
|
|
1762
|
+
p_time_zone: input.timeZone,
|
|
1763
|
+
p_duration_minutes: input.durationMinutes ?? null,
|
|
1764
|
+
p_agenda: input.agenda ?? null,
|
|
1765
|
+
p_recurrence_rule: input.recurrenceRule ?? null,
|
|
1766
|
+
p_settings: settingsPatch(input.settings ?? {})
|
|
1767
|
+
})
|
|
1768
|
+
);
|
|
1769
|
+
return projectMeeting(row);
|
|
1770
|
+
},
|
|
1771
|
+
async updateMeeting(meetingId, changes, options2 = {}) {
|
|
1772
|
+
const row = await guard(
|
|
1773
|
+
"meet.updateMeeting",
|
|
1774
|
+
() => db.rpc(RPCS.updateMeeting, {
|
|
1775
|
+
p_meeting_id: meetingId,
|
|
1776
|
+
p_changes: changesPatch(changes),
|
|
1777
|
+
p_expected_version: options2.expectedVersion ?? null,
|
|
1778
|
+
p_by_user_id: options2.byUserId ?? null
|
|
1779
|
+
})
|
|
1780
|
+
);
|
|
1781
|
+
return projectMeeting(row);
|
|
1782
|
+
},
|
|
1783
|
+
async cancelMeeting(meetingId, reason, byUserId = null) {
|
|
1784
|
+
const row = await guard(
|
|
1785
|
+
"meet.cancelMeeting",
|
|
1786
|
+
() => db.rpc(RPCS.cancelMeeting, {
|
|
1787
|
+
p_meeting_id: meetingId,
|
|
1788
|
+
p_reason: reason,
|
|
1789
|
+
p_by_user_id: byUserId
|
|
1790
|
+
})
|
|
1791
|
+
);
|
|
1792
|
+
return projectMeeting(row);
|
|
1793
|
+
},
|
|
1794
|
+
async archiveMeeting(meetingId, byUserId = null) {
|
|
1795
|
+
const row = await guard(
|
|
1796
|
+
"meet.archiveMeeting",
|
|
1797
|
+
() => db.rpc(RPCS.archiveMeeting, { p_meeting_id: meetingId, p_by_user_id: byUserId })
|
|
1798
|
+
);
|
|
1799
|
+
return projectMeeting(row);
|
|
1800
|
+
},
|
|
1801
|
+
async restoreMeeting(meetingId, byUserId = null) {
|
|
1802
|
+
const row = await guard(
|
|
1803
|
+
"meet.restoreMeeting",
|
|
1804
|
+
() => db.rpc(RPCS.restoreMeeting, { p_meeting_id: meetingId, p_by_user_id: byUserId })
|
|
1805
|
+
);
|
|
1806
|
+
return projectMeeting(row);
|
|
1807
|
+
},
|
|
1808
|
+
async setOccurrence(args) {
|
|
1809
|
+
const row = await guard(
|
|
1810
|
+
"meet.setOccurrence",
|
|
1811
|
+
() => db.rpc(RPCS.setOccurrence, {
|
|
1812
|
+
p_meeting_id: args.meetingId,
|
|
1813
|
+
p_original_start: args.originalStart,
|
|
1814
|
+
p_action: args.action,
|
|
1815
|
+
p_new_start: args.newStart ?? null,
|
|
1816
|
+
p_new_duration_minutes: args.newDurationMinutes ?? null,
|
|
1817
|
+
p_reason: args.reason ?? null,
|
|
1818
|
+
p_by_user_id: args.byUserId ?? null
|
|
1819
|
+
})
|
|
1820
|
+
);
|
|
1821
|
+
return projectOverride(row);
|
|
1822
|
+
},
|
|
1823
|
+
async meetingOccurrences(meetingId, { from, to, limit = 200 }) {
|
|
1824
|
+
const rows = await guard(
|
|
1825
|
+
"meet.meetingOccurrences",
|
|
1826
|
+
() => db.rpc(RPCS.meetingOccurrences, {
|
|
1827
|
+
p_meeting_id: meetingId,
|
|
1828
|
+
p_from: from,
|
|
1829
|
+
p_to: to,
|
|
1830
|
+
p_limit: limit
|
|
1831
|
+
})
|
|
1832
|
+
);
|
|
1833
|
+
return rows.map(projectOccurrence);
|
|
1834
|
+
},
|
|
1835
|
+
async occurrencesBetween({ from, to, organizationId = null, limit = 500 }) {
|
|
1836
|
+
const rows = await guard(
|
|
1837
|
+
"meet.occurrencesBetween",
|
|
1838
|
+
() => db.rpc(RPCS.occurrencesBetween, {
|
|
1839
|
+
p_from: from,
|
|
1840
|
+
p_to: to,
|
|
1841
|
+
p_organization_id: organizationId,
|
|
1842
|
+
p_limit: limit
|
|
1843
|
+
})
|
|
1844
|
+
);
|
|
1845
|
+
return rows.map(projectUpcoming);
|
|
1846
|
+
},
|
|
1847
|
+
async invitees(meetingId) {
|
|
1848
|
+
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 });
|
|
1849
|
+
if (response.error !== null) throw normalizeMeetError(response.error, "meet.invitees");
|
|
1850
|
+
return (response.data ?? []).map(projectInvitee);
|
|
1851
|
+
},
|
|
1852
|
+
async addInvitees(meetingId, people, byUserId = null) {
|
|
1853
|
+
const rows = await guard(
|
|
1854
|
+
"meet.addInvitees",
|
|
1855
|
+
() => db.rpc(RPCS.addInvitees, {
|
|
1856
|
+
p_meeting_id: meetingId,
|
|
1857
|
+
p_invitees: people.map((person) => ({
|
|
1858
|
+
...person.userId !== void 0 ? { user_id: person.userId } : {},
|
|
1859
|
+
...person.email !== void 0 ? { email: person.email } : {},
|
|
1860
|
+
...person.displayName !== void 0 ? { display_name: person.displayName } : {},
|
|
1861
|
+
...person.role !== void 0 ? { role: person.role } : {}
|
|
1862
|
+
})),
|
|
1863
|
+
p_by_user_id: byUserId
|
|
1864
|
+
})
|
|
1865
|
+
);
|
|
1866
|
+
return rows.map(projectInvitee);
|
|
1867
|
+
},
|
|
1868
|
+
async setInviteeRole(inviteeId, role, byUserId = null) {
|
|
1869
|
+
const row = await guard(
|
|
1870
|
+
"meet.setInviteeRole",
|
|
1871
|
+
() => db.rpc(RPCS.setInviteeRole, {
|
|
1872
|
+
p_invitee_id: inviteeId,
|
|
1873
|
+
p_role: role,
|
|
1874
|
+
p_by_user_id: byUserId
|
|
1875
|
+
})
|
|
1876
|
+
);
|
|
1877
|
+
return projectInvitee(row);
|
|
1878
|
+
},
|
|
1879
|
+
async removeInvitee(inviteeId, byUserId = null) {
|
|
1880
|
+
const row = await guard(
|
|
1881
|
+
"meet.removeInvitee",
|
|
1882
|
+
() => db.rpc(RPCS.removeInvitee, { p_invitee_id: inviteeId, p_by_user_id: byUserId })
|
|
1883
|
+
);
|
|
1884
|
+
return projectInvitee(row);
|
|
1885
|
+
},
|
|
1886
|
+
async respond(meetingId, answer, note = null) {
|
|
1887
|
+
const row = await guard(
|
|
1888
|
+
"meet.respond",
|
|
1889
|
+
() => db.rpc(RPCS.respond, {
|
|
1890
|
+
p_meeting_id: meetingId,
|
|
1891
|
+
p_response: answer,
|
|
1892
|
+
p_note: note,
|
|
1893
|
+
p_by_user_id: null
|
|
1894
|
+
})
|
|
1895
|
+
);
|
|
1896
|
+
return projectInvitee(row);
|
|
1897
|
+
},
|
|
1898
|
+
async invitationByToken(secret) {
|
|
1899
|
+
const body = await guard(
|
|
1900
|
+
"meet.invitationByToken",
|
|
1901
|
+
() => db.rpc(RPCS.invitationByToken, { p_secret: secret })
|
|
1902
|
+
);
|
|
1903
|
+
return projectRsvpInvitation(body);
|
|
1904
|
+
},
|
|
1905
|
+
async respondByToken(secret, answer, note = null) {
|
|
1906
|
+
const body = await guard(
|
|
1907
|
+
"meet.respondByToken",
|
|
1908
|
+
() => db.rpc(RPCS.respondByToken, {
|
|
1909
|
+
p_secret: secret,
|
|
1910
|
+
p_response: answer,
|
|
1911
|
+
p_note: note
|
|
1912
|
+
})
|
|
1913
|
+
);
|
|
1914
|
+
return projectRsvpInvitation(body);
|
|
1915
|
+
},
|
|
1529
1916
|
async attendees(meetingId) {
|
|
1530
1917
|
const response = await db.from(TABLES.participants).select(ATTENDEE_COLUMNS).eq("meeting_id", meetingId).order("joined_at", { ascending: true }).order("id", { ascending: true });
|
|
1531
1918
|
if (response.error !== null) throw normalizeMeetError(response.error, "meet.attendees");
|
|
@@ -1707,7 +2094,19 @@ var ROOM_EVENTS = {
|
|
|
1707
2094
|
* production 2026-09-09 the guest lane showed no indicator at all while the
|
|
1708
2095
|
* host recorded (MRI-C8).
|
|
1709
2096
|
*/
|
|
1710
|
-
recording: "matrx.meet.recording"
|
|
2097
|
+
recording: "matrx.meet.recording",
|
|
2098
|
+
/**
|
|
2099
|
+
* A chat message to everyone was SAVED (Meet wave 2). Payload
|
|
2100
|
+
* `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
|
|
2101
|
+
*/
|
|
2102
|
+
chat: "matrx.meet.chat",
|
|
2103
|
+
/**
|
|
2104
|
+
* A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`:
|
|
2105
|
+
* `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the
|
|
2106
|
+
* person concerned; `lower-hands {identities: [...] | "*"}` · `role {identity,
|
|
2107
|
+
* role}` — to everyone.
|
|
2108
|
+
*/
|
|
2109
|
+
moderation: "matrx.meet.moderation"
|
|
1711
2110
|
};
|
|
1712
2111
|
function isRoomEventTopic(topic) {
|
|
1713
2112
|
return topic.startsWith(ROOM_EVENT_PREFIX);
|
|
@@ -1743,6 +2142,10 @@ function applyRoomEvent(args) {
|
|
|
1743
2142
|
return applyEnded(store, payload);
|
|
1744
2143
|
case ROOM_EVENTS.recording:
|
|
1745
2144
|
return applyRecording(store, payload);
|
|
2145
|
+
case ROOM_EVENTS.chat:
|
|
2146
|
+
return applyChat(store, payload);
|
|
2147
|
+
case ROOM_EVENTS.moderation:
|
|
2148
|
+
return applyModeration(store, payload, now);
|
|
1746
2149
|
default:
|
|
1747
2150
|
return ignored(`${topic} is not known to this version of @ai-matrx/meet`);
|
|
1748
2151
|
}
|
|
@@ -1834,6 +2237,101 @@ function applyEnded(store, payload) {
|
|
|
1834
2237
|
store.patch({ meeting: { ...meeting, endedAt } });
|
|
1835
2238
|
return HANDLED;
|
|
1836
2239
|
}
|
|
2240
|
+
function chatFromRow(row) {
|
|
2241
|
+
const id = text2(row["id"]);
|
|
2242
|
+
const identity = text2(row["sender_identity"]);
|
|
2243
|
+
const body = text2(row["body"]);
|
|
2244
|
+
if (id === null || identity === null || body === null) return null;
|
|
2245
|
+
return {
|
|
2246
|
+
id,
|
|
2247
|
+
identity: asParticipantIdentity(identity),
|
|
2248
|
+
displayName: text2(row["sender_name"]) ?? "Participant",
|
|
2249
|
+
text: body,
|
|
2250
|
+
at: text2(row["created_at"]) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2251
|
+
// `false` only from a server whose chat table has not landed: the room heard
|
|
2252
|
+
// it, nobody will see it later — and the line says so.
|
|
2253
|
+
saved: row["saved"] !== false
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
function applyChat(store, payload) {
|
|
2257
|
+
const row = object(payload["message"]);
|
|
2258
|
+
if (row === null) return ignored("a chat event with no `message` row");
|
|
2259
|
+
const meeting = store.snapshot().meeting;
|
|
2260
|
+
if (meeting !== null && text2(row["meeting_id"]) !== null && row["meeting_id"] !== meeting.id) {
|
|
2261
|
+
return ignored("a chat message for a different meeting");
|
|
2262
|
+
}
|
|
2263
|
+
const message = chatFromRow(row);
|
|
2264
|
+
if (message === null) return ignored("a chat row with no id, sender or body");
|
|
2265
|
+
store.addChat(message);
|
|
2266
|
+
return HANDLED;
|
|
2267
|
+
}
|
|
2268
|
+
function moderationText(kind, payload) {
|
|
2269
|
+
const by = text2(payload["by"]) ?? "The host";
|
|
2270
|
+
switch (kind) {
|
|
2271
|
+
case "muted": {
|
|
2272
|
+
const source = text2(payload["source"]) ?? "microphone";
|
|
2273
|
+
return source === "microphone" ? `${by} muted you.` : source === "camera" ? `${by} turned off your camera.` : `${by} stopped your screen share.`;
|
|
2274
|
+
}
|
|
2275
|
+
case "ask-unmute":
|
|
2276
|
+
return `${by} is asking you to unmute.`;
|
|
2277
|
+
case "removed":
|
|
2278
|
+
return `${by} removed you from this meeting.`;
|
|
2279
|
+
default:
|
|
2280
|
+
return null;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
function applyModeration(store, payload, now) {
|
|
2284
|
+
const snapshot = store.snapshot();
|
|
2285
|
+
const meetingId = text2(payload["meeting_id"]);
|
|
2286
|
+
if (snapshot.meeting !== null && meetingId !== null && meetingId !== snapshot.meeting.id) {
|
|
2287
|
+
return ignored("a moderation event for a different meeting");
|
|
2288
|
+
}
|
|
2289
|
+
const kind = text2(payload["kind"]);
|
|
2290
|
+
switch (kind) {
|
|
2291
|
+
case "lower-hands": {
|
|
2292
|
+
const raw = payload["identities"];
|
|
2293
|
+
const everyone = raw === "*";
|
|
2294
|
+
const named = new Set(
|
|
2295
|
+
Array.isArray(raw) ? raw.filter((value) => typeof value === "string") : []
|
|
2296
|
+
);
|
|
2297
|
+
for (const participant of snapshot.participants) {
|
|
2298
|
+
if (participant.handRaised && (everyone || named.has(participant.identity))) {
|
|
2299
|
+
store.setHand(participant.identity, false);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
return HANDLED;
|
|
2303
|
+
}
|
|
2304
|
+
case "role": {
|
|
2305
|
+
const identity = text2(payload["identity"]);
|
|
2306
|
+
const role = text2(payload["role"]);
|
|
2307
|
+
if (identity === null || role === null || !ROLES.includes(role)) {
|
|
2308
|
+
return ignored("a role event with no identity or an unknown role");
|
|
2309
|
+
}
|
|
2310
|
+
if (identity === snapshot.localIdentity) {
|
|
2311
|
+
store.patch({
|
|
2312
|
+
grants: { ...snapshot.grants, role },
|
|
2313
|
+
moderationNotice: {
|
|
2314
|
+
kind: "role",
|
|
2315
|
+
text: role === "host" ? "You are now the host." : role === "cohost" ? "You are now a co-host." : "You are no longer a co-host.",
|
|
2316
|
+
at: now()
|
|
2317
|
+
}
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
return HANDLED;
|
|
2321
|
+
}
|
|
2322
|
+
case "muted":
|
|
2323
|
+
case "ask-unmute":
|
|
2324
|
+
case "removed": {
|
|
2325
|
+
const sentence = moderationText(kind, payload);
|
|
2326
|
+
if (sentence === null) return ignored("a moderation event with no sentence");
|
|
2327
|
+
store.patch({ moderationNotice: { kind, text: sentence, at: now() } });
|
|
2328
|
+
return HANDLED;
|
|
2329
|
+
}
|
|
2330
|
+
default:
|
|
2331
|
+
return ignored(`a moderation kind this version does not know: ${String(kind)}`);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
var ROLES = ["host", "cohost", "participant", "guest"];
|
|
1837
2335
|
|
|
1838
2336
|
// src/core/devices.ts
|
|
1839
2337
|
var KINDS = ["audioinput", "videoinput", "audiooutput"];
|
|
@@ -2035,8 +2533,8 @@ function callStateLabel(state, outgoing) {
|
|
|
2035
2533
|
return "Call";
|
|
2036
2534
|
}
|
|
2037
2535
|
}
|
|
2038
|
-
function countLabel(
|
|
2039
|
-
return `${
|
|
2536
|
+
function countLabel(count2, singular, plural = `${singular}s`) {
|
|
2537
|
+
return `${count2} ${count2 === 1 ? singular : plural}`;
|
|
2040
2538
|
}
|
|
2041
2539
|
|
|
2042
2540
|
// src/core/invitations.ts
|
|
@@ -2166,7 +2664,8 @@ function computeStage(args) {
|
|
|
2166
2664
|
const ordered = orderParticipants(args.participants, args.activeSpeakers);
|
|
2167
2665
|
const sharer = ordered.find((participant) => participant.screenSharing) ?? null;
|
|
2168
2666
|
const pinnedParticipant = args.pinned === null ? null : ordered.find((participant) => participant.identity === args.pinned) ?? null;
|
|
2169
|
-
const
|
|
2667
|
+
const spotlit = args.spotlight == null ? null : ordered.find((participant) => participant.identity === args.spotlight) ?? null;
|
|
2668
|
+
const feature = sharer !== null ? sharer : pinnedParticipant !== null && args.layout !== "grid" ? pinnedParticipant : spotlit !== null ? spotlit : args.layout === "grid" ? null : ordered[0] ?? null;
|
|
2170
2669
|
const rest = feature === null ? ordered : ordered.filter((participant) => participant.identity !== feature.identity);
|
|
2171
2670
|
const pageCount = Math.max(1, Math.ceil(rest.length / pageSize));
|
|
2172
2671
|
const page = Math.min(Math.max(0, args.page ?? 0), pageCount - 1);
|
|
@@ -2180,11 +2679,11 @@ function computeStage(args) {
|
|
|
2180
2679
|
presenting: sharer !== null
|
|
2181
2680
|
};
|
|
2182
2681
|
}
|
|
2183
|
-
function gridColumns(
|
|
2184
|
-
if (
|
|
2185
|
-
if (
|
|
2186
|
-
if (
|
|
2187
|
-
if (
|
|
2682
|
+
function gridColumns(count2) {
|
|
2683
|
+
if (count2 <= 1) return 1;
|
|
2684
|
+
if (count2 <= 4) return 2;
|
|
2685
|
+
if (count2 <= 9) return 3;
|
|
2686
|
+
if (count2 <= 16) return 4;
|
|
2188
2687
|
return 5;
|
|
2189
2688
|
}
|
|
2190
2689
|
|
|
@@ -2192,8 +2691,7 @@ function gridColumns(count) {
|
|
|
2192
2691
|
var DATA_TOPICS = {
|
|
2193
2692
|
hand: "mx.hand",
|
|
2194
2693
|
reaction: "mx.reaction",
|
|
2195
|
-
chat: "mx.chat"
|
|
2196
|
-
hostAction: "mx.host"
|
|
2694
|
+
chat: "mx.chat"
|
|
2197
2695
|
};
|
|
2198
2696
|
var TRANSCRIPTION_TOPIC = "lk.transcription";
|
|
2199
2697
|
var TRANSCRIPTION_ATTRIBUTES = {
|
|
@@ -2254,11 +2752,48 @@ function snapshotParticipant(participant, isLocal) {
|
|
|
2254
2752
|
screenSharing: participant.isScreenShareEnabled
|
|
2255
2753
|
};
|
|
2256
2754
|
}
|
|
2755
|
+
var DEFAULT_AUDIO_PROCESSING = {
|
|
2756
|
+
noiseSuppression: true,
|
|
2757
|
+
echoCancellation: true,
|
|
2758
|
+
autoGainControl: true
|
|
2759
|
+
};
|
|
2760
|
+
function videoEffectsSupported() {
|
|
2761
|
+
const scope = globalThis;
|
|
2762
|
+
if (typeof scope.OffscreenCanvas === "undefined" || scope.document === void 0) return false;
|
|
2763
|
+
try {
|
|
2764
|
+
return scope.document.createElement("canvas").getContext("webgl2") !== null;
|
|
2765
|
+
} catch {
|
|
2766
|
+
return false;
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
async function applyVideoEffect(target, effect) {
|
|
2770
|
+
const { Track } = await import("livekit-client");
|
|
2771
|
+
const track = target.localParticipant.getTrackPublication(Track.Source.Camera)?.track;
|
|
2772
|
+
if (track === void 0) return;
|
|
2773
|
+
try {
|
|
2774
|
+
if (effect.kind === "none") {
|
|
2775
|
+
await track.stopProcessor();
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2778
|
+
const processors = await import("@livekit/track-processors");
|
|
2779
|
+
const processor = effect.kind === "blur" ? processors.BackgroundBlur(effect.radius ?? 12) : processors.VirtualBackground(effect.imageUrl);
|
|
2780
|
+
await track.setProcessor(processor);
|
|
2781
|
+
} catch (error) {
|
|
2782
|
+
throw new MeetError(
|
|
2783
|
+
"media-unsupported",
|
|
2784
|
+
"The background effect could not start on this camera",
|
|
2785
|
+
"Background effects need a browser with WebGL2 (current Chrome, Edge, Firefox or Safari). Your video continues without the effect.",
|
|
2786
|
+
error
|
|
2787
|
+
);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2257
2790
|
var encoder = new TextEncoder();
|
|
2258
2791
|
var decoder = new TextDecoder();
|
|
2259
2792
|
function createLiveKitDriver(handlers, options = {}) {
|
|
2260
2793
|
let room = null;
|
|
2261
2794
|
let projectRoster = null;
|
|
2795
|
+
let audioCapture = DEFAULT_AUDIO_PROCESSING;
|
|
2796
|
+
let videoEffect = { kind: "none" };
|
|
2262
2797
|
async function load() {
|
|
2263
2798
|
if (room !== null) return room;
|
|
2264
2799
|
let module2;
|
|
@@ -2287,7 +2822,9 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
2287
2822
|
handlers.participants([local, ...remotes]);
|
|
2288
2823
|
};
|
|
2289
2824
|
projectRoster = roster;
|
|
2290
|
-
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.
|
|
2825
|
+
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) => {
|
|
2826
|
+
handlers.roomMetadata?.(metadata ?? "");
|
|
2827
|
+
}).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
|
|
2291
2828
|
handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
|
|
2292
2829
|
}).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
|
|
2293
2830
|
handlers.quality(participant.identity, mapQuality(quality));
|
|
@@ -2364,6 +2901,7 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
2364
2901
|
throw normalizeDeviceError(error, "meet.connect");
|
|
2365
2902
|
}
|
|
2366
2903
|
projectRoster?.();
|
|
2904
|
+
handlers.roomMetadata?.(target.metadata ?? "");
|
|
2367
2905
|
},
|
|
2368
2906
|
async disconnect() {
|
|
2369
2907
|
if (room === null) return;
|
|
@@ -2378,14 +2916,19 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
2378
2916
|
},
|
|
2379
2917
|
async setMicrophoneEnabled(enabled) {
|
|
2380
2918
|
try {
|
|
2381
|
-
await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(
|
|
2919
|
+
await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(
|
|
2920
|
+
enabled,
|
|
2921
|
+
audioCapture
|
|
2922
|
+
);
|
|
2382
2923
|
} catch (error) {
|
|
2383
2924
|
throw normalizeDeviceError(error, "meet.setMicrophoneEnabled");
|
|
2384
2925
|
}
|
|
2385
2926
|
},
|
|
2386
2927
|
async setCameraEnabled(enabled) {
|
|
2387
2928
|
try {
|
|
2388
|
-
|
|
2929
|
+
const target = requireRoom("setCameraEnabled");
|
|
2930
|
+
await target.localParticipant.setCameraEnabled(enabled);
|
|
2931
|
+
if (enabled && videoEffect.kind !== "none") await applyVideoEffect(target, videoEffect);
|
|
2389
2932
|
} catch (error) {
|
|
2390
2933
|
throw normalizeDeviceError(error, "meet.setCameraEnabled");
|
|
2391
2934
|
}
|
|
@@ -2397,14 +2940,40 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
2397
2940
|
throw normalizeDeviceError(error, "meet.setScreenShareEnabled");
|
|
2398
2941
|
}
|
|
2399
2942
|
},
|
|
2400
|
-
async publishData(topic, payload) {
|
|
2943
|
+
async publishData(topic, payload, to) {
|
|
2401
2944
|
await requireRoom("publishData").localParticipant.publishData(
|
|
2402
2945
|
// `reliable` is not optional for a control plane: a dropped
|
|
2403
2946
|
// "hand lowered" leaves a hand up on every other screen forever.
|
|
2404
2947
|
encoder.encode(payload),
|
|
2405
|
-
{ reliable: true, topic }
|
|
2948
|
+
to === void 0 || to.length === 0 ? { reliable: true, topic } : { reliable: true, topic, destinationIdentities: [...to] }
|
|
2406
2949
|
);
|
|
2407
2950
|
},
|
|
2951
|
+
async setAudioProcessing(processing) {
|
|
2952
|
+
audioCapture = { ...audioCapture, ...processing };
|
|
2953
|
+
const target = room;
|
|
2954
|
+
if (target === null) return;
|
|
2955
|
+
const { Track } = await import("livekit-client");
|
|
2956
|
+
const publication = target.localParticipant.getTrackPublication(Track.Source.Microphone);
|
|
2957
|
+
const track = publication?.track;
|
|
2958
|
+
if (track === void 0 || publication?.isMuted === true) return;
|
|
2959
|
+
try {
|
|
2960
|
+
await track.restartTrack({
|
|
2961
|
+
...audioCapture,
|
|
2962
|
+
...target.getActiveDevice("audioinput") !== void 0 ? { deviceId: target.getActiveDevice("audioinput") } : {}
|
|
2963
|
+
});
|
|
2964
|
+
} catch (error) {
|
|
2965
|
+
throw normalizeDeviceError(error, "meet.setAudioProcessing");
|
|
2966
|
+
}
|
|
2967
|
+
},
|
|
2968
|
+
async setVideoEffect(effect) {
|
|
2969
|
+
videoEffect = effect;
|
|
2970
|
+
const target = room;
|
|
2971
|
+
if (target === null) return;
|
|
2972
|
+
await applyVideoEffect(target, effect);
|
|
2973
|
+
},
|
|
2974
|
+
supportsVideoEffects() {
|
|
2975
|
+
return videoEffectsSupported();
|
|
2976
|
+
},
|
|
2408
2977
|
attach(identity, kind, element) {
|
|
2409
2978
|
const target = requireRoom("attach");
|
|
2410
2979
|
const source = sources?.[kind];
|
|
@@ -2627,6 +3196,11 @@ function listenOnlyNotice(media) {
|
|
|
2627
3196
|
}
|
|
2628
3197
|
|
|
2629
3198
|
// src/core/store.ts
|
|
3199
|
+
var DEFAULT_EFFECTS = {
|
|
3200
|
+
noiseSuppression: true,
|
|
3201
|
+
background: "none",
|
|
3202
|
+
backgroundImage: null
|
|
3203
|
+
};
|
|
2630
3204
|
var NO_GRANTS = {
|
|
2631
3205
|
role: "participant",
|
|
2632
3206
|
canRecord: false,
|
|
@@ -2649,7 +3223,12 @@ var EMPTY = {
|
|
|
2649
3223
|
recordingSource: "local",
|
|
2650
3224
|
recordingId: null,
|
|
2651
3225
|
chat: [],
|
|
3226
|
+
chatLoaded: false,
|
|
2652
3227
|
reactions: [],
|
|
3228
|
+
spotlight: null,
|
|
3229
|
+
moderationNotice: null,
|
|
3230
|
+
actionNotice: null,
|
|
3231
|
+
effects: DEFAULT_EFFECTS,
|
|
2653
3232
|
transcript: [],
|
|
2654
3233
|
notes: [],
|
|
2655
3234
|
names: {},
|
|
@@ -2688,6 +3267,7 @@ function createMeetStore(initial = {}) {
|
|
|
2688
3267
|
setParticipants(participants) {
|
|
2689
3268
|
const names = learnNames(state.names, participants);
|
|
2690
3269
|
const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
|
|
3270
|
+
const handTimes = new Map(state.participants.map((p) => [p.identity, p.handRaisedAt ?? null]));
|
|
2691
3271
|
const quality = new Map(state.participants.map((p) => [p.identity, p.quality]));
|
|
2692
3272
|
commit({
|
|
2693
3273
|
...state,
|
|
@@ -2701,6 +3281,7 @@ function createMeetStore(initial = {}) {
|
|
|
2701
3281
|
// name book. `roster-names.test.ts` fails if a token ever gets past it.
|
|
2702
3282
|
displayName: displayNameFor(names, participant.identity, participant.displayName),
|
|
2703
3283
|
handRaised: hands.get(participant.identity) ?? participant.handRaised,
|
|
3284
|
+
handRaisedAt: handTimes.get(participant.identity) ?? participant.handRaisedAt ?? null,
|
|
2704
3285
|
quality: participant.quality === "unknown" ? quality.get(participant.identity) ?? "unknown" : participant.quality
|
|
2705
3286
|
}))
|
|
2706
3287
|
});
|
|
@@ -2723,11 +3304,17 @@ function createMeetStore(initial = {}) {
|
|
|
2723
3304
|
)
|
|
2724
3305
|
});
|
|
2725
3306
|
},
|
|
2726
|
-
setHand(identity, raised) {
|
|
3307
|
+
setHand(identity, raised, at) {
|
|
2727
3308
|
commit({
|
|
2728
3309
|
...state,
|
|
2729
3310
|
participants: state.participants.map(
|
|
2730
|
-
(participant) => participant.identity === identity ? {
|
|
3311
|
+
(participant) => participant.identity === identity ? {
|
|
3312
|
+
...participant,
|
|
3313
|
+
handRaised: raised,
|
|
3314
|
+
// A hand that is ALREADY up keeps its place in the queue when
|
|
3315
|
+
// it is re-announced (to a late joiner): the first time wins.
|
|
3316
|
+
handRaisedAt: raised ? participant.handRaised && participant.handRaisedAt != null ? participant.handRaisedAt : at ?? Date.now() : null
|
|
3317
|
+
} : participant
|
|
2731
3318
|
)
|
|
2732
3319
|
});
|
|
2733
3320
|
},
|
|
@@ -2737,6 +3324,31 @@ function createMeetStore(initial = {}) {
|
|
|
2737
3324
|
const names = learnName(state.names, message.identity, message.displayName);
|
|
2738
3325
|
commit({ ...state, chat: chat.slice(-CHAT_LIMIT), names });
|
|
2739
3326
|
},
|
|
3327
|
+
loadChat(messages) {
|
|
3328
|
+
const known = new Set(state.chat.map((message) => message.id));
|
|
3329
|
+
const merged = [...messages.filter((message) => !known.has(message.id)), ...state.chat].sort(
|
|
3330
|
+
(a, b) => a.at < b.at ? -1 : a.at > b.at ? 1 : 0
|
|
3331
|
+
);
|
|
3332
|
+
const names = learnNames(
|
|
3333
|
+
state.names,
|
|
3334
|
+
messages.map((message) => ({ identity: message.identity, displayName: message.displayName }))
|
|
3335
|
+
);
|
|
3336
|
+
commit({ ...state, chat: merged.slice(-CHAT_LIMIT), chatLoaded: true, names });
|
|
3337
|
+
},
|
|
3338
|
+
settleChat(localId, next) {
|
|
3339
|
+
const index = state.chat.findIndex((message) => message.id === localId);
|
|
3340
|
+
if (index === -1) return;
|
|
3341
|
+
const current = state.chat[index];
|
|
3342
|
+
let replacement;
|
|
3343
|
+
if ("failed" in next && typeof next.failed === "string" && !("id" in next)) {
|
|
3344
|
+
replacement = { ...current, failed: next.failed };
|
|
3345
|
+
} else {
|
|
3346
|
+
replacement = next;
|
|
3347
|
+
}
|
|
3348
|
+
const duplicate = replacement.id !== localId && state.chat.some((message) => message.id === replacement.id);
|
|
3349
|
+
const chat = duplicate ? state.chat.filter((message) => message.id !== localId) : state.chat.map((message, position) => position === index ? replacement : message);
|
|
3350
|
+
commit({ ...state, chat });
|
|
3351
|
+
},
|
|
2740
3352
|
addReaction(reaction) {
|
|
2741
3353
|
commit({ ...state, reactions: [...state.reactions, reaction] });
|
|
2742
3354
|
},
|
|
@@ -2827,7 +3439,7 @@ function createMeetStore(initial = {}) {
|
|
|
2827
3439
|
commit({ ...state, media: { ...state.media, [kind]: next } });
|
|
2828
3440
|
},
|
|
2829
3441
|
reset() {
|
|
2830
|
-
commit({ ...EMPTY });
|
|
3442
|
+
commit({ ...EMPTY, effects: state.effects });
|
|
2831
3443
|
}
|
|
2832
3444
|
};
|
|
2833
3445
|
}
|
|
@@ -3157,11 +3769,11 @@ function createMeetingSession(options) {
|
|
|
3157
3769
|
} catch {
|
|
3158
3770
|
body = null;
|
|
3159
3771
|
}
|
|
3160
|
-
const
|
|
3772
|
+
const refusal2 = readServerRefusal(body);
|
|
3161
3773
|
throw new MeetError(
|
|
3162
3774
|
response.status === 403 ? "forbidden" : "transport",
|
|
3163
|
-
|
|
3164
|
-
|
|
3775
|
+
refusal2?.reason ?? `meet.endForEveryone: the server answered ${response.status}`,
|
|
3776
|
+
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."
|
|
3165
3777
|
);
|
|
3166
3778
|
}
|
|
3167
3779
|
let payload = null;
|
|
@@ -3289,6 +3901,57 @@ function fileDownloadPath(fileId) {
|
|
|
3289
3901
|
return `/files/${encodeURIComponent(fileId)}/download?inline=1`;
|
|
3290
3902
|
}
|
|
3291
3903
|
|
|
3904
|
+
// src/core/management.ts
|
|
3905
|
+
async function refusal(response, operation) {
|
|
3906
|
+
let body = null;
|
|
3907
|
+
try {
|
|
3908
|
+
body = await response.json();
|
|
3909
|
+
} catch {
|
|
3910
|
+
body = null;
|
|
3911
|
+
}
|
|
3912
|
+
const said = readServerRefusal(body);
|
|
3913
|
+
return new MeetError(
|
|
3914
|
+
response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
|
|
3915
|
+
`${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
|
|
3916
|
+
said?.remedy || "The meeting itself is saved \u2014 only the telling failed. Try again; the call is idempotent."
|
|
3917
|
+
);
|
|
3918
|
+
}
|
|
3919
|
+
function count(body, key) {
|
|
3920
|
+
const value = body[key];
|
|
3921
|
+
return typeof value === "number" ? value : 0;
|
|
3922
|
+
}
|
|
3923
|
+
async function announceMeeting(api, meetingId) {
|
|
3924
|
+
const operation = "meet.announceMeeting";
|
|
3925
|
+
const response = await api.request({
|
|
3926
|
+
path: meetingPath(MEET_ROUTES.meetingAnnounce, meetingId),
|
|
3927
|
+
method: "POST",
|
|
3928
|
+
requireAuth: true,
|
|
3929
|
+
operation
|
|
3930
|
+
});
|
|
3931
|
+
if (!response.ok) throw await refusal(response, operation);
|
|
3932
|
+
const body = await response.json();
|
|
3933
|
+
return {
|
|
3934
|
+
invitations: count(body, "invitations"),
|
|
3935
|
+
updates: count(body, "updates"),
|
|
3936
|
+
cancellations: count(body, "cancellations"),
|
|
3937
|
+
alreadyCurrent: count(body, "already_current"),
|
|
3938
|
+
remindersQueued: count(body, "reminders_queued"),
|
|
3939
|
+
remindersCancelled: count(body, "reminders_cancelled")
|
|
3940
|
+
};
|
|
3941
|
+
}
|
|
3942
|
+
async function pushMeetingToGoogleCalendar(api, meetingId, target) {
|
|
3943
|
+
const operation = "meet.pushMeetingToGoogleCalendar";
|
|
3944
|
+
const response = await api.request({
|
|
3945
|
+
path: meetingPath(MEET_ROUTES.meetingGoogleCalendar, meetingId),
|
|
3946
|
+
method: "POST",
|
|
3947
|
+
requireAuth: true,
|
|
3948
|
+
operation,
|
|
3949
|
+
body: { connection_id: target.connectionId, calendar_id: target.calendarId ?? "primary" }
|
|
3950
|
+
});
|
|
3951
|
+
if (!response.ok) throw await refusal(response, operation);
|
|
3952
|
+
return await response.json();
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3292
3955
|
// src/core/room.ts
|
|
3293
3956
|
var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
|
|
3294
3957
|
var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
|
|
@@ -3408,7 +4071,37 @@ function createRoomEngine(options) {
|
|
|
3408
4071
|
const handlers = {
|
|
3409
4072
|
participants(list) {
|
|
3410
4073
|
const joinedAt = new Date(now()).toISOString();
|
|
4074
|
+
const before = new Set(store.snapshot().participants.map((p) => p.identity));
|
|
3411
4075
|
store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
|
|
4076
|
+
const snapshot = store.snapshot();
|
|
4077
|
+
const newcomer = snapshot.participants.some(
|
|
4078
|
+
(participant) => !participant.isLocal && !before.has(participant.identity)
|
|
4079
|
+
);
|
|
4080
|
+
const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
|
|
4081
|
+
if (newcomer && before.size > 0 && me?.handRaised === true) {
|
|
4082
|
+
void publish(DATA_TOPICS.hand, { raised: true, raisedAt: me.handRaisedAt ?? now() }).catch(
|
|
4083
|
+
() => void 0
|
|
4084
|
+
);
|
|
4085
|
+
}
|
|
4086
|
+
},
|
|
4087
|
+
roomMetadata(metadata) {
|
|
4088
|
+
let spotlight = null;
|
|
4089
|
+
if (metadata.length > 0) {
|
|
4090
|
+
try {
|
|
4091
|
+
const parsed = JSON.parse(metadata);
|
|
4092
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
4093
|
+
const value = parsed.spotlight;
|
|
4094
|
+
if (typeof value === "string" && value.length > 0) spotlight = asParticipantIdentity(value);
|
|
4095
|
+
}
|
|
4096
|
+
} catch {
|
|
4097
|
+
report(
|
|
4098
|
+
"warn",
|
|
4099
|
+
"meet: the room's metadata is not JSON, so no spotlight was read from it",
|
|
4100
|
+
"Only aidream writes room metadata; check services/meet/moderation.py."
|
|
4101
|
+
);
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
|
|
3412
4105
|
},
|
|
3413
4106
|
activeSpeakers(identities) {
|
|
3414
4107
|
store.setSpeaking(identities.map(asParticipantIdentity));
|
|
@@ -3512,7 +4205,11 @@ function createRoomEngine(options) {
|
|
|
3512
4205
|
const sender = asParticipantIdentity(from ?? "");
|
|
3513
4206
|
switch (topic) {
|
|
3514
4207
|
case DATA_TOPICS.hand:
|
|
3515
|
-
store.setHand(
|
|
4208
|
+
store.setHand(
|
|
4209
|
+
sender,
|
|
4210
|
+
envelope.raised === true,
|
|
4211
|
+
typeof envelope.raisedAt === "number" ? envelope.raisedAt : now()
|
|
4212
|
+
);
|
|
3516
4213
|
return;
|
|
3517
4214
|
case DATA_TOPICS.reaction:
|
|
3518
4215
|
if (typeof envelope.emoji === "string") {
|
|
@@ -3526,12 +4223,14 @@ function createRoomEngine(options) {
|
|
|
3526
4223
|
return;
|
|
3527
4224
|
case DATA_TOPICS.chat:
|
|
3528
4225
|
if (typeof envelope.text === "string") {
|
|
4226
|
+
const local = store.snapshot().localIdentity;
|
|
3529
4227
|
store.addChat({
|
|
3530
4228
|
id: envelope.id ?? `${sender}-${now()}`,
|
|
3531
4229
|
identity: sender,
|
|
3532
4230
|
displayName: envelope.name ?? sender,
|
|
3533
4231
|
text: envelope.text,
|
|
3534
|
-
at: envelope.at ?? new Date(now()).toISOString()
|
|
4232
|
+
at: envelope.at ?? new Date(now()).toISOString(),
|
|
4233
|
+
...typeof envelope.to === "string" && local !== null ? { to: local } : {}
|
|
3535
4234
|
});
|
|
3536
4235
|
}
|
|
3537
4236
|
return;
|
|
@@ -3577,6 +4276,24 @@ function createRoomEngine(options) {
|
|
|
3577
4276
|
store.patch({ phase: "connected", failure: null });
|
|
3578
4277
|
store.setLocalTrack("microphone", IDLE_TRACK);
|
|
3579
4278
|
store.setLocalTrack("camera", IDLE_TRACK);
|
|
4279
|
+
const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
|
|
4280
|
+
store.patch({ effects });
|
|
4281
|
+
if (!effects.noiseSuppression || effects.background !== "none") {
|
|
4282
|
+
try {
|
|
4283
|
+
await applyEffects(created, effects);
|
|
4284
|
+
} catch {
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
for (const kind of ["audioinput", "videoinput", "audiooutput"]) {
|
|
4288
|
+
const chosen = options.resolveDevice?.(kind) ?? null;
|
|
4289
|
+
if (chosen === null || chosen === "default") continue;
|
|
4290
|
+
try {
|
|
4291
|
+
await created.switchDevice(kind, chosen);
|
|
4292
|
+
} catch (error) {
|
|
4293
|
+
const normalized = normalizeDeviceError(error, `meet.switchDevice(${kind})`);
|
|
4294
|
+
report("warn", normalized.message, normalized.remedy);
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
3580
4297
|
if (request.mode === "video") {
|
|
3581
4298
|
await enableTrack(created, "camera", true);
|
|
3582
4299
|
}
|
|
@@ -3599,9 +4316,30 @@ function createRoomEngine(options) {
|
|
|
3599
4316
|
return state;
|
|
3600
4317
|
}
|
|
3601
4318
|
}
|
|
3602
|
-
async function publish(topic, body) {
|
|
4319
|
+
async function publish(topic, body, to) {
|
|
3603
4320
|
if (driver === null) return;
|
|
3604
|
-
await driver.publishData(topic, JSON.stringify(body));
|
|
4321
|
+
await driver.publishData(topic, JSON.stringify(body), to);
|
|
4322
|
+
}
|
|
4323
|
+
async function applyEffects(target, effects) {
|
|
4324
|
+
try {
|
|
4325
|
+
await target.setAudioProcessing?.({
|
|
4326
|
+
noiseSuppression: effects.noiseSuppression,
|
|
4327
|
+
echoCancellation: true,
|
|
4328
|
+
autoGainControl: true
|
|
4329
|
+
});
|
|
4330
|
+
} catch (error) {
|
|
4331
|
+
report("warn", `meet: noise suppression could not be applied: ${String(error)}`, "The microphone keeps working without it.");
|
|
4332
|
+
}
|
|
4333
|
+
if (target.setVideoEffect === void 0) return;
|
|
4334
|
+
const effect = effects.background === "blur" ? { kind: "blur" } : effects.background === "image" && effects.backgroundImage !== null ? { kind: "image", imageUrl: effects.backgroundImage } : { kind: "none" };
|
|
4335
|
+
if (effect.kind !== "none" && target.supportsVideoEffects?.() === false) return;
|
|
4336
|
+
try {
|
|
4337
|
+
await target.setVideoEffect(effect);
|
|
4338
|
+
} catch (error) {
|
|
4339
|
+
const normalized = normalizeMeetError(error, "meet.setVideoEffect");
|
|
4340
|
+
report("warn", normalized.message, normalized.remedy);
|
|
4341
|
+
throw normalized;
|
|
4342
|
+
}
|
|
3605
4343
|
}
|
|
3606
4344
|
return {
|
|
3607
4345
|
join(request) {
|
|
@@ -3657,7 +4395,7 @@ function createRoomEngine(options) {
|
|
|
3657
4395
|
async raiseHand(raised) {
|
|
3658
4396
|
const local = store.snapshot().localIdentity;
|
|
3659
4397
|
if (local !== null) store.setHand(local, raised);
|
|
3660
|
-
await publish(DATA_TOPICS.hand, { raised });
|
|
4398
|
+
await publish(DATA_TOPICS.hand, { raised, raisedAt: now() });
|
|
3661
4399
|
},
|
|
3662
4400
|
async react(emoji) {
|
|
3663
4401
|
const local = store.snapshot().localIdentity;
|
|
@@ -3667,32 +4405,41 @@ function createRoomEngine(options) {
|
|
|
3667
4405
|
}
|
|
3668
4406
|
await publish(DATA_TOPICS.reaction, { id, emoji });
|
|
3669
4407
|
},
|
|
3670
|
-
async
|
|
4408
|
+
async sendPrivateChat(to, text3) {
|
|
3671
4409
|
const trimmed = text3.trim();
|
|
3672
4410
|
if (trimmed.length === 0) return;
|
|
3673
4411
|
const local = store.snapshot().localIdentity;
|
|
3674
4412
|
const id = `${local ?? "local"}-${now()}`;
|
|
3675
4413
|
const at = new Date(now()).toISOString();
|
|
3676
4414
|
if (local !== null) {
|
|
3677
|
-
store.addChat({
|
|
3678
|
-
id,
|
|
3679
|
-
identity: local,
|
|
3680
|
-
displayName: identity.displayName,
|
|
3681
|
-
text: trimmed,
|
|
3682
|
-
at
|
|
3683
|
-
});
|
|
4415
|
+
store.addChat({ id, identity: local, displayName: identity.displayName, text: trimmed, at, to });
|
|
3684
4416
|
}
|
|
3685
|
-
await publish(
|
|
4417
|
+
await publish(
|
|
4418
|
+
DATA_TOPICS.chat,
|
|
4419
|
+
{ id, text: trimmed, at, name: identity.displayName, to },
|
|
4420
|
+
[to]
|
|
4421
|
+
);
|
|
3686
4422
|
},
|
|
3687
|
-
async
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
4423
|
+
async setEffects(next) {
|
|
4424
|
+
const previous = store.snapshot().effects;
|
|
4425
|
+
const merged = { ...previous, ...next };
|
|
4426
|
+
store.patch({ effects: merged });
|
|
4427
|
+
options.effectsStorage?.save(merged);
|
|
4428
|
+
if (driver === null) return;
|
|
4429
|
+
try {
|
|
4430
|
+
await applyEffects(driver, merged);
|
|
4431
|
+
} catch {
|
|
4432
|
+
const reverted = { ...merged, background: previous.background, backgroundImage: previous.backgroundImage };
|
|
4433
|
+
store.patch({ effects: reverted });
|
|
4434
|
+
options.effectsStorage?.save(reverted);
|
|
3694
4435
|
}
|
|
3695
|
-
|
|
4436
|
+
},
|
|
4437
|
+
supportsVideoEffects() {
|
|
4438
|
+
return driver?.supportsVideoEffects?.() ?? false;
|
|
4439
|
+
},
|
|
4440
|
+
async switchDevice(kind, deviceId) {
|
|
4441
|
+
if (driver === null) return;
|
|
4442
|
+
await driver.switchDevice(kind, deviceId);
|
|
3696
4443
|
},
|
|
3697
4444
|
pin(who) {
|
|
3698
4445
|
store.patch({ pinned: who, layout: who === null ? "grid" : "speaker" });
|
|
@@ -3723,6 +4470,192 @@ function hostUserIds(participants) {
|
|
|
3723
4470
|
return participants.filter((participant) => participant.role === "host" && participant.userId !== null).map((participant) => participant.userId);
|
|
3724
4471
|
}
|
|
3725
4472
|
|
|
4473
|
+
// src/core/effects.ts
|
|
4474
|
+
function coerce(value) {
|
|
4475
|
+
if (typeof value !== "object" || value === null) return DEFAULT_EFFECTS;
|
|
4476
|
+
const row = value;
|
|
4477
|
+
const background = row["background"] === "blur" || row["background"] === "image" ? row["background"] : "none";
|
|
4478
|
+
const backgroundImage = typeof row["backgroundImage"] === "string" ? row["backgroundImage"] : null;
|
|
4479
|
+
return {
|
|
4480
|
+
noiseSuppression: row["noiseSuppression"] !== false,
|
|
4481
|
+
background: background === "image" && backgroundImage === null ? "none" : background,
|
|
4482
|
+
backgroundImage
|
|
4483
|
+
};
|
|
4484
|
+
}
|
|
4485
|
+
function createWebEffectsStorage(key = "mx.meet.effects") {
|
|
4486
|
+
let memory = DEFAULT_EFFECTS;
|
|
4487
|
+
const storage = globalThis.localStorage;
|
|
4488
|
+
return {
|
|
4489
|
+
load() {
|
|
4490
|
+
if (storage === void 0) return memory;
|
|
4491
|
+
try {
|
|
4492
|
+
const raw = storage.getItem(key);
|
|
4493
|
+
return raw === null ? memory : coerce(JSON.parse(raw));
|
|
4494
|
+
} catch {
|
|
4495
|
+
return memory;
|
|
4496
|
+
}
|
|
4497
|
+
},
|
|
4498
|
+
save(effects) {
|
|
4499
|
+
memory = effects;
|
|
4500
|
+
try {
|
|
4501
|
+
storage?.setItem(key, JSON.stringify(effects));
|
|
4502
|
+
} catch {
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
};
|
|
4506
|
+
}
|
|
4507
|
+
|
|
4508
|
+
// src/core/in-room.ts
|
|
4509
|
+
var MODERATION_ACTIONS = [
|
|
4510
|
+
"mute",
|
|
4511
|
+
"stop_video",
|
|
4512
|
+
"stop_screen",
|
|
4513
|
+
"mute_all",
|
|
4514
|
+
"ask_unmute",
|
|
4515
|
+
"ask_all_unmute",
|
|
4516
|
+
"spotlight",
|
|
4517
|
+
"lower_hand",
|
|
4518
|
+
"lower_all_hands",
|
|
4519
|
+
"make_cohost",
|
|
4520
|
+
"revoke_cohost",
|
|
4521
|
+
"transfer_host",
|
|
4522
|
+
"rename",
|
|
4523
|
+
"remove",
|
|
4524
|
+
"report"
|
|
4525
|
+
];
|
|
4526
|
+
function createInRoomClient(options) {
|
|
4527
|
+
const { api, store } = options;
|
|
4528
|
+
const now = options.now ?? (() => Date.now());
|
|
4529
|
+
function meetingId(operation) {
|
|
4530
|
+
const meeting = store.snapshot().meeting;
|
|
4531
|
+
if (meeting === null) {
|
|
4532
|
+
throw new MeetError(
|
|
4533
|
+
"not-found",
|
|
4534
|
+
`${operation}: this room has no meeting record`,
|
|
4535
|
+
"Moderation and saved chat belong to a meeting. An ad-hoc call has neither."
|
|
4536
|
+
);
|
|
4537
|
+
}
|
|
4538
|
+
return meeting.id;
|
|
4539
|
+
}
|
|
4540
|
+
function headers() {
|
|
4541
|
+
const token = options.resolveRoomToken();
|
|
4542
|
+
return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
|
|
4543
|
+
}
|
|
4544
|
+
async function refusal2(response, operation, remedy) {
|
|
4545
|
+
let body = null;
|
|
4546
|
+
try {
|
|
4547
|
+
body = await response.json();
|
|
4548
|
+
} catch {
|
|
4549
|
+
body = null;
|
|
4550
|
+
}
|
|
4551
|
+
const said = readServerRefusal(body);
|
|
4552
|
+
return new MeetError(
|
|
4553
|
+
response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
|
|
4554
|
+
said?.reason ?? `${operation}: the server answered ${response.status}`,
|
|
4555
|
+
said?.remedy !== void 0 && said.remedy.length > 0 ? said.remedy : remedy
|
|
4556
|
+
);
|
|
4557
|
+
}
|
|
4558
|
+
return {
|
|
4559
|
+
async moderate(request) {
|
|
4560
|
+
const operation = `meet.moderate(${request.action})`;
|
|
4561
|
+
const hdrs = headers();
|
|
4562
|
+
const response = await api.request({
|
|
4563
|
+
path: MEET_ROUTES.moderate,
|
|
4564
|
+
method: "POST",
|
|
4565
|
+
operation,
|
|
4566
|
+
body: {
|
|
4567
|
+
meeting_id: meetingId(operation),
|
|
4568
|
+
action: request.action,
|
|
4569
|
+
...request.identity != null ? { identity: request.identity } : {},
|
|
4570
|
+
...request.name !== void 0 ? { name: request.name } : {},
|
|
4571
|
+
...request.reason !== void 0 ? { reason: request.reason } : {}
|
|
4572
|
+
},
|
|
4573
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
4574
|
+
});
|
|
4575
|
+
if (!response.ok) {
|
|
4576
|
+
throw await refusal2(
|
|
4577
|
+
response,
|
|
4578
|
+
operation,
|
|
4579
|
+
"Nothing was changed. The server decides host powers from the meeting itself."
|
|
4580
|
+
);
|
|
4581
|
+
}
|
|
4582
|
+
const payload = await response.json();
|
|
4583
|
+
return {
|
|
4584
|
+
action: request.action,
|
|
4585
|
+
affected: Array.isArray(payload["affected"]) ? payload["affected"].filter((value) => typeof value === "string") : [],
|
|
4586
|
+
message: typeof payload["message"] === "string" ? payload["message"] : "Done."
|
|
4587
|
+
};
|
|
4588
|
+
},
|
|
4589
|
+
async sendChat(text3) {
|
|
4590
|
+
const body = text3.trim();
|
|
4591
|
+
if (body.length === 0) return;
|
|
4592
|
+
const operation = "meet.sendChat";
|
|
4593
|
+
const id = meetingId(operation);
|
|
4594
|
+
const snapshot = store.snapshot();
|
|
4595
|
+
const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
|
|
4596
|
+
const localId = `pending-${now()}`;
|
|
4597
|
+
const pending = {
|
|
4598
|
+
id: localId,
|
|
4599
|
+
identity: snapshot.localIdentity ?? "local",
|
|
4600
|
+
displayName: me?.displayName ?? "You",
|
|
4601
|
+
text: body,
|
|
4602
|
+
at: new Date(now()).toISOString(),
|
|
4603
|
+
saved: false
|
|
4604
|
+
};
|
|
4605
|
+
store.addChat(pending);
|
|
4606
|
+
const hdrs = headers();
|
|
4607
|
+
let response;
|
|
4608
|
+
try {
|
|
4609
|
+
response = await api.request({
|
|
4610
|
+
path: MEET_ROUTES.chat,
|
|
4611
|
+
method: "POST",
|
|
4612
|
+
operation,
|
|
4613
|
+
body: { meeting_id: id, body },
|
|
4614
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
4615
|
+
});
|
|
4616
|
+
} catch (error) {
|
|
4617
|
+
store.settleChat(localId, { failed: "Not sent \u2014 the server could not be reached." });
|
|
4618
|
+
throw error;
|
|
4619
|
+
}
|
|
4620
|
+
if (!response.ok) {
|
|
4621
|
+
const error = await refusal2(response, operation, "Your message was not sent.");
|
|
4622
|
+
store.settleChat(localId, { failed: error.message });
|
|
4623
|
+
throw error;
|
|
4624
|
+
}
|
|
4625
|
+
const saved = chatFromRow(await response.json());
|
|
4626
|
+
if (saved === null) {
|
|
4627
|
+
store.settleChat(localId, { failed: "The server's answer could not be read." });
|
|
4628
|
+
return;
|
|
4629
|
+
}
|
|
4630
|
+
store.settleChat(localId, saved);
|
|
4631
|
+
},
|
|
4632
|
+
async loadChat() {
|
|
4633
|
+
const operation = "meet.loadChat";
|
|
4634
|
+
const id = meetingId(operation);
|
|
4635
|
+
const hdrs = headers();
|
|
4636
|
+
const response = await api.request({
|
|
4637
|
+
path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(id)}`,
|
|
4638
|
+
method: "GET",
|
|
4639
|
+
operation,
|
|
4640
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
4641
|
+
});
|
|
4642
|
+
if (!response.ok) {
|
|
4643
|
+
throw await refusal2(
|
|
4644
|
+
response,
|
|
4645
|
+
operation,
|
|
4646
|
+
"Messages sent before you joined could not be read. New messages still arrive."
|
|
4647
|
+
);
|
|
4648
|
+
}
|
|
4649
|
+
const payload = await response.json();
|
|
4650
|
+
const rows = Array.isArray(payload.messages) ? payload.messages : [];
|
|
4651
|
+
const messages = rows.map(
|
|
4652
|
+
(row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
|
|
4653
|
+
).filter((message) => message !== null);
|
|
4654
|
+
store.loadChat(messages);
|
|
4655
|
+
}
|
|
4656
|
+
};
|
|
4657
|
+
}
|
|
4658
|
+
|
|
3726
4659
|
// src/core/record.ts
|
|
3727
4660
|
var import_format = require("@ai-matrx/kit/format");
|
|
3728
4661
|
function activeWrapUpSummary(notes) {
|
|
@@ -3863,7 +4796,7 @@ function meetingWhen(meeting, locale) {
|
|
|
3863
4796
|
if (start === null) return "Time not recorded";
|
|
3864
4797
|
const startedAt = new Date(start);
|
|
3865
4798
|
if (Number.isNaN(startedAt.getTime())) return "Time not recorded";
|
|
3866
|
-
const
|
|
4799
|
+
const when2 = startedAt.toLocaleString(locale, {
|
|
3867
4800
|
weekday: "long",
|
|
3868
4801
|
year: "numeric",
|
|
3869
4802
|
month: "long",
|
|
@@ -3871,11 +4804,11 @@ function meetingWhen(meeting, locale) {
|
|
|
3871
4804
|
hour: "numeric",
|
|
3872
4805
|
minute: "2-digit"
|
|
3873
4806
|
});
|
|
3874
|
-
if (meeting.endedAt === null || meeting.startedAt === null) return
|
|
4807
|
+
if (meeting.endedAt === null || meeting.startedAt === null) return when2;
|
|
3875
4808
|
const endedAt = new Date(meeting.endedAt);
|
|
3876
|
-
if (Number.isNaN(endedAt.getTime())) return
|
|
4809
|
+
if (Number.isNaN(endedAt.getTime())) return when2;
|
|
3877
4810
|
const elapsedMs = endedAt.getTime() - startedAt.getTime();
|
|
3878
|
-
return `${
|
|
4811
|
+
return `${when2} \xB7 ${(0, import_format.formatDurationMs)(elapsedMs, { style: "long" })}`;
|
|
3879
4812
|
}
|
|
3880
4813
|
var GUEST_RECORD_ACCESS_LEVELS = [
|
|
3881
4814
|
"none",
|
|
@@ -3951,8 +4884,8 @@ async function loadGuestMeetingRecord(args) {
|
|
|
3951
4884
|
);
|
|
3952
4885
|
}
|
|
3953
4886
|
if (!response.ok) {
|
|
3954
|
-
const
|
|
3955
|
-
return refusedBundle(meeting,
|
|
4887
|
+
const refusal2 = await readRefusal(response);
|
|
4888
|
+
return refusedBundle(meeting, refusal2.message, refusal2.remedy);
|
|
3956
4889
|
}
|
|
3957
4890
|
const body = await response.json();
|
|
3958
4891
|
const payload = typeof body["data"] === "object" && body["data"] !== null ? body["data"] : body;
|
|
@@ -4043,8 +4976,8 @@ async function setGuestRecordAccess(args) {
|
|
|
4043
4976
|
}
|
|
4044
4977
|
});
|
|
4045
4978
|
if (!response.ok) {
|
|
4046
|
-
const
|
|
4047
|
-
throw new MeetError("forbidden",
|
|
4979
|
+
const refusal2 = await readRefusal(response);
|
|
4980
|
+
throw new MeetError("forbidden", refusal2.message, refusal2.remedy);
|
|
4048
4981
|
}
|
|
4049
4982
|
const body = await response.json();
|
|
4050
4983
|
const payload = typeof body["data"] === "object" && body["data"] !== null ? body["data"] : body;
|