Merge branch 'dev' into feat/moreClonableEmotesv2

This commit is contained in:
sadan 2025-01-26 19:00:14 -05:00
commit eb93253b03
No known key found for this signature in database
107 changed files with 3154 additions and 2475 deletions

View file

@ -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",
{

View file

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.10.9",
"version": "1.11.2",
"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

File diff suppressed because it is too large Load diff

View file

@ -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();

View file

@ -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;
}

View file

@ -11,7 +11,7 @@ import { Logger } from "@utils/Logger";
import { waitFor } from "@webpack";
import { Button, ButtonLooks, 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 {

View file

@ -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;
@ -124,7 +124,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra
interface ContextMenuProps {
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;
@ -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);
}

View file

@ -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>
);
}
);
}

View file

@ -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
);
}

View file

@ -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>
)
);
}

View file

@ -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);
}

View file

@ -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[] = [];

View file

@ -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>
)
);
};

View file

@ -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;
}

View file

@ -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,

View file

@ -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,11 +79,14 @@ const ErrorBoundary = LazyComponent(() => {
if (this.props.noop) return null;
if (this.props.fallback)
return <this.props.fallback
return (
<this.props.fallback
wrappedProps={this.props.wrappedProps}
children={this.props.children}
{...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.";

View file

@ -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;

View file

@ -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>

View file

@ -20,7 +20,7 @@ import "./iconStyles.css";
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"
fill-rule="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" clip-rule="evenodd"
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>
);
@ -212,9 +212,9 @@ export function CogWheel(props: IconProps) {
>
<path
fill="currentColor"
fill-rule="evenodd"
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"
clip-rule="evenodd"
clipRule="evenodd"
/>
</Icon>
);
@ -262,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"
/>

View file

@ -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;

View file

@ -81,7 +81,8 @@ const Components: Record<OptionType, React.ComponentType<ISettingElementProps<an
[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 +110,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
setAuthors(a => [...a, author]);
}
})();
}, []);
}, [plugin.authors]);
async function saveAndClose() {
if (!plugin.options) {
@ -129,7 +130,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 +143,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 }));

View file

@ -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>
);
}

View file

@ -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>;
});
}

View file

@ -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"
}}>

View file

@ -79,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
@ -102,8 +102,9 @@ export default definePlugin({
}
},
userProfileBadge: ContributorBadge,
async start() {
Vencord.Api.Badges.addBadge(ContributorBadge);
await loadBadges();
},

View file

@ -31,7 +31,7 @@ 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});"
}
},
{
@ -39,12 +39,12 @@ export default definePlugin({
replacement: {
// props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply);
// Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid)
match: /(type:this\.props\.chatInputType.+?\.then\()(\i=>\{.+?let (\i)=\i\.\i\.parse\((\i),.+?let (\i)=\i\.\i\.getSendMessageOptionsForReply\(\i\);)(?<=\)\(({.+?})\)\.then.+?)/,
match: /(\{openWarningPopout:.{0,100}type:this.props.chatInputType.+?\.then\()(\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}` +
`if(await Vencord.Api.MessageEvents._handlePreSend(${channel}.id,${parsedMessage},${extra},${replyOptions}))` +
"return{shoudClear:true,shouldRefocus:true};"
"return{shouldClear:false,shouldRefocus:true};"
}
},
{

View file

@ -27,9 +27,9 @@ export default definePlugin({
{
find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}",
replacement: {
match: /(?<=:null),(.{0,40}togglePopout:.+?}\))\]}\):null,(?<=\((\i\.\i),{label:.+?:null,(\i&&!\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/,
replace: (_, ReactButton, ButtonComponent, showReactButton, message) => "" +
`]}):null,Vencord.Api.MessagePopover._buildPopoverElements(${ButtonComponent},${message}),${showReactButton}?${ReactButton}:null,`
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},`
}
}
]

View file

@ -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);$&"
}
]
}

View file

@ -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 }),
});

View file

@ -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,12 +73,12 @@ 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:)/,
@ -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 (

View file

@ -41,7 +41,7 @@ export default definePlugin({
},
{
// Status emojis
find: "#{intl::GUILD_OWNER}",
find: "#{intl::GUILD_OWNER}),children:",
replacement: {
match: /(?<=\.activityEmoji,.+?animate:)\i/,
replace: "!0"

View file

@ -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\].+?}\)\](?=}\))/,
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

View file

@ -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>
);

View file

@ -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;
}

View file

@ -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,",
},
},

View file

@ -75,8 +75,8 @@ 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)]"
}
}],

View file

@ -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);
},
});

View file

@ -95,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: ""
}
},

View file

@ -133,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,
@ -176,6 +179,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();

View file

@ -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>
</>
);

View file

@ -5,6 +5,7 @@
*/
import { React } from "@webpack/common";
import { JSX } from "react";
import { cl } from "../";

View file

@ -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";

View file

@ -222,7 +222,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}

View file

@ -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";
@ -391,7 +391,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 +514,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 +524,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 +558,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 +574,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 +601,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 +853,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 +941,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 +973,7 @@ export default definePlugin({
},
stop() {
removePreSendListener(this.preSend);
removePreEditListener(this.preEdit);
removeMessagePreSendListener(this.preSend);
removeMessagePreEditListener(this.preEdit);
}
});

View file

@ -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;

View file

@ -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: ""

View file

@ -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),"

View file

@ -16,7 +16,7 @@ interface UserMentionComponentProps {
id: string;
channelId: string;
guildId: string;
OriginalComponent: ReactNode;
originalComponent: () => ReactNode;
}
export default definePlugin({
@ -29,7 +29,7 @@ export default definePlugin({
find: ':"text":',
replacement: {
match: /(hidePersonalInformation\).+?)(if\(null!=\i\){.+?return \i)(?=})/,
replace: "$1return $self.UserMentionComponent({...arguments[0],OriginalComponent:(()=>{$2})()});"
replace: "$1return $self.UserMentionComponent({...arguments[0],originalComponent:()=>{$2}});"
}
}
],
@ -42,6 +42,6 @@ export default definePlugin({
channelId={props.channelId}
/>
), {
fallback: ({ wrappedProps }) => wrappedProps.OriginalComponent
fallback: ({ wrappedProps: { originalComponent } }) => originalComponent()
})
});

View file

@ -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() {

View file

@ -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() {
@ -209,14 +206,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)) {
@ -284,29 +280,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 +297,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);
}
}

View file

@ -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(

View file

@ -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}`);

View file

@ -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 | {}> {

View file

@ -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);
}
});

View file

@ -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;
@ -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");
}
});

View file

@ -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}
>

View file

@ -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>

View file

@ -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, {

View file

@ -114,7 +114,7 @@ function SettingsComponent() {
return (
<Flex flexDirection="column">
{tags.map(t => (
<Card style={{ padding: "1em 1em 0" }}>
<Card key={t.name} style={{ padding: "1em 1em 0" }}>
<Forms.FormTitle style={{ width: "fit-content" }}>
<Tooltip text={t.description}>
{({ onMouseEnter, onMouseLeave }) => (
@ -218,7 +218,7 @@ export default definePlugin({
},
// in the member list
{
find: "#{intl::GUILD_OWNER}",
find: "#{intl::GUILD_OWNER}),children:",
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'"

View file

@ -24,6 +24,7 @@ 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");
@ -55,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();

View file

@ -55,7 +55,7 @@ export default definePlugin({
},
{
// Clicking on replied messages to jump
find: "flash:!0,returnMessageId",
find: '("interactionUsernameProfile',
replacement: [
{
match: /.\?(.{1,10}\.show\({.{1,50}#{intl::UNBLOCK_TO_JUMP_TITLE})/,

View file

@ -113,6 +113,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
return (
<div
key={index}
className={cl("modal-list-item-btn")}
onClick={() => selectItem(index)}
role="button"
@ -178,7 +179,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
<div className={cl("modal-divider")} />
<ScrollerThin className={cl("modal-perms")} orientation="auto">
{Object.values(PermissionsBits).map(bit => (
<div className={cl("modal-perms-item")}>
<div key={bit} className={cl("modal-perms-item")}>
<div className={cl("modal-perms-item-icon")}>
{(() => {
const { permissions, overwriteAllow, overwriteDeny } = selectedItem;

View file

@ -192,6 +192,7 @@ function UserPermissionsComponent({ guild, guildMember, closePopout }: { guild:
<div className={classes(RoleRootClasses.root)}>
{userPermissions.map(({ permission, roleColor, roleName }) => (
<Tooltip
key={permission}
text={<GrantedByTooltip roleName={roleName} roleColor={roleColor} />}
tooltipClassName={cl("granted-by-container")}
tooltipContentClassName={cl("granted-by-content")}

View file

@ -7,11 +7,10 @@
import { classNameFactory } from "@api/Styles";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal";
import { extractAndLoadChunksLazy, findComponentByCodeLazy, findExportedComponentLazy } from "@webpack";
import { Button, Forms, Text, TextInput, Toasts, useEffect, useState } from "@webpack/common";
import { Button, Forms, Text, TextInput, Toasts, useMemo, useState } from "@webpack/common";
import { DEFAULT_COLOR, SWATCHES } from "../constants";
import { categories, Category, createCategory, getCategory, updateCategory } from "../data";
import { forceUpdate } from "../index";
import { categoryLen, createCategory, getCategory } from "../data";
interface ColorPickerProps {
color: number | null;
@ -33,51 +32,51 @@ interface ColorPickerWithSwatchesProps {
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)");
const ColorPickerWithSwatches = findExportedComponentLazy<ColorPickerWithSwatchesProps>("ColorPicker", "CustomColorPicker");
export const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}Promise\.all\((\[\i\.\i\("?.+?"?\).+?\])\).then\(\i\.bind\(\i,"?(.+?)"?\)\).{0,50}"UserSettings"/);
export const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}(\i\.\i\("?.+?"?\).*?).then\(\i\.bind\(\i,"?(.+?)"?\)\).{0,50}"UserSettings"/);
const cl = classNameFactory("vc-pindms-modal-");
interface Props {
categoryId: string | null;
initalChannelId: string | null;
initialChannelId: string | null;
modalProps: ModalProps;
}
function useCategory(categoryId: string | null, initalChannelId: string | null) {
const [category, setCategory] = useState<Category | null>(null);
useEffect(() => {
if (categoryId)
setCategory(getCategory(categoryId)!);
else if (initalChannelId)
setCategory({
const category = useMemo(() => {
if (categoryId) {
return getCategory(categoryId);
} else if (initalChannelId) {
return {
id: Toasts.genId(),
name: `Pin Category ${categories.length + 1}`,
name: `Pin Category ${categoryLen() + 1}`,
color: DEFAULT_COLOR,
collapsed: false,
channels: [initalChannelId]
});
}, [categoryId, initalChannelId]);
return {
category,
setCategory
};
}
}, [categoryId, initalChannelId]);
export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Props) {
const { category, setCategory } = useCategory(categoryId, initalChannelId);
return category;
}
export function NewCategoryModal({ categoryId, modalProps, initialChannelId }: Props) {
const category = useCategory(categoryId, initialChannelId);
if (!category) return null;
const onSave = async (e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
if (!categoryId)
await createCategory(category);
else
await updateCategory(category);
const [name, setName] = useState(category.name);
const [color, setColor] = useState(category.color);
const onSave = (e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
category.name = name;
category.color = color;
if (!categoryId) {
createCategory(category);
}
forceUpdate();
modalProps.onClose();
};
@ -93,25 +92,25 @@ export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Pr
<Forms.FormSection>
<Forms.FormTitle>Name</Forms.FormTitle>
<TextInput
value={category.name}
onChange={e => setCategory({ ...category, name: e })}
value={name}
onChange={e => setName(e)}
/>
</Forms.FormSection>
<Forms.FormDivider />
<Forms.FormSection>
<Forms.FormTitle>Color</Forms.FormTitle>
<ColorPickerWithSwatches
key={category.name}
key={category.id}
defaultColor={DEFAULT_COLOR}
colors={SWATCHES}
onChange={c => setCategory({ ...category, color: c! })}
value={category.color}
onChange={c => setColor(c!)}
value={color}
renderDefaultButton={() => null}
renderCustomButton={() => (
<ColorPicker
color={category.color}
onChange={c => setCategory({ ...category, color: c! })}
key={category.name}
color={color}
onChange={c => setColor(c!)}
key={category.id}
showEyeDropper={false}
/>
)}
@ -119,7 +118,7 @@ export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Pr
</Forms.FormSection>
</ModalContent>
<ModalFooter>
<Button type="submit" onClick={onSave} disabled={!category.name}>{categoryId ? "Save" : "Create"}</Button>
<Button type="submit" onClick={onSave} disabled={!name}>{categoryId ? "Save" : "Create"}</Button>
</ModalFooter>
</form>
</ModalRoot>
@ -129,6 +128,6 @@ export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Pr
export const openCategoryModal = (categoryId: string | null, channelId: string | null) =>
openModalLazy(async () => {
await requireSettingsMenu();
return modalProps => <NewCategoryModal categoryId={categoryId} modalProps={modalProps} initalChannelId={channelId} />;
return modalProps => <NewCategoryModal categoryId={categoryId} modalProps={modalProps} initialChannelId={channelId} />;
});

View file

@ -7,8 +7,8 @@
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Menu } from "@webpack/common";
import { addChannelToCategory, canMoveChannelInDirection, categories, isPinned, moveChannel, removeChannelFromCategory } from "../data";
import { forceUpdate, PinOrder, settings } from "../index";
import { addChannelToCategory, canMoveChannelInDirection, currentUserCategories, isPinned, moveChannel, removeChannelFromCategory } from "../data";
import { PinOrder, settings } from "../index";
import { openCategoryModal } from "./CreateCategoryModal";
function createPinMenuItem(channelId: string) {
@ -31,11 +31,12 @@ function createPinMenuItem(channelId: string) {
<Menu.MenuSeparator />
{
categories.map(category => (
currentUserCategories.map(category => (
<Menu.MenuItem
id={`pin-category-${category.name}`}
key={category.id}
id={`pin-category-${category.id}`}
label={category.name}
action={() => addChannelToCategory(channelId, category.id).then(forceUpdate)}
action={() => addChannelToCategory(channelId, category.id)}
/>
))
}
@ -48,7 +49,7 @@ function createPinMenuItem(channelId: string) {
id="unpin-dm"
label="Unpin DM"
color="danger"
action={() => removeChannelFromCategory(channelId).then(forceUpdate)}
action={() => removeChannelFromCategory(channelId)}
/>
{
@ -56,7 +57,7 @@ function createPinMenuItem(channelId: string) {
<Menu.MenuItem
id="move-up"
label="Move Up"
action={() => moveChannel(channelId, -1).then(forceUpdate)}
action={() => moveChannel(channelId, -1)}
/>
)
}
@ -66,7 +67,7 @@ function createPinMenuItem(channelId: string) {
<Menu.MenuItem
id="move-down"
label="Move Down"
action={() => moveChannel(channelId, 1).then(forceUpdate)}
action={() => moveChannel(channelId, 1)}
/>
)
}

View file

@ -6,10 +6,10 @@
import * as DataStore from "@api/DataStore";
import { Settings } from "@api/Settings";
import { useForceUpdater } from "@utils/react";
import { UserStore } from "@webpack/common";
import { DEFAULT_COLOR } from "./constants";
import { forceUpdate, PinOrder, PrivateChannelSortStore, settings } from "./index";
import { PinOrder, PrivateChannelSortStore, settings } from "./index";
export interface Category {
id: string;
@ -24,104 +24,92 @@ const CATEGORY_MIGRATED_PINDMS_KEY = "PinDMsMigratedPinDMs";
const CATEGORY_MIGRATED_KEY = "PinDMsMigratedOldCategories";
const OLD_CATEGORY_KEY = "BetterPinDMsCategories-";
export let categories: Category[] = [];
export async function saveCats(cats: Category[]) {
const { id } = UserStore.getCurrentUser();
await DataStore.set(CATEGORY_BASE_KEY + id, cats);
}
let forceUpdateDms: (() => void) | undefined = undefined;
export let currentUserCategories: Category[] = [];
export async function init() {
const id = UserStore.getCurrentUser()?.id;
await initCategories(id);
await migrateData(id);
forceUpdate();
await migrateData();
const userId = UserStore.getCurrentUser()?.id;
if (userId == null) return;
currentUserCategories = settings.store.userBasedCategoryList[userId] ??= [];
forceUpdateDms?.();
}
export async function initCategories(userId: string) {
categories = await DataStore.get<Category[]>(CATEGORY_BASE_KEY + userId) ?? [];
export function usePinnedDms() {
forceUpdateDms = useForceUpdater();
settings.use(["pinOrder", "canCollapseDmSection", "dmSectionCollapsed", "userBasedCategoryList"]);
}
export function getCategory(id: string) {
return categories.find(c => c.id === id);
return currentUserCategories.find(c => c.id === id);
}
export async function createCategory(category: Category) {
categories.push(category);
await saveCats(categories);
export function getCategoryByIndex(index: number) {
return currentUserCategories[index];
}
export async function updateCategory(category: Category) {
const index = categories.findIndex(c => c.id === category.id);
if (index === -1) return;
categories[index] = category;
await saveCats(categories);
export function createCategory(category: Category) {
currentUserCategories.push(category);
}
export async function addChannelToCategory(channelId: string, categoryId: string) {
const category = categories.find(c => c.id === categoryId);
if (!category) return;
export function addChannelToCategory(channelId: string, categoryId: string) {
const category = currentUserCategories.find(c => c.id === categoryId);
if (category == null) return;
if (category.channels.includes(channelId)) return;
category.channels.push(channelId);
await saveCats(categories);
}
export async function removeChannelFromCategory(channelId: string) {
const category = categories.find(c => c.channels.includes(channelId));
if (!category) return;
export function removeChannelFromCategory(channelId: string) {
const category = currentUserCategories.find(c => c.channels.includes(channelId));
if (category == null) return;
category.channels = category.channels.filter(c => c !== channelId);
await saveCats(categories);
}
export async function removeCategory(categoryId: string) {
const catagory = categories.find(c => c.id === categoryId);
if (!catagory) return;
export function removeCategory(categoryId: string) {
const categoryIndex = currentUserCategories.findIndex(c => c.id === categoryId);
if (categoryIndex === -1) return;
// catagory?.channels.forEach(c => removeChannelFromCategory(c));
categories = categories.filter(c => c.id !== categoryId);
await saveCats(categories);
currentUserCategories.splice(categoryIndex, 1);
}
export async function collapseCategory(id: string, value = true) {
const category = categories.find(c => c.id === id);
if (!category) return;
export function collapseCategory(id: string, value = true) {
const category = currentUserCategories.find(c => c.id === id);
if (category == null) return;
category.collapsed = value;
await saveCats(categories);
}
// utils
// Utils
export function isPinned(id: string) {
return categories.some(c => c.channels.includes(id));
return currentUserCategories.some(c => c.channels.includes(id));
}
export function categoryLen() {
return categories.length;
return currentUserCategories.length;
}
export function getAllUncollapsedChannels() {
if (settings.store.pinOrder === PinOrder.LastMessage) {
const sortedChannels = PrivateChannelSortStore.getPrivateChannelIds();
return categories.filter(c => !c.collapsed).flatMap(c => sortedChannels.filter(channel => c.channels.includes(channel)));
return currentUserCategories.filter(c => !c.collapsed).flatMap(c => sortedChannels.filter(channel => c.channels.includes(channel)));
}
return categories.filter(c => !c.collapsed).flatMap(c => c.channels);
return currentUserCategories.filter(c => !c.collapsed).flatMap(c => c.channels);
}
export function getSections() {
return categories.reduce((acc, category) => {
return currentUserCategories.reduce((acc, category) => {
acc.push(category.channels.length === 0 ? 1 : category.channels.length);
return acc;
}, [] as number[]);
}
// move categories
// Move categories
export const canMoveArrayInDirection = (array: any[], index: number, direction: -1 | 1) => {
const a = array[index];
const b = array[index + direction];
@ -130,18 +118,18 @@ export const canMoveArrayInDirection = (array: any[], index: number, direction:
};
export const canMoveCategoryInDirection = (id: string, direction: -1 | 1) => {
const index = categories.findIndex(m => m.id === id);
return canMoveArrayInDirection(categories, index, direction);
const categoryIndex = currentUserCategories.findIndex(m => m.id === id);
return canMoveArrayInDirection(currentUserCategories, categoryIndex, direction);
};
export const canMoveCategory = (id: string) => canMoveCategoryInDirection(id, -1) || canMoveCategoryInDirection(id, 1);
export const canMoveChannelInDirection = (channelId: string, direction: -1 | 1) => {
const category = categories.find(c => c.channels.includes(channelId));
if (!category) return false;
const category = currentUserCategories.find(c => c.channels.includes(channelId));
if (category == null) return false;
const index = category.channels.indexOf(channelId);
return canMoveArrayInDirection(category.channels, index, direction);
const channelIndex = category.channels.indexOf(channelId);
return canMoveArrayInDirection(category.channels, channelIndex, direction);
};
@ -150,70 +138,44 @@ function swapElementsInArray(array: any[], index1: number, index2: number) {
[array[index1], array[index2]] = [array[index2], array[index1]];
}
// stolen from PinDMs
export async function moveCategory(id: string, direction: -1 | 1) {
const a = categories.findIndex(m => m.id === id);
export function moveCategory(id: string, direction: -1 | 1) {
const a = currentUserCategories.findIndex(m => m.id === id);
const b = a + direction;
swapElementsInArray(categories, a, b);
await saveCats(categories);
swapElementsInArray(currentUserCategories, a, b);
}
export async function moveChannel(channelId: string, direction: -1 | 1) {
const category = categories.find(c => c.channels.includes(channelId));
if (!category) return;
export function moveChannel(channelId: string, direction: -1 | 1) {
const category = currentUserCategories.find(c => c.channels.includes(channelId));
if (category == null) return;
const a = category.channels.indexOf(channelId);
const b = a + direction;
swapElementsInArray(category.channels, a, b);
await saveCats(categories);
}
// migrate data
const getPinDMsPins = () => (Settings.plugins.PinDMs.pinnedDMs || void 0)?.split(",") as string[] | undefined;
async function migratePinDMs() {
if (categories.some(m => m.id === "oldPins")) {
return await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true);
// TODO: Remove DataStore PinnedDms migration once enough time has passed
async function migrateData() {
if (Settings.plugins.PinDMs.dmSectioncollapsed != null) {
settings.store.dmSectionCollapsed = Settings.plugins.PinDMs.dmSectioncollapsed;
delete Settings.plugins.PinDMs.dmSectioncollapsed;
}
const pindmspins = getPinDMsPins();
const dataStoreKeys = await DataStore.keys();
const pinDmsKeys = dataStoreKeys.map(key => String(key)).filter(key => key.startsWith(CATEGORY_BASE_KEY));
// we dont want duplicate pins
const difference = [...new Set(pindmspins)]?.filter(m => !categories.some(c => c.channels.includes(m)));
if (difference?.length) {
categories.push({
id: "oldPins",
name: "Pins",
color: DEFAULT_COLOR,
channels: difference
});
if (pinDmsKeys.length === 0) return;
for (const pinDmsKey of pinDmsKeys) {
const categories = await DataStore.get<Category[]>(pinDmsKey);
if (categories == null) continue;
const userId = pinDmsKey.replace(CATEGORY_BASE_KEY, "");
settings.store.userBasedCategoryList[userId] = categories;
await DataStore.del(pinDmsKey);
}
await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true);
}
async function migrateOldCategories(userId: string) {
const oldCats = await DataStore.get<Category[]>(OLD_CATEGORY_KEY + userId);
// dont want to migrate if the user has already has categories.
if (categories.length === 0 && oldCats?.length) {
categories.push(...(oldCats.filter(m => m.id !== "oldPins")));
}
await DataStore.set(CATEGORY_MIGRATED_KEY, true);
}
export async function migrateData(userId: string) {
const m1 = await DataStore.get(CATEGORY_MIGRATED_KEY), m2 = await DataStore.get(CATEGORY_MIGRATED_PINDMS_KEY);
if (m1 && m2) return;
// want to migrate the old categories first and then slove any conflicts with the PinDMs pins
if (!m1) await migrateOldCategories(userId);
if (!m2) await migratePinDMs();
await saveCats(categories);
await Promise.all([DataStore.del(CATEGORY_MIGRATED_PINDMS_KEY), DataStore.del(CATEGORY_MIGRATED_KEY), DataStore.del(OLD_CATEGORY_KEY)]);
}

View file

@ -12,13 +12,13 @@ import { Devs } from "@utils/constants";
import { classes } from "@utils/misc";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { ContextMenuApi, FluxDispatcher, Menu, React } from "@webpack/common";
import { Clickable, ContextMenuApi, FluxDispatcher, Menu, React } from "@webpack/common";
import { Channel } from "discord-types/general";
import { contextMenus } from "./components/contextMenu";
import { openCategoryModal, requireSettingsMenu } from "./components/CreateCategoryModal";
import { DEFAULT_CHUNK_SIZE } from "./constants";
import { canMoveCategory, canMoveCategoryInDirection, categories, Category, categoryLen, collapseCategory, getAllUncollapsedChannels, getSections, init, isPinned, moveCategory, removeCategory } from "./data";
import { canMoveCategory, canMoveCategoryInDirection, Category, categoryLen, collapseCategory, getAllUncollapsedChannels, getCategoryByIndex, getSections, init, isPinned, moveCategory, removeCategory, usePinnedDms } from "./data";
interface ChannelComponentProps {
children: React.ReactNode,
@ -26,13 +26,11 @@ interface ChannelComponentProps {
selected: boolean;
}
const headerClasses = findByPropsLazy("privateChannelsHeaderContainer");
export const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; };
export let instance: any;
export const forceUpdate = () => instance?.props?._forceUpdate?.();
export const enum PinOrder {
LastMessage,
@ -46,21 +44,28 @@ export const settings = definePluginSettings({
options: [
{ label: "Most recent message", value: PinOrder.LastMessage, default: true },
{ label: "Custom (right click channels to reorder)", value: PinOrder.Custom }
],
onChange: () => forceUpdate()
]
},
dmSectioncollapsed: {
canCollapseDmSection: {
type: OptionType.BOOLEAN,
description: "Collapse DM sections",
description: "Allow uncategorised DMs section to be collapsable",
default: false
},
dmSectionCollapsed: {
type: OptionType.BOOLEAN,
description: "Collapse DM section",
default: false,
onChange: () => forceUpdate()
hidden: true
},
userBasedCategoryList: {
type: OptionType.CUSTOM,
default: {} as Record<string, Category[]>
}
});
export default definePlugin({
name: "PinDMs",
description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or reorder pins, right click DMs",
description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or re-order pins, right click DMs",
authors: [Devs.Ven, Devs.Aria],
settings,
contextMenus,
@ -124,8 +129,8 @@ export default definePlugin({
{
find: ".FRIENDS},\"friends\"",
replacement: {
match: /(?<=\i=\i=>{).{1,100}premiumTabSelected.{0,950}showDMHeader:.+?,/,
replace: "let forceUpdate = Vencord.Util.useForceUpdater();$&_forceUpdate:forceUpdate,"
match: /let{showLibrary:\i,/,
replace: "$self.usePinnedDms();$&"
}
},
@ -149,6 +154,7 @@ export default definePlugin({
}
},
],
sections: null as number[] | null,
set _instance(i: any) {
@ -162,6 +168,7 @@ export default definePlugin({
CONNECTION_OPEN: init,
},
usePinnedDms,
isPinned,
categoryLen,
getSections,
@ -186,11 +193,11 @@ export default definePlugin({
},
makeSpanProps() {
return {
return settings.store.canCollapseDmSection ? {
onClick: () => this.collapseDMList(),
role: "button",
style: { cursor: "pointer" }
};
} : undefined;
},
getChunkSize() {
@ -210,30 +217,27 @@ export default definePlugin({
},
isChannelIndex(sectionIndex: number, channelIndex: number) {
if (settings.store.dmSectioncollapsed && sectionIndex !== 0)
if (settings.store.canCollapseDmSection && settings.store.dmSectionCollapsed && sectionIndex !== 0) {
return true;
const cat = categories[sectionIndex - 1];
return this.isCategoryIndex(sectionIndex) && (cat?.channels?.length === 0 || cat?.channels[channelIndex]);
},
}
isDMSectioncollapsed() {
return settings.store.dmSectioncollapsed;
const category = getCategoryByIndex(sectionIndex - 1);
return this.isCategoryIndex(sectionIndex) && (category?.channels?.length === 0 || category?.channels[channelIndex]);
},
collapseDMList() {
settings.store.dmSectioncollapsed = !settings.store.dmSectioncollapsed;
forceUpdate();
settings.store.dmSectionCollapsed = !settings.store.dmSectionCollapsed;
},
isChannelHidden(categoryIndex: number, channelIndex: number) {
if (categoryIndex === 0) return false;
if (settings.store.dmSectioncollapsed && this.getSections().length + 1 === categoryIndex)
if (settings.store.canCollapseDmSection && settings.store.dmSectionCollapsed && this.getSections().length + 1 === categoryIndex)
return true;
if (!this.instance || !this.isChannelIndex(categoryIndex, channelIndex)) return false;
const category = categories[categoryIndex - 1];
const category = getCategoryByIndex(categoryIndex - 1);
if (!category) return false;
return category.collapsed && this.instance.props.selectedChannelId !== this.getCategoryChannels(category)[channelIndex];
@ -251,18 +255,12 @@ export default definePlugin({
},
renderCategory: ErrorBoundary.wrap(({ section }: { section: number; }) => {
const category = categories[section - 1];
const category = getCategoryByIndex(section - 1);
if (!category) return null;
return (
<h2
className={classes(headerClasses.privateChannelsHeaderContainer, "vc-pindms-section-container", category.collapsed ? "vc-pindms-collapsed" : "")}
style={{ color: `#${category.color.toString(16).padStart(6, "0")}` }}
onClick={async () => {
await collapseCategory(category.id, !category.collapsed);
forceUpdate();
}}
<Clickable
onClick={() => collapseCategory(category.id, !category.collapsed)}
onContextMenu={e => {
ContextMenuApi.openContextMenu(e, () => (
<Menu.Menu
@ -284,14 +282,14 @@ export default definePlugin({
canMoveCategoryInDirection(category.id, -1) && <Menu.MenuItem
id="vc-pindms-move-category-up"
label="Move Up"
action={() => moveCategory(category.id, -1).then(() => forceUpdate())}
action={() => moveCategory(category.id, -1)}
/>
}
{
canMoveCategoryInDirection(category.id, 1) && <Menu.MenuItem
id="vc-pindms-move-category-down"
label="Move Down"
action={() => moveCategory(category.id, 1).then(() => forceUpdate())}
action={() => moveCategory(category.id, 1)}
/>
}
</>
@ -304,13 +302,17 @@ export default definePlugin({
id="vc-pindms-delete-category"
color="danger"
label="Delete Category"
action={() => removeCategory(category.id).then(() => forceUpdate())}
action={() => removeCategory(category.id)}
/>
</Menu.Menu>
));
}}
>
<h2
className={classes(headerClasses.privateChannelsHeaderContainer, "vc-pindms-section-container", category.collapsed ? "vc-pindms-collapsed" : "")}
style={{ color: `#${category.color.toString(16).padStart(6, "0")}` }}
>
<span className={headerClasses.headerText}>
{category?.name ?? "uh oh"}
@ -319,6 +321,7 @@ export default definePlugin({
<path fill="currentColor" d="M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z"></path>
</svg>
</h2>
</Clickable>
);
}, { noop: true }),
@ -341,7 +344,7 @@ export default definePlugin({
},
getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) {
const category = categories[sectionIndex - 1];
const category = getCategoryByIndex(sectionIndex - 1);
if (!category) return { channel: null, category: null };
const channelId = this.getCategoryChannels(category)[index];

View file

@ -18,9 +18,9 @@
import "./style.css";
import { addBadge, BadgePosition, BadgeUserArgs, ProfileBadge, removeBadge } from "@api/Badges";
import { addDecorator, removeDecorator } from "@api/MemberListDecorators";
import { addDecoration, removeDecoration } from "@api/MessageDecorations";
import { addProfileBadge, BadgePosition, BadgeUserArgs, ProfileBadge, removeProfileBadge } from "@api/Badges";
import { addMemberListDecorator, removeMemberListDecorator } from "@api/MemberListDecorators";
import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations";
import { Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
@ -172,26 +172,26 @@ const badge: ProfileBadge = {
const indicatorLocations = {
list: {
description: "In the member list",
onEnable: () => addDecorator("platform-indicator", props =>
onEnable: () => addMemberListDecorator("platform-indicator", props =>
<ErrorBoundary noop>
<PlatformIndicator user={props.user} small={true} />
</ErrorBoundary>
),
onDisable: () => removeDecorator("platform-indicator")
onDisable: () => removeMemberListDecorator("platform-indicator")
},
badges: {
description: "In user profiles, as badges",
onEnable: () => addBadge(badge),
onDisable: () => removeBadge(badge)
onEnable: () => addProfileBadge(badge),
onDisable: () => removeProfileBadge(badge)
},
messages: {
description: "Inside messages",
onEnable: () => addDecoration("platform-indicator", props =>
onEnable: () => addMessageDecoration("platform-indicator", props =>
<ErrorBoundary noop>
<PlatformIndicator user={props.message?.author} wantTopMargin={true} />
</ErrorBoundary>
),
onDisable: () => removeDecoration("platform-indicator")
onDisable: () => removeMessageDecoration("platform-indicator")
}
};

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { generateId, sendBotMessage } from "@api/Commands";
import { Devs } from "@utils/constants";
import definePlugin, { StartAt } from "@utils/types";
@ -73,7 +73,7 @@ const getAttachments = async (channelId: string) =>
);
const PreviewButton: ChatBarButton = ({ isMainChat, isEmpty, type: { attachments } }) => {
const PreviewButton: ChatBarButtonFactory = ({ isMainChat, isEmpty, type: { attachments } }) => {
const channelId = SelectedChannelStore.getChannelId();
const draft = useStateFromStores([DraftStore], () => getDraft(channelId));
@ -121,11 +121,9 @@ export default definePlugin({
name: "PreviewMessage",
description: "Lets you preview your message before sending it.",
authors: [Devs.Aria],
dependencies: ["ChatInputButtonAPI"],
// start early to ensure we're the first plugin to add our button
// This makes the popping in less awkward
startAt: StartAt.Init,
start: () => addChatBarButton("previewMessage", PreviewButton),
stop: () => removeChatBarButton("previewMessage"),
renderChatBarButton: PreviewButton,
});

View file

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addButton, removeButton } from "@api/MessagePopover";
import { Devs } from "@utils/constants";
import { insertTextIntoChatInputBox } from "@utils/discord";
import definePlugin from "@utils/types";
@ -26,10 +25,8 @@ export default definePlugin({
name: "QuickMention",
authors: [Devs.kemo],
description: "Adds a quick mention button to the message actions bar",
dependencies: ["MessagePopoverAPI"],
start() {
addButton("QuickMention", msg => {
renderMessagePopoverButton(msg) {
const channel = ChannelStore.getChannel(msg.channel_id);
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return null;
@ -40,10 +37,6 @@ export default definePlugin({
channel,
onClick: () => insertTextIntoChatInputBox(`<@${msg.author.id}> `)
};
});
},
stop() {
removeButton("QuickMention");
},
Icon: () => (

View file

@ -9,16 +9,11 @@ import "./style.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { filters, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
import { Timestamp } from "@webpack/common";
import { findByPropsLazy } from "@webpack";
import { DateUtils, Timestamp } from "@webpack/common";
import type { Message } from "discord-types/general";
import type { HTMLAttributes } from "react";
const { calendarFormat, dateFormat, isSameDay } = mapMangledModuleLazy("millisecondsInUnit:", {
calendarFormat: filters.byCode("sameElse"),
dateFormat: filters.byCode('":'),
isSameDay: filters.byCode("Math.abs(+"),
});
const MessageClasses = findByPropsLazy("separator", "latin24CompactTimeStamp");
function Sep(props: HTMLAttributes<HTMLElement>) {
@ -46,14 +41,14 @@ function ReplyTimestamp({
return (
<Timestamp
className="vc-reply-timestamp"
compact={isSameDay(refTimestamp, baseTimestamp)}
compact={DateUtils.isSameDay(refTimestamp, baseTimestamp)}
timestamp={refTimestamp}
isInline={false}
>
<Sep>[</Sep>
{isSameDay(refTimestamp, baseTimestamp)
? dateFormat(refTimestamp, "LT")
: calendarFormat(refTimestamp)
{DateUtils.isSameDay(refTimestamp, baseTimestamp)
? DateUtils.dateFormat(refTimestamp, "LT")
: DateUtils.calendarFormat(refTimestamp)
}
<Sep>]</Sep>
</Timestamp>

View file

@ -21,7 +21,7 @@ import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
const SpoilerClasses = findByPropsLazy("spoilerContent");
const MessagesClasses = findByPropsLazy("messagesWrapper");
const MessagesClasses = findByPropsLazy("messagesWrapper", "navigationDescription");
export default definePlugin({
name: "RevealAllSpoilers",

View file

@ -108,7 +108,7 @@ export default definePlugin({
patches: [
{
find: "#{intl::MESSAGE_ACTIONS_MENU_LABEL}",
find: "#{intl::MESSAGE_ACTIONS_MENU_LABEL}),shouldHideMediaOptions:",
replacement: {
match: /favoriteableType:\i,(?<=(\i)\.getAttribute\("data-type"\).+?)/,
replace: (m, target) => `${m}reverseImageSearchType:${target}.getAttribute("data-role"),`

View file

@ -159,7 +159,7 @@ export default LazyComponent(() => {
onClick={() => openBlockModal()}
/>
)}
{review.sender.badges.map(badge => <ReviewBadge {...badge} />)}
{review.sender.badges.map((badge, idx) => <ReviewBadge key={idx} {...badge} />)}
{
!settings.store.hideTimestamps && review.type !== ReviewType.System && (
@ -170,7 +170,13 @@ export default LazyComponent(() => {
<div className={cl("review-comment")}>
{(review.comment.length > 200 && !showAll)
? [Parser.parseGuildEventDescription(review.comment.substring(0, 200)), "...", <br />, (<a onClick={() => setShowAll(true)}>Read more</a>)]
? (
<>
{Parser.parseGuildEventDescription(review.comment.substring(0, 200))}...
<br />
<a onClick={() => setShowAll(true)}>Read more</a>]
</>
)
: Parser.parseGuildEventDescription(review.comment)}
</div>

View file

@ -18,8 +18,7 @@
import "./styles.css";
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Devs } from "@utils/constants";
@ -123,7 +122,7 @@ function PickerModal({ rootProps, close }: { rootProps: ModalProps, close(): voi
);
}
const ChatBarIcon: ChatBarButton = ({ isMainChat }) => {
const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
if (!isMainChat) return null;
return (
@ -147,7 +146,7 @@ const ChatBarIcon: ChatBarButton = ({ isMainChat }) => {
viewBox="0 0 24 24"
style={{ scale: "1.2" }}
>
<g fill="none" fill-rule="evenodd">
<g fill="none" fillRule="evenodd">
<path fill="currentColor" d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7v-5z" />
<rect width="24" height="24" />
</g>
@ -160,22 +159,14 @@ export default definePlugin({
name: "SendTimestamps",
description: "Send timestamps easily via chat box button & text shortcuts. Read the extended description!",
authors: [Devs.Ven, Devs.Tyler, Devs.Grzesiek11],
dependencies: ["MessageEventsAPI", "ChatInputButtonAPI"],
settings,
start() {
addChatBarButton("SendTimestamps", ChatBarIcon);
this.listener = addPreSendListener((_, msg) => {
renderChatBarButton: ChatBarIcon,
onBeforeMessageSend(_, msg) {
if (settings.store.replaceMessageContents) {
msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime);
}
});
},
stop() {
removeChatBarButton("SendTimestamps");
removePreSendListener(this.listener);
},
settingsAboutComponent() {

View file

@ -31,7 +31,8 @@ export function openGuildInfoModal(guild: Guild) {
const enum Tabs {
ServerInfo,
Friends,
BlockedUsers
BlockedUsers,
IgnoredUsers
}
interface GuildProps {
@ -44,7 +45,8 @@ interface RelationshipProps extends GuildProps {
const fetched = {
friends: false,
blocked: false
blocked: false,
ignored: false
};
function renderTimestamp(timestamp: number) {
@ -56,10 +58,12 @@ function renderTimestamp(timestamp: number) {
function GuildInfoModal({ guild }: GuildProps) {
const [friendCount, setFriendCount] = useState<number>();
const [blockedCount, setBlockedCount] = useState<number>();
const [ignoredCount, setIgnoredCount] = useState<number>();
useEffect(() => {
fetched.friends = false;
fetched.blocked = false;
fetched.ignored = false;
}, []);
const [currentTab, setCurrentTab] = useState(Tabs.ServerInfo);
@ -132,12 +136,19 @@ function GuildInfoModal({ guild }: GuildProps) {
>
Blocked Users{blockedCount !== undefined ? ` (${blockedCount})` : ""}
</TabBar.Item>
<TabBar.Item
className={cl("tab", { selected: currentTab === Tabs.IgnoredUsers })}
id={Tabs.IgnoredUsers}
>
Ignored Users{ignoredCount !== undefined ? ` (${ignoredCount})` : ""}
</TabBar.Item>
</TabBar>
<div className={cl("tab-content")}>
{currentTab === Tabs.ServerInfo && <ServerInfoTab guild={guild} />}
{currentTab === Tabs.Friends && <FriendsTab guild={guild} setCount={setFriendCount} />}
{currentTab === Tabs.BlockedUsers && <BlockedUsersTab guild={guild} setCount={setBlockedCount} />}
{currentTab === Tabs.IgnoredUsers && <IgnoredUserTab guild={guild} setCount={setIgnoredCount} />}
</div>
</div>
);
@ -211,7 +222,13 @@ function BlockedUsersTab({ guild, setCount }: RelationshipProps) {
return UserList("blocked", guild, blockedIds, setCount);
}
function UserList(type: "friends" | "blocked", guild: Guild, ids: string[], setCount: (count: number) => void) {
function IgnoredUserTab({ guild, setCount }: RelationshipProps) {
const ignoredIds = Object.keys(RelationshipStore.getRelationships()).filter(id => RelationshipStore.isIgnored(id));
return UserList("ignored", guild, ignoredIds, setCount);
}
function UserList(type: "friends" | "blocked" | "ignored", guild: Guild, ids: string[], setCount: (count: number) => void) {
const missing = [] as string[];
const members = [] as string[];
@ -247,6 +264,7 @@ function UserList(type: "friends" | "blocked", guild: Guild, ids: string[], setC
<ScrollerThin fade className={cl("scroller")}>
{members.map(id =>
<FriendRow
key={id}
user={UserStore.getUser(id)}
status={PresenceStore.getStatus(id) || "offline"}
onSelect={() => openUserProfile(id)}

View file

@ -17,6 +17,7 @@
*/
import { Clipboard } from "@webpack/common";
import { JSX } from "react";
import { cl } from "../utils/misc";
import { CopyButton } from "./CopyButton";

View file

@ -18,6 +18,7 @@
import type { IThemedToken } from "@vap/shiki";
import { hljs } from "@webpack/common";
import { JSX } from "react";
import { cl } from "../utils/misc";
import { ThemeBase } from "./Highlighter";
@ -41,12 +42,12 @@ export const Code = ({
if (useHljs) {
try {
const { value: hljsHtml } = hljs.highlight(lang!, content, true);
const { value: hljsHtml } = hljs.highlight(content, { language: lang!, ignoreIllegals: true });
lines = hljsHtml
.split("\n")
.map((line, i) => <span key={i} dangerouslySetInnerHTML={{ __html: line }} />);
} catch {
lines = content.split("\n").map(line => <span>{line}</span>);
lines = content.split("\n").map((line, idx) => <span key={idx}>{line}</span>);
}
} else {
const renderTokens =
@ -55,11 +56,11 @@ export const Code = ({
.split("\n")
.map(line => [{ color: theme.plainColor, content: line } as IThemedToken]);
lines = renderTokens.map(line => {
lines = renderTokens.map((line, idx) => {
// [Cynthia] this makes it so when you highlight the codeblock
// empty lines are also selected and copied when you Ctrl+C.
if (line.length === 0) {
return <span>{"\n"}</span>;
return <span key={idx}>{"\n"}</span>;
}
return (

View file

@ -97,7 +97,7 @@ function ConnectionsComponent({ id, theme }: { id: string, theme: string; }) {
gap: getSpacingPx(settings.store.iconSpacing),
flexWrap: "wrap"
}}>
{connections.map(connection => <CompactConnectionComponent connection={connection} theme={theme} />)}
{connections.map(connection => <CompactConnectionComponent connection={connection} theme={theme} key={connection.id} />)}
</Flex>
);
}
@ -137,6 +137,7 @@ function CompactConnectionComponent({ connection, theme }: { connection: Connect
className="vc-user-connection"
href={url}
target="_blank"
rel="noreferrer"
onClick={e => {
if (Vencord.Plugins.isPluginEnabled("OpenInApp")) {
const OpenInApp = Vencord.Plugins.plugins.OpenInApp as any as typeof import("../openInApp").default;

View file

@ -268,7 +268,7 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
<div className="shc-lock-screen-tags-container">
<Text variant="text-lg/bold">Available tags:</Text>
<div className="shc-lock-screen-tags">
{availableTags.map(tag => <TagComponent tag={tag} />)}
{availableTags.map(tag => <TagComponent tag={tag} key={tag.id} />)}
</div>
</div>
}

View file

@ -88,7 +88,7 @@ export default definePlugin({
},
// Make channels we dont have access to be the same level as normal ones
{
match: /(activeJoinedRelevantThreads:.{0,50}VIEW_CHANNEL.+?renderLevel:(.+?),threadIds.+?renderLevel:).+?(?=,threadIds)/g,
match: /(this\.record\)\?{renderLevel:(.+?),threadIds.+?renderLevel:).+?(?=,threadIds)/g,
replace: (_, rest, defaultRenderLevel) => `${rest}${defaultRenderLevel}`
},
// Remove permission checking for getRenderLevel function
@ -224,12 +224,12 @@ export default definePlugin({
find: "Missing channel in Channel.renderHeaderToolbar",
replacement: [
{
match: /(?<="renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_TEXT:)(?=.+?(\i\.push.{0,50}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
replace: (_, pushNotificationButtonExpression, channel, isLurking) => `if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
match: /"renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_TEXT:(?=.+?(\i\.push.{0,50}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
replace: (m, pushNotificationButtonExpression, channel, isLurking) => `${m}if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
},
{
match: /(?<="renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_MEDIA:)(?=.+?(\i\.push.{0,40}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
replace: (_, pushNotificationButtonExpression, channel, isLurking) => `if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
match: /"renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_MEDIA:(?=.+?(\i\.push.{0,40}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
replace: (m, pushNotificationButtonExpression, channel, isLurking) => `${m}if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
},
{
match: /"renderMobileToolbar",\(\)=>{.+?case \i\.\i\.GUILD_DIRECTORY:(?<=let{channel:(\i).+?)/,

View file

@ -58,7 +58,7 @@ export default definePlugin({
},
},
{
find: /context:\i,checkElevated:!1\}\),\i\.\i.{0,200}autoTrackExposure/,
find: /,checkElevated:!1}\),\i\.\i\)}(?<=getCurrentUser\(\);return.+?)/,
predicate: () => settings.store.showModView,
replacement: {
match: /return \i\.\i\(\i\.\i\(\{user:\i,context:\i,checkElevated:!1\}\),\i\.\i\)/,
@ -67,7 +67,7 @@ export default definePlugin({
},
// fixes a bug where Members page must be loaded to see highest role, why is Discord depending on MemberSafetyStore.getEnhancedMember for something that can be obtained here?
{
find: "#{intl::GUILD_MEMBER_MOD_VIEW_PERMISSION_GRANTED_BY_ARIA_LABEL}",
find: "#{intl::GUILD_MEMBER_MOD_VIEW_PERMISSION_GRANTED_BY_ARIA_LABEL}),allowOverflow:",
predicate: () => settings.store.showModView,
replacement: {
match: /(role:)\i(?=,guildId.{0,100}role:(\i\[))/,
@ -76,7 +76,7 @@ export default definePlugin({
},
// allows you to open mod view on yourself
{
find: ".MEMBER_SAFETY,{modViewPanel:",
find: 'action:"PRESS_MOD_VIEW",icon:',
predicate: () => settings.store.showModView,
replacement: {
match: /\i(?=\?null)/,

View file

@ -84,13 +84,12 @@ export default definePlugin({
],
TooltipWrapper: ErrorBoundary.wrap(({ message, children, text }: { message: Message; children: FunctionComponent<any>; text: ReactNode; }) => {
if (settings.store.displayStyle === DisplayStyle.Tooltip) return <Tooltip
children={children}
text={renderTimeout(message, false)}
/>;
if (settings.store.displayStyle === DisplayStyle.Tooltip)
return <Tooltip text={renderTimeout(message, false)}>{children}</Tooltip>;
return (
<div className="vc-std-wrapper">
<Tooltip text={text} children={children} />
<Tooltip text={text}>{children}</Tooltip>
<Text variant="text-md/normal" color="status-danger">
{renderTimeout(message, true)} timeout remaining
</Text>

View file

@ -16,8 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents";
import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { addMessagePreSendListener, MessageSendListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -41,7 +41,7 @@ const settings = definePluginSettings({
}
});
const SilentMessageToggle: ChatBarButton = ({ isMainChat }) => {
const SilentMessageToggle: ChatBarButtonFactory = ({ isMainChat }) => {
const [enabled, setEnabled] = useState(lastState);
function setEnabledValue(value: boolean) {
@ -50,15 +50,15 @@ const SilentMessageToggle: ChatBarButton = ({ isMainChat }) => {
}
useEffect(() => {
const listener: SendListener = (_, message) => {
const listener: MessageSendListener = (_, message) => {
if (enabled) {
if (settings.store.autoDisable) setEnabledValue(false);
if (!message.content.startsWith("@silent ")) message.content = "@silent " + message.content;
}
};
addPreSendListener(listener);
return () => void removePreSendListener(listener);
addMessagePreSendListener(listener);
return () => void removeMessagePreSendListener(listener);
}, [enabled]);
if (!isMainChat) return null;
@ -78,7 +78,7 @@ const SilentMessageToggle: ChatBarButton = ({ isMainChat }) => {
{!enabled && <>
<mask id="vc-silent-msg-mask">
<path fill="#fff" d="M0 0h24v24H0Z" />
<path stroke="#000" stroke-width="5.99068" d="M0 24 24 0" />
<path stroke="#000" strokeWidth="5.99068" d="M0 24 24 0" />
</mask>
<path fill="var(--status-danger)" d="m21.178 1.70703 1.414 1.414L4.12103 21.593l-1.414-1.415L21.178 1.70703Z" />
</>}
@ -91,9 +91,7 @@ export default definePlugin({
name: "SilentMessageToggle",
authors: [Devs.Nuckyz, Devs.CatNoir],
description: "Adds a button to the chat bar to toggle sending a silent message.",
dependencies: ["MessageEventsAPI", "ChatInputButtonAPI"],
settings,
start: () => addChatBarButton("SilentMessageToggle", SilentMessageToggle),
stop: () => removeChatBarButton("SilentMessageToggle")
renderChatBarButton: SilentMessageToggle,
});

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
@ -43,7 +43,7 @@ const settings = definePluginSettings({
}
});
const SilentTypingToggle: ChatBarButton = ({ isMainChat }) => {
const SilentTypingToggle: ChatBarButtonFactory = ({ isMainChat }) => {
const { isEnabled, showIcon } = settings.use(["isEnabled", "showIcon"]);
const toggle = () => settings.store.isEnabled = !settings.store.isEnabled;
@ -96,11 +96,12 @@ export default definePlugin({
name: "SilentTyping",
authors: [Devs.Ven, Devs.Rini, Devs.ImBanana],
description: "Hide that you are typing",
dependencies: ["ChatInputButtonAPI"],
settings,
contextMenus: {
"textarea-context": ChatBarContextCheckbox
},
patches: [
{
find: '.dispatch({type:"TYPING_START_LOCAL"',
@ -136,6 +137,5 @@ export default definePlugin({
FluxDispatcher.dispatch({ type: "TYPING_START_LOCAL", channelId });
},
start: () => addChatBarButton("SilentTyping", SilentTypingToggle),
stop: () => removeChatBarButton("SilentTyping"),
renderChatBarButton: SilentTypingToggle,
});

View file

@ -16,12 +16,28 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./styles.css";
import { definePluginSettings } from "@api/Settings";
import { Flex } from "@components/Flex";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { RelationshipStore } from "@webpack/common";
import { DateUtils, RelationshipStore, Text, TooltipContainer } from "@webpack/common";
import { User } from "discord-types/general";
import { PropsWithChildren } from "react";
const formatter = new Intl.DateTimeFormat(undefined, {
month: "numeric",
day: "numeric",
year: "numeric",
});
const cl = classNameFactory("vc-sortFriendRequests-");
function getSince(user: User) {
return new Date(RelationshipStore.getSince(user.id));
}
const settings = definePluginSettings({
showDates: {
@ -48,28 +64,27 @@ export default definePlugin({
find: "#{intl::FRIEND_REQUEST_CANCEL}",
replacement: {
predicate: () => settings.store.showDates,
match: /subText:(\i)(?<=user:(\i).+?)/,
replace: (_, subtext, user) => `subText:$self.makeSubtext(${subtext},${user})`
match: /(?<=\.listItemContents,children:\[)\(0,.+?(?=,\(0)(?<=user:(\i).+?)/,
replace: (children, user) => `$self.WrapperDateComponent({user:${user},children:${children}})`
}
}],
wrapSort(comparator: Function, row: any) {
return row.type === 3 || row.type === 4
? -this.getSince(row.user)
? -getSince(row.user)
: comparator(row);
},
getSince(user: User) {
return new Date(RelationshipStore.getSince(user.id));
},
WrapperDateComponent: ErrorBoundary.wrap(({ user, children }: PropsWithChildren<{ user: User; }>) => {
const since = getSince(user);
makeSubtext(text: string, user: User) {
const since = this.getSince(user);
return (
<Flex flexDirection="column" style={{ gap: 0, flexWrap: "wrap", lineHeight: "0.9rem" }}>
<span>{text}</span>
{!isNaN(since.getTime()) && <span>Received &mdash; {since.toDateString()}</span>}
</Flex>
);
}
return <div className={cl("wrapper")}>
{children}
{!isNaN(since.getTime()) && (
<TooltipContainer text={DateUtils.dateFormat(since, "LLLL")} tooltipClassName={cl("tooltip")}>
<Text variant="text-xs/normal" className={cl("date")}>{formatter.format(since)}</Text>
</TooltipContainer>
)}
</div>;
})
});

View file

@ -0,0 +1,18 @@
.vc-sortFriendRequests-wrapper {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
margin-right: 0.5em;
}
.vc-sortFriendRequests-tooltip {
max-width: none;
white-space: nowrap;
}
.vc-sortFriendRequests-date {
color: var(--text-muted);
font-family: var(--font-code);
}

View file

@ -120,8 +120,8 @@ function ServerTrace({ trace }: ServerTraceProps) {
<Forms.FormSection title="Server Trace" tag="h2">
<code>
<Flex flexDirection="column" style={{ color: "var(--header-primary)", gap: 5, userSelect: "text" }}>
{lines.map(line => (
<span>{line}</span>
{lines.map((line, idx) => (
<span key={idx}>{line}</span>
))}
</Flex>
</code>

View file

@ -17,13 +17,11 @@
*/
import { DataStore } from "@api/index";
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
import { definePluginSettings } from "@api/Settings";
import { Flex } from "@components/Flex";
import { DeleteIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { useForceUpdater } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { Button, Forms, React, TextInput, useState } from "@webpack/common";
@ -35,8 +33,6 @@ type Rule = Record<"find" | "replace" | "onlyIfIncludes", string>;
interface TextReplaceProps {
title: string;
rulesArray: Rule[];
rulesKey: string;
update: () => void;
}
const makeEmptyRule: () => Rule = () => ({
@ -46,34 +42,36 @@ const makeEmptyRule: () => Rule = () => ({
});
const makeEmptyRuleArray = () => [makeEmptyRule()];
let stringRules = makeEmptyRuleArray();
let regexRules = makeEmptyRuleArray();
const settings = definePluginSettings({
replace: {
type: OptionType.COMPONENT,
description: "",
component: () => {
const update = useForceUpdater();
const { stringRules, regexRules } = settings.use(["stringRules", "regexRules"]);
return (
<>
<TextReplace
title="Using String"
rulesArray={stringRules}
rulesKey={STRING_RULES_KEY}
update={update}
/>
<TextReplace
title="Using Regex"
rulesArray={regexRules}
rulesKey={REGEX_RULES_KEY}
update={update}
/>
<TextReplaceTesting />
</>
);
}
},
stringRules: {
type: OptionType.CUSTOM,
default: makeEmptyRuleArray(),
},
regexRules: {
type: OptionType.CUSTOM,
default: makeEmptyRuleArray(),
}
});
function stringToRegex(str: string) {
@ -120,28 +118,24 @@ function Input({ initialValue, onChange, placeholder }: {
);
}
function TextReplace({ title, rulesArray, rulesKey, update }: TextReplaceProps) {
function TextReplace({ title, rulesArray }: TextReplaceProps) {
const isRegexRules = title === "Using Regex";
async function onClickRemove(index: number) {
if (index === rulesArray.length - 1) return;
rulesArray.splice(index, 1);
await DataStore.set(rulesKey, rulesArray);
update();
}
async function onChange(e: string, index: number, key: string) {
if (index === rulesArray.length - 1)
if (index === rulesArray.length - 1) {
rulesArray.push(makeEmptyRule());
}
rulesArray[index][key] = e;
if (rulesArray[index].find === "" && rulesArray[index].replace === "" && rulesArray[index].onlyIfIncludes === "" && index !== rulesArray.length - 1)
if (rulesArray[index].find === "" && rulesArray[index].replace === "" && rulesArray[index].onlyIfIncludes === "" && index !== rulesArray.length - 1) {
rulesArray.splice(index, 1);
await DataStore.set(rulesKey, rulesArray);
update();
}
}
return (
@ -208,20 +202,18 @@ function TextReplaceTesting() {
}
function applyRules(content: string): string {
if (content.length === 0)
if (content.length === 0) {
return content;
}
if (stringRules) {
for (const rule of stringRules) {
for (const rule of settings.store.stringRules) {
if (!rule.find) continue;
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, "");
}
}
if (regexRules) {
for (const rule of regexRules) {
for (const rule of settings.store.regexRules) {
if (!rule.find) continue;
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
@ -232,7 +224,6 @@ function applyRules(content: string): string {
new Logger("TextReplace").error(`Invalid regex: ${rule.find}`);
}
}
}
content = content.trim();
return content;
@ -244,22 +235,27 @@ export default definePlugin({
name: "TextReplace",
description: "Replace text in your messages. You can find pre-made rules in the #textreplace-rules channel in Vencord's Server",
authors: [Devs.AutumnVN, Devs.TheKodeToad],
dependencies: ["MessageEventsAPI"],
settings,
async start() {
stringRules = await DataStore.get(STRING_RULES_KEY) ?? makeEmptyRuleArray();
regexRules = await DataStore.get(REGEX_RULES_KEY) ?? makeEmptyRuleArray();
this.preSend = addPreSendListener((channelId, msg) => {
onBeforeMessageSend(channelId, msg) {
// Channel used for sharing rules, applying rules here would be messy
if (channelId === TEXT_REPLACE_RULES_CHANNEL_ID) return;
msg.content = applyRules(msg.content);
});
},
stop() {
removePreSendListener(this.preSend);
async start() {
// TODO: Remove DataStore rules migrations once enough time has passed
const oldStringRules = await DataStore.get<Rule[]>(STRING_RULES_KEY);
if (oldStringRules != null) {
settings.store.stringRules = oldStringRules;
await DataStore.del(STRING_RULES_KEY);
}
const oldRegexRules = await DataStore.get<Rule[]>(REGEX_RULES_KEY);
if (oldRegexRules != null) {
settings.store.regexRules = oldRegexRules;
await DataStore.del(REGEX_RULES_KEY);
}
}
});

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ChatBarButton } from "@api/ChatButtons";
import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { classes } from "@utils/misc";
import { openModal } from "@utils/modal";
import { Alerts, Forms, Tooltip, useEffect, useState } from "@webpack/common";
@ -40,7 +40,7 @@ export function TranslateIcon({ height = 24, width = 24, className }: { height?:
export let setShouldShowTranslateEnabledTooltip: undefined | ((show: boolean) => void);
export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
export const TranslateChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
const { autoTranslate, showChatBarButton } = settings.use(["autoTranslate", "showChatBarButton"]);
const [shouldShowTranslateEnabledTooltip, setter] = useState(false);

View file

@ -18,11 +18,7 @@
import "./styles.css";
import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
import { addButton, removeButton } from "@api/MessagePopover";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { ChannelStore, Menu } from "@webpack/common";
@ -51,11 +47,12 @@ const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) =>
));
};
let tooltipTimeout: any;
export default definePlugin({
name: "Translate",
description: "Translate messages with Google Translate or DeepL",
authors: [Devs.Ven, Devs.AshtonMemer],
dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"],
settings,
contextMenus: {
"message": messageCtxPatch
@ -63,12 +60,11 @@ export default definePlugin({
// not used, just here in case some other plugin wants it or w/e
translate,
start() {
addAccessory("vc-translation", props => <TranslationAccessory message={props.message} />);
renderMessageAccessory: props => <TranslationAccessory message={props.message} />,
addChatBarButton("vc-translate", TranslateChatBarIcon);
renderChatBarButton: TranslateChatBarIcon,
addButton("vc-translate", message => {
renderMessagePopoverButton(message) {
if (!message.content) return null;
return {
@ -81,10 +77,9 @@ export default definePlugin({
handleTranslate(message.id, trans);
}
};
});
},
let tooltipTimeout: any;
this.preSend = addPreSendListener(async (_, message) => {
async onBeforeMessageSend(_, message) {
if (!settings.store.autoTranslate) return;
if (!message.content) return;
@ -94,14 +89,5 @@ export default definePlugin({
const trans = await translate("sent", message.content);
message.content = trans.text;
});
},
stop() {
removePreSendListener(this.preSend);
removeChatBarButton("vc-translate");
removeButton("vc-translate");
removeAccessory("vc-translation");
},
}
});

View file

@ -23,6 +23,7 @@ import { openUserProfile } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { Avatar, GuildMemberStore, React, RelationshipStore } from "@webpack/common";
import { User } from "discord-types/general";
import { PropsWithChildren } from "react";
const settings = definePluginSettings({
showAvatars: {
@ -100,7 +101,7 @@ export default definePlugin({
{
// Style the indicator and add function call to modify the children before rendering
match: /(?<=children:\[(\i)\.length>0.{0,200}?"aria-atomic":!0,children:)\i(?<=guildId:(\i).+?)/,
replace: "$self.mutateChildren($2,$1,$&),style:$self.TYPING_TEXT_STYLE"
replace: "$self.renderTypingUsers({ users: $1, guildId: $2, children: $& }),style:$self.TYPING_TEXT_STYLE"
},
{
// Changes the indicator to keep the user object when creating the list of typing users
@ -125,7 +126,7 @@ export default definePlugin({
buildSeveralUsers,
mutateChildren(guildId: any, users: User[], children: any) {
renderTypingUsers: ErrorBoundary.wrap(({ guildId, users, children }: PropsWithChildren<{ guildId: string, users: User[]; }>) => {
try {
if (!Array.isArray(children)) {
return children;
@ -133,15 +134,17 @@ export default definePlugin({
let element = 0;
return children.map(c =>
c.type === "strong" || (typeof c !== "string" && !React.isValidElement(c))
? <TypingUser guildId={guildId} user={users[element++]} />
: c
);
return children.map(c => {
if (c.type !== "strong" && !(typeof c !== "string" && !React.isValidElement(c)))
return c;
const user = users[element++];
return <TypingUser key={user.id} guildId={guildId} user={user} />;
});
} catch (e) {
console.error(e);
}
return children;
}
}, { noop: true })
});

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addPreEditListener, addPreSendListener, MessageObject, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
import { MessageObject } from "@api/MessageEvents";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -24,7 +24,7 @@ export default definePlugin({
name: "Unindent",
description: "Trims leading indentation from codeblocks",
authors: [Devs.Ven],
dependencies: ["MessageEventsAPI"],
patches: [
{
find: "inQuote:",
@ -55,13 +55,11 @@ export default definePlugin({
});
},
start() {
this.preSend = addPreSendListener((_, msg) => this.unindentMsg(msg));
this.preEdit = addPreEditListener((_cid, _mid, msg) => this.unindentMsg(msg));
onBeforeMessageSend(_, msg) {
return this.unindentMsg(msg);
},
stop() {
removePreSendListener(this.preSend);
removePreEditListener(this.preEdit);
onBeforeMessageEdit(_cid, _mid, msg) {
return this.unindentMsg(msg);
}
});

View file

@ -21,12 +21,18 @@ import { ImageInvisible, ImageVisible } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Constants, Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common";
import { MessageSnapshot } from "@webpack/types";
const EMBED_SUPPRESSED = 1 << 2;
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => {
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, messageSnapshots, embeds, flags, id: messageId } }) => {
const isEmbedSuppressed = (flags & EMBED_SUPPRESSED) !== 0;
if (!isEmbedSuppressed && !embeds.length) return;
const hasEmbedsInSnapshots = messageSnapshots.some(
(snapshot: MessageSnapshot) => snapshot?.message.embeds.length
);
if (!isEmbedSuppressed && !embeds.length && !hasEmbedsInSnapshots) return;
const hasEmbedPerms = channel.isPrivate() || !!(PermissionStore.getChannelPermissions({ id: channel.id }) & PermissionsBits.EMBED_LINKS);
if (author.id === UserStore.getCurrentUser().id && !hasEmbedPerms) return;

View file

@ -18,8 +18,8 @@
import "./style.css";
import { addDecorator, removeDecorator } from "@api/MemberListDecorators";
import { addDecoration, removeDecoration } from "@api/MessageDecorations";
import { addMemberListDecorator, removeMemberListDecorator } from "@api/MemberListDecorators";
import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -96,16 +96,16 @@ export default definePlugin({
start() {
if (settings.store.showInMemberList) {
addDecorator("UserVoiceShow", ({ user }) => user == null ? null : <VoiceChannelIndicator userId={user.id} />);
addMemberListDecorator("UserVoiceShow", ({ user }) => user == null ? null : <VoiceChannelIndicator userId={user.id} />);
}
if (settings.store.showInMessages) {
addDecoration("UserVoiceShow", ({ message }) => message?.author == null ? null : <VoiceChannelIndicator userId={message.author.id} isMessageIndicator />);
addMessageDecoration("UserVoiceShow", ({ message }) => message?.author == null ? null : <VoiceChannelIndicator userId={message.author.id} isMessageIndicator />);
}
},
stop() {
removeDecorator("UserVoiceShow");
removeDecoration("UserVoiceShow");
removeMemberListDecorator("UserVoiceShow");
removeMessageDecoration("UserVoiceShow");
},
VoiceChannelIndicator

View file

@ -25,6 +25,7 @@ import { wordsToTitle } from "@utils/text";
import definePlugin, { OptionType, PluginOptionsItem, ReporterTestable } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore } from "@webpack/common";
import { ReactElement } from "react";
interface VoiceState {
userId: string;
@ -289,7 +290,7 @@ export default definePlugin({
description: "Undeafen Message (only self for now)",
default: "{{USER}} undeafened"
}
};
} satisfies Record<string, PluginOptionsItem>;
},
settingsAboutComponent({ tempSettings: s }) {
@ -303,7 +304,7 @@ export default definePlugin({
[],
);
let errorComponent: React.ReactElement | null = null;
let errorComponent: ReactElement<any> | null = null;
if (!hasVoices) {
let error = "No narrator voices found. ";
error += navigator.platform?.toLowerCase().includes("linux")

View file

@ -17,7 +17,6 @@
*/
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { addButton, removeButton } from "@api/MessagePopover";
import { definePluginSettings } from "@api/Settings";
import { CodeBlock } from "@components/CodeBlock";
import ErrorBoundary from "@components/ErrorBoundary";
@ -149,8 +148,8 @@ export default definePlugin({
name: "ViewRaw",
description: "Copy and view the raw content/data of any message, channel or guild",
authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna],
dependencies: ["MessagePopoverAPI"],
settings,
contextMenus: {
"guild-context": MakeContextCallback("Guild"),
"channel-context": MakeContextCallback("Channel"),
@ -159,8 +158,7 @@ export default definePlugin({
"user-context": MakeContextCallback("User")
},
start() {
addButton("ViewRaw", msg => {
renderMessagePopoverButton(msg) {
const handleClick = () => {
if (settings.store.clickMethod === "Right") {
copyWithToast(msg.content);
@ -193,10 +191,5 @@ export default definePlugin({
onClick: handleClick,
onContextMenu: handleContextMenu
};
});
},
stop() {
removeButton("ViewRaw");
}
});

View file

@ -23,7 +23,7 @@ import { Queue } from "@utils/Queue";
import { useForceUpdater } from "@utils/react";
import definePlugin from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { ChannelStore, Constants, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common";
import { ChannelStore, Constants, FluxDispatcher, React, RestAPI, Tooltip, useEffect, useLayoutEffect } from "@webpack/common";
import { CustomEmoji } from "@webpack/types";
import { Message, ReactionEmoji, User } from "discord-types/general";
@ -134,18 +134,21 @@ export default definePlugin({
renderUsers(props: RootObject) {
return props.message.reactions.length > 10 ? null : (
<ErrorBoundary noop>
<this._renderUsers {...props} />
<this.UsersComponent {...props} />
</ErrorBoundary>
);
},
_renderUsers({ message, emoji, type }: RootObject) {
UsersComponent({ message, emoji, type }: RootObject) {
const forceUpdate = useForceUpdater();
React.useLayoutEffect(() => { // bc need to prevent autoscrolling
useLayoutEffect(() => { // bc need to prevent autoscrolling
if (Scroll?.scrollCounter > 0) {
Scroll.setAutomaticAnchor(null);
}
});
React.useEffect(() => {
useEffect(() => {
const cb = (e: any) => {
if (e.messageId === message.id)
forceUpdate();
@ -153,7 +156,7 @@ export default definePlugin({
FluxDispatcher.subscribe("MESSAGE_REACTION_ADD_USERS", cb);
return () => FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD_USERS", cb);
}, [message.id]);
}, [message.id, forceUpdate]);
const reactions = getReactionsWithQueue(message, emoji, type);
const users = Object.values(reactions).filter(Boolean) as User[];

View file

@ -6,6 +6,9 @@
import { LiteralUnion } from "type-fest";
export const SYM_IS_PROXY = Symbol("SettingsStore.isProxy");
export const SYM_GET_RAW_TARGET = Symbol("SettingsStore.getRawTarget");
// Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop
type ResolvePropDeep<T, P> = P extends `${infer Pre}.${infer Suf}`
? Pre extends keyof T
@ -28,6 +31,11 @@ interface SettingsStoreOptions {
// merges the SettingsStoreOptions type into the class
export interface SettingsStore<T extends object> extends SettingsStoreOptions { }
interface ProxyContext<T extends object = any> {
root: T;
path: string;
}
/**
* The SettingsStore allows you to easily create a mutable store that
* has support for global and path-based change listeners.
@ -35,6 +43,90 @@ export interface SettingsStore<T extends object> extends SettingsStoreOptions {
export class SettingsStore<T extends object> {
private pathListeners = new Map<string, Set<(newData: any) => void>>();
private globalListeners = new Set<(newData: T, path: string) => void>();
private readonly proxyContexts = new WeakMap<any, ProxyContext<T>>();
private readonly proxyHandler: ProxyHandler<any> = (() => {
const self = this;
return {
get(target, key: any, receiver) {
if (key === SYM_IS_PROXY) {
return true;
}
if (key === SYM_GET_RAW_TARGET) {
return target;
}
let v = Reflect.get(target, key, receiver);
const proxyContext = self.proxyContexts.get(target);
if (proxyContext == null) {
return v;
}
const { root, path } = proxyContext;
if (!(key in target) && self.getDefaultValue != null) {
v = self.getDefaultValue({
target,
key,
root,
path
});
}
if (typeof v === "object" && v !== null && !v[SYM_IS_PROXY]) {
const getPath = `${path}${path && "."}${key}`;
return self.makeProxy(v, root, getPath);
}
return v;
},
set(target, key: string, value) {
if (value?.[SYM_IS_PROXY]) {
value = value[SYM_GET_RAW_TARGET];
}
if (target[key] === value) {
return true;
}
if (!Reflect.set(target, key, value)) {
return false;
}
const proxyContext = self.proxyContexts.get(target);
if (proxyContext == null) {
return true;
}
const { root, path } = proxyContext;
const setPath = `${path}${path && "."}${key}`;
self.notifyListeners(setPath, value, root);
return true;
},
deleteProperty(target, key: string) {
if (!Reflect.deleteProperty(target, key)) {
return false;
}
const proxyContext = self.proxyContexts.get(target);
if (proxyContext == null) {
return true;
}
const { root, path } = proxyContext;
const deletePath = `${path}${path && "."}${key}`;
self.notifyListeners(deletePath, undefined, root);
return true;
}
};
})();
/**
* The store object. Making changes to this object will trigger the applicable change listeners
@ -51,39 +143,35 @@ export class SettingsStore<T extends object> {
Object.assign(this, options);
}
private makeProxy(object: any, root: T = object, path: string = "") {
const self = this;
return new Proxy(object, {
get(target, key: string) {
let v = target[key];
if (!(key in target) && self.getDefaultValue) {
v = self.getDefaultValue({
target,
key,
private makeProxy(object: any, root: T = object, path = "") {
this.proxyContexts.set(object, {
root,
path
});
return new Proxy(object, this.proxyHandler);
}
if (typeof v === "object" && v !== null && !Array.isArray(v))
return self.makeProxy(v, root, `${path}${path && "."}${key}`);
private notifyListeners(pathStr: string, value: any, root: T) {
const paths = pathStr.split(".");
return v;
},
set(target, key: string, value) {
if (target[key] === value) return true;
// Because we support any type of settings with OptionType.CUSTOM, and those objects get proxied recursively,
// the path ends up including all the nested paths (plugins.pluginName.settingName.example.one).
// So, we need to extract the top-level setting path (plugins.pluginName.settingName),
// to be able to notify globalListeners and top-level setting name listeners (let { settingName } = settings.use(["settingName"]),
// with the new value
if (paths.length > 2 && paths[0] === "plugins") {
const settingPath = paths.slice(0, 3);
const settingPathStr = settingPath.join(".");
const settingValue = settingPath.reduce((acc, curr) => acc[curr], root);
Reflect.set(target, key, value);
const setPath = `${path}${path && "."}${key}`;
self.globalListeners.forEach(cb => cb(value, setPath));
self.pathListeners.get(setPath)?.forEach(cb => cb(value));
return true;
this.globalListeners.forEach(cb => cb(root, settingPathStr));
this.pathListeners.get(settingPathStr)?.forEach(cb => cb(settingValue));
} else {
this.globalListeners.forEach(cb => cb(root, pathStr));
}
});
this.pathListeners.get(pathStr)?.forEach(cb => cb(value));
}
/**

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