react-peer-chat 0.8.5 → 0.9.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 +39 -35
- package/dist/chunks/{chunk-NXKKI6WY.js → chunk-JJPIWKLG.js} +2 -32
- package/dist/chunks/chunk-LNEKYYG7.js +33 -0
- package/dist/chunks/chunk-MBDO7IOK.js +229 -0
- package/dist/chunks/chunk-N4TBKQIR.js +81 -0
- package/dist/chunks/chunk-ZYFPSCFE.js +22 -0
- package/dist/components.d.ts +7 -0
- package/dist/components.js +5 -0
- package/dist/hooks.d.ts +11 -0
- package/dist/hooks.js +3 -0
- package/dist/icons.js +2 -1
- package/dist/index.d.ts +5 -10
- package/dist/index.js +5 -298
- package/dist/lib/storage.d.ts +6 -0
- package/dist/lib/storage.js +2 -0
- package/dist/types.d.ts +6 -14
- package/package.json +19 -16
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
|
|
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 {
|
|
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(
|
|
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"
|
|
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
|
|
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. |
|
|
@@ -317,52 +318,55 @@ that are listed in [useChat Hook API Reference](#usechat-hook-1) along with the
|
|
|
317
318
|
| `props` | [`DivProps`](#divprops) | No | - | Props to customize the `<Chat>` component. |
|
|
318
319
|
| `children` | [`Children`](#children) | No | - | See [usage with FaC](#full-customization) |
|
|
319
320
|
|
|
320
|
-
|
|
321
|
+
## Types
|
|
321
322
|
|
|
322
|
-
|
|
323
|
+
### Children
|
|
323
324
|
|
|
324
325
|
```typescript
|
|
325
|
-
import { ReactNode } from "react";
|
|
326
|
-
|
|
326
|
+
import type { ReactNode, SetStateAction } from "react";
|
|
327
|
+
|
|
328
|
+
type RemotePeers = Record<string, string>;
|
|
327
329
|
type Message = {
|
|
328
330
|
id: string;
|
|
329
331
|
text: string;
|
|
330
332
|
};
|
|
331
333
|
type ChildrenOptions = {
|
|
332
|
-
remotePeers
|
|
333
|
-
messages
|
|
334
|
-
sendMessage
|
|
335
|
-
audio
|
|
336
|
-
setAudio
|
|
334
|
+
remotePeers: RemotePeers;
|
|
335
|
+
messages: Message[];
|
|
336
|
+
sendMessage: (message: Message) => void;
|
|
337
|
+
audio: boolean;
|
|
338
|
+
setAudio: (value: SetStateAction<boolean>) => void;
|
|
337
339
|
};
|
|
338
340
|
type Children = (childrenOptions: ChildrenOptions) => ReactNode;
|
|
339
341
|
```
|
|
340
342
|
|
|
341
|
-
|
|
343
|
+
### DialogOptions
|
|
342
344
|
|
|
343
345
|
```typescript
|
|
344
|
-
import { CSSProperties } from "react";
|
|
346
|
+
import type { CSSProperties } from "react";
|
|
347
|
+
|
|
345
348
|
type DialogPosition = "left" | "center" | "right";
|
|
346
349
|
type DialogOptions = {
|
|
347
|
-
position
|
|
348
|
-
style
|
|
350
|
+
position?: DialogPosition;
|
|
351
|
+
style?: CSSProperties;
|
|
349
352
|
};
|
|
350
353
|
```
|
|
351
354
|
|
|
352
|
-
|
|
355
|
+
### DivProps
|
|
353
356
|
|
|
354
357
|
```typescript
|
|
355
|
-
import { DetailedHTMLProps, HTMLAttributes } from "react";
|
|
358
|
+
import type { DetailedHTMLProps, HTMLAttributes } from "react";
|
|
359
|
+
|
|
356
360
|
type DivProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
|
|
357
361
|
```
|
|
358
362
|
|
|
359
|
-
|
|
363
|
+
### ErrorHandler
|
|
360
364
|
|
|
361
365
|
```typescript
|
|
362
366
|
type ErrorHandler = () => void;
|
|
363
367
|
```
|
|
364
368
|
|
|
365
|
-
|
|
369
|
+
### MessageEventHandler
|
|
366
370
|
|
|
367
371
|
```typescript
|
|
368
372
|
type Message = {
|
|
@@ -372,10 +376,10 @@ type Message = {
|
|
|
372
376
|
type MessageEventHandler = (message: Message) => void;
|
|
373
377
|
```
|
|
374
378
|
|
|
375
|
-
|
|
379
|
+
### PeerOptions
|
|
376
380
|
|
|
377
381
|
```typescript
|
|
378
|
-
import { PeerOptions } from "peerjs";
|
|
382
|
+
import type { PeerOptions } from "peerjs";
|
|
379
383
|
```
|
|
380
384
|
|
|
381
385
|
## License
|
|
@@ -1,36 +1,6 @@
|
|
|
1
|
+
import { __spreadValues } from './chunk-LNEKYYG7.js';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __defProps = Object.defineProperties;
|
|
5
|
-
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
6
|
-
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
9
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
10
|
-
var __spreadValues = (a, b) => {
|
|
11
|
-
for (var prop in b || (b = {}))
|
|
12
|
-
if (__hasOwnProp.call(b, prop))
|
|
13
|
-
__defNormalProp(a, prop, b[prop]);
|
|
14
|
-
if (__getOwnPropSymbols)
|
|
15
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
16
|
-
if (__propIsEnum.call(b, prop))
|
|
17
|
-
__defNormalProp(a, prop, b[prop]);
|
|
18
|
-
}
|
|
19
|
-
return a;
|
|
20
|
-
};
|
|
21
|
-
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
22
|
-
var __objRest = (source, exclude) => {
|
|
23
|
-
var target = {};
|
|
24
|
-
for (var prop in source)
|
|
25
|
-
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
26
|
-
target[prop] = source[prop];
|
|
27
|
-
if (source != null && __getOwnPropSymbols)
|
|
28
|
-
for (var prop of __getOwnPropSymbols(source)) {
|
|
29
|
-
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
30
|
-
target[prop] = source[prop];
|
|
31
|
-
}
|
|
32
|
-
return target;
|
|
33
|
-
};
|
|
34
4
|
function BiSolidMessageDetail(props) {
|
|
35
5
|
return /* @__PURE__ */ React.createElement("span", __spreadValues({ className: "rpc-icon-container" }, props), /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "1.25rem", height: "1.25rem" }, /* @__PURE__ */ 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" })));
|
|
36
6
|
}
|
|
@@ -55,4 +25,4 @@ function BsFillMicMuteFill(props) {
|
|
|
55
25
|
return /* @__PURE__ */ React.createElement("span", __spreadValues({ className: "rpc-icon-container" }, props), /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 16 16", fill: "currentColor", width: "1.25rem", height: "1.25rem" }, /* @__PURE__ */ 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" }), /* @__PURE__ */ 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" })));
|
|
56
26
|
}
|
|
57
27
|
|
|
58
|
-
export { BiSolidMessageDetail, BiSolidMessageX, BsFillMicFill, BsFillMicMuteFill, GrSend
|
|
28
|
+
export { BiSolidMessageDetail, BiSolidMessageX, BsFillMicFill, BsFillMicMuteFill, GrSend };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defProps = Object.defineProperties;
|
|
3
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
4
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
7
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
8
|
+
var __spreadValues = (a, b) => {
|
|
9
|
+
for (var prop in b || (b = {}))
|
|
10
|
+
if (__hasOwnProp.call(b, prop))
|
|
11
|
+
__defNormalProp(a, prop, b[prop]);
|
|
12
|
+
if (__getOwnPropSymbols)
|
|
13
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
14
|
+
if (__propIsEnum.call(b, prop))
|
|
15
|
+
__defNormalProp(a, prop, b[prop]);
|
|
16
|
+
}
|
|
17
|
+
return a;
|
|
18
|
+
};
|
|
19
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
20
|
+
var __objRest = (source, exclude) => {
|
|
21
|
+
var target = {};
|
|
22
|
+
for (var prop in source)
|
|
23
|
+
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
24
|
+
target[prop] = source[prop];
|
|
25
|
+
if (source != null && __getOwnPropSymbols)
|
|
26
|
+
for (var prop of __getOwnPropSymbols(source)) {
|
|
27
|
+
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
28
|
+
target[prop] = source[prop];
|
|
29
|
+
}
|
|
30
|
+
return target;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export { __objRest, __spreadProps, __spreadValues };
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { getStorage, setStorage } from './chunk-ZYFPSCFE.js';
|
|
2
|
+
import { __spreadValues, __spreadProps } from './chunk-LNEKYYG7.js';
|
|
3
|
+
import { useState, useRef, useMemo, useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
// src/constants.ts
|
|
6
|
+
var turnAccounts = [
|
|
7
|
+
{ username: "70061a377b51f3a3d01c11e3", credential: "lHV4NYJ5Rfl5JNa9" },
|
|
8
|
+
{ username: "13b19eb65bbf6e9f96d64b72", credential: "7R9P/+7y7Q516Etv" },
|
|
9
|
+
{ username: "3469603f5cdc7ca4a1e891ae", credential: "/jMyLSDbbcgqpVQv" },
|
|
10
|
+
{ username: "a7926f4dcc4a688d41f89752", credential: "ZYM8jFYeb8bQkL+N" },
|
|
11
|
+
{ username: "0be25ab7f61d9d733ba94809", credential: "hiiSwWVch+ftt3SX" },
|
|
12
|
+
{ username: "3c25ba948daeab04f9b66187", credential: "FQB3GQwd27Y0dPeK" }
|
|
13
|
+
];
|
|
14
|
+
var defaults = {
|
|
15
|
+
config: {
|
|
16
|
+
iceServers: [
|
|
17
|
+
{
|
|
18
|
+
urls: ["stun:stun.l.google.com:19302", "stun:stun.relay.metered.ca:80"]
|
|
19
|
+
}
|
|
20
|
+
].concat(
|
|
21
|
+
turnAccounts.map((account) => __spreadValues({
|
|
22
|
+
urls: [
|
|
23
|
+
"turn:standard.relay.metered.ca:80",
|
|
24
|
+
"turn:standard.relay.metered.ca:80?transport=tcp",
|
|
25
|
+
"turn:standard.relay.metered.ca:443",
|
|
26
|
+
"turns:standard.relay.metered.ca:443?transport=tcp"
|
|
27
|
+
]
|
|
28
|
+
}, account))
|
|
29
|
+
)
|
|
30
|
+
},
|
|
31
|
+
peerOptions: {},
|
|
32
|
+
remotePeerId: []
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// src/lib/connection.ts
|
|
36
|
+
function closeConnection(conn) {
|
|
37
|
+
conn.removeAllListeners();
|
|
38
|
+
conn.close();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/lib/utils.ts
|
|
42
|
+
var addPrefix = (str) => `rpc-${str}`;
|
|
43
|
+
|
|
44
|
+
// src/lib/react.ts
|
|
45
|
+
function isSetStateFunction(v) {
|
|
46
|
+
return typeof v === "function";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/hooks.ts
|
|
50
|
+
var { config: defaultConfig, peerOptions: defaultPeerOptions, remotePeerId: defaultRemotePeerId } = defaults;
|
|
51
|
+
function useChat({
|
|
52
|
+
peerId,
|
|
53
|
+
name = "Anonymous User",
|
|
54
|
+
remotePeerId = defaultRemotePeerId,
|
|
55
|
+
peerOptions = defaultPeerOptions,
|
|
56
|
+
text = true,
|
|
57
|
+
recoverChat = false,
|
|
58
|
+
audio: allowed = true,
|
|
59
|
+
onError = () => alert("Browser not supported! Try some other browser."),
|
|
60
|
+
onMicError = () => alert("Microphone not accessible!"),
|
|
61
|
+
onMessageSent,
|
|
62
|
+
onMessageReceived
|
|
63
|
+
}) {
|
|
64
|
+
const [peer, setPeer] = useState();
|
|
65
|
+
const [audio, setAudio] = useAudio(allowed);
|
|
66
|
+
const connRef = useRef({});
|
|
67
|
+
const callsRef = useRef({});
|
|
68
|
+
const audioContextRef = useRef(null);
|
|
69
|
+
const mixerRef = useRef(null);
|
|
70
|
+
const sourceNodesRef = useRef({});
|
|
71
|
+
const [messages, setMessages, addMessage] = useMessages();
|
|
72
|
+
const [remotePeers, setRemotePeers] = useStorage("rpc-remote-peer", {});
|
|
73
|
+
const { completePeerId, completeRemotePeerIds } = useMemo(() => {
|
|
74
|
+
const remotePeerIds = Array.isArray(remotePeerId) ? remotePeerId : [remotePeerId];
|
|
75
|
+
return { completePeerId: addPrefix(peerId), completeRemotePeerIds: remotePeerIds.map(addPrefix) };
|
|
76
|
+
}, [peerId]);
|
|
77
|
+
function handleCall(call) {
|
|
78
|
+
const id = call.peer;
|
|
79
|
+
call.on("stream", (stream) => handleRemoteStream(id, stream));
|
|
80
|
+
call.on("close", () => {
|
|
81
|
+
call.removeAllListeners();
|
|
82
|
+
removePeerAudio(id);
|
|
83
|
+
delete callsRef.current[id];
|
|
84
|
+
});
|
|
85
|
+
callsRef.current[id] = call;
|
|
86
|
+
}
|
|
87
|
+
function handleConnection(conn) {
|
|
88
|
+
connRef.current[conn.peer] = conn;
|
|
89
|
+
conn.on("open", () => {
|
|
90
|
+
conn.on("data", ({ message, messages: messages2, remotePeerName, type }) => {
|
|
91
|
+
if (type === "message") receiveMessage(message);
|
|
92
|
+
else if (type === "init") {
|
|
93
|
+
setRemotePeers((prev) => __spreadProps(__spreadValues({}, prev), { [conn.peer]: remotePeerName }));
|
|
94
|
+
if (recoverChat) setMessages((old) => messages2.length > old.length ? messages2 : old);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
conn.send({ type: "init", remotePeerName: name, messages });
|
|
98
|
+
});
|
|
99
|
+
conn.on("close", conn.removeAllListeners);
|
|
100
|
+
}
|
|
101
|
+
function handleError() {
|
|
102
|
+
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;
|
|
115
|
+
}
|
|
116
|
+
function receiveMessage(message) {
|
|
117
|
+
addMessage(message);
|
|
118
|
+
onMessageReceived == null ? void 0 : onMessageReceived(message);
|
|
119
|
+
}
|
|
120
|
+
function removePeerAudio(peerId2) {
|
|
121
|
+
if (!sourceNodesRef.current[peerId2]) return;
|
|
122
|
+
sourceNodesRef.current[peerId2].disconnect();
|
|
123
|
+
delete sourceNodesRef.current[peerId2];
|
|
124
|
+
}
|
|
125
|
+
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);
|
|
129
|
+
}
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!text && !audio) return;
|
|
132
|
+
import('peerjs').then(
|
|
133
|
+
({
|
|
134
|
+
Peer,
|
|
135
|
+
util: {
|
|
136
|
+
supports: { audioVideo, data }
|
|
137
|
+
}
|
|
138
|
+
}) => {
|
|
139
|
+
if (!data || !audioVideo) return onError();
|
|
140
|
+
const peer2 = new Peer(completePeerId, __spreadValues({ config: defaultConfig }, peerOptions));
|
|
141
|
+
peer2.on("connection", handleConnection);
|
|
142
|
+
setPeer(peer2);
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
return () => {
|
|
146
|
+
setPeer((prev) => {
|
|
147
|
+
prev == null ? void 0 : prev.removeAllListeners();
|
|
148
|
+
prev == null ? void 0 : prev.destroy();
|
|
149
|
+
return void 0;
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
}, [completePeerId]);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (!text || !peer) return;
|
|
155
|
+
const handleOpen = () => completeRemotePeerIds.forEach((id) => handleConnection(peer.connect(id)));
|
|
156
|
+
if (peer.open) handleOpen();
|
|
157
|
+
else peer.once("open", handleOpen);
|
|
158
|
+
return () => {
|
|
159
|
+
Object.values(connRef.current).forEach(closeConnection);
|
|
160
|
+
connRef.current = {};
|
|
161
|
+
};
|
|
162
|
+
}, [text, peer]);
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (!audio || !peer) return;
|
|
165
|
+
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);
|
|
189
|
+
if (peer.open) setupAudio();
|
|
190
|
+
else peer.once("open", setupAudio);
|
|
191
|
+
return () => {
|
|
192
|
+
var _a;
|
|
193
|
+
localStream == null ? void 0 : localStream.getTracks().forEach((track) => track.stop());
|
|
194
|
+
Object.values(callsRef.current).forEach(closeConnection);
|
|
195
|
+
callsRef.current = {};
|
|
196
|
+
Object.keys(sourceNodesRef.current).forEach(removePeerAudio);
|
|
197
|
+
(_a = audioContextRef.current) == null ? void 0 : _a.close();
|
|
198
|
+
audioContextRef.current = null;
|
|
199
|
+
mixerRef.current = null;
|
|
200
|
+
};
|
|
201
|
+
}, [audio, peer]);
|
|
202
|
+
return { peerId: completePeerId, remotePeers, messages, sendMessage, audio, setAudio };
|
|
203
|
+
}
|
|
204
|
+
function useMessages() {
|
|
205
|
+
const [messages, setMessages] = useStorage("rpc-messages", []);
|
|
206
|
+
const addMessage = (message) => setMessages((prev) => prev.concat(message));
|
|
207
|
+
return [messages, setMessages, addMessage];
|
|
208
|
+
}
|
|
209
|
+
function useStorage(key, initialValue, local = false) {
|
|
210
|
+
const [storedValue, setStoredValue] = useState(() => {
|
|
211
|
+
if (typeof window === "undefined") return initialValue;
|
|
212
|
+
return getStorage(key, initialValue, local);
|
|
213
|
+
});
|
|
214
|
+
const setValue = (value) => {
|
|
215
|
+
setStoredValue((prev) => {
|
|
216
|
+
const next = isSetStateFunction(value) ? value(prev) : value;
|
|
217
|
+
setStorage(key, next, local);
|
|
218
|
+
return next;
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
return [storedValue, setValue];
|
|
222
|
+
}
|
|
223
|
+
function useAudio(allowed) {
|
|
224
|
+
const [audio, setAudio] = useStorage("rpc-audio", false, true);
|
|
225
|
+
const enabled = audio && allowed;
|
|
226
|
+
return [enabled, setAudio];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export { useAudio, useChat, useMessages, useStorage };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useChat } from './chunk-MBDO7IOK.js';
|
|
2
|
+
import { BiSolidMessageX, BiSolidMessageDetail, GrSend, BsFillMicFill, BsFillMicMuteFill } from './chunk-JJPIWKLG.js';
|
|
3
|
+
import { __objRest, __spreadValues } from './chunk-LNEKYYG7.js';
|
|
4
|
+
import React, { useRef, useState, useEffect } from 'react';
|
|
5
|
+
|
|
6
|
+
// src/styles.css
|
|
7
|
+
function injectStyle(css) {
|
|
8
|
+
if (typeof document === "undefined") return;
|
|
9
|
+
const head = document.head || document.getElementsByTagName("head")[0];
|
|
10
|
+
const style = document.createElement("style");
|
|
11
|
+
style.type = "text/css";
|
|
12
|
+
if (head.firstChild) {
|
|
13
|
+
head.insertBefore(style, head.firstChild);
|
|
14
|
+
} else {
|
|
15
|
+
head.appendChild(style);
|
|
16
|
+
}
|
|
17
|
+
if (style.styleSheet) {
|
|
18
|
+
style.styleSheet.cssText = css;
|
|
19
|
+
} else {
|
|
20
|
+
style.appendChild(document.createTextNode(css));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
injectStyle('.rpc-font {\n font-family:\n "Trebuchet MS",\n "Lucida Sans Unicode",\n "Lucida Grande",\n "Lucida Sans",\n Arial,\n sans-serif;\n}\n.rpc-main {\n display: flex;\n align-items: center;\n column-gap: 0.5rem;\n}\n.rpc-dialog-container {\n position: relative;\n}\n.rpc-notification {\n position: relative;\n}\n.rpc-notification .rpc-badge {\n background-color: red;\n position: absolute;\n top: 0;\n right: 0;\n width: 6px;\n aspect-ratio: 1;\n border-radius: 100%;\n}\n.rpc-dialog {\n position: absolute;\n background-color: black;\n color: white;\n padding: 0.4rem 0 0.25rem 0;\n border-radius: 0.4rem;\n font-size: small;\n}\n.rpc-position-left {\n left: 0.6rem;\n translate: -100%;\n}\n.rpc-position-center {\n left: 0.5rem;\n translate: -50%;\n}\n.rpc-position-right {\n left: 0.3rem;\n}\n.rpc-hr {\n margin: 0.25rem 0;\n border-color: rgba(255, 255, 255, 0.7);\n}\n.rpc-message-container {\n height: 7rem;\n overflow-y: scroll;\n padding-inline: 0.5rem;\n margin-bottom: 0.25rem;\n}\n.rpc-message-container::-webkit-scrollbar {\n width: 2.5px;\n}\n.rpc-message-container::-webkit-scrollbar-track {\n background: gray;\n}\n.rpc-message-container::-webkit-scrollbar-thumb {\n background-color: white;\n}\n.rpc-heading {\n text-align: center;\n font-size: medium;\n font-weight: bold;\n padding-inline: 0.5rem;\n}\n.rpc-input-container {\n display: flex;\n width: 15rem;\n max-width: 90vw;\n column-gap: 0.25rem;\n padding: 0.25rem 0.5rem;\n}\n.rpc-input {\n color: white;\n width: 100%;\n background-color: black;\n border: none;\n outline: 1px solid rgba(255, 255, 255, 0.8);\n border-radius: 0.25rem;\n padding: 0.3rem 0.25rem;\n}\n.rpc-input::placeholder {\n color: rgba(255, 255, 255, 0.7);\n}\n.rpc-input:focus {\n outline: 2px solid white;\n}\n.rpc-button {\n all: unset;\n display: flex;\n}\n.rpc-icon-container {\n display: flex;\n align-items: center;\n}\n.rpc-invert {\n filter: invert(100%);\n}\n');
|
|
24
|
+
|
|
25
|
+
// src/components.tsx
|
|
26
|
+
function Chat(_a) {
|
|
27
|
+
var _b = _a, { text = true, audio = true, onMessageReceived, dialogOptions, props = {}, children } = _b, hookProps = __objRest(_b, ["text", "audio", "onMessageReceived", "dialogOptions", "props", "children"]);
|
|
28
|
+
const _a2 = useChat(__spreadValues({
|
|
29
|
+
text,
|
|
30
|
+
audio,
|
|
31
|
+
onMessageReceived: modifiedOnMessageReceived
|
|
32
|
+
}, hookProps)), { peerId } = _a2, childrenOptions = __objRest(_a2, ["peerId"]);
|
|
33
|
+
const { remotePeers, messages, sendMessage, audio: audioEnabled, setAudio } = childrenOptions;
|
|
34
|
+
const containerRef = useRef(null);
|
|
35
|
+
const [dialog, setDialog] = useState(false);
|
|
36
|
+
const dialogRef = useRef(null);
|
|
37
|
+
const inputRef = useRef(null);
|
|
38
|
+
const [notification, setNotification] = useState(false);
|
|
39
|
+
function modifiedOnMessageReceived(message) {
|
|
40
|
+
var _a3;
|
|
41
|
+
if (!((_a3 = dialogRef.current) == null ? void 0 : _a3.open)) setNotification(true);
|
|
42
|
+
onMessageReceived == null ? void 0 : onMessageReceived(message);
|
|
43
|
+
}
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
var _a3, _b2;
|
|
46
|
+
if (dialog) (_a3 = dialogRef.current) == null ? void 0 : _a3.show();
|
|
47
|
+
else (_b2 = dialogRef.current) == null ? void 0 : _b2.close();
|
|
48
|
+
}, [dialog]);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const container = containerRef.current;
|
|
51
|
+
if (container) container.scrollTop = container.scrollHeight;
|
|
52
|
+
}, [dialog, remotePeers, messages]);
|
|
53
|
+
return /* @__PURE__ */ React.createElement("div", __spreadValues({ className: "rpc-main rpc-font" }, props), typeof children === "function" ? children(childrenOptions) : /* @__PURE__ */ React.createElement(React.Fragment, null, text && /* @__PURE__ */ React.createElement("div", { className: "rpc-dialog-container" }, dialog ? /* @__PURE__ */ React.createElement(BiSolidMessageX, { title: "Close chat", onClick: () => setDialog(false) }) : /* @__PURE__ */ React.createElement("div", { className: "rpc-notification" }, /* @__PURE__ */ React.createElement(
|
|
54
|
+
BiSolidMessageDetail,
|
|
55
|
+
{
|
|
56
|
+
title: "Open chat",
|
|
57
|
+
onClick: () => {
|
|
58
|
+
setNotification(false);
|
|
59
|
+
setDialog(true);
|
|
60
|
+
}
|
|
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(
|
|
63
|
+
"form",
|
|
64
|
+
{
|
|
65
|
+
className: "rpc-input-container",
|
|
66
|
+
onSubmit: (e) => {
|
|
67
|
+
var _a3;
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
const text2 = (_a3 = inputRef.current) == null ? void 0 : _a3.value;
|
|
70
|
+
if (text2) {
|
|
71
|
+
inputRef.current.value = "";
|
|
72
|
+
sendMessage({ id: peerId, text: text2 });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
/* @__PURE__ */ React.createElement("input", { ref: inputRef, className: "rpc-input rpc-font", placeholder: "Enter a message" }),
|
|
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" }))));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { Chat };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/lib/storage.ts
|
|
2
|
+
var getStorageInstance = (local = true) => local ? localStorage : sessionStorage;
|
|
3
|
+
var removeStorage = (key, local = true) => getStorageInstance(local).removeItem(key);
|
|
4
|
+
var clearChat = () => {
|
|
5
|
+
removeStorage("rpc-remote-peer");
|
|
6
|
+
removeStorage("rpc-messages");
|
|
7
|
+
};
|
|
8
|
+
var setStorage = (key, value, local = true) => getStorageInstance(local).setItem(key, JSON.stringify(value));
|
|
9
|
+
function getStorage(key, fallbackValue, local = true) {
|
|
10
|
+
const value = getStorageInstance(local).getItem(key);
|
|
11
|
+
if (value) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(value);
|
|
14
|
+
} catch (e) {
|
|
15
|
+
removeStorage(key, local);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (fallbackValue !== void 0) setStorage(key, fallbackValue, local);
|
|
19
|
+
return fallbackValue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { clearChat, getStorage, removeStorage, setStorage };
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { SetStateAction } from 'react';
|
|
2
|
+
import { UseChatProps, UseChatReturn, Message } from './types.js';
|
|
3
|
+
import 'peerjs';
|
|
4
|
+
|
|
5
|
+
declare function useChat({ peerId, name, remotePeerId, peerOptions, text, recoverChat, audio: allowed, onError, onMicError, onMessageSent, onMessageReceived, }: UseChatProps): UseChatReturn;
|
|
6
|
+
declare function useMessages(): readonly [Message[], (value: SetStateAction<Message[]>) => void, (message: Message) => void];
|
|
7
|
+
declare function useStorage<T>(key: string, initialValue: T, local?: boolean): readonly [T, (value: SetStateAction<T>) => void];
|
|
8
|
+
declare function useStorage<T>(key: string, initialValue?: T, local?: boolean): readonly [T | undefined, (value: SetStateAction<T | undefined>) => void];
|
|
9
|
+
declare function useAudio(allowed: boolean): readonly [boolean, (value: SetStateAction<boolean>) => void];
|
|
10
|
+
|
|
11
|
+
export { useAudio, useChat, useMessages, useStorage };
|
package/dist/hooks.js
ADDED
package/dist/icons.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { BiSolidMessageDetail, BiSolidMessageX, BsFillMicFill, BsFillMicMuteFill, GrSend } from './chunks/chunk-
|
|
1
|
+
export { BiSolidMessageDetail, BiSolidMessageX, BsFillMicFill, BsFillMicMuteFill, GrSend } from './chunks/chunk-JJPIWKLG.js';
|
|
2
|
+
import './chunks/chunk-LNEKYYG7.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export { default } from './components.js';
|
|
2
|
+
export { useChat } from './hooks.js';
|
|
3
|
+
export { clearChat } from './lib/storage.js';
|
|
4
|
+
import 'react';
|
|
5
|
+
import './types.js';
|
|
3
6
|
import 'peerjs';
|
|
4
|
-
|
|
5
|
-
declare function useChat({ peerId, name, remotePeerId, peerOptions, text, recoverChat, audio: allowed, onError, onMicError, onMessageSent, onMessageReceived, }: UseChatProps): UseChatReturn;
|
|
6
|
-
|
|
7
|
-
declare const clearChat: () => void;
|
|
8
|
-
|
|
9
|
-
declare function Chat({ text, audio, onMessageReceived, dialogOptions, props, children, ...hookProps }: ChatProps): React.JSX.Element;
|
|
10
|
-
|
|
11
|
-
export { clearChat, Chat as default, useChat };
|
package/dist/index.js
CHANGED
|
@@ -1,298 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
{ username: "70061a377b51f3a3d01c11e3", credential: "lHV4NYJ5Rfl5JNa9" },
|
|
7
|
-
{ username: "13b19eb65bbf6e9f96d64b72", credential: "7R9P/+7y7Q516Etv" },
|
|
8
|
-
{ username: "3469603f5cdc7ca4a1e891ae", credential: "/jMyLSDbbcgqpVQv" },
|
|
9
|
-
{ username: "a7926f4dcc4a688d41f89752", credential: "ZYM8jFYeb8bQkL+N" },
|
|
10
|
-
{ username: "0be25ab7f61d9d733ba94809", credential: "hiiSwWVch+ftt3SX" },
|
|
11
|
-
{ username: "3c25ba948daeab04f9b66187", credential: "FQB3GQwd27Y0dPeK" }
|
|
12
|
-
];
|
|
13
|
-
var defaults = {
|
|
14
|
-
config: {
|
|
15
|
-
iceServers: [
|
|
16
|
-
{
|
|
17
|
-
urls: ["stun:stun.l.google.com:19302", "stun:stun.relay.metered.ca:80"]
|
|
18
|
-
}
|
|
19
|
-
].concat(
|
|
20
|
-
turnAccounts.map((account) => __spreadValues({
|
|
21
|
-
urls: [
|
|
22
|
-
"turn:standard.relay.metered.ca:80",
|
|
23
|
-
"turn:standard.relay.metered.ca:80?transport=tcp",
|
|
24
|
-
"turn:standard.relay.metered.ca:443",
|
|
25
|
-
"turns:standard.relay.metered.ca:443?transport=tcp"
|
|
26
|
-
]
|
|
27
|
-
}, account))
|
|
28
|
-
)
|
|
29
|
-
},
|
|
30
|
-
peerOptions: {},
|
|
31
|
-
remotePeerId: []
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// src/lib/connection.ts
|
|
35
|
-
function closeConnection(conn) {
|
|
36
|
-
conn.removeAllListeners();
|
|
37
|
-
conn.close();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// src/lib/storage.ts
|
|
41
|
-
var getStorageInstance = (local = true) => local ? localStorage : sessionStorage;
|
|
42
|
-
var removeStorage = (key, local = true) => getStorageInstance(local).removeItem(key);
|
|
43
|
-
var clearChat = () => {
|
|
44
|
-
removeStorage("rpc-remote-peer");
|
|
45
|
-
removeStorage("rpc-messages");
|
|
46
|
-
};
|
|
47
|
-
var setStorage = (key, value, local = true) => getStorageInstance(local).setItem(key, JSON.stringify(value));
|
|
48
|
-
function getStorage(key, fallbackValue, local = true) {
|
|
49
|
-
const value = getStorageInstance(local).getItem(key);
|
|
50
|
-
if (value) {
|
|
51
|
-
try {
|
|
52
|
-
return JSON.parse(value);
|
|
53
|
-
} catch (e) {
|
|
54
|
-
removeStorage(key, local);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (fallbackValue !== void 0) setStorage(key, fallbackValue, local);
|
|
58
|
-
return fallbackValue;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// src/lib/utils.ts
|
|
62
|
-
var addPrefix = (str) => `rpc-${str}`;
|
|
63
|
-
|
|
64
|
-
// src/lib/react.ts
|
|
65
|
-
function isSetStateFunction(v) {
|
|
66
|
-
return typeof v === "function";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// src/hooks.ts
|
|
70
|
-
var { config: defaultConfig, peerOptions: defaultPeerOptions, remotePeerId: defaultRemotePeerId } = defaults;
|
|
71
|
-
function useChat({
|
|
72
|
-
peerId,
|
|
73
|
-
name = "Anonymous User",
|
|
74
|
-
remotePeerId = defaultRemotePeerId,
|
|
75
|
-
peerOptions = defaultPeerOptions,
|
|
76
|
-
text = true,
|
|
77
|
-
recoverChat = false,
|
|
78
|
-
audio: allowed = true,
|
|
79
|
-
onError = () => alert("Browser not supported! Try some other browser."),
|
|
80
|
-
onMicError = () => alert("Microphone not accessible!"),
|
|
81
|
-
onMessageSent,
|
|
82
|
-
onMessageReceived
|
|
83
|
-
}) {
|
|
84
|
-
const [peer, setPeer] = useState();
|
|
85
|
-
const [audio, setAudio] = useAudio(allowed);
|
|
86
|
-
const connRef = useRef({});
|
|
87
|
-
const localStreamRef = useRef(null);
|
|
88
|
-
const audioStreamRef = useRef(null);
|
|
89
|
-
const callsRef = useRef({});
|
|
90
|
-
const [messages, setMessages, addMessage] = useMessages();
|
|
91
|
-
const [remotePeers, setRemotePeers] = useStorage("rpc-remote-peer", {});
|
|
92
|
-
const { completePeerId, completeRemotePeerIds } = useMemo(() => {
|
|
93
|
-
const remotePeerIds = Array.isArray(remotePeerId) ? remotePeerId : [remotePeerId];
|
|
94
|
-
return { completePeerId: addPrefix(peerId), completeRemotePeerIds: remotePeerIds.map(addPrefix) };
|
|
95
|
-
}, [peerId]);
|
|
96
|
-
function handleConnection(conn) {
|
|
97
|
-
connRef.current[conn.peer] = conn;
|
|
98
|
-
conn.on("open", () => {
|
|
99
|
-
conn.on("data", ({ message, messages: messages2, remotePeerName, type }) => {
|
|
100
|
-
if (type === "message") receiveMessage(message);
|
|
101
|
-
else if (type === "init") {
|
|
102
|
-
setRemotePeers((prev) => __spreadProps(__spreadValues({}, prev), { [conn.peer]: remotePeerName }));
|
|
103
|
-
if (recoverChat) setMessages((old) => messages2.length > old.length ? messages2 : old);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
conn.send({ type: "init", remotePeerName: name, messages });
|
|
107
|
-
});
|
|
108
|
-
conn.on("close", conn.removeAllListeners);
|
|
109
|
-
}
|
|
110
|
-
function handleError() {
|
|
111
|
-
setAudio(false);
|
|
112
|
-
onMicError();
|
|
113
|
-
}
|
|
114
|
-
function handleRemoteStream(remoteStream) {
|
|
115
|
-
if (audioStreamRef.current) audioStreamRef.current.srcObject = remoteStream;
|
|
116
|
-
}
|
|
117
|
-
function receiveMessage(message) {
|
|
118
|
-
addMessage(message);
|
|
119
|
-
onMessageReceived == null ? void 0 : onMessageReceived(message);
|
|
120
|
-
}
|
|
121
|
-
function sendMessage(message) {
|
|
122
|
-
addMessage(message);
|
|
123
|
-
Object.values(connRef.current).forEach((conn) => conn.send({ type: "message", message }));
|
|
124
|
-
onMessageSent == null ? void 0 : onMessageSent(message);
|
|
125
|
-
}
|
|
126
|
-
useEffect(() => {
|
|
127
|
-
if (!text && !audio) return;
|
|
128
|
-
import('peerjs').then(
|
|
129
|
-
({
|
|
130
|
-
Peer,
|
|
131
|
-
util: {
|
|
132
|
-
supports: { audioVideo, data }
|
|
133
|
-
}
|
|
134
|
-
}) => {
|
|
135
|
-
if (!data || !audioVideo) return onError();
|
|
136
|
-
const peer2 = new Peer(completePeerId, __spreadValues({ config: defaultConfig }, peerOptions));
|
|
137
|
-
peer2.on("connection", handleConnection);
|
|
138
|
-
setPeer(peer2);
|
|
139
|
-
}
|
|
140
|
-
);
|
|
141
|
-
return () => {
|
|
142
|
-
setPeer((prev) => {
|
|
143
|
-
prev == null ? void 0 : prev.removeAllListeners();
|
|
144
|
-
prev == null ? void 0 : prev.destroy();
|
|
145
|
-
return void 0;
|
|
146
|
-
});
|
|
147
|
-
};
|
|
148
|
-
}, [completePeerId]);
|
|
149
|
-
useEffect(() => {
|
|
150
|
-
if (!text || !peer) return;
|
|
151
|
-
const handleOpen = () => completeRemotePeerIds.forEach((id) => handleConnection(peer.connect(id)));
|
|
152
|
-
if (peer.open) handleOpen();
|
|
153
|
-
else peer.once("open", handleOpen);
|
|
154
|
-
return () => {
|
|
155
|
-
Object.values(connRef.current).forEach(closeConnection);
|
|
156
|
-
connRef.current = {};
|
|
157
|
-
};
|
|
158
|
-
}, [text, peer]);
|
|
159
|
-
useEffect(() => {
|
|
160
|
-
if (!audio || !peer) return;
|
|
161
|
-
const setupAudio = () => navigator.mediaDevices.getUserMedia({
|
|
162
|
-
video: false,
|
|
163
|
-
audio: {
|
|
164
|
-
autoGainControl: false,
|
|
165
|
-
// Disable automatic gain control
|
|
166
|
-
noiseSuppression: true,
|
|
167
|
-
// Enable noise suppression
|
|
168
|
-
echoCancellation: true
|
|
169
|
-
// Enable echo cancellation
|
|
170
|
-
}
|
|
171
|
-
}).then((stream) => {
|
|
172
|
-
localStreamRef.current = stream;
|
|
173
|
-
completeRemotePeerIds.forEach((id) => {
|
|
174
|
-
const call = peer.call(id, stream);
|
|
175
|
-
call.on("stream", handleRemoteStream);
|
|
176
|
-
call.on("close", call.removeAllListeners);
|
|
177
|
-
callsRef.current[id] = call;
|
|
178
|
-
});
|
|
179
|
-
peer.on("call", (call) => {
|
|
180
|
-
call.answer(stream);
|
|
181
|
-
call.on("stream", handleRemoteStream);
|
|
182
|
-
call.on("close", call.removeAllListeners);
|
|
183
|
-
callsRef.current[call.peer] = call;
|
|
184
|
-
});
|
|
185
|
-
}).catch(handleError);
|
|
186
|
-
if (peer.open) setupAudio();
|
|
187
|
-
else peer.once("open", setupAudio);
|
|
188
|
-
return () => {
|
|
189
|
-
var _a;
|
|
190
|
-
(_a = localStreamRef.current) == null ? void 0 : _a.getTracks().forEach((track) => track.stop());
|
|
191
|
-
localStreamRef.current = null;
|
|
192
|
-
Object.values(callsRef.current).forEach(closeConnection);
|
|
193
|
-
callsRef.current = {};
|
|
194
|
-
};
|
|
195
|
-
}, [audio, peer]);
|
|
196
|
-
return { peerId: completePeerId, audioStreamRef, remotePeers, messages, sendMessage, audio, setAudio };
|
|
197
|
-
}
|
|
198
|
-
function useMessages() {
|
|
199
|
-
const [messages, setMessages] = useStorage("rpc-messages", []);
|
|
200
|
-
const addMessage = (message) => setMessages((prev) => prev.concat(message));
|
|
201
|
-
return [messages, setMessages, addMessage];
|
|
202
|
-
}
|
|
203
|
-
function useStorage(key, initialValue, local = false) {
|
|
204
|
-
const [storedValue, setStoredValue] = useState(() => {
|
|
205
|
-
if (typeof window === "undefined") return initialValue;
|
|
206
|
-
return getStorage(key, initialValue, local);
|
|
207
|
-
});
|
|
208
|
-
const setValue = (value) => {
|
|
209
|
-
setStoredValue((prev) => {
|
|
210
|
-
const next = isSetStateFunction(value) ? value(prev) : value;
|
|
211
|
-
setStorage(key, next, local);
|
|
212
|
-
return next;
|
|
213
|
-
});
|
|
214
|
-
};
|
|
215
|
-
return [storedValue, setValue];
|
|
216
|
-
}
|
|
217
|
-
function useAudio(allowed) {
|
|
218
|
-
const [audio, setAudio] = useStorage("rpc-audio", false, true);
|
|
219
|
-
const enabled = audio && allowed;
|
|
220
|
-
return [enabled, setAudio];
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// src/styles.css
|
|
224
|
-
function injectStyle(css) {
|
|
225
|
-
if (typeof document === "undefined") return;
|
|
226
|
-
const head = document.head || document.getElementsByTagName("head")[0];
|
|
227
|
-
const style = document.createElement("style");
|
|
228
|
-
style.type = "text/css";
|
|
229
|
-
if (head.firstChild) {
|
|
230
|
-
head.insertBefore(style, head.firstChild);
|
|
231
|
-
} else {
|
|
232
|
-
head.appendChild(style);
|
|
233
|
-
}
|
|
234
|
-
if (style.styleSheet) {
|
|
235
|
-
style.styleSheet.cssText = css;
|
|
236
|
-
} else {
|
|
237
|
-
style.appendChild(document.createTextNode(css));
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
injectStyle('.rpc-font {\n font-family:\n "Trebuchet MS",\n "Lucida Sans Unicode",\n "Lucida Grande",\n "Lucida Sans",\n Arial,\n sans-serif;\n}\n.rpc-main {\n display: flex;\n align-items: center;\n column-gap: 0.5rem;\n}\n.rpc-dialog-container {\n position: relative;\n}\n.rpc-notification {\n position: relative;\n}\n.rpc-notification .rpc-badge {\n background-color: red;\n position: absolute;\n top: 0;\n right: 0;\n width: 6px;\n aspect-ratio: 1;\n border-radius: 100%;\n}\n.rpc-dialog {\n position: absolute;\n background-color: black;\n color: white;\n padding: 0.4rem 0 0.25rem 0;\n border-radius: 0.4rem;\n font-size: small;\n}\n.rpc-position-left {\n left: 0.6rem;\n translate: -100%;\n}\n.rpc-position-center {\n left: 0.5rem;\n translate: -50%;\n}\n.rpc-position-right {\n left: 0.3rem;\n}\n.rpc-hr {\n margin: 0.25rem 0;\n border-color: rgba(255, 255, 255, 0.7);\n}\n.rpc-message-container {\n height: 7rem;\n overflow-y: scroll;\n padding-inline: 0.5rem;\n margin-bottom: 0.25rem;\n}\n.rpc-message-container::-webkit-scrollbar {\n width: 2.5px;\n}\n.rpc-message-container::-webkit-scrollbar-track {\n background: gray;\n}\n.rpc-message-container::-webkit-scrollbar-thumb {\n background-color: white;\n}\n.rpc-heading {\n text-align: center;\n font-size: medium;\n font-weight: bold;\n padding-inline: 0.5rem;\n}\n.rpc-input-container {\n display: flex;\n width: 15rem;\n max-width: 90vw;\n column-gap: 0.25rem;\n padding: 0.25rem 0.5rem;\n}\n.rpc-input {\n color: white;\n width: 100%;\n background-color: black;\n border: none;\n outline: 1px solid rgba(255, 255, 255, 0.8);\n border-radius: 0.25rem;\n padding: 0.3rem 0.25rem;\n}\n.rpc-input::placeholder {\n color: rgba(255, 255, 255, 0.7);\n}\n.rpc-input:focus {\n outline: 2px solid white;\n}\n.rpc-button {\n all: unset;\n display: flex;\n}\n.rpc-icon-container {\n display: flex;\n align-items: center;\n}\n.rpc-invert {\n filter: invert(100%);\n}\n');
|
|
241
|
-
|
|
242
|
-
// src/index.tsx
|
|
243
|
-
function Chat(_a) {
|
|
244
|
-
var _b = _a, { text = true, audio = true, onMessageReceived, dialogOptions, props = {}, children } = _b, hookProps = __objRest(_b, ["text", "audio", "onMessageReceived", "dialogOptions", "props", "children"]);
|
|
245
|
-
const _a2 = useChat(__spreadValues({
|
|
246
|
-
text,
|
|
247
|
-
audio,
|
|
248
|
-
onMessageReceived: modifiedOnMessageReceived
|
|
249
|
-
}, hookProps)), { peerId, audioStreamRef } = _a2, childrenOptions = __objRest(_a2, ["peerId", "audioStreamRef"]);
|
|
250
|
-
const { remotePeers, messages, sendMessage, audio: audioEnabled, setAudio } = childrenOptions;
|
|
251
|
-
const containerRef = useRef(null);
|
|
252
|
-
const [dialog, setDialog] = useState(false);
|
|
253
|
-
const dialogRef = useRef(null);
|
|
254
|
-
const inputRef = useRef(null);
|
|
255
|
-
const [notification, setNotification] = useState(false);
|
|
256
|
-
function modifiedOnMessageReceived(message) {
|
|
257
|
-
var _a3;
|
|
258
|
-
if (!((_a3 = dialogRef.current) == null ? void 0 : _a3.open)) setNotification(true);
|
|
259
|
-
onMessageReceived == null ? void 0 : onMessageReceived(message);
|
|
260
|
-
}
|
|
261
|
-
useEffect(() => {
|
|
262
|
-
var _a3, _b2;
|
|
263
|
-
if (dialog) (_a3 = dialogRef.current) == null ? void 0 : _a3.show();
|
|
264
|
-
else (_b2 = dialogRef.current) == null ? void 0 : _b2.close();
|
|
265
|
-
}, [dialog]);
|
|
266
|
-
useEffect(() => {
|
|
267
|
-
const container = containerRef.current;
|
|
268
|
-
if (container) container.scrollTop = container.scrollHeight;
|
|
269
|
-
}, [dialog, remotePeers, messages]);
|
|
270
|
-
return /* @__PURE__ */ React.createElement("div", __spreadValues({ className: "rpc-main rpc-font" }, props), typeof children === "function" ? children(childrenOptions) : /* @__PURE__ */ React.createElement(React.Fragment, null, text && /* @__PURE__ */ React.createElement("div", { className: "rpc-dialog-container" }, dialog ? /* @__PURE__ */ React.createElement(BiSolidMessageX, { title: "Close chat", onClick: () => setDialog(false) }) : /* @__PURE__ */ React.createElement("div", { className: "rpc-notification" }, /* @__PURE__ */ React.createElement(
|
|
271
|
-
BiSolidMessageDetail,
|
|
272
|
-
{
|
|
273
|
-
title: "Open chat",
|
|
274
|
-
onClick: () => {
|
|
275
|
-
setNotification(false);
|
|
276
|
-
setDialog(true);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
), 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(
|
|
280
|
-
"form",
|
|
281
|
-
{
|
|
282
|
-
className: "rpc-input-container",
|
|
283
|
-
onSubmit: (e) => {
|
|
284
|
-
var _a3;
|
|
285
|
-
e.preventDefault();
|
|
286
|
-
const text2 = (_a3 = inputRef.current) == null ? void 0 : _a3.value;
|
|
287
|
-
if (text2) {
|
|
288
|
-
inputRef.current.value = "";
|
|
289
|
-
sendMessage({ id: peerId, text: text2 });
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
},
|
|
293
|
-
/* @__PURE__ */ React.createElement("input", { ref: inputRef, className: "rpc-input rpc-font", placeholder: "Enter a message" }),
|
|
294
|
-
/* @__PURE__ */ React.createElement("button", { type: "submit", className: "rpc-button" }, /* @__PURE__ */ React.createElement(GrSend, { title: "Send message" }))
|
|
295
|
-
)))), 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" } }));
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
export { clearChat, Chat as default, useChat };
|
|
1
|
+
export { Chat as default } from './chunks/chunk-N4TBKQIR.js';
|
|
2
|
+
export { useChat } from './chunks/chunk-MBDO7IOK.js';
|
|
3
|
+
import './chunks/chunk-JJPIWKLG.js';
|
|
4
|
+
export { clearChat } from './chunks/chunk-ZYFPSCFE.js';
|
|
5
|
+
import './chunks/chunk-LNEKYYG7.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
declare const removeStorage: (key: string, local?: boolean) => void;
|
|
2
|
+
declare const clearChat: () => void;
|
|
3
|
+
declare const setStorage: (key: string, value: unknown, local?: boolean) => void;
|
|
4
|
+
declare function getStorage<T>(key: string, fallbackValue?: T, local?: boolean): T | undefined;
|
|
5
|
+
|
|
6
|
+
export { clearChat, getStorage, removeStorage, setStorage };
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PeerOptions, DataConnection, MediaConnection } from 'peerjs';
|
|
2
2
|
export { PeerOptions } from 'peerjs';
|
|
3
|
-
import { DetailedHTMLProps, HTMLAttributes,
|
|
3
|
+
import { CSSProperties, DetailedHTMLProps, HTMLAttributes, SetStateAction, ReactNode } from 'react';
|
|
4
4
|
|
|
5
5
|
type Connection = DataConnection | MediaConnection;
|
|
6
6
|
type ErrorHandler = () => void;
|
|
@@ -24,15 +24,16 @@ type UseChatProps = {
|
|
|
24
24
|
onMessageSent?: MessageEventHandler;
|
|
25
25
|
onMessageReceived?: MessageEventHandler;
|
|
26
26
|
};
|
|
27
|
-
type
|
|
28
|
-
peerId: string;
|
|
29
|
-
audioStreamRef: RefObject<HTMLMediaElement | null>;
|
|
27
|
+
type ChildrenOptions = {
|
|
30
28
|
remotePeers: RemotePeers;
|
|
31
29
|
messages: Message[];
|
|
32
30
|
sendMessage: (message: Message) => void;
|
|
33
31
|
audio: boolean;
|
|
34
32
|
setAudio: (value: SetStateAction<boolean>) => void;
|
|
35
33
|
};
|
|
34
|
+
type UseChatReturn = ChildrenOptions & {
|
|
35
|
+
peerId: string;
|
|
36
|
+
};
|
|
36
37
|
type IconProps = DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>;
|
|
37
38
|
type ChatProps = UseChatProps & {
|
|
38
39
|
dialogOptions?: DialogOptions;
|
|
@@ -40,21 +41,12 @@ type ChatProps = UseChatProps & {
|
|
|
40
41
|
children?: Children;
|
|
41
42
|
};
|
|
42
43
|
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
44
|
type DialogOptions = {
|
|
51
45
|
position?: DialogPosition;
|
|
52
46
|
style?: CSSProperties;
|
|
53
47
|
};
|
|
54
48
|
type DialogPosition = "left" | "center" | "right";
|
|
55
49
|
type DivProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
|
|
56
|
-
type RemotePeers =
|
|
57
|
-
[id: string]: string;
|
|
58
|
-
};
|
|
50
|
+
type RemotePeers = Record<string, string>;
|
|
59
51
|
|
|
60
52
|
export type { ChatProps, Children, ChildrenOptions, Connection, DialogOptions, DialogPosition, DivProps, ErrorHandler, IconProps, 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.
|
|
3
|
+
"version": "0.9.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>",
|
|
@@ -14,54 +14,57 @@
|
|
|
14
14
|
"url": "https://github.com/SahilAggarwal2004/react-peer-chat/issues"
|
|
15
15
|
},
|
|
16
16
|
"type": "module",
|
|
17
|
-
"files": [
|
|
18
|
-
"dist"
|
|
19
|
-
],
|
|
20
17
|
"exports": {
|
|
21
18
|
".": "./dist/index.js",
|
|
22
19
|
"./icons": "./dist/icons.js",
|
|
23
20
|
"./types": "./dist/types.js"
|
|
24
21
|
},
|
|
25
22
|
"main": "dist/index.js",
|
|
26
|
-
"
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
27
26
|
"sideEffects": [
|
|
28
27
|
"**/*.css"
|
|
29
28
|
],
|
|
29
|
+
"types": "dist/index.d.ts",
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"peerjs": "^1.5.5"
|
|
32
32
|
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
35
|
+
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
36
|
+
},
|
|
33
37
|
"devDependencies": {
|
|
34
38
|
"@release-it/conventional-changelog": "^10.0.4",
|
|
35
39
|
"@types/react": "^19.2.7",
|
|
40
|
+
"prettier-package-json": "^2.8.0",
|
|
36
41
|
"release-it": "^19.2.2",
|
|
37
42
|
"tsup": "^8.5.1",
|
|
38
43
|
"typescript": "^5.9.3"
|
|
39
44
|
},
|
|
40
|
-
"peerDependencies": {
|
|
41
|
-
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
42
|
-
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
43
|
-
},
|
|
44
45
|
"keywords": [
|
|
45
|
-
"
|
|
46
|
+
"audio-chat",
|
|
46
47
|
"chat",
|
|
47
48
|
"component",
|
|
48
|
-
"peer",
|
|
49
|
-
"peerjs",
|
|
50
49
|
"p2p",
|
|
50
|
+
"p2p-chat",
|
|
51
|
+
"peer",
|
|
51
52
|
"peer-to-peer",
|
|
52
|
-
"
|
|
53
|
+
"peerjs",
|
|
54
|
+
"react",
|
|
53
55
|
"react-peer-chat",
|
|
54
|
-
"typescript",
|
|
55
|
-
"p2p-chat",
|
|
56
56
|
"text-chat",
|
|
57
|
+
"typescript",
|
|
57
58
|
"voice-chat",
|
|
58
|
-
"
|
|
59
|
+
"webrtc"
|
|
59
60
|
],
|
|
60
61
|
"scripts": {
|
|
61
62
|
"build": "pnpm i && pnpm run compile",
|
|
62
63
|
"compile": "tsup",
|
|
63
64
|
"dev": "tsup --watch",
|
|
64
65
|
"dry-release": "release-it --ci --dry-run",
|
|
66
|
+
"prettier": "prettier-package-json --write package.json",
|
|
67
|
+
"pub": "pnpm login && pnpm publish",
|
|
65
68
|
"release": "release-it --ci"
|
|
66
69
|
}
|
|
67
70
|
}
|