@ai-matrx/meet 0.7.7 → 0.7.8

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
@@ -502,6 +502,29 @@ function readText(data) {
502
502
  }
503
503
  return "";
504
504
  }
505
+ var SERVER_FAILED_SENTENCE = "The server hit an unexpected problem and nothing was changed.";
506
+ function isRawServerText(value) {
507
+ const text3 = value.trim();
508
+ 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);
509
+ }
510
+ async function refusalFromResponse(response, operation, fallbackRemedy) {
511
+ let body = null;
512
+ try {
513
+ body = await response.json();
514
+ } catch {
515
+ body = null;
516
+ }
517
+ const said = readServerRefusal(body);
518
+ const code = response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport";
519
+ const error = new MeetError(
520
+ code,
521
+ said?.reason ?? SERVER_FAILED_SENTENCE,
522
+ said !== null && said.remedy.length > 0 ? said.remedy : fallbackRemedy
523
+ );
524
+ error.operation = operation;
525
+ error.status = response.status;
526
+ return error;
527
+ }
505
528
  function readServerRefusal(body) {
506
529
  if (typeof body === "string") {
507
530
  return body.trim().length === 0 ? null : { reason: body.trim(), remedy: "" };
@@ -520,7 +543,8 @@ function readServerRefusal(body) {
520
543
  return messages.length === 0 ? null : { reason: messages.join("; "), remedy: "" };
521
544
  }
522
545
  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;
546
+ const humanOrNull = (value) => typeof value === "string" && value.trim().length > 0 && !isRawServerText(value) ? value.trim() : null;
547
+ 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
548
  if (reason === null) return null;
525
549
  const remedy = typeof row["remedy"] === "string" ? row["remedy"] : "";
526
550
  if (remedy.length > 0) return { reason, remedy };
@@ -1574,6 +1598,7 @@ var EMPTY = {
1574
1598
  spotlight: null,
1575
1599
  moderationNotice: null,
1576
1600
  actionNotice: null,
1601
+ privateChatRequest: null,
1577
1602
  effects: DEFAULT_EFFECTS,
1578
1603
  transcript: [],
1579
1604
  notes: [],
@@ -3763,6 +3788,10 @@ function createRoomEngine(options) {
3763
3788
  }
3764
3789
  },
3765
3790
  disconnected(reason) {
3791
+ if (current !== null) {
3792
+ tokens.invalidate(current.roomName);
3793
+ current = null;
3794
+ }
3766
3795
  store.patch({ phase: "left" });
3767
3796
  options.onLeft?.(reason);
3768
3797
  }
@@ -4065,19 +4094,8 @@ function createInRoomClient(options) {
4065
4094
  const token = options.resolveRoomToken();
4066
4095
  return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
4067
4096
  }
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
- );
4097
+ function refusal2(response, operation, remedy) {
4098
+ return refusalFromResponse(response, operation, remedy);
4081
4099
  }
4082
4100
  return {
4083
4101
  async moderate(request) {
@@ -6037,195 +6055,596 @@ function useGuestRecordAccess(meeting) {
6037
6055
  // src/react/components.tsx
6038
6056
  import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
6039
6057
  import { ErrorBox } from "@ai-matrx/design-system";
6040
- import { avatarPaletteIndex, formatDurationMs as formatDurationMs2, getInitials } from "@ai-matrx/kit/format";
6058
+ import { avatarPaletteIndex, formatDurationMs as formatDurationMs3, getInitials } from "@ai-matrx/kit/format";
6041
6059
 
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
- }
6060
+ // src/core/after.ts
6061
+ import { formatDurationMs as formatDurationMs2 } from "@ai-matrx/kit/format";
6062
+ function kindOf(person) {
6063
+ if (person.isAgent) return "assistant";
6064
+ if (person.role === "host") return "host";
6065
+ if (person.role === "cohost") return "cohost";
6066
+ if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
6067
+ return "member";
6074
6068
  }
6075
- function countLabel(count2, singular, plural = `${singular}s`) {
6076
- return `${count2} ${count2 === 1 ? singular : plural}`;
6069
+ function ms(iso) {
6070
+ if (iso === null) return null;
6071
+ const at = new Date(iso).getTime();
6072
+ return Number.isNaN(at) ? null : at;
6077
6073
  }
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
6074
+ function attendeeKey(person, names = {}) {
6075
+ if (person.userId !== null) return `user:${person.userId}`;
6076
+ if (person.isAgent) return `agent:${person.identity}`;
6077
+ const name = displayNameFor(names, person.identity, person.displayName).trim().toLowerCase();
6078
+ return name.length > 0 ? `guest-name:${name}` : `identity:${person.identity}`;
6079
+ }
6080
+ function uniqueAttendees(attendees, names = {}) {
6081
+ const byKey = /* @__PURE__ */ new Map();
6082
+ for (const person of attendees) {
6083
+ const key = attendeeKey(person, names);
6084
+ const kept = byKey.get(key);
6085
+ if (kept === void 0) {
6086
+ byKey.set(key, person);
6087
+ continue;
6097
6088
  }
6098
- );
6089
+ const keptJoin = ms(kept.joinedAt);
6090
+ const join = ms(person.joinedAt);
6091
+ if (join !== null && (keptJoin === null || join < keptJoin)) byKey.set(key, person);
6092
+ }
6093
+ return [...byKey.values()];
6094
+ }
6095
+ var KIND_RANK = {
6096
+ host: 0,
6097
+ cohost: 1,
6098
+ member: 2,
6099
+ "signed-in guest": 3,
6100
+ guest: 4,
6101
+ assistant: 5
6102
+ };
6103
+ function coveredMs(intervals) {
6104
+ const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
6105
+ let total = 0;
6106
+ let from = -Infinity;
6107
+ let to = -Infinity;
6108
+ for (const [start, end] of sorted) {
6109
+ if (start > to) {
6110
+ if (to > from) total += to - from;
6111
+ from = start;
6112
+ to = end;
6113
+ } else if (end > to) {
6114
+ to = end;
6115
+ }
6116
+ }
6117
+ if (to > from) total += to - from;
6118
+ return total;
6099
6119
  }
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
- ] });
6120
+ function attendanceReport(meeting, attendees, names = {}) {
6121
+ const end = ms(meeting.endedAt);
6122
+ const perJoin = attendees.map((person) => {
6123
+ const joined = ms(person.joinedAt);
6124
+ const left = ms(person.leftAt);
6125
+ const until = left ?? end;
6126
+ const duration = joined !== null && until !== null && until >= joined ? until - joined : null;
6127
+ return {
6128
+ id: person.id,
6129
+ identity: person.identity,
6130
+ name: displayNameFor(names, person.identity, person.displayName),
6131
+ userId: person.userId,
6132
+ kind: kindOf(person),
6133
+ joinedAt: person.joinedAt,
6134
+ leftAt: person.leftAt,
6135
+ durationMs: duration,
6136
+ durationToEnd: duration !== null && left === null,
6137
+ admission: person.admissionState,
6138
+ joins: 1
6139
+ };
6140
+ });
6141
+ const groups = /* @__PURE__ */ new Map();
6142
+ attendees.forEach((person, index) => {
6143
+ const key = attendeeKey(person, names);
6144
+ const group = groups.get(key) ?? { rows: [], people: [] };
6145
+ group.rows.push(perJoin[index]);
6146
+ group.people.push(person);
6147
+ groups.set(key, group);
6148
+ });
6149
+ const rows = [...groups.values()].map(({ rows: joins }) => {
6150
+ if (joins.length === 1) return joins[0];
6151
+ const came = joins.filter((row) => row.joinedAt !== null);
6152
+ const first = [...came].sort((a, b) => (ms(a.joinedAt) ?? 0) - (ms(b.joinedAt) ?? 0))[0] ?? joins[0];
6153
+ const kind = joins.reduce(
6154
+ (best, row) => KIND_RANK[row.kind] < KIND_RANK[best] ? row.kind : best,
6155
+ first.kind
6156
+ );
6157
+ const open = came.some((row) => row.leftAt === null);
6158
+ const lastLeave = came.reduce((latest, row) => {
6159
+ const left = ms(row.leftAt);
6160
+ return left !== null && (latest === null || left > latest) ? left : latest;
6161
+ }, null);
6162
+ const intervals = came.flatMap((row) => {
6163
+ const joined = ms(row.joinedAt);
6164
+ return joined !== null && row.durationMs !== null ? [[joined, joined + row.durationMs]] : [];
6165
+ });
6166
+ return {
6167
+ ...first,
6168
+ kind,
6169
+ leftAt: open ? null : lastLeave === null ? null : new Date(lastLeave).toISOString(),
6170
+ durationMs: intervals.length === 0 ? null : coveredMs(intervals),
6171
+ durationToEnd: came.some((row) => row.durationToEnd),
6172
+ joins: came.length === 0 ? joins.length : came.length
6173
+ };
6174
+ });
6175
+ const rank = KIND_RANK;
6176
+ return rows.sort((a, b) => {
6177
+ if (rank[a.kind] !== rank[b.kind]) return rank[a.kind] - rank[b.kind];
6178
+ return (ms(a.joinedAt) ?? Number.MAX_SAFE_INTEGER) - (ms(b.joinedAt) ?? Number.MAX_SAFE_INTEGER);
6179
+ });
6106
6180
  }
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
- ] });
6181
+ function attendanceTotals(rows) {
6182
+ const humans = rows.filter((row) => row.kind !== "assistant");
6183
+ const attended = humans.filter((row) => row.joinedAt !== null);
6184
+ const timed = attended.filter((row) => row.durationMs !== null);
6185
+ return {
6186
+ people: attended.length,
6187
+ guests: attended.filter((row) => row.kind === "guest" || row.kind === "signed-in guest").length,
6188
+ neverAdmitted: humans.filter((row) => row.joinedAt === null).length,
6189
+ averageMs: timed.length === 0 ? null : Math.round(timed.reduce((sum, row) => sum + (row.durationMs ?? 0), 0) / timed.length)
6190
+ };
6115
6191
  }
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
- ] });
6192
+ function csvCell(value) {
6193
+ if (value === null) return "";
6194
+ const text3 = String(value);
6195
+ return /[",\n\r]/.test(text3) ? `"${text3.replace(/"/g, '""')}"` : text3;
6121
6196
  }
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
- ] });
6197
+ var ATTENDANCE_CSV_HEADER = [
6198
+ "Meeting",
6199
+ "Meeting started",
6200
+ "Name",
6201
+ "Attended as",
6202
+ "Joined",
6203
+ "Left",
6204
+ "Minutes in the meeting",
6205
+ "Admission"
6206
+ ];
6207
+ function attendanceCsv(reports) {
6208
+ const lines = [ATTENDANCE_CSV_HEADER.join(",")];
6209
+ for (const { meeting, rows } of reports) {
6210
+ for (const row of rows) {
6211
+ lines.push(
6212
+ [
6213
+ meeting.title,
6214
+ meeting.startedAt ?? meeting.scheduledFor,
6215
+ row.name,
6216
+ row.kind,
6217
+ row.joinedAt,
6218
+ row.leftAt,
6219
+ row.durationMs === null ? null : Math.round(row.durationMs / 6e3) / 10,
6220
+ row.admission
6221
+ ].map(csvCell).join(",")
6222
+ );
6223
+ }
6224
+ }
6225
+ return `${lines.join("\r\n")}\r
6226
+ `;
6128
6227
  }
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
- ] });
6228
+ function recordingOffsetMs(at, recording) {
6229
+ if (recording === null || recording.state !== "available") return null;
6230
+ const start = ms(recording.startedAt);
6231
+ const moment = ms(at);
6232
+ if (start === null || moment === null || moment < start) return null;
6233
+ const end = ms(recording.endedAt);
6234
+ if (end !== null && moment > end) return null;
6235
+ return moment - start;
6135
6236
  }
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" }) });
6237
+ function clockPosition(offsetMs) {
6238
+ const total = Math.max(0, Math.floor(offsetMs / 1e3));
6239
+ const h = Math.floor(total / 3600);
6240
+ const m = Math.floor(total % 3600 / 60);
6241
+ const s = total % 60;
6242
+ const ss = String(s).padStart(2, "0");
6243
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
6138
6244
  }
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
- ] });
6245
+ function searchTranscript(segments, query, names = {}) {
6246
+ const needle = fold(query.trim());
6247
+ if (needle.length === 0) return [];
6248
+ const out = [];
6249
+ for (const segment of segments) {
6250
+ const hay = fold(segment.text);
6251
+ const ranges = [];
6252
+ let from = 0;
6253
+ for (; ; ) {
6254
+ const at = hay.indexOf(needle, from);
6255
+ if (at < 0) break;
6256
+ ranges.push([at, at + needle.length]);
6257
+ from = at + needle.length;
6258
+ }
6259
+ const speaker = fold(displayNameFor(names, segment.identity, segment.speaker));
6260
+ if (ranges.length > 0 || speaker.includes(needle)) {
6261
+ out.push({ segmentId: segment.id, ranges });
6262
+ }
6263
+ }
6264
+ return out;
6145
6265
  }
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
- ] });
6266
+ function fold(value) {
6267
+ return value.normalize("NFD").replace(/[̀-ͯ]/g, (mark) => mark.length === 1 ? "" : mark).toLowerCase();
6153
6268
  }
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" }) });
6269
+ function clockOfDay(iso, options) {
6270
+ const at = new Date(iso);
6271
+ if (Number.isNaN(at.getTime())) return "";
6272
+ return at.toLocaleTimeString(options.locale, {
6273
+ hour: "numeric",
6274
+ minute: "2-digit",
6275
+ second: "2-digit",
6276
+ ...options.timeZone !== void 0 ? { timeZone: options.timeZone } : {}
6277
+ });
6156
6278
  }
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
- ] });
6163
- }
6164
- function RecordIcon(props) {
6165
- return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "8", fill: "currentColor", stroke: "none" }) });
6279
+ function vttStamp(offsetMs) {
6280
+ const total = Math.max(0, Math.round(offsetMs));
6281
+ const h = Math.floor(total / 36e5);
6282
+ const m = Math.floor(total % 36e5 / 6e4);
6283
+ const s = Math.floor(total % 6e4 / 1e3);
6284
+ const milli = total % 1e3;
6285
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${String(milli).padStart(3, "0")}`;
6166
6286
  }
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
- ] });
6287
+ function transcriptDocument(bundle, format, options = {}) {
6288
+ const names = options.names ?? bundle.names;
6289
+ const lines = bundle.transcript.filter((line) => line.text.trim().length > 0);
6290
+ const speaker = (line) => displayNameFor(names, line.identity, line.speaker);
6291
+ if (format === "vtt") {
6292
+ const zeroIso = bundle.recording?.state === "available" && bundle.recording.startedAt !== null ? bundle.recording.startedAt : bundle.meeting.startedAt ?? lines[0]?.startedAt ?? null;
6293
+ const zero = ms(zeroIso) ?? 0;
6294
+ const cues = ["WEBVTT", ""];
6295
+ lines.forEach((line, index) => {
6296
+ const start = (ms(line.startedAt) ?? zero) - zero;
6297
+ if (start < 0) return;
6298
+ const next = lines[index + 1];
6299
+ const nextStart = next !== void 0 ? (ms(next.startedAt) ?? zero) - zero : start + 4e3;
6300
+ const end = Math.max(start + 500, Math.min(nextStart, start + 1e4));
6301
+ cues.push(`${vttStamp(start)} --> ${vttStamp(end)}`);
6302
+ cues.push(`<v ${speaker(line).replace(/[<>]/g, "")}>${line.text.replace(/-->/g, "->")}`);
6303
+ cues.push("");
6304
+ });
6305
+ return cues.join("\n");
6306
+ }
6307
+ const out = [];
6308
+ const title = bundle.meeting.title;
6309
+ out.push(format === "md" ? `# ${title} \u2014 transcript` : `${title} \u2014 transcript`);
6310
+ out.push("");
6311
+ let last = null;
6312
+ for (const line of lines) {
6313
+ if (line.identity !== last) {
6314
+ if (last !== null) out.push("");
6315
+ const who = speaker(line);
6316
+ const clock = clockOfDay(line.startedAt, options);
6317
+ out.push(format === "md" ? `**${who}** \xB7 ${clock}` : `${who} (${clock})`);
6318
+ last = line.identity;
6319
+ }
6320
+ out.push(line.text.trim());
6321
+ }
6322
+ if (lines.length === 0) out.push("No transcript was captured for this meeting.");
6323
+ return `${out.join("\n")}
6324
+ `;
6172
6325
  }
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
- ] });
6326
+ function bullet(note) {
6327
+ const owner = note.assigneeDisplayName !== null ? ` \u2014 ${note.assigneeDisplayName}` : "";
6328
+ return `- ${note.text.trim()}${owner}`;
6178
6329
  }
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
- ] });
6330
+ function notesDocument(bundle, options = {}) {
6331
+ const out = [`# ${bundle.meeting.title}`, ""];
6332
+ if (options.when) out.push(`_${options.when}_`, "");
6333
+ const people = uniqueAttendees(
6334
+ bundle.attendees.filter((person) => !person.isAgent && person.joinedAt !== null),
6335
+ bundle.names
6336
+ ).map((person) => displayNameFor(bundle.names, person.identity, person.displayName));
6337
+ if (people.length > 0) out.push(`**Attended:** ${people.join(", ")}`, "");
6338
+ out.push("## Summary", "", bundle.summary?.text.trim() || "No summary was written.", "");
6339
+ out.push("## Decisions", "");
6340
+ out.push(...bundle.decisions.length > 0 ? bundle.decisions.map(bullet) : ["None recorded."], "");
6341
+ out.push("## Action items", "");
6342
+ out.push(
6343
+ ...bundle.actionItems.length > 0 ? bundle.actionItems.map(bullet) : ["None recorded."],
6344
+ ""
6345
+ );
6346
+ if (options.link) out.push(`[Open the meeting record](${options.link})`, "");
6347
+ return out.join("\n");
6184
6348
  }
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
- ] });
6349
+ function meetingLength(meeting) {
6350
+ const start = ms(meeting.startedAt);
6351
+ const end = ms(meeting.endedAt);
6352
+ if (start === null || end === null || end < start) return null;
6353
+ return formatDurationMs2(end - start, { style: "long" });
6190
6354
  }
6191
- function CheckIcon(props) {
6192
- return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("path", { d: "M20 6 9 17l-5-5" }) });
6355
+ async function loadMeetingChat(api, meetingId, roomToken = null) {
6356
+ const operation = "meet.loadMeetingChat";
6357
+ let response;
6358
+ try {
6359
+ response = await api.request({
6360
+ path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(meetingId)}`,
6361
+ method: "GET",
6362
+ operation,
6363
+ requireAuth: roomToken === null,
6364
+ ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
6365
+ });
6366
+ } catch (error) {
6367
+ return {
6368
+ messages: [],
6369
+ refusal: {
6370
+ message: `The chat could not be read (${error.message}).`,
6371
+ remedy: error.remedy ?? "Reload the page."
6372
+ }
6373
+ };
6374
+ }
6375
+ if (!response.ok) {
6376
+ let body = null;
6377
+ try {
6378
+ body = await response.json();
6379
+ } catch {
6380
+ body = null;
6381
+ }
6382
+ const said = readServerRefusal(body);
6383
+ return {
6384
+ messages: [],
6385
+ refusal: {
6386
+ message: said?.reason ?? `The chat could not be read (the server answered ${response.status}).`,
6387
+ remedy: said?.remedy || "Reload the page."
6388
+ }
6389
+ };
6390
+ }
6391
+ const payload = await response.json();
6392
+ const rows = Array.isArray(payload.messages) ? payload.messages : [];
6393
+ return {
6394
+ messages: rows.map(
6395
+ (row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
6396
+ ).filter((message) => message !== null),
6397
+ refusal: null
6398
+ };
6193
6399
  }
6194
- function CloseIcon(props) {
6195
- return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("path", { d: "M18 6 6 18M6 6l12 12" }) });
6400
+ function refusalOf(response, operation) {
6401
+ return refusalFromResponse(response, operation, "Nothing was sent. Try again.");
6196
6402
  }
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
- ] });
6403
+ function asRecipient(row) {
6404
+ const sources = Array.isArray(row["sources"]) ? row["sources"] : [];
6405
+ return {
6406
+ key: String(row["key"] ?? ""),
6407
+ name: String(row["name"] ?? ""),
6408
+ email: typeof row["email"] === "string" ? row["email"] : null,
6409
+ userId: typeof row["user_id"] === "string" ? row["user_id"] : null,
6410
+ sources: sources.filter((s) => s === "invitee" || s === "attendee"),
6411
+ reachable: row["reachable"] !== false
6412
+ };
6202
6413
  }
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
- ] });
6414
+ async function loadFollowUpDraft(api, meetingId) {
6415
+ const operation = "meet.loadFollowUpDraft";
6416
+ const response = await api.request({
6417
+ path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
6418
+ method: "GET",
6419
+ requireAuth: true,
6420
+ operation
6421
+ });
6422
+ if (!response.ok) throw await refusalOf(response, operation);
6423
+ const body = await response.json();
6424
+ const last = body["last_sent"];
6425
+ return {
6426
+ subject: String(body["subject"] ?? ""),
6427
+ body: String(body["body"] ?? ""),
6428
+ recipients: (Array.isArray(body["recipients"]) ? body["recipients"] : []).map(
6429
+ (row) => asRecipient(row)
6430
+ ),
6431
+ wrapUpWritten: body["wrap_up_written"] === true,
6432
+ lastSent: last && typeof last["at"] === "string" ? { at: last["at"], count: typeof last["count"] === "number" ? last["count"] : 0 } : null
6433
+ };
6208
6434
  }
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
- ] });
6435
+ async function sendFollowUp(api, meetingId, input) {
6436
+ const operation = "meet.sendFollowUp";
6437
+ const response = await api.request({
6438
+ path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
6439
+ method: "POST",
6440
+ requireAuth: true,
6441
+ operation,
6442
+ body: {
6443
+ subject: input.subject,
6444
+ body: input.body,
6445
+ recipient_keys: [...input.recipientKeys],
6446
+ dry_run: input.dryRun === true
6447
+ }
6448
+ });
6449
+ if (!response.ok) throw await refusalOf(response, operation);
6450
+ const body = await response.json();
6451
+ return {
6452
+ dryRun: body["dry_run"] === true,
6453
+ queued: Array.isArray(body["queued"]) ? body["queued"] : [],
6454
+ skipped: Array.isArray(body["skipped"]) ? body["skipped"].map((row) => ({
6455
+ key: String(row["key"] ?? ""),
6456
+ reason: String(row["reason"] ?? "")
6457
+ })) : []
6458
+ };
6216
6459
  }
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
- ] });
6460
+
6461
+ // src/core/format.ts
6462
+ function participantSummary(participants) {
6463
+ const names = participants.filter((p) => !p.isLocal).map((p) => p.displayName);
6464
+ if (names.length === 0) return "Just you";
6465
+ if (names.length === 1) return names[0] ?? "";
6466
+ if (names.length === 2) return `${names[0]} and ${names[1]}`;
6467
+ return `${names[0]}, ${names[1]} and ${names.length - 2} ${names.length - 2 === 1 ? "other" : "others"}`;
6223
6468
  }
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
- ] });
6469
+ function callStateLabel(state, outgoing) {
6470
+ switch (state) {
6471
+ case "dialing":
6472
+ case "ringing":
6473
+ return outgoing ? "Calling\u2026" : "Incoming call";
6474
+ case "connecting":
6475
+ return "Connecting\u2026";
6476
+ case "active":
6477
+ return "In call";
6478
+ case "declined":
6479
+ return outgoing ? "Declined" : "You declined";
6480
+ case "missed":
6481
+ return outgoing ? "No answer" : "Missed call";
6482
+ case "busy":
6483
+ return outgoing ? "On another call" : "You were busy";
6484
+ case "cancelled":
6485
+ return outgoing ? "Cancelled" : "Cancelled by caller";
6486
+ case "ended":
6487
+ return "Call ended";
6488
+ case "failed":
6489
+ return "Call failed";
6490
+ default:
6491
+ return "Call";
6492
+ }
6493
+ }
6494
+ function countLabel(count2, singular, plural = `${singular}s`) {
6495
+ return `${count2} ${count2 === 1 ? singular : plural}`;
6496
+ }
6497
+
6498
+ // src/react/icons.tsx
6499
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
6500
+ function Icon({ children, ...props }) {
6501
+ return /* @__PURE__ */ jsx2(
6502
+ "svg",
6503
+ {
6504
+ width: "20",
6505
+ height: "20",
6506
+ viewBox: "0 0 24 24",
6507
+ fill: "none",
6508
+ stroke: "currentColor",
6509
+ strokeWidth: "1.75",
6510
+ strokeLinecap: "round",
6511
+ strokeLinejoin: "round",
6512
+ "aria-hidden": "true",
6513
+ focusable: "false",
6514
+ ...props,
6515
+ children
6516
+ }
6517
+ );
6518
+ }
6519
+ function MicIcon(props) {
6520
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6521
+ /* @__PURE__ */ jsx2("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }),
6522
+ /* @__PURE__ */ jsx2("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
6523
+ /* @__PURE__ */ jsx2("path", { d: "M12 19v3" })
6524
+ ] });
6525
+ }
6526
+ function MicOffIcon(props) {
6527
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6528
+ /* @__PURE__ */ jsx2("path", { d: "M2 2l20 20" }),
6529
+ /* @__PURE__ */ jsx2("path", { d: "M9 9v3a3 3 0 0 0 5.12 2.12" }),
6530
+ /* @__PURE__ */ jsx2("path", { d: "M15 9.34V5a3 3 0 0 0-5.94-.6" }),
6531
+ /* @__PURE__ */ jsx2("path", { d: "M19 10v2a7 7 0 0 1-.11 1.23M5 10v2a7 7 0 0 0 11 5.66" }),
6532
+ /* @__PURE__ */ jsx2("path", { d: "M12 19v3" })
6533
+ ] });
6534
+ }
6535
+ function CameraIcon(props) {
6536
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6537
+ /* @__PURE__ */ jsx2("path", { d: "m23 7-7 5 7 5V7Z" }),
6538
+ /* @__PURE__ */ jsx2("rect", { x: "1", y: "5", width: "15", height: "14", rx: "2" })
6539
+ ] });
6540
+ }
6541
+ function CameraOffIcon(props) {
6542
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6543
+ /* @__PURE__ */ jsx2("path", { d: "M2 2l20 20" }),
6544
+ /* @__PURE__ */ jsx2("path", { d: "M10.66 5H14a2 2 0 0 1 2 2v3.34l1 1L23 7v10" }),
6545
+ /* @__PURE__ */ jsx2("path", { d: "M16 16a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2" })
6546
+ ] });
6547
+ }
6548
+ function ScreenShareIcon(props) {
6549
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6550
+ /* @__PURE__ */ jsx2("rect", { x: "2", y: "3", width: "20", height: "14", rx: "2" }),
6551
+ /* @__PURE__ */ jsx2("path", { d: "M8 21h8M12 17v4" }),
6552
+ /* @__PURE__ */ jsx2("path", { d: "M12 13V8m0 0-2.5 2.5M12 8l2.5 2.5" })
6553
+ ] });
6554
+ }
6555
+ function PhoneIcon(props) {
6556
+ 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" }) });
6557
+ }
6558
+ function PhoneOffIcon(props) {
6559
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6560
+ /* @__PURE__ */ jsx2("path", { d: "M2 2l20 20" }),
6561
+ /* @__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" }),
6562
+ /* @__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" })
6563
+ ] });
6564
+ }
6565
+ function HandIcon(props) {
6566
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6567
+ /* @__PURE__ */ jsx2("path", { d: "M18 11V6a2 2 0 0 0-4 0v5" }),
6568
+ /* @__PURE__ */ jsx2("path", { d: "M14 10V4a2 2 0 0 0-4 0v7" }),
6569
+ /* @__PURE__ */ jsx2("path", { d: "M10 10.5V6a2 2 0 0 0-4 0v8" }),
6570
+ /* @__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" })
6571
+ ] });
6572
+ }
6573
+ function ChatIcon(props) {
6574
+ 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" }) });
6575
+ }
6576
+ function PeopleIcon(props) {
6577
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6578
+ /* @__PURE__ */ jsx2("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }),
6579
+ /* @__PURE__ */ jsx2("circle", { cx: "9", cy: "7", r: "4" }),
6580
+ /* @__PURE__ */ jsx2("path", { d: "M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" })
6581
+ ] });
6582
+ }
6583
+ function RecordIcon(props) {
6584
+ return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "8", fill: "currentColor", stroke: "none" }) });
6585
+ }
6586
+ function SparklesIcon(props) {
6587
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6588
+ /* @__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" }),
6589
+ /* @__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" })
6590
+ ] });
6591
+ }
6592
+ function LockIcon(props) {
6593
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6594
+ /* @__PURE__ */ jsx2("rect", { x: "4", y: "10", width: "16", height: "11", rx: "2" }),
6595
+ /* @__PURE__ */ jsx2("path", { d: "M8 10V7a4 4 0 0 1 8 0v3" })
6596
+ ] });
6597
+ }
6598
+ function LayoutIcon(props) {
6599
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6600
+ /* @__PURE__ */ jsx2("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
6601
+ /* @__PURE__ */ jsx2("path", { d: "M3 9h18M9 21V9" })
6602
+ ] });
6603
+ }
6604
+ function SettingsIcon(props) {
6605
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6606
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "3" }),
6607
+ /* @__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" })
6608
+ ] });
6609
+ }
6610
+ function CheckIcon(props) {
6611
+ return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("path", { d: "M20 6 9 17l-5-5" }) });
6612
+ }
6613
+ function CloseIcon(props) {
6614
+ return /* @__PURE__ */ jsx2(Icon, { ...props, children: /* @__PURE__ */ jsx2("path", { d: "M18 6 6 18M6 6l12 12" }) });
6615
+ }
6616
+ function PinIcon(props) {
6617
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6618
+ /* @__PURE__ */ jsx2("path", { d: "M12 17v5" }),
6619
+ /* @__PURE__ */ jsx2("path", { d: "M9 2h6l-1 6 3.5 3.5V14H6.5v-2.5L10 8 9 2Z" })
6620
+ ] });
6621
+ }
6622
+ function WifiLowIcon(props) {
6623
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6624
+ /* @__PURE__ */ jsx2("path", { d: "M12 20h.01" }),
6625
+ /* @__PURE__ */ jsx2("path", { d: "M8.5 16.5a5 5 0 0 1 7 0" })
6626
+ ] });
6627
+ }
6628
+ function SmileIcon(props) {
6629
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6630
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "10" }),
6631
+ /* @__PURE__ */ jsx2("path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }),
6632
+ /* @__PURE__ */ jsx2("line", { x1: "9", x2: "9.01", y1: "9", y2: "9" }),
6633
+ /* @__PURE__ */ jsx2("line", { x1: "15", x2: "15.01", y1: "9", y2: "9" })
6634
+ ] });
6635
+ }
6636
+ function MoreIcon(props) {
6637
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6638
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "1" }),
6639
+ /* @__PURE__ */ jsx2("circle", { cx: "19", cy: "12", r: "1" }),
6640
+ /* @__PURE__ */ jsx2("circle", { cx: "5", cy: "12", r: "1" })
6641
+ ] });
6642
+ }
6643
+ function SearchIcon(props) {
6644
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
6645
+ /* @__PURE__ */ jsx2("circle", { cx: "11", cy: "11", r: "8" }),
6646
+ /* @__PURE__ */ jsx2("path", { d: "m21 21-4.3-4.3" })
6647
+ ] });
6229
6648
  }
6230
6649
  function KeyboardIcon(props) {
6231
6650
  return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
@@ -6487,6 +6906,15 @@ function ControlBar({ onLeave }) {
6487
6906
  const micUnavailable = useLocalMedia().microphone.unavailable;
6488
6907
  const unread = useUnreadChat(panel === "chat");
6489
6908
  const toggle = (next) => setPanel(panel === next ? "none" : next);
6909
+ const privateChatAt = snapshot?.privateChatRequest?.at ?? null;
6910
+ const meetStore = useMeetHost()?.store ?? null;
6911
+ useEffect3(() => {
6912
+ const request = meetStore?.snapshot().privateChatRequest ?? null;
6913
+ if (request === null) return;
6914
+ setChatTo(request.to);
6915
+ setPanel("chat");
6916
+ meetStore?.patch({ privateChatRequest: null });
6917
+ }, [privateChatAt, meetStore]);
6490
6918
  useMeetShortcuts({
6491
6919
  mic: () => void room.toggleMicrophone(),
6492
6920
  camera: () => void room.toggleCamera(),
@@ -6726,7 +7154,12 @@ function Popover({
6726
7154
  placement = "up"
6727
7155
  }) {
6728
7156
  const ref = useRef3(null);
6729
- const [style, setStyle] = useState3({ visibility: "hidden" });
7157
+ const [style, setStyle] = useState3({
7158
+ position: "fixed",
7159
+ top: 0,
7160
+ left: 0,
7161
+ visibility: "hidden"
7162
+ });
6730
7163
  useLayoutEffect(() => {
6731
7164
  const element = ref.current;
6732
7165
  const anchor = element?.parentElement ?? null;
@@ -6742,14 +7175,35 @@ function Popover({
6742
7175
  );
6743
7176
  const fitsBelow = rect.bottom + gutter + height <= window.innerHeight;
6744
7177
  const fitsAbove = rect.top - gutter - height >= 0;
7178
+ if (!fitsBelow && !fitsAbove) {
7179
+ const top = Math.max(gutter, window.innerHeight - height - gutter);
7180
+ setStyle({
7181
+ position: "fixed",
7182
+ left,
7183
+ top,
7184
+ maxHeight: window.innerHeight - top - gutter,
7185
+ overflowY: "auto"
7186
+ });
7187
+ return;
7188
+ }
6745
7189
  const down = placement === "down" ? fitsBelow || !fitsAbove : !fitsAbove && fitsBelow;
6746
7190
  setStyle(
6747
7191
  down ? { position: "fixed", left, top: rect.bottom + gutter } : { position: "fixed", left, bottom: window.innerHeight - rect.top + gutter }
6748
7192
  );
6749
7193
  };
6750
7194
  place();
7195
+ const frame = requestAnimationFrame(place);
7196
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => place());
7197
+ observer?.observe(document.body);
7198
+ observer?.observe(element);
6751
7199
  window.addEventListener("resize", place);
6752
- return () => window.removeEventListener("resize", place);
7200
+ window.addEventListener("scroll", place, true);
7201
+ return () => {
7202
+ cancelAnimationFrame(frame);
7203
+ observer?.disconnect();
7204
+ window.removeEventListener("resize", place);
7205
+ window.removeEventListener("scroll", place, true);
7206
+ };
6753
7207
  }, [placement]);
6754
7208
  useEffect3(() => {
6755
7209
  const onKey = (event) => {
@@ -7126,11 +7580,15 @@ function PersonMenu({
7126
7580
  const pinned = snapshot?.pinned === participant.identity;
7127
7581
  const spotlit = snapshot?.spotlight === participant.identity;
7128
7582
  const hasAccount = participant.userId !== null;
7583
+ const [refused, setRefused] = useState3(false);
7129
7584
  const act = (request) => {
7585
+ setRefused(false);
7130
7586
  void controls.run(request).then((result) => {
7131
7587
  if (result !== null) onClose();
7588
+ else setRefused(true);
7132
7589
  });
7133
7590
  };
7591
+ 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
7592
  if (mode === "rename" || mode === "report") {
7135
7593
  const renaming = mode === "rename";
7136
7594
  return /* @__PURE__ */ jsx3(Popover, { label: renaming ? "Rename" : "Report", onClose, placement, children: /* @__PURE__ */ jsxs2(
@@ -7168,7 +7626,7 @@ function PersonMenu({
7168
7626
  }
7169
7627
  )
7170
7628
  ] }),
7171
- controls.notice?.failed === true ? /* @__PURE__ */ jsx3("p", { className: "mx-meet__error", children: controls.notice.text }) : null,
7629
+ refusal2,
7172
7630
  /* @__PURE__ */ jsxs2("div", { className: "mx-meet__menu-actions", children: [
7173
7631
  /* @__PURE__ */ jsx3("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Back" }),
7174
7632
  /* @__PURE__ */ jsx3("button", { type: "submit", className: "mx-meet__send", disabled: text3.trim().length === 0 || controls.busy, children: renaming ? "Rename" : "Report" })
@@ -7181,6 +7639,7 @@ function PersonMenu({
7181
7639
  const removing = mode === "remove";
7182
7640
  return /* @__PURE__ */ jsx3(Popover, { label: removing ? "Remove" : "Make host", onClose, placement, children: /* @__PURE__ */ jsxs2("div", { className: "mx-meet__menu-form", children: [
7183
7641
  /* @__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.` }),
7642
+ refusal2,
7184
7643
  /* @__PURE__ */ jsxs2("div", { className: "mx-meet__menu-actions", children: [
7185
7644
  /* @__PURE__ */ jsx3("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Cancel" }),
7186
7645
  /* @__PURE__ */ jsx3(
@@ -7211,11 +7670,15 @@ function PersonMenu({
7211
7670
  ]
7212
7671
  }
7213
7672
  ),
7214
- !self && !participant.isAgent && onMessage !== void 0 ? /* @__PURE__ */ jsxs2(
7673
+ !self && !participant.isAgent && (onMessage !== void 0 || host !== null) ? /* @__PURE__ */ jsxs2(
7215
7674
  MenuItem,
7216
7675
  {
7217
7676
  onSelect: () => {
7218
- onMessage(participant.identity);
7677
+ if (onMessage !== void 0) onMessage(participant.identity);
7678
+ else
7679
+ host?.store.patch({
7680
+ privateChatRequest: { to: participant.identity, at: Date.now() }
7681
+ });
7219
7682
  onClose();
7220
7683
  },
7221
7684
  children: [
@@ -7842,7 +8305,7 @@ function PreJoinBody({
7842
8305
  microphone,
7843
8306
  ...meetingId !== void 0 ? { meetingId } : {},
7844
8307
  ...slug !== void 0 ? { slug } : {}
7845
- }).then(() => onJoined?.()).finally(() => setJoining(false));
8308
+ }).then(() => onJoined?.()).catch(() => void 0).finally(() => setJoining(false));
7846
8309
  },
7847
8310
  children: joining ? "Joining\u2026" : "Join now"
7848
8311
  }
@@ -7978,7 +8441,7 @@ function MeetingRoomBody({
7978
8441
  /* @__PURE__ */ jsx3("h1", { className: "mx-meet__title", children: snapshot?.meeting?.title ?? "Meeting" }),
7979
8442
  /* @__PURE__ */ jsxs2("span", { className: "mx-meet__meta", children: [
7980
8443
  participantSummary(snapshot?.participants ?? []),
7981
- elapsed > 0 ? ` \xB7 ${formatDurationMs2(elapsed)}` : ""
8444
+ elapsed > 0 ? ` \xB7 ${formatDurationMs3(elapsed)}` : ""
7982
8445
  ] }),
7983
8446
  /* @__PURE__ */ jsx3(RecordingIndicator, {}),
7984
8447
  snapshot?.locked === true ? /* @__PURE__ */ jsxs2("span", { className: "mx-meet__badge", children: [
@@ -8284,7 +8747,10 @@ function MeetingRecordBody({
8284
8747
  const blocks = groupTranscript(bundle.transcript, bundle.names);
8285
8748
  const notice = recordingNotice(bundle.recording);
8286
8749
  const fileId = playableRecordingFileId(bundle.recording);
8287
- const attended = bundle.attendees.filter((person) => person.admissionState !== "denied");
8750
+ const attended = uniqueAttendees(
8751
+ bundle.attendees.filter((person) => person.admissionState !== "denied"),
8752
+ bundle.names
8753
+ );
8288
8754
  const restrictionNotices = summarizeMeetingRecordRestrictions(bundle.restrictions);
8289
8755
  const guestFinishedMeeting = host.identity.userId === null && meeting.endedAt !== null;
8290
8756
  const everyRecordPartRefused = ["attendance", "notes", "transcript", "recording"].every(
@@ -8593,514 +9059,172 @@ function RecordingPlayerBody({ fileId, host }) {
8593
9059
  const failure = thrown;
8594
9060
  setError({
8595
9061
  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");
9062
+ remedy: failure.remedy ?? PLAYBACK_RETRY_REMEDY
9063
+ });
9064
+ } finally {
9065
+ setLoading(false);
9066
+ }
9067
+ })();
9068
+ },
9069
+ children: loading ? "Loading the recording\u2026" : "Play the recording"
9070
+ }
9071
+ )
9072
+ ] });
8967
9073
  }
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" });
9074
+ function clockOf(iso) {
9075
+ const at = new Date(iso);
9076
+ if (Number.isNaN(at.getTime())) return "";
9077
+ return at.toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" });
8973
9078
  }
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
- };
9079
+ function MeetingList({ baseUrl }) {
9080
+ const host = useMeetHost();
9081
+ const list = useMeetings();
9082
+ if (host === null) return /* @__PURE__ */ jsx3(InertNotice, { what: "Your meetings" });
9083
+ if (list.failure !== null) {
9084
+ return /* @__PURE__ */ jsxs2(ErrorBox, { as: "div", className: "mx-meet__empty", error: list.failure, operation: "List your meetings", origin: "meet/components", children: [
9085
+ /* @__PURE__ */ jsx3("p", { className: "mx-meet__empty-title", children: "Your meetings could not be listed" }),
9086
+ /* @__PURE__ */ jsx3("p", { className: "mx-meet__empty-body", children: list.failure.message }),
9087
+ /* @__PURE__ */ jsx3("p", { className: "mx-meet__empty-body", children: list.failure.remedy })
9088
+ ] });
8993
9089
  }
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
- };
9090
+ if (list.loading) {
9091
+ 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
9092
  }
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;
9093
+ if (list.meetings.length === 0) {
9094
+ return /* @__PURE__ */ jsxs2("div", { className: "mx-meet__empty", role: "status", children: [
9095
+ /* @__PURE__ */ jsx3("p", { className: "mx-meet__empty-title", children: "No meetings yet" }),
9096
+ /* @__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." })
9097
+ ] });
9025
9098
  }
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
- );
9099
+ const origin = baseUrl ?? (typeof window === "undefined" ? "" : window.location.origin);
9100
+ return /* @__PURE__ */ jsx3("ul", { className: "mx-meet-list", children: list.meetings.map((meeting) => {
9101
+ const ended = meeting.endedAt !== null;
9102
+ return /* @__PURE__ */ jsxs2("li", { className: "mx-meet-list__row", children: [
9103
+ /* @__PURE__ */ jsxs2("div", { className: "mx-meet-list__main", children: [
9104
+ /* @__PURE__ */ jsxs2("span", { className: "mx-meet-list__title", children: [
9105
+ meeting.title,
9106
+ ended ? /* @__PURE__ */ jsx3("span", { className: "mx-meet__badge", children: "Ended" }) : null
9107
+ ] }),
9108
+ /* @__PURE__ */ jsx3("span", { className: "mx-meet-list__when", children: meetingWhen(meeting) })
9109
+ ] }),
9110
+ /* @__PURE__ */ jsx3("a", { className: "mx-meet-list__action", href: meetingLink(origin, meeting.slug), children: ended ? "Open the record" : "Join" })
9111
+ ] }, meeting.id);
9112
+ }) });
9032
9113
  }
9033
- function asRecipient(row) {
9034
- const sources = Array.isArray(row["sources"]) ? row["sources"] : [];
9114
+
9115
+ // src/core/invitations.ts
9116
+ var CALL_INVITE_KIND = "meet.call-invite";
9117
+ var CALL_INVITE_VERSION = 1;
9118
+ function callInviteAction(invite) {
9035
9119
  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
9120
+ kind: CALL_INVITE_KIND,
9121
+ version: CALL_INVITE_VERSION,
9122
+ payload: {
9123
+ invite_id: invite.id,
9124
+ room_name: invite.roomName,
9125
+ mode: invite.mode,
9126
+ caller_name: invite.callerName,
9127
+ expires_at: invite.expiresAt
9128
+ }
9042
9129
  };
9043
9130
  }
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"];
9131
+ function callInviteText(invite) {
9132
+ return invite.mode === "video" ? `${invite.callerName} started a video call` : `${invite.callerName} started an audio call`;
9133
+ }
9134
+ function createCallInviteHandler(options) {
9135
+ const now = options.now ?? (() => Date.now());
9055
9136
  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
9137
+ kind: CALL_INVITE_KIND,
9138
+ versions: [CALL_INVITE_VERSION],
9139
+ choices: (payload) => {
9140
+ if (Date.parse(payload.expires_at) < now()) return [];
9141
+ return [
9142
+ { id: "join", label: payload.mode === "video" ? "Join video" : "Join call", tone: "primary" },
9143
+ { id: "decline", label: "Decline", tone: "neutral" }
9144
+ ];
9145
+ },
9146
+ summarize: (payload) => payload.mode === "video" ? `${payload.caller_name} started a video call` : `${payload.caller_name} started an audio call`,
9147
+ execute: async (payload, context) => {
9148
+ const settledAt = new Date(now()).toISOString();
9149
+ const inviteId = asCallId(payload.invite_id);
9150
+ const pending = await options.repository.pendingInvites(asUserId(String(context.actorId)));
9151
+ const live = pending.find((invite) => invite.id === inviteId) ?? null;
9152
+ if (live === null) {
9153
+ return {
9154
+ kind: CALL_INVITE_KIND,
9155
+ messageId: context.messageId,
9156
+ actorId: context.actorId,
9157
+ outcome: "unavailable",
9158
+ label: "This call has ended",
9159
+ settledAt,
9160
+ 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."
9161
+ };
9162
+ }
9163
+ if (context.choice === "decline") {
9164
+ await options.calls.decline(inviteId);
9165
+ return {
9166
+ kind: CALL_INVITE_KIND,
9167
+ messageId: context.messageId,
9168
+ actorId: context.actorId,
9169
+ outcome: "declined",
9170
+ label: "Declined",
9171
+ settledAt
9172
+ };
9173
+ }
9174
+ const accepted = await options.calls.accept(inviteId, live.mode);
9175
+ await options.onJoin(accepted);
9176
+ return {
9177
+ kind: CALL_INVITE_KIND,
9178
+ messageId: context.messageId,
9179
+ actorId: context.actorId,
9180
+ outcome: "applied",
9181
+ label: "Joined",
9182
+ settledAt
9183
+ };
9184
+ }
9063
9185
  };
9064
9186
  }
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();
9187
+ var MEETING_INVITE_KIND = "meet.meeting-invite";
9188
+ var MEETING_INVITE_VERSION = 1;
9189
+ function meetingInviteAction(payload) {
9190
+ return { kind: MEETING_INVITE_KIND, version: MEETING_INVITE_VERSION, payload };
9191
+ }
9192
+ function createMeetingInviteHandler(options) {
9193
+ const now = options.now ?? (() => Date.now());
9081
9194
  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
- })) : []
9195
+ kind: MEETING_INVITE_KIND,
9196
+ versions: [MEETING_INVITE_VERSION],
9197
+ // A DURABLE LINK NEVER EXPIRES (R3) — the chip stays useful after the
9198
+ // meeting too, because that is when people go looking for the notes.
9199
+ choices: () => [{ id: "open", label: "Open meeting", tone: "primary" }],
9200
+ summarize: (payload) => payload.scheduled_for === null ? payload.title : `${payload.title} \u2014 ${payload.scheduled_for}`,
9201
+ execute: async (payload, context) => {
9202
+ if (payload.slug.length === 0) {
9203
+ throw new MeetError(
9204
+ "invalid-response",
9205
+ "meet.meeting-invite: the payload carries no slug",
9206
+ "A meeting invitation without a durable slug cannot be opened. Fix the sender; never synthesize a link on the receiving side."
9207
+ );
9208
+ }
9209
+ await options.onOpen(payload);
9210
+ return {
9211
+ kind: MEETING_INVITE_KIND,
9212
+ messageId: context.messageId,
9213
+ actorId: context.actorId,
9214
+ outcome: "applied",
9215
+ label: "Opened",
9216
+ settledAt: new Date(now()).toISOString()
9217
+ };
9218
+ }
9088
9219
  };
9089
9220
  }
9090
9221
 
9091
9222
  // 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."
9223
+ function refusal(response, operation) {
9224
+ return refusalFromResponse(
9225
+ response,
9226
+ operation,
9227
+ "The meeting itself is saved \u2014 only the telling failed. Try again; the call is idempotent."
9104
9228
  );
9105
9229
  }
9106
9230
  function count(body, key) {
@@ -9255,6 +9379,7 @@ export {
9255
9379
  attendanceCsv,
9256
9380
  attendanceReport,
9257
9381
  attendanceTotals,
9382
+ attendeeKey,
9258
9383
  callInviteAction,
9259
9384
  callInviteText,
9260
9385
  callStateLabel,
@@ -9358,6 +9483,7 @@ export {
9358
9483
  summarizeMeetingRecordRestrictions,
9359
9484
  transcriptDocument,
9360
9485
  unavailableFrom,
9486
+ uniqueAttendees,
9361
9487
  useAudioLevel,
9362
9488
  useCalls,
9363
9489
  useCanModerate,