diff --git a/src/plugins/alwaysAnimate/index.ts b/src/plugins/alwaysAnimate/index.ts
index dbec3b4e3..20cb4f974 100644
--- a/src/plugins/alwaysAnimate/index.ts
+++ b/src/plugins/alwaysAnimate/index.ts
@@ -31,10 +31,10 @@ export default definePlugin({
// Some modules match the find but the replacement is returned untouched
noWarn: true,
replacement: {
- match: /canAnimate:.+?(?=([,}].*?\)))/g,
+ match: /canAnimate:.+?([,}].*?\))/g,
replace: (m, rest) => {
const destructuringMatch = rest.match(/}=.+/);
- if (destructuringMatch == null) return "canAnimate:!0";
+ if (destructuringMatch == null) return `canAnimate:!0${rest}`;
return m;
}
}
diff --git a/src/plugins/anonymiseFileNames/index.tsx b/src/plugins/anonymiseFileNames/index.tsx
index b424b7a59..526ccd12e 100644
--- a/src/plugins/anonymiseFileNames/index.tsx
+++ b/src/plugins/anonymiseFileNames/index.tsx
@@ -73,13 +73,13 @@ export default definePlugin({
{
find: "instantBatchUpload:function",
replacement: {
- match: /uploadFiles:(.{1,2}),/,
+ match: /uploadFiles:(\i),/,
replace:
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
},
},
{
- find: "message.attachments",
+ find: 'addFilesTo:"message.attachments"',
replacement: {
match: /(\i.uploadFiles\((\i),)/,
replace: "$2.forEach(f=>f.filename=$self.anonymise(f)),$1"
diff --git a/src/plugins/automodContext/README.md b/src/plugins/automodContext/README.md
new file mode 100644
index 000000000..f70d71d90
--- /dev/null
+++ b/src/plugins/automodContext/README.md
@@ -0,0 +1,5 @@
+# AutomodContext
+
+Allows you to jump to the messages surrounding an automod hit
+
+
diff --git a/src/plugins/automodContext/index.tsx b/src/plugins/automodContext/index.tsx
new file mode 100644
index 000000000..5425c5526
--- /dev/null
+++ b/src/plugins/automodContext/index.tsx
@@ -0,0 +1,73 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import ErrorBoundary from "@components/ErrorBoundary";
+import { Devs } from "@utils/constants";
+import definePlugin from "@utils/types";
+import { findByPropsLazy } from "@webpack";
+import { Button, ChannelStore, Text } from "@webpack/common";
+
+const { selectChannel } = findByPropsLazy("selectChannel", "selectVoiceChannel");
+
+function jumpToMessage(channelId: string, messageId: string) {
+ const guildId = ChannelStore.getChannel(channelId)?.guild_id;
+
+ selectChannel({
+ guildId,
+ channelId,
+ messageId,
+ jumpType: "INSTANT"
+ });
+}
+
+function findChannelId(message: any): string | null {
+ const { embeds: [embed] } = message;
+ const channelField = embed.fields.find(({ rawName }) => rawName === "channel_id");
+
+ if (!channelField) {
+ return null;
+ }
+
+ return channelField.rawValue;
+}
+
+export default definePlugin({
+ name: "AutomodContext",
+ description: "Allows you to jump to the messages surrounding an automod hit.",
+ authors: [Devs.JohnyTheCarrot],
+
+ patches: [
+ {
+ find: ".Messages.GUILD_AUTOMOD_REPORT_ISSUES",
+ replacement: {
+ match: /\.Messages\.ACTIONS.+?}\)(?=,(\(0.{0,40}\.dot.*?}\)),)/,
+ replace: (m, dot) => `${m},${dot},$self.renderJumpButton({message:arguments[0].message})`
+ }
+ }
+ ],
+
+ renderJumpButton: ErrorBoundary.wrap(({ message }: { message: any; }) => {
+ const channelId = findChannelId(message);
+
+ if (!channelId) {
+ return null;
+ }
+
+ return (
+
+ );
+ }, { noop: true })
+});
diff --git a/src/plugins/betterFolders/index.tsx b/src/plugins/betterFolders/index.tsx
index d252682f8..38e1b8412 100644
--- a/src/plugins/betterFolders/index.tsx
+++ b/src/plugins/betterFolders/index.tsx
@@ -20,7 +20,7 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
-import { FluxDispatcher, i18n } from "@webpack/common";
+import { FluxDispatcher, i18n, useMemo } from "@webpack/common";
import FolderSideBar from "./FolderSideBar";
@@ -112,13 +112,13 @@ export default definePlugin({
replacement: [
// Create the isBetterFolders variable in the GuildsBar component
{
- match: /(?<=let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?)(?=}=\i,)/,
- replace: ",isBetterFolders"
+ match: /let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?(?=}=\i,)/,
+ replace: "$&,isBetterFolders"
},
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
{
- match: /(useStateFromStoresArray\).{0,25}let \i)=(\i\.\i.getGuildsTree\(\))/,
- replace: (_, rest, guildsTree) => `${rest}=$self.getGuildTree(!!arguments[0].isBetterFolders,${guildsTree},arguments[0].betterFoldersExpandedIds)`
+ match: /\[(\i)\]=(\(0,\i\.useStateFromStoresArray\).{0,40}getGuildsTree\(\).+?}\))(?=,)/,
+ replace: (_, originalTreeVar, rest) => `[betterFoldersOriginalTree]=${rest},${originalTreeVar}=$self.getGuildTree(!!arguments[0].isBetterFolders,betterFoldersOriginalTree,arguments[0].betterFoldersExpandedIds)`
},
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
{
@@ -252,19 +252,21 @@ export default definePlugin({
}
},
- getGuildTree(isBetterFolders: boolean, oldTree: any, expandedFolderIds?: Set
) {
- if (!isBetterFolders || expandedFolderIds == null) return oldTree;
+ getGuildTree(isBetterFolders: boolean, originalTree: any, expandedFolderIds?: Set) {
+ return useMemo(() => {
+ if (!isBetterFolders || expandedFolderIds == null) return originalTree;
- const newTree = new GuildsTree();
- // Children is every folder and guild which is not in a folder, this filters out only the expanded folders
- newTree.root.children = oldTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id));
- // Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them
- newTree.nodes = Object.fromEntries(
- Object.entries(oldTree.nodes)
- .filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId))
- );
+ const newTree = new GuildsTree();
+ // Children is every folder and guild which is not in a folder, this filters out only the expanded folders
+ newTree.root.children = originalTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id));
+ // Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them
+ newTree.nodes = Object.fromEntries(
+ Object.entries(originalTree.nodes)
+ .filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId))
+ );
- return newTree;
+ return newTree;
+ }, [isBetterFolders, originalTree, expandedFolderIds]);
},
makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
diff --git a/src/plugins/betterNotes/index.tsx b/src/plugins/betterNotes/index.tsx
index 2183d98e2..cacdba5fd 100644
--- a/src/plugins/betterNotes/index.tsx
+++ b/src/plugins/betterNotes/index.tsx
@@ -17,6 +17,7 @@
*/
import { Settings } from "@api/Settings";
+import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { canonicalizeMatch } from "@utils/patches";
import definePlugin, { OptionType } from "@utils/types";
@@ -60,7 +61,7 @@ export default definePlugin({
find: ".popularApplicationCommandIds,",
replacement: {
match: /lastSection:(!?\i)}\),/,
- replace: "$&$self.patchPadding($1),"
+ replace: "$&$self.patchPadding({lastSection:$1}),"
}
}
],
@@ -80,10 +81,10 @@ export default definePlugin({
}
},
- patchPadding(lastSection: any) {
- if (!lastSection) return;
+ patchPadding: ErrorBoundary.wrap(({ lastSection }) => {
+ if (!lastSection) return null;
return (
-
+
);
- }
+ })
});
diff --git a/src/plugins/betterRoleContext/README.md b/src/plugins/betterRoleContext/README.md
index 3f3086bdb..e54e1e313 100644
--- a/src/plugins/betterRoleContext/README.md
+++ b/src/plugins/betterRoleContext/README.md
@@ -1,6 +1,6 @@
# BetterRoleContext
-Adds options to copy role color and edit role when right clicking roles in the user profile
+Adds options to copy role color, edit role and view role icon when right clicking roles in the user profile
-
+
diff --git a/src/plugins/betterRoleContext/index.tsx b/src/plugins/betterRoleContext/index.tsx
index 3db3494f9..ecb1ed400 100644
--- a/src/plugins/betterRoleContext/index.tsx
+++ b/src/plugins/betterRoleContext/index.tsx
@@ -4,9 +4,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
+import { definePluginSettings } from "@api/Settings";
+import { ImageIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
-import { getCurrentGuild } from "@utils/discord";
-import definePlugin from "@utils/types";
+import { getCurrentGuild, openImageModal } from "@utils/discord";
+import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Clipboard, GuildStore, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common";
@@ -34,10 +36,34 @@ function AppearanceIcon() {
);
}
+const settings = definePluginSettings({
+ roleIconFileFormat: {
+ type: OptionType.SELECT,
+ description: "File format to use when viewing role icons",
+ options: [
+ {
+ label: "png",
+ value: "png",
+ default: true
+ },
+ {
+ label: "webp",
+ value: "webp",
+ },
+ {
+ label: "jpg",
+ value: "jpg"
+ }
+ ]
+ }
+});
+
export default definePlugin({
name: "BetterRoleContext",
- description: "Adds options to copy role color / edit role when right clicking roles in the user profile",
- authors: [Devs.Ven],
+ description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile",
+ authors: [Devs.Ven, Devs.goodbee],
+
+ settings,
start() {
// DeveloperMode needs to be enabled for the context menu to be shown
@@ -63,6 +89,20 @@ export default definePlugin({
);
}
+ if (role.icon) {
+ children.push(
+ {
+ openImageModal(`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${role.id}/${role.icon}.${settings.store.roleIconFileFormat}`);
+ }}
+ icon={ImageIcon}
+ />
+
+ );
+ }
+
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
children.push(
;
+waitFor(["animating", "baseLayer", "bg", "layer", "layers"], m => Classes = m);
const settings = definePluginSettings({
disableFade: {
@@ -110,26 +111,33 @@ export default definePlugin({
{ // Load menu TOC eagerly
find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format",
replacement: {
- match: /(?<=(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,)(?=\1\(this)/,
- replace: "(async ()=>$2)(),"
+ match: /(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,(?=\1\(this)/,
+ replace: "$&(async ()=>$2)(),"
},
predicate: () => settings.store.eagerLoad
},
{ // Settings cog context menu
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
replacement: {
- match: /\(0,\i.default\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
+ match: /\(0,\i.useDefaultUserSettingsSections\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
replace: "$self.wrapMenu($&)"
}
}
],
+ // This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary
+ // without possibly also catching unrelated errors of children.
+ //
+ // Thus, we sanity check webpack modules & do this really hacky try catch to hopefully prevent hard crashes if something goes wrong.
+ // try catch will only catch errors in the Layer function (hence why it's called as a plain function rather than a component), but
+ // not in children
Layer(props: LayerProps) {
- return (
- props.children as any}>
-
-
- );
+ if (!FocusLock || !ComponentDispatch || !Classes) {
+ new Logger("BetterSettings").error("Failed to find some components");
+ return props.children;
+ }
+
+ return ;
},
wrapMenu(list: SettingsEntry[]) {
diff --git a/src/plugins/colorSighted/index.ts b/src/plugins/colorSighted/index.ts
index d741aaae6..025cfb94e 100644
--- a/src/plugins/colorSighted/index.ts
+++ b/src/plugins/colorSighted/index.ts
@@ -34,9 +34,9 @@ export default definePlugin({
{
find: ".AVATAR_STATUS_MOBILE_16;",
replacement: {
- match: /(?<=fromIsMobile:\i=!0,.+?)status:(\i)/,
+ match: /(fromIsMobile:\i=!0,.+?)status:(\i)/,
// Rename field to force it to always use "online"
- replace: 'status_$:$1="online"'
+ replace: '$1status_$:$2="online"'
}
}
]
diff --git a/src/plugins/crashHandler/index.ts b/src/plugins/crashHandler/index.ts
index f8c76d7f7..3297ca300 100644
--- a/src/plugins/crashHandler/index.ts
+++ b/src/plugins/crashHandler/index.ts
@@ -24,22 +24,20 @@ import { closeAllModals } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { maybePromptToUpdate } from "@utils/updater";
import { filters, findBulk, proxyLazyWebpack } from "@webpack";
-import { FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
+import { DraftType, FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
const CrashHandlerLogger = new Logger("CrashHandler");
-const { ModalStack, DraftManager, DraftType, closeExpressionPicker } = proxyLazyWebpack(() => {
- const modules = findBulk(
+
+const { ModalStack, DraftManager, closeExpressionPicker } = proxyLazyWebpack(() => {
+ const [ModalStack, DraftManager, ExpressionManager] = findBulk(
filters.byProps("pushLazy", "popAll"),
filters.byProps("clearDraft", "saveDraft"),
- filters.byProps("DraftType"),
- filters.byProps("closeExpressionPicker", "openExpressionPicker"),
- );
+ filters.byProps("closeExpressionPicker", "openExpressionPicker"),);
return {
- ModalStack: modules[0],
- DraftManager: modules[1],
- DraftType: modules[2]?.DraftType,
- closeExpressionPicker: modules[3]?.closeExpressionPicker,
+ ModalStack,
+ DraftManager,
+ closeExpressionPicker: ExpressionManager?.closeExpressionPicker,
};
});
@@ -104,7 +102,7 @@ export default definePlugin({
shouldAttemptRecover = false;
// This is enough to avoid a crash loop
- setTimeout(() => shouldAttemptRecover = true, 500);
+ setTimeout(() => shouldAttemptRecover = true, 1000);
} catch { }
try {
@@ -137,8 +135,11 @@ export default definePlugin({
try {
const channelId = SelectedChannelStore.getChannelId();
- DraftManager.clearDraft(channelId, DraftType.ChannelMessage);
- DraftManager.clearDraft(channelId, DraftType.FirstThreadMessage);
+ for (const key in DraftType) {
+ if (!Number.isNaN(Number(key))) continue;
+
+ DraftManager.clearDraft(channelId, DraftType[key]);
+ }
} catch (err) {
CrashHandlerLogger.debug("Failed to clear drafts.", err);
}
diff --git a/src/plugins/ctrlEnterSend/index.ts b/src/plugins/ctrlEnterSend/index.ts
new file mode 100644
index 000000000..4b9dd8e06
--- /dev/null
+++ b/src/plugins/ctrlEnterSend/index.ts
@@ -0,0 +1,68 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2023 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { definePluginSettings } from "@api/Settings";
+import { Devs } from "@utils/constants";
+import definePlugin, { OptionType } from "@utils/types";
+
+export default definePlugin({
+ name: "CtrlEnterSend",
+ authors: [Devs.UlyssesZhan],
+ description: "Use Ctrl+Enter to send messages (customizable)",
+ settings: definePluginSettings({
+ submitRule: {
+ description: "The way to send a message",
+ type: OptionType.SELECT,
+ options: [
+ {
+ label: "Ctrl+Enter (Enter or Shift+Enter for new line)",
+ value: "ctrl+enter"
+ },
+ {
+ label: "Shift+Enter (Enter for new line)",
+ value: "shift+enter"
+ },
+ {
+ label: "Enter (Shift+Enter for new line; Discord default)",
+ value: "enter"
+ }
+ ],
+ default: "ctrl+enter"
+ },
+ sendMessageInTheMiddleOfACodeBlock: {
+ description: "Whether to send a message in the middle of a code block",
+ type: OptionType.BOOLEAN,
+ default: true,
+ }
+ }),
+ patches: [
+ {
+ find: "KeyboardKeys.ENTER&&(!",
+ replacement: {
+ match: /(?<=(\i)\.which===\i\.KeyboardKeys.ENTER&&).{0,100}(\(0,\i\.hasOpenPlainTextCodeBlock\)\(\i\)).{0,100}(?=&&\(\i\.preventDefault)/,
+ replace: "$self.shouldSubmit($1, $2)"
+ }
+ }
+ ],
+ shouldSubmit(event: KeyboardEvent, codeblock: boolean): boolean {
+ let result = false;
+ switch (this.settings.store.submitRule) {
+ case "shift+enter":
+ result = event.shiftKey;
+ break;
+ case "ctrl+enter":
+ result = event.ctrlKey;
+ break;
+ case "enter":
+ result = !event.shiftKey && !event.ctrlKey;
+ break;
+ }
+ if (!this.settings.store.sendMessageInTheMiddleOfACodeBlock) {
+ result &&= !codeblock;
+ }
+ return result;
+ }
+});
diff --git a/src/plugins/customRPC/index.tsx b/src/plugins/customRPC/index.tsx
index 334372e38..f1b2fbf53 100644
--- a/src/plugins/customRPC/index.tsx
+++ b/src/plugins/customRPC/index.tsx
@@ -17,13 +17,16 @@
*/
import { definePluginSettings, Settings } from "@api/Settings";
+import { ErrorCard } from "@components/ErrorCard";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
import { isTruthy } from "@utils/guards";
+import { Margins } from "@utils/margins";
+import { classes } from "@utils/misc";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
-import { ApplicationAssetUtils, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
+import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, StatusSettingsStores, UserStore } from "@webpack/common";
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
@@ -386,17 +389,36 @@ async function setRpc(disable?: boolean) {
export default definePlugin({
name: "CustomRPC",
description: "Allows you to set a custom rich presence.",
- authors: [Devs.captain, Devs.AutumnVN],
+ authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
start: setRpc,
stop: () => setRpc(true),
settings,
settingsAboutComponent: () => {
const activity = useAwaiter(createActivity);
+ const gameActivityEnabled = StatusSettingsStores.ShowCurrentGame.useSetting();
const { profileThemeStyle } = useProfileThemeStyle({});
return (
<>
+ {!gameActivityEnabled && (
+
+ Notice
+ Game activity isn't enabled, people won't be able to see your custom rich presence!
+
+
+
+ )}
+
Go to Discord Developer Portal to create an application and
get the application ID.
@@ -407,7 +429,9 @@ export default definePlugin({
If you want to use image link, download your image and reupload the image to Imgur and get the image link by right-clicking the image and select "Copy image address".
-
+
+
+
{activity[0] &&
noticeMessage === backOnlineMessage)
+ ) return;
+
+ Notices.showNotice(backOnlineMessage, "Exit idle", () => {
+ Notices.popNotice();
+ FluxDispatcher.dispatch({
+ type: "IDLE",
+ idle: false
+ });
+ });
+ },
+
+ getIdleTimeout() { // milliseconds, default is 6e5
+ const { idleTimeout } = settings.store;
+ return idleTimeout === 0 ? Infinity : idleTimeout * 60000;
+ }
+});
diff --git a/src/plugins/dearrow/index.tsx b/src/plugins/dearrow/index.tsx
index b02c80d3d..888e2bb45 100644
--- a/src/plugins/dearrow/index.tsx
+++ b/src/plugins/dearrow/index.tsx
@@ -6,10 +6,11 @@
import "./styles.css";
+import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
-import definePlugin from "@utils/types";
+import definePlugin, { OptionType } from "@utils/types";
import { Tooltip } from "@webpack/common";
import type { Component } from "react";
@@ -34,11 +35,19 @@ interface Props {
};
}
+const enum ReplaceElements {
+ ReplaceAllElements,
+ ReplaceTitlesOnly,
+ ReplaceThumbnailsOnly
+}
+
const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;
async function embedDidMount(this: Component) {
try {
const { embed } = this.props;
+ const { replaceElements } = settings.store;
+
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
const videoId = embedUrlRe.exec(embed.video.url)?.[1];
@@ -58,12 +67,12 @@ async function embedDidMount(this: Component) {
enabled: true
};
- if (hasTitle) {
+ if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {
embed.dearrow.oldTitle = embed.rawTitle;
embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1");
}
- if (hasThumb) {
+ if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
}
@@ -128,10 +137,30 @@ function DearrowButton({ component }: { component: Component; }) {
);
}
+const settings = definePluginSettings({
+ hideButton: {
+ description: "Hides the Dearrow button from YouTube embeds",
+ type: OptionType.BOOLEAN,
+ default: false,
+ restartNeeded: true
+ },
+ replaceElements: {
+ description: "Choose which elements of the embed will be replaced",
+ type: OptionType.SELECT,
+ restartNeeded: true,
+ options: [
+ { label: "Everything (Titles & Thumbnails)", value: ReplaceElements.ReplaceAllElements, default: true },
+ { label: "Titles", value: ReplaceElements.ReplaceTitlesOnly },
+ { label: "Thumbnails", value: ReplaceElements.ReplaceThumbnailsOnly },
+ ],
+ }
+});
+
export default definePlugin({
name: "Dearrow",
description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",
authors: [Devs.Ven],
+ settings,
embedDidMount,
renderButton(component: Component) {
@@ -154,7 +183,8 @@ export default definePlugin({
// add dearrow button
{
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/,
- replace: "children:[$self.renderButton(this),"
+ replace: "children:[$self.renderButton(this),",
+ predicate: () => !settings.store.hideButton
}
]
}],
diff --git a/src/plugins/decor/lib/stores/AuthorizationStore.tsx b/src/plugins/decor/lib/stores/AuthorizationStore.tsx
index e31b1f43c..ba71da99e 100644
--- a/src/plugins/decor/lib/stores/AuthorizationStore.tsx
+++ b/src/plugins/decor/lib/stores/AuthorizationStore.tsx
@@ -9,7 +9,6 @@ import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import { openModal } from "@utils/modal";
import { OAuth2AuthorizeModal, showToast, Toasts, UserStore, zustandCreate, zustandPersist } from "@webpack/common";
-import type { StateStorage } from "zustand/middleware";
import { AUTHORIZE_URL, CLIENT_ID } from "../constants";
@@ -23,7 +22,7 @@ interface AuthorizationState {
isAuthorized: () => boolean;
}
-const indexedDBStorage: StateStorage = {
+const indexedDBStorage = {
async getItem(name: string): Promise {
return DataStore.get(name).then(v => v ?? null);
},
@@ -36,9 +35,9 @@ const indexedDBStorage: StateStorage = {
};
// TODO: Move switching accounts subscription inside the store?
-export const useAuthorizationStore = proxyLazy(() => zustandCreate(
+export const useAuthorizationStore = proxyLazy(() => zustandCreate(
zustandPersist(
- (set, get) => ({
+ (set: any, get: any) => ({
token: null,
tokens: {},
init: () => { set({ token: get().tokens[UserStore.getCurrentUser().id] ?? null }); },
@@ -91,7 +90,7 @@ export const useAuthorizationStore = proxyLazy(() => zustandCreate !!get().token,
- }),
+ } as AuthorizationState),
{
name: "decor-auth",
getStorage: () => indexedDBStorage,
diff --git a/src/plugins/decor/lib/stores/CurrentUserDecorationsStore.ts b/src/plugins/decor/lib/stores/CurrentUserDecorationsStore.ts
index 1485a7438..e2bba6c0a 100644
--- a/src/plugins/decor/lib/stores/CurrentUserDecorationsStore.ts
+++ b/src/plugins/decor/lib/stores/CurrentUserDecorationsStore.ts
@@ -21,7 +21,7 @@ interface UserDecorationsState {
clear: () => void;
}
-export const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate((set, get) => ({
+export const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate((set: any, get: any) => ({
decorations: [],
selectedDecoration: null,
async fetch() {
@@ -53,4 +53,4 @@ export const useCurrentUserDecorationsStore = proxyLazy(() => zustandCreate set({ decorations: [], selectedDecoration: null })
-})));
+} as UserDecorationsState)));
diff --git a/src/plugins/decor/lib/stores/UsersDecorationsStore.ts b/src/plugins/decor/lib/stores/UsersDecorationsStore.ts
index b29945f82..53aa33e63 100644
--- a/src/plugins/decor/lib/stores/UsersDecorationsStore.ts
+++ b/src/plugins/decor/lib/stores/UsersDecorationsStore.ts
@@ -30,7 +30,7 @@ interface UsersDecorationsState {
set: (userId: string, decoration: string | null) => void;
}
-export const useUsersDecorationsStore = proxyLazy(() => zustandCreate((set, get) => ({
+export const useUsersDecorationsStore = proxyLazy(() => zustandCreate((set: any, get: any) => ({
usersDecorations: new Map(),
fetchQueue: new Set(),
bulkFetch: debounce(async () => {
@@ -40,7 +40,7 @@ export const useUsersDecorationsStore = proxyLazy(() => zustandCreate zustandCreate(user ? useUsersDecorationsStore.getState().getAsset(user.id) ?? null : null);
diff --git a/src/plugins/decor/ui/components/DecorSection.tsx b/src/plugins/decor/ui/components/DecorSection.tsx
index f11a87a53..ff044f8c7 100644
--- a/src/plugins/decor/ui/components/DecorSection.tsx
+++ b/src/plugins/decor/ui/components/DecorSection.tsx
@@ -15,7 +15,7 @@ import { openChangeDecorationModal } from "../modals/ChangeDecorationModal";
const CustomizationSection = findByCodeLazy(".customizationSectionBackground");
-interface DecorSectionProps {
+export interface DecorSectionProps {
hideTitle?: boolean;
hideDivider?: boolean;
noMargin?: boolean;
diff --git a/src/plugins/emoteCloner/index.tsx b/src/plugins/emoteCloner/index.tsx
index cd9890a80..b456c351e 100644
--- a/src/plugins/emoteCloner/index.tsx
+++ b/src/plugins/emoteCloner/index.tsx
@@ -24,7 +24,7 @@ import { Margins } from "@utils/margins";
import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal";
import definePlugin from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
-import { EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common";
+import { Constants, EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common";
import { Promisable } from "type-fest";
const StickersStore = findStoreLazy("StickersStore");
@@ -64,7 +64,7 @@ async function fetchSticker(id: string) {
if (cached) return cached;
const { body } = await RestAPI.get({
- url: `/stickers/${id}`
+ url: Constants.Endpoints.STICKER(id)
});
FluxDispatcher.dispatch({
@@ -83,7 +83,7 @@ async function cloneSticker(guildId: string, sticker: Sticker) {
data.append("file", await fetchBlob(getUrl(sticker)));
const { body } = await RestAPI.post({
- url: `/guilds/${guildId}/stickers`,
+ url: Constants.Endpoints.GUILD_STICKER_PACKS(guildId),
body: data,
});
@@ -322,8 +322,9 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =
switch (favoriteableType) {
case "emoji":
const match = props.message.content.match(RegExp(`|https://cdn\\.discordapp\\.com/emojis/${favoriteableId}\\.`));
- if (!match) return;
- const name = match[1] ?? "FakeNitroEmoji";
+ const reaction = props.message.reactions.find(reaction => reaction.emoji.id === favoriteableId);
+ if (!match && !reaction) return;
+ const name = (match && match[1]) ?? reaction?.emoji.name ?? "FakeNitroEmoji";
return buildMenuItem("Emoji", () => ({
id: favoriteableId,
diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx
index 03feda0a8..4ab0e18ee 100644
--- a/src/plugins/fakeNitro/index.tsx
+++ b/src/plugins/fakeNitro/index.tsx
@@ -24,13 +24,12 @@ import { getCurrentGuild } from "@utils/discord";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
-import { Alerts, ChannelStore, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
-import type { CustomEmoji } from "@webpack/types";
+import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
+import type { Emoji } from "@webpack/types";
import type { Message } from "discord-types/general";
import { applyPalette, GIFEncoder, quantize } from "gifenc";
import type { ReactElement, ReactNode } from "react";
-const DRAFT_TYPE = 0;
const StickerStore = findStoreLazy("StickersStore") as {
getPremiumPacks(): StickerPack[];
getAllGuildStickers(): Map;
@@ -39,6 +38,7 @@ const StickerStore = findStoreLazy("StickersStore") as {
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
const ProtoUtils = findByPropsLazy("BINARY_READ_OPTIONS");
+const RoleSubscriptionEmojiUtils = findByPropsLazy("isUnusableRoleSubscriptionEmoji");
function searchProtoClassField(localName: string, protoClass: any) {
const field = protoClass?.fields?.find((field: any) => field.localName === localName);
@@ -54,16 +54,22 @@ const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoCla
const enum EmojiIntentions {
- REACTION = 0,
- STATUS = 1,
- COMMUNITY_CONTENT = 2,
- CHAT = 3,
- GUILD_STICKER_RELATED_EMOJI = 4,
- GUILD_ROLE_BENEFIT_EMOJI = 5,
- COMMUNITY_CONTENT_ONLY = 6,
- SOUNDBOARD = 7
+ REACTION,
+ STATUS,
+ COMMUNITY_CONTENT,
+ CHAT,
+ GUILD_STICKER_RELATED_EMOJI,
+ GUILD_ROLE_BENEFIT_EMOJI,
+ COMMUNITY_CONTENT_ONLY,
+ SOUNDBOARD,
+ VOICE_CHANNEL_TOPIC,
+ GIFT,
+ AUTO_SUGGESTION,
+ POLLS
}
+const IS_BYPASSEABLE_INTENTION = `[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`;
+
const enum StickerType {
PNG = 1,
APNG = 2,
@@ -111,7 +117,7 @@ const hyperLinkRegex = /\[.+?\]\((https?:\/\/.+?)\)/;
const settings = definePluginSettings({
enableEmojiBypass: {
- description: "Allow sending fake emojis",
+ description: "Allows sending fake emojis (also bypasses missing permission to use custom emojis)",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true
@@ -129,7 +135,7 @@ const settings = definePluginSettings({
restartNeeded: true
},
enableStickerBypass: {
- description: "Allow sending fake stickers",
+ description: "Allows sending fake stickers (also bypasses missing permission to use stickers)",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true
@@ -190,7 +196,7 @@ const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, Permi
export default definePlugin({
name: "FakeNitro",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
- description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
+ description: "Allows you to stream in nitro quality, send fake emojis/stickers, use client themes and custom Discord notifications.",
dependencies: ["MessageEventsAPI"],
settings,
@@ -198,37 +204,43 @@ export default definePlugin({
patches: [
{
find: ".PREMIUM_LOCKED;",
+ group: true,
predicate: () => settings.store.enableEmojiBypass,
replacement: [
{
- // Create a variable for the intention of listing the emoji
- match: /(?<=,intention:(\i).+?;)/,
- replace: (_, intention) => `let fakeNitroIntention=${intention};`
+ // Create a variable for the intention of using the emoji
+ match: /(?<=\.USE_EXTERNAL_EMOJIS.+?;)(?<=intention:(\i).+?)/,
+ replace: (_, intention) => `const fakeNitroIntention=${intention};`
},
{
- // Send the intention of listing the emoji to the nitro permission check functions
- match: /\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i(?=\))/g,
- replace: '$&,typeof fakeNitroIntention!=="undefined"?fakeNitroIntention:void 0'
+ // Disallow the emoji for external if the intention doesn't allow it
+ match: /&&!\i&&!\i(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
+ replace: m => `${m}&&!${IS_BYPASSEABLE_INTENTION}`
},
{
- // Disallow the emoji if the intention doesn't allow it
- match: /(&&!\i&&)!(\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
- replace: (_, rest, canUseExternal) => `${rest}(!${canUseExternal}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)))`
+ // Disallow the emoji for unavailable if the intention doesn't allow it
+ match: /!\i\.available(?=\)return \i\.\i\.GUILD_SUBSCRIPTION_UNAVAILABLE;)/,
+ replace: m => `${m}&&!${IS_BYPASSEABLE_INTENTION}`
},
{
- // Make the emoji always available if the intention allows it
- match: /if\(!\i\.available/,
- replace: m => `${m}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))`
+ // Disallow the emoji for premium locked if the intention doesn't allow it
+ match: /!\i\.\i\.canUseEmojisEverywhere\(\i\)/,
+ replace: m => `(${m}&&!${IS_BYPASSEABLE_INTENTION})`
+ },
+ {
+ // Allow animated emojis to be used if the intention allows it
+ match: /(?<=\|\|)\i\.\i\.canUseAnimatedEmojis\(\i\)/,
+ replace: m => `(${m}||${IS_BYPASSEABLE_INTENTION})`
}
]
},
- // Allow emojis and animated emojis to be sent everywhere
+ // Allows the usage of subscription-locked emojis
{
- find: "canUseAnimatedEmojis:function",
- predicate: () => settings.store.enableEmojiBypass,
+ find: "isUnusableRoleSubscriptionEmoji:function",
replacement: {
- match: /((?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))(?=})/g,
- replace: (_, rest, premiumCheck) => `${rest},fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
+ match: /isUnusableRoleSubscriptionEmoji:function/,
+ // Replace the original export with a func that always returns false and alias the original
+ replace: "isUnusableRoleSubscriptionEmoji:()=>()=>false,isUnusableRoleSubscriptionEmojiOriginal:function"
}
},
// Allow stickers to be sent everywhere
@@ -242,10 +254,10 @@ export default definePlugin({
},
// Make stickers always available
{
- find: "\"SENDABLE\"",
+ find: '"SENDABLE"',
predicate: () => settings.store.enableStickerBypass,
replacement: {
- match: /(\w+)\.available\?/,
+ match: /\i\.available\?/,
replace: "true?"
}
},
@@ -332,8 +344,8 @@ export default definePlugin({
{
// Patch the stickers array to add fake nitro stickers
predicate: () => settings.store.transformStickers,
- match: /(?<=renderStickersAccessories\((\i)\){let (\i)=\(0,\i\.\i\)\(\i\).+?;)/,
- replace: (_, message, stickers) => `${stickers}=$self.patchFakeNitroStickers(${stickers},${message});`
+ match: /renderStickersAccessories\((\i)\){let (\i)=\(0,\i\.\i\)\(\i\).+?;/,
+ replace: (m, message, stickers) => `${m}${stickers}=$self.patchFakeNitroStickers(${stickers},${message});`
},
{
// Filter attachments to remove fake nitro stickers or emojis
@@ -797,13 +809,16 @@ export default definePlugin({
gif.finish();
const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
- UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE);
+ UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DraftType.ChannelMessage);
},
- canUseEmote(e: CustomEmoji, channelId: string) {
- if (e.require_colons === false) return true;
+ canUseEmote(e: Emoji, channelId: string) {
+ if (e.type === "UNICODE") return true;
if (e.available === false) return false;
+ const isUnusableRoleSubEmoji = RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmojiOriginal ?? RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmoji;
+ if (isUnusableRoleSubEmoji(e, this.guildId)) return false;
+
if (this.canUseEmotes)
return e.guildId === this.guildId || hasExternalEmojiPerms(channelId);
else
diff --git a/src/plugins/fakeProfileThemes/index.css b/src/plugins/fakeProfileThemes/index.css
new file mode 100644
index 000000000..1c9bebf2d
--- /dev/null
+++ b/src/plugins/fakeProfileThemes/index.css
@@ -0,0 +1,3 @@
+.vc-fpt-preview * {
+ pointer-events: none;
+}
diff --git a/src/plugins/fakeProfileThemes/index.tsx b/src/plugins/fakeProfileThemes/index.tsx
index a1b629d10..7a6bda9a5 100644
--- a/src/plugins/fakeProfileThemes/index.tsx
+++ b/src/plugins/fakeProfileThemes/index.tsx
@@ -17,13 +17,17 @@
*/
// This plugin is a port from Alyxia's Vendetta plugin
+import "./index.css";
+
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins";
-import { copyWithToast } from "@utils/misc";
+import { classes, copyWithToast } from "@utils/misc";
+import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
-import { Button, Forms } from "@webpack/common";
+import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
+import { Button, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
import { User } from "discord-types/general";
import virtualMerge from "virtual-merge";
@@ -81,6 +85,34 @@ const settings = definePluginSettings({
}
});
+interface ColorPickerProps {
+ color: number | null;
+ label: React.ReactElement;
+ showEyeDropper?: boolean;
+ suggestedColors?: string[];
+ onChange(value: number | null): void;
+}
+
+// I can't be bothered to figure out the semantics of this component. The
+// functions surely get some event argument sent to them and they likely aren't
+// all required. If anyone who wants to use this component stumbles across this
+// code, you'll have to do the research yourself.
+interface ProfileModalProps {
+ user: User;
+ pendingThemeColors: [number, number];
+ onAvatarChange: () => void;
+ onBannerChange: () => void;
+ canUsePremiumCustomization: boolean;
+ hideExampleButton: boolean;
+ hideFakeActivity: boolean;
+ isTryItOutFlow: boolean;
+}
+
+const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
+const ProfileModal = findComponentByCodeLazy('"ProfileCustomizationPreview"');
+
+const requireColorPicker = extractAndLoadChunksLazy(["USER_SETTINGS_PROFILE_COLOR_DEFAULT_BUTTON.format"], /createPromise:\(\)=>\i\.\i\("(.+?)"\).then\(\i\.bind\(\i,"(.+?)"\)\)/);
+
export default definePlugin({
name: "FakeProfileThemes",
description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding",
@@ -101,21 +133,98 @@ export default definePlugin({
}
}
],
- settingsAboutComponent: () => (
-
- Usage
-
- After enabling this plugin, you will see custom colors in the profiles of other people using compatible plugins.
- To set your own colors:
-
- - • go to your profile settings
- - • choose your own colors in the Nitro preview
- - • click the "Copy 3y3" button
- - • paste the invisible text anywhere in your bio
-
- Please note: if you are using a theme which hides nitro ads, you should disable it temporarily to set colors.
-
- ),
+ settingsAboutComponent: () => {
+ const existingColors = decode(
+ UserProfileStore.getUserProfile(UserStore.getCurrentUser().id).bio
+ ) ?? [0, 0];
+ const [color1, setColor1] = useState(existingColors[0]);
+ const [color2, setColor2] = useState(existingColors[1]);
+
+ const [, , loadingColorPickerChunk] = useAwaiter(requireColorPicker);
+
+ return (
+
+ Usage
+
+ After enabling this plugin, you will see custom colors in
+ the profiles of other people using compatible plugins.{" "}
+
+ To set your own colors:
+
+ -
+ • use the color pickers below to choose your colors
+
+ - • click the "Copy 3y3" button
+ - • paste the invisible text anywhere in your bio
+
+
+ Color pickers
+ {!loadingColorPickerChunk && (
+
+
+ Primary
+
+ }
+ onChange={(color: number) => {
+ setColor1(color);
+ }}
+ />
+
+ Accent
+
+ }
+ onChange={(color: number) => {
+ setColor2(color);
+ }}
+ />
+
+
+ )}
+
+ Preview
+
+
{ }}
+ onBannerChange={() => { }}
+ canUsePremiumCustomization={true}
+ hideExampleButton={true}
+ hideFakeActivity={true}
+ isTryItOutFlow={true}
+ />
+
+
+ );
+ },
settings,
colorDecodeHook(user: UserProfile) {
if (user) {
diff --git a/src/plugins/friendInvites/index.ts b/src/plugins/friendInvites/index.ts
index e5ff447ed..47e312c31 100644
--- a/src/plugins/friendInvites/index.ts
+++ b/src/plugins/friendInvites/index.ts
@@ -20,7 +20,7 @@ import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption,
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
-import { RestAPI, UserStore } from "@webpack/common";
+import { Constants, RestAPI, UserStore } from "@webpack/common";
const FriendInvites = findByPropsLazy("createFriendInvite");
const { uuid4 } = findByPropsLazy("uuid4");
@@ -58,7 +58,7 @@ export default definePlugin({
if (uses === 1) {
const random = uuid4();
const { body: { invite_suggestions } } = await RestAPI.post({
- url: "/friend-finder/find-friends",
+ url: Constants.Endpoints.FRIEND_FINDER,
body: {
modified_contacts: {
[random]: [1, "", ""]
diff --git a/src/plugins/friendsSince/index.tsx b/src/plugins/friendsSince/index.tsx
index fb431b52b..8ad9dc7f4 100644
--- a/src/plugins/friendsSince/index.tsx
+++ b/src/plugins/friendsSince/index.tsx
@@ -7,6 +7,8 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
+import { Logger } from "@utils/Logger";
+import { classes } from "@utils/misc";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Heading, React, RelationshipStore, Text } from "@webpack/common";
@@ -22,6 +24,7 @@ export default definePlugin({
description: "Shows when you became friends with someone in the user popout",
authors: [Devs.Elvyra],
patches: [
+ // User popup
{
find: ".AnalyticsSections.USER_PROFILE}",
replacement: {
@@ -29,16 +32,34 @@ export default definePlugin({
replace: "$&,$self.friendsSince({ userId: $1 })"
}
},
+ // User DMs "User Profile" popup in the right
{
find: ".UserPopoutUpsellSource.PROFILE_PANEL,",
replacement: {
match: /\i.default,\{userId:(\i)}\)/,
replace: "$&,$self.friendsSince({ userId: $1 })"
}
+ },
+ // User Profile Modal
+ {
+ find: ".userInfoSectionHeader,",
+ replacement: {
+ match: /(\.Messages\.USER_PROFILE_MEMBER_SINCE.+?userId:(.+?),textClassName:)(\i\.userInfoText)}\)/,
+ replace: (_, rest, userId, textClassName) => `${rest}!$self.getFriendSince(${userId}) ? ${textClassName} : void 0 }), $self.friendsSince({ userId: ${userId}, textClassName: ${textClassName} })`
+ }
}
],
- friendsSince: ErrorBoundary.wrap(({ userId }: { userId: string; }) => {
+ getFriendSince(userId: string) {
+ try {
+ return RelationshipStore.getSince(userId);
+ } catch (err) {
+ new Logger("FriendsSince").error(err);
+ return null;
+ }
+ },
+
+ friendsSince: ErrorBoundary.wrap(({ userId, textClassName }: { userId: string; textClassName?: string; }) => {
const friendsSince = RelationshipStore.getSince(userId);
if (!friendsSince) return null;
@@ -61,7 +82,7 @@ export default definePlugin({
)}
-
+
{getCreatedAtDate(friendsSince, locale.getLocale())}
@@ -69,4 +90,3 @@ export default definePlugin({
);
}, { noop: true })
});
-
diff --git a/src/plugins/ignoreActivities/index.tsx b/src/plugins/ignoreActivities/index.tsx
index e2262129d..f687a0caf 100644
--- a/src/plugins/ignoreActivities/index.tsx
+++ b/src/plugins/ignoreActivities/index.tsx
@@ -228,15 +228,15 @@ export default definePlugin({
{
find: ".activityTitleText,variant",
replacement: {
- match: /(?<=\i\.activityTitleText.+?children:(\i)\.name.*?}\),)/,
- replace: (_, props) => `$self.renderToggleActivityButton(${props}),`
+ match: /\.activityTitleText.+?children:(\i)\.name.*?}\),/,
+ replace: (m, props) => `${m}$self.renderToggleActivityButton(${props}),`
},
},
{
find: ".activityCardDetails,children",
replacement: {
- match: /(?<=\i\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),)/,
- replace: (_, props) => `$self.renderToggleActivityButton(${props}),`
+ match: /\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),/,
+ replace: (m, props) => `${m}$self.renderToggleActivityButton(${props}),`
}
}
],
diff --git a/src/plugins/imageZoom/components/Magnifier.tsx b/src/plugins/imageZoom/components/Magnifier.tsx
index 816717350..aadd0903a 100644
--- a/src/plugins/imageZoom/components/Magnifier.tsx
+++ b/src/plugins/imageZoom/components/Magnifier.tsx
@@ -17,6 +17,7 @@
*/
import { classNameFactory } from "@api/Styles";
+import ErrorBoundary from "@components/ErrorBoundary";
import { FluxDispatcher, React, useRef, useState } from "@webpack/common";
import { ELEMENT_ID } from "../constants";
@@ -36,7 +37,7 @@ export interface MagnifierProps {
const cl = classNameFactory("vc-imgzoom-");
-export const Magnifier: React.FC = ({ instance, size: initialSize, zoom: initalZoom }) => {
+export const Magnifier = ErrorBoundary.wrap(({ instance, size: initialSize, zoom: initalZoom }) => {
const [ready, setReady] = useState(false);
const [lensPosition, setLensPosition] = useState({ x: 0, y: 0 });
@@ -199,4 +200,4 @@ export const Magnifier: React.FC = ({ instance, size: initialSiz
)}
);
-};
+}, { noop: true });
diff --git a/src/plugins/index.ts b/src/plugins/index.ts
index 488847d15..a434b4a6f 100644
--- a/src/plugins/index.ts
+++ b/src/plugins/index.ts
@@ -20,6 +20,7 @@ import { registerCommand, unregisterCommand } from "@api/Commands";
import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
import { Settings } from "@api/Settings";
import { Logger } from "@utils/Logger";
+import { canonicalizeFind } from "@utils/patches";
import { Patch, Plugin, StartAt } from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
import { FluxEvents } from "@webpack/types";
@@ -83,8 +84,12 @@ for (const p of pluginsValues) {
if (p.patches && isPluginEnabled(p.name)) {
for (const patch of p.patches) {
patch.plugin = p.name;
- if (!Array.isArray(patch.replacement))
+
+ canonicalizeFind(patch);
+ if (!Array.isArray(patch.replacement)) {
patch.replacement = [patch.replacement];
+ }
+
patches.push(patch);
}
}
@@ -165,13 +170,14 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
}
try {
p.start();
- p.started = true;
} catch (e) {
logger.error(`Failed to start ${name}\n`, e);
return false;
}
}
+ p.started = true;
+
if (commands?.length) {
logger.debug("Registering commands of plugin", name);
for (const cmd of commands) {
@@ -201,6 +207,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) {
const { name, commands, flux, contextMenus } = p;
+
if (p.stop) {
logger.info("Stopping plugin", name);
if (!p.started) {
@@ -209,13 +216,14 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
}
try {
p.stop();
- p.started = false;
} catch (e) {
logger.error(`Failed to stop ${name}\n`, e);
return false;
}
}
+ p.started = false;
+
if (commands?.length) {
logger.debug("Unregistering commands of plugin", name);
for (const cmd of commands) {
diff --git a/src/plugins/invisibleChat.desktop/index.tsx b/src/plugins/invisibleChat.desktop/index.tsx
index fcb0af712..3dfe51e77 100644
--- a/src/plugins/invisibleChat.desktop/index.tsx
+++ b/src/plugins/invisibleChat.desktop/index.tsx
@@ -23,7 +23,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { getStegCloak } from "@utils/dependencies";
import definePlugin, { OptionType } from "@utils/types";
-import { ChannelStore, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common";
+import { ChannelStore, Constants, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common";
import { Message } from "discord-types/general";
import { buildDecModal } from "./components/DecryptionModal";
@@ -153,7 +153,7 @@ export default definePlugin({
// Gets the Embed of a Link
async getEmbed(url: URL): Promise