@ai-matrx/meet 0.11.42 → 0.11.44

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.
@@ -44,9 +44,11 @@ __export(meet_exports, {
44
44
  CameraOffIcon: () => CameraOffIcon,
45
45
  CaptionLanguageSettings: () => CaptionLanguageSettings,
46
46
  Captions: () => Captions,
47
+ CaptionsIcon: () => CaptionsIcon,
47
48
  ChatIcon: () => ChatIcon,
48
49
  ChatPanel: () => ChatPanel,
49
50
  CheckIcon: () => CheckIcon,
51
+ ChevronUpIcon: () => ChevronUpIcon,
50
52
  CloseIcon: () => CloseIcon,
51
53
  ConnectionDot: () => ConnectionDot,
52
54
  ConsentNotice: () => ConsentNotice,
@@ -67,6 +69,7 @@ __export(meet_exports, {
67
69
  MicIcon: () => MicIcon,
68
70
  MicOffIcon: () => MicOffIcon,
69
71
  MoreIcon: () => MoreIcon,
72
+ NotesIcon: () => NotesIcon,
70
73
  PanelShell: () => PanelShell,
71
74
  ParticipantTile: () => ParticipantTile,
72
75
  PencilIcon: () => PencilIcon,
@@ -265,6 +268,8 @@ var MEET_ROUTES = {
265
268
  noteTakerStop: "/api/v1/meet/agent/stop",
266
269
  /** In-meeting Q&A. Streams NDJSON; body is `{meeting_id, question}` only. */
267
270
  intelligenceAsk: "/api/v1/meet/intelligence/ask",
271
+ /** Anyone in the meeting (guests with the room pass included) asks for captions for themselves. */
272
+ noteTakerCaptions: "/api/v1/meet/agent/captions",
268
273
  /** A host ends the meeting for everyone. Served by MRI-A6. */
269
274
  meetingEnd: "/api/v1/meet/end",
270
275
  /**
@@ -360,12 +365,39 @@ function recordingLabel(state) {
360
365
  }
361
366
  }
362
367
 
368
+ // src/core/live-policy.ts
369
+ var DEFAULT_LIVE_POLICY = {
370
+ chatMode: "everyone",
371
+ avLock: "off",
372
+ timeLimitMinutes: 0,
373
+ timeLimitWarningMinutes: 5,
374
+ recordingConsent: "notice"
375
+ };
376
+ var MODERATORS = /* @__PURE__ */ new Set(["host", "cohost"]);
377
+ function isLockedFor(policy, role, kind) {
378
+ if (MODERATORS.has(role)) return false;
379
+ return policy.avLock === "both" || (kind === "microphone" ? policy.avLock === "mic" : policy.avLock === "camera");
380
+ }
381
+ function consentPending(policy, role, recordingOn, given) {
382
+ return recordingOn && !given && policy.recordingConsent !== "notice" && !MODERATORS.has(role);
383
+ }
384
+ function canChat(policy, role) {
385
+ if (policy.chatMode === "everyone") return true;
386
+ return policy.chatMode === "hosts_only" && MODERATORS.has(role);
387
+ }
388
+ function minutesLeft(policy, startedAtMs, nowMs) {
389
+ if (policy.timeLimitMinutes <= 0 || startedAtMs === null) return null;
390
+ return Math.ceil((startedAtMs + policy.timeLimitMinutes * 6e4 - nowMs) / 6e4);
391
+ }
392
+
363
393
  // src/core/attendance.ts
394
+ var IDLE_PREVIEW = { state: "idle", unavailable: null };
364
395
  var IDLE_MEDIA_REGION = {
365
396
  permission: { microphone: "unknown", camera: "unknown" },
366
397
  audioOutput: "ok",
367
398
  notice: null,
368
- offer: null
399
+ offer: null,
400
+ preview: { microphone: IDLE_PREVIEW, camera: IDLE_PREVIEW }
369
401
  };
370
402
  var IDLE_TRACK = { enabled: false, unavailable: null };
371
403
  var IDLE_MEDIA = {
@@ -452,9 +484,17 @@ function autoAssign(identities, rooms, exclude = []) {
452
484
  return out;
453
485
  }
454
486
  function breakoutRemainingMs(plan, now) {
455
- if (plan === null || plan.state !== "open" || plan.endsAt === null) return null;
456
- const end = Date.parse(plan.endsAt);
457
- return Number.isNaN(end) ? null : Math.max(0, end - now);
487
+ if (plan === null || plan.state !== "open") return null;
488
+ const ends = [plan.endsAt, plan.closingAt].map((stamp) => stamp === null ? Number.NaN : Date.parse(stamp)).filter((time) => !Number.isNaN(time));
489
+ if (ends.length === 0) return null;
490
+ return Math.max(0, Math.min(...ends) - now);
491
+ }
492
+ var BREAKOUT_COUNTDOWN_MS = 3e4;
493
+ function breakoutClosingMs(plan, now) {
494
+ if (plan === null || plan.state !== "open") return null;
495
+ const remaining = breakoutRemainingMs(plan, now);
496
+ if (remaining === null) return null;
497
+ return plan.closingAt !== null || remaining <= BREAKOUT_COUNTDOWN_MS ? remaining : null;
458
498
  }
459
499
  function defaultCaptionLanguage(settings, viewerLanguage) {
460
500
  const configured = settings?.captionTranslationLanguage.trim() ?? "";
@@ -578,7 +618,7 @@ function deviceFixSteps(kind, reason, where) {
578
618
  case "safari":
579
619
  return where.os === "ios" ? [`Settings \u203A Apps \u203A Safari \u203A ${device}`, "Choose Allow", "Return here and press Try again"] : ["Safari \u203A Settings for This Website", `Set ${device} to Allow`, "Press Try again"];
580
620
  default:
581
- return ["Click the icon left of the web address", `Set ${device} to Allow`, "Press Try again"];
621
+ return ["Click the site settings icon in the address bar", `Set ${device} to Allow`, "Press Try again"];
582
622
  }
583
623
  case "blocked_os": {
584
624
  const app = where.browser === "chrome" ? "Google Chrome" : where.browser === "edge" ? "Microsoft Edge" : where.browser === "firefox" ? "Firefox" : "your browser";
@@ -662,14 +702,31 @@ function webMediaPlatform() {
662
702
 
663
703
  // src/core/observation.ts
664
704
  function observedRole(role, standing) {
665
- if (role === "host" || role === "cohost") return role;
705
+ if (role === "host" || role === "cohost" || role === "presenter") return role;
666
706
  return isLinkFollower(standing) ? "guest" : "participant";
667
707
  }
668
- function observedDevice(track, permission = "unknown") {
708
+ function observedDevice(track, permission = "unknown", held = null) {
709
+ if (held === "locked") return "locked";
669
710
  if (track.enabled) return "on";
711
+ if (held === "muted_by_host") return "muted_by_host";
670
712
  const reason = deviceUnavailableReason(track, permission);
671
713
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
672
714
  }
715
+ function observedPreview(preview, permission = "unknown") {
716
+ if (permission === "denied") return "unavailable:denied";
717
+ switch (preview.state) {
718
+ case "on":
719
+ return "on";
720
+ case "off":
721
+ return "muted_by_me";
722
+ case "prompting":
723
+ return "prompting";
724
+ case "unavailable":
725
+ return `unavailable:${preview.unavailable?.reason ?? "failed"}`;
726
+ default:
727
+ return null;
728
+ }
729
+ }
673
730
  function endedRecord(snapshot, given) {
674
731
  const record = snapshot?.meeting ?? given;
675
732
  return record !== null && record.endedAt !== null;
@@ -693,20 +750,34 @@ function observe(input) {
693
750
  const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
694
751
  const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
695
752
  const deviceInPlay = phase === "joining" || inCall;
753
+ const region = snapshot?.mediaRegion;
754
+ const atPrejoin = phase === "prejoin" && region !== void 0;
696
755
  return {
697
756
  phase,
698
757
  reason: phase === "ended" ? snapshot?.reason ?? null : snapshot?.reason ?? reasonOf(phase),
699
758
  role,
700
- connection: snapshot?.connection ?? "stable",
759
+ // `net-blip`: in the call, a drop shorter than `reconnect_banner_delay_ms` is not something the
760
+ // person sees — the contract says what is on screen, so it reads `stable` until the banner shows.
761
+ connection: snapshot === null ? "stable" : inCall && snapshot.connection !== "stable" && !snapshot.connectionBanner ? "stable" : snapshot.connection,
701
762
  hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
702
763
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
703
764
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
704
765
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
705
766
  audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
706
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
707
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone) : null
767
+ camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera, lockedFor(snapshot, "camera", inCall)) : atPrejoin ? observedPreview(region.preview.camera, region.permission.camera) : null,
768
+ microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone, lockedFor(snapshot, "microphone", inCall)) : atPrejoin ? observedPreview(region.preview.microphone, region.permission.microphone) : null
708
769
  };
709
770
  }
771
+ function lockedFor(snapshot, kind, inCall) {
772
+ if (!inCall) return null;
773
+ const role = effectiveRole(snapshot);
774
+ if (isLockedFor(snapshot.livePolicy, role, kind)) return "locked";
775
+ if (snapshot.livePolicy.recordingConsent === "view_only_until_agree" && consentPending(snapshot.livePolicy, role, snapshot.recording === "recording", snapshot.recordingConsentGiven)) {
776
+ return "locked";
777
+ }
778
+ if (kind === "microphone" && snapshot.mutedByHost) return "muted_by_host";
779
+ return null;
780
+ }
710
781
  function staticObservation(phase, reason = null) {
711
782
  return {
712
783
  phase,
@@ -796,7 +867,7 @@ function exactNoteTakerParticipant(meeting, participants) {
796
867
  }
797
868
  function noteTakerStatus(args) {
798
869
  const present = exactNoteTakerParticipant(args.meeting, args.participants) !== null;
799
- const raw = args.meeting?.metadata[NOTE_TAKER_METADATA_KEY];
870
+ const raw = args.meeting?.metadata?.[NOTE_TAKER_METADATA_KEY];
800
871
  const row = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
801
872
  const recorded = readState(row["state"]);
802
873
  const detail = typeof row["reason"] === "string" && row["reason"].length > 0 ? row["reason"] : null;
@@ -1218,7 +1289,8 @@ function useRoom() {
1218
1289
  failure: snapshot?.failure ?? null,
1219
1290
  secondDevice: snapshot?.secondDevice ?? null,
1220
1291
  local,
1221
- canShareScreen: engine?.supportsScreenShare() ?? true,
1292
+ // An observer (role `viewer`) publishes nothing: no Share screen to press and be refused (MD-16).
1293
+ canShareScreen: (engine?.supportsScreenShare() ?? true) && !isObserverRole(snapshot?.grants.role ?? null),
1222
1294
  join: (0, import_react5.useCallback)(
1223
1295
  async (request) => requireEngine(engine, "join").join(request),
1224
1296
  [engine]
@@ -1549,6 +1621,45 @@ function useMeetAi() {
1549
1621
  }, [ai, meeting, dispatchNoteTaker])
1550
1622
  };
1551
1623
  }
1624
+ function useCaptions() {
1625
+ const host = useMeetHost();
1626
+ const snapshot = useMeetSnapshot();
1627
+ const ai = useMeetAi();
1628
+ const on = snapshot?.captionsOn === true;
1629
+ const refusal = snapshot?.captionsRefusal ?? null;
1630
+ const taker = ai.noteTaker;
1631
+ const meeting = snapshot?.meeting ?? null;
1632
+ const request = (0, import_react5.useCallback)(() => {
1633
+ if (host === null || meeting === null) return;
1634
+ void host.ai.requestCaptions({ meetingId: meeting.id });
1635
+ }, [host, meeting]);
1636
+ const toggle = (0, import_react5.useCallback)(() => {
1637
+ if (host === null) return;
1638
+ const next = !on;
1639
+ host.store.patch({ captionsOn: next, captionsRefusal: null });
1640
+ if (next && taker.state !== "joining" && taker.state !== "transcribing") request();
1641
+ }, [host, on, taker.state, request]);
1642
+ let state = "off";
1643
+ let reason = null;
1644
+ let remedy = null;
1645
+ if (on) {
1646
+ if (taker.state === "transcribing" && taker.present) {
1647
+ state = "live";
1648
+ } else if (taker.state === "joining") {
1649
+ state = "starting";
1650
+ } else if (refusal !== null && refusal.reason.length > 0) {
1651
+ state = "unavailable";
1652
+ reason = refusal.reason;
1653
+ remedy = refusal.remedy.length > 0 ? refusal.remedy : null;
1654
+ } else if (taker.state === "ended" || taker.state === "handoff_pending") {
1655
+ state = "unavailable";
1656
+ reason = noteTakerLabel(taker);
1657
+ } else {
1658
+ state = "starting";
1659
+ }
1660
+ }
1661
+ return { on, state, reason, remedy, toggle };
1662
+ }
1552
1663
  function useEndMeeting() {
1553
1664
  const host = useMeetHost();
1554
1665
  const snapshot = useMeetSnapshot();
@@ -1586,7 +1697,7 @@ function useConsentNotice() {
1586
1697
  const snapshot = useMeetSnapshot();
1587
1698
  const agent = noteTakerParticipant(snapshot?.participants ?? []);
1588
1699
  if (agent === null) return null;
1589
- return `${agent.displayName} is in this meeting: it is transcribing what is said and writing notes everyone here can read.`;
1700
+ return `AI notes are on. ${agent.displayName} is in this meeting, transcribing what is said and writing notes everyone here can read.`;
1590
1701
  }
1591
1702
  function useMediaSupervisor() {
1592
1703
  const host = useMeetHost();
@@ -1605,6 +1716,12 @@ function useMediaSupervisor() {
1605
1716
  retry: (0, import_react5.useCallback)(
1606
1717
  async (kind) => kind === "microphone" ? engine?.setMicrophoneEnabled(true) : engine?.setCameraEnabled(true),
1607
1718
  [engine]
1719
+ ),
1720
+ reportPreview: (0, import_react5.useCallback)(
1721
+ (kind, outcome) => {
1722
+ void engine?.previewDevice(kind, outcome);
1723
+ },
1724
+ [engine]
1608
1725
  )
1609
1726
  };
1610
1727
  }
@@ -1834,6 +1951,38 @@ function useGuestRecordAccess(meeting) {
1834
1951
  )
1835
1952
  };
1836
1953
  }
1954
+ function useLivePolicy() {
1955
+ const snapshot = useMeetSnapshot();
1956
+ const role = useRole();
1957
+ const [now, setNow] = (0, import_react5.useState)(() => Date.now());
1958
+ const limited = (snapshot?.livePolicy.timeLimitMinutes ?? 0) > 0;
1959
+ (0, import_react5.useEffect)(() => {
1960
+ if (!limited) return void 0;
1961
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
1962
+ return () => clearInterval(timer);
1963
+ }, [limited]);
1964
+ const policy = snapshot?.livePolicy ?? DEFAULT_LIVE_POLICY;
1965
+ const left = minutesLeft(policy, snapshot?.sessionStartedAt ?? null, now);
1966
+ const host = useMeetHost();
1967
+ const pending = consentPending(policy, role, snapshot?.recording === "recording", snapshot?.recordingConsentGiven === true);
1968
+ const viewOnly = pending && policy.recordingConsent === "view_only_until_agree";
1969
+ const idle = snapshot !== null && snapshot.recording !== "recording" && snapshot.recording !== "starting" && snapshot.recording !== "stopping";
1970
+ const given = snapshot?.recordingConsentGiven === true;
1971
+ const store = host?.store ?? null;
1972
+ (0, import_react5.useEffect)(() => {
1973
+ if (idle && given) store?.patch({ recordingConsentGiven: false });
1974
+ }, [idle, given, store]);
1975
+ return {
1976
+ canChat: canChat(policy, role),
1977
+ chatMode: policy.chatMode,
1978
+ micLocked: isLockedFor(policy, role, "microphone") || viewOnly,
1979
+ cameraLocked: isLockedFor(policy, role, "camera") || viewOnly,
1980
+ consent: pending ? policy.recordingConsent === "ok_or_leave" ? "ok_or_leave" : "view_only_until_agree" : null,
1981
+ agree: () => store?.patch({ recordingConsentGiven: true }),
1982
+ minutesLeft: left,
1983
+ timeWarning: left !== null && left <= policy.timeLimitWarningMinutes
1984
+ };
1985
+ }
1837
1986
 
1838
1987
  // src/react/collab-hooks.ts
1839
1988
  var import_react7 = require("react");
@@ -1893,6 +2042,39 @@ function useCollabPanelRequest(onRequest) {
1893
2042
  // src/core/reactions.ts
1894
2043
  var MEET_REACTIONS = ["\u{1F496}", "\u{1F44D}", "\u{1F389}", "\u{1F44F}", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F914}", "\u{1F44E}"];
1895
2044
 
2045
+ // src/core/speaking-detector.ts
2046
+ function createSpeakingWhileMutedDetector(options = {}) {
2047
+ const threshold = options.threshold ?? 0.06;
2048
+ const holdMs = options.holdMs ?? 600;
2049
+ const gapMs = options.gapMs ?? 400;
2050
+ const cooldownMs = options.cooldownMs ?? 2e4;
2051
+ let speechStart = null;
2052
+ let lastLoud = null;
2053
+ let firedAt = null;
2054
+ return {
2055
+ feed(level, at) {
2056
+ if (level >= threshold) {
2057
+ if (speechStart === null || lastLoud === null || at - lastLoud > gapMs) speechStart = at;
2058
+ lastLoud = at;
2059
+ } else if (lastLoud !== null && at - lastLoud > gapMs) {
2060
+ speechStart = null;
2061
+ lastLoud = null;
2062
+ }
2063
+ if (speechStart === null || at - speechStart < holdMs) return false;
2064
+ if (firedAt !== null && at - firedAt < cooldownMs) return false;
2065
+ firedAt = at;
2066
+ speechStart = null;
2067
+ lastLoud = null;
2068
+ return true;
2069
+ },
2070
+ reset() {
2071
+ speechStart = null;
2072
+ lastLoud = null;
2073
+ firedAt = null;
2074
+ }
2075
+ };
2076
+ }
2077
+
1896
2078
  // src/react/facade.ts
1897
2079
  var NO_ACTIONS = {
1898
2080
  join: async () => void 0,
@@ -2049,6 +2231,18 @@ function SparklesIcon(props) {
2049
2231
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M19 15l.9 2.1 2.1.9-2.1.9-.9 2.1-.9-2.1-2.1-.9 2.1-.9L19 15Z" })
2050
2232
  ] });
2051
2233
  }
2234
+ function CaptionsIcon(props) {
2235
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Icon, { ...props, children: [
2236
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
2237
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M10 10.5a2 2 0 1 0 0 3M16 10.5a2 2 0 1 0 0 3" })
2238
+ ] });
2239
+ }
2240
+ function NotesIcon(props) {
2241
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Icon, { ...props, children: [
2242
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M4 20l1-4L16.5 4.5a2.1 2.1 0 0 1 3 3L8 19l-4 1Z" }),
2243
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M14 7l3 3" })
2244
+ ] });
2245
+ }
2052
2246
  function LockIcon(props) {
2053
2247
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Icon, { ...props, children: [
2054
2248
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "4", y: "10", width: "16", height: "11", rx: "2" }),
@@ -2196,6 +2390,9 @@ function UndoIcon(props) {
2196
2390
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11" })
2197
2391
  ] });
2198
2392
  }
2393
+ function ChevronUpIcon(props) {
2394
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "m6 15 6-6 6 6" }) });
2395
+ }
2199
2396
 
2200
2397
  // src/skins/meet/collab.tsx
2201
2398
  var import_jsx_runtime5 = require("react/jsx-runtime");
@@ -2967,7 +3164,7 @@ function BreakoutRun({ plan }) {
2967
3164
  type: "button",
2968
3165
  className: "mx-meet__primary mx-meet__primary--small",
2969
3166
  onClick: () => void act({ action: "breakout_close" }).then(show),
2970
- children: "Bring everyone back"
3167
+ children: "Close rooms"
2971
3168
  }
2972
3169
  ) : null
2973
3170
  ] });
@@ -2985,9 +3182,14 @@ function BreakoutBanner() {
2985
3182
  const { collab, act, moveTo, localIdentity, breakoutRoomId } = useCollab();
2986
3183
  const plan = collab?.breakout ?? null;
2987
3184
  const open = plan?.state === "open" ? plan : null;
2988
- const now = useNow(open?.endsAt != null);
3185
+ const now = useNow(open?.endsAt != null || open?.closingAt != null);
2989
3186
  const remaining = breakoutRemainingMs(open, now);
3187
+ const closing = breakoutClosingMs(open, now);
2990
3188
  const assigned = assignedRoom(open, localIdentity);
3189
+ const [later, setLater] = (0, import_react9.useState)(null);
3190
+ const [autoSince, setAutoSince] = (0, import_react9.useState)(null);
3191
+ const AUTO_MOVE_MS = 1e4;
3192
+ const autoLeft = open?.moveMode === "auto" && assigned !== null && autoSince?.id === open.id && later !== open.id ? Math.max(0, AUTO_MOVE_MS - (now - autoSince.at)) : null;
2991
3193
  const [dismissedBroadcast, setDismissedBroadcast] = (0, import_react9.useState)(null);
2992
3194
  const [failure, setFailure] = (0, import_react9.useState)(null);
2993
3195
  const expireAsked = (0, import_react9.useRef)(null);
@@ -3000,6 +3202,19 @@ function BreakoutBanner() {
3000
3202
  if ((here ?? null) === request.roomId) return;
3001
3203
  void moveTo(request.roomId).then((result) => setFailure(result.ok ? null : result.message));
3002
3204
  }, [moveAt, host, moveTo]);
3205
+ const joinAssigned = async () => {
3206
+ if (assigned === null) return;
3207
+ const said = await act({ action: "breakout_join", roomId: assigned.id });
3208
+ const result = said.ok ? await moveTo(assigned.id) : said;
3209
+ setFailure(result.ok ? null : result.message);
3210
+ };
3211
+ (0, import_react9.useEffect)(() => {
3212
+ if (open === null || open.moveMode !== "auto" || assigned === null || breakoutRoomId !== null) return;
3213
+ if (autoSince?.id !== open.id) setAutoSince({ id: open.id, at: Date.now() });
3214
+ }, [open, assigned, breakoutRoomId, autoSince]);
3215
+ (0, import_react9.useEffect)(() => {
3216
+ if (autoLeft === 0) void joinAssigned();
3217
+ }, [autoLeft]);
3003
3218
  (0, import_react9.useEffect)(() => {
3004
3219
  if (open === null || remaining === null || remaining > 0) return;
3005
3220
  const asked = expireAsked.current;
@@ -3013,27 +3228,25 @@ function BreakoutBanner() {
3013
3228
  if (open === null && failure === null) return null;
3014
3229
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "mx-meet__breakout-banner", role: "status", children: [
3015
3230
  failure !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "mx-meet__collab-error", children: failure }) : null,
3231
+ closing !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { role: "status", "data-meet-notice": "breakout-closing", children: [
3232
+ "Rooms closing in ",
3233
+ Math.ceil(closing / 1e3),
3234
+ room !== null ? " \u2014 you return to the main meeting" : ""
3235
+ ] }) : null,
3016
3236
  room !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
3017
3237
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: room.name }),
3018
- remaining !== null ? ` \xB7 ${clock(remaining)} left` : "",
3238
+ remaining !== null && closing === null ? ` \xB7 ${clock(remaining)} left` : "",
3019
3239
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mx-meet__chip", onClick: () => void moveTo(null), children: "Return to main room" })
3240
+ ] }) : assigned !== null && later !== open?.id ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
3241
+ autoLeft !== null ? `You're being moved to ${assigned.name} in ${Math.ceil(autoLeft / 1e3)} s` : `You're invited to ${assigned.name}`,
3242
+ remaining !== null && closing === null ? ` \xB7 ${clock(remaining)} left` : "",
3243
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void joinAssigned(), children: "Join room" }),
3244
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mx-meet__chip", onClick: () => setLater(open?.id ?? null), children: "Later" })
3020
3245
  ] }) : assigned !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
3021
- "You are assigned to ",
3022
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: assigned.name }),
3023
- remaining !== null ? ` \xB7 ${clock(remaining)} left` : "",
3024
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3025
- "button",
3026
- {
3027
- type: "button",
3028
- className: "mx-meet__primary mx-meet__primary--small",
3029
- onClick: () => void act({ action: "breakout_join", roomId: assigned.id }).then(async (said) => {
3030
- const result = said.ok ? await moveTo(assigned.id) : said;
3031
- setFailure(result.ok ? null : result.message);
3032
- }),
3033
- children: "Join"
3034
- }
3035
- )
3036
- ] }) : open?.allowChoose ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: "Breakout rooms are open \u2014 pick one under Activities." }) : null,
3246
+ assigned.name,
3247
+ " is open",
3248
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mx-meet__chip", onClick: () => void joinAssigned(), children: "Join room" })
3249
+ ] }) : open?.allowChoose ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: "Rooms are open \u2014 pick one under Activities." }) : null,
3037
3250
  showBroadcast ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "mx-meet__breakout-broadcast", children: [
3038
3251
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("strong", { children: [
3039
3252
  broadcast.by,
@@ -3081,6 +3294,33 @@ function CaptionLanguageSettings() {
3081
3294
  collab?.captionTranslationError != null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "mx-meet__panel-note mx-meet__collab-error", children: collab.captionTranslationError }) : null
3082
3295
  ] });
3083
3296
  }
3297
+ function CaptionsControl() {
3298
+ const captions = useCaptions();
3299
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3300
+ ControlButton,
3301
+ {
3302
+ label: captions.on ? "Turn off captions" : "Turn on captions",
3303
+ active: captions.on,
3304
+ pressed: captions.on,
3305
+ onClick: captions.toggle,
3306
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CaptionsIcon, {})
3307
+ }
3308
+ );
3309
+ }
3310
+ function TakeNotesControl() {
3311
+ const ai = useMeetAi();
3312
+ if (!ai.canControlNoteTaker) return null;
3313
+ const running = ai.noteTaker.state === "joining" || ai.noteTaker.state === "transcribing";
3314
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3315
+ ControlButton,
3316
+ {
3317
+ label: running ? "Stop taking notes" : "Take notes for me",
3318
+ active: running,
3319
+ onClick: () => void (running ? ai.stopNoteTaker() : ai.startNoteTaker()),
3320
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(NotesIcon, {})
3321
+ }
3322
+ );
3323
+ }
3084
3324
 
3085
3325
  // src/skins/meet/components.tsx
3086
3326
  var import_jsx_runtime6 = require("react/jsx-runtime");
@@ -3131,6 +3371,7 @@ function ControlButton({
3131
3371
  label,
3132
3372
  active = false,
3133
3373
  danger = false,
3374
+ pressed,
3134
3375
  onClick,
3135
3376
  children
3136
3377
  }) {
@@ -3145,26 +3386,49 @@ function ControlButton({
3145
3386
  ].filter(Boolean).join(" "),
3146
3387
  "aria-label": label,
3147
3388
  title: label,
3389
+ ...pressed !== void 0 ? { "aria-pressed": pressed } : {},
3148
3390
  onClick,
3149
3391
  children
3150
3392
  }
3151
3393
  );
3152
3394
  }
3153
3395
  function RecordingIndicator() {
3154
- const { state, label } = useRecording();
3155
- if (label === null) return null;
3156
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3157
- "span",
3158
- {
3159
- className: `mx-meet__recording mx-meet__recording--${state}`,
3160
- role: "status",
3161
- "aria-live": "polite",
3162
- children: [
3163
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RecordIcon, { width: 10, height: 10 }),
3164
- label
3165
- ]
3396
+ const { state, label, canStart } = useRecording();
3397
+ const snapshot = useMeetSnapshot();
3398
+ const previous = (0, import_react10.useRef)(state);
3399
+ const [stoppedAt, setStoppedAt] = (0, import_react10.useState)(null);
3400
+ (0, import_react10.useEffect)(() => {
3401
+ const was = previous.current;
3402
+ previous.current = state;
3403
+ if ((was === "recording" || was === "starting") && state !== "recording" && state !== "starting") {
3404
+ setStoppedAt(Date.now());
3405
+ const timer = setTimeout(() => setStoppedAt(null), 1e4);
3406
+ return () => clearTimeout(timer);
3166
3407
  }
3167
- );
3408
+ return void 0;
3409
+ }, [state]);
3410
+ const meetingId = snapshot?.meeting?.id ?? null;
3411
+ if (label === null && stoppedAt === null) return null;
3412
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
3413
+ label !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3414
+ "span",
3415
+ {
3416
+ className: `mx-meet__recording mx-meet__recording--${state}`,
3417
+ role: "status",
3418
+ "aria-live": "polite",
3419
+ children: [
3420
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RecordIcon, { width: 10, height: 10 }),
3421
+ label
3422
+ ]
3423
+ }
3424
+ ) : null,
3425
+ stoppedAt !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__recording", role: "status", "data-meet-notice": "recording-stopped", children: "Recording stopped" }) : null,
3426
+ state === "processing" ? (
3427
+ // rec-processing: the host is told it is being made ready (the indicator says Processing…).
3428
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__recording", role: "status", "data-meet-notice": "recording-processing", children: "Your recording is being processed. A link appears here when it is ready." })
3429
+ ) : null,
3430
+ state === "available" && canStart && meetingId !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("a", { className: "mx-meet__link", href: `/meetings/${meetingId}`, target: "_blank", rel: "noreferrer", children: "View recording" }) : null
3431
+ ] });
3168
3432
  }
3169
3433
  function ConnectionDot({ quality }) {
3170
3434
  if (quality === "excellent" || quality === "good" || quality === "unknown") return null;
@@ -3320,6 +3584,7 @@ function ControlBar({ onLeave }) {
3320
3584
  const controls = useHostControls();
3321
3585
  const queue = useHandQueue();
3322
3586
  const micUnavailable = useLocalMedia().microphone.unavailable;
3587
+ const live = useLivePolicy();
3323
3588
  const unread = useUnreadChat(panel === "chat");
3324
3589
  const [whiteboard, setWhiteboard] = (0, import_react10.useState)(false);
3325
3590
  const app = useMeetAppPanel();
@@ -3385,9 +3650,10 @@ function ControlBar({ onLeave }) {
3385
3650
  ) }) }) : null,
3386
3651
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BreakoutBanner, {}),
3387
3652
  panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(KnockNotice, { onView: () => setPanel("people") }) : null,
3653
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SpeakingWhileMutedNudge, {}),
3388
3654
  controls.notice !== null && panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
3389
3655
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__controls", children: [
3390
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3656
+ live.micLocked ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3391
3657
  ControlButton,
3392
3658
  {
3393
3659
  label: micUnavailable !== null ? `Unmute \u2014 ${micUnavailable.remedy}` : room.local?.micEnabled === true ? "Mute (\u2318D)" : "Unmute (\u2318D)",
@@ -3396,7 +3662,8 @@ function ControlBar({ onLeave }) {
3396
3662
  children: room.local?.micEnabled === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, {})
3397
3663
  }
3398
3664
  ),
3399
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3665
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AudioSettingsControl, {}),
3666
+ live.cameraLocked ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3400
3667
  ControlButton,
3401
3668
  {
3402
3669
  label: room.local?.cameraEnabled === true ? "Stop video (\u2318E)" : "Start video (\u2318E)",
@@ -3405,6 +3672,7 @@ function ControlBar({ onLeave }) {
3405
3672
  children: room.local?.cameraEnabled === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CameraIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CameraOffIcon, {})
3406
3673
  }
3407
3674
  ),
3675
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CaptionsControl, {}),
3408
3676
  room.canShareScreen || room.local?.screenSharing === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3409
3677
  ControlButton,
3410
3678
  {
@@ -3469,6 +3737,7 @@ function ControlBar({ onLeave }) {
3469
3737
  onOpen: openActivity
3470
3738
  }
3471
3739
  ),
3740
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(TakeNotesControl, {}),
3472
3741
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AiControl, { active: panel === "ai", onToggle: () => toggle("ai") }),
3473
3742
  recording.canStart ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ControlButton, { label: "Start recording", onClick: () => void recording.start(), children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RecordIcon, {}) }) : null,
3474
3743
  recording.canStop ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ControlButton, { label: "Stop recording", active: true, onClick: () => void recording.stop(), children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RecordIcon, {}) }) : null,
@@ -3509,6 +3778,13 @@ function ControlBar({ onLeave }) {
3509
3778
  ]
3510
3779
  }
3511
3780
  ),
3781
+ !room.canShareScreen && room.local?.screenSharing !== true ? (
3782
+ // `share-unsupported-mobile`: the control is absent; the menu says where it lives.
3783
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__menu-note", role: "note", "data-meet-notice": "share-unsupported", children: [
3784
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ScreenShareIcon, { width: 16, height: 16 }),
3785
+ " Present from a computer"
3786
+ ] })
3787
+ ) : null,
3512
3788
  controls.isHost ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
3513
3789
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__menu-sep", role: "separator" }),
3514
3790
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
@@ -3524,6 +3800,23 @@ function ControlBar({ onLeave }) {
3524
3800
  ]
3525
3801
  }
3526
3802
  ),
3803
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3804
+ MenuItem,
3805
+ {
3806
+ onSelect: () => {
3807
+ setPopover("none");
3808
+ void controls.run({
3809
+ action: "set_av_lock",
3810
+ value: snapshot?.livePolicy.avLock === "both" ? "off" : "both"
3811
+ });
3812
+ },
3813
+ children: [
3814
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, { width: 16, height: 16 }),
3815
+ " ",
3816
+ snapshot?.livePolicy.avLock === "both" ? "Turn on attendees' microphone and camera" : "Turn off attendees' microphone and camera"
3817
+ ]
3818
+ }
3819
+ ),
3527
3820
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3528
3821
  MenuItem,
3529
3822
  {
@@ -3766,6 +4059,94 @@ function Toast({ text, failed, onDone }) {
3766
4059
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: onDone, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CloseIcon, { width: 14, height: 14 }) })
3767
4060
  ] });
3768
4061
  }
4062
+ function AudioSettingsControl() {
4063
+ const host = useMeetHost();
4064
+ const devices = useDeviceSnapshot();
4065
+ const [open, setOpen] = (0, import_react10.useState)(false);
4066
+ const list = (devices?.devices.audioinput ?? []).filter((device) => device.deviceId.length > 0);
4067
+ if (host === null || list.length < 2) return null;
4068
+ const current = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
4069
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "mx-meet__anchor", children: [
4070
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4071
+ ControlButton,
4072
+ {
4073
+ label: "Audio settings",
4074
+ active: open,
4075
+ onClick: () => {
4076
+ if (!open) void host.devices.refresh();
4077
+ setOpen(!open);
4078
+ },
4079
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ChevronUpIcon, {})
4080
+ }
4081
+ ),
4082
+ open ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Popover, { label: "Microphone", onClose: () => setOpen(false), children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__menu", role: "menu", "aria-label": "Microphone", children: list.map((device) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
4083
+ "button",
4084
+ {
4085
+ type: "button",
4086
+ role: "menuitemradio",
4087
+ "aria-checked": device.deviceId === current,
4088
+ className: "mx-meet__menu-item",
4089
+ onClick: () => {
4090
+ setOpen(false);
4091
+ host.devices.select("audioinput", device.deviceId);
4092
+ void host.engine.switchDevice("audioinput", device.deviceId).catch(() => void 0);
4093
+ },
4094
+ children: [
4095
+ device.deviceId === current ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CheckIcon, { width: 16, height: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__menu-gap" }),
4096
+ device.label.length > 0 ? device.label : "Microphone"
4097
+ ]
4098
+ },
4099
+ device.deviceId
4100
+ )) }) }) : null
4101
+ ] });
4102
+ }
4103
+ function SpeakingWhileMutedNudge() {
4104
+ const room = useRoom();
4105
+ const host = useMeetHost();
4106
+ const devices = useDeviceSnapshot();
4107
+ const microphone = useLocalMedia().microphone;
4108
+ const mutedByMe = host !== null && room.local != null && room.local.micEnabled !== true && microphone.unavailable === null;
4109
+ const mic = devices?.selected.audioinput ?? host?.devices.resolve("audioinput") ?? null;
4110
+ const stream = useMicrophoneStream(mutedByMe ? mic : "off");
4111
+ const level = useAudioLevel(stream);
4112
+ const latest = (0, import_react10.useRef)(0);
4113
+ latest.current = level;
4114
+ const [shown, setShown] = (0, import_react10.useState)(false);
4115
+ const [detector] = (0, import_react10.useState)(() => createSpeakingWhileMutedDetector());
4116
+ (0, import_react10.useEffect)(() => {
4117
+ if (!mutedByMe) {
4118
+ detector.reset();
4119
+ setShown(false);
4120
+ return void 0;
4121
+ }
4122
+ const timer = setInterval(() => {
4123
+ if (detector.feed(latest.current, Date.now())) setShown(true);
4124
+ }, 150);
4125
+ return () => clearInterval(timer);
4126
+ }, [mutedByMe, detector]);
4127
+ (0, import_react10.useEffect)(() => {
4128
+ if (!shown) return void 0;
4129
+ const timer = setTimeout(() => setShown(false), 8e3);
4130
+ return () => clearTimeout(timer);
4131
+ }, [shown]);
4132
+ if (!shown || !mutedByMe) return null;
4133
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__notice-bar", role: "status", "data-meet-notice": "speaking-while-muted", children: [
4134
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, { width: 16, height: 16 }),
4135
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "You're muted" }),
4136
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4137
+ "button",
4138
+ {
4139
+ type: "button",
4140
+ className: "mx-meet__primary mx-meet__primary--small",
4141
+ onClick: () => {
4142
+ setShown(false);
4143
+ void room.toggleMicrophone();
4144
+ },
4145
+ children: "Unmute"
4146
+ }
4147
+ )
4148
+ ] });
4149
+ }
3769
4150
  function MediaNotices() {
3770
4151
  const media = useLocalMedia();
3771
4152
  const { region, where, enableAudio, acceptOffer, dismissOffer, dismissNotice, retry } = useMediaSupervisor();
@@ -3851,9 +4232,45 @@ function MeetingOverlays() {
3851
4232
  const reactions = useReactions();
3852
4233
  const { notice, dismiss } = useModerationNotice();
3853
4234
  const room = useRoom();
4235
+ const live = useLivePolicy();
4236
+ const snapshot = useMeetSnapshot();
4237
+ const controls = useHostControls();
4238
+ const isHostNow = useRole() === "host";
3854
4239
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
3855
4240
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ReactionOverlay, { reactions }),
3856
4241
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MediaNotices, {}),
4242
+ snapshot !== null && snapshot.phase === "in_call" && snapshot.participants.length >= 2 && !snapshot.participants.some((p) => p.role === "host" || p.role === "cohost") ? (
4243
+ // meeting-without-host: the call goes on; host-only controls are simply not on screen.
4244
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "host-absent", children: "The host has left the meeting. Host controls are not available until a host is back." })
4245
+ ) : null,
4246
+ live.consent !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__sheet-backdrop", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__sheet", role: "alertdialog", "aria-label": "This meeting is being recorded", "data-meet-notice": "recording-consent", children: [
4247
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h2", { className: "mx-meet__panel-title", children: "This meeting is being recorded" }),
4248
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__confirm-text", children: live.consent === "ok_or_leave" ? "By staying you agree to be recorded. You can leave now if you do not agree." : "Until you agree to the recording you can watch and listen, but not use your microphone or camera." }),
4249
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__menu-actions", children: [
4250
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__send", onClick: live.agree, children: live.consent === "ok_or_leave" ? "OK" : "I agree" }),
4251
+ live.consent === "ok_or_leave" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => void room.leave(), children: "Leave meeting" }) : null
4252
+ ] })
4253
+ ] }) }) : null,
4254
+ snapshot?.canReclaim === true && !isHostNow ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__notice-bar", role: "status", "data-meet-notice": "host-reclaim", children: [
4255
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "You started this meeting. Take back the host?" }),
4256
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4257
+ "button",
4258
+ {
4259
+ type: "button",
4260
+ className: "mx-meet__primary mx-meet__primary--small",
4261
+ onClick: () => void controls.run({ action: "reclaim_host" }),
4262
+ children: "Reclaim host"
4263
+ }
4264
+ )
4265
+ ] }) : null,
4266
+ live.micLocked || live.cameraLocked ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "mic-locked", children: live.micLocked && live.cameraLocked ? "Your microphone and camera are turned off by the organizer." : live.micLocked ? "Your microphone is turned off by the organizer." : "Your camera is turned off by the organizer." }) : null,
4267
+ live.timeWarning && live.minutesLeft !== null && live.minutesLeft > 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "time-warning", children: [
4268
+ "This meeting will end in ",
4269
+ live.minutesLeft,
4270
+ " ",
4271
+ live.minutesLeft === 1 ? "minute" : "minutes",
4272
+ " (time limit)."
4273
+ ] }) : null,
3857
4274
  notice !== null && notice.kind !== "removed" ? (
3858
4275
  // A host's request ("the host is asking you to unmute"), not an error:
3859
4276
  // an assertive status, so it is announced at once without being filed
@@ -3944,6 +4361,7 @@ function ChatPanel({
3944
4361
  }) {
3945
4362
  const snapshot = useMeetSnapshot();
3946
4363
  const room = useRoom();
4364
+ const live = useLivePolicy();
3947
4365
  const [draft, setDraft] = (0, import_react10.useState)("");
3948
4366
  const [recipient, setRecipient] = (0, import_react10.useState)(to);
3949
4367
  const listRef = (0, import_react10.useRef)(null);
@@ -3968,7 +4386,7 @@ function ChatPanel({
3968
4386
  snapshot?.chatLoaded !== true && messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("li", { className: "mx-meet__panel-note", children: "Reading earlier messages\u2026" }) : messages.length === 0 && snapshot?.chatHistoryError == null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("li", { className: "mx-meet__panel-note", children: "No messages yet. Messages to everyone stay with the meeting, so people who join later see them." }) : null,
3969
4387
  messages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ChatLine, { message, local, people }, message.id))
3970
4388
  ] }),
3971
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
4389
+ !live.canChat ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__panel-note", role: "status", "data-meet-notice": "chat-restricted", children: live.chatMode === "off" ? "Chat is turned off for this meeting." : "Chat is limited to the host and co-hosts in this meeting." }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3972
4390
  "form",
3973
4391
  {
3974
4392
  className: "mx-meet__composer mx-meet__composer--chat",
@@ -4340,6 +4758,13 @@ function PersonMenu({
4340
4758
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PeopleIcon, { width: 16, height: 16 }),
4341
4759
  " Make co-host"
4342
4760
  ] }) : null,
4761
+ !participant.isAgent && participant.role !== "cohost" && participant.role !== "host" ? participant.role === "presenter" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(MenuItem, { onSelect: () => act({ action: "revoke_presenter", identity: participant.identity }), children: [
4762
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ScreenShareIcon, { width: 16, height: 16 }),
4763
+ " Remove presenter"
4764
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(MenuItem, { onSelect: () => act({ action: "make_presenter", identity: participant.identity }), children: [
4765
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ScreenShareIcon, { width: 16, height: 16 }),
4766
+ " Make presenter"
4767
+ ] }) : null,
4343
4768
  !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
4344
4769
  MenuItem,
4345
4770
  {
@@ -4571,14 +4996,20 @@ async function playTestSound(sinkId) {
4571
4996
  audio.pause();
4572
4997
  await context.close();
4573
4998
  }
4574
- function useMicrophoneStream(deviceId) {
4999
+ function useMicrophoneStream(deviceId, report, attempt = 0) {
4575
5000
  const [stream, setStream] = (0, import_react10.useState)(null);
5001
+ const reportRef = (0, import_react10.useRef)(report);
5002
+ reportRef.current = report;
4576
5003
  (0, import_react10.useEffect)(() => {
4577
- if (deviceId === "off") return void 0;
5004
+ if (deviceId === "off") {
5005
+ reportRef.current?.("off");
5006
+ return void 0;
5007
+ }
4578
5008
  let live = true;
4579
5009
  let opened = null;
4580
5010
  const media = globalThis.navigator?.mediaDevices;
4581
5011
  if (media === void 0) return void 0;
5012
+ reportRef.current?.("prompting");
4582
5013
  void media.getUserMedia({ audio: deviceId === null ? true : { deviceId: { exact: deviceId } } }).then((next) => {
4583
5014
  if (!live) {
4584
5015
  next.getTracks().forEach((track) => track.stop());
@@ -4586,15 +5017,49 @@ function useMicrophoneStream(deviceId) {
4586
5017
  }
4587
5018
  opened = next;
4588
5019
  setStream(next);
4589
- }).catch(() => setStream(null));
5020
+ reportRef.current?.("on");
5021
+ }).catch((thrown) => {
5022
+ setStream(null);
5023
+ if (live) reportRef.current?.({ error: thrown });
5024
+ });
4590
5025
  return () => {
4591
5026
  live = false;
4592
5027
  opened?.getTracks().forEach((track) => track.stop());
4593
5028
  setStream(null);
4594
5029
  };
4595
- }, [deviceId]);
5030
+ }, [deviceId, attempt]);
4596
5031
  return stream;
4597
5032
  }
5033
+ function PreviewStatus({ onRetry }) {
5034
+ const { region, where } = useMediaSupervisor();
5035
+ const [waited, setWaited] = (0, import_react10.useState)(false);
5036
+ const prompting = region.preview.microphone.state === "prompting" || region.preview.camera.state === "prompting";
5037
+ (0, import_react10.useEffect)(() => {
5038
+ if (!prompting) {
5039
+ setWaited(false);
5040
+ return void 0;
5041
+ }
5042
+ const timer = setTimeout(() => setWaited(true), 800);
5043
+ return () => clearTimeout(timer);
5044
+ }, [prompting]);
5045
+ const refused = ["microphone", "camera"].flatMap((kind) => {
5046
+ const preview = region.preview[kind];
5047
+ const reason = region.permission[kind] === "denied" ? "denied" : preview.state === "unavailable" ? preview.unavailable?.reason ?? "failed" : null;
5048
+ return reason === null ? [] : [{ kind, reason }];
5049
+ });
5050
+ if (refused.length === 0) {
5051
+ if (!prompting || !waited) return null;
5052
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__panel-note", role: "status", "data-meet-notice": "permission-prompt", children: "Allow camera and microphone when your browser asks" });
5053
+ }
5054
+ const first = refused[0];
5055
+ const both = refused.length === 2 && refused[0].reason === refused[1].reason;
5056
+ const label = both ? deviceChipLabel("camera", first.reason).replace(/^Camera/, "Camera and mic") : refused.map((r) => deviceChipLabel(r.kind, r.reason)).join(" \xB7 ");
5057
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__warning", role: "status", "data-meet-notice": deviceNoticeCode(first.kind, first.reason), children: [
5058
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("strong", { children: label }),
5059
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ol", { className: "mx-meet__fix-steps", children: deviceFixSteps(first.kind, first.reason, where).map((step) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("li", { children: step }, step)) }),
5060
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Button, { type: "button", variant: "outline", size: "sm", onClick: onRetry, children: "Try again" })
5061
+ ] });
5062
+ }
4598
5063
  function KnockNotice({ onView }) {
4599
5064
  const { knocker, others, admit, dismiss } = useKnockNotice();
4600
5065
  if (knocker === null) return null;
@@ -4845,8 +5310,19 @@ function Captions({ lines = 3 }) {
4845
5310
  const ai = useMeetAi();
4846
5311
  const visible = ai.transcript.slice(-lines);
4847
5312
  const translation = useCaptionTranslation(visible);
4848
- if (visible.length === 0) return null;
4849
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", children: [
5313
+ const captions = useCaptions();
5314
+ if (!captions.on) return null;
5315
+ if (captions.state === "unavailable") {
5316
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__captions", role: "status", "data-meet-notice": "captions-unavailable", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__caption", children: [
5317
+ "Captions are unavailable. ",
5318
+ captions.reason,
5319
+ captions.remedy !== null ? ` ${captions.remedy}` : ""
5320
+ ] }) });
5321
+ }
5322
+ if (visible.length === 0) {
5323
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", "data-meet-caption": captions.state, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__caption mx-meet__caption--interim", children: captions.state === "starting" ? "Starting captions. They appear as people speak." : "Captions are on. They appear as people speak." }) });
5324
+ }
5325
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", "data-meet-caption": "live", children: [
4850
5326
  visible.map((line) => {
4851
5327
  const translated = translation.language === null ? void 0 : translation.translations[line.id];
4852
5328
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
@@ -4869,7 +5345,7 @@ function Captions({ lines = 3 }) {
4869
5345
  function ConsentNotice() {
4870
5346
  const notice = useConsentNotice();
4871
5347
  if (notice === null) return null;
4872
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__consent", role: "status", children: notice });
5348
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__consent", role: "status", "data-meet-notice": "notes-started", children: notice });
4873
5349
  }
4874
5350
  function AttendanceNotice() {
4875
5351
  const media = useLocalMedia();
@@ -4913,8 +5389,15 @@ function PreJoinBody({
4913
5389
  const [joining, setJoining] = (0, import_react10.useState)(false);
4914
5390
  const camera = devices?.selected.videoinput ?? host.devices.resolve("videoinput");
4915
5391
  const mic = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
4916
- const preview = useCameraPreview(mode === "video" ? camera : void 0, effects.effects);
4917
- const micStream = useMicrophoneStream(microphone ? mic : "off");
5392
+ const { reportPreview } = useMediaSupervisor();
5393
+ const [attempt, setAttempt] = (0, import_react10.useState)(0);
5394
+ const preview = useCameraPreview(
5395
+ mode === "video" ? camera : void 0,
5396
+ effects.effects,
5397
+ (outcome) => reportPreview("camera", outcome),
5398
+ attempt
5399
+ );
5400
+ const micStream = useMicrophoneStream(microphone ? mic : "off", (outcome) => reportPreview("microphone", outcome), attempt);
4918
5401
  const level = useAudioLevel(micStream);
4919
5402
  const [speakerTesting, setSpeakerTesting] = (0, import_react10.useState)(false);
4920
5403
  (0, import_react10.useEffect)(() => {
@@ -4958,6 +5441,7 @@ function PreJoinBody({
4958
5441
  ] }),
4959
5442
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__prejoin-panel", children: [
4960
5443
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h1", { className: "mx-meet__prejoin-title", children: "Ready to join?" }),
5444
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PreviewStatus, { onRetry: () => setAttempt((n) => n + 1) }),
4961
5445
  notice?.kind === "removed" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__warning", children: notice.text }) : null,
4962
5446
  devices?.labelsHidden === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__panel-note", children: [
4963
5447
  "Your browser is hiding device names until you allow camera and microphone access.",
@@ -5060,16 +5544,22 @@ function PreJoinBody({
5060
5544
  ] })
5061
5545
  ] });
5062
5546
  }
5063
- function useCameraPreview(deviceId, effects) {
5547
+ function useCameraPreview(deviceId, effects, report, attempt = 0) {
5548
+ const reportRef = (0, import_react10.useRef)(report);
5549
+ reportRef.current = report;
5064
5550
  const [preview, setPreview] = (0, import_react10.useState)(null);
5065
5551
  const [error, setError] = (0, import_react10.useState)(null);
5066
5552
  const current = (0, import_react10.useRef)(null);
5067
5553
  const initialEffect = (0, import_react10.useRef)(toVideoEffect(effects));
5068
5554
  initialEffect.current = toVideoEffect(effects);
5069
5555
  (0, import_react10.useEffect)(() => {
5070
- if (deviceId === void 0) return void 0;
5556
+ if (deviceId === void 0) {
5557
+ reportRef.current?.("off");
5558
+ return void 0;
5559
+ }
5071
5560
  let live = true;
5072
5561
  setError(null);
5562
+ reportRef.current?.("prompting");
5073
5563
  void createCameraPreview(deviceId, initialEffect.current).then((created) => {
5074
5564
  if (!live) {
5075
5565
  created.stop();
@@ -5077,8 +5567,10 @@ function useCameraPreview(deviceId, effects) {
5077
5567
  }
5078
5568
  current.current = created;
5079
5569
  setPreview(created);
5570
+ reportRef.current?.("on");
5080
5571
  }).catch((thrown) => {
5081
5572
  if (!live) return;
5573
+ reportRef.current?.({ error: thrown });
5082
5574
  const failure = thrown;
5083
5575
  const message = (failure.message ?? "").replace(/^meet\.[A-Za-z.()]+:\s*/, "");
5084
5576
  const sentence = message.length > 0 ? message.charAt(0).toUpperCase() + message.slice(1) : "";
@@ -5090,7 +5582,7 @@ function useCameraPreview(deviceId, effects) {
5090
5582
  current.current = null;
5091
5583
  setPreview(null);
5092
5584
  };
5093
- }, [deviceId]);
5585
+ }, [deviceId, attempt]);
5094
5586
  return {
5095
5587
  preview,
5096
5588
  error,
@@ -5230,6 +5722,14 @@ function MeetSkinRoom({
5230
5722
  }
5231
5723
  function ShareNotice() {
5232
5724
  const { share, actions } = useMeetCore();
5725
+ const [soundHintClosed, setSoundHintClosed] = (0, import_react10.useState)(null);
5726
+ if (share.state === "presenting" && share.audio === false && soundHintClosed !== share.at) {
5727
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "share-audio-missing", children: [
5728
+ "Sound not shared",
5729
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__remedy", children: "To include sound, share a tab and turn on Also share tab audio" }),
5730
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setSoundHintClosed(share.at), children: "Dismiss" })
5731
+ ] });
5732
+ }
5233
5733
  if (share.state !== "refused") return null;
5234
5734
  const text = share.reason === "unsupported" ? "You can't share your screen from this browser" : share.reason === "blocked" ? "Screen sharing is blocked" : "Couldn't start sharing your screen";
5235
5735
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "share-unavailable", children: [
@@ -5342,6 +5842,27 @@ function ExitScreen({ onLeave }) {
5342
5842
  ] });
5343
5843
  break;
5344
5844
  case "disconnected":
5845
+ if (failure?.stage === "join" && (failure.reason ?? null) === null) {
5846
+ title = "Can't connect to the meeting";
5847
+ line = connection === "offline" ? "You're offline. Check your internet connection." : "Check your network. A VPN, firewall or proxy may be blocking the call.";
5848
+ notice = "cannot-connect";
5849
+ actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
5850
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5851
+ import_design_system.Button,
5852
+ {
5853
+ type: "button",
5854
+ disabled: busy,
5855
+ onClick: () => {
5856
+ setBusy(true);
5857
+ void actions.rejoin().catch(() => void 0).finally(() => setBusy(false));
5858
+ },
5859
+ children: "Try again"
5860
+ }
5861
+ ),
5862
+ home
5863
+ ] });
5864
+ break;
5865
+ }
5345
5866
  title = "You lost your connection to the meeting";
5346
5867
  line = connection === "offline" ? "You're offline. You'll rejoin automatically when your internet is back." : connection === "server_unreachable" ? "Can't reach the meeting server. You'll rejoin automatically when it answers." : failure?.remedy ?? "Check your internet connection, then rejoin.";
5347
5868
  notice = "disconnected";