diff --git a/renderer/src/builtins/general/publicservers.js b/renderer/src/builtins/general/publicservers.js index cb3b1af6..86fd66e3 100644 --- a/renderer/src/builtins/general/publicservers.js +++ b/renderer/src/builtins/general/publicservers.js @@ -1,5 +1,5 @@ import Builtin from "../../structs/builtin"; -import {DiscordModules, WebpackModules, Strings, DOMManager, React} from "modules"; +import {DiscordModules, WebpackModules, Strings, DOMManager, React, ReactDOM} from "modules"; import PublicServersMenu from "../../ui/publicservers/menu"; import Globe from "../../ui/icons/globe"; @@ -48,16 +48,17 @@ export default new class PublicServers extends Builtin { if (!PrivateChannelList || !PrivateChannelList.Z) return this.warn("Could not find PrivateChannelList", PrivateChannelList); const PrivateChannelButton = WebpackModules.getModule(m => m?.prototype?.render?.toString().includes("linkButton"), {searchExports: true}); if (!PrivateChannelButton) return this.warn("Could not find PrivateChannelButton", PrivateChannelButton); + this.after(PrivateChannelList, "Z", (_, __, returnValue) => { const destination = returnValue?.props?.children?.props?.children; if (!destination || !Array.isArray(destination)) return; - if (destination.find(b => b?.props?.children?.props?.id === "public-server-button")) return; + if (destination.find(b => b?.props?.children?.props?.id === "public-servers-button")) return; // If it exists, don't try to add again destination.push( React.createElement(ErrorBoundary, null, React.createElement(PrivateChannelButton, { - id: "public-server-button", + id: "public-servers-button", onClick: () => this.openPublicServers(), text: "Public Servers", icon: () => React.createElement(Globe, {color: "currentColor"}) @@ -66,6 +67,42 @@ export default new class PublicServers extends Builtin { ) ); }); + + /** + * On being first enabled, we have no way of forceUpdating the list, + * so clone and modify an existing button and add it to the end + * of the button list. + */ + const header = document.querySelector(`[class*="privateChannelsHeaderContainer-"]`); + if (!header) return; // No known element + const oldButton = header.previousElementSibling; + if (!oldButton.className.includes("channel-")) return; // Not what we expected to be there + + // Clone existing button and set click handler + const newButton = oldButton.cloneNode(true); + newButton.addEventListener("click", (event) => { + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + this.openPublicServers(); + }); + + // Remove existing route and id + const aSlot = newButton.querySelector("a"); + aSlot.href = ""; + aSlot.dataset.listItemId = "public-servers"; + + // Render our icon in the avatar slot + const avatarSlot = newButton.querySelector(`[class*="avatar-"]`); + avatarSlot.replaceChildren(); + ReactDOM.render(React.createElement(Globe, {color: "currentColor"}), avatarSlot); + + // Replace the existing name + const nameSlot = newButton.querySelector(`[class*="name-"]`); + nameSlot.textContent = "Public Servers"; + + // Insert before the header, end of the list + header.parentNode.insertBefore(newButton, header); } disabled() { diff --git a/renderer/src/modules/addonmanager.js b/renderer/src/modules/addonmanager.js index a58d5e19..ae3c514c 100644 --- a/renderer/src/modules/addonmanager.js +++ b/renderer/src/modules/addonmanager.js @@ -206,6 +206,7 @@ export default class AddonManager { if (partialAddon) { partialAddon.partial = true; this.state[partialAddon.id] = false; + this.emit("loaded", partialAddon.id); } return e; } @@ -215,6 +216,7 @@ export default class AddonManager { if (error) { this.state[addon.id] = false; addon.partial = true; + this.emit("loaded", addon.id); return error; } diff --git a/renderer/src/modules/api/contextmenu.js b/renderer/src/modules/api/contextmenu.js index f3f392c0..2ae64f7a 100644 --- a/renderer/src/modules/api/contextmenu.js +++ b/renderer/src/modules/api/contextmenu.js @@ -15,7 +15,7 @@ const MenuComponents = (() => { }; let ContextMenuIndex = null; - const ContextMenuModule = WebpackModules.getModule((m, _, id) => Object.values(m).some(v => v?.FLEXIBLE) && (ContextMenuIndex = id), {searchGetters: false}); + const ContextMenuModule = WebpackModules.getModule((m, _, id) => Object.values(m).some(v => v?.FLEXIBLE) && (ContextMenuIndex = id), {searchExports: false}); const rawMatches = WebpackModules.require.m[ContextMenuIndex].toString().matchAll(/if\(\w+\.type===\w+\.(\w+)\).+?type:"(.+?)"/g); out.Menu = Object.values(ContextMenuModule).find(v => v.toString().includes(".isUsingKeyboardNavigation")); @@ -30,7 +30,7 @@ const MenuComponents = (() => { const ContextMenuActions = (() => { const out = {}; - const ActionsModule = WebpackModules.getModule(m => Object.values(m).some(m => typeof m === "function" && m.toString().includes("CONTEXT_MENU_CLOSE")), {searchGetters: false}); + const ActionsModule = WebpackModules.getModule(m => Object.values(m).some(m => typeof m === "function" && m.toString().includes("CONTEXT_MENU_CLOSE")), {searchExports: false}); for (const key of Object.keys(ActionsModule)) { if (ActionsModule[key].toString().includes("CONTEXT_MENU_CLOSE")) { @@ -50,7 +50,7 @@ class MenuPatcher { static initialize() { const {module, key} = (() => { - const module = WebpackModules.getModule(m => Object.values(m).some(v => typeof v === "function" && v.toString().includes("CONTEXT_MENU_CLOSE")), {searchGetters: false}); + const module = WebpackModules.getModule(m => Object.values(m).some(v => typeof v === "function" && v.toString().includes("CONTEXT_MENU_CLOSE")), {searchExports: false}); const key = Object.keys(module).find(key => module[key].length === 3); return {module, key}; diff --git a/renderer/src/modules/api/webpack.js b/renderer/src/modules/api/webpack.js index 136905ef..98398184 100644 --- a/renderer/src/modules/api/webpack.js +++ b/renderer/src/modules/api/webpack.js @@ -74,7 +74,7 @@ const Webpack = { getModule(filter, options = {}) { if (("first" in options) && typeof(options.first) !== "boolean") return Logger.error("BdApi.Webpack~getModule", "Unsupported type used for options.first", options.first, "boolean expected."); if (("defaultExport" in options) && typeof(options.defaultExport) !== "boolean") return Logger.error("BdApi.Webpack~getModule", "Unsupported type used for options.defaultExport", options.defaultExport, "boolean expected."); - if (("searchGetters" in options) && typeof(options.searchGetters) !== "boolean") return Logger.error("BdApi.Webpack~getModule", "Unsupported type used for options.searchGetters", options.searchGetters, "boolean expected."); + if (("searchExports" in options) && typeof(options.searchExports) !== "boolean") return Logger.error("BdApi.Webpack~getModule", "Unsupported type used for options.searchExports", options.searchExports, "boolean expected."); return WebpackModules.getModule(filter, options); }, @@ -103,7 +103,7 @@ const Webpack = { waitForModule(filter, options = {}) { if (("defaultExport" in options) && typeof(options.defaultExport) !== "boolean") return Logger.error("BdApi.Webpack~waitForModule", "Unsupported type used for options.defaultExport", options.defaultExport, "boolean expected."); if (("signal" in options) && !(options.signal instanceof AbortSignal)) return Logger.error("BdApi.Webpack~waitForModule", "Unsupported type used for options.signal", options.signal, "AbortSignal expected."); - if (("searchGetters" in options) && typeof(options.searchGetters) !== "boolean") return Logger.error("BdApi.Webpack~getModule", "Unsupported type used for options.searchGetters", options.searchGetters, "boolean expected."); + if (("searchExports" in options) && typeof(options.searchExports) !== "boolean") return Logger.error("BdApi.Webpack~getModule", "Unsupported type used for options.searchExports", options.searchExports, "boolean expected."); return WebpackModules.getLazy(filter, options); }, }; diff --git a/renderer/src/ui/modals.js b/renderer/src/ui/modals.js index 7e380efb..819c7eed 100644 --- a/renderer/src/ui/modals.js +++ b/renderer/src/ui/modals.js @@ -1,6 +1,6 @@ import {Config} from "data"; import Logger from "common/logger"; -import {WebpackModules, React, ReactDOM, Settings, Strings, DOMManager, DiscordModules} from "modules"; +import {WebpackModules, React, ReactDOM, Settings, Strings, DOMManager, DiscordModules, DiscordClasses} from "modules"; import FormattableString from "../structs/string"; import AddonErrorModal from "./addonerrormodal"; import ErrorBoundary from "./errorboundary"; @@ -212,26 +212,16 @@ export default class Modals { })))); } - static showChangelogModal(changelog) { - const md = [changelog.description]; - for (const type of changelog.changes) { - md.push(`**${type.title}**`); - for (const entry of type.items) { - md.push(` - ${entry}`); - } - } - Modals.showConfirmationModal(`BetterDiscord v${Config.version}`, md, {cancelText: ""}); - } - - static BROKEN_showChangelogModal(options = {}) { - const ModalStack = WebpackModules.getByProps("push", "update", "pop", "popWithKey"); + static showChangelogModal(options = {}) { + const OriginalModalClasses = WebpackModules.getByProps("hideOnFullscreen", "root"); + const ChangelogModalClasses = WebpackModules.getModule(m => m.modal && m.maxModalWidth); const ChangelogClasses = WebpackModules.getByProps("fixed", "improved"); - const TextElement = WebpackModules.getByDisplayName("LegacyText"); - const FlexChild = WebpackModules.getByProps("Child"); - const Titles = WebpackModules.getByProps("Tags", "default"); - const Changelog = WebpackModules.getModule(m => m.defaultProps && m.defaultProps.selectable == false); + const TextElement = this.TextElement; + const FlexChild = this.FlexElements; + const Titles = this.FormTitle; const MarkdownParser = WebpackModules.getByProps("defaultRules", "parse"); - if (!Changelog || !ModalStack || !ChangelogClasses || !TextElement || !FlexChild || !Titles || !MarkdownParser) return Logger.warn("Modals", "showChangelogModal missing modules"); + + if (!OriginalModalClasses || !ChangelogModalClasses || !ChangelogClasses || !TextElement || !FlexChild || !Titles || !MarkdownParser) return Logger.warn("Modals", "showChangelogModal missing modules"); const {image = "https://i.imgur.com/wuh5yMK.png", description = "", changes = [], title = "BetterDiscord", subtitle = `v${Config.version}`, footer} = options; const ce = React.createElement; @@ -247,47 +237,38 @@ export default class Modals { changelogItems.push(list); } const renderHeader = function() { - return ce(FlexChild.Child, {grow: 1, shrink: 1}, - ce(Titles.default, {tag: Titles.Tags.H4}, title), - ce(TextElement, {size: TextElement.Sizes.SMALL, color: TextElement.Colors.STANDARD, className: ChangelogClasses.date}, subtitle) + return ce(FlexChild, {className: OriginalModalClasses.header, grow: 0, shrink: 0, direction: FlexChild.Direction.VERTICAL}, + ce(Titles, {tag: Titles.Tags.H1, size: TextElement.Sizes.SIZE_20}, title), + ce(TextElement, {size: TextElement.Sizes.SIZE_12, color: TextElement.Colors.STANDARD, className: ChangelogClasses.date}, subtitle) ); }; const renderFooter = () => { - const Anchor = WebpackModules.getModule(m => m.displayName == "Anchor"); const AnchorClasses = WebpackModules.getByProps("anchorUnderlineOnHover") || {anchor: "anchor-3Z-8Bb", anchorUnderlineOnHover: "anchorUnderlineOnHover-2ESHQB"}; const joinSupportServer = (click) => { click.preventDefault(); click.stopPropagation(); - ModalStack.pop(); DiscordModules.InviteActions.acceptInviteAndTransitionToInviteChannel("0Tmfo5ZbORCRqbAd"); }; - const supportLink = Anchor ? ce(Anchor, {onClick: joinSupportServer}, "Join our Discord Server.") : ce("a", {className: `${AnchorClasses.anchor} ${AnchorClasses.anchorUnderlineOnHover}`, onClick: joinSupportServer}, "Join our Discord Server."); - const defaultFooter = ce(TextElement, {size: TextElement.Sizes.SMALL, color: TextElement.Colors.STANDARD}, "Need support? ", supportLink); - return ce(FlexChild.Child, {grow: 1, shrink: 1}, footer ? footer : defaultFooter); + const supportLink = ce("a", {className: `${AnchorClasses.anchor} ${AnchorClasses.anchorUnderlineOnHover}`, onClick: joinSupportServer}, "Join our Discord Server."); + const defaultFooter = ce(TextElement, {size: TextElement.Sizes.SIZE_12, color: TextElement.Colors.STANDARD}, "Need support? ", supportLink); + return ce(FlexChild, {className: OriginalModalClasses.footer + " " + OriginalModalClasses.footerSeparator}, + ce(FlexChild.Child, {grow: 1, shrink: 1}, footer ? footer : defaultFooter) + ); }; - const ModalActions = this.ModalActions; - const OriginalModalClasses = WebpackModules.getByProps("hideOnFullscreen", "root"); - const originalRoot = OriginalModalClasses.root; - if (originalRoot) OriginalModalClasses.root = `${originalRoot} bd-changelog-modal`; - const key = ModalActions.openModal(props => { - return React.createElement(ErrorBoundary, null, React.createElement(Changelog, Object.assign({ - className: `bd-changelog ${ChangelogClasses.container}`, + const body = ce("div", { + className: `${OriginalModalClasses.content} ${ChangelogClasses.container} ${ChangelogModalClasses.content} ${DiscordClasses.Scrollers.thin}` + }, changelogItems); + + const key = this.ModalActions.openModal(props => { + return React.createElement(ErrorBoundary, null, React.createElement(this.ModalRoot, Object.assign({ + className: `bd-changelog-modal ${OriginalModalClasses.root} ${OriginalModalClasses.small} ${ChangelogModalClasses.modal}`, selectable: true, onScroll: _ => _, onClose: _ => _, - renderHeader: renderHeader, - renderFooter: renderFooter, - }, props), changelogItems)); + }, props), renderHeader(), body, renderFooter())); }); - - const closeModal = ModalActions.closeModal; - ModalActions.closeModal = function(k) { - Reflect.apply(closeModal, this, arguments); - setTimeout(() => {if (originalRoot && k === key) OriginalModalClasses.root = originalRoot;}, 1000); - ModalActions.closeModal = closeModal; - }; return key; } diff --git a/renderer/src/ui/settings/addoncard.jsx b/renderer/src/ui/settings/addoncard.jsx index 98d14150..157a915c 100644 --- a/renderer/src/ui/settings/addoncard.jsx +++ b/renderer/src/ui/settings/addoncard.jsx @@ -171,7 +171,7 @@ export default class AddonCard extends React.Component { const description = this.getString(addon.description); const version = this.getString(addon.version); - return