react-peer-chat 0.11.10 → 0.12.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 +5 -0
- package/dist/{icons.d.ts → icons.d.mts} +5 -5
- package/dist/icons.mjs +62 -0
- package/dist/index.d.mts +34 -0
- package/dist/index.mjs +440 -0
- package/dist/style.css +117 -0
- package/dist/types-CwAnkJpd.d.mts +59 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +1 -0
- package/package.json +12 -11
- package/dist/chunks/chunk-6FW6VHDM.js +0 -81
- package/dist/chunks/chunk-FZ4QVG4I.js +0 -53
- package/dist/chunks/chunk-MRYWIJDZ.js +0 -46
- package/dist/chunks/chunk-QIPTWGEX.js +0 -28
- package/dist/chunks/chunk-R74TCSLB.js +0 -310
- package/dist/components.d.ts +0 -7
- package/dist/components.js +0 -5
- package/dist/hooks.d.ts +0 -11
- package/dist/hooks.js +0 -3
- package/dist/icons.js +0 -2
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -5
- package/dist/lib/storage.d.ts +0 -11
- package/dist/lib/storage.js +0 -2
- package/dist/types.d.ts +0 -59
- package/dist/types.js +0 -1
package/README.md
CHANGED
|
@@ -47,6 +47,7 @@ The default export of `react-peer-chat` is the `<Chat>` component, which offers
|
|
|
47
47
|
```jsx
|
|
48
48
|
import React from "react";
|
|
49
49
|
import Chat, { clearChat } from "react-peer-chat";
|
|
50
|
+
import "react-peer-chat/style.css";
|
|
50
51
|
|
|
51
52
|
export default function App() {
|
|
52
53
|
return (
|
|
@@ -64,6 +65,7 @@ export default function App() {
|
|
|
64
65
|
```jsx
|
|
65
66
|
import React from "react";
|
|
66
67
|
import Chat, { clearChat } from "react-peer-chat";
|
|
68
|
+
import "react-peer-chat/style.css";
|
|
67
69
|
|
|
68
70
|
export default function App() {
|
|
69
71
|
return (
|
|
@@ -89,6 +91,7 @@ Use the props provided by the `<Chat>` component for customization.
|
|
|
89
91
|
```jsx
|
|
90
92
|
import React from "react";
|
|
91
93
|
import Chat from "react-peer-chat";
|
|
94
|
+
import "react-peer-chat/style.css";
|
|
92
95
|
|
|
93
96
|
export default function App() {
|
|
94
97
|
return (
|
|
@@ -141,6 +144,7 @@ You can also provide custom ICE servers to avoid connectivity issues if the free
|
|
|
141
144
|
```jsx
|
|
142
145
|
import React from "react";
|
|
143
146
|
import Chat from "react-peer-chat";
|
|
147
|
+
import "react-peer-chat/style.css";
|
|
144
148
|
|
|
145
149
|
export default function App() {
|
|
146
150
|
return (
|
|
@@ -176,6 +180,7 @@ The `useChat` hook is ideal when you want to completely redesign the Chat UI.
|
|
|
176
180
|
import React, { useEffect, useRef, useState } from "react";
|
|
177
181
|
import { clearChat, useChat } from "react-peer-chat";
|
|
178
182
|
import { BiSolidMessageDetail, BiSolidMessageX, BsFillMicFill, BsFillMicMuteFill, GrSend } from "react-peer-chat/icons";
|
|
183
|
+
import "react-peer-chat/style.css";
|
|
179
184
|
|
|
180
185
|
function Chat({ text = true, audio = true, onMessageReceived, dialogOptions, props = {}, children, ...hookProps }) {
|
|
181
186
|
const {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import 'peerjs';
|
|
1
|
+
import { l as IconProps } from "./types-CwAnkJpd.mjs";
|
|
2
|
+
import React from "react";
|
|
4
3
|
|
|
4
|
+
//#region src/icons.d.ts
|
|
5
5
|
declare function BiSolidMessageDetail(props: IconProps): React.JSX.Element;
|
|
6
6
|
declare function BiSolidMessageX(props: IconProps): React.JSX.Element;
|
|
7
7
|
declare function GrSend(props: IconProps): React.JSX.Element;
|
|
8
8
|
declare function BsFillMicFill(props: IconProps): React.JSX.Element;
|
|
9
9
|
declare function BsFillMicMuteFill(props: IconProps): React.JSX.Element;
|
|
10
|
-
|
|
11
|
-
export { BiSolidMessageDetail, BiSolidMessageX, BsFillMicFill, BsFillMicMuteFill, GrSend };
|
|
10
|
+
//#endregion
|
|
11
|
+
export { BiSolidMessageDetail, BiSolidMessageX, BsFillMicFill, BsFillMicMuteFill, GrSend };
|
package/dist/icons.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
//#region src/icons.tsx
|
|
3
|
+
function BiSolidMessageDetail(props) {
|
|
4
|
+
return /* @__PURE__ */ React.createElement("span", {
|
|
5
|
+
className: "rpc-icon-container",
|
|
6
|
+
...props
|
|
7
|
+
}, /* @__PURE__ */ React.createElement("svg", {
|
|
8
|
+
viewBox: "0 0 24 24",
|
|
9
|
+
width: "1.25rem",
|
|
10
|
+
height: "1.25rem"
|
|
11
|
+
}, /* @__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" })));
|
|
12
|
+
}
|
|
13
|
+
function BiSolidMessageX(props) {
|
|
14
|
+
return /* @__PURE__ */ React.createElement("span", {
|
|
15
|
+
className: "rpc-icon-container",
|
|
16
|
+
...props
|
|
17
|
+
}, /* @__PURE__ */ React.createElement("svg", {
|
|
18
|
+
viewBox: "0 0 24 24",
|
|
19
|
+
width: "1.25rem",
|
|
20
|
+
height: "1.25rem"
|
|
21
|
+
}, /* @__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-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" })));
|
|
22
|
+
}
|
|
23
|
+
function GrSend(props) {
|
|
24
|
+
return /* @__PURE__ */ React.createElement("span", {
|
|
25
|
+
className: "rpc-icon-container",
|
|
26
|
+
...props
|
|
27
|
+
}, /* @__PURE__ */ React.createElement("svg", {
|
|
28
|
+
viewBox: "0 0 24 24",
|
|
29
|
+
width: "1.25rem",
|
|
30
|
+
height: "1.25rem",
|
|
31
|
+
className: "rpc-invert"
|
|
32
|
+
}, /* @__PURE__ */ React.createElement("path", {
|
|
33
|
+
fill: "none",
|
|
34
|
+
stroke: "#000",
|
|
35
|
+
strokeWidth: 2,
|
|
36
|
+
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"
|
|
37
|
+
})));
|
|
38
|
+
}
|
|
39
|
+
function BsFillMicFill(props) {
|
|
40
|
+
return /* @__PURE__ */ React.createElement("span", {
|
|
41
|
+
className: "rpc-icon-container",
|
|
42
|
+
...props
|
|
43
|
+
}, /* @__PURE__ */ React.createElement("svg", {
|
|
44
|
+
viewBox: "0 0 16 16",
|
|
45
|
+
fill: "currentColor",
|
|
46
|
+
width: "1.25rem",
|
|
47
|
+
height: "1.25rem"
|
|
48
|
+
}, /* @__PURE__ */ React.createElement("path", { d: "M5 3a3 3 0 0 1 6 0v5a3 3 0 0 1-6 0V3z" }), /* @__PURE__ */ 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" })));
|
|
49
|
+
}
|
|
50
|
+
function BsFillMicMuteFill(props) {
|
|
51
|
+
return /* @__PURE__ */ React.createElement("span", {
|
|
52
|
+
className: "rpc-icon-container",
|
|
53
|
+
...props
|
|
54
|
+
}, /* @__PURE__ */ React.createElement("svg", {
|
|
55
|
+
viewBox: "0 0 16 16",
|
|
56
|
+
fill: "currentColor",
|
|
57
|
+
width: "1.25rem",
|
|
58
|
+
height: "1.25rem"
|
|
59
|
+
}, /* @__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" })));
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { BiSolidMessageDetail, BiSolidMessageX, BsFillMicFill, BsFillMicMuteFill, GrSend };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { b as UseChatReturn, t as ChatProps, y as UseChatProps } from "./types-CwAnkJpd.mjs";
|
|
2
|
+
import React, { SetStateAction } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/components.d.ts
|
|
5
|
+
declare function Chat({
|
|
6
|
+
text,
|
|
7
|
+
audio,
|
|
8
|
+
onMessageReceived,
|
|
9
|
+
dialogOptions,
|
|
10
|
+
props,
|
|
11
|
+
children,
|
|
12
|
+
...hookProps
|
|
13
|
+
}: ChatProps): React.JSX.Element;
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/hooks.d.ts
|
|
16
|
+
declare function useChat({
|
|
17
|
+
peerId,
|
|
18
|
+
name,
|
|
19
|
+
remotePeerId,
|
|
20
|
+
peerOptions,
|
|
21
|
+
text,
|
|
22
|
+
recoverChat,
|
|
23
|
+
audio: allowed,
|
|
24
|
+
onError,
|
|
25
|
+
onPeerError,
|
|
26
|
+
onNetworkError,
|
|
27
|
+
onMessageSent,
|
|
28
|
+
onMessageReceived
|
|
29
|
+
}: UseChatProps): UseChatReturn;
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/lib/storage.d.ts
|
|
32
|
+
declare function clearChat(): void;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { clearChat, Chat as default, useChat };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { BiSolidMessageDetail, BiSolidMessageX, BsFillMicFill, BsFillMicMuteFill, GrSend } from "./icons.mjs";
|
|
2
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
const defaults = {
|
|
4
|
+
config: { iceServers: [{ urls: ["stun:stun.l.google.com:19302", "stun:stun.relay.metered.ca:80"] }].concat([
|
|
5
|
+
{
|
|
6
|
+
username: "70061a377b51f3a3d01c11e3",
|
|
7
|
+
credential: "lHV4NYJ5Rfl5JNa9"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
username: "13b19eb65bbf6e9f96d64b72",
|
|
11
|
+
credential: "7R9P/+7y7Q516Etv"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
username: "3469603f5cdc7ca4a1e891ae",
|
|
15
|
+
credential: "/jMyLSDbbcgqpVQv"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
username: "a7926f4dcc4a688d41f89752",
|
|
19
|
+
credential: "ZYM8jFYeb8bQkL+N"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
username: "0be25ab7f61d9d733ba94809",
|
|
23
|
+
credential: "hiiSwWVch+ftt3SX"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
username: "3c25ba948daeab04f9b66187",
|
|
27
|
+
credential: "FQB3GQwd27Y0dPeK"
|
|
28
|
+
}
|
|
29
|
+
].map((account) => ({
|
|
30
|
+
urls: [
|
|
31
|
+
"turn:standard.relay.metered.ca:80",
|
|
32
|
+
"turn:standard.relay.metered.ca:80?transport=tcp",
|
|
33
|
+
"turn:standard.relay.metered.ca:443",
|
|
34
|
+
"turns:standard.relay.metered.ca:443?transport=tcp"
|
|
35
|
+
],
|
|
36
|
+
...account
|
|
37
|
+
}))) },
|
|
38
|
+
peerOptions: {},
|
|
39
|
+
remotePeerId: []
|
|
40
|
+
};
|
|
41
|
+
const iosRegex = /iPhone|iPad|iPod/i;
|
|
42
|
+
const mobileRegex = /Android|webOS|BlackBerry|IEMobile|Opera Mini/i;
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/lib/connection.ts
|
|
45
|
+
function closeConnection(conn) {
|
|
46
|
+
conn.removeAllListeners();
|
|
47
|
+
conn.close();
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/lib/react.ts
|
|
51
|
+
function isSetStateFunction(v) {
|
|
52
|
+
return typeof v === "function";
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/lib/storage.ts
|
|
56
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
57
|
+
function clearChat() {
|
|
58
|
+
removeStorage("rpc-remote-peer", false);
|
|
59
|
+
removeStorage("rpc-messages", false);
|
|
60
|
+
}
|
|
61
|
+
const getStorageInstance = (local) => local ? localStorage : sessionStorage;
|
|
62
|
+
const getNamespacedKey = (key, local) => `${local ? "local" : "session"}:${key}`;
|
|
63
|
+
function getStorage(key, local, fallbackValue) {
|
|
64
|
+
if (typeof window === "undefined") return fallbackValue;
|
|
65
|
+
const value = getStorageInstance(local).getItem(key);
|
|
66
|
+
if (value) try {
|
|
67
|
+
return JSON.parse(value);
|
|
68
|
+
} catch {
|
|
69
|
+
removeStorage(key, local);
|
|
70
|
+
}
|
|
71
|
+
if (fallbackValue !== void 0) setStorage(key, fallbackValue, local);
|
|
72
|
+
return fallbackValue;
|
|
73
|
+
}
|
|
74
|
+
function publish(key, local, value) {
|
|
75
|
+
const callbacks = listeners.get(getNamespacedKey(key, local));
|
|
76
|
+
if (callbacks) callbacks.forEach((callback) => callback(value));
|
|
77
|
+
}
|
|
78
|
+
function removeStorage(key, local) {
|
|
79
|
+
getStorageInstance(local).removeItem(key);
|
|
80
|
+
publish(key, local);
|
|
81
|
+
}
|
|
82
|
+
function setStorage(key, value, local) {
|
|
83
|
+
const next = isSetStateFunction(value) ? value(getStorage(key, local)) : value;
|
|
84
|
+
getStorageInstance(local).setItem(key, JSON.stringify(next));
|
|
85
|
+
publish(key, local, next);
|
|
86
|
+
}
|
|
87
|
+
function subscribeToStorage(key, local, callback) {
|
|
88
|
+
key = getNamespacedKey(key, local);
|
|
89
|
+
if (!listeners.has(key)) listeners.set(key, /* @__PURE__ */ new Set());
|
|
90
|
+
const set = listeners.get(key);
|
|
91
|
+
set.add(callback);
|
|
92
|
+
return () => {
|
|
93
|
+
set.delete(callback);
|
|
94
|
+
if (set.size === 0) listeners.delete(key);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/lib/utils.ts
|
|
99
|
+
const addPrefix = (str) => `rpc-${str}`;
|
|
100
|
+
function isMobile(iOS = true) {
|
|
101
|
+
let result = navigator.userAgentData?.mobile;
|
|
102
|
+
result ??= mobileRegex.test(navigator.userAgent) || iOS && iosRegex.test(navigator.userAgent);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/hooks.ts
|
|
107
|
+
const { config: defaultConfig, peerOptions: defaultPeerOptions, remotePeerId: defaultRemotePeerId } = defaults;
|
|
108
|
+
function useChat({ peerId, name = "Anonymous User", remotePeerId = defaultRemotePeerId, peerOptions = defaultPeerOptions, text = true, recoverChat = false, audio: allowed = true, onError = console.error, onPeerError = console.error, onNetworkError, onMessageSent, onMessageReceived }) {
|
|
109
|
+
const [peerEpoch, setPeerEpoch] = useState(0);
|
|
110
|
+
const [peerGeneration, setPeerGeneration] = useState(0);
|
|
111
|
+
const [audio, setAudio] = useAudio(allowed);
|
|
112
|
+
const peerRef = useRef(null);
|
|
113
|
+
const scheduleReconnectRef = useRef(null);
|
|
114
|
+
const connRef = useRef({});
|
|
115
|
+
const callsRef = useRef({});
|
|
116
|
+
const localStreamRef = useRef(null);
|
|
117
|
+
const audioContextRef = useRef(null);
|
|
118
|
+
const mixerRef = useRef(null);
|
|
119
|
+
const sourceNodesRef = useRef({});
|
|
120
|
+
const [messages, setMessages, addMessage] = useMessages();
|
|
121
|
+
const [remotePeers, setRemotePeers] = useStorage("rpc-remote-peer", {});
|
|
122
|
+
const { completePeerId, completeRemotePeerIds } = useMemo(() => {
|
|
123
|
+
const remotePeerIds = Array.isArray(remotePeerId) ? remotePeerId : [remotePeerId];
|
|
124
|
+
return {
|
|
125
|
+
completePeerId: addPrefix(peerId),
|
|
126
|
+
completeRemotePeerIds: remotePeerIds.map(addPrefix)
|
|
127
|
+
};
|
|
128
|
+
}, [peerId]);
|
|
129
|
+
function resetConnections(type = "all") {
|
|
130
|
+
switch (type) {
|
|
131
|
+
case "all":
|
|
132
|
+
resetConnections("data");
|
|
133
|
+
resetConnections("call");
|
|
134
|
+
break;
|
|
135
|
+
case "data":
|
|
136
|
+
Object.values(connRef.current).forEach(closeConnection);
|
|
137
|
+
connRef.current = {};
|
|
138
|
+
break;
|
|
139
|
+
case "call":
|
|
140
|
+
Object.values(callsRef.current).forEach(closeConnection);
|
|
141
|
+
Object.keys(sourceNodesRef.current).forEach(removePeerAudio);
|
|
142
|
+
callsRef.current = {};
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function handleConnection(conn) {
|
|
147
|
+
const peerId = conn.peer;
|
|
148
|
+
conn.on("open", () => {
|
|
149
|
+
connRef.current[peerId]?.close();
|
|
150
|
+
connRef.current[peerId] = conn;
|
|
151
|
+
conn.on("data", ({ type, message, messages, remotePeerName }) => {
|
|
152
|
+
switch (type) {
|
|
153
|
+
case "init":
|
|
154
|
+
setRemotePeers((prev) => ({
|
|
155
|
+
...prev,
|
|
156
|
+
[peerId]: remotePeerName
|
|
157
|
+
}));
|
|
158
|
+
if (recoverChat) setMessages((old) => messages.length > old.length ? messages : old);
|
|
159
|
+
break;
|
|
160
|
+
case "message":
|
|
161
|
+
receiveMessage(message);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
conn.send({
|
|
166
|
+
type: "init",
|
|
167
|
+
remotePeerName: name,
|
|
168
|
+
messages
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
conn.on("close", () => {
|
|
172
|
+
conn.removeAllListeners();
|
|
173
|
+
if (connRef.current[peerId] === conn) delete connRef.current[peerId];
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function handleCall(call) {
|
|
177
|
+
const peerId = call.peer;
|
|
178
|
+
if (!call.localStream) {
|
|
179
|
+
if (!localStreamRef.current) return call.close();
|
|
180
|
+
call.answer(localStreamRef.current);
|
|
181
|
+
}
|
|
182
|
+
call.on("stream", () => {
|
|
183
|
+
callsRef.current[peerId]?.close();
|
|
184
|
+
callsRef.current[peerId] = call;
|
|
185
|
+
if (!audioContextRef.current) audioContextRef.current = new AudioContext();
|
|
186
|
+
if (audioContextRef.current.state === "suspended") audioContextRef.current.resume();
|
|
187
|
+
if (!mixerRef.current) {
|
|
188
|
+
mixerRef.current = audioContextRef.current.createGain();
|
|
189
|
+
mixerRef.current.connect(audioContextRef.current.destination);
|
|
190
|
+
}
|
|
191
|
+
removePeerAudio(peerId);
|
|
192
|
+
const audio = new Audio();
|
|
193
|
+
audio.srcObject = call.remoteStream;
|
|
194
|
+
audio.autoplay = true;
|
|
195
|
+
audio.muted = false;
|
|
196
|
+
const source = audioContextRef.current.createMediaElementSource(audio);
|
|
197
|
+
source.connect(mixerRef.current);
|
|
198
|
+
sourceNodesRef.current[peerId] = source;
|
|
199
|
+
});
|
|
200
|
+
call.on("close", () => {
|
|
201
|
+
call.removeAllListeners();
|
|
202
|
+
if (callsRef.current[peerId] === call) {
|
|
203
|
+
removePeerAudio(peerId);
|
|
204
|
+
delete callsRef.current[peerId];
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function receiveMessage(message) {
|
|
209
|
+
addMessage(message);
|
|
210
|
+
onMessageReceived?.(message);
|
|
211
|
+
}
|
|
212
|
+
function removePeerAudio(peerId) {
|
|
213
|
+
const source = sourceNodesRef.current[peerId];
|
|
214
|
+
if (!source) return;
|
|
215
|
+
source.disconnect();
|
|
216
|
+
delete sourceNodesRef.current[peerId];
|
|
217
|
+
}
|
|
218
|
+
function sendMessage(message) {
|
|
219
|
+
const event = {
|
|
220
|
+
type: "message",
|
|
221
|
+
message: {
|
|
222
|
+
...message,
|
|
223
|
+
name
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
addMessage(event.message);
|
|
227
|
+
Object.values(connRef.current).forEach((conn) => conn.send(event));
|
|
228
|
+
onMessageSent?.(event.message);
|
|
229
|
+
}
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
const onOnline = () => peerRef.current?.disconnected && scheduleReconnectRef.current?.();
|
|
232
|
+
window.addEventListener("online", onOnline);
|
|
233
|
+
return () => window.removeEventListener("online", onOnline);
|
|
234
|
+
}, []);
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (!text && !audio) return;
|
|
237
|
+
let destroyed = false;
|
|
238
|
+
let reconnecting = false;
|
|
239
|
+
let reconnectTimer;
|
|
240
|
+
scheduleReconnectRef.current = () => {
|
|
241
|
+
if (destroyed || reconnecting) return;
|
|
242
|
+
reconnecting = true;
|
|
243
|
+
reconnectTimer = setTimeout(() => {
|
|
244
|
+
const peer = peerRef.current;
|
|
245
|
+
if (peer) if (isMobile()) setPeerGeneration((prev) => prev + 1);
|
|
246
|
+
else peer.reconnect();
|
|
247
|
+
reconnecting = false;
|
|
248
|
+
}, 1e3);
|
|
249
|
+
};
|
|
250
|
+
const scheduleReconnect = scheduleReconnectRef.current;
|
|
251
|
+
import("peerjs").then(({ Peer, util: { supports: { audioVideo, data } } }) => {
|
|
252
|
+
if (destroyed) return;
|
|
253
|
+
if (!data || !audioVideo) return onError(/* @__PURE__ */ new Error("Browser not supported! Try some other browser."));
|
|
254
|
+
peerRef.current = new Peer(completePeerId, {
|
|
255
|
+
config: defaultConfig,
|
|
256
|
+
...peerOptions
|
|
257
|
+
});
|
|
258
|
+
setPeerEpoch((prev) => prev + 1);
|
|
259
|
+
const peer = peerRef.current;
|
|
260
|
+
peer.on("connection", handleConnection);
|
|
261
|
+
peer.on("call", handleCall);
|
|
262
|
+
peer.on("disconnected", () => {
|
|
263
|
+
resetConnections();
|
|
264
|
+
scheduleReconnect();
|
|
265
|
+
});
|
|
266
|
+
peer.on("error", (error) => {
|
|
267
|
+
if (error.type === "network" || error.type === "server-error") {
|
|
268
|
+
resetConnections();
|
|
269
|
+
scheduleReconnect();
|
|
270
|
+
onNetworkError?.(error);
|
|
271
|
+
}
|
|
272
|
+
onPeerError(error);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
return () => {
|
|
276
|
+
destroyed = true;
|
|
277
|
+
reconnecting = false;
|
|
278
|
+
clearTimeout(reconnectTimer);
|
|
279
|
+
peerRef.current?.removeAllListeners();
|
|
280
|
+
peerRef.current?.destroy();
|
|
281
|
+
peerRef.current = null;
|
|
282
|
+
};
|
|
283
|
+
}, [completePeerId, peerGeneration]);
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
if (!text) return;
|
|
286
|
+
const peer = peerRef.current;
|
|
287
|
+
if (!peer) return;
|
|
288
|
+
const connectData = () => completeRemotePeerIds.forEach((id) => handleConnection(peer.connect(id)));
|
|
289
|
+
if (peer.open) connectData();
|
|
290
|
+
peer.on("open", connectData);
|
|
291
|
+
return () => {
|
|
292
|
+
peer.off("open", connectData);
|
|
293
|
+
resetConnections("data");
|
|
294
|
+
};
|
|
295
|
+
}, [text, peerEpoch]);
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
if (!audio) return;
|
|
298
|
+
const peer = peerRef.current;
|
|
299
|
+
if (!peer) return;
|
|
300
|
+
const setupAudio = async () => {
|
|
301
|
+
try {
|
|
302
|
+
if (!localStreamRef.current) localStreamRef.current = await navigator.mediaDevices.getUserMedia({
|
|
303
|
+
video: false,
|
|
304
|
+
audio: {
|
|
305
|
+
autoGainControl: true,
|
|
306
|
+
noiseSuppression: true,
|
|
307
|
+
echoCancellation: true
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
completeRemotePeerIds.forEach((id) => localStreamRef.current && handleCall(peer.call(id, localStreamRef.current)));
|
|
311
|
+
} catch {
|
|
312
|
+
setAudio(false);
|
|
313
|
+
onError(/* @__PURE__ */ new Error("Microphone not accessible"));
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
if (peer.open) setupAudio();
|
|
317
|
+
peer.on("open", setupAudio);
|
|
318
|
+
return () => {
|
|
319
|
+
peer.off("open", setupAudio);
|
|
320
|
+
localStreamRef.current?.getTracks().forEach((track) => track.stop());
|
|
321
|
+
resetConnections("call");
|
|
322
|
+
audioContextRef.current?.close();
|
|
323
|
+
localStreamRef.current = null;
|
|
324
|
+
audioContextRef.current = null;
|
|
325
|
+
mixerRef.current = null;
|
|
326
|
+
};
|
|
327
|
+
}, [audio, peerEpoch]);
|
|
328
|
+
return {
|
|
329
|
+
peerId: completePeerId,
|
|
330
|
+
remotePeers,
|
|
331
|
+
messages,
|
|
332
|
+
sendMessage,
|
|
333
|
+
audio,
|
|
334
|
+
setAudio
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function useMessages() {
|
|
338
|
+
const [messages, setMessages] = useStorage("rpc-messages", []);
|
|
339
|
+
const addMessage = (message) => setMessages((prev) => prev.concat(message));
|
|
340
|
+
return [
|
|
341
|
+
messages,
|
|
342
|
+
setMessages,
|
|
343
|
+
addMessage
|
|
344
|
+
];
|
|
345
|
+
}
|
|
346
|
+
function useStorage(key, initialValue, local = false) {
|
|
347
|
+
const [storedValue, setStoredValue] = useState(() => typeof window === "undefined" ? initialValue : getStorage(key, local, initialValue));
|
|
348
|
+
const setValue = (value) => {
|
|
349
|
+
setStoredValue((prev) => {
|
|
350
|
+
const next = isSetStateFunction(value) ? value(prev) : value;
|
|
351
|
+
setStorage(key, next, local);
|
|
352
|
+
return next;
|
|
353
|
+
});
|
|
354
|
+
};
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
return subscribeToStorage(key, local, (value) => setStoredValue(value ?? initialValue));
|
|
357
|
+
}, [key, local]);
|
|
358
|
+
return [storedValue, setValue];
|
|
359
|
+
}
|
|
360
|
+
function useAudio(allowed) {
|
|
361
|
+
const [audio, setAudio] = useStorage("rpc-audio", false, true);
|
|
362
|
+
return [audio && allowed, setAudio];
|
|
363
|
+
}
|
|
364
|
+
//#endregion
|
|
365
|
+
//#region src/components.tsx
|
|
366
|
+
function Chat({ text = true, audio = true, onMessageReceived, dialogOptions, props = {}, children, ...hookProps }) {
|
|
367
|
+
const { peerId, ...childrenOptions } = useChat({
|
|
368
|
+
text,
|
|
369
|
+
audio,
|
|
370
|
+
onMessageReceived: receiveMessageHandler,
|
|
371
|
+
...hookProps
|
|
372
|
+
});
|
|
373
|
+
const { remotePeers, messages, sendMessage, audio: audioEnabled, setAudio } = childrenOptions;
|
|
374
|
+
const containerRef = useRef(null);
|
|
375
|
+
const [dialog, setDialog] = useState(false);
|
|
376
|
+
const dialogRef = useRef(null);
|
|
377
|
+
const inputRef = useRef(null);
|
|
378
|
+
const [notification, setNotification] = useState(false);
|
|
379
|
+
function receiveMessageHandler(message) {
|
|
380
|
+
if (!dialogRef.current?.open) setNotification(true);
|
|
381
|
+
onMessageReceived?.(message);
|
|
382
|
+
}
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
if (dialog) dialogRef.current?.show();
|
|
385
|
+
else dialogRef.current?.close();
|
|
386
|
+
}, [dialog]);
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
const container = containerRef.current;
|
|
389
|
+
if (container) container.scrollTop = container.scrollHeight;
|
|
390
|
+
}, [
|
|
391
|
+
dialog,
|
|
392
|
+
remotePeers,
|
|
393
|
+
messages
|
|
394
|
+
]);
|
|
395
|
+
return /* @__PURE__ */ React.createElement("div", {
|
|
396
|
+
className: "rpc-main rpc-font",
|
|
397
|
+
...props
|
|
398
|
+
}, 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, {
|
|
399
|
+
title: "Close chat",
|
|
400
|
+
onClick: () => setDialog(false)
|
|
401
|
+
}) : /* @__PURE__ */ React.createElement("div", { className: "rpc-notification" }, /* @__PURE__ */ React.createElement(BiSolidMessageDetail, {
|
|
402
|
+
title: "Open chat",
|
|
403
|
+
onClick: () => {
|
|
404
|
+
setNotification(false);
|
|
405
|
+
setDialog(true);
|
|
406
|
+
}
|
|
407
|
+
}), notification && /* @__PURE__ */ React.createElement("span", { className: "rpc-badge" })), /* @__PURE__ */ React.createElement("dialog", {
|
|
408
|
+
ref: dialogRef,
|
|
409
|
+
className: `${dialog ? "rpc-dialog" : ""} rpc-position-${dialogOptions?.position || "center"}`,
|
|
410
|
+
style: dialogOptions?.style
|
|
411
|
+
}, /* @__PURE__ */ React.createElement("div", { className: "rpc-heading" }, "Chat"), /* @__PURE__ */ React.createElement("hr", { className: "rpc-hr" }), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", {
|
|
412
|
+
ref: containerRef,
|
|
413
|
+
className: "rpc-message-container"
|
|
414
|
+
}, messages.map(({ id, name, text }, i) => /* @__PURE__ */ React.createElement("div", { key: i }, /* @__PURE__ */ React.createElement("strong", null, id === peerId ? "You" : name, ": "), /* @__PURE__ */ React.createElement("span", null, text)))), /* @__PURE__ */ React.createElement("hr", { className: "rpc-hr" }), /* @__PURE__ */ React.createElement("form", {
|
|
415
|
+
className: "rpc-input-container",
|
|
416
|
+
onSubmit: (e) => {
|
|
417
|
+
e.preventDefault();
|
|
418
|
+
const text = inputRef.current?.value;
|
|
419
|
+
if (text) {
|
|
420
|
+
inputRef.current.value = "";
|
|
421
|
+
sendMessage({
|
|
422
|
+
id: peerId,
|
|
423
|
+
text
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}, /* @__PURE__ */ React.createElement("input", {
|
|
428
|
+
ref: inputRef,
|
|
429
|
+
className: "rpc-input rpc-font",
|
|
430
|
+
placeholder: "Enter a message"
|
|
431
|
+
}), /* @__PURE__ */ React.createElement("button", {
|
|
432
|
+
type: "submit",
|
|
433
|
+
className: "rpc-button"
|
|
434
|
+
}, /* @__PURE__ */ React.createElement(GrSend, { title: "Send message" })))))), audio && /* @__PURE__ */ React.createElement("button", {
|
|
435
|
+
className: "rpc-button",
|
|
436
|
+
onClick: () => setAudio(!audioEnabled)
|
|
437
|
+
}, audioEnabled ? /* @__PURE__ */ React.createElement(BsFillMicFill, { title: "Turn mic off" }) : /* @__PURE__ */ React.createElement(BsFillMicMuteFill, { title: "Turn mic on" }))));
|
|
438
|
+
}
|
|
439
|
+
//#endregion
|
|
440
|
+
export { clearChat, Chat as default, useChat };
|