@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/react.js
CHANGED
|
@@ -45,7 +45,7 @@ var MeetError = class extends Error {
|
|
|
45
45
|
};
|
|
46
46
|
var FORBIDDEN_CODES = /* @__PURE__ */ new Set(["42501", "PGRST301", "PGRST302"]);
|
|
47
47
|
var NOT_FOUND_CODES = /* @__PURE__ */ new Set(["PGRST116", "PGRST205"]);
|
|
48
|
-
var CONFLICT_CODES = /* @__PURE__ */ new Set(["23505"]);
|
|
48
|
+
var CONFLICT_CODES = /* @__PURE__ */ new Set(["23505", "40001", "55000"]);
|
|
49
49
|
var SESSION_MARKERS = [
|
|
50
50
|
"auth session missing",
|
|
51
51
|
"jwt expired",
|
|
@@ -203,8 +203,28 @@ var MEET_ROUTES = {
|
|
|
203
203
|
*/
|
|
204
204
|
record: "/api/v1/meet/record",
|
|
205
205
|
/** A host chooses what guests may read of ONE meeting (MRF-1). */
|
|
206
|
-
recordAccess: "/api/v1/meet/record/access"
|
|
206
|
+
recordAccess: "/api/v1/meet/record/access",
|
|
207
|
+
/**
|
|
208
|
+
* Meet wave 1 (0.8): bring every invitee up to date after a write — sends the
|
|
209
|
+
* invitation / update / cancellation emails with their `.ics` and re-queues
|
|
210
|
+
* reminders. Idempotent. `{meeting_id}` is substituted by `meetingPath`.
|
|
211
|
+
*/
|
|
212
|
+
meetingAnnounce: "/api/v1/meet/meetings/{meeting_id}/announce",
|
|
213
|
+
/** Put a one-off meeting on the caller's own Google Calendar (gated by the server). */
|
|
214
|
+
meetingGoogleCalendar: "/api/v1/meet/meetings/{meeting_id}/google-calendar",
|
|
215
|
+
/**
|
|
216
|
+
* Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
|
|
217
|
+
* unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
|
|
218
|
+
* Decided by the server from the meeting itself; a guest's pass rides
|
|
219
|
+
* `x-meet-room-token`.
|
|
220
|
+
*/
|
|
221
|
+
moderate: "/api/v1/meet/moderate",
|
|
222
|
+
/** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */
|
|
223
|
+
chat: "/api/v1/meet/chat"
|
|
207
224
|
};
|
|
225
|
+
function meetingPath(template, meetingId) {
|
|
226
|
+
return template.replace("{meeting_id}", encodeURIComponent(meetingId));
|
|
227
|
+
}
|
|
208
228
|
var ROOM_TOKEN_HEADER = "x-meet-room-token";
|
|
209
229
|
var REFRESH_RATIO = 0.8;
|
|
210
230
|
function readString(source, key) {
|
|
@@ -334,6 +354,19 @@ function createTokenClient(options) {
|
|
|
334
354
|
"The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry."
|
|
335
355
|
);
|
|
336
356
|
}
|
|
357
|
+
if (response.status === 410) {
|
|
358
|
+
let reason = "This meeting was cancelled.";
|
|
359
|
+
try {
|
|
360
|
+
const body = await response.json();
|
|
361
|
+
if (typeof body?.detail?.message === "string") reason = body.detail.message;
|
|
362
|
+
} catch {
|
|
363
|
+
}
|
|
364
|
+
throw new MeetError(
|
|
365
|
+
"cancelled",
|
|
366
|
+
reason,
|
|
367
|
+
"Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one."
|
|
368
|
+
);
|
|
369
|
+
}
|
|
337
370
|
if (response.status === 423) {
|
|
338
371
|
throw new MeetError(
|
|
339
372
|
"room-locked",
|
|
@@ -535,12 +568,12 @@ function createMeetAi(options) {
|
|
|
535
568
|
async function dispatch(route, body, operation) {
|
|
536
569
|
const response = await send({ path: route, body, operation, requireAuth: true });
|
|
537
570
|
if (!response.ok) {
|
|
538
|
-
const
|
|
539
|
-
options.store.patch({ noteTakerRefusal:
|
|
571
|
+
const refusal2 = await refusalFor(response, operation);
|
|
572
|
+
options.store.patch({ noteTakerRefusal: refusal2 });
|
|
540
573
|
throw new MeetError(
|
|
541
574
|
response.status === 403 ? "forbidden" : "transport",
|
|
542
|
-
|
|
543
|
-
|
|
575
|
+
refusal2.reason,
|
|
576
|
+
refusal2.remedy
|
|
544
577
|
);
|
|
545
578
|
}
|
|
546
579
|
options.store.patch({ noteTakerRefusal: null });
|
|
@@ -578,11 +611,11 @@ function createMeetAi(options) {
|
|
|
578
611
|
...signal !== void 0 ? { signal } : {}
|
|
579
612
|
});
|
|
580
613
|
if (!response.ok) {
|
|
581
|
-
const
|
|
614
|
+
const refusal2 = await refusalFor(response, "meet.ai.ask");
|
|
582
615
|
throw new MeetError(
|
|
583
616
|
response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : "transport",
|
|
584
|
-
|
|
585
|
-
|
|
617
|
+
refusal2.reason,
|
|
618
|
+
refusal2.remedy
|
|
586
619
|
);
|
|
587
620
|
}
|
|
588
621
|
const body = response.body;
|
|
@@ -1492,6 +1525,11 @@ function captionSegment(args) {
|
|
|
1492
1525
|
}
|
|
1493
1526
|
|
|
1494
1527
|
// src/core/store.ts
|
|
1528
|
+
var DEFAULT_EFFECTS = {
|
|
1529
|
+
noiseSuppression: true,
|
|
1530
|
+
background: "none",
|
|
1531
|
+
backgroundImage: null
|
|
1532
|
+
};
|
|
1495
1533
|
var NO_GRANTS = {
|
|
1496
1534
|
role: "participant",
|
|
1497
1535
|
canRecord: false,
|
|
@@ -1514,7 +1552,12 @@ var EMPTY = {
|
|
|
1514
1552
|
recordingSource: "local",
|
|
1515
1553
|
recordingId: null,
|
|
1516
1554
|
chat: [],
|
|
1555
|
+
chatLoaded: false,
|
|
1517
1556
|
reactions: [],
|
|
1557
|
+
spotlight: null,
|
|
1558
|
+
moderationNotice: null,
|
|
1559
|
+
actionNotice: null,
|
|
1560
|
+
effects: DEFAULT_EFFECTS,
|
|
1518
1561
|
transcript: [],
|
|
1519
1562
|
notes: [],
|
|
1520
1563
|
names: {},
|
|
@@ -1553,6 +1596,7 @@ function createMeetStore(initial = {}) {
|
|
|
1553
1596
|
setParticipants(participants) {
|
|
1554
1597
|
const names = learnNames(state.names, participants);
|
|
1555
1598
|
const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
|
|
1599
|
+
const handTimes = new Map(state.participants.map((p) => [p.identity, p.handRaisedAt ?? null]));
|
|
1556
1600
|
const quality = new Map(state.participants.map((p) => [p.identity, p.quality]));
|
|
1557
1601
|
commit({
|
|
1558
1602
|
...state,
|
|
@@ -1566,6 +1610,7 @@ function createMeetStore(initial = {}) {
|
|
|
1566
1610
|
// name book. `roster-names.test.ts` fails if a token ever gets past it.
|
|
1567
1611
|
displayName: displayNameFor(names, participant.identity, participant.displayName),
|
|
1568
1612
|
handRaised: hands.get(participant.identity) ?? participant.handRaised,
|
|
1613
|
+
handRaisedAt: handTimes.get(participant.identity) ?? participant.handRaisedAt ?? null,
|
|
1569
1614
|
quality: participant.quality === "unknown" ? quality.get(participant.identity) ?? "unknown" : participant.quality
|
|
1570
1615
|
}))
|
|
1571
1616
|
});
|
|
@@ -1588,11 +1633,17 @@ function createMeetStore(initial = {}) {
|
|
|
1588
1633
|
)
|
|
1589
1634
|
});
|
|
1590
1635
|
},
|
|
1591
|
-
setHand(identity, raised) {
|
|
1636
|
+
setHand(identity, raised, at) {
|
|
1592
1637
|
commit({
|
|
1593
1638
|
...state,
|
|
1594
1639
|
participants: state.participants.map(
|
|
1595
|
-
(participant) => participant.identity === identity ? {
|
|
1640
|
+
(participant) => participant.identity === identity ? {
|
|
1641
|
+
...participant,
|
|
1642
|
+
handRaised: raised,
|
|
1643
|
+
// A hand that is ALREADY up keeps its place in the queue when
|
|
1644
|
+
// it is re-announced (to a late joiner): the first time wins.
|
|
1645
|
+
handRaisedAt: raised ? participant.handRaised && participant.handRaisedAt != null ? participant.handRaisedAt : at ?? Date.now() : null
|
|
1646
|
+
} : participant
|
|
1596
1647
|
)
|
|
1597
1648
|
});
|
|
1598
1649
|
},
|
|
@@ -1602,6 +1653,31 @@ function createMeetStore(initial = {}) {
|
|
|
1602
1653
|
const names = learnName(state.names, message.identity, message.displayName);
|
|
1603
1654
|
commit({ ...state, chat: chat.slice(-CHAT_LIMIT), names });
|
|
1604
1655
|
},
|
|
1656
|
+
loadChat(messages) {
|
|
1657
|
+
const known = new Set(state.chat.map((message) => message.id));
|
|
1658
|
+
const merged = [...messages.filter((message) => !known.has(message.id)), ...state.chat].sort(
|
|
1659
|
+
(a, b) => a.at < b.at ? -1 : a.at > b.at ? 1 : 0
|
|
1660
|
+
);
|
|
1661
|
+
const names = learnNames(
|
|
1662
|
+
state.names,
|
|
1663
|
+
messages.map((message) => ({ identity: message.identity, displayName: message.displayName }))
|
|
1664
|
+
);
|
|
1665
|
+
commit({ ...state, chat: merged.slice(-CHAT_LIMIT), chatLoaded: true, names });
|
|
1666
|
+
},
|
|
1667
|
+
settleChat(localId, next) {
|
|
1668
|
+
const index = state.chat.findIndex((message) => message.id === localId);
|
|
1669
|
+
if (index === -1) return;
|
|
1670
|
+
const current = state.chat[index];
|
|
1671
|
+
let replacement;
|
|
1672
|
+
if ("failed" in next && typeof next.failed === "string" && !("id" in next)) {
|
|
1673
|
+
replacement = { ...current, failed: next.failed };
|
|
1674
|
+
} else {
|
|
1675
|
+
replacement = next;
|
|
1676
|
+
}
|
|
1677
|
+
const duplicate = replacement.id !== localId && state.chat.some((message) => message.id === replacement.id);
|
|
1678
|
+
const chat = duplicate ? state.chat.filter((message) => message.id !== localId) : state.chat.map((message, position) => position === index ? replacement : message);
|
|
1679
|
+
commit({ ...state, chat });
|
|
1680
|
+
},
|
|
1605
1681
|
addReaction(reaction) {
|
|
1606
1682
|
commit({ ...state, reactions: [...state.reactions, reaction] });
|
|
1607
1683
|
},
|
|
@@ -1692,7 +1768,7 @@ function createMeetStore(initial = {}) {
|
|
|
1692
1768
|
commit({ ...state, media: { ...state.media, [kind]: next } });
|
|
1693
1769
|
},
|
|
1694
1770
|
reset() {
|
|
1695
|
-
commit({ ...EMPTY });
|
|
1771
|
+
commit({ ...EMPTY, effects: state.effects });
|
|
1696
1772
|
}
|
|
1697
1773
|
};
|
|
1698
1774
|
}
|
|
@@ -2022,11 +2098,11 @@ function createMeetingSession(options) {
|
|
|
2022
2098
|
} catch {
|
|
2023
2099
|
body = null;
|
|
2024
2100
|
}
|
|
2025
|
-
const
|
|
2101
|
+
const refusal2 = readServerRefusal(body);
|
|
2026
2102
|
throw new MeetError(
|
|
2027
2103
|
response.status === 403 ? "forbidden" : "transport",
|
|
2028
|
-
|
|
2029
|
-
|
|
2104
|
+
refusal2?.reason ?? `meet.endForEveryone: the server answered ${response.status}`,
|
|
2105
|
+
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."
|
|
2030
2106
|
);
|
|
2031
2107
|
}
|
|
2032
2108
|
let payload = null;
|
|
@@ -2069,7 +2145,11 @@ var TABLES = {
|
|
|
2069
2145
|
invites: "meet_call_invites",
|
|
2070
2146
|
recordings: "meet_recordings",
|
|
2071
2147
|
transcripts: "meet_transcript_segments",
|
|
2072
|
-
notes: "meet_notes"
|
|
2148
|
+
notes: "meet_notes",
|
|
2149
|
+
/** Wave 1 (0.8): who a meeting was sent to, and what they answered. */
|
|
2150
|
+
invitees: "meet_invitees",
|
|
2151
|
+
/** Wave 1 (0.8): one occurrence of a series cancelled or moved. */
|
|
2152
|
+
occurrenceOverrides: "meet_occurrence_overrides"
|
|
2073
2153
|
};
|
|
2074
2154
|
var RPCS = {
|
|
2075
2155
|
/** Atomic: create the invite row and stamp its expiry in one statement. */
|
|
@@ -2085,10 +2165,41 @@ var RPCS = {
|
|
|
2085
2165
|
admit: "meet_admit_participant",
|
|
2086
2166
|
setLock: "meet_set_lock",
|
|
2087
2167
|
endMeeting: "meet_end_meeting",
|
|
2088
|
-
recordConsent: "meet_record_consent"
|
|
2168
|
+
recordConsent: "meet_record_consent",
|
|
2169
|
+
/*
|
|
2170
|
+
* ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
|
|
2171
|
+
* authorized by the meeting's host or a real iam grant (editor to change,
|
|
2172
|
+
* admin = co-host to archive or name a co-host) — never an organization
|
|
2173
|
+
* match. After any of these writes, call `announceMeeting` so the server
|
|
2174
|
+
* sends invitations / updates / cancellations and re-queues reminders.
|
|
2175
|
+
*/
|
|
2176
|
+
/** Schedule with every setting at create; unset settings come from the knobs. */
|
|
2177
|
+
scheduleMeeting: "meet_schedule_meeting",
|
|
2178
|
+
/** Edit / reschedule — a JSON patch with a version compare-and-swap. */
|
|
2179
|
+
updateMeeting: "meet_update_meeting",
|
|
2180
|
+
/** Cancel is a state (`cancelled_at`); the link keeps resolving. */
|
|
2181
|
+
cancelMeeting: "meet_cancel_meeting",
|
|
2182
|
+
/** Archive (soft delete) — the invitations and exceptions cascade. */
|
|
2183
|
+
archiveMeeting: "meet_archive_meeting",
|
|
2184
|
+
restoreMeeting: "meet_restore_meeting",
|
|
2185
|
+
/** Cancel, move or restore ONE occurrence of a series by its original start. */
|
|
2186
|
+
setOccurrence: "meet_set_occurrence",
|
|
2187
|
+
addInvitees: "meet_add_invitees",
|
|
2188
|
+
setInviteeRole: "meet_set_invitee_role",
|
|
2189
|
+
removeInvitee: "meet_remove_invitee",
|
|
2190
|
+
/** A signed-in invitee answers for themselves. */
|
|
2191
|
+
respond: "meet_respond",
|
|
2192
|
+
/** The emailed RSVP link (no account): read and answer. */
|
|
2193
|
+
invitationByToken: "meet_invitation_by_token",
|
|
2194
|
+
respondByToken: "meet_respond_by_token",
|
|
2195
|
+
/** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */
|
|
2196
|
+
meetingOccurrences: "meet_meeting_occurrences",
|
|
2197
|
+
/** Every occurrence the reader can see between two instants — the Upcoming list. */
|
|
2198
|
+
occurrencesBetween: "meet_occurrences_between"
|
|
2089
2199
|
};
|
|
2090
2200
|
var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
|
|
2091
|
-
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";
|
|
2201
|
+
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";
|
|
2202
|
+
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";
|
|
2092
2203
|
var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
|
|
2093
2204
|
var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
|
|
2094
2205
|
var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
|
|
@@ -2186,6 +2297,138 @@ function projectNote(row) {
|
|
|
2186
2297
|
source: "record"
|
|
2187
2298
|
};
|
|
2188
2299
|
}
|
|
2300
|
+
var RSVP_STATES = ["needs_action", "accepted", "declined", "tentative"];
|
|
2301
|
+
var OCCURRENCE_STATES = ["scheduled", "moved", "cancelled"];
|
|
2302
|
+
function rsvpState(value) {
|
|
2303
|
+
return RSVP_STATES.includes(value) ? value : "needs_action";
|
|
2304
|
+
}
|
|
2305
|
+
function num(row, key) {
|
|
2306
|
+
const value = row[key];
|
|
2307
|
+
return typeof value === "number" ? value : null;
|
|
2308
|
+
}
|
|
2309
|
+
function when(row, key) {
|
|
2310
|
+
const value = row[key];
|
|
2311
|
+
if (value instanceof Date) return value.toISOString();
|
|
2312
|
+
return text(row, key);
|
|
2313
|
+
}
|
|
2314
|
+
function projectInvitee(row) {
|
|
2315
|
+
const operation = "meet.projectInvitee";
|
|
2316
|
+
const userId = text(row, "invitee_user_id");
|
|
2317
|
+
return {
|
|
2318
|
+
id: requireText(row, "id", operation),
|
|
2319
|
+
meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
|
|
2320
|
+
userId: userId === null ? null : asUserId(userId),
|
|
2321
|
+
email: text(row, "email"),
|
|
2322
|
+
displayName: text(row, "display_name"),
|
|
2323
|
+
role: text(row, "role") === "cohost" ? "cohost" : "invitee",
|
|
2324
|
+
rsvpState: rsvpState(text(row, "rsvp_state")),
|
|
2325
|
+
respondedAt: when(row, "responded_at"),
|
|
2326
|
+
rsvpNote: text(row, "rsvp_note"),
|
|
2327
|
+
lastSentSequence: num(row, "last_sent_sequence"),
|
|
2328
|
+
lastSentAt: when(row, "last_sent_at")
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
function projectOccurrence(row) {
|
|
2332
|
+
const operation = "meet.projectOccurrence";
|
|
2333
|
+
const state = text(row, "state");
|
|
2334
|
+
return {
|
|
2335
|
+
meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
|
|
2336
|
+
originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
|
|
2337
|
+
occurrenceStart: when(row, "occurrence_start") ?? requireText(row, "occurrence_start", operation),
|
|
2338
|
+
durationMinutes: num(row, "duration_minutes"),
|
|
2339
|
+
// An unknown state is shown as scheduled, never dropped: the row is real.
|
|
2340
|
+
state: OCCURRENCE_STATES.includes(state) ? state : "scheduled",
|
|
2341
|
+
sequence: num(row, "sequence") ?? 0
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
function projectUpcoming(row) {
|
|
2345
|
+
const operation = "meet.projectUpcoming";
|
|
2346
|
+
const kind = text(row, "kind");
|
|
2347
|
+
const myRole = text(row, "my_role");
|
|
2348
|
+
const myRsvp = text(row, "my_rsvp");
|
|
2349
|
+
return {
|
|
2350
|
+
...projectOccurrence(row),
|
|
2351
|
+
organizationId: asOrganizationId(requireText(row, "organization_id", operation)),
|
|
2352
|
+
title: text(row, "title") ?? "Meeting",
|
|
2353
|
+
slug: requireText(row, "slug", operation),
|
|
2354
|
+
kind: kind === "scheduled" || kind === "recurring" ? kind : "instant",
|
|
2355
|
+
hostUserId: asUserId(requireText(row, "host_user_id", operation)),
|
|
2356
|
+
timeZone: text(row, "time_zone") ?? "UTC",
|
|
2357
|
+
meetingCancelled: bool(row, "meeting_cancelled", false),
|
|
2358
|
+
myRsvp: myRsvp === null ? null : rsvpState(myRsvp),
|
|
2359
|
+
myRole: myRole === "host" || myRole === "cohost" || myRole === "invitee" ? myRole : null
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
function projectOverride(row) {
|
|
2363
|
+
const operation = "meet.projectOverride";
|
|
2364
|
+
return {
|
|
2365
|
+
id: requireText(row, "id", operation),
|
|
2366
|
+
meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
|
|
2367
|
+
originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
|
|
2368
|
+
state: text(row, "state") === "moved" ? "moved" : "cancelled",
|
|
2369
|
+
newStart: when(row, "new_start"),
|
|
2370
|
+
newDurationMinutes: num(row, "new_duration_minutes"),
|
|
2371
|
+
reason: text(row, "reason"),
|
|
2372
|
+
calendarSequence: num(row, "calendar_sequence") ?? 0,
|
|
2373
|
+
deletedAt: when(row, "deleted_at")
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
function projectRsvpInvitation(body) {
|
|
2377
|
+
const operation = "meet.projectRsvpInvitation";
|
|
2378
|
+
if (typeof body !== "object" || body === null) {
|
|
2379
|
+
throw invalidResponse(operation, "the RSVP door returned no object");
|
|
2380
|
+
}
|
|
2381
|
+
const row = body;
|
|
2382
|
+
if (row["ok"] !== true) {
|
|
2383
|
+
return {
|
|
2384
|
+
ok: false,
|
|
2385
|
+
message: text(row, "message") ?? "This invitation link is no longer valid \u2014 ask the host to send it again."
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
const meeting = row["meeting"] ?? {};
|
|
2389
|
+
const invitee = row["invitee"] ?? {};
|
|
2390
|
+
return {
|
|
2391
|
+
ok: true,
|
|
2392
|
+
meeting: {
|
|
2393
|
+
title: text(meeting, "title") ?? "Meeting",
|
|
2394
|
+
slug: requireText(meeting, "slug", operation),
|
|
2395
|
+
scheduledFor: when(meeting, "scheduled_for"),
|
|
2396
|
+
durationMinutes: num(meeting, "duration_minutes"),
|
|
2397
|
+
timeZone: text(meeting, "time_zone") ?? "UTC",
|
|
2398
|
+
agenda: text(meeting, "agenda"),
|
|
2399
|
+
recurrenceRule: text(meeting, "recurrence_rule"),
|
|
2400
|
+
cancelledAt: when(meeting, "cancelled_at"),
|
|
2401
|
+
cancellationReason: text(meeting, "cancellation_reason"),
|
|
2402
|
+
endedAt: when(meeting, "ended_at")
|
|
2403
|
+
},
|
|
2404
|
+
invitee: {
|
|
2405
|
+
displayName: text(invitee, "display_name"),
|
|
2406
|
+
email: text(invitee, "email"),
|
|
2407
|
+
rsvpState: rsvpState(text(invitee, "rsvp_state")),
|
|
2408
|
+
respondedAt: when(invitee, "responded_at")
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
function settingsPatch(settings) {
|
|
2413
|
+
const patch = {};
|
|
2414
|
+
if (settings.lobbyEnabled !== void 0) patch["lobby_enabled"] = settings.lobbyEnabled;
|
|
2415
|
+
if (settings.recordingPolicy !== void 0) patch["recording_policy"] = settings.recordingPolicy;
|
|
2416
|
+
if (settings.aiEnabled !== void 0) patch["ai_enabled"] = settings.aiEnabled;
|
|
2417
|
+
if (settings.joinBeforeHost !== void 0) patch["join_before_host"] = settings.joinBeforeHost;
|
|
2418
|
+
return patch;
|
|
2419
|
+
}
|
|
2420
|
+
function changesPatch(changes) {
|
|
2421
|
+
const patch = settingsPatch(changes);
|
|
2422
|
+
if (changes.title !== void 0) patch["title"] = changes.title;
|
|
2423
|
+
if (changes.agenda !== void 0) patch["agenda"] = changes.agenda;
|
|
2424
|
+
if (changes.timeZone !== void 0) patch["time_zone"] = changes.timeZone;
|
|
2425
|
+
if (changes.scheduledFor !== void 0) patch["scheduled_for"] = changes.scheduledFor;
|
|
2426
|
+
if (changes.scheduledDurationMinutes !== void 0) {
|
|
2427
|
+
patch["scheduled_duration_minutes"] = changes.scheduledDurationMinutes;
|
|
2428
|
+
}
|
|
2429
|
+
if (changes.recurrenceRule !== void 0) patch["recurrence_rule"] = changes.recurrenceRule;
|
|
2430
|
+
return patch;
|
|
2431
|
+
}
|
|
2189
2432
|
function createMeetRepository(options) {
|
|
2190
2433
|
const db = options.client.schema(MEET_SCHEMA);
|
|
2191
2434
|
async function guardVoid(operation, run) {
|
|
@@ -2255,7 +2498,16 @@ function createMeetRepository(options) {
|
|
|
2255
2498
|
lobbyEnabled: bool(row, "lobby_enabled", true),
|
|
2256
2499
|
recordingPolicy: policy === "disabled" || policy === "always-on" ? policy : "host-controlled",
|
|
2257
2500
|
aiEnabled: bool(row, "ai_enabled", true),
|
|
2258
|
-
metadata: typeof metadata === "object" && metadata !== null && !Array.isArray(metadata) ? metadata : {}
|
|
2501
|
+
metadata: typeof metadata === "object" && metadata !== null && !Array.isArray(metadata) ? metadata : {},
|
|
2502
|
+
timeZone: text(row, "time_zone") ?? "UTC",
|
|
2503
|
+
agenda: text(row, "agenda"),
|
|
2504
|
+
recurrenceRule: text(row, "recurrence_rule"),
|
|
2505
|
+
joinBeforeHost: bool(row, "join_before_host", true),
|
|
2506
|
+
cancelledAt: text(row, "cancelled_at"),
|
|
2507
|
+
cancellationReason: text(row, "cancellation_reason"),
|
|
2508
|
+
deletedAt: text(row, "deleted_at"),
|
|
2509
|
+
calendarSequence: typeof row["calendar_sequence"] === "number" ? row["calendar_sequence"] : 0,
|
|
2510
|
+
version: typeof row["version"] === "number" ? row["version"] : 1
|
|
2259
2511
|
};
|
|
2260
2512
|
}
|
|
2261
2513
|
function projectRecording(row) {
|
|
@@ -2342,6 +2594,170 @@ function createMeetRepository(options) {
|
|
|
2342
2594
|
projectNote,
|
|
2343
2595
|
projectRecording,
|
|
2344
2596
|
projectAttendee,
|
|
2597
|
+
projectInvitee,
|
|
2598
|
+
async scheduleMeeting(input) {
|
|
2599
|
+
requireOrg(input.organizationId, "meet.scheduleMeeting");
|
|
2600
|
+
const row = await guard(
|
|
2601
|
+
"meet.scheduleMeeting",
|
|
2602
|
+
() => db.rpc(RPCS.scheduleMeeting, {
|
|
2603
|
+
p_organization_id: input.organizationId,
|
|
2604
|
+
p_host_user_id: input.hostUserId,
|
|
2605
|
+
p_title: input.title,
|
|
2606
|
+
p_scheduled_for: input.scheduledFor,
|
|
2607
|
+
p_time_zone: input.timeZone,
|
|
2608
|
+
p_duration_minutes: input.durationMinutes ?? null,
|
|
2609
|
+
p_agenda: input.agenda ?? null,
|
|
2610
|
+
p_recurrence_rule: input.recurrenceRule ?? null,
|
|
2611
|
+
p_settings: settingsPatch(input.settings ?? {})
|
|
2612
|
+
})
|
|
2613
|
+
);
|
|
2614
|
+
return projectMeeting(row);
|
|
2615
|
+
},
|
|
2616
|
+
async updateMeeting(meetingId, changes, options2 = {}) {
|
|
2617
|
+
const row = await guard(
|
|
2618
|
+
"meet.updateMeeting",
|
|
2619
|
+
() => db.rpc(RPCS.updateMeeting, {
|
|
2620
|
+
p_meeting_id: meetingId,
|
|
2621
|
+
p_changes: changesPatch(changes),
|
|
2622
|
+
p_expected_version: options2.expectedVersion ?? null,
|
|
2623
|
+
p_by_user_id: options2.byUserId ?? null
|
|
2624
|
+
})
|
|
2625
|
+
);
|
|
2626
|
+
return projectMeeting(row);
|
|
2627
|
+
},
|
|
2628
|
+
async cancelMeeting(meetingId, reason, byUserId = null) {
|
|
2629
|
+
const row = await guard(
|
|
2630
|
+
"meet.cancelMeeting",
|
|
2631
|
+
() => db.rpc(RPCS.cancelMeeting, {
|
|
2632
|
+
p_meeting_id: meetingId,
|
|
2633
|
+
p_reason: reason,
|
|
2634
|
+
p_by_user_id: byUserId
|
|
2635
|
+
})
|
|
2636
|
+
);
|
|
2637
|
+
return projectMeeting(row);
|
|
2638
|
+
},
|
|
2639
|
+
async archiveMeeting(meetingId, byUserId = null) {
|
|
2640
|
+
const row = await guard(
|
|
2641
|
+
"meet.archiveMeeting",
|
|
2642
|
+
() => db.rpc(RPCS.archiveMeeting, { p_meeting_id: meetingId, p_by_user_id: byUserId })
|
|
2643
|
+
);
|
|
2644
|
+
return projectMeeting(row);
|
|
2645
|
+
},
|
|
2646
|
+
async restoreMeeting(meetingId, byUserId = null) {
|
|
2647
|
+
const row = await guard(
|
|
2648
|
+
"meet.restoreMeeting",
|
|
2649
|
+
() => db.rpc(RPCS.restoreMeeting, { p_meeting_id: meetingId, p_by_user_id: byUserId })
|
|
2650
|
+
);
|
|
2651
|
+
return projectMeeting(row);
|
|
2652
|
+
},
|
|
2653
|
+
async setOccurrence(args) {
|
|
2654
|
+
const row = await guard(
|
|
2655
|
+
"meet.setOccurrence",
|
|
2656
|
+
() => db.rpc(RPCS.setOccurrence, {
|
|
2657
|
+
p_meeting_id: args.meetingId,
|
|
2658
|
+
p_original_start: args.originalStart,
|
|
2659
|
+
p_action: args.action,
|
|
2660
|
+
p_new_start: args.newStart ?? null,
|
|
2661
|
+
p_new_duration_minutes: args.newDurationMinutes ?? null,
|
|
2662
|
+
p_reason: args.reason ?? null,
|
|
2663
|
+
p_by_user_id: args.byUserId ?? null
|
|
2664
|
+
})
|
|
2665
|
+
);
|
|
2666
|
+
return projectOverride(row);
|
|
2667
|
+
},
|
|
2668
|
+
async meetingOccurrences(meetingId, { from, to, limit = 200 }) {
|
|
2669
|
+
const rows = await guard(
|
|
2670
|
+
"meet.meetingOccurrences",
|
|
2671
|
+
() => db.rpc(RPCS.meetingOccurrences, {
|
|
2672
|
+
p_meeting_id: meetingId,
|
|
2673
|
+
p_from: from,
|
|
2674
|
+
p_to: to,
|
|
2675
|
+
p_limit: limit
|
|
2676
|
+
})
|
|
2677
|
+
);
|
|
2678
|
+
return rows.map(projectOccurrence);
|
|
2679
|
+
},
|
|
2680
|
+
async occurrencesBetween({ from, to, organizationId = null, limit = 500 }) {
|
|
2681
|
+
const rows = await guard(
|
|
2682
|
+
"meet.occurrencesBetween",
|
|
2683
|
+
() => db.rpc(RPCS.occurrencesBetween, {
|
|
2684
|
+
p_from: from,
|
|
2685
|
+
p_to: to,
|
|
2686
|
+
p_organization_id: organizationId,
|
|
2687
|
+
p_limit: limit
|
|
2688
|
+
})
|
|
2689
|
+
);
|
|
2690
|
+
return rows.map(projectUpcoming);
|
|
2691
|
+
},
|
|
2692
|
+
async invitees(meetingId) {
|
|
2693
|
+
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 });
|
|
2694
|
+
if (response.error !== null) throw normalizeMeetError(response.error, "meet.invitees");
|
|
2695
|
+
return (response.data ?? []).map(projectInvitee);
|
|
2696
|
+
},
|
|
2697
|
+
async addInvitees(meetingId, people, byUserId = null) {
|
|
2698
|
+
const rows = await guard(
|
|
2699
|
+
"meet.addInvitees",
|
|
2700
|
+
() => db.rpc(RPCS.addInvitees, {
|
|
2701
|
+
p_meeting_id: meetingId,
|
|
2702
|
+
p_invitees: people.map((person) => ({
|
|
2703
|
+
...person.userId !== void 0 ? { user_id: person.userId } : {},
|
|
2704
|
+
...person.email !== void 0 ? { email: person.email } : {},
|
|
2705
|
+
...person.displayName !== void 0 ? { display_name: person.displayName } : {},
|
|
2706
|
+
...person.role !== void 0 ? { role: person.role } : {}
|
|
2707
|
+
})),
|
|
2708
|
+
p_by_user_id: byUserId
|
|
2709
|
+
})
|
|
2710
|
+
);
|
|
2711
|
+
return rows.map(projectInvitee);
|
|
2712
|
+
},
|
|
2713
|
+
async setInviteeRole(inviteeId, role, byUserId = null) {
|
|
2714
|
+
const row = await guard(
|
|
2715
|
+
"meet.setInviteeRole",
|
|
2716
|
+
() => db.rpc(RPCS.setInviteeRole, {
|
|
2717
|
+
p_invitee_id: inviteeId,
|
|
2718
|
+
p_role: role,
|
|
2719
|
+
p_by_user_id: byUserId
|
|
2720
|
+
})
|
|
2721
|
+
);
|
|
2722
|
+
return projectInvitee(row);
|
|
2723
|
+
},
|
|
2724
|
+
async removeInvitee(inviteeId, byUserId = null) {
|
|
2725
|
+
const row = await guard(
|
|
2726
|
+
"meet.removeInvitee",
|
|
2727
|
+
() => db.rpc(RPCS.removeInvitee, { p_invitee_id: inviteeId, p_by_user_id: byUserId })
|
|
2728
|
+
);
|
|
2729
|
+
return projectInvitee(row);
|
|
2730
|
+
},
|
|
2731
|
+
async respond(meetingId, answer, note = null) {
|
|
2732
|
+
const row = await guard(
|
|
2733
|
+
"meet.respond",
|
|
2734
|
+
() => db.rpc(RPCS.respond, {
|
|
2735
|
+
p_meeting_id: meetingId,
|
|
2736
|
+
p_response: answer,
|
|
2737
|
+
p_note: note,
|
|
2738
|
+
p_by_user_id: null
|
|
2739
|
+
})
|
|
2740
|
+
);
|
|
2741
|
+
return projectInvitee(row);
|
|
2742
|
+
},
|
|
2743
|
+
async invitationByToken(secret) {
|
|
2744
|
+
const body = await guard(
|
|
2745
|
+
"meet.invitationByToken",
|
|
2746
|
+
() => db.rpc(RPCS.invitationByToken, { p_secret: secret })
|
|
2747
|
+
);
|
|
2748
|
+
return projectRsvpInvitation(body);
|
|
2749
|
+
},
|
|
2750
|
+
async respondByToken(secret, answer, note = null) {
|
|
2751
|
+
const body = await guard(
|
|
2752
|
+
"meet.respondByToken",
|
|
2753
|
+
() => db.rpc(RPCS.respondByToken, {
|
|
2754
|
+
p_secret: secret,
|
|
2755
|
+
p_response: answer,
|
|
2756
|
+
p_note: note
|
|
2757
|
+
})
|
|
2758
|
+
);
|
|
2759
|
+
return projectRsvpInvitation(body);
|
|
2760
|
+
},
|
|
2345
2761
|
async attendees(meetingId) {
|
|
2346
2762
|
const response = await db.from(TABLES.participants).select(ATTENDEE_COLUMNS).eq("meeting_id", meetingId).order("joined_at", { ascending: true }).order("id", { ascending: true });
|
|
2347
2763
|
if (response.error !== null) throw normalizeMeetError(response.error, "meet.attendees");
|
|
@@ -2607,7 +3023,19 @@ var ROOM_EVENTS = {
|
|
|
2607
3023
|
* production 2026-09-09 the guest lane showed no indicator at all while the
|
|
2608
3024
|
* host recorded (MRI-C8).
|
|
2609
3025
|
*/
|
|
2610
|
-
recording: "matrx.meet.recording"
|
|
3026
|
+
recording: "matrx.meet.recording",
|
|
3027
|
+
/**
|
|
3028
|
+
* A chat message to everyone was SAVED (Meet wave 2). Payload
|
|
3029
|
+
* `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
|
|
3030
|
+
*/
|
|
3031
|
+
chat: "matrx.meet.chat",
|
|
3032
|
+
/**
|
|
3033
|
+
* A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`:
|
|
3034
|
+
* `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the
|
|
3035
|
+
* person concerned; `lower-hands {identities: [...] | "*"}` · `role {identity,
|
|
3036
|
+
* role}` — to everyone.
|
|
3037
|
+
*/
|
|
3038
|
+
moderation: "matrx.meet.moderation"
|
|
2611
3039
|
};
|
|
2612
3040
|
function isRoomEventTopic(topic) {
|
|
2613
3041
|
return topic.startsWith(ROOM_EVENT_PREFIX);
|
|
@@ -2643,6 +3071,10 @@ function applyRoomEvent(args) {
|
|
|
2643
3071
|
return applyEnded(store, payload);
|
|
2644
3072
|
case ROOM_EVENTS.recording:
|
|
2645
3073
|
return applyRecording(store, payload);
|
|
3074
|
+
case ROOM_EVENTS.chat:
|
|
3075
|
+
return applyChat(store, payload);
|
|
3076
|
+
case ROOM_EVENTS.moderation:
|
|
3077
|
+
return applyModeration(store, payload, now);
|
|
2646
3078
|
default:
|
|
2647
3079
|
return ignored(`${topic} is not known to this version of @ai-matrx/meet`);
|
|
2648
3080
|
}
|
|
@@ -2734,6 +3166,101 @@ function applyEnded(store, payload) {
|
|
|
2734
3166
|
store.patch({ meeting: { ...meeting, endedAt } });
|
|
2735
3167
|
return HANDLED;
|
|
2736
3168
|
}
|
|
3169
|
+
function chatFromRow(row) {
|
|
3170
|
+
const id = text2(row["id"]);
|
|
3171
|
+
const identity = text2(row["sender_identity"]);
|
|
3172
|
+
const body = text2(row["body"]);
|
|
3173
|
+
if (id === null || identity === null || body === null) return null;
|
|
3174
|
+
return {
|
|
3175
|
+
id,
|
|
3176
|
+
identity: asParticipantIdentity(identity),
|
|
3177
|
+
displayName: text2(row["sender_name"]) ?? "Participant",
|
|
3178
|
+
text: body,
|
|
3179
|
+
at: text2(row["created_at"]) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3180
|
+
// `false` only from a server whose chat table has not landed: the room heard
|
|
3181
|
+
// it, nobody will see it later — and the line says so.
|
|
3182
|
+
saved: row["saved"] !== false
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
function applyChat(store, payload) {
|
|
3186
|
+
const row = object(payload["message"]);
|
|
3187
|
+
if (row === null) return ignored("a chat event with no `message` row");
|
|
3188
|
+
const meeting = store.snapshot().meeting;
|
|
3189
|
+
if (meeting !== null && text2(row["meeting_id"]) !== null && row["meeting_id"] !== meeting.id) {
|
|
3190
|
+
return ignored("a chat message for a different meeting");
|
|
3191
|
+
}
|
|
3192
|
+
const message = chatFromRow(row);
|
|
3193
|
+
if (message === null) return ignored("a chat row with no id, sender or body");
|
|
3194
|
+
store.addChat(message);
|
|
3195
|
+
return HANDLED;
|
|
3196
|
+
}
|
|
3197
|
+
function moderationText(kind, payload) {
|
|
3198
|
+
const by = text2(payload["by"]) ?? "The host";
|
|
3199
|
+
switch (kind) {
|
|
3200
|
+
case "muted": {
|
|
3201
|
+
const source = text2(payload["source"]) ?? "microphone";
|
|
3202
|
+
return source === "microphone" ? `${by} muted you.` : source === "camera" ? `${by} turned off your camera.` : `${by} stopped your screen share.`;
|
|
3203
|
+
}
|
|
3204
|
+
case "ask-unmute":
|
|
3205
|
+
return `${by} is asking you to unmute.`;
|
|
3206
|
+
case "removed":
|
|
3207
|
+
return `${by} removed you from this meeting.`;
|
|
3208
|
+
default:
|
|
3209
|
+
return null;
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
function applyModeration(store, payload, now) {
|
|
3213
|
+
const snapshot = store.snapshot();
|
|
3214
|
+
const meetingId = text2(payload["meeting_id"]);
|
|
3215
|
+
if (snapshot.meeting !== null && meetingId !== null && meetingId !== snapshot.meeting.id) {
|
|
3216
|
+
return ignored("a moderation event for a different meeting");
|
|
3217
|
+
}
|
|
3218
|
+
const kind = text2(payload["kind"]);
|
|
3219
|
+
switch (kind) {
|
|
3220
|
+
case "lower-hands": {
|
|
3221
|
+
const raw = payload["identities"];
|
|
3222
|
+
const everyone = raw === "*";
|
|
3223
|
+
const named = new Set(
|
|
3224
|
+
Array.isArray(raw) ? raw.filter((value) => typeof value === "string") : []
|
|
3225
|
+
);
|
|
3226
|
+
for (const participant of snapshot.participants) {
|
|
3227
|
+
if (participant.handRaised && (everyone || named.has(participant.identity))) {
|
|
3228
|
+
store.setHand(participant.identity, false);
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
return HANDLED;
|
|
3232
|
+
}
|
|
3233
|
+
case "role": {
|
|
3234
|
+
const identity = text2(payload["identity"]);
|
|
3235
|
+
const role = text2(payload["role"]);
|
|
3236
|
+
if (identity === null || role === null || !ROLES.includes(role)) {
|
|
3237
|
+
return ignored("a role event with no identity or an unknown role");
|
|
3238
|
+
}
|
|
3239
|
+
if (identity === snapshot.localIdentity) {
|
|
3240
|
+
store.patch({
|
|
3241
|
+
grants: { ...snapshot.grants, role },
|
|
3242
|
+
moderationNotice: {
|
|
3243
|
+
kind: "role",
|
|
3244
|
+
text: role === "host" ? "You are now the host." : role === "cohost" ? "You are now a co-host." : "You are no longer a co-host.",
|
|
3245
|
+
at: now()
|
|
3246
|
+
}
|
|
3247
|
+
});
|
|
3248
|
+
}
|
|
3249
|
+
return HANDLED;
|
|
3250
|
+
}
|
|
3251
|
+
case "muted":
|
|
3252
|
+
case "ask-unmute":
|
|
3253
|
+
case "removed": {
|
|
3254
|
+
const sentence = moderationText(kind, payload);
|
|
3255
|
+
if (sentence === null) return ignored("a moderation event with no sentence");
|
|
3256
|
+
store.patch({ moderationNotice: { kind, text: sentence, at: now() } });
|
|
3257
|
+
return HANDLED;
|
|
3258
|
+
}
|
|
3259
|
+
default:
|
|
3260
|
+
return ignored(`a moderation kind this version does not know: ${String(kind)}`);
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
var ROLES = ["host", "cohost", "participant", "guest"];
|
|
2737
3264
|
|
|
2738
3265
|
// src/core/note-taker.ts
|
|
2739
3266
|
var NOTE_TAKER_IDENTITY_PREFIX = "agent:note_taker:";
|
|
@@ -2769,14 +3296,14 @@ function noteTakerStatus(args) {
|
|
|
2769
3296
|
const row = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
|
|
2770
3297
|
const recorded = readState(row["state"]);
|
|
2771
3298
|
const detail = typeof row["reason"] === "string" && row["reason"].length > 0 ? row["reason"] : null;
|
|
2772
|
-
const
|
|
3299
|
+
const refusal2 = args.refusal ?? null;
|
|
2773
3300
|
if (recorded === null) {
|
|
2774
|
-
return { state: "absent", detail, present, refusal };
|
|
3301
|
+
return { state: "absent", detail, present, refusal: refusal2 };
|
|
2775
3302
|
}
|
|
2776
3303
|
if (recorded === "transcribing" && !present) {
|
|
2777
|
-
return { state: "handoff_pending", detail, present, refusal };
|
|
3304
|
+
return { state: "handoff_pending", detail, present, refusal: refusal2 };
|
|
2778
3305
|
}
|
|
2779
|
-
return { state: recorded, detail, present, refusal };
|
|
3306
|
+
return { state: recorded, detail, present, refusal: refusal2 };
|
|
2780
3307
|
}
|
|
2781
3308
|
function noteTakerLabel(status) {
|
|
2782
3309
|
switch (status.state) {
|
|
@@ -2798,8 +3325,7 @@ function noteTakerLabel(status) {
|
|
|
2798
3325
|
var DATA_TOPICS = {
|
|
2799
3326
|
hand: "mx.hand",
|
|
2800
3327
|
reaction: "mx.reaction",
|
|
2801
|
-
chat: "mx.chat"
|
|
2802
|
-
hostAction: "mx.host"
|
|
3328
|
+
chat: "mx.chat"
|
|
2803
3329
|
};
|
|
2804
3330
|
var TRANSCRIPTION_TOPIC = "lk.transcription";
|
|
2805
3331
|
var TRANSCRIPTION_ATTRIBUTES = {
|
|
@@ -2935,7 +3461,37 @@ function createRoomEngine(options) {
|
|
|
2935
3461
|
const handlers = {
|
|
2936
3462
|
participants(list) {
|
|
2937
3463
|
const joinedAt = new Date(now()).toISOString();
|
|
3464
|
+
const before = new Set(store.snapshot().participants.map((p) => p.identity));
|
|
2938
3465
|
store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
|
|
3466
|
+
const snapshot = store.snapshot();
|
|
3467
|
+
const newcomer = snapshot.participants.some(
|
|
3468
|
+
(participant) => !participant.isLocal && !before.has(participant.identity)
|
|
3469
|
+
);
|
|
3470
|
+
const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
|
|
3471
|
+
if (newcomer && before.size > 0 && me?.handRaised === true) {
|
|
3472
|
+
void publish(DATA_TOPICS.hand, { raised: true, raisedAt: me.handRaisedAt ?? now() }).catch(
|
|
3473
|
+
() => void 0
|
|
3474
|
+
);
|
|
3475
|
+
}
|
|
3476
|
+
},
|
|
3477
|
+
roomMetadata(metadata) {
|
|
3478
|
+
let spotlight = null;
|
|
3479
|
+
if (metadata.length > 0) {
|
|
3480
|
+
try {
|
|
3481
|
+
const parsed = JSON.parse(metadata);
|
|
3482
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
3483
|
+
const value = parsed.spotlight;
|
|
3484
|
+
if (typeof value === "string" && value.length > 0) spotlight = asParticipantIdentity(value);
|
|
3485
|
+
}
|
|
3486
|
+
} catch {
|
|
3487
|
+
report(
|
|
3488
|
+
"warn",
|
|
3489
|
+
"meet: the room's metadata is not JSON, so no spotlight was read from it",
|
|
3490
|
+
"Only aidream writes room metadata; check services/meet/moderation.py."
|
|
3491
|
+
);
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
|
|
2939
3495
|
},
|
|
2940
3496
|
activeSpeakers(identities) {
|
|
2941
3497
|
store.setSpeaking(identities.map(asParticipantIdentity));
|
|
@@ -3039,7 +3595,11 @@ function createRoomEngine(options) {
|
|
|
3039
3595
|
const sender = asParticipantIdentity(from ?? "");
|
|
3040
3596
|
switch (topic) {
|
|
3041
3597
|
case DATA_TOPICS.hand:
|
|
3042
|
-
store.setHand(
|
|
3598
|
+
store.setHand(
|
|
3599
|
+
sender,
|
|
3600
|
+
envelope.raised === true,
|
|
3601
|
+
typeof envelope.raisedAt === "number" ? envelope.raisedAt : now()
|
|
3602
|
+
);
|
|
3043
3603
|
return;
|
|
3044
3604
|
case DATA_TOPICS.reaction:
|
|
3045
3605
|
if (typeof envelope.emoji === "string") {
|
|
@@ -3053,12 +3613,14 @@ function createRoomEngine(options) {
|
|
|
3053
3613
|
return;
|
|
3054
3614
|
case DATA_TOPICS.chat:
|
|
3055
3615
|
if (typeof envelope.text === "string") {
|
|
3616
|
+
const local = store.snapshot().localIdentity;
|
|
3056
3617
|
store.addChat({
|
|
3057
3618
|
id: envelope.id ?? `${sender}-${now()}`,
|
|
3058
3619
|
identity: sender,
|
|
3059
3620
|
displayName: envelope.name ?? sender,
|
|
3060
3621
|
text: envelope.text,
|
|
3061
|
-
at: envelope.at ?? new Date(now()).toISOString()
|
|
3622
|
+
at: envelope.at ?? new Date(now()).toISOString(),
|
|
3623
|
+
...typeof envelope.to === "string" && local !== null ? { to: local } : {}
|
|
3062
3624
|
});
|
|
3063
3625
|
}
|
|
3064
3626
|
return;
|
|
@@ -3104,6 +3666,24 @@ function createRoomEngine(options) {
|
|
|
3104
3666
|
store.patch({ phase: "connected", failure: null });
|
|
3105
3667
|
store.setLocalTrack("microphone", IDLE_TRACK);
|
|
3106
3668
|
store.setLocalTrack("camera", IDLE_TRACK);
|
|
3669
|
+
const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
|
|
3670
|
+
store.patch({ effects });
|
|
3671
|
+
if (!effects.noiseSuppression || effects.background !== "none") {
|
|
3672
|
+
try {
|
|
3673
|
+
await applyEffects(created, effects);
|
|
3674
|
+
} catch {
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
for (const kind of ["audioinput", "videoinput", "audiooutput"]) {
|
|
3678
|
+
const chosen = options.resolveDevice?.(kind) ?? null;
|
|
3679
|
+
if (chosen === null || chosen === "default") continue;
|
|
3680
|
+
try {
|
|
3681
|
+
await created.switchDevice(kind, chosen);
|
|
3682
|
+
} catch (error) {
|
|
3683
|
+
const normalized = normalizeDeviceError(error, `meet.switchDevice(${kind})`);
|
|
3684
|
+
report("warn", normalized.message, normalized.remedy);
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3107
3687
|
if (request.mode === "video") {
|
|
3108
3688
|
await enableTrack(created, "camera", true);
|
|
3109
3689
|
}
|
|
@@ -3126,9 +3706,30 @@ function createRoomEngine(options) {
|
|
|
3126
3706
|
return state;
|
|
3127
3707
|
}
|
|
3128
3708
|
}
|
|
3129
|
-
async function publish(topic, body) {
|
|
3709
|
+
async function publish(topic, body, to) {
|
|
3130
3710
|
if (driver === null) return;
|
|
3131
|
-
await driver.publishData(topic, JSON.stringify(body));
|
|
3711
|
+
await driver.publishData(topic, JSON.stringify(body), to);
|
|
3712
|
+
}
|
|
3713
|
+
async function applyEffects(target, effects) {
|
|
3714
|
+
try {
|
|
3715
|
+
await target.setAudioProcessing?.({
|
|
3716
|
+
noiseSuppression: effects.noiseSuppression,
|
|
3717
|
+
echoCancellation: true,
|
|
3718
|
+
autoGainControl: true
|
|
3719
|
+
});
|
|
3720
|
+
} catch (error) {
|
|
3721
|
+
report("warn", `meet: noise suppression could not be applied: ${String(error)}`, "The microphone keeps working without it.");
|
|
3722
|
+
}
|
|
3723
|
+
if (target.setVideoEffect === void 0) return;
|
|
3724
|
+
const effect = effects.background === "blur" ? { kind: "blur" } : effects.background === "image" && effects.backgroundImage !== null ? { kind: "image", imageUrl: effects.backgroundImage } : { kind: "none" };
|
|
3725
|
+
if (effect.kind !== "none" && target.supportsVideoEffects?.() === false) return;
|
|
3726
|
+
try {
|
|
3727
|
+
await target.setVideoEffect(effect);
|
|
3728
|
+
} catch (error) {
|
|
3729
|
+
const normalized = normalizeMeetError(error, "meet.setVideoEffect");
|
|
3730
|
+
report("warn", normalized.message, normalized.remedy);
|
|
3731
|
+
throw normalized;
|
|
3732
|
+
}
|
|
3132
3733
|
}
|
|
3133
3734
|
return {
|
|
3134
3735
|
join(request) {
|
|
@@ -3184,7 +3785,7 @@ function createRoomEngine(options) {
|
|
|
3184
3785
|
async raiseHand(raised) {
|
|
3185
3786
|
const local = store.snapshot().localIdentity;
|
|
3186
3787
|
if (local !== null) store.setHand(local, raised);
|
|
3187
|
-
await publish(DATA_TOPICS.hand, { raised });
|
|
3788
|
+
await publish(DATA_TOPICS.hand, { raised, raisedAt: now() });
|
|
3188
3789
|
},
|
|
3189
3790
|
async react(emoji) {
|
|
3190
3791
|
const local = store.snapshot().localIdentity;
|
|
@@ -3194,32 +3795,41 @@ function createRoomEngine(options) {
|
|
|
3194
3795
|
}
|
|
3195
3796
|
await publish(DATA_TOPICS.reaction, { id, emoji });
|
|
3196
3797
|
},
|
|
3197
|
-
async
|
|
3798
|
+
async sendPrivateChat(to, text3) {
|
|
3198
3799
|
const trimmed = text3.trim();
|
|
3199
3800
|
if (trimmed.length === 0) return;
|
|
3200
3801
|
const local = store.snapshot().localIdentity;
|
|
3201
3802
|
const id = `${local ?? "local"}-${now()}`;
|
|
3202
3803
|
const at = new Date(now()).toISOString();
|
|
3203
3804
|
if (local !== null) {
|
|
3204
|
-
store.addChat({
|
|
3205
|
-
id,
|
|
3206
|
-
identity: local,
|
|
3207
|
-
displayName: identity.displayName,
|
|
3208
|
-
text: trimmed,
|
|
3209
|
-
at
|
|
3210
|
-
});
|
|
3805
|
+
store.addChat({ id, identity: local, displayName: identity.displayName, text: trimmed, at, to });
|
|
3211
3806
|
}
|
|
3212
|
-
await publish(
|
|
3807
|
+
await publish(
|
|
3808
|
+
DATA_TOPICS.chat,
|
|
3809
|
+
{ id, text: trimmed, at, name: identity.displayName, to },
|
|
3810
|
+
[to]
|
|
3811
|
+
);
|
|
3213
3812
|
},
|
|
3214
|
-
async
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3813
|
+
async setEffects(next) {
|
|
3814
|
+
const previous = store.snapshot().effects;
|
|
3815
|
+
const merged = { ...previous, ...next };
|
|
3816
|
+
store.patch({ effects: merged });
|
|
3817
|
+
options.effectsStorage?.save(merged);
|
|
3818
|
+
if (driver === null) return;
|
|
3819
|
+
try {
|
|
3820
|
+
await applyEffects(driver, merged);
|
|
3821
|
+
} catch {
|
|
3822
|
+
const reverted = { ...merged, background: previous.background, backgroundImage: previous.backgroundImage };
|
|
3823
|
+
store.patch({ effects: reverted });
|
|
3824
|
+
options.effectsStorage?.save(reverted);
|
|
3221
3825
|
}
|
|
3222
|
-
|
|
3826
|
+
},
|
|
3827
|
+
supportsVideoEffects() {
|
|
3828
|
+
return driver?.supportsVideoEffects?.() ?? false;
|
|
3829
|
+
},
|
|
3830
|
+
async switchDevice(kind, deviceId2) {
|
|
3831
|
+
if (driver === null) return;
|
|
3832
|
+
await driver.switchDevice(kind, deviceId2);
|
|
3223
3833
|
},
|
|
3224
3834
|
pin(who) {
|
|
3225
3835
|
store.patch({ pinned: who, layout: who === null ? "grid" : "speaker" });
|
|
@@ -3250,58 +3860,281 @@ function hostUserIds(participants) {
|
|
|
3250
3860
|
return participants.filter((participant) => participant.role === "host" && participant.userId !== null).map((participant) => participant.userId);
|
|
3251
3861
|
}
|
|
3252
3862
|
|
|
3253
|
-
// src/core/
|
|
3254
|
-
function
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
default:
|
|
3265
|
-
return "unknown";
|
|
3266
|
-
}
|
|
3267
|
-
}
|
|
3268
|
-
function mapConnectionState(state) {
|
|
3269
|
-
switch (state) {
|
|
3270
|
-
case "connected":
|
|
3271
|
-
return "connected";
|
|
3272
|
-
case "connecting":
|
|
3273
|
-
return "connecting";
|
|
3274
|
-
case "reconnecting":
|
|
3275
|
-
case "signalReconnecting":
|
|
3276
|
-
return "reconnecting";
|
|
3277
|
-
default:
|
|
3278
|
-
return "disconnected";
|
|
3279
|
-
}
|
|
3863
|
+
// src/core/effects.ts
|
|
3864
|
+
function coerce(value) {
|
|
3865
|
+
if (typeof value !== "object" || value === null) return DEFAULT_EFFECTS;
|
|
3866
|
+
const row = value;
|
|
3867
|
+
const background = row["background"] === "blur" || row["background"] === "image" ? row["background"] : "none";
|
|
3868
|
+
const backgroundImage = typeof row["backgroundImage"] === "string" ? row["backgroundImage"] : null;
|
|
3869
|
+
return {
|
|
3870
|
+
noiseSuppression: row["noiseSuppression"] !== false,
|
|
3871
|
+
background: background === "image" && backgroundImage === null ? "none" : background,
|
|
3872
|
+
backgroundImage
|
|
3873
|
+
};
|
|
3280
3874
|
}
|
|
3281
|
-
function
|
|
3282
|
-
|
|
3875
|
+
function createWebEffectsStorage(key = "mx.meet.effects") {
|
|
3876
|
+
let memory = DEFAULT_EFFECTS;
|
|
3877
|
+
const storage = globalThis.localStorage;
|
|
3283
3878
|
return {
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3879
|
+
load() {
|
|
3880
|
+
if (storage === void 0) return memory;
|
|
3881
|
+
try {
|
|
3882
|
+
const raw = storage.getItem(key);
|
|
3883
|
+
return raw === null ? memory : coerce(JSON.parse(raw));
|
|
3884
|
+
} catch {
|
|
3885
|
+
return memory;
|
|
3886
|
+
}
|
|
3887
|
+
},
|
|
3888
|
+
save(effects) {
|
|
3889
|
+
memory = effects;
|
|
3890
|
+
try {
|
|
3891
|
+
storage?.setItem(key, JSON.stringify(effects));
|
|
3892
|
+
} catch {
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3295
3895
|
};
|
|
3296
3896
|
}
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3897
|
+
|
|
3898
|
+
// src/core/in-room.ts
|
|
3899
|
+
var MODERATION_ACTIONS = [
|
|
3900
|
+
"mute",
|
|
3901
|
+
"stop_video",
|
|
3902
|
+
"stop_screen",
|
|
3903
|
+
"mute_all",
|
|
3904
|
+
"ask_unmute",
|
|
3905
|
+
"ask_all_unmute",
|
|
3906
|
+
"spotlight",
|
|
3907
|
+
"lower_hand",
|
|
3908
|
+
"lower_all_hands",
|
|
3909
|
+
"make_cohost",
|
|
3910
|
+
"revoke_cohost",
|
|
3911
|
+
"transfer_host",
|
|
3912
|
+
"rename",
|
|
3913
|
+
"remove",
|
|
3914
|
+
"report"
|
|
3915
|
+
];
|
|
3916
|
+
function createInRoomClient(options) {
|
|
3917
|
+
const { api, store } = options;
|
|
3918
|
+
const now = options.now ?? (() => Date.now());
|
|
3919
|
+
function meetingId(operation) {
|
|
3920
|
+
const meeting = store.snapshot().meeting;
|
|
3921
|
+
if (meeting === null) {
|
|
3922
|
+
throw new MeetError(
|
|
3923
|
+
"not-found",
|
|
3924
|
+
`${operation}: this room has no meeting record`,
|
|
3925
|
+
"Moderation and saved chat belong to a meeting. An ad-hoc call has neither."
|
|
3926
|
+
);
|
|
3927
|
+
}
|
|
3928
|
+
return meeting.id;
|
|
3929
|
+
}
|
|
3930
|
+
function headers() {
|
|
3931
|
+
const token = options.resolveRoomToken();
|
|
3932
|
+
return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
|
|
3933
|
+
}
|
|
3934
|
+
async function refusal2(response, operation, remedy) {
|
|
3935
|
+
let body = null;
|
|
3936
|
+
try {
|
|
3937
|
+
body = await response.json();
|
|
3938
|
+
} catch {
|
|
3939
|
+
body = null;
|
|
3940
|
+
}
|
|
3941
|
+
const said = readServerRefusal(body);
|
|
3942
|
+
return new MeetError(
|
|
3943
|
+
response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
|
|
3944
|
+
said?.reason ?? `${operation}: the server answered ${response.status}`,
|
|
3945
|
+
said?.remedy !== void 0 && said.remedy.length > 0 ? said.remedy : remedy
|
|
3946
|
+
);
|
|
3947
|
+
}
|
|
3948
|
+
return {
|
|
3949
|
+
async moderate(request) {
|
|
3950
|
+
const operation = `meet.moderate(${request.action})`;
|
|
3951
|
+
const hdrs = headers();
|
|
3952
|
+
const response = await api.request({
|
|
3953
|
+
path: MEET_ROUTES.moderate,
|
|
3954
|
+
method: "POST",
|
|
3955
|
+
operation,
|
|
3956
|
+
body: {
|
|
3957
|
+
meeting_id: meetingId(operation),
|
|
3958
|
+
action: request.action,
|
|
3959
|
+
...request.identity != null ? { identity: request.identity } : {},
|
|
3960
|
+
...request.name !== void 0 ? { name: request.name } : {},
|
|
3961
|
+
...request.reason !== void 0 ? { reason: request.reason } : {}
|
|
3962
|
+
},
|
|
3963
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
3964
|
+
});
|
|
3965
|
+
if (!response.ok) {
|
|
3966
|
+
throw await refusal2(
|
|
3967
|
+
response,
|
|
3968
|
+
operation,
|
|
3969
|
+
"Nothing was changed. The server decides host powers from the meeting itself."
|
|
3970
|
+
);
|
|
3971
|
+
}
|
|
3972
|
+
const payload = await response.json();
|
|
3973
|
+
return {
|
|
3974
|
+
action: request.action,
|
|
3975
|
+
affected: Array.isArray(payload["affected"]) ? payload["affected"].filter((value) => typeof value === "string") : [],
|
|
3976
|
+
message: typeof payload["message"] === "string" ? payload["message"] : "Done."
|
|
3977
|
+
};
|
|
3978
|
+
},
|
|
3979
|
+
async sendChat(text3) {
|
|
3980
|
+
const body = text3.trim();
|
|
3981
|
+
if (body.length === 0) return;
|
|
3982
|
+
const operation = "meet.sendChat";
|
|
3983
|
+
const id = meetingId(operation);
|
|
3984
|
+
const snapshot = store.snapshot();
|
|
3985
|
+
const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
|
|
3986
|
+
const localId = `pending-${now()}`;
|
|
3987
|
+
const pending = {
|
|
3988
|
+
id: localId,
|
|
3989
|
+
identity: snapshot.localIdentity ?? "local",
|
|
3990
|
+
displayName: me?.displayName ?? "You",
|
|
3991
|
+
text: body,
|
|
3992
|
+
at: new Date(now()).toISOString(),
|
|
3993
|
+
saved: false
|
|
3994
|
+
};
|
|
3995
|
+
store.addChat(pending);
|
|
3996
|
+
const hdrs = headers();
|
|
3997
|
+
let response;
|
|
3998
|
+
try {
|
|
3999
|
+
response = await api.request({
|
|
4000
|
+
path: MEET_ROUTES.chat,
|
|
4001
|
+
method: "POST",
|
|
4002
|
+
operation,
|
|
4003
|
+
body: { meeting_id: id, body },
|
|
4004
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
4005
|
+
});
|
|
4006
|
+
} catch (error) {
|
|
4007
|
+
store.settleChat(localId, { failed: "Not sent \u2014 the server could not be reached." });
|
|
4008
|
+
throw error;
|
|
4009
|
+
}
|
|
4010
|
+
if (!response.ok) {
|
|
4011
|
+
const error = await refusal2(response, operation, "Your message was not sent.");
|
|
4012
|
+
store.settleChat(localId, { failed: error.message });
|
|
4013
|
+
throw error;
|
|
4014
|
+
}
|
|
4015
|
+
const saved = chatFromRow(await response.json());
|
|
4016
|
+
if (saved === null) {
|
|
4017
|
+
store.settleChat(localId, { failed: "The server's answer could not be read." });
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
store.settleChat(localId, saved);
|
|
4021
|
+
},
|
|
4022
|
+
async loadChat() {
|
|
4023
|
+
const operation = "meet.loadChat";
|
|
4024
|
+
const id = meetingId(operation);
|
|
4025
|
+
const hdrs = headers();
|
|
4026
|
+
const response = await api.request({
|
|
4027
|
+
path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(id)}`,
|
|
4028
|
+
method: "GET",
|
|
4029
|
+
operation,
|
|
4030
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
4031
|
+
});
|
|
4032
|
+
if (!response.ok) {
|
|
4033
|
+
throw await refusal2(
|
|
4034
|
+
response,
|
|
4035
|
+
operation,
|
|
4036
|
+
"Messages sent before you joined could not be read. New messages still arrive."
|
|
4037
|
+
);
|
|
4038
|
+
}
|
|
4039
|
+
const payload = await response.json();
|
|
4040
|
+
const rows = Array.isArray(payload.messages) ? payload.messages : [];
|
|
4041
|
+
const messages = rows.map(
|
|
4042
|
+
(row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
|
|
4043
|
+
).filter((message) => message !== null);
|
|
4044
|
+
store.loadChat(messages);
|
|
4045
|
+
}
|
|
4046
|
+
};
|
|
4047
|
+
}
|
|
4048
|
+
|
|
4049
|
+
// src/core/livekit-driver.ts
|
|
4050
|
+
function mapQuality(quality) {
|
|
4051
|
+
switch (quality) {
|
|
4052
|
+
case "excellent":
|
|
4053
|
+
return "excellent";
|
|
4054
|
+
case "good":
|
|
4055
|
+
return "good";
|
|
4056
|
+
case "poor":
|
|
4057
|
+
return "poor";
|
|
4058
|
+
case "lost":
|
|
4059
|
+
return "lost";
|
|
4060
|
+
default:
|
|
4061
|
+
return "unknown";
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
function mapConnectionState(state) {
|
|
4065
|
+
switch (state) {
|
|
4066
|
+
case "connected":
|
|
4067
|
+
return "connected";
|
|
4068
|
+
case "connecting":
|
|
4069
|
+
return "connecting";
|
|
4070
|
+
case "reconnecting":
|
|
4071
|
+
case "signalReconnecting":
|
|
4072
|
+
return "reconnecting";
|
|
4073
|
+
default:
|
|
4074
|
+
return "disconnected";
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
function snapshotParticipant(participant, isLocal) {
|
|
4078
|
+
const name = participant.name;
|
|
4079
|
+
return {
|
|
4080
|
+
identity: participant.identity,
|
|
4081
|
+
name: name ?? "",
|
|
4082
|
+
metadata: participant.metadata ?? null,
|
|
4083
|
+
// ATTRIBUTES ARE PART OF THE SNAPSHOT, not read ad hoc later: the note-taker
|
|
4084
|
+
// announces the consent notice here (D10) and the roster is the only place
|
|
4085
|
+
// this package looks at a participant.
|
|
4086
|
+
attributes: participant.attributes ?? {},
|
|
4087
|
+
isLocal,
|
|
4088
|
+
micEnabled: participant.isMicrophoneEnabled,
|
|
4089
|
+
cameraEnabled: participant.isCameraEnabled,
|
|
4090
|
+
screenSharing: participant.isScreenShareEnabled
|
|
4091
|
+
};
|
|
4092
|
+
}
|
|
4093
|
+
var DEFAULT_AUDIO_PROCESSING = {
|
|
4094
|
+
noiseSuppression: true,
|
|
4095
|
+
echoCancellation: true,
|
|
4096
|
+
autoGainControl: true
|
|
4097
|
+
};
|
|
4098
|
+
function videoEffectsSupported() {
|
|
4099
|
+
const scope = globalThis;
|
|
4100
|
+
if (typeof scope.OffscreenCanvas === "undefined" || scope.document === void 0) return false;
|
|
4101
|
+
try {
|
|
4102
|
+
return scope.document.createElement("canvas").getContext("webgl2") !== null;
|
|
4103
|
+
} catch {
|
|
4104
|
+
return false;
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
async function applyVideoEffect(target, effect) {
|
|
4108
|
+
const { Track } = await import("livekit-client");
|
|
4109
|
+
const track = target.localParticipant.getTrackPublication(Track.Source.Camera)?.track;
|
|
4110
|
+
if (track === void 0) return;
|
|
4111
|
+
try {
|
|
4112
|
+
if (effect.kind === "none") {
|
|
4113
|
+
await track.stopProcessor();
|
|
4114
|
+
return;
|
|
4115
|
+
}
|
|
4116
|
+
const processors = await import("@livekit/track-processors");
|
|
4117
|
+
const processor = effect.kind === "blur" ? processors.BackgroundBlur(effect.radius ?? 12) : processors.VirtualBackground(effect.imageUrl);
|
|
4118
|
+
await track.setProcessor(processor);
|
|
4119
|
+
} catch (error) {
|
|
4120
|
+
throw new MeetError(
|
|
4121
|
+
"media-unsupported",
|
|
4122
|
+
"The background effect could not start on this camera",
|
|
4123
|
+
"Background effects need a browser with WebGL2 (current Chrome, Edge, Firefox or Safari). Your video continues without the effect.",
|
|
4124
|
+
error
|
|
4125
|
+
);
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
var encoder = new TextEncoder();
|
|
4129
|
+
var decoder = new TextDecoder();
|
|
4130
|
+
function createLiveKitDriver(handlers, options = {}) {
|
|
4131
|
+
let room = null;
|
|
4132
|
+
let projectRoster = null;
|
|
4133
|
+
let audioCapture = DEFAULT_AUDIO_PROCESSING;
|
|
4134
|
+
let videoEffect = { kind: "none" };
|
|
4135
|
+
async function load() {
|
|
4136
|
+
if (room !== null) return room;
|
|
4137
|
+
let module;
|
|
3305
4138
|
try {
|
|
3306
4139
|
module = await import("livekit-client");
|
|
3307
4140
|
} catch (error) {
|
|
@@ -3327,7 +4160,9 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
3327
4160
|
handlers.participants([local, ...remotes]);
|
|
3328
4161
|
};
|
|
3329
4162
|
projectRoster = roster;
|
|
3330
|
-
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.
|
|
4163
|
+
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) => {
|
|
4164
|
+
handlers.roomMetadata?.(metadata ?? "");
|
|
4165
|
+
}).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
|
|
3331
4166
|
handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
|
|
3332
4167
|
}).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
|
|
3333
4168
|
handlers.quality(participant.identity, mapQuality(quality));
|
|
@@ -3404,6 +4239,7 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
3404
4239
|
throw normalizeDeviceError(error, "meet.connect");
|
|
3405
4240
|
}
|
|
3406
4241
|
projectRoster?.();
|
|
4242
|
+
handlers.roomMetadata?.(target.metadata ?? "");
|
|
3407
4243
|
},
|
|
3408
4244
|
async disconnect() {
|
|
3409
4245
|
if (room === null) return;
|
|
@@ -3418,14 +4254,19 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
3418
4254
|
},
|
|
3419
4255
|
async setMicrophoneEnabled(enabled) {
|
|
3420
4256
|
try {
|
|
3421
|
-
await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(
|
|
4257
|
+
await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(
|
|
4258
|
+
enabled,
|
|
4259
|
+
audioCapture
|
|
4260
|
+
);
|
|
3422
4261
|
} catch (error) {
|
|
3423
4262
|
throw normalizeDeviceError(error, "meet.setMicrophoneEnabled");
|
|
3424
4263
|
}
|
|
3425
4264
|
},
|
|
3426
4265
|
async setCameraEnabled(enabled) {
|
|
3427
4266
|
try {
|
|
3428
|
-
|
|
4267
|
+
const target = requireRoom("setCameraEnabled");
|
|
4268
|
+
await target.localParticipant.setCameraEnabled(enabled);
|
|
4269
|
+
if (enabled && videoEffect.kind !== "none") await applyVideoEffect(target, videoEffect);
|
|
3429
4270
|
} catch (error) {
|
|
3430
4271
|
throw normalizeDeviceError(error, "meet.setCameraEnabled");
|
|
3431
4272
|
}
|
|
@@ -3437,14 +4278,40 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
3437
4278
|
throw normalizeDeviceError(error, "meet.setScreenShareEnabled");
|
|
3438
4279
|
}
|
|
3439
4280
|
},
|
|
3440
|
-
async publishData(topic, payload) {
|
|
4281
|
+
async publishData(topic, payload, to) {
|
|
3441
4282
|
await requireRoom("publishData").localParticipant.publishData(
|
|
3442
4283
|
// `reliable` is not optional for a control plane: a dropped
|
|
3443
4284
|
// "hand lowered" leaves a hand up on every other screen forever.
|
|
3444
4285
|
encoder.encode(payload),
|
|
3445
|
-
{ reliable: true, topic }
|
|
4286
|
+
to === void 0 || to.length === 0 ? { reliable: true, topic } : { reliable: true, topic, destinationIdentities: [...to] }
|
|
3446
4287
|
);
|
|
3447
4288
|
},
|
|
4289
|
+
async setAudioProcessing(processing) {
|
|
4290
|
+
audioCapture = { ...audioCapture, ...processing };
|
|
4291
|
+
const target = room;
|
|
4292
|
+
if (target === null) return;
|
|
4293
|
+
const { Track } = await import("livekit-client");
|
|
4294
|
+
const publication = target.localParticipant.getTrackPublication(Track.Source.Microphone);
|
|
4295
|
+
const track = publication?.track;
|
|
4296
|
+
if (track === void 0 || publication?.isMuted === true) return;
|
|
4297
|
+
try {
|
|
4298
|
+
await track.restartTrack({
|
|
4299
|
+
...audioCapture,
|
|
4300
|
+
...target.getActiveDevice("audioinput") !== void 0 ? { deviceId: target.getActiveDevice("audioinput") } : {}
|
|
4301
|
+
});
|
|
4302
|
+
} catch (error) {
|
|
4303
|
+
throw normalizeDeviceError(error, "meet.setAudioProcessing");
|
|
4304
|
+
}
|
|
4305
|
+
},
|
|
4306
|
+
async setVideoEffect(effect) {
|
|
4307
|
+
videoEffect = effect;
|
|
4308
|
+
const target = room;
|
|
4309
|
+
if (target === null) return;
|
|
4310
|
+
await applyVideoEffect(target, effect);
|
|
4311
|
+
},
|
|
4312
|
+
supportsVideoEffects() {
|
|
4313
|
+
return videoEffectsSupported();
|
|
4314
|
+
},
|
|
3448
4315
|
attach(identity, kind, element) {
|
|
3449
4316
|
const target = requireRoom("attach");
|
|
3450
4317
|
const source = sources?.[kind];
|
|
@@ -3478,6 +4345,52 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
3478
4345
|
}
|
|
3479
4346
|
};
|
|
3480
4347
|
}
|
|
4348
|
+
async function createCameraPreview(deviceId2, effect) {
|
|
4349
|
+
let module;
|
|
4350
|
+
try {
|
|
4351
|
+
module = await import("livekit-client");
|
|
4352
|
+
} catch (error) {
|
|
4353
|
+
throw new MeetError(
|
|
4354
|
+
"misconfigured",
|
|
4355
|
+
"livekit-client could not be loaded for the camera preview",
|
|
4356
|
+
"The bundler excluded `livekit-client`; remove it from any `external` list.",
|
|
4357
|
+
error
|
|
4358
|
+
);
|
|
4359
|
+
}
|
|
4360
|
+
let track;
|
|
4361
|
+
try {
|
|
4362
|
+
track = await module.createLocalVideoTrack(deviceId2 === null ? {} : { deviceId: deviceId2 });
|
|
4363
|
+
} catch (error) {
|
|
4364
|
+
throw normalizeDeviceError(error, "meet.cameraPreview");
|
|
4365
|
+
}
|
|
4366
|
+
const target = track;
|
|
4367
|
+
async function apply(next) {
|
|
4368
|
+
if (next.kind === "none") {
|
|
4369
|
+
await target.stopProcessor();
|
|
4370
|
+
return;
|
|
4371
|
+
}
|
|
4372
|
+
const processors = await import("@livekit/track-processors");
|
|
4373
|
+
await target.setProcessor(
|
|
4374
|
+
next.kind === "blur" ? processors.BackgroundBlur(next.radius ?? 12) : processors.VirtualBackground(next.imageUrl)
|
|
4375
|
+
);
|
|
4376
|
+
}
|
|
4377
|
+
if (effect.kind !== "none" && videoEffectsSupported()) {
|
|
4378
|
+
try {
|
|
4379
|
+
await apply(effect);
|
|
4380
|
+
} catch {
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4383
|
+
return {
|
|
4384
|
+
attach(element) {
|
|
4385
|
+
track.attach(element);
|
|
4386
|
+
},
|
|
4387
|
+
setEffect: apply,
|
|
4388
|
+
stop() {
|
|
4389
|
+
track.stop();
|
|
4390
|
+
track.detach();
|
|
4391
|
+
}
|
|
4392
|
+
};
|
|
4393
|
+
}
|
|
3481
4394
|
|
|
3482
4395
|
// src/react/provider.tsx
|
|
3483
4396
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -3587,7 +4500,9 @@ function MeetRuntime(props) {
|
|
|
3587
4500
|
avatarUrl: props.avatarUrl ?? null,
|
|
3588
4501
|
deviceId: deviceId()
|
|
3589
4502
|
};
|
|
3590
|
-
const
|
|
4503
|
+
const effectsStorage = createWebEffectsStorage();
|
|
4504
|
+
const store = createMeetStore({ effects: effectsStorage.load() });
|
|
4505
|
+
const devicesHolder = { current: null };
|
|
3591
4506
|
const api = createMeetApi({
|
|
3592
4507
|
baseUrl,
|
|
3593
4508
|
organizationId: identity.organizationId,
|
|
@@ -3606,7 +4521,9 @@ function MeetRuntime(props) {
|
|
|
3606
4521
|
identity,
|
|
3607
4522
|
resolveMeeting: (slug) => repository.meetingBySlug(slug),
|
|
3608
4523
|
driverFactory: (handlers) => (driverRef.current ?? createLiveKitDriver)(handlers),
|
|
3609
|
-
onDiagnostic: report
|
|
4524
|
+
onDiagnostic: report,
|
|
4525
|
+
effectsStorage,
|
|
4526
|
+
resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null
|
|
3610
4527
|
});
|
|
3611
4528
|
const meetings = createMeetingSession({
|
|
3612
4529
|
manager,
|
|
@@ -3621,16 +4538,32 @@ function MeetRuntime(props) {
|
|
|
3621
4538
|
void engine.rejoin();
|
|
3622
4539
|
}
|
|
3623
4540
|
});
|
|
3624
|
-
const
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
4541
|
+
const resolveRoomToken = () => {
|
|
4542
|
+
const roomName = store.snapshot().meeting?.roomName;
|
|
4543
|
+
if (roomName === void 0) return null;
|
|
4544
|
+
const active = engine.token();
|
|
4545
|
+
if (active?.roomName === roomName && active.token.length > 0) return active.token;
|
|
4546
|
+
return tokens.remembered(roomName)?.token ?? null;
|
|
4547
|
+
};
|
|
4548
|
+
const ai = createMeetAi({ api, store, resolveRoomToken });
|
|
4549
|
+
const inRoom = createInRoomClient({ api, store, resolveRoomToken });
|
|
4550
|
+
let chatReadFor = null;
|
|
4551
|
+
store.subscribe(() => {
|
|
4552
|
+
const snapshot = store.snapshot();
|
|
4553
|
+
if (snapshot.phase !== "connected" || snapshot.meeting === null) {
|
|
4554
|
+
if (snapshot.phase === "left" || snapshot.phase === "idle") chatReadFor = null;
|
|
4555
|
+
return;
|
|
3633
4556
|
}
|
|
4557
|
+
if (chatReadFor === snapshot.meeting.id) return;
|
|
4558
|
+
chatReadFor = snapshot.meeting.id;
|
|
4559
|
+
void inRoom.loadChat().catch((error) => {
|
|
4560
|
+
const normalized = error;
|
|
4561
|
+
report({
|
|
4562
|
+
level: "warn",
|
|
4563
|
+
message: normalized.message ?? "meet: the saved chat could not be read",
|
|
4564
|
+
...normalized.remedy !== void 0 ? { remedy: normalized.remedy } : {}
|
|
4565
|
+
});
|
|
4566
|
+
});
|
|
3634
4567
|
});
|
|
3635
4568
|
const recording = createRecordingController({
|
|
3636
4569
|
api,
|
|
@@ -3652,6 +4585,7 @@ function MeetRuntime(props) {
|
|
|
3652
4585
|
},
|
|
3653
4586
|
storage: createWebDeviceStorage()
|
|
3654
4587
|
});
|
|
4588
|
+
devicesHolder.current = devices;
|
|
3655
4589
|
const calls = identity.userId === null ? null : createCallCenter({
|
|
3656
4590
|
manager,
|
|
3657
4591
|
repository,
|
|
@@ -3666,6 +4600,7 @@ function MeetRuntime(props) {
|
|
|
3666
4600
|
engine,
|
|
3667
4601
|
calls,
|
|
3668
4602
|
meetings,
|
|
4603
|
+
inRoom,
|
|
3669
4604
|
recording,
|
|
3670
4605
|
devices,
|
|
3671
4606
|
repository,
|
|
@@ -3787,7 +4722,8 @@ function computeStage(args) {
|
|
|
3787
4722
|
const ordered = orderParticipants(args.participants, args.activeSpeakers);
|
|
3788
4723
|
const sharer = ordered.find((participant) => participant.screenSharing) ?? null;
|
|
3789
4724
|
const pinnedParticipant = args.pinned === null ? null : ordered.find((participant) => participant.identity === args.pinned) ?? null;
|
|
3790
|
-
const
|
|
4725
|
+
const spotlit = args.spotlight == null ? null : ordered.find((participant) => participant.identity === args.spotlight) ?? null;
|
|
4726
|
+
const feature = sharer !== null ? sharer : pinnedParticipant !== null && args.layout !== "grid" ? pinnedParticipant : spotlit !== null ? spotlit : args.layout === "grid" ? null : ordered[0] ?? null;
|
|
3791
4727
|
const rest = feature === null ? ordered : ordered.filter((participant) => participant.identity !== feature.identity);
|
|
3792
4728
|
const pageCount = Math.max(1, Math.ceil(rest.length / pageSize));
|
|
3793
4729
|
const page = Math.min(Math.max(0, args.page ?? 0), pageCount - 1);
|
|
@@ -3801,11 +4737,11 @@ function computeStage(args) {
|
|
|
3801
4737
|
presenting: sharer !== null
|
|
3802
4738
|
};
|
|
3803
4739
|
}
|
|
3804
|
-
function gridColumns(
|
|
3805
|
-
if (
|
|
3806
|
-
if (
|
|
3807
|
-
if (
|
|
3808
|
-
if (
|
|
4740
|
+
function gridColumns(count2) {
|
|
4741
|
+
if (count2 <= 1) return 1;
|
|
4742
|
+
if (count2 <= 4) return 2;
|
|
4743
|
+
if (count2 <= 9) return 3;
|
|
4744
|
+
if (count2 <= 16) return 4;
|
|
3809
4745
|
return 5;
|
|
3810
4746
|
}
|
|
3811
4747
|
|
|
@@ -3979,7 +4915,7 @@ function meetingWhen(meeting, locale) {
|
|
|
3979
4915
|
if (start === null) return "Time not recorded";
|
|
3980
4916
|
const startedAt = new Date(start);
|
|
3981
4917
|
if (Number.isNaN(startedAt.getTime())) return "Time not recorded";
|
|
3982
|
-
const
|
|
4918
|
+
const when2 = startedAt.toLocaleString(locale, {
|
|
3983
4919
|
weekday: "long",
|
|
3984
4920
|
year: "numeric",
|
|
3985
4921
|
month: "long",
|
|
@@ -3987,11 +4923,11 @@ function meetingWhen(meeting, locale) {
|
|
|
3987
4923
|
hour: "numeric",
|
|
3988
4924
|
minute: "2-digit"
|
|
3989
4925
|
});
|
|
3990
|
-
if (meeting.endedAt === null || meeting.startedAt === null) return
|
|
4926
|
+
if (meeting.endedAt === null || meeting.startedAt === null) return when2;
|
|
3991
4927
|
const endedAt = new Date(meeting.endedAt);
|
|
3992
|
-
if (Number.isNaN(endedAt.getTime())) return
|
|
4928
|
+
if (Number.isNaN(endedAt.getTime())) return when2;
|
|
3993
4929
|
const elapsedMs = endedAt.getTime() - startedAt.getTime();
|
|
3994
|
-
return `${
|
|
4930
|
+
return `${when2} \xB7 ${formatDurationMs(elapsedMs, { style: "long" })}`;
|
|
3995
4931
|
}
|
|
3996
4932
|
var GUEST_RECORD_ACCESS_LEVELS = [
|
|
3997
4933
|
"none",
|
|
@@ -4098,8 +5034,8 @@ async function loadGuestMeetingRecord(args) {
|
|
|
4098
5034
|
);
|
|
4099
5035
|
}
|
|
4100
5036
|
if (!response.ok) {
|
|
4101
|
-
const
|
|
4102
|
-
return refusedBundle(meeting,
|
|
5037
|
+
const refusal2 = await readRefusal(response);
|
|
5038
|
+
return refusedBundle(meeting, refusal2.message, refusal2.remedy);
|
|
4103
5039
|
}
|
|
4104
5040
|
const body = await response.json();
|
|
4105
5041
|
const payload = typeof body["data"] === "object" && body["data"] !== null ? body["data"] : body;
|
|
@@ -4190,8 +5126,8 @@ async function setGuestRecordAccess(args) {
|
|
|
4190
5126
|
}
|
|
4191
5127
|
});
|
|
4192
5128
|
if (!response.ok) {
|
|
4193
|
-
const
|
|
4194
|
-
throw new MeetError("forbidden",
|
|
5129
|
+
const refusal2 = await readRefusal(response);
|
|
5130
|
+
throw new MeetError("forbidden", refusal2.message, refusal2.remedy);
|
|
4195
5131
|
}
|
|
4196
5132
|
const body = await response.json();
|
|
4197
5133
|
const payload = typeof body["data"] === "object" && body["data"] !== null ? body["data"] : body;
|
|
@@ -4266,7 +5202,14 @@ function useRoom() {
|
|
|
4266
5202
|
[engine]
|
|
4267
5203
|
),
|
|
4268
5204
|
sendChat: useCallback2(
|
|
4269
|
-
async (text3) =>
|
|
5205
|
+
async (text3) => {
|
|
5206
|
+
if (host === null) throw noRuntime("sendChat");
|
|
5207
|
+
await host.inRoom.sendChat(text3);
|
|
5208
|
+
},
|
|
5209
|
+
[host]
|
|
5210
|
+
),
|
|
5211
|
+
sendPrivateChat: useCallback2(
|
|
5212
|
+
async (to, text3) => requireEngine(engine, "sendPrivateChat").sendPrivateChat(to, text3),
|
|
4270
5213
|
[engine]
|
|
4271
5214
|
),
|
|
4272
5215
|
pin: useCallback2(
|
|
@@ -4287,6 +5230,7 @@ function useStage(pageSize = DEFAULT_PAGE_SIZE) {
|
|
|
4287
5230
|
participants: snapshot?.participants ?? [],
|
|
4288
5231
|
layout: snapshot?.layout ?? "grid",
|
|
4289
5232
|
pinned: snapshot?.pinned ?? null,
|
|
5233
|
+
spotlight: snapshot?.spotlight ?? null,
|
|
4290
5234
|
activeSpeakers: snapshot?.activeSpeakers ?? [],
|
|
4291
5235
|
pageSize,
|
|
4292
5236
|
page
|
|
@@ -4619,33 +5563,160 @@ function useHostControls() {
|
|
|
4619
5563
|
const host = useMeetHost();
|
|
4620
5564
|
const snapshot = useMeetSnapshot();
|
|
4621
5565
|
const role = useRole();
|
|
4622
|
-
const engine = host?.engine ?? null;
|
|
4623
5566
|
const me = snapshot?.participants.find(
|
|
4624
5567
|
(participant) => participant.identity === snapshot.localIdentity
|
|
4625
5568
|
);
|
|
4626
5569
|
const actor = me === void 0 ? void 0 : { ...me, role };
|
|
5570
|
+
const [busy, setBusy] = useState2(false);
|
|
5571
|
+
const inRoom = host?.inRoom ?? null;
|
|
5572
|
+
const store = host?.store ?? null;
|
|
5573
|
+
const notice = snapshot?.actionNotice ?? null;
|
|
5574
|
+
const setNotice = useCallback2(
|
|
5575
|
+
(next) => store?.patch({ actionNotice: next === null ? null : { ...next, at: Date.now() } }),
|
|
5576
|
+
[store]
|
|
5577
|
+
);
|
|
5578
|
+
const run = useCallback2(
|
|
5579
|
+
async (request) => {
|
|
5580
|
+
if (inRoom === null) throw noRuntime(`moderate(${request.action})`);
|
|
5581
|
+
setBusy(true);
|
|
5582
|
+
try {
|
|
5583
|
+
const result = await inRoom.moderate(request);
|
|
5584
|
+
setNotice({ text: result.message, failed: false });
|
|
5585
|
+
return result;
|
|
5586
|
+
} catch (thrown) {
|
|
5587
|
+
const error = thrown;
|
|
5588
|
+
setNotice({
|
|
5589
|
+
text: [error.message, error.remedy].filter(Boolean).join(" "),
|
|
5590
|
+
failed: true
|
|
5591
|
+
});
|
|
5592
|
+
return null;
|
|
5593
|
+
} finally {
|
|
5594
|
+
setBusy(false);
|
|
5595
|
+
}
|
|
5596
|
+
},
|
|
5597
|
+
[inRoom, setNotice]
|
|
5598
|
+
);
|
|
4627
5599
|
return {
|
|
4628
5600
|
isHost: isHostRole(role),
|
|
5601
|
+
isOwner: role === "host",
|
|
5602
|
+
notice,
|
|
5603
|
+
busy,
|
|
5604
|
+
clearNotice: useCallback2(() => setNotice(null), [setNotice]),
|
|
4629
5605
|
canModerate: useCallback2(
|
|
4630
5606
|
(target) => canModerateParticipant(actor, target),
|
|
4631
5607
|
[actor]
|
|
4632
5608
|
),
|
|
5609
|
+
run,
|
|
4633
5610
|
mute: useCallback2(
|
|
4634
|
-
async (target) =>
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
[engine]
|
|
5611
|
+
async (target) => {
|
|
5612
|
+
await run({ action: "mute", identity: target.identity });
|
|
5613
|
+
},
|
|
5614
|
+
[run]
|
|
4639
5615
|
),
|
|
4640
5616
|
remove: useCallback2(
|
|
4641
|
-
async (target) =>
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
5617
|
+
async (target) => {
|
|
5618
|
+
await run({ action: "remove", identity: target.identity });
|
|
5619
|
+
},
|
|
5620
|
+
[run]
|
|
5621
|
+
)
|
|
5622
|
+
};
|
|
5623
|
+
}
|
|
5624
|
+
function useHandQueue() {
|
|
5625
|
+
const snapshot = useMeetSnapshot();
|
|
5626
|
+
return useMemo2(
|
|
5627
|
+
() => (snapshot?.participants ?? []).filter((participant) => participant.handRaised).sort((a, b) => (a.handRaisedAt ?? 0) - (b.handRaisedAt ?? 0)),
|
|
5628
|
+
[snapshot?.participants]
|
|
5629
|
+
);
|
|
5630
|
+
}
|
|
5631
|
+
function useMediaEffects() {
|
|
5632
|
+
const host = useMeetHost();
|
|
5633
|
+
const snapshot = useMeetSnapshot();
|
|
5634
|
+
const engine = host?.engine ?? null;
|
|
5635
|
+
return {
|
|
5636
|
+
effects: snapshot?.effects ?? DEFAULT_EFFECTS,
|
|
5637
|
+
backgroundsSupported: engine?.supportsVideoEffects() ?? false,
|
|
5638
|
+
set: useCallback2(
|
|
5639
|
+
async (next) => requireEngine(engine, "setEffects").setEffects(next),
|
|
4645
5640
|
[engine]
|
|
4646
5641
|
)
|
|
4647
5642
|
};
|
|
4648
5643
|
}
|
|
5644
|
+
function useModerationNotice() {
|
|
5645
|
+
const host = useMeetHost();
|
|
5646
|
+
const snapshot = useMeetSnapshot();
|
|
5647
|
+
return {
|
|
5648
|
+
notice: snapshot?.moderationNotice ?? null,
|
|
5649
|
+
dismiss: useCallback2(() => host?.store.patch({ moderationNotice: null }), [host])
|
|
5650
|
+
};
|
|
5651
|
+
}
|
|
5652
|
+
function useAudioLevel(stream) {
|
|
5653
|
+
const [level, setLevel] = useState2(0);
|
|
5654
|
+
useEffect2(() => {
|
|
5655
|
+
if (stream === null || stream.getAudioTracks().length === 0) {
|
|
5656
|
+
setLevel(0);
|
|
5657
|
+
return void 0;
|
|
5658
|
+
}
|
|
5659
|
+
const Context = globalThis.AudioContext ?? globalThis.webkitAudioContext;
|
|
5660
|
+
if (Context === void 0) return void 0;
|
|
5661
|
+
const context = new Context();
|
|
5662
|
+
const source = context.createMediaStreamSource(stream);
|
|
5663
|
+
const analyser = context.createAnalyser();
|
|
5664
|
+
analyser.fftSize = 512;
|
|
5665
|
+
source.connect(analyser);
|
|
5666
|
+
const samples = new Uint8Array(analyser.fftSize);
|
|
5667
|
+
let frame = 0;
|
|
5668
|
+
const tick = () => {
|
|
5669
|
+
analyser.getByteTimeDomainData(samples);
|
|
5670
|
+
let peak = 0;
|
|
5671
|
+
for (const sample of samples) peak = Math.max(peak, Math.abs(sample - 128) / 128);
|
|
5672
|
+
setLevel((previous) => Math.abs(previous - peak) > 0.02 ? peak : previous);
|
|
5673
|
+
frame = requestAnimationFrame(tick);
|
|
5674
|
+
};
|
|
5675
|
+
frame = requestAnimationFrame(tick);
|
|
5676
|
+
return () => {
|
|
5677
|
+
cancelAnimationFrame(frame);
|
|
5678
|
+
source.disconnect();
|
|
5679
|
+
void context.close();
|
|
5680
|
+
};
|
|
5681
|
+
}, [stream]);
|
|
5682
|
+
return level;
|
|
5683
|
+
}
|
|
5684
|
+
var MEET_SHORTCUTS = [
|
|
5685
|
+
{ id: "mic", keys: "\u2318/Ctrl + D", label: "Mute or unmute your microphone" },
|
|
5686
|
+
{ id: "camera", keys: "\u2318/Ctrl + E", label: "Turn your camera on or off" },
|
|
5687
|
+
{ id: "hand", keys: "\u2318/Ctrl + Alt + H", label: "Raise or lower your hand" },
|
|
5688
|
+
{ id: "chat", keys: "\u2318/Ctrl + Alt + C", label: "Open or close chat" },
|
|
5689
|
+
{ id: "people", keys: "\u2318/Ctrl + Alt + P", label: "Open or close people" },
|
|
5690
|
+
{ id: "help", keys: "Shift + ?", label: "Show keyboard shortcuts" }
|
|
5691
|
+
];
|
|
5692
|
+
function useMeetShortcuts(handlers) {
|
|
5693
|
+
const ref = useRef2(handlers);
|
|
5694
|
+
ref.current = handlers;
|
|
5695
|
+
useEffect2(() => {
|
|
5696
|
+
const target = globalThis.window;
|
|
5697
|
+
if (target === void 0) return void 0;
|
|
5698
|
+
const onKey = (event) => {
|
|
5699
|
+
const element = event.target;
|
|
5700
|
+
const typing = element !== null && (element.isContentEditable || element.tagName === "INPUT" || element.tagName === "TEXTAREA" || element.tagName === "SELECT");
|
|
5701
|
+
const mod = event.metaKey || event.ctrlKey;
|
|
5702
|
+
const key = event.key.toLowerCase();
|
|
5703
|
+
let id = null;
|
|
5704
|
+
if (mod && !event.altKey && !event.shiftKey && key === "d") id = "mic";
|
|
5705
|
+
else if (mod && !event.altKey && !event.shiftKey && key === "e") id = "camera";
|
|
5706
|
+
else if (mod && event.altKey && (event.code === "KeyH" || key === "h")) id = "hand";
|
|
5707
|
+
else if (mod && event.altKey && (event.code === "KeyC" || key === "c")) id = "chat";
|
|
5708
|
+
else if (mod && event.altKey && (event.code === "KeyP" || key === "p")) id = "people";
|
|
5709
|
+
else if (!mod && !typing && event.key === "?") id = "help";
|
|
5710
|
+
if (id === null) return;
|
|
5711
|
+
const handler = ref.current[id];
|
|
5712
|
+
if (handler === void 0) return;
|
|
5713
|
+
event.preventDefault();
|
|
5714
|
+
handler();
|
|
5715
|
+
};
|
|
5716
|
+
target.addEventListener("keydown", onKey);
|
|
5717
|
+
return () => target.removeEventListener("keydown", onKey);
|
|
5718
|
+
}, []);
|
|
5719
|
+
}
|
|
4649
5720
|
function useMeeting() {
|
|
4650
5721
|
return useMeetSnapshot()?.meeting ?? null;
|
|
4651
5722
|
}
|
|
@@ -4821,7 +5892,7 @@ function useGuestRecordAccess(meeting) {
|
|
|
4821
5892
|
}
|
|
4822
5893
|
|
|
4823
5894
|
// src/react/components.tsx
|
|
4824
|
-
import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
|
|
5895
|
+
import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
|
|
4825
5896
|
import { ErrorBox } from "@ai-matrx/design-system";
|
|
4826
5897
|
import { avatarPaletteIndex, formatDurationMs as formatDurationMs2, getInitials } from "@ai-matrx/kit/format";
|
|
4827
5898
|
|
|
@@ -4858,8 +5929,8 @@ function callStateLabel(state, outgoing) {
|
|
|
4858
5929
|
return "Call";
|
|
4859
5930
|
}
|
|
4860
5931
|
}
|
|
4861
|
-
function countLabel(
|
|
4862
|
-
return `${
|
|
5932
|
+
function countLabel(count2, singular, plural = `${singular}s`) {
|
|
5933
|
+
return `${count2} ${count2 === 1 ? singular : plural}`;
|
|
4863
5934
|
}
|
|
4864
5935
|
|
|
4865
5936
|
// src/react/icons.tsx
|
|
@@ -4992,6 +6063,60 @@ function WifiLowIcon(props) {
|
|
|
4992
6063
|
/* @__PURE__ */ jsx2("path", { d: "M8.5 16.5a5 5 0 0 1 7 0" })
|
|
4993
6064
|
] });
|
|
4994
6065
|
}
|
|
6066
|
+
function SmileIcon(props) {
|
|
6067
|
+
return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
|
|
6068
|
+
/* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "10" }),
|
|
6069
|
+
/* @__PURE__ */ jsx2("path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }),
|
|
6070
|
+
/* @__PURE__ */ jsx2("line", { x1: "9", x2: "9.01", y1: "9", y2: "9" }),
|
|
6071
|
+
/* @__PURE__ */ jsx2("line", { x1: "15", x2: "15.01", y1: "9", y2: "9" })
|
|
6072
|
+
] });
|
|
6073
|
+
}
|
|
6074
|
+
function MoreIcon(props) {
|
|
6075
|
+
return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
|
|
6076
|
+
/* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "1" }),
|
|
6077
|
+
/* @__PURE__ */ jsx2("circle", { cx: "19", cy: "12", r: "1" }),
|
|
6078
|
+
/* @__PURE__ */ jsx2("circle", { cx: "5", cy: "12", r: "1" })
|
|
6079
|
+
] });
|
|
6080
|
+
}
|
|
6081
|
+
function SearchIcon(props) {
|
|
6082
|
+
return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
|
|
6083
|
+
/* @__PURE__ */ jsx2("circle", { cx: "11", cy: "11", r: "8" }),
|
|
6084
|
+
/* @__PURE__ */ jsx2("path", { d: "m21 21-4.3-4.3" })
|
|
6085
|
+
] });
|
|
6086
|
+
}
|
|
6087
|
+
function KeyboardIcon(props) {
|
|
6088
|
+
return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
|
|
6089
|
+
/* @__PURE__ */ jsx2("path", { d: "M10 8h.01M12 12h.01M14 8h.01M16 12h.01M18 8h.01M6 8h.01M7 16h10M8 12h.01" }),
|
|
6090
|
+
/* @__PURE__ */ jsx2("rect", { width: "20", height: "16", x: "2", y: "4", rx: "2" })
|
|
6091
|
+
] });
|
|
6092
|
+
}
|
|
6093
|
+
function SpotlightIcon(props) {
|
|
6094
|
+
return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("path", { d: "M12 2 15 8.5 22 9.3 17 14 18.2 21 12 17.6 5.8 21 7 14 2 9.3 9 8.5Z" }) });
|
|
6095
|
+
}
|
|
6096
|
+
function SignalIcon({ bars, ...props }) {
|
|
6097
|
+
return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
|
|
6098
|
+
/* @__PURE__ */ jsx2("path", { d: "M4 20v-2", opacity: bars >= 1 ? 1 : 0.3 }),
|
|
6099
|
+
/* @__PURE__ */ jsx2("path", { d: "M10 20v-6", opacity: bars >= 2 ? 1 : 0.3 }),
|
|
6100
|
+
/* @__PURE__ */ jsx2("path", { d: "M16 20V9", opacity: bars >= 3 ? 1 : 0.3 }),
|
|
6101
|
+
/* @__PURE__ */ jsx2("path", { d: "M22 20V4", opacity: bars >= 3 ? 1 : 0.3 })
|
|
6102
|
+
] });
|
|
6103
|
+
}
|
|
6104
|
+
function SpeakerIcon(props) {
|
|
6105
|
+
return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
|
|
6106
|
+
/* @__PURE__ */ jsx2("path", { d: "M11 5 6 9H2v6h4l5 4V5Z" }),
|
|
6107
|
+
/* @__PURE__ */ jsx2("path", { d: "M15.54 8.46a5 5 0 0 1 0 7.07" }),
|
|
6108
|
+
/* @__PURE__ */ jsx2("path", { d: "M19.07 4.93a10 10 0 0 1 0 14.14" })
|
|
6109
|
+
] });
|
|
6110
|
+
}
|
|
6111
|
+
function FlagIcon(props) {
|
|
6112
|
+
return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
|
|
6113
|
+
/* @__PURE__ */ jsx2("path", { d: "M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z" }),
|
|
6114
|
+
/* @__PURE__ */ jsx2("line", { x1: "4", x2: "4", y1: "22", y2: "15" })
|
|
6115
|
+
] });
|
|
6116
|
+
}
|
|
6117
|
+
function PencilIcon(props) {
|
|
6118
|
+
return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("path", { d: "M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" }) });
|
|
6119
|
+
}
|
|
4995
6120
|
|
|
4996
6121
|
// src/react/components.tsx
|
|
4997
6122
|
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
@@ -5092,12 +6217,17 @@ function ParticipantTile({
|
|
|
5092
6217
|
featured = false
|
|
5093
6218
|
}) {
|
|
5094
6219
|
const host = useMeetHost();
|
|
5095
|
-
const
|
|
6220
|
+
const snapshot = useMeetSnapshot();
|
|
6221
|
+
const queue = useHandQueue();
|
|
6222
|
+
const [menu, setMenu] = useState3(false);
|
|
5096
6223
|
const videoRef = useTrack(
|
|
5097
6224
|
participant.identity,
|
|
5098
6225
|
participant.screenSharing ? "screen" : "camera"
|
|
5099
6226
|
);
|
|
5100
6227
|
const showVideo = participant.cameraEnabled || participant.screenSharing;
|
|
6228
|
+
const pinnedHere = snapshot?.pinned === participant.identity;
|
|
6229
|
+
const spotlit = snapshot?.spotlight === participant.identity;
|
|
6230
|
+
const handPosition = participant.handRaised ? queue.findIndex((entry) => entry.identity === participant.identity) + 1 : 0;
|
|
5101
6231
|
return /* @__PURE__ */ jsxs2(
|
|
5102
6232
|
"div",
|
|
5103
6233
|
{
|
|
@@ -5118,44 +6248,59 @@ function ParticipantTile({
|
|
|
5118
6248
|
muted: participant.isLocal
|
|
5119
6249
|
}
|
|
5120
6250
|
) : /* @__PURE__ */ jsx3("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ jsx3(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: featured ? 96 : 56 }) }),
|
|
6251
|
+
participant.handRaised ? /* @__PURE__ */ jsxs2("span", { className: "mx-meet__tile-hand", title: `Hand raised \xB7 ${ordinal(handPosition)} in line`, children: [
|
|
6252
|
+
/* @__PURE__ */ jsx3(HandIcon, { width: 14, height: 14 }),
|
|
6253
|
+
handPosition > 0 ? handPosition : null
|
|
6254
|
+
] }) : null,
|
|
5121
6255
|
/* @__PURE__ */ jsxs2("div", { className: "mx-meet__tile-bar", children: [
|
|
5122
6256
|
/* @__PURE__ */ jsxs2("span", { className: "mx-meet__tile-name", children: [
|
|
6257
|
+
spotlit ? /* @__PURE__ */ jsx3("span", { title: "Spotlighted for everyone", className: "mx-meet__tile-flag", children: /* @__PURE__ */ jsx3(SpotlightIcon, { width: 12, height: 12 }) }) : null,
|
|
5123
6258
|
participant.displayName,
|
|
5124
6259
|
participant.isLocal ? " (you)" : "",
|
|
5125
6260
|
participant.isAgent ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__badge", children: "AI" }) : null,
|
|
5126
6261
|
/* @__PURE__ */ jsx3(RoleBadge, { role: participant.role })
|
|
5127
6262
|
] }),
|
|
5128
6263
|
!participant.micEnabled ? /* @__PURE__ */ jsx3(MicOffIcon, { width: 14, height: 14 }) : null,
|
|
5129
|
-
|
|
5130
|
-
/* @__PURE__ */ jsx3(ConnectionDot, { quality: participant.quality })
|
|
6264
|
+
/* @__PURE__ */ jsx3(QualityBars, { quality: participant.quality })
|
|
5131
6265
|
] }),
|
|
5132
|
-
/* @__PURE__ */ jsxs2("div", { className: "mx-meet__tile-actions", children: [
|
|
6266
|
+
/* @__PURE__ */ jsxs2("div", { className: menu ? "mx-meet__tile-actions mx-meet__tile-actions--open" : "mx-meet__tile-actions", children: [
|
|
5133
6267
|
/* @__PURE__ */ jsx3(
|
|
5134
6268
|
ControlButton,
|
|
5135
6269
|
{
|
|
5136
|
-
label:
|
|
5137
|
-
|
|
6270
|
+
label: pinnedHere ? "Unpin" : `Pin ${participant.displayName} for me`,
|
|
6271
|
+
active: pinnedHere,
|
|
6272
|
+
onClick: () => host?.engine.pin(pinnedHere ? null : participant.identity),
|
|
5138
6273
|
children: /* @__PURE__ */ jsx3(PinIcon, { width: 16, height: 16 })
|
|
5139
6274
|
}
|
|
5140
6275
|
),
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
) : null
|
|
6276
|
+
/* @__PURE__ */ jsxs2("span", { className: "mx-meet__anchor", children: [
|
|
6277
|
+
/* @__PURE__ */ jsx3(
|
|
6278
|
+
ControlButton,
|
|
6279
|
+
{
|
|
6280
|
+
label: `More for ${participant.displayName}`,
|
|
6281
|
+
active: menu,
|
|
6282
|
+
onClick: () => setMenu(!menu),
|
|
6283
|
+
children: /* @__PURE__ */ jsx3(MoreIcon, { width: 16, height: 16 })
|
|
6284
|
+
}
|
|
6285
|
+
),
|
|
6286
|
+
menu ? /* @__PURE__ */ jsx3(PersonMenu, { participant, onClose: () => setMenu(false), placement: "down" }) : null
|
|
6287
|
+
] })
|
|
5154
6288
|
] })
|
|
5155
6289
|
]
|
|
5156
6290
|
}
|
|
5157
6291
|
);
|
|
5158
6292
|
}
|
|
6293
|
+
function ordinal(position) {
|
|
6294
|
+
if (position <= 0) return "";
|
|
6295
|
+
const suffix = position % 100 >= 11 && position % 100 <= 13 ? "th" : { 1: "st", 2: "nd", 3: "rd" }[position % 10] ?? "th";
|
|
6296
|
+
return `${position}${suffix}`;
|
|
6297
|
+
}
|
|
6298
|
+
function QualityBars({ quality }) {
|
|
6299
|
+
if (quality === "unknown") return null;
|
|
6300
|
+
const bars = quality === "excellent" ? 3 : quality === "good" ? 2 : quality === "poor" ? 1 : 0;
|
|
6301
|
+
const label = quality === "lost" ? "Connection lost" : quality === "poor" ? "Poor connection" : `Connection ${quality}`;
|
|
6302
|
+
return /* @__PURE__ */ jsx3("span", { className: `mx-meet__signal mx-meet__signal--${quality}`, title: label, "aria-label": label, role: "img", children: /* @__PURE__ */ jsx3(SignalIcon, { bars, width: 14, height: 14 }) });
|
|
6303
|
+
}
|
|
5159
6304
|
function Stage({ pageSize }) {
|
|
5160
6305
|
const stage = useStage(pageSize);
|
|
5161
6306
|
const columns = gridColumns(stage.tiles.length);
|
|
@@ -5184,22 +6329,52 @@ function Stage({ pageSize }) {
|
|
|
5184
6329
|
] }) : null
|
|
5185
6330
|
] });
|
|
5186
6331
|
}
|
|
6332
|
+
var MEET_REACTIONS = ["\u{1F496}", "\u{1F44D}", "\u{1F389}", "\u{1F44F}", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F914}", "\u{1F44E}"];
|
|
5187
6333
|
function ControlBar({ onLeave }) {
|
|
5188
6334
|
const room = useRoom();
|
|
5189
6335
|
const recording = useRecording();
|
|
5190
6336
|
const [panel, setPanel] = useState3("none");
|
|
6337
|
+
const [chatTo, setChatTo] = useState3(null);
|
|
6338
|
+
const [popover, setPopover] = useState3("none");
|
|
6339
|
+
const [shortcuts, setShortcuts] = useState3(false);
|
|
5191
6340
|
const snapshot = useMeetSnapshot();
|
|
5192
6341
|
const lobby = useLobby();
|
|
6342
|
+
const controls = useHostControls();
|
|
6343
|
+
const queue = useHandQueue();
|
|
5193
6344
|
const micUnavailable = useLocalMedia().microphone.unavailable;
|
|
6345
|
+
const unread = useUnreadChat(panel === "chat");
|
|
6346
|
+
const toggle = (next) => setPanel(panel === next ? "none" : next);
|
|
6347
|
+
useMeetShortcuts({
|
|
6348
|
+
mic: () => void room.toggleMicrophone(),
|
|
6349
|
+
camera: () => void room.toggleCamera(),
|
|
6350
|
+
hand: () => void room.toggleHand(),
|
|
6351
|
+
chat: () => toggle("chat"),
|
|
6352
|
+
people: () => toggle("people"),
|
|
6353
|
+
help: () => setShortcuts(true)
|
|
6354
|
+
});
|
|
6355
|
+
const hasSpotlight = snapshot?.spotlight !== null && snapshot?.spotlight !== void 0;
|
|
5194
6356
|
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
5195
|
-
|
|
5196
|
-
|
|
6357
|
+
/* @__PURE__ */ jsx3(MeetingOverlays, {}),
|
|
6358
|
+
shortcuts ? /* @__PURE__ */ jsx3(ShortcutsSheet, { onClose: () => setShortcuts(false) }) : null,
|
|
6359
|
+
panel === "chat" ? /* @__PURE__ */ jsx3(ChatPanel, { onClose: () => setPanel("none"), to: chatTo, onTo: setChatTo }) : null,
|
|
6360
|
+
panel === "people" ? /* @__PURE__ */ jsx3(
|
|
6361
|
+
PeoplePanel,
|
|
6362
|
+
{
|
|
6363
|
+
onClose: () => setPanel("none"),
|
|
6364
|
+
onMessage: (identity) => {
|
|
6365
|
+
setChatTo(identity);
|
|
6366
|
+
setPanel("chat");
|
|
6367
|
+
}
|
|
6368
|
+
}
|
|
6369
|
+
) : null,
|
|
5197
6370
|
panel === "ai" ? /* @__PURE__ */ jsx3(AiPanel, { onClose: () => setPanel("none") }) : null,
|
|
6371
|
+
panel === "settings" ? /* @__PURE__ */ jsx3(SettingsPanel, { onClose: () => setPanel("none") }) : null,
|
|
6372
|
+
controls.notice !== null && panel !== "people" ? /* @__PURE__ */ jsx3(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
|
|
5198
6373
|
/* @__PURE__ */ jsxs2("div", { className: "mx-meet__controls", children: [
|
|
5199
6374
|
/* @__PURE__ */ jsx3(
|
|
5200
6375
|
ControlButton,
|
|
5201
6376
|
{
|
|
5202
|
-
label: micUnavailable !== null ? `Unmute \u2014 ${micUnavailable.remedy}` : room.local?.micEnabled === true ? "Mute" : "Unmute",
|
|
6377
|
+
label: micUnavailable !== null ? `Unmute \u2014 ${micUnavailable.remedy}` : room.local?.micEnabled === true ? "Mute (\u2318D)" : "Unmute (\u2318D)",
|
|
5203
6378
|
active: room.local?.micEnabled === true,
|
|
5204
6379
|
onClick: () => void room.toggleMicrophone(),
|
|
5205
6380
|
children: room.local?.micEnabled === true ? /* @__PURE__ */ jsx3(MicIcon, {}) : /* @__PURE__ */ jsx3(MicOffIcon, {})
|
|
@@ -5208,7 +6383,7 @@ function ControlBar({ onLeave }) {
|
|
|
5208
6383
|
/* @__PURE__ */ jsx3(
|
|
5209
6384
|
ControlButton,
|
|
5210
6385
|
{
|
|
5211
|
-
label: room.local?.cameraEnabled === true ? "Stop video" : "Start video",
|
|
6386
|
+
label: room.local?.cameraEnabled === true ? "Stop video (\u2318E)" : "Start video (\u2318E)",
|
|
5212
6387
|
active: room.local?.cameraEnabled === true,
|
|
5213
6388
|
onClick: () => void room.toggleCamera(),
|
|
5214
6389
|
children: room.local?.cameraEnabled === true ? /* @__PURE__ */ jsx3(CameraIcon, {}) : /* @__PURE__ */ jsx3(CameraOffIcon, {})
|
|
@@ -5223,34 +6398,155 @@ function ControlBar({ onLeave }) {
|
|
|
5223
6398
|
children: /* @__PURE__ */ jsx3(ScreenShareIcon, {})
|
|
5224
6399
|
}
|
|
5225
6400
|
),
|
|
6401
|
+
/* @__PURE__ */ jsxs2("span", { className: "mx-meet__anchor", children: [
|
|
6402
|
+
/* @__PURE__ */ jsx3(
|
|
6403
|
+
ControlButton,
|
|
6404
|
+
{
|
|
6405
|
+
label: "Send a reaction",
|
|
6406
|
+
active: popover === "reactions",
|
|
6407
|
+
onClick: () => setPopover(popover === "reactions" ? "none" : "reactions"),
|
|
6408
|
+
children: /* @__PURE__ */ jsx3(SmileIcon, {})
|
|
6409
|
+
}
|
|
6410
|
+
),
|
|
6411
|
+
popover === "reactions" ? /* @__PURE__ */ jsx3(Popover, { label: "Reactions", onClose: () => setPopover("none"), children: /* @__PURE__ */ jsx3("div", { className: "mx-meet__emoji-bar", children: MEET_REACTIONS.map((emoji) => /* @__PURE__ */ jsx3(
|
|
6412
|
+
"button",
|
|
6413
|
+
{
|
|
6414
|
+
type: "button",
|
|
6415
|
+
className: "mx-meet__emoji",
|
|
6416
|
+
"aria-label": `React ${emoji}`,
|
|
6417
|
+
onClick: () => void room.react(emoji),
|
|
6418
|
+
children: emoji
|
|
6419
|
+
},
|
|
6420
|
+
emoji
|
|
6421
|
+
)) }) }) : null
|
|
6422
|
+
] }),
|
|
5226
6423
|
/* @__PURE__ */ jsx3(
|
|
5227
6424
|
ControlButton,
|
|
5228
6425
|
{
|
|
5229
|
-
label: room.local?.handRaised === true ? "Lower hand" : "Raise hand",
|
|
6426
|
+
label: room.local?.handRaised === true ? "Lower hand (\u2318\u2325H)" : "Raise hand (\u2318\u2325H)",
|
|
5230
6427
|
active: room.local?.handRaised === true,
|
|
5231
6428
|
onClick: () => void room.toggleHand(),
|
|
5232
6429
|
children: /* @__PURE__ */ jsx3(HandIcon, {})
|
|
5233
6430
|
}
|
|
5234
6431
|
),
|
|
5235
|
-
/* @__PURE__ */
|
|
6432
|
+
/* @__PURE__ */ jsxs2(ControlButton, { label: "Chat (\u2318\u2325C)", active: panel === "chat", onClick: () => toggle("chat"), children: [
|
|
6433
|
+
/* @__PURE__ */ jsx3(ChatIcon, {}),
|
|
6434
|
+
unread > 0 ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__dot", "aria-hidden": "true" }) : null
|
|
6435
|
+
] }),
|
|
5236
6436
|
/* @__PURE__ */ jsxs2(
|
|
5237
6437
|
ControlButton,
|
|
5238
6438
|
{
|
|
5239
6439
|
label: lobby.waiting.length > 0 ? `People \u2014 ${lobby.waiting.length} waiting` : `People \u2014 ${(snapshot?.participants ?? []).length} in the meeting`,
|
|
5240
6440
|
active: panel === "people",
|
|
5241
|
-
onClick: () =>
|
|
6441
|
+
onClick: () => toggle("people"),
|
|
5242
6442
|
children: [
|
|
5243
6443
|
/* @__PURE__ */ jsx3(PeopleIcon, {}),
|
|
5244
|
-
lobby.waiting.length > 0 ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__dot", "aria-hidden": "true" }) : null
|
|
6444
|
+
lobby.waiting.length > 0 || controls.isHost && queue.length > 0 ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__dot", "aria-hidden": "true" }) : null
|
|
5245
6445
|
]
|
|
5246
6446
|
}
|
|
5247
6447
|
),
|
|
5248
|
-
/* @__PURE__ */ jsx3(AiControl, { active: panel === "ai", onToggle: () =>
|
|
6448
|
+
/* @__PURE__ */ jsx3(AiControl, { active: panel === "ai", onToggle: () => toggle("ai") }),
|
|
5249
6449
|
recording.canStart ? /* @__PURE__ */ jsx3(ControlButton, { label: "Start recording", onClick: () => void recording.start(), children: /* @__PURE__ */ jsx3(RecordIcon, {}) }) : null,
|
|
5250
6450
|
recording.canStop ? /* @__PURE__ */ jsx3(ControlButton, { label: "Stop recording", active: true, onClick: () => void recording.stop(), children: /* @__PURE__ */ jsx3(RecordIcon, {}) }) : null,
|
|
5251
|
-
/* @__PURE__ */
|
|
5252
|
-
|
|
5253
|
-
|
|
6451
|
+
/* @__PURE__ */ jsxs2("span", { className: "mx-meet__anchor", children: [
|
|
6452
|
+
/* @__PURE__ */ jsx3(
|
|
6453
|
+
ControlButton,
|
|
6454
|
+
{
|
|
6455
|
+
label: "More options",
|
|
6456
|
+
active: popover === "more" || panel === "settings",
|
|
6457
|
+
onClick: () => setPopover(popover === "more" ? "none" : "more"),
|
|
6458
|
+
children: /* @__PURE__ */ jsx3(MoreIcon, {})
|
|
6459
|
+
}
|
|
6460
|
+
),
|
|
6461
|
+
popover === "more" ? /* @__PURE__ */ jsx3(Popover, { label: "More options", onClose: () => setPopover("none"), children: /* @__PURE__ */ jsxs2("div", { className: "mx-meet__menu", role: "menu", children: [
|
|
6462
|
+
/* @__PURE__ */ jsxs2(
|
|
6463
|
+
MenuItem,
|
|
6464
|
+
{
|
|
6465
|
+
onSelect: () => {
|
|
6466
|
+
setPopover("none");
|
|
6467
|
+
setPanel("settings");
|
|
6468
|
+
},
|
|
6469
|
+
children: [
|
|
6470
|
+
/* @__PURE__ */ jsx3(SettingsIcon, { width: 16, height: 16 }),
|
|
6471
|
+
" Audio, video and effects"
|
|
6472
|
+
]
|
|
6473
|
+
}
|
|
6474
|
+
),
|
|
6475
|
+
/* @__PURE__ */ jsxs2(
|
|
6476
|
+
MenuItem,
|
|
6477
|
+
{
|
|
6478
|
+
onSelect: () => {
|
|
6479
|
+
setPopover("none");
|
|
6480
|
+
setShortcuts(true);
|
|
6481
|
+
},
|
|
6482
|
+
children: [
|
|
6483
|
+
/* @__PURE__ */ jsx3(KeyboardIcon, { width: 16, height: 16 }),
|
|
6484
|
+
" Keyboard shortcuts"
|
|
6485
|
+
]
|
|
6486
|
+
}
|
|
6487
|
+
),
|
|
6488
|
+
controls.isHost ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
6489
|
+
/* @__PURE__ */ jsx3("div", { className: "mx-meet__menu-sep", role: "separator" }),
|
|
6490
|
+
/* @__PURE__ */ jsxs2(
|
|
6491
|
+
MenuItem,
|
|
6492
|
+
{
|
|
6493
|
+
onSelect: () => {
|
|
6494
|
+
setPopover("none");
|
|
6495
|
+
void controls.run({ action: "mute_all" });
|
|
6496
|
+
},
|
|
6497
|
+
children: [
|
|
6498
|
+
/* @__PURE__ */ jsx3(MicOffIcon, { width: 16, height: 16 }),
|
|
6499
|
+
" Mute everyone"
|
|
6500
|
+
]
|
|
6501
|
+
}
|
|
6502
|
+
),
|
|
6503
|
+
/* @__PURE__ */ jsxs2(
|
|
6504
|
+
MenuItem,
|
|
6505
|
+
{
|
|
6506
|
+
onSelect: () => {
|
|
6507
|
+
setPopover("none");
|
|
6508
|
+
void controls.run({ action: "ask_all_unmute" });
|
|
6509
|
+
},
|
|
6510
|
+
children: [
|
|
6511
|
+
/* @__PURE__ */ jsx3(MicIcon, { width: 16, height: 16 }),
|
|
6512
|
+
" Ask everyone to unmute"
|
|
6513
|
+
]
|
|
6514
|
+
}
|
|
6515
|
+
),
|
|
6516
|
+
queue.length > 0 ? /* @__PURE__ */ jsxs2(
|
|
6517
|
+
MenuItem,
|
|
6518
|
+
{
|
|
6519
|
+
onSelect: () => {
|
|
6520
|
+
setPopover("none");
|
|
6521
|
+
void controls.run({ action: "lower_all_hands" });
|
|
6522
|
+
},
|
|
6523
|
+
children: [
|
|
6524
|
+
/* @__PURE__ */ jsx3(HandIcon, { width: 16, height: 16 }),
|
|
6525
|
+
" Lower all hands (",
|
|
6526
|
+
queue.length,
|
|
6527
|
+
")"
|
|
6528
|
+
]
|
|
6529
|
+
}
|
|
6530
|
+
) : null,
|
|
6531
|
+
hasSpotlight ? /* @__PURE__ */ jsxs2(
|
|
6532
|
+
MenuItem,
|
|
6533
|
+
{
|
|
6534
|
+
onSelect: () => {
|
|
6535
|
+
setPopover("none");
|
|
6536
|
+
void controls.run({ action: "spotlight", identity: null });
|
|
6537
|
+
},
|
|
6538
|
+
children: [
|
|
6539
|
+
/* @__PURE__ */ jsx3(SpotlightIcon, { width: 16, height: 16 }),
|
|
6540
|
+
" Remove spotlight"
|
|
6541
|
+
]
|
|
6542
|
+
}
|
|
6543
|
+
) : null
|
|
6544
|
+
] }) : null
|
|
6545
|
+
] }) }) : null
|
|
6546
|
+
] }),
|
|
6547
|
+
/* @__PURE__ */ jsx3(
|
|
6548
|
+
ControlButton,
|
|
6549
|
+
{
|
|
5254
6550
|
label: "Leave",
|
|
5255
6551
|
danger: true,
|
|
5256
6552
|
onClick: () => {
|
|
@@ -5263,6 +6559,150 @@ function ControlBar({ onLeave }) {
|
|
|
5263
6559
|
] })
|
|
5264
6560
|
] });
|
|
5265
6561
|
}
|
|
6562
|
+
function useUnreadChat(open) {
|
|
6563
|
+
const snapshot = useMeetSnapshot();
|
|
6564
|
+
const chat = snapshot?.chat ?? [];
|
|
6565
|
+
const local = snapshot?.localIdentity ?? null;
|
|
6566
|
+
const [seen, setSeen] = useState3(0);
|
|
6567
|
+
useEffect3(() => {
|
|
6568
|
+
if (open) setSeen(chat.length);
|
|
6569
|
+
}, [open, chat.length]);
|
|
6570
|
+
const loaded = snapshot?.chatLoaded === true;
|
|
6571
|
+
const [baseline, setBaseline] = useState3(null);
|
|
6572
|
+
useEffect3(() => {
|
|
6573
|
+
if (loaded && baseline === null) setBaseline(chat.length);
|
|
6574
|
+
}, [loaded, baseline, chat.length]);
|
|
6575
|
+
if (open) return 0;
|
|
6576
|
+
const from = Math.max(seen, baseline ?? chat.length);
|
|
6577
|
+
return chat.slice(from).filter((message) => message.identity !== local).length;
|
|
6578
|
+
}
|
|
6579
|
+
function Popover({
|
|
6580
|
+
label,
|
|
6581
|
+
onClose,
|
|
6582
|
+
children,
|
|
6583
|
+
placement = "up"
|
|
6584
|
+
}) {
|
|
6585
|
+
const ref = useRef3(null);
|
|
6586
|
+
const [style, setStyle] = useState3({ visibility: "hidden" });
|
|
6587
|
+
useLayoutEffect(() => {
|
|
6588
|
+
const element = ref.current;
|
|
6589
|
+
const anchor = element?.parentElement ?? null;
|
|
6590
|
+
if (element === null || anchor === null) return;
|
|
6591
|
+
const place = () => {
|
|
6592
|
+
const rect = anchor.getBoundingClientRect();
|
|
6593
|
+
const width = element.offsetWidth;
|
|
6594
|
+
const height = element.offsetHeight;
|
|
6595
|
+
const gutter = 8;
|
|
6596
|
+
const left = Math.min(
|
|
6597
|
+
Math.max(gutter, rect.left + rect.width / 2 - width / 2),
|
|
6598
|
+
window.innerWidth - width - gutter
|
|
6599
|
+
);
|
|
6600
|
+
const fitsBelow = rect.bottom + gutter + height <= window.innerHeight;
|
|
6601
|
+
const fitsAbove = rect.top - gutter - height >= 0;
|
|
6602
|
+
const down = placement === "down" ? fitsBelow || !fitsAbove : !fitsAbove && fitsBelow;
|
|
6603
|
+
setStyle(
|
|
6604
|
+
down ? { position: "fixed", left, top: rect.bottom + gutter } : { position: "fixed", left, bottom: window.innerHeight - rect.top + gutter }
|
|
6605
|
+
);
|
|
6606
|
+
};
|
|
6607
|
+
place();
|
|
6608
|
+
window.addEventListener("resize", place);
|
|
6609
|
+
return () => window.removeEventListener("resize", place);
|
|
6610
|
+
}, [placement]);
|
|
6611
|
+
useEffect3(() => {
|
|
6612
|
+
const onKey = (event) => {
|
|
6613
|
+
if (event.key === "Escape") onClose();
|
|
6614
|
+
};
|
|
6615
|
+
const onPointer = (event) => {
|
|
6616
|
+
const anchor = ref.current?.parentElement ?? null;
|
|
6617
|
+
if (anchor !== null && !anchor.contains(event.target)) onClose();
|
|
6618
|
+
};
|
|
6619
|
+
window.addEventListener("keydown", onKey);
|
|
6620
|
+
window.addEventListener("pointerdown", onPointer);
|
|
6621
|
+
return () => {
|
|
6622
|
+
window.removeEventListener("keydown", onKey);
|
|
6623
|
+
window.removeEventListener("pointerdown", onPointer);
|
|
6624
|
+
};
|
|
6625
|
+
}, [onClose]);
|
|
6626
|
+
return /* @__PURE__ */ jsx3("div", { ref, className: "mx-meet__popover", style, role: "dialog", "aria-label": label, children });
|
|
6627
|
+
}
|
|
6628
|
+
function MenuItem({
|
|
6629
|
+
onSelect,
|
|
6630
|
+
danger = false,
|
|
6631
|
+
children
|
|
6632
|
+
}) {
|
|
6633
|
+
return /* @__PURE__ */ jsx3(
|
|
6634
|
+
"button",
|
|
6635
|
+
{
|
|
6636
|
+
type: "button",
|
|
6637
|
+
role: "menuitem",
|
|
6638
|
+
className: danger ? "mx-meet__menu-item mx-meet__menu-item--danger" : "mx-meet__menu-item",
|
|
6639
|
+
onClick: onSelect,
|
|
6640
|
+
children
|
|
6641
|
+
}
|
|
6642
|
+
);
|
|
6643
|
+
}
|
|
6644
|
+
function Toast({ text: text3, failed, onDone }) {
|
|
6645
|
+
useEffect3(() => {
|
|
6646
|
+
const timer = setTimeout(onDone, failed ? 7e3 : 4e3);
|
|
6647
|
+
return () => clearTimeout(timer);
|
|
6648
|
+
}, [text3, failed, onDone]);
|
|
6649
|
+
return /* @__PURE__ */ jsxs2("div", { className: failed ? "mx-meet__toast mx-meet__toast--failed" : "mx-meet__toast", role: "status", children: [
|
|
6650
|
+
/* @__PURE__ */ jsx3("span", { children: text3 }),
|
|
6651
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: onDone, children: /* @__PURE__ */ jsx3(CloseIcon, { width: 14, height: 14 }) })
|
|
6652
|
+
] });
|
|
6653
|
+
}
|
|
6654
|
+
function MeetingOverlays() {
|
|
6655
|
+
const reactions = useReactions();
|
|
6656
|
+
const { notice, dismiss } = useModerationNotice();
|
|
6657
|
+
const room = useRoom();
|
|
6658
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
6659
|
+
/* @__PURE__ */ jsx3(ReactionOverlay, { reactions }),
|
|
6660
|
+
notice !== null && notice.kind !== "removed" ? /* @__PURE__ */ jsxs2("div", { className: "mx-meet__notice-bar", role: "alert", children: [
|
|
6661
|
+
/* @__PURE__ */ jsx3("span", { children: notice.text }),
|
|
6662
|
+
notice.kind === "ask-unmute" && room.local?.micEnabled !== true ? /* @__PURE__ */ jsx3(
|
|
6663
|
+
"button",
|
|
6664
|
+
{
|
|
6665
|
+
type: "button",
|
|
6666
|
+
className: "mx-meet__primary mx-meet__primary--small",
|
|
6667
|
+
onClick: () => {
|
|
6668
|
+
dismiss();
|
|
6669
|
+
void room.toggleMicrophone();
|
|
6670
|
+
},
|
|
6671
|
+
children: "Unmute"
|
|
6672
|
+
}
|
|
6673
|
+
) : null,
|
|
6674
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismiss, children: /* @__PURE__ */ jsx3(CloseIcon, { width: 14, height: 14 }) })
|
|
6675
|
+
] }) : null
|
|
6676
|
+
] });
|
|
6677
|
+
}
|
|
6678
|
+
function ShortcutsSheet({ onClose }) {
|
|
6679
|
+
useEffect3(() => {
|
|
6680
|
+
const onKey = (event) => {
|
|
6681
|
+
if (event.key === "Escape") onClose();
|
|
6682
|
+
};
|
|
6683
|
+
window.addEventListener("keydown", onKey);
|
|
6684
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
6685
|
+
}, [onClose]);
|
|
6686
|
+
return /* @__PURE__ */ jsx3("div", { className: "mx-meet__sheet-backdrop", onClick: onClose, children: /* @__PURE__ */ jsxs2(
|
|
6687
|
+
"div",
|
|
6688
|
+
{
|
|
6689
|
+
className: "mx-meet__sheet",
|
|
6690
|
+
role: "dialog",
|
|
6691
|
+
"aria-label": "Keyboard shortcuts",
|
|
6692
|
+
onClick: (event) => event.stopPropagation(),
|
|
6693
|
+
children: [
|
|
6694
|
+
/* @__PURE__ */ jsxs2("header", { className: "mx-meet__panel-head", children: [
|
|
6695
|
+
/* @__PURE__ */ jsx3("h2", { className: "mx-meet__panel-title", children: "Keyboard shortcuts" }),
|
|
6696
|
+
/* @__PURE__ */ jsx3(ControlButton, { label: "Close", onClick: onClose, children: /* @__PURE__ */ jsx3(CloseIcon, { width: 16, height: 16 }) })
|
|
6697
|
+
] }),
|
|
6698
|
+
/* @__PURE__ */ jsx3("dl", { className: "mx-meet__shortcuts", children: MEET_SHORTCUTS.map((shortcut) => /* @__PURE__ */ jsxs2("div", { className: "mx-meet__shortcut", children: [
|
|
6699
|
+
/* @__PURE__ */ jsx3("dt", { children: shortcut.label }),
|
|
6700
|
+
/* @__PURE__ */ jsx3("dd", { children: /* @__PURE__ */ jsx3("kbd", { className: "mx-meet__kbd", children: shortcut.keys }) })
|
|
6701
|
+
] }, shortcut.id)) })
|
|
6702
|
+
]
|
|
6703
|
+
}
|
|
6704
|
+
) });
|
|
6705
|
+
}
|
|
5266
6706
|
function AiControl({ active, onToggle }) {
|
|
5267
6707
|
const snapshot = useMeetSnapshot();
|
|
5268
6708
|
const meeting = snapshot?.meeting ?? null;
|
|
@@ -5282,117 +6722,640 @@ function PanelShell({
|
|
|
5282
6722
|
/* @__PURE__ */ jsx3("div", { className: "mx-meet__panel-body", children })
|
|
5283
6723
|
] });
|
|
5284
6724
|
}
|
|
5285
|
-
function ChatPanel({
|
|
6725
|
+
function ChatPanel({
|
|
6726
|
+
onClose,
|
|
6727
|
+
to = null,
|
|
6728
|
+
onTo
|
|
6729
|
+
}) {
|
|
5286
6730
|
const snapshot = useMeetSnapshot();
|
|
5287
6731
|
const room = useRoom();
|
|
5288
6732
|
const [draft, setDraft] = useState3("");
|
|
6733
|
+
const [recipient, setRecipient] = useState3(to);
|
|
6734
|
+
const listRef = useRef3(null);
|
|
5289
6735
|
const messages = snapshot?.chat ?? [];
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
6736
|
+
const local = snapshot?.localIdentity ?? null;
|
|
6737
|
+
const people = (snapshot?.participants ?? []).filter(
|
|
6738
|
+
(participant) => !participant.isLocal && !participant.isAgent
|
|
6739
|
+
);
|
|
6740
|
+
const recipientName = recipient === null ? null : people.find((p) => p.identity === recipient)?.displayName ?? null;
|
|
6741
|
+
useEffect3(() => setRecipient(to), [to]);
|
|
6742
|
+
useEffect3(() => {
|
|
6743
|
+
const list = listRef.current;
|
|
6744
|
+
if (list !== null) list.scrollTop = list.scrollHeight;
|
|
6745
|
+
}, [messages.length]);
|
|
6746
|
+
const choose = (next) => {
|
|
6747
|
+
setRecipient(next);
|
|
6748
|
+
onTo?.(next);
|
|
6749
|
+
};
|
|
6750
|
+
return /* @__PURE__ */ jsxs2(PanelShell, { title: "Chat", onClose, children: [
|
|
6751
|
+
/* @__PURE__ */ jsxs2("ul", { className: "mx-meet__chat", ref: listRef, children: [
|
|
6752
|
+
snapshot?.chatLoaded !== true && messages.length === 0 ? /* @__PURE__ */ jsx3("li", { className: "mx-meet__panel-note", children: "Reading earlier messages\u2026" }) : messages.length === 0 ? /* @__PURE__ */ jsx3("li", { className: "mx-meet__panel-note", children: "No messages yet. Messages to everyone stay with the meeting, so people who join later see them." }) : null,
|
|
6753
|
+
messages.map((message) => /* @__PURE__ */ jsx3(ChatLine, { message, local, people }, message.id))
|
|
6754
|
+
] }),
|
|
5296
6755
|
/* @__PURE__ */ jsxs2(
|
|
5297
6756
|
"form",
|
|
5298
6757
|
{
|
|
5299
|
-
className: "mx-meet__composer",
|
|
6758
|
+
className: "mx-meet__composer mx-meet__composer--chat",
|
|
5300
6759
|
onSubmit: (event) => {
|
|
5301
6760
|
event.preventDefault();
|
|
5302
6761
|
const text3 = draft;
|
|
6762
|
+
if (text3.trim().length === 0) return;
|
|
5303
6763
|
setDraft("");
|
|
5304
|
-
void room.sendChat(text3);
|
|
6764
|
+
if (recipient === null) void room.sendChat(text3).catch(() => void 0);
|
|
6765
|
+
else void room.sendPrivateChat(recipient, text3);
|
|
5305
6766
|
},
|
|
5306
6767
|
children: [
|
|
5307
|
-
/* @__PURE__ */
|
|
5308
|
-
"
|
|
6768
|
+
people.length > 0 ? /* @__PURE__ */ jsxs2(
|
|
6769
|
+
"select",
|
|
5309
6770
|
{
|
|
5310
|
-
className: "mx-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
6771
|
+
className: "mx-meet__select mx-meet__select--compact",
|
|
6772
|
+
"aria-label": "Send to",
|
|
6773
|
+
value: recipient ?? "",
|
|
6774
|
+
onChange: (event) => choose(event.target.value === "" ? null : event.target.value),
|
|
6775
|
+
children: [
|
|
6776
|
+
/* @__PURE__ */ jsx3("option", { value: "", children: "Everyone" }),
|
|
6777
|
+
people.map((participant) => /* @__PURE__ */ jsxs2("option", { value: participant.identity, children: [
|
|
6778
|
+
participant.displayName,
|
|
6779
|
+
" (private)"
|
|
6780
|
+
] }, participant.identity))
|
|
6781
|
+
]
|
|
5315
6782
|
}
|
|
5316
|
-
),
|
|
5317
|
-
/* @__PURE__ */
|
|
6783
|
+
) : null,
|
|
6784
|
+
/* @__PURE__ */ jsxs2("div", { className: "mx-meet__composer-row", children: [
|
|
6785
|
+
/* @__PURE__ */ jsx3(
|
|
6786
|
+
"input",
|
|
6787
|
+
{
|
|
6788
|
+
className: "mx-meet__input",
|
|
6789
|
+
value: draft,
|
|
6790
|
+
onChange: (event) => setDraft(event.target.value),
|
|
6791
|
+
placeholder: recipientName === null ? "Message everyone" : `Message ${recipientName} privately`,
|
|
6792
|
+
"aria-label": recipientName === null ? "Message everyone" : `Message ${recipientName} privately`,
|
|
6793
|
+
maxLength: 4e3
|
|
6794
|
+
}
|
|
6795
|
+
),
|
|
6796
|
+
/* @__PURE__ */ jsx3("button", { type: "submit", className: "mx-meet__send", disabled: draft.trim().length === 0, children: "Send" })
|
|
6797
|
+
] }),
|
|
6798
|
+
recipientName !== null ? /* @__PURE__ */ jsxs2("p", { className: "mx-meet__panel-note", children: [
|
|
6799
|
+
"Only ",
|
|
6800
|
+
recipientName,
|
|
6801
|
+
" sees this. Private messages are not saved."
|
|
6802
|
+
] }) : null
|
|
5318
6803
|
]
|
|
5319
6804
|
}
|
|
5320
6805
|
)
|
|
5321
6806
|
] });
|
|
5322
6807
|
}
|
|
5323
|
-
function
|
|
6808
|
+
function ChatLine({
|
|
6809
|
+
message,
|
|
6810
|
+
local,
|
|
6811
|
+
people
|
|
6812
|
+
}) {
|
|
6813
|
+
const mine = message.identity === local;
|
|
6814
|
+
const privateTo = message.to ?? null;
|
|
6815
|
+
const counterpart = privateTo === null ? null : mine ? people.find((p) => p.identity === privateTo)?.displayName ?? "them" : "you";
|
|
6816
|
+
return /* @__PURE__ */ jsxs2(
|
|
6817
|
+
"li",
|
|
6818
|
+
{
|
|
6819
|
+
className: [
|
|
6820
|
+
"mx-meet__chat-line",
|
|
6821
|
+
privateTo !== null ? "mx-meet__chat-line--private" : "",
|
|
6822
|
+
message.failed !== void 0 ? "mx-meet__chat-line--failed" : ""
|
|
6823
|
+
].filter(Boolean).join(" "),
|
|
6824
|
+
children: [
|
|
6825
|
+
/* @__PURE__ */ jsxs2("span", { className: "mx-meet__chat-meta", children: [
|
|
6826
|
+
/* @__PURE__ */ jsx3("strong", { children: mine ? "You" : message.displayName }),
|
|
6827
|
+
counterpart !== null ? /* @__PURE__ */ jsxs2("span", { className: "mx-meet__chat-private", children: [
|
|
6828
|
+
"to ",
|
|
6829
|
+
counterpart,
|
|
6830
|
+
" \xB7 private"
|
|
6831
|
+
] }) : null,
|
|
6832
|
+
/* @__PURE__ */ jsx3("time", { dateTime: message.at, children: clockOf(message.at) })
|
|
6833
|
+
] }),
|
|
6834
|
+
/* @__PURE__ */ jsx3("span", { className: "mx-meet__chat-text", children: message.text }),
|
|
6835
|
+
message.failed !== void 0 ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__chat-failed", children: message.failed }) : privateTo === null && message.saved === false ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__chat-pending", children: message.id.startsWith("pending-") ? "Sending\u2026" : "Not saved \u2014 people who join later will not see this" }) : null
|
|
6836
|
+
]
|
|
6837
|
+
}
|
|
6838
|
+
);
|
|
6839
|
+
}
|
|
6840
|
+
function PeoplePanel({
|
|
6841
|
+
onClose,
|
|
6842
|
+
onMessage
|
|
6843
|
+
}) {
|
|
5324
6844
|
const snapshot = useMeetSnapshot();
|
|
5325
6845
|
const controls = useHostControls();
|
|
6846
|
+
const queue = useHandQueue();
|
|
6847
|
+
const [query, setQuery] = useState3("");
|
|
6848
|
+
const [menuFor, setMenuFor] = useState3(null);
|
|
5326
6849
|
const myRole = useRole();
|
|
5327
6850
|
const local = snapshot?.localIdentity ?? null;
|
|
5328
|
-
|
|
6851
|
+
const everyone = snapshot?.participants ?? [];
|
|
6852
|
+
const needle = query.trim().toLowerCase();
|
|
6853
|
+
const shown = needle.length === 0 ? everyone : everyone.filter((participant) => participant.displayName.toLowerCase().includes(needle));
|
|
6854
|
+
return /* @__PURE__ */ jsxs2(PanelShell, { title: `People (${everyone.length})`, onClose, children: [
|
|
5329
6855
|
/* @__PURE__ */ jsx3(LobbyPanel, {}),
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
6856
|
+
queue.length > 0 ? /* @__PURE__ */ jsxs2("section", { className: "mx-meet__section", "aria-label": "Raised hands", children: [
|
|
6857
|
+
/* @__PURE__ */ jsxs2("header", { className: "mx-meet__section-head", children: [
|
|
6858
|
+
/* @__PURE__ */ jsxs2("h3", { className: "mx-meet__section-title", children: [
|
|
6859
|
+
"Raised hands \xB7 ",
|
|
6860
|
+
queue.length
|
|
6861
|
+
] }),
|
|
6862
|
+
controls.isHost ? /* @__PURE__ */ jsx3(
|
|
6863
|
+
"button",
|
|
5337
6864
|
{
|
|
5338
|
-
|
|
6865
|
+
type: "button",
|
|
6866
|
+
className: "mx-meet__link",
|
|
6867
|
+
onClick: () => void controls.run({ action: "lower_all_hands" }),
|
|
6868
|
+
children: "Lower all"
|
|
5339
6869
|
}
|
|
5340
|
-
)
|
|
5341
|
-
isNoteTaker(participant) ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null
|
|
6870
|
+
) : null
|
|
5342
6871
|
] }),
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
6872
|
+
/* @__PURE__ */ jsx3("ol", { className: "mx-meet__queue", children: queue.map((participant) => /* @__PURE__ */ jsxs2("li", { className: "mx-meet__person", children: [
|
|
6873
|
+
/* @__PURE__ */ jsx3(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: 28 }),
|
|
6874
|
+
/* @__PURE__ */ jsxs2("span", { className: "mx-meet__person-name", children: [
|
|
6875
|
+
participant.displayName,
|
|
6876
|
+
participant.identity === local ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__you", children: " (you)" }) : null
|
|
6877
|
+
] }),
|
|
6878
|
+
controls.isHost || participant.identity === local ? /* @__PURE__ */ jsx3(
|
|
6879
|
+
"button",
|
|
6880
|
+
{
|
|
6881
|
+
type: "button",
|
|
6882
|
+
className: "mx-meet__link",
|
|
6883
|
+
onClick: () => void controls.run({ action: "lower_hand", identity: participant.identity }),
|
|
6884
|
+
children: "Lower"
|
|
6885
|
+
}
|
|
6886
|
+
) : null
|
|
6887
|
+
] }, participant.identity)) })
|
|
6888
|
+
] }) : null,
|
|
6889
|
+
controls.isHost && everyone.length > 1 ? /* @__PURE__ */ jsxs2("div", { className: "mx-meet__bulk", children: [
|
|
6890
|
+
/* @__PURE__ */ jsxs2("button", { type: "button", className: "mx-meet__chip", onClick: () => void controls.run({ action: "mute_all" }), children: [
|
|
6891
|
+
/* @__PURE__ */ jsx3(MicOffIcon, { width: 14, height: 14 }),
|
|
6892
|
+
" Mute all"
|
|
6893
|
+
] }),
|
|
6894
|
+
/* @__PURE__ */ jsxs2(
|
|
6895
|
+
"button",
|
|
6896
|
+
{
|
|
6897
|
+
type: "button",
|
|
6898
|
+
className: "mx-meet__chip",
|
|
6899
|
+
onClick: () => void controls.run({ action: "ask_all_unmute" }),
|
|
6900
|
+
children: [
|
|
6901
|
+
/* @__PURE__ */ jsx3(MicIcon, { width: 14, height: 14 }),
|
|
6902
|
+
" Ask all to unmute"
|
|
6903
|
+
]
|
|
6904
|
+
}
|
|
6905
|
+
)
|
|
6906
|
+
] }) : null,
|
|
6907
|
+
everyone.length > 4 ? /* @__PURE__ */ jsxs2("label", { className: "mx-meet__search", children: [
|
|
6908
|
+
/* @__PURE__ */ jsx3(SearchIcon, { width: 16, height: 16 }),
|
|
6909
|
+
/* @__PURE__ */ jsx3(
|
|
6910
|
+
"input",
|
|
6911
|
+
{
|
|
6912
|
+
className: "mx-meet__input mx-meet__input--bare",
|
|
6913
|
+
value: query,
|
|
6914
|
+
onChange: (event) => setQuery(event.target.value),
|
|
6915
|
+
placeholder: "Search people",
|
|
6916
|
+
"aria-label": "Search people"
|
|
6917
|
+
}
|
|
6918
|
+
)
|
|
6919
|
+
] }) : null,
|
|
6920
|
+
controls.notice !== null ? /* @__PURE__ */ jsx3("p", { className: controls.notice.failed ? "mx-meet__error" : "mx-meet__notice", role: "status", children: controls.notice.text }) : null,
|
|
6921
|
+
/* @__PURE__ */ jsxs2("ul", { className: "mx-meet__people", children: [
|
|
6922
|
+
shown.map((participant) => /* @__PURE__ */ jsxs2("li", { className: "mx-meet__person", children: [
|
|
6923
|
+
/* @__PURE__ */ jsx3(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: 32 }),
|
|
6924
|
+
/* @__PURE__ */ jsxs2("span", { className: "mx-meet__person-name", children: [
|
|
6925
|
+
participant.displayName,
|
|
6926
|
+
participant.identity === local ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__you", children: " (you)" }) : null,
|
|
6927
|
+
/* @__PURE__ */ jsx3(RoleBadge, { role: participant.identity === local ? myRole : participant.role }),
|
|
6928
|
+
isNoteTaker(participant) ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null,
|
|
6929
|
+
snapshot?.spotlight === participant.identity ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__badge", children: "Spotlight" }) : null
|
|
6930
|
+
] }),
|
|
6931
|
+
participant.handRaised ? /* @__PURE__ */ jsx3(HandIcon, { width: 14, height: 14 }) : null,
|
|
6932
|
+
!participant.micEnabled && !isNoteTaker(participant) ? /* @__PURE__ */ jsx3(MicOffIcon, { width: 14, height: 14 }) : null,
|
|
6933
|
+
/* @__PURE__ */ jsx3(QualityBars, { quality: participant.quality }),
|
|
6934
|
+
/* @__PURE__ */ jsxs2("span", { className: "mx-meet__anchor", children: [
|
|
6935
|
+
/* @__PURE__ */ jsx3(
|
|
6936
|
+
ControlButton,
|
|
6937
|
+
{
|
|
6938
|
+
label: `More for ${participant.displayName}`,
|
|
6939
|
+
active: menuFor === participant.identity,
|
|
6940
|
+
onClick: () => setMenuFor(menuFor === participant.identity ? null : participant.identity),
|
|
6941
|
+
children: /* @__PURE__ */ jsx3(MoreIcon, { width: 16, height: 16 })
|
|
6942
|
+
}
|
|
6943
|
+
),
|
|
6944
|
+
menuFor === participant.identity ? /* @__PURE__ */ jsx3(
|
|
6945
|
+
PersonMenu,
|
|
6946
|
+
{
|
|
6947
|
+
participant,
|
|
6948
|
+
placement: "down",
|
|
6949
|
+
onClose: () => setMenuFor(null),
|
|
6950
|
+
...onMessage !== void 0 ? { onMessage } : {}
|
|
6951
|
+
}
|
|
6952
|
+
) : null
|
|
6953
|
+
] })
|
|
6954
|
+
] }, participant.identity)),
|
|
6955
|
+
shown.length === 0 ? /* @__PURE__ */ jsxs2("li", { className: "mx-meet__panel-note", children: [
|
|
6956
|
+
"Nobody matches \u201C",
|
|
6957
|
+
query,
|
|
6958
|
+
"\u201D."
|
|
6959
|
+
] }) : null
|
|
6960
|
+
] }),
|
|
6961
|
+
snapshot?.meeting !== null && snapshot?.meeting !== void 0 ? /* @__PURE__ */ jsx3(GuestRecordAccessControl, { meeting: snapshot.meeting }) : null
|
|
5347
6962
|
] });
|
|
5348
6963
|
}
|
|
5349
|
-
function
|
|
6964
|
+
function PersonMenu({
|
|
5350
6965
|
participant,
|
|
5351
|
-
|
|
6966
|
+
onClose,
|
|
6967
|
+
onMessage,
|
|
6968
|
+
placement = "down"
|
|
5352
6969
|
}) {
|
|
5353
|
-
const
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
6970
|
+
const host = useMeetHost();
|
|
6971
|
+
const snapshot = useMeetSnapshot();
|
|
6972
|
+
const controls = useHostControls();
|
|
6973
|
+
const [mode, setMode] = useState3("menu");
|
|
6974
|
+
const [text3, setText] = useState3("");
|
|
6975
|
+
const self = participant.identity === snapshot?.localIdentity;
|
|
6976
|
+
const moderate = controls.canModerate(participant);
|
|
6977
|
+
const pinned = snapshot?.pinned === participant.identity;
|
|
6978
|
+
const spotlit = snapshot?.spotlight === participant.identity;
|
|
6979
|
+
const hasAccount = participant.userId !== null;
|
|
6980
|
+
const act = (request) => {
|
|
6981
|
+
void controls.run(request).then((result) => {
|
|
6982
|
+
if (result !== null) onClose();
|
|
6983
|
+
});
|
|
6984
|
+
};
|
|
6985
|
+
if (mode === "rename" || mode === "report") {
|
|
6986
|
+
const renaming = mode === "rename";
|
|
6987
|
+
return /* @__PURE__ */ jsx3(Popover, { label: renaming ? "Rename" : "Report", onClose, placement, children: /* @__PURE__ */ jsxs2(
|
|
6988
|
+
"form",
|
|
6989
|
+
{
|
|
6990
|
+
className: "mx-meet__menu-form",
|
|
6991
|
+
onSubmit: (event) => {
|
|
6992
|
+
event.preventDefault();
|
|
6993
|
+
if (text3.trim().length === 0) return;
|
|
6994
|
+
act(
|
|
6995
|
+
renaming ? { action: "rename", identity: participant.identity, name: text3.trim() } : { action: "report", identity: participant.identity, reason: text3.trim() }
|
|
6996
|
+
);
|
|
6997
|
+
},
|
|
6998
|
+
children: [
|
|
6999
|
+
/* @__PURE__ */ jsxs2("label", { className: "mx-meet__field", children: [
|
|
7000
|
+
/* @__PURE__ */ jsx3("span", { className: "mx-meet__field-label", children: renaming ? "Name shown in this meeting" : `What did ${participant.displayName} do?` }),
|
|
7001
|
+
renaming ? /* @__PURE__ */ jsx3(
|
|
7002
|
+
"input",
|
|
7003
|
+
{
|
|
7004
|
+
className: "mx-meet__input",
|
|
7005
|
+
autoFocus: true,
|
|
7006
|
+
maxLength: 80,
|
|
7007
|
+
value: text3,
|
|
7008
|
+
onChange: (event) => setText(event.target.value)
|
|
7009
|
+
}
|
|
7010
|
+
) : /* @__PURE__ */ jsx3(
|
|
7011
|
+
"textarea",
|
|
7012
|
+
{
|
|
7013
|
+
className: "mx-meet__input",
|
|
7014
|
+
autoFocus: true,
|
|
7015
|
+
rows: 3,
|
|
7016
|
+
maxLength: 1e3,
|
|
7017
|
+
value: text3,
|
|
7018
|
+
onChange: (event) => setText(event.target.value)
|
|
7019
|
+
}
|
|
7020
|
+
)
|
|
7021
|
+
] }),
|
|
7022
|
+
controls.notice?.failed === true ? /* @__PURE__ */ jsx3("p", { className: "mx-meet__error", children: controls.notice.text }) : null,
|
|
7023
|
+
/* @__PURE__ */ jsxs2("div", { className: "mx-meet__menu-actions", children: [
|
|
7024
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Back" }),
|
|
7025
|
+
/* @__PURE__ */ jsx3("button", { type: "submit", className: "mx-meet__send", disabled: text3.trim().length === 0 || controls.busy, children: renaming ? "Rename" : "Report" })
|
|
7026
|
+
] })
|
|
7027
|
+
]
|
|
7028
|
+
}
|
|
7029
|
+
) });
|
|
7030
|
+
}
|
|
7031
|
+
if (mode === "remove" || mode === "transfer") {
|
|
7032
|
+
const removing = mode === "remove";
|
|
7033
|
+
return /* @__PURE__ */ jsx3(Popover, { label: removing ? "Remove" : "Make host", onClose, placement, children: /* @__PURE__ */ jsxs2("div", { className: "mx-meet__menu-form", children: [
|
|
7034
|
+
/* @__PURE__ */ jsx3("p", { className: "mx-meet__confirm-text", children: removing ? `Remove ${participant.displayName}? They leave immediately and cannot rejoin this meeting.` : `Make ${participant.displayName} the host? They get every host control and own this meeting. You stay on as a co-host.` }),
|
|
7035
|
+
/* @__PURE__ */ jsxs2("div", { className: "mx-meet__menu-actions", children: [
|
|
7036
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Cancel" }),
|
|
7037
|
+
/* @__PURE__ */ jsx3(
|
|
7038
|
+
"button",
|
|
7039
|
+
{
|
|
7040
|
+
type: "button",
|
|
7041
|
+
className: removing ? "mx-meet__send mx-meet__send--danger" : "mx-meet__send",
|
|
7042
|
+
disabled: controls.busy,
|
|
7043
|
+
onClick: () => act({ action: removing ? "remove" : "transfer_host", identity: participant.identity }),
|
|
7044
|
+
children: removing ? "Remove" : "Make host"
|
|
7045
|
+
}
|
|
7046
|
+
)
|
|
7047
|
+
] })
|
|
7048
|
+
] }) });
|
|
7049
|
+
}
|
|
7050
|
+
return /* @__PURE__ */ jsx3(Popover, { label: `Options for ${participant.displayName}`, onClose, placement, children: /* @__PURE__ */ jsxs2("div", { className: "mx-meet__menu", role: "menu", children: [
|
|
7051
|
+
/* @__PURE__ */ jsxs2(
|
|
7052
|
+
MenuItem,
|
|
7053
|
+
{
|
|
7054
|
+
onSelect: () => {
|
|
7055
|
+
host?.engine.pin(pinned ? null : participant.identity);
|
|
7056
|
+
onClose();
|
|
7057
|
+
},
|
|
7058
|
+
children: [
|
|
7059
|
+
/* @__PURE__ */ jsx3(PinIcon, { width: 16, height: 16 }),
|
|
7060
|
+
" ",
|
|
7061
|
+
pinned ? "Unpin" : "Pin for me"
|
|
7062
|
+
]
|
|
7063
|
+
}
|
|
7064
|
+
),
|
|
7065
|
+
!self && !participant.isAgent && onMessage !== void 0 ? /* @__PURE__ */ jsxs2(
|
|
7066
|
+
MenuItem,
|
|
7067
|
+
{
|
|
7068
|
+
onSelect: () => {
|
|
7069
|
+
onMessage(participant.identity);
|
|
7070
|
+
onClose();
|
|
7071
|
+
},
|
|
7072
|
+
children: [
|
|
7073
|
+
/* @__PURE__ */ jsx3(ChatIcon, { width: 16, height: 16 }),
|
|
7074
|
+
" Message privately"
|
|
7075
|
+
]
|
|
7076
|
+
}
|
|
7077
|
+
) : null,
|
|
7078
|
+
controls.isHost ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => act({ action: "spotlight", identity: spotlit ? null : participant.identity }), children: [
|
|
7079
|
+
/* @__PURE__ */ jsx3(SpotlightIcon, { width: 16, height: 16 }),
|
|
7080
|
+
" ",
|
|
7081
|
+
spotlit ? "Remove spotlight" : "Spotlight for everyone"
|
|
7082
|
+
] }) : null,
|
|
7083
|
+
moderate ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
7084
|
+
/* @__PURE__ */ jsx3("div", { className: "mx-meet__menu-sep", role: "separator" }),
|
|
7085
|
+
participant.micEnabled ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => act({ action: "mute", identity: participant.identity }), children: [
|
|
7086
|
+
/* @__PURE__ */ jsx3(MicOffIcon, { width: 16, height: 16 }),
|
|
7087
|
+
" Mute"
|
|
7088
|
+
] }) : !participant.isAgent ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => act({ action: "ask_unmute", identity: participant.identity }), children: [
|
|
7089
|
+
/* @__PURE__ */ jsx3(MicIcon, { width: 16, height: 16 }),
|
|
7090
|
+
" Ask to unmute"
|
|
7091
|
+
] }) : null,
|
|
7092
|
+
participant.cameraEnabled ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => act({ action: "stop_video", identity: participant.identity }), children: [
|
|
7093
|
+
/* @__PURE__ */ jsx3(CameraOffIcon, { width: 16, height: 16 }),
|
|
7094
|
+
" Stop video"
|
|
7095
|
+
] }) : null,
|
|
7096
|
+
participant.screenSharing ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => act({ action: "stop_screen", identity: participant.identity }), children: [
|
|
7097
|
+
/* @__PURE__ */ jsx3(ScreenShareIcon, { width: 16, height: 16 }),
|
|
7098
|
+
" Stop screen share"
|
|
7099
|
+
] }) : null,
|
|
7100
|
+
participant.handRaised ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => act({ action: "lower_hand", identity: participant.identity }), children: [
|
|
7101
|
+
/* @__PURE__ */ jsx3(HandIcon, { width: 16, height: 16 }),
|
|
7102
|
+
" Lower hand"
|
|
7103
|
+
] }) : null,
|
|
7104
|
+
hasAccount && !participant.isAgent && participant.role !== "cohost" ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => act({ action: "make_cohost", identity: participant.identity }), children: [
|
|
7105
|
+
/* @__PURE__ */ jsx3(PeopleIcon, { width: 16, height: 16 }),
|
|
7106
|
+
" Make co-host"
|
|
7107
|
+
] }) : null,
|
|
7108
|
+
!participant.isAgent ? /* @__PURE__ */ jsxs2(
|
|
7109
|
+
MenuItem,
|
|
5363
7110
|
{
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
setConfirming(false);
|
|
5368
|
-
void controls.remove(participant);
|
|
7111
|
+
onSelect: () => {
|
|
7112
|
+
setText(participant.displayName);
|
|
7113
|
+
setMode("rename");
|
|
5369
7114
|
},
|
|
5370
|
-
children:
|
|
7115
|
+
children: [
|
|
7116
|
+
/* @__PURE__ */ jsx3(PencilIcon, { width: 16, height: 16 }),
|
|
7117
|
+
" Rename"
|
|
7118
|
+
]
|
|
5371
7119
|
}
|
|
5372
|
-
)
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
7120
|
+
) : null
|
|
7121
|
+
] }) : null,
|
|
7122
|
+
controls.isOwner && participant.role === "cohost" ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => act({ action: "revoke_cohost", identity: participant.identity }), children: [
|
|
7123
|
+
/* @__PURE__ */ jsx3(PeopleIcon, { width: 16, height: 16 }),
|
|
7124
|
+
" Remove co-host"
|
|
7125
|
+
] }) : null,
|
|
7126
|
+
controls.isOwner && !self && hasAccount && !participant.isAgent ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => setMode("transfer"), children: [
|
|
7127
|
+
/* @__PURE__ */ jsx3(SpotlightIcon, { width: 16, height: 16 }),
|
|
7128
|
+
" Make host"
|
|
7129
|
+
] }) : null,
|
|
7130
|
+
self ? /* @__PURE__ */ jsxs2(
|
|
7131
|
+
MenuItem,
|
|
5379
7132
|
{
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
7133
|
+
onSelect: () => {
|
|
7134
|
+
setText(participant.displayName);
|
|
7135
|
+
setMode("rename");
|
|
7136
|
+
},
|
|
7137
|
+
children: [
|
|
7138
|
+
/* @__PURE__ */ jsx3(PencilIcon, { width: 16, height: 16 }),
|
|
7139
|
+
" Rename yourself"
|
|
7140
|
+
]
|
|
7141
|
+
}
|
|
7142
|
+
) : null,
|
|
7143
|
+
!self && !participant.isAgent ? /* @__PURE__ */ jsxs2(
|
|
7144
|
+
MenuItem,
|
|
7145
|
+
{
|
|
7146
|
+
onSelect: () => {
|
|
7147
|
+
setText("");
|
|
7148
|
+
setMode("report");
|
|
7149
|
+
},
|
|
7150
|
+
children: [
|
|
7151
|
+
/* @__PURE__ */ jsx3(FlagIcon, { width: 16, height: 16 }),
|
|
7152
|
+
" Report"
|
|
7153
|
+
]
|
|
5383
7154
|
}
|
|
5384
7155
|
) : null,
|
|
7156
|
+
moderate && !participant.isAgent ? /* @__PURE__ */ jsxs2(MenuItem, { danger: true, onSelect: () => setMode("remove"), children: [
|
|
7157
|
+
/* @__PURE__ */ jsx3(CloseIcon, { width: 16, height: 16 }),
|
|
7158
|
+
" Remove from meeting"
|
|
7159
|
+
] }) : null
|
|
7160
|
+
] }) });
|
|
7161
|
+
}
|
|
7162
|
+
function SettingsPanel({ onClose }) {
|
|
7163
|
+
const host = useRequiredMeetHost();
|
|
7164
|
+
return /* @__PURE__ */ jsxs2(PanelShell, { title: "Audio, video and effects", onClose, children: [
|
|
5385
7165
|
/* @__PURE__ */ jsx3(
|
|
5386
|
-
|
|
7166
|
+
DeviceSettings,
|
|
5387
7167
|
{
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
7168
|
+
onPick: (kind, deviceId2) => {
|
|
7169
|
+
host.devices.select(kind, deviceId2);
|
|
7170
|
+
void host.engine.switchDevice(kind, deviceId2);
|
|
7171
|
+
}
|
|
5392
7172
|
}
|
|
5393
|
-
)
|
|
7173
|
+
),
|
|
7174
|
+
/* @__PURE__ */ jsx3(EffectsSettings, {})
|
|
7175
|
+
] });
|
|
7176
|
+
}
|
|
7177
|
+
function DeviceSettings({
|
|
7178
|
+
onPick
|
|
7179
|
+
}) {
|
|
7180
|
+
const host = useRequiredMeetHost();
|
|
7181
|
+
const devices = useDeviceSnapshot();
|
|
7182
|
+
const microphone = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
|
|
7183
|
+
const speaker = devices?.selected.audiooutput ?? host.devices.resolve("audiooutput");
|
|
7184
|
+
const stream = useMicrophoneStream(microphone);
|
|
7185
|
+
const level = useAudioLevel(stream);
|
|
7186
|
+
const [testing, setTesting] = useState3(false);
|
|
7187
|
+
useEffect3(() => {
|
|
7188
|
+
void host.devices.refresh();
|
|
7189
|
+
}, [host]);
|
|
7190
|
+
return /* @__PURE__ */ jsxs2("section", { className: "mx-meet__section", "aria-label": "Devices", children: [
|
|
7191
|
+
/* @__PURE__ */ jsx3(DevicePicker, { kind: "audioinput", label: "Microphone", onPick }),
|
|
7192
|
+
/* @__PURE__ */ jsx3("div", { className: "mx-meet__meter", role: "meter", "aria-label": "Microphone level", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(level * 100), children: /* @__PURE__ */ jsx3("span", { style: { width: `${Math.min(100, Math.round(level * 160))}%` } }) }),
|
|
7193
|
+
/* @__PURE__ */ jsx3(DevicePicker, { kind: "audiooutput", label: "Speaker", onPick }),
|
|
7194
|
+
/* @__PURE__ */ jsxs2(
|
|
7195
|
+
"button",
|
|
7196
|
+
{
|
|
7197
|
+
type: "button",
|
|
7198
|
+
className: "mx-meet__chip",
|
|
7199
|
+
disabled: testing,
|
|
7200
|
+
onClick: () => {
|
|
7201
|
+
setTesting(true);
|
|
7202
|
+
void playTestSound(speaker).finally(() => setTesting(false));
|
|
7203
|
+
},
|
|
7204
|
+
children: [
|
|
7205
|
+
/* @__PURE__ */ jsx3(SpeakerIcon, { width: 14, height: 14 }),
|
|
7206
|
+
" ",
|
|
7207
|
+
testing ? "Playing\u2026" : "Test speaker"
|
|
7208
|
+
]
|
|
7209
|
+
}
|
|
7210
|
+
),
|
|
7211
|
+
/* @__PURE__ */ jsx3(DevicePicker, { kind: "videoinput", label: "Camera", onPick })
|
|
7212
|
+
] });
|
|
7213
|
+
}
|
|
7214
|
+
function EffectsSettings({ preview }) {
|
|
7215
|
+
const effects = useMediaEffects();
|
|
7216
|
+
const [error, setError] = useState3(null);
|
|
7217
|
+
const fileRef = useRef3(null);
|
|
7218
|
+
const supported = effects.backgroundsSupported || preview != null;
|
|
7219
|
+
const apply = (next) => {
|
|
7220
|
+
setError(null);
|
|
7221
|
+
void effects.set(next);
|
|
7222
|
+
if (preview != null) {
|
|
7223
|
+
const merged = { ...effects.effects, ...next };
|
|
7224
|
+
void preview.setEffect(toVideoEffect(merged)).catch(
|
|
7225
|
+
() => setError("This browser could not run the background effect.")
|
|
7226
|
+
);
|
|
7227
|
+
}
|
|
7228
|
+
};
|
|
7229
|
+
return /* @__PURE__ */ jsxs2("section", { className: "mx-meet__section", "aria-label": "Effects", children: [
|
|
7230
|
+
/* @__PURE__ */ jsxs2("label", { className: "mx-meet__toggle", children: [
|
|
7231
|
+
/* @__PURE__ */ jsx3(
|
|
7232
|
+
"input",
|
|
7233
|
+
{
|
|
7234
|
+
type: "checkbox",
|
|
7235
|
+
checked: effects.effects.noiseSuppression,
|
|
7236
|
+
onChange: (event) => apply({ noiseSuppression: event.target.checked })
|
|
7237
|
+
}
|
|
7238
|
+
),
|
|
7239
|
+
"Noise suppression",
|
|
7240
|
+
/* @__PURE__ */ jsx3("span", { className: "mx-meet__hint", children: "Filters keyboards, fans and background chatter." })
|
|
7241
|
+
] }),
|
|
7242
|
+
supported ? /* @__PURE__ */ jsxs2("div", { className: "mx-meet__field", children: [
|
|
7243
|
+
/* @__PURE__ */ jsx3("span", { className: "mx-meet__field-label", children: "Background" }),
|
|
7244
|
+
/* @__PURE__ */ jsx3("div", { className: "mx-meet__pills", role: "radiogroup", "aria-label": "Background", children: ["none", "blur", "image"].map((kind) => /* @__PURE__ */ jsx3(
|
|
7245
|
+
"button",
|
|
7246
|
+
{
|
|
7247
|
+
type: "button",
|
|
7248
|
+
role: "radio",
|
|
7249
|
+
"aria-checked": effects.effects.background === kind,
|
|
7250
|
+
className: effects.effects.background === kind ? "mx-meet__pill mx-meet__pill--on" : "mx-meet__pill",
|
|
7251
|
+
onClick: () => {
|
|
7252
|
+
if (kind === "image") {
|
|
7253
|
+
fileRef.current?.click();
|
|
7254
|
+
return;
|
|
7255
|
+
}
|
|
7256
|
+
apply({ background: kind });
|
|
7257
|
+
},
|
|
7258
|
+
children: kind === "none" ? "None" : kind === "blur" ? "Blur" : "Picture\u2026"
|
|
7259
|
+
},
|
|
7260
|
+
kind
|
|
7261
|
+
)) }),
|
|
7262
|
+
/* @__PURE__ */ jsx3(
|
|
7263
|
+
"input",
|
|
7264
|
+
{
|
|
7265
|
+
ref: fileRef,
|
|
7266
|
+
type: "file",
|
|
7267
|
+
accept: "image/*",
|
|
7268
|
+
hidden: true,
|
|
7269
|
+
onChange: (event) => {
|
|
7270
|
+
const file = event.target.files?.[0];
|
|
7271
|
+
event.target.value = "";
|
|
7272
|
+
if (file === void 0) return;
|
|
7273
|
+
void shrinkImage(file).then((url) => apply({ background: "image", backgroundImage: url })).catch(() => setError("That picture could not be read. Try a JPEG or PNG."));
|
|
7274
|
+
}
|
|
7275
|
+
}
|
|
7276
|
+
),
|
|
7277
|
+
error !== null ? /* @__PURE__ */ jsx3("p", { className: "mx-meet__error", children: error }) : null
|
|
7278
|
+
] }) : null
|
|
5394
7279
|
] });
|
|
5395
7280
|
}
|
|
7281
|
+
function toVideoEffect(effects) {
|
|
7282
|
+
if (effects.background === "blur") return { kind: "blur" };
|
|
7283
|
+
if (effects.background === "image" && effects.backgroundImage !== null) {
|
|
7284
|
+
return { kind: "image", imageUrl: effects.backgroundImage };
|
|
7285
|
+
}
|
|
7286
|
+
return { kind: "none" };
|
|
7287
|
+
}
|
|
7288
|
+
async function shrinkImage(file) {
|
|
7289
|
+
const url = URL.createObjectURL(file);
|
|
7290
|
+
try {
|
|
7291
|
+
const image = new Image();
|
|
7292
|
+
image.src = url;
|
|
7293
|
+
await image.decode();
|
|
7294
|
+
const scale = Math.min(1, 1280 / image.naturalWidth);
|
|
7295
|
+
const canvas = document.createElement("canvas");
|
|
7296
|
+
canvas.width = Math.round(image.naturalWidth * scale);
|
|
7297
|
+
canvas.height = Math.round(image.naturalHeight * scale);
|
|
7298
|
+
const context = canvas.getContext("2d");
|
|
7299
|
+
if (context === null) throw new Error("no 2d context");
|
|
7300
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
7301
|
+
return canvas.toDataURL("image/jpeg", 0.85);
|
|
7302
|
+
} finally {
|
|
7303
|
+
URL.revokeObjectURL(url);
|
|
7304
|
+
}
|
|
7305
|
+
}
|
|
7306
|
+
async function playTestSound(sinkId) {
|
|
7307
|
+
const Context = globalThis.AudioContext;
|
|
7308
|
+
if (Context === void 0) return;
|
|
7309
|
+
const context = new Context();
|
|
7310
|
+
const destination = context.createMediaStreamDestination();
|
|
7311
|
+
const gain = context.createGain();
|
|
7312
|
+
gain.gain.value = 0.2;
|
|
7313
|
+
gain.connect(destination);
|
|
7314
|
+
for (const [frequency, start] of [
|
|
7315
|
+
[660, 0],
|
|
7316
|
+
[880, 0.25]
|
|
7317
|
+
]) {
|
|
7318
|
+
const oscillator = context.createOscillator();
|
|
7319
|
+
oscillator.frequency.value = frequency;
|
|
7320
|
+
oscillator.connect(gain);
|
|
7321
|
+
oscillator.start(context.currentTime + start);
|
|
7322
|
+
oscillator.stop(context.currentTime + start + 0.22);
|
|
7323
|
+
}
|
|
7324
|
+
const audio = new Audio();
|
|
7325
|
+
audio.srcObject = destination.stream;
|
|
7326
|
+
const sinkable = audio;
|
|
7327
|
+
if (sinkId !== null && sinkId !== "default" && sinkable.setSinkId !== void 0) {
|
|
7328
|
+
await sinkable.setSinkId(sinkId).catch(() => void 0);
|
|
7329
|
+
}
|
|
7330
|
+
await audio.play().catch(() => void 0);
|
|
7331
|
+
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
7332
|
+
audio.pause();
|
|
7333
|
+
await context.close();
|
|
7334
|
+
}
|
|
7335
|
+
function useMicrophoneStream(deviceId2) {
|
|
7336
|
+
const [stream, setStream] = useState3(null);
|
|
7337
|
+
useEffect3(() => {
|
|
7338
|
+
if (deviceId2 === "off") return void 0;
|
|
7339
|
+
let live = true;
|
|
7340
|
+
let opened = null;
|
|
7341
|
+
const media = globalThis.navigator?.mediaDevices;
|
|
7342
|
+
if (media === void 0) return void 0;
|
|
7343
|
+
void media.getUserMedia({ audio: deviceId2 === null ? true : { deviceId: { exact: deviceId2 } } }).then((next) => {
|
|
7344
|
+
if (!live) {
|
|
7345
|
+
next.getTracks().forEach((track) => track.stop());
|
|
7346
|
+
return;
|
|
7347
|
+
}
|
|
7348
|
+
opened = next;
|
|
7349
|
+
setStream(next);
|
|
7350
|
+
}).catch(() => setStream(null));
|
|
7351
|
+
return () => {
|
|
7352
|
+
live = false;
|
|
7353
|
+
opened?.getTracks().forEach((track) => track.stop());
|
|
7354
|
+
setStream(null);
|
|
7355
|
+
};
|
|
7356
|
+
}, [deviceId2]);
|
|
7357
|
+
return stream;
|
|
7358
|
+
}
|
|
5396
7359
|
function LobbyPanel() {
|
|
5397
7360
|
const lobby = useLobby();
|
|
5398
7361
|
if (!lobby.canAdmit || lobby.waiting.length === 0) return null;
|
|
@@ -5618,16 +7581,40 @@ function PreJoinBody({
|
|
|
5618
7581
|
const room = useRoom();
|
|
5619
7582
|
const devices = useDeviceSnapshot();
|
|
5620
7583
|
const recording = useRecording();
|
|
7584
|
+
const effects = useMediaEffects();
|
|
7585
|
+
const { notice } = useModerationNotice();
|
|
5621
7586
|
const [mode, setMode] = useState3("video");
|
|
5622
7587
|
const [microphone, setMicrophone] = useState3(true);
|
|
5623
7588
|
const [joining, setJoining] = useState3(false);
|
|
7589
|
+
const camera = devices?.selected.videoinput ?? host.devices.resolve("videoinput");
|
|
7590
|
+
const mic = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
|
|
7591
|
+
const preview = useCameraPreview(mode === "video" ? camera : void 0, effects.effects);
|
|
7592
|
+
const micStream = useMicrophoneStream(microphone ? mic : "off");
|
|
7593
|
+
const level = useAudioLevel(micStream);
|
|
7594
|
+
const [speakerTesting, setSpeakerTesting] = useState3(false);
|
|
5624
7595
|
useEffect3(() => {
|
|
5625
7596
|
void host.devices.refresh();
|
|
5626
7597
|
}, [host]);
|
|
5627
7598
|
return /* @__PURE__ */ jsxs2("div", { className: "mx-meet__prejoin", children: [
|
|
5628
|
-
/* @__PURE__ */
|
|
7599
|
+
/* @__PURE__ */ jsxs2("div", { className: "mx-meet__preview", children: [
|
|
7600
|
+
mode === "video" && preview.preview !== null ? /* @__PURE__ */ jsx3(
|
|
7601
|
+
"video",
|
|
7602
|
+
{
|
|
7603
|
+
className: "mx-meet__video mx-meet__video--mirror",
|
|
7604
|
+
autoPlay: true,
|
|
7605
|
+
playsInline: true,
|
|
7606
|
+
muted: true,
|
|
7607
|
+
ref: (element) => {
|
|
7608
|
+
if (element !== null) preview.preview?.attach(element);
|
|
7609
|
+
}
|
|
7610
|
+
}
|
|
7611
|
+
) : /* @__PURE__ */ jsx3("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ jsx3(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 96 }) }),
|
|
7612
|
+
mode === "video" && preview.error !== null ? /* @__PURE__ */ jsx3("p", { className: "mx-meet__preview-note", children: preview.error }) : null,
|
|
7613
|
+
microphone ? /* @__PURE__ */ jsx3("div", { className: "mx-meet__meter mx-meet__meter--preview", role: "meter", "aria-label": "Microphone level", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(level * 100), children: /* @__PURE__ */ jsx3("span", { style: { width: `${Math.min(100, Math.round(level * 160))}%` } }) }) : null
|
|
7614
|
+
] }),
|
|
5629
7615
|
/* @__PURE__ */ jsxs2("div", { className: "mx-meet__prejoin-panel", children: [
|
|
5630
7616
|
/* @__PURE__ */ jsx3("h1", { className: "mx-meet__prejoin-title", children: "Ready to join?" }),
|
|
7617
|
+
notice?.kind === "removed" ? /* @__PURE__ */ jsx3("p", { className: "mx-meet__warning", children: notice.text }) : null,
|
|
5631
7618
|
devices?.labelsHidden === true ? /* @__PURE__ */ jsxs2("p", { className: "mx-meet__panel-note", children: [
|
|
5632
7619
|
"Your browser is hiding device names until you allow camera and microphone access.",
|
|
5633
7620
|
/* @__PURE__ */ jsx3(
|
|
@@ -5644,7 +7631,30 @@ function PreJoinBody({
|
|
|
5644
7631
|
] }) : null,
|
|
5645
7632
|
devices?.fallbackNotice != null ? /* @__PURE__ */ jsx3("p", { className: "mx-meet__warning", children: devices.fallbackNotice }) : null,
|
|
5646
7633
|
/* @__PURE__ */ jsx3(DevicePicker, { kind: "audioinput", label: "Microphone" }),
|
|
7634
|
+
/* @__PURE__ */ jsxs2("div", { className: "mx-meet__field-row", children: [
|
|
7635
|
+
/* @__PURE__ */ jsx3(DevicePicker, { kind: "audiooutput", label: "Speaker" }),
|
|
7636
|
+
/* @__PURE__ */ jsxs2(
|
|
7637
|
+
"button",
|
|
7638
|
+
{
|
|
7639
|
+
type: "button",
|
|
7640
|
+
className: "mx-meet__chip",
|
|
7641
|
+
disabled: speakerTesting,
|
|
7642
|
+
onClick: () => {
|
|
7643
|
+
setSpeakerTesting(true);
|
|
7644
|
+
void playTestSound(devices?.selected.audiooutput ?? host.devices.resolve("audiooutput")).finally(
|
|
7645
|
+
() => setSpeakerTesting(false)
|
|
7646
|
+
);
|
|
7647
|
+
},
|
|
7648
|
+
children: [
|
|
7649
|
+
/* @__PURE__ */ jsx3(SpeakerIcon, { width: 14, height: 14 }),
|
|
7650
|
+
" ",
|
|
7651
|
+
speakerTesting ? "Playing\u2026" : "Test"
|
|
7652
|
+
]
|
|
7653
|
+
}
|
|
7654
|
+
)
|
|
7655
|
+
] }),
|
|
5647
7656
|
/* @__PURE__ */ jsx3(DevicePicker, { kind: "videoinput", label: "Camera" }),
|
|
7657
|
+
/* @__PURE__ */ jsx3(EffectsSettings, { preview: preview.preview }),
|
|
5648
7658
|
recording.consentNotice !== null ? /* @__PURE__ */ jsx3("p", { className: "mx-meet__consent", children: recording.consentNotice }) : null,
|
|
5649
7659
|
/* @__PURE__ */ jsxs2("div", { className: "mx-meet__prejoin-actions", children: [
|
|
5650
7660
|
/* @__PURE__ */ jsxs2("label", { className: "mx-meet__toggle", children: [
|
|
@@ -5676,6 +7686,7 @@ function PreJoinBody({
|
|
|
5676
7686
|
className: "mx-meet__primary",
|
|
5677
7687
|
onClick: () => {
|
|
5678
7688
|
setJoining(true);
|
|
7689
|
+
preview.release();
|
|
5679
7690
|
void room.join({
|
|
5680
7691
|
roomName,
|
|
5681
7692
|
mode,
|
|
@@ -5695,9 +7706,49 @@ function PreJoinBody({
|
|
|
5695
7706
|
] })
|
|
5696
7707
|
] });
|
|
5697
7708
|
}
|
|
7709
|
+
function useCameraPreview(deviceId2, effects) {
|
|
7710
|
+
const [preview, setPreview] = useState3(null);
|
|
7711
|
+
const [error, setError] = useState3(null);
|
|
7712
|
+
const current = useRef3(null);
|
|
7713
|
+
const initialEffect = useRef3(toVideoEffect(effects));
|
|
7714
|
+
initialEffect.current = toVideoEffect(effects);
|
|
7715
|
+
useEffect3(() => {
|
|
7716
|
+
if (deviceId2 === void 0) return void 0;
|
|
7717
|
+
let live = true;
|
|
7718
|
+
setError(null);
|
|
7719
|
+
void createCameraPreview(deviceId2, initialEffect.current).then((created) => {
|
|
7720
|
+
if (!live) {
|
|
7721
|
+
created.stop();
|
|
7722
|
+
return;
|
|
7723
|
+
}
|
|
7724
|
+
current.current = created;
|
|
7725
|
+
setPreview(created);
|
|
7726
|
+
}).catch((thrown) => {
|
|
7727
|
+
if (!live) return;
|
|
7728
|
+
const failure = thrown;
|
|
7729
|
+
setError([failure.message, failure.remedy].filter(Boolean).join(". ") || "The camera did not start.");
|
|
7730
|
+
});
|
|
7731
|
+
return () => {
|
|
7732
|
+
live = false;
|
|
7733
|
+
current.current?.stop();
|
|
7734
|
+
current.current = null;
|
|
7735
|
+
setPreview(null);
|
|
7736
|
+
};
|
|
7737
|
+
}, [deviceId2]);
|
|
7738
|
+
return {
|
|
7739
|
+
preview,
|
|
7740
|
+
error,
|
|
7741
|
+
release: () => {
|
|
7742
|
+
current.current?.stop();
|
|
7743
|
+
current.current = null;
|
|
7744
|
+
setPreview(null);
|
|
7745
|
+
}
|
|
7746
|
+
};
|
|
7747
|
+
}
|
|
5698
7748
|
function DevicePicker({
|
|
5699
7749
|
kind,
|
|
5700
|
-
label
|
|
7750
|
+
label,
|
|
7751
|
+
onPick
|
|
5701
7752
|
}) {
|
|
5702
7753
|
const host = useRequiredMeetHost();
|
|
5703
7754
|
const devices = useDeviceSnapshot();
|
|
@@ -5710,7 +7761,10 @@ function DevicePicker({
|
|
|
5710
7761
|
{
|
|
5711
7762
|
className: "mx-meet__select",
|
|
5712
7763
|
value: devices?.selected[kind] ?? host.devices.resolve(kind) ?? "",
|
|
5713
|
-
onChange: (event) =>
|
|
7764
|
+
onChange: (event) => {
|
|
7765
|
+
if (onPick !== void 0) onPick(kind, event.target.value);
|
|
7766
|
+
else host.devices.select(kind, event.target.value);
|
|
7767
|
+
},
|
|
5714
7768
|
children: list.map((device) => /* @__PURE__ */ jsx3("option", { value: device.deviceId, children: device.label.length > 0 ? device.label : `${label} (name hidden)` }, device.deviceId))
|
|
5715
7769
|
}
|
|
5716
7770
|
)
|
|
@@ -5917,7 +7971,7 @@ function ScheduleMeetingBody({
|
|
|
5917
7971
|
host
|
|
5918
7972
|
}) {
|
|
5919
7973
|
const [title, setTitle] = useState3("");
|
|
5920
|
-
const [
|
|
7974
|
+
const [when2, setWhen] = useState3("");
|
|
5921
7975
|
const [kind, setKind] = useState3("scheduled");
|
|
5922
7976
|
const [busy, setBusy] = useState3(false);
|
|
5923
7977
|
const [link, setLink] = useState3(null);
|
|
@@ -5937,7 +7991,7 @@ function ScheduleMeetingBody({
|
|
|
5937
7991
|
hostUserId: host.identity.userId,
|
|
5938
7992
|
title: title.trim(),
|
|
5939
7993
|
kind,
|
|
5940
|
-
scheduledFor:
|
|
7994
|
+
scheduledFor: when2.length > 0 ? new Date(when2).toISOString() : null
|
|
5941
7995
|
}).then((meeting) => {
|
|
5942
7996
|
setLink(meetingLink(appBaseUrl, meeting.slug));
|
|
5943
7997
|
onScheduled?.(meeting.slug);
|
|
@@ -5967,7 +8021,7 @@ function ScheduleMeetingBody({
|
|
|
5967
8021
|
{
|
|
5968
8022
|
className: "mx-meet__input",
|
|
5969
8023
|
type: "datetime-local",
|
|
5970
|
-
value:
|
|
8024
|
+
value: when2,
|
|
5971
8025
|
onChange: (event) => setWhen(event.target.value)
|
|
5972
8026
|
}
|
|
5973
8027
|
)
|
|
@@ -6547,6 +8601,57 @@ function createMeetingInviteHandler(options) {
|
|
|
6547
8601
|
}
|
|
6548
8602
|
};
|
|
6549
8603
|
}
|
|
8604
|
+
|
|
8605
|
+
// src/core/management.ts
|
|
8606
|
+
async function refusal(response, operation) {
|
|
8607
|
+
let body = null;
|
|
8608
|
+
try {
|
|
8609
|
+
body = await response.json();
|
|
8610
|
+
} catch {
|
|
8611
|
+
body = null;
|
|
8612
|
+
}
|
|
8613
|
+
const said = readServerRefusal(body);
|
|
8614
|
+
return new MeetError(
|
|
8615
|
+
response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
|
|
8616
|
+
`${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
|
|
8617
|
+
said?.remedy || "The meeting itself is saved \u2014 only the telling failed. Try again; the call is idempotent."
|
|
8618
|
+
);
|
|
8619
|
+
}
|
|
8620
|
+
function count(body, key) {
|
|
8621
|
+
const value = body[key];
|
|
8622
|
+
return typeof value === "number" ? value : 0;
|
|
8623
|
+
}
|
|
8624
|
+
async function announceMeeting(api, meetingId) {
|
|
8625
|
+
const operation = "meet.announceMeeting";
|
|
8626
|
+
const response = await api.request({
|
|
8627
|
+
path: meetingPath(MEET_ROUTES.meetingAnnounce, meetingId),
|
|
8628
|
+
method: "POST",
|
|
8629
|
+
requireAuth: true,
|
|
8630
|
+
operation
|
|
8631
|
+
});
|
|
8632
|
+
if (!response.ok) throw await refusal(response, operation);
|
|
8633
|
+
const body = await response.json();
|
|
8634
|
+
return {
|
|
8635
|
+
invitations: count(body, "invitations"),
|
|
8636
|
+
updates: count(body, "updates"),
|
|
8637
|
+
cancellations: count(body, "cancellations"),
|
|
8638
|
+
alreadyCurrent: count(body, "already_current"),
|
|
8639
|
+
remindersQueued: count(body, "reminders_queued"),
|
|
8640
|
+
remindersCancelled: count(body, "reminders_cancelled")
|
|
8641
|
+
};
|
|
8642
|
+
}
|
|
8643
|
+
async function pushMeetingToGoogleCalendar(api, meetingId, target) {
|
|
8644
|
+
const operation = "meet.pushMeetingToGoogleCalendar";
|
|
8645
|
+
const response = await api.request({
|
|
8646
|
+
path: meetingPath(MEET_ROUTES.meetingGoogleCalendar, meetingId),
|
|
8647
|
+
method: "POST",
|
|
8648
|
+
requireAuth: true,
|
|
8649
|
+
operation,
|
|
8650
|
+
body: { connection_id: target.connectionId, calendar_id: target.calendarId ?? "primary" }
|
|
8651
|
+
});
|
|
8652
|
+
if (!response.ok) throw await refusal(response, operation);
|
|
8653
|
+
return await response.json();
|
|
8654
|
+
}
|
|
6550
8655
|
export {
|
|
6551
8656
|
AiPanel,
|
|
6552
8657
|
AttendanceNotice,
|
|
@@ -6567,19 +8672,23 @@ export {
|
|
|
6567
8672
|
ControlBar,
|
|
6568
8673
|
ControlButton,
|
|
6569
8674
|
DATA_TOPICS,
|
|
8675
|
+
DEFAULT_EFFECTS,
|
|
6570
8676
|
DEFAULT_PAGE_SIZE,
|
|
6571
8677
|
END_MEETING_NOT_SERVED,
|
|
6572
8678
|
FILE_SESSION_PATH,
|
|
8679
|
+
FlagIcon,
|
|
6573
8680
|
GUEST_RECORD_ACCESS_CHOICES,
|
|
6574
8681
|
GUEST_RECORD_ACCESS_LEVELS,
|
|
6575
8682
|
GUEST_RECORD_ACCESS_METADATA_KEY,
|
|
6576
8683
|
GUEST_RECORD_NOT_SHARED,
|
|
6577
8684
|
GuestRecordAccessControl,
|
|
6578
8685
|
HandIcon,
|
|
8686
|
+
HostMenu,
|
|
6579
8687
|
IDLE_MEDIA,
|
|
6580
8688
|
IDLE_TRACK,
|
|
6581
8689
|
IncomingCallCard,
|
|
6582
8690
|
IncomingCallHost,
|
|
8691
|
+
KeyboardIcon,
|
|
6583
8692
|
LISTEN_ONLY_NOTICE,
|
|
6584
8693
|
LayoutIcon,
|
|
6585
8694
|
LobbyPanel,
|
|
@@ -6589,15 +8698,20 @@ export {
|
|
|
6589
8698
|
MEET_ANSWER_CONTEXT_EVENT,
|
|
6590
8699
|
MEET_ANSWER_EVENTS,
|
|
6591
8700
|
MEET_EVENTS,
|
|
8701
|
+
MEET_REACTIONS,
|
|
6592
8702
|
MEET_ROUTES,
|
|
6593
8703
|
MEET_SCHEMA,
|
|
8704
|
+
MEET_SHORTCUTS,
|
|
8705
|
+
MODERATION_ACTIONS,
|
|
6594
8706
|
MeetError,
|
|
6595
8707
|
MeetProvider,
|
|
6596
8708
|
MeetingList,
|
|
8709
|
+
MeetingOverlays,
|
|
6597
8710
|
MeetingRecordView,
|
|
6598
8711
|
MeetingRoom,
|
|
6599
8712
|
MicIcon,
|
|
6600
8713
|
MicOffIcon,
|
|
8714
|
+
MoreIcon,
|
|
6601
8715
|
NOTE_TAKER_IDENTITY_PREFIX,
|
|
6602
8716
|
NOTE_TAKER_METADATA_KEY,
|
|
6603
8717
|
NO_GRANTS,
|
|
@@ -6605,12 +8719,15 @@ export {
|
|
|
6605
8719
|
PARTICIPANT_CAN_RECORD_ATTRIBUTE,
|
|
6606
8720
|
PARTICIPANT_ROLE_ATTRIBUTE,
|
|
6607
8721
|
ParticipantTile,
|
|
8722
|
+
PencilIcon,
|
|
6608
8723
|
PeopleIcon,
|
|
6609
8724
|
PeoplePanel,
|
|
8725
|
+
PersonMenu,
|
|
6610
8726
|
PhoneIcon,
|
|
6611
8727
|
PhoneOffIcon,
|
|
6612
8728
|
PinIcon,
|
|
6613
8729
|
PreJoin,
|
|
8730
|
+
QualityBars,
|
|
6614
8731
|
REACTION_TTL_MS,
|
|
6615
8732
|
REFUSED_ABSENCE,
|
|
6616
8733
|
ROLE_RANK,
|
|
@@ -6624,14 +8741,22 @@ export {
|
|
|
6624
8741
|
RecordingPlayer,
|
|
6625
8742
|
ScheduleMeeting,
|
|
6626
8743
|
ScreenShareIcon,
|
|
8744
|
+
SearchIcon,
|
|
6627
8745
|
SettingsIcon,
|
|
8746
|
+
SettingsPanel,
|
|
8747
|
+
ShortcutsSheet,
|
|
8748
|
+
SignalIcon,
|
|
8749
|
+
SmileIcon,
|
|
6628
8750
|
SparklesIcon,
|
|
8751
|
+
SpeakerIcon,
|
|
8752
|
+
SpotlightIcon,
|
|
6629
8753
|
Stage,
|
|
6630
8754
|
TABLES,
|
|
6631
8755
|
TRANSCRIPTION_ATTRIBUTES,
|
|
6632
8756
|
TRANSCRIPTION_TOPIC,
|
|
6633
8757
|
WifiLowIcon,
|
|
6634
8758
|
absenceSentence,
|
|
8759
|
+
announceMeeting,
|
|
6635
8760
|
applyRoomEvent,
|
|
6636
8761
|
asCallId,
|
|
6637
8762
|
asMeetingId,
|
|
@@ -6646,11 +8771,13 @@ export {
|
|
|
6646
8771
|
canModerateParticipant,
|
|
6647
8772
|
canTransition,
|
|
6648
8773
|
captionSegment,
|
|
8774
|
+
changesPatch,
|
|
6649
8775
|
computeStage,
|
|
6650
8776
|
countLabel,
|
|
6651
8777
|
createCallCenter,
|
|
6652
8778
|
createCallInviteHandler,
|
|
6653
8779
|
createDeviceManager,
|
|
8780
|
+
createInRoomClient,
|
|
6654
8781
|
createLiveKitDriver,
|
|
6655
8782
|
createMeetAi,
|
|
6656
8783
|
createMeetApi,
|
|
@@ -6663,6 +8790,7 @@ export {
|
|
|
6663
8790
|
createRoomEngine,
|
|
6664
8791
|
createTokenClient,
|
|
6665
8792
|
createWebDeviceStorage,
|
|
8793
|
+
createWebEffectsStorage,
|
|
6666
8794
|
createWebRoomTokenStorage,
|
|
6667
8795
|
displayNameFor,
|
|
6668
8796
|
fileDownloadPath,
|
|
@@ -6690,6 +8818,7 @@ export {
|
|
|
6690
8818
|
mapQuality,
|
|
6691
8819
|
meetingInviteAction,
|
|
6692
8820
|
meetingLink,
|
|
8821
|
+
meetingPath,
|
|
6693
8822
|
meetingSlug,
|
|
6694
8823
|
meetingTopic,
|
|
6695
8824
|
meetingWhen,
|
|
@@ -6706,9 +8835,15 @@ export {
|
|
|
6706
8835
|
parseRoomToken,
|
|
6707
8836
|
participantSummary,
|
|
6708
8837
|
playableRecordingFileId,
|
|
8838
|
+
projectInvitee,
|
|
6709
8839
|
projectNote,
|
|
8840
|
+
projectOccurrence,
|
|
8841
|
+
projectOverride,
|
|
6710
8842
|
projectParticipant,
|
|
8843
|
+
projectRsvpInvitation,
|
|
6711
8844
|
projectSegment,
|
|
8845
|
+
projectUpcoming,
|
|
8846
|
+
pushMeetingToGoogleCalendar,
|
|
6712
8847
|
readServerRefusal,
|
|
6713
8848
|
readableTranscript,
|
|
6714
8849
|
recordingLabel,
|
|
@@ -6716,8 +8851,10 @@ export {
|
|
|
6716
8851
|
roleOf,
|
|
6717
8852
|
roomTopic,
|
|
6718
8853
|
setGuestRecordAccess,
|
|
8854
|
+
settingsPatch,
|
|
6719
8855
|
snapshotParticipant,
|
|
6720
8856
|
unavailableFrom,
|
|
8857
|
+
useAudioLevel,
|
|
6721
8858
|
useCalls,
|
|
6722
8859
|
useCanModerate,
|
|
6723
8860
|
useCanRecord,
|
|
@@ -6726,19 +8863,23 @@ export {
|
|
|
6726
8863
|
useElapsed,
|
|
6727
8864
|
useEndMeeting,
|
|
6728
8865
|
useGuestRecordAccess,
|
|
8866
|
+
useHandQueue,
|
|
6729
8867
|
useHostControls,
|
|
6730
8868
|
useIsHost,
|
|
6731
8869
|
useListenOnlyNotice,
|
|
6732
8870
|
useLobby,
|
|
6733
8871
|
useLocalMedia,
|
|
6734
8872
|
useLocalUserId,
|
|
8873
|
+
useMediaEffects,
|
|
6735
8874
|
useMeetAi,
|
|
6736
8875
|
useMeetHost,
|
|
8876
|
+
useMeetShortcuts,
|
|
6737
8877
|
useMeetSnapshot,
|
|
6738
8878
|
useMeeting,
|
|
6739
8879
|
useMeetingId,
|
|
6740
8880
|
useMeetingRecord,
|
|
6741
8881
|
useMeetings,
|
|
8882
|
+
useModerationNotice,
|
|
6742
8883
|
useReactions,
|
|
6743
8884
|
useRecording,
|
|
6744
8885
|
useRequiredMeetHost,
|