Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugin): ToastNotifications #1806

Draft
wants to merge 43 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
36331ff
Initial plugin version
ImSkully Oct 15, 2023
a7e2053
update constants.ts Devs
ImSkully Oct 15, 2023
f4ca72a
general improvements
ImSkully Nov 4, 2023
1823e0c
Merge pull request #1 from ImSkully/main
ImSkully Nov 4, 2023
b4b291e
Merge branch 'toastnotifications' of https://github.com/ImSkully/Venc…
ImSkully Nov 4, 2023
d874982
Merge branch 'toastnotifications' into main
ImSkully Nov 9, 2023
9b282bc
Merge pull request #2 from ImSkully/main
ImSkully Nov 9, 2023
4b12cd0
addMention ensure guildId
ImSkully Nov 12, 2023
d772db8
Merge pull request #3 from ImSkully/toastnotifications
ImSkully Nov 12, 2023
1faef7e
Merge branch 'Vendicated:main' into main
ImSkully Nov 24, 2023
fac528e
Merge pull request #5 from ImSkully/main
ImSkully Nov 24, 2023
4675772
Merge branch 'toastnotifications' into main
ImSkully Mar 7, 2024
3343ff6
Merge pull request #7 from ImSkully/main
ImSkully Mar 7, 2024
4b05468
Merge branch 'Vendicated:main' into main
ImSkully Mar 28, 2024
51ff364
Merge pull request #8 from ImSkully/main
ImSkully Mar 28, 2024
b705056
Merge branch 'main' of https://github.com/ImSkully/Vencord
ImSkully Mar 28, 2024
ad9c7e7
Merge branch 'Vendicated:main' into main
ImSkully Mar 30, 2024
4f00696
Merge pull request #9 from ImSkully/main
ImSkully Mar 30, 2024
e584617
Merge branch 'Vendicated:main' into main
ImSkully Apr 17, 2024
0f61394
Merge pull request #10 from ImSkully/main
ImSkully Apr 17, 2024
30e14d0
Merge branch 'Vendicated:main' into main
ImSkully Apr 25, 2024
2e8819e
Merge pull request #11 from ImSkully/main
ImSkully Apr 25, 2024
2d12b68
Merge branch 'Vendicated:main' into main
ImSkully May 6, 2024
708e805
Merge pull request #12 from ImSkully/main
ImSkully May 6, 2024
a648ded
Merge branch 'main' of github.com:Vendicated/Vencord into toastnotifi…
ImSkully May 19, 2024
74b1332
Merge branch 'main' of github.com:Vendicated/Vencord into toastnotifi…
ImSkully Jun 19, 2024
0952f43
Merge branch 'Vendicated:main' into toastnotifications
ImSkully Jun 29, 2024
9ec7942
Initial plugin version
ImSkully Oct 15, 2023
87e92dc
update constants.ts Devs
ImSkully Oct 15, 2023
c3417e4
general improvements
ImSkully Nov 4, 2023
1683038
addMention ensure guildId
ImSkully Nov 12, 2023
dca2608
Merge branch 'toastnotifications' of https://github.com/ImSkully/Venc…
ImSkully Jul 22, 2024
9c836d4
Merge branch 'main' of github.com:Vendicated/Vencord into Vendicated-…
ImSkully Aug 23, 2024
e33666e
Merge branch 'Vendicated-main' into toastnotifications
ImSkully Aug 23, 2024
44c0d07
Merge branch 'main' of github.com:Vendicated/Vencord into toastnotifi…
ImSkully Sep 5, 2024
4464f1d
Merge branch 'main' of github.com:Vendicated/Vencord into toastnotifi…
ImSkully Oct 3, 2024
b9ffcfd
Merge branch 'main' of github.com:Vendicated/Vencord into toastnotifi…
ImSkully Nov 11, 2024
040e817
Merge branch 'main' of github.com:Vendicated/Vencord into toastnotifi…
ImSkully Dec 17, 2024
5fe6f71
Merge branch 'Vendicated:main' into toastnotifications
ImSkully Jan 21, 2025
d7cf022
Merge branch 'Vendicated:main' into toastnotifications
ImSkully Jan 29, 2025
62f5401
Merge branch 'Vendicated:main' into toastnotifications
ImSkully Jan 29, 2025
8618d71
Merge branch 'Vendicated:main' into toastnotifications
ImSkully Feb 5, 2025
27d34e7
Merge branch 'Vendicated:main' into toastnotifications
ImSkully Feb 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Initial plugin version
  • Loading branch information
ImSkully committed Jul 22, 2024
commit 9ec79423f5492be51b575c966f8fcce4fb8e9dfe
133 changes: 133 additions & 0 deletions src/plugins/ToastNotifications/components/NotificationComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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 ErrorBoundary from "@components/ErrorBoundary";
import { classes } from "@utils/misc";
import { React, useEffect, useMemo, useState } from "@webpack/common";

import { NotificationData } from "./Notifications";
import { settings as PluginSettings } from "../index";

export default ErrorBoundary.wrap(function NotificationComponent({
title,
body,
richBody,
icon,
image,
permanent,
dismissOnClick,
index,
onClick,
onClose
}: NotificationData & { index?: number; }) {
const [isHover, setIsHover] = useState(false);
const [elapsed, setElapsed] = useState(0);

// Precompute appearance settings.
const AppearanceSettings = {
position: `toastnotifications-position-${PluginSettings.store.position || "bottom-left"}`,
timeout: (PluginSettings.store.timeout * 1000) || 5000,
opacity: PluginSettings.store.opacity / 100,
};

const start = useMemo(() => Date.now(), [isHover]); // Reset the timer when the user hovers over the notification.

// Precompute the position style.
const positionStyle = useMemo(() => {
if (index === undefined) return {};
const isTopPosition = AppearanceSettings.position.includes("top");
const actualHeight = 115; // Update this with the actual height including margin
const effectiveIndex = index % PluginSettings.store.maxNotifications;
const offset = 10 + (effectiveIndex * actualHeight); // 10 is the base offset

return isTopPosition ? { top: `${offset}px` } : { bottom: `${offset}px` };
}, [index, AppearanceSettings.position]);

// Handle notification timeout.
useEffect(() => {
if (isHover || permanent) return void setElapsed(0);

const intervalId = setInterval(() => {
const elapsed = Date.now() - start;
if (elapsed >= AppearanceSettings.timeout)
onClose!();
else
setElapsed(elapsed);
}, 10);

return () => clearInterval(intervalId);
}, [isHover]);

const timeoutProgress = elapsed / AppearanceSettings.timeout;

// Render the notification.
return (
<button
style={positionStyle}
className={classes("toastnotifications-notification-root", AppearanceSettings.position)}
onClick={() => {
onClick?.();
if (dismissOnClick !== false)
onClose!();
}}
onContextMenu={e => {
e.preventDefault();
e.stopPropagation();
onClose!();
}}
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
>
<div className="toastnotifications-notification">
{icon && <img className="toastnotifications-notification-icon" src={icon} alt="User Avatar" />}
<div className="toastnotifications-notification-content">
<div className="toastnotifications-notification-header">
<h2 className="toastnotifications-notification-title">{title}</h2>
<button
className="toastnotifications-notification-close-btn"
onClick={e => {
e.preventDefault();
e.stopPropagation();
onClose!();
}}
>
<svg width="24" height="24" viewBox="0 0 24 24" role="img" aria-labelledby="toastnotifications-notification-dismiss-title">
<title id="toastnotifications-notification-dismiss-title">Dismiss Notification</title>
<path fill="currentColor" d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z" />
</svg>
</button>
</div>
<div>
{richBody ?? <p className="toastnotifications-notification-p">{body}</p>}
</div>
</div>
</div>
{image && <img className="toastnotifications-notification-img" src={image} alt="ToastNotification Image" />}
{AppearanceSettings.timeout !== 0 && !permanent && (
<div
className="toastnotifications-notification-progressbar"
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: "var(--brand-experiment)" }}
/>
)}
</button>
);
}, {
onError: ({ props }) => props.onClose!()
});
106 changes: 106 additions & 0 deletions src/plugins/ToastNotifications/components/Notifications.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* 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 { ReactDOM, React } from "@webpack/common";
import type { ReactNode } from "react";
import type { Root } from "react-dom/client";

import { settings as PluginSettings } from "../index";
import NotificationComponent from "./NotificationComponent";

let NotificationQueue: JSX.Element[] = [];
let notificationID = 0;
let RootContainer: Root;

/**
* getNotificationContainer()
* Gets the root container for the notifications, creating it if it doesn't exist.
* @returns {Root} The root DOM container.
*/
function getNotificationContainer() {
if (!RootContainer) {
const container = document.createElement("div");
container.id = "toastnotifications-container";
document.body.append(container);
RootContainer = ReactDOM.createRoot(container);
}

return RootContainer;
}

export interface NotificationData {
title: string; // Title to display in the notification.
body: string; // Notification body text.
richBody?: ReactNode; // Same as body, though a rich ReactNode to be rendered within the notification.
icon?: string; // Avatar image of the message author or source.
image?: string; // Large image to display in the notification for attachments.
permanent?: boolean; // Whether or not the notification should be permanent or timeout.
dismissOnClick?: boolean; // Whether or not the notification should be dismissed when clicked.
onClick?(): void;
onClose?(): void;
}

export async function showNotification(notification: NotificationData) {
const root = getNotificationContainer();
const thisNotificationID = notificationID++;

return new Promise<void>(resolve => {
const ToastNotification = (
<NotificationComponent
key={thisNotificationID.toString()}
index={NotificationQueue.length}
{...notification}
onClose={() => {
// Remove this notification from the queue.
NotificationQueue = NotificationQueue.filter((n) => n.key !== thisNotificationID.toString());
notification.onClose?.(); // Trigger the onClose callback if it exists.
console.log(`[DEBUG] [ToastNotifications] Removed #${thisNotificationID} from queue.`);

// Re-render remaining notifications with new reversed indices.
root.render(
<>
{NotificationQueue.map((notification, index) => {
const reversedIndex = (NotificationQueue.length - 1) - index;
return React.cloneElement(notification, { index: reversedIndex });
})}
</>
);

resolve();
}}
/>
);

// Add this notification to the queue.
NotificationQueue.push(ToastNotification);
console.log(`[DEBUG] [ToastNotifications] Added #${thisNotificationID} to queue.`);

// Limit the number of notifications to the configured maximum.
if (NotificationQueue.length > PluginSettings.store.maxNotifications) NotificationQueue.shift();

// Render the notifications.
root.render(
<>
{NotificationQueue.map((notification, index) => {
const reversedIndex = (NotificationQueue.length - 1) - index;
return React.cloneElement(notification, { index: reversedIndex });
})}
</>
);
});
}
135 changes: 135 additions & 0 deletions src/plugins/ToastNotifications/components/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
:root {
/* Body */
--toastnotifications-background-color: var(--background-secondary-alt);
--toastnotifications-text-color: var(--text-normal);
--toastnotifications-border-radius: 6px;
--toastnotifications-width: 25vw;
--toastnotifications-padding: 1.25rem;

/* Title */
--toastnotifications-title-color: var(--header-primary);
--toastnotifications-title-font-size: 1rem;
--toastnotifications-title-font-weight: 600;
--toastnotifications-title-line-height: 1.25rem;

/* Close Button */
--toastnotifications-close-button-color: var(--interactive-normal);
--toastnotifications-close-button-hover-color: var(--interactive-hover);
--toastnotifications-close-button-opacity: 0.5;
--toastnotifications-close-button-hover-opacity: 1;

/* Message Author Image */
--toastnotifications-image-height: 4rem;
--toastnotifications-image-width: var(--toastnotifications-image-height);
--toastnotifications-image-border-radius: 6px;

/* Progress Bar */
--toastnotifications-progressbar-height: 0.25rem;

/* Position Offset - Global inherited offset by all positions */
--toastnotifications-position-offset: 1rem;
}

.toastnotifications-notification-root {
all: unset;
display: flex;
flex-direction: column;
color: var(--toastnotifications-text-color);
background-color: var(--toastnotifications-background-color);
border-radius: var(--toastnotifications-border-radius);
overflow: hidden;
cursor: pointer;
position: absolute;
z-index: 2147483647;
right: 1rem;
width: var(--toastnotifications-width);
min-height: 10vh;
bottom: calc(1rem + var(--notification-index) * 12vh);
}

.toastnotifications-notification {
display: flex;
flex-direction: row;
padding: var(--toastnotifications-padding);
gap: 1.25rem;
}

.toastnotifications-notification-content {
width: 100%;
}

.toastnotifications-notification-header {
display: flex;
justify-content: space-between;
}

.toastnotifications-notification-title {
color: var(--toastnotifications-title-color);
font-size: var(--toastnotifications-title-font-size);
font-weight: var(--toastnotifications-title-font-weight);
line-height: var(--toastnotifications-title-line-height);
}

.toastnotifications-notification-close-btn {
all: unset;
cursor: pointer;
color: var(--toastnotifications-close-button-color);
opacity: var(--toastnotifications-close-button-opacity);
transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
}

.toastnotifications-notification-close-btn:hover {
color: var(--toastnotifications-close-button-hover-color);
opacity: var(--toastnotifications-close-button-hover-opacity);
}

.toastnotifications-notification-icon {
height: var(--toastnotifications-image-height);
width: var(--toastnotifications-image-width);
border-radius: var(--toastnotifications-image-border-radius);
}

.toastnotifications-notification-progressbar {
height: var(--toastnotifications-progressbar-height);
border-radius: 5px;
margin-top: auto;
}

.toastnotifications-notification-p {
margin: 0.5rem 0 0;
line-height: 140%;
}

.toastnotifications-notification-img {
width: 100%;
}

/* Notification Positioning CSS */
.toastnotifications-position-bottom-left {
bottom: var(--toastnotifications-position-offset);
left: var(--toastnotifications-position-offset);
}

.toastnotifications-position-top-left {
top: var(--toastnotifications-position-offset);
left: var(--toastnotifications-position-offset);
}

.toastnotifications-position-top-right {
top: var(--toastnotifications-position-offset);
right: var(--toastnotifications-position-offset);
}

.toastnotifications-position-bottom-right {
bottom: var(--toastnotifications-position-offset);
right: var(--toastnotifications-position-offset);
}

/* Rich Body classes */
.toastnotifications-mention-class {
color: var(--mention-foreground);
background: var(--mention-background);
unicode-bidi: -moz-plaintext;
unicode-bidi: plaintext;
font-weight: 500;
}
Loading