Compare commits

..

No commits in common. "d509dda944bcdc6ccd0d594cdcb1e0331016dbe6" and "e5017fe8794a91fd21d8cfe486563f47ef2a0d42" have entirely different histories.

112 changed files with 1625 additions and 1747 deletions

View file

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

View file

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

View file

@ -57,7 +57,7 @@ const Badges = new Set<ProfileBadge>();
* Register a new badge with the Badges API
* @param badge The badge to register
*/
export function addProfileBadge(badge: ProfileBadge) {
export function addBadge(badge: ProfileBadge) {
badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true });
Badges.add(badge);
}
@ -66,7 +66,7 @@ export function addProfileBadge(badge: ProfileBadge) {
* Unregister a badge from the Badges API
* @param badge The badge to remove
*/
export function removeProfileBadge(badge: ProfileBadge) {
export function removeBadge(badge: ProfileBadge) {
return Badges.delete(badge);
}
@ -100,3 +100,20 @@ export interface BadgeUserArgs {
userId: string;
guildId: string;
}
interface ConnectedAccount {
type: string;
id: string;
name: string;
verified: boolean;
}
interface Profile {
connectedAccounts: ConnectedAccount[];
premiumType: number;
premiumSince: string;
premiumGuildSince?: any;
lastFetched: number;
profileFetchFailed: boolean;
application?: any;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -16,11 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Channel, Message } from "discord-types/general/index.js";
import { JSX } from "react";
export interface MessageDecorationProps {
interface DecorationProps {
author: {
/**
* Will be username if the user has no nickname
@ -46,25 +45,20 @@ export interface MessageDecorationProps {
message: Message;
[key: string]: any;
}
export type MessageDecorationFactory = (props: MessageDecorationProps) => JSX.Element | null;
export type Decoration = (props: DecorationProps) => JSX.Element | null;
export const decorations = new Map<string, MessageDecorationFactory>();
export const decorations = new Map<string, Decoration>();
export function addMessageDecoration(identifier: string, decoration: MessageDecorationFactory) {
export function addDecoration(identifier: string, decoration: Decoration) {
decorations.set(identifier, decoration);
}
export function removeMessageDecoration(identifier: string) {
export function removeDecoration(identifier: string) {
decorations.delete(identifier);
}
export function __addDecorationsToMessage(props: MessageDecorationProps): (JSX.Element | null)[] {
return Array.from(
decorations.entries(),
([key, Decoration]) => (
<ErrorBoundary noop message={`Failed to render ${key} Message Decoration`} key={key}>
<Decoration {...props} />
</ErrorBoundary>
)
);
export function __addDecorationsToMessage(props: DecorationProps): (JSX.Element | null)[] {
return [...decorations.values()].map(decoration => {
return decoration(props);
});
}

View file

@ -73,11 +73,11 @@ export interface MessageExtra {
openWarningPopout: (props: any) => any;
}
export type MessageSendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
export type MessageEditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void | { cancel: boolean; }>;
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void | { cancel: boolean; }>;
const sendListeners = new Set<MessageSendListener>();
const editListeners = new Set<MessageEditListener>();
const sendListeners = new Set<SendListener>();
const editListeners = new Set<EditListener>();
export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra, replyOptions: MessageReplyOptions) {
extra.replyOptions = replyOptions;
@ -111,29 +111,29 @@ export async function _handlePreEdit(channelId: string, messageId: string, messa
/**
* Note: This event fires off before a message is sent, allowing you to edit the message.
*/
export function addMessagePreSendListener(listener: MessageSendListener) {
export function addPreSendListener(listener: SendListener) {
sendListeners.add(listener);
return listener;
}
/**
* Note: This event fires off before a message's edit is applied, allowing you to further edit the message.
*/
export function addMessagePreEditListener(listener: MessageEditListener) {
export function addPreEditListener(listener: EditListener) {
editListeners.add(listener);
return listener;
}
export function removeMessagePreSendListener(listener: MessageSendListener) {
export function removePreSendListener(listener: SendListener) {
return sendListeners.delete(listener);
}
export function removeMessagePreEditListener(listener: MessageEditListener) {
export function removePreEditListener(listener: EditListener) {
return editListeners.delete(listener);
}
// Message clicks
export type MessageClickListener = (message: Message, channel: Channel, event: MouseEvent) => void;
type ClickListener = (message: Message, channel: Channel, event: MouseEvent) => void;
const listeners = new Set<MessageClickListener>();
const listeners = new Set<ClickListener>();
export function _handleClick(message: Message, channel: Channel, event: MouseEvent) {
// message object may be outdated, so (try to) fetch latest one
@ -147,11 +147,11 @@ export function _handleClick(message: Message, channel: Channel, event: MouseEve
}
}
export function addMessageClickListener(listener: MessageClickListener) {
export function addClickListener(listener: ClickListener) {
listeners.add(listener);
return listener;
}
export function removeMessageClickListener(listener: MessageClickListener) {
export function removeClickListener(listener: ClickListener) {
return listeners.delete(listener);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,7 +28,7 @@ import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { isPluginDev } from "@utils/misc";
import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal";
import { closeModal, Modals, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { Forms, Toasts, UserStore } from "@webpack/common";
import { User } from "discord-types/general";
@ -79,7 +79,7 @@ export default definePlugin({
replace: "...$1.props,$& $1.image??"
},
{
match: /(?<="aria-label":(\i)\.description,.{0,200})children:/,
match: /(?<=text:(\i)\.description,.{0,200})children:/,
replace: "children:$1.component ? $self.renderBadgeComponent({ ...$1 }) :"
},
// conditionally override their onClick with badge.onClick if it exists
@ -102,9 +102,8 @@ export default definePlugin({
}
},
userProfileBadge: ContributorBadge,
async start() {
Vencord.Api.Badges.addBadge(ContributorBadge);
await loadBadges();
},
@ -144,8 +143,8 @@ export default definePlugin({
closeModal(modalKey);
VencordNative.native.openExternal("https://github.com/sponsors/Vendicated");
}}>
<ModalRoot {...props}>
<ModalHeader>
<Modals.ModalRoot {...props}>
<Modals.ModalHeader>
<Flex style={{ width: "100%", justifyContent: "center" }}>
<Forms.FormTitle
tag="h2"
@ -159,8 +158,8 @@ export default definePlugin({
Vencord Donor
</Forms.FormTitle>
</Flex>
</ModalHeader>
<ModalContent>
</Modals.ModalHeader>
<Modals.ModalContent>
<Flex>
<img
role="presentation"
@ -183,13 +182,13 @@ export default definePlugin({
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
</Forms.FormText>
</div>
</ModalContent>
<ModalFooter>
</Modals.ModalContent>
<Modals.ModalFooter>
<Flex style={{ width: "100%", justifyContent: "center" }}>
<DonateButton />
</Flex>
</ModalFooter>
</ModalRoot>
</Modals.ModalFooter>
</Modals.ModalRoot>
</ErrorBoundary>
));
},

View file

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

View file

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

View file

@ -1,68 +0,0 @@
/*
* 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

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

View file

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

View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addAccessory } from "@api/MessageAccessories";
import { definePluginSettings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary";
@ -142,7 +143,7 @@ export default definePlugin({
required: true,
description: "Helps us provide support to you",
authors: [Devs.Ven],
dependencies: ["UserSettingsAPI"],
dependencies: ["UserSettingsAPI", "MessageAccessoriesAPI"],
settings,
@ -235,85 +236,6 @@ export default definePlugin({
}
},
renderMessageAccessory(props) {
const buttons = [] as JSX.Element[];
const shouldAddUpdateButton =
!IS_UPDATER_DISABLED
&& (
(props.channel.id === KNOWN_ISSUES_CHANNEL_ID) ||
(props.channel.id === SUPPORT_CHANNEL_ID && props.message.author.id === VENBOT_USER_ID)
)
&& props.message.content?.includes("update");
if (shouldAddUpdateButton) {
buttons.push(
<Button
key="vc-update"
color={Button.Colors.GREEN}
onClick={async () => {
try {
if (await forceUpdate())
showToast("Success! Restarting...", Toasts.Type.SUCCESS);
else
showToast("Already up to date!", Toasts.Type.MESSAGE);
} catch (e) {
new Logger(this.name).error("Error while updating:", e);
showToast("Failed to update :(", Toasts.Type.FAILURE);
}
}}
>
Update Now
</Button>
);
}
if (props.channel.id === SUPPORT_CHANNEL_ID) {
if (props.message.content.includes("/vencord-debug") || props.message.content.includes("/vencord-plugins")) {
buttons.push(
<Button
key="vc-dbg"
onClick={async () => sendMessage(props.channel.id, { content: await generateDebugInfoMessage() })}
>
Run /vencord-debug
</Button>,
<Button
key="vc-plg-list"
onClick={async () => sendMessage(props.channel.id, { content: generatePluginList() })}
>
Run /vencord-plugins
</Button>
);
}
if (props.message.author.id === VENBOT_USER_ID) {
const match = CodeBlockRe.exec(props.message.content || props.message.embeds[0]?.rawDescription || "");
if (match) {
buttons.push(
<Button
key="vc-run-snippet"
onClick={async () => {
try {
await AsyncFunction(match[1])();
showToast("Success!", Toasts.Type.SUCCESS);
} catch (e) {
new Logger(this.name).error("Error while running snippet:", e);
showToast("Failed to run snippet :(", Toasts.Type.FAILURE);
}
}}
>
Run Snippet
</Button>
);
}
}
}
return buttons.length
? <Flex>{buttons}</Flex>
: null;
},
renderContributorDmWarningCard: ErrorBoundary.wrap(({ channel }) => {
const userId = channel.getRecipientId();
if (!isPluginDev(userId)) return null;
@ -328,4 +250,85 @@ export default definePlugin({
</Card>
);
}, { noop: true }),
start() {
addAccessory("vencord-debug", props => {
const buttons = [] as JSX.Element[];
const shouldAddUpdateButton =
!IS_UPDATER_DISABLED
&& (
(props.channel.id === KNOWN_ISSUES_CHANNEL_ID) ||
(props.channel.id === SUPPORT_CHANNEL_ID && props.message.author.id === VENBOT_USER_ID)
)
&& props.message.content?.includes("update");
if (shouldAddUpdateButton) {
buttons.push(
<Button
key="vc-update"
color={Button.Colors.GREEN}
onClick={async () => {
try {
if (await forceUpdate())
showToast("Success! Restarting...", Toasts.Type.SUCCESS);
else
showToast("Already up to date!", Toasts.Type.MESSAGE);
} catch (e) {
new Logger(this.name).error("Error while updating:", e);
showToast("Failed to update :(", Toasts.Type.FAILURE);
}
}}
>
Update Now
</Button>
);
}
if (props.channel.id === SUPPORT_CHANNEL_ID) {
if (props.message.content.includes("/vencord-debug") || props.message.content.includes("/vencord-plugins")) {
buttons.push(
<Button
key="vc-dbg"
onClick={async () => sendMessage(props.channel.id, { content: await generateDebugInfoMessage() })}
>
Run /vencord-debug
</Button>,
<Button
key="vc-plg-list"
onClick={async () => sendMessage(props.channel.id, { content: generatePluginList() })}
>
Run /vencord-plugins
</Button>
);
}
if (props.message.author.id === VENBOT_USER_ID) {
const match = CodeBlockRe.exec(props.message.content || props.message.embeds[0]?.rawDescription || "");
if (match) {
buttons.push(
<Button
key="vc-run-snippet"
onClick={async () => {
try {
await AsyncFunction(match[1])();
showToast("Success!", Toasts.Type.SUCCESS);
} catch (e) {
new Logger(this.name).error("Error while running snippet:", e);
showToast("Failed to run snippet :(", Toasts.Type.FAILURE);
}
}}
>
Run Snippet
</Button>
);
}
}
}
return buttons.length
? <Flex>{buttons}</Flex>
: null;
});
},
});

View file

@ -16,14 +16,14 @@ import { User } from "discord-types/general";
interface UserProfileProps {
popoutProps: Record<string, any>;
currentUser: User;
originalRenderPopout: () => React.ReactNode;
originalPopout: () => React.ReactNode;
}
const UserProfile = findComponentByCodeLazy("UserProfilePopoutWrapper: user cannot be undefined");
const styles = findByPropsLazy("accountProfilePopoutWrapper");
let openAlternatePopout = false;
let accountPanelRef: React.RefObject<Record<PropertyKey, any> | null> = { current: null };
let accountPanelRef: React.MutableRefObject<Record<PropertyKey, any> | null> = { current: null };
const AccountPanelContextMenu = ErrorBoundary.wrap(() => {
const { prioritizeServerProfile } = settings.use(["prioritizeServerProfile"]);
@ -73,12 +73,12 @@ export default definePlugin({
group: true,
replacement: [
{
match: /(?<=\.AVATAR_SIZE\);)/,
match: /(?<=\.SIZE_32\)}\);)/,
replace: "$self.useAccountPanelRef();"
},
{
match: /(\.AVATAR,children:.+?renderPopout:(\i)=>){(.+?)}(?=,position)(?<=currentUser:(\i).+?)/,
replace: (_, rest, popoutProps, originalPopout, currentUser) => `${rest}$self.UserProfile({popoutProps:${popoutProps},currentUser:${currentUser},originalRenderPopout:()=>{${originalPopout}}})`
replace: (_, rest, popoutProps, originalPopout, currentUser) => `${rest}$self.UserProfile({popoutProps:${popoutProps},currentUser:${currentUser},originalPopout:()=>{${originalPopout}}})`
},
{
match: /\.AVATAR,children:.+?(?=renderPopout:)/,
@ -112,17 +112,17 @@ export default definePlugin({
openAlternatePopout = false;
},
UserProfile: ErrorBoundary.wrap(({ popoutProps, currentUser, originalRenderPopout }: UserProfileProps) => {
UserProfile: ErrorBoundary.wrap(({ popoutProps, currentUser, originalPopout }: UserProfileProps) => {
if (
(settings.store.prioritizeServerProfile && openAlternatePopout) ||
(!settings.store.prioritizeServerProfile && !openAlternatePopout)
) {
return originalRenderPopout();
return originalPopout();
}
const currentChannel = getCurrentChannel();
if (currentChannel?.getGuildId() == null) {
return originalRenderPopout();
return originalPopout();
}
return (

View file

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

View file

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

View file

@ -173,8 +173,8 @@ export default definePlugin({
// Disable expanding and collapsing folders transition in the normal GuildsBar sidebar
{
predicate: () => !settings.store.keepIcons,
match: /(?=,\{from:\{height)/,
replace: "&&$self.shouldShowTransition(arguments[0])"
match: /(?<=#{intl::SERVER_FOLDER_PLACEHOLDER}.+?useTransition\)\()/,
replace: "$self.shouldShowTransition(arguments[0])&&"
},
// 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.colorString) {
children.unshift(
children.push(
<Menu.MenuItem
id="vc-copy-role-color"
label="Copy Role Color"
@ -93,20 +93,6 @@ export default definePlugin({
);
}
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
children.unshift(
<Menu.MenuItem
id="vc-edit-role"
label="Edit Role"
action={async () => {
await GuildSettingsActions.open(guild.id, "ROLES");
GuildSettingsActions.selectRole(id);
}}
icon={PencilIcon}
/>
);
}
if (role.icon) {
children.push(
<Menu.MenuItem
@ -124,6 +110,20 @@ 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 { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
import { Constants, React, RestAPI, Tooltip } from "@webpack/common";
import { RenameButton } from "./components/RenameButton";
@ -34,7 +34,7 @@ const UserSettingsModal = findByPropsLazy("saveAccountChanges", "open");
const TimestampClasses = findByPropsLazy("timestampTooltip", "blockquoteContainer");
const SessionIconClasses = findByPropsLazy("sessionIcon");
const BlobMask = findComponentByCodeLazy("!1,lowerBadgeSize:");
const BlobMask = findExportedComponentLazy("BlobMask");
const settings = definePluginSettings({
backgroundCheck: {

View file

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

View file

@ -27,7 +27,7 @@ export default definePlugin({
{
find: '"ChannelAttachButton"',
replacement: {
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),.{0,30}?\.\.\.(\i),/,
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),\.\.\.(\i),/,
replace: "$&onClick:$1,onContextMenu:$2.onClick,",
},
},

View file

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

View file

@ -17,7 +17,11 @@
*/
import {
MessageObject
addPreEditListener,
addPreSendListener,
MessageObject,
removePreEditListener,
removePreSendListener
} from "@api/MessageEvents";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -32,18 +36,7 @@ export default definePlugin({
name: "ClearURLs",
description: "Removes tracking garbage from URLs",
authors: [Devs.adryd],
start() {
this.createRules();
},
onBeforeMessageSend(_, msg) {
return this.onSend(msg);
},
onBeforeMessageEdit(_cid, _mid, msg) {
return this.onSend(msg);
},
dependencies: ["MessageEventsAPI"],
escapeRegExp(str: string) {
return (str && reHasRegExpChar.test(str))
@ -140,4 +133,17 @@ 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,12 +91,15 @@ function ThemeSettings() {
const settings = definePluginSettings({
color: {
description: "Color your Discord client theme will be based around. Light mode isn't supported",
type: OptionType.COMPONENT,
default: "313338",
component: ThemeSettings
component: () => <ThemeSettings />
},
resetColor: {
description: "Reset Theme Color",
type: OptionType.COMPONENT,
default: "313338",
component: () => (
<Button onClick={() => onPickColor(0x313338)}>
Reset Theme Color

View file

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

View file

@ -63,7 +63,7 @@ function makeShortcuts() {
default:
const uniqueMatches = [...new Set(matches)];
if (uniqueMatches.length > 1)
console.warn(`Warning: This filter matches ${uniqueMatches.length} exports. Make it more specific!\n`, uniqueMatches);
console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches);
return matches[0];
}
@ -165,38 +165,11 @@ function loadAndCacheShortcut(key: string, val: any, forceLoad: boolean) {
const currentVal = val.getter();
if (!currentVal || val.preload === false) return currentVal;
function unwrapProxy(value: any) {
if (value[SYM_LAZY_GET]) {
forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED];
} else if (value.$$vencordInternal) {
return forceLoad ? value.$$vencordInternal() : value;
}
const value = currentVal[SYM_LAZY_GET]
? forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED]
: currentVal;
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 });
}
if (value) define(window.shortcutList, key, { value });
return value;
}
@ -206,16 +179,6 @@ export default definePlugin({
description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.",
authors: [Devs.Ven],
patches: [
{
find: 'this,"_changeCallbacks",',
replacement: {
match: /\i\(this,"_changeCallbacks",/,
replace: "Reflect.defineProperty(this,Symbol.toStringTag,{value:this.getName(),configurable:!0,writable:!0,enumerable:!1}),$&"
}
}
],
startAt: StartAt.Init,
start() {
const shortcuts = makeShortcuts();

View file

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

View file

@ -19,7 +19,6 @@
import { definePluginSettings, Settings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
import { isTruthy } from "@utils/guards";
@ -28,14 +27,15 @@ import { classes } from "@utils/misc";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findComponentByCodeLazy } from "@webpack";
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, React, UserStore } from "@webpack/common";
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
const ActivityView = findComponentByCodeLazy(".party?(0", ".card");
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
async function getApplicationAsset(key: string): Promise<string> {
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0];
}
@ -169,7 +169,7 @@ const settings = definePluginSettings({
value: TimestampMode.NOW
},
{
label: "Same as your current time (not reset after 24h)",
label: "Same as your current time",
value: TimestampMode.TIME
},
{
@ -269,7 +269,6 @@ function isStreamLinkDisabled() {
function isStreamLinkValid(value: string) {
if (!isStreamLinkDisabled() && !/https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(value)) return "Streaming link must be a valid URL.";
if (value && value.length > 512) return "Streaming link must be not longer than 512 characters.";
return true;
}
@ -278,9 +277,8 @@ function isTimestampDisabled() {
}
function isImageKeyValid(value: string) {
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//.test(value)) return "Don't use a Discord link. Use an Imgur image link instead.";
if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image (e.g. https://i.imgur.com/...). Right click the image and click 'Copy image address'";
if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image (e.g. https://media.tenor.com/...). Right click the GIF and click 'Copy image address'";
if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image. (e.g. https://i.imgur.com/...)";
if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image. (e.g. https://media.tenor.com/...)";
return true;
}
@ -392,24 +390,13 @@ async function setRpc(disable?: boolean) {
export default definePlugin({
name: "CustomRPC",
description: "Add a fully customisable Rich Presence (Game status) to your Discord profile",
description: "Allows you to set a custom rich presence.",
authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
dependencies: ["UserSettingsAPI"],
start: setRpc,
stop: () => setRpc(true),
settings,
patches: [
{
find: ".party?(0",
all: true,
replacement: {
match: /\i\.id===\i\.id\?null:/,
replace: ""
}
}
],
settingsAboutComponent: () => {
const activity = useAwaiter(createActivity);
const gameActivityEnabled = ShowCurrentGame.useSetting();
@ -423,7 +410,7 @@ export default definePlugin({
style={{ padding: "1em" }}
>
<Forms.FormTitle>Notice</Forms.FormTitle>
<Forms.FormText>Activity Sharing isn't enabled, people won't be able to see your custom rich presence!</Forms.FormText>
<Forms.FormText>Game activity isn't enabled, people won't be able to see your custom rich presence!</Forms.FormText>
<Button
color={Button.Colors.TRANSPARENT}
@ -435,33 +422,24 @@ export default definePlugin({
</ErrorCard>
)}
<Flex flexDirection="column" style={{ gap: ".5em" }} className={Margins.top16}>
<Forms.FormText>
Go to the <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
get the application ID.
</Forms.FormText>
<Forms.FormText>
Upload images in the Rich Presence tab to get the image keys.
</Forms.FormText>
<Forms.FormText>
If you want to use an image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and selecting "Copy image address".
</Forms.FormText>
<Forms.FormText>
You can't see your own buttons on your profile, but everyone else can see it fine.
</Forms.FormText>
<Forms.FormText>
Some weird unicode text ("fonts" 𝖑𝖎𝖐𝖊 𝖙𝖍𝖎𝖘) may cause the rich presence to not show up, try using normal letters instead.
</Forms.FormText>
</Flex>
<Forms.FormText>
Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
get the application ID.
</Forms.FormText>
<Forms.FormText>
Upload images in the Rich Presence tab to get the image keys.
</Forms.FormText>
<Forms.FormText>
If you want to use image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and select "Copy image address".
</Forms.FormText>
<Forms.FormDivider className={Margins.top8} />
<div style={{ width: "284px", ...profileThemeStyle, marginTop: 8, borderRadius: 8, background: "var(--bg-mod-faint)" }}>
{activity[0] && <ActivityView
activity={activity[0]}
user={UserStore.getCurrentUser()}
currentUser={UserStore.getCurrentUser()}
/>}
<div style={{ width: "284px", ...profileThemeStyle, padding: 8, marginTop: 8, borderRadius: 8, background: "var(--bg-mod-faint)" }}>
{activity[0] && <ActivityComponent activity={activity[0]} channelId={SelectedChannelStore.getChannelId()}
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}
application={{ id: settings.store.appID }}
user={UserStore.getCurrentUser()} />}
</div>
</>
);

View file

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

View file

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

View file

@ -13,7 +13,7 @@ export default definePlugin({
authors: [Devs.Nuckyz],
patches: [
{
find: ".handleImageLoad)",
find: "getFormatQuality(){",
replacement: {
match: /(?<=null;return )\i\.\i&&\(\i\|\|!\i\.isAnimated.+?:(?=\i&&\(\i="png"\))/,
replace: ""

View file

@ -27,7 +27,7 @@ export default definePlugin({
authors: [Devs.D3SOX, Devs.Nickyux],
patches: [
{
find: "#{intl::GUILD_OWNER}),children:",
find: "#{intl::GUILD_OWNER}",
replacement: {
match: /,isOwner:(\i),/,
replace: ",_isOwner:$1=$self.isGuildOwner(e),"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,19 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addProfileBadge, removeProfileBadge } from "@api/Badges";
import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { registerCommand, unregisterCommand } from "@api/Commands";
import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
import { 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 { Settings } from "@api/Settings";
import { Logger } from "@utils/Logger";
import { canonicalizeFind } from "@utils/patches";
import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types";
import { Patch, Plugin, ReporterTestable, StartAt } from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
import { FluxEvents } from "@webpack/types";
@ -90,13 +83,6 @@ function isReporterTestable(p: Plugin, part: ReporterTestable) {
: (p.reporterTestable & part) === part;
}
const pluginKeysToBind: Array<keyof PluginDef & `${"on" | "render"}${string}`> = [
"onBeforeMessageEdit", "onBeforeMessageSend", "onMessageClick",
"renderChatBarButton", "renderMemberListDecorator", "renderMessageAccessory", "renderMessageDecoration", "renderMessagePopoverButton"
];
const neededApiPlugins = new Set<string>();
// First round-trip to mark and force enable dependencies
//
// FIXME: might need to revisit this if there's ever nested (dependencies of dependencies) dependencies since this only
@ -120,46 +106,22 @@ for (const p of pluginsValues) if (isPluginEnabled(p.name)) {
dep.isDependency = true;
});
if (p.commands?.length) neededApiPlugins.add("CommandsAPI");
if (p.onBeforeMessageEdit || p.onBeforeMessageSend || p.onMessageClick) neededApiPlugins.add("MessageEventsAPI");
if (p.renderChatBarButton) neededApiPlugins.add("ChatInputButtonAPI");
if (p.renderMemberListDecorator) neededApiPlugins.add("MemberListDecoratorsAPI");
if (p.renderMessageAccessory) neededApiPlugins.add("MessageAccessoriesAPI");
if (p.renderMessageDecoration) neededApiPlugins.add("MessageDecorationsAPI");
if (p.renderMessagePopoverButton) neededApiPlugins.add("MessagePopoverAPI");
if (p.userProfileBadge) neededApiPlugins.add("BadgeAPI");
for (const key of pluginKeysToBind) {
p[key] &&= p[key].bind(p) as any;
if (p.commands?.length) {
Plugins.CommandsAPI.isDependency = true;
settings.CommandsAPI.enabled = true;
}
}
for (const p of neededApiPlugins) {
Plugins[p].isDependency = true;
settings[p].enabled = true;
}
for (const p of pluginsValues) {
if (p.settings) {
p.options ??= {};
p.settings.pluginName = p.name;
for (const name in p.settings.def) {
const def = p.settings.def[name];
p.options ??= {};
for (const [name, def] of Object.entries(p.settings.def)) {
const checks = p.settings.checks?.[name];
p.options[name] = { ...def, ...checks };
}
}
if (p.options) {
for (const name in p.options) {
const opt = p.options[name];
if (opt.onChange != null) {
SettingsStore.addChangeListener(`plugins.${p.name}.${name}`, opt.onChange);
}
}
}
if (p.patches && isPluginEnabled(p.name)) {
if (!IS_REPORTER || isReporterTestable(p, ReporterTestable.Patches)) {
for (const patch of p.patches) {
@ -253,11 +215,7 @@ export function subscribeAllPluginsFluxEvents(fluxDispatcher: typeof FluxDispatc
}
export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) {
const {
name, commands, contextMenus, userProfileBadge,
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
renderChatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton
} = p;
const { name, commands, contextMenus } = p;
if (p.start) {
logger.info("Starting plugin", name);
@ -291,6 +249,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
subscribePluginFluxEvents(p, FluxDispatcher);
}
if (contextMenus) {
logger.debug("Adding context menus patches of plugin", name);
for (const navId in contextMenus) {
@ -298,27 +257,11 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
}
}
if (userProfileBadge) addProfileBadge(userProfileBadge);
if (onBeforeMessageEdit) addMessagePreEditListener(onBeforeMessageEdit);
if (onBeforeMessageSend) addMessagePreSendListener(onBeforeMessageSend);
if (onMessageClick) addMessageClickListener(onMessageClick);
if (renderChatBarButton) addChatBarButton(name, renderChatBarButton);
if (renderMemberListDecorator) addMemberListDecorator(name, renderMemberListDecorator);
if (renderMessageDecoration) addMessageDecoration(name, renderMessageDecoration);
if (renderMessageAccessory) addMessageAccessory(name, renderMessageAccessory);
if (renderMessagePopoverButton) addMessagePopoverButton(name, renderMessagePopoverButton);
return true;
}, p => `startPlugin ${p.name}`);
export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) {
const {
name, commands, contextMenus, userProfileBadge,
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
renderChatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton
} = p;
const { name, commands, contextMenus } = p;
if (p.stop) {
logger.info("Stopping plugin", name);
@ -357,17 +300,5 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
}
}
if (userProfileBadge) removeProfileBadge(userProfileBadge);
if (onBeforeMessageEdit) removeMessagePreEditListener(onBeforeMessageEdit);
if (onBeforeMessageSend) removeMessagePreSendListener(onBeforeMessageSend);
if (onMessageClick) removeMessageClickListener(onMessageClick);
if (renderChatBarButton) removeChatBarButton(name);
if (renderMemberListDecorator) removeMemberListDecorator(name);
if (renderMessageDecoration) removeMessageDecoration(name);
if (renderMessageAccessory) removeMessageAccessory(name);
if (renderMessagePopoverButton) removeMessagePopoverButton(name);
return true;
}, p => `stopPlugin ${p.name}`);

View file

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

View file

@ -1,17 +0,0 @@
# 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

@ -1,85 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { 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 PresenceStore = findByPropsLazy("getLocalPresence");
const presenceStore = findByPropsLazy("getLocalPresence");
async function getApplicationAsset(key: string): Promise<string> {
return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];
@ -124,11 +124,6 @@ const settings = definePluginSettings({
type: OptionType.BOOLEAN,
default: true,
},
hideWithActivity: {
description: "Hide Last.fm presence if you have any other presence",
type: OptionType.BOOLEAN,
default: false,
},
statusName: {
description: "custom status text",
type: OptionType.STRING,
@ -279,16 +274,12 @@ export default definePlugin({
},
async getActivity(): Promise<Activity | null> {
if (settings.store.hideWithActivity) {
if (PresenceStore.getActivities().some(a => a.application_id !== applicationId)) {
return null;
}
}
if (settings.store.hideWithSpotify) {
if (PresenceStore.getActivities().some(a => a.type === ActivityType.LISTENING && a.application_id !== applicationId)) {
// there is already music status because of Spotify or richerCider (probably more)
return null;
for (const activity of presenceStore.getActivities()) {
if (activity.type === ActivityType.LISTENING && activity.application_id !== applicationId) {
// there is already music status because of Spotify or richerCider (probably more)
return null;
}
}
}

View file

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

View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addClickListener, removeClickListener } from "@api/MessageEvents";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -56,64 +57,66 @@ export default definePlugin({
name: "MessageClickActions",
description: "Hold Backspace and click to delete, double click to edit/reply",
authors: [Devs.Ven],
dependencies: ["MessageEventsAPI"],
settings,
start() {
document.addEventListener("keydown", keydown);
document.addEventListener("keyup", keyup);
this.onClick = addClickListener((msg: any, channel, event) => {
const isMe = msg.author.id === UserStore.getCurrentUser().id;
if (!isDeletePressed) {
if (event.detail < 2) return;
if (settings.store.requireModifier && !event.ctrlKey && !event.shiftKey) return;
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return;
if (msg.deleted === true) return;
if (isMe) {
if (!settings.store.enableDoubleClickToEdit || EditStore.isEditing(channel.id, msg.id) || msg.state !== "SENT") return;
MessageActions.startEditMessage(channel.id, msg.id, msg.content);
event.preventDefault();
} else {
if (!settings.store.enableDoubleClickToReply) return;
const EPHEMERAL = 64;
if (msg.hasFlag(EPHEMERAL)) return;
const isShiftPress = event.shiftKey && !settings.store.requireModifier;
const NoReplyMention = Vencord.Plugins.plugins.NoReplyMention as any as typeof import("../noReplyMention").default;
const shouldMention = Vencord.Plugins.isPluginEnabled("NoReplyMention")
? NoReplyMention.shouldMention(msg, isShiftPress)
: !isShiftPress;
FluxDispatcher.dispatch({
type: "CREATE_PENDING_REPLY",
channel,
message: msg,
shouldMention,
showMentionToggle: channel.guild_id !== null
});
}
} else if (settings.store.enableDeleteOnClick && (isMe || PermissionStore.can(PermissionsBits.MANAGE_MESSAGES, channel))) {
if (msg.deleted) {
FluxDispatcher.dispatch({
type: "MESSAGE_DELETE",
channelId: channel.id,
id: msg.id,
mlDeleted: true
});
} else {
MessageActions.deleteMessage(channel.id, msg.id);
}
event.preventDefault();
}
});
},
stop() {
removeClickListener(this.onClick);
document.removeEventListener("keydown", keydown);
document.removeEventListener("keyup", keyup);
},
onMessageClick(msg: any, channel, event) {
const isMe = msg.author.id === UserStore.getCurrentUser().id;
if (!isDeletePressed) {
if (event.detail < 2) return;
if (settings.store.requireModifier && !event.ctrlKey && !event.shiftKey) return;
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return;
if (msg.deleted === true) return;
if (isMe) {
if (!settings.store.enableDoubleClickToEdit || EditStore.isEditing(channel.id, msg.id) || msg.state !== "SENT") return;
MessageActions.startEditMessage(channel.id, msg.id, msg.content);
event.preventDefault();
} else {
if (!settings.store.enableDoubleClickToReply) return;
const EPHEMERAL = 64;
if (msg.hasFlag(EPHEMERAL)) return;
const isShiftPress = event.shiftKey && !settings.store.requireModifier;
const NoReplyMention = Vencord.Plugins.plugins.NoReplyMention as any as typeof import("../noReplyMention").default;
const shouldMention = Vencord.Plugins.isPluginEnabled("NoReplyMention")
? NoReplyMention.shouldMention(msg, isShiftPress)
: !isShiftPress;
FluxDispatcher.dispatch({
type: "CREATE_PENDING_REPLY",
channel,
message: msg,
shouldMention,
showMentionToggle: channel.guild_id !== null
});
}
} else if (settings.store.enableDeleteOnClick && (isMe || PermissionStore.can(PermissionsBits.MANAGE_MESSAGES, channel))) {
if (msg.deleted) {
FluxDispatcher.dispatch({
type: "MESSAGE_DELETE",
channelId: channel.id,
id: msg.id,
mlDeleted: true
});
} else {
MessageActions.deleteMessage(channel.id, msg.id);
}
event.preventDefault();
}
},
}
});

View file

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

View file

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

View file

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

View file

@ -0,0 +1,372 @@
/*
* 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}",
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/>.
*/
import { definePluginSettings, migratePluginSetting } from "@api/Settings";
import { Settings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { runtimeHashMessageKey } from "@utils/intlHash";
import { Logger } from "@utils/Logger";
@ -32,29 +32,10 @@ interface MessageDeleteProps {
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({
name: "NoBlockedMessages",
description: "Hides all blocked/ignored messages from chat completely",
authors: [Devs.rushii, Devs.Samu, Devs.jamesbt365],
settings,
description: "Hides all blocked messages from chat completely.",
authors: [Devs.rushii, Devs.Samu],
patches: [
{
find: "#{intl::BLOCKED_MESSAGES_HIDE}",
@ -70,40 +51,38 @@ export default definePlugin({
'"ReadStateStore"'
].map(find => ({
find,
predicate: () => settings.store.ignoreMessages,
predicate: () => Settings.plugins.NoBlockedMessages.ignoreBlockedMessages === true,
replacement: [
{
match: /(?<=function (\i)\((\i)\){)(?=.*MESSAGE_CREATE:\1)/,
replace: (_, _funcName, props) => `if($self.shouldIgnoreMessage(${props}.message))return;`
replace: (_, _funcName, props) => `if($self.isBlocked(${props}.message))return;`
}
]
}))
],
options: {
ignoreBlockedMessages: {
description: "Completely ignores (recent) incoming messages from blocked users (locally).",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true,
},
},
shouldIgnoreMessage(message: Message) {
isBlocked(message: Message) {
try {
if (RelationshipStore.isBlocked(message.author.id)) {
return true;
}
return settings.store.applyToIgnoredUsers && RelationshipStore.isIgnored(message.author.id);
return RelationshipStore.isBlocked(message.author.id);
} catch (e) {
new Logger("NoBlockedMessages").error("Failed to check if user is blocked or ignored:", e);
return false;
new Logger("NoBlockedMessages").error("Failed to check if user is blocked:", e);
}
},
shouldHide(props: MessageDeleteProps): boolean {
shouldHide(props: MessageDeleteProps) {
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;
return props.collapsedReason() === i18n.t[runtimeHashMessageKey("BLOCKED_MESSAGE_COUNT")]();
} catch (e) {
console.error(e);
return false;
}
return false;
}
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,14 +18,14 @@
import "./style.css";
import { addProfileBadge, BadgePosition, BadgeUserArgs, ProfileBadge, removeProfileBadge } from "@api/Badges";
import { addMemberListDecorator, removeMemberListDecorator } from "@api/MemberListDecorators";
import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations";
import { addBadge, BadgePosition, BadgeUserArgs, ProfileBadge, removeBadge } from "@api/Badges";
import { addDecorator, removeDecorator } from "@api/MemberListDecorators";
import { addDecoration, removeDecoration } from "@api/MessageDecorations";
import { Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { filters, findStoreLazy, mapMangledModuleLazy } from "@webpack";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { PresenceStore, Tooltip, UserStore } from "@webpack/common";
import { User } from "discord-types/general";
@ -70,9 +70,7 @@ const Icons = {
};
type Platform = keyof typeof Icons;
const { useStatusFillColor } = mapMangledModuleLazy(".concat(.5625*", {
useStatusFillColor: filters.byCode(".hex")
});
const StatusUtils = findByPropsLazy("useStatusFillColor", "StatusTypes");
const PlatformIcon = ({ platform, status, small }: { platform: Platform, status: string; small: boolean; }) => {
const tooltip = platform === "embedded"
@ -81,7 +79,7 @@ const PlatformIcon = ({ platform, status, small }: { platform: Platform, status:
const Icon = Icons[platform] ?? Icons.desktop;
return <Icon color={useStatusFillColor(status)} tooltip={tooltip} small={small} />;
return <Icon color={StatusUtils.useStatusFillColor(status)} tooltip={tooltip} small={small} />;
};
function ensureOwnStatus(user: User) {
@ -174,26 +172,26 @@ const badge: ProfileBadge = {
const indicatorLocations = {
list: {
description: "In the member list",
onEnable: () => addMemberListDecorator("platform-indicator", props =>
onEnable: () => addDecorator("platform-indicator", props =>
<ErrorBoundary noop>
<PlatformIndicator user={props.user} small={true} />
</ErrorBoundary>
),
onDisable: () => removeMemberListDecorator("platform-indicator")
onDisable: () => removeDecorator("platform-indicator")
},
badges: {
description: "In user profiles, as badges",
onEnable: () => addProfileBadge(badge),
onDisable: () => removeProfileBadge(badge)
onEnable: () => addBadge(badge),
onDisable: () => removeBadge(badge)
},
messages: {
description: "Inside messages",
onEnable: () => addMessageDecoration("platform-indicator", props =>
onEnable: () => addDecoration("platform-indicator", props =>
<ErrorBoundary noop>
<PlatformIndicator user={props.message?.author} wantTopMargin={true} />
</ErrorBoundary>
),
onDisable: () => removeMessageDecoration("platform-indicator")
onDisable: () => removeDecoration("platform-indicator")
}
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,8 +31,7 @@ export function openGuildInfoModal(guild: Guild) {
const enum Tabs {
ServerInfo,
Friends,
BlockedUsers,
IgnoredUsers
BlockedUsers
}
interface GuildProps {
@ -45,8 +44,7 @@ interface RelationshipProps extends GuildProps {
const fetched = {
friends: false,
blocked: false,
ignored: false
blocked: false
};
function renderTimestamp(timestamp: number) {
@ -58,12 +56,10 @@ function renderTimestamp(timestamp: number) {
function GuildInfoModal({ guild }: GuildProps) {
const [friendCount, setFriendCount] = useState<number>();
const [blockedCount, setBlockedCount] = useState<number>();
const [ignoredCount, setIgnoredCount] = useState<number>();
useEffect(() => {
fetched.friends = false;
fetched.blocked = false;
fetched.ignored = false;
}, []);
const [currentTab, setCurrentTab] = useState(Tabs.ServerInfo);
@ -136,19 +132,12 @@ function GuildInfoModal({ guild }: GuildProps) {
>
Blocked Users{blockedCount !== undefined ? ` (${blockedCount})` : ""}
</TabBar.Item>
<TabBar.Item
className={cl("tab", { selected: currentTab === Tabs.IgnoredUsers })}
id={Tabs.IgnoredUsers}
>
Ignored Users{ignoredCount !== undefined ? ` (${ignoredCount})` : ""}
</TabBar.Item>
</TabBar>
<div className={cl("tab-content")}>
{currentTab === Tabs.ServerInfo && <ServerInfoTab guild={guild} />}
{currentTab === Tabs.Friends && <FriendsTab guild={guild} setCount={setFriendCount} />}
{currentTab === Tabs.BlockedUsers && <BlockedUsersTab guild={guild} setCount={setBlockedCount} />}
{currentTab === Tabs.IgnoredUsers && <IgnoredUserTab guild={guild} setCount={setIgnoredCount} />}
</div>
</div>
);
@ -222,13 +211,7 @@ function BlockedUsersTab({ guild, setCount }: RelationshipProps) {
return UserList("blocked", guild, blockedIds, setCount);
}
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) {
function UserList(type: "friends" | "blocked", guild: Guild, ids: string[], setCount: (count: number) => void) {
const missing = [] 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
{
match: /(this\.record\)\?{renderLevel:(.+?),threadIds.+?renderLevel:).+?(?=,threadIds)/g,
match: /(activeJoinedRelevantThreads:.{0,50}VIEW_CHANNEL.+?renderLevel:(.+?),threadIds.+?renderLevel:).+?(?=,threadIds)/g,
replace: (_, rest, defaultRenderLevel) => `${rest}${defaultRenderLevel}`
},
// Remove permission checking for getRenderLevel function
@ -108,10 +108,8 @@ export default definePlugin({
},
{
// Prevent Discord from trying to connect to hidden voice channels
match: /(?=(\|\||&&)\i\.\i\.selectVoiceChannel\((\i)\.id\))/,
replace: (_, condition, channel) => condition === "||"
? `||$self.isHiddenChannel(${channel})`
: `&&!$self.isHiddenChannel(${channel})`
match: /(?=&&\i\.\i\.selectVoiceChannel\((\i)\.id\))/,
replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})`
},
{
// Make Discord show inside the channel if clicking on a hidden or locked channel
@ -124,10 +122,8 @@ export default definePlugin({
{
find: ".AUDIENCE),{isSubscriptionGated",
replacement: {
match: /(!)?(\i)\.isRoleSubscriptionTemplatePreviewChannel\(\)/,
replace: (m, not, channel) => not
? `${m}&&!$self.isHiddenChannel(${channel})`
: `${m}||$self.isHiddenChannel(${channel})`
match: /!(\i)\.isRoleSubscriptionTemplatePreviewChannel\(\)/,
replace: (m, channel) => `${m}&&!$self.isHiddenChannel(${channel})`
}
},
{
@ -177,10 +173,8 @@ export default definePlugin({
},
// Make voice channels also appear as muted if they are muted
{
match: /(?<=\.wrapper:\i\.notInteractive,)(.+?)(if\()?(\i)(?:\)return |\?)(\i\.MUTED)/,
replace: (_, otherClasses, isIf, isMuted, mutedClassExpression) => isIf
? `${isMuted}?${mutedClassExpression}:"",${otherClasses}if(${isMuted})return ""`
: `${isMuted}?${mutedClassExpression}:"",${otherClasses}${isMuted}?""`
match: /(?<=\.wrapper:\i\.notInteractive,)(.+?)if\((\i)\)return (\i\.MUTED);/,
replace: (_, otherClasses, isMuted, mutedClassExpression) => `${isMuted}?${mutedClassExpression}:"",${otherClasses}if(${isMuted})return "";`
}
]
},
@ -190,8 +184,8 @@ export default definePlugin({
{
// 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,
match: /(?<=\.LOCKED(?:;if\(|:))(?<={channel:(\i).+?)/,
replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&`
match: /\.LOCKED;if\((?<={channel:(\i).+?)/,
replace: (m, channel) => `${m}!$self.isHiddenChannel(${channel})&&`
},
{
// Hide unreads
@ -230,12 +224,12 @@ export default definePlugin({
find: "Missing channel in Channel.renderHeaderToolbar",
replacement: [
{
match: /"renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_TEXT:(?=.+?(\i\.push.{0,50}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
replace: (m, pushNotificationButtonExpression, channel, isLurking) => `${m}if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
match: /(?<="renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_TEXT:)(?=.+?(\i\.push.{0,50}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
replace: (_, pushNotificationButtonExpression, channel, isLurking) => `if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
},
{
match: /"renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_MEDIA:(?=.+?(\i\.push.{0,40}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
replace: (m, pushNotificationButtonExpression, channel, isLurking) => `${m}if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
match: /(?<="renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_MEDIA:)(?=.+?(\i\.push.{0,40}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
replace: (_, pushNotificationButtonExpression, channel, isLurking) => `if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
},
{
match: /"renderMobileToolbar",\(\)=>{.+?case \i\.\i\.GUILD_DIRECTORY:(?<=let{channel:(\i).+?)/,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,12 +23,12 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { findComponentByCodeLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
import { GuildMemberStore, RelationshipStore, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
import { buildSeveralUsers } from "../typingTweaks";
const ThreeDots = findComponentByCodeLazy(".dots,", "dotRadius:");
const ThreeDots = findExportedComponentLazy("Dots", "AnimatedDots");
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
const TypingStore = findStoreLazy("TypingStore");

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,17 +17,18 @@
*/
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { addButton, removeButton } from "@api/MessagePopover";
import { definePluginSettings } from "@api/Settings";
import { CodeBlock } from "@components/CodeBlock";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
import { getCurrentGuild, getIntlMessage } from "@utils/discord";
import { getIntlMessage } from "@utils/discord";
import { Margins } from "@utils/margins";
import { copyWithToast } from "@utils/misc";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { Button, ChannelStore, Forms, GuildStore, Menu, Text } from "@webpack/common";
import { Button, ChannelStore, Forms, Menu, Text } from "@webpack/common";
import { Message } from "discord-types/general";
@ -118,7 +119,7 @@ const settings = definePluginSettings({
}
});
function MakeContextCallback(name: "Guild" | "Role" | "User" | "Channel"): NavContextMenuPatchCallback {
function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenuPatchCallback {
return (children, props) => {
const value = props[name.toLowerCase()];
if (!value) return;
@ -144,71 +145,58 @@ function MakeContextCallback(name: "Guild" | "Role" | "User" | "Channel"): NavCo
};
}
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({
name: "ViewRaw",
description: "Copy and view the raw content/data of any message, channel or guild",
authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna],
dependencies: ["MessagePopoverAPI"],
settings,
contextMenus: {
"guild-context": MakeContextCallback("Guild"),
"guild-settings-role-context": MakeContextCallback("Role"),
"channel-context": MakeContextCallback("Channel"),
"thread-context": MakeContextCallback("Channel"),
"gdm-context": MakeContextCallback("Channel"),
"user-context": MakeContextCallback("User"),
"dev-context": devContextCallback
"user-context": MakeContextCallback("User")
},
renderMessagePopoverButton(msg) {
const handleClick = () => {
if (settings.store.clickMethod === "Right") {
copyWithToast(msg.content);
} else {
openViewRawModalMessage(msg);
}
};
start() {
addButton("ViewRaw", msg => {
const handleClick = () => {
if (settings.store.clickMethod === "Right") {
copyWithToast(msg.content);
} else {
openViewRawModalMessage(msg);
}
};
const handleContextMenu = e => {
if (settings.store.clickMethod === "Left") {
e.preventDefault();
e.stopPropagation();
copyWithToast(msg.content);
} else {
e.preventDefault();
e.stopPropagation();
openViewRawModalMessage(msg);
}
};
const handleContextMenu = e => {
if (settings.store.clickMethod === "Left") {
e.preventDefault();
e.stopPropagation();
copyWithToast(msg.content);
} else {
e.preventDefault();
e.stopPropagation();
openViewRawModalMessage(msg);
}
};
const label = settings.store.clickMethod === "Right"
? "Copy Raw (Left Click) / View Raw (Right Click)"
: "View Raw (Left Click) / Copy Raw (Right Click)";
const label = settings.store.clickMethod === "Right"
? "Copy Raw (Left Click) / View Raw (Right Click)"
: "View Raw (Left Click) / Copy Raw (Right Click)";
return {
label,
icon: CopyIcon,
message: msg,
channel: ChannelStore.getChannel(msg.channel_id),
onClick: handleClick,
onContextMenu: handleContextMenu
};
return {
label,
icon: CopyIcon,
message: msg,
channel: ChannelStore.getChannel(msg.channel_id),
onClick: handleClick,
onContextMenu: handleContextMenu
};
});
},
stop() {
removeButton("ViewRaw");
}
});

View file

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

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