mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-14 00:53:04 -04:00
Merge branch 'dev' into strict-csp
This commit is contained in:
commit
d48f0d0744
225 changed files with 4843 additions and 4074 deletions
|
@ -23,6 +23,7 @@ export * as Util from "./utils";
|
|||
export * as QuickCss from "./utils/quickCss";
|
||||
export * as Updater from "./utils/updater";
|
||||
export * as Webpack from "./webpack";
|
||||
export * as WebpackPatcher from "./webpack/patchWebpack";
|
||||
export { PlainSettings, Settings };
|
||||
|
||||
import "./utils/quickCss";
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Settings } from "@api/Settings";
|
||||
import { PluginIpcMappings } from "@main/ipcPlugins";
|
||||
import type { UserThemeHeader } from "@main/themes";
|
||||
import { IpcEvents } from "@shared/IpcEvents";
|
||||
import { IpcRes } from "@utils/types";
|
||||
import type { Settings } from "api/Settings";
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
function invoke<T = any>(event: IpcEvents, ...args: any[]) {
|
||||
|
|
|
@ -9,7 +9,7 @@ import "./ChatButton.css";
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { waitFor } from "@webpack";
|
||||
import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common";
|
||||
import { Button, ButtonWrapperClasses, Tooltip } from "@webpack/common";
|
||||
import { Channel } from "discord-types/general";
|
||||
import { HTMLProps, JSX, MouseEventHandler, ReactNode } from "react";
|
||||
|
||||
|
@ -110,7 +110,7 @@ export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
|
|||
<Button
|
||||
aria-label={props.tooltip}
|
||||
size=""
|
||||
look={ButtonLooks.BLANK}
|
||||
look={Button.Looks.BLANK}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
innerClassName={`${ButtonWrapperClasses.button} ${ChannelTextAreaClasses?.button}`}
|
||||
|
|
|
@ -31,6 +31,7 @@ export const commands = {} as Record<string, Command>;
|
|||
// hack for plugins being evaluated before we can grab these from webpack
|
||||
const OptPlaceholder = Symbol("OptionalMessageOption") as any as Option;
|
||||
const ReqPlaceholder = Symbol("RequiredMessageOption") as any as Option;
|
||||
|
||||
/**
|
||||
* Optional message option named "message" you can use in commands.
|
||||
* Used in "tableflip" or "shrug"
|
||||
|
@ -44,11 +45,16 @@ export let OptionalMessageOption: Option = OptPlaceholder;
|
|||
*/
|
||||
export let RequiredMessageOption: Option = ReqPlaceholder;
|
||||
|
||||
// Discord's command list has random gaps for some reason, which can cause issues while rendering the commands
|
||||
// Add this offset to every added command to keep them unique
|
||||
let commandIdOffset: number;
|
||||
|
||||
export const _init = function (cmds: Command[]) {
|
||||
try {
|
||||
BUILT_IN = cmds;
|
||||
OptionalMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "shrug")!.options![0];
|
||||
RequiredMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "me")!.options![0];
|
||||
commandIdOffset = Math.abs(BUILT_IN.map(x => Number(x.id)).sort((x, y) => x - y)[0]) - BUILT_IN.length;
|
||||
} catch (e) {
|
||||
new Logger("CommandsAPI").error("Failed to load CommandsApi", e, " - cmds is", cmds);
|
||||
}
|
||||
|
@ -142,7 +148,7 @@ export function registerCommand<C extends Command>(command: C, plugin: string) {
|
|||
command.isVencordCommand = true;
|
||||
command.untranslatedName ??= command.name;
|
||||
command.untranslatedDescription ??= command.description;
|
||||
command.id ??= `-${BUILT_IN.length + 1}`;
|
||||
command.id ??= `-${BUILT_IN.length + commandIdOffset + 1}`;
|
||||
command.applicationId ??= "-1"; // BUILT_IN;
|
||||
command.type ??= ApplicationCommandType.CHAT_INPUT;
|
||||
command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;
|
||||
|
|
|
@ -122,7 +122,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra
|
|||
}
|
||||
|
||||
interface ContextMenuProps {
|
||||
contextMenuApiArguments?: Array<any>;
|
||||
contextMenuAPIArguments?: Array<any>;
|
||||
navId: string;
|
||||
children: Array<ReactElement<any> | null>;
|
||||
"aria-label": string;
|
||||
|
@ -136,7 +136,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
|
|||
children: cloneMenuChildren(props.children),
|
||||
};
|
||||
|
||||
props.contextMenuApiArguments ??= [];
|
||||
props.contextMenuAPIArguments ??= [];
|
||||
const contextMenuPatches = navPatches.get(props.navId);
|
||||
|
||||
if (!Array.isArray(props.children)) props.children = [props.children];
|
||||
|
@ -144,7 +144,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
|
|||
if (contextMenuPatches) {
|
||||
for (const patch of contextMenuPatches) {
|
||||
try {
|
||||
patch(props.children, ...props.contextMenuApiArguments);
|
||||
patch(props.children, ...props.contextMenuAPIArguments);
|
||||
} catch (err) {
|
||||
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
|
||||
}
|
||||
|
@ -153,7 +153,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
|
|||
|
||||
for (const patch of globalPatches) {
|
||||
try {
|
||||
patch(props.navId, props.children, ...props.contextMenuApiArguments);
|
||||
patch(props.navId, props.children, ...props.contextMenuAPIArguments);
|
||||
} catch (err) {
|
||||
ContextMenuLogger.error("Global patch errored,", err);
|
||||
}
|
||||
|
|
|
@ -43,20 +43,21 @@ interface DecoratorProps {
|
|||
export type MemberListDecoratorFactory = (props: DecoratorProps) => JSX.Element | null;
|
||||
type OnlyIn = "guilds" | "dms";
|
||||
|
||||
export const decorators = new Map<string, { render: MemberListDecoratorFactory, onlyIn?: OnlyIn; }>();
|
||||
export const decoratorsFactories = new Map<string, { render: MemberListDecoratorFactory, onlyIn?: OnlyIn; }>();
|
||||
|
||||
export function addMemberListDecorator(identifier: string, render: MemberListDecoratorFactory, onlyIn?: OnlyIn) {
|
||||
decorators.set(identifier, { render, onlyIn });
|
||||
decoratorsFactories.set(identifier, { render, onlyIn });
|
||||
}
|
||||
|
||||
export function removeMemberListDecorator(identifier: string) {
|
||||
decorators.delete(identifier);
|
||||
decoratorsFactories.delete(identifier);
|
||||
}
|
||||
|
||||
export function __getDecorators(props: DecoratorProps): (JSX.Element | null)[] {
|
||||
export function __getDecorators(props: DecoratorProps): JSX.Element {
|
||||
const isInGuild = !!(props.guildId);
|
||||
return Array.from(
|
||||
decorators.entries(),
|
||||
|
||||
const decorators = Array.from(
|
||||
decoratorsFactories.entries(),
|
||||
([key, { render: Decorator, onlyIn }]) => {
|
||||
if ((onlyIn === "guilds" && !isInGuild) || (onlyIn === "dms" && isInGuild))
|
||||
return null;
|
||||
|
@ -68,4 +69,10 @@ export function __getDecorators(props: DecoratorProps): (JSX.Element | null)[] {
|
|||
);
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="vc-member-list-decorators-wrapper">
|
||||
{decorators}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -48,23 +48,29 @@ export interface MessageDecorationProps {
|
|||
}
|
||||
export type MessageDecorationFactory = (props: MessageDecorationProps) => JSX.Element | null;
|
||||
|
||||
export const decorations = new Map<string, MessageDecorationFactory>();
|
||||
export const decorationsFactories = new Map<string, MessageDecorationFactory>();
|
||||
|
||||
export function addMessageDecoration(identifier: string, decoration: MessageDecorationFactory) {
|
||||
decorations.set(identifier, decoration);
|
||||
decorationsFactories.set(identifier, decoration);
|
||||
}
|
||||
|
||||
export function removeMessageDecoration(identifier: string) {
|
||||
decorations.delete(identifier);
|
||||
decorationsFactories.delete(identifier);
|
||||
}
|
||||
|
||||
export function __addDecorationsToMessage(props: MessageDecorationProps): (JSX.Element | null)[] {
|
||||
return Array.from(
|
||||
decorations.entries(),
|
||||
export function __addDecorationsToMessage(props: MessageDecorationProps): JSX.Element {
|
||||
const decorations = Array.from(
|
||||
decorationsFactories.entries(),
|
||||
([key, Decoration]) => (
|
||||
<ErrorBoundary noop message={`Failed to render ${key} Message Decoration`} key={key}>
|
||||
<Decoration {...props} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="vc-message-decorations-wrapper">
|
||||
{decorations}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.visual-refresh .vc-notification-root {
|
||||
background-color: var(--bg-overlay-floating, var(--background-base-low));
|
||||
}
|
||||
|
||||
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
|
||||
position: absolute;
|
||||
z-index: 2147483647;
|
||||
|
|
|
@ -32,9 +32,10 @@ export interface Settings {
|
|||
autoUpdate: boolean;
|
||||
autoUpdateNotification: boolean,
|
||||
useQuickCss: boolean;
|
||||
eagerPatches: boolean;
|
||||
enabledThemes: string[];
|
||||
enableReactDevtools: boolean;
|
||||
themeLinks: string[];
|
||||
enabledThemes: string[];
|
||||
frameless: boolean;
|
||||
transparent: boolean;
|
||||
winCtrlQ: boolean;
|
||||
|
@ -81,6 +82,7 @@ const DefaultSettings: Settings = {
|
|||
autoUpdateNotification: true,
|
||||
useQuickCss: true,
|
||||
themeLinks: [],
|
||||
eagerPatches: IS_REPORTER,
|
||||
enabledThemes: [],
|
||||
enableReactDevtools: false,
|
||||
frameless: false,
|
||||
|
@ -220,6 +222,17 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
|||
}
|
||||
}
|
||||
|
||||
export function migratePluginSetting(pluginName: string, oldSetting: string, newSetting: string) {
|
||||
const settings = SettingsStore.plain.plugins[pluginName];
|
||||
if (!settings) return;
|
||||
|
||||
if (!Object.hasOwn(settings, oldSetting) || Object.hasOwn(settings, newSetting)) return;
|
||||
|
||||
settings[newSetting] = settings[oldSetting];
|
||||
delete settings[oldSetting];
|
||||
SettingsStore.markAsChanged();
|
||||
}
|
||||
|
||||
export function definePluginSettings<
|
||||
Def extends SettingsDefinition,
|
||||
Checks extends SettingsChecks<Def>,
|
||||
|
|
|
@ -17,16 +17,22 @@
|
|||
*/
|
||||
|
||||
import { Button } from "@webpack/common";
|
||||
import { ButtonProps } from "@webpack/types";
|
||||
|
||||
import { Heart } from "./Heart";
|
||||
|
||||
export default function DonateButton(props: any) {
|
||||
export default function DonateButton({
|
||||
look = Button.Looks.LINK,
|
||||
color = Button.Colors.TRANSPARENT,
|
||||
...props
|
||||
}: Partial<ButtonProps>) {
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
look={Button.Looks.LINK}
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
look={look}
|
||||
color={color}
|
||||
onClick={() => VencordNative.native.openExternal("https://github.com/sponsors/Vendicated")}
|
||||
innerClassName="vc-donate-button"
|
||||
>
|
||||
<Heart />
|
||||
Donate
|
||||
|
|
|
@ -16,14 +16,18 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export function Heart() {
|
||||
import { classes } from "@utils/misc";
|
||||
import { SVGProps } from "react";
|
||||
|
||||
export function Heart(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
height="16"
|
||||
width="16"
|
||||
style={{ marginRight: "0.5em", transform: "translateY(2px)" }}
|
||||
{...props}
|
||||
className={classes("vc-heart-icon", props.className)}
|
||||
>
|
||||
<path
|
||||
fill="#db61a2"
|
||||
|
|
|
@ -37,6 +37,7 @@ import { Constructor } from "type-fest";
|
|||
import { PluginMeta } from "~plugins";
|
||||
|
||||
import {
|
||||
ISettingCustomElementProps,
|
||||
ISettingElementProps,
|
||||
SettingBooleanComponent,
|
||||
SettingCustomComponent,
|
||||
|
@ -74,7 +75,7 @@ function makeDummyUser(user: { username: string; id?: string; avatar?: string; }
|
|||
return newUser;
|
||||
}
|
||||
|
||||
const Components: Record<OptionType, React.ComponentType<ISettingElementProps<any>>> = {
|
||||
const Components: Record<OptionType, React.ComponentType<ISettingElementProps<any> | ISettingCustomElementProps<any>>> = {
|
||||
[OptionType.STRING]: SettingTextComponent,
|
||||
[OptionType.NUMBER]: SettingNumericComponent,
|
||||
[OptionType.BIGINT]: SettingNumericComponent,
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
|
||||
import { PluginOptionComponent } from "@utils/types";
|
||||
|
||||
import { ISettingElementProps } from ".";
|
||||
import { ISettingCustomElementProps } from ".";
|
||||
|
||||
export function SettingCustomComponent({ option, onChange, onError }: ISettingElementProps<PluginOptionComponent>) {
|
||||
export function SettingCustomComponent({ option, onChange, onError }: ISettingCustomElementProps<PluginOptionComponent>) {
|
||||
return option.component({ setValue: onChange, setError: onError, option });
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
|
|||
onChange={handleChange}
|
||||
placeholder={option.placeholder ?? "Enter a value"}
|
||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||
maxLength={null}
|
||||
{...option.componentProps}
|
||||
/>
|
||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import { DefinedSettings, PluginOptionBase } from "@utils/types";
|
||||
|
||||
export interface ISettingElementProps<T extends PluginOptionBase> {
|
||||
interface ISettingElementPropsBase<T> {
|
||||
option: T;
|
||||
onChange(newValue: any): void;
|
||||
pluginSettings: {
|
||||
|
@ -30,6 +30,9 @@ export interface ISettingElementProps<T extends PluginOptionBase> {
|
|||
definedSettings?: DefinedSettings;
|
||||
}
|
||||
|
||||
export type ISettingElementProps<T extends PluginOptionBase> = ISettingElementPropsBase<T>;
|
||||
export type ISettingCustomElementProps<T extends Omit<PluginOptionBase, "description" | "placeholder">> = ISettingElementPropsBase<T>;
|
||||
|
||||
export * from "../../Badge";
|
||||
export * from "./SettingBooleanComponent";
|
||||
export * from "./SettingCustomComponent";
|
||||
|
|
|
@ -69,7 +69,7 @@ function ReloadRequiredCard({ required }: { required: boolean; }) {
|
|||
<Forms.FormText className={cl("dep-text")}>
|
||||
Restart now to apply new plugins and their settings
|
||||
</Forms.FormText>
|
||||
<Button onClick={() => location.reload()}>
|
||||
<Button onClick={() => location.reload()} className={cl("restart-button")}>
|
||||
Restart
|
||||
</Button>
|
||||
</>
|
||||
|
@ -158,8 +158,8 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
|
|||
className={classes(ButtonClasses.button, cl("info-button"))}
|
||||
>
|
||||
{plugin.options && !isObjectEmpty(plugin.options)
|
||||
? <CogWheel />
|
||||
: <InfoIcon />}
|
||||
? <CogWheel className={cl("info-icon")} />
|
||||
: <InfoIcon className={cl("info-icon")} />}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
@ -177,10 +177,10 @@ function ExcludedPluginsList({ search }: { search: string; }) {
|
|||
const matchingExcludedPlugins = Object.entries(ExcludedPlugins)
|
||||
.filter(([name]) => name.toLowerCase().includes(search));
|
||||
|
||||
const ExcludedReasons: Record<"web" | "discordDesktop" | "vencordDesktop" | "desktop" | "dev", string> = {
|
||||
const ExcludedReasons: Record<"web" | "discordDesktop" | "vesktop" | "desktop" | "dev", string> = {
|
||||
desktop: "Discord Desktop app or Vesktop",
|
||||
discordDesktop: "Discord Desktop app",
|
||||
vencordDesktop: "Vesktop app",
|
||||
vesktop: "Vesktop app",
|
||||
web: "Vesktop app and the Web version of Discord",
|
||||
dev: "Developer version of Vencord"
|
||||
};
|
||||
|
|
|
@ -63,10 +63,7 @@
|
|||
height: 8em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.vc-plugins-info-card div {
|
||||
line-height: 32px;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.vc-plugins-restart-card {
|
||||
|
@ -76,11 +73,11 @@
|
|||
color: var(--info-warning-text);
|
||||
}
|
||||
|
||||
.vc-plugins-restart-card button {
|
||||
.vc-plugins-restart-button {
|
||||
margin-top: 0.5em;
|
||||
background: var(--info-warning-foreground) !important;
|
||||
}
|
||||
|
||||
.vc-plugins-info-button svg:not(:hover, :focus) {
|
||||
.vc-plugins-info-icon:not(:hover, :focus) {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ interface SwitchProps {
|
|||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SWITCH_ON = "var(--green-360)";
|
||||
const SWITCH_ON = "var(--brand-500)";
|
||||
const SWITCH_OFF = "var(--primary-400)";
|
||||
const SwitchClasses = findByPropsLazy("slider", "input", "container");
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
|
|||
}
|
||||
const canonicalMatch = canonicalizeMatch(new RegExp(match));
|
||||
try {
|
||||
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
|
||||
const canonicalReplace = canonicalizeReplace(replacement, 'Vencord.Plugins.plugins["YourPlugin"]');
|
||||
var patched = src.replace(canonicalMatch, canonicalReplace as string);
|
||||
setReplacementError(void 0);
|
||||
} catch (e) {
|
||||
|
|
77
src/components/VencordSettings/SpecialCard.tsx
Normal file
77
src/components/VencordSettings/SpecialCard.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./specialCard.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Card, Clickable, Forms, React } from "@webpack/common";
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
const cl = classNameFactory("vc-special-");
|
||||
|
||||
interface StyledCardProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
description: string;
|
||||
cardImage?: string;
|
||||
backgroundImage?: string;
|
||||
backgroundColor?: string;
|
||||
buttonTitle?: string;
|
||||
buttonOnClick?: () => void;
|
||||
}
|
||||
|
||||
export function SpecialCard({ title, subtitle, description, cardImage, backgroundImage, backgroundColor, buttonTitle, buttonOnClick: onClick, children }: PropsWithChildren<StyledCardProps>) {
|
||||
const cardStyle: React.CSSProperties = {
|
||||
backgroundColor: backgroundColor || "#9c85ef",
|
||||
backgroundImage: `url(${backgroundImage || ""})`,
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={cl("card", "card-special")} style={cardStyle}>
|
||||
<div className={cl("card-flex")}>
|
||||
<div className={cl("card-flex-main")}>
|
||||
<Forms.FormTitle className={cl("title")} tag="h5">{title}</Forms.FormTitle>
|
||||
<Forms.FormText className={cl("subtitle")}>{subtitle}</Forms.FormText>
|
||||
<Forms.FormText className={cl("text")}>{description}</Forms.FormText>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
{cardImage && (
|
||||
<div className={cl("image-container")}>
|
||||
<img
|
||||
role="presentation"
|
||||
src={cardImage}
|
||||
alt=""
|
||||
className={cl("image")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{buttonTitle && (
|
||||
<>
|
||||
<Forms.FormDivider className={cl("seperator")} />
|
||||
<Clickable onClick={onClick} className={cl("hyperlink")}>
|
||||
<Forms.FormText className={cl("hyperlink-text")}>
|
||||
{buttonTitle}
|
||||
</Forms.FormText>
|
||||
</Clickable>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
|
@ -20,29 +20,38 @@ import { openNotificationLogModal } from "@api/Notifications/notificationLog";
|
|||
import { useSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import DonateButton from "@components/DonateButton";
|
||||
import { openContributorModal } from "@components/PluginSettings/ContributorModal";
|
||||
import { openPluginModal } from "@components/PluginSettings/PluginModal";
|
||||
import { gitRemote } from "@shared/vencordUserAgent";
|
||||
import { DONOR_ROLE_ID, VENCORD_GUILD_ID } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { identity } from "@utils/misc";
|
||||
import { identity, isPluginDev } from "@utils/misc";
|
||||
import { relaunch, showItemInFolder } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { Button, Card, Forms, React, Select, Switch } from "@webpack/common";
|
||||
import { Button, Forms, GuildMemberStore, React, Select, Switch, UserStore } from "@webpack/common";
|
||||
|
||||
import BadgeAPI from "../../plugins/_api/badges";
|
||||
import { Flex, FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from "..";
|
||||
import { openNotificationSettingsModal } from "./NotificationSettings";
|
||||
import { QuickAction, QuickActionCard } from "./quickActions";
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
import { SpecialCard } from "./SpecialCard";
|
||||
|
||||
const cl = classNameFactory("vc-settings-");
|
||||
|
||||
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png";
|
||||
const SHIGGY_DONATE_IMAGE = "https://media.discordapp.net/stickers/1039992459209490513.png";
|
||||
|
||||
const VENNIE_DONATOR_IMAGE = "https://cdn.discordapp.com/emojis/1238120638020063377.png";
|
||||
const COZY_CONTRIB_IMAGE = "https://cdn.discordapp.com/emojis/1026533070955872337.png";
|
||||
|
||||
const DONOR_BACKGROUND_IMAGE = "https://media.discordapp.net/stickers/1311070116305436712.png?size=2048";
|
||||
const CONTRIB_BACKGROUND_IMAGE = "https://media.discordapp.net/stickers/1311070166481895484.png?size=2048";
|
||||
|
||||
type KeysOfType<Object, Type> = {
|
||||
[K in keyof Object]: Object[K] extends Type ? K : never;
|
||||
}[keyof Object];
|
||||
|
||||
|
||||
function VencordSettings() {
|
||||
const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, {
|
||||
fallbackValue: "Loading..."
|
||||
|
@ -55,6 +64,8 @@ function VencordSettings() {
|
|||
const isMac = navigator.platform.toLowerCase().startsWith("mac");
|
||||
const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac;
|
||||
|
||||
const user = UserStore.getCurrentUser();
|
||||
|
||||
const Switches: Array<false | {
|
||||
key: KeysOfType<typeof settings, boolean>;
|
||||
title: string;
|
||||
|
@ -99,7 +110,44 @@ function VencordSettings() {
|
|||
|
||||
return (
|
||||
<SettingsTab title="Vencord Settings">
|
||||
<DonateCard image={donateImage} />
|
||||
{isDonor(user?.id)
|
||||
? (
|
||||
<SpecialCard
|
||||
title="Donations"
|
||||
subtitle="Thank you for donating!"
|
||||
description="You can manage your perks at any time by messaging @vending.machine."
|
||||
cardImage={VENNIE_DONATOR_IMAGE}
|
||||
backgroundImage={DONOR_BACKGROUND_IMAGE}
|
||||
backgroundColor="#ED87A9"
|
||||
>
|
||||
<DonateButtonComponent />
|
||||
</SpecialCard>
|
||||
)
|
||||
: (
|
||||
<SpecialCard
|
||||
title="Support the Project"
|
||||
description="Please consider supporting the development of Vencord by donating!"
|
||||
cardImage={donateImage}
|
||||
backgroundImage={DONOR_BACKGROUND_IMAGE}
|
||||
backgroundColor="#c3a3ce"
|
||||
>
|
||||
<DonateButtonComponent />
|
||||
</SpecialCard>
|
||||
)
|
||||
}
|
||||
{isPluginDev(user?.id) && (
|
||||
<SpecialCard
|
||||
title="Contributions"
|
||||
subtitle="Thank you for contributing!"
|
||||
description="Since you've contributed to Vencord you now have a cool new badge!"
|
||||
cardImage={COZY_CONTRIB_IMAGE}
|
||||
backgroundImage={CONTRIB_BACKGROUND_IMAGE}
|
||||
backgroundColor="#EDCC87"
|
||||
buttonTitle="See what you've contributed to"
|
||||
buttonOnClick={() => openContributorModal(user)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Forms.FormSection title="Quick Actions">
|
||||
<QuickActionCard>
|
||||
<QuickAction
|
||||
|
@ -239,31 +287,19 @@ function VencordSettings() {
|
|||
);
|
||||
}
|
||||
|
||||
interface DonateCardProps {
|
||||
image: string;
|
||||
}
|
||||
|
||||
function DonateCard({ image }: DonateCardProps) {
|
||||
function DonateButtonComponent() {
|
||||
return (
|
||||
<Card className={cl("card", "donate")}>
|
||||
<div>
|
||||
<Forms.FormTitle tag="h5">Support the Project</Forms.FormTitle>
|
||||
<Forms.FormText>Please consider supporting the development of Vencord by donating!</Forms.FormText>
|
||||
<DonateButton style={{ transform: "translateX(-1em)" }} />
|
||||
</div>
|
||||
<img
|
||||
role="presentation"
|
||||
src={image}
|
||||
alt=""
|
||||
height={128}
|
||||
style={{
|
||||
imageRendering: image === SHIGGY_DONATE_IMAGE ? "pixelated" : void 0,
|
||||
marginLeft: "auto",
|
||||
transform: image === DEFAULT_DONATE_IMAGE ? "rotate(10deg)" : void 0
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<DonateButton
|
||||
look={Button.Looks.FILLED}
|
||||
color={Button.Colors.WHITE}
|
||||
style={{ marginTop: "1em" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function isDonor(userId: string): boolean {
|
||||
const donorBadges = BadgeAPI.getDonorBadges(userId);
|
||||
return GuildMemberStore.getMember(VENCORD_GUILD_ID, userId)?.roles.includes(DONOR_ROLE_ID) || !!donorBadges;
|
||||
}
|
||||
|
||||
export default wrapTab(VencordSettings, "Vencord Settings");
|
||||
|
|
|
@ -11,6 +11,11 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.visual-refresh .vc-addon-card {
|
||||
background-color: var(--card-primary-bg);
|
||||
border: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.vc-addon-card-disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
@ -21,6 +26,11 @@
|
|||
box-shadow: var(--elevation-high);
|
||||
}
|
||||
|
||||
.visual-refresh .vc-addon-card:hover {
|
||||
/* same as non-hover, here to overwrite the non-refresh hover background */
|
||||
background-color: var(--card-primary-bg);
|
||||
}
|
||||
|
||||
.vc-addon-header {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
.vc-settings-quickActions-card {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, max-content));
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5em;
|
||||
justify-content: center;
|
||||
padding: 0.5em 0;
|
||||
padding: 0.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
@media (width <=1040px) {
|
||||
.vc-settings-quickActions-card {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.vc-settings-quickActions-pill {
|
||||
all: unset;
|
||||
background: var(--background-secondary);
|
||||
|
@ -14,12 +19,16 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
padding: 8px 12px;
|
||||
border-radius: 9999px;
|
||||
padding: 8px 9px;
|
||||
border-radius: 8px;
|
||||
transition: 0.1s ease-out;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.vc-settings-quickActions-pill:hover {
|
||||
background: var(--background-secondary-alt);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--elevation-high);
|
||||
}
|
||||
|
||||
.vc-settings-quickActions-pill:focus-visible {
|
||||
|
@ -27,7 +36,15 @@
|
|||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.visual-refresh .vc-settings-quickActions-pill {
|
||||
background: var(--button-secondary-background);
|
||||
}
|
||||
|
||||
.visual-refresh .vc-settings-quickActions-pill:hover {
|
||||
background: var(--button-secondary-background-hover);
|
||||
}
|
||||
|
||||
.vc-settings-quickActions-img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
92
src/components/VencordSettings/specialCard.css
Normal file
92
src/components/VencordSettings/specialCard.css
Normal file
|
@ -0,0 +1,92 @@
|
|||
.vc-donate-button {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.vc-donate-button .vc-heart-icon {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.vc-donate-button:hover .vc-heart-icon {
|
||||
transform: scale(1.1);
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.vc-settings-card {
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.vc-special-card-special {
|
||||
padding: 1em 1.5em;
|
||||
margin-bottom: 1em;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.vc-special-card-flex {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.vc-special-card-flex-main {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vc-special-title {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.vc-special-subtitle {
|
||||
color: black;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.vc-special-text {
|
||||
color: black;
|
||||
font-size: 1em;
|
||||
margin-top: .75em;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.vc-special-seperator {
|
||||
margin-top: .75em;
|
||||
border-top: 1px solid white;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.vc-special-hyperlink {
|
||||
margin-top: 1em;
|
||||
cursor: pointer;
|
||||
|
||||
.vc-special-hyperlink-text {
|
||||
color: black;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
transition: text-decoration 0.5s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover .vc-special-hyperlink-text {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.vc-special-image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-left: 1em;
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.vc-special-image {
|
||||
width: 65%;
|
||||
}
|
|
@ -5,3 +5,8 @@
|
|||
.vc-owner-crown-icon {
|
||||
color: var(--text-warning);
|
||||
}
|
||||
|
||||
.vc-heart-icon {
|
||||
margin-right: 0.5em;
|
||||
translate: 0 2px;
|
||||
}
|
||||
|
|
|
@ -23,35 +23,61 @@ if (IS_DEV || IS_REPORTER) {
|
|||
var logger = new Logger("Tracer", "#FFD166");
|
||||
}
|
||||
|
||||
const noop = function () { };
|
||||
|
||||
export const beginTrace = !(IS_DEV || IS_REPORTER) ? noop :
|
||||
export const beginTrace = !(IS_DEV || IS_REPORTER) ? () => { } :
|
||||
function beginTrace(name: string, ...args: any[]) {
|
||||
if (name in traces)
|
||||
if (name in traces) {
|
||||
throw new Error(`Trace ${name} already exists!`);
|
||||
}
|
||||
|
||||
traces[name] = [performance.now(), args];
|
||||
};
|
||||
|
||||
export const finishTrace = !(IS_DEV || IS_REPORTER) ? noop : function finishTrace(name: string) {
|
||||
const end = performance.now();
|
||||
export const finishTrace = !(IS_DEV || IS_REPORTER) ? () => 0 :
|
||||
function finishTrace(name: string) {
|
||||
const end = performance.now();
|
||||
|
||||
const [start, args] = traces[name];
|
||||
delete traces[name];
|
||||
const [start, args] = traces[name];
|
||||
delete traces[name];
|
||||
|
||||
logger.debug(`${name} took ${end - start}ms`, args);
|
||||
};
|
||||
const totalTime = end - start;
|
||||
logger.debug(`${name} took ${totalTime}ms`, args);
|
||||
|
||||
return totalTime;
|
||||
};
|
||||
|
||||
type Func = (...args: any[]) => any;
|
||||
type TraceNameMapper<F extends Func> = (...args: Parameters<F>) => string;
|
||||
|
||||
const noopTracer =
|
||||
<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) => f;
|
||||
function noopTracerWithResults<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) {
|
||||
return function (this: unknown, ...args: Parameters<F>): [ReturnType<F>, number] {
|
||||
return [f.apply(this, args), 0];
|
||||
};
|
||||
}
|
||||
|
||||
function noopTracer<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) {
|
||||
return f;
|
||||
}
|
||||
|
||||
export const traceFunctionWithResults = !(IS_DEV || IS_REPORTER)
|
||||
? noopTracerWithResults
|
||||
: function traceFunctionWithResults<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): (this: unknown, ...args: Parameters<F>) => [ReturnType<F>, number] {
|
||||
return function (this: unknown, ...args: Parameters<F>) {
|
||||
const traceName = mapper?.(...args) ?? name;
|
||||
|
||||
beginTrace(traceName, ...arguments);
|
||||
try {
|
||||
return [f.apply(this, args), finishTrace(traceName)];
|
||||
} catch (e) {
|
||||
finishTrace(traceName);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const traceFunction = !(IS_DEV || IS_REPORTER)
|
||||
? noopTracer
|
||||
: function traceFunction<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): F {
|
||||
return function (this: any, ...args: Parameters<F>) {
|
||||
return function (this: unknown, ...args: Parameters<F>) {
|
||||
const traceName = mapper?.(...args) ?? name;
|
||||
|
||||
beginTrace(traceName, ...arguments);
|
||||
|
|
|
@ -8,23 +8,26 @@ import { Logger } from "@utils/Logger";
|
|||
import { canonicalizeMatch } from "@utils/patches";
|
||||
import * as Webpack from "@webpack";
|
||||
import { wreq } from "@webpack";
|
||||
|
||||
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
|
||||
import { AnyModuleFactory, ModuleFactory } from "@webpack/wreq.d";
|
||||
|
||||
export async function loadLazyChunks() {
|
||||
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
|
||||
|
||||
try {
|
||||
LazyChunkLoaderLogger.log("Loading all chunks...");
|
||||
|
||||
const validChunks = new Set<number>();
|
||||
const invalidChunks = new Set<number>();
|
||||
const deferredRequires = new Set<number>();
|
||||
const validChunks = new Set<PropertyKey>();
|
||||
const invalidChunks = new Set<PropertyKey>();
|
||||
const deferredRequires = new Set<PropertyKey>();
|
||||
|
||||
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
|
||||
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
|
||||
const { promise: chunksSearchingDone, resolve: chunksSearchingResolve } = Promise.withResolvers<void>();
|
||||
|
||||
// True if resolved, false otherwise
|
||||
const chunksSearchPromises = [] as Array<() => boolean>;
|
||||
|
||||
/* This regex loads all language packs which makes webpack finds testing extremely slow, so for now, lets use one which doesnt include those
|
||||
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i(?:\.\i)?\.bind\(\i,"?([^)]+?)"?(?:,[^)]+?)?\)\)/g);
|
||||
*/
|
||||
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);
|
||||
|
||||
let foundCssDebuggingLoad = false;
|
||||
|
@ -34,12 +37,15 @@ export async function loadLazyChunks() {
|
|||
const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : (foundCssDebuggingLoad = factoryCode.includes(".cssDebuggingEnabled&&"));
|
||||
|
||||
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
|
||||
const validChunkGroups = new Set<[chunkIds: number[], entryPoint: number]>();
|
||||
const validChunkGroups = new Set<[chunkIds: PropertyKey[], entryPoint: PropertyKey]>();
|
||||
|
||||
const shouldForceDefer = false;
|
||||
|
||||
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
|
||||
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => Number(m[1])) : [];
|
||||
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => {
|
||||
const numChunkId = Number(m[1]);
|
||||
return Number.isNaN(numChunkId) ? m[1] : numChunkId;
|
||||
}) : [];
|
||||
|
||||
if (chunkIds.length === 0) {
|
||||
return;
|
||||
|
@ -62,7 +68,7 @@ export async function loadLazyChunks() {
|
|||
|
||||
const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
|
||||
.then(r => r.text())
|
||||
.then(t => t.includes("importScripts("));
|
||||
.then(t => /importScripts\(|self\.postMessage/.test(t));
|
||||
|
||||
if (isWorkerAsset) {
|
||||
invalidChunks.add(id);
|
||||
|
@ -74,7 +80,8 @@ export async function loadLazyChunks() {
|
|||
}
|
||||
|
||||
if (!invalidChunkGroup) {
|
||||
validChunkGroups.add([chunkIds, Number(entryPoint)]);
|
||||
const numEntryPoint = Number(entryPoint);
|
||||
validChunkGroups.add([chunkIds, Number.isNaN(numEntryPoint) ? entryPoint : numEntryPoint]);
|
||||
}
|
||||
}));
|
||||
|
||||
|
@ -82,7 +89,7 @@ export async function loadLazyChunks() {
|
|||
await Promise.all(
|
||||
Array.from(validChunkGroups)
|
||||
.map(([chunkIds]) =>
|
||||
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
|
||||
Promise.all(chunkIds.map(id => wreq.e(id)))
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -94,7 +101,7 @@ export async function loadLazyChunks() {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
||||
if (wreq.m[entryPoint]) wreq(entryPoint);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
@ -122,41 +129,44 @@ export async function loadLazyChunks() {
|
|||
}, 0);
|
||||
}
|
||||
|
||||
Webpack.factoryListeners.add(factory => {
|
||||
function factoryListener(factory: AnyModuleFactory | ModuleFactory) {
|
||||
let isResolved = false;
|
||||
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
|
||||
searchAndLoadLazyChunks(String(factory))
|
||||
.then(() => isResolved = true)
|
||||
.catch(() => isResolved = true);
|
||||
|
||||
chunksSearchPromises.push(() => isResolved);
|
||||
});
|
||||
}
|
||||
|
||||
for (const factoryId in wreq.m) {
|
||||
let isResolved = false;
|
||||
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
|
||||
|
||||
chunksSearchPromises.push(() => isResolved);
|
||||
Webpack.factoryListeners.add(factoryListener);
|
||||
for (const moduleId in wreq.m) {
|
||||
factoryListener(wreq.m[moduleId]);
|
||||
}
|
||||
|
||||
await chunksSearchingDone;
|
||||
Webpack.factoryListeners.delete(factoryListener);
|
||||
|
||||
// Require deferred entry points
|
||||
for (const deferredRequire of deferredRequires) {
|
||||
wreq!(deferredRequire as any);
|
||||
wreq(deferredRequire);
|
||||
}
|
||||
|
||||
// All chunks Discord has mapped to asset files, even if they are not used anymore
|
||||
const allChunks = [] as number[];
|
||||
const allChunks = [] as PropertyKey[];
|
||||
|
||||
// Matches "id" or id:
|
||||
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"([\deE]+?)"(?![,}]))|(?:([\deE]+?):)/g)) {
|
||||
for (const currentMatch of String(wreq.u).matchAll(/(?:"([\deE]+?)"(?![,}]))|(?:([\deE]+?):)/g)) {
|
||||
const id = currentMatch[1] ?? currentMatch[2];
|
||||
if (id == null) continue;
|
||||
|
||||
allChunks.push(Number(id));
|
||||
const numId = Number(id);
|
||||
allChunks.push(Number.isNaN(numId) ? id : numId);
|
||||
}
|
||||
|
||||
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
|
||||
|
||||
// Chunks that are not loaded (not used) by Discord code anymore
|
||||
// Chunks which our regex could not catch to load
|
||||
// It will always contain WebWorker assets, and also currently contains some language packs which are loaded differently
|
||||
const chunksLeft = allChunks.filter(id => {
|
||||
return !(validChunks.has(id) || invalidChunks.has(id));
|
||||
});
|
||||
|
@ -164,14 +174,11 @@ export async function loadLazyChunks() {
|
|||
await Promise.all(chunksLeft.map(async id => {
|
||||
const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
|
||||
.then(r => r.text())
|
||||
.then(t => t.includes("importScripts("));
|
||||
.then(t => /importScripts\(|self\.postMessage/.test(t));
|
||||
|
||||
// Loads and requires a chunk
|
||||
// Loads the chunk. Currently this only happens with the language packs which are loaded differently
|
||||
if (!isWorkerAsset) {
|
||||
await wreq.e(id as any);
|
||||
// Technically, the id of the chunk does not match the entry point
|
||||
// But, still try it because we have no way to get the actual entry point
|
||||
if (wreq.m[id]) wreq(id as any);
|
||||
await wreq.e(id);
|
||||
}
|
||||
}));
|
||||
|
||||
|
|
|
@ -6,28 +6,56 @@
|
|||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import * as Webpack from "@webpack";
|
||||
import { patches } from "plugins";
|
||||
import { getBuildNumber, patchTimings } from "@webpack/patcher";
|
||||
|
||||
import { addPatch, patches } from "../plugins";
|
||||
import { loadLazyChunks } from "./loadLazyChunks";
|
||||
|
||||
const ReporterLogger = new Logger("Reporter");
|
||||
|
||||
async function runReporter() {
|
||||
const ReporterLogger = new Logger("Reporter");
|
||||
|
||||
try {
|
||||
ReporterLogger.log("Starting test...");
|
||||
|
||||
let loadLazyChunksResolve: (value: void | PromiseLike<void>) => void;
|
||||
const loadLazyChunksDone = new Promise<void>(r => loadLazyChunksResolve = r);
|
||||
const { promise: loadLazyChunksDone, resolve: loadLazyChunksResolve } = Promise.withResolvers<void>();
|
||||
|
||||
// The main patch for starting the reporter chunk loading
|
||||
addPatch({
|
||||
find: '"Could not find app-mount"',
|
||||
replacement: {
|
||||
match: /(?<="use strict";)/,
|
||||
replace: "Vencord.Webpack._initReporter();"
|
||||
}
|
||||
}, "Vencord Reporter");
|
||||
|
||||
// @ts-ignore
|
||||
Vencord.Webpack._initReporter = function () {
|
||||
// initReporter is called in the patched entry point of Discord
|
||||
// setImmediate to only start searching for lazy chunks after Discord initialized the app
|
||||
setTimeout(() => loadLazyChunks().then(loadLazyChunksResolve), 0);
|
||||
};
|
||||
|
||||
Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve)));
|
||||
await loadLazyChunksDone;
|
||||
|
||||
if (IS_REPORTER && IS_WEB && !IS_VESKTOP) {
|
||||
console.log("[REPORTER_META]", {
|
||||
buildNumber: getBuildNumber(),
|
||||
buildHash: window.GLOBAL_ENV.SENTRY_TAGS.buildId
|
||||
});
|
||||
}
|
||||
|
||||
for (const patch of patches) {
|
||||
if (!patch.all) {
|
||||
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [plugin, moduleId, match, totalTime] of patchTimings) {
|
||||
if (totalTime > 5) {
|
||||
new Logger("WebpackInterceptor").warn(`Patch by ${plugin} took ${Math.round(totalTime * 100) / 100}ms (Module id is ${String(moduleId)}): ${match}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [searchType, args] of Webpack.lazyWebpackSearchHistory) {
|
||||
let method = searchType;
|
||||
|
||||
|
@ -50,9 +78,9 @@ async function runReporter() {
|
|||
result = await Webpack.extractAndLoadChunks(code, matcher);
|
||||
if (result === false) result = null;
|
||||
} else if (method === "mapMangledModule") {
|
||||
const [code, mapper] = args;
|
||||
const [code, mapper, includeBlacklistedExports] = args;
|
||||
|
||||
result = Webpack.mapMangledModule(code, mapper);
|
||||
result = Webpack.mapMangledModule(code, mapper, includeBlacklistedExports);
|
||||
if (Object.keys(result).length !== Object.keys(mapper).length) throw new Error("Webpack Find Fail");
|
||||
} else {
|
||||
// @ts-ignore
|
||||
|
@ -62,14 +90,21 @@ async function runReporter() {
|
|||
if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw new Error("Webpack Find Fail");
|
||||
} catch (e) {
|
||||
let logMessage = searchType;
|
||||
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
|
||||
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
|
||||
else if (method === "mapMangledModule") {
|
||||
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
|
||||
if (args[0].$$vencordProps != null) {
|
||||
logMessage += `(${args[0].$$vencordProps.map(arg => `"${arg}"`).join(", ")})`;
|
||||
} else {
|
||||
logMessage += `(${args[0].toString().slice(0, 147)}...)`;
|
||||
}
|
||||
} else if (method === "extractAndLoadChunks") {
|
||||
logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
|
||||
} else if (method === "mapMangledModule") {
|
||||
const failedMappings = Object.keys(args[1]).filter(key => result?.[key] == null);
|
||||
|
||||
logMessage += `("${args[0]}", {\n${failedMappings.map(mapping => `\t${mapping}: ${args[1][mapping].toString().slice(0, 147)}...`).join(",\n")}\n})`;
|
||||
} else {
|
||||
logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
|
||||
}
|
||||
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
|
||||
|
||||
ReporterLogger.log("Webpack Find Fail:", logMessage);
|
||||
}
|
||||
|
@ -81,4 +116,6 @@ async function runReporter() {
|
|||
}
|
||||
}
|
||||
|
||||
runReporter();
|
||||
// Run after the Vencord object has been created.
|
||||
// We need to add extra properties to it, and it is only created after all of Vencord code has ran
|
||||
setTimeout(runReporter, 0);
|
||||
|
|
7
src/globals.d.ts
vendored
7
src/globals.d.ts
vendored
|
@ -64,13 +64,8 @@ declare global {
|
|||
export var Vesktop: any;
|
||||
export var VesktopNative: any;
|
||||
|
||||
interface Window {
|
||||
webpackChunkdiscord_app: {
|
||||
push(chunk: any): any;
|
||||
pop(): any;
|
||||
};
|
||||
interface Window extends Record<PropertyKey, any> {
|
||||
_: LoDashStatic;
|
||||
[k: string]: any;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
2
src/modules.d.ts
vendored
2
src/modules.d.ts
vendored
|
@ -25,7 +25,7 @@ declare module "~plugins" {
|
|||
folderName: string;
|
||||
userPlugin: boolean;
|
||||
}>;
|
||||
export const ExcludedPlugins: Record<string, "web" | "discordDesktop" | "vencordDesktop" | "desktop" | "dev">;
|
||||
export const ExcludedPlugins: Record<string, "web" | "discordDesktop" | "vesktop" | "desktop" | "dev">;
|
||||
}
|
||||
|
||||
declare module "~pluginNatives" {
|
||||
|
|
|
@ -28,7 +28,7 @@ import { Devs } from "@utils/constants";
|
|||
import { Logger } from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { isPluginDev } from "@utils/misc";
|
||||
import { closeModal, Modals, openModal } from "@utils/modal";
|
||||
import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Forms, Toasts, UserStore } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
|
@ -65,27 +65,25 @@ export default definePlugin({
|
|||
{
|
||||
find: ".FULL_SIZE]:26",
|
||||
replacement: {
|
||||
match: /(?<=(\i)=\(0,\i\.\i\)\(\i\);)return 0===\i.length\?/,
|
||||
replace: "$1.unshift(...$self.getBadges(arguments[0].displayProfile));$&"
|
||||
match: /(?=;return 0===(\i)\.length\?)(?<=(\i)\.useMemo.+?)/,
|
||||
replace: ";$1=$2.useMemo(()=>[...$self.getBadges(arguments[0].displayProfile),...$1],[$1])"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: ".description,delay:",
|
||||
find: "#{intl::PROFILE_USER_BADGES}",
|
||||
replacement: [
|
||||
{
|
||||
// alt: "", aria-hidden: false, src: originalSrc
|
||||
match: /alt:" ","aria-hidden":!0,src:(?=.{0,20}(\i)\.icon)/,
|
||||
// ...badge.props, ..., src: badge.image ?? ...
|
||||
replace: "...$1.props,$& $1.image??"
|
||||
match: /(alt:" ","aria-hidden":!0,src:)(.+?)(?=,)(?<=href:(\i)\.link.+?)/,
|
||||
replace: (_, rest, originalSrc, badge) => `...${badge}.props,${rest}${badge}.image??(${originalSrc})`
|
||||
},
|
||||
{
|
||||
match: /(?<="aria-label":(\i)\.description,.{0,200})children:/,
|
||||
replace: "children:$1.component ? $self.renderBadgeComponent({ ...$1 }) :"
|
||||
replace: "children:$1.component?$self.renderBadgeComponent({...$1}) :"
|
||||
},
|
||||
// conditionally override their onClick with badge.onClick if it exists
|
||||
{
|
||||
match: /href:(\i)\.link/,
|
||||
replace: "...($1.onClick && { onClick: vcE => $1.onClick(vcE, $1) }),$&"
|
||||
replace: "...($1.onClick&&{onClick:vcE=>$1.onClick(vcE,$1)}),$&"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -144,8 +142,8 @@ export default definePlugin({
|
|||
closeModal(modalKey);
|
||||
VencordNative.native.openExternal("https://github.com/sponsors/Vendicated");
|
||||
}}>
|
||||
<Modals.ModalRoot {...props}>
|
||||
<Modals.ModalHeader>
|
||||
<ModalRoot {...props}>
|
||||
<ModalHeader>
|
||||
<Flex style={{ width: "100%", justifyContent: "center" }}>
|
||||
<Forms.FormTitle
|
||||
tag="h2"
|
||||
|
@ -159,8 +157,8 @@ export default definePlugin({
|
|||
Vencord Donor
|
||||
</Forms.FormTitle>
|
||||
</Flex>
|
||||
</Modals.ModalHeader>
|
||||
<Modals.ModalContent>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Flex>
|
||||
<img
|
||||
role="presentation"
|
||||
|
@ -183,13 +181,13 @@ export default definePlugin({
|
|||
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
|
||||
</Forms.FormText>
|
||||
</div>
|
||||
</Modals.ModalContent>
|
||||
<Modals.ModalFooter>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Flex style={{ width: "100%", justifyContent: "center" }}>
|
||||
<DonateButton />
|
||||
</Flex>
|
||||
</Modals.ModalFooter>
|
||||
</Modals.ModalRoot>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
</ErrorBoundary>
|
||||
));
|
||||
},
|
||||
|
|
|
@ -12,11 +12,16 @@ export default definePlugin({
|
|||
description: "API to add buttons to the chat input",
|
||||
authors: [Devs.Ven],
|
||||
|
||||
patches: [{
|
||||
find: '"sticker")',
|
||||
replacement: {
|
||||
match: /return\(!\i\.\i&&(?=\(\i\.isDM.+?(\i)\.push\(.{0,50}"gift")/,
|
||||
replace: "$&(Vencord.Api.ChatButtons._injectButtons($1,arguments[0]),true)&&"
|
||||
patches: [
|
||||
{
|
||||
find: '"sticker")',
|
||||
replacement: {
|
||||
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
|
||||
match: /return\((!)?\i\.\i(?:\|\||&&)(?=\(.+?(\i)\.push)/,
|
||||
replace: (m, not, children) => not
|
||||
? `${m}(Vencord.Api.ChatButtons._injectButtons(${children},arguments[0]),true)&&`
|
||||
: `${m}(Vencord.Api.ChatButtons._injectButtons(${children},arguments[0]),false)||`
|
||||
}
|
||||
}
|
||||
}]
|
||||
]
|
||||
});
|
||||
|
|
|
@ -34,12 +34,22 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
{
|
||||
find: ".Menu,{",
|
||||
find: "navId:",
|
||||
all: true,
|
||||
replacement: {
|
||||
match: /Menu,{(?<=\.jsxs?\)\(\i\.Menu,{)/g,
|
||||
replace: "$&contextMenuApiArguments:typeof arguments!=='undefined'?arguments:[],"
|
||||
}
|
||||
noWarn: true,
|
||||
replacement: [
|
||||
{
|
||||
match: /navId:(?=.+?([,}].*?\)))/g,
|
||||
replace: (m, rest) => {
|
||||
// Check if this navId: match is a destructuring statement, ignore it if it is
|
||||
const destructuringMatch = rest.match(/}=.+/);
|
||||
if (destructuringMatch == null) {
|
||||
return `contextMenuAPIArguments:typeof arguments!=='undefined'?arguments:[],${m}`;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
@ -14,10 +14,17 @@ export default definePlugin({
|
|||
description: "Allows you to omit either width or height when opening an image modal",
|
||||
patches: [
|
||||
{
|
||||
find: "SCALE_DOWN:",
|
||||
find: ".contain,SCALE_DOWN:",
|
||||
replacement: {
|
||||
match: /!\(null==(\i)\|\|0===\i\|\|null==(\i)\|\|0===\i\)/,
|
||||
replace: (_, width, height) => `!((null == ${width} || 0 === ${width}) && (null == ${height} || 0 === ${height}))`
|
||||
match: /(?<="IMAGE"===\i\?)\i(?=\?)/,
|
||||
replace: "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: ".dimensionlessImage,",
|
||||
replacement: {
|
||||
match: /(?<="IMAGE"===\i&&\(\i=)\i(?=\?)/,
|
||||
replace: "true"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -19,10 +19,15 @@
|
|||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
import managedStyle from "./style.css?managed";
|
||||
|
||||
export default definePlugin({
|
||||
name: "MemberListDecoratorsAPI",
|
||||
description: "API to add decorators to member list (both in servers and DMs)",
|
||||
authors: [Devs.TheSun, Devs.Ven],
|
||||
|
||||
managedStyle,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: ".lostPermission)",
|
||||
|
@ -32,7 +37,7 @@ export default definePlugin({
|
|||
replace: "$&vencordProps=$1,"
|
||||
}, {
|
||||
match: /#{intl::GUILD_OWNER}(?=.+?decorators:(\i)\(\)).+?\1=?\(\)=>.+?children:\[/,
|
||||
replace: "$&...(typeof vencordProps=='undefined'?[]:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps)),"
|
||||
replace: "$&(typeof vencordProps=='undefined'?null:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps)),"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -40,8 +45,8 @@ export default definePlugin({
|
|||
find: "PrivateChannel.renderAvatar",
|
||||
replacement: {
|
||||
match: /decorators:(\i\.isSystemDM\(\))\?(.+?):null/,
|
||||
replace: "decorators:[...Vencord.Api.MemberListDecorators.__getDecorators(arguments[0]), $1?$2:null]"
|
||||
replace: "decorators:[Vencord.Api.MemberListDecorators.__getDecorators(arguments[0]),$1?$2:null]"
|
||||
}
|
||||
}
|
||||
],
|
||||
]
|
||||
});
|
11
src/plugins/_api/memberListDecorators/style.css
Normal file
11
src/plugins/_api/memberListDecorators/style.css
Normal file
|
@ -0,0 +1,11 @@
|
|||
.vc-member-list-decorators-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.vc-member-list-decorators-wrapper:not(:empty) {
|
||||
/* Margin to match default Discord decorators */
|
||||
margin-left: 0.25em;
|
||||
}
|
68
src/plugins/_api/menuItemDemangler.ts
Normal file
68
src/plugins/_api/menuItemDemangler.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import { canonicalizeMatch } from "@utils/patches";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
// duplicate values have multiple branches with different types. Just include all to be safe
|
||||
const nameMap = {
|
||||
radio: "MenuRadioItem",
|
||||
separator: "MenuSeparator",
|
||||
checkbox: "MenuCheckboxItem",
|
||||
groupstart: "MenuGroup",
|
||||
|
||||
control: "MenuControlItem",
|
||||
compositecontrol: "MenuControlItem",
|
||||
|
||||
item: "MenuItem",
|
||||
customitem: "MenuItem",
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "MenuItemDemanglerAPI",
|
||||
description: "Demangles Discord's Menu Item module",
|
||||
authors: [Devs.Ven],
|
||||
required: true,
|
||||
patches: [
|
||||
{
|
||||
find: '"Menu API',
|
||||
replacement: {
|
||||
match: /function.{0,80}type===(\i\.\i)\).{0,50}navigable:.+?Menu API/s,
|
||||
replace: (m, mod) => {
|
||||
const nameAssignments = [] as string[];
|
||||
|
||||
// if (t.type === m.MenuItem)
|
||||
const typeCheckRe = canonicalizeMatch(/\(\i\.type===(\i\.\i)\)/g);
|
||||
// push({type:"item"})
|
||||
const pushTypeRe = /type:"(\w+)"/g;
|
||||
|
||||
let typeMatch: RegExpExecArray | null;
|
||||
// for each if (t.type === ...)
|
||||
while ((typeMatch = typeCheckRe.exec(m)) !== null) {
|
||||
// extract the current menu item
|
||||
const item = typeMatch[1];
|
||||
// Set the starting index of the second regex to that of the first to start
|
||||
// matching from after the if
|
||||
pushTypeRe.lastIndex = typeCheckRe.lastIndex;
|
||||
// extract the first type: "..."
|
||||
const type = pushTypeRe.exec(m)?.[1];
|
||||
if (type && type in nameMap) {
|
||||
const name = nameMap[type];
|
||||
nameAssignments.push(`Object.defineProperty(${item},"name",{value:"${name}"})`);
|
||||
}
|
||||
}
|
||||
if (nameAssignments.length < 6) {
|
||||
console.warn("[MenuItemDemanglerAPI] Expected to at least remap 6 items, only remapped", nameAssignments.length);
|
||||
}
|
||||
|
||||
// Merge all our redefines with the actual module
|
||||
return `${nameAssignments.join(";")};${m}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
|
@ -19,17 +19,22 @@
|
|||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
import managedStyle from "./style.css?managed";
|
||||
|
||||
export default definePlugin({
|
||||
name: "MessageDecorationsAPI",
|
||||
description: "API to add decorations to messages",
|
||||
authors: [Devs.TheSun],
|
||||
|
||||
managedStyle,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: '"Message Username"',
|
||||
replacement: {
|
||||
match: /#{intl::GUILD_COMMUNICATION_DISABLED_BOTTOM_SHEET_TITLE}.+?}\),\i(?=\])/,
|
||||
replace: "$&,...Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0])"
|
||||
match: /#{intl::GUILD_COMMUNICATION_DISABLED_BOTTOM_SHEET_TITLE}.+?renderPopout:.+?(?=\])/,
|
||||
replace: "$&,Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0])"
|
||||
}
|
||||
}
|
||||
],
|
||||
]
|
||||
});
|
18
src/plugins/_api/messageDecorations/style.css
Normal file
18
src/plugins/_api/messageDecorations/style.css
Normal file
|
@ -0,0 +1,18 @@
|
|||
.vc-message-decorations-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.vc-message-decorations-wrapper:not(:empty) {
|
||||
/* Margin to match default Discord decorators */
|
||||
margin-left: 0.25em;
|
||||
|
||||
/* Align vertically */
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
top: 0.1rem;
|
||||
height: calc(1rem + 4px);
|
||||
max-height: calc(1rem + 4px)
|
||||
}
|
|
@ -37,12 +37,9 @@ export default definePlugin({
|
|||
{
|
||||
find: ".handleSendMessage,onResize",
|
||||
replacement: {
|
||||
// props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply);
|
||||
// Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid)
|
||||
match: /(\{openWarningPopout:.{0,100}type:this.props.chatInputType.+?\.then\()(\i=>\{.+?let (\i)=\i\.\i\.parse\((\i),.+?let (\i)=\i\.\i\.getSendMessageOptions\(\{.+?\}\);)(?<=\)\(({.+?})\)\.then.+?)/,
|
||||
// props.chatInputType...then((async function(isMessageValid)... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply); if(await Vencord.api...) return { shoudClear:true, shouldRefocus:true };
|
||||
replace: (_, rest1, rest2, parsedMessage, channel, replyOptions, extra) => "" +
|
||||
`${rest1}async ${rest2}` +
|
||||
// https://regex101.com/r/hBlXpl/1
|
||||
match: /let (\i)=\i\.\i\.parse\((\i),.+?let (\i)=\i\.\i\.getSendMessageOptions\(\{.+?\}\);(?<=\)\(({.+?})\)\.then.+?)/,
|
||||
replace: (m, parsedMessage, channel, replyOptions, extra) => m +
|
||||
`if(await Vencord.Api.MessageEvents._handlePreSend(${channel}.id,${parsedMessage},${extra},${replyOptions}))` +
|
||||
"return{shouldClear:false,shouldRefocus:true};"
|
||||
}
|
||||
|
@ -52,8 +49,7 @@ export default definePlugin({
|
|||
replacement: {
|
||||
match: /let\{id:\i}=(\i),{id:\i}=(\i);return \i\.useCallback\((\i)=>\{/,
|
||||
replace: (m, message, channel, event) =>
|
||||
// the message param is shadowed by the event param, so need to alias them
|
||||
`const vcMsg=${message},vcChan=${channel};${m}Vencord.Api.MessageEvents._handleClick(vcMsg, vcChan, ${event});`
|
||||
`const vcMsg=${message},vcChan=${channel};${m}Vencord.Api.MessageEvents._handleClick(vcMsg,vcChan,${event});`
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -20,6 +20,7 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
||||
import { WebpackRequire } from "@webpack/wreq.d";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
disableAnalytics: {
|
||||
|
@ -81,9 +82,9 @@ export default definePlugin({
|
|||
Object.defineProperty(Function.prototype, "g", {
|
||||
configurable: true,
|
||||
|
||||
set(v: any) {
|
||||
set(this: WebpackRequire, globalObj: WebpackRequire["g"]) {
|
||||
Object.defineProperty(this, "g", {
|
||||
value: v,
|
||||
value: globalObj,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
writable: true
|
||||
|
@ -92,11 +93,11 @@ export default definePlugin({
|
|||
// Ensure this is most likely the Sentry WebpackInstance.
|
||||
// Function.g is a very generic property and is not uncommon for another WebpackInstance (or even a React component: <g></g>) to include it
|
||||
const { stack } = new Error();
|
||||
if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || !String(this).includes("exports:{}") || this.c != null) {
|
||||
if (this.c != null || !stack?.includes("http") || !String(this).includes("exports:{}")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assetPath = stack?.match(/\/assets\/.+?\.js/)?.[0];
|
||||
const assetPath = stack.match(/http.+?(?=:\d+?:\d+?$)/m)?.[0];
|
||||
if (!assetPath) {
|
||||
return;
|
||||
}
|
||||
|
@ -106,7 +107,8 @@ export default definePlugin({
|
|||
srcRequest.send();
|
||||
|
||||
// Final condition to see if this is the Sentry WebpackInstance
|
||||
if (!srcRequest.responseText.includes("window.DiscordSentry=")) {
|
||||
// This is matching window.DiscordSentry=, but without `window` to avoid issues on some proxies
|
||||
if (!srcRequest.responseText.includes(".DiscordSentry=")) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,8 @@ export default definePlugin({
|
|||
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
|
||||
},
|
||||
{
|
||||
match: /({(?=.+?function (\i).{0,160}(\i)=\i\.useMemo.{0,140}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/,
|
||||
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
|
||||
match: /({(?=.+?function (\i).{0,160}(\i)=\i\.useMemo.{0,140}return \i\.useMemo\(\(\)=>\i\(\3).+?(?:function\(\){return |\(\)=>))\2/,
|
||||
replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})`
|
||||
}
|
||||
]
|
||||
|
@ -157,6 +158,9 @@ export default definePlugin({
|
|||
aboveActivity: getIntlMessage("ACTIVITY_SETTINGS")
|
||||
};
|
||||
|
||||
if (!names[settingsLocation] || names[settingsLocation].endsWith("_SETTINGS"))
|
||||
return firstChild === "PREMIUM";
|
||||
|
||||
return header === names[settingsLocation];
|
||||
} catch {
|
||||
return firstChild === "PREMIUM";
|
||||
|
|
|
@ -22,7 +22,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||
import { Flex } from "@components/Flex";
|
||||
import { Link } from "@components/Link";
|
||||
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
|
||||
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
|
||||
import { CONTRIB_ROLE_ID, Devs, DONOR_ROLE_ID, KNOWN_ISSUES_CHANNEL_ID, REGULAR_ROLE_ID, SUPPORT_CATEGORY_ID, SUPPORT_CHANNEL_ID, VENBOT_USER_ID, VENCORD_GUILD_ID } from "@utils/constants";
|
||||
import { sendMessage } from "@utils/discord";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
|
@ -32,7 +32,8 @@ import { onlyOnce } from "@utils/onlyOnce";
|
|||
import { makeCodeblock } from "@utils/text";
|
||||
import definePlugin from "@utils/types";
|
||||
import { checkForUpdates, isOutdated, update } from "@utils/updater";
|
||||
import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";
|
||||
import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, PermissionsBits, PermissionStore, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";
|
||||
import { Channel } from "discord-types/general";
|
||||
import { JSX } from "react";
|
||||
|
||||
import gitHash from "~git-hash";
|
||||
|
@ -40,27 +41,24 @@ import plugins, { PluginMeta } from "~plugins";
|
|||
|
||||
import SettingsPlugin from "./settings";
|
||||
|
||||
const VENCORD_GUILD_ID = "1015060230222131221";
|
||||
const VENBOT_USER_ID = "1017176847865352332";
|
||||
const KNOWN_ISSUES_CHANNEL_ID = "1222936386626129920";
|
||||
const CodeBlockRe = /```js\n(.+?)```/s;
|
||||
|
||||
const AllowedChannelIds = [
|
||||
SUPPORT_CHANNEL_ID,
|
||||
const AdditionalAllowedChannelIds = [
|
||||
"1024286218801926184", // Vencord > #bot-spam
|
||||
"1033680203433660458", // Vencord > #v
|
||||
];
|
||||
|
||||
const TrustedRolesIds = [
|
||||
"1026534353167208489", // contributor
|
||||
"1026504932959977532", // regular
|
||||
"1042507929485586532", // donor
|
||||
CONTRIB_ROLE_ID, // contributor
|
||||
REGULAR_ROLE_ID, // regular
|
||||
DONOR_ROLE_ID, // donor
|
||||
];
|
||||
|
||||
const AsyncFunction = async function () { }.constructor;
|
||||
|
||||
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
|
||||
|
||||
const isSupportAllowedChannel = (channel: Channel) => channel.parent_id === SUPPORT_CATEGORY_ID || AdditionalAllowedChannelIds.includes(channel.id);
|
||||
|
||||
async function forceUpdate() {
|
||||
const outdated = await checkForUpdates();
|
||||
if (outdated) {
|
||||
|
@ -158,20 +156,21 @@ export default definePlugin({
|
|||
{
|
||||
name: "vencord-debug",
|
||||
description: "Send Vencord debug info",
|
||||
predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || AllowedChannelIds.includes(ctx.channel.id),
|
||||
predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || isSupportAllowedChannel(ctx.channel),
|
||||
execute: async () => ({ content: await generateDebugInfoMessage() })
|
||||
},
|
||||
{
|
||||
name: "vencord-plugins",
|
||||
description: "Send Vencord plugin list",
|
||||
predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || AllowedChannelIds.includes(ctx.channel.id),
|
||||
predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || isSupportAllowedChannel(ctx.channel),
|
||||
execute: () => ({ content: generatePluginList() })
|
||||
}
|
||||
],
|
||||
|
||||
flux: {
|
||||
async CHANNEL_SELECT({ channelId }) {
|
||||
if (channelId !== SUPPORT_CHANNEL_ID) return;
|
||||
const isSupportChannel = channelId === SUPPORT_CHANNEL_ID || ChannelStore.getChannel(channelId)?.parent_id === SUPPORT_CATEGORY_ID;
|
||||
if (!isSupportChannel) return;
|
||||
|
||||
const selfId = UserStore.getCurrentUser()?.id;
|
||||
if (!selfId || isPluginDev(selfId)) return;
|
||||
|
@ -242,7 +241,7 @@ export default definePlugin({
|
|||
!IS_UPDATER_DISABLED
|
||||
&& (
|
||||
(props.channel.id === KNOWN_ISSUES_CHANNEL_ID) ||
|
||||
(props.channel.id === SUPPORT_CHANNEL_ID && props.message.author.id === VENBOT_USER_ID)
|
||||
(props.channel.parent_id === SUPPORT_CATEGORY_ID && props.message.author.id === VENBOT_USER_ID)
|
||||
)
|
||||
&& props.message.content?.includes("update");
|
||||
|
||||
|
@ -268,7 +267,7 @@ export default definePlugin({
|
|||
);
|
||||
}
|
||||
|
||||
if (props.channel.id === SUPPORT_CHANNEL_ID) {
|
||||
if (props.channel.parent_id === SUPPORT_CATEGORY_ID && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel)) {
|
||||
if (props.message.content.includes("/vencord-debug") || props.message.content.includes("/vencord-plugins")) {
|
||||
buttons.push(
|
||||
<Button
|
||||
|
|
|
@ -73,19 +73,19 @@ export default definePlugin({
|
|||
group: true,
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=\.AVATAR_SIZE\);)/,
|
||||
replace: "$self.useAccountPanelRef();"
|
||||
match: /let{speaking:\i/,
|
||||
replace: "$self.useAccountPanelRef();$&"
|
||||
},
|
||||
{
|
||||
match: /(\.AVATAR,children:.+?renderPopout:(\i)=>){(.+?)}(?=,position)(?<=currentUser:(\i).+?)/,
|
||||
replace: (_, rest, popoutProps, originalPopout, currentUser) => `${rest}$self.UserProfile({popoutProps:${popoutProps},currentUser:${currentUser},originalRenderPopout:()=>{${originalPopout}}})`
|
||||
},
|
||||
{
|
||||
match: /\.AVATAR,children:.+?(?=renderPopout:)/,
|
||||
replace: "$&onRequestClose:$self.onPopoutClose,"
|
||||
match: /\.AVATAR,children:.+?onRequestClose:\(\)=>\{/,
|
||||
replace: "$&$self.onPopoutClose();"
|
||||
},
|
||||
{
|
||||
match: /(?<=\.avatarWrapper,)/,
|
||||
match: /(?<=#{intl::SET_STATUS}\),)/,
|
||||
replace: "ref:$self.accountPanelRef,onContextMenu:$self.openAccountPanelContextMenu,"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -43,16 +43,16 @@ export default definePlugin({
|
|||
// Status emojis
|
||||
find: "#{intl::GUILD_OWNER}),children:",
|
||||
replacement: {
|
||||
match: /(?<=\.activityEmoji,.+?animate:)\i/,
|
||||
replace: "!0"
|
||||
match: /(\.CUSTOM_STATUS.+?animate:)\i/,
|
||||
replace: "$1!0"
|
||||
}
|
||||
},
|
||||
{
|
||||
// Guild Banner
|
||||
find: ".animatedBannerHoverLayer,onMouseEnter:",
|
||||
replacement: {
|
||||
match: /(?<=guildBanner:\i,animate:)\i(?=}\))/,
|
||||
replace: "!0"
|
||||
match: /(\.headerContent.+?guildBanner:\i,animate:)\i/,
|
||||
replace: "$1!0"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -23,7 +23,7 @@ import definePlugin, { ReporterTestable } from "@utils/types";
|
|||
import { findByCodeLazy } from "@webpack";
|
||||
import { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from "@webpack/common";
|
||||
|
||||
const fetchApplicationsRPC = findByCodeLazy("APPLICATION_RPC(", "Client ID");
|
||||
const fetchApplicationsRPC = findByCodeLazy('"Invalid Origin"', ".application");
|
||||
|
||||
async function lookupAsset(applicationId: string, key: string): Promise<string> {
|
||||
return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];
|
||||
|
|
|
@ -17,14 +17,13 @@
|
|||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { useStateFromStores } from "@webpack/common";
|
||||
import { findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Animations, useStateFromStores } from "@webpack/common";
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
import { ExpandedGuildFolderStore, settings } from ".";
|
||||
|
||||
const ChannelRTCStore = findStoreLazy("ChannelRTCStore");
|
||||
const Animations = findByPropsLazy("a", "animated", "useTransition");
|
||||
const GuildsBar = findComponentByCodeLazy('("guildsnav")');
|
||||
|
||||
export default ErrorBoundary.wrap(guildsBarProps => {
|
||||
|
@ -46,7 +45,8 @@ export default ErrorBoundary.wrap(guildsBarProps => {
|
|||
// Also display flex otherwise to fix scrolling
|
||||
const barStyle = {
|
||||
display: isFullscreen ? "none" : "flex",
|
||||
} as CSSProperties;
|
||||
gridArea: "betterFoldersSidebar"
|
||||
} satisfies CSSProperties;
|
||||
|
||||
if (!guilds || !settings.store.sidebarAnim) {
|
||||
return visible
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./sidebarFix.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getIntlMessage } from "@utils/discord";
|
||||
|
@ -173,8 +175,8 @@ export default definePlugin({
|
|||
// Disable expanding and collapsing folders transition in the normal GuildsBar sidebar
|
||||
{
|
||||
predicate: () => !settings.store.keepIcons,
|
||||
match: /(?<=#{intl::SERVER_FOLDER_PLACEHOLDER}.+?useTransition\)\()/,
|
||||
replace: "$self.shouldShowTransition(arguments[0])&&"
|
||||
match: /(?=,\{from:\{height)/,
|
||||
replace: "&&$self.shouldShowTransition(arguments[0])"
|
||||
},
|
||||
// If we are rendering the normal GuildsBar sidebar, we avoid rendering guilds from folders that are expanded
|
||||
{
|
||||
|
@ -185,25 +187,44 @@ export default definePlugin({
|
|||
{
|
||||
// Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar
|
||||
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
|
||||
match: /(?<=\.isExpanded\),children:\[)/,
|
||||
replace: "$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)&&"
|
||||
match: /\.isExpanded\),.{0,30}children:\[/,
|
||||
replace: "$&$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)&&"
|
||||
},
|
||||
{
|
||||
// Decide if we should render the expanded folder icon if we are rendering the Better Folders sidebar
|
||||
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
|
||||
match: /(?<=\.expandedFolderBackground.+?}\),)(?=\i,)/,
|
||||
replace: "!$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)?null:"
|
||||
},
|
||||
{
|
||||
// Discord adds a slight bottom margin of 4px when it's expanded
|
||||
// Which looks off when there's nothing open in the folder
|
||||
predicate: () => !settings.store.keepIcons,
|
||||
match: /(?=className:.{0,50}folderIcon)/,
|
||||
replace: "style:arguments[0]?.isBetterFolders?{}:{marginBottom:0},"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "APPLICATION_LIBRARY,render:",
|
||||
predicate: () => settings.store.sidebar,
|
||||
replacement: {
|
||||
// Render the Better Folders sidebar
|
||||
match: /(container.{0,50}({className:\i\.guilds,themeOverride:\i})\))/,
|
||||
replace: "$1,$self.FolderSideBar({...$2})"
|
||||
}
|
||||
group: true,
|
||||
replacement: [
|
||||
{
|
||||
// Render the Better Folders sidebar
|
||||
// Discord has two different places where they render the sidebar.
|
||||
// One is for visual refresh, one is not,
|
||||
// and each has a bunch of conditions &&ed in front of it.
|
||||
// Add the betterFolders sidebar to both, keeping the conditions Discord uses.
|
||||
match: /(?<=[[,])((?:!?\i&&)+)\(.{0,50}({className:\i\.guilds,themeOverride:\i})\)/g,
|
||||
replace: (m, conditions, props) => `${m},${conditions}$self.FolderSideBar(${props})`
|
||||
},
|
||||
{
|
||||
// Add grid styles to fix aligment with other visual refresh elements
|
||||
match: /(?<=className:)(\i\.base)(?=,)/,
|
||||
replace: "`${$self.gridStyle} ${$1}`"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "#{intl::DISCODO_DISABLED}",
|
||||
|
@ -257,6 +278,8 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
|
||||
gridStyle: "vc-betterFolders-sidebar-grid",
|
||||
|
||||
getGuildTree(isBetterFolders: boolean, originalTree: any, expandedFolderIds?: Set<any>) {
|
||||
return useMemo(() => {
|
||||
if (!isBetterFolders || expandedFolderIds == null) return originalTree;
|
||||
|
|
9
src/plugins/betterFolders/sidebarFix.css
Normal file
9
src/plugins/betterFolders/sidebarFix.css
Normal file
|
@ -0,0 +1,9 @@
|
|||
/* These area names need to be hardcoded. Only betterFoldersSidebar is added by the plugin. */
|
||||
|
||||
.visual-refresh .vc-betterFolders-sidebar-grid {
|
||||
grid-template-columns: [start] min-content [guildsEnd] min-content [sidebarEnd] min-content [channelsEnd] 1fr [end]; /* stylelint-disable-line value-keyword-case */
|
||||
grid-template-areas:
|
||||
"titleBar titleBar titleBar titleBar"
|
||||
"guildsList betterFoldersSidebar notice notice"
|
||||
"guildsList betterFoldersSidebar channelsList page";
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -21,7 +21,7 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
|
||||
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Constants, React, RestAPI, Tooltip } from "@webpack/common";
|
||||
|
||||
import { RenameButton } from "./components/RenameButton";
|
||||
|
@ -34,7 +34,7 @@ const UserSettingsModal = findByPropsLazy("saveAccountChanges", "open");
|
|||
const TimestampClasses = findByPropsLazy("timestampTooltip", "blockquoteContainer");
|
||||
const SessionIconClasses = findByPropsLazy("sessionIcon");
|
||||
|
||||
const BlobMask = findExportedComponentLazy("BlobMask");
|
||||
const BlobMask = findComponentByCodeLazy("!1,lowerBadgeSize:");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
backgroundCheck: {
|
||||
|
|
|
@ -101,8 +101,8 @@ export default definePlugin({
|
|||
find: 'minimal:"contentColumnMinimal"',
|
||||
replacement: [
|
||||
{
|
||||
match: /\(0,\i\.useTransition\)\((\i)/,
|
||||
replace: "(_cb=>_cb(void 0,$1))||$&"
|
||||
match: /(?=\(0,\i\.\i\)\((\i),\{from:\{position:"absolute")/,
|
||||
replace: "(_cb=>_cb(void 0,$1))||"
|
||||
},
|
||||
{
|
||||
match: /\i\.animated\.div/,
|
||||
|
@ -139,11 +139,12 @@ export default definePlugin({
|
|||
// This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary
|
||||
// without possibly also catching unrelated errors of children.
|
||||
//
|
||||
// Thus, we sanity check webpack modules & do this really hacky try catch to hopefully prevent hard crashes if something goes wrong.
|
||||
// try catch will only catch errors in the Layer function (hence why it's called as a plain function rather than a component), but
|
||||
// not in children
|
||||
// Thus, we sanity check webpack modules
|
||||
Layer(props: LayerProps) {
|
||||
if (!FocusLock || !ComponentDispatch || !Classes) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
[FocusLock.$$vencordInternal(), ComponentDispatch, Classes].forEach(e => e.test);
|
||||
} catch {
|
||||
new Logger("BetterSettings").error("Failed to find some components");
|
||||
return props.children;
|
||||
}
|
||||
|
|
|
@ -26,10 +26,18 @@ export default definePlugin({
|
|||
patches: [
|
||||
{
|
||||
find: '"ChannelAttachButton"',
|
||||
replacement: {
|
||||
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),.{0,30}?\.\.\.(\i),/,
|
||||
replace: "$&onClick:$1,onContextMenu:$2.onClick,",
|
||||
},
|
||||
replacement: [
|
||||
{
|
||||
// FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
|
||||
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),.{0,30}?\.\.\.(\i),/,
|
||||
replace: "$&onClick:$1,onContextMenu:$2.onClick,",
|
||||
noWarn: true
|
||||
},
|
||||
{
|
||||
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),.{0,100}\},(\i)\).{0,100}children:\i/,
|
||||
replace: "$&,onClick:$1,onContextMenu:$2.onClick,",
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -24,14 +24,14 @@ let style: HTMLStyleElement;
|
|||
|
||||
function setCss() {
|
||||
style.textContent = `
|
||||
.vc-nsfw-img [class^=imageWrapper] img,
|
||||
.vc-nsfw-img [class^=wrapperPaused] video {
|
||||
.vc-nsfw-img [class^=imageContainer],
|
||||
.vc-nsfw-img [class^=wrapperPaused] {
|
||||
filter: blur(${Settings.plugins.BlurNSFW.blurAmount}px);
|
||||
transition: filter 0.2s;
|
||||
}
|
||||
.vc-nsfw-img [class^=imageWrapper]:hover img,
|
||||
.vc-nsfw-img [class^=wrapperPaused]:hover video {
|
||||
filter: unset;
|
||||
|
||||
&:hover {
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ export default definePlugin({
|
|||
options: {
|
||||
blurAmount: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "Blur Amount",
|
||||
description: "Blur Amount (in pixels)",
|
||||
default: 10,
|
||||
onChange: setCss
|
||||
}
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
.client-theme-settings {
|
||||
.vc-clientTheme-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.client-theme-container {
|
||||
.vc-clientTheme-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.client-theme-settings-labels {
|
||||
.vc-clientTheme-labels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.client-theme-container > [class^="colorSwatch"] > [class^="swatch"] {
|
||||
.vc-clientTheme-container [class^="swatch"] {
|
||||
border: thin solid var(--background-modifier-accent) !important;
|
||||
}
|
||||
|
||||
.client-theme-warning * {
|
||||
.vc-clientTheme-warning-text {
|
||||
color: var(--text-danger);
|
||||
}
|
||||
|
||||
.client-theme-contrast-warning {
|
||||
.vc-clientTheme-contrast-warning {
|
||||
background-color: var(--background-primary);
|
||||
padding: 0.5rem;
|
||||
border-radius: .5rem;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import "./clientTheme.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
|
@ -14,6 +15,8 @@ import definePlugin, { OptionType, StartAt } from "@utils/types";
|
|||
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Button, Forms, ThemeStore, useStateFromStores } from "@webpack/common";
|
||||
|
||||
const cl = classNameFactory("vc-clientTheme-");
|
||||
|
||||
const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)");
|
||||
|
||||
const colorPresets = [
|
||||
|
@ -60,9 +63,9 @@ function ThemeSettings() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="client-theme-settings">
|
||||
<div className="client-theme-container">
|
||||
<div className="client-theme-settings-labels">
|
||||
<div className={cl("settings")}>
|
||||
<div className={cl("container")}>
|
||||
<div className={cl("settings-labels")}>
|
||||
<Forms.FormTitle tag="h3">Theme Color</Forms.FormTitle>
|
||||
<Forms.FormText>Add a color to your Discord client theme</Forms.FormText>
|
||||
</div>
|
||||
|
@ -76,10 +79,10 @@ function ThemeSettings() {
|
|||
{(contrastWarning || nitroThemeEnabled) && (<>
|
||||
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} />
|
||||
<div className={`client-theme-contrast-warning ${contrastWarning ? (isLightTheme ? "theme-dark" : "theme-light") : ""}`}>
|
||||
<div className="client-theme-warning">
|
||||
<Forms.FormText>Warning, your theme won't look good:</Forms.FormText>
|
||||
{contrastWarning && <Forms.FormText>Selected color won't contrast well with text</Forms.FormText>}
|
||||
{nitroThemeEnabled && <Forms.FormText>Nitro themes aren't supported</Forms.FormText>}
|
||||
<div className={cl("warning")}>
|
||||
<Forms.FormText className={cl("warning-text")}>Warning, your theme won't look good:</Forms.FormText>
|
||||
{contrastWarning && <Forms.FormText className={cl("warning-text")}>Selected color won't contrast well with text</Forms.FormText>}
|
||||
{nitroThemeEnabled && <Forms.FormText className={cl("warning-text")}>Nitro themes aren't supported</Forms.FormText>}
|
||||
</div>
|
||||
{(contrastWarning && fixableContrast) && <Button onClick={() => setTheme(oppositeTheme)} color={Button.Colors.RED}>Switch to {oppositeTheme} mode</Button>}
|
||||
{(nitroThemeEnabled) && <Button onClick={() => setTheme(theme)} color={Button.Colors.RED}>Disable Nitro Theme</Button>}
|
||||
|
@ -91,15 +94,12 @@ function ThemeSettings() {
|
|||
|
||||
const settings = definePluginSettings({
|
||||
color: {
|
||||
description: "Color your Discord client theme will be based around. Light mode isn't supported",
|
||||
type: OptionType.COMPONENT,
|
||||
default: "313338",
|
||||
component: () => <ThemeSettings />
|
||||
component: ThemeSettings
|
||||
},
|
||||
resetColor: {
|
||||
description: "Reset Theme Color",
|
||||
type: OptionType.COMPONENT,
|
||||
default: "313338",
|
||||
component: () => (
|
||||
<Button onClick={() => onPickColor(0x313338)}>
|
||||
Reset Theme Color
|
||||
|
@ -126,18 +126,20 @@ export default definePlugin({
|
|||
stop() {
|
||||
document.getElementById("clientThemeVars")?.remove();
|
||||
document.getElementById("clientThemeOffsets")?.remove();
|
||||
document.getElementById("clientThemeLightModeFixes")?.remove();
|
||||
}
|
||||
});
|
||||
|
||||
const variableRegex = /(--primary-\d{3}-hsl):.*?(\S*)%;/g;
|
||||
const visualRefreshVariableRegex = /(--neutral-\d{1,3}-hsl):.*?(\S*)%;/g;
|
||||
const oldVariableRegex = /(--primary-\d{3}-hsl):.*?(\S*)%;/g;
|
||||
const lightVariableRegex = /^--primary-[1-5]\d{2}-hsl/g;
|
||||
const darkVariableRegex = /^--primary-[5-9]\d{2}-hsl/g;
|
||||
|
||||
// generates variables per theme by:
|
||||
// - matching regex (so we can limit what variables are included in light/dark theme, otherwise text becomes unreadable)
|
||||
// - offset from specified center (light/dark theme get different offsets because light uses 100 for background-primary, while dark uses 600)
|
||||
function genThemeSpecificOffsets(variableLightness: Record<string, number>, regex: RegExp, centerVariable: string): string {
|
||||
return Object.entries(variableLightness).filter(([key]) => key.search(regex) > -1)
|
||||
function genThemeSpecificOffsets(variableLightness: Record<string, number>, regex: RegExp | null, centerVariable: string): string {
|
||||
return Object.entries(variableLightness).filter(([key]) => regex == null || key.search(regex) > -1)
|
||||
.map(([key, lightness]) => {
|
||||
const lightnessOffset = lightness - variableLightness[centerVariable];
|
||||
const plusOrMinus = lightnessOffset >= 0 ? "+" : "-";
|
||||
|
@ -146,25 +148,28 @@ function genThemeSpecificOffsets(variableLightness: Record<string, number>, rege
|
|||
.join("\n");
|
||||
}
|
||||
|
||||
|
||||
function generateColorOffsets(styles) {
|
||||
const variableLightness = {} as Record<string, number>;
|
||||
const oldVariableLightness = {} as Record<string, number>;
|
||||
const visualRefreshVariableLightness = {} as Record<string, number>;
|
||||
|
||||
// Get lightness values of --primary variables
|
||||
let variableMatch = variableRegex.exec(styles);
|
||||
while (variableMatch !== null) {
|
||||
const [, variable, lightness] = variableMatch;
|
||||
variableLightness[variable] = parseFloat(lightness);
|
||||
variableMatch = variableRegex.exec(styles);
|
||||
for (const [, variable, lightness] of styles.matchAll(oldVariableRegex)) {
|
||||
oldVariableLightness[variable] = parseFloat(lightness);
|
||||
}
|
||||
|
||||
for (const [, variable, lightness] of styles.matchAll(visualRefreshVariableRegex)) {
|
||||
visualRefreshVariableLightness[variable] = parseFloat(lightness);
|
||||
}
|
||||
|
||||
createStyleSheet("clientThemeOffsets", [
|
||||
`.theme-light {\n ${genThemeSpecificOffsets(variableLightness, lightVariableRegex, "--primary-345-hsl")} \n}`,
|
||||
`.theme-dark {\n ${genThemeSpecificOffsets(variableLightness, darkVariableRegex, "--primary-600-hsl")} \n}`,
|
||||
`.theme-light {\n ${genThemeSpecificOffsets(oldVariableLightness, lightVariableRegex, "--primary-345-hsl")} \n}`,
|
||||
`.theme-dark {\n ${genThemeSpecificOffsets(oldVariableLightness, darkVariableRegex, "--primary-600-hsl")} \n}`,
|
||||
`.visual-refresh.theme-light {\n ${genThemeSpecificOffsets(visualRefreshVariableLightness, null, "--neutral-2-hsl")} \n}`,
|
||||
`.visual-refresh.theme-dark {\n ${genThemeSpecificOffsets(visualRefreshVariableLightness, null, "--neutral-69-hsl")} \n}`,
|
||||
].join("\n\n"));
|
||||
}
|
||||
|
||||
function generateLightModeFixes(styles) {
|
||||
function generateLightModeFixes(styles: string) {
|
||||
const groupLightUsesW500Regex = /\.theme-light[^{]*\{[^}]*var\(--white-500\)[^}]*}/gm;
|
||||
// get light capturing groups that mention --white-500
|
||||
const relevantStyles = [...styles.matchAll(groupLightUsesW500Regex)].flat();
|
||||
|
|
|
@ -5,8 +5,11 @@
|
|||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { ErrorBoundary, Flex } from "@components/index";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
||||
import { Margins } from "@utils/margins";
|
||||
import definePlugin, { defineDefault, OptionType, StartAt } from "@utils/types";
|
||||
import { Checkbox, Forms, Text } from "@webpack/common";
|
||||
|
||||
const Noop = () => { };
|
||||
const NoopLogger = {
|
||||
|
@ -24,6 +27,48 @@ const NoopLogger = {
|
|||
|
||||
const logAllow = new Set();
|
||||
|
||||
interface AllowLevels {
|
||||
error: boolean;
|
||||
warn: boolean;
|
||||
trace: boolean;
|
||||
log: boolean;
|
||||
info: boolean;
|
||||
debug: boolean;
|
||||
}
|
||||
|
||||
interface AllowLevelSettingProps {
|
||||
settingKey: keyof AllowLevels;
|
||||
}
|
||||
|
||||
function AllowLevelSetting({ settingKey }: AllowLevelSettingProps) {
|
||||
const { allowLevel } = settings.use(["allowLevel"]);
|
||||
const value = allowLevel[settingKey];
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
value={value}
|
||||
onChange={(_, newValue) => settings.store.allowLevel[settingKey] = newValue}
|
||||
size={20}
|
||||
>
|
||||
<Text variant="text-sm/normal">{settingKey[0].toUpperCase() + settingKey.slice(1)}</Text>
|
||||
</Checkbox>
|
||||
);
|
||||
}
|
||||
|
||||
const AllowLevelSettings = ErrorBoundary.wrap(() => {
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle tag="h3">Filter List</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom8} type={Forms.FormText.Types.DESCRIPTION}>Always allow loggers of these types</Forms.FormText>
|
||||
<Flex flexDirection="row">
|
||||
{Object.keys(settings.store.allowLevel).map(key => (
|
||||
<AllowLevelSetting key={key} settingKey={key as keyof AllowLevels} />
|
||||
))}
|
||||
</Flex>
|
||||
</Forms.FormSection>
|
||||
);
|
||||
});
|
||||
|
||||
const settings = definePluginSettings({
|
||||
disableLoggers: {
|
||||
type: OptionType.BOOLEAN,
|
||||
|
@ -45,6 +90,18 @@ const settings = definePluginSettings({
|
|||
logAllow.clear();
|
||||
newVal.split(";").map(x => x.trim()).forEach(logAllow.add.bind(logAllow));
|
||||
}
|
||||
},
|
||||
allowLevel: {
|
||||
type: OptionType.COMPONENT,
|
||||
component: AllowLevelSettings,
|
||||
default: defineDefault<AllowLevels>({
|
||||
error: true,
|
||||
warn: false,
|
||||
trace: false,
|
||||
log: false,
|
||||
info: false,
|
||||
debug: false
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -60,17 +117,19 @@ export default definePlugin({
|
|||
this.settings.store.whitelistedLoggers?.split(";").map(x => x.trim()).forEach(logAllow.add.bind(logAllow));
|
||||
},
|
||||
|
||||
Noop,
|
||||
NoopLogger: () => NoopLogger,
|
||||
shouldLog(logger: string) {
|
||||
return logAllow.has(logger);
|
||||
|
||||
shouldLog(logger: string, level: keyof AllowLevels) {
|
||||
return logAllow.has(logger) || settings.store.allowLevel[level] === true;
|
||||
},
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "https://github.com/highlightjs/highlight.js/issues/2277",
|
||||
replacement: {
|
||||
match: /(?<=&&\()console.log\(`Deprecated.+?`\),/,
|
||||
replace: ""
|
||||
match: /\(console.log\(`Deprecated.+?`\),/,
|
||||
replace: "("
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -90,16 +149,15 @@ export default definePlugin({
|
|||
{
|
||||
find: "is not a valid locale.",
|
||||
replacement: {
|
||||
match: /\i\.error\(""\.concat\(\i," is not a valid locale."\)\);/,
|
||||
replace: ""
|
||||
match: /\i\.error(?=\(""\.concat\(\i," is not a valid locale."\)\))/,
|
||||
replace: "$self.Noop"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: 'console.warn("[DEPRECATED] Please use `subscribeWithSelector` middleware");',
|
||||
all: true,
|
||||
find: '"AppCrashedFatalReport: getLastCrash not supported."',
|
||||
replacement: {
|
||||
match: /console\.warn\("\[DEPRECATED\] Please use `subscribeWithSelector` middleware"\);/,
|
||||
replace: ""
|
||||
match: /console\.log(?=\("AppCrashedFatalReport: getLastCrash not supported\."\))/,
|
||||
replace: "$self.Noop"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -137,13 +195,13 @@ export default definePlugin({
|
|||
replace: ""
|
||||
}
|
||||
},
|
||||
// Patches discords generic logger function
|
||||
// Patches Discord generic logger function
|
||||
{
|
||||
find: "Σ:",
|
||||
predicate: () => settings.store.disableLoggers,
|
||||
replacement: {
|
||||
match: /(?<=&&)(?=console)/,
|
||||
replace: "$self.shouldLog(arguments[0])&&"
|
||||
replace: "$self.shouldLog(arguments[0],arguments[1])&&"
|
||||
}
|
||||
},
|
||||
{
|
|
@ -63,7 +63,7 @@ function makeShortcuts() {
|
|||
default:
|
||||
const uniqueMatches = [...new Set(matches)];
|
||||
if (uniqueMatches.length > 1)
|
||||
console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches);
|
||||
console.warn(`Warning: This filter matches ${uniqueMatches.length} exports. Make it more specific!\n`, uniqueMatches);
|
||||
|
||||
return matches[0];
|
||||
}
|
||||
|
@ -82,6 +82,8 @@ function makeShortcuts() {
|
|||
wp: Webpack,
|
||||
wpc: { getter: () => Webpack.cache },
|
||||
wreq: { getter: () => Webpack.wreq },
|
||||
wpPatcher: { getter: () => Vencord.WebpackPatcher },
|
||||
wpInstances: { getter: () => Vencord.WebpackPatcher.allWebpackInstances },
|
||||
wpsearch: search,
|
||||
wpex: extract,
|
||||
wpexs: (code: string) => extract(findModuleId(code)!),
|
||||
|
@ -151,13 +153,16 @@ function makeShortcuts() {
|
|||
openModal: { getter: () => ModalAPI.openModal },
|
||||
openModalLazy: { getter: () => ModalAPI.openModalLazy },
|
||||
|
||||
Stores: {
|
||||
getter: () => Object.fromEntries(
|
||||
Common.Flux.Store.getAll()
|
||||
.map(store => [store.getName(), store] as const)
|
||||
.filter(([name]) => name.length > 1)
|
||||
)
|
||||
}
|
||||
Stores: Webpack.fluxStores,
|
||||
|
||||
// e.g. "2024-05_desktop_visual_refresh", 0
|
||||
setExperiment: (id: string, bucket: number) => {
|
||||
Common.FluxDispatcher.dispatch({
|
||||
type: "EXPERIMENT_OVERRIDE_BUCKET",
|
||||
experimentId: id,
|
||||
experimentBucket: bucket,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -165,11 +170,38 @@ function loadAndCacheShortcut(key: string, val: any, forceLoad: boolean) {
|
|||
const currentVal = val.getter();
|
||||
if (!currentVal || val.preload === false) return currentVal;
|
||||
|
||||
const value = currentVal[SYM_LAZY_GET]
|
||||
? forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED]
|
||||
: currentVal;
|
||||
function unwrapProxy(value: any) {
|
||||
if (value[SYM_LAZY_GET]) {
|
||||
forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED];
|
||||
} else if (value.$$vencordInternal) {
|
||||
return forceLoad ? value.$$vencordInternal() : value;
|
||||
}
|
||||
|
||||
if (value) define(window.shortcutList, key, { value });
|
||||
return value;
|
||||
}
|
||||
|
||||
const value = unwrapProxy(currentVal);
|
||||
if (typeof value === "object" && value !== null) {
|
||||
const descriptors = Object.getOwnPropertyDescriptors(value);
|
||||
|
||||
for (const propKey in descriptors) {
|
||||
if (value[propKey] == null) continue;
|
||||
|
||||
const descriptor = descriptors[propKey];
|
||||
if (descriptor.writable === true || descriptor.set != null) {
|
||||
const currentValue = value[propKey];
|
||||
const newValue = unwrapProxy(currentValue);
|
||||
if (newValue != null && currentValue !== newValue) {
|
||||
value[propKey] = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
define(window.shortcutList, key, { value });
|
||||
define(window, key, { value });
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
|
|
@ -33,11 +33,11 @@ function getEmojiMarkdown(target: Target, copyUnicode: boolean): string {
|
|||
: `:${emojiName}:`;
|
||||
}
|
||||
|
||||
const extension = target?.firstChild.src.match(
|
||||
/https:\/\/cdn\.discordapp\.com\/emojis\/\d+\.(\w+)/
|
||||
)?.[1];
|
||||
const url = new URL(target.firstChild.src);
|
||||
const hasParam = url.searchParams.get("animated") === "true";
|
||||
const isGif = url.pathname.endsWith(".gif");
|
||||
|
||||
return `<${extension === "gif" ? "a" : ""}:${emojiName.replace(/~\d+$/, "")}:${emojiId}>`;
|
||||
return `<${(hasParam || isGif) ? "a" : ""}:${emojiName.replace(/~\d+$/, "")}:${emojiId}>`;
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
|
@ -55,7 +55,7 @@ export default definePlugin({
|
|||
settings,
|
||||
|
||||
contextMenus: {
|
||||
"expression-picker"(children, { target }: { target: Target }) {
|
||||
"expression-picker"(children, { target }: { target: Target; }) {
|
||||
if (target.dataset.type !== "emoji") return;
|
||||
|
||||
children.push(
|
||||
|
|
|
@ -173,6 +173,15 @@ export default definePlugin({
|
|||
} catch (err) {
|
||||
CrashHandlerLogger.debug("Failed to pop all layers.", err);
|
||||
}
|
||||
try {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "DEV_TOOLS_SETTINGS_UPDATE",
|
||||
settings: { displayTools: false, lastOpenTabId: "analytics" }
|
||||
});
|
||||
} catch (err) {
|
||||
CrashHandlerLogger.debug("Failed to close DevTools.", err);
|
||||
}
|
||||
|
||||
if (settings.store.attemptToNavigateToHome) {
|
||||
try {
|
||||
NavigationRouter.transitionToGuild("@me");
|
||||
|
@ -181,7 +190,6 @@ export default definePlugin({
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Set isRecovering to false before setting the state to allow us to handle the next crash error correcty, in case it happens
|
||||
setImmediate(() => isRecovering = false);
|
||||
|
||||
|
|
|
@ -42,10 +42,11 @@ export default definePlugin({
|
|||
// Only one of the two patches will be at effect; Discord often updates to switch between them.
|
||||
// See: https://discord.com/channels/1015060230222131221/1032770730703716362/1261398512017477673
|
||||
{
|
||||
find: ".ENTER&&(!",
|
||||
find: ".selectPreviousCommandOption(",
|
||||
replacement: {
|
||||
match: /(?<=(\i)\.which===\i\.\i.ENTER&&).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=&&\(\i\.preventDefault)/,
|
||||
replace: "$self.shouldSubmit($1, $2)"
|
||||
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
|
||||
match: /(?<=(\i)\.which(?:!==|===)\i\.\i.ENTER(\|\||&&)).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=(?:\|\||&&)\(\i\.preventDefault)/,
|
||||
replace: (_, event, condition, codeblock) => `${condition === "||" ? "!" : ""}$self.shouldSubmit(${event},${codeblock})`
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -47,19 +47,11 @@ export default definePlugin({
|
|||
{
|
||||
match: /\i\.\i\.dispatch\({type:"IDLE",idle:!1}\)/,
|
||||
replace: "$self.handleOnline()"
|
||||
},
|
||||
{
|
||||
match: /(setInterval\(\i,\.25\*)\i\.\i/,
|
||||
replace: "$1$self.getIntervalDelay()" // For web installs
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
getIntervalDelay() {
|
||||
return Math.min(6e5, this.getIdleTimeout());
|
||||
},
|
||||
|
||||
handleOnline() {
|
||||
if (!settings.store.remainInIdle) {
|
||||
FluxDispatcher.dispatch({
|
||||
|
|
|
@ -121,6 +121,7 @@ function DearrowButton({ component }: { component: Component<Props>; }) {
|
|||
height="24px"
|
||||
viewBox="0 0 36 36"
|
||||
aria-label="Toggle Dearrow"
|
||||
className="vc-dearrow-icon"
|
||||
>
|
||||
<path
|
||||
fill="#1213BD"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.vc-dearrow-toggle-off svg {
|
||||
.vc-dearrow-toggle-off .vc-dearrow-icon {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
|
|
|
@ -50,12 +50,24 @@ export default definePlugin({
|
|||
find: ".decorationGridItem,",
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<==)\i=>{let{children.{20,100}decorationGridItem/,
|
||||
replace: "$self.DecorationGridItem=$&"
|
||||
// FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
|
||||
match: /(?<==)\i=>{let{children.{20,200}decorationGridItem/,
|
||||
replace: "$self.DecorationGridItem=$&",
|
||||
noWarn: true
|
||||
},
|
||||
{
|
||||
// FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
|
||||
match: /(?<==)\i=>{let{user:\i,avatarDecoration/,
|
||||
replace: "$self.DecorationGridDecoration=$&"
|
||||
replace: "$self.DecorationGridDecoration=$&",
|
||||
noWarn: true
|
||||
},
|
||||
{
|
||||
match: /(?<==)\i=>{var{children.{20,200}decorationGridItem/,
|
||||
replace: "$self.DecorationGridItem=$&",
|
||||
},
|
||||
{
|
||||
match: /(?<==)\i=>{var{user:\i,avatarDecoration/,
|
||||
replace: "$self.DecorationGridDecoration=$&",
|
||||
},
|
||||
// Remove NEW label from decor avatar decorations
|
||||
{
|
||||
|
@ -87,7 +99,7 @@ export default definePlugin({
|
|||
},
|
||||
// Current user area, at bottom of channels/dm list
|
||||
{
|
||||
find: "renderAvatarWithPopout(){",
|
||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
||||
replacement: [
|
||||
// Use Decor avatar decoration hook
|
||||
{
|
||||
|
|
|
@ -17,7 +17,6 @@ import DecorSection from "./ui/components/DecorSection";
|
|||
export const settings = definePluginSettings({
|
||||
changeDecoration: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Change your avatar decoration",
|
||||
component() {
|
||||
if (!Vencord.Plugins.plugins.Decor.started) return <Forms.FormText>
|
||||
Enable Decor and restart your client to change your avatar decoration.
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { Flex } from "@components/Flex";
|
||||
import { findByCodeLazy } from "@webpack";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { Button, useEffect } from "@webpack/common";
|
||||
|
||||
import { useAuthorizationStore } from "../../lib/stores/AuthorizationStore";
|
||||
|
@ -13,7 +13,7 @@ import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDeco
|
|||
import { cl } from "../";
|
||||
import { openChangeDecorationModal } from "../modals/ChangeDecorationModal";
|
||||
|
||||
const CustomizationSection = findByCodeLazy(".customizationSectionBackground");
|
||||
const CustomizationSection = findComponentByCodeLazy(".customizationSectionBackground");
|
||||
|
||||
export interface DecorSectionProps {
|
||||
hideTitle?: boolean;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { CopyIcon, DeleteIcon } from "@components/Icons";
|
||||
import { Alerts, Clipboard, ContextMenuApi, Menu, UserStore } from "webpack/common";
|
||||
import { Alerts, Clipboard, ContextMenuApi, Menu, UserStore } from "@webpack/common";
|
||||
|
||||
import { Decoration } from "../../lib/api";
|
||||
import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore";
|
||||
|
|
|
@ -19,7 +19,7 @@ import { AvatarDecorationModalPreview } from "../components";
|
|||
|
||||
const FileUpload = findComponentByCodeLazy("fileUploadInput,");
|
||||
|
||||
const { HelpMessage, HelpMessageTypes } = mapMangledModuleLazy('POSITIVE=3]="POSITIVE', {
|
||||
const { HelpMessage, HelpMessageTypes } = mapMangledModuleLazy('POSITIVE="positive', {
|
||||
HelpMessageTypes: filters.byProps("POSITIVE", "WARNING", "INFO"),
|
||||
HelpMessage: filters.byCode(".iconDiv")
|
||||
});
|
||||
|
|
|
@ -160,7 +160,7 @@ function initWs(isManual = false) {
|
|||
return reply("Expected exactly one 'find' matches, found " + keys.length);
|
||||
|
||||
const mod = candidates[keys[0]];
|
||||
let src = String(mod.original ?? mod).replaceAll("\n", "");
|
||||
let src = String(mod).replaceAll("\n", "");
|
||||
|
||||
if (src.startsWith("function(")) {
|
||||
src = "0," + src;
|
||||
|
@ -173,7 +173,7 @@ function initWs(isManual = false) {
|
|||
|
||||
try {
|
||||
const matcher = canonicalizeMatch(parseNode(match));
|
||||
const replacement = canonicalizeReplace(parseNode(replace), "PlaceHolderPluginName");
|
||||
const replacement = canonicalizeReplace(parseNode(replace), 'Vencord.Plugins.plugins["PlaceHolderPluginName"]');
|
||||
|
||||
const newSource = src.replace(matcher, replacement as string);
|
||||
|
||||
|
|
|
@ -25,11 +25,14 @@ import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/moda
|
|||
import definePlugin from "@utils/types";
|
||||
import { findByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Constants, EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common";
|
||||
import { Guild } from "discord-types/general";
|
||||
import { Promisable } from "type-fest";
|
||||
|
||||
const StickersStore = findStoreLazy("StickersStore");
|
||||
const uploadEmoji = findByCodeLazy(".GUILD_EMOJIS(", "EMOJI_UPLOAD_START");
|
||||
|
||||
const getGuildMaxEmojiSlots = findByCodeLazy(".additionalEmojiSlots") as (guild: Guild) => number;
|
||||
|
||||
interface Sticker {
|
||||
t: "Sticker";
|
||||
description: string;
|
||||
|
@ -125,7 +128,7 @@ function getGuildCandidates(data: Data) {
|
|||
|
||||
const { isAnimated } = data as Emoji;
|
||||
|
||||
const emojiSlots = g.getMaxEmojiSlots();
|
||||
const emojiSlots = getGuildMaxEmojiSlots(g);
|
||||
const { emojis } = EmojiStore.getGuilds()[g.id];
|
||||
|
||||
let count = 0;
|
||||
|
|
|
@ -235,7 +235,7 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
{
|
||||
find: ".PREMIUM_LOCKED;",
|
||||
find: ".GUILD_SUBSCRIPTION_UNAVAILABLE;",
|
||||
group: true,
|
||||
predicate: () => settings.store.enableEmojiBypass,
|
||||
replacement: [
|
||||
|
@ -256,8 +256,11 @@ export default definePlugin({
|
|||
},
|
||||
{
|
||||
// Disallow the emoji for premium locked if the intention doesn't allow it
|
||||
match: /!\i\.\i\.canUseEmojisEverywhere\(\i\)/,
|
||||
replace: m => `(${m}&&!${IS_BYPASSEABLE_INTENTION})`
|
||||
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
|
||||
match: /(!)?(\i\.\i\.canUseEmojisEverywhere\(\i\))/,
|
||||
replace: (m, not) => not
|
||||
? `(${m}&&!${IS_BYPASSEABLE_INTENTION})`
|
||||
: `(${m}||${IS_BYPASSEABLE_INTENTION})`
|
||||
},
|
||||
{
|
||||
// Allow animated emojis to be used if the intention allows it
|
||||
|
|
|
@ -9,7 +9,7 @@ import { app } from "electron";
|
|||
|
||||
app.on("browser-window-created", (_, win) => {
|
||||
win.webContents.on("frame-created", (_, { frame }) => {
|
||||
frame.once("dom-ready", () => {
|
||||
frame?.once("dom-ready", () => {
|
||||
if (frame.url.startsWith("https://open.spotify.com/embed/")) {
|
||||
const settings = RendererSettings.store.plugins?.FixSpotifyEmbeds;
|
||||
if (!settings?.enabled) return;
|
||||
|
|
|
@ -9,7 +9,7 @@ import { app } from "electron";
|
|||
|
||||
app.on("browser-window-created", (_, win) => {
|
||||
win.webContents.on("frame-created", (_, { frame }) => {
|
||||
frame.once("dom-ready", () => {
|
||||
frame?.once("dom-ready", () => {
|
||||
if (frame.url.startsWith("https://www.youtube.com/")) {
|
||||
const settings = RendererSettings.store.plugins?.FixYoutubeEmbeds;
|
||||
if (!settings?.enabled) return;
|
||||
|
|
|
@ -31,7 +31,7 @@ export default definePlugin({
|
|||
{
|
||||
name: "create friend invite",
|
||||
description: "Generates a friend invite link.",
|
||||
inputType: ApplicationCommandInputType.BOT,
|
||||
inputType: ApplicationCommandInputType.BUILT_IN,
|
||||
|
||||
execute: async (args, ctx) => {
|
||||
const invite = await FriendInvites.createFriendInvite();
|
||||
|
@ -48,7 +48,7 @@ export default definePlugin({
|
|||
{
|
||||
name: "view friend invites",
|
||||
description: "View a list of all generated friend invites.",
|
||||
inputType: ApplicationCommandInputType.BOT,
|
||||
inputType: ApplicationCommandInputType.BUILT_IN,
|
||||
execute: async (_, ctx) => {
|
||||
const invites = await FriendInvites.getAllFriendInvites();
|
||||
const friendInviteList = invites.map(i =>
|
||||
|
@ -67,7 +67,7 @@ export default definePlugin({
|
|||
{
|
||||
name: "revoke friend invites",
|
||||
description: "Revokes all generated friend invites.",
|
||||
inputType: ApplicationCommandInputType.BOT,
|
||||
inputType: ApplicationCommandInputType.BUILT_IN,
|
||||
execute: async (_, ctx) => {
|
||||
await FriendInvites.revokeFriendInvites();
|
||||
|
||||
|
|
|
@ -22,11 +22,11 @@ import { Devs } from "@utils/constants";
|
|||
import { getIntlMessage } from "@utils/discord";
|
||||
import { NoopComponent } from "@utils/react";
|
||||
import definePlugin from "@utils/types";
|
||||
import { filters, findByPropsLazy, waitFor } from "@webpack";
|
||||
import { filters, findByCodeLazy, waitFor } from "@webpack";
|
||||
import { ChannelStore, ContextMenuApi, UserStore } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
const { useMessageMenu } = findByPropsLazy("useMessageMenu");
|
||||
const useMessageMenu = findByCodeLazy(".MESSAGE,commandTargetId:");
|
||||
|
||||
interface CopyIdMenuItemProps {
|
||||
id: string;
|
||||
|
|
|
@ -8,6 +8,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { UserStore, useStateFromStores } from "@webpack/common";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const UserMentionComponent = findComponentByCodeLazy(".USER_MENTION)");
|
||||
|
@ -34,14 +35,19 @@ export default definePlugin({
|
|||
}
|
||||
],
|
||||
|
||||
UserMentionComponent: ErrorBoundary.wrap((props: UserMentionComponentProps) => (
|
||||
<UserMentionComponent
|
||||
UserMentionComponent: ErrorBoundary.wrap((props: UserMentionComponentProps) => {
|
||||
const user = useStateFromStores([UserStore], () => UserStore.getUser(props.id));
|
||||
if (user == null) {
|
||||
return props.originalComponent();
|
||||
}
|
||||
|
||||
return <UserMentionComponent
|
||||
// This seems to be constant
|
||||
className="mention"
|
||||
userId={props.id}
|
||||
channelId={props.channelId}
|
||||
/>
|
||||
), {
|
||||
/>;
|
||||
}, {
|
||||
fallback: ({ wrappedProps: { originalComponent } }) => originalComponent()
|
||||
})
|
||||
});
|
||||
|
|
|
@ -17,16 +17,15 @@
|
|||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { disableStyle, enableStyle } from "@api/Styles";
|
||||
import { getUserSettingLazy } from "@api/UserSettings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
|
||||
import style from "./style.css?managed";
|
||||
import managedStyle from "./style.css?managed";
|
||||
|
||||
const Button = findComponentByCodeLazy("Button.Sizes.NONE,disabled:");
|
||||
const Button = findComponentByCodeLazy(".NONE,disabled:", ".PANEL_BUTTON");
|
||||
|
||||
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
|
||||
|
||||
|
@ -70,6 +69,7 @@ function GameActivityToggleButton() {
|
|||
icon={makeIcon(showCurrentGame)}
|
||||
role="switch"
|
||||
aria-checked={!showCurrentGame}
|
||||
redGlow={!showCurrentGame}
|
||||
onClick={() => ShowCurrentGame.updateSetting(old => !old)}
|
||||
/>
|
||||
);
|
||||
|
@ -90,11 +90,13 @@ export default definePlugin({
|
|||
dependencies: ["UserSettingsAPI"],
|
||||
settings,
|
||||
|
||||
managedStyle,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
|
||||
replacement: {
|
||||
match: /this\.renderNameZone\(\).+?children:\[/,
|
||||
match: /className:\i\.buttons,.{0,50}children:\[/,
|
||||
replace: "$&$self.GameActivityToggleButton(),"
|
||||
}
|
||||
}
|
||||
|
@ -102,11 +104,4 @@ export default definePlugin({
|
|||
|
||||
GameActivityToggleButton: ErrorBoundary.wrap(GameActivityToggleButton, { noop: true }),
|
||||
|
||||
start() {
|
||||
enableStyle(style);
|
||||
},
|
||||
|
||||
stop() {
|
||||
disableStyle(style);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
[class*="panels"] [class*="avatarWrapper"] {
|
||||
[class^="panels"] [class^="avatarWrapper"] {
|
||||
min-width: 88px;
|
||||
}
|
||||
|
|
|
@ -16,77 +16,92 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { get, set } from "@api/DataStore";
|
||||
import { updateMessage } from "@api/MessageUpdater";
|
||||
import { migratePluginSettings } from "@api/Settings";
|
||||
import { ImageInvisible, ImageVisible } from "@components/Icons";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { classes } from "@utils/misc";
|
||||
import definePlugin from "@utils/types";
|
||||
import { ChannelStore } from "@webpack/common";
|
||||
|
||||
let style: HTMLStyleElement;
|
||||
import { MessageSnapshot } from "@webpack/types";
|
||||
|
||||
const KEY = "HideAttachments_HiddenIds";
|
||||
|
||||
let hiddenMessages: Set<string> = new Set();
|
||||
const getHiddenMessages = () => get(KEY).then(set => {
|
||||
hiddenMessages = set ?? new Set<string>();
|
||||
let hiddenMessages = new Set<string>();
|
||||
|
||||
async function getHiddenMessages() {
|
||||
hiddenMessages = await get(KEY) ?? new Set();
|
||||
return hiddenMessages;
|
||||
});
|
||||
}
|
||||
|
||||
const saveHiddenMessages = (ids: Set<string>) => set(KEY, ids);
|
||||
|
||||
migratePluginSettings("HideMedia", "HideAttachments");
|
||||
|
||||
export default definePlugin({
|
||||
name: "HideAttachments",
|
||||
description: "Hide attachments and Embeds for individual messages via hover button",
|
||||
name: "HideMedia",
|
||||
description: "Hide attachments and embeds for individual messages via hover button",
|
||||
authors: [Devs.Ven],
|
||||
dependencies: ["MessageUpdaterAPI"],
|
||||
|
||||
patches: [{
|
||||
find: "this.renderAttachments(",
|
||||
replacement: {
|
||||
match: /(?<=\i=)this\.render(?:Attachments|Embeds|StickersAccessories)\((\i)\)/g,
|
||||
replace: "$self.shouldHide($1?.id)?null:$&"
|
||||
}
|
||||
}],
|
||||
|
||||
renderMessagePopoverButton(msg) {
|
||||
if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length) return null;
|
||||
// @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",
|
||||
label: isHidden ? "Show Media" : "Hide Media",
|
||||
icon: isHidden ? ImageVisible : ImageInvisible,
|
||||
message: msg,
|
||||
channel: ChannelStore.getChannel(msg.channel_id),
|
||||
onClick: () => this.toggleHide(msg.id)
|
||||
onClick: () => this.toggleHide(msg.channel_id, msg.id)
|
||||
};
|
||||
},
|
||||
|
||||
async start() {
|
||||
style = document.createElement("style");
|
||||
style.id = "VencordHideAttachments";
|
||||
document.head.appendChild(style);
|
||||
renderMessageAccessory({ message }) {
|
||||
if (!this.shouldHide(message.id)) return null;
|
||||
|
||||
return (
|
||||
<span className={classes("vc-hideAttachments-accessory", !message.content && "vc-hideAttachments-no-content")}>
|
||||
Media Hidden
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
||||
async start() {
|
||||
await getHiddenMessages();
|
||||
await this.buildCss();
|
||||
},
|
||||
|
||||
stop() {
|
||||
style.remove();
|
||||
hiddenMessages.clear();
|
||||
},
|
||||
|
||||
async buildCss() {
|
||||
const elements = [...hiddenMessages].map(id => `#message-accessories-${id}`).join(",");
|
||||
style.textContent = `
|
||||
:is(${elements}) :is([class*="embedWrapper"], [class*="clickableSticker"]) {
|
||||
/* important is not necessary, but add it to make sure bad themes won't break it */
|
||||
display: none !important;
|
||||
}
|
||||
:is(${elements})::after {
|
||||
content: "Attachments hidden";
|
||||
color: var(--text-muted);
|
||||
font-size: 80%;
|
||||
}
|
||||
`;
|
||||
shouldHide(messageId: string) {
|
||||
return hiddenMessages.has(messageId);
|
||||
},
|
||||
|
||||
async toggleHide(id: string) {
|
||||
async toggleHide(channelId: string, messageId: string) {
|
||||
const ids = await getHiddenMessages();
|
||||
if (!ids.delete(id))
|
||||
ids.add(id);
|
||||
if (!ids.delete(messageId))
|
||||
ids.add(messageId);
|
||||
|
||||
await saveHiddenMessages(ids);
|
||||
await this.buildCss();
|
||||
updateMessage(channelId, messageId);
|
||||
}
|
||||
});
|
||||
|
|
10
src/plugins/hideAttachments/styles.css
Normal file
10
src/plugins/hideAttachments/styles.css
Normal file
|
@ -0,0 +1,10 @@
|
|||
.vc-hideAttachments-accessory {
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.5em;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.vc-hideAttachments-no-content {
|
||||
margin-top: 0;
|
||||
}
|
|
@ -27,7 +27,7 @@ export default definePlugin({
|
|||
{
|
||||
find: "hasFlag:{writable",
|
||||
replacement: {
|
||||
match: /if\((\i)<=(?:1<<30|1073741824)\)return/,
|
||||
match: /if\((\i)<=(?:0x40000000|(?:1<<30|1073741824))\)return/,
|
||||
replace: "if($1===(1<<20))return false;$&",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -12,7 +12,7 @@ import { Devs } from "@utils/constants";
|
|||
import { Margins } from "@utils/margins";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
import { Button, Forms, showToast, TextInput, Toasts, Tooltip, useEffect, useState } from "webpack/common";
|
||||
import { Button, Forms, showToast, TextInput, Toasts, Tooltip, useEffect, useState } from "@webpack/common";
|
||||
|
||||
const enum ActivitiesTypes {
|
||||
Game,
|
||||
|
@ -73,8 +73,6 @@ function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|||
const ignoredActivityIndex = settings.store.ignoredActivities.findIndex(act => act.id === activity.id);
|
||||
if (ignoredActivityIndex === -1) settings.store.ignoredActivities.push(activity);
|
||||
else settings.store.ignoredActivities.splice(ignoredActivityIndex, 1);
|
||||
|
||||
recalculateActivities();
|
||||
}
|
||||
|
||||
function recalculateActivities() {
|
||||
|
@ -149,8 +147,7 @@ function IdsListComponent(props: { setValue: (value: string) => void; }) {
|
|||
const settings = definePluginSettings({
|
||||
importCustomRPC: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "",
|
||||
component: () => <ImportCustomRPCComponent />
|
||||
component: ImportCustomRPCComponent
|
||||
},
|
||||
listMode: {
|
||||
type: OptionType.SELECT,
|
||||
|
@ -170,7 +167,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));
|
||||
|
@ -245,7 +241,7 @@ export default definePlugin({
|
|||
find: '"LocalActivityStore"',
|
||||
replacement: [
|
||||
{
|
||||
match: /HANG_STATUS.+?(?=!\i\(\)\(\i,\i\)&&)(?<=(\i)\.push.+?)/,
|
||||
match: /\.LISTENING.+?(?=!?\i\(\)\(\i,\i\))(?<=(\i)\.push.+?)/,
|
||||
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`
|
||||
}
|
||||
]
|
||||
|
@ -264,14 +260,7 @@ export default definePlugin({
|
|||
replace: (m, props, nowPlaying) => `${m}$self.renderToggleGameActivityButton(${props},${nowPlaying}),`
|
||||
}
|
||||
},
|
||||
// Discord has 2 different components for activities. Currently, the last is the one being used
|
||||
{
|
||||
find: ".activityTitleText,variant",
|
||||
replacement: {
|
||||
match: /\.activityTitleText.+?children:(\i)\.name.*?}\),/,
|
||||
replace: (m, props) => `${m}$self.renderToggleActivityButton(${props}),`
|
||||
},
|
||||
},
|
||||
// Activities from the apps launcher in the bottom right of the chat bar
|
||||
{
|
||||
find: ".promotedLabelWrapperNonBanner,children",
|
||||
replacement: {
|
||||
|
|
|
@ -195,6 +195,7 @@ export const Magnifier = ErrorBoundary.wrap<MagnifierProps>(({ instance, size: i
|
|||
/>
|
||||
) : (
|
||||
<img
|
||||
className={cl("image")}
|
||||
ref={imageRef}
|
||||
style={{
|
||||
position: "absolute",
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
|
||||
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { disableStyle, enableStyle } from "@api/Styles";
|
||||
import { makeRange } from "@components/PluginSettings/components";
|
||||
import { debounce } from "@shared/debounce";
|
||||
import { Devs } from "@utils/constants";
|
||||
|
@ -29,7 +28,7 @@ import type { Root } from "react-dom/client";
|
|||
|
||||
import { Magnifier, MagnifierProps } from "./components/Magnifier";
|
||||
import { ELEMENT_ID } from "./constants";
|
||||
import styles from "./styles.css?managed";
|
||||
import managedStyle from "./styles.css?managed";
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
saveZoomValues: {
|
||||
|
@ -81,7 +80,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(
|
||||
|
@ -155,12 +159,41 @@ export default definePlugin({
|
|||
authors: [Devs.Aria],
|
||||
tags: ["ImageUtilities"],
|
||||
|
||||
managedStyle,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: ".contain,SCALE_DOWN:",
|
||||
replacement: {
|
||||
match: /\.slide,\i\),/g,
|
||||
replace: `$&id:"${ELEMENT_ID}",`
|
||||
match: /imageClassName:/,
|
||||
replace: `id:"${ELEMENT_ID}",$&`
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
find: ".dimensionlessImage,",
|
||||
replacement: [
|
||||
{
|
||||
match: /className:\i\.media,/,
|
||||
replace: `id:"${ELEMENT_ID}",$&`
|
||||
},
|
||||
{
|
||||
// This patch needs to be above the next one as it uses the zoomed class as an anchor
|
||||
match: /\.zoomed]:.+?,(?=children:)/,
|
||||
replace: "$&onClick:()=>{},"
|
||||
},
|
||||
{
|
||||
match: /className:\i\(\)\(\i\.wrapper,.+?}\),/,
|
||||
replace: ""
|
||||
},
|
||||
]
|
||||
},
|
||||
// Make media viewer options not hide when zoomed in with the default Discord feature
|
||||
{
|
||||
find: '="FOCUS_SENSITIVE",',
|
||||
replacement: {
|
||||
match: /(?<=\.hidden]:)\i/,
|
||||
replace: "false"
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -247,14 +280,12 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
start() {
|
||||
enableStyle(styles);
|
||||
this.element = document.createElement("div");
|
||||
this.element.classList.add("MagnifierContainer");
|
||||
document.body.appendChild(this.element);
|
||||
},
|
||||
|
||||
stop() {
|
||||
disableStyle(styles);
|
||||
// so componenetWillUnMount gets called if Magnifier component is still alive
|
||||
this.root && this.root.unmount();
|
||||
this.element?.remove();
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
border-radius: 0;
|
||||
}
|
||||
|
||||
.vc-imgzoom-nearest-neighbor>img {
|
||||
.vc-imgzoom-nearest-neighbor > .vc-imgzoom-image {
|
||||
image-rendering: pixelated;
|
||||
|
||||
/* https://googlechrome.github.io/samples/image-rendering-pixelated/index.html */
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
Shows your implicit relationships in the Friends tab.
|
||||
|
||||
Implicit relationships on Discord are people with whom you've frecently interacted and share a mutual server; even though Discord thinks you should be friends with them, you haven't added them as friends.
|
||||
Implicit relationships on Discord are people with whom you've frecently interacted and don't have a relationship with. Even though Discord thinks you should be friends with them, you haven't added them as friends!
|
||||
|
||||

|
||||
|
|
|
@ -20,7 +20,7 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
import { ChannelStore, Constants, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserStore } from "@webpack/common";
|
||||
import { Constants, FluxDispatcher, GuildStore, RelationshipStore, RestAPI, SnowflakeUtils, UserStore } from "@webpack/common";
|
||||
import { Settings } from "Vencord";
|
||||
|
||||
const UserAffinitiesStore = findStoreLazy("UserAffinitiesStore");
|
||||
|
@ -34,7 +34,7 @@ export default definePlugin({
|
|||
{
|
||||
find: "#{intl::FRIENDS_ALL_HEADER}",
|
||||
replacement: {
|
||||
match: /toString\(\)\}\);case (\i\.\i)\.BLOCKED/,
|
||||
match: /toString\(\)\}\);case (\i\.\i)\.PENDING/,
|
||||
replace: 'toString()});case $1.IMPLICIT:return "Implicit — "+arguments[1];case $1.BLOCKED'
|
||||
},
|
||||
},
|
||||
|
@ -50,15 +50,15 @@ export default definePlugin({
|
|||
{
|
||||
find: "#{intl::FRIENDS_SECTION_ONLINE}",
|
||||
replacement: {
|
||||
match: /(\(0,\i\.jsx\)\(\i\.TabBar\.Item,\{id:\i\.\i)\.BLOCKED,className:([^\s]+?)\.item,children:\i\.\i\.string\(\i\.\i#{intl::BLOCKED}\)\}\)/,
|
||||
replace: "$1.IMPLICIT,className:$2.item,children:\"Implicit\"}),$&"
|
||||
},
|
||||
match: /,{id:(\i\.\i)\.PENDING,show:.+?className:(\i\.item)/,
|
||||
replace: (rest, relationShipTypes, className) => `,{id:${relationShipTypes}.IMPLICIT,show:true,className:${className},content:"Implicit"}${rest}`
|
||||
}
|
||||
},
|
||||
// Sections content
|
||||
{
|
||||
find: '"FriendsStore"',
|
||||
replacement: {
|
||||
match: /(?<=case (\i\.\i)\.BLOCKED:return (\i)\.type===\i\.\i\.BLOCKED)/,
|
||||
match: /(?<=case (\i\.\i)\.SUGGESTIONS:return \d+===(\i)\.type)/,
|
||||
replace: ";case $1.IMPLICIT:return $2.type===5"
|
||||
},
|
||||
},
|
||||
|
@ -121,55 +121,61 @@ export default definePlugin({
|
|||
: comparator(row);
|
||||
},
|
||||
|
||||
async refreshUserAffinities() {
|
||||
try {
|
||||
await RestAPI.get({ url: "/users/@me/affinities/users", retries: 3 }).then(({ body }) => {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "LOAD_USER_AFFINITIES_SUCCESS",
|
||||
affinities: body,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
// Not a critical error if this fails for some reason
|
||||
}
|
||||
},
|
||||
|
||||
async fetchImplicitRelationships() {
|
||||
// Implicit relationships are defined as users that you:
|
||||
// 1. Have an affinity for
|
||||
// 2. Do not have a relationship with // TODO: Check how this works with pending/blocked relationships
|
||||
// 3. Have a mutual guild with
|
||||
// 2. Do not have a relationship with
|
||||
await this.refreshUserAffinities();
|
||||
const userAffinities: Set<string> = UserAffinitiesStore.getUserAffinitiesUserIds();
|
||||
const relationships = RelationshipStore.getRelationships();
|
||||
const nonFriendAffinities = Array.from(userAffinities).filter(
|
||||
id => !RelationshipStore.getRelationshipType(id)
|
||||
);
|
||||
nonFriendAffinities.forEach(id => {
|
||||
relationships[id] = 5;
|
||||
});
|
||||
RelationshipStore.emitChange();
|
||||
|
||||
// I would love to just check user cache here (falling back to the gateway of course)
|
||||
// However, users in user cache may just be there because they share a DM or group DM with you
|
||||
// So there's no guarantee that a user being in user cache means they have a mutual with you
|
||||
// To get around this, we request users we have DMs with, and ignore them below if we don't get them back
|
||||
const dmUserIds = new Set(
|
||||
Object.values(ChannelStore.getSortedPrivateChannels()).flatMap(c => c.recipients)
|
||||
);
|
||||
const toRequest = nonFriendAffinities.filter(id => !UserStore.getUser(id) || dmUserIds.has(id));
|
||||
const toRequest = nonFriendAffinities.filter(id => !UserStore.getUser(id));
|
||||
const allGuildIds = Object.keys(GuildStore.getGuilds());
|
||||
const sentNonce = SnowflakeUtils.fromTimestamp(Date.now());
|
||||
let count = allGuildIds.length * Math.ceil(toRequest.length / 100);
|
||||
|
||||
// OP 8 Request Guild Members allows 100 user IDs at a time
|
||||
const ignore = new Set(toRequest);
|
||||
const relationships = RelationshipStore.getRelationships();
|
||||
// Note: As we are using OP 8 here, implicit relationships who we do not share a guild
|
||||
// with will not be fetched; so, if they're not otherwise cached, they will not be shown
|
||||
// This should not be a big deal as these should be rare
|
||||
const callback = ({ chunks }) => {
|
||||
for (const chunk of chunks) {
|
||||
const { nonce, members } = chunk;
|
||||
if (nonce !== sentNonce) return;
|
||||
members.forEach(member => {
|
||||
ignore.delete(member.user.id);
|
||||
});
|
||||
const chunkCount = chunks.filter(chunk => chunk.nonce === sentNonce).length;
|
||||
if (chunkCount === 0) return;
|
||||
|
||||
nonFriendAffinities.map(id => UserStore.getUser(id)).filter(user => user && !ignore.has(user.id)).forEach(user => relationships[user.id] = 5);
|
||||
RelationshipStore.emitChange();
|
||||
if (--count === 0) {
|
||||
// @ts-ignore
|
||||
FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
|
||||
}
|
||||
count -= chunkCount;
|
||||
RelationshipStore.emitChange();
|
||||
if (count <= 0) {
|
||||
FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
FluxDispatcher.subscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
|
||||
for (let i = 0; i < toRequest.length; i += 100) {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "GUILD_MEMBERS_REQUEST",
|
||||
guildIds: allGuildIds,
|
||||
userIds: toRequest.slice(i, i + 100),
|
||||
presences: true,
|
||||
nonce: sentNonce,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -26,10 +26,12 @@ import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecor
|
|||
import { addMessageClickListener, addMessagePreEditListener, addMessagePreSendListener, removeMessageClickListener, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents";
|
||||
import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover";
|
||||
import { Settings, SettingsStore } from "@api/Settings";
|
||||
import { disableStyle, enableStyle } from "@api/Styles";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { canonicalizeFind } from "@utils/patches";
|
||||
import { canonicalizeFind, canonicalizeReplacement } from "@utils/patches";
|
||||
import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types";
|
||||
import { FluxDispatcher } from "@webpack/common";
|
||||
import { patches } from "@webpack/patcher";
|
||||
import { FluxEvents } from "@webpack/types";
|
||||
|
||||
import Plugins from "~plugins";
|
||||
|
@ -40,7 +42,7 @@ const logger = new Logger("PluginManager", "#a6d189");
|
|||
|
||||
export const PMLogger = logger;
|
||||
export const plugins = Plugins;
|
||||
export const patches = [] as Patch[];
|
||||
export { patches };
|
||||
|
||||
/** Whether we have subscribed to flux events of all the enabled plugins when FluxDispatcher was ready */
|
||||
let enabledPluginsSubscribedFlux = false;
|
||||
|
@ -57,7 +59,7 @@ export function isPluginEnabled(p: string) {
|
|||
) ?? false;
|
||||
}
|
||||
|
||||
export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string) {
|
||||
export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string, pluginPath = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`) {
|
||||
const patch = newPatch as Patch;
|
||||
patch.plugin = pluginName;
|
||||
|
||||
|
@ -73,10 +75,12 @@ export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string) {
|
|||
patch.replacement = [patch.replacement];
|
||||
}
|
||||
|
||||
if (IS_REPORTER) {
|
||||
patch.replacement.forEach(r => {
|
||||
delete r.predicate;
|
||||
});
|
||||
for (const replacement of patch.replacement) {
|
||||
canonicalizeReplacement(replacement, pluginPath);
|
||||
|
||||
if (IS_REPORTER) {
|
||||
delete replacement.predicate;
|
||||
}
|
||||
}
|
||||
|
||||
patch.replacement = patch.replacement.filter(({ predicate }) => !predicate || predicate());
|
||||
|
@ -141,14 +145,21 @@ for (const p of neededApiPlugins) {
|
|||
|
||||
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 (def.onChange != null) {
|
||||
SettingsStore.addChangeListener(`plugins.${p.name}.${name}`, def.onChange);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -247,7 +258,7 @@ export function subscribeAllPluginsFluxEvents(fluxDispatcher: typeof FluxDispatc
|
|||
|
||||
export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) {
|
||||
const {
|
||||
name, commands, contextMenus, userProfileBadge,
|
||||
name, commands, contextMenus, managedStyle, userProfileBadge,
|
||||
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
|
||||
renderChatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton
|
||||
} = p;
|
||||
|
@ -291,6 +302,8 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
|||
}
|
||||
}
|
||||
|
||||
if (managedStyle) enableStyle(managedStyle);
|
||||
|
||||
if (userProfileBadge) addProfileBadge(userProfileBadge);
|
||||
|
||||
if (onBeforeMessageEdit) addMessagePreEditListener(onBeforeMessageEdit);
|
||||
|
@ -308,7 +321,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
|||
|
||||
export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) {
|
||||
const {
|
||||
name, commands, contextMenus, userProfileBadge,
|
||||
name, commands, contextMenus, managedStyle, userProfileBadge,
|
||||
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
|
||||
renderChatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton
|
||||
} = p;
|
||||
|
@ -350,6 +363,8 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
|
|||
}
|
||||
}
|
||||
|
||||
if (managedStyle) disableStyle(managedStyle);
|
||||
|
||||
if (userProfileBadge) removeProfileBadge(userProfileBadge);
|
||||
|
||||
if (onBeforeMessageEdit) removeMessagePreEditListener(onBeforeMessageEdit);
|
||||
|
|
|
@ -80,8 +80,8 @@ const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
|
|||
<svg
|
||||
aria-hidden
|
||||
role="img"
|
||||
width="24"
|
||||
height="24"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox={"0 0 64 64"}
|
||||
style={{ scale: "1.39", translate: "0 -1px" }}
|
||||
>
|
||||
|
@ -149,6 +149,12 @@ export default definePlugin({
|
|||
|
||||
renderChatBarButton: ChatBarIcon,
|
||||
|
||||
colorCodeFromNumber(color: number): string {
|
||||
return `#${[color >> 16, color >> 8, color]
|
||||
.map(x => (x & 0xFF).toString(16))
|
||||
.join("")}`;
|
||||
},
|
||||
|
||||
// Gets the Embed of a Link
|
||||
async getEmbed(url: URL): Promise<Object | {}> {
|
||||
const { body } = await RestAPI.post({
|
||||
|
@ -157,6 +163,8 @@ export default definePlugin({
|
|||
urls: [url]
|
||||
}
|
||||
});
|
||||
// The endpoint returns the color as a number, but Discord expects a string
|
||||
body.embeds[0].color = this.colorCodeFromNumber(body.embeds[0].color);
|
||||
return await body.embeds[0];
|
||||
},
|
||||
|
||||
|
@ -166,7 +174,7 @@ export default definePlugin({
|
|||
message.embeds.push({
|
||||
type: "rich",
|
||||
rawTitle: "Decrypted Message",
|
||||
color: "0x45f5f5",
|
||||
color: "#45f5f5",
|
||||
rawDescription: revealed,
|
||||
footer: {
|
||||
text: "Made with ❤️ by c0dine and Sammy!",
|
||||
|
|
17
src/plugins/ircColors/README.md
Normal file
17
src/plugins/ircColors/README.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
# IrcColors
|
||||
|
||||
Makes username colors in chat unique, like in IRC clients
|
||||
|
||||

|
||||
|
||||
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.
|
113
src/plugins/ircColors/index.ts
Normal file
113
src/plugins/ircColors/index.ts
Normal file
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { hash as h64 } from "@intrnl/xxhash64";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { useMemo } from "@webpack/common";
|
||||
|
||||
// Calculate a CSS color string based on the user ID
|
||||
function calculateNameColorForUser(id?: string) {
|
||||
const { lightness } = settings.use(["lightness"]);
|
||||
const idHash = useMemo(() => id ? h64(id) : null, [id]);
|
||||
|
||||
return idHash && `hsl(${idHash % 360n}, 100%, ${lightness}%)`;
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
lightness: {
|
||||
description: "Lightness, in %. Change if the colors are too light or too dark",
|
||||
type: OptionType.NUMBER,
|
||||
default: 70,
|
||||
},
|
||||
memberListColors: {
|
||||
description: "Replace role colors in the member list",
|
||||
restartNeeded: true,
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true
|
||||
},
|
||||
applyColorOnlyToUsersWithoutColor: {
|
||||
description: "Apply colors only to users who don't have a predefined color",
|
||||
restartNeeded: false,
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false
|
||||
},
|
||||
applyColorOnlyInDms: {
|
||||
description: "Apply colors only in direct messages; do not apply colors in servers.",
|
||||
restartNeeded: false,
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "IrcColors",
|
||||
description: "Makes username colors in chat unique, like in IRC clients",
|
||||
authors: [Devs.Grzesiek11, Devs.jamesbt365],
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: '="SYSTEM_TAG"',
|
||||
replacement: {
|
||||
match: /\i.gradientClassName]\),style:/,
|
||||
replace: "$&{color:$self.calculateNameColorForMessageContext(arguments[0])},_style:"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "#{intl::GUILD_OWNER}),children:",
|
||||
replacement: {
|
||||
match: /(typingIndicatorRef:.+?},)(\i=.+?)color:null!=.{0,50}?(?=,)/,
|
||||
replace: (_, rest1, rest2) => `${rest1}ircColor=$self.calculateNameColorForListContext(arguments[0]),${rest2}color:ircColor`
|
||||
},
|
||||
predicate: () => settings.store.memberListColors
|
||||
}
|
||||
],
|
||||
|
||||
calculateNameColorForMessageContext(context: any) {
|
||||
const userId: string | undefined = context?.message?.author?.id;
|
||||
const colorString = context?.author?.colorString;
|
||||
const color = calculateNameColorForUser(userId);
|
||||
|
||||
// Color preview in role settings
|
||||
if (context?.message?.channel_id === "1337" && userId === "313337")
|
||||
return colorString;
|
||||
|
||||
if (settings.store.applyColorOnlyInDms && !context?.channel?.isPrivate()) {
|
||||
return colorString;
|
||||
}
|
||||
|
||||
return (!settings.store.applyColorOnlyToUsersWithoutColor || !colorString)
|
||||
? color
|
||||
: colorString;
|
||||
},
|
||||
calculateNameColorForListContext(context: any) {
|
||||
const id = context?.user?.id;
|
||||
const colorString = context?.colorString;
|
||||
const color = calculateNameColorForUser(id);
|
||||
|
||||
if (settings.store.applyColorOnlyInDms && !context?.channel?.isPrivate()) {
|
||||
return colorString;
|
||||
}
|
||||
|
||||
return (!settings.store.applyColorOnlyToUsersWithoutColor || !colorString)
|
||||
? color
|
||||
: colorString;
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -65,10 +65,18 @@ export default definePlugin({
|
|||
patches: [
|
||||
{
|
||||
find: "{isSidebarVisible:",
|
||||
replacement: {
|
||||
match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/,
|
||||
replace: ":[$1?.startsWith('members')?$self.render():null,$2"
|
||||
},
|
||||
replacement: [
|
||||
{
|
||||
// FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
|
||||
match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/,
|
||||
replace: ":[$1?.startsWith('members')?$self.render():null,$2",
|
||||
noWarn: true
|
||||
},
|
||||
{
|
||||
match: /(?<=var\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/,
|
||||
replace: ":[$1?.startsWith('members')?$self.render():null,$2",
|
||||
},
|
||||
],
|
||||
predicate: () => settings.store.memberList
|
||||
},
|
||||
{
|
||||
|
|
|
@ -57,7 +57,7 @@ export default definePlugin({
|
|||
{
|
||||
find: ".ROLE_MENTION)",
|
||||
replacement: {
|
||||
match: /children:\[\i&&.{0,50}\.RoleDot.{0,300},\i(?=\])/,
|
||||
match: /children:\[\i&&.{0,100}className:\i.roleDot,.{0,200},\i(?=\])/,
|
||||
replace: "$&,$self.renderRoleIcon(arguments[0])"
|
||||
}
|
||||
}],
|
||||
|
|
|
@ -9,7 +9,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||
import { Devs } from "@utils/constants";
|
||||
import { isNonNullish } from "@utils/guards";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findExportedComponentLazy } from "@webpack";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { SnowflakeUtils, Tooltip } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
|
@ -26,7 +26,7 @@ interface Diff {
|
|||
}
|
||||
|
||||
const DISCORD_KT_DELAY = 1471228928;
|
||||
const HiddenVisually = findExportedComponentLazy("HiddenVisually");
|
||||
const HiddenVisually = findComponentByCodeLazy(".hiddenVisually]:");
|
||||
|
||||
export default definePlugin({
|
||||
name: "MessageLatency",
|
||||
|
@ -63,11 +63,11 @@ export default definePlugin({
|
|||
|
||||
stringDelta(delta: number, showMillis: boolean) {
|
||||
const diff: Diff = {
|
||||
days: Math.round(delta / (60 * 60 * 24 * 1000)),
|
||||
hours: Math.round((delta / (60 * 60 * 1000)) % 24),
|
||||
minutes: Math.round((delta / (60 * 1000)) % 60),
|
||||
seconds: Math.round(delta / 1000 % 60),
|
||||
milliseconds: Math.round(delta % 1000)
|
||||
days: Math.floor(delta / (60 * 60 * 24 * 1000)),
|
||||
hours: Math.floor((delta / (60 * 60 * 1000)) % 24),
|
||||
minutes: Math.floor((delta / (60 * 1000)) % 60),
|
||||
seconds: Math.floor(delta / 1000 % 60),
|
||||
milliseconds: Math.floor(delta % 1000)
|
||||
};
|
||||
|
||||
const str = (k: DiffKey) => diff[k] > 0 ? `${diff[k]} ${diff[k] > 1 ? k : k.substring(0, k.length - 1)}` : null;
|
||||
|
@ -162,7 +162,7 @@ export default definePlugin({
|
|||
</>
|
||||
}
|
||||
</Tooltip>;
|
||||
});
|
||||
}, { noop: true });
|
||||
},
|
||||
|
||||
Icon({ delta, fill, props }: {
|
||||
|
|
|
@ -120,11 +120,11 @@ const settings = definePluginSettings({
|
|||
},
|
||||
clearMessageCache: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Clear the linked message cache",
|
||||
component: () =>
|
||||
component: () => (
|
||||
<Button onClick={() => messageCache.clear()}>
|
||||
Clear the linked message cache
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,24 +1,8 @@
|
|||
/* Message content highlighting */
|
||||
.messagelogger-deleted [class*="contents"] > :is(div, h1, h2, h3, p) {
|
||||
color: var(--status-danger, #f04747) !important;
|
||||
}
|
||||
|
||||
/* Markdown title highlighting */
|
||||
.messagelogger-deleted [class*="contents"] :is(h1, h2, h3) {
|
||||
color: var(--status-danger, #f04747) !important;
|
||||
}
|
||||
|
||||
/* Bot "thinking" text highlighting */
|
||||
.messagelogger-deleted [class*="colorStandard"] {
|
||||
color: var(--status-danger, #f04747) !important;
|
||||
}
|
||||
|
||||
/* Embed highlighting */
|
||||
.messagelogger-deleted article :is(div, span, h1, h2, h3, p) {
|
||||
color: var(--status-danger, #f04747) !important;
|
||||
}
|
||||
|
||||
.messagelogger-deleted a {
|
||||
color: var(--red-460, #be3535) !important;
|
||||
text-decoration: underline;
|
||||
.messagelogger-deleted {
|
||||
--text-normal: var(--status-danger, #f04747);
|
||||
--interactive-normal: var(--status-danger, #f04747);
|
||||
--text-muted: var(--status-danger, #f04747);
|
||||
--embed-title: var(--red-460, #be3535);
|
||||
--text-link: var(--red-460, #be3535);
|
||||
--header-primary: var(--red-460, #be3535);
|
||||
}
|
||||
|
|
|
@ -211,7 +211,8 @@ export default definePlugin({
|
|||
collapseDeleted: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether to collapse deleted messages, similar to blocked messages",
|
||||
default: false
|
||||
default: false,
|
||||
restartNeeded: true,
|
||||
},
|
||||
logEdits: {
|
||||
type: OptionType.BOOLEAN,
|
||||
|
@ -400,7 +401,7 @@ export default definePlugin({
|
|||
|
||||
{
|
||||
// Updated message transformer(?)
|
||||
find: "THREAD_STARTER_MESSAGE?null===",
|
||||
find: "THREAD_STARTER_MESSAGE?null==",
|
||||
replacement: [
|
||||
{
|
||||
// Pass through editHistory & deleted & original attachments to the "edited message" transformer
|
||||
|
@ -441,15 +442,10 @@ export default definePlugin({
|
|||
{
|
||||
// Attachment renderer
|
||||
find: ".removeMosaicItemHoverButton",
|
||||
group: true,
|
||||
replacement: [
|
||||
{
|
||||
match: /(className:\i,item:\i),/,
|
||||
replace: "$1,item: deleted,"
|
||||
},
|
||||
{
|
||||
match: /\[\i\.obscured\]:.+?,/,
|
||||
replace: "$& 'messagelogger-deleted-attachment': deleted,"
|
||||
match: /\[\i\.obscured\]:.+?,(?<=item:(\i).+?)/,
|
||||
replace: '$&"messagelogger-deleted-attachment":$1.originalItem?.deleted,'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -500,7 +496,7 @@ export default definePlugin({
|
|||
|
||||
{
|
||||
// Message context base menu
|
||||
find: "useMessageMenu:",
|
||||
find: ".MESSAGE,commandTargetId:",
|
||||
replacement: [
|
||||
{
|
||||
// Remove the first section if message is deleted
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
|
||||
.messagelogger-deleted
|
||||
:is(
|
||||
video,
|
||||
.messagelogger-deleted-attachment,
|
||||
.emoji,
|
||||
[data-type="sticker"],
|
||||
iframe,
|
||||
.messagelogger-deleted-attachment,
|
||||
[class|="inlineMediaEmbed"]
|
||||
[class*="embedIframe"],
|
||||
[class*="embedSpotify"],
|
||||
[class*="imageContainer"]
|
||||
) {
|
||||
filter: grayscale(1) !important;
|
||||
transition: 150ms filter ease-in-out;
|
||||
|
@ -17,18 +17,14 @@
|
|||
&[class*="hiddenMosaicItem_"] {
|
||||
filter: grayscale(1) blur(var(--custom-message-attachment-spoiler-blur-radius, 44px)) !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: grayscale(0) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.messagelogger-deleted
|
||||
:is(
|
||||
video,
|
||||
.emoji,
|
||||
[data-type="sticker"],
|
||||
iframe,
|
||||
.messagelogger-deleted-attachment,
|
||||
[class|="inlineMediaEmbed"]
|
||||
):hover {
|
||||
filter: grayscale(0) !important;
|
||||
.messagelogger-deleted [class*="spoilerWarning"] {
|
||||
color: var(--status-danger);
|
||||
}
|
||||
|
||||
.theme-dark .messagelogger-edited {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue