This commit is contained in:
Ulysia 2025-01-30 05:57:16 +01:00
commit 4029ac976b
100 changed files with 1622 additions and 1566 deletions

View file

@ -105,7 +105,13 @@ export default tseslint.config(
"no-invalid-regexp": "error", "no-invalid-regexp": "error",
"no-constant-condition": ["error", { "checkLoops": false }], "no-constant-condition": ["error", { "checkLoops": false }],
"no-duplicate-imports": "error", "no-duplicate-imports": "error",
"dot-notation": "error", "@typescript-eslint/dot-notation": [
"error",
{
"allowPrivateClassPropertyAccess": true,
"allowProtectedClassPropertyAccess": true
}
],
"no-useless-escape": [ "no-useless-escape": [
"error", "error",
{ {

View file

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

View file

@ -57,7 +57,7 @@ const Badges = new Set<ProfileBadge>();
* Register a new badge with the Badges API * Register a new badge with the Badges API
* @param badge The badge to register * @param badge The badge to register
*/ */
export function addBadge(badge: ProfileBadge) { export function addProfileBadge(badge: ProfileBadge) {
badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true }); badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true });
Badges.add(badge); Badges.add(badge);
} }
@ -66,7 +66,7 @@ export function addBadge(badge: ProfileBadge) {
* Unregister a badge from the Badges API * Unregister a badge from the Badges API
* @param badge The badge to remove * @param badge The badge to remove
*/ */
export function removeBadge(badge: ProfileBadge) { export function removeProfileBadge(badge: ProfileBadge) {
return Badges.delete(badge); return Badges.delete(badge);
} }
@ -100,20 +100,3 @@ export interface BadgeUserArgs {
userId: string; userId: string;
guildId: 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

@ -9,7 +9,7 @@ import "./ChatButton.css";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { waitFor } from "@webpack"; import { waitFor } from "@webpack";
import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common"; import { Button, ButtonWrapperClasses, Tooltip } from "@webpack/common";
import { Channel } from "discord-types/general"; import { Channel } from "discord-types/general";
import { HTMLProps, JSX, MouseEventHandler, ReactNode } from "react"; import { HTMLProps, JSX, MouseEventHandler, ReactNode } from "react";
@ -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"); const logger = new Logger("ChatButtons");
export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) { 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 const removeChatBarButton = (id: string) => buttonFactories.delete(id);
export interface ChatBarButtonProps { export interface ChatBarButtonProps {
@ -110,7 +110,7 @@ export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
<Button <Button
aria-label={props.tooltip} aria-label={props.tooltip}
size="" size=""
look={ButtonLooks.BLANK} look={Button.Looks.BLANK}
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
innerClassName={`${ButtonWrapperClasses.button} ${ChannelTextAreaClasses?.button}`} innerClassName={`${ButtonWrapperClasses.button} ${ChannelTextAreaClasses?.button}`}

View file

@ -122,7 +122,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra
} }
interface ContextMenuProps { interface ContextMenuProps {
contextMenuApiArguments?: Array<any>; contextMenuAPIArguments?: Array<any>;
navId: string; navId: string;
children: Array<ReactElement<any> | null>; children: Array<ReactElement<any> | null>;
"aria-label": string; "aria-label": string;
@ -136,7 +136,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
children: cloneMenuChildren(props.children), children: cloneMenuChildren(props.children),
}; };
props.contextMenuApiArguments ??= []; props.contextMenuAPIArguments ??= [];
const contextMenuPatches = navPatches.get(props.navId); const contextMenuPatches = navPatches.get(props.navId);
if (!Array.isArray(props.children)) props.children = [props.children]; if (!Array.isArray(props.children)) props.children = [props.children];
@ -144,7 +144,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
if (contextMenuPatches) { if (contextMenuPatches) {
for (const patch of contextMenuPatches) { for (const patch of contextMenuPatches) {
try { try {
patch(props.children, ...props.contextMenuApiArguments); patch(props.children, ...props.contextMenuAPIArguments);
} catch (err) { } catch (err) {
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err); ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
} }
@ -153,7 +153,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
for (const patch of globalPatches) { for (const patch of globalPatches) {
try { try {
patch(props.navId, props.children, ...props.contextMenuApiArguments); patch(props.navId, props.children, ...props.contextMenuAPIArguments);
} catch (err) { } catch (err) {
ContextMenuLogger.error("Global patch errored,", err); ContextMenuLogger.error("Global patch errored,", err);
} }

View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import ErrorBoundary from "@components/ErrorBoundary";
import { Channel, User } from "discord-types/general/index.js"; import { Channel, User } from "discord-types/general/index.js";
import { JSX } from "react"; import { JSX } from "react";
@ -39,27 +40,32 @@ interface DecoratorProps {
user: User; user: User;
[key: string]: any; [key: string]: any;
} }
export type Decorator = (props: DecoratorProps) => JSX.Element | null; export type MemberListDecoratorFactory = (props: DecoratorProps) => JSX.Element | null;
type OnlyIn = "guilds" | "dms"; 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) { export function addMemberListDecorator(identifier: string, render: MemberListDecoratorFactory, onlyIn?: OnlyIn) {
decorators.set(identifier, { decorator, onlyIn }); decorators.set(identifier, { render, onlyIn });
} }
export function removeDecorator(identifier: string) { export function removeMemberListDecorator(identifier: string) {
decorators.delete(identifier); decorators.delete(identifier);
} }
export function __getDecorators(props: DecoratorProps): (JSX.Element | null)[] { export function __getDecorators(props: DecoratorProps): (JSX.Element | null)[] {
const isInGuild = !!(props.guildId); const isInGuild = !!(props.guildId);
return Array.from(decorators.values(), decoratorObj => { return Array.from(
const { decorator, onlyIn } = decoratorObj; decorators.entries(),
// this can most likely be done cleaner ([key, { render: Decorator, onlyIn }]) => {
if (!onlyIn || (onlyIn === "guilds" && isInGuild) || (onlyIn === "dms" && !isInGuild)) { if ((onlyIn === "guilds" && !isInGuild) || (onlyIn === "dms" && isInGuild))
return decorator(props);
}
return null; return null;
});
return (
<ErrorBoundary noop key={key} message={`Failed to render ${key} Member List Decorator`}>
<Decorator {...props} />
</ErrorBoundary>
);
}
);
} }

View file

@ -16,28 +16,29 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { JSX } from "react"; import ErrorBoundary from "@components/ErrorBoundary";
import { JSX, ReactNode } from "react";
export type AccessoryCallback = (props: Record<string, any>) => JSX.Element | null | Array<JSX.Element | null>; export type MessageAccessoryFactory = (props: Record<string, any>) => ReactNode;
export type Accessory = { export type MessageAccessory = {
callback: AccessoryCallback; render: MessageAccessoryFactory;
position?: number; position?: number;
}; };
export const accessories = new Map<String, Accessory>(); export const accessories = new Map<string, MessageAccessory>();
export function addAccessory( export function addMessageAccessory(
identifier: string, identifier: string,
callback: AccessoryCallback, render: MessageAccessoryFactory,
position?: number position?: number
) { ) {
accessories.set(identifier, { accessories.set(identifier, {
callback, render,
position, position,
}); });
} }
export function removeAccessory(identifier: string) { export function removeMessageAccessory(identifier: string) {
accessories.delete(identifier); accessories.delete(identifier);
} }
@ -45,15 +46,12 @@ export function _modifyAccessories(
elements: JSX.Element[], elements: JSX.Element[],
props: Record<string, any> props: Record<string, any>
) { ) {
for (const accessory of accessories.values()) { for (const [key, accessory] of accessories.entries()) {
let accessories = accessory.callback(props); const res = (
if (accessories == null) <ErrorBoundary message={`Failed to render ${key} Message Accessory`} key={key}>
continue; <accessory.render {...props} />
</ErrorBoundary>
if (!Array.isArray(accessories)) );
accessories = [accessories];
else if (accessories.length === 0)
continue;
elements.splice( elements.splice(
accessory.position != null accessory.position != null
@ -62,7 +60,7 @@ export function _modifyAccessories(
: accessory.position : accessory.position
: elements.length, : elements.length,
0, 0,
...accessories.filter(e => e != null) as JSX.Element[] res
); );
} }

View file

@ -16,10 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import ErrorBoundary from "@components/ErrorBoundary";
import { Channel, Message } from "discord-types/general/index.js"; import { Channel, Message } from "discord-types/general/index.js";
import { JSX } from "react"; import { JSX } from "react";
interface DecorationProps { export interface MessageDecorationProps {
author: { author: {
/** /**
* Will be username if the user has no nickname * Will be username if the user has no nickname
@ -45,20 +46,25 @@ interface DecorationProps {
message: Message; message: Message;
[key: string]: any; [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); decorations.set(identifier, decoration);
} }
export function removeDecoration(identifier: string) { export function removeMessageDecoration(identifier: string) {
decorations.delete(identifier); decorations.delete(identifier);
} }
export function __addDecorationsToMessage(props: DecorationProps): (JSX.Element | null)[] { export function __addDecorationsToMessage(props: MessageDecorationProps): (JSX.Element | null)[] {
return [...decorations.values()].map(decoration => { return Array.from(
return decoration(props); 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; openWarningPopout: (props: any) => any;
} }
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>; export type MessageSendListener = (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 MessageEditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void | { cancel: boolean; }>;
const sendListeners = new Set<SendListener>(); const sendListeners = new Set<MessageSendListener>();
const editListeners = new Set<EditListener>(); const editListeners = new Set<MessageEditListener>();
export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra, replyOptions: MessageReplyOptions) { export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra, replyOptions: MessageReplyOptions) {
extra.replyOptions = replyOptions; 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. * 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); sendListeners.add(listener);
return listener; return listener;
} }
/** /**
* Note: This event fires off before a message's edit is applied, allowing you to further edit the message. * 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); editListeners.add(listener);
return listener; return listener;
} }
export function removePreSendListener(listener: SendListener) { export function removeMessagePreSendListener(listener: MessageSendListener) {
return sendListeners.delete(listener); return sendListeners.delete(listener);
} }
export function removePreEditListener(listener: EditListener) { export function removeMessagePreEditListener(listener: MessageEditListener) {
return editListeners.delete(listener); return editListeners.delete(listener);
} }
// Message clicks // 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) { export function _handleClick(message: Message, channel: Channel, event: MouseEvent) {
// message object may be outdated, so (try to) fetch latest one // 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); listeners.add(listener);
return listener; return listener;
} }
export function removeClickListener(listener: ClickListener) { export function removeMessageClickListener(listener: MessageClickListener) {
return listeners.delete(listener); return listeners.delete(listener);
} }

View file

@ -23,7 +23,7 @@ import type { ComponentType, MouseEventHandler } from "react";
const logger = new Logger("MessagePopover"); const logger = new Logger("MessagePopover");
export interface ButtonItem { export interface MessagePopoverButtonItem {
key?: string, key?: string,
label: string, label: string,
icon: ComponentType<any>, icon: ComponentType<any>,
@ -33,23 +33,23 @@ export interface ButtonItem {
onContextMenu?: MouseEventHandler<HTMLButtonElement>; 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, identifier: string,
item: getButtonItem, item: MessagePopoverButtonFactory,
) { ) {
buttons.set(identifier, item); buttons.set(identifier, item);
} }
export function removeButton(identifier: string) { export function removeMessagePopoverButton(identifier: string) {
buttons.delete(identifier); buttons.delete(identifier);
} }
export function _buildPopoverElements( export function _buildPopoverElements(
Component: React.ComponentType<ButtonItem>, Component: React.ComponentType<MessagePopoverButtonItem>,
message: Message message: Message
) { ) {
const items: React.ReactNode[] = []; const items: React.ReactNode[] = [];

View file

@ -16,41 +16,36 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Logger } from "@utils/Logger"; import ErrorBoundary from "@components/ErrorBoundary";
import { JSX } from "react"; import { ComponentType } from "react";
const logger = new Logger("ServerListAPI");
export const enum ServerListRenderPosition { export const enum ServerListRenderPosition {
Above, Above,
In, In,
} }
const renderFunctionsAbove = new Set<Function>(); const componentsAbove = new Set<ComponentType>();
const renderFunctionsIn = new Set<Function>(); const componentsBelow = new Set<ComponentType>();
function getRenderFunctions(position: ServerListRenderPosition) { 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); getRenderFunctions(position).add(renderFunction);
} }
export function removeServerListElement(position: ServerListRenderPosition, renderFunction: Function) { export function removeServerListElement(position: ServerListRenderPosition, renderFunction: ComponentType) {
getRenderFunctions(position).delete(renderFunction); getRenderFunctions(position).delete(renderFunction);
} }
export const renderAll = (position: ServerListRenderPosition) => { export const renderAll = (position: ServerListRenderPosition) => {
const ret: Array<JSX.Element> = []; return Array.from(
getRenderFunctions(position),
for (const renderFunction of getRenderFunctions(position)) { (Component, i) => (
try { <ErrorBoundary noop key={i}>
ret.unshift(renderFunction()); <Component />
} catch (e) { </ErrorBoundary>
logger.error("Failed to render server list element:", e); )
} );
}
return ret;
}; };

View file

@ -220,6 +220,17 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
} }
} }
export function migratePluginSetting(pluginName: string, oldSetting: string, newSetting: string) {
const settings = SettingsStore.plain.plugins[pluginName];
if (!settings) return;
if (!Object.hasOwn(settings, oldSetting) || Object.hasOwn(settings, newSetting)) return;
settings[newSetting] = settings[oldSetting];
delete settings[oldSetting];
SettingsStore.markAsChanged();
}
export function definePluginSettings< export function definePluginSettings<
Def extends SettingsDefinition, Def extends SettingsDefinition,
Checks extends SettingsChecks<Def>, Checks extends SettingsChecks<Def>,

View file

@ -70,8 +70,7 @@ const ErrorBoundary = LazyComponent(() => {
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
this.props.onError?.({ error, errorInfo, props: this.props.wrappedProps }); this.props.onError?.({ error, errorInfo, props: this.props.wrappedProps });
logger.error("A component threw an Error\n", error); logger.error(`${this.props.message || "A component threw an Error"}\n`, error, errorInfo.componentStack);
logger.error("Component Stack", errorInfo.componentStack);
} }
render() { render() {

View file

@ -37,6 +37,7 @@ import { Constructor } from "type-fest";
import { PluginMeta } from "~plugins"; import { PluginMeta } from "~plugins";
import { import {
ISettingCustomElementProps,
ISettingElementProps, ISettingElementProps,
SettingBooleanComponent, SettingBooleanComponent,
SettingCustomComponent, SettingCustomComponent,
@ -74,14 +75,15 @@ function makeDummyUser(user: { username: string; id?: string; avatar?: string; }
return newUser; return newUser;
} }
const Components: Record<OptionType, React.ComponentType<ISettingElementProps<any>>> = { const Components: Record<OptionType, React.ComponentType<ISettingElementProps<any> | ISettingCustomElementProps<any>>> = {
[OptionType.STRING]: SettingTextComponent, [OptionType.STRING]: SettingTextComponent,
[OptionType.NUMBER]: SettingNumericComponent, [OptionType.NUMBER]: SettingNumericComponent,
[OptionType.BIGINT]: SettingNumericComponent, [OptionType.BIGINT]: SettingNumericComponent,
[OptionType.BOOLEAN]: SettingBooleanComponent, [OptionType.BOOLEAN]: SettingBooleanComponent,
[OptionType.SELECT]: SettingSelectComponent, [OptionType.SELECT]: SettingSelectComponent,
[OptionType.SLIDER]: SettingSliderComponent, [OptionType.SLIDER]: SettingSliderComponent,
[OptionType.COMPONENT]: SettingCustomComponent [OptionType.COMPONENT]: SettingCustomComponent,
[OptionType.CUSTOM]: () => null,
}; };
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) { export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
@ -129,7 +131,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
for (const [key, value] of Object.entries(tempSettings)) { for (const [key, value] of Object.entries(tempSettings)) {
const option = plugin.options[key]; const option = plugin.options[key];
pluginSettings[key] = value; pluginSettings[key] = value;
option?.onChange?.(value);
if (option.type === OptionType.CUSTOM) continue;
if (option?.restartNeeded) restartNeeded = true; if (option?.restartNeeded) restartNeeded = true;
} }
if (restartNeeded) onRestartNeeded(); if (restartNeeded) onRestartNeeded();
@ -141,7 +144,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>; return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
} else { } else {
const options = Object.entries(plugin.options).map(([key, setting]) => { 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) { function onChange(newValue: any) {
setTempSettings(s => ({ ...s, [key]: newValue })); setTempSettings(s => ({ ...s, [key]: newValue }));

View file

@ -18,8 +18,8 @@
import { PluginOptionComponent } from "@utils/types"; import { PluginOptionComponent } from "@utils/types";
import { ISettingElementProps } from "."; import { ISettingCustomElementProps } from ".";
export function SettingCustomComponent({ option, onChange, onError }: ISettingElementProps<PluginOptionComponent>) { export function SettingCustomComponent({ option, onChange, onError }: ISettingCustomElementProps<PluginOptionComponent>) {
return option.component({ setValue: onChange, setError: onError, option }); return option.component({ setValue: onChange, setError: onError, option });
} }

View file

@ -18,7 +18,7 @@
import { DefinedSettings, PluginOptionBase } from "@utils/types"; import { DefinedSettings, PluginOptionBase } from "@utils/types";
export interface ISettingElementProps<T extends PluginOptionBase> { interface ISettingElementPropsBase<T> {
option: T; option: T;
onChange(newValue: any): void; onChange(newValue: any): void;
pluginSettings: { pluginSettings: {
@ -30,6 +30,9 @@ export interface ISettingElementProps<T extends PluginOptionBase> {
definedSettings?: DefinedSettings; definedSettings?: DefinedSettings;
} }
export type ISettingElementProps<T extends PluginOptionBase> = ISettingElementPropsBase<T>;
export type ISettingCustomElementProps<T extends Omit<PluginOptionBase, "description" | "placeholder">> = ISettingElementPropsBase<T>;
export * from "../../Badge"; export * from "../../Badge";
export * from "./SettingBooleanComponent"; export * from "./SettingBooleanComponent";
export * from "./SettingCustomComponent"; export * from "./SettingCustomComponent";

View file

@ -62,14 +62,21 @@ async function runReporter() {
if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw new Error("Webpack Find Fail"); if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw new Error("Webpack Find Fail");
} catch (e) { } catch (e) {
let logMessage = searchType; let logMessage = searchType;
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`; if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`; if (args[0].$$vencordProps != null) {
else if (method === "mapMangledModule") { logMessage += `(${args[0].$$vencordProps.map(arg => `"${arg}"`).join(", ")})`;
} else {
logMessage += `(${args[0].toString().slice(0, 147)}...)`;
}
} else if (method === "extractAndLoadChunks") {
logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
} else if (method === "mapMangledModule") {
const failedMappings = Object.keys(args[1]).filter(key => result?.[key] == null); const failedMappings = Object.keys(args[1]).filter(key => result?.[key] == null);
logMessage += `("${args[0]}", {\n${failedMappings.map(mapping => `\t${mapping}: ${args[1][mapping].toString().slice(0, 147)}...`).join(",\n")}\n})`; logMessage += `("${args[0]}", {\n${failedMappings.map(mapping => `\t${mapping}: ${args[1][mapping].toString().slice(0, 147)}...`).join(",\n")}\n})`;
} else {
logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
} }
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
ReporterLogger.log("Webpack Find Fail:", logMessage); ReporterLogger.log("Webpack Find Fail:", logMessage);
} }

View file

@ -28,7 +28,7 @@ import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { isPluginDev } from "@utils/misc"; import { isPluginDev } from "@utils/misc";
import { closeModal, Modals, openModal } from "@utils/modal"; import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { Forms, Toasts, UserStore } from "@webpack/common"; import { Forms, Toasts, UserStore } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
@ -102,8 +102,9 @@ export default definePlugin({
} }
}, },
userProfileBadge: ContributorBadge,
async start() { async start() {
Vencord.Api.Badges.addBadge(ContributorBadge);
await loadBadges(); await loadBadges();
}, },
@ -143,8 +144,8 @@ export default definePlugin({
closeModal(modalKey); closeModal(modalKey);
VencordNative.native.openExternal("https://github.com/sponsors/Vendicated"); VencordNative.native.openExternal("https://github.com/sponsors/Vendicated");
}}> }}>
<Modals.ModalRoot {...props}> <ModalRoot {...props}>
<Modals.ModalHeader> <ModalHeader>
<Flex style={{ width: "100%", justifyContent: "center" }}> <Flex style={{ width: "100%", justifyContent: "center" }}>
<Forms.FormTitle <Forms.FormTitle
tag="h2" tag="h2"
@ -158,8 +159,8 @@ export default definePlugin({
Vencord Donor Vencord Donor
</Forms.FormTitle> </Forms.FormTitle>
</Flex> </Flex>
</Modals.ModalHeader> </ModalHeader>
<Modals.ModalContent> <ModalContent>
<Flex> <Flex>
<img <img
role="presentation" role="presentation"
@ -182,13 +183,13 @@ export default definePlugin({
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!! Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
</Forms.FormText> </Forms.FormText>
</div> </div>
</Modals.ModalContent> </ModalContent>
<Modals.ModalFooter> <ModalFooter>
<Flex style={{ width: "100%", justifyContent: "center" }}> <Flex style={{ width: "100%", justifyContent: "center" }}>
<DonateButton /> <DonateButton />
</Flex> </Flex>
</Modals.ModalFooter> </ModalFooter>
</Modals.ModalRoot> </ModalRoot>
</ErrorBoundary> </ErrorBoundary>
)); ));
}, },

View file

@ -12,11 +12,15 @@ export default definePlugin({
description: "API to add buttons to the chat input", description: "API to add buttons to the chat input",
authors: [Devs.Ven], authors: [Devs.Ven],
patches: [{ patches: [
{
find: '"sticker")', find: '"sticker")',
replacement: { replacement: {
match: /return\(!\i\.\i&&(?=\(\i\.isDM.+?(\i)\.push\(.{0,50}"gift")/, match: /return\((!)?\i\.\i(?:\|\||&&)(?=\(\i\.isDM.+?(\i)\.push)/,
replace: "$&(Vencord.Api.ChatButtons._injectButtons($1,arguments[0]),true)&&" replace: (m, not, children) => not
? `${m}(Vencord.Api.ChatButtons._injectButtons(${children},arguments[0]),true)&&`
: `${m}(Vencord.Api.ChatButtons._injectButtons(${children},arguments[0]),false)||`
} }
}] }
]
}); });

View file

@ -34,12 +34,22 @@ export default definePlugin({
} }
}, },
{ {
find: ".Menu,{", find: "navId:",
all: true, all: true,
replacement: { noWarn: true,
match: /Menu,{(?<=\.jsxs?\)\(\i\.Menu,{)/g, replacement: [
replace: "$&contextMenuApiArguments:typeof arguments!=='undefined'?arguments:[]," {
match: /navId:(?=.+?([,}].*?\)))/g,
replace: (m, rest) => {
// Check if this navId: match is a destructuring statement, ignore it if it is
const destructuringMatch = rest.match(/}=.+/);
if (destructuringMatch == null) {
return `contextMenuAPIArguments:typeof arguments!=='undefined'?arguments:[],${m}`;
}
return m;
} }
} }
] ]
}
]
}); });

View file

@ -0,0 +1,68 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import { canonicalizeMatch } from "@utils/patches";
import definePlugin from "@utils/types";
// duplicate values have multiple branches with different types. Just include all to be safe
const nameMap = {
radio: "MenuRadioItem",
separator: "MenuSeparator",
checkbox: "MenuCheckboxItem",
groupstart: "MenuGroup",
control: "MenuControlItem",
compositecontrol: "MenuControlItem",
item: "MenuItem",
customitem: "MenuItem",
};
export default definePlugin({
name: "MenuItemDemanglerAPI",
description: "Demangles Discord's Menu Item module",
authors: [Devs.Ven],
required: true,
patches: [
{
find: '"Menu API',
replacement: {
match: /function.{0,80}type===(\i\.\i)\).{0,50}navigable:.+?Menu API/s,
replace: (m, mod) => {
const nameAssignments = [] as string[];
// if (t.type === m.MenuItem)
const typeCheckRe = canonicalizeMatch(/\(\i\.type===(\i\.\i)\)/g);
// push({type:"item"})
const pushTypeRe = /type:"(\w+)"/g;
let typeMatch: RegExpExecArray | null;
// for each if (t.type === ...)
while ((typeMatch = typeCheckRe.exec(m)) !== null) {
// extract the current menu item
const item = typeMatch[1];
// Set the starting index of the second regex to that of the first to start
// matching from after the if
pushTypeRe.lastIndex = typeCheckRe.lastIndex;
// extract the first type: "..."
const type = pushTypeRe.exec(m)?.[1];
if (type && type in nameMap) {
const name = nameMap[type];
nameAssignments.push(`Object.defineProperty(${item},"name",{value:"${name}"})`);
}
}
if (nameAssignments.length < 6) {
console.warn("[MenuItemDemanglerAPI] Expected to at least remap 6 items, only remapped", nameAssignments.length);
}
// Merge all our redefines with the actual module
return `${nameAssignments.join(";")};${m}`;
},
},
},
],
});

View file

@ -65,7 +65,7 @@ export default definePlugin({
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}` replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
}, },
{ {
match: /({(?=.+?function (\i).{0,160}(\i)=\i\.useMemo.{0,140}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/, match: /({(?=.+?function (\i).{0,160}(\i)=\i\.useMemo.{0,140}return \i\.useMemo\(\(\)=>\i\(\3).+?(?:function\(\){return |\(\)=>))\2/,
replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})` replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})`
} }
] ]

View file

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addAccessory } from "@api/MessageAccessories";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings"; import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
@ -143,7 +142,7 @@ export default definePlugin({
required: true, required: true,
description: "Helps us provide support to you", description: "Helps us provide support to you",
authors: [Devs.Ven], authors: [Devs.Ven],
dependencies: ["UserSettingsAPI", "MessageAccessoriesAPI"], dependencies: ["UserSettingsAPI"],
settings, settings,
@ -236,23 +235,7 @@ export default definePlugin({
} }
}, },
renderContributorDmWarningCard: ErrorBoundary.wrap(({ channel }) => { renderMessageAccessory(props) {
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 => {
const buttons = [] as JSX.Element[]; const buttons = [] as JSX.Element[];
const shouldAddUpdateButton = const shouldAddUpdateButton =
@ -329,6 +312,20 @@ export default definePlugin({
return buttons.length return buttons.length
? <Flex>{buttons}</Flex> ? <Flex>{buttons}</Flex>
: null; : 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

@ -23,7 +23,7 @@ const UserProfile = findComponentByCodeLazy("UserProfilePopoutWrapper: user cann
const styles = findByPropsLazy("accountProfilePopoutWrapper"); const styles = findByPropsLazy("accountProfilePopoutWrapper");
let openAlternatePopout = false; 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 AccountPanelContextMenu = ErrorBoundary.wrap(() => {
const { prioritizeServerProfile } = settings.use(["prioritizeServerProfile"]); const { prioritizeServerProfile } = settings.use(["prioritizeServerProfile"]);

View file

@ -43,8 +43,8 @@ export default definePlugin({
// Status emojis // Status emojis
find: "#{intl::GUILD_OWNER}),children:", find: "#{intl::GUILD_OWNER}),children:",
replacement: { replacement: {
match: /(?<=\.activityEmoji,.+?animate:)\i/, match: /(\.CUSTOM_STATUS.+?animate:)\i/,
replace: "!0" replace: (_, rest) => `${rest}!0`
} }
}, },
{ {

View file

@ -17,14 +17,13 @@
*/ */
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; import { findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { useStateFromStores } from "@webpack/common"; import { Animations, useStateFromStores } from "@webpack/common";
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { ExpandedGuildFolderStore, settings } from "."; import { ExpandedGuildFolderStore, settings } from ".";
const ChannelRTCStore = findStoreLazy("ChannelRTCStore"); const ChannelRTCStore = findStoreLazy("ChannelRTCStore");
const Animations = findByPropsLazy("a", "animated", "useTransition");
const GuildsBar = findComponentByCodeLazy('("guildsnav")'); const GuildsBar = findComponentByCodeLazy('("guildsnav")');
export default ErrorBoundary.wrap(guildsBarProps => { export default ErrorBoundary.wrap(guildsBarProps => {

View file

@ -173,8 +173,8 @@ export default definePlugin({
// Disable expanding and collapsing folders transition in the normal GuildsBar sidebar // Disable expanding and collapsing folders transition in the normal GuildsBar sidebar
{ {
predicate: () => !settings.store.keepIcons, predicate: () => !settings.store.keepIcons,
match: /(?<=#{intl::SERVER_FOLDER_PLACEHOLDER}.+?useTransition\)\()/, match: /(?=,\{from:\{height)/,
replace: "$self.shouldShowTransition(arguments[0])&&" replace: "&&$self.shouldShowTransition(arguments[0])"
}, },
// If we are rendering the normal GuildsBar sidebar, we avoid rendering guilds from folders that are expanded // If we are rendering the normal GuildsBar sidebar, we avoid rendering guilds from folders that are expanded
{ {

View file

@ -83,7 +83,7 @@ export default definePlugin({
if (!role) return; if (!role) return;
if (role.colorString) { if (role.colorString) {
children.push( children.unshift(
<Menu.MenuItem <Menu.MenuItem
id="vc-copy-role-color" id="vc-copy-role-color"
label="Copy Role Color" label="Copy Role Color"
@ -93,6 +93,20 @@ export default definePlugin({
); );
} }
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
children.unshift(
<Menu.MenuItem
id="vc-edit-role"
label="Edit Role"
action={async () => {
await GuildSettingsActions.open(guild.id, "ROLES");
GuildSettingsActions.selectRole(id);
}}
icon={PencilIcon}
/>
);
}
if (role.icon) { if (role.icon) {
children.push( children.push(
<Menu.MenuItem <Menu.MenuItem
@ -110,20 +124,6 @@ export default definePlugin({
); );
} }
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
children.push(
<Menu.MenuItem
id="vc-edit-role"
label="Edit Role"
action={async () => {
await GuildSettingsActions.open(guild.id, "ROLES");
GuildSettingsActions.selectRole(id);
}}
icon={PencilIcon}
/>
);
}
} }
} }
}); });

View file

@ -21,7 +21,7 @@ import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack"; import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Constants, React, RestAPI, Tooltip } from "@webpack/common"; import { Constants, React, RestAPI, Tooltip } from "@webpack/common";
import { RenameButton } from "./components/RenameButton"; import { RenameButton } from "./components/RenameButton";
@ -34,7 +34,7 @@ const UserSettingsModal = findByPropsLazy("saveAccountChanges", "open");
const TimestampClasses = findByPropsLazy("timestampTooltip", "blockquoteContainer"); const TimestampClasses = findByPropsLazy("timestampTooltip", "blockquoteContainer");
const SessionIconClasses = findByPropsLazy("sessionIcon"); const SessionIconClasses = findByPropsLazy("sessionIcon");
const BlobMask = findExportedComponentLazy("BlobMask"); const BlobMask = findComponentByCodeLazy("!1,lowerBadgeSize:");
const settings = definePluginSettings({ const settings = definePluginSettings({
backgroundCheck: { backgroundCheck: {

View file

@ -101,8 +101,8 @@ export default definePlugin({
find: 'minimal:"contentColumnMinimal"', find: 'minimal:"contentColumnMinimal"',
replacement: [ replacement: [
{ {
match: /\(0,\i\.useTransition\)\((\i)/, match: /(?=\(0,\i\.\i\)\((\i),\{from:\{position:"absolute")/,
replace: "(_cb=>_cb(void 0,$1))||$&" replace: "(_cb=>_cb(void 0,$1))||"
}, },
{ {
match: /\i\.animated\.div/, match: /\i\.animated\.div/,

View file

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

View file

@ -17,11 +17,7 @@
*/ */
import { import {
addPreEditListener, MessageObject
addPreSendListener,
MessageObject,
removePreEditListener,
removePreSendListener
} from "@api/MessageEvents"; } from "@api/MessageEvents";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
@ -36,7 +32,18 @@ export default definePlugin({
name: "ClearURLs", name: "ClearURLs",
description: "Removes tracking garbage from URLs", description: "Removes tracking garbage from URLs",
authors: [Devs.adryd], 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) { escapeRegExp(str: string) {
return (str && reHasRegExpChar.test(str)) 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

@ -91,15 +91,12 @@ function ThemeSettings() {
const settings = definePluginSettings({ const settings = definePluginSettings({
color: { color: {
description: "Color your Discord client theme will be based around. Light mode isn't supported",
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
default: "313338", default: "313338",
component: () => <ThemeSettings /> component: ThemeSettings
}, },
resetColor: { resetColor: {
description: "Reset Theme Color",
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
default: "313338",
component: () => ( component: () => (
<Button onClick={() => onPickColor(0x313338)}> <Button onClick={() => onPickColor(0x313338)}>
Reset Theme Color Reset Theme Color

View file

@ -69,8 +69,8 @@ export default definePlugin({
{ {
find: "https://github.com/highlightjs/highlight.js/issues/2277", find: "https://github.com/highlightjs/highlight.js/issues/2277",
replacement: { replacement: {
match: /(?<=&&\()console.log\(`Deprecated.+?`\),/, match: /\(console.log\(`Deprecated.+?`\),/,
replace: "" replace: "("
} }
}, },
{ {
@ -95,10 +95,9 @@ export default definePlugin({
} }
}, },
{ {
find: 'console.warn("[DEPRECATED] Please use `subscribeWithSelector` middleware");', find: '"AppCrashedFatalReport: getLastCrash not supported."',
all: true,
replacement: { replacement: {
match: /console\.warn\("\[DEPRECATED\] Please use `subscribeWithSelector` middleware"\);/, match: /console\.log\("AppCrashedFatalReport: getLastCrash not supported\."\);/,
replace: "" replace: ""
} }
}, },

View file

@ -63,7 +63,7 @@ function makeShortcuts() {
default: default:
const uniqueMatches = [...new Set(matches)]; const uniqueMatches = [...new Set(matches)];
if (uniqueMatches.length > 1) if (uniqueMatches.length > 1)
console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches); console.warn(`Warning: This filter matches ${uniqueMatches.length} exports. Make it more specific!\n`, uniqueMatches);
return matches[0]; return matches[0];
} }
@ -165,11 +165,38 @@ function loadAndCacheShortcut(key: string, val: any, forceLoad: boolean) {
const currentVal = val.getter(); const currentVal = val.getter();
if (!currentVal || val.preload === false) return currentVal; if (!currentVal || val.preload === false) return currentVal;
const value = currentVal[SYM_LAZY_GET] function unwrapProxy(value: any) {
? forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED] if (value[SYM_LAZY_GET]) {
: currentVal; forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED];
} else if (value.$$vencordInternal) {
return forceLoad ? value.$$vencordInternal() : value;
}
if (value) define(window.shortcutList, key, { value }); return value;
}
const value = unwrapProxy(currentVal);
if (typeof value === "object" && value !== null) {
const descriptors = Object.getOwnPropertyDescriptors(value);
for (const propKey in descriptors) {
if (value[propKey] == null) continue;
const descriptor = descriptors[propKey];
if (descriptor.writable === true || descriptor.set != null) {
const currentValue = value[propKey];
const newValue = unwrapProxy(currentValue);
if (newValue != null && currentValue !== newValue) {
value[propKey] = newValue;
}
}
}
}
if (value != null) {
define(window.shortcutList, key, { value });
define(window, key, { value });
}
return value; return value;
} }

View file

@ -42,10 +42,10 @@ export default definePlugin({
// Only one of the two patches will be at effect; Discord often updates to switch between them. // Only one of the two patches will be at effect; Discord often updates to switch between them.
// See: https://discord.com/channels/1015060230222131221/1032770730703716362/1261398512017477673 // See: https://discord.com/channels/1015060230222131221/1032770730703716362/1261398512017477673
{ {
find: ".ENTER&&(!", find: ".selectPreviousCommandOption(",
replacement: { replacement: {
match: /(?<=(\i)\.which===\i\.\i.ENTER&&).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=&&\(\i\.preventDefault)/, match: /(?<=(\i)\.which(?:!==|===)\i\.\i.ENTER(\|\||&&)).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=(?:\|\||&&)\(\i\.preventDefault)/,
replace: "$self.shouldSubmit($1, $2)" replace: (_, event, condition, codeblock) => `${condition === "||" ? "!" : ""}$self.shouldSubmit(${event},${codeblock})`
} }
}, },
{ {

View file

@ -17,7 +17,6 @@ import DecorSection from "./ui/components/DecorSection";
export const settings = definePluginSettings({ export const settings = definePluginSettings({
changeDecoration: { changeDecoration: {
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
description: "Change your avatar decoration",
component() { component() {
if (!Vencord.Plugins.plugins.Decor.started) return <Forms.FormText> if (!Vencord.Plugins.plugins.Decor.started) return <Forms.FormText>
Enable Decor and restart your client to change your avatar decoration. Enable Decor and restart your client to change your avatar decoration.

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents"; import { addMessagePreEditListener, addMessagePreSendListener, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies"; import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies";
@ -235,7 +235,7 @@ export default definePlugin({
} }
}, },
{ {
find: ".PREMIUM_LOCKED;", find: ".GUILD_SUBSCRIPTION_UNAVAILABLE;",
group: true, group: true,
predicate: () => settings.store.enableEmojiBypass, predicate: () => settings.store.enableEmojiBypass,
replacement: [ replacement: [
@ -256,8 +256,10 @@ export default definePlugin({
}, },
{ {
// Disallow the emoji for premium locked if the intention doesn't allow it // Disallow the emoji for premium locked if the intention doesn't allow it
match: /!\i\.\i\.canUseEmojisEverywhere\(\i\)/, match: /(!)?(\i\.\i\.canUseEmojisEverywhere\(\i\))/,
replace: m => `(${m}&&!${IS_BYPASSEABLE_INTENTION})` replace: (m, not) => not
? `(${m}&&!${IS_BYPASSEABLE_INTENTION})`
: `(${m}||${IS_BYPASSEABLE_INTENTION})`
}, },
{ {
// Allow animated emojis to be used if the intention allows it // Allow animated emojis to be used if the intention allows it
@ -853,7 +855,7 @@ export default definePlugin({
}); });
} }
this.preSend = addPreSendListener(async (channelId, messageObj, extra) => { this.preSend = addMessagePreSendListener(async (channelId, messageObj, extra) => {
const { guildId } = this; const { guildId } = this;
let hasBypass = false; let hasBypass = false;
@ -941,7 +943,7 @@ export default definePlugin({
return { cancel: false }; return { cancel: false };
}); });
this.preEdit = addPreEditListener(async (channelId, __, messageObj) => { this.preEdit = addMessagePreEditListener(async (channelId, __, messageObj) => {
if (!s.enableEmojiBypass) return; if (!s.enableEmojiBypass) return;
let hasBypass = false; let hasBypass = false;
@ -973,7 +975,7 @@ export default definePlugin({
}, },
stop() { stop() {
removePreSendListener(this.preSend); removeMessagePreSendListener(this.preSend);
removePreEditListener(this.preEdit); removeMessagePreEditListener(this.preEdit);
} }
}); });

View file

@ -22,11 +22,11 @@ import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord"; import { getIntlMessage } from "@utils/discord";
import { NoopComponent } from "@utils/react"; import { NoopComponent } from "@utils/react";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { filters, findByPropsLazy, waitFor } from "@webpack"; import { filters, findByCodeLazy, waitFor } from "@webpack";
import { ChannelStore, ContextMenuApi, UserStore } from "@webpack/common"; import { ChannelStore, ContextMenuApi, UserStore } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
const { useMessageMenu } = findByPropsLazy("useMessageMenu"); const useMessageMenu = findByCodeLazy(".MESSAGE,commandTargetId:");
interface CopyIdMenuItemProps { interface CopyIdMenuItemProps {
id: string; id: string;

View file

@ -26,7 +26,7 @@ import { findComponentByCodeLazy } from "@webpack";
import style from "./style.css?managed"; import style from "./style.css?managed";
const Button = findComponentByCodeLazy("Button.Sizes.NONE,disabled:"); const Button = findComponentByCodeLazy(".NONE,disabled:", ".PANEL_BUTTON");
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!; const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;

View file

@ -17,11 +17,11 @@
*/ */
import { get, set } from "@api/DataStore"; import { get, set } from "@api/DataStore";
import { addButton, removeButton } from "@api/MessagePopover";
import { ImageInvisible, ImageVisible } from "@components/Icons"; import { ImageInvisible, ImageVisible } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { ChannelStore } from "@webpack/common"; import { ChannelStore } from "@webpack/common";
import { MessageSnapshot } from "@webpack/types";
let style: HTMLStyleElement; let style: HTMLStyleElement;
@ -38,18 +38,14 @@ export default definePlugin({
name: "HideAttachments", name: "HideAttachments",
description: "Hide attachments and Embeds for individual messages via hover button", description: "Hide attachments and Embeds for individual messages via hover button",
authors: [Devs.Ven], authors: [Devs.Ven],
dependencies: ["MessagePopoverAPI"],
async start() { renderMessagePopoverButton(msg) {
style = document.createElement("style"); // @ts-ignore - discord-types lags behind discord.
style.id = "VencordHideAttachments"; const hasAttachmentsInShapshots = msg.messageSnapshots.some(
document.head.appendChild(style); (snapshot: MessageSnapshot) => snapshot?.message.attachments.length
);
await getHiddenMessages(); if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length && !hasAttachmentsInShapshots) return null;
await this.buildCss();
addButton("HideAttachments", msg => {
if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length) return null;
const isHidden = hiddenMessages.has(msg.id); const isHidden = hiddenMessages.has(msg.id);
@ -60,13 +56,20 @@ export default definePlugin({
channel: ChannelStore.getChannel(msg.channel_id), channel: ChannelStore.getChannel(msg.channel_id),
onClick: () => this.toggleHide(msg.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() { stop() {
style.remove(); style.remove();
hiddenMessages.clear(); hiddenMessages.clear();
removeButton("HideAttachments");
}, },
async buildCss() { async buildCss() {

View file

@ -27,7 +27,7 @@ export default definePlugin({
{ {
find: "hasFlag:{writable", find: "hasFlag:{writable",
replacement: { replacement: {
match: /if\((\i)<=(?:1<<30|1073741824)\)return/, match: /if\((\i)<=(?:0x40000000|(?:1<<30|1073741824))\)return/,
replace: "if($1===(1<<20))return false;$&", replace: "if($1===(1<<20))return false;$&",
}, },
}, },

View file

@ -4,7 +4,6 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import * as DataStore from "@api/DataStore";
import { definePluginSettings, Settings } from "@api/Settings"; import { definePluginSettings, Settings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings"; import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
@ -62,7 +61,7 @@ const ToggleIconOff = (activity: IgnoredActivity, fill: string) => ToggleIcon(ac
function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) { function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) {
const s = settings.use(["ignoredActivities"]); 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)"); if (ignoredActivities.some(act => act.id === activity.id)) return ToggleIconOff(activity, "var(--status-danger)");
return ToggleIconOn(activity, isPlaying ? "var(--green-300)" : "var(--primary-400)"); 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) { function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>, activity: IgnoredActivity) {
e.stopPropagation(); e.stopPropagation();
const ignoredActivityIndex = getIgnoredActivities().findIndex(act => act.id === activity.id); const ignoredActivityIndex = settings.store.ignoredActivities.findIndex(act => act.id === activity.id);
if (ignoredActivityIndex === -1) settings.store.ignoredActivities = getIgnoredActivities().concat(activity); if (ignoredActivityIndex === -1) settings.store.ignoredActivities.push(activity);
else settings.store.ignoredActivities = getIgnoredActivities().filter((_, index) => index !== ignoredActivityIndex); else settings.store.ignoredActivities.splice(ignoredActivityIndex, 1);
recalculateActivities();
} }
function recalculateActivities() { function recalculateActivities() {
@ -150,8 +147,7 @@ function IdsListComponent(props: { setValue: (value: string) => void; }) {
const settings = definePluginSettings({ const settings = definePluginSettings({
importCustomRPC: { importCustomRPC: {
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
description: "", component: ImportCustomRPCComponent
component: () => <ImportCustomRPCComponent />
}, },
listMode: { listMode: {
type: OptionType.SELECT, type: OptionType.SELECT,
@ -171,7 +167,6 @@ const settings = definePluginSettings({
}, },
idsList: { idsList: {
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
description: "",
default: "", default: "",
onChange(newValue: string) { onChange(newValue: string) {
const ids = new Set(newValue.split(",").map(id => id.trim()).filter(Boolean)); const ids = new Set(newValue.split(",").map(id => id.trim()).filter(Boolean));
@ -209,14 +204,13 @@ const settings = definePluginSettings({
description: "Ignore all competing activities (These are normally special game activities)", description: "Ignore all competing activities (These are normally special game activities)",
default: false, default: false,
onChange: recalculateActivities 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) { function isActivityTypeIgnored(type: number, id?: string) {
if (id && settings.store.idsList.includes(id)) { if (id && settings.store.idsList.includes(id)) {
@ -247,7 +241,7 @@ export default definePlugin({
find: '"LocalActivityStore"', find: '"LocalActivityStore"',
replacement: [ replacement: [
{ {
match: /HANG_STATUS.+?(?=!\i\(\)\(\i,\i\)&&)(?<=(\i)\.push.+?)/, match: /\.LISTENING.+?(?=!?\i\(\)\(\i,\i\))(?<=(\i)\.push.+?)/,
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);` replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`
} }
] ]
@ -284,29 +278,14 @@ export default definePlugin({
], ],
async start() { async start() {
// Migrate allowedIds if (settings.store.ignoredActivities.length !== 0) {
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) {
const gamesSeen = RunningGameStore.getGamesSeen() as { id?: string; exePath: string; }[]; 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 (ignoredActivity.type !== ActivitiesTypes.Game) continue;
if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) { if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) {
getIgnoredActivities().splice(index, 1); settings.store.ignoredActivities.splice(index, 1);
} }
} }
} }
@ -316,11 +295,11 @@ export default definePlugin({
if (isActivityTypeIgnored(props.type, props.application_id)) return false; if (isActivityTypeIgnored(props.type, props.application_id)) return false;
if (props.application_id != null) { 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 { } else {
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath; const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
if (exePath) { if (exePath) {
return !getIgnoredActivities().some(activity => activity.id === exePath); return !settings.store.ignoredActivities.some(activity => activity.id === exePath);
} }
} }

View file

@ -81,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"]); const { square, nearestNeighbour } = settings.use(["square", "nearestNeighbour"]);
children.push( children.push(

View file

@ -50,7 +50,7 @@ export default definePlugin({
{ {
find: "#{intl::FRIENDS_SECTION_ONLINE}", find: "#{intl::FRIENDS_SECTION_ONLINE}",
replacement: { replacement: {
match: /(\(0,\i\.jsx\)\(\i\.TabBar\.Item,\{id:\i\.\i)\.BLOCKED,className:([^\s]+?)\.item,children:\i\.\i\.string\(\i\.\i#{intl::BLOCKED}\)\}\)/, match: /(\(0,\i\.jsx\)\(\i\.\i\.Item,\{id:\i\.\i)\.BLOCKED,className:([^\s]+?)\.item,children:\i\.\i\.string\(\i\.\i#{intl::BLOCKED}\)\}\)/,
replace: "$1.IMPLICIT,className:$2.item,children:\"Implicit\"}),$&" replace: "$1.IMPLICIT,className:$2.item,children:\"Implicit\"}),$&"
}, },
}, },

View file

@ -16,12 +16,19 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addProfileBadge, removeProfileBadge } from "@api/Badges";
import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { registerCommand, unregisterCommand } from "@api/Commands"; import { registerCommand, unregisterCommand } from "@api/Commands";
import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; 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 { Logger } from "@utils/Logger";
import { canonicalizeFind } from "@utils/patches"; 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 { FluxDispatcher } from "@webpack/common";
import { FluxEvents } from "@webpack/types"; import { FluxEvents } from "@webpack/types";
@ -83,6 +90,13 @@ function isReporterTestable(p: Plugin, part: ReporterTestable) {
: (p.reporterTestable & part) === part; : (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 // 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 // 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; dep.isDependency = true;
}); });
if (p.commands?.length) { if (p.commands?.length) neededApiPlugins.add("CommandsAPI");
Plugins.CommandsAPI.isDependency = true; if (p.onBeforeMessageEdit || p.onBeforeMessageSend || p.onMessageClick) neededApiPlugins.add("MessageEventsAPI");
settings.CommandsAPI.enabled = true; 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) { for (const p of pluginsValues) {
if (p.settings) { if (p.settings) {
p.settings.pluginName = p.name;
p.options ??= {}; 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]; const checks = p.settings.checks?.[name];
p.options[name] = { ...def, ...checks }; 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 (p.patches && isPluginEnabled(p.name)) {
if (!IS_REPORTER || isReporterTestable(p, ReporterTestable.Patches)) { if (!IS_REPORTER || isReporterTestable(p, ReporterTestable.Patches)) {
for (const patch of p.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) { 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) { if (p.start) {
logger.info("Starting plugin", name); logger.info("Starting plugin", name);
@ -249,7 +291,6 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
subscribePluginFluxEvents(p, FluxDispatcher); subscribePluginFluxEvents(p, FluxDispatcher);
} }
if (contextMenus) { if (contextMenus) {
logger.debug("Adding context menus patches of plugin", name); logger.debug("Adding context menus patches of plugin", name);
for (const navId in contextMenus) { 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; return true;
}, p => `startPlugin ${p.name}`); }, p => `startPlugin ${p.name}`);
export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) { 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) { if (p.stop) {
logger.info("Stopping plugin", name); 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; return true;
}, p => `stopPlugin ${p.name}`); }, p => `stopPlugin ${p.name}`);

View file

@ -16,8 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addChatBarButton, ChatBarButton } from "@api/ChatButtons"; import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { addButton, removeButton } from "@api/MessagePopover";
import { updateMessage } from "@api/MessageUpdater"; import { updateMessage } from "@api/MessageUpdater";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
@ -66,7 +65,7 @@ function Indicator() {
} }
const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
if (!isMainChat) return null; if (!isMainChat) return null;
return ( return (
@ -104,7 +103,7 @@ export default definePlugin({
name: "InvisibleChat", name: "InvisibleChat",
description: "Encrypt your Messages in a non-suspicious way!", description: "Encrypt your Messages in a non-suspicious way!",
authors: [Devs.SammCheese], authors: [Devs.SammCheese],
dependencies: ["MessagePopoverAPI", "ChatInputButtonAPI", "MessageUpdaterAPI"], dependencies: ["MessageUpdaterAPI"],
reporterTestable: ReporterTestable.Patches, reporterTestable: ReporterTestable.Patches,
settings, 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@:%_+.~#?&//=]*)/, /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/,
), ),
async start() { async start() {
addButton("InvisibleChat", message => { const { default: StegCloak } = await getStegCloak();
steggo = new StegCloak(true, false);
},
renderMessagePopoverButton(message) {
return this.INV_REGEX.test(message?.content) return this.INV_REGEX.test(message?.content)
? { ? {
label: "Decrypt Message", label: "Decrypt Message",
@ -142,18 +145,9 @@ export default definePlugin({
} }
} }
: null; : null;
});
addChatBarButton("InvisibleChat", ChatBarIcon);
const { default: StegCloak } = await getStegCloak();
steggo = new StegCloak(true, false);
}, },
stop() { renderChatBarButton: ChatBarIcon,
removeButton("InvisibleChat");
removeButton("InvisibleChat");
},
// Gets the Embed of a Link // Gets the Embed of a Link
async getEmbed(url: URL): Promise<Object | {}> { async getEmbed(url: URL): Promise<Object | {}> {

View file

@ -0,0 +1,17 @@
# IrcColors
Makes username colors in chat unique, like in IRC clients
![Chat with IrcColors and Compact++ enabled](https://github.com/Vendicated/Vencord/assets/33988779/88e05c0b-a60a-4d10-949e-8b46e1d7226c)
Improves chat readability by assigning every user an unique nickname color,
making distinguishing between different users easier. Inspired by the feature
in many IRC clients, such as HexChat or WeeChat.
Keep in mind this overrides role colors in chat, so if you wish to know
someone's role color without checking their profile, enable the role dot: go to
**User Settings**, **Accessibility** and switch **Role Colors** to **Show role
colors next to names**.
Created for use with the [Compact++](https://gitlab.com/Grzesiek11/compactplusplus-discord-theme)
theme.

View file

@ -0,0 +1,85 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { definePluginSettings } from "@api/Settings";
import { hash as h64 } from "@intrnl/xxhash64";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { useMemo } from "@webpack/common";
// Calculate a CSS color string based on the user ID
function calculateNameColorForUser(id: string) {
const { lightness } = settings.use(["lightness"]);
const idHash = useMemo(() => h64(id), [id]);
return `hsl(${idHash % 360n}, 100%, ${lightness}%)`;
}
const settings = definePluginSettings({
lightness: {
description: "Lightness, in %. Change if the colors are too light or too dark",
type: OptionType.NUMBER,
default: 70,
},
memberListColors: {
description: "Replace role colors in the member list",
restartNeeded: true,
type: OptionType.BOOLEAN,
default: true
}
});
export default definePlugin({
name: "IrcColors",
description: "Makes username colors in chat unique, like in IRC clients",
authors: [Devs.Grzesiek11],
settings,
patches: [
{
find: '="SYSTEM_TAG"',
replacement: {
match: /(?<=className:\i\.username,style:.{0,50}:void 0,)/,
replace: "style:{color:$self.calculateNameColorForMessageContext(arguments[0])},"
}
},
{
find: "#{intl::GUILD_OWNER}),children:",
replacement: {
match: /(?<=\.MEMBER_LIST}\),\[\]\),)(.+?color:)null!=.{0,50}?(?=,)/,
replace: (_, rest) => `ircColor=$self.calculateNameColorForListContext(arguments[0]),${rest}ircColor`
},
predicate: () => settings.store.memberListColors
}
],
calculateNameColorForMessageContext(context: any) {
const id = context?.message?.author?.id;
if (id == null) {
return null;
}
return calculateNameColorForUser(id);
},
calculateNameColorForListContext(context: any) {
const id = context?.user?.id;
if (id == null) {
return null;
}
return calculateNameColorForUser(id);
}
});

View file

@ -86,7 +86,7 @@ const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f";
const logger = new Logger("LastFMRichPresence"); const logger = new Logger("LastFMRichPresence");
const presenceStore = findByPropsLazy("getLocalPresence"); const PresenceStore = findByPropsLazy("getLocalPresence");
async function getApplicationAsset(key: string): Promise<string> { async function getApplicationAsset(key: string): Promise<string> {
return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0]; return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];
@ -124,6 +124,11 @@ const settings = definePluginSettings({
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
default: true, default: true,
}, },
hideWithActivity: {
description: "Hide Last.fm presence if you have any other presence",
type: OptionType.BOOLEAN,
default: false,
},
statusName: { statusName: {
description: "custom status text", description: "custom status text",
type: OptionType.STRING, type: OptionType.STRING,
@ -274,13 +279,17 @@ export default definePlugin({
}, },
async getActivity(): Promise<Activity | null> { async getActivity(): Promise<Activity | null> {
if (settings.store.hideWithSpotify) { if (settings.store.hideWithActivity) {
for (const activity of presenceStore.getActivities()) { if (PresenceStore.getActivities().some(a => a.application_id !== applicationId)) {
if (activity.type === ActivityType.LISTENING && activity.application_id !== applicationId) {
// there is already music status because of Spotify or richerCider (probably more)
return null; return null;
} }
} }
if (settings.store.hideWithSpotify) {
if (PresenceStore.getActivities().some(a => a.type === ActivityType.LISTENING && a.application_id !== applicationId)) {
// there is already music status because of Spotify or richerCider (probably more)
return null;
}
} }
const trackData = await this.fetchTrackData(); const trackData = await this.fetchTrackData();

View file

@ -57,7 +57,7 @@ export default definePlugin({
{ {
find: ".ROLE_MENTION)", find: ".ROLE_MENTION)",
replacement: { replacement: {
match: /children:\[\i&&.{0,50}\.RoleDot.{0,300},\i(?=\])/, match: /children:\[\i&&.{0,100}className:\i.roleDot,.{0,200},\i(?=\])/,
replace: "$&,$self.renderRoleIcon(arguments[0])" replace: "$&,$self.renderRoleIcon(arguments[0])"
} }
}], }],

View file

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addClickListener, removeClickListener } from "@api/MessageEvents";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
@ -57,15 +56,20 @@ export default definePlugin({
name: "MessageClickActions", name: "MessageClickActions",
description: "Hold Backspace and click to delete, double click to edit/reply", description: "Hold Backspace and click to delete, double click to edit/reply",
authors: [Devs.Ven], authors: [Devs.Ven],
dependencies: ["MessageEventsAPI"],
settings, settings,
start() { start() {
document.addEventListener("keydown", keydown); document.addEventListener("keydown", keydown);
document.addEventListener("keyup", keyup); 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; const isMe = msg.author.id === UserStore.getCurrentUser().id;
if (!isDeletePressed) { if (!isDeletePressed) {
if (event.detail < 2) return; if (event.detail < 2) return;
@ -111,12 +115,5 @@ export default definePlugin({
} }
event.preventDefault(); event.preventDefault();
} }
});
}, },
stop() {
removeClickListener(this.onClick);
document.removeEventListener("keydown", keydown);
document.removeEventListener("keyup", keyup);
}
}); });

View file

@ -9,7 +9,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards"; import { isNonNullish } from "@utils/guards";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findExportedComponentLazy } from "@webpack"; import { findComponentByCodeLazy } from "@webpack";
import { SnowflakeUtils, Tooltip } from "@webpack/common"; import { SnowflakeUtils, Tooltip } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
@ -26,7 +26,7 @@ interface Diff {
} }
const DISCORD_KT_DELAY = 1471228928; const DISCORD_KT_DELAY = 1471228928;
const HiddenVisually = findExportedComponentLazy("HiddenVisually"); const HiddenVisually = findComponentByCodeLazy(".hiddenVisually]:");
export default definePlugin({ export default definePlugin({
name: "MessageLatency", name: "MessageLatency",
@ -162,7 +162,7 @@ export default definePlugin({
</> </>
} }
</Tooltip>; </Tooltip>;
}); }, { noop: true });
}, },
Icon({ delta, fill, props }: { Icon({ delta, fill, props }: {

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addAccessory, removeAccessory } from "@api/MessageAccessories"; import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccessories";
import { updateMessage } from "@api/MessageUpdater"; import { updateMessage } from "@api/MessageUpdater";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings"; import { getUserSettingLazy } from "@api/UserSettings";
@ -120,11 +120,11 @@ const settings = definePluginSettings({
}, },
clearMessageCache: { clearMessageCache: {
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
description: "Clear the linked message cache", component: () => (
component: () =>
<Button onClick={() => messageCache.clear()}> <Button onClick={() => messageCache.clear()}>
Clear the linked message cache Clear the linked message cache
</Button> </Button>
)
} }
}); });
@ -373,7 +373,7 @@ export default definePlugin({
settings, settings,
start() { start() {
addAccessory("messageLinkEmbed", props => { addMessageAccessory("messageLinkEmbed", props => {
if (!messageLinkRegex.test(props.message.content)) if (!messageLinkRegex.test(props.message.content))
return null; return null;
@ -391,6 +391,6 @@ export default definePlugin({
}, },
stop() { stop() {
removeAccessory("messageLinkEmbed"); removeMessageAccessory("messageLinkEmbed");
} }
}); });

View file

@ -211,7 +211,8 @@ export default definePlugin({
collapseDeleted: { collapseDeleted: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Whether to collapse deleted messages, similar to blocked messages", description: "Whether to collapse deleted messages, similar to blocked messages",
default: false default: false,
restartNeeded: true,
}, },
logEdits: { logEdits: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
@ -500,7 +501,7 @@ export default definePlugin({
{ {
// Message context base menu // Message context base menu
find: "useMessageMenu:", find: ".MESSAGE,commandTargetId:",
replacement: [ replacement: [
{ {
// Remove the first section if message is deleted // Remove the first section if message is deleted

View file

@ -18,7 +18,7 @@
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage, unregisterCommand } from "@api/Commands"; import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage, unregisterCommand } from "@api/Commands";
import * as DataStore from "@api/DataStore"; import * as DataStore from "@api/DataStore";
import { Settings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
@ -29,23 +29,23 @@ const MessageTagsMarker = Symbol("MessageTags");
interface Tag { interface Tag {
name: string; name: string;
message: string; message: string;
enabled: boolean;
} }
const getTags = () => DataStore.get(DATA_KEY).then<Tag[]>(t => t ?? []); function getTags() {
const getTag = (name: string) => DataStore.get(DATA_KEY).then<Tag | null>((t: Tag[]) => (t ?? []).find((tt: Tag) => tt.name === name) ?? null); return settings.store.tagsList;
const addTag = async (tag: Tag) => { }
const tags = await getTags();
tags.push(tag); function getTag(name: string) {
DataStore.set(DATA_KEY, tags); return settings.store.tagsList[name] ?? null;
return tags; }
};
const removeTag = async (name: string) => { function addTag(tag: Tag) {
let tags = await getTags(); settings.store.tagsList[tag.name] = tag;
tags = await tags.filter((t: Tag) => t.name !== name); }
DataStore.set(DATA_KEY, tags);
return tags; function removeTag(name: string) {
}; delete settings.store.tagsList[name];
}
function createTagCommand(tag: Tag) { function createTagCommand(tag: Tag) {
registerCommand({ registerCommand({
@ -53,14 +53,14 @@ function createTagCommand(tag: Tag) {
description: tag.name, description: tag.name,
inputType: ApplicationCommandInputType.BUILT_IN_TEXT, inputType: ApplicationCommandInputType.BUILT_IN_TEXT,
execute: async (_, ctx) => { execute: async (_, ctx) => {
if (!await getTag(tag.name)) { if (!getTag(tag.name)) {
sendBotMessage(ctx.channel.id, { sendBotMessage(ctx.channel.id, {
content: `${EMOTE} The tag **${tag.name}** does not exist anymore! Please reload ur Discord to fix :)` content: `${EMOTE} The tag **${tag.name}** does not exist anymore! Please reload ur Discord to fix :)`
}); });
return { content: `/${tag.name}` }; 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!` content: `${EMOTE} The tag **${tag.name}** has been sent!`
}); });
return { content: tag.message.replaceAll("\\n", "\n") }; return { content: tag.message.replaceAll("\\n", "\n") };
@ -69,22 +69,38 @@ function createTagCommand(tag: Tag) {
}, "CustomTags"); }, "CustomTags");
} }
const settings = definePluginSettings({
export default definePlugin({
name: "MessageTags",
description: "Allows you to save messages and to use them with a simple command.",
authors: [Devs.Luna],
options: {
clyde: { clyde: {
name: "Clyde message on send", name: "Clyde message on send",
description: "If enabled, clyde will send you an ephemeral message when a tag was used.", description: "If enabled, clyde will send you an ephemeral message when a tag was used.",
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
default: true 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() { 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: [ commands: [
@ -153,19 +169,18 @@ export default definePlugin({
const name: string = findOption(args[0].options, "tag-name", ""); const name: string = findOption(args[0].options, "tag-name", "");
const message: string = findOption(args[0].options, "message", ""); const message: string = findOption(args[0].options, "message", "");
if (await getTag(name)) if (getTag(name))
return sendBotMessage(ctx.channel.id, { return sendBotMessage(ctx.channel.id, {
content: `${EMOTE} A Tag with the name **${name}** already exists!` content: `${EMOTE} A Tag with the name **${name}** already exists!`
}); });
const tag = { const tag = {
name: name, name: name,
enabled: true,
message: message message: message
}; };
createTagCommand(tag); createTagCommand(tag);
await addTag(tag); addTag(tag);
sendBotMessage(ctx.channel.id, { sendBotMessage(ctx.channel.id, {
content: `${EMOTE} Successfully created the tag **${name}**!` content: `${EMOTE} Successfully created the tag **${name}**!`
@ -175,13 +190,13 @@ export default definePlugin({
case "delete": { case "delete": {
const name: string = findOption(args[0].options, "tag-name", ""); const name: string = findOption(args[0].options, "tag-name", "");
if (!await getTag(name)) if (!getTag(name))
return sendBotMessage(ctx.channel.id, { return sendBotMessage(ctx.channel.id, {
content: `${EMOTE} A Tag with the name **${name}** does not exist!` content: `${EMOTE} A Tag with the name **${name}** does not exist!`
}); });
unregisterCommand(name); unregisterCommand(name);
await removeTag(name); removeTag(name);
sendBotMessage(ctx.channel.id, { sendBotMessage(ctx.channel.id, {
content: `${EMOTE} Successfully deleted the tag **${name}**!` content: `${EMOTE} Successfully deleted the tag **${name}**!`
@ -192,10 +207,8 @@ export default definePlugin({
sendBotMessage(ctx.channel.id, { sendBotMessage(ctx.channel.id, {
embeds: [ embeds: [
{ {
// @ts-ignore
title: "All Tags:", title: "All Tags:",
// @ts-ignore description: Object.values(getTags())
description: (await getTags())
.map(tag => `\`${tag.name}\`: ${tag.message.slice(0, 72).replaceAll("\\n", " ")}${tag.message.length > 72 ? "..." : ""}`) .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!`, .join("\n") || `${EMOTE} Woops! There are no tags yet, use \`/tags create\` to create one!`,
// @ts-ignore // @ts-ignore
@ -208,7 +221,7 @@ export default definePlugin({
} }
case "preview": { case "preview": {
const name: string = findOption(args[0].options, "tag-name", ""); const name: string = findOption(args[0].options, "tag-name", "");
const tag = await getTag(name); const tag = getTag(name);
if (!tag) if (!tag)
return sendBotMessage(ctx.channel.id, { return sendBotMessage(ctx.channel.id, {

View file

@ -1,372 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { definePluginSettings } from "@api/Settings";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord";
import { Margins } from "@utils/margins";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findLazy } from "@webpack";
import { Card, ChannelStore, Forms, GuildStore, PermissionsBits, Switch, TextInput, Tooltip } from "@webpack/common";
import type { Permissions, RC } from "@webpack/types";
import type { Channel, Guild, Message, User } from "discord-types/general";
interface Tag {
// name used for identifying, must be alphanumeric + underscores
name: string;
// name shown on the tag itself, can be anything probably; automatically uppercase'd
displayName: string;
description: string;
permissions?: Permissions[];
condition?(message: Message | null, user: User, channel: Channel): boolean;
}
interface TagSetting {
text: string;
showInChat: boolean;
showInNotChat: boolean;
}
interface TagSettings {
WEBHOOK: TagSetting,
OWNER: TagSetting,
ADMINISTRATOR: TagSetting,
MODERATOR_STAFF: TagSetting,
MODERATOR: TagSetting,
VOICE_MODERATOR: TagSetting,
TRIAL_MODERATOR: TagSetting,
[k: string]: TagSetting;
}
// PermissionStore.computePermissions will not work here since it only gets permissions for the current user
const computePermissions: (options: {
user?: { id: string; } | string | null;
context?: Guild | Channel | null;
overwrites?: Channel["permissionOverwrites"] | null;
checkElevated?: boolean /* = true */;
excludeGuildPermissions?: boolean /* = false */;
}) => bigint = findByCodeLazy(".getCurrentUser()", ".computeLurkerPermissionsAllowList()");
const Tag = findLazy(m => m.Types?.[0] === "BOT") as RC<{ type?: number, className?: string, useRemSizes?: boolean; }> & { Types: Record<string, number>; };
const isWebhook = (message: Message, user: User) => !!message?.webhookId && user.isNonUserBot();
const tags: Tag[] = [
{
name: "WEBHOOK",
displayName: "Webhook",
description: "Messages sent by webhooks",
condition: isWebhook
}, {
name: "OWNER",
displayName: "Owner",
description: "Owns the server",
condition: (_, user, channel) => GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id
}, {
name: "ADMINISTRATOR",
displayName: "Admin",
description: "Has the administrator permission",
permissions: ["ADMINISTRATOR"]
}, {
name: "MODERATOR_STAFF",
displayName: "Staff",
description: "Can manage the server, channels or roles",
permissions: ["MANAGE_GUILD", "MANAGE_CHANNELS", "MANAGE_ROLES"]
}, {
name: "MODERATOR",
displayName: "Mod",
description: "Can manage messages or kick/ban people",
permissions: ["MANAGE_MESSAGES", "KICK_MEMBERS", "BAN_MEMBERS"]
}, {
name: "VOICE_MODERATOR",
displayName: "VC Mod",
description: "Can manage voice chats",
permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"]
}, {
name: "CHAT_MODERATOR",
displayName: "Chat Mod",
description: "Can timeout people",
permissions: ["MODERATE_MEMBERS"]
}
];
const defaultSettings = Object.fromEntries(
tags.map(({ name, displayName }) => [name, { text: displayName, showInChat: true, showInNotChat: true }])
) as TagSettings;
function SettingsComponent() {
const tagSettings = settings.store.tagSettings ??= defaultSettings;
return (
<Flex flexDirection="column">
{tags.map(t => (
<Card key={t.name} style={{ padding: "1em 1em 0" }}>
<Forms.FormTitle style={{ width: "fit-content" }}>
<Tooltip text={t.description}>
{({ onMouseEnter, onMouseLeave }) => (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{t.displayName} Tag <Tag type={Tag.Types[t.name]} />
</div>
)}
</Tooltip>
</Forms.FormTitle>
<TextInput
type="text"
value={tagSettings[t.name]?.text ?? t.displayName}
placeholder={`Text on tag (default: ${t.displayName})`}
onChange={v => tagSettings[t.name].text = v}
className={Margins.bottom16}
/>
<Switch
value={tagSettings[t.name]?.showInChat ?? true}
onChange={v => tagSettings[t.name].showInChat = v}
hideBorder
>
Show in messages
</Switch>
<Switch
value={tagSettings[t.name]?.showInNotChat ?? true}
onChange={v => tagSettings[t.name].showInNotChat = v}
hideBorder
>
Show in member list and profiles
</Switch>
</Card>
))}
</Flex>
);
}
const settings = definePluginSettings({
dontShowForBots: {
description: "Don't show extra tags for bots (excluding webhooks)",
type: OptionType.BOOLEAN
},
dontShowBotTag: {
description: "Only show extra tags for bots / Hide [BOT] text",
type: OptionType.BOOLEAN
},
tagSettings: {
type: OptionType.COMPONENT,
component: SettingsComponent,
description: "fill me"
}
});
export default definePlugin({
name: "MoreUserTags",
description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)",
authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN],
settings,
patches: [
// add tags to the tag list
{
find: ".ORIGINAL_POSTER=",
replacement: {
match: /(?=(\i)\[\i\.BOT)/,
replace: "$self.genTagTypes($1);"
}
},
{
find: "#{intl::DISCORD_SYSTEM_MESSAGE_BOT_TAG_TOOLTIP_OFFICIAL}",
replacement: [
// make the tag show the right text
{
match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=(.{0,40}#{intl::APP_TAG}\))/,
replace: (_, origSwitch, variant, tags, displayedText, originalText) =>
`${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}],${originalText})}`
},
// show OP tags correctly
{
match: /(\i)=(\i)===\i(?:\.\i)?\.ORIGINAL_POSTER/,
replace: "$1=$self.isOPTag($2)"
},
// add HTML data attributes (for easier theming)
{
match: /.botText,children:(\i)}\)]/,
replace: "$&,'data-tag':$1.toLowerCase()"
}
],
},
// in messages
{
find: ".Types.ORIGINAL_POSTER",
replacement: {
match: /;return\((\(null==\i\?void 0:\i\.isSystemDM\(\).+?.Types.ORIGINAL_POSTER\)),null==(\i)\)/,
replace: ";$1;$2=$self.getTag({...arguments[0],origType:$2,location:'chat'});return $2 == null"
}
},
// in the member list
{
find: "#{intl::GUILD_OWNER}),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'"
}
},
// pass channel id down props to be used in profiles
{
find: ".hasAvatarForGuild(null==",
replacement: {
match: /(?=usernameIcon:)/,
replace: "moreTags_channelId:arguments[0].channelId,"
}
},
{
find: "#{intl::USER_PROFILE_PRONOUNS}",
replacement: {
match: /(?=,hideBotTag:!0)/,
replace: ",moreTags_channelId:arguments[0].moreTags_channelId"
}
},
// in profiles
{
find: ",overrideDiscriminator:",
group: true,
replacement: [
{
// prevent channel id from getting ghosted
// it's either this or extremely long lookbehind
match: /user:\i,nick:\i,/,
replace: "$&moreTags_channelId,"
}, {
match: /,botType:(\i),botVerified:(\i),(?!discriminatorClass:)(?<=user:(\i).+?)/g,
replace: ",botType:$self.getTag({user:$3,channelId:moreTags_channelId,origType:$1,location:'not-chat'}),botVerified:$2,"
}
]
},
],
start() {
settings.store.tagSettings ??= defaultSettings;
// newly added field might be missing from old users
settings.store.tagSettings.CHAT_MODERATOR ??= {
text: "Chat Mod",
showInChat: true,
showInNotChat: true
};
},
getPermissions(user: User, channel: Channel): string[] {
const guild = GuildStore.getGuild(channel?.guild_id);
if (!guild) return [];
const permissions = computePermissions({ user, context: guild, overwrites: channel.permissionOverwrites });
return Object.entries(PermissionsBits)
.map(([perm, permInt]) =>
permissions & permInt ? perm : ""
)
.filter(Boolean);
},
genTagTypes(obj) {
let i = 100;
tags.forEach(({ name }) => {
obj[name] = ++i;
obj[i] = name;
obj[`${name}-BOT`] = ++i;
obj[i] = `${name}-BOT`;
obj[`${name}-OP`] = ++i;
obj[i] = `${name}-OP`;
});
},
isOPTag: (tag: number) => tag === Tag.Types.ORIGINAL_POSTER || tags.some(t => tag === Tag.Types[`${t.name}-OP`]),
getTagText(passedTagName: string, originalText: string) {
try {
const [tagName, variant] = passedTagName.split("-");
if (!passedTagName) return getIntlMessage("APP_TAG");
const tag = tags.find(({ name }) => tagName === name);
if (!tag) return getIntlMessage("APP_TAG");
if (variant === "BOT" && tagName !== "WEBHOOK" && this.settings.store.dontShowForBots) return getIntlMessage("APP_TAG");
const tagText = settings.store.tagSettings?.[tag.name]?.text || tag.displayName;
switch (variant) {
case "OP":
return `${getIntlMessage("BOT_TAG_FORUM_ORIGINAL_POSTER")}${tagText}`;
case "BOT":
return `${getIntlMessage("APP_TAG")}${tagText}`;
default:
return tagText;
}
} catch {
return originalText;
}
},
getTag({
message, user, channelId, origType, location, channel
}: {
message?: Message,
user: User & { isClyde(): boolean; },
channel?: Channel & { isForumPost(): boolean; isMediaPost(): boolean; },
channelId?: string;
origType?: number;
location: "chat" | "not-chat";
}): number | null {
if (!user)
return null;
if (location === "chat" && user.id === "1")
return Tag.Types.OFFICIAL;
if (user.isClyde())
return Tag.Types.AI;
let type = typeof origType === "number" ? origType : null;
channel ??= ChannelStore.getChannel(channelId!) as any;
if (!channel) return type;
const settings = this.settings.store;
const perms = this.getPermissions(user, channel);
for (const tag of tags) {
if (location === "chat" && !settings.tagSettings[tag.name].showInChat) continue;
if (location === "not-chat" && !settings.tagSettings[tag.name].showInNotChat) continue;
// If the owner tag is disabled, and the user is the owner of the guild,
// avoid adding other tags because the owner will always match the condition for them
if (
tag.name !== "OWNER" &&
GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id &&
(location === "chat" && !settings.tagSettings.OWNER.showInChat) ||
(location === "not-chat" && !settings.tagSettings.OWNER.showInNotChat)
) continue;
if (
tag.permissions?.some(perm => perms.includes(perm)) ||
(tag.condition?.(message!, user, channel))
) {
if ((channel.isForumPost() || channel.isMediaPost()) && channel.ownerId === user.id)
type = Tag.Types[`${tag.name}-OP`];
else if (user.bot && !isWebhook(message!, user) && !settings.dontShowBotTag)
type = Tag.Types[`${tag.name}-BOT`];
else
type = Tag.Types[tag.name];
break;
}
}
return type;
}
});

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Settings } from "@api/Settings"; import { definePluginSettings, migratePluginSetting } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { runtimeHashMessageKey } from "@utils/intlHash"; import { runtimeHashMessageKey } from "@utils/intlHash";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
@ -32,10 +32,29 @@ interface MessageDeleteProps {
collapsedReason: () => any; collapsedReason: () => any;
} }
// Remove this migration once enough time has passed
migratePluginSetting("NoBlockedMessages", "ignoreBlockedMessages", "ignoreMessages");
const settings = definePluginSettings({
ignoreMessages: {
description: "Completely ignores incoming messages from blocked and ignored (if enabled) users",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true
},
applyToIgnoredUsers: {
description: "Additionally apply to 'ignored' users",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: false
}
});
export default definePlugin({ export default definePlugin({
name: "NoBlockedMessages", name: "NoBlockedMessages",
description: "Hides all blocked messages from chat completely.", description: "Hides all blocked/ignored messages from chat completely",
authors: [Devs.rushii, Devs.Samu], authors: [Devs.rushii, Devs.Samu, Devs.jamesbt365],
settings,
patches: [ patches: [
{ {
find: "#{intl::BLOCKED_MESSAGES_HIDE}", find: "#{intl::BLOCKED_MESSAGES_HIDE}",
@ -51,38 +70,40 @@ export default definePlugin({
'"ReadStateStore"' '"ReadStateStore"'
].map(find => ({ ].map(find => ({
find, find,
predicate: () => Settings.plugins.NoBlockedMessages.ignoreBlockedMessages === true, predicate: () => settings.store.ignoreMessages,
replacement: [ replacement: [
{ {
match: /(?<=function (\i)\((\i)\){)(?=.*MESSAGE_CREATE:\1)/, match: /(?<=function (\i)\((\i)\){)(?=.*MESSAGE_CREATE:\1)/,
replace: (_, _funcName, props) => `if($self.isBlocked(${props}.message))return;` replace: (_, _funcName, props) => `if($self.shouldIgnoreMessage(${props}.message))return;`
} }
] ]
})) }))
], ],
options: {
ignoreBlockedMessages: {
description: "Completely ignores (recent) incoming messages from blocked users (locally).",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true,
},
},
isBlocked(message: Message) { shouldIgnoreMessage(message: Message) {
try { try {
return RelationshipStore.isBlocked(message.author.id); if (RelationshipStore.isBlocked(message.author.id)) {
} catch (e) { return true;
new Logger("NoBlockedMessages").error("Failed to check if user is blocked:", e);
} }
}, return settings.store.applyToIgnoredUsers && RelationshipStore.isIgnored(message.author.id);
shouldHide(props: MessageDeleteProps) {
try {
return props.collapsedReason() === i18n.t[runtimeHashMessageKey("BLOCKED_MESSAGE_COUNT")]();
} catch (e) { } catch (e) {
console.error(e); new Logger("NoBlockedMessages").error("Failed to check if user is blocked or ignored:", e);
}
return false; return false;
} }
},
shouldHide(props: MessageDeleteProps): boolean {
try {
const collapsedReason = props.collapsedReason();
const blockedReason = i18n.t[runtimeHashMessageKey("BLOCKED_MESSAGE_COUNT")]();
const ignoredReason = settings.store.applyToIgnoredUsers
? i18n.t[runtimeHashMessageKey("IGNORED_MESSAGE_COUNT")]()
: null;
return collapsedReason === blockedReason || collapsedReason === ignoredReason;
} catch (e) {
console.error(e);
return false;
}
}
}); });

View file

@ -100,8 +100,8 @@ export default definePlugin({
replace: "true" replace: "true"
}, },
{ {
match: /!\(0,\i\.isDesktop\)\(\)/, match: /(!)?\(0,\i\.isDesktop\)\(\)/,
replace: "false" replace: (_, not) => not ? "false" : "true"
} }
] ]
}, },

View file

@ -46,8 +46,8 @@ export default definePlugin({
find: "#{intl::ONBOARDING_CHANNEL_THRESHOLD_WARNING}", find: "#{intl::ONBOARDING_CHANNEL_THRESHOLD_WARNING}",
replacement: [ replacement: [
{ {
match: /{(\i:function\(\){return \i},?){2}}/, match: /{(?:\i:(?:function\(\){return |\(\)=>)\i}?,?){2}}/,
replace: m => m.replaceAll(canonicalizeMatch(/return \i/g), "return ()=>Promise.resolve(true)") replace: m => m.replaceAll(canonicalizeMatch(/(function\(\){return |\(\)=>)\i/g), "$1()=>Promise.resolve(true)")
} }
], ],
predicate: () => settings.store.onboarding predicate: () => settings.store.onboarding

View file

@ -6,12 +6,11 @@
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal"; import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal";
import { extractAndLoadChunksLazy, findComponentByCodeLazy, findExportedComponentLazy } from "@webpack"; import { extractAndLoadChunksLazy, findComponentByCodeLazy } 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 { DEFAULT_COLOR, SWATCHES } from "../constants";
import { categories, Category, createCategory, getCategory, updateCategory } from "../data"; import { categoryLen, createCategory, getCategory } from "../data";
import { forceUpdate } from "../index";
interface ColorPickerProps { interface ColorPickerProps {
color: number | null; color: number | null;
@ -31,7 +30,7 @@ interface ColorPickerWithSwatchesProps {
} }
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)"); const ColorPicker = findComponentByCodeLazy<ColorPickerProps>("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)");
const ColorPickerWithSwatches = findExportedComponentLazy<ColorPickerWithSwatchesProps>("ColorPicker", "CustomColorPicker"); const ColorPickerWithSwatches = findComponentByCodeLazy<ColorPickerWithSwatchesProps>('id:"color-picker"');
export const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}(\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"/);
@ -39,45 +38,45 @@ const cl = classNameFactory("vc-pindms-modal-");
interface Props { interface Props {
categoryId: string | null; categoryId: string | null;
initalChannelId: string | null; initialChannelId: string | null;
modalProps: ModalProps; modalProps: ModalProps;
} }
function useCategory(categoryId: string | null, initalChannelId: string | null) { function useCategory(categoryId: string | null, initalChannelId: string | null) {
const [category, setCategory] = useState<Category | null>(null); const category = useMemo(() => {
if (categoryId) {
useEffect(() => { return getCategory(categoryId);
if (categoryId) } else if (initalChannelId) {
setCategory(getCategory(categoryId)!); return {
else if (initalChannelId)
setCategory({
id: Toasts.genId(), id: Toasts.genId(),
name: `Pin Category ${categories.length + 1}`, name: `Pin Category ${categoryLen() + 1}`,
color: DEFAULT_COLOR, color: DEFAULT_COLOR,
collapsed: false, collapsed: false,
channels: [initalChannelId] channels: [initalChannelId]
}); };
}
}, [categoryId, initalChannelId]); }, [categoryId, initalChannelId]);
return { return category;
category,
setCategory
};
} }
export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Props) { export function NewCategoryModal({ categoryId, modalProps, initialChannelId }: Props) {
const { category, setCategory } = useCategory(categoryId, initalChannelId); const category = useCategory(categoryId, initialChannelId);
if (!category) return null; if (!category) return null;
const onSave = async (e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>) => { const [name, setName] = useState(category.name);
e.preventDefault(); const [color, setColor] = useState(category.color);
if (!categoryId)
await createCategory(category); const onSave = (e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
else e.preventDefault();
await updateCategory(category);
category.name = name;
category.color = color;
if (!categoryId) {
createCategory(category);
}
forceUpdate();
modalProps.onClose(); modalProps.onClose();
}; };
@ -93,25 +92,25 @@ export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Pr
<Forms.FormSection> <Forms.FormSection>
<Forms.FormTitle>Name</Forms.FormTitle> <Forms.FormTitle>Name</Forms.FormTitle>
<TextInput <TextInput
value={category.name} value={name}
onChange={e => setCategory({ ...category, name: e })} onChange={e => setName(e)}
/> />
</Forms.FormSection> </Forms.FormSection>
<Forms.FormDivider /> <Forms.FormDivider />
<Forms.FormSection> <Forms.FormSection>
<Forms.FormTitle>Color</Forms.FormTitle> <Forms.FormTitle>Color</Forms.FormTitle>
<ColorPickerWithSwatches <ColorPickerWithSwatches
key={category.name} key={category.id}
defaultColor={DEFAULT_COLOR} defaultColor={DEFAULT_COLOR}
colors={SWATCHES} colors={SWATCHES}
onChange={c => setCategory({ ...category, color: c! })} onChange={c => setColor(c!)}
value={category.color} value={color}
renderDefaultButton={() => null} renderDefaultButton={() => null}
renderCustomButton={() => ( renderCustomButton={() => (
<ColorPicker <ColorPicker
color={category.color} color={color}
onChange={c => setCategory({ ...category, color: c! })} onChange={c => setColor(c!)}
key={category.name} key={category.id}
showEyeDropper={false} showEyeDropper={false}
/> />
)} )}
@ -119,7 +118,7 @@ export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Pr
</Forms.FormSection> </Forms.FormSection>
</ModalContent> </ModalContent>
<ModalFooter> <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> </ModalFooter>
</form> </form>
</ModalRoot> </ModalRoot>
@ -129,6 +128,6 @@ export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Pr
export const openCategoryModal = (categoryId: string | null, channelId: string | null) => export const openCategoryModal = (categoryId: string | null, channelId: string | null) =>
openModalLazy(async () => { openModalLazy(async () => {
await requireSettingsMenu(); 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 { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Menu } from "@webpack/common"; import { Menu } from "@webpack/common";
import { addChannelToCategory, canMoveChannelInDirection, categories, isPinned, moveChannel, removeChannelFromCategory } from "../data"; import { addChannelToCategory, canMoveChannelInDirection, currentUserCategories, isPinned, moveChannel, removeChannelFromCategory } from "../data";
import { forceUpdate, PinOrder, settings } from "../index"; import { PinOrder, settings } from "../index";
import { openCategoryModal } from "./CreateCategoryModal"; import { openCategoryModal } from "./CreateCategoryModal";
function createPinMenuItem(channelId: string) { function createPinMenuItem(channelId: string) {
@ -31,12 +31,12 @@ function createPinMenuItem(channelId: string) {
<Menu.MenuSeparator /> <Menu.MenuSeparator />
{ {
categories.map(category => ( currentUserCategories.map(category => (
<Menu.MenuItem <Menu.MenuItem
key={category.id} key={category.id}
id={`pin-category-${category.id}`} id={`pin-category-${category.id}`}
label={category.name} label={category.name}
action={() => addChannelToCategory(channelId, category.id).then(forceUpdate)} action={() => addChannelToCategory(channelId, category.id)}
/> />
)) ))
} }
@ -49,7 +49,7 @@ function createPinMenuItem(channelId: string) {
id="unpin-dm" id="unpin-dm"
label="Unpin DM" label="Unpin DM"
color="danger" color="danger"
action={() => removeChannelFromCategory(channelId).then(forceUpdate)} action={() => removeChannelFromCategory(channelId)}
/> />
{ {
@ -57,7 +57,7 @@ function createPinMenuItem(channelId: string) {
<Menu.MenuItem <Menu.MenuItem
id="move-up" id="move-up"
label="Move Up" label="Move Up"
action={() => moveChannel(channelId, -1).then(forceUpdate)} action={() => moveChannel(channelId, -1)}
/> />
) )
} }
@ -67,7 +67,7 @@ function createPinMenuItem(channelId: string) {
<Menu.MenuItem <Menu.MenuItem
id="move-down" id="move-down"
label="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 * as DataStore from "@api/DataStore";
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import { useForceUpdater } from "@utils/react";
import { UserStore } from "@webpack/common"; import { UserStore } from "@webpack/common";
import { DEFAULT_COLOR } from "./constants"; import { PinOrder, PrivateChannelSortStore, settings } from "./index";
import { forceUpdate, PinOrder, PrivateChannelSortStore, settings } from "./index";
export interface Category { export interface Category {
id: string; id: string;
@ -24,104 +24,92 @@ const CATEGORY_MIGRATED_PINDMS_KEY = "PinDMsMigratedPinDMs";
const CATEGORY_MIGRATED_KEY = "PinDMsMigratedOldCategories"; const CATEGORY_MIGRATED_KEY = "PinDMsMigratedOldCategories";
const OLD_CATEGORY_KEY = "BetterPinDMsCategories-"; const OLD_CATEGORY_KEY = "BetterPinDMsCategories-";
let forceUpdateDms: (() => void) | undefined = undefined;
export let categories: Category[] = []; export let currentUserCategories: Category[] = [];
export async function saveCats(cats: Category[]) {
const { id } = UserStore.getCurrentUser();
await DataStore.set(CATEGORY_BASE_KEY + id, cats);
}
export async function init() { export async function init() {
const id = UserStore.getCurrentUser()?.id; await migrateData();
await initCategories(id);
await migrateData(id); const userId = UserStore.getCurrentUser()?.id;
forceUpdate(); if (userId == null) return;
currentUserCategories = settings.store.userBasedCategoryList[userId] ??= [];
forceUpdateDms?.();
} }
export async function initCategories(userId: string) { export function usePinnedDms() {
categories = await DataStore.get<Category[]>(CATEGORY_BASE_KEY + userId) ?? []; forceUpdateDms = useForceUpdater();
settings.use(["pinOrder", "canCollapseDmSection", "dmSectionCollapsed", "userBasedCategoryList"]);
} }
export function getCategory(id: string) { 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) { export function getCategoryByIndex(index: number) {
categories.push(category); return currentUserCategories[index];
await saveCats(categories);
} }
export async function updateCategory(category: Category) { export function createCategory(category: Category) {
const index = categories.findIndex(c => c.id === category.id); currentUserCategories.push(category);
if (index === -1) return;
categories[index] = category;
await saveCats(categories);
} }
export async function addChannelToCategory(channelId: string, categoryId: string) { export function addChannelToCategory(channelId: string, categoryId: string) {
const category = categories.find(c => c.id === categoryId); const category = currentUserCategories.find(c => c.id === categoryId);
if (!category) return; if (category == null) return;
if (category.channels.includes(channelId)) return; if (category.channels.includes(channelId)) return;
category.channels.push(channelId); category.channels.push(channelId);
await saveCats(categories);
} }
export async function removeChannelFromCategory(channelId: string) { export function removeChannelFromCategory(channelId: string) {
const category = categories.find(c => c.channels.includes(channelId)); const category = currentUserCategories.find(c => c.channels.includes(channelId));
if (!category) return; if (category == null) return;
category.channels = category.channels.filter(c => c !== channelId); category.channels = category.channels.filter(c => c !== channelId);
await saveCats(categories);
} }
export async function removeCategory(categoryId: string) { export function removeCategory(categoryId: string) {
const catagory = categories.find(c => c.id === categoryId); const categoryIndex = currentUserCategories.findIndex(c => c.id === categoryId);
if (!catagory) return; if (categoryIndex === -1) return;
// catagory?.channels.forEach(c => removeChannelFromCategory(c)); currentUserCategories.splice(categoryIndex, 1);
categories = categories.filter(c => c.id !== categoryId);
await saveCats(categories);
} }
export async function collapseCategory(id: string, value = true) { export function collapseCategory(id: string, value = true) {
const category = categories.find(c => c.id === id); const category = currentUserCategories.find(c => c.id === id);
if (!category) return; if (category == null) return;
category.collapsed = value; category.collapsed = value;
await saveCats(categories);
} }
// utils // Utils
export function isPinned(id: string) { export function isPinned(id: string) {
return categories.some(c => c.channels.includes(id)); return currentUserCategories.some(c => c.channels.includes(id));
} }
export function categoryLen() { export function categoryLen() {
return categories.length; return currentUserCategories.length;
} }
export function getAllUncollapsedChannels() { export function getAllUncollapsedChannels() {
if (settings.store.pinOrder === PinOrder.LastMessage) { if (settings.store.pinOrder === PinOrder.LastMessage) {
const sortedChannels = PrivateChannelSortStore.getPrivateChannelIds(); 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() { export function getSections() {
return categories.reduce((acc, category) => { return currentUserCategories.reduce((acc, category) => {
acc.push(category.channels.length === 0 ? 1 : category.channels.length); acc.push(category.channels.length === 0 ? 1 : category.channels.length);
return acc; return acc;
}, [] as number[]); }, [] as number[]);
} }
// move categories // Move categories
export const canMoveArrayInDirection = (array: any[], index: number, direction: -1 | 1) => { export const canMoveArrayInDirection = (array: any[], index: number, direction: -1 | 1) => {
const a = array[index]; const a = array[index];
const b = array[index + direction]; 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) => { export const canMoveCategoryInDirection = (id: string, direction: -1 | 1) => {
const index = categories.findIndex(m => m.id === id); const categoryIndex = currentUserCategories.findIndex(m => m.id === id);
return canMoveArrayInDirection(categories, index, direction); return canMoveArrayInDirection(currentUserCategories, categoryIndex, direction);
}; };
export const canMoveCategory = (id: string) => canMoveCategoryInDirection(id, -1) || canMoveCategoryInDirection(id, 1); export const canMoveCategory = (id: string) => canMoveCategoryInDirection(id, -1) || canMoveCategoryInDirection(id, 1);
export const canMoveChannelInDirection = (channelId: string, direction: -1 | 1) => { export const canMoveChannelInDirection = (channelId: string, direction: -1 | 1) => {
const category = categories.find(c => c.channels.includes(channelId)); const category = currentUserCategories.find(c => c.channels.includes(channelId));
if (!category) return false; if (category == null) return false;
const index = category.channels.indexOf(channelId); const channelIndex = category.channels.indexOf(channelId);
return canMoveArrayInDirection(category.channels, index, direction); 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]]; [array[index1], array[index2]] = [array[index2], array[index1]];
} }
// stolen from PinDMs export function moveCategory(id: string, direction: -1 | 1) {
export async function moveCategory(id: string, direction: -1 | 1) { const a = currentUserCategories.findIndex(m => m.id === id);
const a = categories.findIndex(m => m.id === id);
const b = a + direction; const b = a + direction;
swapElementsInArray(categories, a, b); swapElementsInArray(currentUserCategories, a, b);
await saveCats(categories);
} }
export async function moveChannel(channelId: string, direction: -1 | 1) { export function moveChannel(channelId: string, direction: -1 | 1) {
const category = categories.find(c => c.channels.includes(channelId)); const category = currentUserCategories.find(c => c.channels.includes(channelId));
if (!category) return; if (category == null) return;
const a = category.channels.indexOf(channelId); const a = category.channels.indexOf(channelId);
const b = a + direction; const b = a + direction;
swapElementsInArray(category.channels, a, b); swapElementsInArray(category.channels, a, b);
await saveCats(categories);
} }
// TODO: Remove DataStore PinnedDms migration once enough time has passed
async function migrateData() {
// migrate data if (Settings.plugins.PinDMs.dmSectioncollapsed != null) {
const getPinDMsPins = () => (Settings.plugins.PinDMs.pinnedDMs || void 0)?.split(",") as string[] | undefined; settings.store.dmSectionCollapsed = Settings.plugins.PinDMs.dmSectioncollapsed;
delete Settings.plugins.PinDMs.dmSectioncollapsed;
async function migratePinDMs() {
if (categories.some(m => m.id === "oldPins")) {
return await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true);
} }
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 if (pinDmsKeys.length === 0) return;
const difference = [...new Set(pindmspins)]?.filter(m => !categories.some(c => c.channels.includes(m)));
if (difference?.length) { for (const pinDmsKey of pinDmsKeys) {
categories.push({ const categories = await DataStore.get<Category[]>(pinDmsKey);
id: "oldPins", if (categories == null) continue;
name: "Pins",
color: DEFAULT_COLOR, const userId = pinDmsKey.replace(CATEGORY_BASE_KEY, "");
channels: difference settings.store.userBasedCategoryList[userId] = categories;
});
await DataStore.del(pinDmsKey);
} }
await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true); await Promise.all([DataStore.del(CATEGORY_MIGRATED_PINDMS_KEY), DataStore.del(CATEGORY_MIGRATED_KEY), DataStore.del(OLD_CATEGORY_KEY)]);
}
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);
} }

View file

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

View file

@ -18,14 +18,14 @@
import "./style.css"; import "./style.css";
import { addBadge, BadgePosition, BadgeUserArgs, ProfileBadge, removeBadge } from "@api/Badges"; import { addProfileBadge, BadgePosition, BadgeUserArgs, ProfileBadge, removeProfileBadge } from "@api/Badges";
import { addDecorator, removeDecorator } from "@api/MemberListDecorators"; import { addMemberListDecorator, removeMemberListDecorator } from "@api/MemberListDecorators";
import { addDecoration, removeDecoration } from "@api/MessageDecorations"; import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations";
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack"; import { filters, findStoreLazy, mapMangledModuleLazy } from "@webpack";
import { PresenceStore, Tooltip, UserStore } from "@webpack/common"; import { PresenceStore, Tooltip, UserStore } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
@ -70,7 +70,9 @@ const Icons = {
}; };
type Platform = keyof typeof Icons; type Platform = keyof typeof Icons;
const StatusUtils = findByPropsLazy("useStatusFillColor", "StatusTypes"); const { useStatusFillColor } = mapMangledModuleLazy(".concat(.5625*", {
useStatusFillColor: filters.byCode(".hex")
});
const PlatformIcon = ({ platform, status, small }: { platform: Platform, status: string; small: boolean; }) => { const PlatformIcon = ({ platform, status, small }: { platform: Platform, status: string; small: boolean; }) => {
const tooltip = platform === "embedded" const tooltip = platform === "embedded"
@ -79,7 +81,7 @@ const PlatformIcon = ({ platform, status, small }: { platform: Platform, status:
const Icon = Icons[platform] ?? Icons.desktop; const Icon = Icons[platform] ?? Icons.desktop;
return <Icon color={StatusUtils.useStatusFillColor(status)} tooltip={tooltip} small={small} />; return <Icon color={useStatusFillColor(status)} tooltip={tooltip} small={small} />;
}; };
function ensureOwnStatus(user: User) { function ensureOwnStatus(user: User) {
@ -172,26 +174,26 @@ const badge: ProfileBadge = {
const indicatorLocations = { const indicatorLocations = {
list: { list: {
description: "In the member list", description: "In the member list",
onEnable: () => addDecorator("platform-indicator", props => onEnable: () => addMemberListDecorator("platform-indicator", props =>
<ErrorBoundary noop> <ErrorBoundary noop>
<PlatformIndicator user={props.user} small={true} /> <PlatformIndicator user={props.user} small={true} />
</ErrorBoundary> </ErrorBoundary>
), ),
onDisable: () => removeDecorator("platform-indicator") onDisable: () => removeMemberListDecorator("platform-indicator")
}, },
badges: { badges: {
description: "In user profiles, as badges", description: "In user profiles, as badges",
onEnable: () => addBadge(badge), onEnable: () => addProfileBadge(badge),
onDisable: () => removeBadge(badge) onDisable: () => removeProfileBadge(badge)
}, },
messages: { messages: {
description: "Inside messages", description: "Inside messages",
onEnable: () => addDecoration("platform-indicator", props => onEnable: () => addMessageDecoration("platform-indicator", props =>
<ErrorBoundary noop> <ErrorBoundary noop>
<PlatformIndicator user={props.message?.author} wantTopMargin={true} /> <PlatformIndicator user={props.message?.author} wantTopMargin={true} />
</ErrorBoundary> </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/>. * 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 { generateId, sendBotMessage } from "@api/Commands";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { StartAt } from "@utils/types"; 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 channelId = SelectedChannelStore.getChannelId();
const draft = useStateFromStores([DraftStore], () => getDraft(channelId)); const draft = useStateFromStores([DraftStore], () => getDraft(channelId));
@ -121,11 +121,9 @@ export default definePlugin({
name: "PreviewMessage", name: "PreviewMessage",
description: "Lets you preview your message before sending it.", description: "Lets you preview your message before sending it.",
authors: [Devs.Aria], authors: [Devs.Aria],
dependencies: ["ChatInputButtonAPI"],
// start early to ensure we're the first plugin to add our button // start early to ensure we're the first plugin to add our button
// This makes the popping in less awkward // This makes the popping in less awkward
startAt: StartAt.Init, startAt: StartAt.Init,
start: () => addChatBarButton("previewMessage", PreviewButton), renderChatBarButton: PreviewButton,
stop: () => removeChatBarButton("previewMessage"),
}); });

View file

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

View file

@ -196,7 +196,7 @@ function nextReply(isUp: boolean) {
channel, channel,
message, message,
shouldMention: shouldMention(message), shouldMention: shouldMention(message),
showMentionToggle: channel.isPrivate() && message.author.id !== meId, showMentionToggle: !channel.isPrivate() && message.author.id !== meId,
_isQuickReply: true _isQuickReply: true
}); });
ComponentDispatch.dispatchToLastSubscribed("TEXTAREA_FOCUS"); ComponentDispatch.dispatchToLastSubscribed("TEXTAREA_FOCUS");

View file

@ -7,15 +7,12 @@
import { DataStore } from "@api/index"; import { DataStore } from "@api/index";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { openModal } from "@utils/modal"; import { openModal } from "@utils/modal";
import { findByPropsLazy } from "@webpack"; import { OAuth2AuthorizeModal, showToast, Toasts, UserStore } from "@webpack/common";
import { showToast, Toasts, UserStore } from "@webpack/common";
import { ReviewDBAuth } from "./entities"; import { ReviewDBAuth } from "./entities";
const DATA_STORE_KEY = "rdb-auth"; const DATA_STORE_KEY = "rdb-auth";
const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal");
export let Auth: ReviewDBAuth = {}; export let Auth: ReviewDBAuth = {};
export async function initAuth() { export async function initAuth() {

View file

@ -27,7 +27,6 @@ import { cl } from "./utils";
export const settings = definePluginSettings({ export const settings = definePluginSettings({
authorize: { authorize: {
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
description: "Authorize with ReviewDB",
component: () => ( component: () => (
<Button onClick={() => authorize()}> <Button onClick={() => authorize()}>
Authorize with ReviewDB Authorize with ReviewDB
@ -56,7 +55,6 @@ export const settings = definePluginSettings({
}, },
buttons: { buttons: {
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
description: "ReviewDB buttons",
component: () => ( component: () => (
<div className={cl("button-grid")} > <div className={cl("button-grid")} >
<Button onClick={openBlockModal}>Manage Blocked Users</Button> <Button onClick={openBlockModal}>Manage Blocked Users</Button>

View file

@ -18,8 +18,7 @@
import "./styles.css"; import "./styles.css";
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { Devs } from "@utils/constants"; 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; if (!isMainChat) return null;
return ( return (
@ -160,22 +159,14 @@ export default definePlugin({
name: "SendTimestamps", name: "SendTimestamps",
description: "Send timestamps easily via chat box button & text shortcuts. Read the extended description!", description: "Send timestamps easily via chat box button & text shortcuts. Read the extended description!",
authors: [Devs.Ven, Devs.Tyler, Devs.Grzesiek11], authors: [Devs.Ven, Devs.Tyler, Devs.Grzesiek11],
dependencies: ["MessageEventsAPI", "ChatInputButtonAPI"],
settings, settings,
start() { renderChatBarButton: ChatBarIcon,
addChatBarButton("SendTimestamps", ChatBarIcon);
this.listener = addPreSendListener((_, msg) => { onBeforeMessageSend(_, msg) {
if (settings.store.replaceMessageContents) { if (settings.store.replaceMessageContents) {
msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime); msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime);
} }
});
},
stop() {
removeChatBarButton("SendTimestamps");
removePreSendListener(this.listener);
}, },
settingsAboutComponent() { settingsAboutComponent() {

View file

@ -31,7 +31,8 @@ export function openGuildInfoModal(guild: Guild) {
const enum Tabs { const enum Tabs {
ServerInfo, ServerInfo,
Friends, Friends,
BlockedUsers BlockedUsers,
IgnoredUsers
} }
interface GuildProps { interface GuildProps {
@ -44,7 +45,8 @@ interface RelationshipProps extends GuildProps {
const fetched = { const fetched = {
friends: false, friends: false,
blocked: false blocked: false,
ignored: false
}; };
function renderTimestamp(timestamp: number) { function renderTimestamp(timestamp: number) {
@ -56,10 +58,12 @@ function renderTimestamp(timestamp: number) {
function GuildInfoModal({ guild }: GuildProps) { function GuildInfoModal({ guild }: GuildProps) {
const [friendCount, setFriendCount] = useState<number>(); const [friendCount, setFriendCount] = useState<number>();
const [blockedCount, setBlockedCount] = useState<number>(); const [blockedCount, setBlockedCount] = useState<number>();
const [ignoredCount, setIgnoredCount] = useState<number>();
useEffect(() => { useEffect(() => {
fetched.friends = false; fetched.friends = false;
fetched.blocked = false; fetched.blocked = false;
fetched.ignored = false;
}, []); }, []);
const [currentTab, setCurrentTab] = useState(Tabs.ServerInfo); const [currentTab, setCurrentTab] = useState(Tabs.ServerInfo);
@ -132,12 +136,19 @@ function GuildInfoModal({ guild }: GuildProps) {
> >
Blocked Users{blockedCount !== undefined ? ` (${blockedCount})` : ""} Blocked Users{blockedCount !== undefined ? ` (${blockedCount})` : ""}
</TabBar.Item> </TabBar.Item>
<TabBar.Item
className={cl("tab", { selected: currentTab === Tabs.IgnoredUsers })}
id={Tabs.IgnoredUsers}
>
Ignored Users{ignoredCount !== undefined ? ` (${ignoredCount})` : ""}
</TabBar.Item>
</TabBar> </TabBar>
<div className={cl("tab-content")}> <div className={cl("tab-content")}>
{currentTab === Tabs.ServerInfo && <ServerInfoTab guild={guild} />} {currentTab === Tabs.ServerInfo && <ServerInfoTab guild={guild} />}
{currentTab === Tabs.Friends && <FriendsTab guild={guild} setCount={setFriendCount} />} {currentTab === Tabs.Friends && <FriendsTab guild={guild} setCount={setFriendCount} />}
{currentTab === Tabs.BlockedUsers && <BlockedUsersTab guild={guild} setCount={setBlockedCount} />} {currentTab === Tabs.BlockedUsers && <BlockedUsersTab guild={guild} setCount={setBlockedCount} />}
{currentTab === Tabs.IgnoredUsers && <IgnoredUserTab guild={guild} setCount={setIgnoredCount} />}
</div> </div>
</div> </div>
); );
@ -211,7 +222,13 @@ function BlockedUsersTab({ guild, setCount }: RelationshipProps) {
return UserList("blocked", guild, blockedIds, setCount); 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 missing = [] as string[];
const members = [] as string[]; const members = [] as string[];

View file

@ -88,7 +88,7 @@ export default definePlugin({
}, },
// Make channels we dont have access to be the same level as normal ones // 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}` replace: (_, rest, defaultRenderLevel) => `${rest}${defaultRenderLevel}`
}, },
// Remove permission checking for getRenderLevel function // Remove permission checking for getRenderLevel function
@ -108,8 +108,10 @@ export default definePlugin({
}, },
{ {
// Prevent Discord from trying to connect to hidden voice channels // Prevent Discord from trying to connect to hidden voice channels
match: /(?=&&\i\.\i\.selectVoiceChannel\((\i)\.id\))/, match: /(?=(\|\||&&)\i\.\i\.selectVoiceChannel\((\i)\.id\))/,
replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})` replace: (_, condition, channel) => condition === "||"
? `||$self.isHiddenChannel(${channel})`
: `&&!$self.isHiddenChannel(${channel})`
}, },
{ {
// Make Discord show inside the channel if clicking on a hidden or locked channel // Make Discord show inside the channel if clicking on a hidden or locked channel
@ -122,8 +124,10 @@ export default definePlugin({
{ {
find: ".AUDIENCE),{isSubscriptionGated", find: ".AUDIENCE),{isSubscriptionGated",
replacement: { replacement: {
match: /!(\i)\.isRoleSubscriptionTemplatePreviewChannel\(\)/, match: /(!)?(\i)\.isRoleSubscriptionTemplatePreviewChannel\(\)/,
replace: (m, channel) => `${m}&&!$self.isHiddenChannel(${channel})` replace: (m, not, channel) => not
? `${m}&&!$self.isHiddenChannel(${channel})`
: `${m}||$self.isHiddenChannel(${channel})`
} }
}, },
{ {
@ -173,8 +177,10 @@ export default definePlugin({
}, },
// Make voice channels also appear as muted if they are muted // Make voice channels also appear as muted if they are muted
{ {
match: /(?<=\.wrapper:\i\.notInteractive,)(.+?)if\((\i)\)return (\i\.MUTED);/, match: /(?<=\.wrapper:\i\.notInteractive,)(.+?)(if\()?(\i)(?:\)return |\?)(\i\.MUTED)/,
replace: (_, otherClasses, isMuted, mutedClassExpression) => `${isMuted}?${mutedClassExpression}:"",${otherClasses}if(${isMuted})return "";` replace: (_, otherClasses, isIf, isMuted, mutedClassExpression) => isIf
? `${isMuted}?${mutedClassExpression}:"",${otherClasses}if(${isMuted})return ""`
: `${isMuted}?${mutedClassExpression}:"",${otherClasses}${isMuted}?""`
} }
] ]
}, },
@ -184,8 +190,8 @@ export default definePlugin({
{ {
// Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden // Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden
predicate: () => settings.store.hideUnreads === false && settings.store.showMode === ShowMode.HiddenIconWithMutedStyle, predicate: () => settings.store.hideUnreads === false && settings.store.showMode === ShowMode.HiddenIconWithMutedStyle,
match: /\.LOCKED;if\((?<={channel:(\i).+?)/, match: /(?<=\.LOCKED(?:;if\(|:))(?<={channel:(\i).+?)/,
replace: (m, channel) => `${m}!$self.isHiddenChannel(${channel})&&` replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&`
}, },
{ {
// Hide unreads // Hide unreads
@ -224,12 +230,12 @@ export default definePlugin({
find: "Missing channel in Channel.renderHeaderToolbar", find: "Missing channel in Channel.renderHeaderToolbar",
replacement: [ replacement: [
{ {
match: /(?<="renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_TEXT:)(?=.+?(\i\.push.{0,50}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/, 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;}` 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).+?)/, 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;}` replace: (m, pushNotificationButtonExpression, channel, isLurking) => `${m}if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
}, },
{ {
match: /"renderMobileToolbar",\(\)=>{.+?case \i\.\i\.GUILD_DIRECTORY:(?<=let{channel:(\i).+?)/, match: /"renderMobileToolbar",\(\)=>{.+?case \i\.\i\.GUILD_DIRECTORY:(?<=let{channel:(\i).+?)/,

View file

@ -76,8 +76,8 @@ export default definePlugin({
find: "#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}", find: "#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}",
replacement: [ replacement: [
{ {
match: /(\i)\.Tooltip,{(text:.{0,30}#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}\))/, match: /\i\.\i,{(text:.{0,30}#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}\))/,
replace: "$self.TooltipWrapper,{message:arguments[0].message,$2" replace: "$self.TooltipWrapper,{message:arguments[0].message,$1"
} }
] ]
} }

View file

@ -16,8 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; import { addMessagePreSendListener, MessageSendListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; 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); const [enabled, setEnabled] = useState(lastState);
function setEnabledValue(value: boolean) { function setEnabledValue(value: boolean) {
@ -50,15 +50,15 @@ const SilentMessageToggle: ChatBarButton = ({ isMainChat }) => {
} }
useEffect(() => { useEffect(() => {
const listener: SendListener = (_, message) => { const listener: MessageSendListener = (_, message) => {
if (enabled) { if (enabled) {
if (settings.store.autoDisable) setEnabledValue(false); if (settings.store.autoDisable) setEnabledValue(false);
if (!message.content.startsWith("@silent ")) message.content = "@silent " + message.content; if (!message.content.startsWith("@silent ")) message.content = "@silent " + message.content;
} }
}; };
addPreSendListener(listener); addMessagePreSendListener(listener);
return () => void removePreSendListener(listener); return () => void removeMessagePreSendListener(listener);
}, [enabled]); }, [enabled]);
if (!isMainChat) return null; if (!isMainChat) return null;
@ -91,9 +91,7 @@ export default definePlugin({
name: "SilentMessageToggle", name: "SilentMessageToggle",
authors: [Devs.Nuckyz, Devs.CatNoir], authors: [Devs.Nuckyz, Devs.CatNoir],
description: "Adds a button to the chat bar to toggle sending a silent message.", description: "Adds a button to the chat bar to toggle sending a silent message.",
dependencies: ["MessageEventsAPI", "ChatInputButtonAPI"],
settings, settings,
start: () => addChatBarButton("SilentMessageToggle", SilentMessageToggle), renderChatBarButton: SilentMessageToggle,
stop: () => removeChatBarButton("SilentMessageToggle")
}); });

View file

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

View file

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

View file

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

View file

@ -23,12 +23,12 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord"; import { getIntlMessage } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findComponentByCodeLazy, findExportedComponentLazy, findStoreLazy } from "@webpack"; import { findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { GuildMemberStore, RelationshipStore, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; import { GuildMemberStore, RelationshipStore, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
import { buildSeveralUsers } from "../typingTweaks"; import { buildSeveralUsers } from "../typingTweaks";
const ThreeDots = findExportedComponentLazy("Dots", "AnimatedDots"); const ThreeDots = findComponentByCodeLazy(".dots,", "dotRadius:");
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
const TypingStore = findStoreLazy("TypingStore"); const TypingStore = findStoreLazy("TypingStore");
@ -100,6 +100,13 @@ function TypingIndicator({ channelId, guildId }: { channelId: string; guildId: s
{props => ( {props => (
<div className="vc-typing-indicator" {...props}> <div className="vc-typing-indicator" {...props}>
{((settings.store.indicatorMode & IndicatorMode.Avatars) === IndicatorMode.Avatars) && ( {((settings.store.indicatorMode & IndicatorMode.Avatars) === IndicatorMode.Avatars) && (
<div
onClick={e => {
e.stopPropagation();
e.preventDefault();
}}
onKeyPress={e => e.stopPropagation()}
>
<UserSummaryItem <UserSummaryItem
users={typingUsersArray.map(id => UserStore.getUser(id))} users={typingUsersArray.map(id => UserStore.getUser(id))}
guildId={guildId} guildId={guildId}
@ -110,6 +117,7 @@ function TypingIndicator({ channelId, guildId }: { channelId: string; guildId: s
size={16} size={16}
className="vc-typing-indicator-avatars" className="vc-typing-indicator-avatars"
/> />
</div>
)} )}
{((settings.store.indicatorMode & IndicatorMode.Dots) === IndicatorMode.Dots) && ( {((settings.store.indicatorMode & IndicatorMode.Dots) === IndicatorMode.Dots) && (
<div className="vc-typing-indicator-dots"> <div className="vc-typing-indicator-dots">

View file

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

View file

@ -21,12 +21,18 @@ import { ImageInvisible, ImageVisible } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { Constants, Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common"; import { Constants, Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common";
import { MessageSnapshot } from "@webpack/types";
const EMBED_SUPPRESSED = 1 << 2; 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; 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); const hasEmbedPerms = channel.isPrivate() || !!(PermissionStore.getChannelPermissions({ id: channel.id }) & PermissionsBits.EMBED_LINKS);
if (author.id === UserStore.getCurrentUser().id && !hasEmbedPerms) return; if (author.id === UserStore.getCurrentUser().id && !hasEmbedPerms) return;

View file

@ -22,7 +22,7 @@ const VoiceStateStore = findStoreLazy("VoiceStateStore");
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
const Avatar = findComponentByCodeLazy(".status)/2):0"); const Avatar = findComponentByCodeLazy(".status)/2):0");
const GroupDMAvatars = findComponentByCodeLazy(".AvatarSizeSpecs[", "getAvatarURL"); const GroupDMAvatars = findComponentByCodeLazy("frontSrc:", "getAvatarURL");
const ActionButtonClasses = findByPropsLazy("actionButton", "highlight"); const ActionButtonClasses = findByPropsLazy("actionButton", "highlight");

View file

@ -18,8 +18,8 @@
import "./style.css"; import "./style.css";
import { addDecorator, removeDecorator } from "@api/MemberListDecorators"; import { addMemberListDecorator, removeMemberListDecorator } from "@api/MemberListDecorators";
import { addDecoration, removeDecoration } from "@api/MessageDecorations"; import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
@ -96,16 +96,16 @@ export default definePlugin({
start() { start() {
if (settings.store.showInMemberList) { 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) { 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() { stop() {
removeDecorator("UserVoiceShow"); removeMemberListDecorator("UserVoiceShow");
removeDecoration("UserVoiceShow"); removeMessageDecoration("UserVoiceShow");
}, },
VoiceChannelIndicator VoiceChannelIndicator

View file

@ -23,11 +23,11 @@ import { Settings, useSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findExportedComponentLazy } from "@webpack"; import { findComponentByCodeLazy } from "@webpack";
import { Menu, Popout, useState } from "@webpack/common"; import { Menu, Popout, useState } from "@webpack/common";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
const HeaderBarIcon = findExportedComponentLazy("Icon", "Divider"); const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
function VencordPopout(onClose: () => void) { function VencordPopout(onClose: () => void) {
const { useQuickCss } = useSettings(["useQuickCss"]); const { useQuickCss } = useSettings(["useQuickCss"]);

View file

@ -220,16 +220,16 @@ export default definePlugin({
{ {
find: ".cursorPointer:null,children", find: ".cursorPointer:null,children",
replacement: { replacement: {
match: /.Avatar,.+?src:(.+?\))(?=[,}])/, match: /(?=,src:(\i.getAvatarURL\(.+?[)]))/,
replace: (m, avatarUrl) => `${m},onClick:()=>$self.openAvatar(${avatarUrl})` replace: (_, avatarUrl) => `,onClick:()=>$self.openAvatar(${avatarUrl})`
} }
}, },
// User Dms top large icon // User Dms top large icon
{ {
find: 'experimentLocation:"empty_messages"', find: 'experimentLocation:"empty_messages"',
replacement: { replacement: {
match: /.Avatar,.+?src:(.+?\))(?=[,}])/, match: /(?<=SIZE_80,)(?=src:(.+?\))[,}])/,
replace: (m, avatarUrl) => `${m},onClick:()=>$self.openAvatar(${avatarUrl})` replace: (_, avatarUrl) => `onClick:()=>$self.openAvatar(${avatarUrl}),`
} }
} }
] ]

View file

@ -17,18 +17,17 @@
*/ */
import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { addButton, removeButton } from "@api/MessagePopover";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { CodeBlock } from "@components/CodeBlock"; import { CodeBlock } from "@components/CodeBlock";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord"; import { getCurrentGuild, getIntlMessage } from "@utils/discord";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { copyWithToast } from "@utils/misc"; import { copyWithToast } from "@utils/misc";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Button, ChannelStore, Forms, Menu, Text } from "@webpack/common"; import { Button, ChannelStore, Forms, GuildStore, Menu, Text } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
@ -119,7 +118,7 @@ const settings = definePluginSettings({
} }
}); });
function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenuPatchCallback { function MakeContextCallback(name: "Guild" | "Role" | "User" | "Channel"): NavContextMenuPatchCallback {
return (children, props) => { return (children, props) => {
const value = props[name.toLowerCase()]; const value = props[name.toLowerCase()];
if (!value) return; if (!value) return;
@ -145,22 +144,40 @@ function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenu
}; };
} }
const devContextCallback: NavContextMenuPatchCallback = (children, { id }: { id: string; }) => {
const guild = getCurrentGuild();
if (!guild) return;
const role = GuildStore.getRole(guild.id, id);
if (!role) return;
children.push(
<Menu.MenuItem
id={"vc-view-role-raw"}
label="View Raw"
action={() => openViewRawModal(JSON.stringify(role, null, 4), "Role")}
icon={CopyIcon}
/>
);
};
export default definePlugin({ export default definePlugin({
name: "ViewRaw", name: "ViewRaw",
description: "Copy and view the raw content/data of any message, channel or guild", description: "Copy and view the raw content/data of any message, channel or guild",
authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna], authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna],
dependencies: ["MessagePopoverAPI"],
settings, settings,
contextMenus: { contextMenus: {
"guild-context": MakeContextCallback("Guild"), "guild-context": MakeContextCallback("Guild"),
"guild-settings-role-context": MakeContextCallback("Role"),
"channel-context": MakeContextCallback("Channel"), "channel-context": MakeContextCallback("Channel"),
"thread-context": MakeContextCallback("Channel"), "thread-context": MakeContextCallback("Channel"),
"gdm-context": MakeContextCallback("Channel"), "gdm-context": MakeContextCallback("Channel"),
"user-context": MakeContextCallback("User") "user-context": MakeContextCallback("User"),
"dev-context": devContextCallback
}, },
start() { renderMessagePopoverButton(msg) {
addButton("ViewRaw", msg => {
const handleClick = () => { const handleClick = () => {
if (settings.store.clickMethod === "Right") { if (settings.store.clickMethod === "Right") {
copyWithToast(msg.content); copyWithToast(msg.content);
@ -193,10 +210,5 @@ export default definePlugin({
onClick: handleClick, onClick: handleClick,
onContextMenu: handleContextMenu onContextMenu: handleContextMenu
}; };
});
},
stop() {
removeButton("ViewRaw");
} }
}); });

View file

@ -20,10 +20,13 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { saveFile } from "@utils/web"; import { saveFile } from "@utils/web";
import { findByPropsLazy } from "@webpack"; import { filters, mapMangledModuleLazy } from "@webpack";
import { Clipboard, ComponentDispatch } from "@webpack/common"; import { Clipboard, ComponentDispatch } from "@webpack/common";
const ctxMenuCallbacks = findByPropsLazy("contextMenuCallbackNative"); const ctxMenuCallbacks = mapMangledModuleLazy('.tagName)==="TEXTAREA"||', {
contextMenuCallbackWeb: filters.byCode('.tagName)==="INPUT"||'),
contextMenuCallbackNative: filters.byCode('.tagName)==="TEXTAREA"||')
});
async function fetchImage(url: string) { async function fetchImage(url: string) {
const res = await fetch(url); const res = await fetch(url);

View file

@ -93,7 +93,7 @@ function makeRenderMoreUsers(users: User[]) {
}; };
} }
function handleClickAvatar(event: React.MouseEvent<HTMLElement, MouseEvent>) { function handleClickAvatar(event: React.UIEvent<HTMLElement, Event>) {
event.stopPropagation(); event.stopPropagation();
} }
@ -165,7 +165,7 @@ export default definePlugin({
<div <div
style={{ marginLeft: "0.5em", transform: "scale(0.9)" }} style={{ marginLeft: "0.5em", transform: "scale(0.9)" }}
> >
<div onClick={handleClickAvatar}> <div onClick={handleClickAvatar} onKeyPress={handleClickAvatar}>
<UserSummaryItem <UserSummaryItem
users={users} users={users}
guildId={ChannelStore.getChannel(message.channel_id)?.guild_id} guildId={ChannelStore.getChannel(message.channel_id)?.guild_id}

View file

@ -6,6 +6,9 @@
import { LiteralUnion } from "type-fest"; 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 // 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}` type ResolvePropDeep<T, P> = P extends `${infer Pre}.${infer Suf}`
? Pre extends keyof T ? Pre extends keyof T
@ -28,6 +31,11 @@ interface SettingsStoreOptions {
// merges the SettingsStoreOptions type into the class // merges the SettingsStoreOptions type into the class
export interface SettingsStore<T extends object> extends SettingsStoreOptions { } 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 * The SettingsStore allows you to easily create a mutable store that
* has support for global and path-based change listeners. * 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> { export class SettingsStore<T extends object> {
private pathListeners = new Map<string, Set<(newData: any) => void>>(); private pathListeners = new Map<string, Set<(newData: any) => void>>();
private globalListeners = new Set<(newData: T, path: string) => 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 * 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); Object.assign(this, options);
} }
private makeProxy(object: any, root: T = object, path: string = "") { private makeProxy(object: any, root: T = object, path = "") {
const self = this; this.proxyContexts.set(object, {
return new Proxy(object, {
get(target, key: string) {
let v = target[key];
if (!(key in target) && self.getDefaultValue) {
v = self.getDefaultValue({
target,
key,
root, root,
path path
}); });
return new Proxy(object, this.proxyHandler);
} }
if (typeof v === "object" && v !== null && !Array.isArray(v)) private notifyListeners(pathStr: string, value: any, root: T) {
return self.makeProxy(v, root, `${path}${path && "."}${key}`); const paths = pathStr.split(".");
return v; // 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).
set(target, key: string, value) { // So, we need to extract the top-level setting path (plugins.pluginName.settingName),
if (target[key] === value) return true; // 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); this.globalListeners.forEach(cb => cb(root, settingPathStr));
const setPath = `${path}${path && "."}${key}`; this.pathListeners.get(settingPathStr)?.forEach(cb => cb(settingValue));
} else {
self.globalListeners.forEach(cb => cb(value, setPath)); this.globalListeners.forEach(cb => cb(root, pathStr));
self.pathListeners.get(setPath)?.forEach(cb => cb(value));
return true;
} }
});
this.pathListeners.get(pathStr)?.forEach(cb => cb(value));
} }
/** /**

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { findByPropsLazy, findModuleId, proxyLazyWebpack, wreq } from "@webpack"; import { filters, findModuleId, mapMangledModuleLazy, proxyLazyWebpack, wreq } from "@webpack";
import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react"; import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react";
import { LazyComponent } from "./react"; import { LazyComponent } from "./react";
@ -49,7 +49,7 @@ export interface ModalOptions {
type RenderFunction = (props: ModalProps) => ReactNode | Promise<ReactNode>; type RenderFunction = (props: ModalProps) => ReactNode | Promise<ReactNode>;
export const Modals = findByPropsLazy("ModalRoot", "ModalCloseButton") as { interface Modals {
ModalRoot: ComponentType<PropsWithChildren<{ ModalRoot: ComponentType<PropsWithChildren<{
transitionState: ModalTransitionState; transitionState: ModalTransitionState;
size?: ModalSize; size?: ModalSize;
@ -99,7 +99,21 @@ export const Modals = findByPropsLazy("ModalRoot", "ModalCloseButton") as {
hideOnFullscreen?: boolean; hideOnFullscreen?: boolean;
className?: string; className?: string;
}>; }>;
}; }
export const Modals: Modals = mapMangledModuleLazy(':"thin")', {
ModalRoot: filters.componentByCode('.MODAL,"aria-labelledby":'),
ModalHeader: filters.componentByCode(",id:"),
ModalContent: filters.componentByCode(".content,"),
ModalFooter: filters.componentByCode(".footer,"),
ModalCloseButton: filters.componentByCode(".close]:")
});
export const ModalRoot = LazyComponent(() => Modals.ModalRoot);
export const ModalHeader = LazyComponent(() => Modals.ModalHeader);
export const ModalContent = LazyComponent(() => Modals.ModalContent);
export const ModalFooter = LazyComponent(() => Modals.ModalFooter);
export const ModalCloseButton = LazyComponent(() => Modals.ModalCloseButton);
export type MediaModalItem = { export type MediaModalItem = {
url: string; url: string;
@ -135,38 +149,33 @@ export const openMediaModal: (props: MediaModalProps) => void = proxyLazyWebpack
return Object.values<any>(openMediaModalModule).find(v => String(v).includes("modalKey:")); return Object.values<any>(openMediaModalModule).find(v => String(v).includes("modalKey:"));
}); });
export const ModalRoot = LazyComponent(() => Modals.ModalRoot); interface ModalAPI {
export const ModalHeader = LazyComponent(() => Modals.ModalHeader); /**
export const ModalContent = LazyComponent(() => Modals.ModalContent);
export const ModalFooter = LazyComponent(() => Modals.ModalFooter);
export const ModalCloseButton = LazyComponent(() => Modals.ModalCloseButton);
export const ModalAPI = findByPropsLazy("openModalLazy");
/**
* Wait for the render promise to resolve, then open a modal with it. * Wait for the render promise to resolve, then open a modal with it.
* This is equivalent to render().then(openModal) * This is equivalent to render().then(openModal)
* You should use the Modal components exported by this file * You should use the Modal components exported by this file
*/ */
export const openModalLazy: (render: () => Promise<RenderFunction>, options?: ModalOptions & { contextKey?: string; }) => Promise<string> openModalLazy: (render: () => Promise<RenderFunction>, options?: ModalOptions & { contextKey?: string; }) => Promise<string>;
= proxyLazyWebpack(() => ModalAPI.openModalLazy); /**
/**
* Open a Modal with the given render function. * Open a Modal with the given render function.
* You should use the Modal components exported by this file * You should use the Modal components exported by this file
*/ */
export const openModal: (render: RenderFunction, options?: ModalOptions, contextKey?: string) => string openModal: (render: RenderFunction, options?: ModalOptions, contextKey?: string) => string;
= proxyLazyWebpack(() => ModalAPI.openModal); /**
/**
* Close a modal by its key * Close a modal by its key
*/ */
export const closeModal: (modalKey: string, contextKey?: string) => void closeModal: (modalKey: string, contextKey?: string) => void;
= proxyLazyWebpack(() => ModalAPI.closeModal); /**
/**
* Close all open modals * Close all open modals
*/ */
export const closeAllModals: () => void closeAllModals: () => void;
= proxyLazyWebpack(() => ModalAPI.closeAllModals); }
export const ModalAPI: ModalAPI = mapMangledModuleLazy(".modalKey?", {
openModalLazy: filters.byCode(".modalKey?"),
openModal: filters.byCode(",instant:"),
closeModal: filters.byCode(".onCloseCallback()"),
closeAllModals: filters.byCode(".getState();for")
});
export const { openModalLazy, openModal, closeModal, closeAllModals } = ModalAPI;

View file

@ -16,8 +16,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ProfileBadge } from "@api/Badges";
import { ChatBarButtonFactory } from "@api/ChatButtons";
import { Command } from "@api/Commands"; import { Command } from "@api/Commands";
import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { MemberListDecoratorFactory } from "@api/MemberListDecorators";
import { MessageAccessoryFactory } from "@api/MessageAccessories";
import { MessageDecorationFactory } from "@api/MessageDecorations";
import { MessageClickListener, MessageEditListener, MessageSendListener } from "@api/MessageEvents";
import { MessagePopoverButtonFactory } from "@api/MessagePopover";
import { FluxEvents } from "@webpack/types"; import { FluxEvents } from "@webpack/types";
import { JSX } from "react"; import { JSX } from "react";
import { Promisable } from "type-fest"; import { Promisable } from "type-fest";
@ -142,6 +149,20 @@ export interface PluginDef {
toolboxActions?: Record<string, () => void>; toolboxActions?: Record<string, () => void>;
tags?: string[]; tags?: string[];
userProfileBadge?: ProfileBadge;
onMessageClick?: MessageClickListener;
onBeforeMessageSend?: MessageSendListener;
onBeforeMessageEdit?: MessageEditListener;
renderMessagePopoverButton?: MessagePopoverButtonFactory;
renderMessageAccessory?: MessageAccessoryFactory;
renderMessageDecoration?: MessageDecorationFactory;
renderMemberListDecorator?: MemberListDecoratorFactory;
renderChatBarButton?: ChatBarButtonFactory;
} }
export const enum StartAt { export const enum StartAt {
@ -168,6 +189,7 @@ export const enum OptionType {
SELECT, SELECT,
SLIDER, SLIDER,
COMPONENT, COMPONENT,
CUSTOM
} }
export type SettingsDefinition = Record<string, PluginSettingDef>; export type SettingsDefinition = Record<string, PluginSettingDef>;
@ -176,15 +198,16 @@ export type SettingsChecks<D extends SettingsDefinition> = {
(IsDisabled<DefinedSettings<D>> & IsValid<PluginSettingType<D[K]>, DefinedSettings<D>>); (IsDisabled<DefinedSettings<D>> & IsValid<PluginSettingType<D[K]>, DefinedSettings<D>>);
}; };
export type PluginSettingDef = ( export type PluginSettingDef =
(PluginSettingCustomDef & Pick<PluginSettingCommon, "onChange">) |
(PluginSettingComponentDef & Omit<PluginSettingCommon, "description" | "placeholder">) | ((
| PluginSettingStringDef | PluginSettingStringDef
| PluginSettingNumberDef | PluginSettingNumberDef
| PluginSettingBooleanDef | PluginSettingBooleanDef
| PluginSettingSelectDef | PluginSettingSelectDef
| PluginSettingSliderDef | PluginSettingSliderDef
| PluginSettingComponentDef
| PluginSettingBigIntDef | PluginSettingBigIntDef
) & PluginSettingCommon; ) & PluginSettingCommon);
export interface PluginSettingCommon { export interface PluginSettingCommon {
description: string; description: string;
@ -204,12 +227,14 @@ export interface PluginSettingCommon {
*/ */
target?: "WEB" | "DESKTOP" | "BOTH"; target?: "WEB" | "DESKTOP" | "BOTH";
} }
interface IsDisabled<D = unknown> { interface IsDisabled<D = unknown> {
/** /**
* Checks if this setting should be disabled * Checks if this setting should be disabled
*/ */
disabled?(this: D): boolean; disabled?(this: D): boolean;
} }
interface IsValid<T, D = unknown> { interface IsValid<T, D = unknown> {
/** /**
* Prevents the user from saving settings if this is false or a string * Prevents the user from saving settings if this is false or a string
@ -238,12 +263,18 @@ export interface PluginSettingSelectDef {
type: OptionType.SELECT; type: OptionType.SELECT;
options: readonly PluginSettingSelectOption[]; options: readonly PluginSettingSelectOption[];
} }
export interface PluginSettingSelectOption { export interface PluginSettingSelectOption {
label: string; label: string;
value: string | number | boolean; value: string | number | boolean;
default?: boolean; default?: boolean;
} }
export interface PluginSettingCustomDef {
type: OptionType.CUSTOM;
default?: any;
}
export interface PluginSettingSliderDef { export interface PluginSettingSliderDef {
type: OptionType.SLIDER; type: OptionType.SLIDER;
/** /**
@ -292,8 +323,10 @@ type PluginSettingType<O extends PluginSettingDef> = O extends PluginSettingStri
O extends PluginSettingBooleanDef ? boolean : O extends PluginSettingBooleanDef ? boolean :
O extends PluginSettingSelectDef ? O["options"][number]["value"] : O extends PluginSettingSelectDef ? O["options"][number]["value"] :
O extends PluginSettingSliderDef ? number : O extends PluginSettingSliderDef ? number :
O extends PluginSettingComponentDef ? any : O extends PluginSettingComponentDef ? O extends { default: infer Default; } ? Default : any :
O extends PluginSettingCustomDef ? O extends { default: infer Default; } ? Default : any :
never; never;
type PluginSettingDefaultType<O extends PluginSettingDef> = O extends PluginSettingSelectDef ? ( type PluginSettingDefaultType<O extends PluginSettingDef> = O extends PluginSettingSelectDef ? (
O["options"] extends { default?: boolean; }[] ? O["options"][number]["value"] : undefined O["options"] extends { default?: boolean; }[] ? O["options"][number]["value"] : undefined
) : O extends { default: infer T; } ? T : undefined; ) : O extends { default: infer T; } ? T : undefined;
@ -345,13 +378,15 @@ export type PluginOptionsItem =
| PluginOptionBoolean | PluginOptionBoolean
| PluginOptionSelect | PluginOptionSelect
| PluginOptionSlider | PluginOptionSlider
| PluginOptionComponent; | PluginOptionComponent
| PluginOptionCustom;
export type PluginOptionString = PluginSettingStringDef & PluginSettingCommon & IsDisabled & IsValid<string>; export type PluginOptionString = PluginSettingStringDef & PluginSettingCommon & IsDisabled & IsValid<string>;
export type PluginOptionNumber = (PluginSettingNumberDef | PluginSettingBigIntDef) & PluginSettingCommon & IsDisabled & IsValid<number | BigInt>; export type PluginOptionNumber = (PluginSettingNumberDef | PluginSettingBigIntDef) & PluginSettingCommon & IsDisabled & IsValid<number | BigInt>;
export type PluginOptionBoolean = PluginSettingBooleanDef & PluginSettingCommon & IsDisabled & IsValid<boolean>; export type PluginOptionBoolean = PluginSettingBooleanDef & PluginSettingCommon & IsDisabled & IsValid<boolean>;
export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid<PluginSettingSelectOption>; export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid<PluginSettingSelectOption>;
export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid<number>; export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid<number>;
export type PluginOptionComponent = PluginSettingComponentDef & PluginSettingCommon; export type PluginOptionComponent = PluginSettingComponentDef & Omit<PluginSettingCommon, "description" | "placeholder">;
export type PluginOptionCustom = PluginSettingCustomDef & Pick<PluginSettingCommon, "onChange">;
export type PluginNative<PluginExports extends Record<string, (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any>> = { export type PluginNative<PluginExports extends Record<string, (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any>> = {
[key in keyof PluginExports]: [key in keyof PluginExports]:

View file

@ -16,76 +16,87 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { filters, findByPropsLazy, waitFor } from "@webpack"; import { LazyComponent } from "@utils/lazyReact";
import { filters, mapMangledModuleLazy, waitFor } from "@webpack";
import { waitForComponent } from "./internal"; import { waitForComponent } from "./internal";
import * as t from "./types/components"; import * as t from "./types/components";
export let Forms = {} as {
FormTitle: t.FormTitle, const FormTitle = waitForComponent<t.FormTitle>("FormTitle", filters.componentByCode('["defaultMargin".concat', '="h5"'));
FormSection: t.FormSection, const FormText = waitForComponent<t.FormText>("FormText", filters.componentByCode(".SELECTABLE),", ".DISABLED:"));
FormDivider: t.FormDivider, const FormSection = waitForComponent<t.FormSection>("FormSection", filters.componentByCode(".titleId)&&"));
FormText: t.FormText, const FormDivider = waitForComponent<t.FormDivider>("FormDivider", filters.componentByCode(".divider,", ",style:", '"div"', /\.divider,\i\),style:/));
export const Forms = {
FormTitle,
FormText,
FormSection,
FormDivider
}; };
export let Icons = {} as t.Icons; export const Card = waitForComponent<t.Card>("Card", filters.componentByCode(".editable),", ".outline:"));
export const Button = waitForComponent<t.Button>("Button", filters.componentByCode("#{intl::A11Y_LOADING_STARTED}))),!1"));
export const Switch = waitForComponent<t.Switch>("Switch", filters.componentByCode(".labelRow,ref:", ".disabledText"));
const Tooltips = mapMangledModuleLazy(".tooltipTop,bottom:", {
Tooltip: filters.componentByCode("this.renderTooltip()]"),
TooltipContainer: filters.componentByCode('="div",')
}) as {
Tooltip: t.Tooltip,
TooltipContainer: t.TooltipContainer;
};
export const Tooltip = LazyComponent(() => Tooltips.Tooltip);
export const TooltipContainer = LazyComponent(() => Tooltips.TooltipContainer);
export const TextInput = waitForComponent<t.TextInput>("TextInput", filters.componentByCode(".error]:this.hasError()"));
export const TextArea = waitForComponent<t.TextArea>("TextArea", filters.componentByCode("this.getPaddingRight()},id:"));
export const Text = waitForComponent<t.Text>("Text", filters.componentByCode('case"always-white"'));
export const Heading = waitForComponent<t.Heading>("Heading", filters.componentByCode(">6?{", "variant:"));
export const Select = waitForComponent<t.Select>("Select", filters.componentByCode('.selectPositionTop]:"top"===', '"Escape"==='));
export const SearchableSelect = waitForComponent<t.SearchableSelect>("SearchableSelect", filters.componentByCode('.selectPositionTop]:"top"===', ".multi]:"));
export const Slider = waitForComponent<t.Slider>("Slider", filters.componentByCode('"markDash".concat('));
export const Popout = waitForComponent<t.Popout>("Popout", filters.componentByCode("ref:this.ref,preload:"));
export const Dialog = waitForComponent<t.Dialog>("Dialog", filters.componentByCode('role:"dialog",tabIndex:-1'));
export const TabBar = waitForComponent("TabBar", filters.componentByCode("ref:this.tabBarRef,className:"));
export const Paginator = waitForComponent<t.Paginator>("Paginator", filters.componentByCode('rel:"prev",children:'));
export const Clickable = waitForComponent<t.Clickable>("Clickable", filters.componentByCode("this.context?this.renderNonInteractive():"));
export const Avatar = waitForComponent<t.Avatar>("Avatar", filters.componentByCode(".size-1.375*"));
export let createScroller: (scrollbarClassName: string, fadeClassName: string, customThemeClassName: string) => t.ScrollerThin;
export let scrollerClasses: Record<string, string>;
waitFor(filters.byCode('="ltr",orientation:', "customTheme:", "forwardRef"), m => createScroller = m);
waitFor(["thin", "auto", "customTheme"], m => scrollerClasses = m);
export const ScrollerNone = LazyComponent(() => createScroller(scrollerClasses.none, scrollerClasses.fade, scrollerClasses.customTheme));
export const ScrollerThin = LazyComponent(() => createScroller(scrollerClasses.thin, scrollerClasses.fade, scrollerClasses.customTheme));
export const ScrollerAuto = LazyComponent(() => createScroller(scrollerClasses.auto, scrollerClasses.fade, scrollerClasses.customTheme));
const { FocusLock_ } = mapMangledModuleLazy("attachTo:null!==", {
FocusLock_: filters.componentByCode(".containerRef")
}) as {
FocusLock_: t.FocusLock;
};
export const FocusLock = LazyComponent(() => FocusLock_);
export let Card: t.Card;
export let Button: t.Button;
export let Switch: t.Switch;
export let Tooltip: t.Tooltip;
export let TooltipContainer: t.TooltipContainer;
export let TextInput: t.TextInput;
export let TextArea: t.TextArea;
export let Text: t.Text;
export let Heading: t.Heading;
export let Select: t.Select;
export let SearchableSelect: t.SearchableSelect;
export let Slider: t.Slider;
export let ButtonLooks: t.ButtonLooks;
export let Popout: t.Popout;
export let Dialog: t.Dialog;
export let TabBar: any;
export let Paginator: t.Paginator;
export let ScrollerThin: t.ScrollerThin;
export let Clickable: t.Clickable;
export let Avatar: t.Avatar;
export let FocusLock: t.FocusLock;
// token lagger real
/** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */
export let useToken: t.useToken; export let useToken: t.useToken;
waitFor(m => {
if (typeof m !== "function") {
return false;
}
const str = String(m);
return str.includes(".resolve({theme:null") && !str.includes("useMemo");
}, m => useToken = m);
export const MaskedLink = waitForComponent<t.MaskedLink>("MaskedLink", filters.componentByCode("MASKED_LINK)")); export const MaskedLink = waitForComponent<t.MaskedLink>("MaskedLink", filters.componentByCode("MASKED_LINK)"));
export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.byCode("#{intl::MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL}")); export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.componentByCode("#{intl::MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL}"));
export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]); export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]);
export const OAuth2AuthorizeModal = waitForComponent("OAuth2AuthorizeModal", filters.componentByCode(".authorize),children:", ".contentBackground"));
export const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal"); export const Animations = mapMangledModuleLazy(".assign({colorNames:", {
Transition: filters.componentByCode('["items","children"]', ",null,"),
waitFor(["FormItem", "Button"], m => { animated: filters.byProps("div", "text")
({
useToken,
Card,
Button,
FormSwitch: Switch,
Tooltip,
TooltipContainer,
TextInput,
TextArea,
Text,
Select,
SearchableSelect,
Slider,
ButtonLooks,
TabBar,
Popout,
Dialog,
Paginator,
ScrollerThin,
Clickable,
Avatar,
FocusLock,
Heading
} = m);
Forms = m;
Icons = m;
}); });

View file

@ -17,16 +17,29 @@
*/ */
// eslint-disable-next-line path-alias/no-relative // eslint-disable-next-line path-alias/no-relative
import { filters, mapMangledModuleLazy, waitFor } from "../webpack"; import { filters, mapMangledModuleLazy, waitFor, wreq } from "../webpack";
import type * as t from "./types/menu"; import type * as t from "./types/menu";
export let Menu = {} as t.Menu; export const Menu = {} as t.Menu;
waitFor(["MenuItem", "MenuSliderControl"], m => Menu = m); // Relies on .name properties added by the MenuItemDemanglerAPI
waitFor(m => m.name === "MenuCheckboxItem", (_, id) => {
// we have to do this manual require by ID because m is in this case the MenuCheckBoxItem instead of the entire module
const module = wreq(id as any);
for (const e of Object.values(module)) {
if (typeof e === "function" && e.name.startsWith("Menu")) {
Menu[e.name] = e;
}
}
});
waitFor(filters.componentByCode('path:["empty"]'), m => Menu.Menu = m);
waitFor(filters.componentByCode("sliderContainer", "slider", "handleSize:16", "=100"), m => Menu.MenuSliderControl = m);
waitFor(filters.componentByCode('role:"searchbox', "top:2", "query:"), m => Menu.MenuSearchControl = m);
export const ContextMenuApi: t.ContextMenuApi = mapMangledModuleLazy('type:"CONTEXT_MENU_OPEN', { export const ContextMenuApi: t.ContextMenuApi = mapMangledModuleLazy('type:"CONTEXT_MENU_OPEN', {
closeContextMenu: filters.byCode("CONTEXT_MENU_CLOSE"), closeContextMenu: filters.byCode("CONTEXT_MENU_CLOSE"),
openContextMenu: filters.byCode("renderLazy:"), openContextMenu: filters.byCode("renderLazy:"),
openContextMenuLazy: e => typeof e === "function" && e.toString().length < 100 openContextMenuLazy: e => typeof e === "function" && e.toString().length < 100
}); });

View file

@ -50,6 +50,7 @@ export let GuildMemberStore: Stores.GuildMemberStore & t.FluxStore;
export let RelationshipStore: Stores.RelationshipStore & t.FluxStore & { export let RelationshipStore: Stores.RelationshipStore & t.FluxStore & {
/** Get the date (as a string) that the relationship was created */ /** Get the date (as a string) that the relationship was created */
getSince(userId: string): string; getSince(userId: string): string;
isIgnored(userId: string): boolean;
}; };
export let EmojiStore: t.EmojiStore; export let EmojiStore: t.EmojiStore;

View file

@ -16,16 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import type { ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, KeyboardEvent, MouseEvent, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react"; import type { ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react";
import { IconNames } from "./iconNames";
export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code"; export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code";
export type FormTextTypes = Record<"DEFAULT" | "INPUT_PLACEHOLDER" | "DESCRIPTION" | "LABEL_BOLD" | "LABEL_SELECTED" | "LABEL_DESCRIPTOR" | "ERROR" | "SUCCESS", string>; export type FormTextTypes = Record<"DEFAULT" | "INPUT_PLACEHOLDER" | "DESCRIPTION" | "LABEL_BOLD" | "LABEL_SELECTED" | "LABEL_DESCRIPTOR" | "ERROR" | "SUCCESS", string>;
export type HeadingTag = `h${1 | 2 | 3 | 4 | 5 | 6}`; export type HeadingTag = `h${1 | 2 | 3 | 4 | 5 | 6}`;
export type Margins = Record<"marginTop16" | "marginTop8" | "marginBottom8" | "marginTop20" | "marginBottom20", string>; export type Margins = Record<"marginTop16" | "marginTop8" | "marginBottom8" | "marginTop20" | "marginBottom20", string>;
export type ButtonLooks = Record<"FILLED" | "INVERTED" | "OUTLINED" | "LINK" | "BLANK", string>;
export type TextProps = PropsWithChildren<HtmlHTMLAttributes<HTMLDivElement> & { export type TextProps = PropsWithChildren<HtmlHTMLAttributes<HTMLDivElement> & {
variant?: TextVariant; variant?: TextVariant;
@ -471,15 +469,9 @@ export type ScrollerThin = ComponentType<PropsWithChildren<{
onScroll?(): void; onScroll?(): void;
}>>; }>>;
export type Clickable = ComponentType<PropsWithChildren<{ export type Clickable = <T extends "a" | "div" | "span" | "li" = "div">(props: PropsWithChildren<ComponentPropsWithRef<T>> & {
className?: string; tag?: T;
}) => ReactNode;
href?: string;
ignoreKeyPress?: boolean;
onClick?(): void;
onKeyPress?(): void;
}>>;
export type Avatar = ComponentType<PropsWithChildren<{ export type Avatar = ComponentType<PropsWithChildren<{
className?: string; className?: string;
@ -510,4 +502,3 @@ export type Icon = ComponentType<JSX.IntrinsicElements["svg"] & {
colorClass?: string; colorClass?: string;
} & Record<string, any>>; } & Record<string, any>>;
export type Icons = Record<IconNames, Icon>;

File diff suppressed because one or more lines are too long

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Channel, Guild, GuildMember, User } from "discord-types/general"; import { Channel, Guild, GuildMember, Message, User } from "discord-types/general";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { LiteralUnion } from "type-fest"; import { LiteralUnion } from "type-fest";
@ -133,6 +133,10 @@ export type Permissions = "CREATE_INSTANT_INVITE"
export type PermissionsBits = Record<Permissions, bigint>; export type PermissionsBits = Record<Permissions, bigint>;
export interface MessageSnapshot {
message: Message;
}
export interface Locale { export interface Locale {
name: string; name: string;
value: string; value: string;

View file

@ -172,26 +172,36 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
if (!exports) return; if (!exports) return;
// There are (at the time of writing) 11 modules exporting the window
// Make these non enumerable to improve webpack search performance
if (require.c) { if (require.c) {
let foundWindow = false; let shouldMakeNonEnumerable = false;
if (exports === window) { nonEnumerableChecking: {
foundWindow = true; // There are (at the time of writing) 11 modules exporting the window,
} else if (typeof exports === "object") { // and also modules exporting DOMTokenList, which breaks webpack finding
if (exports?.default === window) { // Make these non enumerable to improve search performance and avoid erros
foundWindow = true; if (exports === window || exports[Symbol.toStringTag] === "DOMTokenList") {
} else { shouldMakeNonEnumerable = true;
for (const nested in exports) if (nested.length <= 3) { break nonEnumerableChecking;
if (exports[nested] === window) {
foundWindow = true;
} }
if (typeof exports !== "object") {
break nonEnumerableChecking;
}
if (exports.default === window || exports.default?.[Symbol.toStringTag] === "DOMTokenList") {
shouldMakeNonEnumerable = true;
break nonEnumerableChecking;
}
for (const nested in exports) {
if (exports[nested] === window || exports[nested]?.[Symbol.toStringTag] === "DOMTokenList") {
shouldMakeNonEnumerable = true;
break nonEnumerableChecking;
} }
} }
} }
if (foundWindow) { if (shouldMakeNonEnumerable) {
Object.defineProperty(require.c, id, { Object.defineProperty(require.c, id, {
value: require.c[id], value: require.c[id],
enumerable: false, enumerable: false,
@ -221,7 +231,7 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
subscriptions.delete(filter); subscriptions.delete(filter);
callback(exports.default, id); callback(exports.default, id);
} else { } else {
for (const nested in exports) if (nested.length <= 3) { for (const nested in exports) {
if (exports[nested] && filter(exports[nested])) { if (exports[nested] && filter(exports[nested])) {
subscriptions.delete(filter); subscriptions.delete(filter);
callback(exports[nested], id); callback(exports[nested], id);

View file

@ -56,11 +56,14 @@ export const filters = {
: m => props.every(p => m[p] !== void 0), : m => props.every(p => m[p] !== void 0),
byCode: (...code: CodeFilter): FilterFn => { byCode: (...code: CodeFilter): FilterFn => {
code = code.map(canonicalizeMatch); const parsedCode = code.map(canonicalizeMatch);
return m => { const filter = m => {
if (typeof m !== "function") return false; if (typeof m !== "function") return false;
return stringMatches(Function.prototype.toString.call(m), code); return stringMatches(Function.prototype.toString.call(m), parsedCode);
}; };
filter.$$vencordProps = [...code];
return filter;
}, },
byStoreName: (name: StoreNameFilter): FilterFn => m => byStoreName: (name: StoreNameFilter): FilterFn => m =>
m.constructor?.displayName === name, m.constructor?.displayName === name,
@ -131,8 +134,7 @@ export const find = traceFunction("find", function find(filter: FilterFn, { isIn
return isWaitFor ? [found, key] : found; return isWaitFor ? [found, key] : found;
} }
// the length check makes search about 20% faster for (const nestedMod in mod.exports) {
for (const nestedMod in mod.exports) if (nestedMod.length <= 3) {
const nested = mod.exports[nestedMod]; const nested = mod.exports[nestedMod];
if (nested && filter(nested)) { if (nested && filter(nested)) {
return isWaitFor ? [nested, key] : nested; return isWaitFor ? [nested, key] : nested;
@ -163,7 +165,7 @@ export function findAll(filter: FilterFn) {
if (mod.exports.default && filter(mod.exports.default)) if (mod.exports.default && filter(mod.exports.default))
ret.push(mod.exports.default); ret.push(mod.exports.default);
else for (const nestedMod in mod.exports) if (nestedMod.length <= 3) { else for (const nestedMod in mod.exports) {
const nested = mod.exports[nestedMod]; const nested = mod.exports[nestedMod];
if (nested && filter(nested)) ret.push(nested); if (nested && filter(nested)) ret.push(nested);
} }
@ -226,8 +228,7 @@ export const findBulk = traceFunction("findBulk", function findBulk(...filterFns
break; break;
} }
for (const nestedMod in mod.exports) for (const nestedMod in mod.exports) {
if (nestedMod.length <= 3) {
const nested = mod.exports[nestedMod]; const nested = mod.exports[nestedMod];
if (nested && filter(nested)) { if (nested && filter(nested)) {
results[j] = nested; results[j] = nested;