@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.js CHANGED
@@ -43,6 +43,20 @@ var MeetError = class extends Error {
43
43
  return this.code === "permission-denied" || this.code === "device-not-found" || this.code === "device-in-use" || this.code === "insecure-context";
44
44
  }
45
45
  };
46
+ var SERVER_FAILED_SENTENCE = "The server hit an unexpected problem and nothing was changed.";
47
+ function isRawServerText(value) {
48
+ const text3 = value.trim();
49
+ 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);
50
+ }
51
+ function humanDatabaseSentence(message) {
52
+ const text3 = message.trim();
53
+ if (text3.length === 0 || isRawServerText(text3)) return null;
54
+ if (/permission denied for|violates|relation "|does not exist|syntax error|column "|jwt|pgrst|duplicate key|null value in/i.test(
55
+ text3
56
+ ))
57
+ return null;
58
+ return text3;
59
+ }
46
60
  var FORBIDDEN_CODES = /* @__PURE__ */ new Set(["42501", "PGRST301", "PGRST302"]);
47
61
  var NOT_FOUND_CODES = /* @__PURE__ */ new Set(["PGRST116", "PGRST205"]);
48
62
  var CONFLICT_CODES = /* @__PURE__ */ new Set(["23505", "40001", "55000"]);
@@ -70,8 +84,8 @@ function normalizeMeetError(error, operation) {
70
84
  if (code !== void 0 && FORBIDDEN_CODES.has(code)) {
71
85
  return new MeetError(
72
86
  "forbidden",
73
- `${operation}: denied by the database (${code}: ${message})`,
74
- "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.",
87
+ humanDatabaseSentence(message) ?? "You don't have access to do that in this meeting.",
88
+ "Ask the meeting's host to do it, or to give you access.",
75
89
  error
76
90
  );
77
91
  }
@@ -86,15 +100,16 @@ function normalizeMeetError(error, operation) {
86
100
  if (code !== void 0 && CONFLICT_CODES.has(code)) {
87
101
  return new MeetError(
88
102
  "conflict",
89
- `${operation}: unique violation (${code}: ${message})`,
90
- "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.",
103
+ humanDatabaseSentence(message) ?? "This meeting changed a moment ago, so nothing was saved.",
104
+ "Reload to see the latest, then try again.",
91
105
  error
92
106
  );
93
107
  }
108
+ const hint = error?.hint;
94
109
  return new MeetError(
95
110
  "transport",
96
- `${operation}: ${message}`,
97
- "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.",
111
+ /failed to fetch|networkerror|load failed|network request failed/i.test(message) ? "The server could not be reached." : humanDatabaseSentence(message) ?? SERVER_FAILED_SENTENCE,
112
+ typeof hint === "string" && humanDatabaseSentence(hint) !== null ? hint.trim() : "Nothing was changed \u2014 retry in a moment; if it keeps happening, reload the page.",
98
113
  error
99
114
  );
100
115
  }
@@ -502,6 +517,24 @@ function readText(data) {
502
517
  }
503
518
  return "";
504
519
  }
520
+ async function refusalFromResponse(response, operation, fallbackRemedy) {
521
+ let body = null;
522
+ try {
523
+ body = await response.json();
524
+ } catch {
525
+ body = null;
526
+ }
527
+ const said = readServerRefusal(body);
528
+ const code = response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport";
529
+ const error = new MeetError(
530
+ code,
531
+ said?.reason ?? SERVER_FAILED_SENTENCE,
532
+ said !== null && said.remedy.length > 0 ? said.remedy : fallbackRemedy
533
+ );
534
+ error.operation = operation;
535
+ error.status = response.status;
536
+ return error;
537
+ }
505
538
  function readServerRefusal(body) {
506
539
  if (typeof body === "string") {
507
540
  return body.trim().length === 0 ? null : { reason: body.trim(), remedy: "" };
@@ -520,7 +553,8 @@ function readServerRefusal(body) {
520
553
  return messages.length === 0 ? null : { reason: messages.join("; "), remedy: "" };
521
554
  }
522
555
  if (typeof detail === "object" && detail !== null) return readServerRefusal(detail);
523
- 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;
556
+ const humanOrNull = (value) => typeof value === "string" && value.trim().length > 0 && !isRawServerText(value) ? value.trim() : null;
557
+ 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);
524
558
  if (reason === null) return null;
525
559
  const remedy = typeof row["remedy"] === "string" ? row["remedy"] : "";
526
560
  if (remedy.length > 0) return { reason, remedy };
@@ -1574,6 +1608,7 @@ var EMPTY = {
1574
1608
  spotlight: null,
1575
1609
  moderationNotice: null,
1576
1610
  actionNotice: null,
1611
+ privateChatRequest: null,
1577
1612
  effects: DEFAULT_EFFECTS,
1578
1613
  transcript: [],
1579
1614
  notes: [],
@@ -3763,6 +3798,10 @@ function createRoomEngine(options) {
3763
3798
  }
3764
3799
  },
3765
3800
  disconnected(reason) {
3801
+ if (current !== null) {
3802
+ tokens.invalidate(current.roomName);
3803
+ current = null;
3804
+ }
3766
3805
  store.patch({ phase: "left" });
3767
3806
  options.onLeft?.(reason);
3768
3807
  }
@@ -4065,19 +4104,8 @@ function createInRoomClient(options) {
4065
4104
  const token = options.resolveRoomToken();
4066
4105
  return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
4067
4106
  }
4068
- async function refusal2(response, operation, remedy) {
4069
- let body = null;
4070
- try {
4071
- body = await response.json();
4072
- } catch {
4073
- body = null;
4074
- }
4075
- const said = readServerRefusal(body);
4076
- return new MeetError(
4077
- response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
4078
- said?.reason ?? `${operation}: the server answered ${response.status}`,
4079
- said?.remedy !== void 0 && said.remedy.length > 0 ? said.remedy : remedy
4080
- );
4107
+ function refusal2(response, operation, remedy) {
4108
+ return refusalFromResponse(response, operation, remedy);
4081
4109
  }
4082
4110
  return {
4083
4111
  async moderate(request) {
@@ -6037,195 +6065,596 @@ function useGuestRecordAccess(meeting) {
6037
6065
  // src/react/components.tsx
6038
6066
  import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
6039
6067
  import { ErrorBox } from "@ai-matrx/design-system";
6040
- import { avatarPaletteIndex, formatDurationMs as formatDurationMs2, getInitials } from "@ai-matrx/kit/format";
6068
+ import { avatarPaletteIndex, formatDurationMs as formatDurationMs3, getInitials } from "@ai-matrx/kit/format";
6041
6069
 
6042
- // src/core/format.ts
6043
- function participantSummary(participants) {
6044
- const names = participants.filter((p) => !p.isLocal).map((p) => p.displayName);
6045
- if (names.length === 0) return "Just you";
6046
- if (names.length === 1) return names[0] ?? "";
6047
- if (names.length === 2) return `${names[0]} and ${names[1]}`;
6048
- return `${names[0]}, ${names[1]} and ${names.length - 2} ${names.length - 2 === 1 ? "other" : "others"}`;
6049
- }
6050
- function callStateLabel(state, outgoing) {
6051
- switch (state) {
6052
- case "dialing":
6053
- case "ringing":
6054
- return outgoing ? "Calling\u2026" : "Incoming call";
6055
- case "connecting":
6056
- return "Connecting\u2026";
6057
- case "active":
6058
- return "In call";
6059
- case "declined":
6060
- return outgoing ? "Declined" : "You declined";
6061
- case "missed":
6062
- return outgoing ? "No answer" : "Missed call";
6063
- case "busy":
6064
- return outgoing ? "On another call" : "You were busy";
6065
- case "cancelled":
6066
- return outgoing ? "Cancelled" : "Cancelled by caller";
6067
- case "ended":
6068
- return "Call ended";
6069
- case "failed":
6070
- return "Call failed";
6071
- default:
6072
- return "Call";
6073
- }
6070
+ // src/core/after.ts
6071
+ import { formatDurationMs as formatDurationMs2 } from "@ai-matrx/kit/format";
6072
+ function kindOf(person) {
6073
+ if (person.isAgent) return "assistant";
6074
+ if (person.role === "host") return "host";
6075
+ if (person.role === "cohost") return "cohost";
6076
+ if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
6077
+ return "member";
6074
6078
  }
6075
- function countLabel(count2, singular, plural = `${singular}s`) {
6076
- return `${count2} ${count2 === 1 ? singular : plural}`;
6079
+ function ms(iso) {
6080
+ if (iso === null) return null;
6081
+ const at = new Date(iso).getTime();
6082
+ return Number.isNaN(at) ? null : at;
6077
6083
  }
6078
-
6079
- // src/react/icons.tsx
6080
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
6081
- function Icon({ children, ...props }) {
6082
- return /* @__PURE__ */ jsx2(
6083
- "svg",
6084
- {
6085
- width: "20",
6086
- height: "20",
6087
- viewBox: "0 0 24 24",
6088
- fill: "none",
6089
- stroke: "currentColor",
6090
- strokeWidth: "1.75",
6091
- strokeLinecap: "round",
6092
- strokeLinejoin: "round",
6093
- "aria-hidden": "true",
6094
- focusable: "false",
6095
- ...props,
6096
- children
6084
+ function attendeeKey(person, names = {}) {
6085
+ if (person.userId !== null) return `user:${person.userId}`;
6086
+ if (person.isAgent) return `agent:${person.identity}`;
6087
+ const name = displayNameFor(names, person.identity, person.displayName).trim().toLowerCase();
6088
+ return name.length > 0 ? `guest-name:${name}` : `identity:${person.identity}`;
6089
+ }
6090
+ function uniqueAttendees(attendees, names = {}) {
6091
+ const byKey = /* @__PURE__ */ new Map();
6092
+ for (const person of attendees) {
6093
+ const key = attendeeKey(person, names);
6094
+ const kept = byKey.get(key);
6095
+ if (kept === void 0) {
6096
+ byKey.set(key, person);
6097
+ continue;
6097
6098
  }
6098
- );
6099
+ const keptJoin = ms(kept.joinedAt);
6100
+ const join = ms(person.joinedAt);
6101
+ if (join !== null && (keptJoin === null || join < keptJoin)) byKey.set(key, person);
6102
+ }
6103
+ return [...byKey.values()];
6104
+ }
6105
+ var KIND_RANK = {
6106
+ host: 0,
6107
+ cohost: 1,
6108
+ member: 2,
6109
+ "signed-in guest": 3,
6110
+ guest: 4,
6111
+ assistant: 5
6112
+ };
6113
+ function coveredMs(intervals) {
6114
+ const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
6115
+ let total = 0;
6116
+ let from = -Infinity;
6117
+ let to = -Infinity;
6118
+ for (const [start, end] of sorted) {
6119
+ if (start > to) {
6120
+ if (to > from) total += to - from;
6121
+ from = start;
6122
+ to = end;
6123
+ } else if (end > to) {
6124
+ to = end;
6125
+ }
6126
+ }
6127
+ if (to > from) total += to - from;
6128
+ return total;
6099
6129
  }
6100
- function MicIcon(props) {
6101
- return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6102
- /* @__PURE__ */ jsx2("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }),
6103
- /* @__PURE__ */ jsx2("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
6104
- /* @__PURE__ */ jsx2("path", { d: "M12 19v3" })
6105
- ] });
6130
+ function attendanceReport(meeting, attendees, names = {}) {
6131
+ const end = ms(meeting.endedAt);
6132
+ const perJoin = attendees.map((person) => {
6133
+ const joined = ms(person.joinedAt);
6134
+ const left = ms(person.leftAt);
6135
+ const until = left ?? end;
6136
+ const duration = joined !== null && until !== null && until >= joined ? until - joined : null;
6137
+ return {
6138
+ id: person.id,
6139
+ identity: person.identity,
6140
+ name: displayNameFor(names, person.identity, person.displayName),
6141
+ userId: person.userId,
6142
+ kind: kindOf(person),
6143
+ joinedAt: person.joinedAt,
6144
+ leftAt: person.leftAt,
6145
+ durationMs: duration,
6146
+ durationToEnd: duration !== null && left === null,
6147
+ admission: person.admissionState,
6148
+ joins: 1
6149
+ };
6150
+ });
6151
+ const groups = /* @__PURE__ */ new Map();
6152
+ attendees.forEach((person, index) => {
6153
+ const key = attendeeKey(person, names);
6154
+ const group = groups.get(key) ?? { rows: [], people: [] };
6155
+ group.rows.push(perJoin[index]);
6156
+ group.people.push(person);
6157
+ groups.set(key, group);
6158
+ });
6159
+ const rows = [...groups.values()].map(({ rows: joins }) => {
6160
+ if (joins.length === 1) return joins[0];
6161
+ const came = joins.filter((row) => row.joinedAt !== null);
6162
+ const first = [...came].sort((a, b) => (ms(a.joinedAt) ?? 0) - (ms(b.joinedAt) ?? 0))[0] ?? joins[0];
6163
+ const kind = joins.reduce(
6164
+ (best, row) => KIND_RANK[row.kind] < KIND_RANK[best] ? row.kind : best,
6165
+ first.kind
6166
+ );
6167
+ const open = came.some((row) => row.leftAt === null);
6168
+ const lastLeave = came.reduce((latest, row) => {
6169
+ const left = ms(row.leftAt);
6170
+ return left !== null && (latest === null || left > latest) ? left : latest;
6171
+ }, null);
6172
+ const intervals = came.flatMap((row) => {
6173
+ const joined = ms(row.joinedAt);
6174
+ return joined !== null && row.durationMs !== null ? [[joined, joined + row.durationMs]] : [];
6175
+ });
6176
+ return {
6177
+ ...first,
6178
+ kind,
6179
+ leftAt: open ? null : lastLeave === null ? null : new Date(lastLeave).toISOString(),
6180
+ durationMs: intervals.length === 0 ? null : coveredMs(intervals),
6181
+ durationToEnd: came.some((row) => row.durationToEnd),
6182
+ joins: came.length === 0 ? joins.length : came.length
6183
+ };
6184
+ });
6185
+ const rank = KIND_RANK;
6186
+ return rows.sort((a, b) => {
6187
+ if (rank[a.kind] !== rank[b.kind]) return rank[a.kind] - rank[b.kind];
6188
+ return (ms(a.joinedAt) ?? Number.MAX_SAFE_INTEGER) - (ms(b.joinedAt) ?? Number.MAX_SAFE_INTEGER);
6189
+ });
6106
6190
  }
6107
- function MicOffIcon(props) {
6108
- return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6109
- /* @__PURE__ */ jsx2("path", { d: "M2 2l20 20" }),
6110
- /* @__PURE__ */ jsx2("path", { d: "M9 9v3a3 3 0 0 0 5.12 2.12" }),
6111
- /* @__PURE__ */ jsx2("path", { d: "M15 9.34V5a3 3 0 0 0-5.94-.6" }),
6112
- /* @__PURE__ */ jsx2("path", { d: "M19 10v2a7 7 0 0 1-.11 1.23M5 10v2a7 7 0 0 0 11 5.66" }),
6113
- /* @__PURE__ */ jsx2("path", { d: "M12 19v3" })
6114
- ] });
6191
+ function attendanceTotals(rows) {
6192
+ const humans = rows.filter((row) => row.kind !== "assistant");
6193
+ const attended = humans.filter((row) => row.joinedAt !== null);
6194
+ const timed = attended.filter((row) => row.durationMs !== null);
6195
+ return {
6196
+ people: attended.length,
6197
+ guests: attended.filter((row) => row.kind === "guest" || row.kind === "signed-in guest").length,
6198
+ neverAdmitted: humans.filter((row) => row.joinedAt === null).length,
6199
+ averageMs: timed.length === 0 ? null : Math.round(timed.reduce((sum, row) => sum + (row.durationMs ?? 0), 0) / timed.length)
6200
+ };
6115
6201
  }
6116
- function CameraIcon(props) {
6117
- return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6118
- /* @__PURE__ */ jsx2("path", { d: "m23 7-7 5 7 5V7Z" }),
6119
- /* @__PURE__ */ jsx2("rect", { x: "1", y: "5", width: "15", height: "14", rx: "2" })
6120
- ] });
6202
+ function csvCell(value) {
6203
+ if (value === null) return "";
6204
+ const text3 = String(value);
6205
+ return /[",\n\r]/.test(text3) ? `"${text3.replace(/"/g, '""')}"` : text3;
6121
6206
  }
6122
- function CameraOffIcon(props) {
6123
- return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6124
- /* @__PURE__ */ jsx2("path", { d: "M2 2l20 20" }),
6125
- /* @__PURE__ */ jsx2("path", { d: "M10.66 5H14a2 2 0 0 1 2 2v3.34l1 1L23 7v10" }),
6126
- /* @__PURE__ */ jsx2("path", { d: "M16 16a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2" })
6127
- ] });
6207
+ var ATTENDANCE_CSV_HEADER = [
6208
+ "Meeting",
6209
+ "Meeting started",
6210
+ "Name",
6211
+ "Attended as",
6212
+ "Joined",
6213
+ "Left",
6214
+ "Minutes in the meeting",
6215
+ "Admission"
6216
+ ];
6217
+ function attendanceCsv(reports) {
6218
+ const lines = [ATTENDANCE_CSV_HEADER.join(",")];
6219
+ for (const { meeting, rows } of reports) {
6220
+ for (const row of rows) {
6221
+ lines.push(
6222
+ [
6223
+ meeting.title,
6224
+ meeting.startedAt ?? meeting.scheduledFor,
6225
+ row.name,
6226
+ row.kind,
6227
+ row.joinedAt,
6228
+ row.leftAt,
6229
+ row.durationMs === null ? null : Math.round(row.durationMs / 6e3) / 10,
6230
+ row.admission
6231
+ ].map(csvCell).join(",")
6232
+ );
6233
+ }
6234
+ }
6235
+ return `${lines.join("\r\n")}\r
6236
+ `;
6128
6237
  }
6129
- function ScreenShareIcon(props) {
6130
- return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6131
- /* @__PURE__ */ jsx2("rect", { x: "2", y: "3", width: "20", height: "14", rx: "2" }),
6132
- /* @__PURE__ */ jsx2("path", { d: "M8 21h8M12 17v4" }),
6133
- /* @__PURE__ */ jsx2("path", { d: "M12 13V8m0 0-2.5 2.5M12 8l2.5 2.5" })
6134
- ] });
6238
+ function recordingOffsetMs(at, recording) {
6239
+ if (recording === null || recording.state !== "available") return null;
6240
+ const start = ms(recording.startedAt);
6241
+ const moment = ms(at);
6242
+ if (start === null || moment === null || moment < start) return null;
6243
+ const end = ms(recording.endedAt);
6244
+ if (end !== null && moment > end) return null;
6245
+ return moment - start;
6135
6246
  }
6136
- function PhoneIcon(props) {
6137
- return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("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" }) });
6247
+ function clockPosition(offsetMs) {
6248
+ const total = Math.max(0, Math.floor(offsetMs / 1e3));
6249
+ const h = Math.floor(total / 3600);
6250
+ const m = Math.floor(total % 3600 / 60);
6251
+ const s = total % 60;
6252
+ const ss = String(s).padStart(2, "0");
6253
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
6138
6254
  }
6139
- function PhoneOffIcon(props) {
6140
- return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6141
- /* @__PURE__ */ jsx2("path", { d: "M2 2l20 20" }),
6142
- /* @__PURE__ */ jsx2("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" }),
6143
- /* @__PURE__ */ jsx2("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" })
6144
- ] });
6255
+ function searchTranscript(segments, query, names = {}) {
6256
+ const needle = fold(query.trim());
6257
+ if (needle.length === 0) return [];
6258
+ const out = [];
6259
+ for (const segment of segments) {
6260
+ const hay = fold(segment.text);
6261
+ const ranges = [];
6262
+ let from = 0;
6263
+ for (; ; ) {
6264
+ const at = hay.indexOf(needle, from);
6265
+ if (at < 0) break;
6266
+ ranges.push([at, at + needle.length]);
6267
+ from = at + needle.length;
6268
+ }
6269
+ const speaker = fold(displayNameFor(names, segment.identity, segment.speaker));
6270
+ if (ranges.length > 0 || speaker.includes(needle)) {
6271
+ out.push({ segmentId: segment.id, ranges });
6272
+ }
6273
+ }
6274
+ return out;
6145
6275
  }
6146
- function HandIcon(props) {
6147
- return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6148
- /* @__PURE__ */ jsx2("path", { d: "M18 11V6a2 2 0 0 0-4 0v5" }),
6149
- /* @__PURE__ */ jsx2("path", { d: "M14 10V4a2 2 0 0 0-4 0v7" }),
6150
- /* @__PURE__ */ jsx2("path", { d: "M10 10.5V6a2 2 0 0 0-4 0v8" }),
6151
- /* @__PURE__ */ jsx2("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" })
6152
- ] });
6276
+ function fold(value) {
6277
+ return value.normalize("NFD").replace(/[̀-ͯ]/g, (mark) => mark.length === 1 ? "" : mark).toLowerCase();
6153
6278
  }
6154
- function ChatIcon(props) {
6155
- return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z" }) });
6279
+ function clockOfDay(iso, options) {
6280
+ const at = new Date(iso);
6281
+ if (Number.isNaN(at.getTime())) return "";
6282
+ return at.toLocaleTimeString(options.locale, {
6283
+ hour: "numeric",
6284
+ minute: "2-digit",
6285
+ second: "2-digit",
6286
+ ...options.timeZone !== void 0 ? { timeZone: options.timeZone } : {}
6287
+ });
6156
6288
  }
6157
- function PeopleIcon(props) {
6158
- return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6159
- /* @__PURE__ */ jsx2("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }),
6160
- /* @__PURE__ */ jsx2("circle", { cx: "9", cy: "7", r: "4" }),
6161
- /* @__PURE__ */ jsx2("path", { d: "M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" })
6162
- ] });
6289
+ function vttStamp(offsetMs) {
6290
+ const total = Math.max(0, Math.round(offsetMs));
6291
+ const h = Math.floor(total / 36e5);
6292
+ const m = Math.floor(total % 36e5 / 6e4);
6293
+ const s = Math.floor(total % 6e4 / 1e3);
6294
+ const milli = total % 1e3;
6295
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${String(milli).padStart(3, "0")}`;
6163
6296
  }
6164
- function RecordIcon(props) {
6165
- return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "8", fill: "currentColor", stroke: "none" }) });
6297
+ function transcriptDocument(bundle, format, options = {}) {
6298
+ const names = options.names ?? bundle.names;
6299
+ const lines = bundle.transcript.filter((line) => line.text.trim().length > 0);
6300
+ const speaker = (line) => displayNameFor(names, line.identity, line.speaker);
6301
+ if (format === "vtt") {
6302
+ const zeroIso = bundle.recording?.state === "available" && bundle.recording.startedAt !== null ? bundle.recording.startedAt : bundle.meeting.startedAt ?? lines[0]?.startedAt ?? null;
6303
+ const zero = ms(zeroIso) ?? 0;
6304
+ const cues = ["WEBVTT", ""];
6305
+ lines.forEach((line, index) => {
6306
+ const start = (ms(line.startedAt) ?? zero) - zero;
6307
+ if (start < 0) return;
6308
+ const next = lines[index + 1];
6309
+ const nextStart = next !== void 0 ? (ms(next.startedAt) ?? zero) - zero : start + 4e3;
6310
+ const end = Math.max(start + 500, Math.min(nextStart, start + 1e4));
6311
+ cues.push(`${vttStamp(start)} --> ${vttStamp(end)}`);
6312
+ cues.push(`<v ${speaker(line).replace(/[<>]/g, "")}>${line.text.replace(/-->/g, "->")}`);
6313
+ cues.push("");
6314
+ });
6315
+ return cues.join("\n");
6316
+ }
6317
+ const out = [];
6318
+ const title = bundle.meeting.title;
6319
+ out.push(format === "md" ? `# ${title} \u2014 transcript` : `${title} \u2014 transcript`);
6320
+ out.push("");
6321
+ let last = null;
6322
+ for (const line of lines) {
6323
+ if (line.identity !== last) {
6324
+ if (last !== null) out.push("");
6325
+ const who = speaker(line);
6326
+ const clock = clockOfDay(line.startedAt, options);
6327
+ out.push(format === "md" ? `**${who}** \xB7 ${clock}` : `${who} (${clock})`);
6328
+ last = line.identity;
6329
+ }
6330
+ out.push(line.text.trim());
6331
+ }
6332
+ if (lines.length === 0) out.push("No transcript was captured for this meeting.");
6333
+ return `${out.join("\n")}
6334
+ `;
6166
6335
  }
6167
- function SparklesIcon(props) {
6168
- return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6169
- /* @__PURE__ */ jsx2("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" }),
6170
- /* @__PURE__ */ jsx2("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" })
6171
- ] });
6336
+ function bullet(note) {
6337
+ const owner = note.assigneeDisplayName !== null ? ` \u2014 ${note.assigneeDisplayName}` : "";
6338
+ return `- ${note.text.trim()}${owner}`;
6172
6339
  }
6173
- function LockIcon(props) {
6174
- return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6175
- /* @__PURE__ */ jsx2("rect", { x: "4", y: "10", width: "16", height: "11", rx: "2" }),
6176
- /* @__PURE__ */ jsx2("path", { d: "M8 10V7a4 4 0 0 1 8 0v3" })
6177
- ] });
6340
+ function notesDocument(bundle, options = {}) {
6341
+ const out = [`# ${bundle.meeting.title}`, ""];
6342
+ if (options.when) out.push(`_${options.when}_`, "");
6343
+ const people = uniqueAttendees(
6344
+ bundle.attendees.filter((person) => !person.isAgent && person.joinedAt !== null),
6345
+ bundle.names
6346
+ ).map((person) => displayNameFor(bundle.names, person.identity, person.displayName));
6347
+ if (people.length > 0) out.push(`**Attended:** ${people.join(", ")}`, "");
6348
+ out.push("## Summary", "", bundle.summary?.text.trim() || "No summary was written.", "");
6349
+ out.push("## Decisions", "");
6350
+ out.push(...bundle.decisions.length > 0 ? bundle.decisions.map(bullet) : ["None recorded."], "");
6351
+ out.push("## Action items", "");
6352
+ out.push(
6353
+ ...bundle.actionItems.length > 0 ? bundle.actionItems.map(bullet) : ["None recorded."],
6354
+ ""
6355
+ );
6356
+ if (options.link) out.push(`[Open the meeting record](${options.link})`, "");
6357
+ return out.join("\n");
6178
6358
  }
6179
- function LayoutIcon(props) {
6180
- return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6181
- /* @__PURE__ */ jsx2("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
6182
- /* @__PURE__ */ jsx2("path", { d: "M3 9h18M9 21V9" })
6183
- ] });
6359
+ function meetingLength(meeting) {
6360
+ const start = ms(meeting.startedAt);
6361
+ const end = ms(meeting.endedAt);
6362
+ if (start === null || end === null || end < start) return null;
6363
+ return formatDurationMs2(end - start, { style: "long" });
6184
6364
  }
6185
- function SettingsIcon(props) {
6186
- return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6187
- /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "3" }),
6188
- /* @__PURE__ */ jsx2("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" })
6189
- ] });
6365
+ async function loadMeetingChat(api, meetingId, roomToken = null) {
6366
+ const operation = "meet.loadMeetingChat";
6367
+ let response;
6368
+ try {
6369
+ response = await api.request({
6370
+ path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(meetingId)}`,
6371
+ method: "GET",
6372
+ operation,
6373
+ requireAuth: roomToken === null,
6374
+ ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
6375
+ });
6376
+ } catch (error) {
6377
+ return {
6378
+ messages: [],
6379
+ refusal: {
6380
+ message: `The chat could not be read (${error.message}).`,
6381
+ remedy: error.remedy ?? "Reload the page."
6382
+ }
6383
+ };
6384
+ }
6385
+ if (!response.ok) {
6386
+ let body = null;
6387
+ try {
6388
+ body = await response.json();
6389
+ } catch {
6390
+ body = null;
6391
+ }
6392
+ const said = readServerRefusal(body);
6393
+ return {
6394
+ messages: [],
6395
+ refusal: {
6396
+ message: said?.reason ?? `The chat could not be read (the server answered ${response.status}).`,
6397
+ remedy: said?.remedy || "Reload the page."
6398
+ }
6399
+ };
6400
+ }
6401
+ const payload = await response.json();
6402
+ const rows = Array.isArray(payload.messages) ? payload.messages : [];
6403
+ return {
6404
+ messages: rows.map(
6405
+ (row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
6406
+ ).filter((message) => message !== null),
6407
+ refusal: null
6408
+ };
6190
6409
  }
6191
- function CheckIcon(props) {
6192
- return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("path", { d: "M20 6 9 17l-5-5" }) });
6410
+ function refusalOf(response, operation) {
6411
+ return refusalFromResponse(response, operation, "Nothing was sent. Try again.");
6193
6412
  }
6194
- function CloseIcon(props) {
6195
- return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("path", { d: "M18 6 6 18M6 6l12 12" }) });
6413
+ function asRecipient(row) {
6414
+ const sources = Array.isArray(row["sources"]) ? row["sources"] : [];
6415
+ return {
6416
+ key: String(row["key"] ?? ""),
6417
+ name: String(row["name"] ?? ""),
6418
+ email: typeof row["email"] === "string" ? row["email"] : null,
6419
+ userId: typeof row["user_id"] === "string" ? row["user_id"] : null,
6420
+ sources: sources.filter((s) => s === "invitee" || s === "attendee"),
6421
+ reachable: row["reachable"] !== false
6422
+ };
6196
6423
  }
6197
- function PinIcon(props) {
6198
- return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6199
- /* @__PURE__ */ jsx2("path", { d: "M12 17v5" }),
6200
- /* @__PURE__ */ jsx2("path", { d: "M9 2h6l-1 6 3.5 3.5V14H6.5v-2.5L10 8 9 2Z" })
6201
- ] });
6424
+ async function loadFollowUpDraft(api, meetingId) {
6425
+ const operation = "meet.loadFollowUpDraft";
6426
+ const response = await api.request({
6427
+ path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
6428
+ method: "GET",
6429
+ requireAuth: true,
6430
+ operation
6431
+ });
6432
+ if (!response.ok) throw await refusalOf(response, operation);
6433
+ const body = await response.json();
6434
+ const last = body["last_sent"];
6435
+ return {
6436
+ subject: String(body["subject"] ?? ""),
6437
+ body: String(body["body"] ?? ""),
6438
+ recipients: (Array.isArray(body["recipients"]) ? body["recipients"] : []).map(
6439
+ (row) => asRecipient(row)
6440
+ ),
6441
+ wrapUpWritten: body["wrap_up_written"] === true,
6442
+ lastSent: last && typeof last["at"] === "string" ? { at: last["at"], count: typeof last["count"] === "number" ? last["count"] : 0 } : null
6443
+ };
6202
6444
  }
6203
- function WifiLowIcon(props) {
6204
- return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6205
- /* @__PURE__ */ jsx2("path", { d: "M12 20h.01" }),
6206
- /* @__PURE__ */ jsx2("path", { d: "M8.5 16.5a5 5 0 0 1 7 0" })
6207
- ] });
6445
+ async function sendFollowUp(api, meetingId, input) {
6446
+ const operation = "meet.sendFollowUp";
6447
+ const response = await api.request({
6448
+ path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
6449
+ method: "POST",
6450
+ requireAuth: true,
6451
+ operation,
6452
+ body: {
6453
+ subject: input.subject,
6454
+ body: input.body,
6455
+ recipient_keys: [...input.recipientKeys],
6456
+ dry_run: input.dryRun === true
6457
+ }
6458
+ });
6459
+ if (!response.ok) throw await refusalOf(response, operation);
6460
+ const body = await response.json();
6461
+ return {
6462
+ dryRun: body["dry_run"] === true,
6463
+ queued: Array.isArray(body["queued"]) ? body["queued"] : [],
6464
+ skipped: Array.isArray(body["skipped"]) ? body["skipped"].map((row) => ({
6465
+ key: String(row["key"] ?? ""),
6466
+ reason: String(row["reason"] ?? "")
6467
+ })) : []
6468
+ };
6208
6469
  }
6209
- function SmileIcon(props) {
6210
- return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6211
- /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "10" }),
6212
- /* @__PURE__ */ jsx2("path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }),
6213
- /* @__PURE__ */ jsx2("line", { x1: "9", x2: "9.01", y1: "9", y2: "9" }),
6214
- /* @__PURE__ */ jsx2("line", { x1: "15", x2: "15.01", y1: "9", y2: "9" })
6215
- ] });
6470
+
6471
+ // src/core/format.ts
6472
+ function participantSummary(participants) {
6473
+ const names = participants.filter((p) => !p.isLocal).map((p) => p.displayName);
6474
+ if (names.length === 0) return "Just you";
6475
+ if (names.length === 1) return names[0] ?? "";
6476
+ if (names.length === 2) return `${names[0]} and ${names[1]}`;
6477
+ return `${names[0]}, ${names[1]} and ${names.length - 2} ${names.length - 2 === 1 ? "other" : "others"}`;
6216
6478
  }
6217
- function MoreIcon(props) {
6218
- return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6219
- /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "1" }),
6220
- /* @__PURE__ */ jsx2("circle", { cx: "19", cy: "12", r: "1" }),
6221
- /* @__PURE__ */ jsx2("circle", { cx: "5", cy: "12", r: "1" })
6222
- ] });
6479
+ function callStateLabel(state, outgoing) {
6480
+ switch (state) {
6481
+ case "dialing":
6482
+ case "ringing":
6483
+ return outgoing ? "Calling\u2026" : "Incoming call";
6484
+ case "connecting":
6485
+ return "Connecting\u2026";
6486
+ case "active":
6487
+ return "In call";
6488
+ case "declined":
6489
+ return outgoing ? "Declined" : "You declined";
6490
+ case "missed":
6491
+ return outgoing ? "No answer" : "Missed call";
6492
+ case "busy":
6493
+ return outgoing ? "On another call" : "You were busy";
6494
+ case "cancelled":
6495
+ return outgoing ? "Cancelled" : "Cancelled by caller";
6496
+ case "ended":
6497
+ return "Call ended";
6498
+ case "failed":
6499
+ return "Call failed";
6500
+ default:
6501
+ return "Call";
6502
+ }
6223
6503
  }
6224
- function SearchIcon(props) {
6225
- return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6226
- /* @__PURE__ */ jsx2("circle", { cx: "11", cy: "11", r: "8" }),
6227
- /* @__PURE__ */ jsx2("path", { d: "m21 21-4.3-4.3" })
6228
- ] });
6504
+ function countLabel(count2, singular, plural = `${singular}s`) {
6505
+ return `${count2} ${count2 === 1 ? singular : plural}`;
6506
+ }
6507
+
6508
+ // src/react/icons.tsx
6509
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
6510
+ function Icon({ children, ...props }) {
6511
+ return /* @__PURE__ */ jsx2(
6512
+ "svg",
6513
+ {
6514
+ width: "20",
6515
+ height: "20",
6516
+ viewBox: "0 0 24 24",
6517
+ fill: "none",
6518
+ stroke: "currentColor",
6519
+ strokeWidth: "1.75",
6520
+ strokeLinecap: "round",
6521
+ strokeLinejoin: "round",
6522
+ "aria-hidden": "true",
6523
+ focusable: "false",
6524
+ ...props,
6525
+ children
6526
+ }
6527
+ );
6528
+ }
6529
+ function MicIcon(props) {
6530
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6531
+ /* @__PURE__ */ jsx2("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }),
6532
+ /* @__PURE__ */ jsx2("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
6533
+ /* @__PURE__ */ jsx2("path", { d: "M12 19v3" })
6534
+ ] });
6535
+ }
6536
+ function MicOffIcon(props) {
6537
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6538
+ /* @__PURE__ */ jsx2("path", { d: "M2 2l20 20" }),
6539
+ /* @__PURE__ */ jsx2("path", { d: "M9 9v3a3 3 0 0 0 5.12 2.12" }),
6540
+ /* @__PURE__ */ jsx2("path", { d: "M15 9.34V5a3 3 0 0 0-5.94-.6" }),
6541
+ /* @__PURE__ */ jsx2("path", { d: "M19 10v2a7 7 0 0 1-.11 1.23M5 10v2a7 7 0 0 0 11 5.66" }),
6542
+ /* @__PURE__ */ jsx2("path", { d: "M12 19v3" })
6543
+ ] });
6544
+ }
6545
+ function CameraIcon(props) {
6546
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6547
+ /* @__PURE__ */ jsx2("path", { d: "m23 7-7 5 7 5V7Z" }),
6548
+ /* @__PURE__ */ jsx2("rect", { x: "1", y: "5", width: "15", height: "14", rx: "2" })
6549
+ ] });
6550
+ }
6551
+ function CameraOffIcon(props) {
6552
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6553
+ /* @__PURE__ */ jsx2("path", { d: "M2 2l20 20" }),
6554
+ /* @__PURE__ */ jsx2("path", { d: "M10.66 5H14a2 2 0 0 1 2 2v3.34l1 1L23 7v10" }),
6555
+ /* @__PURE__ */ jsx2("path", { d: "M16 16a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2" })
6556
+ ] });
6557
+ }
6558
+ function ScreenShareIcon(props) {
6559
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6560
+ /* @__PURE__ */ jsx2("rect", { x: "2", y: "3", width: "20", height: "14", rx: "2" }),
6561
+ /* @__PURE__ */ jsx2("path", { d: "M8 21h8M12 17v4" }),
6562
+ /* @__PURE__ */ jsx2("path", { d: "M12 13V8m0 0-2.5 2.5M12 8l2.5 2.5" })
6563
+ ] });
6564
+ }
6565
+ function PhoneIcon(props) {
6566
+ return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("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" }) });
6567
+ }
6568
+ function PhoneOffIcon(props) {
6569
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6570
+ /* @__PURE__ */ jsx2("path", { d: "M2 2l20 20" }),
6571
+ /* @__PURE__ */ jsx2("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" }),
6572
+ /* @__PURE__ */ jsx2("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" })
6573
+ ] });
6574
+ }
6575
+ function HandIcon(props) {
6576
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6577
+ /* @__PURE__ */ jsx2("path", { d: "M18 11V6a2 2 0 0 0-4 0v5" }),
6578
+ /* @__PURE__ */ jsx2("path", { d: "M14 10V4a2 2 0 0 0-4 0v7" }),
6579
+ /* @__PURE__ */ jsx2("path", { d: "M10 10.5V6a2 2 0 0 0-4 0v8" }),
6580
+ /* @__PURE__ */ jsx2("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" })
6581
+ ] });
6582
+ }
6583
+ function ChatIcon(props) {
6584
+ return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z" }) });
6585
+ }
6586
+ function PeopleIcon(props) {
6587
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6588
+ /* @__PURE__ */ jsx2("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }),
6589
+ /* @__PURE__ */ jsx2("circle", { cx: "9", cy: "7", r: "4" }),
6590
+ /* @__PURE__ */ jsx2("path", { d: "M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" })
6591
+ ] });
6592
+ }
6593
+ function RecordIcon(props) {
6594
+ return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "8", fill: "currentColor", stroke: "none" }) });
6595
+ }
6596
+ function SparklesIcon(props) {
6597
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6598
+ /* @__PURE__ */ jsx2("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" }),
6599
+ /* @__PURE__ */ jsx2("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" })
6600
+ ] });
6601
+ }
6602
+ function LockIcon(props) {
6603
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6604
+ /* @__PURE__ */ jsx2("rect", { x: "4", y: "10", width: "16", height: "11", rx: "2" }),
6605
+ /* @__PURE__ */ jsx2("path", { d: "M8 10V7a4 4 0 0 1 8 0v3" })
6606
+ ] });
6607
+ }
6608
+ function LayoutIcon(props) {
6609
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6610
+ /* @__PURE__ */ jsx2("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
6611
+ /* @__PURE__ */ jsx2("path", { d: "M3 9h18M9 21V9" })
6612
+ ] });
6613
+ }
6614
+ function SettingsIcon(props) {
6615
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6616
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "3" }),
6617
+ /* @__PURE__ */ jsx2("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" })
6618
+ ] });
6619
+ }
6620
+ function CheckIcon(props) {
6621
+ return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("path", { d: "M20 6 9 17l-5-5" }) });
6622
+ }
6623
+ function CloseIcon(props) {
6624
+ return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("path", { d: "M18 6 6 18M6 6l12 12" }) });
6625
+ }
6626
+ function PinIcon(props) {
6627
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6628
+ /* @__PURE__ */ jsx2("path", { d: "M12 17v5" }),
6629
+ /* @__PURE__ */ jsx2("path", { d: "M9 2h6l-1 6 3.5 3.5V14H6.5v-2.5L10 8 9 2Z" })
6630
+ ] });
6631
+ }
6632
+ function WifiLowIcon(props) {
6633
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6634
+ /* @__PURE__ */ jsx2("path", { d: "M12 20h.01" }),
6635
+ /* @__PURE__ */ jsx2("path", { d: "M8.5 16.5a5 5 0 0 1 7 0" })
6636
+ ] });
6637
+ }
6638
+ function SmileIcon(props) {
6639
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6640
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "10" }),
6641
+ /* @__PURE__ */ jsx2("path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }),
6642
+ /* @__PURE__ */ jsx2("line", { x1: "9", x2: "9.01", y1: "9", y2: "9" }),
6643
+ /* @__PURE__ */ jsx2("line", { x1: "15", x2: "15.01", y1: "9", y2: "9" })
6644
+ ] });
6645
+ }
6646
+ function MoreIcon(props) {
6647
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6648
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "1" }),
6649
+ /* @__PURE__ */ jsx2("circle", { cx: "19", cy: "12", r: "1" }),
6650
+ /* @__PURE__ */ jsx2("circle", { cx: "5", cy: "12", r: "1" })
6651
+ ] });
6652
+ }
6653
+ function SearchIcon(props) {
6654
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6655
+ /* @__PURE__ */ jsx2("circle", { cx: "11", cy: "11", r: "8" }),
6656
+ /* @__PURE__ */ jsx2("path", { d: "m21 21-4.3-4.3" })
6657
+ ] });
6229
6658
  }
6230
6659
  function KeyboardIcon(props) {
6231
6660
  return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
@@ -6487,6 +6916,15 @@ function ControlBar({ onLeave }) {
6487
6916
  const micUnavailable = useLocalMedia().microphone.unavailable;
6488
6917
  const unread = useUnreadChat(panel === "chat");
6489
6918
  const toggle = (next) => setPanel(panel === next ? "none" : next);
6919
+ const privateChatAt = snapshot?.privateChatRequest?.at ?? null;
6920
+ const meetStore = useMeetHost()?.store ?? null;
6921
+ useEffect3(() => {
6922
+ const request = meetStore?.snapshot().privateChatRequest ?? null;
6923
+ if (request === null) return;
6924
+ setChatTo(request.to);
6925
+ setPanel("chat");
6926
+ meetStore?.patch({ privateChatRequest: null });
6927
+ }, [privateChatAt, meetStore]);
6490
6928
  useMeetShortcuts({
6491
6929
  mic: () => void room.toggleMicrophone(),
6492
6930
  camera: () => void room.toggleCamera(),
@@ -6726,7 +7164,12 @@ function Popover({
6726
7164
  placement = "up"
6727
7165
  }) {
6728
7166
  const ref = useRef3(null);
6729
- const [style, setStyle] = useState3({ visibility: "hidden" });
7167
+ const [style, setStyle] = useState3({
7168
+ position: "fixed",
7169
+ top: 0,
7170
+ left: 0,
7171
+ visibility: "hidden"
7172
+ });
6730
7173
  useLayoutEffect(() => {
6731
7174
  const element = ref.current;
6732
7175
  const anchor = element?.parentElement ?? null;
@@ -6742,14 +7185,35 @@ function Popover({
6742
7185
  );
6743
7186
  const fitsBelow = rect.bottom + gutter + height <= window.innerHeight;
6744
7187
  const fitsAbove = rect.top - gutter - height >= 0;
7188
+ if (!fitsBelow && !fitsAbove) {
7189
+ const top = Math.max(gutter, window.innerHeight - height - gutter);
7190
+ setStyle({
7191
+ position: "fixed",
7192
+ left,
7193
+ top,
7194
+ maxHeight: window.innerHeight - top - gutter,
7195
+ overflowY: "auto"
7196
+ });
7197
+ return;
7198
+ }
6745
7199
  const down = placement === "down" ? fitsBelow || !fitsAbove : !fitsAbove && fitsBelow;
6746
7200
  setStyle(
6747
7201
  down ? { position: "fixed", left, top: rect.bottom + gutter } : { position: "fixed", left, bottom: window.innerHeight - rect.top + gutter }
6748
7202
  );
6749
7203
  };
6750
7204
  place();
7205
+ const frame = requestAnimationFrame(place);
7206
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => place());
7207
+ observer?.observe(document.body);
7208
+ observer?.observe(element);
6751
7209
  window.addEventListener("resize", place);
6752
- return () => window.removeEventListener("resize", place);
7210
+ window.addEventListener("scroll", place, true);
7211
+ return () => {
7212
+ cancelAnimationFrame(frame);
7213
+ observer?.disconnect();
7214
+ window.removeEventListener("resize", place);
7215
+ window.removeEventListener("scroll", place, true);
7216
+ };
6753
7217
  }, [placement]);
6754
7218
  useEffect3(() => {
6755
7219
  const onKey = (event) => {
@@ -7126,11 +7590,15 @@ function PersonMenu({
7126
7590
  const pinned = snapshot?.pinned === participant.identity;
7127
7591
  const spotlit = snapshot?.spotlight === participant.identity;
7128
7592
  const hasAccount = participant.userId !== null;
7593
+ const [refused, setRefused] = useState3(false);
7129
7594
  const act = (request) => {
7595
+ setRefused(false);
7130
7596
  void controls.run(request).then((result) => {
7131
7597
  if (result !== null) onClose();
7598
+ else setRefused(true);
7132
7599
  });
7133
7600
  };
7601
+ const refusal2 = refused && controls.notice?.failed === true ? /* @__PURE__ */ jsx3(ErrorBox, { as: "p", className: "mx-meet__error", message: controls.notice.text, operation: "Change this participant", origin: "meet/components", children: controls.notice.text }) : null;
7134
7602
  if (mode === "rename" || mode === "report") {
7135
7603
  const renaming = mode === "rename";
7136
7604
  return /* @__PURE__ */ jsx3(Popover, { label: renaming ? "Rename" : "Report", onClose, placement, children: /* @__PURE__ */ jsxs2(
@@ -7168,7 +7636,7 @@ function PersonMenu({
7168
7636
  }
7169
7637
  )
7170
7638
  ] }),
7171
- controls.notice?.failed === true ? /* @__PURE__ */ jsx3("p", { className: "mx-meet__error", children: controls.notice.text }) : null,
7639
+ refusal2,
7172
7640
  /* @__PURE__ */ jsxs2("div", { className: "mx-meet__menu-actions", children: [
7173
7641
  /* @__PURE__ */ jsx3("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Back" }),
7174
7642
  /* @__PURE__ */ jsx3("button", { type: "submit", className: "mx-meet__send", disabled: text3.trim().length === 0 || controls.busy, children: renaming ? "Rename" : "Report" })
@@ -7181,6 +7649,7 @@ function PersonMenu({
7181
7649
  const removing = mode === "remove";
7182
7650
  return /* @__PURE__ */ jsx3(Popover, { label: removing ? "Remove" : "Make host", onClose, placement, children: /* @__PURE__ */ jsxs2("div", { className: "mx-meet__menu-form", children: [
7183
7651
  /* @__PURE__ */ jsx3("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.` }),
7652
+ refusal2,
7184
7653
  /* @__PURE__ */ jsxs2("div", { className: "mx-meet__menu-actions", children: [
7185
7654
  /* @__PURE__ */ jsx3("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Cancel" }),
7186
7655
  /* @__PURE__ */ jsx3(
@@ -7211,11 +7680,15 @@ function PersonMenu({
7211
7680
  ]
7212
7681
  }
7213
7682
  ),
7214
- !self && !participant.isAgent && onMessage !== void 0 ? /* @__PURE__ */ jsxs2(
7683
+ !self && !participant.isAgent && (onMessage !== void 0 || host !== null) ? /* @__PURE__ */ jsxs2(
7215
7684
  MenuItem,
7216
7685
  {
7217
7686
  onSelect: () => {
7218
- onMessage(participant.identity);
7687
+ if (onMessage !== void 0) onMessage(participant.identity);
7688
+ else
7689
+ host?.store.patch({
7690
+ privateChatRequest: { to: participant.identity, at: Date.now() }
7691
+ });
7219
7692
  onClose();
7220
7693
  },
7221
7694
  children: [
@@ -7842,7 +8315,7 @@ function PreJoinBody({
7842
8315
  microphone,
7843
8316
  ...meetingId !== void 0 ? { meetingId } : {},
7844
8317
  ...slug !== void 0 ? { slug } : {}
7845
- }).then(() => onJoined?.()).finally(() => setJoining(false));
8318
+ }).then(() => onJoined?.()).catch(() => void 0).finally(() => setJoining(false));
7846
8319
  },
7847
8320
  children: joining ? "Joining\u2026" : "Join now"
7848
8321
  }
@@ -7978,7 +8451,7 @@ function MeetingRoomBody({
7978
8451
  /* @__PURE__ */ jsx3("h1", { className: "mx-meet__title", children: snapshot?.meeting?.title ?? "Meeting" }),
7979
8452
  /* @__PURE__ */ jsxs2("span", { className: "mx-meet__meta", children: [
7980
8453
  participantSummary(snapshot?.participants ?? []),
7981
- elapsed > 0 ? ` \xB7 ${formatDurationMs2(elapsed)}` : ""
8454
+ elapsed > 0 ? ` \xB7 ${formatDurationMs3(elapsed)}` : ""
7982
8455
  ] }),
7983
8456
  /* @__PURE__ */ jsx3(RecordingIndicator, {}),
7984
8457
  snapshot?.locked === true ? /* @__PURE__ */ jsxs2("span", { className: "mx-meet__badge", children: [
@@ -8284,7 +8757,10 @@ function MeetingRecordBody({
8284
8757
  const blocks = groupTranscript(bundle.transcript, bundle.names);
8285
8758
  const notice = recordingNotice(bundle.recording);
8286
8759
  const fileId = playableRecordingFileId(bundle.recording);
8287
- const attended = bundle.attendees.filter((person) => person.admissionState !== "denied");
8760
+ const attended = uniqueAttendees(
8761
+ bundle.attendees.filter((person) => person.admissionState !== "denied"),
8762
+ bundle.names
8763
+ );
8288
8764
  const restrictionNotices = summarizeMeetingRecordRestrictions(bundle.restrictions);
8289
8765
  const guestFinishedMeeting = host.identity.userId === null && meeting.endedAt !== null;
8290
8766
  const everyRecordPartRefused = ["attendance", "notes", "transcript", "recording"].every(
@@ -8593,514 +9069,172 @@ function RecordingPlayerBody({ fileId, host }) {
8593
9069
  const failure = thrown;
8594
9070
  setError({
8595
9071
  message: failure.message ?? "The recording could not be loaded.",
8596
- remedy: failure.remedy ?? PLAYBACK_RETRY_REMEDY
8597
- });
8598
- } finally {
8599
- setLoading(false);
8600
- }
8601
- })();
8602
- },
8603
- children: loading ? "Loading the recording\u2026" : "Play the recording"
8604
- }
8605
- )
8606
- ] });
8607
- }
8608
- function clockOf(iso) {
8609
- const at = new Date(iso);
8610
- if (Number.isNaN(at.getTime())) return "";
8611
- return at.toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" });
8612
- }
8613
- function MeetingList({ baseUrl }) {
8614
- const host = useMeetHost();
8615
- const list = useMeetings();
8616
- if (host === null) return /* @__PURE__ */ jsx3(InertNotice, { what: "Your meetings" });
8617
- if (list.failure !== null) {
8618
- return /* @__PURE__ */ jsxs2(ErrorBox, { as: "div", className: "mx-meet__empty", error: list.failure, operation: "List your meetings", origin: "meet/components", children: [
8619
- /* @__PURE__ */ jsx3("p", { className: "mx-meet__empty-title", children: "Your meetings could not be listed" }),
8620
- /* @__PURE__ */ jsx3("p", { className: "mx-meet__empty-body", children: list.failure.message }),
8621
- /* @__PURE__ */ jsx3("p", { className: "mx-meet__empty-body", children: list.failure.remedy })
8622
- ] });
8623
- }
8624
- if (list.loading) {
8625
- return /* @__PURE__ */ jsx3("div", { className: "mx-meet__empty", role: "status", children: /* @__PURE__ */ jsx3("p", { className: "mx-meet__empty-title", children: "Loading your meetings\u2026" }) });
8626
- }
8627
- if (list.meetings.length === 0) {
8628
- return /* @__PURE__ */ jsxs2("div", { className: "mx-meet__empty", role: "status", children: [
8629
- /* @__PURE__ */ jsx3("p", { className: "mx-meet__empty-title", children: "No meetings yet" }),
8630
- /* @__PURE__ */ jsx3("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." })
8631
- ] });
8632
- }
8633
- const origin = baseUrl ?? (typeof window === "undefined" ? "" : window.location.origin);
8634
- return /* @__PURE__ */ jsx3("ul", { className: "mx-meet-list", children: list.meetings.map((meeting) => {
8635
- const ended = meeting.endedAt !== null;
8636
- return /* @__PURE__ */ jsxs2("li", { className: "mx-meet-list__row", children: [
8637
- /* @__PURE__ */ jsxs2("div", { className: "mx-meet-list__main", children: [
8638
- /* @__PURE__ */ jsxs2("span", { className: "mx-meet-list__title", children: [
8639
- meeting.title,
8640
- ended ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__badge", children: "Ended" }) : null
8641
- ] }),
8642
- /* @__PURE__ */ jsx3("span", { className: "mx-meet-list__when", children: meetingWhen(meeting) })
8643
- ] }),
8644
- /* @__PURE__ */ jsx3("a", { className: "mx-meet-list__action", href: meetingLink(origin, meeting.slug), children: ended ? "Open the record" : "Join" })
8645
- ] }, meeting.id);
8646
- }) });
8647
- }
8648
-
8649
- // src/core/invitations.ts
8650
- var CALL_INVITE_KIND = "meet.call-invite";
8651
- var CALL_INVITE_VERSION = 1;
8652
- function callInviteAction(invite) {
8653
- return {
8654
- kind: CALL_INVITE_KIND,
8655
- version: CALL_INVITE_VERSION,
8656
- payload: {
8657
- invite_id: invite.id,
8658
- room_name: invite.roomName,
8659
- mode: invite.mode,
8660
- caller_name: invite.callerName,
8661
- expires_at: invite.expiresAt
8662
- }
8663
- };
8664
- }
8665
- function callInviteText(invite) {
8666
- return invite.mode === "video" ? `${invite.callerName} started a video call` : `${invite.callerName} started an audio call`;
8667
- }
8668
- function createCallInviteHandler(options) {
8669
- const now = options.now ?? (() => Date.now());
8670
- return {
8671
- kind: CALL_INVITE_KIND,
8672
- versions: [CALL_INVITE_VERSION],
8673
- choices: (payload) => {
8674
- if (Date.parse(payload.expires_at) < now()) return [];
8675
- return [
8676
- { id: "join", label: payload.mode === "video" ? "Join video" : "Join call", tone: "primary" },
8677
- { id: "decline", label: "Decline", tone: "neutral" }
8678
- ];
8679
- },
8680
- summarize: (payload) => payload.mode === "video" ? `${payload.caller_name} started a video call` : `${payload.caller_name} started an audio call`,
8681
- execute: async (payload, context) => {
8682
- const settledAt = new Date(now()).toISOString();
8683
- const inviteId = asCallId(payload.invite_id);
8684
- const pending = await options.repository.pendingInvites(asUserId(String(context.actorId)));
8685
- const live = pending.find((invite) => invite.id === inviteId) ?? null;
8686
- if (live === null) {
8687
- return {
8688
- kind: CALL_INVITE_KIND,
8689
- messageId: context.messageId,
8690
- actorId: context.actorId,
8691
- outcome: "unavailable",
8692
- label: "This call has ended",
8693
- settledAt,
8694
- 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."
8695
- };
8696
- }
8697
- if (context.choice === "decline") {
8698
- await options.calls.decline(inviteId);
8699
- return {
8700
- kind: CALL_INVITE_KIND,
8701
- messageId: context.messageId,
8702
- actorId: context.actorId,
8703
- outcome: "declined",
8704
- label: "Declined",
8705
- settledAt
8706
- };
8707
- }
8708
- const accepted = await options.calls.accept(inviteId, live.mode);
8709
- await options.onJoin(accepted);
8710
- return {
8711
- kind: CALL_INVITE_KIND,
8712
- messageId: context.messageId,
8713
- actorId: context.actorId,
8714
- outcome: "applied",
8715
- label: "Joined",
8716
- settledAt
8717
- };
8718
- }
8719
- };
8720
- }
8721
- var MEETING_INVITE_KIND = "meet.meeting-invite";
8722
- var MEETING_INVITE_VERSION = 1;
8723
- function meetingInviteAction(payload) {
8724
- return { kind: MEETING_INVITE_KIND, version: MEETING_INVITE_VERSION, payload };
8725
- }
8726
- function createMeetingInviteHandler(options) {
8727
- const now = options.now ?? (() => Date.now());
8728
- return {
8729
- kind: MEETING_INVITE_KIND,
8730
- versions: [MEETING_INVITE_VERSION],
8731
- // A DURABLE LINK NEVER EXPIRES (R3) — the chip stays useful after the
8732
- // meeting too, because that is when people go looking for the notes.
8733
- choices: () => [{ id: "open", label: "Open meeting", tone: "primary" }],
8734
- summarize: (payload) => payload.scheduled_for === null ? payload.title : `${payload.title} \u2014 ${payload.scheduled_for}`,
8735
- execute: async (payload, context) => {
8736
- if (payload.slug.length === 0) {
8737
- throw new MeetError(
8738
- "invalid-response",
8739
- "meet.meeting-invite: the payload carries no slug",
8740
- "A meeting invitation without a durable slug cannot be opened. Fix the sender; never synthesize a link on the receiving side."
8741
- );
8742
- }
8743
- await options.onOpen(payload);
8744
- return {
8745
- kind: MEETING_INVITE_KIND,
8746
- messageId: context.messageId,
8747
- actorId: context.actorId,
8748
- outcome: "applied",
8749
- label: "Opened",
8750
- settledAt: new Date(now()).toISOString()
8751
- };
8752
- }
8753
- };
8754
- }
8755
-
8756
- // src/core/after.ts
8757
- import { formatDurationMs as formatDurationMs3 } from "@ai-matrx/kit/format";
8758
- function kindOf(person) {
8759
- if (person.isAgent) return "assistant";
8760
- if (person.role === "host") return "host";
8761
- if (person.role === "cohost") return "cohost";
8762
- if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
8763
- return "member";
8764
- }
8765
- function ms(iso) {
8766
- if (iso === null) return null;
8767
- const at = new Date(iso).getTime();
8768
- return Number.isNaN(at) ? null : at;
8769
- }
8770
- function attendanceReport(meeting, attendees, names = {}) {
8771
- const end = ms(meeting.endedAt);
8772
- const rows = attendees.map((person) => {
8773
- const joined = ms(person.joinedAt);
8774
- const left = ms(person.leftAt);
8775
- const until = left ?? end;
8776
- const duration = joined !== null && until !== null && until >= joined ? until - joined : null;
8777
- return {
8778
- id: person.id,
8779
- identity: person.identity,
8780
- name: displayNameFor(names, person.identity, person.displayName),
8781
- userId: person.userId,
8782
- kind: kindOf(person),
8783
- joinedAt: person.joinedAt,
8784
- leftAt: person.leftAt,
8785
- durationMs: duration,
8786
- durationToEnd: duration !== null && left === null,
8787
- admission: person.admissionState
8788
- };
8789
- });
8790
- const rank = {
8791
- host: 0,
8792
- cohost: 1,
8793
- member: 2,
8794
- "signed-in guest": 3,
8795
- guest: 4,
8796
- assistant: 5
8797
- };
8798
- return rows.sort((a, b) => {
8799
- if (rank[a.kind] !== rank[b.kind]) return rank[a.kind] - rank[b.kind];
8800
- return (ms(a.joinedAt) ?? Number.MAX_SAFE_INTEGER) - (ms(b.joinedAt) ?? Number.MAX_SAFE_INTEGER);
8801
- });
8802
- }
8803
- function attendanceTotals(rows) {
8804
- const humans = rows.filter((row) => row.kind !== "assistant");
8805
- const attended = humans.filter((row) => row.joinedAt !== null);
8806
- const timed = attended.filter((row) => row.durationMs !== null);
8807
- return {
8808
- people: attended.length,
8809
- guests: attended.filter((row) => row.kind === "guest" || row.kind === "signed-in guest").length,
8810
- neverAdmitted: humans.filter((row) => row.joinedAt === null).length,
8811
- averageMs: timed.length === 0 ? null : Math.round(timed.reduce((sum, row) => sum + (row.durationMs ?? 0), 0) / timed.length)
8812
- };
8813
- }
8814
- function csvCell(value) {
8815
- if (value === null) return "";
8816
- const text3 = String(value);
8817
- return /[",\n\r]/.test(text3) ? `"${text3.replace(/"/g, '""')}"` : text3;
8818
- }
8819
- var ATTENDANCE_CSV_HEADER = [
8820
- "Meeting",
8821
- "Meeting started",
8822
- "Name",
8823
- "Attended as",
8824
- "Joined",
8825
- "Left",
8826
- "Minutes in the meeting",
8827
- "Admission"
8828
- ];
8829
- function attendanceCsv(reports) {
8830
- const lines = [ATTENDANCE_CSV_HEADER.join(",")];
8831
- for (const { meeting, rows } of reports) {
8832
- for (const row of rows) {
8833
- lines.push(
8834
- [
8835
- meeting.title,
8836
- meeting.startedAt ?? meeting.scheduledFor,
8837
- row.name,
8838
- row.kind,
8839
- row.joinedAt,
8840
- row.leftAt,
8841
- row.durationMs === null ? null : Math.round(row.durationMs / 6e3) / 10,
8842
- row.admission
8843
- ].map(csvCell).join(",")
8844
- );
8845
- }
8846
- }
8847
- return `${lines.join("\r\n")}\r
8848
- `;
8849
- }
8850
- function recordingOffsetMs(at, recording) {
8851
- if (recording === null || recording.state !== "available") return null;
8852
- const start = ms(recording.startedAt);
8853
- const moment = ms(at);
8854
- if (start === null || moment === null || moment < start) return null;
8855
- const end = ms(recording.endedAt);
8856
- if (end !== null && moment > end) return null;
8857
- return moment - start;
8858
- }
8859
- function clockPosition(offsetMs) {
8860
- const total = Math.max(0, Math.floor(offsetMs / 1e3));
8861
- const h = Math.floor(total / 3600);
8862
- const m = Math.floor(total % 3600 / 60);
8863
- const s = total % 60;
8864
- const ss = String(s).padStart(2, "0");
8865
- return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
8866
- }
8867
- function searchTranscript(segments, query, names = {}) {
8868
- const needle = fold(query.trim());
8869
- if (needle.length === 0) return [];
8870
- const out = [];
8871
- for (const segment of segments) {
8872
- const hay = fold(segment.text);
8873
- const ranges = [];
8874
- let from = 0;
8875
- for (; ; ) {
8876
- const at = hay.indexOf(needle, from);
8877
- if (at < 0) break;
8878
- ranges.push([at, at + needle.length]);
8879
- from = at + needle.length;
8880
- }
8881
- const speaker = fold(displayNameFor(names, segment.identity, segment.speaker));
8882
- if (ranges.length > 0 || speaker.includes(needle)) {
8883
- out.push({ segmentId: segment.id, ranges });
8884
- }
8885
- }
8886
- return out;
8887
- }
8888
- function fold(value) {
8889
- return value.normalize("NFD").replace(/[̀-ͯ]/g, (mark) => mark.length === 1 ? "" : mark).toLowerCase();
8890
- }
8891
- function clockOfDay(iso, options) {
8892
- const at = new Date(iso);
8893
- if (Number.isNaN(at.getTime())) return "";
8894
- return at.toLocaleTimeString(options.locale, {
8895
- hour: "numeric",
8896
- minute: "2-digit",
8897
- second: "2-digit",
8898
- ...options.timeZone !== void 0 ? { timeZone: options.timeZone } : {}
8899
- });
8900
- }
8901
- function vttStamp(offsetMs) {
8902
- const total = Math.max(0, Math.round(offsetMs));
8903
- const h = Math.floor(total / 36e5);
8904
- const m = Math.floor(total % 36e5 / 6e4);
8905
- const s = Math.floor(total % 6e4 / 1e3);
8906
- const milli = total % 1e3;
8907
- return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${String(milli).padStart(3, "0")}`;
8908
- }
8909
- function transcriptDocument(bundle, format, options = {}) {
8910
- const names = options.names ?? bundle.names;
8911
- const lines = bundle.transcript.filter((line) => line.text.trim().length > 0);
8912
- const speaker = (line) => displayNameFor(names, line.identity, line.speaker);
8913
- if (format === "vtt") {
8914
- const zeroIso = bundle.recording?.state === "available" && bundle.recording.startedAt !== null ? bundle.recording.startedAt : bundle.meeting.startedAt ?? lines[0]?.startedAt ?? null;
8915
- const zero = ms(zeroIso) ?? 0;
8916
- const cues = ["WEBVTT", ""];
8917
- lines.forEach((line, index) => {
8918
- const start = (ms(line.startedAt) ?? zero) - zero;
8919
- if (start < 0) return;
8920
- const next = lines[index + 1];
8921
- const nextStart = next !== void 0 ? (ms(next.startedAt) ?? zero) - zero : start + 4e3;
8922
- const end = Math.max(start + 500, Math.min(nextStart, start + 1e4));
8923
- cues.push(`${vttStamp(start)} --> ${vttStamp(end)}`);
8924
- cues.push(`<v ${speaker(line).replace(/[<>]/g, "")}>${line.text.replace(/-->/g, "->")}`);
8925
- cues.push("");
8926
- });
8927
- return cues.join("\n");
8928
- }
8929
- const out = [];
8930
- const title = bundle.meeting.title;
8931
- out.push(format === "md" ? `# ${title} \u2014 transcript` : `${title} \u2014 transcript`);
8932
- out.push("");
8933
- let last = null;
8934
- for (const line of lines) {
8935
- if (line.identity !== last) {
8936
- if (last !== null) out.push("");
8937
- const who = speaker(line);
8938
- const clock = clockOfDay(line.startedAt, options);
8939
- out.push(format === "md" ? `**${who}** \xB7 ${clock}` : `${who} (${clock})`);
8940
- last = line.identity;
8941
- }
8942
- out.push(line.text.trim());
8943
- }
8944
- if (lines.length === 0) out.push("No transcript was captured for this meeting.");
8945
- return `${out.join("\n")}
8946
- `;
8947
- }
8948
- function bullet(note) {
8949
- const owner = note.assigneeDisplayName !== null ? ` \u2014 ${note.assigneeDisplayName}` : "";
8950
- return `- ${note.text.trim()}${owner}`;
8951
- }
8952
- function notesDocument(bundle, options = {}) {
8953
- const out = [`# ${bundle.meeting.title}`, ""];
8954
- if (options.when) out.push(`_${options.when}_`, "");
8955
- const people = bundle.attendees.filter((person) => !person.isAgent && person.joinedAt !== null).map((person) => displayNameFor(bundle.names, person.identity, person.displayName));
8956
- if (people.length > 0) out.push(`**Attended:** ${people.join(", ")}`, "");
8957
- out.push("## Summary", "", bundle.summary?.text.trim() || "No summary was written.", "");
8958
- out.push("## Decisions", "");
8959
- out.push(...bundle.decisions.length > 0 ? bundle.decisions.map(bullet) : ["None recorded."], "");
8960
- out.push("## Action items", "");
8961
- out.push(
8962
- ...bundle.actionItems.length > 0 ? bundle.actionItems.map(bullet) : ["None recorded."],
8963
- ""
8964
- );
8965
- if (options.link) out.push(`[Open the meeting record](${options.link})`, "");
8966
- return out.join("\n");
9072
+ remedy: failure.remedy ?? PLAYBACK_RETRY_REMEDY
9073
+ });
9074
+ } finally {
9075
+ setLoading(false);
9076
+ }
9077
+ })();
9078
+ },
9079
+ children: loading ? "Loading the recording\u2026" : "Play the recording"
9080
+ }
9081
+ )
9082
+ ] });
8967
9083
  }
8968
- function meetingLength(meeting) {
8969
- const start = ms(meeting.startedAt);
8970
- const end = ms(meeting.endedAt);
8971
- if (start === null || end === null || end < start) return null;
8972
- return formatDurationMs3(end - start, { style: "long" });
9084
+ function clockOf(iso) {
9085
+ const at = new Date(iso);
9086
+ if (Number.isNaN(at.getTime())) return "";
9087
+ return at.toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" });
8973
9088
  }
8974
- async function loadMeetingChat(api, meetingId, roomToken = null) {
8975
- const operation = "meet.loadMeetingChat";
8976
- let response;
8977
- try {
8978
- response = await api.request({
8979
- path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(meetingId)}`,
8980
- method: "GET",
8981
- operation,
8982
- requireAuth: roomToken === null,
8983
- ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
8984
- });
8985
- } catch (error) {
8986
- return {
8987
- messages: [],
8988
- refusal: {
8989
- message: `The chat could not be read (${error.message}).`,
8990
- remedy: error.remedy ?? "Reload the page."
8991
- }
8992
- };
9089
+ function MeetingList({ baseUrl }) {
9090
+ const host = useMeetHost();
9091
+ const list = useMeetings();
9092
+ if (host === null) return /* @__PURE__ */ jsx3(InertNotice, { what: "Your meetings" });
9093
+ if (list.failure !== null) {
9094
+ return /* @__PURE__ */ jsxs2(ErrorBox, { as: "div", className: "mx-meet__empty", error: list.failure, operation: "List your meetings", origin: "meet/components", children: [
9095
+ /* @__PURE__ */ jsx3("p", { className: "mx-meet__empty-title", children: "Your meetings could not be listed" }),
9096
+ /* @__PURE__ */ jsx3("p", { className: "mx-meet__empty-body", children: list.failure.message }),
9097
+ /* @__PURE__ */ jsx3("p", { className: "mx-meet__empty-body", children: list.failure.remedy })
9098
+ ] });
8993
9099
  }
8994
- if (!response.ok) {
8995
- let body = null;
8996
- try {
8997
- body = await response.json();
8998
- } catch {
8999
- body = null;
9000
- }
9001
- const said = readServerRefusal(body);
9002
- return {
9003
- messages: [],
9004
- refusal: {
9005
- message: said?.reason ?? `The chat could not be read (the server answered ${response.status}).`,
9006
- remedy: said?.remedy || "Reload the page."
9007
- }
9008
- };
9100
+ if (list.loading) {
9101
+ return /* @__PURE__ */ jsx3("div", { className: "mx-meet__empty", role: "status", children: /* @__PURE__ */ jsx3("p", { className: "mx-meet__empty-title", children: "Loading your meetings\u2026" }) });
9009
9102
  }
9010
- const payload = await response.json();
9011
- const rows = Array.isArray(payload.messages) ? payload.messages : [];
9012
- return {
9013
- messages: rows.map(
9014
- (row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
9015
- ).filter((message) => message !== null),
9016
- refusal: null
9017
- };
9018
- }
9019
- async function refusalOf(response, operation) {
9020
- let body = null;
9021
- try {
9022
- body = await response.json();
9023
- } catch {
9024
- body = null;
9103
+ if (list.meetings.length === 0) {
9104
+ return /* @__PURE__ */ jsxs2("div", { className: "mx-meet__empty", role: "status", children: [
9105
+ /* @__PURE__ */ jsx3("p", { className: "mx-meet__empty-title", children: "No meetings yet" }),
9106
+ /* @__PURE__ */ jsx3("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." })
9107
+ ] });
9025
9108
  }
9026
- const said = readServerRefusal(body);
9027
- return new MeetError(
9028
- response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
9029
- `${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
9030
- said?.remedy || "Nothing was sent. Try again."
9031
- );
9109
+ const origin = baseUrl ?? (typeof window === "undefined" ? "" : window.location.origin);
9110
+ return /* @__PURE__ */ jsx3("ul", { className: "mx-meet-list", children: list.meetings.map((meeting) => {
9111
+ const ended = meeting.endedAt !== null;
9112
+ return /* @__PURE__ */ jsxs2("li", { className: "mx-meet-list__row", children: [
9113
+ /* @__PURE__ */ jsxs2("div", { className: "mx-meet-list__main", children: [
9114
+ /* @__PURE__ */ jsxs2("span", { className: "mx-meet-list__title", children: [
9115
+ meeting.title,
9116
+ ended ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__badge", children: "Ended" }) : null
9117
+ ] }),
9118
+ /* @__PURE__ */ jsx3("span", { className: "mx-meet-list__when", children: meetingWhen(meeting) })
9119
+ ] }),
9120
+ /* @__PURE__ */ jsx3("a", { className: "mx-meet-list__action", href: meetingLink(origin, meeting.slug), children: ended ? "Open the record" : "Join" })
9121
+ ] }, meeting.id);
9122
+ }) });
9032
9123
  }
9033
- function asRecipient(row) {
9034
- const sources = Array.isArray(row["sources"]) ? row["sources"] : [];
9124
+
9125
+ // src/core/invitations.ts
9126
+ var CALL_INVITE_KIND = "meet.call-invite";
9127
+ var CALL_INVITE_VERSION = 1;
9128
+ function callInviteAction(invite) {
9035
9129
  return {
9036
- key: String(row["key"] ?? ""),
9037
- name: String(row["name"] ?? ""),
9038
- email: typeof row["email"] === "string" ? row["email"] : null,
9039
- userId: typeof row["user_id"] === "string" ? row["user_id"] : null,
9040
- sources: sources.filter((s) => s === "invitee" || s === "attendee"),
9041
- reachable: row["reachable"] !== false
9130
+ kind: CALL_INVITE_KIND,
9131
+ version: CALL_INVITE_VERSION,
9132
+ payload: {
9133
+ invite_id: invite.id,
9134
+ room_name: invite.roomName,
9135
+ mode: invite.mode,
9136
+ caller_name: invite.callerName,
9137
+ expires_at: invite.expiresAt
9138
+ }
9042
9139
  };
9043
9140
  }
9044
- async function loadFollowUpDraft(api, meetingId) {
9045
- const operation = "meet.loadFollowUpDraft";
9046
- const response = await api.request({
9047
- path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
9048
- method: "GET",
9049
- requireAuth: true,
9050
- operation
9051
- });
9052
- if (!response.ok) throw await refusalOf(response, operation);
9053
- const body = await response.json();
9054
- const last = body["last_sent"];
9141
+ function callInviteText(invite) {
9142
+ return invite.mode === "video" ? `${invite.callerName} started a video call` : `${invite.callerName} started an audio call`;
9143
+ }
9144
+ function createCallInviteHandler(options) {
9145
+ const now = options.now ?? (() => Date.now());
9055
9146
  return {
9056
- subject: String(body["subject"] ?? ""),
9057
- body: String(body["body"] ?? ""),
9058
- recipients: (Array.isArray(body["recipients"]) ? body["recipients"] : []).map(
9059
- (row) => asRecipient(row)
9060
- ),
9061
- wrapUpWritten: body["wrap_up_written"] === true,
9062
- lastSent: last && typeof last["at"] === "string" ? { at: last["at"], count: typeof last["count"] === "number" ? last["count"] : 0 } : null
9147
+ kind: CALL_INVITE_KIND,
9148
+ versions: [CALL_INVITE_VERSION],
9149
+ choices: (payload) => {
9150
+ if (Date.parse(payload.expires_at) < now()) return [];
9151
+ return [
9152
+ { id: "join", label: payload.mode === "video" ? "Join video" : "Join call", tone: "primary" },
9153
+ { id: "decline", label: "Decline", tone: "neutral" }
9154
+ ];
9155
+ },
9156
+ summarize: (payload) => payload.mode === "video" ? `${payload.caller_name} started a video call` : `${payload.caller_name} started an audio call`,
9157
+ execute: async (payload, context) => {
9158
+ const settledAt = new Date(now()).toISOString();
9159
+ const inviteId = asCallId(payload.invite_id);
9160
+ const pending = await options.repository.pendingInvites(asUserId(String(context.actorId)));
9161
+ const live = pending.find((invite) => invite.id === inviteId) ?? null;
9162
+ if (live === null) {
9163
+ return {
9164
+ kind: CALL_INVITE_KIND,
9165
+ messageId: context.messageId,
9166
+ actorId: context.actorId,
9167
+ outcome: "unavailable",
9168
+ label: "This call has ended",
9169
+ settledAt,
9170
+ 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."
9171
+ };
9172
+ }
9173
+ if (context.choice === "decline") {
9174
+ await options.calls.decline(inviteId);
9175
+ return {
9176
+ kind: CALL_INVITE_KIND,
9177
+ messageId: context.messageId,
9178
+ actorId: context.actorId,
9179
+ outcome: "declined",
9180
+ label: "Declined",
9181
+ settledAt
9182
+ };
9183
+ }
9184
+ const accepted = await options.calls.accept(inviteId, live.mode);
9185
+ await options.onJoin(accepted);
9186
+ return {
9187
+ kind: CALL_INVITE_KIND,
9188
+ messageId: context.messageId,
9189
+ actorId: context.actorId,
9190
+ outcome: "applied",
9191
+ label: "Joined",
9192
+ settledAt
9193
+ };
9194
+ }
9063
9195
  };
9064
9196
  }
9065
- async function sendFollowUp(api, meetingId, input) {
9066
- const operation = "meet.sendFollowUp";
9067
- const response = await api.request({
9068
- path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
9069
- method: "POST",
9070
- requireAuth: true,
9071
- operation,
9072
- body: {
9073
- subject: input.subject,
9074
- body: input.body,
9075
- recipient_keys: [...input.recipientKeys],
9076
- dry_run: input.dryRun === true
9077
- }
9078
- });
9079
- if (!response.ok) throw await refusalOf(response, operation);
9080
- const body = await response.json();
9197
+ var MEETING_INVITE_KIND = "meet.meeting-invite";
9198
+ var MEETING_INVITE_VERSION = 1;
9199
+ function meetingInviteAction(payload) {
9200
+ return { kind: MEETING_INVITE_KIND, version: MEETING_INVITE_VERSION, payload };
9201
+ }
9202
+ function createMeetingInviteHandler(options) {
9203
+ const now = options.now ?? (() => Date.now());
9081
9204
  return {
9082
- dryRun: body["dry_run"] === true,
9083
- queued: Array.isArray(body["queued"]) ? body["queued"] : [],
9084
- skipped: Array.isArray(body["skipped"]) ? body["skipped"].map((row) => ({
9085
- key: String(row["key"] ?? ""),
9086
- reason: String(row["reason"] ?? "")
9087
- })) : []
9205
+ kind: MEETING_INVITE_KIND,
9206
+ versions: [MEETING_INVITE_VERSION],
9207
+ // A DURABLE LINK NEVER EXPIRES (R3) — the chip stays useful after the
9208
+ // meeting too, because that is when people go looking for the notes.
9209
+ choices: () => [{ id: "open", label: "Open meeting", tone: "primary" }],
9210
+ summarize: (payload) => payload.scheduled_for === null ? payload.title : `${payload.title} \u2014 ${payload.scheduled_for}`,
9211
+ execute: async (payload, context) => {
9212
+ if (payload.slug.length === 0) {
9213
+ throw new MeetError(
9214
+ "invalid-response",
9215
+ "meet.meeting-invite: the payload carries no slug",
9216
+ "A meeting invitation without a durable slug cannot be opened. Fix the sender; never synthesize a link on the receiving side."
9217
+ );
9218
+ }
9219
+ await options.onOpen(payload);
9220
+ return {
9221
+ kind: MEETING_INVITE_KIND,
9222
+ messageId: context.messageId,
9223
+ actorId: context.actorId,
9224
+ outcome: "applied",
9225
+ label: "Opened",
9226
+ settledAt: new Date(now()).toISOString()
9227
+ };
9228
+ }
9088
9229
  };
9089
9230
  }
9090
9231
 
9091
9232
  // src/core/management.ts
9092
- async function refusal(response, operation) {
9093
- let body = null;
9094
- try {
9095
- body = await response.json();
9096
- } catch {
9097
- body = null;
9098
- }
9099
- const said = readServerRefusal(body);
9100
- return new MeetError(
9101
- response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
9102
- `${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
9103
- said?.remedy || "The meeting itself is saved \u2014 only the telling failed. Try again; the call is idempotent."
9233
+ function refusal(response, operation) {
9234
+ return refusalFromResponse(
9235
+ response,
9236
+ operation,
9237
+ "The meeting itself is saved \u2014 only the telling failed. Try again; the call is idempotent."
9104
9238
  );
9105
9239
  }
9106
9240
  function count(body, key) {
@@ -9255,6 +9389,7 @@ export {
9255
9389
  attendanceCsv,
9256
9390
  attendanceReport,
9257
9391
  attendanceTotals,
9392
+ attendeeKey,
9258
9393
  callInviteAction,
9259
9394
  callInviteText,
9260
9395
  callStateLabel,
@@ -9358,6 +9493,7 @@ export {
9358
9493
  summarizeMeetingRecordRestrictions,
9359
9494
  transcriptDocument,
9360
9495
  unavailableFrom,
9496
+ uniqueAttendees,
9361
9497
  useAudioLevel,
9362
9498
  useCalls,
9363
9499
  useCanModerate,