@ethora/chat-component 26.10.7 → 26.10.9

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.
Files changed (78) hide show
  1. package/README.md +88 -3
  2. package/dist/ChatProfileModal-BspGrTF_.js +2298 -0
  3. package/dist/{FilePreviewModal-DlOjnYzv.js → FilePreviewModal-DEb--AKO.js} +130 -130
  4. package/dist/{ManageDataModal-BZLGJZze.js → ManageDataModal-B3AvV0ZK.js} +14 -14
  5. package/dist/{MarkdownBody-CtArwCDa.js → MarkdownBody-M-Tzn12K.js} +1 -1
  6. package/dist/{MessageSearchModal-CcUnfZlU.js → MessageSearchModal-aeBm31-J.js} +32 -32
  7. package/dist/{PublicChatsModal-BWn0NXVo.js → PublicChatsModal-CSxFQNuC.js} +19 -19
  8. package/dist/{SideDrawer-CqZl9sxK.js → SideDrawer-DQc3Dz7-.js} +28 -28
  9. package/dist/{UserProfileModal-D0JZ3-uy.js → UserProfileModal-BwXpXfkw.js} +124 -120
  10. package/dist/{UserSettingsModal-OvykYDsT.js → UserSettingsModal-BxWFLCst.js} +2 -2
  11. package/dist/{VideoCallOverlay-DrvO2v_g.js → VideoCallOverlay-BO_2G3ys.js} +5908 -4168
  12. package/dist/{VisibilityModal-pOF9-UXR.js → VisibilityModal-WY5EkrT1.js} +7 -7
  13. package/dist/assets/icons.d.ts +4 -0
  14. package/dist/calls/callDisplay.d.ts +22 -0
  15. package/dist/calls/callMode.d.ts +105 -0
  16. package/dist/calls/openScheduledCall.d.ts +20 -0
  17. package/dist/calls/p2p/P2PCallController.d.ts +160 -0
  18. package/dist/calls/p2p/icePolicy.d.ts +36 -0
  19. package/dist/calls/p2p/negotiator.d.ts +56 -0
  20. package/dist/calls/p2p/p2pRuntime.d.ts +35 -0
  21. package/dist/calls/p2p/signalInbox.d.ts +34 -0
  22. package/dist/calls/p2p/signalStanza.d.ts +51 -0
  23. package/dist/calls/placeCall.d.ts +20 -0
  24. package/dist/calls/scheduledCall.d.ts +101 -0
  25. package/dist/calls/scheduledCallActions.d.ts +12 -0
  26. package/dist/calls/scheduledCallFormat.d.ts +13 -0
  27. package/dist/calls/scheduledCallsConfig.d.ts +27 -0
  28. package/dist/calls/senderTrust.d.ts +7 -0
  29. package/dist/calls/startCall.d.ts +16 -0
  30. package/dist/components/MainComponents/ChatWrapper.d.ts +1 -0
  31. package/dist/components/MainComponents/LoginWrapper.d.ts +1 -0
  32. package/dist/components/MainComponents/ProfileImagePlaceholder.d.ts +1 -0
  33. package/dist/components/MainComponents/ReduxWrapper.d.ts +6 -0
  34. package/dist/components/MenuRoom/MenuRoom.d.ts +1 -0
  35. package/dist/components/Modals/NewChatModal/styledNewChatComponents.d.ts +27 -0
  36. package/dist/components/Modals/styledModalComponents.d.ts +21 -0
  37. package/dist/components/ScheduledCalls/ScheduleCallModal.d.ts +3 -0
  38. package/dist/components/ScheduledCalls/ScheduledCallCancelConfirm.d.ts +8 -0
  39. package/dist/components/ScheduledCalls/ScheduledCallCard.d.ts +7 -0
  40. package/dist/components/ScheduledCalls/ScheduledCallLayer.d.ts +15 -0
  41. package/dist/components/ScheduledCalls/ScheduledCallPreJoin.d.ts +3 -0
  42. package/dist/components/VideoCalls/P2PCallRunner.d.ts +1 -0
  43. package/dist/components/VideoCalls/P2PCallSession.d.ts +14 -0
  44. package/dist/components/VideoCalls/VideoCallSession.d.ts +37 -0
  45. package/dist/{handleCopyClick-BUbVvd2G.js → handleCopyClick-B5hswNPZ.js} +1 -1
  46. package/dist/helpers/callPush.d.ts +2 -0
  47. package/dist/helpers/canLeaveRoom.d.ts +3 -0
  48. package/dist/helpers/chatComponentConfigEvents.d.ts +16 -0
  49. package/dist/helpers/liveConfig.d.ts +46 -0
  50. package/dist/helpers/reportRoom.d.ts +18 -0
  51. package/dist/helpers/scheduledCallMessage.d.ts +23 -0
  52. package/dist/helpers/storeConsole.d.ts +1 -0
  53. package/dist/hooks/useNow.d.ts +6 -0
  54. package/dist/hooks/useRemoteAppearance.d.ts +21 -0
  55. package/dist/hooks/useTypography.d.ts +4 -0
  56. package/dist/{main-CpHMnydn.js → main-BNjPaiYs.js} +15479 -12971
  57. package/dist/main.d.ts +6 -2
  58. package/dist/main.js +25 -21
  59. package/dist/networking/api-requests/callState.api.d.ts +24 -0
  60. package/dist/networking/api-requests/push.api.d.ts +4 -5
  61. package/dist/networking/api-requests/remoteAppearance.api.d.ts +20 -0
  62. package/dist/networking/api-requests/rooms.api.d.ts +2 -1
  63. package/dist/networking/api-requests/scheduledCalls.api.d.ts +19 -0
  64. package/dist/networking/ethoraEvents.d.ts +14 -1
  65. package/dist/networking/xmpp/getRoomAccess.xmpp.d.ts +7 -0
  66. package/dist/networking/xmpp/notifyXmppEvent.d.ts +6 -0
  67. package/dist/networking/xmppClient.d.ts +2 -0
  68. package/dist/roomStore/callSlice.d.ts +23 -1
  69. package/dist/roomStore/chatSettingsSlice.d.ts +9 -1
  70. package/dist/roomStore/index.d.ts +4 -0
  71. package/dist/roomStore/scheduledCallSlice.d.ts +47 -0
  72. package/dist/types/models/config.model.d.ts +71 -1
  73. package/dist/types/models/message.model.d.ts +2 -0
  74. package/dist/types/models/room.model.d.ts +2 -0
  75. package/dist/types/models/scheduledCall.model.d.ts +116 -0
  76. package/dist/types/models/xmpp.model.d.ts +2 -0
  77. package/package.json +1 -1
  78. package/dist/ChatProfileModal-BDZvd0rm.js +0 -2168
@@ -1,7 +1,7 @@
1
1
  import { jsxs as r, jsx as s } from "react/jsx-runtime";
2
2
  import { useState as b, useEffect as x } from "react";
3
- import { U as o, a as L, M as P, u as U, b4 as D, b5 as z } from "./main-CpHMnydn.js";
4
- import { S as I, D as C, a as $, b as k, e as w, f as p } from "./SideDrawer-CqZl9sxK.js";
3
+ import { a4 as o, a as L, Z as P, u as z, bg as D, bh as I } from "./main-BNjPaiYs.js";
4
+ import { S as N, D as C, a as $, b as k, e as w, f as p } from "./SideDrawer-DQc3Dz7-.js";
5
5
  o.div``;
6
6
  o.div`
7
7
  font-size: 16px;
@@ -25,7 +25,7 @@ const O = o.div`
25
25
  `, u = o.input`
26
26
  accent-color: ${({ $radioColor: i }) => i || "var(--ethora-color-primary, #0052CD)"};
27
27
  margin: 0px;
28
- `, M = o.div`
28
+ `, R = o.div`
29
29
  padding: 10px 15px;
30
30
  margin-top: 16px;
31
31
  color: ${({ type: i }) => i === "success" ? "#10b981" : "#ef4444"};
@@ -42,9 +42,9 @@ const O = o.div`
42
42
  `, B = ({
43
43
  handleCloseModal: i
44
44
  }) => {
45
- const S = L(), e = P(), { user: n, config: l } = U(
45
+ const S = L(), e = P(), { user: n, config: l } = z(
46
46
  (t) => t.chatSettingStore
47
- ), g = (t) => S(z(t)), [a, m] = b(n?.isProfileOpen), [c, v] = b(n?.isAssetsOpen), [f, y] = b(null), d = (t, A) => {
47
+ ), g = (t) => S(I(t)), [a, m] = b(n?.isProfileOpen), [c, v] = b(n?.isAssetsOpen), [f, y] = b(null), d = (t, A) => {
48
48
  y({ message: t, type: A }), setTimeout(() => y(null), 3e3);
49
49
  };
50
50
  return x(() => {
@@ -56,7 +56,7 @@ const O = o.div`
56
56
  g(t.user), d(e("notification.saved"), "success");
57
57
  }).catch(() => d(e("toast.error"), "error"));
58
58
  }, [c]), /* @__PURE__ */ r(
59
- I,
59
+ N,
60
60
  {
61
61
  title: e("settings.visibility.title"),
62
62
  onClose: i,
@@ -128,7 +128,7 @@ const O = o.div`
128
128
  /* @__PURE__ */ s(p, { children: e("settings.visibility.individualDescription") })
129
129
  ] }) }) })
130
130
  ] }),
131
- f && /* @__PURE__ */ s(M, { type: f.type, children: f.message })
131
+ f && /* @__PURE__ */ s(R, { type: f.type, children: f.message })
132
132
  ]
133
133
  }
134
134
  );
@@ -43,6 +43,10 @@ export declare const VideoCallIcon: ({ color, ...props }: {
43
43
  [x: string]: any;
44
44
  color?: string;
45
45
  }) => import("react/jsx-runtime").JSX.Element;
46
+ export declare const CalendarIcon: ({ color, ...props }: {
47
+ [x: string]: any;
48
+ color?: string;
49
+ }) => import("react/jsx-runtime").JSX.Element;
46
50
  export declare const AudioCallIcon: ({ color, ...props }: {
47
51
  [x: string]: any;
48
52
  color?: string;
@@ -0,0 +1,22 @@
1
+ /** Shown on the ring screen when no human-readable name is known. */
2
+ export declare const NEUTRAL_INCOMING_CALL_LABEL = "Incoming call";
3
+ /**
4
+ * True for strings that are a chat's internal name or an id rather than
5
+ * something a person would be called: two user JIDs joined (the private chat
6
+ * name), a lone user id, an `<appId>_<uuid>` group name, or any long run of
7
+ * hex / underscore / hyphen.
8
+ */
9
+ export declare const looksLikeRawRoomName: (value: unknown) => boolean;
10
+ /**
11
+ * Plain-text, display-safe version of a name: control characters stripped,
12
+ * whitespace collapsed and trimmed, capped at about 80 characters. Returns ''
13
+ * when nothing usable is left or the value is a raw chat name / id.
14
+ */
15
+ export declare const sanitizeCallName: (value: unknown) => string;
16
+ /** sanitizeCallName with null instead of '' (state-friendly). */
17
+ export declare const safeCallName: (value: unknown) => string | null;
18
+ /**
19
+ * An xmppUsername from a stanza attribute, or null when it is not shaped like
20
+ * one (it is only ever used for lookups and routing, never shown).
21
+ */
22
+ export declare const sanitizeXmppUsername: (value: unknown) => string | null;
@@ -0,0 +1,105 @@
1
+ export type CallMode = 'livekit' | 'p2p';
2
+ export type CallKindName = 'audio' | 'video';
3
+ export type CallRoomType = 'private' | 'group';
4
+ /** Serializable subset of RTCIceServer (kept free of DOM types for redux). */
5
+ export interface IceServerConfig {
6
+ urls: string | string[];
7
+ username?: string;
8
+ credential?: string;
9
+ }
10
+ /**
11
+ * Accepts an array, an `{ iceServers }` envelope or a JSON string of either
12
+ * (the call-token stanza carries the list as a JSON attribute) and returns
13
+ * only well formed stun/turn entries. Anything else is dropped, never thrown.
14
+ */
15
+ export declare const normalizeIceServers: (raw: unknown) => IceServerConfig[];
16
+ /**
17
+ * A LiveKit server URL as the backend publishes it. Only websocket / http(s)
18
+ * URLs are accepted: the value ends up as the connect target of the media
19
+ * session, so anything else (javascript:, data:, garbage) is treated as unset.
20
+ */
21
+ export declare const normalizeLivekitUrl: (raw: unknown) => string | null;
22
+ /** Attributes of a `<data type="call-token">` element, defensively read. */
23
+ export interface ParsedCallTokenAttrs {
24
+ token: string;
25
+ room: string;
26
+ /** Raw `callId` attribute, null when absent (the JWT may still carry it). */
27
+ callId: string | null;
28
+ /** Valid `kind` attribute, null when absent or unrecognised. */
29
+ kind: CallKindName | null;
30
+ mode: CallMode;
31
+ group: boolean;
32
+ /** Callee's ICE servers for `mode=p2p`, null when absent or unparseable. */
33
+ iceServers: IceServerConfig[] | null;
34
+ /**
35
+ * Public LiveKit wss URL for `mode=livekit`, null when the server did not
36
+ * send one (older backends), it is empty, or it is not a usable URL.
37
+ */
38
+ livekitUrl: string | null;
39
+ /**
40
+ * Caller's display name as plain text (control characters stripped, capped,
41
+ * raw chat names refused); '' when absent. Only trustworthy from the system
42
+ * account, see calls/senderTrust.
43
+ */
44
+ callerName: string;
45
+ /** Caller's xmppUsername when well formed, else null. Same trust rule. */
46
+ caller: string | null;
47
+ }
48
+ /**
49
+ * Tolerant reader for the call-token stanza. A server that sends neither
50
+ * `mode` nor `group` (every server before P2P / group calls) yields
51
+ * `mode: 'livekit'`, `group: false`, i.e. exactly the legacy behaviour.
52
+ */
53
+ export declare const parseCallTokenAttrs: (attrs: Record<string, any> | undefined | null) => ParsedCallTokenAttrs;
54
+ /** Normalised `POST /v1/chats/call/create/:chatName` response. */
55
+ export interface CallCreateResult {
56
+ callId: string | null;
57
+ mode: CallMode;
58
+ kind: CallKindName;
59
+ /** Server-reported group flag, null when the server did not say. */
60
+ group: boolean | null;
61
+ /** Public LiveKit wss URL when the server knows it. */
62
+ livekitUrl: string | null;
63
+ iceServers: IceServerConfig[];
64
+ ttlSeconds: number | null;
65
+ }
66
+ /**
67
+ * Reads the create response. Old servers answer with no `mode`, which maps to
68
+ * `livekit` (the caller then waits for its own call-token as before). A p2p
69
+ * answer without a callId is unusable (no way to correlate signalling) and
70
+ * throws.
71
+ */
72
+ export declare const parseCallCreateResponse: (data: unknown, fallbackKind: CallKindName) => CallCreateResult;
73
+ /**
74
+ * Which room types may start / receive calls. Hosts that never set
75
+ * `allowedRoomTypes` get 1:1 and group chats; an explicit list is respected
76
+ * as written (so `['private']` keeps a host on 1:1 only).
77
+ */
78
+ export declare const DEFAULT_CALL_ROOM_TYPES: CallRoomType[];
79
+ export declare const isCallRoomTypeAllowed: (roomType: string | undefined | null, allowedRoomTypes?: CallRoomType[] | null) => boolean;
80
+ export interface BusyCallSnapshot {
81
+ phase: string;
82
+ direction: 'incoming' | 'outgoing' | null;
83
+ mode: CallMode;
84
+ callId: string | null;
85
+ roomBareName: string | null;
86
+ }
87
+ /**
88
+ * What to do with an incoming call-token while another call may be active.
89
+ * - accept: nothing active, ring.
90
+ * - duplicate: same callId as the active call (server re-delivery), ignore.
91
+ * - replace: legacy LiveKit behaviour (a new token overwrites the old
92
+ * call), kept exactly for livekit-vs-livekit.
93
+ * - glare-yield: both sides dialled each other; the lexicographically smaller
94
+ * callId wins on BOTH ends, and this is the loser: drop our
95
+ * outgoing call and ring for theirs.
96
+ * - glare-keep: same race, we hold the winning callId, ignore theirs (the
97
+ * other side yields).
98
+ * - busy: a different call is in progress, decline the newcomer.
99
+ */
100
+ export type IncomingWhileBusyDecision = 'accept' | 'duplicate' | 'replace' | 'glare-yield' | 'glare-keep' | 'busy';
101
+ export declare const decideIncomingWhileBusy: (current: BusyCallSnapshot, incoming: {
102
+ mode: CallMode;
103
+ callId: string | null;
104
+ roomBareName: string | null;
105
+ }) => IncomingWhileBusyDecision;
@@ -0,0 +1,20 @@
1
+ import { ScheduledCallLinkOptions } from './scheduledCall';
2
+ /**
3
+ * Asks the chat to open a scheduled call: select its room and show the
4
+ * pre-join panel. Returns false (and does nothing) when `slug` is not a
5
+ * plausible slug. The request waits in the store until a signed-in <Chat> is
6
+ * mounted, so a deep link can be passed in before either is ready; failures
7
+ * are reported through `config.scheduledCalls.onOpenError`.
8
+ */
9
+ export declare function openScheduledCall(slug: string): boolean;
10
+ /**
11
+ * The slug of a scheduled-call join link (`https://<host>/call/<slug>` or
12
+ * `<scheme>://call/<slug>`), or null when `url` is not one. The hosted web app
13
+ * (`DEFAULT_SCHEDULED_CALL_HOSTS`) and the page's own host are always
14
+ * accepted, plus `options.hosts` or, without them, the hosts of
15
+ * `config.scheduledCalls`. Schemes: `options.schemes`, else the configured
16
+ * ones, unset means any. Same name, options and defaults as the React Native SDK.
17
+ */
18
+ /** The hosted web app: always accepted. */
19
+ export declare const DEFAULT_SCHEDULED_CALL_HOSTS: string[];
20
+ export declare function parseScheduledCallLink(url: unknown, options?: ScheduledCallLinkOptions): string | null;
@@ -0,0 +1,160 @@
1
+ import { IceServerConfig } from '../callMode';
2
+ import { P2PRole } from './negotiator';
3
+ import { P2PSignal, P2PSignalName, P2PSignalPayload } from './signalStanza';
4
+ export type P2PStatus = 'preparing' | 'waiting' | 'connecting' | 'connected' | 'reconnecting' | 'failed' | 'ended';
5
+ export interface P2PSnapshot {
6
+ status: P2PStatus;
7
+ localStream: MediaStream | null;
8
+ remoteStream: MediaStream | null;
9
+ micEnabled: boolean;
10
+ camEnabled: boolean;
11
+ remoteVideoLive: boolean;
12
+ /** Dismissible, non-fatal hint (e.g. the camera could not be re-enabled). */
13
+ notice: string | null;
14
+ }
15
+ export interface P2PCallParams {
16
+ callId: string;
17
+ role: P2PRole;
18
+ kind: 'audio' | 'video';
19
+ /** Peer's xmpp localpart; signals from anyone else are ignored. */
20
+ peerLocal: string;
21
+ iceServers: IceServerConfig[];
22
+ startWithMicOn?: boolean;
23
+ startWithCameraOn?: boolean;
24
+ }
25
+ export interface P2PCallDeps {
26
+ createPeerConnection: (config: RTCConfiguration) => RTCPeerConnection;
27
+ getUserMedia: (constraints: MediaStreamConstraints) => Promise<MediaStream>;
28
+ createMediaStream: () => MediaStream;
29
+ /** Fresh credentials; null/empty keeps the servers already in use. */
30
+ fetchIceServers: () => Promise<IceServerConfig[] | null>;
31
+ sendSignal: (signal: P2PSignalName, payload: P2PSignalPayload) => void;
32
+ log?: (message: string, detail?: unknown) => void;
33
+ }
34
+ export interface P2PCallEvents {
35
+ /** The offer/answer exchange completed: media is now negotiating. */
36
+ onNegotiated?: () => void;
37
+ /** Media is flowing for the first time. */
38
+ onConnected?: () => void;
39
+ /** The peer ended the call (hang up, decline, their own failure). */
40
+ onRemoteEnd?: (reason: string) => void;
41
+ /** The call failed locally; the message is meant for the user. */
42
+ onFailed?: (message: string) => void;
43
+ /**
44
+ * Called exactly once when the call's resources are released. `started` is
45
+ * false for a callee that never accepted (nothing was negotiated).
46
+ */
47
+ onDisposed?: (info: {
48
+ started: boolean;
49
+ }) => void;
50
+ }
51
+ export interface P2PTimings {
52
+ connectTimeoutMs: number;
53
+ offerResendMs: number[];
54
+ }
55
+ export declare const describeMediaError: (error: unknown) => string;
56
+ /**
57
+ * What to show when the PEER ended the call for a reason that is a failure
58
+ * rather than a normal hang up / decline. Null means "just close the call".
59
+ */
60
+ export declare const remoteEndNotice: (reason: string) => string | null;
61
+ export declare class P2PCallController {
62
+ readonly callId: string;
63
+ readonly role: P2PRole;
64
+ readonly kind: 'audio' | 'video';
65
+ private readonly params;
66
+ private readonly deps;
67
+ private readonly events;
68
+ private readonly timings;
69
+ private readonly negotiator;
70
+ private readonly policy;
71
+ private snapshot;
72
+ private readonly listeners;
73
+ private pc;
74
+ private localStream;
75
+ private audioTrack;
76
+ private videoTrack;
77
+ private videoSender;
78
+ private ownedTracks;
79
+ private remoteStream;
80
+ private remoteCleanups;
81
+ private queue;
82
+ private timers;
83
+ private graceTimer;
84
+ private deadlineTimer;
85
+ private connectTimer;
86
+ private started;
87
+ private disposed;
88
+ private endSent;
89
+ private connectedOnce;
90
+ private negotiated;
91
+ private cameraBusy;
92
+ private remoteVideoWatched;
93
+ private remoteVideoStalled;
94
+ private lastConn;
95
+ private candidateGateOpen;
96
+ private heldOutgoingCandidates;
97
+ private sentCandidates;
98
+ private lastOffer;
99
+ constructor(params: P2PCallParams, deps: P2PCallDeps, events?: P2PCallEvents, timings?: Partial<P2PTimings>);
100
+ subscribe: (listener: () => void) => (() => void);
101
+ getSnapshot: () => P2PSnapshot;
102
+ private update;
103
+ private log;
104
+ private later;
105
+ private cancel;
106
+ /**
107
+ * Caller: get media, build the connection, offer. Callee: get media, build
108
+ * the connection, then apply the offer that may already be waiting. Safe to
109
+ * call once; every failure ends the call with a message instead of throwing.
110
+ */
111
+ start(): Promise<void>;
112
+ /** Feed a signal from the peer (already routed by callId). */
113
+ receive(signal: P2PSignal): void;
114
+ /** Local user hung up (or cancelled): tell the peer and release everything. */
115
+ hangup(reason?: string): void;
116
+ /**
117
+ * Release every resource: timers, listeners, media tracks, the connection.
118
+ * Idempotent, safe from any exit path. Tells the peer unless it is already
119
+ * known to be over.
120
+ */
121
+ dispose(): void;
122
+ private sendEnd;
123
+ private fail;
124
+ private handleRemoteEnd;
125
+ private acquireMedia;
126
+ private releaseMedia;
127
+ toggleMic(): void;
128
+ /**
129
+ * Turning the camera off stops sending video and releases the device (the
130
+ * camera light goes out); turning it on opens the camera again and swaps it
131
+ * into the existing sender, so no renegotiation is needed.
132
+ */
133
+ toggleCamera(): Promise<void>;
134
+ dismissNotice(): void;
135
+ private createConnection;
136
+ private emitCandidate;
137
+ private openCandidateGate;
138
+ private onRemoteTrack;
139
+ private remoteVideoLive;
140
+ /**
141
+ * A peer that turns its camera off drops the sender's track (replaceTrack
142
+ * with null). Chrome does not mute the receiving track for that, so the
143
+ * `mute` event alone leaves the last frame frozen on screen. Frames that stop
144
+ * arriving are the signal that does work in every browser, so the received
145
+ * video is polled and a stall counts as "no live picture" until frames flow
146
+ * again.
147
+ */
148
+ private watchRemoteVideo;
149
+ private enqueue;
150
+ private sendOffer;
151
+ private scheduleOfferResends;
152
+ private runCommands;
153
+ private applyOffer;
154
+ private onNegotiated;
155
+ private connState;
156
+ private onConnectionState;
157
+ private runIceAction;
158
+ /** ICE restart with fresh TURN credentials; the caller of this is the policy. */
159
+ private restartIce;
160
+ }
@@ -0,0 +1,36 @@
1
+ export type P2PConnState = 'new' | 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed';
2
+ export type IceAction = {
3
+ type: 'none';
4
+ } | {
5
+ type: 'wait';
6
+ ms: number;
7
+ } | {
8
+ type: 'restart';
9
+ deadlineMs: number;
10
+ } | {
11
+ type: 'recovered';
12
+ } | {
13
+ type: 'give-up';
14
+ reason: string;
15
+ };
16
+ export interface IcePolicyOptions {
17
+ graceMs?: number;
18
+ restartDeadlineMs?: number;
19
+ maxRestartsPerCall?: number;
20
+ }
21
+ export declare class IceRestartPolicy {
22
+ private readonly graceMs;
23
+ private readonly restartDeadlineMs;
24
+ private readonly maxRestarts;
25
+ private restarts;
26
+ private attemptedThisOutage;
27
+ private inOutage;
28
+ constructor(options?: IcePolicyOptions);
29
+ get restartCount(): number;
30
+ private tryRestart;
31
+ onState(state: P2PConnState): IceAction;
32
+ /** The grace period after `disconnected` ran out without recovery. */
33
+ onGraceElapsed(): IceAction;
34
+ /** The restart deadline ran out; `stillDown` is the state at that moment. */
35
+ onRestartDeadline(stillDown: boolean): IceAction;
36
+ }
@@ -0,0 +1,56 @@
1
+ import { P2PSignal } from './signalStanza';
2
+ export type P2PRole = 'caller' | 'callee';
3
+ export type NegotiatorCommand = {
4
+ type: 'apply-offer';
5
+ sdp: string;
6
+ initial: boolean;
7
+ rollback: boolean;
8
+ } | {
9
+ type: 'apply-answer';
10
+ sdp: string;
11
+ } | {
12
+ type: 'add-candidate';
13
+ candidate: RTCIceCandidateInit;
14
+ } | {
15
+ type: 'remote-end';
16
+ reason: string;
17
+ };
18
+ export declare class P2PNegotiator {
19
+ readonly callId: string;
20
+ readonly role: P2PRole;
21
+ private readonly peerLocal;
22
+ private ready;
23
+ private ended;
24
+ private remoteDescriptionSet;
25
+ private initialOfferTaken;
26
+ private heldOffer;
27
+ private localOfferPending;
28
+ private lastOfferSdp;
29
+ private lastAnswerSdp;
30
+ private heldCandidates;
31
+ private seenCandidates;
32
+ constructor(params: {
33
+ callId: string;
34
+ role: P2PRole;
35
+ peerLocal: string;
36
+ });
37
+ get isEnded(): boolean;
38
+ /** True while we have sent an offer and not yet received its answer. */
39
+ get awaitingAnswer(): boolean;
40
+ private accepts;
41
+ receive(signal: P2PSignal): NegotiatorCommand[];
42
+ /**
43
+ * Local media and the peer connection exist. For the callee this releases
44
+ * the offer that arrived while it was still on the ring screen.
45
+ */
46
+ markReady(): NegotiatorCommand[];
47
+ /** We created and set a local offer; its answer is now expected. */
48
+ markLocalOffer(): void;
49
+ /**
50
+ * A remote description (offer or answer) was applied to the connection.
51
+ * Returns the candidates that were waiting for it.
52
+ */
53
+ markRemoteApplied(): NegotiatorCommand[];
54
+ /** Local teardown: later signals are ignored. */
55
+ stop(): void;
56
+ }
@@ -0,0 +1,35 @@
1
+ import { Element } from 'ltx';
2
+ import { P2PSignalInbox } from './signalInbox';
3
+ import { P2PSignalName, P2PSignalPayload } from './signalStanza';
4
+ import { P2PCallController, P2PCallDeps } from './P2PCallController';
5
+ /** Signals that arrive for a call nobody is handling yet wait here. */
6
+ export declare const p2pInbox: P2PSignalInbox;
7
+ /**
8
+ * Entry point for an incoming `<data type="call-p2p">` message (called from
9
+ * the stanza router). Stale, replayed, malformed or self-sent signals are
10
+ * dropped; the rest go to the inbox, which delivers or briefly holds them.
11
+ */
12
+ export declare const handleP2PSignalStanza: (inner: Element) => void;
13
+ /** Sends one signal straight to the peer's bare JID over the live XMPP client. */
14
+ export declare const sendP2PSignal: (params: {
15
+ peerXmppUsername: string;
16
+ callId: string;
17
+ signal: P2PSignalName;
18
+ payload: P2PSignalPayload;
19
+ }) => boolean;
20
+ /** The real browser dependencies for one call's controller. */
21
+ export declare const createBrowserP2PDeps: (params: {
22
+ callId: string;
23
+ peerXmppUsername: string;
24
+ }) => P2PCallDeps;
25
+ export declare const getActiveP2PController: () => P2PCallController | null;
26
+ export declare const subscribeActiveP2P: (listener: () => void) => (() => void);
27
+ /**
28
+ * Returns the controller for `key`, creating it on first use. Effects that
29
+ * unmount and immediately remount (React StrictMode, a parent re-keying) get
30
+ * the SAME controller back instead of tearing the call down and starting a
31
+ * second negotiation: release() only disposes after a tick, and acquire()
32
+ * within that tick cancels it.
33
+ */
34
+ export declare const acquireP2PController: (key: string, create: () => P2PCallController) => P2PCallController;
35
+ export declare const releaseP2PController: (key: string) => void;
@@ -0,0 +1,34 @@
1
+ import { P2PSignal } from './signalStanza';
2
+ export interface SignalInboxOptions {
3
+ /** How long signals for a call with no listener are kept. */
4
+ ttlMs?: number;
5
+ /** Most distinct unclaimed callIds held at once (oldest dropped first). */
6
+ maxPendingCalls?: number;
7
+ /** Most signals kept per unclaimed call. */
8
+ maxPerCall?: number;
9
+ now?: () => number;
10
+ }
11
+ export type SignalDelivery = 'delivered' | 'buffered' | 'dropped';
12
+ type Handler = (signal: P2PSignal) => void;
13
+ export declare class P2PSignalInbox {
14
+ private readonly ttlMs;
15
+ private readonly maxPendingCalls;
16
+ private readonly maxPerCall;
17
+ private readonly now;
18
+ private readonly pending;
19
+ private readonly handlers;
20
+ private readonly closed;
21
+ constructor(options?: SignalInboxOptions);
22
+ private sweep;
23
+ receive(signal: P2PSignal): SignalDelivery;
24
+ /**
25
+ * Starts delivering a call's signals to `handler`, replaying anything held
26
+ * for it first (in arrival order). Returns a detach function that also
27
+ * marks the call closed.
28
+ */
29
+ attach(callId: string, handler: Handler): () => void;
30
+ /** The call is over: forget held signals and drop future stragglers. */
31
+ close(callId: string): void;
32
+ clear(): void;
33
+ }
34
+ export {};
@@ -0,0 +1,51 @@
1
+ import { Element } from 'ltx';
2
+ export declare const P2P_SIGNAL_TYPE = "call-p2p";
3
+ export type P2PSignalName = 'offer' | 'answer' | 'candidate' | 'end';
4
+ export interface P2PSdpPayload {
5
+ type: 'offer' | 'answer';
6
+ sdp: string;
7
+ }
8
+ export interface P2PCandidatePayload {
9
+ candidate: string;
10
+ sdpMid?: string | null;
11
+ sdpMLineIndex?: number | null;
12
+ usernameFragment?: string | null;
13
+ }
14
+ export interface P2PEndPayload {
15
+ reason?: string;
16
+ }
17
+ export type P2PSignalPayload = P2PSdpPayload | P2PCandidatePayload | P2PEndPayload;
18
+ export interface P2PSignal {
19
+ callId: string;
20
+ signal: P2PSignalName;
21
+ payload: P2PSignalPayload;
22
+ /** Sender's bare JID as written in `from` (lower-cased), '' when missing. */
23
+ fromBare: string;
24
+ /** Sender's xmpp localpart (the part before '@'), '' when missing. */
25
+ fromLocal: string;
26
+ /**
27
+ * Age of the stanza from the server's stanza-id timestamp, null when the
28
+ * stanza carries none (direct messages often do not).
29
+ */
30
+ ageMs: number | null;
31
+ /** Carries <delay/>: it was stored offline and replayed, so it is stale. */
32
+ delayed: boolean;
33
+ }
34
+ export declare const MAX_P2P_SIGNAL_AGE_MS = 15000;
35
+ export declare const buildP2PSignalStanza: (params: {
36
+ to: string;
37
+ callId: string;
38
+ signal: P2PSignalName;
39
+ payload?: P2PSignalPayload;
40
+ }) => Element;
41
+ /**
42
+ * Reads a `call-p2p` message. Returns null for anything that is not a
43
+ * well formed signal (wrong type, unknown signal, bad JSON, bad payload).
44
+ */
45
+ export declare const parseP2PSignalStanza: (stanza: Element, now?: number) => P2PSignal | null;
46
+ /**
47
+ * Whether a parsed signal belongs to a live negotiation. An `end` is always
48
+ * accepted unless replayed (it only ever acts on a call that is still
49
+ * active, and stopping a call late is better than never).
50
+ */
51
+ export declare const isP2PSignalFresh: (signal: P2PSignal) => boolean;
@@ -0,0 +1,20 @@
1
+ import { Dispatch } from '@reduxjs/toolkit';
2
+ import { IRoom } from '../types/types';
3
+ import { CallKindName } from './callMode';
4
+ /**
5
+ * Dials a call in `room`: the one path every "start or join a call" entry
6
+ * point shares (the chat header buttons and the scheduled-call Join button).
7
+ * Which transport carries it is the server's decision (p2p for 1:1, LiveKit
8
+ * for groups); a group call that is already running is joined, not duplicated.
9
+ *
10
+ * `chatName` is the bare room name (the JID local part), never the display
11
+ * title. Failures surface through the call slice's error state.
12
+ */
13
+ export declare function placeRoomCall(params: {
14
+ dispatch: Dispatch;
15
+ room: Pick<IRoom, 'jid' | 'name' | 'members'> & {
16
+ type?: IRoom['type'];
17
+ };
18
+ kind: CallKindName;
19
+ selfXmppUsername?: string | null;
20
+ }): Promise<void>;