@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.
- package/README.md +224 -0
- package/dist/assets/music/i_phone_message.mp3 +0 -0
- package/dist/call/WebRTCManager.d.ts +54 -0
- package/dist/call/WebRTCManager.js +274 -0
- package/dist/call/signaling.d.ts +3 -0
- package/dist/call/signaling.js +24 -0
- package/dist/call/types.d.ts +25 -0
- package/dist/call/types.js +13 -0
- package/dist/components/Chat.d.ts +14 -0
- package/dist/components/Chat.js +452 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +2 -0
- package/dist/context/ChatContext.d.ts +59 -0
- package/dist/context/ChatContext.js +647 -0
- package/dist/esm/call/WebRTCManager.d.ts +54 -0
- package/dist/esm/call/WebRTCManager.js +274 -0
- package/dist/esm/call/signaling.d.ts +3 -0
- package/dist/esm/call/signaling.js +24 -0
- package/dist/esm/call/types.d.ts +25 -0
- package/dist/esm/call/types.js +13 -0
- package/dist/esm/components/Chat.d.ts +14 -0
- package/dist/esm/components/Chat.js +452 -0
- package/dist/esm/components/index.d.ts +3 -0
- package/dist/esm/components/index.js +2 -0
- package/dist/esm/context/ChatContext.d.ts +59 -0
- package/dist/esm/context/ChatContext.js +647 -0
- package/dist/esm/hooks/index.d.ts +6 -0
- package/dist/esm/hooks/index.js +6 -0
- package/dist/esm/hooks/useCall.d.ts +15 -0
- package/dist/esm/hooks/useCall.js +38 -0
- package/dist/esm/hooks/useChat.d.ts +23 -0
- package/dist/esm/hooks/useChat.js +40 -0
- package/dist/esm/hooks/useMessages.d.ts +17 -0
- package/dist/esm/hooks/useMessages.js +38 -0
- package/dist/esm/hooks/usePresence.d.ts +4 -0
- package/dist/esm/hooks/usePresence.js +12 -0
- package/dist/esm/hooks/useRooms.d.ts +9 -0
- package/dist/esm/hooks/useRooms.js +19 -0
- package/dist/esm/hooks/useSocket.d.ts +7 -0
- package/dist/esm/hooks/useSocket.js +92 -0
- package/dist/esm/index.d.ts +11 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/utils/index.d.ts +2 -0
- package/dist/esm/utils/index.js +2 -0
- package/dist/esm/utils/ringtone.d.ts +2 -0
- package/dist/esm/utils/ringtone.js +39 -0
- package/dist/esm/utils/testUtils.d.ts +102 -0
- package/dist/esm/utils/testUtils.js +153 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/useCall.d.ts +15 -0
- package/dist/hooks/useCall.js +38 -0
- package/dist/hooks/useChat.d.ts +23 -0
- package/dist/hooks/useChat.js +40 -0
- package/dist/hooks/useMessages.d.ts +17 -0
- package/dist/hooks/useMessages.js +38 -0
- package/dist/hooks/usePresence.d.ts +4 -0
- package/dist/hooks/usePresence.js +12 -0
- package/dist/hooks/useRooms.d.ts +9 -0
- package/dist/hooks/useRooms.js +19 -0
- package/dist/hooks/useSocket.d.ts +7 -0
- package/dist/hooks/useSocket.js +92 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +15 -0
- package/dist/public/music/i_phone_message.mp3 +0 -0
- package/dist/style.css +1226 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/ringtone.d.ts +2 -0
- package/dist/utils/ringtone.js +39 -0
- package/dist/utils/testUtils.d.ts +102 -0
- package/dist/utils/testUtils.js +153 -0
- package/package.json +44 -0
|
@@ -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,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;
|