@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/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 refusal = await refusalFor(response, operation);
539
- options.store.patch({ noteTakerRefusal: refusal });
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
- refusal.reason,
543
- refusal.remedy
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 refusal = await refusalFor(response, "meet.ai.ask");
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
- refusal.reason,
585
- refusal.remedy
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 ? { ...participant, handRaised: raised } : participant
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 refusal = readServerRefusal(body);
2101
+ const refusal2 = readServerRefusal(body);
2026
2102
  throw new MeetError(
2027
2103
  response.status === 403 ? "forbidden" : "transport",
2028
- refusal?.reason ?? `meet.endForEveryone: the server answered ${response.status}`,
2029
- refusal?.remedy !== void 0 && refusal.remedy.length > 0 ? refusal.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."
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 refusal = args.refusal ?? null;
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(sender, envelope.raised === true);
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 sendChat(text3) {
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(DATA_TOPICS.chat, { id, text: trimmed, at, name: identity.displayName });
3807
+ await publish(
3808
+ DATA_TOPICS.chat,
3809
+ { id, text: trimmed, at, name: identity.displayName, to },
3810
+ [to]
3811
+ );
3213
3812
  },
3214
- async publishHostAction(action) {
3215
- if (!isHostRole(effectiveRole(store.snapshot()))) {
3216
- throw new MeetError(
3217
- "forbidden",
3218
- `meet.hostAction(${action.kind}): this participant is not a host`,
3219
- "Host powers come from the server-minted token metadata. Hide the control instead of rendering a dead button (no dead ends)."
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
- await publish(DATA_TOPICS.hostAction, { ...action });
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/livekit-driver.ts
3254
- function mapQuality(quality) {
3255
- switch (quality) {
3256
- case "excellent":
3257
- return "excellent";
3258
- case "good":
3259
- return "good";
3260
- case "poor":
3261
- return "poor";
3262
- case "lost":
3263
- return "lost";
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 snapshotParticipant(participant, isLocal) {
3282
- const name = participant.name;
3875
+ function createWebEffectsStorage(key = "mx.meet.effects") {
3876
+ let memory = DEFAULT_EFFECTS;
3877
+ const storage = globalThis.localStorage;
3283
3878
  return {
3284
- identity: participant.identity,
3285
- name: name ?? "",
3286
- metadata: participant.metadata ?? null,
3287
- // ATTRIBUTES ARE PART OF THE SNAPSHOT, not read ad hoc later: the note-taker
3288
- // announces the consent notice here (D10) and the roster is the only place
3289
- // this package looks at a participant.
3290
- attributes: participant.attributes ?? {},
3291
- isLocal,
3292
- micEnabled: participant.isMicrophoneEnabled,
3293
- cameraEnabled: participant.isCameraEnabled,
3294
- screenSharing: participant.isScreenShareEnabled
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
- var encoder = new TextEncoder();
3298
- var decoder = new TextDecoder();
3299
- function createLiveKitDriver(handlers, options = {}) {
3300
- let room = null;
3301
- let projectRoster = null;
3302
- async function load() {
3303
- if (room !== null) return room;
3304
- let module;
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.ActiveSpeakersChanged, (speakers) => {
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(enabled);
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
- await requireRoom("setCameraEnabled").localParticipant.setCameraEnabled(enabled);
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 store = createMeetStore();
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 ai = createMeetAi({
3625
- api,
3626
- store,
3627
- resolveRoomToken: () => {
3628
- const roomName = store.snapshot().meeting?.roomName;
3629
- if (roomName === void 0) return null;
3630
- const active = engine.token();
3631
- if (active?.roomName === roomName && active.token.length > 0) return active.token;
3632
- return tokens.remembered(roomName)?.token ?? null;
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 feature = sharer !== null ? sharer : args.layout === "grid" ? null : pinnedParticipant ?? ordered[0] ?? null;
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(count) {
3805
- if (count <= 1) return 1;
3806
- if (count <= 4) return 2;
3807
- if (count <= 9) return 3;
3808
- if (count <= 16) return 4;
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 when = startedAt.toLocaleString(locale, {
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 when;
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 when;
4928
+ if (Number.isNaN(endedAt.getTime())) return when2;
3993
4929
  const elapsedMs = endedAt.getTime() - startedAt.getTime();
3994
- return `${when} \xB7 ${formatDurationMs(elapsedMs, { style: "long" })}`;
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 refusal = await readRefusal(response);
4102
- return refusedBundle(meeting, refusal.message, refusal.remedy);
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 refusal = await readRefusal(response);
4194
- throw new MeetError("forbidden", refusal.message, refusal.remedy);
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) => requireEngine(engine, "sendChat").sendChat(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) => requireEngine(engine, "muteParticipant").publishHostAction({
4635
- kind: "mute",
4636
- identity: target.identity
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) => requireEngine(engine, "removeParticipant").publishHostAction({
4642
- kind: "remove",
4643
- identity: target.identity
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(count, singular, plural = `${singular}s`) {
4862
- return `${count} ${count === 1 ? singular : plural}`;
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 canModerate2 = useCanModerate(participant);
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
- participant.handRaised ? /* @__PURE__ */ jsx3(HandIcon, { width: 14, height: 14 }) : null,
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: featured ? "Unpin" : `Pin ${participant.displayName}`,
5137
- onClick: () => host?.engine.pin(featured ? null : participant.identity),
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
- canModerate2 ? /* @__PURE__ */ jsx3(
5142
- ControlButton,
5143
- {
5144
- label: `Mute ${participant.displayName}`,
5145
- onClick: () => {
5146
- void host?.engine.publishHostAction({
5147
- kind: "mute",
5148
- identity: participant.identity
5149
- });
5150
- },
5151
- children: /* @__PURE__ */ jsx3(MicOffIcon, { width: 16, height: 16 })
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
- panel === "chat" ? /* @__PURE__ */ jsx3(ChatPanel, { onClose: () => setPanel("none") }) : null,
5196
- panel === "people" ? /* @__PURE__ */ jsx3(PeoplePanel, { onClose: () => setPanel("none") }) : null,
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__ */ jsx3(ControlButton, { label: "Chat", active: panel === "chat", onClick: () => setPanel(panel === "chat" ? "none" : "chat"), children: /* @__PURE__ */ jsx3(ChatIcon, {}) }),
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: () => setPanel(panel === "people" ? "none" : "people"),
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: () => setPanel(panel === "ai" ? "none" : "ai") }),
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__ */ jsx3(
5252
- ControlButton,
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({ onClose }) {
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
- return /* @__PURE__ */ jsxs2(PanelShell, { title: "In-meeting chat", onClose, children: [
5291
- /* @__PURE__ */ jsx3("p", { className: "mx-meet__panel-note", children: "In-meeting chat is ephemeral \u2014 it disappears when the meeting ends." }),
5292
- /* @__PURE__ */ jsx3("ul", { className: "mx-meet__chat", children: messages.map((message) => /* @__PURE__ */ jsxs2("li", { className: "mx-meet__chat-line", children: [
5293
- /* @__PURE__ */ jsx3("strong", { children: message.displayName }),
5294
- /* @__PURE__ */ jsx3("span", { children: message.text })
5295
- ] }, message.id)) }),
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__ */ jsx3(
5308
- "input",
6768
+ people.length > 0 ? /* @__PURE__ */ jsxs2(
6769
+ "select",
5309
6770
  {
5310
- className: "mx-meet__input",
5311
- value: draft,
5312
- onChange: (event) => setDraft(event.target.value),
5313
- placeholder: "Message everyone",
5314
- "aria-label": "Message everyone"
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__ */ jsx3("button", { type: "submit", className: "mx-meet__send", children: "Send" })
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 PeoplePanel({ onClose }) {
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
- return /* @__PURE__ */ jsxs2(PanelShell, { title: "People", onClose, children: [
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
- snapshot?.meeting !== null && snapshot?.meeting !== void 0 ? /* @__PURE__ */ jsx3(GuestRecordAccessControl, { meeting: snapshot.meeting }) : null,
5331
- /* @__PURE__ */ jsx3("ul", { className: "mx-meet__people", children: (snapshot?.participants ?? []).map((participant) => /* @__PURE__ */ jsxs2("li", { className: "mx-meet__person", children: [
5332
- /* @__PURE__ */ jsx3(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: 32 }),
5333
- /* @__PURE__ */ jsxs2("span", { className: "mx-meet__person-name", children: [
5334
- participant.displayName,
5335
- /* @__PURE__ */ jsx3(
5336
- RoleBadge,
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
- role: participant.identity === local ? myRole : participant.role
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
- !participant.micEnabled && !isNoteTaker(participant) ? /* @__PURE__ */ jsx3(MicOffIcon, { width: 14, height: 14 }) : null,
5344
- participant.handRaised ? /* @__PURE__ */ jsx3(HandIcon, { width: 14, height: 14 }) : null,
5345
- controls.canModerate(participant) ? /* @__PURE__ */ jsx3(PersonHostActions, { participant, controls }) : null
5346
- ] }, participant.identity)) })
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 PersonHostActions({
6964
+ function PersonMenu({
5350
6965
  participant,
5351
- controls
6966
+ onClose,
6967
+ onMessage,
6968
+ placement = "down"
5352
6969
  }) {
5353
- const [confirming, setConfirming] = useState3(false);
5354
- if (confirming) {
5355
- return /* @__PURE__ */ jsxs2("span", { className: "mx-meet__confirm", role: "group", "aria-label": `Remove ${participant.displayName}?`, children: [
5356
- /* @__PURE__ */ jsxs2("span", { className: "mx-meet__confirm-text", children: [
5357
- "Remove ",
5358
- participant.displayName,
5359
- "? They leave the meeting immediately and have to be let back in."
5360
- ] }),
5361
- /* @__PURE__ */ jsx3(
5362
- "button",
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
- type: "button",
5365
- className: "mx-meet__link mx-meet__link--danger",
5366
- onClick: () => {
5367
- setConfirming(false);
5368
- void controls.remove(participant);
7111
+ onSelect: () => {
7112
+ setText(participant.displayName);
7113
+ setMode("rename");
5369
7114
  },
5370
- children: "Remove"
7115
+ children: [
7116
+ /* @__PURE__ */ jsx3(PencilIcon, { width: 16, height: 16 }),
7117
+ " Rename"
7118
+ ]
5371
7119
  }
5372
- ),
5373
- /* @__PURE__ */ jsx3("button", { type: "button", className: "mx-meet__link", onClick: () => setConfirming(false), children: "Keep them" })
5374
- ] });
5375
- }
5376
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
5377
- participant.micEnabled ? /* @__PURE__ */ jsx3(
5378
- ControlButton,
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
- label: `Mute ${participant.displayName}`,
5381
- onClick: () => void controls.mute(participant),
5382
- children: /* @__PURE__ */ jsx3(MicOffIcon, { width: 14, height: 14 })
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
- ControlButton,
7166
+ DeviceSettings,
5387
7167
  {
5388
- label: `Remove ${participant.displayName}`,
5389
- danger: true,
5390
- onClick: () => setConfirming(true),
5391
- children: /* @__PURE__ */ jsx3(CloseIcon, { width: 14, height: 14 })
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__ */ jsx3("div", { className: "mx-meet__preview", children: /* @__PURE__ */ jsx3("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ jsx3(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 96 }) }) }),
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) => host.devices.select(kind, event.target.value),
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 [when, setWhen] = useState3("");
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: when.length > 0 ? new Date(when).toISOString() : null
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: when,
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,