From 50a96e804781731060e0303b9564b8aa9da180b5 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 25 Nov 2022 16:16:07 +0100 Subject: [PATCH 01/18] CallTimer: Fix typo --- src/plugins/callTimer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/callTimer.ts b/src/plugins/callTimer.ts index d58b428cb..264e74bd7 100644 --- a/src/plugins/callTimer.ts +++ b/src/plugins/callTimer.ts @@ -109,7 +109,7 @@ export default definePlugin({ }, stop() { - FluxDispatcher.unsubscribe("RCT_CONNECTION_STATE", this.handleRtcConnectionState); + FluxDispatcher.unsubscribe("RTC_CONNECTION_STATE", this.handleRtcConnectionState); this.style?.remove(); clearInterval(this.interval); } From bb398970ef7d60c68191f5e6e153bdba24e07207 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 25 Nov 2022 18:06:31 +0100 Subject: [PATCH 02/18] HideAttachments: Fix embeds Closes #259 --- src/plugins/HideAttachments.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/HideAttachments.tsx b/src/plugins/HideAttachments.tsx index 067b4f010..0789214df 100644 --- a/src/plugins/HideAttachments.tsx +++ b/src/plugins/HideAttachments.tsx @@ -99,7 +99,7 @@ export default definePlugin({ icon: isHidden ? ImageVisible : ImageInvisible, message: msg, channel: ChannelStore.getChannel(msg.channel_id), - onClick: () => this.toggleHide(msg) + onClick: () => this.toggleHide(msg.id) }); } catch (err) { new Logger("HideAttachments").error(err); @@ -107,10 +107,10 @@ export default definePlugin({ } }, - async toggleHide(message: Message) { + async toggleHide(id: string) { const ids = await getHiddenMessages(); - if (!ids.delete(message.id)) - ids.add(message.id); + if (!ids.delete(id)) + ids.add(id); await saveHiddenMessages(ids); await this.buildCss(); @@ -118,7 +118,7 @@ export default definePlugin({ // update is necessary to rerender the PopOver FluxDispatcher.dispatch({ type: "MESSAGE_UPDATE", - message + message: { id } }); } }); From b60f6cb18d1d55d367ae2f3c322d76b709eacdfe Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 25 Nov 2022 18:07:29 +0100 Subject: [PATCH 03/18] WhoReacted: Make more reliable & don't spam api --- src/components/Monaco.ts | 2 +- src/ipcMain/index.ts | 4 +-- src/plugins/whoReacted.tsx | 60 +++++++++++++++++++++++++++++++++----- src/utils/Queue.ts | 45 ++++++++++++++++++++++++++-- src/utils/misc.tsx | 5 ++++ src/webpack/common.tsx | 1 + 6 files changed, 104 insertions(+), 13 deletions(-) diff --git a/src/components/Monaco.ts b/src/components/Monaco.ts index 16eff859d..6d16c76bb 100644 --- a/src/components/Monaco.ts +++ b/src/components/Monaco.ts @@ -25,7 +25,7 @@ import { find } from "../webpack/webpack"; const queue = new Queue(); const setCss = debounce((css: string) => { - queue.add(() => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, css)); + queue.push(() => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, css)); }); export async function launchMonacoEditor() { diff --git a/src/ipcMain/index.ts b/src/ipcMain/index.ts index 8a60bc6ed..535c005d2 100644 --- a/src/ipcMain/index.ts +++ b/src/ipcMain/index.ts @@ -66,14 +66,14 @@ const settingsWriteQueue = new Queue(); ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss()); ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) => - cssWriteQueue.add(() => writeFile(QUICKCSS_PATH, css)) + cssWriteQueue.push(() => writeFile(QUICKCSS_PATH, css)) ); ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR); ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings()); ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => { - settingsWriteQueue.add(() => writeFile(SETTINGS_FILE, s)); + settingsWriteQueue.push(() => writeFile(SETTINGS_FILE, s)); }); diff --git a/src/plugins/whoReacted.tsx b/src/plugins/whoReacted.tsx index 00799831b..b1fb27d58 100644 --- a/src/plugins/whoReacted.tsx +++ b/src/plugins/whoReacted.tsx @@ -16,20 +16,56 @@ * along with this program. If not, see . */ -import { User } from "discord-types/general"; +import { ReactionEmoji, User } from "discord-types/general"; import ErrorBoundary from "../components/ErrorBoundary"; import { Devs } from "../utils/constants"; -import { LazyComponent, lazyWebpack } from "../utils/misc"; +import { LazyComponent, lazyWebpack, sleep, useForceUpdater } from "../utils/misc"; +import { Queue } from "../utils/Queue"; import definePlugin from "../utils/types"; import { filters, findByCode } from "../webpack"; -import { ChannelStore, React, Tooltip } from "../webpack/common"; +import { ChannelStore, FluxDispatcher, React, RestAPI, Tooltip } from "../webpack/common"; const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers")); const AvatarStyles = lazyWebpack(filters.byProps("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar")); const ReactionStore = lazyWebpack(filters.byProps("getReactions")); +const queue = new Queue(); + +function fetchReactions(msg: Message, emoji: ReactionEmoji) { + const key = emoji.name + (emoji.id ? `:${emoji.id}` : ""); + return RestAPI.get({ + url: `/channels/${msg.channel_id}/messages/${msg.id}/reactions/${key}`, + query: { + limit: 100 + }, + oldFormErrors: true + }) + .then(res => FluxDispatcher.dispatch({ + type: "MESSAGE_REACTION_ADD_USERS", + channelId: msg.channel_id, + messageId: msg.id, + users: res.body, + emoji + })) + .catch(console.error) + .finally(() => sleep(250)); +} + +function getReactionsWithQueue(msg: Message, e: ReactionEmoji) { + const key = `${msg.id}:${e.name}:${e.id ?? ""}`; + const cache = ReactionStore.__getLocalVars().reactions[key] ??= { fetched: false, users: {} }; + if (!cache.fetched) { + queue.unshift(() => + fetchReactions(msg, e) + ); + cache.fetched = true; + } + + return cache.users; +} + function makeRenderMoreUsers(users: User[]) { return function renderMoreUsers(_label: string, _count: number) { return ( @@ -62,7 +98,7 @@ export default definePlugin({ }], renderUsers(props: RootObject) { - return ( + return props.message.reactions.length > 10 ? null : ( @@ -70,8 +106,19 @@ export default definePlugin({ }, _renderUsers({ message, emoji }: RootObject) { - const reactions = ReactionStore.getReactions(message.channel_id, message.id, emoji); - const users = Object.values(reactions) as User[]; + const forceUpdate = useForceUpdater(); + React.useEffect(() => { + const cb = (e: any) => { + if (e.messageId === message.id) + forceUpdate(); + }; + FluxDispatcher.subscribe("MESSAGE_REACTION_ADD_USERS", cb); + + return () => FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD_USERS", cb); + }, [message.id]); + + const reactions = getReactionsWithQueue(message, emoji); + const users = Object.values(reactions).filter(Boolean) as User[]; return (
= Promise.resolve(); + /** + * @param maxSize The maximum amount of functions that can be queued at once. + * If the queue is full, the oldest function will be removed. + */ + constructor(public maxSize = Infinity) { } - add(func: (lastValue: unknown) => Promisable): Promise { - return (this.promise = this.promise.then(func)); + queue = [] as Array<() => Promisable>; + + private promise?: Promise; + + private next() { + const func = this.queue.shift(); + if (func) + this.promise = Promise.resolve() + .then(func) + .then(() => this.next()); + else + this.promise = undefined; + } + + private run() { + if (!this.promise) + this.next(); + } + + push(func: () => Promisable) { + if (this.size >= this.maxSize) + this.queue.shift(); + + this.queue.push(func); + this.run(); + } + + unshift(func: () => Promisable) { + if (this.size >= this.maxSize) + this.queue.pop(); + + this.queue.unshift(func); + this.run(); + } + + get size() { + return this.queue.length; } } diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx index 005bcc084..44fc819a4 100644 --- a/src/utils/misc.tsx +++ b/src/utils/misc.tsx @@ -70,6 +70,11 @@ export function useAwaiter(factory: () => Promise, fallbackValue: T | null return [state.value, state.error, state.pending, () => setSignal(signal + 1)]; } +export function useForceUpdater() { + const [, set] = React.useState(0); + return () => set(s => s + 1); +} + /** * A lazy component. The factory method is called on first render. For example useful * for const Component = LazyComponent(() => findByDisplayName("...").default) diff --git a/src/webpack/common.tsx b/src/webpack/common.tsx index 6ebad1677..2f3768eb9 100644 --- a/src/webpack/common.tsx +++ b/src/webpack/common.tsx @@ -32,6 +32,7 @@ export const Flux = lazyWebpack(filters.byProps("connectStores")); export let React: typeof import("react"); export const ReactDOM: typeof import("react-dom") = lazyWebpack(filters.byProps("createPortal", "render")); +export const RestAPI = lazyWebpack(filters.byProps("getAPIBaseURL", "get")); export const moment: typeof import("moment") = lazyWebpack(filters.byProps("parseTwoDigitYear")); export const MessageStore = lazyWebpack(filters.byProps("getRawMessages")) as Omit & { getMessages(chanId: string): any; }; From c2c6c9fccb3b94340037d2136d647817d0a1c916 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 25 Nov 2022 18:28:15 +0100 Subject: [PATCH 04/18] CallTimer: Fix lag --- src/plugins/callTimer.ts | 116 -------------------------------------- src/plugins/callTimer.tsx | 101 +++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 116 deletions(-) delete mode 100644 src/plugins/callTimer.ts create mode 100644 src/plugins/callTimer.tsx diff --git a/src/plugins/callTimer.ts b/src/plugins/callTimer.ts deleted file mode 100644 index 264e74bd7..000000000 --- a/src/plugins/callTimer.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { Settings } from "../api/settings"; -import { Devs } from "../utils/constants"; -import definePlugin, { OptionType } from "../utils/types"; -import { FluxDispatcher } from "../webpack/common"; - -export default definePlugin({ - name: "CallTimer", - description: "Adds a timer to vcs", - authors: [Devs.Ven], - - style: void 0 as HTMLStyleElement | undefined, - startTime: 0, - interval: void 0 as NodeJS.Timeout | undefined, - - options: { - format: { - type: OptionType.SELECT, - description: "The timer format. This can be any valid moment.js format", - options: [ - { - label: "30d 23:00:42", - value: "stopwatch", - default: true - }, - { - label: "30d 23h 00m 42s", - value: "human" - } - ] - } - }, - - - formatDuration(ms: number) { - // here be dragons (moment fucking sucks) - const human = Settings.plugins.CallTimer.format === "human"; - - const format = (n: number) => human ? n : n.toString().padStart(2, "0"); - const unit = (s: string) => human ? s : ""; - const delim = human ? " " : ":"; - - // thx copilot - const d = Math.floor(ms / 86400000); - const h = Math.floor((ms % 86400000) / 3600000); - const m = Math.floor(((ms % 86400000) % 3600000) / 60000); - const s = Math.floor((((ms % 86400000) % 3600000) % 60000) / 1000); - - let res = ""; - if (d) res += `${d}d `; - if (h || res) res += `${format(h)}${unit("h")}${delim}`; - if (m || res || !human) res += `${format(m)}${unit("m")}${delim}`; - res += `${format(s)}${unit("s")}`; - - return res; - }, - - setTimer(ms: number) { - if (!this.style) return; - - this.style.textContent = ` - [class*="connection-"] [class*="channel-"]::after { - content: "Connected for ${this.formatDuration(ms)}"; - display: block; - } - `; - }, - - start() { - const style = this.style = document.createElement("style"); - style.id = "VencordCallTimer"; - document.head.appendChild(style); - - this.setTimer(0); - - this.handleRtcConnectionState = this.handleRtcConnectionState.bind(this); - FluxDispatcher.subscribe("RTC_CONNECTION_STATE", this.handleRtcConnectionState); - }, - - handleRtcConnectionState(e: { state: string; }) { - if (e.state === "RTC_CONNECTED" || e.state === "RTC_DISCONNECTED") { - clearInterval(this.interval); - if (e.state === "RTC_CONNECTED") { - this.startTime = Date.now(); - this.interval = setInterval( - () => this.setTimer(Math.round(Date.now() - this.startTime)), - 1000 - ); - } else this.startTime = 0; - this.setTimer(0); - } - }, - - stop() { - FluxDispatcher.unsubscribe("RTC_CONNECTION_STATE", this.handleRtcConnectionState); - this.style?.remove(); - clearInterval(this.interval); - } -}); diff --git a/src/plugins/callTimer.tsx b/src/plugins/callTimer.tsx new file mode 100644 index 000000000..40aa1608f --- /dev/null +++ b/src/plugins/callTimer.tsx @@ -0,0 +1,101 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Settings } from "../api/settings"; +import ErrorBoundary from "../components/ErrorBoundary"; +import { Devs } from "../utils/constants"; +import definePlugin, { OptionType } from "../utils/types"; +import { React } from "../webpack/common"; + +function formatDuration(ms: number) { + // here be dragons (moment fucking sucks) + const human = Settings.plugins.CallTimer.format === "human"; + + const format = (n: number) => human ? n : n.toString().padStart(2, "0"); + const unit = (s: string) => human ? s : ""; + const delim = human ? " " : ":"; + + // thx copilot + const d = Math.floor(ms / 86400000); + const h = Math.floor((ms % 86400000) / 3600000); + const m = Math.floor(((ms % 86400000) % 3600000) / 60000); + const s = Math.floor((((ms % 86400000) % 3600000) % 60000) / 1000); + + let res = ""; + if (d) res += `${d}d `; + if (h || res) res += `${format(h)}${unit("h")}${delim}`; + if (m || res || !human) res += `${format(m)}${unit("m")}${delim}`; + res += `${format(s)}${unit("s")}`; + + return res; +} + +export default definePlugin({ + name: "CallTimer", + description: "Adds a timer to vcs", + authors: [Devs.Ven], + + startTime: 0, + interval: void 0 as NodeJS.Timeout | undefined, + + options: { + format: { + type: OptionType.SELECT, + description: "The timer format. This can be any valid moment.js format", + options: [ + { + label: "30d 23:00:42", + value: "stopwatch", + default: true + }, + { + label: "30d 23h 00m 42s", + value: "human" + } + ] + } + }, + + patches: [{ + find: ".renderConnectionStatus=", + replacement: { + match: /(?<=renderConnectionStatus=.+\(\)\.channel,children:)\w/, + replace: "[$&, Vencord.Plugins.plugins.CallTimer.renderTimer(this.props.channel.id)]" + } + }], + renderTimer(channelId: string) { + return + + ; + }, + + Timer({ channelId }: { channelId: string; }) { + const [time, setTime] = React.useState(0); + const startTime = React.useMemo(() => Date.now(), [channelId]); + + React.useEffect(() => { + const interval = setInterval(() => setTime(Date.now() - startTime), 1000); + return () => { + clearInterval(interval); + setTime(0); + }; + }, [channelId]); + + return

Connected for {formatDuration(time)}

; + } +}); From a85ec594a77557e5f48c4cf1aa33680e0da94654 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 25 Nov 2022 19:25:35 +0100 Subject: [PATCH 05/18] [skip ci] docs docs docs --- src/components/PluginSettings/index.tsx | 2 +- src/components/Settings.tsx | 24 +----------------------- src/utils/Queue.ts | 23 ++++++++++++++++++++--- src/utils/debounce.ts | 7 +++++++ src/utils/misc.tsx | 6 ++++++ src/utils/modal.tsx | 12 ++++++++++++ src/webpack/webpack.ts | 22 ++++++++++++++++------ 7 files changed, 63 insertions(+), 33 deletions(-) diff --git a/src/components/PluginSettings/index.tsx b/src/components/PluginSettings/index.tsx index 2de93621f..39fb9112e 100644 --- a/src/components/PluginSettings/index.tsx +++ b/src/components/PluginSettings/index.tsx @@ -302,7 +302,7 @@ export default ErrorBoundary.wrap(function Settings() { changes.add(name)} + onRestartNeeded={name => changes.handleChange(name)} disabled={plugin.required || !!dependency} plugin={plugin} /> diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index e2abff2e3..f8e74955f 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -17,11 +17,10 @@ */ import { useSettings } from "../api/settings"; -import { ChangeList } from "../utils/ChangeList"; import IpcEvents from "../utils/IpcEvents"; import { useAwaiter } from "../utils/misc"; import { downloadSettingsBackup, uploadSettingsBackup } from "../utils/settingsSync"; -import { Alerts, Button, Card, Forms, Margins, Parser, React, Switch } from "../webpack/common"; +import { Button, Card, Forms, Margins, React, Switch } from "../webpack/common"; import DonateButton from "./DonateButton"; import ErrorBoundary from "./ErrorBoundary"; import { Flex } from "./Flex"; @@ -30,27 +29,6 @@ import { handleComponentFailed } from "./handleComponentFailed"; export default ErrorBoundary.wrap(function Settings() { const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke(IpcEvents.GET_SETTINGS_DIR), "Loading..."); const settings = useSettings(); - const changes = React.useMemo(() => new ChangeList(), []); - - React.useEffect(() => { - return () => void (changes.hasChanges && Alerts.show({ - title: "Restart required", - body: ( - <> -

The following plugins require a restart:

-
{changes.map((s, i) => ( - <> - {i > 0 && ", "} - {Parser.parse("`" + s + "`")} - - ))}
- - ), - confirmText: "Restart now", - cancelText: "Later!", - onConfirm: () => location.reload() - })); - }, []); return ( diff --git a/src/utils/Queue.ts b/src/utils/Queue.ts index 6153d4065..86eb79196 100644 --- a/src/utils/Queue.ts +++ b/src/utils/Queue.ts @@ -18,14 +18,18 @@ import { Promisable } from "type-fest"; +/** + * A queue that can be used to run tasks consecutively. + * Highly recommended for things like fetching data from Discord + */ export class Queue { /** * @param maxSize The maximum amount of functions that can be queued at once. - * If the queue is full, the oldest function will be removed. + * If the queue is full, the oldest function will be removed. */ constructor(public maxSize = Infinity) { } - queue = [] as Array<() => Promisable>; + private queue = [] as Array<() => Promisable>; private promise?: Promise; @@ -34,7 +38,7 @@ export class Queue { if (func) this.promise = Promise.resolve() .then(func) - .then(() => this.next()); + .finally(() => this.next()); else this.promise = undefined; } @@ -44,6 +48,11 @@ export class Queue { this.next(); } + /** + * Append a task at the end of the queue. This task will be executed after all other tasks + * If the queue exceeds the specified maxSize, the first task in queue will be removed. + * @param func Task + */ push(func: () => Promisable) { if (this.size >= this.maxSize) this.queue.shift(); @@ -52,6 +61,11 @@ export class Queue { this.run(); } + /** + * Prepend a task at the beginning of the queue. This task will be executed next + * If the queue exceeds the specified maxSize, the last task in queue will be removed. + * @param func Task + */ unshift(func: () => Promisable) { if (this.size >= this.maxSize) this.queue.pop(); @@ -60,6 +74,9 @@ export class Queue { this.run(); } + /** + * The amount of tasks in the queue + */ get size() { return this.queue.length; } diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts index d9e19defc..6e5bba661 100644 --- a/src/utils/debounce.ts +++ b/src/utils/debounce.ts @@ -16,6 +16,13 @@ * along with this program. If not, see . */ +/** + * Returns a new function that will call the wrapped function + * after the specified delay. If the function is called again + * within the delay, the timer will be reset. + * @param func The function to wrap + * @param delay The delay in milliseconds + */ export function debounce(func: T, delay = 300): T { let timeout: NodeJS.Timeout; return function (...args: any[]) { diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx index 44fc819a4..7389cc2ce 100644 --- a/src/utils/misc.tsx +++ b/src/utils/misc.tsx @@ -70,6 +70,9 @@ export function useAwaiter(factory: () => Promise, fallbackValue: T | null return [state.value, state.error, state.pending, () => setSignal(signal + 1)]; } +/** + * Returns a function that can be used to force rerender react components + */ export function useForceUpdater() { const [, set] = React.useState(0); return () => set(s => s + 1); @@ -144,6 +147,9 @@ export function classes(...classes: string[]) { return classes.join(" "); } +/** + * Returns a promise that resolves after the specified amount of time + */ export function sleep(ms: number): Promise { return new Promise(r => setTimeout(r, ms)); } diff --git a/src/utils/modal.tsx b/src/utils/modal.tsx index 2affbd7d1..886e325de 100644 --- a/src/utils/modal.tsx +++ b/src/utils/modal.tsx @@ -76,14 +76,26 @@ const ModalAPI = mapMangledModuleLazy("onCloseRequest:null!=", { openModalLazy: m => m?.length === 1 && filters.byCode(".apply(this,arguments)")(m), }); +/** + * Wait for the render promise to resolve, then open a modal with it. + * This is equivalent to render().then(openModal) + * You should use the Modal components exported by this file + */ export function openModalLazy(render: () => Promise, options?: ModalOptions & { contextKey?: string; }): Promise { return ModalAPI.openModalLazy(render, options); } +/** + * Open a Modal with the given render function. + * You should use the Modal components exported by this file + */ export function openModal(render: RenderFunction, options?: ModalOptions, contextKey?: string): string { return ModalAPI.openModal(render, options, contextKey); } +/** + * Close a modal by its key + */ export function closeModal(modalKey: string, contextKey?: string): void { return ModalAPI.closeModal(modalKey, contextKey); } diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 1231d9e4c..bd06b030c 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -42,8 +42,6 @@ export const filters = { ? m => m[props[0]] !== void 0 : m => props.every(p => m[p] !== void 0), - byDisplayName: (deezNuts: string): FilterFn => m => m.default?.displayName === deezNuts, - byCode: (...code: string[]): FilterFn => m => { if (typeof m !== "function") return false; const s = Function.prototype.toString.call(m); @@ -75,6 +73,9 @@ if (IS_DEV && !IS_WEB) { }, 0); } +/** + * Find the first module that matches the filter + */ export const find = traceFunction("find", function find(filter: FilterFn, getDefault = true, isWaitFor = false) { if (typeof filter !== "function") throw new Error("Invalid filter. Expected a function got " + typeof filter); @@ -283,22 +284,31 @@ export function mapMangledModuleLazy(code: string, mappers: Re return proxyLazy(() => mapMangledModule(code, mappers)); } +/** + * Find the first module that has the specified properties + */ export function findByProps(...props: string[]) { return find(filters.byProps(...props)); } +/** + * Find all modules that have the specified properties + */ export function findAllByProps(...props: string[]) { return findAll(filters.byProps(...props)); } +/** + * Find a function by its code + */ export function findByCode(...code: string[]) { return find(filters.byCode(...code)); } -export function findByDisplayName(deezNuts: string) { - return find(filters.byDisplayName(deezNuts)); -} - +/** + * Wait for a module that matches the provided filter to be registered, + * then call the callback with the module as the first argument + */ export function waitFor(filter: string | string[] | FilterFn, callback: CallbackFn) { if (typeof filter === "string") filter = filters.byProps(filter); From 6b55dee9fbba1a54cbe2ba1f2067e2d14190ab93 Mon Sep 17 00:00:00 2001 From: megumin Date: Fri, 25 Nov 2022 22:38:55 +0000 Subject: [PATCH 06/18] feat(settings): new settings design (#261) --- src/components/PatchHelper.tsx | 7 +- src/components/PluginSettings/index.tsx | 6 +- src/components/Settings.tsx | 140 ------------------ .../VencordSettings/BackupRestoreTab.tsx | 69 +++++++++ src/components/VencordSettings/PluginsTab.tsx | 22 +++ .../{ => VencordSettings}/Updater.tsx | 18 +-- src/components/VencordSettings/VencordTab.tsx | 134 +++++++++++++++++ src/components/VencordSettings/index.tsx | 84 +++++++++++ .../VencordSettings/settingsStyles.css | 23 +++ src/components/index.ts | 3 +- src/plugins/settings.tsx | 21 ++- src/plugins/spotifyControls/SpotifyStore.ts | 2 +- .../{styles.css => spotifyStyles.css} | 0 src/webpack/common.tsx | 2 +- 14 files changed, 369 insertions(+), 162 deletions(-) delete mode 100644 src/components/Settings.tsx create mode 100644 src/components/VencordSettings/BackupRestoreTab.tsx create mode 100644 src/components/VencordSettings/PluginsTab.tsx rename src/components/{ => VencordSettings}/Updater.tsx (94%) create mode 100644 src/components/VencordSettings/VencordTab.tsx create mode 100644 src/components/VencordSettings/index.tsx create mode 100644 src/components/VencordSettings/settingsStyles.css rename src/plugins/spotifyControls/{styles.css => spotifyStyles.css} (100%) diff --git a/src/components/PatchHelper.tsx b/src/components/PatchHelper.tsx index 82b804ae4..0314de895 100644 --- a/src/components/PatchHelper.tsx +++ b/src/components/PatchHelper.tsx @@ -18,7 +18,7 @@ import { debounce } from "../utils/debounce"; import { makeCodeblock } from "../utils/misc"; -import { Button, Clipboard, Forms, Margins, Parser, React, Switch, TextInput } from "../webpack/common"; +import { Button, Clipboard, Forms, Margins, Parser, React, Switch, Text, TextInput } from "../webpack/common"; import { search } from "../webpack/webpack"; import { CheckedTextInput } from "./CheckedTextInput"; import ErrorBoundary from "./ErrorBoundary"; @@ -243,7 +243,8 @@ function PatchHelper() { } return ( - <> + + Patch Helper find Clipboard.copy(code)}>Copy to Clipboard )} - + ); } diff --git a/src/components/PluginSettings/index.tsx b/src/components/PluginSettings/index.tsx index 39fb9112e..f16d55c4b 100644 --- a/src/components/PluginSettings/index.tsx +++ b/src/components/PluginSettings/index.tsx @@ -243,9 +243,9 @@ export default ErrorBoundary.wrap(function Settings() { }; return ( - + - Plugins + Filters @@ -268,6 +268,8 @@ export default ErrorBoundary.wrap(function Settings() {
+ Plugins +
{sortedPlugins?.length ? sortedPlugins .filter(a => !a.required && !dependencyCheck(a.name, depMap).length && pluginFilter(a)) diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx deleted file mode 100644 index f8e74955f..000000000 --- a/src/components/Settings.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { useSettings } from "../api/settings"; -import IpcEvents from "../utils/IpcEvents"; -import { useAwaiter } from "../utils/misc"; -import { downloadSettingsBackup, uploadSettingsBackup } from "../utils/settingsSync"; -import { Button, Card, Forms, Margins, React, Switch } from "../webpack/common"; -import DonateButton from "./DonateButton"; -import ErrorBoundary from "./ErrorBoundary"; -import { Flex } from "./Flex"; -import { handleComponentFailed } from "./handleComponentFailed"; - -export default ErrorBoundary.wrap(function Settings() { - const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke(IpcEvents.GET_SETTINGS_DIR), "Loading..."); - const settings = useSettings(); - - return ( - - -
- Support the Project - - Please consider supporting the Development of Vencord by donating! - - -
- -
- - - Settings - - - - Settings Directory: {settingsDir} - - - {!IS_WEB && - - - - } - - {IS_WEB && } - - - settings.useQuickCss = v} - note="Loads styles from your QuickCss file" - > - Use QuickCss - - {!IS_WEB && settings.enableReactDevtools = v} - note="Requires a full restart" - > - Enable React Developer Tools - } - {!IS_WEB && settings.notifyAboutUpdates = v} - note="Shows a Toast on StartUp" - > - Get notified about new Updates - } - - - Settings Sync - - - - -
- ); -}, { - message: "Failed to render the Settings. If this persists, try using the installer to reinstall!", - onError: handleComponentFailed, -}); diff --git a/src/components/VencordSettings/BackupRestoreTab.tsx b/src/components/VencordSettings/BackupRestoreTab.tsx new file mode 100644 index 000000000..ce0bdaa6e --- /dev/null +++ b/src/components/VencordSettings/BackupRestoreTab.tsx @@ -0,0 +1,69 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { downloadSettingsBackup, uploadSettingsBackup } from "../../utils/settingsSync"; +import { Button, Card, Forms, Margins, Text } from "../../webpack/common"; +import ErrorBoundary from "../ErrorBoundary"; +import { Flex } from "../Flex"; + +function BackupRestoreTab() { + return ( + + + + Warning + Importing a settings file will overwrite your current settings. + + + + You can import and export your Vencord settings as a JSON file. + This allows you to easily transfer your settings to another device, + or recover your settings after reinstalling Vencord or Discord. + + + Settings Export contains: +
    +
  • — Custom QuickCSS
  • +
  • — Plugin Settings
  • +
+
+ + + + +
+ ); +} + +export default ErrorBoundary.wrap(BackupRestoreTab); diff --git a/src/components/VencordSettings/PluginsTab.tsx b/src/components/VencordSettings/PluginsTab.tsx new file mode 100644 index 000000000..0c8968637 --- /dev/null +++ b/src/components/VencordSettings/PluginsTab.tsx @@ -0,0 +1,22 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import ErrorBoundary from "../ErrorBoundary"; +import PluginSettings from "../PluginSettings"; + +export default ErrorBoundary.wrap(PluginSettings); diff --git a/src/components/Updater.tsx b/src/components/VencordSettings/Updater.tsx similarity index 94% rename from src/components/Updater.tsx rename to src/components/VencordSettings/Updater.tsx index 8abdf288f..8f2b75beb 100644 --- a/src/components/Updater.tsx +++ b/src/components/VencordSettings/Updater.tsx @@ -18,14 +18,14 @@ import gitHash from "~git-hash"; -import { classes, useAwaiter } from "../utils/misc"; -import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "../utils/updater"; -import { Alerts, Button, Card, Forms, Margins, Parser, React, Toasts } from "../webpack/common"; -import ErrorBoundary from "./ErrorBoundary"; -import { ErrorCard } from "./ErrorCard"; -import { Flex } from "./Flex"; -import { handleComponentFailed } from "./handleComponentFailed"; -import { Link } from "./Link"; +import { classes, useAwaiter } from "../../utils/misc"; +import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "../../utils/updater"; +import { Alerts, Button, Card, Forms, Margins, Parser, React, Toasts } from "../../webpack/common"; +import ErrorBoundary from "../ErrorBoundary"; +import { ErrorCard } from "../ErrorCard"; +import { Flex } from "../Flex"; +import { handleComponentFailed } from "../handleComponentFailed"; +import { Link } from "../Link"; function withDispatcher(dispatcher: React.Dispatch>, action: () => any) { return async () => { @@ -192,7 +192,7 @@ function Updater() { }; return ( - + Repo {repoPending ? repo : err ? "Failed to retrieve - check console" : ( diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx new file mode 100644 index 000000000..94add5ed6 --- /dev/null +++ b/src/components/VencordSettings/VencordTab.tsx @@ -0,0 +1,134 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + + +import { useSettings } from "../../api/settings"; +import IpcEvents from "../../utils/IpcEvents"; +import { useAwaiter } from "../../utils/misc"; +import { Button, Card, Forms, React, Switch } from "../../webpack/common"; +import DonateButton from "../DonateButton"; +import ErrorBoundary from "../ErrorBoundary"; + +const st = (style: string) => `vcSettings${style}`; + +function VencordSettings() { + const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke(IpcEvents.GET_SETTINGS_DIR), "Loading..."); + const settings = useSettings(); + + return ( + + + + + {IS_WEB ? ( + + ) : ( + + + + + + + )} + + + + + + + settings.useQuickCss = v} + note="Loads styles from your QuickCss file"> + Use QuickCss + + {!IS_WEB && ( + + settings.enableReactDevtools = v} + note="Requires a full restart"> + Enable React Developer Tools + + settings.notifyAboutUpdates = v} + note="Shows a Toast on StartUp"> + Get notified about new Updates + + + )} + + + + ); +} + + + +function DonateCard() { + return ( + +
+ Support the Project + + Please consider supporting the Development of Vencord by donating! + + +
+ +
+ ); +} + +export default ErrorBoundary.wrap(VencordSettings); diff --git a/src/components/VencordSettings/index.tsx b/src/components/VencordSettings/index.tsx new file mode 100644 index 000000000..370189786 --- /dev/null +++ b/src/components/VencordSettings/index.tsx @@ -0,0 +1,84 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import cssText from "~fileContent/settingsStyles.css"; + +import { lazyWebpack } from "../../utils/misc"; +import { filters } from "../../webpack"; +import { Forms, React, Router, Text } from "../../webpack/common"; +import ErrorBoundary from "../ErrorBoundary"; +import BackupRestoreTab from "./BackupRestoreTab"; +import PluginsTab from "./PluginsTab"; +import Updater from "./Updater"; +import VencordSettings from "./VencordTab"; + +const style = document.createElement("style"); +style.textContent = cssText; +document.head.appendChild(style); + +const st = (style: string) => `vcSettings${style}`; + +const TabBar = lazyWebpack(filters.byCode('[role="tab"][aria-disabled="false"]')); + +interface SettingsProps { + tab: string; +} + +const SettingsTabs = { + VencordSettings: { name: "Vencord", component: () => }, + VencordPlugins: { name: "Plugins", component: () => }, + VencordThemes: { name: "Themes", component: () => Coming soon to a Vencord near you! }, + VencordUpdater: { name: "Updater", component: () => Updater ? : null }, + VencordSettingsSync: { name: "Backup & Restore", component: () => }, +}; + + +function Settings(props: SettingsProps) { + const { tab = "VencordSettings" } = props; + + const CurrentTab = SettingsTabs[tab]?.component ?? null; + + return + Vencord Settings + + + {Object.entries(SettingsTabs).map(([key, { name }]) => { + return + {name} + ; + })} + + + + ; +} + +export default function (props: SettingsProps) { + return + + ; +} diff --git a/src/components/VencordSettings/settingsStyles.css b/src/components/VencordSettings/settingsStyles.css new file mode 100644 index 000000000..e90ef141b --- /dev/null +++ b/src/components/VencordSettings/settingsStyles.css @@ -0,0 +1,23 @@ +.vcSettingsTabBar { + margin-top: 20px; + margin-bottom: -2px; + border-bottom: 2px solid var(--background-modifier-accent); +} + +.vcSettingsTabBarItem { + margin-right: 32px; + padding-bottom: 16px; + margin-bottom: -2px; +} + +.vcSettingsQuickActionCard { + padding: 1em; + display: flex; + gap: 1em; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + flex-grow: 1; + flex-direction: row; + margin-bottom: 1em; +} diff --git a/src/components/index.ts b/src/components/index.ts index 80d2cd129..3ee53b02c 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -18,5 +18,4 @@ export { default as PatchHelper } from "./PatchHelper"; export { default as PluginSettings } from "./PluginSettings"; -export { default as Settings } from "./Settings"; -export { default as Updater } from "./Updater"; +export { default as VencordSettings } from "./VencordSettings"; diff --git a/src/plugins/settings.tsx b/src/plugins/settings.tsx index ec4295667..27775725b 100644 --- a/src/plugins/settings.tsx +++ b/src/plugins/settings.tsx @@ -19,8 +19,11 @@ import gitHash from "~git-hash"; import { Devs } from "../utils/constants"; +import { LazyComponent } from "../utils/misc"; import definePlugin from "../utils/types"; +const SettingsComponent = LazyComponent(() => require("../components/VencordSettings").default); + export default definePlugin({ name: "Settings", description: "Adds Settings UI and debug info", @@ -42,13 +45,15 @@ export default definePlugin({ replacement: { match: /\{section:(.{1,2})\.ID\.HEADER,\s*label:(.{1,2})\..{1,2}\.Messages\.ACTIVITY_SETTINGS\}/, replace: (m, mod) => { - const updater = !IS_WEB ? '{section:"VencordUpdater",label:"Updater",element:Vencord.Components.Updater},' : ""; - const patchHelper = IS_DEV ? '{section:"VencordPatchHelper",label:"PatchHelper",element:Vencord.Components.PatchHelper},' : ""; + const updater = !IS_WEB ? '{section:"VencordUpdater",label:"Updater",element:Vencord.Plugins.plugins.Settings.tabs.updater},' : ""; + const patchHelper = IS_DEV ? '{section:"VencordPatchHelper",label:"Patch Helper",element:Vencord.Components.PatchHelper},' : ""; return ( `{section:${mod}.ID.HEADER,label:"Vencord"},` + - '{section:"VencordSetting",label:"Vencord",element:Vencord.Components.Settings},' + - '{section:"VencordPlugins",label:"Plugins",element:Vencord.Components.PluginSettings},' + + '{section:"VencordSettings",label:"Vencord",element:Vencord.Plugins.plugins.Settings.tabs.vencord},' + + '{section:"VencordPlugins",label:"Plugins",element:Vencord.Plugins.plugins.Settings.tabs.plugins},' + + '{section:"VencordThemes",label:"Themes",element:Vencord.Plugins.plugins.Settings.tabs.themes},' + updater + + '{section:"VencordSettingsSync",label:"Backup & Restore",element:Vencord.Plugins.plugins.Settings.tabs.sync},' + patchHelper + `{section:${mod}.ID.DIVIDER},${m}` ); @@ -56,6 +61,14 @@ export default definePlugin({ } }], + tabs: { + vencord: () => , + plugins: () => , + themes: () => , + updater: () => , + sync: () => + }, + get electronVersion() { return VencordNative.getVersions().electron || window.armcord?.electron || null; }, diff --git a/src/plugins/spotifyControls/SpotifyStore.ts b/src/plugins/spotifyControls/SpotifyStore.ts index 0dad5038b..14f233182 100644 --- a/src/plugins/spotifyControls/SpotifyStore.ts +++ b/src/plugins/spotifyControls/SpotifyStore.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import cssText from "~fileContent/styles.css"; +import cssText from "~fileContent/spotifyStyles.css"; import IpcEvents from "../../utils/IpcEvents"; import { lazyWebpack } from "../../utils/misc"; diff --git a/src/plugins/spotifyControls/styles.css b/src/plugins/spotifyControls/spotifyStyles.css similarity index 100% rename from src/plugins/spotifyControls/styles.css rename to src/plugins/spotifyControls/spotifyStyles.css diff --git a/src/webpack/common.tsx b/src/webpack/common.tsx index 2f3768eb9..32f47e663 100644 --- a/src/webpack/common.tsx +++ b/src/webpack/common.tsx @@ -195,7 +195,7 @@ export type TextProps = React.PropsWithChildren & { className?: string; }; -export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-md" | "display-lg" | "code"; +export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code"; type RC = React.ComponentType>>; interface Menu { From 5a942015784887780a38dadb6221b5ee6ac505c9 Mon Sep 17 00:00:00 2001 From: Ven Date: Fri, 25 Nov 2022 23:41:02 +0100 Subject: [PATCH 07/18] Megu blowing up main :blobcatcozyscared: :trolleybus: --- src/components/VencordSettings/VencordTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx index 94add5ed6..0e5846ffd 100644 --- a/src/components/VencordSettings/VencordTab.tsx +++ b/src/components/VencordSettings/VencordTab.tsx @@ -37,7 +37,7 @@ function VencordSettings() { {IS_WEB ? (