@ai-matrx/meet 0.11.36 → 0.11.38

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;
@@ -1133,6 +1162,12 @@ var MEET_ROUTES = {
1133
1162
  state: "/api/v1/meet/state",
1134
1163
  /** S4: `withdraw` (stop waiting) or `leave` (close this tab); the pass may ride in the body. */
1135
1164
  leave: "/api/v1/meet/leave",
1165
+ /**
1166
+ * S8: a host's browser just called an admission DOOR (`meet_admit` · `meet_deny` ·
1167
+ * `meet_undeny` · `meet_admit_all`). Payload-free: the server republishes the room's
1168
+ * replicated state (the People badge's waiting count) from the database truth.
1169
+ */
1170
+ admissionChanged: "/api/v1/meet/admission/changed",
1136
1171
  /** S4: a pass holder (guests included) saw the recording notice, for this run. */
1137
1172
  consent: "/api/v1/meet/consent"
1138
1173
  };
@@ -1217,7 +1252,8 @@ function parseRoomToken(body, roomName) {
1217
1252
  pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
1218
1253
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
1219
1254
  observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
1220
- connectionPolicy: connectionPolicyFrom(payload["connection_policy"])
1255
+ connectionPolicy: connectionPolicyFrom(payload["connection_policy"]),
1256
+ leavePolicy: leavePolicyFrom(payload["leave_policy"])
1221
1257
  };
1222
1258
  }
1223
1259
  function createTokenClient(options) {
@@ -4117,6 +4153,10 @@ var EMPTY = {
4117
4153
  chatHistoryError: null,
4118
4154
  reactions: [],
4119
4155
  spotlight: null,
4156
+ actingHost: null,
4157
+ presenter: null,
4158
+ waitingCount: 0,
4159
+ leavePolicy: DEFAULT_LEAVE_POLICY,
4120
4160
  moderationNotice: null,
4121
4161
  actionNotice: null,
4122
4162
  privateChatRequest: null,
@@ -4178,8 +4218,6 @@ function createMeetStore(initial = {}) {
4178
4218
  },
4179
4219
  setParticipants(participants) {
4180
4220
  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
4221
  const quality = new Map(state.participants.map((p) => [p.identity, p.quality]));
4184
4222
  commit({
4185
4223
  ...state,
@@ -4192,8 +4230,6 @@ function createMeetStore(initial = {}) {
4192
4230
  // identity could reach a screen, and it is the place that asks the
4193
4231
  // name book. `roster-names.test.ts` fails if a token ever gets past it.
4194
4232
  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
4233
  quality: participant.quality === "unknown" ? quality.get(participant.identity) ?? "unknown" : participant.quality
4198
4234
  }))
4199
4235
  });
@@ -4216,20 +4252,6 @@ function createMeetStore(initial = {}) {
4216
4252
  )
4217
4253
  });
4218
4254
  },
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
4255
  addChat(message) {
4234
4256
  if (state.chat.some((existing) => existing.id === message.id)) return;
4235
4257
  const chat = [...state.chat, message];
@@ -4589,7 +4611,7 @@ function createMeetingSession(options) {
4589
4611
  })();
4590
4612
  return reading;
4591
4613
  }
4592
- async function postLeave(intent) {
4614
+ async function postLeave(intent, extra = {}) {
4593
4615
  const meeting = requireMeeting(`meet.${intent}`);
4594
4616
  const pass = options.resolvePass?.() ?? null;
4595
4617
  const response = await options.api.request({
@@ -4597,7 +4619,7 @@ function createMeetingSession(options) {
4597
4619
  method: "POST",
4598
4620
  requireAuth: false,
4599
4621
  operation: `meet.${intent}`,
4600
- body: { meeting_id: meeting.id, intent },
4622
+ body: { meeting_id: meeting.id, intent, ...extra },
4601
4623
  ...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
4602
4624
  });
4603
4625
  if (!response.ok) {
@@ -4611,13 +4633,41 @@ function createMeetingSession(options) {
4611
4633
  throw new MeetError(
4612
4634
  "transport",
4613
4635
  refusal2?.reason ?? `meet.${intent}: the server answered ${response.status}`,
4614
- refusal2?.remedy ?? "You are still in the waiting room. Try again."
4636
+ refusal2?.remedy ?? (intent === "withdraw" ? "You are still in the waiting room. Try again." : "Nothing was changed. Try again.")
4615
4637
  );
4616
4638
  }
4617
4639
  }
4618
4640
  function hint() {
4619
4641
  channel?.send(MEET_EVENTS.stateChanged, { v: null });
4620
4642
  }
4643
+ async function tellServerAdmissionChanged(operation) {
4644
+ const meeting = store.snapshot().meeting;
4645
+ if (meeting === null) return;
4646
+ const pass = options.resolvePass?.() ?? null;
4647
+ try {
4648
+ const response = await options.api.request({
4649
+ path: MEET_ROUTES.admissionChanged,
4650
+ method: "POST",
4651
+ requireAuth: false,
4652
+ operation,
4653
+ body: { meeting_id: meeting.id },
4654
+ ...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
4655
+ });
4656
+ if (!response.ok) {
4657
+ report(
4658
+ "warn",
4659
+ `${operation}: the server answered ${response.status} when told about the admission change`,
4660
+ "The decision was saved. The waiting count in the room may lag until the next change."
4661
+ );
4662
+ }
4663
+ } catch (error) {
4664
+ report(
4665
+ "warn",
4666
+ `${operation}: ${String(error)}`,
4667
+ "The decision was saved. The waiting count in the room may lag until the next change."
4668
+ );
4669
+ }
4670
+ }
4621
4671
  async function refreshQueue() {
4622
4672
  const meeting = store.snapshot().meeting;
4623
4673
  if (meeting === null || isGuest(identity) || !isHostRole(effectiveRole(store.snapshot()))) return;
@@ -4773,6 +4823,7 @@ function createMeetingSession(options) {
4773
4823
  decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
4774
4824
  decidedBy: by
4775
4825
  });
4826
+ await tellServerAdmissionChanged("meet.admit");
4776
4827
  hint();
4777
4828
  },
4778
4829
  async undeny(entry) {
@@ -4785,6 +4836,7 @@ function createMeetingSession(options) {
4785
4836
  byUserId: by
4786
4837
  });
4787
4838
  store.upsertLobby({ ...entry, state: decidedState(row.admissionState), decidedAt: (/* @__PURE__ */ new Date()).toISOString(), decidedBy: by });
4839
+ await tellServerAdmissionChanged("meet.undeny");
4788
4840
  hint();
4789
4841
  },
4790
4842
  async admitAll() {
@@ -4794,6 +4846,7 @@ function createMeetingSession(options) {
4794
4846
  for (const row of rows) {
4795
4847
  store.setAdmission(row.identity, row.admissionState, by);
4796
4848
  }
4849
+ await tellServerAdmissionChanged("meet.admitAll");
4797
4850
  hint();
4798
4851
  },
4799
4852
  startWaiting() {
@@ -4835,6 +4888,10 @@ function createMeetingSession(options) {
4835
4888
  store.dispatch({ type: "admission", admission: "withdrawn" });
4836
4889
  hint();
4837
4890
  },
4891
+ async hostLeave(intent) {
4892
+ if (intent === "end_for_all") await postLeave("end_for_all");
4893
+ else await postLeave("assign_and_leave", { assign_to: intent.assignTo });
4894
+ },
4838
4895
  async askAgain() {
4839
4896
  await options.rejoin?.();
4840
4897
  },
@@ -5295,18 +5352,6 @@ var ROOM_EVENTS = {
5295
5352
  wrapUp: "matrx.meet.wrap_up",
5296
5353
  /** The meeting ended for everyone. Payload `{ meeting_id, ended_at, trigger }`. */
5297
5354
  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
5355
  /**
5311
5356
  * A chat message to everyone was SAVED (Meet wave 2). Payload
5312
5357
  * `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
@@ -5359,8 +5404,6 @@ function applyRoomEvent(args) {
5359
5404
  return applyWrapUp(store, payload, now);
5360
5405
  case ROOM_EVENTS.ended:
5361
5406
  return applyEnded(store, payload);
5362
- case ROOM_EVENTS.recording:
5363
- return applyRecording(store, payload);
5364
5407
  case ROOM_EVENTS.chat:
5365
5408
  return applyChat(store, payload);
5366
5409
  case ROOM_EVENTS.moderation:
@@ -5440,9 +5483,9 @@ function applyWrapUp(store, payload, now) {
5440
5483
  store.announceWrapUp(announcement);
5441
5484
  return HANDLED;
5442
5485
  }
5443
- function applyRecording(store, payload) {
5486
+ function applyRecordingState(store, payload) {
5444
5487
  const row = object(payload["recording"]);
5445
- if (row === null) return ignored("a recording event with no `recording` row");
5488
+ if (row === null) return ignored("room state carries no recording");
5446
5489
  const meetingId = text2(payload["meeting_id"]);
5447
5490
  const meeting = store.snapshot().meeting;
5448
5491
  if (meeting !== null && meetingId !== null && meetingId !== meeting.id) {
@@ -5524,19 +5567,6 @@ function applyModeration(store, payload, now) {
5524
5567
  }
5525
5568
  const kind = text2(payload["kind"]);
5526
5569
  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
5570
  case "role": {
5541
5571
  const identity = text2(payload["identity"]);
5542
5572
  const role = text2(payload["role"]);
@@ -5630,7 +5660,6 @@ function noteTakerLabel(status) {
5630
5660
 
5631
5661
  // src/core/room-driver.ts
5632
5662
  var DATA_TOPICS = {
5633
- hand: "mx.hand",
5634
5663
  reaction: "mx.reaction",
5635
5664
  chat: "mx.chat"
5636
5665
  };
@@ -5654,6 +5683,11 @@ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
5654
5683
  return /administrator|policy/i.test(remedy) ? remedy : SCREEN_SHARE_BLOCKED_REMEDY;
5655
5684
  }
5656
5685
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
5686
+ var PARTICIPANT_HAND_ATTRIBUTE = "matrx.hand";
5687
+ function handFrom(value) {
5688
+ const at = value === void 0 || value.length === 0 ? Number.NaN : Number(value);
5689
+ return Number.isFinite(at) ? { handRaised: true, handRaisedAt: at } : { handRaised: false, handRaisedAt: null };
5690
+ }
5657
5691
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
5658
5692
  var CONNECTION_KIND_ATTRIBUTE = "matrx.participant.connection_kind";
5659
5693
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
@@ -5707,7 +5741,10 @@ function projectParticipant(driver, joinedAt) {
5707
5741
  cameraEnabled: driver.cameraEnabled,
5708
5742
  screenSharing: driver.screenSharing,
5709
5743
  speaking: false,
5710
- handRaised: false,
5744
+ // S8: a hand is the SERVER's (`hand_raised_at` on the person row, `/moderate raise_hand`),
5745
+ // replicated as the `matrx.hand` attribute: epoch ms of the raise, "" when down. It is
5746
+ // therefore delivered to late joiners and survives refresh and reconnect.
5747
+ ...handFrom(driver.attributes[PARTICIPANT_HAND_ATTRIBUTE]),
5711
5748
  quality: "unknown",
5712
5749
  joinedAt
5713
5750
  };
@@ -5837,40 +5874,38 @@ function createRoomEngine(options) {
5837
5874
  },
5838
5875
  participants(list) {
5839
5876
  const joinedAt = new Date(now()).toISOString();
5840
- const before = new Set(store.snapshot().participants.map((p) => p.identity));
5841
5877
  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
5878
  },
5853
5879
  participantLeft(who, reason) {
5854
5880
  departures.set(who, reason);
5855
5881
  },
5856
5882
  roomMetadata(metadata) {
5857
- let spotlight = null;
5883
+ let parsed = {};
5858
5884
  if (metadata.length > 0) {
5859
5885
  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
- }
5886
+ const value = JSON.parse(metadata);
5887
+ if (typeof value === "object" && value !== null) parsed = value;
5865
5888
  } catch {
5866
5889
  report(
5867
5890
  "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."
5891
+ "meet: the room's metadata is not JSON, so the room state was not read from it",
5892
+ "Only aidream writes room metadata; check services/meet/room_state.py."
5870
5893
  );
5871
5894
  }
5872
5895
  }
5873
- if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
5896
+ const key = (name) => {
5897
+ const value = parsed[name];
5898
+ return typeof value === "string" && value.length > 0 ? asParticipantIdentity(personOf(value)) : null;
5899
+ };
5900
+ const waiting = typeof parsed["waiting_count"] === "number" ? parsed["waiting_count"] : 0;
5901
+ const before = store.snapshot();
5902
+ const next = { spotlight: key("spotlight"), actingHost: key("acting_host"), presenter: key("presenter"), waitingCount: waiting };
5903
+ if (before.spotlight !== next.spotlight || before.actingHost !== next.actingHost || before.presenter !== next.presenter || before.waitingCount !== next.waitingCount) {
5904
+ store.patch(next);
5905
+ }
5906
+ if (parsed["recording"] !== void 0 && parsed["recording"] !== null) {
5907
+ applyRecordingState(store, { meeting_id: parsed["meeting_id"], recording: parsed["recording"] });
5908
+ }
5874
5909
  },
5875
5910
  activeSpeakers(identities) {
5876
5911
  store.setSpeaking([...new Set(identities.map((who) => personOf(who)))]);
@@ -5972,13 +6007,6 @@ function createRoomEngine(options) {
5972
6007
  }
5973
6008
  const sender = personOf(from ?? "");
5974
6009
  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
6010
  case DATA_TOPICS.reaction:
5983
6011
  if (typeof envelope.emoji === "string") {
5984
6012
  store.addReaction({
@@ -6077,6 +6105,7 @@ function createRoomEngine(options) {
6077
6105
  current = token;
6078
6106
  lastServerUrl = token.serverUrl;
6079
6107
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
6108
+ if (token.leavePolicy !== void 0) store.patch({ leavePolicy: token.leavePolicy });
6080
6109
  store.patch({
6081
6110
  localIdentity: token.identity,
6082
6111
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -6443,11 +6472,6 @@ function createRoomEngine(options) {
6443
6472
  dismissShareNotice() {
6444
6473
  if (store.snapshot().share.state === "refused") store.patch({ share: NO_SHARE });
6445
6474
  },
6446
- async raiseHand(raised) {
6447
- const local = store.snapshot().localIdentity;
6448
- if (local !== null) store.setHand(local, raised);
6449
- await publish(DATA_TOPICS.hand, { raised, raisedAt: now() });
6450
- },
6451
6475
  async react(emoji) {
6452
6476
  const local = store.snapshot().localIdentity;
6453
6477
  const id = `${local ?? "local"}-${now()}`;
@@ -6576,6 +6600,8 @@ var MODERATION_ACTIONS = [
6576
6600
  "ask_unmute",
6577
6601
  "ask_all_unmute",
6578
6602
  "spotlight",
6603
+ "present",
6604
+ "raise_hand",
6579
6605
  "lower_hand",
6580
6606
  "lower_all_hands",
6581
6607
  "make_cohost",
@@ -6606,7 +6632,22 @@ function createInRoomClient(options) {
6606
6632
  function refusal2(response, operation, remedy) {
6607
6633
  return refusalFromResponse(response, operation, remedy);
6608
6634
  }
6609
- return {
6635
+ let toldPresenting = false;
6636
+ let tellChain = Promise.resolve();
6637
+ store.subscribe(() => {
6638
+ const local = store.snapshot().participants.find((p) => p.isLocal);
6639
+ const sharing = local?.screenSharing === true;
6640
+ if (sharing === toldPresenting || local === void 0) return;
6641
+ toldPresenting = sharing;
6642
+ const on = sharing;
6643
+ tellChain = tellChain.then(() => client.moderate({ action: "present", on })).then(
6644
+ () => void 0,
6645
+ (error) => {
6646
+ console.warn(`[meet] the server did not take presenter=${String(on)}: ${String(error)}`);
6647
+ }
6648
+ );
6649
+ });
6650
+ const client = {
6610
6651
  async moderate(request) {
6611
6652
  const operation = `meet.moderate(${request.action})`;
6612
6653
  const hdrs = headers();
@@ -6619,7 +6660,8 @@ function createInRoomClient(options) {
6619
6660
  action: request.action,
6620
6661
  ...request.identity != null ? { identity: request.identity } : {},
6621
6662
  ...request.name !== void 0 ? { name: request.name } : {},
6622
- ...request.reason !== void 0 ? { reason: request.reason } : {}
6663
+ ...request.reason !== void 0 ? { reason: request.reason } : {},
6664
+ ...request.on !== void 0 ? { on: request.on } : {}
6623
6665
  },
6624
6666
  ...hdrs !== void 0 ? { headers: hdrs } : {}
6625
6667
  });
@@ -6810,6 +6852,7 @@ function createInRoomClient(options) {
6810
6852
  });
6811
6853
  }
6812
6854
  };
6855
+ return client;
6813
6856
  }
6814
6857
 
6815
6858
  // src/core/livekit-driver.ts
@@ -8473,16 +8516,16 @@ function useRoom() {
8473
8516
  async () => requireEngine(engine, "toggleCamera").setCameraEnabled(!(local?.cameraEnabled ?? false)),
8474
8517
  [engine, local]
8475
8518
  ),
8476
- toggleScreenShare: (0, import_react4.useCallback)(
8477
- async () => requireEngine(engine, "toggleScreenShare").setScreenShareEnabled(
8478
- !(local?.screenSharing ?? false)
8479
- ),
8480
- [engine, local]
8481
- ),
8482
- toggleHand: (0, import_react4.useCallback)(
8483
- async () => requireEngine(engine, "toggleHand").raiseHand(!(local?.handRaised ?? false)),
8484
- [engine, local]
8485
- ),
8519
+ toggleScreenShare: (0, import_react4.useCallback)(async () => {
8520
+ const on = !(local?.screenSharing ?? false);
8521
+ await requireEngine(engine, "toggleScreenShare").setScreenShareEnabled(on);
8522
+ }, [engine, local]),
8523
+ // S8: a hand is SERVER state (`/moderate raise_hand · lower_hand`, server time), replicated
8524
+ // back as the `matrx.hand` attribute — no client sets its own hand on the wire.
8525
+ toggleHand: (0, import_react4.useCallback)(async () => {
8526
+ if (host === null) throw noRuntime("toggleHand");
8527
+ await host.inRoom.moderate({ action: local?.handRaised === true ? "lower_hand" : "raise_hand" });
8528
+ }, [host, local]),
8486
8529
  react: (0, import_react4.useCallback)(
8487
8530
  async (emoji) => requireEngine(engine, "react").react(emoji),
8488
8531
  [engine]
@@ -8664,6 +8707,31 @@ function useWaiting() {
8664
8707
  askAgain: (0, import_react4.useCallback)(async () => requireSession(session, "askAgain").askAgain(), [session])
8665
8708
  };
8666
8709
  }
8710
+ function useLeave() {
8711
+ const host = useMeetHost();
8712
+ const snapshot = useMeetSnapshot();
8713
+ const session = host?.meetings ?? null;
8714
+ const engine = host?.engine ?? null;
8715
+ const policy = snapshot?.leavePolicy ?? DEFAULT_LEAVE_POLICY;
8716
+ const participants = snapshot?.participants ?? [];
8717
+ const me = participants.find((p) => p.isLocal);
8718
+ const candidates = participants.filter((p) => !p.isLocal && !p.isAgent && p.role !== "viewer");
8719
+ const choices = leaveChoices({ policy, role: me?.role ?? null, othersPresent: candidates.length > 0 });
8720
+ return {
8721
+ choices,
8722
+ policy,
8723
+ candidates,
8724
+ leave: (0, import_react4.useCallback)(async () => requireEngine(engine, "leave").leave(), [engine]),
8725
+ endForAll: (0, import_react4.useCallback)(async () => requireSession(session, "endForAll").hostLeave("end_for_all"), [session]),
8726
+ assignAndLeave: (0, import_react4.useCallback)(
8727
+ async (person) => {
8728
+ await requireSession(session, "assignAndLeave").hostLeave({ assignTo: String(person.identity) });
8729
+ await requireEngine(engine, "leave").leave();
8730
+ },
8731
+ [session, engine]
8732
+ )
8733
+ };
8734
+ }
8667
8735
  function useRecording() {
8668
8736
  const host = useMeetHost();
8669
8737
  const snapshot = useMeetSnapshot();
@@ -11587,19 +11655,75 @@ function ControlBar({ onLeave }) {
11587
11655
  ] }) : null
11588
11656
  ] }) }) : null
11589
11657
  ] }),
11590
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11591
- ControlButton,
11658
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(LeaveControl, { onLeave })
11659
+ ] })
11660
+ ] });
11661
+ }
11662
+ var LEAVE_WORDS = {
11663
+ leave_or_end: { leave: "Leave call", assign_and_leave: "Assign a new host", end_for_all: "End call for everyone" },
11664
+ must_assign_or_end: { leave: "Leave meeting", assign_and_leave: "Assign a new host", end_for_all: "End meeting for all" },
11665
+ leave_only: { leave: "Leave", assign_and_leave: "Assign a new host", end_for_all: "End meeting" }
11666
+ };
11667
+ function LeaveControl({ onLeave }) {
11668
+ const leave = useLeave();
11669
+ const [open, setOpen] = (0, import_react10.useState)("none");
11670
+ const [failure, setFailure] = (0, import_react10.useState)(null);
11671
+ const words = LEAVE_WORDS[leave.policy.hostLeavePrompt];
11672
+ const run = (work, leaving) => {
11673
+ setFailure(null);
11674
+ void work().then(() => {
11675
+ setOpen("none");
11676
+ if (leaving) onLeave?.();
11677
+ }).catch((error) => setFailure(error instanceof Error ? error.message : String(error)));
11678
+ };
11679
+ const leaveNow = () => run(leave.leave, true);
11680
+ const endNow = () => run(leave.endForAll, false);
11681
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "mx-meet__anchor", children: [
11682
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11683
+ ControlButton,
11684
+ {
11685
+ label: "Leave",
11686
+ danger: true,
11687
+ active: open !== "none",
11688
+ onClick: () => {
11689
+ if (leave.choices.length === 1 && leave.choices[0] === "leave") leaveNow();
11690
+ else setOpen(open === "none" ? "menu" : "none");
11691
+ },
11692
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PhoneOffIcon, {})
11693
+ }
11694
+ ),
11695
+ 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: [
11696
+ open === "menu" ? leave.choices.map((choice) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11697
+ "button",
11592
11698
  {
11593
- label: "Leave",
11594
- danger: true,
11699
+ type: "button",
11700
+ className: choice === "end_for_all" ? "mx-meet__menu-item mx-meet__menu-item--danger" : "mx-meet__menu-item",
11595
11701
  onClick: () => {
11596
- void room.leave();
11597
- onLeave?.();
11702
+ if (choice === "leave") leaveNow();
11703
+ else if (choice === "assign_and_leave") setOpen("assign");
11704
+ else if (leave.policy.endConfirm) setOpen("confirm");
11705
+ else endNow();
11598
11706
  },
11599
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PhoneOffIcon, {})
11600
- }
11601
- )
11602
- ] })
11707
+ children: words[choice]
11708
+ },
11709
+ choice
11710
+ )) : null,
11711
+ open === "assign" ? leave.candidates.map((person) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11712
+ "button",
11713
+ {
11714
+ type: "button",
11715
+ className: "mx-meet__menu-item",
11716
+ onClick: () => run(() => leave.assignAndLeave(person), true),
11717
+ children: person.displayName
11718
+ },
11719
+ String(person.identity)
11720
+ )) : null,
11721
+ open === "confirm" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11722
+ /* @__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 }),
11723
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__menu-item", onClick: () => setOpen("menu"), children: "Cancel" })
11724
+ ] }) : null,
11725
+ failure !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__menu-note", role: "alert", children: failure }) : null
11726
+ ] }) }) : null
11603
11727
  ] });
11604
11728
  }
11605
11729
  function useUnreadChat(open) {