react-peer-chat
Version:
An easy to use react component for impleting peer-to-peer chatting.
297 lines (288 loc) • 14.2 kB
JavaScript
import { __spreadValues, __async, __objRest, BiSolidMessageX, BiSolidMessageDetail, GrSend, BsFillMicFill, BsFillMicMuteFill } from './chunks/chunk-4CBQRGQ3.js';
import React, { useRef, useState, useEffect } from 'react';
// src/connection.tsx
function closeConnection(conn) {
conn.removeAllListeners();
conn.close();
}
// src/constants.ts
var turnAccounts = [
{ username: "70061a377b51f3a3d01c11e3", credential: "lHV4NYJ5Rfl5JNa9" },
{ username: "13b19eb65bbf6e9f96d64b72", credential: "7R9P/+7y7Q516Etv" },
{ username: "3469603f5cdc7ca4a1e891ae", credential: "/jMyLSDbbcgqpVQv" },
{ username: "a7926f4dcc4a688d41f89752", credential: "ZYM8jFYeb8bQkL+N" },
{ username: "0be25ab7f61d9d733ba94809", credential: "hiiSwWVch+ftt3SX" },
{ username: "3c25ba948daeab04f9b66187", credential: "FQB3GQwd27Y0dPeK" }
];
var defaultConfig = {
iceServers: [
{
urls: ["stun:stun.l.google.com:19302", "stun:stun.relay.metered.ca:80"]
}
].concat(
turnAccounts.map((account) => __spreadValues({
urls: [
"turn:standard.relay.metered.ca:80",
"turn:standard.relay.metered.ca:80?transport=tcp",
"turn:standard.relay.metered.ca:443",
"turns:standard.relay.metered.ca:443?transport=tcp"
]
}, account))
)
};
// src/storage.ts
var removeStorage = (key, local = false) => (local ? localStorage : sessionStorage).removeItem(key);
var setStorage = (key, value, local = false) => (local ? localStorage : sessionStorage).setItem(key, JSON.stringify(value));
var getStorage = (key, fallbackValue, local = false) => {
let value = (local ? localStorage : sessionStorage).getItem(key);
try {
if (!value) throw new Error("Value doesn't exist");
value = JSON.parse(value);
} catch (e) {
if (fallbackValue !== void 0) {
value = fallbackValue;
setStorage(key, value, local);
} else {
value = null;
removeStorage(key, local);
}
}
return value;
};
var clearChat = () => {
removeStorage("rpc-remote-peer");
removeStorage("rpc-messages");
};
// src/utils.ts
var addPrefix = (str) => `rpc-${str}`;
// src/hooks.tsx
function useChat({
name,
peerId,
remotePeerId = [],
peerOptions,
text = true,
recoverChat = false,
audio: allowed = true,
onError = () => alert("Browser not supported! Try some other browser."),
onMicError = () => alert("Microphone not accessible!"),
onMessageSent,
onMessageReceived
}) {
const [audio, setAudio] = useAudio(allowed);
const audioStreamRef = useRef(null);
const connRef = useRef({});
const localStream = useRef(null);
const [messages, setMessages, addMessage] = useMessages();
const [peer, setPeer] = useState();
const [remotePeers, setRemotePeers] = useStorage("rpc-remote-peer", {});
peerId = addPrefix(peerId);
if (typeof remotePeerId === "string") remotePeerId = [remotePeerId];
const remotePeerIds = remotePeerId.map(addPrefix);
function handleConnection(conn) {
connRef.current[conn.peer] = conn;
conn.on("open", () => {
conn.on("data", ({ message, messages: messages2, remotePeerName, type }) => {
if (type === "message") receiveMessage(message);
else if (type === "init") {
setRemotePeers((prev) => {
prev[conn.peer] = remotePeerName || "Anonymous User";
return prev;
});
if (recoverChat) setMessages((old) => messages2.length > old.length ? messages2 : old);
}
});
conn.send({ type: "init", remotePeerName: name, messages });
});
conn.on("close", conn.removeAllListeners);
}
function handleError() {
setAudio(false);
onMicError();
}
function handleRemoteStream(remoteStream) {
if (audioStreamRef.current) audioStreamRef.current.srcObject = remoteStream;
}
function receiveMessage(message) {
addMessage(message);
onMessageReceived == null ? void 0 : onMessageReceived(message);
}
function sendMessage(message) {
addMessage(message);
Object.values(connRef.current).forEach((conn) => conn.send({ type: "message", message }));
onMessageSent == null ? void 0 : onMessageSent(message);
}
useEffect(() => {
if (!text && !audio) {
setPeer(void 0);
return;
}
(function() {
return __async(this, null, function* () {
const {
Peer,
util: {
supports: { audioVideo, data }
}
} = yield import('peerjs');
if (!data || !audioVideo) return onError();
const peer2 = new Peer(peerId, __spreadValues({ config: defaultConfig }, peerOptions));
setPeer(peer2);
});
})();
}, [audio]);
useEffect(() => {
if (!peer) return;
let calls = {};
peer.on("open", () => {
remotePeerIds.forEach((id) => {
if (text) handleConnection(peer.connect(id));
});
if (audio) {
const getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
try {
getUserMedia(
{
video: false,
audio: {
autoGainControl: false,
// Disable automatic gain control
noiseSuppression: true,
// Enable noise suppression
echoCancellation: true
// Enable echo cancellation
}
},
(stream) => {
localStream.current = stream;
remotePeerIds.forEach((id) => {
const call = peer.call(id, stream);
call.on("stream", handleRemoteStream);
call.on("close", call.removeAllListeners);
calls[id] = call;
});
peer.on("call", (call) => {
call.answer(stream);
call.on("stream", handleRemoteStream);
call.on("close", call.removeAllListeners);
calls[call.peer] = call;
});
},
handleError
);
} catch (e) {
handleError();
}
}
});
peer.on("connection", handleConnection);
return () => {
var _a;
(_a = localStream.current) == null ? void 0 : _a.getTracks().forEach((track) => track.stop());
Object.values(connRef.current).forEach(closeConnection);
connRef.current = {};
Object.values(calls).forEach(closeConnection);
peer.removeAllListeners();
peer.destroy();
};
}, [peer]);
return { peerId, audioStreamRef, remotePeers, messages, sendMessage, audio, setAudio };
}
function useMessages() {
const [messages, setMessages] = useStorage("rpc-messages", []);
const addMessage = (message) => setMessages((prev) => prev.concat(message));
return [messages, setMessages, addMessage];
}
function useStorage(key, initialValue, local = false) {
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === "undefined") return initialValue;
return getStorage(key, initialValue, local);
});
const setValue = (value) => {
setStoredValue((old) => {
const updatedValue = typeof value === "function" ? value(old) : value;
setStorage(key, updatedValue, local);
return updatedValue;
});
};
return [storedValue, setValue];
}
function useAudio(allowed) {
const [audio, setAudio] = useStorage("rpc-audio", false, true);
const enabled = audio && allowed;
return [enabled, setAudio];
}
// src/styles.css
function injectStyle(css) {
if (typeof document === "undefined") return;
const head = document.head || document.getElementsByTagName("head")[0];
const style = document.createElement("style");
style.type = "text/css";
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
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');
// src/index.tsx
function Chat(_a) {
var _b = _a, { text = true, audio = true, onMessageReceived, dialogOptions, props = {}, children } = _b, hookProps = __objRest(_b, ["text", "audio", "onMessageReceived", "dialogOptions", "props", "children"]);
const _a2 = useChat(__spreadValues({
text,
audio,
onMessageReceived: modifiedOnMessageReceived
}, hookProps)), { peerId, audioStreamRef } = _a2, childrenOptions = __objRest(_a2, ["peerId", "audioStreamRef"]);
const { remotePeers, messages, sendMessage, audio: audioEnabled, setAudio } = childrenOptions;
const containerRef = useRef(null);
const [dialog, setDialog] = useState(false);
const dialogRef = useRef(null);
const inputRef = useRef(null);
const [notification, setNotification] = useState(false);
function modifiedOnMessageReceived(message) {
var _a3;
if (!((_a3 = dialogRef.current) == null ? void 0 : _a3.open)) setNotification(true);
onMessageReceived == null ? void 0 : onMessageReceived(message);
}
useEffect(() => {
var _a3, _b2;
if (dialog) (_a3 = dialogRef.current) == null ? void 0 : _a3.show();
else (_b2 = dialogRef.current) == null ? void 0 : _b2.close();
}, [dialog]);
useEffect(() => {
const container = containerRef.current;
if (container) container.scrollTop = container.scrollHeight;
}, [dialog, remotePeers, messages]);
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(
BiSolidMessageDetail,
{
title: "Open chat",
onClick: () => {
setNotification(false);
setDialog(true);
}
}
), 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(
"form",
{
className: "rpc-input-container",
onSubmit: (e) => {
var _a3;
e.preventDefault();
const text2 = (_a3 = inputRef.current) == null ? void 0 : _a3.value;
if (text2) {
inputRef.current.value = "";
sendMessage({ id: peerId, text: text2 });
}
}
},
/* @__PURE__ */ React.createElement("input", { ref: inputRef, className: "rpc-input rpc-font", placeholder: "Enter a message" }),
/* @__PURE__ */ React.createElement("button", { type: "submit", className: "rpc-button" }, /* @__PURE__ */ React.createElement(GrSend, { title: "Send message" }))
)))), 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" } }));
}
export { clearChat, Chat as default, useChat };