@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.cjs CHANGED
@@ -40,6 +40,7 @@ __export(react_exports, {
40
40
  DATA_TOPICS: () => DATA_TOPICS,
41
41
  DEFAULT_CONNECTION_POLICY: () => DEFAULT_CONNECTION_POLICY,
42
42
  DEFAULT_EFFECTS: () => DEFAULT_EFFECTS,
43
+ DEFAULT_LEAVE_POLICY: () => DEFAULT_LEAVE_POLICY,
43
44
  DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
44
45
  EMPTY_COLLAB: () => EMPTY_COLLAB,
45
46
  END_MEETING_NOT_SERVED: () => END_MEETING_NOT_SERVED,
@@ -83,6 +84,7 @@ __export(react_exports, {
83
84
  NO_SHARE: () => NO_SHARE,
84
85
  ORGANIZATION_HEADER: () => ORGANIZATION_HEADER,
85
86
  PARTICIPANT_CAN_RECORD_ATTRIBUTE: () => PARTICIPANT_CAN_RECORD_ATTRIBUTE,
87
+ PARTICIPANT_HAND_ATTRIBUTE: () => PARTICIPANT_HAND_ATTRIBUTE,
86
88
  PARTICIPANT_ROLE_ATTRIBUTE: () => PARTICIPANT_ROLE_ATTRIBUTE,
87
89
  PARTICIPANT_STANDING_ATTRIBUTE: () => PARTICIPANT_STANDING_ATTRIBUTE,
88
90
  REACTION_TTL_MS: () => REACTION_TTL_MS,
@@ -196,6 +198,8 @@ __export(react_exports, {
196
198
  isWaitingPhase: () => isWaitingPhase,
197
199
  learnName: () => learnName,
198
200
  learnNames: () => learnNames,
201
+ leaveChoices: () => leaveChoices,
202
+ leavePolicyFrom: () => leavePolicyFrom,
199
203
  listenOnlyNotice: () => listenOnlyNotice,
200
204
  loadFollowUpDraft: () => loadFollowUpDraft,
201
205
  loadGuestMeetingRecord: () => loadGuestMeetingRecord,
@@ -297,6 +301,7 @@ __export(react_exports, {
297
301
  useHandQueue: () => useHandQueue,
298
302
  useHostControls: () => useHostControls,
299
303
  useIsHost: () => useIsHost,
304
+ useLeave: () => useLeave,
300
305
  useListenOnlyNotice: () => useListenOnlyNotice,
301
306
  useLobby: () => useLobby,
302
307
  useLocalMedia: () => useLocalMedia,
@@ -1026,6 +1031,30 @@ function invalidResponse(operation, detail) {
1026
1031
  );
1027
1032
  }
1028
1033
 
1034
+ // src/core/leave.ts
1035
+ var DEFAULT_LEAVE_POLICY = { hostLeavePrompt: "leave_or_end", endConfirm: false };
1036
+ var PROMPTS = ["leave_or_end", "must_assign_or_end", "leave_only"];
1037
+ function leavePolicyFrom(raw) {
1038
+ if (typeof raw !== "object" || raw === null) return DEFAULT_LEAVE_POLICY;
1039
+ const value = raw;
1040
+ const prompt = value["host_leave_prompt"];
1041
+ const confirm = value["end_confirm"];
1042
+ return {
1043
+ hostLeavePrompt: PROMPTS.includes(prompt) ? prompt : DEFAULT_LEAVE_POLICY.hostLeavePrompt,
1044
+ endConfirm: typeof confirm === "boolean" ? confirm : DEFAULT_LEAVE_POLICY.endConfirm
1045
+ };
1046
+ }
1047
+ function leaveChoices(args) {
1048
+ if (args.role !== "host") return ["leave"];
1049
+ switch (args.policy.hostLeavePrompt) {
1050
+ case "must_assign_or_end":
1051
+ return args.othersPresent ? ["assign_and_leave", "end_for_all"] : ["leave", "end_for_all"];
1052
+ case "leave_only":
1053
+ case "leave_or_end":
1054
+ return ["leave", "end_for_all"];
1055
+ }
1056
+ }
1057
+
1029
1058
  // src/core/tokens.ts
1030
1059
  function createWebRoomTokenStorage(onDiagnostic) {
1031
1060
  const store = globalThis.localStorage;
@@ -1217,7 +1246,8 @@ function parseRoomToken(body, roomName) {
1217
1246
  pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
1218
1247
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
1219
1248
  observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
1220
- connectionPolicy: connectionPolicyFrom(payload["connection_policy"])
1249
+ connectionPolicy: connectionPolicyFrom(payload["connection_policy"]),
1250
+ leavePolicy: leavePolicyFrom(payload["leave_policy"])
1221
1251
  };
1222
1252
  }
1223
1253
  function createTokenClient(options) {
@@ -4117,6 +4147,10 @@ var EMPTY = {
4117
4147
  chatHistoryError: null,
4118
4148
  reactions: [],
4119
4149
  spotlight: null,
4150
+ actingHost: null,
4151
+ presenter: null,
4152
+ waitingCount: 0,
4153
+ leavePolicy: DEFAULT_LEAVE_POLICY,
4120
4154
  moderationNotice: null,
4121
4155
  actionNotice: null,
4122
4156
  privateChatRequest: null,
@@ -4178,8 +4212,6 @@ function createMeetStore(initial = {}) {
4178
4212
  },
4179
4213
  setParticipants(participants) {
4180
4214
  const names = learnNames(state.names, participants);
4181
- const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
4182
- const handTimes = new Map(state.participants.map((p) => [p.identity, p.handRaisedAt ?? null]));
4183
4215
  const quality = new Map(state.participants.map((p) => [p.identity, p.quality]));
4184
4216
  commit({
4185
4217
  ...state,
@@ -4192,8 +4224,6 @@ function createMeetStore(initial = {}) {
4192
4224
  // identity could reach a screen, and it is the place that asks the
4193
4225
  // name book. `roster-names.test.ts` fails if a token ever gets past it.
4194
4226
  displayName: displayNameFor(names, participant.identity, participant.displayName),
4195
- handRaised: hands.get(participant.identity) ?? participant.handRaised,
4196
- handRaisedAt: handTimes.get(participant.identity) ?? participant.handRaisedAt ?? null,
4197
4227
  quality: participant.quality === "unknown" ? quality.get(participant.identity) ?? "unknown" : participant.quality
4198
4228
  }))
4199
4229
  });
@@ -4216,20 +4246,6 @@ function createMeetStore(initial = {}) {
4216
4246
  )
4217
4247
  });
4218
4248
  },
4219
- setHand(identity, raised, at) {
4220
- commit({
4221
- ...state,
4222
- participants: state.participants.map(
4223
- (participant) => participant.identity === identity ? {
4224
- ...participant,
4225
- handRaised: raised,
4226
- // A hand that is ALREADY up keeps its place in the queue when
4227
- // it is re-announced (to a late joiner): the first time wins.
4228
- handRaisedAt: raised ? participant.handRaised && participant.handRaisedAt != null ? participant.handRaisedAt : at ?? Date.now() : null
4229
- } : participant
4230
- )
4231
- });
4232
- },
4233
4249
  addChat(message) {
4234
4250
  if (state.chat.some((existing) => existing.id === message.id)) return;
4235
4251
  const chat = [...state.chat, message];
@@ -4589,7 +4605,7 @@ function createMeetingSession(options) {
4589
4605
  })();
4590
4606
  return reading;
4591
4607
  }
4592
- async function postLeave(intent) {
4608
+ async function postLeave(intent, extra = {}) {
4593
4609
  const meeting = requireMeeting(`meet.${intent}`);
4594
4610
  const pass = options.resolvePass?.() ?? null;
4595
4611
  const response = await options.api.request({
@@ -4597,7 +4613,7 @@ function createMeetingSession(options) {
4597
4613
  method: "POST",
4598
4614
  requireAuth: false,
4599
4615
  operation: `meet.${intent}`,
4600
- body: { meeting_id: meeting.id, intent },
4616
+ body: { meeting_id: meeting.id, intent, ...extra },
4601
4617
  ...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
4602
4618
  });
4603
4619
  if (!response.ok) {
@@ -4611,7 +4627,7 @@ function createMeetingSession(options) {
4611
4627
  throw new MeetError(
4612
4628
  "transport",
4613
4629
  refusal2?.reason ?? `meet.${intent}: the server answered ${response.status}`,
4614
- refusal2?.remedy ?? "You are still in the waiting room. Try again."
4630
+ refusal2?.remedy ?? (intent === "withdraw" ? "You are still in the waiting room. Try again." : "Nothing was changed. Try again.")
4615
4631
  );
4616
4632
  }
4617
4633
  }
@@ -4835,6 +4851,10 @@ function createMeetingSession(options) {
4835
4851
  store.dispatch({ type: "admission", admission: "withdrawn" });
4836
4852
  hint();
4837
4853
  },
4854
+ async hostLeave(intent) {
4855
+ if (intent === "end_for_all") await postLeave("end_for_all");
4856
+ else await postLeave("assign_and_leave", { assign_to: intent.assignTo });
4857
+ },
4838
4858
  async askAgain() {
4839
4859
  await options.rejoin?.();
4840
4860
  },
@@ -5295,18 +5315,6 @@ var ROOM_EVENTS = {
5295
5315
  wrapUp: "matrx.meet.wrap_up",
5296
5316
  /** The meeting ended for everyone. Payload `{ meeting_id, ended_at, trigger }`. */
5297
5317
  ended: "matrx.meet.ended",
5298
- /**
5299
- * A `meet_recordings` row changed state. Payload
5300
- * `{ meeting_id, recording: { id, state, started_by } }`.
5301
- *
5302
- * 🚨 THIS IS THE ONLY WAY A GUEST LEARNS A RECORDING IS RUNNING. PreJoin
5303
- * promises every joiner an indicator, and `anon` holds no SELECT grant on
5304
- * `meet_recordings` (MRI-A10 settled that widening it is forbidden), so
5305
- * without this packet the promise was a sentence with nothing behind it: on
5306
- * production 2026-09-09 the guest lane showed no indicator at all while the
5307
- * host recorded (MRI-C8).
5308
- */
5309
- recording: "matrx.meet.recording",
5310
5318
  /**
5311
5319
  * A chat message to everyone was SAVED (Meet wave 2). Payload
5312
5320
  * `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
@@ -5359,8 +5367,6 @@ function applyRoomEvent(args) {
5359
5367
  return applyWrapUp(store, payload, now);
5360
5368
  case ROOM_EVENTS.ended:
5361
5369
  return applyEnded(store, payload);
5362
- case ROOM_EVENTS.recording:
5363
- return applyRecording(store, payload);
5364
5370
  case ROOM_EVENTS.chat:
5365
5371
  return applyChat(store, payload);
5366
5372
  case ROOM_EVENTS.moderation:
@@ -5440,9 +5446,9 @@ function applyWrapUp(store, payload, now) {
5440
5446
  store.announceWrapUp(announcement);
5441
5447
  return HANDLED;
5442
5448
  }
5443
- function applyRecording(store, payload) {
5449
+ function applyRecordingState(store, payload) {
5444
5450
  const row = object(payload["recording"]);
5445
- if (row === null) return ignored("a recording event with no `recording` row");
5451
+ if (row === null) return ignored("room state carries no recording");
5446
5452
  const meetingId = text2(payload["meeting_id"]);
5447
5453
  const meeting = store.snapshot().meeting;
5448
5454
  if (meeting !== null && meetingId !== null && meetingId !== meeting.id) {
@@ -5524,19 +5530,6 @@ function applyModeration(store, payload, now) {
5524
5530
  }
5525
5531
  const kind = text2(payload["kind"]);
5526
5532
  switch (kind) {
5527
- case "lower-hands": {
5528
- const raw = payload["identities"];
5529
- const everyone = raw === "*";
5530
- const named = new Set(
5531
- Array.isArray(raw) ? raw.filter((value) => typeof value === "string") : []
5532
- );
5533
- for (const participant of snapshot.participants) {
5534
- if (participant.handRaised && (everyone || named.has(participant.identity))) {
5535
- store.setHand(participant.identity, false);
5536
- }
5537
- }
5538
- return HANDLED;
5539
- }
5540
5533
  case "role": {
5541
5534
  const identity = text2(payload["identity"]);
5542
5535
  const role = text2(payload["role"]);
@@ -5630,7 +5623,6 @@ function noteTakerLabel(status) {
5630
5623
 
5631
5624
  // src/core/room-driver.ts
5632
5625
  var DATA_TOPICS = {
5633
- hand: "mx.hand",
5634
5626
  reaction: "mx.reaction",
5635
5627
  chat: "mx.chat"
5636
5628
  };
@@ -5654,6 +5646,11 @@ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
5654
5646
  return /administrator|policy/i.test(remedy) ? remedy : SCREEN_SHARE_BLOCKED_REMEDY;
5655
5647
  }
5656
5648
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
5649
+ var PARTICIPANT_HAND_ATTRIBUTE = "matrx.hand";
5650
+ function handFrom(value) {
5651
+ const at = value === void 0 || value.length === 0 ? Number.NaN : Number(value);
5652
+ return Number.isFinite(at) ? { handRaised: true, handRaisedAt: at } : { handRaised: false, handRaisedAt: null };
5653
+ }
5657
5654
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
5658
5655
  var CONNECTION_KIND_ATTRIBUTE = "matrx.participant.connection_kind";
5659
5656
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
@@ -5707,7 +5704,10 @@ function projectParticipant(driver, joinedAt) {
5707
5704
  cameraEnabled: driver.cameraEnabled,
5708
5705
  screenSharing: driver.screenSharing,
5709
5706
  speaking: false,
5710
- handRaised: false,
5707
+ // S8: a hand is the SERVER's (`hand_raised_at` on the person row, `/moderate raise_hand`),
5708
+ // replicated as the `matrx.hand` attribute: epoch ms of the raise, "" when down. It is
5709
+ // therefore delivered to late joiners and survives refresh and reconnect.
5710
+ ...handFrom(driver.attributes[PARTICIPANT_HAND_ATTRIBUTE]),
5711
5711
  quality: "unknown",
5712
5712
  joinedAt
5713
5713
  };
@@ -5837,40 +5837,38 @@ function createRoomEngine(options) {
5837
5837
  },
5838
5838
  participants(list) {
5839
5839
  const joinedAt = new Date(now()).toISOString();
5840
- const before = new Set(store.snapshot().participants.map((p) => p.identity));
5841
5840
  projectRoster(list, joinedAt);
5842
- const snapshot = store.snapshot();
5843
- const newcomer = snapshot.participants.some(
5844
- (participant) => !participant.isLocal && !before.has(participant.identity)
5845
- );
5846
- const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
5847
- if (newcomer && before.size > 0 && me?.handRaised === true) {
5848
- void publish(DATA_TOPICS.hand, { raised: true, raisedAt: me.handRaisedAt ?? now() }).catch(
5849
- () => void 0
5850
- );
5851
- }
5852
5841
  },
5853
5842
  participantLeft(who, reason) {
5854
5843
  departures.set(who, reason);
5855
5844
  },
5856
5845
  roomMetadata(metadata) {
5857
- let spotlight = null;
5846
+ let parsed = {};
5858
5847
  if (metadata.length > 0) {
5859
5848
  try {
5860
- const parsed = JSON.parse(metadata);
5861
- if (typeof parsed === "object" && parsed !== null) {
5862
- const value = parsed.spotlight;
5863
- if (typeof value === "string" && value.length > 0) spotlight = asParticipantIdentity(value);
5864
- }
5849
+ const value = JSON.parse(metadata);
5850
+ if (typeof value === "object" && value !== null) parsed = value;
5865
5851
  } catch {
5866
5852
  report(
5867
5853
  "warn",
5868
- "meet: the room's metadata is not JSON, so no spotlight was read from it",
5869
- "Only aidream writes room metadata; check services/meet/moderation.py."
5854
+ "meet: the room's metadata is not JSON, so the room state was not read from it",
5855
+ "Only aidream writes room metadata; check services/meet/room_state.py."
5870
5856
  );
5871
5857
  }
5872
5858
  }
5873
- if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
5859
+ const key = (name) => {
5860
+ const value = parsed[name];
5861
+ return typeof value === "string" && value.length > 0 ? asParticipantIdentity(personOf(value)) : null;
5862
+ };
5863
+ const waiting = typeof parsed["waiting_count"] === "number" ? parsed["waiting_count"] : 0;
5864
+ const before = store.snapshot();
5865
+ const next = { spotlight: key("spotlight"), actingHost: key("acting_host"), presenter: key("presenter"), waitingCount: waiting };
5866
+ if (before.spotlight !== next.spotlight || before.actingHost !== next.actingHost || before.presenter !== next.presenter || before.waitingCount !== next.waitingCount) {
5867
+ store.patch(next);
5868
+ }
5869
+ if (parsed["recording"] !== void 0 && parsed["recording"] !== null) {
5870
+ applyRecordingState(store, { meeting_id: parsed["meeting_id"], recording: parsed["recording"] });
5871
+ }
5874
5872
  },
5875
5873
  activeSpeakers(identities) {
5876
5874
  store.setSpeaking([...new Set(identities.map((who) => personOf(who)))]);
@@ -5972,13 +5970,6 @@ function createRoomEngine(options) {
5972
5970
  }
5973
5971
  const sender = personOf(from ?? "");
5974
5972
  switch (topic) {
5975
- case DATA_TOPICS.hand:
5976
- store.setHand(
5977
- sender,
5978
- envelope.raised === true,
5979
- typeof envelope.raisedAt === "number" ? envelope.raisedAt : now()
5980
- );
5981
- return;
5982
5973
  case DATA_TOPICS.reaction:
5983
5974
  if (typeof envelope.emoji === "string") {
5984
5975
  store.addReaction({
@@ -6077,6 +6068,7 @@ function createRoomEngine(options) {
6077
6068
  current = token;
6078
6069
  lastServerUrl = token.serverUrl;
6079
6070
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
6071
+ if (token.leavePolicy !== void 0) store.patch({ leavePolicy: token.leavePolicy });
6080
6072
  store.patch({
6081
6073
  localIdentity: token.identity,
6082
6074
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -6111,6 +6103,7 @@ function createRoomEngine(options) {
6111
6103
  return;
6112
6104
  }
6113
6105
  driver = created;
6106
+ if (token.connectionKind === "companion") created.setRemoteAudioPlayback?.(false);
6114
6107
  await created.connect(token.serverUrl, token.token);
6115
6108
  store.patch({ failure: null, connection: "stable" });
6116
6109
  store.dispatch({ type: "connected" });
@@ -6372,6 +6365,19 @@ function createRoomEngine(options) {
6372
6365
  }
6373
6366
  },
6374
6367
  leave: leaveRoom,
6368
+ async checkElsewhere(roomName) {
6369
+ const claim = options.tabClaim;
6370
+ const userId = identity.userId;
6371
+ if (claim === void 0 || userId === null) return false;
6372
+ const before = store.snapshot();
6373
+ if (before.phase !== "prejoin" || before.secondDevice !== null) return false;
6374
+ if (!await claim.presentElsewhere(String(roomName), `user:${String(userId)}`)) return false;
6375
+ const now2 = store.snapshot();
6376
+ if (now2.phase !== "prejoin" || now2.secondDevice !== null) return false;
6377
+ store.patch({ secondDevice: { rule: "ask_switch_or_join" } });
6378
+ report("info", "meet: this browser already has you in this meeting \u2014 offering the choice before Join");
6379
+ return true;
6380
+ },
6375
6381
  backToPrejoin() {
6376
6382
  store.dispatch({ type: "back_to_prejoin", click: true });
6377
6383
  },
@@ -6429,11 +6435,6 @@ function createRoomEngine(options) {
6429
6435
  dismissShareNotice() {
6430
6436
  if (store.snapshot().share.state === "refused") store.patch({ share: NO_SHARE });
6431
6437
  },
6432
- async raiseHand(raised) {
6433
- const local = store.snapshot().localIdentity;
6434
- if (local !== null) store.setHand(local, raised);
6435
- await publish(DATA_TOPICS.hand, { raised, raisedAt: now() });
6436
- },
6437
6438
  async react(emoji) {
6438
6439
  const local = store.snapshot().localIdentity;
6439
6440
  const id = `${local ?? "local"}-${now()}`;
@@ -6562,6 +6563,8 @@ var MODERATION_ACTIONS = [
6562
6563
  "ask_unmute",
6563
6564
  "ask_all_unmute",
6564
6565
  "spotlight",
6566
+ "present",
6567
+ "raise_hand",
6565
6568
  "lower_hand",
6566
6569
  "lower_all_hands",
6567
6570
  "make_cohost",
@@ -6605,7 +6608,8 @@ function createInRoomClient(options) {
6605
6608
  action: request.action,
6606
6609
  ...request.identity != null ? { identity: request.identity } : {},
6607
6610
  ...request.name !== void 0 ? { name: request.name } : {},
6608
- ...request.reason !== void 0 ? { reason: request.reason } : {}
6611
+ ...request.reason !== void 0 ? { reason: request.reason } : {},
6612
+ ...request.on !== void 0 ? { on: request.on } : {}
6609
6613
  },
6610
6614
  ...hdrs !== void 0 ? { headers: hdrs } : {}
6611
6615
  });
@@ -6921,6 +6925,7 @@ function createLiveKitDriver(handlers, options = {}) {
6921
6925
  projectRoster = roster;
6922
6926
  created.on(RoomEvent.TrackSubscribed, (track) => {
6923
6927
  if (track.kind !== Track.Kind.Audio || typeof document === "undefined") return;
6928
+ if (!remoteAudioPlayback) return;
6924
6929
  const element = track.attach();
6925
6930
  element.setAttribute("data-mx-meet-audio", "");
6926
6931
  audioSink().appendChild(element);
@@ -7005,6 +7010,7 @@ function createLiveKitDriver(handlers, options = {}) {
7005
7010
  let sources = null;
7006
7011
  let trackEvents = null;
7007
7012
  let audioContainer = null;
7013
+ let remoteAudioPlayback = true;
7008
7014
  function audioSink() {
7009
7015
  if (audioContainer !== null && audioContainer.isConnected) return audioContainer;
7010
7016
  const container = document.createElement("div");
@@ -7163,6 +7169,14 @@ function createLiveKitDriver(handlers, options = {}) {
7163
7169
  throw normalizeDeviceError(error, "meet.switchDevice");
7164
7170
  }
7165
7171
  },
7172
+ setRemoteAudioPlayback(enabled) {
7173
+ remoteAudioPlayback = enabled;
7174
+ if (enabled || audioContainer === null) return;
7175
+ for (const element of Array.from(audioContainer.querySelectorAll("[data-mx-meet-audio]"))) {
7176
+ if (element instanceof HTMLMediaElement) element.srcObject = null;
7177
+ element.remove();
7178
+ }
7179
+ },
7166
7180
  async startAudio() {
7167
7181
  const target = room;
7168
7182
  if (target === null) return;
@@ -8434,6 +8448,10 @@ function useRoom() {
8434
8448
  async (request) => requireEngine(engine, "join").join(request),
8435
8449
  [engine]
8436
8450
  ),
8451
+ checkElsewhere: (0, import_react4.useCallback)(
8452
+ async (roomName) => engine === null ? false : engine.checkElsewhere(roomName),
8453
+ [engine]
8454
+ ),
8437
8455
  leave: (0, import_react4.useCallback)(async () => requireEngine(engine, "leave").leave(), [engine]),
8438
8456
  toggleMicrophone: (0, import_react4.useCallback)(
8439
8457
  async () => requireEngine(engine, "toggleMicrophone").setMicrophoneEnabled(
@@ -8445,16 +8463,20 @@ function useRoom() {
8445
8463
  async () => requireEngine(engine, "toggleCamera").setCameraEnabled(!(local?.cameraEnabled ?? false)),
8446
8464
  [engine, local]
8447
8465
  ),
8448
- toggleScreenShare: (0, import_react4.useCallback)(
8449
- async () => requireEngine(engine, "toggleScreenShare").setScreenShareEnabled(
8450
- !(local?.screenSharing ?? false)
8451
- ),
8452
- [engine, local]
8453
- ),
8454
- toggleHand: (0, import_react4.useCallback)(
8455
- async () => requireEngine(engine, "toggleHand").raiseHand(!(local?.handRaised ?? false)),
8456
- [engine, local]
8457
- ),
8466
+ toggleScreenShare: (0, import_react4.useCallback)(async () => {
8467
+ const on = !(local?.screenSharing ?? false);
8468
+ await requireEngine(engine, "toggleScreenShare").setScreenShareEnabled(on);
8469
+ const sharing = host?.store.snapshot().participants.find((p) => p.isLocal)?.screenSharing ?? false;
8470
+ if (host !== null && sharing === on) {
8471
+ await host.inRoom.moderate({ action: "present", on }).catch(() => void 0);
8472
+ }
8473
+ }, [engine, host, local]),
8474
+ // S8: a hand is SERVER state (`/moderate raise_hand · lower_hand`, server time), replicated
8475
+ // back as the `matrx.hand` attribute — no client sets its own hand on the wire.
8476
+ toggleHand: (0, import_react4.useCallback)(async () => {
8477
+ if (host === null) throw noRuntime("toggleHand");
8478
+ await host.inRoom.moderate({ action: local?.handRaised === true ? "lower_hand" : "raise_hand" });
8479
+ }, [host, local]),
8458
8480
  react: (0, import_react4.useCallback)(
8459
8481
  async (emoji) => requireEngine(engine, "react").react(emoji),
8460
8482
  [engine]
@@ -8636,6 +8658,31 @@ function useWaiting() {
8636
8658
  askAgain: (0, import_react4.useCallback)(async () => requireSession(session, "askAgain").askAgain(), [session])
8637
8659
  };
8638
8660
  }
8661
+ function useLeave() {
8662
+ const host = useMeetHost();
8663
+ const snapshot = useMeetSnapshot();
8664
+ const session = host?.meetings ?? null;
8665
+ const engine = host?.engine ?? null;
8666
+ const policy = snapshot?.leavePolicy ?? DEFAULT_LEAVE_POLICY;
8667
+ const participants = snapshot?.participants ?? [];
8668
+ const me = participants.find((p) => p.isLocal);
8669
+ const candidates = participants.filter((p) => !p.isLocal && !p.isAgent && p.role !== "viewer");
8670
+ const choices = leaveChoices({ policy, role: me?.role ?? null, othersPresent: candidates.length > 0 });
8671
+ return {
8672
+ choices,
8673
+ policy,
8674
+ candidates,
8675
+ leave: (0, import_react4.useCallback)(async () => requireEngine(engine, "leave").leave(), [engine]),
8676
+ endForAll: (0, import_react4.useCallback)(async () => requireSession(session, "endForAll").hostLeave("end_for_all"), [session]),
8677
+ assignAndLeave: (0, import_react4.useCallback)(
8678
+ async (person) => {
8679
+ await requireSession(session, "assignAndLeave").hostLeave({ assignTo: String(person.identity) });
8680
+ await requireEngine(engine, "leave").leave();
8681
+ },
8682
+ [session, engine]
8683
+ )
8684
+ };
8685
+ }
8639
8686
  function useRecording() {
8640
8687
  const host = useMeetHost();
8641
8688
  const snapshot = useMeetSnapshot();
@@ -11559,19 +11606,75 @@ function ControlBar({ onLeave }) {
11559
11606
  ] }) : null
11560
11607
  ] }) }) : null
11561
11608
  ] }),
11562
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11563
- ControlButton,
11609
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(LeaveControl, { onLeave })
11610
+ ] })
11611
+ ] });
11612
+ }
11613
+ var LEAVE_WORDS = {
11614
+ leave_or_end: { leave: "Leave call", assign_and_leave: "Assign a new host", end_for_all: "End call for everyone" },
11615
+ must_assign_or_end: { leave: "Leave meeting", assign_and_leave: "Assign a new host", end_for_all: "End meeting for all" },
11616
+ leave_only: { leave: "Leave", assign_and_leave: "Assign a new host", end_for_all: "End meeting" }
11617
+ };
11618
+ function LeaveControl({ onLeave }) {
11619
+ const leave = useLeave();
11620
+ const [open, setOpen] = (0, import_react10.useState)("none");
11621
+ const [failure, setFailure] = (0, import_react10.useState)(null);
11622
+ const words = LEAVE_WORDS[leave.policy.hostLeavePrompt];
11623
+ const run = (work, leaving) => {
11624
+ setFailure(null);
11625
+ void work().then(() => {
11626
+ setOpen("none");
11627
+ if (leaving) onLeave?.();
11628
+ }).catch((error) => setFailure(error instanceof Error ? error.message : String(error)));
11629
+ };
11630
+ const leaveNow = () => run(leave.leave, true);
11631
+ const endNow = () => run(leave.endForAll, false);
11632
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "mx-meet__anchor", children: [
11633
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11634
+ ControlButton,
11635
+ {
11636
+ label: "Leave",
11637
+ danger: true,
11638
+ active: open !== "none",
11639
+ onClick: () => {
11640
+ if (leave.choices.length === 1 && leave.choices[0] === "leave") leaveNow();
11641
+ else setOpen(open === "none" ? "menu" : "none");
11642
+ },
11643
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PhoneOffIcon, {})
11644
+ }
11645
+ ),
11646
+ open !== "none" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Popover, { label: open === "assign" ? words.assign_and_leave : "Leave", onClose: () => setOpen("none"), children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__menu", "data-meet-leave-menu": leave.policy.hostLeavePrompt, children: [
11647
+ open === "menu" ? leave.choices.map((choice) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11648
+ "button",
11564
11649
  {
11565
- label: "Leave",
11566
- danger: true,
11650
+ type: "button",
11651
+ className: choice === "end_for_all" ? "mx-meet__menu-item mx-meet__menu-item--danger" : "mx-meet__menu-item",
11567
11652
  onClick: () => {
11568
- void room.leave();
11569
- onLeave?.();
11653
+ if (choice === "leave") leaveNow();
11654
+ else if (choice === "assign_and_leave") setOpen("assign");
11655
+ else if (leave.policy.endConfirm) setOpen("confirm");
11656
+ else endNow();
11570
11657
  },
11571
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PhoneOffIcon, {})
11572
- }
11573
- )
11574
- ] })
11658
+ children: words[choice]
11659
+ },
11660
+ choice
11661
+ )) : null,
11662
+ open === "assign" ? leave.candidates.map((person) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11663
+ "button",
11664
+ {
11665
+ type: "button",
11666
+ className: "mx-meet__menu-item",
11667
+ onClick: () => run(() => leave.assignAndLeave(person), true),
11668
+ children: person.displayName
11669
+ },
11670
+ String(person.identity)
11671
+ )) : null,
11672
+ open === "confirm" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11673
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__menu-item mx-meet__menu-item--danger", onClick: endNow, children: words.end_for_all }),
11674
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__menu-item", onClick: () => setOpen("menu"), children: "Cancel" })
11675
+ ] }) : null,
11676
+ failure !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__menu-note", role: "alert", children: failure }) : null
11677
+ ] }) }) : null
11575
11678
  ] });
11576
11679
  }
11577
11680
  function useUnreadChat(open) {
@@ -12845,6 +12948,10 @@ function PreJoinBody({
12845
12948
  (0, import_react10.useEffect)(() => {
12846
12949
  void host.devices.refresh();
12847
12950
  }, [host]);
12951
+ const { checkElsewhere } = room;
12952
+ (0, import_react10.useEffect)(() => {
12953
+ void checkElsewhere(roomName);
12954
+ }, [checkElsewhere, roomName]);
12848
12955
  const joinWith = (secondDevice) => {
12849
12956
  setJoining(true);
12850
12957
  preview.release();