@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.js CHANGED
@@ -786,6 +786,8 @@ var MEET_ROUTES = {
786
786
  noteTakerStop: "/api/v1/meet/agent/stop",
787
787
  /** In-meeting Q&A. Streams NDJSON; body is `{meeting_id, question}` only. */
788
788
  intelligenceAsk: "/api/v1/meet/intelligence/ask",
789
+ /** Anyone in the meeting (guests with the room pass included) asks for captions for themselves. */
790
+ noteTakerCaptions: "/api/v1/meet/agent/captions",
789
791
  /** A host ends the meeting for everyone. Served by MRI-A6. */
790
792
  meetingEnd: "/api/v1/meet/end",
791
793
  /**
@@ -932,7 +934,8 @@ function parseRoomToken(body, roomName) {
932
934
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
933
935
  observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
934
936
  connectionPolicy: connectionPolicyFrom(payload["connection_policy"]),
935
- leavePolicy: leavePolicyFrom(payload["leave_policy"])
937
+ leavePolicy: leavePolicyFrom(payload["leave_policy"]),
938
+ canReclaim: payload["can_reclaim"] === true
936
939
  };
937
940
  }
938
941
  function createTokenClient(options) {
@@ -1394,6 +1397,42 @@ function createMeetAi(options) {
1394
1397
  },
1395
1398
  "meet.ai.startNoteTaker"
1396
1399
  ),
1400
+ requestCaptions: async ({ meetingId }) => {
1401
+ const roomToken = options.resolveRoomToken?.() ?? null;
1402
+ const operation = "meet.ai.requestCaptions";
1403
+ try {
1404
+ const response = await send({
1405
+ path: MEET_ROUTES.noteTakerCaptions,
1406
+ body: { meeting_id: meetingId },
1407
+ operation,
1408
+ requireAuth: roomToken === null,
1409
+ ...roomToken !== null ? { headers: { [MEETING_PASS_HEADER]: roomToken } } : {}
1410
+ });
1411
+ if (!response.ok) {
1412
+ options.store.patch({ captionsRefusal: await refusalFor(response, operation) });
1413
+ return;
1414
+ }
1415
+ let payload = null;
1416
+ try {
1417
+ payload = await response.json();
1418
+ } catch {
1419
+ payload = null;
1420
+ }
1421
+ const row = typeof payload === "object" && payload !== null ? payload : {};
1422
+ const detail = typeof row["detail"] === "string" ? row["detail"] : "";
1423
+ options.store.patch({
1424
+ captionsRefusal: row["dispatched"] === true ? null : { status: 200, reason: detail, remedy: "" }
1425
+ });
1426
+ } catch (error) {
1427
+ options.store.patch({
1428
+ captionsRefusal: {
1429
+ status: 0,
1430
+ reason: error.message,
1431
+ remedy: error.remedy ?? ""
1432
+ }
1433
+ });
1434
+ }
1435
+ },
1397
1436
  stopNoteTaker: ({ meetingId }) => dispatch(
1398
1437
  MEET_ROUTES.noteTakerStop,
1399
1438
  { meeting_id: meetingId },
@@ -2380,6 +2419,55 @@ function recordingLabel(state) {
2380
2419
  }
2381
2420
  }
2382
2421
 
2422
+ // src/core/live-policy.ts
2423
+ var DEFAULT_LIVE_POLICY = {
2424
+ chatMode: "everyone",
2425
+ avLock: "off",
2426
+ timeLimitMinutes: 0,
2427
+ timeLimitWarningMinutes: 5,
2428
+ recordingConsent: "notice"
2429
+ };
2430
+ function oneOf(value, allowed, fallback) {
2431
+ return typeof value === "string" && allowed.includes(value) ? value : fallback;
2432
+ }
2433
+ function whole(value, fallback) {
2434
+ const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
2435
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
2436
+ }
2437
+ function livePolicyFrom(raw) {
2438
+ const o = typeof raw === "object" && raw !== null ? raw : {};
2439
+ return {
2440
+ chatMode: oneOf(o["chat_mode"], ["everyone", "hosts_only", "off"], DEFAULT_LIVE_POLICY.chatMode),
2441
+ avLock: oneOf(o["attendee_av_lock"], ["off", "mic", "camera", "both"], DEFAULT_LIVE_POLICY.avLock),
2442
+ timeLimitMinutes: whole(o["time_limit_minutes"], 0),
2443
+ timeLimitWarningMinutes: whole(o["time_limit_warning_minutes"], DEFAULT_LIVE_POLICY.timeLimitWarningMinutes),
2444
+ recordingConsent: oneOf(
2445
+ o["recording_consent"],
2446
+ ["notice", "ok_or_leave", "view_only_until_agree"],
2447
+ DEFAULT_LIVE_POLICY.recordingConsent
2448
+ )
2449
+ };
2450
+ }
2451
+ function samePolicy(a, b) {
2452
+ return a.chatMode === b.chatMode && a.avLock === b.avLock && a.timeLimitMinutes === b.timeLimitMinutes && a.timeLimitWarningMinutes === b.timeLimitWarningMinutes && a.recordingConsent === b.recordingConsent;
2453
+ }
2454
+ var MODERATORS = /* @__PURE__ */ new Set(["host", "cohost"]);
2455
+ function isLockedFor(policy, role, kind) {
2456
+ if (MODERATORS.has(role)) return false;
2457
+ return policy.avLock === "both" || (kind === "microphone" ? policy.avLock === "mic" : policy.avLock === "camera");
2458
+ }
2459
+ function consentPending(policy, role, recordingOn, given) {
2460
+ return recordingOn && !given && policy.recordingConsent !== "notice" && !MODERATORS.has(role);
2461
+ }
2462
+ function canChat(policy, role) {
2463
+ if (policy.chatMode === "everyone") return true;
2464
+ return policy.chatMode === "hosts_only" && MODERATORS.has(role);
2465
+ }
2466
+ function minutesLeft(policy, startedAtMs, nowMs) {
2467
+ if (policy.timeLimitMinutes <= 0 || startedAtMs === null) return null;
2468
+ return Math.ceil((startedAtMs + policy.timeLimitMinutes * 6e4 - nowMs) / 6e4);
2469
+ }
2470
+
2383
2471
  // src/core/attendance.ts
2384
2472
  var IDLE_MEDIA_REGION = {
2385
2473
  permission: { microphone: "unknown", camera: "unknown" },
@@ -3506,6 +3594,8 @@ function planFromRow(row) {
3506
3594
  minutes: num2(row["minutes"]) ?? 0,
3507
3595
  openedAt: str(row["opened_at"]),
3508
3596
  endsAt: str(row["ends_at"]),
3597
+ closingAt: str(row["closing_at"]),
3598
+ moveMode: row["move_mode"] === "auto" ? "auto" : "invite",
3509
3599
  closedAt: str(row["closed_at"]),
3510
3600
  broadcasts: (Array.isArray(row["broadcasts"]) ? row["broadcasts"] : []).map(broadcastFrom).filter((b) => b !== null)
3511
3601
  };
@@ -3657,9 +3747,17 @@ function autoAssign(identities, rooms, exclude = []) {
3657
3747
  return out;
3658
3748
  }
3659
3749
  function breakoutRemainingMs(plan, now) {
3660
- if (plan === null || plan.state !== "open" || plan.endsAt === null) return null;
3661
- const end = Date.parse(plan.endsAt);
3662
- return Number.isNaN(end) ? null : Math.max(0, end - now);
3750
+ if (plan === null || plan.state !== "open") return null;
3751
+ const ends = [plan.endsAt, plan.closingAt].map((stamp) => stamp === null ? Number.NaN : Date.parse(stamp)).filter((time) => !Number.isNaN(time));
3752
+ if (ends.length === 0) return null;
3753
+ return Math.max(0, Math.min(...ends) - now);
3754
+ }
3755
+ var BREAKOUT_COUNTDOWN_MS = 3e4;
3756
+ function breakoutClosingMs(plan, now) {
3757
+ if (plan === null || plan.state !== "open") return null;
3758
+ const remaining = breakoutRemainingMs(plan, now);
3759
+ if (remaining === null) return null;
3760
+ return plan.closingAt !== null || remaining <= BREAKOUT_COUNTDOWN_MS ? remaining : null;
3663
3761
  }
3664
3762
  function defaultCaptionLanguage(settings, viewerLanguage) {
3665
3763
  const configured = settings?.captionTranslationLanguage.trim() ?? "";
@@ -3836,6 +3934,11 @@ var EMPTY = {
3836
3934
  presenter: null,
3837
3935
  waitingCount: 0,
3838
3936
  leavePolicy: DEFAULT_LEAVE_POLICY,
3937
+ livePolicy: DEFAULT_LIVE_POLICY,
3938
+ canReclaim: false,
3939
+ recordingConsentGiven: false,
3940
+ sessionStartedAt: null,
3941
+ mutedByHost: false,
3839
3942
  moderationNotice: null,
3840
3943
  actionNotice: null,
3841
3944
  privateChatRequest: null,
@@ -3845,6 +3948,8 @@ var EMPTY = {
3845
3948
  names: {},
3846
3949
  wrapUp: null,
3847
3950
  noteTakerRefusal: null,
3951
+ captionsOn: false,
3952
+ captionsRefusal: null,
3848
3953
  media: IDLE_MEDIA,
3849
3954
  mediaRegion: IDLE_MEDIA_REGION,
3850
3955
  failure: null,
@@ -4049,7 +4154,12 @@ function createMeetStore(initial = {}) {
4049
4154
  if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.reason === next.unavailable?.reason && current.unavailable?.message === next.unavailable?.message) {
4050
4155
  return;
4051
4156
  }
4052
- commit({ ...state, media: { ...state.media, [kind]: next } });
4157
+ commit({
4158
+ ...state,
4159
+ media: { ...state.media, [kind]: next },
4160
+ // The person turned their own microphone on again: the host's mute is over.
4161
+ ...kind === "microphone" && next.enabled && state.mutedByHost ? { mutedByHost: false } : {}
4162
+ });
4053
4163
  },
4054
4164
  reset() {
4055
4165
  const phase = nextPhase(state.phase, { type: "reset", teardown: true }) ?? EMPTY.phase;
@@ -4111,6 +4221,7 @@ function applyDoorRow(store, attendee) {
4111
4221
  function decidedState(state) {
4112
4222
  return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
4113
4223
  }
4224
+ var LIVE_POLICY_POLL_MS = 3e3;
4114
4225
  function createMeetingSession(options) {
4115
4226
  const { manager, repository, store, identity } = options;
4116
4227
  let channel = null;
@@ -4252,6 +4363,46 @@ function createMeetingSession(options) {
4252
4363
  stopWaiting();
4253
4364
  }
4254
4365
  }
4366
+ let policyTimer = null;
4367
+ let policyReading = false;
4368
+ async function readLivePolicy() {
4369
+ const meeting = store.snapshot().meeting;
4370
+ const pass = options.resolvePass?.() ?? null;
4371
+ if (meeting === null || pass === null || policyReading) return;
4372
+ policyReading = true;
4373
+ try {
4374
+ const response = await options.api.request({
4375
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}`,
4376
+ method: "GET",
4377
+ requireAuth: false,
4378
+ operation: "meet.state(policy)",
4379
+ headers: { [MEETING_PASS_HEADER]: pass }
4380
+ });
4381
+ if (!response.ok) return;
4382
+ const read = await response.json();
4383
+ const next = livePolicyFrom(read.live_policy);
4384
+ const started = read.session_started_at ? Date.parse(read.session_started_at) : NaN;
4385
+ const snapshot = store.snapshot();
4386
+ const startedAt = Number.isFinite(started) ? started : snapshot.sessionStartedAt;
4387
+ if (!samePolicy(snapshot.livePolicy, next) || startedAt !== snapshot.sessionStartedAt) {
4388
+ store.patch({ livePolicy: next, sessionStartedAt: startedAt });
4389
+ }
4390
+ } catch {
4391
+ } finally {
4392
+ policyReading = false;
4393
+ }
4394
+ }
4395
+ function syncLivePolicyTimer() {
4396
+ const inCall = store.snapshot().phase === "in_call";
4397
+ if (inCall && policyTimer === null) {
4398
+ void readLivePolicy();
4399
+ policyTimer = setInterval(() => void readLivePolicy(), LIVE_POLICY_POLL_MS);
4400
+ } else if (!inCall && policyTimer !== null) {
4401
+ clearInterval(policyTimer);
4402
+ policyTimer = null;
4403
+ }
4404
+ }
4405
+ const unsubscribePolicy = store.subscribe(syncLivePolicyTimer);
4255
4406
  async function readState2() {
4256
4407
  if (reading !== null) return reading;
4257
4408
  const meeting = store.snapshot().meeting;
@@ -4638,6 +4789,9 @@ function createMeetingSession(options) {
4638
4789
  },
4639
4790
  dispose() {
4640
4791
  stopWaiting();
4792
+ unsubscribePolicy();
4793
+ if (policyTimer !== null) clearInterval(policyTimer);
4794
+ policyTimer = null;
4641
4795
  channel?.close();
4642
4796
  channel = null;
4643
4797
  meetingChannel?.close();
@@ -5229,10 +5383,11 @@ function moderationText(kind, payload) {
5229
5383
  switch (kind) {
5230
5384
  case "muted": {
5231
5385
  const source = text2(payload["source"]) ?? "microphone";
5232
- return source === "microphone" ? `${by} muted you.` : source === "camera" ? `${by} turned off your camera.` : `${by} stopped your screen share.`;
5386
+ const who = payload["by_role"] === "host" || payload["by_role"] === "cohost" ? "the host" : by;
5387
+ 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}.`;
5233
5388
  }
5234
5389
  case "ask-unmute":
5235
- return `${by} is asking you to unmute.`;
5390
+ return `${by === "The host" ? "The host" : by} asked you to unmute.`;
5236
5391
  case "removed":
5237
5392
  return `${by} removed you from this meeting.`;
5238
5393
  default:
@@ -5258,7 +5413,7 @@ function applyModeration(store, payload, now) {
5258
5413
  grants: { ...snapshot.grants, role },
5259
5414
  moderationNotice: {
5260
5415
  kind: "role",
5261
- text: role === "host" ? "You are now the host." : role === "cohost" ? "You are now a co-host." : "You are no longer a co-host.",
5416
+ 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.",
5262
5417
  at: now()
5263
5418
  }
5264
5419
  });
@@ -5270,7 +5425,10 @@ function applyModeration(store, payload, now) {
5270
5425
  case "removed": {
5271
5426
  const sentence = moderationText(kind, payload);
5272
5427
  if (sentence === null) return ignored("a moderation event with no sentence");
5273
- store.patch({ moderationNotice: { kind, text: sentence, at: now() } });
5428
+ store.patch({
5429
+ moderationNotice: { kind, text: sentence, at: now() },
5430
+ ...kind === "muted" && (text2(payload["source"]) ?? "microphone") === "microphone" ? { mutedByHost: true } : {}
5431
+ });
5274
5432
  return HANDLED;
5275
5433
  }
5276
5434
  default:
@@ -5309,7 +5467,7 @@ function exactNoteTakerParticipant(meeting, participants) {
5309
5467
  }
5310
5468
  function noteTakerStatus(args) {
5311
5469
  const present = exactNoteTakerParticipant(args.meeting, args.participants) !== null;
5312
- const raw = args.meeting?.metadata[NOTE_TAKER_METADATA_KEY];
5470
+ const raw = args.meeting?.metadata?.[NOTE_TAKER_METADATA_KEY];
5313
5471
  const row = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
5314
5472
  const recorded = readState(row["state"]);
5315
5473
  const detail = typeof row["reason"] === "string" && row["reason"].length > 0 ? row["reason"] : null;
@@ -5786,6 +5944,7 @@ function createRoomEngine(options) {
5786
5944
  lastServerUrl = token.serverUrl;
5787
5945
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
5788
5946
  if (token.leavePolicy !== void 0) store.patch({ leavePolicy: token.leavePolicy });
5947
+ store.patch({ canReclaim: token.canReclaim === true });
5789
5948
  store.patch({
5790
5949
  localIdentity: token.identity,
5791
5950
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -6285,8 +6444,12 @@ var MODERATION_ACTIONS = [
6285
6444
  "lower_hand",
6286
6445
  "lower_all_hands",
6287
6446
  "make_cohost",
6447
+ "make_presenter",
6448
+ "revoke_presenter",
6449
+ "set_av_lock",
6288
6450
  "revoke_cohost",
6289
6451
  "transfer_host",
6452
+ "reclaim_host",
6290
6453
  "rename",
6291
6454
  "remove",
6292
6455
  "report"
@@ -7324,11 +7487,13 @@ import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2 } from
7324
7487
 
7325
7488
  // src/core/observation.ts
7326
7489
  function observedRole(role, standing) {
7327
- if (role === "host" || role === "cohost") return role;
7490
+ if (role === "host" || role === "cohost" || role === "presenter") return role;
7328
7491
  return isLinkFollower(standing) ? "guest" : "participant";
7329
7492
  }
7330
- function observedDevice(track, permission = "unknown") {
7493
+ function observedDevice(track, permission = "unknown", held = null) {
7494
+ if (held === "locked") return "locked";
7331
7495
  if (track.enabled) return "on";
7496
+ if (held === "muted_by_host") return "muted_by_host";
7332
7497
  const reason = deviceUnavailableReason(track, permission);
7333
7498
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
7334
7499
  }
@@ -7365,10 +7530,20 @@ function observe(input) {
7365
7530
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
7366
7531
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
7367
7532
  audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
7368
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
7369
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone) : null
7533
+ camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera, lockedFor(snapshot, "camera", inCall)) : null,
7534
+ microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone, lockedFor(snapshot, "microphone", inCall)) : null
7370
7535
  };
7371
7536
  }
7537
+ function lockedFor(snapshot, kind, inCall) {
7538
+ if (!inCall) return null;
7539
+ const role = effectiveRole(snapshot);
7540
+ if (isLockedFor(snapshot.livePolicy, role, kind)) return "locked";
7541
+ if (snapshot.livePolicy.recordingConsent === "view_only_until_agree" && consentPending(snapshot.livePolicy, role, snapshot.recording === "recording", snapshot.recordingConsentGiven)) {
7542
+ return "locked";
7543
+ }
7544
+ if (kind === "microphone" && snapshot.mutedByHost) return "muted_by_host";
7545
+ return null;
7546
+ }
7372
7547
  function staticObservation(phase, reason = null) {
7373
7548
  return {
7374
7549
  phase,
@@ -8558,6 +8733,45 @@ function useMeetAi() {
8558
8733
  }, [ai, meeting, dispatchNoteTaker])
8559
8734
  };
8560
8735
  }
8736
+ function useCaptions() {
8737
+ const host = useMeetHost();
8738
+ const snapshot = useMeetSnapshot();
8739
+ const ai = useMeetAi();
8740
+ const on = snapshot?.captionsOn === true;
8741
+ const refusal2 = snapshot?.captionsRefusal ?? null;
8742
+ const taker = ai.noteTaker;
8743
+ const meeting = snapshot?.meeting ?? null;
8744
+ const request = useCallback2(() => {
8745
+ if (host === null || meeting === null) return;
8746
+ void host.ai.requestCaptions({ meetingId: meeting.id });
8747
+ }, [host, meeting]);
8748
+ const toggle = useCallback2(() => {
8749
+ if (host === null) return;
8750
+ const next = !on;
8751
+ host.store.patch({ captionsOn: next, captionsRefusal: null });
8752
+ if (next && taker.state !== "joining" && taker.state !== "transcribing") request();
8753
+ }, [host, on, taker.state, request]);
8754
+ let state = "off";
8755
+ let reason = null;
8756
+ let remedy = null;
8757
+ if (on) {
8758
+ if (taker.state === "transcribing" && taker.present) {
8759
+ state = "live";
8760
+ } else if (taker.state === "joining") {
8761
+ state = "starting";
8762
+ } else if (refusal2 !== null && refusal2.reason.length > 0) {
8763
+ state = "unavailable";
8764
+ reason = refusal2.reason;
8765
+ remedy = refusal2.remedy.length > 0 ? refusal2.remedy : null;
8766
+ } else if (taker.state === "ended" || taker.state === "handoff_pending") {
8767
+ state = "unavailable";
8768
+ reason = noteTakerLabel(taker);
8769
+ } else {
8770
+ state = "starting";
8771
+ }
8772
+ }
8773
+ return { on, state, reason, remedy, toggle };
8774
+ }
8561
8775
  function useEndMeeting() {
8562
8776
  const host = useMeetHost();
8563
8777
  const snapshot = useMeetSnapshot();
@@ -8595,7 +8809,7 @@ function useConsentNotice() {
8595
8809
  const snapshot = useMeetSnapshot();
8596
8810
  const agent = noteTakerParticipant(snapshot?.participants ?? []);
8597
8811
  if (agent === null) return null;
8598
- return `${agent.displayName} is in this meeting: it is transcribing what is said and writing notes everyone here can read.`;
8812
+ return `AI notes are on. ${agent.displayName} is in this meeting, transcribing what is said and writing notes everyone here can read.`;
8599
8813
  }
8600
8814
  function useMediaSupervisor() {
8601
8815
  const host = useMeetHost();
@@ -9059,6 +9273,38 @@ function useGuestRecordAccess(meeting) {
9059
9273
  )
9060
9274
  };
9061
9275
  }
9276
+ function useLivePolicy() {
9277
+ const snapshot = useMeetSnapshot();
9278
+ const role = useRole();
9279
+ const [now, setNow] = useState2(() => Date.now());
9280
+ const limited = (snapshot?.livePolicy.timeLimitMinutes ?? 0) > 0;
9281
+ useEffect3(() => {
9282
+ if (!limited) return void 0;
9283
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
9284
+ return () => clearInterval(timer);
9285
+ }, [limited]);
9286
+ const policy = snapshot?.livePolicy ?? DEFAULT_LIVE_POLICY;
9287
+ const left = minutesLeft(policy, snapshot?.sessionStartedAt ?? null, now);
9288
+ const host = useMeetHost();
9289
+ const pending = consentPending(policy, role, snapshot?.recording === "recording", snapshot?.recordingConsentGiven === true);
9290
+ const viewOnly = pending && policy.recordingConsent === "view_only_until_agree";
9291
+ const idle = snapshot !== null && snapshot.recording !== "recording" && snapshot.recording !== "starting" && snapshot.recording !== "stopping";
9292
+ const given = snapshot?.recordingConsentGiven === true;
9293
+ const store = host?.store ?? null;
9294
+ useEffect3(() => {
9295
+ if (idle && given) store?.patch({ recordingConsentGiven: false });
9296
+ }, [idle, given, store]);
9297
+ return {
9298
+ canChat: canChat(policy, role),
9299
+ chatMode: policy.chatMode,
9300
+ micLocked: isLockedFor(policy, role, "microphone") || viewOnly,
9301
+ cameraLocked: isLockedFor(policy, role, "camera") || viewOnly,
9302
+ consent: pending ? policy.recordingConsent === "ok_or_leave" ? "ok_or_leave" : "view_only_until_agree" : null,
9303
+ agree: () => store?.patch({ recordingConsentGiven: true }),
9304
+ minutesLeft: left,
9305
+ timeWarning: left !== null && left <= policy.timeLimitWarningMinutes
9306
+ };
9307
+ }
9062
9308
 
9063
9309
  // src/react/skin-root.tsx
9064
9310
  import { useEffect as useEffect8 } from "react";
@@ -9833,6 +10079,18 @@ function SparklesIcon(props) {
9833
10079
  /* @__PURE__ */ jsx4("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" })
9834
10080
  ] });
9835
10081
  }
10082
+ function CaptionsIcon(props) {
10083
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
10084
+ /* @__PURE__ */ jsx4("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
10085
+ /* @__PURE__ */ jsx4("path", { d: "M10 10.5a2 2 0 1 0 0 3M16 10.5a2 2 0 1 0 0 3" })
10086
+ ] });
10087
+ }
10088
+ function NotesIcon(props) {
10089
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
10090
+ /* @__PURE__ */ jsx4("path", { d: "M4 20l1-4L16.5 4.5a2.1 2.1 0 0 1 3 3L8 19l-4 1Z" }),
10091
+ /* @__PURE__ */ jsx4("path", { d: "M14 7l3 3" })
10092
+ ] });
10093
+ }
9836
10094
  function LockIcon(props) {
9837
10095
  return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
9838
10096
  /* @__PURE__ */ jsx4("rect", { x: "4", y: "10", width: "16", height: "11", rx: "2" }),
@@ -10739,7 +10997,7 @@ function BreakoutRun({ plan }) {
10739
10997
  type: "button",
10740
10998
  className: "mx-meet__primary mx-meet__primary--small",
10741
10999
  onClick: () => void act({ action: "breakout_close" }).then(show),
10742
- children: "Bring everyone back"
11000
+ children: "Close rooms"
10743
11001
  }
10744
11002
  ) : null
10745
11003
  ] });
@@ -10757,9 +11015,14 @@ function BreakoutBanner() {
10757
11015
  const { collab, act, moveTo, localIdentity, breakoutRoomId } = useCollab();
10758
11016
  const plan = collab?.breakout ?? null;
10759
11017
  const open = plan?.state === "open" ? plan : null;
10760
- const now = useNow(open?.endsAt != null);
11018
+ const now = useNow(open?.endsAt != null || open?.closingAt != null);
10761
11019
  const remaining = breakoutRemainingMs(open, now);
11020
+ const closing = breakoutClosingMs(open, now);
10762
11021
  const assigned = assignedRoom(open, localIdentity);
11022
+ const [later, setLater] = useState3(null);
11023
+ const [autoSince, setAutoSince] = useState3(null);
11024
+ const AUTO_MOVE_MS = 1e4;
11025
+ const autoLeft = open?.moveMode === "auto" && assigned !== null && autoSince?.id === open.id && later !== open.id ? Math.max(0, AUTO_MOVE_MS - (now - autoSince.at)) : null;
10763
11026
  const [dismissedBroadcast, setDismissedBroadcast] = useState3(null);
10764
11027
  const [failure, setFailure] = useState3(null);
10765
11028
  const expireAsked = useRef5(null);
@@ -10772,6 +11035,19 @@ function BreakoutBanner() {
10772
11035
  if ((here ?? null) === request.roomId) return;
10773
11036
  void moveTo(request.roomId).then((result) => setFailure(result.ok ? null : result.message));
10774
11037
  }, [moveAt, host, moveTo]);
11038
+ const joinAssigned = async () => {
11039
+ if (assigned === null) return;
11040
+ const said = await act({ action: "breakout_join", roomId: assigned.id });
11041
+ const result = said.ok ? await moveTo(assigned.id) : said;
11042
+ setFailure(result.ok ? null : result.message);
11043
+ };
11044
+ useEffect5(() => {
11045
+ if (open === null || open.moveMode !== "auto" || assigned === null || breakoutRoomId !== null) return;
11046
+ if (autoSince?.id !== open.id) setAutoSince({ id: open.id, at: Date.now() });
11047
+ }, [open, assigned, breakoutRoomId, autoSince]);
11048
+ useEffect5(() => {
11049
+ if (autoLeft === 0) void joinAssigned();
11050
+ }, [autoLeft]);
10775
11051
  useEffect5(() => {
10776
11052
  if (open === null || remaining === null || remaining > 0) return;
10777
11053
  const asked = expireAsked.current;
@@ -10785,27 +11061,25 @@ function BreakoutBanner() {
10785
11061
  if (open === null && failure === null) return null;
10786
11062
  return /* @__PURE__ */ jsxs2("div", { className: "mx-meet__breakout-banner", role: "status", children: [
10787
11063
  failure !== null ? /* @__PURE__ */ jsx5("span", { className: "mx-meet__collab-error", children: failure }) : null,
11064
+ closing !== null ? /* @__PURE__ */ jsxs2("span", { role: "status", "data-meet-notice": "breakout-closing", children: [
11065
+ "Rooms closing in ",
11066
+ Math.ceil(closing / 1e3),
11067
+ room !== null ? " \u2014 you return to the main meeting" : ""
11068
+ ] }) : null,
10788
11069
  room !== null ? /* @__PURE__ */ jsxs2("span", { children: [
10789
11070
  /* @__PURE__ */ jsx5("strong", { children: room.name }),
10790
- remaining !== null ? ` \xB7 ${clock(remaining)} left` : "",
11071
+ remaining !== null && closing === null ? ` \xB7 ${clock(remaining)} left` : "",
10791
11072
  /* @__PURE__ */ jsx5("button", { type: "button", className: "mx-meet__chip", onClick: () => void moveTo(null), children: "Return to main room" })
11073
+ ] }) : assigned !== null && later !== open?.id ? /* @__PURE__ */ jsxs2("span", { children: [
11074
+ autoLeft !== null ? `You're being moved to ${assigned.name} in ${Math.ceil(autoLeft / 1e3)} s` : `You're invited to ${assigned.name}`,
11075
+ remaining !== null && closing === null ? ` \xB7 ${clock(remaining)} left` : "",
11076
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void joinAssigned(), children: "Join room" }),
11077
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "mx-meet__chip", onClick: () => setLater(open?.id ?? null), children: "Later" })
10792
11078
  ] }) : assigned !== null ? /* @__PURE__ */ jsxs2("span", { children: [
10793
- "You are assigned to ",
10794
- /* @__PURE__ */ jsx5("strong", { children: assigned.name }),
10795
- remaining !== null ? ` \xB7 ${clock(remaining)} left` : "",
10796
- /* @__PURE__ */ jsx5(
10797
- "button",
10798
- {
10799
- type: "button",
10800
- className: "mx-meet__primary mx-meet__primary--small",
10801
- onClick: () => void act({ action: "breakout_join", roomId: assigned.id }).then(async (said) => {
10802
- const result = said.ok ? await moveTo(assigned.id) : said;
10803
- setFailure(result.ok ? null : result.message);
10804
- }),
10805
- children: "Join"
10806
- }
10807
- )
10808
- ] }) : open?.allowChoose ? /* @__PURE__ */ jsx5("span", { children: "Breakout rooms are open \u2014 pick one under Activities." }) : null,
11079
+ assigned.name,
11080
+ " is open",
11081
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "mx-meet__chip", onClick: () => void joinAssigned(), children: "Join room" })
11082
+ ] }) : open?.allowChoose ? /* @__PURE__ */ jsx5("span", { children: "Rooms are open \u2014 pick one under Activities." }) : null,
10809
11083
  showBroadcast ? /* @__PURE__ */ jsxs2("span", { className: "mx-meet__breakout-broadcast", children: [
10810
11084
  /* @__PURE__ */ jsxs2("strong", { children: [
10811
11085
  broadcast.by,
@@ -10853,6 +11127,33 @@ function CaptionLanguageSettings() {
10853
11127
  collab?.captionTranslationError != null ? /* @__PURE__ */ jsx5("p", { className: "mx-meet__panel-note mx-meet__collab-error", children: collab.captionTranslationError }) : null
10854
11128
  ] });
10855
11129
  }
11130
+ function CaptionsControl() {
11131
+ const captions = useCaptions();
11132
+ return /* @__PURE__ */ jsx5(
11133
+ ControlButton,
11134
+ {
11135
+ label: captions.on ? "Turn off captions" : "Turn on captions",
11136
+ active: captions.on,
11137
+ pressed: captions.on,
11138
+ onClick: captions.toggle,
11139
+ children: /* @__PURE__ */ jsx5(CaptionsIcon, {})
11140
+ }
11141
+ );
11142
+ }
11143
+ function TakeNotesControl() {
11144
+ const ai = useMeetAi();
11145
+ if (!ai.canControlNoteTaker) return null;
11146
+ const running = ai.noteTaker.state === "joining" || ai.noteTaker.state === "transcribing";
11147
+ return /* @__PURE__ */ jsx5(
11148
+ ControlButton,
11149
+ {
11150
+ label: running ? "Stop taking notes" : "Take notes for me",
11151
+ active: running,
11152
+ onClick: () => void (running ? ai.stopNoteTaker() : ai.startNoteTaker()),
11153
+ children: /* @__PURE__ */ jsx5(NotesIcon, {})
11154
+ }
11155
+ );
11156
+ }
10856
11157
 
10857
11158
  // src/skins/meet/components.tsx
10858
11159
  import { Fragment, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -10903,6 +11204,7 @@ function ControlButton({
10903
11204
  label,
10904
11205
  active = false,
10905
11206
  danger = false,
11207
+ pressed,
10906
11208
  onClick,
10907
11209
  children
10908
11210
  }) {
@@ -10917,26 +11219,49 @@ function ControlButton({
10917
11219
  ].filter(Boolean).join(" "),
10918
11220
  "aria-label": label,
10919
11221
  title: label,
11222
+ ...pressed !== void 0 ? { "aria-pressed": pressed } : {},
10920
11223
  onClick,
10921
11224
  children
10922
11225
  }
10923
11226
  );
10924
11227
  }
10925
11228
  function RecordingIndicator() {
10926
- const { state, label } = useRecording();
10927
- if (label === null) return null;
10928
- return /* @__PURE__ */ jsxs3(
10929
- "span",
10930
- {
10931
- className: `mx-meet__recording mx-meet__recording--${state}`,
10932
- role: "status",
10933
- "aria-live": "polite",
10934
- children: [
10935
- /* @__PURE__ */ jsx6(RecordIcon, { width: 10, height: 10 }),
10936
- label
10937
- ]
10938
- }
10939
- );
11229
+ const { state, label, canStart } = useRecording();
11230
+ const snapshot = useMeetSnapshot();
11231
+ const previous = useRef6(state);
11232
+ const [stoppedAt, setStoppedAt] = useState4(null);
11233
+ useEffect6(() => {
11234
+ const was = previous.current;
11235
+ previous.current = state;
11236
+ if ((was === "recording" || was === "starting") && state !== "recording" && state !== "starting") {
11237
+ setStoppedAt(Date.now());
11238
+ const timer = setTimeout(() => setStoppedAt(null), 1e4);
11239
+ return () => clearTimeout(timer);
11240
+ }
11241
+ return void 0;
11242
+ }, [state]);
11243
+ const meetingId = snapshot?.meeting?.id ?? null;
11244
+ if (label === null && stoppedAt === null) return null;
11245
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
11246
+ label !== null ? /* @__PURE__ */ jsxs3(
11247
+ "span",
11248
+ {
11249
+ className: `mx-meet__recording mx-meet__recording--${state}`,
11250
+ role: "status",
11251
+ "aria-live": "polite",
11252
+ children: [
11253
+ /* @__PURE__ */ jsx6(RecordIcon, { width: 10, height: 10 }),
11254
+ label
11255
+ ]
11256
+ }
11257
+ ) : null,
11258
+ stoppedAt !== null ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__recording", role: "status", "data-meet-notice": "recording-stopped", children: "Recording stopped" }) : null,
11259
+ state === "processing" ? (
11260
+ // rec-processing: the host is told it is being made ready (the indicator says Processing…).
11261
+ /* @__PURE__ */ jsx6("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." })
11262
+ ) : null,
11263
+ state === "available" && canStart && meetingId !== null ? /* @__PURE__ */ jsx6("a", { className: "mx-meet__link", href: `/meetings/${meetingId}`, target: "_blank", rel: "noreferrer", children: "View recording" }) : null
11264
+ ] });
10940
11265
  }
10941
11266
  function RoleBadge({
10942
11267
  role,
@@ -11088,6 +11413,7 @@ function ControlBar({ onLeave }) {
11088
11413
  const controls = useHostControls();
11089
11414
  const queue = useHandQueue();
11090
11415
  const micUnavailable = useLocalMedia().microphone.unavailable;
11416
+ const live = useLivePolicy();
11091
11417
  const unread = useUnreadChat(panel === "chat");
11092
11418
  const [whiteboard, setWhiteboard] = useState4(false);
11093
11419
  const app = useMeetAppPanel();
@@ -11155,7 +11481,7 @@ function ControlBar({ onLeave }) {
11155
11481
  panel !== "people" ? /* @__PURE__ */ jsx6(KnockNotice, { onView: () => setPanel("people") }) : null,
11156
11482
  controls.notice !== null && panel !== "people" ? /* @__PURE__ */ jsx6(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
11157
11483
  /* @__PURE__ */ jsxs3("div", { className: "mx-meet__controls", children: [
11158
- /* @__PURE__ */ jsx6(
11484
+ live.micLocked ? null : /* @__PURE__ */ jsx6(
11159
11485
  ControlButton,
11160
11486
  {
11161
11487
  label: micUnavailable !== null ? `Unmute \u2014 ${micUnavailable.remedy}` : room.local?.micEnabled === true ? "Mute (\u2318D)" : "Unmute (\u2318D)",
@@ -11164,7 +11490,7 @@ function ControlBar({ onLeave }) {
11164
11490
  children: room.local?.micEnabled === true ? /* @__PURE__ */ jsx6(MicIcon, {}) : /* @__PURE__ */ jsx6(MicOffIcon, {})
11165
11491
  }
11166
11492
  ),
11167
- /* @__PURE__ */ jsx6(
11493
+ live.cameraLocked ? null : /* @__PURE__ */ jsx6(
11168
11494
  ControlButton,
11169
11495
  {
11170
11496
  label: room.local?.cameraEnabled === true ? "Stop video (\u2318E)" : "Start video (\u2318E)",
@@ -11173,6 +11499,7 @@ function ControlBar({ onLeave }) {
11173
11499
  children: room.local?.cameraEnabled === true ? /* @__PURE__ */ jsx6(CameraIcon, {}) : /* @__PURE__ */ jsx6(CameraOffIcon, {})
11174
11500
  }
11175
11501
  ),
11502
+ /* @__PURE__ */ jsx6(CaptionsControl, {}),
11176
11503
  room.canShareScreen || room.local?.screenSharing === true ? /* @__PURE__ */ jsx6(
11177
11504
  ControlButton,
11178
11505
  {
@@ -11237,6 +11564,7 @@ function ControlBar({ onLeave }) {
11237
11564
  onOpen: openActivity
11238
11565
  }
11239
11566
  ),
11567
+ /* @__PURE__ */ jsx6(TakeNotesControl, {}),
11240
11568
  /* @__PURE__ */ jsx6(AiControl, { active: panel === "ai", onToggle: () => toggle("ai") }),
11241
11569
  recording.canStart ? /* @__PURE__ */ jsx6(ControlButton, { label: "Start recording", onClick: () => void recording.start(), children: /* @__PURE__ */ jsx6(RecordIcon, {}) }) : null,
11242
11570
  recording.canStop ? /* @__PURE__ */ jsx6(ControlButton, { label: "Stop recording", active: true, onClick: () => void recording.stop(), children: /* @__PURE__ */ jsx6(RecordIcon, {}) }) : null,
@@ -11292,6 +11620,23 @@ function ControlBar({ onLeave }) {
11292
11620
  ]
11293
11621
  }
11294
11622
  ),
11623
+ /* @__PURE__ */ jsxs3(
11624
+ MenuItem,
11625
+ {
11626
+ onSelect: () => {
11627
+ setPopover("none");
11628
+ void controls.run({
11629
+ action: "set_av_lock",
11630
+ value: snapshot?.livePolicy.avLock === "both" ? "off" : "both"
11631
+ });
11632
+ },
11633
+ children: [
11634
+ /* @__PURE__ */ jsx6(MicOffIcon, { width: 16, height: 16 }),
11635
+ " ",
11636
+ snapshot?.livePolicy.avLock === "both" ? "Turn on attendees' microphone and camera" : "Turn off attendees' microphone and camera"
11637
+ ]
11638
+ }
11639
+ ),
11295
11640
  /* @__PURE__ */ jsxs3(
11296
11641
  MenuItem,
11297
11642
  {
@@ -11619,9 +11964,45 @@ function MeetingOverlays() {
11619
11964
  const reactions = useReactions();
11620
11965
  const { notice, dismiss } = useModerationNotice();
11621
11966
  const room = useRoom();
11967
+ const live = useLivePolicy();
11968
+ const snapshot = useMeetSnapshot();
11969
+ const controls = useHostControls();
11970
+ const isHostNow = useRole() === "host";
11622
11971
  return /* @__PURE__ */ jsxs3(Fragment, { children: [
11623
11972
  /* @__PURE__ */ jsx6(ReactionOverlay, { reactions }),
11624
11973
  /* @__PURE__ */ jsx6(MediaNotices, {}),
11974
+ snapshot !== null && snapshot.phase === "in_call" && snapshot.participants.length >= 2 && !snapshot.participants.some((p) => p.role === "host" || p.role === "cohost") ? (
11975
+ // meeting-without-host: the call goes on; host-only controls are simply not on screen.
11976
+ /* @__PURE__ */ jsx6("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." })
11977
+ ) : null,
11978
+ live.consent !== null ? /* @__PURE__ */ jsx6("div", { className: "mx-meet__sheet-backdrop", children: /* @__PURE__ */ jsxs3("div", { className: "mx-meet__sheet", role: "alertdialog", "aria-label": "This meeting is being recorded", "data-meet-notice": "recording-consent", children: [
11979
+ /* @__PURE__ */ jsx6("h2", { className: "mx-meet__panel-title", children: "This meeting is being recorded" }),
11980
+ /* @__PURE__ */ jsx6("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." }),
11981
+ /* @__PURE__ */ jsxs3("div", { className: "mx-meet__menu-actions", children: [
11982
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__send", onClick: live.agree, children: live.consent === "ok_or_leave" ? "OK" : "I agree" }),
11983
+ live.consent === "ok_or_leave" ? /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__link", onClick: () => void room.leave(), children: "Leave meeting" }) : null
11984
+ ] })
11985
+ ] }) }) : null,
11986
+ snapshot?.canReclaim === true && !isHostNow ? /* @__PURE__ */ jsxs3("div", { className: "mx-meet__notice-bar", role: "status", "data-meet-notice": "host-reclaim", children: [
11987
+ /* @__PURE__ */ jsx6("span", { children: "You started this meeting. Take back the host?" }),
11988
+ /* @__PURE__ */ jsx6(
11989
+ "button",
11990
+ {
11991
+ type: "button",
11992
+ className: "mx-meet__primary mx-meet__primary--small",
11993
+ onClick: () => void controls.run({ action: "reclaim_host" }),
11994
+ children: "Reclaim host"
11995
+ }
11996
+ )
11997
+ ] }) : null,
11998
+ live.micLocked || live.cameraLocked ? /* @__PURE__ */ jsx6("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,
11999
+ live.timeWarning && live.minutesLeft !== null && live.minutesLeft > 0 ? /* @__PURE__ */ jsxs3("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "time-warning", children: [
12000
+ "This meeting will end in ",
12001
+ live.minutesLeft,
12002
+ " ",
12003
+ live.minutesLeft === 1 ? "minute" : "minutes",
12004
+ " (time limit)."
12005
+ ] }) : null,
11625
12006
  notice !== null && notice.kind !== "removed" ? (
11626
12007
  // A host's request ("the host is asking you to unmute"), not an error:
11627
12008
  // an assertive status, so it is announced at once without being filed
@@ -11712,6 +12093,7 @@ function ChatPanel({
11712
12093
  }) {
11713
12094
  const snapshot = useMeetSnapshot();
11714
12095
  const room = useRoom();
12096
+ const live = useLivePolicy();
11715
12097
  const [draft, setDraft] = useState4("");
11716
12098
  const [recipient, setRecipient] = useState4(to);
11717
12099
  const listRef = useRef6(null);
@@ -11736,7 +12118,7 @@ function ChatPanel({
11736
12118
  snapshot?.chatLoaded !== true && messages.length === 0 ? /* @__PURE__ */ jsx6("li", { className: "mx-meet__panel-note", children: "Reading earlier messages\u2026" }) : messages.length === 0 && snapshot?.chatHistoryError == null ? /* @__PURE__ */ jsx6("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,
11737
12119
  messages.map((message) => /* @__PURE__ */ jsx6(ChatLine, { message, local, people }, message.id))
11738
12120
  ] }),
11739
- /* @__PURE__ */ jsxs3(
12121
+ !live.canChat ? /* @__PURE__ */ jsx6("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__ */ jsxs3(
11740
12122
  "form",
11741
12123
  {
11742
12124
  className: "mx-meet__composer mx-meet__composer--chat",
@@ -12108,6 +12490,13 @@ function PersonMenu({
12108
12490
  /* @__PURE__ */ jsx6(PeopleIcon, { width: 16, height: 16 }),
12109
12491
  " Make co-host"
12110
12492
  ] }) : null,
12493
+ !participant.isAgent && participant.role !== "cohost" && participant.role !== "host" ? participant.role === "presenter" ? /* @__PURE__ */ jsxs3(MenuItem, { onSelect: () => act({ action: "revoke_presenter", identity: participant.identity }), children: [
12494
+ /* @__PURE__ */ jsx6(ScreenShareIcon, { width: 16, height: 16 }),
12495
+ " Remove presenter"
12496
+ ] }) : /* @__PURE__ */ jsxs3(MenuItem, { onSelect: () => act({ action: "make_presenter", identity: participant.identity }), children: [
12497
+ /* @__PURE__ */ jsx6(ScreenShareIcon, { width: 16, height: 16 }),
12498
+ " Make presenter"
12499
+ ] }) : null,
12111
12500
  !participant.isAgent ? /* @__PURE__ */ jsxs3(
12112
12501
  MenuItem,
12113
12502
  {
@@ -12613,8 +13002,19 @@ function Captions({ lines = 3 }) {
12613
13002
  const ai = useMeetAi();
12614
13003
  const visible = ai.transcript.slice(-lines);
12615
13004
  const translation = useCaptionTranslation(visible);
12616
- if (visible.length === 0) return null;
12617
- return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", children: [
13005
+ const captions = useCaptions();
13006
+ if (!captions.on) return null;
13007
+ if (captions.state === "unavailable") {
13008
+ return /* @__PURE__ */ jsx6("div", { className: "mx-meet__captions", role: "status", "data-meet-notice": "captions-unavailable", children: /* @__PURE__ */ jsxs3("p", { className: "mx-meet__caption", children: [
13009
+ "Captions are unavailable. ",
13010
+ captions.reason,
13011
+ captions.remedy !== null ? ` ${captions.remedy}` : ""
13012
+ ] }) });
13013
+ }
13014
+ if (visible.length === 0) {
13015
+ return /* @__PURE__ */ jsx6("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", "data-meet-caption": captions.state, children: /* @__PURE__ */ jsx6("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." }) });
13016
+ }
13017
+ return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", "data-meet-caption": "live", children: [
12618
13018
  visible.map((line) => {
12619
13019
  const translated = translation.language === null ? void 0 : translation.translations[line.id];
12620
13020
  return /* @__PURE__ */ jsxs3(
@@ -12637,7 +13037,7 @@ function Captions({ lines = 3 }) {
12637
13037
  function ConsentNotice() {
12638
13038
  const notice = useConsentNotice();
12639
13039
  if (notice === null) return null;
12640
- return /* @__PURE__ */ jsx6("p", { className: "mx-meet__consent", role: "status", children: notice });
13040
+ return /* @__PURE__ */ jsx6("p", { className: "mx-meet__consent", role: "status", "data-meet-notice": "notes-started", children: notice });
12641
13041
  }
12642
13042
  function AttendanceNotice() {
12643
13043
  const media = useLocalMedia();
@@ -14104,6 +14504,7 @@ export {
14104
14504
  autoAssign,
14105
14505
  autoRejoinDelayMs,
14106
14506
  availableActivities,
14507
+ breakoutClosingMs,
14107
14508
  breakoutRemainingMs,
14108
14509
  callInviteAction,
14109
14510
  callInviteText,
@@ -14271,6 +14672,7 @@ export {
14271
14672
  useCanModerate,
14272
14673
  useCanRecord,
14273
14674
  useCaptionTranslation,
14675
+ useCaptions,
14274
14676
  useCollab,
14275
14677
  useCollabPanelRequest,
14276
14678
  useConsentNotice,