@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.js CHANGED
@@ -1,6 +1,139 @@
1
1
  "use client";
2
2
  "use client";
3
3
 
4
+ // src/core/tab-claim.ts
5
+ var TAB_ID_KEY = "mx.meet.tab";
6
+ var TAB_ANSWER_MS = 250;
7
+ function validTabId(value) {
8
+ return value !== null && /^[A-Za-z0-9_-]{4,64}$/.test(value);
9
+ }
10
+ function wait(ms2) {
11
+ return new Promise((resolve) => setTimeout(resolve, ms2));
12
+ }
13
+ function createTabClaim(platform = webTabPlatform()) {
14
+ let mine = null;
15
+ let current = null;
16
+ const self = platform.randomId();
17
+ const unsubscribe = platform.bus?.subscribe((message) => {
18
+ if (message.kind === "tab-alive?" && mine !== null && message.tabId === mine && message.from !== self) {
19
+ platform.bus?.post({ kind: "tab-alive", tabId: mine });
20
+ } else if (message.kind === "who-is-in?" && current !== null && mine !== null && message.from !== self && message.room === current.room && message.person === current.person) {
21
+ platform.bus?.post({ kind: "in-room", room: current.room, person: current.person, tabId: mine, to: message.from });
22
+ }
23
+ }) ?? (() => {
24
+ });
25
+ async function someoneAnswersFor(tabId2) {
26
+ const bus = platform.bus;
27
+ if (bus === null) return false;
28
+ let answered = false;
29
+ const stop = bus.subscribe((message) => {
30
+ if (message.kind === "tab-alive" && message.tabId === tabId2) answered = true;
31
+ });
32
+ bus.post({ kind: "tab-alive?", tabId: tabId2, from: self });
33
+ await wait(TAB_ANSWER_MS);
34
+ stop();
35
+ return answered;
36
+ }
37
+ async function claim() {
38
+ const stored = platform.storage?.get(TAB_ID_KEY) ?? null;
39
+ let candidate = validTabId(stored) ? stored : platform.randomId();
40
+ for (let attempt = 0; attempt < 4; attempt++) {
41
+ const held = await platform.tryLock(`${TAB_ID_KEY}:${candidate}`);
42
+ if (held) break;
43
+ if (!await someoneAnswersFor(candidate)) break;
44
+ candidate = platform.randomId();
45
+ }
46
+ mine = candidate;
47
+ platform.storage?.set(TAB_ID_KEY, candidate);
48
+ return candidate;
49
+ }
50
+ const tabId = claim();
51
+ return {
52
+ tabId,
53
+ inRoom(room, person) {
54
+ current = { room, person };
55
+ },
56
+ left() {
57
+ current = null;
58
+ },
59
+ async presentElsewhere(room, person) {
60
+ const bus = platform.bus;
61
+ if (bus === null) return false;
62
+ let seen = false;
63
+ const stop = bus.subscribe((message) => {
64
+ if (message.kind === "in-room" && message.to === self && message.room === room && message.person === person) {
65
+ seen = true;
66
+ }
67
+ });
68
+ bus.post({ kind: "who-is-in?", room, person, from: self });
69
+ await wait(TAB_ANSWER_MS);
70
+ stop();
71
+ return seen;
72
+ },
73
+ dispose() {
74
+ unsubscribe();
75
+ }
76
+ };
77
+ }
78
+ function randomTabId() {
79
+ const crypto = globalThis.crypto;
80
+ const raw = crypto?.randomUUID?.() ?? `${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
81
+ return raw.replace(/[^A-Za-z0-9]/g, "").slice(0, 16);
82
+ }
83
+ function webTabPlatform() {
84
+ const g = globalThis;
85
+ const session = g.sessionStorage;
86
+ const storage = session === void 0 ? null : {
87
+ get(key) {
88
+ try {
89
+ return session.getItem(key);
90
+ } catch {
91
+ return null;
92
+ }
93
+ },
94
+ set(key, value) {
95
+ try {
96
+ session.setItem(key, value);
97
+ } catch {
98
+ }
99
+ }
100
+ };
101
+ const channel = g.BroadcastChannel !== void 0 ? new g.BroadcastChannel("mx.meet.tabs") : null;
102
+ const bus = channel === null ? null : {
103
+ post(message) {
104
+ channel.postMessage(message);
105
+ },
106
+ subscribe(listener) {
107
+ const handler = (event) => {
108
+ const data = event.data;
109
+ if (data !== null && typeof data === "object" && typeof data.kind === "string") listener(data);
110
+ };
111
+ channel.addEventListener("message", handler);
112
+ return () => channel.removeEventListener("message", handler);
113
+ }
114
+ };
115
+ return {
116
+ storage,
117
+ bus,
118
+ randomId: randomTabId,
119
+ tryLock(name) {
120
+ const locks = g.navigator?.locks;
121
+ if (locks === void 0) return Promise.resolve(false);
122
+ return new Promise((resolve) => {
123
+ locks.request(name, { ifAvailable: true }, (lock) => {
124
+ if (lock === null) {
125
+ resolve(false);
126
+ return Promise.resolve();
127
+ }
128
+ resolve(true);
129
+ return new Promise(() => {
130
+ });
131
+ }).catch(() => resolve(false));
132
+ });
133
+ }
134
+ };
135
+ }
136
+
4
137
  // src/core/connection-supervisor.ts
5
138
  var DEFAULT_CONNECTION_POLICY = {
6
139
  bannerDelayMs: 2e3,
@@ -380,12 +513,18 @@ var MeetError = class extends Error {
380
513
  * renders `refused:<reason>` (or `ended`) from it — never from the HTTP status.
381
514
  */
382
515
  reason;
383
- constructor(code, message, remedy, cause, reason) {
516
+ /**
517
+ * A refusal that lifts by itself says when: whole seconds from the moment the server answered
518
+ * (a denial's cooldown, `reknock_cooldown_seconds`). Null for every refusal that does not.
519
+ */
520
+ retryAfterSeconds;
521
+ constructor(code, message, remedy, cause, reason, retryAfterSeconds) {
384
522
  super(message);
385
523
  this.name = "MeetError";
386
524
  this.code = code;
387
525
  this.remedy = remedy;
388
526
  this.reason = reason ?? null;
527
+ this.retryAfterSeconds = retryAfterSeconds ?? null;
389
528
  if (cause !== void 0) this.cause = cause;
390
529
  }
391
530
  /** True when retrying after the host re-establishes a session is the fix. */
@@ -719,7 +858,8 @@ function parseRoomToken(body, roomName) {
719
858
  const serverUrl = readString(payload, "server_url") ?? readString(payload, "serverUrl");
720
859
  const identity = readString(payload, "identity");
721
860
  const expiresAt = readString(payload, "expires_at") ?? readString(payload, "expiresAt");
722
- if (token === null || serverUrl === null || identity === null) {
861
+ const secondDeviceChoice = readString(payload, "second_device_choice");
862
+ if (token === null && secondDeviceChoice === null || serverUrl === null || identity === null) {
723
863
  throw invalidResponse(
724
864
  "meet.token",
725
865
  "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"
@@ -731,11 +871,15 @@ function parseRoomToken(body, roomName) {
731
871
  "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"
732
872
  );
733
873
  }
874
+ const personKey = readString(payload, "person_key") ?? identity;
734
875
  return {
735
- token,
876
+ token: token ?? "",
736
877
  serverUrl,
737
878
  roomName,
738
- identity,
879
+ identity: personKey,
880
+ connectionIdentity: readString(payload, "connection_identity") ?? personKey,
881
+ connectionKind: readString(payload, "connection_kind") === "companion" ? "companion" : "main",
882
+ secondDeviceChoice,
739
883
  displayName: readString(payload, "display_name") ?? readString(payload, "displayName") ?? identity,
740
884
  expiresAt,
741
885
  canPublish: readBoolean(payload, "can_publish", true),
@@ -810,7 +954,9 @@ function createTokenClient(options) {
810
954
  organization_id: input.identity.organizationId,
811
955
  display_name: input.identity.displayName,
812
956
  device_id: input.identity.deviceId,
813
- guest
957
+ guest,
958
+ ...input.tabId !== void 0 ? { tab_id: input.tabId } : {},
959
+ ...input.secondDevice !== void 0 ? { second_device: input.secondDevice } : {}
814
960
  }
815
961
  });
816
962
  } catch (error) {
@@ -827,7 +973,8 @@ function createTokenClient(options) {
827
973
  said?.message ?? "You can't join this meeting.",
828
974
  said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again.",
829
975
  void 0,
830
- said?.reason ?? null
976
+ said?.reason ?? null,
977
+ said?.retryAfterSeconds ?? null
831
978
  );
832
979
  }
833
980
  if (response.status === 404) {
@@ -885,6 +1032,10 @@ function createTokenClient(options) {
885
1032
  if (!expired(entry)) return Promise.resolve(entry.token);
886
1033
  }
887
1034
  const pending = request(input, true).then((token) => {
1035
+ if (token.secondDeviceChoice != null) {
1036
+ cache.delete(input.roomName);
1037
+ return token;
1038
+ }
888
1039
  cache.set(input.roomName, { token, mintedAt: now(), inFlight: null });
889
1040
  remember(token);
890
1041
  return token;
@@ -919,6 +1070,9 @@ function placeholder(roomName) {
919
1070
  serverUrl: "",
920
1071
  roomName,
921
1072
  identity: "",
1073
+ connectionIdentity: "",
1074
+ connectionKind: "main",
1075
+ secondDeviceChoice: null,
922
1076
  displayName: "",
923
1077
  expiresAt: (/* @__PURE__ */ new Date(0)).toISOString(),
924
1078
  canPublish: false,
@@ -943,7 +1097,9 @@ async function serverSentence(response) {
943
1097
  if (message === null) return null;
944
1098
  const remedy = typeof source.remedy === "string" && source.remedy.trim() !== "" ? source.remedy : void 0;
945
1099
  const reason = typeof source.reason === "string" && source.reason !== "" ? source.reason : null;
946
- return { message, remedy, reason };
1100
+ const retry = source.retry_after_seconds;
1101
+ const retryAfterSeconds = typeof retry === "number" && Number.isFinite(retry) && retry > 0 ? Math.ceil(retry) : null;
1102
+ return { message, remedy, reason, retryAfterSeconds };
947
1103
  } catch {
948
1104
  return null;
949
1105
  }
@@ -1394,7 +1550,10 @@ var JOINABLE_FROM = /* @__PURE__ */ new Set([
1394
1550
  "superseded",
1395
1551
  "inactive",
1396
1552
  "denied",
1397
- "knock_expired"
1553
+ "knock_expired",
1554
+ // A refused knock whose rule lets the person ask again (a cooldown that ran out): the machine
1555
+ // allows the press; the server's gate decides whether it knocks or is refused again.
1556
+ "refused:lobby_rejects"
1398
1557
  ]);
1399
1558
  var NOT_ENDABLE = /* @__PURE__ */ new Set([
1400
1559
  "ended",
@@ -1478,6 +1637,8 @@ function nextPhase(from, event) {
1478
1637
  return WAITING_PHASES.has(from) ? "joining" : null;
1479
1638
  case "connected":
1480
1639
  return from === "joining" ? "in_call" : null;
1640
+ case "second_device":
1641
+ return from === "joining" ? "prejoin" : null;
1481
1642
  case "join_failed":
1482
1643
  return from === "joining" ? refusalPhase(event.reason) : null;
1483
1644
  case "exit":
@@ -1495,7 +1656,7 @@ function nextPhase(from, event) {
1495
1656
  case "call_outcome":
1496
1657
  return RINGING.has(from) ? event.outcome : null;
1497
1658
  case "reset":
1498
- return LIVE_PHASES.has(from) ? null : "resolving";
1659
+ return LIVE_PHASES.has(from) && event.teardown !== true ? null : "resolving";
1499
1660
  }
1500
1661
  }
1501
1662
  function resolveExit(input) {
@@ -1801,8 +1962,8 @@ function createCallCenter(options) {
1801
1962
  "A late accept must not resurrect a settled call. Close the ringing UI; the caller has already moved on."
1802
1963
  );
1803
1964
  }
1804
- if (options.store.snapshot().phase === "ringing_in") options.store.dispatch({ type: "call_answered" });
1805
1965
  const settled = await repository.settleCallInvite(id, "connecting", null);
1966
+ if (options.store.snapshot().phase === "ringing_in") options.store.dispatch({ type: "call_answered" });
1806
1967
  options.store.patch({ call: { invite: settled, direction: "incoming" } });
1807
1968
  settleLocal(settled);
1808
1969
  channel?.send(MEET_EVENTS.settled, settled);
@@ -2013,11 +2174,17 @@ function createDeviceManager(options) {
2013
2174
  }
2014
2175
 
2015
2176
  // src/core/names.ts
2177
+ var CONNECTION_SEPARATOR = "~";
2178
+ function personOf(identity) {
2179
+ const value = String(identity ?? "").trim();
2180
+ const cut = value.indexOf(CONNECTION_SEPARATOR);
2181
+ return cut === -1 ? value : value.slice(0, cut);
2182
+ }
2016
2183
  function isGuestIdentity(identity) {
2017
- return String(identity).startsWith("guest:");
2184
+ return personOf(identity).startsWith("guest:");
2018
2185
  }
2019
2186
  function isAgentIdentity(identity) {
2020
- return String(identity).startsWith("agent:");
2187
+ return personOf(identity).startsWith("agent:");
2021
2188
  }
2022
2189
  var NOTE_TAKER_PRODUCT_NAME = "Matrx note-taker";
2023
2190
  var AGENT_PRODUCT_NAME = "Matrx agent";
@@ -3617,6 +3784,7 @@ var EMPTY = {
3617
3784
  waiting: null,
3618
3785
  meeting: null,
3619
3786
  localIdentity: null,
3787
+ secondDevice: null,
3620
3788
  grants: NO_GRANTS,
3621
3789
  participants: [],
3622
3790
  activeSpeakers: [],
@@ -3867,7 +4035,8 @@ function createMeetStore(initial = {}) {
3867
4035
  commit({ ...state, media: { ...state.media, [kind]: next } });
3868
4036
  },
3869
4037
  reset() {
3870
- commit({ ...EMPTY, effects: state.effects });
4038
+ const phase = nextPhase(state.phase, { type: "reset", teardown: true }) ?? EMPTY.phase;
4039
+ commit({ ...EMPTY, effects: state.effects, phase, reason: reasonOf(phase), phaseNotice: null });
3871
4040
  }
3872
4041
  };
3873
4042
  }
@@ -3900,6 +4069,28 @@ function lobbyEntryFrom(attendee, now = Date.now()) {
3900
4069
  decidedBy: null
3901
4070
  };
3902
4071
  }
4072
+ function deniedEntryFrom(attendee) {
4073
+ if (attendee.admissionState !== "denied") return null;
4074
+ return {
4075
+ identity: attendee.identity,
4076
+ displayName: attendee.displayName,
4077
+ avatarUrl: attendee.avatarUrl,
4078
+ userId: attendee.userId,
4079
+ state: "denied",
4080
+ knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4081
+ knockExpiresAt: null,
4082
+ decidedAt: null,
4083
+ decidedBy: null
4084
+ };
4085
+ }
4086
+ function applyDoorRow(store, attendee) {
4087
+ const entry = lobbyEntryFrom(attendee) ?? deniedEntryFrom(attendee);
4088
+ if (entry !== null) {
4089
+ store.upsertLobby(entry);
4090
+ return;
4091
+ }
4092
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4093
+ }
3903
4094
  function decidedState(state) {
3904
4095
  return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
3905
4096
  }
@@ -4034,6 +4225,7 @@ function createMeetingSession(options) {
4034
4225
  admission,
4035
4226
  admittersPresent: read.admitters_present ?? null,
4036
4227
  reknockAllowed: read.reknock_allowed ?? true,
4228
+ reknockAt: typeof read.reknock_in_seconds === "number" && read.reknock_in_seconds > 0 ? Date.now() + read.reknock_in_seconds * 1e3 : null,
4037
4229
  knockExpiresAt: read.knock_expires_at ?? null,
4038
4230
  pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
4039
4231
  liveVersion: read.live_version ?? 0
@@ -4116,11 +4308,7 @@ function createMeetingSession(options) {
4116
4308
  try {
4117
4309
  const attendees = await repository.attendees(meeting.id);
4118
4310
  store.learn(attendees);
4119
- for (const attendee of attendees) {
4120
- const entry = lobbyEntryFrom(attendee);
4121
- if (entry !== null) store.upsertLobby(entry);
4122
- else store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4123
- }
4311
+ for (const attendee of attendees) applyDoorRow(store, attendee);
4124
4312
  } catch {
4125
4313
  }
4126
4314
  }
@@ -4202,12 +4390,7 @@ function createMeetingSession(options) {
4202
4390
  if (row === null) return;
4203
4391
  const attendee = repository.projectAttendee(row);
4204
4392
  store.learn([attendee]);
4205
- const entry = lobbyEntryFrom(attendee);
4206
- if (entry !== null) {
4207
- store.upsertLobby(entry);
4208
- return;
4209
- }
4210
- store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4393
+ applyDoorRow(store, attendee);
4211
4394
  }
4212
4395
  },
4213
4396
  {
@@ -4254,14 +4437,7 @@ function createMeetingSession(options) {
4254
4437
  store.setRecording(latest.state, latest.startedBy, "record", latest.id);
4255
4438
  }
4256
4439
  store.learn(attendees);
4257
- for (const attendee of attendees) {
4258
- const entry = lobbyEntryFrom(attendee);
4259
- if (entry !== null) {
4260
- store.upsertLobby(entry);
4261
- } else {
4262
- store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4263
- }
4264
- }
4440
+ for (const attendee of attendees) applyDoorRow(store, attendee);
4265
4441
  }
4266
4442
  });
4267
4443
  meetingChannel.requestBackfill("manual");
@@ -4313,9 +4489,9 @@ function createMeetingSession(options) {
4313
4489
  },
4314
4490
  stopWaiting,
4315
4491
  readState: readState2,
4316
- async readExit(connection) {
4492
+ async readExit(connection, held) {
4317
4493
  const meeting = store.snapshot().meeting;
4318
- const pass = options.resolvePass?.() ?? null;
4494
+ const pass = held ?? options.resolvePass?.() ?? null;
4319
4495
  if (meeting === null || pass === null) return void 0;
4320
4496
  try {
4321
4497
  const response = await options.api.request({
@@ -5165,6 +5341,7 @@ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
5165
5341
  }
5166
5342
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
5167
5343
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
5344
+ var CONNECTION_KIND_ATTRIBUTE = "matrx.participant.connection_kind";
5168
5345
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
5169
5346
  var KNOWN_STANDINGS = ["member", "invited", "outsider", "guest"];
5170
5347
  function parseStanding(value) {
@@ -5188,7 +5365,8 @@ function projectParticipant(driver, joinedAt) {
5188
5365
  }
5189
5366
  const userId = typeof metadata.user_id === "string" ? asUserId(metadata.user_id) : null;
5190
5367
  return {
5191
- identity: asParticipantIdentity(driver.identity),
5368
+ // S7: a tile is a PERSON; LiveKit names the tab's connection (`<person>~<tab>`).
5369
+ identity: personOf(driver.identity),
5192
5370
  userId,
5193
5371
  // 🚨 A RAW LIVEKIT IDENTITY IS NEVER A NAME (MRI-A18). Through 0.4.12 this
5194
5372
  // line read `driver.name.length > 0 ? driver.name : driver.identity`, and
@@ -5235,6 +5413,11 @@ function createRoomEngine(options) {
5235
5413
  let ghosts = /* @__PURE__ */ new Map();
5236
5414
  let ghostTimer = null;
5237
5415
  let liveRoster = [];
5416
+ let personConnections = /* @__PURE__ */ new Map();
5417
+ let mainConnection = /* @__PURE__ */ new Map();
5418
+ function connectionsOf(who) {
5419
+ return personConnections.get(personOf(who)) ?? [who];
5420
+ }
5238
5421
  const departures = /* @__PURE__ */ new Map();
5239
5422
  function requestAsNow() {
5240
5423
  if (lastRequest === null) return null;
@@ -5299,7 +5482,12 @@ function createRoomEngine(options) {
5299
5482
  function failJoin(error, operation) {
5300
5483
  const normalized = normalizeMeetError(error, operation);
5301
5484
  store.patch({
5302
- failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
5485
+ failure: {
5486
+ message: normalized.message,
5487
+ remedy: normalized.remedy,
5488
+ reason: normalized.reason,
5489
+ retryAt: normalized.retryAfterSeconds === null ? null : now() + normalized.retryAfterSeconds * 1e3
5490
+ }
5303
5491
  });
5304
5492
  store.dispatch({ type: "join_failed", reason: normalized.reason ?? null });
5305
5493
  report(
@@ -5371,10 +5559,10 @@ function createRoomEngine(options) {
5371
5559
  if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
5372
5560
  },
5373
5561
  activeSpeakers(identities) {
5374
- store.setSpeaking(identities.map(asParticipantIdentity));
5562
+ store.setSpeaking([...new Set(identities.map((who) => personOf(who)))]);
5375
5563
  },
5376
5564
  quality(who, value) {
5377
- store.setQuality(asParticipantIdentity(who), value);
5565
+ store.setQuality(personOf(who), value);
5378
5566
  },
5379
5567
  connectionState(state) {
5380
5568
  supervisor.driverState(state);
@@ -5394,7 +5582,7 @@ function createRoomEngine(options) {
5394
5582
  );
5395
5583
  return;
5396
5584
  }
5397
- const speakerIdentity = asParticipantIdentity(event.speakerIdentity);
5585
+ const speakerIdentity = personOf(event.speakerIdentity);
5398
5586
  const speaker = snapshot.participants.find(
5399
5587
  (participant) => participant.identity === speakerIdentity
5400
5588
  );
@@ -5468,7 +5656,7 @@ function createRoomEngine(options) {
5468
5656
  );
5469
5657
  return;
5470
5658
  }
5471
- const sender = asParticipantIdentity(from ?? "");
5659
+ const sender = personOf(from ?? "");
5472
5660
  switch (topic) {
5473
5661
  case DATA_TOPICS.hand:
5474
5662
  store.setHand(
@@ -5510,7 +5698,8 @@ function createRoomEngine(options) {
5510
5698
  }
5511
5699
  };
5512
5700
  async function resolveExitAndApply(hint) {
5513
- const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
5701
+ const connection = current?.connectionIdentity ?? current?.identity ?? store.snapshot().localIdentity ?? null;
5702
+ const heldPass = current?.meetingPass ?? null;
5514
5703
  lastRequest = requestAsNow();
5515
5704
  await dropRoom();
5516
5705
  let server;
@@ -5518,7 +5707,7 @@ function createRoomEngine(options) {
5518
5707
  server = void 0;
5519
5708
  } else {
5520
5709
  try {
5521
- server = await options.readExit(connection);
5710
+ server = await options.readExit(connection, heldPass);
5522
5711
  } catch {
5523
5712
  server = null;
5524
5713
  }
@@ -5548,14 +5737,29 @@ function createRoomEngine(options) {
5548
5737
  }
5549
5738
  async function connect(request) {
5550
5739
  store.patch({ failure: null, waiting: null, roomName: request.roomName, share: NO_SHARE, connection: "stable" });
5740
+ const tabId = options.tabClaim !== void 0 ? await options.tabClaim.tabId : null;
5551
5741
  const token = await tokens.get({
5552
5742
  roomName: request.roomName,
5553
5743
  identity,
5554
5744
  ...request.meetingId !== void 0 ? { meetingId: request.meetingId } : {},
5555
5745
  ...request.slug !== void 0 ? { slug: request.slug } : {},
5556
5746
  ...request.breakoutRoomId != null ? { breakoutRoomId: request.breakoutRoomId } : {},
5557
- ...request.observe === true ? { observe: true } : {}
5747
+ ...request.observe === true ? { observe: true } : {},
5748
+ ...tabId !== null ? { tabId } : {},
5749
+ ...request.secondDevice !== void 0 ? { secondDevice: request.secondDevice } : {}
5558
5750
  });
5751
+ if (token.secondDeviceChoice != null) {
5752
+ store.patch({ secondDevice: { rule: token.secondDeviceChoice }, localIdentity: token.identity });
5753
+ store.dispatch({ type: "second_device" });
5754
+ report("info", `meet: already in this meeting elsewhere \u2014 offering the choice (${token.secondDeviceChoice})`);
5755
+ return;
5756
+ }
5757
+ if (store.snapshot().secondDevice !== null) store.patch({ secondDevice: null });
5758
+ if (request.secondDevice === "switch_here" && lastRequest !== null) {
5759
+ const { secondDevice: _answered, ...rest } = lastRequest;
5760
+ lastRequest = rest;
5761
+ }
5762
+ options.tabClaim?.inRoom(String(request.roomName), String(token.identity));
5559
5763
  current = token;
5560
5764
  lastServerUrl = token.serverUrl;
5561
5765
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
@@ -5620,7 +5824,7 @@ function createRoomEngine(options) {
5620
5824
  if (request.mode === "video") {
5621
5825
  await enableTrack(created, "camera", true);
5622
5826
  }
5623
- if (request.microphone !== false) {
5827
+ if (request.microphone !== false && current?.connectionKind !== "companion") {
5624
5828
  await enableTrack(created, "microphone", true);
5625
5829
  }
5626
5830
  media.onConnected();
@@ -5642,7 +5846,8 @@ function createRoomEngine(options) {
5642
5846
  }
5643
5847
  async function publish(topic, body, to) {
5644
5848
  if (driver === null) return;
5645
- await driver.publishData(topic, JSON.stringify(body), to);
5849
+ const addresses = to === void 0 ? void 0 : to.flatMap((who) => connectionsOf(who));
5850
+ await driver.publishData(topic, JSON.stringify(body), addresses);
5646
5851
  }
5647
5852
  async function applyEffects(target, effects) {
5648
5853
  try {
@@ -5680,8 +5885,19 @@ function createRoomEngine(options) {
5680
5885
  liveRoster = list;
5681
5886
  const merged = mergeGhosts(list, ghosts, at);
5682
5887
  ghosts = merged.kept;
5888
+ const people = /* @__PURE__ */ new Map();
5889
+ const connections = /* @__PURE__ */ new Map();
5890
+ for (const entry of merged.roster) {
5891
+ const person = personOf(entry.identity);
5892
+ connections.set(person, [...connections.get(person) ?? [], entry.identity]);
5893
+ 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);
5894
+ const held = people.get(person);
5895
+ if (held === void 0 || rank > held.rank) people.set(person, { entry, rank });
5896
+ }
5897
+ personConnections = connections;
5898
+ mainConnection = new Map([...people].map(([person, { entry }]) => [person, entry.identity]));
5683
5899
  store.setParticipants(
5684
- merged.roster.map((entry) => {
5900
+ [...people.values()].map(({ entry }) => {
5685
5901
  const projected = projectParticipant(entry, joinedAt);
5686
5902
  return entry.reconnecting === true ? { ...projected, reconnecting: true, quality: "lost" } : projected;
5687
5903
  })
@@ -5858,7 +6074,13 @@ function createRoomEngine(options) {
5858
6074
  },
5859
6075
  async setScreenShareEnabled(enabled) {
5860
6076
  if (driver === null) return;
5861
- if (enabled && driver.supportsScreenShare?.() === false) {
6077
+ let unsupported = false;
6078
+ try {
6079
+ unsupported = enabled && driver.supportsScreenShare?.() === false;
6080
+ } catch {
6081
+ unsupported = false;
6082
+ }
6083
+ if (unsupported) {
5862
6084
  store.patch({ share: { state: "refused", reason: "unsupported", remedy: null, at: now() } });
5863
6085
  report("warn", "meet: this browser cannot share a screen", "Share from a desktop browser (Chrome, Edge, Firefox or Safari).");
5864
6086
  return;
@@ -5957,7 +6179,7 @@ function createRoomEngine(options) {
5957
6179
  },
5958
6180
  attach(who, kind, element) {
5959
6181
  if (driver === null) return () => void 0;
5960
- return driver.attach(who, kind, element);
6182
+ return driver.attach(mainConnection.get(personOf(who)) ?? who, kind, element);
5961
6183
  },
5962
6184
  token: () => current,
5963
6185
  async dispose() {
@@ -6691,6 +6913,12 @@ function defaultDiagnostics(event) {
6691
6913
  else if (event.level === "warn") console.warn(line);
6692
6914
  else console.info(line);
6693
6915
  }
6916
+ var claimedTab = null;
6917
+ function pageTabClaim() {
6918
+ if (typeof window === "undefined") return void 0;
6919
+ claimedTab ??= createTabClaim();
6920
+ return claimedTab;
6921
+ }
6694
6922
  function deviceId() {
6695
6923
  const storage = globalThis.localStorage;
6696
6924
  const key = "mx.meet.device";
@@ -6808,13 +7036,15 @@ function MeetRuntime(props) {
6808
7036
  store,
6809
7037
  tokens,
6810
7038
  identity,
7039
+ // S7: this page's tab id (one per page, claimed at load) — the LiveKit identity's tab half.
7040
+ tabClaim: pageTabClaim(),
6811
7041
  resolveMeeting: (slug) => repository.meetingBySlug(slug),
6812
7042
  driverFactory: (handlers) => (driverRef.current ?? createLiveKitDriver)(handlers),
6813
7043
  onDiagnostic: report,
6814
7044
  effectsStorage,
6815
7045
  resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null,
6816
7046
  // §3.4: on every LiveKit disconnect, ask the server what happened to this connection.
6817
- readExit: (connection) => meetings.readExit(connection),
7047
+ readExit: (connection, pass) => meetings.readExit(connection, pass),
6818
7048
  // S6b: the connection supervisor's ports — the device's network, whether OUR servers (the
6819
7049
  // API and the media server) answer, and the per-tab reload-after-offline marker.
6820
7050
  connection: {
@@ -7883,6 +8113,7 @@ function useRoom() {
7883
8113
  phase: snapshot?.phase ?? "resolving",
7884
8114
  participants: snapshot?.participants ?? [],
7885
8115
  failure: snapshot?.failure ?? null,
8116
+ secondDevice: snapshot?.secondDevice ?? null,
7886
8117
  local,
7887
8118
  canShareScreen: engine?.supportsScreenShare() ?? true,
7888
8119
  join: useCallback2(
@@ -8028,6 +8259,7 @@ function useLobby() {
8028
8259
  const session = host?.meetings ?? null;
8029
8260
  return {
8030
8261
  waiting: (snapshot?.lobby ?? []).filter((entry) => entry.state === "knocking"),
8262
+ declined: (snapshot?.lobby ?? []).filter((entry) => entry.state === "denied"),
8031
8263
  // No runtime means no host powers — `<LobbyPanel>` renders nothing rather
8032
8264
  // than an admit button that would throw.
8033
8265
  canAdmit: session !== null && isHost,
@@ -8049,6 +8281,37 @@ function useLobby() {
8049
8281
  )
8050
8282
  };
8051
8283
  }
8284
+ function useCountdown(untilMs) {
8285
+ const [now, setNow] = useState2(() => Date.now());
8286
+ const active = typeof untilMs === "number" && untilMs > now;
8287
+ useEffect3(() => {
8288
+ if (!active) return void 0;
8289
+ setNow(Date.now());
8290
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
8291
+ return () => clearInterval(timer);
8292
+ }, [active, untilMs]);
8293
+ if (typeof untilMs !== "number") return null;
8294
+ return Math.max(0, untilMs - now);
8295
+ }
8296
+ function useKnockNotice() {
8297
+ const lobby = useLobby();
8298
+ const [dismissed, setDismissed] = useState2([]);
8299
+ const keyOf = (entry) => `${entry.identity}@${entry.knockedAt}`;
8300
+ const open = lobby.canAdmit ? lobby.waiting.filter((entry) => !dismissed.includes(keyOf(entry))) : [];
8301
+ const knocker = open[0] ?? null;
8302
+ return {
8303
+ knocker,
8304
+ others: Math.max(0, open.length - 1),
8305
+ admit: useCallback2(async () => {
8306
+ if (knocker === null) return;
8307
+ setDismissed((list) => [...list, keyOf(knocker)]);
8308
+ await lobby.admit(knocker);
8309
+ }, [knocker, lobby.admit]),
8310
+ dismiss: useCallback2(() => {
8311
+ if (knocker !== null) setDismissed((list) => [...list, keyOf(knocker)]);
8312
+ }, [knocker])
8313
+ };
8314
+ }
8052
8315
  function useWaiting() {
8053
8316
  const host = useMeetHost();
8054
8317
  const snapshot = useMeetSnapshot();
@@ -10798,6 +11061,7 @@ function ControlBar({ onLeave }) {
10798
11061
  }
10799
11062
  ) }) }) : null,
10800
11063
  /* @__PURE__ */ jsx6(BreakoutBanner, {}),
11064
+ panel !== "people" ? /* @__PURE__ */ jsx6(KnockNotice, { onView: () => setPanel("people") }) : null,
10801
11065
  controls.notice !== null && panel !== "people" ? /* @__PURE__ */ jsx6(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
10802
11066
  /* @__PURE__ */ jsxs3("div", { className: "mx-meet__controls", children: [
10803
11067
  /* @__PURE__ */ jsx6(
@@ -11128,10 +11392,21 @@ function MediaNotices() {
11128
11392
  const name = (kind) => kind === "microphone" ? "Microphone" : "Camera";
11129
11393
  if (chips.length === 0 && region.audioOutput !== "blocked" && notice === null && offer === null) return null;
11130
11394
  return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-notices", children: [
11131
- region.audioOutput === "blocked" ? /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-pill", role: "alert", "data-meet-notice": "audio-blocked", children: [
11132
- /* @__PURE__ */ jsx6("span", { children: "Sound is paused in this tab" }),
11133
- /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void enableAudio(), children: "Enable sound" })
11134
- ] }) : null,
11395
+ region.audioOutput === "blocked" ? /* @__PURE__ */ jsxs3(
11396
+ ErrorBox,
11397
+ {
11398
+ as: "div",
11399
+ className: "mx-meet__media-pill",
11400
+ "data-meet-notice": "audio-blocked",
11401
+ message: "Sound is paused in this tab",
11402
+ operation: "Enable meeting sound",
11403
+ origin: "meet/MediaNotices",
11404
+ children: [
11405
+ /* @__PURE__ */ jsx6("span", { children: "Sound is paused in this tab" }),
11406
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void enableAudio(), children: "Enable sound" })
11407
+ ]
11408
+ }
11409
+ ) : null,
11135
11410
  chips.map(({ kind, reason }) => /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-pill", role: "status", "data-meet-notice": deviceNoticeCode(kind, reason), children: [
11136
11411
  /* @__PURE__ */ jsx6("span", { children: deviceChipLabel(kind, reason) }),
11137
11412
  /* @__PURE__ */ jsx6(
@@ -11931,9 +12206,68 @@ function useMicrophoneStream(deviceId2) {
11931
12206
  }, [deviceId2]);
11932
12207
  return stream;
11933
12208
  }
12209
+ function KnockNotice({ onView }) {
12210
+ const { knocker, others, admit, dismiss } = useKnockNotice();
12211
+ if (knocker === null) return null;
12212
+ return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__notice-bar", role: "status", "data-meet-notice": "knock-received", "data-meet-participant-name": knocker.displayName, children: [
12213
+ /* @__PURE__ */ jsxs3("span", { children: [
12214
+ knocker.displayName,
12215
+ " wants to join",
12216
+ others > 0 ? ` (+${others} more)` : ""
12217
+ ] }),
12218
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__link", onClick: () => void admit().catch(() => void 0), children: "Admit" }),
12219
+ /* @__PURE__ */ jsx6(
12220
+ "button",
12221
+ {
12222
+ type: "button",
12223
+ className: "mx-meet__link",
12224
+ onClick: () => {
12225
+ dismiss();
12226
+ onView();
12227
+ },
12228
+ children: "View"
12229
+ }
12230
+ ),
12231
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismiss, children: /* @__PURE__ */ jsx6(CloseIcon, { width: 14, height: 14 }) })
12232
+ ] });
12233
+ }
11934
12234
  function LobbyPanel() {
11935
12235
  const lobby = useLobby();
11936
- if (!lobby.canAdmit || lobby.waiting.length === 0) return null;
12236
+ if (!lobby.canAdmit || lobby.waiting.length === 0 && lobby.declined.length === 0) return null;
12237
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
12238
+ lobby.waiting.length > 0 ? /* @__PURE__ */ jsx6(WaitingList, { lobby }) : null,
12239
+ lobby.declined.length > 0 ? /* @__PURE__ */ jsx6(DeclinedList, { lobby }) : null
12240
+ ] });
12241
+ }
12242
+ function DeclinedList({ lobby }) {
12243
+ return /* @__PURE__ */ jsxs3("section", { className: "mx-meet__lobby", "aria-label": "Declined requests", "data-meet-declined": "", children: [
12244
+ /* @__PURE__ */ jsx6("header", { className: "mx-meet__lobby-head", children: /* @__PURE__ */ jsx6("h3", { className: "mx-meet__lobby-title", children: "Declined" }) }),
12245
+ /* @__PURE__ */ jsx6("ul", { className: "mx-meet__lobby-list", children: lobby.declined.map((entry) => /* @__PURE__ */ jsxs3(
12246
+ "li",
12247
+ {
12248
+ className: "mx-meet__lobby-row",
12249
+ "data-meet-declined-entry": "",
12250
+ "data-meet-participant-name": entry.displayName,
12251
+ children: [
12252
+ /* @__PURE__ */ jsx6(Avatar, { name: entry.displayName, url: entry.avatarUrl, size: 32 }),
12253
+ /* @__PURE__ */ jsx6("span", { className: "mx-meet__person-name", children: entry.displayName }),
12254
+ /* @__PURE__ */ jsx6(
12255
+ "button",
12256
+ {
12257
+ type: "button",
12258
+ className: "mx-meet__link",
12259
+ "aria-label": `Let ${entry.displayName} ask again`,
12260
+ onClick: () => void lobby.undeny(entry),
12261
+ children: "Let them ask again"
12262
+ }
12263
+ )
12264
+ ]
12265
+ },
12266
+ entry.identity
12267
+ )) })
12268
+ ] });
12269
+ }
12270
+ function WaitingList({ lobby }) {
11937
12271
  return /* @__PURE__ */ jsxs3("section", { className: "mx-meet__lobby", "aria-label": "Waiting room", children: [
11938
12272
  /* @__PURE__ */ jsxs3("header", { className: "mx-meet__lobby-head", children: [
11939
12273
  /* @__PURE__ */ jsx6("h3", { className: "mx-meet__lobby-title", children: lobby.waiting.length === 1 ? "1 person is waiting" : `${lobby.waiting.length} people are waiting` }),
@@ -12197,6 +12531,21 @@ function PreJoinBody({
12197
12531
  useEffect6(() => {
12198
12532
  void host.devices.refresh();
12199
12533
  }, [host]);
12534
+ const joinWith = (secondDevice) => {
12535
+ setJoining(true);
12536
+ preview.release();
12537
+ void room.join({
12538
+ roomName,
12539
+ // An observer publishes nothing: no camera, no microphone (MD-16). A companion
12540
+ // ("join here too") never opens the microphone (S7).
12541
+ mode: observe2 ? "audio" : mode,
12542
+ microphone: observe2 || secondDevice === "join_too" ? false : microphone,
12543
+ ...meetingId !== void 0 ? { meetingId } : {},
12544
+ ...slug !== void 0 ? { slug } : {},
12545
+ ...observe2 ? { observe: true } : {},
12546
+ ...secondDevice !== void 0 ? { secondDevice } : {}
12547
+ }).then(() => onJoined?.()).catch(() => void 0).finally(() => setJoining(false));
12548
+ };
12200
12549
  return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__prejoin", children: [
12201
12550
  /* @__PURE__ */ jsxs3("div", { className: "mx-meet__preview", children: [
12202
12551
  mode === "video" && preview.preview !== null ? /* @__PURE__ */ jsx6(
@@ -12281,27 +12630,34 @@ function PreJoinBody({
12281
12630
  ),
12282
12631
  /* @__PURE__ */ jsx6(Label, { htmlFor: "mx-meet-join-mic", children: "Join with microphone" })
12283
12632
  ] }),
12284
- /* @__PURE__ */ jsx6(
12285
- Button,
12286
- {
12287
- type: "button",
12288
- className: "mx-meet__join",
12289
- disabled: joining,
12290
- onClick: () => {
12291
- setJoining(true);
12292
- preview.release();
12293
- void room.join({
12294
- roomName,
12295
- // An observer publishes nothing: no camera, no microphone (MD-16).
12296
- mode: observe2 ? "audio" : mode,
12297
- microphone: observe2 ? false : microphone,
12298
- ...meetingId !== void 0 ? { meetingId } : {},
12299
- ...slug !== void 0 ? { slug } : {},
12300
- ...observe2 ? { observe: true } : {}
12301
- }).then(() => onJoined?.()).catch(() => void 0).finally(() => setJoining(false));
12302
- },
12303
- children: joining ? "Joining\u2026" : observe2 ? "Observe" : "Join now"
12304
- }
12633
+ room.secondDevice === null ? /* @__PURE__ */ jsx6(Button, { type: "button", className: "mx-meet__join", disabled: joining, onClick: () => joinWith(), children: joining ? "Joining\u2026" : observe2 ? "Observe" : "Join now" }) : (
12634
+ // S7: ALREADY IN THIS MEETING on another tab or device — the meeting's choice
12635
+ // (Meet: Switch here / Join here too; Teams: Transfer here / Add this device).
12636
+ /* @__PURE__ */ jsxs3("div", { className: "mx-meet__second-device", "data-meet-region": "second-device", children: [
12637
+ /* @__PURE__ */ jsx6("p", { className: "mx-meet__hint", children: "You're in this meeting on another device" }),
12638
+ /* @__PURE__ */ jsx6(
12639
+ Button,
12640
+ {
12641
+ type: "button",
12642
+ className: "mx-meet__join",
12643
+ "data-meet-control": "switch-here",
12644
+ disabled: joining,
12645
+ onClick: () => joinWith("switch_here"),
12646
+ children: room.secondDevice.rule === "ask_transfer_or_add" ? "Transfer here" : "Switch here"
12647
+ }
12648
+ ),
12649
+ /* @__PURE__ */ jsx6(
12650
+ Button,
12651
+ {
12652
+ type: "button",
12653
+ variant: "outline",
12654
+ "data-meet-control": "join-too",
12655
+ disabled: joining,
12656
+ onClick: () => joinWith("join_too"),
12657
+ children: room.secondDevice.rule === "ask_transfer_or_add" ? "Add this device" : "Join here too"
12658
+ }
12659
+ )
12660
+ ] })
12305
12661
  )
12306
12662
  ] }),
12307
12663
  room.failure !== null ? /* @__PURE__ */ jsxs3("p", { className: "mx-meet__error", children: [
@@ -12499,16 +12855,21 @@ var REFUSED_TITLES = {
12499
12855
  not_invited: "You aren't invited to this meeting",
12500
12856
  locked: "This meeting is locked",
12501
12857
  removed: "You can't rejoin this meeting",
12502
- lobby_rejects: "You can't ask to join this meeting",
12858
+ lobby_rejects: "The host declined your request",
12503
12859
  meeting_full: "This meeting is full",
12504
12860
  platform_capacity: "Meetings are busy right now",
12505
12861
  verification_required: "Verify your account to join",
12506
12862
  registration_required: "Register to join this meeting"
12507
12863
  };
12864
+ function clockText(ms2) {
12865
+ const total = Math.ceil(ms2 / 1e3);
12866
+ return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`;
12867
+ }
12508
12868
  function ExitScreen({ onLeave }) {
12509
12869
  const { phase, reason, host, snapshot, actions, call, connection } = useMeetCore();
12510
12870
  const [busy, setBusy] = useState4(false);
12511
12871
  const failure = snapshot?.failure ?? null;
12872
+ const retryLeft = useCountdown(phase === "refused:lobby_rejects" ? failure?.retryAt ?? null : null);
12512
12873
  const rejoin = /* @__PURE__ */ jsx6(
12513
12874
  Button,
12514
12875
  {
@@ -12551,6 +12912,33 @@ function ExitScreen({ onLeave }) {
12551
12912
  /* @__PURE__ */ jsx6(Button, { type: "button", variant: "outline", onClick: actions.retry, children: "Try again" }),
12552
12913
  home
12553
12914
  ] });
12915
+ if (code === "lobby_rejects") {
12916
+ if (retryLeft !== null && retryLeft > 0) {
12917
+ line = `You can ask again in ${clockText(retryLeft)}`;
12918
+ notice = "reknock-cooldown";
12919
+ actionsNode = home;
12920
+ } else if (retryLeft === 0) {
12921
+ line = "You can ask to join again";
12922
+ actionsNode = /* @__PURE__ */ jsxs3(Fragment, { children: [
12923
+ /* @__PURE__ */ jsx6(
12924
+ Button,
12925
+ {
12926
+ type: "button",
12927
+ disabled: busy,
12928
+ onClick: () => {
12929
+ setBusy(true);
12930
+ void actions.rejoin().catch(() => void 0).finally(() => setBusy(false));
12931
+ },
12932
+ children: "Ask to join again"
12933
+ }
12934
+ ),
12935
+ home
12936
+ ] });
12937
+ } else {
12938
+ line = "Ask the host to let you in, then open the link again";
12939
+ actionsNode = home;
12940
+ }
12941
+ }
12554
12942
  } else {
12555
12943
  switch (phase) {
12556
12944
  case "left":
@@ -12611,7 +12999,7 @@ function ExitScreen({ onLeave }) {
12611
12999
  /* @__PURE__ */ jsx6(Avatar, { name: host?.identity.displayName ?? "", url: host?.identity.avatarUrl ?? null, size: 72 }),
12612
13000
  /* @__PURE__ */ jsx6("h1", { className: "mx-meet__prejoin-title", children: title }),
12613
13001
  line !== null ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__empty-body", children: line }) : null,
12614
- notice !== null ? /* @__PURE__ */ jsx6("span", { role: "status", "data-meet-notice": notice, className: "mx-meet__sr-only", children: title }) : null,
13002
+ notice !== null ? /* @__PURE__ */ jsx6("span", { role: "status", "data-meet-notice": notice, className: "mx-meet__sr-only", children: notice === "reknock-cooldown" && line !== null ? line : title }) : null,
12615
13003
  /* @__PURE__ */ jsx6("div", { className: "mx-meet__waiting-actions", children: actionsNode })
12616
13004
  ] });
12617
13005
  }
@@ -12620,6 +13008,7 @@ function WaitingRoom({ host, onLeave }) {
12620
13008
  const { phaseNotice } = useMeetCore();
12621
13009
  const [busy, setBusy] = useState4(false);
12622
13010
  const admission = waiting?.admission ?? "knocking";
13011
+ const reknockLeft = useCountdown(admission === "denied" ? waiting?.reknockAt ?? null : null);
12623
13012
  const run = (action) => () => {
12624
13013
  setBusy(true);
12625
13014
  void action().catch(() => void 0).finally(() => setBusy(false));
@@ -12627,6 +13016,7 @@ function WaitingRoom({ host, onLeave }) {
12627
13016
  const home = /* @__PURE__ */ jsx6(Button, { type: "button", variant: "outline", onClick: () => onLeave !== void 0 ? onLeave() : window.location.assign("/"), children: "Back to home" });
12628
13017
  let title;
12629
13018
  let line = null;
13019
+ let cooldownNotice = false;
12630
13020
  let actions;
12631
13021
  switch (admission) {
12632
13022
  case "waiting_host":
@@ -12635,8 +13025,12 @@ function WaitingRoom({ host, onLeave }) {
12635
13025
  break;
12636
13026
  case "denied":
12637
13027
  title = "The host declined your request";
13028
+ if (reknockLeft !== null && reknockLeft > 0) {
13029
+ line = `You can ask again in ${clockText(reknockLeft)}`;
13030
+ cooldownNotice = true;
13031
+ }
12638
13032
  actions = /* @__PURE__ */ jsxs3(Fragment, { children: [
12639
- waiting?.reknockAllowed === true ? /* @__PURE__ */ jsx6(Button, { type: "button", disabled: busy, onClick: run(askAgain), children: "Ask to join again" }) : null,
13033
+ waiting?.reknockAllowed === true || waiting?.reknockAt != null && reknockLeft === 0 ? /* @__PURE__ */ jsx6(Button, { type: "button", disabled: busy, onClick: run(askAgain), children: "Ask to join again" }) : null,
12640
13034
  home
12641
13035
  ] });
12642
13036
  break;
@@ -12667,7 +13061,15 @@ function WaitingRoom({ host, onLeave }) {
12667
13061
  /* @__PURE__ */ jsx6(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 72 }),
12668
13062
  /* @__PURE__ */ jsx6("h1", { className: "mx-meet__prejoin-title", children: title }),
12669
13063
  phaseNotice === "sent-to-waiting" ? /* @__PURE__ */ jsx6("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,
12670
- line !== null ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__empty-body", role: "status", "data-meet-notice": "host-absent", children: line }) : null,
13064
+ line !== null ? /* @__PURE__ */ jsx6(
13065
+ "p",
13066
+ {
13067
+ className: "mx-meet__empty-body",
13068
+ role: "status",
13069
+ "data-meet-notice": cooldownNotice ? "reknock-cooldown" : "host-absent",
13070
+ children: line
13071
+ }
13072
+ ) : null,
12671
13073
  /* @__PURE__ */ jsx6("div", { className: "mx-meet__waiting-actions", children: actions })
12672
13074
  ] });
12673
13075
  }