+
{({ onMouseEnter, onMouseLeave }) => (
)}
@@ -51,125 +49,83 @@ export function UserChatButton({ user }: { user: User; }) {
);
}
+function getUserName(user: User) {
+ const username = `@${user.username}`;
+ if (settings.store.whichNameToShow === "global") {
+ return user.globalName ?? username;
+ } else if (settings.store.whichNameToShow === "nickname") {
+ return username;
+ } else if (settings.store.whichNameToShow === "both" && user.globalName) {
+ return `${user.globalName} / ${username}`;
+ }
+ return username;
+}
+
+export function UserChatButton({ user }: { user: User; }) {
+ const isCurrent = (user.id === UserStore.getCurrentUser().id);
+ return (
+
}
+ onClick={() => {
+ if (isCurrent) {
+ NavigationRouter.transitionTo("/users/@me/");
+ return;
+ }
+ const chanId = ChannelStore.getDMFromUserId(user.id);
+ NavigationRouter.transitionTo(`/channels/@me/${chanId}/`);
+ }}
+ />
+ );
+}
+
export function UserMuteButton({ user }: { user: User; }) {
- if (user.id === UserStore.getCurrentUser().id) return null;
+ const isCurrent = (user.id === UserStore.getCurrentUser().id);
+ const isMuted = (isCurrent && MediaEngineStore.isSelfMute()) || MediaEngineStore.isLocalMute(user.id);
+ const color = isMuted ? "var(--status-danger)" : "var(--channels-default)";
return (
-
-
- {({ onMouseEnter, onMouseLeave }) => (
-
- )}
-
-
- );
-}
-
-function DeafenIcon({ user, ...props }) {
- const isDeafened =
- MediaEngineStore.isLocalMute(user.id) &&
- SoundboardStore.isLocalSoundboardMuted(user.id) &&
- MediaEngineStore.isLocalVideoDisabled(user.id);
-
- return (
-
+
:
}
+ onClick={() => {
+ if (isCurrent) {
+ VoiceActions.toggleSelfMute();
+ return;
+ }
+ VoiceActions.toggleLocalMute(user.id);
+ }}
+ />
);
}
export function UserDeafenButton({ user }: { user: User; }) {
- if (user.id === UserStore.getCurrentUser().id) return null;
- const isDeafened =
- MediaEngineStore.isLocalMute(user.id) &&
- SoundboardStore.isLocalSoundboardMuted(user.id) &&
- MediaEngineStore.isLocalVideoDisabled(user.id);
+ const isCurrent = (user.id === UserStore.getCurrentUser().id);
+ const isDeafened = isCurrent && MediaEngineStore.isSelfDeaf() || MediaEngineStore.isLocalMute(user.id) && SoundboardStore.isLocalSoundboardMuted(user.id) && MediaEngineStore.isLocalVideoDisabled(user.id);
+ const color = isDeafened ? "var(--status-danger)" : "var(--channels-default)";
return (
-
-
- {({ onMouseEnter, onMouseLeave }) => (
-
- )}
-
-
+
:
}
+ onClick={() => {
+ if (isCurrent) {
+ VoiceActions.toggleSelfDeaf();
+ return;
+ }
+ VoiceActions.toggleLocalMute(user.id);
+ if (settings.store.muteSoundboard) {
+ VoiceActions.toggleLocalSoundboardMute(user.id);
+ }
+ if (settings.store.disableVideo) {
+ VoiceActions.setDisableLocalVideo(
+ user.id,
+ MediaEngineStore.isLocalVideoDisabled(user.id) ? "ENABLED" : "DISABLED",
+ "default"
+ );
+ }
+ }}
+ />
);
}
From 7bc6cfb53ab7bc375ef6e137a0cb4cbfa5b79701 Mon Sep 17 00:00:00 2001
From: SerStars <90714930+SerStars@users.noreply.github.com>
Date: Mon, 22 Sep 2025 18:26:03 +0200
Subject: [PATCH 2/8] Option to choose between which service shows the poster
in large image (#411)
---
.../jellyfinRichPresence/index.tsx | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/src/equicordplugins/jellyfinRichPresence/index.tsx b/src/equicordplugins/jellyfinRichPresence/index.tsx
index 4de24d4f5..a8e2e05d6 100644
--- a/src/equicordplugins/jellyfinRichPresence/index.tsx
+++ b/src/equicordplugins/jellyfinRichPresence/index.tsx
@@ -87,6 +87,14 @@ const settings = definePluginSettings({
type: OptionType.BOOLEAN,
default: true,
},
+ posterSource: {
+ description: "Choose which poster to display in Rich Presence",
+ type: OptionType.SELECT,
+ options: [
+ { label: "Jellyfin", value: "jellyfin", default: true },
+ { label: "TheMovieDB", value: "tmdb" },
+ ],
+ },
overrideRichPresenceType: {
description: "Override the rich presence type",
type: OptionType.SELECT,
@@ -310,7 +318,14 @@ export default definePlugin({
}
const assets: ActivityAssets = {
- large_image: tmdbData?.posterPath ? await getApplicationAsset(tmdbData.posterPath) : mediaData.imageUrl ? await getApplicationAsset(mediaData.imageUrl) : undefined,
+ large_image:
+ settings.store.posterSource === "tmdb"
+ ? (tmdbData?.posterPath
+ ? await getApplicationAsset(tmdbData.posterPath)
+ : undefined)
+ : (mediaData.imageUrl
+ ? await getApplicationAsset(mediaData.imageUrl)
+ : undefined),
large_text: mediaData.seriesName || mediaData.album || undefined,
};
From 0d17bae4f0b143e2e5949aa0ea5d25de726e329e Mon Sep 17 00:00:00 2001
From: Etorix <92535668+EtorixDev@users.noreply.github.com>
Date: Mon, 22 Sep 2025 09:31:03 -0700
Subject: [PATCH 3/8] Allow AutoMod & allow hiding ignored typing. (#412)
---
src/plugins/noBlockedMessages/index.ts | 15 +++++++++++----
src/plugins/typingIndicator/index.tsx | 12 ++++++++----
2 files changed, 19 insertions(+), 8 deletions(-)
diff --git a/src/plugins/noBlockedMessages/index.ts b/src/plugins/noBlockedMessages/index.ts
index 951ba2a6e..3532e7444 100644
--- a/src/plugins/noBlockedMessages/index.ts
+++ b/src/plugins/noBlockedMessages/index.ts
@@ -56,6 +56,12 @@ const settings = definePluginSettings({
default: false,
restartNeeded: false
},
+ allowAutoModMessages: {
+ description: "Allow messages sent by AutoMod to bypass filtering.",
+ type: OptionType.BOOLEAN,
+ default: true,
+ restartNeeded: false,
+ },
hideBlockedUserReplies: {
description: "Hide replies to blocked/ignored users.",
type: OptionType.BOOLEAN,
@@ -121,6 +127,7 @@ export default definePlugin({
const blocked = this.isBlocked(message);
const replyToBlocked = this.isReplyToBlocked(message);
+ if (message.type === 24 && settings.store.allowAutoModMessages) return [true, blocked];
if (blocked) return [this.hideBlockedMessage(message.author.id), true];
if (replyToBlocked) return [this.hideBlockedMessage(replyToBlocked.author.id), true];
@@ -140,7 +147,7 @@ export default definePlugin({
},
filterStream(channelStream: [ChannelStreamGroupProps | ChannelStreamMessageProps | ChannelStreamDividerProps]) {
- const { alsoHideIgnoredUsers, disableNotifications, hideBlockedUserReplies, defaultHideUsers, overrideUsers } = settings.use();
+ const { alsoHideIgnoredUsers, disableNotifications, hideBlockedUserReplies, allowAutoModMessages, defaultHideUsers, overrideUsers } = settings.use();
const newChannelStream = channelStream.map(item => {
if (item.type === "MESSAGE_GROUP_BLOCKED" || (item.type === "MESSAGE_GROUP_IGNORED" && alsoHideIgnoredUsers)) {
@@ -158,9 +165,9 @@ export default definePlugin({
let lastItem = newChannelStream[newChannelStream.length - 1];
- // Remove the NEW Message divider if it is the last item,
- // implying the messages it was announcing got filtered.
- while (lastItem && lastItem.type === "DIVIDER" && lastItem.unreadId !== undefined) {
+ // Remove the NEW Message and Date dividers if they are the last
+ // item, implying the messages they were separating got filtered.
+ while (lastItem && lastItem.type === "DIVIDER") {
newChannelStream.pop();
lastItem = newChannelStream[newChannelStream.length - 1];
}
diff --git a/src/plugins/typingIndicator/index.tsx b/src/plugins/typingIndicator/index.tsx
index 445144c42..ecd5875e1 100644
--- a/src/plugins/typingIndicator/index.tsx
+++ b/src/plugins/typingIndicator/index.tsx
@@ -68,7 +68,7 @@ function TypingIndicator({ channelId, guildId }: { channelId: string; guildId: s
const myId = UserStore.getCurrentUser()?.id;
const typingUsersArray = Object.keys(typingUsers).filter(id =>
- id !== myId && !(RelationshipStore.isBlocked(id) && !settings.store.includeBlockedUsers)
+ id !== myId && !(RelationshipStore.isBlocked(id) && !settings.store.includeBlockedUsers) && !(RelationshipStore.isIgnored(id) && !settings.store.includeIgnoredUsers)
);
const [a, b, c] = typingUsersArray;
let tooltipText: string;
@@ -145,6 +145,11 @@ const settings = definePluginSettings({
description: "Whether to show the typing indicator for muted channels.",
default: false
},
+ includeIgnoredUsers: {
+ type: OptionType.BOOLEAN,
+ description: "Whether to show the typing indicator for ignored users.",
+ default: false
+ },
includeBlockedUsers: {
type: OptionType.BOOLEAN,
description: "Whether to show the typing indicator for blocked users.",
@@ -168,17 +173,16 @@ export default definePlugin({
settings,
patches: [
- // Normal channel
{
+ // Normal channel.
find: "UNREAD_IMPORTANT:",
replacement: {
match: /\.Children\.count.+?:null(?<=,channel:(\i).+?)/,
replace: "$&,$self.TypingIndicator($1.id,$1.getGuildId())"
}
},
- // Theads
{
- // This is the thread "spine" that shows in the left
+ // Thread "spine" that shows in the left.
find: "M0 15H2c0 1.6569",
replacement: {
match: /mentionsCount:\i.+?null(?<=channel:(\i).+?)/,
From 9f2f511ff48f3e34f4ce4d32236070f76cf0965b Mon Sep 17 00:00:00 2001
From: thororen1234 <78185467+thororen1234@users.noreply.github.com>
Date: Mon, 22 Sep 2025 12:41:52 -0400
Subject: [PATCH 4/8] ListenBrainzRPC
---
src/equicordplugins/listenBrainzRPC/index.tsx | 371 ++++++++++++++++++
src/utils/constants.ts | 6 +-
2 files changed, 376 insertions(+), 1 deletion(-)
create mode 100644 src/equicordplugins/listenBrainzRPC/index.tsx
diff --git a/src/equicordplugins/listenBrainzRPC/index.tsx b/src/equicordplugins/listenBrainzRPC/index.tsx
new file mode 100644
index 000000000..5f87751d5
--- /dev/null
+++ b/src/equicordplugins/listenBrainzRPC/index.tsx
@@ -0,0 +1,371 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2025 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { definePluginSettings } from "@api/Settings";
+import { Link } from "@components/Link";
+import { EquicordDevs } from "@utils/constants";
+import { Logger } from "@utils/Logger";
+import definePlugin, { OptionType } from "@utils/types";
+import { Activity, ActivityAssets, ActivityButton } from "@vencord/discord-types";
+import { ActivityFlags, ActivityType } from "@vencord/discord-types/enums";
+import { findByPropsLazy } from "@webpack";
+import { ApplicationAssetUtils, FluxDispatcher, Forms } from "@webpack/common";
+
+interface TrackData {
+ name: string;
+ album: string;
+ artist: string;
+ durationMs?: number;
+ recordingMBID?: string;
+ url: string;
+ imageUrl?: string;
+}
+
+const enum NameFormat {
+ StatusName = "status-name",
+ ArtistFirst = "artist-first",
+ SongFirst = "song-first",
+ ArtistOnly = "artist",
+ SongOnly = "song",
+ AlbumName = "album",
+}
+
+const applicationId = "1090155131007406132";
+const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f";
+
+const logger = new Logger("ListenBrainzRPC");
+
+const PresenceStore = findByPropsLazy("getLocalPresence");
+
+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: "ListenBrainz",
+ });
+}
+
+const settings = definePluginSettings({
+ username: {
+ description: "ListenBrainz username",
+ type: OptionType.STRING,
+ },
+ mbContact: {
+ description: "ListenBrainz contact",
+ type: OptionType.STRING,
+ },
+ shareUsername: {
+ description:
+ "show link to ListenBrainz profile (may only be visible to other users)",
+ type: OptionType.BOOLEAN,
+ default: false,
+ },
+ shareSong: {
+ description:
+ "show link to song on ListenBrainz (may only be visible to other users)",
+ type: OptionType.BOOLEAN,
+ default: true,
+ },
+ hideWithSpotify: {
+ description: "hide ListenBrainz presence if Spotify is running",
+ type: OptionType.BOOLEAN,
+ default: true,
+ },
+ hideWithActivity: {
+ description: "hide ListenBrainz presence if you have any other presence",
+ type: OptionType.BOOLEAN,
+ default: false,
+ },
+ useTimeBar: {
+ description:
+ "use track duration to display a time bar (must be using Listening status)",
+ type: OptionType.BOOLEAN,
+ default: true,
+ },
+ statusName: {
+ description: "custom status text",
+ type: OptionType.STRING,
+ default: "some music",
+ },
+ nameFormat: {
+ description: "Show name of song and artist in status name",
+ type: OptionType.SELECT,
+ options: [
+ {
+ label: "Use custom status name",
+ value: NameFormat.StatusName,
+ default: true,
+ },
+ {
+ label: "Use format 'artist - song'",
+ value: NameFormat.ArtistFirst,
+ },
+ {
+ label: "Use format 'song - artist'",
+ value: NameFormat.SongFirst,
+ },
+ {
+ label: "Use artist name only",
+ value: NameFormat.ArtistOnly,
+ },
+ {
+ label: "Use song name only",
+ value: NameFormat.SongOnly,
+ },
+ {
+ label:
+ "Use album name (falls back to custom status text if song has no album)",
+ value: NameFormat.AlbumName,
+ },
+ ],
+ },
+ useListeningStatus: {
+ description: 'show "Listening to" status instead of "Playing"',
+ type: OptionType.BOOLEAN,
+ default: true,
+ },
+ missingArt: {
+ description: "When album or album art is missing",
+ type: OptionType.SELECT,
+ options: [
+ {
+ label: "Use large ListenBrainz logo",
+ value: "listenbrainzLogo",
+ default: true,
+ },
+ {
+ label: "Use generic placeholder",
+ value: "placeholder",
+ },
+ ],
+ },
+ useLogo: {
+ description: "Show ListenBrainz logo on album art",
+ type: OptionType.BOOLEAN,
+ default: true,
+ },
+});
+
+var currentRecordingMBID = "";
+var currentStart = 0;
+
+export default definePlugin({
+ name: "ListenBrainzRPC",
+ description: "Little plugin for ListenBrainz rich presence",
+ authors: [EquicordDevs.qouesm],
+
+ settingsAboutComponent: () => (
+ <>
+ About MusicBrainz API
+
+ The MusicBrainz API does not require an API key, but it does require a{" "}
+
+ {" "}
+ meaningful user-agent string
+ {" "}
+ . For most, an email address should suffice.
+
+ >
+ ),
+
+ settings,
+
+ start() {
+ this.updatePresence();
+ this.updateInterval = setInterval(() => {
+ this.updatePresence();
+ }, 16000);
+ },
+
+ stop() {
+ clearInterval(this.updateInterval);
+ },
+
+ async fetchTrackData(): Promise {
+ if (!settings.store.username) return null;
+
+ try {
+ const lbRes = await fetch(
+ `https://api.listenbrainz.org/1/user/${settings.store.username}/playing-now`,
+ );
+ if (!lbRes.ok) throw `${lbRes.status} ${lbRes.statusText}`;
+
+ const lbJson = await lbRes.json();
+ if (lbJson.error) {
+ logger.error(
+ "Error from ListenBrainz API",
+ `${lbJson.error}: ${lbJson.message}`,
+ );
+ return null;
+ }
+
+ const listen = lbJson.payload?.listens?.[0];
+ if (!listen?.playing_now) return null;
+
+ const trackMetadata = listen.track_metadata;
+ const albumName = trackMetadata.release_name || "Unknown";
+ const artistName = trackMetadata.artist_name || "Unknown";
+
+ const mbRes = await fetch(`https://musicbrainz.org/ws/2/release/?query=release:${encodeURIComponent(albumName)}%20AND%20artist:${encodeURIComponent(artistName)}&fmt=json`,);
+
+ if (!mbRes.ok) throw `${mbRes.status} ${mbRes.statusText}`;
+
+ const mbJson = await mbRes.json();
+ const releases = mbJson.releases || [];
+
+ const releaseGroup = releases[0]["release-group"].id;
+
+ const caaRes = await fetch(
+ `https://coverartarchive.org/release-group/${releaseGroup}`,
+ );
+ if (!caaRes.ok) throw `${caaRes.status} ${caaRes.statusText}`;
+ const caaJson = await caaRes.json();
+
+ const url: string = caaJson.release;
+
+ const { images } = caaJson;
+ let imageUrl: string = "";
+ for (const image of images) {
+ imageUrl = image.thumbnails.large || "";
+ if (!imageUrl) continue;
+ break;
+ }
+
+ return {
+ name: trackMetadata.track_name || "Unknown",
+ album: albumName,
+ artist: artistName,
+ durationMs: trackMetadata.additional_info.duration_ms,
+ recordingMBID: trackMetadata.additional_info.recording_mbid,
+ url: url,
+ imageUrl: imageUrl,
+ };
+ } catch (e) {
+ logger.error("Failed to query ListenBrainz API", e);
+ // will clear the rich presence if API fails
+ return null;
+ }
+ },
+
+ async updatePresence() {
+ setActivity(await this.getActivity());
+ },
+
+ getLargeImage(track: TrackData): string | undefined {
+ if (track.imageUrl && !track.imageUrl.includes(placeholderId))
+ return track.imageUrl;
+
+ if (settings.store.missingArt === "placeholder") return "placeholder";
+ },
+
+ async getActivity(): Promise {
+ if (settings.store.hideWithActivity) {
+ if (
+ PresenceStore.getActivities().some(
+ a => a.application_id !== applicationId,
+ )
+ ) {
+ return null;
+ }
+ }
+
+ if (settings.store.hideWithSpotify) {
+ if (
+ PresenceStore.getActivities().some(
+ a =>
+ a.type === ActivityType.LISTENING &&
+ a.application_id !== applicationId,
+ )
+ ) {
+ // there is already music status because of Spotify or richerCider (probably more)
+ return null;
+ }
+ }
+
+ const trackData = await this.fetchTrackData();
+ if (!trackData) return null;
+
+ const largeImage = this.getLargeImage(trackData);
+ const assets: ActivityAssets = largeImage
+ ? {
+ large_image: await getApplicationAsset(largeImage),
+ large_text: trackData.album || undefined,
+ small_image: settings.store.useLogo
+ ? await getApplicationAsset("listenbrainz")
+ : undefined,
+ small_text: "ListenBrainz",
+ }
+ : {
+ large_image: await getApplicationAsset("listenbrainz"),
+ large_text: trackData.album || undefined,
+ };
+
+ const buttons: ActivityButton[] = [];
+
+ if (settings.store.shareUsername)
+ buttons.push({
+ label: "ListenBrainz Profile",
+ url: `https://www.listenbrainz.org/user/${settings.store.username}`,
+ });
+
+ if (settings.store.shareSong)
+ buttons.push({
+ label: "View Song",
+ url: trackData.url,
+ });
+
+ const statusName = (() => {
+ switch (settings.store.nameFormat) {
+ case NameFormat.ArtistFirst:
+ return trackData.artist + " - " + trackData.name;
+ case NameFormat.SongFirst:
+ return trackData.name + " - " + trackData.artist;
+ case NameFormat.ArtistOnly:
+ return trackData.artist;
+ case NameFormat.SongOnly:
+ return trackData.name;
+ case NameFormat.AlbumName:
+ return trackData.album || settings.store.statusName;
+ default:
+ return settings.store.statusName;
+ }
+ })();
+
+ if (trackData.recordingMBID && trackData.recordingMBID !== currentRecordingMBID) {
+ currentRecordingMBID = trackData.recordingMBID;
+ currentStart = Date.now();
+ }
+
+ return {
+ application_id: applicationId,
+ name: statusName,
+
+ details: trackData.name,
+ state: trackData.artist,
+ assets,
+
+ timestamps: {
+ start: settings.store.useTimeBar ? currentStart : undefined,
+ end: settings.store.useTimeBar
+ ? currentStart + (trackData.durationMs ?? 0)
+ : undefined,
+ },
+ buttons: buttons.length ? buttons.map(v => v.label) : undefined,
+ metadata: {
+ button_urls: buttons.map(v => v.url),
+ },
+
+ type: settings.store.useListeningStatus
+ ? ActivityType.LISTENING
+ : ActivityType.PLAYING,
+ flags: ActivityFlags.INSTANCE,
+ };
+ },
+});
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index cec3b5004..e37abdc79 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -1152,7 +1152,11 @@ export const EquicordDevs = Object.freeze({
nicola02nb: {
name: "nicola02nb",
id: 257900031351193600n
- }
+ },
+ qouesm: {
+ name: "qouesm",
+ id: 130388483494641664n,
+ },
} satisfies Record);
// iife so #__PURE__ works correctly
From aaecebf1f49502538027e1a15d9def3d07f30ece Mon Sep 17 00:00:00 2001
From: Etorix <92535668+EtorixDev@users.noreply.github.com>
Date: Mon, 22 Sep 2025 14:14:59 -0700
Subject: [PATCH 5/8] Add support for changing other users' messages client
side. (#413)
---
src/plugins/textReplace/index.tsx | 101 +++++++++++++++++++++---------
1 file changed, 70 insertions(+), 31 deletions(-)
diff --git a/src/plugins/textReplace/index.tsx b/src/plugins/textReplace/index.tsx
index 3b46df9d8..bd975dfa1 100644
--- a/src/plugins/textReplace/index.tsx
+++ b/src/plugins/textReplace/index.tsx
@@ -19,15 +19,13 @@
import { definePluginSettings } from "@api/Settings";
import { Flex } from "@components/Flex";
import { DeleteIcon } from "@components/Icons";
-import { Devs } from "@utils/constants";
+import { Devs, EquicordDevs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
-import { Button, Forms, React, TextInput, useState } from "@webpack/common";
+import { Message } from "@vencord/discord-types";
+import { Button, Forms, React, Select, TextInput, UserStore, useState } from "@webpack/common";
-const STRING_RULES_KEY = "TextReplace_rulesString";
-const REGEX_RULES_KEY = "TextReplace_rulesRegex";
-
-type Rule = Record<"find" | "replace" | "onlyIfIncludes", string>;
+type Rule = Record<"find" | "replace" | "onlyIfIncludes" | "scope", string>;
interface TextReplaceProps {
title: string;
@@ -37,7 +35,8 @@ interface TextReplaceProps {
const makeEmptyRule: () => Rule = () => ({
find: "",
replace: "",
- onlyIfIncludes: ""
+ onlyIfIncludes: "",
+ scope: "myMessages"
});
const makeEmptyRuleArray = () => [makeEmptyRule()];
@@ -65,12 +64,12 @@ const settings = definePluginSettings({
stringRules: {
type: OptionType.CUSTOM,
default: makeEmptyRuleArray(),
- description: "",
+ description: "Rules for replacing text using string matching."
},
regexRules: {
type: OptionType.CUSTOM,
default: makeEmptyRuleArray(),
- description: "",
+ description: "Rules for replacing text using regular expressions."
}
});
@@ -138,10 +137,16 @@ function TextReplace({ title, rulesArray }: TextReplaceProps) {
}
}
+ const scopeOptions = [
+ { label: "Apply to your messages (visible to everyone)", value: "myMessages" },
+ { label: "Apply to others' messages (only visible to you)", value: "othersMessages" },
+ { label: "Apply to all messages", value: "allMessages" }
+ ];
+
return (
<>
{title}
-
+
{
rulesArray.map((rule, index) =>
@@ -163,24 +168,25 @@ function TextReplace({ title, rulesArray }: TextReplaceProps) {
onChange={e => onChange(e, index, "onlyIfIncludes")}
/>
+
+ {(index !== rulesArray.length - 1) &&
+
+
-
+ }
+ {(index !== rulesArray.length - 1) && }
{isRegexRules && renderFindError(rule.find)}
)
@@ -196,12 +202,12 @@ function TextReplaceTesting() {
<>
Test Rules
-
+
>
);
}
-function applyRules(content: string): string {
+function applyRules(content: string, scope: "myMessages" | "othersMessages" | "allMessages"): string {
if (content.length === 0) {
return content;
}
@@ -209,6 +215,7 @@ function applyRules(content: string): string {
for (const rule of settings.store.stringRules) {
if (!rule.find) continue;
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
+ if (rule.scope !== "allMessages" && rule.scope !== scope && scope !== "allMessages") continue;
content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, "");
}
@@ -216,6 +223,7 @@ function applyRules(content: string): string {
for (const rule of settings.store.regexRules) {
if (!rule.find) continue;
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
+ if (rule.scope !== "allMessages" && rule.scope !== scope && scope !== "allMessages") continue;
try {
const regex = stringToRegex(rule.find);
@@ -229,18 +237,49 @@ function applyRules(content: string): string {
return content;
}
-const TEXT_REPLACE_RULES_CHANNEL_ID = "1102784112584040479";
+function modifyIncomingMessage(message: Message) {
+ const currentUser = UserStore.getCurrentUser();
+ const messageAuthor = message.author;
+
+ if (!message.content || !currentUser?.id || !messageAuthor?.id || messageAuthor.id === currentUser.id) {
+ return message.content;
+ }
+
+ return applyRules(message.content, "othersMessages");
+}
+
+const TEXT_REPLACE_RULES_EXEMPT_CHANNEL_IDS = [
+ "1102784112584040479", // Vencord's Text Replace Rules Channel
+ "1419347113745059961", // Equicord's Requests Channel
+];
export default definePlugin({
name: "TextReplace",
- description: "Replace text in your messages. You can find pre-made rules in the #textreplace-rules channel in Vencord's Server",
- authors: [Devs.AutumnVN, Devs.TheKodeToad],
+ description: "Replace text in your or others' messages. You can find pre-made rules in the #textreplace-rules channel in Vencord's Server.",
+ authors: [Devs.AutumnVN, Devs.TheKodeToad, EquicordDevs.Etorix],
settings,
+ modifyIncomingMessage,
+
+ patches: [
+ {
+ find: "!1,hideSimpleEmbedContent",
+ replacement: {
+ match: /(let{toAST:.{0,125}?)\(null!=\i\?\i:\i\).content/,
+ replace: "const textReplaceContent=$self.modifyIncomingMessage(arguments[2]?.contentMessage??arguments[1]);$1textReplaceContent"
+ }
+ },
+ ],
+
+ start() {
+ const { stringRules, regexRules } = settings.store;
+ stringRules.forEach(rule => { if (!rule.scope) rule.scope = "myMessages"; });
+ regexRules.forEach(rule => { if (!rule.scope) rule.scope = "myMessages"; });
+ },
onBeforeMessageSend(channelId, msg) {
- // Channel used for sharing rules, applying rules here would be messy
- if (channelId === TEXT_REPLACE_RULES_CHANNEL_ID) return;
- msg.content = applyRules(msg.content);
+ // Replacing text in channels used for sharing/requesting rules may be messy.
+ if (TEXT_REPLACE_RULES_EXEMPT_CHANNEL_IDS.includes(channelId)) return;
+ msg.content = applyRules(msg.content, "myMessages");
}
});
From 94d5a631e5c98d5f18a21618b8d6d33200f42d46 Mon Sep 17 00:00:00 2001
From: thororen1234 <78185467+thororen1234@users.noreply.github.com>
Date: Mon, 22 Sep 2025 20:44:05 -0400
Subject: [PATCH 6/8] Questify: Fix RadioGroup Find
---
src/equicordplugins/questify/utils/components.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/equicordplugins/questify/utils/components.tsx b/src/equicordplugins/questify/utils/components.tsx
index d182e35e0..933163320 100644
--- a/src/equicordplugins/questify/utils/components.tsx
+++ b/src/equicordplugins/questify/utils/components.tsx
@@ -16,7 +16,7 @@ export const GuildlessServerListItemComponent = findComponentByCodeLazy("tooltip
export const GuildedServerListItemPillComponent = findComponentByCodeLazy('"pill":"empty"');
export const ServerListItemLowerBadgeComponent = findComponentByCodeLazy("STATUS_DANGER,disableColor:");
export const ServerListItemUpperBadgeComponent = findComponentByCodeLazy("STATUS_DANGER,shape:", "iconBadge,");
-export const RadioGroup = findComponentByCodeLazy("\"label\",\"description\"]);");
+export const RadioGroup = findComponentByCodeLazy('["label"', "description:");
export const QuestTile = findComponentByCodeLazy(".rowIndex,trackGuildAndChannelMetadata");
export enum QuestRewardType {
From c36716744186e5d0b3f0e0608051f1c2166f310d Mon Sep 17 00:00:00 2001
From: thororen1234 <78185467+thororen1234@users.noreply.github.com>
Date: Mon, 22 Sep 2025 22:11:36 -0400
Subject: [PATCH 7/8] SortFriendRequests: Add SortFriendList
---
src/plugins/sortFriendRequests/index.tsx | 80 +++++++++++++++++++-----
src/utils/constants.ts | 4 ++
2 files changed, 68 insertions(+), 16 deletions(-)
diff --git a/src/plugins/sortFriendRequests/index.tsx b/src/plugins/sortFriendRequests/index.tsx
index 42af9a3af..d53e6edbf 100644
--- a/src/plugins/sortFriendRequests/index.tsx
+++ b/src/plugins/sortFriendRequests/index.tsx
@@ -21,10 +21,10 @@ import "./styles.css";
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
-import { Devs } from "@utils/constants";
+import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { User } from "@vencord/discord-types";
-import { DateUtils, RelationshipStore, Text, TooltipContainer } from "@webpack/common";
+import { DateUtils, Flex, RelationshipStore, Text, TooltipContainer } from "@webpack/common";
import { PropsWithChildren } from "react";
const formatter = new Intl.DateTimeFormat(undefined, {
@@ -50,24 +50,34 @@ const settings = definePluginSettings({
export default definePlugin({
name: "SortFriendRequests",
- authors: [Devs.Megu],
+ authors: [Devs.Megu, EquicordDevs.CallMeGii],
description: "Sorts friend requests by date of receipt",
settings,
- patches: [{
- find: "getRelationshipCounts(){",
- replacement: {
- match: /\}\)\.sortBy\((.+?)\)\.value\(\)/,
- replace: "}).sortBy(row => $self.wrapSort(($1), row)).value()"
+ patches: [
+ {
+ find: "getRelationshipCounts(){",
+ replacement: {
+ match: /\}\)\.sortBy\((.+?)\)\.value\(\)/,
+ replace: "}).sortBy(row => $self.wrapSort(($1), row)).value()"
+ }
+ }, {
+ find: "#{intl::FRIEND_REQUEST_CANCEL}",
+ replacement: {
+ predicate: () => settings.store.showDates,
+ match: /(?<=\.listItemContents,children:\[)\(0,.+?(?=,\(0)(?<=user:(\i).+?)/,
+ replace: (children, user) => `$self.WrapperDateComponent({user:${user},children:${children}})`
+ }
+ },
+ {
+ find: "peopleListItemRef",
+ replacement: {
+ predicate: () => settings.store.showDates,
+ match: /(?<=children:.*user:(\i),.*subText:).+?(?=,hovered:\i,showAccountIdentifier)/,
+ replace: "$self.makeSubtext($1, $&)"
+ }
}
- }, {
- find: "#{intl::FRIEND_REQUEST_CANCEL}",
- replacement: {
- predicate: () => settings.store.showDates,
- match: /(?<=\.listItemContents,children:\[)\(0,.+?(?=,\(0)(?<=user:(\i).+?)/,
- replace: (children, user) => `$self.WrapperDateComponent({user:${user},children:${children}})`
- }
- }],
+ ],
wrapSort(comparator: Function, row: any) {
return row.type === 3 || row.type === 4
@@ -75,6 +85,44 @@ export default definePlugin({
: comparator(row);
},
+ makeSubtext(user: User, origSubtext: any) {
+ const since = this.getSince(user);
+ if (isNaN(since.getTime())) {
+ return null;
+ }
+
+ return (
+
+ {origSubtext}
+
+
+
+
Added — {since.toDateString()}
+
+
+
+ );
+ },
+
WrapperDateComponent: ErrorBoundary.wrap(({ user, children }: PropsWithChildren<{ user: User; }>) => {
const since = getSince(user);
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index e37abdc79..b2e3e5544 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -1157,6 +1157,10 @@ export const EquicordDevs = Object.freeze({
name: "qouesm",
id: 130388483494641664n,
},
+ CallMeGii: {
+ name: "gii",
+ id: 156481332652802048n,
+ },
} satisfies Record);
// iife so #__PURE__ works correctly
From 14dc683c9cde3e53b1a81c09e26d73f5900aa6cf Mon Sep 17 00:00:00 2001
From: thororen1234 <78185467+thororen1234@users.noreply.github.com>
Date: Tue, 23 Sep 2025 08:42:27 -0400
Subject: [PATCH 8/8] Settings Redesign: Allow Opening Legacy Settings
---
src/plugins/_core/settings.tsx | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx
index 665687b39..974cf7d4a 100644
--- a/src/plugins/_core/settings.tsx
+++ b/src/plugins/_core/settings.tsx
@@ -65,6 +65,15 @@ export default definePlugin({
}
]
},
+ {
+ find: ".DEVELOPER_SECTION,",
+ replacement: [
+ {
+ match: /\i\.\i\.isDeveloper/,
+ replace: "true"
+ },
+ ]
+ },
{
find: "#{intl::USER_SETTINGS_ACTIONS_MENU_LABEL}",
replacement: {