@ai-matrx/meet 0.11.42 → 0.11.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.cjs CHANGED
@@ -122,6 +122,7 @@ __export(react_exports, {
122
122
  autoAssign: () => autoAssign,
123
123
  autoRejoinDelayMs: () => autoRejoinDelayMs,
124
124
  availableActivities: () => availableActivities,
125
+ breakoutClosingMs: () => breakoutClosingMs,
125
126
  breakoutRemainingMs: () => breakoutRemainingMs,
126
127
  callInviteAction: () => callInviteAction,
127
128
  callInviteText: () => callInviteText,
@@ -157,6 +158,7 @@ __export(react_exports, {
157
158
  createReconnectPolicy: () => createReconnectPolicy,
158
159
  createRecordingController: () => createRecordingController,
159
160
  createRoomEngine: () => createRoomEngine,
161
+ createSpeakingWhileMutedDetector: () => createSpeakingWhileMutedDetector,
160
162
  createTokenClient: () => createTokenClient,
161
163
  createWebDeviceStorage: () => createWebDeviceStorage,
162
164
  createWebEffectsStorage: () => createWebEffectsStorage,
@@ -165,6 +167,7 @@ __export(react_exports, {
165
167
  createWebRoomTokenStorage: () => createWebRoomTokenStorage,
166
168
  defaultCaptionLanguage: () => defaultCaptionLanguage,
167
169
  detectBrowser: () => detectBrowser,
170
+ detectCapabilities: () => detectCapabilities,
168
171
  detectOs: () => detectOs,
169
172
  deviceChipLabel: () => deviceChipLabel,
170
173
  deviceFixSteps: () => deviceFixSteps,
@@ -189,6 +192,7 @@ __export(react_exports, {
189
192
  isLinkFollower: () => isLinkFollower,
190
193
  isListenOnly: () => isListenOnly,
191
194
  isLivePhase: () => isLivePhase,
195
+ isMobileUserAgent: () => isMobileUserAgent,
192
196
  isNoteTaker: () => isNoteTaker,
193
197
  isObserverRole: () => isObserverRole,
194
198
  isRealName: () => isRealName,
@@ -281,6 +285,7 @@ __export(react_exports, {
281
285
  transcriptDocument: () => transcriptDocument,
282
286
  unavailableFrom: () => unavailableFrom,
283
287
  uniqueAttendees: () => uniqueAttendees,
288
+ unsupportedWords: () => unsupportedWords,
284
289
  untranslatedLines: () => untranslatedLines,
285
290
  useActivityAttention: () => useActivityAttention,
286
291
  useAudioLevel: () => useAudioLevel,
@@ -289,6 +294,7 @@ __export(react_exports, {
289
294
  useCanModerate: () => useCanModerate,
290
295
  useCanRecord: () => useCanRecord,
291
296
  useCaptionTranslation: () => useCaptionTranslation,
297
+ useCaptions: () => useCaptions,
292
298
  useCollab: () => useCollab,
293
299
  useCollabPanelRequest: () => useCollabPanelRequest,
294
300
  useConsentNotice: () => useConsentNotice,
@@ -1107,6 +1113,8 @@ var MEET_ROUTES = {
1107
1113
  noteTakerStop: "/api/v1/meet/agent/stop",
1108
1114
  /** In-meeting Q&A. Streams NDJSON; body is `{meeting_id, question}` only. */
1109
1115
  intelligenceAsk: "/api/v1/meet/intelligence/ask",
1116
+ /** Anyone in the meeting (guests with the room pass included) asks for captions for themselves. */
1117
+ noteTakerCaptions: "/api/v1/meet/agent/captions",
1110
1118
  /** A host ends the meeting for everyone. Served by MRI-A6. */
1111
1119
  meetingEnd: "/api/v1/meet/end",
1112
1120
  /**
@@ -1253,7 +1261,8 @@ function parseRoomToken(body, roomName) {
1253
1261
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
1254
1262
  observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
1255
1263
  connectionPolicy: connectionPolicyFrom(payload["connection_policy"]),
1256
- leavePolicy: leavePolicyFrom(payload["leave_policy"])
1264
+ leavePolicy: leavePolicyFrom(payload["leave_policy"]),
1265
+ canReclaim: payload["can_reclaim"] === true
1257
1266
  };
1258
1267
  }
1259
1268
  function createTokenClient(options) {
@@ -1715,6 +1724,42 @@ function createMeetAi(options) {
1715
1724
  },
1716
1725
  "meet.ai.startNoteTaker"
1717
1726
  ),
1727
+ requestCaptions: async ({ meetingId }) => {
1728
+ const roomToken = options.resolveRoomToken?.() ?? null;
1729
+ const operation = "meet.ai.requestCaptions";
1730
+ try {
1731
+ const response = await send({
1732
+ path: MEET_ROUTES.noteTakerCaptions,
1733
+ body: { meeting_id: meetingId },
1734
+ operation,
1735
+ requireAuth: roomToken === null,
1736
+ ...roomToken !== null ? { headers: { [MEETING_PASS_HEADER]: roomToken } } : {}
1737
+ });
1738
+ if (!response.ok) {
1739
+ options.store.patch({ captionsRefusal: await refusalFor(response, operation) });
1740
+ return;
1741
+ }
1742
+ let payload = null;
1743
+ try {
1744
+ payload = await response.json();
1745
+ } catch {
1746
+ payload = null;
1747
+ }
1748
+ const row = typeof payload === "object" && payload !== null ? payload : {};
1749
+ const detail = typeof row["detail"] === "string" ? row["detail"] : "";
1750
+ options.store.patch({
1751
+ captionsRefusal: row["dispatched"] === true ? null : { status: 200, reason: detail, remedy: "" }
1752
+ });
1753
+ } catch (error) {
1754
+ options.store.patch({
1755
+ captionsRefusal: {
1756
+ status: 0,
1757
+ reason: error.message,
1758
+ remedy: error.remedy ?? ""
1759
+ }
1760
+ });
1761
+ }
1762
+ },
1718
1763
  stopNoteTaker: ({ meetingId }) => dispatch(
1719
1764
  MEET_ROUTES.noteTakerStop,
1720
1765
  { meeting_id: meetingId },
@@ -2701,12 +2746,63 @@ function recordingLabel(state) {
2701
2746
  }
2702
2747
  }
2703
2748
 
2749
+ // src/core/live-policy.ts
2750
+ var DEFAULT_LIVE_POLICY = {
2751
+ chatMode: "everyone",
2752
+ avLock: "off",
2753
+ timeLimitMinutes: 0,
2754
+ timeLimitWarningMinutes: 5,
2755
+ recordingConsent: "notice"
2756
+ };
2757
+ function oneOf(value, allowed, fallback) {
2758
+ return typeof value === "string" && allowed.includes(value) ? value : fallback;
2759
+ }
2760
+ function whole(value, fallback) {
2761
+ const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
2762
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
2763
+ }
2764
+ function livePolicyFrom(raw) {
2765
+ const o = typeof raw === "object" && raw !== null ? raw : {};
2766
+ return {
2767
+ chatMode: oneOf(o["chat_mode"], ["everyone", "hosts_only", "off"], DEFAULT_LIVE_POLICY.chatMode),
2768
+ avLock: oneOf(o["attendee_av_lock"], ["off", "mic", "camera", "both"], DEFAULT_LIVE_POLICY.avLock),
2769
+ timeLimitMinutes: whole(o["time_limit_minutes"], 0),
2770
+ timeLimitWarningMinutes: whole(o["time_limit_warning_minutes"], DEFAULT_LIVE_POLICY.timeLimitWarningMinutes),
2771
+ recordingConsent: oneOf(
2772
+ o["recording_consent"],
2773
+ ["notice", "ok_or_leave", "view_only_until_agree"],
2774
+ DEFAULT_LIVE_POLICY.recordingConsent
2775
+ )
2776
+ };
2777
+ }
2778
+ function samePolicy(a, b) {
2779
+ return a.chatMode === b.chatMode && a.avLock === b.avLock && a.timeLimitMinutes === b.timeLimitMinutes && a.timeLimitWarningMinutes === b.timeLimitWarningMinutes && a.recordingConsent === b.recordingConsent;
2780
+ }
2781
+ var MODERATORS = /* @__PURE__ */ new Set(["host", "cohost"]);
2782
+ function isLockedFor(policy, role, kind) {
2783
+ if (MODERATORS.has(role)) return false;
2784
+ return policy.avLock === "both" || (kind === "microphone" ? policy.avLock === "mic" : policy.avLock === "camera");
2785
+ }
2786
+ function consentPending(policy, role, recordingOn, given) {
2787
+ return recordingOn && !given && policy.recordingConsent !== "notice" && !MODERATORS.has(role);
2788
+ }
2789
+ function canChat(policy, role) {
2790
+ if (policy.chatMode === "everyone") return true;
2791
+ return policy.chatMode === "hosts_only" && MODERATORS.has(role);
2792
+ }
2793
+ function minutesLeft(policy, startedAtMs, nowMs) {
2794
+ if (policy.timeLimitMinutes <= 0 || startedAtMs === null) return null;
2795
+ return Math.ceil((startedAtMs + policy.timeLimitMinutes * 6e4 - nowMs) / 6e4);
2796
+ }
2797
+
2704
2798
  // src/core/attendance.ts
2799
+ var IDLE_PREVIEW = { state: "idle", unavailable: null };
2705
2800
  var IDLE_MEDIA_REGION = {
2706
2801
  permission: { microphone: "unknown", camera: "unknown" },
2707
2802
  audioOutput: "ok",
2708
2803
  notice: null,
2709
- offer: null
2804
+ offer: null,
2805
+ preview: { microphone: IDLE_PREVIEW, camera: IDLE_PREVIEW }
2710
2806
  };
2711
2807
  var IDLE_TRACK = { enabled: false, unavailable: null };
2712
2808
  var IDLE_MEDIA = {
@@ -3827,6 +3923,8 @@ function planFromRow(row) {
3827
3923
  minutes: num2(row["minutes"]) ?? 0,
3828
3924
  openedAt: str(row["opened_at"]),
3829
3925
  endsAt: str(row["ends_at"]),
3926
+ closingAt: str(row["closing_at"]),
3927
+ moveMode: row["move_mode"] === "auto" ? "auto" : "invite",
3830
3928
  closedAt: str(row["closed_at"]),
3831
3929
  broadcasts: (Array.isArray(row["broadcasts"]) ? row["broadcasts"] : []).map(broadcastFrom).filter((b) => b !== null)
3832
3930
  };
@@ -3978,9 +4076,17 @@ function autoAssign(identities, rooms, exclude = []) {
3978
4076
  return out;
3979
4077
  }
3980
4078
  function breakoutRemainingMs(plan, now) {
3981
- if (plan === null || plan.state !== "open" || plan.endsAt === null) return null;
3982
- const end = Date.parse(plan.endsAt);
3983
- return Number.isNaN(end) ? null : Math.max(0, end - now);
4079
+ if (plan === null || plan.state !== "open") return null;
4080
+ const ends = [plan.endsAt, plan.closingAt].map((stamp) => stamp === null ? Number.NaN : Date.parse(stamp)).filter((time) => !Number.isNaN(time));
4081
+ if (ends.length === 0) return null;
4082
+ return Math.max(0, Math.min(...ends) - now);
4083
+ }
4084
+ var BREAKOUT_COUNTDOWN_MS = 3e4;
4085
+ function breakoutClosingMs(plan, now) {
4086
+ if (plan === null || plan.state !== "open") return null;
4087
+ const remaining = breakoutRemainingMs(plan, now);
4088
+ if (remaining === null) return null;
4089
+ return plan.closingAt !== null || remaining <= BREAKOUT_COUNTDOWN_MS ? remaining : null;
3984
4090
  }
3985
4091
  function defaultCaptionLanguage(settings, viewerLanguage) {
3986
4092
  const configured = settings?.captionTranslationLanguage.trim() ?? "";
@@ -4157,6 +4263,11 @@ var EMPTY = {
4157
4263
  presenter: null,
4158
4264
  waitingCount: 0,
4159
4265
  leavePolicy: DEFAULT_LEAVE_POLICY,
4266
+ livePolicy: DEFAULT_LIVE_POLICY,
4267
+ canReclaim: false,
4268
+ recordingConsentGiven: false,
4269
+ sessionStartedAt: null,
4270
+ mutedByHost: false,
4160
4271
  moderationNotice: null,
4161
4272
  actionNotice: null,
4162
4273
  privateChatRequest: null,
@@ -4166,6 +4277,8 @@ var EMPTY = {
4166
4277
  names: {},
4167
4278
  wrapUp: null,
4168
4279
  noteTakerRefusal: null,
4280
+ captionsOn: false,
4281
+ captionsRefusal: null,
4169
4282
  media: IDLE_MEDIA,
4170
4283
  mediaRegion: IDLE_MEDIA_REGION,
4171
4284
  failure: null,
@@ -4370,7 +4483,12 @@ function createMeetStore(initial = {}) {
4370
4483
  if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.reason === next.unavailable?.reason && current.unavailable?.message === next.unavailable?.message) {
4371
4484
  return;
4372
4485
  }
4373
- commit({ ...state, media: { ...state.media, [kind]: next } });
4486
+ commit({
4487
+ ...state,
4488
+ media: { ...state.media, [kind]: next },
4489
+ // The person turned their own microphone on again: the host's mute is over.
4490
+ ...kind === "microphone" && next.enabled && state.mutedByHost ? { mutedByHost: false } : {}
4491
+ });
4374
4492
  },
4375
4493
  reset() {
4376
4494
  const phase = nextPhase(state.phase, { type: "reset", teardown: true }) ?? EMPTY.phase;
@@ -4432,6 +4550,7 @@ function applyDoorRow(store, attendee) {
4432
4550
  function decidedState(state) {
4433
4551
  return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
4434
4552
  }
4553
+ var LIVE_POLICY_POLL_MS = 3e3;
4435
4554
  function createMeetingSession(options) {
4436
4555
  const { manager, repository, store, identity } = options;
4437
4556
  let channel = null;
@@ -4573,6 +4692,46 @@ function createMeetingSession(options) {
4573
4692
  stopWaiting();
4574
4693
  }
4575
4694
  }
4695
+ let policyTimer = null;
4696
+ let policyReading = false;
4697
+ async function readLivePolicy() {
4698
+ const meeting = store.snapshot().meeting;
4699
+ const pass = options.resolvePass?.() ?? null;
4700
+ if (meeting === null || pass === null || policyReading) return;
4701
+ policyReading = true;
4702
+ try {
4703
+ const response = await options.api.request({
4704
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}`,
4705
+ method: "GET",
4706
+ requireAuth: false,
4707
+ operation: "meet.state(policy)",
4708
+ headers: { [MEETING_PASS_HEADER]: pass }
4709
+ });
4710
+ if (!response.ok) return;
4711
+ const read = await response.json();
4712
+ const next = livePolicyFrom(read.live_policy);
4713
+ const started = read.session_started_at ? Date.parse(read.session_started_at) : NaN;
4714
+ const snapshot = store.snapshot();
4715
+ const startedAt = Number.isFinite(started) ? started : snapshot.sessionStartedAt;
4716
+ if (!samePolicy(snapshot.livePolicy, next) || startedAt !== snapshot.sessionStartedAt) {
4717
+ store.patch({ livePolicy: next, sessionStartedAt: startedAt });
4718
+ }
4719
+ } catch {
4720
+ } finally {
4721
+ policyReading = false;
4722
+ }
4723
+ }
4724
+ function syncLivePolicyTimer() {
4725
+ const inCall = store.snapshot().phase === "in_call";
4726
+ if (inCall && policyTimer === null) {
4727
+ void readLivePolicy();
4728
+ policyTimer = setInterval(() => void readLivePolicy(), LIVE_POLICY_POLL_MS);
4729
+ } else if (!inCall && policyTimer !== null) {
4730
+ clearInterval(policyTimer);
4731
+ policyTimer = null;
4732
+ }
4733
+ }
4734
+ const unsubscribePolicy = store.subscribe(syncLivePolicyTimer);
4576
4735
  async function readState2() {
4577
4736
  if (reading !== null) return reading;
4578
4737
  const meeting = store.snapshot().meeting;
@@ -4959,6 +5118,9 @@ function createMeetingSession(options) {
4959
5118
  },
4960
5119
  dispose() {
4961
5120
  stopWaiting();
5121
+ unsubscribePolicy();
5122
+ if (policyTimer !== null) clearInterval(policyTimer);
5123
+ policyTimer = null;
4962
5124
  channel?.close();
4963
5125
  channel = null;
4964
5126
  meetingChannel?.close();
@@ -4995,6 +5157,47 @@ function createMeetSessionIdentity(options) {
4995
5157
  });
4996
5158
  }
4997
5159
 
5160
+ // src/core/capabilities.ts
5161
+ var IN_APP = [
5162
+ [/Instagram/i, "Instagram"],
5163
+ [/FBAN|FBAV|FB_IAB/i, "Facebook"],
5164
+ [/LinkedInApp/i, "LinkedIn"],
5165
+ [/MicroMessenger/i, "WeChat"],
5166
+ [/Snapchat/i, "Snapchat"],
5167
+ [/\bLine\//i, "LINE"],
5168
+ [/Twitter|TwitterAndroid/i, "X"],
5169
+ [/TikTok|musical_ly|BytedanceWebview/i, "TikTok"]
5170
+ ];
5171
+ function isMobileUserAgent(userAgent, maxTouchPoints = 0) {
5172
+ if (/iPhone|iPod|Android|Mobile|iPad/i.test(userAgent)) return true;
5173
+ return /Macintosh/.test(userAgent) && maxTouchPoints > 1;
5174
+ }
5175
+ function detectCapabilities(scope = typeof globalThis === "undefined" ? null : globalThis) {
5176
+ if (scope === null || scope.navigator === void 0) {
5177
+ return { canJoin: true, reason: null, inAppBrowser: null, mobile: false, canShareScreen: false };
5178
+ }
5179
+ const ua = scope.navigator.userAgent ?? "";
5180
+ const webrtc = typeof scope.RTCPeerConnection === "function" || typeof scope.webkitRTCPeerConnection === "function";
5181
+ const mobile = isMobileUserAgent(ua, scope.navigator.maxTouchPoints ?? 0);
5182
+ const inAppBrowser = IN_APP.find(([pattern]) => pattern.test(ua))?.[1] ?? null;
5183
+ return {
5184
+ canJoin: webrtc,
5185
+ reason: webrtc ? null : "no_webrtc",
5186
+ inAppBrowser,
5187
+ mobile,
5188
+ canShareScreen: !mobile && typeof scope.navigator.mediaDevices?.getDisplayMedia === "function"
5189
+ };
5190
+ }
5191
+ function unsupportedWords(capabilities) {
5192
+ if (capabilities.inAppBrowser !== null) {
5193
+ return {
5194
+ title: `${capabilities.inAppBrowser}'s browser can't join calls`,
5195
+ body: "Open this link in Chrome, Safari, Edge or Firefox"
5196
+ };
5197
+ }
5198
+ return { title: "This browser can't join calls", body: "Open this link in Chrome, Safari, Edge or Firefox" };
5199
+ }
5200
+
4998
5201
  // src/core/media-supervisor.ts
4999
5202
  function classifyDeviceFailure(error, permission) {
5000
5203
  if (permission === "denied") return "denied";
@@ -5055,7 +5258,7 @@ function deviceFixSteps(kind, reason, where) {
5055
5258
  case "safari":
5056
5259
  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"];
5057
5260
  default:
5058
- return ["Click the icon left of the web address", `Set ${device} to Allow`, "Press Try again"];
5261
+ return ["Click the site settings icon in the address bar", `Set ${device} to Allow`, "Press Try again"];
5059
5262
  }
5060
5263
  case "blocked_os": {
5061
5264
  const app = where.browser === "chrome" ? "Google Chrome" : where.browser === "edge" ? "Microsoft Edge" : where.browser === "firefox" ? "Firefox" : "your browser";
@@ -5153,7 +5356,7 @@ function createMediaSupervisor(options) {
5153
5356
  function setRegion(next) {
5154
5357
  const current = region();
5155
5358
  const merged = { ...current, ...next };
5156
- if (merged.audioOutput === current.audioOutput && merged.notice === current.notice && merged.offer === current.offer && merged.permission.microphone === current.permission.microphone && merged.permission.camera === current.permission.camera) {
5359
+ if (merged.audioOutput === current.audioOutput && merged.notice === current.notice && merged.offer === current.offer && merged.preview === current.preview && merged.permission.microphone === current.permission.microphone && merged.permission.camera === current.permission.camera) {
5157
5360
  return;
5158
5361
  }
5159
5362
  store.patch({ mediaRegion: merged });
@@ -5249,7 +5452,7 @@ function createMediaSupervisor(options) {
5249
5452
  if (permission !== "unknown") setPermission(kind, permission);
5250
5453
  return unavailableFrom(normalized, classifyDeviceFailure(normalized, permission));
5251
5454
  }
5252
- return {
5455
+ const api = {
5253
5456
  start() {
5254
5457
  if (started || platform === null) return;
5255
5458
  started = true;
@@ -5271,6 +5474,16 @@ function createMediaSupervisor(options) {
5271
5474
  started = false;
5272
5475
  },
5273
5476
  unavailable,
5477
+ async preview(kind, outcome) {
5478
+ api.start();
5479
+ let next;
5480
+ if (typeof outcome === "string") next = { state: outcome, unavailable: null };
5481
+ else next = { state: "unavailable", unavailable: await unavailable(outcome.error, kind, `meet.${kind}Preview`) };
5482
+ const current = region().preview;
5483
+ const was = current[kind];
5484
+ if (was.state === next.state && was.unavailable?.reason === next.unavailable?.reason && was.unavailable?.message === next.unavailable?.message) return;
5485
+ setRegion({ preview: { ...region().preview, [kind]: next } });
5486
+ },
5274
5487
  onAudioPlayback(canPlay) {
5275
5488
  setRegion({ audioOutput: canPlay ? "ok" : "blocked" });
5276
5489
  },
@@ -5329,6 +5542,7 @@ function createMediaSupervisor(options) {
5329
5542
  setRegion({ notice: null });
5330
5543
  }
5331
5544
  };
5545
+ return api;
5332
5546
  }
5333
5547
 
5334
5548
  // src/core/room-events.ts
@@ -5548,10 +5762,11 @@ function moderationText(kind, payload) {
5548
5762
  switch (kind) {
5549
5763
  case "muted": {
5550
5764
  const source = text2(payload["source"]) ?? "microphone";
5551
- return source === "microphone" ? `${by} muted you.` : source === "camera" ? `${by} turned off your camera.` : `${by} stopped your screen share.`;
5765
+ const who = payload["by_role"] === "host" || payload["by_role"] === "cohost" ? "the host" : by;
5766
+ return source === "microphone" ? `You've been muted by ${who}.` : source === "camera" ? `Your camera was turned off by ${who}.` : `Your screen share was stopped by ${who}.`;
5552
5767
  }
5553
5768
  case "ask-unmute":
5554
- return `${by} is asking you to unmute.`;
5769
+ return `${by === "The host" ? "The host" : by} asked you to unmute.`;
5555
5770
  case "removed":
5556
5771
  return `${by} removed you from this meeting.`;
5557
5772
  default:
@@ -5577,7 +5792,7 @@ function applyModeration(store, payload, now) {
5577
5792
  grants: { ...snapshot.grants, role },
5578
5793
  moderationNotice: {
5579
5794
  kind: "role",
5580
- text: role === "host" ? "You are now the host." : role === "cohost" ? "You are now a co-host." : "You are no longer a co-host.",
5795
+ text: role === "host" ? "You are now the host." : role === "cohost" ? "You're now a co-host." : role === "presenter" ? "You're now a presenter." : snapshot.grants.role === "presenter" ? "You're now a participant: you can no longer present." : "You're now a participant: you are no longer a co-host.",
5581
5796
  at: now()
5582
5797
  }
5583
5798
  });
@@ -5589,7 +5804,10 @@ function applyModeration(store, payload, now) {
5589
5804
  case "removed": {
5590
5805
  const sentence = moderationText(kind, payload);
5591
5806
  if (sentence === null) return ignored("a moderation event with no sentence");
5592
- store.patch({ moderationNotice: { kind, text: sentence, at: now() } });
5807
+ store.patch({
5808
+ moderationNotice: { kind, text: sentence, at: now() },
5809
+ ...kind === "muted" && (text2(payload["source"]) ?? "microphone") === "microphone" ? { mutedByHost: true } : {}
5810
+ });
5593
5811
  return HANDLED;
5594
5812
  }
5595
5813
  default:
@@ -5628,7 +5846,7 @@ function exactNoteTakerParticipant(meeting, participants) {
5628
5846
  }
5629
5847
  function noteTakerStatus(args) {
5630
5848
  const present = exactNoteTakerParticipant(args.meeting, args.participants) !== null;
5631
- const raw = args.meeting?.metadata[NOTE_TAKER_METADATA_KEY];
5849
+ const raw = args.meeting?.metadata?.[NOTE_TAKER_METADATA_KEY];
5632
5850
  const row = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
5633
5851
  const recorded = readState(row["state"]);
5634
5852
  const detail = typeof row["reason"] === "string" && row["reason"].length > 0 ? row["reason"] : null;
@@ -5836,7 +6054,8 @@ function createRoomEngine(options) {
5836
6054
  message: normalized.message,
5837
6055
  remedy: normalized.remedy,
5838
6056
  reason: normalized.reason,
5839
- retryAt: normalized.retryAfterSeconds === null ? null : now() + normalized.retryAfterSeconds * 1e3
6057
+ retryAt: normalized.retryAfterSeconds === null ? null : now() + normalized.retryAfterSeconds * 1e3,
6058
+ stage: "join"
5840
6059
  }
5841
6060
  });
5842
6061
  store.dispatch({ type: "join_failed", reason: normalized.reason ?? null });
@@ -6105,6 +6324,7 @@ function createRoomEngine(options) {
6105
6324
  lastServerUrl = token.serverUrl;
6106
6325
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
6107
6326
  if (token.leavePolicy !== void 0) store.patch({ leavePolicy: token.leavePolicy });
6327
+ store.patch({ canReclaim: token.canReclaim === true });
6108
6328
  store.patch({
6109
6329
  localIdentity: token.identity,
6110
6330
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -6226,6 +6446,10 @@ function createRoomEngine(options) {
6226
6446
  }
6227
6447
  departures.clear();
6228
6448
  liveRoster = list;
6449
+ const localNow = list.find((p) => p.isLocal);
6450
+ if (localNow !== void 0 && !localNow.screenSharing && store.snapshot().share.state === "presenting") {
6451
+ store.patch({ share: NO_SHARE });
6452
+ }
6229
6453
  const merged = mergeGhosts(list, ghosts, at);
6230
6454
  ghosts = merged.kept;
6231
6455
  const people = /* @__PURE__ */ new Map();
@@ -6445,7 +6669,10 @@ function createRoomEngine(options) {
6445
6669
  try {
6446
6670
  await driver.setScreenShareEnabled(enabled);
6447
6671
  ambiguousShareDenials = 0;
6448
- store.patch({ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now() } });
6672
+ const audio = enabled ? driver?.screenShareHasAudio?.() : void 0;
6673
+ store.patch({
6674
+ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now(), ...audio !== void 0 ? { audio } : {} }
6675
+ });
6449
6676
  } catch (error) {
6450
6677
  const kind = classifyScreenShareError(error);
6451
6678
  if (kind === "ambiguous") {
@@ -6512,13 +6739,14 @@ function createRoomEngine(options) {
6512
6739
  return driver?.supportsVideoEffects?.() ?? false;
6513
6740
  },
6514
6741
  supportsScreenShare() {
6515
- return driver?.supportsScreenShare?.() ?? true;
6742
+ return driver?.supportsScreenShare?.() ?? detectCapabilities().canShareScreen;
6516
6743
  },
6517
6744
  async switchDevice(kind, deviceId2) {
6518
6745
  if (driver === null) return;
6519
6746
  await driver.switchDevice(kind, deviceId2);
6520
6747
  },
6521
6748
  enableAudio: () => media.enableAudio(),
6749
+ previewDevice: (kind, outcome) => media.preview(kind, outcome),
6522
6750
  acceptDeviceOffer: () => media.acceptOffer(),
6523
6751
  dismissDeviceOffer: () => media.dismissOffer(),
6524
6752
  dismissDeviceNotice: () => media.dismissNotice(),
@@ -6604,8 +6832,12 @@ var MODERATION_ACTIONS = [
6604
6832
  "lower_hand",
6605
6833
  "lower_all_hands",
6606
6834
  "make_cohost",
6835
+ "make_presenter",
6836
+ "revoke_presenter",
6837
+ "set_av_lock",
6607
6838
  "revoke_cohost",
6608
6839
  "transfer_host",
6840
+ "reclaim_host",
6609
6841
  "rename",
6610
6842
  "remove",
6611
6843
  "report"
@@ -6986,7 +7218,15 @@ function createLiveKitDriver(handlers, options = {}) {
6986
7218
  created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, (participant, reason) => {
6987
7219
  handlers.participantLeft?.(participant.identity, reasonName(reason));
6988
7220
  roster();
6989
- }).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, roster).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ParticipantNameChanged, roster).on(RoomEvent.ParticipantAttributesChanged, roster).on(RoomEvent.ParticipantMetadataChanged, roster).on(RoomEvent.RoomMetadataChanged, (metadata) => {
7221
+ }).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, (publication) => {
7222
+ if (publication.source === Track.Source.ScreenShare) {
7223
+ const ended = () => {
7224
+ void created.localParticipant.setScreenShareEnabled(false).catch(() => void 0);
7225
+ };
7226
+ publication.track?.mediaStreamTrack.addEventListener("ended", ended, { once: true });
7227
+ }
7228
+ roster();
7229
+ }).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ParticipantNameChanged, roster).on(RoomEvent.ParticipantAttributesChanged, roster).on(RoomEvent.ParticipantMetadataChanged, roster).on(RoomEvent.RoomMetadataChanged, (metadata) => {
6990
7230
  handlers.roomMetadata?.(metadata ?? "");
6991
7231
  }).on(RoomEvent.AudioPlaybackStatusChanged, (canPlay) => {
6992
7232
  handlers.audioPlayback?.(canPlay);
@@ -7127,7 +7367,10 @@ function createLiveKitDriver(handlers, options = {}) {
7127
7367
  },
7128
7368
  async setScreenShareEnabled(enabled) {
7129
7369
  try {
7130
- await requireRoom("setScreenShareEnabled").localParticipant.setScreenShareEnabled(enabled);
7370
+ await requireRoom("setScreenShareEnabled").localParticipant.setScreenShareEnabled(
7371
+ enabled,
7372
+ enabled ? { audio: true, systemAudio: "include" } : void 0
7373
+ );
7131
7374
  } catch (error) {
7132
7375
  throw normalizeScreenShareError(error, "meet.setScreenShareEnabled");
7133
7376
  }
@@ -7166,8 +7409,13 @@ function createLiveKitDriver(handlers, options = {}) {
7166
7409
  supportsVideoEffects() {
7167
7410
  return videoEffectsSupported();
7168
7411
  },
7412
+ screenShareHasAudio() {
7413
+ const target = room;
7414
+ if (target === null) return false;
7415
+ return target.localParticipant.getTrackPublications().some((p) => String(p.source) === "screen_share_audio" && p.track !== void 0);
7416
+ },
7169
7417
  supportsScreenShare() {
7170
- return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getDisplayMedia === "function";
7418
+ return detectCapabilities().canShareScreen;
7171
7419
  },
7172
7420
  attach(identity, kind, element) {
7173
7421
  const target = requireRoom("attach");
@@ -7643,14 +7891,31 @@ var import_react3 = require("react");
7643
7891
 
7644
7892
  // src/core/observation.ts
7645
7893
  function observedRole(role, standing) {
7646
- if (role === "host" || role === "cohost") return role;
7894
+ if (role === "host" || role === "cohost" || role === "presenter") return role;
7647
7895
  return isLinkFollower(standing) ? "guest" : "participant";
7648
7896
  }
7649
- function observedDevice(track, permission = "unknown") {
7897
+ function observedDevice(track, permission = "unknown", held = null) {
7898
+ if (held === "locked") return "locked";
7650
7899
  if (track.enabled) return "on";
7900
+ if (held === "muted_by_host") return "muted_by_host";
7651
7901
  const reason = deviceUnavailableReason(track, permission);
7652
7902
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
7653
7903
  }
7904
+ function observedPreview(preview, permission = "unknown") {
7905
+ if (permission === "denied") return "unavailable:denied";
7906
+ switch (preview.state) {
7907
+ case "on":
7908
+ return "on";
7909
+ case "off":
7910
+ return "muted_by_me";
7911
+ case "prompting":
7912
+ return "prompting";
7913
+ case "unavailable":
7914
+ return `unavailable:${preview.unavailable?.reason ?? "failed"}`;
7915
+ default:
7916
+ return null;
7917
+ }
7918
+ }
7654
7919
  function endedRecord(snapshot, given) {
7655
7920
  const record = snapshot?.meeting ?? given;
7656
7921
  return record !== null && record.endedAt !== null;
@@ -7674,20 +7939,34 @@ function observe(input) {
7674
7939
  const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
7675
7940
  const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
7676
7941
  const deviceInPlay = phase === "joining" || inCall;
7942
+ const region = snapshot?.mediaRegion;
7943
+ const atPrejoin = phase === "prejoin" && region !== void 0;
7677
7944
  return {
7678
7945
  phase,
7679
7946
  reason: phase === "ended" ? snapshot?.reason ?? null : snapshot?.reason ?? reasonOf(phase),
7680
7947
  role,
7681
- connection: snapshot?.connection ?? "stable",
7948
+ // `net-blip`: in the call, a drop shorter than `reconnect_banner_delay_ms` is not something the
7949
+ // person sees — the contract says what is on screen, so it reads `stable` until the banner shows.
7950
+ connection: snapshot === null ? "stable" : inCall && snapshot.connection !== "stable" && !snapshot.connectionBanner ? "stable" : snapshot.connection,
7682
7951
  hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
7683
7952
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
7684
7953
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
7685
7954
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
7686
7955
  audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
7687
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
7688
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone) : null
7956
+ 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,
7957
+ 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
7689
7958
  };
7690
7959
  }
7960
+ function lockedFor(snapshot, kind, inCall) {
7961
+ if (!inCall) return null;
7962
+ const role = effectiveRole(snapshot);
7963
+ if (isLockedFor(snapshot.livePolicy, role, kind)) return "locked";
7964
+ if (snapshot.livePolicy.recordingConsent === "view_only_until_agree" && consentPending(snapshot.livePolicy, role, snapshot.recording === "recording", snapshot.recordingConsentGiven)) {
7965
+ return "locked";
7966
+ }
7967
+ if (kind === "microphone" && snapshot.mutedByHost) return "muted_by_host";
7968
+ return null;
7969
+ }
7691
7970
  function staticObservation(phase, reason = null) {
7692
7971
  return {
7693
7972
  phase,
@@ -7775,7 +8054,13 @@ function MeetRoot({
7775
8054
  ...options
7776
8055
  }) {
7777
8056
  const snapshot = useMeetSnapshot();
7778
- const observation = useCoreObservation(options);
8057
+ const probed = useCapabilities();
8058
+ const base = useCoreObservation(options);
8059
+ const unsupported = probed !== null && !probed.canJoin;
8060
+ const observation = (0, import_react3.useMemo)(
8061
+ () => unsupported ? { ...staticObservation("unsupported", probed?.reason ?? null) } : base,
8062
+ [unsupported, probed, base]
8063
+ );
7779
8064
  const attrs = (0, import_react3.useMemo)(() => rootAttributes(observation), [observation]);
7780
8065
  const latest = (0, import_react3.useRef)({ observation, storePhase: snapshot?.phase ?? null });
7781
8066
  latest.current = { observation, storePhase: snapshot?.phase ?? null };
@@ -7793,7 +8078,34 @@ function MeetRoot({
7793
8078
  if (target.__matrxMeet === diagnostics) delete target.__matrxMeet;
7794
8079
  };
7795
8080
  }, []);
7796
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ...attrs, style: { display: "contents" }, children });
8081
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ...attrs, style: { display: "contents" }, children: unsupported && probed !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(UnsupportedBrowser, { capabilities: probed }) : children });
8082
+ }
8083
+ function useCapabilities() {
8084
+ const [found, setFound] = (0, import_react3.useState)(null);
8085
+ (0, import_react3.useEffect)(() => {
8086
+ setFound(detectCapabilities());
8087
+ }, []);
8088
+ return found;
8089
+ }
8090
+ function UnsupportedBrowser({ capabilities }) {
8091
+ const words = unsupportedWords(capabilities);
8092
+ const [copied, setCopied] = (0, import_react3.useState)(false);
8093
+ const href = typeof window === "undefined" ? "" : window.location.href;
8094
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mx-meet", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mx-meet__waiting", "data-meet-exit": "unsupported", children: [
8095
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { className: "mx-meet__prejoin-title", children: words.title }),
8096
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "mx-meet__hint", role: "status", "data-meet-notice": "browser-unsupported", children: words.body }),
8097
+ href.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
8098
+ "button",
8099
+ {
8100
+ type: "button",
8101
+ className: "mx-meet__primary",
8102
+ onClick: () => {
8103
+ void navigator.clipboard?.writeText(href).then(() => setCopied(true), () => void 0);
8104
+ },
8105
+ children: copied ? "Link copied" : "Copy link"
8106
+ }
8107
+ ) : null
8108
+ ] }) });
7797
8109
  }
7798
8110
 
7799
8111
  // src/react/hooks.ts
@@ -8493,7 +8805,8 @@ function useRoom() {
8493
8805
  failure: snapshot?.failure ?? null,
8494
8806
  secondDevice: snapshot?.secondDevice ?? null,
8495
8807
  local,
8496
- canShareScreen: engine?.supportsScreenShare() ?? true,
8808
+ // An observer (role `viewer`) publishes nothing: no Share screen to press and be refused (MD-16).
8809
+ canShareScreen: (engine?.supportsScreenShare() ?? true) && !isObserverRole(snapshot?.grants.role ?? null),
8497
8810
  join: (0, import_react4.useCallback)(
8498
8811
  async (request) => requireEngine(engine, "join").join(request),
8499
8812
  [engine]
@@ -8877,6 +9190,45 @@ function useMeetAi() {
8877
9190
  }, [ai, meeting, dispatchNoteTaker])
8878
9191
  };
8879
9192
  }
9193
+ function useCaptions() {
9194
+ const host = useMeetHost();
9195
+ const snapshot = useMeetSnapshot();
9196
+ const ai = useMeetAi();
9197
+ const on = snapshot?.captionsOn === true;
9198
+ const refusal2 = snapshot?.captionsRefusal ?? null;
9199
+ const taker = ai.noteTaker;
9200
+ const meeting = snapshot?.meeting ?? null;
9201
+ const request = (0, import_react4.useCallback)(() => {
9202
+ if (host === null || meeting === null) return;
9203
+ void host.ai.requestCaptions({ meetingId: meeting.id });
9204
+ }, [host, meeting]);
9205
+ const toggle = (0, import_react4.useCallback)(() => {
9206
+ if (host === null) return;
9207
+ const next = !on;
9208
+ host.store.patch({ captionsOn: next, captionsRefusal: null });
9209
+ if (next && taker.state !== "joining" && taker.state !== "transcribing") request();
9210
+ }, [host, on, taker.state, request]);
9211
+ let state = "off";
9212
+ let reason = null;
9213
+ let remedy = null;
9214
+ if (on) {
9215
+ if (taker.state === "transcribing" && taker.present) {
9216
+ state = "live";
9217
+ } else if (taker.state === "joining") {
9218
+ state = "starting";
9219
+ } else if (refusal2 !== null && refusal2.reason.length > 0) {
9220
+ state = "unavailable";
9221
+ reason = refusal2.reason;
9222
+ remedy = refusal2.remedy.length > 0 ? refusal2.remedy : null;
9223
+ } else if (taker.state === "ended" || taker.state === "handoff_pending") {
9224
+ state = "unavailable";
9225
+ reason = noteTakerLabel(taker);
9226
+ } else {
9227
+ state = "starting";
9228
+ }
9229
+ }
9230
+ return { on, state, reason, remedy, toggle };
9231
+ }
8880
9232
  function useEndMeeting() {
8881
9233
  const host = useMeetHost();
8882
9234
  const snapshot = useMeetSnapshot();
@@ -8914,7 +9266,7 @@ function useConsentNotice() {
8914
9266
  const snapshot = useMeetSnapshot();
8915
9267
  const agent = noteTakerParticipant(snapshot?.participants ?? []);
8916
9268
  if (agent === null) return null;
8917
- return `${agent.displayName} is in this meeting: it is transcribing what is said and writing notes everyone here can read.`;
9269
+ return `AI notes are on. ${agent.displayName} is in this meeting, transcribing what is said and writing notes everyone here can read.`;
8918
9270
  }
8919
9271
  function useMediaSupervisor() {
8920
9272
  const host = useMeetHost();
@@ -8933,6 +9285,12 @@ function useMediaSupervisor() {
8933
9285
  retry: (0, import_react4.useCallback)(
8934
9286
  async (kind) => kind === "microphone" ? engine?.setMicrophoneEnabled(true) : engine?.setCameraEnabled(true),
8935
9287
  [engine]
9288
+ ),
9289
+ reportPreview: (0, import_react4.useCallback)(
9290
+ (kind, outcome) => {
9291
+ void engine?.previewDevice(kind, outcome);
9292
+ },
9293
+ [engine]
8936
9294
  )
8937
9295
  };
8938
9296
  }
@@ -9378,6 +9736,38 @@ function useGuestRecordAccess(meeting) {
9378
9736
  )
9379
9737
  };
9380
9738
  }
9739
+ function useLivePolicy() {
9740
+ const snapshot = useMeetSnapshot();
9741
+ const role = useRole();
9742
+ const [now, setNow] = (0, import_react4.useState)(() => Date.now());
9743
+ const limited = (snapshot?.livePolicy.timeLimitMinutes ?? 0) > 0;
9744
+ (0, import_react4.useEffect)(() => {
9745
+ if (!limited) return void 0;
9746
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
9747
+ return () => clearInterval(timer);
9748
+ }, [limited]);
9749
+ const policy = snapshot?.livePolicy ?? DEFAULT_LIVE_POLICY;
9750
+ const left = minutesLeft(policy, snapshot?.sessionStartedAt ?? null, now);
9751
+ const host = useMeetHost();
9752
+ const pending = consentPending(policy, role, snapshot?.recording === "recording", snapshot?.recordingConsentGiven === true);
9753
+ const viewOnly = pending && policy.recordingConsent === "view_only_until_agree";
9754
+ const idle = snapshot !== null && snapshot.recording !== "recording" && snapshot.recording !== "starting" && snapshot.recording !== "stopping";
9755
+ const given = snapshot?.recordingConsentGiven === true;
9756
+ const store = host?.store ?? null;
9757
+ (0, import_react4.useEffect)(() => {
9758
+ if (idle && given) store?.patch({ recordingConsentGiven: false });
9759
+ }, [idle, given, store]);
9760
+ return {
9761
+ canChat: canChat(policy, role),
9762
+ chatMode: policy.chatMode,
9763
+ micLocked: isLockedFor(policy, role, "microphone") || viewOnly,
9764
+ cameraLocked: isLockedFor(policy, role, "camera") || viewOnly,
9765
+ consent: pending ? policy.recordingConsent === "ok_or_leave" ? "ok_or_leave" : "view_only_until_agree" : null,
9766
+ agree: () => store?.patch({ recordingConsentGiven: true }),
9767
+ minutesLeft: left,
9768
+ timeWarning: left !== null && left <= policy.timeLimitWarningMinutes
9769
+ };
9770
+ }
9381
9771
 
9382
9772
  // src/react/skin-root.tsx
9383
9773
  var import_react12 = require("react");
@@ -9996,6 +10386,39 @@ function useCollabPanelRequest(onRequest) {
9996
10386
  // src/core/reactions.ts
9997
10387
  var MEET_REACTIONS = ["\u{1F496}", "\u{1F44D}", "\u{1F389}", "\u{1F44F}", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F914}", "\u{1F44E}"];
9998
10388
 
10389
+ // src/core/speaking-detector.ts
10390
+ function createSpeakingWhileMutedDetector(options = {}) {
10391
+ const threshold = options.threshold ?? 0.06;
10392
+ const holdMs = options.holdMs ?? 600;
10393
+ const gapMs = options.gapMs ?? 400;
10394
+ const cooldownMs = options.cooldownMs ?? 2e4;
10395
+ let speechStart = null;
10396
+ let lastLoud = null;
10397
+ let firedAt = null;
10398
+ return {
10399
+ feed(level, at) {
10400
+ if (level >= threshold) {
10401
+ if (speechStart === null || lastLoud === null || at - lastLoud > gapMs) speechStart = at;
10402
+ lastLoud = at;
10403
+ } else if (lastLoud !== null && at - lastLoud > gapMs) {
10404
+ speechStart = null;
10405
+ lastLoud = null;
10406
+ }
10407
+ if (speechStart === null || at - speechStart < holdMs) return false;
10408
+ if (firedAt !== null && at - firedAt < cooldownMs) return false;
10409
+ firedAt = at;
10410
+ speechStart = null;
10411
+ lastLoud = null;
10412
+ return true;
10413
+ },
10414
+ reset() {
10415
+ speechStart = null;
10416
+ lastLoud = null;
10417
+ firedAt = null;
10418
+ }
10419
+ };
10420
+ }
10421
+
9999
10422
  // src/react/facade.ts
10000
10423
  var NO_ACTIONS = {
10001
10424
  join: async () => void 0,
@@ -10152,6 +10575,18 @@ function SparklesIcon(props) {
10152
10575
  /* @__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" })
10153
10576
  ] });
10154
10577
  }
10578
+ function CaptionsIcon(props) {
10579
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Icon, { ...props, children: [
10580
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
10581
+ /* @__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" })
10582
+ ] });
10583
+ }
10584
+ function NotesIcon(props) {
10585
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Icon, { ...props, children: [
10586
+ /* @__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" }),
10587
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M14 7l3 3" })
10588
+ ] });
10589
+ }
10155
10590
  function LockIcon(props) {
10156
10591
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Icon, { ...props, children: [
10157
10592
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "4", y: "10", width: "16", height: "11", rx: "2" }),
@@ -10287,6 +10722,9 @@ function UndoIcon(props) {
10287
10722
  /* @__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" })
10288
10723
  ] });
10289
10724
  }
10725
+ function ChevronUpIcon(props) {
10726
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "m6 15 6-6 6 6" }) });
10727
+ }
10290
10728
 
10291
10729
  // src/skins/meet/collab.tsx
10292
10730
  var import_jsx_runtime5 = require("react/jsx-runtime");
@@ -11058,7 +11496,7 @@ function BreakoutRun({ plan }) {
11058
11496
  type: "button",
11059
11497
  className: "mx-meet__primary mx-meet__primary--small",
11060
11498
  onClick: () => void act({ action: "breakout_close" }).then(show),
11061
- children: "Bring everyone back"
11499
+ children: "Close rooms"
11062
11500
  }
11063
11501
  ) : null
11064
11502
  ] });
@@ -11076,9 +11514,14 @@ function BreakoutBanner() {
11076
11514
  const { collab, act, moveTo, localIdentity, breakoutRoomId } = useCollab();
11077
11515
  const plan = collab?.breakout ?? null;
11078
11516
  const open = plan?.state === "open" ? plan : null;
11079
- const now = useNow(open?.endsAt != null);
11517
+ const now = useNow(open?.endsAt != null || open?.closingAt != null);
11080
11518
  const remaining = breakoutRemainingMs(open, now);
11519
+ const closing = breakoutClosingMs(open, now);
11081
11520
  const assigned = assignedRoom(open, localIdentity);
11521
+ const [later, setLater] = (0, import_react9.useState)(null);
11522
+ const [autoSince, setAutoSince] = (0, import_react9.useState)(null);
11523
+ const AUTO_MOVE_MS = 1e4;
11524
+ const autoLeft = open?.moveMode === "auto" && assigned !== null && autoSince?.id === open.id && later !== open.id ? Math.max(0, AUTO_MOVE_MS - (now - autoSince.at)) : null;
11082
11525
  const [dismissedBroadcast, setDismissedBroadcast] = (0, import_react9.useState)(null);
11083
11526
  const [failure, setFailure] = (0, import_react9.useState)(null);
11084
11527
  const expireAsked = (0, import_react9.useRef)(null);
@@ -11091,6 +11534,19 @@ function BreakoutBanner() {
11091
11534
  if ((here ?? null) === request.roomId) return;
11092
11535
  void moveTo(request.roomId).then((result) => setFailure(result.ok ? null : result.message));
11093
11536
  }, [moveAt, host, moveTo]);
11537
+ const joinAssigned = async () => {
11538
+ if (assigned === null) return;
11539
+ const said = await act({ action: "breakout_join", roomId: assigned.id });
11540
+ const result = said.ok ? await moveTo(assigned.id) : said;
11541
+ setFailure(result.ok ? null : result.message);
11542
+ };
11543
+ (0, import_react9.useEffect)(() => {
11544
+ if (open === null || open.moveMode !== "auto" || assigned === null || breakoutRoomId !== null) return;
11545
+ if (autoSince?.id !== open.id) setAutoSince({ id: open.id, at: Date.now() });
11546
+ }, [open, assigned, breakoutRoomId, autoSince]);
11547
+ (0, import_react9.useEffect)(() => {
11548
+ if (autoLeft === 0) void joinAssigned();
11549
+ }, [autoLeft]);
11094
11550
  (0, import_react9.useEffect)(() => {
11095
11551
  if (open === null || remaining === null || remaining > 0) return;
11096
11552
  const asked = expireAsked.current;
@@ -11104,27 +11560,25 @@ function BreakoutBanner() {
11104
11560
  if (open === null && failure === null) return null;
11105
11561
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "mx-meet__breakout-banner", role: "status", children: [
11106
11562
  failure !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "mx-meet__collab-error", children: failure }) : null,
11563
+ closing !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { role: "status", "data-meet-notice": "breakout-closing", children: [
11564
+ "Rooms closing in ",
11565
+ Math.ceil(closing / 1e3),
11566
+ room !== null ? " \u2014 you return to the main meeting" : ""
11567
+ ] }) : null,
11107
11568
  room !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
11108
11569
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: room.name }),
11109
- remaining !== null ? ` \xB7 ${clock(remaining)} left` : "",
11570
+ remaining !== null && closing === null ? ` \xB7 ${clock(remaining)} left` : "",
11110
11571
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mx-meet__chip", onClick: () => void moveTo(null), children: "Return to main room" })
11572
+ ] }) : assigned !== null && later !== open?.id ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
11573
+ autoLeft !== null ? `You're being moved to ${assigned.name} in ${Math.ceil(autoLeft / 1e3)} s` : `You're invited to ${assigned.name}`,
11574
+ remaining !== null && closing === null ? ` \xB7 ${clock(remaining)} left` : "",
11575
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void joinAssigned(), children: "Join room" }),
11576
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mx-meet__chip", onClick: () => setLater(open?.id ?? null), children: "Later" })
11111
11577
  ] }) : assigned !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
11112
- "You are assigned to ",
11113
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: assigned.name }),
11114
- remaining !== null ? ` \xB7 ${clock(remaining)} left` : "",
11115
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
11116
- "button",
11117
- {
11118
- type: "button",
11119
- className: "mx-meet__primary mx-meet__primary--small",
11120
- onClick: () => void act({ action: "breakout_join", roomId: assigned.id }).then(async (said) => {
11121
- const result = said.ok ? await moveTo(assigned.id) : said;
11122
- setFailure(result.ok ? null : result.message);
11123
- }),
11124
- children: "Join"
11125
- }
11126
- )
11127
- ] }) : open?.allowChoose ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: "Breakout rooms are open \u2014 pick one under Activities." }) : null,
11578
+ assigned.name,
11579
+ " is open",
11580
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "mx-meet__chip", onClick: () => void joinAssigned(), children: "Join room" })
11581
+ ] }) : open?.allowChoose ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: "Rooms are open \u2014 pick one under Activities." }) : null,
11128
11582
  showBroadcast ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "mx-meet__breakout-broadcast", children: [
11129
11583
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("strong", { children: [
11130
11584
  broadcast.by,
@@ -11172,6 +11626,33 @@ function CaptionLanguageSettings() {
11172
11626
  collab?.captionTranslationError != null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "mx-meet__panel-note mx-meet__collab-error", children: collab.captionTranslationError }) : null
11173
11627
  ] });
11174
11628
  }
11629
+ function CaptionsControl() {
11630
+ const captions = useCaptions();
11631
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
11632
+ ControlButton,
11633
+ {
11634
+ label: captions.on ? "Turn off captions" : "Turn on captions",
11635
+ active: captions.on,
11636
+ pressed: captions.on,
11637
+ onClick: captions.toggle,
11638
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CaptionsIcon, {})
11639
+ }
11640
+ );
11641
+ }
11642
+ function TakeNotesControl() {
11643
+ const ai = useMeetAi();
11644
+ if (!ai.canControlNoteTaker) return null;
11645
+ const running = ai.noteTaker.state === "joining" || ai.noteTaker.state === "transcribing";
11646
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
11647
+ ControlButton,
11648
+ {
11649
+ label: running ? "Stop taking notes" : "Take notes for me",
11650
+ active: running,
11651
+ onClick: () => void (running ? ai.stopNoteTaker() : ai.startNoteTaker()),
11652
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(NotesIcon, {})
11653
+ }
11654
+ );
11655
+ }
11175
11656
 
11176
11657
  // src/skins/meet/components.tsx
11177
11658
  var import_jsx_runtime6 = require("react/jsx-runtime");
@@ -11222,6 +11703,7 @@ function ControlButton({
11222
11703
  label,
11223
11704
  active = false,
11224
11705
  danger = false,
11706
+ pressed,
11225
11707
  onClick,
11226
11708
  children
11227
11709
  }) {
@@ -11236,26 +11718,49 @@ function ControlButton({
11236
11718
  ].filter(Boolean).join(" "),
11237
11719
  "aria-label": label,
11238
11720
  title: label,
11721
+ ...pressed !== void 0 ? { "aria-pressed": pressed } : {},
11239
11722
  onClick,
11240
11723
  children
11241
11724
  }
11242
11725
  );
11243
11726
  }
11244
11727
  function RecordingIndicator() {
11245
- const { state, label } = useRecording();
11246
- if (label === null) return null;
11247
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
11248
- "span",
11249
- {
11250
- className: `mx-meet__recording mx-meet__recording--${state}`,
11251
- role: "status",
11252
- "aria-live": "polite",
11253
- children: [
11254
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RecordIcon, { width: 10, height: 10 }),
11255
- label
11256
- ]
11257
- }
11258
- );
11728
+ const { state, label, canStart } = useRecording();
11729
+ const snapshot = useMeetSnapshot();
11730
+ const previous = (0, import_react10.useRef)(state);
11731
+ const [stoppedAt, setStoppedAt] = (0, import_react10.useState)(null);
11732
+ (0, import_react10.useEffect)(() => {
11733
+ const was = previous.current;
11734
+ previous.current = state;
11735
+ if ((was === "recording" || was === "starting") && state !== "recording" && state !== "starting") {
11736
+ setStoppedAt(Date.now());
11737
+ const timer = setTimeout(() => setStoppedAt(null), 1e4);
11738
+ return () => clearTimeout(timer);
11739
+ }
11740
+ return void 0;
11741
+ }, [state]);
11742
+ const meetingId = snapshot?.meeting?.id ?? null;
11743
+ if (label === null && stoppedAt === null) return null;
11744
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11745
+ label !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
11746
+ "span",
11747
+ {
11748
+ className: `mx-meet__recording mx-meet__recording--${state}`,
11749
+ role: "status",
11750
+ "aria-live": "polite",
11751
+ children: [
11752
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RecordIcon, { width: 10, height: 10 }),
11753
+ label
11754
+ ]
11755
+ }
11756
+ ) : null,
11757
+ stoppedAt !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__recording", role: "status", "data-meet-notice": "recording-stopped", children: "Recording stopped" }) : null,
11758
+ state === "processing" ? (
11759
+ // rec-processing: the host is told it is being made ready (the indicator says Processing…).
11760
+ /* @__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." })
11761
+ ) : null,
11762
+ 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
11763
+ ] });
11259
11764
  }
11260
11765
  function RoleBadge({
11261
11766
  role,
@@ -11407,6 +11912,7 @@ function ControlBar({ onLeave }) {
11407
11912
  const controls = useHostControls();
11408
11913
  const queue = useHandQueue();
11409
11914
  const micUnavailable = useLocalMedia().microphone.unavailable;
11915
+ const live = useLivePolicy();
11410
11916
  const unread = useUnreadChat(panel === "chat");
11411
11917
  const [whiteboard, setWhiteboard] = (0, import_react10.useState)(false);
11412
11918
  const app = useMeetAppPanel();
@@ -11472,9 +11978,10 @@ function ControlBar({ onLeave }) {
11472
11978
  ) }) }) : null,
11473
11979
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BreakoutBanner, {}),
11474
11980
  panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(KnockNotice, { onView: () => setPanel("people") }) : null,
11981
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SpeakingWhileMutedNudge, {}),
11475
11982
  controls.notice !== null && panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
11476
11983
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__controls", children: [
11477
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11984
+ live.micLocked ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11478
11985
  ControlButton,
11479
11986
  {
11480
11987
  label: micUnavailable !== null ? `Unmute \u2014 ${micUnavailable.remedy}` : room.local?.micEnabled === true ? "Mute (\u2318D)" : "Unmute (\u2318D)",
@@ -11483,7 +11990,8 @@ function ControlBar({ onLeave }) {
11483
11990
  children: room.local?.micEnabled === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, {})
11484
11991
  }
11485
11992
  ),
11486
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11993
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AudioSettingsControl, {}),
11994
+ live.cameraLocked ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11487
11995
  ControlButton,
11488
11996
  {
11489
11997
  label: room.local?.cameraEnabled === true ? "Stop video (\u2318E)" : "Start video (\u2318E)",
@@ -11492,6 +12000,7 @@ function ControlBar({ onLeave }) {
11492
12000
  children: room.local?.cameraEnabled === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CameraIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CameraOffIcon, {})
11493
12001
  }
11494
12002
  ),
12003
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CaptionsControl, {}),
11495
12004
  room.canShareScreen || room.local?.screenSharing === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11496
12005
  ControlButton,
11497
12006
  {
@@ -11556,6 +12065,7 @@ function ControlBar({ onLeave }) {
11556
12065
  onOpen: openActivity
11557
12066
  }
11558
12067
  ),
12068
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(TakeNotesControl, {}),
11559
12069
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AiControl, { active: panel === "ai", onToggle: () => toggle("ai") }),
11560
12070
  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,
11561
12071
  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,
@@ -11596,6 +12106,13 @@ function ControlBar({ onLeave }) {
11596
12106
  ]
11597
12107
  }
11598
12108
  ),
12109
+ !room.canShareScreen && room.local?.screenSharing !== true ? (
12110
+ // `share-unsupported-mobile`: the control is absent; the menu says where it lives.
12111
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__menu-note", role: "note", "data-meet-notice": "share-unsupported", children: [
12112
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ScreenShareIcon, { width: 16, height: 16 }),
12113
+ " Present from a computer"
12114
+ ] })
12115
+ ) : null,
11599
12116
  controls.isHost ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11600
12117
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__menu-sep", role: "separator" }),
11601
12118
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
@@ -11611,6 +12128,23 @@ function ControlBar({ onLeave }) {
11611
12128
  ]
11612
12129
  }
11613
12130
  ),
12131
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
12132
+ MenuItem,
12133
+ {
12134
+ onSelect: () => {
12135
+ setPopover("none");
12136
+ void controls.run({
12137
+ action: "set_av_lock",
12138
+ value: snapshot?.livePolicy.avLock === "both" ? "off" : "both"
12139
+ });
12140
+ },
12141
+ children: [
12142
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, { width: 16, height: 16 }),
12143
+ " ",
12144
+ snapshot?.livePolicy.avLock === "both" ? "Turn on attendees' microphone and camera" : "Turn off attendees' microphone and camera"
12145
+ ]
12146
+ }
12147
+ ),
11614
12148
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
11615
12149
  MenuItem,
11616
12150
  {
@@ -11853,6 +12387,94 @@ function Toast({ text: text3, failed, onDone }) {
11853
12387
  /* @__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 }) })
11854
12388
  ] });
11855
12389
  }
12390
+ function AudioSettingsControl() {
12391
+ const host = useMeetHost();
12392
+ const devices = useDeviceSnapshot();
12393
+ const [open, setOpen] = (0, import_react10.useState)(false);
12394
+ const list = (devices?.devices.audioinput ?? []).filter((device) => device.deviceId.length > 0);
12395
+ if (host === null || list.length < 2) return null;
12396
+ const current = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
12397
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "mx-meet__anchor", children: [
12398
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12399
+ ControlButton,
12400
+ {
12401
+ label: "Audio settings",
12402
+ active: open,
12403
+ onClick: () => {
12404
+ if (!open) void host.devices.refresh();
12405
+ setOpen(!open);
12406
+ },
12407
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ChevronUpIcon, {})
12408
+ }
12409
+ ),
12410
+ 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)(
12411
+ "button",
12412
+ {
12413
+ type: "button",
12414
+ role: "menuitemradio",
12415
+ "aria-checked": device.deviceId === current,
12416
+ className: "mx-meet__menu-item",
12417
+ onClick: () => {
12418
+ setOpen(false);
12419
+ host.devices.select("audioinput", device.deviceId);
12420
+ void host.engine.switchDevice("audioinput", device.deviceId).catch(() => void 0);
12421
+ },
12422
+ children: [
12423
+ 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" }),
12424
+ device.label.length > 0 ? device.label : "Microphone"
12425
+ ]
12426
+ },
12427
+ device.deviceId
12428
+ )) }) }) : null
12429
+ ] });
12430
+ }
12431
+ function SpeakingWhileMutedNudge() {
12432
+ const room = useRoom();
12433
+ const host = useMeetHost();
12434
+ const devices = useDeviceSnapshot();
12435
+ const microphone = useLocalMedia().microphone;
12436
+ const mutedByMe = host !== null && room.local != null && room.local.micEnabled !== true && microphone.unavailable === null;
12437
+ const mic = devices?.selected.audioinput ?? host?.devices.resolve("audioinput") ?? null;
12438
+ const stream = useMicrophoneStream(mutedByMe ? mic : "off");
12439
+ const level = useAudioLevel(stream);
12440
+ const latest = (0, import_react10.useRef)(0);
12441
+ latest.current = level;
12442
+ const [shown, setShown] = (0, import_react10.useState)(false);
12443
+ const [detector] = (0, import_react10.useState)(() => createSpeakingWhileMutedDetector());
12444
+ (0, import_react10.useEffect)(() => {
12445
+ if (!mutedByMe) {
12446
+ detector.reset();
12447
+ setShown(false);
12448
+ return void 0;
12449
+ }
12450
+ const timer = setInterval(() => {
12451
+ if (detector.feed(latest.current, Date.now())) setShown(true);
12452
+ }, 150);
12453
+ return () => clearInterval(timer);
12454
+ }, [mutedByMe, detector]);
12455
+ (0, import_react10.useEffect)(() => {
12456
+ if (!shown) return void 0;
12457
+ const timer = setTimeout(() => setShown(false), 8e3);
12458
+ return () => clearTimeout(timer);
12459
+ }, [shown]);
12460
+ if (!shown || !mutedByMe) return null;
12461
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__notice-bar", role: "status", "data-meet-notice": "speaking-while-muted", children: [
12462
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, { width: 16, height: 16 }),
12463
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "You're muted" }),
12464
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12465
+ "button",
12466
+ {
12467
+ type: "button",
12468
+ className: "mx-meet__primary mx-meet__primary--small",
12469
+ onClick: () => {
12470
+ setShown(false);
12471
+ void room.toggleMicrophone();
12472
+ },
12473
+ children: "Unmute"
12474
+ }
12475
+ )
12476
+ ] });
12477
+ }
11856
12478
  function MediaNotices() {
11857
12479
  const media = useLocalMedia();
11858
12480
  const { region, where, enableAudio, acceptOffer, dismissOffer, dismissNotice, retry } = useMediaSupervisor();
@@ -11938,9 +12560,45 @@ function MeetingOverlays() {
11938
12560
  const reactions = useReactions();
11939
12561
  const { notice, dismiss } = useModerationNotice();
11940
12562
  const room = useRoom();
12563
+ const live = useLivePolicy();
12564
+ const snapshot = useMeetSnapshot();
12565
+ const controls = useHostControls();
12566
+ const isHostNow = useRole() === "host";
11941
12567
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11942
12568
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ReactionOverlay, { reactions }),
11943
12569
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MediaNotices, {}),
12570
+ snapshot !== null && snapshot.phase === "in_call" && snapshot.participants.length >= 2 && !snapshot.participants.some((p) => p.role === "host" || p.role === "cohost") ? (
12571
+ // meeting-without-host: the call goes on; host-only controls are simply not on screen.
12572
+ /* @__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." })
12573
+ ) : null,
12574
+ 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: [
12575
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h2", { className: "mx-meet__panel-title", children: "This meeting is being recorded" }),
12576
+ /* @__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." }),
12577
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__menu-actions", children: [
12578
+ /* @__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" }),
12579
+ 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
12580
+ ] })
12581
+ ] }) }) : null,
12582
+ snapshot?.canReclaim === true && !isHostNow ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__notice-bar", role: "status", "data-meet-notice": "host-reclaim", children: [
12583
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "You started this meeting. Take back the host?" }),
12584
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12585
+ "button",
12586
+ {
12587
+ type: "button",
12588
+ className: "mx-meet__primary mx-meet__primary--small",
12589
+ onClick: () => void controls.run({ action: "reclaim_host" }),
12590
+ children: "Reclaim host"
12591
+ }
12592
+ )
12593
+ ] }) : null,
12594
+ 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,
12595
+ 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: [
12596
+ "This meeting will end in ",
12597
+ live.minutesLeft,
12598
+ " ",
12599
+ live.minutesLeft === 1 ? "minute" : "minutes",
12600
+ " (time limit)."
12601
+ ] }) : null,
11944
12602
  notice !== null && notice.kind !== "removed" ? (
11945
12603
  // A host's request ("the host is asking you to unmute"), not an error:
11946
12604
  // an assertive status, so it is announced at once without being filed
@@ -12031,6 +12689,7 @@ function ChatPanel({
12031
12689
  }) {
12032
12690
  const snapshot = useMeetSnapshot();
12033
12691
  const room = useRoom();
12692
+ const live = useLivePolicy();
12034
12693
  const [draft, setDraft] = (0, import_react10.useState)("");
12035
12694
  const [recipient, setRecipient] = (0, import_react10.useState)(to);
12036
12695
  const listRef = (0, import_react10.useRef)(null);
@@ -12055,7 +12714,7 @@ function ChatPanel({
12055
12714
  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,
12056
12715
  messages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ChatLine, { message, local, people }, message.id))
12057
12716
  ] }),
12058
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
12717
+ !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)(
12059
12718
  "form",
12060
12719
  {
12061
12720
  className: "mx-meet__composer mx-meet__composer--chat",
@@ -12427,6 +13086,13 @@ function PersonMenu({
12427
13086
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PeopleIcon, { width: 16, height: 16 }),
12428
13087
  " Make co-host"
12429
13088
  ] }) : null,
13089
+ !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: [
13090
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ScreenShareIcon, { width: 16, height: 16 }),
13091
+ " Remove presenter"
13092
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(MenuItem, { onSelect: () => act({ action: "make_presenter", identity: participant.identity }), children: [
13093
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ScreenShareIcon, { width: 16, height: 16 }),
13094
+ " Make presenter"
13095
+ ] }) : null,
12430
13096
  !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
12431
13097
  MenuItem,
12432
13098
  {
@@ -12658,14 +13324,20 @@ async function playTestSound(sinkId) {
12658
13324
  audio.pause();
12659
13325
  await context.close();
12660
13326
  }
12661
- function useMicrophoneStream(deviceId2) {
13327
+ function useMicrophoneStream(deviceId2, report, attempt = 0) {
12662
13328
  const [stream, setStream] = (0, import_react10.useState)(null);
13329
+ const reportRef = (0, import_react10.useRef)(report);
13330
+ reportRef.current = report;
12663
13331
  (0, import_react10.useEffect)(() => {
12664
- if (deviceId2 === "off") return void 0;
13332
+ if (deviceId2 === "off") {
13333
+ reportRef.current?.("off");
13334
+ return void 0;
13335
+ }
12665
13336
  let live = true;
12666
13337
  let opened = null;
12667
13338
  const media = globalThis.navigator?.mediaDevices;
12668
13339
  if (media === void 0) return void 0;
13340
+ reportRef.current?.("prompting");
12669
13341
  void media.getUserMedia({ audio: deviceId2 === null ? true : { deviceId: { exact: deviceId2 } } }).then((next) => {
12670
13342
  if (!live) {
12671
13343
  next.getTracks().forEach((track) => track.stop());
@@ -12673,15 +13345,49 @@ function useMicrophoneStream(deviceId2) {
12673
13345
  }
12674
13346
  opened = next;
12675
13347
  setStream(next);
12676
- }).catch(() => setStream(null));
13348
+ reportRef.current?.("on");
13349
+ }).catch((thrown) => {
13350
+ setStream(null);
13351
+ if (live) reportRef.current?.({ error: thrown });
13352
+ });
12677
13353
  return () => {
12678
13354
  live = false;
12679
13355
  opened?.getTracks().forEach((track) => track.stop());
12680
13356
  setStream(null);
12681
13357
  };
12682
- }, [deviceId2]);
13358
+ }, [deviceId2, attempt]);
12683
13359
  return stream;
12684
13360
  }
13361
+ function PreviewStatus({ onRetry }) {
13362
+ const { region, where } = useMediaSupervisor();
13363
+ const [waited, setWaited] = (0, import_react10.useState)(false);
13364
+ const prompting = region.preview.microphone.state === "prompting" || region.preview.camera.state === "prompting";
13365
+ (0, import_react10.useEffect)(() => {
13366
+ if (!prompting) {
13367
+ setWaited(false);
13368
+ return void 0;
13369
+ }
13370
+ const timer = setTimeout(() => setWaited(true), 800);
13371
+ return () => clearTimeout(timer);
13372
+ }, [prompting]);
13373
+ const refused = ["microphone", "camera"].flatMap((kind) => {
13374
+ const preview = region.preview[kind];
13375
+ const reason = region.permission[kind] === "denied" ? "denied" : preview.state === "unavailable" ? preview.unavailable?.reason ?? "failed" : null;
13376
+ return reason === null ? [] : [{ kind, reason }];
13377
+ });
13378
+ if (refused.length === 0) {
13379
+ if (!prompting || !waited) return null;
13380
+ 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" });
13381
+ }
13382
+ const first = refused[0];
13383
+ const both = refused.length === 2 && refused[0].reason === refused[1].reason;
13384
+ const label = both ? deviceChipLabel("camera", first.reason).replace(/^Camera/, "Camera and mic") : refused.map((r) => deviceChipLabel(r.kind, r.reason)).join(" \xB7 ");
13385
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__warning", role: "status", "data-meet-notice": deviceNoticeCode(first.kind, first.reason), children: [
13386
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("strong", { children: label }),
13387
+ /* @__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)) }),
13388
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Button, { type: "button", variant: "outline", size: "sm", onClick: onRetry, children: "Try again" })
13389
+ ] });
13390
+ }
12685
13391
  function KnockNotice({ onView }) {
12686
13392
  const { knocker, others, admit, dismiss } = useKnockNotice();
12687
13393
  if (knocker === null) return null;
@@ -12932,8 +13638,19 @@ function Captions({ lines = 3 }) {
12932
13638
  const ai = useMeetAi();
12933
13639
  const visible = ai.transcript.slice(-lines);
12934
13640
  const translation = useCaptionTranslation(visible);
12935
- if (visible.length === 0) return null;
12936
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", children: [
13641
+ const captions = useCaptions();
13642
+ if (!captions.on) return null;
13643
+ if (captions.state === "unavailable") {
13644
+ 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: [
13645
+ "Captions are unavailable. ",
13646
+ captions.reason,
13647
+ captions.remedy !== null ? ` ${captions.remedy}` : ""
13648
+ ] }) });
13649
+ }
13650
+ if (visible.length === 0) {
13651
+ 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." }) });
13652
+ }
13653
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", "data-meet-caption": "live", children: [
12937
13654
  visible.map((line) => {
12938
13655
  const translated = translation.language === null ? void 0 : translation.translations[line.id];
12939
13656
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
@@ -12956,7 +13673,7 @@ function Captions({ lines = 3 }) {
12956
13673
  function ConsentNotice() {
12957
13674
  const notice = useConsentNotice();
12958
13675
  if (notice === null) return null;
12959
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__consent", role: "status", children: notice });
13676
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__consent", role: "status", "data-meet-notice": "notes-started", children: notice });
12960
13677
  }
12961
13678
  function AttendanceNotice() {
12962
13679
  const media = useLocalMedia();
@@ -13000,8 +13717,15 @@ function PreJoinBody({
13000
13717
  const [joining, setJoining] = (0, import_react10.useState)(false);
13001
13718
  const camera = devices?.selected.videoinput ?? host.devices.resolve("videoinput");
13002
13719
  const mic = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
13003
- const preview = useCameraPreview(mode === "video" ? camera : void 0, effects.effects);
13004
- const micStream = useMicrophoneStream(microphone ? mic : "off");
13720
+ const { reportPreview } = useMediaSupervisor();
13721
+ const [attempt, setAttempt] = (0, import_react10.useState)(0);
13722
+ const preview = useCameraPreview(
13723
+ mode === "video" ? camera : void 0,
13724
+ effects.effects,
13725
+ (outcome) => reportPreview("camera", outcome),
13726
+ attempt
13727
+ );
13728
+ const micStream = useMicrophoneStream(microphone ? mic : "off", (outcome) => reportPreview("microphone", outcome), attempt);
13005
13729
  const level = useAudioLevel(micStream);
13006
13730
  const [speakerTesting, setSpeakerTesting] = (0, import_react10.useState)(false);
13007
13731
  (0, import_react10.useEffect)(() => {
@@ -13045,6 +13769,7 @@ function PreJoinBody({
13045
13769
  ] }),
13046
13770
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__prejoin-panel", children: [
13047
13771
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h1", { className: "mx-meet__prejoin-title", children: "Ready to join?" }),
13772
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PreviewStatus, { onRetry: () => setAttempt((n) => n + 1) }),
13048
13773
  notice?.kind === "removed" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__warning", children: notice.text }) : null,
13049
13774
  devices?.labelsHidden === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__panel-note", children: [
13050
13775
  "Your browser is hiding device names until you allow camera and microphone access.",
@@ -13147,16 +13872,22 @@ function PreJoinBody({
13147
13872
  ] })
13148
13873
  ] });
13149
13874
  }
13150
- function useCameraPreview(deviceId2, effects) {
13875
+ function useCameraPreview(deviceId2, effects, report, attempt = 0) {
13876
+ const reportRef = (0, import_react10.useRef)(report);
13877
+ reportRef.current = report;
13151
13878
  const [preview, setPreview] = (0, import_react10.useState)(null);
13152
13879
  const [error, setError] = (0, import_react10.useState)(null);
13153
13880
  const current = (0, import_react10.useRef)(null);
13154
13881
  const initialEffect = (0, import_react10.useRef)(toVideoEffect(effects));
13155
13882
  initialEffect.current = toVideoEffect(effects);
13156
13883
  (0, import_react10.useEffect)(() => {
13157
- if (deviceId2 === void 0) return void 0;
13884
+ if (deviceId2 === void 0) {
13885
+ reportRef.current?.("off");
13886
+ return void 0;
13887
+ }
13158
13888
  let live = true;
13159
13889
  setError(null);
13890
+ reportRef.current?.("prompting");
13160
13891
  void createCameraPreview(deviceId2, initialEffect.current).then((created) => {
13161
13892
  if (!live) {
13162
13893
  created.stop();
@@ -13164,8 +13895,10 @@ function useCameraPreview(deviceId2, effects) {
13164
13895
  }
13165
13896
  current.current = created;
13166
13897
  setPreview(created);
13898
+ reportRef.current?.("on");
13167
13899
  }).catch((thrown) => {
13168
13900
  if (!live) return;
13901
+ reportRef.current?.({ error: thrown });
13169
13902
  const failure = thrown;
13170
13903
  const message = (failure.message ?? "").replace(/^meet\.[A-Za-z.()]+:\s*/, "");
13171
13904
  const sentence = message.length > 0 ? message.charAt(0).toUpperCase() + message.slice(1) : "";
@@ -13177,7 +13910,7 @@ function useCameraPreview(deviceId2, effects) {
13177
13910
  current.current = null;
13178
13911
  setPreview(null);
13179
13912
  };
13180
- }, [deviceId2]);
13913
+ }, [deviceId2, attempt]);
13181
13914
  return {
13182
13915
  preview,
13183
13916
  error,
@@ -13317,6 +14050,14 @@ function MeetSkinRoom({
13317
14050
  }
13318
14051
  function ShareNotice() {
13319
14052
  const { share, actions } = useMeetCore();
14053
+ const [soundHintClosed, setSoundHintClosed] = (0, import_react10.useState)(null);
14054
+ if (share.state === "presenting" && share.audio === false && soundHintClosed !== share.at) {
14055
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "share-audio-missing", children: [
14056
+ "Sound not shared",
14057
+ /* @__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" }),
14058
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setSoundHintClosed(share.at), children: "Dismiss" })
14059
+ ] });
14060
+ }
13320
14061
  if (share.state !== "refused") return null;
13321
14062
  const text3 = 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";
13322
14063
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "share-unavailable", children: [
@@ -13429,6 +14170,27 @@ function ExitScreen({ onLeave }) {
13429
14170
  ] });
13430
14171
  break;
13431
14172
  case "disconnected":
14173
+ if (failure?.stage === "join" && (failure.reason ?? null) === null) {
14174
+ title = "Can't connect to the meeting";
14175
+ line = connection === "offline" ? "You're offline. Check your internet connection." : "Check your network. A VPN, firewall or proxy may be blocking the call.";
14176
+ notice = "cannot-connect";
14177
+ actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
14178
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
14179
+ import_design_system.Button,
14180
+ {
14181
+ type: "button",
14182
+ disabled: busy,
14183
+ onClick: () => {
14184
+ setBusy(true);
14185
+ void actions.rejoin().catch(() => void 0).finally(() => setBusy(false));
14186
+ },
14187
+ children: "Try again"
14188
+ }
14189
+ ),
14190
+ home
14191
+ ] });
14192
+ break;
14193
+ }
13432
14194
  title = "You lost your connection to the meeting";
13433
14195
  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.";
13434
14196
  notice = "disconnected";