diff options
Diffstat (limited to 'src')
24 files changed, 533 insertions, 105 deletions
diff --git a/src/api/Notifications/NotificationComponent.tsx b/src/api/Notifications/NotificationComponent.tsx new file mode 100644 index 0000000..65d4c43 --- /dev/null +++ b/src/api/Notifications/NotificationComponent.tsx @@ -0,0 +1,92 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import "./styles.css"; + +import { useSettings } from "@api/settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Forms, React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common"; + +import { NotificationData } from "./Notifications"; + +export default ErrorBoundary.wrap(function NotificationComponent({ + title, + body, + richBody, + color, + icon, + onClick, + onClose, + image +}: NotificationData) { + const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications; + const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused()); + + const [isHover, setIsHover] = useState(false); + const [elapsed, setElapsed] = useState(0); + + const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]); + + useEffect(() => { + if (isHover || !hasFocus || timeout === 0) return void setElapsed(0); + + const intervalId = setInterval(() => { + const elapsed = Date.now() - start; + if (elapsed >= timeout) + onClose!(); + else + setElapsed(elapsed); + }, 10); + + return () => clearInterval(intervalId); + }, [timeout, isHover, hasFocus]); + + const timeoutProgress = elapsed / timeout; + + return ( + <button + className="vc-notification-root" + style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }} + onClick={onClick} + onContextMenu={e => { + e.preventDefault(); + e.stopPropagation(); + onClose!(); + }} + onMouseEnter={() => setIsHover(true)} + onMouseLeave={() => setIsHover(false)} + > + <div className="vc-notification"> + {icon && <img className="vc-notification-icon" src={icon} alt="" />} + <div className="vc-notification-content"> + <Forms.FormTitle tag="h2">{title}</Forms.FormTitle> + <div> + {richBody ?? <p className="vc-notification-p">{body}</p>} + </div> + </div> + </div> + {image && <img className="vc-notification-img" src={image} alt="" />} + {timeout !== 0 && ( + <div + className="vc-notification-progressbar" + style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }} + /> + )} + </button> + ); +}); diff --git a/src/api/Notifications/Notifications.tsx b/src/api/Notifications/Notifications.tsx new file mode 100644 index 0000000..9c599aa --- /dev/null +++ b/src/api/Notifications/Notifications.tsx @@ -0,0 +1,92 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { Settings } from "@api/settings"; +import { Queue } from "@utils/Queue"; +import { ReactDOM } from "@webpack/common"; +import type { ReactNode } from "react"; +import type { Root } from "react-dom/client"; + +import NotificationComponent from "./NotificationComponent"; + +const NotificationQueue = new Queue(); + +let reactRoot: Root; +let id = 42; + +function getRoot() { + if (!reactRoot) { + const container = document.createElement("div"); + container.id = "vc-notification-container"; + document.body.append(container); + reactRoot = ReactDOM.createRoot(container); + } + return reactRoot; +} + +export interface NotificationData { + title: string; + body: string; + /** + * Same as body but can be a custom component. + * Will be used over body if present. + * Not supported on desktop notifications, those will fall back to body */ + richBody?: ReactNode; + /** Small icon. This is for things like profile pictures and should be square */ + icon?: string; + /** Large image. Optimally, this should be around 16x9 but it doesn't matter much. Desktop Notifications might not support this */ + image?: string; + onClick?(): void; + onClose?(): void; + color?: string; +} + +function _showNotification(notification: NotificationData, id: number) { + const root = getRoot(); + return new Promise<void>(resolve => { + root.render( + <NotificationComponent key={id} {...notification} onClose={() => { + notification.onClose?.(); + root.render(null); + resolve(); + }} />, + ); + }); +} + +function shouldBeNative() { + const { useNative } = Settings.notifications; + if (useNative === "always") return true; + if (useNative === "not-focused") return !document.hasFocus(); + return false; +} + +export function showNotification(data: NotificationData) { + if (shouldBeNative()) { + const { title, body, icon, image, onClick = null, onClose = null } = data; + const n = new Notification(title, { + body, + icon, + image + }); + n.onclick = onClick; + n.onclose = onClose; + } else { + NotificationQueue.push(() => _showNotification(data, id++)); + } +} diff --git a/src/api/Notifications/index.ts b/src/api/Notifications/index.ts new file mode 100644 index 0000000..cd14587 --- /dev/null +++ b/src/api/Notifications/index.ts @@ -0,0 +1,19 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +export * from "./Notifications"; diff --git a/src/api/Notifications/styles.css b/src/api/Notifications/styles.css new file mode 100644 index 0000000..84d8ff7 --- /dev/null +++ b/src/api/Notifications/styles.css @@ -0,0 +1,49 @@ +.vc-notification-root { + /* clear default button styles */ + all: unset; + display: flex; + flex-direction: column; + width: 25vw; + min-height: 10vh; + color: var(--text-normal); + background-color: var(--background-secondary-alt); + position: absolute; + z-index: 2147483647; + right: 1rem; + border-radius: 6px; + overflow: hidden; + cursor: pointer; +} + +.vc-notification { + display: flex; + flex-direction: row; + padding: 1.25rem; + gap: 1.25rem; +} + +.vc-notification-icon { + height: 4rem; + width: 4rem; + border-radius: 6px; +} + +/* Discord adding 3km margin to generic tags */ +.vc-notification h2 { + margin: unset; +} + +.vc-notification-progressbar { + height: 0.25rem; + border-radius: 5px; + margin-top: auto; +} + +.vc-notification-p { + margin: 0.5rem 0 0; + line-height: 140%; +} + +.vc-notification-img { + width: 100%; +} diff --git a/src/api/index.ts b/src/api/index.ts index 0fef99c..abb5093 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -25,6 +25,7 @@ import * as $MessageDecorations from "./MessageDecorations"; import * as $MessageEventsAPI from "./MessageEvents"; import * as $MessagePopover from "./MessagePopover"; import * as $Notices from "./Notices"; +import * as $Notifications from "./Notifications"; import * as $ServerList from "./ServerList"; import * as $Styles from "./Styles"; @@ -88,3 +89,7 @@ export const MemberListDecorators = $MemberListDecorators; * a */ export const Styles = $Styles; +/** + * An API allowing you to display notifications + */ +export const Notifications = $Notifications; diff --git a/src/api/settings.ts b/src/api/settings.ts index d20e964..c711791 100644 --- a/src/api/settings.ts +++ b/src/api/settings.ts @@ -40,6 +40,12 @@ export interface Settings { [setting: string]: any; }; }; + + notifications: { + timeout: number; + position: "top-right" | "bottom-right"; + useNative: "always" | "never" | "not-focused"; + }; } const DefaultSettings: Settings = { @@ -51,7 +57,13 @@ const DefaultSettings: Settings = { frameless: false, transparent: false, winCtrlQ: false, - plugins: {} + plugins: {}, + + notifications: { + timeout: 5000, + position: "bottom-right", + useNative: "not-focused" + } }; try { diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx index 9f55d57..6ea1ca9 100644 --- a/src/components/VencordSettings/VencordTab.tsx +++ b/src/components/VencordSettings/VencordTab.tsx @@ -22,22 +22,63 @@ import { classNameFactory } from "@api/Styles"; import DonateButton from "@components/DonateButton"; import ErrorBoundary from "@components/ErrorBoundary"; import IpcEvents from "@utils/IpcEvents"; -import { useAwaiter } from "@utils/misc"; -import { Button, Card, Forms, Margins, React, Switch } from "@webpack/common"; +import { Margins } from "@utils/margins"; +import { identity, useAwaiter } from "@utils/misc"; +import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common"; const cl = classNameFactory("vc-settings-"); const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png"; const SHIGGY_DONATE_IMAGE = "https://media.discordapp.net/stickers/1039992459209490513.png"; +type KeysOfType<Object, Type> = { + [K in keyof Object]: Object[K] extends Type ? K : never; +}[keyof Object]; + function VencordSettings() { const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), { fallbackValue: "Loading..." }); const settings = useSettings(); + const notifSettings = settings.notifications; const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []); + const isWindows = navigator.platform.toLowerCase().startsWith("win"); + + const Switches: Array<false | { + key: KeysOfType<typeof settings, boolean>; + title: string; + note: string; + }> = + [ + { + key: "useQuickCss", + title: "Enable Custom CSS", + note: "Loads your Custom CSS" + }, + !IS_WEB && { + key: "enableReactDevtools", + title: "Enable React Developer Tools", + note: "Requires a full restart" + }, + !IS_WEB && !isWindows && { + key: "frameless", + title: "Disable the window frame", + note: "Requires a full restart" + }, + !IS_WEB && { + key: "transparent", + title: "Enable window transparency", + note: "Requires a full restart" + }, + !IS_WEB && isWindows && { + key: "winCtrlQ", + title: "Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)", + note: "Requires a full restart" + } + ]; + return ( <React.Fragment> <DonateCard image={donateImage} /> @@ -82,52 +123,70 @@ function VencordSettings() { <Forms.FormDivider /> - <Forms.FormSection className={Margins.marginTop16} title="Settings"> - <Forms.FormText className={Margins.marginBottom20}> + <Forms.FormSection className={Margins.top16} title="Settings" tag="h5"> + <Forms.FormText className={Margins.bottom20}> Hint: You can change the position of this settings section in the settings of the "Settings" plugin! </Forms.FormText> - <Switch - value={settings.useQuickCss} - onChange={(v: boolean) => settings.useQuickCss = v} - note="Loads styles from your QuickCSS file"> - Use QuickCSS - </Switch> - {!IS_WEB && ( - <React.Fragment> - <Switch - value={settings.enableReactDevtools} - onChange={(v: boolean) => settings.enableReactDevtools = v} - note="Requires a full restart" - > - Enable React Developer Tools - </Switch> - <Switch - value={settings.frameless} - onChange={(v: boolean) => settings.frameless = v} - note="Requires a full restart" - > - Disable the window frame - </Switch> - <Switch - value={settings.transparent} - onChange={(v: boolean) => settings.transparent = v} - note="Requires a full restart" - > - Enable window transparency - </Switch> - {navigator.platform.toLowerCase().startsWith("win") && ( - <Switch - value={settings.winCtrlQ} - onChange={(v: boolean) => settings.winCtrlQ = v} - note="Requires a full restart" - > - Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4) - </Switch> - )} - </React.Fragment> - )} - + {Switches.map(s => s && ( + <Switch + key={s.key} + value={settings[s.key]} + onChange={v => settings[s.key] = v} + note={s.note} + > + {s.title} + </Switch> + ))} </Forms.FormSection> + + + <Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle> + <Forms.FormText className={Margins.bottom8}> + Some plugins may show you notifications. These come in two styles: + <ul> + <li><strong>Vencord Notifications</strong>: These are in-app notifications</li> + <li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li> + </ul> + </Forms.FormText> + <Select + placeholder="Notification Style" + options={[ + { label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true }, + { label: "Always use Desktop notifications", value: "always" }, + { label: "Always use Vencord notifications", value: "never" }, + ]satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>} + closeOnSelect={true} + select={v => notifSettings.useNative = v} + isSelected={v => v === notifSettings.useNative} + serialize={identity} + /> + + <Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle> + <Select + isDisabled={notifSettings.useNative === "always"} + placeholder="Notification Position" + options={[ + { label: "Bottom Right", value: "bottom-right", default: true }, + { label: "Top Right", value: "top-right" }, + ]satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>} + select={v => notifSettings.position = v} + isSelected={v => v === notifSettings.position} + serialize={identity} + /> + + <Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle> + <Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText> + <Slider + disabled={notifSettings.useNative === "always"} + markers={[0, 1000, 2500, 5000, 10_000, 20_000]} + minValue={0} + maxValue={20_000} + initialValue={notifSettings.timeout} + onValueChange={v => notifSettings.timeout = v} + onValueRender={v => (v / 1000).toFixed(2) + "s"} + onMarkerRender={v => (v / 1000) + "s"} + stickToMarkers={false} + /> </React.Fragment> ); } diff --git a/src/modules.d.ts b/src/modules.d.ts index c1a1996..d75a84f 100644 --- a/src/modules.d.ts +++ b/src/modules.d.ts @@ -38,7 +38,8 @@ declare module "~fileContent/*" { export default content; } -declare module "*.css" { } +declare module "*.css"; + declare module "*.css?managed" { const name: string; export default name; diff --git a/src/plugins/spotifyControls/PlayerComponent.tsx b/src/plugins/spotifyControls/PlayerComponent.tsx index f6ad08b..439ecc2 100644 --- a/src/plugins/spotifyControls/PlayerComponent.tsx +++ b/src/plugins/spotifyControls/PlayerComponent.tsx @@ -23,8 +23,8 @@ import { Flex } from "@components/Flex"; import { Link } from "@components/Link"; import { debounce } from "@utils/debounce"; import { classes, LazyComponent } from "@utils/misc"; -import { filters, find, findByCodeLazy } from "@webpack"; -import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState } from "@webpack/common"; +import { filters, find } from "@webpack"; +import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common"; import { SpotifyStore, Track } from "./SpotifyStore"; @@ -37,14 +37,6 @@ function msToHuman(ms: number) { return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; } -const useStateFromStores: <T>( - stores: typeof SpotifyStore[], - mapper: () => T, - idk?: null, - compare?: (old: T, newer: T) => boolean -) => T - = findByCodeLazy("useStateFromStores"); - function Svg(path: string, label: string) { return () => ( <svg diff --git a/src/utils/Queue.ts b/src/utils/Queue.ts index 86eb791..2680f56 100644 --- a/src/utils/Queue.ts +++ b/src/utils/Queue.ts @@ -27,7 +27,7 @@ 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. */ - constructor(public maxSize = Infinity) { } + constructor(public readonly maxSize = Infinity) { } private queue = [] as Array<() => Promisable<unknown>>; diff --git a/src/utils/index.ts b/src/utils/index.ts index b80bde3..cfded6b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -22,6 +22,7 @@ export * from "./debounce"; export * as Discord from "./discord"; export { default as IpcEvents } from "./IpcEvents"; export { default as Logger } from "./Logger"; +export * from "./margins"; export * from "./misc"; export * as Modals from "./modal"; export * from "./onceDefined"; diff --git a/src/utils/margins.ts b/src/utils/margins.ts new file mode 100644 index 0000000..5d7eed7 --- /dev/null +++ b/src/utils/margins.ts @@ -0,0 +1,35 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +let styleStr = ""; + +export const Margins: Record<`${"top" | "bottom" | "left" | "right"}${8 | 16 | 20}`, string> = {} as any; + +for (const dir of ["top", "bottom", "left", "right"] as const) { + for (const size of [8, 16, 20] as const) { + const cl = `vc-m-${dir}-${size}`; + Margins[`${dir}${size}`] = cl; + styleStr += `.${cl}{margin-${dir}:${size}px;}`; + } +} + +document.addEventListener("DOMContentLoaded", () => + document.head.append(Object.assign(document.createElement("style"), { + textContent: styleStr, + id: "vencord-margins" + })), { once: true }); diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx index c64d9e1..a41ab67 100644 --- a/src/utils/misc.tsx +++ b/src/utils/misc.tsx @@ -200,3 +200,7 @@ export const checkIntersecting = (el: Element) => { const documentHeight = Math.max(document.documentElement.clientHeight, window.innerHeight); return !(elementBox.bottom < 0 || elementBox.top - documentHeight >= 0); }; + +export function identity<T>(value: T): T { + return value; +} diff --git a/src/utils/settingsSync.ts b/src/utils/settingsSync.ts index 5cd81e7..18e1854 100644 --- a/src/utils/settingsSync.ts +++ b/src/utils/settingsSync.ts @@ -112,7 +112,6 @@ export async function uploadSettingsBackup(showToast = true): Promise<void> { if (file) { try { - console.log(file); await importSettings(new TextDecoder().decode(file.data)); if (showToast) toastSuccess(); } catch (err) { diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index be585c3..27d103f 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -49,5 +49,8 @@ export const Slider = waitForComponent<t.Slider>("Slider", filters.byCode("close export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]); export const ButtonWrapperClasses = findByPropsLazy("buttonWrapper", "buttonContent") as Record<string, string>; +/** + * @deprecated Use @utils/margins instead + */ export const Margins: t.Margins = findByPropsLazy("marginTop20"); export const ButtonLooks: t.ButtonLooks = findByPropsLazy("BLANK", "FILLED", "INVERTED"); diff --git a/src/webpack/common/internal.tsx b/src/webpack/common/internal.tsx index df768f7..e2f42d8 100644 --- a/src/webpack/common/internal.tsx +++ b/src/webpack/common/internal.tsx @@ -19,7 +19,7 @@ import { LazyComponent } from "@utils/misc"; // eslint-disable-next-line path-alias/no-relative -import { FilterFn, waitFor } from "../webpack"; +import { FilterFn, filters, waitFor } from "../webpack"; export function waitForComponent<T extends React.ComponentType<any> = React.ComponentType<any> & Record<string, any>>(name: string, filter: FilterFn | string | string[]): T { let myValue: T = function () { @@ -34,3 +34,7 @@ export function waitForComponent<T extends React.ComponentType<any> = React.Comp return lazyComponent; } + +export function waitForStore(name: string, cb: (v: any) => void) { + waitFor(filters.byStoreName(name), cb); +} diff --git a/src/webpack/common/react.ts b/src/webpack/common/react.ts index 455f39b..d73a3df 100644 --- a/src/webpack/common/react.ts +++ b/src/webpack/common/react.ts @@ -25,7 +25,7 @@ export let useEffect: typeof React.useEffect; export let useMemo: typeof React.useMemo; export let useRef: typeof React.useRef; -export const ReactDOM: typeof import("react-dom") = findByPropsLazy("createPortal", "render"); +export const ReactDOM: typeof import("react-dom") & typeof import("react-dom/client") = findByPropsLazy("createPortal", "render"); waitFor("useState", m => { React = m; diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts index bcd26b1..0bd9e87 100644 --- a/src/webpack/common/stores.ts +++ b/src/webpack/common/stores.ts @@ -19,36 +19,71 @@ import type * as Stores from "discord-types/stores"; // eslint-disable-next-line path-alias/no-relative -import { filters, findByPropsLazy, mapMangledModuleLazy, waitFor } from "../webpack"; +import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "../webpack"; +import { waitForStore } from "./internal"; +import * as t from "./types/stores"; -export const MessageStore = findByPropsLazy("getRawMessages") as Omit<Stores.MessageStore, "getMessages"> & { +export const Flux: t.Flux = findByPropsLazy("connectStores"); + +type GenericStore = t.FluxStore & Record<string, any>; + +export let MessageStore: Omit<Stores.MessageStore, "getMessages"> & { getMessages(chanId: string): any; }; -export const PermissionStore = findByPropsLazy("can", "getGuildPermissions"); + +// this is not actually a FluxStore export const PrivateChannelsStore = findByPropsLazy("openPrivateChannel"); -export const GuildChannelStore = findByPropsLazy("getChannels"); -export const ReadStateStore = findByPropsLazy("lastMessageId"); -export const PresenceStore = findByPropsLazy("setCurrentUserOnConnectionOpen"); - -export let GuildStore: Stores.GuildStore; -export let UserStore: Stores.UserStore; -export let SelectedChannelStore: Stores.SelectedChannelStore; -export let SelectedGuildStore: any; -export let ChannelStore: Stores.ChannelStore; -export let GuildMemberStore: Stores.GuildMemberStore; -export let RelationshipStore: Stores.RelationshipStore & { +export let PermissionStore: GenericStore; +export let GuildChannelStore: GenericStore; +export let ReadStateStore: GenericStore; +export let PresenceStore: GenericStore; + +export let GuildStore: Stores.GuildStore & t.FluxStore; +export let UserStore: Stores.UserStore & t.FluxStore; +export let SelectedChannelStore: Stores.SelectedChannelStore & t.FluxStore; +export let SelectedGuildStore: t.FluxStore & Record<string, any>; +export let ChannelStore: Stores.ChannelStore & t.FluxStore; +export let GuildMemberStore: Stores.GuildMemberStore & t.FluxStore; +export let RelationshipStore: Stores.RelationshipStore & t.FluxStore & { /** Get the date (as a string) that the relationship was created */ getSince(userId: string): string; }; +export let WindowStore: t.WindowStore; + export const MaskedLinkStore = mapMangledModuleLazy('"MaskedLinkStore"', { openUntrustedLink: filters.byCode(".apply(this,arguments)") }); -waitFor(["getCurrentUser", "initialize"], m => UserStore = m); -waitFor("getSortedPrivateChannels", m => ChannelStore = m); -waitFor("getCurrentlySelectedChannelId", m => SelectedChannelStore = m); -waitFor("getLastSelectedGuildId", m => SelectedGuildStore = m); -waitFor("getGuildCount", m => GuildStore = m); -waitFor(["getMember", "initialize"], m => GuildMemberStore = m); -waitFor("getRelationshipType", m => RelationshipStore = m); +/** + * React hook that returns stateful data for one or more stores + * You might need a custom comparator (4th argument) if your store data is an object + * + * @param stores The stores to listen to + * @param mapper A function that returns the data you need + * @param idk some thing, idk just pass null + * @param isEqual A custom comparator for the data returned by mapper + * + * @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id); + */ +export const useStateFromStores: <T>( + stores: t.FluxStore[], + mapper: () => T, + idk?: any, + isEqual?: (old: T, newer: T) => boolean +) => T + = findByCodeLazy("useStateFromStores"); + +waitForStore("UserStore", s => UserStore = s); +waitForStore("ChannelStore", m => ChannelStore = m); +waitForStore("SelectedChannelStore", m => SelectedChannelStore = m); +waitForStore("SelectedGuildStore", m => SelectedGuildStore = m); +waitForStore("GuildStore", m => GuildStore = m); +waitForStore("GuildMemberStore", m => GuildMemberStore = m); +waitForStore("RelationshipStore", m => RelationshipStore = m); +waitForStore("PermissionStore", m => PermissionStore = m); +waitForStore("PresenceStore", m => PresenceStore = m); +waitForStore("ReadStateStore", m => ReadStateStore = m); +waitForStore("GuildChannelStore", m => GuildChannelStore = m); +waitForStore("MessageStore", m => MessageStore = m); +waitForStore("WindowStore", m => WindowStore = m); diff --git a/src/webpack/common/types/components.d.ts b/src/webpack/common/types/components.d.ts index 3f76c22..9cd01de 100644 --- a/src/webpack/common/types/components.d.ts +++ b/src/webpack/common/types/components.d.ts @@ -215,9 +215,9 @@ export type Select = ComponentType<PropsWithChildren<{ closeOnSelect?: boolean; hideIcon?: boolean; - select?(value: any): void; - isSelected?(value: any): boolean; - serialize?(value: any): string; + select(value: any): void; + isSelected(value: any): boolean; + serialize(value: any): string; clear?(): void; maxVisibleItems?: number; diff --git a/src/webpack/common/types/index.d.ts b/src/webpack/common/types/index.d.ts index 9d6c295..af4b5e1 100644 --- a/src/webpack/common/types/index.d.ts +++ b/src/webpack/common/types/index.d.ts @@ -19,5 +19,6 @@ export * from "./components"; export * from "./fluxEvents"; export * from "./menu"; +export * from "./stores"; export * from "./utils"; diff --git a/src/webpack/common/types/stores.d.ts b/src/webpack/common/types/stores.d.ts new file mode 100644 index 0000000..6af5b27 --- /dev/null +++ b/src/webpack/common/types/stores.d.ts @@ -0,0 +1,40 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { FluxDispatcher, FluxEvents } from "./utils"; + +export class FluxStore { + constructor(dispatcher: FluxDispatcher, eventHandlers?: Partial<Record<FluxEvents, (data: any) => void>>); + + emitChange(): void; + getDispatchToken(): string; + getName(): string; + initialize(): void; + initializeIfNeeded(): void; + __getLocalVars(): Record<string, any>; +} + +export interface Flux { + Store: typeof FluxStore; +} + +export class WindowStore extends FluxStore { + isElementFullScreen(): boolean; + isFocused(): boolean; + windowSize(): Record<"width" | "height", number>; +} diff --git a/src/webpack/common/types/utils.d.ts b/src/webpack/common/types/utils.d.ts index 7222be4..0e2a6ca 100644 --- a/src/webpack/common/types/utils.d.ts +++ b/src/webpack/common/types/utils.d.ts @@ -31,20 +31,6 @@ export interface FluxDispatcher { unsubscribe(event: FluxEvents, callback: (data: any) => void): void; } -declare class FluxStore { - constructor(dispatcher: FluxDispatcher, eventHandlers?: Partial<Record<FluxEvents, (data: any) => void>>); - - emitChange(): void; - getDispatchToken(): string; - getName(): string; - initialize(): void; - initializeIfNeeded(): void; -} - -export interface Flux { - Store: typeof FluxStore; -} - export type Parser = Record< | "parse" | "parseTopic" diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index daac207..b53c340 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -23,7 +23,6 @@ import { _resolveReady,filters, findByCodeLazy, findByPropsLazy, mapMangledModul import type * as t from "./types/utils"; export let FluxDispatcher: t.FluxDispatcher; -export const Flux: t.Flux = findByPropsLazy("connectStores"); export const RestAPI: t.RestAPI = findByPropsLazy("getAPIBaseURL", "get"); export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYear"); diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index e537740..5aa7dc7 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -50,7 +50,7 @@ export const filters = { } return true; }, - byDisplayName: (name: string): FilterFn => m => + byStoreName: (name: string): FilterFn => m => m.constructor?.displayName === name }; @@ -331,15 +331,15 @@ export function findByCodeLazy(...code: string[]) { /** * Find a store by its displayName */ -export function findByDisplayName(name: string) { - return find(filters.byDisplayName(name)); +export function findStore(name: string) { + return find(filters.byStoreName(name)); } /** * findByDisplayName but lazy */ -export function findByDisplayNameLazy(name: string) { - return findLazy(filters.byDisplayName(name)); +export function findStoreLazy(name: string) { + return findLazy(filters.byStoreName(name)); } /** |