mirror of
https://github.com/Vendicated/Vencord.git
synced 2025-02-24 15:35:11 +00:00
Merge branch 'Vendicated:main' into main
This commit is contained in:
commit
dcc5e9ee26
180 changed files with 4338 additions and 3767 deletions
|
@ -31,6 +31,7 @@ Before starting your plugin:
|
|||
- No FakeDeafen or FakeMute
|
||||
- No StereoMic
|
||||
- No plugins that simply hide or redesign ui elements. This can be done with CSS
|
||||
- No plugins that interact with specific Discord bots (official Discord apps like Youtube WatchTogether are okay)
|
||||
- No selfbots or API spam (animated status, message pruner, auto reply, nitro snipers, etc)
|
||||
- No untrusted third party APIs. Popular services like Google or GitHub are fine, but absolutely no self hosted ones
|
||||
- No plugins that require the user to enter their own API key
|
||||
|
|
|
@ -4,10 +4,9 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
import stylistic from "@stylistic/eslint-plugin";
|
||||
import pathAlias from "eslint-plugin-path-alias";
|
||||
import react from "eslint-plugin-react";
|
||||
import header from "eslint-plugin-simple-header";
|
||||
import simpleImportSort from "eslint-plugin-simple-import-sort";
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
|
@ -15,6 +14,22 @@ import tseslint from "typescript-eslint";
|
|||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist", "browser", "packages/vencord-types"] },
|
||||
{
|
||||
files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}", "eslint.config.mjs"],
|
||||
settings: {
|
||||
react: {
|
||||
version: "18"
|
||||
}
|
||||
},
|
||||
...react.configs.flat.recommended,
|
||||
rules: {
|
||||
...react.configs.flat.recommended.rules,
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/display-name": "off",
|
||||
"react/no-unescaped-entities": "off",
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}", "eslint.config.mjs"],
|
||||
plugins: {
|
||||
|
@ -23,7 +38,7 @@ export default tseslint.config(
|
|||
"@typescript-eslint": tseslint.plugin,
|
||||
"simple-import-sort": simpleImportSort,
|
||||
"unused-imports": unusedImports,
|
||||
"path-alias": pathAlias,
|
||||
"path-alias": pathAlias
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
|
@ -90,7 +105,13 @@ export default tseslint.config(
|
|||
"no-invalid-regexp": "error",
|
||||
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||
"no-duplicate-imports": "error",
|
||||
"dot-notation": "error",
|
||||
"@typescript-eslint/dot-notation": [
|
||||
"error",
|
||||
{
|
||||
"allowPrivateClassPropertyAccess": true,
|
||||
"allowProtectedClassPropertyAccess": true
|
||||
}
|
||||
],
|
||||
"no-useless-escape": [
|
||||
"error",
|
||||
{
|
||||
|
|
53
package.json
53
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.10.7",
|
||||
"version": "1.11.3",
|
||||
"description": "The cutest Discord client mod",
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
"bugs": {
|
||||
|
@ -41,48 +41,49 @@
|
|||
"@vap/shiki": "0.10.5",
|
||||
"fflate": "^0.8.2",
|
||||
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"nanoid": "^5.0.7",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"nanoid": "^5.0.9",
|
||||
"virtual-merge": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@stylistic/eslint-plugin": "^2.6.1",
|
||||
"@types/chrome": "^0.0.269",
|
||||
"@types/diff": "^5.2.1",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/node": "^22.0.3",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@stylistic/eslint-plugin": "^2.12.1",
|
||||
"@types/chrome": "^0.0.287",
|
||||
"@types/diff": "^6.0.0",
|
||||
"@types/lodash": "^4.17.14",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@types/yazl": "^2.4.5",
|
||||
"diff": "^5.2.0",
|
||||
"diff": "^7.0.0",
|
||||
"discord-types": "^1.3.26",
|
||||
"esbuild": "^0.15.18",
|
||||
"eslint": "^9.8.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-path-alias": "2.1.0",
|
||||
"eslint-plugin-simple-header": "^1.1.1",
|
||||
"eslint-plugin-react": "^7.37.3",
|
||||
"eslint-plugin-simple-header": "^1.2.1",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.0.1",
|
||||
"highlight.js": "10.7.3",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"highlight.js": "11.7.0",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"moment": "^2.30.1",
|
||||
"puppeteer-core": "^22.15.0",
|
||||
"moment": "^2.22.2",
|
||||
"puppeteer-core": "^23.11.1",
|
||||
"standalone-electron-types": "^1.0.0",
|
||||
"stylelint": "^16.8.1",
|
||||
"stylelint": "^16.12.0",
|
||||
"stylelint-config-standard": "^36.0.1",
|
||||
"ts-patch": "^3.2.1",
|
||||
"ts-pattern": "^5.3.1",
|
||||
"tsx": "^4.16.5",
|
||||
"type-fest": "^4.23.0",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"typescript-transform-paths": "^3.4.7",
|
||||
"ts-patch": "^3.3.0",
|
||||
"ts-pattern": "^5.6.0",
|
||||
"tsx": "^4.19.2",
|
||||
"type-fest": "^4.31.0",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.19.0",
|
||||
"typescript-transform-paths": "^3.5.3",
|
||||
"zip-local": "^0.3.5"
|
||||
},
|
||||
"packageManager": "pnpm@9.1.0",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"eslint@9.8.0": "patches/eslint@9.8.0.patch",
|
||||
"eslint@9.17.0": "patches/eslint@9.17.0.patch",
|
||||
"eslint-plugin-path-alias@2.1.0": "patches/eslint-plugin-path-alias@2.1.0.patch"
|
||||
},
|
||||
"peerDependencyRules": {
|
||||
|
|
3259
pnpm-lock.yaml
generated
3259
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -37,7 +37,8 @@ const CANARY = process.env.USE_CANARY === "true";
|
|||
|
||||
const browser = await pup.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.CHROMIUM_BIN
|
||||
executablePath: process.env.CHROMIUM_BIN,
|
||||
args: ["--no-sandbox"]
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
|
|
@ -57,7 +57,7 @@ const Badges = new Set<ProfileBadge>();
|
|||
* Register a new badge with the Badges API
|
||||
* @param badge The badge to register
|
||||
*/
|
||||
export function addBadge(badge: ProfileBadge) {
|
||||
export function addProfileBadge(badge: ProfileBadge) {
|
||||
badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true });
|
||||
Badges.add(badge);
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ export function addBadge(badge: ProfileBadge) {
|
|||
* Unregister a badge from the Badges API
|
||||
* @param badge The badge to remove
|
||||
*/
|
||||
export function removeBadge(badge: ProfileBadge) {
|
||||
export function removeProfileBadge(badge: ProfileBadge) {
|
||||
return Badges.delete(badge);
|
||||
}
|
||||
|
||||
|
@ -100,20 +100,3 @@ export interface BadgeUserArgs {
|
|||
userId: string;
|
||||
guildId: string;
|
||||
}
|
||||
|
||||
interface ConnectedAccount {
|
||||
type: string;
|
||||
id: string;
|
||||
name: string;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
interface Profile {
|
||||
connectedAccounts: ConnectedAccount[];
|
||||
premiumType: number;
|
||||
premiumSince: string;
|
||||
premiumGuildSince?: any;
|
||||
lastFetched: number;
|
||||
profileFetchFailed: boolean;
|
||||
application?: any;
|
||||
}
|
||||
|
|
|
@ -9,9 +9,9 @@ import "./ChatButton.css";
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { waitFor } from "@webpack";
|
||||
import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common";
|
||||
import { Button, ButtonWrapperClasses, Tooltip } from "@webpack/common";
|
||||
import { Channel } from "discord-types/general";
|
||||
import { HTMLProps, MouseEventHandler, ReactNode } from "react";
|
||||
import { HTMLProps, JSX, MouseEventHandler, ReactNode } from "react";
|
||||
|
||||
let ChannelTextAreaClasses: Record<"button" | "buttonContainer", string>;
|
||||
waitFor(["buttonContainer", "channelTextArea"], m => ChannelTextAreaClasses = m);
|
||||
|
@ -74,9 +74,9 @@ export interface ChatBarProps {
|
|||
};
|
||||
}
|
||||
|
||||
export type ChatBarButton = (props: ChatBarProps & { isMainChat: boolean; }) => JSX.Element | null;
|
||||
export type ChatBarButtonFactory = (props: ChatBarProps & { isMainChat: boolean; }) => JSX.Element | null;
|
||||
|
||||
const buttonFactories = new Map<string, ChatBarButton>();
|
||||
const buttonFactories = new Map<string, ChatBarButtonFactory>();
|
||||
const logger = new Logger("ChatButtons");
|
||||
|
||||
export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) {
|
||||
|
@ -91,7 +91,7 @@ export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) {
|
|||
}
|
||||
}
|
||||
|
||||
export const addChatBarButton = (id: string, button: ChatBarButton) => buttonFactories.set(id, button);
|
||||
export const addChatBarButton = (id: string, button: ChatBarButtonFactory) => buttonFactories.set(id, button);
|
||||
export const removeChatBarButton = (id: string) => buttonFactories.delete(id);
|
||||
|
||||
export interface ChatBarButtonProps {
|
||||
|
@ -99,7 +99,8 @@ export interface ChatBarButtonProps {
|
|||
tooltip: string;
|
||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||
onContextMenu?: MouseEventHandler<HTMLButtonElement>;
|
||||
buttonProps?: Omit<HTMLProps<HTMLButtonElement>, "size" | "onClick" | "onContextMenu">;
|
||||
onAuxClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
buttonProps?: Omit<HTMLProps<HTMLButtonElement>, "size" | "onClick" | "onContextMenu" | "onAuxClick">;
|
||||
}
|
||||
export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
|
||||
return (
|
||||
|
@ -109,12 +110,13 @@ export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
|
|||
<Button
|
||||
aria-label={props.tooltip}
|
||||
size=""
|
||||
look={ButtonLooks.BLANK}
|
||||
look={Button.Looks.BLANK}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
innerClassName={`${ButtonWrapperClasses.button} ${ChannelTextAreaClasses?.button}`}
|
||||
onClick={props.onClick}
|
||||
onContextMenu={props.onContextMenu}
|
||||
onAuxClick={props.onAuxClick}
|
||||
{...props.buttonProps}
|
||||
>
|
||||
<div className={ButtonWrapperClasses.buttonWrapper}>
|
||||
|
|
|
@ -54,5 +54,5 @@ export function sendBotMessage(channelId: string, message: PartialDeep<Message>)
|
|||
export function findOption<T>(args: Argument[], name: string): T & {} | undefined;
|
||||
export function findOption<T>(args: Argument[], name: string, fallbackValue: T): T & {};
|
||||
export function findOption(args: Argument[], name: string, fallbackValue?: any) {
|
||||
return (args.find(a => a.name === name)?.value || fallbackValue) as any;
|
||||
return (args.find(a => a.name === name)?.value ?? fallbackValue) as any;
|
||||
}
|
||||
|
|
|
@ -110,6 +110,7 @@ function registerSubCommands(cmd: Command, plugin: string) {
|
|||
const subCmd = {
|
||||
...cmd,
|
||||
...o,
|
||||
options: o.options !== undefined ? o.options : undefined,
|
||||
type: ApplicationCommandType.CHAT_INPUT,
|
||||
name: `${cmd.name} ${o.name}`,
|
||||
id: `${o.name}-${cmd.id}`,
|
||||
|
|
|
@ -24,13 +24,13 @@ import type { ReactElement } from "react";
|
|||
* @param children The rendered context menu elements
|
||||
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
|
||||
*/
|
||||
export type NavContextMenuPatchCallback = (children: Array<ReactElement | null>, ...args: Array<any>) => void;
|
||||
export type NavContextMenuPatchCallback = (children: Array<ReactElement<any> | null>, ...args: Array<any>) => void;
|
||||
/**
|
||||
* @param navId The navId of the context menu being patched
|
||||
* @param children The rendered context menu elements
|
||||
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
|
||||
*/
|
||||
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<ReactElement | null>, ...args: Array<any>) => void;
|
||||
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<ReactElement<any> | null>, ...args: Array<any>) => void;
|
||||
|
||||
const ContextMenuLogger = new Logger("ContextMenu");
|
||||
|
||||
|
@ -70,7 +70,7 @@ export function addGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback)
|
|||
* @returns Whether the patch was successfully removed from the context menu(s)
|
||||
*/
|
||||
export function removeContextMenuPatch<T extends string | Array<string>>(navId: T, patch: NavContextMenuPatchCallback): T extends string ? boolean : Array<boolean> {
|
||||
const navIds = Array.isArray(navId) ? navId : [navId as string];
|
||||
const navIds: string[] = Array.isArray(navId) ? navId : [navId];
|
||||
|
||||
const results = navIds.map(id => navPatches.get(id)?.delete(patch) ?? false);
|
||||
|
||||
|
@ -92,7 +92,7 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba
|
|||
* @param children The context menu children
|
||||
* @param matchSubstring Whether to check if the id is a substring of the child id
|
||||
*/
|
||||
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null | undefined>, matchSubstring = false): Array<ReactElement | null | undefined> | null {
|
||||
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement<any> | null | undefined>, matchSubstring = false): Array<ReactElement<any> | null | undefined> | null {
|
||||
for (const child of children) {
|
||||
if (child == null) continue;
|
||||
|
||||
|
@ -122,9 +122,9 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra
|
|||
}
|
||||
|
||||
interface ContextMenuProps {
|
||||
contextMenuApiArguments?: Array<any>;
|
||||
contextMenuAPIArguments?: Array<any>;
|
||||
navId: string;
|
||||
children: Array<ReactElement | null>;
|
||||
children: Array<ReactElement<any> | null>;
|
||||
"aria-label": string;
|
||||
onSelect: (() => void) | undefined;
|
||||
onClose: (callback: (...args: Array<any>) => any) => void;
|
||||
|
@ -136,7 +136,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
|
|||
children: cloneMenuChildren(props.children),
|
||||
};
|
||||
|
||||
props.contextMenuApiArguments ??= [];
|
||||
props.contextMenuAPIArguments ??= [];
|
||||
const contextMenuPatches = navPatches.get(props.navId);
|
||||
|
||||
if (!Array.isArray(props.children)) props.children = [props.children];
|
||||
|
@ -144,7 +144,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
|
|||
if (contextMenuPatches) {
|
||||
for (const patch of contextMenuPatches) {
|
||||
try {
|
||||
patch(props.children, ...props.contextMenuApiArguments);
|
||||
patch(props.children, ...props.contextMenuAPIArguments);
|
||||
} catch (err) {
|
||||
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
|
||||
}
|
||||
|
@ -153,7 +153,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
|
|||
|
||||
for (const patch of globalPatches) {
|
||||
try {
|
||||
patch(props.navId, props.children, ...props.contextMenuApiArguments);
|
||||
patch(props.navId, props.children, ...props.contextMenuAPIArguments);
|
||||
} catch (err) {
|
||||
ContextMenuLogger.error("Global patch errored,", err);
|
||||
}
|
||||
|
@ -162,7 +162,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
|
|||
return props;
|
||||
}
|
||||
|
||||
function cloneMenuChildren(obj: ReactElement | Array<ReactElement | null> | null) {
|
||||
function cloneMenuChildren(obj: ReactElement<any> | Array<ReactElement<any> | null> | null) {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(cloneMenuChildren);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,9 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Channel, User } from "discord-types/general/index.js";
|
||||
import { JSX } from "react";
|
||||
|
||||
interface DecoratorProps {
|
||||
activities: any[];
|
||||
|
@ -38,27 +40,32 @@ interface DecoratorProps {
|
|||
user: User;
|
||||
[key: string]: any;
|
||||
}
|
||||
export type Decorator = (props: DecoratorProps) => JSX.Element | null;
|
||||
export type MemberListDecoratorFactory = (props: DecoratorProps) => JSX.Element | null;
|
||||
type OnlyIn = "guilds" | "dms";
|
||||
|
||||
export const decorators = new Map<string, { decorator: Decorator, onlyIn?: OnlyIn; }>();
|
||||
export const decorators = new Map<string, { render: MemberListDecoratorFactory, onlyIn?: OnlyIn; }>();
|
||||
|
||||
export function addDecorator(identifier: string, decorator: Decorator, onlyIn?: OnlyIn) {
|
||||
decorators.set(identifier, { decorator, onlyIn });
|
||||
export function addMemberListDecorator(identifier: string, render: MemberListDecoratorFactory, onlyIn?: OnlyIn) {
|
||||
decorators.set(identifier, { render, onlyIn });
|
||||
}
|
||||
|
||||
export function removeDecorator(identifier: string) {
|
||||
export function removeMemberListDecorator(identifier: string) {
|
||||
decorators.delete(identifier);
|
||||
}
|
||||
|
||||
export function __getDecorators(props: DecoratorProps): (JSX.Element | null)[] {
|
||||
const isInGuild = !!(props.guildId);
|
||||
return Array.from(decorators.values(), decoratorObj => {
|
||||
const { decorator, onlyIn } = decoratorObj;
|
||||
// this can most likely be done cleaner
|
||||
if (!onlyIn || (onlyIn === "guilds" && isInGuild) || (onlyIn === "dms" && !isInGuild)) {
|
||||
return decorator(props);
|
||||
}
|
||||
return Array.from(
|
||||
decorators.entries(),
|
||||
([key, { render: Decorator, onlyIn }]) => {
|
||||
if ((onlyIn === "guilds" && !isInGuild) || (onlyIn === "dms" && isInGuild))
|
||||
return null;
|
||||
});
|
||||
|
||||
return (
|
||||
<ErrorBoundary noop key={key} message={`Failed to render ${key} Member List Decorator`}>
|
||||
<Decorator {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
|
@ -16,26 +16,29 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export type AccessoryCallback = (props: Record<string, any>) => JSX.Element | null | Array<JSX.Element | null>;
|
||||
export type Accessory = {
|
||||
callback: AccessoryCallback;
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { JSX, ReactNode } from "react";
|
||||
|
||||
export type MessageAccessoryFactory = (props: Record<string, any>) => ReactNode;
|
||||
export type MessageAccessory = {
|
||||
render: MessageAccessoryFactory;
|
||||
position?: number;
|
||||
};
|
||||
|
||||
export const accessories = new Map<String, Accessory>();
|
||||
export const accessories = new Map<string, MessageAccessory>();
|
||||
|
||||
export function addAccessory(
|
||||
export function addMessageAccessory(
|
||||
identifier: string,
|
||||
callback: AccessoryCallback,
|
||||
render: MessageAccessoryFactory,
|
||||
position?: number
|
||||
) {
|
||||
accessories.set(identifier, {
|
||||
callback,
|
||||
render,
|
||||
position,
|
||||
});
|
||||
}
|
||||
|
||||
export function removeAccessory(identifier: string) {
|
||||
export function removeMessageAccessory(identifier: string) {
|
||||
accessories.delete(identifier);
|
||||
}
|
||||
|
||||
|
@ -43,15 +46,12 @@ export function _modifyAccessories(
|
|||
elements: JSX.Element[],
|
||||
props: Record<string, any>
|
||||
) {
|
||||
for (const accessory of accessories.values()) {
|
||||
let accessories = accessory.callback(props);
|
||||
if (accessories == null)
|
||||
continue;
|
||||
|
||||
if (!Array.isArray(accessories))
|
||||
accessories = [accessories];
|
||||
else if (accessories.length === 0)
|
||||
continue;
|
||||
for (const [key, accessory] of accessories.entries()) {
|
||||
const res = (
|
||||
<ErrorBoundary message={`Failed to render ${key} Message Accessory`} key={key}>
|
||||
<accessory.render {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
elements.splice(
|
||||
accessory.position != null
|
||||
|
@ -60,7 +60,7 @@ export function _modifyAccessories(
|
|||
: accessory.position
|
||||
: elements.length,
|
||||
0,
|
||||
...accessories.filter(e => e != null) as JSX.Element[]
|
||||
res
|
||||
);
|
||||
}
|
||||
|
|
@ -16,9 +16,11 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Channel, Message } from "discord-types/general/index.js";
|
||||
import { JSX } from "react";
|
||||
|
||||
interface DecorationProps {
|
||||
export interface MessageDecorationProps {
|
||||
author: {
|
||||
/**
|
||||
* Will be username if the user has no nickname
|
||||
|
@ -44,20 +46,25 @@ interface DecorationProps {
|
|||
message: Message;
|
||||
[key: string]: any;
|
||||
}
|
||||
export type Decoration = (props: DecorationProps) => JSX.Element | null;
|
||||
export type MessageDecorationFactory = (props: MessageDecorationProps) => JSX.Element | null;
|
||||
|
||||
export const decorations = new Map<string, Decoration>();
|
||||
export const decorations = new Map<string, MessageDecorationFactory>();
|
||||
|
||||
export function addDecoration(identifier: string, decoration: Decoration) {
|
||||
export function addMessageDecoration(identifier: string, decoration: MessageDecorationFactory) {
|
||||
decorations.set(identifier, decoration);
|
||||
}
|
||||
|
||||
export function removeDecoration(identifier: string) {
|
||||
export function removeMessageDecoration(identifier: string) {
|
||||
decorations.delete(identifier);
|
||||
}
|
||||
|
||||
export function __addDecorationsToMessage(props: DecorationProps): (JSX.Element | null)[] {
|
||||
return [...decorations.values()].map(decoration => {
|
||||
return decoration(props);
|
||||
});
|
||||
export function __addDecorationsToMessage(props: MessageDecorationProps): (JSX.Element | null)[] {
|
||||
return Array.from(
|
||||
decorations.entries(),
|
||||
([key, Decoration]) => (
|
||||
<ErrorBoundary noop message={`Failed to render ${key} Message Decoration`} key={key}>
|
||||
<Decoration {...props} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
);
|
||||
}
|
|
@ -73,11 +73,11 @@ export interface MessageExtra {
|
|||
openWarningPopout: (props: any) => any;
|
||||
}
|
||||
|
||||
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
|
||||
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void | { cancel: boolean; }>;
|
||||
export type MessageSendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
|
||||
export type MessageEditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void | { cancel: boolean; }>;
|
||||
|
||||
const sendListeners = new Set<SendListener>();
|
||||
const editListeners = new Set<EditListener>();
|
||||
const sendListeners = new Set<MessageSendListener>();
|
||||
const editListeners = new Set<MessageEditListener>();
|
||||
|
||||
export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra, replyOptions: MessageReplyOptions) {
|
||||
extra.replyOptions = replyOptions;
|
||||
|
@ -111,29 +111,29 @@ export async function _handlePreEdit(channelId: string, messageId: string, messa
|
|||
/**
|
||||
* Note: This event fires off before a message is sent, allowing you to edit the message.
|
||||
*/
|
||||
export function addPreSendListener(listener: SendListener) {
|
||||
export function addMessagePreSendListener(listener: MessageSendListener) {
|
||||
sendListeners.add(listener);
|
||||
return listener;
|
||||
}
|
||||
/**
|
||||
* Note: This event fires off before a message's edit is applied, allowing you to further edit the message.
|
||||
*/
|
||||
export function addPreEditListener(listener: EditListener) {
|
||||
export function addMessagePreEditListener(listener: MessageEditListener) {
|
||||
editListeners.add(listener);
|
||||
return listener;
|
||||
}
|
||||
export function removePreSendListener(listener: SendListener) {
|
||||
export function removeMessagePreSendListener(listener: MessageSendListener) {
|
||||
return sendListeners.delete(listener);
|
||||
}
|
||||
export function removePreEditListener(listener: EditListener) {
|
||||
export function removeMessagePreEditListener(listener: MessageEditListener) {
|
||||
return editListeners.delete(listener);
|
||||
}
|
||||
|
||||
|
||||
// Message clicks
|
||||
type ClickListener = (message: Message, channel: Channel, event: MouseEvent) => void;
|
||||
export type MessageClickListener = (message: Message, channel: Channel, event: MouseEvent) => void;
|
||||
|
||||
const listeners = new Set<ClickListener>();
|
||||
const listeners = new Set<MessageClickListener>();
|
||||
|
||||
export function _handleClick(message: Message, channel: Channel, event: MouseEvent) {
|
||||
// message object may be outdated, so (try to) fetch latest one
|
||||
|
@ -147,11 +147,11 @@ export function _handleClick(message: Message, channel: Channel, event: MouseEve
|
|||
}
|
||||
}
|
||||
|
||||
export function addClickListener(listener: ClickListener) {
|
||||
export function addMessageClickListener(listener: MessageClickListener) {
|
||||
listeners.add(listener);
|
||||
return listener;
|
||||
}
|
||||
|
||||
export function removeClickListener(listener: ClickListener) {
|
||||
export function removeMessageClickListener(listener: MessageClickListener) {
|
||||
return listeners.delete(listener);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ import type { ComponentType, MouseEventHandler } from "react";
|
|||
|
||||
const logger = new Logger("MessagePopover");
|
||||
|
||||
export interface ButtonItem {
|
||||
export interface MessagePopoverButtonItem {
|
||||
key?: string,
|
||||
label: string,
|
||||
icon: ComponentType<any>,
|
||||
|
@ -33,23 +33,23 @@ export interface ButtonItem {
|
|||
onContextMenu?: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
export type getButtonItem = (message: Message) => ButtonItem | null;
|
||||
export type MessagePopoverButtonFactory = (message: Message) => MessagePopoverButtonItem | null;
|
||||
|
||||
export const buttons = new Map<string, getButtonItem>();
|
||||
export const buttons = new Map<string, MessagePopoverButtonFactory>();
|
||||
|
||||
export function addButton(
|
||||
export function addMessagePopoverButton(
|
||||
identifier: string,
|
||||
item: getButtonItem,
|
||||
item: MessagePopoverButtonFactory,
|
||||
) {
|
||||
buttons.set(identifier, item);
|
||||
}
|
||||
|
||||
export function removeButton(identifier: string) {
|
||||
export function removeMessagePopoverButton(identifier: string) {
|
||||
buttons.delete(identifier);
|
||||
}
|
||||
|
||||
export function _buildPopoverElements(
|
||||
Component: React.ComponentType<ButtonItem>,
|
||||
Component: React.ComponentType<MessagePopoverButtonItem>,
|
||||
message: Message
|
||||
) {
|
||||
const items: React.ReactNode[] = [];
|
||||
|
|
|
@ -16,40 +16,36 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
|
||||
const logger = new Logger("ServerListAPI");
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { ComponentType } from "react";
|
||||
|
||||
export const enum ServerListRenderPosition {
|
||||
Above,
|
||||
In,
|
||||
}
|
||||
|
||||
const renderFunctionsAbove = new Set<Function>();
|
||||
const renderFunctionsIn = new Set<Function>();
|
||||
const componentsAbove = new Set<ComponentType>();
|
||||
const componentsBelow = new Set<ComponentType>();
|
||||
|
||||
function getRenderFunctions(position: ServerListRenderPosition) {
|
||||
return position === ServerListRenderPosition.Above ? renderFunctionsAbove : renderFunctionsIn;
|
||||
return position === ServerListRenderPosition.Above ? componentsAbove : componentsBelow;
|
||||
}
|
||||
|
||||
export function addServerListElement(position: ServerListRenderPosition, renderFunction: Function) {
|
||||
export function addServerListElement(position: ServerListRenderPosition, renderFunction: ComponentType) {
|
||||
getRenderFunctions(position).add(renderFunction);
|
||||
}
|
||||
|
||||
export function removeServerListElement(position: ServerListRenderPosition, renderFunction: Function) {
|
||||
export function removeServerListElement(position: ServerListRenderPosition, renderFunction: ComponentType) {
|
||||
getRenderFunctions(position).delete(renderFunction);
|
||||
}
|
||||
|
||||
export const renderAll = (position: ServerListRenderPosition) => {
|
||||
const ret: Array<JSX.Element> = [];
|
||||
|
||||
for (const renderFunction of getRenderFunctions(position)) {
|
||||
try {
|
||||
ret.unshift(renderFunction());
|
||||
} catch (e) {
|
||||
logger.error("Failed to render server list element:", e);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
return Array.from(
|
||||
getRenderFunctions(position),
|
||||
(Component, i) => (
|
||||
<ErrorBoundary noop key={i}>
|
||||
<Component />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
);
|
||||
};
|
|
@ -23,7 +23,7 @@ import { Logger } from "@utils/Logger";
|
|||
import { mergeDefaults } from "@utils/mergeDefaults";
|
||||
import { putCloudSettings } from "@utils/settingsSync";
|
||||
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
||||
import { React } from "@webpack/common";
|
||||
import { React, useEffect } from "@webpack/common";
|
||||
|
||||
import plugins from "~plugins";
|
||||
|
||||
|
@ -192,7 +192,7 @@ export const Settings = SettingsStore.store;
|
|||
export function useSettings(paths?: UseSettings<Settings>[]) {
|
||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (paths) {
|
||||
paths.forEach(p => SettingsStore.addChangeListener(p, forceUpdate));
|
||||
return () => paths.forEach(p => SettingsStore.removeChangeListener(p, forceUpdate));
|
||||
|
@ -200,7 +200,7 @@ export function useSettings(paths?: UseSettings<Settings>[]) {
|
|||
SettingsStore.addGlobalChangeListener(forceUpdate);
|
||||
return () => SettingsStore.removeGlobalChangeListener(forceUpdate);
|
||||
}
|
||||
}, []);
|
||||
}, [paths]);
|
||||
|
||||
return SettingsStore.store;
|
||||
}
|
||||
|
@ -220,6 +220,17 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
|||
}
|
||||
}
|
||||
|
||||
export function migratePluginSetting(pluginName: string, oldSetting: string, newSetting: string) {
|
||||
const settings = SettingsStore.plain.plugins[pluginName];
|
||||
if (!settings) return;
|
||||
|
||||
if (!Object.hasOwn(settings, oldSetting) || Object.hasOwn(settings, newSetting)) return;
|
||||
|
||||
settings[newSetting] = settings[oldSetting];
|
||||
delete settings[oldSetting];
|
||||
SettingsStore.markAsChanged();
|
||||
}
|
||||
|
||||
export function definePluginSettings<
|
||||
Def extends SettingsDefinition,
|
||||
Checks extends SettingsChecks<Def>,
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export function Badge({ text, color }): JSX.Element {
|
||||
export function Badge({ text, color }) {
|
||||
return (
|
||||
<div className="vc-plugins-badge" style={{
|
||||
backgroundColor: color,
|
||||
|
|
|
@ -27,7 +27,7 @@ interface Props<T = any> {
|
|||
/** Render nothing if an error occurs */
|
||||
noop?: boolean;
|
||||
/** Fallback component to render if an error occurs */
|
||||
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
|
||||
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; wrappedProps: T; }>>;
|
||||
/** called when an error occurs. The props property is only available if using .wrap */
|
||||
onError?(data: { error: Error, errorInfo: React.ErrorInfo, props: T; }): void;
|
||||
/** Custom error message */
|
||||
|
@ -70,8 +70,7 @@ const ErrorBoundary = LazyComponent(() => {
|
|||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
this.props.onError?.({ error, errorInfo, props: this.props.wrappedProps });
|
||||
logger.error("A component threw an Error\n", error);
|
||||
logger.error("Component Stack", errorInfo.componentStack);
|
||||
logger.error(`${this.props.message || "A component threw an Error"}\n`, error, errorInfo.componentStack);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -80,10 +79,14 @@ const ErrorBoundary = LazyComponent(() => {
|
|||
if (this.props.noop) return null;
|
||||
|
||||
if (this.props.fallback)
|
||||
return <this.props.fallback
|
||||
children={this.props.children}
|
||||
return (
|
||||
<this.props.fallback
|
||||
wrappedProps={this.props.wrappedProps}
|
||||
{...this.state}
|
||||
/>;
|
||||
>
|
||||
{this.props.children}
|
||||
</this.props.fallback>
|
||||
);
|
||||
|
||||
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
|
||||
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
.vc-expandableheader-center-flex {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.vc-expandableheader-btn {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./ExpandableHeader.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Text, Tooltip, useState } from "@webpack/common";
|
||||
|
||||
const cl = classNameFactory("vc-expandableheader-");
|
||||
|
||||
export interface ExpandableHeaderProps {
|
||||
onMoreClick?: () => void;
|
||||
moreTooltipText?: string;
|
||||
onDropDownClick?: (state: boolean) => void;
|
||||
defaultState?: boolean;
|
||||
headerText: string;
|
||||
children: React.ReactNode;
|
||||
buttons?: React.ReactNode[];
|
||||
forceOpen?: boolean;
|
||||
}
|
||||
|
||||
export function ExpandableHeader({
|
||||
children,
|
||||
onMoreClick,
|
||||
buttons,
|
||||
moreTooltipText,
|
||||
onDropDownClick,
|
||||
headerText,
|
||||
defaultState = false,
|
||||
forceOpen = false,
|
||||
}: ExpandableHeaderProps) {
|
||||
const [showContent, setShowContent] = useState(defaultState || forceOpen);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "8px"
|
||||
}}>
|
||||
<Text
|
||||
tag="h2"
|
||||
variant="eyebrow"
|
||||
style={{
|
||||
color: "var(--header-primary)",
|
||||
display: "inline"
|
||||
}}
|
||||
>
|
||||
{headerText}
|
||||
</Text>
|
||||
|
||||
<div className={cl("center-flex")}>
|
||||
{
|
||||
buttons ?? null
|
||||
}
|
||||
|
||||
{
|
||||
onMoreClick && // only show more button if callback is provided
|
||||
<Tooltip text={moreTooltipText}>
|
||||
{tooltipProps => (
|
||||
<button
|
||||
{...tooltipProps}
|
||||
className={cl("btn")}
|
||||
onClick={onMoreClick}>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path fill="var(--text-normal)" d="M7 12.001C7 10.8964 6.10457 10.001 5 10.001C3.89543 10.001 3 10.8964 3 12.001C3 13.1055 3.89543 14.001 5 14.001C6.10457 14.001 7 13.1055 7 12.001ZM14 12.001C14 10.8964 13.1046 10.001 12 10.001C10.8954 10.001 10 10.8964 10 12.001C10 13.1055 10.8954 14.001 12 14.001C13.1046 14.001 14 13.1055 14 12.001ZM19 10.001C20.1046 10.001 21 10.8964 21 12.001C21 13.1055 20.1046 14.001 19 14.001C17.8954 14.001 17 13.1055 17 12.001C17 10.8964 17.8954 10.001 19 10.001Z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
|
||||
<Tooltip text={showContent ? "Hide " + headerText : "Show " + headerText}>
|
||||
{tooltipProps => (
|
||||
<button
|
||||
{...tooltipProps}
|
||||
className={cl("btn")}
|
||||
onClick={() => {
|
||||
setShowContent(v => !v);
|
||||
onDropDownClick?.(showContent);
|
||||
}}
|
||||
disabled={forceOpen}
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
transform={showContent ? "scale(1 -1)" : "scale(1 1)"}
|
||||
>
|
||||
<path fill="var(--text-normal)" d="M16.59 8.59003L12 13.17L7.41 8.59003L6 10L12 16L18 10L16.59 8.59003Z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{showContent && children}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { CSSProperties } from "react";
|
||||
import { CSSProperties, JSX } from "react";
|
||||
|
||||
interface Props {
|
||||
columns: number;
|
||||
|
|
|
@ -27,7 +27,7 @@ export function Heart() {
|
|||
>
|
||||
<path
|
||||
fill="#db61a2"
|
||||
fill-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"
|
||||
/>
|
||||
</svg>
|
||||
|
|
|
@ -18,9 +18,9 @@
|
|||
|
||||
import "./iconStyles.css";
|
||||
|
||||
import { getIntlMessage, getTheme, Theme } from "@utils/discord";
|
||||
import { getIntlMessage } from "@utils/discord";
|
||||
import { classes } from "@utils/misc";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import type { JSX, PropsWithChildren } from "react";
|
||||
|
||||
interface BaseIconProps extends IconProps {
|
||||
viewBox: string;
|
||||
|
@ -55,7 +55,7 @@ export function LinkIcon({ height = 24, width = 24, className }: IconProps) {
|
|||
className={classes(className, "vc-link-icon")}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path fill="currentColor" d="M10.59 13.41c.41.39.41 1.03 0 1.42-.39.39-1.03.39-1.42 0a5.003 5.003 0 0 1 0-7.07l3.54-3.54a5.003 5.003 0 0 1 7.07 0 5.003 5.003 0 0 1 0 7.07l-1.49 1.49c.01-.82-.12-1.64-.4-2.42l.47-.48a2.982 2.982 0 0 0 0-4.24 2.982 2.982 0 0 0-4.24 0l-3.53 3.53a2.982 2.982 0 0 0 0 4.24zm2.82-4.24c.39-.39 1.03-.39 1.42 0a5.003 5.003 0 0 1 0 7.07l-3.54 3.54a5.003 5.003 0 0 1-7.07 0 5.003 5.003 0 0 1 0-7.07l1.49-1.49c-.01.82.12 1.64.4 2.43l-.47.47a2.982 2.982 0 0 0 0 4.24 2.982 2.982 0 0 0 4.24 0l3.53-3.53a2.982 2.982 0 0 0 0-4.24.973.973 0 0 1 0-1.42z" />
|
||||
<rect width={width} height={height} />
|
||||
</g>
|
||||
|
@ -122,8 +122,8 @@ export function InfoIcon(props: IconProps) {
|
|||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
transform="translate(2 2)"
|
||||
d="M9,7 L11,7 L11,5 L9,5 L9,7 Z M10,18 C5.59,18 2,14.41 2,10 C2,5.59 5.59,2 10,2 C14.41,2 18,5.59 18,10 C18,14.41 14.41,18 10,18 L10,18 Z M10,4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16,4.4771525 0,10 C-1.33226763e-15,12.6521649 1.0535684,15.195704 2.92893219,17.0710678 C4.80429597,18.9464316 7.3478351,20 10,20 C12.6521649,20 15.195704,18.9464316 17.0710678,17.0710678 C18.9464316,15.195704 20,12.6521649 20,10 C20,7.3478351 18.9464316,4.80429597 17.0710678,2.92893219 C15.195704,1.0535684 12.6521649,2.22044605e-16 10,0 L10,4.4408921e-16 Z M9,15 L11,15 L11,9 L9,9 L9,15 L9,15 Z"
|
||||
fillRule="evenodd"
|
||||
d="M23 12a11 11 0 1 1-22 0 11 11 0 0 1 22 0Zm-9.5-4.75a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm-.77 3.96a1 1 0 1 0-1.96-.42l-1.04 4.86a2.77 2.77 0 0 0 4.31 2.83l.24-.17a1 1 0 1 0-1.16-1.62l-.24.17a.77.77 0 0 1-1.2-.79l1.05-4.86Z" clipRule="evenodd"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
|
@ -211,9 +211,10 @@ export function CogWheel(props: IconProps) {
|
|||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
fill="currentColor"
|
||||
d="M19.738 10H22V14H19.739C19.498 14.931 19.1 15.798 18.565 16.564L20 18L18 20L16.565 18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069 19.498 8.203 19.099 7.436 18.564L6 20L4 18L5.436 16.564C4.901 15.799 4.502 14.932 4.262 14H2V10H4.262C4.502 9.068 4.9 8.202 5.436 7.436L4 6L6 4L7.436 5.436C8.202 4.9 9.068 4.502 10 4.262V2H14V4.261C14.932 4.502 15.797 4.9 16.565 5.435L18 3.999L20 5.999L18.564 7.436C19.099 8.202 19.498 9.069 19.738 10ZM12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16Z"
|
||||
fillRule="evenodd"
|
||||
d="M10.56 1.1c-.46.05-.7.53-.64.98.18 1.16-.19 2.2-.98 2.53-.8.33-1.79-.15-2.49-1.1-.27-.36-.78-.52-1.14-.24-.77.59-1.45 1.27-2.04 2.04-.28.36-.12.87.24 1.14.96.7 1.43 1.7 1.1 2.49-.33.8-1.37 1.16-2.53.98-.45-.07-.93.18-.99.64a11.1 11.1 0 0 0 0 2.88c.06.46.54.7.99.64 1.16-.18 2.2.19 2.53.98.33.8-.14 1.79-1.1 2.49-.36.27-.52.78-.24 1.14.59.77 1.27 1.45 2.04 2.04.36.28.87.12 1.14-.24.7-.95 1.7-1.43 2.49-1.1.8.33 1.16 1.37.98 2.53-.07.45.18.93.64.99a11.1 11.1 0 0 0 2.88 0c.46-.06.7-.54.64-.99-.18-1.16.19-2.2.98-2.53.8-.33 1.79.14 2.49 1.1.27.36.78.52 1.14.24.77-.59 1.45-1.27 2.04-2.04.28-.36.12-.87-.24-1.14-.96-.7-1.43-1.7-1.1-2.49.33-.8 1.37-1.16 2.53-.98.45.07.93-.18.99-.64a11.1 11.1 0 0 0 0-2.88c-.06-.46-.54-.7-.99-.64-1.16.18-2.2-.19-2.53-.98-.33-.8.14-1.79 1.1-2.49.36-.27.52-.78.24-1.14a11.07 11.07 0 0 0-2.04-2.04c-.36-.28-.87-.12-1.14.24-.7.96-1.7 1.43-2.49 1.1-.8-.33-1.16-1.37-.98-2.53.07-.45-.18-.93-.64-.99a11.1 11.1 0 0 0-2.88 0ZM16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
|
@ -261,7 +262,7 @@ export function PlusIcon(props: IconProps) {
|
|||
viewBox="0 0 18 18"
|
||||
>
|
||||
<polygon
|
||||
fill-rule="nonzero"
|
||||
fillRule="nonzero"
|
||||
fill="currentColor"
|
||||
points="15 10 10 10 10 15 8 15 8 10 3 10 3 8 8 8 8 3 10 3 10 8 15 8"
|
||||
/>
|
||||
|
@ -406,23 +407,30 @@ export function PencilIcon(props: IconProps) {
|
|||
);
|
||||
}
|
||||
|
||||
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
|
||||
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
|
||||
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
|
||||
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
|
||||
|
||||
export function GithubIcon(props: ImageProps) {
|
||||
const src = getTheme() === Theme.Light
|
||||
? GithubIconLight
|
||||
: GithubIconDark;
|
||||
|
||||
return <img {...props} src={src} />;
|
||||
export function GithubIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="-3 -3 30 30"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.385.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.757-1.333-1.757-1.09-.745.083-.73.083-.73 1.205.084 1.84 1.237 1.84 1.237 1.07 1.835 2.807 1.305 3.492.998.108-.775.42-1.305.763-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.467-2.38 1.235-3.22-.123-.303-.535-1.523.117-3.176 0 0 1.008-.322 3.3 1.23.957-.266 1.98-.398 3-.403 1.02.005 2.043.137 3 .403 2.29-1.552 3.297-1.23 3.297-1.23.653 1.653.24 2.873.118 3.176.77.84 1.233 1.91 1.233 3.22 0 4.61-2.803 5.625-5.475 5.92.43.37.823 1.102.823 2.222v3.293c0 .32.218.694.825.577C20.565 21.797 24 17.298 24 12c0-6.63-5.37-12-12-12z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function WebsiteIcon(props: ImageProps) {
|
||||
const src = getTheme() === Theme.Light
|
||||
? WebsiteIconLight
|
||||
: WebsiteIconDark;
|
||||
|
||||
return <img {...props} src={src} />;
|
||||
export function WebsiteIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M12 2C6.486 2 2 6.486 2 12s4.486 10 10 10 10-4.486 10-10S17.514 2 12 2zM4 12c0-.899.156-1.762.431-2.569L6 11l2 2v2l2 2 1 1v1.931C7.061 19.436 4 16.072 4 12zm14.33 4.873C17.677 16.347 16.687 16 16 16v-1a2 2 0 0 0-2-2h-4v-3a2 2 0 0 0 2-2V7h1a2 2 0 0 0 2-2v-.411C17.928 5.778 20 8.65 20 12a7.947 7.947 0 0 1-1.67 4.873z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ function ContributorModal({ user }: { user: User; }) {
|
|||
useEffect(() => {
|
||||
if (!profile && !user.bot && user.id)
|
||||
fetchUserProfile(user.id);
|
||||
}, [user.id]);
|
||||
}, [user.id, user.bot, profile]);
|
||||
|
||||
const githubName = profile?.connectedAccounts?.find(a => a.type === "github")?.name;
|
||||
const website = profile?.connectedAccounts?.find(a => a.type === "domain")?.name;
|
||||
|
|
|
@ -6,16 +6,19 @@
|
|||
|
||||
import "./LinkIconButton.css";
|
||||
|
||||
import { getTheme, Theme } from "@utils/discord";
|
||||
import { MaskedLink, Tooltip } from "@webpack/common";
|
||||
|
||||
import { GithubIcon, WebsiteIcon } from "..";
|
||||
|
||||
export function GithubLinkIcon() {
|
||||
return <GithubIcon aria-hidden className={"vc-settings-modal-link-icon"} />;
|
||||
const theme = getTheme() === Theme.Light ? "#000000" : "#FFFFFF";
|
||||
return <GithubIcon aria-hidden fill={theme} className={"vc-settings-modal-link-icon"} />;
|
||||
}
|
||||
|
||||
export function WebsiteLinkIcon() {
|
||||
return <WebsiteIcon aria-hidden className={"vc-settings-modal-link-icon"} />;
|
||||
const theme = getTheme() === Theme.Light ? "#000000" : "#FFFFFF";
|
||||
return <WebsiteIcon aria-hidden fill={theme} className={"vc-settings-modal-link-icon"} />;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -37,6 +37,7 @@ import { Constructor } from "type-fest";
|
|||
import { PluginMeta } from "~plugins";
|
||||
|
||||
import {
|
||||
ISettingCustomElementProps,
|
||||
ISettingElementProps,
|
||||
SettingBooleanComponent,
|
||||
SettingCustomComponent,
|
||||
|
@ -74,14 +75,15 @@ function makeDummyUser(user: { username: string; id?: string; avatar?: string; }
|
|||
return newUser;
|
||||
}
|
||||
|
||||
const Components: Record<OptionType, React.ComponentType<ISettingElementProps<any>>> = {
|
||||
const Components: Record<OptionType, React.ComponentType<ISettingElementProps<any> | ISettingCustomElementProps<any>>> = {
|
||||
[OptionType.STRING]: SettingTextComponent,
|
||||
[OptionType.NUMBER]: SettingNumericComponent,
|
||||
[OptionType.BIGINT]: SettingNumericComponent,
|
||||
[OptionType.BOOLEAN]: SettingBooleanComponent,
|
||||
[OptionType.SELECT]: SettingSelectComponent,
|
||||
[OptionType.SLIDER]: SettingSliderComponent,
|
||||
[OptionType.COMPONENT]: SettingCustomComponent
|
||||
[OptionType.COMPONENT]: SettingCustomComponent,
|
||||
[OptionType.CUSTOM]: () => null,
|
||||
};
|
||||
|
||||
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
|
||||
|
@ -109,7 +111,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
setAuthors(a => [...a, author]);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
}, [plugin.authors]);
|
||||
|
||||
async function saveAndClose() {
|
||||
if (!plugin.options) {
|
||||
|
@ -129,7 +131,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
for (const [key, value] of Object.entries(tempSettings)) {
|
||||
const option = plugin.options[key];
|
||||
pluginSettings[key] = value;
|
||||
option?.onChange?.(value);
|
||||
|
||||
if (option.type === OptionType.CUSTOM) continue;
|
||||
if (option?.restartNeeded) restartNeeded = true;
|
||||
}
|
||||
if (restartNeeded) onRestartNeeded();
|
||||
|
@ -141,7 +144,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
|
||||
} else {
|
||||
const options = Object.entries(plugin.options).map(([key, setting]) => {
|
||||
if (setting.hidden) return null;
|
||||
if (setting.type === OptionType.CUSTOM || setting.hidden) return null;
|
||||
|
||||
function onChange(newValue: any) {
|
||||
setTempSettings(s => ({ ...s, [key]: newValue }));
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
|
||||
import { PluginOptionComponent } from "@utils/types";
|
||||
|
||||
import { ISettingElementProps } from ".";
|
||||
import { ISettingCustomElementProps } from ".";
|
||||
|
||||
export function SettingCustomComponent({ option, onChange, onError }: ISettingElementProps<PluginOptionComponent>) {
|
||||
export function SettingCustomComponent({ option, onChange, onError }: ISettingCustomElementProps<PluginOptionComponent>) {
|
||||
return option.component({ setValue: onChange, setError: onError, option });
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import { DefinedSettings, PluginOptionBase } from "@utils/types";
|
||||
|
||||
export interface ISettingElementProps<T extends PluginOptionBase> {
|
||||
interface ISettingElementPropsBase<T> {
|
||||
option: T;
|
||||
onChange(newValue: any): void;
|
||||
pluginSettings: {
|
||||
|
@ -30,6 +30,9 @@ export interface ISettingElementProps<T extends PluginOptionBase> {
|
|||
definedSettings?: DefinedSettings;
|
||||
}
|
||||
|
||||
export type ISettingElementProps<T extends PluginOptionBase> = ISettingElementPropsBase<T>;
|
||||
export type ISettingCustomElementProps<T extends Omit<PluginOptionBase, "description" | "placeholder">> = ISettingElementPropsBase<T>;
|
||||
|
||||
export * from "../../Badge";
|
||||
export * from "./SettingBooleanComponent";
|
||||
export * from "./SettingCustomComponent";
|
||||
|
|
|
@ -35,6 +35,7 @@ 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, useMemo } from "@webpack/common";
|
||||
import { JSX } from "react";
|
||||
|
||||
import Plugins, { ExcludedPlugins } from "~plugins";
|
||||
|
||||
|
@ -387,7 +388,7 @@ function makeDependencyList(deps: string[]) {
|
|||
return (
|
||||
<React.Fragment>
|
||||
<Forms.FormText>This plugin is required by:</Forms.FormText>
|
||||
{deps.map((dep: string) => <Forms.FormText className={cl("dep-text")}>{dep}</Forms.FormText>)}
|
||||
{deps.map((dep: string) => <Forms.FormText key={dep} className={cl("dep-text")}>{dep}</Forms.FormText>)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -111,9 +111,9 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
|
|||
}
|
||||
|
||||
function renderDiff() {
|
||||
return diff?.map(p => {
|
||||
return diff?.map((p, idx) => {
|
||||
const color = p.added ? "lime" : p.removed ? "red" : "grey";
|
||||
return <div style={{ color, userSelect: "text", wordBreak: "break-all", lineBreak: "anywhere" }}>{p.value}</div>;
|
||||
return <div key={idx} style={{ color, userSelect: "text", wordBreak: "break-all", lineBreak: "anywhere" }}>{p.value}</div>;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>
|
|||
title: "Oops!",
|
||||
body: (
|
||||
<ErrorCard>
|
||||
{err.split("\n").map(line => <div>{Parser.parse(line)}</div>)}
|
||||
{err.split("\n").map((line, idx) => <div key={idx}>{Parser.parse(line)}</div>)}
|
||||
</ErrorCard>
|
||||
)
|
||||
});
|
||||
|
@ -87,7 +87,7 @@ function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof
|
|||
return (
|
||||
<Card style={{ padding: "0 0.5em" }}>
|
||||
{updates.map(({ hash, author, message }) => (
|
||||
<div style={{
|
||||
<div key={hash} style={{
|
||||
marginTop: "0.5em",
|
||||
marginBottom: "0.5em"
|
||||
}}>
|
||||
|
|
|
@ -10,7 +10,6 @@ export * from "./CodeBlock";
|
|||
export * from "./DonateButton";
|
||||
export { default as ErrorBoundary } from "./ErrorBoundary";
|
||||
export * from "./ErrorCard";
|
||||
export * from "./ExpandableHeader";
|
||||
export * from "./Flex";
|
||||
export * from "./Heart";
|
||||
export * from "./Icons";
|
||||
|
|
|
@ -62,14 +62,21 @@ async function runReporter() {
|
|||
if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw new Error("Webpack Find Fail");
|
||||
} catch (e) {
|
||||
let logMessage = searchType;
|
||||
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
|
||||
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
|
||||
else if (method === "mapMangledModule") {
|
||||
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
|
||||
if (args[0].$$vencordProps != null) {
|
||||
logMessage += `(${args[0].$$vencordProps.map(arg => `"${arg}"`).join(", ")})`;
|
||||
} else {
|
||||
logMessage += `(${args[0].toString().slice(0, 147)}...)`;
|
||||
}
|
||||
} else if (method === "extractAndLoadChunks") {
|
||||
logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
|
||||
} else if (method === "mapMangledModule") {
|
||||
const failedMappings = Object.keys(args[1]).filter(key => result?.[key] == null);
|
||||
|
||||
logMessage += `("${args[0]}", {\n${failedMappings.map(mapping => `\t${mapping}: ${args[1][mapping].toString().slice(0, 147)}...`).join(",\n")}\n})`;
|
||||
} else {
|
||||
logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
|
||||
}
|
||||
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
|
||||
|
||||
ReporterLogger.log("Webpack Find Fail:", logMessage);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
import { onceDefined } from "@shared/onceDefined";
|
||||
import electron, { app, BrowserWindowConstructorOptions, Menu, nativeTheme } from "electron";
|
||||
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
|
||||
import { dirname, join } from "path";
|
||||
|
||||
import { initIpc } from "./ipcMain";
|
||||
|
@ -100,19 +100,6 @@ if (!IS_VANILLA) {
|
|||
|
||||
super(options);
|
||||
initIpc(this);
|
||||
|
||||
// Workaround for https://github.com/electron/electron/issues/43367. Vesktop also has its own workaround
|
||||
// @TODO: Remove this when the issue is fixed
|
||||
if (IS_DISCORD_DESKTOP) {
|
||||
this.webContents.on("devtools-opened", () => {
|
||||
if (!nativeTheme.shouldUseDarkColors) return;
|
||||
|
||||
nativeTheme.themeSource = "light";
|
||||
setTimeout(() => {
|
||||
nativeTheme.themeSource = "dark";
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
} else super(options);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,13 +71,16 @@ export async function installExt(id: string) {
|
|||
// React Devtools v4.25
|
||||
// v4.27 is broken in Electron, see https://github.com/facebook/react/issues/25843
|
||||
// Unfortunately, Google does not serve old versions, so this is the only way
|
||||
// This zip file is pinned to long commit hash so it cannot be changed remotely
|
||||
? "https://raw.githubusercontent.com/Vendicated/random-files/f6f550e4c58ac5f2012095a130406c2ab25b984d/fmkadmapgofadopljbjfkapdkoienihi.zip"
|
||||
: `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`;
|
||||
: `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=${process.versions.chrome}`;
|
||||
|
||||
const buf = await get(url, {
|
||||
headers: {
|
||||
"User-Agent": "Vencord (https://github.com/Vendicated/Vencord)"
|
||||
"User-Agent": `Electron ${process.versions.electron} ~ Vencord (https://github.com/Vendicated/Vencord)`
|
||||
}
|
||||
});
|
||||
|
||||
await extract(crxToZip(buf), extDir).catch(console.error);
|
||||
}
|
||||
|
||||
|
|
5
src/plugins/_api/badges/fixDiscordBadgePadding.css
Normal file
5
src/plugins/_api/badges/fixDiscordBadgePadding.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
/* the profile popout badge container(s) */
|
||||
[class*="biteSize_"] [class*="tags_"] [class*="container_"] {
|
||||
/* Discord has padding set to 2px instead of 1px, which causes the 12th badge to wrap to a new line. */
|
||||
padding: 0 1px;
|
||||
}
|
|
@ -16,6 +16,8 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./fixDiscordBadgePadding.css";
|
||||
|
||||
import { _getBadges, BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
|
||||
import DonateButton from "@components/DonateButton";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
|
@ -26,7 +28,7 @@ import { Devs } from "@utils/constants";
|
|||
import { Logger } from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { isPluginDev } from "@utils/misc";
|
||||
import { closeModal, Modals, openModal } from "@utils/modal";
|
||||
import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Forms, Toasts, UserStore } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
|
@ -77,7 +79,7 @@ export default definePlugin({
|
|||
replace: "...$1.props,$& $1.image??"
|
||||
},
|
||||
{
|
||||
match: /(?<=text:(\i)\.description,.{0,200})children:/,
|
||||
match: /(?<="aria-label":(\i)\.description,.{0,200})children:/,
|
||||
replace: "children:$1.component ? $self.renderBadgeComponent({ ...$1 }) :"
|
||||
},
|
||||
// conditionally override their onClick with badge.onClick if it exists
|
||||
|
@ -100,8 +102,9 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
|
||||
userProfileBadge: ContributorBadge,
|
||||
|
||||
async start() {
|
||||
Vencord.Api.Badges.addBadge(ContributorBadge);
|
||||
await loadBadges();
|
||||
},
|
||||
|
||||
|
@ -141,8 +144,8 @@ export default definePlugin({
|
|||
closeModal(modalKey);
|
||||
VencordNative.native.openExternal("https://github.com/sponsors/Vendicated");
|
||||
}}>
|
||||
<Modals.ModalRoot {...props}>
|
||||
<Modals.ModalHeader>
|
||||
<ModalRoot {...props}>
|
||||
<ModalHeader>
|
||||
<Flex style={{ width: "100%", justifyContent: "center" }}>
|
||||
<Forms.FormTitle
|
||||
tag="h2"
|
||||
|
@ -156,8 +159,8 @@ export default definePlugin({
|
|||
Vencord Donor
|
||||
</Forms.FormTitle>
|
||||
</Flex>
|
||||
</Modals.ModalHeader>
|
||||
<Modals.ModalContent>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Flex>
|
||||
<img
|
||||
role="presentation"
|
||||
|
@ -180,13 +183,13 @@ export default definePlugin({
|
|||
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
|
||||
</Forms.FormText>
|
||||
</div>
|
||||
</Modals.ModalContent>
|
||||
<Modals.ModalFooter>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Flex style={{ width: "100%", justifyContent: "center" }}>
|
||||
<DonateButton />
|
||||
</Flex>
|
||||
</Modals.ModalFooter>
|
||||
</Modals.ModalRoot>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
</ErrorBoundary>
|
||||
));
|
||||
},
|
||||
|
|
|
@ -12,11 +12,15 @@ export default definePlugin({
|
|||
description: "API to add buttons to the chat input",
|
||||
authors: [Devs.Ven],
|
||||
|
||||
patches: [{
|
||||
patches: [
|
||||
{
|
||||
find: '"sticker")',
|
||||
replacement: {
|
||||
match: /return\(!\i\.\i&&(?=\(\i\.isDM.+?(\i)\.push\(.{0,50}"gift")/,
|
||||
replace: "$&(Vencord.Api.ChatButtons._injectButtons($1,arguments[0]),true)&&"
|
||||
match: /return\((!)?\i\.\i(?:\|\||&&)(?=\(\i\.isDM.+?(\i)\.push)/,
|
||||
replace: (m, not, children) => not
|
||||
? `${m}(Vencord.Api.ChatButtons._injectButtons(${children},arguments[0]),true)&&`
|
||||
: `${m}(Vencord.Api.ChatButtons._injectButtons(${children},arguments[0]),false)||`
|
||||
}
|
||||
}]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
@ -34,12 +34,22 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
{
|
||||
find: ".Menu,{",
|
||||
find: "navId:",
|
||||
all: true,
|
||||
replacement: {
|
||||
match: /Menu,{(?<=\.jsxs?\)\(\i\.Menu,{)/g,
|
||||
replace: "$&contextMenuApiArguments:typeof arguments!=='undefined'?arguments:[],"
|
||||
noWarn: true,
|
||||
replacement: [
|
||||
{
|
||||
match: /navId:(?=.+?([,}].*?\)))/g,
|
||||
replace: (m, rest) => {
|
||||
// Check if this navId: match is a destructuring statement, ignore it if it is
|
||||
const destructuringMatch = rest.match(/}=.+/);
|
||||
if (destructuringMatch == null) {
|
||||
return `contextMenuAPIArguments:typeof arguments!=='undefined'?arguments:[],${m}`;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
68
src/plugins/_api/menuItemDemangler.ts
Normal file
68
src/plugins/_api/menuItemDemangler.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import { canonicalizeMatch } from "@utils/patches";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
// duplicate values have multiple branches with different types. Just include all to be safe
|
||||
const nameMap = {
|
||||
radio: "MenuRadioItem",
|
||||
separator: "MenuSeparator",
|
||||
checkbox: "MenuCheckboxItem",
|
||||
groupstart: "MenuGroup",
|
||||
|
||||
control: "MenuControlItem",
|
||||
compositecontrol: "MenuControlItem",
|
||||
|
||||
item: "MenuItem",
|
||||
customitem: "MenuItem",
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "MenuItemDemanglerAPI",
|
||||
description: "Demangles Discord's Menu Item module",
|
||||
authors: [Devs.Ven],
|
||||
required: true,
|
||||
patches: [
|
||||
{
|
||||
find: '"Menu API',
|
||||
replacement: {
|
||||
match: /function.{0,80}type===(\i\.\i)\).{0,50}navigable:.+?Menu API/s,
|
||||
replace: (m, mod) => {
|
||||
const nameAssignments = [] as string[];
|
||||
|
||||
// if (t.type === m.MenuItem)
|
||||
const typeCheckRe = canonicalizeMatch(/\(\i\.type===(\i\.\i)\)/g);
|
||||
// push({type:"item"})
|
||||
const pushTypeRe = /type:"(\w+)"/g;
|
||||
|
||||
let typeMatch: RegExpExecArray | null;
|
||||
// for each if (t.type === ...)
|
||||
while ((typeMatch = typeCheckRe.exec(m)) !== null) {
|
||||
// extract the current menu item
|
||||
const item = typeMatch[1];
|
||||
// Set the starting index of the second regex to that of the first to start
|
||||
// matching from after the if
|
||||
pushTypeRe.lastIndex = typeCheckRe.lastIndex;
|
||||
// extract the first type: "..."
|
||||
const type = pushTypeRe.exec(m)?.[1];
|
||||
if (type && type in nameMap) {
|
||||
const name = nameMap[type];
|
||||
nameAssignments.push(`Object.defineProperty(${item},"name",{value:"${name}"})`);
|
||||
}
|
||||
}
|
||||
if (nameAssignments.length < 6) {
|
||||
console.warn("[MenuItemDemanglerAPI] Expected to at least remap 6 items, only remapped", nameAssignments.length);
|
||||
}
|
||||
|
||||
// Merge all our redefines with the actual module
|
||||
return `${nameAssignments.join(";")};${m}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
|
@ -31,20 +31,21 @@ export default definePlugin({
|
|||
replace: (match, args) => "" +
|
||||
`async ${match}` +
|
||||
`if(await Vencord.Api.MessageEvents._handlePreEdit(${args}))` +
|
||||
"return Promise.resolve({shoudClear:true,shouldRefocus:true});"
|
||||
"return Promise.resolve({shouldClear:false,shouldRefocus:true});"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: ".handleSendMessage,onResize",
|
||||
replacement: {
|
||||
// FIXME: Simplify this change once all branches share the same code
|
||||
// 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: /(\{openWarningPopout:.{0,100}type:this.props.chatInputType.+?\.then\((?:async )?)(\i=>\{.+?let (\i)=\i\.\i\.parse\((\i),.+?let (\i)=\i\.\i\.getSendMessageOptions\(\{.+?\}\);)(?<=\)\(({.+?})\)\.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}` +
|
||||
`${rest1}${rest1.includes("async") ? "" : "async "}${rest2}` +
|
||||
`if(await Vencord.Api.MessageEvents._handlePreSend(${channel}.id,${parsedMessage},${extra},${replyOptions}))` +
|
||||
"return{shoudClear:true,shouldRefocus:true};"
|
||||
"return{shouldClear:false,shouldRefocus:true};"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -23,11 +23,14 @@ export default definePlugin({
|
|||
name: "MessagePopoverAPI",
|
||||
description: "API to add buttons to message popovers.",
|
||||
authors: [Devs.KingFish, Devs.Ven, Devs.Nuckyz],
|
||||
patches: [{
|
||||
patches: [
|
||||
{
|
||||
find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}",
|
||||
replacement: {
|
||||
match: /\.jsx\)\((\i\.\i),\{label:\i\.\i\.string\(\i\.\i#{intl::MESSAGE_ACTION_REPLY}.{0,200}?"reply-self".{0,50}?\}\):null(?=,.+?message:(\i))/,
|
||||
replace: "$&,Vencord.Api.MessagePopover._buildPopoverElements($1,$2)"
|
||||
match: /(?<=:null),(.{0,40}togglePopout:.+?}\)),(.+?)\]}\):null,(?<=\((\i\.\i),{label:.+?:null,(\i&&!\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/,
|
||||
replace: (_, ReactButton, PotionButton, ButtonComponent, showReactButton, message) => "" +
|
||||
`]}):null,Vencord.Api.MessagePopover._buildPopoverElements(${ButtonComponent},${message}),${showReactButton}?${ReactButton}:null,${showReactButton}&&${PotionButton},`
|
||||
}
|
||||
}],
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
@ -34,7 +34,7 @@ export default definePlugin({
|
|||
},
|
||||
{
|
||||
match: /(?<=,NOTICE_DISMISS:function\(\i\){)return null!=(\i)/,
|
||||
replace: "if($1.id==\"VencordNotice\")return($1=null,Vencord.Api.Notices.nextNotice(),true);$&"
|
||||
replace: "if($1?.id==\"VencordNotice\")return($1=null,Vencord.Api.Notices.nextNotice(),true);$&"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -59,6 +59,14 @@ export default definePlugin({
|
|||
replace: "$&return;"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: ".BetterDiscord||null!=",
|
||||
replacement: {
|
||||
// Make hasClientMods return false
|
||||
match: /(?=let \i=window;)/,
|
||||
replace: "return false;"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ export default definePlugin({
|
|||
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
|
||||
},
|
||||
{
|
||||
match: /({(?=.+?function (\i).{0,160}(\i)=\i\.useMemo.{0,140}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/,
|
||||
match: /({(?=.+?function (\i).{0,160}(\i)=\i\.useMemo.{0,140}return \i\.useMemo\(\(\)=>\i\(\3).+?(?:function\(\){return |\(\)=>))\2/,
|
||||
replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})`
|
||||
}
|
||||
]
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { addAccessory } from "@api/MessageAccessories";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { getUserSettingLazy } from "@api/UserSettings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
|
@ -34,6 +33,7 @@ import { makeCodeblock } from "@utils/text";
|
|||
import definePlugin from "@utils/types";
|
||||
import { checkForUpdates, isOutdated, update } from "@utils/updater";
|
||||
import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";
|
||||
import { JSX } from "react";
|
||||
|
||||
import gitHash from "~git-hash";
|
||||
import plugins, { PluginMeta } from "~plugins";
|
||||
|
@ -142,7 +142,7 @@ export default definePlugin({
|
|||
required: true,
|
||||
description: "Helps us provide support to you",
|
||||
authors: [Devs.Ven],
|
||||
dependencies: ["UserSettingsAPI", "MessageAccessoriesAPI"],
|
||||
dependencies: ["UserSettingsAPI"],
|
||||
|
||||
settings,
|
||||
|
||||
|
@ -235,23 +235,7 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
|
||||
renderContributorDmWarningCard: ErrorBoundary.wrap(({ channel }) => {
|
||||
const userId = channel.getRecipientId();
|
||||
if (!isPluginDev(userId)) return null;
|
||||
if (RelationshipStore.isFriend(userId) || isPluginDev(UserStore.getCurrentUser()?.id)) return null;
|
||||
|
||||
return (
|
||||
<Card className={`vc-plugins-restart-card ${Margins.top8}`}>
|
||||
Please do not private message Vencord plugin developers for support!
|
||||
<br />
|
||||
Instead, use the Vencord support channel: {Parser.parse("https://discord.com/channels/1015060230222131221/1026515880080842772")}
|
||||
{!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"}
|
||||
</Card>
|
||||
);
|
||||
}, { noop: true }),
|
||||
|
||||
start() {
|
||||
addAccessory("vencord-debug", props => {
|
||||
renderMessageAccessory(props) {
|
||||
const buttons = [] as JSX.Element[];
|
||||
|
||||
const shouldAddUpdateButton =
|
||||
|
@ -328,6 +312,20 @@ export default definePlugin({
|
|||
return buttons.length
|
||||
? <Flex>{buttons}</Flex>
|
||||
: null;
|
||||
});
|
||||
},
|
||||
|
||||
renderContributorDmWarningCard: ErrorBoundary.wrap(({ channel }) => {
|
||||
const userId = channel.getRecipientId();
|
||||
if (!isPluginDev(userId)) return null;
|
||||
if (RelationshipStore.isFriend(userId) || isPluginDev(UserStore.getCurrentUser()?.id)) return null;
|
||||
|
||||
return (
|
||||
<Card className={`vc-plugins-restart-card ${Margins.top8}`}>
|
||||
Please do not private message Vencord plugin developers for support!
|
||||
<br />
|
||||
Instead, use the Vencord support channel: {Parser.parse("https://discord.com/channels/1015060230222131221/1026515880080842772")}
|
||||
{!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"}
|
||||
</Card>
|
||||
);
|
||||
}, { noop: true }),
|
||||
});
|
||||
|
|
|
@ -16,14 +16,14 @@ import { User } from "discord-types/general";
|
|||
interface UserProfileProps {
|
||||
popoutProps: Record<string, any>;
|
||||
currentUser: User;
|
||||
originalPopout: () => React.ReactNode;
|
||||
originalRenderPopout: () => React.ReactNode;
|
||||
}
|
||||
|
||||
const UserProfile = findComponentByCodeLazy("UserProfilePopoutWrapper: user cannot be undefined");
|
||||
const styles = findByPropsLazy("accountProfilePopoutWrapper");
|
||||
|
||||
let openAlternatePopout = false;
|
||||
let accountPanelRef: React.MutableRefObject<Record<PropertyKey, any> | null> = { current: null };
|
||||
let accountPanelRef: React.RefObject<Record<PropertyKey, any> | null> = { current: null };
|
||||
|
||||
const AccountPanelContextMenu = ErrorBoundary.wrap(() => {
|
||||
const { prioritizeServerProfile } = settings.use(["prioritizeServerProfile"]);
|
||||
|
@ -73,19 +73,19 @@ export default definePlugin({
|
|||
group: true,
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=\.SIZE_32\)}\);)/,
|
||||
match: /(?<=\.AVATAR_SIZE\);)/,
|
||||
replace: "$self.useAccountPanelRef();"
|
||||
},
|
||||
{
|
||||
match: /(\.AVATAR,children:.+?renderPopout:(\i)=>){(.+?)}(?=,position)(?<=currentUser:(\i).+?)/,
|
||||
replace: (_, rest, popoutProps, originalPopout, currentUser) => `${rest}$self.UserProfile({popoutProps:${popoutProps},currentUser:${currentUser},originalPopout:()=>{${originalPopout}}})`
|
||||
replace: (_, rest, popoutProps, originalPopout, currentUser) => `${rest}$self.UserProfile({popoutProps:${popoutProps},currentUser:${currentUser},originalRenderPopout:()=>{${originalPopout}}})`
|
||||
},
|
||||
{
|
||||
match: /\.AVATAR,children:.+?(?=renderPopout:)/,
|
||||
replace: "$&onRequestClose:$self.onPopoutClose,"
|
||||
},
|
||||
{
|
||||
match: /(?<=.avatarWrapper,)/,
|
||||
match: /(?<=\.avatarWrapper,)/,
|
||||
replace: "ref:$self.accountPanelRef,onContextMenu:$self.openAccountPanelContextMenu,"
|
||||
}
|
||||
]
|
||||
|
@ -112,17 +112,17 @@ export default definePlugin({
|
|||
openAlternatePopout = false;
|
||||
},
|
||||
|
||||
UserProfile: ErrorBoundary.wrap(({ popoutProps, currentUser, originalPopout }: UserProfileProps) => {
|
||||
UserProfile: ErrorBoundary.wrap(({ popoutProps, currentUser, originalRenderPopout }: UserProfileProps) => {
|
||||
if (
|
||||
(settings.store.prioritizeServerProfile && openAlternatePopout) ||
|
||||
(!settings.store.prioritizeServerProfile && !openAlternatePopout)
|
||||
) {
|
||||
return originalPopout();
|
||||
return originalRenderPopout();
|
||||
}
|
||||
|
||||
const currentChannel = getCurrentChannel();
|
||||
if (currentChannel?.getGuildId() == null) {
|
||||
return originalPopout();
|
||||
return originalRenderPopout();
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -41,10 +41,10 @@ export default definePlugin({
|
|||
},
|
||||
{
|
||||
// Status emojis
|
||||
find: "#{intl::GUILD_OWNER}",
|
||||
find: "#{intl::GUILD_OWNER}),children:",
|
||||
replacement: {
|
||||
match: /(?<=\.activityEmoji,.+?animate:)\i/,
|
||||
replace: "!0"
|
||||
match: /(\.CUSTOM_STATUS.+?animate:)\i/,
|
||||
replace: (_, rest) => `${rest}!0`
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -51,7 +51,7 @@ export default definePlugin({
|
|||
{
|
||||
find: "bitbucket.org",
|
||||
replacement: {
|
||||
match: /function \i\(\i\){(?=.{0,60}\.parse\(\i\))/,
|
||||
match: /function \i\(\i\){(?=.{0,30}pathname:\i)/,
|
||||
replace: "$&return null;"
|
||||
},
|
||||
predicate: () => settings.store.file
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { canonicalizeMatch } from "@utils/patches";
|
||||
import { execFile } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
|
@ -26,7 +27,7 @@ interface RemoteData {
|
|||
let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null;
|
||||
|
||||
const APPLE_MUSIC_BUNDLE_REGEX = /<script type="module" crossorigin src="([a-zA-Z0-9.\-/]+)"><\/script>/;
|
||||
const APPLE_MUSIC_TOKEN_REGEX = /\w+="([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)",\w+="x-apple-jingle-correlation-key"/;
|
||||
const APPLE_MUSIC_TOKEN_REGEX = canonicalizeMatch(/Promise.allSettled\(\i\)\}const \i="([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)"/);
|
||||
|
||||
let cachedToken: string | undefined = undefined;
|
||||
|
||||
|
|
|
@ -17,14 +17,13 @@
|
|||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { useStateFromStores } from "@webpack/common";
|
||||
import { findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Animations, useStateFromStores } from "@webpack/common";
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
import { ExpandedGuildFolderStore, settings } from ".";
|
||||
|
||||
const ChannelRTCStore = findStoreLazy("ChannelRTCStore");
|
||||
const Animations = findByPropsLazy("a", "animated", "useTransition");
|
||||
const GuildsBar = findComponentByCodeLazy('("guildsnav")');
|
||||
|
||||
export default ErrorBoundary.wrap(guildsBarProps => {
|
||||
|
|
|
@ -123,7 +123,7 @@ export default definePlugin({
|
|||
},
|
||||
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
|
||||
{
|
||||
match: /lastTargetNode:\i\[\i\.length-1\].+?Fragment.+?\]}\)\]/,
|
||||
match: /lastTargetNode:\i\[\i\.length-1\].+?}\)(?::null)?\](?=}\))/,
|
||||
replace: "$&.filter($self.makeGuildsBarGuildListFilter(!!arguments[0]?.isBetterFolders))"
|
||||
},
|
||||
// If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children
|
||||
|
@ -159,7 +159,7 @@ export default definePlugin({
|
|||
]
|
||||
},
|
||||
{
|
||||
find: ".FOLDER_ITEM_GUILD_ICON_MARGIN);",
|
||||
find: ".expandedFolderBackground,",
|
||||
predicate: () => settings.store.sidebar,
|
||||
replacement: [
|
||||
// We use arguments[0] to access the isBetterFolders variable in this nested folder component (the parent exports all the props so we don't have to patch it)
|
||||
|
@ -173,8 +173,8 @@ export default definePlugin({
|
|||
// Disable expanding and collapsing folders transition in the normal GuildsBar sidebar
|
||||
{
|
||||
predicate: () => !settings.store.keepIcons,
|
||||
match: /(?<=#{intl::SERVER_FOLDER_PLACEHOLDER}.+?useTransition\)\()/,
|
||||
replace: "$self.shouldShowTransition(arguments[0])&&"
|
||||
match: /(?=,\{from:\{height)/,
|
||||
replace: "&&$self.shouldShowTransition(arguments[0])"
|
||||
},
|
||||
// If we are rendering the normal GuildsBar sidebar, we avoid rendering guilds from folders that are expanded
|
||||
{
|
||||
|
@ -185,7 +185,7 @@ export default definePlugin({
|
|||
{
|
||||
// Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar
|
||||
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
|
||||
match: /(?<=\.wrapper,children:\[)/,
|
||||
match: /(?<=\.isExpanded\),children:\[)/,
|
||||
replace: "$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)&&"
|
||||
},
|
||||
{
|
||||
|
@ -276,23 +276,29 @@ export default definePlugin({
|
|||
|
||||
makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
|
||||
return child => {
|
||||
if (isBetterFolders) {
|
||||
if (!isBetterFolders) return true;
|
||||
|
||||
try {
|
||||
return child?.props?.["aria-label"] === getIntlMessage("SERVERS");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
},
|
||||
|
||||
makeGuildsBarTreeFilter(isBetterFolders: boolean) {
|
||||
return child => {
|
||||
if (isBetterFolders) {
|
||||
return child?.props?.onScroll != null;
|
||||
}
|
||||
if (!isBetterFolders) return true;
|
||||
|
||||
if (child?.props?.className?.includes("itemsContainer") && child.props.children != null) {
|
||||
// Filter out everything but the scroller for the guild list
|
||||
child.props.children = child.props.children.filter(child => child?.props?.onScroll != null);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@ export default definePlugin({
|
|||
if (!role) return;
|
||||
|
||||
if (role.colorString) {
|
||||
children.push(
|
||||
children.unshift(
|
||||
<Menu.MenuItem
|
||||
id="vc-copy-role-color"
|
||||
label="Copy Role Color"
|
||||
|
@ -93,6 +93,20 @@ export default definePlugin({
|
|||
);
|
||||
}
|
||||
|
||||
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
|
||||
children.unshift(
|
||||
<Menu.MenuItem
|
||||
id="vc-edit-role"
|
||||
label="Edit Role"
|
||||
action={async () => {
|
||||
await GuildSettingsActions.open(guild.id, "ROLES");
|
||||
GuildSettingsActions.selectRole(id);
|
||||
}}
|
||||
icon={PencilIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (role.icon) {
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
|
@ -110,20 +124,6 @@ export default definePlugin({
|
|||
|
||||
);
|
||||
}
|
||||
|
||||
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
id="vc-edit-role"
|
||||
label="Edit Role"
|
||||
action={async () => {
|
||||
await GuildSettingsActions.open(guild.id, "ROLES");
|
||||
GuildSettingsActions.selectRole(id);
|
||||
}}
|
||||
icon={PencilIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -99,7 +99,7 @@ export const UnknownIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElemen
|
|||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path fill-rule="evenodd" d="M4.475 5.458c-.284 0-.514-.237-.47-.517C4.28 3.24 5.576 2 7.825 2c2.25 0 3.767 1.36 3.767 3.215 0 1.344-.665 2.288-1.79 2.973-1.1.659-1.414 1.118-1.414 2.01v.03a.5.5 0 0 1-.5.5h-.77a.5.5 0 0 1-.5-.495l-.003-.2c-.043-1.221.477-2.001 1.645-2.712 1.03-.632 1.397-1.135 1.397-2.028 0-.979-.758-1.698-1.926-1.698-1.009 0-1.71.529-1.938 1.402-.066.254-.278.461-.54.461h-.777ZM7.496 14c.622 0 1.095-.474 1.095-1.09 0-.618-.473-1.092-1.095-1.092-.606 0-1.087.474-1.087 1.091S6.89 14 7.496 14Z" />
|
||||
<path fillRule="evenodd" d="M4.475 5.458c-.284 0-.514-.237-.47-.517C4.28 3.24 5.576 2 7.825 2c2.25 0 3.767 1.36 3.767 3.215 0 1.344-.665 2.288-1.79 2.973-1.1.659-1.414 1.118-1.414 2.01v.03a.5.5 0 0 1-.5.5h-.77a.5.5 0 0 1-.5-.495l-.003-.2c-.043-1.221.477-2.001 1.645-2.712 1.03-.632 1.397-1.135 1.397-2.028 0-.979-.758-1.698-1.926-1.698-1.009 0-1.71.529-1.938 1.402-.066.254-.278.461-.54.461h-.777ZM7.496 14c.622 0 1.095-.474 1.095-1.09 0-.618-.473-1.092-1.095-1.092-.606 0-1.087.474-1.087 1.091S6.89 14 7.496 14Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
|
||||
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Constants, React, RestAPI, Tooltip } from "@webpack/common";
|
||||
|
||||
import { RenameButton } from "./components/RenameButton";
|
||||
|
@ -34,7 +34,7 @@ const UserSettingsModal = findByPropsLazy("saveAccountChanges", "open");
|
|||
const TimestampClasses = findByPropsLazy("timestampTooltip", "blockquoteContainer");
|
||||
const SessionIconClasses = findByPropsLazy("sessionIcon");
|
||||
|
||||
const BlobMask = findExportedComponentLazy("BlobMask");
|
||||
const BlobMask = findComponentByCodeLazy("!1,lowerBadgeSize:");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
backgroundCheck: {
|
||||
|
|
|
@ -101,8 +101,8 @@ export default definePlugin({
|
|||
find: 'minimal:"contentColumnMinimal"',
|
||||
replacement: [
|
||||
{
|
||||
match: /\(0,\i\.useTransition\)\((\i)/,
|
||||
replace: "(_cb=>_cb(void 0,$1))||$&"
|
||||
match: /(?=\(0,\i\.\i\)\((\i),\{from:\{position:"absolute")/,
|
||||
replace: "(_cb=>_cb(void 0,$1))||"
|
||||
},
|
||||
{
|
||||
match: /\i\.animated\.div/,
|
||||
|
@ -114,7 +114,7 @@ export default definePlugin({
|
|||
{ // Load menu TOC eagerly
|
||||
find: "#{intl::USER_SETTINGS_WITH_BUILD_OVERRIDE}",
|
||||
replacement: {
|
||||
match: /(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?null!=\i&&.{0,100}?(await Promise\.all[^};]*?\)\)).*?,(?=\1\(this)/,
|
||||
match: /(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?null!=\i&&.{0,100}?(await [^};]*?\)\)).*?,(?=\1\(this)/,
|
||||
replace: "$&(async ()=>$2)(),"
|
||||
},
|
||||
predicate: () => settings.store.eagerLoad
|
||||
|
@ -173,7 +173,7 @@ export default definePlugin({
|
|||
}
|
||||
return this;
|
||||
},
|
||||
map(render: (item: SettingsEntry) => ReactElement) {
|
||||
map(render: (item: SettingsEntry) => ReactElement<any>) {
|
||||
return items
|
||||
.filter(a => a.items.length > 0)
|
||||
.map(({ label, items }) => {
|
||||
|
@ -181,11 +181,14 @@ export default definePlugin({
|
|||
if (label) {
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
key={label}
|
||||
id={label.replace(/\W/, "_")}
|
||||
label={label}
|
||||
children={children}
|
||||
action={children[0].props.action}
|
||||
/>);
|
||||
>
|
||||
{children}
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
} else {
|
||||
return children;
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ export default definePlugin({
|
|||
{
|
||||
find: '"ChannelAttachButton"',
|
||||
replacement: {
|
||||
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),\.\.\.(\i),/,
|
||||
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),.{0,30}?\.\.\.(\i),/,
|
||||
replace: "$&onClick:$1,onContextMenu:$2.onClick,",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -75,10 +75,11 @@ export default definePlugin({
|
|||
patches: [{
|
||||
find: "renderConnectionStatus(){",
|
||||
replacement: {
|
||||
match: /(?<=renderConnectionStatus\(\)\{.+\.channel,children:)\i/,
|
||||
replace: "[$&, $self.renderTimer(this.props.channel.id)]"
|
||||
match: /(renderConnectionStatus\(\){.+\.channel,children:)(.+?}\):\i)(?=}\))/,
|
||||
replace: "$1[$2,$self.renderTimer(this.props.channel.id)]"
|
||||
}
|
||||
}],
|
||||
|
||||
renderTimer(channelId: string) {
|
||||
return <ErrorBoundary noop>
|
||||
<this.Timer channelId={channelId} />
|
||||
|
|
|
@ -17,11 +17,7 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
addPreEditListener,
|
||||
addPreSendListener,
|
||||
MessageObject,
|
||||
removePreEditListener,
|
||||
removePreSendListener
|
||||
MessageObject
|
||||
} from "@api/MessageEvents";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
@ -36,7 +32,18 @@ export default definePlugin({
|
|||
name: "ClearURLs",
|
||||
description: "Removes tracking garbage from URLs",
|
||||
authors: [Devs.adryd],
|
||||
dependencies: ["MessageEventsAPI"],
|
||||
|
||||
start() {
|
||||
this.createRules();
|
||||
},
|
||||
|
||||
onBeforeMessageSend(_, msg) {
|
||||
return this.onSend(msg);
|
||||
},
|
||||
|
||||
onBeforeMessageEdit(_cid, _mid, msg) {
|
||||
return this.onSend(msg);
|
||||
},
|
||||
|
||||
escapeRegExp(str: string) {
|
||||
return (str && reHasRegExpChar.test(str))
|
||||
|
@ -133,17 +140,4 @@ export default definePlugin({
|
|||
);
|
||||
}
|
||||
},
|
||||
|
||||
start() {
|
||||
this.createRules();
|
||||
this.preSend = addPreSendListener((_, msg) => this.onSend(msg));
|
||||
this.preEdit = addPreEditListener((_cid, _mid, msg) =>
|
||||
this.onSend(msg)
|
||||
);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removePreSendListener(this.preSend);
|
||||
removePreEditListener(this.preEdit);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -91,15 +91,12 @@ function ThemeSettings() {
|
|||
|
||||
const settings = definePluginSettings({
|
||||
color: {
|
||||
description: "Color your Discord client theme will be based around. Light mode isn't supported",
|
||||
type: OptionType.COMPONENT,
|
||||
default: "313338",
|
||||
component: () => <ThemeSettings />
|
||||
component: ThemeSettings
|
||||
},
|
||||
resetColor: {
|
||||
description: "Reset Theme Color",
|
||||
type: OptionType.COMPONENT,
|
||||
default: "313338",
|
||||
component: () => (
|
||||
<Button onClick={() => onPickColor(0x313338)}>
|
||||
Reset Theme Color
|
||||
|
@ -110,7 +107,7 @@ const settings = definePluginSettings({
|
|||
|
||||
export default definePlugin({
|
||||
name: "ClientTheme",
|
||||
authors: [Devs.F53, Devs.Nuckyz],
|
||||
authors: [Devs.Nuckyz],
|
||||
description: "Recreation of the old client theme experiment. Add a color to your Discord client theme",
|
||||
settings,
|
||||
|
||||
|
|
|
@ -67,9 +67,16 @@ export default definePlugin({
|
|||
|
||||
patches: [
|
||||
{
|
||||
find: 'react-spring: The "interpolate" function',
|
||||
find: "https://github.com/highlightjs/highlight.js/issues/2277",
|
||||
replacement: {
|
||||
match: /,console.warn\('react-spring: The "interpolate" function is deprecated in v10 \(use "to" instead\)'\)/,
|
||||
match: /\(console.log\(`Deprecated.+?`\),/,
|
||||
replace: "("
|
||||
}
|
||||
},
|
||||
{
|
||||
find: 'The "interpolate" function is deprecated in v10 (use "to" instead)',
|
||||
replacement: {
|
||||
match: /,console.warn\(\i\+'The "interpolate" function is deprecated in v10 \(use "to" instead\)'\)/,
|
||||
replace: ""
|
||||
}
|
||||
},
|
||||
|
@ -88,10 +95,9 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
{
|
||||
find: 'console.warn("[DEPRECATED] Please use `subscribeWithSelector` middleware");',
|
||||
all: true,
|
||||
find: '"AppCrashedFatalReport: getLastCrash not supported."',
|
||||
replacement: {
|
||||
match: /console\.warn\("\[DEPRECATED\] Please use `subscribeWithSelector` middleware"\);/,
|
||||
match: /console\.log\("AppCrashedFatalReport: getLastCrash not supported\."\);/,
|
||||
replace: ""
|
||||
}
|
||||
},
|
||||
|
@ -126,28 +132,7 @@ export default definePlugin({
|
|||
{
|
||||
find: "Slow dispatch on",
|
||||
replacement: {
|
||||
match: /\i\.totalTime>100&&\i\.verbose\("Slow dispatch on ".+?\)\);/,
|
||||
replace: ""
|
||||
}
|
||||
},
|
||||
// Zustand section
|
||||
{
|
||||
find: "[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`.",
|
||||
replacement: [
|
||||
{
|
||||
match: /&&console\.warn\("\[DEPRECATED\] Passing a vanilla store will be unsupported in a future version\. Instead use `import { useStore } from 'zustand'`\."\)/,
|
||||
replace: ""
|
||||
},
|
||||
{
|
||||
match: /console\.warn\("\[DEPRECATED\] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`\. They can be imported from 'zustand\/traditional'\. https:\/\/github\.com\/pmndrs\/zustand\/discussions\/1937"\),/,
|
||||
replace: ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "[DEPRECATED] `getStorage`, `serialize` and `deserialize` options are deprecated. Use `storage` option instead.",
|
||||
replacement: {
|
||||
match: /console\.warn\("\[DEPRECATED\] `getStorage`, `serialize` and `deserialize` options are deprecated\. Use `storage` option instead\."\),/,
|
||||
match: /\i\.totalTime>\i&&\i\.verbose\("Slow dispatch on ".+?\)\);/,
|
||||
replace: ""
|
||||
}
|
||||
},
|
||||
|
|
|
@ -20,6 +20,7 @@ import { Devs } from "@utils/constants";
|
|||
import { getCurrentChannel, getCurrentGuild } from "@utils/discord";
|
||||
import { runtimeHashMessageKey } from "@utils/intlHash";
|
||||
import { SYM_LAZY_CACHED, SYM_LAZY_GET } from "@utils/lazy";
|
||||
import { ModalAPI } from "@utils/modal";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches";
|
||||
import definePlugin, { PluginNative, StartAt } from "@utils/types";
|
||||
|
@ -62,7 +63,7 @@ function makeShortcuts() {
|
|||
default:
|
||||
const uniqueMatches = [...new Set(matches)];
|
||||
if (uniqueMatches.length > 1)
|
||||
console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches);
|
||||
console.warn(`Warning: This filter matches ${uniqueMatches.length} exports. Make it more specific!\n`, uniqueMatches);
|
||||
|
||||
return matches[0];
|
||||
}
|
||||
|
@ -132,7 +133,10 @@ function makeShortcuts() {
|
|||
});
|
||||
}
|
||||
|
||||
Common.ReactDOM.render(Common.React.createElement(component, props), doc.body.appendChild(document.createElement("div")));
|
||||
const root = Common.ReactDOM.createRoot(doc.body.appendChild(document.createElement("div")));
|
||||
root.render(Common.React.createElement(component, props));
|
||||
|
||||
doc.addEventListener("close", () => root.unmount(), { once: true });
|
||||
},
|
||||
|
||||
preEnable: (plugin: string) => (Vencord.Settings.plugins[plugin] ??= { enabled: true }).enabled = true,
|
||||
|
@ -144,6 +148,8 @@ function makeShortcuts() {
|
|||
me: { getter: () => Common.UserStore.getCurrentUser(), preload: false },
|
||||
meId: { getter: () => Common.UserStore.getCurrentUser().id, preload: false },
|
||||
messages: { getter: () => Common.MessageStore.getMessages(Common.SelectedChannelStore.getChannelId()), preload: false },
|
||||
openModal: { getter: () => ModalAPI.openModal },
|
||||
openModalLazy: { getter: () => ModalAPI.openModalLazy },
|
||||
|
||||
Stores: {
|
||||
getter: () => Object.fromEntries(
|
||||
|
@ -159,11 +165,38 @@ function loadAndCacheShortcut(key: string, val: any, forceLoad: boolean) {
|
|||
const currentVal = val.getter();
|
||||
if (!currentVal || val.preload === false) return currentVal;
|
||||
|
||||
const value = currentVal[SYM_LAZY_GET]
|
||||
? forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED]
|
||||
: currentVal;
|
||||
function unwrapProxy(value: any) {
|
||||
if (value[SYM_LAZY_GET]) {
|
||||
forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED];
|
||||
} else if (value.$$vencordInternal) {
|
||||
return forceLoad ? value.$$vencordInternal() : value;
|
||||
}
|
||||
|
||||
if (value) define(window.shortcutList, key, { value });
|
||||
return value;
|
||||
}
|
||||
|
||||
const value = unwrapProxy(currentVal);
|
||||
if (typeof value === "object" && value !== null) {
|
||||
const descriptors = Object.getOwnPropertyDescriptors(value);
|
||||
|
||||
for (const propKey in descriptors) {
|
||||
if (value[propKey] == null) continue;
|
||||
|
||||
const descriptor = descriptors[propKey];
|
||||
if (descriptor.writable === true || descriptor.set != null) {
|
||||
const currentValue = value[propKey];
|
||||
const newValue = unwrapProxy(currentValue);
|
||||
if (newValue != null && currentValue !== newValue) {
|
||||
value[propKey] = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
define(window.shortcutList, key, { value });
|
||||
define(window, key, { value });
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
@ -173,6 +206,16 @@ export default definePlugin({
|
|||
description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.",
|
||||
authors: [Devs.Ven],
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: 'this,"_changeCallbacks",',
|
||||
replacement: {
|
||||
match: /\i\(this,"_changeCallbacks",/,
|
||||
replace: "Reflect.defineProperty(this,Symbol.toStringTag,{value:this.getName(),configurable:!0,writable:!0,enumerable:!1}),$&"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
startAt: StartAt.Init,
|
||||
start() {
|
||||
const shortcuts = makeShortcuts();
|
||||
|
|
|
@ -42,10 +42,10 @@ export default definePlugin({
|
|||
// Only one of the two patches will be at effect; Discord often updates to switch between them.
|
||||
// See: https://discord.com/channels/1015060230222131221/1032770730703716362/1261398512017477673
|
||||
{
|
||||
find: ".ENTER&&(!",
|
||||
find: ".selectPreviousCommandOption(",
|
||||
replacement: {
|
||||
match: /(?<=(\i)\.which===\i\.\i.ENTER&&).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=&&\(\i\.preventDefault)/,
|
||||
replace: "$self.shouldSubmit($1, $2)"
|
||||
match: /(?<=(\i)\.which(?:!==|===)\i\.\i.ENTER(\|\||&&)).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=(?:\|\||&&)\(\i\.preventDefault)/,
|
||||
replace: (_, event, condition, codeblock) => `${condition === "||" ? "!" : ""}$self.shouldSubmit(${event},${codeblock})`
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import { definePluginSettings, Settings } from "@api/Settings";
|
||||
import { getUserSettingLazy } from "@api/UserSettings";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { Link } from "@components/Link";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { isTruthy } from "@utils/guards";
|
||||
|
@ -27,15 +28,14 @@ import { classes } from "@utils/misc";
|
|||
import { useAwaiter } from "@utils/react";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByCodeLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
|
||||
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, React, UserStore } from "@webpack/common";
|
||||
|
||||
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
|
||||
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
|
||||
const ActivityView = findComponentByCodeLazy(".party?(0", ".card");
|
||||
|
||||
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
|
||||
|
||||
async function getApplicationAsset(key: string): Promise<string> {
|
||||
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
|
||||
return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0];
|
||||
}
|
||||
|
||||
|
@ -169,7 +169,7 @@ const settings = definePluginSettings({
|
|||
value: TimestampMode.NOW
|
||||
},
|
||||
{
|
||||
label: "Same as your current time",
|
||||
label: "Same as your current time (not reset after 24h)",
|
||||
value: TimestampMode.TIME
|
||||
},
|
||||
{
|
||||
|
@ -269,6 +269,7 @@ function isStreamLinkDisabled() {
|
|||
|
||||
function isStreamLinkValid(value: string) {
|
||||
if (!isStreamLinkDisabled() && !/https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(value)) return "Streaming link must be a valid URL.";
|
||||
if (value && value.length > 512) return "Streaming link must be not longer than 512 characters.";
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -277,8 +278,9 @@ function isTimestampDisabled() {
|
|||
}
|
||||
|
||||
function isImageKeyValid(value: string) {
|
||||
if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image. (e.g. https://i.imgur.com/...)";
|
||||
if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image. (e.g. https://media.tenor.com/...)";
|
||||
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//.test(value)) return "Don't use a Discord link. Use an Imgur image link instead.";
|
||||
if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image (e.g. https://i.imgur.com/...). Right click the image and click 'Copy image address'";
|
||||
if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image (e.g. https://media.tenor.com/...). Right click the GIF and click 'Copy image address'";
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -390,13 +392,24 @@ async function setRpc(disable?: boolean) {
|
|||
|
||||
export default definePlugin({
|
||||
name: "CustomRPC",
|
||||
description: "Allows you to set a custom rich presence.",
|
||||
description: "Add a fully customisable Rich Presence (Game status) to your Discord profile",
|
||||
authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
|
||||
dependencies: ["UserSettingsAPI"],
|
||||
start: setRpc,
|
||||
stop: () => setRpc(true),
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: ".party?(0",
|
||||
all: true,
|
||||
replacement: {
|
||||
match: /\i\.id===\i\.id\?null:/,
|
||||
replace: ""
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
settingsAboutComponent: () => {
|
||||
const activity = useAwaiter(createActivity);
|
||||
const gameActivityEnabled = ShowCurrentGame.useSetting();
|
||||
|
@ -410,7 +423,7 @@ export default definePlugin({
|
|||
style={{ padding: "1em" }}
|
||||
>
|
||||
<Forms.FormTitle>Notice</Forms.FormTitle>
|
||||
<Forms.FormText>Game activity isn't enabled, people won't be able to see your custom rich presence!</Forms.FormText>
|
||||
<Forms.FormText>Activity Sharing isn't enabled, people won't be able to see your custom rich presence!</Forms.FormText>
|
||||
|
||||
<Button
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
|
@ -422,24 +435,33 @@ export default definePlugin({
|
|||
</ErrorCard>
|
||||
)}
|
||||
|
||||
<Flex flexDirection="column" style={{ gap: ".5em" }} className={Margins.top16}>
|
||||
<Forms.FormText>
|
||||
Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
|
||||
Go to the <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
|
||||
get the application ID.
|
||||
</Forms.FormText>
|
||||
<Forms.FormText>
|
||||
Upload images in the Rich Presence tab to get the image keys.
|
||||
</Forms.FormText>
|
||||
<Forms.FormText>
|
||||
If you want to use image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and select "Copy image address".
|
||||
If you want to use an image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and selecting "Copy image address".
|
||||
</Forms.FormText>
|
||||
<Forms.FormText>
|
||||
You can't see your own buttons on your profile, but everyone else can see it fine.
|
||||
</Forms.FormText>
|
||||
<Forms.FormText>
|
||||
Some weird unicode text ("fonts" 𝖑𝖎𝖐𝖊 𝖙𝖍𝖎𝖘) may cause the rich presence to not show up, try using normal letters instead.
|
||||
</Forms.FormText>
|
||||
</Flex>
|
||||
|
||||
<Forms.FormDivider className={Margins.top8} />
|
||||
|
||||
<div style={{ width: "284px", ...profileThemeStyle, padding: 8, marginTop: 8, borderRadius: 8, background: "var(--bg-mod-faint)" }}>
|
||||
{activity[0] && <ActivityComponent activity={activity[0]} channelId={SelectedChannelStore.getChannelId()}
|
||||
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}
|
||||
application={{ id: settings.store.appID }}
|
||||
user={UserStore.getCurrentUser()} />}
|
||||
<div style={{ width: "284px", ...profileThemeStyle, marginTop: 8, borderRadius: 8, background: "var(--bg-mod-faint)" }}>
|
||||
{activity[0] && <ActivityView
|
||||
activity={activity[0]}
|
||||
user={UserStore.getCurrentUser()}
|
||||
currentUser={UserStore.getCurrentUser()}
|
||||
/>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -17,7 +17,6 @@ import DecorSection from "./ui/components/DecorSection";
|
|||
export const settings = definePluginSettings({
|
||||
changeDecoration: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Change your avatar decoration",
|
||||
component() {
|
||||
if (!Vencord.Plugins.plugins.Decor.started) return <Forms.FormText>
|
||||
Enable Decor and restart your client to change your avatar decoration.
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { React } from "@webpack/common";
|
||||
import { JSX } from "react";
|
||||
|
||||
import { cl } from "../";
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { classes } from "@utils/misc";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { React } from "@webpack/common";
|
||||
import { JSX } from "react";
|
||||
|
||||
import { cl } from "../";
|
||||
import Grid, { GridProps } from "./Grid";
|
||||
|
|
|
@ -16,11 +16,9 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { migratePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
migratePluginSettings("DisableCallIdle", "DisableDMCallIdle");
|
||||
export default definePlugin({
|
||||
name: "DisableCallIdle",
|
||||
description: "Disables automatically getting kicked from a DM voice call after 3 minutes and being moved to an AFK voice channel.",
|
||||
|
|
|
@ -211,7 +211,7 @@ function CloneModal({ data }: { data: Sticker | Emoji; }) {
|
|||
alignItems: "center"
|
||||
}}>
|
||||
{guilds.map(g => (
|
||||
<Tooltip text={g.name}>
|
||||
<Tooltip key={g.id} text={g.name}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<div
|
||||
onMouseLeave={onMouseLeave}
|
||||
|
@ -310,7 +310,8 @@ function buildMenuItem(type: "Emoji" | "Sticker", fetchData: () => Promisable<Om
|
|||
}
|
||||
|
||||
function isGifUrl(url: string) {
|
||||
return new URL(url).pathname.endsWith(".gif");
|
||||
const u = new URL(url);
|
||||
return u.pathname.endsWith(".gif") || u.searchParams.get("animated") === "true";
|
||||
}
|
||||
|
||||
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
|
||||
import { addMessagePreEditListener, addMessagePreSendListener, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies";
|
||||
|
@ -207,8 +207,8 @@ function makeBypassPatches(): Omit<Patch, "plugin"> {
|
|||
return {
|
||||
find: "canUseCustomStickersEverywhere:",
|
||||
replacement: mapping.map(({ func, predicate }) => ({
|
||||
match: new RegExp(String.raw`(?<=${func}:function\(\i(?:,\i)?\){)`),
|
||||
replace: "return true;",
|
||||
match: new RegExp(String.raw`(?<=${func}:)\i`),
|
||||
replace: "() => true",
|
||||
predicate
|
||||
}))
|
||||
};
|
||||
|
@ -235,7 +235,7 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
{
|
||||
find: ".PREMIUM_LOCKED;",
|
||||
find: ".GUILD_SUBSCRIPTION_UNAVAILABLE;",
|
||||
group: true,
|
||||
predicate: () => settings.store.enableEmojiBypass,
|
||||
replacement: [
|
||||
|
@ -256,8 +256,10 @@ export default definePlugin({
|
|||
},
|
||||
{
|
||||
// Disallow the emoji for premium locked if the intention doesn't allow it
|
||||
match: /!\i\.\i\.canUseEmojisEverywhere\(\i\)/,
|
||||
replace: m => `(${m}&&!${IS_BYPASSEABLE_INTENTION})`
|
||||
match: /(!)?(\i\.\i\.canUseEmojisEverywhere\(\i\))/,
|
||||
replace: (m, not) => not
|
||||
? `(${m}&&!${IS_BYPASSEABLE_INTENTION})`
|
||||
: `(${m}||${IS_BYPASSEABLE_INTENTION})`
|
||||
},
|
||||
{
|
||||
// Allow animated emojis to be used if the intention allows it
|
||||
|
@ -297,8 +299,8 @@ export default definePlugin({
|
|||
replacement: [
|
||||
{
|
||||
// Overwrite incoming connection settings proto with our local settings
|
||||
match: /CONNECTION_OPEN:function\((\i)\){/,
|
||||
replace: (m, props) => `${m}$self.handleProtoChange(${props}.userSettingsProto,${props}.user);`
|
||||
match: /function (\i)\((\i)\){(?=.*CONNECTION_OPEN:\1)/,
|
||||
replace: (m, funcName, props) => `${m}$self.handleProtoChange(${props}.userSettingsProto,${props}.user);`
|
||||
},
|
||||
{
|
||||
// Overwrite non local proto changes with our local settings
|
||||
|
@ -391,7 +393,7 @@ export default definePlugin({
|
|||
},
|
||||
// Separate patch for allowing using custom app icons
|
||||
{
|
||||
find: /\.getCurrentDesktopIcon.{0,25}\.isPremium/,
|
||||
find: "?24:30,",
|
||||
replacement: {
|
||||
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
|
||||
replace: "true"
|
||||
|
@ -514,7 +516,7 @@ export default definePlugin({
|
|||
return array.filter(item => item != null);
|
||||
},
|
||||
|
||||
ensureChildrenIsArray(child: ReactElement) {
|
||||
ensureChildrenIsArray(child: ReactElement<any>) {
|
||||
if (!Array.isArray(child.props.children)) child.props.children = [child.props.children];
|
||||
},
|
||||
|
||||
|
@ -524,7 +526,7 @@ export default definePlugin({
|
|||
|
||||
let nextIndex = content.length;
|
||||
|
||||
const transformLinkChild = (child: ReactElement) => {
|
||||
const transformLinkChild = (child: ReactElement<any>) => {
|
||||
if (settings.store.transformEmojis) {
|
||||
const fakeNitroMatch = child.props.href.match(fakeNitroEmojiRegex);
|
||||
if (fakeNitroMatch) {
|
||||
|
@ -558,7 +560,7 @@ export default definePlugin({
|
|||
return child;
|
||||
};
|
||||
|
||||
const transformChild = (child: ReactElement) => {
|
||||
const transformChild = (child: ReactElement<any>) => {
|
||||
if (child?.props?.trusted != null) return transformLinkChild(child);
|
||||
if (child?.props?.children != null) {
|
||||
if (!Array.isArray(child.props.children)) {
|
||||
|
@ -574,7 +576,7 @@ export default definePlugin({
|
|||
return child;
|
||||
};
|
||||
|
||||
const modifyChild = (child: ReactElement) => {
|
||||
const modifyChild = (child: ReactElement<any>) => {
|
||||
const newChild = transformChild(child);
|
||||
|
||||
if (newChild?.type === "ul" || newChild?.type === "ol") {
|
||||
|
@ -601,7 +603,7 @@ export default definePlugin({
|
|||
return newChild;
|
||||
};
|
||||
|
||||
const modifyChildren = (children: Array<ReactElement>) => {
|
||||
const modifyChildren = (children: Array<ReactElement<any>>) => {
|
||||
for (const [index, child] of children.entries()) children[index] = modifyChild(child);
|
||||
|
||||
children = this.clearEmptyArrayItems(children);
|
||||
|
@ -853,7 +855,7 @@ export default definePlugin({
|
|||
});
|
||||
}
|
||||
|
||||
this.preSend = addPreSendListener(async (channelId, messageObj, extra) => {
|
||||
this.preSend = addMessagePreSendListener(async (channelId, messageObj, extra) => {
|
||||
const { guildId } = this;
|
||||
|
||||
let hasBypass = false;
|
||||
|
@ -941,7 +943,7 @@ export default definePlugin({
|
|||
return { cancel: false };
|
||||
});
|
||||
|
||||
this.preEdit = addPreEditListener(async (channelId, __, messageObj) => {
|
||||
this.preEdit = addMessagePreEditListener(async (channelId, __, messageObj) => {
|
||||
if (!s.enableEmojiBypass) return;
|
||||
|
||||
let hasBypass = false;
|
||||
|
@ -973,7 +975,7 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
stop() {
|
||||
removePreSendListener(this.preSend);
|
||||
removePreEditListener(this.preEdit);
|
||||
removeMessagePreSendListener(this.preSend);
|
||||
removeMessagePreEditListener(this.preEdit);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -29,6 +29,7 @@ import definePlugin, { OptionType } from "@utils/types";
|
|||
import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { Button, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
import { ReactElement } from "react";
|
||||
import virtualMerge from "virtual-merge";
|
||||
|
||||
interface UserProfile extends User {
|
||||
|
@ -87,7 +88,7 @@ const settings = definePluginSettings({
|
|||
|
||||
interface ColorPickerProps {
|
||||
color: number | null;
|
||||
label: React.ReactElement;
|
||||
label: ReactElement<any>;
|
||||
showEyeDropper?: boolean;
|
||||
suggestedColors?: string[];
|
||||
onChange(value: number | null): void;
|
||||
|
|
|
@ -57,7 +57,7 @@ export default definePlugin({
|
|||
{
|
||||
// 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)(.{1,500}\i)=(\i)\.slice\(0,(Math\.max\(\i,\i(?:-\i\.length){2}\))\)/,
|
||||
// ,maxCount:Infinity ... endEmojis = (emojis.sliceTo = n, emojis)
|
||||
replace: ",maxCount:Infinity$2=($3.sliceTo = $4, $3)"
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export default definePlugin({
|
|||
authors: [Devs.Nuckyz],
|
||||
patches: [
|
||||
{
|
||||
find: "getFormatQuality(){",
|
||||
find: ".handleImageLoad)",
|
||||
replacement: {
|
||||
match: /(?<=null;return )\i\.\i&&\(\i\|\|!\i\.isAnimated.+?:(?=\i&&\(\i="png"\))/,
|
||||
replace: ""
|
||||
|
|
|
@ -27,7 +27,7 @@ export default definePlugin({
|
|||
authors: [Devs.D3SOX, Devs.Nickyux],
|
||||
patches: [
|
||||
{
|
||||
find: "#{intl::GUILD_OWNER}",
|
||||
find: "#{intl::GUILD_OWNER}),children:",
|
||||
replacement: {
|
||||
match: /,isOwner:(\i),/,
|
||||
replace: ",_isOwner:$1=$self.isGuildOwner(e),"
|
||||
|
|
|
@ -22,11 +22,11 @@ import { Devs } from "@utils/constants";
|
|||
import { getIntlMessage } from "@utils/discord";
|
||||
import { NoopComponent } from "@utils/react";
|
||||
import definePlugin from "@utils/types";
|
||||
import { filters, findByPropsLazy, waitFor } from "@webpack";
|
||||
import { filters, findByCodeLazy, waitFor } from "@webpack";
|
||||
import { ChannelStore, ContextMenuApi, UserStore } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
const { useMessageMenu } = findByPropsLazy("useMessageMenu");
|
||||
const useMessageMenu = findByCodeLazy(".MESSAGE,commandTargetId:");
|
||||
|
||||
interface CopyIdMenuItemProps {
|
||||
id: string;
|
||||
|
|
9
src/plugins/fullUserInChatbox/README.md
Normal file
9
src/plugins/fullUserInChatbox/README.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Full User In Chatbox
|
||||
|
||||
Adds the full user mention to the textbox
|
||||
|
||||
Adds the avatar if you have mentioned avatars enabled
|
||||
|
||||
Provides the full context menu to make it easy to access the users profile, and other common actions
|
||||
|
||||
https://github.com/user-attachments/assets/cd9edb33-99c8-4c8d-b669-8cddd05f4b45
|
47
src/plugins/fullUserInChatbox/index.tsx
Normal file
47
src/plugins/fullUserInChatbox/index.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const UserMentionComponent = findComponentByCodeLazy(".USER_MENTION)");
|
||||
|
||||
interface UserMentionComponentProps {
|
||||
id: string;
|
||||
channelId: string;
|
||||
guildId: string;
|
||||
originalComponent: () => ReactNode;
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "FullUserInChatbox",
|
||||
description: "Makes the user mention in the chatbox have more functionalities, like left/right clicking",
|
||||
authors: [Devs.sadan],
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: ':"text":',
|
||||
replacement: {
|
||||
match: /(hidePersonalInformation\).+?)(if\(null!=\i\){.+?return \i)(?=})/,
|
||||
replace: "$1return $self.UserMentionComponent({...arguments[0],originalComponent:()=>{$2}});"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
UserMentionComponent: ErrorBoundary.wrap((props: UserMentionComponentProps) => (
|
||||
<UserMentionComponent
|
||||
// This seems to be constant
|
||||
className="mention"
|
||||
userId={props.id}
|
||||
channelId={props.channelId}
|
||||
/>
|
||||
), {
|
||||
fallback: ({ wrappedProps: { originalComponent } }) => originalComponent()
|
||||
})
|
||||
});
|
|
@ -26,7 +26,7 @@ import { findComponentByCodeLazy } from "@webpack";
|
|||
|
||||
import style from "./style.css?managed";
|
||||
|
||||
const Button = findComponentByCodeLazy("Button.Sizes.NONE,disabled:");
|
||||
const Button = findComponentByCodeLazy(".NONE,disabled:", ".PANEL_BUTTON");
|
||||
|
||||
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
|
||||
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
*/
|
||||
|
||||
import { get, set } from "@api/DataStore";
|
||||
import { addButton, removeButton } from "@api/MessagePopover";
|
||||
import { ImageInvisible, ImageVisible } from "@components/Icons";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { ChannelStore } from "@webpack/common";
|
||||
import { MessageSnapshot } from "@webpack/types";
|
||||
|
||||
let style: HTMLStyleElement;
|
||||
|
||||
|
@ -38,18 +38,14 @@ export default definePlugin({
|
|||
name: "HideAttachments",
|
||||
description: "Hide attachments and Embeds for individual messages via hover button",
|
||||
authors: [Devs.Ven],
|
||||
dependencies: ["MessagePopoverAPI"],
|
||||
|
||||
async start() {
|
||||
style = document.createElement("style");
|
||||
style.id = "VencordHideAttachments";
|
||||
document.head.appendChild(style);
|
||||
renderMessagePopoverButton(msg) {
|
||||
// @ts-ignore - discord-types lags behind discord.
|
||||
const hasAttachmentsInShapshots = msg.messageSnapshots.some(
|
||||
(snapshot: MessageSnapshot) => snapshot?.message.attachments.length
|
||||
);
|
||||
|
||||
await getHiddenMessages();
|
||||
await this.buildCss();
|
||||
|
||||
addButton("HideAttachments", msg => {
|
||||
if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length) return null;
|
||||
if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length && !hasAttachmentsInShapshots) return null;
|
||||
|
||||
const isHidden = hiddenMessages.has(msg.id);
|
||||
|
||||
|
@ -60,13 +56,20 @@ export default definePlugin({
|
|||
channel: ChannelStore.getChannel(msg.channel_id),
|
||||
onClick: () => this.toggleHide(msg.id)
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
async start() {
|
||||
style = document.createElement("style");
|
||||
style.id = "VencordHideAttachments";
|
||||
document.head.appendChild(style);
|
||||
|
||||
await getHiddenMessages();
|
||||
await this.buildCss();
|
||||
},
|
||||
|
||||
stop() {
|
||||
style.remove();
|
||||
hiddenMessages.clear();
|
||||
removeButton("HideAttachments");
|
||||
},
|
||||
|
||||
async buildCss() {
|
||||
|
|
|
@ -27,7 +27,7 @@ export default definePlugin({
|
|||
{
|
||||
find: "hasFlag:{writable",
|
||||
replacement: {
|
||||
match: /if\((\i)<=(?:1<<30|1073741824)\)return/,
|
||||
match: /if\((\i)<=(?:0x40000000|(?:1<<30|1073741824))\)return/,
|
||||
replace: "if($1===(1<<20))return false;$&",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import * as DataStore from "@api/DataStore";
|
||||
import { definePluginSettings, Settings } from "@api/Settings";
|
||||
import { getUserSettingLazy } from "@api/UserSettings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
|
@ -62,7 +61,7 @@ const ToggleIconOff = (activity: IgnoredActivity, fill: string) => ToggleIcon(ac
|
|||
|
||||
function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) {
|
||||
const s = settings.use(["ignoredActivities"]);
|
||||
const { ignoredActivities = [] } = s;
|
||||
const { ignoredActivities } = s;
|
||||
|
||||
if (ignoredActivities.some(act => act.id === activity.id)) return ToggleIconOff(activity, "var(--status-danger)");
|
||||
return ToggleIconOn(activity, isPlaying ? "var(--green-300)" : "var(--primary-400)");
|
||||
|
@ -71,11 +70,9 @@ function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) {
|
|||
function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>, activity: IgnoredActivity) {
|
||||
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);
|
||||
|
||||
recalculateActivities();
|
||||
const ignoredActivityIndex = settings.store.ignoredActivities.findIndex(act => act.id === activity.id);
|
||||
if (ignoredActivityIndex === -1) settings.store.ignoredActivities.push(activity);
|
||||
else settings.store.ignoredActivities.splice(ignoredActivityIndex, 1);
|
||||
}
|
||||
|
||||
function recalculateActivities() {
|
||||
|
@ -150,8 +147,7 @@ function IdsListComponent(props: { setValue: (value: string) => void; }) {
|
|||
const settings = definePluginSettings({
|
||||
importCustomRPC: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "",
|
||||
component: () => <ImportCustomRPCComponent />
|
||||
component: ImportCustomRPCComponent
|
||||
},
|
||||
listMode: {
|
||||
type: OptionType.SELECT,
|
||||
|
@ -171,7 +167,6 @@ const settings = definePluginSettings({
|
|||
},
|
||||
idsList: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "",
|
||||
default: "",
|
||||
onChange(newValue: string) {
|
||||
const ids = new Set(newValue.split(",").map(id => id.trim()).filter(Boolean));
|
||||
|
@ -209,14 +204,13 @@ const settings = definePluginSettings({
|
|||
description: "Ignore all competing activities (These are normally special game activities)",
|
||||
default: false,
|
||||
onChange: recalculateActivities
|
||||
},
|
||||
ignoredActivities: {
|
||||
type: OptionType.CUSTOM,
|
||||
default: [] as IgnoredActivity[],
|
||||
onChange: recalculateActivities
|
||||
}
|
||||
}).withPrivateSettings<{
|
||||
ignoredActivities: IgnoredActivity[];
|
||||
}>();
|
||||
|
||||
function getIgnoredActivities() {
|
||||
return settings.store.ignoredActivities ??= [];
|
||||
}
|
||||
});
|
||||
|
||||
function isActivityTypeIgnored(type: number, id?: string) {
|
||||
if (id && settings.store.idsList.includes(id)) {
|
||||
|
@ -247,7 +241,7 @@ export default definePlugin({
|
|||
find: '"LocalActivityStore"',
|
||||
replacement: [
|
||||
{
|
||||
match: /HANG_STATUS.+?(?=!\i\(\)\(\i,\i\)&&)(?<=(\i)\.push.+?)/,
|
||||
match: /\.LISTENING.+?(?=!?\i\(\)\(\i,\i\))(?<=(\i)\.push.+?)/,
|
||||
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`
|
||||
}
|
||||
]
|
||||
|
@ -284,29 +278,14 @@ export default definePlugin({
|
|||
],
|
||||
|
||||
async start() {
|
||||
// Migrate allowedIds
|
||||
if (Settings.plugins.IgnoreActivities.allowedIds) {
|
||||
settings.store.idsList = Settings.plugins.IgnoreActivities.allowedIds;
|
||||
delete Settings.plugins.IgnoreActivities.allowedIds; // Remove allowedIds
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
if (getIgnoredActivities().length !== 0) {
|
||||
if (settings.store.ignoredActivities.length !== 0) {
|
||||
const gamesSeen = RunningGameStore.getGamesSeen() as { id?: string; exePath: string; }[];
|
||||
|
||||
for (const [index, ignoredActivity] of getIgnoredActivities().entries()) {
|
||||
for (const [index, ignoredActivity] of settings.store.ignoredActivities.entries()) {
|
||||
if (ignoredActivity.type !== ActivitiesTypes.Game) continue;
|
||||
|
||||
if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) {
|
||||
getIgnoredActivities().splice(index, 1);
|
||||
settings.store.ignoredActivities.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -316,11 +295,11 @@ export default definePlugin({
|
|||
if (isActivityTypeIgnored(props.type, props.application_id)) return false;
|
||||
|
||||
if (props.application_id != null) {
|
||||
return !getIgnoredActivities().some(activity => activity.id === props.application_id) || (settings.store.listMode === FilterMode.Whitelist && settings.store.idsList.includes(props.application_id));
|
||||
return !settings.store.ignoredActivities.some(activity => activity.id === props.application_id) || (settings.store.listMode === FilterMode.Whitelist && settings.store.idsList.includes(props.application_id));
|
||||
} else {
|
||||
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
|
||||
if (exePath) {
|
||||
return !getIgnoredActivities().some(activity => activity.id === exePath);
|
||||
return !settings.store.ignoredActivities.some(activity => activity.id === exePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { FluxDispatcher, React, useRef, useState } from "@webpack/common";
|
||||
import { FluxDispatcher, useLayoutEffect, useRef, useState } from "@webpack/common";
|
||||
|
||||
import { ELEMENT_ID } from "../constants";
|
||||
import { settings } from "../index";
|
||||
|
@ -55,7 +55,7 @@ export const Magnifier = ErrorBoundary.wrap<MagnifierProps>(({ instance, size: i
|
|||
const imageRef = useRef<HTMLImageElement | null>(null);
|
||||
|
||||
// since we accessing document im gonna use useLayoutEffect
|
||||
React.useLayoutEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Shift") {
|
||||
isShiftDown.current = true;
|
||||
|
@ -135,12 +135,14 @@ export const Magnifier = ErrorBoundary.wrap<MagnifierProps>(({ instance, size: i
|
|||
|
||||
setReady(true);
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
document.addEventListener("keyup", onKeyUp);
|
||||
document.addEventListener("mousemove", updateMousePosition);
|
||||
document.addEventListener("mousedown", onMouseDown);
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
document.addEventListener("wheel", onWheel);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
document.removeEventListener("keyup", onKeyUp);
|
||||
|
@ -148,13 +150,15 @@ export const Magnifier = ErrorBoundary.wrap<MagnifierProps>(({ instance, size: i
|
|||
document.removeEventListener("mousedown", onMouseDown);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
document.removeEventListener("wheel", onWheel);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => () => {
|
||||
if (settings.store.saveZoomValues) {
|
||||
settings.store.zoom = zoom.current;
|
||||
settings.store.size = size.current;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
|
||||
if (!ready) return null;
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import { debounce } from "@shared/debounce";
|
|||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Menu, ReactDOM } from "@webpack/common";
|
||||
import { JSX } from "react";
|
||||
import type { Root } from "react-dom/client";
|
||||
|
||||
import { Magnifier, MagnifierProps } from "./components/Magnifier";
|
||||
|
@ -80,7 +81,12 @@ export const settings = definePluginSettings({
|
|||
});
|
||||
|
||||
|
||||
const imageContextMenuPatch: NavContextMenuPatchCallback = children => {
|
||||
const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
|
||||
// Discord re-uses the image context menu for links to for the copy and open buttons
|
||||
if ("href" in props) return;
|
||||
// emojis in user statuses
|
||||
if (props.target?.classList?.contains("emoji")) return;
|
||||
|
||||
const { square, nearestNeighbour } = settings.use(["square", "nearestNeighbour"]);
|
||||
|
||||
children.push(
|
||||
|
|
|
@ -50,7 +50,7 @@ export default definePlugin({
|
|||
{
|
||||
find: "#{intl::FRIENDS_SECTION_ONLINE}",
|
||||
replacement: {
|
||||
match: /(\(0,\i\.jsx\)\(\i\.TabBar\.Item,\{id:\i\.\i)\.BLOCKED,className:([^\s]+?)\.item,children:\i\.\i\.string\(\i\.\i#{intl::BLOCKED}\)\}\)/,
|
||||
match: /(\(0,\i\.jsx\)\(\i\.\i\.Item,\{id:\i\.\i)\.BLOCKED,className:([^\s]+?)\.item,children:\i\.\i\.string\(\i\.\i#{intl::BLOCKED}\)\}\)/,
|
||||
replace: "$1.IMPLICIT,className:$2.item,children:\"Implicit\"}),$&"
|
||||
},
|
||||
},
|
||||
|
|
|
@ -16,12 +16,19 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { addProfileBadge, removeProfileBadge } from "@api/Badges";
|
||||
import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons";
|
||||
import { registerCommand, unregisterCommand } from "@api/Commands";
|
||||
import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
|
||||
import { Settings } from "@api/Settings";
|
||||
import { addMemberListDecorator, removeMemberListDecorator } from "@api/MemberListDecorators";
|
||||
import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccessories";
|
||||
import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations";
|
||||
import { addMessageClickListener, addMessagePreEditListener, addMessagePreSendListener, removeMessageClickListener, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents";
|
||||
import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover";
|
||||
import { Settings, SettingsStore } from "@api/Settings";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { canonicalizeFind } from "@utils/patches";
|
||||
import { Patch, Plugin, ReporterTestable, StartAt } from "@utils/types";
|
||||
import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types";
|
||||
import { FluxDispatcher } from "@webpack/common";
|
||||
import { FluxEvents } from "@webpack/types";
|
||||
|
||||
|
@ -83,6 +90,13 @@ function isReporterTestable(p: Plugin, part: ReporterTestable) {
|
|||
: (p.reporterTestable & part) === part;
|
||||
}
|
||||
|
||||
const pluginKeysToBind: Array<keyof PluginDef & `${"on" | "render"}${string}`> = [
|
||||
"onBeforeMessageEdit", "onBeforeMessageSend", "onMessageClick",
|
||||
"renderChatBarButton", "renderMemberListDecorator", "renderMessageAccessory", "renderMessageDecoration", "renderMessagePopoverButton"
|
||||
];
|
||||
|
||||
const neededApiPlugins = new Set<string>();
|
||||
|
||||
// First round-trip to mark and force enable dependencies
|
||||
//
|
||||
// FIXME: might need to revisit this if there's ever nested (dependencies of dependencies) dependencies since this only
|
||||
|
@ -106,22 +120,46 @@ for (const p of pluginsValues) if (isPluginEnabled(p.name)) {
|
|||
dep.isDependency = true;
|
||||
});
|
||||
|
||||
if (p.commands?.length) {
|
||||
Plugins.CommandsAPI.isDependency = true;
|
||||
settings.CommandsAPI.enabled = true;
|
||||
if (p.commands?.length) neededApiPlugins.add("CommandsAPI");
|
||||
if (p.onBeforeMessageEdit || p.onBeforeMessageSend || p.onMessageClick) neededApiPlugins.add("MessageEventsAPI");
|
||||
if (p.renderChatBarButton) neededApiPlugins.add("ChatInputButtonAPI");
|
||||
if (p.renderMemberListDecorator) neededApiPlugins.add("MemberListDecoratorsAPI");
|
||||
if (p.renderMessageAccessory) neededApiPlugins.add("MessageAccessoriesAPI");
|
||||
if (p.renderMessageDecoration) neededApiPlugins.add("MessageDecorationsAPI");
|
||||
if (p.renderMessagePopoverButton) neededApiPlugins.add("MessagePopoverAPI");
|
||||
if (p.userProfileBadge) neededApiPlugins.add("BadgeAPI");
|
||||
|
||||
for (const key of pluginKeysToBind) {
|
||||
p[key] &&= p[key].bind(p) as any;
|
||||
}
|
||||
}
|
||||
|
||||
for (const p of neededApiPlugins) {
|
||||
Plugins[p].isDependency = true;
|
||||
settings[p].enabled = true;
|
||||
}
|
||||
|
||||
for (const p of pluginsValues) {
|
||||
if (p.settings) {
|
||||
p.settings.pluginName = p.name;
|
||||
p.options ??= {};
|
||||
for (const [name, def] of Object.entries(p.settings.def)) {
|
||||
|
||||
p.settings.pluginName = p.name;
|
||||
for (const name in p.settings.def) {
|
||||
const def = p.settings.def[name];
|
||||
const checks = p.settings.checks?.[name];
|
||||
p.options[name] = { ...def, ...checks };
|
||||
}
|
||||
}
|
||||
|
||||
if (p.options) {
|
||||
for (const name in p.options) {
|
||||
const opt = p.options[name];
|
||||
if (opt.onChange != null) {
|
||||
SettingsStore.addChangeListener(`plugins.${p.name}.${name}`, opt.onChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (p.patches && isPluginEnabled(p.name)) {
|
||||
if (!IS_REPORTER || isReporterTestable(p, ReporterTestable.Patches)) {
|
||||
for (const patch of p.patches) {
|
||||
|
@ -215,7 +253,11 @@ export function subscribeAllPluginsFluxEvents(fluxDispatcher: typeof FluxDispatc
|
|||
}
|
||||
|
||||
export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) {
|
||||
const { name, commands, contextMenus } = p;
|
||||
const {
|
||||
name, commands, contextMenus, userProfileBadge,
|
||||
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
|
||||
renderChatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton
|
||||
} = p;
|
||||
|
||||
if (p.start) {
|
||||
logger.info("Starting plugin", name);
|
||||
|
@ -249,7 +291,6 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
|||
subscribePluginFluxEvents(p, FluxDispatcher);
|
||||
}
|
||||
|
||||
|
||||
if (contextMenus) {
|
||||
logger.debug("Adding context menus patches of plugin", name);
|
||||
for (const navId in contextMenus) {
|
||||
|
@ -257,11 +298,27 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
|||
}
|
||||
}
|
||||
|
||||
if (userProfileBadge) addProfileBadge(userProfileBadge);
|
||||
|
||||
if (onBeforeMessageEdit) addMessagePreEditListener(onBeforeMessageEdit);
|
||||
if (onBeforeMessageSend) addMessagePreSendListener(onBeforeMessageSend);
|
||||
if (onMessageClick) addMessageClickListener(onMessageClick);
|
||||
|
||||
if (renderChatBarButton) addChatBarButton(name, renderChatBarButton);
|
||||
if (renderMemberListDecorator) addMemberListDecorator(name, renderMemberListDecorator);
|
||||
if (renderMessageDecoration) addMessageDecoration(name, renderMessageDecoration);
|
||||
if (renderMessageAccessory) addMessageAccessory(name, renderMessageAccessory);
|
||||
if (renderMessagePopoverButton) addMessagePopoverButton(name, renderMessagePopoverButton);
|
||||
|
||||
return true;
|
||||
}, p => `startPlugin ${p.name}`);
|
||||
|
||||
export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) {
|
||||
const { name, commands, contextMenus } = p;
|
||||
const {
|
||||
name, commands, contextMenus, userProfileBadge,
|
||||
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
|
||||
renderChatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton
|
||||
} = p;
|
||||
|
||||
if (p.stop) {
|
||||
logger.info("Stopping plugin", name);
|
||||
|
@ -300,5 +357,17 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
|
|||
}
|
||||
}
|
||||
|
||||
if (userProfileBadge) removeProfileBadge(userProfileBadge);
|
||||
|
||||
if (onBeforeMessageEdit) removeMessagePreEditListener(onBeforeMessageEdit);
|
||||
if (onBeforeMessageSend) removeMessagePreSendListener(onBeforeMessageSend);
|
||||
if (onMessageClick) removeMessageClickListener(onMessageClick);
|
||||
|
||||
if (renderChatBarButton) removeChatBarButton(name);
|
||||
if (renderMemberListDecorator) removeMemberListDecorator(name);
|
||||
if (renderMessageDecoration) removeMessageDecoration(name);
|
||||
if (renderMessageAccessory) removeMessageAccessory(name);
|
||||
if (renderMessagePopoverButton) removeMessagePopoverButton(name);
|
||||
|
||||
return true;
|
||||
}, p => `stopPlugin ${p.name}`);
|
||||
|
|
|
@ -16,8 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { addChatBarButton, ChatBarButton } from "@api/ChatButtons";
|
||||
import { addButton, removeButton } from "@api/MessagePopover";
|
||||
import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
|
||||
import { updateMessage } from "@api/MessageUpdater";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
|
@ -66,7 +65,7 @@ function Indicator() {
|
|||
|
||||
}
|
||||
|
||||
const ChatBarIcon: ChatBarButton = ({ isMainChat }) => {
|
||||
const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
if (!isMainChat) return null;
|
||||
|
||||
return (
|
||||
|
@ -104,7 +103,7 @@ export default definePlugin({
|
|||
name: "InvisibleChat",
|
||||
description: "Encrypt your Messages in a non-suspicious way!",
|
||||
authors: [Devs.SammCheese],
|
||||
dependencies: ["MessagePopoverAPI", "ChatInputButtonAPI", "MessageUpdaterAPI"],
|
||||
dependencies: ["MessageUpdaterAPI"],
|
||||
reporterTestable: ReporterTestable.Patches,
|
||||
settings,
|
||||
|
||||
|
@ -125,7 +124,11 @@ export default definePlugin({
|
|||
/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/,
|
||||
),
|
||||
async start() {
|
||||
addButton("InvisibleChat", message => {
|
||||
const { default: StegCloak } = await getStegCloak();
|
||||
steggo = new StegCloak(true, false);
|
||||
},
|
||||
|
||||
renderMessagePopoverButton(message) {
|
||||
return this.INV_REGEX.test(message?.content)
|
||||
? {
|
||||
label: "Decrypt Message",
|
||||
|
@ -142,18 +145,9 @@ export default definePlugin({
|
|||
}
|
||||
}
|
||||
: null;
|
||||
});
|
||||
|
||||
addChatBarButton("InvisibleChat", ChatBarIcon);
|
||||
|
||||
const { default: StegCloak } = await getStegCloak();
|
||||
steggo = new StegCloak(true, false);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeButton("InvisibleChat");
|
||||
removeButton("InvisibleChat");
|
||||
},
|
||||
renderChatBarButton: ChatBarIcon,
|
||||
|
||||
// Gets the Embed of a Link
|
||||
async getEmbed(url: URL): Promise<Object | {}> {
|
||||
|
|
17
src/plugins/ircColors/README.md
Normal file
17
src/plugins/ircColors/README.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
# IrcColors
|
||||
|
||||
Makes username colors in chat unique, like in IRC clients
|
||||
|
||||

|
||||
|
||||
Improves chat readability by assigning every user an unique nickname color,
|
||||
making distinguishing between different users easier. Inspired by the feature
|
||||
in many IRC clients, such as HexChat or WeeChat.
|
||||
|
||||
Keep in mind this overrides role colors in chat, so if you wish to know
|
||||
someone's role color without checking their profile, enable the role dot: go to
|
||||
**User Settings**, **Accessibility** and switch **Role Colors** to **Show role
|
||||
colors next to names**.
|
||||
|
||||
Created for use with the [Compact++](https://gitlab.com/Grzesiek11/compactplusplus-discord-theme)
|
||||
theme.
|
85
src/plugins/ircColors/index.ts
Normal file
85
src/plugins/ircColors/index.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { hash as h64 } from "@intrnl/xxhash64";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { useMemo } from "@webpack/common";
|
||||
|
||||
// Calculate a CSS color string based on the user ID
|
||||
function calculateNameColorForUser(id: string) {
|
||||
const { lightness } = settings.use(["lightness"]);
|
||||
const idHash = useMemo(() => h64(id), [id]);
|
||||
|
||||
return `hsl(${idHash % 360n}, 100%, ${lightness}%)`;
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
lightness: {
|
||||
description: "Lightness, in %. Change if the colors are too light or too dark",
|
||||
type: OptionType.NUMBER,
|
||||
default: 70,
|
||||
},
|
||||
memberListColors: {
|
||||
description: "Replace role colors in the member list",
|
||||
restartNeeded: true,
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "IrcColors",
|
||||
description: "Makes username colors in chat unique, like in IRC clients",
|
||||
authors: [Devs.Grzesiek11],
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: '="SYSTEM_TAG"',
|
||||
replacement: {
|
||||
match: /(?<=className:\i\.username,style:.{0,50}:void 0,)/,
|
||||
replace: "style:{color:$self.calculateNameColorForMessageContext(arguments[0])},"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "#{intl::GUILD_OWNER}),children:",
|
||||
replacement: {
|
||||
match: /(?<=\.MEMBER_LIST}\),\[\]\),)(.+?color:)null!=.{0,50}?(?=,)/,
|
||||
replace: (_, rest) => `ircColor=$self.calculateNameColorForListContext(arguments[0]),${rest}ircColor`
|
||||
},
|
||||
predicate: () => settings.store.memberListColors
|
||||
}
|
||||
],
|
||||
|
||||
calculateNameColorForMessageContext(context: any) {
|
||||
const id = context?.message?.author?.id;
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
return calculateNameColorForUser(id);
|
||||
},
|
||||
calculateNameColorForListContext(context: any) {
|
||||
const id = context?.user?.id;
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
return calculateNameColorForUser(id);
|
||||
}
|
||||
});
|
|
@ -86,7 +86,7 @@ const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f";
|
|||
|
||||
const logger = new Logger("LastFMRichPresence");
|
||||
|
||||
const presenceStore = findByPropsLazy("getLocalPresence");
|
||||
const PresenceStore = findByPropsLazy("getLocalPresence");
|
||||
|
||||
async function getApplicationAsset(key: string): Promise<string> {
|
||||
return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];
|
||||
|
@ -124,6 +124,11 @@ const settings = definePluginSettings({
|
|||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
hideWithActivity: {
|
||||
description: "Hide Last.fm presence if you have any other presence",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
},
|
||||
statusName: {
|
||||
description: "custom status text",
|
||||
type: OptionType.STRING,
|
||||
|
@ -274,13 +279,17 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
async getActivity(): Promise<Activity | null> {
|
||||
if (settings.store.hideWithSpotify) {
|
||||
for (const activity of presenceStore.getActivities()) {
|
||||
if (activity.type === ActivityType.LISTENING && activity.application_id !== applicationId) {
|
||||
// there is already music status because of Spotify or richerCider (probably more)
|
||||
if (settings.store.hideWithActivity) {
|
||||
if (PresenceStore.getActivities().some(a => a.application_id !== applicationId)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.store.hideWithSpotify) {
|
||||
if (PresenceStore.getActivities().some(a => a.type === ActivityType.LISTENING && a.application_id !== applicationId)) {
|
||||
// there is already music status because of Spotify or richerCider (probably more)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const trackData = await this.fetchTrackData();
|
||||
|
|
|
@ -57,7 +57,7 @@ export default definePlugin({
|
|||
{
|
||||
find: ".ROLE_MENTION)",
|
||||
replacement: {
|
||||
match: /children:\[\i&&.{0,50}\.RoleDot.{0,300},\i(?=\])/,
|
||||
match: /children:\[\i&&.{0,100}className:\i.roleDot,.{0,200},\i(?=\])/,
|
||||
replace: "$&,$self.renderRoleIcon(arguments[0])"
|
||||
}
|
||||
}],
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { addClickListener, removeClickListener } from "@api/MessageEvents";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
@ -57,15 +56,20 @@ export default definePlugin({
|
|||
name: "MessageClickActions",
|
||||
description: "Hold Backspace and click to delete, double click to edit/reply",
|
||||
authors: [Devs.Ven],
|
||||
dependencies: ["MessageEventsAPI"],
|
||||
|
||||
settings,
|
||||
|
||||
start() {
|
||||
document.addEventListener("keydown", keydown);
|
||||
document.addEventListener("keyup", keyup);
|
||||
},
|
||||
|
||||
this.onClick = addClickListener((msg: any, channel, event) => {
|
||||
stop() {
|
||||
document.removeEventListener("keydown", keydown);
|
||||
document.removeEventListener("keyup", keyup);
|
||||
},
|
||||
|
||||
onMessageClick(msg: any, channel, event) {
|
||||
const isMe = msg.author.id === UserStore.getCurrentUser().id;
|
||||
if (!isDeletePressed) {
|
||||
if (event.detail < 2) return;
|
||||
|
@ -111,12 +115,5 @@ export default definePlugin({
|
|||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeClickListener(this.onClick);
|
||||
document.removeEventListener("keydown", keydown);
|
||||
document.removeEventListener("keyup", keyup);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||
import { Devs } from "@utils/constants";
|
||||
import { isNonNullish } from "@utils/guards";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findExportedComponentLazy } from "@webpack";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { SnowflakeUtils, Tooltip } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
|
@ -26,7 +26,7 @@ interface Diff {
|
|||
}
|
||||
|
||||
const DISCORD_KT_DELAY = 1471228928;
|
||||
const HiddenVisually = findExportedComponentLazy("HiddenVisually");
|
||||
const HiddenVisually = findComponentByCodeLazy(".hiddenVisually]:");
|
||||
|
||||
export default definePlugin({
|
||||
name: "MessageLatency",
|
||||
|
@ -162,7 +162,7 @@ export default definePlugin({
|
|||
</>
|
||||
}
|
||||
</Tooltip>;
|
||||
});
|
||||
}, { noop: true });
|
||||
},
|
||||
|
||||
Icon({ delta, fill, props }: {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
|
||||
import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccessories";
|
||||
import { updateMessage } from "@api/MessageUpdater";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { getUserSettingLazy } from "@api/UserSettings";
|
||||
|
@ -41,6 +41,7 @@ import {
|
|||
UserStore
|
||||
} from "@webpack/common";
|
||||
import { Channel, Message } from "discord-types/general";
|
||||
import { JSX } from "react";
|
||||
|
||||
const messageCache = new Map<string, {
|
||||
message?: Message;
|
||||
|
@ -119,11 +120,11 @@ const settings = definePluginSettings({
|
|||
},
|
||||
clearMessageCache: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Clear the linked message cache",
|
||||
component: () =>
|
||||
component: () => (
|
||||
<Button onClick={() => messageCache.clear()}>
|
||||
Clear the linked message cache
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -347,10 +348,10 @@ function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
|||
? parse(message.content)
|
||||
: [noContent(message.attachments.length, message.embeds.length)]
|
||||
}
|
||||
{images.map(a => {
|
||||
{images.map((a, idx) => {
|
||||
const { width, height } = computeWidthAndHeight(a.width, a.height);
|
||||
return (
|
||||
<div>
|
||||
<div key={idx}>
|
||||
<img src={a.url} width={width} height={height} />
|
||||
</div>
|
||||
);
|
||||
|
@ -372,7 +373,7 @@ export default definePlugin({
|
|||
settings,
|
||||
|
||||
start() {
|
||||
addAccessory("messageLinkEmbed", props => {
|
||||
addMessageAccessory("messageLinkEmbed", props => {
|
||||
if (!messageLinkRegex.test(props.message.content))
|
||||
return null;
|
||||
|
||||
|
@ -390,6 +391,6 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
stop() {
|
||||
removeAccessory("messageLinkEmbed");
|
||||
removeMessageAccessory("messageLinkEmbed");
|
||||
}
|
||||
});
|
||||
|
|
|
@ -69,6 +69,7 @@ export function HistoryModal({ modalProps, message }: { modalProps: ModalProps;
|
|||
|
||||
{timestamps.map((timestamp, index) => (
|
||||
<TabBar.Item
|
||||
key={index}
|
||||
className="vc-settings-tab-bar-item"
|
||||
id={index}
|
||||
>
|
||||
|
|
|
@ -169,8 +169,8 @@ export default definePlugin({
|
|||
|
||||
return Settings.plugins.MessageLogger.inlineEdits && (
|
||||
<>
|
||||
{message.editHistory?.map(edit => (
|
||||
<div className="messagelogger-edited">
|
||||
{message.editHistory?.map((edit, idx) => (
|
||||
<div key={idx} className="messagelogger-edited">
|
||||
{parseEditContent(edit.content, message)}
|
||||
<Timestamp
|
||||
timestamp={edit.timestamp}
|
||||
|
@ -211,7 +211,8 @@ export default definePlugin({
|
|||
collapseDeleted: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether to collapse deleted messages, similar to blocked messages",
|
||||
default: false
|
||||
default: false,
|
||||
restartNeeded: true,
|
||||
},
|
||||
logEdits: {
|
||||
type: OptionType.BOOLEAN,
|
||||
|
@ -304,7 +305,7 @@ export default definePlugin({
|
|||
{...props}
|
||||
className={classes("messagelogger-edit-marker", className)}
|
||||
onClick={() => openHistoryModal(message)}
|
||||
aria-role="button"
|
||||
role="button"
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
|
@ -346,35 +347,35 @@ 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: /function (?=.+?MESSAGE_DELETE:(\i))\1\((\i)\){let.+?((?:\i\.){2})getOrCreate.+?}(?=function)/,
|
||||
replace:
|
||||
"MESSAGE_DELETE:function($1){" +
|
||||
" var cache = $2getOrCreate($1.channelId);" +
|
||||
" cache = $self.handleDelete(cache, $1, false);" +
|
||||
" $2commit(cache);" +
|
||||
"},"
|
||||
"function $1($2){" +
|
||||
" var cache = $3getOrCreate($2.channelId);" +
|
||||
" cache = $self.handleDelete(cache, $2, false);" +
|
||||
" $3commit(cache);" +
|
||||
"}"
|
||||
},
|
||||
{
|
||||
// Add deleted=true to all target messages in the MESSAGE_DELETE_BULK event
|
||||
match: /MESSAGE_DELETE_BULK:function\((\i)\){let.+?((?:\i\.){2})getOrCreate.+?},/,
|
||||
match: /function (?=.+?MESSAGE_DELETE_BULK:(\i))\1\((\i)\){let.+?((?:\i\.){2})getOrCreate.+?}(?=function)/,
|
||||
replace:
|
||||
"MESSAGE_DELETE_BULK:function($1){" +
|
||||
" var cache = $2getOrCreate($1.channelId);" +
|
||||
" cache = $self.handleDelete(cache, $1, true);" +
|
||||
" $2commit(cache);" +
|
||||
"},"
|
||||
"function $1($2){" +
|
||||
" var cache = $3getOrCreate($2.channelId);" +
|
||||
" cache = $self.handleDelete(cache, $2, true);" +
|
||||
" $3commit(cache);" +
|
||||
"}"
|
||||
},
|
||||
{
|
||||
// Add current cached content + new edit time to cached message's editHistory
|
||||
match: /(MESSAGE_UPDATE:function\((\i)\).+?)\.update\((\i)/,
|
||||
match: /(function (\i)\((\i)\).+?)\.update\((\i)(?=.*MESSAGE_UPDATE:\2)/,
|
||||
replace: "$1" +
|
||||
".update($3,m =>" +
|
||||
" (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message, true)) ? m :" +
|
||||
" $2.message.edited_timestamp && $2.message.content !== m.content ?" +
|
||||
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
|
||||
".update($4,m =>" +
|
||||
" (($3.message.flags & 64) === 64 || $self.shouldIgnore($3.message, true)) ? m :" +
|
||||
" $3.message.edited_timestamp && $3.message.content !== m.content ?" +
|
||||
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($3.message, m)]) :" +
|
||||
" m" +
|
||||
")" +
|
||||
".update($3"
|
||||
".update($4"
|
||||
},
|
||||
{
|
||||
// fix up key (edit last message) attempting to edit a deleted message
|
||||
|
@ -488,19 +489,19 @@ export default definePlugin({
|
|||
find: '"ReferencedMessageStore"',
|
||||
replacement: [
|
||||
{
|
||||
match: /MESSAGE_DELETE:function\((\i)\).+?},/,
|
||||
replace: "MESSAGE_DELETE:function($1){},"
|
||||
match: /MESSAGE_DELETE:\i,/,
|
||||
replace: "MESSAGE_DELETE:()=>{},"
|
||||
},
|
||||
{
|
||||
match: /MESSAGE_DELETE_BULK:function\((\i)\).+?},/,
|
||||
replace: "MESSAGE_DELETE_BULK:function($1){},"
|
||||
match: /MESSAGE_DELETE_BULK:\i,/,
|
||||
replace: "MESSAGE_DELETE_BULK:()=>{},"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
// Message context base menu
|
||||
find: "useMessageMenu:",
|
||||
find: ".MESSAGE,commandTargetId:",
|
||||
replacement: [
|
||||
{
|
||||
// Remove the first section if message is deleted
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage, unregisterCommand } from "@api/Commands";
|
||||
import * as DataStore from "@api/DataStore";
|
||||
import { Settings } from "@api/Settings";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
|
@ -29,23 +29,23 @@ const MessageTagsMarker = Symbol("MessageTags");
|
|||
interface Tag {
|
||||
name: string;
|
||||
message: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
const getTags = () => DataStore.get(DATA_KEY).then<Tag[]>(t => t ?? []);
|
||||
const getTag = (name: string) => DataStore.get(DATA_KEY).then<Tag | null>((t: Tag[]) => (t ?? []).find((tt: Tag) => tt.name === name) ?? null);
|
||||
const addTag = async (tag: Tag) => {
|
||||
const tags = await getTags();
|
||||
tags.push(tag);
|
||||
DataStore.set(DATA_KEY, tags);
|
||||
return tags;
|
||||
};
|
||||
const removeTag = async (name: string) => {
|
||||
let tags = await getTags();
|
||||
tags = await tags.filter((t: Tag) => t.name !== name);
|
||||
DataStore.set(DATA_KEY, tags);
|
||||
return tags;
|
||||
};
|
||||
function getTags() {
|
||||
return settings.store.tagsList;
|
||||
}
|
||||
|
||||
function getTag(name: string) {
|
||||
return settings.store.tagsList[name] ?? null;
|
||||
}
|
||||
|
||||
function addTag(tag: Tag) {
|
||||
settings.store.tagsList[tag.name] = tag;
|
||||
}
|
||||
|
||||
function removeTag(name: string) {
|
||||
delete settings.store.tagsList[name];
|
||||
}
|
||||
|
||||
function createTagCommand(tag: Tag) {
|
||||
registerCommand({
|
||||
|
@ -53,14 +53,14 @@ function createTagCommand(tag: Tag) {
|
|||
description: tag.name,
|
||||
inputType: ApplicationCommandInputType.BUILT_IN_TEXT,
|
||||
execute: async (_, ctx) => {
|
||||
if (!await getTag(tag.name)) {
|
||||
if (!getTag(tag.name)) {
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
content: `${EMOTE} The tag **${tag.name}** does not exist anymore! Please reload ur Discord to fix :)`
|
||||
});
|
||||
return { content: `/${tag.name}` };
|
||||
}
|
||||
|
||||
if (Settings.plugins.MessageTags.clyde) sendBotMessage(ctx.channel.id, {
|
||||
if (settings.store.clyde) sendBotMessage(ctx.channel.id, {
|
||||
content: `${EMOTE} The tag **${tag.name}** has been sent!`
|
||||
});
|
||||
return { content: tag.message.replaceAll("\\n", "\n") };
|
||||
|
@ -69,22 +69,38 @@ function createTagCommand(tag: Tag) {
|
|||
}, "CustomTags");
|
||||
}
|
||||
|
||||
|
||||
export default definePlugin({
|
||||
name: "MessageTags",
|
||||
description: "Allows you to save messages and to use them with a simple command.",
|
||||
authors: [Devs.Luna],
|
||||
options: {
|
||||
const settings = definePluginSettings({
|
||||
clyde: {
|
||||
name: "Clyde message on send",
|
||||
description: "If enabled, clyde will send you an ephemeral message when a tag was used.",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
tagsList: {
|
||||
type: OptionType.CUSTOM,
|
||||
default: {} as Record<string, Tag>,
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "MessageTags",
|
||||
description: "Allows you to save messages and to use them with a simple command.",
|
||||
authors: [Devs.Luna],
|
||||
settings,
|
||||
|
||||
async start() {
|
||||
for (const tag of await getTags()) createTagCommand(tag);
|
||||
// TODO: Remove DataStore tags migration once enough time has passed
|
||||
const oldTags = await DataStore.get<Tag[]>(DATA_KEY);
|
||||
if (oldTags != null) {
|
||||
// @ts-ignore
|
||||
settings.store.tagsList = Object.fromEntries(oldTags.map(oldTag => (delete oldTag.enabled, [oldTag.name, oldTag])));
|
||||
await DataStore.del(DATA_KEY);
|
||||
}
|
||||
|
||||
const tags = getTags();
|
||||
for (const tagName in tags) {
|
||||
createTagCommand(tags[tagName]);
|
||||
}
|
||||
},
|
||||
|
||||
commands: [
|
||||
|
@ -153,19 +169,18 @@ export default definePlugin({
|
|||
const name: string = findOption(args[0].options, "tag-name", "");
|
||||
const message: string = findOption(args[0].options, "message", "");
|
||||
|
||||
if (await getTag(name))
|
||||
if (getTag(name))
|
||||
return sendBotMessage(ctx.channel.id, {
|
||||
content: `${EMOTE} A Tag with the name **${name}** already exists!`
|
||||
});
|
||||
|
||||
const tag = {
|
||||
name: name,
|
||||
enabled: true,
|
||||
message: message
|
||||
};
|
||||
|
||||
createTagCommand(tag);
|
||||
await addTag(tag);
|
||||
addTag(tag);
|
||||
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
content: `${EMOTE} Successfully created the tag **${name}**!`
|
||||
|
@ -175,13 +190,13 @@ export default definePlugin({
|
|||
case "delete": {
|
||||
const name: string = findOption(args[0].options, "tag-name", "");
|
||||
|
||||
if (!await getTag(name))
|
||||
if (!getTag(name))
|
||||
return sendBotMessage(ctx.channel.id, {
|
||||
content: `${EMOTE} A Tag with the name **${name}** does not exist!`
|
||||
});
|
||||
|
||||
unregisterCommand(name);
|
||||
await removeTag(name);
|
||||
removeTag(name);
|
||||
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
content: `${EMOTE} Successfully deleted the tag **${name}**!`
|
||||
|
@ -192,10 +207,8 @@ export default definePlugin({
|
|||
sendBotMessage(ctx.channel.id, {
|
||||
embeds: [
|
||||
{
|
||||
// @ts-ignore
|
||||
title: "All Tags:",
|
||||
// @ts-ignore
|
||||
description: (await getTags())
|
||||
description: Object.values(getTags())
|
||||
.map(tag => `\`${tag.name}\`: ${tag.message.slice(0, 72).replaceAll("\\n", " ")}${tag.message.length > 72 ? "..." : ""}`)
|
||||
.join("\n") || `${EMOTE} Woops! There are no tags yet, use \`/tags create\` to create one!`,
|
||||
// @ts-ignore
|
||||
|
@ -208,7 +221,7 @@ export default definePlugin({
|
|||
}
|
||||
case "preview": {
|
||||
const name: string = findOption(args[0].options, "tag-name", "");
|
||||
const tag = await getTag(name);
|
||||
const tag = getTag(name);
|
||||
|
||||
if (!tag)
|
||||
return sendBotMessage(ctx.channel.id, {
|
||||
|
|
|
@ -1,374 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getIntlMessage } from "@utils/discord";
|
||||
import { Margins } from "@utils/margins";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByCodeLazy, findLazy } from "@webpack";
|
||||
import { Card, ChannelStore, Forms, GuildStore, PermissionsBits, Switch, TextInput, Tooltip } from "@webpack/common";
|
||||
import type { Permissions, RC } from "@webpack/types";
|
||||
import type { Channel, Guild, Message, User } from "discord-types/general";
|
||||
|
||||
interface Tag {
|
||||
// name used for identifying, must be alphanumeric + underscores
|
||||
name: string;
|
||||
// name shown on the tag itself, can be anything probably; automatically uppercase'd
|
||||
displayName: string;
|
||||
description: string;
|
||||
permissions?: Permissions[];
|
||||
condition?(message: Message | null, user: User, channel: Channel): boolean;
|
||||
}
|
||||
|
||||
interface TagSetting {
|
||||
text: string;
|
||||
showInChat: boolean;
|
||||
showInNotChat: boolean;
|
||||
}
|
||||
interface TagSettings {
|
||||
WEBHOOK: TagSetting,
|
||||
OWNER: TagSetting,
|
||||
ADMINISTRATOR: TagSetting,
|
||||
MODERATOR_STAFF: TagSetting,
|
||||
MODERATOR: TagSetting,
|
||||
VOICE_MODERATOR: TagSetting,
|
||||
TRIAL_MODERATOR: TagSetting,
|
||||
[k: string]: TagSetting;
|
||||
}
|
||||
|
||||
// PermissionStore.computePermissions will not work here since it only gets permissions for the current user
|
||||
const computePermissions: (options: {
|
||||
user?: { id: string; } | string | null;
|
||||
context?: Guild | Channel | null;
|
||||
overwrites?: Channel["permissionOverwrites"] | null;
|
||||
checkElevated?: boolean /* = true */;
|
||||
excludeGuildPermissions?: boolean /* = false */;
|
||||
}) => bigint = findByCodeLazy(".getCurrentUser()", ".computeLurkerPermissionsAllowList()");
|
||||
|
||||
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();
|
||||
|
||||
const tags: Tag[] = [
|
||||
{
|
||||
name: "WEBHOOK",
|
||||
displayName: "Webhook",
|
||||
description: "Messages sent by webhooks",
|
||||
condition: isWebhook
|
||||
}, {
|
||||
name: "OWNER",
|
||||
displayName: "Owner",
|
||||
description: "Owns the server",
|
||||
condition: (_, user, channel) => GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id
|
||||
}, {
|
||||
name: "ADMINISTRATOR",
|
||||
displayName: "Admin",
|
||||
description: "Has the administrator permission",
|
||||
permissions: ["ADMINISTRATOR"]
|
||||
}, {
|
||||
name: "MODERATOR_STAFF",
|
||||
displayName: "Staff",
|
||||
description: "Can manage the server, channels or roles",
|
||||
permissions: ["MANAGE_GUILD", "MANAGE_CHANNELS", "MANAGE_ROLES"]
|
||||
}, {
|
||||
name: "MODERATOR",
|
||||
displayName: "Mod",
|
||||
description: "Can manage messages or kick/ban people",
|
||||
permissions: ["MANAGE_MESSAGES", "KICK_MEMBERS", "BAN_MEMBERS"]
|
||||
}, {
|
||||
name: "VOICE_MODERATOR",
|
||||
displayName: "VC Mod",
|
||||
description: "Can manage voice chats",
|
||||
permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"]
|
||||
}, {
|
||||
name: "CHAT_MODERATOR",
|
||||
displayName: "Chat Mod",
|
||||
description: "Can timeout people",
|
||||
permissions: ["MODERATE_MEMBERS"]
|
||||
}
|
||||
];
|
||||
const defaultSettings = Object.fromEntries(
|
||||
tags.map(({ name, displayName }) => [name, { text: displayName, showInChat: true, showInNotChat: true }])
|
||||
) as TagSettings;
|
||||
|
||||
function SettingsComponent() {
|
||||
const tagSettings = settings.store.tagSettings ??= defaultSettings;
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column">
|
||||
{tags.map(t => (
|
||||
<Card style={{ padding: "1em 1em 0" }}>
|
||||
<Forms.FormTitle style={{ width: "fit-content" }}>
|
||||
<Tooltip text={t.description}>
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
<div
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{t.displayName} Tag <Tag type={Tag.Types[t.name]} />
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Forms.FormTitle>
|
||||
|
||||
<TextInput
|
||||
type="text"
|
||||
value={tagSettings[t.name]?.text ?? t.displayName}
|
||||
placeholder={`Text on tag (default: ${t.displayName})`}
|
||||
onChange={v => tagSettings[t.name].text = v}
|
||||
className={Margins.bottom16}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
value={tagSettings[t.name]?.showInChat ?? true}
|
||||
onChange={v => tagSettings[t.name].showInChat = v}
|
||||
hideBorder
|
||||
>
|
||||
Show in messages
|
||||
</Switch>
|
||||
|
||||
<Switch
|
||||
value={tagSettings[t.name]?.showInNotChat ?? true}
|
||||
onChange={v => tagSettings[t.name].showInNotChat = v}
|
||||
hideBorder
|
||||
>
|
||||
Show in member list and profiles
|
||||
</Switch>
|
||||
</Card>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
dontShowForBots: {
|
||||
description: "Don't show extra tags for bots (excluding webhooks)",
|
||||
type: OptionType.BOOLEAN
|
||||
},
|
||||
dontShowBotTag: {
|
||||
description: "Only show extra tags for bots / Hide [BOT] text",
|
||||
type: OptionType.BOOLEAN
|
||||
},
|
||||
tagSettings: {
|
||||
type: OptionType.COMPONENT,
|
||||
component: SettingsComponent,
|
||||
description: "fill me"
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "MoreUserTags",
|
||||
description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)",
|
||||
authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN],
|
||||
settings,
|
||||
patches: [
|
||||
// add tags to the tag list
|
||||
{
|
||||
find: ".ORIGINAL_POSTER=",
|
||||
replacement: {
|
||||
match: /\((\i)=\{\}\)\)\[(\i)\.BOT/,
|
||||
replace: "($1=$self.getTagTypes()))[$2.BOT"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "#{intl::DISCORD_SYSTEM_MESSAGE_BOT_TAG_TOOLTIP_OFFICIAL}",
|
||||
replacement: [
|
||||
// make the tag show the right text
|
||||
{
|
||||
match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=(.{0,40}#{intl::APP_TAG}\))/,
|
||||
replace: (_, origSwitch, variant, tags, displayedText, originalText) =>
|
||||
`${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}],${originalText})}`
|
||||
},
|
||||
// show OP tags correctly
|
||||
{
|
||||
match: /(\i)=(\i)===\i(?:\.\i)?\.ORIGINAL_POSTER/,
|
||||
replace: "$1=$self.isOPTag($2)"
|
||||
},
|
||||
// add HTML data attributes (for easier theming)
|
||||
{
|
||||
match: /.botText,children:(\i)}\)]/,
|
||||
replace: "$&,'data-tag':$1.toLowerCase()"
|
||||
}
|
||||
],
|
||||
},
|
||||
// in messages
|
||||
{
|
||||
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"
|
||||
}
|
||||
},
|
||||
// in the member list
|
||||
{
|
||||
find: "#{intl::GUILD_OWNER}",
|
||||
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'"
|
||||
}
|
||||
},
|
||||
// pass channel id down props to be used in profiles
|
||||
{
|
||||
find: ".hasAvatarForGuild(null==",
|
||||
replacement: {
|
||||
match: /(?=usernameIcon:)/,
|
||||
replace: "moreTags_channelId:arguments[0].channelId,"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "#{intl::USER_PROFILE_PRONOUNS}",
|
||||
replacement: {
|
||||
match: /(?=,hideBotTag:!0)/,
|
||||
replace: ",moreTags_channelId:arguments[0].moreTags_channelId"
|
||||
}
|
||||
},
|
||||
// in profiles
|
||||
{
|
||||
find: ",overrideDiscriminator:",
|
||||
group: true,
|
||||
replacement: [
|
||||
{
|
||||
// prevent channel id from getting ghosted
|
||||
// it's either this or extremely long lookbehind
|
||||
match: /user:\i,nick:\i,/,
|
||||
replace: "$&moreTags_channelId,"
|
||||
}, {
|
||||
match: /,botType:(\i),botVerified:(\i),(?!discriminatorClass:)(?<=user:(\i).+?)/g,
|
||||
replace: ",botType:$self.getTag({user:$3,channelId:moreTags_channelId,origType:$1,location:'not-chat'}),botVerified:$2,"
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
|
||||
start() {
|
||||
settings.store.tagSettings ??= defaultSettings;
|
||||
|
||||
// newly added field might be missing from old users
|
||||
settings.store.tagSettings.CHAT_MODERATOR ??= {
|
||||
text: "Chat Mod",
|
||||
showInChat: true,
|
||||
showInNotChat: true
|
||||
};
|
||||
},
|
||||
|
||||
getPermissions(user: User, channel: Channel): string[] {
|
||||
const guild = GuildStore.getGuild(channel?.guild_id);
|
||||
if (!guild) return [];
|
||||
|
||||
const permissions = computePermissions({ user, context: guild, overwrites: channel.permissionOverwrites });
|
||||
return Object.entries(PermissionsBits)
|
||||
.map(([perm, permInt]) =>
|
||||
permissions & permInt ? perm : ""
|
||||
)
|
||||
.filter(Boolean);
|
||||
},
|
||||
|
||||
getTagTypes() {
|
||||
const obj = {};
|
||||
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`;
|
||||
});
|
||||
return obj;
|
||||
},
|
||||
|
||||
isOPTag: (tag: number) => tag === Tag.Types.ORIGINAL_POSTER || tags.some(t => tag === Tag.Types[`${t.name}-OP`]),
|
||||
|
||||
getTagText(passedTagName: string, originalText: string) {
|
||||
try {
|
||||
const [tagName, variant] = passedTagName.split("-");
|
||||
if (!passedTagName) return getIntlMessage("APP_TAG");
|
||||
const tag = tags.find(({ name }) => tagName === name);
|
||||
if (!tag) return getIntlMessage("APP_TAG");
|
||||
if (variant === "BOT" && tagName !== "WEBHOOK" && this.settings.store.dontShowForBots) return getIntlMessage("APP_TAG");
|
||||
|
||||
const tagText = settings.store.tagSettings?.[tag.name]?.text || tag.displayName;
|
||||
switch (variant) {
|
||||
case "OP":
|
||||
return `${getIntlMessage("BOT_TAG_FORUM_ORIGINAL_POSTER")} • ${tagText}`;
|
||||
case "BOT":
|
||||
return `${getIntlMessage("APP_TAG")} • ${tagText}`;
|
||||
default:
|
||||
return tagText;
|
||||
}
|
||||
} catch {
|
||||
return originalText;
|
||||
}
|
||||
},
|
||||
|
||||
getTag({
|
||||
message, user, channelId, origType, location, channel
|
||||
}: {
|
||||
message?: Message,
|
||||
user: User & { isClyde(): boolean; },
|
||||
channel?: Channel & { isForumPost(): boolean; isMediaPost(): 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())
|
||||
return Tag.Types.AI;
|
||||
|
||||
let type = typeof origType === "number" ? origType : null;
|
||||
|
||||
channel ??= ChannelStore.getChannel(channelId!) as any;
|
||||
if (!channel) return type;
|
||||
|
||||
const settings = this.settings.store;
|
||||
const perms = this.getPermissions(user, channel);
|
||||
|
||||
for (const tag of tags) {
|
||||
if (location === "chat" && !settings.tagSettings[tag.name].showInChat) continue;
|
||||
if (location === "not-chat" && !settings.tagSettings[tag.name].showInNotChat) continue;
|
||||
|
||||
// If the owner tag is disabled, and the user is the owner of the guild,
|
||||
// avoid adding other tags because the owner will always match the condition for them
|
||||
if (
|
||||
tag.name !== "OWNER" &&
|
||||
GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id &&
|
||||
(location === "chat" && !settings.tagSettings.OWNER.showInChat) ||
|
||||
(location === "not-chat" && !settings.tagSettings.OWNER.showInNotChat)
|
||||
) continue;
|
||||
|
||||
if (
|
||||
tag.permissions?.some(perm => perms.includes(perm)) ||
|
||||
(tag.condition?.(message!, user, channel))
|
||||
) {
|
||||
if ((channel.isForumPost() || channel.isMediaPost()) && channel.ownerId === user.id)
|
||||
type = Tag.Types[`${tag.name}-OP`];
|
||||
else if (user.bot && !isWebhook(message!, user) && !settings.dontShowBotTag)
|
||||
type = Tag.Types[`${tag.name}-BOT`];
|
||||
else
|
||||
type = Tag.Types[tag.name];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return type;
|
||||
}
|
||||
});
|
|
@ -19,10 +19,12 @@
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { isNonNullish } from "@utils/guards";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, useMemo, UserStore } from "@webpack/common";
|
||||
import { Channel, User } from "discord-types/general";
|
||||
import { JSX } from "react";
|
||||
|
||||
const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel");
|
||||
const UserUtils = findByPropsLazy("getGlobalName");
|
||||
|
@ -54,6 +56,7 @@ function getMutualGDMCountText(user: User) {
|
|||
function renderClickableGDMs(mutualDms: Channel[], onClose: () => void) {
|
||||
return mutualDms.map(c => (
|
||||
<Clickable
|
||||
key={c.id}
|
||||
className={ProfileListClasses.listRow}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
|
@ -87,7 +90,7 @@ export default definePlugin({
|
|||
replacement: [
|
||||
{
|
||||
match: /\i\.useEffect.{0,100}(\i)\[0\]\.section/,
|
||||
replace: "$self.pushSection($1, arguments[0].user);$&"
|
||||
replace: "$self.pushSection($1,arguments[0].user);$&"
|
||||
},
|
||||
{
|
||||
match: /\(0,\i\.jsx\)\(\i,\{items:\i,section:(\i)/,
|
||||
|
@ -97,14 +100,32 @@ export default definePlugin({
|
|||
},
|
||||
{
|
||||
find: 'section:"MUTUAL_FRIENDS"',
|
||||
replacement: {
|
||||
match: /\.openUserProfileModal.+?\)}\)}\)(?<=(\(0,\i\.jsxs?\)\(\i\.\i,{className:(\i)\.divider}\)).+?)/,
|
||||
replace: "$&,$self.renderDMPageList({user: arguments[0].user, Divider: $1, listStyle: $2.list})"
|
||||
replacement: [
|
||||
{
|
||||
match: /\i\|\|\i(?=\?\(0,\i\.jsxs?\)\(\i\.\i\.Overlay,)/,
|
||||
replace: "$&||$self.getMutualGroupDms(arguments[0].user.id).length>0"
|
||||
},
|
||||
{
|
||||
match: /\.openUserProfileModal.+?\)}\)}\)(?<=,(\i)&&(\i)&&(\(0,\i\.jsxs?\)\(\i\.\i,{className:(\i)\.divider}\)).+?)/,
|
||||
replace: (m, hasMutualGuilds, hasMutualFriends, Divider, classes) => "" +
|
||||
`${m},$self.renderDMPageList({user:arguments[0].user,hasDivider:${hasMutualGuilds}||${hasMutualFriends},Divider:${Divider},listStyle:${classes}.list})`
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
getMutualGroupDms(userId: string) {
|
||||
try {
|
||||
return getMutualGroupDms(userId);
|
||||
} catch (e) {
|
||||
new Logger("MutualGroupDMs").error("Failed to get mutual group dms:", e);
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
|
||||
pushSection(sections: any[], user: User) {
|
||||
try {
|
||||
if (isBotOrSelf(user) || sections[IS_PATCHED]) return;
|
||||
|
||||
sections[IS_PATCHED] = true;
|
||||
|
@ -112,11 +133,13 @@ export default definePlugin({
|
|||
section: "MUTUAL_GDMS",
|
||||
text: getMutualGDMCountText(user)
|
||||
});
|
||||
} catch (e) {
|
||||
new Logger("MutualGroupDMs").error("Failed to push mutual group dms section:", e);
|
||||
}
|
||||
},
|
||||
|
||||
renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
|
||||
const mutualGDms = useMemo(() => getMutualGroupDms(user.id), [user.id]);
|
||||
|
||||
const entries = renderClickableGDMs(mutualGDms, onClose);
|
||||
|
||||
return (
|
||||
|
@ -138,14 +161,13 @@ export default definePlugin({
|
|||
);
|
||||
}),
|
||||
|
||||
renderDMPageList: ErrorBoundary.wrap(({ user, Divider, listStyle }: { user: User, Divider: JSX.Element, listStyle: string; }) => {
|
||||
renderDMPageList: ErrorBoundary.wrap(({ user, hasDivider, Divider, listStyle }: { user: User, hasDivider: boolean, Divider: JSX.Element, listStyle: string; }) => {
|
||||
const mutualGDms = getMutualGroupDms(user.id);
|
||||
if (mutualGDms.length === 0) return null;
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{Divider}
|
||||
{hasDivider && Divider}
|
||||
<ExpandableList
|
||||
listClassName={listStyle}
|
||||
header={"Mutual Groups"}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue