@ai-matrx/meet 0.10.7 → 0.11.0

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. */
@@ -3004,8 +3213,15 @@ var NO_GRANTS = {
3004
3213
  canPublish: false,
3005
3214
  canSubscribe: false
3006
3215
  };
3216
+ var NO_SHARE = { state: "none", reason: null, remedy: null, at: 0 };
3007
3217
  var EMPTY = {
3008
- phase: "idle",
3218
+ phase: "resolving",
3219
+ reason: null,
3220
+ phaseNotice: null,
3221
+ roomName: null,
3222
+ connection: "stable",
3223
+ share: NO_SHARE,
3224
+ call: null,
3009
3225
  waiting: null,
3010
3226
  meeting: null,
3011
3227
  localIdentity: null,
@@ -3069,8 +3285,20 @@ function createMeetStore(initial = {}) {
3069
3285
  return () => listeners.delete(listener);
3070
3286
  },
3071
3287
  patch(next) {
3288
+ if ("phase" in next || "reason" in next || "phaseNotice" in next) {
3289
+ throw new TypeError("meet store: the phase moves only through store.dispatch(event)");
3290
+ }
3072
3291
  commit({ ...state, ...next });
3073
3292
  },
3293
+ dispatch(event, detail) {
3294
+ const to = nextPhase(state.phase, event);
3295
+ if (to === null) return false;
3296
+ const reason = detail?.reason !== void 0 ? detail.reason : reasonOf(to);
3297
+ const phaseNotice = detail?.notice ?? null;
3298
+ if (to === state.phase && reason === state.reason && phaseNotice === state.phaseNotice) return true;
3299
+ commit({ ...state, phase: to, reason, phaseNotice });
3300
+ return true;
3301
+ },
3074
3302
  setParticipants(participants) {
3075
3303
  const names = learnNames(state.names, participants);
3076
3304
  const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
@@ -3388,7 +3616,7 @@ function createMeetingSession(options) {
3388
3616
  }
3389
3617
  function apply(read) {
3390
3618
  const snapshot = store.snapshot();
3391
- if (snapshot.phase !== "lobby") return;
3619
+ if (!isWaitingPhase(snapshot.phase)) return;
3392
3620
  const admission = read.admission_state ?? null;
3393
3621
  const ended = admission === "ended" || read.session_phase === "ended" || read.meeting_phase === "ended";
3394
3622
  if (admission === "admitted" && !ended) {
@@ -3405,6 +3633,7 @@ function createMeetingSession(options) {
3405
3633
  if (read.meeting_phase === "ended" && meeting !== null && meeting.endedAt === null) {
3406
3634
  store.patch({ meeting: { ...meeting, endedAt: (/* @__PURE__ */ new Date()).toISOString() } });
3407
3635
  }
3636
+ store.dispatch({ type: "admission", admission: "ended" }, { reason: "ended" });
3408
3637
  return;
3409
3638
  }
3410
3639
  if (admission === null) return;
@@ -3416,6 +3645,7 @@ function createMeetingSession(options) {
3416
3645
  pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
3417
3646
  liveVersion: read.live_version ?? 0
3418
3647
  });
3648
+ store.dispatch({ type: "admission", admission });
3419
3649
  if (admission === "denied" || admission === "expired" || admission === "withdrawn" || admission === "removed") {
3420
3650
  stopWaiting();
3421
3651
  }
@@ -3511,7 +3741,7 @@ function createMeetingSession(options) {
3511
3741
  // 🚨 PAYLOAD-FREE. Every receiver reads again; a spoofed hint only causes a read.
3512
3742
  event: MEET_EVENTS.stateChanged,
3513
3743
  onMessage: () => {
3514
- if (store.snapshot().phase === "lobby") void readState2();
3744
+ if (isWaitingPhase(store.snapshot().phase)) void readState2();
3515
3745
  void refreshQueue();
3516
3746
  }
3517
3747
  }
@@ -3523,7 +3753,7 @@ function createMeetingSession(options) {
3523
3753
  if (meeting === null) return;
3524
3754
  const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
3525
3755
  store.patch({ meeting: fresh, locked: fresh.locked });
3526
- if (store.snapshot().phase === "lobby") void readState2();
3756
+ if (isWaitingPhase(store.snapshot().phase)) void readState2();
3527
3757
  }
3528
3758
  });
3529
3759
  },
@@ -3690,10 +3920,34 @@ function createMeetingSession(options) {
3690
3920
  },
3691
3921
  stopWaiting,
3692
3922
  readState: readState2,
3923
+ async readExit(connection) {
3924
+ const meeting = store.snapshot().meeting;
3925
+ const pass = options.resolvePass?.() ?? null;
3926
+ if (meeting === null || pass === null) return void 0;
3927
+ try {
3928
+ const response = await options.api.request({
3929
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}&connection=${encodeURIComponent(connection)}`,
3930
+ method: "GET",
3931
+ requireAuth: false,
3932
+ operation: "meet.state(exit)",
3933
+ headers: { [MEETING_PASS_HEADER]: pass }
3934
+ });
3935
+ if (!response.ok) return null;
3936
+ const read = await response.json();
3937
+ return {
3938
+ exitReason: read.exit_reason ?? null,
3939
+ sessionPhase: read.session_phase ?? null,
3940
+ meetingPhase: read.meeting_phase ?? null
3941
+ };
3942
+ } catch {
3943
+ return null;
3944
+ }
3945
+ },
3693
3946
  async withdraw() {
3694
3947
  stopWaiting();
3695
3948
  await postLeave("withdraw");
3696
3949
  patchWaiting({ admission: "withdrawn" });
3950
+ store.dispatch({ type: "admission", admission: "withdrawn" });
3697
3951
  hint();
3698
3952
  },
3699
3953
  async askAgain() {
@@ -4177,6 +4431,9 @@ var TRANSCRIPTION_ATTRIBUTES = {
4177
4431
  };
4178
4432
 
4179
4433
  // src/core/room.ts
4434
+ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
4435
+ return /administrator|policy/i.test(remedy) ? remedy : SCREEN_SHARE_BLOCKED_REMEDY;
4436
+ }
4180
4437
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
4181
4438
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
4182
4439
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
@@ -4244,7 +4501,7 @@ function createRoomEngine(options) {
4244
4501
  let disposed = false;
4245
4502
  let moving = false;
4246
4503
  const confirmingEnds = /* @__PURE__ */ new WeakSet();
4247
- async function leaveRoom() {
4504
+ async function dropRoom() {
4248
4505
  const target = driver;
4249
4506
  driver = null;
4250
4507
  if (current !== null) {
@@ -4253,9 +4510,14 @@ function createRoomEngine(options) {
4253
4510
  }
4254
4511
  store.setLocalTrack("microphone", IDLE_TRACK);
4255
4512
  store.setLocalTrack("camera", IDLE_TRACK);
4256
- store.patch({ phase: "left" });
4513
+ store.patch({ share: NO_SHARE, connection: "stable" });
4257
4514
  await target?.disconnect();
4258
4515
  }
4516
+ async function leaveRoom() {
4517
+ store.dispatch({ type: "leave" });
4518
+ await dropRoom();
4519
+ options.onLeft?.(null);
4520
+ }
4259
4521
  async function confirmMeetingEnd() {
4260
4522
  const meeting = store.snapshot().meeting;
4261
4523
  const target = driver;
@@ -4277,7 +4539,9 @@ function createRoomEngine(options) {
4277
4539
  const latest = store.snapshot().meeting;
4278
4540
  if (latest === null) return;
4279
4541
  store.patch({ meeting: { ...latest, endedAt: verified.endedAt } });
4280
- await leaveRoom();
4542
+ store.dispatch({ type: "ended" }, { reason: "ended" });
4543
+ await dropRoom();
4544
+ options.onLeft?.("ended");
4281
4545
  } catch (error) {
4282
4546
  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
4547
  } finally {
@@ -4289,12 +4553,12 @@ function createRoomEngine(options) {
4289
4553
  options.onDiagnostic?.(remedy === void 0 ? { level, message } : { level, message, remedy });
4290
4554
  }
4291
4555
  let ambiguousShareDenials = 0;
4292
- function fail(error, operation) {
4556
+ function failJoin(error, operation) {
4293
4557
  const normalized = normalizeMeetError(error, operation);
4294
4558
  store.patch({
4295
- phase: "failed",
4296
4559
  failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
4297
4560
  });
4561
+ store.dispatch({ type: "join_failed", reason: normalized.reason ?? null });
4298
4562
  report(
4299
4563
  normalized.code === "session-unavailable" ? "warn" : "error",
4300
4564
  normalized.message,
@@ -4302,6 +4566,14 @@ function createRoomEngine(options) {
4302
4566
  );
4303
4567
  return normalized;
4304
4568
  }
4569
+ async function lostRoom(error, operation) {
4570
+ const normalized = normalizeMeetError(error, operation);
4571
+ store.patch({ failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason } });
4572
+ await dropRoom();
4573
+ store.dispatch({ type: "exit", to: "disconnected" });
4574
+ report("error", normalized.message, normalized.remedy);
4575
+ return normalized;
4576
+ }
4305
4577
  const handlers = {
4306
4578
  participants(list) {
4307
4579
  const joinedAt = new Date(now()).toISOString();
@@ -4344,9 +4616,12 @@ function createRoomEngine(options) {
4344
4616
  store.setQuality(asParticipantIdentity(who), value);
4345
4617
  },
4346
4618
  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" });
4619
+ if (state === "connected") {
4620
+ if (store.snapshot().connection !== "stable") store.patch({ connection: "stable" });
4621
+ store.dispatch({ type: "connected" });
4622
+ } else if (state === "reconnecting") {
4623
+ store.patch({ connection: "reconnecting" });
4624
+ }
4350
4625
  },
4351
4626
  transcription(event) {
4352
4627
  const snapshot = store.snapshot();
@@ -4473,17 +4748,48 @@ function createRoomEngine(options) {
4473
4748
  }
4474
4749
  },
4475
4750
  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);
4751
+ if (moving || driver === null) return;
4752
+ void resolveExitAndApply(reason);
4483
4753
  }
4484
4754
  };
4755
+ async function resolveExitAndApply(hint) {
4756
+ const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
4757
+ await dropRoom();
4758
+ let server;
4759
+ if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
4760
+ server = void 0;
4761
+ } else {
4762
+ try {
4763
+ server = await options.readExit(connection);
4764
+ } catch {
4765
+ server = null;
4766
+ }
4767
+ }
4768
+ if (disposed) return;
4769
+ const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
4770
+ store.patch({ connection: resolved.connection });
4771
+ if (resolved.to === "waiting:knocking") {
4772
+ store.patch({
4773
+ waiting: {
4774
+ admission: "knocking",
4775
+ admittersPresent: null,
4776
+ reknockAllowed: true,
4777
+ knockExpiresAt: null,
4778
+ pollSeconds: store.snapshot().waiting?.pollSeconds ?? 3,
4779
+ liveVersion: 0
4780
+ }
4781
+ });
4782
+ }
4783
+ store.dispatch({ type: "exit", to: resolved.to }, { reason: resolved.reason, notice: resolved.notice });
4784
+ report(
4785
+ resolved.to === "disconnected" ? "warn" : "info",
4786
+ `meet: the room connection ended (${hint ?? "no LiveKit reason"}) \u2192 ${resolved.to}`,
4787
+ resolved.to === "disconnected" ? "Press Rejoin; the meeting is still going." : "The screen says what happened."
4788
+ );
4789
+ options.onLeft?.(hint);
4790
+ }
4485
4791
  async function connect(request) {
4486
- store.patch({ phase: "requesting-token", failure: null, waiting: null });
4792
+ store.patch({ failure: null, waiting: null, roomName: request.roomName, share: NO_SHARE, connection: "stable" });
4487
4793
  const token = await tokens.get({
4488
4794
  roomName: request.roomName,
4489
4795
  identity,
@@ -4506,10 +4812,10 @@ function createRoomEngine(options) {
4506
4812
  }
4507
4813
  });
4508
4814
  if (token.lobby || !token.canSubscribe) {
4815
+ const admission = token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking";
4509
4816
  store.patch({
4510
- phase: "lobby",
4511
4817
  waiting: {
4512
- admission: token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking",
4818
+ admission,
4513
4819
  admittersPresent: null,
4514
4820
  reknockAllowed: true,
4515
4821
  knockExpiresAt: null,
@@ -4517,9 +4823,10 @@ function createRoomEngine(options) {
4517
4823
  liveVersion: 0
4518
4824
  }
4519
4825
  });
4826
+ store.dispatch({ type: "admission", admission });
4520
4827
  return;
4521
4828
  }
4522
- store.patch({ phase: "connecting", waiting: null });
4829
+ store.patch({ waiting: null });
4523
4830
  const created = await options.driverFactory(handlers);
4524
4831
  if (disposed) {
4525
4832
  await created.disconnect();
@@ -4527,7 +4834,8 @@ function createRoomEngine(options) {
4527
4834
  }
4528
4835
  driver = created;
4529
4836
  await created.connect(token.serverUrl, token.token);
4530
- store.patch({ phase: "connected", failure: null });
4837
+ store.patch({ failure: null, connection: "stable" });
4838
+ store.dispatch({ type: "connected" });
4531
4839
  store.setLocalTrack("microphone", IDLE_TRACK);
4532
4840
  store.setLocalTrack("camera", IDLE_TRACK);
4533
4841
  const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
@@ -4598,9 +4906,17 @@ function createRoomEngine(options) {
4598
4906
  return {
4599
4907
  join(request) {
4600
4908
  if (joining !== null) return joining;
4909
+ if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
4910
+ report(
4911
+ "warn",
4912
+ `meet.join: cannot join from "${store.snapshot().phase}"`,
4913
+ "Join is offered from the pre-join, left, disconnected and replaced screens only."
4914
+ );
4915
+ return Promise.resolve();
4916
+ }
4601
4917
  lastRequest = request;
4602
4918
  joining = connect(request).catch((error) => {
4603
- throw fail(error, "meet.join");
4919
+ throw failJoin(error, "meet.join");
4604
4920
  }).finally(() => {
4605
4921
  joining = null;
4606
4922
  });
@@ -4617,10 +4933,16 @@ function createRoomEngine(options) {
4617
4933
  return;
4618
4934
  }
4619
4935
  if (joining !== null) return joining;
4936
+ const phase = store.snapshot().phase;
4937
+ const moved = phase === "joining" || store.dispatch({ type: "join", click: true }) || store.dispatch({ type: "admitted" });
4938
+ if (!moved) {
4939
+ report("warn", `meet.rejoin: cannot rejoin from "${phase}"`, "Rejoin is offered after leaving, a lost connection, or admission.");
4940
+ return;
4941
+ }
4620
4942
  tokens.invalidate(request.roomName);
4621
4943
  current = null;
4622
4944
  joining = connect(request).catch((error) => {
4623
- throw fail(error, "meet.rejoin");
4945
+ throw failJoin(error, "meet.rejoin");
4624
4946
  }).finally(() => {
4625
4947
  joining = null;
4626
4948
  });
@@ -4665,11 +4987,11 @@ function createRoomEngine(options) {
4665
4987
  joining = connect(lastRequest).finally(() => {
4666
4988
  joining = null;
4667
4989
  });
4668
- await joining.catch((again) => {
4669
- throw fail(again, "meet.moveToBreakout");
4990
+ await joining.catch(async (again) => {
4991
+ throw await lostRoom(again, "meet.moveToBreakout");
4670
4992
  });
4671
4993
  } else {
4672
- throw fail(error, "meet.moveToBreakout");
4994
+ throw await lostRoom(error, "meet.moveToBreakout");
4673
4995
  }
4674
4996
  throw normalized;
4675
4997
  } finally {
@@ -4677,6 +4999,9 @@ function createRoomEngine(options) {
4677
4999
  }
4678
5000
  },
4679
5001
  leave: leaveRoom,
5002
+ backToPrejoin() {
5003
+ store.dispatch({ type: "back_to_prejoin", click: true });
5004
+ },
4680
5005
  // Turning a device on mid-meeting is the SAME operation as turning it on
4681
5006
  // during the join, so it runs the same function. A failure here used to
4682
5007
  // stamp the room `failed` — a busy microphone would blank a live meeting.
@@ -4690,21 +5015,41 @@ function createRoomEngine(options) {
4690
5015
  },
4691
5016
  async setScreenShareEnabled(enabled) {
4692
5017
  if (driver === null) return;
5018
+ if (enabled && driver.supportsScreenShare?.() === false) {
5019
+ store.patch({ share: { state: "refused", reason: "unsupported", remedy: null, at: now() } });
5020
+ report("warn", "meet: this browser cannot share a screen", "Share from a desktop browser (Chrome, Edge, Firefox or Safari).");
5021
+ return;
5022
+ }
5023
+ if (enabled) store.patch({ share: { state: "picking", reason: null, remedy: null, at: now() } });
4693
5024
  try {
4694
5025
  await driver.setScreenShareEnabled(enabled);
4695
5026
  ambiguousShareDenials = 0;
5027
+ store.patch({ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now() } });
4696
5028
  } catch (error) {
4697
5029
  const kind = classifyScreenShareError(error);
4698
5030
  if (kind === "ambiguous") {
4699
5031
  ambiguousShareDenials += 1;
4700
- if (ambiguousShareDenials < 2) return;
5032
+ if (ambiguousShareDenials < 2) {
5033
+ store.patch({ share: NO_SHARE });
5034
+ return;
5035
+ }
4701
5036
  }
5037
+ ambiguousShareDenials = 0;
4702
5038
  const normalized = normalizeScreenShareError(error, "meet.setScreenShareEnabled");
4703
5039
  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() } });
5040
+ store.patch({
5041
+ share: {
5042
+ state: "refused",
5043
+ reason: kind === "other" ? "failed" : "blocked",
5044
+ remedy: kind === "other" ? normalized.remedy : SCREEN_SHARE_BLOCKED_REMEDY_FOR(normalized.remedy),
5045
+ at: now()
5046
+ }
5047
+ });
4706
5048
  }
4707
5049
  },
5050
+ dismissShareNotice() {
5051
+ if (store.snapshot().share.state === "refused") store.patch({ share: NO_SHARE });
5052
+ },
4708
5053
  async raiseHand(raised) {
4709
5054
  const local = store.snapshot().localIdentity;
4710
5055
  if (local !== null) store.setHand(local, raised);
@@ -5206,7 +5551,8 @@ function createLiveKitDriver(handlers, options = {}) {
5206
5551
  }).on(RoomEvent.Disconnected, (reason) => {
5207
5552
  audioContainer?.remove();
5208
5553
  audioContainer = null;
5209
- handlers.disconnected(reason === void 0 ? null : String(reason));
5554
+ const named = reason === void 0 ? null : module?.DisconnectReason?.[reason] ?? String(reason);
5555
+ handlers.disconnected(named);
5210
5556
  });
5211
5557
  try {
5212
5558
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -5594,7 +5940,9 @@ function MeetRuntime(props) {
5594
5940
  driverFactory: (handlers) => (driverRef.current ?? createLiveKitDriver)(handlers),
5595
5941
  onDiagnostic: report,
5596
5942
  effectsStorage,
5597
- resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null
5943
+ resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null,
5944
+ // §3.4: on every LiveKit disconnect, ask the server what happened to this connection.
5945
+ readExit: (connection) => meetings.readExit(connection)
5598
5946
  });
5599
5947
  const meetings = createMeetingSession({
5600
5948
  manager,
@@ -5629,17 +5977,17 @@ function MeetRuntime(props) {
5629
5977
  const inRoom = createInRoomClient({ api, store, resolveRoomToken });
5630
5978
  store.subscribe(() => {
5631
5979
  const snapshot = store.snapshot();
5632
- if (snapshot.phase === "lobby" && snapshot.waiting !== null && (snapshot.waiting.admission === "knocking" || snapshot.waiting.admission === "waiting_host")) {
5980
+ if (isWaitingPhase(snapshot.phase) && snapshot.waiting !== null && (snapshot.waiting.admission === "knocking" || snapshot.waiting.admission === "waiting_host")) {
5633
5981
  meetings.startWaiting();
5634
- } else if (snapshot.phase !== "lobby") {
5982
+ } else if (!isWaitingPhase(snapshot.phase)) {
5635
5983
  meetings.stopWaiting();
5636
5984
  }
5637
5985
  });
5638
5986
  let chatReadFor = null;
5639
5987
  store.subscribe(() => {
5640
5988
  const snapshot = store.snapshot();
5641
- if (snapshot.phase !== "connected" || snapshot.meeting === null) {
5642
- if (snapshot.phase === "left" || snapshot.phase === "idle") chatReadFor = null;
5989
+ if (snapshot.phase !== "in_call" || snapshot.meeting === null) {
5990
+ if (snapshot.phase === "left" || snapshot.phase === "prejoin" || snapshot.phase === "resolving") chatReadFor = null;
5643
5991
  return;
5644
5992
  }
5645
5993
  if (chatReadFor === snapshot.meeting.id) return;
@@ -5692,6 +6040,10 @@ function MeetRuntime(props) {
5692
6040
  store,
5693
6041
  identity,
5694
6042
  onIncoming: (invite) => incomingRef.current?.(invite),
6043
+ // §3.6: my call was answered — the caller joins the call's room on the same machine.
6044
+ onAnswered: (invite) => {
6045
+ void engine.join({ roomName: invite.roomName, mode: invite.mode }).catch(() => void 0);
6046
+ },
5695
6047
  onDiagnostic: report,
5696
6048
  ...props.ringTimeoutMs !== void 0 ? { ringTimeoutMs: props.ringTimeoutMs } : {}
5697
6049
  });
@@ -5831,43 +6183,12 @@ function endedRecord(snapshot, given) {
5831
6183
  function observedPhase(input) {
5832
6184
  const { snapshot } = input;
5833
6185
  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
- }
6186
+ const phase = snapshot.phase;
6187
+ if (endedRecord(snapshot, input.meeting ?? null) && !isLivePhase(phase) && !isCallPhase(phase)) return "ended";
6188
+ return phase;
5853
6189
  }
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
- }
6190
+ function isCallPhase(phase) {
6191
+ return phase.startsWith("ringing_") || phase.startsWith("call_") || phase === "answered_elsewhere";
5871
6192
  }
5872
6193
  var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
5873
6194
  function observe(input) {
@@ -5880,10 +6201,10 @@ function observe(input) {
5880
6201
  const deviceInPlay = phase === "joining" || inCall;
5881
6202
  return {
5882
6203
  phase,
5883
- reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
6204
+ reason: phase === "ended" ? snapshot?.reason ?? null : snapshot?.reason ?? reasonOf(phase),
5884
6205
  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,
6206
+ connection: snapshot?.connection ?? "stable",
6207
+ hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
5887
6208
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
5888
6209
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
5889
6210
  audioBlocked: null,
@@ -6684,7 +7005,7 @@ function useRoom() {
6684
7005
  (participant) => participant.identity === snapshot.localIdentity
6685
7006
  ) ?? null;
6686
7007
  return {
6687
- phase: snapshot?.phase ?? "idle",
7008
+ phase: snapshot?.phase ?? "resolving",
6688
7009
  participants: snapshot?.participants ?? [],
6689
7010
  failure: snapshot?.failure ?? null,
6690
7011
  local,
@@ -8035,7 +8356,7 @@ function useSharedFocus(panelKey) {
8035
8356
  const snapshot = useMeetSnapshot();
8036
8357
  const focus = snapshot?.collab.focus ?? null;
8037
8358
  const mine = focus !== null && focus.panel === panelKey ? focus : null;
8038
- const canMove = !isObserverRole(snapshot?.grants.role) && snapshot?.phase === "connected";
8359
+ const canMove = !isObserverRole(snapshot?.grants.role) && snapshot?.phase === "in_call";
8039
8360
  return {
8040
8361
  section: mine?.section || null,
8041
8362
  byName: mine?.byName || null,
@@ -8111,6 +8432,36 @@ function useCollabPanelRequest(onRequest) {
8111
8432
  var MEET_REACTIONS = ["\u{1F496}", "\u{1F44D}", "\u{1F389}", "\u{1F44F}", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F914}", "\u{1F44E}"];
8112
8433
 
8113
8434
  // src/react/facade.ts
8435
+ var NO_ACTIONS = {
8436
+ join: async () => void 0,
8437
+ rejoin: async () => void 0,
8438
+ leave: async () => void 0,
8439
+ backToPrejoin: () => void 0,
8440
+ retry: () => void 0,
8441
+ resolved: () => void 0,
8442
+ dismissShareNotice: () => void 0,
8443
+ dismissCall: () => void 0
8444
+ };
8445
+ function actionsFor(host) {
8446
+ if (host === null) return NO_ACTIONS;
8447
+ const { engine, store } = host;
8448
+ return {
8449
+ join: (request) => engine.join(request),
8450
+ rejoin: () => engine.rejoin(),
8451
+ leave: () => engine.leave(),
8452
+ backToPrejoin: () => engine.backToPrejoin(),
8453
+ retry: () => {
8454
+ store.dispatch({ type: "retry", click: true });
8455
+ },
8456
+ resolved: () => {
8457
+ store.dispatch({ type: "resolved" });
8458
+ },
8459
+ dismissShareNotice: () => engine.dismissShareNotice(),
8460
+ dismissCall: () => {
8461
+ if (store.dispatch({ type: "reset" })) store.patch({ call: null });
8462
+ }
8463
+ };
8464
+ }
8114
8465
  function useMeetCore() {
8115
8466
  const host = useMeetHost();
8116
8467
  const snapshot = useMeetSnapshot();
@@ -8118,9 +8469,24 @@ function useMeetCore() {
8118
8469
  const role = useRole();
8119
8470
  const isHost = useIsHost();
8120
8471
  const observation = useCoreObservation();
8472
+ const actions = useMemo5(() => actionsFor(host), [host]);
8121
8473
  return useMemo5(
8122
- () => ({ host, snapshot, devices, role, isHost, observation }),
8123
- [host, snapshot, devices, role, isHost, observation]
8474
+ () => ({
8475
+ host,
8476
+ snapshot,
8477
+ devices,
8478
+ role,
8479
+ isHost,
8480
+ observation,
8481
+ phase: snapshot?.phase ?? "resolving",
8482
+ reason: snapshot?.reason ?? null,
8483
+ phaseNotice: snapshot?.phaseNotice ?? null,
8484
+ connection: snapshot?.connection ?? "stable",
8485
+ share: snapshot?.share ?? NO_SHARE,
8486
+ call: snapshot?.call ?? null,
8487
+ actions
8488
+ }),
8489
+ [host, snapshot, devices, role, isHost, observation, actions]
8124
8490
  );
8125
8491
  }
8126
8492
 
@@ -11051,11 +11417,52 @@ function MeetSkinRoom({
11051
11417
  headerControls,
11052
11418
  observe: observe2 = false
11053
11419
  }) {
11054
- const { host, snapshot, isHost } = useMeetCore();
11420
+ const core = useMeetCore();
11421
+ const { host, snapshot, isHost, phase } = core;
11055
11422
  const elapsed = useElapsed(snapshot?.meeting?.startedAt ?? null);
11056
- const phase = snapshot?.phase ?? "idle";
11057
11423
  if (host === null) return null;
11058
- if (phase === "idle" || phase === "failed" || phase === "left") {
11424
+ const screen = (() => {
11425
+ if (isRefusedPhase(phase)) return "refused";
11426
+ switch (phase) {
11427
+ case "resolving":
11428
+ return "resolving";
11429
+ case "prejoin":
11430
+ case "guest_name":
11431
+ return "prejoin";
11432
+ case "joining":
11433
+ case "in_call":
11434
+ return "room";
11435
+ case "waiting:host_not_started":
11436
+ case "waiting:knocking":
11437
+ case "denied":
11438
+ case "knock_expired":
11439
+ return "waiting";
11440
+ case "left":
11441
+ return snapshot?.waiting?.admission === "withdrawn" ? "waiting" : "exit";
11442
+ case "disconnected":
11443
+ case "removed":
11444
+ case "superseded":
11445
+ case "inactive":
11446
+ case "unsupported":
11447
+ case "ended":
11448
+ case "record":
11449
+ case "ringing_out":
11450
+ case "ringing_in":
11451
+ case "call_declined":
11452
+ case "call_missed":
11453
+ case "call_busy":
11454
+ case "answered_elsewhere":
11455
+ return "exit";
11456
+ default: {
11457
+ const unhandled = phase;
11458
+ return unhandled;
11459
+ }
11460
+ }
11461
+ })();
11462
+ if (screen === "resolving") {
11463
+ return /* @__PURE__ */ jsx6("div", { className: "mx-meet", children: /* @__PURE__ */ jsx6("p", { className: "mx-meet__empty-body", role: "status", children: "Getting the meeting ready\u2026" }) });
11464
+ }
11465
+ if (screen === "prejoin") {
11059
11466
  return /* @__PURE__ */ jsx6("div", { className: "mx-meet", children: /* @__PURE__ */ jsx6(
11060
11467
  PreJoin,
11061
11468
  {
@@ -11066,9 +11473,12 @@ function MeetSkinRoom({
11066
11473
  }
11067
11474
  ) });
11068
11475
  }
11069
- if (phase === "lobby") {
11476
+ if (screen === "waiting") {
11070
11477
  return /* @__PURE__ */ jsx6("div", { className: "mx-meet mx-meet--lobby", children: /* @__PURE__ */ jsx6(WaitingRoom, { host, ...onLeave !== void 0 ? { onLeave } : {} }) });
11071
11478
  }
11479
+ if (screen === "refused" || screen === "exit") {
11480
+ return /* @__PURE__ */ jsx6("div", { className: "mx-meet mx-meet--lobby", children: /* @__PURE__ */ jsx6(ExitScreen, { ...onLeave !== void 0 ? { onLeave } : {} }) });
11481
+ }
11072
11482
  return /* @__PURE__ */ jsxs3("div", { className: "mx-meet", children: [
11073
11483
  /* @__PURE__ */ jsx6(ConsentNotice, {}),
11074
11484
  /* @__PURE__ */ jsx6(AttendanceNotice, {}),
@@ -11088,14 +11498,148 @@ function MeetSkinRoom({
11088
11498
  headerControls !== void 0 && headerControls !== null ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__header-controls", "data-meet-header-controls": "", children: headerControls }) : null,
11089
11499
  isHost ? /* @__PURE__ */ jsx6(HostMenu, {}) : null
11090
11500
  ] }),
11091
- phase === "reconnecting" ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__banner", role: "status", children: "Reconnecting\u2026" }) : null,
11501
+ core.connection !== "stable" ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__banner", role: "status", children: "Reconnecting\u2026" }) : null,
11502
+ /* @__PURE__ */ jsx6(ShareNotice, {}),
11092
11503
  /* @__PURE__ */ jsx6(Stage, {}),
11093
11504
  /* @__PURE__ */ jsx6(Captions, {}),
11094
11505
  /* @__PURE__ */ jsx6(ControlBar, { ...onLeave !== void 0 ? { onLeave } : {} })
11095
11506
  ] });
11096
11507
  }
11508
+ function ShareNotice() {
11509
+ const { share, actions } = useMeetCore();
11510
+ if (share.state !== "refused") return null;
11511
+ 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";
11512
+ return /* @__PURE__ */ jsxs3("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "share-unavailable", children: [
11513
+ text3,
11514
+ share.remedy !== null ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__remedy", children: share.remedy }) : null,
11515
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__link", onClick: actions.dismissShareNotice, children: "Dismiss" })
11516
+ ] });
11517
+ }
11518
+ var REFUSED_TITLES = {
11519
+ not_found: "Check your meeting code",
11520
+ cancelled: "This meeting was cancelled",
11521
+ expired: "This meeting link has expired",
11522
+ too_early: "This meeting hasn't started yet",
11523
+ sign_in_required: "Sign in to join this meeting",
11524
+ wrong_account: "You're signed in with a different account",
11525
+ not_invited: "You aren't invited to this meeting",
11526
+ locked: "This meeting is locked",
11527
+ removed: "You can't rejoin this meeting",
11528
+ lobby_rejects: "You can't ask to join this meeting",
11529
+ meeting_full: "This meeting is full",
11530
+ platform_capacity: "Meetings are busy right now",
11531
+ verification_required: "Verify your account to join",
11532
+ registration_required: "Register to join this meeting"
11533
+ };
11534
+ function ExitScreen({ onLeave }) {
11535
+ const { phase, reason, host, snapshot, actions, call } = useMeetCore();
11536
+ const [busy, setBusy] = useState4(false);
11537
+ const failure = snapshot?.failure ?? null;
11538
+ const rejoin = /* @__PURE__ */ jsx6(
11539
+ Button,
11540
+ {
11541
+ type: "button",
11542
+ disabled: busy,
11543
+ onClick: () => {
11544
+ setBusy(true);
11545
+ void actions.rejoin().catch(() => void 0).finally(() => setBusy(false));
11546
+ },
11547
+ children: "Rejoin"
11548
+ }
11549
+ );
11550
+ const home = /* @__PURE__ */ jsx6(
11551
+ "a",
11552
+ {
11553
+ className: "mx-meet__link",
11554
+ href: "/",
11555
+ onClick: (event) => {
11556
+ if (call !== null) {
11557
+ event.preventDefault();
11558
+ actions.dismissCall();
11559
+ }
11560
+ if (onLeave !== void 0) {
11561
+ event.preventDefault();
11562
+ onLeave();
11563
+ }
11564
+ },
11565
+ children: "Go home"
11566
+ }
11567
+ );
11568
+ let title;
11569
+ let line = null;
11570
+ let notice = null;
11571
+ let actionsNode = home;
11572
+ if (phase.startsWith("refused:")) {
11573
+ const code = phase.slice("refused:".length);
11574
+ title = REFUSED_TITLES[code] ?? "You can't join this meeting";
11575
+ line = failure?.remedy ?? null;
11576
+ actionsNode = /* @__PURE__ */ jsxs3(Fragment, { children: [
11577
+ /* @__PURE__ */ jsx6(Button, { type: "button", variant: "outline", onClick: actions.retry, children: "Try again" }),
11578
+ home
11579
+ ] });
11580
+ } else {
11581
+ switch (phase) {
11582
+ case "left":
11583
+ title = "You left the meeting";
11584
+ actionsNode = /* @__PURE__ */ jsxs3(Fragment, { children: [
11585
+ rejoin,
11586
+ home
11587
+ ] });
11588
+ break;
11589
+ case "disconnected":
11590
+ title = "You lost your connection to the meeting";
11591
+ line = failure?.remedy ?? "Check your internet connection, then rejoin.";
11592
+ notice = "disconnected";
11593
+ actionsNode = /* @__PURE__ */ jsxs3(Fragment, { children: [
11594
+ rejoin,
11595
+ home
11596
+ ] });
11597
+ break;
11598
+ case "removed":
11599
+ title = "You've been removed from the meeting";
11600
+ notice = "removed";
11601
+ break;
11602
+ case "superseded":
11603
+ title = "You joined from another tab or device";
11604
+ line = "This screen left the meeting so you aren't in it twice.";
11605
+ notice = "displaced";
11606
+ actionsNode = /* @__PURE__ */ jsxs3(Fragment, { children: [
11607
+ rejoin,
11608
+ home
11609
+ ] });
11610
+ break;
11611
+ case "inactive":
11612
+ title = "You left the meeting because this tab was inactive";
11613
+ actionsNode = /* @__PURE__ */ jsxs3(Fragment, { children: [
11614
+ rejoin,
11615
+ home
11616
+ ] });
11617
+ break;
11618
+ case "unsupported":
11619
+ title = "This browser can't join meetings";
11620
+ line = "Open the link in Chrome, Edge, Firefox or Safari.";
11621
+ break;
11622
+ case "ended":
11623
+ case "record":
11624
+ title = reason === "time_limit" ? "The meeting reached its time limit" : "The meeting has ended";
11625
+ notice = "meeting-ended";
11626
+ break;
11627
+ default:
11628
+ title = "The call has ended";
11629
+ break;
11630
+ }
11631
+ }
11632
+ return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__waiting", "data-meet-exit": phase, children: [
11633
+ /* @__PURE__ */ jsx6(Avatar, { name: host?.identity.displayName ?? "", url: host?.identity.avatarUrl ?? null, size: 72 }),
11634
+ /* @__PURE__ */ jsx6("h1", { className: "mx-meet__prejoin-title", children: title }),
11635
+ line !== null ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__empty-body", children: line }) : null,
11636
+ notice !== null ? /* @__PURE__ */ jsx6("span", { role: "status", "data-meet-notice": notice, className: "mx-meet__sr-only", children: title }) : null,
11637
+ /* @__PURE__ */ jsx6("div", { className: "mx-meet__waiting-actions", children: actionsNode })
11638
+ ] });
11639
+ }
11097
11640
  function WaitingRoom({ host, onLeave }) {
11098
11641
  const { waiting, withdraw, askAgain } = useWaiting();
11642
+ const { phaseNotice } = useMeetCore();
11099
11643
  const [busy, setBusy] = useState4(false);
11100
11644
  const admission = waiting?.admission ?? "knocking";
11101
11645
  const run = (action) => () => {
@@ -11144,6 +11688,7 @@ function WaitingRoom({ host, onLeave }) {
11144
11688
  return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__waiting", children: [
11145
11689
  /* @__PURE__ */ jsx6(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 72 }),
11146
11690
  /* @__PURE__ */ jsx6("h1", { className: "mx-meet__prejoin-title", children: title }),
11691
+ 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
11692
  line !== null ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__empty-body", role: "status", "data-meet-notice": "host-absent", children: line }) : null,
11148
11693
  /* @__PURE__ */ jsx6("div", { className: "mx-meet__waiting-actions", children: actions })
11149
11694
  ] });
@@ -11638,10 +12183,17 @@ function MeetingSkinRootBody({
11638
12183
  host
11639
12184
  }) {
11640
12185
  const snapshot = useMeetSnapshot();
11641
- const phase = snapshot?.phase ?? "idle";
12186
+ const phase = snapshot?.phase ?? "resolving";
11642
12187
  useEffect8(() => {
11643
12188
  host.meetings.watch(roomName);
11644
12189
  }, [host, roomName]);
12190
+ useEffect8(() => {
12191
+ const now = host.store.snapshot();
12192
+ if (now.phase !== "resolving" && now.roomName !== null && now.roomName !== roomName && now.call === null) {
12193
+ host.store.dispatch({ type: "reset" });
12194
+ }
12195
+ if (host.store.snapshot().phase === "resolving") host.store.dispatch({ type: "resolved" });
12196
+ }, [host, roomName, phase]);
11645
12197
  useEffect8(() => {
11646
12198
  if (given === void 0) return;
11647
12199
  if (host.store.snapshot().meeting !== null) return;
@@ -11653,7 +12205,7 @@ function MeetingSkinRootBody({
11653
12205
  host.meetings.watchMeeting(asMeetingId(watchedId));
11654
12206
  }, [host, watchedId]);
11655
12207
  const record = snapshot?.meeting ?? given ?? null;
11656
- if (hasEnded(record) && phase !== "connected" && phase !== "reconnecting") {
12208
+ if (hasEnded(record) && !isLivePhase(phase)) {
11657
12209
  return /* @__PURE__ */ jsx8(MeetingRecordView, { meeting: record });
11658
12210
  }
11659
12211
  return /* @__PURE__ */ jsx8(
@@ -11670,11 +12222,49 @@ function MeetingSkinRootBody({
11670
12222
  }
11671
12223
 
11672
12224
  // src/react/calls-ui.tsx
11673
- import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
12225
+ import { useEffect as useEffect9 } from "react";
12226
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
11674
12227
  function IncomingCallHost() {
11675
12228
  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)) });
12229
+ const { call, phase, actions } = useMeetCore();
12230
+ useEffect9(() => {
12231
+ if (call !== null && phase === "left") actions.dismissCall();
12232
+ }, [call, phase, actions]);
12233
+ const outcome = call !== null && (phase === "call_declined" || phase === "call_missed" || phase === "call_busy" || phase === "answered_elsewhere");
12234
+ useEffect9(() => {
12235
+ if (!outcome) return void 0;
12236
+ const timer = setTimeout(() => actions.dismissCall(), 5e3);
12237
+ return () => clearTimeout(timer);
12238
+ }, [outcome, actions]);
12239
+ const inCall = call !== null && (phase === "joining" || phase === "in_call" || phase === "disconnected" || phase === "removed" || phase === "superseded");
12240
+ return /* @__PURE__ */ jsxs5(Fragment3, { children: [
12241
+ 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,
12242
+ calls.incoming.length > 0 || call !== null && (phase === "ringing_out" || outcome) ? /* @__PURE__ */ jsxs5("div", { className: "mx-meet__ringing-stack", children: [
12243
+ call !== null && phase === "ringing_out" ? /* @__PURE__ */ jsx9(OutgoingCallCard, { invite: call.invite }) : null,
12244
+ outcome && call !== null ? /* @__PURE__ */ jsx9(CallOutcomeCard, { invite: call.invite }) : null,
12245
+ calls.incoming.map((invite) => /* @__PURE__ */ jsx9(IncomingCallCard, { invite }, invite.id))
12246
+ ] }) : null
12247
+ ] });
12248
+ }
12249
+ function OutgoingCallCard({ invite }) {
12250
+ const calls = useCalls();
12251
+ return /* @__PURE__ */ jsxs5("section", { className: "mx-meet__ringing", role: "status", "aria-label": "Calling", "data-meet-notice": "ringing-out", children: [
12252
+ /* @__PURE__ */ jsx9(Avatar, { name: invite.callerName, url: invite.callerAvatarUrl, size: 56 }),
12253
+ /* @__PURE__ */ jsxs5("div", { className: "mx-meet__ringing-text", children: [
12254
+ /* @__PURE__ */ jsx9("strong", { children: "Calling\u2026" }),
12255
+ /* @__PURE__ */ jsx9("span", { children: invite.mode === "video" ? "Video call" : "Voice call" })
12256
+ ] }),
12257
+ /* @__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, {}) }) })
12258
+ ] });
12259
+ }
12260
+ function CallOutcomeCard({ invite }) {
12261
+ const { phase, actions, call } = useMeetCore();
12262
+ const outgoing = call?.direction === "outgoing";
12263
+ 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}`;
12264
+ return /* @__PURE__ */ jsxs5("section", { className: "mx-meet__ringing", role: "status", "aria-label": text3, "data-meet-call-outcome": phase, children: [
12265
+ /* @__PURE__ */ jsx9("div", { className: "mx-meet__ringing-text", children: /* @__PURE__ */ jsx9("strong", { children: text3 }) }),
12266
+ /* @__PURE__ */ jsx9("div", { className: "mx-meet__ringing-actions", children: /* @__PURE__ */ jsx9(ControlButton, { label: "Close", onClick: actions.dismissCall, children: /* @__PURE__ */ jsx9(PhoneOffIcon, {}) }) })
12267
+ ] });
11678
12268
  }
11679
12269
  function IncomingCallCard({ invite }) {
11680
12270
  const calls = useCalls();
@@ -11849,28 +12439,6 @@ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
11849
12439
  if (!response.ok) throw await refusal(response, operation);
11850
12440
  return await response.json();
11851
12441
  }
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
12442
  export {
11875
12443
  ATTENDANCE_CSV_HEADER,
11876
12444
  CALL_INVITE_KIND,
@@ -11889,11 +12457,13 @@ export {
11889
12457
  GUEST_RECORD_NOT_SHARED,
11890
12458
  GuestRecordAccessControl,
11891
12459
  IDLE_MEDIA,
12460
+ IDLE_PHASES,
11892
12461
  IDLE_TRACK,
11893
12462
  IncomingCallCard,
11894
12463
  IncomingCallHost,
11895
12464
  JOIN_REFUSAL_REASONS,
11896
12465
  LISTEN_ONLY_NOTICE,
12466
+ LIVE_PHASES,
11897
12467
  MEETING_INVITE_KIND,
11898
12468
  MEETING_INVITE_VERSION,
11899
12469
  MEETING_PASS_HEADER,
@@ -11915,6 +12485,7 @@ export {
11915
12485
  NOTE_TAKER_IDENTITY_PREFIX,
11916
12486
  NOTE_TAKER_METADATA_KEY,
11917
12487
  NO_GRANTS,
12488
+ NO_SHARE,
11918
12489
  ORGANIZATION_HEADER,
11919
12490
  PARTICIPANT_CAN_RECORD_ATTRIBUTE,
11920
12491
  PARTICIPANT_ROLE_ATTRIBUTE,
@@ -11929,9 +12500,11 @@ export {
11929
12500
  TABLES,
11930
12501
  TRANSCRIPTION_ATTRIBUTES,
11931
12502
  TRANSCRIPTION_TOPIC,
12503
+ WAITING_PHASES,
11932
12504
  absenceSentence,
11933
12505
  activeWrapUpSummary,
11934
12506
  activityAttention,
12507
+ admissionPhase,
11935
12508
  announceMeeting,
11936
12509
  appPanelOf,
11937
12510
  applyCollabPacket,
@@ -12001,10 +12574,13 @@ export {
12001
12574
  isJoinRefusalReason,
12002
12575
  isLinkFollower,
12003
12576
  isListenOnly,
12577
+ isLivePhase,
12004
12578
  isNoteTaker,
12005
12579
  isObserverRole,
12006
12580
  isRealName,
12581
+ isRefusedPhase,
12007
12582
  isRoomEventTopic,
12583
+ isWaitingPhase,
12008
12584
  learnName,
12009
12585
  learnNames,
12010
12586
  listenOnlyNotice,
@@ -12029,6 +12605,7 @@ export {
12029
12605
  mergeRecord,
12030
12606
  microphoneNoticeCode,
12031
12607
  moveToRoom,
12608
+ nextPhase,
12032
12609
  nextTranslationBatch,
12033
12610
  normalizeDeviceError,
12034
12611
  normalizeMeetError,
@@ -12061,10 +12638,13 @@ export {
12061
12638
  pushMeetingToGoogleCalendar,
12062
12639
  readServerRefusal,
12063
12640
  readableTranscript,
12641
+ reasonOf,
12064
12642
  recordingLabel,
12065
12643
  recordingNotice,
12066
12644
  recordingOffsetMs,
12645
+ refusalPhase,
12067
12646
  rememberedMeetingPass,
12647
+ resolveExit,
12068
12648
  roleOf,
12069
12649
  roomTopic,
12070
12650
  rootAttributes,