@ai-matrx/meet 0.11.36 → 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/index.cjs CHANGED
@@ -38,6 +38,7 @@ __export(src_exports, {
38
38
  DATA_TOPICS: () => DATA_TOPICS,
39
39
  DEFAULT_CONNECTION_POLICY: () => DEFAULT_CONNECTION_POLICY,
40
40
  DEFAULT_EFFECTS: () => DEFAULT_EFFECTS,
41
+ DEFAULT_LEAVE_POLICY: () => DEFAULT_LEAVE_POLICY,
41
42
  DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
42
43
  EMPTY_COLLAB: () => EMPTY_COLLAB,
43
44
  END_MEETING_NOT_SERVED: () => END_MEETING_NOT_SERVED,
@@ -72,6 +73,7 @@ __export(src_exports, {
72
73
  NO_SHARE: () => NO_SHARE,
73
74
  ORGANIZATION_HEADER: () => ORGANIZATION_HEADER,
74
75
  PARTICIPANT_CAN_RECORD_ATTRIBUTE: () => PARTICIPANT_CAN_RECORD_ATTRIBUTE,
76
+ PARTICIPANT_HAND_ATTRIBUTE: () => PARTICIPANT_HAND_ATTRIBUTE,
75
77
  PARTICIPANT_ROLE_ATTRIBUTE: () => PARTICIPANT_ROLE_ATTRIBUTE,
76
78
  PARTICIPANT_STANDING_ATTRIBUTE: () => PARTICIPANT_STANDING_ATTRIBUTE,
77
79
  REACTION_TTL_MS: () => REACTION_TTL_MS,
@@ -184,6 +186,8 @@ __export(src_exports, {
184
186
  isWaitingPhase: () => isWaitingPhase,
185
187
  learnName: () => learnName,
186
188
  learnNames: () => learnNames,
189
+ leaveChoices: () => leaveChoices,
190
+ leavePolicyFrom: () => leavePolicyFrom,
187
191
  listenOnlyNotice: () => listenOnlyNotice,
188
192
  loadFollowUpDraft: () => loadFollowUpDraft,
189
193
  loadGuestMeetingRecord: () => loadGuestMeetingRecord,
@@ -831,6 +835,30 @@ function mergeGhosts(live, ghosts, at) {
831
835
  return { roster: [...live, ...extra], kept };
832
836
  }
833
837
 
838
+ // src/core/leave.ts
839
+ var DEFAULT_LEAVE_POLICY = { hostLeavePrompt: "leave_or_end", endConfirm: false };
840
+ var PROMPTS = ["leave_or_end", "must_assign_or_end", "leave_only"];
841
+ function leavePolicyFrom(raw) {
842
+ if (typeof raw !== "object" || raw === null) return DEFAULT_LEAVE_POLICY;
843
+ const value = raw;
844
+ const prompt = value["host_leave_prompt"];
845
+ const confirm = value["end_confirm"];
846
+ return {
847
+ hostLeavePrompt: PROMPTS.includes(prompt) ? prompt : DEFAULT_LEAVE_POLICY.hostLeavePrompt,
848
+ endConfirm: typeof confirm === "boolean" ? confirm : DEFAULT_LEAVE_POLICY.endConfirm
849
+ };
850
+ }
851
+ function leaveChoices(args) {
852
+ if (args.role !== "host") return ["leave"];
853
+ switch (args.policy.hostLeavePrompt) {
854
+ case "must_assign_or_end":
855
+ return args.othersPresent ? ["assign_and_leave", "end_for_all"] : ["leave", "end_for_all"];
856
+ case "leave_only":
857
+ case "leave_or_end":
858
+ return ["leave", "end_for_all"];
859
+ }
860
+ }
861
+
834
862
  // src/core/tokens.ts
835
863
  function createWebRoomTokenStorage(onDiagnostic) {
836
864
  const store = globalThis.localStorage;
@@ -1022,7 +1050,8 @@ function parseRoomToken(body, roomName) {
1022
1050
  pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
1023
1051
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
1024
1052
  observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
1025
- connectionPolicy: connectionPolicyFrom(payload["connection_policy"])
1053
+ connectionPolicy: connectionPolicyFrom(payload["connection_policy"]),
1054
+ leavePolicy: leavePolicyFrom(payload["leave_policy"])
1026
1055
  };
1027
1056
  }
1028
1057
  function createTokenClient(options) {
@@ -3721,6 +3750,10 @@ var EMPTY = {
3721
3750
  chatHistoryError: null,
3722
3751
  reactions: [],
3723
3752
  spotlight: null,
3753
+ actingHost: null,
3754
+ presenter: null,
3755
+ waitingCount: 0,
3756
+ leavePolicy: DEFAULT_LEAVE_POLICY,
3724
3757
  moderationNotice: null,
3725
3758
  actionNotice: null,
3726
3759
  privateChatRequest: null,
@@ -3782,8 +3815,6 @@ function createMeetStore(initial = {}) {
3782
3815
  },
3783
3816
  setParticipants(participants) {
3784
3817
  const names = learnNames(state.names, participants);
3785
- const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
3786
- const handTimes = new Map(state.participants.map((p) => [p.identity, p.handRaisedAt ?? null]));
3787
3818
  const quality = new Map(state.participants.map((p) => [p.identity, p.quality]));
3788
3819
  commit({
3789
3820
  ...state,
@@ -3796,8 +3827,6 @@ function createMeetStore(initial = {}) {
3796
3827
  // identity could reach a screen, and it is the place that asks the
3797
3828
  // name book. `roster-names.test.ts` fails if a token ever gets past it.
3798
3829
  displayName: displayNameFor(names, participant.identity, participant.displayName),
3799
- handRaised: hands.get(participant.identity) ?? participant.handRaised,
3800
- handRaisedAt: handTimes.get(participant.identity) ?? participant.handRaisedAt ?? null,
3801
3830
  quality: participant.quality === "unknown" ? quality.get(participant.identity) ?? "unknown" : participant.quality
3802
3831
  }))
3803
3832
  });
@@ -3820,20 +3849,6 @@ function createMeetStore(initial = {}) {
3820
3849
  )
3821
3850
  });
3822
3851
  },
3823
- setHand(identity, raised, at) {
3824
- commit({
3825
- ...state,
3826
- participants: state.participants.map(
3827
- (participant) => participant.identity === identity ? {
3828
- ...participant,
3829
- handRaised: raised,
3830
- // A hand that is ALREADY up keeps its place in the queue when
3831
- // it is re-announced (to a late joiner): the first time wins.
3832
- handRaisedAt: raised ? participant.handRaised && participant.handRaisedAt != null ? participant.handRaisedAt : at ?? Date.now() : null
3833
- } : participant
3834
- )
3835
- });
3836
- },
3837
3852
  addChat(message) {
3838
3853
  if (state.chat.some((existing) => existing.id === message.id)) return;
3839
3854
  const chat = [...state.chat, message];
@@ -3982,18 +3997,6 @@ var ROOM_EVENTS = {
3982
3997
  wrapUp: "matrx.meet.wrap_up",
3983
3998
  /** The meeting ended for everyone. Payload `{ meeting_id, ended_at, trigger }`. */
3984
3999
  ended: "matrx.meet.ended",
3985
- /**
3986
- * A `meet_recordings` row changed state. Payload
3987
- * `{ meeting_id, recording: { id, state, started_by } }`.
3988
- *
3989
- * 🚨 THIS IS THE ONLY WAY A GUEST LEARNS A RECORDING IS RUNNING. PreJoin
3990
- * promises every joiner an indicator, and `anon` holds no SELECT grant on
3991
- * `meet_recordings` (MRI-A10 settled that widening it is forbidden), so
3992
- * without this packet the promise was a sentence with nothing behind it: on
3993
- * production 2026-09-09 the guest lane showed no indicator at all while the
3994
- * host recorded (MRI-C8).
3995
- */
3996
- recording: "matrx.meet.recording",
3997
4000
  /**
3998
4001
  * A chat message to everyone was SAVED (Meet wave 2). Payload
3999
4002
  * `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
@@ -4046,8 +4049,6 @@ function applyRoomEvent(args) {
4046
4049
  return applyWrapUp(store, payload, now);
4047
4050
  case ROOM_EVENTS.ended:
4048
4051
  return applyEnded(store, payload);
4049
- case ROOM_EVENTS.recording:
4050
- return applyRecording(store, payload);
4051
4052
  case ROOM_EVENTS.chat:
4052
4053
  return applyChat(store, payload);
4053
4054
  case ROOM_EVENTS.moderation:
@@ -4127,9 +4128,9 @@ function applyWrapUp(store, payload, now) {
4127
4128
  store.announceWrapUp(announcement);
4128
4129
  return HANDLED;
4129
4130
  }
4130
- function applyRecording(store, payload) {
4131
+ function applyRecordingState(store, payload) {
4131
4132
  const row = object(payload["recording"]);
4132
- if (row === null) return ignored("a recording event with no `recording` row");
4133
+ if (row === null) return ignored("room state carries no recording");
4133
4134
  const meetingId = text2(payload["meeting_id"]);
4134
4135
  const meeting = store.snapshot().meeting;
4135
4136
  if (meeting !== null && meetingId !== null && meetingId !== meeting.id) {
@@ -4211,19 +4212,6 @@ function applyModeration(store, payload, now) {
4211
4212
  }
4212
4213
  const kind = text2(payload["kind"]);
4213
4214
  switch (kind) {
4214
- case "lower-hands": {
4215
- const raw = payload["identities"];
4216
- const everyone = raw === "*";
4217
- const named = new Set(
4218
- Array.isArray(raw) ? raw.filter((value) => typeof value === "string") : []
4219
- );
4220
- for (const participant of snapshot.participants) {
4221
- if (participant.handRaised && (everyone || named.has(participant.identity))) {
4222
- store.setHand(participant.identity, false);
4223
- }
4224
- }
4225
- return HANDLED;
4226
- }
4227
4215
  case "role": {
4228
4216
  const identity = text2(payload["identity"]);
4229
4217
  const role = text2(payload["role"]);
@@ -4615,7 +4603,6 @@ function gridColumns(count2) {
4615
4603
 
4616
4604
  // src/core/room-driver.ts
4617
4605
  var DATA_TOPICS = {
4618
- hand: "mx.hand",
4619
4606
  reaction: "mx.reaction",
4620
4607
  chat: "mx.chat"
4621
4608
  };
@@ -5249,7 +5236,7 @@ function createMeetingSession(options) {
5249
5236
  })();
5250
5237
  return reading;
5251
5238
  }
5252
- async function postLeave(intent) {
5239
+ async function postLeave(intent, extra = {}) {
5253
5240
  const meeting = requireMeeting(`meet.${intent}`);
5254
5241
  const pass = options.resolvePass?.() ?? null;
5255
5242
  const response = await options.api.request({
@@ -5257,7 +5244,7 @@ function createMeetingSession(options) {
5257
5244
  method: "POST",
5258
5245
  requireAuth: false,
5259
5246
  operation: `meet.${intent}`,
5260
- body: { meeting_id: meeting.id, intent },
5247
+ body: { meeting_id: meeting.id, intent, ...extra },
5261
5248
  ...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
5262
5249
  });
5263
5250
  if (!response.ok) {
@@ -5271,7 +5258,7 @@ function createMeetingSession(options) {
5271
5258
  throw new MeetError(
5272
5259
  "transport",
5273
5260
  refusal2?.reason ?? `meet.${intent}: the server answered ${response.status}`,
5274
- refusal2?.remedy ?? "You are still in the waiting room. Try again."
5261
+ refusal2?.remedy ?? (intent === "withdraw" ? "You are still in the waiting room. Try again." : "Nothing was changed. Try again.")
5275
5262
  );
5276
5263
  }
5277
5264
  }
@@ -5495,6 +5482,10 @@ function createMeetingSession(options) {
5495
5482
  store.dispatch({ type: "admission", admission: "withdrawn" });
5496
5483
  hint();
5497
5484
  },
5485
+ async hostLeave(intent) {
5486
+ if (intent === "end_for_all") await postLeave("end_for_all");
5487
+ else await postLeave("assign_and_leave", { assign_to: intent.assignTo });
5488
+ },
5498
5489
  async askAgain() {
5499
5490
  await options.rejoin?.();
5500
5491
  },
@@ -6456,6 +6447,11 @@ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
6456
6447
  return /administrator|policy/i.test(remedy) ? remedy : SCREEN_SHARE_BLOCKED_REMEDY;
6457
6448
  }
6458
6449
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
6450
+ var PARTICIPANT_HAND_ATTRIBUTE = "matrx.hand";
6451
+ function handFrom(value) {
6452
+ const at = value === void 0 || value.length === 0 ? Number.NaN : Number(value);
6453
+ return Number.isFinite(at) ? { handRaised: true, handRaisedAt: at } : { handRaised: false, handRaisedAt: null };
6454
+ }
6459
6455
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
6460
6456
  var CONNECTION_KIND_ATTRIBUTE = "matrx.participant.connection_kind";
6461
6457
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
@@ -6509,7 +6505,10 @@ function projectParticipant(driver, joinedAt) {
6509
6505
  cameraEnabled: driver.cameraEnabled,
6510
6506
  screenSharing: driver.screenSharing,
6511
6507
  speaking: false,
6512
- handRaised: false,
6508
+ // S8: a hand is the SERVER's (`hand_raised_at` on the person row, `/moderate raise_hand`),
6509
+ // replicated as the `matrx.hand` attribute: epoch ms of the raise, "" when down. It is
6510
+ // therefore delivered to late joiners and survives refresh and reconnect.
6511
+ ...handFrom(driver.attributes[PARTICIPANT_HAND_ATTRIBUTE]),
6513
6512
  quality: "unknown",
6514
6513
  joinedAt
6515
6514
  };
@@ -6639,40 +6638,38 @@ function createRoomEngine(options) {
6639
6638
  },
6640
6639
  participants(list) {
6641
6640
  const joinedAt = new Date(now()).toISOString();
6642
- const before = new Set(store.snapshot().participants.map((p) => p.identity));
6643
6641
  projectRoster(list, joinedAt);
6644
- const snapshot = store.snapshot();
6645
- const newcomer = snapshot.participants.some(
6646
- (participant) => !participant.isLocal && !before.has(participant.identity)
6647
- );
6648
- const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
6649
- if (newcomer && before.size > 0 && me?.handRaised === true) {
6650
- void publish(DATA_TOPICS.hand, { raised: true, raisedAt: me.handRaisedAt ?? now() }).catch(
6651
- () => void 0
6652
- );
6653
- }
6654
6642
  },
6655
6643
  participantLeft(who, reason) {
6656
6644
  departures.set(who, reason);
6657
6645
  },
6658
6646
  roomMetadata(metadata) {
6659
- let spotlight = null;
6647
+ let parsed = {};
6660
6648
  if (metadata.length > 0) {
6661
6649
  try {
6662
- const parsed = JSON.parse(metadata);
6663
- if (typeof parsed === "object" && parsed !== null) {
6664
- const value = parsed.spotlight;
6665
- if (typeof value === "string" && value.length > 0) spotlight = asParticipantIdentity(value);
6666
- }
6650
+ const value = JSON.parse(metadata);
6651
+ if (typeof value === "object" && value !== null) parsed = value;
6667
6652
  } catch {
6668
6653
  report(
6669
6654
  "warn",
6670
- "meet: the room's metadata is not JSON, so no spotlight was read from it",
6671
- "Only aidream writes room metadata; check services/meet/moderation.py."
6655
+ "meet: the room's metadata is not JSON, so the room state was not read from it",
6656
+ "Only aidream writes room metadata; check services/meet/room_state.py."
6672
6657
  );
6673
6658
  }
6674
6659
  }
6675
- if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
6660
+ const key = (name) => {
6661
+ const value = parsed[name];
6662
+ return typeof value === "string" && value.length > 0 ? asParticipantIdentity(personOf(value)) : null;
6663
+ };
6664
+ const waiting = typeof parsed["waiting_count"] === "number" ? parsed["waiting_count"] : 0;
6665
+ const before = store.snapshot();
6666
+ const next = { spotlight: key("spotlight"), actingHost: key("acting_host"), presenter: key("presenter"), waitingCount: waiting };
6667
+ if (before.spotlight !== next.spotlight || before.actingHost !== next.actingHost || before.presenter !== next.presenter || before.waitingCount !== next.waitingCount) {
6668
+ store.patch(next);
6669
+ }
6670
+ if (parsed["recording"] !== void 0 && parsed["recording"] !== null) {
6671
+ applyRecordingState(store, { meeting_id: parsed["meeting_id"], recording: parsed["recording"] });
6672
+ }
6676
6673
  },
6677
6674
  activeSpeakers(identities) {
6678
6675
  store.setSpeaking([...new Set(identities.map((who) => personOf(who)))]);
@@ -6774,13 +6771,6 @@ function createRoomEngine(options) {
6774
6771
  }
6775
6772
  const sender = personOf(from ?? "");
6776
6773
  switch (topic) {
6777
- case DATA_TOPICS.hand:
6778
- store.setHand(
6779
- sender,
6780
- envelope.raised === true,
6781
- typeof envelope.raisedAt === "number" ? envelope.raisedAt : now()
6782
- );
6783
- return;
6784
6774
  case DATA_TOPICS.reaction:
6785
6775
  if (typeof envelope.emoji === "string") {
6786
6776
  store.addReaction({
@@ -6879,6 +6869,7 @@ function createRoomEngine(options) {
6879
6869
  current = token;
6880
6870
  lastServerUrl = token.serverUrl;
6881
6871
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
6872
+ if (token.leavePolicy !== void 0) store.patch({ leavePolicy: token.leavePolicy });
6882
6873
  store.patch({
6883
6874
  localIdentity: token.identity,
6884
6875
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -7245,11 +7236,6 @@ function createRoomEngine(options) {
7245
7236
  dismissShareNotice() {
7246
7237
  if (store.snapshot().share.state === "refused") store.patch({ share: NO_SHARE });
7247
7238
  },
7248
- async raiseHand(raised) {
7249
- const local = store.snapshot().localIdentity;
7250
- if (local !== null) store.setHand(local, raised);
7251
- await publish(DATA_TOPICS.hand, { raised, raisedAt: now() });
7252
- },
7253
7239
  async react(emoji) {
7254
7240
  const local = store.snapshot().localIdentity;
7255
7241
  const id = `${local ?? "local"}-${now()}`;
@@ -7378,6 +7364,8 @@ var MODERATION_ACTIONS = [
7378
7364
  "ask_unmute",
7379
7365
  "ask_all_unmute",
7380
7366
  "spotlight",
7367
+ "present",
7368
+ "raise_hand",
7381
7369
  "lower_hand",
7382
7370
  "lower_all_hands",
7383
7371
  "make_cohost",
@@ -7421,7 +7409,8 @@ function createInRoomClient(options) {
7421
7409
  action: request.action,
7422
7410
  ...request.identity != null ? { identity: request.identity } : {},
7423
7411
  ...request.name !== void 0 ? { name: request.name } : {},
7424
- ...request.reason !== void 0 ? { reason: request.reason } : {}
7412
+ ...request.reason !== void 0 ? { reason: request.reason } : {},
7413
+ ...request.on !== void 0 ? { on: request.on } : {}
7425
7414
  },
7426
7415
  ...hdrs !== void 0 ? { headers: hdrs } : {}
7427
7416
  });