react-peer-chat 0.8.6 → 0.10.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 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 or handle the audio stream differently, instead of using traditional playback methods.
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 { peerId, audioStreamRef, ...childrenOptions } = useChat({
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(childrenOptions)
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" onClick={() => setAudio(false)} /> : <BsFillMicMuteFill title="Turn mic on" onClick={() => setAudio(true)} />}
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 paramerters to the hook:
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 = { [id: string]: string };
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?: RemotePeers;
334
- messages?: Message[];
335
- sendMessage?: (message: Message) => void;
336
- audio?: boolean;
337
- setAudio?: (audio: boolean) => void;
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: DialogPosition;
350
- style: CSSProperties;
350
+ position?: DialogPosition;
351
+ style?: CSSProperties;
351
352
  };
352
353
  ```
353
354
 
@@ -64,15 +64,41 @@ 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 peerId2 = call.peer;
79
+ call.on("stream", () => {
80
+ callsRef.current[peerId2] = call;
81
+ if (!audioContextRef.current) audioContextRef.current = new AudioContext();
82
+ if (audioContextRef.current.state === "suspended") audioContextRef.current.resume();
83
+ if (!mixerRef.current) {
84
+ mixerRef.current = audioContextRef.current.createGain();
85
+ mixerRef.current.connect(audioContextRef.current.destination);
86
+ }
87
+ removePeerAudio(peerId2);
88
+ const audio2 = new Audio();
89
+ audio2.srcObject = call.remoteStream;
90
+ audio2.autoplay = true;
91
+ audio2.muted = false;
92
+ const source = audioContextRef.current.createMediaElementSource(audio2);
93
+ source.connect(mixerRef.current);
94
+ sourceNodesRef.current[peerId2] = source;
95
+ });
96
+ call.on("close", () => {
97
+ call.removeAllListeners();
98
+ removePeerAudio(peerId2);
99
+ delete callsRef.current[peerId2];
100
+ });
101
+ }
76
102
  function handleConnection(conn) {
77
103
  connRef.current[conn.peer] = conn;
78
104
  conn.on("open", () => {
@@ -91,17 +117,20 @@ function useChat({
91
117
  setAudio(false);
92
118
  onMicError();
93
119
  }
94
- function handleRemoteStream(remoteStream) {
95
- if (audioStreamRef.current) audioStreamRef.current.srcObject = remoteStream;
96
- }
97
120
  function receiveMessage(message) {
98
121
  addMessage(message);
99
122
  onMessageReceived == null ? void 0 : onMessageReceived(message);
100
123
  }
124
+ function removePeerAudio(peerId2) {
125
+ if (!sourceNodesRef.current[peerId2]) return;
126
+ sourceNodesRef.current[peerId2].disconnect();
127
+ delete sourceNodesRef.current[peerId2];
128
+ }
101
129
  function sendMessage(message) {
102
- addMessage(message);
103
- Object.values(connRef.current).forEach((conn) => conn.send({ type: "message", message }));
104
- onMessageSent == null ? void 0 : onMessageSent(message);
130
+ const event = { type: "message", message: __spreadProps(__spreadValues({}, message), { name }) };
131
+ addMessage(event.message);
132
+ Object.values(connRef.current).forEach((conn) => conn.send(event));
133
+ onMessageSent == null ? void 0 : onMessageSent(event.message);
105
134
  }
106
135
  useEffect(() => {
107
136
  if (!text && !audio) return;
@@ -138,42 +167,41 @@ function useChat({
138
167
  }, [text, peer]);
139
168
  useEffect(() => {
140
169
  if (!audio || !peer) return;
170
+ let localStream;
141
171
  const setupAudio = () => navigator.mediaDevices.getUserMedia({
142
172
  video: false,
143
173
  audio: {
144
- autoGainControl: false,
145
- // Disable automatic gain control
174
+ autoGainControl: true,
146
175
  noiseSuppression: true,
147
- // Enable noise suppression
148
176
  echoCancellation: true
149
- // Enable echo cancellation
150
177
  }
151
178
  }).then((stream) => {
152
- localStreamRef.current = stream;
179
+ localStream = stream;
153
180
  completeRemotePeerIds.forEach((id) => {
181
+ if (callsRef.current[id]) return;
154
182
  const call = peer.call(id, stream);
155
- call.on("stream", handleRemoteStream);
156
- call.on("close", call.removeAllListeners);
157
- callsRef.current[id] = call;
183
+ handleCall(call);
158
184
  });
159
185
  peer.on("call", (call) => {
186
+ if (callsRef.current[call.peer]) return call.close();
160
187
  call.answer(stream);
161
- call.on("stream", handleRemoteStream);
162
- call.on("close", call.removeAllListeners);
163
- callsRef.current[call.peer] = call;
188
+ handleCall(call);
164
189
  });
165
190
  }).catch(handleError);
166
191
  if (peer.open) setupAudio();
167
192
  else peer.once("open", setupAudio);
168
193
  return () => {
169
194
  var _a;
170
- (_a = localStreamRef.current) == null ? void 0 : _a.getTracks().forEach((track) => track.stop());
171
- localStreamRef.current = null;
195
+ localStream == null ? void 0 : localStream.getTracks().forEach((track) => track.stop());
172
196
  Object.values(callsRef.current).forEach(closeConnection);
173
197
  callsRef.current = {};
198
+ Object.keys(sourceNodesRef.current).forEach(removePeerAudio);
199
+ (_a = audioContextRef.current) == null ? void 0 : _a.close();
200
+ audioContextRef.current = null;
201
+ mixerRef.current = null;
174
202
  };
175
203
  }, [audio, peer]);
176
- return { peerId: completePeerId, audioStreamRef, remotePeers, messages, sendMessage, audio, setAudio };
204
+ return { peerId: completePeerId, remotePeers, messages, sendMessage, audio, setAudio };
177
205
  }
178
206
  function useMessages() {
179
207
  const [messages, setMessages] = useStorage("rpc-messages", []);
@@ -1,4 +1,4 @@
1
- import { useChat } from './chunk-GT3RG6VA.js';
1
+ import { useChat } from './chunk-L3CFU5IB.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';
@@ -28,15 +28,15 @@ function Chat(_a) {
28
28
  const _a2 = useChat(__spreadValues({
29
29
  text,
30
30
  audio,
31
- onMessageReceived: modifiedOnMessageReceived
32
- }, hookProps)), { peerId, audioStreamRef } = _a2, childrenOptions = __objRest(_a2, ["peerId", "audioStreamRef"]);
31
+ onMessageReceived: receiveMessageHandler
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);
36
36
  const dialogRef = useRef(null);
37
37
  const inputRef = useRef(null);
38
38
  const [notification, setNotification] = useState(false);
39
- function modifiedOnMessageReceived(message) {
39
+ function receiveMessageHandler(message) {
40
40
  var _a3;
41
41
  if (!((_a3 = dialogRef.current) == null ? void 0 : _a3.open)) setNotification(true);
42
42
  onMessageReceived == null ? void 0 : onMessageReceived(message);
@@ -59,7 +59,7 @@ function Chat(_a) {
59
59
  setDialog(true);
60
60
  }
61
61
  }
62
- ), notification && /* @__PURE__ */ React.createElement("span", { className: "rpc-badge" })), /* @__PURE__ */ React.createElement("dialog", { ref: dialogRef, className: `${dialog ? "rpc-dialog" : ""} rpc-position-${(dialogOptions == null ? void 0 : dialogOptions.position) || "center"}`, style: dialogOptions == null ? void 0 : dialogOptions.style }, /* @__PURE__ */ React.createElement("div", { className: "rpc-heading" }, "Chat"), /* @__PURE__ */ React.createElement("hr", { className: "rpc-hr" }), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { ref: containerRef, className: "rpc-message-container" }, messages.map(({ id, text: text2 }, i) => /* @__PURE__ */ React.createElement("div", { key: i }, /* @__PURE__ */ React.createElement("strong", null, id === peerId ? "You" : remotePeers[id], ": "), /* @__PURE__ */ React.createElement("span", null, text2)))), /* @__PURE__ */ React.createElement("hr", { className: "rpc-hr" }), /* @__PURE__ */ React.createElement(
62
+ ), notification && /* @__PURE__ */ React.createElement("span", { className: "rpc-badge" })), /* @__PURE__ */ React.createElement("dialog", { ref: dialogRef, className: `${dialog ? "rpc-dialog" : ""} rpc-position-${(dialogOptions == null ? void 0 : dialogOptions.position) || "center"}`, style: dialogOptions == null ? void 0 : dialogOptions.style }, /* @__PURE__ */ React.createElement("div", { className: "rpc-heading" }, "Chat"), /* @__PURE__ */ React.createElement("hr", { className: "rpc-hr" }), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { ref: containerRef, className: "rpc-message-container" }, messages.map(({ id, name, text: text2 }, i) => /* @__PURE__ */ React.createElement("div", { key: i }, /* @__PURE__ */ React.createElement("strong", null, id === peerId ? "You" : name, ": "), /* @__PURE__ */ React.createElement("span", null, text2)))), /* @__PURE__ */ React.createElement("hr", { className: "rpc-hr" }), /* @__PURE__ */ React.createElement(
63
63
  "form",
64
64
  {
65
65
  className: "rpc-input-container",
@@ -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" }))), audio && audioEnabled && /* @__PURE__ */ React.createElement("audio", { ref: audioStreamRef, autoPlay: true, style: { display: "none" } }));
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 };
@@ -1,5 +1,5 @@
1
- export { Chat as default } from './chunks/chunk-YDZ27MG7.js';
2
- import './chunks/chunk-GT3RG6VA.js';
1
+ export { Chat as default } from './chunks/chunk-OB5LLPQI.js';
2
+ import './chunks/chunk-L3CFU5IB.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
@@ -1,3 +1,3 @@
1
- export { useAudio, useChat, useMessages, useStorage } from './chunks/chunk-GT3RG6VA.js';
1
+ export { useAudio, useChat, useMessages, useStorage } from './chunks/chunk-L3CFU5IB.js';
2
2
  import './chunks/chunk-ZYFPSCFE.js';
3
3
  import './chunks/chunk-LNEKYYG7.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- export { Chat as default } from './chunks/chunk-YDZ27MG7.js';
2
- export { useChat } from './chunks/chunk-GT3RG6VA.js';
1
+ export { Chat as default } from './chunks/chunk-OB5LLPQI.js';
2
+ export { useChat } from './chunks/chunk-L3CFU5IB.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,13 +1,16 @@
1
1
  import { PeerOptions, DataConnection, MediaConnection } from 'peerjs';
2
2
  export { PeerOptions } from 'peerjs';
3
- import { CSSProperties, DetailedHTMLProps, HTMLAttributes, ReactNode, RefObject, SetStateAction } from 'react';
3
+ import { CSSProperties, DetailedHTMLProps, HTMLAttributes, SetStateAction, ReactNode } from 'react';
4
4
 
5
5
  type Connection = DataConnection | MediaConnection;
6
6
  type ErrorHandler = () => void;
7
- type Message = {
7
+ type InputMessage = {
8
8
  id: string;
9
9
  text: string;
10
10
  };
11
+ type Message = InputMessage & {
12
+ name: string;
13
+ };
11
14
  type MessageEventHandler = (message: Message) => void;
12
15
 
13
16
  type RemotePeerId = string | string[];
@@ -24,15 +27,16 @@ type UseChatProps = {
24
27
  onMessageSent?: MessageEventHandler;
25
28
  onMessageReceived?: MessageEventHandler;
26
29
  };
27
- type UseChatReturn = {
28
- peerId: string;
29
- audioStreamRef: RefObject<HTMLMediaElement | null>;
30
+ type ChildrenOptions = {
30
31
  remotePeers: RemotePeers;
31
32
  messages: Message[];
32
- sendMessage: (message: Message) => void;
33
+ sendMessage: (message: InputMessage) => void;
33
34
  audio: boolean;
34
35
  setAudio: (value: SetStateAction<boolean>) => void;
35
36
  };
37
+ type UseChatReturn = ChildrenOptions & {
38
+ peerId: string;
39
+ };
36
40
  type IconProps = DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>;
37
41
  type ChatProps = UseChatProps & {
38
42
  dialogOptions?: DialogOptions;
@@ -40,21 +44,12 @@ type ChatProps = UseChatProps & {
40
44
  children?: Children;
41
45
  };
42
46
  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
47
  type DialogOptions = {
51
48
  position?: DialogPosition;
52
49
  style?: CSSProperties;
53
50
  };
54
51
  type DialogPosition = "left" | "center" | "right";
55
52
  type DivProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
56
- type RemotePeers = {
57
- [id: string]: string;
58
- };
53
+ type RemotePeers = Record<string, string>;
59
54
 
60
- export type { ChatProps, Children, ChildrenOptions, Connection, DialogOptions, DialogPosition, DivProps, ErrorHandler, IconProps, Message, MessageEventHandler, RemotePeerId, RemotePeers, UseChatProps, UseChatReturn };
55
+ export type { ChatProps, Children, ChildrenOptions, Connection, DialogOptions, DialogPosition, DivProps, ErrorHandler, IconProps, InputMessage, Message, MessageEventHandler, RemotePeerId, RemotePeers, UseChatProps, UseChatReturn };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-peer-chat",
3
- "version": "0.8.6",
3
+ "version": "0.10.0",
4
4
  "description": "An easy to use react component for impleting peer-to-peer chatting.",
5
5
  "license": "MIT",
6
6
  "author": "Sahil Aggarwal <aggarwalsahil2004@gmail.com>",