diff --git a/package-lock.json b/package-lock.json index 813e8e3..c0135c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,9 @@ "eslint-plugin-react": "^7.30.1", "express": "^4.18.1", "get-port": "^6.1.2", + "local-storage": "^2.0.0", "mysql": "^2.18.1", + "peerjs": "^1.4.6", "qrcode": "^1.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -928,6 +930,14 @@ "node": ">=6" } }, + "node_modules/@swc/helpers": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.3.17.tgz", + "integrity": "sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@szmarczak/http-timer": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", @@ -4929,6 +4939,11 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -6929,6 +6944,11 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/local-storage": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/local-storage/-/local-storage-2.0.0.tgz", + "integrity": "sha512-/0sRoeijw7yr/igbVVygDuq6dlYCmtsuTmmpnweVlVtl/s10pf5BCq8LWBxW/AMyFJ3MhMUuggMZiYlx6qr9tw==" + }, "node_modules/locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -8084,6 +8104,29 @@ "node": "*" } }, + "node_modules/peerjs": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/peerjs/-/peerjs-1.4.6.tgz", + "integrity": "sha512-0XA105/9yBFGxfpyCjlI1bcBiyPmXHs8+UDvO2j1WGnY+FXilMn35+P/3t8HzKnUnR1SX0PRkDSk8kM17ciNxA==", + "dependencies": { + "@swc/helpers": "^0.3.13", + "eventemitter3": "^4.0.7", + "peerjs-js-binarypack": "1.0.1", + "webrtc-adapter": "^7.7.1" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/peer" + } + }, + "node_modules/peerjs-js-binarypack": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/peerjs-js-binarypack/-/peerjs-js-binarypack-1.0.1.tgz", + "integrity": "sha512-N6aeia3NhdpV7kiGxJV5xQiZZCVEEVjRz2T2C6UZQiBkHWHzUv/oWA4myQLcwBwO8LUoR1KWW5oStvwVesmfCg==" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -9044,6 +9087,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/rtcpeerconnection-shim": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz", + "integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==", + "dependencies": { + "sdp": "^2.6.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -9115,6 +9170,11 @@ "loose-envify": "^1.1.0" } }, + "node_modules/sdp": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz", + "integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw==" + }, "node_modules/semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -9932,10 +9992,9 @@ } }, "node_modules/tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "devOptional": true + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -10574,6 +10633,19 @@ "integrity": "sha512-5NUqC2JquIL2pBAAo/VfBP6KuGkHIZQXW/lNKupLPfhViwh8wNsu0BObtl09yuKZszeEUfbXz8xhrHvSG16Nqw==", "dev": true }, + "node_modules/webrtc-adapter": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz", + "integrity": "sha512-TbrbBmiQBL9n0/5bvDdORc6ZfRY/Z7JnEj+EYOD1ghseZdpJ+nF2yx14k3LgQKc7JZnG7HAcL+zHnY25So9d7A==", + "dependencies": { + "rtcpeerconnection-shim": "^1.2.15", + "sdp": "^2.12.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, "node_modules/whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", @@ -11545,6 +11617,14 @@ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" }, + "@swc/helpers": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.3.17.tgz", + "integrity": "sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==", + "requires": { + "tslib": "^2.4.0" + } + }, "@szmarczak/http-timer": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", @@ -14519,6 +14599,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -16032,6 +16117,11 @@ "integrity": "sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==", "dev": true }, + "local-storage": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/local-storage/-/local-storage-2.0.0.tgz", + "integrity": "sha512-/0sRoeijw7yr/igbVVygDuq6dlYCmtsuTmmpnweVlVtl/s10pf5BCq8LWBxW/AMyFJ3MhMUuggMZiYlx6qr9tw==" + }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -16889,6 +16979,22 @@ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true }, + "peerjs": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/peerjs/-/peerjs-1.4.6.tgz", + "integrity": "sha512-0XA105/9yBFGxfpyCjlI1bcBiyPmXHs8+UDvO2j1WGnY+FXilMn35+P/3t8HzKnUnR1SX0PRkDSk8kM17ciNxA==", + "requires": { + "@swc/helpers": "^0.3.13", + "eventemitter3": "^4.0.7", + "peerjs-js-binarypack": "1.0.1", + "webrtc-adapter": "^7.7.1" + } + }, + "peerjs-js-binarypack": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/peerjs-js-binarypack/-/peerjs-js-binarypack-1.0.1.tgz", + "integrity": "sha512-N6aeia3NhdpV7kiGxJV5xQiZZCVEEVjRz2T2C6UZQiBkHWHzUv/oWA4myQLcwBwO8LUoR1KWW5oStvwVesmfCg==" + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -17599,6 +17705,14 @@ "fsevents": "~2.3.2" } }, + "rtcpeerconnection-shim": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz", + "integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==", + "requires": { + "sdp": "^2.6.0" + } + }, "run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -17653,6 +17767,11 @@ "loose-envify": "^1.1.0" } }, + "sdp": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz", + "integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw==" + }, "semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -18283,10 +18402,9 @@ } }, "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "devOptional": true + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, "tsutils": { "version": "3.21.0", @@ -18717,6 +18835,15 @@ "integrity": "sha512-5NUqC2JquIL2pBAAo/VfBP6KuGkHIZQXW/lNKupLPfhViwh8wNsu0BObtl09yuKZszeEUfbXz8xhrHvSG16Nqw==", "dev": true }, + "webrtc-adapter": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz", + "integrity": "sha512-TbrbBmiQBL9n0/5bvDdORc6ZfRY/Z7JnEj+EYOD1ghseZdpJ+nF2yx14k3LgQKc7JZnG7HAcL+zHnY25So9d7A==", + "requires": { + "rtcpeerconnection-shim": "^1.2.15", + "sdp": "^2.12.0" + } + }, "whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", diff --git a/package.json b/package.json index 2323c57..d33a261 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,9 @@ "eslint-plugin-react": "^7.30.1", "express": "^4.18.1", "get-port": "^6.1.2", + "local-storage": "^2.0.0", "mysql": "^2.18.1", + "peerjs": "^1.4.6", "qrcode": "^1.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/packages/renderer/src/Router.tsx b/packages/renderer/src/Router.tsx index 484a174..feb67a4 100644 --- a/packages/renderer/src/Router.tsx +++ b/packages/renderer/src/Router.tsx @@ -2,8 +2,10 @@ import { useContext, useEffect } from "react"; import ServerConnection from "./components/ServerConnection"; import Sidebar from "./components/Sidebar"; import TwoPanel from "./components/TwoPanel"; +import Voice from "./components/Voice"; import { SettingsContext } from "./contexts/EphemeralState/EphemeralState"; import useHomeServer from "./contexts/PersistentState/useHomeServerNative"; +import useChannel from "./hooks/useChannel"; import useClientId from "./hooks/useClientId"; import useSessionToken from "./hooks/useSessionToken"; import Channels from "./pages/Channels"; @@ -22,6 +24,7 @@ export default function Router(props: RouterProps) { const { sessionToken } = useSessionToken(); const { homeServer } = useHomeServer(); const { isSettingsOpen, closeSettings } = useContext(SettingsContext); + const { voice, text } = useChannel(); const configured = homeServer !== null && @@ -43,7 +46,27 @@ export default function Router(props: RouterProps) { sidebar={300} > - + {voice ? ( +
+
+ +
+
+ +
+
+ ) : ( + + )} )} diff --git a/packages/renderer/src/components/Voice.tsx b/packages/renderer/src/components/Voice.tsx new file mode 100644 index 0000000..b23c30e --- /dev/null +++ b/packages/renderer/src/components/Voice.tsx @@ -0,0 +1,37 @@ +import { useCallback, useContext } from "react" +import { PeerContext } from "../contexts/EphemeralState/PeerState"; +import useChannel from "../hooks/useChannel"; +import { useApi } from "../lib/useApi"; + + + +export default function Voice(props: any) { + const { uid } = props; + const { connected, peerId } = useContext(PeerContext); + const { channel } = useChannel(); + + const { send } = useApi({ + + }); + + const joinCall = useCallback(() => { + if(peerId === null || connected === false) return; + send('voice:join', { peerId, channelId: channel }) + }, [connected, peerId, channel]) + + return
+
+ Peer Info + connected: {connected ? 'true' : 'false'}

+ PeerId: {peerId}

+
+
+ Actions + +
+
+} \ No newline at end of file diff --git a/packages/renderer/src/contexts/EphemeralState/EphemeralState.tsx b/packages/renderer/src/contexts/EphemeralState/EphemeralState.tsx index d30e960..226b777 100644 --- a/packages/renderer/src/contexts/EphemeralState/EphemeralState.tsx +++ b/packages/renderer/src/contexts/EphemeralState/EphemeralState.tsx @@ -1,11 +1,19 @@ import { createContext, useState, useMemo, useEffect } from "react"; +import UserMediaState from "./UserMediaState"; +import PeerState from "./PeerState"; + +export type ChannelType = 'text' | 'voice'; export const ChannelContext = createContext<{ channel: string | null, - setChannel: (uid: string) => void + text: boolean, + voice: boolean, + setChannel: (uid: string, type: ChannelType) => void }>({ channel: null, setChannel: () => {}, + text: false, + voice: false, }); export const TransparencyContext = createContext<(transparent: boolean) => void>(() => {}); export const SettingsContext = createContext<{ @@ -24,12 +32,38 @@ export default function EphemeralState(props: { }) { const [channel, setChannel] = useState(null); + const [voice, setVoice] = useState(false); + const [text, setText] = useState(false); const [transparent, setTransparent] = useState(false); const [settings, setSettings] = useState(false); const channelContextValue = useMemo(() => { - return { channel, setChannel }; + return { + channel, + setChannel: (uid: string, channelType: ChannelType) => { + setChannel(uid); + switch(channelType) { + case 'text': { + setVoice(false); + setText(true); + break; + } + case 'voice': { + setVoice(true); + setText(false); + break; + } + default: { + setVoice(false); + setText(false); + break; + } + } + }, + voice, + text + }; }, [channel, setChannel]); useEffect(() => { @@ -46,7 +80,11 @@ export default function EphemeralState(props: { closeSettings: () => setSettings(false), isSettingsOpen: settings, }}> - {props.children} + + + {props.children} + + diff --git a/packages/renderer/src/contexts/EphemeralState/PeerState.tsx b/packages/renderer/src/contexts/EphemeralState/PeerState.tsx new file mode 100644 index 0000000..3d52f47 --- /dev/null +++ b/packages/renderer/src/contexts/EphemeralState/PeerState.tsx @@ -0,0 +1,82 @@ +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { Peer, MediaConnection } from "peerjs"; +import { UserMediaContext } from "./UserMediaState"; + +export const PeerContext = createContext<{ + connected: boolean; + peerId: string | null +}>({ + connected: false, + peerId: null +}); + +function useCurrent(thing: T) { + const thingRef = useRef(thing); + + useEffect(() => { + thingRef.current = thing; + }, [thing]); + + return thingRef.current; +} + +export default function PeerState(props: any) { + + const { mediaStream } = useContext(UserMediaContext); + // TODO ability to disable until needed + // const [enabled, setEnabled] = useState(true); + const [connected, setConnected] = useState(false); + const [peer, setPeer] = useState(null); + const [peerId, setPeerId] = useState(null); + const [incomingCalls, setIncomingCalls] = useState([]); + + const addCall = useCurrent(useCallback((call: MediaConnection) => { + // HACK lookout for possible timing issues here. + // if we get two incomming calls before a re-render + // then our state could be out of date?! + // a possible solution is to cache the + // append to the list, and if the cache and + // state disagree, add to the cache, and set state + // to the cached value. + setIncomingCalls([...incomingCalls, call]); + }, [incomingCalls])) + + useEffect(() => { + if(connected) return; + + const peer = new Peer(); + setPeer(peer); + + peer.on('open', (id: string) => { + setConnected(true); + setPeerId(id); + }); + + peer.on('close', () => { + setConnected(false); + setPeerId(null); + setPeer(null); + }); + + peer.on('call', (call: MediaConnection) => { + addCall(call); + }); + }, [connected]); + + const dial = useCallback((id: string) => { + if(peer === null) return; + if(mediaStream === null) return; + peer.call(id, mediaStream); + }, [peer, mediaStream]) + + const value = useMemo(() => ({ + connected, + peerId, + }), [connected, peerId]); + + return + {props.children} + +} + + diff --git a/packages/renderer/src/contexts/EphemeralState/UserMediaState.tsx b/packages/renderer/src/contexts/EphemeralState/UserMediaState.tsx new file mode 100644 index 0000000..1e2e211 --- /dev/null +++ b/packages/renderer/src/contexts/EphemeralState/UserMediaState.tsx @@ -0,0 +1,53 @@ +import { createContext, useCallback, useMemo, useState } from "react"; + +export const UserMediaContext = createContext<{ + enabled: boolean; + mediaStream: MediaStream | null; + enable: () => void; + disable: () => void; +}>({ + enabled: false, + mediaStream: null, + enable: () => {}, + disable: () => {}, +}); + +export default function UserMediaState(props: any) { + + const [mediaStream, setMediaStream] = useState(null); + const [enabled, setEnabled] = useState(false); + + const enable = useCallback(async () => { + const newStream = await navigator.mediaDevices.getUserMedia({ + audio: true, + video: false, + }); + + setMediaStream(newStream); + setEnabled(true); + }, []); + + const disable = useCallback(async () => { + if(mediaStream === null) return; + + for(const track of mediaStream?.getTracks()) { + track.stop(); + } + + setMediaStream(null); + setEnabled(false); + }, [mediaStream]) + + const value = useMemo(() => ({ + enabled: false, + mediaStream: null, + enable, + disable + }), []); + + + + return + {props.children} + +} \ No newline at end of file diff --git a/packages/renderer/src/contexts/EphemeralState/VoiceChannelState.tsx b/packages/renderer/src/contexts/EphemeralState/VoiceChannelState.tsx new file mode 100644 index 0000000..94cd37b --- /dev/null +++ b/packages/renderer/src/contexts/EphemeralState/VoiceChannelState.tsx @@ -0,0 +1,43 @@ +import { createContext, useContext, useEffect, useMemo, useState } from "react"; +import { PeerContext } from "./PeerState"; +import { useApi } from "/@/lib/useApi"; + +interface RemotePeer { + mediaStream: MediaStream | null; + peerId: string; +} + +export const VoiceChannelContext = createContext<{ + voiceChannelId: string | null; + setVoiceChannelId: (channelId: string | null) => void +}>({ + voiceChannelId: null, + setVoiceChannelId: () => {} +}); + +export default function VoiceChannelState(props: any) { + + const [voiceChannelId, setVoiceChannelId] = useState(null); + const { peerId, incommingCalls } = useContext(PeerContext); + const [peers, setPeers] = useState([]); + + const { send } = useApi({ + 'voice:list'() { + + } + }) + + useEffect(() => { + + }, [voiceChannelId]) + + + const value = useMemo(() => ({ + voiceChannelId, + setVoiceChannelId, + }), [voiceChannelId, setVoiceChannelId]) + + return + {props.children} + +} \ No newline at end of file diff --git a/packages/renderer/src/lib/api.ts b/packages/renderer/src/lib/api.ts index bd360fa..0e928c4 100644 --- a/packages/renderer/src/lib/api.ts +++ b/packages/renderer/src/lib/api.ts @@ -4,6 +4,25 @@ export function connectApi(url: string) { let connectionAttempts = 0; let destroy = false; let routers: any[] = []; + let keepalive: NodeJS.Timer | null = null; + + function startKeepalive() { + keepalive = setInterval(() => { + if(socket !== null) { + socket.send(JSON.stringify({ + action: 'up', + data: {} + })) + } else { + stopKeepalive(); + } + }, 30_000); + } + + function stopKeepalive() { + if(keepalive !== null) + clearInterval(keepalive); + } const connect = async () => { try { @@ -25,23 +44,26 @@ export function connectApi(url: string) { if(socket === null) return; connectionAttempts = 0; console.log('connected to', url); + startKeepalive(); // socket.send('Hello Server!'); }); - + socket.addEventListener('message', (event) => { const {action, data} = JSON.parse(event.data); // console.debug('[IN]', action, data); const routeFound = routers .map(router => router(action, data)) .reduce((a, b) => a + b, 0); - if(routeFound === 0) { + if(routeFound === 0 && action !== 'up') { console.warn(`route <${action}> not found`); } }); socket.addEventListener('close', () => { - if(destroy) return; + stopKeepalive(); socket = null; + if(destroy) return; + connect(); }); }; diff --git a/packages/renderer/src/lib/native.ts b/packages/renderer/src/lib/native.ts index 213c059..467d848 100644 --- a/packages/renderer/src/lib/native.ts +++ b/packages/renderer/src/lib/native.ts @@ -1,7 +1,14 @@ import * as preload from '#preload'; - // console.log('#preload', preload); +function ls(key: string, value?: string) { + if(value === undefined) { + return localStorage.getItem(key); + } else { + localStorage.setItem(key, value); + } +} + const functions: any = (function() { const electron = !!preload.versions; const cordova = 'cordova' in globalThis; @@ -13,17 +20,23 @@ const functions: any = (function() { let homeServer: string | null = null; return { getClientId() { - return cid; + return ls('clientId'); }, setClientId(id: any) { - cid = id; + ls('clientId', id); }, getHomeServer() { - return homeServer; + return ls('homeServer'); }, setHomeServer(str: string) { - homeServer = str; - } + ls('homeServer', str); + }, + getSessionToken() { + return ls('sessionToken'); + }, + setSessionToken(str: string) { + ls('sessionToken', str); + }, }; } })(); diff --git a/packages/renderer/src/pages/Channel.tsx b/packages/renderer/src/pages/Channel.tsx index 9f4539e..b66db00 100644 --- a/packages/renderer/src/pages/Channel.tsx +++ b/packages/renderer/src/pages/Channel.tsx @@ -1,18 +1,54 @@ import { CgHashtag } from "react-icons/cg"; +import { MdVolumeUp } from "react-icons/md"; +import { BsQuestionLg } from 'react-icons/bs'; +import { ChannelType } from "../contexts/EphemeralState/EphemeralState"; import useChannel from "../hooks/useChannel"; import useHover from "../hooks/useHover"; +import { useApi } from "../lib/useApi"; +import { useContext, useEffect, useState } from "react"; +import { VoiceChannelContext } from "../contexts/EphemeralState/VoiceChannelState"; interface ChannelProps { unread: number; uid: string; name: string; + type: ChannelType; } export default function Channel(props: ChannelProps) { const { channel, setChannel } = useChannel(); - const { unread, uid, name } = props; + const { unread, uid, name, type } = props; const [ref, hover] = useHover(); const selected = channel === uid; + const { voiceChannelId } = useContext(VoiceChannelContext); + + const [participants, setParticipants] = useState([]); + + const { send } = useApi({ + 'voice:join'(data: any) { + if(type !== 'voice' || data.channelId !== uid) return; + setParticipants([...participants, { + clientId: data.clientId, + peerId: data.peerId, + channelId: data.channelId + }]) + console.log('JOIN', data); + }, + 'voice:list'(data: any) { + if(type !== 'voice') return; + console.log('CURRENTS', data); + }, + 'voice:leave'(data: any) { + console.log(data); + }, + }, [uid, type, participants]) + + useEffect(() => { + if(type !== 'voice') return; + + setParticipants([]); + send('voice:list', { uid }); + }, [uid]); return (
{ - setChannel(uid); + setChannel(uid, type); }} ref={ref} > - + {(type === 'text') ? ( + + ) : ((type === 'voice') ? ( + + ) : ( + + ))}
{}}>Delete */} +

+ {participants.map(participant => ( +
+ {participant.clientId} +
+ ))}
) } \ No newline at end of file diff --git a/packages/renderer/src/pages/Channels.tsx b/packages/renderer/src/pages/Channels.tsx index b6044e7..efc5dea 100644 --- a/packages/renderer/src/pages/Channels.tsx +++ b/packages/renderer/src/pages/Channels.tsx @@ -10,10 +10,12 @@ import useChannel from '../hooks/useChannel'; import useClientId from '../hooks/useClientId'; import useHomeServer from '../contexts/PersistentState/useHomeServerNative'; import Channel from './Channel'; +import { ChannelType } from '../contexts/EphemeralState/EphemeralState'; interface IChannel { uid: string; name: string; + type: ChannelType; } interface IUnreads { @@ -25,12 +27,12 @@ export default function Channels() { const [channels, setChannels] = useState([]); const [unreads, setUnreads] = useState({}); - const { channel, setChannel } = useChannel() - const { clientId } = useClientId() + const { channel, setChannel } = useChannel(); + const { clientId } = useClientId(); const { send } = useApi({ - 'channels:list'(data: IChannel[]) { - setChannels(data); + 'channels:list'(data: any) { + setChannels(data.channels); }, 'channel:add'(channel: IChannel) { setChannels([...channels, channel]); @@ -53,7 +55,7 @@ export default function Channels() { useEffect(() => { if(channels.length === 0) return; if(channel !== null) return; - setChannel(channels[0].uid); + setChannel(channels[1].uid, channels[1].type); }, [channel, channels]); useEffect(() => { @@ -66,7 +68,7 @@ export default function Channels() { useEffect(() => { if(clientId === null) return; - send('client:get', { clientId }); + // send('client:get', { clientId }); }, [clientId]); const textbox = useRef(null); @@ -90,6 +92,7 @@ export default function Channels() { diff --git a/packages/renderer/vite.config.js b/packages/renderer/vite.config.js index 373eee1..0904de6 100644 --- a/packages/renderer/vite.config.js +++ b/packages/renderer/vite.config.js @@ -21,6 +21,7 @@ const config = { }, base: '', server: { + host: true, fs: { strict: true, }, diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 1ddb084..296b4d2 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,5 +1,5 @@ import router from './lib/router'; -import { expose } from './lib/WebSocketServer'; +import { expose, reply } from './lib/WebSocketServer'; import message from './routers/message'; import channel from './routers/channel'; @@ -8,7 +8,9 @@ import totp from './routers/totp'; const api = router({ up() { - console.log(Date.now()); + return reply({ + time: Date.now() + }); }, message: message, messages: message, @@ -19,6 +21,7 @@ const api = router({ totp: totp, session: session, sessions: session, + voice: voice }); expose(api, 3000); @@ -27,6 +30,7 @@ expose(api, 3000); import { update } from './db/migrate'; import session from './routers/session'; +import voice from './routers/voice'; try { update(); diff --git a/packages/server/src/lib/get/clientId/by/username.ts b/packages/server/src/lib/get/clientId/by/username.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/server/src/lib/get/displayName/by/clientId.ts b/packages/server/src/lib/get/displayName/by/clientId.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/server/src/lib/get/totpKey/by/clientId.ts b/packages/server/src/lib/get/totpKey/by/clientId.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/server/src/routers/channel.ts b/packages/server/src/routers/channel.ts index b7bd4ce..728f107 100644 --- a/packages/server/src/routers/channel.ts +++ b/packages/server/src/routers/channel.ts @@ -5,10 +5,21 @@ import add from '../db/snippets/channel/new.sql'; import { broadcast, reply } from '../lib/WebSocketServer'; import { v4 } from 'uuid'; +export const mockVoiceChannels = [ + { + uid: v4(), + name: 'Voice Test', + type: 'voice' + } +] + export default router({ async list() { const res = await query(list); - return reply(res ?? undefined); + if(res === null) return; + return reply({ + channels: [...(res.map(v => ({...v, type: 'text'}))), ...mockVoiceChannels] + }); }, async add(channel: any) { const name = channel.name; diff --git a/packages/server/src/routers/voice.ts b/packages/server/src/routers/voice.ts new file mode 100644 index 0000000..c89cc3a --- /dev/null +++ b/packages/server/src/routers/voice.ts @@ -0,0 +1,62 @@ +import router from "../lib/router"; +import { broadcast, reply } from "../lib/WebSocketServer"; + +function filterInPlace(a: T[], condition: (v: T, i: number, a: T[]) => boolean) { + let i = 0, j = 0; + + let copy = [...a]; + let removed = []; + + while (i < a.length) { + const val = a[i]; + if (condition(val, i, copy)) a[j++] = val; + else removed.push(val); + i++; + } + + a.length = j; + return removed; +} + +interface ClientChannelRelationship { + clientId: string; + channelId: string; + peerId: string; +} + +const participants: ClientChannelRelationship[] = []; + +export default router({ + async join(data: any) { + const { $clientId, channelId, peerId } = data; + // TODO validate channel exists + if(participants + .filter(v => v.clientId === $clientId) + .length !== 0) { + // TODO REMOVE USER FROM THIS PLACE + } + + const user_channel = { + clientId: $clientId, + peerId, + channelId + }; + + participants.push(user_channel); + + return broadcast(user_channel) + }, + async list(data: any) { + const { uid } = data; + return reply({ + uid, + participants: participants.filter(v => uid === v.channelId) + }); + }, + async leave(data: any) { + const { $clientId } = data; + const removed = filterInPlace(participants, (v) => v.clientId !== $clientId); + console.log('removed', removed); + return broadcast(removed[0]); + }, +}) \ No newline at end of file diff --git a/rtc-test/EventEmitter.mjs b/rtc-test/EventEmitter.mjs new file mode 100644 index 0000000..e69de29 diff --git a/rtc-test/index.html b/rtc-test/index.html index c428d6d..a64843d 100644 --- a/rtc-test/index.html +++ b/rtc-test/index.html @@ -8,11 +8,31 @@ #tracks { display: grid; grid-template-columns: 1fr 1fr; - grid-auto-flow: column; + grid-auto-flow: row; + } + #tracks fieldset { + width: '50%' } +
+
+ Call +
+ + + + +
+ + + +
+ +
Peer Connection
Peer ID:
@@ -31,25 +51,8 @@
-
- Call -
- - -
- - - -
- -
- -
-