@ai-matrx/meet 0.11.42 → 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();
@@ -5548,10 +5704,11 @@ function moderationText(kind, payload) {
5548
5704
  switch (kind) {
5549
5705
  case "muted": {
5550
5706
  const source = text2(payload["source"]) ?? "microphone";
5551
- 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}.`;
5552
5709
  }
5553
5710
  case "ask-unmute":
5554
- return `${by} is asking you to unmute.`;
5711
+ return `${by === "The host" ? "The host" : by} asked you to unmute.`;
5555
5712
  case "removed":
5556
5713
  return `${by} removed you from this meeting.`;
5557
5714
  default:
@@ -5577,7 +5734,7 @@ function applyModeration(store, payload, now) {
5577
5734
  grants: { ...snapshot.grants, role },
5578
5735
  moderationNotice: {
5579
5736
  kind: "role",
5580
- 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.",
5581
5738
  at: now()
5582
5739
  }
5583
5740
  });
@@ -5589,7 +5746,10 @@ function applyModeration(store, payload, now) {
5589
5746
  case "removed": {
5590
5747
  const sentence = moderationText(kind, payload);
5591
5748
  if (sentence === null) return ignored("a moderation event with no sentence");
5592
- 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
+ });
5593
5753
  return HANDLED;
5594
5754
  }
5595
5755
  default:
@@ -5628,7 +5788,7 @@ function exactNoteTakerParticipant(meeting, participants) {
5628
5788
  }
5629
5789
  function noteTakerStatus(args) {
5630
5790
  const present = exactNoteTakerParticipant(args.meeting, args.participants) !== null;
5631
- const raw = args.meeting?.metadata[NOTE_TAKER_METADATA_KEY];
5791
+ const raw = args.meeting?.metadata?.[NOTE_TAKER_METADATA_KEY];
5632
5792
  const row = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
5633
5793
  const recorded = readState(row["state"]);
5634
5794
  const detail = typeof row["reason"] === "string" && row["reason"].length > 0 ? row["reason"] : null;
@@ -6105,6 +6265,7 @@ function createRoomEngine(options) {
6105
6265
  lastServerUrl = token.serverUrl;
6106
6266
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
6107
6267
  if (token.leavePolicy !== void 0) store.patch({ leavePolicy: token.leavePolicy });
6268
+ store.patch({ canReclaim: token.canReclaim === true });
6108
6269
  store.patch({
6109
6270
  localIdentity: token.identity,
6110
6271
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -6604,8 +6765,12 @@ var MODERATION_ACTIONS = [
6604
6765
  "lower_hand",
6605
6766
  "lower_all_hands",
6606
6767
  "make_cohost",
6768
+ "make_presenter",
6769
+ "revoke_presenter",
6770
+ "set_av_lock",
6607
6771
  "revoke_cohost",
6608
6772
  "transfer_host",
6773
+ "reclaim_host",
6609
6774
  "rename",
6610
6775
  "remove",
6611
6776
  "report"
@@ -7643,11 +7808,13 @@ var import_react3 = require("react");
7643
7808
 
7644
7809
  // src/core/observation.ts
7645
7810
  function observedRole(role, standing) {
7646
- if (role === "host" || role === "cohost") return role;
7811
+ if (role === "host" || role === "cohost" || role === "presenter") return role;
7647
7812
  return isLinkFollower(standing) ? "guest" : "participant";
7648
7813
  }
7649
- function observedDevice(track, permission = "unknown") {
7814
+ function observedDevice(track, permission = "unknown", held = null) {
7815
+ if (held === "locked") return "locked";
7650
7816
  if (track.enabled) return "on";
7817
+ if (held === "muted_by_host") return "muted_by_host";
7651
7818
  const reason = deviceUnavailableReason(track, permission);
7652
7819
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
7653
7820
  }
@@ -7684,10 +7851,20 @@ function observe(input) {
7684
7851
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
7685
7852
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
7686
7853
  audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
7687
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
7688
- 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
7689
7856
  };
7690
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
+ }
7691
7868
  function staticObservation(phase, reason = null) {
7692
7869
  return {
7693
7870
  phase,
@@ -8877,6 +9054,45 @@ function useMeetAi() {
8877
9054
  }, [ai, meeting, dispatchNoteTaker])
8878
9055
  };
8879
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
+ }
8880
9096
  function useEndMeeting() {
8881
9097
  const host = useMeetHost();
8882
9098
  const snapshot = useMeetSnapshot();
@@ -8914,7 +9130,7 @@ function useConsentNotice() {
8914
9130
  const snapshot = useMeetSnapshot();
8915
9131
  const agent = noteTakerParticipant(snapshot?.participants ?? []);
8916
9132
  if (agent === null) return null;
8917
- 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.`;
8918
9134
  }
8919
9135
  function useMediaSupervisor() {
8920
9136
  const host = useMeetHost();
@@ -9378,6 +9594,38 @@ function useGuestRecordAccess(meeting) {
9378
9594
  )
9379
9595
  };
9380
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
+ }
9381
9629
 
9382
9630
  // src/react/skin-root.tsx
9383
9631
  var import_react12 = require("react");
@@ -10152,6 +10400,18 @@ function SparklesIcon(props) {
10152
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" })
10153
10401
  ] });
10154
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
+ }
10155
10415
  function LockIcon(props) {
10156
10416
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Icon, { ...props, children: [
10157
10417
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "4", y: "10", width: "16", height: "11", rx: "2" }),
@@ -11058,7 +11318,7 @@ function BreakoutRun({ plan }) {
11058
11318
  type: "button",
11059
11319
  className: "mx-meet__primary mx-meet__primary--small",
11060
11320
  onClick: () => void act({ action: "breakout_close" }).then(show),
11061
- children: "Bring everyone back"
11321
+ children: "Close rooms"
11062
11322
  }
11063
11323
  ) : null
11064
11324
  ] });
@@ -11076,9 +11336,14 @@ function BreakoutBanner() {
11076
11336
  const { collab, act, moveTo, localIdentity, breakoutRoomId } = useCollab();
11077
11337
  const plan = collab?.breakout ?? null;
11078
11338
  const open = plan?.state === "open" ? plan : null;
11079
- const now = useNow(open?.endsAt != null);
11339
+ const now = useNow(open?.endsAt != null || open?.closingAt != null);
11080
11340
  const remaining = breakoutRemainingMs(open, now);
11341
+ const closing = breakoutClosingMs(open, now);
11081
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;
11082
11347
  const [dismissedBroadcast, setDismissedBroadcast] = (0, import_react9.useState)(null);
11083
11348
  const [failure, setFailure] = (0, import_react9.useState)(null);
11084
11349
  const expireAsked = (0, import_react9.useRef)(null);
@@ -11091,6 +11356,19 @@ function BreakoutBanner() {
11091
11356
  if ((here ?? null) === request.roomId) return;
11092
11357
  void moveTo(request.roomId).then((result) => setFailure(result.ok ? null : result.message));
11093
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]);
11094
11372
  (0, import_react9.useEffect)(() => {
11095
11373
  if (open === null || remaining === null || remaining > 0) return;
11096
11374
  const asked = expireAsked.current;
@@ -11104,27 +11382,25 @@ function BreakoutBanner() {
11104
11382
  if (open === null && failure === null) return null;
11105
11383
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "mx-meet__breakout-banner", role: "status", children: [
11106
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,
11107
11390
  room !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
11108
11391
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: room.name }),
11109
- remaining !== null ? ` \xB7 ${clock(remaining)} left` : "",
11392
+ remaining !== null && closing === null ? ` \xB7 ${clock(remaining)} left` : "",
11110
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" })
11111
11399
  ] }) : assigned !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
11112
- "You are assigned to ",
11113
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: assigned.name }),
11114
- remaining !== null ? ` \xB7 ${clock(remaining)} left` : "",
11115
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
11116
- "button",
11117
- {
11118
- type: "button",
11119
- className: "mx-meet__primary mx-meet__primary--small",
11120
- onClick: () => void act({ action: "breakout_join", roomId: assigned.id }).then(async (said) => {
11121
- const result = said.ok ? await moveTo(assigned.id) : said;
11122
- setFailure(result.ok ? null : result.message);
11123
- }),
11124
- children: "Join"
11125
- }
11126
- )
11127
- ] }) : 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,
11128
11404
  showBroadcast ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "mx-meet__breakout-broadcast", children: [
11129
11405
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("strong", { children: [
11130
11406
  broadcast.by,
@@ -11172,6 +11448,33 @@ function CaptionLanguageSettings() {
11172
11448
  collab?.captionTranslationError != null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "mx-meet__panel-note mx-meet__collab-error", children: collab.captionTranslationError }) : null
11173
11449
  ] });
11174
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
+ }
11175
11478
 
11176
11479
  // src/skins/meet/components.tsx
11177
11480
  var import_jsx_runtime6 = require("react/jsx-runtime");
@@ -11222,6 +11525,7 @@ function ControlButton({
11222
11525
  label,
11223
11526
  active = false,
11224
11527
  danger = false,
11528
+ pressed,
11225
11529
  onClick,
11226
11530
  children
11227
11531
  }) {
@@ -11236,26 +11540,49 @@ function ControlButton({
11236
11540
  ].filter(Boolean).join(" "),
11237
11541
  "aria-label": label,
11238
11542
  title: label,
11543
+ ...pressed !== void 0 ? { "aria-pressed": pressed } : {},
11239
11544
  onClick,
11240
11545
  children
11241
11546
  }
11242
11547
  );
11243
11548
  }
11244
11549
  function RecordingIndicator() {
11245
- const { state, label } = useRecording();
11246
- if (label === null) return null;
11247
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
11248
- "span",
11249
- {
11250
- className: `mx-meet__recording mx-meet__recording--${state}`,
11251
- role: "status",
11252
- "aria-live": "polite",
11253
- children: [
11254
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RecordIcon, { width: 10, height: 10 }),
11255
- label
11256
- ]
11257
- }
11258
- );
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
+ ] });
11259
11586
  }
11260
11587
  function RoleBadge({
11261
11588
  role,
@@ -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",
@@ -12427,6 +12811,13 @@ function PersonMenu({
12427
12811
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PeopleIcon, { width: 16, height: 16 }),
12428
12812
  " Make co-host"
12429
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,
12430
12821
  !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
12431
12822
  MenuItem,
12432
12823
  {
@@ -12932,8 +13323,19 @@ function Captions({ lines = 3 }) {
12932
13323
  const ai = useMeetAi();
12933
13324
  const visible = ai.transcript.slice(-lines);
12934
13325
  const translation = useCaptionTranslation(visible);
12935
- if (visible.length === 0) return null;
12936
- 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: [
12937
13339
  visible.map((line) => {
12938
13340
  const translated = translation.language === null ? void 0 : translation.translations[line.id];
12939
13341
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
@@ -12956,7 +13358,7 @@ function Captions({ lines = 3 }) {
12956
13358
  function ConsentNotice() {
12957
13359
  const notice = useConsentNotice();
12958
13360
  if (notice === null) return null;
12959
- 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 });
12960
13362
  }
12961
13363
  function AttendanceNotice() {
12962
13364
  const media = useLocalMedia();