Merge branch 'dev'
This commit is contained in:
commit
ff31cc0593
17 changed files with 735 additions and 309 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "equicord",
|
"name": "equicord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.14.4.1",
|
"version": "1.14.5.0",
|
||||||
"description": "The other cutest Discord client mod",
|
"description": "The other cutest Discord client mod",
|
||||||
"homepage": "https://github.com/Equicord/Equicord#readme",
|
"homepage": "https://github.com/Equicord/Equicord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export interface UserAndActivity {
|
||||||
activity: Activity;
|
activity: Activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DiscordPlatform = "desktop" | "mobile" | "web" | "embedded";
|
export type DiscordPlatform = "desktop" | "mobile" | "web" | "embedded" | "vr";
|
||||||
|
|
||||||
export interface PresenceStoreState {
|
export interface PresenceStoreState {
|
||||||
presencesForGuilds: Record<string, Record<string, { status: OnlineStatus; activities: Activity[]; clientStatus: Partial<Record<DiscordPlatform, OnlineStatus>>; }>>;
|
presencesForGuilds: Record<string, Record<string, { status: OnlineStatus; activities: Activity[]; clientStatus: Partial<Record<DiscordPlatform, OnlineStatus>>; }>>;
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,19 @@ export function migratePluginToSettings(deleteOldSettings: boolean, newName: str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function migrateSettingToPlugin(newName: string, oldName: string, settingName: string) {
|
||||||
|
const { plugins } = SettingsStore.plain;
|
||||||
|
const newPlugin = plugins[newName];
|
||||||
|
const oldPlugin = plugins[oldName];
|
||||||
|
|
||||||
|
if (newPlugin && oldPlugin?.enabled && oldPlugin?.[settingName]) {
|
||||||
|
logger.info(`Migrating setting ${settingName} from ${oldName} to seperate plugin ${newName}`);
|
||||||
|
delete oldPlugin[settingName];
|
||||||
|
newPlugin.enabled = true;
|
||||||
|
SettingsStore.markAsChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function migrateSettingsFromPlugin(newPlugin: string, oldPlugin: string, ...settings: string[]) {
|
export function migrateSettingsFromPlugin(newPlugin: string, oldPlugin: string, ...settings: string[]) {
|
||||||
const { plugins } = SettingsStore.plain;
|
const { plugins } = SettingsStore.plain;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,19 +96,22 @@ export async function authorizeCloud() {
|
||||||
const res = await fetch(location, {
|
const res = await fetch(location, {
|
||||||
headers: { Accept: "application/json" }
|
headers: { Accept: "application/json" }
|
||||||
});
|
});
|
||||||
const { secret } = await res.json();
|
const data = await res.json();
|
||||||
if (secret) {
|
if (data.secret) {
|
||||||
logger.info("Authorized with secret");
|
logger.info("Authorized with cloud");
|
||||||
await setAuthorization(secret);
|
await setAuthorization(data.secret);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Integration",
|
title: "Cloud Integration",
|
||||||
body: "Cloud integrations enabled!"
|
body: "Cloud integrations enabled!"
|
||||||
});
|
});
|
||||||
Settings.cloud.authenticated = true;
|
Settings.cloud.authenticated = true;
|
||||||
} else {
|
} else {
|
||||||
|
logger.error("OAuth callback returned no secret", data);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Integration",
|
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;
|
Settings.cloud.authenticated = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as DataStore from "@api/DataStore";
|
||||||
import { showNotification } from "@api/Notifications";
|
import { showNotification } from "@api/Notifications";
|
||||||
import { PlainSettings, Settings } from "@api/Settings";
|
import { PlainSettings, Settings } from "@api/Settings";
|
||||||
import { localStorage } from "@utils/localStorage";
|
import { localStorage } from "@utils/localStorage";
|
||||||
|
|
@ -14,196 +15,483 @@ import { deflateSync, inflateSync } from "fflate";
|
||||||
|
|
||||||
import { deauthorizeCloud, getCloudAuth, getCloudUrl } from "./cloudSetup";
|
import { deauthorizeCloud, getCloudAuth, getCloudUrl } from "./cloudSetup";
|
||||||
import { exportSettings, importSettings } from "./offline";
|
import { exportSettings, importSettings } from "./offline";
|
||||||
|
import { ManifestEntry, SyncRequest, SyncResponse } from "./types";
|
||||||
|
|
||||||
const logger = new Logger("SettingsSync:Cloud", "#39b7e0");
|
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<ApiVersion> {
|
||||||
|
return await DataStore.get<ApiVersion>(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<string> {
|
||||||
|
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<ManifestEntry[]> {
|
||||||
|
return await DataStore.get<ManifestEntry[]>(MANIFEST_STORE_KEY) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveLocalManifest(manifest: ManifestEntry[]) {
|
||||||
|
await DataStore.set(MANIFEST_STORE_KEY, manifest);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildLocalData(): Promise<Map<string, Uint8Array>> {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = new Map<string, Uint8Array>();
|
||||||
|
|
||||||
|
data.set("settings", encoder.encode(JSON.stringify(VencordNative.settings.get())));
|
||||||
|
|
||||||
|
const quickCss = await VencordNative.quickCss.get();
|
||||||
|
if (quickCss) data.set("quickCss", encoder.encode(quickCss));
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyDownloads(downloads: SyncResponse["downloads"]) {
|
||||||
|
if (downloads.length === 0) return false;
|
||||||
|
|
||||||
|
let settingsChanged = false;
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
for (const dl of downloads) {
|
||||||
|
const text = decoder.decode(fromBase64(dl.value));
|
||||||
|
|
||||||
|
if (dl.key === "settings") {
|
||||||
|
try {
|
||||||
|
await importSettings(JSON.stringify({ settings: JSON.parse(text) }), "all", true);
|
||||||
|
settingsChanged = true;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Failed to apply settings download", e);
|
||||||
|
}
|
||||||
|
} else if (dl.key === "quickCss") {
|
||||||
|
await VencordNative.quickCss.set(text);
|
||||||
|
settingsChanged = true;
|
||||||
|
} else if (dl.key.startsWith("dataStore/")) {
|
||||||
|
const dsKey = dl.key.slice("dataStore/".length);
|
||||||
|
try {
|
||||||
|
await DataStore.set(dsKey, JSON.parse(text));
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Failed to apply dataStore download for ${dsKey}`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return settingsChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAuthFailure() {
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: "Cloud sync was disabled because this account isn't connected. Reconnect in Cloud Settings.",
|
||||||
|
color: "var(--yellow-360)",
|
||||||
|
onClick: () => SettingsRouter.openUserSettings("equicord_cloud_panel"),
|
||||||
|
});
|
||||||
|
Settings.cloud.authenticated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doSyncV2(uploads: SyncRequest["uploads"], clientManifest: ManifestEntry[]): Promise<SyncResponse | null> {
|
||||||
|
let res: Response;
|
||||||
|
try {
|
||||||
|
res = await fetch(new URL("/v2/sync", getCloudUrl()), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: await getCloudAuth(),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ client_manifest: clientManifest, uploads } satisfies SyncRequest),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
logger.info("Server does not support v2, falling back to v1");
|
||||||
|
await setApiVersion("v1");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status === 404) {
|
||||||
|
logger.info("Server does not support v2, falling back to v1");
|
||||||
|
await setApiVersion("v1");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status === 401) {
|
||||||
|
handleAuthFailure();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
logger.error(`Sync failed, API returned ${res.status}`);
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: `Could not synchronize settings (API returned ${res.status}).`,
|
||||||
|
color: "var(--red-360)",
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function putV2(manual?: boolean) {
|
||||||
|
const localManifest = await getLocalManifest();
|
||||||
|
const manifestMap = new Map(localManifest.map(e => [e.key, e]));
|
||||||
|
|
||||||
|
const localData = await buildLocalData();
|
||||||
|
const uploads: SyncRequest["uploads"] = [];
|
||||||
|
|
||||||
|
for (const [key, value] of localData) {
|
||||||
|
const checksum = await computeChecksum(value);
|
||||||
|
const existing = manifestMap.get(key);
|
||||||
|
|
||||||
|
if (!existing || existing.checksum !== checksum)
|
||||||
|
uploads.push({ key, value: toBase64(value), checksum });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploads.length === 0 && !manual) {
|
||||||
|
logger.info("No changes to push");
|
||||||
|
delete localStorage.Vencord_settingsDirty;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await doSyncV2(uploads, localManifest);
|
||||||
|
if (!response) return;
|
||||||
|
|
||||||
|
for (const err of response.errors)
|
||||||
|
logger.error(`Sync error for ${err.key}: ${err.error}`);
|
||||||
|
|
||||||
|
const hadDownloads = await applyDownloads(response.downloads);
|
||||||
|
await saveLocalManifest(response.server_manifest);
|
||||||
|
|
||||||
|
PlainSettings.cloud.settingsSyncVersion = Date.now();
|
||||||
|
await VencordNative.settings.set(PlainSettings);
|
||||||
|
|
||||||
|
logger.info(`Sync complete: ${response.uploaded.length} uploaded, ${response.downloads.length} downloaded`);
|
||||||
|
|
||||||
|
if (manual) {
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: hadDownloads
|
||||||
|
? "Settings synced! Click here to restart to fully apply changes."
|
||||||
|
: "Settings synchronized to the cloud!",
|
||||||
|
color: "var(--green-360)",
|
||||||
|
onClick: hadDownloads ? (IS_WEB ? () => location.reload() : relaunch) : undefined,
|
||||||
|
noPersist: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delete localStorage.Vencord_settingsDirty;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getV2(shouldNotify: boolean, force: boolean) {
|
||||||
|
const localManifest = force ? [] : await getLocalManifest();
|
||||||
|
|
||||||
|
const response = await doSyncV2([], localManifest);
|
||||||
|
if (!response) return false;
|
||||||
|
|
||||||
|
for (const err of response.errors)
|
||||||
|
logger.error(`Sync error for ${err.key}: ${err.error}`);
|
||||||
|
|
||||||
|
if (response.downloads.length === 0) {
|
||||||
|
logger.info("Settings up to date");
|
||||||
|
if (shouldNotify)
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: "Your settings are up to date.",
|
||||||
|
noPersist: true,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsChanged = await applyDownloads(response.downloads);
|
||||||
|
await saveLocalManifest(response.server_manifest);
|
||||||
|
|
||||||
|
PlainSettings.cloud.settingsSyncVersion = Date.now();
|
||||||
|
await VencordNative.settings.set(PlainSettings);
|
||||||
|
|
||||||
|
logger.info(`Pulled ${response.downloads.length} keys from cloud`);
|
||||||
|
|
||||||
|
if (shouldNotify)
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: settingsChanged
|
||||||
|
? "Your settings have been updated! Click here to restart to fully apply changes!"
|
||||||
|
: "Cloud data synchronized.",
|
||||||
|
color: "var(--green-360)",
|
||||||
|
onClick: settingsChanged ? (IS_WEB ? () => location.reload() : relaunch) : undefined,
|
||||||
|
noPersist: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
delete localStorage.Vencord_settingsDirty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteV2() {
|
||||||
|
const auth = await getCloudAuth();
|
||||||
|
|
||||||
|
const manifestRes = await fetch(new URL("/v2/manifest", getCloudUrl()), {
|
||||||
|
headers: { Authorization: auth },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!manifestRes.ok) {
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: `Could not fetch manifest for deletion (API returned ${manifestRes.status}).`,
|
||||||
|
color: "var(--red-360)",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entries }: { entries: ManifestEntry[] } = await manifestRes.json();
|
||||||
|
|
||||||
|
await Promise.all(entries.map(async entry => {
|
||||||
|
const res = await fetch(new URL(`/v2/data/${encodeURIComponent(entry.key)}`, getCloudUrl()), {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { Authorization: auth },
|
||||||
|
});
|
||||||
|
if (!res.ok && res.status !== 404)
|
||||||
|
logger.error(`Failed to delete key ${entry.key}: ${res.status}`);
|
||||||
|
}));
|
||||||
|
|
||||||
|
await saveLocalManifest([]);
|
||||||
|
|
||||||
|
PlainSettings.cloud.settingsSyncVersion = 0;
|
||||||
|
await VencordNative.settings.set(PlainSettings);
|
||||||
|
|
||||||
|
logger.info("Settings deleted from cloud successfully");
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: "Settings deleted from cloud!",
|
||||||
|
color: "var(--green-360)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function putV1(manual?: boolean) {
|
||||||
|
const settings = await exportSettings({ syncDataStore: false, minify: true });
|
||||||
|
|
||||||
|
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Authorization: await getCloudAuth(),
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
},
|
||||||
|
body: deflateSync(new TextEncoder().encode(settings)) as Uint8Array<ArrayBuffer>,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
logger.error(`Failed to sync up, API returned ${res.status}`);
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: `Could not synchronize settings to cloud (API returned ${res.status}).`,
|
||||||
|
color: "var(--red-360)",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { written } = await res.json();
|
||||||
|
PlainSettings.cloud.settingsSyncVersion = written;
|
||||||
|
VencordNative.settings.set(PlainSettings);
|
||||||
|
|
||||||
|
logger.info("Settings uploaded to cloud successfully");
|
||||||
|
|
||||||
|
if (manual) {
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: "Synchronized settings to the cloud!",
|
||||||
|
noPersist: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delete localStorage.Vencord_settingsDirty;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getV1(shouldNotify: boolean, force: boolean) {
|
||||||
|
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: await getCloudAuth(),
|
||||||
|
Accept: "application/octet-stream",
|
||||||
|
"If-None-Match": Settings.cloud.settingsSyncVersion.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 401) {
|
||||||
|
handleAuthFailure();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status === 404) {
|
||||||
|
logger.info("No settings on the cloud");
|
||||||
|
if (shouldNotify)
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: "There are no settings in the cloud.",
|
||||||
|
noPersist: true,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status === 304) {
|
||||||
|
logger.info("Settings up to date");
|
||||||
|
if (shouldNotify)
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: "Your settings are up to date.",
|
||||||
|
noPersist: true,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
logger.error(`Failed to sync down, API returned ${res.status}`);
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: `Could not synchronize settings from the cloud (API returned ${res.status}).`,
|
||||||
|
color: "var(--red-360)",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const written = Number(res.headers.get("etag")!);
|
||||||
|
const localWritten = Settings.cloud.settingsSyncVersion;
|
||||||
|
|
||||||
|
if (!force && written < localWritten) {
|
||||||
|
if (shouldNotify)
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: "Your local settings are newer than the cloud ones.",
|
||||||
|
noPersist: true,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.arrayBuffer();
|
||||||
|
const settings = new TextDecoder().decode(inflateSync(new Uint8Array(data)));
|
||||||
|
await importSettings(settings, "all", true);
|
||||||
|
|
||||||
|
PlainSettings.cloud.settingsSyncVersion = written;
|
||||||
|
VencordNative.settings.set(PlainSettings);
|
||||||
|
|
||||||
|
logger.info("Settings loaded from cloud successfully");
|
||||||
|
if (shouldNotify)
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: "Your settings have been updated! Click here to restart to fully apply changes!",
|
||||||
|
color: "var(--green-360)",
|
||||||
|
onClick: IS_WEB ? () => location.reload() : relaunch,
|
||||||
|
noPersist: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
delete localStorage.Vencord_settingsDirty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteV1() {
|
||||||
|
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { Authorization: await getCloudAuth() },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
logger.error(`Failed to delete, API returned ${res.status}`);
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: `Could not delete settings (API returned ${res.status}).`,
|
||||||
|
color: "var(--red-360)",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Settings deleted from cloud successfully");
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: "Settings deleted from cloud!",
|
||||||
|
color: "var(--green-360)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function shouldCloudSync(direction: "push" | "pull") {
|
export function shouldCloudSync(direction: "push" | "pull") {
|
||||||
const localDirection = localStorage.Vencord_cloudSyncDirection;
|
const localDirection = localStorage.Vencord_cloudSyncDirection;
|
||||||
|
|
||||||
return localDirection === direction || localDirection === "both";
|
return localDirection === direction || localDirection === "both";
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putCloudSettings(manual?: boolean) {
|
export async function putCloudSettings(manual?: boolean) {
|
||||||
const settings = await exportSettings({ syncDataStore: false, minify: true });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
|
const version = await getApiVersion();
|
||||||
method: "PUT",
|
if (version === "v2") {
|
||||||
headers: {
|
await putV2(manual);
|
||||||
Authorization: await getCloudAuth(),
|
if (await getApiVersion() === "v1")
|
||||||
"Content-Type": "application/octet-stream"
|
await putV1(manual);
|
||||||
},
|
} else {
|
||||||
body: deflateSync(new TextEncoder().encode(settings)) as Uint8Array<ArrayBuffer>
|
await putV1(manual);
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
logger.error(`Failed to sync up, API returned ${res.status}`);
|
|
||||||
showNotification({
|
|
||||||
title: "Cloud Settings",
|
|
||||||
body: `Could not synchronize settings to cloud (API returned ${res.status}).`,
|
|
||||||
color: "var(--red-360)"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { written } = await res.json();
|
|
||||||
PlainSettings.cloud.settingsSyncVersion = written;
|
|
||||||
VencordNative.settings.set(PlainSettings);
|
|
||||||
|
|
||||||
logger.info("Settings uploaded to cloud successfully");
|
|
||||||
|
|
||||||
if (manual) {
|
|
||||||
showNotification({
|
|
||||||
title: "Cloud Settings",
|
|
||||||
body: "Synchronized settings to the cloud!",
|
|
||||||
noPersist: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
delete localStorage.Vencord_settingsDirty;
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error("Failed to sync up", e);
|
logger.error("Failed to sync up", e);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Settings",
|
title: "Cloud Settings",
|
||||||
body: `Could not synchronize settings to the cloud (${e.toString()}).`,
|
body: `Could not synchronize settings to the cloud (${e.toString()}).`,
|
||||||
color: "var(--red-360)"
|
color: "var(--red-360)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCloudSettings(shouldNotify = true, force = false) {
|
export async function getCloudSettings(shouldNotify = true, force = false) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
|
const version = await getApiVersion();
|
||||||
method: "GET",
|
if (version === "v2") {
|
||||||
headers: {
|
const result = await getV2(shouldNotify, force);
|
||||||
Authorization: await getCloudAuth(),
|
if (await getApiVersion() === "v1")
|
||||||
Accept: "application/octet-stream",
|
return await getV1(shouldNotify, force);
|
||||||
"If-None-Match": Settings.cloud.settingsSyncVersion.toString()
|
return result;
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status === 401) {
|
|
||||||
// User switched to an account that isn't connected to cloud
|
|
||||||
showNotification({
|
|
||||||
title: "Cloud Settings",
|
|
||||||
body: "Cloud sync was disabled because this account isn't connected to the cloud App. You can enable it again by connecting this account in Cloud Settings. (note: it will store your preferences separately)",
|
|
||||||
color: "var(--yellow-360)",
|
|
||||||
onClick: () => SettingsRouter.openUserSettings("equicord_cloud_panel")
|
|
||||||
});
|
|
||||||
// Disable cloud sync globally
|
|
||||||
Settings.cloud.authenticated = false;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return await getV1(shouldNotify, force);
|
||||||
if (res.status === 404) {
|
|
||||||
logger.info("No settings on the cloud");
|
|
||||||
if (shouldNotify)
|
|
||||||
showNotification({
|
|
||||||
title: "Cloud Settings",
|
|
||||||
body: "There are no settings in the cloud.",
|
|
||||||
noPersist: true
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.status === 304) {
|
|
||||||
logger.info("Settings up to date");
|
|
||||||
if (shouldNotify)
|
|
||||||
showNotification({
|
|
||||||
title: "Cloud Settings",
|
|
||||||
body: "Your settings are up to date.",
|
|
||||||
noPersist: true
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
logger.error(`Failed to sync down, API returned ${res.status}`);
|
|
||||||
showNotification({
|
|
||||||
title: "Cloud Settings",
|
|
||||||
body: `Could not synchronize settings from the cloud (API returned ${res.status}).`,
|
|
||||||
color: "var(--red-360)"
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const written = Number(res.headers.get("etag")!);
|
|
||||||
const localWritten = Settings.cloud.settingsSyncVersion;
|
|
||||||
|
|
||||||
// don't need to check for written > localWritten because the server will return 304 due to if-none-match
|
|
||||||
if (!force && written < localWritten) {
|
|
||||||
if (shouldNotify)
|
|
||||||
showNotification({
|
|
||||||
title: "Cloud Settings",
|
|
||||||
body: "Your local settings are newer than the cloud ones.",
|
|
||||||
noPersist: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.arrayBuffer();
|
|
||||||
|
|
||||||
const settings = new TextDecoder().decode(inflateSync(new Uint8Array(data)));
|
|
||||||
await importSettings(settings, "all", true);
|
|
||||||
|
|
||||||
// sync with server timestamp instead of local one
|
|
||||||
PlainSettings.cloud.settingsSyncVersion = written;
|
|
||||||
VencordNative.settings.set(PlainSettings);
|
|
||||||
|
|
||||||
logger.info("Settings loaded from cloud successfully");
|
|
||||||
if (shouldNotify)
|
|
||||||
showNotification({
|
|
||||||
title: "Cloud Settings",
|
|
||||||
body: "Your settings have been updated! Click here to restart to fully apply changes!",
|
|
||||||
color: "var(--green-360)",
|
|
||||||
onClick: IS_WEB ? () => location.reload() : relaunch,
|
|
||||||
noPersist: true
|
|
||||||
});
|
|
||||||
|
|
||||||
delete localStorage.Vencord_settingsDirty;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error("Failed to sync down", e);
|
logger.error("Failed to sync down", e);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Settings",
|
title: "Cloud Settings",
|
||||||
body: `Could not synchronize settings from the cloud (${e.toString()}).`,
|
body: `Could not synchronize settings from the cloud (${e.toString()}).`,
|
||||||
color: "var(--red-360)"
|
color: "var(--red-360)",
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteCloudSettings() {
|
export async function deleteCloudSettings() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
|
const version = await getApiVersion();
|
||||||
method: "DELETE",
|
if (version === "v2")
|
||||||
headers: { Authorization: await getCloudAuth() },
|
await deleteV2();
|
||||||
});
|
else
|
||||||
|
await deleteV1();
|
||||||
if (!res.ok) {
|
|
||||||
logger.error(`Failed to delete, API returned ${res.status}`);
|
|
||||||
showNotification({
|
|
||||||
title: "Cloud Settings",
|
|
||||||
body: `Could not delete settings (API returned ${res.status}).`,
|
|
||||||
color: "var(--red-360)"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Settings deleted from cloud successfully");
|
|
||||||
showNotification({
|
|
||||||
title: "Cloud Settings",
|
|
||||||
body: "Settings deleted from cloud!",
|
|
||||||
color: "var(--green-360)"
|
|
||||||
});
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error("Failed to delete", e);
|
logger.error("Failed to delete", e);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Settings",
|
title: "Cloud Settings",
|
||||||
body: `Could not delete settings (${e.toString()}).`,
|
body: `Could not delete settings (${e.toString()}).`,
|
||||||
color: "var(--red-360)"
|
color: "var(--red-360)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -211,7 +499,7 @@ export async function deleteCloudSettings() {
|
||||||
export async function eraseAllCloudData() {
|
export async function eraseAllCloudData() {
|
||||||
const res = await fetch(new URL("/v1/", getCloudUrl()), {
|
const res = await fetch(new URL("/v1/", getCloudUrl()), {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: { Authorization: await getCloudAuth() }
|
headers: { Authorization: await getCloudAuth() },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
@ -219,17 +507,18 @@ export async function eraseAllCloudData() {
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Integrations",
|
title: "Cloud Integrations",
|
||||||
body: `Could not erase all data (API returned ${res.status}), please contact support.`,
|
body: `Could not erase all data (API returned ${res.status}), please contact support.`,
|
||||||
color: "var(--red-360)"
|
color: "var(--red-360)",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Settings.cloud.authenticated = false;
|
Settings.cloud.authenticated = false;
|
||||||
await deauthorizeCloud();
|
await deauthorizeCloud();
|
||||||
|
await saveLocalManifest([]);
|
||||||
|
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Integrations",
|
title: "Cloud Integrations",
|
||||||
body: "Successfully erased all data.",
|
body: "Successfully erased all data.",
|
||||||
color: "var(--green-360)"
|
color: "var(--green-360)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
47
src/api/SettingsSync/types.ts
Normal file
47
src/api/SettingsSync/types.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2026 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ManifestEntry {
|
||||||
|
key: string;
|
||||||
|
version: number;
|
||||||
|
checksum: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncUpload {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
checksum?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncRequest {
|
||||||
|
client_manifest: ManifestEntry[];
|
||||||
|
uploads: SyncUpload[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncDownload {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
version: number;
|
||||||
|
checksum: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncUploadResult {
|
||||||
|
key: string;
|
||||||
|
version: number;
|
||||||
|
checksum: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncError {
|
||||||
|
key: string;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncResponse {
|
||||||
|
server_manifest: ManifestEntry[];
|
||||||
|
downloads: SyncDownload[];
|
||||||
|
uploaded: SyncUploadResult[];
|
||||||
|
errors: SyncError[];
|
||||||
|
}
|
||||||
|
|
@ -21,16 +21,18 @@ import type { CSSProperties, HTMLAttributes } from "react";
|
||||||
export interface FlexProps extends HTMLAttributes<HTMLDivElement> {
|
export interface FlexProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
flexDirection?: CSSProperties["flexDirection"];
|
flexDirection?: CSSProperties["flexDirection"];
|
||||||
gap?: CSSProperties["gap"];
|
gap?: CSSProperties["gap"];
|
||||||
|
alignContent?: CSSProperties["alignContent"];
|
||||||
justifyContent?: CSSProperties["justifyContent"];
|
justifyContent?: CSSProperties["justifyContent"];
|
||||||
alignItems?: CSSProperties["alignItems"];
|
alignItems?: CSSProperties["alignItems"];
|
||||||
flexWrap?: CSSProperties["flexWrap"];
|
flexWrap?: CSSProperties["flexWrap"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Flex({ flexDirection, gap = "1em", justifyContent, alignItems, flexWrap, children, style, ...restProps }: FlexProps) {
|
export function Flex({ flexDirection, gap = "1em", alignContent, justifyContent, alignItems, flexWrap, children, style, ...restProps }: FlexProps) {
|
||||||
style = {
|
style = {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection,
|
flexDirection,
|
||||||
gap,
|
gap,
|
||||||
|
alignContent,
|
||||||
justifyContent,
|
justifyContent,
|
||||||
alignItems,
|
alignItems,
|
||||||
flexWrap,
|
flexWrap,
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ export default definePlugin({
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
name: "refresh",
|
name: "refresh",
|
||||||
description: "Refreshes Discord application commands locally",
|
description: "Refreshes the specified applications commands locally",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
name: "user",
|
name: "user",
|
||||||
|
|
|
||||||
|
|
@ -86,12 +86,6 @@ const settings = definePluginSettings({
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
noDefaultHangStatus: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Disable the default hang status when joining voice channels",
|
|
||||||
restartNeeded: true,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
refreshSlashCommands: {
|
refreshSlashCommands: {
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Refreshes Slash Commands to show newly added commands without restarting your client.",
|
description: "Refreshes Slash Commands to show newly added commands without restarting your client.",
|
||||||
|
|
@ -210,15 +204,6 @@ export default definePlugin({
|
||||||
replace: "$& && false"
|
replace: "$& && false"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// No Default Hang Status
|
|
||||||
{
|
|
||||||
find: ".CHILLING)",
|
|
||||||
predicate: () => settings.store.noDefaultHangStatus,
|
|
||||||
replacement: {
|
|
||||||
match: /{enableHangStatus:(\i),/,
|
|
||||||
replace: "{_enableHangStatus:$1=false,"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Force Role Icon
|
// Force Role Icon
|
||||||
{
|
{
|
||||||
find: "Message Username",
|
find: "Message Username",
|
||||||
|
|
|
||||||
|
|
@ -1126,12 +1126,24 @@ function getLastFilterChoices(): { group: string; filter: string; }[] | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLastSortChoice(sort: string): void {
|
function setLastSortChoice(sort: string): void {
|
||||||
|
if (!sort) sort = "questify";
|
||||||
settings.store.lastQuestPageSort = sort;
|
settings.store.lastQuestPageSort = sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLastFilterChoices(filters: { group: string; filter: string; }[]): void {
|
function setLastFilterChoices(filters: { group: string; filter: string; }[] | null): void {
|
||||||
if (!filters || !Object.keys(filters).length || !Object.values(filters).every(f => f.group && f.filter)) { return; }
|
if (!filters || filters.length === 0) {
|
||||||
settings.store.lastQuestPageFilters = JSON.parse(JSON.stringify(filters)).reduce((acc, item) => ({ ...acc, [item.filter]: item }), {});
|
settings.store.lastQuestPageFilters = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filters.every(f => f && f.group && f.filter)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.store.lastQuestPageFilters = (JSON.parse(JSON.stringify(filters)) as { group: string; filter: string; }[]).reduce((acc, item) => {
|
||||||
|
acc[item.filter] = item;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQuestAcceptedButtonProps(quest: Quest, text: string, disabled: boolean, onClick?: () => void) {
|
function getQuestAcceptedButtonProps(quest: Quest, text: string, disabled: boolean, onClick?: () => void) {
|
||||||
|
|
@ -1247,8 +1259,8 @@ export default definePlugin({
|
||||||
replace: "const shouldHideSponsoredQuestBanner=$self.shouldHideSponsoredQuestBanner();"
|
replace: "const shouldHideSponsoredQuestBanner=$self.shouldHideSponsoredQuestBanner();"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<=if\(null!=\i\))return(.{0,60}?}\))/,
|
match: /(?<=return null;if\(\i)(\).{0,30}?null!=\i)/,
|
||||||
replace: "if(!shouldHideSponsoredQuestBanner)return $1"
|
replace: "&&!shouldHideSponsoredQuestBanner$1&&!shouldHideSponsoredQuestBanner"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -1533,24 +1545,18 @@ export default definePlugin({
|
||||||
replace: "$self.getLastSortChoice()??$1"
|
replace: "$self.getLastSortChoice()??$1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Set the initial filters.
|
// Set the initial filters and update the filters and sort method when they change.
|
||||||
match: /(get\(\i\)\)\?\?)(\i)/,
|
match: /(get\(\i\)\)\?\?)(\i,\[\i\]\),\i=\i.useCallback\((\i)=>{)(.{0,60}?useCallback\((\i)=>{)/,
|
||||||
replace: "$1$self.getLastFilterChoices()??$2"
|
replace: "$1$self.getLastFilterChoices()??$2$self.setLastSortChoice($3);$4$self.setLastFilterChoices($5);"
|
||||||
},
|
|
||||||
{
|
|
||||||
// Update the last used sort method when it changes.
|
|
||||||
match: /(onChange:)(\i)(.{0,40}?selectedSortMethod)/,
|
|
||||||
replace: "$1(value)=>{$self.settings.store.lastQuestPageSort=value;$2(value);}$3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Update the last used filter choices when they change.
|
|
||||||
match: /(onChange:)(\i)(.{0,40}?selectedFilters)/,
|
|
||||||
replace: "$1(value)=>{$self.settings.store.lastQuestPageFilters=value.reduce((acc,item)=>({...acc,[item.filter]:item}),{});$2(value);}$3"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Update the last used sort and filter choices when the toggle setting for either is changed.
|
// Update the last used sort and filter choices when the toggle setting for either is changed.
|
||||||
match: /(?<=ALL,\i.useMemo\(\(\)=>\()({sortMethod:(\i),filters:(\i))/,
|
match: /(?<=ALL,\i.useMemo\(\(\)=>\()({sortMethod:(\i),filters:(\i))/,
|
||||||
replace: "$self.setLastSortChoice($2),$self.setLastFilterChoices($3),$1"
|
replace: "$self.setLastSortChoice($2),$self.setLastFilterChoices($3),$1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /(?<=resetSortingFiltering:\(\)=>{\i\(\),\i\()\i.\i.SUGGESTED/,
|
||||||
|
replace: '"questify"'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,14 @@
|
||||||
import * as DataStore from "@api/DataStore";
|
import * as DataStore from "@api/DataStore";
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { EquicordDevs } from "@utils/constants";
|
import { EquicordDevs } from "@utils/constants";
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
import definePlugin, { makeRange, OptionType } from "@utils/types";
|
import definePlugin, { makeRange, OptionType } from "@utils/types";
|
||||||
|
import { VoiceState } from "@vencord/discord-types";
|
||||||
import { ChannelStore, FluxDispatcher, UserStore, VoiceStateStore } from "@webpack/common";
|
import { ChannelStore, FluxDispatcher, UserStore, VoiceStateStore } from "@webpack/common";
|
||||||
|
|
||||||
const DATASTORE_KEY = "VCLastVoiceChannel";
|
const DATASTORE_KEY = "VCLastVoiceChannel";
|
||||||
const DATASTORE_SESSION_KEY = "VCLastVoiceChannelSession";
|
const DATASTORE_SESSION_KEY = "VCLastVoiceChannelSession";
|
||||||
|
const logger = new Logger("VoiceRejoin");
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
rejoinDelay: {
|
rejoinDelay: {
|
||||||
|
|
@ -48,12 +51,15 @@ const settings = definePluginSettings({
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "VoiceRejoin",
|
name: "VoiceRejoin",
|
||||||
description: "Rejoins DM/Server call automatically when restarting Discord.",
|
description: "Rejoins DM/Server call automatically when restarting Discord.",
|
||||||
authors: [EquicordDevs.omaw],
|
authors: [EquicordDevs.omaw, EquicordDevs.keircn],
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
flux: {
|
flux: {
|
||||||
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: any[]; }) {
|
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
|
||||||
const myUserId = UserStore.getCurrentUser().id;
|
const currentUser = UserStore.getCurrentUser();
|
||||||
|
if (!currentUser) return;
|
||||||
|
|
||||||
|
const myUserId = currentUser.id;
|
||||||
const myState = voiceStates.find(s => s.userId === myUserId);
|
const myState = voiceStates.find(s => s.userId === myUserId);
|
||||||
if (!myState) return;
|
if (!myState) return;
|
||||||
|
|
||||||
|
|
@ -63,77 +69,92 @@ export default definePlugin({
|
||||||
channelId: myState.channelId,
|
channelId: myState.channelId,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
DataStore.set(DATASTORE_KEY, saved);
|
void Promise.all([
|
||||||
DataStore.set(DATASTORE_SESSION_KEY, true);
|
DataStore.set(DATASTORE_KEY, saved),
|
||||||
|
DataStore.set(DATASTORE_SESSION_KEY, true)
|
||||||
|
]).catch(err => logger.error("Failed to persist last voice channel", err));
|
||||||
} else {
|
} 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() {
|
async CONNECTION_OPEN() {
|
||||||
const wasInVC = await DataStore.get(DATASTORE_SESSION_KEY);
|
const wasInVC = await DataStore.get(DATASTORE_SESSION_KEY);
|
||||||
if (wasInVC === false) {
|
if (wasInVC === false) {
|
||||||
DataStore.del(DATASTORE_KEY);
|
await DataStore.del(DATASTORE_KEY);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const saved = await DataStore.get(DATASTORE_KEY);
|
try {
|
||||||
if (!saved?.channelId) return;
|
const saved = await DataStore.get(DATASTORE_KEY);
|
||||||
|
if (!saved?.channelId) return;
|
||||||
|
|
||||||
const channel = ChannelStore.getChannel(saved.channelId);
|
let channel = ChannelStore.getChannel(saved.channelId);
|
||||||
const isDM = channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM();
|
for (let i = 0; i < 20 && !channel; i++) {
|
||||||
const myUserId = UserStore.getCurrentUser().id;
|
await new Promise(resolve => setTimeout(resolve, 250));
|
||||||
const myVoiceState = VoiceStateStore.getVoiceStateForUser(myUserId);
|
channel = ChannelStore.getChannel(saved.channelId);
|
||||||
const preventionMode = settings.store.preventReconnectIfCallEnded;
|
}
|
||||||
const timeoutMs = settings.store.rejoinTimeout * 1000;
|
|
||||||
|
|
||||||
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) {
|
if (!channel) {
|
||||||
DataStore.set(DATASTORE_SESSION_KEY, false);
|
await DataStore.set(DATASTORE_SESSION_KEY, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldPrevent =
|
const currentUser = UserStore.getCurrentUser();
|
||||||
preventionMode === "both" ||
|
if (!currentUser) return;
|
||||||
(preventionMode === "dms" && isDM) ||
|
|
||||||
(preventionMode === "servers" && !isDM);
|
|
||||||
|
|
||||||
if (shouldPrevent) {
|
const isDM = channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM();
|
||||||
const connectedUsers = VoiceStateStore.getVoiceStatesForChannel(saved.channelId);
|
const myUserId = currentUser.id;
|
||||||
const othersInCall = Object.values(connectedUsers).filter(
|
const myVoiceState = VoiceStateStore.getVoiceStateForUser(myUserId);
|
||||||
(vs: any) => vs.userId !== UserStore.getCurrentUser().id
|
const preventionMode = settings.store.preventReconnectIfCallEnded;
|
||||||
);
|
const timeoutMs = settings.store.rejoinTimeout * 1000;
|
||||||
|
|
||||||
if (othersInCall.length === 0) {
|
if (saved.timestamp && Date.now() - saved.timestamp > timeoutMs) {
|
||||||
DataStore.set(DATASTORE_SESSION_KEY, false);
|
await DataStore.set(DATASTORE_SESSION_KEY, false);
|
||||||
return;
|
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<string, VoiceState>;
|
||||||
|
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);
|
}, settings.store.rejoinDelay * 1000);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,14 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { Heading, HeadingSecondary } from "@components/Heading";
|
import { Heading, HeadingSecondary } from "@components/Heading";
|
||||||
import { Paragraph } from "@components/Paragraph";
|
import { Paragraph } from "@components/Paragraph";
|
||||||
import { EquicordDevs } from "@utils/constants";
|
import { Devs, EquicordDevs } from "@utils/constants";
|
||||||
import { classNameFactory } from "@utils/css";
|
import { classNameFactory } from "@utils/css";
|
||||||
import { getIntlMessage, openUserProfile } from "@utils/discord";
|
import { getIntlMessage, openUserProfile } from "@utils/discord";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
|
|
@ -25,12 +27,49 @@ interface WatchingProps {
|
||||||
guildId?: string;
|
guildId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cl = classNameFactory("whosWatching-");
|
const ApplicationStreamingStore = findStoreLazy("ApplicationStreamingStore");
|
||||||
|
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
|
||||||
|
const AvatarStyles = findCssClassesLazy("moreUsers", "clickableAvatar", "avatar");
|
||||||
|
const cl = classNameFactory("vc-whos-watching-");
|
||||||
|
|
||||||
function getUsername(user: any): string {
|
function getUsername(user: User): string {
|
||||||
return RelationshipStore.getNickname(user.id) || user.globalName || user.username;
|
return RelationshipStore.getNickname(user.id) || user.globalName || user.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Watching({ userIds, guildId }: WatchingProps): JSX.Element {
|
||||||
|
let missingUsers = 0;
|
||||||
|
const users = userIds.map(id => UserStore.getUser(id)).filter(user => Boolean(user) ? true : (missingUsers += 1, false));
|
||||||
|
return (
|
||||||
|
<div className={cl("content")}>
|
||||||
|
{userIds.length ?
|
||||||
|
(
|
||||||
|
<div className={cl("spectating")}>
|
||||||
|
<Heading>{getIntlMessage("SPECTATORS", { numViewers: userIds.length })}</Heading>
|
||||||
|
<Flex flexDirection="column" gap="6" >
|
||||||
|
{users.map(user => (
|
||||||
|
<Flex key={user.id} flexDirection="row" gap="6" alignContent="center">
|
||||||
|
<img className={cl("user-avatar")} src={user.getAvatarURL(guildId)} alt="" />
|
||||||
|
{getUsername(user)}
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
{missingUsers > 0 &&
|
||||||
|
<span className={cl("more-users")}>
|
||||||
|
{`+${getIntlMessage("NUM_USERS", { num: missingUsers })}`}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<span className={cl("no-viewers")}>
|
||||||
|
No spectators
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
showPanel: {
|
showPanel: {
|
||||||
description: "Show spectators under screenshare panel",
|
description: "Show spectators under screenshare panel",
|
||||||
|
|
@ -40,53 +79,23 @@ const settings = definePluginSettings({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function Watching({ userIds, guildId }: WatchingProps): JSX.Element {
|
|
||||||
// Missing Users happen when UserStore.getUser(id) returns null
|
|
||||||
// The client should automatically cache spectators, so this might not be possible but it's better to be sure just in case
|
|
||||||
let missingUsers = 0;
|
|
||||||
const users = userIds.map(id => UserStore.getUser(id)).filter(user => Boolean(user) ? true : (missingUsers += 1, false));
|
|
||||||
return (
|
|
||||||
<div className={cl("content")}>
|
|
||||||
{userIds.length ?
|
|
||||||
(<>
|
|
||||||
<Heading>{getIntlMessage("SPECTATORS", { numViewers: userIds.length })}</Heading>
|
|
||||||
<Flex flexDirection="column" style={{ gap: 6 }} >
|
|
||||||
{users.map(user => (
|
|
||||||
<Flex key={user.id} flexDirection="row" style={{ gap: 6, alignContent: "center" }} className={cl("user")} >
|
|
||||||
<img src={user.getAvatarURL(guildId)} style={{ borderRadius: 8, width: 16, height: 16 }} alt="" />
|
|
||||||
{getUsername(user)}
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
{missingUsers > 0 && <span className={cl("more_users")}>{`+${getIntlMessage("NUM_USERS", { num: missingUsers })}`}</span>}
|
|
||||||
</Flex>
|
|
||||||
</>)
|
|
||||||
: (<span className={cl("no_viewers")}>No spectators</span>)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ApplicationStreamingStore = findStoreLazy("ApplicationStreamingStore");
|
|
||||||
|
|
||||||
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
|
|
||||||
const AvatarStyles = findCssClassesLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar", "avatar");
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "WhosWatching",
|
name: "WhosWatching",
|
||||||
description: "Hover over the screenshare icon to view what users are watching your stream",
|
description: "Hover over the screenshare icon to view what users are watching your stream",
|
||||||
authors: [EquicordDevs.Fres],
|
authors: [EquicordDevs.Fres, Devs.thororen],
|
||||||
settings,
|
settings,
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: ".Masks.STATUS_SCREENSHARE,width:32",
|
find: ".Masks.STATUS_SCREENSHARE,width:32",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /jsx\)\((\i\.\i),{mask:/,
|
match: /\((\i\.\i)(?=,{mask:\i\.\i\.Masks\.STATUS_SCREENSHARE)/,
|
||||||
replace: "jsx)($self.component({OriginalComponent:$1}),{mask:"
|
replace: "($self.component({OriginalComponent:$1})"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "this.renderEmbeddedActivity()",
|
find: "this.renderEmbeddedActivity()",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /"div"(?=.{0,50}this.renderActions)/,
|
match: /"div"(?=.{0,40}?,this.renderActions\(\))/,
|
||||||
replace: "$self.WrapperComponent"
|
replace: "$self.WrapperComponent"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,8 +104,8 @@ export default definePlugin({
|
||||||
const stream = useStateFromStores([ApplicationStreamingStore], () => ApplicationStreamingStore.getCurrentUserActiveStream());
|
const stream = useStateFromStores([ApplicationStreamingStore], () => ApplicationStreamingStore.getCurrentUserActiveStream());
|
||||||
if (!stream) return <div {...props}>{props.children}</div>;
|
if (!stream) return <div {...props}>{props.children}</div>;
|
||||||
|
|
||||||
const userIds: string[] = ApplicationStreamingStore.getViewerIds(stream);
|
|
||||||
let missingUsers = 0;
|
let missingUsers = 0;
|
||||||
|
const userIds: string[] = ApplicationStreamingStore.getViewerIds(stream);
|
||||||
const users = userIds.map(id => UserStore.getUser(id)).filter(user => Boolean(user) ? true : (missingUsers += 1, false));
|
const users = userIds.map(id => UserStore.getUser(id)).filter(user => Boolean(user) ? true : (missingUsers += 1, false));
|
||||||
|
|
||||||
function renderMoreUsers(_label: string, count: number) {
|
function renderMoreUsers(_label: string, count: number) {
|
||||||
|
|
@ -117,14 +126,14 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={cl("screenshare-panel")}>
|
||||||
<div {...props}>{props.children}</div>
|
<div {...props}>{props.children}</div>
|
||||||
<div className={classes(cl("spectators_panel"), Margins.top8)}>
|
<div className={classes(cl("spectating-panel"), Margins.top8)}>
|
||||||
<HeadingSecondary style={{ marginTop: 8, marginBottom: 0, textTransform: "uppercase" }}>
|
<HeadingSecondary className={cl("spectating-header")}>
|
||||||
{getIntlMessage("SPECTATORS", { numViewers: userIds.length })}
|
{getIntlMessage("SPECTATORS", { numViewers: userIds.length })}
|
||||||
</HeadingSecondary>
|
</HeadingSecondary>
|
||||||
{users.length ?
|
{users.length ?
|
||||||
<>
|
<div className={cl("spectating-users")}>
|
||||||
<UserSummaryItem
|
<UserSummaryItem
|
||||||
users={users}
|
users={users}
|
||||||
count={userIds.length}
|
count={userIds.length}
|
||||||
|
|
@ -147,20 +156,20 @@ export default definePlugin({
|
||||||
</Clickable>
|
</Clickable>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
: <Paragraph>No spectators</Paragraph>
|
: <Paragraph>
|
||||||
|
No spectators
|
||||||
|
</Paragraph>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
component: function ({ OriginalComponent }) {
|
component: function ({ OriginalComponent }) {
|
||||||
return ErrorBoundary.wrap((props: any) => {
|
return ErrorBoundary.wrap(props => {
|
||||||
const stream = useStateFromStores(
|
const stream = useStateFromStores([ApplicationStreamingStore], () => ApplicationStreamingStore.getCurrentUserActiveStream());
|
||||||
[ApplicationStreamingStore],
|
|
||||||
() => ApplicationStreamingStore.getCurrentUserActiveStream()
|
|
||||||
);
|
|
||||||
if (!stream) return null;
|
if (!stream) return null;
|
||||||
|
|
||||||
const viewers = ApplicationStreamingStore.getViewerIds(stream);
|
const viewers = ApplicationStreamingStore.getViewerIds(stream);
|
||||||
return <Tooltip text={<Watching userIds={viewers} guildId={stream.guildId} />}>
|
return <Tooltip text={<Watching userIds={viewers} guildId={stream.guildId} />}>
|
||||||
{({ onMouseEnter, onMouseLeave }) => (
|
{({ onMouseEnter, onMouseLeave }) => (
|
||||||
|
|
|
||||||
11
src/equicordplugins/whosWatching/styles.css
Normal file
11
src/equicordplugins/whosWatching/styles.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -20,7 +20,7 @@ import { gitHashShort } from "@shared/vencordUserAgent";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { isTruthy } from "@utils/guards";
|
import { isTruthy } from "@utils/guards";
|
||||||
import definePlugin, { IconProps, OptionType } from "@utils/types";
|
import definePlugin, { IconProps, OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy, waitFor } from "@webpack";
|
||||||
import { React } from "@webpack/common";
|
import { React } from "@webpack/common";
|
||||||
import type { ComponentType, PropsWithChildren, ReactNode } from "react";
|
import type { ComponentType, PropsWithChildren, ReactNode } from "react";
|
||||||
|
|
||||||
|
|
@ -46,7 +46,14 @@ const enum LayoutType {
|
||||||
CUSTOM = 18
|
CUSTOM = 18
|
||||||
}
|
}
|
||||||
|
|
||||||
const LayoutTypes: typeof LayoutType = findByPropsLazy("SECTION", "SIDEBAR_ITEM", "PANEL");
|
let LayoutTypes = {
|
||||||
|
SECTION: 1,
|
||||||
|
SIDEBAR_ITEM: 2,
|
||||||
|
PANEL: 3,
|
||||||
|
CATEGORY: 5,
|
||||||
|
CUSTOM: 19,
|
||||||
|
};
|
||||||
|
waitFor(["SECTION", "SIDEBAR_ITEM", "PANEL", "CUSTOM"], v => LayoutTypes = v);
|
||||||
|
|
||||||
const enum SectionType {
|
const enum SectionType {
|
||||||
HEADER = "HEADER",
|
HEADER = "HEADER",
|
||||||
|
|
@ -175,20 +182,25 @@ export default definePlugin({
|
||||||
key: key + "_panel",
|
key: key + "_panel",
|
||||||
type: LayoutTypes.PANEL,
|
type: LayoutTypes.PANEL,
|
||||||
useTitle: () => panelTitle,
|
useTitle: () => panelTitle,
|
||||||
buildLayout: () => [],
|
buildLayout: () => [{
|
||||||
StronglyDiscouragedCustomComponent: () => <Component />,
|
type: LayoutTypes.CATEGORY,
|
||||||
render: () => <Component />,
|
key: key + "_category",
|
||||||
|
buildLayout: () => [{
|
||||||
|
type: LayoutTypes.CUSTOM,
|
||||||
|
key: key + "_custom",
|
||||||
|
Component: Component,
|
||||||
|
useSearchTerms: () => [title]
|
||||||
|
}]
|
||||||
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return ({
|
||||||
key,
|
key,
|
||||||
type: LayoutTypes.SIDEBAR_ITEM,
|
type: LayoutTypes.SIDEBAR_ITEM,
|
||||||
legacySearchKey: title.toUpperCase(),
|
|
||||||
getLegacySearchKey: () => title.toUpperCase(),
|
|
||||||
useTitle: () => title,
|
useTitle: () => title,
|
||||||
icon: () => <Icon width={20} height={20} />,
|
icon: () => <Icon width={20} height={20} />,
|
||||||
buildLayout: () => [panel]
|
buildLayout: () => [panel]
|
||||||
};
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getSettingsSectionMappings() {
|
getSettingsSectionMappings() {
|
||||||
|
|
|
||||||
|
|
@ -161,9 +161,6 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
buildPluginMenuEntries,
|
|
||||||
buildThemeMenuEntries,
|
|
||||||
|
|
||||||
// This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary
|
// This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary
|
||||||
// without possibly also catching unrelated errors of children.
|
// without possibly also catching unrelated errors of children.
|
||||||
//
|
//
|
||||||
|
|
|
||||||
5
src/plugins/noDefaultHangStatus/README.md
Normal file
5
src/plugins/noDefaultHangStatus/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# NoDefaultHangStatus
|
||||||
|
|
||||||
|
Disable the default hang status when joining voice channels
|
||||||
|
|
||||||
|

|
||||||
26
src/plugins/noDefaultHangStatus/index.ts
Normal file
26
src/plugins/noDefaultHangStatus/index.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { migrateSettingToPlugin } from "@api/Settings";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
|
migrateSettingToPlugin("NoDefaultHangStatus", "EquicordHelper", "noDefaultHangStatus");
|
||||||
|
export default definePlugin({
|
||||||
|
name: "NoDefaultHangStatus",
|
||||||
|
description: "Disable the default hang status when joining voice channels",
|
||||||
|
authors: [Devs.D3SOX],
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ".CHILLING)",
|
||||||
|
replacement: {
|
||||||
|
match: /{enableHangStatus:(\i),/,
|
||||||
|
replace: "{_enableHangStatus:$1=false,"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue