Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
thororen1234 2025-01-22 21:20:51 -05:00
commit acf04bb01d
49 changed files with 944 additions and 868 deletions

View file

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

View file

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

View file

@ -74,9 +74,9 @@ export interface ChatBarProps {
};
}
export type ChatBarButton = (props: ChatBarProps & { isMainChat: boolean; }) => JSX.Element | null;
export type ChatBarButtonFactory = (props: ChatBarProps & { isMainChat: boolean; }) => JSX.Element | null;
const buttonFactories = new Map<string, ChatBarButton>();
const buttonFactories = new Map<string, ChatBarButtonFactory>();
const logger = new Logger("ChatButtons");
export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) {
@ -91,7 +91,7 @@ export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) {
}
}
export const addChatBarButton = (id: string, button: ChatBarButton) => buttonFactories.set(id, button);
export const addChatBarButton = (id: string, button: ChatBarButtonFactory) => buttonFactories.set(id, button);
export const removeChatBarButton = (id: string) => buttonFactories.delete(id);
export interface ChatBarButtonProps {

View file

@ -16,6 +16,7 @@
* 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";
@ -39,27 +40,32 @@ interface DecoratorProps {
user: User;
[key: string]: any;
}
export type Decorator = (props: DecoratorProps) => JSX.Element | null;
export type MemberListDecoratorFactory = (props: DecoratorProps) => JSX.Element | null;
type OnlyIn = "guilds" | "dms";
export const decorators = new Map<string, { decorator: Decorator, onlyIn?: OnlyIn; }>();
export const decorators = new Map<string, { render: MemberListDecoratorFactory, onlyIn?: OnlyIn; }>();
export function addDecorator(identifier: string, decorator: Decorator, onlyIn?: OnlyIn) {
decorators.set(identifier, { decorator, onlyIn });
export function addMemberListDecorator(identifier: string, render: MemberListDecoratorFactory, onlyIn?: OnlyIn) {
decorators.set(identifier, { render, onlyIn });
}
export function removeDecorator(identifier: string) {
export function removeMemberListDecorator(identifier: string) {
decorators.delete(identifier);
}
export function __getDecorators(props: DecoratorProps): (JSX.Element | null)[] {
const isInGuild = !!(props.guildId);
return Array.from(decorators.values(), decoratorObj => {
const { decorator, onlyIn } = decoratorObj;
// this can most likely be done cleaner
if (!onlyIn || (onlyIn === "guilds" && isInGuild) || (onlyIn === "dms" && !isInGuild)) {
return decorator(props);
}
return Array.from(
decorators.entries(),
([key, { render: Decorator, onlyIn }]) => {
if ((onlyIn === "guilds" && !isInGuild) || (onlyIn === "dms" && isInGuild))
return null;
});
return (
<ErrorBoundary noop key={key} message={`Failed to render ${key} Member List Decorator`}>
<Decorator {...props} />
</ErrorBoundary>
);
}
);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -81,7 +81,8 @@ const Components: Record<OptionType, React.ComponentType<ISettingElementProps<an
[OptionType.BOOLEAN]: SettingBooleanComponent,
[OptionType.SELECT]: SettingSelectComponent,
[OptionType.SLIDER]: SettingSliderComponent,
[OptionType.COMPONENT]: SettingCustomComponent
[OptionType.COMPONENT]: SettingCustomComponent,
[OptionType.CUSTOM]: () => null,
};
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
@ -129,7 +130,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
for (const [key, value] of Object.entries(tempSettings)) {
const option = plugin.options[key];
pluginSettings[key] = value;
option?.onChange?.(value);
if (option.type === OptionType.CUSTOM) continue;
if (option?.restartNeeded) restartNeeded = true;
}
if (plugin.afterSave) {
@ -148,7 +150,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
} else {
const options = Object.entries(plugin.options).map(([key, setting]) => {
if (setting.hidden) return null;
if (setting.type === OptionType.CUSTOM || setting.hidden) return null;
function onChange(newValue: any) {
setTempSettings(s => ({ ...s, [key]: newValue }));

View file

@ -118,8 +118,8 @@ export default definePlugin({
},
async start() {
Vencord.Api.Badges.addBadge(ContributorBadge);
Vencord.Api.Badges.addBadge(EquicordContributorBadge);
Vencord.Api.Badges.addProfileBadge(ContributorBadge);
Vencord.Api.Badges.addProfileBadge(EquicordContributorBadge);
await loadAllBadges();
},

View file

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addAccessory } from "@api/MessageAccessories";
import { definePluginSettings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary";
@ -143,7 +142,7 @@ export default definePlugin({
required: true,
description: "Helps us provide support to you",
authors: [Devs.Ven],
dependencies: ["UserSettingsAPI", "MessageAccessoriesAPI"],
dependencies: ["UserSettingsAPI"],
settings,
@ -256,23 +255,7 @@ export default definePlugin({
}
},
renderContributorDmWarningCard: ErrorBoundary.wrap(({ channel }) => {
const userId = channel.getRecipientId();
if (!isPluginDev(userId) || !isEquicordPluginDev(userId)) return null;
if (RelationshipStore.isFriend(userId) || isPluginDev(UserStore.getCurrentUser()?.id) || isEquicordPluginDev(UserStore.getCurrentUser()?.id)) return null;
return (
<Card className={`vc-plugins-restart-card ${Margins.top8}`}>
Please do not private message plugin developers for support!
<br />
Instead, use the support channel: {Parser.parse("https://discord.com/channels/1173279886065029291/1173342942858055721")}
{!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"}
</Card>
);
}, { noop: true }),
start() {
addAccessory("equicord-debug", props => {
renderMessageAccessory(props) {
const buttons = [] as JSX.Element[];
const shouldAddUpdateButton =
@ -306,19 +289,19 @@ export default definePlugin({
}
if (props.channel.id === SUPPORT_CHANNEL_ID) {
if (props.message.content.includes("/equicord-debug") || props.message.content.includes("/equicord-plugins")) {
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 /equicord-debug
Run /vencord-debug
</Button>,
<Button
key="vc-plg-list"
onClick={async () => sendMessage(props.channel.id, { content: generatePluginList() })}
>
Run /equicord-plugins
Run /vencord-plugins
</Button>
);
}
@ -349,6 +332,20 @@ export default definePlugin({
return buttons.length
? <Flex>{buttons}</Flex>
: null;
});
},
renderContributorDmWarningCard: ErrorBoundary.wrap(({ channel }) => {
const userId = channel.getRecipientId();
if (!isPluginDev(userId) || !isEquicordPluginDev(userId)) return null;
if (RelationshipStore.isFriend(userId) || isPluginDev(UserStore.getCurrentUser()?.id) || isEquicordPluginDev(UserStore.getCurrentUser()?.id)) return null;
return (
<Card className={`vc-plugins-restart-card ${Margins.top8}`}>
Please do not private message plugin developers for support!
<br />
Instead, use the support channel: {Parser.parse("https://discord.com/channels/1173279886065029291/1173342942858055721")}
{!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"}
</Card>
);
}, { noop: true }),
});

View file

@ -23,7 +23,7 @@ const UserProfile = findComponentByCodeLazy("UserProfilePopoutWrapper: user cann
const styles = findByPropsLazy("accountProfilePopoutWrapper");
let openAlternatePopout = false;
let accountPanelRef: React.MutableRefObject<Record<PropertyKey, any> | null> = { current: null };
let accountPanelRef: React.RefObject<Record<PropertyKey, any> | null> = { current: null };
const AccountPanelContextMenu = ErrorBoundary.wrap(() => {
const { prioritizeServerProfile } = settings.use(["prioritizeServerProfile"]);

View file

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

View file

@ -17,11 +17,7 @@
*/
import {
addPreEditListener,
addPreSendListener,
MessageObject,
removePreEditListener,
removePreSendListener
MessageObject
} from "@api/MessageEvents";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -36,7 +32,18 @@ export default definePlugin({
name: "ClearURLs",
description: "Removes tracking garbage from URLs",
authors: [Devs.adryd],
dependencies: ["MessageEventsAPI"],
start() {
this.createRules();
},
onBeforeMessageSend(_, msg) {
return this.onSend(msg);
},
onBeforeMessageEdit(_cid, _mid, msg) {
return this.onSend(msg);
},
escapeRegExp(str: string) {
return (str && reHasRegExpChar.test(str))
@ -133,17 +140,4 @@ export default definePlugin({
);
}
},
start() {
this.createRules();
this.preSend = addPreSendListener((_, msg) => this.onSend(msg));
this.preEdit = addPreEditListener((_cid, _mid, msg) =>
this.onSend(msg)
);
},
stop() {
removePreSendListener(this.preSend);
removePreEditListener(this.preEdit);
},
});

View file

@ -31,7 +31,7 @@ import { findByCodeLazy, findComponentByCodeLazy } from "@webpack";
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, React, UserStore } from "@webpack/common";
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
const ActivityView = findComponentByCodeLazy('location:"UserProfileActivityCard",');
const ActivityView = findComponentByCodeLazy(".party?(0", ".card");
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
@ -399,6 +399,17 @@ export default definePlugin({
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();
@ -449,7 +460,7 @@ export default definePlugin({
{activity[0] && <ActivityView
activity={activity[0]}
user={UserStore.getCurrentUser()}
currentUser={{ id: "0" }}
currentUser={UserStore.getCurrentUser()}
/>}
</div>
</>

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
import { addMessagePreEditListener, addMessagePreSendListener, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { definePluginSettings, Settings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies";
@ -399,7 +399,7 @@ export default definePlugin({
},
// Separate patch for allowing using custom app icons
{
find: /\.getCurrentDesktopIcon.{0,25}\.isPremium/,
find: "?24:30,",
replacement: {
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
replace: "true"
@ -858,7 +858,7 @@ export default definePlugin({
});
}
this.preSend = addPreSendListener(async (channelId, messageObj, extra) => {
this.preSend = addMessagePreSendListener(async (channelId, messageObj, extra) => {
const { guildId } = this;
let hasBypass = false;
@ -949,7 +949,7 @@ export default definePlugin({
return { cancel: false };
});
this.preEdit = addPreEditListener(async (channelId, __, messageObj) => {
this.preEdit = addMessagePreEditListener(async (channelId, __, messageObj) => {
if (!s.enableEmojiBypass) return;
let hasBypass = false;
@ -981,7 +981,7 @@ export default definePlugin({
},
stop() {
removePreSendListener(this.preSend);
removePreEditListener(this.preEdit);
removeMessagePreSendListener(this.preSend);
removeMessagePreEditListener(this.preEdit);
}
});

View file

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

View file

@ -17,7 +17,6 @@
*/
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";
@ -38,17 +37,8 @@ export default definePlugin({
name: "HideAttachments",
description: "Hide attachments and Embeds for individual messages via hover button",
authors: [Devs.Ven],
dependencies: ["MessagePopoverAPI"],
async start() {
style = document.createElement("style");
style.id = "VencordHideAttachments";
document.head.appendChild(style);
await getHiddenMessages();
await this.buildCss();
addButton("HideAttachments", msg => {
renderMessagePopoverButton(msg) {
if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length) return null;
const isHidden = hiddenMessages.has(msg.id);
@ -60,13 +50,20 @@ export default definePlugin({
channel: ChannelStore.getChannel(msg.channel_id),
onClick: () => this.toggleHide(msg.id)
};
});
},
async start() {
style = document.createElement("style");
style.id = "VencordHideAttachments";
document.head.appendChild(style);
await getHiddenMessages();
await this.buildCss();
},
stop() {
style.remove();
hiddenMessages.clear();
removeButton("HideAttachments");
},
async buildCss() {

View file

@ -4,7 +4,6 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import * as DataStore from "@api/DataStore";
import { definePluginSettings, Settings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary";
@ -62,7 +61,7 @@ const ToggleIconOff = (activity: IgnoredActivity, fill: string) => ToggleIcon(ac
function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) {
const s = settings.use(["ignoredActivities"]);
const { ignoredActivities = [] } = s;
const { ignoredActivities } = s;
if (ignoredActivities.some(act => act.id === activity.id)) return ToggleIconOff(activity, "var(--status-danger)");
return ToggleIconOn(activity, isPlaying ? "var(--green-300)" : "var(--primary-400)");
@ -71,9 +70,9 @@ function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) {
function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>, activity: IgnoredActivity) {
e.stopPropagation();
const ignoredActivityIndex = getIgnoredActivities().findIndex(act => act.id === activity.id);
if (ignoredActivityIndex === -1) settings.store.ignoredActivities = getIgnoredActivities().concat(activity);
else settings.store.ignoredActivities = getIgnoredActivities().filter((_, index) => index !== ignoredActivityIndex);
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);
recalculateActivities();
}
@ -209,14 +208,13 @@ const settings = definePluginSettings({
description: "Ignore all competing activities (These are normally special game activities)",
default: false,
onChange: recalculateActivities
},
ignoredActivities: {
type: OptionType.CUSTOM,
default: [] as IgnoredActivity[],
onChange: recalculateActivities
}
}).withPrivateSettings<{
ignoredActivities: IgnoredActivity[];
}>();
function getIgnoredActivities() {
return settings.store.ignoredActivities ??= [];
}
});
function isActivityTypeIgnored(type: number, id?: string) {
if (id && settings.store.idsList.includes(id)) {
@ -284,29 +282,14 @@ export default definePlugin({
],
async start() {
// Migrate allowedIds
if (Settings.plugins.IgnoreActivities.allowedIds) {
settings.store.idsList = Settings.plugins.IgnoreActivities.allowedIds;
delete Settings.plugins.IgnoreActivities.allowedIds; // Remove allowedIds
}
const oldIgnoredActivitiesData = await DataStore.get<Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities");
if (oldIgnoredActivitiesData != null) {
settings.store.ignoredActivities = Array.from(oldIgnoredActivitiesData.values())
.map(activity => ({ ...activity, name: "Unknown Name" }));
DataStore.del("IgnoreActivities_ignoredActivities");
}
if (getIgnoredActivities().length !== 0) {
if (settings.store.ignoredActivities.length !== 0) {
const gamesSeen = RunningGameStore.getGamesSeen() as { id?: string; exePath: string; }[];
for (const [index, ignoredActivity] of getIgnoredActivities().entries()) {
for (const [index, ignoredActivity] of settings.store.ignoredActivities.entries()) {
if (ignoredActivity.type !== ActivitiesTypes.Game) continue;
if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) {
getIgnoredActivities().splice(index, 1);
settings.store.ignoredActivities.splice(index, 1);
}
}
}
@ -316,11 +299,11 @@ export default definePlugin({
if (isActivityTypeIgnored(props.type, props.application_id)) return false;
if (props.application_id != null) {
return !getIgnoredActivities().some(activity => activity.id === props.application_id) || (settings.store.listMode === FilterMode.Whitelist && settings.store.idsList.includes(props.application_id));
return !settings.store.ignoredActivities.some(activity => activity.id === props.application_id) || (settings.store.listMode === FilterMode.Whitelist && settings.store.idsList.includes(props.application_id));
} else {
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
if (exePath) {
return !getIgnoredActivities().some(activity => activity.id === exePath);
return !settings.store.ignoredActivities.some(activity => activity.id === exePath);
}
}

View file

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

View file

@ -16,8 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { addButton, removeButton } from "@api/MessagePopover";
import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { updateMessage } from "@api/MessageUpdater";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
@ -54,7 +53,7 @@ function Indicator() {
aria-label="Hidden Message Indicator (InvisibleChat)"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
src="https://github.com/SammCheese/invisible-chat/blob/1c1a1111d5aed4ddd04bb76a8f10d138944e1d5a/src/assets/lock.png"
src="https://github.com/SammCheese/invisible-chat/raw/NewReplugged/src/assets/lock.png"
width={20}
height={20}
style={{ transform: "translateY(4p)", paddingInline: 4 }}
@ -66,17 +65,12 @@ function Indicator() {
}
const settings = definePluginSettings({
savedPasswords: {
type: OptionType.STRING,
default: "password, Password",
description: "Saved Passwords (Seperated with a , )"
}
});
const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
if (!isMainChat) return null;
const generateChatButton: ChatBarButton = () => {
return (
<ChatBarButton tooltip="Encrypt Message"
<ChatBarButton
tooltip="Encrypt Message"
onClick={() => buildEncModal()}
buttonProps={{
@ -97,11 +91,19 @@ const generateChatButton: ChatBarButton = () => {
);
};
const settings = definePluginSettings({
savedPasswords: {
type: OptionType.STRING,
default: "password, Password",
description: "Saved Passwords (Seperated with a , )"
}
});
export default definePlugin({
name: "InvisibleChat",
description: "Encrypt your Messages in a non-suspicious way!",
authors: [Devs.SammCheese],
dependencies: ["MessagePopoverAPI", "ChatInputButtonAPI", "MessageUpdaterAPI"],
dependencies: ["MessageUpdaterAPI"],
reporterTestable: ReporterTestable.Patches,
settings,
@ -124,8 +126,9 @@ export default definePlugin({
async start() {
const { default: StegCloak } = await getStegCloak();
steggo = new StegCloak(true, false);
},
addButton("invDecrypt", message => {
renderMessagePopoverButton(message) {
return this.INV_REGEX.test(message?.content)
? {
label: "Decrypt Message",
@ -142,14 +145,9 @@ export default definePlugin({
}
}
: null;
});
addChatBarButton("invButton", generateChatButton);
},
stop() {
removeButton("invDecrypt");
removeChatBarButton("invButton");
},
renderChatBarButton: ChatBarIcon,
// Gets the Embed of a Link
async getEmbed(url: URL): Promise<Object | {}> {
@ -184,7 +182,6 @@ export default definePlugin({
updateMessage(message.channel_id, message.id, { embeds: message.embeds });
},
chatBarIcon: ErrorBoundary.wrap(generateChatButton, { noop: true }),
popOverIcon: () => <PopOverIcon />,
indicator: ErrorBoundary.wrap(Indicator, { noop: true })
});

View file

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

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccessories";
import { updateMessage } from "@api/MessageUpdater";
import { definePluginSettings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings";
@ -373,7 +373,7 @@ export default definePlugin({
settings,
start() {
addAccessory("messageLinkEmbed", props => {
addMessageAccessory("messageLinkEmbed", props => {
if (!messageLinkRegex.test(props.message.content))
return null;
@ -390,6 +390,6 @@ export default definePlugin({
}, 4 /* just above rich embeds */);
},
stop() {
removeAccessory("messageLinkEmbed");
},
removeMessageAccessory("messageLinkEmbed");
}
});

View file

@ -18,7 +18,7 @@
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage, unregisterCommand } from "@api/Commands";
import * as DataStore from "@api/DataStore";
import { Settings } from "@api/Settings";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -33,23 +33,23 @@ const author = {
interface Tag {
name: string;
message: string;
enabled: boolean;
}
const getTags = () => DataStore.get(DATA_KEY).then<Tag[]>(t => t ?? []);
const getTag = (name: string) => DataStore.get(DATA_KEY).then<Tag | null>((t: Tag[]) => (t ?? []).find((tt: Tag) => tt.name === name) ?? null);
const addTag = async (tag: Tag) => {
const tags = await getTags();
tags.push(tag);
DataStore.set(DATA_KEY, tags);
return tags;
};
const removeTag = async (name: string) => {
let tags = await getTags();
tags = await tags.filter((t: Tag) => t.name !== name);
DataStore.set(DATA_KEY, tags);
return tags;
};
function getTags() {
return settings.store.tagsList;
}
function getTag(name: string) {
return settings.store.tagsList[name] ?? null;
}
function addTag(tag: Tag) {
settings.store.tagsList[tag.name] = tag;
}
function removeTag(name: string) {
delete settings.store.tagsList[name];
}
function createTagCommand(tag: Tag) {
registerCommand({
@ -57,7 +57,7 @@ function createTagCommand(tag: Tag) {
description: tag.name,
inputType: ApplicationCommandInputType.BUILT_IN_TEXT,
execute: async (_, ctx) => {
if (!await getTag(tag.name)) {
if (!getTag(tag.name)) {
sendBotMessage(ctx.channel.id, {
author,
content: `${EMOTE} The tag **${tag.name}** does not exist anymore! Please reload ur Discord to fix :)`
@ -65,8 +65,7 @@ function createTagCommand(tag: Tag) {
return { content: `/${tag.name}` };
}
if (Settings.plugins.MessageTags.clyde) sendBotMessage(ctx.channel.id, {
author,
if (settings.store.clyde) sendBotMessage(ctx.channel.id, {
content: `${EMOTE} The tag **${tag.name}** has been sent!`
});
return { content: tag.message.replaceAll("\\n", "\n") };
@ -75,22 +74,38 @@ function createTagCommand(tag: Tag) {
}, "CustomTags");
}
export default definePlugin({
name: "MessageTags",
description: "Allows you to save messages and to use them with a simple command.",
authors: [Devs.Luna],
options: {
const settings = definePluginSettings({
clyde: {
name: "Clyde message on send",
description: "If enabled, clyde will send you an ephemeral message when a tag was used.",
type: OptionType.BOOLEAN,
default: true
}
},
tagsList: {
type: OptionType.CUSTOM,
default: {} as Record<string, Tag>,
}
});
export default definePlugin({
name: "MessageTags",
description: "Allows you to save messages and to use them with a simple command.",
authors: [Devs.Luna],
settings,
async start() {
for (const tag of await getTags()) createTagCommand(tag);
// TODO: Remove DataStore tags migration once enough time has passed
const oldTags = await DataStore.get<Tag[]>(DATA_KEY);
if (oldTags != null) {
// @ts-ignore
settings.store.tagsList = Object.fromEntries(oldTags.map(oldTag => (delete oldTag.enabled, [oldTag.name, oldTag])));
await DataStore.del(DATA_KEY);
}
const tags = getTags();
for (const tagName in tags) {
createTagCommand(tags[tagName]);
}
},
commands: [
@ -159,7 +174,7 @@ export default definePlugin({
const name: string = findOption(args[0].options, "tag-name", "");
const message: string = findOption(args[0].options, "message", "");
if (await getTag(name))
if (getTag(name))
return sendBotMessage(ctx.channel.id, {
author,
content: `${EMOTE} A Tag with the name **${name}** already exists!`
@ -167,12 +182,11 @@ export default definePlugin({
const tag = {
name: name,
enabled: true,
message: message
};
createTagCommand(tag);
await addTag(tag);
addTag(tag);
sendBotMessage(ctx.channel.id, {
author,
@ -183,14 +197,14 @@ export default definePlugin({
case "delete": {
const name: string = findOption(args[0].options, "tag-name", "");
if (!await getTag(name))
if (!getTag(name))
return sendBotMessage(ctx.channel.id, {
author,
content: `${EMOTE} A Tag with the name **${name}** does not exist!`
});
unregisterCommand(name);
await removeTag(name);
removeTag(name);
sendBotMessage(ctx.channel.id, {
author,
@ -203,10 +217,8 @@ export default definePlugin({
author,
embeds: [
{
// @ts-ignore
title: "All Tags:",
// @ts-ignore
description: (await getTags())
description: Object.values(getTags())
.map(tag => `\`${tag.name}\`: ${tag.message.slice(0, 72).replaceAll("\\n", " ")}${tag.message.length > 72 ? "..." : ""}`)
.join("\n") || `${EMOTE} Woops! There are no tags yet, use \`/tags create\` to create one!`,
// @ts-ignore
@ -219,7 +231,7 @@ export default definePlugin({
}
case "preview": {
const name: string = findOption(args[0].options, "tag-name", "");
const tag = await getTag(name);
const tag = getTag(name);
if (!tag)
return sendBotMessage(ctx.channel.id, {

View file

@ -218,7 +218,7 @@ export default definePlugin({
},
// in the member list
{
find: "#{intl::GUILD_OWNER}",
find: "#{intl::GUILD_OWNER}),children:",
replacement: {
match: /(?<type>\i)=\(null==.{0,100}\.BOT;return null!=(?<user>\i)&&\i\.bot/,
replace: "$<type> = $self.getTag({user: $<user>, channel: arguments[0].channel, origType: $<user>.bot ? 0 : null, location: 'not-chat' }); return typeof $<type> === 'number'"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,8 +16,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ProfileBadge } from "@api/Badges";
import { ChatBarButtonFactory } from "@api/ChatButtons";
import { Command } from "@api/Commands";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { MemberListDecoratorFactory } from "@api/MemberListDecorators";
import { MessageAccessoryFactory } from "@api/MessageAccessories";
import { MessageDecorationFactory } from "@api/MessageDecorations";
import { MessageClickListener, MessageEditListener, MessageSendListener } from "@api/MessageEvents";
import { MessagePopoverButtonFactory } from "@api/MessagePopover";
import { FluxEvents } from "@webpack/types";
import { JSX } from "react";
import { Promisable } from "type-fest";
@ -147,6 +154,20 @@ export interface PluginDef {
toolboxActions?: Record<string, () => void>;
tags?: string[];
userProfileBadge?: ProfileBadge;
onMessageClick?: MessageClickListener;
onBeforeMessageSend?: MessageSendListener;
onBeforeMessageEdit?: MessageEditListener;
renderMessagePopoverButton?: MessagePopoverButtonFactory;
renderMessageAccessory?: MessageAccessoryFactory;
renderMessageDecoration?: MessageDecorationFactory;
renderMemberListDecorator?: MemberListDecoratorFactory;
renderChatBarButton?: ChatBarButtonFactory;
}
export const enum StartAt {
@ -173,6 +194,7 @@ export const enum OptionType {
SELECT,
SLIDER,
COMPONENT,
CUSTOM
}
export type SettingsDefinition = Record<string, PluginSettingDef>;
@ -181,7 +203,7 @@ export type SettingsChecks<D extends SettingsDefinition> = {
(IsDisabled<DefinedSettings<D>> & IsValid<PluginSettingType<D[K]>, DefinedSettings<D>>);
};
export type PluginSettingDef = (
export type PluginSettingDef = (PluginSettingCustomDef & Pick<PluginSettingCommon, "onChange">) | ((
| PluginSettingStringDef
| PluginSettingNumberDef
| PluginSettingBooleanDef
@ -189,7 +211,7 @@ export type PluginSettingDef = (
| PluginSettingSliderDef
| PluginSettingComponentDef
| PluginSettingBigIntDef
) & PluginSettingCommon;
) & PluginSettingCommon);
export interface PluginSettingCommon {
description: string;
@ -243,12 +265,18 @@ export interface PluginSettingSelectDef {
type: OptionType.SELECT;
options: readonly PluginSettingSelectOption[];
}
export interface PluginSettingSelectOption {
label: string;
value: string | number | boolean;
default?: boolean;
}
export interface PluginSettingCustomDef {
type: OptionType.CUSTOM;
default?: any;
}
export interface PluginSettingSliderDef {
type: OptionType.SLIDER;
/**
@ -298,7 +326,9 @@ type PluginSettingType<O extends PluginSettingDef> = O extends PluginSettingStri
O extends PluginSettingSelectDef ? O["options"][number]["value"] :
O extends PluginSettingSliderDef ? number :
O extends PluginSettingComponentDef ? any :
O extends PluginSettingCustomDef ? O extends { default: infer Default; } ? Default : any :
never;
type PluginSettingDefaultType<O extends PluginSettingDef> = O extends PluginSettingSelectDef ? (
O["options"] extends { default?: boolean; }[] ? O["options"][number]["value"] : undefined
) : O extends { default: infer T; } ? T : undefined;
@ -350,13 +380,15 @@ export type PluginOptionsItem =
| PluginOptionBoolean
| PluginOptionSelect
| PluginOptionSlider
| PluginOptionComponent;
| PluginOptionComponent
| PluginOptionCustom;
export type PluginOptionString = PluginSettingStringDef & PluginSettingCommon & IsDisabled & IsValid<string>;
export type PluginOptionNumber = (PluginSettingNumberDef | PluginSettingBigIntDef) & PluginSettingCommon & IsDisabled & IsValid<number | BigInt>;
export type PluginOptionBoolean = PluginSettingBooleanDef & PluginSettingCommon & IsDisabled & IsValid<boolean>;
export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid<PluginSettingSelectOption>;
export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid<number>;
export type PluginOptionComponent = PluginSettingComponentDef & PluginSettingCommon;
export type PluginOptionCustom = PluginSettingCustomDef & Pick<PluginSettingCommon, "onChange">;
export type PluginNative<PluginExports extends Record<string, (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any>> = {
[key in keyof PluginExports]:

View file

@ -16,8 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type { Moment } from "moment";
import type { ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, KeyboardEvent, MouseEvent, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react";
import type { ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react";
import { IconNames } from "./iconNames";
@ -472,15 +471,9 @@ export type ScrollerThin = ComponentType<PropsWithChildren<{
onScroll?(): void;
}>>;
export type Clickable = ComponentType<PropsWithChildren<{
className?: string;
href?: string;
ignoreKeyPress?: boolean;
onClick?(e: React.MouseEvent<any>): void;
onKeyPress?(): void;
}>>;
export type Clickable = <T extends "a" | "div" | "span" | "li" = "div">(props: PropsWithChildren<ComponentPropsWithRef<T>> & {
tag?: T;
}) => ReactNode;
export type Avatar = ComponentType<PropsWithChildren<{
className?: string;