@ai-matrx/meet 0.11.35 → 0.11.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.js CHANGED
@@ -556,6 +556,30 @@ function mergeGhosts(live, ghosts, at) {
556
556
  return { roster: [...live, ...extra], kept };
557
557
  }
558
558
 
559
+ // src/core/leave.ts
560
+ var DEFAULT_LEAVE_POLICY = { hostLeavePrompt: "leave_or_end", endConfirm: false };
561
+ var PROMPTS = ["leave_or_end", "must_assign_or_end", "leave_only"];
562
+ function leavePolicyFrom(raw) {
563
+ if (typeof raw !== "object" || raw === null) return DEFAULT_LEAVE_POLICY;
564
+ const value = raw;
565
+ const prompt = value["host_leave_prompt"];
566
+ const confirm = value["end_confirm"];
567
+ return {
568
+ hostLeavePrompt: PROMPTS.includes(prompt) ? prompt : DEFAULT_LEAVE_POLICY.hostLeavePrompt,
569
+ endConfirm: typeof confirm === "boolean" ? confirm : DEFAULT_LEAVE_POLICY.endConfirm
570
+ };
571
+ }
572
+ function leaveChoices(args) {
573
+ if (args.role !== "host") return ["leave"];
574
+ switch (args.policy.hostLeavePrompt) {
575
+ case "must_assign_or_end":
576
+ return args.othersPresent ? ["assign_and_leave", "end_for_all"] : ["leave", "end_for_all"];
577
+ case "leave_only":
578
+ case "leave_or_end":
579
+ return ["leave", "end_for_all"];
580
+ }
581
+ }
582
+
559
583
  // src/core/tokens.ts
560
584
  function createWebRoomTokenStorage(onDiagnostic) {
561
585
  const store = globalThis.localStorage;
@@ -747,7 +771,8 @@ function parseRoomToken(body, roomName) {
747
771
  pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
748
772
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
749
773
  observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
750
- connectionPolicy: connectionPolicyFrom(payload["connection_policy"])
774
+ connectionPolicy: connectionPolicyFrom(payload["connection_policy"]),
775
+ leavePolicy: leavePolicyFrom(payload["leave_policy"])
751
776
  };
752
777
  }
753
778
  function createTokenClient(options) {
@@ -3446,6 +3471,10 @@ var EMPTY = {
3446
3471
  chatHistoryError: null,
3447
3472
  reactions: [],
3448
3473
  spotlight: null,
3474
+ actingHost: null,
3475
+ presenter: null,
3476
+ waitingCount: 0,
3477
+ leavePolicy: DEFAULT_LEAVE_POLICY,
3449
3478
  moderationNotice: null,
3450
3479
  actionNotice: null,
3451
3480
  privateChatRequest: null,
@@ -3507,8 +3536,6 @@ function createMeetStore(initial = {}) {
3507
3536
  },
3508
3537
  setParticipants(participants) {
3509
3538
  const names = learnNames(state.names, participants);
3510
- const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
3511
- const handTimes = new Map(state.participants.map((p) => [p.identity, p.handRaisedAt ?? null]));
3512
3539
  const quality = new Map(state.participants.map((p) => [p.identity, p.quality]));
3513
3540
  commit({
3514
3541
  ...state,
@@ -3521,8 +3548,6 @@ function createMeetStore(initial = {}) {
3521
3548
  // identity could reach a screen, and it is the place that asks the
3522
3549
  // name book. `roster-names.test.ts` fails if a token ever gets past it.
3523
3550
  displayName: displayNameFor(names, participant.identity, participant.displayName),
3524
- handRaised: hands.get(participant.identity) ?? participant.handRaised,
3525
- handRaisedAt: handTimes.get(participant.identity) ?? participant.handRaisedAt ?? null,
3526
3551
  quality: participant.quality === "unknown" ? quality.get(participant.identity) ?? "unknown" : participant.quality
3527
3552
  }))
3528
3553
  });
@@ -3545,20 +3570,6 @@ function createMeetStore(initial = {}) {
3545
3570
  )
3546
3571
  });
3547
3572
  },
3548
- setHand(identity, raised, at) {
3549
- commit({
3550
- ...state,
3551
- participants: state.participants.map(
3552
- (participant) => participant.identity === identity ? {
3553
- ...participant,
3554
- handRaised: raised,
3555
- // A hand that is ALREADY up keeps its place in the queue when
3556
- // it is re-announced (to a late joiner): the first time wins.
3557
- handRaisedAt: raised ? participant.handRaised && participant.handRaisedAt != null ? participant.handRaisedAt : at ?? Date.now() : null
3558
- } : participant
3559
- )
3560
- });
3561
- },
3562
3573
  addChat(message) {
3563
3574
  if (state.chat.some((existing) => existing.id === message.id)) return;
3564
3575
  const chat = [...state.chat, message];
@@ -3707,18 +3718,6 @@ var ROOM_EVENTS = {
3707
3718
  wrapUp: "matrx.meet.wrap_up",
3708
3719
  /** The meeting ended for everyone. Payload `{ meeting_id, ended_at, trigger }`. */
3709
3720
  ended: "matrx.meet.ended",
3710
- /**
3711
- * A `meet_recordings` row changed state. Payload
3712
- * `{ meeting_id, recording: { id, state, started_by } }`.
3713
- *
3714
- * 🚨 THIS IS THE ONLY WAY A GUEST LEARNS A RECORDING IS RUNNING. PreJoin
3715
- * promises every joiner an indicator, and `anon` holds no SELECT grant on
3716
- * `meet_recordings` (MRI-A10 settled that widening it is forbidden), so
3717
- * without this packet the promise was a sentence with nothing behind it: on
3718
- * production 2026-09-09 the guest lane showed no indicator at all while the
3719
- * host recorded (MRI-C8).
3720
- */
3721
- recording: "matrx.meet.recording",
3722
3721
  /**
3723
3722
  * A chat message to everyone was SAVED (Meet wave 2). Payload
3724
3723
  * `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
@@ -3771,8 +3770,6 @@ function applyRoomEvent(args) {
3771
3770
  return applyWrapUp(store, payload, now);
3772
3771
  case ROOM_EVENTS.ended:
3773
3772
  return applyEnded(store, payload);
3774
- case ROOM_EVENTS.recording:
3775
- return applyRecording(store, payload);
3776
3773
  case ROOM_EVENTS.chat:
3777
3774
  return applyChat(store, payload);
3778
3775
  case ROOM_EVENTS.moderation:
@@ -3852,9 +3849,9 @@ function applyWrapUp(store, payload, now) {
3852
3849
  store.announceWrapUp(announcement);
3853
3850
  return HANDLED;
3854
3851
  }
3855
- function applyRecording(store, payload) {
3852
+ function applyRecordingState(store, payload) {
3856
3853
  const row = object(payload["recording"]);
3857
- if (row === null) return ignored("a recording event with no `recording` row");
3854
+ if (row === null) return ignored("room state carries no recording");
3858
3855
  const meetingId = text2(payload["meeting_id"]);
3859
3856
  const meeting = store.snapshot().meeting;
3860
3857
  if (meeting !== null && meetingId !== null && meetingId !== meeting.id) {
@@ -3936,19 +3933,6 @@ function applyModeration(store, payload, now) {
3936
3933
  }
3937
3934
  const kind = text2(payload["kind"]);
3938
3935
  switch (kind) {
3939
- case "lower-hands": {
3940
- const raw = payload["identities"];
3941
- const everyone = raw === "*";
3942
- const named = new Set(
3943
- Array.isArray(raw) ? raw.filter((value) => typeof value === "string") : []
3944
- );
3945
- for (const participant of snapshot.participants) {
3946
- if (participant.handRaised && (everyone || named.has(participant.identity))) {
3947
- store.setHand(participant.identity, false);
3948
- }
3949
- }
3950
- return HANDLED;
3951
- }
3952
3936
  case "role": {
3953
3937
  const identity = text2(payload["identity"]);
3954
3938
  const role = text2(payload["role"]);
@@ -4340,7 +4324,6 @@ function gridColumns(count2) {
4340
4324
 
4341
4325
  // src/core/room-driver.ts
4342
4326
  var DATA_TOPICS = {
4343
- hand: "mx.hand",
4344
4327
  reaction: "mx.reaction",
4345
4328
  chat: "mx.chat"
4346
4329
  };
@@ -4482,6 +4465,7 @@ function createLiveKitDriver(handlers, options = {}) {
4482
4465
  projectRoster = roster;
4483
4466
  created.on(RoomEvent.TrackSubscribed, (track) => {
4484
4467
  if (track.kind !== Track.Kind.Audio || typeof document === "undefined") return;
4468
+ if (!remoteAudioPlayback) return;
4485
4469
  const element = track.attach();
4486
4470
  element.setAttribute("data-mx-meet-audio", "");
4487
4471
  audioSink().appendChild(element);
@@ -4566,6 +4550,7 @@ function createLiveKitDriver(handlers, options = {}) {
4566
4550
  let sources = null;
4567
4551
  let trackEvents = null;
4568
4552
  let audioContainer = null;
4553
+ let remoteAudioPlayback = true;
4569
4554
  function audioSink() {
4570
4555
  if (audioContainer !== null && audioContainer.isConnected) return audioContainer;
4571
4556
  const container = document.createElement("div");
@@ -4724,6 +4709,14 @@ function createLiveKitDriver(handlers, options = {}) {
4724
4709
  throw normalizeDeviceError(error, "meet.switchDevice");
4725
4710
  }
4726
4711
  },
4712
+ setRemoteAudioPlayback(enabled) {
4713
+ remoteAudioPlayback = enabled;
4714
+ if (enabled || audioContainer === null) return;
4715
+ for (const element of Array.from(audioContainer.querySelectorAll("[data-mx-meet-audio]"))) {
4716
+ if (element instanceof HTMLMediaElement) element.srcObject = null;
4717
+ element.remove();
4718
+ }
4719
+ },
4727
4720
  async startAudio() {
4728
4721
  const target = room;
4729
4722
  if (target === null) return;
@@ -4964,7 +4957,7 @@ function createMeetingSession(options) {
4964
4957
  })();
4965
4958
  return reading;
4966
4959
  }
4967
- async function postLeave(intent) {
4960
+ async function postLeave(intent, extra = {}) {
4968
4961
  const meeting = requireMeeting(`meet.${intent}`);
4969
4962
  const pass = options.resolvePass?.() ?? null;
4970
4963
  const response = await options.api.request({
@@ -4972,7 +4965,7 @@ function createMeetingSession(options) {
4972
4965
  method: "POST",
4973
4966
  requireAuth: false,
4974
4967
  operation: `meet.${intent}`,
4975
- body: { meeting_id: meeting.id, intent },
4968
+ body: { meeting_id: meeting.id, intent, ...extra },
4976
4969
  ...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
4977
4970
  });
4978
4971
  if (!response.ok) {
@@ -4986,7 +4979,7 @@ function createMeetingSession(options) {
4986
4979
  throw new MeetError(
4987
4980
  "transport",
4988
4981
  refusal2?.reason ?? `meet.${intent}: the server answered ${response.status}`,
4989
- refusal2?.remedy ?? "You are still in the waiting room. Try again."
4982
+ refusal2?.remedy ?? (intent === "withdraw" ? "You are still in the waiting room. Try again." : "Nothing was changed. Try again.")
4990
4983
  );
4991
4984
  }
4992
4985
  }
@@ -5210,6 +5203,10 @@ function createMeetingSession(options) {
5210
5203
  store.dispatch({ type: "admission", admission: "withdrawn" });
5211
5204
  hint();
5212
5205
  },
5206
+ async hostLeave(intent) {
5207
+ if (intent === "end_for_all") await postLeave("end_for_all");
5208
+ else await postLeave("assign_and_leave", { assign_to: intent.assignTo });
5209
+ },
5213
5210
  async askAgain() {
5214
5211
  await options.rejoin?.();
5215
5212
  },
@@ -6171,6 +6168,11 @@ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
6171
6168
  return /administrator|policy/i.test(remedy) ? remedy : SCREEN_SHARE_BLOCKED_REMEDY;
6172
6169
  }
6173
6170
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
6171
+ var PARTICIPANT_HAND_ATTRIBUTE = "matrx.hand";
6172
+ function handFrom(value) {
6173
+ const at = value === void 0 || value.length === 0 ? Number.NaN : Number(value);
6174
+ return Number.isFinite(at) ? { handRaised: true, handRaisedAt: at } : { handRaised: false, handRaisedAt: null };
6175
+ }
6174
6176
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
6175
6177
  var CONNECTION_KIND_ATTRIBUTE = "matrx.participant.connection_kind";
6176
6178
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
@@ -6224,7 +6226,10 @@ function projectParticipant(driver, joinedAt) {
6224
6226
  cameraEnabled: driver.cameraEnabled,
6225
6227
  screenSharing: driver.screenSharing,
6226
6228
  speaking: false,
6227
- handRaised: false,
6229
+ // S8: a hand is the SERVER's (`hand_raised_at` on the person row, `/moderate raise_hand`),
6230
+ // replicated as the `matrx.hand` attribute: epoch ms of the raise, "" when down. It is
6231
+ // therefore delivered to late joiners and survives refresh and reconnect.
6232
+ ...handFrom(driver.attributes[PARTICIPANT_HAND_ATTRIBUTE]),
6228
6233
  quality: "unknown",
6229
6234
  joinedAt
6230
6235
  };
@@ -6354,40 +6359,38 @@ function createRoomEngine(options) {
6354
6359
  },
6355
6360
  participants(list) {
6356
6361
  const joinedAt = new Date(now()).toISOString();
6357
- const before = new Set(store.snapshot().participants.map((p) => p.identity));
6358
6362
  projectRoster(list, joinedAt);
6359
- const snapshot = store.snapshot();
6360
- const newcomer = snapshot.participants.some(
6361
- (participant) => !participant.isLocal && !before.has(participant.identity)
6362
- );
6363
- const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
6364
- if (newcomer && before.size > 0 && me?.handRaised === true) {
6365
- void publish(DATA_TOPICS.hand, { raised: true, raisedAt: me.handRaisedAt ?? now() }).catch(
6366
- () => void 0
6367
- );
6368
- }
6369
6363
  },
6370
6364
  participantLeft(who, reason) {
6371
6365
  departures.set(who, reason);
6372
6366
  },
6373
6367
  roomMetadata(metadata) {
6374
- let spotlight = null;
6368
+ let parsed = {};
6375
6369
  if (metadata.length > 0) {
6376
6370
  try {
6377
- const parsed = JSON.parse(metadata);
6378
- if (typeof parsed === "object" && parsed !== null) {
6379
- const value = parsed.spotlight;
6380
- if (typeof value === "string" && value.length > 0) spotlight = asParticipantIdentity(value);
6381
- }
6371
+ const value = JSON.parse(metadata);
6372
+ if (typeof value === "object" && value !== null) parsed = value;
6382
6373
  } catch {
6383
6374
  report(
6384
6375
  "warn",
6385
- "meet: the room's metadata is not JSON, so no spotlight was read from it",
6386
- "Only aidream writes room metadata; check services/meet/moderation.py."
6376
+ "meet: the room's metadata is not JSON, so the room state was not read from it",
6377
+ "Only aidream writes room metadata; check services/meet/room_state.py."
6387
6378
  );
6388
6379
  }
6389
6380
  }
6390
- if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
6381
+ const key = (name) => {
6382
+ const value = parsed[name];
6383
+ return typeof value === "string" && value.length > 0 ? asParticipantIdentity(personOf(value)) : null;
6384
+ };
6385
+ const waiting = typeof parsed["waiting_count"] === "number" ? parsed["waiting_count"] : 0;
6386
+ const before = store.snapshot();
6387
+ const next = { spotlight: key("spotlight"), actingHost: key("acting_host"), presenter: key("presenter"), waitingCount: waiting };
6388
+ if (before.spotlight !== next.spotlight || before.actingHost !== next.actingHost || before.presenter !== next.presenter || before.waitingCount !== next.waitingCount) {
6389
+ store.patch(next);
6390
+ }
6391
+ if (parsed["recording"] !== void 0 && parsed["recording"] !== null) {
6392
+ applyRecordingState(store, { meeting_id: parsed["meeting_id"], recording: parsed["recording"] });
6393
+ }
6391
6394
  },
6392
6395
  activeSpeakers(identities) {
6393
6396
  store.setSpeaking([...new Set(identities.map((who) => personOf(who)))]);
@@ -6489,13 +6492,6 @@ function createRoomEngine(options) {
6489
6492
  }
6490
6493
  const sender = personOf(from ?? "");
6491
6494
  switch (topic) {
6492
- case DATA_TOPICS.hand:
6493
- store.setHand(
6494
- sender,
6495
- envelope.raised === true,
6496
- typeof envelope.raisedAt === "number" ? envelope.raisedAt : now()
6497
- );
6498
- return;
6499
6495
  case DATA_TOPICS.reaction:
6500
6496
  if (typeof envelope.emoji === "string") {
6501
6497
  store.addReaction({
@@ -6594,6 +6590,7 @@ function createRoomEngine(options) {
6594
6590
  current = token;
6595
6591
  lastServerUrl = token.serverUrl;
6596
6592
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
6593
+ if (token.leavePolicy !== void 0) store.patch({ leavePolicy: token.leavePolicy });
6597
6594
  store.patch({
6598
6595
  localIdentity: token.identity,
6599
6596
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -6628,6 +6625,7 @@ function createRoomEngine(options) {
6628
6625
  return;
6629
6626
  }
6630
6627
  driver = created;
6628
+ if (token.connectionKind === "companion") created.setRemoteAudioPlayback?.(false);
6631
6629
  await created.connect(token.serverUrl, token.token);
6632
6630
  store.patch({ failure: null, connection: "stable" });
6633
6631
  store.dispatch({ type: "connected" });
@@ -6889,6 +6887,19 @@ function createRoomEngine(options) {
6889
6887
  }
6890
6888
  },
6891
6889
  leave: leaveRoom,
6890
+ async checkElsewhere(roomName) {
6891
+ const claim = options.tabClaim;
6892
+ const userId = identity.userId;
6893
+ if (claim === void 0 || userId === null) return false;
6894
+ const before = store.snapshot();
6895
+ if (before.phase !== "prejoin" || before.secondDevice !== null) return false;
6896
+ if (!await claim.presentElsewhere(String(roomName), `user:${String(userId)}`)) return false;
6897
+ const now2 = store.snapshot();
6898
+ if (now2.phase !== "prejoin" || now2.secondDevice !== null) return false;
6899
+ store.patch({ secondDevice: { rule: "ask_switch_or_join" } });
6900
+ report("info", "meet: this browser already has you in this meeting \u2014 offering the choice before Join");
6901
+ return true;
6902
+ },
6892
6903
  backToPrejoin() {
6893
6904
  store.dispatch({ type: "back_to_prejoin", click: true });
6894
6905
  },
@@ -6946,11 +6957,6 @@ function createRoomEngine(options) {
6946
6957
  dismissShareNotice() {
6947
6958
  if (store.snapshot().share.state === "refused") store.patch({ share: NO_SHARE });
6948
6959
  },
6949
- async raiseHand(raised) {
6950
- const local = store.snapshot().localIdentity;
6951
- if (local !== null) store.setHand(local, raised);
6952
- await publish(DATA_TOPICS.hand, { raised, raisedAt: now() });
6953
- },
6954
6960
  async react(emoji) {
6955
6961
  const local = store.snapshot().localIdentity;
6956
6962
  const id = `${local ?? "local"}-${now()}`;
@@ -7079,6 +7085,8 @@ var MODERATION_ACTIONS = [
7079
7085
  "ask_unmute",
7080
7086
  "ask_all_unmute",
7081
7087
  "spotlight",
7088
+ "present",
7089
+ "raise_hand",
7082
7090
  "lower_hand",
7083
7091
  "lower_all_hands",
7084
7092
  "make_cohost",
@@ -7122,7 +7130,8 @@ function createInRoomClient(options) {
7122
7130
  action: request.action,
7123
7131
  ...request.identity != null ? { identity: request.identity } : {},
7124
7132
  ...request.name !== void 0 ? { name: request.name } : {},
7125
- ...request.reason !== void 0 ? { reason: request.reason } : {}
7133
+ ...request.reason !== void 0 ? { reason: request.reason } : {},
7134
+ ...request.on !== void 0 ? { on: request.on } : {}
7126
7135
  },
7127
7136
  ...hdrs !== void 0 ? { headers: hdrs } : {}
7128
7137
  });
@@ -8119,6 +8128,7 @@ export {
8119
8128
  DATA_TOPICS,
8120
8129
  DEFAULT_CONNECTION_POLICY,
8121
8130
  DEFAULT_EFFECTS,
8131
+ DEFAULT_LEAVE_POLICY,
8122
8132
  DEFAULT_PAGE_SIZE,
8123
8133
  EMPTY_COLLAB,
8124
8134
  END_MEETING_NOT_SERVED,
@@ -8153,6 +8163,7 @@ export {
8153
8163
  NO_SHARE,
8154
8164
  ORGANIZATION_HEADER,
8155
8165
  PARTICIPANT_CAN_RECORD_ATTRIBUTE,
8166
+ PARTICIPANT_HAND_ATTRIBUTE,
8156
8167
  PARTICIPANT_ROLE_ATTRIBUTE,
8157
8168
  PARTICIPANT_STANDING_ATTRIBUTE,
8158
8169
  REACTION_TTL_MS,
@@ -8265,6 +8276,8 @@ export {
8265
8276
  isWaitingPhase,
8266
8277
  learnName,
8267
8278
  learnNames,
8279
+ leaveChoices,
8280
+ leavePolicyFrom,
8268
8281
  listenOnlyNotice,
8269
8282
  loadFollowUpDraft,
8270
8283
  loadGuestMeetingRecord,