diff --git a/src/renderer/coremods/utilityClasses/index.ts b/src/renderer/coremods/utilityClasses/index.ts new file mode 100644 index 000000000..6c64bf3f6 --- /dev/null +++ b/src/renderer/coremods/utilityClasses/index.ts @@ -0,0 +1,122 @@ +import { Injector } from "@replugged"; +import { getByProps, getByStoreName } from "src/renderer/modules/webpack"; +import { users } from "@common"; +import type React from "react"; +import type { Store } from "src/renderer/modules/common/flux"; +import type { Message } from "discord-types/general"; + +const inject = new Injector(); +const html = document.documentElement; + +interface TabBarItemProps { + id: string; + "aria-controls"?: string; +} + +interface TabBarItemType extends React.Component { + render(): React.ReactElement; +} + +// Re-adds the tab bar item's ID as an always-present attribute +function tabBarItemId(): void { + const TabBarModule = getByProps<{ TabBar: { Item: { prototype: TabBarItemType } } }>("TabBar"); + if (!TabBarModule) { + throw new Error("Failed to find TabBar module!"); + } + + inject.after( + TabBarModule.TabBar.Item.prototype, + "render", + function (this: TabBarItemType, _, res) { + if (typeof this.props.id === "string") { + res.props["aria-controls"] = `${this.props.id.replace(/\s+/g, "-").toLowerCase()}-tab`; + } + return res; + }, + ); +} + +interface ClientThemesBackgroundStore extends Store { + gradientPreset: { id: number } | undefined; // undefined when no nitro theme is selected +} +type ThemeIDMap = Record & Record; + +function onNitroThemeChange(store: ClientThemesBackgroundStore, ThemeIDMap: ThemeIDMap): void { + if (!store.gradientPreset) { + html.removeAttribute("data-nitro-theme"); + } else { + const theme = ThemeIDMap[store.gradientPreset.id]; + html.setAttribute("data-nitro-theme", theme); + } +} + +// Adds the currently active nitro theme as a class on the html element +function nitroThemeClass(): void { + const ClientThemesBackgroundStore = getByStoreName( + "ClientThemesBackgroundStore", + ); + if (!ClientThemesBackgroundStore) { + throw new Error("Failed to find ClientThemesBackgroundStore!"); + } + const ThemeIDMap = getByProps("MINT_APPLE"); + if (!ThemeIDMap) { + throw new Error("Failed to find ThemeIDs module!"); + } + + // update theme attribute when theme changes + ClientThemesBackgroundStore.addChangeListener(() => { + onNitroThemeChange(ClientThemesBackgroundStore, ThemeIDMap); + }); + onNitroThemeChange(ClientThemesBackgroundStore, ThemeIDMap); +} + +function messageDataAttributes(): void { + const Message = getByProps<{ + default: { type: (msg: { message: Message }) => React.ReactElement }; + getElementFromMessage: unknown; + }>("getElementFromMessage"); + + if (!Message) { + throw new Error("Failed to find Message module!"); + } + + inject.after(Message.default, "type", ([{ message }], res) => { + const props = res.props?.children?.props?.children?.props; + if (!props) return; + props["data-is-author-self"] = message.author.id === users.getCurrentUser().id; + props["data-is-author-bot"] = message.author.bot; + // webhooks are also considered bots + if (message.author.bot) { + props["data-is-author-webhook"] = Boolean(message.webhookId); + } + props["data-author-id"] = message.author.id; + props["data-message-type"] = message.type; // raw enum value, seems consistent enough to be useful + if (message.blocked) props["data-is-blocked"] = "true"; + return res; + }); +} + +function addHtmlClasses(): void { + if (!html.classList.contains("replugged")) { + html.classList.add("replugged"); + } +} + +let observer: MutationObserver; + +export function start(): void { + tabBarItemId(); + nitroThemeClass(); + messageDataAttributes(); + + // generic stuff + observer = new MutationObserver(addHtmlClasses); + observer.observe(html, { attributeFilter: ["class"] }); +} + +export function stop(): void { + inject.uninjectAll(); + observer.disconnect(); + html.classList.remove("replugged"); + html.removeAttribute("data-nitro-theme"); +} diff --git a/src/renderer/managers/coremods.ts b/src/renderer/managers/coremods.ts index cafd75fb7..d422eaaf6 100644 --- a/src/renderer/managers/coremods.ts +++ b/src/renderer/managers/coremods.ts @@ -34,6 +34,7 @@ export namespace coremods { export let watcher: Coremod; export let commands: Coremod; export let welcome: Coremod; + export let utilityClasses: Coremod; } export async function start(name: keyof typeof coremods): Promise { @@ -59,6 +60,7 @@ export async function startAll(): Promise { coremods.watcher = await import("../coremods/watcher"); coremods.commands = await import("../coremods/commands"); coremods.welcome = await import("../coremods/welcome"); + coremods.utilityClasses = await import("../coremods/utilityClasses"); await Promise.all( Object.entries(coremods).map(async ([name, mod]) => {