@ai-matrx/meet 0.11.35 → 0.11.37

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
@@ -710,6 +710,30 @@ function invalidResponse(operation, detail) {
710
710
  );
711
711
  }
712
712
 
713
+ // src/core/leave.ts
714
+ var DEFAULT_LEAVE_POLICY = { hostLeavePrompt: "leave_or_end", endConfirm: false };
715
+ var PROMPTS = ["leave_or_end", "must_assign_or_end", "leave_only"];
716
+ function leavePolicyFrom(raw) {
717
+ if (typeof raw !== "object" || raw === null) return DEFAULT_LEAVE_POLICY;
718
+ const value = raw;
719
+ const prompt = value["host_leave_prompt"];
720
+ const confirm = value["end_confirm"];
721
+ return {
722
+ hostLeavePrompt: PROMPTS.includes(prompt) ? prompt : DEFAULT_LEAVE_POLICY.hostLeavePrompt,
723
+ endConfirm: typeof confirm === "boolean" ? confirm : DEFAULT_LEAVE_POLICY.endConfirm
724
+ };
725
+ }
726
+ function leaveChoices(args) {
727
+ if (args.role !== "host") return ["leave"];
728
+ switch (args.policy.hostLeavePrompt) {
729
+ case "must_assign_or_end":
730
+ return args.othersPresent ? ["assign_and_leave", "end_for_all"] : ["leave", "end_for_all"];
731
+ case "leave_only":
732
+ case "leave_or_end":
733
+ return ["leave", "end_for_all"];
734
+ }
735
+ }
736
+
713
737
  // src/core/tokens.ts
714
738
  function createWebRoomTokenStorage(onDiagnostic) {
715
739
  const store = globalThis.localStorage;
@@ -901,7 +925,8 @@ function parseRoomToken(body, roomName) {
901
925
  pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
902
926
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
903
927
  observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
904
- connectionPolicy: connectionPolicyFrom(payload["connection_policy"])
928
+ connectionPolicy: connectionPolicyFrom(payload["connection_policy"]),
929
+ leavePolicy: leavePolicyFrom(payload["leave_policy"])
905
930
  };
906
931
  }
907
932
  function createTokenClient(options) {
@@ -3801,6 +3826,10 @@ var EMPTY = {
3801
3826
  chatHistoryError: null,
3802
3827
  reactions: [],
3803
3828
  spotlight: null,
3829
+ actingHost: null,
3830
+ presenter: null,
3831
+ waitingCount: 0,
3832
+ leavePolicy: DEFAULT_LEAVE_POLICY,
3804
3833
  moderationNotice: null,
3805
3834
  actionNotice: null,
3806
3835
  privateChatRequest: null,
@@ -3862,8 +3891,6 @@ function createMeetStore(initial = {}) {
3862
3891
  },
3863
3892
  setParticipants(participants) {
3864
3893
  const names = learnNames(state.names, participants);
3865
- const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
3866
- const handTimes = new Map(state.participants.map((p) => [p.identity, p.handRaisedAt ?? null]));
3867
3894
  const quality = new Map(state.participants.map((p) => [p.identity, p.quality]));
3868
3895
  commit({
3869
3896
  ...state,
@@ -3876,8 +3903,6 @@ function createMeetStore(initial = {}) {
3876
3903
  // identity could reach a screen, and it is the place that asks the
3877
3904
  // name book. `roster-names.test.ts` fails if a token ever gets past it.
3878
3905
  displayName: displayNameFor(names, participant.identity, participant.displayName),
3879
- handRaised: hands.get(participant.identity) ?? participant.handRaised,
3880
- handRaisedAt: handTimes.get(participant.identity) ?? participant.handRaisedAt ?? null,
3881
3906
  quality: participant.quality === "unknown" ? quality.get(participant.identity) ?? "unknown" : participant.quality
3882
3907
  }))
3883
3908
  });
@@ -3900,20 +3925,6 @@ function createMeetStore(initial = {}) {
3900
3925
  )
3901
3926
  });
3902
3927
  },
3903
- setHand(identity, raised, at) {
3904
- commit({
3905
- ...state,
3906
- participants: state.participants.map(
3907
- (participant) => participant.identity === identity ? {
3908
- ...participant,
3909
- handRaised: raised,
3910
- // A hand that is ALREADY up keeps its place in the queue when
3911
- // it is re-announced (to a late joiner): the first time wins.
3912
- handRaisedAt: raised ? participant.handRaised && participant.handRaisedAt != null ? participant.handRaisedAt : at ?? Date.now() : null
3913
- } : participant
3914
- )
3915
- });
3916
- },
3917
3928
  addChat(message) {
3918
3929
  if (state.chat.some((existing) => existing.id === message.id)) return;
3919
3930
  const chat = [...state.chat, message];
@@ -4273,7 +4284,7 @@ function createMeetingSession(options) {
4273
4284
  })();
4274
4285
  return reading;
4275
4286
  }
4276
- async function postLeave(intent) {
4287
+ async function postLeave(intent, extra = {}) {
4277
4288
  const meeting = requireMeeting(`meet.${intent}`);
4278
4289
  const pass = options.resolvePass?.() ?? null;
4279
4290
  const response = await options.api.request({
@@ -4281,7 +4292,7 @@ function createMeetingSession(options) {
4281
4292
  method: "POST",
4282
4293
  requireAuth: false,
4283
4294
  operation: `meet.${intent}`,
4284
- body: { meeting_id: meeting.id, intent },
4295
+ body: { meeting_id: meeting.id, intent, ...extra },
4285
4296
  ...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
4286
4297
  });
4287
4298
  if (!response.ok) {
@@ -4295,7 +4306,7 @@ function createMeetingSession(options) {
4295
4306
  throw new MeetError(
4296
4307
  "transport",
4297
4308
  refusal2?.reason ?? `meet.${intent}: the server answered ${response.status}`,
4298
- refusal2?.remedy ?? "You are still in the waiting room. Try again."
4309
+ refusal2?.remedy ?? (intent === "withdraw" ? "You are still in the waiting room. Try again." : "Nothing was changed. Try again.")
4299
4310
  );
4300
4311
  }
4301
4312
  }
@@ -4519,6 +4530,10 @@ function createMeetingSession(options) {
4519
4530
  store.dispatch({ type: "admission", admission: "withdrawn" });
4520
4531
  hint();
4521
4532
  },
4533
+ async hostLeave(intent) {
4534
+ if (intent === "end_for_all") await postLeave("end_for_all");
4535
+ else await postLeave("assign_and_leave", { assign_to: intent.assignTo });
4536
+ },
4522
4537
  async askAgain() {
4523
4538
  await options.rejoin?.();
4524
4539
  },
@@ -4981,18 +4996,6 @@ var ROOM_EVENTS = {
4981
4996
  wrapUp: "matrx.meet.wrap_up",
4982
4997
  /** The meeting ended for everyone. Payload `{ meeting_id, ended_at, trigger }`. */
4983
4998
  ended: "matrx.meet.ended",
4984
- /**
4985
- * A `meet_recordings` row changed state. Payload
4986
- * `{ meeting_id, recording: { id, state, started_by } }`.
4987
- *
4988
- * 🚨 THIS IS THE ONLY WAY A GUEST LEARNS A RECORDING IS RUNNING. PreJoin
4989
- * promises every joiner an indicator, and `anon` holds no SELECT grant on
4990
- * `meet_recordings` (MRI-A10 settled that widening it is forbidden), so
4991
- * without this packet the promise was a sentence with nothing behind it: on
4992
- * production 2026-09-09 the guest lane showed no indicator at all while the
4993
- * host recorded (MRI-C8).
4994
- */
4995
- recording: "matrx.meet.recording",
4996
4999
  /**
4997
5000
  * A chat message to everyone was SAVED (Meet wave 2). Payload
4998
5001
  * `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
@@ -5045,8 +5048,6 @@ function applyRoomEvent(args) {
5045
5048
  return applyWrapUp(store, payload, now);
5046
5049
  case ROOM_EVENTS.ended:
5047
5050
  return applyEnded(store, payload);
5048
- case ROOM_EVENTS.recording:
5049
- return applyRecording(store, payload);
5050
5051
  case ROOM_EVENTS.chat:
5051
5052
  return applyChat(store, payload);
5052
5053
  case ROOM_EVENTS.moderation:
@@ -5126,9 +5127,9 @@ function applyWrapUp(store, payload, now) {
5126
5127
  store.announceWrapUp(announcement);
5127
5128
  return HANDLED;
5128
5129
  }
5129
- function applyRecording(store, payload) {
5130
+ function applyRecordingState(store, payload) {
5130
5131
  const row = object(payload["recording"]);
5131
- if (row === null) return ignored("a recording event with no `recording` row");
5132
+ if (row === null) return ignored("room state carries no recording");
5132
5133
  const meetingId = text2(payload["meeting_id"]);
5133
5134
  const meeting = store.snapshot().meeting;
5134
5135
  if (meeting !== null && meetingId !== null && meetingId !== meeting.id) {
@@ -5210,19 +5211,6 @@ function applyModeration(store, payload, now) {
5210
5211
  }
5211
5212
  const kind = text2(payload["kind"]);
5212
5213
  switch (kind) {
5213
- case "lower-hands": {
5214
- const raw = payload["identities"];
5215
- const everyone = raw === "*";
5216
- const named = new Set(
5217
- Array.isArray(raw) ? raw.filter((value) => typeof value === "string") : []
5218
- );
5219
- for (const participant of snapshot.participants) {
5220
- if (participant.handRaised && (everyone || named.has(participant.identity))) {
5221
- store.setHand(participant.identity, false);
5222
- }
5223
- }
5224
- return HANDLED;
5225
- }
5226
5214
  case "role": {
5227
5215
  const identity = text2(payload["identity"]);
5228
5216
  const role = text2(payload["role"]);
@@ -5316,7 +5304,6 @@ function noteTakerLabel(status) {
5316
5304
 
5317
5305
  // src/core/room-driver.ts
5318
5306
  var DATA_TOPICS = {
5319
- hand: "mx.hand",
5320
5307
  reaction: "mx.reaction",
5321
5308
  chat: "mx.chat"
5322
5309
  };
@@ -5340,6 +5327,11 @@ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
5340
5327
  return /administrator|policy/i.test(remedy) ? remedy : SCREEN_SHARE_BLOCKED_REMEDY;
5341
5328
  }
5342
5329
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
5330
+ var PARTICIPANT_HAND_ATTRIBUTE = "matrx.hand";
5331
+ function handFrom(value) {
5332
+ const at = value === void 0 || value.length === 0 ? Number.NaN : Number(value);
5333
+ return Number.isFinite(at) ? { handRaised: true, handRaisedAt: at } : { handRaised: false, handRaisedAt: null };
5334
+ }
5343
5335
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
5344
5336
  var CONNECTION_KIND_ATTRIBUTE = "matrx.participant.connection_kind";
5345
5337
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
@@ -5393,7 +5385,10 @@ function projectParticipant(driver, joinedAt) {
5393
5385
  cameraEnabled: driver.cameraEnabled,
5394
5386
  screenSharing: driver.screenSharing,
5395
5387
  speaking: false,
5396
- handRaised: false,
5388
+ // S8: a hand is the SERVER's (`hand_raised_at` on the person row, `/moderate raise_hand`),
5389
+ // replicated as the `matrx.hand` attribute: epoch ms of the raise, "" when down. It is
5390
+ // therefore delivered to late joiners and survives refresh and reconnect.
5391
+ ...handFrom(driver.attributes[PARTICIPANT_HAND_ATTRIBUTE]),
5397
5392
  quality: "unknown",
5398
5393
  joinedAt
5399
5394
  };
@@ -5523,40 +5518,38 @@ function createRoomEngine(options) {
5523
5518
  },
5524
5519
  participants(list) {
5525
5520
  const joinedAt = new Date(now()).toISOString();
5526
- const before = new Set(store.snapshot().participants.map((p) => p.identity));
5527
5521
  projectRoster(list, joinedAt);
5528
- const snapshot = store.snapshot();
5529
- const newcomer = snapshot.participants.some(
5530
- (participant) => !participant.isLocal && !before.has(participant.identity)
5531
- );
5532
- const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
5533
- if (newcomer && before.size > 0 && me?.handRaised === true) {
5534
- void publish(DATA_TOPICS.hand, { raised: true, raisedAt: me.handRaisedAt ?? now() }).catch(
5535
- () => void 0
5536
- );
5537
- }
5538
5522
  },
5539
5523
  participantLeft(who, reason) {
5540
5524
  departures.set(who, reason);
5541
5525
  },
5542
5526
  roomMetadata(metadata) {
5543
- let spotlight = null;
5527
+ let parsed = {};
5544
5528
  if (metadata.length > 0) {
5545
5529
  try {
5546
- const parsed = JSON.parse(metadata);
5547
- if (typeof parsed === "object" && parsed !== null) {
5548
- const value = parsed.spotlight;
5549
- if (typeof value === "string" && value.length > 0) spotlight = asParticipantIdentity(value);
5550
- }
5530
+ const value = JSON.parse(metadata);
5531
+ if (typeof value === "object" && value !== null) parsed = value;
5551
5532
  } catch {
5552
5533
  report(
5553
5534
  "warn",
5554
- "meet: the room's metadata is not JSON, so no spotlight was read from it",
5555
- "Only aidream writes room metadata; check services/meet/moderation.py."
5535
+ "meet: the room's metadata is not JSON, so the room state was not read from it",
5536
+ "Only aidream writes room metadata; check services/meet/room_state.py."
5556
5537
  );
5557
5538
  }
5558
5539
  }
5559
- if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
5540
+ const key = (name) => {
5541
+ const value = parsed[name];
5542
+ return typeof value === "string" && value.length > 0 ? asParticipantIdentity(personOf(value)) : null;
5543
+ };
5544
+ const waiting = typeof parsed["waiting_count"] === "number" ? parsed["waiting_count"] : 0;
5545
+ const before = store.snapshot();
5546
+ const next = { spotlight: key("spotlight"), actingHost: key("acting_host"), presenter: key("presenter"), waitingCount: waiting };
5547
+ if (before.spotlight !== next.spotlight || before.actingHost !== next.actingHost || before.presenter !== next.presenter || before.waitingCount !== next.waitingCount) {
5548
+ store.patch(next);
5549
+ }
5550
+ if (parsed["recording"] !== void 0 && parsed["recording"] !== null) {
5551
+ applyRecordingState(store, { meeting_id: parsed["meeting_id"], recording: parsed["recording"] });
5552
+ }
5560
5553
  },
5561
5554
  activeSpeakers(identities) {
5562
5555
  store.setSpeaking([...new Set(identities.map((who) => personOf(who)))]);
@@ -5658,13 +5651,6 @@ function createRoomEngine(options) {
5658
5651
  }
5659
5652
  const sender = personOf(from ?? "");
5660
5653
  switch (topic) {
5661
- case DATA_TOPICS.hand:
5662
- store.setHand(
5663
- sender,
5664
- envelope.raised === true,
5665
- typeof envelope.raisedAt === "number" ? envelope.raisedAt : now()
5666
- );
5667
- return;
5668
5654
  case DATA_TOPICS.reaction:
5669
5655
  if (typeof envelope.emoji === "string") {
5670
5656
  store.addReaction({
@@ -5763,6 +5749,7 @@ function createRoomEngine(options) {
5763
5749
  current = token;
5764
5750
  lastServerUrl = token.serverUrl;
5765
5751
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
5752
+ if (token.leavePolicy !== void 0) store.patch({ leavePolicy: token.leavePolicy });
5766
5753
  store.patch({
5767
5754
  localIdentity: token.identity,
5768
5755
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -5797,6 +5784,7 @@ function createRoomEngine(options) {
5797
5784
  return;
5798
5785
  }
5799
5786
  driver = created;
5787
+ if (token.connectionKind === "companion") created.setRemoteAudioPlayback?.(false);
5800
5788
  await created.connect(token.serverUrl, token.token);
5801
5789
  store.patch({ failure: null, connection: "stable" });
5802
5790
  store.dispatch({ type: "connected" });
@@ -6058,6 +6046,19 @@ function createRoomEngine(options) {
6058
6046
  }
6059
6047
  },
6060
6048
  leave: leaveRoom,
6049
+ async checkElsewhere(roomName) {
6050
+ const claim = options.tabClaim;
6051
+ const userId = identity.userId;
6052
+ if (claim === void 0 || userId === null) return false;
6053
+ const before = store.snapshot();
6054
+ if (before.phase !== "prejoin" || before.secondDevice !== null) return false;
6055
+ if (!await claim.presentElsewhere(String(roomName), `user:${String(userId)}`)) return false;
6056
+ const now2 = store.snapshot();
6057
+ if (now2.phase !== "prejoin" || now2.secondDevice !== null) return false;
6058
+ store.patch({ secondDevice: { rule: "ask_switch_or_join" } });
6059
+ report("info", "meet: this browser already has you in this meeting \u2014 offering the choice before Join");
6060
+ return true;
6061
+ },
6061
6062
  backToPrejoin() {
6062
6063
  store.dispatch({ type: "back_to_prejoin", click: true });
6063
6064
  },
@@ -6115,11 +6116,6 @@ function createRoomEngine(options) {
6115
6116
  dismissShareNotice() {
6116
6117
  if (store.snapshot().share.state === "refused") store.patch({ share: NO_SHARE });
6117
6118
  },
6118
- async raiseHand(raised) {
6119
- const local = store.snapshot().localIdentity;
6120
- if (local !== null) store.setHand(local, raised);
6121
- await publish(DATA_TOPICS.hand, { raised, raisedAt: now() });
6122
- },
6123
6119
  async react(emoji) {
6124
6120
  const local = store.snapshot().localIdentity;
6125
6121
  const id = `${local ?? "local"}-${now()}`;
@@ -6248,6 +6244,8 @@ var MODERATION_ACTIONS = [
6248
6244
  "ask_unmute",
6249
6245
  "ask_all_unmute",
6250
6246
  "spotlight",
6247
+ "present",
6248
+ "raise_hand",
6251
6249
  "lower_hand",
6252
6250
  "lower_all_hands",
6253
6251
  "make_cohost",
@@ -6291,7 +6289,8 @@ function createInRoomClient(options) {
6291
6289
  action: request.action,
6292
6290
  ...request.identity != null ? { identity: request.identity } : {},
6293
6291
  ...request.name !== void 0 ? { name: request.name } : {},
6294
- ...request.reason !== void 0 ? { reason: request.reason } : {}
6292
+ ...request.reason !== void 0 ? { reason: request.reason } : {},
6293
+ ...request.on !== void 0 ? { on: request.on } : {}
6295
6294
  },
6296
6295
  ...hdrs !== void 0 ? { headers: hdrs } : {}
6297
6296
  });
@@ -6607,6 +6606,7 @@ function createLiveKitDriver(handlers, options = {}) {
6607
6606
  projectRoster = roster;
6608
6607
  created.on(RoomEvent.TrackSubscribed, (track) => {
6609
6608
  if (track.kind !== Track.Kind.Audio || typeof document === "undefined") return;
6609
+ if (!remoteAudioPlayback) return;
6610
6610
  const element = track.attach();
6611
6611
  element.setAttribute("data-mx-meet-audio", "");
6612
6612
  audioSink().appendChild(element);
@@ -6691,6 +6691,7 @@ function createLiveKitDriver(handlers, options = {}) {
6691
6691
  let sources = null;
6692
6692
  let trackEvents = null;
6693
6693
  let audioContainer = null;
6694
+ let remoteAudioPlayback = true;
6694
6695
  function audioSink() {
6695
6696
  if (audioContainer !== null && audioContainer.isConnected) return audioContainer;
6696
6697
  const container = document.createElement("div");
@@ -6849,6 +6850,14 @@ function createLiveKitDriver(handlers, options = {}) {
6849
6850
  throw normalizeDeviceError(error, "meet.switchDevice");
6850
6851
  }
6851
6852
  },
6853
+ setRemoteAudioPlayback(enabled) {
6854
+ remoteAudioPlayback = enabled;
6855
+ if (enabled || audioContainer === null) return;
6856
+ for (const element of Array.from(audioContainer.querySelectorAll("[data-mx-meet-audio]"))) {
6857
+ if (element instanceof HTMLMediaElement) element.srcObject = null;
6858
+ element.remove();
6859
+ }
6860
+ },
6852
6861
  async startAudio() {
6853
6862
  const target = room;
6854
6863
  if (target === null) return;
@@ -8120,6 +8129,10 @@ function useRoom() {
8120
8129
  async (request) => requireEngine(engine, "join").join(request),
8121
8130
  [engine]
8122
8131
  ),
8132
+ checkElsewhere: useCallback2(
8133
+ async (roomName) => engine === null ? false : engine.checkElsewhere(roomName),
8134
+ [engine]
8135
+ ),
8123
8136
  leave: useCallback2(async () => requireEngine(engine, "leave").leave(), [engine]),
8124
8137
  toggleMicrophone: useCallback2(
8125
8138
  async () => requireEngine(engine, "toggleMicrophone").setMicrophoneEnabled(
@@ -8131,16 +8144,20 @@ function useRoom() {
8131
8144
  async () => requireEngine(engine, "toggleCamera").setCameraEnabled(!(local?.cameraEnabled ?? false)),
8132
8145
  [engine, local]
8133
8146
  ),
8134
- toggleScreenShare: useCallback2(
8135
- async () => requireEngine(engine, "toggleScreenShare").setScreenShareEnabled(
8136
- !(local?.screenSharing ?? false)
8137
- ),
8138
- [engine, local]
8139
- ),
8140
- toggleHand: useCallback2(
8141
- async () => requireEngine(engine, "toggleHand").raiseHand(!(local?.handRaised ?? false)),
8142
- [engine, local]
8143
- ),
8147
+ toggleScreenShare: useCallback2(async () => {
8148
+ const on = !(local?.screenSharing ?? false);
8149
+ await requireEngine(engine, "toggleScreenShare").setScreenShareEnabled(on);
8150
+ const sharing = host?.store.snapshot().participants.find((p) => p.isLocal)?.screenSharing ?? false;
8151
+ if (host !== null && sharing === on) {
8152
+ await host.inRoom.moderate({ action: "present", on }).catch(() => void 0);
8153
+ }
8154
+ }, [engine, host, local]),
8155
+ // S8: a hand is SERVER state (`/moderate raise_hand · lower_hand`, server time), replicated
8156
+ // back as the `matrx.hand` attribute — no client sets its own hand on the wire.
8157
+ toggleHand: useCallback2(async () => {
8158
+ if (host === null) throw noRuntime("toggleHand");
8159
+ await host.inRoom.moderate({ action: local?.handRaised === true ? "lower_hand" : "raise_hand" });
8160
+ }, [host, local]),
8144
8161
  react: useCallback2(
8145
8162
  async (emoji) => requireEngine(engine, "react").react(emoji),
8146
8163
  [engine]
@@ -8322,6 +8339,31 @@ function useWaiting() {
8322
8339
  askAgain: useCallback2(async () => requireSession(session, "askAgain").askAgain(), [session])
8323
8340
  };
8324
8341
  }
8342
+ function useLeave() {
8343
+ const host = useMeetHost();
8344
+ const snapshot = useMeetSnapshot();
8345
+ const session = host?.meetings ?? null;
8346
+ const engine = host?.engine ?? null;
8347
+ const policy = snapshot?.leavePolicy ?? DEFAULT_LEAVE_POLICY;
8348
+ const participants = snapshot?.participants ?? [];
8349
+ const me = participants.find((p) => p.isLocal);
8350
+ const candidates = participants.filter((p) => !p.isLocal && !p.isAgent && p.role !== "viewer");
8351
+ const choices = leaveChoices({ policy, role: me?.role ?? null, othersPresent: candidates.length > 0 });
8352
+ return {
8353
+ choices,
8354
+ policy,
8355
+ candidates,
8356
+ leave: useCallback2(async () => requireEngine(engine, "leave").leave(), [engine]),
8357
+ endForAll: useCallback2(async () => requireSession(session, "endForAll").hostLeave("end_for_all"), [session]),
8358
+ assignAndLeave: useCallback2(
8359
+ async (person) => {
8360
+ await requireSession(session, "assignAndLeave").hostLeave({ assignTo: String(person.identity) });
8361
+ await requireEngine(engine, "leave").leave();
8362
+ },
8363
+ [session, engine]
8364
+ )
8365
+ };
8366
+ }
8325
8367
  function useRecording() {
8326
8368
  const host = useMeetHost();
8327
8369
  const snapshot = useMeetSnapshot();
@@ -11245,19 +11287,75 @@ function ControlBar({ onLeave }) {
11245
11287
  ] }) : null
11246
11288
  ] }) }) : null
11247
11289
  ] }),
11248
- /* @__PURE__ */ jsx6(
11249
- ControlButton,
11290
+ /* @__PURE__ */ jsx6(LeaveControl, { onLeave })
11291
+ ] })
11292
+ ] });
11293
+ }
11294
+ var LEAVE_WORDS = {
11295
+ leave_or_end: { leave: "Leave call", assign_and_leave: "Assign a new host", end_for_all: "End call for everyone" },
11296
+ must_assign_or_end: { leave: "Leave meeting", assign_and_leave: "Assign a new host", end_for_all: "End meeting for all" },
11297
+ leave_only: { leave: "Leave", assign_and_leave: "Assign a new host", end_for_all: "End meeting" }
11298
+ };
11299
+ function LeaveControl({ onLeave }) {
11300
+ const leave = useLeave();
11301
+ const [open, setOpen] = useState4("none");
11302
+ const [failure, setFailure] = useState4(null);
11303
+ const words = LEAVE_WORDS[leave.policy.hostLeavePrompt];
11304
+ const run = (work, leaving) => {
11305
+ setFailure(null);
11306
+ void work().then(() => {
11307
+ setOpen("none");
11308
+ if (leaving) onLeave?.();
11309
+ }).catch((error) => setFailure(error instanceof Error ? error.message : String(error)));
11310
+ };
11311
+ const leaveNow = () => run(leave.leave, true);
11312
+ const endNow = () => run(leave.endForAll, false);
11313
+ return /* @__PURE__ */ jsxs3("span", { className: "mx-meet__anchor", children: [
11314
+ /* @__PURE__ */ jsx6(
11315
+ ControlButton,
11316
+ {
11317
+ label: "Leave",
11318
+ danger: true,
11319
+ active: open !== "none",
11320
+ onClick: () => {
11321
+ if (leave.choices.length === 1 && leave.choices[0] === "leave") leaveNow();
11322
+ else setOpen(open === "none" ? "menu" : "none");
11323
+ },
11324
+ children: /* @__PURE__ */ jsx6(PhoneOffIcon, {})
11325
+ }
11326
+ ),
11327
+ open !== "none" ? /* @__PURE__ */ jsx6(Popover, { label: open === "assign" ? words.assign_and_leave : "Leave", onClose: () => setOpen("none"), children: /* @__PURE__ */ jsxs3("div", { className: "mx-meet__menu", "data-meet-leave-menu": leave.policy.hostLeavePrompt, children: [
11328
+ open === "menu" ? leave.choices.map((choice) => /* @__PURE__ */ jsx6(
11329
+ "button",
11250
11330
  {
11251
- label: "Leave",
11252
- danger: true,
11331
+ type: "button",
11332
+ className: choice === "end_for_all" ? "mx-meet__menu-item mx-meet__menu-item--danger" : "mx-meet__menu-item",
11253
11333
  onClick: () => {
11254
- void room.leave();
11255
- onLeave?.();
11334
+ if (choice === "leave") leaveNow();
11335
+ else if (choice === "assign_and_leave") setOpen("assign");
11336
+ else if (leave.policy.endConfirm) setOpen("confirm");
11337
+ else endNow();
11256
11338
  },
11257
- children: /* @__PURE__ */ jsx6(PhoneOffIcon, {})
11258
- }
11259
- )
11260
- ] })
11339
+ children: words[choice]
11340
+ },
11341
+ choice
11342
+ )) : null,
11343
+ open === "assign" ? leave.candidates.map((person) => /* @__PURE__ */ jsx6(
11344
+ "button",
11345
+ {
11346
+ type: "button",
11347
+ className: "mx-meet__menu-item",
11348
+ onClick: () => run(() => leave.assignAndLeave(person), true),
11349
+ children: person.displayName
11350
+ },
11351
+ String(person.identity)
11352
+ )) : null,
11353
+ open === "confirm" ? /* @__PURE__ */ jsxs3(Fragment, { children: [
11354
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__menu-item mx-meet__menu-item--danger", onClick: endNow, children: words.end_for_all }),
11355
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__menu-item", onClick: () => setOpen("menu"), children: "Cancel" })
11356
+ ] }) : null,
11357
+ failure !== null ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__menu-note", role: "alert", children: failure }) : null
11358
+ ] }) }) : null
11261
11359
  ] });
11262
11360
  }
11263
11361
  function useUnreadChat(open) {
@@ -12531,6 +12629,10 @@ function PreJoinBody({
12531
12629
  useEffect6(() => {
12532
12630
  void host.devices.refresh();
12533
12631
  }, [host]);
12632
+ const { checkElsewhere } = room;
12633
+ useEffect6(() => {
12634
+ void checkElsewhere(roomName);
12635
+ }, [checkElsewhere, roomName]);
12534
12636
  const joinWith = (secondDevice) => {
12535
12637
  setJoining(true);
12536
12638
  preview.release();
@@ -13861,6 +13963,7 @@ export {
13861
13963
  DATA_TOPICS,
13862
13964
  DEFAULT_CONNECTION_POLICY,
13863
13965
  DEFAULT_EFFECTS,
13966
+ DEFAULT_LEAVE_POLICY,
13864
13967
  DEFAULT_PAGE_SIZE,
13865
13968
  EMPTY_COLLAB,
13866
13969
  END_MEETING_NOT_SERVED,
@@ -13904,6 +14007,7 @@ export {
13904
14007
  NO_SHARE,
13905
14008
  ORGANIZATION_HEADER,
13906
14009
  PARTICIPANT_CAN_RECORD_ATTRIBUTE,
14010
+ PARTICIPANT_HAND_ATTRIBUTE,
13907
14011
  PARTICIPANT_ROLE_ATTRIBUTE,
13908
14012
  PARTICIPANT_STANDING_ATTRIBUTE,
13909
14013
  REACTION_TTL_MS,
@@ -14017,6 +14121,8 @@ export {
14017
14121
  isWaitingPhase,
14018
14122
  learnName,
14019
14123
  learnNames,
14124
+ leaveChoices,
14125
+ leavePolicyFrom,
14020
14126
  listenOnlyNotice,
14021
14127
  loadFollowUpDraft,
14022
14128
  loadGuestMeetingRecord,
@@ -14118,6 +14224,7 @@ export {
14118
14224
  useHandQueue,
14119
14225
  useHostControls,
14120
14226
  useIsHost,
14227
+ useLeave,
14121
14228
  useListenOnlyNotice,
14122
14229
  useLobby,
14123
14230
  useLocalMedia,