@ai-matrx/meet 0.11.31 → 0.11.33

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.
@@ -8,11 +8,17 @@ import { useEffect as useEffect6, useLayoutEffect, useRef as useRef6, useState a
8
8
  import { useMemo as useMemo5 } from "react";
9
9
 
10
10
  // src/core/names.ts
11
+ var CONNECTION_SEPARATOR = "~";
12
+ function personOf(identity) {
13
+ const value = String(identity ?? "").trim();
14
+ const cut = value.indexOf(CONNECTION_SEPARATOR);
15
+ return cut === -1 ? value : value.slice(0, cut);
16
+ }
11
17
  function isGuestIdentity(identity) {
12
- return String(identity).startsWith("guest:");
18
+ return personOf(identity).startsWith("guest:");
13
19
  }
14
20
  function isAgentIdentity(identity) {
15
- return String(identity).startsWith("agent:");
21
+ return personOf(identity).startsWith("agent:");
16
22
  }
17
23
  var NOTE_TAKER_PRODUCT_NAME = "Matrx note-taker";
18
24
  var AGENT_PRODUCT_NAME = "Matrx agent";
@@ -62,12 +68,18 @@ var MeetError = class extends Error {
62
68
  * renders `refused:<reason>` (or `ended`) from it — never from the HTTP status.
63
69
  */
64
70
  reason;
65
- constructor(code, message, remedy, cause, reason) {
71
+ /**
72
+ * A refusal that lifts by itself says when: whole seconds from the moment the server answered
73
+ * (a denial's cooldown, `reknock_cooldown_seconds`). Null for every refusal that does not.
74
+ */
75
+ retryAfterSeconds;
76
+ constructor(code, message, remedy, cause, reason, retryAfterSeconds) {
66
77
  super(message);
67
78
  this.name = "MeetError";
68
79
  this.code = code;
69
80
  this.remedy = remedy;
70
81
  this.reason = reason ?? null;
82
+ this.retryAfterSeconds = retryAfterSeconds ?? null;
71
83
  if (cause !== void 0) this.cause = cause;
72
84
  }
73
85
  /** True when retrying after the host re-establishes a session is the fix. */
@@ -1093,6 +1105,7 @@ function useRoom() {
1093
1105
  phase: snapshot?.phase ?? "resolving",
1094
1106
  participants: snapshot?.participants ?? [],
1095
1107
  failure: snapshot?.failure ?? null,
1108
+ secondDevice: snapshot?.secondDevice ?? null,
1096
1109
  local,
1097
1110
  canShareScreen: engine?.supportsScreenShare() ?? true,
1098
1111
  join: useCallback2(
@@ -1185,6 +1198,7 @@ function useLobby() {
1185
1198
  const session = host?.meetings ?? null;
1186
1199
  return {
1187
1200
  waiting: (snapshot?.lobby ?? []).filter((entry) => entry.state === "knocking"),
1201
+ declined: (snapshot?.lobby ?? []).filter((entry) => entry.state === "denied"),
1188
1202
  // No runtime means no host powers — `<LobbyPanel>` renders nothing rather
1189
1203
  // than an admit button that would throw.
1190
1204
  canAdmit: session !== null && isHost,
@@ -1206,6 +1220,37 @@ function useLobby() {
1206
1220
  )
1207
1221
  };
1208
1222
  }
1223
+ function useCountdown(untilMs) {
1224
+ const [now, setNow] = useState2(() => Date.now());
1225
+ const active = typeof untilMs === "number" && untilMs > now;
1226
+ useEffect3(() => {
1227
+ if (!active) return void 0;
1228
+ setNow(Date.now());
1229
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
1230
+ return () => clearInterval(timer);
1231
+ }, [active, untilMs]);
1232
+ if (typeof untilMs !== "number") return null;
1233
+ return Math.max(0, untilMs - now);
1234
+ }
1235
+ function useKnockNotice() {
1236
+ const lobby = useLobby();
1237
+ const [dismissed, setDismissed] = useState2([]);
1238
+ const keyOf = (entry) => `${entry.identity}@${entry.knockedAt}`;
1239
+ const open = lobby.canAdmit ? lobby.waiting.filter((entry) => !dismissed.includes(keyOf(entry))) : [];
1240
+ const knocker = open[0] ?? null;
1241
+ return {
1242
+ knocker,
1243
+ others: Math.max(0, open.length - 1),
1244
+ admit: useCallback2(async () => {
1245
+ if (knocker === null) return;
1246
+ setDismissed((list) => [...list, keyOf(knocker)]);
1247
+ await lobby.admit(knocker);
1248
+ }, [knocker, lobby.admit]),
1249
+ dismiss: useCallback2(() => {
1250
+ if (knocker !== null) setDismissed((list) => [...list, keyOf(knocker)]);
1251
+ }, [knocker])
1252
+ };
1253
+ }
1209
1254
  function useWaiting() {
1210
1255
  const host = useMeetHost();
1211
1256
  const snapshot = useMeetSnapshot();
@@ -3196,6 +3241,7 @@ function ControlBar({ onLeave }) {
3196
3241
  }
3197
3242
  ) }) }) : null,
3198
3243
  /* @__PURE__ */ jsx6(BreakoutBanner, {}),
3244
+ panel !== "people" ? /* @__PURE__ */ jsx6(KnockNotice, { onView: () => setPanel("people") }) : null,
3199
3245
  controls.notice !== null && panel !== "people" ? /* @__PURE__ */ jsx6(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
3200
3246
  /* @__PURE__ */ jsxs3("div", { className: "mx-meet__controls", children: [
3201
3247
  /* @__PURE__ */ jsx6(
@@ -3526,10 +3572,21 @@ function MediaNotices() {
3526
3572
  const name = (kind) => kind === "microphone" ? "Microphone" : "Camera";
3527
3573
  if (chips.length === 0 && region.audioOutput !== "blocked" && notice === null && offer === null) return null;
3528
3574
  return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-notices", children: [
3529
- region.audioOutput === "blocked" ? /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-pill", role: "alert", "data-meet-notice": "audio-blocked", children: [
3530
- /* @__PURE__ */ jsx6("span", { children: "Sound is paused in this tab" }),
3531
- /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void enableAudio(), children: "Enable sound" })
3532
- ] }) : null,
3575
+ region.audioOutput === "blocked" ? /* @__PURE__ */ jsxs3(
3576
+ ErrorBox,
3577
+ {
3578
+ as: "div",
3579
+ className: "mx-meet__media-pill",
3580
+ "data-meet-notice": "audio-blocked",
3581
+ message: "Sound is paused in this tab",
3582
+ operation: "Enable meeting sound",
3583
+ origin: "meet/MediaNotices",
3584
+ children: [
3585
+ /* @__PURE__ */ jsx6("span", { children: "Sound is paused in this tab" }),
3586
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void enableAudio(), children: "Enable sound" })
3587
+ ]
3588
+ }
3589
+ ) : null,
3533
3590
  chips.map(({ kind, reason }) => /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-pill", role: "status", "data-meet-notice": deviceNoticeCode(kind, reason), children: [
3534
3591
  /* @__PURE__ */ jsx6("span", { children: deviceChipLabel(kind, reason) }),
3535
3592
  /* @__PURE__ */ jsx6(
@@ -4329,9 +4386,68 @@ function useMicrophoneStream(deviceId) {
4329
4386
  }, [deviceId]);
4330
4387
  return stream;
4331
4388
  }
4389
+ function KnockNotice({ onView }) {
4390
+ const { knocker, others, admit, dismiss } = useKnockNotice();
4391
+ if (knocker === null) return null;
4392
+ return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__notice-bar", role: "status", "data-meet-notice": "knock-received", "data-meet-participant-name": knocker.displayName, children: [
4393
+ /* @__PURE__ */ jsxs3("span", { children: [
4394
+ knocker.displayName,
4395
+ " wants to join",
4396
+ others > 0 ? ` (+${others} more)` : ""
4397
+ ] }),
4398
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__link", onClick: () => void admit().catch(() => void 0), children: "Admit" }),
4399
+ /* @__PURE__ */ jsx6(
4400
+ "button",
4401
+ {
4402
+ type: "button",
4403
+ className: "mx-meet__link",
4404
+ onClick: () => {
4405
+ dismiss();
4406
+ onView();
4407
+ },
4408
+ children: "View"
4409
+ }
4410
+ ),
4411
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismiss, children: /* @__PURE__ */ jsx6(CloseIcon, { width: 14, height: 14 }) })
4412
+ ] });
4413
+ }
4332
4414
  function LobbyPanel() {
4333
4415
  const lobby = useLobby();
4334
- if (!lobby.canAdmit || lobby.waiting.length === 0) return null;
4416
+ if (!lobby.canAdmit || lobby.waiting.length === 0 && lobby.declined.length === 0) return null;
4417
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
4418
+ lobby.waiting.length > 0 ? /* @__PURE__ */ jsx6(WaitingList, { lobby }) : null,
4419
+ lobby.declined.length > 0 ? /* @__PURE__ */ jsx6(DeclinedList, { lobby }) : null
4420
+ ] });
4421
+ }
4422
+ function DeclinedList({ lobby }) {
4423
+ return /* @__PURE__ */ jsxs3("section", { className: "mx-meet__lobby", "aria-label": "Declined requests", "data-meet-declined": "", children: [
4424
+ /* @__PURE__ */ jsx6("header", { className: "mx-meet__lobby-head", children: /* @__PURE__ */ jsx6("h3", { className: "mx-meet__lobby-title", children: "Declined" }) }),
4425
+ /* @__PURE__ */ jsx6("ul", { className: "mx-meet__lobby-list", children: lobby.declined.map((entry) => /* @__PURE__ */ jsxs3(
4426
+ "li",
4427
+ {
4428
+ className: "mx-meet__lobby-row",
4429
+ "data-meet-declined-entry": "",
4430
+ "data-meet-participant-name": entry.displayName,
4431
+ children: [
4432
+ /* @__PURE__ */ jsx6(Avatar, { name: entry.displayName, url: entry.avatarUrl, size: 32 }),
4433
+ /* @__PURE__ */ jsx6("span", { className: "mx-meet__person-name", children: entry.displayName }),
4434
+ /* @__PURE__ */ jsx6(
4435
+ "button",
4436
+ {
4437
+ type: "button",
4438
+ className: "mx-meet__link",
4439
+ "aria-label": `Let ${entry.displayName} ask again`,
4440
+ onClick: () => void lobby.undeny(entry),
4441
+ children: "Let them ask again"
4442
+ }
4443
+ )
4444
+ ]
4445
+ },
4446
+ entry.identity
4447
+ )) })
4448
+ ] });
4449
+ }
4450
+ function WaitingList({ lobby }) {
4335
4451
  return /* @__PURE__ */ jsxs3("section", { className: "mx-meet__lobby", "aria-label": "Waiting room", children: [
4336
4452
  /* @__PURE__ */ jsxs3("header", { className: "mx-meet__lobby-head", children: [
4337
4453
  /* @__PURE__ */ jsx6("h3", { className: "mx-meet__lobby-title", children: lobby.waiting.length === 1 ? "1 person is waiting" : `${lobby.waiting.length} people are waiting` }),
@@ -4595,6 +4711,21 @@ function PreJoinBody({
4595
4711
  useEffect6(() => {
4596
4712
  void host.devices.refresh();
4597
4713
  }, [host]);
4714
+ const joinWith = (secondDevice) => {
4715
+ setJoining(true);
4716
+ preview.release();
4717
+ void room.join({
4718
+ roomName,
4719
+ // An observer publishes nothing: no camera, no microphone (MD-16). A companion
4720
+ // ("join here too") never opens the microphone (S7).
4721
+ mode: observe2 ? "audio" : mode,
4722
+ microphone: observe2 || secondDevice === "join_too" ? false : microphone,
4723
+ ...meetingId !== void 0 ? { meetingId } : {},
4724
+ ...slug !== void 0 ? { slug } : {},
4725
+ ...observe2 ? { observe: true } : {},
4726
+ ...secondDevice !== void 0 ? { secondDevice } : {}
4727
+ }).then(() => onJoined?.()).catch(() => void 0).finally(() => setJoining(false));
4728
+ };
4598
4729
  return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__prejoin", children: [
4599
4730
  /* @__PURE__ */ jsxs3("div", { className: "mx-meet__preview", children: [
4600
4731
  mode === "video" && preview.preview !== null ? /* @__PURE__ */ jsx6(
@@ -4679,27 +4810,34 @@ function PreJoinBody({
4679
4810
  ),
4680
4811
  /* @__PURE__ */ jsx6(Label, { htmlFor: "mx-meet-join-mic", children: "Join with microphone" })
4681
4812
  ] }),
4682
- /* @__PURE__ */ jsx6(
4683
- Button,
4684
- {
4685
- type: "button",
4686
- className: "mx-meet__join",
4687
- disabled: joining,
4688
- onClick: () => {
4689
- setJoining(true);
4690
- preview.release();
4691
- void room.join({
4692
- roomName,
4693
- // An observer publishes nothing: no camera, no microphone (MD-16).
4694
- mode: observe2 ? "audio" : mode,
4695
- microphone: observe2 ? false : microphone,
4696
- ...meetingId !== void 0 ? { meetingId } : {},
4697
- ...slug !== void 0 ? { slug } : {},
4698
- ...observe2 ? { observe: true } : {}
4699
- }).then(() => onJoined?.()).catch(() => void 0).finally(() => setJoining(false));
4700
- },
4701
- children: joining ? "Joining\u2026" : observe2 ? "Observe" : "Join now"
4702
- }
4813
+ room.secondDevice === null ? /* @__PURE__ */ jsx6(Button, { type: "button", className: "mx-meet__join", disabled: joining, onClick: () => joinWith(), children: joining ? "Joining\u2026" : observe2 ? "Observe" : "Join now" }) : (
4814
+ // S7: ALREADY IN THIS MEETING on another tab or device — the meeting's choice
4815
+ // (Meet: Switch here / Join here too; Teams: Transfer here / Add this device).
4816
+ /* @__PURE__ */ jsxs3("div", { className: "mx-meet__second-device", "data-meet-region": "second-device", children: [
4817
+ /* @__PURE__ */ jsx6("p", { className: "mx-meet__hint", children: "You're in this meeting on another device" }),
4818
+ /* @__PURE__ */ jsx6(
4819
+ Button,
4820
+ {
4821
+ type: "button",
4822
+ className: "mx-meet__join",
4823
+ "data-meet-control": "switch-here",
4824
+ disabled: joining,
4825
+ onClick: () => joinWith("switch_here"),
4826
+ children: room.secondDevice.rule === "ask_transfer_or_add" ? "Transfer here" : "Switch here"
4827
+ }
4828
+ ),
4829
+ /* @__PURE__ */ jsx6(
4830
+ Button,
4831
+ {
4832
+ type: "button",
4833
+ variant: "outline",
4834
+ "data-meet-control": "join-too",
4835
+ disabled: joining,
4836
+ onClick: () => joinWith("join_too"),
4837
+ children: room.secondDevice.rule === "ask_transfer_or_add" ? "Add this device" : "Join here too"
4838
+ }
4839
+ )
4840
+ ] })
4703
4841
  )
4704
4842
  ] }),
4705
4843
  room.failure !== null ? /* @__PURE__ */ jsxs3("p", { className: "mx-meet__error", children: [
@@ -4897,16 +5035,21 @@ var REFUSED_TITLES = {
4897
5035
  not_invited: "You aren't invited to this meeting",
4898
5036
  locked: "This meeting is locked",
4899
5037
  removed: "You can't rejoin this meeting",
4900
- lobby_rejects: "You can't ask to join this meeting",
5038
+ lobby_rejects: "The host declined your request",
4901
5039
  meeting_full: "This meeting is full",
4902
5040
  platform_capacity: "Meetings are busy right now",
4903
5041
  verification_required: "Verify your account to join",
4904
5042
  registration_required: "Register to join this meeting"
4905
5043
  };
5044
+ function clockText(ms) {
5045
+ const total = Math.ceil(ms / 1e3);
5046
+ return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`;
5047
+ }
4906
5048
  function ExitScreen({ onLeave }) {
4907
5049
  const { phase, reason, host, snapshot, actions, call, connection } = useMeetCore();
4908
5050
  const [busy, setBusy] = useState4(false);
4909
5051
  const failure = snapshot?.failure ?? null;
5052
+ const retryLeft = useCountdown(phase === "refused:lobby_rejects" ? failure?.retryAt ?? null : null);
4910
5053
  const rejoin = /* @__PURE__ */ jsx6(
4911
5054
  Button,
4912
5055
  {
@@ -4949,6 +5092,33 @@ function ExitScreen({ onLeave }) {
4949
5092
  /* @__PURE__ */ jsx6(Button, { type: "button", variant: "outline", onClick: actions.retry, children: "Try again" }),
4950
5093
  home
4951
5094
  ] });
5095
+ if (code === "lobby_rejects") {
5096
+ if (retryLeft !== null && retryLeft > 0) {
5097
+ line = `You can ask again in ${clockText(retryLeft)}`;
5098
+ notice = "reknock-cooldown";
5099
+ actionsNode = home;
5100
+ } else if (retryLeft === 0) {
5101
+ line = "You can ask to join again";
5102
+ actionsNode = /* @__PURE__ */ jsxs3(Fragment, { children: [
5103
+ /* @__PURE__ */ jsx6(
5104
+ Button,
5105
+ {
5106
+ type: "button",
5107
+ disabled: busy,
5108
+ onClick: () => {
5109
+ setBusy(true);
5110
+ void actions.rejoin().catch(() => void 0).finally(() => setBusy(false));
5111
+ },
5112
+ children: "Ask to join again"
5113
+ }
5114
+ ),
5115
+ home
5116
+ ] });
5117
+ } else {
5118
+ line = "Ask the host to let you in, then open the link again";
5119
+ actionsNode = home;
5120
+ }
5121
+ }
4952
5122
  } else {
4953
5123
  switch (phase) {
4954
5124
  case "left":
@@ -5009,7 +5179,7 @@ function ExitScreen({ onLeave }) {
5009
5179
  /* @__PURE__ */ jsx6(Avatar, { name: host?.identity.displayName ?? "", url: host?.identity.avatarUrl ?? null, size: 72 }),
5010
5180
  /* @__PURE__ */ jsx6("h1", { className: "mx-meet__prejoin-title", children: title }),
5011
5181
  line !== null ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__empty-body", children: line }) : null,
5012
- notice !== null ? /* @__PURE__ */ jsx6("span", { role: "status", "data-meet-notice": notice, className: "mx-meet__sr-only", children: title }) : null,
5182
+ notice !== null ? /* @__PURE__ */ jsx6("span", { role: "status", "data-meet-notice": notice, className: "mx-meet__sr-only", children: notice === "reknock-cooldown" && line !== null ? line : title }) : null,
5013
5183
  /* @__PURE__ */ jsx6("div", { className: "mx-meet__waiting-actions", children: actionsNode })
5014
5184
  ] });
5015
5185
  }
@@ -5018,6 +5188,7 @@ function WaitingRoom({ host, onLeave }) {
5018
5188
  const { phaseNotice } = useMeetCore();
5019
5189
  const [busy, setBusy] = useState4(false);
5020
5190
  const admission = waiting?.admission ?? "knocking";
5191
+ const reknockLeft = useCountdown(admission === "denied" ? waiting?.reknockAt ?? null : null);
5021
5192
  const run = (action) => () => {
5022
5193
  setBusy(true);
5023
5194
  void action().catch(() => void 0).finally(() => setBusy(false));
@@ -5025,6 +5196,7 @@ function WaitingRoom({ host, onLeave }) {
5025
5196
  const home = /* @__PURE__ */ jsx6(Button, { type: "button", variant: "outline", onClick: () => onLeave !== void 0 ? onLeave() : window.location.assign("/"), children: "Back to home" });
5026
5197
  let title;
5027
5198
  let line = null;
5199
+ let cooldownNotice = false;
5028
5200
  let actions;
5029
5201
  switch (admission) {
5030
5202
  case "waiting_host":
@@ -5033,8 +5205,12 @@ function WaitingRoom({ host, onLeave }) {
5033
5205
  break;
5034
5206
  case "denied":
5035
5207
  title = "The host declined your request";
5208
+ if (reknockLeft !== null && reknockLeft > 0) {
5209
+ line = `You can ask again in ${clockText(reknockLeft)}`;
5210
+ cooldownNotice = true;
5211
+ }
5036
5212
  actions = /* @__PURE__ */ jsxs3(Fragment, { children: [
5037
- waiting?.reknockAllowed === true ? /* @__PURE__ */ jsx6(Button, { type: "button", disabled: busy, onClick: run(askAgain), children: "Ask to join again" }) : null,
5213
+ waiting?.reknockAllowed === true || waiting?.reknockAt != null && reknockLeft === 0 ? /* @__PURE__ */ jsx6(Button, { type: "button", disabled: busy, onClick: run(askAgain), children: "Ask to join again" }) : null,
5038
5214
  home
5039
5215
  ] });
5040
5216
  break;
@@ -5065,7 +5241,15 @@ function WaitingRoom({ host, onLeave }) {
5065
5241
  /* @__PURE__ */ jsx6(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 72 }),
5066
5242
  /* @__PURE__ */ jsx6("h1", { className: "mx-meet__prejoin-title", children: title }),
5067
5243
  phaseNotice === "sent-to-waiting" ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__empty-body", role: "status", "data-meet-notice": "sent-to-waiting", children: "The host moved you back to the waiting room" }) : null,
5068
- line !== null ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__empty-body", role: "status", "data-meet-notice": "host-absent", children: line }) : null,
5244
+ line !== null ? /* @__PURE__ */ jsx6(
5245
+ "p",
5246
+ {
5247
+ className: "mx-meet__empty-body",
5248
+ role: "status",
5249
+ "data-meet-notice": cooldownNotice ? "reknock-cooldown" : "host-absent",
5250
+ children: line
5251
+ }
5252
+ ) : null,
5069
5253
  /* @__PURE__ */ jsx6("div", { className: "mx-meet__waiting-actions", children: actions })
5070
5254
  ] });
5071
5255
  }