From 1d995e58f515dbeb908ba34bf70f829bfd3ccfac Mon Sep 17 00:00:00 2001 From: Ven Date: Fri, 10 Feb 2023 22:33:34 +0100 Subject: [PATCH] Notification API (#467) Co-authored-by: Ven Co-authored-by: afn Co-authored-by: afn --- .eslintrc.json | 1 - .../Notifications/NotificationComponent.tsx | 92 +++++++++++ src/api/Notifications/Notifications.tsx | 92 +++++++++++ src/api/Notifications/index.ts | 19 +++ src/api/Notifications/styles.css | 49 ++++++ src/api/index.ts | 5 + src/api/settings.ts | 14 +- src/components/VencordSettings/VencordTab.tsx | 149 ++++++++++++------ src/modules.d.ts | 3 +- .../spotifyControls/PlayerComponent.tsx | 12 +- src/utils/Queue.ts | 2 +- src/utils/index.ts | 1 + src/utils/margins.ts | 35 ++++ src/utils/misc.tsx | 4 + src/utils/settingsSync.ts | 1 - src/webpack/common/components.ts | 3 + src/webpack/common/internal.tsx | 6 +- src/webpack/common/react.ts | 2 +- src/webpack/common/stores.ts | 77 ++++++--- src/webpack/common/types/components.d.ts | 6 +- src/webpack/common/types/index.d.ts | 1 + src/webpack/common/types/stores.d.ts | 40 +++++ src/webpack/common/types/utils.d.ts | 14 -- src/webpack/common/utils.ts | 1 - src/webpack/webpack.ts | 10 +- 25 files changed, 533 insertions(+), 106 deletions(-) create mode 100644 src/api/Notifications/NotificationComponent.tsx create mode 100644 src/api/Notifications/Notifications.tsx create mode 100644 src/api/Notifications/index.ts create mode 100644 src/api/Notifications/styles.css create mode 100644 src/utils/margins.ts create mode 100644 src/webpack/common/types/stores.d.ts diff --git a/.eslintrc.json b/.eslintrc.json index de17afa0f..aaaaaeb69 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -82,7 +82,6 @@ "no-constant-condition": ["error", { "checkLoops": false }], "no-duplicate-imports": "error", "no-extra-semi": "error", - "consistent-return": ["warn", { "treatUndefinedAsUnspecified": true }], "dot-notation": "error", "no-useless-escape": [ "error", diff --git a/src/api/Notifications/NotificationComponent.tsx b/src/api/Notifications/NotificationComponent.tsx new file mode 100644 index 000000000..65d4c433f --- /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 . +*/ + +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 ( + + ); +}); diff --git a/src/api/Notifications/Notifications.tsx b/src/api/Notifications/Notifications.tsx new file mode 100644 index 000000000..9c599aa65 --- /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 . +*/ + +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(resolve => { + root.render( + { + 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 000000000..cd14587b6 --- /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 . +*/ + +export * from "./Notifications"; diff --git a/src/api/Notifications/styles.css b/src/api/Notifications/styles.css new file mode 100644 index 000000000..84d8ff777 --- /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 0fef99cda..abb509348 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 d20e9642f..c7117918a 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 9f55d57f9..6ea1ca9d2 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 = { + [K in keyof Object]: Object[K] extends Type ? K : never; +}[keyof Object]; + function VencordSettings() { const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke(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; + 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 ( @@ -82,52 +123,70 @@ function VencordSettings() { - - + + Hint: You can change the position of this settings section in the settings of the "Settings" plugin! - 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.frameless = v} - note="Requires a full restart" - > - Disable the window frame - - settings.transparent = v} - note="Requires a full restart" - > - Enable window transparency - - {navigator.platform.toLowerCase().startsWith("win") && ( - settings.winCtrlQ = v} - note="Requires a full restart" - > - Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4) - - )} - - )} - + {Switches.map(s => s && ( + settings[s.key] = v} + note={s.note} + > + {s.title} + + ))} + + + Notification Style + + Some plugins may show you notifications. These come in two styles: +
    +
  • Vencord Notifications: These are in-app notifications
  • +
  • Desktop Notifications: Native Desktop notifications (like when you get a ping)
  • +
+
+ >} + select={v => notifSettings.position = v} + isSelected={v => v === notifSettings.position} + serialize={identity} + /> + + Notification Timeout + Set to 0s to never automatically time out + notifSettings.timeout = v} + onValueRender={v => (v / 1000).toFixed(2) + "s"} + onMarkerRender={v => (v / 1000) + "s"} + stickToMarkers={false} + />
); } diff --git a/src/modules.d.ts b/src/modules.d.ts index c1a1996e7..d75a84f74 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 f6ad08b83..439ecc249 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: ( - stores: typeof SpotifyStore[], - mapper: () => T, - idk?: null, - compare?: (old: T, newer: T) => boolean -) => T - = findByCodeLazy("useStateFromStores"); - function Svg(path: string, label: string) { return () => ( Promisable>; diff --git a/src/utils/index.ts b/src/utils/index.ts index b80bde3b2..cfded6b5d 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 000000000..5d7eed766 --- /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 . +*/ + +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 c64d9e1f6..a41ab6730 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(value: T): T { + return value; +} diff --git a/src/utils/settingsSync.ts b/src/utils/settingsSync.ts index 5cd81e7e4..18e185478 100644 --- a/src/utils/settingsSync.ts +++ b/src/utils/settingsSync.ts @@ -112,7 +112,6 @@ export async function uploadSettingsBackup(showToast = true): Promise { 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 be585c3a4..27d103fce 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -49,5 +49,8 @@ export const Slider = waitForComponent("Slider", filters.byCode("close export const Flex = waitForComponent("Flex", ["Justify", "Align", "Wrap"]); export const ButtonWrapperClasses = findByPropsLazy("buttonWrapper", "buttonContent") as Record; +/** + * @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 df768f763..e2f42d8c3 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 = React.ComponentType & Record>(name: string, filter: FilterFn | string | string[]): T { let myValue: T = function () { @@ -34,3 +34,7 @@ export function waitForComponent = 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 455f39bef..d73a3dfea 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 bcd26b1ef..0bd9e87fb 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 & { +export const Flux: t.Flux = findByPropsLazy("connectStores"); + +type GenericStore = t.FluxStore & Record; + +export let MessageStore: Omit & { getMessages(chanId: string): any; }; -export const PermissionStore = findByPropsLazy("can", "getGuildPermissions"); -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 & { +// this is not actually a FluxStore +export const PrivateChannelsStore = findByPropsLazy("openPrivateChannel"); +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; +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: ( + 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 3f76c22ed..9cd01de2f 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. +*/ + +import { FluxDispatcher, FluxEvents } from "./utils"; + +export class FluxStore { + constructor(dispatcher: FluxDispatcher, eventHandlers?: Partial void>>); + + emitChange(): void; + getDispatchToken(): string; + getName(): string; + initialize(): void; + initializeIfNeeded(): void; + __getLocalVars(): Record; +} + +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 7222be458..0e2a6ca84 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 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 daac207d2..b53c34082 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 e537740f9..5aa7dc724 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)); } /**