@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.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;
@@ -817,6 +841,12 @@ var MEET_ROUTES = {
817
841
  state: "/api/v1/meet/state",
818
842
  /** S4: `withdraw` (stop waiting) or `leave` (close this tab); the pass may ride in the body. */
819
843
  leave: "/api/v1/meet/leave",
844
+ /**
845
+ * S8: a host's browser just called an admission DOOR (`meet_admit` · `meet_deny` ·
846
+ * `meet_undeny` · `meet_admit_all`). Payload-free: the server republishes the room's
847
+ * replicated state (the People badge's waiting count) from the database truth.
848
+ */
849
+ admissionChanged: "/api/v1/meet/admission/changed",
820
850
  /** S4: a pass holder (guests included) saw the recording notice, for this run. */
821
851
  consent: "/api/v1/meet/consent"
822
852
  };
@@ -901,7 +931,8 @@ function parseRoomToken(body, roomName) {
901
931
  pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
902
932
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
903
933
  observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
904
- connectionPolicy: connectionPolicyFrom(payload["connection_policy"])
934
+ connectionPolicy: connectionPolicyFrom(payload["connection_policy"]),
935
+ leavePolicy: leavePolicyFrom(payload["leave_policy"])
905
936
  };
906
937
  }
907
938
  function createTokenClient(options) {
@@ -3801,6 +3832,10 @@ var EMPTY = {
3801
3832
  chatHistoryError: null,
3802
3833
  reactions: [],
3803
3834
  spotlight: null,
3835
+ actingHost: null,
3836
+ presenter: null,
3837
+ waitingCount: 0,
3838
+ leavePolicy: DEFAULT_LEAVE_POLICY,
3804
3839
  moderationNotice: null,
3805
3840
  actionNotice: null,
3806
3841
  privateChatRequest: null,
@@ -3862,8 +3897,6 @@ function createMeetStore(initial = {}) {
3862
3897
  },
3863
3898
  setParticipants(participants) {
3864
3899
  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
3900
  const quality = new Map(state.participants.map((p) => [p.identity, p.quality]));
3868
3901
  commit({
3869
3902
  ...state,
@@ -3876,8 +3909,6 @@ function createMeetStore(initial = {}) {
3876
3909
  // identity could reach a screen, and it is the place that asks the
3877
3910
  // name book. `roster-names.test.ts` fails if a token ever gets past it.
3878
3911
  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
3912
  quality: participant.quality === "unknown" ? quality.get(participant.identity) ?? "unknown" : participant.quality
3882
3913
  }))
3883
3914
  });
@@ -3900,20 +3931,6 @@ function createMeetStore(initial = {}) {
3900
3931
  )
3901
3932
  });
3902
3933
  },
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
3934
  addChat(message) {
3918
3935
  if (state.chat.some((existing) => existing.id === message.id)) return;
3919
3936
  const chat = [...state.chat, message];
@@ -4273,7 +4290,7 @@ function createMeetingSession(options) {
4273
4290
  })();
4274
4291
  return reading;
4275
4292
  }
4276
- async function postLeave(intent) {
4293
+ async function postLeave(intent, extra = {}) {
4277
4294
  const meeting = requireMeeting(`meet.${intent}`);
4278
4295
  const pass = options.resolvePass?.() ?? null;
4279
4296
  const response = await options.api.request({
@@ -4281,7 +4298,7 @@ function createMeetingSession(options) {
4281
4298
  method: "POST",
4282
4299
  requireAuth: false,
4283
4300
  operation: `meet.${intent}`,
4284
- body: { meeting_id: meeting.id, intent },
4301
+ body: { meeting_id: meeting.id, intent, ...extra },
4285
4302
  ...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
4286
4303
  });
4287
4304
  if (!response.ok) {
@@ -4295,13 +4312,41 @@ function createMeetingSession(options) {
4295
4312
  throw new MeetError(
4296
4313
  "transport",
4297
4314
  refusal2?.reason ?? `meet.${intent}: the server answered ${response.status}`,
4298
- refusal2?.remedy ?? "You are still in the waiting room. Try again."
4315
+ refusal2?.remedy ?? (intent === "withdraw" ? "You are still in the waiting room. Try again." : "Nothing was changed. Try again.")
4299
4316
  );
4300
4317
  }
4301
4318
  }
4302
4319
  function hint() {
4303
4320
  channel?.send(MEET_EVENTS.stateChanged, { v: null });
4304
4321
  }
4322
+ async function tellServerAdmissionChanged(operation) {
4323
+ const meeting = store.snapshot().meeting;
4324
+ if (meeting === null) return;
4325
+ const pass = options.resolvePass?.() ?? null;
4326
+ try {
4327
+ const response = await options.api.request({
4328
+ path: MEET_ROUTES.admissionChanged,
4329
+ method: "POST",
4330
+ requireAuth: false,
4331
+ operation,
4332
+ body: { meeting_id: meeting.id },
4333
+ ...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
4334
+ });
4335
+ if (!response.ok) {
4336
+ report(
4337
+ "warn",
4338
+ `${operation}: the server answered ${response.status} when told about the admission change`,
4339
+ "The decision was saved. The waiting count in the room may lag until the next change."
4340
+ );
4341
+ }
4342
+ } catch (error) {
4343
+ report(
4344
+ "warn",
4345
+ `${operation}: ${String(error)}`,
4346
+ "The decision was saved. The waiting count in the room may lag until the next change."
4347
+ );
4348
+ }
4349
+ }
4305
4350
  async function refreshQueue() {
4306
4351
  const meeting = store.snapshot().meeting;
4307
4352
  if (meeting === null || isGuest(identity) || !isHostRole(effectiveRole(store.snapshot()))) return;
@@ -4457,6 +4502,7 @@ function createMeetingSession(options) {
4457
4502
  decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
4458
4503
  decidedBy: by
4459
4504
  });
4505
+ await tellServerAdmissionChanged("meet.admit");
4460
4506
  hint();
4461
4507
  },
4462
4508
  async undeny(entry) {
@@ -4469,6 +4515,7 @@ function createMeetingSession(options) {
4469
4515
  byUserId: by
4470
4516
  });
4471
4517
  store.upsertLobby({ ...entry, state: decidedState(row.admissionState), decidedAt: (/* @__PURE__ */ new Date()).toISOString(), decidedBy: by });
4518
+ await tellServerAdmissionChanged("meet.undeny");
4472
4519
  hint();
4473
4520
  },
4474
4521
  async admitAll() {
@@ -4478,6 +4525,7 @@ function createMeetingSession(options) {
4478
4525
  for (const row of rows) {
4479
4526
  store.setAdmission(row.identity, row.admissionState, by);
4480
4527
  }
4528
+ await tellServerAdmissionChanged("meet.admitAll");
4481
4529
  hint();
4482
4530
  },
4483
4531
  startWaiting() {
@@ -4519,6 +4567,10 @@ function createMeetingSession(options) {
4519
4567
  store.dispatch({ type: "admission", admission: "withdrawn" });
4520
4568
  hint();
4521
4569
  },
4570
+ async hostLeave(intent) {
4571
+ if (intent === "end_for_all") await postLeave("end_for_all");
4572
+ else await postLeave("assign_and_leave", { assign_to: intent.assignTo });
4573
+ },
4522
4574
  async askAgain() {
4523
4575
  await options.rejoin?.();
4524
4576
  },
@@ -4981,18 +5033,6 @@ var ROOM_EVENTS = {
4981
5033
  wrapUp: "matrx.meet.wrap_up",
4982
5034
  /** The meeting ended for everyone. Payload `{ meeting_id, ended_at, trigger }`. */
4983
5035
  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
5036
  /**
4997
5037
  * A chat message to everyone was SAVED (Meet wave 2). Payload
4998
5038
  * `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
@@ -5045,8 +5085,6 @@ function applyRoomEvent(args) {
5045
5085
  return applyWrapUp(store, payload, now);
5046
5086
  case ROOM_EVENTS.ended:
5047
5087
  return applyEnded(store, payload);
5048
- case ROOM_EVENTS.recording:
5049
- return applyRecording(store, payload);
5050
5088
  case ROOM_EVENTS.chat:
5051
5089
  return applyChat(store, payload);
5052
5090
  case ROOM_EVENTS.moderation:
@@ -5126,9 +5164,9 @@ function applyWrapUp(store, payload, now) {
5126
5164
  store.announceWrapUp(announcement);
5127
5165
  return HANDLED;
5128
5166
  }
5129
- function applyRecording(store, payload) {
5167
+ function applyRecordingState(store, payload) {
5130
5168
  const row = object(payload["recording"]);
5131
- if (row === null) return ignored("a recording event with no `recording` row");
5169
+ if (row === null) return ignored("room state carries no recording");
5132
5170
  const meetingId = text2(payload["meeting_id"]);
5133
5171
  const meeting = store.snapshot().meeting;
5134
5172
  if (meeting !== null && meetingId !== null && meetingId !== meeting.id) {
@@ -5210,19 +5248,6 @@ function applyModeration(store, payload, now) {
5210
5248
  }
5211
5249
  const kind = text2(payload["kind"]);
5212
5250
  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
5251
  case "role": {
5227
5252
  const identity = text2(payload["identity"]);
5228
5253
  const role = text2(payload["role"]);
@@ -5316,7 +5341,6 @@ function noteTakerLabel(status) {
5316
5341
 
5317
5342
  // src/core/room-driver.ts
5318
5343
  var DATA_TOPICS = {
5319
- hand: "mx.hand",
5320
5344
  reaction: "mx.reaction",
5321
5345
  chat: "mx.chat"
5322
5346
  };
@@ -5340,6 +5364,11 @@ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
5340
5364
  return /administrator|policy/i.test(remedy) ? remedy : SCREEN_SHARE_BLOCKED_REMEDY;
5341
5365
  }
5342
5366
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
5367
+ var PARTICIPANT_HAND_ATTRIBUTE = "matrx.hand";
5368
+ function handFrom(value) {
5369
+ const at = value === void 0 || value.length === 0 ? Number.NaN : Number(value);
5370
+ return Number.isFinite(at) ? { handRaised: true, handRaisedAt: at } : { handRaised: false, handRaisedAt: null };
5371
+ }
5343
5372
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
5344
5373
  var CONNECTION_KIND_ATTRIBUTE = "matrx.participant.connection_kind";
5345
5374
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
@@ -5393,7 +5422,10 @@ function projectParticipant(driver, joinedAt) {
5393
5422
  cameraEnabled: driver.cameraEnabled,
5394
5423
  screenSharing: driver.screenSharing,
5395
5424
  speaking: false,
5396
- handRaised: false,
5425
+ // S8: a hand is the SERVER's (`hand_raised_at` on the person row, `/moderate raise_hand`),
5426
+ // replicated as the `matrx.hand` attribute: epoch ms of the raise, "" when down. It is
5427
+ // therefore delivered to late joiners and survives refresh and reconnect.
5428
+ ...handFrom(driver.attributes[PARTICIPANT_HAND_ATTRIBUTE]),
5397
5429
  quality: "unknown",
5398
5430
  joinedAt
5399
5431
  };
@@ -5523,40 +5555,38 @@ function createRoomEngine(options) {
5523
5555
  },
5524
5556
  participants(list) {
5525
5557
  const joinedAt = new Date(now()).toISOString();
5526
- const before = new Set(store.snapshot().participants.map((p) => p.identity));
5527
5558
  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
5559
  },
5539
5560
  participantLeft(who, reason) {
5540
5561
  departures.set(who, reason);
5541
5562
  },
5542
5563
  roomMetadata(metadata) {
5543
- let spotlight = null;
5564
+ let parsed = {};
5544
5565
  if (metadata.length > 0) {
5545
5566
  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
- }
5567
+ const value = JSON.parse(metadata);
5568
+ if (typeof value === "object" && value !== null) parsed = value;
5551
5569
  } catch {
5552
5570
  report(
5553
5571
  "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."
5572
+ "meet: the room's metadata is not JSON, so the room state was not read from it",
5573
+ "Only aidream writes room metadata; check services/meet/room_state.py."
5556
5574
  );
5557
5575
  }
5558
5576
  }
5559
- if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
5577
+ const key = (name) => {
5578
+ const value = parsed[name];
5579
+ return typeof value === "string" && value.length > 0 ? asParticipantIdentity(personOf(value)) : null;
5580
+ };
5581
+ const waiting = typeof parsed["waiting_count"] === "number" ? parsed["waiting_count"] : 0;
5582
+ const before = store.snapshot();
5583
+ const next = { spotlight: key("spotlight"), actingHost: key("acting_host"), presenter: key("presenter"), waitingCount: waiting };
5584
+ if (before.spotlight !== next.spotlight || before.actingHost !== next.actingHost || before.presenter !== next.presenter || before.waitingCount !== next.waitingCount) {
5585
+ store.patch(next);
5586
+ }
5587
+ if (parsed["recording"] !== void 0 && parsed["recording"] !== null) {
5588
+ applyRecordingState(store, { meeting_id: parsed["meeting_id"], recording: parsed["recording"] });
5589
+ }
5560
5590
  },
5561
5591
  activeSpeakers(identities) {
5562
5592
  store.setSpeaking([...new Set(identities.map((who) => personOf(who)))]);
@@ -5658,13 +5688,6 @@ function createRoomEngine(options) {
5658
5688
  }
5659
5689
  const sender = personOf(from ?? "");
5660
5690
  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
5691
  case DATA_TOPICS.reaction:
5669
5692
  if (typeof envelope.emoji === "string") {
5670
5693
  store.addReaction({
@@ -5763,6 +5786,7 @@ function createRoomEngine(options) {
5763
5786
  current = token;
5764
5787
  lastServerUrl = token.serverUrl;
5765
5788
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
5789
+ if (token.leavePolicy !== void 0) store.patch({ leavePolicy: token.leavePolicy });
5766
5790
  store.patch({
5767
5791
  localIdentity: token.identity,
5768
5792
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -6129,11 +6153,6 @@ function createRoomEngine(options) {
6129
6153
  dismissShareNotice() {
6130
6154
  if (store.snapshot().share.state === "refused") store.patch({ share: NO_SHARE });
6131
6155
  },
6132
- async raiseHand(raised) {
6133
- const local = store.snapshot().localIdentity;
6134
- if (local !== null) store.setHand(local, raised);
6135
- await publish(DATA_TOPICS.hand, { raised, raisedAt: now() });
6136
- },
6137
6156
  async react(emoji) {
6138
6157
  const local = store.snapshot().localIdentity;
6139
6158
  const id = `${local ?? "local"}-${now()}`;
@@ -6262,6 +6281,8 @@ var MODERATION_ACTIONS = [
6262
6281
  "ask_unmute",
6263
6282
  "ask_all_unmute",
6264
6283
  "spotlight",
6284
+ "present",
6285
+ "raise_hand",
6265
6286
  "lower_hand",
6266
6287
  "lower_all_hands",
6267
6288
  "make_cohost",
@@ -6292,7 +6313,22 @@ function createInRoomClient(options) {
6292
6313
  function refusal2(response, operation, remedy) {
6293
6314
  return refusalFromResponse(response, operation, remedy);
6294
6315
  }
6295
- return {
6316
+ let toldPresenting = false;
6317
+ let tellChain = Promise.resolve();
6318
+ store.subscribe(() => {
6319
+ const local = store.snapshot().participants.find((p) => p.isLocal);
6320
+ const sharing = local?.screenSharing === true;
6321
+ if (sharing === toldPresenting || local === void 0) return;
6322
+ toldPresenting = sharing;
6323
+ const on = sharing;
6324
+ tellChain = tellChain.then(() => client.moderate({ action: "present", on })).then(
6325
+ () => void 0,
6326
+ (error) => {
6327
+ console.warn(`[meet] the server did not take presenter=${String(on)}: ${String(error)}`);
6328
+ }
6329
+ );
6330
+ });
6331
+ const client = {
6296
6332
  async moderate(request) {
6297
6333
  const operation = `meet.moderate(${request.action})`;
6298
6334
  const hdrs = headers();
@@ -6305,7 +6341,8 @@ function createInRoomClient(options) {
6305
6341
  action: request.action,
6306
6342
  ...request.identity != null ? { identity: request.identity } : {},
6307
6343
  ...request.name !== void 0 ? { name: request.name } : {},
6308
- ...request.reason !== void 0 ? { reason: request.reason } : {}
6344
+ ...request.reason !== void 0 ? { reason: request.reason } : {},
6345
+ ...request.on !== void 0 ? { on: request.on } : {}
6309
6346
  },
6310
6347
  ...hdrs !== void 0 ? { headers: hdrs } : {}
6311
6348
  });
@@ -6496,6 +6533,7 @@ function createInRoomClient(options) {
6496
6533
  });
6497
6534
  }
6498
6535
  };
6536
+ return client;
6499
6537
  }
6500
6538
 
6501
6539
  // src/core/livekit-driver.ts
@@ -8159,16 +8197,16 @@ function useRoom() {
8159
8197
  async () => requireEngine(engine, "toggleCamera").setCameraEnabled(!(local?.cameraEnabled ?? false)),
8160
8198
  [engine, local]
8161
8199
  ),
8162
- toggleScreenShare: useCallback2(
8163
- async () => requireEngine(engine, "toggleScreenShare").setScreenShareEnabled(
8164
- !(local?.screenSharing ?? false)
8165
- ),
8166
- [engine, local]
8167
- ),
8168
- toggleHand: useCallback2(
8169
- async () => requireEngine(engine, "toggleHand").raiseHand(!(local?.handRaised ?? false)),
8170
- [engine, local]
8171
- ),
8200
+ toggleScreenShare: useCallback2(async () => {
8201
+ const on = !(local?.screenSharing ?? false);
8202
+ await requireEngine(engine, "toggleScreenShare").setScreenShareEnabled(on);
8203
+ }, [engine, local]),
8204
+ // S8: a hand is SERVER state (`/moderate raise_hand · lower_hand`, server time), replicated
8205
+ // back as the `matrx.hand` attribute — no client sets its own hand on the wire.
8206
+ toggleHand: useCallback2(async () => {
8207
+ if (host === null) throw noRuntime("toggleHand");
8208
+ await host.inRoom.moderate({ action: local?.handRaised === true ? "lower_hand" : "raise_hand" });
8209
+ }, [host, local]),
8172
8210
  react: useCallback2(
8173
8211
  async (emoji) => requireEngine(engine, "react").react(emoji),
8174
8212
  [engine]
@@ -8350,6 +8388,31 @@ function useWaiting() {
8350
8388
  askAgain: useCallback2(async () => requireSession(session, "askAgain").askAgain(), [session])
8351
8389
  };
8352
8390
  }
8391
+ function useLeave() {
8392
+ const host = useMeetHost();
8393
+ const snapshot = useMeetSnapshot();
8394
+ const session = host?.meetings ?? null;
8395
+ const engine = host?.engine ?? null;
8396
+ const policy = snapshot?.leavePolicy ?? DEFAULT_LEAVE_POLICY;
8397
+ const participants = snapshot?.participants ?? [];
8398
+ const me = participants.find((p) => p.isLocal);
8399
+ const candidates = participants.filter((p) => !p.isLocal && !p.isAgent && p.role !== "viewer");
8400
+ const choices = leaveChoices({ policy, role: me?.role ?? null, othersPresent: candidates.length > 0 });
8401
+ return {
8402
+ choices,
8403
+ policy,
8404
+ candidates,
8405
+ leave: useCallback2(async () => requireEngine(engine, "leave").leave(), [engine]),
8406
+ endForAll: useCallback2(async () => requireSession(session, "endForAll").hostLeave("end_for_all"), [session]),
8407
+ assignAndLeave: useCallback2(
8408
+ async (person) => {
8409
+ await requireSession(session, "assignAndLeave").hostLeave({ assignTo: String(person.identity) });
8410
+ await requireEngine(engine, "leave").leave();
8411
+ },
8412
+ [session, engine]
8413
+ )
8414
+ };
8415
+ }
8353
8416
  function useRecording() {
8354
8417
  const host = useMeetHost();
8355
8418
  const snapshot = useMeetSnapshot();
@@ -11273,19 +11336,75 @@ function ControlBar({ onLeave }) {
11273
11336
  ] }) : null
11274
11337
  ] }) }) : null
11275
11338
  ] }),
11276
- /* @__PURE__ */ jsx6(
11277
- ControlButton,
11339
+ /* @__PURE__ */ jsx6(LeaveControl, { onLeave })
11340
+ ] })
11341
+ ] });
11342
+ }
11343
+ var LEAVE_WORDS = {
11344
+ leave_or_end: { leave: "Leave call", assign_and_leave: "Assign a new host", end_for_all: "End call for everyone" },
11345
+ must_assign_or_end: { leave: "Leave meeting", assign_and_leave: "Assign a new host", end_for_all: "End meeting for all" },
11346
+ leave_only: { leave: "Leave", assign_and_leave: "Assign a new host", end_for_all: "End meeting" }
11347
+ };
11348
+ function LeaveControl({ onLeave }) {
11349
+ const leave = useLeave();
11350
+ const [open, setOpen] = useState4("none");
11351
+ const [failure, setFailure] = useState4(null);
11352
+ const words = LEAVE_WORDS[leave.policy.hostLeavePrompt];
11353
+ const run = (work, leaving) => {
11354
+ setFailure(null);
11355
+ void work().then(() => {
11356
+ setOpen("none");
11357
+ if (leaving) onLeave?.();
11358
+ }).catch((error) => setFailure(error instanceof Error ? error.message : String(error)));
11359
+ };
11360
+ const leaveNow = () => run(leave.leave, true);
11361
+ const endNow = () => run(leave.endForAll, false);
11362
+ return /* @__PURE__ */ jsxs3("span", { className: "mx-meet__anchor", children: [
11363
+ /* @__PURE__ */ jsx6(
11364
+ ControlButton,
11365
+ {
11366
+ label: "Leave",
11367
+ danger: true,
11368
+ active: open !== "none",
11369
+ onClick: () => {
11370
+ if (leave.choices.length === 1 && leave.choices[0] === "leave") leaveNow();
11371
+ else setOpen(open === "none" ? "menu" : "none");
11372
+ },
11373
+ children: /* @__PURE__ */ jsx6(PhoneOffIcon, {})
11374
+ }
11375
+ ),
11376
+ 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: [
11377
+ open === "menu" ? leave.choices.map((choice) => /* @__PURE__ */ jsx6(
11378
+ "button",
11278
11379
  {
11279
- label: "Leave",
11280
- danger: true,
11380
+ type: "button",
11381
+ className: choice === "end_for_all" ? "mx-meet__menu-item mx-meet__menu-item--danger" : "mx-meet__menu-item",
11281
11382
  onClick: () => {
11282
- void room.leave();
11283
- onLeave?.();
11383
+ if (choice === "leave") leaveNow();
11384
+ else if (choice === "assign_and_leave") setOpen("assign");
11385
+ else if (leave.policy.endConfirm) setOpen("confirm");
11386
+ else endNow();
11284
11387
  },
11285
- children: /* @__PURE__ */ jsx6(PhoneOffIcon, {})
11286
- }
11287
- )
11288
- ] })
11388
+ children: words[choice]
11389
+ },
11390
+ choice
11391
+ )) : null,
11392
+ open === "assign" ? leave.candidates.map((person) => /* @__PURE__ */ jsx6(
11393
+ "button",
11394
+ {
11395
+ type: "button",
11396
+ className: "mx-meet__menu-item",
11397
+ onClick: () => run(() => leave.assignAndLeave(person), true),
11398
+ children: person.displayName
11399
+ },
11400
+ String(person.identity)
11401
+ )) : null,
11402
+ open === "confirm" ? /* @__PURE__ */ jsxs3(Fragment, { children: [
11403
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__menu-item mx-meet__menu-item--danger", onClick: endNow, children: words.end_for_all }),
11404
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__menu-item", onClick: () => setOpen("menu"), children: "Cancel" })
11405
+ ] }) : null,
11406
+ failure !== null ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__menu-note", role: "alert", children: failure }) : null
11407
+ ] }) }) : null
11289
11408
  ] });
11290
11409
  }
11291
11410
  function useUnreadChat(open) {
@@ -13893,6 +14012,7 @@ export {
13893
14012
  DATA_TOPICS,
13894
14013
  DEFAULT_CONNECTION_POLICY,
13895
14014
  DEFAULT_EFFECTS,
14015
+ DEFAULT_LEAVE_POLICY,
13896
14016
  DEFAULT_PAGE_SIZE,
13897
14017
  EMPTY_COLLAB,
13898
14018
  END_MEETING_NOT_SERVED,
@@ -13936,6 +14056,7 @@ export {
13936
14056
  NO_SHARE,
13937
14057
  ORGANIZATION_HEADER,
13938
14058
  PARTICIPANT_CAN_RECORD_ATTRIBUTE,
14059
+ PARTICIPANT_HAND_ATTRIBUTE,
13939
14060
  PARTICIPANT_ROLE_ATTRIBUTE,
13940
14061
  PARTICIPANT_STANDING_ATTRIBUTE,
13941
14062
  REACTION_TTL_MS,
@@ -14049,6 +14170,8 @@ export {
14049
14170
  isWaitingPhase,
14050
14171
  learnName,
14051
14172
  learnNames,
14173
+ leaveChoices,
14174
+ leavePolicyFrom,
14052
14175
  listenOnlyNotice,
14053
14176
  loadFollowUpDraft,
14054
14177
  loadGuestMeetingRecord,
@@ -14150,6 +14273,7 @@ export {
14150
14273
  useHandQueue,
14151
14274
  useHostControls,
14152
14275
  useIsHost,
14276
+ useLeave,
14153
14277
  useListenOnlyNotice,
14154
14278
  useLobby,
14155
14279
  useLocalMedia,