@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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog — `@ai-matrx/meet`
2
2
 
3
+ ## 0.11.37
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/meet/v0.11.36..npm/meet/v0.11.37 -- apps/shared/meet`).
7
+ No source changes intended and no consumer action required.
8
+
9
+ ## 0.11.36
10
+
11
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
12
+ `git diff npm/meet/v0.11.35..npm/meet/v0.11.36 -- apps/shared/meet`).
13
+ No source changes intended and no consumer action required.
14
+
3
15
  ## 0.11.35
4
16
 
5
17
  Automatic changed-only republish (docs/metadata drift since the last tag — see
package/dist/core.cjs CHANGED
@@ -38,6 +38,7 @@ __export(core_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(core_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(core_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
  };
@@ -4757,6 +4744,7 @@ function createLiveKitDriver(handlers, options = {}) {
4757
4744
  projectRoster = roster;
4758
4745
  created.on(RoomEvent.TrackSubscribed, (track) => {
4759
4746
  if (track.kind !== Track.Kind.Audio || typeof document === "undefined") return;
4747
+ if (!remoteAudioPlayback) return;
4760
4748
  const element = track.attach();
4761
4749
  element.setAttribute("data-mx-meet-audio", "");
4762
4750
  audioSink().appendChild(element);
@@ -4841,6 +4829,7 @@ function createLiveKitDriver(handlers, options = {}) {
4841
4829
  let sources = null;
4842
4830
  let trackEvents = null;
4843
4831
  let audioContainer = null;
4832
+ let remoteAudioPlayback = true;
4844
4833
  function audioSink() {
4845
4834
  if (audioContainer !== null && audioContainer.isConnected) return audioContainer;
4846
4835
  const container = document.createElement("div");
@@ -4999,6 +4988,14 @@ function createLiveKitDriver(handlers, options = {}) {
4999
4988
  throw normalizeDeviceError(error, "meet.switchDevice");
5000
4989
  }
5001
4990
  },
4991
+ setRemoteAudioPlayback(enabled) {
4992
+ remoteAudioPlayback = enabled;
4993
+ if (enabled || audioContainer === null) return;
4994
+ for (const element of Array.from(audioContainer.querySelectorAll("[data-mx-meet-audio]"))) {
4995
+ if (element instanceof HTMLMediaElement) element.srcObject = null;
4996
+ element.remove();
4997
+ }
4998
+ },
5002
4999
  async startAudio() {
5003
5000
  const target = room;
5004
5001
  if (target === null) return;
@@ -5239,7 +5236,7 @@ function createMeetingSession(options) {
5239
5236
  })();
5240
5237
  return reading;
5241
5238
  }
5242
- async function postLeave(intent) {
5239
+ async function postLeave(intent, extra = {}) {
5243
5240
  const meeting = requireMeeting(`meet.${intent}`);
5244
5241
  const pass = options.resolvePass?.() ?? null;
5245
5242
  const response = await options.api.request({
@@ -5247,7 +5244,7 @@ function createMeetingSession(options) {
5247
5244
  method: "POST",
5248
5245
  requireAuth: false,
5249
5246
  operation: `meet.${intent}`,
5250
- body: { meeting_id: meeting.id, intent },
5247
+ body: { meeting_id: meeting.id, intent, ...extra },
5251
5248
  ...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
5252
5249
  });
5253
5250
  if (!response.ok) {
@@ -5261,7 +5258,7 @@ function createMeetingSession(options) {
5261
5258
  throw new MeetError(
5262
5259
  "transport",
5263
5260
  refusal2?.reason ?? `meet.${intent}: the server answered ${response.status}`,
5264
- 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.")
5265
5262
  );
5266
5263
  }
5267
5264
  }
@@ -5485,6 +5482,10 @@ function createMeetingSession(options) {
5485
5482
  store.dispatch({ type: "admission", admission: "withdrawn" });
5486
5483
  hint();
5487
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
+ },
5488
5489
  async askAgain() {
5489
5490
  await options.rejoin?.();
5490
5491
  },
@@ -6446,6 +6447,11 @@ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
6446
6447
  return /administrator|policy/i.test(remedy) ? remedy : SCREEN_SHARE_BLOCKED_REMEDY;
6447
6448
  }
6448
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
+ }
6449
6455
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
6450
6456
  var CONNECTION_KIND_ATTRIBUTE = "matrx.participant.connection_kind";
6451
6457
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
@@ -6499,7 +6505,10 @@ function projectParticipant(driver, joinedAt) {
6499
6505
  cameraEnabled: driver.cameraEnabled,
6500
6506
  screenSharing: driver.screenSharing,
6501
6507
  speaking: false,
6502
- 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]),
6503
6512
  quality: "unknown",
6504
6513
  joinedAt
6505
6514
  };
@@ -6629,40 +6638,38 @@ function createRoomEngine(options) {
6629
6638
  },
6630
6639
  participants(list) {
6631
6640
  const joinedAt = new Date(now()).toISOString();
6632
- const before = new Set(store.snapshot().participants.map((p) => p.identity));
6633
6641
  projectRoster(list, joinedAt);
6634
- const snapshot = store.snapshot();
6635
- const newcomer = snapshot.participants.some(
6636
- (participant) => !participant.isLocal && !before.has(participant.identity)
6637
- );
6638
- const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
6639
- if (newcomer && before.size > 0 && me?.handRaised === true) {
6640
- void publish(DATA_TOPICS.hand, { raised: true, raisedAt: me.handRaisedAt ?? now() }).catch(
6641
- () => void 0
6642
- );
6643
- }
6644
6642
  },
6645
6643
  participantLeft(who, reason) {
6646
6644
  departures.set(who, reason);
6647
6645
  },
6648
6646
  roomMetadata(metadata) {
6649
- let spotlight = null;
6647
+ let parsed = {};
6650
6648
  if (metadata.length > 0) {
6651
6649
  try {
6652
- const parsed = JSON.parse(metadata);
6653
- if (typeof parsed === "object" && parsed !== null) {
6654
- const value = parsed.spotlight;
6655
- if (typeof value === "string" && value.length > 0) spotlight = asParticipantIdentity(value);
6656
- }
6650
+ const value = JSON.parse(metadata);
6651
+ if (typeof value === "object" && value !== null) parsed = value;
6657
6652
  } catch {
6658
6653
  report(
6659
6654
  "warn",
6660
- "meet: the room's metadata is not JSON, so no spotlight was read from it",
6661
- "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."
6662
6657
  );
6663
6658
  }
6664
6659
  }
6665
- 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
+ }
6666
6673
  },
6667
6674
  activeSpeakers(identities) {
6668
6675
  store.setSpeaking([...new Set(identities.map((who) => personOf(who)))]);
@@ -6764,13 +6771,6 @@ function createRoomEngine(options) {
6764
6771
  }
6765
6772
  const sender = personOf(from ?? "");
6766
6773
  switch (topic) {
6767
- case DATA_TOPICS.hand:
6768
- store.setHand(
6769
- sender,
6770
- envelope.raised === true,
6771
- typeof envelope.raisedAt === "number" ? envelope.raisedAt : now()
6772
- );
6773
- return;
6774
6774
  case DATA_TOPICS.reaction:
6775
6775
  if (typeof envelope.emoji === "string") {
6776
6776
  store.addReaction({
@@ -6869,6 +6869,7 @@ function createRoomEngine(options) {
6869
6869
  current = token;
6870
6870
  lastServerUrl = token.serverUrl;
6871
6871
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
6872
+ if (token.leavePolicy !== void 0) store.patch({ leavePolicy: token.leavePolicy });
6872
6873
  store.patch({
6873
6874
  localIdentity: token.identity,
6874
6875
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -6903,6 +6904,7 @@ function createRoomEngine(options) {
6903
6904
  return;
6904
6905
  }
6905
6906
  driver = created;
6907
+ if (token.connectionKind === "companion") created.setRemoteAudioPlayback?.(false);
6906
6908
  await created.connect(token.serverUrl, token.token);
6907
6909
  store.patch({ failure: null, connection: "stable" });
6908
6910
  store.dispatch({ type: "connected" });
@@ -7164,6 +7166,19 @@ function createRoomEngine(options) {
7164
7166
  }
7165
7167
  },
7166
7168
  leave: leaveRoom,
7169
+ async checkElsewhere(roomName) {
7170
+ const claim = options.tabClaim;
7171
+ const userId = identity.userId;
7172
+ if (claim === void 0 || userId === null) return false;
7173
+ const before = store.snapshot();
7174
+ if (before.phase !== "prejoin" || before.secondDevice !== null) return false;
7175
+ if (!await claim.presentElsewhere(String(roomName), `user:${String(userId)}`)) return false;
7176
+ const now2 = store.snapshot();
7177
+ if (now2.phase !== "prejoin" || now2.secondDevice !== null) return false;
7178
+ store.patch({ secondDevice: { rule: "ask_switch_or_join" } });
7179
+ report("info", "meet: this browser already has you in this meeting \u2014 offering the choice before Join");
7180
+ return true;
7181
+ },
7167
7182
  backToPrejoin() {
7168
7183
  store.dispatch({ type: "back_to_prejoin", click: true });
7169
7184
  },
@@ -7221,11 +7236,6 @@ function createRoomEngine(options) {
7221
7236
  dismissShareNotice() {
7222
7237
  if (store.snapshot().share.state === "refused") store.patch({ share: NO_SHARE });
7223
7238
  },
7224
- async raiseHand(raised) {
7225
- const local = store.snapshot().localIdentity;
7226
- if (local !== null) store.setHand(local, raised);
7227
- await publish(DATA_TOPICS.hand, { raised, raisedAt: now() });
7228
- },
7229
7239
  async react(emoji) {
7230
7240
  const local = store.snapshot().localIdentity;
7231
7241
  const id = `${local ?? "local"}-${now()}`;
@@ -7354,6 +7364,8 @@ var MODERATION_ACTIONS = [
7354
7364
  "ask_unmute",
7355
7365
  "ask_all_unmute",
7356
7366
  "spotlight",
7367
+ "present",
7368
+ "raise_hand",
7357
7369
  "lower_hand",
7358
7370
  "lower_all_hands",
7359
7371
  "make_cohost",
@@ -7397,7 +7409,8 @@ function createInRoomClient(options) {
7397
7409
  action: request.action,
7398
7410
  ...request.identity != null ? { identity: request.identity } : {},
7399
7411
  ...request.name !== void 0 ? { name: request.name } : {},
7400
- ...request.reason !== void 0 ? { reason: request.reason } : {}
7412
+ ...request.reason !== void 0 ? { reason: request.reason } : {},
7413
+ ...request.on !== void 0 ? { on: request.on } : {}
7401
7414
  },
7402
7415
  ...hdrs !== void 0 ? { headers: hdrs } : {}
7403
7416
  });