@ai-matrx/meet 0.10.7 → 0.11.1

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
@@ -1010,6 +1010,166 @@ var MEET_EVENTS = {
1010
1010
  stateChanged: "mx.state.changed"
1011
1011
  };
1012
1012
 
1013
+ // src/core/reasons.ts
1014
+ var JOIN_REFUSAL_REASONS = [
1015
+ "not_found",
1016
+ "cancelled",
1017
+ "ended",
1018
+ "expired",
1019
+ "too_early",
1020
+ "sign_in_required",
1021
+ "wrong_account",
1022
+ "not_invited",
1023
+ "locked",
1024
+ "removed",
1025
+ "lobby_rejects",
1026
+ "meeting_full",
1027
+ "platform_capacity",
1028
+ "verification_required",
1029
+ "registration_required"
1030
+ ];
1031
+ function isJoinRefusalReason(value) {
1032
+ return typeof value === "string" && JOIN_REFUSAL_REASONS.includes(value);
1033
+ }
1034
+
1035
+ // src/core/phase.ts
1036
+ var LIVE_PHASES = /* @__PURE__ */ new Set(["joining", "in_call"]);
1037
+ var WAITING_PHASES = /* @__PURE__ */ new Set([
1038
+ "waiting:host_not_started",
1039
+ "waiting:knocking",
1040
+ "denied",
1041
+ "knock_expired"
1042
+ ]);
1043
+ var JOINABLE_FROM = /* @__PURE__ */ new Set([
1044
+ "prejoin",
1045
+ "left",
1046
+ "disconnected",
1047
+ "superseded",
1048
+ "inactive",
1049
+ "denied",
1050
+ "knock_expired"
1051
+ ]);
1052
+ var NOT_ENDABLE = /* @__PURE__ */ new Set([
1053
+ "ended",
1054
+ "record",
1055
+ "unsupported",
1056
+ "ringing_out",
1057
+ "ringing_in",
1058
+ "call_declined",
1059
+ "call_missed",
1060
+ "call_busy",
1061
+ "answered_elsewhere"
1062
+ ]);
1063
+ var RINGING = /* @__PURE__ */ new Set(["ringing_out", "ringing_in"]);
1064
+ var IDLE_PHASES = /* @__PURE__ */ new Set([
1065
+ "resolving",
1066
+ "left",
1067
+ "call_declined",
1068
+ "call_missed",
1069
+ "call_busy",
1070
+ "answered_elsewhere"
1071
+ ]);
1072
+ function isLivePhase(phase) {
1073
+ return LIVE_PHASES.has(phase);
1074
+ }
1075
+ function isWaitingPhase(phase) {
1076
+ return WAITING_PHASES.has(phase);
1077
+ }
1078
+ function isRefusedPhase(phase) {
1079
+ return phase.startsWith("refused:");
1080
+ }
1081
+ function reasonOf(phase) {
1082
+ return isRefusedPhase(phase) ? phase.slice("refused:".length) : null;
1083
+ }
1084
+ function refusalPhase(reason) {
1085
+ if (reason === null || !isJoinRefusalReason(reason)) return "disconnected";
1086
+ return reason === "ended" ? "ended" : `refused:${reason}`;
1087
+ }
1088
+ function admissionPhase(admission) {
1089
+ switch (admission) {
1090
+ case "waiting_host":
1091
+ return "waiting:host_not_started";
1092
+ case "knocking":
1093
+ return "waiting:knocking";
1094
+ case "denied":
1095
+ return "denied";
1096
+ case "expired":
1097
+ return "knock_expired";
1098
+ case "withdrawn":
1099
+ return "left";
1100
+ case "ended":
1101
+ return "ended";
1102
+ case "removed":
1103
+ return "refused:removed";
1104
+ default:
1105
+ return null;
1106
+ }
1107
+ }
1108
+ function nextPhase(from, event) {
1109
+ switch (event.type) {
1110
+ case "resolved":
1111
+ return from === "resolving" ? "prejoin" : null;
1112
+ case "unsupported":
1113
+ return from === "resolving" ? "unsupported" : null;
1114
+ case "join":
1115
+ return JOINABLE_FROM.has(from) ? "joining" : null;
1116
+ case "back_to_prejoin":
1117
+ return from === "left" || from === "disconnected" ? "prejoin" : null;
1118
+ case "retry":
1119
+ return isRefusedPhase(from) || from === "unsupported" ? "resolving" : null;
1120
+ case "admission": {
1121
+ if (from !== "joining" && !WAITING_PHASES.has(from)) return null;
1122
+ return admissionPhase(event.admission);
1123
+ }
1124
+ case "admitted":
1125
+ return WAITING_PHASES.has(from) ? "joining" : null;
1126
+ case "connected":
1127
+ return from === "joining" ? "in_call" : null;
1128
+ case "join_failed":
1129
+ return from === "joining" ? refusalPhase(event.reason) : null;
1130
+ case "exit":
1131
+ return from === "in_call" || from === "joining" ? event.to : null;
1132
+ case "leave":
1133
+ return from === "in_call" || from === "joining" || WAITING_PHASES.has(from) ? "left" : null;
1134
+ case "ended":
1135
+ return NOT_ENDABLE.has(from) ? null : "ended";
1136
+ case "ring_out":
1137
+ return IDLE_PHASES.has(from) || from === "prejoin" ? "ringing_out" : null;
1138
+ case "ring_in":
1139
+ return IDLE_PHASES.has(from) ? "ringing_in" : null;
1140
+ case "call_answered":
1141
+ return RINGING.has(from) ? "joining" : null;
1142
+ case "call_outcome":
1143
+ return RINGING.has(from) ? event.outcome : null;
1144
+ case "reset":
1145
+ return LIVE_PHASES.has(from) ? null : "resolving";
1146
+ }
1147
+ }
1148
+ function resolveExit(input) {
1149
+ const { server, hint } = input;
1150
+ if (server === null) {
1151
+ return { to: "disconnected", reason: null, connection: "server_unreachable", notice: null };
1152
+ }
1153
+ const word = server?.exitReason ?? null;
1154
+ if (server !== void 0 && (server.sessionPhase === "ended" || server.meetingPhase === "ended" || word === "ended" || word === "time_limit")) {
1155
+ return { to: "ended", reason: word === "time_limit" ? "time_limit" : "ended", connection: "stable", notice: null };
1156
+ }
1157
+ if (word === "removed") return { to: "removed", reason: "removed", connection: "stable", notice: null };
1158
+ if (word === "displaced") return { to: "superseded", reason: "displaced", connection: "stable", notice: null };
1159
+ if (word === "sent_to_waiting") {
1160
+ return { to: "waiting:knocking", reason: "sent_to_waiting", connection: "stable", notice: "sent-to-waiting" };
1161
+ }
1162
+ if (hint === "DUPLICATE_IDENTITY") return { to: "superseded", reason: "displaced", connection: "stable", notice: null };
1163
+ if (hint === "PARTICIPANT_REMOVED" && word === null && server === void 0) {
1164
+ return { to: "removed", reason: "removed", connection: "stable", notice: null };
1165
+ }
1166
+ if (hint === "ROOM_DELETED" && server === void 0) {
1167
+ return { to: "ended", reason: "ended", connection: "stable", notice: null };
1168
+ }
1169
+ if (input.frozen === true) return { to: "inactive", reason: "inactive", connection: "stable", notice: null };
1170
+ return { to: "disconnected", reason: null, connection: "stable", notice: null };
1171
+ }
1172
+
1013
1173
  // src/core/types.ts
1014
1174
  var asMeetingId = (value) => value;
1015
1175
  var asCallId = (value) => value;
@@ -1101,6 +1261,39 @@ function createCallCenter(options) {
1101
1261
  clearRing(invite.id);
1102
1262
  incoming.delete(invite.id);
1103
1263
  if (outgoing !== null && outgoing.id === invite.id) outgoing = invite;
1264
+ const held = options.store.snapshot().call;
1265
+ if (held !== null && held.invite.id === invite.id && held.direction === "incoming") {
1266
+ const outcome = invite.state === "connecting" || invite.state === "active" ? "answered_elsewhere" : invite.state === "declined" ? "reset" : "call_missed";
1267
+ const moved = outcome === "reset" ? options.store.dispatch({ type: "reset" }) : options.store.dispatch({ type: "call_outcome", outcome });
1268
+ if (moved) options.store.patch({ call: outcome === "reset" ? null : { ...held, invite } });
1269
+ }
1270
+ options.onSettled?.(invite);
1271
+ }
1272
+ function settleOutgoing(invite) {
1273
+ if (outgoing === null || outgoing.id !== invite.id) return;
1274
+ clearRing(invite.id);
1275
+ outgoing = invite;
1276
+ const store = options.store;
1277
+ const held = store.snapshot().call;
1278
+ if (held !== null && held.invite.id === invite.id) store.patch({ call: { ...held, invite } });
1279
+ switch (invite.state) {
1280
+ case "connecting":
1281
+ case "active":
1282
+ if (store.dispatch({ type: "call_answered" })) options.onAnswered?.(invite);
1283
+ break;
1284
+ case "declined":
1285
+ store.dispatch({ type: "call_outcome", outcome: "call_declined" });
1286
+ break;
1287
+ case "busy":
1288
+ store.dispatch({ type: "call_outcome", outcome: "call_busy" });
1289
+ break;
1290
+ case "missed":
1291
+ case "failed":
1292
+ store.dispatch({ type: "call_outcome", outcome: "call_missed" });
1293
+ break;
1294
+ default:
1295
+ break;
1296
+ }
1104
1297
  options.onSettled?.(invite);
1105
1298
  }
1106
1299
  function ingest(invite) {
@@ -1109,7 +1302,7 @@ function createCallCenter(options) {
1109
1302
  if (identity.userId === null) return;
1110
1303
  if (invite.callerUserId === identity.userId) return;
1111
1304
  const phase = options.store.snapshot().phase;
1112
- if (phase === "connected" || phase === "connecting" || phase === "reconnecting") {
1305
+ if (isLivePhase(phase) || phase === "ringing_out") {
1113
1306
  void repository.settleCallInvite(invite.id, "busy", null).then((settled) => {
1114
1307
  to(settled.callerUserId).send(MEET_EVENTS.settled, settled);
1115
1308
  }).catch((error) => {
@@ -1122,6 +1315,9 @@ function createCallCenter(options) {
1122
1315
  return;
1123
1316
  }
1124
1317
  incoming.set(invite.id, { ...invite, state: "ringing" });
1318
+ if (options.store.snapshot().call === null && options.store.dispatch({ type: "ring_in" })) {
1319
+ options.store.patch({ call: { invite: { ...invite, state: "ringing" }, direction: "incoming" } });
1320
+ }
1125
1321
  options.onIncoming?.(invite);
1126
1322
  timeouts.set(
1127
1323
  invite.id,
@@ -1159,6 +1355,7 @@ function createCallCenter(options) {
1159
1355
  const invite = message.data;
1160
1356
  const held = incoming.get(invite.id);
1161
1357
  if (held !== void 0) settleLocal(invite);
1358
+ else if (outgoing?.id === invite.id) settleOutgoing(invite);
1162
1359
  }
1163
1360
  }
1164
1361
  ],
@@ -1225,6 +1422,9 @@ function createCallCenter(options) {
1225
1422
  expiresAt: new Date(timers.now() + ringTimeoutMs).toISOString()
1226
1423
  });
1227
1424
  outgoing = invite;
1425
+ if (options.store.dispatch({ type: "ring_out" })) {
1426
+ options.store.patch({ call: { invite, direction: "outgoing" } });
1427
+ }
1228
1428
  for (const callee of people) {
1229
1429
  to(callee).send(MEET_EVENTS.ring, invite);
1230
1430
  }
@@ -1233,8 +1433,7 @@ function createCallCenter(options) {
1233
1433
  timers.setTimeout(() => {
1234
1434
  if (outgoing?.id !== invite.id) return;
1235
1435
  void repository.settleCallInvite(invite.id, "missed", null).then((settled) => {
1236
- outgoing = settled;
1237
- options.onSettled?.(settled);
1436
+ settleOutgoing(settled);
1238
1437
  }).catch(() => void 0);
1239
1438
  }, ringTimeoutMs)
1240
1439
  );
@@ -1249,7 +1448,9 @@ function createCallCenter(options) {
1249
1448
  "A late accept must not resurrect a settled call. Close the ringing UI; the caller has already moved on."
1250
1449
  );
1251
1450
  }
1451
+ if (options.store.snapshot().phase === "ringing_in") options.store.dispatch({ type: "call_answered" });
1252
1452
  const settled = await repository.settleCallInvite(id, "connecting", null);
1453
+ options.store.patch({ call: { invite: settled, direction: "incoming" } });
1253
1454
  settleLocal(settled);
1254
1455
  channel?.send(MEET_EVENTS.settled, settled);
1255
1456
  to(settled.callerUserId).send(MEET_EVENTS.settled, { ...settled, mode });
@@ -1257,6 +1458,9 @@ function createCallCenter(options) {
1257
1458
  },
1258
1459
  async decline(id, message) {
1259
1460
  const settled = await repository.settleCallInvite(id, "declined", message ?? null);
1461
+ if (options.store.snapshot().call?.invite.id === id && options.store.dispatch({ type: "reset" })) {
1462
+ options.store.patch({ call: null });
1463
+ }
1260
1464
  settleLocal(settled);
1261
1465
  channel?.send(MEET_EVENTS.settled, settled);
1262
1466
  to(settled.callerUserId).send(MEET_EVENTS.settled, settled);
@@ -1266,6 +1470,9 @@ function createCallCenter(options) {
1266
1470
  const settled = await repository.settleCallInvite(id, "cancelled", null);
1267
1471
  clearRing(id);
1268
1472
  if (outgoing?.id === id) outgoing = settled;
1473
+ if (options.store.snapshot().call?.invite.id === id && options.store.dispatch({ type: "reset" })) {
1474
+ options.store.patch({ call: null });
1475
+ }
1269
1476
  for (const callee of settled.calleeUserIds) {
1270
1477
  to(callee).send(MEET_EVENTS.cancel, settled);
1271
1478
  }
@@ -1651,7 +1858,9 @@ var TABLES = {
1651
1858
  /** Wave 1 (0.8): who a meeting was sent to, and what they answered. */
1652
1859
  invitees: "meet_invitees",
1653
1860
  /** Wave 1 (0.8): one occurrence of a series cancelled or moved. */
1654
- occurrenceOverrides: "meet_occurrence_overrides"
1861
+ occurrenceOverrides: "meet_occurrence_overrides",
1862
+ /** S5: one row per RUN of a meeting; carries that run's note-taker state. */
1863
+ sessions: "meet_sessions"
1655
1864
  };
1656
1865
  var RPCS = {
1657
1866
  /** Atomic: create the invite row and stamp its expiry in one statement. */
@@ -2127,6 +2336,18 @@ function createMeetRepository(options) {
2127
2336
  );
2128
2337
  }
2129
2338
  }
2339
+ async function withRunNoteTaker(meeting) {
2340
+ const response = await db.from(TABLES.sessions).select("note_taker,started_at").eq("meeting_id", meeting.id).order("started_at", { ascending: false }).limit(1);
2341
+ const run = response.error === null ? (response.data ?? [])[0] : void 0;
2342
+ const noteTaker = run?.["note_taker"];
2343
+ if (typeof noteTaker !== "object" || noteTaker === null || Array.isArray(noteTaker)) {
2344
+ return meeting;
2345
+ }
2346
+ return {
2347
+ ...meeting,
2348
+ metadata: { ...meeting.metadata ?? {}, note_taker: noteTaker }
2349
+ };
2350
+ }
2130
2351
  return {
2131
2352
  schema: MEET_SCHEMA,
2132
2353
  tables: TABLES,
@@ -2365,13 +2586,13 @@ function createMeetRepository(options) {
2365
2586
  "meet.meetingBySlug",
2366
2587
  () => db.rpc(RPCS.meetingBySlug, { p_slug: slug })
2367
2588
  );
2368
- return projectMeeting(row);
2589
+ return withRunNoteTaker(projectMeeting(row));
2369
2590
  },
2370
2591
  async meeting(id) {
2371
2592
  const response = await db.from(TABLES.meetings).select(MEETING_COLUMNS).eq("id", id).single();
2372
2593
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.meeting");
2373
2594
  if (response.data === null) throw invalidResponse("meet.meeting", "no such meeting");
2374
- return projectMeeting(response.data);
2595
+ return withRunNoteTaker(projectMeeting(response.data));
2375
2596
  },
2376
2597
  async listMeetings({ organizationId = null, limit = 25, before = null }) {
2377
2598
  let query = db.from(TABLES.meetings).select(MEETING_COLUMNS);
@@ -3004,8 +3225,15 @@ var NO_GRANTS = {
3004
3225
  canPublish: false,
3005
3226
  canSubscribe: false
3006
3227
  };
3228
+ var NO_SHARE = { state: "none", reason: null, remedy: null, at: 0 };
3007
3229
  var EMPTY = {
3008
- phase: "idle",
3230
+ phase: "resolving",
3231
+ reason: null,
3232
+ phaseNotice: null,
3233
+ roomName: null,
3234
+ connection: "stable",
3235
+ share: NO_SHARE,
3236
+ call: null,
3009
3237
  waiting: null,
3010
3238
  meeting: null,
3011
3239
  localIdentity: null,
@@ -3069,8 +3297,20 @@ function createMeetStore(initial = {}) {
3069
3297
  return () => listeners.delete(listener);
3070
3298
  },
3071
3299
  patch(next) {
3300
+ if ("phase" in next || "reason" in next || "phaseNotice" in next) {
3301
+ throw new TypeError("meet store: the phase moves only through store.dispatch(event)");
3302
+ }
3072
3303
  commit({ ...state, ...next });
3073
3304
  },
3305
+ dispatch(event, detail) {
3306
+ const to = nextPhase(state.phase, event);
3307
+ if (to === null) return false;
3308
+ const reason = detail?.reason !== void 0 ? detail.reason : reasonOf(to);
3309
+ const phaseNotice = detail?.notice ?? null;
3310
+ if (to === state.phase && reason === state.reason && phaseNotice === state.phaseNotice) return true;
3311
+ commit({ ...state, phase: to, reason, phaseNotice });
3312
+ return true;
3313
+ },
3074
3314
  setParticipants(participants) {
3075
3315
  const names = learnNames(state.names, participants);
3076
3316
  const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
@@ -3388,7 +3628,7 @@ function createMeetingSession(options) {
3388
3628
  }
3389
3629
  function apply(read) {
3390
3630
  const snapshot = store.snapshot();
3391
- if (snapshot.phase !== "lobby") return;
3631
+ if (!isWaitingPhase(snapshot.phase)) return;
3392
3632
  const admission = read.admission_state ?? null;
3393
3633
  const ended = admission === "ended" || read.session_phase === "ended" || read.meeting_phase === "ended";
3394
3634
  if (admission === "admitted" && !ended) {
@@ -3405,6 +3645,7 @@ function createMeetingSession(options) {
3405
3645
  if (read.meeting_phase === "ended" && meeting !== null && meeting.endedAt === null) {
3406
3646
  store.patch({ meeting: { ...meeting, endedAt: (/* @__PURE__ */ new Date()).toISOString() } });
3407
3647
  }
3648
+ store.dispatch({ type: "admission", admission: "ended" }, { reason: "ended" });
3408
3649
  return;
3409
3650
  }
3410
3651
  if (admission === null) return;
@@ -3416,6 +3657,7 @@ function createMeetingSession(options) {
3416
3657
  pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
3417
3658
  liveVersion: read.live_version ?? 0
3418
3659
  });
3660
+ store.dispatch({ type: "admission", admission });
3419
3661
  if (admission === "denied" || admission === "expired" || admission === "withdrawn" || admission === "removed") {
3420
3662
  stopWaiting();
3421
3663
  }
@@ -3511,7 +3753,7 @@ function createMeetingSession(options) {
3511
3753
  // 🚨 PAYLOAD-FREE. Every receiver reads again; a spoofed hint only causes a read.
3512
3754
  event: MEET_EVENTS.stateChanged,
3513
3755
  onMessage: () => {
3514
- if (store.snapshot().phase === "lobby") void readState2();
3756
+ if (isWaitingPhase(store.snapshot().phase)) void readState2();
3515
3757
  void refreshQueue();
3516
3758
  }
3517
3759
  }
@@ -3523,7 +3765,7 @@ function createMeetingSession(options) {
3523
3765
  if (meeting === null) return;
3524
3766
  const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
3525
3767
  store.patch({ meeting: fresh, locked: fresh.locked });
3526
- if (store.snapshot().phase === "lobby") void readState2();
3768
+ if (isWaitingPhase(store.snapshot().phase)) void readState2();
3527
3769
  }
3528
3770
  });
3529
3771
  },
@@ -3690,10 +3932,34 @@ function createMeetingSession(options) {
3690
3932
  },
3691
3933
  stopWaiting,
3692
3934
  readState: readState2,
3935
+ async readExit(connection) {
3936
+ const meeting = store.snapshot().meeting;
3937
+ const pass = options.resolvePass?.() ?? null;
3938
+ if (meeting === null || pass === null) return void 0;
3939
+ try {
3940
+ const response = await options.api.request({
3941
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}&connection=${encodeURIComponent(connection)}`,
3942
+ method: "GET",
3943
+ requireAuth: false,
3944
+ operation: "meet.state(exit)",
3945
+ headers: { [MEETING_PASS_HEADER]: pass }
3946
+ });
3947
+ if (!response.ok) return null;
3948
+ const read = await response.json();
3949
+ return {
3950
+ exitReason: read.exit_reason ?? null,
3951
+ sessionPhase: read.session_phase ?? null,
3952
+ meetingPhase: read.meeting_phase ?? null
3953
+ };
3954
+ } catch {
3955
+ return null;
3956
+ }
3957
+ },
3693
3958
  async withdraw() {
3694
3959
  stopWaiting();
3695
3960
  await postLeave("withdraw");
3696
3961
  patchWaiting({ admission: "withdrawn" });
3962
+ store.dispatch({ type: "admission", admission: "withdrawn" });
3697
3963
  hint();
3698
3964
  },
3699
3965
  async askAgain() {
@@ -4177,6 +4443,9 @@ var TRANSCRIPTION_ATTRIBUTES = {
4177
4443
  };
4178
4444
 
4179
4445
  // src/core/room.ts
4446
+ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
4447
+ return /administrator|policy/i.test(remedy) ? remedy : SCREEN_SHARE_BLOCKED_REMEDY;
4448
+ }
4180
4449
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
4181
4450
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
4182
4451
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
@@ -4244,7 +4513,7 @@ function createRoomEngine(options) {
4244
4513
  let disposed = false;
4245
4514
  let moving = false;
4246
4515
  const confirmingEnds = /* @__PURE__ */ new WeakSet();
4247
- async function leaveRoom() {
4516
+ async function dropRoom() {
4248
4517
  const target = driver;
4249
4518
  driver = null;
4250
4519
  if (current !== null) {
@@ -4253,9 +4522,14 @@ function createRoomEngine(options) {
4253
4522
  }
4254
4523
  store.setLocalTrack("microphone", IDLE_TRACK);
4255
4524
  store.setLocalTrack("camera", IDLE_TRACK);
4256
- store.patch({ phase: "left" });
4525
+ store.patch({ share: NO_SHARE, connection: "stable" });
4257
4526
  await target?.disconnect();
4258
4527
  }
4528
+ async function leaveRoom() {
4529
+ store.dispatch({ type: "leave" });
4530
+ await dropRoom();
4531
+ options.onLeft?.(null);
4532
+ }
4259
4533
  async function confirmMeetingEnd() {
4260
4534
  const meeting = store.snapshot().meeting;
4261
4535
  const target = driver;
@@ -4277,7 +4551,9 @@ function createRoomEngine(options) {
4277
4551
  const latest = store.snapshot().meeting;
4278
4552
  if (latest === null) return;
4279
4553
  store.patch({ meeting: { ...latest, endedAt: verified.endedAt } });
4280
- await leaveRoom();
4554
+ store.dispatch({ type: "ended" }, { reason: "ended" });
4555
+ await dropRoom();
4556
+ options.onLeft?.("ended");
4281
4557
  } catch (error) {
4282
4558
  report("error", `Meet could not confirm or finish meeting teardown: ${String(error)}`, "Check the meeting link again; if media remains active, close this meeting tab.");
4283
4559
  } finally {
@@ -4289,12 +4565,12 @@ function createRoomEngine(options) {
4289
4565
  options.onDiagnostic?.(remedy === void 0 ? { level, message } : { level, message, remedy });
4290
4566
  }
4291
4567
  let ambiguousShareDenials = 0;
4292
- function fail(error, operation) {
4568
+ function failJoin(error, operation) {
4293
4569
  const normalized = normalizeMeetError(error, operation);
4294
4570
  store.patch({
4295
- phase: "failed",
4296
4571
  failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
4297
4572
  });
4573
+ store.dispatch({ type: "join_failed", reason: normalized.reason ?? null });
4298
4574
  report(
4299
4575
  normalized.code === "session-unavailable" ? "warn" : "error",
4300
4576
  normalized.message,
@@ -4302,6 +4578,14 @@ function createRoomEngine(options) {
4302
4578
  );
4303
4579
  return normalized;
4304
4580
  }
4581
+ async function lostRoom(error, operation) {
4582
+ const normalized = normalizeMeetError(error, operation);
4583
+ store.patch({ failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason } });
4584
+ await dropRoom();
4585
+ store.dispatch({ type: "exit", to: "disconnected" });
4586
+ report("error", normalized.message, normalized.remedy);
4587
+ return normalized;
4588
+ }
4305
4589
  const handlers = {
4306
4590
  participants(list) {
4307
4591
  const joinedAt = new Date(now()).toISOString();
@@ -4344,9 +4628,12 @@ function createRoomEngine(options) {
4344
4628
  store.setQuality(asParticipantIdentity(who), value);
4345
4629
  },
4346
4630
  connectionState(state) {
4347
- if (state === "connected") store.patch({ phase: "connected", failure: null });
4348
- else if (state === "reconnecting") store.patch({ phase: "reconnecting" });
4349
- else if (state === "connecting") store.patch({ phase: "connecting" });
4631
+ if (state === "connected") {
4632
+ if (store.snapshot().connection !== "stable") store.patch({ connection: "stable" });
4633
+ store.dispatch({ type: "connected" });
4634
+ } else if (state === "reconnecting") {
4635
+ store.patch({ connection: "reconnecting" });
4636
+ }
4350
4637
  },
4351
4638
  transcription(event) {
4352
4639
  const snapshot = store.snapshot();
@@ -4473,17 +4760,48 @@ function createRoomEngine(options) {
4473
4760
  }
4474
4761
  },
4475
4762
  disconnected(reason) {
4476
- if (moving) return;
4477
- if (current !== null) {
4478
- tokens.invalidate(current.roomName);
4479
- current = null;
4480
- }
4481
- store.patch({ phase: "left" });
4482
- options.onLeft?.(reason);
4763
+ if (moving || driver === null) return;
4764
+ void resolveExitAndApply(reason);
4483
4765
  }
4484
4766
  };
4767
+ async function resolveExitAndApply(hint) {
4768
+ const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
4769
+ await dropRoom();
4770
+ let server;
4771
+ if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
4772
+ server = void 0;
4773
+ } else {
4774
+ try {
4775
+ server = await options.readExit(connection);
4776
+ } catch {
4777
+ server = null;
4778
+ }
4779
+ }
4780
+ if (disposed) return;
4781
+ const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
4782
+ store.patch({ connection: resolved.connection });
4783
+ if (resolved.to === "waiting:knocking") {
4784
+ store.patch({
4785
+ waiting: {
4786
+ admission: "knocking",
4787
+ admittersPresent: null,
4788
+ reknockAllowed: true,
4789
+ knockExpiresAt: null,
4790
+ pollSeconds: store.snapshot().waiting?.pollSeconds ?? 3,
4791
+ liveVersion: 0
4792
+ }
4793
+ });
4794
+ }
4795
+ store.dispatch({ type: "exit", to: resolved.to }, { reason: resolved.reason, notice: resolved.notice });
4796
+ report(
4797
+ resolved.to === "disconnected" ? "warn" : "info",
4798
+ `meet: the room connection ended (${hint ?? "no LiveKit reason"}) \u2192 ${resolved.to}`,
4799
+ resolved.to === "disconnected" ? "Press Rejoin; the meeting is still going." : "The screen says what happened."
4800
+ );
4801
+ options.onLeft?.(hint);
4802
+ }
4485
4803
  async function connect(request) {
4486
- store.patch({ phase: "requesting-token", failure: null, waiting: null });
4804
+ store.patch({ failure: null, waiting: null, roomName: request.roomName, share: NO_SHARE, connection: "stable" });
4487
4805
  const token = await tokens.get({
4488
4806
  roomName: request.roomName,
4489
4807
  identity,
@@ -4506,10 +4824,10 @@ function createRoomEngine(options) {
4506
4824
  }
4507
4825
  });
4508
4826
  if (token.lobby || !token.canSubscribe) {
4827
+ const admission = token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking";
4509
4828
  store.patch({
4510
- phase: "lobby",
4511
4829
  waiting: {
4512
- admission: token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking",
4830
+ admission,
4513
4831
  admittersPresent: null,
4514
4832
  reknockAllowed: true,
4515
4833
  knockExpiresAt: null,
@@ -4517,9 +4835,10 @@ function createRoomEngine(options) {
4517
4835
  liveVersion: 0
4518
4836
  }
4519
4837
  });
4838
+ store.dispatch({ type: "admission", admission });
4520
4839
  return;
4521
4840
  }
4522
- store.patch({ phase: "connecting", waiting: null });
4841
+ store.patch({ waiting: null });
4523
4842
  const created = await options.driverFactory(handlers);
4524
4843
  if (disposed) {
4525
4844
  await created.disconnect();
@@ -4527,7 +4846,8 @@ function createRoomEngine(options) {
4527
4846
  }
4528
4847
  driver = created;
4529
4848
  await created.connect(token.serverUrl, token.token);
4530
- store.patch({ phase: "connected", failure: null });
4849
+ store.patch({ failure: null, connection: "stable" });
4850
+ store.dispatch({ type: "connected" });
4531
4851
  store.setLocalTrack("microphone", IDLE_TRACK);
4532
4852
  store.setLocalTrack("camera", IDLE_TRACK);
4533
4853
  const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
@@ -4598,9 +4918,17 @@ function createRoomEngine(options) {
4598
4918
  return {
4599
4919
  join(request) {
4600
4920
  if (joining !== null) return joining;
4921
+ if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
4922
+ report(
4923
+ "warn",
4924
+ `meet.join: cannot join from "${store.snapshot().phase}"`,
4925
+ "Join is offered from the pre-join, left, disconnected and replaced screens only."
4926
+ );
4927
+ return Promise.resolve();
4928
+ }
4601
4929
  lastRequest = request;
4602
4930
  joining = connect(request).catch((error) => {
4603
- throw fail(error, "meet.join");
4931
+ throw failJoin(error, "meet.join");
4604
4932
  }).finally(() => {
4605
4933
  joining = null;
4606
4934
  });
@@ -4617,10 +4945,16 @@ function createRoomEngine(options) {
4617
4945
  return;
4618
4946
  }
4619
4947
  if (joining !== null) return joining;
4948
+ const phase = store.snapshot().phase;
4949
+ const moved = phase === "joining" || store.dispatch({ type: "join", click: true }) || store.dispatch({ type: "admitted" });
4950
+ if (!moved) {
4951
+ report("warn", `meet.rejoin: cannot rejoin from "${phase}"`, "Rejoin is offered after leaving, a lost connection, or admission.");
4952
+ return;
4953
+ }
4620
4954
  tokens.invalidate(request.roomName);
4621
4955
  current = null;
4622
4956
  joining = connect(request).catch((error) => {
4623
- throw fail(error, "meet.rejoin");
4957
+ throw failJoin(error, "meet.rejoin");
4624
4958
  }).finally(() => {
4625
4959
  joining = null;
4626
4960
  });
@@ -4665,11 +4999,11 @@ function createRoomEngine(options) {
4665
4999
  joining = connect(lastRequest).finally(() => {
4666
5000
  joining = null;
4667
5001
  });
4668
- await joining.catch((again) => {
4669
- throw fail(again, "meet.moveToBreakout");
5002
+ await joining.catch(async (again) => {
5003
+ throw await lostRoom(again, "meet.moveToBreakout");
4670
5004
  });
4671
5005
  } else {
4672
- throw fail(error, "meet.moveToBreakout");
5006
+ throw await lostRoom(error, "meet.moveToBreakout");
4673
5007
  }
4674
5008
  throw normalized;
4675
5009
  } finally {
@@ -4677,6 +5011,9 @@ function createRoomEngine(options) {
4677
5011
  }
4678
5012
  },
4679
5013
  leave: leaveRoom,
5014
+ backToPrejoin() {
5015
+ store.dispatch({ type: "back_to_prejoin", click: true });
5016
+ },
4680
5017
  // Turning a device on mid-meeting is the SAME operation as turning it on
4681
5018
  // during the join, so it runs the same function. A failure here used to
4682
5019
  // stamp the room `failed` — a busy microphone would blank a live meeting.
@@ -4690,21 +5027,41 @@ function createRoomEngine(options) {
4690
5027
  },
4691
5028
  async setScreenShareEnabled(enabled) {
4692
5029
  if (driver === null) return;
5030
+ if (enabled && driver.supportsScreenShare?.() === false) {
5031
+ store.patch({ share: { state: "refused", reason: "unsupported", remedy: null, at: now() } });
5032
+ report("warn", "meet: this browser cannot share a screen", "Share from a desktop browser (Chrome, Edge, Firefox or Safari).");
5033
+ return;
5034
+ }
5035
+ if (enabled) store.patch({ share: { state: "picking", reason: null, remedy: null, at: now() } });
4693
5036
  try {
4694
5037
  await driver.setScreenShareEnabled(enabled);
4695
5038
  ambiguousShareDenials = 0;
5039
+ store.patch({ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now() } });
4696
5040
  } catch (error) {
4697
5041
  const kind = classifyScreenShareError(error);
4698
5042
  if (kind === "ambiguous") {
4699
5043
  ambiguousShareDenials += 1;
4700
- if (ambiguousShareDenials < 2) return;
5044
+ if (ambiguousShareDenials < 2) {
5045
+ store.patch({ share: NO_SHARE });
5046
+ return;
5047
+ }
4701
5048
  }
5049
+ ambiguousShareDenials = 0;
4702
5050
  const normalized = normalizeScreenShareError(error, "meet.setScreenShareEnabled");
4703
5051
  report("warn", normalized.message, normalized.remedy);
4704
- const text3 = kind === "other" ? `Screen sharing is not available. ${normalized.remedy}` : `Screen sharing did not start. ${SCREEN_SHARE_BLOCKED_REMEDY}`;
4705
- store.patch({ actionNotice: { text: text3, failed: true, at: now() } });
5052
+ store.patch({
5053
+ share: {
5054
+ state: "refused",
5055
+ reason: kind === "other" ? "failed" : "blocked",
5056
+ remedy: kind === "other" ? normalized.remedy : SCREEN_SHARE_BLOCKED_REMEDY_FOR(normalized.remedy),
5057
+ at: now()
5058
+ }
5059
+ });
4706
5060
  }
4707
5061
  },
5062
+ dismissShareNotice() {
5063
+ if (store.snapshot().share.state === "refused") store.patch({ share: NO_SHARE });
5064
+ },
4708
5065
  async raiseHand(raised) {
4709
5066
  const local = store.snapshot().localIdentity;
4710
5067
  if (local !== null) store.setHand(local, raised);
@@ -5206,7 +5563,8 @@ function createLiveKitDriver(handlers, options = {}) {
5206
5563
  }).on(RoomEvent.Disconnected, (reason) => {
5207
5564
  audioContainer?.remove();
5208
5565
  audioContainer = null;
5209
- handlers.disconnected(reason === void 0 ? null : String(reason));
5566
+ const named = reason === void 0 ? null : module?.DisconnectReason?.[reason] ?? String(reason);
5567
+ handlers.disconnected(named);
5210
5568
  });
5211
5569
  try {
5212
5570
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -5594,7 +5952,9 @@ function MeetRuntime(props) {
5594
5952
  driverFactory: (handlers) => (driverRef.current ?? createLiveKitDriver)(handlers),
5595
5953
  onDiagnostic: report,
5596
5954
  effectsStorage,
5597
- resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null
5955
+ resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null,
5956
+ // §3.4: on every LiveKit disconnect, ask the server what happened to this connection.
5957
+ readExit: (connection) => meetings.readExit(connection)
5598
5958
  });
5599
5959
  const meetings = createMeetingSession({
5600
5960
  manager,
@@ -5629,17 +5989,17 @@ function MeetRuntime(props) {
5629
5989
  const inRoom = createInRoomClient({ api, store, resolveRoomToken });
5630
5990
  store.subscribe(() => {
5631
5991
  const snapshot = store.snapshot();
5632
- if (snapshot.phase === "lobby" && snapshot.waiting !== null && (snapshot.waiting.admission === "knocking" || snapshot.waiting.admission === "waiting_host")) {
5992
+ if (isWaitingPhase(snapshot.phase) && snapshot.waiting !== null && (snapshot.waiting.admission === "knocking" || snapshot.waiting.admission === "waiting_host")) {
5633
5993
  meetings.startWaiting();
5634
- } else if (snapshot.phase !== "lobby") {
5994
+ } else if (!isWaitingPhase(snapshot.phase)) {
5635
5995
  meetings.stopWaiting();
5636
5996
  }
5637
5997
  });
5638
5998
  let chatReadFor = null;
5639
5999
  store.subscribe(() => {
5640
6000
  const snapshot = store.snapshot();
5641
- if (snapshot.phase !== "connected" || snapshot.meeting === null) {
5642
- if (snapshot.phase === "left" || snapshot.phase === "idle") chatReadFor = null;
6001
+ if (snapshot.phase !== "in_call" || snapshot.meeting === null) {
6002
+ if (snapshot.phase === "left" || snapshot.phase === "prejoin" || snapshot.phase === "resolving") chatReadFor = null;
5643
6003
  return;
5644
6004
  }
5645
6005
  if (chatReadFor === snapshot.meeting.id) return;
@@ -5692,6 +6052,10 @@ function MeetRuntime(props) {
5692
6052
  store,
5693
6053
  identity,
5694
6054
  onIncoming: (invite) => incomingRef.current?.(invite),
6055
+ // §3.6: my call was answered — the caller joins the call's room on the same machine.
6056
+ onAnswered: (invite) => {
6057
+ void engine.join({ roomName: invite.roomName, mode: invite.mode }).catch(() => void 0);
6058
+ },
5695
6059
  onDiagnostic: report,
5696
6060
  ...props.ringTimeoutMs !== void 0 ? { ringTimeoutMs: props.ringTimeoutMs } : {}
5697
6061
  });
@@ -5831,43 +6195,12 @@ function endedRecord(snapshot, given) {
5831
6195
  function observedPhase(input) {
5832
6196
  const { snapshot } = input;
5833
6197
  if (snapshot === null) return "resolving";
5834
- const raw = snapshot.phase;
5835
- const live = raw === "connected" || raw === "reconnecting";
5836
- if (endedRecord(snapshot, input.meeting ?? null) && !live) return "ended";
5837
- const reason = raw === "failed" ? snapshot.failure?.reason ?? null : null;
5838
- if (reason !== null) return reason === "ended" ? "ended" : `refused:${reason}`;
5839
- switch (raw) {
5840
- case "idle":
5841
- case "failed":
5842
- case "left":
5843
- return "prejoin";
5844
- case "lobby":
5845
- return waitingPhase(snapshot);
5846
- case "requesting-token":
5847
- case "connecting":
5848
- return "joining";
5849
- case "connected":
5850
- case "reconnecting":
5851
- return "in_call";
5852
- }
6198
+ const phase = snapshot.phase;
6199
+ if (endedRecord(snapshot, input.meeting ?? null) && !isLivePhase(phase) && !isCallPhase(phase)) return "ended";
6200
+ return phase;
5853
6201
  }
5854
- function waitingPhase(snapshot) {
5855
- switch (snapshot.waiting?.admission) {
5856
- case "waiting_host":
5857
- return "waiting:host_not_started";
5858
- case "denied":
5859
- return "denied";
5860
- case "expired":
5861
- return "knock_expired";
5862
- case "withdrawn":
5863
- return "left";
5864
- case "ended":
5865
- return "ended";
5866
- case "removed":
5867
- return "refused:removed";
5868
- default:
5869
- return "waiting:knocking";
5870
- }
6202
+ function isCallPhase(phase) {
6203
+ return phase.startsWith("ringing_") || phase.startsWith("call_") || phase === "answered_elsewhere";
5871
6204
  }
5872
6205
  var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
5873
6206
  function observe(input) {
@@ -5880,10 +6213,10 @@ function observe(input) {
5880
6213
  const deviceInPlay = phase === "joining" || inCall;
5881
6214
  return {
5882
6215
  phase,
5883
- reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
6216
+ reason: phase === "ended" ? snapshot?.reason ?? null : snapshot?.reason ?? reasonOf(phase),
5884
6217
  role,
5885
- connection: snapshot?.phase === "reconnecting" ? "reconnecting" : "stable",
5886
- hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot?.phase === "lobby" ? snapshot.waiting?.admittersPresent ?? null : null,
6218
+ connection: snapshot?.connection ?? "stable",
6219
+ hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
5887
6220
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
5888
6221
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
5889
6222
  audioBlocked: null,
@@ -6684,7 +7017,7 @@ function useRoom() {
6684
7017
  (participant) => participant.identity === snapshot.localIdentity
6685
7018
  ) ?? null;
6686
7019
  return {
6687
- phase: snapshot?.phase ?? "idle",
7020
+ phase: snapshot?.phase ?? "resolving",
6688
7021
  participants: snapshot?.participants ?? [],
6689
7022
  failure: snapshot?.failure ?? null,
6690
7023
  local,
@@ -8035,7 +8368,7 @@ function useSharedFocus(panelKey) {
8035
8368
  const snapshot = useMeetSnapshot();
8036
8369
  const focus = snapshot?.collab.focus ?? null;
8037
8370
  const mine = focus !== null && focus.panel === panelKey ? focus : null;
8038
- const canMove = !isObserverRole(snapshot?.grants.role) && snapshot?.phase === "connected";
8371
+ const canMove = !isObserverRole(snapshot?.grants.role) && snapshot?.phase === "in_call";
8039
8372
  return {
8040
8373
  section: mine?.section || null,
8041
8374
  byName: mine?.byName || null,
@@ -8111,6 +8444,36 @@ function useCollabPanelRequest(onRequest) {
8111
8444
  var MEET_REACTIONS = ["\u{1F496}", "\u{1F44D}", "\u{1F389}", "\u{1F44F}", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F914}", "\u{1F44E}"];
8112
8445
 
8113
8446
  // src/react/facade.ts
8447
+ var NO_ACTIONS = {
8448
+ join: async () => void 0,
8449
+ rejoin: async () => void 0,
8450
+ leave: async () => void 0,
8451
+ backToPrejoin: () => void 0,
8452
+ retry: () => void 0,
8453
+ resolved: () => void 0,
8454
+ dismissShareNotice: () => void 0,
8455
+ dismissCall: () => void 0
8456
+ };
8457
+ function actionsFor(host) {
8458
+ if (host === null) return NO_ACTIONS;
8459
+ const { engine, store } = host;
8460
+ return {
8461
+ join: (request) => engine.join(request),
8462
+ rejoin: () => engine.rejoin(),
8463
+ leave: () => engine.leave(),
8464
+ backToPrejoin: () => engine.backToPrejoin(),
8465
+ retry: () => {
8466
+ store.dispatch({ type: "retry", click: true });
8467
+ },
8468
+ resolved: () => {
8469
+ store.dispatch({ type: "resolved" });
8470
+ },
8471
+ dismissShareNotice: () => engine.dismissShareNotice(),
8472
+ dismissCall: () => {
8473
+ if (store.dispatch({ type: "reset" })) store.patch({ call: null });
8474
+ }
8475
+ };
8476
+ }
8114
8477
  function useMeetCore() {
8115
8478
  const host = useMeetHost();
8116
8479
  const snapshot = useMeetSnapshot();
@@ -8118,9 +8481,24 @@ function useMeetCore() {
8118
8481
  const role = useRole();
8119
8482
  const isHost = useIsHost();
8120
8483
  const observation = useCoreObservation();
8484
+ const actions = useMemo5(() => actionsFor(host), [host]);
8121
8485
  return useMemo5(
8122
- () => ({ host, snapshot, devices, role, isHost, observation }),
8123
- [host, snapshot, devices, role, isHost, observation]
8486
+ () => ({
8487
+ host,
8488
+ snapshot,
8489
+ devices,
8490
+ role,
8491
+ isHost,
8492
+ observation,
8493
+ phase: snapshot?.phase ?? "resolving",
8494
+ reason: snapshot?.reason ?? null,
8495
+ phaseNotice: snapshot?.phaseNotice ?? null,
8496
+ connection: snapshot?.connection ?? "stable",
8497
+ share: snapshot?.share ?? NO_SHARE,
8498
+ call: snapshot?.call ?? null,
8499
+ actions
8500
+ }),
8501
+ [host, snapshot, devices, role, isHost, observation, actions]
8124
8502
  );
8125
8503
  }
8126
8504
 
@@ -11051,11 +11429,52 @@ function MeetSkinRoom({
11051
11429
  headerControls,
11052
11430
  observe: observe2 = false
11053
11431
  }) {
11054
- const { host, snapshot, isHost } = useMeetCore();
11432
+ const core = useMeetCore();
11433
+ const { host, snapshot, isHost, phase } = core;
11055
11434
  const elapsed = useElapsed(snapshot?.meeting?.startedAt ?? null);
11056
- const phase = snapshot?.phase ?? "idle";
11057
11435
  if (host === null) return null;
11058
- if (phase === "idle" || phase === "failed" || phase === "left") {
11436
+ const screen = (() => {
11437
+ if (isRefusedPhase(phase)) return "refused";
11438
+ switch (phase) {
11439
+ case "resolving":
11440
+ return "resolving";
11441
+ case "prejoin":
11442
+ case "guest_name":
11443
+ return "prejoin";
11444
+ case "joining":
11445
+ case "in_call":
11446
+ return "room";
11447
+ case "waiting:host_not_started":
11448
+ case "waiting:knocking":
11449
+ case "denied":
11450
+ case "knock_expired":
11451
+ return "waiting";
11452
+ case "left":
11453
+ return snapshot?.waiting?.admission === "withdrawn" ? "waiting" : "exit";
11454
+ case "disconnected":
11455
+ case "removed":
11456
+ case "superseded":
11457
+ case "inactive":
11458
+ case "unsupported":
11459
+ case "ended":
11460
+ case "record":
11461
+ case "ringing_out":
11462
+ case "ringing_in":
11463
+ case "call_declined":
11464
+ case "call_missed":
11465
+ case "call_busy":
11466
+ case "answered_elsewhere":
11467
+ return "exit";
11468
+ default: {
11469
+ const unhandled = phase;
11470
+ return unhandled;
11471
+ }
11472
+ }
11473
+ })();
11474
+ if (screen === "resolving") {
11475
+ return /* @__PURE__ */ jsx6("div", { className: "mx-meet", children: /* @__PURE__ */ jsx6("p", { className: "mx-meet__empty-body", role: "status", children: "Getting the meeting ready\u2026" }) });
11476
+ }
11477
+ if (screen === "prejoin") {
11059
11478
  return /* @__PURE__ */ jsx6("div", { className: "mx-meet", children: /* @__PURE__ */ jsx6(
11060
11479
  PreJoin,
11061
11480
  {
@@ -11066,9 +11485,12 @@ function MeetSkinRoom({
11066
11485
  }
11067
11486
  ) });
11068
11487
  }
11069
- if (phase === "lobby") {
11488
+ if (screen === "waiting") {
11070
11489
  return /* @__PURE__ */ jsx6("div", { className: "mx-meet mx-meet--lobby", children: /* @__PURE__ */ jsx6(WaitingRoom, { host, ...onLeave !== void 0 ? { onLeave } : {} }) });
11071
11490
  }
11491
+ if (screen === "refused" || screen === "exit") {
11492
+ return /* @__PURE__ */ jsx6("div", { className: "mx-meet mx-meet--lobby", children: /* @__PURE__ */ jsx6(ExitScreen, { ...onLeave !== void 0 ? { onLeave } : {} }) });
11493
+ }
11072
11494
  return /* @__PURE__ */ jsxs3("div", { className: "mx-meet", children: [
11073
11495
  /* @__PURE__ */ jsx6(ConsentNotice, {}),
11074
11496
  /* @__PURE__ */ jsx6(AttendanceNotice, {}),
@@ -11088,14 +11510,148 @@ function MeetSkinRoom({
11088
11510
  headerControls !== void 0 && headerControls !== null ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__header-controls", "data-meet-header-controls": "", children: headerControls }) : null,
11089
11511
  isHost ? /* @__PURE__ */ jsx6(HostMenu, {}) : null
11090
11512
  ] }),
11091
- phase === "reconnecting" ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__banner", role: "status", children: "Reconnecting\u2026" }) : null,
11513
+ core.connection !== "stable" ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__banner", role: "status", children: "Reconnecting\u2026" }) : null,
11514
+ /* @__PURE__ */ jsx6(ShareNotice, {}),
11092
11515
  /* @__PURE__ */ jsx6(Stage, {}),
11093
11516
  /* @__PURE__ */ jsx6(Captions, {}),
11094
11517
  /* @__PURE__ */ jsx6(ControlBar, { ...onLeave !== void 0 ? { onLeave } : {} })
11095
11518
  ] });
11096
11519
  }
11520
+ function ShareNotice() {
11521
+ const { share, actions } = useMeetCore();
11522
+ if (share.state !== "refused") return null;
11523
+ const text3 = share.reason === "unsupported" ? "You can't share your screen from this browser" : share.reason === "blocked" ? "Screen sharing is blocked" : "Couldn't start sharing your screen";
11524
+ return /* @__PURE__ */ jsxs3("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "share-unavailable", children: [
11525
+ text3,
11526
+ share.remedy !== null ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__remedy", children: share.remedy }) : null,
11527
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__link", onClick: actions.dismissShareNotice, children: "Dismiss" })
11528
+ ] });
11529
+ }
11530
+ var REFUSED_TITLES = {
11531
+ not_found: "Check your meeting code",
11532
+ cancelled: "This meeting was cancelled",
11533
+ expired: "This meeting link has expired",
11534
+ too_early: "This meeting hasn't started yet",
11535
+ sign_in_required: "Sign in to join this meeting",
11536
+ wrong_account: "You're signed in with a different account",
11537
+ not_invited: "You aren't invited to this meeting",
11538
+ locked: "This meeting is locked",
11539
+ removed: "You can't rejoin this meeting",
11540
+ lobby_rejects: "You can't ask to join this meeting",
11541
+ meeting_full: "This meeting is full",
11542
+ platform_capacity: "Meetings are busy right now",
11543
+ verification_required: "Verify your account to join",
11544
+ registration_required: "Register to join this meeting"
11545
+ };
11546
+ function ExitScreen({ onLeave }) {
11547
+ const { phase, reason, host, snapshot, actions, call } = useMeetCore();
11548
+ const [busy, setBusy] = useState4(false);
11549
+ const failure = snapshot?.failure ?? null;
11550
+ const rejoin = /* @__PURE__ */ jsx6(
11551
+ Button,
11552
+ {
11553
+ type: "button",
11554
+ disabled: busy,
11555
+ onClick: () => {
11556
+ setBusy(true);
11557
+ void actions.rejoin().catch(() => void 0).finally(() => setBusy(false));
11558
+ },
11559
+ children: "Rejoin"
11560
+ }
11561
+ );
11562
+ const home = /* @__PURE__ */ jsx6(
11563
+ "a",
11564
+ {
11565
+ className: "mx-meet__link",
11566
+ href: "/",
11567
+ onClick: (event) => {
11568
+ if (call !== null) {
11569
+ event.preventDefault();
11570
+ actions.dismissCall();
11571
+ }
11572
+ if (onLeave !== void 0) {
11573
+ event.preventDefault();
11574
+ onLeave();
11575
+ }
11576
+ },
11577
+ children: "Go home"
11578
+ }
11579
+ );
11580
+ let title;
11581
+ let line = null;
11582
+ let notice = null;
11583
+ let actionsNode = home;
11584
+ if (phase.startsWith("refused:")) {
11585
+ const code = phase.slice("refused:".length);
11586
+ title = REFUSED_TITLES[code] ?? "You can't join this meeting";
11587
+ line = failure?.remedy ?? null;
11588
+ actionsNode = /* @__PURE__ */ jsxs3(Fragment, { children: [
11589
+ /* @__PURE__ */ jsx6(Button, { type: "button", variant: "outline", onClick: actions.retry, children: "Try again" }),
11590
+ home
11591
+ ] });
11592
+ } else {
11593
+ switch (phase) {
11594
+ case "left":
11595
+ title = "You left the meeting";
11596
+ actionsNode = /* @__PURE__ */ jsxs3(Fragment, { children: [
11597
+ rejoin,
11598
+ home
11599
+ ] });
11600
+ break;
11601
+ case "disconnected":
11602
+ title = "You lost your connection to the meeting";
11603
+ line = failure?.remedy ?? "Check your internet connection, then rejoin.";
11604
+ notice = "disconnected";
11605
+ actionsNode = /* @__PURE__ */ jsxs3(Fragment, { children: [
11606
+ rejoin,
11607
+ home
11608
+ ] });
11609
+ break;
11610
+ case "removed":
11611
+ title = "You've been removed from the meeting";
11612
+ notice = "removed";
11613
+ break;
11614
+ case "superseded":
11615
+ title = "You joined from another tab or device";
11616
+ line = "This screen left the meeting so you aren't in it twice.";
11617
+ notice = "displaced";
11618
+ actionsNode = /* @__PURE__ */ jsxs3(Fragment, { children: [
11619
+ rejoin,
11620
+ home
11621
+ ] });
11622
+ break;
11623
+ case "inactive":
11624
+ title = "You left the meeting because this tab was inactive";
11625
+ actionsNode = /* @__PURE__ */ jsxs3(Fragment, { children: [
11626
+ rejoin,
11627
+ home
11628
+ ] });
11629
+ break;
11630
+ case "unsupported":
11631
+ title = "This browser can't join meetings";
11632
+ line = "Open the link in Chrome, Edge, Firefox or Safari.";
11633
+ break;
11634
+ case "ended":
11635
+ case "record":
11636
+ title = reason === "time_limit" ? "The meeting reached its time limit" : "The meeting has ended";
11637
+ notice = "meeting-ended";
11638
+ break;
11639
+ default:
11640
+ title = "The call has ended";
11641
+ break;
11642
+ }
11643
+ }
11644
+ return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__waiting", "data-meet-exit": phase, children: [
11645
+ /* @__PURE__ */ jsx6(Avatar, { name: host?.identity.displayName ?? "", url: host?.identity.avatarUrl ?? null, size: 72 }),
11646
+ /* @__PURE__ */ jsx6("h1", { className: "mx-meet__prejoin-title", children: title }),
11647
+ line !== null ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__empty-body", children: line }) : null,
11648
+ notice !== null ? /* @__PURE__ */ jsx6("span", { role: "status", "data-meet-notice": notice, className: "mx-meet__sr-only", children: title }) : null,
11649
+ /* @__PURE__ */ jsx6("div", { className: "mx-meet__waiting-actions", children: actionsNode })
11650
+ ] });
11651
+ }
11097
11652
  function WaitingRoom({ host, onLeave }) {
11098
11653
  const { waiting, withdraw, askAgain } = useWaiting();
11654
+ const { phaseNotice } = useMeetCore();
11099
11655
  const [busy, setBusy] = useState4(false);
11100
11656
  const admission = waiting?.admission ?? "knocking";
11101
11657
  const run = (action) => () => {
@@ -11144,6 +11700,7 @@ function WaitingRoom({ host, onLeave }) {
11144
11700
  return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__waiting", children: [
11145
11701
  /* @__PURE__ */ jsx6(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 72 }),
11146
11702
  /* @__PURE__ */ jsx6("h1", { className: "mx-meet__prejoin-title", children: title }),
11703
+ phaseNotice === "sent-to-waiting" ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__empty-body", role: "status", "data-meet-notice": "sent-to-waiting", children: "The host moved you back to the waiting room" }) : null,
11147
11704
  line !== null ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__empty-body", role: "status", "data-meet-notice": "host-absent", children: line }) : null,
11148
11705
  /* @__PURE__ */ jsx6("div", { className: "mx-meet__waiting-actions", children: actions })
11149
11706
  ] });
@@ -11638,10 +12195,17 @@ function MeetingSkinRootBody({
11638
12195
  host
11639
12196
  }) {
11640
12197
  const snapshot = useMeetSnapshot();
11641
- const phase = snapshot?.phase ?? "idle";
12198
+ const phase = snapshot?.phase ?? "resolving";
11642
12199
  useEffect8(() => {
11643
12200
  host.meetings.watch(roomName);
11644
12201
  }, [host, roomName]);
12202
+ useEffect8(() => {
12203
+ const now = host.store.snapshot();
12204
+ if (now.phase !== "resolving" && now.roomName !== null && now.roomName !== roomName && now.call === null) {
12205
+ host.store.dispatch({ type: "reset" });
12206
+ }
12207
+ if (host.store.snapshot().phase === "resolving") host.store.dispatch({ type: "resolved" });
12208
+ }, [host, roomName, phase]);
11645
12209
  useEffect8(() => {
11646
12210
  if (given === void 0) return;
11647
12211
  if (host.store.snapshot().meeting !== null) return;
@@ -11653,7 +12217,7 @@ function MeetingSkinRootBody({
11653
12217
  host.meetings.watchMeeting(asMeetingId(watchedId));
11654
12218
  }, [host, watchedId]);
11655
12219
  const record = snapshot?.meeting ?? given ?? null;
11656
- if (hasEnded(record) && phase !== "connected" && phase !== "reconnecting") {
12220
+ if (hasEnded(record) && !isLivePhase(phase)) {
11657
12221
  return /* @__PURE__ */ jsx8(MeetingRecordView, { meeting: record });
11658
12222
  }
11659
12223
  return /* @__PURE__ */ jsx8(
@@ -11670,11 +12234,49 @@ function MeetingSkinRootBody({
11670
12234
  }
11671
12235
 
11672
12236
  // src/react/calls-ui.tsx
11673
- import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
12237
+ import { useEffect as useEffect9 } from "react";
12238
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
11674
12239
  function IncomingCallHost() {
11675
12240
  const calls = useCalls();
11676
- if (calls.incoming.length === 0) return null;
11677
- return /* @__PURE__ */ jsx9("div", { className: "mx-meet__ringing-stack", children: calls.incoming.map((invite) => /* @__PURE__ */ jsx9(IncomingCallCard, { invite }, invite.id)) });
12241
+ const { call, phase, actions } = useMeetCore();
12242
+ useEffect9(() => {
12243
+ if (call !== null && phase === "left") actions.dismissCall();
12244
+ }, [call, phase, actions]);
12245
+ const outcome = call !== null && (phase === "call_declined" || phase === "call_missed" || phase === "call_busy" || phase === "answered_elsewhere");
12246
+ useEffect9(() => {
12247
+ if (!outcome) return void 0;
12248
+ const timer = setTimeout(() => actions.dismissCall(), 5e3);
12249
+ return () => clearTimeout(timer);
12250
+ }, [outcome, actions]);
12251
+ const inCall = call !== null && (phase === "joining" || phase === "in_call" || phase === "disconnected" || phase === "removed" || phase === "superseded");
12252
+ return /* @__PURE__ */ jsxs5(Fragment3, { children: [
12253
+ inCall && call !== null ? /* @__PURE__ */ jsx9("div", { className: "mx-meet__call-overlay", "data-meet-call": call.invite.id, children: /* @__PURE__ */ jsx9(MeetRoot, { children: /* @__PURE__ */ jsx9(MeetSkinRoom, { roomName: call.invite.roomName, onLeave: actions.dismissCall }) }) }) : null,
12254
+ calls.incoming.length > 0 || call !== null && (phase === "ringing_out" || outcome) ? /* @__PURE__ */ jsxs5("div", { className: "mx-meet__ringing-stack", children: [
12255
+ call !== null && phase === "ringing_out" ? /* @__PURE__ */ jsx9(OutgoingCallCard, { invite: call.invite }) : null,
12256
+ outcome && call !== null ? /* @__PURE__ */ jsx9(CallOutcomeCard, { invite: call.invite }) : null,
12257
+ calls.incoming.map((invite) => /* @__PURE__ */ jsx9(IncomingCallCard, { invite }, invite.id))
12258
+ ] }) : null
12259
+ ] });
12260
+ }
12261
+ function OutgoingCallCard({ invite }) {
12262
+ const calls = useCalls();
12263
+ return /* @__PURE__ */ jsxs5("section", { className: "mx-meet__ringing", role: "status", "aria-label": "Calling", "data-meet-notice": "ringing-out", children: [
12264
+ /* @__PURE__ */ jsx9(Avatar, { name: invite.callerName, url: invite.callerAvatarUrl, size: 56 }),
12265
+ /* @__PURE__ */ jsxs5("div", { className: "mx-meet__ringing-text", children: [
12266
+ /* @__PURE__ */ jsx9("strong", { children: "Calling\u2026" }),
12267
+ /* @__PURE__ */ jsx9("span", { children: invite.mode === "video" ? "Video call" : "Voice call" })
12268
+ ] }),
12269
+ /* @__PURE__ */ jsx9("div", { className: "mx-meet__ringing-actions", children: /* @__PURE__ */ jsx9(ControlButton, { label: "Cancel call", danger: true, onClick: () => void calls.cancel(invite), children: /* @__PURE__ */ jsx9(PhoneOffIcon, {}) }) })
12270
+ ] });
12271
+ }
12272
+ function CallOutcomeCard({ invite }) {
12273
+ const { phase, actions, call } = useMeetCore();
12274
+ const outgoing = call?.direction === "outgoing";
12275
+ const text3 = phase === "call_declined" ? "Call declined" : phase === "call_busy" ? "They're on another call" : phase === "answered_elsewhere" ? "Answered on another device" : outgoing ? "No answer" : `Missed call from ${invite.callerName}`;
12276
+ return /* @__PURE__ */ jsxs5("section", { className: "mx-meet__ringing", role: "status", "aria-label": text3, "data-meet-call-outcome": phase, children: [
12277
+ /* @__PURE__ */ jsx9("div", { className: "mx-meet__ringing-text", children: /* @__PURE__ */ jsx9("strong", { children: text3 }) }),
12278
+ /* @__PURE__ */ jsx9("div", { className: "mx-meet__ringing-actions", children: /* @__PURE__ */ jsx9(ControlButton, { label: "Close", onClick: actions.dismissCall, children: /* @__PURE__ */ jsx9(PhoneOffIcon, {}) }) })
12279
+ ] });
11678
12280
  }
11679
12281
  function IncomingCallCard({ invite }) {
11680
12282
  const calls = useCalls();
@@ -11849,28 +12451,6 @@ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
11849
12451
  if (!response.ok) throw await refusal(response, operation);
11850
12452
  return await response.json();
11851
12453
  }
11852
-
11853
- // src/core/reasons.ts
11854
- var JOIN_REFUSAL_REASONS = [
11855
- "not_found",
11856
- "cancelled",
11857
- "ended",
11858
- "expired",
11859
- "too_early",
11860
- "sign_in_required",
11861
- "wrong_account",
11862
- "not_invited",
11863
- "locked",
11864
- "removed",
11865
- "lobby_rejects",
11866
- "meeting_full",
11867
- "platform_capacity",
11868
- "verification_required",
11869
- "registration_required"
11870
- ];
11871
- function isJoinRefusalReason(value) {
11872
- return typeof value === "string" && JOIN_REFUSAL_REASONS.includes(value);
11873
- }
11874
12454
  export {
11875
12455
  ATTENDANCE_CSV_HEADER,
11876
12456
  CALL_INVITE_KIND,
@@ -11889,11 +12469,13 @@ export {
11889
12469
  GUEST_RECORD_NOT_SHARED,
11890
12470
  GuestRecordAccessControl,
11891
12471
  IDLE_MEDIA,
12472
+ IDLE_PHASES,
11892
12473
  IDLE_TRACK,
11893
12474
  IncomingCallCard,
11894
12475
  IncomingCallHost,
11895
12476
  JOIN_REFUSAL_REASONS,
11896
12477
  LISTEN_ONLY_NOTICE,
12478
+ LIVE_PHASES,
11897
12479
  MEETING_INVITE_KIND,
11898
12480
  MEETING_INVITE_VERSION,
11899
12481
  MEETING_PASS_HEADER,
@@ -11915,6 +12497,7 @@ export {
11915
12497
  NOTE_TAKER_IDENTITY_PREFIX,
11916
12498
  NOTE_TAKER_METADATA_KEY,
11917
12499
  NO_GRANTS,
12500
+ NO_SHARE,
11918
12501
  ORGANIZATION_HEADER,
11919
12502
  PARTICIPANT_CAN_RECORD_ATTRIBUTE,
11920
12503
  PARTICIPANT_ROLE_ATTRIBUTE,
@@ -11929,9 +12512,11 @@ export {
11929
12512
  TABLES,
11930
12513
  TRANSCRIPTION_ATTRIBUTES,
11931
12514
  TRANSCRIPTION_TOPIC,
12515
+ WAITING_PHASES,
11932
12516
  absenceSentence,
11933
12517
  activeWrapUpSummary,
11934
12518
  activityAttention,
12519
+ admissionPhase,
11935
12520
  announceMeeting,
11936
12521
  appPanelOf,
11937
12522
  applyCollabPacket,
@@ -12001,10 +12586,13 @@ export {
12001
12586
  isJoinRefusalReason,
12002
12587
  isLinkFollower,
12003
12588
  isListenOnly,
12589
+ isLivePhase,
12004
12590
  isNoteTaker,
12005
12591
  isObserverRole,
12006
12592
  isRealName,
12593
+ isRefusedPhase,
12007
12594
  isRoomEventTopic,
12595
+ isWaitingPhase,
12008
12596
  learnName,
12009
12597
  learnNames,
12010
12598
  listenOnlyNotice,
@@ -12029,6 +12617,7 @@ export {
12029
12617
  mergeRecord,
12030
12618
  microphoneNoticeCode,
12031
12619
  moveToRoom,
12620
+ nextPhase,
12032
12621
  nextTranslationBatch,
12033
12622
  normalizeDeviceError,
12034
12623
  normalizeMeetError,
@@ -12061,10 +12650,13 @@ export {
12061
12650
  pushMeetingToGoogleCalendar,
12062
12651
  readServerRefusal,
12063
12652
  readableTranscript,
12653
+ reasonOf,
12064
12654
  recordingLabel,
12065
12655
  recordingNotice,
12066
12656
  recordingOffsetMs,
12657
+ refusalPhase,
12067
12658
  rememberedMeetingPass,
12659
+ resolveExit,
12068
12660
  roleOf,
12069
12661
  roomTopic,
12070
12662
  rootAttributes,