@ai-matrx/meet 0.7.7 → 0.7.9

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.cjs CHANGED
@@ -148,6 +148,7 @@ __export(react_exports, {
148
148
  attendanceCsv: () => attendanceCsv,
149
149
  attendanceReport: () => attendanceReport,
150
150
  attendanceTotals: () => attendanceTotals,
151
+ attendeeKey: () => attendeeKey,
151
152
  callInviteAction: () => callInviteAction,
152
153
  callInviteText: () => callInviteText,
153
154
  callStateLabel: () => callStateLabel,
@@ -251,6 +252,7 @@ __export(react_exports, {
251
252
  summarizeMeetingRecordRestrictions: () => summarizeMeetingRecordRestrictions,
252
253
  transcriptDocument: () => transcriptDocument,
253
254
  unavailableFrom: () => unavailableFrom,
255
+ uniqueAttendees: () => uniqueAttendees,
254
256
  useAudioLevel: () => useAudioLevel,
255
257
  useCalls: () => useCalls,
256
258
  useCanModerate: () => useCanModerate,
@@ -319,6 +321,20 @@ var MeetError = class extends Error {
319
321
  return this.code === "permission-denied" || this.code === "device-not-found" || this.code === "device-in-use" || this.code === "insecure-context";
320
322
  }
321
323
  };
324
+ var SERVER_FAILED_SENTENCE = "The server hit an unexpected problem and nothing was changed.";
325
+ function isRawServerText(value) {
326
+ const text3 = value.trim();
327
+ return /^internal server error\b/i.test(text3) || /^[A-Z][A-Za-z0-9]*(Error|Exception)$/.test(text3) || /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/.test(text3);
328
+ }
329
+ function humanDatabaseSentence(message) {
330
+ const text3 = message.trim();
331
+ if (text3.length === 0 || isRawServerText(text3)) return null;
332
+ if (/permission denied for|violates|relation "|does not exist|syntax error|column "|jwt|pgrst|duplicate key|null value in/i.test(
333
+ text3
334
+ ))
335
+ return null;
336
+ return text3;
337
+ }
322
338
  var FORBIDDEN_CODES = /* @__PURE__ */ new Set(["42501", "PGRST301", "PGRST302"]);
323
339
  var NOT_FOUND_CODES = /* @__PURE__ */ new Set(["PGRST116", "PGRST205"]);
324
340
  var CONFLICT_CODES = /* @__PURE__ */ new Set(["23505", "40001", "55000"]);
@@ -346,8 +362,8 @@ function normalizeMeetError(error, operation) {
346
362
  if (code !== void 0 && FORBIDDEN_CODES.has(code)) {
347
363
  return new MeetError(
348
364
  "forbidden",
349
- `${operation}: denied by the database (${code}: ${message})`,
350
- "Authorization is RLS plus auth-checked RPCs (R5). Fix the policy or the caller's membership \u2014 never route around it with a service-role client in a browser.",
365
+ humanDatabaseSentence(message) ?? "You don't have access to do that in this meeting.",
366
+ "Ask the meeting's host to do it, or to give you access.",
351
367
  error
352
368
  );
353
369
  }
@@ -362,15 +378,16 @@ function normalizeMeetError(error, operation) {
362
378
  if (code !== void 0 && CONFLICT_CODES.has(code)) {
363
379
  return new MeetError(
364
380
  "conflict",
365
- `${operation}: unique violation (${code}: ${message})`,
366
- "A concurrent writer won. Re-read the meeting rather than retrying the insert \u2014 for a call invite this is the idempotency key doing its job.",
381
+ humanDatabaseSentence(message) ?? "This meeting changed a moment ago, so nothing was saved.",
382
+ "Reload to see the latest, then try again.",
367
383
  error
368
384
  );
369
385
  }
386
+ const hint = error?.hint;
370
387
  return new MeetError(
371
388
  "transport",
372
- `${operation}: ${message}`,
373
- "Transient transport failure. Reads retry once; a join failure surfaces to the room phase as `failed` with this message rather than hanging on a spinner.",
389
+ /failed to fetch|networkerror|load failed|network request failed/i.test(message) ? "The server could not be reached." : humanDatabaseSentence(message) ?? SERVER_FAILED_SENTENCE,
390
+ typeof hint === "string" && humanDatabaseSentence(hint) !== null ? hint.trim() : "Nothing was changed \u2014 retry in a moment; if it keeps happening, reload the page.",
374
391
  error
375
392
  );
376
393
  }
@@ -778,6 +795,24 @@ function readText(data) {
778
795
  }
779
796
  return "";
780
797
  }
798
+ async function refusalFromResponse(response, operation, fallbackRemedy) {
799
+ let body = null;
800
+ try {
801
+ body = await response.json();
802
+ } catch {
803
+ body = null;
804
+ }
805
+ const said = readServerRefusal(body);
806
+ const code = response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport";
807
+ const error = new MeetError(
808
+ code,
809
+ said?.reason ?? SERVER_FAILED_SENTENCE,
810
+ said !== null && said.remedy.length > 0 ? said.remedy : fallbackRemedy
811
+ );
812
+ error.operation = operation;
813
+ error.status = response.status;
814
+ return error;
815
+ }
781
816
  function readServerRefusal(body) {
782
817
  if (typeof body === "string") {
783
818
  return body.trim().length === 0 ? null : { reason: body.trim(), remedy: "" };
@@ -796,7 +831,8 @@ function readServerRefusal(body) {
796
831
  return messages.length === 0 ? null : { reason: messages.join("; "), remedy: "" };
797
832
  }
798
833
  if (typeof detail === "object" && detail !== null) return readServerRefusal(detail);
799
- const reason = typeof row["reason"] === "string" ? row["reason"] : typeof row["message"] === "string" ? row["message"] : typeof detail === "string" ? detail : typeof row["error"] === "string" ? row["error"] : null;
834
+ const humanOrNull = (value) => typeof value === "string" && value.trim().length > 0 && !isRawServerText(value) ? value.trim() : null;
835
+ const reason = humanOrNull(row["reason"]) ?? humanOrNull(row["message"]) ?? humanOrNull(row["user_message"]) ?? (typeof detail === "string" ? humanOrNull(detail) : null) ?? (typeof row["message"] === "string" || typeof row["error"] === "string" || typeof detail === "string" ? SERVER_FAILED_SENTENCE : null);
800
836
  if (reason === null) return null;
801
837
  const remedy = typeof row["remedy"] === "string" ? row["remedy"] : "";
802
838
  if (remedy.length > 0) return { reason, remedy };
@@ -1850,6 +1886,7 @@ var EMPTY = {
1850
1886
  spotlight: null,
1851
1887
  moderationNotice: null,
1852
1888
  actionNotice: null,
1889
+ privateChatRequest: null,
1853
1890
  effects: DEFAULT_EFFECTS,
1854
1891
  transcript: [],
1855
1892
  notes: [],
@@ -4039,6 +4076,10 @@ function createRoomEngine(options) {
4039
4076
  }
4040
4077
  },
4041
4078
  disconnected(reason) {
4079
+ if (current !== null) {
4080
+ tokens.invalidate(current.roomName);
4081
+ current = null;
4082
+ }
4042
4083
  store.patch({ phase: "left" });
4043
4084
  options.onLeft?.(reason);
4044
4085
  }
@@ -4341,19 +4382,8 @@ function createInRoomClient(options) {
4341
4382
  const token = options.resolveRoomToken();
4342
4383
  return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
4343
4384
  }
4344
- async function refusal2(response, operation, remedy) {
4345
- let body = null;
4346
- try {
4347
- body = await response.json();
4348
- } catch {
4349
- body = null;
4350
- }
4351
- const said = readServerRefusal(body);
4352
- return new MeetError(
4353
- response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
4354
- said?.reason ?? `${operation}: the server answered ${response.status}`,
4355
- said?.remedy !== void 0 && said.remedy.length > 0 ? said.remedy : remedy
4356
- );
4385
+ function refusal2(response, operation, remedy) {
4386
+ return refusalFromResponse(response, operation, remedy);
4357
4387
  }
4358
4388
  return {
4359
4389
  async moderate(request) {
@@ -6313,216 +6343,617 @@ function useGuestRecordAccess(meeting) {
6313
6343
  // src/react/components.tsx
6314
6344
  var import_react4 = require("react");
6315
6345
  var import_design_system = require("@ai-matrx/design-system");
6316
- var import_format2 = require("@ai-matrx/kit/format");
6346
+ var import_format3 = require("@ai-matrx/kit/format");
6317
6347
 
6318
- // src/core/format.ts
6319
- function participantSummary(participants) {
6320
- const names = participants.filter((p) => !p.isLocal).map((p) => p.displayName);
6321
- if (names.length === 0) return "Just you";
6322
- if (names.length === 1) return names[0] ?? "";
6323
- if (names.length === 2) return `${names[0]} and ${names[1]}`;
6324
- return `${names[0]}, ${names[1]} and ${names.length - 2} ${names.length - 2 === 1 ? "other" : "others"}`;
6325
- }
6326
- function callStateLabel(state, outgoing) {
6327
- switch (state) {
6328
- case "dialing":
6329
- case "ringing":
6330
- return outgoing ? "Calling\u2026" : "Incoming call";
6331
- case "connecting":
6332
- return "Connecting\u2026";
6333
- case "active":
6334
- return "In call";
6335
- case "declined":
6336
- return outgoing ? "Declined" : "You declined";
6337
- case "missed":
6338
- return outgoing ? "No answer" : "Missed call";
6339
- case "busy":
6340
- return outgoing ? "On another call" : "You were busy";
6341
- case "cancelled":
6342
- return outgoing ? "Cancelled" : "Cancelled by caller";
6343
- case "ended":
6344
- return "Call ended";
6345
- case "failed":
6346
- return "Call failed";
6347
- default:
6348
- return "Call";
6349
- }
6348
+ // src/core/after.ts
6349
+ var import_format2 = require("@ai-matrx/kit/format");
6350
+ function kindOf(person) {
6351
+ if (person.isAgent) return "assistant";
6352
+ if (person.role === "host") return "host";
6353
+ if (person.role === "cohost") return "cohost";
6354
+ if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
6355
+ return "member";
6350
6356
  }
6351
- function countLabel(count2, singular, plural = `${singular}s`) {
6352
- return `${count2} ${count2 === 1 ? singular : plural}`;
6357
+ function ms(iso) {
6358
+ if (iso === null) return null;
6359
+ const at = new Date(iso).getTime();
6360
+ return Number.isNaN(at) ? null : at;
6353
6361
  }
6354
-
6355
- // src/react/icons.tsx
6356
- var import_jsx_runtime2 = require("react/jsx-runtime");
6357
- function Icon({ children, ...props }) {
6358
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
6359
- "svg",
6360
- {
6361
- width: "20",
6362
- height: "20",
6363
- viewBox: "0 0 24 24",
6364
- fill: "none",
6365
- stroke: "currentColor",
6366
- strokeWidth: "1.75",
6367
- strokeLinecap: "round",
6368
- strokeLinejoin: "round",
6369
- "aria-hidden": "true",
6370
- focusable: "false",
6371
- ...props,
6372
- children
6362
+ function attendeeKey(person, names = {}) {
6363
+ if (person.userId !== null) return `user:${person.userId}`;
6364
+ if (person.isAgent) return `agent:${person.identity}`;
6365
+ const name = displayNameFor(names, person.identity, person.displayName).trim().toLowerCase();
6366
+ return name.length > 0 ? `guest-name:${name}` : `identity:${person.identity}`;
6367
+ }
6368
+ function uniqueAttendees(attendees, names = {}) {
6369
+ const byKey = /* @__PURE__ */ new Map();
6370
+ for (const person of attendees) {
6371
+ const key = attendeeKey(person, names);
6372
+ const kept = byKey.get(key);
6373
+ if (kept === void 0) {
6374
+ byKey.set(key, person);
6375
+ continue;
6373
6376
  }
6374
- );
6375
- }
6376
- function MicIcon(props) {
6377
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6378
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }),
6379
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
6380
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 19v3" })
6381
- ] });
6377
+ const keptJoin = ms(kept.joinedAt);
6378
+ const join = ms(person.joinedAt);
6379
+ if (join !== null && (keptJoin === null || join < keptJoin)) byKey.set(key, person);
6380
+ }
6381
+ return [...byKey.values()];
6382
+ }
6383
+ var KIND_RANK = {
6384
+ host: 0,
6385
+ cohost: 1,
6386
+ member: 2,
6387
+ "signed-in guest": 3,
6388
+ guest: 4,
6389
+ assistant: 5
6390
+ };
6391
+ function coveredMs(intervals) {
6392
+ const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
6393
+ let total = 0;
6394
+ let from = -Infinity;
6395
+ let to = -Infinity;
6396
+ for (const [start, end] of sorted) {
6397
+ if (start > to) {
6398
+ if (to > from) total += to - from;
6399
+ from = start;
6400
+ to = end;
6401
+ } else if (end > to) {
6402
+ to = end;
6403
+ }
6404
+ }
6405
+ if (to > from) total += to - from;
6406
+ return total;
6382
6407
  }
6383
- function MicOffIcon(props) {
6384
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6385
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M2 2l20 20" }),
6386
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M9 9v3a3 3 0 0 0 5.12 2.12" }),
6387
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M15 9.34V5a3 3 0 0 0-5.94-.6" }),
6388
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19 10v2a7 7 0 0 1-.11 1.23M5 10v2a7 7 0 0 0 11 5.66" }),
6389
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 19v3" })
6390
- ] });
6408
+ function attendanceReport(meeting, attendees, names = {}) {
6409
+ const end = ms(meeting.endedAt);
6410
+ const perJoin = attendees.map((person) => {
6411
+ const joined = ms(person.joinedAt);
6412
+ const left = ms(person.leftAt);
6413
+ const until = left ?? end;
6414
+ const duration = joined !== null && until !== null && until >= joined ? until - joined : null;
6415
+ return {
6416
+ id: person.id,
6417
+ identity: person.identity,
6418
+ name: displayNameFor(names, person.identity, person.displayName),
6419
+ userId: person.userId,
6420
+ kind: kindOf(person),
6421
+ joinedAt: person.joinedAt,
6422
+ leftAt: person.leftAt,
6423
+ durationMs: duration,
6424
+ durationToEnd: duration !== null && left === null,
6425
+ admission: person.admissionState,
6426
+ joins: 1
6427
+ };
6428
+ });
6429
+ const groups = /* @__PURE__ */ new Map();
6430
+ attendees.forEach((person, index) => {
6431
+ const key = attendeeKey(person, names);
6432
+ const group = groups.get(key) ?? { rows: [], people: [] };
6433
+ group.rows.push(perJoin[index]);
6434
+ group.people.push(person);
6435
+ groups.set(key, group);
6436
+ });
6437
+ const rows = [...groups.values()].map(({ rows: joins }) => {
6438
+ if (joins.length === 1) return joins[0];
6439
+ const came = joins.filter((row) => row.joinedAt !== null);
6440
+ const first = [...came].sort((a, b) => (ms(a.joinedAt) ?? 0) - (ms(b.joinedAt) ?? 0))[0] ?? joins[0];
6441
+ const kind = joins.reduce(
6442
+ (best, row) => KIND_RANK[row.kind] < KIND_RANK[best] ? row.kind : best,
6443
+ first.kind
6444
+ );
6445
+ const open = came.some((row) => row.leftAt === null);
6446
+ const lastLeave = came.reduce((latest, row) => {
6447
+ const left = ms(row.leftAt);
6448
+ return left !== null && (latest === null || left > latest) ? left : latest;
6449
+ }, null);
6450
+ const intervals = came.flatMap((row) => {
6451
+ const joined = ms(row.joinedAt);
6452
+ return joined !== null && row.durationMs !== null ? [[joined, joined + row.durationMs]] : [];
6453
+ });
6454
+ return {
6455
+ ...first,
6456
+ kind,
6457
+ leftAt: open ? null : lastLeave === null ? null : new Date(lastLeave).toISOString(),
6458
+ durationMs: intervals.length === 0 ? null : coveredMs(intervals),
6459
+ durationToEnd: came.some((row) => row.durationToEnd),
6460
+ joins: came.length === 0 ? joins.length : came.length
6461
+ };
6462
+ });
6463
+ const rank = KIND_RANK;
6464
+ return rows.sort((a, b) => {
6465
+ if (rank[a.kind] !== rank[b.kind]) return rank[a.kind] - rank[b.kind];
6466
+ return (ms(a.joinedAt) ?? Number.MAX_SAFE_INTEGER) - (ms(b.joinedAt) ?? Number.MAX_SAFE_INTEGER);
6467
+ });
6391
6468
  }
6392
- function CameraIcon(props) {
6393
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6394
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m23 7-7 5 7 5V7Z" }),
6395
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "1", y: "5", width: "15", height: "14", rx: "2" })
6396
- ] });
6469
+ function attendanceTotals(rows) {
6470
+ const humans = rows.filter((row) => row.kind !== "assistant");
6471
+ const attended = humans.filter((row) => row.joinedAt !== null);
6472
+ const timed = attended.filter((row) => row.durationMs !== null);
6473
+ return {
6474
+ people: attended.length,
6475
+ guests: attended.filter((row) => row.kind === "guest" || row.kind === "signed-in guest").length,
6476
+ neverAdmitted: humans.filter((row) => row.joinedAt === null).length,
6477
+ averageMs: timed.length === 0 ? null : Math.round(timed.reduce((sum, row) => sum + (row.durationMs ?? 0), 0) / timed.length)
6478
+ };
6397
6479
  }
6398
- function CameraOffIcon(props) {
6399
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6400
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M2 2l20 20" }),
6401
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10.66 5H14a2 2 0 0 1 2 2v3.34l1 1L23 7v10" }),
6402
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M16 16a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2" })
6403
- ] });
6480
+ function csvCell(value) {
6481
+ if (value === null) return "";
6482
+ const text3 = String(value);
6483
+ return /[",\n\r]/.test(text3) ? `"${text3.replace(/"/g, '""')}"` : text3;
6404
6484
  }
6405
- function ScreenShareIcon(props) {
6406
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6407
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "2", y: "3", width: "20", height: "14", rx: "2" }),
6408
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8 21h8M12 17v4" }),
6409
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 13V8m0 0-2.5 2.5M12 8l2.5 2.5" })
6410
- ] });
6485
+ var ATTENDANCE_CSV_HEADER = [
6486
+ "Meeting",
6487
+ "Meeting started",
6488
+ "Name",
6489
+ "Attended as",
6490
+ "Joined",
6491
+ "Left",
6492
+ "Minutes in the meeting",
6493
+ "Admission"
6494
+ ];
6495
+ function attendanceCsv(reports) {
6496
+ const lines = [ATTENDANCE_CSV_HEADER.join(",")];
6497
+ for (const { meeting, rows } of reports) {
6498
+ for (const row of rows) {
6499
+ lines.push(
6500
+ [
6501
+ meeting.title,
6502
+ meeting.startedAt ?? meeting.scheduledFor,
6503
+ row.name,
6504
+ row.kind,
6505
+ row.joinedAt,
6506
+ row.leftAt,
6507
+ row.durationMs === null ? null : Math.round(row.durationMs / 6e3) / 10,
6508
+ row.admission
6509
+ ].map(csvCell).join(",")
6510
+ );
6511
+ }
6512
+ }
6513
+ return `${lines.join("\r\n")}\r
6514
+ `;
6411
6515
  }
6412
- function PhoneIcon(props) {
6413
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.79 19.79 0 0 1 2.12 4.18 2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.9.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92Z" }) });
6516
+ function recordingOffsetMs(at, recording) {
6517
+ if (recording === null || recording.state !== "available") return null;
6518
+ const start = ms(recording.startedAt);
6519
+ const moment = ms(at);
6520
+ if (start === null || moment === null || moment < start) return null;
6521
+ const end = ms(recording.endedAt);
6522
+ if (end !== null && moment > end) return null;
6523
+ return moment - start;
6414
6524
  }
6415
- function PhoneOffIcon(props) {
6416
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6417
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M2 2l20 20" }),
6418
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45c.9.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67" }),
6419
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M5.53 5.53A19.79 19.79 0 0 1 2.12 4.18 2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91" })
6420
- ] });
6525
+ function clockPosition(offsetMs) {
6526
+ const total = Math.max(0, Math.floor(offsetMs / 1e3));
6527
+ const h = Math.floor(total / 3600);
6528
+ const m = Math.floor(total % 3600 / 60);
6529
+ const s = total % 60;
6530
+ const ss = String(s).padStart(2, "0");
6531
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
6421
6532
  }
6422
- function HandIcon(props) {
6423
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6424
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M18 11V6a2 2 0 0 0-4 0v5" }),
6425
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M14 10V4a2 2 0 0 0-4 0v7" }),
6426
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10 10.5V6a2 2 0 0 0-4 0v8" }),
6427
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2a8 8 0 0 1-8-8v-1a2 2 0 1 1 4 0" })
6428
- ] });
6533
+ function searchTranscript(segments, query, names = {}) {
6534
+ const needle = fold(query.trim());
6535
+ if (needle.length === 0) return [];
6536
+ const out = [];
6537
+ for (const segment of segments) {
6538
+ const hay = fold(segment.text);
6539
+ const ranges = [];
6540
+ let from = 0;
6541
+ for (; ; ) {
6542
+ const at = hay.indexOf(needle, from);
6543
+ if (at < 0) break;
6544
+ ranges.push([at, at + needle.length]);
6545
+ from = at + needle.length;
6546
+ }
6547
+ const speaker = fold(displayNameFor(names, segment.identity, segment.speaker));
6548
+ if (ranges.length > 0 || speaker.includes(needle)) {
6549
+ out.push({ segmentId: segment.id, ranges });
6550
+ }
6551
+ }
6552
+ return out;
6429
6553
  }
6430
- function ChatIcon(props) {
6431
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z" }) });
6554
+ function fold(value) {
6555
+ return value.normalize("NFD").replace(/[̀-ͯ]/g, (mark) => mark.length === 1 ? "" : mark).toLowerCase();
6432
6556
  }
6433
- function PeopleIcon(props) {
6434
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6435
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }),
6436
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "9", cy: "7", r: "4" }),
6437
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" })
6438
- ] });
6439
- }
6440
- function RecordIcon(props) {
6441
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "8", fill: "currentColor", stroke: "none" }) });
6442
- }
6443
- function SparklesIcon(props) {
6444
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6445
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 3l1.9 4.6L18.5 9.5l-4.6 1.9L12 16l-1.9-4.6L5.5 9.5l4.6-1.9L12 3Z" }),
6446
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19 15l.9 2.1 2.1.9-2.1.9-.9 2.1-.9-2.1-2.1-.9 2.1-.9L19 15Z" })
6447
- ] });
6448
- }
6449
- function LockIcon(props) {
6450
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6451
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "4", y: "10", width: "16", height: "11", rx: "2" }),
6452
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8 10V7a4 4 0 0 1 8 0v3" })
6453
- ] });
6454
- }
6455
- function LayoutIcon(props) {
6456
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6457
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
6458
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M3 9h18M9 21V9" })
6459
- ] });
6557
+ function clockOfDay(iso, options) {
6558
+ const at = new Date(iso);
6559
+ if (Number.isNaN(at.getTime())) return "";
6560
+ return at.toLocaleTimeString(options.locale, {
6561
+ hour: "numeric",
6562
+ minute: "2-digit",
6563
+ second: "2-digit",
6564
+ ...options.timeZone !== void 0 ? { timeZone: options.timeZone } : {}
6565
+ });
6460
6566
  }
6461
- function SettingsIcon(props) {
6462
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6463
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "3" }),
6464
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z" })
6465
- ] });
6567
+ function vttStamp(offsetMs) {
6568
+ const total = Math.max(0, Math.round(offsetMs));
6569
+ const h = Math.floor(total / 36e5);
6570
+ const m = Math.floor(total % 36e5 / 6e4);
6571
+ const s = Math.floor(total % 6e4 / 1e3);
6572
+ const milli = total % 1e3;
6573
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${String(milli).padStart(3, "0")}`;
6466
6574
  }
6467
- function CheckIcon(props) {
6468
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M20 6 9 17l-5-5" }) });
6575
+ function transcriptDocument(bundle, format, options = {}) {
6576
+ const names = options.names ?? bundle.names;
6577
+ const lines = bundle.transcript.filter((line) => line.text.trim().length > 0);
6578
+ const speaker = (line) => displayNameFor(names, line.identity, line.speaker);
6579
+ if (format === "vtt") {
6580
+ const zeroIso = bundle.recording?.state === "available" && bundle.recording.startedAt !== null ? bundle.recording.startedAt : bundle.meeting.startedAt ?? lines[0]?.startedAt ?? null;
6581
+ const zero = ms(zeroIso) ?? 0;
6582
+ const cues = ["WEBVTT", ""];
6583
+ lines.forEach((line, index) => {
6584
+ const start = (ms(line.startedAt) ?? zero) - zero;
6585
+ if (start < 0) return;
6586
+ const next = lines[index + 1];
6587
+ const nextStart = next !== void 0 ? (ms(next.startedAt) ?? zero) - zero : start + 4e3;
6588
+ const end = Math.max(start + 500, Math.min(nextStart, start + 1e4));
6589
+ cues.push(`${vttStamp(start)} --> ${vttStamp(end)}`);
6590
+ cues.push(`<v ${speaker(line).replace(/[<>]/g, "")}>${line.text.replace(/-->/g, "->")}`);
6591
+ cues.push("");
6592
+ });
6593
+ return cues.join("\n");
6594
+ }
6595
+ const out = [];
6596
+ const title = bundle.meeting.title;
6597
+ out.push(format === "md" ? `# ${title} \u2014 transcript` : `${title} \u2014 transcript`);
6598
+ out.push("");
6599
+ let last = null;
6600
+ for (const line of lines) {
6601
+ if (line.identity !== last) {
6602
+ if (last !== null) out.push("");
6603
+ const who = speaker(line);
6604
+ const clock = clockOfDay(line.startedAt, options);
6605
+ out.push(format === "md" ? `**${who}** \xB7 ${clock}` : `${who} (${clock})`);
6606
+ last = line.identity;
6607
+ }
6608
+ out.push(line.text.trim());
6609
+ }
6610
+ if (lines.length === 0) out.push("No transcript was captured for this meeting.");
6611
+ return `${out.join("\n")}
6612
+ `;
6469
6613
  }
6470
- function CloseIcon(props) {
6471
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M18 6 6 18M6 6l12 12" }) });
6614
+ function bullet(note) {
6615
+ const owner = note.assigneeDisplayName !== null ? ` \u2014 ${note.assigneeDisplayName}` : "";
6616
+ return `- ${note.text.trim()}${owner}`;
6472
6617
  }
6473
- function PinIcon(props) {
6474
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6475
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 17v5" }),
6476
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M9 2h6l-1 6 3.5 3.5V14H6.5v-2.5L10 8 9 2Z" })
6477
- ] });
6618
+ function notesDocument(bundle, options = {}) {
6619
+ const out = [`# ${bundle.meeting.title}`, ""];
6620
+ if (options.when) out.push(`_${options.when}_`, "");
6621
+ const people = uniqueAttendees(
6622
+ bundle.attendees.filter((person) => !person.isAgent && person.joinedAt !== null),
6623
+ bundle.names
6624
+ ).map((person) => displayNameFor(bundle.names, person.identity, person.displayName));
6625
+ if (people.length > 0) out.push(`**Attended:** ${people.join(", ")}`, "");
6626
+ out.push("## Summary", "", bundle.summary?.text.trim() || "No summary was written.", "");
6627
+ out.push("## Decisions", "");
6628
+ out.push(...bundle.decisions.length > 0 ? bundle.decisions.map(bullet) : ["None recorded."], "");
6629
+ out.push("## Action items", "");
6630
+ out.push(
6631
+ ...bundle.actionItems.length > 0 ? bundle.actionItems.map(bullet) : ["None recorded."],
6632
+ ""
6633
+ );
6634
+ if (options.link) out.push(`[Open the meeting record](${options.link})`, "");
6635
+ return out.join("\n");
6478
6636
  }
6479
- function WifiLowIcon(props) {
6480
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6481
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 20h.01" }),
6482
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8.5 16.5a5 5 0 0 1 7 0" })
6483
- ] });
6637
+ function meetingLength(meeting) {
6638
+ const start = ms(meeting.startedAt);
6639
+ const end = ms(meeting.endedAt);
6640
+ if (start === null || end === null || end < start) return null;
6641
+ return (0, import_format2.formatDurationMs)(end - start, { style: "long" });
6484
6642
  }
6485
- function SmileIcon(props) {
6486
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6487
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
6488
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }),
6489
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "9", x2: "9.01", y1: "9", y2: "9" }),
6490
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "15", x2: "15.01", y1: "9", y2: "9" })
6491
- ] });
6643
+ async function loadMeetingChat(api, meetingId, roomToken = null) {
6644
+ const operation = "meet.loadMeetingChat";
6645
+ let response;
6646
+ try {
6647
+ response = await api.request({
6648
+ path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(meetingId)}`,
6649
+ method: "GET",
6650
+ operation,
6651
+ requireAuth: roomToken === null,
6652
+ ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
6653
+ });
6654
+ } catch (error) {
6655
+ return {
6656
+ messages: [],
6657
+ refusal: {
6658
+ message: `The chat could not be read (${error.message}).`,
6659
+ remedy: error.remedy ?? "Reload the page."
6660
+ }
6661
+ };
6662
+ }
6663
+ if (!response.ok) {
6664
+ let body = null;
6665
+ try {
6666
+ body = await response.json();
6667
+ } catch {
6668
+ body = null;
6669
+ }
6670
+ const said = readServerRefusal(body);
6671
+ return {
6672
+ messages: [],
6673
+ refusal: {
6674
+ message: said?.reason ?? `The chat could not be read (the server answered ${response.status}).`,
6675
+ remedy: said?.remedy || "Reload the page."
6676
+ }
6677
+ };
6678
+ }
6679
+ const payload = await response.json();
6680
+ const rows = Array.isArray(payload.messages) ? payload.messages : [];
6681
+ return {
6682
+ messages: rows.map(
6683
+ (row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
6684
+ ).filter((message) => message !== null),
6685
+ refusal: null
6686
+ };
6492
6687
  }
6493
- function MoreIcon(props) {
6494
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6495
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "1" }),
6496
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "19", cy: "12", r: "1" }),
6497
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "5", cy: "12", r: "1" })
6498
- ] });
6688
+ function refusalOf(response, operation) {
6689
+ return refusalFromResponse(response, operation, "Nothing was sent. Try again.");
6499
6690
  }
6500
- function SearchIcon(props) {
6501
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6502
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "11", cy: "11", r: "8" }),
6503
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m21 21-4.3-4.3" })
6504
- ] });
6691
+ function asRecipient(row) {
6692
+ const sources = Array.isArray(row["sources"]) ? row["sources"] : [];
6693
+ return {
6694
+ key: String(row["key"] ?? ""),
6695
+ name: String(row["name"] ?? ""),
6696
+ email: typeof row["email"] === "string" ? row["email"] : null,
6697
+ userId: typeof row["user_id"] === "string" ? row["user_id"] : null,
6698
+ sources: sources.filter((s) => s === "invitee" || s === "attendee"),
6699
+ reachable: row["reachable"] !== false
6700
+ };
6505
6701
  }
6506
- function KeyboardIcon(props) {
6507
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6508
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10 8h.01M12 12h.01M14 8h.01M16 12h.01M18 8h.01M6 8h.01M7 16h10M8 12h.01" }),
6509
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { width: "20", height: "16", x: "2", y: "4", rx: "2" })
6510
- ] });
6702
+ async function loadFollowUpDraft(api, meetingId) {
6703
+ const operation = "meet.loadFollowUpDraft";
6704
+ const response = await api.request({
6705
+ path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
6706
+ method: "GET",
6707
+ requireAuth: true,
6708
+ operation
6709
+ });
6710
+ if (!response.ok) throw await refusalOf(response, operation);
6711
+ const body = await response.json();
6712
+ const last = body["last_sent"];
6713
+ return {
6714
+ subject: String(body["subject"] ?? ""),
6715
+ body: String(body["body"] ?? ""),
6716
+ recipients: (Array.isArray(body["recipients"]) ? body["recipients"] : []).map(
6717
+ (row) => asRecipient(row)
6718
+ ),
6719
+ wrapUpWritten: body["wrap_up_written"] === true,
6720
+ lastSent: last && typeof last["at"] === "string" ? { at: last["at"], count: typeof last["count"] === "number" ? last["count"] : 0 } : null
6721
+ };
6511
6722
  }
6512
- function SpotlightIcon(props) {
6513
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 2 15 8.5 22 9.3 17 14 18.2 21 12 17.6 5.8 21 7 14 2 9.3 9 8.5Z" }) });
6723
+ async function sendFollowUp(api, meetingId, input) {
6724
+ const operation = "meet.sendFollowUp";
6725
+ const response = await api.request({
6726
+ path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
6727
+ method: "POST",
6728
+ requireAuth: true,
6729
+ operation,
6730
+ body: {
6731
+ subject: input.subject,
6732
+ body: input.body,
6733
+ recipient_keys: [...input.recipientKeys],
6734
+ dry_run: input.dryRun === true
6735
+ }
6736
+ });
6737
+ if (!response.ok) throw await refusalOf(response, operation);
6738
+ const body = await response.json();
6739
+ return {
6740
+ dryRun: body["dry_run"] === true,
6741
+ queued: Array.isArray(body["queued"]) ? body["queued"] : [],
6742
+ skipped: Array.isArray(body["skipped"]) ? body["skipped"].map((row) => ({
6743
+ key: String(row["key"] ?? ""),
6744
+ reason: String(row["reason"] ?? "")
6745
+ })) : []
6746
+ };
6514
6747
  }
6515
- function SignalIcon({ bars, ...props }) {
6516
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6517
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M4 20v-2", opacity: bars >= 1 ? 1 : 0.3 }),
6518
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10 20v-6", opacity: bars >= 2 ? 1 : 0.3 }),
6519
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M16 20V9", opacity: bars >= 3 ? 1 : 0.3 }),
6520
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M22 20V4", opacity: bars >= 3 ? 1 : 0.3 })
6521
- ] });
6748
+
6749
+ // src/core/format.ts
6750
+ function participantSummary(participants) {
6751
+ const names = participants.filter((p) => !p.isLocal).map((p) => p.displayName);
6752
+ if (names.length === 0) return "Just you";
6753
+ if (names.length === 1) return names[0] ?? "";
6754
+ if (names.length === 2) return `${names[0]} and ${names[1]}`;
6755
+ return `${names[0]}, ${names[1]} and ${names.length - 2} ${names.length - 2 === 1 ? "other" : "others"}`;
6522
6756
  }
6523
- function SpeakerIcon(props) {
6524
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6525
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M11 5 6 9H2v6h4l5 4V5Z" }),
6757
+ function callStateLabel(state, outgoing) {
6758
+ switch (state) {
6759
+ case "dialing":
6760
+ case "ringing":
6761
+ return outgoing ? "Calling\u2026" : "Incoming call";
6762
+ case "connecting":
6763
+ return "Connecting\u2026";
6764
+ case "active":
6765
+ return "In call";
6766
+ case "declined":
6767
+ return outgoing ? "Declined" : "You declined";
6768
+ case "missed":
6769
+ return outgoing ? "No answer" : "Missed call";
6770
+ case "busy":
6771
+ return outgoing ? "On another call" : "You were busy";
6772
+ case "cancelled":
6773
+ return outgoing ? "Cancelled" : "Cancelled by caller";
6774
+ case "ended":
6775
+ return "Call ended";
6776
+ case "failed":
6777
+ return "Call failed";
6778
+ default:
6779
+ return "Call";
6780
+ }
6781
+ }
6782
+ function countLabel(count2, singular, plural = `${singular}s`) {
6783
+ return `${count2} ${count2 === 1 ? singular : plural}`;
6784
+ }
6785
+
6786
+ // src/react/icons.tsx
6787
+ var import_jsx_runtime2 = require("react/jsx-runtime");
6788
+ function Icon({ children, ...props }) {
6789
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
6790
+ "svg",
6791
+ {
6792
+ width: "20",
6793
+ height: "20",
6794
+ viewBox: "0 0 24 24",
6795
+ fill: "none",
6796
+ stroke: "currentColor",
6797
+ strokeWidth: "1.75",
6798
+ strokeLinecap: "round",
6799
+ strokeLinejoin: "round",
6800
+ "aria-hidden": "true",
6801
+ focusable: "false",
6802
+ ...props,
6803
+ children
6804
+ }
6805
+ );
6806
+ }
6807
+ function MicIcon(props) {
6808
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6809
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }),
6810
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
6811
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 19v3" })
6812
+ ] });
6813
+ }
6814
+ function MicOffIcon(props) {
6815
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6816
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M2 2l20 20" }),
6817
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M9 9v3a3 3 0 0 0 5.12 2.12" }),
6818
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M15 9.34V5a3 3 0 0 0-5.94-.6" }),
6819
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19 10v2a7 7 0 0 1-.11 1.23M5 10v2a7 7 0 0 0 11 5.66" }),
6820
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 19v3" })
6821
+ ] });
6822
+ }
6823
+ function CameraIcon(props) {
6824
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6825
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m23 7-7 5 7 5V7Z" }),
6826
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "1", y: "5", width: "15", height: "14", rx: "2" })
6827
+ ] });
6828
+ }
6829
+ function CameraOffIcon(props) {
6830
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6831
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M2 2l20 20" }),
6832
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10.66 5H14a2 2 0 0 1 2 2v3.34l1 1L23 7v10" }),
6833
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M16 16a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2" })
6834
+ ] });
6835
+ }
6836
+ function ScreenShareIcon(props) {
6837
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6838
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "2", y: "3", width: "20", height: "14", rx: "2" }),
6839
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8 21h8M12 17v4" }),
6840
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 13V8m0 0-2.5 2.5M12 8l2.5 2.5" })
6841
+ ] });
6842
+ }
6843
+ function PhoneIcon(props) {
6844
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.79 19.79 0 0 1 2.12 4.18 2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.9.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92Z" }) });
6845
+ }
6846
+ function PhoneOffIcon(props) {
6847
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6848
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M2 2l20 20" }),
6849
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45c.9.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67" }),
6850
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M5.53 5.53A19.79 19.79 0 0 1 2.12 4.18 2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91" })
6851
+ ] });
6852
+ }
6853
+ function HandIcon(props) {
6854
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6855
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M18 11V6a2 2 0 0 0-4 0v5" }),
6856
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M14 10V4a2 2 0 0 0-4 0v7" }),
6857
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10 10.5V6a2 2 0 0 0-4 0v8" }),
6858
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2a8 8 0 0 1-8-8v-1a2 2 0 1 1 4 0" })
6859
+ ] });
6860
+ }
6861
+ function ChatIcon(props) {
6862
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z" }) });
6863
+ }
6864
+ function PeopleIcon(props) {
6865
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6866
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }),
6867
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "9", cy: "7", r: "4" }),
6868
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" })
6869
+ ] });
6870
+ }
6871
+ function RecordIcon(props) {
6872
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "8", fill: "currentColor", stroke: "none" }) });
6873
+ }
6874
+ function SparklesIcon(props) {
6875
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6876
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 3l1.9 4.6L18.5 9.5l-4.6 1.9L12 16l-1.9-4.6L5.5 9.5l4.6-1.9L12 3Z" }),
6877
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19 15l.9 2.1 2.1.9-2.1.9-.9 2.1-.9-2.1-2.1-.9 2.1-.9L19 15Z" })
6878
+ ] });
6879
+ }
6880
+ function LockIcon(props) {
6881
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6882
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "4", y: "10", width: "16", height: "11", rx: "2" }),
6883
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8 10V7a4 4 0 0 1 8 0v3" })
6884
+ ] });
6885
+ }
6886
+ function LayoutIcon(props) {
6887
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6888
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
6889
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M3 9h18M9 21V9" })
6890
+ ] });
6891
+ }
6892
+ function SettingsIcon(props) {
6893
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6894
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "3" }),
6895
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z" })
6896
+ ] });
6897
+ }
6898
+ function CheckIcon(props) {
6899
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M20 6 9 17l-5-5" }) });
6900
+ }
6901
+ function CloseIcon(props) {
6902
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M18 6 6 18M6 6l12 12" }) });
6903
+ }
6904
+ function PinIcon(props) {
6905
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6906
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 17v5" }),
6907
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M9 2h6l-1 6 3.5 3.5V14H6.5v-2.5L10 8 9 2Z" })
6908
+ ] });
6909
+ }
6910
+ function WifiLowIcon(props) {
6911
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6912
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 20h.01" }),
6913
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8.5 16.5a5 5 0 0 1 7 0" })
6914
+ ] });
6915
+ }
6916
+ function SmileIcon(props) {
6917
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6918
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
6919
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }),
6920
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "9", x2: "9.01", y1: "9", y2: "9" }),
6921
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "15", x2: "15.01", y1: "9", y2: "9" })
6922
+ ] });
6923
+ }
6924
+ function MoreIcon(props) {
6925
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6926
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "1" }),
6927
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "19", cy: "12", r: "1" }),
6928
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "5", cy: "12", r: "1" })
6929
+ ] });
6930
+ }
6931
+ function SearchIcon(props) {
6932
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6933
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "11", cy: "11", r: "8" }),
6934
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m21 21-4.3-4.3" })
6935
+ ] });
6936
+ }
6937
+ function KeyboardIcon(props) {
6938
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6939
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10 8h.01M12 12h.01M14 8h.01M16 12h.01M18 8h.01M6 8h.01M7 16h10M8 12h.01" }),
6940
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { width: "20", height: "16", x: "2", y: "4", rx: "2" })
6941
+ ] });
6942
+ }
6943
+ function SpotlightIcon(props) {
6944
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 2 15 8.5 22 9.3 17 14 18.2 21 12 17.6 5.8 21 7 14 2 9.3 9 8.5Z" }) });
6945
+ }
6946
+ function SignalIcon({ bars, ...props }) {
6947
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6948
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M4 20v-2", opacity: bars >= 1 ? 1 : 0.3 }),
6949
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10 20v-6", opacity: bars >= 2 ? 1 : 0.3 }),
6950
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M16 20V9", opacity: bars >= 3 ? 1 : 0.3 }),
6951
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M22 20V4", opacity: bars >= 3 ? 1 : 0.3 })
6952
+ ] });
6953
+ }
6954
+ function SpeakerIcon(props) {
6955
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
6956
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M11 5 6 9H2v6h4l5 4V5Z" }),
6526
6957
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M15.54 8.46a5 5 0 0 1 0 7.07" }),
6527
6958
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19.07 4.93a10 10 0 0 1 0 14.14" })
6528
6959
  ] });
@@ -6553,7 +6984,7 @@ function Avatar({
6553
6984
  url,
6554
6985
  size = 40
6555
6986
  }) {
6556
- const tint = (0, import_format2.avatarPaletteIndex)(name);
6987
+ const tint = (0, import_format3.avatarPaletteIndex)(name);
6557
6988
  if (url != null && url.length > 0) {
6558
6989
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6559
6990
  "img",
@@ -6578,7 +7009,7 @@ function Avatar({
6578
7009
  background: `var(--mx-meet-avatar-${tint})`
6579
7010
  },
6580
7011
  "aria-hidden": "true",
6581
- children: (0, import_format2.getInitials)(name)
7012
+ children: (0, import_format3.getInitials)(name)
6582
7013
  }
6583
7014
  );
6584
7015
  }
@@ -6763,6 +7194,15 @@ function ControlBar({ onLeave }) {
6763
7194
  const micUnavailable = useLocalMedia().microphone.unavailable;
6764
7195
  const unread = useUnreadChat(panel === "chat");
6765
7196
  const toggle = (next) => setPanel(panel === next ? "none" : next);
7197
+ const privateChatAt = snapshot?.privateChatRequest?.at ?? null;
7198
+ const meetStore = useMeetHost()?.store ?? null;
7199
+ (0, import_react4.useEffect)(() => {
7200
+ const request = meetStore?.snapshot().privateChatRequest ?? null;
7201
+ if (request === null) return;
7202
+ setChatTo(request.to);
7203
+ setPanel("chat");
7204
+ meetStore?.patch({ privateChatRequest: null });
7205
+ }, [privateChatAt, meetStore]);
6766
7206
  useMeetShortcuts({
6767
7207
  mic: () => void room.toggleMicrophone(),
6768
7208
  camera: () => void room.toggleCamera(),
@@ -7002,7 +7442,12 @@ function Popover({
7002
7442
  placement = "up"
7003
7443
  }) {
7004
7444
  const ref = (0, import_react4.useRef)(null);
7005
- const [style, setStyle] = (0, import_react4.useState)({ visibility: "hidden" });
7445
+ const [style, setStyle] = (0, import_react4.useState)({
7446
+ position: "fixed",
7447
+ top: 0,
7448
+ left: 0,
7449
+ visibility: "hidden"
7450
+ });
7006
7451
  (0, import_react4.useLayoutEffect)(() => {
7007
7452
  const element = ref.current;
7008
7453
  const anchor = element?.parentElement ?? null;
@@ -7018,14 +7463,35 @@ function Popover({
7018
7463
  );
7019
7464
  const fitsBelow = rect.bottom + gutter + height <= window.innerHeight;
7020
7465
  const fitsAbove = rect.top - gutter - height >= 0;
7466
+ if (!fitsBelow && !fitsAbove) {
7467
+ const top = Math.max(gutter, window.innerHeight - height - gutter);
7468
+ setStyle({
7469
+ position: "fixed",
7470
+ left,
7471
+ top,
7472
+ maxHeight: window.innerHeight - top - gutter,
7473
+ overflowY: "auto"
7474
+ });
7475
+ return;
7476
+ }
7021
7477
  const down = placement === "down" ? fitsBelow || !fitsAbove : !fitsAbove && fitsBelow;
7022
7478
  setStyle(
7023
7479
  down ? { position: "fixed", left, top: rect.bottom + gutter } : { position: "fixed", left, bottom: window.innerHeight - rect.top + gutter }
7024
7480
  );
7025
7481
  };
7026
7482
  place();
7483
+ const frame = requestAnimationFrame(place);
7484
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => place());
7485
+ observer?.observe(document.body);
7486
+ observer?.observe(element);
7027
7487
  window.addEventListener("resize", place);
7028
- return () => window.removeEventListener("resize", place);
7488
+ window.addEventListener("scroll", place, true);
7489
+ return () => {
7490
+ cancelAnimationFrame(frame);
7491
+ observer?.disconnect();
7492
+ window.removeEventListener("resize", place);
7493
+ window.removeEventListener("scroll", place, true);
7494
+ };
7029
7495
  }, [placement]);
7030
7496
  (0, import_react4.useEffect)(() => {
7031
7497
  const onKey = (event) => {
@@ -7402,11 +7868,15 @@ function PersonMenu({
7402
7868
  const pinned = snapshot?.pinned === participant.identity;
7403
7869
  const spotlit = snapshot?.spotlight === participant.identity;
7404
7870
  const hasAccount = participant.userId !== null;
7871
+ const [refused, setRefused] = (0, import_react4.useState)(false);
7405
7872
  const act = (request) => {
7873
+ setRefused(false);
7406
7874
  void controls.run(request).then((result) => {
7407
7875
  if (result !== null) onClose();
7876
+ else setRefused(true);
7408
7877
  });
7409
7878
  };
7879
+ const refusal2 = refused && controls.notice?.failed === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_design_system.ErrorBox, { as: "p", className: "mx-meet__error", message: controls.notice.text, operation: "Change this participant", origin: "meet/components", children: controls.notice.text }) : null;
7410
7880
  if (mode === "rename" || mode === "report") {
7411
7881
  const renaming = mode === "rename";
7412
7882
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: renaming ? "Rename" : "Report", onClose, placement, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
@@ -7444,7 +7914,7 @@ function PersonMenu({
7444
7914
  }
7445
7915
  )
7446
7916
  ] }),
7447
- controls.notice?.failed === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__error", children: controls.notice.text }) : null,
7917
+ refusal2,
7448
7918
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu-actions", children: [
7449
7919
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Back" }),
7450
7920
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: "mx-meet__send", disabled: text3.trim().length === 0 || controls.busy, children: renaming ? "Rename" : "Report" })
@@ -7457,6 +7927,7 @@ function PersonMenu({
7457
7927
  const removing = mode === "remove";
7458
7928
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: removing ? "Remove" : "Make host", onClose, placement, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu-form", children: [
7459
7929
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__confirm-text", children: removing ? `Remove ${participant.displayName}? They leave immediately and cannot rejoin this meeting.` : `Make ${participant.displayName} the host? They get every host control and own this meeting. You stay on as a co-host.` }),
7930
+ refusal2,
7460
7931
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu-actions", children: [
7461
7932
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Cancel" }),
7462
7933
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -7487,11 +7958,15 @@ function PersonMenu({
7487
7958
  ]
7488
7959
  }
7489
7960
  ),
7490
- !self && !participant.isAgent && onMessage !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7961
+ !self && !participant.isAgent && (onMessage !== void 0 || host !== null) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7491
7962
  MenuItem,
7492
7963
  {
7493
7964
  onSelect: () => {
7494
- onMessage(participant.identity);
7965
+ if (onMessage !== void 0) onMessage(participant.identity);
7966
+ else
7967
+ host?.store.patch({
7968
+ privateChatRequest: { to: participant.identity, at: Date.now() }
7969
+ });
7495
7970
  onClose();
7496
7971
  },
7497
7972
  children: [
@@ -8118,7 +8593,7 @@ function PreJoinBody({
8118
8593
  microphone,
8119
8594
  ...meetingId !== void 0 ? { meetingId } : {},
8120
8595
  ...slug !== void 0 ? { slug } : {}
8121
- }).then(() => onJoined?.()).finally(() => setJoining(false));
8596
+ }).then(() => onJoined?.()).catch(() => void 0).finally(() => setJoining(false));
8122
8597
  },
8123
8598
  children: joining ? "Joining\u2026" : "Join now"
8124
8599
  }
@@ -8254,7 +8729,7 @@ function MeetingRoomBody({
8254
8729
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { className: "mx-meet__title", children: snapshot?.meeting?.title ?? "Meeting" }),
8255
8730
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__meta", children: [
8256
8731
  participantSummary(snapshot?.participants ?? []),
8257
- elapsed > 0 ? ` \xB7 ${(0, import_format2.formatDurationMs)(elapsed)}` : ""
8732
+ elapsed > 0 ? ` \xB7 ${(0, import_format3.formatDurationMs)(elapsed)}` : ""
8258
8733
  ] }),
8259
8734
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RecordingIndicator, {}),
8260
8735
  snapshot?.locked === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__badge", children: [
@@ -8560,7 +9035,10 @@ function MeetingRecordBody({
8560
9035
  const blocks = groupTranscript(bundle.transcript, bundle.names);
8561
9036
  const notice = recordingNotice(bundle.recording);
8562
9037
  const fileId = playableRecordingFileId(bundle.recording);
8563
- const attended = bundle.attendees.filter((person) => person.admissionState !== "denied");
9038
+ const attended = uniqueAttendees(
9039
+ bundle.attendees.filter((person) => person.admissionState !== "denied"),
9040
+ bundle.names
9041
+ );
8564
9042
  const restrictionNotices = summarizeMeetingRecordRestrictions(bundle.restrictions);
8565
9043
  const guestFinishedMeeting = host.identity.userId === null && meeting.endedAt !== null;
8566
9044
  const everyRecordPartRefused = ["attendance", "notes", "transcript", "recording"].every(
@@ -8869,514 +9347,172 @@ function RecordingPlayerBody({ fileId, host }) {
8869
9347
  const failure = thrown;
8870
9348
  setError({
8871
9349
  message: failure.message ?? "The recording could not be loaded.",
8872
- remedy: failure.remedy ?? PLAYBACK_RETRY_REMEDY
8873
- });
8874
- } finally {
8875
- setLoading(false);
8876
- }
8877
- })();
8878
- },
8879
- children: loading ? "Loading the recording\u2026" : "Play the recording"
8880
- }
8881
- )
8882
- ] });
8883
- }
8884
- function clockOf(iso) {
8885
- const at = new Date(iso);
8886
- if (Number.isNaN(at.getTime())) return "";
8887
- return at.toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" });
8888
- }
8889
- function MeetingList({ baseUrl }) {
8890
- const host = useMeetHost();
8891
- const list = useMeetings();
8892
- if (host === null) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(InertNotice, { what: "Your meetings" });
8893
- if (list.failure !== null) {
8894
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_design_system.ErrorBox, { as: "div", className: "mx-meet__empty", error: list.failure, operation: "List your meetings", origin: "meet/components", children: [
8895
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__empty-title", children: "Your meetings could not be listed" }),
8896
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__empty-body", children: list.failure.message }),
8897
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__empty-body", children: list.failure.remedy })
8898
- ] });
8899
- }
8900
- if (list.loading) {
8901
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__empty", role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__empty-title", children: "Loading your meetings\u2026" }) });
8902
- }
8903
- if (list.meetings.length === 0) {
8904
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__empty", role: "status", children: [
8905
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__empty-title", children: "No meetings yet" }),
8906
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__empty-body", children: "Create one above. Its link works before, during and after the meeting \u2014 afterwards it opens the record." })
8907
- ] });
8908
- }
8909
- const origin = baseUrl ?? (typeof window === "undefined" ? "" : window.location.origin);
8910
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { className: "mx-meet-list", children: list.meetings.map((meeting) => {
8911
- const ended = meeting.endedAt !== null;
8912
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: "mx-meet-list__row", children: [
8913
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet-list__main", children: [
8914
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet-list__title", children: [
8915
- meeting.title,
8916
- ended ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__badge", children: "Ended" }) : null
8917
- ] }),
8918
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet-list__when", children: meetingWhen(meeting) })
8919
- ] }),
8920
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("a", { className: "mx-meet-list__action", href: meetingLink(origin, meeting.slug), children: ended ? "Open the record" : "Join" })
8921
- ] }, meeting.id);
8922
- }) });
8923
- }
8924
-
8925
- // src/core/invitations.ts
8926
- var CALL_INVITE_KIND = "meet.call-invite";
8927
- var CALL_INVITE_VERSION = 1;
8928
- function callInviteAction(invite) {
8929
- return {
8930
- kind: CALL_INVITE_KIND,
8931
- version: CALL_INVITE_VERSION,
8932
- payload: {
8933
- invite_id: invite.id,
8934
- room_name: invite.roomName,
8935
- mode: invite.mode,
8936
- caller_name: invite.callerName,
8937
- expires_at: invite.expiresAt
8938
- }
8939
- };
8940
- }
8941
- function callInviteText(invite) {
8942
- return invite.mode === "video" ? `${invite.callerName} started a video call` : `${invite.callerName} started an audio call`;
8943
- }
8944
- function createCallInviteHandler(options) {
8945
- const now = options.now ?? (() => Date.now());
8946
- return {
8947
- kind: CALL_INVITE_KIND,
8948
- versions: [CALL_INVITE_VERSION],
8949
- choices: (payload) => {
8950
- if (Date.parse(payload.expires_at) < now()) return [];
8951
- return [
8952
- { id: "join", label: payload.mode === "video" ? "Join video" : "Join call", tone: "primary" },
8953
- { id: "decline", label: "Decline", tone: "neutral" }
8954
- ];
8955
- },
8956
- summarize: (payload) => payload.mode === "video" ? `${payload.caller_name} started a video call` : `${payload.caller_name} started an audio call`,
8957
- execute: async (payload, context) => {
8958
- const settledAt = new Date(now()).toISOString();
8959
- const inviteId = asCallId(payload.invite_id);
8960
- const pending = await options.repository.pendingInvites(asUserId(String(context.actorId)));
8961
- const live = pending.find((invite) => invite.id === inviteId) ?? null;
8962
- if (live === null) {
8963
- return {
8964
- kind: CALL_INVITE_KIND,
8965
- messageId: context.messageId,
8966
- actorId: context.actorId,
8967
- outcome: "unavailable",
8968
- label: "This call has ended",
8969
- settledAt,
8970
- detail: "The invite is no longer ringing \u2014 it was answered elsewhere, cancelled, or it timed out. The chips are replaced by the outcome rather than left clickable."
8971
- };
8972
- }
8973
- if (context.choice === "decline") {
8974
- await options.calls.decline(inviteId);
8975
- return {
8976
- kind: CALL_INVITE_KIND,
8977
- messageId: context.messageId,
8978
- actorId: context.actorId,
8979
- outcome: "declined",
8980
- label: "Declined",
8981
- settledAt
8982
- };
8983
- }
8984
- const accepted = await options.calls.accept(inviteId, live.mode);
8985
- await options.onJoin(accepted);
8986
- return {
8987
- kind: CALL_INVITE_KIND,
8988
- messageId: context.messageId,
8989
- actorId: context.actorId,
8990
- outcome: "applied",
8991
- label: "Joined",
8992
- settledAt
8993
- };
8994
- }
8995
- };
8996
- }
8997
- var MEETING_INVITE_KIND = "meet.meeting-invite";
8998
- var MEETING_INVITE_VERSION = 1;
8999
- function meetingInviteAction(payload) {
9000
- return { kind: MEETING_INVITE_KIND, version: MEETING_INVITE_VERSION, payload };
9001
- }
9002
- function createMeetingInviteHandler(options) {
9003
- const now = options.now ?? (() => Date.now());
9004
- return {
9005
- kind: MEETING_INVITE_KIND,
9006
- versions: [MEETING_INVITE_VERSION],
9007
- // A DURABLE LINK NEVER EXPIRES (R3) — the chip stays useful after the
9008
- // meeting too, because that is when people go looking for the notes.
9009
- choices: () => [{ id: "open", label: "Open meeting", tone: "primary" }],
9010
- summarize: (payload) => payload.scheduled_for === null ? payload.title : `${payload.title} \u2014 ${payload.scheduled_for}`,
9011
- execute: async (payload, context) => {
9012
- if (payload.slug.length === 0) {
9013
- throw new MeetError(
9014
- "invalid-response",
9015
- "meet.meeting-invite: the payload carries no slug",
9016
- "A meeting invitation without a durable slug cannot be opened. Fix the sender; never synthesize a link on the receiving side."
9017
- );
9018
- }
9019
- await options.onOpen(payload);
9020
- return {
9021
- kind: MEETING_INVITE_KIND,
9022
- messageId: context.messageId,
9023
- actorId: context.actorId,
9024
- outcome: "applied",
9025
- label: "Opened",
9026
- settledAt: new Date(now()).toISOString()
9027
- };
9028
- }
9029
- };
9030
- }
9031
-
9032
- // src/core/after.ts
9033
- var import_format4 = require("@ai-matrx/kit/format");
9034
- function kindOf(person) {
9035
- if (person.isAgent) return "assistant";
9036
- if (person.role === "host") return "host";
9037
- if (person.role === "cohost") return "cohost";
9038
- if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
9039
- return "member";
9040
- }
9041
- function ms(iso) {
9042
- if (iso === null) return null;
9043
- const at = new Date(iso).getTime();
9044
- return Number.isNaN(at) ? null : at;
9045
- }
9046
- function attendanceReport(meeting, attendees, names = {}) {
9047
- const end = ms(meeting.endedAt);
9048
- const rows = attendees.map((person) => {
9049
- const joined = ms(person.joinedAt);
9050
- const left = ms(person.leftAt);
9051
- const until = left ?? end;
9052
- const duration = joined !== null && until !== null && until >= joined ? until - joined : null;
9053
- return {
9054
- id: person.id,
9055
- identity: person.identity,
9056
- name: displayNameFor(names, person.identity, person.displayName),
9057
- userId: person.userId,
9058
- kind: kindOf(person),
9059
- joinedAt: person.joinedAt,
9060
- leftAt: person.leftAt,
9061
- durationMs: duration,
9062
- durationToEnd: duration !== null && left === null,
9063
- admission: person.admissionState
9064
- };
9065
- });
9066
- const rank = {
9067
- host: 0,
9068
- cohost: 1,
9069
- member: 2,
9070
- "signed-in guest": 3,
9071
- guest: 4,
9072
- assistant: 5
9073
- };
9074
- return rows.sort((a, b) => {
9075
- if (rank[a.kind] !== rank[b.kind]) return rank[a.kind] - rank[b.kind];
9076
- return (ms(a.joinedAt) ?? Number.MAX_SAFE_INTEGER) - (ms(b.joinedAt) ?? Number.MAX_SAFE_INTEGER);
9077
- });
9078
- }
9079
- function attendanceTotals(rows) {
9080
- const humans = rows.filter((row) => row.kind !== "assistant");
9081
- const attended = humans.filter((row) => row.joinedAt !== null);
9082
- const timed = attended.filter((row) => row.durationMs !== null);
9083
- return {
9084
- people: attended.length,
9085
- guests: attended.filter((row) => row.kind === "guest" || row.kind === "signed-in guest").length,
9086
- neverAdmitted: humans.filter((row) => row.joinedAt === null).length,
9087
- averageMs: timed.length === 0 ? null : Math.round(timed.reduce((sum, row) => sum + (row.durationMs ?? 0), 0) / timed.length)
9088
- };
9089
- }
9090
- function csvCell(value) {
9091
- if (value === null) return "";
9092
- const text3 = String(value);
9093
- return /[",\n\r]/.test(text3) ? `"${text3.replace(/"/g, '""')}"` : text3;
9094
- }
9095
- var ATTENDANCE_CSV_HEADER = [
9096
- "Meeting",
9097
- "Meeting started",
9098
- "Name",
9099
- "Attended as",
9100
- "Joined",
9101
- "Left",
9102
- "Minutes in the meeting",
9103
- "Admission"
9104
- ];
9105
- function attendanceCsv(reports) {
9106
- const lines = [ATTENDANCE_CSV_HEADER.join(",")];
9107
- for (const { meeting, rows } of reports) {
9108
- for (const row of rows) {
9109
- lines.push(
9110
- [
9111
- meeting.title,
9112
- meeting.startedAt ?? meeting.scheduledFor,
9113
- row.name,
9114
- row.kind,
9115
- row.joinedAt,
9116
- row.leftAt,
9117
- row.durationMs === null ? null : Math.round(row.durationMs / 6e3) / 10,
9118
- row.admission
9119
- ].map(csvCell).join(",")
9120
- );
9121
- }
9122
- }
9123
- return `${lines.join("\r\n")}\r
9124
- `;
9125
- }
9126
- function recordingOffsetMs(at, recording) {
9127
- if (recording === null || recording.state !== "available") return null;
9128
- const start = ms(recording.startedAt);
9129
- const moment = ms(at);
9130
- if (start === null || moment === null || moment < start) return null;
9131
- const end = ms(recording.endedAt);
9132
- if (end !== null && moment > end) return null;
9133
- return moment - start;
9134
- }
9135
- function clockPosition(offsetMs) {
9136
- const total = Math.max(0, Math.floor(offsetMs / 1e3));
9137
- const h = Math.floor(total / 3600);
9138
- const m = Math.floor(total % 3600 / 60);
9139
- const s = total % 60;
9140
- const ss = String(s).padStart(2, "0");
9141
- return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
9142
- }
9143
- function searchTranscript(segments, query, names = {}) {
9144
- const needle = fold(query.trim());
9145
- if (needle.length === 0) return [];
9146
- const out = [];
9147
- for (const segment of segments) {
9148
- const hay = fold(segment.text);
9149
- const ranges = [];
9150
- let from = 0;
9151
- for (; ; ) {
9152
- const at = hay.indexOf(needle, from);
9153
- if (at < 0) break;
9154
- ranges.push([at, at + needle.length]);
9155
- from = at + needle.length;
9156
- }
9157
- const speaker = fold(displayNameFor(names, segment.identity, segment.speaker));
9158
- if (ranges.length > 0 || speaker.includes(needle)) {
9159
- out.push({ segmentId: segment.id, ranges });
9160
- }
9161
- }
9162
- return out;
9163
- }
9164
- function fold(value) {
9165
- return value.normalize("NFD").replace(/[̀-ͯ]/g, (mark) => mark.length === 1 ? "" : mark).toLowerCase();
9166
- }
9167
- function clockOfDay(iso, options) {
9168
- const at = new Date(iso);
9169
- if (Number.isNaN(at.getTime())) return "";
9170
- return at.toLocaleTimeString(options.locale, {
9171
- hour: "numeric",
9172
- minute: "2-digit",
9173
- second: "2-digit",
9174
- ...options.timeZone !== void 0 ? { timeZone: options.timeZone } : {}
9175
- });
9176
- }
9177
- function vttStamp(offsetMs) {
9178
- const total = Math.max(0, Math.round(offsetMs));
9179
- const h = Math.floor(total / 36e5);
9180
- const m = Math.floor(total % 36e5 / 6e4);
9181
- const s = Math.floor(total % 6e4 / 1e3);
9182
- const milli = total % 1e3;
9183
- return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${String(milli).padStart(3, "0")}`;
9184
- }
9185
- function transcriptDocument(bundle, format, options = {}) {
9186
- const names = options.names ?? bundle.names;
9187
- const lines = bundle.transcript.filter((line) => line.text.trim().length > 0);
9188
- const speaker = (line) => displayNameFor(names, line.identity, line.speaker);
9189
- if (format === "vtt") {
9190
- const zeroIso = bundle.recording?.state === "available" && bundle.recording.startedAt !== null ? bundle.recording.startedAt : bundle.meeting.startedAt ?? lines[0]?.startedAt ?? null;
9191
- const zero = ms(zeroIso) ?? 0;
9192
- const cues = ["WEBVTT", ""];
9193
- lines.forEach((line, index) => {
9194
- const start = (ms(line.startedAt) ?? zero) - zero;
9195
- if (start < 0) return;
9196
- const next = lines[index + 1];
9197
- const nextStart = next !== void 0 ? (ms(next.startedAt) ?? zero) - zero : start + 4e3;
9198
- const end = Math.max(start + 500, Math.min(nextStart, start + 1e4));
9199
- cues.push(`${vttStamp(start)} --> ${vttStamp(end)}`);
9200
- cues.push(`<v ${speaker(line).replace(/[<>]/g, "")}>${line.text.replace(/-->/g, "->")}`);
9201
- cues.push("");
9202
- });
9203
- return cues.join("\n");
9204
- }
9205
- const out = [];
9206
- const title = bundle.meeting.title;
9207
- out.push(format === "md" ? `# ${title} \u2014 transcript` : `${title} \u2014 transcript`);
9208
- out.push("");
9209
- let last = null;
9210
- for (const line of lines) {
9211
- if (line.identity !== last) {
9212
- if (last !== null) out.push("");
9213
- const who = speaker(line);
9214
- const clock = clockOfDay(line.startedAt, options);
9215
- out.push(format === "md" ? `**${who}** \xB7 ${clock}` : `${who} (${clock})`);
9216
- last = line.identity;
9217
- }
9218
- out.push(line.text.trim());
9219
- }
9220
- if (lines.length === 0) out.push("No transcript was captured for this meeting.");
9221
- return `${out.join("\n")}
9222
- `;
9223
- }
9224
- function bullet(note) {
9225
- const owner = note.assigneeDisplayName !== null ? ` \u2014 ${note.assigneeDisplayName}` : "";
9226
- return `- ${note.text.trim()}${owner}`;
9227
- }
9228
- function notesDocument(bundle, options = {}) {
9229
- const out = [`# ${bundle.meeting.title}`, ""];
9230
- if (options.when) out.push(`_${options.when}_`, "");
9231
- const people = bundle.attendees.filter((person) => !person.isAgent && person.joinedAt !== null).map((person) => displayNameFor(bundle.names, person.identity, person.displayName));
9232
- if (people.length > 0) out.push(`**Attended:** ${people.join(", ")}`, "");
9233
- out.push("## Summary", "", bundle.summary?.text.trim() || "No summary was written.", "");
9234
- out.push("## Decisions", "");
9235
- out.push(...bundle.decisions.length > 0 ? bundle.decisions.map(bullet) : ["None recorded."], "");
9236
- out.push("## Action items", "");
9237
- out.push(
9238
- ...bundle.actionItems.length > 0 ? bundle.actionItems.map(bullet) : ["None recorded."],
9239
- ""
9240
- );
9241
- if (options.link) out.push(`[Open the meeting record](${options.link})`, "");
9242
- return out.join("\n");
9350
+ remedy: failure.remedy ?? PLAYBACK_RETRY_REMEDY
9351
+ });
9352
+ } finally {
9353
+ setLoading(false);
9354
+ }
9355
+ })();
9356
+ },
9357
+ children: loading ? "Loading the recording\u2026" : "Play the recording"
9358
+ }
9359
+ )
9360
+ ] });
9243
9361
  }
9244
- function meetingLength(meeting) {
9245
- const start = ms(meeting.startedAt);
9246
- const end = ms(meeting.endedAt);
9247
- if (start === null || end === null || end < start) return null;
9248
- return (0, import_format4.formatDurationMs)(end - start, { style: "long" });
9362
+ function clockOf(iso) {
9363
+ const at = new Date(iso);
9364
+ if (Number.isNaN(at.getTime())) return "";
9365
+ return at.toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" });
9249
9366
  }
9250
- async function loadMeetingChat(api, meetingId, roomToken = null) {
9251
- const operation = "meet.loadMeetingChat";
9252
- let response;
9253
- try {
9254
- response = await api.request({
9255
- path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(meetingId)}`,
9256
- method: "GET",
9257
- operation,
9258
- requireAuth: roomToken === null,
9259
- ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
9260
- });
9261
- } catch (error) {
9262
- return {
9263
- messages: [],
9264
- refusal: {
9265
- message: `The chat could not be read (${error.message}).`,
9266
- remedy: error.remedy ?? "Reload the page."
9267
- }
9268
- };
9367
+ function MeetingList({ baseUrl }) {
9368
+ const host = useMeetHost();
9369
+ const list = useMeetings();
9370
+ if (host === null) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(InertNotice, { what: "Your meetings" });
9371
+ if (list.failure !== null) {
9372
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_design_system.ErrorBox, { as: "div", className: "mx-meet__empty", error: list.failure, operation: "List your meetings", origin: "meet/components", children: [
9373
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__empty-title", children: "Your meetings could not be listed" }),
9374
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__empty-body", children: list.failure.message }),
9375
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__empty-body", children: list.failure.remedy })
9376
+ ] });
9269
9377
  }
9270
- if (!response.ok) {
9271
- let body = null;
9272
- try {
9273
- body = await response.json();
9274
- } catch {
9275
- body = null;
9276
- }
9277
- const said = readServerRefusal(body);
9278
- return {
9279
- messages: [],
9280
- refusal: {
9281
- message: said?.reason ?? `The chat could not be read (the server answered ${response.status}).`,
9282
- remedy: said?.remedy || "Reload the page."
9283
- }
9284
- };
9378
+ if (list.loading) {
9379
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__empty", role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__empty-title", children: "Loading your meetings\u2026" }) });
9285
9380
  }
9286
- const payload = await response.json();
9287
- const rows = Array.isArray(payload.messages) ? payload.messages : [];
9288
- return {
9289
- messages: rows.map(
9290
- (row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
9291
- ).filter((message) => message !== null),
9292
- refusal: null
9293
- };
9294
- }
9295
- async function refusalOf(response, operation) {
9296
- let body = null;
9297
- try {
9298
- body = await response.json();
9299
- } catch {
9300
- body = null;
9381
+ if (list.meetings.length === 0) {
9382
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__empty", role: "status", children: [
9383
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__empty-title", children: "No meetings yet" }),
9384
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__empty-body", children: "Create one above. Its link works before, during and after the meeting \u2014 afterwards it opens the record." })
9385
+ ] });
9301
9386
  }
9302
- const said = readServerRefusal(body);
9303
- return new MeetError(
9304
- response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
9305
- `${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
9306
- said?.remedy || "Nothing was sent. Try again."
9307
- );
9387
+ const origin = baseUrl ?? (typeof window === "undefined" ? "" : window.location.origin);
9388
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { className: "mx-meet-list", children: list.meetings.map((meeting) => {
9389
+ const ended = meeting.endedAt !== null;
9390
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: "mx-meet-list__row", children: [
9391
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet-list__main", children: [
9392
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet-list__title", children: [
9393
+ meeting.title,
9394
+ ended ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__badge", children: "Ended" }) : null
9395
+ ] }),
9396
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet-list__when", children: meetingWhen(meeting) })
9397
+ ] }),
9398
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("a", { className: "mx-meet-list__action", href: meetingLink(origin, meeting.slug), children: ended ? "Open the record" : "Join" })
9399
+ ] }, meeting.id);
9400
+ }) });
9308
9401
  }
9309
- function asRecipient(row) {
9310
- const sources = Array.isArray(row["sources"]) ? row["sources"] : [];
9402
+
9403
+ // src/core/invitations.ts
9404
+ var CALL_INVITE_KIND = "meet.call-invite";
9405
+ var CALL_INVITE_VERSION = 1;
9406
+ function callInviteAction(invite) {
9311
9407
  return {
9312
- key: String(row["key"] ?? ""),
9313
- name: String(row["name"] ?? ""),
9314
- email: typeof row["email"] === "string" ? row["email"] : null,
9315
- userId: typeof row["user_id"] === "string" ? row["user_id"] : null,
9316
- sources: sources.filter((s) => s === "invitee" || s === "attendee"),
9317
- reachable: row["reachable"] !== false
9408
+ kind: CALL_INVITE_KIND,
9409
+ version: CALL_INVITE_VERSION,
9410
+ payload: {
9411
+ invite_id: invite.id,
9412
+ room_name: invite.roomName,
9413
+ mode: invite.mode,
9414
+ caller_name: invite.callerName,
9415
+ expires_at: invite.expiresAt
9416
+ }
9318
9417
  };
9319
9418
  }
9320
- async function loadFollowUpDraft(api, meetingId) {
9321
- const operation = "meet.loadFollowUpDraft";
9322
- const response = await api.request({
9323
- path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
9324
- method: "GET",
9325
- requireAuth: true,
9326
- operation
9327
- });
9328
- if (!response.ok) throw await refusalOf(response, operation);
9329
- const body = await response.json();
9330
- const last = body["last_sent"];
9419
+ function callInviteText(invite) {
9420
+ return invite.mode === "video" ? `${invite.callerName} started a video call` : `${invite.callerName} started an audio call`;
9421
+ }
9422
+ function createCallInviteHandler(options) {
9423
+ const now = options.now ?? (() => Date.now());
9331
9424
  return {
9332
- subject: String(body["subject"] ?? ""),
9333
- body: String(body["body"] ?? ""),
9334
- recipients: (Array.isArray(body["recipients"]) ? body["recipients"] : []).map(
9335
- (row) => asRecipient(row)
9336
- ),
9337
- wrapUpWritten: body["wrap_up_written"] === true,
9338
- lastSent: last && typeof last["at"] === "string" ? { at: last["at"], count: typeof last["count"] === "number" ? last["count"] : 0 } : null
9425
+ kind: CALL_INVITE_KIND,
9426
+ versions: [CALL_INVITE_VERSION],
9427
+ choices: (payload) => {
9428
+ if (Date.parse(payload.expires_at) < now()) return [];
9429
+ return [
9430
+ { id: "join", label: payload.mode === "video" ? "Join video" : "Join call", tone: "primary" },
9431
+ { id: "decline", label: "Decline", tone: "neutral" }
9432
+ ];
9433
+ },
9434
+ summarize: (payload) => payload.mode === "video" ? `${payload.caller_name} started a video call` : `${payload.caller_name} started an audio call`,
9435
+ execute: async (payload, context) => {
9436
+ const settledAt = new Date(now()).toISOString();
9437
+ const inviteId = asCallId(payload.invite_id);
9438
+ const pending = await options.repository.pendingInvites(asUserId(String(context.actorId)));
9439
+ const live = pending.find((invite) => invite.id === inviteId) ?? null;
9440
+ if (live === null) {
9441
+ return {
9442
+ kind: CALL_INVITE_KIND,
9443
+ messageId: context.messageId,
9444
+ actorId: context.actorId,
9445
+ outcome: "unavailable",
9446
+ label: "This call has ended",
9447
+ settledAt,
9448
+ detail: "The invite is no longer ringing \u2014 it was answered elsewhere, cancelled, or it timed out. The chips are replaced by the outcome rather than left clickable."
9449
+ };
9450
+ }
9451
+ if (context.choice === "decline") {
9452
+ await options.calls.decline(inviteId);
9453
+ return {
9454
+ kind: CALL_INVITE_KIND,
9455
+ messageId: context.messageId,
9456
+ actorId: context.actorId,
9457
+ outcome: "declined",
9458
+ label: "Declined",
9459
+ settledAt
9460
+ };
9461
+ }
9462
+ const accepted = await options.calls.accept(inviteId, live.mode);
9463
+ await options.onJoin(accepted);
9464
+ return {
9465
+ kind: CALL_INVITE_KIND,
9466
+ messageId: context.messageId,
9467
+ actorId: context.actorId,
9468
+ outcome: "applied",
9469
+ label: "Joined",
9470
+ settledAt
9471
+ };
9472
+ }
9339
9473
  };
9340
9474
  }
9341
- async function sendFollowUp(api, meetingId, input) {
9342
- const operation = "meet.sendFollowUp";
9343
- const response = await api.request({
9344
- path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
9345
- method: "POST",
9346
- requireAuth: true,
9347
- operation,
9348
- body: {
9349
- subject: input.subject,
9350
- body: input.body,
9351
- recipient_keys: [...input.recipientKeys],
9352
- dry_run: input.dryRun === true
9353
- }
9354
- });
9355
- if (!response.ok) throw await refusalOf(response, operation);
9356
- const body = await response.json();
9475
+ var MEETING_INVITE_KIND = "meet.meeting-invite";
9476
+ var MEETING_INVITE_VERSION = 1;
9477
+ function meetingInviteAction(payload) {
9478
+ return { kind: MEETING_INVITE_KIND, version: MEETING_INVITE_VERSION, payload };
9479
+ }
9480
+ function createMeetingInviteHandler(options) {
9481
+ const now = options.now ?? (() => Date.now());
9357
9482
  return {
9358
- dryRun: body["dry_run"] === true,
9359
- queued: Array.isArray(body["queued"]) ? body["queued"] : [],
9360
- skipped: Array.isArray(body["skipped"]) ? body["skipped"].map((row) => ({
9361
- key: String(row["key"] ?? ""),
9362
- reason: String(row["reason"] ?? "")
9363
- })) : []
9483
+ kind: MEETING_INVITE_KIND,
9484
+ versions: [MEETING_INVITE_VERSION],
9485
+ // A DURABLE LINK NEVER EXPIRES (R3) — the chip stays useful after the
9486
+ // meeting too, because that is when people go looking for the notes.
9487
+ choices: () => [{ id: "open", label: "Open meeting", tone: "primary" }],
9488
+ summarize: (payload) => payload.scheduled_for === null ? payload.title : `${payload.title} \u2014 ${payload.scheduled_for}`,
9489
+ execute: async (payload, context) => {
9490
+ if (payload.slug.length === 0) {
9491
+ throw new MeetError(
9492
+ "invalid-response",
9493
+ "meet.meeting-invite: the payload carries no slug",
9494
+ "A meeting invitation without a durable slug cannot be opened. Fix the sender; never synthesize a link on the receiving side."
9495
+ );
9496
+ }
9497
+ await options.onOpen(payload);
9498
+ return {
9499
+ kind: MEETING_INVITE_KIND,
9500
+ messageId: context.messageId,
9501
+ actorId: context.actorId,
9502
+ outcome: "applied",
9503
+ label: "Opened",
9504
+ settledAt: new Date(now()).toISOString()
9505
+ };
9506
+ }
9364
9507
  };
9365
9508
  }
9366
9509
 
9367
9510
  // src/core/management.ts
9368
- async function refusal(response, operation) {
9369
- let body = null;
9370
- try {
9371
- body = await response.json();
9372
- } catch {
9373
- body = null;
9374
- }
9375
- const said = readServerRefusal(body);
9376
- return new MeetError(
9377
- response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
9378
- `${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
9379
- said?.remedy || "The meeting itself is saved \u2014 only the telling failed. Try again; the call is idempotent."
9511
+ function refusal(response, operation) {
9512
+ return refusalFromResponse(
9513
+ response,
9514
+ operation,
9515
+ "The meeting itself is saved \u2014 only the telling failed. Try again; the call is idempotent."
9380
9516
  );
9381
9517
  }
9382
9518
  function count(body, key) {