users in channels, kindof

main
Valerie 2022-08-07 20:10:11 -04:00
parent e775e4a240
commit c0289d92e3
21 changed files with 720 additions and 100 deletions

143
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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}
>
<Sidebar></Sidebar>
<Chat></Chat>
{voice ? (
<div style={{
height: '100%',
width: '100%'
}}>
<div style={{
height: '50%',
width: '100%'
}}>
<Voice></Voice>
</div>
<div style={{
height: '50%',
width: '100%'
}}>
<Chat></Chat>
</div>
</div>
) : (
<Chat></Chat>
)}
</TwoPanel>
)}
</ServerConnection>

View File

@ -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 <div style={{
width: '100%',
maxWidth: '500px',
margin: '0px auto',
}}>
<fieldset>
<legend>Peer Info</legend>
connected: {connected ? 'true' : 'false'}<br></br>
PeerId: {peerId}<br></br>
</fieldset>
<fieldset>
<legend>Actions</legend>
<button onClick={joinCall}>Join Call</button>
</fieldset>
</div>
}

View File

@ -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<string | null>(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}
<UserMediaState>
<PeerState>
{props.children}
</PeerState>
</UserMediaState>
</SettingsContext.Provider>
</TransparencyContext.Provider>
</ChannelContext.Provider>

View File

@ -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<T>(thing: T) {
const thingRef = useRef<T>(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<Peer | null>(null);
const [peerId, setPeerId] = useState<string | null>(null);
const [incomingCalls, setIncomingCalls] = useState<MediaConnection[]>([]);
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 <PeerContext.Provider value={value}>
{props.children}
</PeerContext.Provider>
}

View File

@ -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<MediaStream | null>(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 <UserMediaContext.Provider value={value}>
{props.children}
</UserMediaContext.Provider>
}

View File

@ -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<string | null>(null);
const { peerId, incommingCalls } = useContext(PeerContext);
const [peers, setPeers] = useState<RemotePeer[]>([]);
const { send } = useApi({
'voice:list'() {
}
})
useEffect(() => {
}, [voiceChannelId])
const value = useMemo(() => ({
voiceChannelId,
setVoiceChannelId,
}), [voiceChannelId, setVoiceChannelId])
return <VoiceChannelContext.Provider value={value}>
{props.children}
</VoiceChannelContext.Provider>
}

View File

@ -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();
});
};

View File

@ -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);
},
};
}
})();

View File

@ -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<HTMLDivElement>();
const selected = channel === uid;
const { voiceChannelId } = useContext(VoiceChannelContext);
const [participants, setParticipants] = useState<any[]>([]);
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 (
<div
@ -30,19 +66,41 @@ export default function Channel(props: ChannelProps) {
transition: 'background 300ms, color 300ms',
}}
onClick={() => {
setChannel(uid);
setChannel(uid, type);
}}
ref={ref}
>
<CgHashtag color={
selected ? 'var(--neutral-9)' :
hover ? 'var(--neutral-7)' :
'var(--neutral-7)'
} size={24} style={{
margin: '4px',
transition: 'background 300ms, color 300ms',
transform:'skew(-5deg, 0deg)',
}}></CgHashtag>
{(type === 'text') ? (
<CgHashtag color={
selected ? 'var(--neutral-9)' :
hover ? 'var(--neutral-7)' :
'var(--neutral-7)'
} size={24} style={{
margin: '4px',
transition: 'background 300ms, color 300ms',
transform:'skew(-5deg, 0deg)',
}}></CgHashtag>
) : ((type === 'voice') ? (
<MdVolumeUp color={
selected ? 'var(--neutral-9)' :
hover ? 'var(--neutral-7)' :
'var(--neutral-7)'
} size={24} style={{
margin: '4px',
transition: 'background 300ms, color 300ms',
transform:'skew(20deg, 0deg)',
}}></MdVolumeUp>
) : (
<BsQuestionLg color={
selected ? 'var(--neutral-9)' :
hover ? 'var(--neutral-7)' :
'var(--neutral-7)'
} size={24} style={{
margin: '4px',
transition: 'background 300ms, color 300ms',
transform:'skew(20deg, 0deg)',
}}></BsQuestionLg>
))}
<div style={{
lineHeight: '32px',
color: selected ? 'var(--neutral-9)' :
@ -66,6 +124,12 @@ export default function Channel(props: ChannelProps) {
marginLeft: '8px',
fontSize: '10px',
}} href="#" onClick={() => {}}>Delete</a> */}
<br></br>
{participants.map(participant => (
<div key={participant.clientId}>
{participant.clientId}
</div>
))}
</div>
)
}

View File

@ -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<IChannel[]>([]);
const [unreads, setUnreads] = useState<IUnreads>({});
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<HTMLInputElement>(null);
@ -90,6 +92,7 @@ export default function Channels() {
<Channel
key={c.uid}
uid={c.uid}
type={c.type}
unread={unreads[c.uid] ?? 0}
name={c.name}
></Channel>

View File

@ -21,6 +21,7 @@ const config = {
},
base: '',
server: {
host: true,
fs: {
strict: true,
},

View File

@ -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();

View File

@ -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;

View File

@ -0,0 +1,62 @@
import router from "../lib/router";
import { broadcast, reply } from "../lib/WebSocketServer";
function filterInPlace<T>(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]);
},
})

View File

View File

@ -8,11 +8,31 @@
#tracks {
display: grid;
grid-template-columns: 1fr 1fr;
grid-auto-flow: column;
grid-auto-flow: row;
}
#tracks fieldset {
width: '50%'
}
</style>
</head>
<body>
<div id="tracks"></div>
<fieldset>
<legend>Call</legend>
<div id="call_status"></div>
<label>Audio</label>
<select id="in_audio">
</select>
<br />
<label>Video</label>
<select id="in_video">
</select>
<br />
</fieldset>
<fieldset>
<legend>Peer Connection</legend>
<div>Peer ID: <span id="pid"></span></div>
@ -31,25 +51,8 @@
<div id="in_status"></div>
<button disabled type="submit" id="answer_btn">ANSWER</button>
</fieldset>
<fieldset>
<legend>Call</legend>
<div id="call_status"></div>
<label>Audio</label>
<select id="in_audio">
</select>
<br />
<label>Video</label>
<select id="in_video">
</select>
<br />
</fieldset>
<div id="tracks"></div>
<script>
const eStatus = document.getElementById('status');
const ePid = document.getElementById('pid');
@ -70,6 +73,19 @@
let incommingCall = null;
let currentVideoTrackId = null;
let currentAudioTrackId = null;
let selfBoxRemove = null;
let ctx = null;
setInterval(() => {
if(ctx === null) return;
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, 160, 90);
setTimeout(() => {
if(ctx === null) return;
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 160, 90);
}, 100);
}, 200);
function closeLocalMediaStream() {
for(const track of localMediaStream.getTracks()) {
@ -78,6 +94,7 @@
}
async function createLocalMediaStream() {
console.log('creating stream');
if(localMediaStream !== null) {
closeLocalMediaStream();
}
@ -100,22 +117,22 @@
width: 160,
height: 90
});
canvas.getContext("2d").fillRect(0, 0, 160, 90);
ctx = canvas.getContext("2d");
const blankStream = canvas.captureStream();
const videoTrack = blankStream.getVideoTracks()[0];
console.log('empty tracks', blankStream, videoTrack);
localMediaStream.addTrack(videoTrack);
}
updateOutgoing();
};
console.dir(eAudioOptions);
async function updateOutgoing() {
await createLocalMediaStream();
for(const peerId in connections) {
const { call } = connections[peerId];
const { call, completed } = connections[peerId];
if(!completed) continue;
if(call.peerConnection === undefined) debugger;
const audioTrack = localMediaStream.getTracks()
.filter(track => track.kind === 'audio')[0];
const videoTrack = localMediaStream.getTracks()
@ -128,12 +145,21 @@
call.peerConnection.getSenders()
.filter(sender => sender.track.kind === 'video')[0]
.replaceTrack(videoTrack)
}
selfBoxRemove?.();
if(Object.keys(connections.filter(c => c.completed)).length === 0) {
selfBoxRemove = addVideoBox(localMediaStream, 'Self', () => {
alert('DISCO ALL CONNS');
}).remove;
} else {
selfBoxRemove = null;
}
}
eAudioOptions.onchange = updateOutgoing;
eVideoOptions.onchange = updateOutgoing;
eAudioOptions.onchange = createLocalMediaStream;
eVideoOptions.onchange = createLocalMediaStream;
// get permissions and enumerate devices
(async function() {
@ -145,8 +171,7 @@
track.stop();
}
const devices = await navigator.mediaDevices.enumerateDevices();
console.log(devices);
for(const device of devices.filter(v => v.kind === 'audioinput')) {
const elem = document.createElement('option');
elem.innerText = device.label;
@ -236,24 +261,7 @@
onCall(call.peer);
}
function onCall(peerId) {
const { call, conn, mediaStream, completed } = connections[peerId];
if(!call || !conn || !mediaStream) return;
if(completed) return;
connections[peerId].completed = true;
console.log('fully connected!');
eOutgoingStatus.innerText = 'Inactive';
eCallBtn.disabled = false;
eCallPid.value = '';
eIncommingStatus.innerText = '';
eAnswerBtn.disabled = true;
// while(eTracks.firstChild) {
// eTracks.removeChild(eTracks.firstChild);
// }
function addVideoBox(stream, name, end) {
const root = document.createElement('fieldset');
const elem = document.createElement('video');
const legend = document.createElement('legend');
@ -262,15 +270,12 @@
elem.autoplay = true;
elem.controls = true;
elem.style.width = '100%';
elem.srcObject = mediaStream;
elem.srcObject = stream;
legend.innerText = peerId;
legend.innerText = name;
endBtn.innerText = "END";
endBtn.addEventListener('click', () => {
conn.close();
call.close();
});
endBtn.addEventListener('click', end);
// root.appendChild(document.createElement('br'));
root.appendChild(legend);
@ -280,14 +285,44 @@
eTracks.appendChild(root);
conn.on('close', () => {
while(root.firstChild) {
root.removeChild(root.firstChild);
return {
remove() {
while(root.firstChild) {
root.removeChild(root.firstChild);
}
root.remove();
}
root.remove();
delete connections[peerId];
if(Object.keys(connections).length === 0) closeLocalMediaStream();
});
}
}
function onCall(peerId) {
const { call, conn, mediaStream, completed } = connections[peerId];
if(!call || !conn || !mediaStream) return;
if(completed) return;
connections[peerId].completed = true;
eOutgoingStatus.innerText = 'Inactive';
eCallBtn.disabled = false;
eCallPid.value = '';
eIncommingStatus.innerText = '';
eAnswerBtn.disabled = true;
}
function updateRemoteBoxes() {
for(const peerId in connections.filter(c => c.completed)) {
const { mediaStream, call, conn } = connections[peerId];
const { remove } = addVideoBox(mediaStream, peerId.split('-')[0], () => {
conn.close();
call.close();
});
conn.on('close', () => {
remove();
delete connections[peerId];
if(Object.keys(connections).length === 0) closeLocalMediaStream();
});
}
}
// eEndBtn.addEventListener('click', () => {