@ai-matrx/meet 0.11.41 → 0.11.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.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();
@@ -5041,8 +5195,7 @@ var ROOM_EVENTS = {
5041
5195
  /**
5042
5196
  * A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`:
5043
5197
  * `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the
5044
- * person concerned; `lower-hands {identities: [...] | "*"}` · `role {identity,
5045
- * role}` — to everyone.
5198
+ * person concerned; `role {identity, role}` — to everyone.
5046
5199
  */
5047
5200
  moderation: "matrx.meet.moderation",
5048
5201
  /**
@@ -5230,10 +5383,11 @@ function moderationText(kind, payload) {
5230
5383
  switch (kind) {
5231
5384
  case "muted": {
5232
5385
  const source = text2(payload["source"]) ?? "microphone";
5233
- 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}.`;
5234
5388
  }
5235
5389
  case "ask-unmute":
5236
- return `${by} is asking you to unmute.`;
5390
+ return `${by === "The host" ? "The host" : by} asked you to unmute.`;
5237
5391
  case "removed":
5238
5392
  return `${by} removed you from this meeting.`;
5239
5393
  default:
@@ -5259,7 +5413,7 @@ function applyModeration(store, payload, now) {
5259
5413
  grants: { ...snapshot.grants, role },
5260
5414
  moderationNotice: {
5261
5415
  kind: "role",
5262
- 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.",
5263
5417
  at: now()
5264
5418
  }
5265
5419
  });
@@ -5271,7 +5425,10 @@ function applyModeration(store, payload, now) {
5271
5425
  case "removed": {
5272
5426
  const sentence = moderationText(kind, payload);
5273
5427
  if (sentence === null) return ignored("a moderation event with no sentence");
5274
- 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
+ });
5275
5432
  return HANDLED;
5276
5433
  }
5277
5434
  default:
@@ -5310,7 +5467,7 @@ function exactNoteTakerParticipant(meeting, participants) {
5310
5467
  }
5311
5468
  function noteTakerStatus(args) {
5312
5469
  const present = exactNoteTakerParticipant(args.meeting, args.participants) !== null;
5313
- const raw = args.meeting?.metadata[NOTE_TAKER_METADATA_KEY];
5470
+ const raw = args.meeting?.metadata?.[NOTE_TAKER_METADATA_KEY];
5314
5471
  const row = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
5315
5472
  const recorded = readState(row["state"]);
5316
5473
  const detail = typeof row["reason"] === "string" && row["reason"].length > 0 ? row["reason"] : null;
@@ -5787,6 +5944,7 @@ function createRoomEngine(options) {
5787
5944
  lastServerUrl = token.serverUrl;
5788
5945
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
5789
5946
  if (token.leavePolicy !== void 0) store.patch({ leavePolicy: token.leavePolicy });
5947
+ store.patch({ canReclaim: token.canReclaim === true });
5790
5948
  store.patch({
5791
5949
  localIdentity: token.identity,
5792
5950
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -6281,13 +6439,17 @@ var MODERATION_ACTIONS = [
6281
6439
  "ask_unmute",
6282
6440
  "ask_all_unmute",
6283
6441
  "spotlight",
6284
- "present",
6442
+ "sync_presenter",
6285
6443
  "raise_hand",
6286
6444
  "lower_hand",
6287
6445
  "lower_all_hands",
6288
6446
  "make_cohost",
6447
+ "make_presenter",
6448
+ "revoke_presenter",
6449
+ "set_av_lock",
6289
6450
  "revoke_cohost",
6290
6451
  "transfer_host",
6452
+ "reclaim_host",
6291
6453
  "rename",
6292
6454
  "remove",
6293
6455
  "report"
@@ -6313,18 +6475,17 @@ function createInRoomClient(options) {
6313
6475
  function refusal2(response, operation, remedy) {
6314
6476
  return refusalFromResponse(response, operation, remedy);
6315
6477
  }
6316
- let toldPresenting = false;
6478
+ let toldSharing = false;
6317
6479
  let tellChain = Promise.resolve();
6318
6480
  store.subscribe(() => {
6319
6481
  const local = store.snapshot().participants.find((p) => p.isLocal);
6320
6482
  const sharing = local?.screenSharing === true;
6321
- if (sharing === toldPresenting || local === void 0) return;
6322
- toldPresenting = sharing;
6323
- const on = sharing;
6324
- tellChain = tellChain.then(() => client.moderate({ action: "present", on })).then(
6483
+ if (sharing === toldSharing || local === void 0) return;
6484
+ toldSharing = sharing;
6485
+ tellChain = tellChain.then(() => client.moderate({ action: "sync_presenter" })).then(
6325
6486
  () => void 0,
6326
6487
  (error) => {
6327
- console.warn(`[meet] the server did not take presenter=${String(on)}: ${String(error)}`);
6488
+ console.warn(`[meet] the server did not take the screen-share change: ${String(error)}`);
6328
6489
  }
6329
6490
  );
6330
6491
  });
@@ -6341,8 +6502,7 @@ function createInRoomClient(options) {
6341
6502
  action: request.action,
6342
6503
  ...request.identity != null ? { identity: request.identity } : {},
6343
6504
  ...request.name !== void 0 ? { name: request.name } : {},
6344
- ...request.reason !== void 0 ? { reason: request.reason } : {},
6345
- ...request.on !== void 0 ? { on: request.on } : {}
6505
+ ...request.reason !== void 0 ? { reason: request.reason } : {}
6346
6506
  },
6347
6507
  ...hdrs !== void 0 ? { headers: hdrs } : {}
6348
6508
  });
@@ -7327,11 +7487,13 @@ import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2 } from
7327
7487
 
7328
7488
  // src/core/observation.ts
7329
7489
  function observedRole(role, standing) {
7330
- if (role === "host" || role === "cohost") return role;
7490
+ if (role === "host" || role === "cohost" || role === "presenter") return role;
7331
7491
  return isLinkFollower(standing) ? "guest" : "participant";
7332
7492
  }
7333
- function observedDevice(track, permission = "unknown") {
7493
+ function observedDevice(track, permission = "unknown", held = null) {
7494
+ if (held === "locked") return "locked";
7334
7495
  if (track.enabled) return "on";
7496
+ if (held === "muted_by_host") return "muted_by_host";
7335
7497
  const reason = deviceUnavailableReason(track, permission);
7336
7498
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
7337
7499
  }
@@ -7368,10 +7530,20 @@ function observe(input) {
7368
7530
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
7369
7531
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
7370
7532
  audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
7371
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
7372
- 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
7373
7535
  };
7374
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
+ }
7375
7547
  function staticObservation(phase, reason = null) {
7376
7548
  return {
7377
7549
  phase,
@@ -8561,6 +8733,45 @@ function useMeetAi() {
8561
8733
  }, [ai, meeting, dispatchNoteTaker])
8562
8734
  };
8563
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
+ }
8564
8775
  function useEndMeeting() {
8565
8776
  const host = useMeetHost();
8566
8777
  const snapshot = useMeetSnapshot();
@@ -8598,7 +8809,7 @@ function useConsentNotice() {
8598
8809
  const snapshot = useMeetSnapshot();
8599
8810
  const agent = noteTakerParticipant(snapshot?.participants ?? []);
8600
8811
  if (agent === null) return null;
8601
- 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.`;
8602
8813
  }
8603
8814
  function useMediaSupervisor() {
8604
8815
  const host = useMeetHost();
@@ -9062,6 +9273,38 @@ function useGuestRecordAccess(meeting) {
9062
9273
  )
9063
9274
  };
9064
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
+ }
9065
9308
 
9066
9309
  // src/react/skin-root.tsx
9067
9310
  import { useEffect as useEffect8 } from "react";
@@ -9836,6 +10079,18 @@ function SparklesIcon(props) {
9836
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" })
9837
10080
  ] });
9838
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
+ }
9839
10094
  function LockIcon(props) {
9840
10095
  return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
9841
10096
  /* @__PURE__ */ jsx4("rect", { x: "4", y: "10", width: "16", height: "11", rx: "2" }),
@@ -10742,7 +10997,7 @@ function BreakoutRun({ plan }) {
10742
10997
  type: "button",
10743
10998
  className: "mx-meet__primary mx-meet__primary--small",
10744
10999
  onClick: () => void act({ action: "breakout_close" }).then(show),
10745
- children: "Bring everyone back"
11000
+ children: "Close rooms"
10746
11001
  }
10747
11002
  ) : null
10748
11003
  ] });
@@ -10760,9 +11015,14 @@ function BreakoutBanner() {
10760
11015
  const { collab, act, moveTo, localIdentity, breakoutRoomId } = useCollab();
10761
11016
  const plan = collab?.breakout ?? null;
10762
11017
  const open = plan?.state === "open" ? plan : null;
10763
- const now = useNow(open?.endsAt != null);
11018
+ const now = useNow(open?.endsAt != null || open?.closingAt != null);
10764
11019
  const remaining = breakoutRemainingMs(open, now);
11020
+ const closing = breakoutClosingMs(open, now);
10765
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;
10766
11026
  const [dismissedBroadcast, setDismissedBroadcast] = useState3(null);
10767
11027
  const [failure, setFailure] = useState3(null);
10768
11028
  const expireAsked = useRef5(null);
@@ -10775,6 +11035,19 @@ function BreakoutBanner() {
10775
11035
  if ((here ?? null) === request.roomId) return;
10776
11036
  void moveTo(request.roomId).then((result) => setFailure(result.ok ? null : result.message));
10777
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]);
10778
11051
  useEffect5(() => {
10779
11052
  if (open === null || remaining === null || remaining > 0) return;
10780
11053
  const asked = expireAsked.current;
@@ -10788,27 +11061,25 @@ function BreakoutBanner() {
10788
11061
  if (open === null && failure === null) return null;
10789
11062
  return /* @__PURE__ */ jsxs2("div", { className: "mx-meet__breakout-banner", role: "status", children: [
10790
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,
10791
11069
  room !== null ? /* @__PURE__ */ jsxs2("span", { children: [
10792
11070
  /* @__PURE__ */ jsx5("strong", { children: room.name }),
10793
- remaining !== null ? ` \xB7 ${clock(remaining)} left` : "",
11071
+ remaining !== null && closing === null ? ` \xB7 ${clock(remaining)} left` : "",
10794
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" })
10795
11078
  ] }) : assigned !== null ? /* @__PURE__ */ jsxs2("span", { children: [
10796
- "You are assigned to ",
10797
- /* @__PURE__ */ jsx5("strong", { children: assigned.name }),
10798
- remaining !== null ? ` \xB7 ${clock(remaining)} left` : "",
10799
- /* @__PURE__ */ jsx5(
10800
- "button",
10801
- {
10802
- type: "button",
10803
- className: "mx-meet__primary mx-meet__primary--small",
10804
- onClick: () => void act({ action: "breakout_join", roomId: assigned.id }).then(async (said) => {
10805
- const result = said.ok ? await moveTo(assigned.id) : said;
10806
- setFailure(result.ok ? null : result.message);
10807
- }),
10808
- children: "Join"
10809
- }
10810
- )
10811
- ] }) : 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,
10812
11083
  showBroadcast ? /* @__PURE__ */ jsxs2("span", { className: "mx-meet__breakout-broadcast", children: [
10813
11084
  /* @__PURE__ */ jsxs2("strong", { children: [
10814
11085
  broadcast.by,
@@ -10856,6 +11127,33 @@ function CaptionLanguageSettings() {
10856
11127
  collab?.captionTranslationError != null ? /* @__PURE__ */ jsx5("p", { className: "mx-meet__panel-note mx-meet__collab-error", children: collab.captionTranslationError }) : null
10857
11128
  ] });
10858
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
+ }
10859
11157
 
10860
11158
  // src/skins/meet/components.tsx
10861
11159
  import { Fragment, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -10906,6 +11204,7 @@ function ControlButton({
10906
11204
  label,
10907
11205
  active = false,
10908
11206
  danger = false,
11207
+ pressed,
10909
11208
  onClick,
10910
11209
  children
10911
11210
  }) {
@@ -10920,26 +11219,49 @@ function ControlButton({
10920
11219
  ].filter(Boolean).join(" "),
10921
11220
  "aria-label": label,
10922
11221
  title: label,
11222
+ ...pressed !== void 0 ? { "aria-pressed": pressed } : {},
10923
11223
  onClick,
10924
11224
  children
10925
11225
  }
10926
11226
  );
10927
11227
  }
10928
11228
  function RecordingIndicator() {
10929
- const { state, label } = useRecording();
10930
- if (label === null) return null;
10931
- return /* @__PURE__ */ jsxs3(
10932
- "span",
10933
- {
10934
- className: `mx-meet__recording mx-meet__recording--${state}`,
10935
- role: "status",
10936
- "aria-live": "polite",
10937
- children: [
10938
- /* @__PURE__ */ jsx6(RecordIcon, { width: 10, height: 10 }),
10939
- label
10940
- ]
10941
- }
10942
- );
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
+ ] });
10943
11265
  }
10944
11266
  function RoleBadge({
10945
11267
  role,
@@ -10966,6 +11288,7 @@ function ParticipantTile({
10966
11288
  const showVideo = participant.cameraEnabled || participant.screenSharing;
10967
11289
  const pinnedHere = snapshot?.pinned === participant.identity;
10968
11290
  const spotlit = snapshot?.spotlight === participant.identity;
11291
+ const presenting = snapshot?.presenter === participant.identity;
10969
11292
  const handPosition = participant.handRaised ? queue.findIndex((entry) => entry.identity === participant.identity) + 1 : 0;
10970
11293
  return /* @__PURE__ */ jsxs3(
10971
11294
  "div",
@@ -10977,6 +11300,7 @@ function ParticipantTile({
10977
11300
  ].filter(Boolean).join(" "),
10978
11301
  "data-identity": participant.identity,
10979
11302
  ...participantAttributes(participant),
11303
+ ...presenting ? { "data-meet-participant-presenting": "true" } : {},
10980
11304
  children: [
10981
11305
  showVideo ? /* @__PURE__ */ jsx6(
10982
11306
  "video",
@@ -10988,6 +11312,7 @@ function ParticipantTile({
10988
11312
  muted: participant.isLocal
10989
11313
  }
10990
11314
  ) : /* @__PURE__ */ jsx6("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ jsx6(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: featured ? 96 : 56 }) }),
11315
+ presenting ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__tile-presenting", role: "status", children: participant.isLocal ? "You are presenting" : `${participant.displayName} is presenting` }) : null,
10991
11316
  participant.handRaised ? /* @__PURE__ */ jsxs3("span", { className: "mx-meet__tile-hand", title: `Hand raised \xB7 ${ordinal(handPosition)} in line`, children: [
10992
11317
  /* @__PURE__ */ jsx6(HandIcon, { width: 14, height: 14 }),
10993
11318
  handPosition > 0 ? handPosition : null
@@ -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",
@@ -11919,7 +12301,8 @@ function PeoplePanel({
11919
12301
  }
11920
12302
  ),
11921
12303
  isNoteTaker(participant) ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null,
11922
- snapshot?.spotlight === participant.identity ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "Spotlight" }) : null
12304
+ snapshot?.spotlight === participant.identity ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "Spotlight" }) : null,
12305
+ snapshot?.presenter === participant.identity ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", "data-meet-presenting": "true", children: "Presenting" }) : null
11923
12306
  ] }),
11924
12307
  participant.handRaised ? /* @__PURE__ */ jsx6(HandIcon, { width: 14, height: 14 }) : null,
11925
12308
  !participant.micEnabled && !isNoteTaker(participant) ? /* @__PURE__ */ jsx6(MicOffIcon, { width: 14, height: 14 }) : null,
@@ -12107,6 +12490,13 @@ function PersonMenu({
12107
12490
  /* @__PURE__ */ jsx6(PeopleIcon, { width: 16, height: 16 }),
12108
12491
  " Make co-host"
12109
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,
12110
12500
  !participant.isAgent ? /* @__PURE__ */ jsxs3(
12111
12501
  MenuItem,
12112
12502
  {
@@ -12612,8 +13002,19 @@ function Captions({ lines = 3 }) {
12612
13002
  const ai = useMeetAi();
12613
13003
  const visible = ai.transcript.slice(-lines);
12614
13004
  const translation = useCaptionTranslation(visible);
12615
- if (visible.length === 0) return null;
12616
- 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: [
12617
13018
  visible.map((line) => {
12618
13019
  const translated = translation.language === null ? void 0 : translation.translations[line.id];
12619
13020
  return /* @__PURE__ */ jsxs3(
@@ -12636,7 +13037,7 @@ function Captions({ lines = 3 }) {
12636
13037
  function ConsentNotice() {
12637
13038
  const notice = useConsentNotice();
12638
13039
  if (notice === null) return null;
12639
- 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 });
12640
13041
  }
12641
13042
  function AttendanceNotice() {
12642
13043
  const media = useLocalMedia();
@@ -14103,6 +14504,7 @@ export {
14103
14504
  autoAssign,
14104
14505
  autoRejoinDelayMs,
14105
14506
  availableActivities,
14507
+ breakoutClosingMs,
14106
14508
  breakoutRemainingMs,
14107
14509
  callInviteAction,
14108
14510
  callInviteText,
@@ -14270,6 +14672,7 @@ export {
14270
14672
  useCanModerate,
14271
14673
  useCanRecord,
14272
14674
  useCaptionTranslation,
14675
+ useCaptions,
14273
14676
  useCollab,
14274
14677
  useCollabPanelRequest,
14275
14678
  useConsentNotice,