@enfin/chat 1.2.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.
Files changed (73) hide show
  1. package/README.md +224 -0
  2. package/dist/assets/music/i_phone_message.mp3 +0 -0
  3. package/dist/call/WebRTCManager.d.ts +54 -0
  4. package/dist/call/WebRTCManager.js +274 -0
  5. package/dist/call/signaling.d.ts +3 -0
  6. package/dist/call/signaling.js +24 -0
  7. package/dist/call/types.d.ts +25 -0
  8. package/dist/call/types.js +13 -0
  9. package/dist/components/Chat.d.ts +14 -0
  10. package/dist/components/Chat.js +452 -0
  11. package/dist/components/index.d.ts +3 -0
  12. package/dist/components/index.js +2 -0
  13. package/dist/context/ChatContext.d.ts +59 -0
  14. package/dist/context/ChatContext.js +647 -0
  15. package/dist/esm/call/WebRTCManager.d.ts +54 -0
  16. package/dist/esm/call/WebRTCManager.js +274 -0
  17. package/dist/esm/call/signaling.d.ts +3 -0
  18. package/dist/esm/call/signaling.js +24 -0
  19. package/dist/esm/call/types.d.ts +25 -0
  20. package/dist/esm/call/types.js +13 -0
  21. package/dist/esm/components/Chat.d.ts +14 -0
  22. package/dist/esm/components/Chat.js +452 -0
  23. package/dist/esm/components/index.d.ts +3 -0
  24. package/dist/esm/components/index.js +2 -0
  25. package/dist/esm/context/ChatContext.d.ts +59 -0
  26. package/dist/esm/context/ChatContext.js +647 -0
  27. package/dist/esm/hooks/index.d.ts +6 -0
  28. package/dist/esm/hooks/index.js +6 -0
  29. package/dist/esm/hooks/useCall.d.ts +15 -0
  30. package/dist/esm/hooks/useCall.js +38 -0
  31. package/dist/esm/hooks/useChat.d.ts +23 -0
  32. package/dist/esm/hooks/useChat.js +40 -0
  33. package/dist/esm/hooks/useMessages.d.ts +17 -0
  34. package/dist/esm/hooks/useMessages.js +38 -0
  35. package/dist/esm/hooks/usePresence.d.ts +4 -0
  36. package/dist/esm/hooks/usePresence.js +12 -0
  37. package/dist/esm/hooks/useRooms.d.ts +9 -0
  38. package/dist/esm/hooks/useRooms.js +19 -0
  39. package/dist/esm/hooks/useSocket.d.ts +7 -0
  40. package/dist/esm/hooks/useSocket.js +92 -0
  41. package/dist/esm/index.d.ts +11 -0
  42. package/dist/esm/index.js +15 -0
  43. package/dist/esm/utils/index.d.ts +2 -0
  44. package/dist/esm/utils/index.js +2 -0
  45. package/dist/esm/utils/ringtone.d.ts +2 -0
  46. package/dist/esm/utils/ringtone.js +39 -0
  47. package/dist/esm/utils/testUtils.d.ts +102 -0
  48. package/dist/esm/utils/testUtils.js +153 -0
  49. package/dist/hooks/index.d.ts +6 -0
  50. package/dist/hooks/index.js +6 -0
  51. package/dist/hooks/useCall.d.ts +15 -0
  52. package/dist/hooks/useCall.js +38 -0
  53. package/dist/hooks/useChat.d.ts +23 -0
  54. package/dist/hooks/useChat.js +40 -0
  55. package/dist/hooks/useMessages.d.ts +17 -0
  56. package/dist/hooks/useMessages.js +38 -0
  57. package/dist/hooks/usePresence.d.ts +4 -0
  58. package/dist/hooks/usePresence.js +12 -0
  59. package/dist/hooks/useRooms.d.ts +9 -0
  60. package/dist/hooks/useRooms.js +19 -0
  61. package/dist/hooks/useSocket.d.ts +7 -0
  62. package/dist/hooks/useSocket.js +92 -0
  63. package/dist/index.d.ts +11 -0
  64. package/dist/index.js +15 -0
  65. package/dist/public/music/i_phone_message.mp3 +0 -0
  66. package/dist/style.css +1226 -0
  67. package/dist/utils/index.d.ts +2 -0
  68. package/dist/utils/index.js +2 -0
  69. package/dist/utils/ringtone.d.ts +2 -0
  70. package/dist/utils/ringtone.js +39 -0
  71. package/dist/utils/testUtils.d.ts +102 -0
  72. package/dist/utils/testUtils.js +153 -0
  73. package/package.json +44 -0
package/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # @enfin/chat
2
+
3
+ React frontend SDK for chat platform — components, hooks, and context provider.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @enfin/chat react react-dom socket.io-client
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ### 1. Wrap Your App with ChatProvider
14
+
15
+ ```tsx
16
+ import { ChatProvider } from '@enfin/chat';
17
+ import Chat from '@enfin/chat';
18
+
19
+ function App() {
20
+ return (
21
+ <ChatProvider
22
+ config={{ apiKey: 'chat_xxx' }}
23
+ userId="user_123"
24
+ userName="Alice"
25
+ serverUrl="http://localhost:3002"
26
+ >
27
+ <Chat />
28
+ </ChatProvider>
29
+ );
30
+ }
31
+ ```
32
+
33
+ Pass `serverUrl` as your chat-server URL. It connects to `${serverUrl}/chat` namespace and calls REST at `${serverUrl}/api/*`.
34
+
35
+ ## ChatProvider Props
36
+
37
+ | Prop | Type | Required | Description |
38
+ |------|------|----------|-------------|
39
+ | `config` | `ChatConfig` | Yes | `{ apiKey: string }` |
40
+ | `userId` | string | Yes | Unique user ID |
41
+ | `userName` | string | Yes | Display name |
42
+ | `serverUrl` | string | Yes | Your chat-server URL |
43
+
44
+ ### ChatConfig
45
+
46
+ ```ts
47
+ interface ChatConfig {
48
+ apiKey: string;
49
+ validationUrl?: string; // optional custom validation endpoint
50
+ ringtoneUrl?: string; // optional override for the incoming-call ringtone
51
+ }
52
+ ```
53
+
54
+ #### `ringtoneUrl` — custom incoming-call ringtone
55
+
56
+ By default the SDK plays a bundled `i_phone_message.mp3` whenever an audio call comes in. Pass `ringtoneUrl` to override it with your own file. Works with absolute URLs or any path the consumer's bundler/dev server can resolve.
57
+
58
+ ```tsx
59
+ <ChatProvider
60
+ config={{
61
+ apiKey: 'chat_xxx',
62
+ ringtoneUrl: '/sounds/my-ring.mp3', // served by your app
63
+ }}
64
+ userId="user_123"
65
+ userName="Alice"
66
+ serverUrl="http://localhost:3002"
67
+ >
68
+ ```
69
+
70
+ - Anything that HTML5 `<audio>` accepts works: `/sounds/ring.mp3`, `https://cdn.example.com/ring.ogg`, or a Vite/Webpack `import` resolved URL.
71
+ - If the override URL fails to load, the browser console logs `[RINGTONE] play() rejected: ...` and the call UI keeps working.
72
+ - The default mp3 is bundled at `dist/public/music/i_phone_message.mp3` and shipped with the package — no extra setup needed.
73
+
74
+ ## Default Chat Component
75
+
76
+ The `<Chat />` component provides full UI:
77
+
78
+ - User registration/login
79
+ - Room list (direct and group)
80
+ - Real-time messaging
81
+ - File uploads with drag-and-drop
82
+ - Typing indicators
83
+ - Online presence (green dot)
84
+ - Audio calls (WebRTC)
85
+
86
+ ### Customize Appearance
87
+
88
+ Styles are in `styles.css`. Override CSS variables:
89
+
90
+ ```css
91
+ :root {
92
+ --chat-primary: #007bff;
93
+ --chat-bg: #ffffff;
94
+ --chat-text: #333333;
95
+ --chat-border: #e0e0e0;
96
+ --chat-radius: 8px;
97
+ --chat-font: system-ui, sans-serif;
98
+ }
99
+ ```
100
+
101
+ ## Build Custom UI
102
+
103
+ Use hooks instead of `<Chat />`:
104
+
105
+ ```tsx
106
+ import { useChatContext, useRooms, useMessages, useCall, useSocket } from '@enfin/chat';
107
+ import { ChatProvider } from '@enfin/chat';
108
+
109
+ function CustomChat() {
110
+ const { userId, messages, rooms, currentRoom, sendMessage, selectRoom } = useChatContext();
111
+ const { activeCall, startCall, endCall, acceptCall } = useCall();
112
+
113
+ // Send message
114
+ await sendMessage(currentRoom._id, 'Hello!');
115
+
116
+ // Start call
117
+ await startCall(currentRoom._id, 'participant_456');
118
+ }
119
+ ```
120
+
121
+ ### useChatContext
122
+
123
+ Returns everything:
124
+
125
+ ```ts
126
+ interface ChatContextValue {
127
+ userId: string;
128
+ userName: string;
129
+ messages: Message[];
130
+ rooms: Room[];
131
+ users: ChatUser[];
132
+ currentRoom?: Room;
133
+ activeCall?: AudioCall;
134
+ typingUsers: PresenceStatus[];
135
+ connected: boolean;
136
+ loading: boolean;
137
+ error?: string;
138
+ sendMessage: (roomId, content, file?) => Promise<void>;
139
+ selectRoom: (roomId) => void;
140
+ createRoom: (name, type, members) => Promise<Room>;
141
+ startCall: (roomId, participantId) => Promise<void>;
142
+ endCall: () => Promise<void>;
143
+ acceptCall: () => Promise<void>;
144
+ mute: () => void;
145
+ unmute: () => void;
146
+ isMuted: boolean;
147
+ socket?: Socket;
148
+ refreshUsers: () => Promise<void>;
149
+ }
150
+ ```
151
+
152
+ ### useRooms
153
+
154
+ ```ts
155
+ const { rooms, loading, error, createRoom, openDirectRoom } = useRooms();
156
+ ```
157
+
158
+ ### useMessages
159
+
160
+ ```ts
161
+ const { messages, loading, sendMessage, markRead } = useMessages(roomId);
162
+ ```
163
+
164
+ ### useCall
165
+
166
+ ```ts
167
+ const { activeCall, startCall, endCall, acceptCall, mute, unmute, isMuted } = useCall();
168
+ ```
169
+
170
+ ### useSocket
171
+
172
+ Raw Socket.IO access:
173
+
174
+ ```ts
175
+ const socket = useSocket();
176
+ // socket.emit('message', { roomId, content });
177
+ // socket.on('message', (msg) => { ... });
178
+ ```
179
+
180
+ ## Types
181
+
182
+ All types are exported:
183
+
184
+ ```ts
185
+ import { Message, Room, User, ChatConfig } from '@enfin/chat';
186
+ ```
187
+
188
+ ## Environment Variables
189
+
190
+ ### Vite
191
+
192
+ ```env
193
+ VITE_CHAT_API_KEY=chat_xxx
194
+ VITE_SERVER_URL=http://localhost:3002
195
+ ```
196
+
197
+ ### CRA
198
+
199
+ ```env
200
+ REACT_APP_CHAT_API_KEY=chat_xxx
201
+ REACT_APP_SERVER_URL=http://localhost:3002
202
+ ```
203
+
204
+ ### Next.js
205
+
206
+ In `.env.local`:
207
+
208
+ ```env
209
+ NEXT_PUBLIC_CHAT_API_KEY=chat_xxx
210
+ NEXT_PUBLIC_SERVER_URL=http://localhost:3002
211
+ ```
212
+
213
+ Then access via `process.env.NEXT_PUBLIC_*`.
214
+
215
+ ## Server Requirement
216
+
217
+ Your backend must run `@enfin/chat-server`. The frontend SDK communicates with it via:
218
+
219
+ - Socket.IO: `${serverUrl}/chat`
220
+ - REST: `${serverUrl}/api/*`
221
+
222
+ ---
223
+
224
+ For backend server, see `@enfin/chat-server`.
@@ -0,0 +1,54 @@
1
+ import { WebRTCConfig, SignalPayload } from './types';
2
+ export type WebRTCManagerState = 'idle' | 'requesting-media' | 'connecting' | 'connected' | 'failed' | 'closed';
3
+ export interface WebRTCManagerCallbacks {
4
+ sendSignal: (payload: SignalPayload) => void;
5
+ onStateChange?: (state: WebRTCManagerState) => void;
6
+ onRemoteStream?: (stream: MediaStream) => void;
7
+ onLocalStream?: (stream: MediaStream) => void;
8
+ onError?: (err: Error) => void;
9
+ }
10
+ export declare class WebRTCManager {
11
+ private pc;
12
+ private localStream;
13
+ private remoteStream;
14
+ private state;
15
+ private config;
16
+ private callbacks;
17
+ private isInitiator;
18
+ private currentRoomId;
19
+ private hasReceivedRemote;
20
+ private pendingOffer;
21
+ private bufferedOffer;
22
+ private pendingIceCandidates;
23
+ private isCallee;
24
+ private isAccepted;
25
+ constructor(config: WebRTCConfig, callbacks: WebRTCManagerCallbacks);
26
+ setSignalEmitter(sendSignal: (payload: import('./types').SignalPayload) => void): void;
27
+ getState(): WebRTCManagerState;
28
+ getRemoteStream(): MediaStream | null;
29
+ getLocalStream(): MediaStream | null;
30
+ private setState;
31
+ private buildPeerConnection;
32
+ startAsCaller(roomId: string): Promise<void>;
33
+ /**
34
+ * Sends the buffered offer + flushes any pending ICE candidates.
35
+ * Call this AFTER the server has confirmed the call record exists,
36
+ * so the callee is ready to receive `call:signal` events.
37
+ */
38
+ sendOffer(): Promise<void>;
39
+ handleOffer(offerSdp: string, roomId: string): Promise<void>;
40
+ private flushPendingIce;
41
+ handleAnswer(answerSdp: string): Promise<void>;
42
+ handleIceCandidate(candidate: string, sdpMid: string | null, sdpMLineIndex: number): Promise<void>;
43
+ acquireLocalStream(): Promise<MediaStream>;
44
+ setMuted(muted: boolean): void;
45
+ processPendingOffer(): Promise<void>;
46
+ /**
47
+ * Mark this manager as a callee (it received the offer via call:incoming,
48
+ * not via startCall). While not yet accepted, incoming offers are buffered
49
+ * so we don't request mic / open PC before the user clicks Accept.
50
+ */
51
+ markAsCallee(): void;
52
+ markAsAccepted(): void;
53
+ close(): void;
54
+ }
@@ -0,0 +1,274 @@
1
+ import { DEFAULT_WEBRTC_CONFIG } from './types';
2
+ export class WebRTCManager {
3
+ constructor(config, callbacks) {
4
+ this.pc = null;
5
+ this.localStream = null;
6
+ this.remoteStream = null;
7
+ this.state = 'idle';
8
+ this.isInitiator = false;
9
+ this.currentRoomId = null;
10
+ this.hasReceivedRemote = false;
11
+ this.pendingOffer = null;
12
+ this.bufferedOffer = null;
13
+ this.pendingIceCandidates = [];
14
+ this.isCallee = false;
15
+ this.isAccepted = false;
16
+ this.config = { ...DEFAULT_WEBRTC_CONFIG, ...config };
17
+ this.callbacks = callbacks;
18
+ }
19
+ setSignalEmitter(sendSignal) {
20
+ this.callbacks = { ...this.callbacks, sendSignal };
21
+ }
22
+ getState() {
23
+ return this.state;
24
+ }
25
+ getRemoteStream() {
26
+ return this.remoteStream;
27
+ }
28
+ getLocalStream() {
29
+ return this.localStream;
30
+ }
31
+ setState(next) {
32
+ this.state = next;
33
+ this.callbacks.onStateChange?.(next);
34
+ }
35
+ buildPeerConnection() {
36
+ const pc = new RTCPeerConnection({
37
+ iceServers: this.config.iceServers,
38
+ iceTransportPolicy: this.config.iceTransportPolicy
39
+ });
40
+ pc.onicecandidate = (event) => {
41
+ if (event.candidate && this.currentRoomId) {
42
+ this.callbacks.sendSignal({
43
+ type: 'ice-candidate',
44
+ candidate: event.candidate.candidate,
45
+ sdpMid: event.candidate.sdpMid,
46
+ sdpMLineIndex: event.candidate.sdpMLineIndex,
47
+ roomId: this.currentRoomId
48
+ });
49
+ }
50
+ };
51
+ pc.ontrack = (event) => {
52
+ // Chrome fires ontrack for locally added tracks too (the transceiver
53
+ // emits a track event for the sender's stream). Filter those out so
54
+ // the audio element only ever plays the REMOTE peer's stream.
55
+ if (event.transceiver && event.transceiver.direction === 'sendonly') {
56
+ return;
57
+ }
58
+ // Also reject the event if every stream is actually our own local stream.
59
+ const remoteStream = event.streams.find(s => s !== this.localStream) || event.streams[0];
60
+ if (remoteStream && remoteStream !== this.localStream) {
61
+ this.remoteStream = remoteStream;
62
+ this.hasReceivedRemote = true;
63
+ this.callbacks.onRemoteStream?.(remoteStream);
64
+ this.setState('connected');
65
+ }
66
+ };
67
+ pc.onconnectionstatechange = () => {
68
+ const s = pc.connectionState;
69
+ if (s === 'connected')
70
+ this.setState('connected');
71
+ else if (s === 'failed')
72
+ this.setState('failed');
73
+ else if (s === 'closed')
74
+ this.setState('closed');
75
+ else if (s === 'connecting')
76
+ this.setState('connecting');
77
+ };
78
+ pc.oniceconnectionstatechange = () => {
79
+ const s = pc.iceConnectionState;
80
+ if (s === 'connected' || s === 'completed')
81
+ this.setState('connected');
82
+ else if (s === 'failed')
83
+ this.setState('failed');
84
+ };
85
+ return pc;
86
+ }
87
+ async startAsCaller(roomId) {
88
+ if (this.state !== 'idle' && this.state !== 'closed') {
89
+ return;
90
+ }
91
+ this.isInitiator = true;
92
+ this.currentRoomId = roomId;
93
+ this.hasReceivedRemote = false;
94
+ this.setState('requesting-media');
95
+ try {
96
+ await this.acquireLocalStream();
97
+ const pc = this.buildPeerConnection();
98
+ this.localStream.getTracks().forEach(track => {
99
+ pc.addTrack(track, this.localStream);
100
+ });
101
+ this.pc = pc;
102
+ const offer = await pc.createOffer();
103
+ await pc.setLocalDescription(offer);
104
+ // Buffer the offer — don't send yet. The caller (startCall) will
105
+ // invoke sendOffer() after the server confirms the call exists.
106
+ this.bufferedOffer = { sdp: offer.sdp, roomId };
107
+ this.setState('connecting');
108
+ }
109
+ catch (err) {
110
+ this.setState('failed');
111
+ this.callbacks.onError?.(err);
112
+ throw err;
113
+ }
114
+ }
115
+ /**
116
+ * Sends the buffered offer + flushes any pending ICE candidates.
117
+ * Call this AFTER the server has confirmed the call record exists,
118
+ * so the callee is ready to receive `call:signal` events.
119
+ */
120
+ async sendOffer() {
121
+ if (!this.bufferedOffer || !this.pc)
122
+ return;
123
+ const { sdp, roomId } = this.bufferedOffer;
124
+ this.callbacks.sendSignal({
125
+ type: 'offer',
126
+ sdp,
127
+ roomId
128
+ });
129
+ this.bufferedOffer = null;
130
+ // Flush any ICE candidates that arrived before the offer was sent
131
+ await this.flushPendingIce();
132
+ }
133
+ async handleOffer(offerSdp, roomId) {
134
+ // Callee: always buffer the offer until the user accepts the call.
135
+ // We don't want to request mic / open PC before the user clicks Accept.
136
+ if (this.isCallee && !this.isAccepted) {
137
+ this.pendingOffer = { sdp: offerSdp, roomId };
138
+ return;
139
+ }
140
+ if (this.state !== 'idle' && this.state !== 'closed') {
141
+ // Already in a call or call in progress - buffer the offer
142
+ if (this.isInitiator)
143
+ return; // initiator ignores incoming offers
144
+ this.pendingOffer = { sdp: offerSdp, roomId };
145
+ return;
146
+ }
147
+ this.isInitiator = false;
148
+ this.currentRoomId = roomId;
149
+ this.hasReceivedRemote = false;
150
+ this.setState('requesting-media');
151
+ try {
152
+ await this.acquireLocalStream();
153
+ const pc = this.buildPeerConnection();
154
+ this.localStream.getTracks().forEach(track => {
155
+ pc.addTrack(track, this.localStream);
156
+ });
157
+ this.pc = pc;
158
+ await pc.setRemoteDescription(new RTCSessionDescription({ type: 'offer', sdp: offerSdp }));
159
+ const answer = await pc.createAnswer();
160
+ await pc.setLocalDescription(answer);
161
+ this.callbacks.sendSignal({
162
+ type: 'answer',
163
+ sdp: answer.sdp,
164
+ roomId
165
+ });
166
+ this.setState('connecting');
167
+ // Flush buffered ICE candidates
168
+ await this.flushPendingIce();
169
+ }
170
+ catch (err) {
171
+ this.setState('failed');
172
+ this.callbacks.onError?.(err);
173
+ throw err;
174
+ }
175
+ }
176
+ async flushPendingIce() {
177
+ if (!this.pc)
178
+ return;
179
+ for (const c of this.pendingIceCandidates) {
180
+ try {
181
+ await this.pc.addIceCandidate(new RTCIceCandidate(c));
182
+ }
183
+ catch { /* benign */ }
184
+ }
185
+ this.pendingIceCandidates = [];
186
+ }
187
+ async handleAnswer(answerSdp) {
188
+ if (!this.pc)
189
+ return;
190
+ if (this.pc.signalingState !== 'have-local-offer') {
191
+ return;
192
+ }
193
+ await this.pc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: answerSdp }));
194
+ }
195
+ async handleIceCandidate(candidate, sdpMid, sdpMLineIndex) {
196
+ if (!this.pc) {
197
+ // Buffer until PC exists
198
+ this.pendingIceCandidates.push({ candidate, sdpMid, sdpMLineIndex });
199
+ return;
200
+ }
201
+ try {
202
+ await this.pc.addIceCandidate(new RTCIceCandidate({
203
+ candidate,
204
+ sdpMid,
205
+ sdpMLineIndex
206
+ }));
207
+ }
208
+ catch (err) {
209
+ // Common benign failure: candidate arrives after PC closed
210
+ if (this.pc.connectionState !== 'closed') {
211
+ this.callbacks.onError?.(err);
212
+ }
213
+ }
214
+ }
215
+ async acquireLocalStream() {
216
+ if (this.localStream)
217
+ return this.localStream;
218
+ this.localStream = await navigator.mediaDevices.getUserMedia({
219
+ audio: this.config.audioConstraints
220
+ });
221
+ this.callbacks.onLocalStream?.(this.localStream);
222
+ return this.localStream;
223
+ }
224
+ setMuted(muted) {
225
+ if (!this.localStream)
226
+ return;
227
+ this.localStream.getAudioTracks().forEach(track => {
228
+ track.enabled = !muted;
229
+ });
230
+ }
231
+ async processPendingOffer() {
232
+ if (!this.pendingOffer)
233
+ return;
234
+ const { sdp, roomId } = this.pendingOffer;
235
+ this.pendingOffer = null;
236
+ this.isAccepted = true;
237
+ await this.handleOffer(sdp, roomId);
238
+ }
239
+ /**
240
+ * Mark this manager as a callee (it received the offer via call:incoming,
241
+ * not via startCall). While not yet accepted, incoming offers are buffered
242
+ * so we don't request mic / open PC before the user clicks Accept.
243
+ */
244
+ markAsCallee() {
245
+ this.isCallee = true;
246
+ this.isAccepted = false;
247
+ }
248
+ markAsAccepted() {
249
+ this.isAccepted = true;
250
+ }
251
+ close() {
252
+ if (this.localStream && this.localStream.getTracks) {
253
+ this.localStream.getTracks().forEach(track => track.stop());
254
+ this.localStream = null;
255
+ }
256
+ if (this.pc) {
257
+ try {
258
+ this.pc.close();
259
+ }
260
+ catch { /* noop */ }
261
+ this.pc = null;
262
+ }
263
+ this.remoteStream = null;
264
+ this.isInitiator = false;
265
+ this.currentRoomId = null;
266
+ this.hasReceivedRemote = false;
267
+ this.pendingOffer = null;
268
+ this.bufferedOffer = null;
269
+ this.pendingIceCandidates = [];
270
+ this.isCallee = false;
271
+ this.isAccepted = false;
272
+ this.setState('closed');
273
+ }
274
+ }
@@ -0,0 +1,3 @@
1
+ import type { Socket } from 'socket.io-client';
2
+ import type { WebRTCManager } from './WebRTCManager';
3
+ export declare function attachSignaling(socket: Socket, manager: WebRTCManager): () => void;
@@ -0,0 +1,24 @@
1
+ export function attachSignaling(socket, manager) {
2
+ const onSignal = (payload) => {
3
+ if (!payload || !payload.type || !payload.roomId)
4
+ return;
5
+ switch (payload.type) {
6
+ case 'offer':
7
+ manager.handleOffer(payload.sdp, payload.roomId).catch(() => { });
8
+ break;
9
+ case 'answer':
10
+ manager.handleAnswer(payload.sdp).catch(() => { });
11
+ break;
12
+ case 'ice-candidate':
13
+ manager.handleIceCandidate(payload.candidate, payload.sdpMid, payload.sdpMLineIndex).catch(() => { });
14
+ break;
15
+ }
16
+ };
17
+ manager.setSignalEmitter((payload) => {
18
+ socket.emit('call:signal', payload);
19
+ });
20
+ socket.on('call:signal', onSignal);
21
+ return () => {
22
+ socket.off('call:signal', onSignal);
23
+ };
24
+ }
@@ -0,0 +1,25 @@
1
+ export type SignalType = 'offer' | 'answer' | 'ice-candidate';
2
+ export interface SignalOffer {
3
+ type: 'offer';
4
+ sdp: string;
5
+ roomId: string;
6
+ }
7
+ export interface SignalAnswer {
8
+ type: 'answer';
9
+ sdp: string;
10
+ roomId: string;
11
+ }
12
+ export interface SignalIceCandidate {
13
+ type: 'ice-candidate';
14
+ candidate: string;
15
+ sdpMid: string | null;
16
+ sdpMLineIndex: number;
17
+ roomId: string;
18
+ }
19
+ export type SignalPayload = SignalOffer | SignalAnswer | SignalIceCandidate;
20
+ export interface WebRTCConfig {
21
+ iceServers?: RTCIceServer[];
22
+ iceTransportPolicy?: RTCIceTransportPolicy;
23
+ audioConstraints?: MediaTrackConstraints;
24
+ }
25
+ export declare const DEFAULT_WEBRTC_CONFIG: Required<WebRTCConfig>;
@@ -0,0 +1,13 @@
1
+ // WebRTC Signaling Types for audio calls
2
+ export const DEFAULT_WEBRTC_CONFIG = {
3
+ iceServers: [
4
+ { urls: 'stun:stun.l.google.com:19302' },
5
+ { urls: 'stun:stun1.l.google.com:19302' }
6
+ ],
7
+ iceTransportPolicy: 'all',
8
+ audioConstraints: {
9
+ echoCancellation: true,
10
+ noiseSuppression: true,
11
+ autoGainControl: true
12
+ }
13
+ };
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ export interface ChatProps {
3
+ theme?: 'light' | 'dark';
4
+ }
5
+ /**
6
+ * Default Chat Component
7
+ *
8
+ * Two-pane layout:
9
+ * - Top: app header with current user (avatar, name, switch button)
10
+ * - Left: list of registered users (click to select a peer)
11
+ * - Right: messages + input area for the selected peer
12
+ */
13
+ export declare function Chat({ theme }: ChatProps): React.JSX.Element;
14
+ export default Chat;