{props.children}
-
-
+
+
{getIntlMessage("SPECTATORS", { numViewers: userIds.length })}
{users.length ?
- <>
+
)}
/>
- >
- : No spectators
+
+ :
+ No spectators
+
}
- >
+
);
}),
component: function ({ OriginalComponent }) {
- return ErrorBoundary.wrap((props: any) => {
- const stream = useStateFromStores(
- [ApplicationStreamingStore],
- () => ApplicationStreamingStore.getCurrentUserActiveStream()
- );
+ return ErrorBoundary.wrap(props => {
+ const stream = useStateFromStores([ApplicationStreamingStore], () => ApplicationStreamingStore.getCurrentUserActiveStream());
if (!stream) return null;
+
const viewers = ApplicationStreamingStore.getViewerIds(stream);
return
}>
{({ onMouseEnter, onMouseLeave }) => (
diff --git a/src/equicordplugins/whosWatching/styles.css b/src/equicordplugins/whosWatching/styles.css
new file mode 100644
index 000000000..66a965fb3
--- /dev/null
+++ b/src/equicordplugins/whosWatching/styles.css
@@ -0,0 +1,11 @@
+.vc-whos-watching-user-avatar {
+ border-radius: 8px;
+ width: 16px;
+ height: 16px;
+}
+
+.vc-whos-watching-spectating-header {
+ margin-top: 8px;
+ margin-bottom: 0;
+ text-transform: uppercase;
+}
\ No newline at end of file
From dd26c5ed0fb360e33b5aacc3c7ecd271a1496ecf Mon Sep 17 00:00:00 2001
From: Key
Date: Tue, 10 Mar 2026 22:04:35 +0000
Subject: [PATCH 019/153] VoiceRejoin: fix stuff (#900)
* voiceRejoin: fix potential race condition
* fix: localstorage
---
src/equicordplugins/voiceRejoin/index.tsx | 125 +++++++++++++---------
1 file changed, 73 insertions(+), 52 deletions(-)
diff --git a/src/equicordplugins/voiceRejoin/index.tsx b/src/equicordplugins/voiceRejoin/index.tsx
index ad882d0df..12042df43 100644
--- a/src/equicordplugins/voiceRejoin/index.tsx
+++ b/src/equicordplugins/voiceRejoin/index.tsx
@@ -7,11 +7,14 @@
import * as DataStore from "@api/DataStore";
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
+import { Logger } from "@utils/Logger";
import definePlugin, { makeRange, OptionType } from "@utils/types";
+import { VoiceState } from "@vencord/discord-types";
import { ChannelStore, FluxDispatcher, UserStore, VoiceStateStore } from "@webpack/common";
const DATASTORE_KEY = "VCLastVoiceChannel";
const DATASTORE_SESSION_KEY = "VCLastVoiceChannelSession";
+const logger = new Logger("VoiceRejoin");
const settings = definePluginSettings({
rejoinDelay: {
@@ -48,12 +51,15 @@ const settings = definePluginSettings({
export default definePlugin({
name: "VoiceRejoin",
description: "Rejoins DM/Server call automatically when restarting Discord.",
- authors: [EquicordDevs.omaw],
+ authors: [EquicordDevs.omaw, EquicordDevs.keircn],
settings,
flux: {
- VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: any[]; }) {
- const myUserId = UserStore.getCurrentUser().id;
+ VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
+ const currentUser = UserStore.getCurrentUser();
+ if (!currentUser) return;
+
+ const myUserId = currentUser.id;
const myState = voiceStates.find(s => s.userId === myUserId);
if (!myState) return;
@@ -63,77 +69,92 @@ export default definePlugin({
channelId: myState.channelId,
timestamp: Date.now(),
};
- DataStore.set(DATASTORE_KEY, saved);
- DataStore.set(DATASTORE_SESSION_KEY, true);
+ void Promise.all([
+ DataStore.set(DATASTORE_KEY, saved),
+ DataStore.set(DATASTORE_SESSION_KEY, true)
+ ]).catch(err => logger.error("Failed to persist last voice channel", err));
} else {
- DataStore.set(DATASTORE_SESSION_KEY, false);
+ void DataStore.set(DATASTORE_SESSION_KEY, false)
+ .catch(err => logger.error("Failed to persist voice session state", err));
}
},
async CONNECTION_OPEN() {
const wasInVC = await DataStore.get(DATASTORE_SESSION_KEY);
if (wasInVC === false) {
- DataStore.del(DATASTORE_KEY);
+ await DataStore.del(DATASTORE_KEY);
return;
}
setTimeout(async () => {
- const saved = await DataStore.get(DATASTORE_KEY);
- if (!saved?.channelId) return;
+ try {
+ const saved = await DataStore.get(DATASTORE_KEY);
+ if (!saved?.channelId) return;
- const channel = ChannelStore.getChannel(saved.channelId);
- const isDM = channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM();
- const myUserId = UserStore.getCurrentUser().id;
- const myVoiceState = VoiceStateStore.getVoiceStateForUser(myUserId);
- const preventionMode = settings.store.preventReconnectIfCallEnded;
- const timeoutMs = settings.store.rejoinTimeout * 1000;
+ let channel = ChannelStore.getChannel(saved.channelId);
+ for (let i = 0; i < 20 && !channel; i++) {
+ await new Promise(resolve => setTimeout(resolve, 250));
+ channel = ChannelStore.getChannel(saved.channelId);
+ }
- if (saved.timestamp && Date.now() - saved.timestamp > timeoutMs) {
- DataStore.set(DATASTORE_SESSION_KEY, false);
- return;
- }
-
- if (settings.store.applyOnlyToDms && !isDM) {
- DataStore.set(DATASTORE_SESSION_KEY, false);
- return;
- }
-
- if (preventionMode !== "none") {
if (!channel) {
- DataStore.set(DATASTORE_SESSION_KEY, false);
+ await DataStore.set(DATASTORE_SESSION_KEY, false);
return;
}
- const shouldPrevent =
- preventionMode === "both" ||
- (preventionMode === "dms" && isDM) ||
- (preventionMode === "servers" && !isDM);
+ const currentUser = UserStore.getCurrentUser();
+ if (!currentUser) return;
- if (shouldPrevent) {
- const connectedUsers = VoiceStateStore.getVoiceStatesForChannel(saved.channelId);
- const othersInCall = Object.values(connectedUsers).filter(
- (vs: any) => vs.userId !== UserStore.getCurrentUser().id
- );
+ const isDM = channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM();
+ const myUserId = currentUser.id;
+ const myVoiceState = VoiceStateStore.getVoiceStateForUser(myUserId);
+ const preventionMode = settings.store.preventReconnectIfCallEnded;
+ const timeoutMs = settings.store.rejoinTimeout * 1000;
- if (othersInCall.length === 0) {
- DataStore.set(DATASTORE_SESSION_KEY, false);
- return;
+ if (saved.timestamp && Date.now() - saved.timestamp > timeoutMs) {
+ await DataStore.set(DATASTORE_SESSION_KEY, false);
+ return;
+ }
+
+ if (settings.store.applyOnlyToDms && !isDM) {
+ await DataStore.set(DATASTORE_SESSION_KEY, false);
+ return;
+ }
+
+ if (preventionMode !== "none") {
+ const shouldPrevent =
+ preventionMode === "both" ||
+ (preventionMode === "dms" && isDM) ||
+ (preventionMode === "servers" && !isDM);
+
+ if (shouldPrevent) {
+ const connectedUsers = VoiceStateStore.getVoiceStatesForChannel(saved.channelId) as Record;
+ const othersInCall = Object.values(connectedUsers).filter(
+ vs => vs.userId !== myUserId
+ );
+
+ if (othersInCall.length === 0) {
+ await DataStore.set(DATASTORE_SESSION_KEY, false);
+ return;
+ }
}
}
+
+ if (myVoiceState?.channelId) {
+ await DataStore.set(DATASTORE_SESSION_KEY, false);
+ return;
+ }
+
+ FluxDispatcher.dispatch({
+ type: "VOICE_CHANNEL_SELECT",
+ guildId: saved.guildId,
+ channelId: saved.channelId,
+ });
+
+ await DataStore.set(DATASTORE_SESSION_KEY, true);
+ } catch (err) {
+ logger.error("Failed to run voice rejoin", err);
}
-
- if (myVoiceState?.channelId) {
- DataStore.set(DATASTORE_SESSION_KEY, false);
- return;
- }
-
- FluxDispatcher.dispatch({
- type: "VOICE_CHANNEL_SELECT",
- guildId: saved.guildId,
- channelId: saved.channelId,
- });
-
- DataStore.set(DATASTORE_SESSION_KEY, true);
}, settings.store.rejoinDelay * 1000);
},
},
From 6ccd7e34820b97ac44cacb214684490847374c22 Mon Sep 17 00:00:00 2001
From: creations
Date: Tue, 10 Mar 2026 18:08:32 -0400
Subject: [PATCH 020/153] equicloud v2 (#902)
* equicloud v2
* make sure v1 works too ( forgot about vencloud )
* lint
---
src/api/SettingsSync/cloudSetup.tsx | 13 +-
src/api/SettingsSync/cloudSync.ts | 595 +++++++++++++++++++++-------
src/api/SettingsSync/types.ts | 47 +++
3 files changed, 497 insertions(+), 158 deletions(-)
create mode 100644 src/api/SettingsSync/types.ts
diff --git a/src/api/SettingsSync/cloudSetup.tsx b/src/api/SettingsSync/cloudSetup.tsx
index 20a21859d..3cbc97656 100644
--- a/src/api/SettingsSync/cloudSetup.tsx
+++ b/src/api/SettingsSync/cloudSetup.tsx
@@ -96,19 +96,22 @@ export async function authorizeCloud() {
const res = await fetch(location, {
headers: { Accept: "application/json" }
});
- const { secret } = await res.json();
- if (secret) {
- logger.info("Authorized with secret");
- await setAuthorization(secret);
+ const data = await res.json();
+ if (data.secret) {
+ logger.info("Authorized with cloud");
+ await setAuthorization(data.secret);
showNotification({
title: "Cloud Integration",
body: "Cloud integrations enabled!"
});
Settings.cloud.authenticated = true;
} else {
+ logger.error("OAuth callback returned no secret", data);
showNotification({
title: "Cloud Integration",
- body: "Setup failed (no secret returned?)."
+ body: data.error
+ ? `Setup failed: ${data.error}`
+ : "Setup failed (no secret returned)."
});
Settings.cloud.authenticated = false;
}
diff --git a/src/api/SettingsSync/cloudSync.ts b/src/api/SettingsSync/cloudSync.ts
index 91dc199c0..da501d7e4 100644
--- a/src/api/SettingsSync/cloudSync.ts
+++ b/src/api/SettingsSync/cloudSync.ts
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
+import * as DataStore from "@api/DataStore";
import { showNotification } from "@api/Notifications";
import { PlainSettings, Settings } from "@api/Settings";
import { localStorage } from "@utils/localStorage";
@@ -14,196 +15,483 @@ import { deflateSync, inflateSync } from "fflate";
import { deauthorizeCloud, getCloudAuth, getCloudUrl } from "./cloudSetup";
import { exportSettings, importSettings } from "./offline";
+import { ManifestEntry, SyncRequest, SyncResponse } from "./types";
const logger = new Logger("SettingsSync:Cloud", "#39b7e0");
+const MANIFEST_STORE_KEY = "Vencord_cloudManifest";
+const API_VERSION_KEY = "Vencord_cloudApiVersion";
+
+type ApiVersion = "v2" | "v1";
+
+async function getApiVersion(): Promise {
+ return await DataStore.get(API_VERSION_KEY) ?? "v2";
+}
+
+async function setApiVersion(version: ApiVersion) {
+ await DataStore.set(API_VERSION_KEY, version);
+}
+
+function toBase64(data: Uint8Array): string {
+ let binary = "";
+ for (let i = 0; i < data.length; i++)
+ binary += String.fromCharCode(data[i]);
+ return btoa(binary);
+}
+
+function fromBase64(b64: string): Uint8Array {
+ const binary = atob(b64);
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++)
+ bytes[i] = binary.charCodeAt(i);
+ return bytes;
+}
+
+async function computeChecksum(data: Uint8Array): Promise {
+ const hash = await crypto.subtle.digest("SHA-256", new Uint8Array(data));
+ const bytes = new Uint8Array(hash, 0, 8);
+ return Array.from(bytes, b => b.toString(16).padStart(2, "0")).join("");
+}
+
+async function getLocalManifest(): Promise {
+ return await DataStore.get(MANIFEST_STORE_KEY) ?? [];
+}
+
+async function saveLocalManifest(manifest: ManifestEntry[]) {
+ await DataStore.set(MANIFEST_STORE_KEY, manifest);
+}
+
+async function buildLocalData(): Promise