UNPKG

react-peer-chat

Version:

An easy to use react component for impleting peer-to-peer chatting.

297 lines (288 loc) 14.2 kB
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 };