diff --git a/package.json b/package.json index 6fba8651f..1cc67bdca 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.0.7", + "version": "1.0.8", "description": "The cutest Discord client mod", "keywords": [], "homepage": "https://github.com/Vendicated/Vencord#readme", diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index a13640e10..1eb2eb8d7 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -17,8 +17,9 @@ */ import Logger from "@utils/Logger"; +import { Margins } from "@utils/margins"; import { LazyComponent } from "@utils/misc"; -import { Margins, React } from "@webpack/common"; +import { React } from "@webpack/common"; import { ErrorCard } from "./ErrorCard"; @@ -84,15 +85,13 @@ const ErrorBoundary = LazyComponent(() => { const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console."; return ( - +

Oh no!

{msg}

{this.state.message} {!!this.state.stack && ( -
+                            
                                 {this.state.stack}
                             
)} diff --git a/src/components/ErrorCard.css b/src/components/ErrorCard.css new file mode 100644 index 000000000..5146aa03f --- /dev/null +++ b/src/components/ErrorCard.css @@ -0,0 +1,7 @@ +.vc-error-card { + padding: 2em; + background-color: #e7828430; + border: 1px solid #e78284; + border-radius: 5px; + color: var(--text-normal, white); +} diff --git a/src/components/ErrorCard.tsx b/src/components/ErrorCard.tsx index e749ea4b6..7ce8cad1f 100644 --- a/src/components/ErrorCard.tsx +++ b/src/components/ErrorCard.tsx @@ -16,24 +16,15 @@ * along with this program. If not, see . */ -import { Card } from "@webpack/common"; +import "./ErrorCard.css"; -interface Props { - style?: React.CSSProperties; - className?: string; -} -export function ErrorCard(props: React.PropsWithChildren) { +import { classes } from "@utils/misc"; +import type { HTMLProps } from "react"; + +export function ErrorCard(props: React.PropsWithChildren>) { return ( - +
{props.children} - +
); } diff --git a/src/plugins/apiMenuItemDeobfuscator.ts b/src/plugins/apiMenuItemDeobfuscator.ts index 5703456bb..4b8554fae 100644 --- a/src/plugins/apiMenuItemDeobfuscator.ts +++ b/src/plugins/apiMenuItemDeobfuscator.ts @@ -43,7 +43,7 @@ export default definePlugin({ { find: '"Menu API', replacement: { - match: /function.{0,80}type===(.{1,3})\..{1,3}\).{0,50}navigable:.+?Menu API/s, + match: /function.{0,80}type===(\i)\).{0,50}navigable:.+?Menu API/s, replace: (m, mod) => { let nicenNames = ""; const redefines = [] as string[]; diff --git a/src/plugins/consoleShortcuts.ts b/src/plugins/consoleShortcuts.ts index ae0de9f83..83b52916f 100644 --- a/src/plugins/consoleShortcuts.ts +++ b/src/plugins/consoleShortcuts.ts @@ -18,6 +18,9 @@ import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; +import * as Webpack from "@webpack"; +import { extract, filters, findAll, search } from "@webpack"; +import { React } from "@webpack/common"; const WEB_ONLY = (f: string) => () => { throw new Error(`'${f}' is Discord Desktop only.`); @@ -29,19 +32,48 @@ export default definePlugin({ authors: [Devs.Ven], getShortcuts() { + function newFindWrapper(filterFactory: (props: any) => Webpack.FilterFn) { + const cache = new Map(); + + return function (filterProps: any) { + const cacheKey = String(filterProps); + if (cache.has(cacheKey)) return cache.get(cacheKey); + + const matches = findAll(filterFactory(filterProps)); + + const result = (() => { + switch (matches.length) { + case 0: return null; + case 1: return matches[0]; + 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); + + return matches[0]; + } + })(); + if (result && cacheKey) cache.set(cacheKey, result); + return result; + }; + } + return { - toClip: IS_WEB ? WEB_ONLY("toClip") : window.DiscordNative.clipboard.copy, - fromClip: IS_WEB ? WEB_ONLY("fromClip") : window.DiscordNative.clipboard.read, wp: Vencord.Webpack, - wpc: Vencord.Webpack.wreq.c, - wreq: Vencord.Webpack.wreq, - wpsearch: Vencord.Webpack.search, - wpex: Vencord.Webpack.extract, + wpc: Webpack.wreq.c, + wreq: Webpack.wreq, + wpsearch: search, + wpex: extract, wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!), - findByProps: Vencord.Webpack.findByProps, - find: Vencord.Webpack.find, - Plugins: Vencord.Plugins, - React: Vencord.Webpack.Common.React, + find: newFindWrapper(f => f), + findAll, + findByProps: newFindWrapper(filters.byProps), + findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)), + findByCode: newFindWrapper(filters.byCode), + findAllByCode: (code: string) => findAll(filters.byCode(code)), + PluginsApi: Vencord.Plugins, + plugins: Vencord.Plugins.plugins, + React, Settings: Vencord.Settings, Api: Vencord.Api, reload: () => location.reload(), diff --git a/src/plugins/customRPC.tsx b/src/plugins/customRPC.tsx index 9a0901b76..fe6d574db 100644 --- a/src/plugins/customRPC.tsx +++ b/src/plugins/customRPC.tsx @@ -19,6 +19,7 @@ import { definePluginSettings } from "@api/settings"; import { Link } from "@components/Link"; import { Devs } from "@utils/constants"; +import { isTruthy } from "@utils/guards"; import { useAwaiter } from "@utils/misc"; import definePlugin, { OptionType } from "@utils/types"; import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack"; @@ -56,11 +57,11 @@ interface ActivityAssets { } interface Activity { - state: string; + state?: string; details?: string; timestamps?: { - start?: Number; - end?: Number; + start?: number; + end?: number; }; assets?: ActivityAssets; buttons?: Array; @@ -70,7 +71,7 @@ interface Activity { button_urls?: Array; }; type: ActivityType; - flags: Number; + flags: number; } enum ActivityType { @@ -93,13 +94,13 @@ const numOpt = (description: string) => ({ onChange: setRpc }) as const; -const choice = (label: string, value: any, _default?: Boolean) => ({ +const choice = (label: string, value: any, _default?: boolean) => ({ label, value, default: _default }) as const; -const choiceOpt = (description: string, options) => ({ +const choiceOpt = (description: string, options: T) => ({ type: OptionType.SELECT, description, onChange: setRpc, @@ -173,13 +174,13 @@ async function createActivity(): Promise { activity.buttons = [ buttonOneText, buttonTwoText - ].filter(Boolean); + ].filter(isTruthy); activity.metadata = { button_urls: [ buttonOneURL, buttonTwoURL - ].filter(Boolean) + ].filter(isTruthy) }; } @@ -206,12 +207,10 @@ async function createActivity(): Promise { delete activity[k]; } - // WHAT DO YOU WANT FROM ME - // eslint-disable-next-line consistent-return return activity; } -async function setRpc(disable?: Boolean) { +async function setRpc(disable?: boolean) { const activity: Activity | undefined = await createActivity(); FluxDispatcher.dispatch({ diff --git a/src/plugins/lastfm.tsx b/src/plugins/lastfm.tsx index bf3f63c9e..9a0647c76 100644 --- a/src/plugins/lastfm.tsx +++ b/src/plugins/lastfm.tsx @@ -34,7 +34,7 @@ interface Activity { state: string; details?: string; timestamps?: { - start?: Number; + start?: number; }; assets?: ActivityAssets; buttons?: Array; @@ -43,8 +43,8 @@ interface Activity { metadata?: { button_urls?: Array; }; - type: Number; - flags: Number; + type: number; + flags: number; } interface TrackData { diff --git a/src/plugins/revealAllSpoilers.ts b/src/plugins/revealAllSpoilers.ts index b508b6a0a..ead169fce 100644 --- a/src/plugins/revealAllSpoilers.ts +++ b/src/plugins/revealAllSpoilers.ts @@ -30,10 +30,10 @@ export default definePlugin({ patches: [ { - find: ".revealSpoiler=function", + find: ".removeObscurity=function", replacement: { - match: /\.revealSpoiler=function\((.{1,2})\){/, - replace: ".revealSpoiler=function($1){$self.reveal($1);" + match: /\.removeObscurity=function\((\i)\){/, + replace: ".removeObscurity=function($1){$self.reveal($1);" } } ], diff --git a/src/plugins/supportHelper.tsx b/src/plugins/supportHelper.tsx new file mode 100644 index 000000000..9eb21093f --- /dev/null +++ b/src/plugins/supportHelper.tsx @@ -0,0 +1,91 @@ +/* + * 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 . +*/ + +import { DataStore } from "@api/index"; +import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants"; +import { makeCodeblock } from "@utils/misc"; +import definePlugin from "@utils/types"; +import { isOutdated } from "@utils/updater"; +import { Alerts, FluxDispatcher, Forms, UserStore } from "@webpack/common"; + +import gitHash from "~git-hash"; +import plugins from "~plugins"; + +import settings from "./settings"; + +const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss"; + +export default definePlugin({ + name: "SupportHelper", + required: true, + description: "Helps me provide support to you", + authors: [Devs.Ven], + + commands: [{ + name: "vencord-debug", + description: "Send Vencord Debug info", + predicate: ctx => ctx.channel.id === SUPPORT_CHANNEL_ID, + execute() { + const { RELEASE_CHANNEL } = window.GLOBAL_ENV; + + const debugInfo = ` +**Vencord Debug Info** + +> Discord Branch: ${RELEASE_CHANNEL} +> Client: ${typeof DiscordNative === "undefined" ? window.armcord ? "Armcord" : `Web (${navigator.userAgent})` : `Desktop (Electron v${settings.electronVersion})`} +> Platform: ${window.navigator.platform} +> Vencord Version: ${gitHash}${settings.additionalInfo} +> Outdated: ${isOutdated} +> Enabled Plugins: +${makeCodeblock(Object.keys(plugins).filter(Vencord.Plugins.isPluginEnabled).join(", "))} +`; + + return { + content: debugInfo.trim() + }; + } + }], + + rememberDismiss() { + DataStore.set(REMEMBER_DISMISS_KEY, gitHash); + }, + + start() { + FluxDispatcher.subscribe("CHANNEL_SELECT", async ({ channelId }) => { + if (channelId !== SUPPORT_CHANNEL_ID) return; + + const myId = BigInt(UserStore.getCurrentUser().id); + if (Object.values(Devs).some(d => d.id === myId)) return; + + if (isOutdated && gitHash !== await DataStore.get(REMEMBER_DISMISS_KEY)) { + Alerts.show({ + title: "Hold on!", + body:
+ You are using an outdated version of Vencord! Chances are, your issue is already fixed. + + Please first update using the Updater Page in Settings, or use the VencordInstaller (Update Vencord Button) + to do so, in case you can't access the Updater page. + +
, + onCancel: this.rememberDismiss, + onConfirm: this.rememberDismiss + }); + } + }); + } +}); diff --git a/src/plugins/typingTweaks.tsx b/src/plugins/typingTweaks.tsx index db8c438b5..47b69783e 100644 --- a/src/plugins/typingTweaks.tsx +++ b/src/plugins/typingTweaks.tsx @@ -24,7 +24,7 @@ import { findByCodeLazy } from "@webpack"; import { GuildMemberStore, React, RelationshipStore } from "@webpack/common"; import { User } from "discord-types/general"; -const Avatar = findByCodeLazy(".Positions.TOP,spacing:"); +const Avatar = findByCodeLazy('"top",spacing:'); const settings = definePluginSettings({ showAvatars: { diff --git a/src/plugins/vcDoubleClick.ts b/src/plugins/vcDoubleClick.ts index 64c676cfc..695e8c51f 100644 --- a/src/plugins/vcDoubleClick.ts +++ b/src/plugins/vcDoubleClick.ts @@ -48,7 +48,7 @@ export default definePlugin({ }, { // channel mentions - find: ".EMOJI_IN_MESSAGE_HOVER", + find: ".shouldCloseDefaultModals", replacement: { match: /onClick:(\i)(?=,.{0,30}className:"channelMention")/, replace: "onClick:(_vcEv)=>(_vcEv.detail>=2||_vcEv.target.className.includes('MentionText'))&&($1)()", diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 4f24d03b4..0805872e4 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -22,6 +22,7 @@ import gitRemote from "~git-remote"; export const WEBPACK_CHUNK = "webpackChunkdiscord_app"; export const REACT_GLOBAL = "Vencord.Webpack.Common.React"; export const VENCORD_USER_AGENT = `Vencord/${gitHash}${gitRemote ? ` (https://github.com/${gitRemote})` : ""}`; +export const SUPPORT_CHANNEL_ID = "1026515880080842772"; // Add yourself here if you made a plugin export const Devs = /* #__PURE__*/ Object.freeze({ diff --git a/src/utils/guards.ts b/src/utils/guards.ts new file mode 100644 index 000000000..499f4b83e --- /dev/null +++ b/src/utils/guards.ts @@ -0,0 +1,25 @@ +/* + * 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 . +*/ + +export function isTruthy(item: T): item is Exclude { + return Boolean(item); +} + +export function isNonNullish(item: T): item is Exclude { + return item != null; +} diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx index a41ab6730..b64dff9a0 100644 --- a/src/utils/misc.tsx +++ b/src/utils/misc.tsx @@ -141,8 +141,8 @@ export function humanFriendlyJoin(elements: any[], mapper: (e: any) => string = * Calls .join(" ") on the arguments * classes("one", "two") => "one two" */ -export function classes(...classes: string[]) { - return classes.filter(c => typeof c === "string").join(" "); +export function classes(...classes: Array) { + return classes.filter(Boolean).join(" "); } /** diff --git a/src/utils/types.ts b/src/utils/types.ts index 5ab685761..24915a6e0 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -229,9 +229,12 @@ type PluginSettingType = O extends PluginSettingStri O extends PluginSettingSliderDef ? number : O extends PluginSettingComponentDef ? any : never; +type PluginSettingDefaultType = O extends PluginSettingSelectDef ? ( + O["options"] extends { default?: boolean; }[] ? O["options"][number]["value"] : undefined +) : O extends { default: infer T; } ? T : undefined; type SettingsStore = { - [K in keyof D]: PluginSettingType; + [K in keyof D]: PluginSettingType | PluginSettingDefaultType; }; /** An instance of defined plugin settings */ diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index 0c029715c..63511a7b6 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -35,7 +35,7 @@ export const Forms = { export const Card = waitForComponent("Card", m => m.Types?.PRIMARY && m.defaultProps); export const Button = waitForComponent("Button", ["Hovers", "Looks", "Sizes"]); export const Switch = waitForComponent("Switch", filters.byCode("tooltipNote", "ringTarget")); -export const Tooltip = waitForComponent("Tooltip", ["Positions", "Colors"]); +export const Tooltip = waitForComponent("Tooltip", filters.byCode("shouldShowTooltip:!1", "clickableOnMobile||")); export const Timestamp = waitForComponent("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format")); export const TextInput = waitForComponent("TextInput", ["defaultProps", "Sizes", "contextType"]); export const TextArea = waitForComponent("TextArea", filters.byCode("handleSetRef", "textArea")); @@ -45,6 +45,10 @@ export const Text = waitForComponent("Text", m => { return (s.length < 1500 && s.includes("data-text-variant") && s.includes("always-white")); }); export const Select = waitForComponent("Select", filters.byCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems")); +const searchableSelectFilter = filters.byCode("autoFocus", ".Messages.SELECT"); +export const SearchableSelect = waitForComponent("SearchableSelect", m => + m.render && searchableSelectFilter(m.render) +); export const Slider = waitForComponent("Slider", filters.byCode("closestMarkerIndex", "stickToMarkers")); export const Flex = waitForComponent("Flex", ["Justify", "Align", "Wrap"]); diff --git a/src/webpack/common/types/components.d.ts b/src/webpack/common/types/components.d.ts index 9cd01de2f..4dfa7dc83 100644 --- a/src/webpack/common/types/components.d.ts +++ b/src/webpack/common/types/components.d.ts @@ -90,16 +90,17 @@ export type Tooltip = ComponentType<{ /** Tooltip.Colors.BLACK */ color?: string; - /** Tooltip.Positions.TOP */ + /** TooltipPositions.TOP */ position?: string; tooltipClassName?: string; tooltipContentClassName?: string; }> & { - Positions: Record<"BOTTOM" | "CENTER" | "LEFT" | "RIGHT" | "TOP" | "WINDOW_CENTER", string>; Colors: Record<"BLACK" | "BRAND" | "CUSTOM" | "GREEN" | "GREY" | "PRIMARY" | "RED" | "YELLOW", string>; }; +export type TooltipPositions = Record<"BOTTOM" | "CENTER" | "LEFT" | "RIGHT" | "TOP" | "WINDOW_CENTER", string>; + export type Card = ComponentType & { editable?: boolean; outline?: boolean; @@ -234,6 +235,49 @@ export type Select = ComponentType>; +export type SearchableSelect = ComponentType; // TODO + value?: SelectOption; + + /** + * - 0 ~ Filled + * - 1 ~ Custom + */ + look?: 0 | 1; + className?: string; + popoutClassName?: string; + wrapperClassName?: string; + popoutPosition?: "top" | "left" | "right" | "bottom" | "center" | "window_center"; + optionClassName?: string; + + autoFocus?: boolean; + isDisabled?: boolean; + clearable?: boolean; + closeOnSelect?: boolean; + clearOnSelect?: boolean; + multi?: boolean; + + onChange(value: any): void; + onSearchChange?(value: string): void; + + onClose?(): void; + onOpen?(): void; + onBlur?(): void; + + renderOptionPrefix?(option: SelectOption): ReactNode; + renderOptionSuffix?(option: SelectOption): ReactNode; + + filter?(option: SelectOption[], query: string): SelectOption[]; + + centerCaret?: boolean; + debounceTime?: number; + maxVisibleItems?: number; + popoutWidth?: number; + + "aria-labelledby"?: boolean; +}>>; + export type Slider = ComponentType