From eef3e11e62ac490389b08b69d67478862535c5bf Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 24 Jun 2021 10:54:32 +0100 Subject: [PATCH] Load mediasoup client and add voice UI. --- src/context/Voice.tsx | 42 ++++-- src/pages/channels/Channel.tsx | 3 + src/pages/channels/voice/VoiceHeader.tsx | 183 +++++++++++++++++++++++ src/pages/invite/Invite.tsx | 1 + 4 files changed, 216 insertions(+), 13 deletions(-) create mode 100644 src/pages/channels/voice/VoiceHeader.tsx diff --git a/src/context/Voice.tsx b/src/context/Voice.tsx index e78b0918..30d6bbbe 100644 --- a/src/context/Voice.tsx +++ b/src/context/Voice.tsx @@ -1,5 +1,6 @@ import { createContext } from "preact"; import { Children } from "../types/Preact"; +import { useForceUpdate } from "./revoltjs/hooks"; import { AppContext } from "./revoltjs/RevoltClient"; import type VoiceClient from "../lib/vortex/VoiceClient"; import type { ProduceType, VoiceUser } from "../lib/vortex/Types"; @@ -40,7 +41,7 @@ type Props = { export default function Voice({ children }: Props) { const revoltClient = useContext(AppContext); - const [client,] = useState(undefined); + const [client, setClient] = useState(undefined); const [state, setState] = useState({ status: VoiceStatus.LOADING, participants: new Map() @@ -55,11 +56,21 @@ export default function Voice({ children }: Props) { } useEffect(() => { - if (!client?.supported()) { - setStatus(VoiceStatus.UNAVAILABLE); - } else { - setStatus(VoiceStatus.READY); - } + import('../lib/vortex/VoiceClient') + .then(({ default: VoiceClient }) => { + const client = new VoiceClient(); + setClient(client); + + if (!client?.supported()) { + setStatus(VoiceStatus.UNAVAILABLE); + } else { + setStatus(VoiceStatus.READY); + } + }) + .catch(err => { + console.error('Failed to load voice library!', err); + setStatus(VoiceStatus.UNAVAILABLE); + }) }, []); const isConnecting = useRef(false); @@ -83,7 +94,7 @@ export default function Voice({ children }: Props) { } // ! FIXME: use configuration to check if voso is enabled - //await client.connect("wss://voso.revolt.chat/ws"); + // await client.connect("wss://voso.revolt.chat/ws"); await client.connect("wss://voso.revolt.chat/ws", channelId); setStatus(VoiceStatus.AUTHENTICATING); @@ -120,8 +131,8 @@ export default function Voice({ children }: Props) { startProducing: async (type: ProduceType) => { switch (type) { case "audio": { - if (client?.audioProducer !== undefined) return; - if (navigator.mediaDevices === undefined) return; + if (client?.audioProducer !== undefined) return console.log('No audio producer.'); // ! FIXME: let the user know + if (navigator.mediaDevices === undefined) return console.log('No media devices.'); // ! FIXME: let the user know const mediaStream = await navigator.mediaDevices.getUserMedia( { audio: true @@ -142,27 +153,32 @@ export default function Voice({ children }: Props) { } }, [ client ]); + const { forceUpdate } = useForceUpdate(); useEffect(() => { if (!client?.supported()) return; - /* client.on("startProduce", forceUpdate); + // ! FIXME: message for fatal: + // ! get rid of these force updates + // ! handle it through state or smth + + client.on("startProduce", forceUpdate); client.on("stopProduce", forceUpdate); client.on("userJoined", forceUpdate); client.on("userLeft", forceUpdate); client.on("userStartProduce", forceUpdate); client.on("userStopProduce", forceUpdate); - client.on("close", forceUpdate); */ + client.on("close", forceUpdate); return () => { - /* client.removeListener("startProduce", forceUpdate); + client.removeListener("startProduce", forceUpdate); client.removeListener("stopProduce", forceUpdate); client.removeListener("userJoined", forceUpdate); client.removeListener("userLeft", forceUpdate); client.removeListener("userStartProduce", forceUpdate); client.removeListener("userStopProduce", forceUpdate); - client.removeListener("close", forceUpdate); */ + client.removeListener("close", forceUpdate); }; }, [ client, state ]); diff --git a/src/pages/channels/Channel.tsx b/src/pages/channels/Channel.tsx index 70a7d02b..6732f54e 100644 --- a/src/pages/channels/Channel.tsx +++ b/src/pages/channels/Channel.tsx @@ -11,6 +11,7 @@ import MemberSidebar from "../../components/navigation/right/MemberSidebar"; import JumpToBottom from "../../components/common/messaging/bars/JumpToBottom"; import TypingIndicator from "../../components/common/messaging/bars/TypingIndicator"; import { Channel } from "revolt.js"; +import VoiceHeader from "./voice/VoiceHeader"; const ChannelMain = styled.div` flex-grow: 1; @@ -48,6 +49,7 @@ function TextChannel({ channel }: { channel: Channel }) { setMembers(!showMembers)} /> + @@ -61,6 +63,7 @@ function TextChannel({ channel }: { channel: Channel }) { function VoiceChannel({ channel }: { channel: Channel }) { return <> + ; } diff --git a/src/pages/channels/voice/VoiceHeader.tsx b/src/pages/channels/voice/VoiceHeader.tsx new file mode 100644 index 00000000..b99b0f0b --- /dev/null +++ b/src/pages/channels/voice/VoiceHeader.tsx @@ -0,0 +1,183 @@ +import { Text } from "preact-i18n"; +import styled from "styled-components"; +import { useContext } from "preact/hooks"; +import { BarChart } from "@styled-icons/bootstrap"; +import Button from "../../../components/ui/Button"; +import UserIcon from "../../../components/common/user/UserIcon"; +import { useForceUpdate, useSelf, useUsers } from "../../../context/revoltjs/hooks"; +import { VoiceContext, VoiceOperationsContext, VoiceStatus } from "../../../context/Voice"; + +interface Props { + id: string +} + +const VoiceBase = styled.div` + padding: 20px; + background: var(--secondary-background); + + .status { + position: absolute; + color: var(--success); + background: var(--primary-background); + display: flex; + align-items: center; + padding: 10px; + font-size: 14px; + font-weight: 600; + border-radius: 7px; + flex: 1 0; + user-select: none; + + svg { + margin-right: 4px; + cursor: help; + } + } + + display: flex; + flex-direction: column; + + .participants { + margin: 20px 0; + justify-content: center; + pointer-events: none; + user-select: none; + display: flex; + gap: 16px; + + .disconnected { + opacity: 0.5; + } + } + + .actions { + display: flex; + justify-content: center; + gap: 10px; + } +`; + +export default function VoiceHeader({ id }: Props) { + const { status, participants, roomId } = useContext(VoiceContext); + if (roomId !== id) return null; + + const { isProducing, startProducing, stopProducing, disconnect } = useContext(VoiceOperationsContext); + + const ctx = useForceUpdate(); + const self = useSelf(ctx); + const keys = participants ? Array.from(participants.keys()) : undefined; + const users = keys ? useUsers(keys, ctx) : undefined; + + return ( + +
+ { users && users.length !== 0 ? users.map((user, index) => { + const id = keys![index]; + return ( +
+ +
+ ); + }) : self !== undefined && ( +
+ +
+ )} +
+
+ + { status === VoiceStatus.CONNECTED && } +
+
+ + { isProducing("audio") ? ( + + ) : ( + + )} +
+
+ ) +} + +/**{voice.roomId === id && ( +
+
+ {participants.length !== 0 ? participants.map((user, index) => { + const id = participantIds[index]; + return ( +
+ +
+ ); + }) : self !== undefined && ( +
+ +
+ )} +
+
+ + { voice.status === VoiceStatus.CONNECTED && } +
+
+ + {voice.operations.isProducing("audio") ? ( + + ) : ( + + )} +
+
+ )} */ diff --git a/src/pages/invite/Invite.tsx b/src/pages/invite/Invite.tsx index 831895d6..080f3cbd 100644 --- a/src/pages/invite/Invite.tsx +++ b/src/pages/invite/Invite.tsx @@ -40,6 +40,7 @@ export default function Invite() { ) } + // ! FIXME: add i18n translations return (