@ai-matrx/meet 0.11.41 → 0.11.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,6 +44,7 @@ __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,
@@ -67,6 +68,7 @@ __export(meet_exports, {
67
68
  MicIcon: () => MicIcon,
68
69
  MicOffIcon: () => MicOffIcon,
69
70
  MoreIcon: () => MoreIcon,
71
+ NotesIcon: () => NotesIcon,
70
72
  PanelShell: () => PanelShell,
71
73
  ParticipantTile: () => ParticipantTile,
72
74
  PencilIcon: () => PencilIcon,
@@ -265,6 +267,8 @@ var MEET_ROUTES = {
265
267
  noteTakerStop: "/api/v1/meet/agent/stop",
266
268
  /** In-meeting Q&A. Streams NDJSON; body is `{meeting_id, question}` only. */
267
269
  intelligenceAsk: "/api/v1/meet/intelligence/ask",
270
+ /** Anyone in the meeting (guests with the room pass included) asks for captions for themselves. */
271
+ noteTakerCaptions: "/api/v1/meet/agent/captions",
268
272
  /** A host ends the meeting for everyone. Served by MRI-A6. */
269
273
  meetingEnd: "/api/v1/meet/end",
270
274
  /**
@@ -360,6 +364,31 @@ function recordingLabel(state) {
360
364
  }
361
365
  }
362
366
 
367
+ // src/core/live-policy.ts
368
+ var DEFAULT_LIVE_POLICY = {
369
+ chatMode: "everyone",
370
+ avLock: "off",
371
+ timeLimitMinutes: 0,
372
+ timeLimitWarningMinutes: 5,
373
+ recordingConsent: "notice"
374
+ };
375
+ var MODERATORS = /* @__PURE__ */ new Set(["host", "cohost"]);
376
+ function isLockedFor(policy, role, kind) {
377
+ if (MODERATORS.has(role)) return false;
378
+ return policy.avLock === "both" || (kind === "microphone" ? policy.avLock === "mic" : policy.avLock === "camera");
379
+ }
380
+ function consentPending(policy, role, recordingOn, given) {
381
+ return recordingOn && !given && policy.recordingConsent !== "notice" && !MODERATORS.has(role);
382
+ }
383
+ function canChat(policy, role) {
384
+ if (policy.chatMode === "everyone") return true;
385
+ return policy.chatMode === "hosts_only" && MODERATORS.has(role);
386
+ }
387
+ function minutesLeft(policy, startedAtMs, nowMs) {
388
+ if (policy.timeLimitMinutes <= 0 || startedAtMs === null) return null;
389
+ return Math.ceil((startedAtMs + policy.timeLimitMinutes * 6e4 - nowMs) / 6e4);
390
+ }
391
+
363
392
  // src/core/attendance.ts
364
393
  var IDLE_MEDIA_REGION = {
365
394
  permission: { microphone: "unknown", camera: "unknown" },
@@ -452,9 +481,17 @@ function autoAssign(identities, rooms, exclude = []) {
452
481
  return out;
453
482
  }
454
483
  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);
484
+ if (plan === null || plan.state !== "open") return null;
485
+ const ends = [plan.endsAt, plan.closingAt].map((stamp) => stamp === null ? Number.NaN : Date.parse(stamp)).filter((time) => !Number.isNaN(time));
486
+ if (ends.length === 0) return null;
487
+ return Math.max(0, Math.min(...ends) - now);
488
+ }
489
+ var BREAKOUT_COUNTDOWN_MS = 3e4;
490
+ function breakoutClosingMs(plan, now) {
491
+ if (plan === null || plan.state !== "open") return null;
492
+ const remaining = breakoutRemainingMs(plan, now);
493
+ if (remaining === null) return null;
494
+ return plan.closingAt !== null || remaining <= BREAKOUT_COUNTDOWN_MS ? remaining : null;
458
495
  }
459
496
  function defaultCaptionLanguage(settings, viewerLanguage) {
460
497
  const configured = settings?.captionTranslationLanguage.trim() ?? "";
@@ -662,11 +699,13 @@ function webMediaPlatform() {
662
699
 
663
700
  // src/core/observation.ts
664
701
  function observedRole(role, standing) {
665
- if (role === "host" || role === "cohost") return role;
702
+ if (role === "host" || role === "cohost" || role === "presenter") return role;
666
703
  return isLinkFollower(standing) ? "guest" : "participant";
667
704
  }
668
- function observedDevice(track, permission = "unknown") {
705
+ function observedDevice(track, permission = "unknown", held = null) {
706
+ if (held === "locked") return "locked";
669
707
  if (track.enabled) return "on";
708
+ if (held === "muted_by_host") return "muted_by_host";
670
709
  const reason = deviceUnavailableReason(track, permission);
671
710
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
672
711
  }
@@ -703,10 +742,20 @@ function observe(input) {
703
742
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
704
743
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
705
744
  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
745
+ camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera, lockedFor(snapshot, "camera", inCall)) : null,
746
+ microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone, lockedFor(snapshot, "microphone", inCall)) : null
708
747
  };
709
748
  }
749
+ function lockedFor(snapshot, kind, inCall) {
750
+ if (!inCall) return null;
751
+ const role = effectiveRole(snapshot);
752
+ if (isLockedFor(snapshot.livePolicy, role, kind)) return "locked";
753
+ if (snapshot.livePolicy.recordingConsent === "view_only_until_agree" && consentPending(snapshot.livePolicy, role, snapshot.recording === "recording", snapshot.recordingConsentGiven)) {
754
+ return "locked";
755
+ }
756
+ if (kind === "microphone" && snapshot.mutedByHost) return "muted_by_host";
757
+ return null;
758
+ }
710
759
  function staticObservation(phase, reason = null) {
711
760
  return {
712
761
  phase,
@@ -796,7 +845,7 @@ function exactNoteTakerParticipant(meeting, participants) {
796
845
  }
797
846
  function noteTakerStatus(args) {
798
847
  const present = exactNoteTakerParticipant(args.meeting, args.participants) !== null;
799
- const raw = args.meeting?.metadata[NOTE_TAKER_METADATA_KEY];
848
+ const raw = args.meeting?.metadata?.[NOTE_TAKER_METADATA_KEY];
800
849
  const row = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
801
850
  const recorded = readState(row["state"]);
802
851
  const detail = typeof row["reason"] === "string" && row["reason"].length > 0 ? row["reason"] : null;
@@ -1549,6 +1598,45 @@ function useMeetAi() {
1549
1598
  }, [ai, meeting, dispatchNoteTaker])
1550
1599
  };
1551
1600
  }
1601
+ function useCaptions() {
1602
+ const host = useMeetHost();
1603
+ const snapshot = useMeetSnapshot();
1604
+ const ai = useMeetAi();
1605
+ const on = snapshot?.captionsOn === true;
1606
+ const refusal = snapshot?.captionsRefusal ?? null;
1607
+ const taker = ai.noteTaker;
1608
+ const meeting = snapshot?.meeting ?? null;
1609
+ const request = (0, import_react5.useCallback)(() => {
1610
+ if (host === null || meeting === null) return;
1611
+ void host.ai.requestCaptions({ meetingId: meeting.id });
1612
+ }, [host, meeting]);
1613
+ const toggle = (0, import_react5.useCallback)(() => {
1614
+ if (host === null) return;
1615
+ const next = !on;
1616
+ host.store.patch({ captionsOn: next, captionsRefusal: null });
1617
+ if (next && taker.state !== "joining" && taker.state !== "transcribing") request();
1618
+ }, [host, on, taker.state, request]);
1619
+ let state = "off";
1620
+ let reason = null;
1621
+ let remedy = null;
1622
+ if (on) {
1623
+ if (taker.state === "transcribing" && taker.present) {
1624
+ state = "live";
1625
+ } else if (taker.state === "joining") {
1626
+ state = "starting";
1627
+ } else if (refusal !== null && refusal.reason.length > 0) {
1628
+ state = "unavailable";
1629
+ reason = refusal.reason;
1630
+ remedy = refusal.remedy.length > 0 ? refusal.remedy : null;
1631
+ } else if (taker.state === "ended" || taker.state === "handoff_pending") {
1632
+ state = "unavailable";
1633
+ reason = noteTakerLabel(taker);
1634
+ } else {
1635
+ state = "starting";
1636
+ }
1637
+ }
1638
+ return { on, state, reason, remedy, toggle };
1639
+ }
1552
1640
  function useEndMeeting() {
1553
1641
  const host = useMeetHost();
1554
1642
  const snapshot = useMeetSnapshot();
@@ -1586,7 +1674,7 @@ function useConsentNotice() {
1586
1674
  const snapshot = useMeetSnapshot();
1587
1675
  const agent = noteTakerParticipant(snapshot?.participants ?? []);
1588
1676
  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.`;
1677
+ return `AI notes are on. ${agent.displayName} is in this meeting, transcribing what is said and writing notes everyone here can read.`;
1590
1678
  }
1591
1679
  function useMediaSupervisor() {
1592
1680
  const host = useMeetHost();
@@ -1834,6 +1922,38 @@ function useGuestRecordAccess(meeting) {
1834
1922
  )
1835
1923
  };
1836
1924
  }
1925
+ function useLivePolicy() {
1926
+ const snapshot = useMeetSnapshot();
1927
+ const role = useRole();
1928
+ const [now, setNow] = (0, import_react5.useState)(() => Date.now());
1929
+ const limited = (snapshot?.livePolicy.timeLimitMinutes ?? 0) > 0;
1930
+ (0, import_react5.useEffect)(() => {
1931
+ if (!limited) return void 0;
1932
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
1933
+ return () => clearInterval(timer);
1934
+ }, [limited]);
1935
+ const policy = snapshot?.livePolicy ?? DEFAULT_LIVE_POLICY;
1936
+ const left = minutesLeft(policy, snapshot?.sessionStartedAt ?? null, now);
1937
+ const host = useMeetHost();
1938
+ const pending = consentPending(policy, role, snapshot?.recording === "recording", snapshot?.recordingConsentGiven === true);
1939
+ const viewOnly = pending && policy.recordingConsent === "view_only_until_agree";
1940
+ const idle = snapshot !== null && snapshot.recording !== "recording" && snapshot.recording !== "starting" && snapshot.recording !== "stopping";
1941
+ const given = snapshot?.recordingConsentGiven === true;
1942
+ const store = host?.store ?? null;
1943
+ (0, import_react5.useEffect)(() => {
1944
+ if (idle && given) store?.patch({ recordingConsentGiven: false });
1945
+ }, [idle, given, store]);
1946
+ return {
1947
+ canChat: canChat(policy, role),
1948
+ chatMode: policy.chatMode,
1949
+ micLocked: isLockedFor(policy, role, "microphone") || viewOnly,
1950
+ cameraLocked: isLockedFor(policy, role, "camera") || viewOnly,
1951
+ consent: pending ? policy.recordingConsent === "ok_or_leave" ? "ok_or_leave" : "view_only_until_agree" : null,
1952
+ agree: () => store?.patch({ recordingConsentGiven: true }),
1953
+ minutesLeft: left,
1954
+ timeWarning: left !== null && left <= policy.timeLimitWarningMinutes
1955
+ };
1956
+ }
1837
1957
 
1838
1958
  // src/react/collab-hooks.ts
1839
1959
  var import_react7 = require("react");
@@ -2049,6 +2169,18 @@ function SparklesIcon(props) {
2049
2169
  /* @__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
2170
  ] });
2051
2171
  }
2172
+ function CaptionsIcon(props) {
2173
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Icon, { ...props, children: [
2174
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
2175
+ /* @__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" })
2176
+ ] });
2177
+ }
2178
+ function NotesIcon(props) {
2179
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Icon, { ...props, children: [
2180
+ /* @__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" }),
2181
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M14 7l3 3" })
2182
+ ] });
2183
+ }
2052
2184
  function LockIcon(props) {
2053
2185
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Icon, { ...props, children: [
2054
2186
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "4", y: "10", width: "16", height: "11", rx: "2" }),
@@ -2967,7 +3099,7 @@ function BreakoutRun({ plan }) {
2967
3099
  type: "button",
2968
3100
  className: "mx-meet__primary mx-meet__primary--small",
2969
3101
  onClick: () => void act({ action: "breakout_close" }).then(show),
2970
- children: "Bring everyone back"
3102
+ children: "Close rooms"
2971
3103
  }
2972
3104
  ) : null
2973
3105
  ] });
@@ -2985,9 +3117,14 @@ function BreakoutBanner() {
2985
3117
  const { collab, act, moveTo, localIdentity, breakoutRoomId } = useCollab();
2986
3118
  const plan = collab?.breakout ?? null;
2987
3119
  const open = plan?.state === "open" ? plan : null;
2988
- const now = useNow(open?.endsAt != null);
3120
+ const now = useNow(open?.endsAt != null || open?.closingAt != null);
2989
3121
  const remaining = breakoutRemainingMs(open, now);
3122
+ const closing = breakoutClosingMs(open, now);
2990
3123
  const assigned = assignedRoom(open, localIdentity);
3124
+ const [later, setLater] = (0, import_react9.useState)(null);
3125
+ const [autoSince, setAutoSince] = (0, import_react9.useState)(null);
3126
+ const AUTO_MOVE_MS = 1e4;
3127
+ 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
3128
  const [dismissedBroadcast, setDismissedBroadcast] = (0, import_react9.useState)(null);
2992
3129
  const [failure, setFailure] = (0, import_react9.useState)(null);
2993
3130
  const expireAsked = (0, import_react9.useRef)(null);
@@ -3000,6 +3137,19 @@ function BreakoutBanner() {
3000
3137
  if ((here ?? null) === request.roomId) return;
3001
3138
  void moveTo(request.roomId).then((result) => setFailure(result.ok ? null : result.message));
3002
3139
  }, [moveAt, host, moveTo]);
3140
+ const joinAssigned = async () => {
3141
+ if (assigned === null) return;
3142
+ const said = await act({ action: "breakout_join", roomId: assigned.id });
3143
+ const result = said.ok ? await moveTo(assigned.id) : said;
3144
+ setFailure(result.ok ? null : result.message);
3145
+ };
3146
+ (0, import_react9.useEffect)(() => {
3147
+ if (open === null || open.moveMode !== "auto" || assigned === null || breakoutRoomId !== null) return;
3148
+ if (autoSince?.id !== open.id) setAutoSince({ id: open.id, at: Date.now() });
3149
+ }, [open, assigned, breakoutRoomId, autoSince]);
3150
+ (0, import_react9.useEffect)(() => {
3151
+ if (autoLeft === 0) void joinAssigned();
3152
+ }, [autoLeft]);
3003
3153
  (0, import_react9.useEffect)(() => {
3004
3154
  if (open === null || remaining === null || remaining > 0) return;
3005
3155
  const asked = expireAsked.current;
@@ -3013,27 +3163,25 @@ function BreakoutBanner() {
3013
3163
  if (open === null && failure === null) return null;
3014
3164
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "mx-meet__breakout-banner", role: "status", children: [
3015
3165
  failure !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "mx-meet__collab-error", children: failure }) : null,
3166
+ closing !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { role: "status", "data-meet-notice": "breakout-closing", children: [
3167
+ "Rooms closing in ",
3168
+ Math.ceil(closing / 1e3),
3169
+ room !== null ? " \u2014 you return to the main meeting" : ""
3170
+ ] }) : null,
3016
3171
  room !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
3017
3172
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: room.name }),
3018
- remaining !== null ? ` \xB7 ${clock(remaining)} left` : "",
3173
+ remaining !== null && closing === null ? ` \xB7 ${clock(remaining)} left` : "",
3019
3174
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mx-meet__chip", onClick: () => void moveTo(null), children: "Return to main room" })
3175
+ ] }) : assigned !== null && later !== open?.id ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
3176
+ autoLeft !== null ? `You're being moved to ${assigned.name} in ${Math.ceil(autoLeft / 1e3)} s` : `You're invited to ${assigned.name}`,
3177
+ remaining !== null && closing === null ? ` \xB7 ${clock(remaining)} left` : "",
3178
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void joinAssigned(), children: "Join room" }),
3179
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mx-meet__chip", onClick: () => setLater(open?.id ?? null), children: "Later" })
3020
3180
  ] }) : 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,
3181
+ assigned.name,
3182
+ " is open",
3183
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mx-meet__chip", onClick: () => void joinAssigned(), children: "Join room" })
3184
+ ] }) : open?.allowChoose ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: "Rooms are open \u2014 pick one under Activities." }) : null,
3037
3185
  showBroadcast ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "mx-meet__breakout-broadcast", children: [
3038
3186
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("strong", { children: [
3039
3187
  broadcast.by,
@@ -3081,6 +3229,33 @@ function CaptionLanguageSettings() {
3081
3229
  collab?.captionTranslationError != null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "mx-meet__panel-note mx-meet__collab-error", children: collab.captionTranslationError }) : null
3082
3230
  ] });
3083
3231
  }
3232
+ function CaptionsControl() {
3233
+ const captions = useCaptions();
3234
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3235
+ ControlButton,
3236
+ {
3237
+ label: captions.on ? "Turn off captions" : "Turn on captions",
3238
+ active: captions.on,
3239
+ pressed: captions.on,
3240
+ onClick: captions.toggle,
3241
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CaptionsIcon, {})
3242
+ }
3243
+ );
3244
+ }
3245
+ function TakeNotesControl() {
3246
+ const ai = useMeetAi();
3247
+ if (!ai.canControlNoteTaker) return null;
3248
+ const running = ai.noteTaker.state === "joining" || ai.noteTaker.state === "transcribing";
3249
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3250
+ ControlButton,
3251
+ {
3252
+ label: running ? "Stop taking notes" : "Take notes for me",
3253
+ active: running,
3254
+ onClick: () => void (running ? ai.stopNoteTaker() : ai.startNoteTaker()),
3255
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(NotesIcon, {})
3256
+ }
3257
+ );
3258
+ }
3084
3259
 
3085
3260
  // src/skins/meet/components.tsx
3086
3261
  var import_jsx_runtime6 = require("react/jsx-runtime");
@@ -3131,6 +3306,7 @@ function ControlButton({
3131
3306
  label,
3132
3307
  active = false,
3133
3308
  danger = false,
3309
+ pressed,
3134
3310
  onClick,
3135
3311
  children
3136
3312
  }) {
@@ -3145,26 +3321,49 @@ function ControlButton({
3145
3321
  ].filter(Boolean).join(" "),
3146
3322
  "aria-label": label,
3147
3323
  title: label,
3324
+ ...pressed !== void 0 ? { "aria-pressed": pressed } : {},
3148
3325
  onClick,
3149
3326
  children
3150
3327
  }
3151
3328
  );
3152
3329
  }
3153
3330
  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
- ]
3331
+ const { state, label, canStart } = useRecording();
3332
+ const snapshot = useMeetSnapshot();
3333
+ const previous = (0, import_react10.useRef)(state);
3334
+ const [stoppedAt, setStoppedAt] = (0, import_react10.useState)(null);
3335
+ (0, import_react10.useEffect)(() => {
3336
+ const was = previous.current;
3337
+ previous.current = state;
3338
+ if ((was === "recording" || was === "starting") && state !== "recording" && state !== "starting") {
3339
+ setStoppedAt(Date.now());
3340
+ const timer = setTimeout(() => setStoppedAt(null), 1e4);
3341
+ return () => clearTimeout(timer);
3166
3342
  }
3167
- );
3343
+ return void 0;
3344
+ }, [state]);
3345
+ const meetingId = snapshot?.meeting?.id ?? null;
3346
+ if (label === null && stoppedAt === null) return null;
3347
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
3348
+ label !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3349
+ "span",
3350
+ {
3351
+ className: `mx-meet__recording mx-meet__recording--${state}`,
3352
+ role: "status",
3353
+ "aria-live": "polite",
3354
+ children: [
3355
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RecordIcon, { width: 10, height: 10 }),
3356
+ label
3357
+ ]
3358
+ }
3359
+ ) : null,
3360
+ stoppedAt !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__recording", role: "status", "data-meet-notice": "recording-stopped", children: "Recording stopped" }) : null,
3361
+ state === "processing" ? (
3362
+ // rec-processing: the host is told it is being made ready (the indicator says Processing…).
3363
+ /* @__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." })
3364
+ ) : null,
3365
+ 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
3366
+ ] });
3168
3367
  }
3169
3368
  function ConnectionDot({ quality }) {
3170
3369
  if (quality === "excellent" || quality === "good" || quality === "unknown") return null;
@@ -3195,6 +3394,7 @@ function ParticipantTile({
3195
3394
  const showVideo = participant.cameraEnabled || participant.screenSharing;
3196
3395
  const pinnedHere = snapshot?.pinned === participant.identity;
3197
3396
  const spotlit = snapshot?.spotlight === participant.identity;
3397
+ const presenting = snapshot?.presenter === participant.identity;
3198
3398
  const handPosition = participant.handRaised ? queue.findIndex((entry) => entry.identity === participant.identity) + 1 : 0;
3199
3399
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3200
3400
  "div",
@@ -3206,6 +3406,7 @@ function ParticipantTile({
3206
3406
  ].filter(Boolean).join(" "),
3207
3407
  "data-identity": participant.identity,
3208
3408
  ...participantAttributes(participant),
3409
+ ...presenting ? { "data-meet-participant-presenting": "true" } : {},
3209
3410
  children: [
3210
3411
  showVideo ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3211
3412
  "video",
@@ -3217,6 +3418,7 @@ function ParticipantTile({
3217
3418
  muted: participant.isLocal
3218
3419
  }
3219
3420
  ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: featured ? 96 : 56 }) }),
3421
+ presenting ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__tile-presenting", role: "status", children: participant.isLocal ? "You are presenting" : `${participant.displayName} is presenting` }) : null,
3220
3422
  participant.handRaised ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "mx-meet__tile-hand", title: `Hand raised \xB7 ${ordinal(handPosition)} in line`, children: [
3221
3423
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(HandIcon, { width: 14, height: 14 }),
3222
3424
  handPosition > 0 ? handPosition : null
@@ -3317,6 +3519,7 @@ function ControlBar({ onLeave }) {
3317
3519
  const controls = useHostControls();
3318
3520
  const queue = useHandQueue();
3319
3521
  const micUnavailable = useLocalMedia().microphone.unavailable;
3522
+ const live = useLivePolicy();
3320
3523
  const unread = useUnreadChat(panel === "chat");
3321
3524
  const [whiteboard, setWhiteboard] = (0, import_react10.useState)(false);
3322
3525
  const app = useMeetAppPanel();
@@ -3384,7 +3587,7 @@ function ControlBar({ onLeave }) {
3384
3587
  panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(KnockNotice, { onView: () => setPanel("people") }) : null,
3385
3588
  controls.notice !== null && panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
3386
3589
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__controls", children: [
3387
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3590
+ live.micLocked ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3388
3591
  ControlButton,
3389
3592
  {
3390
3593
  label: micUnavailable !== null ? `Unmute \u2014 ${micUnavailable.remedy}` : room.local?.micEnabled === true ? "Mute (\u2318D)" : "Unmute (\u2318D)",
@@ -3393,7 +3596,7 @@ function ControlBar({ onLeave }) {
3393
3596
  children: room.local?.micEnabled === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, {})
3394
3597
  }
3395
3598
  ),
3396
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3599
+ live.cameraLocked ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3397
3600
  ControlButton,
3398
3601
  {
3399
3602
  label: room.local?.cameraEnabled === true ? "Stop video (\u2318E)" : "Start video (\u2318E)",
@@ -3402,6 +3605,7 @@ function ControlBar({ onLeave }) {
3402
3605
  children: room.local?.cameraEnabled === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CameraIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CameraOffIcon, {})
3403
3606
  }
3404
3607
  ),
3608
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CaptionsControl, {}),
3405
3609
  room.canShareScreen || room.local?.screenSharing === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3406
3610
  ControlButton,
3407
3611
  {
@@ -3466,6 +3670,7 @@ function ControlBar({ onLeave }) {
3466
3670
  onOpen: openActivity
3467
3671
  }
3468
3672
  ),
3673
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(TakeNotesControl, {}),
3469
3674
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AiControl, { active: panel === "ai", onToggle: () => toggle("ai") }),
3470
3675
  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,
3471
3676
  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,
@@ -3521,6 +3726,23 @@ function ControlBar({ onLeave }) {
3521
3726
  ]
3522
3727
  }
3523
3728
  ),
3729
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3730
+ MenuItem,
3731
+ {
3732
+ onSelect: () => {
3733
+ setPopover("none");
3734
+ void controls.run({
3735
+ action: "set_av_lock",
3736
+ value: snapshot?.livePolicy.avLock === "both" ? "off" : "both"
3737
+ });
3738
+ },
3739
+ children: [
3740
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, { width: 16, height: 16 }),
3741
+ " ",
3742
+ snapshot?.livePolicy.avLock === "both" ? "Turn on attendees' microphone and camera" : "Turn off attendees' microphone and camera"
3743
+ ]
3744
+ }
3745
+ ),
3524
3746
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3525
3747
  MenuItem,
3526
3748
  {
@@ -3848,9 +4070,45 @@ function MeetingOverlays() {
3848
4070
  const reactions = useReactions();
3849
4071
  const { notice, dismiss } = useModerationNotice();
3850
4072
  const room = useRoom();
4073
+ const live = useLivePolicy();
4074
+ const snapshot = useMeetSnapshot();
4075
+ const controls = useHostControls();
4076
+ const isHostNow = useRole() === "host";
3851
4077
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
3852
4078
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ReactionOverlay, { reactions }),
3853
4079
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MediaNotices, {}),
4080
+ snapshot !== null && snapshot.phase === "in_call" && snapshot.participants.length >= 2 && !snapshot.participants.some((p) => p.role === "host" || p.role === "cohost") ? (
4081
+ // meeting-without-host: the call goes on; host-only controls are simply not on screen.
4082
+ /* @__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." })
4083
+ ) : null,
4084
+ 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: [
4085
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h2", { className: "mx-meet__panel-title", children: "This meeting is being recorded" }),
4086
+ /* @__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." }),
4087
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__menu-actions", children: [
4088
+ /* @__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" }),
4089
+ 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
4090
+ ] })
4091
+ ] }) }) : null,
4092
+ snapshot?.canReclaim === true && !isHostNow ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__notice-bar", role: "status", "data-meet-notice": "host-reclaim", children: [
4093
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "You started this meeting. Take back the host?" }),
4094
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4095
+ "button",
4096
+ {
4097
+ type: "button",
4098
+ className: "mx-meet__primary mx-meet__primary--small",
4099
+ onClick: () => void controls.run({ action: "reclaim_host" }),
4100
+ children: "Reclaim host"
4101
+ }
4102
+ )
4103
+ ] }) : null,
4104
+ 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,
4105
+ 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: [
4106
+ "This meeting will end in ",
4107
+ live.minutesLeft,
4108
+ " ",
4109
+ live.minutesLeft === 1 ? "minute" : "minutes",
4110
+ " (time limit)."
4111
+ ] }) : null,
3854
4112
  notice !== null && notice.kind !== "removed" ? (
3855
4113
  // A host's request ("the host is asking you to unmute"), not an error:
3856
4114
  // an assertive status, so it is announced at once without being filed
@@ -3941,6 +4199,7 @@ function ChatPanel({
3941
4199
  }) {
3942
4200
  const snapshot = useMeetSnapshot();
3943
4201
  const room = useRoom();
4202
+ const live = useLivePolicy();
3944
4203
  const [draft, setDraft] = (0, import_react10.useState)("");
3945
4204
  const [recipient, setRecipient] = (0, import_react10.useState)(to);
3946
4205
  const listRef = (0, import_react10.useRef)(null);
@@ -3965,7 +4224,7 @@ function ChatPanel({
3965
4224
  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,
3966
4225
  messages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ChatLine, { message, local, people }, message.id))
3967
4226
  ] }),
3968
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
4227
+ !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)(
3969
4228
  "form",
3970
4229
  {
3971
4230
  className: "mx-meet__composer mx-meet__composer--chat",
@@ -4148,7 +4407,8 @@ function PeoplePanel({
4148
4407
  }
4149
4408
  ),
4150
4409
  isNoteTaker(participant) ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null,
4151
- snapshot?.spotlight === participant.identity ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Spotlight" }) : null
4410
+ snapshot?.spotlight === participant.identity ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Spotlight" }) : null,
4411
+ snapshot?.presenter === participant.identity ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", "data-meet-presenting": "true", children: "Presenting" }) : null
4152
4412
  ] }),
4153
4413
  participant.handRaised ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(HandIcon, { width: 14, height: 14 }) : null,
4154
4414
  !participant.micEnabled && !isNoteTaker(participant) ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, { width: 14, height: 14 }) : null,
@@ -4336,6 +4596,13 @@ function PersonMenu({
4336
4596
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PeopleIcon, { width: 16, height: 16 }),
4337
4597
  " Make co-host"
4338
4598
  ] }) : null,
4599
+ !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: [
4600
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ScreenShareIcon, { width: 16, height: 16 }),
4601
+ " Remove presenter"
4602
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(MenuItem, { onSelect: () => act({ action: "make_presenter", identity: participant.identity }), children: [
4603
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ScreenShareIcon, { width: 16, height: 16 }),
4604
+ " Make presenter"
4605
+ ] }) : null,
4339
4606
  !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
4340
4607
  MenuItem,
4341
4608
  {
@@ -4841,8 +5108,19 @@ function Captions({ lines = 3 }) {
4841
5108
  const ai = useMeetAi();
4842
5109
  const visible = ai.transcript.slice(-lines);
4843
5110
  const translation = useCaptionTranslation(visible);
4844
- if (visible.length === 0) return null;
4845
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", children: [
5111
+ const captions = useCaptions();
5112
+ if (!captions.on) return null;
5113
+ if (captions.state === "unavailable") {
5114
+ 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: [
5115
+ "Captions are unavailable. ",
5116
+ captions.reason,
5117
+ captions.remedy !== null ? ` ${captions.remedy}` : ""
5118
+ ] }) });
5119
+ }
5120
+ if (visible.length === 0) {
5121
+ 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." }) });
5122
+ }
5123
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", "data-meet-caption": "live", children: [
4846
5124
  visible.map((line) => {
4847
5125
  const translated = translation.language === null ? void 0 : translation.translations[line.id];
4848
5126
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
@@ -4865,7 +5143,7 @@ function Captions({ lines = 3 }) {
4865
5143
  function ConsentNotice() {
4866
5144
  const notice = useConsentNotice();
4867
5145
  if (notice === null) return null;
4868
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__consent", role: "status", children: notice });
5146
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__consent", role: "status", "data-meet-notice": "notes-started", children: notice });
4869
5147
  }
4870
5148
  function AttendanceNotice() {
4871
5149
  const media = useLocalMedia();