react-peer-chat 0.9.0 → 0.11.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
@@ -11,6 +11,7 @@ A simple-to-use React component for implementing peer-to-peer chatting, powered
11
11
  - Option to clear chat on command
12
12
  - Supports audio/voice chat with automatic mixing for multiple peers
13
13
  - Multiple peer connections. See [multi-peer usage](#multi-peer-usage)
14
+ - Automatic reconnection handling for network interruptions
14
15
  - Fully customizable. See [usage with FaC](#full-customization)
15
16
 
16
17
  ## Installation
@@ -100,8 +101,9 @@ export default function App() {
100
101
  style: { padding: "4px" },
101
102
  }}
102
103
  props={{ title: "React Peer Chat Component" }}
103
- onError={() => console.error("Browser not supported!")}
104
- onMicError={() => console.error("Microphone not accessible!")}
104
+ onError={(error) => console.error("Fatal error:", error)}
105
+ onPeerError={(error) => console.error("Peer error:", error.type, error)}
106
+ onNetworkError={(error) => console.log("Reconnecting...")}
105
107
  />
106
108
  );
107
109
  }
@@ -120,8 +122,8 @@ export default function App() {
120
122
  name='John Doe'
121
123
  peerId='my-unique-id'
122
124
  remotePeerId='remote-unique-id'
123
- onError={() => console.error('Browser not supported!')}
124
- onMicError={() => console.error('Microphone not accessible!')}
125
+ onError={(error) => console.error('Fatal error:', error)}
126
+ onPeerError={(error) => console.error('Peer error:', error.type, error)}
125
127
  >
126
128
  {({ remotePeers, messages, sendMessage, audio, setAudio }) => (
127
129
  <YourCustomComponent>
@@ -293,19 +295,20 @@ export default function App() {
293
295
 
294
296
  Here is the full API for the `useChat` hook, these options can be passed as parameters to the hook:
295
297
 
296
- | Parameter | Type | Required | Default | Description |
297
- | ------------------- | --------------------------------------------- | -------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
298
- | `name` | `string` | No | Anonymous User | Name of the peer which will be shown to the remote peer. |
299
- | `peerId` | `string` | Yes | - | It is the unique id that is alloted to a peer. It uniquely identifies a peer from other peers. |
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. |
301
- | `text` | `boolean` | No | `true` | Text chat will be enabled if this property is set to true. |
302
- | `recoverChat` | `boolean` | No | `false` | Old chats will be recovered upon reconnecting with the same peer(s). |
303
- | `audio` | `boolean` | No | `true` | Voice chat will be enabled if this property is set to true. Audio from multiple peers is automatically mixed. |
304
- | `peerOptions` | [`PeerOptions`](#peeroptions) | No | - | Options to customize peerjs Peer instance. |
305
- | `onError` | [`ErrorHandler`](#errorhandler) | No | `() => alert('Browser not supported! Try some other browser.')` | Function to be executed if browser doesn't support `WebRTC` |
306
- | `onMicError` | [`ErrorHandler`](#errorhandler) | No | `() => alert('Microphone not accessible!')` | Function to be executed when microphone is not accessible. |
307
- | `onMessageSent` | [`MessageEventHandler`](#messageeventhandler) | No | - | Function to be executed when a text message is sent to other peers. |
308
- | `onMessageReceived` | [`MessageEventHandler`](#messageeventhandler) | No | - | Function to be executed when a text message is received from other peers. |
298
+ | Parameter | Type | Required | Default | Description |
299
+ | ------------------- | --------------------------------------------- | -------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
300
+ | `name` | `string` | No | Anonymous User | Name of the peer which will be shown to the remote peer. |
301
+ | `peerId` | `string` | Yes | - | It is the unique id that is alloted to a peer. It uniquely identifies a peer from other peers. |
302
+ | `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. |
303
+ | `text` | `boolean` | No | `true` | Text chat will be enabled if this property is set to true. |
304
+ | `recoverChat` | `boolean` | No | `false` | Old chats will be recovered upon reconnecting with the same peer(s). |
305
+ | `audio` | `boolean` | No | `true` | Voice chat will be enabled if this property is set to true. Audio from multiple peers is automatically mixed. |
306
+ | `peerOptions` | [`PeerOptions`](#peeroptions) | No | - | Options to customize peerjs Peer instance. |
307
+ | `onError` | [`ErrorHandler`](#errorhandler) | No | `console.error` | Function to be executed for fatal errors (browser not supported, microphone not accessible). |
308
+ | `onPeerError` | [`PeerErrorHandler`](#peererrorhandler) | No | `console.error` | Function to be executed for all peer runtime errors. The library automatically handles reconnection for network errors. |
309
+ | `onNetworkError` | [`PeerErrorHandler`](#peererrorhandler) | No | - | Function to be executed for network/server errors (which trigger automatic reconnection). Useful for showing "reconnecting..." UI. |
310
+ | `onMessageSent` | [`MessageEventHandler`](#messageeventhandler) | No | - | Function to be executed when a text message is sent to other peers. |
311
+ | `onMessageReceived` | [`MessageEventHandler`](#messageeventhandler) | No | - | Function to be executed when a text message is received from other peers. |
309
312
 
310
313
  ### Chat Component
311
314
 
@@ -363,7 +366,15 @@ type DivProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement
363
366
  ### ErrorHandler
364
367
 
365
368
  ```typescript
366
- type ErrorHandler = () => void;
369
+ type ErrorHandler = (error: Error) => void;
370
+ ```
371
+
372
+ ### PeerErrorHandler
373
+
374
+ ```typescript
375
+ import type { PeerError, PeerErrorType } from "peerjs";
376
+
377
+ export type PeerErrorHandler = ErrorHandler<PeerError<`${PeerErrorType}`>>;
367
378
  ```
368
379
 
369
380
  ### MessageEventHandler
@@ -384,4 +395,4 @@ import type { PeerOptions } from "peerjs";
384
395
 
385
396
  ## License
386
397
 
387
- This project is licensed under the [MIT License](LICENSE).
398
+ This project is licensed under the [MIT License](LICENSE)
@@ -1,4 +1,4 @@
1
- import { useChat } from './chunk-MBDO7IOK.js';
1
+ import { useChat } from './chunk-FBLX5IM6.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,7 +28,7 @@ function Chat(_a) {
28
28
  const _a2 = useChat(__spreadValues({
29
29
  text,
30
30
  audio,
31
- onMessageReceived: modifiedOnMessageReceived
31
+ onMessageReceived: receiveMessageHandler
32
32
  }, hookProps)), { peerId } = _a2, childrenOptions = __objRest(_a2, ["peerId"]);
33
33
  const { remotePeers, messages, sendMessage, audio: audioEnabled, setAudio } = childrenOptions;
34
34
  const containerRef = useRef(null);
@@ -36,7 +36,7 @@ function Chat(_a) {
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",
@@ -56,8 +56,9 @@ function useChat({
56
56
  text = true,
57
57
  recoverChat = false,
58
58
  audio: allowed = true,
59
- onError = () => alert("Browser not supported! Try some other browser."),
60
- onMicError = () => alert("Microphone not accessible!"),
59
+ onError = console.error,
60
+ onPeerError = console.error,
61
+ onNetworkError,
61
62
  onMessageSent,
62
63
  onMessageReceived
63
64
  }) {
@@ -75,14 +76,29 @@ function useChat({
75
76
  return { completePeerId: addPrefix(peerId), completeRemotePeerIds: remotePeerIds.map(addPrefix) };
76
77
  }, [peerId]);
77
78
  function handleCall(call) {
78
- const id = call.peer;
79
- call.on("stream", (stream) => handleRemoteStream(id, stream));
79
+ const peerId2 = call.peer;
80
+ call.on("stream", () => {
81
+ callsRef.current[peerId2] = call;
82
+ if (!audioContextRef.current) audioContextRef.current = new AudioContext();
83
+ if (audioContextRef.current.state === "suspended") audioContextRef.current.resume();
84
+ if (!mixerRef.current) {
85
+ mixerRef.current = audioContextRef.current.createGain();
86
+ mixerRef.current.connect(audioContextRef.current.destination);
87
+ }
88
+ removePeerAudio(peerId2);
89
+ const audio2 = new Audio();
90
+ audio2.srcObject = call.remoteStream;
91
+ audio2.autoplay = true;
92
+ audio2.muted = false;
93
+ const source = audioContextRef.current.createMediaElementSource(audio2);
94
+ source.connect(mixerRef.current);
95
+ sourceNodesRef.current[peerId2] = source;
96
+ });
80
97
  call.on("close", () => {
81
98
  call.removeAllListeners();
82
- removePeerAudio(id);
83
- delete callsRef.current[id];
99
+ removePeerAudio(peerId2);
100
+ delete callsRef.current[peerId2];
84
101
  });
85
- callsRef.current[id] = call;
86
102
  }
87
103
  function handleConnection(conn) {
88
104
  connRef.current[conn.peer] = conn;
@@ -98,20 +114,9 @@ function useChat({
98
114
  });
99
115
  conn.on("close", conn.removeAllListeners);
100
116
  }
101
- function handleError() {
117
+ function handleMediaError() {
102
118
  setAudio(false);
103
- onMicError();
104
- }
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;
119
+ onError(new Error("Microphone not accessible!"));
115
120
  }
116
121
  function receiveMessage(message) {
117
122
  addMessage(message);
@@ -123,9 +128,10 @@ function useChat({
123
128
  delete sourceNodesRef.current[peerId2];
124
129
  }
125
130
  function sendMessage(message) {
126
- addMessage(message);
127
- Object.values(connRef.current).forEach((conn) => conn.send({ type: "message", message }));
128
- onMessageSent == null ? void 0 : onMessageSent(message);
131
+ const event = { type: "message", message: __spreadProps(__spreadValues({}, message), { name }) };
132
+ addMessage(event.message);
133
+ Object.values(connRef.current).forEach((conn) => conn.send(event));
134
+ onMessageSent == null ? void 0 : onMessageSent(event.message);
129
135
  }
130
136
  useEffect(() => {
131
137
  if (!text && !audio) return;
@@ -136,9 +142,17 @@ function useChat({
136
142
  supports: { audioVideo, data }
137
143
  }
138
144
  }) => {
139
- if (!data || !audioVideo) return onError();
145
+ if (!data || !audioVideo) return onError(new Error("Browser not supported! Try some other browser."));
140
146
  const peer2 = new Peer(completePeerId, __spreadValues({ config: defaultConfig }, peerOptions));
141
147
  peer2.on("connection", handleConnection);
148
+ peer2.on("disconnected", () => peer2.reconnect());
149
+ peer2.on("error", (error) => {
150
+ if (error.type === "network" || error.type === "server-error") {
151
+ setTimeout(() => peer2.reconnect(), 1e3);
152
+ onNetworkError == null ? void 0 : onNetworkError(error);
153
+ }
154
+ onPeerError(error);
155
+ });
142
156
  setPeer(peer2);
143
157
  }
144
158
  );
@@ -154,8 +168,9 @@ function useChat({
154
168
  if (!text || !peer) return;
155
169
  const handleOpen = () => completeRemotePeerIds.forEach((id) => handleConnection(peer.connect(id)));
156
170
  if (peer.open) handleOpen();
157
- else peer.once("open", handleOpen);
171
+ peer.on("open", handleOpen);
158
172
  return () => {
173
+ peer.off("open", handleOpen);
159
174
  Object.values(connRef.current).forEach(closeConnection);
160
175
  connRef.current = {};
161
176
  };
@@ -163,33 +178,35 @@ function useChat({
163
178
  useEffect(() => {
164
179
  if (!audio || !peer) return;
165
180
  let localStream;
166
- const setupAudio = () => navigator.mediaDevices.getUserMedia({
167
- video: false,
168
- audio: {
169
- autoGainControl: false,
170
- // Disable automatic gain control
171
- noiseSuppression: true,
172
- // Enable noise suppression
173
- echoCancellation: true
174
- // Enable echo cancellation
175
- }
176
- }).then((stream) => {
177
- localStream = stream;
178
- completeRemotePeerIds.forEach((id) => {
179
- if (callsRef.current[id]) return;
180
- const call = peer.call(id, stream);
181
- handleCall(call);
182
- });
183
- peer.on("call", (call) => {
184
- if (callsRef.current[call.peer]) return call.close();
185
- call.answer(stream);
186
- handleCall(call);
187
- });
188
- }).catch(handleError);
181
+ const setupAudio = () => {
182
+ if (!navigator.mediaDevices) return handleMediaError();
183
+ navigator.mediaDevices.getUserMedia({
184
+ video: false,
185
+ audio: {
186
+ autoGainControl: true,
187
+ noiseSuppression: true,
188
+ echoCancellation: true
189
+ }
190
+ }).then((stream) => {
191
+ localStream = stream;
192
+ completeRemotePeerIds.forEach((id) => {
193
+ if (callsRef.current[id]) return;
194
+ const call = peer.call(id, stream);
195
+ handleCall(call);
196
+ });
197
+ peer.on("call", (call) => {
198
+ if (callsRef.current[call.peer]) return call.close();
199
+ call.answer(stream);
200
+ handleCall(call);
201
+ });
202
+ }).catch(handleMediaError);
203
+ };
189
204
  if (peer.open) setupAudio();
190
- else peer.once("open", setupAudio);
205
+ peer.on("open", setupAudio);
191
206
  return () => {
192
207
  var _a;
208
+ peer.off("open", setupAudio);
209
+ peer.off("call");
193
210
  localStream == null ? void 0 : localStream.getTracks().forEach((track) => track.stop());
194
211
  Object.values(callsRef.current).forEach(closeConnection);
195
212
  callsRef.current = {};
@@ -1,5 +1,5 @@
1
- export { Chat as default } from './chunks/chunk-N4TBKQIR.js';
2
- import './chunks/chunk-MBDO7IOK.js';
1
+ export { Chat as default } from './chunks/chunk-6JONVNJK.js';
2
+ import './chunks/chunk-FBLX5IM6.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.d.ts CHANGED
@@ -2,7 +2,7 @@ import { SetStateAction } from 'react';
2
2
  import { UseChatProps, UseChatReturn, Message } from './types.js';
3
3
  import 'peerjs';
4
4
 
5
- declare function useChat({ peerId, name, remotePeerId, peerOptions, text, recoverChat, audio: allowed, onError, onMicError, onMessageSent, onMessageReceived, }: UseChatProps): UseChatReturn;
5
+ declare function useChat({ peerId, name, remotePeerId, peerOptions, text, recoverChat, audio: allowed, onError, onPeerError, onNetworkError, onMessageSent, onMessageReceived, }: UseChatProps): UseChatReturn;
6
6
  declare function useMessages(): readonly [Message[], (value: SetStateAction<Message[]>) => void, (message: Message) => void];
7
7
  declare function useStorage<T>(key: string, initialValue: T, local?: boolean): readonly [T, (value: SetStateAction<T>) => void];
8
8
  declare function useStorage<T>(key: string, initialValue?: T, local?: boolean): readonly [T | undefined, (value: SetStateAction<T | undefined>) => void];
package/dist/hooks.js CHANGED
@@ -1,3 +1,3 @@
1
- export { useAudio, useChat, useMessages, useStorage } from './chunks/chunk-MBDO7IOK.js';
1
+ export { useAudio, useChat, useMessages, useStorage } from './chunks/chunk-FBLX5IM6.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-N4TBKQIR.js';
2
- export { useChat } from './chunks/chunk-MBDO7IOK.js';
1
+ export { Chat as default } from './chunks/chunk-6JONVNJK.js';
2
+ export { useChat } from './chunks/chunk-FBLX5IM6.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,14 +1,18 @@
1
- import { PeerOptions, DataConnection, MediaConnection } from 'peerjs';
1
+ import { PeerOptions, PeerError, PeerErrorType, DataConnection, MediaConnection } from 'peerjs';
2
2
  export { PeerOptions } from 'peerjs';
3
3
  import { CSSProperties, DetailedHTMLProps, HTMLAttributes, SetStateAction, ReactNode } from 'react';
4
4
 
5
5
  type Connection = DataConnection | MediaConnection;
6
- type ErrorHandler = () => void;
7
- type Message = {
6
+ type ErrorHandler<E = Error> = (error: E) => void;
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;
15
+ type PeerErrorHandler = ErrorHandler<PeerError<`${PeerErrorType}`>>;
12
16
 
13
17
  type RemotePeerId = string | string[];
14
18
  type UseChatProps = {
@@ -20,14 +24,15 @@ type UseChatProps = {
20
24
  audio?: boolean;
21
25
  peerOptions?: PeerOptions;
22
26
  onError?: ErrorHandler;
23
- onMicError?: ErrorHandler;
27
+ onPeerError?: PeerErrorHandler;
28
+ onNetworkError?: PeerErrorHandler;
24
29
  onMessageSent?: MessageEventHandler;
25
30
  onMessageReceived?: MessageEventHandler;
26
31
  };
27
32
  type ChildrenOptions = {
28
33
  remotePeers: RemotePeers;
29
34
  messages: Message[];
30
- sendMessage: (message: Message) => void;
35
+ sendMessage: (message: InputMessage) => void;
31
36
  audio: boolean;
32
37
  setAudio: (value: SetStateAction<boolean>) => void;
33
38
  };
@@ -49,4 +54,4 @@ type DialogPosition = "left" | "center" | "right";
49
54
  type DivProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
50
55
  type RemotePeers = Record<string, string>;
51
56
 
52
- export type { ChatProps, Children, ChildrenOptions, Connection, DialogOptions, DialogPosition, DivProps, ErrorHandler, IconProps, Message, MessageEventHandler, RemotePeerId, RemotePeers, UseChatProps, UseChatReturn };
57
+ export type { ChatProps, Children, ChildrenOptions, Connection, DialogOptions, DialogPosition, DivProps, ErrorHandler, IconProps, InputMessage, Message, MessageEventHandler, PeerErrorHandler, RemotePeerId, RemotePeers, UseChatProps, UseChatReturn };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-peer-chat",
3
- "version": "0.9.0",
3
+ "version": "0.11.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>",