react-peer-chat 0.1.11 → 0.2.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.
@@ -0,0 +1,11 @@
1
+ * {
2
+ -webkit-tap-highlight-color: transparent;
3
+ }
4
+
5
+ .scale-down {
6
+ scale: calc(2/3);
7
+ }
8
+
9
+ .invert {
10
+ filter: invert(100%);
11
+ }
package/build/icons.d.ts CHANGED
@@ -1,3 +1,7 @@
1
1
  import React from "react";
2
+ import './icons.css';
3
+ export declare function BiSolidMessageDetail(props: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>): React.JSX.Element;
4
+ export declare function BiSolidMessageX(props: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>): React.JSX.Element;
5
+ export declare function GrSend(props: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>): React.JSX.Element;
2
6
  export declare function BsFillMicFill(props: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>): React.JSX.Element;
3
7
  export declare function BsFillMicMuteFill(props: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>): React.JSX.Element;
package/build/icons.js CHANGED
@@ -1,13 +1,39 @@
1
1
  import React from "react";
2
+ import './icons.css';
3
+ export function BiSolidMessageDetail(props) {
4
+ return <span {...props}>
5
+ <svg fill="currentColor" width='1.5rem' height='1.5rem' className='scale-down'>
6
+ <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"/>
7
+ </svg>
8
+ </span>;
9
+ }
10
+ export function BiSolidMessageX(props) {
11
+ return <span {...props}>
12
+ <svg fill="currentColor" width='1.5rem' height='1.5rem' className='scale-down'>
13
+ <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"/>
14
+ </svg>
15
+ </span>;
16
+ }
17
+ export function GrSend(props) {
18
+ return <span {...props}>
19
+ <svg fill="currentColor" width='1.5rem' height='1.5rem' className='scale-down invert'>
20
+ <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"/>
21
+ </svg>
22
+ </span>;
23
+ }
2
24
  export function BsFillMicFill(props) {
3
- return React.createElement("span", Object.assign({}, props),
4
- React.createElement("svg", { fill: "currentColor", width: '16px', height: '16px' },
5
- React.createElement("path", { d: "M5 3a3 3 0 0 1 6 0v5a3 3 0 0 1-6 0V3z" }),
6
- 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" })));
25
+ return <span {...props}>
26
+ <svg fill="currentColor" width='1rem' height='1rem'>
27
+ <path d="M5 3a3 3 0 0 1 6 0v5a3 3 0 0 1-6 0V3z"/>
28
+ <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"/>
29
+ </svg>
30
+ </span>;
7
31
  }
8
32
  export function BsFillMicMuteFill(props) {
9
- return React.createElement("span", Object.assign({}, props),
10
- React.createElement("svg", { fill: "currentColor", width: '16px', height: '16px' },
11
- 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" }),
12
- 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" })));
33
+ return <span {...props}>
34
+ <svg fill="currentColor" width='1rem' height='1rem'>
35
+ <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"/>
36
+ <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"/>
37
+ </svg>
38
+ </span>;
13
39
  }
@@ -0,0 +1,77 @@
1
+ .main {
2
+ display: flex;
3
+ align-items: center;
4
+ column-gap: 0.5rem;
5
+ }
6
+
7
+ .notification {
8
+ position: relative;
9
+ }
10
+
11
+ .notification .badge {
12
+ background-color: red;
13
+ position: absolute;
14
+ top: 0.25rem;
15
+ right: 0.25rem;
16
+ width: 0.25rem;
17
+ height: 0.25rem;
18
+ border-radius: 100%;
19
+ }
20
+
21
+ .dialog {
22
+ display: flex;
23
+ flex-direction: column;
24
+ row-gap: 0.25rem;
25
+ background-color: black;
26
+ color: white;
27
+ padding-top: 0.25rem;
28
+ outline: 2px solid white;
29
+ border-radius: 0.25rem;
30
+ font-size: smaller;
31
+ translate: -50%;
32
+ left: 50%;
33
+ }
34
+
35
+ .message-container {
36
+ height: 6rem;
37
+ overflow-y: scroll;
38
+ padding-inline: 0.5rem;
39
+ margin-bottom: 0.25rem;
40
+ }
41
+
42
+ .message-container::-webkit-scrollbar {
43
+ width: 2.5px;
44
+ }
45
+
46
+ .message-container::-webkit-scrollbar-track {
47
+ background: gray;
48
+ }
49
+
50
+ .message-container::-webkit-scrollbar-thumb {
51
+ background-color: white;
52
+ }
53
+
54
+ .heading {
55
+ text-align: center;
56
+ font-weight: bold;
57
+ padding-inline: 0.5rem;
58
+ }
59
+
60
+ .input-container {
61
+ display: flex;
62
+ column-gap: 0.25rem;
63
+ padding-inline: 0.5rem;
64
+ padding-block: 0.4rem;
65
+ }
66
+
67
+ .input {
68
+ background-color: black;
69
+ border: none;
70
+ outline: 1px solid rgba(255, 255, 255, 0.8);
71
+ border-radius: 0.25rem;
72
+ padding-inline: 0.25rem;
73
+ }
74
+
75
+ .input:focus {
76
+ outline: 2px solid white;
77
+ }
package/build/index.d.ts CHANGED
@@ -1,11 +1,34 @@
1
- import React from 'react';
1
+ import React, { CSSProperties, DetailedHTMLProps, HTMLAttributes, ReactNode, RefObject } from 'react';
2
2
  import { PeerOptions } from 'peerjs';
3
- type Id = string | number;
3
+ import './index.css';
4
+ type Message = {
5
+ id: string;
6
+ text: string;
7
+ };
8
+ type ChildrenOptions = {
9
+ notification?: boolean;
10
+ messages?: Message[];
11
+ addMessage?: (message: Message) => void;
12
+ dialogRef?: RefObject<HTMLDialogElement>;
13
+ audio?: boolean;
14
+ setAudio?: (audio: boolean) => void;
15
+ };
16
+ type DialogPosition = 'left' | 'center' | 'up';
17
+ type DialogOptions = {
18
+ position: DialogPosition;
19
+ style: CSSProperties;
20
+ };
4
21
  type Props = {
5
- peerId: Id;
6
- remotePeerId: Id;
22
+ name?: string;
23
+ peerId: string;
24
+ remotePeerId: string;
25
+ text?: boolean;
26
+ voice?: boolean;
7
27
  peerOptions?: PeerOptions;
28
+ dialogOptions?: DialogOptions;
8
29
  onError?: () => void;
9
- };
10
- export default function Chat({ peerId, remotePeerId, peerOptions, onError }: Props): React.JSX.Element;
30
+ children?: ReactNode | ((childrenOptions: ChildrenOptions) => ReactNode);
31
+ } & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
32
+ export default function Chat({ name, peerId, remotePeerId, peerOptions, text, voice, dialogOptions, onError, children, ...props }: Props): React.JSX.Element;
33
+ export declare const cleanStorage: () => void;
11
34
  export {};
package/build/index.js CHANGED
@@ -7,18 +7,69 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
+ var __rest = (this && this.__rest) || function (s, e) {
11
+ var t = {};
12
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
13
+ t[p] = s[p];
14
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
15
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
16
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
17
+ t[p[i]] = s[p[i]];
18
+ }
19
+ return t;
20
+ };
10
21
  import React, { useEffect, useRef, useState } from 'react';
11
- import useStorage from './useStorage';
12
- import { BsFillMicFill, BsFillMicMuteFill } from './icons';
13
- export default function Chat({ peerId, remotePeerId, peerOptions, onError = () => console.error("Can not access microphone!") }) {
22
+ import useStorage, { removeStorage } from './storage';
23
+ import { BiSolidMessageDetail, BiSolidMessageX, BsFillMicFill, BsFillMicMuteFill, GrSend } from './icons';
24
+ import './index.css';
25
+ export default function Chat(_a) {
26
+ var { name, peerId, remotePeerId, peerOptions, text = true, voice = true, dialogOptions, onError = () => console.error("Can not access microphone!"), children } = _a, props = __rest(_a, ["name", "peerId", "remotePeerId", "peerOptions", "text", "voice", "dialogOptions", "onError", "children"]);
14
27
  const [peer, setPeer] = useState();
28
+ const [opponentName, setOpponentName] = useState();
29
+ const [notification, setNotification] = useState(false);
30
+ const [messages, setMessages] = useStorage('rpc-messages', [], { save: true });
31
+ const connRef = useRef();
32
+ const [dialog, setDialog] = useState(false);
33
+ const dialogRef = useRef(null);
34
+ const containerRef = useRef(null);
35
+ const inputRef = useRef(null);
15
36
  const [audio, setAudio] = useStorage('rpc-audio', false, { local: true, save: true });
16
37
  const streamRef = useRef(null);
17
38
  const localStream = useRef();
39
+ remotePeerId = `rpc-${remotePeerId}`;
18
40
  const handleRemoteStream = (remoteStream) => streamRef.current.srcObject = remoteStream;
41
+ function addMessage(message, send = false) {
42
+ var _a, _b;
43
+ setMessages(old => old.concat(message));
44
+ if (send)
45
+ (_a = connRef.current) === null || _a === void 0 ? void 0 : _a.send({ type: 'message', message });
46
+ else if (!((_b = dialogRef.current) === null || _b === void 0 ? void 0 : _b.open))
47
+ setNotification(true);
48
+ }
49
+ function handleConnection(conn, setName = false) {
50
+ if (setName)
51
+ setOpponentName(conn.metadata || 'Anonymous User');
52
+ connRef.current = conn;
53
+ conn.on('open', () => {
54
+ conn.send({ type: 'messages', messages });
55
+ conn.on('data', ({ type, message, messages }) => {
56
+ if (type === 'message')
57
+ addMessage(message);
58
+ else if (type === 'messages') {
59
+ setMessages(old => {
60
+ if (messages.length > old.length)
61
+ return messages;
62
+ return old;
63
+ });
64
+ }
65
+ });
66
+ });
67
+ }
19
68
  useEffect(() => {
20
- if (!audio)
69
+ if (!text && !audio) {
70
+ setPeer(undefined);
21
71
  return;
72
+ }
22
73
  (function loadPeer() {
23
74
  return __awaiter(this, void 0, void 0, function* () {
24
75
  const Peer = (yield import('peerjs')).default;
@@ -32,37 +83,97 @@ export default function Chat({ peerId, remotePeerId, peerOptions, onError = () =
32
83
  return;
33
84
  let call;
34
85
  peer.on('open', () => {
35
- const getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
36
- getUserMedia({
37
- video: false,
38
- audio: {
39
- autoGainControl: false,
40
- noiseSuppression: true,
41
- echoCancellation: true
42
- }
43
- }, (stream) => {
44
- localStream.current = stream;
45
- call = peer.call(`rpc-${remotePeerId}`, stream);
46
- call.on('stream', handleRemoteStream);
47
- call.on('close', call.removeAllListeners);
48
- peer.on('call', (e) => {
49
- call = e;
50
- call.answer(stream);
86
+ if (text)
87
+ handleConnection(peer.connect(remotePeerId, { metadata: name }), true);
88
+ if (audio) {
89
+ const getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
90
+ getUserMedia({
91
+ video: false,
92
+ audio: {
93
+ autoGainControl: false,
94
+ noiseSuppression: true,
95
+ echoCancellation: true
96
+ }
97
+ }, (stream) => {
98
+ localStream.current = stream;
99
+ call = peer.call(remotePeerId, stream);
51
100
  call.on('stream', handleRemoteStream);
52
101
  call.on('close', call.removeAllListeners);
53
- });
54
- }, onError);
102
+ peer.on('call', e => {
103
+ call = e;
104
+ call.answer(stream);
105
+ call.on('stream', handleRemoteStream);
106
+ call.on('close', call.removeAllListeners);
107
+ });
108
+ }, onError);
109
+ }
55
110
  });
111
+ peer.on('connection', handleConnection);
56
112
  return () => {
57
- var _a;
113
+ var _a, _b, _c;
58
114
  (_a = localStream.current) === null || _a === void 0 ? void 0 : _a.getTracks().forEach(track => track.stop());
115
+ (_b = connRef.current) === null || _b === void 0 ? void 0 : _b.removeAllListeners();
116
+ (_c = connRef.current) === null || _c === void 0 ? void 0 : _c.close();
59
117
  call === null || call === void 0 ? void 0 : call.removeAllListeners();
60
118
  call === null || call === void 0 ? void 0 : call.close();
61
119
  peer.removeAllListeners();
62
120
  peer.destroy();
63
121
  };
64
122
  }, [peer]);
65
- return React.createElement("button", { onClick: () => setAudio(audio => !audio) }, audio ? React.createElement(React.Fragment, null,
66
- React.createElement("audio", { ref: streamRef, autoPlay: true, className: 'hidden' }),
67
- React.createElement(BsFillMicFill, { title: "Turn mic off" })) : React.createElement(BsFillMicMuteFill, { title: "Turn mic on" }));
123
+ useEffect(() => {
124
+ var _a, _b;
125
+ if (dialog)
126
+ (_a = dialogRef.current) === null || _a === void 0 ? void 0 : _a.show();
127
+ else
128
+ (_b = dialogRef.current) === null || _b === void 0 ? void 0 : _b.close();
129
+ }, [dialog]);
130
+ useEffect(() => {
131
+ const container = containerRef.current;
132
+ if (container)
133
+ container.scrollTop = container.scrollHeight;
134
+ }, [dialog, opponentName, messages]);
135
+ return <div className='main' {...props}>
136
+ {typeof children === 'function' ? children({ notification, messages, addMessage, dialogRef, audio, setAudio }) : <>
137
+ {text && <div>
138
+ {dialog ? <BiSolidMessageX onClick={() => setDialog(false)}/>
139
+ : <div className='notification'>
140
+ <BiSolidMessageDetail onClick={() => {
141
+ setNotification(false);
142
+ setDialog(true);
143
+ }}/>
144
+ {notification && <span className='badge'/>}
145
+ </div>}
146
+ <dialog ref={dialogRef} className={`${dialog ? 'dialog' : ''} position-${(dialogOptions === null || dialogOptions === void 0 ? void 0 : dialogOptions.position) || 'center'}`} style={dialogOptions === null || dialogOptions === void 0 ? void 0 : dialogOptions.style}>
147
+ <div className='heading'>Chat</div>
148
+ <hr />
149
+ <div>
150
+ <div ref={containerRef} className='message-container'>
151
+ {opponentName && messages.map(({ id, text }, i) => <div key={i}>
152
+ <strong>{id === peerId ? 'You' : opponentName}: </strong>
153
+ <span>{text}</span>
154
+ </div>)}
155
+ </div>
156
+ <hr />
157
+ <form className='input-container' onSubmit={e => {
158
+ var _a;
159
+ e.preventDefault();
160
+ const text = (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.value;
161
+ if (text) {
162
+ inputRef.current.value = '';
163
+ addMessage({ id: peerId, text }, true);
164
+ }
165
+ }}>
166
+ <input ref={inputRef} className='input' placeholder='Enter a message'/>
167
+ <button type='submit'><GrSend /></button>
168
+ </form>
169
+ </div>
170
+ </dialog>
171
+ </div>}
172
+ {voice && <div>
173
+ {audio ? <BsFillMicFill title="Turn mic off" onClick={() => setAudio(false)}/> : <BsFillMicMuteFill title="Turn mic on" onClick={() => setAudio(true)}/>}
174
+ </div>}
175
+ </>}
176
+ {voice && audio && <audio ref={streamRef} autoPlay style={{ display: 'none' }}/>}
177
+ </div>;
68
178
  }
179
+ export const cleanStorage = () => removeStorage('rpc-messages');
@@ -1,3 +1,4 @@
1
+ export declare const removeStorage: (key: string, local?: boolean) => void;
1
2
  export default function useStorage<Value>(key: string, initialValue: Value, { local, save }?: {
2
3
  local?: boolean | undefined;
3
4
  save?: boolean | undefined;
@@ -1,6 +1,6 @@
1
1
  import { useState } from "react";
2
2
  const setStorage = (key, value, local = false) => (local ? localStorage : sessionStorage).setItem(key, JSON.stringify(value));
3
- const removeStorage = (key, local = false) => (local ? localStorage : sessionStorage).removeItem(key);
3
+ export const removeStorage = (key, local = false) => (local ? localStorage : sessionStorage).removeItem(key);
4
4
  const getStorage = (key, fallbackValue, local = false) => {
5
5
  if (typeof window === "undefined")
6
6
  return fallbackValue;
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "react-peer-chat",
3
- "version": "0.1.11",
3
+ "version": "0.2.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",
7
7
  "scripts": {
8
- "build": "npm i && tsc"
8
+ "copy": "copy .\\src\\*.css build",
9
+ "build": "npm i && tsc && npm run copy"
9
10
  },
10
11
  "repository": {
11
12
  "type": "git",
@@ -38,4 +39,4 @@
38
39
  "dependencies": {
39
40
  "peerjs": "^1.5.0"
40
41
  }
41
- }
42
+ }