@ai-matrx/meet 0.10.6 → 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.cjs CHANGED
@@ -49,11 +49,13 @@ __export(react_exports, {
49
49
  GUEST_RECORD_NOT_SHARED: () => GUEST_RECORD_NOT_SHARED,
50
50
  GuestRecordAccessControl: () => GuestRecordAccessControl,
51
51
  IDLE_MEDIA: () => IDLE_MEDIA,
52
+ IDLE_PHASES: () => IDLE_PHASES,
52
53
  IDLE_TRACK: () => IDLE_TRACK,
53
54
  IncomingCallCard: () => IncomingCallCard,
54
55
  IncomingCallHost: () => IncomingCallHost,
55
56
  JOIN_REFUSAL_REASONS: () => JOIN_REFUSAL_REASONS,
56
57
  LISTEN_ONLY_NOTICE: () => LISTEN_ONLY_NOTICE,
58
+ LIVE_PHASES: () => LIVE_PHASES,
57
59
  MEETING_INVITE_KIND: () => MEETING_INVITE_KIND,
58
60
  MEETING_INVITE_VERSION: () => MEETING_INVITE_VERSION,
59
61
  MEETING_PASS_HEADER: () => MEETING_PASS_HEADER,
@@ -75,6 +77,7 @@ __export(react_exports, {
75
77
  NOTE_TAKER_IDENTITY_PREFIX: () => NOTE_TAKER_IDENTITY_PREFIX,
76
78
  NOTE_TAKER_METADATA_KEY: () => NOTE_TAKER_METADATA_KEY,
77
79
  NO_GRANTS: () => NO_GRANTS,
80
+ NO_SHARE: () => NO_SHARE,
78
81
  ORGANIZATION_HEADER: () => ORGANIZATION_HEADER,
79
82
  PARTICIPANT_CAN_RECORD_ATTRIBUTE: () => PARTICIPANT_CAN_RECORD_ATTRIBUTE,
80
83
  PARTICIPANT_ROLE_ATTRIBUTE: () => PARTICIPANT_ROLE_ATTRIBUTE,
@@ -89,9 +92,11 @@ __export(react_exports, {
89
92
  TABLES: () => TABLES,
90
93
  TRANSCRIPTION_ATTRIBUTES: () => TRANSCRIPTION_ATTRIBUTES,
91
94
  TRANSCRIPTION_TOPIC: () => TRANSCRIPTION_TOPIC,
95
+ WAITING_PHASES: () => WAITING_PHASES,
92
96
  absenceSentence: () => absenceSentence,
93
97
  activeWrapUpSummary: () => activeWrapUpSummary,
94
98
  activityAttention: () => activityAttention,
99
+ admissionPhase: () => admissionPhase,
95
100
  announceMeeting: () => announceMeeting,
96
101
  appPanelOf: () => appPanelOf,
97
102
  applyCollabPacket: () => applyCollabPacket,
@@ -161,10 +166,13 @@ __export(react_exports, {
161
166
  isJoinRefusalReason: () => isJoinRefusalReason,
162
167
  isLinkFollower: () => isLinkFollower,
163
168
  isListenOnly: () => isListenOnly,
169
+ isLivePhase: () => isLivePhase,
164
170
  isNoteTaker: () => isNoteTaker,
165
171
  isObserverRole: () => isObserverRole,
166
172
  isRealName: () => isRealName,
173
+ isRefusedPhase: () => isRefusedPhase,
167
174
  isRoomEventTopic: () => isRoomEventTopic,
175
+ isWaitingPhase: () => isWaitingPhase,
168
176
  learnName: () => learnName,
169
177
  learnNames: () => learnNames,
170
178
  listenOnlyNotice: () => listenOnlyNotice,
@@ -189,6 +197,7 @@ __export(react_exports, {
189
197
  mergeRecord: () => mergeRecord,
190
198
  microphoneNoticeCode: () => microphoneNoticeCode,
191
199
  moveToRoom: () => moveToRoom,
200
+ nextPhase: () => nextPhase,
192
201
  nextTranslationBatch: () => nextTranslationBatch,
193
202
  normalizeDeviceError: () => normalizeDeviceError,
194
203
  normalizeMeetError: () => normalizeMeetError,
@@ -221,10 +230,13 @@ __export(react_exports, {
221
230
  pushMeetingToGoogleCalendar: () => pushMeetingToGoogleCalendar,
222
231
  readServerRefusal: () => readServerRefusal,
223
232
  readableTranscript: () => readableTranscript,
233
+ reasonOf: () => reasonOf,
224
234
  recordingLabel: () => recordingLabel,
225
235
  recordingNotice: () => recordingNotice,
226
236
  recordingOffsetMs: () => recordingOffsetMs,
237
+ refusalPhase: () => refusalPhase,
227
238
  rememberedMeetingPass: () => rememberedMeetingPass,
239
+ resolveExit: () => resolveExit,
228
240
  roleOf: () => roleOf,
229
241
  roomTopic: () => roomTopic,
230
242
  rootAttributes: () => rootAttributes,
@@ -1291,6 +1303,166 @@ var MEET_EVENTS = {
1291
1303
  stateChanged: "mx.state.changed"
1292
1304
  };
1293
1305
 
1306
+ // src/core/reasons.ts
1307
+ var JOIN_REFUSAL_REASONS = [
1308
+ "not_found",
1309
+ "cancelled",
1310
+ "ended",
1311
+ "expired",
1312
+ "too_early",
1313
+ "sign_in_required",
1314
+ "wrong_account",
1315
+ "not_invited",
1316
+ "locked",
1317
+ "removed",
1318
+ "lobby_rejects",
1319
+ "meeting_full",
1320
+ "platform_capacity",
1321
+ "verification_required",
1322
+ "registration_required"
1323
+ ];
1324
+ function isJoinRefusalReason(value) {
1325
+ return typeof value === "string" && JOIN_REFUSAL_REASONS.includes(value);
1326
+ }
1327
+
1328
+ // src/core/phase.ts
1329
+ var LIVE_PHASES = /* @__PURE__ */ new Set(["joining", "in_call"]);
1330
+ var WAITING_PHASES = /* @__PURE__ */ new Set([
1331
+ "waiting:host_not_started",
1332
+ "waiting:knocking",
1333
+ "denied",
1334
+ "knock_expired"
1335
+ ]);
1336
+ var JOINABLE_FROM = /* @__PURE__ */ new Set([
1337
+ "prejoin",
1338
+ "left",
1339
+ "disconnected",
1340
+ "superseded",
1341
+ "inactive",
1342
+ "denied",
1343
+ "knock_expired"
1344
+ ]);
1345
+ var NOT_ENDABLE = /* @__PURE__ */ new Set([
1346
+ "ended",
1347
+ "record",
1348
+ "unsupported",
1349
+ "ringing_out",
1350
+ "ringing_in",
1351
+ "call_declined",
1352
+ "call_missed",
1353
+ "call_busy",
1354
+ "answered_elsewhere"
1355
+ ]);
1356
+ var RINGING = /* @__PURE__ */ new Set(["ringing_out", "ringing_in"]);
1357
+ var IDLE_PHASES = /* @__PURE__ */ new Set([
1358
+ "resolving",
1359
+ "left",
1360
+ "call_declined",
1361
+ "call_missed",
1362
+ "call_busy",
1363
+ "answered_elsewhere"
1364
+ ]);
1365
+ function isLivePhase(phase) {
1366
+ return LIVE_PHASES.has(phase);
1367
+ }
1368
+ function isWaitingPhase(phase) {
1369
+ return WAITING_PHASES.has(phase);
1370
+ }
1371
+ function isRefusedPhase(phase) {
1372
+ return phase.startsWith("refused:");
1373
+ }
1374
+ function reasonOf(phase) {
1375
+ return isRefusedPhase(phase) ? phase.slice("refused:".length) : null;
1376
+ }
1377
+ function refusalPhase(reason) {
1378
+ if (reason === null || !isJoinRefusalReason(reason)) return "disconnected";
1379
+ return reason === "ended" ? "ended" : `refused:${reason}`;
1380
+ }
1381
+ function admissionPhase(admission) {
1382
+ switch (admission) {
1383
+ case "waiting_host":
1384
+ return "waiting:host_not_started";
1385
+ case "knocking":
1386
+ return "waiting:knocking";
1387
+ case "denied":
1388
+ return "denied";
1389
+ case "expired":
1390
+ return "knock_expired";
1391
+ case "withdrawn":
1392
+ return "left";
1393
+ case "ended":
1394
+ return "ended";
1395
+ case "removed":
1396
+ return "refused:removed";
1397
+ default:
1398
+ return null;
1399
+ }
1400
+ }
1401
+ function nextPhase(from, event) {
1402
+ switch (event.type) {
1403
+ case "resolved":
1404
+ return from === "resolving" ? "prejoin" : null;
1405
+ case "unsupported":
1406
+ return from === "resolving" ? "unsupported" : null;
1407
+ case "join":
1408
+ return JOINABLE_FROM.has(from) ? "joining" : null;
1409
+ case "back_to_prejoin":
1410
+ return from === "left" || from === "disconnected" ? "prejoin" : null;
1411
+ case "retry":
1412
+ return isRefusedPhase(from) || from === "unsupported" ? "resolving" : null;
1413
+ case "admission": {
1414
+ if (from !== "joining" && !WAITING_PHASES.has(from)) return null;
1415
+ return admissionPhase(event.admission);
1416
+ }
1417
+ case "admitted":
1418
+ return WAITING_PHASES.has(from) ? "joining" : null;
1419
+ case "connected":
1420
+ return from === "joining" ? "in_call" : null;
1421
+ case "join_failed":
1422
+ return from === "joining" ? refusalPhase(event.reason) : null;
1423
+ case "exit":
1424
+ return from === "in_call" || from === "joining" ? event.to : null;
1425
+ case "leave":
1426
+ return from === "in_call" || from === "joining" || WAITING_PHASES.has(from) ? "left" : null;
1427
+ case "ended":
1428
+ return NOT_ENDABLE.has(from) ? null : "ended";
1429
+ case "ring_out":
1430
+ return IDLE_PHASES.has(from) || from === "prejoin" ? "ringing_out" : null;
1431
+ case "ring_in":
1432
+ return IDLE_PHASES.has(from) ? "ringing_in" : null;
1433
+ case "call_answered":
1434
+ return RINGING.has(from) ? "joining" : null;
1435
+ case "call_outcome":
1436
+ return RINGING.has(from) ? event.outcome : null;
1437
+ case "reset":
1438
+ return LIVE_PHASES.has(from) ? null : "resolving";
1439
+ }
1440
+ }
1441
+ function resolveExit(input) {
1442
+ const { server, hint } = input;
1443
+ if (server === null) {
1444
+ return { to: "disconnected", reason: null, connection: "server_unreachable", notice: null };
1445
+ }
1446
+ const word = server?.exitReason ?? null;
1447
+ if (server !== void 0 && (server.sessionPhase === "ended" || server.meetingPhase === "ended" || word === "ended" || word === "time_limit")) {
1448
+ return { to: "ended", reason: word === "time_limit" ? "time_limit" : "ended", connection: "stable", notice: null };
1449
+ }
1450
+ if (word === "removed") return { to: "removed", reason: "removed", connection: "stable", notice: null };
1451
+ if (word === "displaced") return { to: "superseded", reason: "displaced", connection: "stable", notice: null };
1452
+ if (word === "sent_to_waiting") {
1453
+ return { to: "waiting:knocking", reason: "sent_to_waiting", connection: "stable", notice: "sent-to-waiting" };
1454
+ }
1455
+ if (hint === "DUPLICATE_IDENTITY") return { to: "superseded", reason: "displaced", connection: "stable", notice: null };
1456
+ if (hint === "PARTICIPANT_REMOVED" && word === null && server === void 0) {
1457
+ return { to: "removed", reason: "removed", connection: "stable", notice: null };
1458
+ }
1459
+ if (hint === "ROOM_DELETED" && server === void 0) {
1460
+ return { to: "ended", reason: "ended", connection: "stable", notice: null };
1461
+ }
1462
+ if (input.frozen === true) return { to: "inactive", reason: "inactive", connection: "stable", notice: null };
1463
+ return { to: "disconnected", reason: null, connection: "stable", notice: null };
1464
+ }
1465
+
1294
1466
  // src/core/types.ts
1295
1467
  var asMeetingId = (value) => value;
1296
1468
  var asCallId = (value) => value;
@@ -1382,6 +1554,39 @@ function createCallCenter(options) {
1382
1554
  clearRing(invite.id);
1383
1555
  incoming.delete(invite.id);
1384
1556
  if (outgoing !== null && outgoing.id === invite.id) outgoing = invite;
1557
+ const held = options.store.snapshot().call;
1558
+ if (held !== null && held.invite.id === invite.id && held.direction === "incoming") {
1559
+ const outcome = invite.state === "connecting" || invite.state === "active" ? "answered_elsewhere" : invite.state === "declined" ? "reset" : "call_missed";
1560
+ const moved = outcome === "reset" ? options.store.dispatch({ type: "reset" }) : options.store.dispatch({ type: "call_outcome", outcome });
1561
+ if (moved) options.store.patch({ call: outcome === "reset" ? null : { ...held, invite } });
1562
+ }
1563
+ options.onSettled?.(invite);
1564
+ }
1565
+ function settleOutgoing(invite) {
1566
+ if (outgoing === null || outgoing.id !== invite.id) return;
1567
+ clearRing(invite.id);
1568
+ outgoing = invite;
1569
+ const store = options.store;
1570
+ const held = store.snapshot().call;
1571
+ if (held !== null && held.invite.id === invite.id) store.patch({ call: { ...held, invite } });
1572
+ switch (invite.state) {
1573
+ case "connecting":
1574
+ case "active":
1575
+ if (store.dispatch({ type: "call_answered" })) options.onAnswered?.(invite);
1576
+ break;
1577
+ case "declined":
1578
+ store.dispatch({ type: "call_outcome", outcome: "call_declined" });
1579
+ break;
1580
+ case "busy":
1581
+ store.dispatch({ type: "call_outcome", outcome: "call_busy" });
1582
+ break;
1583
+ case "missed":
1584
+ case "failed":
1585
+ store.dispatch({ type: "call_outcome", outcome: "call_missed" });
1586
+ break;
1587
+ default:
1588
+ break;
1589
+ }
1385
1590
  options.onSettled?.(invite);
1386
1591
  }
1387
1592
  function ingest(invite) {
@@ -1390,7 +1595,7 @@ function createCallCenter(options) {
1390
1595
  if (identity.userId === null) return;
1391
1596
  if (invite.callerUserId === identity.userId) return;
1392
1597
  const phase = options.store.snapshot().phase;
1393
- if (phase === "connected" || phase === "connecting" || phase === "reconnecting") {
1598
+ if (isLivePhase(phase) || phase === "ringing_out") {
1394
1599
  void repository.settleCallInvite(invite.id, "busy", null).then((settled) => {
1395
1600
  to(settled.callerUserId).send(MEET_EVENTS.settled, settled);
1396
1601
  }).catch((error) => {
@@ -1403,6 +1608,9 @@ function createCallCenter(options) {
1403
1608
  return;
1404
1609
  }
1405
1610
  incoming.set(invite.id, { ...invite, state: "ringing" });
1611
+ if (options.store.snapshot().call === null && options.store.dispatch({ type: "ring_in" })) {
1612
+ options.store.patch({ call: { invite: { ...invite, state: "ringing" }, direction: "incoming" } });
1613
+ }
1406
1614
  options.onIncoming?.(invite);
1407
1615
  timeouts.set(
1408
1616
  invite.id,
@@ -1440,6 +1648,7 @@ function createCallCenter(options) {
1440
1648
  const invite = message.data;
1441
1649
  const held = incoming.get(invite.id);
1442
1650
  if (held !== void 0) settleLocal(invite);
1651
+ else if (outgoing?.id === invite.id) settleOutgoing(invite);
1443
1652
  }
1444
1653
  }
1445
1654
  ],
@@ -1506,6 +1715,9 @@ function createCallCenter(options) {
1506
1715
  expiresAt: new Date(timers.now() + ringTimeoutMs).toISOString()
1507
1716
  });
1508
1717
  outgoing = invite;
1718
+ if (options.store.dispatch({ type: "ring_out" })) {
1719
+ options.store.patch({ call: { invite, direction: "outgoing" } });
1720
+ }
1509
1721
  for (const callee of people) {
1510
1722
  to(callee).send(MEET_EVENTS.ring, invite);
1511
1723
  }
@@ -1514,8 +1726,7 @@ function createCallCenter(options) {
1514
1726
  timers.setTimeout(() => {
1515
1727
  if (outgoing?.id !== invite.id) return;
1516
1728
  void repository.settleCallInvite(invite.id, "missed", null).then((settled) => {
1517
- outgoing = settled;
1518
- options.onSettled?.(settled);
1729
+ settleOutgoing(settled);
1519
1730
  }).catch(() => void 0);
1520
1731
  }, ringTimeoutMs)
1521
1732
  );
@@ -1530,7 +1741,9 @@ function createCallCenter(options) {
1530
1741
  "A late accept must not resurrect a settled call. Close the ringing UI; the caller has already moved on."
1531
1742
  );
1532
1743
  }
1744
+ if (options.store.snapshot().phase === "ringing_in") options.store.dispatch({ type: "call_answered" });
1533
1745
  const settled = await repository.settleCallInvite(id, "connecting", null);
1746
+ options.store.patch({ call: { invite: settled, direction: "incoming" } });
1534
1747
  settleLocal(settled);
1535
1748
  channel?.send(MEET_EVENTS.settled, settled);
1536
1749
  to(settled.callerUserId).send(MEET_EVENTS.settled, { ...settled, mode });
@@ -1538,6 +1751,9 @@ function createCallCenter(options) {
1538
1751
  },
1539
1752
  async decline(id, message) {
1540
1753
  const settled = await repository.settleCallInvite(id, "declined", message ?? null);
1754
+ if (options.store.snapshot().call?.invite.id === id && options.store.dispatch({ type: "reset" })) {
1755
+ options.store.patch({ call: null });
1756
+ }
1541
1757
  settleLocal(settled);
1542
1758
  channel?.send(MEET_EVENTS.settled, settled);
1543
1759
  to(settled.callerUserId).send(MEET_EVENTS.settled, settled);
@@ -1547,6 +1763,9 @@ function createCallCenter(options) {
1547
1763
  const settled = await repository.settleCallInvite(id, "cancelled", null);
1548
1764
  clearRing(id);
1549
1765
  if (outgoing?.id === id) outgoing = settled;
1766
+ if (options.store.snapshot().call?.invite.id === id && options.store.dispatch({ type: "reset" })) {
1767
+ options.store.patch({ call: null });
1768
+ }
1550
1769
  for (const callee of settled.calleeUserIds) {
1551
1770
  to(callee).send(MEET_EVENTS.cancel, settled);
1552
1771
  }
@@ -1932,7 +2151,9 @@ var TABLES = {
1932
2151
  /** Wave 1 (0.8): who a meeting was sent to, and what they answered. */
1933
2152
  invitees: "meet_invitees",
1934
2153
  /** Wave 1 (0.8): one occurrence of a series cancelled or moved. */
1935
- occurrenceOverrides: "meet_occurrence_overrides"
2154
+ occurrenceOverrides: "meet_occurrence_overrides",
2155
+ /** S5: one row per RUN of a meeting; carries that run's note-taker state. */
2156
+ sessions: "meet_sessions"
1936
2157
  };
1937
2158
  var RPCS = {
1938
2159
  /** Atomic: create the invite row and stamp its expiry in one statement. */
@@ -3285,8 +3506,15 @@ var NO_GRANTS = {
3285
3506
  canPublish: false,
3286
3507
  canSubscribe: false
3287
3508
  };
3509
+ var NO_SHARE = { state: "none", reason: null, remedy: null, at: 0 };
3288
3510
  var EMPTY = {
3289
- phase: "idle",
3511
+ phase: "resolving",
3512
+ reason: null,
3513
+ phaseNotice: null,
3514
+ roomName: null,
3515
+ connection: "stable",
3516
+ share: NO_SHARE,
3517
+ call: null,
3290
3518
  waiting: null,
3291
3519
  meeting: null,
3292
3520
  localIdentity: null,
@@ -3350,8 +3578,20 @@ function createMeetStore(initial = {}) {
3350
3578
  return () => listeners.delete(listener);
3351
3579
  },
3352
3580
  patch(next) {
3581
+ if ("phase" in next || "reason" in next || "phaseNotice" in next) {
3582
+ throw new TypeError("meet store: the phase moves only through store.dispatch(event)");
3583
+ }
3353
3584
  commit({ ...state, ...next });
3354
3585
  },
3586
+ dispatch(event, detail) {
3587
+ const to = nextPhase(state.phase, event);
3588
+ if (to === null) return false;
3589
+ const reason = detail?.reason !== void 0 ? detail.reason : reasonOf(to);
3590
+ const phaseNotice = detail?.notice ?? null;
3591
+ if (to === state.phase && reason === state.reason && phaseNotice === state.phaseNotice) return true;
3592
+ commit({ ...state, phase: to, reason, phaseNotice });
3593
+ return true;
3594
+ },
3355
3595
  setParticipants(participants) {
3356
3596
  const names = learnNames(state.names, participants);
3357
3597
  const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
@@ -3669,7 +3909,7 @@ function createMeetingSession(options) {
3669
3909
  }
3670
3910
  function apply(read) {
3671
3911
  const snapshot = store.snapshot();
3672
- if (snapshot.phase !== "lobby") return;
3912
+ if (!isWaitingPhase(snapshot.phase)) return;
3673
3913
  const admission = read.admission_state ?? null;
3674
3914
  const ended = admission === "ended" || read.session_phase === "ended" || read.meeting_phase === "ended";
3675
3915
  if (admission === "admitted" && !ended) {
@@ -3686,6 +3926,7 @@ function createMeetingSession(options) {
3686
3926
  if (read.meeting_phase === "ended" && meeting !== null && meeting.endedAt === null) {
3687
3927
  store.patch({ meeting: { ...meeting, endedAt: (/* @__PURE__ */ new Date()).toISOString() } });
3688
3928
  }
3929
+ store.dispatch({ type: "admission", admission: "ended" }, { reason: "ended" });
3689
3930
  return;
3690
3931
  }
3691
3932
  if (admission === null) return;
@@ -3697,6 +3938,7 @@ function createMeetingSession(options) {
3697
3938
  pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
3698
3939
  liveVersion: read.live_version ?? 0
3699
3940
  });
3941
+ store.dispatch({ type: "admission", admission });
3700
3942
  if (admission === "denied" || admission === "expired" || admission === "withdrawn" || admission === "removed") {
3701
3943
  stopWaiting();
3702
3944
  }
@@ -3792,7 +4034,7 @@ function createMeetingSession(options) {
3792
4034
  // 🚨 PAYLOAD-FREE. Every receiver reads again; a spoofed hint only causes a read.
3793
4035
  event: MEET_EVENTS.stateChanged,
3794
4036
  onMessage: () => {
3795
- if (store.snapshot().phase === "lobby") void readState2();
4037
+ if (isWaitingPhase(store.snapshot().phase)) void readState2();
3796
4038
  void refreshQueue();
3797
4039
  }
3798
4040
  }
@@ -3804,7 +4046,7 @@ function createMeetingSession(options) {
3804
4046
  if (meeting === null) return;
3805
4047
  const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
3806
4048
  store.patch({ meeting: fresh, locked: fresh.locked });
3807
- if (store.snapshot().phase === "lobby") void readState2();
4049
+ if (isWaitingPhase(store.snapshot().phase)) void readState2();
3808
4050
  }
3809
4051
  });
3810
4052
  },
@@ -3971,10 +4213,34 @@ function createMeetingSession(options) {
3971
4213
  },
3972
4214
  stopWaiting,
3973
4215
  readState: readState2,
4216
+ async readExit(connection) {
4217
+ const meeting = store.snapshot().meeting;
4218
+ const pass = options.resolvePass?.() ?? null;
4219
+ if (meeting === null || pass === null) return void 0;
4220
+ try {
4221
+ const response = await options.api.request({
4222
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}&connection=${encodeURIComponent(connection)}`,
4223
+ method: "GET",
4224
+ requireAuth: false,
4225
+ operation: "meet.state(exit)",
4226
+ headers: { [MEETING_PASS_HEADER]: pass }
4227
+ });
4228
+ if (!response.ok) return null;
4229
+ const read = await response.json();
4230
+ return {
4231
+ exitReason: read.exit_reason ?? null,
4232
+ sessionPhase: read.session_phase ?? null,
4233
+ meetingPhase: read.meeting_phase ?? null
4234
+ };
4235
+ } catch {
4236
+ return null;
4237
+ }
4238
+ },
3974
4239
  async withdraw() {
3975
4240
  stopWaiting();
3976
4241
  await postLeave("withdraw");
3977
4242
  patchWaiting({ admission: "withdrawn" });
4243
+ store.dispatch({ type: "admission", admission: "withdrawn" });
3978
4244
  hint();
3979
4245
  },
3980
4246
  async askAgain() {
@@ -4456,6 +4722,9 @@ var TRANSCRIPTION_ATTRIBUTES = {
4456
4722
  };
4457
4723
 
4458
4724
  // src/core/room.ts
4725
+ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
4726
+ return /administrator|policy/i.test(remedy) ? remedy : SCREEN_SHARE_BLOCKED_REMEDY;
4727
+ }
4459
4728
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
4460
4729
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
4461
4730
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
@@ -4523,7 +4792,7 @@ function createRoomEngine(options) {
4523
4792
  let disposed = false;
4524
4793
  let moving = false;
4525
4794
  const confirmingEnds = /* @__PURE__ */ new WeakSet();
4526
- async function leaveRoom() {
4795
+ async function dropRoom() {
4527
4796
  const target = driver;
4528
4797
  driver = null;
4529
4798
  if (current !== null) {
@@ -4532,9 +4801,14 @@ function createRoomEngine(options) {
4532
4801
  }
4533
4802
  store.setLocalTrack("microphone", IDLE_TRACK);
4534
4803
  store.setLocalTrack("camera", IDLE_TRACK);
4535
- store.patch({ phase: "left" });
4804
+ store.patch({ share: NO_SHARE, connection: "stable" });
4536
4805
  await target?.disconnect();
4537
4806
  }
4807
+ async function leaveRoom() {
4808
+ store.dispatch({ type: "leave" });
4809
+ await dropRoom();
4810
+ options.onLeft?.(null);
4811
+ }
4538
4812
  async function confirmMeetingEnd() {
4539
4813
  const meeting = store.snapshot().meeting;
4540
4814
  const target = driver;
@@ -4556,7 +4830,9 @@ function createRoomEngine(options) {
4556
4830
  const latest = store.snapshot().meeting;
4557
4831
  if (latest === null) return;
4558
4832
  store.patch({ meeting: { ...latest, endedAt: verified.endedAt } });
4559
- await leaveRoom();
4833
+ store.dispatch({ type: "ended" }, { reason: "ended" });
4834
+ await dropRoom();
4835
+ options.onLeft?.("ended");
4560
4836
  } catch (error) {
4561
4837
  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.");
4562
4838
  } finally {
@@ -4568,12 +4844,12 @@ function createRoomEngine(options) {
4568
4844
  options.onDiagnostic?.(remedy === void 0 ? { level, message } : { level, message, remedy });
4569
4845
  }
4570
4846
  let ambiguousShareDenials = 0;
4571
- function fail(error, operation) {
4847
+ function failJoin(error, operation) {
4572
4848
  const normalized = normalizeMeetError(error, operation);
4573
4849
  store.patch({
4574
- phase: "failed",
4575
4850
  failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
4576
4851
  });
4852
+ store.dispatch({ type: "join_failed", reason: normalized.reason ?? null });
4577
4853
  report(
4578
4854
  normalized.code === "session-unavailable" ? "warn" : "error",
4579
4855
  normalized.message,
@@ -4581,6 +4857,14 @@ function createRoomEngine(options) {
4581
4857
  );
4582
4858
  return normalized;
4583
4859
  }
4860
+ async function lostRoom(error, operation) {
4861
+ const normalized = normalizeMeetError(error, operation);
4862
+ store.patch({ failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason } });
4863
+ await dropRoom();
4864
+ store.dispatch({ type: "exit", to: "disconnected" });
4865
+ report("error", normalized.message, normalized.remedy);
4866
+ return normalized;
4867
+ }
4584
4868
  const handlers = {
4585
4869
  participants(list) {
4586
4870
  const joinedAt = new Date(now()).toISOString();
@@ -4623,9 +4907,12 @@ function createRoomEngine(options) {
4623
4907
  store.setQuality(asParticipantIdentity(who), value);
4624
4908
  },
4625
4909
  connectionState(state) {
4626
- if (state === "connected") store.patch({ phase: "connected", failure: null });
4627
- else if (state === "reconnecting") store.patch({ phase: "reconnecting" });
4628
- else if (state === "connecting") store.patch({ phase: "connecting" });
4910
+ if (state === "connected") {
4911
+ if (store.snapshot().connection !== "stable") store.patch({ connection: "stable" });
4912
+ store.dispatch({ type: "connected" });
4913
+ } else if (state === "reconnecting") {
4914
+ store.patch({ connection: "reconnecting" });
4915
+ }
4629
4916
  },
4630
4917
  transcription(event) {
4631
4918
  const snapshot = store.snapshot();
@@ -4752,17 +5039,48 @@ function createRoomEngine(options) {
4752
5039
  }
4753
5040
  },
4754
5041
  disconnected(reason) {
4755
- if (moving) return;
4756
- if (current !== null) {
4757
- tokens.invalidate(current.roomName);
4758
- current = null;
4759
- }
4760
- store.patch({ phase: "left" });
4761
- options.onLeft?.(reason);
5042
+ if (moving || driver === null) return;
5043
+ void resolveExitAndApply(reason);
4762
5044
  }
4763
5045
  };
5046
+ async function resolveExitAndApply(hint) {
5047
+ const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
5048
+ await dropRoom();
5049
+ let server;
5050
+ if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
5051
+ server = void 0;
5052
+ } else {
5053
+ try {
5054
+ server = await options.readExit(connection);
5055
+ } catch {
5056
+ server = null;
5057
+ }
5058
+ }
5059
+ if (disposed) return;
5060
+ const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
5061
+ store.patch({ connection: resolved.connection });
5062
+ if (resolved.to === "waiting:knocking") {
5063
+ store.patch({
5064
+ waiting: {
5065
+ admission: "knocking",
5066
+ admittersPresent: null,
5067
+ reknockAllowed: true,
5068
+ knockExpiresAt: null,
5069
+ pollSeconds: store.snapshot().waiting?.pollSeconds ?? 3,
5070
+ liveVersion: 0
5071
+ }
5072
+ });
5073
+ }
5074
+ store.dispatch({ type: "exit", to: resolved.to }, { reason: resolved.reason, notice: resolved.notice });
5075
+ report(
5076
+ resolved.to === "disconnected" ? "warn" : "info",
5077
+ `meet: the room connection ended (${hint ?? "no LiveKit reason"}) \u2192 ${resolved.to}`,
5078
+ resolved.to === "disconnected" ? "Press Rejoin; the meeting is still going." : "The screen says what happened."
5079
+ );
5080
+ options.onLeft?.(hint);
5081
+ }
4764
5082
  async function connect(request) {
4765
- store.patch({ phase: "requesting-token", failure: null, waiting: null });
5083
+ store.patch({ failure: null, waiting: null, roomName: request.roomName, share: NO_SHARE, connection: "stable" });
4766
5084
  const token = await tokens.get({
4767
5085
  roomName: request.roomName,
4768
5086
  identity,
@@ -4785,10 +5103,10 @@ function createRoomEngine(options) {
4785
5103
  }
4786
5104
  });
4787
5105
  if (token.lobby || !token.canSubscribe) {
5106
+ const admission = token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking";
4788
5107
  store.patch({
4789
- phase: "lobby",
4790
5108
  waiting: {
4791
- admission: token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking",
5109
+ admission,
4792
5110
  admittersPresent: null,
4793
5111
  reknockAllowed: true,
4794
5112
  knockExpiresAt: null,
@@ -4796,9 +5114,10 @@ function createRoomEngine(options) {
4796
5114
  liveVersion: 0
4797
5115
  }
4798
5116
  });
5117
+ store.dispatch({ type: "admission", admission });
4799
5118
  return;
4800
5119
  }
4801
- store.patch({ phase: "connecting", waiting: null });
5120
+ store.patch({ waiting: null });
4802
5121
  const created = await options.driverFactory(handlers);
4803
5122
  if (disposed) {
4804
5123
  await created.disconnect();
@@ -4806,7 +5125,8 @@ function createRoomEngine(options) {
4806
5125
  }
4807
5126
  driver = created;
4808
5127
  await created.connect(token.serverUrl, token.token);
4809
- store.patch({ phase: "connected", failure: null });
5128
+ store.patch({ failure: null, connection: "stable" });
5129
+ store.dispatch({ type: "connected" });
4810
5130
  store.setLocalTrack("microphone", IDLE_TRACK);
4811
5131
  store.setLocalTrack("camera", IDLE_TRACK);
4812
5132
  const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
@@ -4877,9 +5197,17 @@ function createRoomEngine(options) {
4877
5197
  return {
4878
5198
  join(request) {
4879
5199
  if (joining !== null) return joining;
5200
+ if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
5201
+ report(
5202
+ "warn",
5203
+ `meet.join: cannot join from "${store.snapshot().phase}"`,
5204
+ "Join is offered from the pre-join, left, disconnected and replaced screens only."
5205
+ );
5206
+ return Promise.resolve();
5207
+ }
4880
5208
  lastRequest = request;
4881
5209
  joining = connect(request).catch((error) => {
4882
- throw fail(error, "meet.join");
5210
+ throw failJoin(error, "meet.join");
4883
5211
  }).finally(() => {
4884
5212
  joining = null;
4885
5213
  });
@@ -4896,10 +5224,16 @@ function createRoomEngine(options) {
4896
5224
  return;
4897
5225
  }
4898
5226
  if (joining !== null) return joining;
5227
+ const phase = store.snapshot().phase;
5228
+ const moved = phase === "joining" || store.dispatch({ type: "join", click: true }) || store.dispatch({ type: "admitted" });
5229
+ if (!moved) {
5230
+ report("warn", `meet.rejoin: cannot rejoin from "${phase}"`, "Rejoin is offered after leaving, a lost connection, or admission.");
5231
+ return;
5232
+ }
4899
5233
  tokens.invalidate(request.roomName);
4900
5234
  current = null;
4901
5235
  joining = connect(request).catch((error) => {
4902
- throw fail(error, "meet.rejoin");
5236
+ throw failJoin(error, "meet.rejoin");
4903
5237
  }).finally(() => {
4904
5238
  joining = null;
4905
5239
  });
@@ -4944,11 +5278,11 @@ function createRoomEngine(options) {
4944
5278
  joining = connect(lastRequest).finally(() => {
4945
5279
  joining = null;
4946
5280
  });
4947
- await joining.catch((again) => {
4948
- throw fail(again, "meet.moveToBreakout");
5281
+ await joining.catch(async (again) => {
5282
+ throw await lostRoom(again, "meet.moveToBreakout");
4949
5283
  });
4950
5284
  } else {
4951
- throw fail(error, "meet.moveToBreakout");
5285
+ throw await lostRoom(error, "meet.moveToBreakout");
4952
5286
  }
4953
5287
  throw normalized;
4954
5288
  } finally {
@@ -4956,6 +5290,9 @@ function createRoomEngine(options) {
4956
5290
  }
4957
5291
  },
4958
5292
  leave: leaveRoom,
5293
+ backToPrejoin() {
5294
+ store.dispatch({ type: "back_to_prejoin", click: true });
5295
+ },
4959
5296
  // Turning a device on mid-meeting is the SAME operation as turning it on
4960
5297
  // during the join, so it runs the same function. A failure here used to
4961
5298
  // stamp the room `failed` — a busy microphone would blank a live meeting.
@@ -4969,21 +5306,41 @@ function createRoomEngine(options) {
4969
5306
  },
4970
5307
  async setScreenShareEnabled(enabled) {
4971
5308
  if (driver === null) return;
5309
+ if (enabled && driver.supportsScreenShare?.() === false) {
5310
+ store.patch({ share: { state: "refused", reason: "unsupported", remedy: null, at: now() } });
5311
+ report("warn", "meet: this browser cannot share a screen", "Share from a desktop browser (Chrome, Edge, Firefox or Safari).");
5312
+ return;
5313
+ }
5314
+ if (enabled) store.patch({ share: { state: "picking", reason: null, remedy: null, at: now() } });
4972
5315
  try {
4973
5316
  await driver.setScreenShareEnabled(enabled);
4974
5317
  ambiguousShareDenials = 0;
5318
+ store.patch({ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now() } });
4975
5319
  } catch (error) {
4976
5320
  const kind = classifyScreenShareError(error);
4977
5321
  if (kind === "ambiguous") {
4978
5322
  ambiguousShareDenials += 1;
4979
- if (ambiguousShareDenials < 2) return;
5323
+ if (ambiguousShareDenials < 2) {
5324
+ store.patch({ share: NO_SHARE });
5325
+ return;
5326
+ }
4980
5327
  }
5328
+ ambiguousShareDenials = 0;
4981
5329
  const normalized = normalizeScreenShareError(error, "meet.setScreenShareEnabled");
4982
5330
  report("warn", normalized.message, normalized.remedy);
4983
- const text3 = kind === "other" ? `Screen sharing is not available. ${normalized.remedy}` : `Screen sharing did not start. ${SCREEN_SHARE_BLOCKED_REMEDY}`;
4984
- store.patch({ actionNotice: { text: text3, failed: true, at: now() } });
5331
+ store.patch({
5332
+ share: {
5333
+ state: "refused",
5334
+ reason: kind === "other" ? "failed" : "blocked",
5335
+ remedy: kind === "other" ? normalized.remedy : SCREEN_SHARE_BLOCKED_REMEDY_FOR(normalized.remedy),
5336
+ at: now()
5337
+ }
5338
+ });
4985
5339
  }
4986
5340
  },
5341
+ dismissShareNotice() {
5342
+ if (store.snapshot().share.state === "refused") store.patch({ share: NO_SHARE });
5343
+ },
4987
5344
  async raiseHand(raised) {
4988
5345
  const local = store.snapshot().localIdentity;
4989
5346
  if (local !== null) store.setHand(local, raised);
@@ -5485,7 +5842,8 @@ function createLiveKitDriver(handlers, options = {}) {
5485
5842
  }).on(RoomEvent.Disconnected, (reason) => {
5486
5843
  audioContainer?.remove();
5487
5844
  audioContainer = null;
5488
- handlers.disconnected(reason === void 0 ? null : String(reason));
5845
+ const named = reason === void 0 ? null : module2?.DisconnectReason?.[reason] ?? String(reason);
5846
+ handlers.disconnected(named);
5489
5847
  });
5490
5848
  try {
5491
5849
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -5873,7 +6231,9 @@ function MeetRuntime(props) {
5873
6231
  driverFactory: (handlers) => (driverRef.current ?? createLiveKitDriver)(handlers),
5874
6232
  onDiagnostic: report,
5875
6233
  effectsStorage,
5876
- resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null
6234
+ resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null,
6235
+ // §3.4: on every LiveKit disconnect, ask the server what happened to this connection.
6236
+ readExit: (connection) => meetings.readExit(connection)
5877
6237
  });
5878
6238
  const meetings = createMeetingSession({
5879
6239
  manager,
@@ -5908,17 +6268,17 @@ function MeetRuntime(props) {
5908
6268
  const inRoom = createInRoomClient({ api, store, resolveRoomToken });
5909
6269
  store.subscribe(() => {
5910
6270
  const snapshot = store.snapshot();
5911
- if (snapshot.phase === "lobby" && snapshot.waiting !== null && (snapshot.waiting.admission === "knocking" || snapshot.waiting.admission === "waiting_host")) {
6271
+ if (isWaitingPhase(snapshot.phase) && snapshot.waiting !== null && (snapshot.waiting.admission === "knocking" || snapshot.waiting.admission === "waiting_host")) {
5912
6272
  meetings.startWaiting();
5913
- } else if (snapshot.phase !== "lobby") {
6273
+ } else if (!isWaitingPhase(snapshot.phase)) {
5914
6274
  meetings.stopWaiting();
5915
6275
  }
5916
6276
  });
5917
6277
  let chatReadFor = null;
5918
6278
  store.subscribe(() => {
5919
6279
  const snapshot = store.snapshot();
5920
- if (snapshot.phase !== "connected" || snapshot.meeting === null) {
5921
- if (snapshot.phase === "left" || snapshot.phase === "idle") chatReadFor = null;
6280
+ if (snapshot.phase !== "in_call" || snapshot.meeting === null) {
6281
+ if (snapshot.phase === "left" || snapshot.phase === "prejoin" || snapshot.phase === "resolving") chatReadFor = null;
5922
6282
  return;
5923
6283
  }
5924
6284
  if (chatReadFor === snapshot.meeting.id) return;
@@ -5971,6 +6331,10 @@ function MeetRuntime(props) {
5971
6331
  store,
5972
6332
  identity,
5973
6333
  onIncoming: (invite) => incomingRef.current?.(invite),
6334
+ // §3.6: my call was answered — the caller joins the call's room on the same machine.
6335
+ onAnswered: (invite) => {
6336
+ void engine.join({ roomName: invite.roomName, mode: invite.mode }).catch(() => void 0);
6337
+ },
5974
6338
  onDiagnostic: report,
5975
6339
  ...props.ringTimeoutMs !== void 0 ? { ringTimeoutMs: props.ringTimeoutMs } : {}
5976
6340
  });
@@ -6110,43 +6474,12 @@ function endedRecord(snapshot, given) {
6110
6474
  function observedPhase(input) {
6111
6475
  const { snapshot } = input;
6112
6476
  if (snapshot === null) return "resolving";
6113
- const raw = snapshot.phase;
6114
- const live = raw === "connected" || raw === "reconnecting";
6115
- if (endedRecord(snapshot, input.meeting ?? null) && !live) return "ended";
6116
- const reason = raw === "failed" ? snapshot.failure?.reason ?? null : null;
6117
- if (reason !== null) return reason === "ended" ? "ended" : `refused:${reason}`;
6118
- switch (raw) {
6119
- case "idle":
6120
- case "failed":
6121
- case "left":
6122
- return "prejoin";
6123
- case "lobby":
6124
- return waitingPhase(snapshot);
6125
- case "requesting-token":
6126
- case "connecting":
6127
- return "joining";
6128
- case "connected":
6129
- case "reconnecting":
6130
- return "in_call";
6131
- }
6477
+ const phase = snapshot.phase;
6478
+ if (endedRecord(snapshot, input.meeting ?? null) && !isLivePhase(phase) && !isCallPhase(phase)) return "ended";
6479
+ return phase;
6132
6480
  }
6133
- function waitingPhase(snapshot) {
6134
- switch (snapshot.waiting?.admission) {
6135
- case "waiting_host":
6136
- return "waiting:host_not_started";
6137
- case "denied":
6138
- return "denied";
6139
- case "expired":
6140
- return "knock_expired";
6141
- case "withdrawn":
6142
- return "left";
6143
- case "ended":
6144
- return "ended";
6145
- case "removed":
6146
- return "refused:removed";
6147
- default:
6148
- return "waiting:knocking";
6149
- }
6481
+ function isCallPhase(phase) {
6482
+ return phase.startsWith("ringing_") || phase.startsWith("call_") || phase === "answered_elsewhere";
6150
6483
  }
6151
6484
  var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
6152
6485
  function observe(input) {
@@ -6159,10 +6492,10 @@ function observe(input) {
6159
6492
  const deviceInPlay = phase === "joining" || inCall;
6160
6493
  return {
6161
6494
  phase,
6162
- reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
6495
+ reason: phase === "ended" ? snapshot?.reason ?? null : snapshot?.reason ?? reasonOf(phase),
6163
6496
  role,
6164
- connection: snapshot?.phase === "reconnecting" ? "reconnecting" : "stable",
6165
- hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot?.phase === "lobby" ? snapshot.waiting?.admittersPresent ?? null : null,
6497
+ connection: snapshot?.connection ?? "stable",
6498
+ hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
6166
6499
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
6167
6500
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
6168
6501
  audioBlocked: null,
@@ -6963,7 +7296,7 @@ function useRoom() {
6963
7296
  (participant) => participant.identity === snapshot.localIdentity
6964
7297
  ) ?? null;
6965
7298
  return {
6966
- phase: snapshot?.phase ?? "idle",
7299
+ phase: snapshot?.phase ?? "resolving",
6967
7300
  participants: snapshot?.participants ?? [],
6968
7301
  failure: snapshot?.failure ?? null,
6969
7302
  local,
@@ -8314,7 +8647,7 @@ function useSharedFocus(panelKey) {
8314
8647
  const snapshot = useMeetSnapshot();
8315
8648
  const focus = snapshot?.collab.focus ?? null;
8316
8649
  const mine = focus !== null && focus.panel === panelKey ? focus : null;
8317
- const canMove = !isObserverRole(snapshot?.grants.role) && snapshot?.phase === "connected";
8650
+ const canMove = !isObserverRole(snapshot?.grants.role) && snapshot?.phase === "in_call";
8318
8651
  return {
8319
8652
  section: mine?.section || null,
8320
8653
  byName: mine?.byName || null,
@@ -8390,6 +8723,36 @@ function useCollabPanelRequest(onRequest) {
8390
8723
  var MEET_REACTIONS = ["\u{1F496}", "\u{1F44D}", "\u{1F389}", "\u{1F44F}", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F914}", "\u{1F44E}"];
8391
8724
 
8392
8725
  // src/react/facade.ts
8726
+ var NO_ACTIONS = {
8727
+ join: async () => void 0,
8728
+ rejoin: async () => void 0,
8729
+ leave: async () => void 0,
8730
+ backToPrejoin: () => void 0,
8731
+ retry: () => void 0,
8732
+ resolved: () => void 0,
8733
+ dismissShareNotice: () => void 0,
8734
+ dismissCall: () => void 0
8735
+ };
8736
+ function actionsFor(host) {
8737
+ if (host === null) return NO_ACTIONS;
8738
+ const { engine, store } = host;
8739
+ return {
8740
+ join: (request) => engine.join(request),
8741
+ rejoin: () => engine.rejoin(),
8742
+ leave: () => engine.leave(),
8743
+ backToPrejoin: () => engine.backToPrejoin(),
8744
+ retry: () => {
8745
+ store.dispatch({ type: "retry", click: true });
8746
+ },
8747
+ resolved: () => {
8748
+ store.dispatch({ type: "resolved" });
8749
+ },
8750
+ dismissShareNotice: () => engine.dismissShareNotice(),
8751
+ dismissCall: () => {
8752
+ if (store.dispatch({ type: "reset" })) store.patch({ call: null });
8753
+ }
8754
+ };
8755
+ }
8393
8756
  function useMeetCore() {
8394
8757
  const host = useMeetHost();
8395
8758
  const snapshot = useMeetSnapshot();
@@ -8397,9 +8760,24 @@ function useMeetCore() {
8397
8760
  const role = useRole();
8398
8761
  const isHost = useIsHost();
8399
8762
  const observation = useCoreObservation();
8763
+ const actions = (0, import_react8.useMemo)(() => actionsFor(host), [host]);
8400
8764
  return (0, import_react8.useMemo)(
8401
- () => ({ host, snapshot, devices, role, isHost, observation }),
8402
- [host, snapshot, devices, role, isHost, observation]
8765
+ () => ({
8766
+ host,
8767
+ snapshot,
8768
+ devices,
8769
+ role,
8770
+ isHost,
8771
+ observation,
8772
+ phase: snapshot?.phase ?? "resolving",
8773
+ reason: snapshot?.reason ?? null,
8774
+ phaseNotice: snapshot?.phaseNotice ?? null,
8775
+ connection: snapshot?.connection ?? "stable",
8776
+ share: snapshot?.share ?? NO_SHARE,
8777
+ call: snapshot?.call ?? null,
8778
+ actions
8779
+ }),
8780
+ [host, snapshot, devices, role, isHost, observation, actions]
8403
8781
  );
8404
8782
  }
8405
8783
 
@@ -11330,11 +11708,52 @@ function MeetSkinRoom({
11330
11708
  headerControls,
11331
11709
  observe: observe2 = false
11332
11710
  }) {
11333
- const { host, snapshot, isHost } = useMeetCore();
11711
+ const core = useMeetCore();
11712
+ const { host, snapshot, isHost, phase } = core;
11334
11713
  const elapsed = useElapsed(snapshot?.meeting?.startedAt ?? null);
11335
- const phase = snapshot?.phase ?? "idle";
11336
11714
  if (host === null) return null;
11337
- if (phase === "idle" || phase === "failed" || phase === "left") {
11715
+ const screen = (() => {
11716
+ if (isRefusedPhase(phase)) return "refused";
11717
+ switch (phase) {
11718
+ case "resolving":
11719
+ return "resolving";
11720
+ case "prejoin":
11721
+ case "guest_name":
11722
+ return "prejoin";
11723
+ case "joining":
11724
+ case "in_call":
11725
+ return "room";
11726
+ case "waiting:host_not_started":
11727
+ case "waiting:knocking":
11728
+ case "denied":
11729
+ case "knock_expired":
11730
+ return "waiting";
11731
+ case "left":
11732
+ return snapshot?.waiting?.admission === "withdrawn" ? "waiting" : "exit";
11733
+ case "disconnected":
11734
+ case "removed":
11735
+ case "superseded":
11736
+ case "inactive":
11737
+ case "unsupported":
11738
+ case "ended":
11739
+ case "record":
11740
+ case "ringing_out":
11741
+ case "ringing_in":
11742
+ case "call_declined":
11743
+ case "call_missed":
11744
+ case "call_busy":
11745
+ case "answered_elsewhere":
11746
+ return "exit";
11747
+ default: {
11748
+ const unhandled = phase;
11749
+ return unhandled;
11750
+ }
11751
+ }
11752
+ })();
11753
+ if (screen === "resolving") {
11754
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__empty-body", role: "status", children: "Getting the meeting ready\u2026" }) });
11755
+ }
11756
+ if (screen === "prejoin") {
11338
11757
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11339
11758
  PreJoin,
11340
11759
  {
@@ -11345,9 +11764,12 @@ function MeetSkinRoom({
11345
11764
  }
11346
11765
  ) });
11347
11766
  }
11348
- if (phase === "lobby") {
11767
+ if (screen === "waiting") {
11349
11768
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet mx-meet--lobby", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(WaitingRoom, { host, ...onLeave !== void 0 ? { onLeave } : {} }) });
11350
11769
  }
11770
+ if (screen === "refused" || screen === "exit") {
11771
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet mx-meet--lobby", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ExitScreen, { ...onLeave !== void 0 ? { onLeave } : {} }) });
11772
+ }
11351
11773
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet", children: [
11352
11774
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ConsentNotice, {}),
11353
11775
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AttendanceNotice, {}),
@@ -11367,14 +11789,148 @@ function MeetSkinRoom({
11367
11789
  headerControls !== void 0 && headerControls !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__header-controls", "data-meet-header-controls": "", children: headerControls }) : null,
11368
11790
  isHost ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(HostMenu, {}) : null
11369
11791
  ] }),
11370
- phase === "reconnecting" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__banner", role: "status", children: "Reconnecting\u2026" }) : null,
11792
+ core.connection !== "stable" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__banner", role: "status", children: "Reconnecting\u2026" }) : null,
11793
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ShareNotice, {}),
11371
11794
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Stage, {}),
11372
11795
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Captions, {}),
11373
11796
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ControlBar, { ...onLeave !== void 0 ? { onLeave } : {} })
11374
11797
  ] });
11375
11798
  }
11799
+ function ShareNotice() {
11800
+ const { share, actions } = useMeetCore();
11801
+ if (share.state !== "refused") return null;
11802
+ 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";
11803
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "share-unavailable", children: [
11804
+ text3,
11805
+ share.remedy !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__remedy", children: share.remedy }) : null,
11806
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__link", onClick: actions.dismissShareNotice, children: "Dismiss" })
11807
+ ] });
11808
+ }
11809
+ var REFUSED_TITLES = {
11810
+ not_found: "Check your meeting code",
11811
+ cancelled: "This meeting was cancelled",
11812
+ expired: "This meeting link has expired",
11813
+ too_early: "This meeting hasn't started yet",
11814
+ sign_in_required: "Sign in to join this meeting",
11815
+ wrong_account: "You're signed in with a different account",
11816
+ not_invited: "You aren't invited to this meeting",
11817
+ locked: "This meeting is locked",
11818
+ removed: "You can't rejoin this meeting",
11819
+ lobby_rejects: "You can't ask to join this meeting",
11820
+ meeting_full: "This meeting is full",
11821
+ platform_capacity: "Meetings are busy right now",
11822
+ verification_required: "Verify your account to join",
11823
+ registration_required: "Register to join this meeting"
11824
+ };
11825
+ function ExitScreen({ onLeave }) {
11826
+ const { phase, reason, host, snapshot, actions, call } = useMeetCore();
11827
+ const [busy, setBusy] = (0, import_react10.useState)(false);
11828
+ const failure = snapshot?.failure ?? null;
11829
+ const rejoin = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11830
+ import_design_system.Button,
11831
+ {
11832
+ type: "button",
11833
+ disabled: busy,
11834
+ onClick: () => {
11835
+ setBusy(true);
11836
+ void actions.rejoin().catch(() => void 0).finally(() => setBusy(false));
11837
+ },
11838
+ children: "Rejoin"
11839
+ }
11840
+ );
11841
+ const home = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11842
+ "a",
11843
+ {
11844
+ className: "mx-meet__link",
11845
+ href: "/",
11846
+ onClick: (event) => {
11847
+ if (call !== null) {
11848
+ event.preventDefault();
11849
+ actions.dismissCall();
11850
+ }
11851
+ if (onLeave !== void 0) {
11852
+ event.preventDefault();
11853
+ onLeave();
11854
+ }
11855
+ },
11856
+ children: "Go home"
11857
+ }
11858
+ );
11859
+ let title;
11860
+ let line = null;
11861
+ let notice = null;
11862
+ let actionsNode = home;
11863
+ if (phase.startsWith("refused:")) {
11864
+ const code = phase.slice("refused:".length);
11865
+ title = REFUSED_TITLES[code] ?? "You can't join this meeting";
11866
+ line = failure?.remedy ?? null;
11867
+ actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11868
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Button, { type: "button", variant: "outline", onClick: actions.retry, children: "Try again" }),
11869
+ home
11870
+ ] });
11871
+ } else {
11872
+ switch (phase) {
11873
+ case "left":
11874
+ title = "You left the meeting";
11875
+ actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11876
+ rejoin,
11877
+ home
11878
+ ] });
11879
+ break;
11880
+ case "disconnected":
11881
+ title = "You lost your connection to the meeting";
11882
+ line = failure?.remedy ?? "Check your internet connection, then rejoin.";
11883
+ notice = "disconnected";
11884
+ actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11885
+ rejoin,
11886
+ home
11887
+ ] });
11888
+ break;
11889
+ case "removed":
11890
+ title = "You've been removed from the meeting";
11891
+ notice = "removed";
11892
+ break;
11893
+ case "superseded":
11894
+ title = "You joined from another tab or device";
11895
+ line = "This screen left the meeting so you aren't in it twice.";
11896
+ notice = "displaced";
11897
+ actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11898
+ rejoin,
11899
+ home
11900
+ ] });
11901
+ break;
11902
+ case "inactive":
11903
+ title = "You left the meeting because this tab was inactive";
11904
+ actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11905
+ rejoin,
11906
+ home
11907
+ ] });
11908
+ break;
11909
+ case "unsupported":
11910
+ title = "This browser can't join meetings";
11911
+ line = "Open the link in Chrome, Edge, Firefox or Safari.";
11912
+ break;
11913
+ case "ended":
11914
+ case "record":
11915
+ title = reason === "time_limit" ? "The meeting reached its time limit" : "The meeting has ended";
11916
+ notice = "meeting-ended";
11917
+ break;
11918
+ default:
11919
+ title = "The call has ended";
11920
+ break;
11921
+ }
11922
+ }
11923
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__waiting", "data-meet-exit": phase, children: [
11924
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Avatar, { name: host?.identity.displayName ?? "", url: host?.identity.avatarUrl ?? null, size: 72 }),
11925
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h1", { className: "mx-meet__prejoin-title", children: title }),
11926
+ line !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__empty-body", children: line }) : null,
11927
+ notice !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { role: "status", "data-meet-notice": notice, className: "mx-meet__sr-only", children: title }) : null,
11928
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__waiting-actions", children: actionsNode })
11929
+ ] });
11930
+ }
11376
11931
  function WaitingRoom({ host, onLeave }) {
11377
11932
  const { waiting, withdraw, askAgain } = useWaiting();
11933
+ const { phaseNotice } = useMeetCore();
11378
11934
  const [busy, setBusy] = (0, import_react10.useState)(false);
11379
11935
  const admission = waiting?.admission ?? "knocking";
11380
11936
  const run = (action) => () => {
@@ -11423,6 +11979,7 @@ function WaitingRoom({ host, onLeave }) {
11423
11979
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__waiting", children: [
11424
11980
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 72 }),
11425
11981
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h1", { className: "mx-meet__prejoin-title", children: title }),
11982
+ phaseNotice === "sent-to-waiting" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("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,
11426
11983
  line !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__empty-body", role: "status", "data-meet-notice": "host-absent", children: line }) : null,
11427
11984
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__waiting-actions", children: actions })
11428
11985
  ] });
@@ -11917,10 +12474,17 @@ function MeetingSkinRootBody({
11917
12474
  host
11918
12475
  }) {
11919
12476
  const snapshot = useMeetSnapshot();
11920
- const phase = snapshot?.phase ?? "idle";
12477
+ const phase = snapshot?.phase ?? "resolving";
11921
12478
  (0, import_react12.useEffect)(() => {
11922
12479
  host.meetings.watch(roomName);
11923
12480
  }, [host, roomName]);
12481
+ (0, import_react12.useEffect)(() => {
12482
+ const now = host.store.snapshot();
12483
+ if (now.phase !== "resolving" && now.roomName !== null && now.roomName !== roomName && now.call === null) {
12484
+ host.store.dispatch({ type: "reset" });
12485
+ }
12486
+ if (host.store.snapshot().phase === "resolving") host.store.dispatch({ type: "resolved" });
12487
+ }, [host, roomName, phase]);
11924
12488
  (0, import_react12.useEffect)(() => {
11925
12489
  if (given === void 0) return;
11926
12490
  if (host.store.snapshot().meeting !== null) return;
@@ -11932,7 +12496,7 @@ function MeetingSkinRootBody({
11932
12496
  host.meetings.watchMeeting(asMeetingId(watchedId));
11933
12497
  }, [host, watchedId]);
11934
12498
  const record = snapshot?.meeting ?? given ?? null;
11935
- if (hasEnded(record) && phase !== "connected" && phase !== "reconnecting") {
12499
+ if (hasEnded(record) && !isLivePhase(phase)) {
11936
12500
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(MeetingRecordView, { meeting: record });
11937
12501
  }
11938
12502
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
@@ -11949,11 +12513,49 @@ function MeetingSkinRootBody({
11949
12513
  }
11950
12514
 
11951
12515
  // src/react/calls-ui.tsx
12516
+ var import_react13 = require("react");
11952
12517
  var import_jsx_runtime9 = require("react/jsx-runtime");
11953
12518
  function IncomingCallHost() {
11954
12519
  const calls = useCalls();
11955
- if (calls.incoming.length === 0) return null;
11956
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "mx-meet__ringing-stack", children: calls.incoming.map((invite) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(IncomingCallCard, { invite }, invite.id)) });
12520
+ const { call, phase, actions } = useMeetCore();
12521
+ (0, import_react13.useEffect)(() => {
12522
+ if (call !== null && phase === "left") actions.dismissCall();
12523
+ }, [call, phase, actions]);
12524
+ const outcome = call !== null && (phase === "call_declined" || phase === "call_missed" || phase === "call_busy" || phase === "answered_elsewhere");
12525
+ (0, import_react13.useEffect)(() => {
12526
+ if (!outcome) return void 0;
12527
+ const timer = setTimeout(() => actions.dismissCall(), 5e3);
12528
+ return () => clearTimeout(timer);
12529
+ }, [outcome, actions]);
12530
+ const inCall = call !== null && (phase === "joining" || phase === "in_call" || phase === "disconnected" || phase === "removed" || phase === "superseded");
12531
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
12532
+ inCall && call !== null ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "mx-meet__call-overlay", "data-meet-call": call.invite.id, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(MeetRoot, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(MeetSkinRoom, { roomName: call.invite.roomName, onLeave: actions.dismissCall }) }) }) : null,
12533
+ calls.incoming.length > 0 || call !== null && (phase === "ringing_out" || outcome) ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "mx-meet__ringing-stack", children: [
12534
+ call !== null && phase === "ringing_out" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(OutgoingCallCard, { invite: call.invite }) : null,
12535
+ outcome && call !== null ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CallOutcomeCard, { invite: call.invite }) : null,
12536
+ calls.incoming.map((invite) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(IncomingCallCard, { invite }, invite.id))
12537
+ ] }) : null
12538
+ ] });
12539
+ }
12540
+ function OutgoingCallCard({ invite }) {
12541
+ const calls = useCalls();
12542
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { className: "mx-meet__ringing", role: "status", "aria-label": "Calling", "data-meet-notice": "ringing-out", children: [
12543
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Avatar, { name: invite.callerName, url: invite.callerAvatarUrl, size: 56 }),
12544
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "mx-meet__ringing-text", children: [
12545
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("strong", { children: "Calling\u2026" }),
12546
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: invite.mode === "video" ? "Video call" : "Voice call" })
12547
+ ] }),
12548
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "mx-meet__ringing-actions", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ControlButton, { label: "Cancel call", danger: true, onClick: () => void calls.cancel(invite), children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PhoneOffIcon, {}) }) })
12549
+ ] });
12550
+ }
12551
+ function CallOutcomeCard({ invite }) {
12552
+ const { phase, actions, call } = useMeetCore();
12553
+ const outgoing = call?.direction === "outgoing";
12554
+ 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}`;
12555
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { className: "mx-meet__ringing", role: "status", "aria-label": text3, "data-meet-call-outcome": phase, children: [
12556
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "mx-meet__ringing-text", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("strong", { children: text3 }) }),
12557
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "mx-meet__ringing-actions", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ControlButton, { label: "Close", onClick: actions.dismissCall, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PhoneOffIcon, {}) }) })
12558
+ ] });
11957
12559
  }
11958
12560
  function IncomingCallCard({ invite }) {
11959
12561
  const calls = useCalls();
@@ -12128,26 +12730,4 @@ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
12128
12730
  if (!response.ok) throw await refusal(response, operation);
12129
12731
  return await response.json();
12130
12732
  }
12131
-
12132
- // src/core/reasons.ts
12133
- var JOIN_REFUSAL_REASONS = [
12134
- "not_found",
12135
- "cancelled",
12136
- "ended",
12137
- "expired",
12138
- "too_early",
12139
- "sign_in_required",
12140
- "wrong_account",
12141
- "not_invited",
12142
- "locked",
12143
- "removed",
12144
- "lobby_rejects",
12145
- "meeting_full",
12146
- "platform_capacity",
12147
- "verification_required",
12148
- "registration_required"
12149
- ];
12150
- function isJoinRefusalReason(value) {
12151
- return typeof value === "string" && JOIN_REFUSAL_REASONS.includes(value);
12152
- }
12153
12733
  //# sourceMappingURL=react.cjs.map