@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.cjs CHANGED
@@ -51,19 +51,23 @@ __export(react_exports, {
51
51
  ControlBar: () => ControlBar,
52
52
  ControlButton: () => ControlButton,
53
53
  DATA_TOPICS: () => DATA_TOPICS,
54
+ DEFAULT_EFFECTS: () => DEFAULT_EFFECTS,
54
55
  DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
55
56
  END_MEETING_NOT_SERVED: () => END_MEETING_NOT_SERVED,
56
57
  FILE_SESSION_PATH: () => FILE_SESSION_PATH,
58
+ FlagIcon: () => FlagIcon,
57
59
  GUEST_RECORD_ACCESS_CHOICES: () => GUEST_RECORD_ACCESS_CHOICES,
58
60
  GUEST_RECORD_ACCESS_LEVELS: () => GUEST_RECORD_ACCESS_LEVELS,
59
61
  GUEST_RECORD_ACCESS_METADATA_KEY: () => GUEST_RECORD_ACCESS_METADATA_KEY,
60
62
  GUEST_RECORD_NOT_SHARED: () => GUEST_RECORD_NOT_SHARED,
61
63
  GuestRecordAccessControl: () => GuestRecordAccessControl,
62
64
  HandIcon: () => HandIcon,
65
+ HostMenu: () => HostMenu,
63
66
  IDLE_MEDIA: () => IDLE_MEDIA,
64
67
  IDLE_TRACK: () => IDLE_TRACK,
65
68
  IncomingCallCard: () => IncomingCallCard,
66
69
  IncomingCallHost: () => IncomingCallHost,
70
+ KeyboardIcon: () => KeyboardIcon,
67
71
  LISTEN_ONLY_NOTICE: () => LISTEN_ONLY_NOTICE,
68
72
  LayoutIcon: () => LayoutIcon,
69
73
  LobbyPanel: () => LobbyPanel,
@@ -73,15 +77,20 @@ __export(react_exports, {
73
77
  MEET_ANSWER_CONTEXT_EVENT: () => MEET_ANSWER_CONTEXT_EVENT,
74
78
  MEET_ANSWER_EVENTS: () => MEET_ANSWER_EVENTS,
75
79
  MEET_EVENTS: () => MEET_EVENTS,
80
+ MEET_REACTIONS: () => MEET_REACTIONS,
76
81
  MEET_ROUTES: () => MEET_ROUTES,
77
82
  MEET_SCHEMA: () => MEET_SCHEMA,
83
+ MEET_SHORTCUTS: () => MEET_SHORTCUTS,
84
+ MODERATION_ACTIONS: () => MODERATION_ACTIONS,
78
85
  MeetError: () => MeetError,
79
86
  MeetProvider: () => MeetProvider,
80
87
  MeetingList: () => MeetingList,
88
+ MeetingOverlays: () => MeetingOverlays,
81
89
  MeetingRecordView: () => MeetingRecordView,
82
90
  MeetingRoom: () => MeetingRoom,
83
91
  MicIcon: () => MicIcon,
84
92
  MicOffIcon: () => MicOffIcon,
93
+ MoreIcon: () => MoreIcon,
85
94
  NOTE_TAKER_IDENTITY_PREFIX: () => NOTE_TAKER_IDENTITY_PREFIX,
86
95
  NOTE_TAKER_METADATA_KEY: () => NOTE_TAKER_METADATA_KEY,
87
96
  NO_GRANTS: () => NO_GRANTS,
@@ -89,12 +98,15 @@ __export(react_exports, {
89
98
  PARTICIPANT_CAN_RECORD_ATTRIBUTE: () => PARTICIPANT_CAN_RECORD_ATTRIBUTE,
90
99
  PARTICIPANT_ROLE_ATTRIBUTE: () => PARTICIPANT_ROLE_ATTRIBUTE,
91
100
  ParticipantTile: () => ParticipantTile,
101
+ PencilIcon: () => PencilIcon,
92
102
  PeopleIcon: () => PeopleIcon,
93
103
  PeoplePanel: () => PeoplePanel,
104
+ PersonMenu: () => PersonMenu,
94
105
  PhoneIcon: () => PhoneIcon,
95
106
  PhoneOffIcon: () => PhoneOffIcon,
96
107
  PinIcon: () => PinIcon,
97
108
  PreJoin: () => PreJoin,
109
+ QualityBars: () => QualityBars,
98
110
  REACTION_TTL_MS: () => REACTION_TTL_MS,
99
111
  REFUSED_ABSENCE: () => REFUSED_ABSENCE,
100
112
  ROLE_RANK: () => ROLE_RANK,
@@ -108,8 +120,15 @@ __export(react_exports, {
108
120
  RecordingPlayer: () => RecordingPlayer,
109
121
  ScheduleMeeting: () => ScheduleMeeting,
110
122
  ScreenShareIcon: () => ScreenShareIcon,
123
+ SearchIcon: () => SearchIcon,
111
124
  SettingsIcon: () => SettingsIcon,
125
+ SettingsPanel: () => SettingsPanel,
126
+ ShortcutsSheet: () => ShortcutsSheet,
127
+ SignalIcon: () => SignalIcon,
128
+ SmileIcon: () => SmileIcon,
112
129
  SparklesIcon: () => SparklesIcon,
130
+ SpeakerIcon: () => SpeakerIcon,
131
+ SpotlightIcon: () => SpotlightIcon,
113
132
  Stage: () => Stage,
114
133
  TABLES: () => TABLES,
115
134
  TRANSCRIPTION_ATTRIBUTES: () => TRANSCRIPTION_ATTRIBUTES,
@@ -137,6 +156,7 @@ __export(react_exports, {
137
156
  createCallCenter: () => createCallCenter,
138
157
  createCallInviteHandler: () => createCallInviteHandler,
139
158
  createDeviceManager: () => createDeviceManager,
159
+ createInRoomClient: () => createInRoomClient,
140
160
  createLiveKitDriver: () => createLiveKitDriver,
141
161
  createMeetAi: () => createMeetAi,
142
162
  createMeetApi: () => createMeetApi,
@@ -149,6 +169,7 @@ __export(react_exports, {
149
169
  createRoomEngine: () => createRoomEngine,
150
170
  createTokenClient: () => createTokenClient,
151
171
  createWebDeviceStorage: () => createWebDeviceStorage,
172
+ createWebEffectsStorage: () => createWebEffectsStorage,
152
173
  createWebRoomTokenStorage: () => createWebRoomTokenStorage,
153
174
  displayNameFor: () => displayNameFor,
154
175
  fileDownloadPath: () => fileDownloadPath,
@@ -212,6 +233,7 @@ __export(react_exports, {
212
233
  settingsPatch: () => settingsPatch,
213
234
  snapshotParticipant: () => snapshotParticipant,
214
235
  unavailableFrom: () => unavailableFrom,
236
+ useAudioLevel: () => useAudioLevel,
215
237
  useCalls: () => useCalls,
216
238
  useCanModerate: () => useCanModerate,
217
239
  useCanRecord: () => useCanRecord,
@@ -220,19 +242,23 @@ __export(react_exports, {
220
242
  useElapsed: () => useElapsed,
221
243
  useEndMeeting: () => useEndMeeting,
222
244
  useGuestRecordAccess: () => useGuestRecordAccess,
245
+ useHandQueue: () => useHandQueue,
223
246
  useHostControls: () => useHostControls,
224
247
  useIsHost: () => useIsHost,
225
248
  useListenOnlyNotice: () => useListenOnlyNotice,
226
249
  useLobby: () => useLobby,
227
250
  useLocalMedia: () => useLocalMedia,
228
251
  useLocalUserId: () => useLocalUserId,
252
+ useMediaEffects: () => useMediaEffects,
229
253
  useMeetAi: () => useMeetAi,
230
254
  useMeetHost: () => useMeetHost,
255
+ useMeetShortcuts: () => useMeetShortcuts,
231
256
  useMeetSnapshot: () => useMeetSnapshot,
232
257
  useMeeting: () => useMeeting,
233
258
  useMeetingId: () => useMeetingId,
234
259
  useMeetingRecord: () => useMeetingRecord,
235
260
  useMeetings: () => useMeetings,
261
+ useModerationNotice: () => useModerationNotice,
236
262
  useReactions: () => useReactions,
237
263
  useRecording: () => useRecording,
238
264
  useRequiredMeetHost: () => useRequiredMeetHost,
@@ -442,7 +468,16 @@ var MEET_ROUTES = {
442
468
  */
443
469
  meetingAnnounce: "/api/v1/meet/meetings/{meeting_id}/announce",
444
470
  /** Put a one-off meeting on the caller's own Google Calendar (gated by the server). */
445
- meetingGoogleCalendar: "/api/v1/meet/meetings/{meeting_id}/google-calendar"
471
+ meetingGoogleCalendar: "/api/v1/meet/meetings/{meeting_id}/google-calendar",
472
+ /**
473
+ * Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
474
+ * unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
475
+ * Decided by the server from the meeting itself; a guest's pass rides
476
+ * `x-meet-room-token`.
477
+ */
478
+ moderate: "/api/v1/meet/moderate",
479
+ /** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */
480
+ chat: "/api/v1/meet/chat"
446
481
  };
447
482
  function meetingPath(template, meetingId) {
448
483
  return template.replace("{meeting_id}", encodeURIComponent(meetingId));
@@ -1747,6 +1782,11 @@ function captionSegment(args) {
1747
1782
  }
1748
1783
 
1749
1784
  // src/core/store.ts
1785
+ var DEFAULT_EFFECTS = {
1786
+ noiseSuppression: true,
1787
+ background: "none",
1788
+ backgroundImage: null
1789
+ };
1750
1790
  var NO_GRANTS = {
1751
1791
  role: "participant",
1752
1792
  canRecord: false,
@@ -1769,7 +1809,12 @@ var EMPTY = {
1769
1809
  recordingSource: "local",
1770
1810
  recordingId: null,
1771
1811
  chat: [],
1812
+ chatLoaded: false,
1772
1813
  reactions: [],
1814
+ spotlight: null,
1815
+ moderationNotice: null,
1816
+ actionNotice: null,
1817
+ effects: DEFAULT_EFFECTS,
1773
1818
  transcript: [],
1774
1819
  notes: [],
1775
1820
  names: {},
@@ -1808,6 +1853,7 @@ function createMeetStore(initial = {}) {
1808
1853
  setParticipants(participants) {
1809
1854
  const names = learnNames(state.names, participants);
1810
1855
  const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
1856
+ const handTimes = new Map(state.participants.map((p) => [p.identity, p.handRaisedAt ?? null]));
1811
1857
  const quality = new Map(state.participants.map((p) => [p.identity, p.quality]));
1812
1858
  commit({
1813
1859
  ...state,
@@ -1821,6 +1867,7 @@ function createMeetStore(initial = {}) {
1821
1867
  // name book. `roster-names.test.ts` fails if a token ever gets past it.
1822
1868
  displayName: displayNameFor(names, participant.identity, participant.displayName),
1823
1869
  handRaised: hands.get(participant.identity) ?? participant.handRaised,
1870
+ handRaisedAt: handTimes.get(participant.identity) ?? participant.handRaisedAt ?? null,
1824
1871
  quality: participant.quality === "unknown" ? quality.get(participant.identity) ?? "unknown" : participant.quality
1825
1872
  }))
1826
1873
  });
@@ -1843,11 +1890,17 @@ function createMeetStore(initial = {}) {
1843
1890
  )
1844
1891
  });
1845
1892
  },
1846
- setHand(identity, raised) {
1893
+ setHand(identity, raised, at) {
1847
1894
  commit({
1848
1895
  ...state,
1849
1896
  participants: state.participants.map(
1850
- (participant) => participant.identity === identity ? { ...participant, handRaised: raised } : participant
1897
+ (participant) => participant.identity === identity ? {
1898
+ ...participant,
1899
+ handRaised: raised,
1900
+ // A hand that is ALREADY up keeps its place in the queue when
1901
+ // it is re-announced (to a late joiner): the first time wins.
1902
+ handRaisedAt: raised ? participant.handRaised && participant.handRaisedAt != null ? participant.handRaisedAt : at ?? Date.now() : null
1903
+ } : participant
1851
1904
  )
1852
1905
  });
1853
1906
  },
@@ -1857,6 +1910,31 @@ function createMeetStore(initial = {}) {
1857
1910
  const names = learnName(state.names, message.identity, message.displayName);
1858
1911
  commit({ ...state, chat: chat.slice(-CHAT_LIMIT), names });
1859
1912
  },
1913
+ loadChat(messages) {
1914
+ const known = new Set(state.chat.map((message) => message.id));
1915
+ const merged = [...messages.filter((message) => !known.has(message.id)), ...state.chat].sort(
1916
+ (a, b) => a.at < b.at ? -1 : a.at > b.at ? 1 : 0
1917
+ );
1918
+ const names = learnNames(
1919
+ state.names,
1920
+ messages.map((message) => ({ identity: message.identity, displayName: message.displayName }))
1921
+ );
1922
+ commit({ ...state, chat: merged.slice(-CHAT_LIMIT), chatLoaded: true, names });
1923
+ },
1924
+ settleChat(localId, next) {
1925
+ const index = state.chat.findIndex((message) => message.id === localId);
1926
+ if (index === -1) return;
1927
+ const current = state.chat[index];
1928
+ let replacement;
1929
+ if ("failed" in next && typeof next.failed === "string" && !("id" in next)) {
1930
+ replacement = { ...current, failed: next.failed };
1931
+ } else {
1932
+ replacement = next;
1933
+ }
1934
+ const duplicate = replacement.id !== localId && state.chat.some((message) => message.id === replacement.id);
1935
+ const chat = duplicate ? state.chat.filter((message) => message.id !== localId) : state.chat.map((message, position) => position === index ? replacement : message);
1936
+ commit({ ...state, chat });
1937
+ },
1860
1938
  addReaction(reaction) {
1861
1939
  commit({ ...state, reactions: [...state.reactions, reaction] });
1862
1940
  },
@@ -1947,7 +2025,7 @@ function createMeetStore(initial = {}) {
1947
2025
  commit({ ...state, media: { ...state.media, [kind]: next } });
1948
2026
  },
1949
2027
  reset() {
1950
- commit({ ...EMPTY });
2028
+ commit({ ...EMPTY, effects: state.effects });
1951
2029
  }
1952
2030
  };
1953
2031
  }
@@ -3202,7 +3280,19 @@ var ROOM_EVENTS = {
3202
3280
  * production 2026-09-09 the guest lane showed no indicator at all while the
3203
3281
  * host recorded (MRI-C8).
3204
3282
  */
3205
- recording: "matrx.meet.recording"
3283
+ recording: "matrx.meet.recording",
3284
+ /**
3285
+ * A chat message to everyone was SAVED (Meet wave 2). Payload
3286
+ * `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
3287
+ */
3288
+ chat: "matrx.meet.chat",
3289
+ /**
3290
+ * A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`:
3291
+ * `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the
3292
+ * person concerned; `lower-hands {identities: [...] | "*"}` · `role {identity,
3293
+ * role}` — to everyone.
3294
+ */
3295
+ moderation: "matrx.meet.moderation"
3206
3296
  };
3207
3297
  function isRoomEventTopic(topic) {
3208
3298
  return topic.startsWith(ROOM_EVENT_PREFIX);
@@ -3238,6 +3328,10 @@ function applyRoomEvent(args) {
3238
3328
  return applyEnded(store, payload);
3239
3329
  case ROOM_EVENTS.recording:
3240
3330
  return applyRecording(store, payload);
3331
+ case ROOM_EVENTS.chat:
3332
+ return applyChat(store, payload);
3333
+ case ROOM_EVENTS.moderation:
3334
+ return applyModeration(store, payload, now);
3241
3335
  default:
3242
3336
  return ignored(`${topic} is not known to this version of @ai-matrx/meet`);
3243
3337
  }
@@ -3329,6 +3423,101 @@ function applyEnded(store, payload) {
3329
3423
  store.patch({ meeting: { ...meeting, endedAt } });
3330
3424
  return HANDLED;
3331
3425
  }
3426
+ function chatFromRow(row) {
3427
+ const id = text2(row["id"]);
3428
+ const identity = text2(row["sender_identity"]);
3429
+ const body = text2(row["body"]);
3430
+ if (id === null || identity === null || body === null) return null;
3431
+ return {
3432
+ id,
3433
+ identity: asParticipantIdentity(identity),
3434
+ displayName: text2(row["sender_name"]) ?? "Participant",
3435
+ text: body,
3436
+ at: text2(row["created_at"]) ?? (/* @__PURE__ */ new Date()).toISOString(),
3437
+ // `false` only from a server whose chat table has not landed: the room heard
3438
+ // it, nobody will see it later — and the line says so.
3439
+ saved: row["saved"] !== false
3440
+ };
3441
+ }
3442
+ function applyChat(store, payload) {
3443
+ const row = object(payload["message"]);
3444
+ if (row === null) return ignored("a chat event with no `message` row");
3445
+ const meeting = store.snapshot().meeting;
3446
+ if (meeting !== null && text2(row["meeting_id"]) !== null && row["meeting_id"] !== meeting.id) {
3447
+ return ignored("a chat message for a different meeting");
3448
+ }
3449
+ const message = chatFromRow(row);
3450
+ if (message === null) return ignored("a chat row with no id, sender or body");
3451
+ store.addChat(message);
3452
+ return HANDLED;
3453
+ }
3454
+ function moderationText(kind, payload) {
3455
+ const by = text2(payload["by"]) ?? "The host";
3456
+ switch (kind) {
3457
+ case "muted": {
3458
+ const source = text2(payload["source"]) ?? "microphone";
3459
+ return source === "microphone" ? `${by} muted you.` : source === "camera" ? `${by} turned off your camera.` : `${by} stopped your screen share.`;
3460
+ }
3461
+ case "ask-unmute":
3462
+ return `${by} is asking you to unmute.`;
3463
+ case "removed":
3464
+ return `${by} removed you from this meeting.`;
3465
+ default:
3466
+ return null;
3467
+ }
3468
+ }
3469
+ function applyModeration(store, payload, now) {
3470
+ const snapshot = store.snapshot();
3471
+ const meetingId = text2(payload["meeting_id"]);
3472
+ if (snapshot.meeting !== null && meetingId !== null && meetingId !== snapshot.meeting.id) {
3473
+ return ignored("a moderation event for a different meeting");
3474
+ }
3475
+ const kind = text2(payload["kind"]);
3476
+ switch (kind) {
3477
+ case "lower-hands": {
3478
+ const raw = payload["identities"];
3479
+ const everyone = raw === "*";
3480
+ const named = new Set(
3481
+ Array.isArray(raw) ? raw.filter((value) => typeof value === "string") : []
3482
+ );
3483
+ for (const participant of snapshot.participants) {
3484
+ if (participant.handRaised && (everyone || named.has(participant.identity))) {
3485
+ store.setHand(participant.identity, false);
3486
+ }
3487
+ }
3488
+ return HANDLED;
3489
+ }
3490
+ case "role": {
3491
+ const identity = text2(payload["identity"]);
3492
+ const role = text2(payload["role"]);
3493
+ if (identity === null || role === null || !ROLES.includes(role)) {
3494
+ return ignored("a role event with no identity or an unknown role");
3495
+ }
3496
+ if (identity === snapshot.localIdentity) {
3497
+ store.patch({
3498
+ grants: { ...snapshot.grants, role },
3499
+ moderationNotice: {
3500
+ kind: "role",
3501
+ text: role === "host" ? "You are now the host." : role === "cohost" ? "You are now a co-host." : "You are no longer a co-host.",
3502
+ at: now()
3503
+ }
3504
+ });
3505
+ }
3506
+ return HANDLED;
3507
+ }
3508
+ case "muted":
3509
+ case "ask-unmute":
3510
+ case "removed": {
3511
+ const sentence = moderationText(kind, payload);
3512
+ if (sentence === null) return ignored("a moderation event with no sentence");
3513
+ store.patch({ moderationNotice: { kind, text: sentence, at: now() } });
3514
+ return HANDLED;
3515
+ }
3516
+ default:
3517
+ return ignored(`a moderation kind this version does not know: ${String(kind)}`);
3518
+ }
3519
+ }
3520
+ var ROLES = ["host", "cohost", "participant", "guest"];
3332
3521
 
3333
3522
  // src/core/note-taker.ts
3334
3523
  var NOTE_TAKER_IDENTITY_PREFIX = "agent:note_taker:";
@@ -3393,8 +3582,7 @@ function noteTakerLabel(status) {
3393
3582
  var DATA_TOPICS = {
3394
3583
  hand: "mx.hand",
3395
3584
  reaction: "mx.reaction",
3396
- chat: "mx.chat",
3397
- hostAction: "mx.host"
3585
+ chat: "mx.chat"
3398
3586
  };
3399
3587
  var TRANSCRIPTION_TOPIC = "lk.transcription";
3400
3588
  var TRANSCRIPTION_ATTRIBUTES = {
@@ -3530,7 +3718,37 @@ function createRoomEngine(options) {
3530
3718
  const handlers = {
3531
3719
  participants(list) {
3532
3720
  const joinedAt = new Date(now()).toISOString();
3721
+ const before = new Set(store.snapshot().participants.map((p) => p.identity));
3533
3722
  store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
3723
+ const snapshot = store.snapshot();
3724
+ const newcomer = snapshot.participants.some(
3725
+ (participant) => !participant.isLocal && !before.has(participant.identity)
3726
+ );
3727
+ const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
3728
+ if (newcomer && before.size > 0 && me?.handRaised === true) {
3729
+ void publish(DATA_TOPICS.hand, { raised: true, raisedAt: me.handRaisedAt ?? now() }).catch(
3730
+ () => void 0
3731
+ );
3732
+ }
3733
+ },
3734
+ roomMetadata(metadata) {
3735
+ let spotlight = null;
3736
+ if (metadata.length > 0) {
3737
+ try {
3738
+ const parsed = JSON.parse(metadata);
3739
+ if (typeof parsed === "object" && parsed !== null) {
3740
+ const value = parsed.spotlight;
3741
+ if (typeof value === "string" && value.length > 0) spotlight = asParticipantIdentity(value);
3742
+ }
3743
+ } catch {
3744
+ report(
3745
+ "warn",
3746
+ "meet: the room's metadata is not JSON, so no spotlight was read from it",
3747
+ "Only aidream writes room metadata; check services/meet/moderation.py."
3748
+ );
3749
+ }
3750
+ }
3751
+ if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
3534
3752
  },
3535
3753
  activeSpeakers(identities) {
3536
3754
  store.setSpeaking(identities.map(asParticipantIdentity));
@@ -3634,7 +3852,11 @@ function createRoomEngine(options) {
3634
3852
  const sender = asParticipantIdentity(from ?? "");
3635
3853
  switch (topic) {
3636
3854
  case DATA_TOPICS.hand:
3637
- store.setHand(sender, envelope.raised === true);
3855
+ store.setHand(
3856
+ sender,
3857
+ envelope.raised === true,
3858
+ typeof envelope.raisedAt === "number" ? envelope.raisedAt : now()
3859
+ );
3638
3860
  return;
3639
3861
  case DATA_TOPICS.reaction:
3640
3862
  if (typeof envelope.emoji === "string") {
@@ -3648,12 +3870,14 @@ function createRoomEngine(options) {
3648
3870
  return;
3649
3871
  case DATA_TOPICS.chat:
3650
3872
  if (typeof envelope.text === "string") {
3873
+ const local = store.snapshot().localIdentity;
3651
3874
  store.addChat({
3652
3875
  id: envelope.id ?? `${sender}-${now()}`,
3653
3876
  identity: sender,
3654
3877
  displayName: envelope.name ?? sender,
3655
3878
  text: envelope.text,
3656
- at: envelope.at ?? new Date(now()).toISOString()
3879
+ at: envelope.at ?? new Date(now()).toISOString(),
3880
+ ...typeof envelope.to === "string" && local !== null ? { to: local } : {}
3657
3881
  });
3658
3882
  }
3659
3883
  return;
@@ -3699,6 +3923,24 @@ function createRoomEngine(options) {
3699
3923
  store.patch({ phase: "connected", failure: null });
3700
3924
  store.setLocalTrack("microphone", IDLE_TRACK);
3701
3925
  store.setLocalTrack("camera", IDLE_TRACK);
3926
+ const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
3927
+ store.patch({ effects });
3928
+ if (!effects.noiseSuppression || effects.background !== "none") {
3929
+ try {
3930
+ await applyEffects(created, effects);
3931
+ } catch {
3932
+ }
3933
+ }
3934
+ for (const kind of ["audioinput", "videoinput", "audiooutput"]) {
3935
+ const chosen = options.resolveDevice?.(kind) ?? null;
3936
+ if (chosen === null || chosen === "default") continue;
3937
+ try {
3938
+ await created.switchDevice(kind, chosen);
3939
+ } catch (error) {
3940
+ const normalized = normalizeDeviceError(error, `meet.switchDevice(${kind})`);
3941
+ report("warn", normalized.message, normalized.remedy);
3942
+ }
3943
+ }
3702
3944
  if (request.mode === "video") {
3703
3945
  await enableTrack(created, "camera", true);
3704
3946
  }
@@ -3721,9 +3963,30 @@ function createRoomEngine(options) {
3721
3963
  return state;
3722
3964
  }
3723
3965
  }
3724
- async function publish(topic, body) {
3966
+ async function publish(topic, body, to) {
3725
3967
  if (driver === null) return;
3726
- await driver.publishData(topic, JSON.stringify(body));
3968
+ await driver.publishData(topic, JSON.stringify(body), to);
3969
+ }
3970
+ async function applyEffects(target, effects) {
3971
+ try {
3972
+ await target.setAudioProcessing?.({
3973
+ noiseSuppression: effects.noiseSuppression,
3974
+ echoCancellation: true,
3975
+ autoGainControl: true
3976
+ });
3977
+ } catch (error) {
3978
+ report("warn", `meet: noise suppression could not be applied: ${String(error)}`, "The microphone keeps working without it.");
3979
+ }
3980
+ if (target.setVideoEffect === void 0) return;
3981
+ const effect = effects.background === "blur" ? { kind: "blur" } : effects.background === "image" && effects.backgroundImage !== null ? { kind: "image", imageUrl: effects.backgroundImage } : { kind: "none" };
3982
+ if (effect.kind !== "none" && target.supportsVideoEffects?.() === false) return;
3983
+ try {
3984
+ await target.setVideoEffect(effect);
3985
+ } catch (error) {
3986
+ const normalized = normalizeMeetError(error, "meet.setVideoEffect");
3987
+ report("warn", normalized.message, normalized.remedy);
3988
+ throw normalized;
3989
+ }
3727
3990
  }
3728
3991
  return {
3729
3992
  join(request) {
@@ -3779,7 +4042,7 @@ function createRoomEngine(options) {
3779
4042
  async raiseHand(raised) {
3780
4043
  const local = store.snapshot().localIdentity;
3781
4044
  if (local !== null) store.setHand(local, raised);
3782
- await publish(DATA_TOPICS.hand, { raised });
4045
+ await publish(DATA_TOPICS.hand, { raised, raisedAt: now() });
3783
4046
  },
3784
4047
  async react(emoji) {
3785
4048
  const local = store.snapshot().localIdentity;
@@ -3789,32 +4052,41 @@ function createRoomEngine(options) {
3789
4052
  }
3790
4053
  await publish(DATA_TOPICS.reaction, { id, emoji });
3791
4054
  },
3792
- async sendChat(text3) {
4055
+ async sendPrivateChat(to, text3) {
3793
4056
  const trimmed = text3.trim();
3794
4057
  if (trimmed.length === 0) return;
3795
4058
  const local = store.snapshot().localIdentity;
3796
4059
  const id = `${local ?? "local"}-${now()}`;
3797
4060
  const at = new Date(now()).toISOString();
3798
4061
  if (local !== null) {
3799
- store.addChat({
3800
- id,
3801
- identity: local,
3802
- displayName: identity.displayName,
3803
- text: trimmed,
3804
- at
3805
- });
4062
+ store.addChat({ id, identity: local, displayName: identity.displayName, text: trimmed, at, to });
3806
4063
  }
3807
- await publish(DATA_TOPICS.chat, { id, text: trimmed, at, name: identity.displayName });
4064
+ await publish(
4065
+ DATA_TOPICS.chat,
4066
+ { id, text: trimmed, at, name: identity.displayName, to },
4067
+ [to]
4068
+ );
3808
4069
  },
3809
- async publishHostAction(action) {
3810
- if (!isHostRole(effectiveRole(store.snapshot()))) {
3811
- throw new MeetError(
3812
- "forbidden",
3813
- `meet.hostAction(${action.kind}): this participant is not a host`,
3814
- "Host powers come from the server-minted token metadata. Hide the control instead of rendering a dead button (no dead ends)."
3815
- );
4070
+ async setEffects(next) {
4071
+ const previous = store.snapshot().effects;
4072
+ const merged = { ...previous, ...next };
4073
+ store.patch({ effects: merged });
4074
+ options.effectsStorage?.save(merged);
4075
+ if (driver === null) return;
4076
+ try {
4077
+ await applyEffects(driver, merged);
4078
+ } catch {
4079
+ const reverted = { ...merged, background: previous.background, backgroundImage: previous.backgroundImage };
4080
+ store.patch({ effects: reverted });
4081
+ options.effectsStorage?.save(reverted);
3816
4082
  }
3817
- await publish(DATA_TOPICS.hostAction, { ...action });
4083
+ },
4084
+ supportsVideoEffects() {
4085
+ return driver?.supportsVideoEffects?.() ?? false;
4086
+ },
4087
+ async switchDevice(kind, deviceId2) {
4088
+ if (driver === null) return;
4089
+ await driver.switchDevice(kind, deviceId2);
3818
4090
  },
3819
4091
  pin(who) {
3820
4092
  store.patch({ pinned: who, layout: who === null ? "grid" : "speaker" });
@@ -3845,6 +4117,192 @@ function hostUserIds(participants) {
3845
4117
  return participants.filter((participant) => participant.role === "host" && participant.userId !== null).map((participant) => participant.userId);
3846
4118
  }
3847
4119
 
4120
+ // src/core/effects.ts
4121
+ function coerce(value) {
4122
+ if (typeof value !== "object" || value === null) return DEFAULT_EFFECTS;
4123
+ const row = value;
4124
+ const background = row["background"] === "blur" || row["background"] === "image" ? row["background"] : "none";
4125
+ const backgroundImage = typeof row["backgroundImage"] === "string" ? row["backgroundImage"] : null;
4126
+ return {
4127
+ noiseSuppression: row["noiseSuppression"] !== false,
4128
+ background: background === "image" && backgroundImage === null ? "none" : background,
4129
+ backgroundImage
4130
+ };
4131
+ }
4132
+ function createWebEffectsStorage(key = "mx.meet.effects") {
4133
+ let memory = DEFAULT_EFFECTS;
4134
+ const storage = globalThis.localStorage;
4135
+ return {
4136
+ load() {
4137
+ if (storage === void 0) return memory;
4138
+ try {
4139
+ const raw = storage.getItem(key);
4140
+ return raw === null ? memory : coerce(JSON.parse(raw));
4141
+ } catch {
4142
+ return memory;
4143
+ }
4144
+ },
4145
+ save(effects) {
4146
+ memory = effects;
4147
+ try {
4148
+ storage?.setItem(key, JSON.stringify(effects));
4149
+ } catch {
4150
+ }
4151
+ }
4152
+ };
4153
+ }
4154
+
4155
+ // src/core/in-room.ts
4156
+ var MODERATION_ACTIONS = [
4157
+ "mute",
4158
+ "stop_video",
4159
+ "stop_screen",
4160
+ "mute_all",
4161
+ "ask_unmute",
4162
+ "ask_all_unmute",
4163
+ "spotlight",
4164
+ "lower_hand",
4165
+ "lower_all_hands",
4166
+ "make_cohost",
4167
+ "revoke_cohost",
4168
+ "transfer_host",
4169
+ "rename",
4170
+ "remove",
4171
+ "report"
4172
+ ];
4173
+ function createInRoomClient(options) {
4174
+ const { api, store } = options;
4175
+ const now = options.now ?? (() => Date.now());
4176
+ function meetingId(operation) {
4177
+ const meeting = store.snapshot().meeting;
4178
+ if (meeting === null) {
4179
+ throw new MeetError(
4180
+ "not-found",
4181
+ `${operation}: this room has no meeting record`,
4182
+ "Moderation and saved chat belong to a meeting. An ad-hoc call has neither."
4183
+ );
4184
+ }
4185
+ return meeting.id;
4186
+ }
4187
+ function headers() {
4188
+ const token = options.resolveRoomToken();
4189
+ return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
4190
+ }
4191
+ async function refusal2(response, operation, remedy) {
4192
+ let body = null;
4193
+ try {
4194
+ body = await response.json();
4195
+ } catch {
4196
+ body = null;
4197
+ }
4198
+ const said = readServerRefusal(body);
4199
+ return new MeetError(
4200
+ response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
4201
+ said?.reason ?? `${operation}: the server answered ${response.status}`,
4202
+ said?.remedy !== void 0 && said.remedy.length > 0 ? said.remedy : remedy
4203
+ );
4204
+ }
4205
+ return {
4206
+ async moderate(request) {
4207
+ const operation = `meet.moderate(${request.action})`;
4208
+ const hdrs = headers();
4209
+ const response = await api.request({
4210
+ path: MEET_ROUTES.moderate,
4211
+ method: "POST",
4212
+ operation,
4213
+ body: {
4214
+ meeting_id: meetingId(operation),
4215
+ action: request.action,
4216
+ ...request.identity != null ? { identity: request.identity } : {},
4217
+ ...request.name !== void 0 ? { name: request.name } : {},
4218
+ ...request.reason !== void 0 ? { reason: request.reason } : {}
4219
+ },
4220
+ ...hdrs !== void 0 ? { headers: hdrs } : {}
4221
+ });
4222
+ if (!response.ok) {
4223
+ throw await refusal2(
4224
+ response,
4225
+ operation,
4226
+ "Nothing was changed. The server decides host powers from the meeting itself."
4227
+ );
4228
+ }
4229
+ const payload = await response.json();
4230
+ return {
4231
+ action: request.action,
4232
+ affected: Array.isArray(payload["affected"]) ? payload["affected"].filter((value) => typeof value === "string") : [],
4233
+ message: typeof payload["message"] === "string" ? payload["message"] : "Done."
4234
+ };
4235
+ },
4236
+ async sendChat(text3) {
4237
+ const body = text3.trim();
4238
+ if (body.length === 0) return;
4239
+ const operation = "meet.sendChat";
4240
+ const id = meetingId(operation);
4241
+ const snapshot = store.snapshot();
4242
+ const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
4243
+ const localId = `pending-${now()}`;
4244
+ const pending = {
4245
+ id: localId,
4246
+ identity: snapshot.localIdentity ?? "local",
4247
+ displayName: me?.displayName ?? "You",
4248
+ text: body,
4249
+ at: new Date(now()).toISOString(),
4250
+ saved: false
4251
+ };
4252
+ store.addChat(pending);
4253
+ const hdrs = headers();
4254
+ let response;
4255
+ try {
4256
+ response = await api.request({
4257
+ path: MEET_ROUTES.chat,
4258
+ method: "POST",
4259
+ operation,
4260
+ body: { meeting_id: id, body },
4261
+ ...hdrs !== void 0 ? { headers: hdrs } : {}
4262
+ });
4263
+ } catch (error) {
4264
+ store.settleChat(localId, { failed: "Not sent \u2014 the server could not be reached." });
4265
+ throw error;
4266
+ }
4267
+ if (!response.ok) {
4268
+ const error = await refusal2(response, operation, "Your message was not sent.");
4269
+ store.settleChat(localId, { failed: error.message });
4270
+ throw error;
4271
+ }
4272
+ const saved = chatFromRow(await response.json());
4273
+ if (saved === null) {
4274
+ store.settleChat(localId, { failed: "The server's answer could not be read." });
4275
+ return;
4276
+ }
4277
+ store.settleChat(localId, saved);
4278
+ },
4279
+ async loadChat() {
4280
+ const operation = "meet.loadChat";
4281
+ const id = meetingId(operation);
4282
+ const hdrs = headers();
4283
+ const response = await api.request({
4284
+ path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(id)}`,
4285
+ method: "GET",
4286
+ operation,
4287
+ ...hdrs !== void 0 ? { headers: hdrs } : {}
4288
+ });
4289
+ if (!response.ok) {
4290
+ throw await refusal2(
4291
+ response,
4292
+ operation,
4293
+ "Messages sent before you joined could not be read. New messages still arrive."
4294
+ );
4295
+ }
4296
+ const payload = await response.json();
4297
+ const rows = Array.isArray(payload.messages) ? payload.messages : [];
4298
+ const messages = rows.map(
4299
+ (row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
4300
+ ).filter((message) => message !== null);
4301
+ store.loadChat(messages);
4302
+ }
4303
+ };
4304
+ }
4305
+
3848
4306
  // src/core/livekit-driver.ts
3849
4307
  function mapQuality(quality) {
3850
4308
  switch (quality) {
@@ -3889,11 +4347,48 @@ function snapshotParticipant(participant, isLocal) {
3889
4347
  screenSharing: participant.isScreenShareEnabled
3890
4348
  };
3891
4349
  }
4350
+ var DEFAULT_AUDIO_PROCESSING = {
4351
+ noiseSuppression: true,
4352
+ echoCancellation: true,
4353
+ autoGainControl: true
4354
+ };
4355
+ function videoEffectsSupported() {
4356
+ const scope = globalThis;
4357
+ if (typeof scope.OffscreenCanvas === "undefined" || scope.document === void 0) return false;
4358
+ try {
4359
+ return scope.document.createElement("canvas").getContext("webgl2") !== null;
4360
+ } catch {
4361
+ return false;
4362
+ }
4363
+ }
4364
+ async function applyVideoEffect(target, effect) {
4365
+ const { Track } = await import("livekit-client");
4366
+ const track = target.localParticipant.getTrackPublication(Track.Source.Camera)?.track;
4367
+ if (track === void 0) return;
4368
+ try {
4369
+ if (effect.kind === "none") {
4370
+ await track.stopProcessor();
4371
+ return;
4372
+ }
4373
+ const processors = await import("@livekit/track-processors");
4374
+ const processor = effect.kind === "blur" ? processors.BackgroundBlur(effect.radius ?? 12) : processors.VirtualBackground(effect.imageUrl);
4375
+ await track.setProcessor(processor);
4376
+ } catch (error) {
4377
+ throw new MeetError(
4378
+ "media-unsupported",
4379
+ "The background effect could not start on this camera",
4380
+ "Background effects need a browser with WebGL2 (current Chrome, Edge, Firefox or Safari). Your video continues without the effect.",
4381
+ error
4382
+ );
4383
+ }
4384
+ }
3892
4385
  var encoder = new TextEncoder();
3893
4386
  var decoder = new TextDecoder();
3894
4387
  function createLiveKitDriver(handlers, options = {}) {
3895
4388
  let room = null;
3896
4389
  let projectRoster = null;
4390
+ let audioCapture = DEFAULT_AUDIO_PROCESSING;
4391
+ let videoEffect = { kind: "none" };
3897
4392
  async function load() {
3898
4393
  if (room !== null) return room;
3899
4394
  let module2;
@@ -3922,7 +4417,9 @@ function createLiveKitDriver(handlers, options = {}) {
3922
4417
  handlers.participants([local, ...remotes]);
3923
4418
  };
3924
4419
  projectRoster = roster;
3925
- 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) => {
4420
+ 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) => {
4421
+ handlers.roomMetadata?.(metadata ?? "");
4422
+ }).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
3926
4423
  handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
3927
4424
  }).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
3928
4425
  handlers.quality(participant.identity, mapQuality(quality));
@@ -3999,6 +4496,7 @@ function createLiveKitDriver(handlers, options = {}) {
3999
4496
  throw normalizeDeviceError(error, "meet.connect");
4000
4497
  }
4001
4498
  projectRoster?.();
4499
+ handlers.roomMetadata?.(target.metadata ?? "");
4002
4500
  },
4003
4501
  async disconnect() {
4004
4502
  if (room === null) return;
@@ -4013,14 +4511,19 @@ function createLiveKitDriver(handlers, options = {}) {
4013
4511
  },
4014
4512
  async setMicrophoneEnabled(enabled) {
4015
4513
  try {
4016
- await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(enabled);
4514
+ await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(
4515
+ enabled,
4516
+ audioCapture
4517
+ );
4017
4518
  } catch (error) {
4018
4519
  throw normalizeDeviceError(error, "meet.setMicrophoneEnabled");
4019
4520
  }
4020
4521
  },
4021
4522
  async setCameraEnabled(enabled) {
4022
4523
  try {
4023
- await requireRoom("setCameraEnabled").localParticipant.setCameraEnabled(enabled);
4524
+ const target = requireRoom("setCameraEnabled");
4525
+ await target.localParticipant.setCameraEnabled(enabled);
4526
+ if (enabled && videoEffect.kind !== "none") await applyVideoEffect(target, videoEffect);
4024
4527
  } catch (error) {
4025
4528
  throw normalizeDeviceError(error, "meet.setCameraEnabled");
4026
4529
  }
@@ -4032,14 +4535,40 @@ function createLiveKitDriver(handlers, options = {}) {
4032
4535
  throw normalizeDeviceError(error, "meet.setScreenShareEnabled");
4033
4536
  }
4034
4537
  },
4035
- async publishData(topic, payload) {
4538
+ async publishData(topic, payload, to) {
4036
4539
  await requireRoom("publishData").localParticipant.publishData(
4037
4540
  // `reliable` is not optional for a control plane: a dropped
4038
4541
  // "hand lowered" leaves a hand up on every other screen forever.
4039
4542
  encoder.encode(payload),
4040
- { reliable: true, topic }
4543
+ to === void 0 || to.length === 0 ? { reliable: true, topic } : { reliable: true, topic, destinationIdentities: [...to] }
4041
4544
  );
4042
4545
  },
4546
+ async setAudioProcessing(processing) {
4547
+ audioCapture = { ...audioCapture, ...processing };
4548
+ const target = room;
4549
+ if (target === null) return;
4550
+ const { Track } = await import("livekit-client");
4551
+ const publication = target.localParticipant.getTrackPublication(Track.Source.Microphone);
4552
+ const track = publication?.track;
4553
+ if (track === void 0 || publication?.isMuted === true) return;
4554
+ try {
4555
+ await track.restartTrack({
4556
+ ...audioCapture,
4557
+ ...target.getActiveDevice("audioinput") !== void 0 ? { deviceId: target.getActiveDevice("audioinput") } : {}
4558
+ });
4559
+ } catch (error) {
4560
+ throw normalizeDeviceError(error, "meet.setAudioProcessing");
4561
+ }
4562
+ },
4563
+ async setVideoEffect(effect) {
4564
+ videoEffect = effect;
4565
+ const target = room;
4566
+ if (target === null) return;
4567
+ await applyVideoEffect(target, effect);
4568
+ },
4569
+ supportsVideoEffects() {
4570
+ return videoEffectsSupported();
4571
+ },
4043
4572
  attach(identity, kind, element) {
4044
4573
  const target = requireRoom("attach");
4045
4574
  const source = sources?.[kind];
@@ -4073,6 +4602,52 @@ function createLiveKitDriver(handlers, options = {}) {
4073
4602
  }
4074
4603
  };
4075
4604
  }
4605
+ async function createCameraPreview(deviceId2, effect) {
4606
+ let module2;
4607
+ try {
4608
+ module2 = await import("livekit-client");
4609
+ } catch (error) {
4610
+ throw new MeetError(
4611
+ "misconfigured",
4612
+ "livekit-client could not be loaded for the camera preview",
4613
+ "The bundler excluded `livekit-client`; remove it from any `external` list.",
4614
+ error
4615
+ );
4616
+ }
4617
+ let track;
4618
+ try {
4619
+ track = await module2.createLocalVideoTrack(deviceId2 === null ? {} : { deviceId: deviceId2 });
4620
+ } catch (error) {
4621
+ throw normalizeDeviceError(error, "meet.cameraPreview");
4622
+ }
4623
+ const target = track;
4624
+ async function apply(next) {
4625
+ if (next.kind === "none") {
4626
+ await target.stopProcessor();
4627
+ return;
4628
+ }
4629
+ const processors = await import("@livekit/track-processors");
4630
+ await target.setProcessor(
4631
+ next.kind === "blur" ? processors.BackgroundBlur(next.radius ?? 12) : processors.VirtualBackground(next.imageUrl)
4632
+ );
4633
+ }
4634
+ if (effect.kind !== "none" && videoEffectsSupported()) {
4635
+ try {
4636
+ await apply(effect);
4637
+ } catch {
4638
+ }
4639
+ }
4640
+ return {
4641
+ attach(element) {
4642
+ track.attach(element);
4643
+ },
4644
+ setEffect: apply,
4645
+ stop() {
4646
+ track.stop();
4647
+ track.detach();
4648
+ }
4649
+ };
4650
+ }
4076
4651
 
4077
4652
  // src/react/provider.tsx
4078
4653
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -4182,7 +4757,9 @@ function MeetRuntime(props) {
4182
4757
  avatarUrl: props.avatarUrl ?? null,
4183
4758
  deviceId: deviceId()
4184
4759
  };
4185
- const store = createMeetStore();
4760
+ const effectsStorage = createWebEffectsStorage();
4761
+ const store = createMeetStore({ effects: effectsStorage.load() });
4762
+ const devicesHolder = { current: null };
4186
4763
  const api = createMeetApi({
4187
4764
  baseUrl,
4188
4765
  organizationId: identity.organizationId,
@@ -4201,7 +4778,9 @@ function MeetRuntime(props) {
4201
4778
  identity,
4202
4779
  resolveMeeting: (slug) => repository.meetingBySlug(slug),
4203
4780
  driverFactory: (handlers) => (driverRef.current ?? createLiveKitDriver)(handlers),
4204
- onDiagnostic: report
4781
+ onDiagnostic: report,
4782
+ effectsStorage,
4783
+ resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null
4205
4784
  });
4206
4785
  const meetings = createMeetingSession({
4207
4786
  manager,
@@ -4216,16 +4795,32 @@ function MeetRuntime(props) {
4216
4795
  void engine.rejoin();
4217
4796
  }
4218
4797
  });
4219
- const ai = createMeetAi({
4220
- api,
4221
- store,
4222
- resolveRoomToken: () => {
4223
- const roomName = store.snapshot().meeting?.roomName;
4224
- if (roomName === void 0) return null;
4225
- const active = engine.token();
4226
- if (active?.roomName === roomName && active.token.length > 0) return active.token;
4227
- return tokens.remembered(roomName)?.token ?? null;
4798
+ const resolveRoomToken = () => {
4799
+ const roomName = store.snapshot().meeting?.roomName;
4800
+ if (roomName === void 0) return null;
4801
+ const active = engine.token();
4802
+ if (active?.roomName === roomName && active.token.length > 0) return active.token;
4803
+ return tokens.remembered(roomName)?.token ?? null;
4804
+ };
4805
+ const ai = createMeetAi({ api, store, resolveRoomToken });
4806
+ const inRoom = createInRoomClient({ api, store, resolveRoomToken });
4807
+ let chatReadFor = null;
4808
+ store.subscribe(() => {
4809
+ const snapshot = store.snapshot();
4810
+ if (snapshot.phase !== "connected" || snapshot.meeting === null) {
4811
+ if (snapshot.phase === "left" || snapshot.phase === "idle") chatReadFor = null;
4812
+ return;
4228
4813
  }
4814
+ if (chatReadFor === snapshot.meeting.id) return;
4815
+ chatReadFor = snapshot.meeting.id;
4816
+ void inRoom.loadChat().catch((error) => {
4817
+ const normalized = error;
4818
+ report({
4819
+ level: "warn",
4820
+ message: normalized.message ?? "meet: the saved chat could not be read",
4821
+ ...normalized.remedy !== void 0 ? { remedy: normalized.remedy } : {}
4822
+ });
4823
+ });
4229
4824
  });
4230
4825
  const recording = createRecordingController({
4231
4826
  api,
@@ -4247,6 +4842,7 @@ function MeetRuntime(props) {
4247
4842
  },
4248
4843
  storage: createWebDeviceStorage()
4249
4844
  });
4845
+ devicesHolder.current = devices;
4250
4846
  const calls = identity.userId === null ? null : createCallCenter({
4251
4847
  manager,
4252
4848
  repository,
@@ -4261,6 +4857,7 @@ function MeetRuntime(props) {
4261
4857
  engine,
4262
4858
  calls,
4263
4859
  meetings,
4860
+ inRoom,
4264
4861
  recording,
4265
4862
  devices,
4266
4863
  repository,
@@ -4382,7 +4979,8 @@ function computeStage(args) {
4382
4979
  const ordered = orderParticipants(args.participants, args.activeSpeakers);
4383
4980
  const sharer = ordered.find((participant) => participant.screenSharing) ?? null;
4384
4981
  const pinnedParticipant = args.pinned === null ? null : ordered.find((participant) => participant.identity === args.pinned) ?? null;
4385
- const feature = sharer !== null ? sharer : args.layout === "grid" ? null : pinnedParticipant ?? ordered[0] ?? null;
4982
+ const spotlit = args.spotlight == null ? null : ordered.find((participant) => participant.identity === args.spotlight) ?? null;
4983
+ const feature = sharer !== null ? sharer : pinnedParticipant !== null && args.layout !== "grid" ? pinnedParticipant : spotlit !== null ? spotlit : args.layout === "grid" ? null : ordered[0] ?? null;
4386
4984
  const rest = feature === null ? ordered : ordered.filter((participant) => participant.identity !== feature.identity);
4387
4985
  const pageCount = Math.max(1, Math.ceil(rest.length / pageSize));
4388
4986
  const page = Math.min(Math.max(0, args.page ?? 0), pageCount - 1);
@@ -4861,7 +5459,14 @@ function useRoom() {
4861
5459
  [engine]
4862
5460
  ),
4863
5461
  sendChat: (0, import_react3.useCallback)(
4864
- async (text3) => requireEngine(engine, "sendChat").sendChat(text3),
5462
+ async (text3) => {
5463
+ if (host === null) throw noRuntime("sendChat");
5464
+ await host.inRoom.sendChat(text3);
5465
+ },
5466
+ [host]
5467
+ ),
5468
+ sendPrivateChat: (0, import_react3.useCallback)(
5469
+ async (to, text3) => requireEngine(engine, "sendPrivateChat").sendPrivateChat(to, text3),
4865
5470
  [engine]
4866
5471
  ),
4867
5472
  pin: (0, import_react3.useCallback)(
@@ -4882,6 +5487,7 @@ function useStage(pageSize = DEFAULT_PAGE_SIZE) {
4882
5487
  participants: snapshot?.participants ?? [],
4883
5488
  layout: snapshot?.layout ?? "grid",
4884
5489
  pinned: snapshot?.pinned ?? null,
5490
+ spotlight: snapshot?.spotlight ?? null,
4885
5491
  activeSpeakers: snapshot?.activeSpeakers ?? [],
4886
5492
  pageSize,
4887
5493
  page
@@ -5214,33 +5820,160 @@ function useHostControls() {
5214
5820
  const host = useMeetHost();
5215
5821
  const snapshot = useMeetSnapshot();
5216
5822
  const role = useRole();
5217
- const engine = host?.engine ?? null;
5218
5823
  const me = snapshot?.participants.find(
5219
5824
  (participant) => participant.identity === snapshot.localIdentity
5220
5825
  );
5221
5826
  const actor = me === void 0 ? void 0 : { ...me, role };
5827
+ const [busy, setBusy] = (0, import_react3.useState)(false);
5828
+ const inRoom = host?.inRoom ?? null;
5829
+ const store = host?.store ?? null;
5830
+ const notice = snapshot?.actionNotice ?? null;
5831
+ const setNotice = (0, import_react3.useCallback)(
5832
+ (next) => store?.patch({ actionNotice: next === null ? null : { ...next, at: Date.now() } }),
5833
+ [store]
5834
+ );
5835
+ const run = (0, import_react3.useCallback)(
5836
+ async (request) => {
5837
+ if (inRoom === null) throw noRuntime(`moderate(${request.action})`);
5838
+ setBusy(true);
5839
+ try {
5840
+ const result = await inRoom.moderate(request);
5841
+ setNotice({ text: result.message, failed: false });
5842
+ return result;
5843
+ } catch (thrown) {
5844
+ const error = thrown;
5845
+ setNotice({
5846
+ text: [error.message, error.remedy].filter(Boolean).join(" "),
5847
+ failed: true
5848
+ });
5849
+ return null;
5850
+ } finally {
5851
+ setBusy(false);
5852
+ }
5853
+ },
5854
+ [inRoom, setNotice]
5855
+ );
5222
5856
  return {
5223
5857
  isHost: isHostRole(role),
5858
+ isOwner: role === "host",
5859
+ notice,
5860
+ busy,
5861
+ clearNotice: (0, import_react3.useCallback)(() => setNotice(null), [setNotice]),
5224
5862
  canModerate: (0, import_react3.useCallback)(
5225
5863
  (target) => canModerateParticipant(actor, target),
5226
5864
  [actor]
5227
5865
  ),
5866
+ run,
5228
5867
  mute: (0, import_react3.useCallback)(
5229
- async (target) => requireEngine(engine, "muteParticipant").publishHostAction({
5230
- kind: "mute",
5231
- identity: target.identity
5232
- }),
5233
- [engine]
5868
+ async (target) => {
5869
+ await run({ action: "mute", identity: target.identity });
5870
+ },
5871
+ [run]
5234
5872
  ),
5235
5873
  remove: (0, import_react3.useCallback)(
5236
- async (target) => requireEngine(engine, "removeParticipant").publishHostAction({
5237
- kind: "remove",
5238
- identity: target.identity
5239
- }),
5874
+ async (target) => {
5875
+ await run({ action: "remove", identity: target.identity });
5876
+ },
5877
+ [run]
5878
+ )
5879
+ };
5880
+ }
5881
+ function useHandQueue() {
5882
+ const snapshot = useMeetSnapshot();
5883
+ return (0, import_react3.useMemo)(
5884
+ () => (snapshot?.participants ?? []).filter((participant) => participant.handRaised).sort((a, b) => (a.handRaisedAt ?? 0) - (b.handRaisedAt ?? 0)),
5885
+ [snapshot?.participants]
5886
+ );
5887
+ }
5888
+ function useMediaEffects() {
5889
+ const host = useMeetHost();
5890
+ const snapshot = useMeetSnapshot();
5891
+ const engine = host?.engine ?? null;
5892
+ return {
5893
+ effects: snapshot?.effects ?? DEFAULT_EFFECTS,
5894
+ backgroundsSupported: engine?.supportsVideoEffects() ?? false,
5895
+ set: (0, import_react3.useCallback)(
5896
+ async (next) => requireEngine(engine, "setEffects").setEffects(next),
5240
5897
  [engine]
5241
5898
  )
5242
5899
  };
5243
5900
  }
5901
+ function useModerationNotice() {
5902
+ const host = useMeetHost();
5903
+ const snapshot = useMeetSnapshot();
5904
+ return {
5905
+ notice: snapshot?.moderationNotice ?? null,
5906
+ dismiss: (0, import_react3.useCallback)(() => host?.store.patch({ moderationNotice: null }), [host])
5907
+ };
5908
+ }
5909
+ function useAudioLevel(stream) {
5910
+ const [level, setLevel] = (0, import_react3.useState)(0);
5911
+ (0, import_react3.useEffect)(() => {
5912
+ if (stream === null || stream.getAudioTracks().length === 0) {
5913
+ setLevel(0);
5914
+ return void 0;
5915
+ }
5916
+ const Context = globalThis.AudioContext ?? globalThis.webkitAudioContext;
5917
+ if (Context === void 0) return void 0;
5918
+ const context = new Context();
5919
+ const source = context.createMediaStreamSource(stream);
5920
+ const analyser = context.createAnalyser();
5921
+ analyser.fftSize = 512;
5922
+ source.connect(analyser);
5923
+ const samples = new Uint8Array(analyser.fftSize);
5924
+ let frame = 0;
5925
+ const tick = () => {
5926
+ analyser.getByteTimeDomainData(samples);
5927
+ let peak = 0;
5928
+ for (const sample of samples) peak = Math.max(peak, Math.abs(sample - 128) / 128);
5929
+ setLevel((previous) => Math.abs(previous - peak) > 0.02 ? peak : previous);
5930
+ frame = requestAnimationFrame(tick);
5931
+ };
5932
+ frame = requestAnimationFrame(tick);
5933
+ return () => {
5934
+ cancelAnimationFrame(frame);
5935
+ source.disconnect();
5936
+ void context.close();
5937
+ };
5938
+ }, [stream]);
5939
+ return level;
5940
+ }
5941
+ var MEET_SHORTCUTS = [
5942
+ { id: "mic", keys: "\u2318/Ctrl + D", label: "Mute or unmute your microphone" },
5943
+ { id: "camera", keys: "\u2318/Ctrl + E", label: "Turn your camera on or off" },
5944
+ { id: "hand", keys: "\u2318/Ctrl + Alt + H", label: "Raise or lower your hand" },
5945
+ { id: "chat", keys: "\u2318/Ctrl + Alt + C", label: "Open or close chat" },
5946
+ { id: "people", keys: "\u2318/Ctrl + Alt + P", label: "Open or close people" },
5947
+ { id: "help", keys: "Shift + ?", label: "Show keyboard shortcuts" }
5948
+ ];
5949
+ function useMeetShortcuts(handlers) {
5950
+ const ref = (0, import_react3.useRef)(handlers);
5951
+ ref.current = handlers;
5952
+ (0, import_react3.useEffect)(() => {
5953
+ const target = globalThis.window;
5954
+ if (target === void 0) return void 0;
5955
+ const onKey = (event) => {
5956
+ const element = event.target;
5957
+ const typing = element !== null && (element.isContentEditable || element.tagName === "INPUT" || element.tagName === "TEXTAREA" || element.tagName === "SELECT");
5958
+ const mod = event.metaKey || event.ctrlKey;
5959
+ const key = event.key.toLowerCase();
5960
+ let id = null;
5961
+ if (mod && !event.altKey && !event.shiftKey && key === "d") id = "mic";
5962
+ else if (mod && !event.altKey && !event.shiftKey && key === "e") id = "camera";
5963
+ else if (mod && event.altKey && (event.code === "KeyH" || key === "h")) id = "hand";
5964
+ else if (mod && event.altKey && (event.code === "KeyC" || key === "c")) id = "chat";
5965
+ else if (mod && event.altKey && (event.code === "KeyP" || key === "p")) id = "people";
5966
+ else if (!mod && !typing && event.key === "?") id = "help";
5967
+ if (id === null) return;
5968
+ const handler = ref.current[id];
5969
+ if (handler === void 0) return;
5970
+ event.preventDefault();
5971
+ handler();
5972
+ };
5973
+ target.addEventListener("keydown", onKey);
5974
+ return () => target.removeEventListener("keydown", onKey);
5975
+ }, []);
5976
+ }
5244
5977
  function useMeeting() {
5245
5978
  return useMeetSnapshot()?.meeting ?? null;
5246
5979
  }
@@ -5587,13 +6320,67 @@ function WifiLowIcon(props) {
5587
6320
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8.5 16.5a5 5 0 0 1 7 0" })
5588
6321
  ] });
5589
6322
  }
5590
-
5591
- // src/react/components.tsx
5592
- var import_jsx_runtime3 = require("react/jsx-runtime");
5593
- function InertNotice({ what }) {
5594
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__empty", role: "status", children: [
5595
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "mx-meet__empty-title", children: [
5596
- what,
6323
+ function SmileIcon(props) {
6324
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6325
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
6326
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }),
6327
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "9", x2: "9.01", y1: "9", y2: "9" }),
6328
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "15", x2: "15.01", y1: "9", y2: "9" })
6329
+ ] });
6330
+ }
6331
+ function MoreIcon(props) {
6332
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6333
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "1" }),
6334
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "19", cy: "12", r: "1" }),
6335
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "5", cy: "12", r: "1" })
6336
+ ] });
6337
+ }
6338
+ function SearchIcon(props) {
6339
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6340
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "11", cy: "11", r: "8" }),
6341
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m21 21-4.3-4.3" })
6342
+ ] });
6343
+ }
6344
+ function KeyboardIcon(props) {
6345
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6346
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10 8h.01M12 12h.01M14 8h.01M16 12h.01M18 8h.01M6 8h.01M7 16h10M8 12h.01" }),
6347
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { width: "20", height: "16", x: "2", y: "4", rx: "2" })
6348
+ ] });
6349
+ }
6350
+ function SpotlightIcon(props) {
6351
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("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" }) });
6352
+ }
6353
+ function SignalIcon({ bars, ...props }) {
6354
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6355
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M4 20v-2", opacity: bars >= 1 ? 1 : 0.3 }),
6356
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10 20v-6", opacity: bars >= 2 ? 1 : 0.3 }),
6357
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M16 20V9", opacity: bars >= 3 ? 1 : 0.3 }),
6358
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M22 20V4", opacity: bars >= 3 ? 1 : 0.3 })
6359
+ ] });
6360
+ }
6361
+ function SpeakerIcon(props) {
6362
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6363
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M11 5 6 9H2v6h4l5 4V5Z" }),
6364
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M15.54 8.46a5 5 0 0 1 0 7.07" }),
6365
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19.07 4.93a10 10 0 0 1 0 14.14" })
6366
+ ] });
6367
+ }
6368
+ function FlagIcon(props) {
6369
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6370
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("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" }),
6371
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "4", x2: "4", y1: "22", y2: "15" })
6372
+ ] });
6373
+ }
6374
+ function PencilIcon(props) {
6375
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" }) });
6376
+ }
6377
+
6378
+ // src/react/components.tsx
6379
+ var import_jsx_runtime3 = require("react/jsx-runtime");
6380
+ function InertNotice({ what }) {
6381
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__empty", role: "status", children: [
6382
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "mx-meet__empty-title", children: [
6383
+ what,
5597
6384
  " is not ready yet"
5598
6385
  ] }),
5599
6386
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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." })
@@ -5687,12 +6474,17 @@ function ParticipantTile({
5687
6474
  featured = false
5688
6475
  }) {
5689
6476
  const host = useMeetHost();
5690
- const canModerate2 = useCanModerate(participant);
6477
+ const snapshot = useMeetSnapshot();
6478
+ const queue = useHandQueue();
6479
+ const [menu, setMenu] = (0, import_react4.useState)(false);
5691
6480
  const videoRef = useTrack(
5692
6481
  participant.identity,
5693
6482
  participant.screenSharing ? "screen" : "camera"
5694
6483
  );
5695
6484
  const showVideo = participant.cameraEnabled || participant.screenSharing;
6485
+ const pinnedHere = snapshot?.pinned === participant.identity;
6486
+ const spotlit = snapshot?.spotlight === participant.identity;
6487
+ const handPosition = participant.handRaised ? queue.findIndex((entry) => entry.identity === participant.identity) + 1 : 0;
5696
6488
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
5697
6489
  "div",
5698
6490
  {
@@ -5713,44 +6505,59 @@ function ParticipantTile({
5713
6505
  muted: participant.isLocal
5714
6506
  }
5715
6507
  ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: featured ? 96 : 56 }) }),
6508
+ participant.handRaised ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__tile-hand", title: `Hand raised \xB7 ${ordinal(handPosition)} in line`, children: [
6509
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, { width: 14, height: 14 }),
6510
+ handPosition > 0 ? handPosition : null
6511
+ ] }) : null,
5716
6512
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__tile-bar", children: [
5717
6513
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__tile-name", children: [
6514
+ spotlit ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { title: "Spotlighted for everyone", className: "mx-meet__tile-flag", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpotlightIcon, { width: 12, height: 12 }) }) : null,
5718
6515
  participant.displayName,
5719
6516
  participant.isLocal ? " (you)" : "",
5720
6517
  participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__badge", children: "AI" }) : null,
5721
6518
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RoleBadge, { role: participant.role })
5722
6519
  ] }),
5723
6520
  !participant.micEnabled ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 14, height: 14 }) : null,
5724
- participant.handRaised ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, { width: 14, height: 14 }) : null,
5725
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ConnectionDot, { quality: participant.quality })
6521
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(QualityBars, { quality: participant.quality })
5726
6522
  ] }),
5727
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__tile-actions", children: [
6523
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: menu ? "mx-meet__tile-actions mx-meet__tile-actions--open" : "mx-meet__tile-actions", children: [
5728
6524
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5729
6525
  ControlButton,
5730
6526
  {
5731
- label: featured ? "Unpin" : `Pin ${participant.displayName}`,
5732
- onClick: () => host?.engine.pin(featured ? null : participant.identity),
6527
+ label: pinnedHere ? "Unpin" : `Pin ${participant.displayName} for me`,
6528
+ active: pinnedHere,
6529
+ onClick: () => host?.engine.pin(pinnedHere ? null : participant.identity),
5733
6530
  children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PinIcon, { width: 16, height: 16 })
5734
6531
  }
5735
6532
  ),
5736
- canModerate2 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5737
- ControlButton,
5738
- {
5739
- label: `Mute ${participant.displayName}`,
5740
- onClick: () => {
5741
- void host?.engine.publishHostAction({
5742
- kind: "mute",
5743
- identity: participant.identity
5744
- });
5745
- },
5746
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 16, height: 16 })
5747
- }
5748
- ) : null
6533
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__anchor", children: [
6534
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6535
+ ControlButton,
6536
+ {
6537
+ label: `More for ${participant.displayName}`,
6538
+ active: menu,
6539
+ onClick: () => setMenu(!menu),
6540
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MoreIcon, { width: 16, height: 16 })
6541
+ }
6542
+ ),
6543
+ menu ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PersonMenu, { participant, onClose: () => setMenu(false), placement: "down" }) : null
6544
+ ] })
5749
6545
  ] })
5750
6546
  ]
5751
6547
  }
5752
6548
  );
5753
6549
  }
6550
+ function ordinal(position) {
6551
+ if (position <= 0) return "";
6552
+ const suffix = position % 100 >= 11 && position % 100 <= 13 ? "th" : { 1: "st", 2: "nd", 3: "rd" }[position % 10] ?? "th";
6553
+ return `${position}${suffix}`;
6554
+ }
6555
+ function QualityBars({ quality }) {
6556
+ if (quality === "unknown") return null;
6557
+ const bars = quality === "excellent" ? 3 : quality === "good" ? 2 : quality === "poor" ? 1 : 0;
6558
+ const label = quality === "lost" ? "Connection lost" : quality === "poor" ? "Poor connection" : `Connection ${quality}`;
6559
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: `mx-meet__signal mx-meet__signal--${quality}`, title: label, "aria-label": label, role: "img", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SignalIcon, { bars, width: 14, height: 14 }) });
6560
+ }
5754
6561
  function Stage({ pageSize }) {
5755
6562
  const stage = useStage(pageSize);
5756
6563
  const columns = gridColumns(stage.tiles.length);
@@ -5779,22 +6586,52 @@ function Stage({ pageSize }) {
5779
6586
  ] }) : null
5780
6587
  ] });
5781
6588
  }
6589
+ var MEET_REACTIONS = ["\u{1F496}", "\u{1F44D}", "\u{1F389}", "\u{1F44F}", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F914}", "\u{1F44E}"];
5782
6590
  function ControlBar({ onLeave }) {
5783
6591
  const room = useRoom();
5784
6592
  const recording = useRecording();
5785
6593
  const [panel, setPanel] = (0, import_react4.useState)("none");
6594
+ const [chatTo, setChatTo] = (0, import_react4.useState)(null);
6595
+ const [popover, setPopover] = (0, import_react4.useState)("none");
6596
+ const [shortcuts, setShortcuts] = (0, import_react4.useState)(false);
5786
6597
  const snapshot = useMeetSnapshot();
5787
6598
  const lobby = useLobby();
6599
+ const controls = useHostControls();
6600
+ const queue = useHandQueue();
5788
6601
  const micUnavailable = useLocalMedia().microphone.unavailable;
6602
+ const unread = useUnreadChat(panel === "chat");
6603
+ const toggle = (next) => setPanel(panel === next ? "none" : next);
6604
+ useMeetShortcuts({
6605
+ mic: () => void room.toggleMicrophone(),
6606
+ camera: () => void room.toggleCamera(),
6607
+ hand: () => void room.toggleHand(),
6608
+ chat: () => toggle("chat"),
6609
+ people: () => toggle("people"),
6610
+ help: () => setShortcuts(true)
6611
+ });
6612
+ const hasSpotlight = snapshot?.spotlight !== null && snapshot?.spotlight !== void 0;
5789
6613
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
5790
- panel === "chat" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatPanel, { onClose: () => setPanel("none") }) : null,
5791
- panel === "people" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PeoplePanel, { onClose: () => setPanel("none") }) : null,
6614
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MeetingOverlays, {}),
6615
+ shortcuts ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ShortcutsSheet, { onClose: () => setShortcuts(false) }) : null,
6616
+ panel === "chat" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatPanel, { onClose: () => setPanel("none"), to: chatTo, onTo: setChatTo }) : null,
6617
+ panel === "people" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6618
+ PeoplePanel,
6619
+ {
6620
+ onClose: () => setPanel("none"),
6621
+ onMessage: (identity) => {
6622
+ setChatTo(identity);
6623
+ setPanel("chat");
6624
+ }
6625
+ }
6626
+ ) : null,
5792
6627
  panel === "ai" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AiPanel, { onClose: () => setPanel("none") }) : null,
6628
+ panel === "settings" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SettingsPanel, { onClose: () => setPanel("none") }) : null,
6629
+ controls.notice !== null && panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
5793
6630
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__controls", children: [
5794
6631
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5795
6632
  ControlButton,
5796
6633
  {
5797
- label: micUnavailable !== null ? `Unmute \u2014 ${micUnavailable.remedy}` : room.local?.micEnabled === true ? "Mute" : "Unmute",
6634
+ label: micUnavailable !== null ? `Unmute \u2014 ${micUnavailable.remedy}` : room.local?.micEnabled === true ? "Mute (\u2318D)" : "Unmute (\u2318D)",
5798
6635
  active: room.local?.micEnabled === true,
5799
6636
  onClick: () => void room.toggleMicrophone(),
5800
6637
  children: room.local?.micEnabled === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, {})
@@ -5803,7 +6640,7 @@ function ControlBar({ onLeave }) {
5803
6640
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5804
6641
  ControlButton,
5805
6642
  {
5806
- label: room.local?.cameraEnabled === true ? "Stop video" : "Start video",
6643
+ label: room.local?.cameraEnabled === true ? "Stop video (\u2318E)" : "Start video (\u2318E)",
5807
6644
  active: room.local?.cameraEnabled === true,
5808
6645
  onClick: () => void room.toggleCamera(),
5809
6646
  children: room.local?.cameraEnabled === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CameraIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CameraOffIcon, {})
@@ -5818,31 +6655,152 @@ function ControlBar({ onLeave }) {
5818
6655
  children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ScreenShareIcon, {})
5819
6656
  }
5820
6657
  ),
6658
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__anchor", children: [
6659
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6660
+ ControlButton,
6661
+ {
6662
+ label: "Send a reaction",
6663
+ active: popover === "reactions",
6664
+ onClick: () => setPopover(popover === "reactions" ? "none" : "reactions"),
6665
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SmileIcon, {})
6666
+ }
6667
+ ),
6668
+ popover === "reactions" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: "Reactions", onClose: () => setPopover("none"), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__emoji-bar", children: MEET_REACTIONS.map((emoji) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6669
+ "button",
6670
+ {
6671
+ type: "button",
6672
+ className: "mx-meet__emoji",
6673
+ "aria-label": `React ${emoji}`,
6674
+ onClick: () => void room.react(emoji),
6675
+ children: emoji
6676
+ },
6677
+ emoji
6678
+ )) }) }) : null
6679
+ ] }),
5821
6680
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5822
6681
  ControlButton,
5823
6682
  {
5824
- label: room.local?.handRaised === true ? "Lower hand" : "Raise hand",
6683
+ label: room.local?.handRaised === true ? "Lower hand (\u2318\u2325H)" : "Raise hand (\u2318\u2325H)",
5825
6684
  active: room.local?.handRaised === true,
5826
6685
  onClick: () => void room.toggleHand(),
5827
6686
  children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, {})
5828
6687
  }
5829
6688
  ),
5830
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ControlButton, { label: "Chat", active: panel === "chat", onClick: () => setPanel(panel === "chat" ? "none" : "chat"), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatIcon, {}) }),
6689
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(ControlButton, { label: "Chat (\u2318\u2325C)", active: panel === "chat", onClick: () => toggle("chat"), children: [
6690
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatIcon, {}),
6691
+ unread > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__dot", "aria-hidden": "true" }) : null
6692
+ ] }),
5831
6693
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
5832
6694
  ControlButton,
5833
6695
  {
5834
6696
  label: lobby.waiting.length > 0 ? `People \u2014 ${lobby.waiting.length} waiting` : `People \u2014 ${(snapshot?.participants ?? []).length} in the meeting`,
5835
6697
  active: panel === "people",
5836
- onClick: () => setPanel(panel === "people" ? "none" : "people"),
6698
+ onClick: () => toggle("people"),
5837
6699
  children: [
5838
6700
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PeopleIcon, {}),
5839
- lobby.waiting.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__dot", "aria-hidden": "true" }) : null
6701
+ lobby.waiting.length > 0 || controls.isHost && queue.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__dot", "aria-hidden": "true" }) : null
5840
6702
  ]
5841
6703
  }
5842
6704
  ),
5843
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AiControl, { active: panel === "ai", onToggle: () => setPanel(panel === "ai" ? "none" : "ai") }),
6705
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AiControl, { active: panel === "ai", onToggle: () => toggle("ai") }),
5844
6706
  recording.canStart ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ControlButton, { label: "Start recording", onClick: () => void recording.start(), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RecordIcon, {}) }) : null,
5845
6707
  recording.canStop ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ControlButton, { label: "Stop recording", active: true, onClick: () => void recording.stop(), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RecordIcon, {}) }) : null,
6708
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__anchor", children: [
6709
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6710
+ ControlButton,
6711
+ {
6712
+ label: "More options",
6713
+ active: popover === "more" || panel === "settings",
6714
+ onClick: () => setPopover(popover === "more" ? "none" : "more"),
6715
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MoreIcon, {})
6716
+ }
6717
+ ),
6718
+ popover === "more" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: "More options", onClose: () => setPopover("none"), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu", role: "menu", children: [
6719
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
6720
+ MenuItem,
6721
+ {
6722
+ onSelect: () => {
6723
+ setPopover("none");
6724
+ setPanel("settings");
6725
+ },
6726
+ children: [
6727
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SettingsIcon, { width: 16, height: 16 }),
6728
+ " Audio, video and effects"
6729
+ ]
6730
+ }
6731
+ ),
6732
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
6733
+ MenuItem,
6734
+ {
6735
+ onSelect: () => {
6736
+ setPopover("none");
6737
+ setShortcuts(true);
6738
+ },
6739
+ children: [
6740
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(KeyboardIcon, { width: 16, height: 16 }),
6741
+ " Keyboard shortcuts"
6742
+ ]
6743
+ }
6744
+ ),
6745
+ controls.isHost ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
6746
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__menu-sep", role: "separator" }),
6747
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
6748
+ MenuItem,
6749
+ {
6750
+ onSelect: () => {
6751
+ setPopover("none");
6752
+ void controls.run({ action: "mute_all" });
6753
+ },
6754
+ children: [
6755
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 16, height: 16 }),
6756
+ " Mute everyone"
6757
+ ]
6758
+ }
6759
+ ),
6760
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
6761
+ MenuItem,
6762
+ {
6763
+ onSelect: () => {
6764
+ setPopover("none");
6765
+ void controls.run({ action: "ask_all_unmute" });
6766
+ },
6767
+ children: [
6768
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicIcon, { width: 16, height: 16 }),
6769
+ " Ask everyone to unmute"
6770
+ ]
6771
+ }
6772
+ ),
6773
+ queue.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
6774
+ MenuItem,
6775
+ {
6776
+ onSelect: () => {
6777
+ setPopover("none");
6778
+ void controls.run({ action: "lower_all_hands" });
6779
+ },
6780
+ children: [
6781
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, { width: 16, height: 16 }),
6782
+ " Lower all hands (",
6783
+ queue.length,
6784
+ ")"
6785
+ ]
6786
+ }
6787
+ ) : null,
6788
+ hasSpotlight ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
6789
+ MenuItem,
6790
+ {
6791
+ onSelect: () => {
6792
+ setPopover("none");
6793
+ void controls.run({ action: "spotlight", identity: null });
6794
+ },
6795
+ children: [
6796
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpotlightIcon, { width: 16, height: 16 }),
6797
+ " Remove spotlight"
6798
+ ]
6799
+ }
6800
+ ) : null
6801
+ ] }) : null
6802
+ ] }) }) : null
6803
+ ] }),
5846
6804
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5847
6805
  ControlButton,
5848
6806
  {
@@ -5858,6 +6816,150 @@ function ControlBar({ onLeave }) {
5858
6816
  ] })
5859
6817
  ] });
5860
6818
  }
6819
+ function useUnreadChat(open) {
6820
+ const snapshot = useMeetSnapshot();
6821
+ const chat = snapshot?.chat ?? [];
6822
+ const local = snapshot?.localIdentity ?? null;
6823
+ const [seen, setSeen] = (0, import_react4.useState)(0);
6824
+ (0, import_react4.useEffect)(() => {
6825
+ if (open) setSeen(chat.length);
6826
+ }, [open, chat.length]);
6827
+ const loaded = snapshot?.chatLoaded === true;
6828
+ const [baseline, setBaseline] = (0, import_react4.useState)(null);
6829
+ (0, import_react4.useEffect)(() => {
6830
+ if (loaded && baseline === null) setBaseline(chat.length);
6831
+ }, [loaded, baseline, chat.length]);
6832
+ if (open) return 0;
6833
+ const from = Math.max(seen, baseline ?? chat.length);
6834
+ return chat.slice(from).filter((message) => message.identity !== local).length;
6835
+ }
6836
+ function Popover({
6837
+ label,
6838
+ onClose,
6839
+ children,
6840
+ placement = "up"
6841
+ }) {
6842
+ const ref = (0, import_react4.useRef)(null);
6843
+ const [style, setStyle] = (0, import_react4.useState)({ visibility: "hidden" });
6844
+ (0, import_react4.useLayoutEffect)(() => {
6845
+ const element = ref.current;
6846
+ const anchor = element?.parentElement ?? null;
6847
+ if (element === null || anchor === null) return;
6848
+ const place = () => {
6849
+ const rect = anchor.getBoundingClientRect();
6850
+ const width = element.offsetWidth;
6851
+ const height = element.offsetHeight;
6852
+ const gutter = 8;
6853
+ const left = Math.min(
6854
+ Math.max(gutter, rect.left + rect.width / 2 - width / 2),
6855
+ window.innerWidth - width - gutter
6856
+ );
6857
+ const fitsBelow = rect.bottom + gutter + height <= window.innerHeight;
6858
+ const fitsAbove = rect.top - gutter - height >= 0;
6859
+ const down = placement === "down" ? fitsBelow || !fitsAbove : !fitsAbove && fitsBelow;
6860
+ setStyle(
6861
+ down ? { position: "fixed", left, top: rect.bottom + gutter } : { position: "fixed", left, bottom: window.innerHeight - rect.top + gutter }
6862
+ );
6863
+ };
6864
+ place();
6865
+ window.addEventListener("resize", place);
6866
+ return () => window.removeEventListener("resize", place);
6867
+ }, [placement]);
6868
+ (0, import_react4.useEffect)(() => {
6869
+ const onKey = (event) => {
6870
+ if (event.key === "Escape") onClose();
6871
+ };
6872
+ const onPointer = (event) => {
6873
+ const anchor = ref.current?.parentElement ?? null;
6874
+ if (anchor !== null && !anchor.contains(event.target)) onClose();
6875
+ };
6876
+ window.addEventListener("keydown", onKey);
6877
+ window.addEventListener("pointerdown", onPointer);
6878
+ return () => {
6879
+ window.removeEventListener("keydown", onKey);
6880
+ window.removeEventListener("pointerdown", onPointer);
6881
+ };
6882
+ }, [onClose]);
6883
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref, className: "mx-meet__popover", style, role: "dialog", "aria-label": label, children });
6884
+ }
6885
+ function MenuItem({
6886
+ onSelect,
6887
+ danger = false,
6888
+ children
6889
+ }) {
6890
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6891
+ "button",
6892
+ {
6893
+ type: "button",
6894
+ role: "menuitem",
6895
+ className: danger ? "mx-meet__menu-item mx-meet__menu-item--danger" : "mx-meet__menu-item",
6896
+ onClick: onSelect,
6897
+ children
6898
+ }
6899
+ );
6900
+ }
6901
+ function Toast({ text: text3, failed, onDone }) {
6902
+ (0, import_react4.useEffect)(() => {
6903
+ const timer = setTimeout(onDone, failed ? 7e3 : 4e3);
6904
+ return () => clearTimeout(timer);
6905
+ }, [text3, failed, onDone]);
6906
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: failed ? "mx-meet__toast mx-meet__toast--failed" : "mx-meet__toast", role: "status", children: [
6907
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: text3 }),
6908
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: onDone, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 14, height: 14 }) })
6909
+ ] });
6910
+ }
6911
+ function MeetingOverlays() {
6912
+ const reactions = useReactions();
6913
+ const { notice, dismiss } = useModerationNotice();
6914
+ const room = useRoom();
6915
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
6916
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ReactionOverlay, { reactions }),
6917
+ notice !== null && notice.kind !== "removed" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__notice-bar", role: "alert", children: [
6918
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: notice.text }),
6919
+ notice.kind === "ask-unmute" && room.local?.micEnabled !== true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6920
+ "button",
6921
+ {
6922
+ type: "button",
6923
+ className: "mx-meet__primary mx-meet__primary--small",
6924
+ onClick: () => {
6925
+ dismiss();
6926
+ void room.toggleMicrophone();
6927
+ },
6928
+ children: "Unmute"
6929
+ }
6930
+ ) : null,
6931
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismiss, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 14, height: 14 }) })
6932
+ ] }) : null
6933
+ ] });
6934
+ }
6935
+ function ShortcutsSheet({ onClose }) {
6936
+ (0, import_react4.useEffect)(() => {
6937
+ const onKey = (event) => {
6938
+ if (event.key === "Escape") onClose();
6939
+ };
6940
+ window.addEventListener("keydown", onKey);
6941
+ return () => window.removeEventListener("keydown", onKey);
6942
+ }, [onClose]);
6943
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__sheet-backdrop", onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
6944
+ "div",
6945
+ {
6946
+ className: "mx-meet__sheet",
6947
+ role: "dialog",
6948
+ "aria-label": "Keyboard shortcuts",
6949
+ onClick: (event) => event.stopPropagation(),
6950
+ children: [
6951
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("header", { className: "mx-meet__panel-head", children: [
6952
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { className: "mx-meet__panel-title", children: "Keyboard shortcuts" }),
6953
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ControlButton, { label: "Close", onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 16, height: 16 }) })
6954
+ ] }),
6955
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dl", { className: "mx-meet__shortcuts", children: MEET_SHORTCUTS.map((shortcut) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__shortcut", children: [
6956
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dt", { children: shortcut.label }),
6957
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dd", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("kbd", { className: "mx-meet__kbd", children: shortcut.keys }) })
6958
+ ] }, shortcut.id)) })
6959
+ ]
6960
+ }
6961
+ ) });
6962
+ }
5861
6963
  function AiControl({ active, onToggle }) {
5862
6964
  const snapshot = useMeetSnapshot();
5863
6965
  const meeting = snapshot?.meeting ?? null;
@@ -5877,117 +6979,640 @@ function PanelShell({
5877
6979
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__panel-body", children })
5878
6980
  ] });
5879
6981
  }
5880
- function ChatPanel({ onClose }) {
6982
+ function ChatPanel({
6983
+ onClose,
6984
+ to = null,
6985
+ onTo
6986
+ }) {
5881
6987
  const snapshot = useMeetSnapshot();
5882
6988
  const room = useRoom();
5883
6989
  const [draft, setDraft] = (0, import_react4.useState)("");
6990
+ const [recipient, setRecipient] = (0, import_react4.useState)(to);
6991
+ const listRef = (0, import_react4.useRef)(null);
5884
6992
  const messages = snapshot?.chat ?? [];
5885
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PanelShell, { title: "In-meeting chat", onClose, children: [
5886
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__panel-note", children: "In-meeting chat is ephemeral \u2014 it disappears when the meeting ends." }),
5887
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { className: "mx-meet__chat", children: messages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: "mx-meet__chat-line", children: [
5888
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: message.displayName }),
5889
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: message.text })
5890
- ] }, message.id)) }),
6993
+ const local = snapshot?.localIdentity ?? null;
6994
+ const people = (snapshot?.participants ?? []).filter(
6995
+ (participant) => !participant.isLocal && !participant.isAgent
6996
+ );
6997
+ const recipientName = recipient === null ? null : people.find((p) => p.identity === recipient)?.displayName ?? null;
6998
+ (0, import_react4.useEffect)(() => setRecipient(to), [to]);
6999
+ (0, import_react4.useEffect)(() => {
7000
+ const list = listRef.current;
7001
+ if (list !== null) list.scrollTop = list.scrollHeight;
7002
+ }, [messages.length]);
7003
+ const choose = (next) => {
7004
+ setRecipient(next);
7005
+ onTo?.(next);
7006
+ };
7007
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PanelShell, { title: "Chat", onClose, children: [
7008
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("ul", { className: "mx-meet__chat", ref: listRef, children: [
7009
+ snapshot?.chatLoaded !== true && messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("li", { className: "mx-meet__panel-note", children: "Reading earlier messages\u2026" }) : messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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,
7010
+ messages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatLine, { message, local, people }, message.id))
7011
+ ] }),
5891
7012
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
5892
7013
  "form",
5893
7014
  {
5894
- className: "mx-meet__composer",
7015
+ className: "mx-meet__composer mx-meet__composer--chat",
5895
7016
  onSubmit: (event) => {
5896
7017
  event.preventDefault();
5897
7018
  const text3 = draft;
7019
+ if (text3.trim().length === 0) return;
5898
7020
  setDraft("");
5899
- void room.sendChat(text3);
7021
+ if (recipient === null) void room.sendChat(text3).catch(() => void 0);
7022
+ else void room.sendPrivateChat(recipient, text3);
5900
7023
  },
5901
7024
  children: [
5902
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5903
- "input",
7025
+ people.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7026
+ "select",
5904
7027
  {
5905
- className: "mx-meet__input",
5906
- value: draft,
5907
- onChange: (event) => setDraft(event.target.value),
5908
- placeholder: "Message everyone",
5909
- "aria-label": "Message everyone"
7028
+ className: "mx-meet__select mx-meet__select--compact",
7029
+ "aria-label": "Send to",
7030
+ value: recipient ?? "",
7031
+ onChange: (event) => choose(event.target.value === "" ? null : event.target.value),
7032
+ children: [
7033
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "", children: "Everyone" }),
7034
+ people.map((participant) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("option", { value: participant.identity, children: [
7035
+ participant.displayName,
7036
+ " (private)"
7037
+ ] }, participant.identity))
7038
+ ]
5910
7039
  }
5911
- ),
5912
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: "mx-meet__send", children: "Send" })
7040
+ ) : null,
7041
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__composer-row", children: [
7042
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7043
+ "input",
7044
+ {
7045
+ className: "mx-meet__input",
7046
+ value: draft,
7047
+ onChange: (event) => setDraft(event.target.value),
7048
+ placeholder: recipientName === null ? "Message everyone" : `Message ${recipientName} privately`,
7049
+ "aria-label": recipientName === null ? "Message everyone" : `Message ${recipientName} privately`,
7050
+ maxLength: 4e3
7051
+ }
7052
+ ),
7053
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: "mx-meet__send", disabled: draft.trim().length === 0, children: "Send" })
7054
+ ] }),
7055
+ recipientName !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "mx-meet__panel-note", children: [
7056
+ "Only ",
7057
+ recipientName,
7058
+ " sees this. Private messages are not saved."
7059
+ ] }) : null
5913
7060
  ]
5914
7061
  }
5915
7062
  )
5916
7063
  ] });
5917
7064
  }
5918
- function PeoplePanel({ onClose }) {
7065
+ function ChatLine({
7066
+ message,
7067
+ local,
7068
+ people
7069
+ }) {
7070
+ const mine = message.identity === local;
7071
+ const privateTo = message.to ?? null;
7072
+ const counterpart = privateTo === null ? null : mine ? people.find((p) => p.identity === privateTo)?.displayName ?? "them" : "you";
7073
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7074
+ "li",
7075
+ {
7076
+ className: [
7077
+ "mx-meet__chat-line",
7078
+ privateTo !== null ? "mx-meet__chat-line--private" : "",
7079
+ message.failed !== void 0 ? "mx-meet__chat-line--failed" : ""
7080
+ ].filter(Boolean).join(" "),
7081
+ children: [
7082
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__chat-meta", children: [
7083
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: mine ? "You" : message.displayName }),
7084
+ counterpart !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__chat-private", children: [
7085
+ "to ",
7086
+ counterpart,
7087
+ " \xB7 private"
7088
+ ] }) : null,
7089
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("time", { dateTime: message.at, children: clockOf(message.at) })
7090
+ ] }),
7091
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__chat-text", children: message.text }),
7092
+ message.failed !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__chat-failed", children: message.failed }) : privateTo === null && message.saved === false ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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
7093
+ ]
7094
+ }
7095
+ );
7096
+ }
7097
+ function PeoplePanel({
7098
+ onClose,
7099
+ onMessage
7100
+ }) {
5919
7101
  const snapshot = useMeetSnapshot();
5920
7102
  const controls = useHostControls();
7103
+ const queue = useHandQueue();
7104
+ const [query, setQuery] = (0, import_react4.useState)("");
7105
+ const [menuFor, setMenuFor] = (0, import_react4.useState)(null);
5921
7106
  const myRole = useRole();
5922
7107
  const local = snapshot?.localIdentity ?? null;
5923
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PanelShell, { title: "People", onClose, children: [
7108
+ const everyone = snapshot?.participants ?? [];
7109
+ const needle = query.trim().toLowerCase();
7110
+ const shown = needle.length === 0 ? everyone : everyone.filter((participant) => participant.displayName.toLowerCase().includes(needle));
7111
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PanelShell, { title: `People (${everyone.length})`, onClose, children: [
5924
7112
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LobbyPanel, {}),
5925
- snapshot?.meeting !== null && snapshot?.meeting !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GuestRecordAccessControl, { meeting: snapshot.meeting }) : null,
5926
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { className: "mx-meet__people", children: (snapshot?.participants ?? []).map((participant) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: "mx-meet__person", children: [
5927
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: 32 }),
5928
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__person-name", children: [
5929
- participant.displayName,
5930
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5931
- RoleBadge,
7113
+ queue.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "mx-meet__section", "aria-label": "Raised hands", children: [
7114
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("header", { className: "mx-meet__section-head", children: [
7115
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("h3", { className: "mx-meet__section-title", children: [
7116
+ "Raised hands \xB7 ",
7117
+ queue.length
7118
+ ] }),
7119
+ controls.isHost ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7120
+ "button",
5932
7121
  {
5933
- role: participant.identity === local ? myRole : participant.role
7122
+ type: "button",
7123
+ className: "mx-meet__link",
7124
+ onClick: () => void controls.run({ action: "lower_all_hands" }),
7125
+ children: "Lower all"
5934
7126
  }
5935
- ),
5936
- isNoteTaker(participant) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null
7127
+ ) : null
5937
7128
  ] }),
5938
- !participant.micEnabled && !isNoteTaker(participant) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 14, height: 14 }) : null,
5939
- participant.handRaised ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, { width: 14, height: 14 }) : null,
5940
- controls.canModerate(participant) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PersonHostActions, { participant, controls }) : null
5941
- ] }, participant.identity)) })
7129
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ol", { className: "mx-meet__queue", children: queue.map((participant) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: "mx-meet__person", children: [
7130
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: 28 }),
7131
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__person-name", children: [
7132
+ participant.displayName,
7133
+ participant.identity === local ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__you", children: " (you)" }) : null
7134
+ ] }),
7135
+ controls.isHost || participant.identity === local ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7136
+ "button",
7137
+ {
7138
+ type: "button",
7139
+ className: "mx-meet__link",
7140
+ onClick: () => void controls.run({ action: "lower_hand", identity: participant.identity }),
7141
+ children: "Lower"
7142
+ }
7143
+ ) : null
7144
+ ] }, participant.identity)) })
7145
+ ] }) : null,
7146
+ controls.isHost && everyone.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__bulk", children: [
7147
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { type: "button", className: "mx-meet__chip", onClick: () => void controls.run({ action: "mute_all" }), children: [
7148
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 14, height: 14 }),
7149
+ " Mute all"
7150
+ ] }),
7151
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7152
+ "button",
7153
+ {
7154
+ type: "button",
7155
+ className: "mx-meet__chip",
7156
+ onClick: () => void controls.run({ action: "ask_all_unmute" }),
7157
+ children: [
7158
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicIcon, { width: 14, height: 14 }),
7159
+ " Ask all to unmute"
7160
+ ]
7161
+ }
7162
+ )
7163
+ ] }) : null,
7164
+ everyone.length > 4 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "mx-meet__search", children: [
7165
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SearchIcon, { width: 16, height: 16 }),
7166
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7167
+ "input",
7168
+ {
7169
+ className: "mx-meet__input mx-meet__input--bare",
7170
+ value: query,
7171
+ onChange: (event) => setQuery(event.target.value),
7172
+ placeholder: "Search people",
7173
+ "aria-label": "Search people"
7174
+ }
7175
+ )
7176
+ ] }) : null,
7177
+ controls.notice !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: controls.notice.failed ? "mx-meet__error" : "mx-meet__notice", role: "status", children: controls.notice.text }) : null,
7178
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("ul", { className: "mx-meet__people", children: [
7179
+ shown.map((participant) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: "mx-meet__person", children: [
7180
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: 32 }),
7181
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__person-name", children: [
7182
+ participant.displayName,
7183
+ participant.identity === local ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__you", children: " (you)" }) : null,
7184
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RoleBadge, { role: participant.identity === local ? myRole : participant.role }),
7185
+ isNoteTaker(participant) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null,
7186
+ snapshot?.spotlight === participant.identity ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__badge", children: "Spotlight" }) : null
7187
+ ] }),
7188
+ participant.handRaised ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, { width: 14, height: 14 }) : null,
7189
+ !participant.micEnabled && !isNoteTaker(participant) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 14, height: 14 }) : null,
7190
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(QualityBars, { quality: participant.quality }),
7191
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__anchor", children: [
7192
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7193
+ ControlButton,
7194
+ {
7195
+ label: `More for ${participant.displayName}`,
7196
+ active: menuFor === participant.identity,
7197
+ onClick: () => setMenuFor(menuFor === participant.identity ? null : participant.identity),
7198
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MoreIcon, { width: 16, height: 16 })
7199
+ }
7200
+ ),
7201
+ menuFor === participant.identity ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7202
+ PersonMenu,
7203
+ {
7204
+ participant,
7205
+ placement: "down",
7206
+ onClose: () => setMenuFor(null),
7207
+ ...onMessage !== void 0 ? { onMessage } : {}
7208
+ }
7209
+ ) : null
7210
+ ] })
7211
+ ] }, participant.identity)),
7212
+ shown.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: "mx-meet__panel-note", children: [
7213
+ "Nobody matches \u201C",
7214
+ query,
7215
+ "\u201D."
7216
+ ] }) : null
7217
+ ] }),
7218
+ snapshot?.meeting !== null && snapshot?.meeting !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GuestRecordAccessControl, { meeting: snapshot.meeting }) : null
5942
7219
  ] });
5943
7220
  }
5944
- function PersonHostActions({
7221
+ function PersonMenu({
5945
7222
  participant,
5946
- controls
7223
+ onClose,
7224
+ onMessage,
7225
+ placement = "down"
5947
7226
  }) {
5948
- const [confirming, setConfirming] = (0, import_react4.useState)(false);
5949
- if (confirming) {
5950
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__confirm", role: "group", "aria-label": `Remove ${participant.displayName}?`, children: [
5951
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__confirm-text", children: [
5952
- "Remove ",
5953
- participant.displayName,
5954
- "? They leave the meeting immediately and have to be let back in."
5955
- ] }),
5956
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5957
- "button",
7227
+ const host = useMeetHost();
7228
+ const snapshot = useMeetSnapshot();
7229
+ const controls = useHostControls();
7230
+ const [mode, setMode] = (0, import_react4.useState)("menu");
7231
+ const [text3, setText] = (0, import_react4.useState)("");
7232
+ const self = participant.identity === snapshot?.localIdentity;
7233
+ const moderate = controls.canModerate(participant);
7234
+ const pinned = snapshot?.pinned === participant.identity;
7235
+ const spotlit = snapshot?.spotlight === participant.identity;
7236
+ const hasAccount = participant.userId !== null;
7237
+ const act = (request) => {
7238
+ void controls.run(request).then((result) => {
7239
+ if (result !== null) onClose();
7240
+ });
7241
+ };
7242
+ if (mode === "rename" || mode === "report") {
7243
+ const renaming = mode === "rename";
7244
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: renaming ? "Rename" : "Report", onClose, placement, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7245
+ "form",
7246
+ {
7247
+ className: "mx-meet__menu-form",
7248
+ onSubmit: (event) => {
7249
+ event.preventDefault();
7250
+ if (text3.trim().length === 0) return;
7251
+ act(
7252
+ renaming ? { action: "rename", identity: participant.identity, name: text3.trim() } : { action: "report", identity: participant.identity, reason: text3.trim() }
7253
+ );
7254
+ },
7255
+ children: [
7256
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "mx-meet__field", children: [
7257
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__field-label", children: renaming ? "Name shown in this meeting" : `What did ${participant.displayName} do?` }),
7258
+ renaming ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7259
+ "input",
7260
+ {
7261
+ className: "mx-meet__input",
7262
+ autoFocus: true,
7263
+ maxLength: 80,
7264
+ value: text3,
7265
+ onChange: (event) => setText(event.target.value)
7266
+ }
7267
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7268
+ "textarea",
7269
+ {
7270
+ className: "mx-meet__input",
7271
+ autoFocus: true,
7272
+ rows: 3,
7273
+ maxLength: 1e3,
7274
+ value: text3,
7275
+ onChange: (event) => setText(event.target.value)
7276
+ }
7277
+ )
7278
+ ] }),
7279
+ controls.notice?.failed === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__error", children: controls.notice.text }) : null,
7280
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu-actions", children: [
7281
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Back" }),
7282
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: "mx-meet__send", disabled: text3.trim().length === 0 || controls.busy, children: renaming ? "Rename" : "Report" })
7283
+ ] })
7284
+ ]
7285
+ }
7286
+ ) });
7287
+ }
7288
+ if (mode === "remove" || mode === "transfer") {
7289
+ const removing = mode === "remove";
7290
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: removing ? "Remove" : "Make host", onClose, placement, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu-form", children: [
7291
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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.` }),
7292
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu-actions", children: [
7293
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Cancel" }),
7294
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7295
+ "button",
7296
+ {
7297
+ type: "button",
7298
+ className: removing ? "mx-meet__send mx-meet__send--danger" : "mx-meet__send",
7299
+ disabled: controls.busy,
7300
+ onClick: () => act({ action: removing ? "remove" : "transfer_host", identity: participant.identity }),
7301
+ children: removing ? "Remove" : "Make host"
7302
+ }
7303
+ )
7304
+ ] })
7305
+ ] }) });
7306
+ }
7307
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: `Options for ${participant.displayName}`, onClose, placement, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu", role: "menu", children: [
7308
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7309
+ MenuItem,
7310
+ {
7311
+ onSelect: () => {
7312
+ host?.engine.pin(pinned ? null : participant.identity);
7313
+ onClose();
7314
+ },
7315
+ children: [
7316
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PinIcon, { width: 16, height: 16 }),
7317
+ " ",
7318
+ pinned ? "Unpin" : "Pin for me"
7319
+ ]
7320
+ }
7321
+ ),
7322
+ !self && !participant.isAgent && onMessage !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7323
+ MenuItem,
7324
+ {
7325
+ onSelect: () => {
7326
+ onMessage(participant.identity);
7327
+ onClose();
7328
+ },
7329
+ children: [
7330
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatIcon, { width: 16, height: 16 }),
7331
+ " Message privately"
7332
+ ]
7333
+ }
7334
+ ) : null,
7335
+ controls.isHost ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "spotlight", identity: spotlit ? null : participant.identity }), children: [
7336
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpotlightIcon, { width: 16, height: 16 }),
7337
+ " ",
7338
+ spotlit ? "Remove spotlight" : "Spotlight for everyone"
7339
+ ] }) : null,
7340
+ moderate ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
7341
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__menu-sep", role: "separator" }),
7342
+ participant.micEnabled ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "mute", identity: participant.identity }), children: [
7343
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 16, height: 16 }),
7344
+ " Mute"
7345
+ ] }) : !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "ask_unmute", identity: participant.identity }), children: [
7346
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicIcon, { width: 16, height: 16 }),
7347
+ " Ask to unmute"
7348
+ ] }) : null,
7349
+ participant.cameraEnabled ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "stop_video", identity: participant.identity }), children: [
7350
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CameraOffIcon, { width: 16, height: 16 }),
7351
+ " Stop video"
7352
+ ] }) : null,
7353
+ participant.screenSharing ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "stop_screen", identity: participant.identity }), children: [
7354
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ScreenShareIcon, { width: 16, height: 16 }),
7355
+ " Stop screen share"
7356
+ ] }) : null,
7357
+ participant.handRaised ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "lower_hand", identity: participant.identity }), children: [
7358
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, { width: 16, height: 16 }),
7359
+ " Lower hand"
7360
+ ] }) : null,
7361
+ hasAccount && !participant.isAgent && participant.role !== "cohost" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "make_cohost", identity: participant.identity }), children: [
7362
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PeopleIcon, { width: 16, height: 16 }),
7363
+ " Make co-host"
7364
+ ] }) : null,
7365
+ !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7366
+ MenuItem,
5958
7367
  {
5959
- type: "button",
5960
- className: "mx-meet__link mx-meet__link--danger",
5961
- onClick: () => {
5962
- setConfirming(false);
5963
- void controls.remove(participant);
7368
+ onSelect: () => {
7369
+ setText(participant.displayName);
7370
+ setMode("rename");
5964
7371
  },
5965
- children: "Remove"
7372
+ children: [
7373
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PencilIcon, { width: 16, height: 16 }),
7374
+ " Rename"
7375
+ ]
5966
7376
  }
5967
- ),
5968
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setConfirming(false), children: "Keep them" })
5969
- ] });
5970
- }
5971
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
5972
- participant.micEnabled ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5973
- ControlButton,
7377
+ ) : null
7378
+ ] }) : null,
7379
+ controls.isOwner && participant.role === "cohost" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "revoke_cohost", identity: participant.identity }), children: [
7380
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PeopleIcon, { width: 16, height: 16 }),
7381
+ " Remove co-host"
7382
+ ] }) : null,
7383
+ controls.isOwner && !self && hasAccount && !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => setMode("transfer"), children: [
7384
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpotlightIcon, { width: 16, height: 16 }),
7385
+ " Make host"
7386
+ ] }) : null,
7387
+ self ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7388
+ MenuItem,
5974
7389
  {
5975
- label: `Mute ${participant.displayName}`,
5976
- onClick: () => void controls.mute(participant),
5977
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 14, height: 14 })
7390
+ onSelect: () => {
7391
+ setText(participant.displayName);
7392
+ setMode("rename");
7393
+ },
7394
+ children: [
7395
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PencilIcon, { width: 16, height: 16 }),
7396
+ " Rename yourself"
7397
+ ]
5978
7398
  }
5979
7399
  ) : null,
7400
+ !self && !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7401
+ MenuItem,
7402
+ {
7403
+ onSelect: () => {
7404
+ setText("");
7405
+ setMode("report");
7406
+ },
7407
+ children: [
7408
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FlagIcon, { width: 16, height: 16 }),
7409
+ " Report"
7410
+ ]
7411
+ }
7412
+ ) : null,
7413
+ moderate && !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { danger: true, onSelect: () => setMode("remove"), children: [
7414
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 16, height: 16 }),
7415
+ " Remove from meeting"
7416
+ ] }) : null
7417
+ ] }) });
7418
+ }
7419
+ function SettingsPanel({ onClose }) {
7420
+ const host = useRequiredMeetHost();
7421
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PanelShell, { title: "Audio, video and effects", onClose, children: [
5980
7422
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5981
- ControlButton,
7423
+ DeviceSettings,
5982
7424
  {
5983
- label: `Remove ${participant.displayName}`,
5984
- danger: true,
5985
- onClick: () => setConfirming(true),
5986
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 14, height: 14 })
7425
+ onPick: (kind, deviceId2) => {
7426
+ host.devices.select(kind, deviceId2);
7427
+ void host.engine.switchDevice(kind, deviceId2);
7428
+ }
5987
7429
  }
5988
- )
7430
+ ),
7431
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EffectsSettings, {})
7432
+ ] });
7433
+ }
7434
+ function DeviceSettings({
7435
+ onPick
7436
+ }) {
7437
+ const host = useRequiredMeetHost();
7438
+ const devices = useDeviceSnapshot();
7439
+ const microphone = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
7440
+ const speaker = devices?.selected.audiooutput ?? host.devices.resolve("audiooutput");
7441
+ const stream = useMicrophoneStream(microphone);
7442
+ const level = useAudioLevel(stream);
7443
+ const [testing, setTesting] = (0, import_react4.useState)(false);
7444
+ (0, import_react4.useEffect)(() => {
7445
+ void host.devices.refresh();
7446
+ }, [host]);
7447
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "mx-meet__section", "aria-label": "Devices", children: [
7448
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "audioinput", label: "Microphone", onPick }),
7449
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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__ */ (0, import_jsx_runtime3.jsx)("span", { style: { width: `${Math.min(100, Math.round(level * 160))}%` } }) }),
7450
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "audiooutput", label: "Speaker", onPick }),
7451
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7452
+ "button",
7453
+ {
7454
+ type: "button",
7455
+ className: "mx-meet__chip",
7456
+ disabled: testing,
7457
+ onClick: () => {
7458
+ setTesting(true);
7459
+ void playTestSound(speaker).finally(() => setTesting(false));
7460
+ },
7461
+ children: [
7462
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpeakerIcon, { width: 14, height: 14 }),
7463
+ " ",
7464
+ testing ? "Playing\u2026" : "Test speaker"
7465
+ ]
7466
+ }
7467
+ ),
7468
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "videoinput", label: "Camera", onPick })
7469
+ ] });
7470
+ }
7471
+ function EffectsSettings({ preview }) {
7472
+ const effects = useMediaEffects();
7473
+ const [error, setError] = (0, import_react4.useState)(null);
7474
+ const fileRef = (0, import_react4.useRef)(null);
7475
+ const supported = effects.backgroundsSupported || preview != null;
7476
+ const apply = (next) => {
7477
+ setError(null);
7478
+ void effects.set(next);
7479
+ if (preview != null) {
7480
+ const merged = { ...effects.effects, ...next };
7481
+ void preview.setEffect(toVideoEffect(merged)).catch(
7482
+ () => setError("This browser could not run the background effect.")
7483
+ );
7484
+ }
7485
+ };
7486
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "mx-meet__section", "aria-label": "Effects", children: [
7487
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "mx-meet__toggle", children: [
7488
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7489
+ "input",
7490
+ {
7491
+ type: "checkbox",
7492
+ checked: effects.effects.noiseSuppression,
7493
+ onChange: (event) => apply({ noiseSuppression: event.target.checked })
7494
+ }
7495
+ ),
7496
+ "Noise suppression",
7497
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__hint", children: "Filters keyboards, fans and background chatter." })
7498
+ ] }),
7499
+ supported ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__field", children: [
7500
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__field-label", children: "Background" }),
7501
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__pills", role: "radiogroup", "aria-label": "Background", children: ["none", "blur", "image"].map((kind) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7502
+ "button",
7503
+ {
7504
+ type: "button",
7505
+ role: "radio",
7506
+ "aria-checked": effects.effects.background === kind,
7507
+ className: effects.effects.background === kind ? "mx-meet__pill mx-meet__pill--on" : "mx-meet__pill",
7508
+ onClick: () => {
7509
+ if (kind === "image") {
7510
+ fileRef.current?.click();
7511
+ return;
7512
+ }
7513
+ apply({ background: kind });
7514
+ },
7515
+ children: kind === "none" ? "None" : kind === "blur" ? "Blur" : "Picture\u2026"
7516
+ },
7517
+ kind
7518
+ )) }),
7519
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7520
+ "input",
7521
+ {
7522
+ ref: fileRef,
7523
+ type: "file",
7524
+ accept: "image/*",
7525
+ hidden: true,
7526
+ onChange: (event) => {
7527
+ const file = event.target.files?.[0];
7528
+ event.target.value = "";
7529
+ if (file === void 0) return;
7530
+ void shrinkImage(file).then((url) => apply({ background: "image", backgroundImage: url })).catch(() => setError("That picture could not be read. Try a JPEG or PNG."));
7531
+ }
7532
+ }
7533
+ ),
7534
+ error !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__error", children: error }) : null
7535
+ ] }) : null
5989
7536
  ] });
5990
7537
  }
7538
+ function toVideoEffect(effects) {
7539
+ if (effects.background === "blur") return { kind: "blur" };
7540
+ if (effects.background === "image" && effects.backgroundImage !== null) {
7541
+ return { kind: "image", imageUrl: effects.backgroundImage };
7542
+ }
7543
+ return { kind: "none" };
7544
+ }
7545
+ async function shrinkImage(file) {
7546
+ const url = URL.createObjectURL(file);
7547
+ try {
7548
+ const image = new Image();
7549
+ image.src = url;
7550
+ await image.decode();
7551
+ const scale = Math.min(1, 1280 / image.naturalWidth);
7552
+ const canvas = document.createElement("canvas");
7553
+ canvas.width = Math.round(image.naturalWidth * scale);
7554
+ canvas.height = Math.round(image.naturalHeight * scale);
7555
+ const context = canvas.getContext("2d");
7556
+ if (context === null) throw new Error("no 2d context");
7557
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
7558
+ return canvas.toDataURL("image/jpeg", 0.85);
7559
+ } finally {
7560
+ URL.revokeObjectURL(url);
7561
+ }
7562
+ }
7563
+ async function playTestSound(sinkId) {
7564
+ const Context = globalThis.AudioContext;
7565
+ if (Context === void 0) return;
7566
+ const context = new Context();
7567
+ const destination = context.createMediaStreamDestination();
7568
+ const gain = context.createGain();
7569
+ gain.gain.value = 0.2;
7570
+ gain.connect(destination);
7571
+ for (const [frequency, start] of [
7572
+ [660, 0],
7573
+ [880, 0.25]
7574
+ ]) {
7575
+ const oscillator = context.createOscillator();
7576
+ oscillator.frequency.value = frequency;
7577
+ oscillator.connect(gain);
7578
+ oscillator.start(context.currentTime + start);
7579
+ oscillator.stop(context.currentTime + start + 0.22);
7580
+ }
7581
+ const audio = new Audio();
7582
+ audio.srcObject = destination.stream;
7583
+ const sinkable = audio;
7584
+ if (sinkId !== null && sinkId !== "default" && sinkable.setSinkId !== void 0) {
7585
+ await sinkable.setSinkId(sinkId).catch(() => void 0);
7586
+ }
7587
+ await audio.play().catch(() => void 0);
7588
+ await new Promise((resolve) => setTimeout(resolve, 700));
7589
+ audio.pause();
7590
+ await context.close();
7591
+ }
7592
+ function useMicrophoneStream(deviceId2) {
7593
+ const [stream, setStream] = (0, import_react4.useState)(null);
7594
+ (0, import_react4.useEffect)(() => {
7595
+ if (deviceId2 === "off") return void 0;
7596
+ let live = true;
7597
+ let opened = null;
7598
+ const media = globalThis.navigator?.mediaDevices;
7599
+ if (media === void 0) return void 0;
7600
+ void media.getUserMedia({ audio: deviceId2 === null ? true : { deviceId: { exact: deviceId2 } } }).then((next) => {
7601
+ if (!live) {
7602
+ next.getTracks().forEach((track) => track.stop());
7603
+ return;
7604
+ }
7605
+ opened = next;
7606
+ setStream(next);
7607
+ }).catch(() => setStream(null));
7608
+ return () => {
7609
+ live = false;
7610
+ opened?.getTracks().forEach((track) => track.stop());
7611
+ setStream(null);
7612
+ };
7613
+ }, [deviceId2]);
7614
+ return stream;
7615
+ }
5991
7616
  function LobbyPanel() {
5992
7617
  const lobby = useLobby();
5993
7618
  if (!lobby.canAdmit || lobby.waiting.length === 0) return null;
@@ -6213,16 +7838,40 @@ function PreJoinBody({
6213
7838
  const room = useRoom();
6214
7839
  const devices = useDeviceSnapshot();
6215
7840
  const recording = useRecording();
7841
+ const effects = useMediaEffects();
7842
+ const { notice } = useModerationNotice();
6216
7843
  const [mode, setMode] = (0, import_react4.useState)("video");
6217
7844
  const [microphone, setMicrophone] = (0, import_react4.useState)(true);
6218
7845
  const [joining, setJoining] = (0, import_react4.useState)(false);
7846
+ const camera = devices?.selected.videoinput ?? host.devices.resolve("videoinput");
7847
+ const mic = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
7848
+ const preview = useCameraPreview(mode === "video" ? camera : void 0, effects.effects);
7849
+ const micStream = useMicrophoneStream(microphone ? mic : "off");
7850
+ const level = useAudioLevel(micStream);
7851
+ const [speakerTesting, setSpeakerTesting] = (0, import_react4.useState)(false);
6219
7852
  (0, import_react4.useEffect)(() => {
6220
7853
  void host.devices.refresh();
6221
7854
  }, [host]);
6222
7855
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__prejoin", children: [
6223
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__preview", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 96 }) }) }),
7856
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__preview", children: [
7857
+ mode === "video" && preview.preview !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7858
+ "video",
7859
+ {
7860
+ className: "mx-meet__video mx-meet__video--mirror",
7861
+ autoPlay: true,
7862
+ playsInline: true,
7863
+ muted: true,
7864
+ ref: (element) => {
7865
+ if (element !== null) preview.preview?.attach(element);
7866
+ }
7867
+ }
7868
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 96 }) }),
7869
+ mode === "video" && preview.error !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__preview-note", children: preview.error }) : null,
7870
+ microphone ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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__ */ (0, import_jsx_runtime3.jsx)("span", { style: { width: `${Math.min(100, Math.round(level * 160))}%` } }) }) : null
7871
+ ] }),
6224
7872
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__prejoin-panel", children: [
6225
7873
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { className: "mx-meet__prejoin-title", children: "Ready to join?" }),
7874
+ notice?.kind === "removed" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__warning", children: notice.text }) : null,
6226
7875
  devices?.labelsHidden === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "mx-meet__panel-note", children: [
6227
7876
  "Your browser is hiding device names until you allow camera and microphone access.",
6228
7877
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -6239,7 +7888,30 @@ function PreJoinBody({
6239
7888
  ] }) : null,
6240
7889
  devices?.fallbackNotice != null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__warning", children: devices.fallbackNotice }) : null,
6241
7890
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "audioinput", label: "Microphone" }),
7891
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__field-row", children: [
7892
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "audiooutput", label: "Speaker" }),
7893
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7894
+ "button",
7895
+ {
7896
+ type: "button",
7897
+ className: "mx-meet__chip",
7898
+ disabled: speakerTesting,
7899
+ onClick: () => {
7900
+ setSpeakerTesting(true);
7901
+ void playTestSound(devices?.selected.audiooutput ?? host.devices.resolve("audiooutput")).finally(
7902
+ () => setSpeakerTesting(false)
7903
+ );
7904
+ },
7905
+ children: [
7906
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpeakerIcon, { width: 14, height: 14 }),
7907
+ " ",
7908
+ speakerTesting ? "Playing\u2026" : "Test"
7909
+ ]
7910
+ }
7911
+ )
7912
+ ] }),
6242
7913
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "videoinput", label: "Camera" }),
7914
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EffectsSettings, { preview: preview.preview }),
6243
7915
  recording.consentNotice !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__consent", children: recording.consentNotice }) : null,
6244
7916
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__prejoin-actions", children: [
6245
7917
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "mx-meet__toggle", children: [
@@ -6271,6 +7943,7 @@ function PreJoinBody({
6271
7943
  className: "mx-meet__primary",
6272
7944
  onClick: () => {
6273
7945
  setJoining(true);
7946
+ preview.release();
6274
7947
  void room.join({
6275
7948
  roomName,
6276
7949
  mode,
@@ -6290,9 +7963,49 @@ function PreJoinBody({
6290
7963
  ] })
6291
7964
  ] });
6292
7965
  }
7966
+ function useCameraPreview(deviceId2, effects) {
7967
+ const [preview, setPreview] = (0, import_react4.useState)(null);
7968
+ const [error, setError] = (0, import_react4.useState)(null);
7969
+ const current = (0, import_react4.useRef)(null);
7970
+ const initialEffect = (0, import_react4.useRef)(toVideoEffect(effects));
7971
+ initialEffect.current = toVideoEffect(effects);
7972
+ (0, import_react4.useEffect)(() => {
7973
+ if (deviceId2 === void 0) return void 0;
7974
+ let live = true;
7975
+ setError(null);
7976
+ void createCameraPreview(deviceId2, initialEffect.current).then((created) => {
7977
+ if (!live) {
7978
+ created.stop();
7979
+ return;
7980
+ }
7981
+ current.current = created;
7982
+ setPreview(created);
7983
+ }).catch((thrown) => {
7984
+ if (!live) return;
7985
+ const failure = thrown;
7986
+ setError([failure.message, failure.remedy].filter(Boolean).join(". ") || "The camera did not start.");
7987
+ });
7988
+ return () => {
7989
+ live = false;
7990
+ current.current?.stop();
7991
+ current.current = null;
7992
+ setPreview(null);
7993
+ };
7994
+ }, [deviceId2]);
7995
+ return {
7996
+ preview,
7997
+ error,
7998
+ release: () => {
7999
+ current.current?.stop();
8000
+ current.current = null;
8001
+ setPreview(null);
8002
+ }
8003
+ };
8004
+ }
6293
8005
  function DevicePicker({
6294
8006
  kind,
6295
- label
8007
+ label,
8008
+ onPick
6296
8009
  }) {
6297
8010
  const host = useRequiredMeetHost();
6298
8011
  const devices = useDeviceSnapshot();
@@ -6305,7 +8018,10 @@ function DevicePicker({
6305
8018
  {
6306
8019
  className: "mx-meet__select",
6307
8020
  value: devices?.selected[kind] ?? host.devices.resolve(kind) ?? "",
6308
- onChange: (event) => host.devices.select(kind, event.target.value),
8021
+ onChange: (event) => {
8022
+ if (onPick !== void 0) onPick(kind, event.target.value);
8023
+ else host.devices.select(kind, event.target.value);
8024
+ },
6309
8025
  children: list.map((device) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: device.deviceId, children: device.label.length > 0 ? device.label : `${label} (name hidden)` }, device.deviceId))
6310
8026
  }
6311
8027
  )