From 1d995e58f515dbeb908ba34bf70f829bfd3ccfac Mon Sep 17 00:00:00 2001 From: Ven Date: Fri, 10 Feb 2023 22:33:34 +0100 Subject: Notification API (#467) Co-authored-by: Ven Co-authored-by: afn Co-authored-by: afn --- src/api/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 +++- 6 files changed, 270 insertions(+), 1 deletion(-) 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 (limited to 'src/api') 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 . +*/ + +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 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 . +*/ + +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 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 . +*/ + +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 { -- cgit