Notification API (#467)
Co-authored-by: Ven <vendicated@riseup.net> Co-authored-by: afn <hey@afn.lol> Co-authored-by: afn <afnzmn@gmail.com>main
parent
6114bc6b16
commit
1d995e58f5
@ -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>
|
||||||
|
);
|
||||||
|
});
|
@ -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++));
|
||||||
|
}
|
||||||
|
}
|
@ -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";
|
@ -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%;
|
||||||
|
}
|
@ -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 });
|
@ -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>;
|
||||||
|
}
|
Loading…
Reference in new issue