@ai-matrx/meet 0.7.3 → 0.7.6

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