@ai-matrx/meet 0.7.2 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.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,14 +120,22 @@ __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,
116
135
  TRANSCRIPTION_TOPIC: () => TRANSCRIPTION_TOPIC,
117
136
  WifiLowIcon: () => WifiLowIcon,
118
137
  absenceSentence: () => absenceSentence,
138
+ announceMeeting: () => announceMeeting,
119
139
  applyRoomEvent: () => applyRoomEvent,
120
140
  asCallId: () => asCallId,
121
141
  asMeetingId: () => asMeetingId,
@@ -130,11 +150,13 @@ __export(react_exports, {
130
150
  canModerateParticipant: () => canModerateParticipant,
131
151
  canTransition: () => canTransition,
132
152
  captionSegment: () => captionSegment,
153
+ changesPatch: () => changesPatch,
133
154
  computeStage: () => computeStage,
134
155
  countLabel: () => countLabel,
135
156
  createCallCenter: () => createCallCenter,
136
157
  createCallInviteHandler: () => createCallInviteHandler,
137
158
  createDeviceManager: () => createDeviceManager,
159
+ createInRoomClient: () => createInRoomClient,
138
160
  createLiveKitDriver: () => createLiveKitDriver,
139
161
  createMeetAi: () => createMeetAi,
140
162
  createMeetApi: () => createMeetApi,
@@ -147,6 +169,7 @@ __export(react_exports, {
147
169
  createRoomEngine: () => createRoomEngine,
148
170
  createTokenClient: () => createTokenClient,
149
171
  createWebDeviceStorage: () => createWebDeviceStorage,
172
+ createWebEffectsStorage: () => createWebEffectsStorage,
150
173
  createWebRoomTokenStorage: () => createWebRoomTokenStorage,
151
174
  displayNameFor: () => displayNameFor,
152
175
  fileDownloadPath: () => fileDownloadPath,
@@ -174,6 +197,7 @@ __export(react_exports, {
174
197
  mapQuality: () => mapQuality,
175
198
  meetingInviteAction: () => meetingInviteAction,
176
199
  meetingLink: () => meetingLink,
200
+ meetingPath: () => meetingPath,
177
201
  meetingSlug: () => meetingSlug,
178
202
  meetingTopic: () => meetingTopic,
179
203
  meetingWhen: () => meetingWhen,
@@ -190,9 +214,15 @@ __export(react_exports, {
190
214
  parseRoomToken: () => parseRoomToken,
191
215
  participantSummary: () => participantSummary,
192
216
  playableRecordingFileId: () => playableRecordingFileId,
217
+ projectInvitee: () => projectInvitee,
193
218
  projectNote: () => projectNote,
219
+ projectOccurrence: () => projectOccurrence,
220
+ projectOverride: () => projectOverride,
194
221
  projectParticipant: () => projectParticipant,
222
+ projectRsvpInvitation: () => projectRsvpInvitation,
195
223
  projectSegment: () => projectSegment,
224
+ projectUpcoming: () => projectUpcoming,
225
+ pushMeetingToGoogleCalendar: () => pushMeetingToGoogleCalendar,
196
226
  readServerRefusal: () => readServerRefusal,
197
227
  readableTranscript: () => readableTranscript,
198
228
  recordingLabel: () => recordingLabel,
@@ -200,8 +230,10 @@ __export(react_exports, {
200
230
  roleOf: () => roleOf,
201
231
  roomTopic: () => roomTopic,
202
232
  setGuestRecordAccess: () => setGuestRecordAccess,
233
+ settingsPatch: () => settingsPatch,
203
234
  snapshotParticipant: () => snapshotParticipant,
204
235
  unavailableFrom: () => unavailableFrom,
236
+ useAudioLevel: () => useAudioLevel,
205
237
  useCalls: () => useCalls,
206
238
  useCanModerate: () => useCanModerate,
207
239
  useCanRecord: () => useCanRecord,
@@ -210,19 +242,23 @@ __export(react_exports, {
210
242
  useElapsed: () => useElapsed,
211
243
  useEndMeeting: () => useEndMeeting,
212
244
  useGuestRecordAccess: () => useGuestRecordAccess,
245
+ useHandQueue: () => useHandQueue,
213
246
  useHostControls: () => useHostControls,
214
247
  useIsHost: () => useIsHost,
215
248
  useListenOnlyNotice: () => useListenOnlyNotice,
216
249
  useLobby: () => useLobby,
217
250
  useLocalMedia: () => useLocalMedia,
218
251
  useLocalUserId: () => useLocalUserId,
252
+ useMediaEffects: () => useMediaEffects,
219
253
  useMeetAi: () => useMeetAi,
220
254
  useMeetHost: () => useMeetHost,
255
+ useMeetShortcuts: () => useMeetShortcuts,
221
256
  useMeetSnapshot: () => useMeetSnapshot,
222
257
  useMeeting: () => useMeeting,
223
258
  useMeetingId: () => useMeetingId,
224
259
  useMeetingRecord: () => useMeetingRecord,
225
260
  useMeetings: () => useMeetings,
261
+ useModerationNotice: () => useModerationNotice,
226
262
  useReactions: () => useReactions,
227
263
  useRecording: () => useRecording,
228
264
  useRequiredMeetHost: () => useRequiredMeetHost,
@@ -266,7 +302,7 @@ var MeetError = class extends Error {
266
302
  };
267
303
  var FORBIDDEN_CODES = /* @__PURE__ */ new Set(["42501", "PGRST301", "PGRST302"]);
268
304
  var NOT_FOUND_CODES = /* @__PURE__ */ new Set(["PGRST116", "PGRST205"]);
269
- var CONFLICT_CODES = /* @__PURE__ */ new Set(["23505"]);
305
+ var CONFLICT_CODES = /* @__PURE__ */ new Set(["23505", "40001", "55000"]);
270
306
  var SESSION_MARKERS = [
271
307
  "auth session missing",
272
308
  "jwt expired",
@@ -424,8 +460,28 @@ var MEET_ROUTES = {
424
460
  */
425
461
  record: "/api/v1/meet/record",
426
462
  /** A host chooses what guests may read of ONE meeting (MRF-1). */
427
- recordAccess: "/api/v1/meet/record/access"
463
+ recordAccess: "/api/v1/meet/record/access",
464
+ /**
465
+ * Meet wave 1 (0.8): bring every invitee up to date after a write — sends the
466
+ * invitation / update / cancellation emails with their `.ics` and re-queues
467
+ * reminders. Idempotent. `{meeting_id}` is substituted by `meetingPath`.
468
+ */
469
+ meetingAnnounce: "/api/v1/meet/meetings/{meeting_id}/announce",
470
+ /** Put a one-off meeting on the caller's own Google Calendar (gated by the server). */
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"
428
481
  };
482
+ function meetingPath(template, meetingId) {
483
+ return template.replace("{meeting_id}", encodeURIComponent(meetingId));
484
+ }
429
485
  var ROOM_TOKEN_HEADER = "x-meet-room-token";
430
486
  var REFRESH_RATIO = 0.8;
431
487
  function readString(source, key) {
@@ -555,6 +611,19 @@ function createTokenClient(options) {
555
611
  "The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry."
556
612
  );
557
613
  }
614
+ if (response.status === 410) {
615
+ let reason = "This meeting was cancelled.";
616
+ try {
617
+ const body = await response.json();
618
+ if (typeof body?.detail?.message === "string") reason = body.detail.message;
619
+ } catch {
620
+ }
621
+ throw new MeetError(
622
+ "cancelled",
623
+ reason,
624
+ "Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one."
625
+ );
626
+ }
558
627
  if (response.status === 423) {
559
628
  throw new MeetError(
560
629
  "room-locked",
@@ -756,12 +825,12 @@ function createMeetAi(options) {
756
825
  async function dispatch(route, body, operation) {
757
826
  const response = await send({ path: route, body, operation, requireAuth: true });
758
827
  if (!response.ok) {
759
- const refusal = await refusalFor(response, operation);
760
- options.store.patch({ noteTakerRefusal: refusal });
828
+ const refusal2 = await refusalFor(response, operation);
829
+ options.store.patch({ noteTakerRefusal: refusal2 });
761
830
  throw new MeetError(
762
831
  response.status === 403 ? "forbidden" : "transport",
763
- refusal.reason,
764
- refusal.remedy
832
+ refusal2.reason,
833
+ refusal2.remedy
765
834
  );
766
835
  }
767
836
  options.store.patch({ noteTakerRefusal: null });
@@ -799,11 +868,11 @@ function createMeetAi(options) {
799
868
  ...signal !== void 0 ? { signal } : {}
800
869
  });
801
870
  if (!response.ok) {
802
- const refusal = await refusalFor(response, "meet.ai.ask");
871
+ const refusal2 = await refusalFor(response, "meet.ai.ask");
803
872
  throw new MeetError(
804
873
  response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : "transport",
805
- refusal.reason,
806
- refusal.remedy
874
+ refusal2.reason,
875
+ refusal2.remedy
807
876
  );
808
877
  }
809
878
  const body = response.body;
@@ -1713,6 +1782,11 @@ function captionSegment(args) {
1713
1782
  }
1714
1783
 
1715
1784
  // src/core/store.ts
1785
+ var DEFAULT_EFFECTS = {
1786
+ noiseSuppression: true,
1787
+ background: "none",
1788
+ backgroundImage: null
1789
+ };
1716
1790
  var NO_GRANTS = {
1717
1791
  role: "participant",
1718
1792
  canRecord: false,
@@ -1735,7 +1809,12 @@ var EMPTY = {
1735
1809
  recordingSource: "local",
1736
1810
  recordingId: null,
1737
1811
  chat: [],
1812
+ chatLoaded: false,
1738
1813
  reactions: [],
1814
+ spotlight: null,
1815
+ moderationNotice: null,
1816
+ actionNotice: null,
1817
+ effects: DEFAULT_EFFECTS,
1739
1818
  transcript: [],
1740
1819
  notes: [],
1741
1820
  names: {},
@@ -1774,6 +1853,7 @@ function createMeetStore(initial = {}) {
1774
1853
  setParticipants(participants) {
1775
1854
  const names = learnNames(state.names, participants);
1776
1855
  const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
1856
+ const handTimes = new Map(state.participants.map((p) => [p.identity, p.handRaisedAt ?? null]));
1777
1857
  const quality = new Map(state.participants.map((p) => [p.identity, p.quality]));
1778
1858
  commit({
1779
1859
  ...state,
@@ -1787,6 +1867,7 @@ function createMeetStore(initial = {}) {
1787
1867
  // name book. `roster-names.test.ts` fails if a token ever gets past it.
1788
1868
  displayName: displayNameFor(names, participant.identity, participant.displayName),
1789
1869
  handRaised: hands.get(participant.identity) ?? participant.handRaised,
1870
+ handRaisedAt: handTimes.get(participant.identity) ?? participant.handRaisedAt ?? null,
1790
1871
  quality: participant.quality === "unknown" ? quality.get(participant.identity) ?? "unknown" : participant.quality
1791
1872
  }))
1792
1873
  });
@@ -1809,11 +1890,17 @@ function createMeetStore(initial = {}) {
1809
1890
  )
1810
1891
  });
1811
1892
  },
1812
- setHand(identity, raised) {
1893
+ setHand(identity, raised, at) {
1813
1894
  commit({
1814
1895
  ...state,
1815
1896
  participants: state.participants.map(
1816
- (participant) => participant.identity === identity ? { ...participant, handRaised: raised } : participant
1897
+ (participant) => participant.identity === identity ? {
1898
+ ...participant,
1899
+ handRaised: raised,
1900
+ // A hand that is ALREADY up keeps its place in the queue when
1901
+ // it is re-announced (to a late joiner): the first time wins.
1902
+ handRaisedAt: raised ? participant.handRaised && participant.handRaisedAt != null ? participant.handRaisedAt : at ?? Date.now() : null
1903
+ } : participant
1817
1904
  )
1818
1905
  });
1819
1906
  },
@@ -1823,6 +1910,31 @@ function createMeetStore(initial = {}) {
1823
1910
  const names = learnName(state.names, message.identity, message.displayName);
1824
1911
  commit({ ...state, chat: chat.slice(-CHAT_LIMIT), names });
1825
1912
  },
1913
+ loadChat(messages) {
1914
+ const known = new Set(state.chat.map((message) => message.id));
1915
+ const merged = [...messages.filter((message) => !known.has(message.id)), ...state.chat].sort(
1916
+ (a, b) => a.at < b.at ? -1 : a.at > b.at ? 1 : 0
1917
+ );
1918
+ const names = learnNames(
1919
+ state.names,
1920
+ messages.map((message) => ({ identity: message.identity, displayName: message.displayName }))
1921
+ );
1922
+ commit({ ...state, chat: merged.slice(-CHAT_LIMIT), chatLoaded: true, names });
1923
+ },
1924
+ settleChat(localId, next) {
1925
+ const index = state.chat.findIndex((message) => message.id === localId);
1926
+ if (index === -1) return;
1927
+ const current = state.chat[index];
1928
+ let replacement;
1929
+ if ("failed" in next && typeof next.failed === "string" && !("id" in next)) {
1930
+ replacement = { ...current, failed: next.failed };
1931
+ } else {
1932
+ replacement = next;
1933
+ }
1934
+ const duplicate = replacement.id !== localId && state.chat.some((message) => message.id === replacement.id);
1935
+ const chat = duplicate ? state.chat.filter((message) => message.id !== localId) : state.chat.map((message, position) => position === index ? replacement : message);
1936
+ commit({ ...state, chat });
1937
+ },
1826
1938
  addReaction(reaction) {
1827
1939
  commit({ ...state, reactions: [...state.reactions, reaction] });
1828
1940
  },
@@ -1913,7 +2025,7 @@ function createMeetStore(initial = {}) {
1913
2025
  commit({ ...state, media: { ...state.media, [kind]: next } });
1914
2026
  },
1915
2027
  reset() {
1916
- commit({ ...EMPTY });
2028
+ commit({ ...EMPTY, effects: state.effects });
1917
2029
  }
1918
2030
  };
1919
2031
  }
@@ -2243,11 +2355,11 @@ function createMeetingSession(options) {
2243
2355
  } catch {
2244
2356
  body = null;
2245
2357
  }
2246
- const refusal = readServerRefusal(body);
2358
+ const refusal2 = readServerRefusal(body);
2247
2359
  throw new MeetError(
2248
2360
  response.status === 403 ? "forbidden" : "transport",
2249
- refusal?.reason ?? `meet.endForEveryone: the server answered ${response.status}`,
2250
- refusal?.remedy !== void 0 && refusal.remedy.length > 0 ? refusal.remedy : "The meeting is still running. Only a host may end it, and the server re-checks that \u2014 being a host on this screen is not the decision."
2361
+ refusal2?.reason ?? `meet.endForEveryone: the server answered ${response.status}`,
2362
+ refusal2?.remedy !== void 0 && refusal2.remedy.length > 0 ? refusal2.remedy : "The meeting is still running. Only a host may end it, and the server re-checks that \u2014 being a host on this screen is not the decision."
2251
2363
  );
2252
2364
  }
2253
2365
  let payload = null;
@@ -2290,7 +2402,11 @@ var TABLES = {
2290
2402
  invites: "meet_call_invites",
2291
2403
  recordings: "meet_recordings",
2292
2404
  transcripts: "meet_transcript_segments",
2293
- notes: "meet_notes"
2405
+ notes: "meet_notes",
2406
+ /** Wave 1 (0.8): who a meeting was sent to, and what they answered. */
2407
+ invitees: "meet_invitees",
2408
+ /** Wave 1 (0.8): one occurrence of a series cancelled or moved. */
2409
+ occurrenceOverrides: "meet_occurrence_overrides"
2294
2410
  };
2295
2411
  var RPCS = {
2296
2412
  /** Atomic: create the invite row and stamp its expiry in one statement. */
@@ -2306,10 +2422,41 @@ var RPCS = {
2306
2422
  admit: "meet_admit_participant",
2307
2423
  setLock: "meet_set_lock",
2308
2424
  endMeeting: "meet_end_meeting",
2309
- recordConsent: "meet_record_consent"
2425
+ recordConsent: "meet_record_consent",
2426
+ /*
2427
+ * ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
2428
+ * authorized by the meeting's host or a real iam grant (editor to change,
2429
+ * admin = co-host to archive or name a co-host) — never an organization
2430
+ * match. After any of these writes, call `announceMeeting` so the server
2431
+ * sends invitations / updates / cancellations and re-queues reminders.
2432
+ */
2433
+ /** Schedule with every setting at create; unset settings come from the knobs. */
2434
+ scheduleMeeting: "meet_schedule_meeting",
2435
+ /** Edit / reschedule — a JSON patch with a version compare-and-swap. */
2436
+ updateMeeting: "meet_update_meeting",
2437
+ /** Cancel is a state (`cancelled_at`); the link keeps resolving. */
2438
+ cancelMeeting: "meet_cancel_meeting",
2439
+ /** Archive (soft delete) — the invitations and exceptions cascade. */
2440
+ archiveMeeting: "meet_archive_meeting",
2441
+ restoreMeeting: "meet_restore_meeting",
2442
+ /** Cancel, move or restore ONE occurrence of a series by its original start. */
2443
+ setOccurrence: "meet_set_occurrence",
2444
+ addInvitees: "meet_add_invitees",
2445
+ setInviteeRole: "meet_set_invitee_role",
2446
+ removeInvitee: "meet_remove_invitee",
2447
+ /** A signed-in invitee answers for themselves. */
2448
+ respond: "meet_respond",
2449
+ /** The emailed RSVP link (no account): read and answer. */
2450
+ invitationByToken: "meet_invitation_by_token",
2451
+ respondByToken: "meet_respond_by_token",
2452
+ /** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */
2453
+ meetingOccurrences: "meet_meeting_occurrences",
2454
+ /** Every occurrence the reader can see between two instants — the Upcoming list. */
2455
+ occurrencesBetween: "meet_occurrences_between"
2310
2456
  };
2311
2457
  var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
2312
- var MEETING_COLUMNS = "id,organization_id,room_name,slug,title,kind,host_user_id,scheduled_for,scheduled_duration_minutes,started_at,ended_at,locked,lobby_enabled,recording_policy,ai_enabled,metadata";
2458
+ var MEETING_COLUMNS = "id,organization_id,room_name,slug,title,kind,host_user_id,scheduled_for,scheduled_duration_minutes,started_at,ended_at,locked,lobby_enabled,recording_policy,ai_enabled,metadata,time_zone,agenda,recurrence_rule,join_before_host,cancelled_at,cancellation_reason,deleted_at,calendar_sequence,version";
2459
+ var INVITEE_COLUMNS = "id,meeting_id,invitee_user_id,email,display_name,role,rsvp_state,responded_at,rsvp_note,last_sent_sequence,last_sent_at";
2313
2460
  var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
2314
2461
  var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
2315
2462
  var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
@@ -2407,6 +2554,138 @@ function projectNote(row) {
2407
2554
  source: "record"
2408
2555
  };
2409
2556
  }
2557
+ var RSVP_STATES = ["needs_action", "accepted", "declined", "tentative"];
2558
+ var OCCURRENCE_STATES = ["scheduled", "moved", "cancelled"];
2559
+ function rsvpState(value) {
2560
+ return RSVP_STATES.includes(value) ? value : "needs_action";
2561
+ }
2562
+ function num(row, key) {
2563
+ const value = row[key];
2564
+ return typeof value === "number" ? value : null;
2565
+ }
2566
+ function when(row, key) {
2567
+ const value = row[key];
2568
+ if (value instanceof Date) return value.toISOString();
2569
+ return text(row, key);
2570
+ }
2571
+ function projectInvitee(row) {
2572
+ const operation = "meet.projectInvitee";
2573
+ const userId = text(row, "invitee_user_id");
2574
+ return {
2575
+ id: requireText(row, "id", operation),
2576
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2577
+ userId: userId === null ? null : asUserId(userId),
2578
+ email: text(row, "email"),
2579
+ displayName: text(row, "display_name"),
2580
+ role: text(row, "role") === "cohost" ? "cohost" : "invitee",
2581
+ rsvpState: rsvpState(text(row, "rsvp_state")),
2582
+ respondedAt: when(row, "responded_at"),
2583
+ rsvpNote: text(row, "rsvp_note"),
2584
+ lastSentSequence: num(row, "last_sent_sequence"),
2585
+ lastSentAt: when(row, "last_sent_at")
2586
+ };
2587
+ }
2588
+ function projectOccurrence(row) {
2589
+ const operation = "meet.projectOccurrence";
2590
+ const state = text(row, "state");
2591
+ return {
2592
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2593
+ originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
2594
+ occurrenceStart: when(row, "occurrence_start") ?? requireText(row, "occurrence_start", operation),
2595
+ durationMinutes: num(row, "duration_minutes"),
2596
+ // An unknown state is shown as scheduled, never dropped: the row is real.
2597
+ state: OCCURRENCE_STATES.includes(state) ? state : "scheduled",
2598
+ sequence: num(row, "sequence") ?? 0
2599
+ };
2600
+ }
2601
+ function projectUpcoming(row) {
2602
+ const operation = "meet.projectUpcoming";
2603
+ const kind = text(row, "kind");
2604
+ const myRole = text(row, "my_role");
2605
+ const myRsvp = text(row, "my_rsvp");
2606
+ return {
2607
+ ...projectOccurrence(row),
2608
+ organizationId: asOrganizationId(requireText(row, "organization_id", operation)),
2609
+ title: text(row, "title") ?? "Meeting",
2610
+ slug: requireText(row, "slug", operation),
2611
+ kind: kind === "scheduled" || kind === "recurring" ? kind : "instant",
2612
+ hostUserId: asUserId(requireText(row, "host_user_id", operation)),
2613
+ timeZone: text(row, "time_zone") ?? "UTC",
2614
+ meetingCancelled: bool(row, "meeting_cancelled", false),
2615
+ myRsvp: myRsvp === null ? null : rsvpState(myRsvp),
2616
+ myRole: myRole === "host" || myRole === "cohost" || myRole === "invitee" ? myRole : null
2617
+ };
2618
+ }
2619
+ function projectOverride(row) {
2620
+ const operation = "meet.projectOverride";
2621
+ return {
2622
+ id: requireText(row, "id", operation),
2623
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2624
+ originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
2625
+ state: text(row, "state") === "moved" ? "moved" : "cancelled",
2626
+ newStart: when(row, "new_start"),
2627
+ newDurationMinutes: num(row, "new_duration_minutes"),
2628
+ reason: text(row, "reason"),
2629
+ calendarSequence: num(row, "calendar_sequence") ?? 0,
2630
+ deletedAt: when(row, "deleted_at")
2631
+ };
2632
+ }
2633
+ function projectRsvpInvitation(body) {
2634
+ const operation = "meet.projectRsvpInvitation";
2635
+ if (typeof body !== "object" || body === null) {
2636
+ throw invalidResponse(operation, "the RSVP door returned no object");
2637
+ }
2638
+ const row = body;
2639
+ if (row["ok"] !== true) {
2640
+ return {
2641
+ ok: false,
2642
+ message: text(row, "message") ?? "This invitation link is no longer valid \u2014 ask the host to send it again."
2643
+ };
2644
+ }
2645
+ const meeting = row["meeting"] ?? {};
2646
+ const invitee = row["invitee"] ?? {};
2647
+ return {
2648
+ ok: true,
2649
+ meeting: {
2650
+ title: text(meeting, "title") ?? "Meeting",
2651
+ slug: requireText(meeting, "slug", operation),
2652
+ scheduledFor: when(meeting, "scheduled_for"),
2653
+ durationMinutes: num(meeting, "duration_minutes"),
2654
+ timeZone: text(meeting, "time_zone") ?? "UTC",
2655
+ agenda: text(meeting, "agenda"),
2656
+ recurrenceRule: text(meeting, "recurrence_rule"),
2657
+ cancelledAt: when(meeting, "cancelled_at"),
2658
+ cancellationReason: text(meeting, "cancellation_reason"),
2659
+ endedAt: when(meeting, "ended_at")
2660
+ },
2661
+ invitee: {
2662
+ displayName: text(invitee, "display_name"),
2663
+ email: text(invitee, "email"),
2664
+ rsvpState: rsvpState(text(invitee, "rsvp_state")),
2665
+ respondedAt: when(invitee, "responded_at")
2666
+ }
2667
+ };
2668
+ }
2669
+ function settingsPatch(settings) {
2670
+ const patch = {};
2671
+ if (settings.lobbyEnabled !== void 0) patch["lobby_enabled"] = settings.lobbyEnabled;
2672
+ if (settings.recordingPolicy !== void 0) patch["recording_policy"] = settings.recordingPolicy;
2673
+ if (settings.aiEnabled !== void 0) patch["ai_enabled"] = settings.aiEnabled;
2674
+ if (settings.joinBeforeHost !== void 0) patch["join_before_host"] = settings.joinBeforeHost;
2675
+ return patch;
2676
+ }
2677
+ function changesPatch(changes) {
2678
+ const patch = settingsPatch(changes);
2679
+ if (changes.title !== void 0) patch["title"] = changes.title;
2680
+ if (changes.agenda !== void 0) patch["agenda"] = changes.agenda;
2681
+ if (changes.timeZone !== void 0) patch["time_zone"] = changes.timeZone;
2682
+ if (changes.scheduledFor !== void 0) patch["scheduled_for"] = changes.scheduledFor;
2683
+ if (changes.scheduledDurationMinutes !== void 0) {
2684
+ patch["scheduled_duration_minutes"] = changes.scheduledDurationMinutes;
2685
+ }
2686
+ if (changes.recurrenceRule !== void 0) patch["recurrence_rule"] = changes.recurrenceRule;
2687
+ return patch;
2688
+ }
2410
2689
  function createMeetRepository(options) {
2411
2690
  const db = options.client.schema(MEET_SCHEMA);
2412
2691
  async function guardVoid(operation, run) {
@@ -2476,7 +2755,16 @@ function createMeetRepository(options) {
2476
2755
  lobbyEnabled: bool(row, "lobby_enabled", true),
2477
2756
  recordingPolicy: policy === "disabled" || policy === "always-on" ? policy : "host-controlled",
2478
2757
  aiEnabled: bool(row, "ai_enabled", true),
2479
- metadata: typeof metadata === "object" && metadata !== null && !Array.isArray(metadata) ? metadata : {}
2758
+ metadata: typeof metadata === "object" && metadata !== null && !Array.isArray(metadata) ? metadata : {},
2759
+ timeZone: text(row, "time_zone") ?? "UTC",
2760
+ agenda: text(row, "agenda"),
2761
+ recurrenceRule: text(row, "recurrence_rule"),
2762
+ joinBeforeHost: bool(row, "join_before_host", true),
2763
+ cancelledAt: text(row, "cancelled_at"),
2764
+ cancellationReason: text(row, "cancellation_reason"),
2765
+ deletedAt: text(row, "deleted_at"),
2766
+ calendarSequence: typeof row["calendar_sequence"] === "number" ? row["calendar_sequence"] : 0,
2767
+ version: typeof row["version"] === "number" ? row["version"] : 1
2480
2768
  };
2481
2769
  }
2482
2770
  function projectRecording(row) {
@@ -2563,6 +2851,170 @@ function createMeetRepository(options) {
2563
2851
  projectNote,
2564
2852
  projectRecording,
2565
2853
  projectAttendee,
2854
+ projectInvitee,
2855
+ async scheduleMeeting(input) {
2856
+ requireOrg(input.organizationId, "meet.scheduleMeeting");
2857
+ const row = await guard(
2858
+ "meet.scheduleMeeting",
2859
+ () => db.rpc(RPCS.scheduleMeeting, {
2860
+ p_organization_id: input.organizationId,
2861
+ p_host_user_id: input.hostUserId,
2862
+ p_title: input.title,
2863
+ p_scheduled_for: input.scheduledFor,
2864
+ p_time_zone: input.timeZone,
2865
+ p_duration_minutes: input.durationMinutes ?? null,
2866
+ p_agenda: input.agenda ?? null,
2867
+ p_recurrence_rule: input.recurrenceRule ?? null,
2868
+ p_settings: settingsPatch(input.settings ?? {})
2869
+ })
2870
+ );
2871
+ return projectMeeting(row);
2872
+ },
2873
+ async updateMeeting(meetingId, changes, options2 = {}) {
2874
+ const row = await guard(
2875
+ "meet.updateMeeting",
2876
+ () => db.rpc(RPCS.updateMeeting, {
2877
+ p_meeting_id: meetingId,
2878
+ p_changes: changesPatch(changes),
2879
+ p_expected_version: options2.expectedVersion ?? null,
2880
+ p_by_user_id: options2.byUserId ?? null
2881
+ })
2882
+ );
2883
+ return projectMeeting(row);
2884
+ },
2885
+ async cancelMeeting(meetingId, reason, byUserId = null) {
2886
+ const row = await guard(
2887
+ "meet.cancelMeeting",
2888
+ () => db.rpc(RPCS.cancelMeeting, {
2889
+ p_meeting_id: meetingId,
2890
+ p_reason: reason,
2891
+ p_by_user_id: byUserId
2892
+ })
2893
+ );
2894
+ return projectMeeting(row);
2895
+ },
2896
+ async archiveMeeting(meetingId, byUserId = null) {
2897
+ const row = await guard(
2898
+ "meet.archiveMeeting",
2899
+ () => db.rpc(RPCS.archiveMeeting, { p_meeting_id: meetingId, p_by_user_id: byUserId })
2900
+ );
2901
+ return projectMeeting(row);
2902
+ },
2903
+ async restoreMeeting(meetingId, byUserId = null) {
2904
+ const row = await guard(
2905
+ "meet.restoreMeeting",
2906
+ () => db.rpc(RPCS.restoreMeeting, { p_meeting_id: meetingId, p_by_user_id: byUserId })
2907
+ );
2908
+ return projectMeeting(row);
2909
+ },
2910
+ async setOccurrence(args) {
2911
+ const row = await guard(
2912
+ "meet.setOccurrence",
2913
+ () => db.rpc(RPCS.setOccurrence, {
2914
+ p_meeting_id: args.meetingId,
2915
+ p_original_start: args.originalStart,
2916
+ p_action: args.action,
2917
+ p_new_start: args.newStart ?? null,
2918
+ p_new_duration_minutes: args.newDurationMinutes ?? null,
2919
+ p_reason: args.reason ?? null,
2920
+ p_by_user_id: args.byUserId ?? null
2921
+ })
2922
+ );
2923
+ return projectOverride(row);
2924
+ },
2925
+ async meetingOccurrences(meetingId, { from, to, limit = 200 }) {
2926
+ const rows = await guard(
2927
+ "meet.meetingOccurrences",
2928
+ () => db.rpc(RPCS.meetingOccurrences, {
2929
+ p_meeting_id: meetingId,
2930
+ p_from: from,
2931
+ p_to: to,
2932
+ p_limit: limit
2933
+ })
2934
+ );
2935
+ return rows.map(projectOccurrence);
2936
+ },
2937
+ async occurrencesBetween({ from, to, organizationId = null, limit = 500 }) {
2938
+ const rows = await guard(
2939
+ "meet.occurrencesBetween",
2940
+ () => db.rpc(RPCS.occurrencesBetween, {
2941
+ p_from: from,
2942
+ p_to: to,
2943
+ p_organization_id: organizationId,
2944
+ p_limit: limit
2945
+ })
2946
+ );
2947
+ return rows.map(projectUpcoming);
2948
+ },
2949
+ async invitees(meetingId) {
2950
+ const response = await db.from(TABLES.invitees).select(INVITEE_COLUMNS).eq("meeting_id", meetingId).is("deleted_at", null).order("created_at", { ascending: true }).order("id", { ascending: true });
2951
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.invitees");
2952
+ return (response.data ?? []).map(projectInvitee);
2953
+ },
2954
+ async addInvitees(meetingId, people, byUserId = null) {
2955
+ const rows = await guard(
2956
+ "meet.addInvitees",
2957
+ () => db.rpc(RPCS.addInvitees, {
2958
+ p_meeting_id: meetingId,
2959
+ p_invitees: people.map((person) => ({
2960
+ ...person.userId !== void 0 ? { user_id: person.userId } : {},
2961
+ ...person.email !== void 0 ? { email: person.email } : {},
2962
+ ...person.displayName !== void 0 ? { display_name: person.displayName } : {},
2963
+ ...person.role !== void 0 ? { role: person.role } : {}
2964
+ })),
2965
+ p_by_user_id: byUserId
2966
+ })
2967
+ );
2968
+ return rows.map(projectInvitee);
2969
+ },
2970
+ async setInviteeRole(inviteeId, role, byUserId = null) {
2971
+ const row = await guard(
2972
+ "meet.setInviteeRole",
2973
+ () => db.rpc(RPCS.setInviteeRole, {
2974
+ p_invitee_id: inviteeId,
2975
+ p_role: role,
2976
+ p_by_user_id: byUserId
2977
+ })
2978
+ );
2979
+ return projectInvitee(row);
2980
+ },
2981
+ async removeInvitee(inviteeId, byUserId = null) {
2982
+ const row = await guard(
2983
+ "meet.removeInvitee",
2984
+ () => db.rpc(RPCS.removeInvitee, { p_invitee_id: inviteeId, p_by_user_id: byUserId })
2985
+ );
2986
+ return projectInvitee(row);
2987
+ },
2988
+ async respond(meetingId, answer, note = null) {
2989
+ const row = await guard(
2990
+ "meet.respond",
2991
+ () => db.rpc(RPCS.respond, {
2992
+ p_meeting_id: meetingId,
2993
+ p_response: answer,
2994
+ p_note: note,
2995
+ p_by_user_id: null
2996
+ })
2997
+ );
2998
+ return projectInvitee(row);
2999
+ },
3000
+ async invitationByToken(secret) {
3001
+ const body = await guard(
3002
+ "meet.invitationByToken",
3003
+ () => db.rpc(RPCS.invitationByToken, { p_secret: secret })
3004
+ );
3005
+ return projectRsvpInvitation(body);
3006
+ },
3007
+ async respondByToken(secret, answer, note = null) {
3008
+ const body = await guard(
3009
+ "meet.respondByToken",
3010
+ () => db.rpc(RPCS.respondByToken, {
3011
+ p_secret: secret,
3012
+ p_response: answer,
3013
+ p_note: note
3014
+ })
3015
+ );
3016
+ return projectRsvpInvitation(body);
3017
+ },
2566
3018
  async attendees(meetingId) {
2567
3019
  const response = await db.from(TABLES.participants).select(ATTENDEE_COLUMNS).eq("meeting_id", meetingId).order("joined_at", { ascending: true }).order("id", { ascending: true });
2568
3020
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.attendees");
@@ -2828,7 +3280,19 @@ var ROOM_EVENTS = {
2828
3280
  * production 2026-09-09 the guest lane showed no indicator at all while the
2829
3281
  * host recorded (MRI-C8).
2830
3282
  */
2831
- recording: "matrx.meet.recording"
3283
+ recording: "matrx.meet.recording",
3284
+ /**
3285
+ * A chat message to everyone was SAVED (Meet wave 2). Payload
3286
+ * `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
3287
+ */
3288
+ chat: "matrx.meet.chat",
3289
+ /**
3290
+ * A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`:
3291
+ * `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the
3292
+ * person concerned; `lower-hands {identities: [...] | "*"}` · `role {identity,
3293
+ * role}` — to everyone.
3294
+ */
3295
+ moderation: "matrx.meet.moderation"
2832
3296
  };
2833
3297
  function isRoomEventTopic(topic) {
2834
3298
  return topic.startsWith(ROOM_EVENT_PREFIX);
@@ -2864,6 +3328,10 @@ function applyRoomEvent(args) {
2864
3328
  return applyEnded(store, payload);
2865
3329
  case ROOM_EVENTS.recording:
2866
3330
  return applyRecording(store, payload);
3331
+ case ROOM_EVENTS.chat:
3332
+ return applyChat(store, payload);
3333
+ case ROOM_EVENTS.moderation:
3334
+ return applyModeration(store, payload, now);
2867
3335
  default:
2868
3336
  return ignored(`${topic} is not known to this version of @ai-matrx/meet`);
2869
3337
  }
@@ -2955,6 +3423,101 @@ function applyEnded(store, payload) {
2955
3423
  store.patch({ meeting: { ...meeting, endedAt } });
2956
3424
  return HANDLED;
2957
3425
  }
3426
+ function chatFromRow(row) {
3427
+ const id = text2(row["id"]);
3428
+ const identity = text2(row["sender_identity"]);
3429
+ const body = text2(row["body"]);
3430
+ if (id === null || identity === null || body === null) return null;
3431
+ return {
3432
+ id,
3433
+ identity: asParticipantIdentity(identity),
3434
+ displayName: text2(row["sender_name"]) ?? "Participant",
3435
+ text: body,
3436
+ at: text2(row["created_at"]) ?? (/* @__PURE__ */ new Date()).toISOString(),
3437
+ // `false` only from a server whose chat table has not landed: the room heard
3438
+ // it, nobody will see it later — and the line says so.
3439
+ saved: row["saved"] !== false
3440
+ };
3441
+ }
3442
+ function applyChat(store, payload) {
3443
+ const row = object(payload["message"]);
3444
+ if (row === null) return ignored("a chat event with no `message` row");
3445
+ const meeting = store.snapshot().meeting;
3446
+ if (meeting !== null && text2(row["meeting_id"]) !== null && row["meeting_id"] !== meeting.id) {
3447
+ return ignored("a chat message for a different meeting");
3448
+ }
3449
+ const message = chatFromRow(row);
3450
+ if (message === null) return ignored("a chat row with no id, sender or body");
3451
+ store.addChat(message);
3452
+ return HANDLED;
3453
+ }
3454
+ function moderationText(kind, payload) {
3455
+ const by = text2(payload["by"]) ?? "The host";
3456
+ switch (kind) {
3457
+ case "muted": {
3458
+ const source = text2(payload["source"]) ?? "microphone";
3459
+ return source === "microphone" ? `${by} muted you.` : source === "camera" ? `${by} turned off your camera.` : `${by} stopped your screen share.`;
3460
+ }
3461
+ case "ask-unmute":
3462
+ return `${by} is asking you to unmute.`;
3463
+ case "removed":
3464
+ return `${by} removed you from this meeting.`;
3465
+ default:
3466
+ return null;
3467
+ }
3468
+ }
3469
+ function applyModeration(store, payload, now) {
3470
+ const snapshot = store.snapshot();
3471
+ const meetingId = text2(payload["meeting_id"]);
3472
+ if (snapshot.meeting !== null && meetingId !== null && meetingId !== snapshot.meeting.id) {
3473
+ return ignored("a moderation event for a different meeting");
3474
+ }
3475
+ const kind = text2(payload["kind"]);
3476
+ switch (kind) {
3477
+ case "lower-hands": {
3478
+ const raw = payload["identities"];
3479
+ const everyone = raw === "*";
3480
+ const named = new Set(
3481
+ Array.isArray(raw) ? raw.filter((value) => typeof value === "string") : []
3482
+ );
3483
+ for (const participant of snapshot.participants) {
3484
+ if (participant.handRaised && (everyone || named.has(participant.identity))) {
3485
+ store.setHand(participant.identity, false);
3486
+ }
3487
+ }
3488
+ return HANDLED;
3489
+ }
3490
+ case "role": {
3491
+ const identity = text2(payload["identity"]);
3492
+ const role = text2(payload["role"]);
3493
+ if (identity === null || role === null || !ROLES.includes(role)) {
3494
+ return ignored("a role event with no identity or an unknown role");
3495
+ }
3496
+ if (identity === snapshot.localIdentity) {
3497
+ store.patch({
3498
+ grants: { ...snapshot.grants, role },
3499
+ moderationNotice: {
3500
+ kind: "role",
3501
+ text: role === "host" ? "You are now the host." : role === "cohost" ? "You are now a co-host." : "You are no longer a co-host.",
3502
+ at: now()
3503
+ }
3504
+ });
3505
+ }
3506
+ return HANDLED;
3507
+ }
3508
+ case "muted":
3509
+ case "ask-unmute":
3510
+ case "removed": {
3511
+ const sentence = moderationText(kind, payload);
3512
+ if (sentence === null) return ignored("a moderation event with no sentence");
3513
+ store.patch({ moderationNotice: { kind, text: sentence, at: now() } });
3514
+ return HANDLED;
3515
+ }
3516
+ default:
3517
+ return ignored(`a moderation kind this version does not know: ${String(kind)}`);
3518
+ }
3519
+ }
3520
+ var ROLES = ["host", "cohost", "participant", "guest"];
2958
3521
 
2959
3522
  // src/core/note-taker.ts
2960
3523
  var NOTE_TAKER_IDENTITY_PREFIX = "agent:note_taker:";
@@ -2990,14 +3553,14 @@ function noteTakerStatus(args) {
2990
3553
  const row = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
2991
3554
  const recorded = readState(row["state"]);
2992
3555
  const detail = typeof row["reason"] === "string" && row["reason"].length > 0 ? row["reason"] : null;
2993
- const refusal = args.refusal ?? null;
3556
+ const refusal2 = args.refusal ?? null;
2994
3557
  if (recorded === null) {
2995
- return { state: "absent", detail, present, refusal };
3558
+ return { state: "absent", detail, present, refusal: refusal2 };
2996
3559
  }
2997
3560
  if (recorded === "transcribing" && !present) {
2998
- return { state: "handoff_pending", detail, present, refusal };
3561
+ return { state: "handoff_pending", detail, present, refusal: refusal2 };
2999
3562
  }
3000
- return { state: recorded, detail, present, refusal };
3563
+ return { state: recorded, detail, present, refusal: refusal2 };
3001
3564
  }
3002
3565
  function noteTakerLabel(status) {
3003
3566
  switch (status.state) {
@@ -3019,8 +3582,7 @@ function noteTakerLabel(status) {
3019
3582
  var DATA_TOPICS = {
3020
3583
  hand: "mx.hand",
3021
3584
  reaction: "mx.reaction",
3022
- chat: "mx.chat",
3023
- hostAction: "mx.host"
3585
+ chat: "mx.chat"
3024
3586
  };
3025
3587
  var TRANSCRIPTION_TOPIC = "lk.transcription";
3026
3588
  var TRANSCRIPTION_ATTRIBUTES = {
@@ -3156,7 +3718,37 @@ function createRoomEngine(options) {
3156
3718
  const handlers = {
3157
3719
  participants(list) {
3158
3720
  const joinedAt = new Date(now()).toISOString();
3721
+ const before = new Set(store.snapshot().participants.map((p) => p.identity));
3159
3722
  store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
3723
+ const snapshot = store.snapshot();
3724
+ const newcomer = snapshot.participants.some(
3725
+ (participant) => !participant.isLocal && !before.has(participant.identity)
3726
+ );
3727
+ const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
3728
+ if (newcomer && before.size > 0 && me?.handRaised === true) {
3729
+ void publish(DATA_TOPICS.hand, { raised: true, raisedAt: me.handRaisedAt ?? now() }).catch(
3730
+ () => void 0
3731
+ );
3732
+ }
3733
+ },
3734
+ roomMetadata(metadata) {
3735
+ let spotlight = null;
3736
+ if (metadata.length > 0) {
3737
+ try {
3738
+ const parsed = JSON.parse(metadata);
3739
+ if (typeof parsed === "object" && parsed !== null) {
3740
+ const value = parsed.spotlight;
3741
+ if (typeof value === "string" && value.length > 0) spotlight = asParticipantIdentity(value);
3742
+ }
3743
+ } catch {
3744
+ report(
3745
+ "warn",
3746
+ "meet: the room's metadata is not JSON, so no spotlight was read from it",
3747
+ "Only aidream writes room metadata; check services/meet/moderation.py."
3748
+ );
3749
+ }
3750
+ }
3751
+ if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
3160
3752
  },
3161
3753
  activeSpeakers(identities) {
3162
3754
  store.setSpeaking(identities.map(asParticipantIdentity));
@@ -3260,7 +3852,11 @@ function createRoomEngine(options) {
3260
3852
  const sender = asParticipantIdentity(from ?? "");
3261
3853
  switch (topic) {
3262
3854
  case DATA_TOPICS.hand:
3263
- store.setHand(sender, envelope.raised === true);
3855
+ store.setHand(
3856
+ sender,
3857
+ envelope.raised === true,
3858
+ typeof envelope.raisedAt === "number" ? envelope.raisedAt : now()
3859
+ );
3264
3860
  return;
3265
3861
  case DATA_TOPICS.reaction:
3266
3862
  if (typeof envelope.emoji === "string") {
@@ -3274,12 +3870,14 @@ function createRoomEngine(options) {
3274
3870
  return;
3275
3871
  case DATA_TOPICS.chat:
3276
3872
  if (typeof envelope.text === "string") {
3873
+ const local = store.snapshot().localIdentity;
3277
3874
  store.addChat({
3278
3875
  id: envelope.id ?? `${sender}-${now()}`,
3279
3876
  identity: sender,
3280
3877
  displayName: envelope.name ?? sender,
3281
3878
  text: envelope.text,
3282
- at: envelope.at ?? new Date(now()).toISOString()
3879
+ at: envelope.at ?? new Date(now()).toISOString(),
3880
+ ...typeof envelope.to === "string" && local !== null ? { to: local } : {}
3283
3881
  });
3284
3882
  }
3285
3883
  return;
@@ -3325,6 +3923,24 @@ function createRoomEngine(options) {
3325
3923
  store.patch({ phase: "connected", failure: null });
3326
3924
  store.setLocalTrack("microphone", IDLE_TRACK);
3327
3925
  store.setLocalTrack("camera", IDLE_TRACK);
3926
+ const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
3927
+ store.patch({ effects });
3928
+ if (!effects.noiseSuppression || effects.background !== "none") {
3929
+ try {
3930
+ await applyEffects(created, effects);
3931
+ } catch {
3932
+ }
3933
+ }
3934
+ for (const kind of ["audioinput", "videoinput", "audiooutput"]) {
3935
+ const chosen = options.resolveDevice?.(kind) ?? null;
3936
+ if (chosen === null || chosen === "default") continue;
3937
+ try {
3938
+ await created.switchDevice(kind, chosen);
3939
+ } catch (error) {
3940
+ const normalized = normalizeDeviceError(error, `meet.switchDevice(${kind})`);
3941
+ report("warn", normalized.message, normalized.remedy);
3942
+ }
3943
+ }
3328
3944
  if (request.mode === "video") {
3329
3945
  await enableTrack(created, "camera", true);
3330
3946
  }
@@ -3347,9 +3963,30 @@ function createRoomEngine(options) {
3347
3963
  return state;
3348
3964
  }
3349
3965
  }
3350
- async function publish(topic, body) {
3966
+ async function publish(topic, body, to) {
3351
3967
  if (driver === null) return;
3352
- await driver.publishData(topic, JSON.stringify(body));
3968
+ await driver.publishData(topic, JSON.stringify(body), to);
3969
+ }
3970
+ async function applyEffects(target, effects) {
3971
+ try {
3972
+ await target.setAudioProcessing?.({
3973
+ noiseSuppression: effects.noiseSuppression,
3974
+ echoCancellation: true,
3975
+ autoGainControl: true
3976
+ });
3977
+ } catch (error) {
3978
+ report("warn", `meet: noise suppression could not be applied: ${String(error)}`, "The microphone keeps working without it.");
3979
+ }
3980
+ if (target.setVideoEffect === void 0) return;
3981
+ const effect = effects.background === "blur" ? { kind: "blur" } : effects.background === "image" && effects.backgroundImage !== null ? { kind: "image", imageUrl: effects.backgroundImage } : { kind: "none" };
3982
+ if (effect.kind !== "none" && target.supportsVideoEffects?.() === false) return;
3983
+ try {
3984
+ await target.setVideoEffect(effect);
3985
+ } catch (error) {
3986
+ const normalized = normalizeMeetError(error, "meet.setVideoEffect");
3987
+ report("warn", normalized.message, normalized.remedy);
3988
+ throw normalized;
3989
+ }
3353
3990
  }
3354
3991
  return {
3355
3992
  join(request) {
@@ -3405,7 +4042,7 @@ function createRoomEngine(options) {
3405
4042
  async raiseHand(raised) {
3406
4043
  const local = store.snapshot().localIdentity;
3407
4044
  if (local !== null) store.setHand(local, raised);
3408
- await publish(DATA_TOPICS.hand, { raised });
4045
+ await publish(DATA_TOPICS.hand, { raised, raisedAt: now() });
3409
4046
  },
3410
4047
  async react(emoji) {
3411
4048
  const local = store.snapshot().localIdentity;
@@ -3415,32 +4052,41 @@ function createRoomEngine(options) {
3415
4052
  }
3416
4053
  await publish(DATA_TOPICS.reaction, { id, emoji });
3417
4054
  },
3418
- async sendChat(text3) {
4055
+ async sendPrivateChat(to, text3) {
3419
4056
  const trimmed = text3.trim();
3420
4057
  if (trimmed.length === 0) return;
3421
4058
  const local = store.snapshot().localIdentity;
3422
4059
  const id = `${local ?? "local"}-${now()}`;
3423
4060
  const at = new Date(now()).toISOString();
3424
4061
  if (local !== null) {
3425
- store.addChat({
3426
- id,
3427
- identity: local,
3428
- displayName: identity.displayName,
3429
- text: trimmed,
3430
- at
3431
- });
4062
+ store.addChat({ id, identity: local, displayName: identity.displayName, text: trimmed, at, to });
3432
4063
  }
3433
- await publish(DATA_TOPICS.chat, { id, text: trimmed, at, name: identity.displayName });
4064
+ await publish(
4065
+ DATA_TOPICS.chat,
4066
+ { id, text: trimmed, at, name: identity.displayName, to },
4067
+ [to]
4068
+ );
3434
4069
  },
3435
- async publishHostAction(action) {
3436
- if (!isHostRole(effectiveRole(store.snapshot()))) {
3437
- throw new MeetError(
3438
- "forbidden",
3439
- `meet.hostAction(${action.kind}): this participant is not a host`,
3440
- "Host powers come from the server-minted token metadata. Hide the control instead of rendering a dead button (no dead ends)."
3441
- );
4070
+ async setEffects(next) {
4071
+ const previous = store.snapshot().effects;
4072
+ const merged = { ...previous, ...next };
4073
+ store.patch({ effects: merged });
4074
+ options.effectsStorage?.save(merged);
4075
+ if (driver === null) return;
4076
+ try {
4077
+ await applyEffects(driver, merged);
4078
+ } catch {
4079
+ const reverted = { ...merged, background: previous.background, backgroundImage: previous.backgroundImage };
4080
+ store.patch({ effects: reverted });
4081
+ options.effectsStorage?.save(reverted);
3442
4082
  }
3443
- await publish(DATA_TOPICS.hostAction, { ...action });
4083
+ },
4084
+ supportsVideoEffects() {
4085
+ return driver?.supportsVideoEffects?.() ?? false;
4086
+ },
4087
+ async switchDevice(kind, deviceId2) {
4088
+ if (driver === null) return;
4089
+ await driver.switchDevice(kind, deviceId2);
3444
4090
  },
3445
4091
  pin(who) {
3446
4092
  store.patch({ pinned: who, layout: who === null ? "grid" : "speaker" });
@@ -3471,58 +4117,281 @@ function hostUserIds(participants) {
3471
4117
  return participants.filter((participant) => participant.role === "host" && participant.userId !== null).map((participant) => participant.userId);
3472
4118
  }
3473
4119
 
3474
- // src/core/livekit-driver.ts
3475
- function mapQuality(quality) {
3476
- switch (quality) {
3477
- case "excellent":
3478
- return "excellent";
3479
- case "good":
3480
- return "good";
3481
- case "poor":
3482
- return "poor";
3483
- case "lost":
3484
- return "lost";
3485
- default:
3486
- return "unknown";
3487
- }
3488
- }
3489
- function mapConnectionState(state) {
3490
- switch (state) {
3491
- case "connected":
3492
- return "connected";
3493
- case "connecting":
3494
- return "connecting";
3495
- case "reconnecting":
3496
- case "signalReconnecting":
3497
- return "reconnecting";
3498
- default:
3499
- return "disconnected";
3500
- }
4120
+ // src/core/effects.ts
4121
+ function coerce(value) {
4122
+ if (typeof value !== "object" || value === null) return DEFAULT_EFFECTS;
4123
+ const row = value;
4124
+ const background = row["background"] === "blur" || row["background"] === "image" ? row["background"] : "none";
4125
+ const backgroundImage = typeof row["backgroundImage"] === "string" ? row["backgroundImage"] : null;
4126
+ return {
4127
+ noiseSuppression: row["noiseSuppression"] !== false,
4128
+ background: background === "image" && backgroundImage === null ? "none" : background,
4129
+ backgroundImage
4130
+ };
3501
4131
  }
3502
- function snapshotParticipant(participant, isLocal) {
3503
- const name = participant.name;
4132
+ function createWebEffectsStorage(key = "mx.meet.effects") {
4133
+ let memory = DEFAULT_EFFECTS;
4134
+ const storage = globalThis.localStorage;
3504
4135
  return {
3505
- identity: participant.identity,
3506
- name: name ?? "",
3507
- metadata: participant.metadata ?? null,
3508
- // ATTRIBUTES ARE PART OF THE SNAPSHOT, not read ad hoc later: the note-taker
3509
- // announces the consent notice here (D10) and the roster is the only place
3510
- // this package looks at a participant.
3511
- attributes: participant.attributes ?? {},
3512
- isLocal,
3513
- micEnabled: participant.isMicrophoneEnabled,
3514
- cameraEnabled: participant.isCameraEnabled,
3515
- screenSharing: participant.isScreenShareEnabled
4136
+ load() {
4137
+ if (storage === void 0) return memory;
4138
+ try {
4139
+ const raw = storage.getItem(key);
4140
+ return raw === null ? memory : coerce(JSON.parse(raw));
4141
+ } catch {
4142
+ return memory;
4143
+ }
4144
+ },
4145
+ save(effects) {
4146
+ memory = effects;
4147
+ try {
4148
+ storage?.setItem(key, JSON.stringify(effects));
4149
+ } catch {
4150
+ }
4151
+ }
3516
4152
  };
3517
4153
  }
3518
- var encoder = new TextEncoder();
3519
- var decoder = new TextDecoder();
3520
- function createLiveKitDriver(handlers, options = {}) {
3521
- let room = null;
3522
- let projectRoster = null;
3523
- async function load() {
3524
- if (room !== null) return room;
3525
- let module2;
4154
+
4155
+ // src/core/in-room.ts
4156
+ var MODERATION_ACTIONS = [
4157
+ "mute",
4158
+ "stop_video",
4159
+ "stop_screen",
4160
+ "mute_all",
4161
+ "ask_unmute",
4162
+ "ask_all_unmute",
4163
+ "spotlight",
4164
+ "lower_hand",
4165
+ "lower_all_hands",
4166
+ "make_cohost",
4167
+ "revoke_cohost",
4168
+ "transfer_host",
4169
+ "rename",
4170
+ "remove",
4171
+ "report"
4172
+ ];
4173
+ function createInRoomClient(options) {
4174
+ const { api, store } = options;
4175
+ const now = options.now ?? (() => Date.now());
4176
+ function meetingId(operation) {
4177
+ const meeting = store.snapshot().meeting;
4178
+ if (meeting === null) {
4179
+ throw new MeetError(
4180
+ "not-found",
4181
+ `${operation}: this room has no meeting record`,
4182
+ "Moderation and saved chat belong to a meeting. An ad-hoc call has neither."
4183
+ );
4184
+ }
4185
+ return meeting.id;
4186
+ }
4187
+ function headers() {
4188
+ const token = options.resolveRoomToken();
4189
+ return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
4190
+ }
4191
+ async function refusal2(response, operation, remedy) {
4192
+ let body = null;
4193
+ try {
4194
+ body = await response.json();
4195
+ } catch {
4196
+ body = null;
4197
+ }
4198
+ const said = readServerRefusal(body);
4199
+ return new MeetError(
4200
+ response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
4201
+ said?.reason ?? `${operation}: the server answered ${response.status}`,
4202
+ said?.remedy !== void 0 && said.remedy.length > 0 ? said.remedy : remedy
4203
+ );
4204
+ }
4205
+ return {
4206
+ async moderate(request) {
4207
+ const operation = `meet.moderate(${request.action})`;
4208
+ const hdrs = headers();
4209
+ const response = await api.request({
4210
+ path: MEET_ROUTES.moderate,
4211
+ method: "POST",
4212
+ operation,
4213
+ body: {
4214
+ meeting_id: meetingId(operation),
4215
+ action: request.action,
4216
+ ...request.identity != null ? { identity: request.identity } : {},
4217
+ ...request.name !== void 0 ? { name: request.name } : {},
4218
+ ...request.reason !== void 0 ? { reason: request.reason } : {}
4219
+ },
4220
+ ...hdrs !== void 0 ? { headers: hdrs } : {}
4221
+ });
4222
+ if (!response.ok) {
4223
+ throw await refusal2(
4224
+ response,
4225
+ operation,
4226
+ "Nothing was changed. The server decides host powers from the meeting itself."
4227
+ );
4228
+ }
4229
+ const payload = await response.json();
4230
+ return {
4231
+ action: request.action,
4232
+ affected: Array.isArray(payload["affected"]) ? payload["affected"].filter((value) => typeof value === "string") : [],
4233
+ message: typeof payload["message"] === "string" ? payload["message"] : "Done."
4234
+ };
4235
+ },
4236
+ async sendChat(text3) {
4237
+ const body = text3.trim();
4238
+ if (body.length === 0) return;
4239
+ const operation = "meet.sendChat";
4240
+ const id = meetingId(operation);
4241
+ const snapshot = store.snapshot();
4242
+ const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
4243
+ const localId = `pending-${now()}`;
4244
+ const pending = {
4245
+ id: localId,
4246
+ identity: snapshot.localIdentity ?? "local",
4247
+ displayName: me?.displayName ?? "You",
4248
+ text: body,
4249
+ at: new Date(now()).toISOString(),
4250
+ saved: false
4251
+ };
4252
+ store.addChat(pending);
4253
+ const hdrs = headers();
4254
+ let response;
4255
+ try {
4256
+ response = await api.request({
4257
+ path: MEET_ROUTES.chat,
4258
+ method: "POST",
4259
+ operation,
4260
+ body: { meeting_id: id, body },
4261
+ ...hdrs !== void 0 ? { headers: hdrs } : {}
4262
+ });
4263
+ } catch (error) {
4264
+ store.settleChat(localId, { failed: "Not sent \u2014 the server could not be reached." });
4265
+ throw error;
4266
+ }
4267
+ if (!response.ok) {
4268
+ const error = await refusal2(response, operation, "Your message was not sent.");
4269
+ store.settleChat(localId, { failed: error.message });
4270
+ throw error;
4271
+ }
4272
+ const saved = chatFromRow(await response.json());
4273
+ if (saved === null) {
4274
+ store.settleChat(localId, { failed: "The server's answer could not be read." });
4275
+ return;
4276
+ }
4277
+ store.settleChat(localId, saved);
4278
+ },
4279
+ async loadChat() {
4280
+ const operation = "meet.loadChat";
4281
+ const id = meetingId(operation);
4282
+ const hdrs = headers();
4283
+ const response = await api.request({
4284
+ path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(id)}`,
4285
+ method: "GET",
4286
+ operation,
4287
+ ...hdrs !== void 0 ? { headers: hdrs } : {}
4288
+ });
4289
+ if (!response.ok) {
4290
+ throw await refusal2(
4291
+ response,
4292
+ operation,
4293
+ "Messages sent before you joined could not be read. New messages still arrive."
4294
+ );
4295
+ }
4296
+ const payload = await response.json();
4297
+ const rows = Array.isArray(payload.messages) ? payload.messages : [];
4298
+ const messages = rows.map(
4299
+ (row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
4300
+ ).filter((message) => message !== null);
4301
+ store.loadChat(messages);
4302
+ }
4303
+ };
4304
+ }
4305
+
4306
+ // src/core/livekit-driver.ts
4307
+ function mapQuality(quality) {
4308
+ switch (quality) {
4309
+ case "excellent":
4310
+ return "excellent";
4311
+ case "good":
4312
+ return "good";
4313
+ case "poor":
4314
+ return "poor";
4315
+ case "lost":
4316
+ return "lost";
4317
+ default:
4318
+ return "unknown";
4319
+ }
4320
+ }
4321
+ function mapConnectionState(state) {
4322
+ switch (state) {
4323
+ case "connected":
4324
+ return "connected";
4325
+ case "connecting":
4326
+ return "connecting";
4327
+ case "reconnecting":
4328
+ case "signalReconnecting":
4329
+ return "reconnecting";
4330
+ default:
4331
+ return "disconnected";
4332
+ }
4333
+ }
4334
+ function snapshotParticipant(participant, isLocal) {
4335
+ const name = participant.name;
4336
+ return {
4337
+ identity: participant.identity,
4338
+ name: name ?? "",
4339
+ metadata: participant.metadata ?? null,
4340
+ // ATTRIBUTES ARE PART OF THE SNAPSHOT, not read ad hoc later: the note-taker
4341
+ // announces the consent notice here (D10) and the roster is the only place
4342
+ // this package looks at a participant.
4343
+ attributes: participant.attributes ?? {},
4344
+ isLocal,
4345
+ micEnabled: participant.isMicrophoneEnabled,
4346
+ cameraEnabled: participant.isCameraEnabled,
4347
+ screenSharing: participant.isScreenShareEnabled
4348
+ };
4349
+ }
4350
+ var DEFAULT_AUDIO_PROCESSING = {
4351
+ noiseSuppression: true,
4352
+ echoCancellation: true,
4353
+ autoGainControl: true
4354
+ };
4355
+ function videoEffectsSupported() {
4356
+ const scope = globalThis;
4357
+ if (typeof scope.OffscreenCanvas === "undefined" || scope.document === void 0) return false;
4358
+ try {
4359
+ return scope.document.createElement("canvas").getContext("webgl2") !== null;
4360
+ } catch {
4361
+ return false;
4362
+ }
4363
+ }
4364
+ async function applyVideoEffect(target, effect) {
4365
+ const { Track } = await import("livekit-client");
4366
+ const track = target.localParticipant.getTrackPublication(Track.Source.Camera)?.track;
4367
+ if (track === void 0) return;
4368
+ try {
4369
+ if (effect.kind === "none") {
4370
+ await track.stopProcessor();
4371
+ return;
4372
+ }
4373
+ const processors = await import("@livekit/track-processors");
4374
+ const processor = effect.kind === "blur" ? processors.BackgroundBlur(effect.radius ?? 12) : processors.VirtualBackground(effect.imageUrl);
4375
+ await track.setProcessor(processor);
4376
+ } catch (error) {
4377
+ throw new MeetError(
4378
+ "media-unsupported",
4379
+ "The background effect could not start on this camera",
4380
+ "Background effects need a browser with WebGL2 (current Chrome, Edge, Firefox or Safari). Your video continues without the effect.",
4381
+ error
4382
+ );
4383
+ }
4384
+ }
4385
+ var encoder = new TextEncoder();
4386
+ var decoder = new TextDecoder();
4387
+ function createLiveKitDriver(handlers, options = {}) {
4388
+ let room = null;
4389
+ let projectRoster = null;
4390
+ let audioCapture = DEFAULT_AUDIO_PROCESSING;
4391
+ let videoEffect = { kind: "none" };
4392
+ async function load() {
4393
+ if (room !== null) return room;
4394
+ let module2;
3526
4395
  try {
3527
4396
  module2 = await import("livekit-client");
3528
4397
  } catch (error) {
@@ -3548,7 +4417,9 @@ function createLiveKitDriver(handlers, options = {}) {
3548
4417
  handlers.participants([local, ...remotes]);
3549
4418
  };
3550
4419
  projectRoster = roster;
3551
- created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, roster).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, roster).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
4420
+ created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, roster).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, roster).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ParticipantNameChanged, roster).on(RoomEvent.ParticipantAttributesChanged, roster).on(RoomEvent.ParticipantMetadataChanged, roster).on(RoomEvent.RoomMetadataChanged, (metadata) => {
4421
+ handlers.roomMetadata?.(metadata ?? "");
4422
+ }).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
3552
4423
  handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
3553
4424
  }).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
3554
4425
  handlers.quality(participant.identity, mapQuality(quality));
@@ -3625,6 +4496,7 @@ function createLiveKitDriver(handlers, options = {}) {
3625
4496
  throw normalizeDeviceError(error, "meet.connect");
3626
4497
  }
3627
4498
  projectRoster?.();
4499
+ handlers.roomMetadata?.(target.metadata ?? "");
3628
4500
  },
3629
4501
  async disconnect() {
3630
4502
  if (room === null) return;
@@ -3639,14 +4511,19 @@ function createLiveKitDriver(handlers, options = {}) {
3639
4511
  },
3640
4512
  async setMicrophoneEnabled(enabled) {
3641
4513
  try {
3642
- await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(enabled);
4514
+ await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(
4515
+ enabled,
4516
+ audioCapture
4517
+ );
3643
4518
  } catch (error) {
3644
4519
  throw normalizeDeviceError(error, "meet.setMicrophoneEnabled");
3645
4520
  }
3646
4521
  },
3647
4522
  async setCameraEnabled(enabled) {
3648
4523
  try {
3649
- await requireRoom("setCameraEnabled").localParticipant.setCameraEnabled(enabled);
4524
+ const target = requireRoom("setCameraEnabled");
4525
+ await target.localParticipant.setCameraEnabled(enabled);
4526
+ if (enabled && videoEffect.kind !== "none") await applyVideoEffect(target, videoEffect);
3650
4527
  } catch (error) {
3651
4528
  throw normalizeDeviceError(error, "meet.setCameraEnabled");
3652
4529
  }
@@ -3658,14 +4535,40 @@ function createLiveKitDriver(handlers, options = {}) {
3658
4535
  throw normalizeDeviceError(error, "meet.setScreenShareEnabled");
3659
4536
  }
3660
4537
  },
3661
- async publishData(topic, payload) {
4538
+ async publishData(topic, payload, to) {
3662
4539
  await requireRoom("publishData").localParticipant.publishData(
3663
4540
  // `reliable` is not optional for a control plane: a dropped
3664
4541
  // "hand lowered" leaves a hand up on every other screen forever.
3665
4542
  encoder.encode(payload),
3666
- { reliable: true, topic }
4543
+ to === void 0 || to.length === 0 ? { reliable: true, topic } : { reliable: true, topic, destinationIdentities: [...to] }
3667
4544
  );
3668
4545
  },
4546
+ async setAudioProcessing(processing) {
4547
+ audioCapture = { ...audioCapture, ...processing };
4548
+ const target = room;
4549
+ if (target === null) return;
4550
+ const { Track } = await import("livekit-client");
4551
+ const publication = target.localParticipant.getTrackPublication(Track.Source.Microphone);
4552
+ const track = publication?.track;
4553
+ if (track === void 0 || publication?.isMuted === true) return;
4554
+ try {
4555
+ await track.restartTrack({
4556
+ ...audioCapture,
4557
+ ...target.getActiveDevice("audioinput") !== void 0 ? { deviceId: target.getActiveDevice("audioinput") } : {}
4558
+ });
4559
+ } catch (error) {
4560
+ throw normalizeDeviceError(error, "meet.setAudioProcessing");
4561
+ }
4562
+ },
4563
+ async setVideoEffect(effect) {
4564
+ videoEffect = effect;
4565
+ const target = room;
4566
+ if (target === null) return;
4567
+ await applyVideoEffect(target, effect);
4568
+ },
4569
+ supportsVideoEffects() {
4570
+ return videoEffectsSupported();
4571
+ },
3669
4572
  attach(identity, kind, element) {
3670
4573
  const target = requireRoom("attach");
3671
4574
  const source = sources?.[kind];
@@ -3699,6 +4602,52 @@ function createLiveKitDriver(handlers, options = {}) {
3699
4602
  }
3700
4603
  };
3701
4604
  }
4605
+ async function createCameraPreview(deviceId2, effect) {
4606
+ let module2;
4607
+ try {
4608
+ module2 = await import("livekit-client");
4609
+ } catch (error) {
4610
+ throw new MeetError(
4611
+ "misconfigured",
4612
+ "livekit-client could not be loaded for the camera preview",
4613
+ "The bundler excluded `livekit-client`; remove it from any `external` list.",
4614
+ error
4615
+ );
4616
+ }
4617
+ let track;
4618
+ try {
4619
+ track = await module2.createLocalVideoTrack(deviceId2 === null ? {} : { deviceId: deviceId2 });
4620
+ } catch (error) {
4621
+ throw normalizeDeviceError(error, "meet.cameraPreview");
4622
+ }
4623
+ const target = track;
4624
+ async function apply(next) {
4625
+ if (next.kind === "none") {
4626
+ await target.stopProcessor();
4627
+ return;
4628
+ }
4629
+ const processors = await import("@livekit/track-processors");
4630
+ await target.setProcessor(
4631
+ next.kind === "blur" ? processors.BackgroundBlur(next.radius ?? 12) : processors.VirtualBackground(next.imageUrl)
4632
+ );
4633
+ }
4634
+ if (effect.kind !== "none" && videoEffectsSupported()) {
4635
+ try {
4636
+ await apply(effect);
4637
+ } catch {
4638
+ }
4639
+ }
4640
+ return {
4641
+ attach(element) {
4642
+ track.attach(element);
4643
+ },
4644
+ setEffect: apply,
4645
+ stop() {
4646
+ track.stop();
4647
+ track.detach();
4648
+ }
4649
+ };
4650
+ }
3702
4651
 
3703
4652
  // src/react/provider.tsx
3704
4653
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -3808,7 +4757,9 @@ function MeetRuntime(props) {
3808
4757
  avatarUrl: props.avatarUrl ?? null,
3809
4758
  deviceId: deviceId()
3810
4759
  };
3811
- const store = createMeetStore();
4760
+ const effectsStorage = createWebEffectsStorage();
4761
+ const store = createMeetStore({ effects: effectsStorage.load() });
4762
+ const devicesHolder = { current: null };
3812
4763
  const api = createMeetApi({
3813
4764
  baseUrl,
3814
4765
  organizationId: identity.organizationId,
@@ -3827,7 +4778,9 @@ function MeetRuntime(props) {
3827
4778
  identity,
3828
4779
  resolveMeeting: (slug) => repository.meetingBySlug(slug),
3829
4780
  driverFactory: (handlers) => (driverRef.current ?? createLiveKitDriver)(handlers),
3830
- onDiagnostic: report
4781
+ onDiagnostic: report,
4782
+ effectsStorage,
4783
+ resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null
3831
4784
  });
3832
4785
  const meetings = createMeetingSession({
3833
4786
  manager,
@@ -3842,16 +4795,32 @@ function MeetRuntime(props) {
3842
4795
  void engine.rejoin();
3843
4796
  }
3844
4797
  });
3845
- const ai = createMeetAi({
3846
- api,
3847
- store,
3848
- resolveRoomToken: () => {
3849
- const roomName = store.snapshot().meeting?.roomName;
3850
- if (roomName === void 0) return null;
3851
- const active = engine.token();
3852
- if (active?.roomName === roomName && active.token.length > 0) return active.token;
3853
- return tokens.remembered(roomName)?.token ?? null;
4798
+ const resolveRoomToken = () => {
4799
+ const roomName = store.snapshot().meeting?.roomName;
4800
+ if (roomName === void 0) return null;
4801
+ const active = engine.token();
4802
+ if (active?.roomName === roomName && active.token.length > 0) return active.token;
4803
+ return tokens.remembered(roomName)?.token ?? null;
4804
+ };
4805
+ const ai = createMeetAi({ api, store, resolveRoomToken });
4806
+ const inRoom = createInRoomClient({ api, store, resolveRoomToken });
4807
+ let chatReadFor = null;
4808
+ store.subscribe(() => {
4809
+ const snapshot = store.snapshot();
4810
+ if (snapshot.phase !== "connected" || snapshot.meeting === null) {
4811
+ if (snapshot.phase === "left" || snapshot.phase === "idle") chatReadFor = null;
4812
+ return;
3854
4813
  }
4814
+ if (chatReadFor === snapshot.meeting.id) return;
4815
+ chatReadFor = snapshot.meeting.id;
4816
+ void inRoom.loadChat().catch((error) => {
4817
+ const normalized = error;
4818
+ report({
4819
+ level: "warn",
4820
+ message: normalized.message ?? "meet: the saved chat could not be read",
4821
+ ...normalized.remedy !== void 0 ? { remedy: normalized.remedy } : {}
4822
+ });
4823
+ });
3855
4824
  });
3856
4825
  const recording = createRecordingController({
3857
4826
  api,
@@ -3873,6 +4842,7 @@ function MeetRuntime(props) {
3873
4842
  },
3874
4843
  storage: createWebDeviceStorage()
3875
4844
  });
4845
+ devicesHolder.current = devices;
3876
4846
  const calls = identity.userId === null ? null : createCallCenter({
3877
4847
  manager,
3878
4848
  repository,
@@ -3887,6 +4857,7 @@ function MeetRuntime(props) {
3887
4857
  engine,
3888
4858
  calls,
3889
4859
  meetings,
4860
+ inRoom,
3890
4861
  recording,
3891
4862
  devices,
3892
4863
  repository,
@@ -4008,7 +4979,8 @@ function computeStage(args) {
4008
4979
  const ordered = orderParticipants(args.participants, args.activeSpeakers);
4009
4980
  const sharer = ordered.find((participant) => participant.screenSharing) ?? null;
4010
4981
  const pinnedParticipant = args.pinned === null ? null : ordered.find((participant) => participant.identity === args.pinned) ?? null;
4011
- const feature = sharer !== null ? sharer : args.layout === "grid" ? null : pinnedParticipant ?? ordered[0] ?? null;
4982
+ const spotlit = args.spotlight == null ? null : ordered.find((participant) => participant.identity === args.spotlight) ?? null;
4983
+ const feature = sharer !== null ? sharer : pinnedParticipant !== null && args.layout !== "grid" ? pinnedParticipant : spotlit !== null ? spotlit : args.layout === "grid" ? null : ordered[0] ?? null;
4012
4984
  const rest = feature === null ? ordered : ordered.filter((participant) => participant.identity !== feature.identity);
4013
4985
  const pageCount = Math.max(1, Math.ceil(rest.length / pageSize));
4014
4986
  const page = Math.min(Math.max(0, args.page ?? 0), pageCount - 1);
@@ -4022,11 +4994,11 @@ function computeStage(args) {
4022
4994
  presenting: sharer !== null
4023
4995
  };
4024
4996
  }
4025
- function gridColumns(count) {
4026
- if (count <= 1) return 1;
4027
- if (count <= 4) return 2;
4028
- if (count <= 9) return 3;
4029
- if (count <= 16) return 4;
4997
+ function gridColumns(count2) {
4998
+ if (count2 <= 1) return 1;
4999
+ if (count2 <= 4) return 2;
5000
+ if (count2 <= 9) return 3;
5001
+ if (count2 <= 16) return 4;
4030
5002
  return 5;
4031
5003
  }
4032
5004
 
@@ -4200,7 +5172,7 @@ function meetingWhen(meeting, locale) {
4200
5172
  if (start === null) return "Time not recorded";
4201
5173
  const startedAt = new Date(start);
4202
5174
  if (Number.isNaN(startedAt.getTime())) return "Time not recorded";
4203
- const when = startedAt.toLocaleString(locale, {
5175
+ const when2 = startedAt.toLocaleString(locale, {
4204
5176
  weekday: "long",
4205
5177
  year: "numeric",
4206
5178
  month: "long",
@@ -4208,11 +5180,11 @@ function meetingWhen(meeting, locale) {
4208
5180
  hour: "numeric",
4209
5181
  minute: "2-digit"
4210
5182
  });
4211
- if (meeting.endedAt === null || meeting.startedAt === null) return when;
5183
+ if (meeting.endedAt === null || meeting.startedAt === null) return when2;
4212
5184
  const endedAt = new Date(meeting.endedAt);
4213
- if (Number.isNaN(endedAt.getTime())) return when;
5185
+ if (Number.isNaN(endedAt.getTime())) return when2;
4214
5186
  const elapsedMs = endedAt.getTime() - startedAt.getTime();
4215
- return `${when} \xB7 ${(0, import_format.formatDurationMs)(elapsedMs, { style: "long" })}`;
5187
+ return `${when2} \xB7 ${(0, import_format.formatDurationMs)(elapsedMs, { style: "long" })}`;
4216
5188
  }
4217
5189
  var GUEST_RECORD_ACCESS_LEVELS = [
4218
5190
  "none",
@@ -4319,8 +5291,8 @@ async function loadGuestMeetingRecord(args) {
4319
5291
  );
4320
5292
  }
4321
5293
  if (!response.ok) {
4322
- const refusal = await readRefusal(response);
4323
- return refusedBundle(meeting, refusal.message, refusal.remedy);
5294
+ const refusal2 = await readRefusal(response);
5295
+ return refusedBundle(meeting, refusal2.message, refusal2.remedy);
4324
5296
  }
4325
5297
  const body = await response.json();
4326
5298
  const payload = typeof body["data"] === "object" && body["data"] !== null ? body["data"] : body;
@@ -4411,8 +5383,8 @@ async function setGuestRecordAccess(args) {
4411
5383
  }
4412
5384
  });
4413
5385
  if (!response.ok) {
4414
- const refusal = await readRefusal(response);
4415
- throw new MeetError("forbidden", refusal.message, refusal.remedy);
5386
+ const refusal2 = await readRefusal(response);
5387
+ throw new MeetError("forbidden", refusal2.message, refusal2.remedy);
4416
5388
  }
4417
5389
  const body = await response.json();
4418
5390
  const payload = typeof body["data"] === "object" && body["data"] !== null ? body["data"] : body;
@@ -4487,7 +5459,14 @@ function useRoom() {
4487
5459
  [engine]
4488
5460
  ),
4489
5461
  sendChat: (0, import_react3.useCallback)(
4490
- async (text3) => requireEngine(engine, "sendChat").sendChat(text3),
5462
+ async (text3) => {
5463
+ if (host === null) throw noRuntime("sendChat");
5464
+ await host.inRoom.sendChat(text3);
5465
+ },
5466
+ [host]
5467
+ ),
5468
+ sendPrivateChat: (0, import_react3.useCallback)(
5469
+ async (to, text3) => requireEngine(engine, "sendPrivateChat").sendPrivateChat(to, text3),
4491
5470
  [engine]
4492
5471
  ),
4493
5472
  pin: (0, import_react3.useCallback)(
@@ -4508,6 +5487,7 @@ function useStage(pageSize = DEFAULT_PAGE_SIZE) {
4508
5487
  participants: snapshot?.participants ?? [],
4509
5488
  layout: snapshot?.layout ?? "grid",
4510
5489
  pinned: snapshot?.pinned ?? null,
5490
+ spotlight: snapshot?.spotlight ?? null,
4511
5491
  activeSpeakers: snapshot?.activeSpeakers ?? [],
4512
5492
  pageSize,
4513
5493
  page
@@ -4840,33 +5820,160 @@ function useHostControls() {
4840
5820
  const host = useMeetHost();
4841
5821
  const snapshot = useMeetSnapshot();
4842
5822
  const role = useRole();
4843
- const engine = host?.engine ?? null;
4844
5823
  const me = snapshot?.participants.find(
4845
5824
  (participant) => participant.identity === snapshot.localIdentity
4846
5825
  );
4847
5826
  const actor = me === void 0 ? void 0 : { ...me, role };
5827
+ const [busy, setBusy] = (0, import_react3.useState)(false);
5828
+ const inRoom = host?.inRoom ?? null;
5829
+ const store = host?.store ?? null;
5830
+ const notice = snapshot?.actionNotice ?? null;
5831
+ const setNotice = (0, import_react3.useCallback)(
5832
+ (next) => store?.patch({ actionNotice: next === null ? null : { ...next, at: Date.now() } }),
5833
+ [store]
5834
+ );
5835
+ const run = (0, import_react3.useCallback)(
5836
+ async (request) => {
5837
+ if (inRoom === null) throw noRuntime(`moderate(${request.action})`);
5838
+ setBusy(true);
5839
+ try {
5840
+ const result = await inRoom.moderate(request);
5841
+ setNotice({ text: result.message, failed: false });
5842
+ return result;
5843
+ } catch (thrown) {
5844
+ const error = thrown;
5845
+ setNotice({
5846
+ text: [error.message, error.remedy].filter(Boolean).join(" "),
5847
+ failed: true
5848
+ });
5849
+ return null;
5850
+ } finally {
5851
+ setBusy(false);
5852
+ }
5853
+ },
5854
+ [inRoom, setNotice]
5855
+ );
4848
5856
  return {
4849
5857
  isHost: isHostRole(role),
5858
+ isOwner: role === "host",
5859
+ notice,
5860
+ busy,
5861
+ clearNotice: (0, import_react3.useCallback)(() => setNotice(null), [setNotice]),
4850
5862
  canModerate: (0, import_react3.useCallback)(
4851
5863
  (target) => canModerateParticipant(actor, target),
4852
5864
  [actor]
4853
5865
  ),
5866
+ run,
4854
5867
  mute: (0, import_react3.useCallback)(
4855
- async (target) => requireEngine(engine, "muteParticipant").publishHostAction({
4856
- kind: "mute",
4857
- identity: target.identity
4858
- }),
4859
- [engine]
5868
+ async (target) => {
5869
+ await run({ action: "mute", identity: target.identity });
5870
+ },
5871
+ [run]
4860
5872
  ),
4861
5873
  remove: (0, import_react3.useCallback)(
4862
- async (target) => requireEngine(engine, "removeParticipant").publishHostAction({
4863
- kind: "remove",
4864
- identity: target.identity
4865
- }),
5874
+ async (target) => {
5875
+ await run({ action: "remove", identity: target.identity });
5876
+ },
5877
+ [run]
5878
+ )
5879
+ };
5880
+ }
5881
+ function useHandQueue() {
5882
+ const snapshot = useMeetSnapshot();
5883
+ return (0, import_react3.useMemo)(
5884
+ () => (snapshot?.participants ?? []).filter((participant) => participant.handRaised).sort((a, b) => (a.handRaisedAt ?? 0) - (b.handRaisedAt ?? 0)),
5885
+ [snapshot?.participants]
5886
+ );
5887
+ }
5888
+ function useMediaEffects() {
5889
+ const host = useMeetHost();
5890
+ const snapshot = useMeetSnapshot();
5891
+ const engine = host?.engine ?? null;
5892
+ return {
5893
+ effects: snapshot?.effects ?? DEFAULT_EFFECTS,
5894
+ backgroundsSupported: engine?.supportsVideoEffects() ?? false,
5895
+ set: (0, import_react3.useCallback)(
5896
+ async (next) => requireEngine(engine, "setEffects").setEffects(next),
4866
5897
  [engine]
4867
5898
  )
4868
5899
  };
4869
5900
  }
5901
+ function useModerationNotice() {
5902
+ const host = useMeetHost();
5903
+ const snapshot = useMeetSnapshot();
5904
+ return {
5905
+ notice: snapshot?.moderationNotice ?? null,
5906
+ dismiss: (0, import_react3.useCallback)(() => host?.store.patch({ moderationNotice: null }), [host])
5907
+ };
5908
+ }
5909
+ function useAudioLevel(stream) {
5910
+ const [level, setLevel] = (0, import_react3.useState)(0);
5911
+ (0, import_react3.useEffect)(() => {
5912
+ if (stream === null || stream.getAudioTracks().length === 0) {
5913
+ setLevel(0);
5914
+ return void 0;
5915
+ }
5916
+ const Context = globalThis.AudioContext ?? globalThis.webkitAudioContext;
5917
+ if (Context === void 0) return void 0;
5918
+ const context = new Context();
5919
+ const source = context.createMediaStreamSource(stream);
5920
+ const analyser = context.createAnalyser();
5921
+ analyser.fftSize = 512;
5922
+ source.connect(analyser);
5923
+ const samples = new Uint8Array(analyser.fftSize);
5924
+ let frame = 0;
5925
+ const tick = () => {
5926
+ analyser.getByteTimeDomainData(samples);
5927
+ let peak = 0;
5928
+ for (const sample of samples) peak = Math.max(peak, Math.abs(sample - 128) / 128);
5929
+ setLevel((previous) => Math.abs(previous - peak) > 0.02 ? peak : previous);
5930
+ frame = requestAnimationFrame(tick);
5931
+ };
5932
+ frame = requestAnimationFrame(tick);
5933
+ return () => {
5934
+ cancelAnimationFrame(frame);
5935
+ source.disconnect();
5936
+ void context.close();
5937
+ };
5938
+ }, [stream]);
5939
+ return level;
5940
+ }
5941
+ var MEET_SHORTCUTS = [
5942
+ { id: "mic", keys: "\u2318/Ctrl + D", label: "Mute or unmute your microphone" },
5943
+ { id: "camera", keys: "\u2318/Ctrl + E", label: "Turn your camera on or off" },
5944
+ { id: "hand", keys: "\u2318/Ctrl + Alt + H", label: "Raise or lower your hand" },
5945
+ { id: "chat", keys: "\u2318/Ctrl + Alt + C", label: "Open or close chat" },
5946
+ { id: "people", keys: "\u2318/Ctrl + Alt + P", label: "Open or close people" },
5947
+ { id: "help", keys: "Shift + ?", label: "Show keyboard shortcuts" }
5948
+ ];
5949
+ function useMeetShortcuts(handlers) {
5950
+ const ref = (0, import_react3.useRef)(handlers);
5951
+ ref.current = handlers;
5952
+ (0, import_react3.useEffect)(() => {
5953
+ const target = globalThis.window;
5954
+ if (target === void 0) return void 0;
5955
+ const onKey = (event) => {
5956
+ const element = event.target;
5957
+ const typing = element !== null && (element.isContentEditable || element.tagName === "INPUT" || element.tagName === "TEXTAREA" || element.tagName === "SELECT");
5958
+ const mod = event.metaKey || event.ctrlKey;
5959
+ const key = event.key.toLowerCase();
5960
+ let id = null;
5961
+ if (mod && !event.altKey && !event.shiftKey && key === "d") id = "mic";
5962
+ else if (mod && !event.altKey && !event.shiftKey && key === "e") id = "camera";
5963
+ else if (mod && event.altKey && (event.code === "KeyH" || key === "h")) id = "hand";
5964
+ else if (mod && event.altKey && (event.code === "KeyC" || key === "c")) id = "chat";
5965
+ else if (mod && event.altKey && (event.code === "KeyP" || key === "p")) id = "people";
5966
+ else if (!mod && !typing && event.key === "?") id = "help";
5967
+ if (id === null) return;
5968
+ const handler = ref.current[id];
5969
+ if (handler === void 0) return;
5970
+ event.preventDefault();
5971
+ handler();
5972
+ };
5973
+ target.addEventListener("keydown", onKey);
5974
+ return () => target.removeEventListener("keydown", onKey);
5975
+ }, []);
5976
+ }
4870
5977
  function useMeeting() {
4871
5978
  return useMeetSnapshot()?.meeting ?? null;
4872
5979
  }
@@ -5079,8 +6186,8 @@ function callStateLabel(state, outgoing) {
5079
6186
  return "Call";
5080
6187
  }
5081
6188
  }
5082
- function countLabel(count, singular, plural = `${singular}s`) {
5083
- return `${count} ${count === 1 ? singular : plural}`;
6189
+ function countLabel(count2, singular, plural = `${singular}s`) {
6190
+ return `${count2} ${count2 === 1 ? singular : plural}`;
5084
6191
  }
5085
6192
 
5086
6193
  // src/react/icons.tsx
@@ -5213,6 +6320,60 @@ function WifiLowIcon(props) {
5213
6320
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8.5 16.5a5 5 0 0 1 7 0" })
5214
6321
  ] });
5215
6322
  }
6323
+ function SmileIcon(props) {
6324
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6325
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
6326
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }),
6327
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "9", x2: "9.01", y1: "9", y2: "9" }),
6328
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "15", x2: "15.01", y1: "9", y2: "9" })
6329
+ ] });
6330
+ }
6331
+ function MoreIcon(props) {
6332
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6333
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "1" }),
6334
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "19", cy: "12", r: "1" }),
6335
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "5", cy: "12", r: "1" })
6336
+ ] });
6337
+ }
6338
+ function SearchIcon(props) {
6339
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6340
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "11", cy: "11", r: "8" }),
6341
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m21 21-4.3-4.3" })
6342
+ ] });
6343
+ }
6344
+ function KeyboardIcon(props) {
6345
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6346
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10 8h.01M12 12h.01M14 8h.01M16 12h.01M18 8h.01M6 8h.01M7 16h10M8 12h.01" }),
6347
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { width: "20", height: "16", x: "2", y: "4", rx: "2" })
6348
+ ] });
6349
+ }
6350
+ function SpotlightIcon(props) {
6351
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 2 15 8.5 22 9.3 17 14 18.2 21 12 17.6 5.8 21 7 14 2 9.3 9 8.5Z" }) });
6352
+ }
6353
+ function SignalIcon({ bars, ...props }) {
6354
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6355
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M4 20v-2", opacity: bars >= 1 ? 1 : 0.3 }),
6356
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10 20v-6", opacity: bars >= 2 ? 1 : 0.3 }),
6357
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M16 20V9", opacity: bars >= 3 ? 1 : 0.3 }),
6358
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M22 20V4", opacity: bars >= 3 ? 1 : 0.3 })
6359
+ ] });
6360
+ }
6361
+ function SpeakerIcon(props) {
6362
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6363
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M11 5 6 9H2v6h4l5 4V5Z" }),
6364
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M15.54 8.46a5 5 0 0 1 0 7.07" }),
6365
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19.07 4.93a10 10 0 0 1 0 14.14" })
6366
+ ] });
6367
+ }
6368
+ function FlagIcon(props) {
6369
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6370
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z" }),
6371
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "4", x2: "4", y1: "22", y2: "15" })
6372
+ ] });
6373
+ }
6374
+ function PencilIcon(props) {
6375
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" }) });
6376
+ }
5216
6377
 
5217
6378
  // src/react/components.tsx
5218
6379
  var import_jsx_runtime3 = require("react/jsx-runtime");
@@ -5313,12 +6474,17 @@ function ParticipantTile({
5313
6474
  featured = false
5314
6475
  }) {
5315
6476
  const host = useMeetHost();
5316
- const canModerate2 = useCanModerate(participant);
6477
+ const snapshot = useMeetSnapshot();
6478
+ const queue = useHandQueue();
6479
+ const [menu, setMenu] = (0, import_react4.useState)(false);
5317
6480
  const videoRef = useTrack(
5318
6481
  participant.identity,
5319
6482
  participant.screenSharing ? "screen" : "camera"
5320
6483
  );
5321
6484
  const showVideo = participant.cameraEnabled || participant.screenSharing;
6485
+ const pinnedHere = snapshot?.pinned === participant.identity;
6486
+ const spotlit = snapshot?.spotlight === participant.identity;
6487
+ const handPosition = participant.handRaised ? queue.findIndex((entry) => entry.identity === participant.identity) + 1 : 0;
5322
6488
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
5323
6489
  "div",
5324
6490
  {
@@ -5339,44 +6505,59 @@ function ParticipantTile({
5339
6505
  muted: participant.isLocal
5340
6506
  }
5341
6507
  ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: featured ? 96 : 56 }) }),
6508
+ participant.handRaised ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__tile-hand", title: `Hand raised \xB7 ${ordinal(handPosition)} in line`, children: [
6509
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, { width: 14, height: 14 }),
6510
+ handPosition > 0 ? handPosition : null
6511
+ ] }) : null,
5342
6512
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__tile-bar", children: [
5343
6513
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__tile-name", children: [
6514
+ spotlit ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { title: "Spotlighted for everyone", className: "mx-meet__tile-flag", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpotlightIcon, { width: 12, height: 12 }) }) : null,
5344
6515
  participant.displayName,
5345
6516
  participant.isLocal ? " (you)" : "",
5346
6517
  participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__badge", children: "AI" }) : null,
5347
6518
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RoleBadge, { role: participant.role })
5348
6519
  ] }),
5349
6520
  !participant.micEnabled ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 14, height: 14 }) : null,
5350
- participant.handRaised ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, { width: 14, height: 14 }) : null,
5351
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ConnectionDot, { quality: participant.quality })
6521
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(QualityBars, { quality: participant.quality })
5352
6522
  ] }),
5353
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__tile-actions", children: [
6523
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: menu ? "mx-meet__tile-actions mx-meet__tile-actions--open" : "mx-meet__tile-actions", children: [
5354
6524
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5355
6525
  ControlButton,
5356
6526
  {
5357
- label: featured ? "Unpin" : `Pin ${participant.displayName}`,
5358
- onClick: () => host?.engine.pin(featured ? null : participant.identity),
6527
+ label: pinnedHere ? "Unpin" : `Pin ${participant.displayName} for me`,
6528
+ active: pinnedHere,
6529
+ onClick: () => host?.engine.pin(pinnedHere ? null : participant.identity),
5359
6530
  children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PinIcon, { width: 16, height: 16 })
5360
6531
  }
5361
6532
  ),
5362
- canModerate2 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5363
- ControlButton,
5364
- {
5365
- label: `Mute ${participant.displayName}`,
5366
- onClick: () => {
5367
- void host?.engine.publishHostAction({
5368
- kind: "mute",
5369
- identity: participant.identity
5370
- });
5371
- },
5372
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 16, height: 16 })
5373
- }
5374
- ) : null
6533
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__anchor", children: [
6534
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6535
+ ControlButton,
6536
+ {
6537
+ label: `More for ${participant.displayName}`,
6538
+ active: menu,
6539
+ onClick: () => setMenu(!menu),
6540
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MoreIcon, { width: 16, height: 16 })
6541
+ }
6542
+ ),
6543
+ menu ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PersonMenu, { participant, onClose: () => setMenu(false), placement: "down" }) : null
6544
+ ] })
5375
6545
  ] })
5376
6546
  ]
5377
6547
  }
5378
6548
  );
5379
6549
  }
6550
+ function ordinal(position) {
6551
+ if (position <= 0) return "";
6552
+ const suffix = position % 100 >= 11 && position % 100 <= 13 ? "th" : { 1: "st", 2: "nd", 3: "rd" }[position % 10] ?? "th";
6553
+ return `${position}${suffix}`;
6554
+ }
6555
+ function QualityBars({ quality }) {
6556
+ if (quality === "unknown") return null;
6557
+ const bars = quality === "excellent" ? 3 : quality === "good" ? 2 : quality === "poor" ? 1 : 0;
6558
+ const label = quality === "lost" ? "Connection lost" : quality === "poor" ? "Poor connection" : `Connection ${quality}`;
6559
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: `mx-meet__signal mx-meet__signal--${quality}`, title: label, "aria-label": label, role: "img", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SignalIcon, { bars, width: 14, height: 14 }) });
6560
+ }
5380
6561
  function Stage({ pageSize }) {
5381
6562
  const stage = useStage(pageSize);
5382
6563
  const columns = gridColumns(stage.tiles.length);
@@ -5405,22 +6586,52 @@ function Stage({ pageSize }) {
5405
6586
  ] }) : null
5406
6587
  ] });
5407
6588
  }
6589
+ var MEET_REACTIONS = ["\u{1F496}", "\u{1F44D}", "\u{1F389}", "\u{1F44F}", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F914}", "\u{1F44E}"];
5408
6590
  function ControlBar({ onLeave }) {
5409
6591
  const room = useRoom();
5410
6592
  const recording = useRecording();
5411
6593
  const [panel, setPanel] = (0, import_react4.useState)("none");
6594
+ const [chatTo, setChatTo] = (0, import_react4.useState)(null);
6595
+ const [popover, setPopover] = (0, import_react4.useState)("none");
6596
+ const [shortcuts, setShortcuts] = (0, import_react4.useState)(false);
5412
6597
  const snapshot = useMeetSnapshot();
5413
6598
  const lobby = useLobby();
6599
+ const controls = useHostControls();
6600
+ const queue = useHandQueue();
5414
6601
  const micUnavailable = useLocalMedia().microphone.unavailable;
6602
+ const unread = useUnreadChat(panel === "chat");
6603
+ const toggle = (next) => setPanel(panel === next ? "none" : next);
6604
+ useMeetShortcuts({
6605
+ mic: () => void room.toggleMicrophone(),
6606
+ camera: () => void room.toggleCamera(),
6607
+ hand: () => void room.toggleHand(),
6608
+ chat: () => toggle("chat"),
6609
+ people: () => toggle("people"),
6610
+ help: () => setShortcuts(true)
6611
+ });
6612
+ const hasSpotlight = snapshot?.spotlight !== null && snapshot?.spotlight !== void 0;
5415
6613
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
5416
- panel === "chat" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatPanel, { onClose: () => setPanel("none") }) : null,
5417
- panel === "people" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PeoplePanel, { onClose: () => setPanel("none") }) : null,
6614
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MeetingOverlays, {}),
6615
+ shortcuts ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ShortcutsSheet, { onClose: () => setShortcuts(false) }) : null,
6616
+ panel === "chat" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatPanel, { onClose: () => setPanel("none"), to: chatTo, onTo: setChatTo }) : null,
6617
+ panel === "people" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6618
+ PeoplePanel,
6619
+ {
6620
+ onClose: () => setPanel("none"),
6621
+ onMessage: (identity) => {
6622
+ setChatTo(identity);
6623
+ setPanel("chat");
6624
+ }
6625
+ }
6626
+ ) : null,
5418
6627
  panel === "ai" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AiPanel, { onClose: () => setPanel("none") }) : null,
6628
+ panel === "settings" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SettingsPanel, { onClose: () => setPanel("none") }) : null,
6629
+ controls.notice !== null && panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
5419
6630
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__controls", children: [
5420
6631
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5421
6632
  ControlButton,
5422
6633
  {
5423
- label: micUnavailable !== null ? `Unmute \u2014 ${micUnavailable.remedy}` : room.local?.micEnabled === true ? "Mute" : "Unmute",
6634
+ label: micUnavailable !== null ? `Unmute \u2014 ${micUnavailable.remedy}` : room.local?.micEnabled === true ? "Mute (\u2318D)" : "Unmute (\u2318D)",
5424
6635
  active: room.local?.micEnabled === true,
5425
6636
  onClick: () => void room.toggleMicrophone(),
5426
6637
  children: room.local?.micEnabled === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, {})
@@ -5429,7 +6640,7 @@ function ControlBar({ onLeave }) {
5429
6640
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5430
6641
  ControlButton,
5431
6642
  {
5432
- label: room.local?.cameraEnabled === true ? "Stop video" : "Start video",
6643
+ label: room.local?.cameraEnabled === true ? "Stop video (\u2318E)" : "Start video (\u2318E)",
5433
6644
  active: room.local?.cameraEnabled === true,
5434
6645
  onClick: () => void room.toggleCamera(),
5435
6646
  children: room.local?.cameraEnabled === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CameraIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CameraOffIcon, {})
@@ -5444,34 +6655,155 @@ function ControlBar({ onLeave }) {
5444
6655
  children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ScreenShareIcon, {})
5445
6656
  }
5446
6657
  ),
6658
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__anchor", children: [
6659
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6660
+ ControlButton,
6661
+ {
6662
+ label: "Send a reaction",
6663
+ active: popover === "reactions",
6664
+ onClick: () => setPopover(popover === "reactions" ? "none" : "reactions"),
6665
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SmileIcon, {})
6666
+ }
6667
+ ),
6668
+ popover === "reactions" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: "Reactions", onClose: () => setPopover("none"), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__emoji-bar", children: MEET_REACTIONS.map((emoji) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6669
+ "button",
6670
+ {
6671
+ type: "button",
6672
+ className: "mx-meet__emoji",
6673
+ "aria-label": `React ${emoji}`,
6674
+ onClick: () => void room.react(emoji),
6675
+ children: emoji
6676
+ },
6677
+ emoji
6678
+ )) }) }) : null
6679
+ ] }),
5447
6680
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5448
6681
  ControlButton,
5449
6682
  {
5450
- label: room.local?.handRaised === true ? "Lower hand" : "Raise hand",
6683
+ label: room.local?.handRaised === true ? "Lower hand (\u2318\u2325H)" : "Raise hand (\u2318\u2325H)",
5451
6684
  active: room.local?.handRaised === true,
5452
6685
  onClick: () => void room.toggleHand(),
5453
6686
  children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, {})
5454
6687
  }
5455
6688
  ),
5456
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ControlButton, { label: "Chat", active: panel === "chat", onClick: () => setPanel(panel === "chat" ? "none" : "chat"), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatIcon, {}) }),
6689
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(ControlButton, { label: "Chat (\u2318\u2325C)", active: panel === "chat", onClick: () => toggle("chat"), children: [
6690
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatIcon, {}),
6691
+ unread > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__dot", "aria-hidden": "true" }) : null
6692
+ ] }),
5457
6693
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
5458
6694
  ControlButton,
5459
6695
  {
5460
6696
  label: lobby.waiting.length > 0 ? `People \u2014 ${lobby.waiting.length} waiting` : `People \u2014 ${(snapshot?.participants ?? []).length} in the meeting`,
5461
6697
  active: panel === "people",
5462
- onClick: () => setPanel(panel === "people" ? "none" : "people"),
6698
+ onClick: () => toggle("people"),
5463
6699
  children: [
5464
6700
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PeopleIcon, {}),
5465
- lobby.waiting.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__dot", "aria-hidden": "true" }) : null
6701
+ lobby.waiting.length > 0 || controls.isHost && queue.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__dot", "aria-hidden": "true" }) : null
5466
6702
  ]
5467
6703
  }
5468
6704
  ),
5469
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AiControl, { active: panel === "ai", onToggle: () => setPanel(panel === "ai" ? "none" : "ai") }),
6705
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AiControl, { active: panel === "ai", onToggle: () => toggle("ai") }),
5470
6706
  recording.canStart ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ControlButton, { label: "Start recording", onClick: () => void recording.start(), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RecordIcon, {}) }) : null,
5471
6707
  recording.canStop ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ControlButton, { label: "Stop recording", active: true, onClick: () => void recording.stop(), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RecordIcon, {}) }) : null,
5472
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5473
- ControlButton,
5474
- {
6708
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__anchor", children: [
6709
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6710
+ ControlButton,
6711
+ {
6712
+ label: "More options",
6713
+ active: popover === "more" || panel === "settings",
6714
+ onClick: () => setPopover(popover === "more" ? "none" : "more"),
6715
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MoreIcon, {})
6716
+ }
6717
+ ),
6718
+ popover === "more" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: "More options", onClose: () => setPopover("none"), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu", role: "menu", children: [
6719
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
6720
+ MenuItem,
6721
+ {
6722
+ onSelect: () => {
6723
+ setPopover("none");
6724
+ setPanel("settings");
6725
+ },
6726
+ children: [
6727
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SettingsIcon, { width: 16, height: 16 }),
6728
+ " Audio, video and effects"
6729
+ ]
6730
+ }
6731
+ ),
6732
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
6733
+ MenuItem,
6734
+ {
6735
+ onSelect: () => {
6736
+ setPopover("none");
6737
+ setShortcuts(true);
6738
+ },
6739
+ children: [
6740
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(KeyboardIcon, { width: 16, height: 16 }),
6741
+ " Keyboard shortcuts"
6742
+ ]
6743
+ }
6744
+ ),
6745
+ controls.isHost ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
6746
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__menu-sep", role: "separator" }),
6747
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
6748
+ MenuItem,
6749
+ {
6750
+ onSelect: () => {
6751
+ setPopover("none");
6752
+ void controls.run({ action: "mute_all" });
6753
+ },
6754
+ children: [
6755
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 16, height: 16 }),
6756
+ " Mute everyone"
6757
+ ]
6758
+ }
6759
+ ),
6760
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
6761
+ MenuItem,
6762
+ {
6763
+ onSelect: () => {
6764
+ setPopover("none");
6765
+ void controls.run({ action: "ask_all_unmute" });
6766
+ },
6767
+ children: [
6768
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicIcon, { width: 16, height: 16 }),
6769
+ " Ask everyone to unmute"
6770
+ ]
6771
+ }
6772
+ ),
6773
+ queue.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
6774
+ MenuItem,
6775
+ {
6776
+ onSelect: () => {
6777
+ setPopover("none");
6778
+ void controls.run({ action: "lower_all_hands" });
6779
+ },
6780
+ children: [
6781
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, { width: 16, height: 16 }),
6782
+ " Lower all hands (",
6783
+ queue.length,
6784
+ ")"
6785
+ ]
6786
+ }
6787
+ ) : null,
6788
+ hasSpotlight ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
6789
+ MenuItem,
6790
+ {
6791
+ onSelect: () => {
6792
+ setPopover("none");
6793
+ void controls.run({ action: "spotlight", identity: null });
6794
+ },
6795
+ children: [
6796
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpotlightIcon, { width: 16, height: 16 }),
6797
+ " Remove spotlight"
6798
+ ]
6799
+ }
6800
+ ) : null
6801
+ ] }) : null
6802
+ ] }) }) : null
6803
+ ] }),
6804
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6805
+ ControlButton,
6806
+ {
5475
6807
  label: "Leave",
5476
6808
  danger: true,
5477
6809
  onClick: () => {
@@ -5484,6 +6816,150 @@ function ControlBar({ onLeave }) {
5484
6816
  ] })
5485
6817
  ] });
5486
6818
  }
6819
+ function useUnreadChat(open) {
6820
+ const snapshot = useMeetSnapshot();
6821
+ const chat = snapshot?.chat ?? [];
6822
+ const local = snapshot?.localIdentity ?? null;
6823
+ const [seen, setSeen] = (0, import_react4.useState)(0);
6824
+ (0, import_react4.useEffect)(() => {
6825
+ if (open) setSeen(chat.length);
6826
+ }, [open, chat.length]);
6827
+ const loaded = snapshot?.chatLoaded === true;
6828
+ const [baseline, setBaseline] = (0, import_react4.useState)(null);
6829
+ (0, import_react4.useEffect)(() => {
6830
+ if (loaded && baseline === null) setBaseline(chat.length);
6831
+ }, [loaded, baseline, chat.length]);
6832
+ if (open) return 0;
6833
+ const from = Math.max(seen, baseline ?? chat.length);
6834
+ return chat.slice(from).filter((message) => message.identity !== local).length;
6835
+ }
6836
+ function Popover({
6837
+ label,
6838
+ onClose,
6839
+ children,
6840
+ placement = "up"
6841
+ }) {
6842
+ const ref = (0, import_react4.useRef)(null);
6843
+ const [style, setStyle] = (0, import_react4.useState)({ visibility: "hidden" });
6844
+ (0, import_react4.useLayoutEffect)(() => {
6845
+ const element = ref.current;
6846
+ const anchor = element?.parentElement ?? null;
6847
+ if (element === null || anchor === null) return;
6848
+ const place = () => {
6849
+ const rect = anchor.getBoundingClientRect();
6850
+ const width = element.offsetWidth;
6851
+ const height = element.offsetHeight;
6852
+ const gutter = 8;
6853
+ const left = Math.min(
6854
+ Math.max(gutter, rect.left + rect.width / 2 - width / 2),
6855
+ window.innerWidth - width - gutter
6856
+ );
6857
+ const fitsBelow = rect.bottom + gutter + height <= window.innerHeight;
6858
+ const fitsAbove = rect.top - gutter - height >= 0;
6859
+ const down = placement === "down" ? fitsBelow || !fitsAbove : !fitsAbove && fitsBelow;
6860
+ setStyle(
6861
+ down ? { position: "fixed", left, top: rect.bottom + gutter } : { position: "fixed", left, bottom: window.innerHeight - rect.top + gutter }
6862
+ );
6863
+ };
6864
+ place();
6865
+ window.addEventListener("resize", place);
6866
+ return () => window.removeEventListener("resize", place);
6867
+ }, [placement]);
6868
+ (0, import_react4.useEffect)(() => {
6869
+ const onKey = (event) => {
6870
+ if (event.key === "Escape") onClose();
6871
+ };
6872
+ const onPointer = (event) => {
6873
+ const anchor = ref.current?.parentElement ?? null;
6874
+ if (anchor !== null && !anchor.contains(event.target)) onClose();
6875
+ };
6876
+ window.addEventListener("keydown", onKey);
6877
+ window.addEventListener("pointerdown", onPointer);
6878
+ return () => {
6879
+ window.removeEventListener("keydown", onKey);
6880
+ window.removeEventListener("pointerdown", onPointer);
6881
+ };
6882
+ }, [onClose]);
6883
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref, className: "mx-meet__popover", style, role: "dialog", "aria-label": label, children });
6884
+ }
6885
+ function MenuItem({
6886
+ onSelect,
6887
+ danger = false,
6888
+ children
6889
+ }) {
6890
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6891
+ "button",
6892
+ {
6893
+ type: "button",
6894
+ role: "menuitem",
6895
+ className: danger ? "mx-meet__menu-item mx-meet__menu-item--danger" : "mx-meet__menu-item",
6896
+ onClick: onSelect,
6897
+ children
6898
+ }
6899
+ );
6900
+ }
6901
+ function Toast({ text: text3, failed, onDone }) {
6902
+ (0, import_react4.useEffect)(() => {
6903
+ const timer = setTimeout(onDone, failed ? 7e3 : 4e3);
6904
+ return () => clearTimeout(timer);
6905
+ }, [text3, failed, onDone]);
6906
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: failed ? "mx-meet__toast mx-meet__toast--failed" : "mx-meet__toast", role: "status", children: [
6907
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: text3 }),
6908
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: onDone, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 14, height: 14 }) })
6909
+ ] });
6910
+ }
6911
+ function MeetingOverlays() {
6912
+ const reactions = useReactions();
6913
+ const { notice, dismiss } = useModerationNotice();
6914
+ const room = useRoom();
6915
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
6916
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ReactionOverlay, { reactions }),
6917
+ notice !== null && notice.kind !== "removed" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__notice-bar", role: "alert", children: [
6918
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: notice.text }),
6919
+ notice.kind === "ask-unmute" && room.local?.micEnabled !== true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6920
+ "button",
6921
+ {
6922
+ type: "button",
6923
+ className: "mx-meet__primary mx-meet__primary--small",
6924
+ onClick: () => {
6925
+ dismiss();
6926
+ void room.toggleMicrophone();
6927
+ },
6928
+ children: "Unmute"
6929
+ }
6930
+ ) : null,
6931
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismiss, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 14, height: 14 }) })
6932
+ ] }) : null
6933
+ ] });
6934
+ }
6935
+ function ShortcutsSheet({ onClose }) {
6936
+ (0, import_react4.useEffect)(() => {
6937
+ const onKey = (event) => {
6938
+ if (event.key === "Escape") onClose();
6939
+ };
6940
+ window.addEventListener("keydown", onKey);
6941
+ return () => window.removeEventListener("keydown", onKey);
6942
+ }, [onClose]);
6943
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__sheet-backdrop", onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
6944
+ "div",
6945
+ {
6946
+ className: "mx-meet__sheet",
6947
+ role: "dialog",
6948
+ "aria-label": "Keyboard shortcuts",
6949
+ onClick: (event) => event.stopPropagation(),
6950
+ children: [
6951
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("header", { className: "mx-meet__panel-head", children: [
6952
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { className: "mx-meet__panel-title", children: "Keyboard shortcuts" }),
6953
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ControlButton, { label: "Close", onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 16, height: 16 }) })
6954
+ ] }),
6955
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dl", { className: "mx-meet__shortcuts", children: MEET_SHORTCUTS.map((shortcut) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__shortcut", children: [
6956
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dt", { children: shortcut.label }),
6957
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dd", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("kbd", { className: "mx-meet__kbd", children: shortcut.keys }) })
6958
+ ] }, shortcut.id)) })
6959
+ ]
6960
+ }
6961
+ ) });
6962
+ }
5487
6963
  function AiControl({ active, onToggle }) {
5488
6964
  const snapshot = useMeetSnapshot();
5489
6965
  const meeting = snapshot?.meeting ?? null;
@@ -5503,117 +6979,640 @@ function PanelShell({
5503
6979
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__panel-body", children })
5504
6980
  ] });
5505
6981
  }
5506
- function ChatPanel({ onClose }) {
6982
+ function ChatPanel({
6983
+ onClose,
6984
+ to = null,
6985
+ onTo
6986
+ }) {
5507
6987
  const snapshot = useMeetSnapshot();
5508
6988
  const room = useRoom();
5509
6989
  const [draft, setDraft] = (0, import_react4.useState)("");
6990
+ const [recipient, setRecipient] = (0, import_react4.useState)(to);
6991
+ const listRef = (0, import_react4.useRef)(null);
5510
6992
  const messages = snapshot?.chat ?? [];
5511
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PanelShell, { title: "In-meeting chat", onClose, children: [
5512
- /* @__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." }),
5513
- /* @__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: [
5514
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: message.displayName }),
5515
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: message.text })
5516
- ] }, message.id)) }),
6993
+ const local = snapshot?.localIdentity ?? null;
6994
+ const people = (snapshot?.participants ?? []).filter(
6995
+ (participant) => !participant.isLocal && !participant.isAgent
6996
+ );
6997
+ const recipientName = recipient === null ? null : people.find((p) => p.identity === recipient)?.displayName ?? null;
6998
+ (0, import_react4.useEffect)(() => setRecipient(to), [to]);
6999
+ (0, import_react4.useEffect)(() => {
7000
+ const list = listRef.current;
7001
+ if (list !== null) list.scrollTop = list.scrollHeight;
7002
+ }, [messages.length]);
7003
+ const choose = (next) => {
7004
+ setRecipient(next);
7005
+ onTo?.(next);
7006
+ };
7007
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PanelShell, { title: "Chat", onClose, children: [
7008
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("ul", { className: "mx-meet__chat", ref: listRef, children: [
7009
+ snapshot?.chatLoaded !== true && messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("li", { className: "mx-meet__panel-note", children: "Reading earlier messages\u2026" }) : messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("li", { className: "mx-meet__panel-note", children: "No messages yet. Messages to everyone stay with the meeting, so people who join later see them." }) : null,
7010
+ messages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatLine, { message, local, people }, message.id))
7011
+ ] }),
5517
7012
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
5518
7013
  "form",
5519
7014
  {
5520
- className: "mx-meet__composer",
7015
+ className: "mx-meet__composer mx-meet__composer--chat",
5521
7016
  onSubmit: (event) => {
5522
7017
  event.preventDefault();
5523
7018
  const text3 = draft;
7019
+ if (text3.trim().length === 0) return;
5524
7020
  setDraft("");
5525
- void room.sendChat(text3);
7021
+ if (recipient === null) void room.sendChat(text3).catch(() => void 0);
7022
+ else void room.sendPrivateChat(recipient, text3);
5526
7023
  },
5527
7024
  children: [
5528
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5529
- "input",
7025
+ people.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7026
+ "select",
5530
7027
  {
5531
- className: "mx-meet__input",
5532
- value: draft,
5533
- onChange: (event) => setDraft(event.target.value),
5534
- placeholder: "Message everyone",
5535
- "aria-label": "Message everyone"
7028
+ className: "mx-meet__select mx-meet__select--compact",
7029
+ "aria-label": "Send to",
7030
+ value: recipient ?? "",
7031
+ onChange: (event) => choose(event.target.value === "" ? null : event.target.value),
7032
+ children: [
7033
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "", children: "Everyone" }),
7034
+ people.map((participant) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("option", { value: participant.identity, children: [
7035
+ participant.displayName,
7036
+ " (private)"
7037
+ ] }, participant.identity))
7038
+ ]
5536
7039
  }
5537
- ),
5538
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: "mx-meet__send", children: "Send" })
7040
+ ) : null,
7041
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__composer-row", children: [
7042
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7043
+ "input",
7044
+ {
7045
+ className: "mx-meet__input",
7046
+ value: draft,
7047
+ onChange: (event) => setDraft(event.target.value),
7048
+ placeholder: recipientName === null ? "Message everyone" : `Message ${recipientName} privately`,
7049
+ "aria-label": recipientName === null ? "Message everyone" : `Message ${recipientName} privately`,
7050
+ maxLength: 4e3
7051
+ }
7052
+ ),
7053
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: "mx-meet__send", disabled: draft.trim().length === 0, children: "Send" })
7054
+ ] }),
7055
+ recipientName !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "mx-meet__panel-note", children: [
7056
+ "Only ",
7057
+ recipientName,
7058
+ " sees this. Private messages are not saved."
7059
+ ] }) : null
5539
7060
  ]
5540
7061
  }
5541
7062
  )
5542
7063
  ] });
5543
7064
  }
5544
- function PeoplePanel({ onClose }) {
7065
+ function ChatLine({
7066
+ message,
7067
+ local,
7068
+ people
7069
+ }) {
7070
+ const mine = message.identity === local;
7071
+ const privateTo = message.to ?? null;
7072
+ const counterpart = privateTo === null ? null : mine ? people.find((p) => p.identity === privateTo)?.displayName ?? "them" : "you";
7073
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7074
+ "li",
7075
+ {
7076
+ className: [
7077
+ "mx-meet__chat-line",
7078
+ privateTo !== null ? "mx-meet__chat-line--private" : "",
7079
+ message.failed !== void 0 ? "mx-meet__chat-line--failed" : ""
7080
+ ].filter(Boolean).join(" "),
7081
+ children: [
7082
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__chat-meta", children: [
7083
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: mine ? "You" : message.displayName }),
7084
+ counterpart !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__chat-private", children: [
7085
+ "to ",
7086
+ counterpart,
7087
+ " \xB7 private"
7088
+ ] }) : null,
7089
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("time", { dateTime: message.at, children: clockOf(message.at) })
7090
+ ] }),
7091
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__chat-text", children: message.text }),
7092
+ message.failed !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__chat-failed", children: message.failed }) : privateTo === null && message.saved === false ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__chat-pending", children: message.id.startsWith("pending-") ? "Sending\u2026" : "Not saved \u2014 people who join later will not see this" }) : null
7093
+ ]
7094
+ }
7095
+ );
7096
+ }
7097
+ function PeoplePanel({
7098
+ onClose,
7099
+ onMessage
7100
+ }) {
5545
7101
  const snapshot = useMeetSnapshot();
5546
7102
  const controls = useHostControls();
7103
+ const queue = useHandQueue();
7104
+ const [query, setQuery] = (0, import_react4.useState)("");
7105
+ const [menuFor, setMenuFor] = (0, import_react4.useState)(null);
5547
7106
  const myRole = useRole();
5548
7107
  const local = snapshot?.localIdentity ?? null;
5549
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PanelShell, { title: "People", onClose, children: [
7108
+ const everyone = snapshot?.participants ?? [];
7109
+ const needle = query.trim().toLowerCase();
7110
+ const shown = needle.length === 0 ? everyone : everyone.filter((participant) => participant.displayName.toLowerCase().includes(needle));
7111
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PanelShell, { title: `People (${everyone.length})`, onClose, children: [
5550
7112
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LobbyPanel, {}),
5551
- snapshot?.meeting !== null && snapshot?.meeting !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GuestRecordAccessControl, { meeting: snapshot.meeting }) : null,
5552
- /* @__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: [
5553
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: 32 }),
5554
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__person-name", children: [
5555
- participant.displayName,
5556
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5557
- RoleBadge,
7113
+ queue.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "mx-meet__section", "aria-label": "Raised hands", children: [
7114
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("header", { className: "mx-meet__section-head", children: [
7115
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("h3", { className: "mx-meet__section-title", children: [
7116
+ "Raised hands \xB7 ",
7117
+ queue.length
7118
+ ] }),
7119
+ controls.isHost ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7120
+ "button",
5558
7121
  {
5559
- role: participant.identity === local ? myRole : participant.role
7122
+ type: "button",
7123
+ className: "mx-meet__link",
7124
+ onClick: () => void controls.run({ action: "lower_all_hands" }),
7125
+ children: "Lower all"
5560
7126
  }
5561
- ),
5562
- isNoteTaker(participant) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null
7127
+ ) : null
5563
7128
  ] }),
5564
- !participant.micEnabled && !isNoteTaker(participant) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 14, height: 14 }) : null,
5565
- participant.handRaised ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, { width: 14, height: 14 }) : null,
5566
- controls.canModerate(participant) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PersonHostActions, { participant, controls }) : null
5567
- ] }, participant.identity)) })
7129
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ol", { className: "mx-meet__queue", children: queue.map((participant) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: "mx-meet__person", children: [
7130
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: 28 }),
7131
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__person-name", children: [
7132
+ participant.displayName,
7133
+ participant.identity === local ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__you", children: " (you)" }) : null
7134
+ ] }),
7135
+ controls.isHost || participant.identity === local ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7136
+ "button",
7137
+ {
7138
+ type: "button",
7139
+ className: "mx-meet__link",
7140
+ onClick: () => void controls.run({ action: "lower_hand", identity: participant.identity }),
7141
+ children: "Lower"
7142
+ }
7143
+ ) : null
7144
+ ] }, participant.identity)) })
7145
+ ] }) : null,
7146
+ controls.isHost && everyone.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__bulk", children: [
7147
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { type: "button", className: "mx-meet__chip", onClick: () => void controls.run({ action: "mute_all" }), children: [
7148
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 14, height: 14 }),
7149
+ " Mute all"
7150
+ ] }),
7151
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7152
+ "button",
7153
+ {
7154
+ type: "button",
7155
+ className: "mx-meet__chip",
7156
+ onClick: () => void controls.run({ action: "ask_all_unmute" }),
7157
+ children: [
7158
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicIcon, { width: 14, height: 14 }),
7159
+ " Ask all to unmute"
7160
+ ]
7161
+ }
7162
+ )
7163
+ ] }) : null,
7164
+ everyone.length > 4 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "mx-meet__search", children: [
7165
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SearchIcon, { width: 16, height: 16 }),
7166
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7167
+ "input",
7168
+ {
7169
+ className: "mx-meet__input mx-meet__input--bare",
7170
+ value: query,
7171
+ onChange: (event) => setQuery(event.target.value),
7172
+ placeholder: "Search people",
7173
+ "aria-label": "Search people"
7174
+ }
7175
+ )
7176
+ ] }) : null,
7177
+ controls.notice !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: controls.notice.failed ? "mx-meet__error" : "mx-meet__notice", role: "status", children: controls.notice.text }) : null,
7178
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("ul", { className: "mx-meet__people", children: [
7179
+ shown.map((participant) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: "mx-meet__person", children: [
7180
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: 32 }),
7181
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__person-name", children: [
7182
+ participant.displayName,
7183
+ participant.identity === local ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__you", children: " (you)" }) : null,
7184
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RoleBadge, { role: participant.identity === local ? myRole : participant.role }),
7185
+ isNoteTaker(participant) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null,
7186
+ snapshot?.spotlight === participant.identity ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__badge", children: "Spotlight" }) : null
7187
+ ] }),
7188
+ participant.handRaised ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, { width: 14, height: 14 }) : null,
7189
+ !participant.micEnabled && !isNoteTaker(participant) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 14, height: 14 }) : null,
7190
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(QualityBars, { quality: participant.quality }),
7191
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__anchor", children: [
7192
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7193
+ ControlButton,
7194
+ {
7195
+ label: `More for ${participant.displayName}`,
7196
+ active: menuFor === participant.identity,
7197
+ onClick: () => setMenuFor(menuFor === participant.identity ? null : participant.identity),
7198
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MoreIcon, { width: 16, height: 16 })
7199
+ }
7200
+ ),
7201
+ menuFor === participant.identity ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7202
+ PersonMenu,
7203
+ {
7204
+ participant,
7205
+ placement: "down",
7206
+ onClose: () => setMenuFor(null),
7207
+ ...onMessage !== void 0 ? { onMessage } : {}
7208
+ }
7209
+ ) : null
7210
+ ] })
7211
+ ] }, participant.identity)),
7212
+ shown.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: "mx-meet__panel-note", children: [
7213
+ "Nobody matches \u201C",
7214
+ query,
7215
+ "\u201D."
7216
+ ] }) : null
7217
+ ] }),
7218
+ snapshot?.meeting !== null && snapshot?.meeting !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GuestRecordAccessControl, { meeting: snapshot.meeting }) : null
5568
7219
  ] });
5569
7220
  }
5570
- function PersonHostActions({
7221
+ function PersonMenu({
5571
7222
  participant,
5572
- controls
7223
+ onClose,
7224
+ onMessage,
7225
+ placement = "down"
5573
7226
  }) {
5574
- const [confirming, setConfirming] = (0, import_react4.useState)(false);
5575
- if (confirming) {
5576
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__confirm", role: "group", "aria-label": `Remove ${participant.displayName}?`, children: [
5577
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__confirm-text", children: [
5578
- "Remove ",
5579
- participant.displayName,
5580
- "? They leave the meeting immediately and have to be let back in."
5581
- ] }),
5582
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5583
- "button",
7227
+ const host = useMeetHost();
7228
+ const snapshot = useMeetSnapshot();
7229
+ const controls = useHostControls();
7230
+ const [mode, setMode] = (0, import_react4.useState)("menu");
7231
+ const [text3, setText] = (0, import_react4.useState)("");
7232
+ const self = participant.identity === snapshot?.localIdentity;
7233
+ const moderate = controls.canModerate(participant);
7234
+ const pinned = snapshot?.pinned === participant.identity;
7235
+ const spotlit = snapshot?.spotlight === participant.identity;
7236
+ const hasAccount = participant.userId !== null;
7237
+ const act = (request) => {
7238
+ void controls.run(request).then((result) => {
7239
+ if (result !== null) onClose();
7240
+ });
7241
+ };
7242
+ if (mode === "rename" || mode === "report") {
7243
+ const renaming = mode === "rename";
7244
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: renaming ? "Rename" : "Report", onClose, placement, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7245
+ "form",
7246
+ {
7247
+ className: "mx-meet__menu-form",
7248
+ onSubmit: (event) => {
7249
+ event.preventDefault();
7250
+ if (text3.trim().length === 0) return;
7251
+ act(
7252
+ renaming ? { action: "rename", identity: participant.identity, name: text3.trim() } : { action: "report", identity: participant.identity, reason: text3.trim() }
7253
+ );
7254
+ },
7255
+ children: [
7256
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "mx-meet__field", children: [
7257
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__field-label", children: renaming ? "Name shown in this meeting" : `What did ${participant.displayName} do?` }),
7258
+ renaming ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7259
+ "input",
7260
+ {
7261
+ className: "mx-meet__input",
7262
+ autoFocus: true,
7263
+ maxLength: 80,
7264
+ value: text3,
7265
+ onChange: (event) => setText(event.target.value)
7266
+ }
7267
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7268
+ "textarea",
7269
+ {
7270
+ className: "mx-meet__input",
7271
+ autoFocus: true,
7272
+ rows: 3,
7273
+ maxLength: 1e3,
7274
+ value: text3,
7275
+ onChange: (event) => setText(event.target.value)
7276
+ }
7277
+ )
7278
+ ] }),
7279
+ controls.notice?.failed === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__error", children: controls.notice.text }) : null,
7280
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu-actions", children: [
7281
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Back" }),
7282
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: "mx-meet__send", disabled: text3.trim().length === 0 || controls.busy, children: renaming ? "Rename" : "Report" })
7283
+ ] })
7284
+ ]
7285
+ }
7286
+ ) });
7287
+ }
7288
+ if (mode === "remove" || mode === "transfer") {
7289
+ const removing = mode === "remove";
7290
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: removing ? "Remove" : "Make host", onClose, placement, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu-form", children: [
7291
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__confirm-text", children: removing ? `Remove ${participant.displayName}? They leave immediately and cannot rejoin this meeting.` : `Make ${participant.displayName} the host? They get every host control and own this meeting. You stay on as a co-host.` }),
7292
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu-actions", children: [
7293
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Cancel" }),
7294
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7295
+ "button",
7296
+ {
7297
+ type: "button",
7298
+ className: removing ? "mx-meet__send mx-meet__send--danger" : "mx-meet__send",
7299
+ disabled: controls.busy,
7300
+ onClick: () => act({ action: removing ? "remove" : "transfer_host", identity: participant.identity }),
7301
+ children: removing ? "Remove" : "Make host"
7302
+ }
7303
+ )
7304
+ ] })
7305
+ ] }) });
7306
+ }
7307
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: `Options for ${participant.displayName}`, onClose, placement, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu", role: "menu", children: [
7308
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7309
+ MenuItem,
7310
+ {
7311
+ onSelect: () => {
7312
+ host?.engine.pin(pinned ? null : participant.identity);
7313
+ onClose();
7314
+ },
7315
+ children: [
7316
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PinIcon, { width: 16, height: 16 }),
7317
+ " ",
7318
+ pinned ? "Unpin" : "Pin for me"
7319
+ ]
7320
+ }
7321
+ ),
7322
+ !self && !participant.isAgent && onMessage !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7323
+ MenuItem,
7324
+ {
7325
+ onSelect: () => {
7326
+ onMessage(participant.identity);
7327
+ onClose();
7328
+ },
7329
+ children: [
7330
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatIcon, { width: 16, height: 16 }),
7331
+ " Message privately"
7332
+ ]
7333
+ }
7334
+ ) : null,
7335
+ controls.isHost ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "spotlight", identity: spotlit ? null : participant.identity }), children: [
7336
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpotlightIcon, { width: 16, height: 16 }),
7337
+ " ",
7338
+ spotlit ? "Remove spotlight" : "Spotlight for everyone"
7339
+ ] }) : null,
7340
+ moderate ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
7341
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__menu-sep", role: "separator" }),
7342
+ participant.micEnabled ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "mute", identity: participant.identity }), children: [
7343
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 16, height: 16 }),
7344
+ " Mute"
7345
+ ] }) : !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "ask_unmute", identity: participant.identity }), children: [
7346
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicIcon, { width: 16, height: 16 }),
7347
+ " Ask to unmute"
7348
+ ] }) : null,
7349
+ participant.cameraEnabled ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "stop_video", identity: participant.identity }), children: [
7350
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CameraOffIcon, { width: 16, height: 16 }),
7351
+ " Stop video"
7352
+ ] }) : null,
7353
+ participant.screenSharing ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "stop_screen", identity: participant.identity }), children: [
7354
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ScreenShareIcon, { width: 16, height: 16 }),
7355
+ " Stop screen share"
7356
+ ] }) : null,
7357
+ participant.handRaised ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "lower_hand", identity: participant.identity }), children: [
7358
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, { width: 16, height: 16 }),
7359
+ " Lower hand"
7360
+ ] }) : null,
7361
+ hasAccount && !participant.isAgent && participant.role !== "cohost" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "make_cohost", identity: participant.identity }), children: [
7362
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PeopleIcon, { width: 16, height: 16 }),
7363
+ " Make co-host"
7364
+ ] }) : null,
7365
+ !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7366
+ MenuItem,
5584
7367
  {
5585
- type: "button",
5586
- className: "mx-meet__link mx-meet__link--danger",
5587
- onClick: () => {
5588
- setConfirming(false);
5589
- void controls.remove(participant);
7368
+ onSelect: () => {
7369
+ setText(participant.displayName);
7370
+ setMode("rename");
5590
7371
  },
5591
- children: "Remove"
7372
+ children: [
7373
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PencilIcon, { width: 16, height: 16 }),
7374
+ " Rename"
7375
+ ]
5592
7376
  }
5593
- ),
5594
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setConfirming(false), children: "Keep them" })
5595
- ] });
5596
- }
5597
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
5598
- participant.micEnabled ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5599
- ControlButton,
7377
+ ) : null
7378
+ ] }) : null,
7379
+ controls.isOwner && participant.role === "cohost" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "revoke_cohost", identity: participant.identity }), children: [
7380
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PeopleIcon, { width: 16, height: 16 }),
7381
+ " Remove co-host"
7382
+ ] }) : null,
7383
+ controls.isOwner && !self && hasAccount && !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => setMode("transfer"), children: [
7384
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpotlightIcon, { width: 16, height: 16 }),
7385
+ " Make host"
7386
+ ] }) : null,
7387
+ self ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7388
+ MenuItem,
5600
7389
  {
5601
- label: `Mute ${participant.displayName}`,
5602
- onClick: () => void controls.mute(participant),
5603
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 14, height: 14 })
7390
+ onSelect: () => {
7391
+ setText(participant.displayName);
7392
+ setMode("rename");
7393
+ },
7394
+ children: [
7395
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PencilIcon, { width: 16, height: 16 }),
7396
+ " Rename yourself"
7397
+ ]
7398
+ }
7399
+ ) : null,
7400
+ !self && !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7401
+ MenuItem,
7402
+ {
7403
+ onSelect: () => {
7404
+ setText("");
7405
+ setMode("report");
7406
+ },
7407
+ children: [
7408
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FlagIcon, { width: 16, height: 16 }),
7409
+ " Report"
7410
+ ]
5604
7411
  }
5605
7412
  ) : null,
7413
+ moderate && !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { danger: true, onSelect: () => setMode("remove"), children: [
7414
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 16, height: 16 }),
7415
+ " Remove from meeting"
7416
+ ] }) : null
7417
+ ] }) });
7418
+ }
7419
+ function SettingsPanel({ onClose }) {
7420
+ const host = useRequiredMeetHost();
7421
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PanelShell, { title: "Audio, video and effects", onClose, children: [
5606
7422
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5607
- ControlButton,
7423
+ DeviceSettings,
5608
7424
  {
5609
- label: `Remove ${participant.displayName}`,
5610
- danger: true,
5611
- onClick: () => setConfirming(true),
5612
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 14, height: 14 })
7425
+ onPick: (kind, deviceId2) => {
7426
+ host.devices.select(kind, deviceId2);
7427
+ void host.engine.switchDevice(kind, deviceId2);
7428
+ }
5613
7429
  }
5614
- )
7430
+ ),
7431
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EffectsSettings, {})
7432
+ ] });
7433
+ }
7434
+ function DeviceSettings({
7435
+ onPick
7436
+ }) {
7437
+ const host = useRequiredMeetHost();
7438
+ const devices = useDeviceSnapshot();
7439
+ const microphone = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
7440
+ const speaker = devices?.selected.audiooutput ?? host.devices.resolve("audiooutput");
7441
+ const stream = useMicrophoneStream(microphone);
7442
+ const level = useAudioLevel(stream);
7443
+ const [testing, setTesting] = (0, import_react4.useState)(false);
7444
+ (0, import_react4.useEffect)(() => {
7445
+ void host.devices.refresh();
7446
+ }, [host]);
7447
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "mx-meet__section", "aria-label": "Devices", children: [
7448
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "audioinput", label: "Microphone", onPick }),
7449
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__meter", role: "meter", "aria-label": "Microphone level", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(level * 100), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { width: `${Math.min(100, Math.round(level * 160))}%` } }) }),
7450
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "audiooutput", label: "Speaker", onPick }),
7451
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7452
+ "button",
7453
+ {
7454
+ type: "button",
7455
+ className: "mx-meet__chip",
7456
+ disabled: testing,
7457
+ onClick: () => {
7458
+ setTesting(true);
7459
+ void playTestSound(speaker).finally(() => setTesting(false));
7460
+ },
7461
+ children: [
7462
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpeakerIcon, { width: 14, height: 14 }),
7463
+ " ",
7464
+ testing ? "Playing\u2026" : "Test speaker"
7465
+ ]
7466
+ }
7467
+ ),
7468
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "videoinput", label: "Camera", onPick })
7469
+ ] });
7470
+ }
7471
+ function EffectsSettings({ preview }) {
7472
+ const effects = useMediaEffects();
7473
+ const [error, setError] = (0, import_react4.useState)(null);
7474
+ const fileRef = (0, import_react4.useRef)(null);
7475
+ const supported = effects.backgroundsSupported || preview != null;
7476
+ const apply = (next) => {
7477
+ setError(null);
7478
+ void effects.set(next);
7479
+ if (preview != null) {
7480
+ const merged = { ...effects.effects, ...next };
7481
+ void preview.setEffect(toVideoEffect(merged)).catch(
7482
+ () => setError("This browser could not run the background effect.")
7483
+ );
7484
+ }
7485
+ };
7486
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "mx-meet__section", "aria-label": "Effects", children: [
7487
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "mx-meet__toggle", children: [
7488
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7489
+ "input",
7490
+ {
7491
+ type: "checkbox",
7492
+ checked: effects.effects.noiseSuppression,
7493
+ onChange: (event) => apply({ noiseSuppression: event.target.checked })
7494
+ }
7495
+ ),
7496
+ "Noise suppression",
7497
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__hint", children: "Filters keyboards, fans and background chatter." })
7498
+ ] }),
7499
+ supported ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__field", children: [
7500
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__field-label", children: "Background" }),
7501
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__pills", role: "radiogroup", "aria-label": "Background", children: ["none", "blur", "image"].map((kind) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7502
+ "button",
7503
+ {
7504
+ type: "button",
7505
+ role: "radio",
7506
+ "aria-checked": effects.effects.background === kind,
7507
+ className: effects.effects.background === kind ? "mx-meet__pill mx-meet__pill--on" : "mx-meet__pill",
7508
+ onClick: () => {
7509
+ if (kind === "image") {
7510
+ fileRef.current?.click();
7511
+ return;
7512
+ }
7513
+ apply({ background: kind });
7514
+ },
7515
+ children: kind === "none" ? "None" : kind === "blur" ? "Blur" : "Picture\u2026"
7516
+ },
7517
+ kind
7518
+ )) }),
7519
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7520
+ "input",
7521
+ {
7522
+ ref: fileRef,
7523
+ type: "file",
7524
+ accept: "image/*",
7525
+ hidden: true,
7526
+ onChange: (event) => {
7527
+ const file = event.target.files?.[0];
7528
+ event.target.value = "";
7529
+ if (file === void 0) return;
7530
+ void shrinkImage(file).then((url) => apply({ background: "image", backgroundImage: url })).catch(() => setError("That picture could not be read. Try a JPEG or PNG."));
7531
+ }
7532
+ }
7533
+ ),
7534
+ error !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__error", children: error }) : null
7535
+ ] }) : null
5615
7536
  ] });
5616
7537
  }
7538
+ function toVideoEffect(effects) {
7539
+ if (effects.background === "blur") return { kind: "blur" };
7540
+ if (effects.background === "image" && effects.backgroundImage !== null) {
7541
+ return { kind: "image", imageUrl: effects.backgroundImage };
7542
+ }
7543
+ return { kind: "none" };
7544
+ }
7545
+ async function shrinkImage(file) {
7546
+ const url = URL.createObjectURL(file);
7547
+ try {
7548
+ const image = new Image();
7549
+ image.src = url;
7550
+ await image.decode();
7551
+ const scale = Math.min(1, 1280 / image.naturalWidth);
7552
+ const canvas = document.createElement("canvas");
7553
+ canvas.width = Math.round(image.naturalWidth * scale);
7554
+ canvas.height = Math.round(image.naturalHeight * scale);
7555
+ const context = canvas.getContext("2d");
7556
+ if (context === null) throw new Error("no 2d context");
7557
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
7558
+ return canvas.toDataURL("image/jpeg", 0.85);
7559
+ } finally {
7560
+ URL.revokeObjectURL(url);
7561
+ }
7562
+ }
7563
+ async function playTestSound(sinkId) {
7564
+ const Context = globalThis.AudioContext;
7565
+ if (Context === void 0) return;
7566
+ const context = new Context();
7567
+ const destination = context.createMediaStreamDestination();
7568
+ const gain = context.createGain();
7569
+ gain.gain.value = 0.2;
7570
+ gain.connect(destination);
7571
+ for (const [frequency, start] of [
7572
+ [660, 0],
7573
+ [880, 0.25]
7574
+ ]) {
7575
+ const oscillator = context.createOscillator();
7576
+ oscillator.frequency.value = frequency;
7577
+ oscillator.connect(gain);
7578
+ oscillator.start(context.currentTime + start);
7579
+ oscillator.stop(context.currentTime + start + 0.22);
7580
+ }
7581
+ const audio = new Audio();
7582
+ audio.srcObject = destination.stream;
7583
+ const sinkable = audio;
7584
+ if (sinkId !== null && sinkId !== "default" && sinkable.setSinkId !== void 0) {
7585
+ await sinkable.setSinkId(sinkId).catch(() => void 0);
7586
+ }
7587
+ await audio.play().catch(() => void 0);
7588
+ await new Promise((resolve) => setTimeout(resolve, 700));
7589
+ audio.pause();
7590
+ await context.close();
7591
+ }
7592
+ function useMicrophoneStream(deviceId2) {
7593
+ const [stream, setStream] = (0, import_react4.useState)(null);
7594
+ (0, import_react4.useEffect)(() => {
7595
+ if (deviceId2 === "off") return void 0;
7596
+ let live = true;
7597
+ let opened = null;
7598
+ const media = globalThis.navigator?.mediaDevices;
7599
+ if (media === void 0) return void 0;
7600
+ void media.getUserMedia({ audio: deviceId2 === null ? true : { deviceId: { exact: deviceId2 } } }).then((next) => {
7601
+ if (!live) {
7602
+ next.getTracks().forEach((track) => track.stop());
7603
+ return;
7604
+ }
7605
+ opened = next;
7606
+ setStream(next);
7607
+ }).catch(() => setStream(null));
7608
+ return () => {
7609
+ live = false;
7610
+ opened?.getTracks().forEach((track) => track.stop());
7611
+ setStream(null);
7612
+ };
7613
+ }, [deviceId2]);
7614
+ return stream;
7615
+ }
5617
7616
  function LobbyPanel() {
5618
7617
  const lobby = useLobby();
5619
7618
  if (!lobby.canAdmit || lobby.waiting.length === 0) return null;
@@ -5839,16 +7838,40 @@ function PreJoinBody({
5839
7838
  const room = useRoom();
5840
7839
  const devices = useDeviceSnapshot();
5841
7840
  const recording = useRecording();
7841
+ const effects = useMediaEffects();
7842
+ const { notice } = useModerationNotice();
5842
7843
  const [mode, setMode] = (0, import_react4.useState)("video");
5843
7844
  const [microphone, setMicrophone] = (0, import_react4.useState)(true);
5844
7845
  const [joining, setJoining] = (0, import_react4.useState)(false);
7846
+ const camera = devices?.selected.videoinput ?? host.devices.resolve("videoinput");
7847
+ const mic = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
7848
+ const preview = useCameraPreview(mode === "video" ? camera : void 0, effects.effects);
7849
+ const micStream = useMicrophoneStream(microphone ? mic : "off");
7850
+ const level = useAudioLevel(micStream);
7851
+ const [speakerTesting, setSpeakerTesting] = (0, import_react4.useState)(false);
5845
7852
  (0, import_react4.useEffect)(() => {
5846
7853
  void host.devices.refresh();
5847
7854
  }, [host]);
5848
7855
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__prejoin", children: [
5849
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__preview", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 96 }) }) }),
7856
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__preview", children: [
7857
+ mode === "video" && preview.preview !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7858
+ "video",
7859
+ {
7860
+ className: "mx-meet__video mx-meet__video--mirror",
7861
+ autoPlay: true,
7862
+ playsInline: true,
7863
+ muted: true,
7864
+ ref: (element) => {
7865
+ if (element !== null) preview.preview?.attach(element);
7866
+ }
7867
+ }
7868
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 96 }) }),
7869
+ mode === "video" && preview.error !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__preview-note", children: preview.error }) : null,
7870
+ microphone ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__meter mx-meet__meter--preview", role: "meter", "aria-label": "Microphone level", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(level * 100), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { width: `${Math.min(100, Math.round(level * 160))}%` } }) }) : null
7871
+ ] }),
5850
7872
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__prejoin-panel", children: [
5851
7873
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { className: "mx-meet__prejoin-title", children: "Ready to join?" }),
7874
+ notice?.kind === "removed" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__warning", children: notice.text }) : null,
5852
7875
  devices?.labelsHidden === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "mx-meet__panel-note", children: [
5853
7876
  "Your browser is hiding device names until you allow camera and microphone access.",
5854
7877
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -5865,7 +7888,30 @@ function PreJoinBody({
5865
7888
  ] }) : null,
5866
7889
  devices?.fallbackNotice != null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__warning", children: devices.fallbackNotice }) : null,
5867
7890
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "audioinput", label: "Microphone" }),
7891
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__field-row", children: [
7892
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "audiooutput", label: "Speaker" }),
7893
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7894
+ "button",
7895
+ {
7896
+ type: "button",
7897
+ className: "mx-meet__chip",
7898
+ disabled: speakerTesting,
7899
+ onClick: () => {
7900
+ setSpeakerTesting(true);
7901
+ void playTestSound(devices?.selected.audiooutput ?? host.devices.resolve("audiooutput")).finally(
7902
+ () => setSpeakerTesting(false)
7903
+ );
7904
+ },
7905
+ children: [
7906
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpeakerIcon, { width: 14, height: 14 }),
7907
+ " ",
7908
+ speakerTesting ? "Playing\u2026" : "Test"
7909
+ ]
7910
+ }
7911
+ )
7912
+ ] }),
5868
7913
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "videoinput", label: "Camera" }),
7914
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EffectsSettings, { preview: preview.preview }),
5869
7915
  recording.consentNotice !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__consent", children: recording.consentNotice }) : null,
5870
7916
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__prejoin-actions", children: [
5871
7917
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "mx-meet__toggle", children: [
@@ -5897,6 +7943,7 @@ function PreJoinBody({
5897
7943
  className: "mx-meet__primary",
5898
7944
  onClick: () => {
5899
7945
  setJoining(true);
7946
+ preview.release();
5900
7947
  void room.join({
5901
7948
  roomName,
5902
7949
  mode,
@@ -5916,9 +7963,49 @@ function PreJoinBody({
5916
7963
  ] })
5917
7964
  ] });
5918
7965
  }
7966
+ function useCameraPreview(deviceId2, effects) {
7967
+ const [preview, setPreview] = (0, import_react4.useState)(null);
7968
+ const [error, setError] = (0, import_react4.useState)(null);
7969
+ const current = (0, import_react4.useRef)(null);
7970
+ const initialEffect = (0, import_react4.useRef)(toVideoEffect(effects));
7971
+ initialEffect.current = toVideoEffect(effects);
7972
+ (0, import_react4.useEffect)(() => {
7973
+ if (deviceId2 === void 0) return void 0;
7974
+ let live = true;
7975
+ setError(null);
7976
+ void createCameraPreview(deviceId2, initialEffect.current).then((created) => {
7977
+ if (!live) {
7978
+ created.stop();
7979
+ return;
7980
+ }
7981
+ current.current = created;
7982
+ setPreview(created);
7983
+ }).catch((thrown) => {
7984
+ if (!live) return;
7985
+ const failure = thrown;
7986
+ setError([failure.message, failure.remedy].filter(Boolean).join(". ") || "The camera did not start.");
7987
+ });
7988
+ return () => {
7989
+ live = false;
7990
+ current.current?.stop();
7991
+ current.current = null;
7992
+ setPreview(null);
7993
+ };
7994
+ }, [deviceId2]);
7995
+ return {
7996
+ preview,
7997
+ error,
7998
+ release: () => {
7999
+ current.current?.stop();
8000
+ current.current = null;
8001
+ setPreview(null);
8002
+ }
8003
+ };
8004
+ }
5919
8005
  function DevicePicker({
5920
8006
  kind,
5921
- label
8007
+ label,
8008
+ onPick
5922
8009
  }) {
5923
8010
  const host = useRequiredMeetHost();
5924
8011
  const devices = useDeviceSnapshot();
@@ -5931,7 +8018,10 @@ function DevicePicker({
5931
8018
  {
5932
8019
  className: "mx-meet__select",
5933
8020
  value: devices?.selected[kind] ?? host.devices.resolve(kind) ?? "",
5934
- onChange: (event) => host.devices.select(kind, event.target.value),
8021
+ onChange: (event) => {
8022
+ if (onPick !== void 0) onPick(kind, event.target.value);
8023
+ else host.devices.select(kind, event.target.value);
8024
+ },
5935
8025
  children: list.map((device) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: device.deviceId, children: device.label.length > 0 ? device.label : `${label} (name hidden)` }, device.deviceId))
5936
8026
  }
5937
8027
  )
@@ -6138,7 +8228,7 @@ function ScheduleMeetingBody({
6138
8228
  host
6139
8229
  }) {
6140
8230
  const [title, setTitle] = (0, import_react4.useState)("");
6141
- const [when, setWhen] = (0, import_react4.useState)("");
8231
+ const [when2, setWhen] = (0, import_react4.useState)("");
6142
8232
  const [kind, setKind] = (0, import_react4.useState)("scheduled");
6143
8233
  const [busy, setBusy] = (0, import_react4.useState)(false);
6144
8234
  const [link, setLink] = (0, import_react4.useState)(null);
@@ -6158,7 +8248,7 @@ function ScheduleMeetingBody({
6158
8248
  hostUserId: host.identity.userId,
6159
8249
  title: title.trim(),
6160
8250
  kind,
6161
- scheduledFor: when.length > 0 ? new Date(when).toISOString() : null
8251
+ scheduledFor: when2.length > 0 ? new Date(when2).toISOString() : null
6162
8252
  }).then((meeting) => {
6163
8253
  setLink(meetingLink(appBaseUrl, meeting.slug));
6164
8254
  onScheduled?.(meeting.slug);
@@ -6188,7 +8278,7 @@ function ScheduleMeetingBody({
6188
8278
  {
6189
8279
  className: "mx-meet__input",
6190
8280
  type: "datetime-local",
6191
- value: when,
8281
+ value: when2,
6192
8282
  onChange: (event) => setWhen(event.target.value)
6193
8283
  }
6194
8284
  )
@@ -6768,4 +8858,55 @@ function createMeetingInviteHandler(options) {
6768
8858
  }
6769
8859
  };
6770
8860
  }
8861
+
8862
+ // src/core/management.ts
8863
+ async function refusal(response, operation) {
8864
+ let body = null;
8865
+ try {
8866
+ body = await response.json();
8867
+ } catch {
8868
+ body = null;
8869
+ }
8870
+ const said = readServerRefusal(body);
8871
+ return new MeetError(
8872
+ response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
8873
+ `${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
8874
+ said?.remedy || "The meeting itself is saved \u2014 only the telling failed. Try again; the call is idempotent."
8875
+ );
8876
+ }
8877
+ function count(body, key) {
8878
+ const value = body[key];
8879
+ return typeof value === "number" ? value : 0;
8880
+ }
8881
+ async function announceMeeting(api, meetingId) {
8882
+ const operation = "meet.announceMeeting";
8883
+ const response = await api.request({
8884
+ path: meetingPath(MEET_ROUTES.meetingAnnounce, meetingId),
8885
+ method: "POST",
8886
+ requireAuth: true,
8887
+ operation
8888
+ });
8889
+ if (!response.ok) throw await refusal(response, operation);
8890
+ const body = await response.json();
8891
+ return {
8892
+ invitations: count(body, "invitations"),
8893
+ updates: count(body, "updates"),
8894
+ cancellations: count(body, "cancellations"),
8895
+ alreadyCurrent: count(body, "already_current"),
8896
+ remindersQueued: count(body, "reminders_queued"),
8897
+ remindersCancelled: count(body, "reminders_cancelled")
8898
+ };
8899
+ }
8900
+ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
8901
+ const operation = "meet.pushMeetingToGoogleCalendar";
8902
+ const response = await api.request({
8903
+ path: meetingPath(MEET_ROUTES.meetingGoogleCalendar, meetingId),
8904
+ method: "POST",
8905
+ requireAuth: true,
8906
+ operation,
8907
+ body: { connection_id: target.connectionId, calendar_id: target.calendarId ?? "primary" }
8908
+ });
8909
+ if (!response.ok) throw await refusal(response, operation);
8910
+ return await response.json();
8911
+ }
6771
8912
  //# sourceMappingURL=react.cjs.map