@ai-matrx/meet 0.11.42 → 0.11.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- export { A as AiPanel, a as AttendanceNotice, b as Avatar, C as Captions, c as ChatPanel, d as ConnectionDot, e as ConsentNotice, f as ControlBar, g as ControlButton, H as HostMenu, L as LobbyPanel, M as MeetSkinRoom, h as MeetingOverlays, i as MenuItem, P as PanelShell, j as ParticipantTile, k as PeoplePanel, l as PersonMenu, m as Popover, n as PreJoin, Q as QualityBars, R as ReactionOverlay, o as RecordingIndicator, S as SettingsPanel, p as ShortcutsSheet, q as Stage, T as Toast } from '../components-roCEgyFH.cjs';
1
+ export { A as AiPanel, a as AttendanceNotice, b as Avatar, C as Captions, c as ChatPanel, d as ConnectionDot, e as ConsentNotice, f as ControlBar, g as ControlButton, H as HostMenu, L as LobbyPanel, M as MeetSkinRoom, h as MeetingOverlays, i as MenuItem, P as PanelShell, j as ParticipantTile, k as PeoplePanel, l as PersonMenu, m as Popover, n as PreJoin, Q as QualityBars, R as ReactionOverlay, o as RecordingIndicator, S as SettingsPanel, p as ShortcutsSheet, q as Stage, T as Toast } from '../components-BxK993iW.cjs';
2
2
  import { ReactNode, SVGProps } from 'react';
3
- import { C as CollabPanel } from '../collab-Bf5evcLO.cjs';
3
+ import { C as CollabPanel } from '../collab-BNJNgXFB.cjs';
4
4
 
5
5
  /**
6
6
  * POLLS, Q&A, THE SHARED WHITEBOARD AND BREAKOUT ROOMS — the in-room surfaces (Meet wave 5).
@@ -73,6 +73,8 @@ declare function ChatIcon(props: IconProps): React.ReactElement;
73
73
  declare function PeopleIcon(props: IconProps): React.ReactElement;
74
74
  declare function RecordIcon(props: IconProps): React.ReactElement;
75
75
  declare function SparklesIcon(props: IconProps): React.ReactElement;
76
+ declare function CaptionsIcon(props: IconProps): React.ReactElement;
77
+ declare function NotesIcon(props: IconProps): React.ReactElement;
76
78
  declare function LockIcon(props: IconProps): React.ReactElement;
77
79
  declare function LayoutIcon(props: IconProps): React.ReactElement;
78
80
  declare function SettingsIcon(props: IconProps): React.ReactElement;
@@ -110,4 +112,4 @@ declare function HighlighterIcon(props: IconProps): React.ReactElement;
110
112
  /** Lucide `undo-2`. */
111
113
  declare function UndoIcon(props: IconProps): React.ReactElement;
112
114
 
113
- export { ActivitiesControl, ActivitiesIcon, BreakoutBanner, BreakoutIcon, BreakoutsPanel, CameraIcon, CameraOffIcon, CaptionLanguageSettings, ChatIcon, CheckIcon, CloseIcon, EraserIcon, FlagIcon, HandIcon, HighlighterIcon, KeyboardIcon, LayoutIcon, LockIcon, MicIcon, MicOffIcon, MoreIcon, PencilIcon, PeopleIcon, PhoneIcon, PhoneOffIcon, PinIcon, PollIcon, PollsPanel, QaPanel, QuestionIcon, RecordIcon, ScreenShareIcon, SearchIcon, SettingsIcon, SignalIcon, SmileIcon, SparklesIcon, SpeakerIcon, SpotlightIcon, UndoIcon, UpvoteIcon, WhiteboardIcon, WhiteboardPanel, WifiLowIcon };
115
+ export { ActivitiesControl, ActivitiesIcon, BreakoutBanner, BreakoutIcon, BreakoutsPanel, CameraIcon, CameraOffIcon, CaptionLanguageSettings, CaptionsIcon, ChatIcon, CheckIcon, CloseIcon, EraserIcon, FlagIcon, HandIcon, HighlighterIcon, KeyboardIcon, LayoutIcon, LockIcon, MicIcon, MicOffIcon, MoreIcon, NotesIcon, PencilIcon, PeopleIcon, PhoneIcon, PhoneOffIcon, PinIcon, PollIcon, PollsPanel, QaPanel, QuestionIcon, RecordIcon, ScreenShareIcon, SearchIcon, SettingsIcon, SignalIcon, SmileIcon, SparklesIcon, SpeakerIcon, SpotlightIcon, UndoIcon, UpvoteIcon, WhiteboardIcon, WhiteboardPanel, WifiLowIcon };
@@ -1,6 +1,6 @@
1
- export { A as AiPanel, a as AttendanceNotice, b as Avatar, C as Captions, c as ChatPanel, d as ConnectionDot, e as ConsentNotice, f as ControlBar, g as ControlButton, H as HostMenu, L as LobbyPanel, M as MeetSkinRoom, h as MeetingOverlays, i as MenuItem, P as PanelShell, j as ParticipantTile, k as PeoplePanel, l as PersonMenu, m as Popover, n as PreJoin, Q as QualityBars, R as ReactionOverlay, o as RecordingIndicator, S as SettingsPanel, p as ShortcutsSheet, q as Stage, T as Toast } from '../components-Frpvls4N.js';
1
+ export { A as AiPanel, a as AttendanceNotice, b as Avatar, C as Captions, c as ChatPanel, d as ConnectionDot, e as ConsentNotice, f as ControlBar, g as ControlButton, H as HostMenu, L as LobbyPanel, M as MeetSkinRoom, h as MeetingOverlays, i as MenuItem, P as PanelShell, j as ParticipantTile, k as PeoplePanel, l as PersonMenu, m as Popover, n as PreJoin, Q as QualityBars, R as ReactionOverlay, o as RecordingIndicator, S as SettingsPanel, p as ShortcutsSheet, q as Stage, T as Toast } from '../components-BDtGzHf4.js';
2
2
  import { ReactNode, SVGProps } from 'react';
3
- import { C as CollabPanel } from '../collab-Bf5evcLO.js';
3
+ import { C as CollabPanel } from '../collab-BNJNgXFB.js';
4
4
 
5
5
  /**
6
6
  * POLLS, Q&A, THE SHARED WHITEBOARD AND BREAKOUT ROOMS — the in-room surfaces (Meet wave 5).
@@ -73,6 +73,8 @@ declare function ChatIcon(props: IconProps): React.ReactElement;
73
73
  declare function PeopleIcon(props: IconProps): React.ReactElement;
74
74
  declare function RecordIcon(props: IconProps): React.ReactElement;
75
75
  declare function SparklesIcon(props: IconProps): React.ReactElement;
76
+ declare function CaptionsIcon(props: IconProps): React.ReactElement;
77
+ declare function NotesIcon(props: IconProps): React.ReactElement;
76
78
  declare function LockIcon(props: IconProps): React.ReactElement;
77
79
  declare function LayoutIcon(props: IconProps): React.ReactElement;
78
80
  declare function SettingsIcon(props: IconProps): React.ReactElement;
@@ -110,4 +112,4 @@ declare function HighlighterIcon(props: IconProps): React.ReactElement;
110
112
  /** Lucide `undo-2`. */
111
113
  declare function UndoIcon(props: IconProps): React.ReactElement;
112
114
 
113
- export { ActivitiesControl, ActivitiesIcon, BreakoutBanner, BreakoutIcon, BreakoutsPanel, CameraIcon, CameraOffIcon, CaptionLanguageSettings, ChatIcon, CheckIcon, CloseIcon, EraserIcon, FlagIcon, HandIcon, HighlighterIcon, KeyboardIcon, LayoutIcon, LockIcon, MicIcon, MicOffIcon, MoreIcon, PencilIcon, PeopleIcon, PhoneIcon, PhoneOffIcon, PinIcon, PollIcon, PollsPanel, QaPanel, QuestionIcon, RecordIcon, ScreenShareIcon, SearchIcon, SettingsIcon, SignalIcon, SmileIcon, SparklesIcon, SpeakerIcon, SpotlightIcon, UndoIcon, UpvoteIcon, WhiteboardIcon, WhiteboardPanel, WifiLowIcon };
115
+ export { ActivitiesControl, ActivitiesIcon, BreakoutBanner, BreakoutIcon, BreakoutsPanel, CameraIcon, CameraOffIcon, CaptionLanguageSettings, CaptionsIcon, ChatIcon, CheckIcon, CloseIcon, EraserIcon, FlagIcon, HandIcon, HighlighterIcon, KeyboardIcon, LayoutIcon, LockIcon, MicIcon, MicOffIcon, MoreIcon, NotesIcon, PencilIcon, PeopleIcon, PhoneIcon, PhoneOffIcon, PinIcon, PollIcon, PollsPanel, QaPanel, QuestionIcon, RecordIcon, ScreenShareIcon, SearchIcon, SettingsIcon, SignalIcon, SmileIcon, SparklesIcon, SpeakerIcon, SpotlightIcon, UndoIcon, UpvoteIcon, WhiteboardIcon, WhiteboardPanel, WifiLowIcon };
@@ -160,6 +160,8 @@ var MEET_ROUTES = {
160
160
  noteTakerStop: "/api/v1/meet/agent/stop",
161
161
  /** In-meeting Q&A. Streams NDJSON; body is `{meeting_id, question}` only. */
162
162
  intelligenceAsk: "/api/v1/meet/intelligence/ask",
163
+ /** Anyone in the meeting (guests with the room pass included) asks for captions for themselves. */
164
+ noteTakerCaptions: "/api/v1/meet/agent/captions",
163
165
  /** A host ends the meeting for everyone. Served by MRI-A6. */
164
166
  meetingEnd: "/api/v1/meet/end",
165
167
  /**
@@ -255,6 +257,31 @@ function recordingLabel(state) {
255
257
  }
256
258
  }
257
259
 
260
+ // src/core/live-policy.ts
261
+ var DEFAULT_LIVE_POLICY = {
262
+ chatMode: "everyone",
263
+ avLock: "off",
264
+ timeLimitMinutes: 0,
265
+ timeLimitWarningMinutes: 5,
266
+ recordingConsent: "notice"
267
+ };
268
+ var MODERATORS = /* @__PURE__ */ new Set(["host", "cohost"]);
269
+ function isLockedFor(policy, role, kind) {
270
+ if (MODERATORS.has(role)) return false;
271
+ return policy.avLock === "both" || (kind === "microphone" ? policy.avLock === "mic" : policy.avLock === "camera");
272
+ }
273
+ function consentPending(policy, role, recordingOn, given) {
274
+ return recordingOn && !given && policy.recordingConsent !== "notice" && !MODERATORS.has(role);
275
+ }
276
+ function canChat(policy, role) {
277
+ if (policy.chatMode === "everyone") return true;
278
+ return policy.chatMode === "hosts_only" && MODERATORS.has(role);
279
+ }
280
+ function minutesLeft(policy, startedAtMs, nowMs) {
281
+ if (policy.timeLimitMinutes <= 0 || startedAtMs === null) return null;
282
+ return Math.ceil((startedAtMs + policy.timeLimitMinutes * 6e4 - nowMs) / 6e4);
283
+ }
284
+
258
285
  // src/core/attendance.ts
259
286
  var IDLE_MEDIA_REGION = {
260
287
  permission: { microphone: "unknown", camera: "unknown" },
@@ -347,9 +374,17 @@ function autoAssign(identities, rooms, exclude = []) {
347
374
  return out;
348
375
  }
349
376
  function breakoutRemainingMs(plan, now) {
350
- if (plan === null || plan.state !== "open" || plan.endsAt === null) return null;
351
- const end = Date.parse(plan.endsAt);
352
- return Number.isNaN(end) ? null : Math.max(0, end - now);
377
+ if (plan === null || plan.state !== "open") return null;
378
+ const ends = [plan.endsAt, plan.closingAt].map((stamp) => stamp === null ? Number.NaN : Date.parse(stamp)).filter((time) => !Number.isNaN(time));
379
+ if (ends.length === 0) return null;
380
+ return Math.max(0, Math.min(...ends) - now);
381
+ }
382
+ var BREAKOUT_COUNTDOWN_MS = 3e4;
383
+ function breakoutClosingMs(plan, now) {
384
+ if (plan === null || plan.state !== "open") return null;
385
+ const remaining = breakoutRemainingMs(plan, now);
386
+ if (remaining === null) return null;
387
+ return plan.closingAt !== null || remaining <= BREAKOUT_COUNTDOWN_MS ? remaining : null;
353
388
  }
354
389
  function defaultCaptionLanguage(settings, viewerLanguage) {
355
390
  const configured = settings?.captionTranslationLanguage.trim() ?? "";
@@ -557,11 +592,13 @@ function webMediaPlatform() {
557
592
 
558
593
  // src/core/observation.ts
559
594
  function observedRole(role, standing) {
560
- if (role === "host" || role === "cohost") return role;
595
+ if (role === "host" || role === "cohost" || role === "presenter") return role;
561
596
  return isLinkFollower(standing) ? "guest" : "participant";
562
597
  }
563
- function observedDevice(track, permission = "unknown") {
598
+ function observedDevice(track, permission = "unknown", held = null) {
599
+ if (held === "locked") return "locked";
564
600
  if (track.enabled) return "on";
601
+ if (held === "muted_by_host") return "muted_by_host";
565
602
  const reason = deviceUnavailableReason(track, permission);
566
603
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
567
604
  }
@@ -598,10 +635,20 @@ function observe(input) {
598
635
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
599
636
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
600
637
  audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
601
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
602
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone) : null
638
+ camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera, lockedFor(snapshot, "camera", inCall)) : null,
639
+ microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone, lockedFor(snapshot, "microphone", inCall)) : null
603
640
  };
604
641
  }
642
+ function lockedFor(snapshot, kind, inCall) {
643
+ if (!inCall) return null;
644
+ const role = effectiveRole(snapshot);
645
+ if (isLockedFor(snapshot.livePolicy, role, kind)) return "locked";
646
+ if (snapshot.livePolicy.recordingConsent === "view_only_until_agree" && consentPending(snapshot.livePolicy, role, snapshot.recording === "recording", snapshot.recordingConsentGiven)) {
647
+ return "locked";
648
+ }
649
+ if (kind === "microphone" && snapshot.mutedByHost) return "muted_by_host";
650
+ return null;
651
+ }
605
652
  function staticObservation(phase, reason = null) {
606
653
  return {
607
654
  phase,
@@ -704,7 +751,7 @@ function exactNoteTakerParticipant(meeting, participants) {
704
751
  }
705
752
  function noteTakerStatus(args) {
706
753
  const present = exactNoteTakerParticipant(args.meeting, args.participants) !== null;
707
- const raw = args.meeting?.metadata[NOTE_TAKER_METADATA_KEY];
754
+ const raw = args.meeting?.metadata?.[NOTE_TAKER_METADATA_KEY];
708
755
  const row = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
709
756
  const recorded = readState(row["state"]);
710
757
  const detail = typeof row["reason"] === "string" && row["reason"].length > 0 ? row["reason"] : null;
@@ -1457,6 +1504,45 @@ function useMeetAi() {
1457
1504
  }, [ai, meeting, dispatchNoteTaker])
1458
1505
  };
1459
1506
  }
1507
+ function useCaptions() {
1508
+ const host = useMeetHost();
1509
+ const snapshot = useMeetSnapshot();
1510
+ const ai = useMeetAi();
1511
+ const on = snapshot?.captionsOn === true;
1512
+ const refusal = snapshot?.captionsRefusal ?? null;
1513
+ const taker = ai.noteTaker;
1514
+ const meeting = snapshot?.meeting ?? null;
1515
+ const request = useCallback2(() => {
1516
+ if (host === null || meeting === null) return;
1517
+ void host.ai.requestCaptions({ meetingId: meeting.id });
1518
+ }, [host, meeting]);
1519
+ const toggle = useCallback2(() => {
1520
+ if (host === null) return;
1521
+ const next = !on;
1522
+ host.store.patch({ captionsOn: next, captionsRefusal: null });
1523
+ if (next && taker.state !== "joining" && taker.state !== "transcribing") request();
1524
+ }, [host, on, taker.state, request]);
1525
+ let state = "off";
1526
+ let reason = null;
1527
+ let remedy = null;
1528
+ if (on) {
1529
+ if (taker.state === "transcribing" && taker.present) {
1530
+ state = "live";
1531
+ } else if (taker.state === "joining") {
1532
+ state = "starting";
1533
+ } else if (refusal !== null && refusal.reason.length > 0) {
1534
+ state = "unavailable";
1535
+ reason = refusal.reason;
1536
+ remedy = refusal.remedy.length > 0 ? refusal.remedy : null;
1537
+ } else if (taker.state === "ended" || taker.state === "handoff_pending") {
1538
+ state = "unavailable";
1539
+ reason = noteTakerLabel(taker);
1540
+ } else {
1541
+ state = "starting";
1542
+ }
1543
+ }
1544
+ return { on, state, reason, remedy, toggle };
1545
+ }
1460
1546
  function useEndMeeting() {
1461
1547
  const host = useMeetHost();
1462
1548
  const snapshot = useMeetSnapshot();
@@ -1494,7 +1580,7 @@ function useConsentNotice() {
1494
1580
  const snapshot = useMeetSnapshot();
1495
1581
  const agent = noteTakerParticipant(snapshot?.participants ?? []);
1496
1582
  if (agent === null) return null;
1497
- return `${agent.displayName} is in this meeting: it is transcribing what is said and writing notes everyone here can read.`;
1583
+ return `AI notes are on. ${agent.displayName} is in this meeting, transcribing what is said and writing notes everyone here can read.`;
1498
1584
  }
1499
1585
  function useMediaSupervisor() {
1500
1586
  const host = useMeetHost();
@@ -1742,6 +1828,38 @@ function useGuestRecordAccess(meeting) {
1742
1828
  )
1743
1829
  };
1744
1830
  }
1831
+ function useLivePolicy() {
1832
+ const snapshot = useMeetSnapshot();
1833
+ const role = useRole();
1834
+ const [now, setNow] = useState2(() => Date.now());
1835
+ const limited = (snapshot?.livePolicy.timeLimitMinutes ?? 0) > 0;
1836
+ useEffect3(() => {
1837
+ if (!limited) return void 0;
1838
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
1839
+ return () => clearInterval(timer);
1840
+ }, [limited]);
1841
+ const policy = snapshot?.livePolicy ?? DEFAULT_LIVE_POLICY;
1842
+ const left = minutesLeft(policy, snapshot?.sessionStartedAt ?? null, now);
1843
+ const host = useMeetHost();
1844
+ const pending = consentPending(policy, role, snapshot?.recording === "recording", snapshot?.recordingConsentGiven === true);
1845
+ const viewOnly = pending && policy.recordingConsent === "view_only_until_agree";
1846
+ const idle = snapshot !== null && snapshot.recording !== "recording" && snapshot.recording !== "starting" && snapshot.recording !== "stopping";
1847
+ const given = snapshot?.recordingConsentGiven === true;
1848
+ const store = host?.store ?? null;
1849
+ useEffect3(() => {
1850
+ if (idle && given) store?.patch({ recordingConsentGiven: false });
1851
+ }, [idle, given, store]);
1852
+ return {
1853
+ canChat: canChat(policy, role),
1854
+ chatMode: policy.chatMode,
1855
+ micLocked: isLockedFor(policy, role, "microphone") || viewOnly,
1856
+ cameraLocked: isLockedFor(policy, role, "camera") || viewOnly,
1857
+ consent: pending ? policy.recordingConsent === "ok_or_leave" ? "ok_or_leave" : "view_only_until_agree" : null,
1858
+ agree: () => store?.patch({ recordingConsentGiven: true }),
1859
+ minutesLeft: left,
1860
+ timeWarning: left !== null && left <= policy.timeLimitWarningMinutes
1861
+ };
1862
+ }
1745
1863
 
1746
1864
  // src/react/collab-hooks.ts
1747
1865
  import { useEffect as useEffect4, useRef as useRef4 } from "react";
@@ -1957,6 +2075,18 @@ function SparklesIcon(props) {
1957
2075
  /* @__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" })
1958
2076
  ] });
1959
2077
  }
2078
+ function CaptionsIcon(props) {
2079
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
2080
+ /* @__PURE__ */ jsx4("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
2081
+ /* @__PURE__ */ jsx4("path", { d: "M10 10.5a2 2 0 1 0 0 3M16 10.5a2 2 0 1 0 0 3" })
2082
+ ] });
2083
+ }
2084
+ function NotesIcon(props) {
2085
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
2086
+ /* @__PURE__ */ jsx4("path", { d: "M4 20l1-4L16.5 4.5a2.1 2.1 0 0 1 3 3L8 19l-4 1Z" }),
2087
+ /* @__PURE__ */ jsx4("path", { d: "M14 7l3 3" })
2088
+ ] });
2089
+ }
1960
2090
  function LockIcon(props) {
1961
2091
  return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
1962
2092
  /* @__PURE__ */ jsx4("rect", { x: "4", y: "10", width: "16", height: "11", rx: "2" }),
@@ -2875,7 +3005,7 @@ function BreakoutRun({ plan }) {
2875
3005
  type: "button",
2876
3006
  className: "mx-meet__primary mx-meet__primary--small",
2877
3007
  onClick: () => void act({ action: "breakout_close" }).then(show),
2878
- children: "Bring everyone back"
3008
+ children: "Close rooms"
2879
3009
  }
2880
3010
  ) : null
2881
3011
  ] });
@@ -2893,9 +3023,14 @@ function BreakoutBanner() {
2893
3023
  const { collab, act, moveTo, localIdentity, breakoutRoomId } = useCollab();
2894
3024
  const plan = collab?.breakout ?? null;
2895
3025
  const open = plan?.state === "open" ? plan : null;
2896
- const now = useNow(open?.endsAt != null);
3026
+ const now = useNow(open?.endsAt != null || open?.closingAt != null);
2897
3027
  const remaining = breakoutRemainingMs(open, now);
3028
+ const closing = breakoutClosingMs(open, now);
2898
3029
  const assigned = assignedRoom(open, localIdentity);
3030
+ const [later, setLater] = useState3(null);
3031
+ const [autoSince, setAutoSince] = useState3(null);
3032
+ const AUTO_MOVE_MS = 1e4;
3033
+ const autoLeft = open?.moveMode === "auto" && assigned !== null && autoSince?.id === open.id && later !== open.id ? Math.max(0, AUTO_MOVE_MS - (now - autoSince.at)) : null;
2899
3034
  const [dismissedBroadcast, setDismissedBroadcast] = useState3(null);
2900
3035
  const [failure, setFailure] = useState3(null);
2901
3036
  const expireAsked = useRef5(null);
@@ -2908,6 +3043,19 @@ function BreakoutBanner() {
2908
3043
  if ((here ?? null) === request.roomId) return;
2909
3044
  void moveTo(request.roomId).then((result) => setFailure(result.ok ? null : result.message));
2910
3045
  }, [moveAt, host, moveTo]);
3046
+ const joinAssigned = async () => {
3047
+ if (assigned === null) return;
3048
+ const said = await act({ action: "breakout_join", roomId: assigned.id });
3049
+ const result = said.ok ? await moveTo(assigned.id) : said;
3050
+ setFailure(result.ok ? null : result.message);
3051
+ };
3052
+ useEffect5(() => {
3053
+ if (open === null || open.moveMode !== "auto" || assigned === null || breakoutRoomId !== null) return;
3054
+ if (autoSince?.id !== open.id) setAutoSince({ id: open.id, at: Date.now() });
3055
+ }, [open, assigned, breakoutRoomId, autoSince]);
3056
+ useEffect5(() => {
3057
+ if (autoLeft === 0) void joinAssigned();
3058
+ }, [autoLeft]);
2911
3059
  useEffect5(() => {
2912
3060
  if (open === null || remaining === null || remaining > 0) return;
2913
3061
  const asked = expireAsked.current;
@@ -2921,27 +3069,25 @@ function BreakoutBanner() {
2921
3069
  if (open === null && failure === null) return null;
2922
3070
  return /* @__PURE__ */ jsxs2("div", { className: "mx-meet__breakout-banner", role: "status", children: [
2923
3071
  failure !== null ? /* @__PURE__ */ jsx5("span", { className: "mx-meet__collab-error", children: failure }) : null,
3072
+ closing !== null ? /* @__PURE__ */ jsxs2("span", { role: "status", "data-meet-notice": "breakout-closing", children: [
3073
+ "Rooms closing in ",
3074
+ Math.ceil(closing / 1e3),
3075
+ room !== null ? " \u2014 you return to the main meeting" : ""
3076
+ ] }) : null,
2924
3077
  room !== null ? /* @__PURE__ */ jsxs2("span", { children: [
2925
3078
  /* @__PURE__ */ jsx5("strong", { children: room.name }),
2926
- remaining !== null ? ` \xB7 ${clock(remaining)} left` : "",
3079
+ remaining !== null && closing === null ? ` \xB7 ${clock(remaining)} left` : "",
2927
3080
  /* @__PURE__ */ jsx5("button", { type: "button", className: "mx-meet__chip", onClick: () => void moveTo(null), children: "Return to main room" })
3081
+ ] }) : assigned !== null && later !== open?.id ? /* @__PURE__ */ jsxs2("span", { children: [
3082
+ autoLeft !== null ? `You're being moved to ${assigned.name} in ${Math.ceil(autoLeft / 1e3)} s` : `You're invited to ${assigned.name}`,
3083
+ remaining !== null && closing === null ? ` \xB7 ${clock(remaining)} left` : "",
3084
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void joinAssigned(), children: "Join room" }),
3085
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "mx-meet__chip", onClick: () => setLater(open?.id ?? null), children: "Later" })
2928
3086
  ] }) : assigned !== null ? /* @__PURE__ */ jsxs2("span", { children: [
2929
- "You are assigned to ",
2930
- /* @__PURE__ */ jsx5("strong", { children: assigned.name }),
2931
- remaining !== null ? ` \xB7 ${clock(remaining)} left` : "",
2932
- /* @__PURE__ */ jsx5(
2933
- "button",
2934
- {
2935
- type: "button",
2936
- className: "mx-meet__primary mx-meet__primary--small",
2937
- onClick: () => void act({ action: "breakout_join", roomId: assigned.id }).then(async (said) => {
2938
- const result = said.ok ? await moveTo(assigned.id) : said;
2939
- setFailure(result.ok ? null : result.message);
2940
- }),
2941
- children: "Join"
2942
- }
2943
- )
2944
- ] }) : open?.allowChoose ? /* @__PURE__ */ jsx5("span", { children: "Breakout rooms are open \u2014 pick one under Activities." }) : null,
3087
+ assigned.name,
3088
+ " is open",
3089
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "mx-meet__chip", onClick: () => void joinAssigned(), children: "Join room" })
3090
+ ] }) : open?.allowChoose ? /* @__PURE__ */ jsx5("span", { children: "Rooms are open \u2014 pick one under Activities." }) : null,
2945
3091
  showBroadcast ? /* @__PURE__ */ jsxs2("span", { className: "mx-meet__breakout-broadcast", children: [
2946
3092
  /* @__PURE__ */ jsxs2("strong", { children: [
2947
3093
  broadcast.by,
@@ -2989,6 +3135,33 @@ function CaptionLanguageSettings() {
2989
3135
  collab?.captionTranslationError != null ? /* @__PURE__ */ jsx5("p", { className: "mx-meet__panel-note mx-meet__collab-error", children: collab.captionTranslationError }) : null
2990
3136
  ] });
2991
3137
  }
3138
+ function CaptionsControl() {
3139
+ const captions = useCaptions();
3140
+ return /* @__PURE__ */ jsx5(
3141
+ ControlButton,
3142
+ {
3143
+ label: captions.on ? "Turn off captions" : "Turn on captions",
3144
+ active: captions.on,
3145
+ pressed: captions.on,
3146
+ onClick: captions.toggle,
3147
+ children: /* @__PURE__ */ jsx5(CaptionsIcon, {})
3148
+ }
3149
+ );
3150
+ }
3151
+ function TakeNotesControl() {
3152
+ const ai = useMeetAi();
3153
+ if (!ai.canControlNoteTaker) return null;
3154
+ const running = ai.noteTaker.state === "joining" || ai.noteTaker.state === "transcribing";
3155
+ return /* @__PURE__ */ jsx5(
3156
+ ControlButton,
3157
+ {
3158
+ label: running ? "Stop taking notes" : "Take notes for me",
3159
+ active: running,
3160
+ onClick: () => void (running ? ai.stopNoteTaker() : ai.startNoteTaker()),
3161
+ children: /* @__PURE__ */ jsx5(NotesIcon, {})
3162
+ }
3163
+ );
3164
+ }
2992
3165
 
2993
3166
  // src/skins/meet/components.tsx
2994
3167
  import { Fragment, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -3039,6 +3212,7 @@ function ControlButton({
3039
3212
  label,
3040
3213
  active = false,
3041
3214
  danger = false,
3215
+ pressed,
3042
3216
  onClick,
3043
3217
  children
3044
3218
  }) {
@@ -3053,26 +3227,49 @@ function ControlButton({
3053
3227
  ].filter(Boolean).join(" "),
3054
3228
  "aria-label": label,
3055
3229
  title: label,
3230
+ ...pressed !== void 0 ? { "aria-pressed": pressed } : {},
3056
3231
  onClick,
3057
3232
  children
3058
3233
  }
3059
3234
  );
3060
3235
  }
3061
3236
  function RecordingIndicator() {
3062
- const { state, label } = useRecording();
3063
- if (label === null) return null;
3064
- return /* @__PURE__ */ jsxs3(
3065
- "span",
3066
- {
3067
- className: `mx-meet__recording mx-meet__recording--${state}`,
3068
- role: "status",
3069
- "aria-live": "polite",
3070
- children: [
3071
- /* @__PURE__ */ jsx6(RecordIcon, { width: 10, height: 10 }),
3072
- label
3073
- ]
3237
+ const { state, label, canStart } = useRecording();
3238
+ const snapshot = useMeetSnapshot();
3239
+ const previous = useRef6(state);
3240
+ const [stoppedAt, setStoppedAt] = useState4(null);
3241
+ useEffect6(() => {
3242
+ const was = previous.current;
3243
+ previous.current = state;
3244
+ if ((was === "recording" || was === "starting") && state !== "recording" && state !== "starting") {
3245
+ setStoppedAt(Date.now());
3246
+ const timer = setTimeout(() => setStoppedAt(null), 1e4);
3247
+ return () => clearTimeout(timer);
3074
3248
  }
3075
- );
3249
+ return void 0;
3250
+ }, [state]);
3251
+ const meetingId = snapshot?.meeting?.id ?? null;
3252
+ if (label === null && stoppedAt === null) return null;
3253
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
3254
+ label !== null ? /* @__PURE__ */ jsxs3(
3255
+ "span",
3256
+ {
3257
+ className: `mx-meet__recording mx-meet__recording--${state}`,
3258
+ role: "status",
3259
+ "aria-live": "polite",
3260
+ children: [
3261
+ /* @__PURE__ */ jsx6(RecordIcon, { width: 10, height: 10 }),
3262
+ label
3263
+ ]
3264
+ }
3265
+ ) : null,
3266
+ stoppedAt !== null ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__recording", role: "status", "data-meet-notice": "recording-stopped", children: "Recording stopped" }) : null,
3267
+ state === "processing" ? (
3268
+ // rec-processing: the host is told it is being made ready (the indicator says Processing…).
3269
+ /* @__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." })
3270
+ ) : null,
3271
+ state === "available" && canStart && meetingId !== null ? /* @__PURE__ */ jsx6("a", { className: "mx-meet__link", href: `/meetings/${meetingId}`, target: "_blank", rel: "noreferrer", children: "View recording" }) : null
3272
+ ] });
3076
3273
  }
3077
3274
  function ConnectionDot({ quality }) {
3078
3275
  if (quality === "excellent" || quality === "good" || quality === "unknown") return null;
@@ -3228,6 +3425,7 @@ function ControlBar({ onLeave }) {
3228
3425
  const controls = useHostControls();
3229
3426
  const queue = useHandQueue();
3230
3427
  const micUnavailable = useLocalMedia().microphone.unavailable;
3428
+ const live = useLivePolicy();
3231
3429
  const unread = useUnreadChat(panel === "chat");
3232
3430
  const [whiteboard, setWhiteboard] = useState4(false);
3233
3431
  const app = useMeetAppPanel();
@@ -3295,7 +3493,7 @@ function ControlBar({ onLeave }) {
3295
3493
  panel !== "people" ? /* @__PURE__ */ jsx6(KnockNotice, { onView: () => setPanel("people") }) : null,
3296
3494
  controls.notice !== null && panel !== "people" ? /* @__PURE__ */ jsx6(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
3297
3495
  /* @__PURE__ */ jsxs3("div", { className: "mx-meet__controls", children: [
3298
- /* @__PURE__ */ jsx6(
3496
+ live.micLocked ? null : /* @__PURE__ */ jsx6(
3299
3497
  ControlButton,
3300
3498
  {
3301
3499
  label: micUnavailable !== null ? `Unmute \u2014 ${micUnavailable.remedy}` : room.local?.micEnabled === true ? "Mute (\u2318D)" : "Unmute (\u2318D)",
@@ -3304,7 +3502,7 @@ function ControlBar({ onLeave }) {
3304
3502
  children: room.local?.micEnabled === true ? /* @__PURE__ */ jsx6(MicIcon, {}) : /* @__PURE__ */ jsx6(MicOffIcon, {})
3305
3503
  }
3306
3504
  ),
3307
- /* @__PURE__ */ jsx6(
3505
+ live.cameraLocked ? null : /* @__PURE__ */ jsx6(
3308
3506
  ControlButton,
3309
3507
  {
3310
3508
  label: room.local?.cameraEnabled === true ? "Stop video (\u2318E)" : "Start video (\u2318E)",
@@ -3313,6 +3511,7 @@ function ControlBar({ onLeave }) {
3313
3511
  children: room.local?.cameraEnabled === true ? /* @__PURE__ */ jsx6(CameraIcon, {}) : /* @__PURE__ */ jsx6(CameraOffIcon, {})
3314
3512
  }
3315
3513
  ),
3514
+ /* @__PURE__ */ jsx6(CaptionsControl, {}),
3316
3515
  room.canShareScreen || room.local?.screenSharing === true ? /* @__PURE__ */ jsx6(
3317
3516
  ControlButton,
3318
3517
  {
@@ -3377,6 +3576,7 @@ function ControlBar({ onLeave }) {
3377
3576
  onOpen: openActivity
3378
3577
  }
3379
3578
  ),
3579
+ /* @__PURE__ */ jsx6(TakeNotesControl, {}),
3380
3580
  /* @__PURE__ */ jsx6(AiControl, { active: panel === "ai", onToggle: () => toggle("ai") }),
3381
3581
  recording.canStart ? /* @__PURE__ */ jsx6(ControlButton, { label: "Start recording", onClick: () => void recording.start(), children: /* @__PURE__ */ jsx6(RecordIcon, {}) }) : null,
3382
3582
  recording.canStop ? /* @__PURE__ */ jsx6(ControlButton, { label: "Stop recording", active: true, onClick: () => void recording.stop(), children: /* @__PURE__ */ jsx6(RecordIcon, {}) }) : null,
@@ -3432,6 +3632,23 @@ function ControlBar({ onLeave }) {
3432
3632
  ]
3433
3633
  }
3434
3634
  ),
3635
+ /* @__PURE__ */ jsxs3(
3636
+ MenuItem,
3637
+ {
3638
+ onSelect: () => {
3639
+ setPopover("none");
3640
+ void controls.run({
3641
+ action: "set_av_lock",
3642
+ value: snapshot?.livePolicy.avLock === "both" ? "off" : "both"
3643
+ });
3644
+ },
3645
+ children: [
3646
+ /* @__PURE__ */ jsx6(MicOffIcon, { width: 16, height: 16 }),
3647
+ " ",
3648
+ snapshot?.livePolicy.avLock === "both" ? "Turn on attendees' microphone and camera" : "Turn off attendees' microphone and camera"
3649
+ ]
3650
+ }
3651
+ ),
3435
3652
  /* @__PURE__ */ jsxs3(
3436
3653
  MenuItem,
3437
3654
  {
@@ -3759,9 +3976,45 @@ function MeetingOverlays() {
3759
3976
  const reactions = useReactions();
3760
3977
  const { notice, dismiss } = useModerationNotice();
3761
3978
  const room = useRoom();
3979
+ const live = useLivePolicy();
3980
+ const snapshot = useMeetSnapshot();
3981
+ const controls = useHostControls();
3982
+ const isHostNow = useRole() === "host";
3762
3983
  return /* @__PURE__ */ jsxs3(Fragment, { children: [
3763
3984
  /* @__PURE__ */ jsx6(ReactionOverlay, { reactions }),
3764
3985
  /* @__PURE__ */ jsx6(MediaNotices, {}),
3986
+ snapshot !== null && snapshot.phase === "in_call" && snapshot.participants.length >= 2 && !snapshot.participants.some((p) => p.role === "host" || p.role === "cohost") ? (
3987
+ // meeting-without-host: the call goes on; host-only controls are simply not on screen.
3988
+ /* @__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." })
3989
+ ) : null,
3990
+ 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: [
3991
+ /* @__PURE__ */ jsx6("h2", { className: "mx-meet__panel-title", children: "This meeting is being recorded" }),
3992
+ /* @__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." }),
3993
+ /* @__PURE__ */ jsxs3("div", { className: "mx-meet__menu-actions", children: [
3994
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__send", onClick: live.agree, children: live.consent === "ok_or_leave" ? "OK" : "I agree" }),
3995
+ live.consent === "ok_or_leave" ? /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__link", onClick: () => void room.leave(), children: "Leave meeting" }) : null
3996
+ ] })
3997
+ ] }) }) : null,
3998
+ snapshot?.canReclaim === true && !isHostNow ? /* @__PURE__ */ jsxs3("div", { className: "mx-meet__notice-bar", role: "status", "data-meet-notice": "host-reclaim", children: [
3999
+ /* @__PURE__ */ jsx6("span", { children: "You started this meeting. Take back the host?" }),
4000
+ /* @__PURE__ */ jsx6(
4001
+ "button",
4002
+ {
4003
+ type: "button",
4004
+ className: "mx-meet__primary mx-meet__primary--small",
4005
+ onClick: () => void controls.run({ action: "reclaim_host" }),
4006
+ children: "Reclaim host"
4007
+ }
4008
+ )
4009
+ ] }) : null,
4010
+ 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,
4011
+ live.timeWarning && live.minutesLeft !== null && live.minutesLeft > 0 ? /* @__PURE__ */ jsxs3("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "time-warning", children: [
4012
+ "This meeting will end in ",
4013
+ live.minutesLeft,
4014
+ " ",
4015
+ live.minutesLeft === 1 ? "minute" : "minutes",
4016
+ " (time limit)."
4017
+ ] }) : null,
3765
4018
  notice !== null && notice.kind !== "removed" ? (
3766
4019
  // A host's request ("the host is asking you to unmute"), not an error:
3767
4020
  // an assertive status, so it is announced at once without being filed
@@ -3852,6 +4105,7 @@ function ChatPanel({
3852
4105
  }) {
3853
4106
  const snapshot = useMeetSnapshot();
3854
4107
  const room = useRoom();
4108
+ const live = useLivePolicy();
3855
4109
  const [draft, setDraft] = useState4("");
3856
4110
  const [recipient, setRecipient] = useState4(to);
3857
4111
  const listRef = useRef6(null);
@@ -3876,7 +4130,7 @@ function ChatPanel({
3876
4130
  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,
3877
4131
  messages.map((message) => /* @__PURE__ */ jsx6(ChatLine, { message, local, people }, message.id))
3878
4132
  ] }),
3879
- /* @__PURE__ */ jsxs3(
4133
+ !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(
3880
4134
  "form",
3881
4135
  {
3882
4136
  className: "mx-meet__composer mx-meet__composer--chat",
@@ -4248,6 +4502,13 @@ function PersonMenu({
4248
4502
  /* @__PURE__ */ jsx6(PeopleIcon, { width: 16, height: 16 }),
4249
4503
  " Make co-host"
4250
4504
  ] }) : null,
4505
+ !participant.isAgent && participant.role !== "cohost" && participant.role !== "host" ? participant.role === "presenter" ? /* @__PURE__ */ jsxs3(MenuItem, { onSelect: () => act({ action: "revoke_presenter", identity: participant.identity }), children: [
4506
+ /* @__PURE__ */ jsx6(ScreenShareIcon, { width: 16, height: 16 }),
4507
+ " Remove presenter"
4508
+ ] }) : /* @__PURE__ */ jsxs3(MenuItem, { onSelect: () => act({ action: "make_presenter", identity: participant.identity }), children: [
4509
+ /* @__PURE__ */ jsx6(ScreenShareIcon, { width: 16, height: 16 }),
4510
+ " Make presenter"
4511
+ ] }) : null,
4251
4512
  !participant.isAgent ? /* @__PURE__ */ jsxs3(
4252
4513
  MenuItem,
4253
4514
  {
@@ -4753,8 +5014,19 @@ function Captions({ lines = 3 }) {
4753
5014
  const ai = useMeetAi();
4754
5015
  const visible = ai.transcript.slice(-lines);
4755
5016
  const translation = useCaptionTranslation(visible);
4756
- if (visible.length === 0) return null;
4757
- return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", children: [
5017
+ const captions = useCaptions();
5018
+ if (!captions.on) return null;
5019
+ if (captions.state === "unavailable") {
5020
+ return /* @__PURE__ */ jsx6("div", { className: "mx-meet__captions", role: "status", "data-meet-notice": "captions-unavailable", children: /* @__PURE__ */ jsxs3("p", { className: "mx-meet__caption", children: [
5021
+ "Captions are unavailable. ",
5022
+ captions.reason,
5023
+ captions.remedy !== null ? ` ${captions.remedy}` : ""
5024
+ ] }) });
5025
+ }
5026
+ if (visible.length === 0) {
5027
+ 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." }) });
5028
+ }
5029
+ return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", "data-meet-caption": "live", children: [
4758
5030
  visible.map((line) => {
4759
5031
  const translated = translation.language === null ? void 0 : translation.translations[line.id];
4760
5032
  return /* @__PURE__ */ jsxs3(
@@ -4777,7 +5049,7 @@ function Captions({ lines = 3 }) {
4777
5049
  function ConsentNotice() {
4778
5050
  const notice = useConsentNotice();
4779
5051
  if (notice === null) return null;
4780
- return /* @__PURE__ */ jsx6("p", { className: "mx-meet__consent", role: "status", children: notice });
5052
+ return /* @__PURE__ */ jsx6("p", { className: "mx-meet__consent", role: "status", "data-meet-notice": "notes-started", children: notice });
4781
5053
  }
4782
5054
  function AttendanceNotice() {
4783
5055
  const media = useLocalMedia();
@@ -5491,6 +5763,7 @@ export {
5491
5763
  CameraOffIcon,
5492
5764
  CaptionLanguageSettings,
5493
5765
  Captions,
5766
+ CaptionsIcon,
5494
5767
  ChatIcon,
5495
5768
  ChatPanel,
5496
5769
  CheckIcon,
@@ -5514,6 +5787,7 @@ export {
5514
5787
  MicIcon,
5515
5788
  MicOffIcon,
5516
5789
  MoreIcon,
5790
+ NotesIcon,
5517
5791
  PanelShell,
5518
5792
  ParticipantTile,
5519
5793
  PencilIcon,