Compare commits

...

35 Commits

Author SHA1 Message Date
Lewis Crichton
5d1736d020 feat: rework consent 2023-10-28 18:07:19 +01:00
Lewis Crichton
3de02708a6 chore: add TODO comment as reminder 2023-10-28 13:23:50 +01:00
Lewis Crichton
49c331fcc9 feat: anonymous telemetry 2023-10-28 13:19:59 +01:00
Lewis Crichton
ba53acdca7 chore: dedupe platform consts 2023-10-28 12:46:48 +01:00
Nuckyz
bfb48b4faf BetterFolders: Option to choose whether to keep guild icons (#1918) 2023-10-28 04:00:17 +00:00
Hugo C
9dd8e72245 fix: PiP replacing video download button (#1910) 2023-10-28 02:22:04 +02:00
AutumnVN
aae790f1c1 vencordToolbox: correct hover color + oneko (#1913) 2023-10-28 02:21:35 +02:00
AutumnVN
7f17e70697 noSystemBadge: fix (#1914) 2023-10-28 02:20:29 +02:00
Vendicated
b3311c6f12 BetterGifAltText: Fix displaying undefined 2023-10-28 02:19:43 +02:00
Nuckyz
bc09225258 Add missing patches predicates to BetterFolders 2023-10-27 20:10:44 -03:00
Nuckyz
9ce923d4d7 Fix BetterFolders 2023-10-27 19:56:11 -03:00
Nuckyz
7845af0802 Remove hacks to support no longer active versions of Discord 2023-10-27 19:55:39 -03:00
Nuckyz
89672882b9 PermViewer: Fix incorrectly displaying some role names as Unknown Role 2023-10-27 14:33:18 -03:00
Nuckyz
e05c630a54 SHC: Make Chat Input Bar channel list include hidden channels 2023-10-27 14:29:25 -03:00
Nuckyz
38834ef7ac Fix git updater 2023-10-27 13:03:52 -03:00
Luna
98d49af728 Fix git updater for other branches (#1915) 2023-10-27 12:09:39 -03:00
Susheel Thapa
0afe319141 fix typo in multiple files (#1911) 2023-10-27 12:09:38 -03:00
Vendicated
a9e67e2955 Bump to v1.6.1 2023-10-27 03:53:58 +02:00
Nuckyz
1676956f61 Fix hidden channels triggering the unread box 2023-10-26 20:59:48 -03:00
Nuckyz
8692109bc5 Add comments to some SHC patches 2023-10-26 18:13:25 -03:00
Nuckyz
0847f205b8 Fix some hidden channels not collapsing 2023-10-26 18:11:00 -03:00
AutumnVN
2f94e167c4 noProfileThemes: fix usrbg compatibility (#1905) 2023-10-26 22:52:48 +02:00
Vendicated
589c070773 fix not removing vencord 'chunks' from webpack 2023-10-26 22:51:07 +02:00
Nuckyz
8567ff6239 Little bit of SHC cleanup 2023-10-26 16:51:37 -03:00
Vendicated
e4701769a5 Fix entry webpack modules not being patched 2023-10-26 21:21:21 +02:00
Vendicated
25f101602d fix modules being patched multiple times 2023-10-26 21:03:05 +02:00
Nuckyz
85bfa1e719 Fix duplicated WebContextMenus find 2023-10-26 15:36:05 -03:00
Nuckyz
b48998d485 More accurate ShowAllMessageButtons patch 2023-10-26 15:25:29 -03:00
Nuckyz
6d605050e1 Fix BetterNoteBox 2023-10-26 15:07:44 -03:00
TheKodeToad
07c4a097e0 Fix EmoteCloner (#1907) 2023-10-26 13:49:18 -03:00
TheKodeToad
c1de41436a Fix plugins using promptToUpload (#1908) 2023-10-26 13:49:06 -03:00
Nuckyz
64c6f5740f Fix FakeNitro completely (#1903) 2023-10-26 03:19:26 +00:00
AutumnVN
03523446c1 silentTyping: fix showIcon toggle (#1898) 2023-10-26 00:50:33 +00:00
Vendicated
25dc25c707 Fix VoiceMessages 2023-10-26 02:28:17 +02:00
Hugo C
8ac8048845 fix: ImageZoom + PiP (#1894)
Co-authored-by: V <vendicated@riseup.net>
2023-10-26 02:17:31 +02:00
41 changed files with 739 additions and 539 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.6.0", "version": "1.6.1",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {

View File

@ -289,7 +289,7 @@ function runTime(token: string) {
setTimeout(() => console.log("PUPPETEER_TEST_DONE_SIGNAL"), 1000); setTimeout(() => console.log("PUPPETEER_TEST_DONE_SIGNAL"), 1000);
}, 1000)); }, 1000));
} catch (e) { } catch (e) {
console.error("[PUP_DEBUG]", "A fatal error occured"); console.error("[PUP_DEBUG]", "A fatal error occurred");
console.error("[PUP_DEBUG]", e); console.error("[PUP_DEBUG]", e);
process.exit(1); process.exit(1);
} }

View File

@ -34,6 +34,7 @@ import { patches, PMLogger, startAllPlugins } from "./plugins";
import { localStorage } from "./utils/localStorage"; import { localStorage } from "./utils/localStorage";
import { relaunch } from "./utils/native"; import { relaunch } from "./utils/native";
import { getCloudSettings, putCloudSettings } from "./utils/settingsSync"; import { getCloudSettings, putCloudSettings } from "./utils/settingsSync";
import { sendTelemetry } from "./utils/telemetry";
import { checkForUpdates, update, UpdateLogger } from "./utils/updater"; import { checkForUpdates, update, UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack"; import { onceReady } from "./webpack";
import { SettingsRouter } from "./webpack/common"; import { SettingsRouter } from "./webpack/common";
@ -83,6 +84,8 @@ async function init() {
syncSettings(); syncSettings();
sendTelemetry();
if (!IS_WEB) { if (!IS_WEB) {
try { try {
const isOutdated = await checkForUpdates(); const isOutdated = await checkForUpdates();

View File

@ -69,7 +69,7 @@ export function addGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback)
* Remove a context menu patch * Remove a context menu patch
* @param navId The navId(s) for the context menu(s) to remove the patch * @param navId The navId(s) for the context menu(s) to remove the patch
* @param patch The patch to be removed * @param patch The patch to be removed
* @returns Wheter the patch was sucessfully removed from the context menu(s) * @returns Whether the patch was successfully removed from the context menu(s)
*/ */
export function removeContextMenuPatch<T extends string | Array<string>>(navId: T, patch: NavContextMenuPatchCallback): T extends string ? boolean : Array<boolean> { export function removeContextMenuPatch<T extends string | Array<string>>(navId: T, patch: NavContextMenuPatchCallback): T extends string ? boolean : Array<boolean> {
const navIds = Array.isArray(navId) ? navId : [navId as string]; const navIds = Array.isArray(navId) ? navId : [navId as string];
@ -82,7 +82,7 @@ export function removeContextMenuPatch<T extends string | Array<string>>(navId:
/** /**
* Remove a global context menu patch * Remove a global context menu patch
* @param patch The patch to be removed * @param patch The patch to be removed
* @returns Wheter the patch was sucessfully removed * @returns Whether the patch was successfully removed
*/ */
export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback): boolean { export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback): boolean {
return globalPatches.delete(patch); return globalPatches.delete(patch);

View File

@ -61,6 +61,8 @@ export interface Settings {
settingsSync: boolean; settingsSync: boolean;
settingsSyncVersion: number; settingsSyncVersion: number;
}; };
telemetry?: boolean; // tri-state, undefined = ask
} }
const DefaultSettings: Settings = { const DefaultSettings: Settings = {
@ -91,7 +93,9 @@ const DefaultSettings: Settings = {
url: "https://api.vencord.dev/", url: "https://api.vencord.dev/",
settingsSync: false, settingsSync: false,
settingsSyncVersion: 0 settingsSyncVersion: 0
} },
telemetry: undefined
}; };
try { try {

View File

@ -46,7 +46,7 @@ function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>
if (code === "ENOENT") if (code === "ENOENT")
var err = `Command \`${path}\` not found.\nPlease install it and try again`; var err = `Command \`${path}\` not found.\nPlease install it and try again`;
else { else {
var err = `An error occured while running \`${cmd}\`:\n`; var err = `An error occurred while running \`${cmd}\`:\n`;
err += stderr || `Code \`${code}\`. See the console for more info`; err += stderr || `Code \`${code}\`. See the console for more info`;
} }

View File

@ -21,6 +21,7 @@ import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton"; import DonateButton from "@components/DonateButton";
import { ErrorCard } from "@components/ErrorCard"; import { ErrorCard } from "@components/ErrorCard";
import { isMac, isWindows } from "@utils/constants";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { identity } from "@utils/misc"; import { identity } from "@utils/misc";
import { relaunch, showItemInFolder } from "@utils/native"; import { relaunch, showItemInFolder } from "@utils/native";
@ -46,9 +47,6 @@ function VencordSettings() {
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []); const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
const isWindows = navigator.platform.toLowerCase().startsWith("win");
const isMac = navigator.platform.toLowerCase().startsWith("mac");
const Switches: Array<false | { const Switches: Array<false | {
key: KeysOfType<typeof settings, boolean>; key: KeysOfType<typeof settings, boolean>;
title: string; title: string;
@ -93,6 +91,11 @@ function VencordSettings() {
key: "macosTranslucency", key: "macosTranslucency",
title: "Enable translucent window", title: "Enable translucent window",
note: "Requires a full restart" note: "Requires a full restart"
},
{
key: "telemetry",
title: "Enable Telemetry",
note: "We only gather anonymous telemetry data. All data deleted after 3 days if you opt out."
} }
]; ];

View File

@ -49,7 +49,9 @@ async function getRepo() {
async function calculateGitChanges() { async function calculateGitChanges() {
await git("fetch"); await git("fetch");
const res = await git("log", "HEAD...origin/main", "--pretty=format:%an/%h/%s"); const branch = await git("branch", "--show-current");
const res = await git("log", `HEAD...origin/${branch.stdout.trim()}`, "--pretty=format:%an/%h/%s");
const commits = res.stdout.trim(); const commits = res.stdout.trim();
return commits ? commits.split("\n").map(line => { return commits ? commits.split("\n").map(line => {

View File

@ -16,56 +16,34 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Settings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { findByPropsLazy, findStoreLazy } from "@webpack"; import { LazyComponent } from "@utils/react";
import { i18n, React, useStateFromStores } from "@webpack/common"; import { find, findByPropsLazy } from "@webpack";
import { React, useStateFromStores } from "@webpack/common";
const cl = classNameFactory("vc-bf-"); import { ExpandedGuildFolderStore, settings } from ".";
const classes = findByPropsLazy("sidebar", "guilds");
const Animations = findByPropsLazy("a", "animated", "useTransition"); const Animations = findByPropsLazy("a", "animated", "useTransition");
const ChannelRTCStore = findStoreLazy("ChannelRTCStore"); const GuildsBar = LazyComponent(() => find(m => m.type?.toString().includes('("guildsnav")')));
const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
function Guilds(props: { export default ErrorBoundary.wrap(guildsBarProps => {
className: string;
bfGuildFolders: any[];
}) {
// @ts-expect-error
const res = Vencord.Plugins.plugins.BetterFolders.Guilds(props);
// TODO: Make this better
const scrollerProps = res.props.children?.props?.children?.props?.children?.[1]?.props;
if (scrollerProps?.children) {
const servers = scrollerProps.children.find(c => c?.props?.["aria-label"] === i18n.Messages.SERVERS);
if (servers) scrollerProps.children = servers;
}
return res;
}
export default ErrorBoundary.wrap(() => {
const expandedFolders = useStateFromStores([ExpandedGuildFolderStore], () => ExpandedGuildFolderStore.getExpandedFolders()); const expandedFolders = useStateFromStores([ExpandedGuildFolderStore], () => ExpandedGuildFolderStore.getExpandedFolders());
const fullscreen = useStateFromStores([ChannelRTCStore], () => ChannelRTCStore.isFullscreenInContext());
const guilds = document.querySelector(`.${classes.guilds}`);
const visible = !!expandedFolders.size;
const className = cl("folder-sidebar", { fullscreen });
const Sidebar = ( const Sidebar = (
<Guilds <GuildsBar
className={classes.guilds} {...guildsBarProps}
bfGuildFolders={Array.from(expandedFolders)} isBetterFolders={true}
/> />
); );
if (!guilds || !Settings.plugins.BetterFolders.sidebarAnim) const visible = !!expandedFolders.size;
const guilds = document.querySelector(guildsBarProps.className.split(" ").map(c => `.${c}`).join(""));
if (!guilds || !settings.store.sidebarAnim) {
return visible return visible
? <div className={className}>{Sidebar}</div> ? <div style={{ display: "flex " }}>{Sidebar}</div>
: null; : null;
}
return ( return (
<Animations.Transition <Animations.Transition
@ -75,11 +53,13 @@ export default ErrorBoundary.wrap(() => {
leave={{ width: 0 }} leave={{ width: 0 }}
config={{ duration: 200 }} config={{ duration: 200 }}
> >
{(style, show) => show && ( {(style, show) =>
<Animations.animated.div style={style} className={className}> show && (
{Sidebar} <Animations.animated.div style={{ ...style, display: "flex" }}>
</Animations.animated.div> {Sidebar}
)} </Animations.animated.div>
)
}
</Animations.Transition> </Animations.Transition>
); );
}, { noop: true }); }, { noop: true });

View File

@ -1,17 +0,0 @@
.vc-bf-folder-sidebar [class*="wrapper-"] > [class*="listItem-"]:first-of-type,
.vc-bf-folder-sidebar [class*="unreadMentionsIndicator"] {
display: none;
}
.vc-bf-folder-sidebar [class*="expandedFolderBackground-"] {
background-color: transparent;
}
.vc-bf-folder-sidebar {
display: flex;
}
.vc-bf-fullscreen {
width: 0 !important;
visibility: hidden;
}

View File

@ -1,177 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 <https://www.gnu.org/licenses/>.
*/
import "./betterFolders.css";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
import { FluxDispatcher } from "@webpack/common";
import FolderSideBar from "./FolderSideBar";
const GuildsTree = findLazy(m => m.prototype?.convertToFolder);
const GuildFolderStore = findStoreLazy("SortedGuildStore");
const ExpandedFolderStore = findStoreLazy("ExpandedGuildFolderStore");
const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");
const settings = definePluginSettings({
sidebar: {
type: OptionType.BOOLEAN,
description: "Display servers from folder on dedicated sidebar",
default: true,
},
sidebarAnim: {
type: OptionType.BOOLEAN,
description: "Animate opening the folder sidebar",
default: true,
},
closeAllFolders: {
type: OptionType.BOOLEAN,
description: "Close all folders when selecting a server not in a folder",
default: false,
},
closeAllHomeButton: {
type: OptionType.BOOLEAN,
description: "Close all folders when clicking on the home button",
default: false,
},
closeOthers: {
type: OptionType.BOOLEAN,
description: "Close other folders when opening a folder",
default: false,
},
forceOpen: {
type: OptionType.BOOLEAN,
description: "Force a folder to open when switching to a server of that folder",
default: false,
},
});
export default definePlugin({
name: "BetterFolders",
description: "Shows server folders on dedicated sidebar and adds folder related improvements",
authors: [Devs.juby, Devs.AutumnVN],
patches: [
{
find: '("guildsnav")',
predicate: () => settings.store.sidebar,
replacement: [
{
match: /(\i)\(\){return \i\(\(0,\i\.jsx\)\("div",{className:\i\(\)\.guildSeparator}\)\)}/,
replace: "$&$self.Separator=$1;"
},
// Folder component patch
{
match: /\i\(\(function\(\i,\i,\i\){var \i=\i\.key;return.+\(\i\)},\i\)}\)\)/,
replace: "arguments[0].bfHideServers?null:$&"
},
// BEGIN Guilds component patch
{
match: /(\i)\.themeOverride,(.{15,25}\(function\(\){var \i=)(\i\.\i\.getGuildsTree\(\))/,
replace: "$1.themeOverride,bfPatch=$1.bfGuildFolders,$2bfPatch?$self.getGuildsTree(bfPatch,$3):$3"
},
{
match: /return(\(0,\i\.jsx\))(\(\i,{)(folderNode:\i,setNodeRef:\i\.setNodeRef,draggable:!0,.+},\i\.id\));case/,
replace: "var bfHideServers=typeof bfPatch==='undefined',folder=$1$2bfHideServers,$3;return !bfHideServers&&arguments[1]?[$1($self.Separator,{}),folder]:folder;case"
},
// END
{
match: /\("guildsnav"\);return\(0,\i\.jsx\)\(.{1,6},{navigator:\i,children:\(0,\i\.jsx\)\(/,
replace: "$&$self.Guilds="
}
]
},
{
find: "APPLICATION_LIBRARY,render",
predicate: () => settings.store.sidebar,
replacement: {
match: /(\(0,\i\.jsx\))\(\i\..,{className:\i\(\)\.guilds,themeOverride:\i}\)/,
replace: "$&,$1($self.FolderSideBar,{})"
}
},
{
find: '("guildsnav")',
predicate: () => settings.store.closeAllHomeButton,
replacement: {
match: ",onClick:function(){if(!__OVERLAY__){",
replace: "$&$self.closeFolders();"
}
}
],
settings,
start() {
const getGuildFolder = (id: string) => GuildFolderStore.getGuildFolders().find(f => f.guildIds.includes(id));
FluxDispatcher.subscribe("CHANNEL_SELECT", this.onSwitch = data => {
if (!settings.store.closeAllFolders && !settings.store.forceOpen)
return;
if (this.lastGuildId !== data.guildId) {
this.lastGuildId = data.guildId;
const guildFolder = getGuildFolder(data.guildId);
if (guildFolder?.folderId) {
if (settings.store.forceOpen && !ExpandedFolderStore.isFolderExpanded(guildFolder.folderId))
FolderUtils.toggleGuildFolderExpand(guildFolder.folderId);
} else if (settings.store.closeAllFolders)
this.closeFolders();
}
});
FluxDispatcher.subscribe("TOGGLE_GUILD_FOLDER_EXPAND", this.onToggleFolder = e => {
if (settings.store.closeOthers && !this.dispatching)
FluxDispatcher.wait(() => {
const expandedFolders = ExpandedFolderStore.getExpandedFolders();
if (expandedFolders.size > 1) {
this.dispatching = true;
for (const id of expandedFolders) if (id !== e.folderId)
FolderUtils.toggleGuildFolderExpand(id);
this.dispatching = false;
}
});
});
},
stop() {
FluxDispatcher.unsubscribe("CHANNEL_SELECT", this.onSwitch);
FluxDispatcher.unsubscribe("TOGGLE_GUILD_FOLDER_EXPAND", this.onToggleFolder);
},
FolderSideBar,
getGuildsTree(folders, oldTree) {
const tree = new GuildsTree();
tree.root.children = oldTree.root.children.filter(e => folders.includes(e.id));
tree.nodes = folders.map(id => oldTree.nodes[id]);
return tree;
},
closeFolders() {
for (const id of ExpandedFolderStore.getExpandedFolders())
FolderUtils.toggleGuildFolderExpand(id);
},
});

View File

@ -0,0 +1,236 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 <https://www.gnu.org/licenses/>.
*/
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 FolderSideBar from "./FolderSideBar";
const GuildFolderStore = findStoreLazy("SortedGuildStore");
export const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");
let lastGuildId = null as string | null;
let dispatchingFoldersClose = false;
function getGuildFolder(id: string) {
return GuildFolderStore.getGuildFolders().find(folder => folder.guildIds.includes(id));
}
function closeFolders() {
for (const id of ExpandedGuildFolderStore.getExpandedFolders())
FolderUtils.toggleGuildFolderExpand(id);
}
export const settings = definePluginSettings({
sidebar: {
type: OptionType.BOOLEAN,
description: "Display servers from folder on dedicated sidebar",
restartNeeded: true,
default: true
},
sidebarAnim: {
type: OptionType.BOOLEAN,
description: "Animate opening the folder sidebar",
restartNeeded: true,
default: true
},
closeAllFolders: {
type: OptionType.BOOLEAN,
description: "Close all folders when selecting a server not in a folder",
default: false
},
closeAllHomeButton: {
type: OptionType.BOOLEAN,
description: "Close all folders when clicking on the home button",
restartNeeded: true,
default: false
},
closeOthers: {
type: OptionType.BOOLEAN,
description: "Close other folders when opening a folder",
default: false
},
forceOpen: {
type: OptionType.BOOLEAN,
description: "Force a folder to open when switching to a server of that folder",
default: false
},
keepIcons: {
type: OptionType.BOOLEAN,
description: "Keep showing guild icons in the primary guild bar folder when it's open in the BetterFolders sidebar",
restartNeeded: true,
default: false
}
});
export default definePlugin({
name: "BetterFolders",
description: "Shows server folders on dedicated sidebar and adds folder related improvements",
authors: [Devs.juby, Devs.AutumnVN, Devs.Nuckyz],
settings,
patches: [
{
find: '("guildsnav")',
predicate: () => settings.store.sidebar,
replacement: [
// Create the isBetterFolders variable in the GuildsBar component
{
match: /(?<=let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?)(?=}=\i,)/,
replace: ",isBetterFolders"
},
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
{
match: /lastTargetNode:\i\[\i\.length-1\].+?Fragment.+?\]}\)\]/,
replace: '$&.filter($self.makeGuildsBarGuildListFilter(typeof isBetterFolders!=="undefined"?isBetterFolders:false))'
},
// If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children
{
match: /unreadMentionsIndicatorBottom,barClassName.+?}\)\]/,
replace: '$&.filter($self.makeGuildsBarTreeFilter(typeof isBetterFolders!=="undefined"?isBetterFolders:false))'
},
// Export the isBetterFolders variable to the folders component
{
match: /(?<=\.Messages\.SERVERS.+?switch\((\i)\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,)/,
replace: 'isBetterFolders:typeof isBetterFolders!=="undefined"?isBetterFolders:false,'
},
// Avoid rendering servers that are not in folders in the Better Folders sidebar
{
match: /(?<=\.Messages\.SERVERS.+?switch\((\i)\.type\){case \i\.\i\.FOLDER:.+?GUILD:)/,
replace: 'if((typeof isBetterFolders!=="undefined"?isBetterFolders:false)&&$1.parentId==null)return null;'
}
]
},
{
find: ".FOLDER_ITEM_GUILD_ICON_MARGIN);",
predicate: () => settings.store.sidebar,
replacement: [
// Create the isBetterFolders variable in the nested folders component (the parent exports all the props so we don't have to patch it)
{
match: /(?<=let{folderNode:\i,setNodeRef:\i,)/,
replace: "isBetterFolders,"
},
// If we are rendering the normal GuildsBar sidebar, we make Discord think the folder is always collapsed to show better icons (the mini guild icons) and avoid transitions
{
predicate: () => settings.store.keepIcons,
match: /(?<=let{folderNode:\i,setNodeRef:\i,.+?expanded:(\i).+?;)(?=let)/,
replace: '$1=(typeof isBetterFolders!=="undefined"?isBetterFolders:false)?$1:false;'
},
// If we are rendering the Better Folders sidebar, we filter out folders that are not expanded
{
match: /(?=return\(0,\i.\i\)\("div")(?<=selected:\i,expanded:(\i),.+?)/,
replace: (_, expanded) => `if((typeof isBetterFolders!=="undefined"?isBetterFolders:false)&&!${expanded})return null;`
},
// Disable expanding and collapsing folders transition in the normal GuildsBar sidebar
{
predicate: () => !settings.store.keepIcons,
match: /(?<=\.Messages\.SERVER_FOLDER_PLACEHOLDER.+?useTransition\)\()/,
replace: '(typeof isBetterFolders!=="undefined"?isBetterFolders:false)&&'
},
// If we are rendering the normal GuildsBar sidebar, we avoid rendering guilds from folders that are expanded
{
predicate: () => !settings.store.keepIcons,
match: /expandedFolderBackground,.+?,(?=\i\(\(\i,\i,\i\)=>{let{key.{0,45}ul)(?<=selected:\i,expanded:(\i),.+?)/,
replace: (m, expanded) => `${m}((typeof isBetterFolders!=="undefined"?isBetterFolders:false)||!${expanded})&&`
}
]
},
{
find: "APPLICATION_LIBRARY,render",
predicate: () => settings.store.sidebar,
replacement: {
// Render the Better Folders sidebar
match: /(?<=({className:\i\.guilds,themeOverride:\i})\))/,
replace: ",$self.FolderSideBar($1)"
}
},
{
find: ".Messages.DISCODO_DISABLED",
predicate: () => settings.store.closeAllHomeButton,
replacement: {
// Close all folders when clicking the home button
match: /(?<=onClick:\(\)=>{)(?=.{0,200}"discodo")/,
replace: "$self.closeFolders();"
}
}
],
flux: {
CHANNEL_SELECT(data) {
if (!settings.store.closeAllFolders && !settings.store.forceOpen)
return;
if (lastGuildId !== data.guildId) {
lastGuildId = data.guildId;
const guildFolder = getGuildFolder(data.guildId);
if (guildFolder?.folderId) {
if (settings.store.forceOpen && !ExpandedGuildFolderStore.isFolderExpanded(guildFolder.folderId)) {
FolderUtils.toggleGuildFolderExpand(guildFolder.folderId);
}
} else if (settings.store.closeAllFolders) {
closeFolders();
}
}
},
TOGGLE_GUILD_FOLDER_EXPAND(data) {
if (settings.store.closeOthers && !dispatchingFoldersClose) {
dispatchingFoldersClose = true;
FluxDispatcher.wait(() => {
const expandedFolders = ExpandedGuildFolderStore.getExpandedFolders();
if (expandedFolders.size > 1) {
for (const id of expandedFolders) if (id !== data.folderId)
FolderUtils.toggleGuildFolderExpand(id);
}
dispatchingFoldersClose = false;
});
}
}
},
makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
return child => {
if (isBetterFolders) {
return child?.props?.["aria-label"] === i18n.Messages.SERVERS;
}
return true;
};
},
makeGuildsBarTreeFilter(isBetterFolders: boolean) {
return child => {
if (isBetterFolders) {
return "onScroll" in child.props;
}
return true;
};
},
FolderSideBar: guildsBarProps => <FolderSideBar {...guildsBarProps} />,
closeFolders
});

View File

@ -45,7 +45,8 @@ export default definePlugin({
], ],
altify(props: any) { altify(props: any) {
if (props.alt && props.alt !== "GIF") return props.alt; props.alt ??= "GIF";
if (props.alt !== "GIF") return props.alt;
let url: string = props.original || props.src; let url: string = props.original || props.src;
try { try {

View File

@ -32,10 +32,16 @@ export default definePlugin({
{ {
find: "hideNote:", find: "hideNote:",
all: true, all: true,
// Some modules match the find but the replacement is returned untouched
noWarn: true,
predicate: () => Vencord.Settings.plugins.BetterNotesBox.hide, predicate: () => Vencord.Settings.plugins.BetterNotesBox.hide,
replacement: { replacement: {
match: /hideNote:.+?(?=[,}])/g, match: /hideNote:.+?(?=([,}].*?\)))/g,
replace: "hideNote:true", replace: (m, rest) => {
const destructuringMatch = rest.match(/}=.+/);
if (destructuringMatch == null) return "hideNote:!0";
return m;
}
} }
}, },
{ {

View File

@ -395,7 +395,7 @@ export default definePlugin({
return ( return (
<> <>
<Forms.FormText> <Forms.FormText>
Go to <Link href="https://discord.com/developers/applications">Discord Deverloper Portal</Link> to create an application and Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
get the application ID. get the application ID.
</Forms.FormText> </Forms.FormText>
<Forms.FormText> <Forms.FormText>

View File

@ -23,12 +23,12 @@ import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal"; import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByCodeLazy, findStoreLazy } from "@webpack"; import { findByPropsLazy, findStoreLazy } from "@webpack";
import { EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common"; import { EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common";
import { Promisable } from "type-fest"; import { Promisable } from "type-fest";
const StickersStore = findStoreLazy("StickersStore"); const StickersStore = findStoreLazy("StickersStore");
const uploadEmoji = findByCodeLazy('"EMOJI_UPLOAD_START"', "GUILD_EMOJIS("); const EmojiManager = findByPropsLazy("fetchEmoji", "uploadEmoji", "deleteEmoji");
interface Sticker { interface Sticker {
t: "Sticker"; t: "Sticker";
@ -106,7 +106,7 @@ async function cloneEmoji(guildId: string, emoji: Emoji) {
reader.readAsDataURL(data); reader.readAsDataURL(data);
}); });
return uploadEmoji({ return EmojiManager.uploadEmoji({
guildId, guildId,
name: emoji.name.split("~")[0], name: emoji.name.split("~")[0],
image: dataUrl image: dataUrl

View File

@ -19,7 +19,7 @@
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard"; import { ErrorCard } from "@components/ErrorCard";
import { Devs } from "@utils/constants"; import { Devs, isMac } from "@utils/constants";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
@ -96,9 +96,8 @@ export default definePlugin({
], ],
settingsAboutComponent: () => { settingsAboutComponent: () => {
const isMacOS = navigator.platform.includes("Mac"); const modKey = isMac ? "cmd" : "ctrl";
const modKey = isMacOS ? "cmd" : "ctrl"; const altKey = isMac ? "opt" : "alt";
const altKey = isMacOS ? "opt" : "alt";
return ( return (
<React.Fragment> <React.Fragment>
<Forms.FormTitle tag="h3">More Information</Forms.FormTitle> <Forms.FormTitle tag="h3">More Information</Forms.FormTitle>

View File

@ -24,35 +24,33 @@ import { getCurrentGuild } from "@utils/discord";
import { proxyLazy } from "@utils/lazy"; import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack"; import { findByPropsLazy, findStoreLazy } from "@webpack";
import { ChannelStore, EmojiStore, FluxDispatcher, lodash, Parser, PermissionStore, UserStore } from "@webpack/common"; import { ChannelStore, EmojiStore, FluxDispatcher, lodash, Parser, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
import type { Message } from "discord-types/general"; import type { Message } from "discord-types/general";
import { applyPalette, GIFEncoder, quantize } from "gifenc"; import { applyPalette, GIFEncoder, quantize } from "gifenc";
import type { ReactElement, ReactNode } from "react"; import type { ReactElement, ReactNode } from "react";
const DRAFT_TYPE = 0; const DRAFT_TYPE = 0;
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
const PreloadedUserSettingsProtoHandler = findLazy(m => m.ProtoClass?.typeName === "discord_protos.discord_users.v1.PreloadedUserSettings");
const ReaderFactory = findByPropsLazy("readerFactory");
const StickerStore = findStoreLazy("StickersStore") as { const StickerStore = findStoreLazy("StickersStore") as {
getPremiumPacks(): StickerPack[]; getPremiumPacks(): StickerPack[];
getAllGuildStickers(): Map<string, Sticker[]>; getAllGuildStickers(): Map<string, Sticker[]>;
getStickerById(id: string): Sticker | undefined; getStickerById(id: string): Sticker | undefined;
}; };
function searchProtoClass(localName: string, parentProtoClass: any) { const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
if (!parentProtoClass) return; const ProtoUtils = findByPropsLazy("BINARY_READ_OPTIONS");
const field = parentProtoClass.fields.find(field => field.localName === localName); function searchProtoClassField(localName: string, protoClass: any) {
const field = protoClass?.fields?.find((field: any) => field.localName === localName);
if (!field) return; if (!field) return;
const getter: any = Object.values(field).find(value => typeof value === "function"); const fieldGetter = Object.values(field).find(value => typeof value === "function") as any;
return getter?.(); return fieldGetter?.();
} }
const AppearanceSettingsProto = proxyLazy(() => searchProtoClass("appearance", PreloadedUserSettingsProtoHandler.ProtoClass)); const PreloadedUserSettingsActionCreators = proxyLazy(() => UserSettingsActionCreators.PreloadedUserSettingsActionCreators);
const ClientThemeSettingsProto = proxyLazy(() => searchProtoClass("clientThemeSettings", AppearanceSettingsProto)); const AppearanceSettingsActionCreators = proxyLazy(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass));
const ClientThemeSettingsActionsCreators = proxyLazy(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators));
const USE_EXTERNAL_EMOJIS = 1n << 18n; const USE_EXTERNAL_EMOJIS = 1n << 18n;
const USE_EXTERNAL_STICKERS = 1n << 37n; const USE_EXTERNAL_STICKERS = 1n << 37n;
@ -176,31 +174,37 @@ export default definePlugin({
predicate: () => settings.store.enableEmojiBypass, predicate: () => settings.store.enableEmojiBypass,
replacement: [ replacement: [
{ {
// Create a variable for the intention of listing the emoji
match: /(?<=,intention:(\i).+?;)/, match: /(?<=,intention:(\i).+?;)/,
replace: (_, intention) => `var fakeNitroIntention=${intention};` replace: (_, intention) => `let fakeNitroIntention=${intention};`
}, },
{ {
// Send the intention of listing the emoji to the nitro permission check functions
match: /\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i(?=\))/g, match: /\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i(?=\))/g,
replace: '$&,typeof fakeNitroIntention!=="undefined"?fakeNitroIntention:void 0' replace: '$&,typeof fakeNitroIntention!=="undefined"?fakeNitroIntention:void 0'
}, },
{ {
// Disallow the emoji if the intention doesn't allow it
match: /(&&!\i&&)!(\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/, 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)))` replace: (_, rest, canUseExternal) => `${rest}(!${canUseExternal}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)))`
}, },
{ {
// Make the emoji always available if the intention allows it
match: /if\(!\i\.available/, match: /if\(!\i\.available/,
replace: m => `${m}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))` replace: m => `${m}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))`
} }
] ]
}, },
// Allow emojis and animated emojis to be sent everywhere
{ {
find: "canUseAnimatedEmojis:function", find: "canUseAnimatedEmojis:function",
predicate: () => settings.store.enableEmojiBypass, predicate: () => settings.store.enableEmojiBypass,
replacement: { replacement: {
match: /((?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))/g, match: /((?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))(?=})/g,
replace: (_, rest, premiumCheck) => `${rest},fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)` replace: (_, rest, premiumCheck) => `${rest},fakeNitroIntention){${premiumCheck}||fakeNitroIntention!=null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
} }
}, },
// Allow stickers to be sent everywhere
{ {
find: "canUseStickersEverywhere:function", find: "canUseStickersEverywhere:function",
predicate: () => settings.store.enableStickerBypass, predicate: () => settings.store.enableStickerBypass,
@ -209,6 +213,7 @@ export default definePlugin({
replace: "$&return true;" replace: "$&return true;"
}, },
}, },
// Make stickers always available
{ {
find: "\"SENDABLE\"", find: "\"SENDABLE\"",
predicate: () => settings.store.enableStickerBypass, predicate: () => settings.store.enableStickerBypass,
@ -217,6 +222,7 @@ export default definePlugin({
replace: "true?" replace: "true?"
} }
}, },
// Allow streaming with high quality
{ {
find: "canUseHighVideoUploadQuality:function", find: "canUseHighVideoUploadQuality:function",
predicate: () => settings.store.enableStreamQualityBypass, predicate: () => settings.store.enableStreamQualityBypass,
@ -230,6 +236,7 @@ export default definePlugin({
}; };
}) })
}, },
// Remove boost requirements to stream with high quality
{ {
find: "STREAM_FPS_OPTION.format", find: "STREAM_FPS_OPTION.format",
predicate: () => settings.store.enableStreamQualityBypass, predicate: () => settings.store.enableStreamQualityBypass,
@ -238,6 +245,7 @@ export default definePlugin({
replace: "" replace: ""
} }
}, },
// Allow client themes to be changeable
{ {
find: "canUseClientThemes:function", find: "canUseClientThemes:function",
replacement: { replacement: {
@ -249,19 +257,22 @@ export default definePlugin({
find: '.displayName="UserSettingsProtoStore"', find: '.displayName="UserSettingsProtoStore"',
replacement: [ replacement: [
{ {
// Overwrite incoming connection settings proto with our local settings
match: /CONNECTION_OPEN:function\((\i)\){/, match: /CONNECTION_OPEN:function\((\i)\){/,
replace: (m, props) => `${m}$self.handleProtoChange(${props}.userSettingsProto,${props}.user);` replace: (m, props) => `${m}$self.handleProtoChange(${props}.userSettingsProto,${props}.user);`
}, },
{ {
match: /=(\i)\.local;/, // Overwrite non local proto changes with our local settings
replace: (m, props) => `${m}${props}.local||$self.handleProtoChange(${props}.settings.proto);` match: /let{settings:/,
replace: "arguments[0].local||$self.handleProtoChange(arguments[0].settings.proto);$&"
} }
] ]
}, },
// Call our function to handle changing the gradient theme when selecting a new one
{ {
find: "updateTheme:function", find: ",updateTheme(",
replacement: { replacement: {
match: /(function \i\(\i\){var (\i)=\i\.backgroundGradientPresetId.+?)(\i\.\i\.updateAsync.+?theme=(.+?);.+?\),\i\))/, match: /(function \i\(\i\){let{backgroundGradientPresetId:(\i).+?)(\i\.\i\.updateAsync.+?theme=(.+?),.+?},\i\))/,
replace: (_, rest, backgroundGradientPresetId, originalCall, theme) => `${rest}$self.handleGradientThemeSelect(${backgroundGradientPresetId},${theme},()=>${originalCall});` replace: (_, rest, backgroundGradientPresetId, originalCall, theme) => `${rest}$self.handleGradientThemeSelect(${backgroundGradientPresetId},${theme},()=>${originalCall});`
} }
}, },
@ -269,11 +280,13 @@ export default definePlugin({
find: '["strong","em","u","text","inlineCode","s","spoiler"]', find: '["strong","em","u","text","inlineCode","s","spoiler"]',
replacement: [ replacement: [
{ {
// Call our function to decide whether the emoji link should be kept or not
predicate: () => settings.store.transformEmojis, predicate: () => settings.store.transformEmojis,
match: /1!==(\i)\.length\|\|1!==\i\.length/, match: /1!==(\i)\.length\|\|1!==\i\.length/,
replace: (m, content) => `${m}||$self.shouldKeepEmojiLink(${content}[0])` replace: (m, content) => `${m}||$self.shouldKeepEmojiLink(${content}[0])`
}, },
{ {
// Patch the rendered message content to add fake nitro emojis or remove sticker links
predicate: () => settings.store.transformEmojis || settings.store.transformStickers, predicate: () => settings.store.transformEmojis || settings.store.transformStickers,
match: /(?=return{hasSpoilerEmbeds:\i,content:(\i)})/, match: /(?=return{hasSpoilerEmbeds:\i,content:(\i)})/,
replace: (_, content) => `${content}=$self.patchFakeNitroEmojisOrRemoveStickersLinks(${content},arguments[2]?.formatInline);` replace: (_, content) => `${content}=$self.patchFakeNitroEmojisOrRemoveStickersLinks(${content},arguments[2]?.formatInline);`
@ -281,36 +294,41 @@ export default definePlugin({
] ]
}, },
{ {
find: "renderEmbeds=function", find: "renderEmbeds(",
replacement: [ replacement: [
{ {
// Call our function to decide whether the embed should be ignored or not
predicate: () => settings.store.transformEmojis || settings.store.transformStickers, predicate: () => settings.store.transformEmojis || settings.store.transformStickers,
match: /(renderEmbeds=function\((\i)\){)(.+?embeds\.map\(\(function\((\i)\){)/, match: /(renderEmbeds\((\i)\){)(.+?embeds\.map\((\i)=>{)/,
replace: (_, rest1, message, rest2, embed) => `${rest1}const fakeNitroMessage=${message};${rest2}if($self.shouldIgnoreEmbed(${embed},fakeNitroMessage))return null;` replace: (_, rest1, message, rest2, embed) => `${rest1}const fakeNitroMessage=${message};${rest2}if($self.shouldIgnoreEmbed(${embed},fakeNitroMessage))return null;`
}, },
{ {
// Patch the stickers array to add fake nitro stickers
predicate: () => settings.store.transformStickers, predicate: () => settings.store.transformStickers,
match: /renderStickersAccessories=function\((\i)\){var (\i)=\(0,\i\.\i\)\(\i\),/, match: /(?<=renderStickersAccessories\((\i)\){let (\i)=\(0,\i\.\i\)\(\i\).+?;)/,
replace: (m, message, stickers) => `${m}${stickers}=$self.patchFakeNitroStickers(${stickers},${message}),` replace: (_, message, stickers) => `${stickers}=$self.patchFakeNitroStickers(${stickers},${message});`
}, },
{ {
// Filter attachments to remove fake nitro stickers or emojis
predicate: () => settings.store.transformStickers, predicate: () => settings.store.transformStickers,
match: /renderAttachments=function\(\i\){var \i=this,(\i)=\i.attachments.+?;/, match: /renderAttachments\(\i\){let{attachments:(\i).+?;/,
replace: (m, attachments) => `${m}${attachments}=$self.filterAttachments(${attachments});` replace: (m, attachments) => `${m}${attachments}=$self.filterAttachments(${attachments});`
} }
] ]
}, },
{ {
find: ".STICKER_IN_MESSAGE_HOVER,", find: ".Messages.STICKER_POPOUT_UNJOINED_PRIVATE_GUILD_DESCRIPTION.format",
predicate: () => settings.store.transformStickers, predicate: () => settings.store.transformStickers,
replacement: [ replacement: [
{ {
match: /var (\i)=\i\.renderableSticker,.{0,50}closePopout.+?channel:\i,closePopout:\i,/, // Export the renderable sticker to be used in the fake nitro sticker notice
replace: (m, renderableSticker) => `${m}renderableSticker:${renderableSticker},` match: /let{renderableSticker:(\i).{0,250}isGuildSticker.+?channel:\i,/,
replace: (m, renderableSticker) => `${m}fakeNitroRenderableSticker:${renderableSticker},`
}, },
{ {
match: /(emojiSection.{0,50}description:)(\i)(?<=(\i)\.sticker,.+?)(?=,)/, // Add the fake nitro sticker notice
replace: (_, rest, reactNode, props) => `${rest}$self.addFakeNotice(${FakeNoticeType.Sticker},${reactNode},!!${props}.renderableSticker?.fake)` match: /(let \i,{sticker:\i,channel:\i,closePopout:\i.+?}=(\i).+?;)(.+?description:)(\i)(?=,sticker:\i)/,
replace: (_, rest, props, rest2, reactNode) => `${rest}let{fakeNitroRenderableSticker}=${props};${rest2}$self.addFakeNotice(${FakeNoticeType.Sticker},${reactNode},!!fakeNitroRenderableSticker?.fake)`
} }
] ]
}, },
@ -318,6 +336,7 @@ export default definePlugin({
find: ".EMOJI_UPSELL_POPOUT_MORE_EMOJIS_OPENED,", find: ".EMOJI_UPSELL_POPOUT_MORE_EMOJIS_OPENED,",
predicate: () => settings.store.transformEmojis, predicate: () => settings.store.transformEmojis,
replacement: { replacement: {
// Export the emoji node to be used in the fake nitro emoji notice
match: /isDiscoverable:\i,shouldHideRoleSubscriptionCTA:\i,(?<={node:(\i),.+?)/, match: /isDiscoverable:\i,shouldHideRoleSubscriptionCTA:\i,(?<={node:(\i),.+?)/,
replace: (m, node) => `${m}fakeNitroNode:${node},` replace: (m, node) => `${m}fakeNitroNode:${node},`
} }
@ -326,10 +345,12 @@ export default definePlugin({
find: ".Messages.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION", find: ".Messages.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION",
predicate: () => settings.store.transformEmojis, predicate: () => settings.store.transformEmojis,
replacement: { replacement: {
// Add the fake nitro emoji notice
match: /(?<=isDiscoverable:\i,emojiComesFromCurrentGuild:\i,.+?}=(\i).+?;)(.+?return )(.{0,1000}\.Messages\.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION.+?)(?=},)/, match: /(?<=isDiscoverable:\i,emojiComesFromCurrentGuild:\i,.+?}=(\i).+?;)(.+?return )(.{0,1000}\.Messages\.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION.+?)(?=},)/,
replace: (_, props, rest, reactNode) => `var fakeNitroNode=${props}.fakeNitroNode;${rest}$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},fakeNitroNode?.fake)` replace: (_, props, rest, reactNode) => `let{fakeNitroNode}=${props};${rest}$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!fakeNitroNode?.fake)`
} }
}, },
// Allow using custom app icons
{ {
find: "canUsePremiumAppIcons:function", find: "canUsePremiumAppIcons:function",
replacement: { replacement: {
@ -337,6 +358,7 @@ export default definePlugin({
replace: "$&return true;" replace: "$&return true;"
} }
}, },
// Separate patch for allowing using custom app icons
{ {
find: "location:\"AppIconHome\"", find: "location:\"AppIconHome\"",
replacement: { replacement: {
@ -359,26 +381,30 @@ export default definePlugin({
}, },
handleProtoChange(proto: any, user: any) { handleProtoChange(proto: any, user: any) {
if (proto == null || typeof proto === "string" || !UserSettingsProtoStore || (!proto.appearance && !AppearanceSettingsProto)) return; if (proto == null || typeof proto === "string" || !UserSettingsProtoStore || !PreloadedUserSettingsActionCreators || !AppearanceSettingsActionCreators || !ClientThemeSettingsActionsCreators) return;
const premiumType: number = user?.premium_type ?? UserStore?.getCurrentUser()?.premiumType ?? 0; const premiumType: number = user?.premium_type ?? UserStore?.getCurrentUser()?.premiumType ?? 0;
if (premiumType !== 2) { if (premiumType !== 2) {
proto.appearance ??= AppearanceSettingsProto.create(); proto.appearance ??= AppearanceSettingsActionCreators.create();
if (UserSettingsProtoStore.settings.appearance?.theme != null) { if (UserSettingsProtoStore.settings.appearance?.theme != null) {
proto.appearance.theme = UserSettingsProtoStore.settings.appearance.theme; const appearanceSettingsDummy = AppearanceSettingsActionCreators.create({
theme: UserSettingsProtoStore.settings.appearance.theme
});
proto.appearance.theme = appearanceSettingsDummy.theme;
} }
if (UserSettingsProtoStore.settings.appearance?.clientThemeSettings?.backgroundGradientPresetId?.value != null && ClientThemeSettingsProto) { if (UserSettingsProtoStore.settings.appearance?.clientThemeSettings?.backgroundGradientPresetId?.value != null) {
const clientThemeSettingsDummyProto = ClientThemeSettingsProto.create({ const clientThemeSettingsDummy = ClientThemeSettingsActionsCreators.create({
backgroundGradientPresetId: { backgroundGradientPresetId: {
value: UserSettingsProtoStore.settings.appearance.clientThemeSettings.backgroundGradientPresetId.value value: UserSettingsProtoStore.settings.appearance.clientThemeSettings.backgroundGradientPresetId.value
} }
}); });
proto.appearance.clientThemeSettings ??= clientThemeSettingsDummyProto; proto.appearance.clientThemeSettings ??= clientThemeSettingsDummy;
proto.appearance.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummyProto.backgroundGradientPresetId; proto.appearance.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummy.backgroundGradientPresetId;
} }
} }
}, },
@ -387,26 +413,26 @@ export default definePlugin({
const premiumType = UserStore?.getCurrentUser()?.premiumType ?? 0; const premiumType = UserStore?.getCurrentUser()?.premiumType ?? 0;
if (premiumType === 2 || backgroundGradientPresetId == null) return original(); if (premiumType === 2 || backgroundGradientPresetId == null) return original();
if (!AppearanceSettingsProto || !ClientThemeSettingsProto || !ReaderFactory) return; if (!PreloadedUserSettingsActionCreators || !AppearanceSettingsActionCreators || !ClientThemeSettingsActionsCreators || !ProtoUtils) return;
const currentAppearanceProto = PreloadedUserSettingsProtoHandler.getCurrentValue().appearance; const currentAppearanceSettings = PreloadedUserSettingsActionCreators.getCurrentValue().appearance;
const newAppearanceProto = currentAppearanceProto != null const newAppearanceProto = currentAppearanceSettings != null
? AppearanceSettingsProto.fromBinary(AppearanceSettingsProto.toBinary(currentAppearanceProto), ReaderFactory) ? AppearanceSettingsActionCreators.fromBinary(AppearanceSettingsActionCreators.toBinary(currentAppearanceSettings), ProtoUtils.BINARY_READ_OPTIONS)
: AppearanceSettingsProto.create(); : AppearanceSettingsActionCreators.create();
newAppearanceProto.theme = theme; newAppearanceProto.theme = theme;
const clientThemeSettingsDummyProto = ClientThemeSettingsProto.create({ const clientThemeSettingsDummy = ClientThemeSettingsActionsCreators.create({
backgroundGradientPresetId: { backgroundGradientPresetId: {
value: backgroundGradientPresetId value: backgroundGradientPresetId
} }
}); });
newAppearanceProto.clientThemeSettings ??= clientThemeSettingsDummyProto; newAppearanceProto.clientThemeSettings ??= clientThemeSettingsDummy;
newAppearanceProto.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummyProto.backgroundGradientPresetId; newAppearanceProto.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummy.backgroundGradientPresetId;
const proto = PreloadedUserSettingsProtoHandler.ProtoClass.create(); const proto = PreloadedUserSettingsActionCreators.ProtoClass.create();
proto.appearance = newAppearanceProto; proto.appearance = newAppearanceProto;
FluxDispatcher.dispatch({ FluxDispatcher.dispatch({
@ -729,7 +755,7 @@ export default definePlugin({
gif.finish(); gif.finish();
const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" }); const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE); UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE);
}, },
start() { start() {

View File

@ -25,12 +25,6 @@
box-shadow: none; box-shadow: none;
} }
[class*="modalCarouselWrapper"] {
height: fit-content;
top: 50%;
transform: translateY(-50%);
}
[class|="wrapper"]:has(> #vc-imgzoom-magnify-modal) { [class|="wrapper"]:has(> #vc-imgzoom-magnify-modal) {
position: absolute; position: absolute;
left: 50%; left: 50%;

View File

@ -30,7 +30,7 @@ export default definePlugin({
// = isPremiumAtLeast(user.premiumType, TIER_2) // = isPremiumAtLeast(user.premiumType, TIER_2)
match: /=(?=\i\.\i\.isPremiumAtLeast\(null==(\i))/, match: /=(?=\i\.\i\.isPremiumAtLeast\(null==(\i))/,
// = user.banner && isPremiumAtLeast(user.premiumType, TIER_2) // = user.banner && isPremiumAtLeast(user.premiumType, TIER_2)
replace: "=$1?.banner&&" replace: "=(arguments[0]?.bannerSrc||$1?.banner)&&"
} }
}, },
{ {

View File

@ -25,15 +25,15 @@ export default definePlugin({
authors: [Devs.rushii], authors: [Devs.rushii],
patches: [ patches: [
{ {
find: "setSystemTrayApplications:function", find: ",setSystemTrayApplications",
replacement: [ replacement: [
{ {
match: /setBadge:function.+?},/, match: /setBadge\(\i\).+?},/,
replace: "setBadge:function(){}," replace: "setBadge(){},"
}, },
{ {
match: /setSystemTrayIcon:function.+?},/, match: /setSystemTrayIcon\(\i\).+?},/,
replace: "setSystemTrayIcon:function(){}," replace: "setSystemTrayIcon(){},"
} }
] ]
} }

View File

@ -135,9 +135,9 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
<Text variant="text-md/normal"> <Text variant="text-md/normal">
{ {
permission.type === PermissionType.Role permission.type === PermissionType.Role
? role?.name || "Unknown Role" ? role?.name ?? "Unknown Role"
: permission.type === PermissionType.User : permission.type === PermissionType.User
? (user && getUniqueUsername(user)) || "Unknown User" ? (user && getUniqueUsername(user)) ?? "Unknown User"
: ( : (
<Flex style={{ gap: "0.2em", justifyItems: "center" }}> <Flex style={{ gap: "0.2em", justifyItems: "center" }}>
@owner @owner

View File

@ -20,7 +20,8 @@ import { ApplicationCommandInputType, ApplicationCommandOptionType, Argument, Co
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { makeLazy } from "@utils/lazy"; import { makeLazy } from "@utils/lazy";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { UploadHandler, UserUtils } from "@webpack/common";
import { applyPalette, GIFEncoder, quantize } from "gifenc"; import { applyPalette, GIFEncoder, quantize } from "gifenc";
const DRAFT_TYPE = 0; const DRAFT_TYPE = 0;
@ -35,8 +36,6 @@ const getFrames = makeLazy(() => Promise.all(
)) ))
); );
const fetchUser = findByCodeLazy(".USER(");
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
const UploadStore = findByPropsLazy("getUploads"); const UploadStore = findByPropsLazy("getUploads");
function loadImage(source: File | string) { function loadImage(source: File | string) {
@ -70,7 +69,7 @@ async function resolveImage(options: Argument[], ctx: CommandContext, noServerPf
return opt.value; return opt.value;
case "user": case "user":
try { try {
const user = await fetchUser(opt.value); const user = await UserUtils.getUser(opt.value);
return user.getAvatarURL(noServerPfp ? void 0 : ctx.guild?.id, 2048).replace(/\?size=\d+$/, "?size=2048"); return user.getAvatarURL(noServerPfp ? void 0 : ctx.guild?.id, 2048).replace(/\?size=\d+$/, "?size=2048");
} catch (err) { } catch (err) {
console.error("[petpet] Failed to fetch user\n", err); console.error("[petpet] Failed to fetch user\n", err);
@ -175,7 +174,7 @@ export default definePlugin({
const file = new File([gif.bytesView()], "petpet.gif", { type: "image/gif" }); const file = new File([gif.bytesView()], "petpet.gif", { type: "image/gif" });
// Immediately after the command finishes, Discord clears all input, including pending attachments. // Immediately after the command finishes, Discord clears all input, including pending attachments.
// Thus, setTimeout is needed to make this execute after Discord cleared the input // Thus, setTimeout is needed to make this execute after Discord cleared the input
setTimeout(() => promptToUpload([file], cmdCtx.channel, DRAFT_TYPE), 10); setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DRAFT_TYPE), 10);
}, },
}, },
] ]

View File

@ -28,8 +28,8 @@ export default definePlugin({
{ {
find: ".nonMediaAttachment]", find: ".nonMediaAttachment]",
replacement: { replacement: {
match: /\.nonMediaAttachment\].{0,10}children:\[\S{3}/, match: /\.nonMediaAttachment\].{0,10}children:\[(\S)/,
replace: "$&&&$self.renderPiPButton()," replace: "$&,$1&&$self.renderPiPButton(),"
}, },
}, },
], ],

View File

@ -17,7 +17,7 @@
*/ */
import { definePluginSettings, Settings } from "@api/Settings"; import { definePluginSettings, Settings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs, isMac } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { ChannelStore, FluxDispatcher as Dispatcher, MessageStore, PermissionsBits, PermissionStore, SelectedChannelStore, UserStore } from "@webpack/common"; import { ChannelStore, FluxDispatcher as Dispatcher, MessageStore, PermissionsBits, PermissionStore, SelectedChannelStore, UserStore } from "@webpack/common";
@ -25,7 +25,6 @@ import { Message } from "discord-types/general";
const Kangaroo = findByPropsLazy("jumpToMessage"); const Kangaroo = findByPropsLazy("jumpToMessage");
const isMac = navigator.platform.includes("Mac"); // bruh
let replyIdx = -1; let replyIdx = -1;
let editIdx = -1; let editIdx = -1;

View File

@ -28,8 +28,8 @@ export default definePlugin({
{ {
find: ".Messages.MESSAGE_UTILITIES_A11Y_LABEL", find: ".Messages.MESSAGE_UTILITIES_A11Y_LABEL",
replacement: { replacement: {
match: /isExpanded:\i&&.*?,/, match: /isExpanded:\i&&(.+?),/,
replace: "isExpanded:true," replace: "isExpanded:$1,"
} }
} }
] ]

View File

@ -70,18 +70,27 @@ export default definePlugin({
// RenderLevel defines if a channel is hidden, collapsed in category, visible, etc // RenderLevel defines if a channel is hidden, collapsed in category, visible, etc
find: ".CannotShow=", find: ".CannotShow=",
replacement: [ replacement: [
// Remove the special logic for channels we don't have access to
{ {
match: /if\(!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL.+?{if\(this\.id===\i\).+?threadIds:\i}}/, match: /if\(!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL.+?{if\(this\.id===\i\).+?threadIds:\i}}/,
replace: "" replace: ""
}, },
// Do not check for unreads when selecting the render level if the channel is hidden
{
match: /(?=!1===\i.\i\.hasRelevantUnread\(this\.record\))/,
replace: "$self.isHiddenChannel(this.record)||"
},
// Make channels we dont have access to be the same level as normal ones
{ {
match: /(?<=renderLevel:(\i\(this,\i\)\?\i\.Show:\i\.WouldShowIfUncollapsed).+?renderLevel:).+?(?=,)/, match: /(?<=renderLevel:(\i\(this,\i\)\?\i\.Show:\i\.WouldShowIfUncollapsed).+?renderLevel:).+?(?=,)/,
replace: (_, renderLevelExpression) => renderLevelExpression replace: (_, renderLevelExpression) => renderLevelExpression
}, },
// Make channels we dont have access to be the same level as normal ones
{ {
match: /(?<=activeJoinedRelevantThreads.+?renderLevel:.+?,threadIds:\i\(this.record.+?renderLevel:)(\i)\..+?(?=,)/, match: /(?<=activeJoinedRelevantThreads.+?renderLevel:.+?,threadIds:\i\(this.record.+?renderLevel:)(\i)\..+?(?=,)/,
replace: (_, RenderLevels) => `${RenderLevels}.Show` replace: (_, RenderLevels) => `${RenderLevels}.Show`
}, },
// Remove permission checking for getRenderLevel function
{ {
match: /(?<=getRenderLevel\(\i\){.+?return)!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL,this\.record\)\|\|/, match: /(?<=getRenderLevel\(\i\){.+?return)!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL,this\.record\)\|\|/,
replace: " " replace: " "
@ -186,13 +195,29 @@ export default definePlugin({
] ]
}, },
{ {
// Hide New unreads box for hidden channels // Hide the new version of unreads box for hidden channels
find: '.displayName="ChannelListUnreadsStore"', find: '.displayName="ChannelListUnreadsStore"',
replacement: { replacement: {
match: /(?<=if\(null==(\i))(?=.{0,160}?hasRelevantUnread\(\i\))/g, // Global because Discord has multiple methods like that in the same module match: /(?<=if\(null==(\i))(?=.{0,160}?hasRelevantUnread\(\i\))/g, // Global because Discord has multiple methods like that in the same module
replace: (_, channel) => `||$self.isHiddenChannel(${channel})` replace: (_, channel) => `||$self.isHiddenChannel(${channel})`
} }
}, },
{
// Make the old version of unreads box not visible for hidden channels
find: "renderBottomUnread(){",
replacement: {
match: /(?=&&\i\.\i\.hasRelevantUnread\((\i\.record)\))/,
replace: "&&!$self.isHiddenChannel($1)"
}
},
{
// Make the state of the old version of unreads box not include hidden channels
find: ".useFlattenedChannelIdListWithThreads)",
replacement: {
match: /(?=&&\i\.\i\.hasRelevantUnread\((\i)\))/,
replace: "&&!$self.isHiddenChannel($1)"
}
},
// Only render the channel header and buttons that work when transitioning to a hidden channel // Only render the channel header and buttons that work when transitioning to a hidden channel
{ {
find: "Missing channel in Channel.renderHeaderToolbar", find: "Missing channel in Channel.renderHeaderToolbar",
@ -275,7 +300,7 @@ export default definePlugin({
match: /MANAGE_ROLES.{0,90}?return(?=\(.+?(\(0,\i\.jsxs\)\("div",{className:\i\.members.+?guildId:(\i)\.guild_id.+?roleColor.+?\]}\)))/, match: /MANAGE_ROLES.{0,90}?return(?=\(.+?(\(0,\i\.jsxs\)\("div",{className:\i\.members.+?guildId:(\i)\.guild_id.+?roleColor.+?\]}\)))/,
replace: (m, component, channel) => { replace: (m, component, channel) => {
// Export the channel for the users allowed component patch // Export the channel for the users allowed component patch
component = component.replace(canonicalizeMatch(/(?<=users:\i)/), `,channel:${channel}`); component = component.replace(canonicalizeMatch(/(?<=users:\i)/), `,shcChannel:${channel}`);
// Always render the component for multiple allowed users // Always render the component for multiple allowed users
component = component.replace(canonicalizeMatch(/1!==\i\.length/), "true"); component = component.replace(canonicalizeMatch(/1!==\i\.length/), "true");
@ -290,22 +315,22 @@ export default definePlugin({
{ {
// Create a variable for the channel prop // Create a variable for the channel prop
match: /maxUsers:\i,users:\i.+?=(\i).+?;/, match: /maxUsers:\i,users:\i.+?=(\i).+?;/,
replace: (m, props) => `${m}var channel=${props}.channel;` replace: (m, props) => `${m}let{shcChannel}=${props};`
}, },
{ {
// Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen // Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen
match: /\i>0(?=&&.{0,60}renderPopout)/, match: /\i>0(?=&&.{0,60}renderPopout)/,
replace: m => `($self.isHiddenChannel(typeof channel!=="undefined"?channel:void 0,true)?true:${m})` replace: m => `($self.isHiddenChannel(shcChannel,true)?true:${m})`
}, },
{ {
// Prevent Discord from overwriting the last children with the plus button if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen // Prevent Discord from overwriting the last children with the plus button if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
match: /(?<=\.value\(\),(\i)=.+?length-)1(?=\]=.{0,60}renderPopout)/, match: /(?<=\.value\(\),(\i)=.+?length-)1(?=\]=.{0,60}renderPopout)/,
replace: (_, amount) => `($self.isHiddenChannel(typeof channel!=="undefined"?channel:void 0,true)&&${amount}<=0?0:1)` replace: (_, amount) => `($self.isHiddenChannel(shcChannel,true)&&${amount}<=0?0:1)`
}, },
{ {
// Show only the plus text without overflowed children amount if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen // Show only the plus text without overflowed children amount if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
match: /(?<="\+",)(\i)\+1/, match: /(?<="\+",)(\i)\+1/,
replace: (m, amount) => `$self.isHiddenChannel(typeof channel!=="undefined"?channel:void 0,true)&&${amount}<=0?"":${m}` replace: (m, amount) => `$self.isHiddenChannel(shcChannel,true)&&${amount}<=0?"":${m}`
} }
] ]
}, },
@ -389,6 +414,22 @@ export default definePlugin({
} }
] ]
}, },
{
// Make the chat input bar channel list contain hidden channels
find: ",queryStaticRouteChannels(",
replacement: [
{
// Make the getChannels call to GuildChannelStore return hidden channels
match: /(?<=queryChannels\(\i\){.+?getChannels\(\i)(?=\))/,
replace: ",true"
},
{
// Avoid filtering out hidden channels from the channel list
match: /(?<=queryChannels\(\i\){.+?isGuildChannelType\)\((\i)\.type\))(?=&&!\i\.\i\.can\()/,
replace: "&&!$self.isHiddenChannel($1)"
}
]
},
{ {
find: "\"^/guild-stages/(\\\\d+)(?:/)?(\\\\d+)?\"", find: "\"^/guild-stages/(\\\\d+)(?:/)?(\\\\d+)?\"",
replacement: { replacement: {
@ -416,7 +457,7 @@ export default definePlugin({
{ {
// Filter hidden channels from GuildChannelStore.getChannels unless told otherwise // Filter hidden channels from GuildChannelStore.getChannels unless told otherwise
match: /(?<=getChannels\(\i)(\){.+?)return (.+?)}/, match: /(?<=getChannels\(\i)(\){.+?)return (.+?)}/,
replace: (_, rest, channels) => `,shouldIncludeHidden=false${rest}return $self.resolveGuildChannels(${channels},shouldIncludeHidden);}` replace: (_, rest, channels) => `,shouldIncludeHidden${rest}return $self.resolveGuildChannels(${channels},shouldIncludeHidden??false);}`
} }
] ]
}, },

View File

@ -86,6 +86,7 @@ export default definePlugin({
}, },
{ {
find: "ChannelTextAreaButtons", find: "ChannelTextAreaButtons",
predicate: () => settings.store.showIcon,
replacement: { replacement: {
match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/,
replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()",

View File

@ -2,7 +2,7 @@
padding: 0.375rem 0.5rem; padding: 0.375rem 0.5rem;
border-bottom: 1px solid var(--background-modifier-accent); border-bottom: 1px solid var(--background-modifier-accent);
--vc-spotify-green: #1db954; /* so cusotm themes can easily change it */ --vc-spotify-green: #1db954; /* so custom themes can easily change it */
} }
.theme-light #vc-spotify-player { .theme-light #vc-spotify-player {
@ -167,7 +167,7 @@
} }
#vc-spotify-progress-bar > [class^="slider"] [class^="grabber"] { #vc-spotify-progress-bar > [class^="slider"] [class^="grabber"] {
/* these importants are neccessary, it applies a width and height through inline styles */ /* these importants are necessary, it applies a width and height through inline styles */
height: 10px !important; height: 10px !important;
width: 10px !important; width: 10px !important;
background-color: var(--interactive-normal); background-color: var(--interactive-normal);

View File

@ -1,12 +1,16 @@
.vc-toolbox-btn, .vc-toolbox-btn,
.vc-toolbox-btn svg { .vc-toolbox-btn>svg {
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
} }
.vc-toolbox-btn svg { .vc-toolbox-btn>svg {
color: var(--interactive-normal); color: var(--interactive-normal);
} }
:is(.vc-toolbox-btn:hover, .vc-toolbox-btn[class*="selected"]) svg { .vc-toolbox-btn[class*="selected"]>svg {
color: var(--interactive-active); color: var(--interactive-active);
} }
.vc-toolbox-btn:hover>svg {
color: var(--interactive-hover);
}

View File

@ -89,10 +89,10 @@ function VencordPopout(onClose: () => void) {
); );
} }
function VencordPopoutIcon() { function VencordPopoutIcon(isShown: boolean) {
return ( return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width={24} height={24}> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 27 27" width={24} height={24}>
<path fill="currentColor" d="M53 10h7v1h-1v1h-1v1h-1v1h-1v1h-1v1h5v1h-7v-1h1v-1h1v-1h1v-1h1v-1h1v-1h-5m-43 1v32h2v2h2v2h2v2h2v2h2v2h2v2h2v2h2v2h8v-2h2V46h-2v2h-2v2h-4v-2h-2v-2h-2v-2h-2v-2h-2v-2h-2V12m24 0v27h-2v3h4v-6h2v-2h4V12m13 2h5v1h-1v1h-1v1h-1v1h3v1h-5v-1h1v-1h1v-1h1v-1h-3m8 5h1v5h1v-1h1v1h-1v1h1v-1h1v1h-1v3h-1v1h-2v1h-1v1h1v-1h2v-1h1v2h-1v1h-2v1h-1v-1h-1v1h-6v-1h-1v-1h-1v-2h1v1h2v1h3v1h1v-1h-1v-1h-3v-1h-4v-4h1v-2h1v-1h1v-1h1v2h1v1h1v-1h1v1h-1v1h2v-2h1v-2h1v-1h1m-13 4h2v1h-1v4h1v2h1v1h1v1h1v1h4v1h-6v-1h-6v-1h-1v-5h1v-1h1v-2h2m17 3h1v3h-1v1h-1v1h-1v2h-2v-2h2v-1h1v-1h1m1 0h1v3h-1v1h-2v-1h1v-1h1m-30 2v8h-8v32h8v8h32v-8h8v-8H70v8H54V44h16v8h16v-8h-8v-8h-1v1h-7v-1h-2v1h-8v-1" /> <path fill="currentColor" d={isShown ? "M9 0h1v1h1v2h1v2h3V3h1V1h1V0h1v2h1v2h1v7h-1v-1h-3V9h1V6h-1v4h-3v1h1v-1h2v1h3v1h-1v1h-3v2h1v1h1v1h1v3h-1v4h-2v-1h-1v-4h-1v4h-1v1h-2v-4H9v-3h1v-1h1v-1h1v-2H9v-1H8v-1h3V6h-1v3h1v1H8v1H7V4h1V2h1M5 19h2v1h1v1h1v3H4v-1h2v-1H4v-2h1m15-1h2v1h1v2h-2v1h2v1h-5v-3h1v-1h1m4 3h4v1h-4" : "M0 0h7v1H6v1H5v1H4v1H3v1H2v1h5v1H0V6h1V5h1V4h1V3h1V2h1V1H0m13 2h5v1h-1v1h-1v1h-1v1h3v1h-5V7h1V6h1V5h1V4h-3m8 5h1v5h1v-1h1v1h-1v1h1v-1h1v1h-1v3h-1v1h-2v1h-1v1h1v-1h2v-1h1v2h-1v1h-2v1h-1v-1h-1v1h-6v-1h-1v-1h-1v-2h1v1h2v1h3v1h1v-1h-1v-1h-3v-1h-4v-4h1v-2h1v-1h1v-1h1v2h1v1h1v-1h1v1h-1v1h2v-2h1v-2h1v-1h1M8 14h2v1H9v4h1v2h1v1h1v1h1v1h4v1h-6v-1H5v-1H4v-5h1v-1h1v-2h2m17 3h1v3h-1v1h-1v1h-1v2h-2v-2h2v-1h1v-1h1m1 0h1v3h-1v1h-2v-1h1v-1h1"} />
</svg> </svg>
); );
} }
@ -114,7 +114,7 @@ function VencordPopoutButton() {
className="vc-toolbox-btn" className="vc-toolbox-btn"
onClick={() => setShow(v => !v)} onClick={() => setShow(v => !v)}
tooltip={isShown ? null : "Vencord Toolbox"} tooltip={isShown ? null : "Vencord Toolbox"}
icon={VencordPopoutIcon} icon={() => VencordPopoutIcon(isShown)}
selected={isShown} selected={isShown}
/> />
)} )}

View File

@ -17,7 +17,7 @@
*/ */
import { LazyComponent, useTimer } from "@utils/react"; import { LazyComponent, useTimer } from "@utils/react";
import { findByCode } from "@webpack"; import { find } from "@webpack";
import { cl } from "./utils"; import { cl } from "./utils";
@ -25,7 +25,7 @@ interface VoiceMessageProps {
src: string; src: string;
waveform: string; waveform: string;
} }
const VoiceMessage = LazyComponent<VoiceMessageProps>(() => findByCode('["onVolumeChange","volume","onMute"]')); const VoiceMessage = LazyComponent<VoiceMessageProps>(() => find(m => m.type?.toString().includes("waveform:")));
export type VoicePreviewOptions = { export type VoicePreviewOptions = {
src?: string; src?: string;

View File

@ -25,7 +25,7 @@ import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModa
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { chooseFile } from "@utils/web"; import { chooseFile } from "@utils/web";
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack"; import { findByPropsLazy, findStoreLazy } from "@webpack";
import { Button, FluxDispatcher, Forms, lodash, Menu, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common"; import { Button, FluxDispatcher, Forms, lodash, Menu, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
import { ComponentType } from "react"; import { ComponentType } from "react";
@ -35,7 +35,7 @@ import { cl } from "./utils";
import { VoicePreview } from "./VoicePreview"; import { VoicePreview } from "./VoicePreview";
import { VoiceRecorderWeb } from "./WebRecorder"; import { VoiceRecorderWeb } from "./WebRecorder";
const CloudUpload = findLazy(m => m.prototype?.uploadFileToCloud); const CloudUtils = findByPropsLazy("CloudUpload");
const MessageCreator = findByPropsLazy("getSendMessageOptionsForReply", "sendMessage"); const MessageCreator = findByPropsLazy("getSendMessageOptionsForReply", "sendMessage");
const PendingReplyStore = findStoreLazy("PendingReplyStore"); const PendingReplyStore = findStoreLazy("PendingReplyStore");
const OptionClasses = findByPropsLazy("optionName", "optionIcon", "optionLabel"); const OptionClasses = findByPropsLazy("optionName", "optionIcon", "optionLabel");
@ -76,7 +76,7 @@ function sendAudio(blob: Blob, meta: AudioMetadata) {
const reply = PendingReplyStore.getPendingReply(channelId); const reply = PendingReplyStore.getPendingReply(channelId);
if (reply) FluxDispatcher.dispatch({ type: "DELETE_PENDING_REPLY", channelId }); if (reply) FluxDispatcher.dispatch({ type: "DELETE_PENDING_REPLY", channelId });
const upload = new CloudUpload({ const upload = new CloudUtils.CloudUpload({
file: new File([blob], "voice-message.ogg", { type: "audio/ogg; codecs=opus" }), file: new File([blob], "voice-message.ogg", { type: "audio/ogg; codecs=opus" }),
isClip: false, isClip: false,
isThumbnail: false, isThumbnail: false,

View File

@ -147,7 +147,7 @@ export default definePlugin({
} }
}, },
{ {
find: 'navId:"textarea-context"', find: ".SLASH_COMMAND_SUGGESTIONS_TOGGLED,{",
predicate: () => settings.store.addBack, predicate: () => settings.store.addBack,
replacement: [ replacement: [
{ {

View File

@ -389,3 +389,10 @@ export const DevsById = /* #__PURE__*/ (() =>
.map(([_, v]) => [v.id, v] as const) .map(([_, v]) => [v.id, v] as const)
)) ))
)() as Record<string, Dev>; )() as Record<string, Dev>;
const { platform } = navigator;
export const isWindows = platform.startsWith("Win");
export const isMac = platform.startsWith("Mac");
export const isLinux = platform.startsWith("Linux");

69
src/utils/telemetry.tsx Normal file
View File

@ -0,0 +1,69 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Settings } from "@api/Settings";
import { Alerts } from "@webpack/common";
import { isPluginEnabled } from "../plugins";
import { Plugins } from "../Vencord";
import { isLinux, isMac, isWindows } from "./constants";
export function sendTelemetry() {
// TODO: READ THIS CHECK BEFORE RELEASING!!
// if (IS_DEV) return; // don't send on devbuilds, usually contains incorrect data
// if we have not yet told the user about the telemetry's existence, or they haven't agreed at all, DON'T send a
// probe now, but tell them and then let them decide if they want to opt in or not.
if (Settings.telemetry === undefined) {
Alerts.show({
title: "Telemetry Notice",
body: <>
<p>
Vencord has a telemetry feature that sends anonymous data to us, which we use to improve the mod. We
gather your operating system, the version of Vencord you're using and a list of enabled plugins, and
we can use this data to help improve it for yourself and everyone else.
</p>
<p>
If you don't want this, that's okay! We haven't sent anything yet. Please decide if you want to allow
us to gather a little bit of data. You can change this setting at any time in the future. If you
grant consent, we will start sending the data above the next time you reload or restart Discord.
</p>
</>,
confirmText: "Yes, that's fine",
cancelText: "No, I don't want that",
onConfirm() {
Settings.telemetry = true;
},
onCancel() {
Settings.telemetry = false;
}
});
return;
}
// if it's disabled in settings, obviously don't do anything
if (!Settings.telemetry) return;
const activePluginsList = Object.keys(Plugins.plugins)
.filter(p => isPluginEnabled(p));
let operatingSystem = "Unknown";
if (isWindows) operatingSystem = "Windows";
else if (isMac) operatingSystem = "macOS";
else if (isLinux) operatingSystem = "Linux";
const data = {
version: VERSION,
plugins: activePluginsList,
operatingSystem
};
navigator.sendBeacon("https://api.vencord.dev/v1/telemetry", JSON.stringify(data));
}

View File

@ -269,7 +269,7 @@ export interface DefinedSettings<
store: SettingsStore<Def> & PrivateSettings; store: SettingsStore<Def> & PrivateSettings;
/** /**
* React hook for getting the settings for this plugin * React hook for getting the settings for this plugin
* @param filter optional filter to avoid rerenders for irrelavent settings * @param filter optional filter to avoid rerenders for irrelevent settings
*/ */
use<F extends Extract<keyof Def | keyof PrivateSettings, string>>(filter?: F[]): Pick<SettingsStore<Def> & PrivateSettings, F>; use<F extends Extract<keyof Def | keyof PrivateSettings, string>>(filter?: F[]): Pick<SettingsStore<Def> & PrivateSettings, F>;
/** Definitions of each setting */ /** Definitions of each setting */

View File

@ -20,7 +20,7 @@ import { proxyLazy } from "@utils/lazy";
import type * as Stores from "discord-types/stores"; import type * as Stores from "discord-types/stores";
// eslint-disable-next-line path-alias/no-relative // eslint-disable-next-line path-alias/no-relative
import { filters, findByCode, findByProps, findByPropsLazy, mapMangledModuleLazy } from "../webpack"; import { filters, findByProps, findByPropsLazy, mapMangledModuleLazy } from "../webpack";
import { waitForStore } from "./internal"; import { waitForStore } from "./internal";
import * as t from "./types/stores"; import * as t from "./types/stores";
@ -84,14 +84,7 @@ export const useStateFromStores: <T>(
idk?: any, idk?: any,
isEqual?: (old: T, newer: T) => boolean isEqual?: (old: T, newer: T) => boolean
) => T ) => T
// FIXME: hack to support old stable and new canary = proxyLazy(() => findByProps("useStateFromStores").useStateFromStores);
= proxyLazy(() => {
try {
return findByProps("useStateFromStores").useStateFromStores;
} catch {
return findByCode('("useStateFromStores")');
}
});
waitForStore("DraftStore", s => DraftStore = s); waitForStore("DraftStore", s => DraftStore = s);
waitForStore("UserStore", s => UserStore = s); waitForStore("UserStore", s => UserStore = s);

View File

@ -17,7 +17,7 @@
*/ */
import { proxyLazy } from "@utils/lazy"; import { proxyLazy } from "@utils/lazy";
import type { User } from "discord-types/general"; import type { Channel, User } from "discord-types/general";
// eslint-disable-next-line path-alias/no-relative // eslint-disable-next-line path-alias/no-relative
import { _resolveReady, filters, find, findByPropsLazy, findLazy, mapMangledModuleLazy, waitFor } from "../webpack"; import { _resolveReady, filters, find, findByPropsLazy, findLazy, mapMangledModuleLazy, waitFor } from "../webpack";
@ -94,6 +94,9 @@ export function showToast(message: string, type = ToastType.MESSAGE) {
} }
export const UserUtils = findByPropsLazy("getUser", "fetchCurrentUser") as { getUser: (id: string) => Promise<User>; }; export const UserUtils = findByPropsLazy("getUser", "fetchCurrentUser") as { getUser: (id: string) => Promise<User>; };
export const UploadHandler = findByPropsLazy("showUploadFileSizeExceededError", "promptToUpload") as {
promptToUpload: (files: File[], channel: Channel, draftType: Number) => void;
};
export const ApplicationAssetUtils = findByPropsLazy("fetchAssetIds", "getAssetImage") as { export const ApplicationAssetUtils = findByPropsLazy("fetchAssetIds", "getAssetImage") as {
fetchAssetIds: (applicationId: string, e: string[]) => Promise<string[]>; fetchAssetIds: (applicationId: string, e: string[]) => Promise<string[]>;
@ -133,11 +136,4 @@ waitFor("parseTopic", m => Parser = m);
export let SettingsRouter: any; export let SettingsRouter: any;
waitFor(["open", "saveAccountChanges"], m => SettingsRouter = m); waitFor(["open", "saveAccountChanges"], m => SettingsRouter = m);
// FIXME: hack to support old stable and new canary export const PermissionsBits: t.PermissionsBits = proxyLazy(() => find(m => typeof m.Permissions?.ADMINISTRATOR === "bigint").Permissions);
export const PermissionsBits: t.PermissionsBits = proxyLazy(() => {
try {
return find(m => m.Permissions?.ADMINISTRATOR).Permissions;
} catch {
return find(m => typeof m.ADMINISTRATOR === "bigint");
}
});

View File

@ -29,7 +29,7 @@ let webpackChunk: any[];
const logger = new Logger("WebpackInterceptor", "#8caaee"); const logger = new Logger("WebpackInterceptor", "#8caaee");
if (window[WEBPACK_CHUNK]) { if (window[WEBPACK_CHUNK]) {
logger.info(`Patching ${WEBPACK_CHUNK}.push (was already existant, likely from cache!)`); logger.info(`Patching ${WEBPACK_CHUNK}.push (was already existent, likely from cache!)`);
_initWebpack(window[WEBPACK_CHUNK]); _initWebpack(window[WEBPACK_CHUNK]);
patchPush(window[WEBPACK_CHUNK]); patchPush(window[WEBPACK_CHUNK]);
} else { } else {
@ -53,174 +53,35 @@ if (window[WEBPACK_CHUNK]) {
}, },
configurable: true configurable: true
}); });
// wreq.m is the webpack module factory.
// normally, this is populated via webpackGlobal.push, which we patch below.
// However, Discord has their .m prepopulated.
// Thus, we use this hack to immediately access their wreq.m and patch all already existing factories
Object.defineProperty(Function.prototype, "m", {
set(v: any) {
// When using react devtools or other extensions, we may also catch their webpack here.
// This ensures we actually got the right one
if (new Error().stack?.includes("discord.com")) {
logger.info("Found webpack module factory");
patchFactories(v);
delete (Function.prototype as any).m;
}
Object.defineProperty(this, "m", {
value: v,
configurable: true,
});
},
configurable: true
});
} }
function patchPush(webpackGlobal: any) { function patchPush(webpackGlobal: any) {
function handlePush(chunk: any) { function handlePush(chunk: any) {
try { try {
const modules = chunk[1]; patchFactories(chunk[1]);
const { subscriptions, listeners } = Vencord.Webpack;
const { patches } = Vencord.Plugins;
for (const id in modules) {
let mod = modules[id];
// Discords Webpack chunks for some ungodly reason contain random
// newlines. Cyn recommended this workaround and it seems to work fine,
// however this could potentially break code, so if anything goes weird,
// this is probably why.
// Additionally, `[actual newline]` is one less char than "\n", so if Discord
// ever targets newer browsers, the minifier could potentially use this trick and
// cause issues.
let code: string = mod.toString().replaceAll("\n", "");
// a very small minority of modules use function() instead of arrow functions,
// but, unnamed toplevel functions aren't valid. However 0, function() makes it a statement
if (code.startsWith("function(")) {
code = "0," + code;
}
const originalMod = mod;
const patchedBy = new Set();
const factory = modules[id] = function (module, exports, require) {
try {
mod(module, exports, require);
} catch (err) {
// Just rethrow discord errors
if (mod === originalMod) throw err;
logger.error("Error in patched chunk", err);
return void originalMod(module, exports, require);
}
exports = module.exports;
if (!exports) return;
// There are (at the time of writing) 11 modules exporting the window
// Make these non enumerable to improve webpack search performance
if (exports === window) {
Object.defineProperty(require.c, id, {
value: require.c[id],
enumerable: false,
configurable: true,
writable: true
});
return;
}
const numberId = Number(id);
for (const callback of listeners) {
try {
callback(exports, numberId);
} catch (err) {
logger.error("Error in webpack listener", err);
}
}
for (const [filter, callback] of subscriptions) {
try {
if (filter(exports)) {
subscriptions.delete(filter);
callback(exports, numberId);
} else if (typeof exports === "object") {
if (exports.default && filter(exports.default)) {
subscriptions.delete(filter);
callback(exports.default, numberId);
}
for (const nested in exports) if (nested.length <= 3) {
if (exports[nested] && filter(exports[nested])) {
subscriptions.delete(filter);
callback(exports[nested], numberId);
}
}
}
} catch (err) {
logger.error("Error while firing callback for webpack chunk", err);
}
}
} as any as { toString: () => string, original: any, (...args: any[]): void; };
// for some reason throws some error on which calling .toString() leads to infinite recursion
// when you force load all chunks???
try {
factory.toString = () => mod.toString();
factory.original = originalMod;
} catch { }
for (let i = 0; i < patches.length; i++) {
const patch = patches[i];
const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace));
if (patch.predicate && !patch.predicate()) continue;
if (code.includes(patch.find)) {
patchedBy.add(patch.plugin);
// we change all patch.replacement to array in plugins/index
for (const replacement of patch.replacement as PatchReplacement[]) {
if (replacement.predicate && !replacement.predicate()) continue;
const lastMod = mod;
const lastCode = code;
canonicalizeReplacement(replacement, patch.plugin);
try {
const newCode = executePatch(replacement.match, replacement.replace as string);
if (newCode === code && !patch.noWarn) {
(window.explosivePlugins ??= new Set<string>()).add(patch.plugin);
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`);
if (IS_DEV) {
logger.debug("Function Source:\n", code);
}
} else {
code = newCode;
mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`);
}
} catch (err) {
logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err);
if (IS_DEV) {
const changeSize = code.length - lastCode.length;
const match = lastCode.match(replacement.match)!;
// Use 200 surrounding characters of context
const start = Math.max(0, match.index! - 200);
const end = Math.min(lastCode.length, match.index! + match[0].length + 200);
// (changeSize may be negative)
const endPatched = end + changeSize;
const context = lastCode.slice(start, end);
const patchedContext = code.slice(start, endPatched);
// inline require to avoid including it in !IS_DEV builds
const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext);
let fmt = "%c %s ";
const elements = [] as string[];
for (const d of diff) {
const color = d.removed
? "red"
: d.added
? "lime"
: "grey";
fmt += "%c%s";
elements.push("color:" + color, d.value);
}
logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context);
logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext);
const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff");
logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements);
}
code = lastCode;
mod = lastMod;
patchedBy.delete(patch.plugin);
}
}
if (!patch.all) patches.splice(i--, 1);
}
}
}
} catch (err) { } catch (err) {
logger.error("Error in handlePush", err); logger.error("Error in handlePush", err);
} }
@ -229,13 +90,183 @@ function patchPush(webpackGlobal: any) {
} }
handlePush.$$vencordOriginal = webpackGlobal.push; handlePush.$$vencordOriginal = webpackGlobal.push;
// Webpack overwrites .push with its own push like so: `d.push = n.bind(null, d.push.bind(d));`
// it wraps the old push (`d.push.bind(d)`). this old push is in this case our handlePush.
// If we then repatched the new push, we would end up with recursive patching, which leads to our patches
// being applied multiple times.
// Thus, override bind to use the original push
handlePush.bind = (...args: unknown[]) => handlePush.$$vencordOriginal.bind(...args);
Object.defineProperty(webpackGlobal, "push", { Object.defineProperty(webpackGlobal, "push", {
get: () => handlePush, get: () => handlePush,
set(v) { set(v) {
delete webpackGlobal.push; handlePush.$$vencordOriginal = v;
webpackGlobal.push = v;
patchPush(webpackGlobal);
}, },
configurable: true configurable: true
}); });
} }
function patchFactories(factories: Record<string | number, (module: { exports: any; }, exports: any, require: any) => void>) {
const { subscriptions, listeners } = Vencord.Webpack;
const { patches } = Vencord.Plugins;
for (const id in factories) {
let mod = factories[id];
// Discords Webpack chunks for some ungodly reason contain random
// newlines. Cyn recommended this workaround and it seems to work fine,
// however this could potentially break code, so if anything goes weird,
// this is probably why.
// Additionally, `[actual newline]` is one less char than "\n", so if Discord
// ever targets newer browsers, the minifier could potentially use this trick and
// cause issues.
let code: string = mod.toString().replaceAll("\n", "");
// a very small minority of modules use function() instead of arrow functions,
// but, unnamed toplevel functions aren't valid. However 0, function() makes it a statement
if (code.startsWith("function(")) {
code = "0," + code;
}
const originalMod = mod;
const patchedBy = new Set();
const factory = factories[id] = function (module, exports, require) {
try {
mod(module, exports, require);
} catch (err) {
// Just rethrow discord errors
if (mod === originalMod) throw err;
logger.error("Error in patched chunk", err);
return void originalMod(module, exports, require);
}
exports = module.exports;
if (!exports) return;
// There are (at the time of writing) 11 modules exporting the window
// Make these non enumerable to improve webpack search performance
if (exports === window) {
Object.defineProperty(require.c, id, {
value: require.c[id],
enumerable: false,
configurable: true,
writable: true
});
return;
}
const numberId = Number(id);
for (const callback of listeners) {
try {
callback(exports, numberId);
} catch (err) {
logger.error("Error in webpack listener", err);
}
}
for (const [filter, callback] of subscriptions) {
try {
if (filter(exports)) {
subscriptions.delete(filter);
callback(exports, numberId);
} else if (typeof exports === "object") {
if (exports.default && filter(exports.default)) {
subscriptions.delete(filter);
callback(exports.default, numberId);
}
for (const nested in exports) if (nested.length <= 3) {
if (exports[nested] && filter(exports[nested])) {
subscriptions.delete(filter);
callback(exports[nested], numberId);
}
}
}
} catch (err) {
logger.error("Error while firing callback for webpack chunk", err);
}
}
} as any as { toString: () => string, original: any, (...args: any[]): void; };
// for some reason throws some error on which calling .toString() leads to infinite recursion
// when you force load all chunks???
try {
factory.toString = () => mod.toString();
factory.original = originalMod;
} catch { }
for (let i = 0; i < patches.length; i++) {
const patch = patches[i];
const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace));
if (patch.predicate && !patch.predicate()) continue;
if (code.includes(patch.find)) {
patchedBy.add(patch.plugin);
// we change all patch.replacement to array in plugins/index
for (const replacement of patch.replacement as PatchReplacement[]) {
if (replacement.predicate && !replacement.predicate()) continue;
const lastMod = mod;
const lastCode = code;
canonicalizeReplacement(replacement, patch.plugin);
try {
const newCode = executePatch(replacement.match, replacement.replace as string);
if (newCode === code && !patch.noWarn) {
(window.explosivePlugins ??= new Set<string>()).add(patch.plugin);
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`);
if (IS_DEV) {
logger.debug("Function Source:\n", code);
}
} else {
code = newCode;
mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`);
}
} catch (err) {
logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err);
if (IS_DEV) {
const changeSize = code.length - lastCode.length;
const match = lastCode.match(replacement.match)!;
// Use 200 surrounding characters of context
const start = Math.max(0, match.index! - 200);
const end = Math.min(lastCode.length, match.index! + match[0].length + 200);
// (changeSize may be negative)
const endPatched = end + changeSize;
const context = lastCode.slice(start, end);
const patchedContext = code.slice(start, endPatched);
// inline require to avoid including it in !IS_DEV builds
const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext);
let fmt = "%c %s ";
const elements = [] as string[];
for (const d of diff) {
const color = d.removed
? "red"
: d.added
? "lime"
: "grey";
fmt += "%c%s";
elements.push("color:" + color, d.value);
}
logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context);
logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext);
const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff");
logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements);
}
code = lastCode;
mod = lastMod;
patchedBy.delete(patch.plugin);
}
}
if (!patch.all) patches.splice(i--, 1);
}
}
}
}

View File

@ -63,10 +63,10 @@ export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) {
if (cache !== void 0) throw "no."; if (cache !== void 0) throw "no.";
instance.push([[Symbol("Vencord")], {}, r => wreq = r]); instance.push([[Symbol("Vencord")], {}, r => wreq = r]);
instance.pop();
if (!wreq) return false; if (!wreq) return false;
cache = wreq.c; cache = wreq.c;
instance.pop();
for (const id in cache) { for (const id in cache) {
const { exports } = cache[id]; const { exports } = cache[id];