-
+
diff --git a/src/components/PluginSettings/index.tsx b/src/components/PluginSettings/index.tsx
index 7c2d3107..1bfaee37 100644
--- a/src/components/PluginSettings/index.tsx
+++ b/src/components/PluginSettings/index.tsx
@@ -244,7 +244,7 @@ export default function PluginSettings() {
}));
}, []);
- const depMap = React.useMemo(() => {
+ const depMap = useMemo(() => {
const o = {} as Record
;
for (const plugin in Plugins) {
const deps = Plugins[plugin].dependencies;
diff --git a/src/components/ThemeSettings/ThemesTab.tsx b/src/components/ThemeSettings/ThemesTab.tsx
index 0e752efc..dc9dc7f4 100644
--- a/src/components/ThemeSettings/ThemesTab.tsx
+++ b/src/components/ThemeSettings/ThemesTab.tsx
@@ -34,6 +34,7 @@ import { useAwaiter } from "@utils/react";
import type { ThemeHeader } from "@utils/themes";
import { getThemeInfo, stripBOM, type UserThemeHeader } from "@utils/themes/bd";
import { usercssParse } from "@utils/themes/usercss";
+import { getStylusWebStoreUrl } from "@utils/web";
import { findLazy } from "@webpack";
import { Button, Card, Forms, React, showToast, TabBar, TextInput, Tooltip, useEffect, useMemo, useRef, useState } from "@webpack/common";
import type { ComponentType, Ref, SyntheticEvent } from "react";
@@ -502,4 +503,20 @@ function ThemesTab() {
);
}
-export default wrapTab(ThemesTab, "Themes");
+function UserscriptThemesTab() {
+ return (
+
+
+ Themes are not supported on the Userscript!
+
+
+ You can instead install themes with the Stylus extension!
+
+
+
+ );
+}
+
+export default IS_USERSCRIPT
+ ? wrapTab(UserscriptThemesTab, "Themes")
+ : wrapTab(ThemesTab, "Themes");
diff --git a/src/equicordplugins/ABSRPC/index.tsx b/src/equicordplugins/ABSRPC/index.tsx
new file mode 100644
index 00000000..48a71d0c
--- /dev/null
+++ b/src/equicordplugins/ABSRPC/index.tsx
@@ -0,0 +1,250 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2025 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+// alot of the code is from JellyfinRPC
+import { definePluginSettings } from "@api/Settings";
+import { EquicordDevs } from "@utils/constants";
+import { Logger } from "@utils/Logger";
+import definePlugin, { OptionType } from "@utils/types";
+import { ApplicationAssetUtils, FluxDispatcher, Forms, showToast } from "@webpack/common";
+
+
+interface ActivityAssets {
+ large_image?: string;
+ large_text?: string;
+ small_image?: string;
+ small_text?: string;
+}
+
+interface Activity {
+ state: string;
+ details?: string;
+ timestamps?: {
+ start?: number;
+ };
+ assets?: ActivityAssets;
+ name: string;
+ application_id: string;
+ metadata?: {
+ button_urls?: Array;
+ };
+ type: number;
+ flags: number;
+}
+
+interface MediaData {
+ name: string;
+ type: string;
+ author?: string;
+ series?: string;
+ duration?: number;
+ currentTime?: number;
+ progress?: number;
+ url?: string;
+ imageUrl?: string;
+ isFinished?: boolean;
+}
+
+
+
+const settings = definePluginSettings({
+ serverUrl: {
+ description: "AudioBookShelf server URL (e.g., https://abs.example.com)",
+ type: OptionType.STRING,
+ },
+ username: {
+ description: "AudioBookShelf username",
+ type: OptionType.STRING,
+ },
+ password: {
+ description: "AudioBookShelf password",
+ type: OptionType.STRING,
+ },
+});
+
+const applicationId = "1381423044907503636";
+
+const logger = new Logger("AudioBookShelfRichPresence");
+
+let authToken: string | null = null;
+
+async function getApplicationAsset(key: string): Promise {
+ return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];
+}
+
+function setActivity(activity: Activity | null) {
+ FluxDispatcher.dispatch({
+ type: "LOCAL_ACTIVITY_UPDATE",
+ activity,
+ socketId: "ABSRPC",
+ });
+}
+
+export default definePlugin({
+ name: "AudioBookShelfRichPresence",
+ description: "Rich presence for AudioBookShelf media server",
+ authors: [EquicordDevs.vmohammad],
+
+ settingsAboutComponent: () => (
+ <>
+ How to connect to AudioBookShelf
+
+ Enter your AudioBookShelf server URL, username, and password to display your currently playing audiobooks as Discord Rich Presence.
+
+ The plugin will automatically authenticate and fetch your listening progress.
+
+ >
+ ),
+
+ settings,
+
+ start() {
+ this.updatePresence();
+ this.updateInterval = setInterval(() => { this.updatePresence(); }, 10000);
+ },
+
+ stop() {
+ clearInterval(this.updateInterval);
+ },
+
+ async authenticate(): Promise {
+ if (!settings.store.serverUrl || !settings.store.username || !settings.store.password) {
+ logger.warn("AudioBookShelf server URL, username, or password is not set in settings.");
+ showToast("AudioBookShelf RPC is not configured.", "failure", {
+ duration: 15000,
+ });
+ return false;
+ }
+
+ try {
+ const baseUrl = settings.store.serverUrl.replace(/\/$/, "");
+ const url = `${baseUrl}/login`;
+
+ const res = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ username: settings.store.username,
+ password: settings.store.password,
+ }),
+ });
+
+ if (!res.ok) throw `${res.status} ${res.statusText}`;
+
+ const data = await res.json();
+ authToken = data.user?.token;
+ return !!authToken;
+ } catch (e) {
+ logger.error("Failed to authenticate with AudioBookShelf", e);
+ authToken = null;
+ return false;
+ }
+ },
+
+ async fetchMediaData(): Promise {
+ if (!authToken && !(await this.authenticate())) {
+ return null;
+ }
+
+ const isPlayingNow = session => {
+ const now = Date.now();
+ const lastUpdate = session.updatedAt;
+ const diffSeconds = (now - lastUpdate) / 1000;
+ return diffSeconds <= 30;
+ };
+ try {
+ const baseUrl = settings.store.serverUrl!.replace(/\/$/, "");
+ const url = `${baseUrl}/api/me/listening-sessions`;
+
+ const res = await fetch(url, {
+ headers: {
+ "Authorization": `Bearer ${authToken}`,
+ },
+ });
+
+ if (!res.ok) {
+ if (res.status === 401) {
+ authToken = null;
+ if (await this.authenticate()) {
+ return this.fetchMediaData();
+ }
+ }
+ throw `${res.status} ${res.statusText}`;
+ }
+
+ const { sessions } = await res.json();
+ const activeSession = sessions.find((session: any) =>
+ session.updatedAt && !session.isFinished
+ );
+
+ if (!activeSession || !isPlayingNow(activeSession)) return null;
+
+ const { mediaMetadata: media, mediaType, duration, currentTime, libraryItemId } = activeSession;
+ if (!media) return null;
+ console.log(media);
+ return {
+ name: media.title || "Unknown",
+ type: mediaType || "book",
+ author: media.author || media.publisher,
+ series: media.series[0]?.name,
+ duration,
+ currentTime,
+ imageUrl: libraryItemId ? `${baseUrl}/api/items/${libraryItemId}/cover` : undefined,
+ isFinished: activeSession.isFinished || false,
+ };
+ } catch (e) {
+ logger.error("Failed to query AudioBookShelf API", e);
+ return null;
+ }
+ },
+
+ async updatePresence() {
+ setActivity(await this.getActivity());
+ },
+
+ async getActivity(): Promise {
+ const mediaData = await this.fetchMediaData();
+ if (!mediaData || mediaData.isFinished) return null;
+
+ const largeImage = mediaData.imageUrl;
+ console.log("Large Image URL:", largeImage);
+ const assets: ActivityAssets = {
+ large_image: largeImage ? await getApplicationAsset(largeImage) : await getApplicationAsset("audiobookshelf"),
+ large_text: mediaData.series || mediaData.author || undefined,
+ };
+
+ const getDetails = () => {
+ return mediaData.name;
+ };
+
+ const getState = () => {
+ if (mediaData.series && mediaData.author) {
+ return `${mediaData.series} • ${mediaData.author}`;
+ }
+ return mediaData.author || "AudioBook";
+ };
+
+ const timestamps = mediaData.currentTime && mediaData.duration ? {
+ start: Date.now() - (mediaData.currentTime * 1000),
+ end: Date.now() + ((mediaData.duration - mediaData.currentTime) * 1000)
+ } : undefined;
+
+ return {
+ application_id: applicationId,
+ name: "AudioBookShelf",
+
+ details: getDetails(),
+ state: getState(),
+ assets,
+ timestamps,
+
+ type: 2,
+ flags: 1,
+ };
+ }
+});
diff --git a/src/equicordplugins/betterActivities/components/SpotifyIcon.tsx b/src/equicordplugins/betterActivities/components/SpotifyIcon.tsx
deleted file mode 100644
index 9210169e..00000000
--- a/src/equicordplugins/betterActivities/components/SpotifyIcon.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
- * Vencord, a Discord client mod
- * Copyright (c) 2024 Vendicated and contributors
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-import type { SVGProps } from "react";
-
-export function SpotifyIcon(props: SVGProps) {
- return ( );
-}
diff --git a/src/equicordplugins/betterActivities/index.tsx b/src/equicordplugins/betterActivities/index.tsx
index 6212eb33..ae5d6d1e 100644
--- a/src/equicordplugins/betterActivities/index.tsx
+++ b/src/equicordplugins/betterActivities/index.tsx
@@ -35,16 +35,17 @@ export default definePlugin({
patches: [
{
// Patch activity icons
- find: "isBlockedOrIgnored(null",
+ find: '"ActivityStatus"',
replacement: {
match: /(?<=hideTooltip:.{0,4}}=(\i).*?{}\))\]/,
replace: ",$self.patchActivityList($1)]"
},
predicate: () => settings.store.memberList,
+ all: true
},
{
// Show all activities in the user popout/sidebar
- find: "hasAvatarForGuild(null",
+ find: '"UserProfilePopoutBody"',
replacement: {
match: /(?<=(\i)\.id\)\}\)\),(\i).*?)\(0,.{0,100}\i\.id,onClose:\i\}\)/,
replace: "$self.showAllActivitiesComponent({ activity: $2, user: $1 })"
diff --git a/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx b/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx
index 9e4b7893..0a3c39bf 100644
--- a/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx
+++ b/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx
@@ -10,14 +10,13 @@ import { React, Tooltip } from "@webpack/common";
import { JSX } from "react";
import { ActivityTooltip } from "../components/ActivityTooltip";
-import { SpotifyIcon } from "../components/SpotifyIcon";
import { TwitchIcon } from "../components/TwitchIcon";
import { settings } from "../settings";
import { ActivityListIcon, ActivityListProps, ApplicationIcon, IconCSSProperties } from "../types";
import { cl, getApplicationIcons } from "../utils";
-// if discord one day decides to change their icon this needs to be updated
-const DefaultActivityIcon = findComponentByCodeLazy("M6,7 L2,7 L2,6 L6,6 L6,7 Z M8,5 L2,5 L2,4 L8,4 L8,5 Z M8,3 L2,3 L2,2 L8,2 L8,3 Z M8.88888889,0 L1.11111111,0 C0.494444444,0 0,0.494444444 0,1.11111111 L0,8.88888889 C0,9.50253861 0.497461389,10 1.11111111,10 L8.88888889,10 C9.50253861,10 10,9.50253861 10,8.88888889 L10,1.11111111 C10,0.494444444 9.5,0 8.88888889,0 Z");
+// Discord no longer shows an icon here by default but we use the one from the popout now here
+const DefaultActivityIcon = findComponentByCodeLazy("M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5Zm6.81 7c-.54 0-1 .26-1.23.61A1 1 0 0 1 8.92 8.5 3.49 3.49 0 0 1 11.82 7c1.81 0 3.43 1.38 3.43 3.25 0 1.45-.98 2.61-2.27 3.06a1 1 0 0 1-1.96.37l-.19-1a1 1 0 0 1 .98-1.18c.87 0 1.44-.63 1.44-1.25S12.68 9 11.81 9ZM13 16a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm7-10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM18.5 20a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM7 18.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM5.5 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z");
export function patchActivityList({ activities, user, hideTooltip }: ActivityListProps): JSX.Element | null {
const icons: ActivityListIcon[] = [];
@@ -57,7 +56,6 @@ export function patchActivityList({ activities, user, hideTooltip }: ActivityLis
}
};
addActivityIcon("Twitch", TwitchIcon);
- addActivityIcon("Spotify", SpotifyIcon);
if (icons.length) {
const iconStyle: IconCSSProperties = {
@@ -86,7 +84,7 @@ export function patchActivityList({ activities, user, hideTooltip }: ActivityLis
// We need to filter out custom statuses
const shouldShow = activities.filter(a => a.type !== 4).length !== icons.length;
if (shouldShow) {
- return ;
+ return ;
}
}
diff --git a/src/equicordplugins/betterActivities/utils.tsx b/src/equicordplugins/betterActivities/utils.tsx
index 4798ad44..6e3afb03 100644
--- a/src/equicordplugins/betterActivities/utils.tsx
+++ b/src/equicordplugins/betterActivities/utils.tsx
@@ -39,11 +39,11 @@ export function getActivityApplication(activity: Activity | null) {
export function getApplicationIcons(activities: Activity[], preferSmall = false): ApplicationIcon[] {
const applicationIcons: ApplicationIcon[] = [];
- const applications = activities.filter(activity => activity.application_id || activity.platform);
+ const applications = activities.filter(activity => activity.application_id || activity.platform || activity?.id?.startsWith("spotify:"));
for (const activity of applications) {
- const { assets, application_id, platform } = activity;
- if (!application_id && !platform) continue;
+ const { assets, application_id, platform, id } = activity;
+ if (!application_id && !platform && !id.startsWith("spotify:")) continue;
if (assets) {
const { small_image, small_text, large_image, large_text } = assets;
@@ -59,6 +59,12 @@ export function getApplicationIcons(activities: Activity[], preferSmall = false)
activity
});
}
+ } else if (image.startsWith("spotify:")) {
+ const url = `https://i.scdn.co/image/${image.split(":")[1]}`;
+ applicationIcons.push({
+ image: { src: url, alt },
+ activity
+ });
} else {
const src = `https://cdn.discordapp.com/app-assets/${application_id}/${image}.png`;
applicationIcons.push({
diff --git a/src/equicordplugins/betterAudioPlayer/style.css b/src/equicordplugins/betterAudioPlayer/style.css
index e96b9338..7ce03332 100644
--- a/src/equicordplugins/betterAudioPlayer/style.css
+++ b/src/equicordplugins/betterAudioPlayer/style.css
@@ -7,4 +7,4 @@
pointer-events: none;
z-index: 1;
border: none;
-}
\ No newline at end of file
+}
diff --git a/src/equicordplugins/bypassPinPrompt/index.ts b/src/equicordplugins/bypassPinPrompt/index.ts
index 0d4ac7ed..d050f723 100644
--- a/src/equicordplugins/bypassPinPrompt/index.ts
+++ b/src/equicordplugins/bypassPinPrompt/index.ts
@@ -9,7 +9,7 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "BypassPinPrompt",
- description: "Bypass the pin prompt when pinning messages",
+ description: "Bypass the pin prompt when using the pin functions",
authors: [EquicordDevs.thororen],
patches: [
{
diff --git a/src/equicordplugins/_core/equicordHelper.tsx b/src/equicordplugins/equicordHelper/index.tsx
similarity index 100%
rename from src/equicordplugins/_core/equicordHelper.tsx
rename to src/equicordplugins/equicordHelper/index.tsx
diff --git a/src/equicordplugins/equicordHelper/native.ts b/src/equicordplugins/equicordHelper/native.ts
new file mode 100644
index 00000000..84887c48
--- /dev/null
+++ b/src/equicordplugins/equicordHelper/native.ts
@@ -0,0 +1,9 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2025 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { CspPolicies, MediaScriptsAndCssSrc } from "@main/csp";
+
+CspPolicies["*"] = MediaScriptsAndCssSrc;
diff --git a/src/equicordplugins/equicordToolbox/index.tsx b/src/equicordplugins/equicordToolbox/index.tsx
index 9326c189..59876492 100644
--- a/src/equicordplugins/equicordToolbox/index.tsx
+++ b/src/equicordplugins/equicordToolbox/index.tsx
@@ -147,7 +147,7 @@ function VencordPopoutButton() {
function ToolboxFragmentWrapper({ children }: { children: ReactNode[]; }) {
children.splice(
children.length - 1, 0,
-
+
);
diff --git a/src/equicordplugins/fixFileExtensions/index.tsx b/src/equicordplugins/fixFileExtensions/index.tsx
index 85d15030..e4664a7e 100644
--- a/src/equicordplugins/fixFileExtensions/index.tsx
+++ b/src/equicordplugins/fixFileExtensions/index.tsx
@@ -24,8 +24,6 @@ export const reverseExtensionMap = Object.entries(extensionMap).reduce((acc, [ta
return acc;
}, {} as Record);
-type ExtUpload = Upload & { fixExtension?: boolean; };
-
export default definePlugin({
name: "FixFileExtensions",
authors: [EquicordDevs.thororen],
@@ -34,15 +32,21 @@ export default definePlugin({
patches: [
// Taken from AnonymiseFileNames
{
- find: 'type:"UPLOAD_START"',
- replacement: {
- match: /await \i\.uploadFiles\((\i),/,
- replace: "$1.forEach($self.fixExt),$&"
- },
+ find: "async uploadFiles(",
+ replacement: [
+ {
+ match: /async uploadFiles\((\i),\i\){/,
+ replace: "$&$1.forEach($self.fixExt);"
+ },
+ {
+ match: /async uploadFilesSimple\((\i)\){/,
+ replace: "$&$1.forEach($self.fixExt);"
+ }
+ ],
predicate: () => !Settings.plugins.AnonymiseFileNames.enabled,
},
],
- fixExt(upload: ExtUpload) {
+ fixExt(upload: Upload) {
const file = upload.filename;
const tarMatch = tarExtMatcher.exec(file);
const extIdx = tarMatch?.index ?? file.lastIndexOf(".");
diff --git a/src/equicordplugins/furudoSpeak.dev/providers/Ollama.ts b/src/equicordplugins/furudoSpeak.dev/providers/Ollama.ts
index 32d836b4..e36d40de 100644
--- a/src/equicordplugins/furudoSpeak.dev/providers/Ollama.ts
+++ b/src/equicordplugins/furudoSpeak.dev/providers/Ollama.ts
@@ -24,7 +24,7 @@ export default async (
}: FurudoSettings,
repliedMessage?: Message
): Promise => {
- const completion = await fetch("http://localhost:11434/api/chat", {
+ const completion = await fetch("http://127.0.0.1:11434/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
diff --git a/src/equicordplugins/jellyfinRichPresence/index.tsx b/src/equicordplugins/jellyfinRichPresence/index.tsx
new file mode 100644
index 00000000..aa27bedf
--- /dev/null
+++ b/src/equicordplugins/jellyfinRichPresence/index.tsx
@@ -0,0 +1,215 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2025 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+// alot of the code is from LastFMRichPresence
+import { definePluginSettings } from "@api/Settings";
+import { EquicordDevs } from "@utils/constants";
+import { Logger } from "@utils/Logger";
+import definePlugin, { OptionType } from "@utils/types";
+import { ApplicationAssetUtils, FluxDispatcher, Forms, showToast } from "@webpack/common";
+
+
+interface ActivityAssets {
+ large_image?: string;
+ large_text?: string;
+ small_image?: string;
+ small_text?: string;
+}
+
+interface Activity {
+ state: string;
+ details?: string;
+ timestamps?: {
+ start?: number;
+ };
+ assets?: ActivityAssets;
+ name: string;
+ application_id: string;
+ metadata?: {
+ button_urls?: Array;
+ };
+ type: number;
+ flags: number;
+}
+
+interface MediaData {
+ name: string;
+ type: string;
+ artist?: string;
+ album?: string;
+ seriesName?: string;
+ seasonNumber?: number;
+ episodeNumber?: number;
+ year?: number;
+ url?: string;
+ imageUrl?: string;
+ duration?: number;
+ position?: number;
+}
+
+
+
+const settings = definePluginSettings({
+ serverUrl: {
+ description: "Jellyfin server URL (e.g., https://jellyfin.example.com)",
+ type: OptionType.STRING,
+ },
+ apiKey: {
+ description: "Jellyfin API key obtained from your Jellyfin administration dashboard",
+ type: OptionType.STRING,
+ },
+ userId: {
+ description: "Jellyfin user ID obtained from your user profile URL",
+ type: OptionType.STRING,
+ },
+});
+
+const applicationId = "1381368130164625469";
+
+const logger = new Logger("JellyfinRichPresence");
+
+async function getApplicationAsset(key: string): Promise {
+ return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];
+}
+
+function setActivity(activity: Activity | null) {
+ FluxDispatcher.dispatch({
+ type: "LOCAL_ACTIVITY_UPDATE",
+ activity,
+ socketId: "Jellyfin",
+ });
+}
+
+export default definePlugin({
+ name: "JellyfinRichPresence",
+ description: "Rich presence for Jellyfin media server",
+ authors: [EquicordDevs.vmohammad],
+
+ settingsAboutComponent: () => (
+ <>
+ How to get an API key
+
+ An API key is required to fetch your current media. To get one, go to your
+ Jellyfin dashboard, navigate to Administration {">"} API Keys and
+ create a new API key.
+
+ You'll also need your User ID, which can be found in the url of your user profile page.
+
+ >
+ ),
+
+ settings,
+
+ start() {
+ this.updatePresence();
+ this.updateInterval = setInterval(() => { this.updatePresence(); }, 10000);
+ },
+
+ stop() {
+ clearInterval(this.updateInterval);
+ },
+
+ async fetchMediaData(): Promise {
+ if (!settings.store.serverUrl || !settings.store.apiKey || !settings.store.userId) {
+ logger.warn("Jellyfin server URL, API key, or user ID is not set in settings.");
+ showToast("JellyfinRPC is not configured.", "failure", {
+ duration: 15000,
+ });
+ return null;
+ }
+
+ try {
+ const baseUrl = settings.store.serverUrl.replace(/\/$/, "");
+ const url = `${baseUrl}/Sessions?api_key=${settings.store.apiKey}`;
+
+ const res = await fetch(url);
+ if (!res.ok) throw `${res.status} ${res.statusText}`;
+
+ const sessions = await res.json();
+ const userSession = sessions.find((session: any) =>
+ session.UserId === settings.store.userId && session.NowPlayingItem
+ );
+
+ if (!userSession || !userSession.NowPlayingItem) return null;
+
+ const item = userSession.NowPlayingItem;
+ const playState = userSession.PlayState;
+
+ if (playState?.IsPaused) return null;
+
+ const imageUrl = item.ImageTags?.Primary
+ ? `${baseUrl}/Items/${item.Id}/Images/Primary`
+ : undefined;
+
+ return {
+ name: item.Name || "Unknown",
+ type: item.Type,
+ artist: item.Artists?.[0] || item.AlbumArtist,
+ album: item.Album,
+ seriesName: item.SeriesName,
+ seasonNumber: item.ParentIndexNumber,
+ episodeNumber: item.IndexNumber,
+ year: item.ProductionYear,
+ url: `${baseUrl}/web/#!/details?id=${item.Id}`,
+ imageUrl,
+ duration: item.RunTimeTicks ? Math.floor(item.RunTimeTicks / 10000000) : undefined,
+ position: playState?.PositionTicks ? Math.floor(playState.PositionTicks / 10000000) : undefined
+ };
+ } catch (e) {
+ logger.error("Failed to query Jellyfin API", e);
+ return null;
+ }
+ },
+
+ async updatePresence() {
+ setActivity(await this.getActivity());
+ },
+
+ async getActivity(): Promise {
+ const mediaData = await this.fetchMediaData();
+ if (!mediaData) return null;
+
+ const largeImage = mediaData.imageUrl;
+ const assets: ActivityAssets = {
+ large_image: largeImage ? await getApplicationAsset(largeImage) : await getApplicationAsset("jellyfin"),
+ large_text: mediaData.album || mediaData.seriesName || undefined,
+ };
+
+ const getDetails = () => {
+ if (mediaData.type === "Episode" && mediaData.seriesName) {
+ return mediaData.name;
+ }
+ return mediaData.name;
+ };
+
+ const getState = () => {
+ if (mediaData.type === "Episode" && mediaData.seriesName) {
+ const season = mediaData.seasonNumber ? `S${mediaData.seasonNumber}` : "";
+ const episode = mediaData.episodeNumber ? `E${mediaData.episodeNumber}` : "";
+ return `${mediaData.seriesName} ${season}${episode}`.trim();
+ }
+ return mediaData.artist || (mediaData.year ? `(${mediaData.year})` : undefined);
+ };
+
+ const timestamps = mediaData.position && mediaData.duration ? {
+ start: Date.now() - (mediaData.position * 1000),
+ end: Date.now() + ((mediaData.duration - mediaData.position) * 1000)
+ } : undefined;
+
+ return {
+ application_id: applicationId,
+ name: "Jellyfin",
+
+ details: getDetails(),
+ state: getState() || "something",
+ assets,
+ timestamps,
+
+ type: 3,
+ flags: 1,
+ };
+ }
+});
diff --git a/src/equicordplugins/questCompleter.discordDesktop/index.tsx b/src/equicordplugins/questCompleter.discordDesktop/index.tsx
index 19a13533..549ea700 100644
--- a/src/equicordplugins/questCompleter.discordDesktop/index.tsx
+++ b/src/equicordplugins/questCompleter.discordDesktop/index.tsx
@@ -51,40 +51,40 @@ async function openCompleteQuestUI() {
const applicationId = quest.config.application.id;
const applicationName = quest.config.application.name;
const taskName = ["WATCH_VIDEO", "PLAY_ON_DESKTOP", "STREAM_ON_DESKTOP", "PLAY_ACTIVITY"].find(x => quest.config.taskConfig.tasks[x] != null);
+ const icon = `https://cdn.discordapp.com/quests/${quest.id}/${theme}/${quest.config.assets.gameTile}`;
// @ts-ignore
const secondsNeeded = quest.config.taskConfig.tasks[taskName].target;
// @ts-ignore
- const secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0;
- const icon = `https://cdn.discordapp.com/assets/quests/${quest.id}/${theme}/${quest.config.assets.gameTile}`;
+ let secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0;
if (taskName === "WATCH_VIDEO") {
- const tolerance = 2, speed = 10;
- const diff = Math.floor((Date.now() - new Date(quest.userStatus.enrolledAt).getTime()) / 1000);
- const startingPoint = Math.min(Math.max(Math.ceil(secondsDone), diff), secondsNeeded);
+ const maxFuture = 10, speed = 7, interval = 1;
+ const enrolledAt = new Date(quest.userStatus.enrolledAt).getTime();
const fn = async () => {
- for (let i = startingPoint; i <= secondsNeeded; i += speed) {
- try {
- await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: Math.min(secondsNeeded, i + Math.random()) } });
- } catch (ex) {
- console.log("Failed to send increment of", i, ex);
+ while (true) {
+ const maxAllowed = Math.floor((Date.now() - enrolledAt) / 1000) + maxFuture;
+ const diff = maxAllowed - secondsDone;
+ const timestamp = secondsDone + speed;
+ if (diff >= speed) {
+ await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: Math.min(secondsNeeded, timestamp + Math.random()) } });
+ secondsDone = Math.min(secondsNeeded, timestamp);
}
- await new Promise(resolve => setTimeout(resolve, tolerance * 1000));
- }
- if ((secondsNeeded - secondsDone) % speed !== 0) {
- await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: secondsNeeded } });
- showNotification({
- title: `${applicationName} - Quest Completer`,
- body: "Quest Completed.",
- icon: icon,
- });
+ if (timestamp >= secondsNeeded) {
+ break;
+ }
+ await new Promise(resolve => setTimeout(resolve, interval * 1000));
}
+ showNotification({
+ title: `${applicationName} - Quest Completer`,
+ body: "Quest Completed.",
+ icon: icon,
+ });
};
fn();
showNotification({
title: `${applicationName} - Quest Completer`,
- body: `Wait for ${Math.ceil((secondsNeeded - startingPoint) / speed * tolerance)} more seconds.`,
+ body: `Spoofing video for ${applicationName}.`,
icon: icon,
});
- console.log(`Spoofing video for ${applicationName}.`);
} else if (taskName === "PLAY_ON_DESKTOP") {
RestAPI.get({ url: `/applications/public?application_ids=${applicationId}` }).then(res => {
const appData = res.body[0];
diff --git a/src/equicordplugins/streamingCodecDisabler/index.ts b/src/equicordplugins/streamingCodecDisabler/index.ts
new file mode 100644
index 00000000..f26113e9
--- /dev/null
+++ b/src/equicordplugins/streamingCodecDisabler/index.ts
@@ -0,0 +1,81 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2025 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { definePluginSettings, Settings } from "@api/Settings";
+import { EquicordDevs } from "@utils/constants";
+import definePlugin, { OptionType } from "@utils/types";
+import { findStoreLazy } from "@webpack";
+
+let mediaEngine = findStoreLazy("MediaEngineStore");
+
+const originalCodecStatuses: {
+ AV1: boolean,
+ H265: boolean,
+ H264: boolean;
+} = {
+ AV1: true,
+ H265: true,
+ H264: true
+};
+
+const settings = definePluginSettings({
+ disableAv1Codec: {
+ description: "Make Discord not consider using AV1 for streaming.",
+ type: OptionType.BOOLEAN,
+ default: false
+ },
+ disableH265Codec: {
+ description: "Make Discord not consider using H265 for streaming.",
+ type: OptionType.BOOLEAN,
+ default: false
+ },
+ disableH264Codec: {
+ description: "Make Discord not consider using H264 for streaming.",
+ type: OptionType.BOOLEAN,
+ default: false
+ },
+});
+
+export default definePlugin({
+ name: "StreamingCodecDisabler",
+ description: "Disable codecs for streaming of your choice",
+ authors: [EquicordDevs.davidkra230],
+ settings,
+
+ patches: [
+ {
+ find: "setVideoBroadcast(this.shouldConnectionBroadcastVideo",
+ replacement: {
+ match: /setGoLiveSource\(.,.\)\{/,
+ replace: "$&$self.updateDisabledCodecs();"
+ },
+ }
+ ],
+
+ async updateDisabledCodecs() {
+ mediaEngine.setAv1Enabled(originalCodecStatuses.AV1 && !Settings.plugins.StreamingCodecDisabler.disableAv1Codec);
+ mediaEngine.setH265Enabled(originalCodecStatuses.H265 && !Settings.plugins.StreamingCodecDisabler.disableH265Codec);
+ mediaEngine.setH264Enabled(originalCodecStatuses.H264 && !Settings.plugins.StreamingCodecDisabler.disableH264Codec);
+ },
+
+ async start() {
+ mediaEngine = mediaEngine.getMediaEngine();
+ const options = Object.keys(originalCodecStatuses);
+ // [{"codec":"","decode":false,"encode":false}]
+ const CodecCapabilities = JSON.parse(await new Promise(res => mediaEngine.getCodecCapabilities(res)));
+ CodecCapabilities.forEach((codec: { codec: string; encode: boolean; }) => {
+ if (options.includes(codec.codec)) {
+ originalCodecStatuses[codec.codec] = codec.encode;
+ }
+ });
+ },
+
+ async stop() {
+ mediaEngine.setAv1Enabled(originalCodecStatuses.AV1);
+ mediaEngine.setH265Enabled(originalCodecStatuses.H265);
+ mediaEngine.setH264Enabled(originalCodecStatuses.H264);
+ }
+});
diff --git a/src/equicordplugins/timezones/database.tsx b/src/equicordplugins/timezones/database.tsx
index f382c917..a313828e 100644
--- a/src/equicordplugins/timezones/database.tsx
+++ b/src/equicordplugins/timezones/database.tsx
@@ -8,7 +8,6 @@ import { openModal } from "@utils/index";
import { OAuth2AuthorizeModal, showToast, Toasts } from "@webpack/common";
const databaseTimezones: Record = {};
-
const DOMAIN = "https://timezone.creations.works";
const REDIRECT_URI = `${DOMAIN}/auth/discord/callback`;
const CLIENT_ID = "1377021506810417173";
@@ -26,7 +25,6 @@ export async function loadDatabaseTimezones(): Promise {
const res = await fetch(`${DOMAIN}/list`, {
headers: { Accept: "application/json" }
});
-
if (res.ok) {
const json = await res.json();
for (const id in json) {
@@ -34,10 +32,8 @@ export async function loadDatabaseTimezones(): Promise {
value: json[id]?.timezone ?? null
};
}
-
return true;
}
-
return false;
} catch (e) {
console.error("Failed to fetch timezones list:", e);
@@ -45,30 +41,93 @@ export async function loadDatabaseTimezones(): Promise {
}
}
-export async function setTimezone(timezone: string): Promise {
- const res = await fetch(`${DOMAIN}/set?timezone=${encodeURIComponent(timezone)}`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Accept: "application/json"
- },
- credentials: "include"
- });
+async function checkAuthentication(): Promise {
+ try {
+ const res = await fetch(`${DOMAIN}/me`, {
+ credentials: "include",
+ headers: { Accept: "application/json" }
+ });
+ return res.ok;
+ } catch (e) {
+ console.error("Failed to check authentication:", e);
+ return false;
+ }
+}
- return res.ok;
+export async function setTimezone(timezone: string): Promise {
+ const isAuthenticated = await checkAuthentication();
+
+ if (!isAuthenticated) {
+ return new Promise(resolve => {
+ authModal(() => {
+ setTimezoneInternal(timezone).then(resolve);
+ });
+ });
+ }
+
+ return setTimezoneInternal(timezone);
+}
+
+async function setTimezoneInternal(timezone: string): Promise {
+ const formData = new URLSearchParams();
+ formData.append("timezone", timezone);
+
+ try {
+ const res = await fetch(`${DOMAIN}/set`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ Accept: "application/json"
+ },
+ credentials: "include",
+ body: formData
+ });
+
+ if (!res.ok) {
+ const error = await res.json().catch(() => ({ message: "Unknown error" }));
+ showToast(error.message || "Failed to set timezone", Toasts.Type.FAILURE);
+ return false;
+ }
+
+ showToast("Timezone updated successfully!", Toasts.Type.SUCCESS);
+ return true;
+ } catch (e) {
+ console.error("Error setting timezone:", e);
+ showToast("Failed to set timezone", Toasts.Type.FAILURE);
+ return false;
+ }
}
export async function deleteTimezone(): Promise {
- const res = await fetch(`${DOMAIN}/delete`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Accept: "application/json"
- },
- credentials: "include"
- });
+ const isAuthenticated = await checkAuthentication();
- return res.ok;
+ if (!isAuthenticated) {
+ showToast("You must be logged in to delete your timezone", Toasts.Type.FAILURE);
+ return false;
+ }
+
+ try {
+ const res = await fetch(`${DOMAIN}/delete`, {
+ method: "DELETE",
+ headers: {
+ Accept: "application/json"
+ },
+ credentials: "include"
+ });
+
+ if (!res.ok) {
+ const error = await res.json().catch(() => ({ message: "Unknown error" }));
+ showToast(error.message || "Failed to delete timezone", Toasts.Type.FAILURE);
+ return false;
+ }
+
+ showToast("Timezone deleted successfully!", Toasts.Type.SUCCESS);
+ return true;
+ } catch (e) {
+ console.error("Error deleting timezone:", e);
+ showToast("Failed to delete timezone", Toasts.Type.FAILURE);
+ return false;
+ }
}
export function authModal(callback?: () => void) {
@@ -83,21 +142,17 @@ export function authModal(callback?: () => void) {
cancelCompletesFlow={false}
callback={async (res: any) => {
if (!res || !res.location) return;
-
try {
const url = new URL(res.location);
-
const r = await fetch(url, {
credentials: "include",
headers: { Accept: "application/json" }
});
-
const json = await r.json();
if (!r.ok) {
showToast(json.message ?? "Authorization failed", Toasts.Type.FAILURE);
return;
}
-
showToast("Authorization successful!", Toasts.Type.SUCCESS);
callback?.();
} catch (e) {
diff --git a/src/equicordplugins/timezones/index.tsx b/src/equicordplugins/timezones/index.tsx
index c16b432d..2871c342 100644
--- a/src/equicordplugins/timezones/index.tsx
+++ b/src/equicordplugins/timezones/index.tsx
@@ -17,7 +17,7 @@ import { findByPropsLazy } from "@webpack";
import { Button, Menu, showToast, Toasts, Tooltip, useEffect, UserStore, useState } from "@webpack/common";
import { Message, User } from "discord-types/general";
-import { authModal, deleteTimezone, getTimezone, loadDatabaseTimezones, setUserDatabaseTimezone } from "./database";
+import { deleteTimezone, getTimezone, loadDatabaseTimezones, setUserDatabaseTimezone } from "./database";
import { SetTimezoneModal } from "./TimezoneModal";
export let timezones: Record = {};
@@ -68,9 +68,7 @@ export const settings = definePluginSettings({
type: OptionType.COMPONENT,
component: () => (
{
- authModal(async () => {
- openModal(modalProps => );
- });
+ openModal(modalProps => );
}}>
Set Timezone on Database
@@ -83,11 +81,19 @@ export const settings = definePluginSettings({
component: () => (
{
- authModal(async () => {
+ onClick={async () => {
+ try {
await setUserDatabaseTimezone(UserStore.getCurrentUser().id, null);
- await deleteTimezone();
- });
+ const success = await deleteTimezone();
+ if (success) {
+ showToast("Database timezone reset successfully!", Toasts.Type.SUCCESS);
+ } else {
+ showToast("Failed to reset database timezone", Toasts.Type.FAILURE);
+ }
+ } catch (error) {
+ console.error("Error resetting database timezone:", error);
+ showToast("Failed to reset database timezone", Toasts.Type.FAILURE);
+ }
}}
>
Reset Database Timezones
@@ -228,9 +234,7 @@ export default definePlugin({
toolboxActions: {
"Set Database Timezone": () => {
- authModal(async () => {
- openModal(modalProps => );
- });
+ openModal(modalProps => );
},
"Refresh Database Timezones": async () => {
try {
@@ -265,9 +269,7 @@ export default definePlugin({
{
- authModal(async () => {
- openModal(modalProps => );
- });
+ openModal(modalProps => );
}}
>
Want to save your timezone to the database? Click here to set it.
diff --git a/src/equicordplugins/tosuRPC/index.ts b/src/equicordplugins/tosuRPC/index.ts
index b1727773..05d908b3 100644
--- a/src/equicordplugins/tosuRPC/index.ts
+++ b/src/equicordplugins/tosuRPC/index.ts
@@ -29,7 +29,7 @@ export default definePlugin({
authors: [Devs.AutumnVN],
start() {
(function connect() {
- ws = new WebSocket("ws://localhost:24050/websocket/v2");
+ ws = new WebSocket("ws://127.0.0.1:24050/websocket/v2");
ws.addEventListener("error", () => ws.close());
ws.addEventListener("close", () => wsReconnect = setTimeout(connect, 5000));
ws.addEventListener("message", ({ data }) => throttledOnMessage(data));
diff --git a/src/equicordplugins/wallpaperFree/components/ctxmenu.tsx b/src/equicordplugins/wallpaperFree/components/ctxmenu.tsx
index a2fe4e8a..6ce575d0 100644
--- a/src/equicordplugins/wallpaperFree/components/ctxmenu.tsx
+++ b/src/equicordplugins/wallpaperFree/components/ctxmenu.tsx
@@ -8,8 +8,7 @@ import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { openModal } from "@utils/modal";
import { ChannelStore, FluxDispatcher, Menu } from "@webpack/common";
-import { SetCustomWallpaperModal, SetDiscordWallpaperModal } from "./modal";
-import { ChatWallpaperStore, fetchWallpapers } from "./util";
+import { SetWallpaperModal } from "./modal";
const addWallpaperMenu = (channelId?: string, guildId?: string) => {
@@ -22,25 +21,18 @@ const addWallpaperMenu = (channelId?: string, guildId?: string) => {
url,
});
};
+
return (
WallpaperFreeStore.getUrl(channel));
+ Wallpaper({ url }: { url: string; }) {
+ // no we cant place the hook here
+ if (!url) return null;
- if (!forceReplace && wp?.id)
- return wp;
-
- if (url) {
- return {
- wallpaperId: "id",
- vcWallpaperUrl: url,
- isViewable: true,
- };
- }
-
- return void 0;
+ return
+
+ ;
},
+ WallpaperState(channel: Channel) {
+ return useStateFromStores([WallpaperFreeStore], () => WallpaperFreeStore.getUrl(channel));
+ }
});
diff --git a/src/equicordplugins/wallpaperFree/styles.css b/src/equicordplugins/wallpaperFree/styles.css
index 6a90c8b1..ee8c715a 100644
--- a/src/equicordplugins/wallpaperFree/styles.css
+++ b/src/equicordplugins/wallpaperFree/styles.css
@@ -1,30 +1,8 @@
-.vc-wpfree-discord-wp-modal {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
- gap: 24px;
- padding: 8px 0;
-}
-
-.vc-wpfree-discord-wp-icon-container {
- border-radius: 10px;
- box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
- padding: 16px;
- display: flex;
- flex-direction: column;
- align-items: center
-}
-
-.vc-wpfree-discord-wp-icon-img {
- width: 120px;
- height: 68px;
- object-fit: cover;
- border-radius: 6px;
- margin-bottom: 8px;
- border: 1px solid var(--background-modifier-accent);
-}
-
-.vc-wpfree-discord-set-buttons {
- display: flex;
- gap: 8px;
- margin-top: 12px
+.vc-wpfree-wp-container,
+.wallpaperContainer {
+ background-position: 100% 100%;
+ background-repeat: no-repeat;
+ background-size: cover;
+ inset: 0;
+ position: absolute;
}
diff --git a/src/globals.d.ts b/src/globals.d.ts
index c04fe994..aad0d544 100644
--- a/src/globals.d.ts
+++ b/src/globals.d.ts
@@ -29,11 +29,12 @@ declare global {
* replace: "IS_WEB?foo:bar"
* // GOOD
* replace: IS_WEB ? "foo" : "bar"
- * // also good
+ * // also okay
* replace: `${IS_WEB}?foo:bar`
*/
export var IS_WEB: boolean;
export var IS_EXTENSION: boolean;
+ export var IS_USERSCRIPT: boolean;
export var IS_STANDALONE: boolean;
export var IS_UPDATER_DISABLED: boolean;
export var IS_DEV: boolean;
diff --git a/src/main/csp.ts b/src/main/csp.ts
new file mode 100644
index 00000000..2faee606
--- /dev/null
+++ b/src/main/csp.ts
@@ -0,0 +1,142 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2025 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { session } from "electron";
+
+type PolicyMap = Record;
+
+export const ConnectSrc = ["connect-src"];
+export const MediaSrc = [...ConnectSrc, "img-src", "media-src"];
+export const CssSrc = ["style-src", "font-src"];
+export const MediaAndCssSrc = [...MediaSrc, ...CssSrc];
+export const MediaScriptsAndCssSrc = [...MediaAndCssSrc, "script-src", "worker-src"];
+
+// Plugins can whitelist their own domains by importing this object in their native.ts
+// script and just adding to it. But generally, you should just edit this file instead
+
+export const CspPolicies: PolicyMap = {
+ "*.github.io": MediaAndCssSrc, // GitHub pages, used by most themes
+ "github.com": MediaAndCssSrc, // GitHub content (stuff uploaded to markdown forms), used by most themes
+ "raw.githubusercontent.com": MediaAndCssSrc, // GitHub raw, used by some themes
+ "*.gitlab.io": MediaAndCssSrc, // GitLab pages, used by some themes
+ "gitlab.com": MediaAndCssSrc, // GitLab raw, used by some themes
+ "*.codeberg.page": MediaAndCssSrc, // Codeberg pages, used by some themes
+ "codeberg.org": MediaAndCssSrc, // Codeberg raw, used by some themes
+
+ "*.githack.com": MediaAndCssSrc, // githack (namely raw.githack.com), used by some themes
+ "jsdelivr.net": MediaAndCssSrc, // jsDelivr, used by very few themes
+
+ "fonts.googleapis.com": CssSrc, // Google Fonts, used by many themes
+
+ "i.imgur.com": MediaSrc, // Imgur, used by some themes
+ "i.ibb.co": MediaSrc, // ImgBB, used by some themes
+ "i.pinimg.com": MediaSrc, // Pinterest, used by some themes
+ "*.tenor.com": MediaSrc, // Tenor, used by some themes
+ "files.catbox.moe": MediaSrc, // Catbox, used by some themes
+
+ "cdn.discordapp.com": MediaAndCssSrc, // Discord CDN, used by Vencord and some themes to load media
+ "media.discordapp.net": MediaSrc, // Discord media CDN, possible alternative to Discord CDN
+
+ // CDNs used for some things by Vencord.
+ // FIXME: we really should not be using CDNs anymore
+ "cdnjs.cloudflare.com": MediaScriptsAndCssSrc,
+ "cdn.jsdelivr.net": MediaScriptsAndCssSrc,
+
+ // Function Specific
+ "api.github.com": ConnectSrc, // used for updating Vencord itself
+ "ws.audioscrobbler.com": ConnectSrc, // Last.fm API
+ "translate-pa.googleapis.com": ConnectSrc, // Google Translate API
+ "*.vencord.dev": MediaSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev)
+ "manti.vendicated.dev": MediaSrc, // ReviewDB API
+ "decor.fieryflames.dev": ConnectSrc, // Decor API
+ "ugc.decor.fieryflames.dev": MediaSrc, // Decor CDN
+ "sponsor.ajay.app": ConnectSrc, // Dearrow API
+ "dearrow-thumb.ajay.app": MediaSrc, // Dearrow Thumbnail CDN
+ "usrbg.is-hardly.online": MediaSrc, // USRBG API
+ "icons.duckduckgo.com": MediaSrc, // DuckDuckGo Favicon API (Reverse Image Search)
+};
+
+const findHeader = (headers: PolicyMap, headerName: Lowercase) => {
+ return Object.keys(headers).find(h => h.toLowerCase() === headerName);
+};
+
+const parsePolicy = (policy: string): PolicyMap => {
+ const result: PolicyMap = {};
+ policy.split(";").forEach(directive => {
+ const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
+ if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
+ result[directiveKey] = directiveValue;
+ }
+ });
+
+ return result;
+};
+
+const stringifyPolicy = (policy: PolicyMap): string =>
+ Object.entries(policy)
+ .filter(([, values]) => values?.length)
+ .map(directive => directive.flat().join(" "))
+ .join("; ");
+
+
+const patchCsp = (headers: PolicyMap) => {
+ const reportOnlyHeader = findHeader(headers, "content-security-policy-report-only");
+ if (reportOnlyHeader)
+ delete headers[reportOnlyHeader];
+
+ const header = findHeader(headers, "content-security-policy");
+
+ if (header) {
+ const csp = parsePolicy(headers[header][0]);
+
+ const pushDirective = (directive: string, ...values: string[]) => {
+ csp[directive] ??= [...(csp["default-src"] ?? [])];
+ csp[directive].push(...values);
+ };
+
+ pushDirective("style-src", "'unsafe-inline'");
+ // we could make unsafe-inline safe by using strict-dynamic with a random nonce on our Vencord loader script https://content-security-policy.com/strict-dynamic/
+ // HOWEVER, at the time of writing (24 Jan 2025), Discord is INSANE and also uses unsafe-inline
+ // Once they stop using it, we also should
+ pushDirective("script-src", "'unsafe-inline'", "'unsafe-eval'");
+
+ for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
+ pushDirective(directive, "blob:", "data:", "vencord:");
+ }
+
+ for (const [host, directives] of Object.entries(CspPolicies)) {
+ for (const directive of directives) {
+ pushDirective(directive, host);
+ }
+ }
+
+ headers[header] = [stringifyPolicy(csp)];
+ }
+};
+
+export function initCsp() {
+ session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
+ if (responseHeaders) {
+ if (resourceType === "mainFrame")
+ patchCsp(responseHeaders);
+
+ // Fix hosts that don't properly set the css content type, such as
+ // raw.githubusercontent.com
+ if (resourceType === "stylesheet") {
+ const header = findHeader(responseHeaders, "content-type");
+ if (header)
+ responseHeaders[header] = ["text/css"];
+ }
+ }
+
+ cb({ cancel: false, responseHeaders });
+ });
+
+ // assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
+ // For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
+ // impossible to load css from github raw despite our fix above
+ session.defaultSession.webRequest.onHeadersReceived = () => { };
+}
diff --git a/src/main/index.ts b/src/main/index.ts
index 8f9d13c3..28ce041d 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -16,9 +16,11 @@
* along with this program. If not, see .
*/
-import { app, protocol, session } from "electron";
+import { app, net, protocol } from "electron";
import { join } from "path";
+import { pathToFileURL } from "url";
+import { initCsp } from "./csp";
import { ensureSafePath } from "./ipcMain";
import { RendererSettings } from "./settings";
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
@@ -26,55 +28,71 @@ import { installExt } from "./utils/extensions";
if (!IS_VANILLA && !IS_EXTENSION) {
app.whenReady().then(() => {
- // Source Maps! Maybe there's a better way but since the renderer is executed
- // from a string I don't think any other form of sourcemaps would work
- protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
- let url = unsafeUrl.slice("vencord://".length);
+ protocol.handle("vencord", ({ url: unsafeUrl }) => {
+ let url = decodeURI(unsafeUrl).slice("vencord://".length).replace(/\?v=\d+$/, "");
+
if (url.endsWith("/")) url = url.slice(0, -1);
+
if (url.startsWith("/themes/")) {
const theme = url.slice("/themes/".length);
+
const safeUrl = ensureSafePath(THEMES_DIR, theme);
if (!safeUrl) {
- cb({ statusCode: 403 });
- return;
+ return new Response(null, {
+ status: 404
+ });
}
- cb(safeUrl.replace(/\?v=\d+$/, ""));
- return;
+
+ return net.fetch(pathToFileURL(safeUrl).toString());
}
+
+ // Source Maps! Maybe there's a better way but since the renderer is executed
+ // from a string I don't think any other form of sourcemaps would work
+
switch (url) {
case "renderer.js.map":
case "preload.js.map":
case "patcher.js.map":
case "main.js.map":
- cb(join(__dirname, url));
- break;
+ return net.fetch(pathToFileURL(join(__dirname, url)).toString());
default:
- cb({ statusCode: 403 });
+ return new Response(null, {
+ status: 404
+ });
}
});
- protocol.registerFileProtocol("equicord", ({ url: unsafeUrl }, cb) => {
- let url = unsafeUrl.slice("equicord://".length);
+ protocol.handle("equicord", ({ url: unsafeUrl }) => {
+ let url = decodeURI(unsafeUrl).slice("equicord://".length).replace(/\?v=\d+$/, "");
+
if (url.endsWith("/")) url = url.slice(0, -1);
+
if (url.startsWith("/themes/")) {
const theme = url.slice("/themes/".length);
+
const safeUrl = ensureSafePath(THEMES_DIR, theme);
if (!safeUrl) {
- cb({ statusCode: 403 });
- return;
+ return new Response(null, {
+ status: 404
+ });
}
- cb(safeUrl.replace(/\?v=\d+$/, ""));
- return;
+
+ return net.fetch(pathToFileURL(safeUrl).toString());
}
+
+ // Source Maps! Maybe there's a better way but since the renderer is executed
+ // from a string I don't think any other form of sourcemaps would work
+
switch (url) {
case "renderer.js.map":
case "preload.js.map":
case "patcher.js.map":
case "main.js.map":
- cb(join(__dirname, url));
- break;
+ return net.fetch(pathToFileURL(join(__dirname, url)).toString());
default:
- cb({ statusCode: 403 });
+ return new Response(null, {
+ status: 404
+ });
}
});
@@ -86,70 +104,7 @@ if (!IS_VANILLA && !IS_EXTENSION) {
} catch { }
- const findHeader = (headers: Record, headerName: Lowercase) => {
- return Object.keys(headers).find(h => h.toLowerCase() === headerName);
- };
-
- // Remove CSP
- type PolicyResult = Record;
-
- const parsePolicy = (policy: string): PolicyResult => {
- const result: PolicyResult = {};
- policy.split(";").forEach(directive => {
- const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
- if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
- result[directiveKey] = directiveValue;
- }
- });
-
- return result;
- };
- const stringifyPolicy = (policy: PolicyResult): string =>
- Object.entries(policy)
- .filter(([, values]) => values?.length)
- .map(directive => directive.flat().join(" "))
- .join("; ");
-
- const patchCsp = (headers: Record) => {
- const header = findHeader(headers, "content-security-policy");
-
- if (header) {
- const csp = parsePolicy(headers[header][0]);
-
- for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
- csp[directive] ??= [];
- csp[directive].push("*", "blob:", "data:", "vencord:", "'unsafe-inline'");
- }
-
- // TODO: Restrict this to only imported packages with fixed version.
- // Perhaps auto generate with esbuild
- csp["script-src"] ??= [];
- csp["script-src"].push("'unsafe-eval'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com");
- headers[header] = [stringifyPolicy(csp)];
- }
- };
-
- session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
- if (responseHeaders) {
- if (resourceType === "mainFrame")
- patchCsp(responseHeaders);
-
- // Fix hosts that don't properly set the css content type, such as
- // raw.githubusercontent.com
- if (resourceType === "stylesheet") {
- const header = findHeader(responseHeaders, "content-type");
- if (header)
- responseHeaders[header] = ["text/css"];
- }
- }
-
- cb({ cancel: false, responseHeaders });
- });
-
- // assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
- // For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
- // impossible to load css from github raw despite our fix above
- session.defaultSession.webRequest.onHeadersReceived = () => { };
+ initCsp();
});
}
diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts
index 43fd2099..f7034a68 100644
--- a/src/main/ipcMain.ts
+++ b/src/main/ipcMain.ts
@@ -34,7 +34,7 @@ import { makeLinksOpenExternally } from "./utils/externalLinks";
mkdirSync(THEMES_DIR, { recursive: true });
export function ensureSafePath(basePath: string, path: string) {
- const normalizedBasePath = normalize(basePath);
+ const normalizedBasePath = normalize(basePath + "/");
const newPath = join(basePath, path);
const normalizedPath = normalize(newPath);
return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null;
diff --git a/src/main/updater/common.ts b/src/main/updater/common.ts
index bd77c417..178266af 100644
--- a/src/main/updater/common.ts
+++ b/src/main/updater/common.ts
@@ -30,7 +30,10 @@ export function serializeErrors(func: (...args: any[]) => any) {
ok: false,
error: e instanceof Error ? {
// prototypes get lost, so turn error into plain object
- ...e
+ ...e,
+ message: e.message,
+ name: e.name,
+ stack: e.stack
} : e
};
}
diff --git a/src/main/updater/http.ts b/src/main/updater/http.ts
index c0affa69..13929daa 100644
--- a/src/main/updater/http.ts
+++ b/src/main/updater/http.ts
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import { get } from "@main/utils/simpleGet";
+import { fetchBuffer, fetchJson } from "@main/utils/http";
import { IpcEvents } from "@shared/IpcEvents";
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
import { ipcMain } from "electron";
@@ -30,8 +30,8 @@ import { ASAR_FILE, serializeErrors } from "./common";
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
let PendingUpdate: string | null = null;
-async function githubGet(endpoint: string) {
- return get(API_BASE + endpoint, {
+async function githubGet(endpoint: string) {
+ return fetchJson(API_BASE + endpoint, {
headers: {
Accept: "application/vnd.github+json",
// "All API requests MUST include a valid User-Agent header.
@@ -45,9 +45,8 @@ async function calculateGitChanges() {
const isOutdated = await fetchUpdates();
if (!isOutdated) return [];
- const res = await githubGet(`/compare/${gitHash}...HEAD`);
+ const data = await githubGet(`/compare/${gitHash}...HEAD`);
- const data = JSON.parse(res.toString("utf-8"));
return data.commits.map((c: any) => ({
// github api only sends the long sha
hash: c.sha.slice(0, 7),
@@ -57,9 +56,8 @@ async function calculateGitChanges() {
}
async function fetchUpdates() {
- const release = await githubGet("/releases/latest");
+ const data = await githubGet("/releases/latest");
- const data = JSON.parse(release.toString());
const hash = data.name.slice(data.name.lastIndexOf(" ") + 1);
if (hash === gitHash)
return false;
@@ -74,7 +72,7 @@ async function fetchUpdates() {
async function applyUpdates() {
if (!PendingUpdate) return true;
- const data = await get(PendingUpdate);
+ const data = await fetchBuffer(PendingUpdate);
originalWriteFileSync(__dirname, data);
PendingUpdate = null;
diff --git a/src/main/utils/extensions.ts b/src/main/utils/extensions.ts
index 4af7129f..b48eb33d 100644
--- a/src/main/utils/extensions.ts
+++ b/src/main/utils/extensions.ts
@@ -24,7 +24,7 @@ import { join } from "path";
import { DATA_DIR } from "./constants";
import { crxToZip } from "./crxToZip";
-import { get } from "./simpleGet";
+import { fetchBuffer } from "./http";
const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
@@ -69,13 +69,14 @@ export async function installExt(id: string) {
} catch (err) {
const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=${process.versions.chrome}`;
- const buf = await get(url, {
+ const buf = await fetchBuffer(url, {
headers: {
"User-Agent": `Electron ${process.versions.electron} ~ Equicord (https://github.com/Equicord/Equicord)`
}
});
- await extract(crxToZip(buf), extDir).catch(console.error);
+ await extract(crxToZip(buf), extDir)
+ .catch(err => console.error(`Failed to extract extension ${id}`, err));
}
session.defaultSession.loadExtension(extDir);
diff --git a/src/main/utils/http.ts b/src/main/utils/http.ts
new file mode 100644
index 00000000..05dbca40
--- /dev/null
+++ b/src/main/utils/http.ts
@@ -0,0 +1,70 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * 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 { createWriteStream } from "original-fs";
+import { Readable } from "stream";
+import { finished } from "stream/promises";
+
+type Url = string | URL;
+
+export async function checkedFetch(url: Url, options?: RequestInit) {
+ try {
+ var res = await fetch(url, options);
+ } catch (err) {
+ if (err instanceof Error && err.cause) {
+ err = err.cause;
+ }
+
+ throw new Error(`${options?.method ?? "GET"} ${url} failed: ${err}`);
+ }
+
+ if (res.ok) {
+ return res;
+ }
+
+ let message = `${options?.method ?? "GET"} ${url}: ${res.status} ${res.statusText}`;
+ try {
+ const reason = await res.text();
+ message += `\n${reason}`;
+ } catch { }
+
+ throw new Error(message);
+}
+
+export async function fetchJson(url: Url, options?: RequestInit) {
+ const res = await checkedFetch(url, options);
+ return res.json() as Promise;
+}
+
+export async function fetchBuffer(url: Url, options?: RequestInit) {
+ const res = await checkedFetch(url, options);
+ const buf = await res.arrayBuffer();
+
+ return Buffer.from(buf);
+}
+
+export async function downloadToFile(url: Url, path: string, options?: RequestInit) {
+ const res = await checkedFetch(url, options);
+ if (!res.body) {
+ throw new Error(`Download ${url}: response body is empty`);
+ }
+
+ // @ts-expect-error weird type conflict
+ const body = Readable.fromWeb(res.body);
+ await finished(body.pipe(createWriteStream(path)));
+}
diff --git a/src/main/utils/simpleGet.ts b/src/main/utils/simpleGet.ts
deleted file mode 100644
index 1a8302c0..00000000
--- a/src/main/utils/simpleGet.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 Vendicated and contributors
- *
- * 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 https from "https";
-
-export function get(url: string, options: https.RequestOptions = {}) {
- return new Promise((resolve, reject) => {
- https.get(url, options, res => {
- const { statusCode, statusMessage, headers } = res;
- if (statusCode! >= 400)
- return void reject(`${statusCode}: ${statusMessage} - ${url}`);
- if (statusCode! >= 300)
- return void resolve(get(headers.location!, options));
-
- const chunks = [] as Buffer[];
- res.on("error", reject);
-
- res.on("data", chunk => chunks.push(chunk));
- res.once("end", () => resolve(Buffer.concat(chunks)));
- });
- });
-}
diff --git a/src/plugins/_api/badges/index.tsx b/src/plugins/_api/badges/index.tsx
index e8724cb3..2fbb40ab 100644
--- a/src/plugins/_api/badges/index.tsx
+++ b/src/plugins/_api/badges/index.tsx
@@ -31,7 +31,7 @@ import { User } from "discord-types/general";
import { EquicordDonorModal, VencordDonorModal } from "./modals";
-const CONTRIBUTOR_BADGE = "https://vencord.dev/assets/favicon.png";
+const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/emojis/1092089799109775453.png?size=64";
const EQUICORD_CONTRIBUTOR_BADGE = "https://i.imgur.com/57ATLZu.png";
const EQUICORD_DONOR_BADGE = "https://cdn.nest.rip/uploads/78cb1e77-b7a6-4242-9089-e91f866159bf.png";
diff --git a/src/plugins/anonymiseFileNames/index.tsx b/src/plugins/anonymiseFileNames/index.tsx
index d835d154..51e3e5ca 100644
--- a/src/plugins/anonymiseFileNames/index.tsx
+++ b/src/plugins/anonymiseFileNames/index.tsx
@@ -79,11 +79,17 @@ export default definePlugin({
patches: [
{
- find: 'type:"UPLOAD_START"',
- replacement: {
- match: /await \i\.uploadFiles\((\i),/,
- replace: "$1.forEach($self.anonymise),$&"
- },
+ find: "async uploadFiles(",
+ replacement: [
+ {
+ match: /async uploadFiles\((\i),\i\){/,
+ replace: "$&$1.forEach($self.anonymise);"
+ },
+ {
+ match: /async uploadFilesSimple\((\i)\){/,
+ replace: "$&$1.forEach($self.anonymise);"
+ }
+ ],
},
{
find: "#{intl::ATTACHMENT_UTILITIES_SPOILER}",
diff --git a/src/plugins/betterFolders/index.tsx b/src/plugins/betterFolders/index.tsx
index 193590a0..3e671ae8 100644
--- a/src/plugins/betterFolders/index.tsx
+++ b/src/plugins/betterFolders/index.tsx
@@ -20,7 +20,6 @@ import "./style.css";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
-import { getIntlMessage } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
import { FluxDispatcher } from "@webpack/common";
@@ -350,7 +349,7 @@ export default definePlugin({
}
try {
- return child?.props?.["aria-label"] === getIntlMessage("SERVERS");
+ return child?.props?.renderTreeNode !== null;
} catch (e) {
console.error(e);
return true;
@@ -390,14 +389,6 @@ export default definePlugin({
}
},
- makeNewButtonFilter(isBetterFolders: boolean) {
- return child => {
- if (!isBetterFolders) return true;
-
- return !child?.props?.barClassName;
- };
- },
-
shouldShowTransition(props: any) {
// Pending guilds
if (props?.folderNode?.id === 1) return true;
diff --git a/src/plugins/decor/index.tsx b/src/plugins/decor/index.tsx
index 248c93bc..05b62667 100644
--- a/src/plugins/decor/index.tsx
+++ b/src/plugins/decor/index.tsx
@@ -139,5 +139,5 @@ export default definePlugin({
}
},
- DecorSection: ErrorBoundary.wrap(DecorSection)
+ DecorSection: ErrorBoundary.wrap(DecorSection, { noop: true })
});
diff --git a/src/plugins/devCompanion.dev/initWs.tsx b/src/plugins/devCompanion.dev/initWs.tsx
index 93b89b68..211dbbcb 100644
--- a/src/plugins/devCompanion.dev/initWs.tsx
+++ b/src/plugins/devCompanion.dev/initWs.tsx
@@ -29,7 +29,7 @@ export let socket: WebSocket | undefined;
export function initWs(isManual = false) {
let wasConnected = isManual;
let hasErrored = false;
- const ws = socket = new WebSocket(`ws://localhost:${PORT}`);
+ const ws = socket = new WebSocket(`ws://127.0.0.1:${PORT}`);
function replyData(data: OutgoingMessage) {
ws.send(JSON.stringify(data));
diff --git a/src/plugins/favGifSearch/index.tsx b/src/plugins/favGifSearch/index.tsx
index 80f7117e..be3d587b 100644
--- a/src/plugins/favGifSearch/index.tsx
+++ b/src/plugins/favGifSearch/index.tsx
@@ -118,7 +118,7 @@ export default definePlugin({
renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) {
this.instance = instance;
return (
-
+
);
diff --git a/src/plugins/mentionAvatars/index.tsx b/src/plugins/mentionAvatars/index.tsx
index 2121d24c..23125339 100644
--- a/src/plugins/mentionAvatars/index.tsx
+++ b/src/plugins/mentionAvatars/index.tsx
@@ -98,7 +98,7 @@ export default definePlugin({
src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${roleId}/${role.icon}.webp?size=24&quality=lossless`}
/>
);
- }),
+ }, { noop: true }),
});
function getUsernameString(username: string) {
diff --git a/src/plugins/messageClickActions/index.ts b/src/plugins/messageClickActions/index.ts
index 19ccaa95..723ece12 100644
--- a/src/plugins/messageClickActions/index.ts
+++ b/src/plugins/messageClickActions/index.ts
@@ -20,7 +20,8 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
-import { FluxDispatcher, PermissionsBits, PermissionStore, UserStore } from "@webpack/common";
+import { FluxDispatcher, PermissionsBits, PermissionStore, UserStore, WindowStore } from "@webpack/common";
+import NoReplyMentionPlugin from "plugins/noReplyMention";
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
const EditStore = findByPropsLazy("isEditing", "isEditingAny");
@@ -28,6 +29,7 @@ const EditStore = findByPropsLazy("isEditing", "isEditingAny");
let isDeletePressed = false;
const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true);
const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false);
+const focusChanged = () => !WindowStore.isFocused() && (isDeletePressed = false);
const settings = definePluginSettings({
enableDeleteOnClick: {
@@ -62,11 +64,13 @@ export default definePlugin({
start() {
document.addEventListener("keydown", keydown);
document.addEventListener("keyup", keyup);
+ WindowStore.addChangeListener(focusChanged);
},
stop() {
document.removeEventListener("keydown", keydown);
document.removeEventListener("keyup", keyup);
+ WindowStore.removeChangeListener(focusChanged);
},
onMessageClick(msg: any, channel, event) {
@@ -89,9 +93,8 @@ export default definePlugin({
if (msg.hasFlag(EPHEMERAL)) return;
const isShiftPress = event.shiftKey && !settings.store.requireModifier;
- const NoReplyMention = Vencord.Plugins.plugins.NoReplyMention as any as typeof import("../noReplyMention").default;
- const shouldMention = Vencord.Plugins.isPluginEnabled("NoReplyMention")
- ? NoReplyMention.shouldMention(msg, isShiftPress)
+ const shouldMention = Vencord.Plugins.isPluginEnabled(NoReplyMentionPlugin.name)
+ ? NoReplyMentionPlugin.shouldMention(msg, isShiftPress)
: !isShiftPress;
FluxDispatcher.dispatch({
diff --git a/src/plugins/messageLinkEmbeds/index.tsx b/src/plugins/messageLinkEmbeds/index.tsx
index 14f75a0e..ea8f3dad 100644
--- a/src/plugins/messageLinkEmbeds/index.tsx
+++ b/src/plugins/messageLinkEmbeds/index.tsx
@@ -20,7 +20,6 @@ import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccesso
import { updateMessage } from "@api/MessageUpdater";
import { definePluginSettings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings";
-import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants.js";
import { classes } from "@utils/misc";
import { Queue } from "@utils/Queue";
@@ -373,7 +372,7 @@ export default definePlugin({
settings,
start() {
- addMessageAccessory("messageLinkEmbed", props => {
+ addMessageAccessory("MessageLinkEmbeds", props => {
if (!messageLinkRegex.test(props.message.content))
return null;
@@ -381,15 +380,13 @@ export default definePlugin({
messageLinkRegex.lastIndex = 0;
return (
-
-
-
+
);
}, 4 /* just above rich embeds */);
},
stop() {
- removeMessageAccessory("messageLinkEmbed");
+ removeMessageAccessory("MessageLinkEmbeds");
}
});
diff --git a/src/plugins/mutualGroupDMs/index.tsx b/src/plugins/mutualGroupDMs/index.tsx
index e46824b4..796a91db 100644
--- a/src/plugins/mutualGroupDMs/index.tsx
+++ b/src/plugins/mutualGroupDMs/index.tsx
@@ -121,14 +121,9 @@ export default definePlugin({
},
// Make the gap between each item smaller so our tab can fit.
{
- match: /className:\i\.tabBar/,
- replace: '$& + " vc-mutual-gdms-modal-v2-tab-bar"'
+ match: /type:"top",/,
+ replace: '$&className:"vc-mutual-gdms-modal-v2-tab-bar",'
},
- // Make the tab bar item text smaller so our tab can fit.
- {
- match: /(\.tabBarItem.+?variant:)"heading-md\/normal"/,
- replace: '$1"heading-sm/normal"'
- }
]
},
{
@@ -209,5 +204,5 @@ export default definePlugin({
/>
>
);
- })
+ }, { noop: true })
});
diff --git a/src/plugins/mutualGroupDMs/style.css b/src/plugins/mutualGroupDMs/style.css
index f0ad3c60..b6d992ad 100644
--- a/src/plugins/mutualGroupDMs/style.css
+++ b/src/plugins/mutualGroupDMs/style.css
@@ -3,5 +3,5 @@
}
.vc-mutual-gdms-modal-v2-tab-bar {
- gap: 12px;
+ --space-xl: 16px;
}
diff --git a/src/plugins/pauseInvitesForever/index.tsx b/src/plugins/pauseInvitesForever/index.tsx
index b648f92e..432d1c1c 100644
--- a/src/plugins/pauseInvitesForever/index.tsx
+++ b/src/plugins/pauseInvitesForever/index.tsx
@@ -75,5 +75,5 @@ export default definePlugin({
}}> Pause Indefinitely.}
);
- })
+ }, { noop: true })
});
diff --git a/src/plugins/quickReply/index.ts b/src/plugins/quickReply/index.ts
index f08e9faa..5a6b45f9 100644
--- a/src/plugins/quickReply/index.ts
+++ b/src/plugins/quickReply/index.ts
@@ -16,19 +16,20 @@
* along with this program. If not, see