diff --git a/src/equicordplugins/followVoiceUser/index.tsx b/src/equicordplugins/followVoiceUser/index.tsx
index 49c6f52a..0fc0c393 100644
--- a/src/equicordplugins/followVoiceUser/index.tsx
+++ b/src/equicordplugins/followVoiceUser/index.tsx
@@ -11,7 +11,7 @@ import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
-import { Forms, Menu, React } from "@webpack/common";
+import { Forms, Menu, React, VoiceStateStore } from "@webpack/common";
import { VoiceState } from "@webpack/types";
import { Channel, User } from "discord-types/general";
@@ -29,7 +29,6 @@ interface UserContextProps {
let followedUserInfo: TFollowedUserInfo = null;
const voiceChannelAction = findByPropsLazy("selectVoiceChannel");
-const VoiceStateStore = findStoreLazy("VoiceStateStore");
const UserStore = findStoreLazy("UserStore");
const RelationshipStore = findStoreLazy("RelationshipStore");
diff --git a/src/equicordplugins/orbolayBridge/index.tsx b/src/equicordplugins/orbolayBridge/index.tsx
new file mode 100644
index 00000000..e4165fa7
--- /dev/null
+++ b/src/equicordplugins/orbolayBridge/index.tsx
@@ -0,0 +1,314 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 OpenAsar
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { definePluginSettings } from "@api/Settings";
+import { EquicordDevs } from "@utils/constants";
+import definePlugin, { OptionType } from "@utils/types";
+import { ChannelStore, FluxDispatcher, GuildMemberStore, Toasts, UserStore, VoiceStateStore } from "@webpack/common";
+
+interface ChannelState {
+ userId: string;
+ channelId: string;
+ deaf: boolean;
+ mute: boolean;
+ stream: boolean;
+ selfDeaf: boolean;
+ selfMute: boolean;
+ selfStream: boolean;
+}
+
+const settings = definePluginSettings({
+ port: {
+ type: OptionType.NUMBER,
+ description: "Port to connect to",
+ default: 6888,
+ restartNeeded: true
+ },
+ messageAlignment: {
+ type: OptionType.SELECT,
+ description: "Alignment of messages in the overlay",
+ options: [
+ { label: "Top left", value: "topleft", default: true },
+ { label: "Top right", value: "topright" },
+ { label: "Bottom left", value: "bottomleft" },
+ { label: "Bottom right", value: "bottomright" },
+ ],
+ default: "topright",
+ restartNeeded: true
+ },
+ userAlignment: {
+ type: OptionType.SELECT,
+ description: "Alignment of users in the overlay",
+ options: [
+ { label: "Top left", value: "topleft", default: true },
+ { label: "Top right", value: "topright" },
+ { label: "Bottom left", value: "bottomleft" },
+ { label: "Bottom right", value: "bottomright" },
+ ],
+ default: "topleft",
+ restartNeeded: true
+ },
+ voiceSemitransparent: {
+ type: OptionType.BOOLEAN,
+ description: "Make voice channel members transparent",
+ default: true,
+ restartNeeded: true
+ },
+ messagesSemitransparent: {
+ type: OptionType.BOOLEAN,
+ description: "Make message notifications transparent",
+ default: false,
+ restartNeeded: true
+ },
+});
+
+let ws: WebSocket | null = null;
+let currentChannel = null;
+
+const waitForPopulate = async fn => {
+ while (true) {
+ const result = await fn();
+ if (result) return result;
+ await new Promise(r => setTimeout(r, 500));
+ }
+};
+
+function stateToPayload(guildId: string, state: ChannelState) {
+ const user = UserStore.getUser(state.userId);
+ const nickname = GuildMemberStore.getNick(guildId, state.userId);
+ return {
+ userId: state.userId,
+ username: nickname || (user as any).globalName || user.username,
+ avatarUrl: user.avatar,
+ channelId: state.channelId,
+ deaf: state.deaf || state.selfDeaf,
+ mute: state.mute || state.selfMute,
+ streaming: state.selfStream,
+ speaking: false,
+ };
+}
+
+const incoming = payload => {
+ switch (payload.cmd) {
+ case "TOGGLE_MUTE":
+ FluxDispatcher.dispatch({
+ type: "AUDIO_TOGGLE_SELF_MUTE",
+ syncRemote: true,
+ playSoundEffect: true,
+ context: "default"
+ });
+ break;
+ case "TOGGLE_DEAF":
+ FluxDispatcher.dispatch({
+ type: "AUDIO_TOGGLE_SELF_DEAF",
+ syncRemote: true,
+ playSoundEffect: true,
+ context: "default"
+ });
+ break;
+ case "DISCONNECT":
+ FluxDispatcher.dispatch({
+ type: "VOICE_CHANNEL_SELECT",
+ channelId: null
+ });
+ break;
+ case "STOP_STREAM": {
+ const userId = UserStore.getCurrentUser()?.id;
+ const voiceState = VoiceStateStore.getVoiceStateForUser(userId);
+ const channel = ChannelStore.getChannel(voiceState.channelId);
+
+ // If any of these are null, we can't do anything
+ if (!userId || !voiceState || !channel) return;
+
+ FluxDispatcher.dispatch({
+ type: "STREAM_STOP",
+ streamKey: `guild:${channel.guild_id}:${voiceState.channelId}:${userId}`,
+ appContext: "APP"
+ });
+ }
+ }
+};
+
+const handleSpeaking = dispatch => {
+ ws?.send(
+ JSON.stringify({
+ cmd: "VOICE_STATE_UPDATE",
+ state: {
+ userId: dispatch.userId,
+ speaking: dispatch.speakingFlags === 1,
+ },
+ })
+ );
+};
+
+const handleMessageNotification = dispatch => {
+ ws?.send(
+ JSON.stringify({
+ cmd: "MESSAGE_NOTIFICATION",
+ message: {
+ title: dispatch.title,
+ body: dispatch.body,
+ icon: dispatch.icon,
+ channelId: dispatch.channelId,
+ }
+ })
+ );
+};
+
+const handleVoiceStateUpdates = async dispatch => {
+ // Ensure we are in the channel that the update is for
+ const { id } = UserStore.getCurrentUser();
+
+ for (const state of dispatch.voiceStates) {
+ const ourState = state.userId === id;
+ const { guildId } = state;
+
+ if (ourState) {
+ if (state.channelId && state.channelId !== currentChannel) {
+ const voiceStates = await waitForPopulate(() =>
+ VoiceStateStore?.getVoiceStatesForChannel(state.channelId)
+ );
+
+ ws?.send(
+ JSON.stringify({
+ cmd: "CHANNEL_JOINED",
+ states: Object.values(voiceStates).map(s => stateToPayload(guildId, s as ChannelState)),
+ })
+ );
+
+ currentChannel = state.channelId;
+
+ break;
+ } else if (!state.channelId) {
+ ws?.send(
+ JSON.stringify({
+ cmd: "CHANNEL_LEFT",
+ })
+ );
+
+ currentChannel = null;
+
+ break;
+ }
+ }
+
+ // If this is for the channel we are in, send a VOICE_STATE_UPDATE
+ if (
+ !!currentChannel &&
+ (state.channelId === currentChannel ||
+ state.oldChannelId === currentChannel)
+ ) {
+ ws?.send(
+ JSON.stringify({
+ cmd: "VOICE_STATE_UPDATE",
+ state: stateToPayload(guildId, state as ChannelState),
+ })
+ );
+ }
+ }
+};
+
+const createWebsocket = () => {
+ console.log("Attempting to connect to Orbolay server");
+
+ // First ensure old connection is closed
+ if (ws?.close) ws.close();
+
+ setTimeout(() => {
+ // If the ws is not ready, kill it and log
+ if (ws?.readyState !== WebSocket.OPEN) {
+ Toasts.show({
+ message: "Orbolay websocket could not connect. Is it running?",
+ type: Toasts.Type.FAILURE,
+ id: Toasts.genId(),
+ });
+ ws = null;
+ return;
+ }
+ }, 1000);
+
+ ws = new WebSocket("ws://127.0.0.1:" + settings.store.port);
+ ws.onerror = e => {
+ ws?.close?.();
+ ws = null;
+ throw e;
+ };
+ ws.onmessage = e => {
+ incoming(JSON.parse(e.data));
+ };
+ ws.onclose = () => {
+ ws = null;
+ };
+ ws.onopen = async () => {
+ Toasts.show({
+ message: "Connected to Orbolay server",
+ type: Toasts.Type.SUCCESS,
+ id: Toasts.genId(),
+ });
+
+ // Send over the config
+ const config = {
+ ...settings.store,
+ userId: null,
+ };
+
+ // Ensure we track the current user id
+ config.userId = await waitForPopulate(() => UserStore.getCurrentUser().id);
+
+ ws?.send(JSON.stringify({ cmd: "REGISTER_CONFIG", ...config }));
+
+ // Send initial channel joined (if the user is in a channel)
+ const userVoiceState = VoiceStateStore.getVoiceStateForUser(config.userId);
+
+ if (!userVoiceState) return;
+
+ const channelState = VoiceStateStore.getVoiceStatesForChannel(userVoiceState.channelId);
+ const { guildId } = userVoiceState;
+
+ ws?.send(
+ JSON.stringify({
+ cmd: "CHANNEL_JOINED",
+ states: Object.values(channelState).map(s => stateToPayload(guildId, s as ChannelState)),
+ })
+ );
+
+ currentChannel = userVoiceState.channelId;
+ };
+};
+
+export default definePlugin({
+ name: "Orbolay Bridge",
+ description: "Bridge plugin to connect Orbolay to Discord",
+ authors: [EquicordDevs.SpikeHD],
+ settings,
+ start() {
+ createWebsocket();
+
+ FluxDispatcher.subscribe("SPEAKING", handleSpeaking);
+ FluxDispatcher.subscribe("VOICE_STATE_UPDATES", handleVoiceStateUpdates);
+ FluxDispatcher.subscribe("RPC_NOTIFICATION_CREATE", handleMessageNotification);
+ },
+ stop() {
+ ws?.close();
+ ws = null;
+
+ FluxDispatcher.unsubscribe("SPEAKING", handleSpeaking);
+ FluxDispatcher.unsubscribe("VOICE_STATE_UPDATES", handleVoiceStateUpdates);
+ FluxDispatcher.unsubscribe("RPC_NOTIFICATION_CREATE", handleMessageNotification);
+ }
+});
diff --git a/src/equicordplugins/randomVoice/index.tsx b/src/equicordplugins/randomVoice/index.tsx
index 96ea92f9..c7d5cb87 100644
--- a/src/equicordplugins/randomVoice/index.tsx
+++ b/src/equicordplugins/randomVoice/index.tsx
@@ -12,13 +12,12 @@ import { debounce } from "@shared/debounce";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByCode, findByProps, findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
-import { ChannelRouter, ChannelStore, ContextMenuApi, GuildStore, Menu, PermissionsBits, PermissionStore, React, SelectedChannelStore, Toasts, UserStore } from "@webpack/common";
+import { ChannelRouter, ChannelStore, ContextMenuApi, GuildStore, Menu, PermissionsBits, PermissionStore, React, SelectedChannelStore, Toasts, UserStore, VoiceStateStore } from "@webpack/common";
import style from "./styles.css?managed";
const ChatVoiceIcon = findComponentByCodeLazy("0l1.8-1.8c.17");
const Button = findComponentByCodeLazy(".NONE,disabled:", ".PANEL_BUTTON");
-const VoiceStateStore = findStoreLazy("VoiceStateStore");
const MediaEngineStore = findStoreLazy("MediaEngineStore");
const ChannelActions = findByPropsLazy("selectChannel", "selectVoiceChannel");
const { toggleSelfMute } = findByPropsLazy("toggleSelfMute");
diff --git a/src/equicordplugins/vcNarratorCustom/index.tsx b/src/equicordplugins/vcNarratorCustom/index.tsx
index 471974ca..fa8d8b92 100644
--- a/src/equicordplugins/vcNarratorCustom/index.tsx
+++ b/src/equicordplugins/vcNarratorCustom/index.tsx
@@ -11,7 +11,6 @@ import { wordsToTitle } from "@utils/text";
import definePlugin, {
OptionType,
} from "@utils/types";
-import { findByPropsLazy } from "@webpack";
import {
Button,
ChannelStore,
@@ -22,6 +21,7 @@ import {
SelectedGuildStore,
useMemo,
UserStore,
+ VoiceStateStore,
} from "@webpack/common";
// Create an in-memory cache (temporary, lost on restart)
@@ -77,11 +77,6 @@ interface VoiceState {
selfMute: boolean;
}
-const VoiceStateStore = findByPropsLazy(
- "getVoiceStatesForChannel",
- "getCurrentClientVoiceChannelId"
-);
-
// Mute/Deaf for other people than you is commented out, because otherwise someone can spam it and it will be annoying
// Filtering out events is not as simple as just dropping duplicates, as otherwise mute, unmute, mute would
// not say the second mute, which would lead you to believe they're unmuted
diff --git a/src/equicordplugins/voiceChatUtils/index.tsx b/src/equicordplugins/voiceChatUtils/index.tsx
index ec232cf6..3c3b3998 100644
--- a/src/equicordplugins/voiceChatUtils/index.tsx
+++ b/src/equicordplugins/voiceChatUtils/index.tsx
@@ -9,12 +9,9 @@ import { definePluginSettings } from "@api/Settings";
import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
-import { findStoreLazy } from "@webpack";
-import { GuildChannelStore, Menu, React, RestAPI, UserStore } from "@webpack/common";
+import { GuildChannelStore, Menu, React, RestAPI, UserStore, VoiceStateStore } from "@webpack/common";
import type { Channel } from "discord-types/general";
-const VoiceStateStore = findStoreLazy("VoiceStateStore");
-
async function runSequential(promises: Promise[]): Promise {
const results: T[] = [];
diff --git a/src/equicordplugins/voiceJoinMessages/index.ts b/src/equicordplugins/voiceJoinMessages/index.ts
index ea2048d4..f4adf6b9 100644
--- a/src/equicordplugins/voiceJoinMessages/index.ts
+++ b/src/equicordplugins/voiceJoinMessages/index.ts
@@ -8,12 +8,11 @@ import { definePluginSettings } from "@api/Settings";
import { Devs, EquicordDevs } from "@utils/constants";
import { humanFriendlyJoin } from "@utils/text";
import definePlugin, { OptionType } from "@utils/types";
-import { findByCodeLazy, findByPropsLazy } from "@webpack";
-import { ChannelStore, FluxDispatcher, MessageActions, MessageStore, RelationshipStore, SelectedChannelStore, UserStore } from "@webpack/common";
+import { findByCodeLazy } from "@webpack";
+import { ChannelStore, FluxDispatcher, MessageActions, MessageStore, RelationshipStore, SelectedChannelStore, UserStore, VoiceStateStore } from "@webpack/common";
import { Message, User } from "discord-types/general";
const createBotMessage = findByCodeLazy('username:"Clyde"');
-const SortedVoiceStateStore = findByPropsLazy("getVoiceStatesForChannel", "getCurrentClientVoiceChannelId");
const settings = definePluginSettings({
friendDirectMessages: {
@@ -127,7 +126,7 @@ export default definePlugin({
const selfInChannel = SelectedChannelStore.getVoiceChannelId() === channelId;
let memberListContent = "";
if (settings.store.friendDirectMessagesShowMembers || settings.store.friendDirectMessagesShowMemberCount) {
- const voiceState = SortedVoiceStateStore.getVoiceStatesForChannel(channelId);
+ const voiceState = VoiceStateStore.getVoiceStatesForChannel(channelId);
const sortedVoiceStates: User[] = Object.values(voiceState as { [key: string]: VoiceState; })
.filter((voiceState: VoiceState) => { voiceState.user && voiceState.user.id !== userId; })
.map((voiceState: VoiceState) => voiceState.user);
diff --git a/src/plugins/userVoiceShow/components.tsx b/src/plugins/userVoiceShow/components.tsx
index 136febd6..6f3780ef 100644
--- a/src/plugins/userVoiceShow/components.tsx
+++ b/src/plugins/userVoiceShow/components.tsx
@@ -7,8 +7,8 @@
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { classes } from "@utils/misc";
-import { filters, findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findStoreLazy, mapMangledModuleLazy } from "@webpack";
-import { ChannelRouter, ChannelStore, GuildStore, IconUtils, match, P, PermissionsBits, PermissionStore, React, showToast, Text, Toasts, Tooltip, useMemo, UserStore, useStateFromStores } from "@webpack/common";
+import { filters, findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, mapMangledModuleLazy } from "@webpack";
+import { ChannelRouter, ChannelStore, GuildStore, IconUtils, match, P, PermissionsBits, PermissionStore, React, showToast, Text, Toasts, Tooltip, useMemo, UserStore, useStateFromStores, VoiceStateStore } from "@webpack/common";
import { Channel } from "discord-types/general";
const cl = classNameFactory("vc-uvs-");
@@ -18,7 +18,6 @@ const { useChannelName } = mapMangledModuleLazy("#{intl::GROUP_DM_ALONE}", {
useChannelName: filters.byCode("()=>null==")
});
const getDMChannelIcon = findByCodeLazy(".getChannelIconURL({");
-const VoiceStateStore = findStoreLazy("VoiceStateStore");
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
const Avatar = findComponentByCodeLazy(".status)/2):0");
diff --git a/src/plugins/vcNarrator/index.tsx b/src/plugins/vcNarrator/index.tsx
index bd4685e2..bd75c112 100644
--- a/src/plugins/vcNarrator/index.tsx
+++ b/src/plugins/vcNarrator/index.tsx
@@ -22,8 +22,7 @@ import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { wordsToTitle } from "@utils/text";
import definePlugin, { ReporterTestable } from "@utils/types";
-import { findByPropsLazy } from "@webpack";
-import { Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore } from "@webpack/common";
+import { Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore, VoiceStateStore } from "@webpack/common";
import { ReactElement } from "react";
import { getCurrentVoice, settings } from "./settings";
@@ -38,8 +37,6 @@ interface VoiceState {
selfMute: boolean;
}
-const VoiceStateStore = findByPropsLazy("getVoiceStatesForChannel", "getCurrentClientVoiceChannelId");
-
// Mute/Deaf for other people than you is commented out, because otherwise someone can spam it and it will be annoying
// Filtering out events is not as simple as just dropping duplicates, as otherwise mute, unmute, mute would
// not say the second mute, which would lead you to believe they're unmuted
diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts
index 0d24ad93..d00649c7 100644
--- a/src/webpack/common/stores.ts
+++ b/src/webpack/common/stores.ts
@@ -38,6 +38,7 @@ export let PermissionStore: GenericStore;
export let GuildChannelStore: GenericStore;
export let ReadStateStore: GenericStore;
export let PresenceStore: GenericStore;
+export let VoiceStateStore: GenericStore;
export let GuildStore: t.GuildStore;
export let UserStore: Stores.UserStore & t.FluxStore;
@@ -90,3 +91,4 @@ waitForStore("ThemeStore", m => {
// Importing this directly can easily cause circular imports. For this reason, use a non import access here.
Vencord.QuickCss.initQuickCssThemeStore();
});
+waitForStore("VoiceStateStore", m => VoiceStateStore = m);