aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/Notifications/NotificationComponent.tsx92
-rw-r--r--src/api/Notifications/Notifications.tsx92
-rw-r--r--src/api/Notifications/index.ts19
-rw-r--r--src/api/Notifications/styles.css49
-rw-r--r--src/api/index.ts5
-rw-r--r--src/api/settings.ts14
-rw-r--r--src/components/VencordSettings/VencordTab.tsx149
-rw-r--r--src/modules.d.ts3
-rw-r--r--src/plugins/spotifyControls/PlayerComponent.tsx12
-rw-r--r--src/utils/Queue.ts2
-rw-r--r--src/utils/index.ts1
-rw-r--r--src/utils/margins.ts35
-rw-r--r--src/utils/misc.tsx4
-rw-r--r--src/utils/settingsSync.ts1
-rw-r--r--src/webpack/common/components.ts3
-rw-r--r--src/webpack/common/internal.tsx6
-rw-r--r--src/webpack/common/react.ts2
-rw-r--r--src/webpack/common/stores.ts77
-rw-r--r--src/webpack/common/types/components.d.ts6
-rw-r--r--src/webpack/common/types/index.d.ts1
-rw-r--r--src/webpack/common/types/stores.d.ts40
-rw-r--r--src/webpack/common/types/utils.d.ts14
-rw-r--r--src/webpack/common/utils.ts1
-rw-r--r--src/webpack/webpack.ts10
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));
}
/**