From a016e1d980f779127fbfd8fcd0bbc119616ff776 Mon Sep 17 00:00:00 2001 From: Declan Chidlow Date: Wed, 28 Aug 2024 10:32:00 +0800 Subject: [PATCH] Remove voice --- src/components/common/ChannelIcon.tsx | 6 +- src/components/common/user/UserIcon.tsx | 32 -- .../modals/components/CreateChannel.tsx | 27 +- src/lib/ContextMenus.tsx | 14 +- src/lib/vortex/Signaling.ts | 192 ---------- src/lib/vortex/Types.ts | 111 ------ src/lib/vortex/VoiceClient.ts | 340 ------------------ src/lib/vortex/VoiceState.ts | 210 ----------- src/mobx/stores/NotificationOptions.ts | 1 - src/pages/channels/Channel.tsx | 14 - src/pages/channels/actions/HeaderActions.tsx | 2 - src/pages/channels/voice/VoiceHeader.tsx | 179 --------- src/pages/friends/Friend.tsx | 15 - src/pages/settings/Settings.tsx | 12 - src/pages/settings/panes/Audio.tsx | 272 -------------- 15 files changed, 8 insertions(+), 1419 deletions(-) delete mode 100644 src/lib/vortex/Signaling.ts delete mode 100644 src/lib/vortex/Types.ts delete mode 100644 src/lib/vortex/VoiceClient.ts delete mode 100644 src/lib/vortex/VoiceState.ts delete mode 100644 src/pages/channels/voice/VoiceHeader.tsx delete mode 100644 src/pages/settings/panes/Audio.tsx diff --git a/src/components/common/ChannelIcon.tsx b/src/components/common/ChannelIcon.tsx index d11d6633..e3c63d36 100644 --- a/src/components/common/ChannelIcon.tsx +++ b/src/components/common/ChannelIcon.tsx @@ -37,14 +37,10 @@ export default observer( const isServerChannel = server || (target && - (target.channel_type === "TextChannel" || - target.channel_type === "VoiceChannel")); + (target.channel_type === "TextChannel")); if (typeof iconURL === "undefined") { if (isServerChannel) { - if (target?.channel_type === "VoiceChannel") { - return ; - } return ; } } diff --git a/src/components/common/user/UserIcon.tsx b/src/components/common/user/UserIcon.tsx index 778e1492..f78f4a16 100644 --- a/src/components/common/user/UserIcon.tsx +++ b/src/components/common/user/UserIcon.tsx @@ -1,8 +1,6 @@ -import { VolumeMute, MicrophoneOff } from "@styled-icons/boxicons-solid"; import { observer } from "mobx-react-lite"; import { useParams } from "react-router-dom"; import { User, API } from "revolt.js"; -import styled, { css } from "styled-components/macro"; import { useApplicationState } from "../../../mobx/State"; @@ -11,11 +9,9 @@ import fallback from "../assets/user.png"; import { useClient } from "../../../controllers/client/ClientController"; import IconBase, { IconBaseProps } from "../IconBase"; -type VoiceStatus = "muted" | "deaf"; interface Props extends IconBaseProps { status?: boolean; override?: string; - voice?: VoiceStatus; masquerade?: API.Masquerade; showServerIdentity?: boolean; } @@ -34,22 +30,6 @@ export function useStatusColour(user?: User) { : theme.getVariable("status-invisible"); } -const VoiceIndicator = styled.div<{ status: VoiceStatus }>` - width: 10px; - height: 10px; - border-radius: var(--border-radius-half); - - display: flex; - align-items: center; - justify-content: center; - - ${(props) => - (props.status === "muted" || props.status === "deaf") && - css` - background: var(--error); - `} -`; - export default observer( ( props: Props & @@ -131,18 +111,6 @@ export default observer( fill={useStatusColour(target)} /> )} - {props.voice && ( - - - {(props.voice === "deaf" && ( - - )) || - (props.voice === "muted" && ( - - ))} - - - )} ); }, diff --git a/src/controllers/modals/components/CreateChannel.tsx b/src/controllers/modals/components/CreateChannel.tsx index e1d00e65..0967dec3 100644 --- a/src/controllers/modals/components/CreateChannel.tsx +++ b/src/controllers/modals/components/CreateChannel.tsx @@ -22,7 +22,6 @@ export default function CreateChannel({ title={} schema={{ name: "text", - type: "radio", }} data={{ name: { @@ -30,32 +29,10 @@ export default function CreateChannel({ ) as React.ReactChild, }, - type: { - field: ( - - ) as React.ReactChild, - choices: [ - { - name: ( - - ) as React.ReactChild, - value: "Text", - }, - { - name: ( - - ) as React.ReactChild, - value: "Voice", - }, - ], - }, }} - defaults={{ - type: "Text", - }} - callback={async ({ name, type }) => { + callback={async ({ name }) => { const channel = await target.createChannel({ - type: type as "Text" | "Voice", + type: "Text", name, }); diff --git a/src/lib/ContextMenus.tsx b/src/lib/ContextMenus.tsx index 55c2dbb7..b51fd8cc 100644 --- a/src/lib/ContextMenus.tsx +++ b/src/lib/ContextMenus.tsx @@ -1,5 +1,5 @@ import { ChevronRight, Trash } from "@styled-icons/boxicons-regular"; -import { Cog, UserVoice } from "@styled-icons/boxicons-solid"; +import { Cog } from "@styled-icons/boxicons-solid"; import { isFirefox } from "react-device-detect"; import { useHistory } from "react-router-dom"; import { Channel, Message, Server, User, API, Permission, UserPermission, Member } from "revolt.js"; @@ -163,8 +163,7 @@ export default function ContextMenus() { case "mark_as_read": { if ( - data.channel.channel_type === "SavedMessages" || - data.channel.channel_type === "VoiceChannel" + data.channel.channel_type === "SavedMessages" ) return; @@ -574,8 +573,7 @@ export default function ContextMenus() { const user = uid ? client.users.get(uid) : undefined; const serverChannel = targetChannel && - (targetChannel.channel_type === "TextChannel" || - targetChannel.channel_type === "VoiceChannel") + (targetChannel.channel_type === "TextChannel") ? targetChannel : undefined; @@ -961,7 +959,7 @@ export default function ContextMenus() { pushDivider(); if (channel) { - if (channel.channel_type !== "VoiceChannel") { + if (channel.channel_type) { generateAction( { action: "open_notification_options", @@ -998,7 +996,6 @@ export default function ContextMenus() { }); break; case "TextChannel": - case "VoiceChannel": if ( channelPermissions & Permission.InviteOthers @@ -1275,7 +1272,6 @@ export default function ContextMenus() { - {client.user!.status?.text && ( @@ -1293,4 +1289,4 @@ export default function ContextMenus() { ); -} \ No newline at end of file +} diff --git a/src/lib/vortex/Signaling.ts b/src/lib/vortex/Signaling.ts deleted file mode 100644 index 16973c95..00000000 --- a/src/lib/vortex/Signaling.ts +++ /dev/null @@ -1,192 +0,0 @@ -import EventEmitter from "eventemitter3"; - -import { - RtpCapabilities, - RtpParameters, -} from "mediasoup-client/lib/RtpParameters"; -import { DtlsParameters } from "mediasoup-client/lib/Transport"; - -import { - AuthenticationResult, - Room, - TransportInitDataTuple, - WSCommandType, - WSErrorCode, - ProduceType, - ConsumerData, -} from "./Types"; - -interface SignalingEvents { - open: (event: Event) => void; - close: (event: CloseEvent) => void; - error: (event: Event) => void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data: (data: any) => void; -} - -export default class Signaling extends EventEmitter { - ws?: WebSocket; - index: number; - pending: Map void>; - - constructor() { - super(); - this.index = 0; - this.pending = new Map(); - } - - connected(): boolean { - return ( - this.ws !== undefined && - this.ws.readyState !== WebSocket.CLOSING && - this.ws.readyState !== WebSocket.CLOSED - ); - } - - connect(address: string): Promise { - this.disconnect(); - this.ws = new WebSocket(address); - this.ws.onopen = (e) => this.emit("open", e); - this.ws.onclose = (e) => this.emit("close", e); - this.ws.onerror = (e) => this.emit("error", e); - this.ws.onmessage = (e) => this.parseData(e); - - let finished = false; - return new Promise((resolve, reject) => { - this.once("open", () => { - if (finished) return; - finished = true; - resolve(); - }); - - this.once("error", () => { - if (finished) return; - finished = true; - reject(); - }); - }); - } - - disconnect() { - if ( - this.ws !== undefined && - this.ws.readyState !== WebSocket.CLOSED && - this.ws.readyState !== WebSocket.CLOSING - ) - this.ws.close(1000); - } - - private parseData(event: MessageEvent) { - if (typeof event.data !== "string") return; - const json = JSON.parse(event.data); - const entry = this.pending.get(json.id); - if (entry === undefined) { - this.emit("data", json); - return; - } - - entry(json); - } - - /* eslint-disable @typescript-eslint/no-explicit-any */ - sendRequest(type: string, data?: any): Promise { - if (this.ws === undefined || this.ws.readyState !== WebSocket.OPEN) - return Promise.reject({ error: WSErrorCode.NotConnected }); - - const ws = this.ws; - return new Promise((resolve, reject) => { - if (this.index >= 2 ** 32) this.index = 0; - while (this.pending.has(this.index)) this.index++; - const onClose = (e: CloseEvent) => { - reject({ - error: e.code, - message: e.reason, - }); - }; - - const finishedFn = (data: any) => { - this.removeListener("close", onClose); - if (data.error) - reject({ - error: data.error, - message: data.message, - data: data.data, - }); - resolve(data.data); - }; - - this.pending.set(this.index, finishedFn); - this.once("close", onClose); - const json = { - id: this.index, - type, - data, - }; - ws.send(`${JSON.stringify(json)}\n`); - this.index++; - }); - } - /* eslint-enable @typescript-eslint/no-explicit-any */ - - authenticate(token: string, roomId: string): Promise { - return this.sendRequest(WSCommandType.Authenticate, { token, roomId }); - } - - async roomInfo(): Promise { - const room = await this.sendRequest(WSCommandType.RoomInfo); - return { - id: room.id, - videoAllowed: room.videoAllowed, - users: new Map(Object.entries(room.users)), - }; - } - - initializeTransports( - rtpCapabilities: RtpCapabilities, - ): Promise { - return this.sendRequest(WSCommandType.InitializeTransports, { - mode: "SplitWebRTC", - rtpCapabilities, - }); - } - - connectTransport( - id: string, - dtlsParameters: DtlsParameters, - ): Promise { - return this.sendRequest(WSCommandType.ConnectTransport, { - id, - dtlsParameters, - }); - } - - async startProduce( - type: ProduceType, - rtpParameters: RtpParameters, - ): Promise { - const result = await this.sendRequest(WSCommandType.StartProduce, { - type, - rtpParameters, - }); - return result.producerId; - } - - stopProduce(type: ProduceType): Promise { - return this.sendRequest(WSCommandType.StopProduce, { type }); - } - - startConsume(userId: string, type: ProduceType): Promise { - return this.sendRequest(WSCommandType.StartConsume, { type, userId }); - } - - stopConsume(consumerId: string): Promise { - return this.sendRequest(WSCommandType.StopConsume, { id: consumerId }); - } - - setConsumerPause(consumerId: string, paused: boolean): Promise { - return this.sendRequest(WSCommandType.SetConsumerPause, { - id: consumerId, - paused, - }); - } -} diff --git a/src/lib/vortex/Types.ts b/src/lib/vortex/Types.ts deleted file mode 100644 index 7cee61c7..00000000 --- a/src/lib/vortex/Types.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Consumer } from "mediasoup-client/lib/Consumer"; -import { - MediaKind, - RtpCapabilities, - RtpParameters, -} from "mediasoup-client/lib/RtpParameters"; -import { SctpParameters } from "mediasoup-client/lib/SctpParameters"; -import { - DtlsParameters, - IceCandidate, - IceParameters, -} from "mediasoup-client/lib/Transport"; - -export enum WSEventType { - UserJoined = "UserJoined", - UserLeft = "UserLeft", - - UserStartProduce = "UserStartProduce", - UserStopProduce = "UserStopProduce", -} - -export enum WSCommandType { - Authenticate = "Authenticate", - RoomInfo = "RoomInfo", - - InitializeTransports = "InitializeTransports", - ConnectTransport = "ConnectTransport", - - StartProduce = "StartProduce", - StopProduce = "StopProduce", - - StartConsume = "StartConsume", - StopConsume = "StopConsume", - SetConsumerPause = "SetConsumerPause", -} - -export enum WSErrorCode { - NotConnected = 0, - NotFound = 404, - - TransportConnectionFailure = 601, - - ProducerFailure = 611, - ProducerNotFound = 614, - - ConsumerFailure = 621, - ConsumerNotFound = 624, -} - -export enum WSCloseCode { - // Sent when the received data is not a string, or is unparseable - InvalidData = 1003, - Unauthorized = 4001, - RoomClosed = 4004, - // Sent when a client tries to send an opcode in the wrong state - InvalidState = 1002, - ServerError = 1011, -} - -export interface VoiceError { - error: WSErrorCode | WSCloseCode; - message: string; -} - -export type ProduceType = "audio"; //| "video" | "saudio" | "svideo"; - -export interface AuthenticationResult { - userId: string; - roomId: string; - rtpCapabilities: RtpCapabilities; -} - -export interface Room { - id: string; - videoAllowed: boolean; - users: Map; -} - -export interface VoiceUser { - audio?: boolean; - //video?: boolean, - //saudio?: boolean, - //svideo?: boolean, -} - -export interface ConsumerList { - audio?: Consumer; - //video?: Consumer, - //saudio?: Consumer, - //svideo?: Consumer, -} - -export interface TransportInitData { - id: string; - iceParameters: IceParameters; - iceCandidates: IceCandidate[]; - dtlsParameters: DtlsParameters; - sctpParameters: SctpParameters | undefined; -} - -export interface TransportInitDataTuple { - sendTransport: TransportInitData; - recvTransport: TransportInitData; -} - -export interface ConsumerData { - id: string; - producerId: string; - kind: MediaKind; - rtpParameters: RtpParameters; -} diff --git a/src/lib/vortex/VoiceClient.ts b/src/lib/vortex/VoiceClient.ts deleted file mode 100644 index b84e31eb..00000000 --- a/src/lib/vortex/VoiceClient.ts +++ /dev/null @@ -1,340 +0,0 @@ -import EventEmitter from "eventemitter3"; -import * as mediasoupClient from "mediasoup-client"; -import { types } from "mediasoup-client"; - -import { Device, Producer, Transport } from "mediasoup-client/lib/types"; - -import { useApplicationState } from "../../mobx/State"; - -import Signaling from "./Signaling"; -import { - ProduceType, - WSEventType, - VoiceError, - VoiceUser, - ConsumerList, - WSErrorCode, -} from "./Types"; - -const UnsupportedError = types.UnsupportedError; - -interface VoiceEvents { - ready: () => void; - error: (error: Error) => void; - close: (error?: VoiceError) => void; - - startProduce: (type: ProduceType) => void; - stopProduce: (type: ProduceType) => void; - - userJoined: (userId: string) => void; - userLeft: (userId: string) => void; - - userStartProduce: (userId: string, type: ProduceType) => void; - userStopProduce: (userId: string, type: ProduceType) => void; -} - -export default class VoiceClient extends EventEmitter { - private _supported: boolean; - - device?: Device; - signaling: Signaling; - - sendTransport?: Transport; - recvTransport?: Transport; - - isDeaf?: boolean; - - userId?: string; - roomId?: string; - participants: Map; - consumers: Map; - - audioProducer?: Producer; - constructor() { - super(); - this._supported = mediasoupClient.detectDevice() !== undefined; - this.signaling = new Signaling(); - - this.participants = new Map(); - this.consumers = new Map(); - - this.isDeaf = false; - - const state = useApplicationState(); - - this.signaling.on( - "data", - (json) => { - const data = json.data; - switch (json.type) { - case WSEventType.UserJoined: { - this.participants.set(data.id, {}); - state.settings.sounds.playSound("call_join"); - this.emit("userJoined", data.id); - break; - } - case WSEventType.UserLeft: { - this.participants.delete(data.id); - state.settings.sounds.playSound("call_leave"); - this.emit("userLeft", data.id); - - if (this.recvTransport) this.stopConsume(data.id); - break; - } - case WSEventType.UserStartProduce: { - const user = this.participants.get(data.id); - if (user === undefined) return; - switch (data.type) { - case "audio": - user.audio = true; - break; - default: - throw new Error( - `Invalid produce type ${data.type}`, - ); - } - - if (this.recvTransport) - this.startConsume(data.id, data.type); - this.emit("userStartProduce", data.id, data.type); - break; - } - case WSEventType.UserStopProduce: { - const user = this.participants.get(data.id); - if (user === undefined) return; - switch (data.type) { - case "audio": - user.audio = false; - break; - default: - throw new Error( - `Invalid produce type ${data.type}`, - ); - } - - if (this.recvTransport) - this.stopConsume(data.id, data.type); - this.emit("userStopProduce", data.id, data.type); - break; - } - } - }, - this, - ); - - this.signaling.on( - "error", - () => { - this.emit("error", new Error("Signaling error")); - }, - this, - ); - - this.signaling.on( - "close", - (error) => { - this.disconnect( - { - error: error.code, - message: error.reason, - }, - true, - ); - }, - this, - ); - } - - supported() { - return this._supported; - } - throwIfUnsupported() { - if (!this._supported) throw new UnsupportedError("RTC not supported"); - } - - connect(address: string, roomId: string) { - this.throwIfUnsupported(); - this.device = new Device(); - this.roomId = roomId; - return this.signaling.connect(address); - } - - disconnect(error?: VoiceError, ignoreDisconnected?: boolean) { - if (!this.signaling.connected() && !ignoreDisconnected) return; - this.signaling.disconnect(); - this.participants = new Map(); - this.consumers = new Map(); - this.userId = undefined; - this.roomId = undefined; - - this.audioProducer = undefined; - - if (this.sendTransport) this.sendTransport.close(); - if (this.recvTransport) this.recvTransport.close(); - this.sendTransport = undefined; - this.recvTransport = undefined; - - this.emit("close", error); - } - - async authenticate(token: string) { - this.throwIfUnsupported(); - if (this.device === undefined || this.roomId === undefined) - throw new ReferenceError("Voice Client is in an invalid state"); - const result = await this.signaling.authenticate(token, this.roomId); - const [room] = await Promise.all([ - this.signaling.roomInfo(), - this.device.load({ routerRtpCapabilities: result.rtpCapabilities }), - ]); - - this.userId = result.userId; - this.participants = room.users; - } - - async initializeTransports() { - this.throwIfUnsupported(); - if (this.device === undefined) - throw new ReferenceError("Voice Client is in an invalid state"); - const initData = await this.signaling.initializeTransports( - this.device.rtpCapabilities, - ); - - this.sendTransport = this.device.createSendTransport( - initData.sendTransport, - ); - this.recvTransport = this.device.createRecvTransport( - initData.recvTransport, - ); - - const connectTransport = (transport: Transport) => { - transport.on("connect", ({ dtlsParameters }, callback, errback) => { - this.signaling - .connectTransport(transport.id, dtlsParameters) - .then(callback) - .catch(errback); - }); - }; - - connectTransport(this.sendTransport); - connectTransport(this.recvTransport); - - this.sendTransport.on("produce", (parameters, callback, errback) => { - const type = parameters.appData.type; - if ( - parameters.kind === "audio" && - type !== "audio" && - type !== "saudio" - ) - return errback(); - if ( - parameters.kind === "video" && - type !== "video" && - type !== "svideo" - ) - return errback(); - this.signaling - .startProduce(type, parameters.rtpParameters) - .then((id) => callback({ id })) - .catch(errback); - }); - - this.emit("ready"); - for (const user of this.participants) { - if (user[1].audio && user[0] !== this.userId) - this.startConsume(user[0], "audio"); - } - } - - private async startConsume(userId: string, type: ProduceType) { - if (this.recvTransport === undefined) - throw new Error("Receive transport undefined"); - const consumers = this.consumers.get(userId) || {}; - const consumerParams = await this.signaling.startConsume(userId, type); - const consumer = await this.recvTransport.consume(consumerParams); - switch (type) { - case "audio": - consumers.audio = consumer; - } - - const mediaStream = new MediaStream([consumer.track]); - const audio = new Audio(); - audio.srcObject = mediaStream; - await this.signaling.setConsumerPause(consumer.id, false); - audio.play(); - this.consumers.set(userId, consumers); - } - - private async stopConsume(userId: string, type?: ProduceType) { - const consumers = this.consumers.get(userId); - if (consumers === undefined) return; - if (type === undefined) { - if (consumers.audio !== undefined) consumers.audio.close(); - this.consumers.delete(userId); - } else { - switch (type) { - case "audio": { - if (consumers.audio !== undefined) { - consumers.audio.close(); - this.signaling.stopConsume(consumers.audio.id); - } - consumers.audio = undefined; - break; - } - } - - this.consumers.set(userId, consumers); - } - } - - async startProduce(track: MediaStreamTrack, type: ProduceType) { - if (this.sendTransport === undefined) - throw new Error("Send transport undefined"); - const producer = await this.sendTransport.produce({ - track, - appData: { type }, - }); - - switch (type) { - case "audio": - this.audioProducer = producer; - break; - } - - const participant = this.participants.get(this.userId || ""); - if (participant !== undefined) { - participant[type] = true; - this.participants.set(this.userId || "", participant); - } - - this.emit("startProduce", type); - } - - async stopProduce(type: ProduceType) { - let producer; - switch (type) { - case "audio": - producer = this.audioProducer; - this.audioProducer = undefined; - break; - } - - if (producer !== undefined) { - producer.close(); - this.emit("stopProduce", type); - } - - const participant = this.participants.get(this.userId || ""); - if (participant !== undefined) { - participant[type] = false; - this.participants.set(this.userId || "", participant); - } - - try { - await this.signaling.stopProduce(type); - } catch (error) { - // eslint-disable-next-line - if ((error as any).error === WSErrorCode.ProducerNotFound) return; - throw error; - } - } -} diff --git a/src/lib/vortex/VoiceState.ts b/src/lib/vortex/VoiceState.ts deleted file mode 100644 index f284820b..00000000 --- a/src/lib/vortex/VoiceState.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { action, makeAutoObservable, runInAction } from "mobx"; -import { Channel, Nullable, toNullable } from "revolt.js"; - -import type { ProduceType, VoiceUser } from "./Types"; -import type VoiceClient from "./VoiceClient"; - -export enum VoiceStatus { - LOADING = 0, - UNAVAILABLE, - ERRORED, - READY = 3, - CONNECTING = 4, - UNLOADED = 5, - AUTHENTICATING, - RTC_CONNECTING, - CONNECTED, - // RECONNECTING -} - -// This is an example of how to implement MobX state. -// * Note for better implementation: -// * MobX state should be implemented on the VoiceClient itself. -class VoiceStateReference { - client?: VoiceClient; - connecting?: boolean; - - status: VoiceStatus; - roomId: Nullable; - participants: Map; - - constructor() { - this.roomId = null; - this.status = VoiceStatus.UNLOADED; - this.participants = new Map(); - - this.syncState = this.syncState.bind(this); - this.connect = this.connect.bind(this); - this.disconnect = this.disconnect.bind(this); - - makeAutoObservable(this, { - client: false, - connecting: false, - }); - } - - // This takes information from the voice - // client and applies it to the state here. - @action syncState() { - if (!this.client) return; - this.roomId = toNullable(this.client.roomId); - this.participants.clear(); - this.client.participants.forEach((v, k) => this.participants.set(k, v)); - } - - // This imports and constructs the voice client. - @action async loadVoice() { - if (this.status !== VoiceStatus.UNLOADED) return; - this.status = VoiceStatus.LOADING; - - try { - const { default: VoiceClient } = await import("./VoiceClient"); - const client = new VoiceClient(); - - client.on("startProduce", this.syncState); - client.on("stopProduce", this.syncState); - client.on("userJoined", this.syncState); - client.on("userLeft", this.syncState); - client.on("userStartProduce", this.syncState); - client.on("userStopProduce", this.syncState); - - runInAction(() => { - if (!client.supported()) { - this.status = VoiceStatus.UNAVAILABLE; - } else { - this.status = VoiceStatus.READY; - this.client = client; - } - }); - } catch (err) { - console.error("Failed to load voice library!", err); - runInAction(() => { - this.status = VoiceStatus.UNAVAILABLE; - }); - } - } - - // Connect to a voice channel. - @action async connect(channel: Channel) { - if (!this.client?.supported()) throw new Error("RTC is unavailable"); - - this.connecting = true; - this.status = VoiceStatus.CONNECTING; - - try { - const call = await channel.joinCall(); - - await this.client.connect( - channel.client.configuration!.features.voso.ws, - channel._id, - ); - - runInAction(() => { - this.status = VoiceStatus.AUTHENTICATING; - }); - - await this.client.authenticate(call.token); - this.syncState(); - - runInAction(() => { - this.status = VoiceStatus.RTC_CONNECTING; - }); - - await this.client.initializeTransports(); - } catch (err) { - console.error(err); - - runInAction(() => { - this.status = VoiceStatus.READY; - }); - - return channel; - } - - runInAction(() => { - this.status = VoiceStatus.CONNECTED; - this.connecting = false; - }); - - return channel; - } - - // Disconnect from current channel. - @action disconnect() { - this.connecting = false; - this.status = VoiceStatus.READY; - - this.client?.disconnect(); - this.syncState(); - } - - isProducing(type: ProduceType) { - switch (type) { - case "audio": - return this.client?.audioProducer !== undefined; - } - } - - isDeaf() { - if (!this.client) return false; - - return this.client.isDeaf; - } - - async startDeafen() { - if (!this.client) return console.log("No client object"); // ! TODO: let the user know - if (this.client.isDeaf) return; - - this.client.isDeaf = true; - this.client?.consumers.forEach((consumer) => { - consumer.audio?.pause(); - }); - - this.syncState(); - } - async stopDeafen() { - if (!this.client) return console.log("No client object"); // ! TODO: let the user know - if (!this.client.isDeaf) return; - - this.client.isDeaf = false; - this.client?.consumers.forEach((consumer) => { - consumer.audio?.resume(); - }); - - this.syncState(); - } - - async startProducing(type: ProduceType) { - switch (type) { - case "audio": { - if (this.client?.audioProducer !== undefined) - return console.log("No audio producer."); // ! TODO: let the user know - - if (navigator.mediaDevices === undefined) - return console.log("No media devices."); // ! TODO: let the user know - - const mediaDevice = - window.localStorage.getItem("audioInputDevice"); - - const mediaStream = await navigator.mediaDevices.getUserMedia({ - audio: mediaDevice ? { deviceId: mediaDevice } : true, - }); - - await this.client?.startProduce( - mediaStream.getAudioTracks()[0], - "audio", - ); - } - } - - this.syncState(); - } - - async stopProducing(type: ProduceType) { - await this.client?.stopProduce(type); - - this.syncState(); - } -} - -export const voiceState = new VoiceStateReference(); diff --git a/src/mobx/stores/NotificationOptions.ts b/src/mobx/stores/NotificationOptions.ts index d0b79c94..9433a2a0 100644 --- a/src/mobx/stores/NotificationOptions.ts +++ b/src/mobx/stores/NotificationOptions.ts @@ -30,7 +30,6 @@ export const DEFAULT_STATES: { DirectMessage: "all", Group: "all", TextChannel: undefined!, - VoiceChannel: undefined!, }; /** diff --git a/src/pages/channels/Channel.tsx b/src/pages/channels/Channel.tsx index 3545fd55..55318ad5 100644 --- a/src/pages/channels/Channel.tsx +++ b/src/pages/channels/Channel.tsx @@ -26,7 +26,6 @@ import { PageHeader } from "../../components/ui/Header"; import { useClient } from "../../controllers/client/ClientController"; import ChannelHeader from "./ChannelHeader"; import { MessageArea } from "./messaging/MessageArea"; -import VoiceHeader from "./voice/VoiceHeader"; const ChannelMain = styled.div.attrs({ "data-component": "channel" })` flex-grow: 1; @@ -126,9 +125,6 @@ export const Channel = observer( } const channel = client.channels.get(id)!; - if (channel.channel_type === "VoiceChannel") { - return ; - } return ; }, @@ -191,7 +187,6 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => { - @@ -208,15 +203,6 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => { ); }); -function VoiceChannel({ channel }: { channel: ChannelI }) { - return ( - <> - - - - ); -} - function ChannelPlaceholder() { return ( diff --git a/src/pages/channels/actions/HeaderActions.tsx b/src/pages/channels/actions/HeaderActions.tsx index cf13311b..8eeb8aec 100644 --- a/src/pages/channels/actions/HeaderActions.tsx +++ b/src/pages/channels/actions/HeaderActions.tsx @@ -16,7 +16,6 @@ import { IconButton } from "@revoltchat/ui"; import { chainedDefer, defer } from "../../../lib/defer"; import { internalEmit } from "../../../lib/eventEmitter"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; -import { voiceState, VoiceStatus } from "../../../lib/vortex/VoiceState"; import { useApplicationState } from "../../../mobx/State"; import { SIDEBAR_MEMBERS } from "../../../mobx/stores/Layout"; @@ -132,7 +131,6 @@ export default function HeaderActions({ channel }: ChannelHeaderProps) { )} - {(channel.channel_type === "Group" || channel.channel_type === "TextChannel") && ( diff --git a/src/pages/channels/voice/VoiceHeader.tsx b/src/pages/channels/voice/VoiceHeader.tsx deleted file mode 100644 index d9666b80..00000000 --- a/src/pages/channels/voice/VoiceHeader.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { - BarChartAlt2, - Microphone, - MicrophoneOff, - PhoneOff, - VolumeFull, - VolumeMute, -} from "@styled-icons/boxicons-solid"; -import { observer } from "mobx-react-lite"; -import styled from "styled-components/macro"; - -import { Text } from "preact-i18n"; -import { useMemo } from "preact/hooks"; - -import { Button } from "@revoltchat/ui"; - -import { voiceState, VoiceStatus } from "../../../lib/vortex/VoiceState"; - -import Tooltip from "../../../components/common/Tooltip"; -import UserIcon from "../../../components/common/user/UserIcon"; -import { useClient } from "../../../controllers/client/ClientController"; -import { modalController } from "../../../controllers/modals/ModalController"; - -interface Props { - id: string; -} - -const VoiceBase = styled.div` - margin-top: 48px; - padding: 20px; - background: var(--secondary-background); - flex-grow: 1; - - .status { - flex: 1 0; - display: flex; - position: absolute; - align-items: center; - - padding: 10px; - font-size: 13px; - font-weight: 500; - user-select: none; - gap: 6px; - - color: var(--success); - border-radius: var(--border-radius); - background: var(--primary-background); - - svg { - cursor: help; - } - } - - display: flex; - flex-direction: column; - - .participants { - margin: 40px 20px; - justify-content: center; - user-select: none; - display: flex; - flex-flow: row wrap; - gap: 16px; - - div:hover img { - opacity: 0.8; - } - - .disconnected { - opacity: 0.5; - } - } - - .actions { - display: flex; - justify-content: center; - gap: 10px; - } -`; - -export default observer(({ id }: Props) => { - if (voiceState.roomId !== id) return null; - - const client = useClient(); - const self = client.users.get(client.user!._id); - - const keys = Array.from(voiceState.participants.keys()); - const users = useMemo(() => { - return keys.map((key) => client.users.get(key)); - // eslint-disable-next-line - }, [keys]); - - return ( - -
- {users && users.length !== 0 - ? users.map((user, index) => { - const user_id = keys![index]; - return ( -
- - modalController.push({ - type: "user_profile", - user_id, - }) - } - /> -
- ); - }) - : self !== undefined && ( -
- -
- )} -
-
- - {voiceState.status === VoiceStatus.CONNECTED && ( - - )} -
-
- - - - {voiceState.isProducing("audio") ? ( - - - - ) : ( - - - - )} - {voiceState.isDeaf() ? ( - - - - ) : ( - - - - )} -
-
- ); -}); diff --git a/src/pages/friends/Friend.tsx b/src/pages/friends/Friend.tsx index 42bf61ff..e64183eb 100644 --- a/src/pages/friends/Friend.tsx +++ b/src/pages/friends/Friend.tsx @@ -12,7 +12,6 @@ import { Text } from "preact-i18n"; import { IconButton } from "@revoltchat/ui"; import { stopPropagation } from "../../lib/stopPropagation"; -import { voiceState } from "../../lib/vortex/VoiceState"; import UserIcon from "../../components/common/user/UserIcon"; import UserStatus from "../../components/common/user/UserStatus"; @@ -32,20 +31,6 @@ export const Friend = observer(({ user }: Props) => { subtext = ; actions.push( <> - - stopPropagation( - ev, - user - .openDM() - .then(voiceState.connect) - .then((x) => history.push(`/channel/${x._id}`)), - ) - }> - - { icon: , title: , }, - { - category: ( - - ), - id: "audio", - icon: , - title: , - }, { id: "appearance", icon: , @@ -229,9 +220,6 @@ export default observer(() => { - - diff --git a/src/pages/settings/panes/Audio.tsx b/src/pages/settings/panes/Audio.tsx deleted file mode 100644 index f65d07c0..00000000 --- a/src/pages/settings/panes/Audio.tsx +++ /dev/null @@ -1,272 +0,0 @@ -import styles from "./Panes.module.scss"; -import { Text } from "preact-i18n"; -import { useEffect, useState } from "preact/hooks"; - -import { Button, Category, ComboBox, Tip } from "@revoltchat/ui"; - -import { stopPropagation } from "../../../lib/stopPropagation"; -import { voiceState } from "../../../lib/vortex/VoiceState"; - -import { I18nError } from "../../../context/Locale"; - -import opusSVG from "../assets/opus_logo.svg"; - -{ - /*import OpusSVG from "../assets/opus_logo.svg";*/ -} - -const constraints = { audio: true }; - -// TODO: do not rewrite this code until voice is rewritten! - -export function Audio() { - const [mediaStream, setMediaStream] = useState( - undefined, - ); - const [mediaDevices, setMediaDevices] = useState< - MediaDeviceInfo[] | undefined - >(undefined); - const [permission, setPermission] = useState( - undefined, - ); - const [error, setError] = useState(undefined); - - const askOrGetPermission = async () => { - try { - const result = await navigator.mediaDevices.getUserMedia( - constraints, - ); - - setMediaStream(result); - } catch (err) { - // The user has blocked the permission - setError(err as DOMException); - } - - try { - const { state } = await navigator.permissions.query({ - // eslint-disable-next-line - // @ts-ignore: very few browsers accept this `PermissionName`, but it has been drafted in https://www.w3.org/TR/permissions/#powerful-features-registry - name: "microphone", - }); - - setPermission(state); - } catch (err) { - // the browser might not support `query` functionnality or `PermissionName` - // nothing to do - } - }; - - useEffect(() => { - return () => { - if (mediaStream) { - // close microphone access on unmount - mediaStream.getTracks().forEach((track) => { - track.stop(); - }); - } - }; - }, [mediaStream]); - - useEffect(() => { - if (!mediaStream) { - return; - } - - navigator.mediaDevices.enumerateDevices().then( - (devices) => { - setMediaDevices(devices); - }, - (err) => { - setError(err as DOMException); - }, - ); - }, [mediaStream]); - - const handleAskForPermission = ( - ev: JSX.TargetedMouseEvent, - ) => { - stopPropagation(ev); - setError(undefined); - askOrGetPermission(); - }; - - return ( - <> -
- - - We are currently{" "} - - rebuilding the client - {" "} - and{" "} - - the voice server - {" "} - from scratch. -
-
- The old voice should work in most cases, but it may - inexplicably not connect in some scenarios and / or - exhibit weird behaviour. -
-
- - {!permission && ( - - - - )} - - {error && permission === "prompt" && ( - - - - - - . - - )} - -
-
-

- -

-
- - changeAudioDevice( - e.currentTarget.value, - "input", - ) - }> - {mediaDevices - ?.filter( - (device) => - device.kind === "audioinput", - ) - .map((device) => { - return ( - - ); - })} - - {/*TOFIX: add logic to sound notches*/} - {/*
-
-
-
-
-
-
-
-
-
-
-
*/} - {!permission && ( - - )} - {error && error.name === "NotAllowedError" && ( - - - - )} -
-
-
-

- -

- {/* TOFIX: create audio output combobox*/} - - changeAudioDevice( - e.currentTarget.value, - "output", - ) - }> - {mediaDevices - ?.filter( - (device) => device.kind === "audiooutput", - ) - .map((device) => { - return ( - - ); - })} - - {/*
-
-
-
-
-
-
-
-
-
-
-
*/} -
-
-
-
-
- - Audio codec powered by Opus -
- - ); -} - -function changeAudioDevice(deviceId: string, deviceType: string) { - if (deviceType === "input") { - window.localStorage.setItem("audioInputDevice", deviceId); - if (voiceState.isProducing("audio")) { - voiceState.stopProducing("audio"); - voiceState.startProducing("audio"); - } - } else if (deviceType === "output") { - window.localStorage.setItem("audioOutputDevice", deviceId); - } -}