Compare commits

..

1 Commits

Author SHA1 Message Date
Lewis Crichton
64c7581286 feat(permissionfreewill): onboarding deletion 2023-10-03 17:46:17 +01:00
158 changed files with 2692 additions and 1217 deletions

View File

@ -51,10 +51,7 @@
"eqeqeq": ["error", "always", { "null": "ignore" }],
"spaced-comment": ["error", "always", { "markers": ["!"] }],
"yoda": "error",
"prefer-destructuring": ["error", {
"VariableDeclarator": { "array": false, "object": true },
"AssignmentExpression": { "array": false, "object": false }
}],
"prefer-destructuring": ["error", { "object": true, "array": false }],
"operator-assignment": ["error", "always"],
"no-useless-computed-key": "error",
"no-unneeded-ternary": ["error", { "defaultAssignment": false }],

View File

@ -36,10 +36,26 @@ jobs:
- name: Publish extension
run: |
# Do not fail so that even if chrome fails, firefox gets a shot. But also store exit code to fail workflow later
EXIT_CODE=0
# Chrome
cd dist/chromium-unpacked
pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish
pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish || EXIT_CODE=$?
# Firefox
cd ../firefox-unpacked
npm i -g web-ext@7.4.0 web-ext-submit@7.4.0
web-ext-submit || EXIT_CODE=$?
exit $EXIT_CODE
env:
# Chrome
EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
# Firefox
WEB_EXT_API_KEY: ${{ secrets.WEBEXT_USER }}
WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_SECRET }}

View File

@ -22,7 +22,29 @@ The cutest Discord client mod
## Installing / Uninstalling
Visit https://vencord.dev/download
Click the below button to install Vencord to the Discord Desktop app
[![Download and run the Installer](https://img.shields.io/github/v/release/Vencord/Installer?label=Download%20Vencord%20Installer&style=for-the-badge)](https://github.com/Vencord/Installer#vencord-installer)
## Installing on Browser
[![Get it on the Firefox Webstore](https://blog.mozilla.org/addons/files/2015/11/get-the-addon.png)](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) [![Get it on the Chrome Webstore](https://storage.googleapis.com/web-dev-uploads/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/vencord-web/cbghhgpcnddeihccjmnadmkaejncjndb)
Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that the CSS Editor, Themes loaded from remote sources and co. will not work in the UserScript. Use the extension if you need any of those
<details>
<summary>Alternative Downloads</summary>
## Vencord Desktop
> **Warning**
> This is an alternative app. It currently doesn't support keybinds and possibly some more features. If you just want to install to the normal Discord Desktop app, scroll up
As an alternative to the Discord Desktop app, Vencord also has its own standalone Desktop app that is snappier and lighter than Discord's official Desktop app
[![Download Vencord Desktop](https://img.shields.io/github/v/release/Vencord/Desktop?label=Download%20Vencord%20Desktop&style=for-the-badge)](https://github.com/Vencord/Desktop#vencord-desktop)
</details>
## Join our Support/Community Server

View File

@ -1,32 +0,0 @@
/**
* @template T
* @param {T[]} arr
* @param {(v: T) => boolean} predicate
*/
function removeFirst(arr, predicate) {
const idx = arr.findIndex(predicate);
if (idx !== -1) arr.splice(idx, 1);
}
chrome.webRequest.onHeadersReceived.addListener(
({ responseHeaders, type, url }) => {
if (!responseHeaders) return;
if (type === "main_frame") {
// In main frame requests, the CSP needs to be removed to enable fetching of custom css
// as desired by the user
removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-security-policy");
} else if (type === "stylesheet" && url.startsWith("https://raw.githubusercontent.com/")) {
// Most users will load css from GitHub, but GitHub doesn't set the correct content type,
// so we fix it here
removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-type");
responseHeaders.push({
name: "Content-Type",
value: "text/css"
});
}
return { responseHeaders };
},
{ urls: ["https://raw.githubusercontent.com/*", "*://*.discord.com/*"], types: ["main_frame", "stylesheet"] },
["blocking", "responseHeaders"]
);

View File

@ -26,11 +26,7 @@
}
],
"background": {
"scripts": ["background.js"]
},
"web_accessible_resources": ["dist/Vencord.js", "dist/Vencord.css"],
"web_accessible_resources": ["dist/*", "third-party/*"],
"browser_specific_settings": {
"gecko": {

View File

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.6.0",
"version": "1.5.4",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@ -94,7 +94,7 @@
"build": {
"overwriteDest": true
},
"sourceDir": "./dist/firefox-unpacked"
"sourceDir": "./dist/extension-v2-unpacked"
},
"engines": {
"node": ">=18",

View File

@ -43,7 +43,7 @@ const nodeCommonOpts = {
format: "cjs",
platform: "node",
target: ["esnext"],
external: ["electron", "original-fs", ...commonOpts.external],
external: ["electron", ...commonOpts.external],
define: defines,
};

View File

@ -145,11 +145,11 @@ async function loadDir(dir, basePath = "") {
/**
* @type {(target: string, files: string[]) => Promise<void>}
*/
async function buildExtension(target, files) {
async function buildExtension(target, files, noMonaco = false) {
const entries = {
"dist/Vencord.js": await readFile("dist/extension.js"),
"dist/Vencord.css": await readFile("dist/extension.css"),
...await loadDir("dist/monaco"),
...(noMonaco ? {} : await loadDir("dist/monaco")),
...Object.fromEntries(await Promise.all(RnNoiseFiles.map(async file =>
[`third-party/rnnoise/${file.replace(/^dist\//, "")}`, await readFile(`node_modules/@sapphi-red/web-noise-suppressor/${file}`)]
))),
@ -195,11 +195,8 @@ const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content
await Promise.all([
appendCssRuntime,
buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]),
buildExtension("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"]),
buildExtension("firefox-unpacked", ["content.js", "manifestv2.json", "icon.png"], true),
]);
Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension-chrome.zip");
console.info("Packed Chromium Extension written to dist/extension-chrome.zip");
Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip");
console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension.zip");
console.info("Packed Chromium Extension written to dist/extension.zip");

View File

@ -61,13 +61,6 @@ const report = {
otherErrors: [] as string[]
};
const IGNORED_DISCORD_ERRORS = [
"KeybindStore: Looking for callback action",
"Unable to process domain list delta: Client revision number is null",
"Downloading the full bad domains file",
/\[GatewaySocket\].{0,110}Cannot access '/
] as Array<string | RegExp>;
function toCodeBlock(s: string) {
s = s.replace(/```/g, "`\u200B`\u200B`");
return "```" + s + " ```";
@ -93,8 +86,6 @@ async function printReport() {
console.log(` - Error: ${toCodeBlock(p.error)}`);
});
report.otherErrors = report.otherErrors.filter(e => !IGNORED_DISCORD_ERRORS.some(regex => e.match(regex)));
console.log("## Discord Errors");
report.otherErrors.forEach(e => {
console.log(`- ${toCodeBlock(e)}`);
@ -268,7 +259,7 @@ function runTime(token: string) {
const { wreq } = Vencord.Webpack;
console.error("[PUP_DEBUG]", "Loading all chunks...");
const ids = Function("return" + wreq.u.toString().match(/(?<=\()\{.+?\}/s)![0])();
const ids = Function("return" + wreq.u.toString().match(/\{.+\}/s)![0])();
for (const id in ids) {
const isWasm = await fetch(wreq.p + wreq.u(id))
.then(r => r.text())

View File

@ -136,7 +136,7 @@ if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && navigator.platform.toLow
document.addEventListener("DOMContentLoaded", () => {
document.head.append(Object.assign(document.createElement("style"), {
id: "vencord-native-titlebar-style",
textContent: "[class*=titleBar]{display: none!important}"
textContent: "[class*=titleBar-]{display: none!important}"
}));
}, { once: true });
}

View File

@ -17,14 +17,14 @@
*/
import { mergeDefaults } from "@utils/misc";
import { findByPropsLazy } from "@webpack";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { SnowflakeUtils } from "@webpack/common";
import { Message } from "discord-types/general";
import type { PartialDeep } from "type-fest";
import { Argument } from "./types";
const MessageCreator = findByPropsLazy("createBotMessage");
const createBotMessage = findByCodeLazy('username:"Clyde"');
const MessageSender = findByPropsLazy("receiveMessage");
export function generateId() {
@ -38,7 +38,7 @@ export function generateId() {
* @returns {Message}
*/
export function sendBotMessage(channelId: string, message: PartialDeep<Message>): Message {
const botMessage = MessageCreator.createBotMessage({ channelId, content: "", embeds: [] });
const botMessage = createBotMessage({ channelId, content: "", embeds: [] });
MessageSender.receiveMessage(channelId, mergeDefaults(message, botMessage));

View File

@ -20,6 +20,7 @@ import { Channel, User } from "discord-types/general/index.js";
interface DecoratorProps {
activities: any[];
canUseAvatarDecorations: boolean;
channel: Channel;
/**
* Only for DM members
@ -51,9 +52,9 @@ export function removeDecorator(identifier: string) {
decorators.delete(identifier);
}
export function __getDecorators(props: DecoratorProps): (JSX.Element | null)[] {
export function __addDecoratorsToList(props: DecoratorProps): (JSX.Element | null)[] {
const isInGuild = !!(props.guildId);
return Array.from(decorators.values(), decoratorObj => {
return [...decorators.values()].map(decoratorObj => {
const { decorator, onlyIn } = decoratorObj;
// this can most likely be done cleaner
if (!onlyIn || (onlyIn === "guilds" && isInGuild) || (onlyIn === "dms" && !isInGuild)) {

View File

@ -237,7 +237,6 @@ type ResolvePropDeep<T, P> = P extends "" ? T :
export function addSettingsListener<Path extends keyof Settings>(path: Path, onUpdate: (newValue: Settings[Path], path: Path) => void): void;
export function addSettingsListener<Path extends string>(path: Path, onUpdate: (newValue: Path extends "" ? any : ResolvePropDeep<Settings, Path>, path: Path extends "" ? string : Path) => void): void;
export function addSettingsListener(path: string, onUpdate: (newValue: any, path: string) => void) {
if (path)
((onUpdate as SubscriptionCallback)._paths ??= []).push(path);
subscriptions.add(onUpdate);
}

69
src/api/SettingsStore.ts Normal file
View File

@ -0,0 +1,69 @@
/*
* 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 { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import { findModuleId, wreq } from "@webpack";
import { Settings } from "./Settings";
interface Setting<T> {
/**
* Get the setting value
*/
getSetting(): T;
/**
* Update the setting value
* @param value The new value
*/
updateSetting(value: T | ((old: T) => T)): Promise<void>;
/**
* React hook for automatically updating components when the setting is updated
*/
useSetting(): T;
settingsStoreApiGroup: string;
settingsStoreApiName: string;
}
const SettingsStores: Array<Setting<any>> | undefined = proxyLazy(() => {
const modId = findModuleId('"textAndImages","renderSpoilers"');
if (modId == null) return new Logger("SettingsStoreAPI").error("Didn't find stores module.");
const mod = wreq(modId);
if (mod == null) return;
return Object.values(mod).filter((s: any) => s?.settingsStoreApiGroup) as any;
});
/**
* Get the store for a setting
* @param group The setting group
* @param name The name of the setting
*/
export function getSettingStore<T = any>(group: string, name: string): Setting<T> | undefined {
if (!Settings.plugins.SettingsStoreAPI.enabled) throw new Error("Cannot use SettingsStoreAPI without setting as dependency.");
return SettingsStores?.find(s => s?.settingsStoreApiGroup === group && s?.settingsStoreApiName === name);
}
/**
* getSettingStore but lazy
*/
export function getSettingStoreLazy<T = any>(group: string, name: string) {
return proxyLazy(() => getSettingStore<T>(group, name));
}

View File

@ -29,6 +29,7 @@ import * as $Notices from "./Notices";
import * as $Notifications from "./Notifications";
import * as $ServerList from "./ServerList";
import * as $Settings from "./Settings";
import * as $SettingsStore from "./SettingsStore";
import * as $Styles from "./Styles";
/**
@ -90,6 +91,10 @@ export const MemberListDecorators = $MemberListDecorators;
* An API allowing you to persist data
*/
export const Settings = $Settings;
/**
* An API allowing you to read, manipulate and automatically update components based on Discord settings
*/
export const SettingsStore = $SettingsStore;
/**
* An API allowing you to dynamically load styles
* a

View File

@ -94,7 +94,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
(async () => {
for (const user of plugin.authors.slice(0, 6)) {
const author = user.id
? await UserUtils.getUser(`${user.id}`)
? await UserUtils.fetchUser(`${user.id}`)
.catch(() => makeDummyUser({ username: user.name }))
: makeDummyUser({ username: user.name });

View File

@ -17,7 +17,6 @@
font-size: 20px;
height: 20px;
position: relative;
text-wrap: nowrap;
}
.vc-author-modal-name::before {

View File

@ -34,7 +34,7 @@ import { openModalLazy } from "@utils/modal";
import { useAwaiter } from "@utils/react";
import { Plugin } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
import { Alerts, Button, Card, Forms, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
import Plugins from "~plugins";
@ -251,7 +251,7 @@ export default function PluginSettings() {
}
DataStore.set("Vencord_existingPlugins", existingTimestamps);
return lodash.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins;
return window._.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins;
}));
type P = JSX.Element | JSX.Element[];

View File

@ -18,14 +18,16 @@
import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { DeleteIcon } from "@components/Icons";
import { Link } from "@components/Link";
import { IsFirefox } from "@utils/constants";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { findByPropsLazy, findLazy } from "@webpack";
import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack";
import { Button, Card, FluxDispatcher, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
import { UserThemeHeader } from "main/themes";
import type { ComponentType, Ref, SyntheticEvent } from "react";
@ -41,7 +43,7 @@ type FileInput = ComponentType<{
}>;
const InviteActions = findByPropsLazy("resolveInvite");
const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef);
const FileInput: FileInput = findByCodeLazy("activateUploadDialogue=");
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
const cl = classNameFactory("vc-settings-theme-");
@ -249,12 +251,14 @@ function ThemesTab() {
>
Load missing Themes
</Button>
{!IsFirefox && (
<Button
onClick={() => VencordNative.quickCss.openEditor()}
size={Button.Sizes.SMALL}
>
Edit QuickCSS
</Button>
)}
</>
</Card>
@ -316,6 +320,15 @@ function ThemesTab() {
return (
<SettingsTab title="Themes">
{IsFirefox && (
<ErrorCard>
<Forms.FormTitle tag="h5">Warning</Forms.FormTitle>
<Forms.FormText>
You are using Firefox. Expect the vast majority of themes to not work.
If this is a problem, use a chromium browser or Discord Desktop / Vesktop.
</Forms.FormText>
</ErrorCard>
)}
<TabBar
type="top"
look="brand"

View File

@ -21,6 +21,7 @@ import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton";
import { ErrorCard } from "@components/ErrorCard";
import { IsFirefox } from "@utils/constants";
import { Margins } from "@utils/margins";
import { identity } from "@utils/misc";
import { relaunch, showItemInFolder } from "@utils/native";
@ -109,12 +110,14 @@ function VencordSettings() {
Restart Client
</Button>
)}
{!IsFirefox && (
<Button
onClick={() => VencordNative.quickCss.openEditor()}
size={Button.Sizes.SMALL}
disabled={settingsDir === "Loading..."}>
Open QuickCSS File
</Button>
)}
{!IS_WEB && (
<Button
onClick={() => showItemInFolder(settingsDir)}

View File

@ -62,10 +62,6 @@ if (IS_VESKTOP || !IS_VANILLA) {
} catch { }
const findHeader = (headers: Record<string, string[]>, headerName: Lowercase<string>) => {
return Object.keys(headers).find(h => h.toLowerCase() === headerName);
};
// Remove CSP
type PolicyResult = Record<string, string[]>;
@ -77,7 +73,6 @@ if (IS_VESKTOP || !IS_VANILLA) {
result[directiveKey] = directiveValue;
}
});
return result;
};
const stringifyPolicy = (policy: PolicyResult): string =>
@ -86,39 +81,31 @@ if (IS_VESKTOP || !IS_VANILLA) {
.map(directive => directive.flat().join(" "))
.join("; ");
const patchCsp = (headers: Record<string, string[]>) => {
const header = findHeader(headers, "content-security-policy");
if (header) {
function patchCsp(headers: Record<string, string[]>, header: string) {
if (header in headers) {
const csp = parsePolicy(headers[header][0]);
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
csp[directive] ??= [];
csp[directive].push("*", "blob:", "data:", "vencord:", "'unsafe-inline'");
csp[directive] = ["*", "blob:", "data:", "vencord:", "'unsafe-inline'"];
}
// TODO: Restrict this to only imported packages with fixed version.
// Perhaps auto generate with esbuild
csp["script-src"] ??= [];
csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
headers[header] = [stringifyPolicy(csp)];
}
};
}
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders);
patchCsp(responseHeaders, "content-security-policy");
// Fix hosts that don't properly set the css content type, such as
// raw.githubusercontent.com
if (resourceType === "stylesheet") {
const header = findHeader(responseHeaders, "content-type");
if (header)
responseHeaders[header] = ["text/css"];
if (resourceType === "stylesheet")
responseHeaders["content-type"] = ["text/css"];
}
}
cb({ cancel: false, responseHeaders });
});

View File

@ -17,7 +17,7 @@
*/
import { app } from "electron";
import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "original-fs";
import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "fs";
import { basename, dirname, join } from "path";
function isNewer($new: string, old: string) {

View File

@ -85,19 +85,17 @@ export default definePlugin({
},
{
// alt: "", aria-hidden: false, src: originalSrc
match: /alt:" ","aria-hidden":!0,src:(?=(\i)\.src)/,
match: /alt:" ","aria-hidden":!0,src:(?=(\i)\.src)/g,
// ...badge.props, ..., src: badge.image ?? ...
replace: "...$1.props,$& $1.image??"
},
// replace their component with ours if applicable
{
match: /(?<=text:(\i)\.description,spacing:12,)children:/,
replace: "children:$1.component ? () => $self.renderBadgeComponent($1) :"
match: /children:function(?<=(\i)\.(?:tooltip|description),spacing:\d.+?)/g,
replace: "children:$1.component ? () => $self.renderBadgeComponent($1) : function"
},
// conditionally override their onClick with badge.onClick if it exists
{
match: /href:(\i)\.link/,
replace: "...($1.onClick && { onClick: $1.onClick }),$&"
match: /onClick:function(?=.{0,200}href:(\i)\.link)/,
replace: "onClick:$1.onClick??function"
}
]
}

View File

@ -26,7 +26,7 @@ export default definePlugin({
patches: [
// obtain BUILT_IN_COMMANDS instance
{
find: ',"tenor"',
find: '"giphy","tenor"',
replacement: [
{
// Matches BUILT_IN_COMMANDS. This is not exported so this is
@ -34,7 +34,7 @@ export default definePlugin({
// patch simpler
// textCommands = builtInCommands.filter(...)
match: /(?<=\w=)(\w)(\.filter\(.{0,60}tenor)/,
match: /(?<=\w=)(\w)(\.filter\(.{0,30}giphy)/,
replace: "Vencord.Api.Commands._init($1)$2",
}
],
@ -44,8 +44,8 @@ export default definePlugin({
find: "Unexpected value for option",
replacement: {
// return [2, cmd.execute(args, ctx)]
match: /,(\i)\.execute\((\i),(\i)\)/,
replace: (_, cmd, args, ctx) => `,Vencord.Api.Commands._handleCommand(${cmd}, ${args}, ${ctx})`
match: /,(.{1,2})\.execute\((.{1,2}),(.{1,2})\)]/,
replace: (_, cmd, args, ctx) => `,Vencord.Api.Commands._handleCommand(${cmd}, ${args}, ${ctx})]`
}
},
// Show plugin name instead of "Built-In"

View File

@ -29,8 +29,8 @@ export default definePlugin({
{
find: "♫ (つ。◕‿‿◕。)つ ♪",
replacement: {
match: /let{navId:/,
replace: "Vencord.Api.ContextMenu._patchContextMenu(arguments[0]);$&"
match: /(?<=function \i\((\i)\){)(?=var \i,\i=\i\.navId)/,
replace: (_, props) => `Vencord.Api.ContextMenu._patchContextMenu(${props});`
}
},
{

View File

@ -22,25 +22,20 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "MemberListDecoratorsAPI",
description: "API to add decorators to member list (both in servers and DMs)",
authors: [Devs.TheSun, Devs.Ven],
authors: [Devs.TheSun],
patches: [
{
find: ".lostPermission)",
replacement: [
{
match: /let\{[^}]*lostPermissionTooltipText:\i[^}]*\}=(\i),/,
replace: "$&vencordProps=$1,"
}, {
match: /decorators:.{0,100}?children:\[/,
replace: "$&...(typeof vencordProps=='undefined'?[]:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps)),"
find: "lostPermissionTooltipText,",
replacement: {
match: /Fragment,{children:\[(.{30,80})\]/,
replace: "Fragment,{children:Vencord.Api.MemberListDecorators.__addDecoratorsToList(this.props).concat($1)"
}
]
},
{
find: "PrivateChannel.renderAvatar",
replacement: {
match: /decorators:(\i\.isSystemDM\(\))\?(.+?):null/,
replace: "decorators:[...Vencord.Api.MemberListDecorators.__getDecorators(arguments[0]), $1?$2:null]"
match: /(subText:(.{1,2})\.renderSubtitle\(\).{1,50}decorators):(.{30,100}:null)/,
replace: "$1:Vencord.Api.MemberListDecorators.__addDecoratorsToList($2.props).concat($3)"
}
}
],

View File

@ -27,8 +27,9 @@ export default definePlugin({
{
find: ".Messages.REMOVE_ATTACHMENT_BODY",
replacement: {
match: /(?<=.container\)?,children:)(\[.+?\])/,
replace: "Vencord.Api.MessageAccessories._modifyAccessories($1,this.props)",
match: /(.container\)?,children:)(\[[^\]]+\])(}\)\};return)/,
replace: (_, pre, accessories, post) =>
`${pre}Vencord.Api.MessageAccessories._modifyAccessories(${accessories},this.props)${post}`,
},
},
],

View File

@ -25,10 +25,10 @@ export default definePlugin({
authors: [Devs.TheSun],
patches: [
{
find: '"Message Username"',
find: ".withMentionPrefix",
replacement: {
match: /currentUserIsPremium:.{0,70}{children:\i(?=}\))/,
replace: "$&.concat(Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0]))"
match: /(.roleDot.{10,50}{children:.{1,2})}\)/,
replace: "$1.concat(Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0]))})"
}
}
],

View File

@ -27,8 +27,10 @@ export default definePlugin({
{
find: '"MessageActionCreators"',
replacement: {
match: /async editMessage\(.+?\)\{/,
replace: "$&await Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
// editMessage: function (...) {
match: /\beditMessage:(function\(.+?\))\{/,
// editMessage: async function (...) { await handlePreEdit(...); ...
replace: "editMessage:async $1{await Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
}
},
{
@ -36,7 +38,7 @@ export default definePlugin({
replacement: {
// props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply);
// Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid)
match: /(type:this\.props\.chatInputType.+?\.then\()(\i=>\{.+?let (\i)=\i\.\i\.parse\((\i),.+?let (\i)=\i\.\i\.getSendMessageOptionsForReply\(\i\);)(?<=\)\(({.+?})\)\.then.+?)/,
match: /(props\.chatInputType.+?\.then\(\()(function.+?var (\i)=\i\.\i\.parse\((\i),.+?var (\i)=\i\.\i\.getSendMessageOptionsForReply\(\i\);)(?<=\)\(({.+?})\)\.then.+?)/,
// props.chatInputType...then((async function(isMessageValid)... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply); if(await Vencord.api...) return { shoudClear:true, shouldRefocus:true };
replace: (_, rest1, rest2, parsedMessage, channel, replyOptions, extra) => "" +
`${rest1}async ${rest2}` +
@ -47,10 +49,10 @@ export default definePlugin({
{
find: '("interactionUsernameProfile',
replacement: {
match: /let\{id:\i}=(\i),{id:\i}=(\i);return \i\.useCallback\((\i)=>\{/,
match: /var \i=(\i)\.id,\i=(\i)\.id;return \i\.useCallback\(\(?function\((\i)\){/,
replace: (m, message, channel, event) =>
// the message param is shadowed by the event param, so need to alias them
`const vcMsg=${message},vcChan=${channel};${m}Vencord.Api.MessageEvents._handleClick(vcMsg, vcChan, ${event});`
`var _msg=${message},_chan=${channel};${m}Vencord.Api.MessageEvents._handleClick(_msg, _chan, ${event});`
}
}
]

View File

@ -29,12 +29,12 @@ export default definePlugin({
find: 'displayName="NoticeStore"',
replacement: [
{
match: /\i=null;(?=.{0,80}getPremiumSubscription\(\))/g,
replace: "if(Vencord.Api.Notices.currentNotice)return false;$&"
match: /(?=;\i=null;.{0,70}getPremiumSubscription)/g,
replace: ";if(Vencord.Api.Notices.currentNotice)return false"
},
{
match: /(?<=,NOTICE_DISMISS:function\(\i\){)return null!=(\i)/,
replace: "if($1.id==\"VencordNotice\")return($1=null,Vencord.Api.Notices.nextNotice(),true);$&"
match: /(?<=,NOTICE_DISMISS:function\(\i\){)(?=if\(null==(\i)\))/,
replace: (_, notice) => `if(${notice}.id=="VencordNotice")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);`
}
]
}

View File

@ -27,15 +27,15 @@ export default definePlugin({
{
find: "Messages.DISCODO_DISABLED",
replacement: {
match: /(?<=Messages\.DISCODO_DISABLED.+?return)(\(.{0,75}?tutorialContainer.+?}\))(?=}function)/,
replace: "[$1].concat(Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.Above))"
match: /(Messages\.DISCODO_DISABLED.+?return)(\(.{0,75}?tutorialContainer.+?}\))(?=}function)/,
replace: "$1[$2].concat(Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.Above))"
}
},
{
find: "Messages.SERVERS,children",
replacement: {
match: /(?<=Messages\.SERVERS,children:).+?default:return null\}\}\)/,
replace: "Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.In).concat($&)"
match: /(Messages\.SERVERS,children:)(.+?default:return null\}\}\)\))/,
replace: "$1Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.In).concat($2)"
}
}
]

View File

@ -0,0 +1,38 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "SettingsStoreAPI",
description: "Patches Discord's SettingsStores to expose their group and name",
authors: [Devs.Nuckyz],
patches: [
{
find: '"textAndImages","renderSpoilers"',
replacement: [
{
match: /(?<=INFREQUENT_USER_ACTION.{0,20}),useSetting:function/,
replace: ",settingsStoreApiGroup:arguments[0],settingsStoreApiName:arguments[1]$&"
}
]
}
]
});

View File

@ -26,7 +26,7 @@ export default definePlugin({
required: true,
patches: [
{
find: "AnalyticsActionHandlers.handle",
find: "TRACKING_URL:",
replacement: {
match: /^.+$/,
replace: "()=>{}",
@ -43,21 +43,20 @@ export default definePlugin({
find: ".METRICS,",
replacement: [
{
match: /this\._intervalId=/,
replace: "this._intervalId=undefined&&"
match: /this\._intervalId.+?12e4\)/,
replace: ""
},
{
match: /(increment\(\i\){)/,
replace: "$1return;"
match: /(?<=increment=function\(\i\){)/,
replace: "return;"
}
]
},
{
find: ".installedLogHooks)",
replacement: {
// if getDebugLogging() returns false, the hooks don't get installed.
match: "getDebugLogging(){",
replace: "getDebugLogging(){return false;"
match: /if\(\i\.getDebugLogging\(\)&&!\i\.installedLogHooks\)/,
replace: "if(false)"
}
},
]

View File

@ -39,9 +39,8 @@ export default definePlugin({
addContextMenuPatch("user-settings-cog", children => () => {
const section = children.find(c => Array.isArray(c) && c.some(it => it?.props?.id === "VencordSettings")) as any;
section?.forEach(c => {
const id = c?.props?.id;
if (id?.startsWith("Vencord") || id?.startsWith("Vesktop")) {
c.props.action = () => SettingsRouter.open(id);
if (c?.props?.id?.startsWith("Vencord")) {
c.props.action = () => SettingsRouter.open(c.props.id);
}
});
});
@ -63,26 +62,26 @@ export default definePlugin({
replacement: {
get match() {
switch (Settings.plugins.Settings.settingsLocation) {
case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS\}/;
case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS\}/;
case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS\}/;
case "belowActivity": return /(?<=\{section:(\i\.\i)\.DIVIDER},)\{section:"changelog"/;
case "bottom": return /\{section:(\i\.\i)\.CUSTOM,\s*element:.+?}/;
case "top": return /\{section:(\i)\.ID\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS\}/;
case "aboveNitro": return /\{section:(\i)\.ID\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS\}/;
case "belowNitro": return /\{section:(\i)\.ID\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS\}/;
case "belowActivity": return /(?<=\{section:(\i)\.ID\.DIVIDER},)\{section:"changelog"/;
case "bottom": return /\{section:(\i)\.ID\.CUSTOM,\s*element:.+?}/;
case "aboveActivity":
default:
return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS\}/;
return /\{section:(\i)\.ID\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS\}/;
}
},
replace: "...$self.makeSettingsCategories($1),$&"
}
}],
customSections: [] as ((SectionTypes: Record<string, unknown>) => any)[],
customSections: [] as ((ID: Record<string, unknown>) => any)[],
makeSettingsCategories(SectionTypes: Record<string, unknown>) {
makeSettingsCategories({ ID }: { ID: Record<string, unknown>; }) {
return [
{
section: SectionTypes.HEADER,
section: ID.HEADER,
label: "Vencord",
className: "vc-settings-header"
},
@ -128,9 +127,9 @@ export default definePlugin({
element: require("@components/VencordSettings/PatchHelperTab").default,
className: "vc-patch-helper"
},
...this.customSections.map(func => func(SectionTypes)),
...this.customSections.map(func => func(ID)),
{
section: SectionTypes.DIVIDER
section: ID.DIVIDER
}
].filter(Boolean);
},

View File

@ -17,7 +17,7 @@
*/
import { DataStore } from "@api/index";
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
import { Devs, IsFirefox, SUPPORT_CHANNEL_ID } from "@utils/constants";
import { isPluginDev } from "@utils/misc";
import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types";
@ -30,6 +30,7 @@ import plugins from "~plugins";
import settings from "./settings";
const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
const FIREFOX_DISMISS_KEY = "Vencord-Firefox-Warning-Dismiss";
const AllowedChannelIds = [
SUPPORT_CHANNEL_ID,
@ -115,6 +116,22 @@ ${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "
onConfirm: rememberDismiss
});
}
if (IsFirefox) {
const rememberDismiss = () => DataStore.set(FIREFOX_DISMISS_KEY, true);
Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>You are using Firefox.</Forms.FormText>
<Forms.FormText>Due to Firefox's stupid extension guidelines, most themes and many plugins will not function correctly.</Forms.FormText>
<Forms.FormText>Do not report bugs. Do not ask for help with broken plugins.</Forms.FormText>
<Forms.FormText>Instead, use a chromium browser, Discord Desktop, or Vesktop.</Forms.FormText>
</div>,
onCancel: rememberDismiss,
onConfirm: rememberDismiss
});
}
}
}
});

View File

@ -27,15 +27,15 @@ export default definePlugin({
{
find: ".displayName=\"MaskedLinkStore\"",
replacement: {
match: /(?<=isTrustedDomain\(\i\){)return \i\(\i\)/,
replace: "return true"
match: /\.isTrustedDomain=function\(.\){return.+?};/,
replace: ".isTrustedDomain=function(){return true};"
}
},
{
find: "isSuspiciousDownload:",
find: '"7z","ade","adp"',
replacement: {
match: /function \i\(\i\){(?=.{0,60}\.parse\(\i\))/,
replace: "$&return null;"
match: /JSON\.parse\('\[.+?'\)/,
replace: "[]"
}
}
]

View File

@ -20,19 +20,26 @@ import { popNotice, showNotice } from "@api/Notices";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from "@webpack/common";
import { filters, findByCodeLazy, mapMangledModuleLazy } from "@webpack";
import { FluxDispatcher, Forms, Toasts } from "@webpack/common";
const RpcUtils = findByPropsLazy("fetchApplicationsRPC", "getRemoteIconURL");
const assetManager = mapMangledModuleLazy(
"getAssetImage: size must === [number, number] for Twitch",
{
getAsset: filters.byCode("apply("),
}
);
const lookupRpcApp = findByCodeLazy(".APPLICATION_RPC(");
async function lookupAsset(applicationId: string, key: string): Promise<string> {
return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];
return (await assetManager.getAsset(applicationId, [key, undefined]))[0];
}
const apps: any = {};
async function lookupApp(applicationId: string): Promise<string> {
const socket: any = {};
await RpcUtils.fetchApplicationsRPC(socket, applicationId);
await lookupRpcApp(socket, applicationId);
return socket.application;
}
@ -51,26 +58,6 @@ export default definePlugin({
</>
),
async handleEvent(e: MessageEvent<any>) {
const data = JSON.parse(e.data);
const { activity } = data;
const assets = activity?.assets;
if (assets?.large_image) assets.large_image = await lookupAsset(activity.application_id, assets.large_image);
if (assets?.small_image) assets.small_image = await lookupAsset(activity.application_id, assets.small_image);
if (activity) {
const appId = activity.application_id;
apps[appId] ||= await lookupApp(appId);
const app = apps[appId];
activity.name ||= app.name;
}
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", ...data });
},
async start() {
// ArmCord comes with its own arRPC implementation, so this plugin just confuses users
if ("armcord" in window) return;
@ -78,7 +65,22 @@ export default definePlugin({
if (ws) ws.close();
ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket
ws.onmessage = this.handleEvent;
ws.onmessage = async e => { // on message, set status to data
const data = JSON.parse(e.data);
if (data.activity?.assets?.large_image) data.activity.assets.large_image = await lookupAsset(data.activity.application_id, data.activity.assets.large_image);
if (data.activity?.assets?.small_image) data.activity.assets.small_image = await lookupAsset(data.activity.application_id, data.activity.assets.small_image);
if (data.activity) {
const appId = data.activity.application_id;
apps[appId] ||= await lookupApp(appId);
const app = apps[appId];
data.activity.name ||= app.name;
}
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", ...data });
};
const connectionSuccessful = await new Promise(res => setTimeout(() => res(ws.readyState === WebSocket.OPEN), 1000)); // check if open after 1s
if (!connectionSuccessful) {

View File

@ -27,7 +27,7 @@ export default definePlugin({
{
find: "BAN_CONFIRM_TITLE.",
replacement: {
match: /src:\i\("\d+"\)/g,
match: /src:\w\(\d+\)/g,
replace: "src: Vencord.Settings.plugins.BANger.source"
}
}

View File

@ -34,18 +34,17 @@ export default definePlugin({
},
},
{
find: ".Messages.GIF,",
find: ".embedGallerySide",
replacement: {
match: /alt:(\i)=(\i\.default\.Messages\.GIF)(?=,[^}]*\}=(\i))/,
match: /(?<==(.{1,3})\.alt.{0,20})\?.{0,5}\.Messages\.GIF/,
replace:
// rename prop so we can always use default value
"alt_$$:$1=$self.altify($3)||$2",
"?($1.alt='GIF',$self.altify($1))",
},
},
],
altify(props: any) {
if (props.alt && props.alt !== "GIF") return props.alt;
if (props.alt !== "GIF") return props.alt;
let url: string = props.original || props.src;
try {

View File

@ -19,9 +19,6 @@
import { Settings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
const UserPopoutSectionCssClasses = findByPropsLazy("section", "lastSection");
export default definePlugin({
name: "BetterNotesBox",
@ -37,20 +34,12 @@ export default definePlugin({
match: /hideNote:.+?(?=[,}])/g,
replace: "hideNote:true",
}
},
{
}, {
find: "Messages.NOTE_PLACEHOLDER",
replacement: {
match: /\.NOTE_PLACEHOLDER,/,
replace: "$&spellCheck:!Vencord.Settings.plugins.BetterNotesBox.noSpellCheck,"
}
},
{
find: ".Messages.NOTE}",
replacement: {
match: /(?<=return \i\?)null(?=:\(0,\i\.jsxs)/,
replace: "$self.patchPadding(arguments[0])"
}
}
],
@ -67,12 +56,5 @@ export default definePlugin({
disabled: () => Settings.plugins.BetterNotesBox.hide,
default: false
}
},
patchPadding(e: any) {
if (!e.lastSection) return;
return (
<div className={UserPopoutSectionCssClasses.lastSection}></div>
);
}
});

View File

@ -38,7 +38,6 @@ export default definePlugin({
{
find: '"dot"===',
all: true,
noWarn: true,
predicate: () => Settings.plugins.BetterRoleDot.bothStyles,
replacement: {
match: /"(?:username|dot)"===\i(?!\.\i)/g,
@ -49,7 +48,6 @@ export default definePlugin({
{
find: ".ADD_ROLE_A11Y_LABEL",
predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles,
noWarn: true,
replacement: {
match: /"dot"===\i/,
replace: "true"
@ -58,7 +56,6 @@ export default definePlugin({
{
find: ".roleVerifiedIcon",
predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles,
noWarn: true,
replacement: {
match: /"dot"===\i/,
replace: "true"

View File

@ -29,8 +29,9 @@ export default definePlugin({
replacement: {
// Discord merges multiple props here with Object.assign()
// This patch passes a third object to it with which we override onClick and onContextMenu
match: /CHAT_ATTACH_UPLOAD_OR_INVITE,onDoubleClick:(.+?:void 0),\.\.\.(\i),/,
replace: "$&onClick:$1,onContextMenu:$2.onClick,",
match: /CHAT_ATTACH_UPLOAD_OR_INVITE,onDoubleClick:(.+?:void 0)\},(.{1,3})\)/,
replace: (m, onDblClick, otherProps) =>
`${m.slice(0, -1)},{onClick:${onDblClick},onContextMenu:${otherProps}.onClick})`,
},
},
],

View File

@ -45,8 +45,11 @@ export default definePlugin({
{
find: ".embedWrapper,embed",
replacement: [{
match: /\.embedWrapper/g,
replace: "$&+(this.props.channel.nsfw?' vc-nsfw-img':'')"
match: /(\.renderEmbed=.+?(.)=.\.props)(.+?\.embedWrapper)/g,
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
}, {
match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\.embedWrapper)/g,
replace: "$1,vcProps=$2$3+(vcProps.nsfw?' vc-nsfw-img':'')"
}]
}
],

View File

@ -73,9 +73,9 @@ export default definePlugin({
},
patches: [{
find: "renderConnectionStatus(){",
find: ".renderConnectionStatus=",
replacement: {
match: /(?<=renderConnectionStatus\(\)\{.+\.channel,children:)\i/,
match: /(?<=renderConnectionStatus=.+\.channel,children:)\w/,
replace: "[$&, $self.renderTimer(this.props.channel.id)]"
}
}],

View File

@ -34,9 +34,8 @@ export default definePlugin({
{
find: ".AVATAR_STATUS_MOBILE_16;",
replacement: {
match: /(?<=fromIsMobile:\i=!0,.+?)status:(\i)/,
// Rename field to force it to always use "online"
replace: 'status_$:$1="online"'
match: /(\.fromIsMobile,.+?)\i.status/,
replace: (_, rest) => `${rest}"online"`
}
}
]

View File

@ -22,15 +22,23 @@ import { Devs } from "@utils/constants";
import { isTruthy } from "@utils/guards";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { ApplicationAssetUtils, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
import { FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
const ActivityComponent = findByCodeLazy("onOpenGameProfile");
const ActivityClassName = findByPropsLazy("activity", "buttonColor");
const Colors = findByPropsLazy("profileColors");
const assetManager = mapMangledModuleLazy(
"getAssetImage: size must === [number, number] for Twitch",
{
getAsset: filters.byCode("apply("),
}
);
async function getApplicationAsset(key: string): Promise<string> {
return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0];
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
return (await assetManager.getAsset(settings.store.appID, [key, undefined]))[0];
}
interface ActivityAssets {

View File

@ -147,8 +147,8 @@ export default definePlugin({
replacement: [
// patch componentDidMount to replace embed thumbnail and title
{
match: /render\(\)\{let\{embed:/,
replace: "componentDidMount=$self.embedDidMount;$&"
match: /(\i).render=function.{0,50}\i\.embed/,
replace: "$1.componentDidMount=$self.embedDidMount,$&"
},
// add dearrow button

View File

@ -27,7 +27,7 @@ export default definePlugin({
{
find: ".Messages.BOT_CALL_IDLE_DISCONNECT",
replacement: {
match: /(?<=function \i\(\){)(?=.{1,120}\.Messages\.BOT_CALL_IDLE_DISCONNECT)/,
match: /(?<=function \i\(\){)(?=.{1,100}\.Messages\.BOT_CALL_IDLE_DISCONNECT)/,
replace: "return;"
}
}

View File

@ -33,6 +33,12 @@ const settings = definePluginSettings({
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true
},
forceStagingBanner: {
description: "Whether to force Staging banner under user area.",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true
}
});
@ -52,7 +58,7 @@ export default definePlugin({
{
find: "Object.defineProperties(this,{isDeveloper",
replacement: {
match: /(?<={isDeveloper:\{[^}]+?,get:\(\)=>)\i/,
match: /(?<={isDeveloper:\{[^}]+?,get:function\(\)\{return )\w/,
replace: "true"
}
},
@ -64,26 +70,25 @@ export default definePlugin({
}
},
{
find: ".isStaff=()",
find: ".isStaff=function(){",
predicate: () => settings.store.enableIsStaff,
replacement: [
{
match: /=>*?(\i)\.hasFlag\((\i\.\i)\.STAFF\)}/,
replace: (_, user, flags) => `=>Vencord.Webpack.Common.UserStore.getCurrentUser()?.id===${user}.id||${user}.hasFlag(${flags}.STAFF)}`
match: /return\s*?(\i)\.hasFlag\((\i\.\i)\.STAFF\)}/,
replace: (_, user, flags) => `return Vencord.Webpack.Common.UserStore.getCurrentUser()?.id===${user}.id||${user}.hasFlag(${flags}.STAFF)}`
},
{
match: /hasFreePremium\(\){return this.isStaff\(\)\s*?\|\|/,
replace: "hasFreePremium(){return ",
match: /hasFreePremium=function\(\){return this.isStaff\(\)\s*?\|\|/,
replace: "hasFreePremium=function(){return ",
}
]
},
// Fix search history being disabled / broken with isStaff
{
find: '("showNewSearch")',
predicate: () => settings.store.enableIsStaff,
find: ".Messages.DEV_NOTICE_STAGING",
predicate: () => settings.store.forceStagingBanner,
replacement: {
match: /(?<=showNewSearch"\);return)\s?/,
replace: "!1&&"
match: /"staging"===window\.GLOBAL_ENV\.RELEASE_CHANNEL/,
replace: "true"
}
},
{

View File

@ -25,7 +25,7 @@ import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
import { ChannelStore, EmojiStore, FluxDispatcher, lodash, Parser, PermissionStore, UserStore } from "@webpack/common";
import { ChannelStore, EmojiStore, FluxDispatcher, Parser, PermissionStore, UserStore } from "@webpack/common";
import type { Message } from "discord-types/general";
import { applyPalette, GIFEncoder, quantize } from "gifenc";
import type { ReactElement, ReactNode } from "react";
@ -176,8 +176,8 @@ export default definePlugin({
predicate: () => settings.store.enableEmojiBypass,
replacement: [
{
match: /(?<=,intention:(\i).+?;)/,
replace: (_, intention) => `var fakeNitroIntention=${intention};`
match: /(?<=(\i)=\i\.intention)/,
replace: (_, intention) => `,fakeNitroIntention=${intention}`
},
{
match: /\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i(?=\))/g,
@ -222,7 +222,8 @@ export default definePlugin({
predicate: () => settings.store.enableStreamQualityBypass,
replacement: [
"canUseHighVideoUploadQuality",
"canStreamQuality",
// TODO: Remove the last two when they get removed from stable
"(?:canStreamQuality|canStreamHighQuality|canStreamMidQuality)",
].map(func => {
return {
match: new RegExp(`${func}:function\\(\\i(?:,\\i)?\\){`, "g"),
@ -234,7 +235,7 @@ export default definePlugin({
find: "STREAM_FPS_OPTION.format",
predicate: () => settings.store.enableStreamQualityBypass,
replacement: {
match: /guildPremiumTier:\i\.\i\.TIER_\d,?/g,
match: /(userPremiumType|guildPremiumTier):.{0,10}TIER_\d,?/g,
replace: ""
}
},
@ -295,7 +296,7 @@ export default definePlugin({
},
{
predicate: () => settings.store.transformStickers,
match: /renderAttachments=function\(\i\){var \i=this,(\i)=\i.attachments.+?;/,
match: /renderAttachments=function\(\i\){var (\i)=\i.attachments.+?;/,
replace: (m, attachments) => `${m}${attachments}=$self.filterAttachments(${attachments});`
}
]
@ -318,7 +319,7 @@ export default definePlugin({
find: ".EMOJI_UPSELL_POPOUT_MORE_EMOJIS_OPENED,",
predicate: () => settings.store.transformEmojis,
replacement: {
match: /isDiscoverable:\i,shouldHideRoleSubscriptionCTA:\i,(?<={node:(\i),.+?)/,
match: /isDiscoverable:\i,shouldHideRoleSubscriptionCTA:\i,(?<=(\i)=\i\.node.+?)/,
replace: (m, node) => `${m}fakeNitroNode:${node},`
}
},
@ -326,22 +327,8 @@ export default definePlugin({
find: ".Messages.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION",
predicate: () => settings.store.transformEmojis,
replacement: {
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)`
}
},
{
find: "canUsePremiumAppIcons:function",
replacement: {
match: /canUsePremiumAppIcons:function\(\i\){/,
replace: "$&return true;"
}
},
{
find: "location:\"AppIconHome\"",
replacement: {
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
replace: "true"
match: /(?<=\.Messages\.EMOJI_POPOUT_ADDED_PACK_DESCRIPTION.+?return ).{0,1200}\.Messages\.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION.+?(?=}\()/,
replace: reactNode => `$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!arguments[0]?.fakeNitroNode?.fake)`
}
}
],
@ -532,7 +519,7 @@ export default definePlugin({
};
try {
return modifyChildren(lodash.cloneDeep(content));
return modifyChildren(window._.cloneDeep(content));
} catch (err) {
new Logger("FakeNitro").error(err);
return content;

View File

@ -87,15 +87,15 @@ export default definePlugin({
authors: [Devs.Alyxia, Devs.Remty],
patches: [
{
find: "UserProfileStore",
find: "getUserProfile=",
replacement: {
match: /(?<=getUserProfile\(\i\){return )(\i\[\i\])/,
match: /(?<=getUserProfile=function\(\i\){return )(\i\[\i\])/,
replace: "$self.colorDecodeHook($1)"
}
}, {
find: ".USER_SETTINGS_PROFILE_THEME_ACCENT",
replacement: {
match: /RESET_PROFILE_THEME}\)(?<=color:(\i),.{0,500}?color:(\i),.{0,500}?)/,
match: /RESET_PROFILE_THEME}\)(?<=},color:(\i).+?},color:(\i).+?)/,
replace: "$&,$self.addCopy3y3Button({primary:$1,accent:$2})"
}
}

View File

@ -39,25 +39,20 @@ export default definePlugin({
description: "Puts your favorite emoji first in the emoji autocomplete.",
patches: [
{
find: "renderResults({results:",
find: ".activeCommandOption",
replacement: [
{
// https://regex101.com/r/N7kpLM/1
match: /let \i=.{1,100}renderResults\({results:(\i)\.query\.results,/,
replace: "$self.sortEmojis($1);$&"
},
],
// = someFunc(a.selectedIndex); ...trackEmojiSearch({ state: theState, isInPopoutExperimental: someBool })
match: /=\i\(\i\.selectedIndex\);(?=.+?state:(\i),isInPopoutExperiment:\i)/,
// self.sortEmojis(theState)
replace: "$&$self.sortEmojis($1);"
},
{
find: "MAX_AUTOCOMPLETE_RESULTS+",
replacement: [
// set maxCount to Infinity so our sortEmojis callback gets the entire list, not just the first 10
// and remove Discord's emojiResult slice, storing the endIndex on the array for us to use later
{
// https://regex101.com/r/x2mobQ/1
// searchEmojis(...,maxCount: stuff) ... endEmojis = emojis.slice(0, maxCount - gifResults.length)
match: /,maxCount:(\i)(.{1,500}\i)=(\i)\.slice\(0,(\i-\i\.length)\)/,
match: /,maxCount:(\i)(.+?)=(\i)\.slice\(0,(\1-\i\.length)\)/,
// ,maxCount:Infinity ... endEmojis = (emojis.sliceTo = n, emojis)
replace: ",maxCount:Infinity$2=($3.sliceTo=$4,$3)"
}

View File

@ -91,13 +91,13 @@ export default definePlugin({
patches: [
{
find: "renderHeaderContent()",
find: "renderCategoryExtras",
replacement: [
{
// https://regex101.com/r/07gpzP/1
// https://regex101.com/r/4uHtTE/1
// ($1 renderHeaderContent=function { ... switch (x) ... case FAVORITES:return) ($2) ($3 case default:return r.jsx(($<searchComp>), {...props}))
match: /(renderHeaderContent\(\).{1,150}FAVORITES:return)(.{1,150});(case.{1,200}default:return\(0,\i\.jsx\)\((?<searchComp>\i\..{1,10}),)/,
replace: "$1 this.state.resultType === 'Favorites' ? $self.renderSearchBar(this, $<searchComp>) : $2;$3"
match: /(renderHeaderContent=function.{1,150}FAVORITES:return)(.{1,150};)(case.{1,200}default:return\(0,\i\.jsx\)\((?<searchComp>\i\.\i))/,
replace: "$1 this.state.resultType === \"Favorites\" ? $self.renderSearchBar(this, $<searchComp>) : $2; $3"
},
{
// to persist filtered favorites when component re-renders.
@ -182,7 +182,7 @@ function SearchBar({ instance, SearchBarComponent }: { instance: Instance; Searc
ref={ref}
autoFocus={true}
className={containerClasses.searchBar}
size={SearchBarComponent.Sizes.SMALL}
size={SearchBarComponent.Sizes.MEDIUM}
onChange={onChange}
onClear={() => {
setQuery("");

View File

@ -19,7 +19,6 @@
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { GuildStore } from "@webpack/common";
import { Channel, User } from "discord-types/general";
export default definePlugin({
name: "ForceOwnerCrown",
@ -27,22 +26,33 @@ export default definePlugin({
authors: [Devs.D3SOX, Devs.Nickyux],
patches: [
{
find: "AVATAR_DECORATION_PADDING:",
// This is the logic where it decides whether to render the owner crown or not
find: ".renderOwner=",
replacement: {
match: /,isOwner:(\i),/,
replace: ",_isOwner:$1=$self.isGuildOwner(e),"
}
match: /isOwner;return null!=(\w+)?&&/g,
replace: "isOwner;if($self.isGuildOwner(this.props)){$1=true;}return null!=$1&&"
}
},
],
isGuildOwner(props: { user: User, channel: Channel, isOwner: boolean, guildId?: string; }) {
if (!props?.user?.id) return props.isOwner;
if (props.channel?.type === 3 /* GROUP_DM */)
return props.isOwner;
isGuildOwner(props) {
// Check if channel is a Group DM, if so return false
if (props?.channel?.type === 3) {
return false;
}
// guild id is in props twice, fallback if the first is undefined
const guildId = props.guildId ?? props.channel?.guild_id;
const userId = props.user.id;
const guildId = props?.guildId ?? props?.channel?.guild_id;
const userId = props?.user?.id;
return GuildStore.getGuild(guildId)?.ownerId === userId;
if (guildId && userId) {
const guild = GuildStore.getGuild(guildId);
if (guild) {
return guild.ownerId === userId;
}
console.error("[ForceOwnerCrown] failed to get guild", { guildId, guild, props });
} else {
console.error("[ForceOwnerCrown] no guildId or userId", { guildId, userId, props });
}
return false;
},
});

View File

@ -16,15 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getSettingStoreLazy } from "@api/SettingsStore";
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByCodeLazy } from "@webpack";
import { StatusSettingsStores } from "@webpack/common";
import style from "./style.css?managed";
const ShowCurrentGame = getSettingStoreLazy<boolean>("status", "showCurrentGame");
const Button = findByCodeLazy("Button.Sizes.NONE,disabled:");
function makeIcon(showCurrentGame?: boolean) {
@ -49,7 +50,7 @@ function makeIcon(showCurrentGame?: boolean) {
}
function GameActivityToggleButton() {
const showCurrentGame = StatusSettingsStores.ShowCurrentGame.useSetting();
const showCurrentGame = ShowCurrentGame?.useSetting();
return (
<Button
@ -57,7 +58,7 @@ function GameActivityToggleButton() {
icon={makeIcon(showCurrentGame)}
role="switch"
aria-checked={!showCurrentGame}
onClick={() => StatusSettingsStores.ShowCurrentGame.updateSetting(old => !old)}
onClick={() => ShowCurrentGame?.updateSetting(old => !old)}
/>
);
}
@ -66,6 +67,7 @@ export default definePlugin({
name: "GameActivityToggle",
description: "Adds a button next to the mic and deafen button to toggle game activity.",
authors: [Devs.Nuckyz, Devs.RuukuLada],
dependencies: ["SettingsStoreAPI"],
patches: [
{

View File

@ -1,3 +1,3 @@
[class*="withTagAsButton"] {
min-width: 88px !important;
min-width: 88px;
}

View File

@ -33,7 +33,7 @@ export default definePlugin({
patches: [{
find: ".handleSelectGIF=",
replacement: {
match: /\.handleSelectGIF=\i=>\{/,
match: /\.handleSelectGIF=function.+?\{/,
replace: ".handleSelectGIF=function(gif){return $self.handleSelect(gif);"
}
}],

View File

@ -18,9 +18,8 @@
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { proxyLazy } from "@utils/lazy";
import definePlugin, { OptionType } from "@utils/types";
import { findByProps, findByPropsLazy } from "@webpack";
import { findByPropsLazy } from "@webpack";
import { ContextMenu, FluxDispatcher, Menu } from "@webpack/common";
import { Channel, Message } from "discord-types/general";
@ -51,7 +50,6 @@ const settings = definePluginSettings({
}>();
const MessageActions = findByPropsLazy("sendGreetMessage");
const WELCOME_STICKERS = proxyLazy(() => findByProps("WELCOME_STICKERS")?.WELCOME_STICKERS);
function greet(channel: Channel, message: Message, stickers: string[]) {
const options = MessageActions.getSendMessageOptionsForReply({
@ -77,7 +75,7 @@ function greet(channel: Channel, message: Message, stickers: string[]) {
}
function GreetMenu({ channel, message }: { message: Message, channel: Channel; }) {
function GreetMenu({ stickers, channel, message }: { stickers: Sticker[], message: Message, channel: Channel; }) {
const s = settings.use(["greetMode", "multiGreetChoices"]);
const { greetMode, multiGreetChoices = [] } = s;
@ -107,7 +105,7 @@ function GreetMenu({ channel, message }: { message: Message, channel: Channel; }
<Menu.MenuGroup
label="Greet Stickers"
>
{WELCOME_STICKERS.map(sticker => (
{stickers.map(sticker => (
<Menu.MenuItem
key={sticker.id}
id={"greet-" + sticker.id}
@ -125,7 +123,7 @@ function GreetMenu({ channel, message }: { message: Message, channel: Channel; }
label="Unholy Multi-Greet"
id="unholy-multi-greet"
>
{WELCOME_STICKERS.map(sticker => {
{stickers.map(sticker => {
const checked = multiGreetChoices.some(s => s === sticker.id);
return (
@ -170,20 +168,21 @@ export default definePlugin({
{
find: "Messages.WELCOME_CTA_LABEL",
replacement: {
match: /innerClassName:\i\.welcomeCTAButton,(?<={channel:\i,message:\i}=(\i).{0,400}?)/,
replace: "$&onContextMenu:(vcEvent)=>$self.pickSticker(vcEvent, $1),"
match: /innerClassName:\i\(\).welcomeCTAButton,(?<=%\i\.length;return (\i)\[\i\].+?)/,
replace: "$&onContextMenu:(e)=>$self.pickSticker(e,$1,arguments[0]),"
}
}
],
pickSticker(
event: React.UIEvent,
stickers: Sticker[],
props: {
channel: Channel,
message: Message;
}
) {
if (!(props.message as any).deleted)
ContextMenu.open(event, () => <GreetMenu {...props} />);
ContextMenu.open(event, () => <GreetMenu stickers={stickers} {...props} />);
}
});

View File

@ -25,10 +25,10 @@ export default definePlugin({
authors: [Devs.botato, Devs.Animal],
patches: [
{
find: "hasFlag:{writable",
find: "),{hasFlag:",
replacement: {
match: /if\((\i)<=(?:1<<30|1073741824)\)return/,
replace: "if($1===(1<<20))return false;$&",
match: /(if\((.{1,2})<=1<<30\)return)/,
replace: "if($2===(1<<20)){return false};$1",
},
},
],

View File

@ -1,17 +1,28 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as DataStore from "@api/DataStore";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { useForceUpdater } from "@utils/react";
import definePlugin from "@utils/types";
import { findStoreLazy } from "@webpack";
import { StatusSettingsStores, Tooltip } from "webpack/common";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { Tooltip } from "webpack/common";
const enum ActivitiesTypes {
Game,
@ -20,153 +31,203 @@ const enum ActivitiesTypes {
interface IgnoredActivity {
id: string;
name: string;
type: ActivitiesTypes;
}
const RegisteredGamesClasses = findByPropsLazy("overlayToggleIconOff", "overlayToggleIconOn");
const TryItOutClasses = findByPropsLazy("tryItOutBadge", "tryItOutBadgeIcon");
const BaseShapeRoundClasses = findByPropsLazy("baseShapeRound", "baseShapeRoundLeft", "baseShapeRoundRight");
const RunningGameStore = findStoreLazy("RunningGameStore");
function ToggleIcon(activity: IgnoredActivity, tooltipText: string, path: string, fill: string) {
function ToggleIconOff() {
return (
<svg
className={RegisteredGamesClasses.overlayToggleIconOff}
height="24"
width="24"
viewBox="0 2.2 32 26"
aria-hidden={true}
role="img"
>
<g
fill="none"
fillRule="evenodd"
>
<path
className={RegisteredGamesClasses.fill}
fill="currentColor"
d="M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z"
/>
<rect
className={RegisteredGamesClasses.fill}
x="3"
y="26"
width="26"
height="2"
transform="rotate(-45 2 20)"
/>
</g>
</svg>
);
}
function ToggleIconOn({ forceWhite }: { forceWhite?: boolean; }) {
return (
<svg
className={RegisteredGamesClasses.overlayToggleIconOn}
height="24"
width="24"
viewBox="0 2.2 32 26"
>
<path
className={forceWhite ? "" : RegisteredGamesClasses.fill}
fill={forceWhite ? "var(--white-500)" : ""}
d="M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z"
/>
</svg>
);
}
function ToggleActivityComponent({ activity, forceWhite, forceLeftMargin }: { activity: IgnoredActivity; forceWhite?: boolean; forceLeftMargin?: boolean; }) {
const forceUpdate = useForceUpdater();
return (
<Tooltip text={tooltipText}>
{tooltipProps => (
<button
{...tooltipProps}
<Tooltip text="Toggle activity">
{({ onMouseLeave, onMouseEnter }) => (
<div
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
className={RegisteredGamesClasses.overlayToggleIcon}
role="button"
aria-label="Toggle activity"
tabIndex={0}
style={forceLeftMargin ? { marginLeft: "2px" } : undefined}
onClick={e => handleActivityToggle(e, activity, forceUpdate)}
style={{ all: "unset", cursor: "pointer", display: "flex", justifyContent: "center", alignItems: "center" }}
>
<svg
width="24"
height="24"
viewBox="0 -960 960 960"
>
<path fill={fill} d={path} />
</svg>
</button>
{
ignoredActivitiesCache.has(activity.id)
? <ToggleIconOff />
: <ToggleIconOn forceWhite={forceWhite} />
}
</div>
)}
</Tooltip>
);
}
const ToggleIconOn = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, "Disable Activity", "M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-146 0-266-81.5T40-500q54-137 174-218.5T480-800q146 0 266 81.5T920-500q-54 137-174 218.5T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z", fill);
const ToggleIconOff = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, "Enable Activity", "m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z", fill);
function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) {
if (getIgnoredActivities().some(act => act.id === activity.id)) return ToggleIconOff(activity, "var(--status-danger)");
return ToggleIconOn(activity, isPlaying ? "var(--green-300)" : "var(--primary-400)");
function ToggleActivityComponentWithBackground({ activity }: { activity: IgnoredActivity; }) {
return (
<div
className={`${TryItOutClasses.tryItOutBadge} ${BaseShapeRoundClasses.baseShapeRound}`}
style={{ padding: "0px 2px", height: 28 }}
>
<ToggleActivityComponent activity={activity} forceWhite={true} />
</div>
);
}
function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>, activity: IgnoredActivity, forceUpdateButton: () => void) {
function handleActivityToggle(e: React.MouseEvent<HTMLDivElement, MouseEvent>, activity: IgnoredActivity, forceUpdateComponent: () => void) {
e.stopPropagation();
const ignoredActivityIndex = getIgnoredActivities().findIndex(act => act.id === activity.id);
if (ignoredActivityIndex === -1) settings.store.ignoredActivities = getIgnoredActivities().concat(activity);
else settings.store.ignoredActivities = getIgnoredActivities().filter((_, index) => index !== ignoredActivityIndex);
// Trigger activities recalculation
StatusSettingsStores.ShowCurrentGame.updateSetting(old => old);
forceUpdateButton();
if (ignoredActivitiesCache.has(activity.id)) ignoredActivitiesCache.delete(activity.id);
else ignoredActivitiesCache.set(activity.id, activity);
forceUpdateComponent();
saveCacheToDatastore();
}
const settings = definePluginSettings({}).withPrivateSettings<{
ignoredActivities: IgnoredActivity[];
}>();
function getIgnoredActivities() {
return settings.store.ignoredActivities ??= [];
async function saveCacheToDatastore() {
await DataStore.set("IgnoreActivities_ignoredActivities", ignoredActivitiesCache);
}
let ignoredActivitiesCache = new Map<IgnoredActivity["id"], IgnoredActivity>();
export default definePlugin({
name: "IgnoreActivities",
authors: [Devs.Nuckyz],
description: "Ignore activities from showing up on your status ONLY. You can configure which ones are ignored from the Registered Games and Activities tabs.",
settings,
description: "Ignore certain activities (like games and actual activities) from showing up on your status. You can configure which ones are ignored from the Registered Games and Activities tabs.",
patches: [
{
find: '.displayName="LocalActivityStore"',
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
replacement: {
match: /!(\i)(\)return null;var \i=(\i)\.overlay.+?children:)(\[.{0,70}overlayStatusText.+?\])(?=}\)}\(\))/,
replace: (_, platformCheck, restWithoutPlatformCheck, props, children) => "false"
+ `${restWithoutPlatformCheck}`
+ `(${platformCheck}?${children}:[])`
+ `.concat(Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(${props}))`
}
},
{
find: ".overlayBadge",
replacement: [
{
match: /LISTENING.+?}\),(?<=(\i)\.push.+?)/,
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored),`
match: /(?<=\(\)\.badgeContainer,children:).{0,50}?name:(\i)\.name.+?null/,
replace: (m, props) => `[${m},$self.renderToggleActivityButton(${props})]`
},
{
match: /(?<=\(\)\.badgeContainer,children:).{0,50}?name:(\i\.application)\.name.+?null/,
replace: (m, props) => `${m},$self.renderToggleActivityButton(${props})`
}
]
},
{
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
find: '.displayName="LocalActivityStore"',
replacement: {
match: /\.Messages\.SETTINGS_GAMES_TOGGLE_OVERLAY.+?}\(\),(?<={overlay:\i,.+?=(\i),.+?)(?=!(\i))/,
replace: (m, props, nowPlaying) => `${m}$self.renderToggleGameActivityButton(${props},${nowPlaying}),`
}
},
{
find: ".activityTitleText,variant",
replacement: {
match: /(?<=\i\.activityTitleText.+?children:(\i)\.name.*?}\),)/,
replace: (_, props) => `$self.renderToggleActivityButton(${props}),`
},
},
{
find: ".activityCardDetails,children",
replacement: {
match: /(?<=\i\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),)/,
replace: (_, props) => `$self.renderToggleActivityButton(${props}),`
match: /LISTENING.+?\)\);(?<=(\i)\.push.+?)/,
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`
}
}
],
async start() {
const oldIgnoredActivitiesData = await DataStore.get<Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities");
if (oldIgnoredActivitiesData != null) {
settings.store.ignoredActivities = Array.from(oldIgnoredActivitiesData.values())
.map(activity => ({ ...activity, name: "Unknown Name" }));
DataStore.del("IgnoreActivities_ignoredActivities");
const ignoredActivitiesData = await DataStore.get<string[] | Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities") ?? new Map<IgnoredActivity["id"], IgnoredActivity>();
/** Migrate old data */
if (Array.isArray(ignoredActivitiesData)) {
for (const id of ignoredActivitiesData) {
ignoredActivitiesCache.set(id, { id, type: ActivitiesTypes.Game });
}
if (getIgnoredActivities().length !== 0) {
const gamesSeen = RunningGameStore.getGamesSeen() as { id?: string; exePath: string; }[];
await saveCacheToDatastore();
} else ignoredActivitiesCache = ignoredActivitiesData;
for (const [index, ignoredActivity] of getIgnoredActivities().entries()) {
if (ignoredActivitiesCache.size !== 0) {
const gamesSeen: { id?: string; exePath: string; }[] = RunningGameStore.getGamesSeen();
for (const ignoredActivity of ignoredActivitiesCache.values()) {
if (ignoredActivity.type !== ActivitiesTypes.Game) continue;
if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) {
getIgnoredActivities().splice(index, 1);
/** Custom added game which no longer exists */
ignoredActivitiesCache.delete(ignoredActivity.id);
}
}
await saveCacheToDatastore();
}
},
renderToggleGameActivityButton(props: { id?: string; exePath: string; }) {
return (
<ErrorBoundary noop>
<ToggleActivityComponent activity={{ id: props.id ?? props.exePath, type: ActivitiesTypes.Game }} forceLeftMargin={true} />
</ErrorBoundary>
);
},
renderToggleActivityButton(props: { id: string; }) {
return (
<ErrorBoundary noop>
<ToggleActivityComponentWithBackground activity={{ id: props.id, type: ActivitiesTypes.Embedded }} />
</ErrorBoundary>
);
},
isActivityNotIgnored(props: { type: number; application_id?: string; name?: string; }) {
if (props.type === 0 || props.type === 3) {
if (props.application_id != null) return !getIgnoredActivities().some(activity => activity.id === props.application_id);
if (props.type === 0) {
if (props.application_id !== undefined) return !ignoredActivitiesCache.has(props.application_id);
else {
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
if (exePath) return !getIgnoredActivities().some(activity => activity.id === exePath);
if (exePath) return !ignoredActivitiesCache.has(exePath);
}
}
return true;
},
renderToggleGameActivityButton(props: { id?: string; name: string, exePath: string; }, nowPlaying: boolean) {
return (
<ErrorBoundary noop>
<div style={{ marginLeft: 12, zIndex: 0 }}>
{ToggleActivityComponent({ id: props.id ?? props.exePath, name: props.name, type: ActivitiesTypes.Game }, nowPlaying)}
</div>
</ErrorBoundary>
);
},
renderToggleActivityButton(props: { id: string; name: string; }) {
return (
<ErrorBoundary noop>
{ToggleActivityComponent({ id: props.id, name: props.name, type: ActivitiesTypes.Embedded })}
</ErrorBoundary>
);
}
});

View File

@ -37,6 +37,13 @@ export const settings = definePluginSettings({
default: true,
},
preventCarouselFromClosingOnClick: {
type: OptionType.BOOLEAN,
// Thanks chat gpt
description: "Allow the image modal in the image slideshow thing / carousel to remain open when clicking on the image",
default: true,
},
invertScroll: {
type: OptionType.BOOLEAN,
description: "Invert scroll",
@ -94,7 +101,7 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
/>
<Menu.MenuCheckboxItem
id="vc-nearest-neighbour"
label="Nearest Neighbour"
label="Nearset Neighbour"
checked={settings.store.nearestNeighbour}
action={() => {
settings.store.nearestNeighbour = !settings.store.nearestNeighbour;
@ -156,14 +163,10 @@ export default definePlugin({
patches: [
{
find: "Messages.OPEN_IN_BROWSER",
find: '"renderLinkComponent","maxWidth"',
replacement: {
// there are 2 image thingies. one for carosuel and one for the single image.
// so thats why i added global flag.
// also idk if this patch is good, should it be more specific?
// https://regex101.com/r/xfvNvV/1
match: /return.{1,200}\.wrapper.{1,200}src:\i,/g,
replace: `$&id: '${ELEMENT_ID}',`
match: /(return\(.{1,100}\(\)\.wrapper.{1,100})(src)/,
replace: `$1id: '${ELEMENT_ID}',$2`
}
},
@ -171,21 +174,29 @@ export default definePlugin({
find: "handleImageLoad=",
replacement: [
{
match: /showThumbhashPlaceholder:\i,/,
replace: "...$self.makeProps(this),$&"
match: /(render=function\(\){.{1,500}limitResponsiveWidth.{1,600})onMouseEnter:/,
replace: "$1...$self.makeProps(this),onMouseEnter:"
},
{
match: /componentDidMount\(\){/,
match: /componentDidMount=function\(\){/,
replace: "$&$self.renderMagnifier(this);",
},
{
match: /componentWillUnmount\(\){/,
match: /componentWillUnmount=function\(\){/,
replace: "$&$self.unMountMagnifier();"
}
]
},
{
find: ".carouselModal,",
replacement: {
match: /onClick:(\i),/,
replace: "onClick:$self.settings.store.preventCarouselFromClosingOnClick ? () => {} : $1,"
}
}
],
settings,

View File

@ -131,15 +131,15 @@ export default definePlugin({
// Indicator
find: ".Messages.MESSAGE_EDITED,",
replacement: {
match: /let\{className:\i,message:\i[^}]*\}=(\i)/,
match: /var .,.,.=(.)\.className,.=.\.message,.=.\.children,.=.\.content,.=.\.onUpdate/gm,
replace: "try {$1 && $self.INV_REGEX.test($1.message.content) ? $1.content.push($self.indicator()) : null } catch {};$&"
}
},
{
find: "ChannelTextAreaButtons",
find: ".activeCommandOption",
replacement: {
match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/,
replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()",
match: /(.)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/,
replace: "$&;try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}",
}
},
],

View File

@ -21,8 +21,8 @@ import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { ApplicationAssetUtils, FluxDispatcher, Forms } from "@webpack/common";
import { filters, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
import { FluxDispatcher, Forms } from "@webpack/common";
interface ActivityAssets {
large_image?: string;
@ -86,9 +86,15 @@ const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f";
const logger = new Logger("LastFMRichPresence");
const presenceStore = findByPropsLazy("getLocalPresence");
const assetManager = mapMangledModuleLazy(
"getAssetImage: size must === [number, number] for Twitch",
{
getAsset: filters.byCode("apply("),
}
);
async function getApplicationAsset(key: string): Promise<string> {
return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];
return (await assetManager.getAsset(applicationId, [key, undefined]))[0];
}
function setActivity(activity: Activity | null) {

View File

@ -16,9 +16,8 @@
* 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 definePlugin from "@utils/types";
// These are Xor encrypted to prevent you from spoiling yourself when you read the source code.
// don't worry about it :P
@ -61,35 +60,17 @@ const quotes = [
"Wdn`khc'|f*eghl{%"
];
const settings = definePluginSettings({
replaceEvents: {
description: "Replace Event Quotes too",
type: OptionType.BOOLEAN,
default: true
}
});
export default definePlugin({
name: "LoadingQuotes",
description: "Replace Discords loading quotes",
authors: [Devs.Ven, Devs.KraXen72],
settings,
patches: [
{
find: ".LOADING_DID_YOU_KNOW}",
replacement: [
{
match: /\._loadingText=function\(\)\{/,
replace: "$&return $self.quote;",
find: ".LOADING_DID_YOU_KNOW",
replacement: {
match: /\._loadingText=.+?random\(.+?;/s,
replace: "._loadingText=$self.quote;",
},
{
match: /\._eventLoadingText=function\(\)\{/,
replace: "$&return $self.quote;",
predicate: () => settings.store.replaceEvents
}
],
},
],

View File

@ -105,10 +105,10 @@ export default definePlugin({
authors: [Devs.Ven, Devs.Commandtechno],
patches: [{
find: "{isSidebarVisible:",
find: ".isSidebarVisible,",
replacement: {
match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/,
replace: ":[$1?.startsWith('members')?$self.render():null,$2"
match: /(var (\i)=\i\.className.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/,
replace: "$1:[$2?.startsWith('members')?$self.render():null,$3"
}
}],

View File

@ -18,6 +18,7 @@
import { addAccessory } from "@api/MessageAccessories";
import { definePluginSettings } from "@api/Settings";
import { getSettingStoreLazy } from "@api/SettingsStore";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants.js";
import { classes } from "@utils/misc";
@ -35,7 +36,6 @@ import {
PermissionStore,
RestAPI,
Text,
TextAndImagesSettingsStores,
UserStore
} from "@webpack/common";
import { Channel, Guild, Message } from "discord-types/general";
@ -46,11 +46,12 @@ const messageCache = new Map<string, {
}>();
const Embed = LazyComponent(() => findByCode(".inlineMediaEmbed"));
const AutoModEmbed = LazyComponent(() => findByCode(".withFooter]:", "childrenMessageContent:"));
const ChannelMessage = LazyComponent(() => find(m => m.type?.toString()?.includes("renderSimpleAccessories)")));
const ChannelMessage = LazyComponent(() => find(m => m.type?.toString()?.includes('["message","compact","className",')));
const SearchResultClasses = findByPropsLazy("message", "searchResult");
let AutoModEmbed: React.ComponentType<any> = () => null;
const messageLinkRegex = /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(\d{17,20}|@me)\/(\d{17,20})\/(\d{17,20})/g;
const tenorRegex = /^https:\/\/(?:www\.)?tenor\.com\//;
@ -318,9 +319,10 @@ function ChannelMessageEmbedAccessory({ message, channel, guildID }: MessageEmbe
/>;
}
const compactModeEnabled = getSettingStoreLazy<boolean>("textAndImages", "messageDisplayCompact")!;
function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
const { message, channel, guildID } = props;
const compact = TextAndImagesSettingsStores.MessageDisplayCompact.useSetting();
const isDM = guildID === "@me";
const images = getImages(message);
const { parse } = Parser;
@ -336,7 +338,7 @@ function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
<span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span>
</Text>
}
compact={compact}
compact={compactModeEnabled.getSetting()}
content={
<>
{message.content || message.attachments.length <= images.length
@ -363,7 +365,20 @@ export default definePlugin({
name: "MessageLinkEmbeds",
description: "Adds a preview to messages that link another message",
authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev],
dependencies: ["MessageAccessoriesAPI"],
dependencies: ["MessageAccessoriesAPI", "SettingsStoreAPI"],
patches: [
{
find: ".embedCard",
replacement: [{
match: /function (\i)\(\i\){var \i=\i\.message,\i=\i\.channel.{0,200}\.hideTimestamp/,
replace: "$self.AutoModEmbed=$1;$&"
}]
}
],
set AutoModEmbed(e: any) {
AutoModEmbed = e;
},
settings,

View File

@ -1,10 +1,10 @@
/* Message content highlighting */
.messagelogger-deleted [class*="contents"] > :is(div, h1, h2, h3, p) {
.messagelogger-deleted [class*="contents-"] > :is(div, h1, h2, h3, p) {
color: #f04747 !important;
}
/* Bot "thinking" text highlighting */
.messagelogger-deleted [class*="colorStandard"] {
.messagelogger-deleted [class*="colorStandard-"] {
color: #f04747 !important;
}

View File

@ -210,7 +210,7 @@ export default definePlugin({
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id);
},
// Based on canary 63b8f1b4f2025213c5cf62f0966625bee3d53136
// Based on canary 9ab8626bcebceaea6da570b9c586172d02b9c996
patches: [
{
// MessageStore
@ -219,7 +219,7 @@ export default definePlugin({
replacement: [
{
// Add deleted=true to all target messages in the MESSAGE_DELETE event
match: /MESSAGE_DELETE:function\((\i)\){let.+?((?:\i\.){2})getOrCreate.+?},/,
match: /MESSAGE_DELETE:function\((\w)\){var .+?((?:\w{1,2}\.){2})getOrCreate.+?},/,
replace:
"MESSAGE_DELETE:function($1){" +
" var cache = $2getOrCreate($1.channelId);" +
@ -229,7 +229,7 @@ export default definePlugin({
},
{
// Add deleted=true to all target messages in the MESSAGE_DELETE_BULK event
match: /MESSAGE_DELETE_BULK:function\((\i)\){let.+?((?:\i\.){2})getOrCreate.+?},/,
match: /MESSAGE_DELETE_BULK:function\((\w)\){var .+?((?:\w{1,2}\.){2})getOrCreate.+?},/,
replace:
"MESSAGE_DELETE_BULK:function($1){" +
" var cache = $2getOrCreate($1.channelId);" +
@ -239,7 +239,7 @@ export default definePlugin({
},
{
// Add current cached content + new edit time to cached message's editHistory
match: /(MESSAGE_UPDATE:function\((\i)\).+?)\.update\((\i)/,
match: /(MESSAGE_UPDATE:function\((\w)\).+?)\.update\((\w)/,
replace: "$1" +
".update($3,m =>" +
" (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message)) ? m :" +
@ -251,8 +251,8 @@ export default definePlugin({
},
{
// fix up key (edit last message) attempting to edit a deleted message
match: /(?<=getLastEditableMessage\(\i\)\{.{0,200}\.find\((\i)=>)/,
replace: "!$1.deleted &&"
match: /(?<=getLastEditableMessage=.{0,200}\.find\(\(function\((\i)\)\{)return/,
replace: "return !$1.deleted &&"
}
]
},
@ -260,13 +260,13 @@ export default definePlugin({
{
// Message domain model
// Module 451
find: "}addReaction(",
find: "isFirstMessageInForumPost=function",
replacement: [
{
match: /this\.customRenderedContent=(\i)\.customRenderedContent,/,
replace: "this.customRenderedContent = $1.customRenderedContent," +
"this.deleted = $1.deleted || false," +
"this.editHistory = $1.editHistory || [],"
match: /(\w)\.customRenderedContent=(\w)\.customRenderedContent;/,
replace: "$1.customRenderedContent = $2.customRenderedContent;" +
"$1.deleted = $2.deleted || false;" +
"$1.editHistory = $2.editHistory || [];"
}
]
},
@ -283,7 +283,7 @@ export default definePlugin({
// },
{
// Pass through editHistory & deleted & original attachments to the "edited message" transformer
match: /interactionData:(\i)\.interactionData/,
match: /interactionData:(\w)\.interactionData/,
replace:
"interactionData:$1.interactionData," +
"deleted:$1.deleted," +
@ -299,7 +299,7 @@ export default definePlugin({
{
// Construct new edited message and add editHistory & deleted (ref above)
// Pass in custom data to attachment parser to mark attachments deleted as well
match: /attachments:(\i)\((\i)\)/,
match: /attachments:(\w{1,2})\((\w)\)/,
replace:
"attachments: $1((() => {" +
" let old = arguments[1]?.attachments;" +
@ -315,7 +315,7 @@ export default definePlugin({
},
{
// Preserve deleted attribute on attachments
match: /(\((\i)\){return null==\2\.attachments.+?)spoiler:/,
match: /(\((\w)\){return null==\2\.attachments.+?)spoiler:/,
replace:
"$1deleted: arguments[0]?.deleted," +
"spoiler:"
@ -326,15 +326,15 @@ export default definePlugin({
{
// Attachment renderer
// Module 96063
find: ".removeAttachmentHoverButton",
find: "().removeAttachmentHoverButton",
replacement: [
{
match: /(className:\i,attachment:\i),/,
replace: "$1,attachment: {deleted},"
match: /((\w)\.className,\w=\2\.attachment),/,
replace: "$1,deleted=$2.attachment?.deleted,"
},
{
match: /\[\i\.obscured\]:.+?,/,
replace: "$& 'messagelogger-deleted-attachment': deleted,"
match: /\["className","attachment".+?className:/,
replace: "$& (deleted ? 'messagelogger-deleted-attachment ' : '') +"
}
]
},
@ -360,7 +360,7 @@ export default definePlugin({
{
// Render editHistory in the deepest div for message content
match: /(\)\("div",\{id:.+?children:\[)/,
replace: "$1 (arguments[0].message.editHistory?.length > 0 ? arguments[0].message.editHistory.map(edit => $self.renderEdit(edit)) : null), "
replace: "$1 (arguments[0].message.editHistory.length > 0 ? arguments[0].message.editHistory.map(edit => $self.renderEdit(edit)) : null), "
}
]
},
@ -371,11 +371,11 @@ export default definePlugin({
find: "displayName=\"ReferencedMessageStore\"",
replacement: [
{
match: /MESSAGE_DELETE:function\((\i)\).+?},/,
match: /MESSAGE_DELETE:function\((\w)\).+?},/,
replace: "MESSAGE_DELETE:function($1){},"
},
{
match: /MESSAGE_DELETE_BULK:function\((\i)\).+?},/,
match: /MESSAGE_DELETE_BULK:function\((\w)\).+?},/,
replace: "MESSAGE_DELETE_BULK:function($1){},"
}
]
@ -384,7 +384,7 @@ export default definePlugin({
{
// Message context base menu
// Module 600300
find: "useMessageMenu:",
find: "id:\"remove-reactions\"",
replacement: [
{
// Remove the first section if message is deleted

View File

@ -22,7 +22,7 @@ import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findLazy } from "@webpack";
import { Card, ChannelStore, Forms, GuildStore, PermissionsBits, Switch, TextInput, Tooltip, useState } from "@webpack/common";
import { Card, ChannelStore, Forms, GuildStore, Switch, TextInput, Tooltip, useState } from "@webpack/common";
import { RC } from "@webpack/types";
import { Channel, Message, User } from "discord-types/general";
@ -53,11 +53,14 @@ interface TagSettings {
[k: string]: TagSetting;
}
const CLYDE_ID = "1081004946872352958";
// PermissionStore.computePermissions is not the same function and doesn't work here
const PermissionUtil = findByPropsLazy("computePermissions", "canEveryoneRole") as {
computePermissions({ ...args }): bigint;
};
const Permissions = findByPropsLazy("SEND_MESSAGES", "VIEW_CREATOR_MONETIZATION_ANALYTICS") as Record<PermissionName, bigint>;
const Tag = findLazy(m => m.Types?.[0] === "BOT") as RC<{ type?: number, className?: string, useRemSizes?: boolean; }> & { Types: Record<string, number>; };
const isWebhook = (message: Message, user: User) => !!message?.webhookId && user.isNonUserBot();
@ -187,14 +190,17 @@ export default definePlugin({
patches: [
// add tags to the tag list
{
find: "BotTagTypes:",
replacement: {
match: /\((\i)=\{\}\)\)\[(\i)\.BOT/,
replace: "($1=$self.getTagTypes()))[$2.BOT"
find: '.BOT=0]="BOT"',
replacement: [
// add tags to the exported tags list (Tag.Types)
{
match: /(\i)\[.\.BOT=0\]="BOT";/,
replace: "$&$1=$self.addTagVariants($1);"
}
]
},
{
find: ".DISCORD_SYSTEM_MESSAGE_BOT_TAG_TOOLTIP,",
find: ".DISCORD_SYSTEM_MESSAGE_BOT_TAG_TOOLTIP;",
replacement: [
// make the tag show the right text
{
@ -209,25 +215,25 @@ export default definePlugin({
},
// add HTML data attributes (for easier theming)
{
match: /.botText,children:(\i)}\)]/,
replace: "$&,'data-tag':$1.toLowerCase()"
match: /children:\[(?=\i,\(0,\i\.jsx\)\("span",{className:\i\(\)\.botText,children:(\i)}\)\])/,
replace: "'data-tag':$1.toLowerCase(),children:["
}
],
},
// in messages
{
find: "renderSystemTag:",
find: ".Types.ORIGINAL_POSTER",
replacement: {
match: /;return\((\(null==\i\?void 0:\i\.isSystemDM\(\).+?.Types.ORIGINAL_POSTER\)),null==(\i)\)/,
replace: ";$1;$2=$self.getTag({...arguments[0],origType:$2,location:'chat'});return $2 == null"
match: /return null==(\i)\?null:\(0,/,
replace: "$1=$self.getTag({...arguments[0],origType:$1,location:'chat'});$&"
}
},
// in the member list
{
find: ".Messages.GUILD_OWNER,",
find: ".renderBot=function(){",
replacement: {
match: /(?<type>\i)=\(null==.{0,100}\.BOT;return null!=(?<user>\i)&&\i\.bot/,
replace: "$<type> = $self.getTag({user: $<user>, channel: arguments[0].channel, origType: $<user>.bot ? 0 : null, location: 'not-chat' }); return typeof $<type> === 'number'"
match: /\.BOT;return null!=(\i)&&.{0,10}\?(.{0,50})\.botTag,type:\i/,
replace: ".BOT;var type=$self.getTag({...this.props,origType:$1.bot?0:null,location:'not-chat'});return type!==null?$2.botTag,type"
}
},
// pass channel id down props to be used in profiles
@ -247,18 +253,11 @@ export default definePlugin({
},
// in profiles
{
find: ",overrideDiscriminator:",
replacement: [
{
// prevent channel id from getting ghosted
// it's either this or extremely long lookbehind
match: /user:\i,nick:\i,/,
replace: "$&moreTags_channelId,"
}, {
find: ",botType:",
replacement: {
match: /,botType:(\i\((\i)\)),/g,
replace: ",botType:$self.getTag({user:$2,channelId:moreTags_channelId,origType:$1,location:'not-chat'}),"
replace: ",botType:$self.getTag({user:$2,channelId:arguments[0].moreTags_channelId,origType:$1,location:'not-chat'}),"
}
]
},
],
@ -298,25 +297,24 @@ export default definePlugin({
if (!guild) return [];
const permissions = PermissionUtil.computePermissions({ user, context: guild, overwrites: channel.permissionOverwrites });
return Object.entries(PermissionsBits)
return Object.entries(Permissions)
.map(([perm, permInt]) =>
permissions & permInt ? perm : ""
)
.filter(Boolean);
},
getTagTypes() {
const obj = {};
addTagVariants(tagConstant) {
let i = 100;
tags.forEach(({ name }) => {
obj[name] = ++i;
obj[i] = name;
obj[`${name}-BOT`] = ++i;
obj[i] = `${name}-BOT`;
obj[`${name}-OP`] = ++i;
obj[i] = `${name}-OP`;
tagConstant[name] = ++i;
tagConstant[i] = name;
tagConstant[`${name}-BOT`] = ++i;
tagConstant[i] = `${name}-BOT`;
tagConstant[`${name}-OP`] = ++i;
tagConstant[i] = `${name}-OP`;
});
return obj;
return tagConstant;
},
isOPTag: (tag: number) => tag === Tag.Types.ORIGINAL_POSTER || tags.some(t => tag === Tag.Types[`${t.name}-OP`]),
@ -343,17 +341,15 @@ export default definePlugin({
message, user, channelId, origType, location, channel
}: {
message?: Message,
user: User & { isClyde(): boolean; },
user: User,
channel?: Channel & { isForumPost(): boolean; },
channelId?: string;
origType?: number;
location: "chat" | "not-chat";
}): number | null {
if (!user)
return null;
if (location === "chat" && user.id === "1")
return Tag.Types.OFFICIAL;
if (user.isClyde())
if (user.id === CLYDE_ID)
return Tag.Types.AI;
let type = typeof origType === "number" ? origType : null;
@ -381,6 +377,7 @@ export default definePlugin({
break;
}
}
return type;
}
});

View File

@ -45,16 +45,16 @@ export default definePlugin({
authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince],
patches: [
{
find: ",acceptInvite(",
find: ",acceptInvite:function",
replacement: {
match: /INVITE_ACCEPT_SUCCESS.+?,(\i)=null!==.+?;/,
match: /INVITE_ACCEPT_SUCCESS.+?;(\i)=null.+?;/,
replace: (m, guildId) => `${m}$self.handleMute(${guildId});`
}
},
{
find: "{joinGuild:",
find: "{joinGuild:function",
replacement: {
match: /guildId:(\i),lurker:(\i).{0,20}}\)\);/,
match: /guildId:(\w+),lurker:(\w+).{0,20}\)}\)\);/,
replace: (m, guildId, lurker) => `${m}if(!${lurker})$self.handleMute(${guildId});`
}
}

View File

@ -47,17 +47,16 @@ export default definePlugin({
patches: [
{
find: ".Messages.USER_PROFILE_MODAL", // Note: the module is lazy-loaded
replacement: {
replacement: [
{
match: /(?<=\.MUTUAL_GUILDS\}\),)(?=(\i\.bot).{0,20}(\(0,\i\.jsx\)\(.{0,100}id:))/,
replace: '($1||arguments[0].isCurrentUser)?null:$2"MUTUAL_GDMS",children:"Mutual Groups"}),'
}
replace: '$1?null:$2"MUTUAL_GDMS",children:"Mutual Groups"}),'
},
{
find: ".UserProfileSections.USER_INFO_CONNECTIONS:",
replacement: {
match: /(?<={user:(\i),onClose:(\i)}\);)(?=case \i\.\i\.MUTUAL_FRIENDS)/,
replace: "case \"MUTUAL_GDMS\":return $self.renderMutualGDMs($1,$2);"
}
]
}
],

View File

@ -29,11 +29,11 @@ export default definePlugin({
authors: [Devs.rushii, Devs.Samu],
patches: [
{
find: "Messages.BLOCKED_MESSAGES_HIDE",
find: 'safety_prompt:"DMSpamExperiment",response:"show_redacted_messages"',
replacement: [
{
match: /let\{[^}]*collapsedReason[^}]*\}/,
replace: "return null;$&"
match: /\.collapsedReason;return/,
replace: ".collapsedReason;return null;return;"
}
]
},

View File

@ -26,8 +26,8 @@ export default definePlugin({
patches: [{
find: "setDevtoolsCallbacks",
replacement: {
match: /if\(null!=\i&&"0.0.0"===\i\.remoteApp\.getVersion\(\)\)/,
replace: "if(true)"
match: /if\(.{0,10}\|\|"0.0.0"!==.{0,2}\.remoteApp\.getVersion\(\)\)/,
replace: "if(false)"
}
}]
});

View File

@ -1,43 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { disableStyle, enableStyle } from "@api/Styles";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import style from "./styles.css?managed";
export default definePlugin({
name: "NoMosaic",
authors: [Devs.AutumnVN],
description: "Removes Discord new image mosaic",
tags: ["image", "mosaic", "media"],
patches: [
{
find: ".oneByTwoLayoutThreeGrid",
replacement: [{
match: /mediaLayoutType:\i\.\i\.MOSAIC/,
replace: 'mediaLayoutType:"RESPONSIVE"'
},
{
match: /null!==\(\i=\i\.get\(\i\)\)&&void 0!==\i\?\i:"INVALID"/,
replace: '"INVALID"',
},]
},
{
find: "Messages.REMOVE_ATTACHMENT_TOOLTIP_TEXT",
replacement: {
match: /\i===\i\.\i\.MOSAIC/,
replace: "true"
}
}],
start() {
enableStyle(style);
},
stop() {
disableStyle(style);
}
});

View File

@ -1,3 +0,0 @@
[class^="nonMediaAttachmentsContainer_"] [class*="messageAttachment_"] {
position: relative;
}

View File

@ -55,18 +55,18 @@ export default definePlugin({
// or by searching for "showProgressBadge:"
patches: [
{
find: "getPendingCount(){",
find: ".getPendingCount=",
predicate: () => settings.store.hideFriendRequestsCount,
replacement: {
match: /(?<=getPendingCount\(\)\{)/,
match: /(?<=\.getPendingCount=function\(\)\{)/,
replace: "return 0;"
}
},
{
find: "getMessageRequestsCount(){",
find: ".getMessageRequestsCount=",
predicate: () => settings.store.hideMessageRequestsCount,
replacement: {
match: /(?<=getMessageRequestsCount\(\)\{)/,
match: /(?<=\.getMessageRequestsCount=function\(\)\{)/,
replace: "return 0;"
}
},
@ -84,10 +84,8 @@ export default definePlugin({
find: "showProgressBadge:",
predicate: () => settings.store.hidePremiumOffersCount,
replacement: {
// The two groups inside the first group grab the minified names of the variables,
// they are then referenced later to find unviewedTrialCount + unviewedDiscountCount.
match: /(?<=\{unviewedTrialCount:(\i),unviewedDiscountCount:(\i)\}.{0,200}\i=)\1\+\2/,
replace: "0"
match: /\(function\(\){return \i\.\i\.getUnacknowledgedOffers\(\i\)\.length}\)/,
replace: "(function(){return 0})"
}
}
],

View File

@ -34,19 +34,19 @@ export default definePlugin({
}
},
{
find: ".avatarPositionPremiumNoBanner,default:",
find: "().avatarPositionPremiumNoBanner,default:",
replacement: {
// premiumUserWithoutBanner: foo().avatarPositionPremiumNoBanner, default: foo().avatarPositionNormal
match: /\.avatarPositionPremiumNoBanner(?=,default:\i\.(\i))/,
match: /\.avatarPositionPremiumNoBanner(?=,default:\i\(\)\.(\i))/,
// premiumUserWithoutBanner: foo().avatarPositionNormal...
replace: ".$1"
}
},
{
find: "hasThemeColors(){",
find: ".hasThemeColors=function(){",
replacement: {
match: /get canUsePremiumProfileCustomization\(\){return /,
replace: "$&false &&"
match: /(?<=key:"canUsePremiumProfileCustomization",get:function\(\){return)/,
replace: " false;"
}
}
]

View File

@ -25,11 +25,14 @@ export default definePlugin({
authors: [Devs.Nuckyz],
patches: [
{
find: '"ApplicationStreamPreviewUploadManager"',
replacement: {
match: /await \i\.\i\.(makeChunkedRequest\(|post\(\{url:)\i\.\i\.STREAM_PREVIEW.+?\}\)/g,
replace: "0"
}
find: '("ApplicationStreamPreviewUploadManager")',
replacement: [
String.raw`\i\.\i\.makeChunkedRequest\(`,
String.raw`\i\.\i\.post\({url:`
].map(match => ({
match: new RegExp(String.raw`(?=return\[(\d),${match}\i\.\i\.STREAM_PREVIEW.+?}\)\];)`),
replace: (_, code) => `return[${code},Promise.resolve({body:"",status:204})];`
}))
}
]
});

View File

@ -1,21 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "NoTypingAnimation",
authors: [Devs.AutumnVN],
description: "Disables the CPU-intensive typing dots animation",
patches: [{
find: "dotCycle",
replacement: {
match: /document.hasFocus\(\)/,
replace: "false"
}
}]
});

View File

@ -33,7 +33,7 @@ export default definePlugin({
}
},
{
find: "renderJumpButton()",
find: "renderJumpButton=function()",
replacement: {
match: /if\(.{1,10}\)(.{1,10}\.show\({.{1,50}UNBLOCK_TO_JUMP_TITLE)/,
replace: "if(false)$1"

View File

@ -4,68 +4,36 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { ChannelStore, ReadStateStore, UserStore } from "@webpack/common";
import { MessageJSON } from "discord-types/general";
import definePlugin from "@utils/types";
import { ChannelStore, ReadStateStore } from "@webpack/common";
import { Message } from "discord-types/general";
const enum ChannelType {
DM = 1,
GROUP_DM = 3
}
const settings = definePluginSettings({
channelToAffect: {
type: OptionType.SELECT,
description: "Select the type of DM for the plugin to affect",
options: [
{ label: "Both", value: "both_dms", default: true },
{ label: "User DMs", value: "user_dm" },
{ label: "Group DMs", value: "group_dm" },
]
},
allowMentions: {
type: OptionType.BOOLEAN,
description: "Receive audio pings for @mentions",
default: false,
},
allowEveryone: {
type: OptionType.BOOLEAN,
description: "Receive audio pings for @everyone and @here in group DMs",
default: false,
},
});
export default definePlugin({
name: "OnePingPerDM",
description: "If unread messages are sent by a user in DMs multiple times, you'll only receive one audio ping. Read the messages to reset the limit",
authors: [Devs.ProffDea],
settings,
patches: [{
find: ".getDesktopType()===",
replacement: [{
match: /(\i\.\i\.getDesktopType\(\)===\i\.\i\.NEVER)\)/,
replace: "$&if(!$self.isPrivateChannelRead(arguments[0]?.message))return;else "
match: /if\((\i\.\i\.getDesktopType\(\)===\i\.\i\.NEVER)\){/,
replace: "if($1){if(!$self.isPrivateChannelRead(arguments[0]?.message))return;"
},
{
match: /sound:(\i\?\i:void 0,volume:\i,onClick)/,
match: /sound:(\i\?\i:void 0,volume:\i,onClick:)/,
replace: "sound:!$self.isPrivateChannelRead(arguments[0]?.message)?undefined:$1"
}]
}],
isPrivateChannelRead(message: MessageJSON) {
isPrivateChannelRead(message: Message) {
const channelType = ChannelStore.getChannel(message.channel_id)?.type;
if (channelType !== ChannelType.DM && channelType !== ChannelType.GROUP_DM) {
return false;
}
if (
(channelType === ChannelType.DM && settings.store.channelToAffect === "group_dm") ||
(channelType === ChannelType.GROUP_DM && settings.store.channelToAffect === "user_dm") ||
(settings.store.allowMentions && message.mentions.some(m => m.id === UserStore.getCurrentUser().id)) ||
(settings.store.allowEveryone && message.mention_everyone)
) {
return true;
}
return ReadStateStore.getOldestUnreadMessageId(message.channel_id) === message.id;
},
});

View File

@ -53,10 +53,10 @@ export default definePlugin({
patches: [
{
find: "trackAnnouncementMessageLinkClicked({",
find: '"MaskedLinkStore"',
replacement: {
match: /(?<=handleClick:function\(\)\{return (\i)\}.+?)async function \1\(.+?\)\{/,
replace: "$& if(await $self.handleLink(...arguments)) return;"
match: /return ((\i)\.apply\(this,arguments\))(?=\}function \i.{0,250}\.trusted)/,
replace: "return $self.handleLink(...arguments).then(handled => handled||$1)"
}
},
// Make Spotify profile activity links open in app on web
@ -71,7 +71,7 @@ export default definePlugin({
{
find: ".CONNECTED_ACCOUNT_VIEWED,",
replacement: {
match: /(?<=href:\i,onClick:\i=>\{)(?=.{0,10}\i=(\i)\.type,.{0,100}CONNECTED_ACCOUNT_VIEWED)/,
match: /(?<=href:\i,onClick:function\(\i\)\{)(?=\i=(\i)\.type,.{0,50}CONNECTED_ACCOUNT_VIEWED)/,
replace: "$self.handleAccountView(arguments[0],$1.type,$1.id);"
}
}

View File

@ -7,3 +7,8 @@ you don't want to do this") and onboarding requirements ("Making this change wil
This plugin will let you create permissions in servers that **WILL** lock you out of channels until an administrator
can resolve it for you. Please be careful with the overwrites you are making and check carefully.
## Community Server Channels
Community Server channels (i.e., `#rules` and `#moderator-only`) are actually mandatory and their existence is enforced
by the API, therefore this plugin cannot remove the restrictions behind them.

View File

@ -31,7 +31,7 @@ export default definePlugin({
patches: [
// Permission lockout, just set the check to true
{
find: ".showPermissionLockoutModal(",
find: "Messages.SELF_DENY_PERMISSION_BODY",
replacement: [
{
match: /case"DENY":.{0,50}if\((?=\i\.\i\.can)/,
@ -42,12 +42,22 @@ export default definePlugin({
},
// Onboarding, same thing but we need to prevent the check
{
find: ".ONBOARDING_CHANNEL_THRESHOLD_WARNING",
find: "Messages.ONBOARDING_CHANNEL_THRESHOLD_WARNING",
replacement: [
{
// are we java yet?
match: /(?<=(?:isDefaultChannelThresholdMetAfterDelete|checkDefaultChannelThresholdMetAfterChannelPermissionDeny):function\(\)\{)return \i(?=\})/g,
replace: "return () => true"
match: /case 1:if\((?=!\i\.sent.{20,30}Messages\.CANNOT_CHANGE_CHANNEL_PERMS)/,
replace: "$&false&&"
}
],
predicate: () => settings.store.onboarding
},
// Onboarding deletion
{
find: "Messages.DELETE_DEFAULT_CHANNEL_BODY",
replacement: [
{
match: /if\((?=null!=\i.{5,20}Messages.DELETE_DEFAULT_CHANNEL_BODY)/,
replace: "$&false&&"
}
],
predicate: () => settings.store.onboarding

View File

@ -161,7 +161,7 @@ export default definePlugin({
patches: [
{
find: ".popularApplicationCommandIds,",
find: ".Messages.BOT_PROFILE_SLASH_COMMANDS",
replacement: {
match: /showBorder:.{0,60}}\),(?<=guild:(\i),guildMember:(\i),.+?)/,
replace: (m, guild, guildMember) => `${m}$self.UserPermissions(${guild},${guildMember}),`

View File

@ -24,12 +24,13 @@ export default definePlugin({
description: "Adds picture in picture to videos (next to the Download button)",
authors: [Devs.Lumap],
settings,
patches: [
{
find: ".nonMediaAttachment]",
find: ".onRemoveAttachment,",
replacement: {
match: /\.nonMediaAttachment\].{0,10}children:\[\S{3}/,
replace: "$&&&$self.renderPiPButton(),"
match: /\.nonMediaAttachment,!(\i).{0,7}children:\[(\i),/,
replace: "$&$1&&$2&&$self.renderPiPButton(),"
},
},
],

View File

@ -1,8 +0,0 @@
.vc-pip-button {
color: var(--interactive-normal);
}
.vc-pip-button:hover {
background-color: var(--background-modifier-hover);
color: var(--interactive-hover);
}

View File

@ -66,7 +66,7 @@ export default definePlugin({
// filter Discord's privateChannelIds list to remove pins, and pass
// pinCount as prop. This needs to be here so that the entire DM list receives
// updates on pin/unpin
match: /(?<=\i,{channels:\i,)privateChannelIds:(\i),/,
match: /privateChannelIds:(\i),/,
replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c)),pinCount:$self.usePinCount($1),"
},
{
@ -75,39 +75,39 @@ export default definePlugin({
// - Section 1: buttons for pages like Friends & Library
// - Section 2: our pinned dms
// - Section 3: the normal dm list
match: /(?<=renderRow:this\.renderRow,)sections:\[\i,/,
match: /(?<=renderRow:(\i)\.renderRow,)sections:\[\i,/,
// For some reason, adding our sections when no private channels are ready yet
// makes DMs infinitely load. Thus usePinCount returns either a single element
// array with the count, or an empty array. Due to spreading, only in the former
// case will an element be added to the outer array
// Thanks for the fix, Strencher!
replace: "$&...this.props.pinCount??[],"
replace: "$&...$1.props.pinCount,"
},
{
// Patch renderSection (renders the header) to set the text to "Pinned DMs" instead of "Direct Messages"
// lookbehind is used to lookup parameter name. We could use arguments[0], but
// if children ever is wrapped in an iife, it will break
match: /children:(\i\.\i\.Messages.DIRECT_MESSAGES)(?<=renderSection=(\i)=>{.+?)/,
match: /children:(\i\.\i\.Messages.DIRECT_MESSAGES)(?<=renderSection=function\((\i)\).+?)/,
replace: "children:$2.section===1?'Pinned DMs':$1"
},
{
// Patch channel lookup inside renderDM
// channel=channels[channelIds[row]];
match: /(?<=renderDM=\((\i),(\i)\)=>{.*?this.state,\i=\i\[\i\],\i=)((\i)\[\i\]);/,
match: /(?<=preRenderedChildren,(\i)=)((\i)\[\i\[\i\]\]);/,
// section 1 is us, manually get our own channel
// section === 1 ? getChannel(channels, row) : channels[channelIds[row]];
replace: "$1===1?$self.getChannel($4,$2):$3;"
replace: "arguments[0]===1?$self.getChannel($3,arguments[1]):$2;"
},
{
// Fix getRowHeight's check for whether this is the DMs section
// section === DMS
match: /===\i\.DMS&&0/,
match: /===\i.DMS&&0/,
// section -1 === DMS
replace: "-1$&"
},
{
// Override scrollToChannel to properly account for pinned channels
match: /(?<=scrollTo\(\{to:\i\}\):\(\i\+=)(\d+)\*\(.+?(?=,)/,
match: /(?<=else\{\i\+=)(\i)\*\(.+?(?=;)/,
replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)"
}
]
@ -115,19 +115,19 @@ export default definePlugin({
// Fix Alt Up/Down navigation
{
find: ".Routes.APPLICATION_STORE&&",
find: '"mod+alt+right"',
replacement: {
// channelIds = __OVERLAY__ ? stuff : [...getStaticPaths(),...channelIds)]
match: /(?<=\i=__OVERLAY__\?\i:\[\.\.\.\i\(\),\.\.\.)\i/,
// channelIds = __OVERLAY__ ? stuff : toArray(getStaticPaths()).concat(toArray(channelIds))
match: /(?<=(\i)=__OVERLAY__\?\i:.{0,10})\.concat\((.{0,10})\)/,
// ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))
replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))"
replace: ".concat($self.getSnapshot()).concat($2.filter(c=>!$self.isPinned(c)))"
}
},
// fix alt+shift+up/down
{
find: ".getFlattenedGuildIds()],",
find: '"alt+shift+down"',
replacement: {
match: /(?<=\i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/,
match: /(?<=return \i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/,
replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))"
}
},

View File

@ -23,20 +23,20 @@ import { Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { findByCodeLazy, findStoreLazy } from "@webpack";
import { PresenceStore, Tooltip, UserStore } from "@webpack/common";
import { User } from "discord-types/general";
const SessionsStore = findStoreLazy("SessionsStore");
function Icon(path: string, opts?: { viewBox?: string; width?: number; height?: number; }) {
return ({ color, tooltip, small }: { color: string; tooltip: string; small: boolean; }) => (
return ({ color, tooltip }: { color: string; tooltip: string; }) => (
<Tooltip text={tooltip} >
{(tooltipProps: any) => (
<svg
{...tooltipProps}
height={(opts?.height ?? 20) - (small ? 3 : 0)}
width={(opts?.width ?? 20) - (small ? 3 : 0)}
height={opts?.height ?? 20}
width={opts?.width ?? 20}
viewBox={opts?.viewBox ?? "0 0 24 24"}
fill={color}
>
@ -55,18 +55,18 @@ const Icons = {
};
type Platform = keyof typeof Icons;
const StatusUtils = findByPropsLazy("getStatusColor", "StatusTypes");
const getStatusColor = findByCodeLazy(".TWITCH", ".STREAMING", ".INVISIBLE");
const PlatformIcon = ({ platform, status, small }: { platform: Platform, status: string; small: boolean; }) => {
const PlatformIcon = ({ platform, status }: { platform: Platform, status: string; }) => {
const tooltip = platform[0].toUpperCase() + platform.slice(1);
const Icon = Icons[platform] ?? Icons.desktop;
return <Icon color={`var(--${StatusUtils.getStatusColor(status)}`} tooltip={tooltip} small={small} />;
return <Icon color={`var(--${getStatusColor(status)}`} tooltip={tooltip} />;
};
const getStatus = (id: string): Record<Platform, string> => PresenceStore.getState()?.clientStatuses?.[id];
const PlatformIndicator = ({ user, wantMargin = true, wantTopMargin = false, small = false }: { user: User; wantMargin?: boolean; wantTopMargin?: boolean; small?: boolean; }) => {
const PlatformIndicator = ({ user, wantMargin = true, wantTopMargin = false }: { user: User; wantMargin?: boolean; wantTopMargin?: boolean; }) => {
if (!user || user.bot) return null;
if (user.id === UserStore.getCurrentUser().id) {
@ -99,7 +99,6 @@ const PlatformIndicator = ({ user, wantMargin = true, wantTopMargin = false, sma
key={platform}
platform={platform as Platform}
status={status}
small={small}
/>
));
@ -138,7 +137,7 @@ const indicatorLocations = {
description: "In the member list",
onEnable: () => addDecorator("platform-indicator", props =>
<ErrorBoundary noop>
<PlatformIndicator user={props.user} small={true} />
<PlatformIndicator user={props.user} />
</ErrorBoundary>
),
onDisable: () => removeDecorator("platform-indicator")
@ -198,13 +197,13 @@ export default definePlugin({
replacement: [
{
// Return the STATUS_ONLINE_MOBILE mask if the user is on mobile, no matter the status
match: /\.STATUS_TYPING;switch(?=.+?(if\(\i\)return \i\.\i\.Masks\.STATUS_ONLINE_MOBILE))/,
replace: ".STATUS_TYPING;$1;switch"
match: /(?<=return \i\.\i\.Masks\.STATUS_TYPING;)(.+?)(\i)\?(\i\.\i\.Masks\.STATUS_ONLINE_MOBILE):/,
replace: (_, rest, isMobile, mobileMask) => `if(${isMobile})return ${mobileMask};${rest}`
},
{
// Return the STATUS_ONLINE_MOBILE mask if the user is on mobile, no matter the status
match: /switch\(\i\)\{case \i\.\i\.ONLINE:(if\(\i\)return\{[^}]+\})/,
replace: "$1;$&"
match: /(switch\(\i\){case \i\.\i\.ONLINE:return )(\i)\?({.+?}):/,
replace: (_, rest, isMobile, component) => `if(${isMobile})return${component};${rest}`
}
]
},
@ -230,7 +229,7 @@ export default definePlugin({
]
},
{
find: "}isMobileOnline(",
find: "isMobileOnline=function",
predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator,
replacement: {
// Make isMobileOnline return true no matter what is the user status

View File

@ -129,10 +129,10 @@ export default definePlugin({
authors: [Devs.Aria],
patches: [
{
find: "ChannelTextAreaButtons",
find: ".activeCommandOption",
replacement: {
match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/,
replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()",
match: /(.)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/,
replace: "$&;try{$2||$1.unshift($self.previewIcon(arguments[0]))}catch{}",
}
},
],

View File

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { classes } from "@utils/misc";
import { findByPropsLazy } from "@webpack";
import { UserStore } from "@webpack/common";
@ -40,17 +39,17 @@ function shouldShow(message: Message): boolean {
return true;
}
export const PronounsChatComponentWrapper = ErrorBoundary.wrap(({ message }: { message: Message; }) => {
export function PronounsChatComponentWrapper({ message }: { message: Message; }) {
return shouldShow(message)
? <PronounsChatComponent message={message} />
: null;
}, { noop: true });
}
export const CompactPronounsChatComponentWrapper = ErrorBoundary.wrap(({ message }: { message: Message; }) => {
export function CompactPronounsChatComponentWrapper({ message }: { message: Message; }) {
return shouldShow(message)
? <CompactPronounsChatComponent message={message} />
: null;
}, { noop: true });
}
function PronounsChatComponent({ message }: { message: Message; }) {
const [result] = useFormattedPronouns(message.author.id);
@ -64,7 +63,7 @@ function PronounsChatComponent({ message }: { message: Message; }) {
: null;
}
export const CompactPronounsChatComponent = ErrorBoundary.wrap(({ message }: { message: Message; }) => {
export function CompactPronounsChatComponent({ message }: { message: Message; }) {
const [result] = useFormattedPronouns(message.author.id);
return result
@ -74,4 +73,4 @@ export const CompactPronounsChatComponent = ErrorBoundary.wrap(({ message }: { m
> {result}</span>
)
: null;
}, { noop: true });
}

View File

@ -41,7 +41,7 @@ export default definePlugin({
find: "showCommunicationDisabledStyles",
replacement: {
match: /("span",{id:\i,className:\i,children:\i}\))/,
replace: "$1, $self.CompactPronounsChatComponentWrapper(arguments[0])"
replace: "$1, $self.CompactPronounsChatComponentWrapper(e)"
}
},
// Patch the chat timestamp element (normal mode)
@ -49,7 +49,7 @@ export default definePlugin({
find: "showCommunicationDisabledStyles",
replacement: {
match: /(?<=return\s*\(0,\i\.jsxs?\)\(.+!\i&&)(\(0,\i.jsxs?\)\(.+?\{.+?\}\))/,
replace: "[$1, $self.PronounsChatComponentWrapper(arguments[0])]"
replace: "[$1, $self.PronounsChatComponentWrapper(e)]"
}
},
// Patch the profile popout username header to use our pronoun hook instead of Discord's pronouns
@ -57,18 +57,19 @@ export default definePlugin({
find: ".userTagNoNickname",
replacement: [
{
match: /{user:(\i),[^}]*,pronouns:(\i),[^}]*}=\i;/,
replace: "$&let vcPronounSource;[$2,vcPronounSource]=$self.useProfilePronouns($1.id);"
match: /,(\i)=(\i)\.pronouns/,
replace: ",[$1,vcPronounSource]=$self.useProfilePronouns($2.user.id)"
},
PRONOUN_TOOLTIP_PATCH
]
},
// Patch the profile modal username header to use our pronoun hook instead of Discord's pronouns
{
find: ".nameTagSmall)",
find: ".USER_PROFILE_ACTIVITY",
replacement: [
{
match: /\.getName\(\i\);(?<=displayProfile.{0,200})/,
/* FIXME: old name is getGlobalName, new name is getName. Remove optional Global once stable has also migrated */
match: /\.get(?:Global)?Name\(\i\);(?<=displayProfile.{0,200})/,
replace: "$&const [vcPronounce,vcPronounSource]=$self.useProfilePronouns(arguments[0].user.id,true);if(arguments[0].displayProfile&&vcPronounce)arguments[0].displayProfile.pronouns=vcPronounce;"
},
PRONOUN_TOOLTIP_PATCH

View File

@ -37,7 +37,7 @@ export async function onRelationshipRemove({ relationship: { type, id } }: Relat
return;
}
const user = await UserUtils.getUser(id)
const user = await UserUtils.fetchUser(id)
.catch(() => null);
if (!user) return;

View File

@ -31,23 +31,23 @@ export default definePlugin({
patches: [
{
find: "removeRelationship:(",
find: "removeRelationship:function(",
replacement: {
match: /(removeRelationship:\((\i),\i,\i\)=>)/,
replace: "$1($self.removeFriend($2),0)||"
match: /(removeRelationship:function\((\i),\i,\i\){)/,
replace: "$1$self.removeFriend($2);"
}
},
{
find: "async leaveGuild(",
find: "leaveGuild:function(",
replacement: {
match: /(leaveGuild\((\i)\){)/,
match: /(leaveGuild:function\((\i)\){)/,
replace: "$1$self.removeGuild($2);"
}
},
{
find: "},closePrivateChannel(",
find: "closePrivateChannel:function(",
replacement: {
match: /(closePrivateChannel\((\i)\){)/,
match: /(closePrivateChannel:function\((\i)\){)/,
replace: "$1$self.removeGroup($2);"
}
}

View File

@ -68,7 +68,7 @@ export async function syncAndRunChecks() {
for (const id of oldFriends.friends) {
if (friends.friends.includes(id)) continue;
const user = await UserUtils.getUser(id).catch(() => void 0);
const user = await UserUtils.fetchUser(id).catch(() => void 0);
if (user)
notify(
`You are no longer friends with ${getUniqueUsername(user)}.`,
@ -85,7 +85,7 @@ export async function syncAndRunChecks() {
[RelationshipType.FRIEND, RelationshipType.BLOCKED, RelationshipType.OUTGOING_REQUEST].includes(RelationshipStore.getRelationshipType(id))
) continue;
const user = await UserUtils.getUser(id).catch(() => void 0);
const user = await UserUtils.fetchUser(id).catch(() => void 0);
if (user)
notify(
`Friend request from ${getUniqueUsername(user)} has been revoked.`,

View File

@ -30,9 +30,9 @@ export default definePlugin({
patches: [
{
find: ".removeObscurity=",
find: ".removeObscurity=function",
replacement: {
match: /(?<=\.removeObscurity=(\i)=>{)/,
match: /(?<=\.removeObscurity=function\((\i)\){)/,
replace: (_, event) => `$self.reveal(${event});`
}
}

View File

@ -0,0 +1,61 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { DeleteIcon } from "@components/Icons";
import { classes } from "@utils/misc";
import { findByPropsLazy } from "@webpack";
import { Tooltip } from "@webpack/common";
const iconClasses = findByPropsLazy("button", "wrapper", "disabled", "separator");
export function DeleteButton({ onClick }: { onClick(): void; }) {
return (
<Tooltip text="Delete Review">
{props => (
<div
{...props}
className={classes(iconClasses.button, iconClasses.dangerous)}
onClick={onClick}
>
<DeleteIcon width="20" height="20" />
</div>
)}
</Tooltip>
);
}
export function ReportButton({ onClick }: { onClick(): void; }) {
return (
<Tooltip text="Report Review">
{props => (
<div
{...props}
className={iconClasses.button}
onClick={onClick}
>
<svg width="20" height="20" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M20,6.002H14V3.002C14,2.45 13.553,2.002 13,2.002H4C3.447,2.002 3,2.45 3,3.002V22.002H5V14.002H10.586L8.293,16.295C8.007,16.581 7.922,17.011 8.076,17.385C8.23,17.759 8.596,18.002 9,18.002H20C20.553,18.002 21,17.554 21,17.002V7.002C21,6.45 20.553,6.002 20,6.002Z"
/>
</svg>
</div>
)}
</Tooltip>
);
}

View File

@ -0,0 +1,46 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { MaskedLinkStore, Tooltip } from "@webpack/common";
import { Badge } from "../entities";
import { cl } from "../utils";
export default function ReviewBadge(badge: Badge) {
return (
<Tooltip
text={badge.name}>
{({ onMouseEnter, onMouseLeave }) => (
<img
className={cl("badge")}
width="22px"
height="22px"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
src={badge.icon}
alt={badge.description}
onClick={() =>
MaskedLinkStore.openUntrustedLink({
href: badge.redirectURL,
})
}
/>
)}
</Tooltip>
);
}

View File

@ -0,0 +1,147 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { openUserProfile } from "@utils/discord";
import { classes } from "@utils/misc";
import { LazyComponent } from "@utils/react";
import { filters, findBulk } from "@webpack";
import { Alerts, moment, Parser, Timestamp, UserStore } from "@webpack/common";
import { Review, ReviewType } from "../entities";
import { deleteReview, reportReview } from "../reviewDbApi";
import { settings } from "../settings";
import { canDeleteReview, cl, showToast } from "../utils";
import { DeleteButton, ReportButton } from "./MessageButton";
import ReviewBadge from "./ReviewBadge";
export default LazyComponent(() => {
// this is terrible, blame ven
const p = filters.byProps;
const [
{ cozyMessage, buttons, message, buttonsInner, groupStart },
{ container, isHeader },
{ avatar, clickable, username, wrapper, cozy },
buttonClasses,
botTag
] = findBulk(
p("cozyMessage"),
p("container", "isHeader"),
p("avatar", "zalgo"),
p("button", "wrapper", "selected"),
p("botTag", "botTagRegular")
);
const dateFormat = new Intl.DateTimeFormat();
return function ReviewComponent({ review, refetch }: { review: Review; refetch(): void; }) {
function openModal() {
openUserProfile(review.sender.discordID);
}
function delReview() {
Alerts.show({
title: "Are you sure?",
body: "Do you really want to delete this review?",
confirmText: "Delete",
cancelText: "Nevermind",
onConfirm: () => {
deleteReview(review.id).then(res => {
if (res.success) {
refetch();
}
showToast(res.message);
});
}
});
}
function reportRev() {
Alerts.show({
title: "Are you sure?",
body: "Do you really you want to report this review?",
confirmText: "Report",
cancelText: "Nevermind",
// confirmColor: "red", this just adds a class name and breaks the submit button guh
onConfirm: () => reportReview(review.id)
});
}
return (
<div className={classes(cozyMessage, wrapper, message, groupStart, cozy, cl("review"))} style={
{
marginLeft: "0px",
paddingLeft: "52px", // wth is this
paddingRight: "16px"
}
}>
<img
className={classes(avatar, clickable)}
onClick={openModal}
src={review.sender.profilePhoto || "/assets/1f0bfc0865d324c2587920a7d80c609b.png?size=128"}
style={{ left: "0px", zIndex: 0 }}
/>
<div style={{ display: "inline-flex", justifyContent: "center", alignItems: "center" }}>
<span
className={classes(clickable, username)}
style={{ color: "var(--channels-default)", fontSize: "14px" }}
onClick={() => openModal()}
>
{review.sender.username}
</span>
{review.type === ReviewType.System && (
<span
className={classes(botTag.botTagVerified, botTag.botTagRegular, botTag.botTag, botTag.px, botTag.rem)}
style={{ marginLeft: "4px" }}>
<span className={botTag.botText}>
System
</span>
</span>
)}
</div>
{review.sender.badges.map(badge => <ReviewBadge {...badge} />)}
{
!settings.store.hideTimestamps && review.type !== ReviewType.System && (
<Timestamp timestamp={moment(review.timestamp * 1000)} >
{dateFormat.format(review.timestamp * 1000)}
</Timestamp>)
}
<div className={cl("review-comment")}>
{Parser.parseGuildEventDescription(review.comment)}
</div>
{review.id !== 0 && (
<div className={classes(container, isHeader, buttons)} style={{
padding: "0px",
}}>
<div className={classes(buttonClasses.wrapper, buttonsInner)} >
<ReportButton onClick={reportRev} />
{canDeleteReview(review, UserStore.getCurrentUser().id) && (
<DeleteButton onClick={delReview} />
)}
</div>
</div>
)}
</div>
);
};
});

View File

@ -0,0 +1,104 @@
/*
* 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 ErrorBoundary from "@components/ErrorBoundary";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { useForceUpdater } from "@utils/react";
import { Paginator, Text, useRef, useState } from "@webpack/common";
import { Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
import { settings } from "../settings";
import { cl } from "../utils";
import ReviewComponent from "./ReviewComponent";
import ReviewsView, { ReviewsInputComponent } from "./ReviewsView";
function Modal({ modalProps, discordId, name }: { modalProps: any; discordId: string; name: string; }) {
const [data, setData] = useState<Response>();
const [signal, refetch] = useForceUpdater(true);
const [page, setPage] = useState(1);
const ref = useRef<HTMLDivElement>(null);
const reviewCount = data?.reviewCount;
const ownReview = data?.reviews.find(r => r.sender.discordID === settings.store.user?.discordID);
return (
<ErrorBoundary>
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<ModalHeader>
<Text variant="heading-lg/semibold" className={cl("modal-header")}>
{name}'s Reviews
{!!reviewCount && <span> ({reviewCount} Reviews)</span>}
</Text>
<ModalCloseButton onClick={modalProps.onClose} />
</ModalHeader>
<ModalContent scrollerRef={ref}>
<div className={cl("modal-reviews")}>
<ReviewsView
discordId={discordId}
name={name}
page={page}
refetchSignal={signal}
onFetchReviews={setData}
scrollToTop={() => ref.current?.scrollTo({ top: 0, behavior: "smooth" })}
hideOwnReview
/>
</div>
</ModalContent>
<ModalFooter className={cl("modal-footer")}>
<div>
{ownReview && (
<ReviewComponent
refetch={refetch}
review={ownReview}
/>
)}
<ReviewsInputComponent
isAuthor={ownReview != null}
discordId={discordId}
name={name}
refetch={refetch}
/>
{!!reviewCount && (
<Paginator
currentPage={page}
maxVisiblePages={5}
pageSize={REVIEWS_PER_PAGE}
totalCount={reviewCount}
onPageChange={setPage}
/>
)}
</div>
</ModalFooter>
</ModalRoot>
</ErrorBoundary>
);
}
export function openReviewsModal(discordId: string, name: string) {
openModal(props => (
<Modal
modalProps={props}
discordId={discordId}
name={name}
/>
));
}

Some files were not shown because too many files have changed in this diff Show More