@ai-matrx/meet 0.11.43 → 0.11.45

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.
@@ -48,6 +48,7 @@ __export(meet_exports, {
48
48
  ChatIcon: () => ChatIcon,
49
49
  ChatPanel: () => ChatPanel,
50
50
  CheckIcon: () => CheckIcon,
51
+ ChevronUpIcon: () => ChevronUpIcon,
51
52
  CloseIcon: () => CloseIcon,
52
53
  ConnectionDot: () => ConnectionDot,
53
54
  ConsentNotice: () => ConsentNotice,
@@ -114,6 +115,33 @@ var import_react10 = require("react");
114
115
  // src/react/facade.ts
115
116
  var import_react8 = require("react");
116
117
 
118
+ // src/core/phase.ts
119
+ var LIVE_PHASES = /* @__PURE__ */ new Set(["joining", "in_call"]);
120
+ var WAITING_PHASES = /* @__PURE__ */ new Set([
121
+ "waiting:host_not_started",
122
+ "waiting:knocking",
123
+ "denied",
124
+ "knock_expired"
125
+ ]);
126
+ var RETRYABLE_REFUSALS = /* @__PURE__ */ new Set([
127
+ "refused:locked",
128
+ "refused:too_early",
129
+ "refused:meeting_full",
130
+ "refused:platform_capacity"
131
+ ]);
132
+ function isLivePhase(phase) {
133
+ return LIVE_PHASES.has(phase);
134
+ }
135
+ function isWaitingPhase(phase) {
136
+ return WAITING_PHASES.has(phase);
137
+ }
138
+ function isRefusedPhase(phase) {
139
+ return phase.startsWith("refused:");
140
+ }
141
+ function reasonOf(phase) {
142
+ return isRefusedPhase(phase) ? phase.slice("refused:".length) : null;
143
+ }
144
+
117
145
  // src/core/names.ts
118
146
  var CONNECTION_SEPARATOR = "~";
119
147
  function personOf(identity) {
@@ -390,11 +418,13 @@ function minutesLeft(policy, startedAtMs, nowMs) {
390
418
  }
391
419
 
392
420
  // src/core/attendance.ts
421
+ var IDLE_PREVIEW = { state: "idle", unavailable: null };
393
422
  var IDLE_MEDIA_REGION = {
394
423
  permission: { microphone: "unknown", camera: "unknown" },
395
424
  audioOutput: "ok",
396
425
  notice: null,
397
- offer: null
426
+ offer: null,
427
+ preview: { microphone: IDLE_PREVIEW, camera: IDLE_PREVIEW }
398
428
  };
399
429
  var IDLE_TRACK = { enabled: false, unavailable: null };
400
430
  var IDLE_MEDIA = {
@@ -427,27 +457,6 @@ function isObserverRole(role) {
427
457
  return role === "viewer";
428
458
  }
429
459
 
430
- // src/core/phase.ts
431
- var LIVE_PHASES = /* @__PURE__ */ new Set(["joining", "in_call"]);
432
- var WAITING_PHASES = /* @__PURE__ */ new Set([
433
- "waiting:host_not_started",
434
- "waiting:knocking",
435
- "denied",
436
- "knock_expired"
437
- ]);
438
- function isLivePhase(phase) {
439
- return LIVE_PHASES.has(phase);
440
- }
441
- function isWaitingPhase(phase) {
442
- return WAITING_PHASES.has(phase);
443
- }
444
- function isRefusedPhase(phase) {
445
- return phase.startsWith("refused:");
446
- }
447
- function reasonOf(phase) {
448
- return isRefusedPhase(phase) ? phase.slice("refused:".length) : null;
449
- }
450
-
451
460
  // src/core/app-panel.ts
452
461
  function appPanelOf(meeting) {
453
462
  const raw = meeting?.metadata?.["app_panel"];
@@ -615,7 +624,7 @@ function deviceFixSteps(kind, reason, where) {
615
624
  case "safari":
616
625
  return where.os === "ios" ? [`Settings \u203A Apps \u203A Safari \u203A ${device}`, "Choose Allow", "Return here and press Try again"] : ["Safari \u203A Settings for This Website", `Set ${device} to Allow`, "Press Try again"];
617
626
  default:
618
- return ["Click the icon left of the web address", `Set ${device} to Allow`, "Press Try again"];
627
+ return ["Click the site settings icon in the address bar", `Set ${device} to Allow`, "Press Try again"];
619
628
  }
620
629
  case "blocked_os": {
621
630
  const app = where.browser === "chrome" ? "Google Chrome" : where.browser === "edge" ? "Microsoft Edge" : where.browser === "firefox" ? "Firefox" : "your browser";
@@ -709,6 +718,21 @@ function observedDevice(track, permission = "unknown", held = null) {
709
718
  const reason = deviceUnavailableReason(track, permission);
710
719
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
711
720
  }
721
+ function observedPreview(preview, permission = "unknown") {
722
+ if (permission === "denied") return "unavailable:denied";
723
+ switch (preview.state) {
724
+ case "on":
725
+ return "on";
726
+ case "off":
727
+ return "muted_by_me";
728
+ case "prompting":
729
+ return "prompting";
730
+ case "unavailable":
731
+ return `unavailable:${preview.unavailable?.reason ?? "failed"}`;
732
+ default:
733
+ return null;
734
+ }
735
+ }
712
736
  function endedRecord(snapshot, given) {
713
737
  const record = snapshot?.meeting ?? given;
714
738
  return record !== null && record.endedAt !== null;
@@ -732,18 +756,22 @@ function observe(input) {
732
756
  const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
733
757
  const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
734
758
  const deviceInPlay = phase === "joining" || inCall;
759
+ const region = snapshot?.mediaRegion;
760
+ const atPrejoin = phase === "prejoin" && region !== void 0;
735
761
  return {
736
762
  phase,
737
763
  reason: phase === "ended" ? snapshot?.reason ?? null : snapshot?.reason ?? reasonOf(phase),
738
764
  role,
739
- connection: snapshot?.connection ?? "stable",
765
+ // `net-blip`: in the call, a drop shorter than `reconnect_banner_delay_ms` is not something the
766
+ // person sees — the contract says what is on screen, so it reads `stable` until the banner shows.
767
+ connection: snapshot === null ? "stable" : inCall && snapshot.connection !== "stable" && !snapshot.connectionBanner ? "stable" : snapshot.connection,
740
768
  hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
741
769
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
742
770
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
743
771
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
744
772
  audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : 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
773
+ camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera, lockedFor(snapshot, "camera", inCall)) : atPrejoin ? observedPreview(region.preview.camera, region.permission.camera) : null,
774
+ microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone, lockedFor(snapshot, "microphone", inCall)) : atPrejoin ? observedPreview(region.preview.microphone, region.permission.microphone) : null
747
775
  };
748
776
  }
749
777
  function lockedFor(snapshot, kind, inCall) {
@@ -1267,7 +1295,8 @@ function useRoom() {
1267
1295
  failure: snapshot?.failure ?? null,
1268
1296
  secondDevice: snapshot?.secondDevice ?? null,
1269
1297
  local,
1270
- canShareScreen: engine?.supportsScreenShare() ?? true,
1298
+ // An observer (role `viewer`) publishes nothing: no Share screen to press and be refused (MD-16).
1299
+ canShareScreen: (engine?.supportsScreenShare() ?? true) && !isObserverRole(snapshot?.grants.role ?? null),
1271
1300
  join: (0, import_react5.useCallback)(
1272
1301
  async (request) => requireEngine(engine, "join").join(request),
1273
1302
  [engine]
@@ -1693,6 +1722,12 @@ function useMediaSupervisor() {
1693
1722
  retry: (0, import_react5.useCallback)(
1694
1723
  async (kind) => kind === "microphone" ? engine?.setMicrophoneEnabled(true) : engine?.setCameraEnabled(true),
1695
1724
  [engine]
1725
+ ),
1726
+ reportPreview: (0, import_react5.useCallback)(
1727
+ (kind, outcome) => {
1728
+ void engine?.previewDevice(kind, outcome);
1729
+ },
1730
+ [engine]
1696
1731
  )
1697
1732
  };
1698
1733
  }
@@ -2013,6 +2048,39 @@ function useCollabPanelRequest(onRequest) {
2013
2048
  // src/core/reactions.ts
2014
2049
  var MEET_REACTIONS = ["\u{1F496}", "\u{1F44D}", "\u{1F389}", "\u{1F44F}", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F914}", "\u{1F44E}"];
2015
2050
 
2051
+ // src/core/speaking-detector.ts
2052
+ function createSpeakingWhileMutedDetector(options = {}) {
2053
+ const threshold = options.threshold ?? 0.06;
2054
+ const holdMs = options.holdMs ?? 600;
2055
+ const gapMs = options.gapMs ?? 400;
2056
+ const cooldownMs = options.cooldownMs ?? 2e4;
2057
+ let speechStart = null;
2058
+ let lastLoud = null;
2059
+ let firedAt = null;
2060
+ return {
2061
+ feed(level, at) {
2062
+ if (level >= threshold) {
2063
+ if (speechStart === null || lastLoud === null || at - lastLoud > gapMs) speechStart = at;
2064
+ lastLoud = at;
2065
+ } else if (lastLoud !== null && at - lastLoud > gapMs) {
2066
+ speechStart = null;
2067
+ lastLoud = null;
2068
+ }
2069
+ if (speechStart === null || at - speechStart < holdMs) return false;
2070
+ if (firedAt !== null && at - firedAt < cooldownMs) return false;
2071
+ firedAt = at;
2072
+ speechStart = null;
2073
+ lastLoud = null;
2074
+ return true;
2075
+ },
2076
+ reset() {
2077
+ speechStart = null;
2078
+ lastLoud = null;
2079
+ firedAt = null;
2080
+ }
2081
+ };
2082
+ }
2083
+
2016
2084
  // src/react/facade.ts
2017
2085
  var NO_ACTIONS = {
2018
2086
  join: async () => void 0,
@@ -2033,6 +2101,10 @@ function actionsFor(host) {
2033
2101
  leave: () => engine.leave(),
2034
2102
  backToPrejoin: () => engine.backToPrejoin(),
2035
2103
  retry: () => {
2104
+ if (RETRYABLE_REFUSALS.has(store.snapshot().phase)) {
2105
+ void engine.rejoin().catch(() => void 0);
2106
+ return;
2107
+ }
2036
2108
  store.dispatch({ type: "retry", click: true });
2037
2109
  },
2038
2110
  resolved: () => {
@@ -2328,6 +2400,9 @@ function UndoIcon(props) {
2328
2400
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11" })
2329
2401
  ] });
2330
2402
  }
2403
+ function ChevronUpIcon(props) {
2404
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "m6 15 6-6 6 6" }) });
2405
+ }
2331
2406
 
2332
2407
  // src/skins/meet/collab.tsx
2333
2408
  var import_jsx_runtime5 = require("react/jsx-runtime");
@@ -3585,6 +3660,7 @@ function ControlBar({ onLeave }) {
3585
3660
  ) }) }) : null,
3586
3661
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BreakoutBanner, {}),
3587
3662
  panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(KnockNotice, { onView: () => setPanel("people") }) : null,
3663
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SpeakingWhileMutedNudge, {}),
3588
3664
  controls.notice !== null && panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
3589
3665
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__controls", children: [
3590
3666
  live.micLocked ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -3596,6 +3672,7 @@ function ControlBar({ onLeave }) {
3596
3672
  children: room.local?.micEnabled === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, {})
3597
3673
  }
3598
3674
  ),
3675
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AudioSettingsControl, {}),
3599
3676
  live.cameraLocked ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3600
3677
  ControlButton,
3601
3678
  {
@@ -3711,6 +3788,13 @@ function ControlBar({ onLeave }) {
3711
3788
  ]
3712
3789
  }
3713
3790
  ),
3791
+ !room.canShareScreen && room.local?.screenSharing !== true ? (
3792
+ // `share-unsupported-mobile`: the control is absent; the menu says where it lives.
3793
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__menu-note", role: "note", "data-meet-notice": "share-unsupported", children: [
3794
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ScreenShareIcon, { width: 16, height: 16 }),
3795
+ " Present from a computer"
3796
+ ] })
3797
+ ) : null,
3714
3798
  controls.isHost ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
3715
3799
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__menu-sep", role: "separator" }),
3716
3800
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
@@ -3756,6 +3840,7 @@ function ControlBar({ onLeave }) {
3756
3840
  ]
3757
3841
  }
3758
3842
  ),
3843
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(WaitingRoomRule, {}),
3759
3844
  queue.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3760
3845
  MenuItem,
3761
3846
  {
@@ -3975,6 +4060,58 @@ function MenuItem({
3975
4060
  }
3976
4061
  );
3977
4062
  }
4063
+ var LOBBY_BYPASS_CHOICES = [
4064
+ { value: "everyone", label: "Everyone" },
4065
+ { value: "org_and_invited", label: "Your organization and invited people" },
4066
+ { value: "org", label: "Your organization" },
4067
+ { value: "invited", label: "Invited people" },
4068
+ { value: "host_only", label: "Only hosts" }
4069
+ ];
4070
+ function WaitingRoomRule() {
4071
+ const host = useRequiredMeetHost();
4072
+ const [open, setOpen] = (0, import_react10.useState)(false);
4073
+ const [value, setValue] = (0, import_react10.useState)(null);
4074
+ const [notice, setNotice] = (0, import_react10.useState)(null);
4075
+ (0, import_react10.useEffect)(() => {
4076
+ let live = true;
4077
+ void host.meetings.meetingRule("lobby_bypass").then((rule) => {
4078
+ if (live) setValue(typeof rule === "string" ? rule : null);
4079
+ }).catch(() => void 0);
4080
+ return () => {
4081
+ live = false;
4082
+ };
4083
+ }, [host]);
4084
+ const current = LOBBY_BYPASS_CHOICES.find((choice) => choice.value === value)?.label ?? null;
4085
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
4086
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(MenuItem, { onSelect: () => setOpen(!open), children: [
4087
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(LockIcon, { width: 16, height: 16 }),
4088
+ " Who can skip the waiting room",
4089
+ current !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__menu-value", children: current }) : null
4090
+ ] }),
4091
+ open ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { role: "group", "aria-label": "Who can skip the waiting room", "data-meet-rule": "lobby_bypass", children: [
4092
+ LOBBY_BYPASS_CHOICES.map((choice) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
4093
+ "button",
4094
+ {
4095
+ type: "button",
4096
+ role: "menuitemradio",
4097
+ "aria-checked": choice.value === value,
4098
+ className: "mx-meet__menu-item mx-meet__menu-item--sub",
4099
+ onClick: () => {
4100
+ setNotice(null);
4101
+ void host.meetings.setMeetingRule("lobby_bypass", choice.value).then(() => setValue(choice.value)).catch((error) => setNotice(error instanceof Error ? error.message : String(error)));
4102
+ },
4103
+ children: [
4104
+ choice.value === value ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CheckIcon, { width: 14, height: 14 }) : null,
4105
+ " ",
4106
+ choice.label
4107
+ ]
4108
+ },
4109
+ choice.value
4110
+ )),
4111
+ notice !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__menu-note", role: "status", children: notice }) : null
4112
+ ] }) : null
4113
+ ] });
4114
+ }
3978
4115
  function Toast({ text, failed, onDone }) {
3979
4116
  (0, import_react10.useEffect)(() => {
3980
4117
  const timer = setTimeout(onDone, failed ? 7e3 : 4e3);
@@ -3985,6 +4122,94 @@ function Toast({ text, failed, onDone }) {
3985
4122
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: onDone, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CloseIcon, { width: 14, height: 14 }) })
3986
4123
  ] });
3987
4124
  }
4125
+ function AudioSettingsControl() {
4126
+ const host = useMeetHost();
4127
+ const devices = useDeviceSnapshot();
4128
+ const [open, setOpen] = (0, import_react10.useState)(false);
4129
+ const list = (devices?.devices.audioinput ?? []).filter((device) => device.deviceId.length > 0);
4130
+ if (host === null || list.length < 2) return null;
4131
+ const current = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
4132
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "mx-meet__anchor", children: [
4133
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4134
+ ControlButton,
4135
+ {
4136
+ label: "Audio settings",
4137
+ active: open,
4138
+ onClick: () => {
4139
+ if (!open) void host.devices.refresh();
4140
+ setOpen(!open);
4141
+ },
4142
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ChevronUpIcon, {})
4143
+ }
4144
+ ),
4145
+ open ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Popover, { label: "Microphone", onClose: () => setOpen(false), children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__menu", role: "menu", "aria-label": "Microphone", children: list.map((device) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
4146
+ "button",
4147
+ {
4148
+ type: "button",
4149
+ role: "menuitemradio",
4150
+ "aria-checked": device.deviceId === current,
4151
+ className: "mx-meet__menu-item",
4152
+ onClick: () => {
4153
+ setOpen(false);
4154
+ host.devices.select("audioinput", device.deviceId);
4155
+ void host.engine.switchDevice("audioinput", device.deviceId).catch(() => void 0);
4156
+ },
4157
+ children: [
4158
+ device.deviceId === current ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CheckIcon, { width: 16, height: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__menu-gap" }),
4159
+ device.label.length > 0 ? device.label : "Microphone"
4160
+ ]
4161
+ },
4162
+ device.deviceId
4163
+ )) }) }) : null
4164
+ ] });
4165
+ }
4166
+ function SpeakingWhileMutedNudge() {
4167
+ const room = useRoom();
4168
+ const host = useMeetHost();
4169
+ const devices = useDeviceSnapshot();
4170
+ const microphone = useLocalMedia().microphone;
4171
+ const mutedByMe = host !== null && room.local != null && room.local.micEnabled !== true && microphone.unavailable === null;
4172
+ const mic = devices?.selected.audioinput ?? host?.devices.resolve("audioinput") ?? null;
4173
+ const stream = useMicrophoneStream(mutedByMe ? mic : "off");
4174
+ const level = useAudioLevel(stream);
4175
+ const latest = (0, import_react10.useRef)(0);
4176
+ latest.current = level;
4177
+ const [shown, setShown] = (0, import_react10.useState)(false);
4178
+ const [detector] = (0, import_react10.useState)(() => createSpeakingWhileMutedDetector());
4179
+ (0, import_react10.useEffect)(() => {
4180
+ if (!mutedByMe) {
4181
+ detector.reset();
4182
+ setShown(false);
4183
+ return void 0;
4184
+ }
4185
+ const timer = setInterval(() => {
4186
+ if (detector.feed(latest.current, Date.now())) setShown(true);
4187
+ }, 150);
4188
+ return () => clearInterval(timer);
4189
+ }, [mutedByMe, detector]);
4190
+ (0, import_react10.useEffect)(() => {
4191
+ if (!shown) return void 0;
4192
+ const timer = setTimeout(() => setShown(false), 8e3);
4193
+ return () => clearTimeout(timer);
4194
+ }, [shown]);
4195
+ if (!shown || !mutedByMe) return null;
4196
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__notice-bar", role: "status", "data-meet-notice": "speaking-while-muted", children: [
4197
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, { width: 16, height: 16 }),
4198
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "You're muted" }),
4199
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4200
+ "button",
4201
+ {
4202
+ type: "button",
4203
+ className: "mx-meet__primary mx-meet__primary--small",
4204
+ onClick: () => {
4205
+ setShown(false);
4206
+ void room.toggleMicrophone();
4207
+ },
4208
+ children: "Unmute"
4209
+ }
4210
+ )
4211
+ ] });
4212
+ }
3988
4213
  function MediaNotices() {
3989
4214
  const media = useLocalMedia();
3990
4215
  const { region, where, enableAudio, acceptOffer, dismissOffer, dismissNotice, retry } = useMediaSupervisor();
@@ -4834,14 +5059,20 @@ async function playTestSound(sinkId) {
4834
5059
  audio.pause();
4835
5060
  await context.close();
4836
5061
  }
4837
- function useMicrophoneStream(deviceId) {
5062
+ function useMicrophoneStream(deviceId, report, attempt = 0) {
4838
5063
  const [stream, setStream] = (0, import_react10.useState)(null);
5064
+ const reportRef = (0, import_react10.useRef)(report);
5065
+ reportRef.current = report;
4839
5066
  (0, import_react10.useEffect)(() => {
4840
- if (deviceId === "off") return void 0;
5067
+ if (deviceId === "off") {
5068
+ reportRef.current?.("off");
5069
+ return void 0;
5070
+ }
4841
5071
  let live = true;
4842
5072
  let opened = null;
4843
5073
  const media = globalThis.navigator?.mediaDevices;
4844
5074
  if (media === void 0) return void 0;
5075
+ reportRef.current?.("prompting");
4845
5076
  void media.getUserMedia({ audio: deviceId === null ? true : { deviceId: { exact: deviceId } } }).then((next) => {
4846
5077
  if (!live) {
4847
5078
  next.getTracks().forEach((track) => track.stop());
@@ -4849,15 +5080,49 @@ function useMicrophoneStream(deviceId) {
4849
5080
  }
4850
5081
  opened = next;
4851
5082
  setStream(next);
4852
- }).catch(() => setStream(null));
5083
+ reportRef.current?.("on");
5084
+ }).catch((thrown) => {
5085
+ setStream(null);
5086
+ if (live) reportRef.current?.({ error: thrown });
5087
+ });
4853
5088
  return () => {
4854
5089
  live = false;
4855
5090
  opened?.getTracks().forEach((track) => track.stop());
4856
5091
  setStream(null);
4857
5092
  };
4858
- }, [deviceId]);
5093
+ }, [deviceId, attempt]);
4859
5094
  return stream;
4860
5095
  }
5096
+ function PreviewStatus({ onRetry }) {
5097
+ const { region, where } = useMediaSupervisor();
5098
+ const [waited, setWaited] = (0, import_react10.useState)(false);
5099
+ const prompting = region.preview.microphone.state === "prompting" || region.preview.camera.state === "prompting";
5100
+ (0, import_react10.useEffect)(() => {
5101
+ if (!prompting) {
5102
+ setWaited(false);
5103
+ return void 0;
5104
+ }
5105
+ const timer = setTimeout(() => setWaited(true), 800);
5106
+ return () => clearTimeout(timer);
5107
+ }, [prompting]);
5108
+ const refused = ["microphone", "camera"].flatMap((kind) => {
5109
+ const preview = region.preview[kind];
5110
+ const reason = region.permission[kind] === "denied" ? "denied" : preview.state === "unavailable" ? preview.unavailable?.reason ?? "failed" : null;
5111
+ return reason === null ? [] : [{ kind, reason }];
5112
+ });
5113
+ if (refused.length === 0) {
5114
+ if (!prompting || !waited) return null;
5115
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__panel-note", role: "status", "data-meet-notice": "permission-prompt", children: "Allow camera and microphone when your browser asks" });
5116
+ }
5117
+ const first = refused[0];
5118
+ const both = refused.length === 2 && refused[0].reason === refused[1].reason;
5119
+ const label = both ? deviceChipLabel("camera", first.reason).replace(/^Camera/, "Camera and mic") : refused.map((r) => deviceChipLabel(r.kind, r.reason)).join(" \xB7 ");
5120
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__warning", role: "status", "data-meet-notice": deviceNoticeCode(first.kind, first.reason), children: [
5121
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("strong", { children: label }),
5122
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ol", { className: "mx-meet__fix-steps", children: deviceFixSteps(first.kind, first.reason, where).map((step) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("li", { children: step }, step)) }),
5123
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Button, { type: "button", variant: "outline", size: "sm", onClick: onRetry, children: "Try again" })
5124
+ ] });
5125
+ }
4861
5126
  function KnockNotice({ onView }) {
4862
5127
  const { knocker, others, admit, dismiss } = useKnockNotice();
4863
5128
  if (knocker === null) return null;
@@ -4935,7 +5200,11 @@ function WaitingList({ lobby }) {
4935
5200
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Avatar, { name: entry.displayName, url: entry.avatarUrl, size: 32 }),
4936
5201
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "mx-meet__person-name", children: [
4937
5202
  entry.displayName,
4938
- entry.userId === null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Guest" }) : null
5203
+ entry.userId === null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Guest" }) : null,
5204
+ entry.flag === "previously_removed" ? (
5205
+ // `mod-removed-bypass`: advice for the host, never a block — Admit stays.
5206
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge mx-meet__badge--warn", "data-meet-notice": "knock-flagged", children: "Previously removed" })
5207
+ ) : null
4939
5208
  ] }),
4940
5209
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ControlButton, { label: `Admit ${entry.displayName}`, onClick: () => void lobby.admit(entry), children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CheckIcon, { width: 16, height: 16 }) }),
4941
5210
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -5187,8 +5456,15 @@ function PreJoinBody({
5187
5456
  const [joining, setJoining] = (0, import_react10.useState)(false);
5188
5457
  const camera = devices?.selected.videoinput ?? host.devices.resolve("videoinput");
5189
5458
  const mic = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
5190
- const preview = useCameraPreview(mode === "video" ? camera : void 0, effects.effects);
5191
- const micStream = useMicrophoneStream(microphone ? mic : "off");
5459
+ const { reportPreview } = useMediaSupervisor();
5460
+ const [attempt, setAttempt] = (0, import_react10.useState)(0);
5461
+ const preview = useCameraPreview(
5462
+ mode === "video" ? camera : void 0,
5463
+ effects.effects,
5464
+ (outcome) => reportPreview("camera", outcome),
5465
+ attempt
5466
+ );
5467
+ const micStream = useMicrophoneStream(microphone ? mic : "off", (outcome) => reportPreview("microphone", outcome), attempt);
5192
5468
  const level = useAudioLevel(micStream);
5193
5469
  const [speakerTesting, setSpeakerTesting] = (0, import_react10.useState)(false);
5194
5470
  (0, import_react10.useEffect)(() => {
@@ -5232,6 +5508,7 @@ function PreJoinBody({
5232
5508
  ] }),
5233
5509
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__prejoin-panel", children: [
5234
5510
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h1", { className: "mx-meet__prejoin-title", children: "Ready to join?" }),
5511
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PreviewStatus, { onRetry: () => setAttempt((n) => n + 1) }),
5235
5512
  notice?.kind === "removed" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__warning", children: notice.text }) : null,
5236
5513
  devices?.labelsHidden === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__panel-note", children: [
5237
5514
  "Your browser is hiding device names until you allow camera and microphone access.",
@@ -5334,16 +5611,22 @@ function PreJoinBody({
5334
5611
  ] })
5335
5612
  ] });
5336
5613
  }
5337
- function useCameraPreview(deviceId, effects) {
5614
+ function useCameraPreview(deviceId, effects, report, attempt = 0) {
5615
+ const reportRef = (0, import_react10.useRef)(report);
5616
+ reportRef.current = report;
5338
5617
  const [preview, setPreview] = (0, import_react10.useState)(null);
5339
5618
  const [error, setError] = (0, import_react10.useState)(null);
5340
5619
  const current = (0, import_react10.useRef)(null);
5341
5620
  const initialEffect = (0, import_react10.useRef)(toVideoEffect(effects));
5342
5621
  initialEffect.current = toVideoEffect(effects);
5343
5622
  (0, import_react10.useEffect)(() => {
5344
- if (deviceId === void 0) return void 0;
5623
+ if (deviceId === void 0) {
5624
+ reportRef.current?.("off");
5625
+ return void 0;
5626
+ }
5345
5627
  let live = true;
5346
5628
  setError(null);
5629
+ reportRef.current?.("prompting");
5347
5630
  void createCameraPreview(deviceId, initialEffect.current).then((created) => {
5348
5631
  if (!live) {
5349
5632
  created.stop();
@@ -5351,8 +5634,10 @@ function useCameraPreview(deviceId, effects) {
5351
5634
  }
5352
5635
  current.current = created;
5353
5636
  setPreview(created);
5637
+ reportRef.current?.("on");
5354
5638
  }).catch((thrown) => {
5355
5639
  if (!live) return;
5640
+ reportRef.current?.({ error: thrown });
5356
5641
  const failure = thrown;
5357
5642
  const message = (failure.message ?? "").replace(/^meet\.[A-Za-z.()]+:\s*/, "");
5358
5643
  const sentence = message.length > 0 ? message.charAt(0).toUpperCase() + message.slice(1) : "";
@@ -5364,7 +5649,7 @@ function useCameraPreview(deviceId, effects) {
5364
5649
  current.current = null;
5365
5650
  setPreview(null);
5366
5651
  };
5367
- }, [deviceId]);
5652
+ }, [deviceId, attempt]);
5368
5653
  return {
5369
5654
  preview,
5370
5655
  error,
@@ -5504,6 +5789,14 @@ function MeetSkinRoom({
5504
5789
  }
5505
5790
  function ShareNotice() {
5506
5791
  const { share, actions } = useMeetCore();
5792
+ const [soundHintClosed, setSoundHintClosed] = (0, import_react10.useState)(null);
5793
+ if (share.state === "presenting" && share.audio === false && soundHintClosed !== share.at) {
5794
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "share-audio-missing", children: [
5795
+ "Sound not shared",
5796
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__remedy", children: "To include sound, share a tab and turn on Also share tab audio" }),
5797
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setSoundHintClosed(share.at), children: "Dismiss" })
5798
+ ] });
5799
+ }
5507
5800
  if (share.state !== "refused") return null;
5508
5801
  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";
5509
5802
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "share-unavailable", children: [
@@ -5536,7 +5829,20 @@ function ExitScreen({ onLeave }) {
5536
5829
  const { phase, reason, host, snapshot, actions, call, connection } = useMeetCore();
5537
5830
  const [busy, setBusy] = (0, import_react10.useState)(false);
5538
5831
  const failure = snapshot?.failure ?? null;
5539
- const retryLeft = useCountdown(phase === "refused:lobby_rejects" ? failure?.retryAt ?? null : null);
5832
+ const retryLeft = useCountdown(
5833
+ phase === "refused:lobby_rejects" || phase === "refused:too_early" ? failure?.retryAt ?? null : null
5834
+ );
5835
+ const reaskedAtZero = (0, import_react10.useRef)(false);
5836
+ (0, import_react10.useEffect)(() => {
5837
+ if (phase !== "refused:too_early") {
5838
+ reaskedAtZero.current = false;
5839
+ return;
5840
+ }
5841
+ if (retryLeft === 0 && !reaskedAtZero.current) {
5842
+ reaskedAtZero.current = true;
5843
+ actions.retry();
5844
+ }
5845
+ }, [phase, retryLeft, actions]);
5540
5846
  const rejoin = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5541
5847
  import_design_system.Button,
5542
5848
  {
@@ -5574,11 +5880,19 @@ function ExitScreen({ onLeave }) {
5574
5880
  if (phase.startsWith("refused:")) {
5575
5881
  const code = phase.slice("refused:".length);
5576
5882
  title = REFUSED_TITLES[code] ?? "You can't join this meeting";
5577
- line = code === "too_early" || code === "registration_required" || code === "cancelled" ? failure?.remedy ?? null : null;
5883
+ line = code === "too_early" || code === "registration_required" || code === "cancelled" || code === "wrong_account" ? failure?.remedy ?? null : null;
5884
+ if (code === "too_early" && retryLeft !== null && retryLeft > 0) line = `Opens in ${clockText(retryLeft)}`;
5578
5885
  actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
5579
5886
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Button, { type: "button", variant: "outline", onClick: actions.retry, children: "Try again" }),
5580
5887
  home
5581
5888
  ] });
5889
+ if (code === "sign_in_required" || code === "wrong_account") {
5890
+ const back = typeof window === "undefined" ? "/" : `${window.location.pathname}${window.location.search}`;
5891
+ actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
5892
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Button, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("a", { href: `/login?redirectTo=${encodeURIComponent(back)}`, children: code === "sign_in_required" ? "Sign in" : "Switch account" }) }),
5893
+ home
5894
+ ] });
5895
+ }
5582
5896
  if (code === "lobby_rejects") {
5583
5897
  if (retryLeft !== null && retryLeft > 0) {
5584
5898
  line = `You can ask again in ${clockText(retryLeft)}`;
@@ -5616,6 +5930,27 @@ function ExitScreen({ onLeave }) {
5616
5930
  ] });
5617
5931
  break;
5618
5932
  case "disconnected":
5933
+ if (failure?.stage === "join" && (failure.reason ?? null) === null) {
5934
+ title = "Can't connect to the meeting";
5935
+ line = connection === "offline" ? "You're offline. Check your internet connection." : "Check your network. A VPN, firewall or proxy may be blocking the call.";
5936
+ notice = "cannot-connect";
5937
+ actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
5938
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5939
+ import_design_system.Button,
5940
+ {
5941
+ type: "button",
5942
+ disabled: busy,
5943
+ onClick: () => {
5944
+ setBusy(true);
5945
+ void actions.rejoin().catch(() => void 0).finally(() => setBusy(false));
5946
+ },
5947
+ children: "Try again"
5948
+ }
5949
+ ),
5950
+ home
5951
+ ] });
5952
+ break;
5953
+ }
5619
5954
  title = "You lost your connection to the meeting";
5620
5955
  line = connection === "offline" ? "You're offline. You'll rejoin automatically when your internet is back." : connection === "server_unreachable" ? "Can't reach the meeting server. You'll rejoin automatically when it answers." : failure?.remedy ?? "Check your internet connection, then rejoin.";
5621
5956
  notice = "disconnected";