@ai-matrx/meet 0.7.3 → 0.7.5

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