react-peer-chat 0.3.10 → 0.4.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
@@ -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,9 +74,9 @@ 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
  }}
@@ -79,13 +97,13 @@ import Chat from 'react-peer-chat'
79
97
  export default function App() {
80
98
  return <Chat
81
99
  name='John Doe'
82
- peerId='some-unique-id'
83
- remotePeerId='another-unique-id'
100
+ peerId='my-unique-id'
101
+ remotePeerId='remote-unique-id'
84
102
  onError={() => {
85
103
  console.error('Microphone not accessible!');
86
104
  }}
87
105
  >
88
- {({ remotePeerName, messages, addMessage, audio, setAudio }) => (
106
+ {({ remotePeers, messages, addMessage, audio, setAudio }) => (
89
107
  <YourCustomComponent>
90
108
  {...}
91
109
  </YourCustomComponent>
@@ -97,11 +115,11 @@ export default function App() {
97
115
  Here is the full API for the `<Chat>` component, these properties can be set on an instance of Chat:
98
116
  | Parameter | Type | Required | Default | Description |
99
117
  | - | - | - | - | - |
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. |
118
+ | `name` | `string` | No | Anonymous User | Name of the peer which will be shown to the remote peer. |
119
+ | `peerId` | `string` | Yes | - | It is the unique id that is alloted to a peer. It uniquely identifies a peer from other peers. |
120
+ | `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). |
121
+ | `text` | `boolean` | No | `true` | Text chat will be enabled if this property is set to true. |
122
+ | `voice` | `boolean` | No | `true` | Voice chat will be enabled if this property is set to true. |
105
123
  | `peerOptions` | [`PeerOptions`](#PeerOptions) | No | - | Options to customize peerjs Peer instance. |
106
124
  | `dialogOptions` | [`DialogOptions`](#DialogOptions) | No | { position: 'center' } | Options to customize text dialog box styling. |
107
125
  | `onError` | `Function` | No | `() => alert('Microphone not accessible!')` | Function to be executed when microphone is not accessible. |
@@ -124,12 +142,13 @@ type DialogOptions = {
124
142
  #### Children
125
143
  ```typescript
126
144
  import { ReactNode } from 'react';
145
+ type RemotePeers = { [id: string]: string }
127
146
  type Message = {
128
147
  id: string;
129
148
  text: string;
130
149
  };
131
150
  type ChildrenOptions = {
132
- remotePeerName?: string;
151
+ remotePeers?: RemotePeers;
133
152
  messages?: Message[];
134
153
  addMessage?: (message: Message, sendToRemotePeer?: boolean) => void;
135
154
  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,7 +26,7 @@ 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;
@@ -33,3 +37,4 @@ export interface ChatProps {
33
37
  }
34
38
  export default function Chat({ name, peerId, remotePeerId, peerOptions, text, voice, dialogOptions, onError, children, props }: ChatProps): React.JSX.Element;
35
39
  export declare const clearChat: () => void;
40
+ export {};
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("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,55 @@ 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() {
55
+ (async function () {
50
56
  const { Peer } = await import('peerjs');
51
- const peer = new Peer(`rpc-${peerId}`, peerOptions);
57
+ const peer = new Peer(peerId, peerOptions);
52
58
  setPeer(peer);
53
59
  })();
54
60
  }, [audio]);
55
61
  useEffect(() => {
56
62
  if (!peer)
57
63
  return;
58
- let call;
64
+ let calls = {};
59
65
  peer.on('open', () => {
60
- if (remotePeerId) {
61
- remotePeerId = `rpc-${remotePeerId}`;
66
+ remotePeerIds.forEach(id => {
62
67
  if (text)
63
- handleConnection(peer.connect(remotePeerId, { metadata: name }));
64
- }
68
+ handleConnection(peer.connect(id));
69
+ });
65
70
  if (audio) {
66
71
  const getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
67
72
  try {
@@ -74,16 +79,17 @@ export default function Chat({ name, peerId, remotePeerId, peerOptions, text = t
74
79
  }
75
80
  }, (stream) => {
76
81
  localStream.current = stream;
77
- if (remotePeerId) {
78
- call = peer.call(remotePeerId, stream);
82
+ remotePeerIds.forEach(id => {
83
+ const call = peer.call(id, stream);
79
84
  call.on('stream', handleRemoteStream);
80
85
  call.on('close', call.removeAllListeners);
81
- }
82
- peer.on('call', e => {
83
- call = e;
86
+ calls[id] = call;
87
+ });
88
+ peer.on('call', call => {
84
89
  call.answer(stream);
85
90
  call.on('stream', handleRemoteStream);
86
91
  call.on('close', call.removeAllListeners);
92
+ calls[call.peer] = call;
87
93
  });
88
94
  }, onError);
89
95
  }
@@ -94,12 +100,11 @@ export default function Chat({ name, peerId, remotePeerId, peerOptions, text = t
94
100
  });
95
101
  peer.on('connection', handleConnection);
96
102
  return () => {
97
- var _a, _b, _c;
103
+ var _a;
98
104
  (_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();
105
+ Object.values(connRef.current).forEach(closeConnection);
106
+ connRef.current = {};
107
+ Object.values(calls).forEach(closeConnection);
103
108
  peer.removeAllListeners();
104
109
  peer.destroy();
105
110
  };
@@ -115,9 +120,9 @@ export default function Chat({ name, peerId, remotePeerId, peerOptions, text = t
115
120
  const container = containerRef.current;
116
121
  if (container)
117
122
  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,
123
+ }, [dialog, remotePeers, messages]);
124
+ return React.createElement("div", Object.assign({ className: 'rpc-main rpc-font' }, props),
125
+ typeof children === 'function' ? children({ remotePeers, messages, addMessage, audio, setAudio }) : React.createElement(React.Fragment, null,
121
126
  text && React.createElement("div", { className: 'rpc-dialog-container' },
122
127
  dialog ? React.createElement(BiSolidMessageX, { title: 'Close chat', onClick: () => setDialog(false) }) : React.createElement("div", { className: 'rpc-notification' },
123
128
  React.createElement(BiSolidMessageDetail, { title: 'Open chat', onClick: () => {
@@ -131,7 +136,7 @@ export default function Chat({ name, peerId, remotePeerId, peerOptions, text = t
131
136
  React.createElement("div", null,
132
137
  React.createElement("div", { ref: containerRef, className: 'rpc-message-container' }, messages.map(({ id, text }, i) => React.createElement("div", { key: i },
133
138
  React.createElement("strong", null,
134
- id === peerId ? 'You' : remotePeerName,
139
+ id === peerId ? 'You' : remotePeers[id],
135
140
  ": "),
136
141
  React.createElement("span", null, text)))),
137
142
  React.createElement("hr", { className: 'rpc-hr' }),
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.0",
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",