From 068be35fdd03c4255edfb316f28844e93e3b10fb Mon Sep 17 00:00:00 2001 From: Neodymium <68879269+Neodymium7@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:40:52 -0800 Subject: [PATCH 1/4] Improve library typings --- lib/settings.ts | 2 +- lib/strings.ts | 14 +++++++++++--- lib/updater.ts | 18 +++++++++++++----- lib/utils/webpack.ts | 20 ++++++++++++++------ tsconfig.json | 2 ++ 5 files changed, 41 insertions(+), 15 deletions(-) diff --git a/lib/settings.ts b/lib/settings.ts index d8eec44..4f33e69 100644 --- a/lib/settings.ts +++ b/lib/settings.ts @@ -115,4 +115,4 @@ type Settings> = S extends SettingsManager; // "foo" * ``` */ -export type SettingsKey, T = any, U = Settings> = KeysOfType; +export type SettingsKey, T = any> = KeysOfType, T>; diff --git a/lib/strings.ts b/lib/strings.ts index cccacee..348b794 100644 --- a/lib/strings.ts +++ b/lib/strings.ts @@ -13,9 +13,17 @@ type LocalesObject = { [code in LocaleCode]?: Record; }; -const LocaleStore = /* @__PURE__ */ Webpack.getStore("LocaleStore"); +type ChangeListener = () => void; -export class StringsManager { +interface LocaleStore { + locale: LocaleCode; + addReactChangeListener: (l: ChangeListener) => void; + removeReactChangeListener: (l: ChangeListener) => void; +} + +const LocaleStore: LocaleStore = /* @__PURE__ */ Webpack.getStore("LocaleStore"); + +export class StringsManager { private locales: T; private defaultLocale: D; private strings: T[D]; @@ -33,7 +41,7 @@ export class StringsManager { } private setLocale = () => { - this.strings = this.locales[LocaleStore.locale] || this.locales[this.defaultLocale]; + this.strings = (this.locales[LocaleStore.locale] as T[D]) || this.locales[this.defaultLocale]; }; /** diff --git a/lib/updater.ts b/lib/updater.ts index 99d81d1..f872610 100644 --- a/lib/updater.ts +++ b/lib/updater.ts @@ -1,14 +1,20 @@ -import { Meta, Net, Logger, UI, Plugins, DOM } from "betterdiscord"; +import { Meta, Net, Logger, UI, Plugins, DOM, CloseNotice } from "betterdiscord"; import { writeFileSync } from "fs"; import { join } from "path"; import { getClasses } from "./utils/webpack"; +interface Updater { + closeUpdateNotice: CloseNotice | undefined; + checkForUpdates(meta: Meta): void; + closeNotice(): void; +} + const hoverClass = getClasses("anchorUnderlineOnHover")?.anchorUnderlineOnHover || ""; -const findVersion = (pluginContents: string) => { +const findVersion = (pluginContents: string): string => { const lines = pluginContents.split("\n"); - const versionLine = lines.find((line) => line.includes("@version")); - return versionLine.split(/\s+/).pop(); + const versionLine = lines.find((line) => line.includes("@version"))!; + return versionLine.split(/\s+/).pop()!; }; const updatePlugin = (name: string, newContents: string) => { @@ -31,7 +37,9 @@ const showUpdateNotice = (name: string, version: string, newContents: string) => }); }; -export const Updater = { +export const Updater: Updater = { + closeUpdateNotice: undefined, + async checkForUpdates(meta: Meta) { const url = `https://raw.githubusercontent.com/Neodymium7/BetterDiscordStuff/main/${meta.name}/${meta.name}.plugin.js`; diff --git a/lib/utils/webpack.ts b/lib/utils/webpack.ts index a7cd1a0..37733fb 100644 --- a/lib/utils/webpack.ts +++ b/lib/utils/webpack.ts @@ -1,4 +1,5 @@ import { Webpack, Logger, SearchOptions, ModuleFilter } from "betterdiscord"; +import React from "react"; interface IconProps { width?: string; @@ -8,7 +9,7 @@ interface IconProps { color?: string; } -type Icon = (props: IconProps) => JSX.Element; +type Icon = (props: IconProps) => React.ReactNode; /** * Options for the `expect` function. @@ -24,6 +25,14 @@ type ExpectOptions = { onError?: () => void; }; +type ExpectOptionsFallback = ExpectOptions & { + fallback: T; +}; + +type ExpectOptionsFatal = ExpectOptions & { + fatal: true; +}; + /** * Options for the `expectModule` function. Takes all options for a normal `getModule` query as well as: * - `filter`: A function to use to filter modules. @@ -75,6 +84,9 @@ export function getIcon(searchString: string): Icon | undefined { }); } +export function expect(object: T, options: ExpectOptionsFallback): NonNullable; +export function expect(object: T, options: ExpectOptionsFatal): NonNullable; +export function expect(object: T, options: ExpectOptions): T | undefined; export function expect(object: T, options: ExpectOptions): T | undefined { if (object) return object; @@ -122,10 +134,6 @@ export function expectClasses(name: string, classes: T[]) { export function expectSelectors(name: string, classes: T[]) { return expect(getSelectors(...classes), { name, - fallback: classes.reduce((obj, key) => { - obj[key] = null; - return obj; - }, {} as { [key in T]: string }), }); } @@ -138,7 +146,7 @@ export function expectSelectors(name: string, classes: T[]) { export function expectIcon(name: string, searchString: string) { return expect(getIcon(searchString), { name, - fallback: (_props: IconProps) => null as unknown as JSX.Element, + fallback: (_props: IconProps) => null, }); } diff --git a/tsconfig.json b/tsconfig.json index 88564ad..c614f2b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,8 @@ { "compilerOptions": { "target": "es2022", + "noImplicitAny": true, + "strict": true, "moduleResolution": "node", "resolveJsonModule": true, "esModuleInterop": true, From 84dcb984707d25fc2e6a4d7749d60d482f1ff253 Mon Sep 17 00:00:00 2001 From: Neodymium <68879269+Neodymium7@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:57:54 -0800 Subject: [PATCH 2/4] Consolidate common modules and improve typings --- ActivityIcons/src/components/ActivityIcon.tsx | 6 +- .../src/components/ListeningIcon.tsx | 4 +- .../src/components/SettingsPanel.tsx | 2 +- ActivityIcons/src/components/WatchingIcon.tsx | 4 +- ActivityIcons/src/index.tsx | 26 ++-- ActivityIcons/src/modules/discordmodules.tsx | 45 ++----- ActivityIcons/src/modules/utils.ts | 2 +- .../src/components/ActivityToggleButton.tsx | 8 +- ActivityToggle/src/index.tsx | 21 ++-- ActivityToggle/src/modules.ts | 43 ++----- .../src/components/SettingsPanel.tsx | 27 +++-- AvatarSettingsButton/src/index.tsx | 42 +++---- .../src/modules/discordmodules.tsx | 60 ++-------- AvatarSettingsButton/src/modules/tooltip.ts | 5 +- .../src/TypingIndicator.tsx | 10 +- ChannelTypingIndicator/src/index.tsx | 11 +- .../src/modules/discordmodules.ts | 32 ++--- ChannelTypingIndicator/src/modules/utils.ts | 5 +- ClickableTextMentions/src/index.tsx | 34 ++++-- ClickableTextMentions/src/modules.tsx | 33 ------ PinnedMessageIcons/src/index.tsx | 23 ++-- .../src/components/SettingsPanel.tsx | 2 +- RoleMentionIcons/src/index.tsx | 19 +-- .../src/modules/discordmodules.tsx | 24 +--- RoleMentionIcons/src/modules/utils.ts | 13 +- TypingUsersPopouts/src/index.tsx | 38 +++--- TypingUsersPopouts/src/modules.tsx | 41 +------ VoiceActivity/src/components/GuildImage.tsx | 7 +- .../src/components/SettingsPanel.tsx | 6 +- VoiceActivity/src/components/VoiceIcon.tsx | 33 +++--- .../src/components/VoiceProfileSection.tsx | 15 +-- VoiceActivity/src/index.tsx | 44 ++++--- VoiceActivity/src/modules/discordmodules.tsx | 112 ++++-------------- VoiceActivity/src/modules/utils.ts | 22 ++-- bundlebd.config.js | 3 +- common/discord/components.tsx | 24 ++++ common/discord/icons.tsx | 42 +++++++ common/discord/modules.ts | 48 ++++++++ common/discord/stores.ts | 22 ++++ common/discord/types.ts | 0 {lib => common/lib}/changelog.ts | 10 +- {lib => common/lib}/index.ts | 0 {lib => common/lib}/settings.ts | 0 {lib => common/lib}/strings.ts | 0 {lib => common/lib}/updater.ts | 0 common/lib/utils/react.tsx | 12 ++ {lib => common/lib}/utils/string.ts | 0 {lib => common/lib}/utils/webpack.ts | 109 ++++++++++------- tsconfig.json | 6 +- 49 files changed, 532 insertions(+), 563 deletions(-) delete mode 100644 ClickableTextMentions/src/modules.tsx create mode 100644 common/discord/components.tsx create mode 100644 common/discord/icons.tsx create mode 100644 common/discord/modules.ts create mode 100644 common/discord/stores.ts create mode 100644 common/discord/types.ts rename {lib => common/lib}/changelog.ts (60%) rename {lib => common/lib}/index.ts (100%) rename {lib => common/lib}/settings.ts (100%) rename {lib => common/lib}/strings.ts (100%) rename {lib => common/lib}/updater.ts (100%) create mode 100644 common/lib/utils/react.tsx rename {lib => common/lib}/utils/string.ts (100%) rename {lib => common/lib}/utils/webpack.ts (78%) diff --git a/ActivityIcons/src/components/ActivityIcon.tsx b/ActivityIcons/src/components/ActivityIcon.tsx index c906085..5063848 100644 --- a/ActivityIcons/src/components/ActivityIcon.tsx +++ b/ActivityIcons/src/components/ActivityIcon.tsx @@ -1,9 +1,9 @@ import { Components } from "betterdiscord"; -import { Icons } from "../modules/discordmodules"; import { Settings, Strings, isBot } from "../modules/utils"; import { Component as Playstation } from "../assets/playstation.svg"; import { Component as Xbox } from "../assets/xbox.svg"; import { parseStringReact } from "@lib/utils/string"; +import { Activity, RichActivity } from "@discord/icons"; interface ActivityIconProps { activities: any[]; @@ -55,10 +55,10 @@ export default function ActivityIcon(props: ActivityIconProps) { }); } - let icon = ; + let icon = ; if (platformIcons && onPS) icon = ; if (platformIcons && onXbox) icon = ; - if (richPresenceIcons && hasRP) icon = ; + if (richPresenceIcons && hasRP) icon = ; return tooltip ? ( diff --git a/ActivityIcons/src/components/ListeningIcon.tsx b/ActivityIcons/src/components/ListeningIcon.tsx index 0415b84..3f81d90 100644 --- a/ActivityIcons/src/components/ListeningIcon.tsx +++ b/ActivityIcons/src/components/ListeningIcon.tsx @@ -1,7 +1,7 @@ import { Components } from "betterdiscord"; -import { Icons } from "../modules/discordmodules"; import { isBot, Settings, Strings } from "../modules/utils"; import { parseString } from "@lib/utils/string"; +import { Headset } from "@discord/icons"; interface ListeningIconProps { activities: any[]; @@ -34,7 +34,7 @@ export default function ListeningIcon(props: ListeningIconProps) { > {(props) => (
- +
)}
diff --git a/ActivityIcons/src/components/SettingsPanel.tsx b/ActivityIcons/src/components/SettingsPanel.tsx index 0d763c0..6ccaa04 100644 --- a/ActivityIcons/src/components/SettingsPanel.tsx +++ b/ActivityIcons/src/components/SettingsPanel.tsx @@ -1,4 +1,4 @@ -import { Common } from "../modules/discordmodules"; +import { Common } from "@discord/components"; import { Settings, Strings } from "../modules/utils"; export default function SettingsPanel() { diff --git a/ActivityIcons/src/components/WatchingIcon.tsx b/ActivityIcons/src/components/WatchingIcon.tsx index ed1337e..fa2e5cd 100644 --- a/ActivityIcons/src/components/WatchingIcon.tsx +++ b/ActivityIcons/src/components/WatchingIcon.tsx @@ -1,6 +1,6 @@ import { Components } from "betterdiscord"; -import { Icons } from "../modules/discordmodules"; import { isBot, Settings } from "../modules/utils"; +import { Screen } from "@discord/icons"; interface WatchingIconProps { activities: any[]; @@ -19,7 +19,7 @@ export default function WatchingIcon(props: WatchingIconProps) { {activity.name}}> {(props) => (
- +
)}
diff --git a/ActivityIcons/src/index.tsx b/ActivityIcons/src/index.tsx index ed54cb4..73466d3 100644 --- a/ActivityIcons/src/index.tsx +++ b/ActivityIcons/src/index.tsx @@ -1,4 +1,4 @@ -import { DOM, Patcher, Meta } from "betterdiscord"; +import { DOM, Patcher, Meta, Plugin, Changes } from "betterdiscord"; import { showChangelog } from "@lib"; import { ActivityStatus, @@ -14,7 +14,7 @@ import ListeningIcon from "./components/ListeningIcon"; import SettingsPanel from "./components/SettingsPanel"; import WatchingIcon from "./components/WatchingIcon"; -export default class ActivityIcons { +export default class ActivityIcons implements Plugin { meta: Meta; constructor(meta: Meta) { @@ -22,17 +22,19 @@ export default class ActivityIcons { } start() { - showChangelog(changelog, this.meta); + showChangelog(changelog as Changes[], this.meta); DOM.addStyle(styles); Strings.subscribe(); this.patchActivityStatus(); } patchActivityStatus() { - Patcher.after(ActivityStatus, "ZP", (_, [props]: [any], ret) => { + if (!ActivityStatus) return; + const [module, key] = ActivityStatus; + Patcher.after(module, key, (_, [props], ret) => { if (!ret) return; - const defaultIconIndex = ret.props.children.findIndex((element) => + const defaultIconIndex = ret.props.children.findIndex((element: any) => element?.props?.className?.startsWith("icon") ); @@ -46,18 +48,20 @@ export default class ActivityIcons { ); }); - forceUpdateAll(memberSelector, (i) => i.user); - forceUpdateAll(peopleListItemSelector, (i) => i.user); - forceUpdateAll(privateChannelSelector); + this.forceUpdateComponents(); + } + + forceUpdateComponents() { + if (memberSelector) forceUpdateAll(memberSelector, (i) => i.user); + if (peopleListItemSelector) forceUpdateAll(peopleListItemSelector, (i) => i.user); + if (privateChannelSelector) forceUpdateAll(privateChannelSelector); } stop() { Patcher.unpatchAll(); DOM.removeStyle(); Strings.unsubscribe(); - forceUpdateAll(memberSelector, (i) => i.user); - forceUpdateAll(peopleListItemSelector, (i) => i.user); - forceUpdateAll(privateChannelSelector); + this.forceUpdateComponents(); } getSettingsPanel() { diff --git a/ActivityIcons/src/modules/discordmodules.tsx b/ActivityIcons/src/modules/discordmodules.tsx index fc10751..aa62478 100644 --- a/ActivityIcons/src/modules/discordmodules.tsx +++ b/ActivityIcons/src/modules/discordmodules.tsx @@ -1,45 +1,16 @@ import { Webpack } from "betterdiscord"; -import { expectModule, expectClasses, expectSelectors, expectIcon } from "@lib/utils/webpack"; +import { expectClasses, expectSelectors, expectWithKey } from "@lib/utils/webpack"; +import { AnyComponent } from "@lib/utils/react"; -const { - Filters: { byKeys, byStrings }, -} = Webpack; - -const Error = (_props) => ( -
-

Error: Component not found

-
-); - -export const ActivityStatus: any = expectModule({ - filter: byStrings("QuestsIcon", "hangStatusActivity"), +export const ActivityStatus = expectWithKey({ + filter: Webpack.Filters.byStrings("QuestsIcon", "hangStatusActivity"), name: "ActivityStatus", - defaultExport: false, - fatal: true, -}); - -export const Icons = { - Activity: expectIcon( - "Activity", - "M20.97 4.06c0 .18.08.35.24.43.55.28.9.82 1.04 1.42.3 1.24.75 3.7.75 7.09v4.91a3.09" - ), - RichActivity: expectIcon("RichActivity", "M6,7 L2,7 L2,6 L6,6 L6,7 Z M8,5 L2,5 L2,4 L8,4"), - Headset: expectIcon("Headset", "M12 3a9 9 0 0 0-8.95 10h1.87a5 5 0 0 1 4.1 2.13l1.37 1.97a3.1 3.1 0 0"), - Screen: expectIcon("Screen", "M5 2a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5ZM13.5 20a.5.5"), -}; - -export const Common = expectModule({ - filter: byKeys("FormSwitch"), - name: "Common", - fallback: { - FormSwitch: Error, - }, }); -export const Margins = expectClasses("Margins", ["marginBottom8"]); +export const marginClasses = expectClasses("Margins", ["marginBottom8"]); -export const peopleListItemSelector = expectSelectors("People List Classes", ["peopleListItem"]).peopleListItem; +export const peopleListItemSelector = expectSelectors("People List Classes", ["peopleListItem"])?.peopleListItem; -export const memberSelector = expectSelectors("Member Class", ["memberInner", "member"]).member; +export const memberSelector = expectSelectors("Member Class", ["memberInner", "member"])?.member; -export const privateChannelSelector = expectSelectors("Private Channel Classes", ["favoriteIcon", "channel"]).channel; +export const privateChannelSelector = expectSelectors("Private Channel Classes", ["favoriteIcon", "channel"])?.channel; diff --git a/ActivityIcons/src/modules/utils.ts b/ActivityIcons/src/modules/utils.ts index 7afa897..38ac728 100644 --- a/ActivityIcons/src/modules/utils.ts +++ b/ActivityIcons/src/modules/utils.ts @@ -12,7 +12,7 @@ export const Settings = new SettingsManager({ export const Strings = new StringsManager(locales, "en-US"); -export function forceUpdateAll(selector: string, propsFilter = (_) => true) { +export function forceUpdateAll(selector: string, propsFilter = (_: any) => true) { const elements: NodeListOf = document.querySelectorAll(selector); for (const element of elements) { const instance = ReactUtils.getInternalInstance(element); diff --git a/ActivityToggle/src/components/ActivityToggleButton.tsx b/ActivityToggle/src/components/ActivityToggleButton.tsx index 57af0ef..c9af95d 100644 --- a/ActivityToggle/src/components/ActivityToggleButton.tsx +++ b/ActivityToggle/src/components/ActivityToggleButton.tsx @@ -1,6 +1,8 @@ import { ContextMenu, UI } from "betterdiscord"; -import { Activity, PanelButton, Sections, Settings, UserSettingsWindow, playSound, ShowCurrentGame } from "../modules"; +import { PanelButton, playSound, ShowCurrentGame } from "../modules"; import ActivityDisabledIcon from "./ActivityDisabledIcon"; +import { Activity, Settings } from "@discord/icons"; +import { SettingsSections, UserSettingsWindow } from "@discord/modules"; export default function ActivityToggleButton() { const activityEnabled = ShowCurrentGame.useSetting(); @@ -14,7 +16,7 @@ export default function ActivityToggleButton() { return UI.alert("Error", "Could not update setting. See the console for more information."); } ShowCurrentGame.updateSetting(!activityEnabled); - playSound(activityEnabled ? "activity_user_left" : "activity_user_join", 0.4); + playSound?.(activityEnabled ? "activity_user_left" : "activity_user_join", 0.4); }} onContextMenu={(e: React.MouseEvent) => { ContextMenu.open( @@ -30,7 +32,7 @@ export default function ActivityToggleButton() { "Could not open settings window. See the console for more information." ); } - UserSettingsWindow.setSection(Sections.ACTIVITY_PRIVACY); + UserSettingsWindow.setSection(SettingsSections.ACTIVITY_PRIVACY); UserSettingsWindow.open(); }, }, diff --git a/ActivityToggle/src/index.tsx b/ActivityToggle/src/index.tsx index b32023a..4e2b7ca 100644 --- a/ActivityToggle/src/index.tsx +++ b/ActivityToggle/src/index.tsx @@ -1,9 +1,9 @@ -import { DOM, Meta, Patcher, ReactUtils, Utils } from "betterdiscord"; +import { DOM, Meta, Patcher, Plugin, ReactUtils, Utils } from "betterdiscord"; import { AccountSelectors } from "./modules"; import ActivityToggleButton from "./components/ActivityToggleButton"; import { Updater } from "@lib/updater"; -export default class ActivityToggle { +export default class ActivityToggle implements Plugin { forceUpdate?: () => void; meta: Meta; @@ -13,18 +13,23 @@ export default class ActivityToggle { start() { Updater.checkForUpdates(this.meta); - DOM.addStyle(`${AccountSelectors.avatarWrapper} { min-width: 70px; }`); + if (AccountSelectors) DOM.addStyle(`${AccountSelectors.avatarWrapper} { min-width: 70px; }`); this.patch(); } patch() { - const owner: any = ReactUtils.getOwnerInstance(document.querySelector(AccountSelectors.container)); + if (!AccountSelectors) return; + const element = document.querySelector(AccountSelectors.container); + if (!element) return; + const owner: any = ReactUtils.getOwnerInstance(element as HTMLElement); + if (!owner) return; const Account = owner._reactInternals.type; this.forceUpdate = owner.forceUpdate.bind(owner); - Patcher.after(Account.prototype, "render", (_that, [_props], ret) => { - const buttonContainerFilter = (i) => - Array.isArray(i?.props?.children) && i.props.children.some((e) => e?.props?.hasOwnProperty("selfMute")); + Patcher.after(Account.prototype, "render", (_that, _args, ret) => { + const buttonContainerFilter = (i: any) => + Array.isArray(i?.props?.children) && + i.props.children.some((e: any) => e?.props?.hasOwnProperty("selfMute")); const buttonContainer = Utils.findInTree(ret.props.children, buttonContainerFilter, { walkable: ["children", "props"], @@ -32,7 +37,7 @@ export default class ActivityToggle { buttonContainer.props.children.unshift(); }); - this.forceUpdate(); + this.forceUpdate?.(); } stop() { diff --git a/ActivityToggle/src/modules.ts b/ActivityToggle/src/modules.ts index f2085aa..c85e4b6 100644 --- a/ActivityToggle/src/modules.ts +++ b/ActivityToggle/src/modules.ts @@ -1,35 +1,19 @@ import { Webpack } from "betterdiscord"; -import { expectModule, expectSelectors, expectIcon } from "@lib/utils/webpack"; +import { byType, expectModule, expectSelectors } from "@lib/utils/webpack"; import React from "react"; +import { EmptyComponent } from "@lib/utils/react"; -const { - Filters: { byKeys, byStrings }, -} = Webpack; - -export const Sections = expectModule({ - filter: byKeys("ACCOUNT", "ACCESSIBILITY"), - searchExports: true, - name: "Sections", - fallback: { - ACTIVITY_PRIVACY: "Activity Privacy", - }, -}); - -export const PanelButton: React.FunctionComponent = expectModule({ - filter: byStrings("PANEL_BUTTON"), +export const PanelButton = expectModule({ + filter: Webpack.Filters.byStrings("PANEL_BUTTON"), name: "PanelButton", - fatal: true, + fallback: EmptyComponent, }); -export const Activity = expectIcon( - "Activity", - "M20.97 4.06c0 .18.08.35.24.43.55.28.9.82 1.04 1.42.3 1.24.75 3.7.75 7.09v4.91a3.09" -); - -export const Settings = expectIcon("Settings", "M10.56 1.1c-.46.05-.7.53-.64.98.18 1.16-.19 2.2-.98 2.53"); - -export const playSound: (id: string, vol: number) => void = expectModule({ - filter: byStrings("Unable to find sound for pack name:"), +export const playSound = expectModule<(sound: string, volume: number) => void>({ + filter: Webpack.Filters.combine( + Webpack.Filters.byStrings("Unable to find sound for pack name:"), + byType("function") + ), name: "playSound", searchExports: true, }); @@ -40,16 +24,11 @@ export const ShowCurrentGame = expectModule({ fallback: { G6: { useSetting: () => React.useState(true), - updateSetting: undefined as (...args: any) => void, + updateSetting: undefined as ((...args: any) => void) | undefined, }, }, })?.G6; -export const UserSettingsWindow: any = expectModule({ - filter: byKeys("open", "updateAccount"), - name: "UserSettingsWindow", -}); - export const AccountSelectors = expectSelectors("Account Classes", [ "avatarWrapper", "accountProfilePopoutWrapper", diff --git a/AvatarSettingsButton/src/components/SettingsPanel.tsx b/AvatarSettingsButton/src/components/SettingsPanel.tsx index cd2c9d1..2dbdf7c 100644 --- a/AvatarSettingsButton/src/components/SettingsPanel.tsx +++ b/AvatarSettingsButton/src/components/SettingsPanel.tsx @@ -1,4 +1,5 @@ -import { Margins, Common } from "../modules/discordmodules"; +import { Common } from "@discord/components"; +import { marginClasses } from "../modules/discordmodules"; import { Settings, Strings } from "../modules/utils"; const { RadioGroup, FormItem, FormText, FormDivider, FormSwitch } = Common; @@ -9,7 +10,7 @@ export default function SettingsPanel() { return ( <> - + {Strings.get("SETTINGS_CLICK_NOTE")} Settings.set("click", value)} + onChange={({ value }: { value: number }) => Settings.set("click", value)} value={settings.click} /> - + - - + + {Strings.get("SETTINGS_RIGHT_CLICK_NOTE")} Settings.set("contextmenu", value)} + onChange={({ value }: { value: number }) => Settings.set("contextmenu", value)} value={settings.contextmenu} /> - + - - + + {Strings.get("SETTINGS_MIDDLE_CLICK_NOTE")} Settings.set("middleclick", value)} + onChange={({ value }: { value: number }) => Settings.set("middleclick", value)} value={settings.middleclick} /> - + Settings.set("showTooltip", v)} diff --git a/AvatarSettingsButton/src/index.tsx b/AvatarSettingsButton/src/index.tsx index b61bfc5..c219a0d 100644 --- a/AvatarSettingsButton/src/index.tsx +++ b/AvatarSettingsButton/src/index.tsx @@ -1,26 +1,27 @@ -import { DOM, Meta } from "betterdiscord"; +import { Changes, DOM, Meta, Plugin } from "betterdiscord"; import { showChangelog } from "@lib"; import { changelog } from "./manifest.json"; -import { Sections, UserSettingsWindow, accountClasses } from "./modules/discordmodules"; +import { accountClasses } from "./modules/discordmodules"; import { Settings, Strings } from "./modules/utils"; import Tooltip from "./modules/tooltip"; import SettingsPanel from "./components/SettingsPanel"; +import { UserSettingsWindow, SettingsSections } from "@discord/modules"; const settingsSelector = `.${accountClasses.container} button:nth-last-child(1)`; -export default class AvatarSettingsButton { +export default class AvatarSettingsButton implements Plugin { meta: Meta; - target: Element = null; - tooltip: Tooltip = null; - clearListeners: () => void; - lastContextMenuTimestamp: number; + target: HTMLElement | null = null; + tooltip: Tooltip | null = null; + clearListeners?: () => void; + lastContextMenuTimestamp?: number; constructor(meta: Meta) { this.meta = meta; } start() { - showChangelog(changelog, this.meta); + showChangelog(changelog as Changes[], this.meta); DOM.addStyle(`${settingsSelector} { display: none; } .${accountClasses.avatarWrapper} { width: 100%; }`); Strings.subscribe(); Settings.addListener(() => { @@ -33,12 +34,13 @@ export default class AvatarSettingsButton { this.addTooltip(); } - observer({ addedNodes }) { + observer({ addedNodes }: MutationRecord) { for (const node of addedNodes) { if (node.nodeType === Node.TEXT_NODE) continue; + if (!(node instanceof HTMLElement)) continue; const avatarWrapper = node.querySelector(`.${accountClasses.avatarWrapper}`); - if (avatarWrapper) { + if (avatarWrapper instanceof HTMLElement) { this.target = avatarWrapper; this.addListeners(); this.addTooltip(); @@ -47,7 +49,7 @@ export default class AvatarSettingsButton { } openPopout() { - this.target.dispatchEvent( + this.target?.dispatchEvent( new MouseEvent("click", { bubbles: true, }) @@ -55,14 +57,14 @@ export default class AvatarSettingsButton { } openSettings() { - UserSettingsWindow.setSection(Sections.ACCOUNT); + UserSettingsWindow?.setSection(SettingsSections.ACCOUNT); if (document.querySelector(`.${accountClasses.accountProfilePopoutWrapper}`)) this.openPopout(); - UserSettingsWindow.open(); + UserSettingsWindow?.open(); } openContextMenu(e: MouseEvent) { if (document.querySelector(`.${accountClasses.accountProfilePopoutWrapper}`)) this.openPopout(); - document.querySelector(settingsSelector).dispatchEvent( + document.querySelector(settingsSelector)?.dispatchEvent( new MouseEvent("contextmenu", { bubbles: true, clientX: e.clientX, @@ -87,7 +89,7 @@ export default class AvatarSettingsButton { if (e.isTrusted) { e.preventDefault(); e.stopPropagation(); - clickAction(e); + clickAction?.(e); this.tooltip?.forceHide(); } }; @@ -100,14 +102,14 @@ export default class AvatarSettingsButton { } this.lastContextMenuTimestamp = e.timeStamp; - contextmenuAction(e); + contextmenuAction?.(e); this.tooltip?.forceHide(); }; const middleclickAction = actions[Settings.get("middleclick")]; const middleclick = (e: MouseEvent) => { if (e.button === 1) { - middleclickAction(e); + middleclickAction?.(e); this.tooltip?.forceHide(); } }; @@ -117,9 +119,9 @@ export default class AvatarSettingsButton { this.target.addEventListener("mousedown", middleclick); this.clearListeners = () => { - this.target.removeEventListener("click", click); - this.target.removeEventListener("contextmenu", contextmenu); - this.target.removeEventListener("mousedown", middleclick); + this.target?.removeEventListener("click", click); + this.target?.removeEventListener("contextmenu", contextmenu); + this.target?.removeEventListener("mousedown", middleclick); }; } diff --git a/AvatarSettingsButton/src/modules/discordmodules.tsx b/AvatarSettingsButton/src/modules/discordmodules.tsx index 6137a37..7767b89 100644 --- a/AvatarSettingsButton/src/modules/discordmodules.tsx +++ b/AvatarSettingsButton/src/modules/discordmodules.tsx @@ -1,54 +1,12 @@ -import { Webpack } from "betterdiscord"; -import { expectModule, expectClasses, expectSelectors } from "@lib/utils/webpack"; +import { expectClasses, expectSelectors } from "@lib/utils/webpack"; -const { - Filters: { byKeys }, -} = Webpack; - -interface AccountClasses { - container: string; - avatarWrapper: string; - accountProfilePopoutWrapper: string; -} - -const Error = (_props) => ( -
-

Error: Component not found

-
-); - -export const Common = expectModule({ - filter: byKeys("FormSwitch", "RadioGroup", "FormItem", "FormText", "FormDivider"), - name: "Common", - fallback: { - FormSwitch: Error, - RadioGroup: Error, - FormItem: Error, - FormText: Error, - FormDivider: Error, - }, -}); - -export const UserSettingsWindow: any = expectModule({ - filter: byKeys("saveAccountChanges"), - name: "UserSettingsWindow", - fatal: true, -}); - -export const Sections = expectModule({ - filter: byKeys("ACCOUNT", "CHANGE_LOG"), - searchExports: true, - name: "Sections", - fallback: { ACCOUNT: "My Account" }, -}); - -export const accountClasses = expectModule({ - filter: byKeys("accountProfilePopoutWrapper"), - name: "Account Classes", - fatal: true, -}); +export const accountClasses = expectClasses("Account Classes", [ + "accountProfilePopoutWrapper", + "container", + "avatarWrapper", +]); -export const Margins = expectClasses("Margins", ["marginTop20", "marginBottom8"]); +export const marginClasses = expectClasses("Margin Classes", ["marginTop20", "marginBottom8"]); export const tooltipClasses = expectClasses("Tooltip Classes", [ "tooltip", @@ -58,6 +16,6 @@ export const tooltipClasses = expectClasses("Tooltip Classes", [ "tooltipContent", ]); -export const layerContainerSelector = expectSelectors("Layer Container Class", ["layerContainer"]).layerContainer; +export const layerContainerSelector = expectSelectors("Layer Container Class", ["layerContainer"])?.layerContainer; -export const appSelector = expectSelectors("App Class", ["appAsidePanelWrapper", "app"]).app; +export const appSelector = expectSelectors("App Class", ["appAsidePanelWrapper", "app"])?.app; diff --git a/AvatarSettingsButton/src/modules/tooltip.ts b/AvatarSettingsButton/src/modules/tooltip.ts index 0991c5c..dd8f36b 100644 --- a/AvatarSettingsButton/src/modules/tooltip.ts +++ b/AvatarSettingsButton/src/modules/tooltip.ts @@ -4,12 +4,12 @@ export default class Tooltip { private target: HTMLElement; private tooltip: HTMLElement; private layerContainer: HTMLElement; - private ref: HTMLElement; + private ref: HTMLElement | null = null; private clearListeners: () => void; constructor(target: HTMLElement, text: string) { this.target = target; - this.layerContainer = document.querySelector(`${appSelector} ~ ${layerContainerSelector}`); + this.layerContainer = document.querySelector(`${appSelector} ~ ${layerContainerSelector}`)!; const pointer = document.createElement("div"); pointer.className = tooltipClasses.tooltipPointer; @@ -55,6 +55,7 @@ export default class Tooltip { hide() { const ref = this.ref; + if (!ref) return; ref.style.opacity = "0"; ref.style.transform = "scale(0.95)"; setTimeout(() => ref?.remove(), 100); diff --git a/ChannelTypingIndicator/src/TypingIndicator.tsx b/ChannelTypingIndicator/src/TypingIndicator.tsx index 5ba59b6..8845628 100644 --- a/ChannelTypingIndicator/src/TypingIndicator.tsx +++ b/ChannelTypingIndicator/src/TypingIndicator.tsx @@ -1,9 +1,15 @@ import { Components } from "betterdiscord"; -import { RelationshipStore, TypingDots, TypingStore, UserStore, useStateFromStores } from "./modules/discordmodules"; +import { TypingDots } from "./modules/discordmodules"; import { getDisplayName, Strings } from "./modules/utils"; import { parseStringReact } from "@lib/utils/string"; +import { RelationshipStore, TypingStore, UserStore, useStateFromStores } from "@discord/stores"; -export function TypingIndicator({ channelId, guildId }) { +interface TypingIndicatorProps { + channelId: string; + guildId: string; +} + +export function TypingIndicator({ channelId, guildId }: TypingIndicatorProps) { const typingUsersState = useStateFromStores([TypingStore], () => TypingStore.getTypingUsers(channelId)); const typingUsersIds = Object.keys(typingUsersState).filter( diff --git a/ChannelTypingIndicator/src/index.tsx b/ChannelTypingIndicator/src/index.tsx index 794fa83..3fdf466 100644 --- a/ChannelTypingIndicator/src/index.tsx +++ b/ChannelTypingIndicator/src/index.tsx @@ -1,10 +1,10 @@ -import { DOM, Meta, Patcher, Utils } from "betterdiscord"; +import { DOM, Meta, Patcher, Plugin, Utils } from "betterdiscord"; import { Channel, Thread } from "./modules/discordmodules"; import { TypingIndicator } from "./TypingIndicator"; import { Strings } from "./modules/utils"; import { Updater } from "@lib/updater"; -export default class ChannelTypingIndicator { +export default class ChannelTypingIndicator implements Plugin { meta: Meta; constructor(meta: Meta) { @@ -20,7 +20,9 @@ export default class ChannelTypingIndicator { } patchChannel() { - Patcher.after(Channel, "Z", (_, [props]: [any], ret) => { + if (!Channel) return; + const [module, key] = Channel; + Patcher.after(module, key, (_, [props], ret) => { const target = Utils.findInTree(ret, (x) => x?.className?.includes("linkTop"), { walkable: ["props", "children"], }); @@ -29,7 +31,8 @@ export default class ChannelTypingIndicator { } patchThread() { - Patcher.after(Thread, "type", (_, [props]: [any], ret) => { + if (!Thread) return; + Patcher.after(Thread, "type", (_, [props], ret) => { const target = Utils.findInTree(ret, (x) => x?.className?.includes("linkTop"), { walkable: ["props", "children"], }); diff --git a/ChannelTypingIndicator/src/modules/discordmodules.ts b/ChannelTypingIndicator/src/modules/discordmodules.ts index de6c7f6..4186b72 100644 --- a/ChannelTypingIndicator/src/modules/discordmodules.ts +++ b/ChannelTypingIndicator/src/modules/discordmodules.ts @@ -1,36 +1,20 @@ import { Webpack } from "betterdiscord"; -import { expectModule } from "@lib/utils/webpack"; +import { expectModule, expectWithKey } from "@lib/utils/webpack"; +import { AnyComponent, AnyMemo, EmptyComponent } from "@lib/utils/react"; -const { - Filters: { byStrings }, -} = Webpack; - -export const Channel: any = expectModule({ - filter: byStrings("UNREAD_LESS_IMPORTANT"), +export const Channel = expectWithKey({ + filter: Webpack.Filters.byStrings("UNREAD_LESS_IMPORTANT"), name: "TypingUsersContainer", - defaultExport: false, }); -export const Thread: any = expectModule({ - filter: (m) => m?.type && byStrings("thread:", "GUILD_CHANNEL_LIST")(m.type), +export const Thread = expectModule({ + filter: (m) => m?.type && Webpack.Filters.byStrings("thread:", "GUILD_CHANNEL_LIST")(m.type), name: "Thread", }); -export const TypingDots: any = expectModule({ +export const TypingDots = expectModule({ filter: (m) => m?.type?.render?.toString().includes("dotRadius"), name: "TypingDots", searchExports: true, - fatal: true, + fallback: EmptyComponent, }); - -export const useStateFromStores: any = expectModule({ - filter: byStrings("useStateFromStores"), - name: "Flux", - searchExports: true, - fatal: true, -}); - -export const UserStore = Webpack.getStore("UserStore"); -export const GuildMemberStore = Webpack.getStore("GuildMemberStore"); -export const RelationshipStore = Webpack.getStore("RelationshipStore"); -export const TypingStore = Webpack.getStore("TypingStore"); diff --git a/ChannelTypingIndicator/src/modules/utils.ts b/ChannelTypingIndicator/src/modules/utils.ts index 607d48f..736fdd2 100644 --- a/ChannelTypingIndicator/src/modules/utils.ts +++ b/ChannelTypingIndicator/src/modules/utils.ts @@ -1,11 +1,12 @@ import { StringsManager } from "@lib"; import locales from "../locales.json"; -import { GuildMemberStore, UserStore } from "./discordmodules"; +import { GuildMemberStore, UserStore } from "@discord/stores"; export const Strings = new StringsManager(locales, "en-US"); export const getDisplayName = (userId: string, guildId: string): string => { const { nick } = GuildMemberStore.getMember(guildId, userId); if (nick) return nick; - return UserStore.getUser(userId).globalName; + const user = UserStore.getUser(userId); + return user.globalName || user.username; }; diff --git a/ClickableTextMentions/src/index.tsx b/ClickableTextMentions/src/index.tsx index 818fecd..38634e9 100644 --- a/ClickableTextMentions/src/index.tsx +++ b/ClickableTextMentions/src/index.tsx @@ -1,39 +1,51 @@ -import { Patcher, Webpack, Logger, Meta } from "betterdiscord"; -import { Popout, loadProfile, UserPopout, UserStore } from "./modules"; +import { Patcher, Webpack, Logger, Meta, Plugin } from "betterdiscord"; import { Updater } from "@lib/updater"; +import { AnyComponent } from "@lib/utils/react"; +import { UserStore } from "@discord/stores"; +import { Common, UserPopout } from "@discord/components"; +import { loadProfile } from "@discord/modules"; const { getWithKey, Filters: { byStrings }, } = Webpack; -const [Module, key] = getWithKey(byStrings(".hidePersonalInformation", "#", "<@", ".discriminator")); +const [Module, key] = getWithKey(byStrings(".hidePersonalInformation", "#", "<@", ".discriminator")); if (!Module) Logger.error("Text area mention module not found."); const onClick = (e: React.MouseEvent) => { e.preventDefault(); }; -function PopoutWrapper({ id, guildId, channelId, children }) { +interface PopoutWrapperProps { + id: string; + guildId: string; + channelId: string; + children: React.ReactElement; +} + +function PopoutWrapper({ id, guildId, channelId, children }: PopoutWrapperProps) { // Disable default click action children.props.onClick = onClick; const user = UserStore.getUser(id); return ( - } - preload={() => loadProfile(user.id, user.getAvatarURL(guildId, 80), { guildId, channelId })} + renderPopout={(props: any) => ( + + )} + preload={() => loadProfile?.(user.id, user.getAvatarURL(guildId, 80), { guildId, channelId })} > - {(props) => {children}} - + {(props: any) => {children}} + ); } -export default class ClickableTextMentions { +export default class ClickableTextMentions implements Plugin { meta: Meta; constructor(meta: Meta) { @@ -51,7 +63,7 @@ export default class ClickableTextMentions { Patcher.after(Module, key, (_, [props]: [any], ret) => { const original = ret.props.children; - ret.props.children = (childrenProps) => { + ret.props.children = (childrenProps: any) => { const mention = original(childrenProps).props.children; return {mention}; }; diff --git a/ClickableTextMentions/src/modules.tsx b/ClickableTextMentions/src/modules.tsx deleted file mode 100644 index e2022c0..0000000 --- a/ClickableTextMentions/src/modules.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Webpack } from "betterdiscord"; -import { expectModule } from "@lib/utils/webpack"; - -const { - Filters: { byStrings, byKeys }, -} = Webpack; - -const ErrorPopout = (props: { message: string }) => ( -
- {props.message} -
-); - -export const UserPopout = expectModule({ - filter: (m) => m.toString?.().includes("UserProfilePopoutWrapper"), - name: "UserPopout", - fallback: (_props: any) => , -}); - -export const Popout = expectModule({ - filter: byKeys("Popout"), - name: "Common", - fallback: { - Popout: (props: any) => props.children(), - }, -}).Popout; - -export const loadProfile: any = expectModule({ - filter: byStrings("preloadUserBanner"), - name: "loadProfile", -}); - -export const UserStore = Webpack.getStore("UserStore"); diff --git a/PinnedMessageIcons/src/index.tsx b/PinnedMessageIcons/src/index.tsx index 8926aa8..fc690d5 100644 --- a/PinnedMessageIcons/src/index.tsx +++ b/PinnedMessageIcons/src/index.tsx @@ -1,19 +1,17 @@ -import { DOM, Patcher, Webpack, Logger, UI, Data, Meta } from "betterdiscord"; +import { DOM, Patcher, Webpack, Logger, UI, Data, Meta, Plugin } from "betterdiscord"; import { getSelectors, getIcon } from "@lib/utils/webpack"; import { Updater } from "@lib/updater"; - -const { getModule } = Webpack; +import { AnyComponent } from "@lib/utils/react"; const Pin = getIcon("M19.38 11.38a3 3 0 0 0 4.24 0l.03-.03a.5.5 0 0 0 0-.7L13.35.35a.5.5"); -const Message = getModule((m) => m.Z?.toString?.().includes("childrenRepliedMessage")); +const Message = Webpack.getWithKey(Webpack.Filters.byStrings("childrenRepliedMessage", "focusProps")); const messageSelectors = getSelectors("message", "mentioned", "replying"); -if (!Message) Logger.error("Message module not found"); if (!Pin) Logger.error("Pin icon not found."); if (!messageSelectors) Logger.error("Message selectors icon not found."); -export default class PinnedMessageIcons { - settings: { backgroundEnabled: boolean }; +export default class PinnedMessageIcons implements Plugin { + settings!: { backgroundEnabled: boolean }; meta: Meta; constructor(meta: Meta) { @@ -23,8 +21,6 @@ export default class PinnedMessageIcons { start() { Updater.checkForUpdates(this.meta); - if (!Message) return; - this.settings = Data.load("settings"); if (!this.settings) { this.settings = { backgroundEnabled: true }; @@ -36,7 +32,10 @@ export default class PinnedMessageIcons { } patch() { - Patcher.after(Message, "Z", (_, [props]: [any], ret) => { + const [module, key] = Message; + if (!module) return Logger.error("Message module not found"); + + Patcher.after(module, key, (_, [props], ret) => { if (!props.childrenMessageContent.props.message) return ret; const isPinned = props.childrenMessageContent.props.message.pinned; @@ -89,8 +88,8 @@ export default class PinnedMessageIcons { value: this.settings.backgroundEnabled, }, ], - onChange: (_, id, value) => { - this.settings[id] = value; + onChange: (_, _id, value) => { + this.settings.backgroundEnabled = value; DOM.removeStyle(); this.addStyle(); Data.save("settings", this.settings); diff --git a/RoleMentionIcons/src/components/SettingsPanel.tsx b/RoleMentionIcons/src/components/SettingsPanel.tsx index dc63f1c..7243d0f 100644 --- a/RoleMentionIcons/src/components/SettingsPanel.tsx +++ b/RoleMentionIcons/src/components/SettingsPanel.tsx @@ -1,4 +1,4 @@ -import { Common } from "../modules/discordmodules"; +import { Common } from "@discord/components"; import { Settings, Strings } from "../modules/utils"; export default function SettingsPanel() { diff --git a/RoleMentionIcons/src/index.tsx b/RoleMentionIcons/src/index.tsx index ef25030..855bdc6 100644 --- a/RoleMentionIcons/src/index.tsx +++ b/RoleMentionIcons/src/index.tsx @@ -1,11 +1,12 @@ -import { DOM, Meta } from "betterdiscord"; +import { DOM, Meta, Plugin, Changes } from "betterdiscord"; import { showChangelog } from "@lib"; import { changelog } from "./manifest.json"; import SettingsPanel from "./components/SettingsPanel"; -import { GuildStore, roleMention } from "./modules/discordmodules"; +import { roleMention } from "./modules/discordmodules"; import { Settings, Strings, filter, getIconElement, getProps, peopleSVG } from "./modules/utils"; +import { GuildStore } from "@discord/stores"; -export default class RoleMentionIcons { +export default class RoleMentionIcons implements Plugin { clearCallbacks: Set<() => void>; meta: Meta; @@ -15,7 +16,7 @@ export default class RoleMentionIcons { } start() { - showChangelog(changelog, this.meta); + showChangelog(changelog as Changes[], this.meta); DOM.addStyle( `.role-mention-icon { position: relative; height: 1em; width: 1em; margin-left: 4px; } .${roleMention} { display: inline-flex; align-items: center; }` ); @@ -25,31 +26,33 @@ export default class RoleMentionIcons { this.processElements(elements); } - observer({ addedNodes, removedNodes }) { + observer({ addedNodes, removedNodes }: MutationRecord) { for (const node of addedNodes) { if (node.nodeType === Node.TEXT_NODE) continue; + if (!(node instanceof HTMLElement)) continue; const elements = Array.from(node.getElementsByClassName(roleMention)); this.processElements(elements); } for (const node of removedNodes) { if (node.nodeType === Node.TEXT_NODE) continue; + if (!(node instanceof HTMLElement)) continue; if (node.querySelector(".role-mention-icon")) this.clearCallbacks.clear(); } } - processElements(elements) { + processElements(elements: Element[]) { if (!elements.length) return; for (const element of elements) { - const props = getProps(element, (e) => e.roleName || e.roleId); + const props = getProps(element as HTMLElement, (e) => e.roleName || e.roleId); if (!props) return; const isEveryone = props.roleName === "@everyone"; const isHere = props.roleName === "@here"; let role; if (props.guildId) { - role = filter(GuildStore.getRoles(props.guildId), (r) => r.id === props.roleId); + role = filter(GuildStore.getRoles(props.guildId), (r: any) => r.id === props.roleId); role = role[Object.keys(role)[0]]; } if ((Settings.get("everyone") || !isEveryone) && (Settings.get("here") || !isHere)) { diff --git a/RoleMentionIcons/src/modules/discordmodules.tsx b/RoleMentionIcons/src/modules/discordmodules.tsx index bac8d86..e337c1c 100644 --- a/RoleMentionIcons/src/modules/discordmodules.tsx +++ b/RoleMentionIcons/src/modules/discordmodules.tsx @@ -1,25 +1,3 @@ -import { Webpack } from "betterdiscord"; -import { expectModule, expectClasses } from "@lib/utils/webpack"; - -const { - Filters: { byKeys }, - getStore, -} = Webpack; - -const Error = (_props) => ( -
-

Error: Component not found

-
-); - -export const Common = expectModule({ - filter: byKeys("FormSwitch"), - name: "Common", - fallback: { - FormSwitch: Error, - }, -}); +import { expectClasses } from "@lib/utils/webpack"; export const roleMention = expectClasses("Role Mention Class", ["roleMention"]).roleMention.split(" ")[0]; - -export const GuildStore = getStore("GuildStore"); diff --git a/RoleMentionIcons/src/modules/utils.ts b/RoleMentionIcons/src/modules/utils.ts index 76ec82d..832dcb0 100644 --- a/RoleMentionIcons/src/modules/utils.ts +++ b/RoleMentionIcons/src/modules/utils.ts @@ -1,4 +1,4 @@ -import { ReactUtils } from "betterdiscord"; +import { DOM, ReactUtils } from "betterdiscord"; import { SettingsManager, StringsManager } from "@lib"; import locales from "../locales.json"; @@ -10,12 +10,9 @@ export const Settings = new SettingsManager({ export const Strings = new StringsManager(locales, "en-US"); -export const peopleSVG = (() => { - const element = document.createElement("div"); - element.innerHTML = - ''; - return element.firstChild; -})(); +export const peopleSVG = DOM.parseHTML( + '' +) as HTMLElement; export const getIconElement = (roleId: string, roleIcon: string) => { const icon = document.createElement("img"); @@ -29,7 +26,7 @@ export const getIconElement = (roleId: string, roleIcon: string) => { // @ts-ignore // From https://github.com/rauenzi/BetterDiscordAddons/blob/692abbd1877ff6d837dc8a606767d019e52ebe23/Plugins/RoleMembers/RoleMembers.plugin.js#L60-L61 const from = (arr) => arr && arr.length > 0 && Object.assign(...arr.map(([k, v]) => ({ [k]: v }))); -export const filter = (obj, predicate) => +export const filter = (obj: any, predicate: any) => from( Object.entries(obj).filter((o) => { return predicate(o[1]); diff --git a/TypingUsersPopouts/src/index.tsx b/TypingUsersPopouts/src/index.tsx index cbbb6aa..b63b082 100644 --- a/TypingUsersPopouts/src/index.tsx +++ b/TypingUsersPopouts/src/index.tsx @@ -1,19 +1,14 @@ -import { DOM, Patcher, Utils, Meta } from "betterdiscord"; +import { DOM, Patcher, Utils, Meta, Plugin, Changes } from "betterdiscord"; import { showChangelog } from "@lib"; import { changelog } from "./manifest.json"; -import { - Common, - RelationshipStore, - UserPopout, - UserStore, - loadProfile, - typingSelector, - TypingUsersContainer, -} from "./modules"; +import { typingSelector, TypingUsersContainer } from "./modules"; +import { RelationshipStore, UserStore } from "@discord/stores"; +import { Common, UserPopout } from "@discord/components"; +import { loadProfile } from "@discord/modules"; const nameSelector = `${typingSelector} strong`; -export default class TypingUsersPopouts { +export default class TypingUsersPopouts implements Plugin { meta: Meta; constructor(meta: Meta) { @@ -21,13 +16,15 @@ export default class TypingUsersPopouts { } start() { - showChangelog(changelog, this.meta); + showChangelog(changelog as Changes[], this.meta); DOM.addStyle(`${nameSelector} { cursor: pointer; } ${nameSelector}:hover { text-decoration: underline; }`); this.patch(); } patch() { - const patchType = (props, ret) => { + if (!TypingUsersContainer) return; + + const patchType = (props: any, ret: any) => { const text = Utils.findInTree(ret, (e) => e.children?.length && e.children[0]?.type === "strong", { walkable: ["props", "children"], }); @@ -40,7 +37,7 @@ export default class TypingUsersPopouts { const guildId = channel.guild_id; let i = 0; - text.children = text.children.map((e) => { + text.children = text.children.map((e: React.ReactElement) => { if (e.type !== "strong") return e; const user = UserStore.getUser(typingUsersIds[i++]); @@ -50,28 +47,29 @@ export default class TypingUsersPopouts { align="left" position="top" key={user.id} - renderPopout={(props) => ( + renderPopout={(props: any) => ( )} preload={() => - loadProfile(user.id, user.getAvatarURL(guildId, 80), { guildId, channelId: channel.id }) + loadProfile?.(user.id, user.getAvatarURL(guildId, 80), { guildId, channelId: channel.id }) } > - {(props) => } + {(props: any) => } ); }); }; - let patchedType; + let patchedType: ((props: any) => React.ReactNode) | undefined; - Patcher.after(TypingUsersContainer, "Z", (_, __, containerRet) => { + const [module, key] = TypingUsersContainer; + Patcher.after(module, key, (_, __, containerRet) => { if (patchedType) { containerRet.type = patchedType; return containerRet; } - const original = containerRet.type; + const original = containerRet.type as React.FunctionComponent; patchedType = (props) => { const ret = original(props); diff --git a/TypingUsersPopouts/src/modules.tsx b/TypingUsersPopouts/src/modules.tsx index 661490c..02aecb4 100644 --- a/TypingUsersPopouts/src/modules.tsx +++ b/TypingUsersPopouts/src/modules.tsx @@ -1,42 +1,11 @@ +import { AnyComponent } from "@lib/utils/react"; +import { expectSelectors, expectWithKey } from "@lib/utils/webpack"; import { Webpack } from "betterdiscord"; -import { expectModule, expectSelectors } from "@lib/utils/webpack"; -const { - Filters: { byStrings, byKeys }, -} = Webpack; - -const ErrorPopout = (props: { message: string }) => ( -
- {props.message} -
-); - -export const TypingUsersContainer: any = expectModule({ - filter: (m) => m.Z?.toString?.().includes("typingUsers:"), +export const TypingUsersContainer = expectWithKey({ + filter: Webpack.Filters.byStrings("typingUsers:"), name: "TypingUsersContainer", fatal: true, }); -export const UserPopout = expectModule({ - filter: (m) => m.toString?.().includes("UserProfilePopoutWrapper"), - name: "UserPopout", - fallback: (_props: any) => , -}); - -export const Common = expectModule({ - filter: byKeys("Popout"), - name: "Common", - fallback: { - Popout: (props: any) => props.children(), - }, -}); - -export const loadProfile: any = expectModule({ - filter: byStrings("preloadUserBanner"), - name: "loadProfile", -}); - -export const typingSelector = expectSelectors("Typing Class", ["typingDots", "typing"]).typing; - -export const UserStore = Webpack.getStore("UserStore"); -export const RelationshipStore = Webpack.getStore("RelationshipStore"); +export const typingSelector = expectSelectors("Typing Class", ["typingDots", "typing"])?.typing; diff --git a/VoiceActivity/src/components/GuildImage.tsx b/VoiceActivity/src/components/GuildImage.tsx index 62d3d7a..d6479f1 100644 --- a/VoiceActivity/src/components/GuildImage.tsx +++ b/VoiceActivity/src/components/GuildImage.tsx @@ -1,6 +1,7 @@ -import { GuildActions, getAcronym, transitionTo } from "../modules/discordmodules"; +import { getAcronym } from "../modules/discordmodules"; import styles from "../styles/guildimage.module.scss"; import defaultGroupIcon from "../assets/default_group_icon.png"; +import { GuildActions, transitionTo } from "@discord/modules"; interface GuildImageProps { guild: any; @@ -17,7 +18,7 @@ const getIconFontSize = (name: string) => { }; const getImageLink = (guild: any, channel: any) => { - let image: string; + let image = ""; if (guild && guild.icon) { image = `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.webp?size=96`; } else if (channel.icon) { @@ -33,7 +34,7 @@ export default function GuildImage(props: GuildImageProps) { const onClick = () => { if (props.guild) GuildActions?.transitionToGuildSync(props.guild.id); - else if (props.channelPath) transitionTo(props.channelPath); + else if (props.channelPath) transitionTo?.(props.channelPath); }; if (image) { diff --git a/VoiceActivity/src/components/SettingsPanel.tsx b/VoiceActivity/src/components/SettingsPanel.tsx index 467623a..3634ba2 100644 --- a/VoiceActivity/src/components/SettingsPanel.tsx +++ b/VoiceActivity/src/components/SettingsPanel.tsx @@ -1,6 +1,6 @@ import { SettingsKey } from "@lib"; -import { Common } from "../modules/discordmodules"; import { Settings, Strings } from "../modules/utils"; +import { Common } from "@discord/components"; type SwitchSetting = SettingsKey; @@ -52,7 +52,7 @@ const settings: SettingsInfo = { }, }; -const SettingsSwitchItem = (props: SwitchItemProps) => { +const SettingsSwitchItem: React.FunctionComponent = (props) => { const value = Settings.useSettingsState(props.setting)[props.setting]; return ( @@ -70,7 +70,7 @@ const SettingsSwitchItem = (props: SwitchItemProps) => { export default function SettingsPanel() { return ( <> - {Object.keys(settings).map((key: SwitchSetting) => { + {(Object.keys(settings) as SwitchSetting[]).map((key) => { const { name, note } = settings[key]; return ; })} diff --git a/VoiceActivity/src/components/VoiceIcon.tsx b/VoiceActivity/src/components/VoiceIcon.tsx index 58ea7c3..4acd832 100644 --- a/VoiceActivity/src/components/VoiceIcon.tsx +++ b/VoiceActivity/src/components/VoiceIcon.tsx @@ -1,15 +1,16 @@ +import { ChannelStore, GuildStore, UserStore } from "@discord/stores"; import { Settings, Strings, groupDMName, canViewChannel, useUserVoiceState } from "../modules/utils"; -import { Common, Icons, Stores, transitionTo } from "../modules/discordmodules"; import styles from "../styles/voiceicon.module.scss"; +import { CallJoin, Deafened, Muted, People, Speaker, Stage, Video } from "@discord/icons"; +import { Components } from "betterdiscord"; +import { transitionTo } from "@discord/modules"; interface VoiceIconProps { userId: string; context: string; } -const { ChannelStore, GuildStore, UserStore } = Stores; - -export default function VoiceIcon(props: VoiceIconProps) { +export default function VoiceIcon(props: VoiceIconProps): React.ReactNode { const settingsState = Settings.useSettingsState( "showMemberListIcons", "showDMListIcons", @@ -50,12 +51,12 @@ export default function VoiceIcon(props: VoiceIconProps) { if (guild) { text = guild.name; subtext = channel.name; - TooltipIcon = Icons.Speaker; + TooltipIcon = Speaker; channelPath = `/channels/${guild.id}/${channel.id}`; } else { text = channel.name; subtext = Strings.get("VOICE_CALL"); - TooltipIcon = Icons.CallJoin; + TooltipIcon = CallJoin; channelPath = `/channels/@me/${channel.id}`; } @@ -67,16 +68,16 @@ export default function VoiceIcon(props: VoiceIconProps) { case 3: text = channel.name || groupDMName(channel.recipients); subtext = Strings.get("GROUP_CALL"); - TooltipIcon = Icons.People; + TooltipIcon = People; break; case 13: - TooltipIcon = Icons.Stage; + TooltipIcon = Stage; } - let Icon = Icons.Speaker; - if (settingsState.showStatusIcons && (voiceState.selfDeaf || voiceState.deaf)) Icon = Icons.Deafened; - else if (settingsState.showStatusIcons && (voiceState.selfMute || voiceState.mute)) Icon = Icons.Muted; - else if (settingsState.showStatusIcons && voiceState.selfVideo) Icon = Icons.Video; + let Icon = Speaker; + if (settingsState.showStatusIcons && (voiceState.selfDeaf || voiceState.deaf)) Icon = Deafened; + else if (settingsState.showStatusIcons && (voiceState.selfMute || voiceState.mute)) Icon = Muted; + else if (settingsState.showStatusIcons && voiceState.selfVideo) Icon = Video; return (
{ e.stopPropagation(); e.preventDefault(); - if (channelPath) transitionTo(channelPath); + if (channelPath) transitionTo?.(channelPath); }} > -
@@ -106,7 +107,7 @@ export default function VoiceIcon(props: VoiceIconProps) {
} > - {(props: any) => ( + {(props) => (
{!voiceState.selfStream ? ( @@ -115,7 +116,7 @@ export default function VoiceIcon(props: VoiceIconProps) { )}
)} -
+
); } diff --git a/VoiceActivity/src/components/VoiceProfileSection.tsx b/VoiceActivity/src/components/VoiceProfileSection.tsx index 000b8fa..9a710d9 100644 --- a/VoiceActivity/src/components/VoiceProfileSection.tsx +++ b/VoiceActivity/src/components/VoiceProfileSection.tsx @@ -1,8 +1,11 @@ import { Components, ContextMenu } from "betterdiscord"; -import { ChannelActions, Icons, Stores, transitionTo, PartyMembers, MoreIcon } from "../modules/discordmodules"; +import { PartyMembers, MoreIcon } from "../modules/discordmodules"; import { Settings, Strings, groupDMName, canViewChannel, useUserVoiceState } from "../modules/utils"; import styles from "../styles/voiceprofilesection.module.scss"; import GuildImage from "./GuildImage"; +import { ChannelStore, GuildStore, SelectedChannelStore, UserStore, VoiceStateStore } from "@discord/stores"; +import { CallJoin, Speaker, Stage } from "@discord/icons"; +import { ChannelActions, transitionTo } from "@discord/modules"; interface VoiceProfileSectionProps { userId: string; @@ -10,8 +13,6 @@ interface VoiceProfileSectionProps { panel?: boolean; } -const { ChannelStore, GuildStore, UserStore, VoiceStateStore, SelectedChannelStore } = Stores; - export default function VoiceProfileSection(props: VoiceProfileSectionProps) { const settingsState = Settings.useSettingsState( "showProfileSection", @@ -55,14 +56,14 @@ export default function VoiceProfileSection(props: VoiceProfileSectionProps) { text = [

{guild.name}

,
{channel.name}
]; viewButton = Strings.get("VIEW"); joinButton = inCurrentChannel ? Strings.get("JOIN_DISABLED") : Strings.get("JOIN"); - Icon = Icons.Speaker; + Icon = Speaker; channelPath = `/channels/${guild.id}/${channel.id}`; } else { headerText = Strings.get("HEADER_VOICE"); text =

{channel.name}

; viewButton = Strings.get("VIEW_CALL"); joinButton = inCurrentChannel ? Strings.get("JOIN_DISABLED_CALL") : Strings.get("JOIN_CALL"); - Icon = Icons.CallJoin; + Icon = CallJoin; channelPath = `/channels/@me/${channel.id}`; } @@ -83,7 +84,7 @@ export default function VoiceProfileSection(props: VoiceProfileSectionProps) { break; case 13: headerText = Strings.get("HEADER_STAGE"); - Icon = Icons.Stage; + Icon = Stage; } const section = ( @@ -154,7 +155,7 @@ export default function VoiceProfileSection(props: VoiceProfileSectionProps) { className={styles.button} disabled={channelSelected} onClick={() => { - if (channelPath) transitionTo(channelPath); + if (channelPath) transitionTo?.(channelPath); }} > {viewButton} diff --git a/VoiceActivity/src/index.tsx b/VoiceActivity/src/index.tsx index dbc65ee..0300cd4 100644 --- a/VoiceActivity/src/index.tsx +++ b/VoiceActivity/src/index.tsx @@ -1,13 +1,11 @@ -import { ContextMenu, DOM, Patcher, Utils, Meta } from "betterdiscord"; +import { ContextMenu, DOM, Patcher, Utils, Meta, Plugin, Changes } from "betterdiscord"; import styles from "styles"; import { showChangelog } from "@lib"; import { changelog } from "./manifest.json"; import { MemberListItem, - Stores, children, iconWrapperSelector, - useStateFromStores, UserPanelBody, UserPopoutBody, PrivateChannel, @@ -19,10 +17,11 @@ import iconStyles from "./styles/voiceicon.module.scss"; import VoiceIcon from "./components/VoiceIcon"; import VoiceProfileSection from "./components/VoiceProfileSection"; import SettingsPanel from "./components/SettingsPanel"; +import { useStateFromStores, VoiceStateStore } from "@discord/stores"; const guildIconSelector = `div:not([data-dnd-name]) + ${iconWrapperSelector}`; -export default class VoiceActivity { +export default class VoiceActivity implements Plugin { meta: Meta; contextMenuUnpatches = new Set<() => void>(); @@ -31,7 +30,7 @@ export default class VoiceActivity { } start() { - showChangelog(changelog, this.meta); + showChangelog(changelog as Changes[], this.meta); DOM.addStyle(styles() + `${children}:empty { margin-left: 0; } ${children} { display: flex; gap: 8px; }`); Strings.subscribe(); this.patchPeopleListItem(); @@ -45,23 +44,29 @@ export default class VoiceActivity { } patchUserPanel() { - Patcher.after(UserPanelBody, "Z", (_, [props]: [any], ret) => { + if (!UserPanelBody) return; + const [module, key] = UserPanelBody; + Patcher.after(module, key, (_, [props], ret) => { ret.props.children.splice(1, 0, ); }); } patchUserPopout() { - Patcher.after(UserPopoutBody, "Z", (_, [props]: [any], ret) => { + if (!UserPopoutBody) return; + const [module, key] = UserPopoutBody; + Patcher.after(module, key, (_, [props], ret) => { ret.props.children.splice(5, 0, ); }); } patchMemberListItem() { - Patcher.after(MemberListItem, "Z", (_, [props]: [any], ret) => { + if (!MemberListItem) return; + const [module, key] = MemberListItem; + Patcher.after(module, key, (_, [props], ret) => { if (!props.user) return ret; const children = ret.props.children; - ret.props.children = (childrenProps) => { + ret.props.children = (childrenProps: any) => { const childrenRet = children(childrenProps); const icon = ; @@ -76,7 +81,8 @@ export default class VoiceActivity { } patchPrivateChannel() { - const patchType = (props, ret) => { + if (!PrivateChannel) return; + const patchType = (props: any, ret: any) => { if (props.channel.type !== 1) return; // Plugin compatibility fix (PlatformIndicators) @@ -84,7 +90,7 @@ export default class VoiceActivity { if (typeof target.props.children != "function") target = ret.props.children; const children = target.props.children; - target.props.children = (childrenProps) => { + target.props.children = (childrenProps: any) => { const childrenRet = children(childrenProps); const privateChannel = Utils.findInTree(childrenRet, (e) => e?.children?.props?.avatar, { @@ -101,18 +107,19 @@ export default class VoiceActivity { }; }; - let patchedType; + let patchedType: ((props: any) => React.ReactNode) | undefined; - Patcher.after(PrivateChannel, "ZP", (_, __, containerRet) => { + const [module, key] = PrivateChannel; + Patcher.after(module, key, (_, __, containerRet) => { // Compatibility fix (ChannelsPreview) - let target = containerRet.children || containerRet; + let target: React.ReactElement = (containerRet as any).children || containerRet; if (patchedType) { target.type = patchedType; return containerRet; } - const original = target.type; + const original = target.type as React.FunctionComponent; patchedType = (props) => { const ret = original(props); @@ -125,6 +132,7 @@ export default class VoiceActivity { } patchPeopleListItem() { + if (!PeopleListItem) return; Patcher.after(PeopleListItem.prototype, "render", (that: any, _, ret) => { if (!that.props.user) return; @@ -147,7 +155,9 @@ export default class VoiceActivity { } patchGuildIcon() { - Patcher.before(GuildIcon, "type", (_, [props]: [any]) => { + if (!GuildIcon) return; + + Patcher.before(GuildIcon, "type", (_, [props]) => { if (!props?.guild) return; const { showGuildIcons, ignoredGuilds, ignoredChannels } = Settings.useSettingsState( @@ -155,7 +165,7 @@ export default class VoiceActivity { "ignoredGuilds", "ignoredChannels" ); - const mediaState = useStateFromStores([Stores.VoiceStateStore], () => + const mediaState = useStateFromStores([VoiceStateStore], () => getGuildMediaState(props.guild.id, ignoredChannels) ); diff --git a/VoiceActivity/src/modules/discordmodules.tsx b/VoiceActivity/src/modules/discordmodules.tsx index 9f9e2b5..2ebb0e0 100644 --- a/VoiceActivity/src/modules/discordmodules.tsx +++ b/VoiceActivity/src/modules/discordmodules.tsx @@ -1,76 +1,52 @@ import { Webpack } from "betterdiscord"; -import { expectModule, expectSelectors, expectIcon } from "@lib/utils/webpack"; +import { expectModule, expectSelectors, expectWithKey } from "@lib/utils/webpack"; +import { AnyComponent, AnyMemo, EmptyComponent } from "@lib/utils/react"; -const { - Filters: { byKeys, byStrings }, - getStore, -} = Webpack; - -const Error = (_props) => ( -
-

Error: Component not found

-
-); - -export const MemberListItem: any = expectModule({ - filter: byStrings("memberInner"), +export const MemberListItem = expectWithKey({ + filter: Webpack.Filters.byStrings("memberInner", "renderPopout"), name: "MemberListItem", - defaultExport: false, }); -export const UserPanelBody: any = expectModule({ - filter: byStrings("PANEL", "getUserProfile"), +export const UserPanelBody = expectWithKey({ + filter: Webpack.Filters.byStrings("PANEL", "getUserProfile"), name: "UserPanelBody", - defaultExport: false, }); -export const UserPopoutBody: any = expectModule({ - filter: byStrings("BITE_SIZE", '"profile"'), +export const UserPopoutBody = expectWithKey({ + filter: Webpack.Filters.byStrings("BITE_SIZE", '"profile"'), name: "UserPopoutBody", - defaultExport: false, }); -export const PrivateChannel: any = expectModule({ - filter: byStrings("PrivateChannel", "getTypingUsers"), +export const PrivateChannel = expectWithKey({ + filter: Webpack.Filters.byStrings("PrivateChannel", "getTypingUsers"), name: "PrivateChannel", defaultExport: false, }); -export const GuildIcon: any = expectModule({ - filter: (m) => m?.type && byStrings("guild", "mediaState")(m.type), +export const GuildIcon = expectModule({ + filter: (m) => m?.type && Webpack.Filters.byStrings("guild", "mediaState")(m.type), name: "GuildIcon", }); -export const PeopleListItem: any = expectModule({ - filter: (m) => m?.prototype?.render && byStrings("this.peopleListItemRef")(m), +export const PeopleListItem = expectModule>({ + filter: (m) => m?.prototype?.render && Webpack.Filters.byStrings("this.peopleListItemRef")(m), name: "PeopleListItem", }); -export const PartyMembers: any = expectModule({ - filter: byStrings("overflowCountClassName"), +export const PartyMembers = expectModule({ + filter: Webpack.Filters.byStrings("overflowCountClassName"), name: "PartyMembers", - fallback: (_props) => null, + fallback: EmptyComponent, }); -export const MoreIcon: any = expectModule({ - filter: byStrings("MoreHorizontalIcon", "contextMenu"), +export const MoreIcon = expectModule({ + filter: Webpack.Filters.byStrings("MoreHorizontalIcon", "contextMenu"), name: "MoreIcon", - fallback: (_props) => null, -}); - -export const GuildActions: any = expectModule({ filter: byKeys("requestMembers"), name: "GuildActions" }); - -export const ChannelActions: any = expectModule({ filter: byKeys("selectChannel"), name: "ChannelActions" }); - -export const useStateFromStores: any = expectModule({ - filter: byStrings("useStateFromStores"), - name: "Flux", - fatal: true, - searchExports: true, + fallback: EmptyComponent, }); export const Permissions = expectModule({ - filter: byKeys("VIEW_CREATOR_MONETIZATION_ANALYTICS"), + filter: Webpack.Filters.byKeys("VIEW_CREATOR_MONETIZATION_ANALYTICS"), searchExports: true, name: "Permissions", fallback: { @@ -78,51 +54,13 @@ export const Permissions = expectModule({ }, }); -export const transitionTo: (path: string) => null = expectModule({ - filter: byStrings("transitionTo -"), - searchExports: true, - name: "transitionTo", -}); - export const getAcronym = expectModule({ - filter: byStrings('.replace(/\'s /g," ").replace(/\\w+/g,'), + filter: Webpack.Filters.byStrings('.replace(/\'s /g," ").replace(/\\w+/g,'), searchExports: true, name: "getAcronym", - fallback: (i: string) => i, + fallback: (name: string) => name, }); -export const Common = expectModule({ - filter: byKeys("Popout", "Avatar", "FormSwitch", "Tooltip"), - name: "Common", - fallback: { - Popout: (props) =>
, - Avatar: (_props) => null, - FormSwitch: Error, - Tooltip: (props) =>
, - }, -}); - -export const Icons = { - CallJoin: expectIcon("CallJoin", "M2 7.4A5.4 5.4 0 0 1 7.4 2c.36 0 .7.22.83.55l1.93 4.64a1 1"), - People: expectIcon("People", "M14.5 8a3 3 0 1 0-2.7-4.3c-.2.4.06.86.44 1.12a5 5 0 0 1 2.14 "), - Speaker: expectIcon("Speaker", "M12 3a1 1 0 0 0-1-1h-.06a1 1 0 0 0-.74.32L5.92 7H3a1 1"), - Muted: expectIcon("Muted", "m2.7 22.7 20-20a1 1 0 0 0-1.4-1.4l-20 20a1 1 0 1 0 1.4"), - Deafened: expectIcon("Deafened", "M22.7 2.7a1 1 0 0 0-1.4-1.4l-20 20a1 1 0 1 0 1.4 1.4l20-20ZM17.06"), - Video: expectIcon("Video", "M4 4a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h11a3 3"), - Stage: expectIcon("Stage", "M19.61 18.25a1.08 1.08 0 0 1-.07-1.33 9 9 0 1 0-15.07"), -}; - -export const iconWrapperSelector = expectSelectors("Icon Wrapper Class", ["wrapper", "folderEndWrapper"]).wrapper; - -export const children = expectSelectors("Children Class", ["avatar", "children"]).children; +export const iconWrapperSelector = expectSelectors("Icon Wrapper Class", ["wrapper", "folderEndWrapper"])?.wrapper; -export const Stores = { - UserStore: getStore("UserStore"), - GuildChannelStore: getStore("GuildChannelStore"), - VoiceStateStore: getStore("VoiceStateStore"), - GuildStore: getStore("GuildStore"), - ChannelStore: getStore("ChannelStore"), - SelectedChannelStore: getStore("SelectedChannelStore"), - GuildMemberStore: getStore("GuildMemberStore"), - PermissionStore: getStore("PermissionStore"), -}; +export const children = expectSelectors("Children Class", ["avatar", "children"])?.children; diff --git a/VoiceActivity/src/modules/utils.ts b/VoiceActivity/src/modules/utils.ts index 1de03c6..e9de5b7 100644 --- a/VoiceActivity/src/modules/utils.ts +++ b/VoiceActivity/src/modules/utils.ts @@ -1,9 +1,15 @@ -import { Patcher, ReactUtils } from "betterdiscord"; +import { Logger, Patcher, ReactUtils } from "betterdiscord"; import { SettingsManager, StringsManager } from "@lib"; -import { Stores, Permissions, useStateFromStores } from "./discordmodules"; +import { Permissions } from "./discordmodules"; import locales from "../locales.json"; - -const { UserStore, GuildChannelStore, VoiceStateStore, ChannelStore, PermissionStore } = Stores; +import { + ChannelStore, + GuildChannelStore, + PermissionStore, + UserStore, + useStateFromStores, + VoiceStateStore, +} from "@discord/stores"; export const Settings = new SettingsManager({ showProfileSection: true, @@ -14,8 +20,8 @@ export const Settings = new SettingsManager({ currentChannelColor: true, showStatusIcons: true, ignoreEnabled: false, - ignoredChannels: [], - ignoredGuilds: [], + ignoredChannels: [] as string[], + ignoredGuilds: [] as string[], }); export const Strings = new StringsManager(locales, "en-US"); @@ -58,8 +64,10 @@ export function groupDMName(members: any[]): string { return "Unnamed"; } -export function forceRerender(element: HTMLElement) { +export function forceRerender(element: HTMLElement | null) { + if (!element) return Logger.error("Force rerender failed: target element not found"); const ownerInstance = ReactUtils.getOwnerInstance(element); + if (!ownerInstance) return Logger.error("Force rerender failed: ownerInstance component not found"); const cancel = Patcher.instead(ownerInstance, "render", () => { cancel(); return null; diff --git a/bundlebd.config.js b/bundlebd.config.js index 9764663..358112d 100644 --- a/bundlebd.config.js +++ b/bundlebd.config.js @@ -4,6 +4,7 @@ module.exports = defineConfig((plugin, dev) => ({ input: `${plugin}/src`, output: dev ? `${plugin}/dist` : `${plugin}`, importAliases: { - "@lib/*": `lib/*`, + "@lib/*": `common/lib/*`, + "@discord/*": `common/discord/*`, }, })); diff --git a/common/discord/components.tsx b/common/discord/components.tsx new file mode 100644 index 0000000..90a17c3 --- /dev/null +++ b/common/discord/components.tsx @@ -0,0 +1,24 @@ +import { EmptyComponent, EmptyWrapperComponent, ErrorPopout } from "@lib/utils/react"; +import { expectModule } from "@lib/utils/webpack"; +import { Webpack } from "betterdiscord"; + +export const Common = /* @__PURE__ */ expectModule({ + filter: /* @__PURE__ */ Webpack.Filters.byKeys("Popout", "Avatar", "FormSwitch", "Tooltip"), + name: "Common", + fallback: { + Popout: EmptyWrapperComponent, + Avatar: EmptyComponent, + FormSwitch: EmptyComponent, + Tooltip: EmptyWrapperComponent, + RadioGroup: EmptyComponent, + FormItem: EmptyComponent, + FormText: EmptyComponent, + FormDivider: EmptyComponent, + }, +}); + +export const UserPopout = /* @__PURE__ */ expectModule({ + filter: (m) => m.toString?.().includes("UserProfilePopoutWrapper"), + name: "UserPopout", + fallback: ErrorPopout, +}); diff --git a/common/discord/icons.tsx b/common/discord/icons.tsx new file mode 100644 index 0000000..64daad7 --- /dev/null +++ b/common/discord/icons.tsx @@ -0,0 +1,42 @@ +import { expectIcon } from "@lib/utils/webpack"; + +export const CallJoin = /* @__PURE__ */ expectIcon( + "CallJoin", + "M2 7.4A5.4 5.4 0 0 1 7.4 2c.36 0 .7.22.83.55l1.93 4.64a1 1" +); +export const People = /* @__PURE__ */ expectIcon( + "People", + "M14.5 8a3 3 0 1 0-2.7-4.3c-.2.4.06.86.44 1.12a5 5 0 0 1 2.14 " +); +export const Speaker = /* @__PURE__ */ expectIcon("Speaker", "M12 3a1 1 0 0 0-1-1h-.06a1 1 0 0 0-.74.32L5.92 7H3a1 1"); +export const Muted = /* @__PURE__ */ expectIcon("Muted", "m2.7 22.7 20-20a1 1 0 0 0-1.4-1.4l-20 20a1 1 0 1 0 1.4"); +export const Deafened = /* @__PURE__ */ expectIcon( + "Deafened", + "M22.7 2.7a1 1 0 0 0-1.4-1.4l-20 20a1 1 0 1 0 1.4 1.4l20-20ZM17.06" +); +export const Video = /* @__PURE__ */ expectIcon("Video", "M4 4a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h11a3 3"); +export const Stage = /* @__PURE__ */ expectIcon("Stage", "M19.61 18.25a1.08 1.08 0 0 1-.07-1.33 9 9 0 1 0-15.07"); +export const Activity = /* @__PURE__ */ expectIcon( + "Activity", + "M20.97 4.06c0 .18.08.35.24.43.55.28.9.82 1.04 1.42.3 1.24.75 3.7.75 7.09v4.91a3.09" +); +export const Settings = /* @__PURE__ */ expectIcon( + "Settings", + "M10.56 1.1c-.46.05-.7.53-.64.98.18 1.16-.19 2.2-.98 2.53" +); +export const RichActivity = /* @__PURE__ */ expectIcon( + "RichActivity", + "M6,7 L2,7 L2,6 L6,6 L6,7 Z M8,5 L2,5 L2,4 L8,4" +); +export const Headset = /* @__PURE__ */ expectIcon( + "Headset", + "M12 3a9 9 0 0 0-8.95 10h1.87a5 5 0 0 1 4.1 2.13l1.37 1.97a3.1 3.1 0 0" +); +export const Screen = /* @__PURE__ */ expectIcon( + "Screen", + "M5 2a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5ZM13.5 20a.5.5" +); +export const Pin = /* @__PURE__ */ expectIcon( + "Pin", + "M19.38 11.38a3 3 0 0 0 4.24 0l.03-.03a.5.5 0 0 0 0-.7L13.35.35a.5.5" +); diff --git a/common/discord/modules.ts b/common/discord/modules.ts new file mode 100644 index 0000000..4fe08e1 --- /dev/null +++ b/common/discord/modules.ts @@ -0,0 +1,48 @@ +import { byType, expectModule } from "@lib/utils/webpack"; +import { Webpack } from "betterdiscord"; + +export const transitionTo = /* @__PURE__ */ expectModule<(path: string) => void>({ + filter: /* @__PURE__ */ Webpack.Filters.combine( + /* @__PURE__ */ Webpack.Filters.byStrings("transitionTo -"), + /* @__PURE__ */ byType("function") + ), + searchExports: true, + name: "transitionTo", +}); + +export const loadProfile = /* @__PURE__ */ expectModule< + (userId: string, avatarURL: string, context: { guildId: string; channelId: string }) => void +>({ + filter: /* @__PURE__ */ Webpack.Filters.combine( + /* @__PURE__ */ Webpack.Filters.byStrings("preloadUserBanner"), + /* @__PURE__ */ byType("function") + ), + name: "loadProfile", +}); + +export const GuildActions = /* @__PURE__ */ expectModule<{ transitionToGuildSync(guildId: string): void }>({ + filter: /* @__PURE__ */ Webpack.Filters.byKeys("requestMembers", "transitionToGuildSync"), + name: "GuildActions", +}); + +export const ChannelActions = /* @__PURE__ */ expectModule<{ + selectVoiceChannel(channelId: string, x?: boolean): void; +}>({ + filter: /* @__PURE__ */ Webpack.Filters.byKeys("selectChannel", "selectVoiceChannel"), + name: "ChannelActions", +}); + +export const SettingsSections = /* @__PURE__ */ expectModule({ + filter: /* @__PURE__ */ Webpack.Filters.byKeys("ACCOUNT", "CHANGE_LOG"), + searchExports: true, + name: "SettingsSections", + fallback: { ACCOUNT: "My Account", ACTIVITY_PRIVACY: "Activity Privacy" }, +}); + +export const UserSettingsWindow = /* @__PURE__ */ expectModule<{ + setSection: (section: string) => void; + open: () => void; +}>({ + filter: /* @__PURE__ */ Webpack.Filters.byKeys("saveAccountChanges", "setSection", "open"), + name: "UserSettingsWindow", +}); diff --git a/common/discord/stores.ts b/common/discord/stores.ts new file mode 100644 index 0000000..6747382 --- /dev/null +++ b/common/discord/stores.ts @@ -0,0 +1,22 @@ +import { expectModule } from "@lib/utils/webpack"; +import { Webpack } from "betterdiscord"; + +export const UserStore = /* @__PURE__ */ Webpack.getStore("UserStore"); +export const GuildChannelStore = /* @__PURE__ */ Webpack.getStore("GuildChannelStore"); +export const VoiceStateStore = /* @__PURE__ */ Webpack.getStore("VoiceStateStore"); +export const GuildStore = /* @__PURE__ */ Webpack.getStore("GuildStore"); +export const ChannelStore = /* @__PURE__ */ Webpack.getStore("ChannelStore"); +export const SelectedChannelStore = /* @__PURE__ */ Webpack.getStore("SelectedChannelStore"); +export const GuildMemberStore = /* @__PURE__ */ Webpack.getStore("GuildMemberStore"); +export const PermissionStore = /* @__PURE__ */ Webpack.getStore("PermissionStore"); +export const RelationshipStore = /* @__PURE__ */ Webpack.getStore("RelationshipStore"); +export const TypingStore = /* @__PURE__ */ Webpack.getStore("TypingStore"); + +export const useStateFromStores = /* @__PURE__ */ expectModule({ + filter: /* @__PURE__ */ Webpack.Filters.byStrings("useStateFromStores"), + name: "Flux", + fallback(stores: any[], callback: () => T): T { + return callback(); + }, + searchExports: true, +}); diff --git a/common/discord/types.ts b/common/discord/types.ts new file mode 100644 index 0000000..e69de29 diff --git a/lib/changelog.ts b/common/lib/changelog.ts similarity index 60% rename from lib/changelog.ts rename to common/lib/changelog.ts index fc5bbf8..942e32d 100644 --- a/lib/changelog.ts +++ b/common/lib/changelog.ts @@ -1,12 +1,6 @@ -import { Data, UI, Meta } from "betterdiscord"; +import { Data, UI, Meta, Changes } from "betterdiscord"; -type Changelog = { - title: string; - type?: string; - items: string[]; -}[]; - -export function showChangelog(changes: Changelog, meta: Meta) { +export function showChangelog(changes: Changes[], meta: Meta) { if (!changes || changes.length == 0) return; const changelogVersion = Data.load("changelogVersion"); diff --git a/lib/index.ts b/common/lib/index.ts similarity index 100% rename from lib/index.ts rename to common/lib/index.ts diff --git a/lib/settings.ts b/common/lib/settings.ts similarity index 100% rename from lib/settings.ts rename to common/lib/settings.ts diff --git a/lib/strings.ts b/common/lib/strings.ts similarity index 100% rename from lib/strings.ts rename to common/lib/strings.ts diff --git a/lib/updater.ts b/common/lib/updater.ts similarity index 100% rename from lib/updater.ts rename to common/lib/updater.ts diff --git a/common/lib/utils/react.tsx b/common/lib/utils/react.tsx new file mode 100644 index 0000000..5245d94 --- /dev/null +++ b/common/lib/utils/react.tsx @@ -0,0 +1,12 @@ +export type AnyComponent = (props: any) => React.ReactElement; +export type AnyMemo = React.MemoExoticComponent; + +export const EmptyComponent: React.FunctionComponent = (props) => null; + +export const EmptyWrapperComponent: React.FunctionComponent = (props) =>
; + +export const ErrorPopout: React.FunctionComponent = (props) => ( +
+ Error: Popout component not found +
+); diff --git a/lib/utils/string.ts b/common/lib/utils/string.ts similarity index 100% rename from lib/utils/string.ts rename to common/lib/utils/string.ts diff --git a/lib/utils/webpack.ts b/common/lib/utils/webpack.ts similarity index 78% rename from lib/utils/webpack.ts rename to common/lib/utils/webpack.ts index 37733fb..ac57146 100644 --- a/lib/utils/webpack.ts +++ b/common/lib/utils/webpack.ts @@ -1,4 +1,4 @@ -import { Webpack, Logger, SearchOptions, ModuleFilter } from "betterdiscord"; +import { Webpack, Logger, ModuleFilter, ModuleQuery, ModuleKey, WithKeyResult } from "betterdiscord"; import React from "react"; interface IconProps { @@ -9,42 +9,7 @@ interface IconProps { color?: string; } -type Icon = (props: IconProps) => React.ReactNode; - -/** - * Options for the `expect` function. - * - `name`: The name of the module (for error logging) - * - `fatal`: Whether or not to stop plugin execution when the module is not found - * - `fallback`: A fallback value to use when the module is not found - * - `onError`: A callback function that is run when the module is not found - */ -type ExpectOptions = { - name: string; - fatal?: boolean; - fallback?: T; - onError?: () => void; -}; - -type ExpectOptionsFallback = ExpectOptions & { - fallback: T; -}; - -type ExpectOptionsFatal = ExpectOptions & { - fatal: true; -}; - -/** - * Options for the `expectModule` function. Takes all options for a normal `getModule` query as well as: - * - `filter`: A function to use to filter modules. - * - `name`: The name of the module (for error logging) - * - `fatal`: Whether or not to stop plugin execution when the module is not found - * - `fallback`: A fallback value to use when the module is not found - * - `onError`: A callback function that is run when the module is not found - */ -type ExpectOptionsWithFilter = ExpectOptions & - SearchOptions & { - filter: ModuleFilter; - }; +type Icon = React.FunctionComponent; /** * Gets a module of classes using desired classes. @@ -84,8 +49,47 @@ export function getIcon(searchString: string): Icon | undefined { }); } -export function expect(object: T, options: ExpectOptionsFallback): NonNullable; -export function expect(object: T, options: ExpectOptionsFatal): NonNullable; +/** + * Options for the `expect` function. + * - `name`: The name of the module (for error logging) + * - `fatal`: Whether or not to stop plugin execution when the module is not found + * - `fallback`: A fallback value to use when the module is not found + * - `onError`: A callback function that is run when the module is not found + */ +type ExpectOptions = { + name: string; + fatal?: boolean; + fallback?: NonNullable; + onError?: () => void; +}; + +type ExpectOptionsFallback = ExpectOptions & { + fallback: NonNullable; +}; +type ExpectOptionsFatal = ExpectOptions & { + fatal: true; +}; +type ExpectOptionsNotNull = ExpectOptionsFallback | ExpectOptionsFatal; + +/** + * Options for the `expectModule` function. Takes all options for a normal `getModule` query as well as: + * - `filter`: A function to use to filter modules. + * - `name`: The name of the module (for error logging) + * - `fatal`: Whether or not to stop plugin execution when the module is not found + * - `fallback`: A fallback value to use when the module is not found + * - `onError`: A callback function that is run when the module is not found + */ +type ExpectModuleOptions = ExpectOptions & ModuleQuery; + +type ExpectModuleOptionsFallback = ExpectModuleOptions & { + fallback: NonNullable; +}; +type ExpectModuleOptionsFatal = ExpectModuleOptions & { + fatal: true; +}; +type ExpectModuleOptionsNotNull = ExpectModuleOptionsFallback | ExpectModuleOptionsFatal; + +export function expect(object: T, options: ExpectOptionsNotNull): NonNullable; export function expect(object: T, options: ExpectOptions): T | undefined; export function expect(object: T, options: ExpectOptions): T | undefined { if (object) return object; @@ -97,7 +101,7 @@ export function expect(object: T, options: ExpectOptions): T | undefined { options.onError?.(); if (options.fatal) throw new Error(errorMessage); - return options.fallback!; + return options.fallback; } /** @@ -105,8 +109,25 @@ export function expect(object: T, options: ExpectOptions): T | undefined { * @param options Options for the module search and error handling, including a filter. * @returns The found module or the fallback if the module cannot be found. */ -export function expectModule(options: ExpectOptionsWithFilter) { - return expect(Webpack.getModule(options.filter, options), options); +export function expectModule(options: ExpectModuleOptionsNotNull): T; +export function expectModule(options: ExpectModuleOptions): T | undefined; +export function expectModule(options: ExpectModuleOptions): T | undefined { + return expect(Webpack.getModule(options.filter, options), options); +} + +export function expectWithKey(options: ExpectModuleOptionsNotNull): WithKeyResult; +export function expectWithKey(options: ExpectModuleOptions): WithKeyResult | undefined; +export function expectWithKey(options: ExpectModuleOptions): WithKeyResult | undefined { + const [module, key] = Webpack.getWithKey(options.filter, options); + if (module) return [module, key]; + + const fallback = expect(module, options); + if (fallback) { + const key = "__key" as ModuleKey; + return [{ [key]: fallback }, key]; + } + + return undefined; } /** @@ -159,6 +180,10 @@ export function byId(id: string): ModuleFilter { return (_e, _m, i) => i === id; } +export function byType(type: string): ModuleFilter { + return (e) => typeof e === type; +} + /** * Generates a Webpack filter to get a module with property values that satisfy a set of filters. * @param filters Filters that property values on the module must satisfy. diff --git a/tsconfig.json b/tsconfig.json index c614f2b..172950e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,8 +8,10 @@ "esModuleInterop": true, "jsx": "react-jsx", "paths": { - "@lib": ["./lib"], - "@lib/*": ["./lib/*"] + "@lib": ["./common/lib"], + "@lib/*": ["./common/lib/*"], + "@discord": ["./common/discord"], + "@discord/*": ["./common/discord/*"] } } } From 5e5397922057e6f7f672c444ce3574a1452ed83e Mon Sep 17 00:00:00 2001 From: Neodymium <68879269+Neodymium7@users.noreply.github.com> Date: Sun, 22 Dec 2024 16:18:05 -0800 Subject: [PATCH 3/4] Update typings --- ActivityIcons/src/components/ListeningIcon.tsx | 2 +- ActivityIcons/src/components/WatchingIcon.tsx | 2 +- ChannelTypingIndicator/src/TypingIndicator.tsx | 2 +- VoiceActivity/src/components/VoiceIcon.tsx | 2 +- package-lock.json | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ActivityIcons/src/components/ListeningIcon.tsx b/ActivityIcons/src/components/ListeningIcon.tsx index 3f81d90..58fd5e1 100644 --- a/ActivityIcons/src/components/ListeningIcon.tsx +++ b/ActivityIcons/src/components/ListeningIcon.tsx @@ -32,7 +32,7 @@ export default function ListeningIcon(props: ListeningIconProps) { } position="top" > - {(props) => ( + {(props: any) => (
diff --git a/ActivityIcons/src/components/WatchingIcon.tsx b/ActivityIcons/src/components/WatchingIcon.tsx index fa2e5cd..3044bfe 100644 --- a/ActivityIcons/src/components/WatchingIcon.tsx +++ b/ActivityIcons/src/components/WatchingIcon.tsx @@ -17,7 +17,7 @@ export default function WatchingIcon(props: WatchingIconProps) { return ( {activity.name}
}> - {(props) => ( + {(props: any) => (
diff --git a/ChannelTypingIndicator/src/TypingIndicator.tsx b/ChannelTypingIndicator/src/TypingIndicator.tsx index 8845628..d5d0d82 100644 --- a/ChannelTypingIndicator/src/TypingIndicator.tsx +++ b/ChannelTypingIndicator/src/TypingIndicator.tsx @@ -45,7 +45,7 @@ export function TypingIndicator({ channelId, guildId }: TypingIndicatorProps) { return ( - {(props) => ( + {(props: any) => (
diff --git a/VoiceActivity/src/components/VoiceIcon.tsx b/VoiceActivity/src/components/VoiceIcon.tsx index 4acd832..c928e56 100644 --- a/VoiceActivity/src/components/VoiceIcon.tsx +++ b/VoiceActivity/src/components/VoiceIcon.tsx @@ -107,7 +107,7 @@ export default function VoiceIcon(props: VoiceIconProps): React.ReactNode {
} > - {(props) => ( + {(props: any) => (
{!voiceState.selfStream ? ( diff --git a/package-lock.json b/package-lock.json index bb54e79..861b850 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2883,8 +2883,8 @@ }, "node_modules/@types/bdapi": { "name": "betterdiscord-types", - "version": "1.9.7", - "resolved": "git+ssh://git@github.com/zerthox/betterdiscord-types.git#76e913fbd5d03441e8d8e6341c27af979a95f4e3", + "version": "1.11.0", + "resolved": "git+ssh://git@github.com/zerthox/betterdiscord-types.git#45b2b93bdc836c0f0dd7e0a46b34854749131c9f", "dev": true, "dependencies": { "@types/react": "^18.0.37", @@ -7310,7 +7310,7 @@ "dev": true }, "@types/bdapi": { - "version": "git+ssh://git@github.com/zerthox/betterdiscord-types.git#76e913fbd5d03441e8d8e6341c27af979a95f4e3", + "version": "git+ssh://git@github.com/zerthox/betterdiscord-types.git#45b2b93bdc836c0f0dd7e0a46b34854749131c9f", "dev": true, "from": "@types/bdapi@github:zerthox/betterdiscord-types", "requires": { From 595184b11537d3f2d63720538b1802ead7359c5a Mon Sep 17 00:00:00 2001 From: Neodymium <68879269+Neodymium7@users.noreply.github.com> Date: Sun, 22 Dec 2024 16:18:57 -0800 Subject: [PATCH 4/4] Bump ChannelTypingIndicator version --- ChannelTypingIndicator/src/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChannelTypingIndicator/src/manifest.json b/ChannelTypingIndicator/src/manifest.json index ad6f256..bc23204 100644 --- a/ChannelTypingIndicator/src/manifest.json +++ b/ChannelTypingIndicator/src/manifest.json @@ -1,7 +1,7 @@ { "name": "ChannelTypingIndicator", "author": "Neodymium", - "version": "1.0.2", + "version": "1.0.3", "description": "Adds an indicator to server channels when users are typing.", "source": "https://github.com/Neodymium7/BetterDiscordStuff/blob/main/ChannelTypingIndicator/ChannelTypingIndicator.plugin.js", "invite": "fRbsqH87Av"