@ai-matrx/meet 0.11.47 → 0.11.49

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.
@@ -394,17 +394,34 @@ function recordingLabel(state) {
394
394
 
395
395
  // src/core/live-policy.ts
396
396
  var DEFAULT_LIVE_POLICY = {
397
+ headlessPowers: "members_admit",
397
398
  chatMode: "everyone",
398
399
  avLock: "off",
399
400
  timeLimitMinutes: 0,
400
401
  timeLimitWarningMinutes: 5,
401
- recordingConsent: "notice"
402
+ recordingConsent: "notice",
403
+ shareWhoCan: "everyone",
404
+ shareConcurrency: "one_takeover"
402
405
  };
403
406
  var MODERATORS = /* @__PURE__ */ new Set(["host", "cohost"]);
404
407
  function isLockedFor(policy, role, kind) {
405
408
  if (MODERATORS.has(role)) return false;
406
409
  return policy.avLock === "both" || (kind === "microphone" ? policy.avLock === "mic" : policy.avLock === "camera");
407
410
  }
411
+ function canShareUnder(policy, role, standing) {
412
+ if (role === "viewer") return false;
413
+ if (MODERATORS.has(role)) return true;
414
+ switch (policy.shareWhoCan) {
415
+ case "host_only":
416
+ return false;
417
+ case "presenters":
418
+ return role === "presenter";
419
+ case "org":
420
+ return role === "presenter" || standing === "member";
421
+ default:
422
+ return true;
423
+ }
424
+ }
408
425
  function consentPending(policy, role, recordingOn, given) {
409
426
  return recordingOn && !given && policy.recordingConsent !== "notice" && !MODERATORS.has(role);
410
427
  }
@@ -416,6 +433,12 @@ function minutesLeft(policy, startedAtMs, nowMs) {
416
433
  if (policy.timeLimitMinutes <= 0 || startedAtMs === null) return null;
417
434
  return Math.ceil((startedAtMs + policy.timeLimitMinutes * 6e4 - nowMs) / 6e4);
418
435
  }
436
+ function mayAdmitNow(role, participants, policy) {
437
+ if (role === "none") return false;
438
+ if (role === "host" || role === "cohost") return true;
439
+ const hostIn = participants.some((p) => p.role === "host" || p.role === "cohost");
440
+ return !hostIn && policy.headlessPowers === "members_admit";
441
+ }
419
442
 
420
443
  // src/core/attendance.ts
421
444
  var IDLE_PREVIEW = { state: "idle", unavailable: null };
@@ -753,7 +776,7 @@ function observe(input) {
753
776
  const phase = observedPhase(input);
754
777
  const inCall = phase === "in_call";
755
778
  const actual = inCall && snapshot !== null ? effectiveRole(snapshot) : "none";
756
- const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
779
+ const canAdmit = snapshot !== null && mayAdmitNow(actual, snapshot.participants, snapshot.livePolicy);
757
780
  const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
758
781
  const deviceInPlay = phase === "joining" || inCall;
759
782
  const region = snapshot?.mediaRegion;
@@ -1284,6 +1307,13 @@ function requireSession(session, action) {
1284
1307
  if (session === null) throw noRuntime(action);
1285
1308
  return session;
1286
1309
  }
1310
+ function otherPresenterOf(snapshot) {
1311
+ if (snapshot === null || snapshot.presenter === null || snapshot.presenter === snapshot.localIdentity) return null;
1312
+ const rule = snapshot.livePolicy.shareConcurrency;
1313
+ if (rule === "multiple") return null;
1314
+ const who = snapshot.participants.find((p) => p.identity === snapshot.presenter);
1315
+ return who === void 0 ? null : { name: who.displayName, rule };
1316
+ }
1287
1317
  function useRoom() {
1288
1318
  const host = useMeetHost();
1289
1319
  const snapshot = useMeetSnapshot();
@@ -1298,7 +1328,9 @@ function useRoom() {
1298
1328
  secondDevice: snapshot?.secondDevice ?? null,
1299
1329
  local,
1300
1330
  // An observer (role `viewer`) publishes nothing: no Share screen to press and be refused (MD-16).
1301
- canShareScreen: (engine?.supportsScreenShare() ?? true) && !isObserverRole(snapshot?.grants.role ?? null),
1331
+ sharingAllowed: snapshot === null || canShareUnder(snapshot.livePolicy, effectiveRole(snapshot), effectiveStanding(snapshot)),
1332
+ canShareScreen: (engine?.supportsScreenShare() ?? true) && !isObserverRole(snapshot?.grants.role ?? null) && (snapshot === null || canShareUnder(snapshot.livePolicy, effectiveRole(snapshot), effectiveStanding(snapshot))),
1333
+ otherPresenter: otherPresenterOf(snapshot),
1302
1334
  join: (0, import_react5.useCallback)(
1303
1335
  async (request) => requireEngine(engine, "join").join(request),
1304
1336
  [engine]
@@ -1396,7 +1428,7 @@ function useLobby() {
1396
1428
  declined: (snapshot?.lobby ?? []).filter((entry) => entry.state === "denied"),
1397
1429
  // No runtime means no host powers — `<LobbyPanel>` renders nothing rather
1398
1430
  // than an admit button that would throw.
1399
- canAdmit: session !== null && isHost,
1431
+ canAdmit: session !== null && (isHost || snapshot !== null && snapshot.phase === "in_call" && mayAdmitNow(snapshot.participants.find((p) => p.identity === snapshot.localIdentity)?.role ?? "none", snapshot.participants, snapshot.livePolicy)),
1400
1432
  admit: (0, import_react5.useCallback)(
1401
1433
  async (entry) => requireSession(session, "admit").admit(entry, true),
1402
1434
  [session]
@@ -3510,7 +3542,8 @@ function ParticipantTile({
3510
3542
  participant.isLocal ? " (you)" : "",
3511
3543
  participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "AI" }) : null,
3512
3544
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RoleBadge, { role: participant.role, standing: participant.standing }),
3513
- participant.reconnecting === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Reconnecting\u2026" }) : null
3545
+ participant.reconnecting === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Reconnecting\u2026" }) : null,
3546
+ participant.reconnecting !== true && participant.videoPaused === true && !participant.isLocal ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", "data-meet-video-paused": "", children: "Video paused to save audio" }) : null
3514
3547
  ] }),
3515
3548
  !participant.micEnabled ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, { width: 14, height: 14 }) : null,
3516
3549
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(QualityBars, { quality: participant.quality })
@@ -3600,6 +3633,8 @@ function ControlBar({ onLeave }) {
3600
3633
  const queue = useHandQueue();
3601
3634
  const micUnavailable = useLocalMedia().microphone.unavailable;
3602
3635
  const live = useLivePolicy();
3636
+ const core = useMeetCore();
3637
+ const [askPresentInstead, setAskPresentInstead] = (0, import_react10.useState)(false);
3603
3638
  const unread = useUnreadChat(panel === "chat");
3604
3639
  const [whiteboard, setWhiteboard] = (0, import_react10.useState)(false);
3605
3640
  const app = useMeetAppPanel();
@@ -3688,15 +3723,39 @@ function ControlBar({ onLeave }) {
3688
3723
  }
3689
3724
  ),
3690
3725
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CaptionsControl, {}),
3691
- room.canShareScreen || room.local?.screenSharing === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3692
- ControlButton,
3693
- {
3694
- label: room.local?.screenSharing === true ? "Stop sharing" : "Share screen",
3695
- active: room.local?.screenSharing === true,
3696
- onClick: () => void room.toggleScreenShare(),
3697
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ScreenShareIcon, {})
3698
- }
3699
- ) : null,
3726
+ room.canShareScreen || room.local?.screenSharing === true && core.share.state !== "ended" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "mx-meet__anchor", children: [
3727
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3728
+ ControlButton,
3729
+ {
3730
+ label: room.local?.screenSharing === true ? "Stop sharing" : "Share screen",
3731
+ active: room.local?.screenSharing === true,
3732
+ onClick: () => {
3733
+ if (room.local?.screenSharing !== true && room.otherPresenter !== null) setAskPresentInstead(true);
3734
+ else void room.toggleScreenShare();
3735
+ },
3736
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ScreenShareIcon, {})
3737
+ }
3738
+ ),
3739
+ askPresentInstead && room.otherPresenter !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__confirm", role: "group", "aria-label": "Someone else is presenting", "data-meet-notice": "share-takeover", children: [
3740
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "mx-meet__confirm-text", children: [
3741
+ room.otherPresenter.name,
3742
+ " is presenting"
3743
+ ] }),
3744
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3745
+ "button",
3746
+ {
3747
+ type: "button",
3748
+ className: "mx-meet__primary mx-meet__primary--small",
3749
+ onClick: () => {
3750
+ setAskPresentInstead(false);
3751
+ void room.toggleScreenShare();
3752
+ },
3753
+ children: room.otherPresenter.rule === "one_takeover" ? "Present instead" : "Share anyway"
3754
+ }
3755
+ ),
3756
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setAskPresentInstead(false), children: "Cancel" })
3757
+ ] }) : null
3758
+ ] }) : null,
3700
3759
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "mx-meet__anchor", children: [
3701
3760
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3702
3761
  ControlButton,
@@ -3793,7 +3852,7 @@ function ControlBar({ onLeave }) {
3793
3852
  ]
3794
3853
  }
3795
3854
  ),
3796
- !room.canShareScreen && room.local?.screenSharing !== true ? (
3855
+ !room.canShareScreen && room.sharingAllowed && room.local?.screenSharing !== true ? (
3797
3856
  // `share-unsupported-mobile`: the control is absent; the menu says where it lives.
3798
3857
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__menu-note", role: "note", "data-meet-notice": "share-unsupported", children: [
3799
3858
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ScreenShareIcon, { width: 16, height: 16 }),
@@ -5798,13 +5857,53 @@ function MeetSkinRoom({
5798
5857
  ] }),
5799
5858
  core.connectionBanner && core.connection !== "stable" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": `connection-${core.connection}`, children: CONNECTION_BANNER[core.connection] }) : null,
5800
5859
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ShareNotice, {}),
5860
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(NetworkNotice, {}),
5801
5861
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Stage, {}),
5802
5862
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Captions, {}),
5803
5863
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ControlBar, { ...onLeave !== void 0 ? { onLeave } : {} })
5804
5864
  ] });
5805
5865
  }
5866
+ var RESTRICTED_NOTICE_MS = 3e4;
5867
+ function NetworkNotice() {
5868
+ const { snapshot, connectionBanner } = useMeetCore();
5869
+ const room = useRoom();
5870
+ const network = snapshot?.localNetwork;
5871
+ const [closed, setClosed] = (0, import_react10.useState)(() => /* @__PURE__ */ new Set());
5872
+ const [restrictedSince, setRestrictedSince] = (0, import_react10.useState)(null);
5873
+ const [, setTick] = (0, import_react10.useState)(0);
5874
+ const restricted = network?.path === "restricted";
5875
+ (0, import_react10.useEffect)(() => {
5876
+ if (!restricted || restrictedSince !== null) return;
5877
+ setRestrictedSince(Date.now());
5878
+ const timer = setTimeout(() => setTick((n) => n + 1), RESTRICTED_NOTICE_MS + 50);
5879
+ return () => clearTimeout(timer);
5880
+ }, [restricted, restrictedSince]);
5881
+ if (network === void 0 || snapshot?.phase !== "in_call" || connectionBanner) return null;
5882
+ const close = (code) => () => setClosed((prev) => /* @__PURE__ */ new Set([...prev, code]));
5883
+ const cameraOn = room.local?.cameraEnabled === true;
5884
+ if (network.videoPaused && !cameraOn) {
5885
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "video-paused", children: [
5886
+ "Your video is paused to save your audio",
5887
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => void room.toggleCamera(), children: "Try video again" })
5888
+ ] });
5889
+ }
5890
+ if (network.quality === "poor" && !closed.has("connection-poor")) {
5891
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "connection-poor", children: [
5892
+ "Your connection is unstable",
5893
+ cameraOn ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => void room.toggleCamera(), children: "Turn off camera" }) : null,
5894
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__link", onClick: close("connection-poor"), children: "Dismiss" })
5895
+ ] });
5896
+ }
5897
+ if (restricted && !closed.has("restricted-network") && restrictedSince !== null && Date.now() - restrictedSince < RESTRICTED_NOTICE_MS) {
5898
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "restricted-network", children: [
5899
+ "Restricted network \xB7 quality may be reduced",
5900
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__link", onClick: close("restricted-network"), children: "Dismiss" })
5901
+ ] });
5902
+ }
5903
+ return null;
5904
+ }
5806
5905
  function ShareNotice() {
5807
- const { share, actions } = useMeetCore();
5906
+ const { share, actions, snapshot } = useMeetCore();
5808
5907
  const [soundHintClosed, setSoundHintClosed] = (0, import_react10.useState)(null);
5809
5908
  if (share.state === "presenting" && share.audio === false && soundHintClosed !== share.at) {
5810
5909
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "share-audio-missing", children: [
@@ -5813,6 +5912,16 @@ function ShareNotice() {
5813
5912
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setSoundHintClosed(share.at), children: "Dismiss" })
5814
5913
  ] });
5815
5914
  }
5915
+ if (share.state === "ended") {
5916
+ const replaced = share.reason === "replaced";
5917
+ const rule = snapshot?.livePolicy.shareWhoCan ?? "host_only";
5918
+ const why = replaced ? `${share.by ?? "Someone"} is now presenting` : rule === "presenters" ? "Sharing is turned off. Only presenters can share their screen" : rule === "org" ? "Sharing is turned off. Only people in the organization can share their screen" : "Sharing is turned off. Only the host can share their screen";
5919
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": replaced ? "share-ended" : "share-unavailable", children: [
5920
+ replaced ? "Your presentation ended" : "Your presentation was stopped",
5921
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__remedy", children: why }),
5922
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => actions.dismissShareNotice(), children: "Dismiss" })
5923
+ ] });
5924
+ }
5816
5925
  if (share.state !== "refused") return null;
5817
5926
  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";
5818
5927
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "share-unavailable", children: [