@ai-matrx/meet 0.11.31 → 0.11.33

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
@@ -330,6 +330,139 @@ __export(react_exports, {
330
330
  });
331
331
  module.exports = __toCommonJS(react_exports);
332
332
 
333
+ // src/core/tab-claim.ts
334
+ var TAB_ID_KEY = "mx.meet.tab";
335
+ var TAB_ANSWER_MS = 250;
336
+ function validTabId(value) {
337
+ return value !== null && /^[A-Za-z0-9_-]{4,64}$/.test(value);
338
+ }
339
+ function wait(ms2) {
340
+ return new Promise((resolve) => setTimeout(resolve, ms2));
341
+ }
342
+ function createTabClaim(platform = webTabPlatform()) {
343
+ let mine = null;
344
+ let current = null;
345
+ const self = platform.randomId();
346
+ const unsubscribe = platform.bus?.subscribe((message) => {
347
+ if (message.kind === "tab-alive?" && mine !== null && message.tabId === mine && message.from !== self) {
348
+ platform.bus?.post({ kind: "tab-alive", tabId: mine });
349
+ } else if (message.kind === "who-is-in?" && current !== null && mine !== null && message.from !== self && message.room === current.room && message.person === current.person) {
350
+ platform.bus?.post({ kind: "in-room", room: current.room, person: current.person, tabId: mine, to: message.from });
351
+ }
352
+ }) ?? (() => {
353
+ });
354
+ async function someoneAnswersFor(tabId2) {
355
+ const bus = platform.bus;
356
+ if (bus === null) return false;
357
+ let answered = false;
358
+ const stop = bus.subscribe((message) => {
359
+ if (message.kind === "tab-alive" && message.tabId === tabId2) answered = true;
360
+ });
361
+ bus.post({ kind: "tab-alive?", tabId: tabId2, from: self });
362
+ await wait(TAB_ANSWER_MS);
363
+ stop();
364
+ return answered;
365
+ }
366
+ async function claim() {
367
+ const stored = platform.storage?.get(TAB_ID_KEY) ?? null;
368
+ let candidate = validTabId(stored) ? stored : platform.randomId();
369
+ for (let attempt = 0; attempt < 4; attempt++) {
370
+ const held = await platform.tryLock(`${TAB_ID_KEY}:${candidate}`);
371
+ if (held) break;
372
+ if (!await someoneAnswersFor(candidate)) break;
373
+ candidate = platform.randomId();
374
+ }
375
+ mine = candidate;
376
+ platform.storage?.set(TAB_ID_KEY, candidate);
377
+ return candidate;
378
+ }
379
+ const tabId = claim();
380
+ return {
381
+ tabId,
382
+ inRoom(room, person) {
383
+ current = { room, person };
384
+ },
385
+ left() {
386
+ current = null;
387
+ },
388
+ async presentElsewhere(room, person) {
389
+ const bus = platform.bus;
390
+ if (bus === null) return false;
391
+ let seen = false;
392
+ const stop = bus.subscribe((message) => {
393
+ if (message.kind === "in-room" && message.to === self && message.room === room && message.person === person) {
394
+ seen = true;
395
+ }
396
+ });
397
+ bus.post({ kind: "who-is-in?", room, person, from: self });
398
+ await wait(TAB_ANSWER_MS);
399
+ stop();
400
+ return seen;
401
+ },
402
+ dispose() {
403
+ unsubscribe();
404
+ }
405
+ };
406
+ }
407
+ function randomTabId() {
408
+ const crypto = globalThis.crypto;
409
+ const raw = crypto?.randomUUID?.() ?? `${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
410
+ return raw.replace(/[^A-Za-z0-9]/g, "").slice(0, 16);
411
+ }
412
+ function webTabPlatform() {
413
+ const g = globalThis;
414
+ const session = g.sessionStorage;
415
+ const storage = session === void 0 ? null : {
416
+ get(key) {
417
+ try {
418
+ return session.getItem(key);
419
+ } catch {
420
+ return null;
421
+ }
422
+ },
423
+ set(key, value) {
424
+ try {
425
+ session.setItem(key, value);
426
+ } catch {
427
+ }
428
+ }
429
+ };
430
+ const channel = g.BroadcastChannel !== void 0 ? new g.BroadcastChannel("mx.meet.tabs") : null;
431
+ const bus = channel === null ? null : {
432
+ post(message) {
433
+ channel.postMessage(message);
434
+ },
435
+ subscribe(listener) {
436
+ const handler = (event) => {
437
+ const data = event.data;
438
+ if (data !== null && typeof data === "object" && typeof data.kind === "string") listener(data);
439
+ };
440
+ channel.addEventListener("message", handler);
441
+ return () => channel.removeEventListener("message", handler);
442
+ }
443
+ };
444
+ return {
445
+ storage,
446
+ bus,
447
+ randomId: randomTabId,
448
+ tryLock(name) {
449
+ const locks = g.navigator?.locks;
450
+ if (locks === void 0) return Promise.resolve(false);
451
+ return new Promise((resolve) => {
452
+ locks.request(name, { ifAvailable: true }, (lock) => {
453
+ if (lock === null) {
454
+ resolve(false);
455
+ return Promise.resolve();
456
+ }
457
+ resolve(true);
458
+ return new Promise(() => {
459
+ });
460
+ }).catch(() => resolve(false));
461
+ });
462
+ }
463
+ };
464
+ }
465
+
333
466
  // src/core/connection-supervisor.ts
334
467
  var DEFAULT_CONNECTION_POLICY = {
335
468
  bannerDelayMs: 2e3,
@@ -696,12 +829,18 @@ var MeetError = class extends Error {
696
829
  * renders `refused:<reason>` (or `ended`) from it — never from the HTTP status.
697
830
  */
698
831
  reason;
699
- constructor(code, message, remedy, cause, reason) {
832
+ /**
833
+ * A refusal that lifts by itself says when: whole seconds from the moment the server answered
834
+ * (a denial's cooldown, `reknock_cooldown_seconds`). Null for every refusal that does not.
835
+ */
836
+ retryAfterSeconds;
837
+ constructor(code, message, remedy, cause, reason, retryAfterSeconds) {
700
838
  super(message);
701
839
  this.name = "MeetError";
702
840
  this.code = code;
703
841
  this.remedy = remedy;
704
842
  this.reason = reason ?? null;
843
+ this.retryAfterSeconds = retryAfterSeconds ?? null;
705
844
  if (cause !== void 0) this.cause = cause;
706
845
  }
707
846
  /** True when retrying after the host re-establishes a session is the fix. */
@@ -1035,7 +1174,8 @@ function parseRoomToken(body, roomName) {
1035
1174
  const serverUrl = readString(payload, "server_url") ?? readString(payload, "serverUrl");
1036
1175
  const identity = readString(payload, "identity");
1037
1176
  const expiresAt = readString(payload, "expires_at") ?? readString(payload, "expiresAt");
1038
- if (token === null || serverUrl === null || identity === null) {
1177
+ const secondDeviceChoice = readString(payload, "second_device_choice");
1178
+ if (token === null && secondDeviceChoice === null || serverUrl === null || identity === null) {
1039
1179
  throw invalidResponse(
1040
1180
  "meet.token",
1041
1181
  "the response is missing token, server_url, or identity \u2014 joining with a partial token would fail inside LiveKit with an opaque error instead of here"
@@ -1047,11 +1187,15 @@ function parseRoomToken(body, roomName) {
1047
1187
  "the response carries no expires_at. Without it the package cannot refresh before the token dies, and a long meeting silently loses its ability to reconnect"
1048
1188
  );
1049
1189
  }
1190
+ const personKey = readString(payload, "person_key") ?? identity;
1050
1191
  return {
1051
- token,
1192
+ token: token ?? "",
1052
1193
  serverUrl,
1053
1194
  roomName,
1054
- identity,
1195
+ identity: personKey,
1196
+ connectionIdentity: readString(payload, "connection_identity") ?? personKey,
1197
+ connectionKind: readString(payload, "connection_kind") === "companion" ? "companion" : "main",
1198
+ secondDeviceChoice,
1055
1199
  displayName: readString(payload, "display_name") ?? readString(payload, "displayName") ?? identity,
1056
1200
  expiresAt,
1057
1201
  canPublish: readBoolean(payload, "can_publish", true),
@@ -1126,7 +1270,9 @@ function createTokenClient(options) {
1126
1270
  organization_id: input.identity.organizationId,
1127
1271
  display_name: input.identity.displayName,
1128
1272
  device_id: input.identity.deviceId,
1129
- guest
1273
+ guest,
1274
+ ...input.tabId !== void 0 ? { tab_id: input.tabId } : {},
1275
+ ...input.secondDevice !== void 0 ? { second_device: input.secondDevice } : {}
1130
1276
  }
1131
1277
  });
1132
1278
  } catch (error) {
@@ -1143,7 +1289,8 @@ function createTokenClient(options) {
1143
1289
  said?.message ?? "You can't join this meeting.",
1144
1290
  said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again.",
1145
1291
  void 0,
1146
- said?.reason ?? null
1292
+ said?.reason ?? null,
1293
+ said?.retryAfterSeconds ?? null
1147
1294
  );
1148
1295
  }
1149
1296
  if (response.status === 404) {
@@ -1201,6 +1348,10 @@ function createTokenClient(options) {
1201
1348
  if (!expired(entry)) return Promise.resolve(entry.token);
1202
1349
  }
1203
1350
  const pending = request(input, true).then((token) => {
1351
+ if (token.secondDeviceChoice != null) {
1352
+ cache.delete(input.roomName);
1353
+ return token;
1354
+ }
1204
1355
  cache.set(input.roomName, { token, mintedAt: now(), inFlight: null });
1205
1356
  remember(token);
1206
1357
  return token;
@@ -1235,6 +1386,9 @@ function placeholder(roomName) {
1235
1386
  serverUrl: "",
1236
1387
  roomName,
1237
1388
  identity: "",
1389
+ connectionIdentity: "",
1390
+ connectionKind: "main",
1391
+ secondDeviceChoice: null,
1238
1392
  displayName: "",
1239
1393
  expiresAt: (/* @__PURE__ */ new Date(0)).toISOString(),
1240
1394
  canPublish: false,
@@ -1259,7 +1413,9 @@ async function serverSentence(response) {
1259
1413
  if (message === null) return null;
1260
1414
  const remedy = typeof source.remedy === "string" && source.remedy.trim() !== "" ? source.remedy : void 0;
1261
1415
  const reason = typeof source.reason === "string" && source.reason !== "" ? source.reason : null;
1262
- return { message, remedy, reason };
1416
+ const retry = source.retry_after_seconds;
1417
+ const retryAfterSeconds = typeof retry === "number" && Number.isFinite(retry) && retry > 0 ? Math.ceil(retry) : null;
1418
+ return { message, remedy, reason, retryAfterSeconds };
1263
1419
  } catch {
1264
1420
  return null;
1265
1421
  }
@@ -1710,7 +1866,10 @@ var JOINABLE_FROM = /* @__PURE__ */ new Set([
1710
1866
  "superseded",
1711
1867
  "inactive",
1712
1868
  "denied",
1713
- "knock_expired"
1869
+ "knock_expired",
1870
+ // A refused knock whose rule lets the person ask again (a cooldown that ran out): the machine
1871
+ // allows the press; the server's gate decides whether it knocks or is refused again.
1872
+ "refused:lobby_rejects"
1714
1873
  ]);
1715
1874
  var NOT_ENDABLE = /* @__PURE__ */ new Set([
1716
1875
  "ended",
@@ -1794,6 +1953,8 @@ function nextPhase(from, event) {
1794
1953
  return WAITING_PHASES.has(from) ? "joining" : null;
1795
1954
  case "connected":
1796
1955
  return from === "joining" ? "in_call" : null;
1956
+ case "second_device":
1957
+ return from === "joining" ? "prejoin" : null;
1797
1958
  case "join_failed":
1798
1959
  return from === "joining" ? refusalPhase(event.reason) : null;
1799
1960
  case "exit":
@@ -1811,7 +1972,7 @@ function nextPhase(from, event) {
1811
1972
  case "call_outcome":
1812
1973
  return RINGING.has(from) ? event.outcome : null;
1813
1974
  case "reset":
1814
- return LIVE_PHASES.has(from) ? null : "resolving";
1975
+ return LIVE_PHASES.has(from) && event.teardown !== true ? null : "resolving";
1815
1976
  }
1816
1977
  }
1817
1978
  function resolveExit(input) {
@@ -2117,8 +2278,8 @@ function createCallCenter(options) {
2117
2278
  "A late accept must not resurrect a settled call. Close the ringing UI; the caller has already moved on."
2118
2279
  );
2119
2280
  }
2120
- if (options.store.snapshot().phase === "ringing_in") options.store.dispatch({ type: "call_answered" });
2121
2281
  const settled = await repository.settleCallInvite(id, "connecting", null);
2282
+ if (options.store.snapshot().phase === "ringing_in") options.store.dispatch({ type: "call_answered" });
2122
2283
  options.store.patch({ call: { invite: settled, direction: "incoming" } });
2123
2284
  settleLocal(settled);
2124
2285
  channel?.send(MEET_EVENTS.settled, settled);
@@ -2329,11 +2490,17 @@ function createDeviceManager(options) {
2329
2490
  }
2330
2491
 
2331
2492
  // src/core/names.ts
2493
+ var CONNECTION_SEPARATOR = "~";
2494
+ function personOf(identity) {
2495
+ const value = String(identity ?? "").trim();
2496
+ const cut = value.indexOf(CONNECTION_SEPARATOR);
2497
+ return cut === -1 ? value : value.slice(0, cut);
2498
+ }
2332
2499
  function isGuestIdentity(identity) {
2333
- return String(identity).startsWith("guest:");
2500
+ return personOf(identity).startsWith("guest:");
2334
2501
  }
2335
2502
  function isAgentIdentity(identity) {
2336
- return String(identity).startsWith("agent:");
2503
+ return personOf(identity).startsWith("agent:");
2337
2504
  }
2338
2505
  var NOTE_TAKER_PRODUCT_NAME = "Matrx note-taker";
2339
2506
  var AGENT_PRODUCT_NAME = "Matrx agent";
@@ -3933,6 +4100,7 @@ var EMPTY = {
3933
4100
  waiting: null,
3934
4101
  meeting: null,
3935
4102
  localIdentity: null,
4103
+ secondDevice: null,
3936
4104
  grants: NO_GRANTS,
3937
4105
  participants: [],
3938
4106
  activeSpeakers: [],
@@ -4183,7 +4351,8 @@ function createMeetStore(initial = {}) {
4183
4351
  commit({ ...state, media: { ...state.media, [kind]: next } });
4184
4352
  },
4185
4353
  reset() {
4186
- commit({ ...EMPTY, effects: state.effects });
4354
+ const phase = nextPhase(state.phase, { type: "reset", teardown: true }) ?? EMPTY.phase;
4355
+ commit({ ...EMPTY, effects: state.effects, phase, reason: reasonOf(phase), phaseNotice: null });
4187
4356
  }
4188
4357
  };
4189
4358
  }
@@ -4216,6 +4385,28 @@ function lobbyEntryFrom(attendee, now = Date.now()) {
4216
4385
  decidedBy: null
4217
4386
  };
4218
4387
  }
4388
+ function deniedEntryFrom(attendee) {
4389
+ if (attendee.admissionState !== "denied") return null;
4390
+ return {
4391
+ identity: attendee.identity,
4392
+ displayName: attendee.displayName,
4393
+ avatarUrl: attendee.avatarUrl,
4394
+ userId: attendee.userId,
4395
+ state: "denied",
4396
+ knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4397
+ knockExpiresAt: null,
4398
+ decidedAt: null,
4399
+ decidedBy: null
4400
+ };
4401
+ }
4402
+ function applyDoorRow(store, attendee) {
4403
+ const entry = lobbyEntryFrom(attendee) ?? deniedEntryFrom(attendee);
4404
+ if (entry !== null) {
4405
+ store.upsertLobby(entry);
4406
+ return;
4407
+ }
4408
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4409
+ }
4219
4410
  function decidedState(state) {
4220
4411
  return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
4221
4412
  }
@@ -4350,6 +4541,7 @@ function createMeetingSession(options) {
4350
4541
  admission,
4351
4542
  admittersPresent: read.admitters_present ?? null,
4352
4543
  reknockAllowed: read.reknock_allowed ?? true,
4544
+ reknockAt: typeof read.reknock_in_seconds === "number" && read.reknock_in_seconds > 0 ? Date.now() + read.reknock_in_seconds * 1e3 : null,
4353
4545
  knockExpiresAt: read.knock_expires_at ?? null,
4354
4546
  pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
4355
4547
  liveVersion: read.live_version ?? 0
@@ -4432,11 +4624,7 @@ function createMeetingSession(options) {
4432
4624
  try {
4433
4625
  const attendees = await repository.attendees(meeting.id);
4434
4626
  store.learn(attendees);
4435
- for (const attendee of attendees) {
4436
- const entry = lobbyEntryFrom(attendee);
4437
- if (entry !== null) store.upsertLobby(entry);
4438
- else store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4439
- }
4627
+ for (const attendee of attendees) applyDoorRow(store, attendee);
4440
4628
  } catch {
4441
4629
  }
4442
4630
  }
@@ -4518,12 +4706,7 @@ function createMeetingSession(options) {
4518
4706
  if (row === null) return;
4519
4707
  const attendee = repository.projectAttendee(row);
4520
4708
  store.learn([attendee]);
4521
- const entry = lobbyEntryFrom(attendee);
4522
- if (entry !== null) {
4523
- store.upsertLobby(entry);
4524
- return;
4525
- }
4526
- store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4709
+ applyDoorRow(store, attendee);
4527
4710
  }
4528
4711
  },
4529
4712
  {
@@ -4570,14 +4753,7 @@ function createMeetingSession(options) {
4570
4753
  store.setRecording(latest.state, latest.startedBy, "record", latest.id);
4571
4754
  }
4572
4755
  store.learn(attendees);
4573
- for (const attendee of attendees) {
4574
- const entry = lobbyEntryFrom(attendee);
4575
- if (entry !== null) {
4576
- store.upsertLobby(entry);
4577
- } else {
4578
- store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4579
- }
4580
- }
4756
+ for (const attendee of attendees) applyDoorRow(store, attendee);
4581
4757
  }
4582
4758
  });
4583
4759
  meetingChannel.requestBackfill("manual");
@@ -5479,6 +5655,7 @@ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
5479
5655
  }
5480
5656
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
5481
5657
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
5658
+ var CONNECTION_KIND_ATTRIBUTE = "matrx.participant.connection_kind";
5482
5659
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
5483
5660
  var KNOWN_STANDINGS = ["member", "invited", "outsider", "guest"];
5484
5661
  function parseStanding(value) {
@@ -5502,7 +5679,8 @@ function projectParticipant(driver, joinedAt) {
5502
5679
  }
5503
5680
  const userId = typeof metadata.user_id === "string" ? asUserId(metadata.user_id) : null;
5504
5681
  return {
5505
- identity: asParticipantIdentity(driver.identity),
5682
+ // S7: a tile is a PERSON; LiveKit names the tab's connection (`<person>~<tab>`).
5683
+ identity: personOf(driver.identity),
5506
5684
  userId,
5507
5685
  // 🚨 A RAW LIVEKIT IDENTITY IS NEVER A NAME (MRI-A18). Through 0.4.12 this
5508
5686
  // line read `driver.name.length > 0 ? driver.name : driver.identity`, and
@@ -5549,6 +5727,11 @@ function createRoomEngine(options) {
5549
5727
  let ghosts = /* @__PURE__ */ new Map();
5550
5728
  let ghostTimer = null;
5551
5729
  let liveRoster = [];
5730
+ let personConnections = /* @__PURE__ */ new Map();
5731
+ let mainConnection = /* @__PURE__ */ new Map();
5732
+ function connectionsOf(who) {
5733
+ return personConnections.get(personOf(who)) ?? [who];
5734
+ }
5552
5735
  const departures = /* @__PURE__ */ new Map();
5553
5736
  function requestAsNow() {
5554
5737
  if (lastRequest === null) return null;
@@ -5613,7 +5796,12 @@ function createRoomEngine(options) {
5613
5796
  function failJoin(error, operation) {
5614
5797
  const normalized = normalizeMeetError(error, operation);
5615
5798
  store.patch({
5616
- failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
5799
+ failure: {
5800
+ message: normalized.message,
5801
+ remedy: normalized.remedy,
5802
+ reason: normalized.reason,
5803
+ retryAt: normalized.retryAfterSeconds === null ? null : now() + normalized.retryAfterSeconds * 1e3
5804
+ }
5617
5805
  });
5618
5806
  store.dispatch({ type: "join_failed", reason: normalized.reason ?? null });
5619
5807
  report(
@@ -5685,10 +5873,10 @@ function createRoomEngine(options) {
5685
5873
  if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
5686
5874
  },
5687
5875
  activeSpeakers(identities) {
5688
- store.setSpeaking(identities.map(asParticipantIdentity));
5876
+ store.setSpeaking([...new Set(identities.map((who) => personOf(who)))]);
5689
5877
  },
5690
5878
  quality(who, value) {
5691
- store.setQuality(asParticipantIdentity(who), value);
5879
+ store.setQuality(personOf(who), value);
5692
5880
  },
5693
5881
  connectionState(state) {
5694
5882
  supervisor.driverState(state);
@@ -5708,7 +5896,7 @@ function createRoomEngine(options) {
5708
5896
  );
5709
5897
  return;
5710
5898
  }
5711
- const speakerIdentity = asParticipantIdentity(event.speakerIdentity);
5899
+ const speakerIdentity = personOf(event.speakerIdentity);
5712
5900
  const speaker = snapshot.participants.find(
5713
5901
  (participant) => participant.identity === speakerIdentity
5714
5902
  );
@@ -5782,7 +5970,7 @@ function createRoomEngine(options) {
5782
5970
  );
5783
5971
  return;
5784
5972
  }
5785
- const sender = asParticipantIdentity(from ?? "");
5973
+ const sender = personOf(from ?? "");
5786
5974
  switch (topic) {
5787
5975
  case DATA_TOPICS.hand:
5788
5976
  store.setHand(
@@ -5824,7 +6012,7 @@ function createRoomEngine(options) {
5824
6012
  }
5825
6013
  };
5826
6014
  async function resolveExitAndApply(hint) {
5827
- const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
6015
+ const connection = current?.connectionIdentity ?? current?.identity ?? store.snapshot().localIdentity ?? null;
5828
6016
  lastRequest = requestAsNow();
5829
6017
  await dropRoom();
5830
6018
  let server;
@@ -5862,14 +6050,29 @@ function createRoomEngine(options) {
5862
6050
  }
5863
6051
  async function connect(request) {
5864
6052
  store.patch({ failure: null, waiting: null, roomName: request.roomName, share: NO_SHARE, connection: "stable" });
6053
+ const tabId = options.tabClaim !== void 0 ? await options.tabClaim.tabId : null;
5865
6054
  const token = await tokens.get({
5866
6055
  roomName: request.roomName,
5867
6056
  identity,
5868
6057
  ...request.meetingId !== void 0 ? { meetingId: request.meetingId } : {},
5869
6058
  ...request.slug !== void 0 ? { slug: request.slug } : {},
5870
6059
  ...request.breakoutRoomId != null ? { breakoutRoomId: request.breakoutRoomId } : {},
5871
- ...request.observe === true ? { observe: true } : {}
6060
+ ...request.observe === true ? { observe: true } : {},
6061
+ ...tabId !== null ? { tabId } : {},
6062
+ ...request.secondDevice !== void 0 ? { secondDevice: request.secondDevice } : {}
5872
6063
  });
6064
+ if (token.secondDeviceChoice != null) {
6065
+ store.patch({ secondDevice: { rule: token.secondDeviceChoice }, localIdentity: token.identity });
6066
+ store.dispatch({ type: "second_device" });
6067
+ report("info", `meet: already in this meeting elsewhere \u2014 offering the choice (${token.secondDeviceChoice})`);
6068
+ return;
6069
+ }
6070
+ if (store.snapshot().secondDevice !== null) store.patch({ secondDevice: null });
6071
+ if (request.secondDevice === "switch_here" && lastRequest !== null) {
6072
+ const { secondDevice: _answered, ...rest } = lastRequest;
6073
+ lastRequest = rest;
6074
+ }
6075
+ options.tabClaim?.inRoom(String(request.roomName), String(token.identity));
5873
6076
  current = token;
5874
6077
  lastServerUrl = token.serverUrl;
5875
6078
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
@@ -5934,7 +6137,7 @@ function createRoomEngine(options) {
5934
6137
  if (request.mode === "video") {
5935
6138
  await enableTrack(created, "camera", true);
5936
6139
  }
5937
- if (request.microphone !== false) {
6140
+ if (request.microphone !== false && current?.connectionKind !== "companion") {
5938
6141
  await enableTrack(created, "microphone", true);
5939
6142
  }
5940
6143
  media.onConnected();
@@ -5956,7 +6159,8 @@ function createRoomEngine(options) {
5956
6159
  }
5957
6160
  async function publish(topic, body, to) {
5958
6161
  if (driver === null) return;
5959
- await driver.publishData(topic, JSON.stringify(body), to);
6162
+ const addresses = to === void 0 ? void 0 : to.flatMap((who) => connectionsOf(who));
6163
+ await driver.publishData(topic, JSON.stringify(body), addresses);
5960
6164
  }
5961
6165
  async function applyEffects(target, effects) {
5962
6166
  try {
@@ -5994,8 +6198,19 @@ function createRoomEngine(options) {
5994
6198
  liveRoster = list;
5995
6199
  const merged = mergeGhosts(list, ghosts, at);
5996
6200
  ghosts = merged.kept;
6201
+ const people = /* @__PURE__ */ new Map();
6202
+ const connections = /* @__PURE__ */ new Map();
6203
+ for (const entry of merged.roster) {
6204
+ const person = personOf(entry.identity);
6205
+ connections.set(person, [...connections.get(person) ?? [], entry.identity]);
6206
+ const rank = (entry.isLocal ? 8 : 0) + (entry.reconnecting === true ? 0 : 4) + (entry.attributes[CONNECTION_KIND_ATTRIBUTE] === "companion" ? 0 : 2) + (entry.cameraEnabled || entry.micEnabled ? 1 : 0);
6207
+ const held = people.get(person);
6208
+ if (held === void 0 || rank > held.rank) people.set(person, { entry, rank });
6209
+ }
6210
+ personConnections = connections;
6211
+ mainConnection = new Map([...people].map(([person, { entry }]) => [person, entry.identity]));
5997
6212
  store.setParticipants(
5998
- merged.roster.map((entry) => {
6213
+ [...people.values()].map(({ entry }) => {
5999
6214
  const projected = projectParticipant(entry, joinedAt);
6000
6215
  return entry.reconnecting === true ? { ...projected, reconnecting: true, quality: "lost" } : projected;
6001
6216
  })
@@ -6172,7 +6387,13 @@ function createRoomEngine(options) {
6172
6387
  },
6173
6388
  async setScreenShareEnabled(enabled) {
6174
6389
  if (driver === null) return;
6175
- if (enabled && driver.supportsScreenShare?.() === false) {
6390
+ let unsupported = false;
6391
+ try {
6392
+ unsupported = enabled && driver.supportsScreenShare?.() === false;
6393
+ } catch {
6394
+ unsupported = false;
6395
+ }
6396
+ if (unsupported) {
6176
6397
  store.patch({ share: { state: "refused", reason: "unsupported", remedy: null, at: now() } });
6177
6398
  report("warn", "meet: this browser cannot share a screen", "Share from a desktop browser (Chrome, Edge, Firefox or Safari).");
6178
6399
  return;
@@ -6271,7 +6492,7 @@ function createRoomEngine(options) {
6271
6492
  },
6272
6493
  attach(who, kind, element) {
6273
6494
  if (driver === null) return () => void 0;
6274
- return driver.attach(who, kind, element);
6495
+ return driver.attach(mainConnection.get(personOf(who)) ?? who, kind, element);
6275
6496
  },
6276
6497
  token: () => current,
6277
6498
  async dispose() {
@@ -7005,6 +7226,12 @@ function defaultDiagnostics(event) {
7005
7226
  else if (event.level === "warn") console.warn(line);
7006
7227
  else console.info(line);
7007
7228
  }
7229
+ var claimedTab = null;
7230
+ function pageTabClaim() {
7231
+ if (typeof window === "undefined") return void 0;
7232
+ claimedTab ??= createTabClaim();
7233
+ return claimedTab;
7234
+ }
7008
7235
  function deviceId() {
7009
7236
  const storage = globalThis.localStorage;
7010
7237
  const key = "mx.meet.device";
@@ -7122,6 +7349,8 @@ function MeetRuntime(props) {
7122
7349
  store,
7123
7350
  tokens,
7124
7351
  identity,
7352
+ // S7: this page's tab id (one per page, claimed at load) — the LiveKit identity's tab half.
7353
+ tabClaim: pageTabClaim(),
7125
7354
  resolveMeeting: (slug) => repository.meetingBySlug(slug),
7126
7355
  driverFactory: (handlers) => (driverRef.current ?? createLiveKitDriver)(handlers),
7127
7356
  onDiagnostic: report,
@@ -8197,6 +8426,7 @@ function useRoom() {
8197
8426
  phase: snapshot?.phase ?? "resolving",
8198
8427
  participants: snapshot?.participants ?? [],
8199
8428
  failure: snapshot?.failure ?? null,
8429
+ secondDevice: snapshot?.secondDevice ?? null,
8200
8430
  local,
8201
8431
  canShareScreen: engine?.supportsScreenShare() ?? true,
8202
8432
  join: (0, import_react4.useCallback)(
@@ -8342,6 +8572,7 @@ function useLobby() {
8342
8572
  const session = host?.meetings ?? null;
8343
8573
  return {
8344
8574
  waiting: (snapshot?.lobby ?? []).filter((entry) => entry.state === "knocking"),
8575
+ declined: (snapshot?.lobby ?? []).filter((entry) => entry.state === "denied"),
8345
8576
  // No runtime means no host powers — `<LobbyPanel>` renders nothing rather
8346
8577
  // than an admit button that would throw.
8347
8578
  canAdmit: session !== null && isHost,
@@ -8363,6 +8594,37 @@ function useLobby() {
8363
8594
  )
8364
8595
  };
8365
8596
  }
8597
+ function useCountdown(untilMs) {
8598
+ const [now, setNow] = (0, import_react4.useState)(() => Date.now());
8599
+ const active = typeof untilMs === "number" && untilMs > now;
8600
+ (0, import_react4.useEffect)(() => {
8601
+ if (!active) return void 0;
8602
+ setNow(Date.now());
8603
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
8604
+ return () => clearInterval(timer);
8605
+ }, [active, untilMs]);
8606
+ if (typeof untilMs !== "number") return null;
8607
+ return Math.max(0, untilMs - now);
8608
+ }
8609
+ function useKnockNotice() {
8610
+ const lobby = useLobby();
8611
+ const [dismissed, setDismissed] = (0, import_react4.useState)([]);
8612
+ const keyOf = (entry) => `${entry.identity}@${entry.knockedAt}`;
8613
+ const open = lobby.canAdmit ? lobby.waiting.filter((entry) => !dismissed.includes(keyOf(entry))) : [];
8614
+ const knocker = open[0] ?? null;
8615
+ return {
8616
+ knocker,
8617
+ others: Math.max(0, open.length - 1),
8618
+ admit: (0, import_react4.useCallback)(async () => {
8619
+ if (knocker === null) return;
8620
+ setDismissed((list) => [...list, keyOf(knocker)]);
8621
+ await lobby.admit(knocker);
8622
+ }, [knocker, lobby.admit]),
8623
+ dismiss: (0, import_react4.useCallback)(() => {
8624
+ if (knocker !== null) setDismissed((list) => [...list, keyOf(knocker)]);
8625
+ }, [knocker])
8626
+ };
8627
+ }
8366
8628
  function useWaiting() {
8367
8629
  const host = useMeetHost();
8368
8630
  const snapshot = useMeetSnapshot();
@@ -11112,6 +11374,7 @@ function ControlBar({ onLeave }) {
11112
11374
  }
11113
11375
  ) }) }) : null,
11114
11376
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BreakoutBanner, {}),
11377
+ panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(KnockNotice, { onView: () => setPanel("people") }) : null,
11115
11378
  controls.notice !== null && panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
11116
11379
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__controls", children: [
11117
11380
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -11442,10 +11705,21 @@ function MediaNotices() {
11442
11705
  const name = (kind) => kind === "microphone" ? "Microphone" : "Camera";
11443
11706
  if (chips.length === 0 && region.audioOutput !== "blocked" && notice === null && offer === null) return null;
11444
11707
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__media-notices", children: [
11445
- region.audioOutput === "blocked" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__media-pill", role: "alert", "data-meet-notice": "audio-blocked", children: [
11446
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "Sound is paused in this tab" }),
11447
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void enableAudio(), children: "Enable sound" })
11448
- ] }) : null,
11708
+ region.audioOutput === "blocked" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
11709
+ import_design_system.ErrorBox,
11710
+ {
11711
+ as: "div",
11712
+ className: "mx-meet__media-pill",
11713
+ "data-meet-notice": "audio-blocked",
11714
+ message: "Sound is paused in this tab",
11715
+ operation: "Enable meeting sound",
11716
+ origin: "meet/MediaNotices",
11717
+ children: [
11718
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "Sound is paused in this tab" }),
11719
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void enableAudio(), children: "Enable sound" })
11720
+ ]
11721
+ }
11722
+ ) : null,
11449
11723
  chips.map(({ kind, reason }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__media-pill", role: "status", "data-meet-notice": deviceNoticeCode(kind, reason), children: [
11450
11724
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: deviceChipLabel(kind, reason) }),
11451
11725
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -12245,9 +12519,68 @@ function useMicrophoneStream(deviceId2) {
12245
12519
  }, [deviceId2]);
12246
12520
  return stream;
12247
12521
  }
12522
+ function KnockNotice({ onView }) {
12523
+ const { knocker, others, admit, dismiss } = useKnockNotice();
12524
+ if (knocker === null) return null;
12525
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__notice-bar", role: "status", "data-meet-notice": "knock-received", "data-meet-participant-name": knocker.displayName, children: [
12526
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
12527
+ knocker.displayName,
12528
+ " wants to join",
12529
+ others > 0 ? ` (+${others} more)` : ""
12530
+ ] }),
12531
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => void admit().catch(() => void 0), children: "Admit" }),
12532
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12533
+ "button",
12534
+ {
12535
+ type: "button",
12536
+ className: "mx-meet__link",
12537
+ onClick: () => {
12538
+ dismiss();
12539
+ onView();
12540
+ },
12541
+ children: "View"
12542
+ }
12543
+ ),
12544
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismiss, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CloseIcon, { width: 14, height: 14 }) })
12545
+ ] });
12546
+ }
12248
12547
  function LobbyPanel() {
12249
12548
  const lobby = useLobby();
12250
- if (!lobby.canAdmit || lobby.waiting.length === 0) return null;
12549
+ if (!lobby.canAdmit || lobby.waiting.length === 0 && lobby.declined.length === 0) return null;
12550
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
12551
+ lobby.waiting.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(WaitingList, { lobby }) : null,
12552
+ lobby.declined.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DeclinedList, { lobby }) : null
12553
+ ] });
12554
+ }
12555
+ function DeclinedList({ lobby }) {
12556
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("section", { className: "mx-meet__lobby", "aria-label": "Declined requests", "data-meet-declined": "", children: [
12557
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("header", { className: "mx-meet__lobby-head", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h3", { className: "mx-meet__lobby-title", children: "Declined" }) }),
12558
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className: "mx-meet__lobby-list", children: lobby.declined.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
12559
+ "li",
12560
+ {
12561
+ className: "mx-meet__lobby-row",
12562
+ "data-meet-declined-entry": "",
12563
+ "data-meet-participant-name": entry.displayName,
12564
+ children: [
12565
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Avatar, { name: entry.displayName, url: entry.avatarUrl, size: 32 }),
12566
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__person-name", children: entry.displayName }),
12567
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12568
+ "button",
12569
+ {
12570
+ type: "button",
12571
+ className: "mx-meet__link",
12572
+ "aria-label": `Let ${entry.displayName} ask again`,
12573
+ onClick: () => void lobby.undeny(entry),
12574
+ children: "Let them ask again"
12575
+ }
12576
+ )
12577
+ ]
12578
+ },
12579
+ entry.identity
12580
+ )) })
12581
+ ] });
12582
+ }
12583
+ function WaitingList({ lobby }) {
12251
12584
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("section", { className: "mx-meet__lobby", "aria-label": "Waiting room", children: [
12252
12585
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("header", { className: "mx-meet__lobby-head", children: [
12253
12586
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h3", { className: "mx-meet__lobby-title", children: lobby.waiting.length === 1 ? "1 person is waiting" : `${lobby.waiting.length} people are waiting` }),
@@ -12511,6 +12844,21 @@ function PreJoinBody({
12511
12844
  (0, import_react10.useEffect)(() => {
12512
12845
  void host.devices.refresh();
12513
12846
  }, [host]);
12847
+ const joinWith = (secondDevice) => {
12848
+ setJoining(true);
12849
+ preview.release();
12850
+ void room.join({
12851
+ roomName,
12852
+ // An observer publishes nothing: no camera, no microphone (MD-16). A companion
12853
+ // ("join here too") never opens the microphone (S7).
12854
+ mode: observe2 ? "audio" : mode,
12855
+ microphone: observe2 || secondDevice === "join_too" ? false : microphone,
12856
+ ...meetingId !== void 0 ? { meetingId } : {},
12857
+ ...slug !== void 0 ? { slug } : {},
12858
+ ...observe2 ? { observe: true } : {},
12859
+ ...secondDevice !== void 0 ? { secondDevice } : {}
12860
+ }).then(() => onJoined?.()).catch(() => void 0).finally(() => setJoining(false));
12861
+ };
12514
12862
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__prejoin", children: [
12515
12863
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__preview", children: [
12516
12864
  mode === "video" && preview.preview !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -12595,27 +12943,34 @@ function PreJoinBody({
12595
12943
  ),
12596
12944
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Label, { htmlFor: "mx-meet-join-mic", children: "Join with microphone" })
12597
12945
  ] }),
12598
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12599
- import_design_system.Button,
12600
- {
12601
- type: "button",
12602
- className: "mx-meet__join",
12603
- disabled: joining,
12604
- onClick: () => {
12605
- setJoining(true);
12606
- preview.release();
12607
- void room.join({
12608
- roomName,
12609
- // An observer publishes nothing: no camera, no microphone (MD-16).
12610
- mode: observe2 ? "audio" : mode,
12611
- microphone: observe2 ? false : microphone,
12612
- ...meetingId !== void 0 ? { meetingId } : {},
12613
- ...slug !== void 0 ? { slug } : {},
12614
- ...observe2 ? { observe: true } : {}
12615
- }).then(() => onJoined?.()).catch(() => void 0).finally(() => setJoining(false));
12616
- },
12617
- children: joining ? "Joining\u2026" : observe2 ? "Observe" : "Join now"
12618
- }
12946
+ room.secondDevice === null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Button, { type: "button", className: "mx-meet__join", disabled: joining, onClick: () => joinWith(), children: joining ? "Joining\u2026" : observe2 ? "Observe" : "Join now" }) : (
12947
+ // S7: ALREADY IN THIS MEETING on another tab or device — the meeting's choice
12948
+ // (Meet: Switch here / Join here too; Teams: Transfer here / Add this device).
12949
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__second-device", "data-meet-region": "second-device", children: [
12950
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__hint", children: "You're in this meeting on another device" }),
12951
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12952
+ import_design_system.Button,
12953
+ {
12954
+ type: "button",
12955
+ className: "mx-meet__join",
12956
+ "data-meet-control": "switch-here",
12957
+ disabled: joining,
12958
+ onClick: () => joinWith("switch_here"),
12959
+ children: room.secondDevice.rule === "ask_transfer_or_add" ? "Transfer here" : "Switch here"
12960
+ }
12961
+ ),
12962
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12963
+ import_design_system.Button,
12964
+ {
12965
+ type: "button",
12966
+ variant: "outline",
12967
+ "data-meet-control": "join-too",
12968
+ disabled: joining,
12969
+ onClick: () => joinWith("join_too"),
12970
+ children: room.secondDevice.rule === "ask_transfer_or_add" ? "Add this device" : "Join here too"
12971
+ }
12972
+ )
12973
+ ] })
12619
12974
  )
12620
12975
  ] }),
12621
12976
  room.failure !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__error", children: [
@@ -12813,16 +13168,21 @@ var REFUSED_TITLES = {
12813
13168
  not_invited: "You aren't invited to this meeting",
12814
13169
  locked: "This meeting is locked",
12815
13170
  removed: "You can't rejoin this meeting",
12816
- lobby_rejects: "You can't ask to join this meeting",
13171
+ lobby_rejects: "The host declined your request",
12817
13172
  meeting_full: "This meeting is full",
12818
13173
  platform_capacity: "Meetings are busy right now",
12819
13174
  verification_required: "Verify your account to join",
12820
13175
  registration_required: "Register to join this meeting"
12821
13176
  };
13177
+ function clockText(ms2) {
13178
+ const total = Math.ceil(ms2 / 1e3);
13179
+ return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`;
13180
+ }
12822
13181
  function ExitScreen({ onLeave }) {
12823
13182
  const { phase, reason, host, snapshot, actions, call, connection } = useMeetCore();
12824
13183
  const [busy, setBusy] = (0, import_react10.useState)(false);
12825
13184
  const failure = snapshot?.failure ?? null;
13185
+ const retryLeft = useCountdown(phase === "refused:lobby_rejects" ? failure?.retryAt ?? null : null);
12826
13186
  const rejoin = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12827
13187
  import_design_system.Button,
12828
13188
  {
@@ -12865,6 +13225,33 @@ function ExitScreen({ onLeave }) {
12865
13225
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Button, { type: "button", variant: "outline", onClick: actions.retry, children: "Try again" }),
12866
13226
  home
12867
13227
  ] });
13228
+ if (code === "lobby_rejects") {
13229
+ if (retryLeft !== null && retryLeft > 0) {
13230
+ line = `You can ask again in ${clockText(retryLeft)}`;
13231
+ notice = "reknock-cooldown";
13232
+ actionsNode = home;
13233
+ } else if (retryLeft === 0) {
13234
+ line = "You can ask to join again";
13235
+ actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
13236
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
13237
+ import_design_system.Button,
13238
+ {
13239
+ type: "button",
13240
+ disabled: busy,
13241
+ onClick: () => {
13242
+ setBusy(true);
13243
+ void actions.rejoin().catch(() => void 0).finally(() => setBusy(false));
13244
+ },
13245
+ children: "Ask to join again"
13246
+ }
13247
+ ),
13248
+ home
13249
+ ] });
13250
+ } else {
13251
+ line = "Ask the host to let you in, then open the link again";
13252
+ actionsNode = home;
13253
+ }
13254
+ }
12868
13255
  } else {
12869
13256
  switch (phase) {
12870
13257
  case "left":
@@ -12925,7 +13312,7 @@ function ExitScreen({ onLeave }) {
12925
13312
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Avatar, { name: host?.identity.displayName ?? "", url: host?.identity.avatarUrl ?? null, size: 72 }),
12926
13313
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h1", { className: "mx-meet__prejoin-title", children: title }),
12927
13314
  line !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__empty-body", children: line }) : null,
12928
- notice !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { role: "status", "data-meet-notice": notice, className: "mx-meet__sr-only", children: title }) : null,
13315
+ notice !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { role: "status", "data-meet-notice": notice, className: "mx-meet__sr-only", children: notice === "reknock-cooldown" && line !== null ? line : title }) : null,
12929
13316
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__waiting-actions", children: actionsNode })
12930
13317
  ] });
12931
13318
  }
@@ -12934,6 +13321,7 @@ function WaitingRoom({ host, onLeave }) {
12934
13321
  const { phaseNotice } = useMeetCore();
12935
13322
  const [busy, setBusy] = (0, import_react10.useState)(false);
12936
13323
  const admission = waiting?.admission ?? "knocking";
13324
+ const reknockLeft = useCountdown(admission === "denied" ? waiting?.reknockAt ?? null : null);
12937
13325
  const run = (action) => () => {
12938
13326
  setBusy(true);
12939
13327
  void action().catch(() => void 0).finally(() => setBusy(false));
@@ -12941,6 +13329,7 @@ function WaitingRoom({ host, onLeave }) {
12941
13329
  const home = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Button, { type: "button", variant: "outline", onClick: () => onLeave !== void 0 ? onLeave() : window.location.assign("/"), children: "Back to home" });
12942
13330
  let title;
12943
13331
  let line = null;
13332
+ let cooldownNotice = false;
12944
13333
  let actions;
12945
13334
  switch (admission) {
12946
13335
  case "waiting_host":
@@ -12949,8 +13338,12 @@ function WaitingRoom({ host, onLeave }) {
12949
13338
  break;
12950
13339
  case "denied":
12951
13340
  title = "The host declined your request";
13341
+ if (reknockLeft !== null && reknockLeft > 0) {
13342
+ line = `You can ask again in ${clockText(reknockLeft)}`;
13343
+ cooldownNotice = true;
13344
+ }
12952
13345
  actions = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
12953
- waiting?.reknockAllowed === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Button, { type: "button", disabled: busy, onClick: run(askAgain), children: "Ask to join again" }) : null,
13346
+ waiting?.reknockAllowed === true || waiting?.reknockAt != null && reknockLeft === 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Button, { type: "button", disabled: busy, onClick: run(askAgain), children: "Ask to join again" }) : null,
12954
13347
  home
12955
13348
  ] });
12956
13349
  break;
@@ -12981,7 +13374,15 @@ function WaitingRoom({ host, onLeave }) {
12981
13374
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 72 }),
12982
13375
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h1", { className: "mx-meet__prejoin-title", children: title }),
12983
13376
  phaseNotice === "sent-to-waiting" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__empty-body", role: "status", "data-meet-notice": "sent-to-waiting", children: "The host moved you back to the waiting room" }) : null,
12984
- line !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__empty-body", role: "status", "data-meet-notice": "host-absent", children: line }) : null,
13377
+ line !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
13378
+ "p",
13379
+ {
13380
+ className: "mx-meet__empty-body",
13381
+ role: "status",
13382
+ "data-meet-notice": cooldownNotice ? "reknock-cooldown" : "host-absent",
13383
+ children: line
13384
+ }
13385
+ ) : null,
12985
13386
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__waiting-actions", children: actions })
12986
13387
  ] });
12987
13388
  }