react-peer-chat 0.8.6 → 0.9.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 +26 -25
- package/dist/chunks/{chunk-GT3RG6VA.js → chunk-MBDO7IOK.js} +40 -14
- package/dist/chunks/{chunk-YDZ27MG7.js → chunk-N4TBKQIR.js} +3 -3
- package/dist/components.js +2 -2
- package/dist/hooks.js +1 -1
- package/dist/index.js +2 -2
- package/dist/types.d.ts +6 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ A simple-to-use React component for implementing peer-to-peer chatting, powered
|
|
|
9
9
|
- Supports persistent text chat across page reloads
|
|
10
10
|
- Recovers old chats upon reconnection
|
|
11
11
|
- Option to clear chat on command
|
|
12
|
-
- Supports audio/voice chat
|
|
12
|
+
- Supports audio/voice chat with automatic mixing for multiple peers
|
|
13
13
|
- Multiple peer connections. See [multi-peer usage](#multi-peer-usage)
|
|
14
14
|
- Fully customizable. See [usage with FaC](#full-customization)
|
|
15
15
|
|
|
@@ -166,7 +166,9 @@ export default function App() {
|
|
|
166
166
|
|
|
167
167
|
### useChat Hook
|
|
168
168
|
|
|
169
|
-
The `useChat` hook is ideal when you want to completely redesign the Chat UI
|
|
169
|
+
The `useChat` hook is ideal when you want to completely redesign the Chat UI.
|
|
170
|
+
|
|
171
|
+
#### Basic Usage
|
|
170
172
|
|
|
171
173
|
```jsx
|
|
172
174
|
import React, { useEffect, useRef, useState } from "react";
|
|
@@ -174,13 +176,19 @@ import { clearChat, useChat } from "react-peer-chat";
|
|
|
174
176
|
import { BiSolidMessageDetail, BiSolidMessageX, BsFillMicFill, BsFillMicMuteFill, GrSend } from "react-peer-chat/icons";
|
|
175
177
|
|
|
176
178
|
function Chat({ text = true, audio = true, onMessageReceived, dialogOptions, props = {}, children, ...hookProps }) {
|
|
177
|
-
const {
|
|
179
|
+
const {
|
|
180
|
+
peerId, // Complete peer ID
|
|
181
|
+
remotePeers, // Object mapping remote peer IDs to their names
|
|
182
|
+
messages, // Array of all chat messages
|
|
183
|
+
sendMessage, // Function to send a message to all connected peers
|
|
184
|
+
audio: audioEnabled, // Current audio state (enabled/disabled)
|
|
185
|
+
setAudio, // Function to toggle audio on/off (only works if audio option is set to true)
|
|
186
|
+
} = useChat({
|
|
178
187
|
text,
|
|
179
188
|
audio,
|
|
180
189
|
onMessageReceived: modifiedOnMessageReceived,
|
|
181
190
|
...hookProps,
|
|
182
191
|
});
|
|
183
|
-
const { remotePeers, messages, sendMessage, audio: audioEnabled, setAudio } = childrenOptions;
|
|
184
192
|
const containerRef = useRef(null);
|
|
185
193
|
const [dialog, setDialog] = useState(false);
|
|
186
194
|
const dialogRef = useRef(null);
|
|
@@ -205,7 +213,7 @@ function Chat({ text = true, audio = true, onMessageReceived, dialogOptions, pro
|
|
|
205
213
|
return (
|
|
206
214
|
<div className="rpc-main rpc-font" {...props}>
|
|
207
215
|
{typeof children === "function" ? (
|
|
208
|
-
children(
|
|
216
|
+
children({ remotePeers, messages, sendMessage, audio: audioEnabled, setAudio })
|
|
209
217
|
) : (
|
|
210
218
|
<>
|
|
211
219
|
{text && (
|
|
@@ -258,17 +266,12 @@ function Chat({ text = true, audio = true, onMessageReceived, dialogOptions, pro
|
|
|
258
266
|
</div>
|
|
259
267
|
)}
|
|
260
268
|
{audio && (
|
|
261
|
-
<button>
|
|
262
|
-
{audioEnabled ? <BsFillMicFill title="Turn mic off"
|
|
269
|
+
<button className="rpc-button" onClick={() => setAudio(!audioEnabled)}>
|
|
270
|
+
{audioEnabled ? <BsFillMicFill title="Turn mic off" /> : <BsFillMicMuteFill title="Turn mic on" />}
|
|
263
271
|
</button>
|
|
264
272
|
)}
|
|
265
273
|
</>
|
|
266
274
|
)}
|
|
267
|
-
{audio && (
|
|
268
|
-
<button className="rpc-button" onClick={() => setAudio(!audioEnabled)}>
|
|
269
|
-
{audioEnabled ? <BsFillMicFill title="Turn mic off" /> : <BsFillMicMuteFill title="Turn mic on" />}
|
|
270
|
-
</button>
|
|
271
|
-
)}
|
|
272
275
|
</div>
|
|
273
276
|
);
|
|
274
277
|
}
|
|
@@ -284,13 +287,11 @@ export default function App() {
|
|
|
284
287
|
}
|
|
285
288
|
```
|
|
286
289
|
|
|
287
|
-
#### Basic Usage
|
|
288
|
-
|
|
289
290
|
## API Reference
|
|
290
291
|
|
|
291
292
|
### useChat Hook
|
|
292
293
|
|
|
293
|
-
Here is the full API for the `useChat` hook, these options can be passed as
|
|
294
|
+
Here is the full API for the `useChat` hook, these options can be passed as parameters to the hook:
|
|
294
295
|
|
|
295
296
|
| Parameter | Type | Required | Default | Description |
|
|
296
297
|
| ------------------- | --------------------------------------------- | -------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -299,7 +300,7 @@ Here is the full API for the `useChat` hook, these options can be passed as para
|
|
|
299
300
|
| `remotePeerId` | `string \| string[]` | No | - | Unique id(s) of remote peer(s) to connect to. Read at mount and when `peerId` changes; changes to this prop alone won't create new connections. |
|
|
300
301
|
| `text` | `boolean` | No | `true` | Text chat will be enabled if this property is set to true. |
|
|
301
302
|
| `recoverChat` | `boolean` | No | `false` | Old chats will be recovered upon reconnecting with the same peer(s). |
|
|
302
|
-
| `audio` | `boolean` | No | `true` | Voice chat will be enabled if this property is set to true.
|
|
303
|
+
| `audio` | `boolean` | No | `true` | Voice chat will be enabled if this property is set to true. Audio from multiple peers is automatically mixed. |
|
|
303
304
|
| `peerOptions` | [`PeerOptions`](#peeroptions) | No | - | Options to customize peerjs Peer instance. |
|
|
304
305
|
| `onError` | [`ErrorHandler`](#errorhandler) | No | `() => alert('Browser not supported! Try some other browser.')` | Function to be executed if browser doesn't support `WebRTC` |
|
|
305
306
|
| `onMicError` | [`ErrorHandler`](#errorhandler) | No | `() => alert('Microphone not accessible!')` | Function to be executed when microphone is not accessible. |
|
|
@@ -322,19 +323,19 @@ that are listed in [useChat Hook API Reference](#usechat-hook-1) along with the
|
|
|
322
323
|
### Children
|
|
323
324
|
|
|
324
325
|
```typescript
|
|
325
|
-
import type { ReactNode } from "react";
|
|
326
|
+
import type { ReactNode, SetStateAction } from "react";
|
|
326
327
|
|
|
327
|
-
type RemotePeers =
|
|
328
|
+
type RemotePeers = Record<string, string>;
|
|
328
329
|
type Message = {
|
|
329
330
|
id: string;
|
|
330
331
|
text: string;
|
|
331
332
|
};
|
|
332
333
|
type ChildrenOptions = {
|
|
333
|
-
remotePeers
|
|
334
|
-
messages
|
|
335
|
-
sendMessage
|
|
336
|
-
audio
|
|
337
|
-
setAudio
|
|
334
|
+
remotePeers: RemotePeers;
|
|
335
|
+
messages: Message[];
|
|
336
|
+
sendMessage: (message: Message) => void;
|
|
337
|
+
audio: boolean;
|
|
338
|
+
setAudio: (value: SetStateAction<boolean>) => void;
|
|
338
339
|
};
|
|
339
340
|
type Children = (childrenOptions: ChildrenOptions) => ReactNode;
|
|
340
341
|
```
|
|
@@ -346,8 +347,8 @@ import type { CSSProperties } from "react";
|
|
|
346
347
|
|
|
347
348
|
type DialogPosition = "left" | "center" | "right";
|
|
348
349
|
type DialogOptions = {
|
|
349
|
-
position
|
|
350
|
-
style
|
|
350
|
+
position?: DialogPosition;
|
|
351
|
+
style?: CSSProperties;
|
|
351
352
|
};
|
|
352
353
|
```
|
|
353
354
|
|
|
@@ -64,15 +64,26 @@ function useChat({
|
|
|
64
64
|
const [peer, setPeer] = useState();
|
|
65
65
|
const [audio, setAudio] = useAudio(allowed);
|
|
66
66
|
const connRef = useRef({});
|
|
67
|
-
const localStreamRef = useRef(null);
|
|
68
|
-
const audioStreamRef = useRef(null);
|
|
69
67
|
const callsRef = useRef({});
|
|
68
|
+
const audioContextRef = useRef(null);
|
|
69
|
+
const mixerRef = useRef(null);
|
|
70
|
+
const sourceNodesRef = useRef({});
|
|
70
71
|
const [messages, setMessages, addMessage] = useMessages();
|
|
71
72
|
const [remotePeers, setRemotePeers] = useStorage("rpc-remote-peer", {});
|
|
72
73
|
const { completePeerId, completeRemotePeerIds } = useMemo(() => {
|
|
73
74
|
const remotePeerIds = Array.isArray(remotePeerId) ? remotePeerId : [remotePeerId];
|
|
74
75
|
return { completePeerId: addPrefix(peerId), completeRemotePeerIds: remotePeerIds.map(addPrefix) };
|
|
75
76
|
}, [peerId]);
|
|
77
|
+
function handleCall(call) {
|
|
78
|
+
const id = call.peer;
|
|
79
|
+
call.on("stream", (stream) => handleRemoteStream(id, stream));
|
|
80
|
+
call.on("close", () => {
|
|
81
|
+
call.removeAllListeners();
|
|
82
|
+
removePeerAudio(id);
|
|
83
|
+
delete callsRef.current[id];
|
|
84
|
+
});
|
|
85
|
+
callsRef.current[id] = call;
|
|
86
|
+
}
|
|
76
87
|
function handleConnection(conn) {
|
|
77
88
|
connRef.current[conn.peer] = conn;
|
|
78
89
|
conn.on("open", () => {
|
|
@@ -91,13 +102,26 @@ function useChat({
|
|
|
91
102
|
setAudio(false);
|
|
92
103
|
onMicError();
|
|
93
104
|
}
|
|
94
|
-
function handleRemoteStream(remoteStream) {
|
|
95
|
-
if (
|
|
105
|
+
function handleRemoteStream(peerId2, remoteStream) {
|
|
106
|
+
if (!audioContextRef.current) audioContextRef.current = new AudioContext();
|
|
107
|
+
if (!mixerRef.current) {
|
|
108
|
+
mixerRef.current = audioContextRef.current.createGain();
|
|
109
|
+
mixerRef.current.connect(audioContextRef.current.destination);
|
|
110
|
+
}
|
|
111
|
+
removePeerAudio(peerId2);
|
|
112
|
+
const source = audioContextRef.current.createMediaStreamSource(remoteStream);
|
|
113
|
+
source.connect(mixerRef.current);
|
|
114
|
+
sourceNodesRef.current[peerId2] = source;
|
|
96
115
|
}
|
|
97
116
|
function receiveMessage(message) {
|
|
98
117
|
addMessage(message);
|
|
99
118
|
onMessageReceived == null ? void 0 : onMessageReceived(message);
|
|
100
119
|
}
|
|
120
|
+
function removePeerAudio(peerId2) {
|
|
121
|
+
if (!sourceNodesRef.current[peerId2]) return;
|
|
122
|
+
sourceNodesRef.current[peerId2].disconnect();
|
|
123
|
+
delete sourceNodesRef.current[peerId2];
|
|
124
|
+
}
|
|
101
125
|
function sendMessage(message) {
|
|
102
126
|
addMessage(message);
|
|
103
127
|
Object.values(connRef.current).forEach((conn) => conn.send({ type: "message", message }));
|
|
@@ -138,6 +162,7 @@ function useChat({
|
|
|
138
162
|
}, [text, peer]);
|
|
139
163
|
useEffect(() => {
|
|
140
164
|
if (!audio || !peer) return;
|
|
165
|
+
let localStream;
|
|
141
166
|
const setupAudio = () => navigator.mediaDevices.getUserMedia({
|
|
142
167
|
video: false,
|
|
143
168
|
audio: {
|
|
@@ -149,31 +174,32 @@ function useChat({
|
|
|
149
174
|
// Enable echo cancellation
|
|
150
175
|
}
|
|
151
176
|
}).then((stream) => {
|
|
152
|
-
|
|
177
|
+
localStream = stream;
|
|
153
178
|
completeRemotePeerIds.forEach((id) => {
|
|
179
|
+
if (callsRef.current[id]) return;
|
|
154
180
|
const call = peer.call(id, stream);
|
|
155
|
-
call
|
|
156
|
-
call.on("close", call.removeAllListeners);
|
|
157
|
-
callsRef.current[id] = call;
|
|
181
|
+
handleCall(call);
|
|
158
182
|
});
|
|
159
183
|
peer.on("call", (call) => {
|
|
184
|
+
if (callsRef.current[call.peer]) return call.close();
|
|
160
185
|
call.answer(stream);
|
|
161
|
-
call
|
|
162
|
-
call.on("close", call.removeAllListeners);
|
|
163
|
-
callsRef.current[call.peer] = call;
|
|
186
|
+
handleCall(call);
|
|
164
187
|
});
|
|
165
188
|
}).catch(handleError);
|
|
166
189
|
if (peer.open) setupAudio();
|
|
167
190
|
else peer.once("open", setupAudio);
|
|
168
191
|
return () => {
|
|
169
192
|
var _a;
|
|
170
|
-
|
|
171
|
-
localStreamRef.current = null;
|
|
193
|
+
localStream == null ? void 0 : localStream.getTracks().forEach((track) => track.stop());
|
|
172
194
|
Object.values(callsRef.current).forEach(closeConnection);
|
|
173
195
|
callsRef.current = {};
|
|
196
|
+
Object.keys(sourceNodesRef.current).forEach(removePeerAudio);
|
|
197
|
+
(_a = audioContextRef.current) == null ? void 0 : _a.close();
|
|
198
|
+
audioContextRef.current = null;
|
|
199
|
+
mixerRef.current = null;
|
|
174
200
|
};
|
|
175
201
|
}, [audio, peer]);
|
|
176
|
-
return { peerId: completePeerId,
|
|
202
|
+
return { peerId: completePeerId, remotePeers, messages, sendMessage, audio, setAudio };
|
|
177
203
|
}
|
|
178
204
|
function useMessages() {
|
|
179
205
|
const [messages, setMessages] = useStorage("rpc-messages", []);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useChat } from './chunk-
|
|
1
|
+
import { useChat } from './chunk-MBDO7IOK.js';
|
|
2
2
|
import { BiSolidMessageX, BiSolidMessageDetail, GrSend, BsFillMicFill, BsFillMicMuteFill } from './chunk-JJPIWKLG.js';
|
|
3
3
|
import { __objRest, __spreadValues } from './chunk-LNEKYYG7.js';
|
|
4
4
|
import React, { useRef, useState, useEffect } from 'react';
|
|
@@ -29,7 +29,7 @@ function Chat(_a) {
|
|
|
29
29
|
text,
|
|
30
30
|
audio,
|
|
31
31
|
onMessageReceived: modifiedOnMessageReceived
|
|
32
|
-
}, hookProps)), { peerId
|
|
32
|
+
}, hookProps)), { peerId } = _a2, childrenOptions = __objRest(_a2, ["peerId"]);
|
|
33
33
|
const { remotePeers, messages, sendMessage, audio: audioEnabled, setAudio } = childrenOptions;
|
|
34
34
|
const containerRef = useRef(null);
|
|
35
35
|
const [dialog, setDialog] = useState(false);
|
|
@@ -75,7 +75,7 @@ function Chat(_a) {
|
|
|
75
75
|
},
|
|
76
76
|
/* @__PURE__ */ React.createElement("input", { ref: inputRef, className: "rpc-input rpc-font", placeholder: "Enter a message" }),
|
|
77
77
|
/* @__PURE__ */ React.createElement("button", { type: "submit", className: "rpc-button" }, /* @__PURE__ */ React.createElement(GrSend, { title: "Send message" }))
|
|
78
|
-
)))), audio && /* @__PURE__ */ React.createElement("button", { className: "rpc-button", onClick: () => setAudio(!audioEnabled) }, audioEnabled ? /* @__PURE__ */ React.createElement(BsFillMicFill, { title: "Turn mic off" }) : /* @__PURE__ */ React.createElement(BsFillMicMuteFill, { title: "Turn mic on" })))
|
|
78
|
+
)))), audio && /* @__PURE__ */ React.createElement("button", { className: "rpc-button", onClick: () => setAudio(!audioEnabled) }, audioEnabled ? /* @__PURE__ */ React.createElement(BsFillMicFill, { title: "Turn mic off" }) : /* @__PURE__ */ React.createElement(BsFillMicMuteFill, { title: "Turn mic on" }))));
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
export { Chat };
|
package/dist/components.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { Chat as default } from './chunks/chunk-
|
|
2
|
-
import './chunks/chunk-
|
|
1
|
+
export { Chat as default } from './chunks/chunk-N4TBKQIR.js';
|
|
2
|
+
import './chunks/chunk-MBDO7IOK.js';
|
|
3
3
|
import './chunks/chunk-JJPIWKLG.js';
|
|
4
4
|
import './chunks/chunk-ZYFPSCFE.js';
|
|
5
5
|
import './chunks/chunk-LNEKYYG7.js';
|
package/dist/hooks.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { Chat as default } from './chunks/chunk-
|
|
2
|
-
export { useChat } from './chunks/chunk-
|
|
1
|
+
export { Chat as default } from './chunks/chunk-N4TBKQIR.js';
|
|
2
|
+
export { useChat } from './chunks/chunk-MBDO7IOK.js';
|
|
3
3
|
import './chunks/chunk-JJPIWKLG.js';
|
|
4
4
|
export { clearChat } from './chunks/chunk-ZYFPSCFE.js';
|
|
5
5
|
import './chunks/chunk-LNEKYYG7.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PeerOptions, DataConnection, MediaConnection } from 'peerjs';
|
|
2
2
|
export { PeerOptions } from 'peerjs';
|
|
3
|
-
import { CSSProperties, DetailedHTMLProps, HTMLAttributes,
|
|
3
|
+
import { CSSProperties, DetailedHTMLProps, HTMLAttributes, SetStateAction, ReactNode } from 'react';
|
|
4
4
|
|
|
5
5
|
type Connection = DataConnection | MediaConnection;
|
|
6
6
|
type ErrorHandler = () => void;
|
|
@@ -24,15 +24,16 @@ type UseChatProps = {
|
|
|
24
24
|
onMessageSent?: MessageEventHandler;
|
|
25
25
|
onMessageReceived?: MessageEventHandler;
|
|
26
26
|
};
|
|
27
|
-
type
|
|
28
|
-
peerId: string;
|
|
29
|
-
audioStreamRef: RefObject<HTMLMediaElement | null>;
|
|
27
|
+
type ChildrenOptions = {
|
|
30
28
|
remotePeers: RemotePeers;
|
|
31
29
|
messages: Message[];
|
|
32
30
|
sendMessage: (message: Message) => void;
|
|
33
31
|
audio: boolean;
|
|
34
32
|
setAudio: (value: SetStateAction<boolean>) => void;
|
|
35
33
|
};
|
|
34
|
+
type UseChatReturn = ChildrenOptions & {
|
|
35
|
+
peerId: string;
|
|
36
|
+
};
|
|
36
37
|
type IconProps = DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>;
|
|
37
38
|
type ChatProps = UseChatProps & {
|
|
38
39
|
dialogOptions?: DialogOptions;
|
|
@@ -40,21 +41,12 @@ type ChatProps = UseChatProps & {
|
|
|
40
41
|
children?: Children;
|
|
41
42
|
};
|
|
42
43
|
type Children = (childrenOptions: ChildrenOptions) => ReactNode;
|
|
43
|
-
type ChildrenOptions = {
|
|
44
|
-
remotePeers?: RemotePeers;
|
|
45
|
-
messages?: Message[];
|
|
46
|
-
sendMessage?: (message: Message) => void;
|
|
47
|
-
audio?: boolean;
|
|
48
|
-
setAudio?: (audio: boolean) => void;
|
|
49
|
-
};
|
|
50
44
|
type DialogOptions = {
|
|
51
45
|
position?: DialogPosition;
|
|
52
46
|
style?: CSSProperties;
|
|
53
47
|
};
|
|
54
48
|
type DialogPosition = "left" | "center" | "right";
|
|
55
49
|
type DivProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
|
|
56
|
-
type RemotePeers =
|
|
57
|
-
[id: string]: string;
|
|
58
|
-
};
|
|
50
|
+
type RemotePeers = Record<string, string>;
|
|
59
51
|
|
|
60
52
|
export type { ChatProps, Children, ChildrenOptions, Connection, DialogOptions, DialogPosition, DivProps, ErrorHandler, IconProps, Message, MessageEventHandler, RemotePeerId, RemotePeers, UseChatProps, UseChatReturn };
|
package/package.json
CHANGED