react-peer-chat 0.3.10 → 0.4.1

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
@@ -1,13 +1,13 @@
1
1
  # react-peer-chat
2
2
  An easy to use react component for impleting peer-to-peer chatting using [peerjs](https://peerjs.com/) under the hood.
3
3
 
4
- It is as easy as to import a React component!
5
4
  ## Features
6
5
  - Peer-to-peer chat without need to have any knowledge about WebRTC
7
6
  - Easy to use
8
7
  - Supports text chat that persists on page reload
9
8
  - Clear text chat on command
10
9
  - Supports voice chat
10
+ - Multiple peer connections. See [multi peer usage](#Multi-Peer-Usage)
11
11
  - Fully Customizable. See [usage with FoC](#Full-Customization)
12
12
  ## Installation
13
13
  To install react-peer-chat
@@ -38,8 +38,26 @@ export default function App() {
38
38
  return <div>
39
39
  <Chat
40
40
  name='John Doe'
41
- peerId='some-unique-id'
42
- remotePeerId='another-unique-id'
41
+ peerId='my-unique-id'
42
+ remotePeerId='remote-unique-id'
43
+ />
44
+ {/* Text chat will be cleared when following button is clicked. */}
45
+ <button onClick={clearChat}>Clear Chat</button>
46
+ </div>
47
+ }
48
+ ```
49
+ #### Multi Peer Usage
50
+ ```jsx
51
+ import React from 'react';
52
+ import Chat, { clearChat } from 'react-peer-chat';
53
+ import 'react-peer-chat/build/styles.css';
54
+
55
+ export default function App() {
56
+ return <div>
57
+ <Chat
58
+ name='John Doe'
59
+ peerId='my-unique-id'
60
+ remotePeerId={['remote-unique-id-1', 'remote-unique-id-2', 'remote-unique-id-3']} // Array of remote peer ids
43
61
  />
44
62
  {/* Text chat will be cleared when following button is clicked. */}
45
63
  <button onClick={clearChat}>Clear Chat</button>
@@ -56,16 +74,15 @@ import 'react-peer-chat/build/styles.css';
56
74
  export default function App() {
57
75
  return <Chat
58
76
  name='John Doe'
59
- peerId='some-unique-id'
60
- remotePeerId='another-unique-id'
61
- dialogOptions={{
77
+ peerId='my-unique-id'
78
+ remotePeerId='remote-unique-id'
79
+ dialogOptions={{
62
80
  position: 'left',
63
81
  style: { padding: '4px' }
64
82
  }}
65
83
  props={{ title: 'React Peer Chat Component' }}
66
- onError={() => {
67
- console.error('Microphone not accessible!');
68
- }}
84
+ onError={() => console.error('Browser not supported!')}
85
+ onMicError={() => console.error('Microphone not accessible!')}
69
86
  />
70
87
  }
71
88
  ```
@@ -79,13 +96,12 @@ import Chat from 'react-peer-chat'
79
96
  export default function App() {
80
97
  return <Chat
81
98
  name='John Doe'
82
- peerId='some-unique-id'
83
- remotePeerId='another-unique-id'
84
- onError={() => {
85
- console.error('Microphone not accessible!');
86
- }}
99
+ peerId='my-unique-id'
100
+ remotePeerId='remote-unique-id'
101
+ onError={() => console.error('Browser not supported!')}
102
+ onMicError={() => console.error('Microphone not accessible!')}
87
103
  >
88
- {({ remotePeerName, messages, addMessage, audio, setAudio }) => (
104
+ {({ remotePeers, messages, addMessage, audio, setAudio }) => (
89
105
  <YourCustomComponent>
90
106
  {...}
91
107
  </YourCustomComponent>
@@ -97,14 +113,15 @@ export default function App() {
97
113
  Here is the full API for the `<Chat>` component, these properties can be set on an instance of Chat:
98
114
  | Parameter | Type | Required | Default | Description |
99
115
  | - | - | - | - | - |
100
- | `name` | `String` | No | Anonymous User | Name of the peer which will be shown to the remote peer. |
101
- | `peerId` | `String` | Yes | - | It is the unique id that is alloted to a peer. It uniquely identifies a peer from other peers. |
102
- | `remotePeerId` | `String` | No | - | It is the unique id of the remote peer. If provided, the peer will try to connect to the remote peer. |
103
- | `text` | `Boolean` | No | `true` | Text chat will be enabled if this property is set to true. |
104
- | `voice` | `Boolean` | No | `true` | Voice chat will be enabled if this property is set to true. |
116
+ | `name` | `string` | No | Anonymous User | Name of the peer which will be shown to the remote peer. |
117
+ | `peerId` | `string` | Yes | - | It is the unique id that is alloted to a peer. It uniquely identifies a peer from other peers. |
118
+ | `remotePeerId` | `string \| string[]` | No | - | It is the unique id (or array of unique ids) of the remote peer(s). If provided, the peer will try to connect to the remote peer(s). |
119
+ | `text` | `boolean` | No | `true` | Text chat will be enabled if this property is set to true. |
120
+ | `voice` | `boolean` | No | `true` | Voice chat will be enabled if this property is set to true. |
105
121
  | `peerOptions` | [`PeerOptions`](#PeerOptions) | No | - | Options to customize peerjs Peer instance. |
106
122
  | `dialogOptions` | [`DialogOptions`](#DialogOptions) | No | { position: 'center' } | Options to customize text dialog box styling. |
107
- | `onError` | `Function` | No | `() => alert('Microphone not accessible!')` | Function to be executed when microphone is not accessible. |
123
+ | `onError` | `Function` | No | `() => alert('Browser not supported! Try some other browser.')` | Function to be executed if browser doesn't support [WebRTC](https://webrtc.org/). |
124
+ | `onMicError` | `Function` | No | `() => alert('Microphone not accessible!')` | Function to be executed when microphone is not accessible. |
108
125
  | `props` | `React.DetailedHTMLProps` | No | - | Props to customize the `<Chat>` component. |
109
126
  | `children` | [`Children`](#Children) | No | - | Props to customize the `<Chat>` component. |
110
127
  ### Types
@@ -124,12 +141,13 @@ type DialogOptions = {
124
141
  #### Children
125
142
  ```typescript
126
143
  import { ReactNode } from 'react';
144
+ type RemotePeers = { [id: string]: string }
127
145
  type Message = {
128
146
  id: string;
129
147
  text: string;
130
148
  };
131
149
  type ChildrenOptions = {
132
- remotePeerName?: string;
150
+ remotePeers?: RemotePeers;
133
151
  messages?: Message[];
134
152
  addMessage?: (message: Message, sendToRemotePeer?: boolean) => void;
135
153
  audio?: boolean;
package/build/icons.js CHANGED
@@ -1,27 +1,27 @@
1
1
  import React from "react";
2
2
  export function BiSolidMessageDetail(props) {
3
- return React.createElement("span", { className: 'rpc-icon-container', ...props },
3
+ return React.createElement("span", Object.assign({ className: 'rpc-icon-container' }, props),
4
4
  React.createElement("svg", { fill: "currentColor", width: '1.5rem', height: '1.5rem', className: 'rpc-scale-down' },
5
5
  React.createElement("path", { d: "M20 2H4c-1.103 0-2 .894-2 1.992v12.016C2 17.106 2.897 18 4 18h3v4l6.351-4H20c1.103 0 2-.894 2-1.992V3.992A1.998 1.998 0 0 0 20 2zm-6 11H7v-2h7v2zm3-4H7V7h10v2z" })));
6
6
  }
7
7
  export function BiSolidMessageX(props) {
8
- return React.createElement("span", { className: 'rpc-icon-container', ...props },
8
+ return React.createElement("span", Object.assign({ className: 'rpc-icon-container' }, props),
9
9
  React.createElement("svg", { fill: "currentColor", width: '1.5rem', height: '1.5rem', className: 'rpc-scale-down' },
10
10
  React.createElement("path", { d: "M20 2H4c-1.103 0-2 .894-2 1.992v12.016C2 17.106 2.897 18 4 18h3v4l6.351-4H20c1.103 0 2-.894 2-1.992V3.992A1.998 1.998 0 0 0 20 2zm-3.293 11.293-1.414 1.414L12 11.414l-3.293 3.293-1.414-1.414L10.586 10 7.293 6.707l1.414-1.414L12 8.586l3.293-3.293 1.414 1.414L13.414 10l3.293 3.293z" })));
11
11
  }
12
12
  export function GrSend(props) {
13
- return React.createElement("span", { className: 'rpc-icon-container', ...props },
13
+ return React.createElement("span", Object.assign({ className: 'rpc-icon-container' }, props),
14
14
  React.createElement("svg", { fill: "currentColor", width: '1.5rem', height: '1.5rem', className: 'rpc-scale-down rpc-invert' },
15
15
  React.createElement("path", { fill: "none", stroke: "#000", strokeWidth: 2, d: "M22,3 L2,11 L20.5,19 L22,3 Z M10,20.5 L13,16 M15.5,9.5 L9,14 L9.85884537,20.0119176 C9.93680292,20.5576204 10.0751625,20.5490248 10.1651297,20.009222 L11,15 L15.5,9.5 Z" })));
16
16
  }
17
17
  export function BsFillMicFill(props) {
18
- return React.createElement("span", { className: 'rpc-icon-container', ...props },
18
+ return React.createElement("span", Object.assign({ className: 'rpc-icon-container' }, props),
19
19
  React.createElement("svg", { fill: "currentColor", width: '1rem', height: '1rem' },
20
20
  React.createElement("path", { d: "M5 3a3 3 0 0 1 6 0v5a3 3 0 0 1-6 0V3z" }),
21
21
  React.createElement("path", { d: "M3.5 6.5A.5.5 0 0 1 4 7v1a4 4 0 0 0 8 0V7a.5.5 0 0 1 1 0v1a5 5 0 0 1-4.5 4.975V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 .5-.5z" })));
22
22
  }
23
23
  export function BsFillMicMuteFill(props) {
24
- return React.createElement("span", { className: 'rpc-icon-container', ...props },
24
+ return React.createElement("span", Object.assign({ className: 'rpc-icon-container' }, props),
25
25
  React.createElement("svg", { fill: "currentColor", width: '1rem', height: '1rem' },
26
26
  React.createElement("path", { d: "M13 8c0 .564-.094 1.107-.266 1.613l-.814-.814A4.02 4.02 0 0 0 12 8V7a.5.5 0 0 1 1 0v1zm-5 4c.818 0 1.578-.245 2.212-.667l.718.719a4.973 4.973 0 0 1-2.43.923V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 1 0v1a4 4 0 0 0 4 4zm3-9v4.879L5.158 2.037A3.001 3.001 0 0 1 11 3z" }),
27
27
  React.createElement("path", { d: "M9.486 10.607 5 6.12V8a3 3 0 0 0 4.486 2.607zm-7.84-9.253 12 12 .708-.708-12-12-.708.708z" })));
package/build/index.d.ts CHANGED
@@ -1,17 +1,21 @@
1
1
  import React, { CSSProperties, DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react';
2
2
  import { PeerOptions as ImportedPeerOptions } from 'peerjs';
3
+ export type RemotePeerId = string | string[];
3
4
  export type PeerOptions = ImportedPeerOptions;
4
5
  export type DialogPosition = 'left' | 'center' | 'right';
5
6
  export interface DialogOptions {
6
7
  position?: DialogPosition;
7
8
  style?: CSSProperties;
8
9
  }
10
+ type RemotePeers = {
11
+ [id: string]: string;
12
+ };
9
13
  export interface Message {
10
14
  id: string;
11
15
  text: string;
12
16
  }
13
17
  export interface ChildrenOptions {
14
- remotePeerName?: string;
18
+ remotePeers?: RemotePeers;
15
19
  messages?: Message[];
16
20
  addMessage?: (message: Message, sendToRemotePeer?: boolean) => void;
17
21
  audio?: boolean;
@@ -22,14 +26,16 @@ export type Props = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivEle
22
26
  export interface ChatProps {
23
27
  name?: string;
24
28
  peerId: string;
25
- remotePeerId?: string;
29
+ remotePeerId?: RemotePeerId;
26
30
  text?: boolean;
27
31
  voice?: boolean;
28
32
  peerOptions?: PeerOptions;
29
33
  dialogOptions?: DialogOptions;
30
34
  onError?: Function;
35
+ onMicError?: Function;
31
36
  children?: Children;
32
37
  props?: Props;
33
38
  }
34
- export default function Chat({ name, peerId, remotePeerId, peerOptions, text, voice, dialogOptions, onError, children, props }: ChatProps): React.JSX.Element;
39
+ export type { IconProps } from './icons.js';
40
+ export default function Chat({ name, peerId, remotePeerId, peerOptions, text, voice, dialogOptions, onError, onMicError, children, props }: ChatProps): React.JSX.Element;
35
41
  export declare const clearChat: () => void;
package/build/index.js CHANGED
@@ -1,12 +1,16 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
2
  import useStorage, { removeStorage } from './storage.js';
3
3
  import { BiSolidMessageDetail, BiSolidMessageX, BsFillMicFill, BsFillMicMuteFill, GrSend } from './icons.js';
4
- export default function Chat({ name, peerId, remotePeerId, peerOptions, text = true, voice = true, dialogOptions, onError = () => alert("Microphone not accessible!"), children, props = {} }) {
4
+ function closeConnection(conn) {
5
+ conn.removeAllListeners();
6
+ conn.close();
7
+ }
8
+ export default function Chat({ name, peerId, remotePeerId = [], peerOptions, text = true, voice = true, dialogOptions, onError = () => alert('Browser not supported! Try some other browser.'), onMicError = () => alert('Microphone not accessible!'), children, props = {} }) {
5
9
  const [peer, setPeer] = useState();
6
10
  const [notification, setNotification] = useState(false);
7
- const [remotePeerName, setRemotePeer] = useStorage('rpc-remote-peer', '', { save: true });
11
+ const [remotePeers, setRemotePeers] = useStorage('rpc-remote-peer', {}, { save: true });
8
12
  const [messages, setMessages] = useStorage('rpc-messages', [], { save: true });
9
- const connRef = useRef();
13
+ const connRef = useRef({});
10
14
  const [dialog, setDialog] = useState(false);
11
15
  const dialogRef = useRef(null);
12
16
  const containerRef = useRef(null);
@@ -14,54 +18,57 @@ export default function Chat({ name, peerId, remotePeerId, peerOptions, text = t
14
18
  const [audio, setAudio] = useStorage('rpc-audio', false, { local: true, save: true });
15
19
  const streamRef = useRef(null);
16
20
  const localStream = useRef();
21
+ peerId = `rpc-${peerId}`;
22
+ if (typeof remotePeerId === 'string')
23
+ remotePeerId = [remotePeerId];
24
+ const remotePeerIds = remotePeerId.map(id => `rpc-${id}`);
17
25
  const handleRemoteStream = (remoteStream) => streamRef.current.srcObject = remoteStream;
18
26
  function addMessage(message, sendToRemotePeer = false) {
19
- var _a, _b;
20
- setMessages(old => old.concat(message));
27
+ var _a;
28
+ setMessages(prev => prev.concat(message));
21
29
  if (sendToRemotePeer)
22
- (_a = connRef.current) === null || _a === void 0 ? void 0 : _a.send({ type: 'message', message });
23
- else if (!((_b = dialogRef.current) === null || _b === void 0 ? void 0 : _b.open))
30
+ Object.values(connRef.current).forEach(conn => conn.send({ type: 'message', message }));
31
+ else if (!((_a = dialogRef.current) === null || _a === void 0 ? void 0 : _a.open))
24
32
  setNotification(true);
25
33
  }
26
34
  function handleConnection(conn) {
27
- connRef.current = conn;
35
+ connRef.current[conn.peer] = conn;
28
36
  conn.on('open', () => {
29
- conn.on('data', ({ type, message, remotePeerName, messages }) => {
37
+ conn.on('data', ({ type, message, remotePeerName }) => {
30
38
  if (type === 'message')
31
39
  addMessage(message);
32
- else if (type === 'init') {
33
- setRemotePeer(remotePeerName || 'Anonymous User');
34
- setMessages(old => {
35
- if (messages.length > old.length)
36
- return messages;
37
- return old;
40
+ else if (type === 'init')
41
+ setRemotePeers(prev => {
42
+ prev[conn.peer] = remotePeerName || 'Anonymous User';
43
+ return prev;
38
44
  });
39
- }
40
45
  });
41
- conn.send({ type: 'init', remotePeerName: name, messages });
46
+ conn.send({ type: 'init', remotePeerName: name });
42
47
  });
48
+ conn.on('close', conn.removeAllListeners);
43
49
  }
44
50
  useEffect(() => {
45
51
  if (!text && !audio) {
46
52
  setPeer(undefined);
47
53
  return;
48
54
  }
49
- (async function loadPeer() {
50
- const { Peer } = await import('peerjs');
51
- const peer = new Peer(`rpc-${peerId}`, peerOptions);
55
+ (async function () {
56
+ const { Peer, util: { supports: { audioVideo, data } } } = await import('peerjs');
57
+ if (!data || !audioVideo)
58
+ return onError();
59
+ const peer = new Peer(peerId, peerOptions);
52
60
  setPeer(peer);
53
61
  })();
54
62
  }, [audio]);
55
63
  useEffect(() => {
56
64
  if (!peer)
57
65
  return;
58
- let call;
66
+ let calls = {};
59
67
  peer.on('open', () => {
60
- if (remotePeerId) {
61
- remotePeerId = `rpc-${remotePeerId}`;
68
+ remotePeerIds.forEach(id => {
62
69
  if (text)
63
- handleConnection(peer.connect(remotePeerId, { metadata: name }));
64
- }
70
+ handleConnection(peer.connect(id));
71
+ });
65
72
  if (audio) {
66
73
  const getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
67
74
  try {
@@ -74,32 +81,32 @@ export default function Chat({ name, peerId, remotePeerId, peerOptions, text = t
74
81
  }
75
82
  }, (stream) => {
76
83
  localStream.current = stream;
77
- if (remotePeerId) {
78
- call = peer.call(remotePeerId, stream);
84
+ remotePeerIds.forEach(id => {
85
+ const call = peer.call(id, stream);
79
86
  call.on('stream', handleRemoteStream);
80
87
  call.on('close', call.removeAllListeners);
81
- }
82
- peer.on('call', e => {
83
- call = e;
88
+ calls[id] = call;
89
+ });
90
+ peer.on('call', call => {
84
91
  call.answer(stream);
85
92
  call.on('stream', handleRemoteStream);
86
93
  call.on('close', call.removeAllListeners);
94
+ calls[call.peer] = call;
87
95
  });
88
- }, onError);
96
+ }, onMicError);
89
97
  }
90
98
  catch (_a) {
91
- onError();
99
+ onMicError();
92
100
  }
93
101
  }
94
102
  });
95
103
  peer.on('connection', handleConnection);
96
104
  return () => {
97
- var _a, _b, _c;
105
+ var _a;
98
106
  (_a = localStream.current) === null || _a === void 0 ? void 0 : _a.getTracks().forEach(track => track.stop());
99
- (_b = connRef.current) === null || _b === void 0 ? void 0 : _b.removeAllListeners();
100
- (_c = connRef.current) === null || _c === void 0 ? void 0 : _c.close();
101
- call === null || call === void 0 ? void 0 : call.removeAllListeners();
102
- call === null || call === void 0 ? void 0 : call.close();
107
+ Object.values(connRef.current).forEach(closeConnection);
108
+ connRef.current = {};
109
+ Object.values(calls).forEach(closeConnection);
103
110
  peer.removeAllListeners();
104
111
  peer.destroy();
105
112
  };
@@ -115,9 +122,9 @@ export default function Chat({ name, peerId, remotePeerId, peerOptions, text = t
115
122
  const container = containerRef.current;
116
123
  if (container)
117
124
  container.scrollTop = container.scrollHeight;
118
- }, [dialog, remotePeerName, messages]);
119
- return React.createElement("div", { className: 'rpc-main rpc-font', ...props },
120
- typeof children === 'function' ? children({ remotePeerName, messages, addMessage, audio, setAudio }) : React.createElement(React.Fragment, null,
125
+ }, [dialog, remotePeers, messages]);
126
+ return React.createElement("div", Object.assign({ className: 'rpc-main rpc-font' }, props),
127
+ typeof children === 'function' ? children({ remotePeers, messages, addMessage, audio, setAudio }) : React.createElement(React.Fragment, null,
121
128
  text && React.createElement("div", { className: 'rpc-dialog-container' },
122
129
  dialog ? React.createElement(BiSolidMessageX, { title: 'Close chat', onClick: () => setDialog(false) }) : React.createElement("div", { className: 'rpc-notification' },
123
130
  React.createElement(BiSolidMessageDetail, { title: 'Open chat', onClick: () => {
@@ -131,7 +138,7 @@ export default function Chat({ name, peerId, remotePeerId, peerOptions, text = t
131
138
  React.createElement("div", null,
132
139
  React.createElement("div", { ref: containerRef, className: 'rpc-message-container' }, messages.map(({ id, text }, i) => React.createElement("div", { key: i },
133
140
  React.createElement("strong", null,
134
- id === peerId ? 'You' : remotePeerName,
141
+ id === peerId ? 'You' : remotePeers[id],
135
142
  ": "),
136
143
  React.createElement("span", null, text)))),
137
144
  React.createElement("hr", { className: 'rpc-hr' }),
@@ -147,7 +154,7 @@ export default function Chat({ name, peerId, remotePeerId, peerOptions, text = t
147
154
  React.createElement("input", { ref: inputRef, className: 'rpc-input rpc-font', placeholder: 'Enter a message' }),
148
155
  React.createElement("button", { type: 'submit', className: 'rpc-button' },
149
156
  React.createElement(GrSend, { title: 'Send message' })))))),
150
- voice && React.createElement("div", null, audio ? React.createElement(BsFillMicFill, { title: "Turn mic off", onClick: () => setAudio(false) }) : React.createElement(BsFillMicMuteFill, { title: "Turn mic on", onClick: () => setAudio(true) }))),
157
+ voice && React.createElement("div", null, audio ? React.createElement(BsFillMicFill, { title: 'Turn mic off', onClick: () => setAudio(false) }) : React.createElement(BsFillMicMuteFill, { title: 'Turn mic on', onClick: () => setAudio(true) }))),
151
158
  voice && audio && React.createElement("audio", { ref: streamRef, autoPlay: true, style: { display: 'none' } }));
152
159
  }
153
160
  export const clearChat = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-peer-chat",
3
- "version": "0.3.10",
3
+ "version": "0.4.1",
4
4
  "description": "An easy to use react component for impleting peer-to-peer chatting.",
5
5
  "main": "./build/index.js",
6
6
  "type": "module",