Merge branch 'dev'

This commit is contained in:
thororen1234 2025-01-27 16:13:36 -05:00
commit 8a52ddde7a
99 changed files with 1280 additions and 1567 deletions

View file

@ -10,7 +10,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
### Extra included plugins
<details>
<summary>149 additional plugins</summary>
<summary>148 additional plugins</summary>
### All Platforms
- AllCallTimers by MaxHerbold & D3SOX
@ -77,7 +77,6 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- ImagePreview by Creations
- ImgToGif by zyqunix
- InRole by nin0dev
- IrcColors by Grzesiek11
- IRememberYou by zoodogood
- Jumpscare by Surgedevs
- JumpToStart by Samwich
@ -186,11 +185,10 @@ MacOS
Linux
- [CLI](https://github.com/Equicord/Equilotl/releases/latest/download/EquilotlCli-Linux)
- [AUR](https://aur.archlinux.org/packages?O=0&K=equibop)
- [AUR](https://aur.archlinux.org/packages?O=0&K=equicord)
```shell
sh -c "$(curl -sS https://raw.githubusercontent.com/Equicord/Equicord/refs/heads/main/misc/install.sh)"
```
## Installing Equicord Devbuild
### Dependencies

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

@ -1,55 +1,50 @@
#!/bin/sh
# Exit immediately if a command exits with a non-zero status
set -e
# Check if the script is run as root
# Constants
INSTALLER_PATH="$HOME/.equilotl"
GITHUB_URL="https://github.com/Equicord/Equilotl/releases/latest/download/EquilotlCli-Linux"
# Check for root
if [ "$(id -u)" -eq 0 ]; then
echo "Run this script as a normal user, not root!"
echo "Run me as a normal user, not root!"
exit 1
fi
# Define variables
installer_path="$HOME/.equilotl"
github_url="https://github.com/Equicord/Equilotl/releases/latest/download/EquilotlCli-Linux"
download_installer() {
curl -sSL "$GITHUB_URL" --output "$INSTALLER_PATH"
chmod +x "$INSTALLER_PATH"
}
# Inform user about the update check
echo "Checking if the installer needs updating..."
# Fetch the latest modified date from the server
latest_modified=$(curl -sI "$github_url" | grep -i "last-modified" | cut -d' ' -f2-)
if [ -f "$INSTALLER_PATH" ]; then
latest_modified=$(curl -sI "$GITHUB_URL" | grep -i "last-modified" | cut -d' ' -f2-)
local_modified=$(stat -c "%y" "$INSTALLER_PATH" | cut -d' ' -f1-2)
# Check if the installer exists locally
if [ -f "$installer_path" ]; then
# Get the local file's last modified date
local_modified=$(stat -c "%y" "$installer_path" | cut -d' ' -f1-2)
# Compare the local file's date with the server's latest modified date
if [ "$local_modified" = "$latest_modified" ]; then
echo "The installer is up-to-date."
else
echo "The installer is outdated. Downloading the latest version..."
curl -sSL "$github_url" --output "$installer_path"
chmod +x "$installer_path"
download_installer
fi
else
echo "Installer not found. Downloading it..."
curl -sSL "$github_url" --output "$installer_path"
chmod +x "$installer_path"
download_installer
fi
# Check for sudo or doas availability to run the installer with elevated privileges
# Try to run the installer with sudo or doas
if command -v sudo >/dev/null; then
echo "Running installer with sudo..."
sudo "$installer_path"
sudo "$INSTALLER_PATH"
elif command -v doas >/dev/null; then
echo "Running installer with doas..."
doas "$installer_path"
doas "$INSTALLER_PATH"
else
echo "Neither sudo nor doas were found. Please install one to proceed."
exit 1
fi
# Provide script attribution
# Credits
echo "Original script forked from Vencord"
echo "Modified by PhoenixAceVFX & Crxaw for Equicord Updater"

View file

@ -122,4 +122,4 @@
"node": ">=18",
"pnpm": ">=9"
}
}
}

View file

@ -296,6 +296,7 @@ page.on("error", e => {
page.on("pageerror", e => {
if (e.message.includes("Sentry successfully disabled")) return;
if (e.message.includes("the network is offline")) return;
if (e.message.includes("Cannot read properties of undefined (reading 'includes')")) return;
if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) {
console.error("[Page Error]", e.message);

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>
);
}
return null;
});
);
}

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

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
import { addMessagePreSendListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -34,13 +34,13 @@ export default definePlugin({
}
],
start() {
this.preSend = addPreSendListener((_, msg) => {
this.preSend = addMessagePreSendListener((_, msg) => {
msg.content = msg.content.replace(/@someone/g, () => `<@${randomUser()}>`);
});
},
stop() {
removePreSendListener(this.preSend);
removeMessagePreSendListener(this.preSend);
}
});

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addButton, removeButton } from "@api/MessagePopover";
import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover";
import { definePluginSettings } from "@api/Settings";
import { CodeBlock } from "@components/CodeBlock";
import ErrorBoundary from "@components/ErrorBoundary";
@ -116,7 +116,7 @@ export default definePlugin({
},
start() {
addButton("DecodeBase64", msg => {
addMessagePopoverButton("DecodeBase64", msg => {
const handleClick = () => {
const base64Strings = findBase64Strings(msg.content);
const decodedContent = decodeBase64Strings(base64Strings);
@ -157,6 +157,6 @@ export default definePlugin({
},
stop() {
removeButton("DecodeBase64");
removeMessagePopoverButton("DecodeBase64");
}
});

View file

@ -8,8 +8,7 @@ import { classNameFactory } from "@api/Styles";
import { getIntlMessage } from "@utils/discord";
import { classes } from "@utils/misc";
import { closeModal, openModal } from "@utils/modal";
import { findByPropsLazy } from "@webpack";
import { Avatar, ChannelStore, ContextMenuApi, FluxDispatcher, GuildStore, Menu, ReadStateStore, ReadStateUtils, Text, Tooltip, useDrag, useDrop, useEffect, useRef, UserStore } from "@webpack/common";
import { Avatar, ChannelStore, ContextMenuApi, FluxDispatcher, GuildStore, Icons, Menu, ReadStateStore, ReadStateUtils, Text, Tooltip, useDrag, useDrop, useEffect, useRef, UserStore } from "@webpack/common";
import { BasicChannelTabsProps, Bookmark, BookmarkFolder, BookmarkProps, CircleQuestionIcon, isBookmarkFolder, settings, switchChannel, useBookmarks } from "../util";
import { NotificationDot } from "./ChannelTab";
@ -17,7 +16,7 @@ import { BookmarkContextMenu, EditModal } from "./ContextMenus";
const cl = classNameFactory("vc-channeltabs-");
const { StarIcon } = findByPropsLazy("StarIcon");
const { StarIcon } = Icons;
function FolderIcon({ fill }: { fill: string; }) {
return (

View file

@ -8,7 +8,7 @@ import { classNameFactory } from "@api/Styles";
import { getIntlMessage, getUniqueUsername } from "@utils/discord";
import { classes } from "@utils/misc";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Avatar, ChannelStore, ContextMenuApi, Dots, GuildStore, PresenceStore, ReadStateStore, Text, TypingStore, useDrag, useDrop, useRef, UserStore, useStateFromStores } from "@webpack/common";
import { Avatar, ChannelStore, ContextMenuApi, Dots, GuildStore, Icons, PresenceStore, ReadStateStore, Text, TypingStore, useDrag, useDrop, useRef, UserStore, useStateFromStores } from "@webpack/common";
import { Channel, Guild, User } from "discord-types/general";
import { ChannelTabsProps, CircleQuestionIcon, closeTab, isTabSelected, moveDraggedTabs, moveToTab, openedTabs, settings } from "../util";
@ -17,7 +17,7 @@ import { TabContextMenu } from "./ContextMenus";
const { getBadgeWidthForValue } = findByPropsLazy("getBadgeWidthForValue");
const dotStyles = findByPropsLazy("numberBadge", "textBadge");
const { FriendsIcon } = findByPropsLazy("FriendsIcon");
const { FriendsIcon } = Icons;
const ChannelTypeIcon = findComponentByCodeLazy(".iconContainerWithGuildIcon,");
const cl = classNameFactory("vc-channeltabs-");

View file

@ -6,8 +6,7 @@
import { classNameFactory } from "@api/Styles";
import { useForceUpdater } from "@utils/react";
import { findByPropsLazy } from "@webpack";
import { Button, ContextMenuApi, Flex, FluxDispatcher, Forms, useCallback, useEffect, useRef, UserStore, useState } from "@webpack/common";
import { Button, ContextMenuApi, Flex, FluxDispatcher, Forms, Icons, useCallback, useEffect, useRef, UserStore, useState } from "@webpack/common";
import { BasicChannelTabsProps, ChannelTabsProps, createTab, handleChannelSwitch, openedTabs, openStartupTabs, saveTabs, settings, setUpdaterFunction, useGhostTabs } from "../util";
import BookmarkContainer from "./BookmarkContainer";
@ -16,7 +15,7 @@ import { BasicContextMenu } from "./ContextMenus";
type TabSet = Record<string, ChannelTabsProps[]>;
const { PlusSmallIcon } = findByPropsLazy("PlusSmallIcon");
const { PlusSmallIcon } = Icons;
const cl = classNameFactory("vc-channeltabs-");
export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {

View file

@ -7,7 +7,7 @@
import { definePluginSettings } from "@api/Settings";
import { Logger } from "@utils/Logger";
import { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Icons } from "@webpack/common";
import { ChannelTabsPreview } from "../components/ChannelTabsContainer";
@ -74,4 +74,4 @@ export const settings = definePluginSettings({
}
});
export const { CircleQuestionIcon } = findByPropsLazy("CircleQuestionIcon");
export const { CircleQuestionIcon } = Icons;

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addPreSendListener } from "@api/MessageEvents";
import { addMessagePreSendListener } from "@api/MessageEvents";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Alerts, ChannelStore, Forms } from "@webpack/common";
@ -41,7 +41,7 @@ export default definePlugin({
authors: [Devs.Samwich],
dependencies: ["MessageEventsAPI"],
start() {
this.preSend = addPreSendListener(async (channelId, messageObj, extra) => {
this.preSend = addMessagePreSendListener(async (channelId, messageObj, extra) => {
if (ChannelStore.getChannel(channelId.toString()).isDM()) return { cancel: false };

View file

@ -4,14 +4,14 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { addChatBarButton, ChatBarButton, ChatBarButtonFactory, removeChatBarButton } from "@api/ChatButtons";
import {
ApplicationCommandInputType,
ApplicationCommandOptionType,
sendBotMessage,
} from "@api/Commands";
import * as DataStore from "@api/DataStore";
import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents";
import { addMessagePreSendListener, MessageSendListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { Devs } from "@utils/constants";
import { sleep } from "@utils/misc";
import definePlugin from "@utils/types";
@ -41,12 +41,12 @@ interface IMessageCreate {
message: Message;
}
const ChatBarIcon: ChatBarButton = ({ isMainChat }) => {
const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
[enabled, setEnabled] = useState(false);
const [buttonDisabled, setButtonDisabled] = useState(false);
useEffect(() => {
const listener: SendListener = async (_, message) => {
const listener: MessageSendListener = async (_, message) => {
if (enabled) {
const groupChannel = await DataStore.get("encryptcordChannelId");
// @ts-expect-error typing issue
@ -72,8 +72,8 @@ const ChatBarIcon: ChatBarButton = ({ isMainChat }) => {
}
};
addPreSendListener(listener);
return () => void removePreSendListener(listener);
addMessagePreSendListener(listener);
return () => void removeMessagePreSendListener(listener);
}, [enabled]);
if (!isMainChat) return null;

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addButton, removeButton } from "@api/MessagePopover";
import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover";
import { disableStyle, enableStyle } from "@api/Styles";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -72,7 +72,7 @@ export default definePlugin({
authors: [Devs.newwares],
start() {
enableStyle(styles);
addButton("vc-findreply", message => {
addMessagePopoverButton("vc-findreply", message => {
if (!message.id) return null;
const replies = findReplies(message);
if (Vencord.Settings.plugins.FindReply.hideButtonIfNoReply && !replies.length) return null;
@ -117,7 +117,7 @@ export default definePlugin({
});
},
stop() {
removeButton("vc-findreply");
removeMessagePopoverButton("vc-findreply");
root && root.unmount();
element?.remove();
disableStyle(styles);

View file

@ -148,10 +148,10 @@ export default definePlugin({
description: "Adds badges showcasing how long you have been friends with a user for",
authors: [Devs.Samwich],
start() {
getBadgesToApply().forEach(thing => Vencord.Api.Badges.addBadge(thing));
getBadgesToApply().forEach(thing => Vencord.Api.Badges.addProfileBadge(thing));
},
stop() {
getBadgesToApply().forEach(thing => Vencord.Api.Badges.removeBadge(thing));
getBadgesToApply().forEach(thing => Vencord.Api.Badges.removeProfileBadge(thing));
},
});

View file

@ -4,10 +4,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { addChatBarButton, ChatBarButton, ChatBarButtonFactory, removeChatBarButton } from "@api/ChatButtons";
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents";
import { addMessagePreSendListener, MessageSendListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { definePluginSettings, Settings } from "@api/Settings";
import { ErrorCard } from "@components/ErrorCard";
import { Link } from "@components/Link";
@ -96,14 +96,14 @@ const furudosettings = definePluginSettings(
function messageSendListenerFuncs() {
if (furudosettings.store.isEnabled) {
addPreSendListener(presendObject);
addMessagePreSendListener(presendObject);
} else {
removePreSendListener(presendObject);
removeMessagePreSendListener(presendObject);
}
}
const FurudoSpeakChatToggle: ChatBarButton = ({ isMainChat }) => {
const FurudoSpeakChatToggle: ChatBarButtonFactory = ({ isMainChat }) => {
const { isEnabled, showIcon } = furudosettings.use(["isEnabled", "showIcon"]);
const toggle = async () => {
const done = await togglefunc();
@ -160,7 +160,7 @@ const ChatBarContextCheckbox: NavContextMenuPatchCallback = children => {
};
const presendObject: SendListener = async (channelId, msg, extra) => {
const presendObject: MessageSendListener = async (channelId, msg, extra) => {
const messageRef = extra.replyOptions.messageReference;
const repliedMessage = ((messageRef?.message_id && messageRef.channel_id) && MessageStore.getMessage(messageRef?.channel_id, messageRef?.message_id)) || undefined;
const apikey = Settings.plugins.FurudoSpeak.apiKey || "";
@ -243,6 +243,6 @@ export default definePlugin({
stop: () => {
removeChatBarButton("FurudoSpeak");
removePreSendListener(presendObject);
removeMessagePreSendListener(presendObject);
}
});

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addBadge, BadgePosition, ProfileBadge, removeBadge } from "@api/Badges";
import { addProfileBadge, BadgePosition, ProfileBadge, removeProfileBadge } from "@api/Badges";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs, EquicordDevs } from "@utils/constants";
@ -137,8 +137,8 @@ export default definePlugin({
description: "Adds global badges from other client mods",
authors: [Devs.HypedDomi, EquicordDevs.Wolfie],
start: () => addBadge(Badge),
stop: () => removeBadge(Badge),
start: () => addProfileBadge(Badge),
stop: () => removeProfileBadge(Badge),
options: {
showPrefix: {

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
import { addMessagePreSendListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -77,13 +77,13 @@ export default definePlugin({
}
}),
async start() {
this.preSend = addPreSendListener((_channelId, msg) => {
this.preSend = addMessagePreSendListener((_channelId, msg) => {
const newContent = this.replaceBadVerbs(this.replaceBadNouns(msg.content));
msg.content = newContent;
});
},
stop() {
removePreSendListener(this.preSend);
removeMessagePreSendListener(this.preSend);
},
getEnabledBadNouns() {
const thingToReturn: string[] = [];

View file

@ -8,7 +8,7 @@ import "./styles.css";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { get, set } from "@api/DataStore";
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccessories";
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { EquicordDevs } from "@utils/constants";
@ -147,7 +147,7 @@ export default definePlugin({
buildCss();
}
addAccessory("vc-hide-message", ({ message }) => {
addMessageAccessory("vc-hide-message", ({ message }) => {
if (hiddenMessages.has(message.id) && settings.store.showNotice) return <HideMessageAccessory id={message.id} />;
return null;
});
@ -156,7 +156,7 @@ export default definePlugin({
async stop() {
for (const id of hiddenMessages.keys()) revealMessage(id);
removeAccessory("vc-hide-message");
removeMessageAccessory("vc-hide-message");
style.remove();
hiddenMessages.clear();

View file

@ -20,7 +20,7 @@ import "./style.css";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { DataStore } from "@api/index";
import { addButton, removeButton } from "@api/MessagePopover";
import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover";
import ErrorBoundary from "@components/ErrorBoundary";
import { EquicordDevs } from "@utils/constants";
import { classes } from "@utils/misc";
@ -114,7 +114,7 @@ export default definePlugin({
if (await DataStore.keys(HolyNoteStore).then(keys => !keys.includes("Main"))) return noteHandler.newNoteBook("Main");
if (!noteHandlerCache.has("Main")) await DataStoreToCache();
addButton("HolyNotes", message => {
addMessagePopoverButton("HolyNotes", message => {
return {
label: "Save Note",
icon: NoteButtonPopover,
@ -127,6 +127,6 @@ export default definePlugin({
},
async stop() {
removeButton("HolyNotes");
removeMessagePopoverButton("HolyNotes");
}
});

View file

@ -18,7 +18,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addButton, removeButton } from "@api/MessagePopover";
import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { classes } from "@utils/misc";
@ -103,7 +103,7 @@ export default definePlugin({
return id !== "" ? id : this.settings.store.emojiID;
},
async start() {
addButton("Husk", msg => {
addMessagePopoverButton("Husk", msg => {
return {
label: "Husk",
icon: Husk,
@ -121,7 +121,7 @@ export default definePlugin({
},
stop() {
removeButton("Husk");
removeMessagePopoverButton("Husk");
},
});

View file

@ -7,7 +7,7 @@
import "styles.css?managed";
import { DataStore } from "@api/index";
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
import { addMessagePreSendListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { Heart } from "@components/Heart";
import { EquicordDevs } from "@utils/constants";
import { openUserProfile } from "@utils/discord";
@ -364,7 +364,7 @@ export default definePlugin({
await data.initializeUsersCollection();
data.writeGuildsOwnersToCollection();
data.writeMembersFromUserGuildsToCollection();
data._onMessagePreSend_preSend = addPreSendListener(
data._onMessagePreSend_preSend = addMessagePreSendListener(
data.onMessagePreSend.bind(data)
);
data.storageAutoSaveProtocol();
@ -385,7 +385,7 @@ export default definePlugin({
stop() {
const dataManager = this.dataManager as Data;
removePreSendListener(dataManager._onMessagePreSend_preSend);
removeMessagePreSendListener(dataManager._onMessagePreSend_preSend);
clearInterval(dataManager._storageAutoSaveProtocol_interval);
},
});

View file

@ -328,17 +328,16 @@ export default definePlugin({
},
{
find: ".guildFilter:null",
replacement: {
match: /function (\i)\(\i\){let{message:\i,gotoMessage/,
replace: "$self.renderMsg = $1; $&"
}
},
{
find: ".guildFilter:null",
replacement: {
match: /onClick:\(\)=>(\i\.\i\.deleteRecentMention\((\i)\.id\))/,
replace: "onClick: () => $2._keyword ? $self.deleteKeyword($2.id) : $1"
}
replacement: [
{
match: /function (\i)\(\i\){let{message:\i,gotoMessage/,
replace: "$self.renderMsg = $1; $&"
},
{
match: /onClick:\(\)=>(\i\.\i\.deleteRecentMention\((\i)\.id\))/,
replace: "onClick: () => $2._keyword ? $self.deleteKeyword($2.id) : $1"
}
]
}
],

View file

@ -15,6 +15,7 @@ import { preload, unload } from "./images";
import { cl } from "./ui";
import openQrModal from "./ui/modals/QrModal";
const qrModalOpen = false;
export default definePlugin({
name: "LoginWithQR",
description: "Allows you to login to another device by scanning a login QR code, just like on mobile!",
@ -87,7 +88,7 @@ export default definePlugin({
}
],
qrModalOpen: false,
qrModalOpen,
insertScanQrButton: (button: ReactElement) => (
<div className={cl("settings-btns")}>

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { addChatBarButton, ChatBarButton, ChatBarButtonFactory, removeChatBarButton } from "@api/ChatButtons";
import { Devs } from "@utils/constants";
import { getCurrentChannel, sendMessage } from "@utils/discord";
import definePlugin from "@utils/types";
@ -14,7 +14,7 @@ async function handleButtonClick() {
sendMessage(getCurrentChannel().id, { content: "meow" });
}
const ChatBarIcon: ChatBarButton = () => {
const ChatBarIcon: ChatBarButtonFactory = () => {
return (
<ChatBarButton tooltip="Meow" onClick={handleButtonClick}>
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 576 512"><path fill="currentColor" d="M320 192h17.1c22.1 38.3 63.5 64 110.9 64c11 0 21.8-1.4 32-4v228c0 17.7-14.3 32-32 32s-32-14.3-32-32V339.2L280 448h56c17.7 0 32 14.3 32 32s-14.3 32-32 32H192c-53 0-96-43-96-96V192.5c0-16.1-12-29.8-28-31.8l-7.9-1c-17.5-2.2-30-18.2-27.8-35.7S50.5 94 68 96.2l7.9 1c48 6 84.1 46.8 84.1 95.3v85.3c34.4-51.7 93.2-85.8 160-85.8m160 26.5c-10 3.5-20.8 5.5-32 5.5c-28.4 0-54-12.4-71.6-32c-3.7-4.1-7-8.5-9.9-13.2C357.3 164 352 146.6 352 128V10.7C352 4.8 356.7.1 362.6 0h.2c3.3 0 6.4 1.6 8.4 4.2v.1l12.8 17l27.2 36.3L416 64h64l4.8-6.4L512 21.3l12.8-17v-.1c2-2.6 5.1-4.2 8.4-4.2h.2c5.9.1 10.6 4.8 10.6 10.7V128c0 17.3-4.6 33.6-12.6 47.6c-11.3 19.8-29.6 35.2-51.4 42.9M432 128a16 16 0 1 0-32 0a16 16 0 1 0 32 0m48 16a16 16 0 1 0 0-32a16 16 0 1 0 0 32" /></svg>

View file

@ -32,7 +32,7 @@ export default definePlugin({
patches: [
{
find: "#{intl::STICKER_BUTTON_LABEL}",
find: "ChannelStickerPickerButton",
replacement: [{
match: /(children:\(0,\i\.jsx\)\()([\w.]+?)(,{innerClassName.{10,30}\.stickerButton)/,
replace: (_, head, button, tail) => {
@ -56,7 +56,7 @@ export default definePlugin({
{
find: ".gifts)",
replacement: {
match: /\(null===\(\i=\i\.stickers\).*?(\i)\.push\((\(0,\i\.jsx\))\((.+?),{disabled:\w,type:(\i)},"sticker"\)\)/,
match: /,.{0,5}\(null===\(\w=\w\.stickers\)\|\|void 0.*?(\w)\.push\((\(0,\w\.jsx\))\((.+?),{disabled:\w,type:(\w)},"sticker"\)\)\)/,
replace: (m, _, jsx, compo, type) => {
const c = "arguments[0].type";
return `${m},${c}?.submit?.button&&${_}.push(${jsx}(${compo},{disabled:!${c}?.submit?.button,type:${type},stickersType:"stickers+"},"stickers+"))`;
@ -64,7 +64,7 @@ export default definePlugin({
}
},
{
find: "role:\"tablist\",\"aria-label\":",
find: `role:"tablist","aria-label":`,
replacement: {
match: /role:"tablist",.*?,?"aria-label":.+?\),children:(\[.*?\)\]}\)}\):null,)(.*?closePopout:\w.*?:null)/s,
replace: m => {

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
import { addMessagePreSendListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
@ -13,12 +13,12 @@ export default definePlugin({
authors: [Devs.Samwich],
dependencies: ["MessageEventsAPI"],
start() {
this.preSend = addPreSendListener((channelId, msg) => {
this.preSend = addMessagePreSendListener((channelId, msg) => {
msg.content = textProcessing(msg.content);
});
},
stop() {
this.preSend = removePreSendListener((channelId, msg) => {
this.preSend = removeMessagePreSendListener((channelId, msg) => {
msg.content = textProcessing(msg.content);
});
},

View file

@ -5,9 +5,9 @@
*/
import {
addPreSendListener,
removePreSendListener,
SendListener,
addMessagePreSendListener,
MessageSendListener,
removeMessagePreSendListener,
} from "@api/MessageEvents";
import {
definePluginSettings,
@ -17,7 +17,7 @@ import {
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
const presendObject: SendListener = (channelId, msg) => {
const presendObject: MessageSendListener = (channelId, msg) => {
msg.content = textProcessing(msg.content);
};
@ -38,10 +38,10 @@ export default definePlugin({
authors: [Devs.Samwich],
dependencies: ["MessageEventsAPI"],
start() {
addPreSendListener(presendObject);
addMessagePreSendListener(presendObject);
},
stop() {
removePreSendListener(presendObject);
removeMessagePreSendListener(presendObject);
},
settings,
});

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
import { addMessagePreSendListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -36,12 +36,12 @@ export default definePlugin({
settings,
start() {
this.preSend = addPreSendListener((_, msg) => {
this.preSend = addMessagePreSendListener((_, msg) => {
msg.content = replaceQuestionMarks(msg.content);
});
},
stop() {
removePreSendListener(this.preSend);
removeMessagePreSendListener(this.preSend);
}
});

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addPreSendListener, MessageExtra, MessageObject, removePreSendListener } from "@api/MessageEvents";
import { addMessagePreSendListener, MessageExtra, MessageObject, removeMessagePreSendListener } from "@api/MessageEvents";
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -14,6 +14,6 @@ export default definePlugin({
name: "RemixMe",
description: "Turns every single message with attachment to have remix tag",
authors: [EquicordDevs.kvba],
start: () => addPreSendListener(handleMessage),
stop: () => removePreSendListener(handleMessage)
start: () => addMessagePreSendListener(handleMessage),
stop: () => removeMessagePreSendListener(handleMessage)
});

View file

@ -17,7 +17,7 @@
*/
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { addButton, removeButton } from "@api/MessagePopover";
import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover";
import { classNameFactory } from "@api/Styles";
import { EquicordDevs } from "@utils/constants";
import { sendMessage } from "@utils/discord";
@ -103,7 +103,7 @@ export default definePlugin({
"message": messageCtxPatch
},
start() {
addButton("vc-repeat", message => {
addMessagePopoverButton("vc-repeat", message => {
if (!message.content && message.stickerItems.length === 0) return null;
return {
@ -119,7 +119,7 @@ export default definePlugin({
document.addEventListener("keydown", keydownListener);
},
stop() {
removeButton("vc-repeat");
removeMessagePopoverButton("vc-repeat");
document.removeEventListener("keyup", keyupListener);
document.removeEventListener("keydown", keydownListener);

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { addChatBarButton, ChatBarButton, ChatBarButtonFactory, removeChatBarButton } from "@api/ChatButtons";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { openModal } from "@utils/modal";
@ -21,7 +21,7 @@ const settings = definePluginSettings({
}
});
const SekaiStickerChatButton: ChatBarButton = () => {
const SekaiStickerChatButton: ChatBarButtonFactory = () => {
return (
<ChatBarButton onClick={() => openModal(props => <SekaiStickersModal modalProps={props} settings={settings} />)} tooltip="Sekai Stickers">
{kanadeSvg()}

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents";
import { addMessagePreSendListener, MessageSendListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { definePluginSettings, Settings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -12,7 +12,7 @@ import { MessageStore } from "@webpack/common";
import { transferMessage } from "./native";
const presendObject: SendListener = async (channelId, msg, extra) => {
const presendObject: MessageSendListener = async (channelId, msg, extra) => {
const messageRef = extra.replyOptions.messageReference;
const repliedMessage = ((messageRef?.message_id && messageRef.channel_id) && MessageStore.getMessage(messageRef?.channel_id, messageRef?.message_id)) || undefined;
msg.content = await transferMessage(msg, Settings.plugins.Shakespearean.model, repliedMessage);
@ -32,9 +32,9 @@ export default definePlugin({
}
}),
start() {
addPreSendListener(presendObject);
addMessagePreSendListener(presendObject);
},
stop() {
removePreSendListener(presendObject);
removeMessagePreSendListener(presendObject);
}
});

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addDecoration, removeDecoration } from "@api/MessageDecorations";
import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations";
import { Devs, EquicordDevs } from "@utils/constants";
import { isEquicordPluginDev, isPluginDev } from "@utils/misc";
import definePlugin from "@utils/types";
@ -145,9 +145,9 @@ export default definePlugin({
dependencies: ["MessageDecorationsAPI"],
settings,
start: () => {
addDecoration("vc-show-badges-in-chat", props => props.message?.author ? <ChatBadges author={props.message.author} /> : null);
addMessageDecoration("vc-show-badges-in-chat", props => props.message?.author ? <ChatBadges author={props.message.author} /> : null);
},
stop: () => {
removeDecoration("vc-show-badges-in-chat");
removeMessageDecoration("vc-show-badges-in-chat");
}
});

View file

@ -4,10 +4,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { addChatBarButton, ChatBarButton, ChatBarButtonFactory, removeChatBarButton } from "@api/ChatButtons";
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
import { addMessagePreSendListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -40,7 +40,7 @@ const settings = definePluginSettings(
},
});
const SignatureToggle: ChatBarButton = ({ isMainChat }) => {
const SignatureToggle: ChatBarButtonFactory = ({ isMainChat }) => {
const { isEnabled, showIcon } = settings.use(["isEnabled", "showIcon"]);
const toggle = () => settings.store.isEnabled = !settings.store.isEnabled;
@ -104,12 +104,12 @@ export default definePlugin({
start: () => {
if (settings.store.isEnabled) true;
addChatBarButton("Signature", SignatureToggle);
addPreSendListener(handleMessage);
addMessagePreSendListener(handleMessage);
},
stop: () => {
if (settings.store.isEnabled) false;
removeChatBarButton("Signature");
removePreSendListener(handleMessage);
removeMessagePreSendListener(handleMessage);
},

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ChatBarButton } from "@api/ChatButtons";
import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { classes } from "@utils/misc";
import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common";
@ -58,7 +58,7 @@ export function IconWithTooltip({ text, icon, onClick }) {
</Tooltip>;
}
export const ChatBarIcon: ChatBarButton = () => {
export const ChatBarIcon: ChatBarButtonFactory = () => {
return (
<ChatBarButton tooltip="Open SoundBoard Log"
onClick={openSoundBoardLog}>

View file

@ -16,15 +16,15 @@
* 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 { addChatBarButton, ChatBarButton, ChatBarButtonFactory, removeChatBarButton } from "@api/ChatButtons";
import { addMessagePreSendListener, MessageSendListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { React, useEffect, useState } from "@webpack/common";
let lastState = false;
const ReverseMessageToggle: ChatBarButton = ({ isMainChat }) => {
const ReverseMessageToggle: ChatBarButtonFactory = ({ isMainChat }) => {
const [enabled, setEnabled] = useState(lastState);
function setEnabledValue(value: boolean) {
@ -34,12 +34,12 @@ const ReverseMessageToggle: ChatBarButton = ({ isMainChat }) => {
}
useEffect(() => {
const listener: SendListener = async (_, message) => {
const listener: MessageSendListener = async (_, message) => {
if (enabled && message.content) message.content = message.content.split("").reverse().join("");
};
addPreSendListener(listener);
return () => void removePreSendListener(listener);
addMessagePreSendListener(listener);
return () => void removeMessagePreSendListener(listener);
}, [enabled]);
if (!isMainChat) return null;

View file

@ -19,8 +19,8 @@
import "./style.css";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
import { addButton, removeButton } from "@api/MessagePopover";
import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccessories";
import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { ChannelStore, Menu } from "@webpack/common";
@ -56,9 +56,9 @@ export default definePlugin({
},
start() {
addAccessory("ec-translation", props => <Accessory message={props.message} />);
addMessageAccessory("ec-translation", props => <Accessory message={props.message} />);
addButton("ec-translate", message => {
addMessagePopoverButton("ec-translate", message => {
if (!message.content) return null;
return {
@ -71,7 +71,7 @@ export default definePlugin({
});
},
stop() {
removeButton("ec-translate");
removeAccessory("ec-translation");
removeMessagePopoverButton("ec-translate");
removeMessageAccessory("ec-translation");
}
});

View file

@ -18,8 +18,8 @@
import "./style.css";
import { addAccessory } from "@api/MessageAccessories";
import { addButton } from "@api/MessagePopover";
import { addMessageAccessory } from "@api/MessageAccessories";
import { addMessagePopoverButton } from "@api/MessagePopover";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -57,8 +57,8 @@ export default definePlugin({
description: "Converts metric units to Imperal units and vice versa",
authors: [Devs.sadan],
start() {
addAccessory("vc-converter", props => <ConverterAccessory message={props.message} />);
addButton("vc-converter", message => {
addMessageAccessory("vc-converter", props => <ConverterAccessory message={props.message} />);
addMessagePopoverButton("vc-converter", message => {
if (!message.content) return null;
return {
label: "Convert Units",

View file

@ -23,18 +23,18 @@ const settings = definePluginSettings({
{ label: "Nitro", value: true, default: true },
],
},
urlForDB: {
databaseToUse: {
type: OptionType.SELECT,
description: "Which Database url to use to load avatars, KNOW WHAT YOUR DOING",
options: [
{
label: "UserPFP Default DB",
label: "UserPFP Main DB",
value: "https://userpfp.github.io/UserPFP/source/data.json",
default: true
},
{
label: "UserPFP Backup DB",
value: "https://userpfp.thororen.com/data.json"
value: "https://userpfp.equicord.fyi/data.json"
}
]
}
@ -74,7 +74,7 @@ export default definePlugin({
return data.avatars[user.id] ?? original(user, animated, size);
},
async start() {
const res = await fetch(settings.store.urlForDB)
await fetch(settings.store.databaseToUse)
.then(async res => {
if (res.ok) this.data = data = await res.json();
})

View file

@ -17,7 +17,7 @@
*/
import { findOption, RequiredMessageOption } from "@api/Commands";
import { addPreEditListener, addPreSendListener, MessageObject, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
import { addMessagePreEditListener, addMessagePreSendListener, MessageObject, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -199,14 +199,14 @@ export default definePlugin({
},
start() {
this.preSend = addPreSendListener((_, msg) => this.onSend(msg));
this.preEdit = addPreEditListener((_cid, _mid, msg) =>
this.preSend = addMessagePreSendListener((_, msg) => this.onSend(msg));
this.preEdit = addMessagePreEditListener((_cid, _mid, msg) =>
this.onSend(msg)
);
},
stop() {
removePreSendListener(this.preSend);
removePreEditListener(this.preEdit);
removeMessagePreSendListener(this.preSend);
removeMessagePreEditListener(this.preEdit);
},
});

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { addChatBarButton, ChatBarButton, ChatBarButtonFactory, removeChatBarButton } from "@api/ChatButtons";
import { Devs } from "@utils/constants";
import { getCurrentChannel, sendMessage } from "@utils/discord";
import definePlugin from "@utils/types";
@ -14,7 +14,7 @@ async function handleButtonClick() {
sendMessage(getCurrentChannel().id, { content: "woof" });
}
const ChatBarIcon: ChatBarButton = () => {
const ChatBarIcon: ChatBarButtonFactory = () => {
return (
<ChatBarButton tooltip="Woof" onClick={handleButtonClick}>
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 576 512"><path fill="currentColor" d="m309.6 158.5l23.1-138.7C334.6 8.4 344.5 0 356.1 0c7.5 0 14.5 3.5 19 9.5L392 32h52.1c12.7 0 24.9 5.1 33.9 14.1L496 64h56c13.3 0 24 10.7 24 24v24c0 44.2-35.8 80-80 80h-69.3l-5.1 30.5zM416 256.1V480c0 17.7-14.3 32-32 32h-32c-17.7 0-32-14.3-32-32V364.8c-24 12.3-51.2 19.2-80 19.2s-56-6.9-80-19.2V480c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V249.8c-28.8-10.9-51.4-35.3-59.2-66.5L1 167.8c-4.3-17.1 6.1-34.5 23.3-38.8s34.5 6.1 38.8 23.3l3.9 15.5C70.5 182 83.3 192 98 192h205.8zM464 80a16 16 0 1 0-32 0a16 16 0 1 0 32 0" /></svg>

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents";
import { addMessagePreSendListener, MessageSendListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { definePluginSettings } from "@api/Settings";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -19,7 +19,7 @@ const settings = definePluginSettings(
}
);
const presendObject: SendListener = (_, msg) => {
const presendObject: MessageSendListener = (_, msg) => {
const sentences = msg.content.split(/(?<=\w\.)\s/);
const blockedWordsArray: string[] = settings.store.blockedWords.split(", ");
@ -39,9 +39,9 @@ export default definePlugin({
settings,
start() {
addPreSendListener(presendObject);
addMessagePreSendListener(presendObject);
},
stop() {
removePreSendListener(presendObject);
removeMessagePreSendListener(presendObject);
}
});

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,6 +255,85 @@ export default definePlugin({
}
},
renderMessageAccessory(props) {
const buttons = [] as JSX.Element[];
const shouldAddUpdateButton =
!IS_UPDATER_DISABLED
&& (
(props.channel.id === KNOWN_ISSUES_CHANNEL_ID) ||
(props.channel.id === SUPPORT_CHANNEL_ID && props.message.author.id === VENBOT_USER_ID)
)
&& props.message.content?.includes("update");
if (shouldAddUpdateButton) {
buttons.push(
<Button
key="vc-update"
color={Button.Colors.GREEN}
onClick={async () => {
try {
if (await forceUpdate())
showToast("Success! Restarting...", Toasts.Type.SUCCESS);
else
showToast("Already up to date!", Toasts.Type.MESSAGE);
} catch (e) {
new Logger(this.name).error("Error while updating:", e);
showToast("Failed to update :(", Toasts.Type.FAILURE);
}
}}
>
Update Now
</Button>
);
}
if (props.channel.id === SUPPORT_CHANNEL_ID) {
if (props.message.content.includes("/vencord-debug") || props.message.content.includes("/vencord-plugins")) {
buttons.push(
<Button
key="vc-dbg"
onClick={async () => sendMessage(props.channel.id, { content: await generateDebugInfoMessage() })}
>
Run /vencord-debug
</Button>,
<Button
key="vc-plg-list"
onClick={async () => sendMessage(props.channel.id, { content: generatePluginList() })}
>
Run /vencord-plugins
</Button>
);
}
if (props.message.author.id === VENBOT_USER_ID) {
const match = CodeBlockRe.exec(props.message.content || props.message.embeds[0]?.rawDescription || "");
if (match) {
buttons.push(
<Button
key="vc-run-snippet"
onClick={async () => {
try {
await AsyncFunction(match[1])();
showToast("Success!", Toasts.Type.SUCCESS);
} catch (e) {
new Logger(this.name).error("Error while running snippet:", e);
showToast("Failed to run snippet :(", Toasts.Type.FAILURE);
}
}}
>
Run Snippet
</Button>
);
}
}
}
return buttons.length
? <Flex>{buttons}</Flex>
: null;
},
renderContributorDmWarningCard: ErrorBoundary.wrap(({ channel }) => {
const userId = channel.getRecipientId();
if (!isPluginDev(userId) || !isEquicordPluginDev(userId)) return null;
@ -270,85 +348,4 @@ export default definePlugin({
</Card>
);
}, { noop: true }),
start() {
addAccessory("equicord-debug", props => {
const buttons = [] as JSX.Element[];
const shouldAddUpdateButton =
!IS_UPDATER_DISABLED
&& (
(props.channel.id === KNOWN_ISSUES_CHANNEL_ID) ||
(props.channel.id === SUPPORT_CHANNEL_ID && props.message.author.id === VENBOT_USER_ID)
)
&& props.message.content?.includes("update");
if (shouldAddUpdateButton) {
buttons.push(
<Button
key="vc-update"
color={Button.Colors.GREEN}
onClick={async () => {
try {
if (await forceUpdate())
showToast("Success! Restarting...", Toasts.Type.SUCCESS);
else
showToast("Already up to date!", Toasts.Type.MESSAGE);
} catch (e) {
new Logger(this.name).error("Error while updating:", e);
showToast("Failed to update :(", Toasts.Type.FAILURE);
}
}}
>
Update Now
</Button>
);
}
if (props.channel.id === SUPPORT_CHANNEL_ID) {
if (props.message.content.includes("/equicord-debug") || props.message.content.includes("/equicord-plugins")) {
buttons.push(
<Button
key="vc-dbg"
onClick={async () => sendMessage(props.channel.id, { content: await generateDebugInfoMessage() })}
>
Run /equicord-debug
</Button>,
<Button
key="vc-plg-list"
onClick={async () => sendMessage(props.channel.id, { content: generatePluginList() })}
>
Run /equicord-plugins
</Button>
);
}
if (props.message.author.id === VENBOT_USER_ID) {
const match = CodeBlockRe.exec(props.message.content || props.message.embeds[0]?.rawDescription || "");
if (match) {
buttons.push(
<Button
key="vc-run-snippet"
onClick={async () => {
try {
await AsyncFunction(match[1])();
showToast("Success!", Toasts.Type.SUCCESS);
} catch (e) {
new Logger(this.name).error("Error while running snippet:", e);
showToast("Failed to run snippet :(", Toasts.Type.FAILURE);
}
}}
>
Run Snippet
</Button>
);
}
}
}
return buttons.length
? <Flex>{buttons}</Flex>
: null;
});
},
});

View file

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

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

View file

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

View file

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

View file

@ -95,10 +95,9 @@ export default definePlugin({
}
},
{
find: 'console.warn("[DEPRECATED] Please use `subscribeWithSelector` middleware");',
all: true,
find: '"AppCrashedFatalReport: getLastCrash not supported."',
replacement: {
match: /console\.warn\("\[DEPRECATED\] Please use `subscribeWithSelector` middleware"\);/,
match: /console\.log\("AppCrashedFatalReport: getLastCrash not supported\."\);/,
replace: ""
}
},

View file

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

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

View file

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

View file

@ -81,7 +81,12 @@ export const settings = definePluginSettings({
});
const imageContextMenuPatch: NavContextMenuPatchCallback = children => {
const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
// Discord re-uses the image context menu for links to for the copy and open buttons
if ("href" in props) return;
// emojis in user statuses
if (props.target?.classList?.contains("emoji")) return;
const { square, nearestNeighbour } = settings.use(["square", "nearestNeighbour"]);
children.push(

View file

@ -16,12 +16,19 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addProfileBadge, removeProfileBadge } from "@api/Badges";
import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { registerCommand, unregisterCommand } from "@api/Commands";
import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
import { Settings } from "@api/Settings";
import { addMemberListDecorator, removeMemberListDecorator } from "@api/MemberListDecorators";
import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccessories";
import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations";
import { addMessageClickListener, addMessagePreEditListener, addMessagePreSendListener, removeMessageClickListener, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover";
import { Settings, SettingsStore } from "@api/Settings";
import { Logger } from "@utils/Logger";
import { canonicalizeFind } from "@utils/patches";
import { Patch, Plugin, ReporterTestable, StartAt } from "@utils/types";
import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
import { FluxEvents } from "@webpack/types";
@ -83,6 +90,13 @@ function isReporterTestable(p: Plugin, part: ReporterTestable) {
: (p.reporterTestable & part) === part;
}
const pluginKeysToBind: Array<keyof PluginDef & `${"on" | "render"}${string}`> = [
"onBeforeMessageEdit", "onBeforeMessageSend", "onMessageClick",
"renderChatBarButton", "renderMemberListDecorator", "renderMessageAccessory", "renderMessageDecoration", "renderMessagePopoverButton"
];
const neededApiPlugins = new Set<string>();
// First round-trip to mark and force enable dependencies
//
// FIXME: might need to revisit this if there's ever nested (dependencies of dependencies) dependencies since this only
@ -106,22 +120,46 @@ for (const p of pluginsValues) if (isPluginEnabled(p.name)) {
dep.isDependency = true;
});
if (p.commands?.length) {
Plugins.CommandsAPI.isDependency = true;
settings.CommandsAPI.enabled = true;
if (p.commands?.length) neededApiPlugins.add("CommandsAPI");
if (p.onBeforeMessageEdit || p.onBeforeMessageSend || p.onMessageClick) neededApiPlugins.add("MessageEventsAPI");
if (p.renderChatBarButton) neededApiPlugins.add("ChatInputButtonAPI");
if (p.renderMemberListDecorator) neededApiPlugins.add("MemberListDecoratorsAPI");
if (p.renderMessageAccessory) neededApiPlugins.add("MessageAccessoriesAPI");
if (p.renderMessageDecoration) neededApiPlugins.add("MessageDecorationsAPI");
if (p.renderMessagePopoverButton) neededApiPlugins.add("MessagePopoverAPI");
if (p.userProfileBadge) neededApiPlugins.add("BadgeAPI");
for (const key of pluginKeysToBind) {
p[key] &&= p[key].bind(p) as any;
}
}
for (const p of neededApiPlugins) {
Plugins[p].isDependency = true;
settings[p].enabled = true;
}
for (const p of pluginsValues) {
if (p.settings) {
p.settings.pluginName = p.name;
p.options ??= {};
for (const [name, def] of Object.entries(p.settings.def)) {
p.settings.pluginName = p.name;
for (const name in p.settings.def) {
const def = p.settings.def[name];
const checks = p.settings.checks?.[name];
p.options[name] = { ...def, ...checks };
}
}
if (p.options) {
for (const name in p.options) {
const opt = p.options[name];
if (opt.onChange != null) {
SettingsStore.addChangeListener(`plugins.${p.name}.${name}`, opt.onChange);
}
}
}
if (p.patches && isPluginEnabled(p.name)) {
if (!IS_REPORTER || isReporterTestable(p, ReporterTestable.Patches)) {
for (const patch of p.patches) {
@ -216,7 +254,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 +292,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 +299,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 +358,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,32 +126,28 @@ export default definePlugin({
async start() {
const { default: StegCloak } = await getStegCloak();
steggo = new StegCloak(true, false);
},
addButton("invDecrypt", message => {
return this.INV_REGEX.test(message?.content)
? {
label: "Decrypt Message",
icon: this.popOverIcon,
message: message,
channel: ChannelStore.getChannel(message.channel_id),
onClick: async () => {
const res = await iteratePasswords(message);
renderMessagePopoverButton(message) {
return this.INV_REGEX.test(message?.content)
? {
label: "Decrypt Message",
icon: this.popOverIcon,
message: message,
channel: ChannelStore.getChannel(message.channel_id),
onClick: async () => {
const res = await iteratePasswords(message);
if (res)
this.buildEmbed(message, res);
else
buildDecModal({ message });
}
if (res)
this.buildEmbed(message, res);
else
buildDecModal({ message });
}
: null;
});
addChatBarButton("invButton", generateChatButton);
}
: null;
},
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

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

View file

@ -17,39 +17,22 @@
*/
import { definePluginSettings } from "@api/Settings";
import { hash as h64 } from "@intrnl/xxhash64";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
// Compute a 64-bit FNV-1a hash of the passed data
function hash(data: ArrayBuffer) {
const fnvPrime = 1099511628211n;
const offsetBasis = 14695981039346656037n;
let result = offsetBasis;
for (const byte of new Uint8Array(data)) {
result ^= BigInt(byte);
result = (result * fnvPrime) % 2n ** 32n;
}
return result;
}
import { useMemo } from "@webpack/common";
// Calculate a CSS color string based on the user ID
function calculateNameColorForUser(id: bigint) {
const idBuffer = new ArrayBuffer(16);
{
const idView = new DataView(idBuffer);
idView.setBigUint64(0, id);
}
const idHash = hash(idBuffer);
function calculateNameColorForUser(id: string) {
const { lightness } = settings.use(["lightness"]);
const idHash = useMemo(() => h64(id), [id]);
return `hsl(${idHash % 360n}, 100%, ${settings.store.lightness}%)`;
return `hsl(${idHash % 360n}, 100%, ${lightness}%)`;
}
const settings = definePluginSettings({
lightness: {
description: "Lightness, in %. Change if the colors are too light or too dark.",
restartNeeded: true,
description: "Lightness, in %. Change if the colors are too light or too dark",
type: OptionType.NUMBER,
default: 70,
},
@ -57,36 +40,46 @@ const settings = definePluginSettings({
description: "Replace role colors in the member list",
restartNeeded: true,
type: OptionType.BOOLEAN,
default: false,
},
default: true
}
});
export default definePlugin({
name: "IrcColors",
description: "Makes username colors in chat unique, like in IRC clients",
authors: [Devs.Grzesiek11],
settings,
patches: [
{
find: "=\"SYSTEM_TAG\"",
find: '="SYSTEM_TAG"',
replacement: {
match: /(?<=className:\i\.username,style:.{0,50}:void 0,)/,
replace: "style:{color:$self.calculateNameColorForMessageContext(arguments[0])},",
},
replace: "style:{color:$self.calculateNameColorForMessageContext(arguments[0])},"
}
},
{
find: ".NameWithRole,{roleName:",
find: "#{intl::GUILD_OWNER}),children:",
replacement: {
match: /(?<=color:)null!=.{0,50}?(?=,)/,
replace: "$self.calculateNameColorForListContext(arguments[0])",
match: /(?<=\.MEMBER_LIST}\),\[\]\),)(.+?color:)null!=.{0,50}?(?=,)/,
replace: (_, rest) => `ircColor=$self.calculateNameColorForListContext(arguments[0]),${rest}ircColor`
},
predicate: () => settings.store.memberListColors,
},
predicate: () => settings.store.memberListColors
}
],
settings,
calculateNameColorForMessageContext(context: any) {
return calculateNameColorForUser(BigInt(context.message.author.id));
const id = context?.message?.author?.id;
if (id == null) {
return null;
}
return calculateNameColorForUser(id);
},
calculateNameColorForListContext(context: any) {
return calculateNameColorForUser(BigInt(context.user.id));
},
const id = context?.user?.id;
if (id == null) {
return null;
}
return calculateNameColorForUser(id);
}
});

View file

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

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

View file

@ -163,7 +163,7 @@ export default definePlugin({
</>
}
</Tooltip>;
});
}, { noop: true });
},
Icon({ delta, fill, props }: {

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { 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

@ -215,9 +215,9 @@ export default definePlugin({
},
collapseDeleted: {
type: OptionType.BOOLEAN,
description:
"Whether to collapse deleted messages, similar to blocked messages",
description: "Whether to collapse deleted messages, similar to blocked messages",
default: false,
restartNeeded: true,
},
logEdits: {
type: OptionType.BOOLEAN,

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");
}
const settings = definePluginSettings({
clyde: {
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>,
description: "",
}
});
export default definePlugin({
name: "MessageTags",
description: "Allows you to save messages and to use them with a simple command.",
authors: [Devs.Luna],
options: {
clyde: {
name: "Clyde message on send",
description: "If enabled, clyde will send you an ephemeral message when a tag was used.",
type: OptionType.BOOLEAN,
default: true
}
},
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

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

View file

@ -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
};
return category;
}
export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Props) {
const { category, setCategory } = useCategory(categoryId, initalChannelId);
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,29 @@ 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[]>,
description: "",
}
});
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 +130,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 +155,7 @@ export default definePlugin({
}
},
],
sections: null as number[] | null,
set _instance(i: any) {
@ -162,6 +169,7 @@ export default definePlugin({
CONNECTION_OPEN: init,
},
usePinnedDms,
isPinned,
categoryLen,
getSections,
@ -186,11 +194,11 @@ export default definePlugin({
},
makeSpanProps() {
return {
return settings.store.canCollapseDmSection ? {
onClick: () => this.collapseDMList(),
role: "button",
style: { cursor: "pointer" }
};
} : undefined;
},
getChunkSize() {
@ -210,30 +218,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 +256,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 +283,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,7 +303,7 @@ export default definePlugin({
id="vc-pindms-delete-category"
color="danger"
label="Delete Category"
action={() => removeCategory(category.id).then(() => forceUpdate())}
action={() => removeCategory(category.id)}
/>
@ -312,13 +311,18 @@ export default definePlugin({
));
}}
>
<span className={headerClasses.headerText}>
{category?.name ?? "uh oh"}
</span>
<svg className="vc-pindms-collapse-icon" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" d="M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z"></path>
</svg>
</h2>
<h2
className={classes(headerClasses.privateChannelsHeaderContainer, "vc-pindms-section-container", category.collapsed ? "vc-pindms-collapsed" : "")}
style={{ color: `#${category.color.toString(16).padStart(6, "0")}` }}
>
<span className={headerClasses.headerText}>
{category?.name ?? "uh oh"}
</span>
<svg className="vc-pindms-collapse-icon" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" d="M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z"></path>
</svg>
</h2>
</Clickable>
);
}, { noop: true }),
@ -341,7 +345,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,24 +25,18 @@ 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 => {
const channel = ChannelStore.getChannel(msg.channel_id);
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return null;
renderMessagePopoverButton(msg) {
const channel = ChannelStore.getChannel(msg.channel_id);
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return null;
return {
label: "Quick Mention",
icon: this.Icon,
message: msg,
channel,
onClick: () => insertTextIntoChatInputBox(`<@${msg.author.id}> `)
};
});
},
stop() {
removeButton("QuickMention");
return {
label: "Quick Mention",
icon: this.Icon,
message: msg,
channel,
onClick: () => insertTextIntoChatInputBox(`<@${msg.author.id}> `)
};
},
Icon: () => (

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) => {
if (settings.store.replaceMessageContents) {
msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime);
}
});
},
renderChatBarButton: ChatBarIcon,
stop() {
removeChatBarButton("SendTimestamps");
removePreSendListener(this.listener);
onBeforeMessageSend(_, msg) {
if (settings.store.replaceMessageContents) {
msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime);
}
},
settingsAboutComponent() {

View file

@ -34,6 +34,7 @@ const enum Tabs {
ServerInfo,
Friends,
BlockedUsers,
IgnoredUsers,
MutualMembers
}
@ -47,7 +48,8 @@ interface RelationshipProps extends GuildProps {
const fetched = {
friends: false,
blocked: false
blocked: false,
ignored: false
};
function renderTimestamp(timestamp: number) {
@ -59,11 +61,13 @@ function renderTimestamp(timestamp: number) {
function GuildInfoModal({ guild }: GuildProps) {
const [friendCount, setFriendCount] = useState<number>();
const [blockedCount, setBlockedCount] = useState<number>();
const [ignoredCount, setIgnoredCount] = useState<number>();
const [mutualMembersCount, setMutualMembersCount] = useState<number>();
useEffect(() => {
fetched.friends = false;
fetched.blocked = false;
fetched.ignored = false;
}, []);
const [currentTab, setCurrentTab] = useState(Tabs.ServerInfo);
@ -122,25 +126,55 @@ function GuildInfoModal({ guild }: GuildProps) {
className={cl("tab", { selected: currentTab === Tabs.ServerInfo })}
id={Tabs.ServerInfo}
>
Server Info
<div style={{ textAlign: "center" }}>
<div>
Server Info
</div>
</div>
</TabBar.Item>
<TabBar.Item
className={cl("tab", { selected: currentTab === Tabs.Friends })}
id={Tabs.Friends}
>
Friends{friendCount !== undefined ? ` (${friendCount})` : ""}
<div style={{ textAlign: "center" }}>
<div>
Friends
</div>
{friendCount !== undefined ? ` (${friendCount})` : ""}
</div>
</TabBar.Item>
<TabBar.Item
className={cl("tab", { selected: currentTab === Tabs.MutualMembers })}
id={Tabs.MutualMembers}
>
Mutual Server Members{mutualMembersCount !== undefined ? ` (${mutualMembersCount})` : ""}
<div style={{ textAlign: "center" }}>
<div>
Mutual Members
</div>{mutualMembersCount !== undefined ? ` (${mutualMembersCount})` : ""}
</div>
</TabBar.Item>
<TabBar.Item
className={cl("tab", { selected: currentTab === Tabs.BlockedUsers })}
id={Tabs.BlockedUsers}
>
Blocked Users{blockedCount !== undefined ? ` (${blockedCount})` : ""}
<div style={{ textAlign: "center" }}>
<div>
Blocked Users
</div>
{blockedCount !== undefined ? ` (${blockedCount})` : ""}
</div>
</TabBar.Item>
<TabBar.Item
className={cl("tab", { selected: currentTab === Tabs.IgnoredUsers })}
id={Tabs.IgnoredUsers}
>
<div style={{ textAlign: "center" }}>
<div>
Ignored Users
</div>
{ignoredCount !== undefined ? `(${ignoredCount})` : ""}
</div>
</TabBar.Item>
</TabBar>
@ -149,6 +183,7 @@ function GuildInfoModal({ guild }: GuildProps) {
{currentTab === Tabs.Friends && <FriendsTab guild={guild} setCount={setFriendCount} />}
{currentTab === Tabs.MutualMembers && <MutualMembersTab guild={guild} setCount={setMutualMembersCount} />}
{currentTab === Tabs.BlockedUsers && <BlockedUsersTab guild={guild} setCount={setBlockedCount} />}
{currentTab === Tabs.IgnoredUsers && <IgnoredUserTab guild={guild} setCount={setIgnoredCount} />}
</div>
</div>
);
@ -222,7 +257,13 @@ function BlockedUsersTab({ guild, setCount }: RelationshipProps) {
return UserList("blocked", guild, blockedIds, setCount);
}
function UserList(type: "friends" | "blocked", guild: Guild, ids: string[], setCount: (count: number) => void) {
function IgnoredUserTab({ guild, setCount }: RelationshipProps) {
const ignoredIds = Object.keys(RelationshipStore.getRelationships()).filter(id => RelationshipStore.isIgnored(id));
return UserList("ignored", guild, ignoredIds, setCount);
}
function UserList(type: "friends" | "blocked" | "ignored", guild: Guild, ids: string[], setCount: (count: number) => void) {
const missing = [] as string[];
const members = [] as string[];

View file

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

View file

@ -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,38 @@ 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(),
description: "",
},
regexRules: {
type: OptionType.CUSTOM,
default: makeEmptyRuleArray(),
description: "",
}
});
function stringToRegex(str: string) {
@ -120,28 +120,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,29 +204,26 @@ function TextReplaceTesting() {
}
function applyRules(content: string): string {
if (content.length === 0)
if (content.length === 0) {
return content;
if (stringRules) {
for (const rule of stringRules) {
if (!rule.find) continue;
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, "");
}
}
if (regexRules) {
for (const rule of regexRules) {
if (!rule.find) continue;
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
for (const rule of settings.store.stringRules) {
if (!rule.find) continue;
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
try {
const regex = stringToRegex(rule.find);
content = content.replace(regex, rule.replace.replaceAll("\\n", "\n"));
} catch (e) {
new Logger("TextReplace").error(`Invalid regex: ${rule.find}`);
}
content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, "");
}
for (const rule of settings.store.regexRules) {
if (!rule.find) continue;
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
try {
const regex = stringToRegex(rule.find);
content = content.replace(regex, rule.replace.replaceAll("\\n", "\n"));
} catch (e) {
new Logger("TextReplace").error(`Invalid regex: ${rule.find}`);
}
}
@ -244,22 +237,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) => {
// Channel used for sharing rules, applying rules here would be messy
if (channelId === TEXT_REPLACE_RULES_CHANNEL_ID) return;
msg.content = applyRules(msg.content);
});
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,45 +60,34 @@ 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 => {
if (!message.content) return null;
renderMessagePopoverButton(message) {
if (!message.content) return null;
return {
label: "Translate",
icon: TranslateIcon,
message,
channel: ChannelStore.getChannel(message.channel_id),
onClick: async () => {
const trans = await translate("received", message.content);
handleTranslate(message.id, trans);
}
};
});
let tooltipTimeout: any;
this.preSend = addPreSendListener(async (_, message) => {
if (!settings.store.autoTranslate) return;
if (!message.content) return;
setShouldShowTranslateEnabledTooltip?.(true);
clearTimeout(tooltipTimeout);
tooltipTimeout = setTimeout(() => setShouldShowTranslateEnabledTooltip?.(false), 2000);
const trans = await translate("sent", message.content);
message.content = trans.text;
});
return {
label: "Translate",
icon: TranslateIcon,
message,
channel: ChannelStore.getChannel(message.channel_id),
onClick: async () => {
const trans = await translate("received", message.content);
handleTranslate(message.id, trans);
}
};
},
stop() {
removePreSendListener(this.preSend);
removeChatBarButton("vc-translate");
removeButton("vc-translate");
removeAccessory("vc-translation");
},
async onBeforeMessageSend(_, message) {
if (!settings.store.autoTranslate) return;
if (!message.content) return;
setShouldShowTranslateEnabledTooltip?.(true);
clearTimeout(tooltipTimeout);
tooltipTimeout = setTimeout(() => setShouldShowTranslateEnabledTooltip?.(false), 2000);
const trans = await translate("sent", message.content);
message.content = trans.text;
}
});

View file

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

View file

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

View file

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

View file

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

View file

@ -6,6 +6,9 @@
import { LiteralUnion } from "type-fest";
export const SYM_IS_PROXY = Symbol("SettingsStore.isProxy");
export const SYM_GET_RAW_TARGET = Symbol("SettingsStore.getRawTarget");
// Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop
type ResolvePropDeep<T, P> = P extends `${infer Pre}.${infer Suf}`
? Pre extends keyof T
@ -28,6 +31,11 @@ interface SettingsStoreOptions {
// merges the SettingsStoreOptions type into the class
export interface SettingsStore<T extends object> extends SettingsStoreOptions { }
interface ProxyContext<T extends object = any> {
root: T;
path: string;
}
/**
* The SettingsStore allows you to easily create a mutable store that
* has support for global and path-based change listeners.
@ -35,6 +43,90 @@ export interface SettingsStore<T extends object> extends SettingsStoreOptions {
export class SettingsStore<T extends object> {
private pathListeners = new Map<string, Set<(newData: any) => void>>();
private globalListeners = new Set<(newData: T, path: string) => void>();
private readonly proxyContexts = new WeakMap<any, ProxyContext<T>>();
private readonly proxyHandler: ProxyHandler<any> = (() => {
const self = this;
return {
get(target, key: any, receiver) {
if (key === SYM_IS_PROXY) {
return true;
}
if (key === SYM_GET_RAW_TARGET) {
return target;
}
let v = Reflect.get(target, key, receiver);
const proxyContext = self.proxyContexts.get(target);
if (proxyContext == null) {
return v;
}
const { root, path } = proxyContext;
if (!(key in target) && self.getDefaultValue != null) {
v = self.getDefaultValue({
target,
key,
root,
path
});
}
if (typeof v === "object" && v !== null && !v[SYM_IS_PROXY]) {
const getPath = `${path}${path && "."}${key}`;
return self.makeProxy(v, root, getPath);
}
return v;
},
set(target, key: string, value) {
if (value?.[SYM_IS_PROXY]) {
value = value[SYM_GET_RAW_TARGET];
}
if (target[key] === value) {
return true;
}
if (!Reflect.set(target, key, value)) {
return false;
}
const proxyContext = self.proxyContexts.get(target);
if (proxyContext == null) {
return true;
}
const { root, path } = proxyContext;
const setPath = `${path}${path && "."}${key}`;
self.notifyListeners(setPath, value, root);
return true;
},
deleteProperty(target, key: string) {
if (!Reflect.deleteProperty(target, key)) {
return false;
}
const proxyContext = self.proxyContexts.get(target);
if (proxyContext == null) {
return true;
}
const { root, path } = proxyContext;
const deletePath = `${path}${path && "."}${key}`;
self.notifyListeners(deletePath, undefined, root);
return true;
}
};
})();
/**
* The store object. Making changes to this object will trigger the applicable change listeners
@ -51,39 +143,35 @@ export class SettingsStore<T extends object> {
Object.assign(this, options);
}
private makeProxy(object: any, root: T = object, path: string = "") {
const self = this;
return new Proxy(object, {
get(target, key: string) {
let v = target[key];
if (!(key in target) && self.getDefaultValue) {
v = self.getDefaultValue({
target,
key,
root,
path
});
}
if (typeof v === "object" && v !== null && !Array.isArray(v))
return self.makeProxy(v, root, `${path}${path && "."}${key}`);
return v;
},
set(target, key: string, value) {
if (target[key] === value) return true;
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;
}
private makeProxy(object: any, root: T = object, path = "") {
this.proxyContexts.set(object, {
root,
path
});
return new Proxy(object, this.proxyHandler);
}
private notifyListeners(pathStr: string, value: any, root: T) {
const paths = pathStr.split(".");
// 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);
this.globalListeners.forEach(cb => cb(root, settingPathStr));
this.pathListeners.get(settingPathStr)?.forEach(cb => cb(settingValue));
} else {
this.globalListeners.forEach(cb => cb(root, pathStr));
}
this.pathListeners.get(pathStr)?.forEach(cb => cb(value));
}
/**

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 = (PluginSettingCommon & 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

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

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;

View file

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