revite/src/context/Voice.tsx
2021-07-05 11:25:20 +01:00

207 lines
6.9 KiB
TypeScript

import { createContext } from "preact";
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import type { ProduceType, VoiceUser } from "../lib/vortex/Types";
import type VoiceClient from "../lib/vortex/VoiceClient";
import { Children } from "../types/Preact";
import { SoundContext } from "./Settings";
import { AppContext } from "./revoltjs/RevoltClient";
import { useForceUpdate } from "./revoltjs/hooks";
export enum VoiceStatus {
LOADING = 0,
UNAVAILABLE,
ERRORED,
READY = 3,
CONNECTING = 4,
AUTHENTICATING,
RTC_CONNECTING,
CONNECTED,
// RECONNECTING
}
export interface VoiceOperations {
connect: (channelId: string) => Promise<void>;
disconnect: () => void;
isProducing: (type: ProduceType) => boolean;
startProducing: (type: ProduceType) => Promise<void>;
stopProducing: (type: ProduceType) => Promise<void> | undefined;
}
export interface VoiceState {
roomId?: string;
status: VoiceStatus;
participants?: Readonly<Map<string, VoiceUser>>;
}
// They should be present from first render. - insert's words
export const VoiceContext = createContext<VoiceState>(null!);
export const VoiceOperationsContext = createContext<VoiceOperations>(null!);
type Props = {
children: Children;
};
export default function Voice({ children }: Props) {
const revoltClient = useContext(AppContext);
const [client, setClient] = useState<VoiceClient | undefined>(undefined);
const [state, setState] = useState<VoiceState>({
status: VoiceStatus.LOADING,
participants: new Map(),
});
function setStatus(status: VoiceStatus, roomId?: string) {
setState({
status,
roomId: roomId ?? client?.roomId,
participants: client?.participants ?? new Map(),
});
}
useEffect(() => {
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);
const operations: VoiceOperations = useMemo(() => {
return {
connect: async (channelId) => {
if (!client?.supported()) throw new Error("RTC is unavailable");
isConnecting.current = true;
setStatus(VoiceStatus.CONNECTING, channelId);
try {
const call = await revoltClient.channels.joinCall(
channelId,
);
if (!isConnecting.current) {
setStatus(VoiceStatus.READY);
return;
}
// ! 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",
channelId,
);
setStatus(VoiceStatus.AUTHENTICATING);
await client.authenticate(call.token);
setStatus(VoiceStatus.RTC_CONNECTING);
await client.initializeTransports();
} catch (error) {
console.error(error);
setStatus(VoiceStatus.READY);
return;
}
setStatus(VoiceStatus.CONNECTED);
isConnecting.current = false;
},
disconnect: () => {
if (!client?.supported()) throw new Error("RTC is unavailable");
// if (status <= VoiceStatus.READY) return;
// this will not update in this context
isConnecting.current = false;
client.disconnect();
setStatus(VoiceStatus.READY);
},
isProducing: (type: ProduceType) => {
switch (type) {
case "audio":
return client?.audioProducer !== undefined;
}
},
startProducing: async (type: ProduceType) => {
switch (type) {
case "audio": {
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,
});
await client?.startProduce(
mediaStream.getAudioTracks()[0],
"audio",
);
return;
}
}
},
stopProducing: (type: ProduceType) => {
return client?.stopProduce(type);
},
};
}, [client]);
const { forceUpdate } = useForceUpdate();
const playSound = useContext(SoundContext);
useEffect(() => {
if (!client?.supported()) return;
// ! 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", () => {
playSound("call_join");
forceUpdate();
});
client.on("userLeft", () => {
playSound("call_leave");
forceUpdate();
});
client.on("userStartProduce", forceUpdate);
client.on("userStopProduce", forceUpdate);
client.on("close", forceUpdate);
return () => {
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, state]);
return (
<VoiceContext.Provider value={state}>
<VoiceOperationsContext.Provider value={operations}>
{children}
</VoiceOperationsContext.Provider>
</VoiceContext.Provider>
);
}