@ai-matrx/meet 0.11.32 → 0.11.34

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");
@@ -4629,9 +4805,9 @@ function createMeetingSession(options) {
4629
4805
  },
4630
4806
  stopWaiting,
4631
4807
  readState: readState2,
4632
- async readExit(connection) {
4808
+ async readExit(connection, held) {
4633
4809
  const meeting = store.snapshot().meeting;
4634
- const pass = options.resolvePass?.() ?? null;
4810
+ const pass = held ?? options.resolvePass?.() ?? null;
4635
4811
  if (meeting === null || pass === null) return void 0;
4636
4812
  try {
4637
4813
  const response = await options.api.request({
@@ -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,8 @@ 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;
6016
+ const heldPass = current?.meetingPass ?? null;
5828
6017
  lastRequest = requestAsNow();
5829
6018
  await dropRoom();
5830
6019
  let server;
@@ -5832,7 +6021,7 @@ function createRoomEngine(options) {
5832
6021
  server = void 0;
5833
6022
  } else {
5834
6023
  try {
5835
- server = await options.readExit(connection);
6024
+ server = await options.readExit(connection, heldPass);
5836
6025
  } catch {
5837
6026
  server = null;
5838
6027
  }
@@ -5862,14 +6051,29 @@ function createRoomEngine(options) {
5862
6051
  }
5863
6052
  async function connect(request) {
5864
6053
  store.patch({ failure: null, waiting: null, roomName: request.roomName, share: NO_SHARE, connection: "stable" });
6054
+ const tabId = options.tabClaim !== void 0 ? await options.tabClaim.tabId : null;
5865
6055
  const token = await tokens.get({
5866
6056
  roomName: request.roomName,
5867
6057
  identity,
5868
6058
  ...request.meetingId !== void 0 ? { meetingId: request.meetingId } : {},
5869
6059
  ...request.slug !== void 0 ? { slug: request.slug } : {},
5870
6060
  ...request.breakoutRoomId != null ? { breakoutRoomId: request.breakoutRoomId } : {},
5871
- ...request.observe === true ? { observe: true } : {}
6061
+ ...request.observe === true ? { observe: true } : {},
6062
+ ...tabId !== null ? { tabId } : {},
6063
+ ...request.secondDevice !== void 0 ? { secondDevice: request.secondDevice } : {}
5872
6064
  });
6065
+ if (token.secondDeviceChoice != null) {
6066
+ store.patch({ secondDevice: { rule: token.secondDeviceChoice }, localIdentity: token.identity });
6067
+ store.dispatch({ type: "second_device" });
6068
+ report("info", `meet: already in this meeting elsewhere \u2014 offering the choice (${token.secondDeviceChoice})`);
6069
+ return;
6070
+ }
6071
+ if (store.snapshot().secondDevice !== null) store.patch({ secondDevice: null });
6072
+ if (request.secondDevice === "switch_here" && lastRequest !== null) {
6073
+ const { secondDevice: _answered, ...rest } = lastRequest;
6074
+ lastRequest = rest;
6075
+ }
6076
+ options.tabClaim?.inRoom(String(request.roomName), String(token.identity));
5873
6077
  current = token;
5874
6078
  lastServerUrl = token.serverUrl;
5875
6079
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
@@ -5934,7 +6138,7 @@ function createRoomEngine(options) {
5934
6138
  if (request.mode === "video") {
5935
6139
  await enableTrack(created, "camera", true);
5936
6140
  }
5937
- if (request.microphone !== false) {
6141
+ if (request.microphone !== false && current?.connectionKind !== "companion") {
5938
6142
  await enableTrack(created, "microphone", true);
5939
6143
  }
5940
6144
  media.onConnected();
@@ -5956,7 +6160,8 @@ function createRoomEngine(options) {
5956
6160
  }
5957
6161
  async function publish(topic, body, to) {
5958
6162
  if (driver === null) return;
5959
- await driver.publishData(topic, JSON.stringify(body), to);
6163
+ const addresses = to === void 0 ? void 0 : to.flatMap((who) => connectionsOf(who));
6164
+ await driver.publishData(topic, JSON.stringify(body), addresses);
5960
6165
  }
5961
6166
  async function applyEffects(target, effects) {
5962
6167
  try {
@@ -5994,8 +6199,19 @@ function createRoomEngine(options) {
5994
6199
  liveRoster = list;
5995
6200
  const merged = mergeGhosts(list, ghosts, at);
5996
6201
  ghosts = merged.kept;
6202
+ const people = /* @__PURE__ */ new Map();
6203
+ const connections = /* @__PURE__ */ new Map();
6204
+ for (const entry of merged.roster) {
6205
+ const person = personOf(entry.identity);
6206
+ connections.set(person, [...connections.get(person) ?? [], entry.identity]);
6207
+ 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);
6208
+ const held = people.get(person);
6209
+ if (held === void 0 || rank > held.rank) people.set(person, { entry, rank });
6210
+ }
6211
+ personConnections = connections;
6212
+ mainConnection = new Map([...people].map(([person, { entry }]) => [person, entry.identity]));
5997
6213
  store.setParticipants(
5998
- merged.roster.map((entry) => {
6214
+ [...people.values()].map(({ entry }) => {
5999
6215
  const projected = projectParticipant(entry, joinedAt);
6000
6216
  return entry.reconnecting === true ? { ...projected, reconnecting: true, quality: "lost" } : projected;
6001
6217
  })
@@ -6172,7 +6388,13 @@ function createRoomEngine(options) {
6172
6388
  },
6173
6389
  async setScreenShareEnabled(enabled) {
6174
6390
  if (driver === null) return;
6175
- if (enabled && driver.supportsScreenShare?.() === false) {
6391
+ let unsupported = false;
6392
+ try {
6393
+ unsupported = enabled && driver.supportsScreenShare?.() === false;
6394
+ } catch {
6395
+ unsupported = false;
6396
+ }
6397
+ if (unsupported) {
6176
6398
  store.patch({ share: { state: "refused", reason: "unsupported", remedy: null, at: now() } });
6177
6399
  report("warn", "meet: this browser cannot share a screen", "Share from a desktop browser (Chrome, Edge, Firefox or Safari).");
6178
6400
  return;
@@ -6271,7 +6493,7 @@ function createRoomEngine(options) {
6271
6493
  },
6272
6494
  attach(who, kind, element) {
6273
6495
  if (driver === null) return () => void 0;
6274
- return driver.attach(who, kind, element);
6496
+ return driver.attach(mainConnection.get(personOf(who)) ?? who, kind, element);
6275
6497
  },
6276
6498
  token: () => current,
6277
6499
  async dispose() {
@@ -7005,6 +7227,12 @@ function defaultDiagnostics(event) {
7005
7227
  else if (event.level === "warn") console.warn(line);
7006
7228
  else console.info(line);
7007
7229
  }
7230
+ var claimedTab = null;
7231
+ function pageTabClaim() {
7232
+ if (typeof window === "undefined") return void 0;
7233
+ claimedTab ??= createTabClaim();
7234
+ return claimedTab;
7235
+ }
7008
7236
  function deviceId() {
7009
7237
  const storage = globalThis.localStorage;
7010
7238
  const key = "mx.meet.device";
@@ -7122,13 +7350,15 @@ function MeetRuntime(props) {
7122
7350
  store,
7123
7351
  tokens,
7124
7352
  identity,
7353
+ // S7: this page's tab id (one per page, claimed at load) — the LiveKit identity's tab half.
7354
+ tabClaim: pageTabClaim(),
7125
7355
  resolveMeeting: (slug) => repository.meetingBySlug(slug),
7126
7356
  driverFactory: (handlers) => (driverRef.current ?? createLiveKitDriver)(handlers),
7127
7357
  onDiagnostic: report,
7128
7358
  effectsStorage,
7129
7359
  resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null,
7130
7360
  // §3.4: on every LiveKit disconnect, ask the server what happened to this connection.
7131
- readExit: (connection) => meetings.readExit(connection),
7361
+ readExit: (connection, pass) => meetings.readExit(connection, pass),
7132
7362
  // S6b: the connection supervisor's ports — the device's network, whether OUR servers (the
7133
7363
  // API and the media server) answer, and the per-tab reload-after-offline marker.
7134
7364
  connection: {
@@ -8197,6 +8427,7 @@ function useRoom() {
8197
8427
  phase: snapshot?.phase ?? "resolving",
8198
8428
  participants: snapshot?.participants ?? [],
8199
8429
  failure: snapshot?.failure ?? null,
8430
+ secondDevice: snapshot?.secondDevice ?? null,
8200
8431
  local,
8201
8432
  canShareScreen: engine?.supportsScreenShare() ?? true,
8202
8433
  join: (0, import_react4.useCallback)(
@@ -8342,6 +8573,7 @@ function useLobby() {
8342
8573
  const session = host?.meetings ?? null;
8343
8574
  return {
8344
8575
  waiting: (snapshot?.lobby ?? []).filter((entry) => entry.state === "knocking"),
8576
+ declined: (snapshot?.lobby ?? []).filter((entry) => entry.state === "denied"),
8345
8577
  // No runtime means no host powers — `<LobbyPanel>` renders nothing rather
8346
8578
  // than an admit button that would throw.
8347
8579
  canAdmit: session !== null && isHost,
@@ -8363,6 +8595,37 @@ function useLobby() {
8363
8595
  )
8364
8596
  };
8365
8597
  }
8598
+ function useCountdown(untilMs) {
8599
+ const [now, setNow] = (0, import_react4.useState)(() => Date.now());
8600
+ const active = typeof untilMs === "number" && untilMs > now;
8601
+ (0, import_react4.useEffect)(() => {
8602
+ if (!active) return void 0;
8603
+ setNow(Date.now());
8604
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
8605
+ return () => clearInterval(timer);
8606
+ }, [active, untilMs]);
8607
+ if (typeof untilMs !== "number") return null;
8608
+ return Math.max(0, untilMs - now);
8609
+ }
8610
+ function useKnockNotice() {
8611
+ const lobby = useLobby();
8612
+ const [dismissed, setDismissed] = (0, import_react4.useState)([]);
8613
+ const keyOf = (entry) => `${entry.identity}@${entry.knockedAt}`;
8614
+ const open = lobby.canAdmit ? lobby.waiting.filter((entry) => !dismissed.includes(keyOf(entry))) : [];
8615
+ const knocker = open[0] ?? null;
8616
+ return {
8617
+ knocker,
8618
+ others: Math.max(0, open.length - 1),
8619
+ admit: (0, import_react4.useCallback)(async () => {
8620
+ if (knocker === null) return;
8621
+ setDismissed((list) => [...list, keyOf(knocker)]);
8622
+ await lobby.admit(knocker);
8623
+ }, [knocker, lobby.admit]),
8624
+ dismiss: (0, import_react4.useCallback)(() => {
8625
+ if (knocker !== null) setDismissed((list) => [...list, keyOf(knocker)]);
8626
+ }, [knocker])
8627
+ };
8628
+ }
8366
8629
  function useWaiting() {
8367
8630
  const host = useMeetHost();
8368
8631
  const snapshot = useMeetSnapshot();
@@ -11112,6 +11375,7 @@ function ControlBar({ onLeave }) {
11112
11375
  }
11113
11376
  ) }) }) : null,
11114
11377
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BreakoutBanner, {}),
11378
+ panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(KnockNotice, { onView: () => setPanel("people") }) : null,
11115
11379
  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
11380
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__controls", children: [
11117
11381
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -11442,10 +11706,21 @@ function MediaNotices() {
11442
11706
  const name = (kind) => kind === "microphone" ? "Microphone" : "Camera";
11443
11707
  if (chips.length === 0 && region.audioOutput !== "blocked" && notice === null && offer === null) return null;
11444
11708
  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,
11709
+ region.audioOutput === "blocked" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
11710
+ import_design_system.ErrorBox,
11711
+ {
11712
+ as: "div",
11713
+ className: "mx-meet__media-pill",
11714
+ "data-meet-notice": "audio-blocked",
11715
+ message: "Sound is paused in this tab",
11716
+ operation: "Enable meeting sound",
11717
+ origin: "meet/MediaNotices",
11718
+ children: [
11719
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "Sound is paused in this tab" }),
11720
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void enableAudio(), children: "Enable sound" })
11721
+ ]
11722
+ }
11723
+ ) : null,
11449
11724
  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
11725
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: deviceChipLabel(kind, reason) }),
11451
11726
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -12245,9 +12520,68 @@ function useMicrophoneStream(deviceId2) {
12245
12520
  }, [deviceId2]);
12246
12521
  return stream;
12247
12522
  }
12523
+ function KnockNotice({ onView }) {
12524
+ const { knocker, others, admit, dismiss } = useKnockNotice();
12525
+ if (knocker === null) return null;
12526
+ 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: [
12527
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
12528
+ knocker.displayName,
12529
+ " wants to join",
12530
+ others > 0 ? ` (+${others} more)` : ""
12531
+ ] }),
12532
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => void admit().catch(() => void 0), children: "Admit" }),
12533
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12534
+ "button",
12535
+ {
12536
+ type: "button",
12537
+ className: "mx-meet__link",
12538
+ onClick: () => {
12539
+ dismiss();
12540
+ onView();
12541
+ },
12542
+ children: "View"
12543
+ }
12544
+ ),
12545
+ /* @__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 }) })
12546
+ ] });
12547
+ }
12248
12548
  function LobbyPanel() {
12249
12549
  const lobby = useLobby();
12250
- if (!lobby.canAdmit || lobby.waiting.length === 0) return null;
12550
+ if (!lobby.canAdmit || lobby.waiting.length === 0 && lobby.declined.length === 0) return null;
12551
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
12552
+ lobby.waiting.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(WaitingList, { lobby }) : null,
12553
+ lobby.declined.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DeclinedList, { lobby }) : null
12554
+ ] });
12555
+ }
12556
+ function DeclinedList({ lobby }) {
12557
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("section", { className: "mx-meet__lobby", "aria-label": "Declined requests", "data-meet-declined": "", children: [
12558
+ /* @__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" }) }),
12559
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className: "mx-meet__lobby-list", children: lobby.declined.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
12560
+ "li",
12561
+ {
12562
+ className: "mx-meet__lobby-row",
12563
+ "data-meet-declined-entry": "",
12564
+ "data-meet-participant-name": entry.displayName,
12565
+ children: [
12566
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Avatar, { name: entry.displayName, url: entry.avatarUrl, size: 32 }),
12567
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__person-name", children: entry.displayName }),
12568
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12569
+ "button",
12570
+ {
12571
+ type: "button",
12572
+ className: "mx-meet__link",
12573
+ "aria-label": `Let ${entry.displayName} ask again`,
12574
+ onClick: () => void lobby.undeny(entry),
12575
+ children: "Let them ask again"
12576
+ }
12577
+ )
12578
+ ]
12579
+ },
12580
+ entry.identity
12581
+ )) })
12582
+ ] });
12583
+ }
12584
+ function WaitingList({ lobby }) {
12251
12585
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("section", { className: "mx-meet__lobby", "aria-label": "Waiting room", children: [
12252
12586
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("header", { className: "mx-meet__lobby-head", children: [
12253
12587
  /* @__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 +12845,21 @@ function PreJoinBody({
12511
12845
  (0, import_react10.useEffect)(() => {
12512
12846
  void host.devices.refresh();
12513
12847
  }, [host]);
12848
+ const joinWith = (secondDevice) => {
12849
+ setJoining(true);
12850
+ preview.release();
12851
+ void room.join({
12852
+ roomName,
12853
+ // An observer publishes nothing: no camera, no microphone (MD-16). A companion
12854
+ // ("join here too") never opens the microphone (S7).
12855
+ mode: observe2 ? "audio" : mode,
12856
+ microphone: observe2 || secondDevice === "join_too" ? false : microphone,
12857
+ ...meetingId !== void 0 ? { meetingId } : {},
12858
+ ...slug !== void 0 ? { slug } : {},
12859
+ ...observe2 ? { observe: true } : {},
12860
+ ...secondDevice !== void 0 ? { secondDevice } : {}
12861
+ }).then(() => onJoined?.()).catch(() => void 0).finally(() => setJoining(false));
12862
+ };
12514
12863
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__prejoin", children: [
12515
12864
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__preview", children: [
12516
12865
  mode === "video" && preview.preview !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -12595,27 +12944,34 @@ function PreJoinBody({
12595
12944
  ),
12596
12945
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Label, { htmlFor: "mx-meet-join-mic", children: "Join with microphone" })
12597
12946
  ] }),
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
- }
12947
+ 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" }) : (
12948
+ // S7: ALREADY IN THIS MEETING on another tab or device — the meeting's choice
12949
+ // (Meet: Switch here / Join here too; Teams: Transfer here / Add this device).
12950
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__second-device", "data-meet-region": "second-device", children: [
12951
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__hint", children: "You're in this meeting on another device" }),
12952
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12953
+ import_design_system.Button,
12954
+ {
12955
+ type: "button",
12956
+ className: "mx-meet__join",
12957
+ "data-meet-control": "switch-here",
12958
+ disabled: joining,
12959
+ onClick: () => joinWith("switch_here"),
12960
+ children: room.secondDevice.rule === "ask_transfer_or_add" ? "Transfer here" : "Switch here"
12961
+ }
12962
+ ),
12963
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12964
+ import_design_system.Button,
12965
+ {
12966
+ type: "button",
12967
+ variant: "outline",
12968
+ "data-meet-control": "join-too",
12969
+ disabled: joining,
12970
+ onClick: () => joinWith("join_too"),
12971
+ children: room.secondDevice.rule === "ask_transfer_or_add" ? "Add this device" : "Join here too"
12972
+ }
12973
+ )
12974
+ ] })
12619
12975
  )
12620
12976
  ] }),
12621
12977
  room.failure !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__error", children: [
@@ -12813,16 +13169,21 @@ var REFUSED_TITLES = {
12813
13169
  not_invited: "You aren't invited to this meeting",
12814
13170
  locked: "This meeting is locked",
12815
13171
  removed: "You can't rejoin this meeting",
12816
- lobby_rejects: "You can't ask to join this meeting",
13172
+ lobby_rejects: "The host declined your request",
12817
13173
  meeting_full: "This meeting is full",
12818
13174
  platform_capacity: "Meetings are busy right now",
12819
13175
  verification_required: "Verify your account to join",
12820
13176
  registration_required: "Register to join this meeting"
12821
13177
  };
13178
+ function clockText(ms2) {
13179
+ const total = Math.ceil(ms2 / 1e3);
13180
+ return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`;
13181
+ }
12822
13182
  function ExitScreen({ onLeave }) {
12823
13183
  const { phase, reason, host, snapshot, actions, call, connection } = useMeetCore();
12824
13184
  const [busy, setBusy] = (0, import_react10.useState)(false);
12825
13185
  const failure = snapshot?.failure ?? null;
13186
+ const retryLeft = useCountdown(phase === "refused:lobby_rejects" ? failure?.retryAt ?? null : null);
12826
13187
  const rejoin = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12827
13188
  import_design_system.Button,
12828
13189
  {
@@ -12865,6 +13226,33 @@ function ExitScreen({ onLeave }) {
12865
13226
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Button, { type: "button", variant: "outline", onClick: actions.retry, children: "Try again" }),
12866
13227
  home
12867
13228
  ] });
13229
+ if (code === "lobby_rejects") {
13230
+ if (retryLeft !== null && retryLeft > 0) {
13231
+ line = `You can ask again in ${clockText(retryLeft)}`;
13232
+ notice = "reknock-cooldown";
13233
+ actionsNode = home;
13234
+ } else if (retryLeft === 0) {
13235
+ line = "You can ask to join again";
13236
+ actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
13237
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
13238
+ import_design_system.Button,
13239
+ {
13240
+ type: "button",
13241
+ disabled: busy,
13242
+ onClick: () => {
13243
+ setBusy(true);
13244
+ void actions.rejoin().catch(() => void 0).finally(() => setBusy(false));
13245
+ },
13246
+ children: "Ask to join again"
13247
+ }
13248
+ ),
13249
+ home
13250
+ ] });
13251
+ } else {
13252
+ line = "Ask the host to let you in, then open the link again";
13253
+ actionsNode = home;
13254
+ }
13255
+ }
12868
13256
  } else {
12869
13257
  switch (phase) {
12870
13258
  case "left":
@@ -12925,7 +13313,7 @@ function ExitScreen({ onLeave }) {
12925
13313
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Avatar, { name: host?.identity.displayName ?? "", url: host?.identity.avatarUrl ?? null, size: 72 }),
12926
13314
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h1", { className: "mx-meet__prejoin-title", children: title }),
12927
13315
  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,
13316
+ 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
13317
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__waiting-actions", children: actionsNode })
12930
13318
  ] });
12931
13319
  }
@@ -12934,6 +13322,7 @@ function WaitingRoom({ host, onLeave }) {
12934
13322
  const { phaseNotice } = useMeetCore();
12935
13323
  const [busy, setBusy] = (0, import_react10.useState)(false);
12936
13324
  const admission = waiting?.admission ?? "knocking";
13325
+ const reknockLeft = useCountdown(admission === "denied" ? waiting?.reknockAt ?? null : null);
12937
13326
  const run = (action) => () => {
12938
13327
  setBusy(true);
12939
13328
  void action().catch(() => void 0).finally(() => setBusy(false));
@@ -12941,6 +13330,7 @@ function WaitingRoom({ host, onLeave }) {
12941
13330
  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
13331
  let title;
12943
13332
  let line = null;
13333
+ let cooldownNotice = false;
12944
13334
  let actions;
12945
13335
  switch (admission) {
12946
13336
  case "waiting_host":
@@ -12949,8 +13339,12 @@ function WaitingRoom({ host, onLeave }) {
12949
13339
  break;
12950
13340
  case "denied":
12951
13341
  title = "The host declined your request";
13342
+ if (reknockLeft !== null && reknockLeft > 0) {
13343
+ line = `You can ask again in ${clockText(reknockLeft)}`;
13344
+ cooldownNotice = true;
13345
+ }
12952
13346
  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,
13347
+ 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
13348
  home
12955
13349
  ] });
12956
13350
  break;
@@ -12981,7 +13375,15 @@ function WaitingRoom({ host, onLeave }) {
12981
13375
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 72 }),
12982
13376
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h1", { className: "mx-meet__prejoin-title", children: title }),
12983
13377
  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,
13378
+ line !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
13379
+ "p",
13380
+ {
13381
+ className: "mx-meet__empty-body",
13382
+ role: "status",
13383
+ "data-meet-notice": cooldownNotice ? "reknock-cooldown" : "host-absent",
13384
+ children: line
13385
+ }
13386
+ ) : null,
12985
13387
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__waiting-actions", children: actions })
12986
13388
  ] });
12987
13389
  }