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
+
+
+
+}
\ 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%'
}
+
+
-
-
-
-