@ai-matrx/meet 0.7.3 → 0.7.6

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
@@ -211,7 +211,16 @@ var MEET_ROUTES = {
211
211
  */
212
212
  meetingAnnounce: "/api/v1/meet/meetings/{meeting_id}/announce",
213
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"
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"
215
224
  };
216
225
  function meetingPath(template, meetingId) {
217
226
  return template.replace("{meeting_id}", encodeURIComponent(meetingId));
@@ -1516,6 +1525,11 @@ function captionSegment(args) {
1516
1525
  }
1517
1526
 
1518
1527
  // src/core/store.ts
1528
+ var DEFAULT_EFFECTS = {
1529
+ noiseSuppression: true,
1530
+ background: "none",
1531
+ backgroundImage: null
1532
+ };
1519
1533
  var NO_GRANTS = {
1520
1534
  role: "participant",
1521
1535
  canRecord: false,
@@ -1538,7 +1552,13 @@ var EMPTY = {
1538
1552
  recordingSource: "local",
1539
1553
  recordingId: null,
1540
1554
  chat: [],
1555
+ chatLoaded: false,
1556
+ chatHistoryError: null,
1541
1557
  reactions: [],
1558
+ spotlight: null,
1559
+ moderationNotice: null,
1560
+ actionNotice: null,
1561
+ effects: DEFAULT_EFFECTS,
1542
1562
  transcript: [],
1543
1563
  notes: [],
1544
1564
  names: {},
@@ -1577,6 +1597,7 @@ function createMeetStore(initial = {}) {
1577
1597
  setParticipants(participants) {
1578
1598
  const names = learnNames(state.names, participants);
1579
1599
  const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
1600
+ const handTimes = new Map(state.participants.map((p) => [p.identity, p.handRaisedAt ?? null]));
1580
1601
  const quality = new Map(state.participants.map((p) => [p.identity, p.quality]));
1581
1602
  commit({
1582
1603
  ...state,
@@ -1590,6 +1611,7 @@ function createMeetStore(initial = {}) {
1590
1611
  // name book. `roster-names.test.ts` fails if a token ever gets past it.
1591
1612
  displayName: displayNameFor(names, participant.identity, participant.displayName),
1592
1613
  handRaised: hands.get(participant.identity) ?? participant.handRaised,
1614
+ handRaisedAt: handTimes.get(participant.identity) ?? participant.handRaisedAt ?? null,
1593
1615
  quality: participant.quality === "unknown" ? quality.get(participant.identity) ?? "unknown" : participant.quality
1594
1616
  }))
1595
1617
  });
@@ -1612,11 +1634,17 @@ function createMeetStore(initial = {}) {
1612
1634
  )
1613
1635
  });
1614
1636
  },
1615
- setHand(identity, raised) {
1637
+ setHand(identity, raised, at) {
1616
1638
  commit({
1617
1639
  ...state,
1618
1640
  participants: state.participants.map(
1619
- (participant) => participant.identity === identity ? { ...participant, handRaised: raised } : participant
1641
+ (participant) => participant.identity === identity ? {
1642
+ ...participant,
1643
+ handRaised: raised,
1644
+ // A hand that is ALREADY up keeps its place in the queue when
1645
+ // it is re-announced (to a late joiner): the first time wins.
1646
+ handRaisedAt: raised ? participant.handRaised && participant.handRaisedAt != null ? participant.handRaisedAt : at ?? Date.now() : null
1647
+ } : participant
1620
1648
  )
1621
1649
  });
1622
1650
  },
@@ -1626,6 +1654,31 @@ function createMeetStore(initial = {}) {
1626
1654
  const names = learnName(state.names, message.identity, message.displayName);
1627
1655
  commit({ ...state, chat: chat.slice(-CHAT_LIMIT), names });
1628
1656
  },
1657
+ loadChat(messages) {
1658
+ const known = new Set(state.chat.map((message) => message.id));
1659
+ const merged = [...messages.filter((message) => !known.has(message.id)), ...state.chat].sort(
1660
+ (a, b) => a.at < b.at ? -1 : a.at > b.at ? 1 : 0
1661
+ );
1662
+ const names = learnNames(
1663
+ state.names,
1664
+ messages.map((message) => ({ identity: message.identity, displayName: message.displayName }))
1665
+ );
1666
+ commit({ ...state, chat: merged.slice(-CHAT_LIMIT), chatLoaded: true, chatHistoryError: null, names });
1667
+ },
1668
+ settleChat(localId, next) {
1669
+ const index = state.chat.findIndex((message) => message.id === localId);
1670
+ if (index === -1) return;
1671
+ const current = state.chat[index];
1672
+ let replacement;
1673
+ if ("failed" in next && typeof next.failed === "string" && !("id" in next)) {
1674
+ replacement = { ...current, failed: next.failed };
1675
+ } else {
1676
+ replacement = next;
1677
+ }
1678
+ const duplicate = replacement.id !== localId && state.chat.some((message) => message.id === replacement.id);
1679
+ const chat = duplicate ? state.chat.filter((message) => message.id !== localId) : state.chat.map((message, position) => position === index ? replacement : message);
1680
+ commit({ ...state, chat });
1681
+ },
1629
1682
  addReaction(reaction) {
1630
1683
  commit({ ...state, reactions: [...state.reactions, reaction] });
1631
1684
  },
@@ -1716,7 +1769,7 @@ function createMeetStore(initial = {}) {
1716
1769
  commit({ ...state, media: { ...state.media, [kind]: next } });
1717
1770
  },
1718
1771
  reset() {
1719
- commit({ ...EMPTY });
1772
+ commit({ ...EMPTY, effects: state.effects });
1720
1773
  }
1721
1774
  };
1722
1775
  }
@@ -2971,7 +3024,19 @@ var ROOM_EVENTS = {
2971
3024
  * production 2026-09-09 the guest lane showed no indicator at all while the
2972
3025
  * host recorded (MRI-C8).
2973
3026
  */
2974
- recording: "matrx.meet.recording"
3027
+ recording: "matrx.meet.recording",
3028
+ /**
3029
+ * A chat message to everyone was SAVED (Meet wave 2). Payload
3030
+ * `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
3031
+ */
3032
+ chat: "matrx.meet.chat",
3033
+ /**
3034
+ * A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`:
3035
+ * `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the
3036
+ * person concerned; `lower-hands {identities: [...] | "*"}` · `role {identity,
3037
+ * role}` — to everyone.
3038
+ */
3039
+ moderation: "matrx.meet.moderation"
2975
3040
  };
2976
3041
  function isRoomEventTopic(topic) {
2977
3042
  return topic.startsWith(ROOM_EVENT_PREFIX);
@@ -3007,6 +3072,10 @@ function applyRoomEvent(args) {
3007
3072
  return applyEnded(store, payload);
3008
3073
  case ROOM_EVENTS.recording:
3009
3074
  return applyRecording(store, payload);
3075
+ case ROOM_EVENTS.chat:
3076
+ return applyChat(store, payload);
3077
+ case ROOM_EVENTS.moderation:
3078
+ return applyModeration(store, payload, now);
3010
3079
  default:
3011
3080
  return ignored(`${topic} is not known to this version of @ai-matrx/meet`);
3012
3081
  }
@@ -3098,6 +3167,101 @@ function applyEnded(store, payload) {
3098
3167
  store.patch({ meeting: { ...meeting, endedAt } });
3099
3168
  return HANDLED;
3100
3169
  }
3170
+ function chatFromRow(row) {
3171
+ const id = text2(row["id"]);
3172
+ const identity = text2(row["sender_identity"]);
3173
+ const body = text2(row["body"]);
3174
+ if (id === null || identity === null || body === null) return null;
3175
+ return {
3176
+ id,
3177
+ identity: asParticipantIdentity(identity),
3178
+ displayName: text2(row["sender_name"]) ?? "Participant",
3179
+ text: body,
3180
+ at: text2(row["created_at"]) ?? (/* @__PURE__ */ new Date()).toISOString(),
3181
+ // `false` only from a server whose chat table has not landed: the room heard
3182
+ // it, nobody will see it later — and the line says so.
3183
+ saved: row["saved"] !== false
3184
+ };
3185
+ }
3186
+ function applyChat(store, payload) {
3187
+ const row = object(payload["message"]);
3188
+ if (row === null) return ignored("a chat event with no `message` row");
3189
+ const meeting = store.snapshot().meeting;
3190
+ if (meeting !== null && text2(row["meeting_id"]) !== null && row["meeting_id"] !== meeting.id) {
3191
+ return ignored("a chat message for a different meeting");
3192
+ }
3193
+ const message = chatFromRow(row);
3194
+ if (message === null) return ignored("a chat row with no id, sender or body");
3195
+ store.addChat(message);
3196
+ return HANDLED;
3197
+ }
3198
+ function moderationText(kind, payload) {
3199
+ const by = text2(payload["by"]) ?? "The host";
3200
+ switch (kind) {
3201
+ case "muted": {
3202
+ const source = text2(payload["source"]) ?? "microphone";
3203
+ return source === "microphone" ? `${by} muted you.` : source === "camera" ? `${by} turned off your camera.` : `${by} stopped your screen share.`;
3204
+ }
3205
+ case "ask-unmute":
3206
+ return `${by} is asking you to unmute.`;
3207
+ case "removed":
3208
+ return `${by} removed you from this meeting.`;
3209
+ default:
3210
+ return null;
3211
+ }
3212
+ }
3213
+ function applyModeration(store, payload, now) {
3214
+ const snapshot = store.snapshot();
3215
+ const meetingId = text2(payload["meeting_id"]);
3216
+ if (snapshot.meeting !== null && meetingId !== null && meetingId !== snapshot.meeting.id) {
3217
+ return ignored("a moderation event for a different meeting");
3218
+ }
3219
+ const kind = text2(payload["kind"]);
3220
+ switch (kind) {
3221
+ case "lower-hands": {
3222
+ const raw = payload["identities"];
3223
+ const everyone = raw === "*";
3224
+ const named = new Set(
3225
+ Array.isArray(raw) ? raw.filter((value) => typeof value === "string") : []
3226
+ );
3227
+ for (const participant of snapshot.participants) {
3228
+ if (participant.handRaised && (everyone || named.has(participant.identity))) {
3229
+ store.setHand(participant.identity, false);
3230
+ }
3231
+ }
3232
+ return HANDLED;
3233
+ }
3234
+ case "role": {
3235
+ const identity = text2(payload["identity"]);
3236
+ const role = text2(payload["role"]);
3237
+ if (identity === null || role === null || !ROLES.includes(role)) {
3238
+ return ignored("a role event with no identity or an unknown role");
3239
+ }
3240
+ if (identity === snapshot.localIdentity) {
3241
+ store.patch({
3242
+ grants: { ...snapshot.grants, role },
3243
+ moderationNotice: {
3244
+ kind: "role",
3245
+ text: role === "host" ? "You are now the host." : role === "cohost" ? "You are now a co-host." : "You are no longer a co-host.",
3246
+ at: now()
3247
+ }
3248
+ });
3249
+ }
3250
+ return HANDLED;
3251
+ }
3252
+ case "muted":
3253
+ case "ask-unmute":
3254
+ case "removed": {
3255
+ const sentence = moderationText(kind, payload);
3256
+ if (sentence === null) return ignored("a moderation event with no sentence");
3257
+ store.patch({ moderationNotice: { kind, text: sentence, at: now() } });
3258
+ return HANDLED;
3259
+ }
3260
+ default:
3261
+ return ignored(`a moderation kind this version does not know: ${String(kind)}`);
3262
+ }
3263
+ }
3264
+ var ROLES = ["host", "cohost", "participant", "guest"];
3101
3265
 
3102
3266
  // src/core/note-taker.ts
3103
3267
  var NOTE_TAKER_IDENTITY_PREFIX = "agent:note_taker:";
@@ -3162,8 +3326,7 @@ function noteTakerLabel(status) {
3162
3326
  var DATA_TOPICS = {
3163
3327
  hand: "mx.hand",
3164
3328
  reaction: "mx.reaction",
3165
- chat: "mx.chat",
3166
- hostAction: "mx.host"
3329
+ chat: "mx.chat"
3167
3330
  };
3168
3331
  var TRANSCRIPTION_TOPIC = "lk.transcription";
3169
3332
  var TRANSCRIPTION_ATTRIBUTES = {
@@ -3299,7 +3462,37 @@ function createRoomEngine(options) {
3299
3462
  const handlers = {
3300
3463
  participants(list) {
3301
3464
  const joinedAt = new Date(now()).toISOString();
3465
+ const before = new Set(store.snapshot().participants.map((p) => p.identity));
3302
3466
  store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
3467
+ const snapshot = store.snapshot();
3468
+ const newcomer = snapshot.participants.some(
3469
+ (participant) => !participant.isLocal && !before.has(participant.identity)
3470
+ );
3471
+ const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
3472
+ if (newcomer && before.size > 0 && me?.handRaised === true) {
3473
+ void publish(DATA_TOPICS.hand, { raised: true, raisedAt: me.handRaisedAt ?? now() }).catch(
3474
+ () => void 0
3475
+ );
3476
+ }
3477
+ },
3478
+ roomMetadata(metadata) {
3479
+ let spotlight = null;
3480
+ if (metadata.length > 0) {
3481
+ try {
3482
+ const parsed = JSON.parse(metadata);
3483
+ if (typeof parsed === "object" && parsed !== null) {
3484
+ const value = parsed.spotlight;
3485
+ if (typeof value === "string" && value.length > 0) spotlight = asParticipantIdentity(value);
3486
+ }
3487
+ } catch {
3488
+ report(
3489
+ "warn",
3490
+ "meet: the room's metadata is not JSON, so no spotlight was read from it",
3491
+ "Only aidream writes room metadata; check services/meet/moderation.py."
3492
+ );
3493
+ }
3494
+ }
3495
+ if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
3303
3496
  },
3304
3497
  activeSpeakers(identities) {
3305
3498
  store.setSpeaking(identities.map(asParticipantIdentity));
@@ -3403,7 +3596,11 @@ function createRoomEngine(options) {
3403
3596
  const sender = asParticipantIdentity(from ?? "");
3404
3597
  switch (topic) {
3405
3598
  case DATA_TOPICS.hand:
3406
- store.setHand(sender, envelope.raised === true);
3599
+ store.setHand(
3600
+ sender,
3601
+ envelope.raised === true,
3602
+ typeof envelope.raisedAt === "number" ? envelope.raisedAt : now()
3603
+ );
3407
3604
  return;
3408
3605
  case DATA_TOPICS.reaction:
3409
3606
  if (typeof envelope.emoji === "string") {
@@ -3417,12 +3614,14 @@ function createRoomEngine(options) {
3417
3614
  return;
3418
3615
  case DATA_TOPICS.chat:
3419
3616
  if (typeof envelope.text === "string") {
3617
+ const local = store.snapshot().localIdentity;
3420
3618
  store.addChat({
3421
3619
  id: envelope.id ?? `${sender}-${now()}`,
3422
3620
  identity: sender,
3423
3621
  displayName: envelope.name ?? sender,
3424
3622
  text: envelope.text,
3425
- at: envelope.at ?? new Date(now()).toISOString()
3623
+ at: envelope.at ?? new Date(now()).toISOString(),
3624
+ ...typeof envelope.to === "string" && local !== null ? { to: local } : {}
3426
3625
  });
3427
3626
  }
3428
3627
  return;
@@ -3468,6 +3667,24 @@ function createRoomEngine(options) {
3468
3667
  store.patch({ phase: "connected", failure: null });
3469
3668
  store.setLocalTrack("microphone", IDLE_TRACK);
3470
3669
  store.setLocalTrack("camera", IDLE_TRACK);
3670
+ const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
3671
+ store.patch({ effects });
3672
+ if (!effects.noiseSuppression || effects.background !== "none") {
3673
+ try {
3674
+ await applyEffects(created, effects);
3675
+ } catch {
3676
+ }
3677
+ }
3678
+ for (const kind of ["audioinput", "videoinput", "audiooutput"]) {
3679
+ const chosen = options.resolveDevice?.(kind) ?? null;
3680
+ if (chosen === null || chosen === "default") continue;
3681
+ try {
3682
+ await created.switchDevice(kind, chosen);
3683
+ } catch (error) {
3684
+ const normalized = normalizeDeviceError(error, `meet.switchDevice(${kind})`);
3685
+ report("warn", normalized.message, normalized.remedy);
3686
+ }
3687
+ }
3471
3688
  if (request.mode === "video") {
3472
3689
  await enableTrack(created, "camera", true);
3473
3690
  }
@@ -3490,9 +3707,30 @@ function createRoomEngine(options) {
3490
3707
  return state;
3491
3708
  }
3492
3709
  }
3493
- async function publish(topic, body) {
3710
+ async function publish(topic, body, to) {
3494
3711
  if (driver === null) return;
3495
- await driver.publishData(topic, JSON.stringify(body));
3712
+ await driver.publishData(topic, JSON.stringify(body), to);
3713
+ }
3714
+ async function applyEffects(target, effects) {
3715
+ try {
3716
+ await target.setAudioProcessing?.({
3717
+ noiseSuppression: effects.noiseSuppression,
3718
+ echoCancellation: true,
3719
+ autoGainControl: true
3720
+ });
3721
+ } catch (error) {
3722
+ report("warn", `meet: noise suppression could not be applied: ${String(error)}`, "The microphone keeps working without it.");
3723
+ }
3724
+ if (target.setVideoEffect === void 0) return;
3725
+ const effect = effects.background === "blur" ? { kind: "blur" } : effects.background === "image" && effects.backgroundImage !== null ? { kind: "image", imageUrl: effects.backgroundImage } : { kind: "none" };
3726
+ if (effect.kind !== "none" && target.supportsVideoEffects?.() === false) return;
3727
+ try {
3728
+ await target.setVideoEffect(effect);
3729
+ } catch (error) {
3730
+ const normalized = normalizeMeetError(error, "meet.setVideoEffect");
3731
+ report("warn", normalized.message, normalized.remedy);
3732
+ throw normalized;
3733
+ }
3496
3734
  }
3497
3735
  return {
3498
3736
  join(request) {
@@ -3548,7 +3786,7 @@ function createRoomEngine(options) {
3548
3786
  async raiseHand(raised) {
3549
3787
  const local = store.snapshot().localIdentity;
3550
3788
  if (local !== null) store.setHand(local, raised);
3551
- await publish(DATA_TOPICS.hand, { raised });
3789
+ await publish(DATA_TOPICS.hand, { raised, raisedAt: now() });
3552
3790
  },
3553
3791
  async react(emoji) {
3554
3792
  const local = store.snapshot().localIdentity;
@@ -3558,32 +3796,41 @@ function createRoomEngine(options) {
3558
3796
  }
3559
3797
  await publish(DATA_TOPICS.reaction, { id, emoji });
3560
3798
  },
3561
- async sendChat(text3) {
3799
+ async sendPrivateChat(to, text3) {
3562
3800
  const trimmed = text3.trim();
3563
3801
  if (trimmed.length === 0) return;
3564
3802
  const local = store.snapshot().localIdentity;
3565
3803
  const id = `${local ?? "local"}-${now()}`;
3566
3804
  const at = new Date(now()).toISOString();
3567
3805
  if (local !== null) {
3568
- store.addChat({
3569
- id,
3570
- identity: local,
3571
- displayName: identity.displayName,
3572
- text: trimmed,
3573
- at
3574
- });
3806
+ store.addChat({ id, identity: local, displayName: identity.displayName, text: trimmed, at, to });
3575
3807
  }
3576
- await publish(DATA_TOPICS.chat, { id, text: trimmed, at, name: identity.displayName });
3808
+ await publish(
3809
+ DATA_TOPICS.chat,
3810
+ { id, text: trimmed, at, name: identity.displayName, to },
3811
+ [to]
3812
+ );
3577
3813
  },
3578
- async publishHostAction(action) {
3579
- if (!isHostRole(effectiveRole(store.snapshot()))) {
3580
- throw new MeetError(
3581
- "forbidden",
3582
- `meet.hostAction(${action.kind}): this participant is not a host`,
3583
- "Host powers come from the server-minted token metadata. Hide the control instead of rendering a dead button (no dead ends)."
3584
- );
3814
+ async setEffects(next) {
3815
+ const previous = store.snapshot().effects;
3816
+ const merged = { ...previous, ...next };
3817
+ store.patch({ effects: merged });
3818
+ options.effectsStorage?.save(merged);
3819
+ if (driver === null) return;
3820
+ try {
3821
+ await applyEffects(driver, merged);
3822
+ } catch {
3823
+ const reverted = { ...merged, background: previous.background, backgroundImage: previous.backgroundImage };
3824
+ store.patch({ effects: reverted });
3825
+ options.effectsStorage?.save(reverted);
3585
3826
  }
3586
- await publish(DATA_TOPICS.hostAction, { ...action });
3827
+ },
3828
+ supportsVideoEffects() {
3829
+ return driver?.supportsVideoEffects?.() ?? false;
3830
+ },
3831
+ async switchDevice(kind, deviceId2) {
3832
+ if (driver === null) return;
3833
+ await driver.switchDevice(kind, deviceId2);
3587
3834
  },
3588
3835
  pin(who) {
3589
3836
  store.patch({ pinned: who, layout: who === null ? "grid" : "speaker" });
@@ -3614,6 +3861,192 @@ function hostUserIds(participants) {
3614
3861
  return participants.filter((participant) => participant.role === "host" && participant.userId !== null).map((participant) => participant.userId);
3615
3862
  }
3616
3863
 
3864
+ // src/core/effects.ts
3865
+ function coerce(value) {
3866
+ if (typeof value !== "object" || value === null) return DEFAULT_EFFECTS;
3867
+ const row = value;
3868
+ const background = row["background"] === "blur" || row["background"] === "image" ? row["background"] : "none";
3869
+ const backgroundImage = typeof row["backgroundImage"] === "string" ? row["backgroundImage"] : null;
3870
+ return {
3871
+ noiseSuppression: row["noiseSuppression"] !== false,
3872
+ background: background === "image" && backgroundImage === null ? "none" : background,
3873
+ backgroundImage
3874
+ };
3875
+ }
3876
+ function createWebEffectsStorage(key = "mx.meet.effects") {
3877
+ let memory = DEFAULT_EFFECTS;
3878
+ const storage = globalThis.localStorage;
3879
+ return {
3880
+ load() {
3881
+ if (storage === void 0) return memory;
3882
+ try {
3883
+ const raw = storage.getItem(key);
3884
+ return raw === null ? memory : coerce(JSON.parse(raw));
3885
+ } catch {
3886
+ return memory;
3887
+ }
3888
+ },
3889
+ save(effects) {
3890
+ memory = effects;
3891
+ try {
3892
+ storage?.setItem(key, JSON.stringify(effects));
3893
+ } catch {
3894
+ }
3895
+ }
3896
+ };
3897
+ }
3898
+
3899
+ // src/core/in-room.ts
3900
+ var MODERATION_ACTIONS = [
3901
+ "mute",
3902
+ "stop_video",
3903
+ "stop_screen",
3904
+ "mute_all",
3905
+ "ask_unmute",
3906
+ "ask_all_unmute",
3907
+ "spotlight",
3908
+ "lower_hand",
3909
+ "lower_all_hands",
3910
+ "make_cohost",
3911
+ "revoke_cohost",
3912
+ "transfer_host",
3913
+ "rename",
3914
+ "remove",
3915
+ "report"
3916
+ ];
3917
+ function createInRoomClient(options) {
3918
+ const { api, store } = options;
3919
+ const now = options.now ?? (() => Date.now());
3920
+ function meetingId(operation) {
3921
+ const meeting = store.snapshot().meeting;
3922
+ if (meeting === null) {
3923
+ throw new MeetError(
3924
+ "not-found",
3925
+ `${operation}: this room has no meeting record`,
3926
+ "Moderation and saved chat belong to a meeting. An ad-hoc call has neither."
3927
+ );
3928
+ }
3929
+ return meeting.id;
3930
+ }
3931
+ function headers() {
3932
+ const token = options.resolveRoomToken();
3933
+ return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
3934
+ }
3935
+ async function refusal2(response, operation, remedy) {
3936
+ let body = null;
3937
+ try {
3938
+ body = await response.json();
3939
+ } catch {
3940
+ body = null;
3941
+ }
3942
+ const said = readServerRefusal(body);
3943
+ return new MeetError(
3944
+ response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
3945
+ said?.reason ?? `${operation}: the server answered ${response.status}`,
3946
+ said?.remedy !== void 0 && said.remedy.length > 0 ? said.remedy : remedy
3947
+ );
3948
+ }
3949
+ return {
3950
+ async moderate(request) {
3951
+ const operation = `meet.moderate(${request.action})`;
3952
+ const hdrs = headers();
3953
+ const response = await api.request({
3954
+ path: MEET_ROUTES.moderate,
3955
+ method: "POST",
3956
+ operation,
3957
+ body: {
3958
+ meeting_id: meetingId(operation),
3959
+ action: request.action,
3960
+ ...request.identity != null ? { identity: request.identity } : {},
3961
+ ...request.name !== void 0 ? { name: request.name } : {},
3962
+ ...request.reason !== void 0 ? { reason: request.reason } : {}
3963
+ },
3964
+ ...hdrs !== void 0 ? { headers: hdrs } : {}
3965
+ });
3966
+ if (!response.ok) {
3967
+ throw await refusal2(
3968
+ response,
3969
+ operation,
3970
+ "Nothing was changed. The server decides host powers from the meeting itself."
3971
+ );
3972
+ }
3973
+ const payload = await response.json();
3974
+ return {
3975
+ action: request.action,
3976
+ affected: Array.isArray(payload["affected"]) ? payload["affected"].filter((value) => typeof value === "string") : [],
3977
+ message: typeof payload["message"] === "string" ? payload["message"] : "Done."
3978
+ };
3979
+ },
3980
+ async sendChat(text3) {
3981
+ const body = text3.trim();
3982
+ if (body.length === 0) return;
3983
+ const operation = "meet.sendChat";
3984
+ const id = meetingId(operation);
3985
+ const snapshot = store.snapshot();
3986
+ const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
3987
+ const localId = `pending-${now()}`;
3988
+ const pending = {
3989
+ id: localId,
3990
+ identity: snapshot.localIdentity ?? "local",
3991
+ displayName: me?.displayName ?? "You",
3992
+ text: body,
3993
+ at: new Date(now()).toISOString(),
3994
+ saved: false
3995
+ };
3996
+ store.addChat(pending);
3997
+ const hdrs = headers();
3998
+ let response;
3999
+ try {
4000
+ response = await api.request({
4001
+ path: MEET_ROUTES.chat,
4002
+ method: "POST",
4003
+ operation,
4004
+ body: { meeting_id: id, body },
4005
+ ...hdrs !== void 0 ? { headers: hdrs } : {}
4006
+ });
4007
+ } catch (error) {
4008
+ store.settleChat(localId, { failed: "Not sent \u2014 the server could not be reached." });
4009
+ throw error;
4010
+ }
4011
+ if (!response.ok) {
4012
+ const error = await refusal2(response, operation, "Your message was not sent.");
4013
+ store.settleChat(localId, { failed: error.message });
4014
+ throw error;
4015
+ }
4016
+ const saved = chatFromRow(await response.json());
4017
+ if (saved === null) {
4018
+ store.settleChat(localId, { failed: "The server's answer could not be read." });
4019
+ return;
4020
+ }
4021
+ store.settleChat(localId, saved);
4022
+ },
4023
+ async loadChat() {
4024
+ const operation = "meet.loadChat";
4025
+ const id = meetingId(operation);
4026
+ const hdrs = headers();
4027
+ const response = await api.request({
4028
+ path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(id)}`,
4029
+ method: "GET",
4030
+ operation,
4031
+ ...hdrs !== void 0 ? { headers: hdrs } : {}
4032
+ });
4033
+ if (!response.ok) {
4034
+ throw await refusal2(
4035
+ response,
4036
+ operation,
4037
+ "Messages sent before you joined could not be read. New messages still arrive."
4038
+ );
4039
+ }
4040
+ const payload = await response.json();
4041
+ const rows = Array.isArray(payload.messages) ? payload.messages : [];
4042
+ const messages = rows.map(
4043
+ (row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
4044
+ ).filter((message) => message !== null);
4045
+ store.loadChat(messages);
4046
+ }
4047
+ };
4048
+ }
4049
+
3617
4050
  // src/core/livekit-driver.ts
3618
4051
  function mapQuality(quality) {
3619
4052
  switch (quality) {
@@ -3658,11 +4091,48 @@ function snapshotParticipant(participant, isLocal) {
3658
4091
  screenSharing: participant.isScreenShareEnabled
3659
4092
  };
3660
4093
  }
4094
+ var DEFAULT_AUDIO_PROCESSING = {
4095
+ noiseSuppression: true,
4096
+ echoCancellation: true,
4097
+ autoGainControl: true
4098
+ };
4099
+ function videoEffectsSupported() {
4100
+ const scope = globalThis;
4101
+ if (typeof scope.OffscreenCanvas === "undefined" || scope.document === void 0) return false;
4102
+ try {
4103
+ return scope.document.createElement("canvas").getContext("webgl2") !== null;
4104
+ } catch {
4105
+ return false;
4106
+ }
4107
+ }
4108
+ async function applyVideoEffect(target, effect) {
4109
+ const { Track } = await import("livekit-client");
4110
+ const track = target.localParticipant.getTrackPublication(Track.Source.Camera)?.track;
4111
+ if (track === void 0) return;
4112
+ try {
4113
+ if (effect.kind === "none") {
4114
+ await track.stopProcessor();
4115
+ return;
4116
+ }
4117
+ const processors = await import("@livekit/track-processors");
4118
+ const processor = effect.kind === "blur" ? processors.BackgroundBlur(effect.radius ?? 12) : processors.VirtualBackground(effect.imageUrl);
4119
+ await track.setProcessor(processor);
4120
+ } catch (error) {
4121
+ throw new MeetError(
4122
+ "media-unsupported",
4123
+ "The background effect could not start on this camera",
4124
+ "Background effects need a browser with WebGL2 (current Chrome, Edge, Firefox or Safari). Your video continues without the effect.",
4125
+ error
4126
+ );
4127
+ }
4128
+ }
3661
4129
  var encoder = new TextEncoder();
3662
4130
  var decoder = new TextDecoder();
3663
4131
  function createLiveKitDriver(handlers, options = {}) {
3664
4132
  let room = null;
3665
4133
  let projectRoster = null;
4134
+ let audioCapture = DEFAULT_AUDIO_PROCESSING;
4135
+ let videoEffect = { kind: "none" };
3666
4136
  async function load() {
3667
4137
  if (room !== null) return room;
3668
4138
  let module;
@@ -3691,7 +4161,9 @@ function createLiveKitDriver(handlers, options = {}) {
3691
4161
  handlers.participants([local, ...remotes]);
3692
4162
  };
3693
4163
  projectRoster = roster;
3694
- 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) => {
4164
+ 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) => {
4165
+ handlers.roomMetadata?.(metadata ?? "");
4166
+ }).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
3695
4167
  handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
3696
4168
  }).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
3697
4169
  handlers.quality(participant.identity, mapQuality(quality));
@@ -3768,6 +4240,7 @@ function createLiveKitDriver(handlers, options = {}) {
3768
4240
  throw normalizeDeviceError(error, "meet.connect");
3769
4241
  }
3770
4242
  projectRoster?.();
4243
+ handlers.roomMetadata?.(target.metadata ?? "");
3771
4244
  },
3772
4245
  async disconnect() {
3773
4246
  if (room === null) return;
@@ -3782,14 +4255,19 @@ function createLiveKitDriver(handlers, options = {}) {
3782
4255
  },
3783
4256
  async setMicrophoneEnabled(enabled) {
3784
4257
  try {
3785
- await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(enabled);
4258
+ await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(
4259
+ enabled,
4260
+ audioCapture
4261
+ );
3786
4262
  } catch (error) {
3787
4263
  throw normalizeDeviceError(error, "meet.setMicrophoneEnabled");
3788
4264
  }
3789
4265
  },
3790
4266
  async setCameraEnabled(enabled) {
3791
4267
  try {
3792
- await requireRoom("setCameraEnabled").localParticipant.setCameraEnabled(enabled);
4268
+ const target = requireRoom("setCameraEnabled");
4269
+ await target.localParticipant.setCameraEnabled(enabled);
4270
+ if (enabled && videoEffect.kind !== "none") await applyVideoEffect(target, videoEffect);
3793
4271
  } catch (error) {
3794
4272
  throw normalizeDeviceError(error, "meet.setCameraEnabled");
3795
4273
  }
@@ -3801,14 +4279,40 @@ function createLiveKitDriver(handlers, options = {}) {
3801
4279
  throw normalizeDeviceError(error, "meet.setScreenShareEnabled");
3802
4280
  }
3803
4281
  },
3804
- async publishData(topic, payload) {
4282
+ async publishData(topic, payload, to) {
3805
4283
  await requireRoom("publishData").localParticipant.publishData(
3806
4284
  // `reliable` is not optional for a control plane: a dropped
3807
4285
  // "hand lowered" leaves a hand up on every other screen forever.
3808
4286
  encoder.encode(payload),
3809
- { reliable: true, topic }
4287
+ to === void 0 || to.length === 0 ? { reliable: true, topic } : { reliable: true, topic, destinationIdentities: [...to] }
3810
4288
  );
3811
4289
  },
4290
+ async setAudioProcessing(processing) {
4291
+ audioCapture = { ...audioCapture, ...processing };
4292
+ const target = room;
4293
+ if (target === null) return;
4294
+ const { Track } = await import("livekit-client");
4295
+ const publication = target.localParticipant.getTrackPublication(Track.Source.Microphone);
4296
+ const track = publication?.track;
4297
+ if (track === void 0 || publication?.isMuted === true) return;
4298
+ try {
4299
+ await track.restartTrack({
4300
+ ...audioCapture,
4301
+ ...target.getActiveDevice("audioinput") !== void 0 ? { deviceId: target.getActiveDevice("audioinput") } : {}
4302
+ });
4303
+ } catch (error) {
4304
+ throw normalizeDeviceError(error, "meet.setAudioProcessing");
4305
+ }
4306
+ },
4307
+ async setVideoEffect(effect) {
4308
+ videoEffect = effect;
4309
+ const target = room;
4310
+ if (target === null) return;
4311
+ await applyVideoEffect(target, effect);
4312
+ },
4313
+ supportsVideoEffects() {
4314
+ return videoEffectsSupported();
4315
+ },
3812
4316
  attach(identity, kind, element) {
3813
4317
  const target = requireRoom("attach");
3814
4318
  const source = sources?.[kind];
@@ -3842,6 +4346,52 @@ function createLiveKitDriver(handlers, options = {}) {
3842
4346
  }
3843
4347
  };
3844
4348
  }
4349
+ async function createCameraPreview(deviceId2, effect) {
4350
+ let module;
4351
+ try {
4352
+ module = await import("livekit-client");
4353
+ } catch (error) {
4354
+ throw new MeetError(
4355
+ "misconfigured",
4356
+ "livekit-client could not be loaded for the camera preview",
4357
+ "The bundler excluded `livekit-client`; remove it from any `external` list.",
4358
+ error
4359
+ );
4360
+ }
4361
+ let track;
4362
+ try {
4363
+ track = await module.createLocalVideoTrack(deviceId2 === null ? {} : { deviceId: deviceId2 });
4364
+ } catch (error) {
4365
+ throw normalizeDeviceError(error, "meet.cameraPreview");
4366
+ }
4367
+ const target = track;
4368
+ async function apply(next) {
4369
+ if (next.kind === "none") {
4370
+ await target.stopProcessor();
4371
+ return;
4372
+ }
4373
+ const processors = await import("@livekit/track-processors");
4374
+ await target.setProcessor(
4375
+ next.kind === "blur" ? processors.BackgroundBlur(next.radius ?? 12) : processors.VirtualBackground(next.imageUrl)
4376
+ );
4377
+ }
4378
+ if (effect.kind !== "none" && videoEffectsSupported()) {
4379
+ try {
4380
+ await apply(effect);
4381
+ } catch {
4382
+ }
4383
+ }
4384
+ return {
4385
+ attach(element) {
4386
+ track.attach(element);
4387
+ },
4388
+ setEffect: apply,
4389
+ stop() {
4390
+ track.stop();
4391
+ track.detach();
4392
+ }
4393
+ };
4394
+ }
3845
4395
 
3846
4396
  // src/react/provider.tsx
3847
4397
  import { jsx } from "react/jsx-runtime";
@@ -3951,7 +4501,9 @@ function MeetRuntime(props) {
3951
4501
  avatarUrl: props.avatarUrl ?? null,
3952
4502
  deviceId: deviceId()
3953
4503
  };
3954
- const store = createMeetStore();
4504
+ const effectsStorage = createWebEffectsStorage();
4505
+ const store = createMeetStore({ effects: effectsStorage.load() });
4506
+ const devicesHolder = { current: null };
3955
4507
  const api = createMeetApi({
3956
4508
  baseUrl,
3957
4509
  organizationId: identity.organizationId,
@@ -3970,7 +4522,9 @@ function MeetRuntime(props) {
3970
4522
  identity,
3971
4523
  resolveMeeting: (slug) => repository.meetingBySlug(slug),
3972
4524
  driverFactory: (handlers) => (driverRef.current ?? createLiveKitDriver)(handlers),
3973
- onDiagnostic: report
4525
+ onDiagnostic: report,
4526
+ effectsStorage,
4527
+ resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null
3974
4528
  });
3975
4529
  const meetings = createMeetingSession({
3976
4530
  manager,
@@ -3985,16 +4539,36 @@ function MeetRuntime(props) {
3985
4539
  void engine.rejoin();
3986
4540
  }
3987
4541
  });
3988
- const ai = createMeetAi({
3989
- api,
3990
- store,
3991
- resolveRoomToken: () => {
3992
- const roomName = store.snapshot().meeting?.roomName;
3993
- if (roomName === void 0) return null;
3994
- const active = engine.token();
3995
- if (active?.roomName === roomName && active.token.length > 0) return active.token;
3996
- return tokens.remembered(roomName)?.token ?? null;
4542
+ const resolveRoomToken = () => {
4543
+ const roomName = store.snapshot().meeting?.roomName;
4544
+ if (roomName === void 0) return null;
4545
+ const active = engine.token();
4546
+ if (active?.roomName === roomName && active.token.length > 0) return active.token;
4547
+ return tokens.remembered(roomName)?.token ?? null;
4548
+ };
4549
+ const ai = createMeetAi({ api, store, resolveRoomToken });
4550
+ const inRoom = createInRoomClient({ api, store, resolveRoomToken });
4551
+ let chatReadFor = null;
4552
+ store.subscribe(() => {
4553
+ const snapshot = store.snapshot();
4554
+ if (snapshot.phase !== "connected" || snapshot.meeting === null) {
4555
+ if (snapshot.phase === "left" || snapshot.phase === "idle") chatReadFor = null;
4556
+ return;
3997
4557
  }
4558
+ if (chatReadFor === snapshot.meeting.id) return;
4559
+ chatReadFor = snapshot.meeting.id;
4560
+ void inRoom.loadChat().catch((error) => {
4561
+ const normalized = error;
4562
+ store.patch({
4563
+ chatLoaded: true,
4564
+ chatHistoryError: normalized.remedy ?? "Messages sent before you joined could not be read. New messages still arrive."
4565
+ });
4566
+ report({
4567
+ level: "warn",
4568
+ message: normalized.message ?? "meet: the saved chat could not be read",
4569
+ ...normalized.remedy !== void 0 ? { remedy: normalized.remedy } : {}
4570
+ });
4571
+ });
3998
4572
  });
3999
4573
  const recording = createRecordingController({
4000
4574
  api,
@@ -4016,6 +4590,7 @@ function MeetRuntime(props) {
4016
4590
  },
4017
4591
  storage: createWebDeviceStorage()
4018
4592
  });
4593
+ devicesHolder.current = devices;
4019
4594
  const calls = identity.userId === null ? null : createCallCenter({
4020
4595
  manager,
4021
4596
  repository,
@@ -4030,6 +4605,7 @@ function MeetRuntime(props) {
4030
4605
  engine,
4031
4606
  calls,
4032
4607
  meetings,
4608
+ inRoom,
4033
4609
  recording,
4034
4610
  devices,
4035
4611
  repository,
@@ -4151,7 +4727,8 @@ function computeStage(args) {
4151
4727
  const ordered = orderParticipants(args.participants, args.activeSpeakers);
4152
4728
  const sharer = ordered.find((participant) => participant.screenSharing) ?? null;
4153
4729
  const pinnedParticipant = args.pinned === null ? null : ordered.find((participant) => participant.identity === args.pinned) ?? null;
4154
- const feature = sharer !== null ? sharer : args.layout === "grid" ? null : pinnedParticipant ?? ordered[0] ?? null;
4730
+ const spotlit = args.spotlight == null ? null : ordered.find((participant) => participant.identity === args.spotlight) ?? null;
4731
+ const feature = sharer !== null ? sharer : pinnedParticipant !== null && args.layout !== "grid" ? pinnedParticipant : spotlit !== null ? spotlit : args.layout === "grid" ? null : ordered[0] ?? null;
4155
4732
  const rest = feature === null ? ordered : ordered.filter((participant) => participant.identity !== feature.identity);
4156
4733
  const pageCount = Math.max(1, Math.ceil(rest.length / pageSize));
4157
4734
  const page = Math.min(Math.max(0, args.page ?? 0), pageCount - 1);
@@ -4630,7 +5207,14 @@ function useRoom() {
4630
5207
  [engine]
4631
5208
  ),
4632
5209
  sendChat: useCallback2(
4633
- async (text3) => requireEngine(engine, "sendChat").sendChat(text3),
5210
+ async (text3) => {
5211
+ if (host === null) throw noRuntime("sendChat");
5212
+ await host.inRoom.sendChat(text3);
5213
+ },
5214
+ [host]
5215
+ ),
5216
+ sendPrivateChat: useCallback2(
5217
+ async (to, text3) => requireEngine(engine, "sendPrivateChat").sendPrivateChat(to, text3),
4634
5218
  [engine]
4635
5219
  ),
4636
5220
  pin: useCallback2(
@@ -4651,6 +5235,7 @@ function useStage(pageSize = DEFAULT_PAGE_SIZE) {
4651
5235
  participants: snapshot?.participants ?? [],
4652
5236
  layout: snapshot?.layout ?? "grid",
4653
5237
  pinned: snapshot?.pinned ?? null,
5238
+ spotlight: snapshot?.spotlight ?? null,
4654
5239
  activeSpeakers: snapshot?.activeSpeakers ?? [],
4655
5240
  pageSize,
4656
5241
  page
@@ -4983,33 +5568,160 @@ function useHostControls() {
4983
5568
  const host = useMeetHost();
4984
5569
  const snapshot = useMeetSnapshot();
4985
5570
  const role = useRole();
4986
- const engine = host?.engine ?? null;
4987
5571
  const me = snapshot?.participants.find(
4988
5572
  (participant) => participant.identity === snapshot.localIdentity
4989
5573
  );
4990
5574
  const actor = me === void 0 ? void 0 : { ...me, role };
5575
+ const [busy, setBusy] = useState2(false);
5576
+ const inRoom = host?.inRoom ?? null;
5577
+ const store = host?.store ?? null;
5578
+ const notice = snapshot?.actionNotice ?? null;
5579
+ const setNotice = useCallback2(
5580
+ (next) => store?.patch({ actionNotice: next === null ? null : { ...next, at: Date.now() } }),
5581
+ [store]
5582
+ );
5583
+ const run = useCallback2(
5584
+ async (request) => {
5585
+ if (inRoom === null) throw noRuntime(`moderate(${request.action})`);
5586
+ setBusy(true);
5587
+ try {
5588
+ const result = await inRoom.moderate(request);
5589
+ setNotice({ text: result.message, failed: false });
5590
+ return result;
5591
+ } catch (thrown) {
5592
+ const error = thrown;
5593
+ setNotice({
5594
+ text: [error.message, error.remedy].filter(Boolean).join(" "),
5595
+ failed: true
5596
+ });
5597
+ return null;
5598
+ } finally {
5599
+ setBusy(false);
5600
+ }
5601
+ },
5602
+ [inRoom, setNotice]
5603
+ );
4991
5604
  return {
4992
5605
  isHost: isHostRole(role),
5606
+ isOwner: role === "host",
5607
+ notice,
5608
+ busy,
5609
+ clearNotice: useCallback2(() => setNotice(null), [setNotice]),
4993
5610
  canModerate: useCallback2(
4994
5611
  (target) => canModerateParticipant(actor, target),
4995
5612
  [actor]
4996
5613
  ),
5614
+ run,
4997
5615
  mute: useCallback2(
4998
- async (target) => requireEngine(engine, "muteParticipant").publishHostAction({
4999
- kind: "mute",
5000
- identity: target.identity
5001
- }),
5002
- [engine]
5616
+ async (target) => {
5617
+ await run({ action: "mute", identity: target.identity });
5618
+ },
5619
+ [run]
5003
5620
  ),
5004
5621
  remove: useCallback2(
5005
- async (target) => requireEngine(engine, "removeParticipant").publishHostAction({
5006
- kind: "remove",
5007
- identity: target.identity
5008
- }),
5622
+ async (target) => {
5623
+ await run({ action: "remove", identity: target.identity });
5624
+ },
5625
+ [run]
5626
+ )
5627
+ };
5628
+ }
5629
+ function useHandQueue() {
5630
+ const snapshot = useMeetSnapshot();
5631
+ return useMemo2(
5632
+ () => (snapshot?.participants ?? []).filter((participant) => participant.handRaised).sort((a, b) => (a.handRaisedAt ?? 0) - (b.handRaisedAt ?? 0)),
5633
+ [snapshot?.participants]
5634
+ );
5635
+ }
5636
+ function useMediaEffects() {
5637
+ const host = useMeetHost();
5638
+ const snapshot = useMeetSnapshot();
5639
+ const engine = host?.engine ?? null;
5640
+ return {
5641
+ effects: snapshot?.effects ?? DEFAULT_EFFECTS,
5642
+ backgroundsSupported: engine?.supportsVideoEffects() ?? false,
5643
+ set: useCallback2(
5644
+ async (next) => requireEngine(engine, "setEffects").setEffects(next),
5009
5645
  [engine]
5010
5646
  )
5011
5647
  };
5012
5648
  }
5649
+ function useModerationNotice() {
5650
+ const host = useMeetHost();
5651
+ const snapshot = useMeetSnapshot();
5652
+ return {
5653
+ notice: snapshot?.moderationNotice ?? null,
5654
+ dismiss: useCallback2(() => host?.store.patch({ moderationNotice: null }), [host])
5655
+ };
5656
+ }
5657
+ function useAudioLevel(stream) {
5658
+ const [level, setLevel] = useState2(0);
5659
+ useEffect2(() => {
5660
+ if (stream === null || stream.getAudioTracks().length === 0) {
5661
+ setLevel(0);
5662
+ return void 0;
5663
+ }
5664
+ const Context = globalThis.AudioContext ?? globalThis.webkitAudioContext;
5665
+ if (Context === void 0) return void 0;
5666
+ const context = new Context();
5667
+ const source = context.createMediaStreamSource(stream);
5668
+ const analyser = context.createAnalyser();
5669
+ analyser.fftSize = 512;
5670
+ source.connect(analyser);
5671
+ const samples = new Uint8Array(analyser.fftSize);
5672
+ let frame = 0;
5673
+ const tick = () => {
5674
+ analyser.getByteTimeDomainData(samples);
5675
+ let peak = 0;
5676
+ for (const sample of samples) peak = Math.max(peak, Math.abs(sample - 128) / 128);
5677
+ setLevel((previous) => Math.abs(previous - peak) > 0.02 ? peak : previous);
5678
+ frame = requestAnimationFrame(tick);
5679
+ };
5680
+ frame = requestAnimationFrame(tick);
5681
+ return () => {
5682
+ cancelAnimationFrame(frame);
5683
+ source.disconnect();
5684
+ void context.close();
5685
+ };
5686
+ }, [stream]);
5687
+ return level;
5688
+ }
5689
+ var MEET_SHORTCUTS = [
5690
+ { id: "mic", keys: "\u2318/Ctrl + D", label: "Mute or unmute your microphone" },
5691
+ { id: "camera", keys: "\u2318/Ctrl + E", label: "Turn your camera on or off" },
5692
+ { id: "hand", keys: "\u2318/Ctrl + Alt + H", label: "Raise or lower your hand" },
5693
+ { id: "chat", keys: "\u2318/Ctrl + Alt + C", label: "Open or close chat" },
5694
+ { id: "people", keys: "\u2318/Ctrl + Alt + P", label: "Open or close people" },
5695
+ { id: "help", keys: "Shift + ?", label: "Show keyboard shortcuts" }
5696
+ ];
5697
+ function useMeetShortcuts(handlers) {
5698
+ const ref = useRef2(handlers);
5699
+ ref.current = handlers;
5700
+ useEffect2(() => {
5701
+ const target = globalThis.window;
5702
+ if (target === void 0) return void 0;
5703
+ const onKey = (event) => {
5704
+ const element = event.target;
5705
+ const typing = element !== null && (element.isContentEditable || element.tagName === "INPUT" || element.tagName === "TEXTAREA" || element.tagName === "SELECT");
5706
+ const mod = event.metaKey || event.ctrlKey;
5707
+ const key = event.key.toLowerCase();
5708
+ let id = null;
5709
+ if (mod && !event.altKey && !event.shiftKey && key === "d") id = "mic";
5710
+ else if (mod && !event.altKey && !event.shiftKey && key === "e") id = "camera";
5711
+ else if (mod && event.altKey && (event.code === "KeyH" || key === "h")) id = "hand";
5712
+ else if (mod && event.altKey && (event.code === "KeyC" || key === "c")) id = "chat";
5713
+ else if (mod && event.altKey && (event.code === "KeyP" || key === "p")) id = "people";
5714
+ else if (!mod && !typing && event.key === "?") id = "help";
5715
+ if (id === null) return;
5716
+ const handler = ref.current[id];
5717
+ if (handler === void 0) return;
5718
+ event.preventDefault();
5719
+ handler();
5720
+ };
5721
+ target.addEventListener("keydown", onKey);
5722
+ return () => target.removeEventListener("keydown", onKey);
5723
+ }, []);
5724
+ }
5013
5725
  function useMeeting() {
5014
5726
  return useMeetSnapshot()?.meeting ?? null;
5015
5727
  }
@@ -5185,7 +5897,7 @@ function useGuestRecordAccess(meeting) {
5185
5897
  }
5186
5898
 
5187
5899
  // src/react/components.tsx
5188
- import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
5900
+ import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
5189
5901
  import { ErrorBox } from "@ai-matrx/design-system";
5190
5902
  import { avatarPaletteIndex, formatDurationMs as formatDurationMs2, getInitials } from "@ai-matrx/kit/format";
5191
5903
 
@@ -5356,12 +6068,66 @@ function WifiLowIcon(props) {
5356
6068
  /* @__PURE__ */ jsx2("path", { d: "M8.5 16.5a5 5 0 0 1 7 0" })
5357
6069
  ] });
5358
6070
  }
5359
-
5360
- // src/react/components.tsx
5361
- import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
5362
- function InertNotice({ what }) {
5363
- return /* @__PURE__ */ jsxs2("div", { className: "mx-meet__empty", role: "status", children: [
5364
- /* @__PURE__ */ jsxs2("p", { className: "mx-meet__empty-title", children: [
6071
+ function SmileIcon(props) {
6072
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6073
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "10" }),
6074
+ /* @__PURE__ */ jsx2("path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }),
6075
+ /* @__PURE__ */ jsx2("line", { x1: "9", x2: "9.01", y1: "9", y2: "9" }),
6076
+ /* @__PURE__ */ jsx2("line", { x1: "15", x2: "15.01", y1: "9", y2: "9" })
6077
+ ] });
6078
+ }
6079
+ function MoreIcon(props) {
6080
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6081
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "1" }),
6082
+ /* @__PURE__ */ jsx2("circle", { cx: "19", cy: "12", r: "1" }),
6083
+ /* @__PURE__ */ jsx2("circle", { cx: "5", cy: "12", r: "1" })
6084
+ ] });
6085
+ }
6086
+ function SearchIcon(props) {
6087
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6088
+ /* @__PURE__ */ jsx2("circle", { cx: "11", cy: "11", r: "8" }),
6089
+ /* @__PURE__ */ jsx2("path", { d: "m21 21-4.3-4.3" })
6090
+ ] });
6091
+ }
6092
+ function KeyboardIcon(props) {
6093
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6094
+ /* @__PURE__ */ jsx2("path", { d: "M10 8h.01M12 12h.01M14 8h.01M16 12h.01M18 8h.01M6 8h.01M7 16h10M8 12h.01" }),
6095
+ /* @__PURE__ */ jsx2("rect", { width: "20", height: "16", x: "2", y: "4", rx: "2" })
6096
+ ] });
6097
+ }
6098
+ function SpotlightIcon(props) {
6099
+ 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" }) });
6100
+ }
6101
+ function SignalIcon({ bars, ...props }) {
6102
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6103
+ /* @__PURE__ */ jsx2("path", { d: "M4 20v-2", opacity: bars >= 1 ? 1 : 0.3 }),
6104
+ /* @__PURE__ */ jsx2("path", { d: "M10 20v-6", opacity: bars >= 2 ? 1 : 0.3 }),
6105
+ /* @__PURE__ */ jsx2("path", { d: "M16 20V9", opacity: bars >= 3 ? 1 : 0.3 }),
6106
+ /* @__PURE__ */ jsx2("path", { d: "M22 20V4", opacity: bars >= 3 ? 1 : 0.3 })
6107
+ ] });
6108
+ }
6109
+ function SpeakerIcon(props) {
6110
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6111
+ /* @__PURE__ */ jsx2("path", { d: "M11 5 6 9H2v6h4l5 4V5Z" }),
6112
+ /* @__PURE__ */ jsx2("path", { d: "M15.54 8.46a5 5 0 0 1 0 7.07" }),
6113
+ /* @__PURE__ */ jsx2("path", { d: "M19.07 4.93a10 10 0 0 1 0 14.14" })
6114
+ ] });
6115
+ }
6116
+ function FlagIcon(props) {
6117
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6118
+ /* @__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" }),
6119
+ /* @__PURE__ */ jsx2("line", { x1: "4", x2: "4", y1: "22", y2: "15" })
6120
+ ] });
6121
+ }
6122
+ function PencilIcon(props) {
6123
+ 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" }) });
6124
+ }
6125
+
6126
+ // src/react/components.tsx
6127
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
6128
+ function InertNotice({ what }) {
6129
+ return /* @__PURE__ */ jsxs2("div", { className: "mx-meet__empty", role: "status", children: [
6130
+ /* @__PURE__ */ jsxs2("p", { className: "mx-meet__empty-title", children: [
5365
6131
  what,
5366
6132
  " is not ready yet"
5367
6133
  ] }),
@@ -5456,12 +6222,17 @@ function ParticipantTile({
5456
6222
  featured = false
5457
6223
  }) {
5458
6224
  const host = useMeetHost();
5459
- const canModerate2 = useCanModerate(participant);
6225
+ const snapshot = useMeetSnapshot();
6226
+ const queue = useHandQueue();
6227
+ const [menu, setMenu] = useState3(false);
5460
6228
  const videoRef = useTrack(
5461
6229
  participant.identity,
5462
6230
  participant.screenSharing ? "screen" : "camera"
5463
6231
  );
5464
6232
  const showVideo = participant.cameraEnabled || participant.screenSharing;
6233
+ const pinnedHere = snapshot?.pinned === participant.identity;
6234
+ const spotlit = snapshot?.spotlight === participant.identity;
6235
+ const handPosition = participant.handRaised ? queue.findIndex((entry) => entry.identity === participant.identity) + 1 : 0;
5465
6236
  return /* @__PURE__ */ jsxs2(
5466
6237
  "div",
5467
6238
  {
@@ -5482,44 +6253,59 @@ function ParticipantTile({
5482
6253
  muted: participant.isLocal
5483
6254
  }
5484
6255
  ) : /* @__PURE__ */ jsx3("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ jsx3(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: featured ? 96 : 56 }) }),
6256
+ participant.handRaised ? /* @__PURE__ */ jsxs2("span", { className: "mx-meet__tile-hand", title: `Hand raised \xB7 ${ordinal(handPosition)} in line`, children: [
6257
+ /* @__PURE__ */ jsx3(HandIcon, { width: 14, height: 14 }),
6258
+ handPosition > 0 ? handPosition : null
6259
+ ] }) : null,
5485
6260
  /* @__PURE__ */ jsxs2("div", { className: "mx-meet__tile-bar", children: [
5486
6261
  /* @__PURE__ */ jsxs2("span", { className: "mx-meet__tile-name", children: [
6262
+ spotlit ? /* @__PURE__ */ jsx3("span", { title: "Spotlighted for everyone", className: "mx-meet__tile-flag", children: /* @__PURE__ */ jsx3(SpotlightIcon, { width: 12, height: 12 }) }) : null,
5487
6263
  participant.displayName,
5488
6264
  participant.isLocal ? " (you)" : "",
5489
6265
  participant.isAgent ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__badge", children: "AI" }) : null,
5490
6266
  /* @__PURE__ */ jsx3(RoleBadge, { role: participant.role })
5491
6267
  ] }),
5492
6268
  !participant.micEnabled ? /* @__PURE__ */ jsx3(MicOffIcon, { width: 14, height: 14 }) : null,
5493
- participant.handRaised ? /* @__PURE__ */ jsx3(HandIcon, { width: 14, height: 14 }) : null,
5494
- /* @__PURE__ */ jsx3(ConnectionDot, { quality: participant.quality })
6269
+ /* @__PURE__ */ jsx3(QualityBars, { quality: participant.quality })
5495
6270
  ] }),
5496
- /* @__PURE__ */ jsxs2("div", { className: "mx-meet__tile-actions", children: [
6271
+ /* @__PURE__ */ jsxs2("div", { className: menu ? "mx-meet__tile-actions mx-meet__tile-actions--open" : "mx-meet__tile-actions", children: [
5497
6272
  /* @__PURE__ */ jsx3(
5498
6273
  ControlButton,
5499
6274
  {
5500
- label: featured ? "Unpin" : `Pin ${participant.displayName}`,
5501
- onClick: () => host?.engine.pin(featured ? null : participant.identity),
6275
+ label: pinnedHere ? "Unpin" : `Pin ${participant.displayName} for me`,
6276
+ active: pinnedHere,
6277
+ onClick: () => host?.engine.pin(pinnedHere ? null : participant.identity),
5502
6278
  children: /* @__PURE__ */ jsx3(PinIcon, { width: 16, height: 16 })
5503
6279
  }
5504
6280
  ),
5505
- canModerate2 ? /* @__PURE__ */ jsx3(
5506
- ControlButton,
5507
- {
5508
- label: `Mute ${participant.displayName}`,
5509
- onClick: () => {
5510
- void host?.engine.publishHostAction({
5511
- kind: "mute",
5512
- identity: participant.identity
5513
- });
5514
- },
5515
- children: /* @__PURE__ */ jsx3(MicOffIcon, { width: 16, height: 16 })
5516
- }
5517
- ) : null
6281
+ /* @__PURE__ */ jsxs2("span", { className: "mx-meet__anchor", children: [
6282
+ /* @__PURE__ */ jsx3(
6283
+ ControlButton,
6284
+ {
6285
+ label: `More for ${participant.displayName}`,
6286
+ active: menu,
6287
+ onClick: () => setMenu(!menu),
6288
+ children: /* @__PURE__ */ jsx3(MoreIcon, { width: 16, height: 16 })
6289
+ }
6290
+ ),
6291
+ menu ? /* @__PURE__ */ jsx3(PersonMenu, { participant, onClose: () => setMenu(false), placement: "down" }) : null
6292
+ ] })
5518
6293
  ] })
5519
6294
  ]
5520
6295
  }
5521
6296
  );
5522
6297
  }
6298
+ function ordinal(position) {
6299
+ if (position <= 0) return "";
6300
+ const suffix = position % 100 >= 11 && position % 100 <= 13 ? "th" : { 1: "st", 2: "nd", 3: "rd" }[position % 10] ?? "th";
6301
+ return `${position}${suffix}`;
6302
+ }
6303
+ function QualityBars({ quality }) {
6304
+ if (quality === "unknown") return null;
6305
+ const bars = quality === "excellent" ? 3 : quality === "good" ? 2 : quality === "poor" ? 1 : 0;
6306
+ const label = quality === "lost" ? "Connection lost" : quality === "poor" ? "Poor connection" : `Connection ${quality}`;
6307
+ 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 }) });
6308
+ }
5523
6309
  function Stage({ pageSize }) {
5524
6310
  const stage = useStage(pageSize);
5525
6311
  const columns = gridColumns(stage.tiles.length);
@@ -5548,22 +6334,52 @@ function Stage({ pageSize }) {
5548
6334
  ] }) : null
5549
6335
  ] });
5550
6336
  }
6337
+ var MEET_REACTIONS = ["\u{1F496}", "\u{1F44D}", "\u{1F389}", "\u{1F44F}", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F914}", "\u{1F44E}"];
5551
6338
  function ControlBar({ onLeave }) {
5552
6339
  const room = useRoom();
5553
6340
  const recording = useRecording();
5554
6341
  const [panel, setPanel] = useState3("none");
6342
+ const [chatTo, setChatTo] = useState3(null);
6343
+ const [popover, setPopover] = useState3("none");
6344
+ const [shortcuts, setShortcuts] = useState3(false);
5555
6345
  const snapshot = useMeetSnapshot();
5556
6346
  const lobby = useLobby();
6347
+ const controls = useHostControls();
6348
+ const queue = useHandQueue();
5557
6349
  const micUnavailable = useLocalMedia().microphone.unavailable;
6350
+ const unread = useUnreadChat(panel === "chat");
6351
+ const toggle = (next) => setPanel(panel === next ? "none" : next);
6352
+ useMeetShortcuts({
6353
+ mic: () => void room.toggleMicrophone(),
6354
+ camera: () => void room.toggleCamera(),
6355
+ hand: () => void room.toggleHand(),
6356
+ chat: () => toggle("chat"),
6357
+ people: () => toggle("people"),
6358
+ help: () => setShortcuts(true)
6359
+ });
6360
+ const hasSpotlight = snapshot?.spotlight !== null && snapshot?.spotlight !== void 0;
5558
6361
  return /* @__PURE__ */ jsxs2(Fragment, { children: [
5559
- panel === "chat" ? /* @__PURE__ */ jsx3(ChatPanel, { onClose: () => setPanel("none") }) : null,
5560
- panel === "people" ? /* @__PURE__ */ jsx3(PeoplePanel, { onClose: () => setPanel("none") }) : null,
6362
+ /* @__PURE__ */ jsx3(MeetingOverlays, {}),
6363
+ shortcuts ? /* @__PURE__ */ jsx3(ShortcutsSheet, { onClose: () => setShortcuts(false) }) : null,
6364
+ panel === "chat" ? /* @__PURE__ */ jsx3(ChatPanel, { onClose: () => setPanel("none"), to: chatTo, onTo: setChatTo }) : null,
6365
+ panel === "people" ? /* @__PURE__ */ jsx3(
6366
+ PeoplePanel,
6367
+ {
6368
+ onClose: () => setPanel("none"),
6369
+ onMessage: (identity) => {
6370
+ setChatTo(identity);
6371
+ setPanel("chat");
6372
+ }
6373
+ }
6374
+ ) : null,
5561
6375
  panel === "ai" ? /* @__PURE__ */ jsx3(AiPanel, { onClose: () => setPanel("none") }) : null,
6376
+ panel === "settings" ? /* @__PURE__ */ jsx3(SettingsPanel, { onClose: () => setPanel("none") }) : null,
6377
+ controls.notice !== null && panel !== "people" ? /* @__PURE__ */ jsx3(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
5562
6378
  /* @__PURE__ */ jsxs2("div", { className: "mx-meet__controls", children: [
5563
6379
  /* @__PURE__ */ jsx3(
5564
6380
  ControlButton,
5565
6381
  {
5566
- label: micUnavailable !== null ? `Unmute \u2014 ${micUnavailable.remedy}` : room.local?.micEnabled === true ? "Mute" : "Unmute",
6382
+ label: micUnavailable !== null ? `Unmute \u2014 ${micUnavailable.remedy}` : room.local?.micEnabled === true ? "Mute (\u2318D)" : "Unmute (\u2318D)",
5567
6383
  active: room.local?.micEnabled === true,
5568
6384
  onClick: () => void room.toggleMicrophone(),
5569
6385
  children: room.local?.micEnabled === true ? /* @__PURE__ */ jsx3(MicIcon, {}) : /* @__PURE__ */ jsx3(MicOffIcon, {})
@@ -5572,7 +6388,7 @@ function ControlBar({ onLeave }) {
5572
6388
  /* @__PURE__ */ jsx3(
5573
6389
  ControlButton,
5574
6390
  {
5575
- label: room.local?.cameraEnabled === true ? "Stop video" : "Start video",
6391
+ label: room.local?.cameraEnabled === true ? "Stop video (\u2318E)" : "Start video (\u2318E)",
5576
6392
  active: room.local?.cameraEnabled === true,
5577
6393
  onClick: () => void room.toggleCamera(),
5578
6394
  children: room.local?.cameraEnabled === true ? /* @__PURE__ */ jsx3(CameraIcon, {}) : /* @__PURE__ */ jsx3(CameraOffIcon, {})
@@ -5587,31 +6403,152 @@ function ControlBar({ onLeave }) {
5587
6403
  children: /* @__PURE__ */ jsx3(ScreenShareIcon, {})
5588
6404
  }
5589
6405
  ),
6406
+ /* @__PURE__ */ jsxs2("span", { className: "mx-meet__anchor", children: [
6407
+ /* @__PURE__ */ jsx3(
6408
+ ControlButton,
6409
+ {
6410
+ label: "Send a reaction",
6411
+ active: popover === "reactions",
6412
+ onClick: () => setPopover(popover === "reactions" ? "none" : "reactions"),
6413
+ children: /* @__PURE__ */ jsx3(SmileIcon, {})
6414
+ }
6415
+ ),
6416
+ 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(
6417
+ "button",
6418
+ {
6419
+ type: "button",
6420
+ className: "mx-meet__emoji",
6421
+ "aria-label": `React ${emoji}`,
6422
+ onClick: () => void room.react(emoji),
6423
+ children: emoji
6424
+ },
6425
+ emoji
6426
+ )) }) }) : null
6427
+ ] }),
5590
6428
  /* @__PURE__ */ jsx3(
5591
6429
  ControlButton,
5592
6430
  {
5593
- label: room.local?.handRaised === true ? "Lower hand" : "Raise hand",
6431
+ label: room.local?.handRaised === true ? "Lower hand (\u2318\u2325H)" : "Raise hand (\u2318\u2325H)",
5594
6432
  active: room.local?.handRaised === true,
5595
6433
  onClick: () => void room.toggleHand(),
5596
6434
  children: /* @__PURE__ */ jsx3(HandIcon, {})
5597
6435
  }
5598
6436
  ),
5599
- /* @__PURE__ */ jsx3(ControlButton, { label: "Chat", active: panel === "chat", onClick: () => setPanel(panel === "chat" ? "none" : "chat"), children: /* @__PURE__ */ jsx3(ChatIcon, {}) }),
6437
+ /* @__PURE__ */ jsxs2(ControlButton, { label: "Chat (\u2318\u2325C)", active: panel === "chat", onClick: () => toggle("chat"), children: [
6438
+ /* @__PURE__ */ jsx3(ChatIcon, {}),
6439
+ unread > 0 ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__dot", "aria-hidden": "true" }) : null
6440
+ ] }),
5600
6441
  /* @__PURE__ */ jsxs2(
5601
6442
  ControlButton,
5602
6443
  {
5603
6444
  label: lobby.waiting.length > 0 ? `People \u2014 ${lobby.waiting.length} waiting` : `People \u2014 ${(snapshot?.participants ?? []).length} in the meeting`,
5604
6445
  active: panel === "people",
5605
- onClick: () => setPanel(panel === "people" ? "none" : "people"),
6446
+ onClick: () => toggle("people"),
5606
6447
  children: [
5607
6448
  /* @__PURE__ */ jsx3(PeopleIcon, {}),
5608
- lobby.waiting.length > 0 ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__dot", "aria-hidden": "true" }) : null
6449
+ lobby.waiting.length > 0 || controls.isHost && queue.length > 0 ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__dot", "aria-hidden": "true" }) : null
5609
6450
  ]
5610
6451
  }
5611
6452
  ),
5612
- /* @__PURE__ */ jsx3(AiControl, { active: panel === "ai", onToggle: () => setPanel(panel === "ai" ? "none" : "ai") }),
6453
+ /* @__PURE__ */ jsx3(AiControl, { active: panel === "ai", onToggle: () => toggle("ai") }),
5613
6454
  recording.canStart ? /* @__PURE__ */ jsx3(ControlButton, { label: "Start recording", onClick: () => void recording.start(), children: /* @__PURE__ */ jsx3(RecordIcon, {}) }) : null,
5614
6455
  recording.canStop ? /* @__PURE__ */ jsx3(ControlButton, { label: "Stop recording", active: true, onClick: () => void recording.stop(), children: /* @__PURE__ */ jsx3(RecordIcon, {}) }) : null,
6456
+ /* @__PURE__ */ jsxs2("span", { className: "mx-meet__anchor", children: [
6457
+ /* @__PURE__ */ jsx3(
6458
+ ControlButton,
6459
+ {
6460
+ label: "More options",
6461
+ active: popover === "more" || panel === "settings",
6462
+ onClick: () => setPopover(popover === "more" ? "none" : "more"),
6463
+ children: /* @__PURE__ */ jsx3(MoreIcon, {})
6464
+ }
6465
+ ),
6466
+ popover === "more" ? /* @__PURE__ */ jsx3(Popover, { label: "More options", onClose: () => setPopover("none"), children: /* @__PURE__ */ jsxs2("div", { className: "mx-meet__menu", role: "menu", children: [
6467
+ /* @__PURE__ */ jsxs2(
6468
+ MenuItem,
6469
+ {
6470
+ onSelect: () => {
6471
+ setPopover("none");
6472
+ setPanel("settings");
6473
+ },
6474
+ children: [
6475
+ /* @__PURE__ */ jsx3(SettingsIcon, { width: 16, height: 16 }),
6476
+ " Audio, video and effects"
6477
+ ]
6478
+ }
6479
+ ),
6480
+ /* @__PURE__ */ jsxs2(
6481
+ MenuItem,
6482
+ {
6483
+ onSelect: () => {
6484
+ setPopover("none");
6485
+ setShortcuts(true);
6486
+ },
6487
+ children: [
6488
+ /* @__PURE__ */ jsx3(KeyboardIcon, { width: 16, height: 16 }),
6489
+ " Keyboard shortcuts"
6490
+ ]
6491
+ }
6492
+ ),
6493
+ controls.isHost ? /* @__PURE__ */ jsxs2(Fragment, { children: [
6494
+ /* @__PURE__ */ jsx3("div", { className: "mx-meet__menu-sep", role: "separator" }),
6495
+ /* @__PURE__ */ jsxs2(
6496
+ MenuItem,
6497
+ {
6498
+ onSelect: () => {
6499
+ setPopover("none");
6500
+ void controls.run({ action: "mute_all" });
6501
+ },
6502
+ children: [
6503
+ /* @__PURE__ */ jsx3(MicOffIcon, { width: 16, height: 16 }),
6504
+ " Mute everyone"
6505
+ ]
6506
+ }
6507
+ ),
6508
+ /* @__PURE__ */ jsxs2(
6509
+ MenuItem,
6510
+ {
6511
+ onSelect: () => {
6512
+ setPopover("none");
6513
+ void controls.run({ action: "ask_all_unmute" });
6514
+ },
6515
+ children: [
6516
+ /* @__PURE__ */ jsx3(MicIcon, { width: 16, height: 16 }),
6517
+ " Ask everyone to unmute"
6518
+ ]
6519
+ }
6520
+ ),
6521
+ queue.length > 0 ? /* @__PURE__ */ jsxs2(
6522
+ MenuItem,
6523
+ {
6524
+ onSelect: () => {
6525
+ setPopover("none");
6526
+ void controls.run({ action: "lower_all_hands" });
6527
+ },
6528
+ children: [
6529
+ /* @__PURE__ */ jsx3(HandIcon, { width: 16, height: 16 }),
6530
+ " Lower all hands (",
6531
+ queue.length,
6532
+ ")"
6533
+ ]
6534
+ }
6535
+ ) : null,
6536
+ hasSpotlight ? /* @__PURE__ */ jsxs2(
6537
+ MenuItem,
6538
+ {
6539
+ onSelect: () => {
6540
+ setPopover("none");
6541
+ void controls.run({ action: "spotlight", identity: null });
6542
+ },
6543
+ children: [
6544
+ /* @__PURE__ */ jsx3(SpotlightIcon, { width: 16, height: 16 }),
6545
+ " Remove spotlight"
6546
+ ]
6547
+ }
6548
+ ) : null
6549
+ ] }) : null
6550
+ ] }) }) : null
6551
+ ] }),
5615
6552
  /* @__PURE__ */ jsx3(
5616
6553
  ControlButton,
5617
6554
  {
@@ -5627,6 +6564,150 @@ function ControlBar({ onLeave }) {
5627
6564
  ] })
5628
6565
  ] });
5629
6566
  }
6567
+ function useUnreadChat(open) {
6568
+ const snapshot = useMeetSnapshot();
6569
+ const chat = snapshot?.chat ?? [];
6570
+ const local = snapshot?.localIdentity ?? null;
6571
+ const [seen, setSeen] = useState3(0);
6572
+ useEffect3(() => {
6573
+ if (open) setSeen(chat.length);
6574
+ }, [open, chat.length]);
6575
+ const loaded = snapshot?.chatLoaded === true;
6576
+ const [baseline, setBaseline] = useState3(null);
6577
+ useEffect3(() => {
6578
+ if (loaded && baseline === null) setBaseline(chat.length);
6579
+ }, [loaded, baseline, chat.length]);
6580
+ if (open) return 0;
6581
+ const from = Math.max(seen, baseline ?? chat.length);
6582
+ return chat.slice(from).filter((message) => message.identity !== local).length;
6583
+ }
6584
+ function Popover({
6585
+ label,
6586
+ onClose,
6587
+ children,
6588
+ placement = "up"
6589
+ }) {
6590
+ const ref = useRef3(null);
6591
+ const [style, setStyle] = useState3({ visibility: "hidden" });
6592
+ useLayoutEffect(() => {
6593
+ const element = ref.current;
6594
+ const anchor = element?.parentElement ?? null;
6595
+ if (element === null || anchor === null) return;
6596
+ const place = () => {
6597
+ const rect = anchor.getBoundingClientRect();
6598
+ const width = element.offsetWidth;
6599
+ const height = element.offsetHeight;
6600
+ const gutter = 8;
6601
+ const left = Math.min(
6602
+ Math.max(gutter, rect.left + rect.width / 2 - width / 2),
6603
+ window.innerWidth - width - gutter
6604
+ );
6605
+ const fitsBelow = rect.bottom + gutter + height <= window.innerHeight;
6606
+ const fitsAbove = rect.top - gutter - height >= 0;
6607
+ const down = placement === "down" ? fitsBelow || !fitsAbove : !fitsAbove && fitsBelow;
6608
+ setStyle(
6609
+ down ? { position: "fixed", left, top: rect.bottom + gutter } : { position: "fixed", left, bottom: window.innerHeight - rect.top + gutter }
6610
+ );
6611
+ };
6612
+ place();
6613
+ window.addEventListener("resize", place);
6614
+ return () => window.removeEventListener("resize", place);
6615
+ }, [placement]);
6616
+ useEffect3(() => {
6617
+ const onKey = (event) => {
6618
+ if (event.key === "Escape") onClose();
6619
+ };
6620
+ const onPointer = (event) => {
6621
+ const anchor = ref.current?.parentElement ?? null;
6622
+ if (anchor !== null && !anchor.contains(event.target)) onClose();
6623
+ };
6624
+ window.addEventListener("keydown", onKey);
6625
+ window.addEventListener("pointerdown", onPointer);
6626
+ return () => {
6627
+ window.removeEventListener("keydown", onKey);
6628
+ window.removeEventListener("pointerdown", onPointer);
6629
+ };
6630
+ }, [onClose]);
6631
+ return /* @__PURE__ */ jsx3("div", { ref, className: "mx-meet__popover", style, role: "dialog", "aria-label": label, children });
6632
+ }
6633
+ function MenuItem({
6634
+ onSelect,
6635
+ danger = false,
6636
+ children
6637
+ }) {
6638
+ return /* @__PURE__ */ jsx3(
6639
+ "button",
6640
+ {
6641
+ type: "button",
6642
+ role: "menuitem",
6643
+ className: danger ? "mx-meet__menu-item mx-meet__menu-item--danger" : "mx-meet__menu-item",
6644
+ onClick: onSelect,
6645
+ children
6646
+ }
6647
+ );
6648
+ }
6649
+ function Toast({ text: text3, failed, onDone }) {
6650
+ useEffect3(() => {
6651
+ const timer = setTimeout(onDone, failed ? 7e3 : 4e3);
6652
+ return () => clearTimeout(timer);
6653
+ }, [text3, failed, onDone]);
6654
+ return /* @__PURE__ */ jsxs2("div", { className: failed ? "mx-meet__toast mx-meet__toast--failed" : "mx-meet__toast", role: "status", children: [
6655
+ /* @__PURE__ */ jsx3("span", { children: text3 }),
6656
+ /* @__PURE__ */ jsx3("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: onDone, children: /* @__PURE__ */ jsx3(CloseIcon, { width: 14, height: 14 }) })
6657
+ ] });
6658
+ }
6659
+ function MeetingOverlays() {
6660
+ const reactions = useReactions();
6661
+ const { notice, dismiss } = useModerationNotice();
6662
+ const room = useRoom();
6663
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
6664
+ /* @__PURE__ */ jsx3(ReactionOverlay, { reactions }),
6665
+ notice !== null && notice.kind !== "removed" ? /* @__PURE__ */ jsxs2("div", { className: "mx-meet__notice-bar", role: "alert", children: [
6666
+ /* @__PURE__ */ jsx3("span", { children: notice.text }),
6667
+ notice.kind === "ask-unmute" && room.local?.micEnabled !== true ? /* @__PURE__ */ jsx3(
6668
+ "button",
6669
+ {
6670
+ type: "button",
6671
+ className: "mx-meet__primary mx-meet__primary--small",
6672
+ onClick: () => {
6673
+ dismiss();
6674
+ void room.toggleMicrophone();
6675
+ },
6676
+ children: "Unmute"
6677
+ }
6678
+ ) : null,
6679
+ /* @__PURE__ */ jsx3("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismiss, children: /* @__PURE__ */ jsx3(CloseIcon, { width: 14, height: 14 }) })
6680
+ ] }) : null
6681
+ ] });
6682
+ }
6683
+ function ShortcutsSheet({ onClose }) {
6684
+ useEffect3(() => {
6685
+ const onKey = (event) => {
6686
+ if (event.key === "Escape") onClose();
6687
+ };
6688
+ window.addEventListener("keydown", onKey);
6689
+ return () => window.removeEventListener("keydown", onKey);
6690
+ }, [onClose]);
6691
+ return /* @__PURE__ */ jsx3("div", { className: "mx-meet__sheet-backdrop", onClick: onClose, children: /* @__PURE__ */ jsxs2(
6692
+ "div",
6693
+ {
6694
+ className: "mx-meet__sheet",
6695
+ role: "dialog",
6696
+ "aria-label": "Keyboard shortcuts",
6697
+ onClick: (event) => event.stopPropagation(),
6698
+ children: [
6699
+ /* @__PURE__ */ jsxs2("header", { className: "mx-meet__panel-head", children: [
6700
+ /* @__PURE__ */ jsx3("h2", { className: "mx-meet__panel-title", children: "Keyboard shortcuts" }),
6701
+ /* @__PURE__ */ jsx3(ControlButton, { label: "Close", onClick: onClose, children: /* @__PURE__ */ jsx3(CloseIcon, { width: 16, height: 16 }) })
6702
+ ] }),
6703
+ /* @__PURE__ */ jsx3("dl", { className: "mx-meet__shortcuts", children: MEET_SHORTCUTS.map((shortcut) => /* @__PURE__ */ jsxs2("div", { className: "mx-meet__shortcut", children: [
6704
+ /* @__PURE__ */ jsx3("dt", { children: shortcut.label }),
6705
+ /* @__PURE__ */ jsx3("dd", { children: /* @__PURE__ */ jsx3("kbd", { className: "mx-meet__kbd", children: shortcut.keys }) })
6706
+ ] }, shortcut.id)) })
6707
+ ]
6708
+ }
6709
+ ) });
6710
+ }
5630
6711
  function AiControl({ active, onToggle }) {
5631
6712
  const snapshot = useMeetSnapshot();
5632
6713
  const meeting = snapshot?.meeting ?? null;
@@ -5646,117 +6727,641 @@ function PanelShell({
5646
6727
  /* @__PURE__ */ jsx3("div", { className: "mx-meet__panel-body", children })
5647
6728
  ] });
5648
6729
  }
5649
- function ChatPanel({ onClose }) {
6730
+ function ChatPanel({
6731
+ onClose,
6732
+ to = null,
6733
+ onTo
6734
+ }) {
5650
6735
  const snapshot = useMeetSnapshot();
5651
6736
  const room = useRoom();
5652
6737
  const [draft, setDraft] = useState3("");
6738
+ const [recipient, setRecipient] = useState3(to);
6739
+ const listRef = useRef3(null);
5653
6740
  const messages = snapshot?.chat ?? [];
5654
- return /* @__PURE__ */ jsxs2(PanelShell, { title: "In-meeting chat", onClose, children: [
5655
- /* @__PURE__ */ jsx3("p", { className: "mx-meet__panel-note", children: "In-meeting chat is ephemeral \u2014 it disappears when the meeting ends." }),
5656
- /* @__PURE__ */ jsx3("ul", { className: "mx-meet__chat", children: messages.map((message) => /* @__PURE__ */ jsxs2("li", { className: "mx-meet__chat-line", children: [
5657
- /* @__PURE__ */ jsx3("strong", { children: message.displayName }),
5658
- /* @__PURE__ */ jsx3("span", { children: message.text })
5659
- ] }, message.id)) }),
6741
+ const local = snapshot?.localIdentity ?? null;
6742
+ const people = (snapshot?.participants ?? []).filter(
6743
+ (participant) => !participant.isLocal && !participant.isAgent
6744
+ );
6745
+ const recipientName = recipient === null ? null : people.find((p) => p.identity === recipient)?.displayName ?? null;
6746
+ useEffect3(() => setRecipient(to), [to]);
6747
+ useEffect3(() => {
6748
+ const list = listRef.current;
6749
+ if (list !== null) list.scrollTop = list.scrollHeight;
6750
+ }, [messages.length]);
6751
+ const choose = (next) => {
6752
+ setRecipient(next);
6753
+ onTo?.(next);
6754
+ };
6755
+ return /* @__PURE__ */ jsxs2(PanelShell, { title: "Chat", onClose, children: [
6756
+ /* @__PURE__ */ jsxs2("ul", { className: "mx-meet__chat", ref: listRef, children: [
6757
+ snapshot?.chatHistoryError != null ? /* @__PURE__ */ jsx3("li", { className: "mx-meet__panel-note", children: snapshot.chatHistoryError }) : null,
6758
+ snapshot?.chatLoaded !== true && messages.length === 0 ? /* @__PURE__ */ jsx3("li", { className: "mx-meet__panel-note", children: "Reading earlier messages\u2026" }) : messages.length === 0 && snapshot?.chatHistoryError == null ? /* @__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,
6759
+ messages.map((message) => /* @__PURE__ */ jsx3(ChatLine, { message, local, people }, message.id))
6760
+ ] }),
5660
6761
  /* @__PURE__ */ jsxs2(
5661
6762
  "form",
5662
6763
  {
5663
- className: "mx-meet__composer",
6764
+ className: "mx-meet__composer mx-meet__composer--chat",
5664
6765
  onSubmit: (event) => {
5665
6766
  event.preventDefault();
5666
6767
  const text3 = draft;
6768
+ if (text3.trim().length === 0) return;
5667
6769
  setDraft("");
5668
- void room.sendChat(text3);
6770
+ if (recipient === null) void room.sendChat(text3).catch(() => void 0);
6771
+ else void room.sendPrivateChat(recipient, text3);
5669
6772
  },
5670
6773
  children: [
5671
- /* @__PURE__ */ jsx3(
5672
- "input",
6774
+ people.length > 0 ? /* @__PURE__ */ jsxs2(
6775
+ "select",
5673
6776
  {
5674
- className: "mx-meet__input",
5675
- value: draft,
5676
- onChange: (event) => setDraft(event.target.value),
5677
- placeholder: "Message everyone",
5678
- "aria-label": "Message everyone"
6777
+ className: "mx-meet__select mx-meet__select--compact",
6778
+ "aria-label": "Send to",
6779
+ value: recipient ?? "",
6780
+ onChange: (event) => choose(event.target.value === "" ? null : event.target.value),
6781
+ children: [
6782
+ /* @__PURE__ */ jsx3("option", { value: "", children: "Everyone" }),
6783
+ people.map((participant) => /* @__PURE__ */ jsxs2("option", { value: participant.identity, children: [
6784
+ participant.displayName,
6785
+ " (private)"
6786
+ ] }, participant.identity))
6787
+ ]
5679
6788
  }
5680
- ),
5681
- /* @__PURE__ */ jsx3("button", { type: "submit", className: "mx-meet__send", children: "Send" })
6789
+ ) : null,
6790
+ /* @__PURE__ */ jsxs2("div", { className: "mx-meet__composer-row", children: [
6791
+ /* @__PURE__ */ jsx3(
6792
+ "input",
6793
+ {
6794
+ className: "mx-meet__input",
6795
+ value: draft,
6796
+ onChange: (event) => setDraft(event.target.value),
6797
+ placeholder: recipientName === null ? "Message everyone" : `Message ${recipientName} privately`,
6798
+ "aria-label": recipientName === null ? "Message everyone" : `Message ${recipientName} privately`,
6799
+ maxLength: 4e3
6800
+ }
6801
+ ),
6802
+ /* @__PURE__ */ jsx3("button", { type: "submit", className: "mx-meet__send", disabled: draft.trim().length === 0, children: "Send" })
6803
+ ] }),
6804
+ recipientName !== null ? /* @__PURE__ */ jsxs2("p", { className: "mx-meet__panel-note", children: [
6805
+ "Only ",
6806
+ recipientName,
6807
+ " sees this. Private messages are not saved."
6808
+ ] }) : null
5682
6809
  ]
5683
6810
  }
5684
6811
  )
5685
6812
  ] });
5686
6813
  }
5687
- function PeoplePanel({ onClose }) {
6814
+ function ChatLine({
6815
+ message,
6816
+ local,
6817
+ people
6818
+ }) {
6819
+ const mine = message.identity === local;
6820
+ const privateTo = message.to ?? null;
6821
+ const counterpart = privateTo === null ? null : mine ? people.find((p) => p.identity === privateTo)?.displayName ?? "them" : "you";
6822
+ return /* @__PURE__ */ jsxs2(
6823
+ "li",
6824
+ {
6825
+ className: [
6826
+ "mx-meet__chat-line",
6827
+ privateTo !== null ? "mx-meet__chat-line--private" : "",
6828
+ message.failed !== void 0 ? "mx-meet__chat-line--failed" : ""
6829
+ ].filter(Boolean).join(" "),
6830
+ children: [
6831
+ /* @__PURE__ */ jsxs2("span", { className: "mx-meet__chat-meta", children: [
6832
+ /* @__PURE__ */ jsx3("strong", { children: mine ? "You" : message.displayName }),
6833
+ counterpart !== null ? /* @__PURE__ */ jsxs2("span", { className: "mx-meet__chat-private", children: [
6834
+ "to ",
6835
+ counterpart,
6836
+ " \xB7 private"
6837
+ ] }) : null,
6838
+ /* @__PURE__ */ jsx3("time", { dateTime: message.at, children: clockOf(message.at) })
6839
+ ] }),
6840
+ /* @__PURE__ */ jsx3("span", { className: "mx-meet__chat-text", children: message.text }),
6841
+ 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
6842
+ ]
6843
+ }
6844
+ );
6845
+ }
6846
+ function PeoplePanel({
6847
+ onClose,
6848
+ onMessage
6849
+ }) {
5688
6850
  const snapshot = useMeetSnapshot();
5689
6851
  const controls = useHostControls();
6852
+ const queue = useHandQueue();
6853
+ const [query, setQuery] = useState3("");
6854
+ const [menuFor, setMenuFor] = useState3(null);
5690
6855
  const myRole = useRole();
5691
6856
  const local = snapshot?.localIdentity ?? null;
5692
- return /* @__PURE__ */ jsxs2(PanelShell, { title: "People", onClose, children: [
6857
+ const everyone = snapshot?.participants ?? [];
6858
+ const needle = query.trim().toLowerCase();
6859
+ const shown = needle.length === 0 ? everyone : everyone.filter((participant) => participant.displayName.toLowerCase().includes(needle));
6860
+ return /* @__PURE__ */ jsxs2(PanelShell, { title: `People (${everyone.length})`, onClose, children: [
5693
6861
  /* @__PURE__ */ jsx3(LobbyPanel, {}),
5694
- snapshot?.meeting !== null && snapshot?.meeting !== void 0 ? /* @__PURE__ */ jsx3(GuestRecordAccessControl, { meeting: snapshot.meeting }) : null,
5695
- /* @__PURE__ */ jsx3("ul", { className: "mx-meet__people", children: (snapshot?.participants ?? []).map((participant) => /* @__PURE__ */ jsxs2("li", { className: "mx-meet__person", children: [
5696
- /* @__PURE__ */ jsx3(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: 32 }),
5697
- /* @__PURE__ */ jsxs2("span", { className: "mx-meet__person-name", children: [
5698
- participant.displayName,
5699
- /* @__PURE__ */ jsx3(
5700
- RoleBadge,
6862
+ queue.length > 0 ? /* @__PURE__ */ jsxs2("section", { className: "mx-meet__section", "aria-label": "Raised hands", children: [
6863
+ /* @__PURE__ */ jsxs2("header", { className: "mx-meet__section-head", children: [
6864
+ /* @__PURE__ */ jsxs2("h3", { className: "mx-meet__section-title", children: [
6865
+ "Raised hands \xB7 ",
6866
+ queue.length
6867
+ ] }),
6868
+ controls.isHost ? /* @__PURE__ */ jsx3(
6869
+ "button",
5701
6870
  {
5702
- role: participant.identity === local ? myRole : participant.role
6871
+ type: "button",
6872
+ className: "mx-meet__link",
6873
+ onClick: () => void controls.run({ action: "lower_all_hands" }),
6874
+ children: "Lower all"
5703
6875
  }
5704
- ),
5705
- isNoteTaker(participant) ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null
6876
+ ) : null
5706
6877
  ] }),
5707
- !participant.micEnabled && !isNoteTaker(participant) ? /* @__PURE__ */ jsx3(MicOffIcon, { width: 14, height: 14 }) : null,
5708
- participant.handRaised ? /* @__PURE__ */ jsx3(HandIcon, { width: 14, height: 14 }) : null,
5709
- controls.canModerate(participant) ? /* @__PURE__ */ jsx3(PersonHostActions, { participant, controls }) : null
5710
- ] }, participant.identity)) })
6878
+ /* @__PURE__ */ jsx3("ol", { className: "mx-meet__queue", children: queue.map((participant) => /* @__PURE__ */ jsxs2("li", { className: "mx-meet__person", children: [
6879
+ /* @__PURE__ */ jsx3(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: 28 }),
6880
+ /* @__PURE__ */ jsxs2("span", { className: "mx-meet__person-name", children: [
6881
+ participant.displayName,
6882
+ participant.identity === local ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__you", children: " (you)" }) : null
6883
+ ] }),
6884
+ controls.isHost || participant.identity === local ? /* @__PURE__ */ jsx3(
6885
+ "button",
6886
+ {
6887
+ type: "button",
6888
+ className: "mx-meet__link",
6889
+ onClick: () => void controls.run({ action: "lower_hand", identity: participant.identity }),
6890
+ children: "Lower"
6891
+ }
6892
+ ) : null
6893
+ ] }, participant.identity)) })
6894
+ ] }) : null,
6895
+ controls.isHost && everyone.length > 1 ? /* @__PURE__ */ jsxs2("div", { className: "mx-meet__bulk", children: [
6896
+ /* @__PURE__ */ jsxs2("button", { type: "button", className: "mx-meet__chip", onClick: () => void controls.run({ action: "mute_all" }), children: [
6897
+ /* @__PURE__ */ jsx3(MicOffIcon, { width: 14, height: 14 }),
6898
+ " Mute all"
6899
+ ] }),
6900
+ /* @__PURE__ */ jsxs2(
6901
+ "button",
6902
+ {
6903
+ type: "button",
6904
+ className: "mx-meet__chip",
6905
+ onClick: () => void controls.run({ action: "ask_all_unmute" }),
6906
+ children: [
6907
+ /* @__PURE__ */ jsx3(MicIcon, { width: 14, height: 14 }),
6908
+ " Ask all to unmute"
6909
+ ]
6910
+ }
6911
+ )
6912
+ ] }) : null,
6913
+ everyone.length > 4 ? /* @__PURE__ */ jsxs2("label", { className: "mx-meet__search", children: [
6914
+ /* @__PURE__ */ jsx3(SearchIcon, { width: 16, height: 16 }),
6915
+ /* @__PURE__ */ jsx3(
6916
+ "input",
6917
+ {
6918
+ className: "mx-meet__input mx-meet__input--bare",
6919
+ value: query,
6920
+ onChange: (event) => setQuery(event.target.value),
6921
+ placeholder: "Search people",
6922
+ "aria-label": "Search people"
6923
+ }
6924
+ )
6925
+ ] }) : null,
6926
+ controls.notice !== null ? /* @__PURE__ */ jsx3("p", { className: controls.notice.failed ? "mx-meet__error" : "mx-meet__notice", role: "status", children: controls.notice.text }) : null,
6927
+ /* @__PURE__ */ jsxs2("ul", { className: "mx-meet__people", children: [
6928
+ shown.map((participant) => /* @__PURE__ */ jsxs2("li", { className: "mx-meet__person", children: [
6929
+ /* @__PURE__ */ jsx3(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: 32 }),
6930
+ /* @__PURE__ */ jsxs2("span", { className: "mx-meet__person-name", children: [
6931
+ participant.displayName,
6932
+ participant.identity === local ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__you", children: " (you)" }) : null,
6933
+ /* @__PURE__ */ jsx3(RoleBadge, { role: participant.identity === local ? myRole : participant.role }),
6934
+ isNoteTaker(participant) ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null,
6935
+ snapshot?.spotlight === participant.identity ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__badge", children: "Spotlight" }) : null
6936
+ ] }),
6937
+ participant.handRaised ? /* @__PURE__ */ jsx3(HandIcon, { width: 14, height: 14 }) : null,
6938
+ !participant.micEnabled && !isNoteTaker(participant) ? /* @__PURE__ */ jsx3(MicOffIcon, { width: 14, height: 14 }) : null,
6939
+ /* @__PURE__ */ jsx3(QualityBars, { quality: participant.quality }),
6940
+ /* @__PURE__ */ jsxs2("span", { className: "mx-meet__anchor", children: [
6941
+ /* @__PURE__ */ jsx3(
6942
+ ControlButton,
6943
+ {
6944
+ label: `More for ${participant.displayName}`,
6945
+ active: menuFor === participant.identity,
6946
+ onClick: () => setMenuFor(menuFor === participant.identity ? null : participant.identity),
6947
+ children: /* @__PURE__ */ jsx3(MoreIcon, { width: 16, height: 16 })
6948
+ }
6949
+ ),
6950
+ menuFor === participant.identity ? /* @__PURE__ */ jsx3(
6951
+ PersonMenu,
6952
+ {
6953
+ participant,
6954
+ placement: "down",
6955
+ onClose: () => setMenuFor(null),
6956
+ ...onMessage !== void 0 ? { onMessage } : {}
6957
+ }
6958
+ ) : null
6959
+ ] })
6960
+ ] }, participant.identity)),
6961
+ shown.length === 0 ? /* @__PURE__ */ jsxs2("li", { className: "mx-meet__panel-note", children: [
6962
+ "Nobody matches \u201C",
6963
+ query,
6964
+ "\u201D."
6965
+ ] }) : null
6966
+ ] }),
6967
+ snapshot?.meeting !== null && snapshot?.meeting !== void 0 ? /* @__PURE__ */ jsx3(GuestRecordAccessControl, { meeting: snapshot.meeting }) : null
5711
6968
  ] });
5712
6969
  }
5713
- function PersonHostActions({
6970
+ function PersonMenu({
5714
6971
  participant,
5715
- controls
6972
+ onClose,
6973
+ onMessage,
6974
+ placement = "down"
5716
6975
  }) {
5717
- const [confirming, setConfirming] = useState3(false);
5718
- if (confirming) {
5719
- return /* @__PURE__ */ jsxs2("span", { className: "mx-meet__confirm", role: "group", "aria-label": `Remove ${participant.displayName}?`, children: [
5720
- /* @__PURE__ */ jsxs2("span", { className: "mx-meet__confirm-text", children: [
5721
- "Remove ",
5722
- participant.displayName,
5723
- "? They leave the meeting immediately and have to be let back in."
5724
- ] }),
5725
- /* @__PURE__ */ jsx3(
5726
- "button",
6976
+ const host = useMeetHost();
6977
+ const snapshot = useMeetSnapshot();
6978
+ const controls = useHostControls();
6979
+ const [mode, setMode] = useState3("menu");
6980
+ const [text3, setText] = useState3("");
6981
+ const self = participant.identity === snapshot?.localIdentity;
6982
+ const moderate = controls.canModerate(participant);
6983
+ const pinned = snapshot?.pinned === participant.identity;
6984
+ const spotlit = snapshot?.spotlight === participant.identity;
6985
+ const hasAccount = participant.userId !== null;
6986
+ const act = (request) => {
6987
+ void controls.run(request).then((result) => {
6988
+ if (result !== null) onClose();
6989
+ });
6990
+ };
6991
+ if (mode === "rename" || mode === "report") {
6992
+ const renaming = mode === "rename";
6993
+ return /* @__PURE__ */ jsx3(Popover, { label: renaming ? "Rename" : "Report", onClose, placement, children: /* @__PURE__ */ jsxs2(
6994
+ "form",
6995
+ {
6996
+ className: "mx-meet__menu-form",
6997
+ onSubmit: (event) => {
6998
+ event.preventDefault();
6999
+ if (text3.trim().length === 0) return;
7000
+ act(
7001
+ renaming ? { action: "rename", identity: participant.identity, name: text3.trim() } : { action: "report", identity: participant.identity, reason: text3.trim() }
7002
+ );
7003
+ },
7004
+ children: [
7005
+ /* @__PURE__ */ jsxs2("label", { className: "mx-meet__field", children: [
7006
+ /* @__PURE__ */ jsx3("span", { className: "mx-meet__field-label", children: renaming ? "Name shown in this meeting" : `What did ${participant.displayName} do?` }),
7007
+ renaming ? /* @__PURE__ */ jsx3(
7008
+ "input",
7009
+ {
7010
+ className: "mx-meet__input",
7011
+ autoFocus: true,
7012
+ maxLength: 80,
7013
+ value: text3,
7014
+ onChange: (event) => setText(event.target.value)
7015
+ }
7016
+ ) : /* @__PURE__ */ jsx3(
7017
+ "textarea",
7018
+ {
7019
+ className: "mx-meet__input",
7020
+ autoFocus: true,
7021
+ rows: 3,
7022
+ maxLength: 1e3,
7023
+ value: text3,
7024
+ onChange: (event) => setText(event.target.value)
7025
+ }
7026
+ )
7027
+ ] }),
7028
+ controls.notice?.failed === true ? /* @__PURE__ */ jsx3("p", { className: "mx-meet__error", children: controls.notice.text }) : null,
7029
+ /* @__PURE__ */ jsxs2("div", { className: "mx-meet__menu-actions", children: [
7030
+ /* @__PURE__ */ jsx3("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Back" }),
7031
+ /* @__PURE__ */ jsx3("button", { type: "submit", className: "mx-meet__send", disabled: text3.trim().length === 0 || controls.busy, children: renaming ? "Rename" : "Report" })
7032
+ ] })
7033
+ ]
7034
+ }
7035
+ ) });
7036
+ }
7037
+ if (mode === "remove" || mode === "transfer") {
7038
+ const removing = mode === "remove";
7039
+ return /* @__PURE__ */ jsx3(Popover, { label: removing ? "Remove" : "Make host", onClose, placement, children: /* @__PURE__ */ jsxs2("div", { className: "mx-meet__menu-form", children: [
7040
+ /* @__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.` }),
7041
+ /* @__PURE__ */ jsxs2("div", { className: "mx-meet__menu-actions", children: [
7042
+ /* @__PURE__ */ jsx3("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Cancel" }),
7043
+ /* @__PURE__ */ jsx3(
7044
+ "button",
7045
+ {
7046
+ type: "button",
7047
+ className: removing ? "mx-meet__send mx-meet__send--danger" : "mx-meet__send",
7048
+ disabled: controls.busy,
7049
+ onClick: () => act({ action: removing ? "remove" : "transfer_host", identity: participant.identity }),
7050
+ children: removing ? "Remove" : "Make host"
7051
+ }
7052
+ )
7053
+ ] })
7054
+ ] }) });
7055
+ }
7056
+ return /* @__PURE__ */ jsx3(Popover, { label: `Options for ${participant.displayName}`, onClose, placement, children: /* @__PURE__ */ jsxs2("div", { className: "mx-meet__menu", role: "menu", children: [
7057
+ /* @__PURE__ */ jsxs2(
7058
+ MenuItem,
7059
+ {
7060
+ onSelect: () => {
7061
+ host?.engine.pin(pinned ? null : participant.identity);
7062
+ onClose();
7063
+ },
7064
+ children: [
7065
+ /* @__PURE__ */ jsx3(PinIcon, { width: 16, height: 16 }),
7066
+ " ",
7067
+ pinned ? "Unpin" : "Pin for me"
7068
+ ]
7069
+ }
7070
+ ),
7071
+ !self && !participant.isAgent && onMessage !== void 0 ? /* @__PURE__ */ jsxs2(
7072
+ MenuItem,
7073
+ {
7074
+ onSelect: () => {
7075
+ onMessage(participant.identity);
7076
+ onClose();
7077
+ },
7078
+ children: [
7079
+ /* @__PURE__ */ jsx3(ChatIcon, { width: 16, height: 16 }),
7080
+ " Message privately"
7081
+ ]
7082
+ }
7083
+ ) : null,
7084
+ controls.isHost ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => act({ action: "spotlight", identity: spotlit ? null : participant.identity }), children: [
7085
+ /* @__PURE__ */ jsx3(SpotlightIcon, { width: 16, height: 16 }),
7086
+ " ",
7087
+ spotlit ? "Remove spotlight" : "Spotlight for everyone"
7088
+ ] }) : null,
7089
+ moderate ? /* @__PURE__ */ jsxs2(Fragment, { children: [
7090
+ /* @__PURE__ */ jsx3("div", { className: "mx-meet__menu-sep", role: "separator" }),
7091
+ participant.micEnabled ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => act({ action: "mute", identity: participant.identity }), children: [
7092
+ /* @__PURE__ */ jsx3(MicOffIcon, { width: 16, height: 16 }),
7093
+ " Mute"
7094
+ ] }) : !participant.isAgent ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => act({ action: "ask_unmute", identity: participant.identity }), children: [
7095
+ /* @__PURE__ */ jsx3(MicIcon, { width: 16, height: 16 }),
7096
+ " Ask to unmute"
7097
+ ] }) : null,
7098
+ participant.cameraEnabled ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => act({ action: "stop_video", identity: participant.identity }), children: [
7099
+ /* @__PURE__ */ jsx3(CameraOffIcon, { width: 16, height: 16 }),
7100
+ " Stop video"
7101
+ ] }) : null,
7102
+ participant.screenSharing ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => act({ action: "stop_screen", identity: participant.identity }), children: [
7103
+ /* @__PURE__ */ jsx3(ScreenShareIcon, { width: 16, height: 16 }),
7104
+ " Stop screen share"
7105
+ ] }) : null,
7106
+ participant.handRaised ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => act({ action: "lower_hand", identity: participant.identity }), children: [
7107
+ /* @__PURE__ */ jsx3(HandIcon, { width: 16, height: 16 }),
7108
+ " Lower hand"
7109
+ ] }) : null,
7110
+ hasAccount && !participant.isAgent && participant.role !== "cohost" ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => act({ action: "make_cohost", identity: participant.identity }), children: [
7111
+ /* @__PURE__ */ jsx3(PeopleIcon, { width: 16, height: 16 }),
7112
+ " Make co-host"
7113
+ ] }) : null,
7114
+ !participant.isAgent ? /* @__PURE__ */ jsxs2(
7115
+ MenuItem,
5727
7116
  {
5728
- type: "button",
5729
- className: "mx-meet__link mx-meet__link--danger",
5730
- onClick: () => {
5731
- setConfirming(false);
5732
- void controls.remove(participant);
7117
+ onSelect: () => {
7118
+ setText(participant.displayName);
7119
+ setMode("rename");
5733
7120
  },
5734
- children: "Remove"
7121
+ children: [
7122
+ /* @__PURE__ */ jsx3(PencilIcon, { width: 16, height: 16 }),
7123
+ " Rename"
7124
+ ]
5735
7125
  }
5736
- ),
5737
- /* @__PURE__ */ jsx3("button", { type: "button", className: "mx-meet__link", onClick: () => setConfirming(false), children: "Keep them" })
5738
- ] });
5739
- }
5740
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
5741
- participant.micEnabled ? /* @__PURE__ */ jsx3(
5742
- ControlButton,
7126
+ ) : null
7127
+ ] }) : null,
7128
+ controls.isOwner && participant.role === "cohost" ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => act({ action: "revoke_cohost", identity: participant.identity }), children: [
7129
+ /* @__PURE__ */ jsx3(PeopleIcon, { width: 16, height: 16 }),
7130
+ " Remove co-host"
7131
+ ] }) : null,
7132
+ controls.isOwner && !self && hasAccount && !participant.isAgent ? /* @__PURE__ */ jsxs2(MenuItem, { onSelect: () => setMode("transfer"), children: [
7133
+ /* @__PURE__ */ jsx3(SpotlightIcon, { width: 16, height: 16 }),
7134
+ " Make host"
7135
+ ] }) : null,
7136
+ self ? /* @__PURE__ */ jsxs2(
7137
+ MenuItem,
5743
7138
  {
5744
- label: `Mute ${participant.displayName}`,
5745
- onClick: () => void controls.mute(participant),
5746
- children: /* @__PURE__ */ jsx3(MicOffIcon, { width: 14, height: 14 })
7139
+ onSelect: () => {
7140
+ setText(participant.displayName);
7141
+ setMode("rename");
7142
+ },
7143
+ children: [
7144
+ /* @__PURE__ */ jsx3(PencilIcon, { width: 16, height: 16 }),
7145
+ " Rename yourself"
7146
+ ]
5747
7147
  }
5748
7148
  ) : null,
7149
+ !self && !participant.isAgent ? /* @__PURE__ */ jsxs2(
7150
+ MenuItem,
7151
+ {
7152
+ onSelect: () => {
7153
+ setText("");
7154
+ setMode("report");
7155
+ },
7156
+ children: [
7157
+ /* @__PURE__ */ jsx3(FlagIcon, { width: 16, height: 16 }),
7158
+ " Report"
7159
+ ]
7160
+ }
7161
+ ) : null,
7162
+ moderate && !participant.isAgent ? /* @__PURE__ */ jsxs2(MenuItem, { danger: true, onSelect: () => setMode("remove"), children: [
7163
+ /* @__PURE__ */ jsx3(CloseIcon, { width: 16, height: 16 }),
7164
+ " Remove from meeting"
7165
+ ] }) : null
7166
+ ] }) });
7167
+ }
7168
+ function SettingsPanel({ onClose }) {
7169
+ const host = useRequiredMeetHost();
7170
+ return /* @__PURE__ */ jsxs2(PanelShell, { title: "Audio, video and effects", onClose, children: [
5749
7171
  /* @__PURE__ */ jsx3(
5750
- ControlButton,
7172
+ DeviceSettings,
5751
7173
  {
5752
- label: `Remove ${participant.displayName}`,
5753
- danger: true,
5754
- onClick: () => setConfirming(true),
5755
- children: /* @__PURE__ */ jsx3(CloseIcon, { width: 14, height: 14 })
7174
+ onPick: (kind, deviceId2) => {
7175
+ host.devices.select(kind, deviceId2);
7176
+ void host.engine.switchDevice(kind, deviceId2);
7177
+ }
5756
7178
  }
5757
- )
7179
+ ),
7180
+ /* @__PURE__ */ jsx3(EffectsSettings, {})
7181
+ ] });
7182
+ }
7183
+ function DeviceSettings({
7184
+ onPick
7185
+ }) {
7186
+ const host = useRequiredMeetHost();
7187
+ const devices = useDeviceSnapshot();
7188
+ const microphone = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
7189
+ const speaker = devices?.selected.audiooutput ?? host.devices.resolve("audiooutput");
7190
+ const stream = useMicrophoneStream(microphone);
7191
+ const level = useAudioLevel(stream);
7192
+ const [testing, setTesting] = useState3(false);
7193
+ useEffect3(() => {
7194
+ void host.devices.refresh();
7195
+ }, [host]);
7196
+ return /* @__PURE__ */ jsxs2("section", { className: "mx-meet__section", "aria-label": "Devices", children: [
7197
+ /* @__PURE__ */ jsx3(DevicePicker, { kind: "audioinput", label: "Microphone", onPick }),
7198
+ /* @__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))}%` } }) }),
7199
+ /* @__PURE__ */ jsx3(DevicePicker, { kind: "audiooutput", label: "Speaker", onPick }),
7200
+ /* @__PURE__ */ jsxs2(
7201
+ "button",
7202
+ {
7203
+ type: "button",
7204
+ className: "mx-meet__chip",
7205
+ disabled: testing,
7206
+ onClick: () => {
7207
+ setTesting(true);
7208
+ void playTestSound(speaker).finally(() => setTesting(false));
7209
+ },
7210
+ children: [
7211
+ /* @__PURE__ */ jsx3(SpeakerIcon, { width: 14, height: 14 }),
7212
+ " ",
7213
+ testing ? "Playing\u2026" : "Test speaker"
7214
+ ]
7215
+ }
7216
+ ),
7217
+ /* @__PURE__ */ jsx3(DevicePicker, { kind: "videoinput", label: "Camera", onPick })
5758
7218
  ] });
5759
7219
  }
7220
+ function EffectsSettings({ preview }) {
7221
+ const effects = useMediaEffects();
7222
+ const [error, setError] = useState3(null);
7223
+ const fileRef = useRef3(null);
7224
+ const supported = effects.backgroundsSupported || preview != null || videoEffectsSupported();
7225
+ const apply = (next) => {
7226
+ setError(null);
7227
+ void effects.set(next);
7228
+ if (preview != null) {
7229
+ const merged = { ...effects.effects, ...next };
7230
+ void preview.setEffect(toVideoEffect(merged)).catch(
7231
+ () => setError("This browser could not run the background effect.")
7232
+ );
7233
+ }
7234
+ };
7235
+ return /* @__PURE__ */ jsxs2("section", { className: "mx-meet__section", "aria-label": "Effects", children: [
7236
+ /* @__PURE__ */ jsxs2("label", { className: "mx-meet__toggle", children: [
7237
+ /* @__PURE__ */ jsx3(
7238
+ "input",
7239
+ {
7240
+ type: "checkbox",
7241
+ checked: effects.effects.noiseSuppression,
7242
+ onChange: (event) => apply({ noiseSuppression: event.target.checked })
7243
+ }
7244
+ ),
7245
+ "Noise suppression",
7246
+ /* @__PURE__ */ jsx3("span", { className: "mx-meet__hint", children: "Filters keyboards, fans and background chatter." })
7247
+ ] }),
7248
+ supported ? /* @__PURE__ */ jsxs2("div", { className: "mx-meet__field", children: [
7249
+ /* @__PURE__ */ jsx3("span", { className: "mx-meet__field-label", children: "Background" }),
7250
+ /* @__PURE__ */ jsx3("div", { className: "mx-meet__pills", role: "radiogroup", "aria-label": "Background", children: ["none", "blur", "image"].map((kind) => /* @__PURE__ */ jsx3(
7251
+ "button",
7252
+ {
7253
+ type: "button",
7254
+ role: "radio",
7255
+ "aria-checked": effects.effects.background === kind,
7256
+ className: effects.effects.background === kind ? "mx-meet__pill mx-meet__pill--on" : "mx-meet__pill",
7257
+ onClick: () => {
7258
+ if (kind === "image") {
7259
+ fileRef.current?.click();
7260
+ return;
7261
+ }
7262
+ apply({ background: kind });
7263
+ },
7264
+ children: kind === "none" ? "None" : kind === "blur" ? "Blur" : "Picture\u2026"
7265
+ },
7266
+ kind
7267
+ )) }),
7268
+ /* @__PURE__ */ jsx3(
7269
+ "input",
7270
+ {
7271
+ ref: fileRef,
7272
+ type: "file",
7273
+ accept: "image/*",
7274
+ hidden: true,
7275
+ onChange: (event) => {
7276
+ const file = event.target.files?.[0];
7277
+ event.target.value = "";
7278
+ if (file === void 0) return;
7279
+ void shrinkImage(file).then((url) => apply({ background: "image", backgroundImage: url })).catch(() => setError("That picture could not be read. Try a JPEG or PNG."));
7280
+ }
7281
+ }
7282
+ ),
7283
+ error !== null ? /* @__PURE__ */ jsx3("p", { className: "mx-meet__error", children: error }) : null
7284
+ ] }) : null
7285
+ ] });
7286
+ }
7287
+ function toVideoEffect(effects) {
7288
+ if (effects.background === "blur") return { kind: "blur" };
7289
+ if (effects.background === "image" && effects.backgroundImage !== null) {
7290
+ return { kind: "image", imageUrl: effects.backgroundImage };
7291
+ }
7292
+ return { kind: "none" };
7293
+ }
7294
+ async function shrinkImage(file) {
7295
+ const url = URL.createObjectURL(file);
7296
+ try {
7297
+ const image = new Image();
7298
+ image.src = url;
7299
+ await image.decode();
7300
+ const scale = Math.min(1, 1280 / image.naturalWidth);
7301
+ const canvas = document.createElement("canvas");
7302
+ canvas.width = Math.round(image.naturalWidth * scale);
7303
+ canvas.height = Math.round(image.naturalHeight * scale);
7304
+ const context = canvas.getContext("2d");
7305
+ if (context === null) throw new Error("no 2d context");
7306
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
7307
+ return canvas.toDataURL("image/jpeg", 0.85);
7308
+ } finally {
7309
+ URL.revokeObjectURL(url);
7310
+ }
7311
+ }
7312
+ async function playTestSound(sinkId) {
7313
+ const Context = globalThis.AudioContext;
7314
+ if (Context === void 0) return;
7315
+ const context = new Context();
7316
+ const destination = context.createMediaStreamDestination();
7317
+ const gain = context.createGain();
7318
+ gain.gain.value = 0.2;
7319
+ gain.connect(destination);
7320
+ for (const [frequency, start] of [
7321
+ [660, 0],
7322
+ [880, 0.25]
7323
+ ]) {
7324
+ const oscillator = context.createOscillator();
7325
+ oscillator.frequency.value = frequency;
7326
+ oscillator.connect(gain);
7327
+ oscillator.start(context.currentTime + start);
7328
+ oscillator.stop(context.currentTime + start + 0.22);
7329
+ }
7330
+ const audio = new Audio();
7331
+ audio.srcObject = destination.stream;
7332
+ const sinkable = audio;
7333
+ if (sinkId !== null && sinkId !== "default" && sinkable.setSinkId !== void 0) {
7334
+ await sinkable.setSinkId(sinkId).catch(() => void 0);
7335
+ }
7336
+ await audio.play().catch(() => void 0);
7337
+ await new Promise((resolve) => setTimeout(resolve, 700));
7338
+ audio.pause();
7339
+ await context.close();
7340
+ }
7341
+ function useMicrophoneStream(deviceId2) {
7342
+ const [stream, setStream] = useState3(null);
7343
+ useEffect3(() => {
7344
+ if (deviceId2 === "off") return void 0;
7345
+ let live = true;
7346
+ let opened = null;
7347
+ const media = globalThis.navigator?.mediaDevices;
7348
+ if (media === void 0) return void 0;
7349
+ void media.getUserMedia({ audio: deviceId2 === null ? true : { deviceId: { exact: deviceId2 } } }).then((next) => {
7350
+ if (!live) {
7351
+ next.getTracks().forEach((track) => track.stop());
7352
+ return;
7353
+ }
7354
+ opened = next;
7355
+ setStream(next);
7356
+ }).catch(() => setStream(null));
7357
+ return () => {
7358
+ live = false;
7359
+ opened?.getTracks().forEach((track) => track.stop());
7360
+ setStream(null);
7361
+ };
7362
+ }, [deviceId2]);
7363
+ return stream;
7364
+ }
5760
7365
  function LobbyPanel() {
5761
7366
  const lobby = useLobby();
5762
7367
  if (!lobby.canAdmit || lobby.waiting.length === 0) return null;
@@ -5982,16 +7587,40 @@ function PreJoinBody({
5982
7587
  const room = useRoom();
5983
7588
  const devices = useDeviceSnapshot();
5984
7589
  const recording = useRecording();
7590
+ const effects = useMediaEffects();
7591
+ const { notice } = useModerationNotice();
5985
7592
  const [mode, setMode] = useState3("video");
5986
7593
  const [microphone, setMicrophone] = useState3(true);
5987
7594
  const [joining, setJoining] = useState3(false);
7595
+ const camera = devices?.selected.videoinput ?? host.devices.resolve("videoinput");
7596
+ const mic = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
7597
+ const preview = useCameraPreview(mode === "video" ? camera : void 0, effects.effects);
7598
+ const micStream = useMicrophoneStream(microphone ? mic : "off");
7599
+ const level = useAudioLevel(micStream);
7600
+ const [speakerTesting, setSpeakerTesting] = useState3(false);
5988
7601
  useEffect3(() => {
5989
7602
  void host.devices.refresh();
5990
7603
  }, [host]);
5991
7604
  return /* @__PURE__ */ jsxs2("div", { className: "mx-meet__prejoin", children: [
5992
- /* @__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 }) }) }),
7605
+ /* @__PURE__ */ jsxs2("div", { className: "mx-meet__preview", children: [
7606
+ mode === "video" && preview.preview !== null ? /* @__PURE__ */ jsx3(
7607
+ "video",
7608
+ {
7609
+ className: "mx-meet__video mx-meet__video--mirror",
7610
+ autoPlay: true,
7611
+ playsInline: true,
7612
+ muted: true,
7613
+ ref: (element) => {
7614
+ if (element !== null) preview.preview?.attach(element);
7615
+ }
7616
+ }
7617
+ ) : /* @__PURE__ */ jsx3("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ jsx3(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 96 }) }),
7618
+ mode === "video" && preview.error !== null ? /* @__PURE__ */ jsx3("p", { className: "mx-meet__preview-note", children: preview.error }) : null,
7619
+ microphone && micStream !== null ? /* @__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
7620
+ ] }),
5993
7621
  /* @__PURE__ */ jsxs2("div", { className: "mx-meet__prejoin-panel", children: [
5994
7622
  /* @__PURE__ */ jsx3("h1", { className: "mx-meet__prejoin-title", children: "Ready to join?" }),
7623
+ notice?.kind === "removed" ? /* @__PURE__ */ jsx3("p", { className: "mx-meet__warning", children: notice.text }) : null,
5995
7624
  devices?.labelsHidden === true ? /* @__PURE__ */ jsxs2("p", { className: "mx-meet__panel-note", children: [
5996
7625
  "Your browser is hiding device names until you allow camera and microphone access.",
5997
7626
  /* @__PURE__ */ jsx3(
@@ -6008,7 +7637,30 @@ function PreJoinBody({
6008
7637
  ] }) : null,
6009
7638
  devices?.fallbackNotice != null ? /* @__PURE__ */ jsx3("p", { className: "mx-meet__warning", children: devices.fallbackNotice }) : null,
6010
7639
  /* @__PURE__ */ jsx3(DevicePicker, { kind: "audioinput", label: "Microphone" }),
7640
+ /* @__PURE__ */ jsxs2("div", { className: "mx-meet__field-row", children: [
7641
+ /* @__PURE__ */ jsx3(DevicePicker, { kind: "audiooutput", label: "Speaker" }),
7642
+ /* @__PURE__ */ jsxs2(
7643
+ "button",
7644
+ {
7645
+ type: "button",
7646
+ className: "mx-meet__chip",
7647
+ disabled: speakerTesting,
7648
+ onClick: () => {
7649
+ setSpeakerTesting(true);
7650
+ void playTestSound(devices?.selected.audiooutput ?? host.devices.resolve("audiooutput")).finally(
7651
+ () => setSpeakerTesting(false)
7652
+ );
7653
+ },
7654
+ children: [
7655
+ /* @__PURE__ */ jsx3(SpeakerIcon, { width: 14, height: 14 }),
7656
+ " ",
7657
+ speakerTesting ? "Playing\u2026" : "Test"
7658
+ ]
7659
+ }
7660
+ )
7661
+ ] }),
6011
7662
  /* @__PURE__ */ jsx3(DevicePicker, { kind: "videoinput", label: "Camera" }),
7663
+ /* @__PURE__ */ jsx3(EffectsSettings, { preview: preview.preview }),
6012
7664
  recording.consentNotice !== null ? /* @__PURE__ */ jsx3("p", { className: "mx-meet__consent", children: recording.consentNotice }) : null,
6013
7665
  /* @__PURE__ */ jsxs2("div", { className: "mx-meet__prejoin-actions", children: [
6014
7666
  /* @__PURE__ */ jsxs2("label", { className: "mx-meet__toggle", children: [
@@ -6040,6 +7692,7 @@ function PreJoinBody({
6040
7692
  className: "mx-meet__primary",
6041
7693
  onClick: () => {
6042
7694
  setJoining(true);
7695
+ preview.release();
6043
7696
  void room.join({
6044
7697
  roomName,
6045
7698
  mode,
@@ -6059,9 +7712,51 @@ function PreJoinBody({
6059
7712
  ] })
6060
7713
  ] });
6061
7714
  }
7715
+ function useCameraPreview(deviceId2, effects) {
7716
+ const [preview, setPreview] = useState3(null);
7717
+ const [error, setError] = useState3(null);
7718
+ const current = useRef3(null);
7719
+ const initialEffect = useRef3(toVideoEffect(effects));
7720
+ initialEffect.current = toVideoEffect(effects);
7721
+ useEffect3(() => {
7722
+ if (deviceId2 === void 0) return void 0;
7723
+ let live = true;
7724
+ setError(null);
7725
+ void createCameraPreview(deviceId2, initialEffect.current).then((created) => {
7726
+ if (!live) {
7727
+ created.stop();
7728
+ return;
7729
+ }
7730
+ current.current = created;
7731
+ setPreview(created);
7732
+ }).catch((thrown) => {
7733
+ if (!live) return;
7734
+ const failure = thrown;
7735
+ const message = (failure.message ?? "").replace(/^meet\.[A-Za-z.()]+:\s*/, "");
7736
+ const sentence = message.length > 0 ? message.charAt(0).toUpperCase() + message.slice(1) : "";
7737
+ setError([sentence, failure.remedy].filter(Boolean).join(". ") || "The camera did not start.");
7738
+ });
7739
+ return () => {
7740
+ live = false;
7741
+ current.current?.stop();
7742
+ current.current = null;
7743
+ setPreview(null);
7744
+ };
7745
+ }, [deviceId2]);
7746
+ return {
7747
+ preview,
7748
+ error,
7749
+ release: () => {
7750
+ current.current?.stop();
7751
+ current.current = null;
7752
+ setPreview(null);
7753
+ }
7754
+ };
7755
+ }
6062
7756
  function DevicePicker({
6063
7757
  kind,
6064
- label
7758
+ label,
7759
+ onPick
6065
7760
  }) {
6066
7761
  const host = useRequiredMeetHost();
6067
7762
  const devices = useDeviceSnapshot();
@@ -6074,7 +7769,10 @@ function DevicePicker({
6074
7769
  {
6075
7770
  className: "mx-meet__select",
6076
7771
  value: devices?.selected[kind] ?? host.devices.resolve(kind) ?? "",
6077
- onChange: (event) => host.devices.select(kind, event.target.value),
7772
+ onChange: (event) => {
7773
+ if (onPick !== void 0) onPick(kind, event.target.value);
7774
+ else host.devices.select(kind, event.target.value);
7775
+ },
6078
7776
  children: list.map((device) => /* @__PURE__ */ jsx3("option", { value: device.deviceId, children: device.label.length > 0 ? device.label : `${label} (name hidden)` }, device.deviceId))
6079
7777
  }
6080
7778
  )
@@ -6982,19 +8680,23 @@ export {
6982
8680
  ControlBar,
6983
8681
  ControlButton,
6984
8682
  DATA_TOPICS,
8683
+ DEFAULT_EFFECTS,
6985
8684
  DEFAULT_PAGE_SIZE,
6986
8685
  END_MEETING_NOT_SERVED,
6987
8686
  FILE_SESSION_PATH,
8687
+ FlagIcon,
6988
8688
  GUEST_RECORD_ACCESS_CHOICES,
6989
8689
  GUEST_RECORD_ACCESS_LEVELS,
6990
8690
  GUEST_RECORD_ACCESS_METADATA_KEY,
6991
8691
  GUEST_RECORD_NOT_SHARED,
6992
8692
  GuestRecordAccessControl,
6993
8693
  HandIcon,
8694
+ HostMenu,
6994
8695
  IDLE_MEDIA,
6995
8696
  IDLE_TRACK,
6996
8697
  IncomingCallCard,
6997
8698
  IncomingCallHost,
8699
+ KeyboardIcon,
6998
8700
  LISTEN_ONLY_NOTICE,
6999
8701
  LayoutIcon,
7000
8702
  LobbyPanel,
@@ -7004,15 +8706,20 @@ export {
7004
8706
  MEET_ANSWER_CONTEXT_EVENT,
7005
8707
  MEET_ANSWER_EVENTS,
7006
8708
  MEET_EVENTS,
8709
+ MEET_REACTIONS,
7007
8710
  MEET_ROUTES,
7008
8711
  MEET_SCHEMA,
8712
+ MEET_SHORTCUTS,
8713
+ MODERATION_ACTIONS,
7009
8714
  MeetError,
7010
8715
  MeetProvider,
7011
8716
  MeetingList,
8717
+ MeetingOverlays,
7012
8718
  MeetingRecordView,
7013
8719
  MeetingRoom,
7014
8720
  MicIcon,
7015
8721
  MicOffIcon,
8722
+ MoreIcon,
7016
8723
  NOTE_TAKER_IDENTITY_PREFIX,
7017
8724
  NOTE_TAKER_METADATA_KEY,
7018
8725
  NO_GRANTS,
@@ -7020,12 +8727,15 @@ export {
7020
8727
  PARTICIPANT_CAN_RECORD_ATTRIBUTE,
7021
8728
  PARTICIPANT_ROLE_ATTRIBUTE,
7022
8729
  ParticipantTile,
8730
+ PencilIcon,
7023
8731
  PeopleIcon,
7024
8732
  PeoplePanel,
8733
+ PersonMenu,
7025
8734
  PhoneIcon,
7026
8735
  PhoneOffIcon,
7027
8736
  PinIcon,
7028
8737
  PreJoin,
8738
+ QualityBars,
7029
8739
  REACTION_TTL_MS,
7030
8740
  REFUSED_ABSENCE,
7031
8741
  ROLE_RANK,
@@ -7039,8 +8749,15 @@ export {
7039
8749
  RecordingPlayer,
7040
8750
  ScheduleMeeting,
7041
8751
  ScreenShareIcon,
8752
+ SearchIcon,
7042
8753
  SettingsIcon,
8754
+ SettingsPanel,
8755
+ ShortcutsSheet,
8756
+ SignalIcon,
8757
+ SmileIcon,
7043
8758
  SparklesIcon,
8759
+ SpeakerIcon,
8760
+ SpotlightIcon,
7044
8761
  Stage,
7045
8762
  TABLES,
7046
8763
  TRANSCRIPTION_ATTRIBUTES,
@@ -7068,6 +8785,7 @@ export {
7068
8785
  createCallCenter,
7069
8786
  createCallInviteHandler,
7070
8787
  createDeviceManager,
8788
+ createInRoomClient,
7071
8789
  createLiveKitDriver,
7072
8790
  createMeetAi,
7073
8791
  createMeetApi,
@@ -7080,6 +8798,7 @@ export {
7080
8798
  createRoomEngine,
7081
8799
  createTokenClient,
7082
8800
  createWebDeviceStorage,
8801
+ createWebEffectsStorage,
7083
8802
  createWebRoomTokenStorage,
7084
8803
  displayNameFor,
7085
8804
  fileDownloadPath,
@@ -7143,6 +8862,7 @@ export {
7143
8862
  settingsPatch,
7144
8863
  snapshotParticipant,
7145
8864
  unavailableFrom,
8865
+ useAudioLevel,
7146
8866
  useCalls,
7147
8867
  useCanModerate,
7148
8868
  useCanRecord,
@@ -7151,19 +8871,23 @@ export {
7151
8871
  useElapsed,
7152
8872
  useEndMeeting,
7153
8873
  useGuestRecordAccess,
8874
+ useHandQueue,
7154
8875
  useHostControls,
7155
8876
  useIsHost,
7156
8877
  useListenOnlyNotice,
7157
8878
  useLobby,
7158
8879
  useLocalMedia,
7159
8880
  useLocalUserId,
8881
+ useMediaEffects,
7160
8882
  useMeetAi,
7161
8883
  useMeetHost,
8884
+ useMeetShortcuts,
7162
8885
  useMeetSnapshot,
7163
8886
  useMeeting,
7164
8887
  useMeetingId,
7165
8888
  useMeetingRecord,
7166
8889
  useMeetings,
8890
+ useModerationNotice,
7167
8891
  useReactions,
7168
8892
  useRecording,
7169
8893
  useRequiredMeetHost,