@ai-matrx/meet 0.7.6 → 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.cjs CHANGED
@@ -32,6 +32,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
32
32
  // src/react.ts
33
33
  var react_exports = {};
34
34
  __export(react_exports, {
35
+ ATTENDANCE_CSV_HEADER: () => ATTENDANCE_CSV_HEADER,
35
36
  AiPanel: () => AiPanel,
36
37
  AttendanceNotice: () => AttendanceNotice,
37
38
  Avatar: () => Avatar,
@@ -135,6 +136,7 @@ __export(react_exports, {
135
136
  TRANSCRIPTION_TOPIC: () => TRANSCRIPTION_TOPIC,
136
137
  WifiLowIcon: () => WifiLowIcon,
137
138
  absenceSentence: () => absenceSentence,
139
+ activeWrapUpSummary: () => activeWrapUpSummary,
138
140
  announceMeeting: () => announceMeeting,
139
141
  applyRoomEvent: () => applyRoomEvent,
140
142
  asCallId: () => asCallId,
@@ -143,6 +145,10 @@ __export(react_exports, {
143
145
  asParticipantIdentity: () => asParticipantIdentity,
144
146
  asRoomName: () => asRoomName,
145
147
  asUserId: () => asUserId,
148
+ attendanceCsv: () => attendanceCsv,
149
+ attendanceReport: () => attendanceReport,
150
+ attendanceTotals: () => attendanceTotals,
151
+ attendeeKey: () => attendeeKey,
146
152
  callInviteAction: () => callInviteAction,
147
153
  callInviteText: () => callInviteText,
148
154
  callStateLabel: () => callStateLabel,
@@ -151,6 +157,7 @@ __export(react_exports, {
151
157
  canTransition: () => canTransition,
152
158
  captionSegment: () => captionSegment,
153
159
  changesPatch: () => changesPatch,
160
+ clockPosition: () => clockPosition,
154
161
  computeStage: () => computeStage,
155
162
  countLabel: () => countLabel,
156
163
  createCallCenter: () => createCallCenter,
@@ -191,11 +198,14 @@ __export(react_exports, {
191
198
  learnName: () => learnName,
192
199
  learnNames: () => learnNames,
193
200
  listenOnlyNotice: () => listenOnlyNotice,
201
+ loadFollowUpDraft: () => loadFollowUpDraft,
194
202
  loadGuestMeetingRecord: () => loadGuestMeetingRecord,
203
+ loadMeetingChat: () => loadMeetingChat,
195
204
  loadMeetingRecord: () => loadMeetingRecord,
196
205
  mapConnectionState: () => mapConnectionState,
197
206
  mapQuality: () => mapQuality,
198
207
  meetingInviteAction: () => meetingInviteAction,
208
+ meetingLength: () => meetingLength,
199
209
  meetingLink: () => meetingLink,
200
210
  meetingPath: () => meetingPath,
201
211
  meetingSlug: () => meetingSlug,
@@ -210,16 +220,19 @@ __export(react_exports, {
210
220
  noteTakerLabel: () => noteTakerLabel,
211
221
  noteTakerParticipant: () => noteTakerParticipant,
212
222
  noteTakerStatus: () => noteTakerStatus,
223
+ notesDocument: () => notesDocument,
213
224
  orderParticipants: () => orderParticipants,
214
225
  parseRoomToken: () => parseRoomToken,
215
226
  participantSummary: () => participantSummary,
216
227
  playableRecordingFileId: () => playableRecordingFileId,
217
228
  projectInvitee: () => projectInvitee,
229
+ projectLibraryRecording: () => projectLibraryRecording,
218
230
  projectNote: () => projectNote,
219
231
  projectOccurrence: () => projectOccurrence,
220
232
  projectOverride: () => projectOverride,
221
233
  projectParticipant: () => projectParticipant,
222
234
  projectRsvpInvitation: () => projectRsvpInvitation,
235
+ projectSearchHit: () => projectSearchHit,
223
236
  projectSegment: () => projectSegment,
224
237
  projectUpcoming: () => projectUpcoming,
225
238
  pushMeetingToGoogleCalendar: () => pushMeetingToGoogleCalendar,
@@ -227,12 +240,19 @@ __export(react_exports, {
227
240
  readableTranscript: () => readableTranscript,
228
241
  recordingLabel: () => recordingLabel,
229
242
  recordingNotice: () => recordingNotice,
243
+ recordingOffsetMs: () => recordingOffsetMs,
230
244
  roleOf: () => roleOf,
231
245
  roomTopic: () => roomTopic,
246
+ searchTranscript: () => searchTranscript,
247
+ sendFollowUp: () => sendFollowUp,
232
248
  setGuestRecordAccess: () => setGuestRecordAccess,
233
249
  settingsPatch: () => settingsPatch,
234
250
  snapshotParticipant: () => snapshotParticipant,
251
+ snippetRuns: () => snippetRuns,
252
+ summarizeMeetingRecordRestrictions: () => summarizeMeetingRecordRestrictions,
253
+ transcriptDocument: () => transcriptDocument,
235
254
  unavailableFrom: () => unavailableFrom,
255
+ uniqueAttendees: () => uniqueAttendees,
236
256
  useAudioLevel: () => useAudioLevel,
237
257
  useCalls: () => useCalls,
238
258
  useCanModerate: () => useCanModerate,
@@ -267,7 +287,8 @@ __export(react_exports, {
267
287
  useStage: () => useStage,
268
288
  useTrack: () => useTrack,
269
289
  userTopic: () => userTopic,
270
- wasRefused: () => wasRefused
290
+ wasRefused: () => wasRefused,
291
+ wrapUpStatus: () => wrapUpStatus
271
292
  });
272
293
  module.exports = __toCommonJS(react_exports);
273
294
 
@@ -469,6 +490,12 @@ var MEET_ROUTES = {
469
490
  meetingAnnounce: "/api/v1/meet/meetings/{meeting_id}/announce",
470
491
  /** Put a one-off meeting on the caller's own Google Calendar (gated by the server). */
471
492
  meetingGoogleCalendar: "/api/v1/meet/meetings/{meeting_id}/google-calendar",
493
+ /**
494
+ * Meet wave 3: the recap email after the meeting. `GET` = the draft (subject,
495
+ * body from the wrap-up, who it can go to); `POST` = send what the host
496
+ * reviewed (`dry_run` renders and queues nothing). Never sent automatically.
497
+ */
498
+ meetingFollowUp: "/api/v1/meet/meetings/{meeting_id}/follow-up",
472
499
  /**
473
500
  * Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
474
501
  * unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
@@ -598,10 +625,11 @@ function createTokenClient(options) {
598
625
  throw error;
599
626
  }
600
627
  if (response.status === 401 || response.status === 403) {
628
+ const said = await serverSentence(response);
601
629
  throw new MeetError(
602
630
  "forbidden",
603
- `meet.token: the server refused this join (${response.status})`,
604
- "Authorization is the server's (R5). The joiner is not a member of this meeting's organization, or the meeting is locked \u2014 never mint around it on the client."
631
+ said?.message ?? "You can't join this meeting.",
632
+ said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again."
605
633
  );
606
634
  }
607
635
  if (response.status === 404) {
@@ -612,12 +640,7 @@ function createTokenClient(options) {
612
640
  );
613
641
  }
614
642
  if (response.status === 410) {
615
- let reason = "This meeting was cancelled.";
616
- try {
617
- const body = await response.json();
618
- if (typeof body?.detail?.message === "string") reason = body.detail.message;
619
- } catch {
620
- }
643
+ const reason = (await serverSentence(response))?.message ?? "This meeting was cancelled.";
621
644
  throw new MeetError(
622
645
  "cancelled",
623
646
  reason,
@@ -625,10 +648,11 @@ function createTokenClient(options) {
625
648
  );
626
649
  }
627
650
  if (response.status === 423) {
651
+ const said = await serverSentence(response);
628
652
  throw new MeetError(
629
653
  "room-locked",
630
- "meet.token: the meeting is locked",
631
- "The host locked the room. Ask them to unlock or admit you; this is a normal state, not a failure."
654
+ said?.message ?? "The meeting is locked.",
655
+ said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again."
632
656
  );
633
657
  }
634
658
  if (response.status === 503) {
@@ -699,6 +723,19 @@ function placeholder(roomName) {
699
723
  lobby: false
700
724
  };
701
725
  }
726
+ async function serverSentence(response) {
727
+ try {
728
+ const body = await response.json();
729
+ const source = body !== null && typeof body.detail === "object" && body.detail !== null ? body.detail : body;
730
+ if (source === null) return null;
731
+ const message = typeof source.user_message === "string" && source.user_message.trim() !== "" ? source.user_message : typeof source.message === "string" && source.message.trim() !== "" ? source.message : null;
732
+ if (message === null) return null;
733
+ const remedy = typeof source.remedy === "string" && source.remedy.trim() !== "" ? source.remedy : void 0;
734
+ return { message, remedy };
735
+ } catch {
736
+ return null;
737
+ }
738
+ }
702
739
 
703
740
  // src/core/ai.ts
704
741
  var MEET_ANSWER_EVENTS = {
@@ -743,6 +780,29 @@ function readText(data) {
743
780
  }
744
781
  return "";
745
782
  }
783
+ var SERVER_FAILED_SENTENCE = "The server hit an unexpected problem and nothing was changed.";
784
+ function isRawServerText(value) {
785
+ const text3 = value.trim();
786
+ 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);
787
+ }
788
+ async function refusalFromResponse(response, operation, fallbackRemedy) {
789
+ let body = null;
790
+ try {
791
+ body = await response.json();
792
+ } catch {
793
+ body = null;
794
+ }
795
+ const said = readServerRefusal(body);
796
+ const code = response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport";
797
+ const error = new MeetError(
798
+ code,
799
+ said?.reason ?? SERVER_FAILED_SENTENCE,
800
+ said !== null && said.remedy.length > 0 ? said.remedy : fallbackRemedy
801
+ );
802
+ error.operation = operation;
803
+ error.status = response.status;
804
+ return error;
805
+ }
746
806
  function readServerRefusal(body) {
747
807
  if (typeof body === "string") {
748
808
  return body.trim().length === 0 ? null : { reason: body.trim(), remedy: "" };
@@ -761,7 +821,8 @@ function readServerRefusal(body) {
761
821
  return messages.length === 0 ? null : { reason: messages.join("; "), remedy: "" };
762
822
  }
763
823
  if (typeof detail === "object" && detail !== null) return readServerRefusal(detail);
764
- 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;
824
+ const humanOrNull = (value) => typeof value === "string" && value.trim().length > 0 && !isRawServerText(value) ? value.trim() : null;
825
+ 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);
765
826
  if (reason === null) return null;
766
827
  const remedy = typeof row["remedy"] === "string" ? row["remedy"] : "";
767
828
  if (remedy.length > 0) return { reason, remedy };
@@ -1142,7 +1203,7 @@ var DEFAULT_RING_TIMEOUT_MS = 45e3;
1142
1203
  function createCallCenter(options) {
1143
1204
  const { manager, repository, identity } = options;
1144
1205
  const timers = options.timers ?? {
1145
- setTimeout: (run, ms) => setTimeout(run, ms),
1206
+ setTimeout: (run, ms2) => setTimeout(run, ms2),
1146
1207
  clearTimeout: (handle) => clearTimeout(handle),
1147
1208
  now: () => Date.now()
1148
1209
  };
@@ -1815,6 +1876,7 @@ var EMPTY = {
1815
1876
  spotlight: null,
1816
1877
  moderationNotice: null,
1817
1878
  actionNotice: null,
1879
+ privateChatRequest: null,
1818
1880
  effects: DEFAULT_EFFECTS,
1819
1881
  transcript: [],
1820
1882
  notes: [],
@@ -2080,7 +2142,9 @@ function createMeetingSession(options) {
2080
2142
  return identity.userId;
2081
2143
  }
2082
2144
  function knockIdentity() {
2083
- return asParticipantIdentity(identity.userId ?? `guest:${identity.deviceId}`);
2145
+ return asParticipantIdentity(
2146
+ identity.userId !== null ? `user:${identity.userId}` : `guest:${identity.deviceId}`
2147
+ );
2084
2148
  }
2085
2149
  function requireMeeting(operation) {
2086
2150
  const meeting = store.snapshot().meeting;
@@ -2115,8 +2179,9 @@ function createMeetingSession(options) {
2115
2179
  verdict.admitted ? "admitted" : "denied",
2116
2180
  verdict.by === null ? null : verdict.by
2117
2181
  );
2118
- if (verdict.admitted && decided === knockIdentity()) {
2119
- options.onAdmitted?.();
2182
+ if (decided === knockIdentity()) {
2183
+ if (verdict.admitted) options.onAdmitted?.();
2184
+ else options.onDenied?.();
2120
2185
  }
2121
2186
  }
2122
2187
  },
@@ -2453,7 +2518,16 @@ var RPCS = {
2453
2518
  /** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */
2454
2519
  meetingOccurrences: "meet_meeting_occurrences",
2455
2520
  /** Every occurrence the reader can see between two instants — the Upcoming list. */
2456
- occurrencesBetween: "meet_occurrences_between"
2521
+ occurrencesBetween: "meet_occurrences_between",
2522
+ /*
2523
+ * ── MEET WAVE 3 — after the meeting. Both INVOKER (the reader's RLS) and both
2524
+ * scoped to the reader's OWN meetings (host, invitee, attended) — RLS is the
2525
+ * ceiling, never the view.
2526
+ */
2527
+ /** The recordings library. */
2528
+ myRecordings: "meet_my_recordings",
2529
+ /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
2530
+ search: "meet_search"
2457
2531
  };
2458
2532
  var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
2459
2533
  var MEETING_COLUMNS = "id,organization_id,room_name,slug,title,kind,host_user_id,scheduled_for,scheduled_duration_minutes,started_at,ended_at,locked,lobby_enabled,recording_policy,ai_enabled,metadata,time_zone,agenda,recurrence_rule,join_before_host,cancelled_at,cancellation_reason,deleted_at,calendar_sequence,version";
@@ -3158,12 +3232,117 @@ function createMeetRepository(options) {
3158
3232
  );
3159
3233
  },
3160
3234
  async recordings(meetingId) {
3161
- const response = await db.from(TABLES.recordings).select(RECORDING_COLUMNS).eq("meeting_id", meetingId).order("started_at", { ascending: false });
3235
+ const response = await db.from(TABLES.recordings).select(RECORDING_COLUMNS).eq("meeting_id", meetingId).is("deleted_at", null).order("started_at", { ascending: false });
3162
3236
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.recordings");
3163
3237
  return (response.data ?? []).map(projectRecording);
3238
+ },
3239
+ async myRecordings({ includeArchived = false, limit = 200 } = {}) {
3240
+ const rows = await guard(
3241
+ "meet.myRecordings",
3242
+ () => db.rpc(RPCS.myRecordings, { p_include_archived: includeArchived, p_limit: limit })
3243
+ );
3244
+ return rows.map(projectLibraryRecording);
3245
+ },
3246
+ async searchMeetings(query, limit = 60) {
3247
+ if (query.trim().length === 0) return [];
3248
+ const rows = await guard(
3249
+ "meet.searchMeetings",
3250
+ () => db.rpc(RPCS.search, { p_query: query, p_limit: limit })
3251
+ );
3252
+ return rows.map(projectSearchHit);
3253
+ },
3254
+ async renameRecording(recordingId, title) {
3255
+ const clean = title === null ? null : title.trim().slice(0, 200) || null;
3256
+ const response = await db.from(TABLES.recordings).update({ title: clean }).eq("id", recordingId).select("id");
3257
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.renameRecording");
3258
+ if ((response.data ?? []).length === 0) {
3259
+ throw normalizeMeetError(
3260
+ { message: "permission denied: only the meeting's host or an editor can rename it", code: "42501" },
3261
+ "meet.renameRecording"
3262
+ );
3263
+ }
3264
+ },
3265
+ async setRecordingArchived(recordingId, archived) {
3266
+ const response = await db.from(TABLES.recordings).update({ deleted_at: archived ? (/* @__PURE__ */ new Date()).toISOString() : null }).eq("id", recordingId).select("id");
3267
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.setRecordingArchived");
3268
+ if ((response.data ?? []).length === 0) {
3269
+ throw normalizeMeetError(
3270
+ { message: "permission denied: only the meeting's host or an editor can archive it", code: "42501" },
3271
+ "meet.setRecordingArchived"
3272
+ );
3273
+ }
3164
3274
  }
3165
3275
  };
3166
3276
  }
3277
+ var LIBRARY_STATES = [
3278
+ "idle",
3279
+ "starting",
3280
+ "recording",
3281
+ "stopping",
3282
+ "processing",
3283
+ "available",
3284
+ "expired",
3285
+ "failed"
3286
+ ];
3287
+ function projectLibraryRecording(row) {
3288
+ const operation = "meet.projectLibraryRecording";
3289
+ const state = text(row, "state");
3290
+ const role = text(row, "my_role");
3291
+ return {
3292
+ id: requireText(row, "recording_id", operation),
3293
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
3294
+ meetingTitle: text(row, "meeting_title") ?? "Meeting",
3295
+ meetingSlug: requireText(row, "meeting_slug", operation),
3296
+ role: role === "host" || role === "cohost" || role === "invitee" ? role : "attendee",
3297
+ state: LIBRARY_STATES.includes(state) ? state : "failed",
3298
+ title: text(row, "recording_title"),
3299
+ fileId: text(row, "file_id"),
3300
+ startedAt: text(row, "started_at"),
3301
+ endedAt: text(row, "ended_at"),
3302
+ durationMs: num(row, "duration_ms"),
3303
+ sizeBytes: num(row, "size_bytes"),
3304
+ failureReason: text(row, "failure_reason"),
3305
+ archivedAt: text(row, "deleted_at"),
3306
+ organizationId: asOrganizationId(requireText(row, "organization_id", operation))
3307
+ };
3308
+ }
3309
+ var SEARCH_SOURCES = [
3310
+ "title",
3311
+ "summary",
3312
+ "decision",
3313
+ "action_item",
3314
+ "note",
3315
+ "transcript"
3316
+ ];
3317
+ function projectSearchHit(row) {
3318
+ const operation = "meet.projectSearchHit";
3319
+ const source = text(row, "source");
3320
+ return {
3321
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
3322
+ meetingTitle: text(row, "meeting_title") ?? "Meeting",
3323
+ meetingSlug: requireText(row, "meeting_slug", operation),
3324
+ meetingAt: text(row, "meeting_at"),
3325
+ source: SEARCH_SOURCES.includes(source) ? source : "note",
3326
+ refId: requireText(row, "ref_id", operation),
3327
+ speaker: text(row, "speaker"),
3328
+ snippet: text(row, "snippet") ?? "",
3329
+ at: text(row, "said_at"),
3330
+ rank: num(row, "rank") ?? 0
3331
+ };
3332
+ }
3333
+ function snippetRuns(snippet) {
3334
+ const runs = [];
3335
+ const pattern = /⟦([^⟧]*)⟧/g;
3336
+ let last = 0;
3337
+ for (const match of snippet.matchAll(pattern)) {
3338
+ const start = match.index ?? 0;
3339
+ if (start > last) runs.push({ text: snippet.slice(last, start), hit: false });
3340
+ runs.push({ text: match[1] ?? "", hit: true });
3341
+ last = start + match[0].length;
3342
+ }
3343
+ if (last < snippet.length) runs.push({ text: snippet.slice(last), hit: false });
3344
+ return runs;
3345
+ }
3167
3346
 
3168
3347
  // src/core/session-identity.ts
3169
3348
  var RESOLVING = Object.freeze({
@@ -3887,6 +4066,10 @@ function createRoomEngine(options) {
3887
4066
  }
3888
4067
  },
3889
4068
  disconnected(reason) {
4069
+ if (current !== null) {
4070
+ tokens.invalidate(current.roomName);
4071
+ current = null;
4072
+ }
3890
4073
  store.patch({ phase: "left" });
3891
4074
  options.onLeft?.(reason);
3892
4075
  }
@@ -4189,19 +4372,8 @@ function createInRoomClient(options) {
4189
4372
  const token = options.resolveRoomToken();
4190
4373
  return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
4191
4374
  }
4192
- async function refusal2(response, operation, remedy) {
4193
- let body = null;
4194
- try {
4195
- body = await response.json();
4196
- } catch {
4197
- body = null;
4198
- }
4199
- const said = readServerRefusal(body);
4200
- return new MeetError(
4201
- response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
4202
- said?.reason ?? `${operation}: the server answered ${response.status}`,
4203
- said?.remedy !== void 0 && said.remedy.length > 0 ? said.remedy : remedy
4204
- );
4375
+ function refusal2(response, operation, remedy) {
4376
+ return refusalFromResponse(response, operation, remedy);
4205
4377
  }
4206
4378
  return {
4207
4379
  async moderate(request) {
@@ -4794,6 +4966,11 @@ function MeetRuntime(props) {
4794
4966
  // publish nor subscribe, so the host saying yes has to mint a new one.
4795
4967
  onAdmitted: () => {
4796
4968
  void engine.rejoin();
4969
+ },
4970
+ // A "no" is ALSO a mint: the server refuses it with its own sentence, which
4971
+ // turns the waiting room into the pre-join screen showing that sentence.
4972
+ onDenied: () => {
4973
+ void engine.rejoin().catch(() => void 0);
4797
4974
  }
4798
4975
  });
4799
4976
  const resolveRoomToken = () => {
@@ -6156,7 +6333,408 @@ function useGuestRecordAccess(meeting) {
6156
6333
  // src/react/components.tsx
6157
6334
  var import_react4 = require("react");
6158
6335
  var import_design_system = require("@ai-matrx/design-system");
6336
+ var import_format3 = require("@ai-matrx/kit/format");
6337
+
6338
+ // src/core/after.ts
6159
6339
  var import_format2 = require("@ai-matrx/kit/format");
6340
+ function kindOf(person) {
6341
+ if (person.isAgent) return "assistant";
6342
+ if (person.role === "host") return "host";
6343
+ if (person.role === "cohost") return "cohost";
6344
+ if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
6345
+ return "member";
6346
+ }
6347
+ function ms(iso) {
6348
+ if (iso === null) return null;
6349
+ const at = new Date(iso).getTime();
6350
+ return Number.isNaN(at) ? null : at;
6351
+ }
6352
+ function attendeeKey(person, names = {}) {
6353
+ if (person.userId !== null) return `user:${person.userId}`;
6354
+ if (person.isAgent) return `agent:${person.identity}`;
6355
+ const name = displayNameFor(names, person.identity, person.displayName).trim().toLowerCase();
6356
+ return name.length > 0 ? `guest-name:${name}` : `identity:${person.identity}`;
6357
+ }
6358
+ function uniqueAttendees(attendees, names = {}) {
6359
+ const byKey = /* @__PURE__ */ new Map();
6360
+ for (const person of attendees) {
6361
+ const key = attendeeKey(person, names);
6362
+ const kept = byKey.get(key);
6363
+ if (kept === void 0) {
6364
+ byKey.set(key, person);
6365
+ continue;
6366
+ }
6367
+ const keptJoin = ms(kept.joinedAt);
6368
+ const join = ms(person.joinedAt);
6369
+ if (join !== null && (keptJoin === null || join < keptJoin)) byKey.set(key, person);
6370
+ }
6371
+ return [...byKey.values()];
6372
+ }
6373
+ var KIND_RANK = {
6374
+ host: 0,
6375
+ cohost: 1,
6376
+ member: 2,
6377
+ "signed-in guest": 3,
6378
+ guest: 4,
6379
+ assistant: 5
6380
+ };
6381
+ function coveredMs(intervals) {
6382
+ const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
6383
+ let total = 0;
6384
+ let from = -Infinity;
6385
+ let to = -Infinity;
6386
+ for (const [start, end] of sorted) {
6387
+ if (start > to) {
6388
+ if (to > from) total += to - from;
6389
+ from = start;
6390
+ to = end;
6391
+ } else if (end > to) {
6392
+ to = end;
6393
+ }
6394
+ }
6395
+ if (to > from) total += to - from;
6396
+ return total;
6397
+ }
6398
+ function attendanceReport(meeting, attendees, names = {}) {
6399
+ const end = ms(meeting.endedAt);
6400
+ const perJoin = attendees.map((person) => {
6401
+ const joined = ms(person.joinedAt);
6402
+ const left = ms(person.leftAt);
6403
+ const until = left ?? end;
6404
+ const duration = joined !== null && until !== null && until >= joined ? until - joined : null;
6405
+ return {
6406
+ id: person.id,
6407
+ identity: person.identity,
6408
+ name: displayNameFor(names, person.identity, person.displayName),
6409
+ userId: person.userId,
6410
+ kind: kindOf(person),
6411
+ joinedAt: person.joinedAt,
6412
+ leftAt: person.leftAt,
6413
+ durationMs: duration,
6414
+ durationToEnd: duration !== null && left === null,
6415
+ admission: person.admissionState,
6416
+ joins: 1
6417
+ };
6418
+ });
6419
+ const groups = /* @__PURE__ */ new Map();
6420
+ attendees.forEach((person, index) => {
6421
+ const key = attendeeKey(person, names);
6422
+ const group = groups.get(key) ?? { rows: [], people: [] };
6423
+ group.rows.push(perJoin[index]);
6424
+ group.people.push(person);
6425
+ groups.set(key, group);
6426
+ });
6427
+ const rows = [...groups.values()].map(({ rows: joins }) => {
6428
+ if (joins.length === 1) return joins[0];
6429
+ const came = joins.filter((row) => row.joinedAt !== null);
6430
+ const first = [...came].sort((a, b) => (ms(a.joinedAt) ?? 0) - (ms(b.joinedAt) ?? 0))[0] ?? joins[0];
6431
+ const kind = joins.reduce(
6432
+ (best, row) => KIND_RANK[row.kind] < KIND_RANK[best] ? row.kind : best,
6433
+ first.kind
6434
+ );
6435
+ const open = came.some((row) => row.leftAt === null);
6436
+ const lastLeave = came.reduce((latest, row) => {
6437
+ const left = ms(row.leftAt);
6438
+ return left !== null && (latest === null || left > latest) ? left : latest;
6439
+ }, null);
6440
+ const intervals = came.flatMap((row) => {
6441
+ const joined = ms(row.joinedAt);
6442
+ return joined !== null && row.durationMs !== null ? [[joined, joined + row.durationMs]] : [];
6443
+ });
6444
+ return {
6445
+ ...first,
6446
+ kind,
6447
+ leftAt: open ? null : lastLeave === null ? null : new Date(lastLeave).toISOString(),
6448
+ durationMs: intervals.length === 0 ? null : coveredMs(intervals),
6449
+ durationToEnd: came.some((row) => row.durationToEnd),
6450
+ joins: came.length === 0 ? joins.length : came.length
6451
+ };
6452
+ });
6453
+ const rank = KIND_RANK;
6454
+ return rows.sort((a, b) => {
6455
+ if (rank[a.kind] !== rank[b.kind]) return rank[a.kind] - rank[b.kind];
6456
+ return (ms(a.joinedAt) ?? Number.MAX_SAFE_INTEGER) - (ms(b.joinedAt) ?? Number.MAX_SAFE_INTEGER);
6457
+ });
6458
+ }
6459
+ function attendanceTotals(rows) {
6460
+ const humans = rows.filter((row) => row.kind !== "assistant");
6461
+ const attended = humans.filter((row) => row.joinedAt !== null);
6462
+ const timed = attended.filter((row) => row.durationMs !== null);
6463
+ return {
6464
+ people: attended.length,
6465
+ guests: attended.filter((row) => row.kind === "guest" || row.kind === "signed-in guest").length,
6466
+ neverAdmitted: humans.filter((row) => row.joinedAt === null).length,
6467
+ averageMs: timed.length === 0 ? null : Math.round(timed.reduce((sum, row) => sum + (row.durationMs ?? 0), 0) / timed.length)
6468
+ };
6469
+ }
6470
+ function csvCell(value) {
6471
+ if (value === null) return "";
6472
+ const text3 = String(value);
6473
+ return /[",\n\r]/.test(text3) ? `"${text3.replace(/"/g, '""')}"` : text3;
6474
+ }
6475
+ var ATTENDANCE_CSV_HEADER = [
6476
+ "Meeting",
6477
+ "Meeting started",
6478
+ "Name",
6479
+ "Attended as",
6480
+ "Joined",
6481
+ "Left",
6482
+ "Minutes in the meeting",
6483
+ "Admission"
6484
+ ];
6485
+ function attendanceCsv(reports) {
6486
+ const lines = [ATTENDANCE_CSV_HEADER.join(",")];
6487
+ for (const { meeting, rows } of reports) {
6488
+ for (const row of rows) {
6489
+ lines.push(
6490
+ [
6491
+ meeting.title,
6492
+ meeting.startedAt ?? meeting.scheduledFor,
6493
+ row.name,
6494
+ row.kind,
6495
+ row.joinedAt,
6496
+ row.leftAt,
6497
+ row.durationMs === null ? null : Math.round(row.durationMs / 6e3) / 10,
6498
+ row.admission
6499
+ ].map(csvCell).join(",")
6500
+ );
6501
+ }
6502
+ }
6503
+ return `${lines.join("\r\n")}\r
6504
+ `;
6505
+ }
6506
+ function recordingOffsetMs(at, recording) {
6507
+ if (recording === null || recording.state !== "available") return null;
6508
+ const start = ms(recording.startedAt);
6509
+ const moment = ms(at);
6510
+ if (start === null || moment === null || moment < start) return null;
6511
+ const end = ms(recording.endedAt);
6512
+ if (end !== null && moment > end) return null;
6513
+ return moment - start;
6514
+ }
6515
+ function clockPosition(offsetMs) {
6516
+ const total = Math.max(0, Math.floor(offsetMs / 1e3));
6517
+ const h = Math.floor(total / 3600);
6518
+ const m = Math.floor(total % 3600 / 60);
6519
+ const s = total % 60;
6520
+ const ss = String(s).padStart(2, "0");
6521
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
6522
+ }
6523
+ function searchTranscript(segments, query, names = {}) {
6524
+ const needle = fold(query.trim());
6525
+ if (needle.length === 0) return [];
6526
+ const out = [];
6527
+ for (const segment of segments) {
6528
+ const hay = fold(segment.text);
6529
+ const ranges = [];
6530
+ let from = 0;
6531
+ for (; ; ) {
6532
+ const at = hay.indexOf(needle, from);
6533
+ if (at < 0) break;
6534
+ ranges.push([at, at + needle.length]);
6535
+ from = at + needle.length;
6536
+ }
6537
+ const speaker = fold(displayNameFor(names, segment.identity, segment.speaker));
6538
+ if (ranges.length > 0 || speaker.includes(needle)) {
6539
+ out.push({ segmentId: segment.id, ranges });
6540
+ }
6541
+ }
6542
+ return out;
6543
+ }
6544
+ function fold(value) {
6545
+ return value.normalize("NFD").replace(/[̀-ͯ]/g, (mark) => mark.length === 1 ? "" : mark).toLowerCase();
6546
+ }
6547
+ function clockOfDay(iso, options) {
6548
+ const at = new Date(iso);
6549
+ if (Number.isNaN(at.getTime())) return "";
6550
+ return at.toLocaleTimeString(options.locale, {
6551
+ hour: "numeric",
6552
+ minute: "2-digit",
6553
+ second: "2-digit",
6554
+ ...options.timeZone !== void 0 ? { timeZone: options.timeZone } : {}
6555
+ });
6556
+ }
6557
+ function vttStamp(offsetMs) {
6558
+ const total = Math.max(0, Math.round(offsetMs));
6559
+ const h = Math.floor(total / 36e5);
6560
+ const m = Math.floor(total % 36e5 / 6e4);
6561
+ const s = Math.floor(total % 6e4 / 1e3);
6562
+ const milli = total % 1e3;
6563
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${String(milli).padStart(3, "0")}`;
6564
+ }
6565
+ function transcriptDocument(bundle, format, options = {}) {
6566
+ const names = options.names ?? bundle.names;
6567
+ const lines = bundle.transcript.filter((line) => line.text.trim().length > 0);
6568
+ const speaker = (line) => displayNameFor(names, line.identity, line.speaker);
6569
+ if (format === "vtt") {
6570
+ const zeroIso = bundle.recording?.state === "available" && bundle.recording.startedAt !== null ? bundle.recording.startedAt : bundle.meeting.startedAt ?? lines[0]?.startedAt ?? null;
6571
+ const zero = ms(zeroIso) ?? 0;
6572
+ const cues = ["WEBVTT", ""];
6573
+ lines.forEach((line, index) => {
6574
+ const start = (ms(line.startedAt) ?? zero) - zero;
6575
+ if (start < 0) return;
6576
+ const next = lines[index + 1];
6577
+ const nextStart = next !== void 0 ? (ms(next.startedAt) ?? zero) - zero : start + 4e3;
6578
+ const end = Math.max(start + 500, Math.min(nextStart, start + 1e4));
6579
+ cues.push(`${vttStamp(start)} --> ${vttStamp(end)}`);
6580
+ cues.push(`<v ${speaker(line).replace(/[<>]/g, "")}>${line.text.replace(/-->/g, "->")}`);
6581
+ cues.push("");
6582
+ });
6583
+ return cues.join("\n");
6584
+ }
6585
+ const out = [];
6586
+ const title = bundle.meeting.title;
6587
+ out.push(format === "md" ? `# ${title} \u2014 transcript` : `${title} \u2014 transcript`);
6588
+ out.push("");
6589
+ let last = null;
6590
+ for (const line of lines) {
6591
+ if (line.identity !== last) {
6592
+ if (last !== null) out.push("");
6593
+ const who = speaker(line);
6594
+ const clock = clockOfDay(line.startedAt, options);
6595
+ out.push(format === "md" ? `**${who}** \xB7 ${clock}` : `${who} (${clock})`);
6596
+ last = line.identity;
6597
+ }
6598
+ out.push(line.text.trim());
6599
+ }
6600
+ if (lines.length === 0) out.push("No transcript was captured for this meeting.");
6601
+ return `${out.join("\n")}
6602
+ `;
6603
+ }
6604
+ function bullet(note) {
6605
+ const owner = note.assigneeDisplayName !== null ? ` \u2014 ${note.assigneeDisplayName}` : "";
6606
+ return `- ${note.text.trim()}${owner}`;
6607
+ }
6608
+ function notesDocument(bundle, options = {}) {
6609
+ const out = [`# ${bundle.meeting.title}`, ""];
6610
+ if (options.when) out.push(`_${options.when}_`, "");
6611
+ const people = uniqueAttendees(
6612
+ bundle.attendees.filter((person) => !person.isAgent && person.joinedAt !== null),
6613
+ bundle.names
6614
+ ).map((person) => displayNameFor(bundle.names, person.identity, person.displayName));
6615
+ if (people.length > 0) out.push(`**Attended:** ${people.join(", ")}`, "");
6616
+ out.push("## Summary", "", bundle.summary?.text.trim() || "No summary was written.", "");
6617
+ out.push("## Decisions", "");
6618
+ out.push(...bundle.decisions.length > 0 ? bundle.decisions.map(bullet) : ["None recorded."], "");
6619
+ out.push("## Action items", "");
6620
+ out.push(
6621
+ ...bundle.actionItems.length > 0 ? bundle.actionItems.map(bullet) : ["None recorded."],
6622
+ ""
6623
+ );
6624
+ if (options.link) out.push(`[Open the meeting record](${options.link})`, "");
6625
+ return out.join("\n");
6626
+ }
6627
+ function meetingLength(meeting) {
6628
+ const start = ms(meeting.startedAt);
6629
+ const end = ms(meeting.endedAt);
6630
+ if (start === null || end === null || end < start) return null;
6631
+ return (0, import_format2.formatDurationMs)(end - start, { style: "long" });
6632
+ }
6633
+ async function loadMeetingChat(api, meetingId, roomToken = null) {
6634
+ const operation = "meet.loadMeetingChat";
6635
+ let response;
6636
+ try {
6637
+ response = await api.request({
6638
+ path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(meetingId)}`,
6639
+ method: "GET",
6640
+ operation,
6641
+ requireAuth: roomToken === null,
6642
+ ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
6643
+ });
6644
+ } catch (error) {
6645
+ return {
6646
+ messages: [],
6647
+ refusal: {
6648
+ message: `The chat could not be read (${error.message}).`,
6649
+ remedy: error.remedy ?? "Reload the page."
6650
+ }
6651
+ };
6652
+ }
6653
+ if (!response.ok) {
6654
+ let body = null;
6655
+ try {
6656
+ body = await response.json();
6657
+ } catch {
6658
+ body = null;
6659
+ }
6660
+ const said = readServerRefusal(body);
6661
+ return {
6662
+ messages: [],
6663
+ refusal: {
6664
+ message: said?.reason ?? `The chat could not be read (the server answered ${response.status}).`,
6665
+ remedy: said?.remedy || "Reload the page."
6666
+ }
6667
+ };
6668
+ }
6669
+ const payload = await response.json();
6670
+ const rows = Array.isArray(payload.messages) ? payload.messages : [];
6671
+ return {
6672
+ messages: rows.map(
6673
+ (row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
6674
+ ).filter((message) => message !== null),
6675
+ refusal: null
6676
+ };
6677
+ }
6678
+ function refusalOf(response, operation) {
6679
+ return refusalFromResponse(response, operation, "Nothing was sent. Try again.");
6680
+ }
6681
+ function asRecipient(row) {
6682
+ const sources = Array.isArray(row["sources"]) ? row["sources"] : [];
6683
+ return {
6684
+ key: String(row["key"] ?? ""),
6685
+ name: String(row["name"] ?? ""),
6686
+ email: typeof row["email"] === "string" ? row["email"] : null,
6687
+ userId: typeof row["user_id"] === "string" ? row["user_id"] : null,
6688
+ sources: sources.filter((s) => s === "invitee" || s === "attendee"),
6689
+ reachable: row["reachable"] !== false
6690
+ };
6691
+ }
6692
+ async function loadFollowUpDraft(api, meetingId) {
6693
+ const operation = "meet.loadFollowUpDraft";
6694
+ const response = await api.request({
6695
+ path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
6696
+ method: "GET",
6697
+ requireAuth: true,
6698
+ operation
6699
+ });
6700
+ if (!response.ok) throw await refusalOf(response, operation);
6701
+ const body = await response.json();
6702
+ const last = body["last_sent"];
6703
+ return {
6704
+ subject: String(body["subject"] ?? ""),
6705
+ body: String(body["body"] ?? ""),
6706
+ recipients: (Array.isArray(body["recipients"]) ? body["recipients"] : []).map(
6707
+ (row) => asRecipient(row)
6708
+ ),
6709
+ wrapUpWritten: body["wrap_up_written"] === true,
6710
+ lastSent: last && typeof last["at"] === "string" ? { at: last["at"], count: typeof last["count"] === "number" ? last["count"] : 0 } : null
6711
+ };
6712
+ }
6713
+ async function sendFollowUp(api, meetingId, input) {
6714
+ const operation = "meet.sendFollowUp";
6715
+ const response = await api.request({
6716
+ path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
6717
+ method: "POST",
6718
+ requireAuth: true,
6719
+ operation,
6720
+ body: {
6721
+ subject: input.subject,
6722
+ body: input.body,
6723
+ recipient_keys: [...input.recipientKeys],
6724
+ dry_run: input.dryRun === true
6725
+ }
6726
+ });
6727
+ if (!response.ok) throw await refusalOf(response, operation);
6728
+ const body = await response.json();
6729
+ return {
6730
+ dryRun: body["dry_run"] === true,
6731
+ queued: Array.isArray(body["queued"]) ? body["queued"] : [],
6732
+ skipped: Array.isArray(body["skipped"]) ? body["skipped"].map((row) => ({
6733
+ key: String(row["key"] ?? ""),
6734
+ reason: String(row["reason"] ?? "")
6735
+ })) : []
6736
+ };
6737
+ }
6160
6738
 
6161
6739
  // src/core/format.ts
6162
6740
  function participantSummary(participants) {
@@ -6396,7 +6974,7 @@ function Avatar({
6396
6974
  url,
6397
6975
  size = 40
6398
6976
  }) {
6399
- const tint = (0, import_format2.avatarPaletteIndex)(name);
6977
+ const tint = (0, import_format3.avatarPaletteIndex)(name);
6400
6978
  if (url != null && url.length > 0) {
6401
6979
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6402
6980
  "img",
@@ -6421,7 +6999,7 @@ function Avatar({
6421
6999
  background: `var(--mx-meet-avatar-${tint})`
6422
7000
  },
6423
7001
  "aria-hidden": "true",
6424
- children: (0, import_format2.getInitials)(name)
7002
+ children: (0, import_format3.getInitials)(name)
6425
7003
  }
6426
7004
  );
6427
7005
  }
@@ -6606,6 +7184,15 @@ function ControlBar({ onLeave }) {
6606
7184
  const micUnavailable = useLocalMedia().microphone.unavailable;
6607
7185
  const unread = useUnreadChat(panel === "chat");
6608
7186
  const toggle = (next) => setPanel(panel === next ? "none" : next);
7187
+ const privateChatAt = snapshot?.privateChatRequest?.at ?? null;
7188
+ const meetStore = useMeetHost()?.store ?? null;
7189
+ (0, import_react4.useEffect)(() => {
7190
+ const request = meetStore?.snapshot().privateChatRequest ?? null;
7191
+ if (request === null) return;
7192
+ setChatTo(request.to);
7193
+ setPanel("chat");
7194
+ meetStore?.patch({ privateChatRequest: null });
7195
+ }, [privateChatAt, meetStore]);
6609
7196
  useMeetShortcuts({
6610
7197
  mic: () => void room.toggleMicrophone(),
6611
7198
  camera: () => void room.toggleCamera(),
@@ -6845,7 +7432,12 @@ function Popover({
6845
7432
  placement = "up"
6846
7433
  }) {
6847
7434
  const ref = (0, import_react4.useRef)(null);
6848
- const [style, setStyle] = (0, import_react4.useState)({ visibility: "hidden" });
7435
+ const [style, setStyle] = (0, import_react4.useState)({
7436
+ position: "fixed",
7437
+ top: 0,
7438
+ left: 0,
7439
+ visibility: "hidden"
7440
+ });
6849
7441
  (0, import_react4.useLayoutEffect)(() => {
6850
7442
  const element = ref.current;
6851
7443
  const anchor = element?.parentElement ?? null;
@@ -6861,14 +7453,35 @@ function Popover({
6861
7453
  );
6862
7454
  const fitsBelow = rect.bottom + gutter + height <= window.innerHeight;
6863
7455
  const fitsAbove = rect.top - gutter - height >= 0;
7456
+ if (!fitsBelow && !fitsAbove) {
7457
+ const top = Math.max(gutter, window.innerHeight - height - gutter);
7458
+ setStyle({
7459
+ position: "fixed",
7460
+ left,
7461
+ top,
7462
+ maxHeight: window.innerHeight - top - gutter,
7463
+ overflowY: "auto"
7464
+ });
7465
+ return;
7466
+ }
6864
7467
  const down = placement === "down" ? fitsBelow || !fitsAbove : !fitsAbove && fitsBelow;
6865
7468
  setStyle(
6866
7469
  down ? { position: "fixed", left, top: rect.bottom + gutter } : { position: "fixed", left, bottom: window.innerHeight - rect.top + gutter }
6867
7470
  );
6868
7471
  };
6869
7472
  place();
7473
+ const frame = requestAnimationFrame(place);
7474
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => place());
7475
+ observer?.observe(document.body);
7476
+ observer?.observe(element);
6870
7477
  window.addEventListener("resize", place);
6871
- return () => window.removeEventListener("resize", place);
7478
+ window.addEventListener("scroll", place, true);
7479
+ return () => {
7480
+ cancelAnimationFrame(frame);
7481
+ observer?.disconnect();
7482
+ window.removeEventListener("resize", place);
7483
+ window.removeEventListener("scroll", place, true);
7484
+ };
6872
7485
  }, [placement]);
6873
7486
  (0, import_react4.useEffect)(() => {
6874
7487
  const onKey = (event) => {
@@ -6919,22 +7532,27 @@ function MeetingOverlays() {
6919
7532
  const room = useRoom();
6920
7533
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
6921
7534
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ReactionOverlay, { reactions }),
6922
- notice !== null && notice.kind !== "removed" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__notice-bar", role: "alert", children: [
6923
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: notice.text }),
6924
- notice.kind === "ask-unmute" && room.local?.micEnabled !== true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6925
- "button",
6926
- {
6927
- type: "button",
6928
- className: "mx-meet__primary mx-meet__primary--small",
6929
- onClick: () => {
6930
- dismiss();
6931
- void room.toggleMicrophone();
6932
- },
6933
- children: "Unmute"
6934
- }
6935
- ) : null,
6936
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismiss, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 14, height: 14 }) })
6937
- ] }) : null
7535
+ notice !== null && notice.kind !== "removed" ? (
7536
+ // A host's request ("the host is asking you to unmute"), not an error:
7537
+ // an assertive status, so it is announced at once without being filed
7538
+ // as a failure (design-system error-box census).
7539
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__notice-bar", role: "status", "aria-live": "assertive", children: [
7540
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: notice.text }),
7541
+ notice.kind === "ask-unmute" && room.local?.micEnabled !== true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7542
+ "button",
7543
+ {
7544
+ type: "button",
7545
+ className: "mx-meet__primary mx-meet__primary--small",
7546
+ onClick: () => {
7547
+ dismiss();
7548
+ void room.toggleMicrophone();
7549
+ },
7550
+ children: "Unmute"
7551
+ }
7552
+ ) : null,
7553
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismiss, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 14, height: 14 }) })
7554
+ ] })
7555
+ ) : null
6938
7556
  ] });
6939
7557
  }
6940
7558
  function ShortcutsSheet({ onClose }) {
@@ -7240,11 +7858,15 @@ function PersonMenu({
7240
7858
  const pinned = snapshot?.pinned === participant.identity;
7241
7859
  const spotlit = snapshot?.spotlight === participant.identity;
7242
7860
  const hasAccount = participant.userId !== null;
7861
+ const [refused, setRefused] = (0, import_react4.useState)(false);
7243
7862
  const act = (request) => {
7863
+ setRefused(false);
7244
7864
  void controls.run(request).then((result) => {
7245
7865
  if (result !== null) onClose();
7866
+ else setRefused(true);
7246
7867
  });
7247
7868
  };
7869
+ const refusal2 = refused && controls.notice?.failed === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_design_system.ErrorBox, { as: "p", className: "mx-meet__error", message: controls.notice.text, operation: "Change this participant", origin: "meet/components", children: controls.notice.text }) : null;
7248
7870
  if (mode === "rename" || mode === "report") {
7249
7871
  const renaming = mode === "rename";
7250
7872
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: renaming ? "Rename" : "Report", onClose, placement, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
@@ -7282,7 +7904,7 @@ function PersonMenu({
7282
7904
  }
7283
7905
  )
7284
7906
  ] }),
7285
- controls.notice?.failed === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__error", children: controls.notice.text }) : null,
7907
+ refusal2,
7286
7908
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu-actions", children: [
7287
7909
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Back" }),
7288
7910
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: "mx-meet__send", disabled: text3.trim().length === 0 || controls.busy, children: renaming ? "Rename" : "Report" })
@@ -7295,6 +7917,7 @@ function PersonMenu({
7295
7917
  const removing = mode === "remove";
7296
7918
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: removing ? "Remove" : "Make host", onClose, placement, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu-form", children: [
7297
7919
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__confirm-text", children: removing ? `Remove ${participant.displayName}? They leave immediately and cannot rejoin this meeting.` : `Make ${participant.displayName} the host? They get every host control and own this meeting. You stay on as a co-host.` }),
7920
+ refusal2,
7298
7921
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu-actions", children: [
7299
7922
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Cancel" }),
7300
7923
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -7325,11 +7948,15 @@ function PersonMenu({
7325
7948
  ]
7326
7949
  }
7327
7950
  ),
7328
- !self && !participant.isAgent && onMessage !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7951
+ !self && !participant.isAgent && (onMessage !== void 0 || host !== null) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
7329
7952
  MenuItem,
7330
7953
  {
7331
7954
  onSelect: () => {
7332
- onMessage(participant.identity);
7955
+ if (onMessage !== void 0) onMessage(participant.identity);
7956
+ else
7957
+ host?.store.patch({
7958
+ privateChatRequest: { to: participant.identity, at: Date.now() }
7959
+ });
7333
7960
  onClose();
7334
7961
  },
7335
7962
  children: [
@@ -7956,7 +8583,7 @@ function PreJoinBody({
7956
8583
  microphone,
7957
8584
  ...meetingId !== void 0 ? { meetingId } : {},
7958
8585
  ...slug !== void 0 ? { slug } : {}
7959
- }).then(() => onJoined?.()).finally(() => setJoining(false));
8586
+ }).then(() => onJoined?.()).catch(() => void 0).finally(() => setJoining(false));
7960
8587
  },
7961
8588
  children: joining ? "Joining\u2026" : "Join now"
7962
8589
  }
@@ -8092,7 +8719,7 @@ function MeetingRoomBody({
8092
8719
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { className: "mx-meet__title", children: snapshot?.meeting?.title ?? "Meeting" }),
8093
8720
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__meta", children: [
8094
8721
  participantSummary(snapshot?.participants ?? []),
8095
- elapsed > 0 ? ` \xB7 ${(0, import_format2.formatDurationMs)(elapsed)}` : ""
8722
+ elapsed > 0 ? ` \xB7 ${(0, import_format3.formatDurationMs)(elapsed)}` : ""
8096
8723
  ] }),
8097
8724
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RecordingIndicator, {}),
8098
8725
  snapshot?.locked === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__badge", children: [
@@ -8398,7 +9025,10 @@ function MeetingRecordBody({
8398
9025
  const blocks = groupTranscript(bundle.transcript, bundle.names);
8399
9026
  const notice = recordingNotice(bundle.recording);
8400
9027
  const fileId = playableRecordingFileId(bundle.recording);
8401
- const attended = bundle.attendees.filter((person) => person.admissionState !== "denied");
9028
+ const attended = uniqueAttendees(
9029
+ bundle.attendees.filter((person) => person.admissionState !== "denied"),
9030
+ bundle.names
9031
+ );
8402
9032
  const restrictionNotices = summarizeMeetingRecordRestrictions(bundle.restrictions);
8403
9033
  const guestFinishedMeeting = host.identity.userId === null && meeting.endedAt !== null;
8404
9034
  const everyRecordPartRefused = ["attendance", "notes", "transcript", "recording"].every(
@@ -8868,18 +9498,11 @@ function createMeetingInviteHandler(options) {
8868
9498
  }
8869
9499
 
8870
9500
  // src/core/management.ts
8871
- async function refusal(response, operation) {
8872
- let body = null;
8873
- try {
8874
- body = await response.json();
8875
- } catch {
8876
- body = null;
8877
- }
8878
- const said = readServerRefusal(body);
8879
- return new MeetError(
8880
- response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
8881
- `${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
8882
- said?.remedy || "The meeting itself is saved \u2014 only the telling failed. Try again; the call is idempotent."
9501
+ function refusal(response, operation) {
9502
+ return refusalFromResponse(
9503
+ response,
9504
+ operation,
9505
+ "The meeting itself is saved \u2014 only the telling failed. Try again; the call is idempotent."
8883
9506
  );
8884
9507
  }
8885
9508
  function count(body, key) {