@ai-matrx/meet 0.11.41 → 0.11.43

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
@@ -122,6 +122,7 @@ __export(react_exports, {
122
122
  autoAssign: () => autoAssign,
123
123
  autoRejoinDelayMs: () => autoRejoinDelayMs,
124
124
  availableActivities: () => availableActivities,
125
+ breakoutClosingMs: () => breakoutClosingMs,
125
126
  breakoutRemainingMs: () => breakoutRemainingMs,
126
127
  callInviteAction: () => callInviteAction,
127
128
  callInviteText: () => callInviteText,
@@ -289,6 +290,7 @@ __export(react_exports, {
289
290
  useCanModerate: () => useCanModerate,
290
291
  useCanRecord: () => useCanRecord,
291
292
  useCaptionTranslation: () => useCaptionTranslation,
293
+ useCaptions: () => useCaptions,
292
294
  useCollab: () => useCollab,
293
295
  useCollabPanelRequest: () => useCollabPanelRequest,
294
296
  useConsentNotice: () => useConsentNotice,
@@ -1107,6 +1109,8 @@ var MEET_ROUTES = {
1107
1109
  noteTakerStop: "/api/v1/meet/agent/stop",
1108
1110
  /** In-meeting Q&A. Streams NDJSON; body is `{meeting_id, question}` only. */
1109
1111
  intelligenceAsk: "/api/v1/meet/intelligence/ask",
1112
+ /** Anyone in the meeting (guests with the room pass included) asks for captions for themselves. */
1113
+ noteTakerCaptions: "/api/v1/meet/agent/captions",
1110
1114
  /** A host ends the meeting for everyone. Served by MRI-A6. */
1111
1115
  meetingEnd: "/api/v1/meet/end",
1112
1116
  /**
@@ -1253,7 +1257,8 @@ function parseRoomToken(body, roomName) {
1253
1257
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
1254
1258
  observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
1255
1259
  connectionPolicy: connectionPolicyFrom(payload["connection_policy"]),
1256
- leavePolicy: leavePolicyFrom(payload["leave_policy"])
1260
+ leavePolicy: leavePolicyFrom(payload["leave_policy"]),
1261
+ canReclaim: payload["can_reclaim"] === true
1257
1262
  };
1258
1263
  }
1259
1264
  function createTokenClient(options) {
@@ -1715,6 +1720,42 @@ function createMeetAi(options) {
1715
1720
  },
1716
1721
  "meet.ai.startNoteTaker"
1717
1722
  ),
1723
+ requestCaptions: async ({ meetingId }) => {
1724
+ const roomToken = options.resolveRoomToken?.() ?? null;
1725
+ const operation = "meet.ai.requestCaptions";
1726
+ try {
1727
+ const response = await send({
1728
+ path: MEET_ROUTES.noteTakerCaptions,
1729
+ body: { meeting_id: meetingId },
1730
+ operation,
1731
+ requireAuth: roomToken === null,
1732
+ ...roomToken !== null ? { headers: { [MEETING_PASS_HEADER]: roomToken } } : {}
1733
+ });
1734
+ if (!response.ok) {
1735
+ options.store.patch({ captionsRefusal: await refusalFor(response, operation) });
1736
+ return;
1737
+ }
1738
+ let payload = null;
1739
+ try {
1740
+ payload = await response.json();
1741
+ } catch {
1742
+ payload = null;
1743
+ }
1744
+ const row = typeof payload === "object" && payload !== null ? payload : {};
1745
+ const detail = typeof row["detail"] === "string" ? row["detail"] : "";
1746
+ options.store.patch({
1747
+ captionsRefusal: row["dispatched"] === true ? null : { status: 200, reason: detail, remedy: "" }
1748
+ });
1749
+ } catch (error) {
1750
+ options.store.patch({
1751
+ captionsRefusal: {
1752
+ status: 0,
1753
+ reason: error.message,
1754
+ remedy: error.remedy ?? ""
1755
+ }
1756
+ });
1757
+ }
1758
+ },
1718
1759
  stopNoteTaker: ({ meetingId }) => dispatch(
1719
1760
  MEET_ROUTES.noteTakerStop,
1720
1761
  { meeting_id: meetingId },
@@ -2701,6 +2742,55 @@ function recordingLabel(state) {
2701
2742
  }
2702
2743
  }
2703
2744
 
2745
+ // src/core/live-policy.ts
2746
+ var DEFAULT_LIVE_POLICY = {
2747
+ chatMode: "everyone",
2748
+ avLock: "off",
2749
+ timeLimitMinutes: 0,
2750
+ timeLimitWarningMinutes: 5,
2751
+ recordingConsent: "notice"
2752
+ };
2753
+ function oneOf(value, allowed, fallback) {
2754
+ return typeof value === "string" && allowed.includes(value) ? value : fallback;
2755
+ }
2756
+ function whole(value, fallback) {
2757
+ const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
2758
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
2759
+ }
2760
+ function livePolicyFrom(raw) {
2761
+ const o = typeof raw === "object" && raw !== null ? raw : {};
2762
+ return {
2763
+ chatMode: oneOf(o["chat_mode"], ["everyone", "hosts_only", "off"], DEFAULT_LIVE_POLICY.chatMode),
2764
+ avLock: oneOf(o["attendee_av_lock"], ["off", "mic", "camera", "both"], DEFAULT_LIVE_POLICY.avLock),
2765
+ timeLimitMinutes: whole(o["time_limit_minutes"], 0),
2766
+ timeLimitWarningMinutes: whole(o["time_limit_warning_minutes"], DEFAULT_LIVE_POLICY.timeLimitWarningMinutes),
2767
+ recordingConsent: oneOf(
2768
+ o["recording_consent"],
2769
+ ["notice", "ok_or_leave", "view_only_until_agree"],
2770
+ DEFAULT_LIVE_POLICY.recordingConsent
2771
+ )
2772
+ };
2773
+ }
2774
+ function samePolicy(a, b) {
2775
+ return a.chatMode === b.chatMode && a.avLock === b.avLock && a.timeLimitMinutes === b.timeLimitMinutes && a.timeLimitWarningMinutes === b.timeLimitWarningMinutes && a.recordingConsent === b.recordingConsent;
2776
+ }
2777
+ var MODERATORS = /* @__PURE__ */ new Set(["host", "cohost"]);
2778
+ function isLockedFor(policy, role, kind) {
2779
+ if (MODERATORS.has(role)) return false;
2780
+ return policy.avLock === "both" || (kind === "microphone" ? policy.avLock === "mic" : policy.avLock === "camera");
2781
+ }
2782
+ function consentPending(policy, role, recordingOn, given) {
2783
+ return recordingOn && !given && policy.recordingConsent !== "notice" && !MODERATORS.has(role);
2784
+ }
2785
+ function canChat(policy, role) {
2786
+ if (policy.chatMode === "everyone") return true;
2787
+ return policy.chatMode === "hosts_only" && MODERATORS.has(role);
2788
+ }
2789
+ function minutesLeft(policy, startedAtMs, nowMs) {
2790
+ if (policy.timeLimitMinutes <= 0 || startedAtMs === null) return null;
2791
+ return Math.ceil((startedAtMs + policy.timeLimitMinutes * 6e4 - nowMs) / 6e4);
2792
+ }
2793
+
2704
2794
  // src/core/attendance.ts
2705
2795
  var IDLE_MEDIA_REGION = {
2706
2796
  permission: { microphone: "unknown", camera: "unknown" },
@@ -3827,6 +3917,8 @@ function planFromRow(row) {
3827
3917
  minutes: num2(row["minutes"]) ?? 0,
3828
3918
  openedAt: str(row["opened_at"]),
3829
3919
  endsAt: str(row["ends_at"]),
3920
+ closingAt: str(row["closing_at"]),
3921
+ moveMode: row["move_mode"] === "auto" ? "auto" : "invite",
3830
3922
  closedAt: str(row["closed_at"]),
3831
3923
  broadcasts: (Array.isArray(row["broadcasts"]) ? row["broadcasts"] : []).map(broadcastFrom).filter((b) => b !== null)
3832
3924
  };
@@ -3978,9 +4070,17 @@ function autoAssign(identities, rooms, exclude = []) {
3978
4070
  return out;
3979
4071
  }
3980
4072
  function breakoutRemainingMs(plan, now) {
3981
- if (plan === null || plan.state !== "open" || plan.endsAt === null) return null;
3982
- const end = Date.parse(plan.endsAt);
3983
- return Number.isNaN(end) ? null : Math.max(0, end - now);
4073
+ if (plan === null || plan.state !== "open") return null;
4074
+ const ends = [plan.endsAt, plan.closingAt].map((stamp) => stamp === null ? Number.NaN : Date.parse(stamp)).filter((time) => !Number.isNaN(time));
4075
+ if (ends.length === 0) return null;
4076
+ return Math.max(0, Math.min(...ends) - now);
4077
+ }
4078
+ var BREAKOUT_COUNTDOWN_MS = 3e4;
4079
+ function breakoutClosingMs(plan, now) {
4080
+ if (plan === null || plan.state !== "open") return null;
4081
+ const remaining = breakoutRemainingMs(plan, now);
4082
+ if (remaining === null) return null;
4083
+ return plan.closingAt !== null || remaining <= BREAKOUT_COUNTDOWN_MS ? remaining : null;
3984
4084
  }
3985
4085
  function defaultCaptionLanguage(settings, viewerLanguage) {
3986
4086
  const configured = settings?.captionTranslationLanguage.trim() ?? "";
@@ -4157,6 +4257,11 @@ var EMPTY = {
4157
4257
  presenter: null,
4158
4258
  waitingCount: 0,
4159
4259
  leavePolicy: DEFAULT_LEAVE_POLICY,
4260
+ livePolicy: DEFAULT_LIVE_POLICY,
4261
+ canReclaim: false,
4262
+ recordingConsentGiven: false,
4263
+ sessionStartedAt: null,
4264
+ mutedByHost: false,
4160
4265
  moderationNotice: null,
4161
4266
  actionNotice: null,
4162
4267
  privateChatRequest: null,
@@ -4166,6 +4271,8 @@ var EMPTY = {
4166
4271
  names: {},
4167
4272
  wrapUp: null,
4168
4273
  noteTakerRefusal: null,
4274
+ captionsOn: false,
4275
+ captionsRefusal: null,
4169
4276
  media: IDLE_MEDIA,
4170
4277
  mediaRegion: IDLE_MEDIA_REGION,
4171
4278
  failure: null,
@@ -4370,7 +4477,12 @@ function createMeetStore(initial = {}) {
4370
4477
  if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.reason === next.unavailable?.reason && current.unavailable?.message === next.unavailable?.message) {
4371
4478
  return;
4372
4479
  }
4373
- commit({ ...state, media: { ...state.media, [kind]: next } });
4480
+ commit({
4481
+ ...state,
4482
+ media: { ...state.media, [kind]: next },
4483
+ // The person turned their own microphone on again: the host's mute is over.
4484
+ ...kind === "microphone" && next.enabled && state.mutedByHost ? { mutedByHost: false } : {}
4485
+ });
4374
4486
  },
4375
4487
  reset() {
4376
4488
  const phase = nextPhase(state.phase, { type: "reset", teardown: true }) ?? EMPTY.phase;
@@ -4432,6 +4544,7 @@ function applyDoorRow(store, attendee) {
4432
4544
  function decidedState(state) {
4433
4545
  return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
4434
4546
  }
4547
+ var LIVE_POLICY_POLL_MS = 3e3;
4435
4548
  function createMeetingSession(options) {
4436
4549
  const { manager, repository, store, identity } = options;
4437
4550
  let channel = null;
@@ -4573,6 +4686,46 @@ function createMeetingSession(options) {
4573
4686
  stopWaiting();
4574
4687
  }
4575
4688
  }
4689
+ let policyTimer = null;
4690
+ let policyReading = false;
4691
+ async function readLivePolicy() {
4692
+ const meeting = store.snapshot().meeting;
4693
+ const pass = options.resolvePass?.() ?? null;
4694
+ if (meeting === null || pass === null || policyReading) return;
4695
+ policyReading = true;
4696
+ try {
4697
+ const response = await options.api.request({
4698
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}`,
4699
+ method: "GET",
4700
+ requireAuth: false,
4701
+ operation: "meet.state(policy)",
4702
+ headers: { [MEETING_PASS_HEADER]: pass }
4703
+ });
4704
+ if (!response.ok) return;
4705
+ const read = await response.json();
4706
+ const next = livePolicyFrom(read.live_policy);
4707
+ const started = read.session_started_at ? Date.parse(read.session_started_at) : NaN;
4708
+ const snapshot = store.snapshot();
4709
+ const startedAt = Number.isFinite(started) ? started : snapshot.sessionStartedAt;
4710
+ if (!samePolicy(snapshot.livePolicy, next) || startedAt !== snapshot.sessionStartedAt) {
4711
+ store.patch({ livePolicy: next, sessionStartedAt: startedAt });
4712
+ }
4713
+ } catch {
4714
+ } finally {
4715
+ policyReading = false;
4716
+ }
4717
+ }
4718
+ function syncLivePolicyTimer() {
4719
+ const inCall = store.snapshot().phase === "in_call";
4720
+ if (inCall && policyTimer === null) {
4721
+ void readLivePolicy();
4722
+ policyTimer = setInterval(() => void readLivePolicy(), LIVE_POLICY_POLL_MS);
4723
+ } else if (!inCall && policyTimer !== null) {
4724
+ clearInterval(policyTimer);
4725
+ policyTimer = null;
4726
+ }
4727
+ }
4728
+ const unsubscribePolicy = store.subscribe(syncLivePolicyTimer);
4576
4729
  async function readState2() {
4577
4730
  if (reading !== null) return reading;
4578
4731
  const meeting = store.snapshot().meeting;
@@ -4959,6 +5112,9 @@ function createMeetingSession(options) {
4959
5112
  },
4960
5113
  dispose() {
4961
5114
  stopWaiting();
5115
+ unsubscribePolicy();
5116
+ if (policyTimer !== null) clearInterval(policyTimer);
5117
+ policyTimer = null;
4962
5118
  channel?.close();
4963
5119
  channel = null;
4964
5120
  meetingChannel?.close();
@@ -5360,8 +5516,7 @@ var ROOM_EVENTS = {
5360
5516
  /**
5361
5517
  * A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`:
5362
5518
  * `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the
5363
- * person concerned; `lower-hands {identities: [...] | "*"}` · `role {identity,
5364
- * role}` — to everyone.
5519
+ * person concerned; `role {identity, role}` — to everyone.
5365
5520
  */
5366
5521
  moderation: "matrx.meet.moderation",
5367
5522
  /**
@@ -5549,10 +5704,11 @@ function moderationText(kind, payload) {
5549
5704
  switch (kind) {
5550
5705
  case "muted": {
5551
5706
  const source = text2(payload["source"]) ?? "microphone";
5552
- return source === "microphone" ? `${by} muted you.` : source === "camera" ? `${by} turned off your camera.` : `${by} stopped your screen share.`;
5707
+ const who = payload["by_role"] === "host" || payload["by_role"] === "cohost" ? "the host" : by;
5708
+ return source === "microphone" ? `You've been muted by ${who}.` : source === "camera" ? `Your camera was turned off by ${who}.` : `Your screen share was stopped by ${who}.`;
5553
5709
  }
5554
5710
  case "ask-unmute":
5555
- return `${by} is asking you to unmute.`;
5711
+ return `${by === "The host" ? "The host" : by} asked you to unmute.`;
5556
5712
  case "removed":
5557
5713
  return `${by} removed you from this meeting.`;
5558
5714
  default:
@@ -5578,7 +5734,7 @@ function applyModeration(store, payload, now) {
5578
5734
  grants: { ...snapshot.grants, role },
5579
5735
  moderationNotice: {
5580
5736
  kind: "role",
5581
- text: role === "host" ? "You are now the host." : role === "cohost" ? "You are now a co-host." : "You are no longer a co-host.",
5737
+ text: role === "host" ? "You are now the host." : role === "cohost" ? "You're now a co-host." : role === "presenter" ? "You're now a presenter." : snapshot.grants.role === "presenter" ? "You're now a participant: you can no longer present." : "You're now a participant: you are no longer a co-host.",
5582
5738
  at: now()
5583
5739
  }
5584
5740
  });
@@ -5590,7 +5746,10 @@ function applyModeration(store, payload, now) {
5590
5746
  case "removed": {
5591
5747
  const sentence = moderationText(kind, payload);
5592
5748
  if (sentence === null) return ignored("a moderation event with no sentence");
5593
- store.patch({ moderationNotice: { kind, text: sentence, at: now() } });
5749
+ store.patch({
5750
+ moderationNotice: { kind, text: sentence, at: now() },
5751
+ ...kind === "muted" && (text2(payload["source"]) ?? "microphone") === "microphone" ? { mutedByHost: true } : {}
5752
+ });
5594
5753
  return HANDLED;
5595
5754
  }
5596
5755
  default:
@@ -5629,7 +5788,7 @@ function exactNoteTakerParticipant(meeting, participants) {
5629
5788
  }
5630
5789
  function noteTakerStatus(args) {
5631
5790
  const present = exactNoteTakerParticipant(args.meeting, args.participants) !== null;
5632
- const raw = args.meeting?.metadata[NOTE_TAKER_METADATA_KEY];
5791
+ const raw = args.meeting?.metadata?.[NOTE_TAKER_METADATA_KEY];
5633
5792
  const row = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
5634
5793
  const recorded = readState(row["state"]);
5635
5794
  const detail = typeof row["reason"] === "string" && row["reason"].length > 0 ? row["reason"] : null;
@@ -6106,6 +6265,7 @@ function createRoomEngine(options) {
6106
6265
  lastServerUrl = token.serverUrl;
6107
6266
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
6108
6267
  if (token.leavePolicy !== void 0) store.patch({ leavePolicy: token.leavePolicy });
6268
+ store.patch({ canReclaim: token.canReclaim === true });
6109
6269
  store.patch({
6110
6270
  localIdentity: token.identity,
6111
6271
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -6600,13 +6760,17 @@ var MODERATION_ACTIONS = [
6600
6760
  "ask_unmute",
6601
6761
  "ask_all_unmute",
6602
6762
  "spotlight",
6603
- "present",
6763
+ "sync_presenter",
6604
6764
  "raise_hand",
6605
6765
  "lower_hand",
6606
6766
  "lower_all_hands",
6607
6767
  "make_cohost",
6768
+ "make_presenter",
6769
+ "revoke_presenter",
6770
+ "set_av_lock",
6608
6771
  "revoke_cohost",
6609
6772
  "transfer_host",
6773
+ "reclaim_host",
6610
6774
  "rename",
6611
6775
  "remove",
6612
6776
  "report"
@@ -6632,18 +6796,17 @@ function createInRoomClient(options) {
6632
6796
  function refusal2(response, operation, remedy) {
6633
6797
  return refusalFromResponse(response, operation, remedy);
6634
6798
  }
6635
- let toldPresenting = false;
6799
+ let toldSharing = false;
6636
6800
  let tellChain = Promise.resolve();
6637
6801
  store.subscribe(() => {
6638
6802
  const local = store.snapshot().participants.find((p) => p.isLocal);
6639
6803
  const sharing = local?.screenSharing === true;
6640
- if (sharing === toldPresenting || local === void 0) return;
6641
- toldPresenting = sharing;
6642
- const on = sharing;
6643
- tellChain = tellChain.then(() => client.moderate({ action: "present", on })).then(
6804
+ if (sharing === toldSharing || local === void 0) return;
6805
+ toldSharing = sharing;
6806
+ tellChain = tellChain.then(() => client.moderate({ action: "sync_presenter" })).then(
6644
6807
  () => void 0,
6645
6808
  (error) => {
6646
- console.warn(`[meet] the server did not take presenter=${String(on)}: ${String(error)}`);
6809
+ console.warn(`[meet] the server did not take the screen-share change: ${String(error)}`);
6647
6810
  }
6648
6811
  );
6649
6812
  });
@@ -6660,8 +6823,7 @@ function createInRoomClient(options) {
6660
6823
  action: request.action,
6661
6824
  ...request.identity != null ? { identity: request.identity } : {},
6662
6825
  ...request.name !== void 0 ? { name: request.name } : {},
6663
- ...request.reason !== void 0 ? { reason: request.reason } : {},
6664
- ...request.on !== void 0 ? { on: request.on } : {}
6826
+ ...request.reason !== void 0 ? { reason: request.reason } : {}
6665
6827
  },
6666
6828
  ...hdrs !== void 0 ? { headers: hdrs } : {}
6667
6829
  });
@@ -7646,11 +7808,13 @@ var import_react3 = require("react");
7646
7808
 
7647
7809
  // src/core/observation.ts
7648
7810
  function observedRole(role, standing) {
7649
- if (role === "host" || role === "cohost") return role;
7811
+ if (role === "host" || role === "cohost" || role === "presenter") return role;
7650
7812
  return isLinkFollower(standing) ? "guest" : "participant";
7651
7813
  }
7652
- function observedDevice(track, permission = "unknown") {
7814
+ function observedDevice(track, permission = "unknown", held = null) {
7815
+ if (held === "locked") return "locked";
7653
7816
  if (track.enabled) return "on";
7817
+ if (held === "muted_by_host") return "muted_by_host";
7654
7818
  const reason = deviceUnavailableReason(track, permission);
7655
7819
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
7656
7820
  }
@@ -7687,10 +7851,20 @@ function observe(input) {
7687
7851
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
7688
7852
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
7689
7853
  audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
7690
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
7691
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone) : null
7854
+ camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera, lockedFor(snapshot, "camera", inCall)) : null,
7855
+ microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone, lockedFor(snapshot, "microphone", inCall)) : null
7692
7856
  };
7693
7857
  }
7858
+ function lockedFor(snapshot, kind, inCall) {
7859
+ if (!inCall) return null;
7860
+ const role = effectiveRole(snapshot);
7861
+ if (isLockedFor(snapshot.livePolicy, role, kind)) return "locked";
7862
+ if (snapshot.livePolicy.recordingConsent === "view_only_until_agree" && consentPending(snapshot.livePolicy, role, snapshot.recording === "recording", snapshot.recordingConsentGiven)) {
7863
+ return "locked";
7864
+ }
7865
+ if (kind === "microphone" && snapshot.mutedByHost) return "muted_by_host";
7866
+ return null;
7867
+ }
7694
7868
  function staticObservation(phase, reason = null) {
7695
7869
  return {
7696
7870
  phase,
@@ -8880,6 +9054,45 @@ function useMeetAi() {
8880
9054
  }, [ai, meeting, dispatchNoteTaker])
8881
9055
  };
8882
9056
  }
9057
+ function useCaptions() {
9058
+ const host = useMeetHost();
9059
+ const snapshot = useMeetSnapshot();
9060
+ const ai = useMeetAi();
9061
+ const on = snapshot?.captionsOn === true;
9062
+ const refusal2 = snapshot?.captionsRefusal ?? null;
9063
+ const taker = ai.noteTaker;
9064
+ const meeting = snapshot?.meeting ?? null;
9065
+ const request = (0, import_react4.useCallback)(() => {
9066
+ if (host === null || meeting === null) return;
9067
+ void host.ai.requestCaptions({ meetingId: meeting.id });
9068
+ }, [host, meeting]);
9069
+ const toggle = (0, import_react4.useCallback)(() => {
9070
+ if (host === null) return;
9071
+ const next = !on;
9072
+ host.store.patch({ captionsOn: next, captionsRefusal: null });
9073
+ if (next && taker.state !== "joining" && taker.state !== "transcribing") request();
9074
+ }, [host, on, taker.state, request]);
9075
+ let state = "off";
9076
+ let reason = null;
9077
+ let remedy = null;
9078
+ if (on) {
9079
+ if (taker.state === "transcribing" && taker.present) {
9080
+ state = "live";
9081
+ } else if (taker.state === "joining") {
9082
+ state = "starting";
9083
+ } else if (refusal2 !== null && refusal2.reason.length > 0) {
9084
+ state = "unavailable";
9085
+ reason = refusal2.reason;
9086
+ remedy = refusal2.remedy.length > 0 ? refusal2.remedy : null;
9087
+ } else if (taker.state === "ended" || taker.state === "handoff_pending") {
9088
+ state = "unavailable";
9089
+ reason = noteTakerLabel(taker);
9090
+ } else {
9091
+ state = "starting";
9092
+ }
9093
+ }
9094
+ return { on, state, reason, remedy, toggle };
9095
+ }
8883
9096
  function useEndMeeting() {
8884
9097
  const host = useMeetHost();
8885
9098
  const snapshot = useMeetSnapshot();
@@ -8917,7 +9130,7 @@ function useConsentNotice() {
8917
9130
  const snapshot = useMeetSnapshot();
8918
9131
  const agent = noteTakerParticipant(snapshot?.participants ?? []);
8919
9132
  if (agent === null) return null;
8920
- return `${agent.displayName} is in this meeting: it is transcribing what is said and writing notes everyone here can read.`;
9133
+ return `AI notes are on. ${agent.displayName} is in this meeting, transcribing what is said and writing notes everyone here can read.`;
8921
9134
  }
8922
9135
  function useMediaSupervisor() {
8923
9136
  const host = useMeetHost();
@@ -9381,6 +9594,38 @@ function useGuestRecordAccess(meeting) {
9381
9594
  )
9382
9595
  };
9383
9596
  }
9597
+ function useLivePolicy() {
9598
+ const snapshot = useMeetSnapshot();
9599
+ const role = useRole();
9600
+ const [now, setNow] = (0, import_react4.useState)(() => Date.now());
9601
+ const limited = (snapshot?.livePolicy.timeLimitMinutes ?? 0) > 0;
9602
+ (0, import_react4.useEffect)(() => {
9603
+ if (!limited) return void 0;
9604
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
9605
+ return () => clearInterval(timer);
9606
+ }, [limited]);
9607
+ const policy = snapshot?.livePolicy ?? DEFAULT_LIVE_POLICY;
9608
+ const left = minutesLeft(policy, snapshot?.sessionStartedAt ?? null, now);
9609
+ const host = useMeetHost();
9610
+ const pending = consentPending(policy, role, snapshot?.recording === "recording", snapshot?.recordingConsentGiven === true);
9611
+ const viewOnly = pending && policy.recordingConsent === "view_only_until_agree";
9612
+ const idle = snapshot !== null && snapshot.recording !== "recording" && snapshot.recording !== "starting" && snapshot.recording !== "stopping";
9613
+ const given = snapshot?.recordingConsentGiven === true;
9614
+ const store = host?.store ?? null;
9615
+ (0, import_react4.useEffect)(() => {
9616
+ if (idle && given) store?.patch({ recordingConsentGiven: false });
9617
+ }, [idle, given, store]);
9618
+ return {
9619
+ canChat: canChat(policy, role),
9620
+ chatMode: policy.chatMode,
9621
+ micLocked: isLockedFor(policy, role, "microphone") || viewOnly,
9622
+ cameraLocked: isLockedFor(policy, role, "camera") || viewOnly,
9623
+ consent: pending ? policy.recordingConsent === "ok_or_leave" ? "ok_or_leave" : "view_only_until_agree" : null,
9624
+ agree: () => store?.patch({ recordingConsentGiven: true }),
9625
+ minutesLeft: left,
9626
+ timeWarning: left !== null && left <= policy.timeLimitWarningMinutes
9627
+ };
9628
+ }
9384
9629
 
9385
9630
  // src/react/skin-root.tsx
9386
9631
  var import_react12 = require("react");
@@ -10155,6 +10400,18 @@ function SparklesIcon(props) {
10155
10400
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M19 15l.9 2.1 2.1.9-2.1.9-.9 2.1-.9-2.1-2.1-.9 2.1-.9L19 15Z" })
10156
10401
  ] });
10157
10402
  }
10403
+ function CaptionsIcon(props) {
10404
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Icon, { ...props, children: [
10405
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
10406
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M10 10.5a2 2 0 1 0 0 3M16 10.5a2 2 0 1 0 0 3" })
10407
+ ] });
10408
+ }
10409
+ function NotesIcon(props) {
10410
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Icon, { ...props, children: [
10411
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M4 20l1-4L16.5 4.5a2.1 2.1 0 0 1 3 3L8 19l-4 1Z" }),
10412
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M14 7l3 3" })
10413
+ ] });
10414
+ }
10158
10415
  function LockIcon(props) {
10159
10416
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Icon, { ...props, children: [
10160
10417
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "4", y: "10", width: "16", height: "11", rx: "2" }),
@@ -11061,7 +11318,7 @@ function BreakoutRun({ plan }) {
11061
11318
  type: "button",
11062
11319
  className: "mx-meet__primary mx-meet__primary--small",
11063
11320
  onClick: () => void act({ action: "breakout_close" }).then(show),
11064
- children: "Bring everyone back"
11321
+ children: "Close rooms"
11065
11322
  }
11066
11323
  ) : null
11067
11324
  ] });
@@ -11079,9 +11336,14 @@ function BreakoutBanner() {
11079
11336
  const { collab, act, moveTo, localIdentity, breakoutRoomId } = useCollab();
11080
11337
  const plan = collab?.breakout ?? null;
11081
11338
  const open = plan?.state === "open" ? plan : null;
11082
- const now = useNow(open?.endsAt != null);
11339
+ const now = useNow(open?.endsAt != null || open?.closingAt != null);
11083
11340
  const remaining = breakoutRemainingMs(open, now);
11341
+ const closing = breakoutClosingMs(open, now);
11084
11342
  const assigned = assignedRoom(open, localIdentity);
11343
+ const [later, setLater] = (0, import_react9.useState)(null);
11344
+ const [autoSince, setAutoSince] = (0, import_react9.useState)(null);
11345
+ const AUTO_MOVE_MS = 1e4;
11346
+ const autoLeft = open?.moveMode === "auto" && assigned !== null && autoSince?.id === open.id && later !== open.id ? Math.max(0, AUTO_MOVE_MS - (now - autoSince.at)) : null;
11085
11347
  const [dismissedBroadcast, setDismissedBroadcast] = (0, import_react9.useState)(null);
11086
11348
  const [failure, setFailure] = (0, import_react9.useState)(null);
11087
11349
  const expireAsked = (0, import_react9.useRef)(null);
@@ -11094,6 +11356,19 @@ function BreakoutBanner() {
11094
11356
  if ((here ?? null) === request.roomId) return;
11095
11357
  void moveTo(request.roomId).then((result) => setFailure(result.ok ? null : result.message));
11096
11358
  }, [moveAt, host, moveTo]);
11359
+ const joinAssigned = async () => {
11360
+ if (assigned === null) return;
11361
+ const said = await act({ action: "breakout_join", roomId: assigned.id });
11362
+ const result = said.ok ? await moveTo(assigned.id) : said;
11363
+ setFailure(result.ok ? null : result.message);
11364
+ };
11365
+ (0, import_react9.useEffect)(() => {
11366
+ if (open === null || open.moveMode !== "auto" || assigned === null || breakoutRoomId !== null) return;
11367
+ if (autoSince?.id !== open.id) setAutoSince({ id: open.id, at: Date.now() });
11368
+ }, [open, assigned, breakoutRoomId, autoSince]);
11369
+ (0, import_react9.useEffect)(() => {
11370
+ if (autoLeft === 0) void joinAssigned();
11371
+ }, [autoLeft]);
11097
11372
  (0, import_react9.useEffect)(() => {
11098
11373
  if (open === null || remaining === null || remaining > 0) return;
11099
11374
  const asked = expireAsked.current;
@@ -11107,27 +11382,25 @@ function BreakoutBanner() {
11107
11382
  if (open === null && failure === null) return null;
11108
11383
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "mx-meet__breakout-banner", role: "status", children: [
11109
11384
  failure !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "mx-meet__collab-error", children: failure }) : null,
11385
+ closing !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { role: "status", "data-meet-notice": "breakout-closing", children: [
11386
+ "Rooms closing in ",
11387
+ Math.ceil(closing / 1e3),
11388
+ room !== null ? " \u2014 you return to the main meeting" : ""
11389
+ ] }) : null,
11110
11390
  room !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
11111
11391
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: room.name }),
11112
- remaining !== null ? ` \xB7 ${clock(remaining)} left` : "",
11392
+ remaining !== null && closing === null ? ` \xB7 ${clock(remaining)} left` : "",
11113
11393
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mx-meet__chip", onClick: () => void moveTo(null), children: "Return to main room" })
11394
+ ] }) : assigned !== null && later !== open?.id ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
11395
+ autoLeft !== null ? `You're being moved to ${assigned.name} in ${Math.ceil(autoLeft / 1e3)} s` : `You're invited to ${assigned.name}`,
11396
+ remaining !== null && closing === null ? ` \xB7 ${clock(remaining)} left` : "",
11397
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void joinAssigned(), children: "Join room" }),
11398
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mx-meet__chip", onClick: () => setLater(open?.id ?? null), children: "Later" })
11114
11399
  ] }) : assigned !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
11115
- "You are assigned to ",
11116
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: assigned.name }),
11117
- remaining !== null ? ` \xB7 ${clock(remaining)} left` : "",
11118
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
11119
- "button",
11120
- {
11121
- type: "button",
11122
- className: "mx-meet__primary mx-meet__primary--small",
11123
- onClick: () => void act({ action: "breakout_join", roomId: assigned.id }).then(async (said) => {
11124
- const result = said.ok ? await moveTo(assigned.id) : said;
11125
- setFailure(result.ok ? null : result.message);
11126
- }),
11127
- children: "Join"
11128
- }
11129
- )
11130
- ] }) : open?.allowChoose ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: "Breakout rooms are open \u2014 pick one under Activities." }) : null,
11400
+ assigned.name,
11401
+ " is open",
11402
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mx-meet__chip", onClick: () => void joinAssigned(), children: "Join room" })
11403
+ ] }) : open?.allowChoose ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: "Rooms are open \u2014 pick one under Activities." }) : null,
11131
11404
  showBroadcast ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "mx-meet__breakout-broadcast", children: [
11132
11405
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("strong", { children: [
11133
11406
  broadcast.by,
@@ -11175,6 +11448,33 @@ function CaptionLanguageSettings() {
11175
11448
  collab?.captionTranslationError != null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "mx-meet__panel-note mx-meet__collab-error", children: collab.captionTranslationError }) : null
11176
11449
  ] });
11177
11450
  }
11451
+ function CaptionsControl() {
11452
+ const captions = useCaptions();
11453
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
11454
+ ControlButton,
11455
+ {
11456
+ label: captions.on ? "Turn off captions" : "Turn on captions",
11457
+ active: captions.on,
11458
+ pressed: captions.on,
11459
+ onClick: captions.toggle,
11460
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CaptionsIcon, {})
11461
+ }
11462
+ );
11463
+ }
11464
+ function TakeNotesControl() {
11465
+ const ai = useMeetAi();
11466
+ if (!ai.canControlNoteTaker) return null;
11467
+ const running = ai.noteTaker.state === "joining" || ai.noteTaker.state === "transcribing";
11468
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
11469
+ ControlButton,
11470
+ {
11471
+ label: running ? "Stop taking notes" : "Take notes for me",
11472
+ active: running,
11473
+ onClick: () => void (running ? ai.stopNoteTaker() : ai.startNoteTaker()),
11474
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(NotesIcon, {})
11475
+ }
11476
+ );
11477
+ }
11178
11478
 
11179
11479
  // src/skins/meet/components.tsx
11180
11480
  var import_jsx_runtime6 = require("react/jsx-runtime");
@@ -11225,6 +11525,7 @@ function ControlButton({
11225
11525
  label,
11226
11526
  active = false,
11227
11527
  danger = false,
11528
+ pressed,
11228
11529
  onClick,
11229
11530
  children
11230
11531
  }) {
@@ -11239,26 +11540,49 @@ function ControlButton({
11239
11540
  ].filter(Boolean).join(" "),
11240
11541
  "aria-label": label,
11241
11542
  title: label,
11543
+ ...pressed !== void 0 ? { "aria-pressed": pressed } : {},
11242
11544
  onClick,
11243
11545
  children
11244
11546
  }
11245
11547
  );
11246
11548
  }
11247
11549
  function RecordingIndicator() {
11248
- const { state, label } = useRecording();
11249
- if (label === null) return null;
11250
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
11251
- "span",
11252
- {
11253
- className: `mx-meet__recording mx-meet__recording--${state}`,
11254
- role: "status",
11255
- "aria-live": "polite",
11256
- children: [
11257
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RecordIcon, { width: 10, height: 10 }),
11258
- label
11259
- ]
11260
- }
11261
- );
11550
+ const { state, label, canStart } = useRecording();
11551
+ const snapshot = useMeetSnapshot();
11552
+ const previous = (0, import_react10.useRef)(state);
11553
+ const [stoppedAt, setStoppedAt] = (0, import_react10.useState)(null);
11554
+ (0, import_react10.useEffect)(() => {
11555
+ const was = previous.current;
11556
+ previous.current = state;
11557
+ if ((was === "recording" || was === "starting") && state !== "recording" && state !== "starting") {
11558
+ setStoppedAt(Date.now());
11559
+ const timer = setTimeout(() => setStoppedAt(null), 1e4);
11560
+ return () => clearTimeout(timer);
11561
+ }
11562
+ return void 0;
11563
+ }, [state]);
11564
+ const meetingId = snapshot?.meeting?.id ?? null;
11565
+ if (label === null && stoppedAt === null) return null;
11566
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11567
+ label !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
11568
+ "span",
11569
+ {
11570
+ className: `mx-meet__recording mx-meet__recording--${state}`,
11571
+ role: "status",
11572
+ "aria-live": "polite",
11573
+ children: [
11574
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RecordIcon, { width: 10, height: 10 }),
11575
+ label
11576
+ ]
11577
+ }
11578
+ ) : null,
11579
+ stoppedAt !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__recording", role: "status", "data-meet-notice": "recording-stopped", children: "Recording stopped" }) : null,
11580
+ state === "processing" ? (
11581
+ // rec-processing: the host is told it is being made ready (the indicator says Processing…).
11582
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__recording", role: "status", "data-meet-notice": "recording-processing", children: "Your recording is being processed. A link appears here when it is ready." })
11583
+ ) : null,
11584
+ state === "available" && canStart && meetingId !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("a", { className: "mx-meet__link", href: `/meetings/${meetingId}`, target: "_blank", rel: "noreferrer", children: "View recording" }) : null
11585
+ ] });
11262
11586
  }
11263
11587
  function RoleBadge({
11264
11588
  role,
@@ -11285,6 +11609,7 @@ function ParticipantTile({
11285
11609
  const showVideo = participant.cameraEnabled || participant.screenSharing;
11286
11610
  const pinnedHere = snapshot?.pinned === participant.identity;
11287
11611
  const spotlit = snapshot?.spotlight === participant.identity;
11612
+ const presenting = snapshot?.presenter === participant.identity;
11288
11613
  const handPosition = participant.handRaised ? queue.findIndex((entry) => entry.identity === participant.identity) + 1 : 0;
11289
11614
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
11290
11615
  "div",
@@ -11296,6 +11621,7 @@ function ParticipantTile({
11296
11621
  ].filter(Boolean).join(" "),
11297
11622
  "data-identity": participant.identity,
11298
11623
  ...participantAttributes(participant),
11624
+ ...presenting ? { "data-meet-participant-presenting": "true" } : {},
11299
11625
  children: [
11300
11626
  showVideo ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11301
11627
  "video",
@@ -11307,6 +11633,7 @@ function ParticipantTile({
11307
11633
  muted: participant.isLocal
11308
11634
  }
11309
11635
  ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: featured ? 96 : 56 }) }),
11636
+ presenting ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__tile-presenting", role: "status", children: participant.isLocal ? "You are presenting" : `${participant.displayName} is presenting` }) : null,
11310
11637
  participant.handRaised ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "mx-meet__tile-hand", title: `Hand raised \xB7 ${ordinal(handPosition)} in line`, children: [
11311
11638
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(HandIcon, { width: 14, height: 14 }),
11312
11639
  handPosition > 0 ? handPosition : null
@@ -11407,6 +11734,7 @@ function ControlBar({ onLeave }) {
11407
11734
  const controls = useHostControls();
11408
11735
  const queue = useHandQueue();
11409
11736
  const micUnavailable = useLocalMedia().microphone.unavailable;
11737
+ const live = useLivePolicy();
11410
11738
  const unread = useUnreadChat(panel === "chat");
11411
11739
  const [whiteboard, setWhiteboard] = (0, import_react10.useState)(false);
11412
11740
  const app = useMeetAppPanel();
@@ -11474,7 +11802,7 @@ function ControlBar({ onLeave }) {
11474
11802
  panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(KnockNotice, { onView: () => setPanel("people") }) : null,
11475
11803
  controls.notice !== null && panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
11476
11804
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__controls", children: [
11477
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11805
+ live.micLocked ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11478
11806
  ControlButton,
11479
11807
  {
11480
11808
  label: micUnavailable !== null ? `Unmute \u2014 ${micUnavailable.remedy}` : room.local?.micEnabled === true ? "Mute (\u2318D)" : "Unmute (\u2318D)",
@@ -11483,7 +11811,7 @@ function ControlBar({ onLeave }) {
11483
11811
  children: room.local?.micEnabled === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, {})
11484
11812
  }
11485
11813
  ),
11486
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11814
+ live.cameraLocked ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11487
11815
  ControlButton,
11488
11816
  {
11489
11817
  label: room.local?.cameraEnabled === true ? "Stop video (\u2318E)" : "Start video (\u2318E)",
@@ -11492,6 +11820,7 @@ function ControlBar({ onLeave }) {
11492
11820
  children: room.local?.cameraEnabled === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CameraIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CameraOffIcon, {})
11493
11821
  }
11494
11822
  ),
11823
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CaptionsControl, {}),
11495
11824
  room.canShareScreen || room.local?.screenSharing === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11496
11825
  ControlButton,
11497
11826
  {
@@ -11556,6 +11885,7 @@ function ControlBar({ onLeave }) {
11556
11885
  onOpen: openActivity
11557
11886
  }
11558
11887
  ),
11888
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(TakeNotesControl, {}),
11559
11889
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AiControl, { active: panel === "ai", onToggle: () => toggle("ai") }),
11560
11890
  recording.canStart ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ControlButton, { label: "Start recording", onClick: () => void recording.start(), children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RecordIcon, {}) }) : null,
11561
11891
  recording.canStop ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ControlButton, { label: "Stop recording", active: true, onClick: () => void recording.stop(), children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RecordIcon, {}) }) : null,
@@ -11611,6 +11941,23 @@ function ControlBar({ onLeave }) {
11611
11941
  ]
11612
11942
  }
11613
11943
  ),
11944
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
11945
+ MenuItem,
11946
+ {
11947
+ onSelect: () => {
11948
+ setPopover("none");
11949
+ void controls.run({
11950
+ action: "set_av_lock",
11951
+ value: snapshot?.livePolicy.avLock === "both" ? "off" : "both"
11952
+ });
11953
+ },
11954
+ children: [
11955
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, { width: 16, height: 16 }),
11956
+ " ",
11957
+ snapshot?.livePolicy.avLock === "both" ? "Turn on attendees' microphone and camera" : "Turn off attendees' microphone and camera"
11958
+ ]
11959
+ }
11960
+ ),
11614
11961
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
11615
11962
  MenuItem,
11616
11963
  {
@@ -11938,9 +12285,45 @@ function MeetingOverlays() {
11938
12285
  const reactions = useReactions();
11939
12286
  const { notice, dismiss } = useModerationNotice();
11940
12287
  const room = useRoom();
12288
+ const live = useLivePolicy();
12289
+ const snapshot = useMeetSnapshot();
12290
+ const controls = useHostControls();
12291
+ const isHostNow = useRole() === "host";
11941
12292
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11942
12293
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ReactionOverlay, { reactions }),
11943
12294
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MediaNotices, {}),
12295
+ snapshot !== null && snapshot.phase === "in_call" && snapshot.participants.length >= 2 && !snapshot.participants.some((p) => p.role === "host" || p.role === "cohost") ? (
12296
+ // meeting-without-host: the call goes on; host-only controls are simply not on screen.
12297
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "host-absent", children: "The host has left the meeting. Host controls are not available until a host is back." })
12298
+ ) : null,
12299
+ live.consent !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__sheet-backdrop", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__sheet", role: "alertdialog", "aria-label": "This meeting is being recorded", "data-meet-notice": "recording-consent", children: [
12300
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h2", { className: "mx-meet__panel-title", children: "This meeting is being recorded" }),
12301
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__confirm-text", children: live.consent === "ok_or_leave" ? "By staying you agree to be recorded. You can leave now if you do not agree." : "Until you agree to the recording you can watch and listen, but not use your microphone or camera." }),
12302
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__menu-actions", children: [
12303
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__send", onClick: live.agree, children: live.consent === "ok_or_leave" ? "OK" : "I agree" }),
12304
+ live.consent === "ok_or_leave" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => void room.leave(), children: "Leave meeting" }) : null
12305
+ ] })
12306
+ ] }) }) : null,
12307
+ snapshot?.canReclaim === true && !isHostNow ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__notice-bar", role: "status", "data-meet-notice": "host-reclaim", children: [
12308
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "You started this meeting. Take back the host?" }),
12309
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12310
+ "button",
12311
+ {
12312
+ type: "button",
12313
+ className: "mx-meet__primary mx-meet__primary--small",
12314
+ onClick: () => void controls.run({ action: "reclaim_host" }),
12315
+ children: "Reclaim host"
12316
+ }
12317
+ )
12318
+ ] }) : null,
12319
+ live.micLocked || live.cameraLocked ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "mic-locked", children: live.micLocked && live.cameraLocked ? "Your microphone and camera are turned off by the organizer." : live.micLocked ? "Your microphone is turned off by the organizer." : "Your camera is turned off by the organizer." }) : null,
12320
+ live.timeWarning && live.minutesLeft !== null && live.minutesLeft > 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "time-warning", children: [
12321
+ "This meeting will end in ",
12322
+ live.minutesLeft,
12323
+ " ",
12324
+ live.minutesLeft === 1 ? "minute" : "minutes",
12325
+ " (time limit)."
12326
+ ] }) : null,
11944
12327
  notice !== null && notice.kind !== "removed" ? (
11945
12328
  // A host's request ("the host is asking you to unmute"), not an error:
11946
12329
  // an assertive status, so it is announced at once without being filed
@@ -12031,6 +12414,7 @@ function ChatPanel({
12031
12414
  }) {
12032
12415
  const snapshot = useMeetSnapshot();
12033
12416
  const room = useRoom();
12417
+ const live = useLivePolicy();
12034
12418
  const [draft, setDraft] = (0, import_react10.useState)("");
12035
12419
  const [recipient, setRecipient] = (0, import_react10.useState)(to);
12036
12420
  const listRef = (0, import_react10.useRef)(null);
@@ -12055,7 +12439,7 @@ function ChatPanel({
12055
12439
  snapshot?.chatLoaded !== true && messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("li", { className: "mx-meet__panel-note", children: "Reading earlier messages\u2026" }) : messages.length === 0 && snapshot?.chatHistoryError == null ? /* @__PURE__ */ (0, import_jsx_runtime6.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,
12056
12440
  messages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ChatLine, { message, local, people }, message.id))
12057
12441
  ] }),
12058
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
12442
+ !live.canChat ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__panel-note", role: "status", "data-meet-notice": "chat-restricted", children: live.chatMode === "off" ? "Chat is turned off for this meeting." : "Chat is limited to the host and co-hosts in this meeting." }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
12059
12443
  "form",
12060
12444
  {
12061
12445
  className: "mx-meet__composer mx-meet__composer--chat",
@@ -12238,7 +12622,8 @@ function PeoplePanel({
12238
12622
  }
12239
12623
  ),
12240
12624
  isNoteTaker(participant) ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null,
12241
- snapshot?.spotlight === participant.identity ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Spotlight" }) : null
12625
+ snapshot?.spotlight === participant.identity ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Spotlight" }) : null,
12626
+ snapshot?.presenter === participant.identity ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", "data-meet-presenting": "true", children: "Presenting" }) : null
12242
12627
  ] }),
12243
12628
  participant.handRaised ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(HandIcon, { width: 14, height: 14 }) : null,
12244
12629
  !participant.micEnabled && !isNoteTaker(participant) ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, { width: 14, height: 14 }) : null,
@@ -12426,6 +12811,13 @@ function PersonMenu({
12426
12811
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PeopleIcon, { width: 16, height: 16 }),
12427
12812
  " Make co-host"
12428
12813
  ] }) : null,
12814
+ !participant.isAgent && participant.role !== "cohost" && participant.role !== "host" ? participant.role === "presenter" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(MenuItem, { onSelect: () => act({ action: "revoke_presenter", identity: participant.identity }), children: [
12815
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ScreenShareIcon, { width: 16, height: 16 }),
12816
+ " Remove presenter"
12817
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(MenuItem, { onSelect: () => act({ action: "make_presenter", identity: participant.identity }), children: [
12818
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ScreenShareIcon, { width: 16, height: 16 }),
12819
+ " Make presenter"
12820
+ ] }) : null,
12429
12821
  !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
12430
12822
  MenuItem,
12431
12823
  {
@@ -12931,8 +13323,19 @@ function Captions({ lines = 3 }) {
12931
13323
  const ai = useMeetAi();
12932
13324
  const visible = ai.transcript.slice(-lines);
12933
13325
  const translation = useCaptionTranslation(visible);
12934
- if (visible.length === 0) return null;
12935
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", children: [
13326
+ const captions = useCaptions();
13327
+ if (!captions.on) return null;
13328
+ if (captions.state === "unavailable") {
13329
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__captions", role: "status", "data-meet-notice": "captions-unavailable", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__caption", children: [
13330
+ "Captions are unavailable. ",
13331
+ captions.reason,
13332
+ captions.remedy !== null ? ` ${captions.remedy}` : ""
13333
+ ] }) });
13334
+ }
13335
+ if (visible.length === 0) {
13336
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", "data-meet-caption": captions.state, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__caption mx-meet__caption--interim", children: captions.state === "starting" ? "Starting captions. They appear as people speak." : "Captions are on. They appear as people speak." }) });
13337
+ }
13338
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", "data-meet-caption": "live", children: [
12936
13339
  visible.map((line) => {
12937
13340
  const translated = translation.language === null ? void 0 : translation.translations[line.id];
12938
13341
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
@@ -12955,7 +13358,7 @@ function Captions({ lines = 3 }) {
12955
13358
  function ConsentNotice() {
12956
13359
  const notice = useConsentNotice();
12957
13360
  if (notice === null) return null;
12958
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__consent", role: "status", children: notice });
13361
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__consent", role: "status", "data-meet-notice": "notes-started", children: notice });
12959
13362
  }
12960
13363
  function AttendanceNotice() {
12961
13364
  const media = useLocalMedia();