From 77f15e3f7d4250ad0b7f611b918296692107556b Mon Sep 17 00:00:00 2001 From: Tharki-God Date: Tue, 9 Apr 2024 07:54:05 +0530 Subject: [PATCH 1/9] uwu --- src/renderer/apis/notification.ts | 95 +++++++++ .../coremods/notification/icons/Close.tsx | 9 + .../coremods/notification/icons/Danger.tsx | 9 + .../coremods/notification/icons/Info.tsx | 9 + .../coremods/notification/icons/Success.tsx | 9 + .../coremods/notification/icons/Warning.tsx | 9 + .../coremods/notification/icons/index.ts | 13 ++ src/renderer/coremods/notification/index.tsx | 5 + .../coremods/notification/notification.css | 187 ++++++++++++++++++ .../coremods/notification/notification.tsx | 160 +++++++++++++++ .../coremods/notification/plaintextPatches.ts | 14 ++ src/renderer/modules/common/components.ts | 3 +- .../modules/components/ButtonItem.tsx | 2 +- src/renderer/modules/components/Progress.tsx | 14 ++ src/renderer/modules/components/index.ts | 5 + src/renderer/replugged.ts | 3 + 16 files changed, 544 insertions(+), 2 deletions(-) create mode 100644 src/renderer/apis/notification.ts create mode 100644 src/renderer/coremods/notification/icons/Close.tsx create mode 100644 src/renderer/coremods/notification/icons/Danger.tsx create mode 100644 src/renderer/coremods/notification/icons/Info.tsx create mode 100644 src/renderer/coremods/notification/icons/Success.tsx create mode 100644 src/renderer/coremods/notification/icons/Warning.tsx create mode 100644 src/renderer/coremods/notification/icons/index.ts create mode 100644 src/renderer/coremods/notification/index.tsx create mode 100644 src/renderer/coremods/notification/notification.css create mode 100644 src/renderer/coremods/notification/notification.tsx create mode 100644 src/renderer/coremods/notification/plaintextPatches.ts create mode 100644 src/renderer/modules/components/Progress.tsx diff --git a/src/renderer/apis/notification.ts b/src/renderer/apis/notification.ts new file mode 100644 index 000000000..8985a0508 --- /dev/null +++ b/src/renderer/apis/notification.ts @@ -0,0 +1,95 @@ +import { ButtonItemProps } from "@components/ButtonItem"; +export interface NotificationProps { + id?: string; + timeout: number; + origin?: string; + name?: string; + color?: string; + iconColor?: string; + imageClassName?: string | undefined; + header: React.ReactNode; + content: React.ReactNode; + image: string; + icon?: React.FunctionComponent> | false; + buttons?: ButtonItemProps[]; + className?: string; + style?: React.CSSProperties; + hideProgressBar?: boolean; + type?: string; +} +export interface NotificationPropsWithId extends NotificationProps { + id: string; + origin: string; + name: string; + color: string; +} +class RPNotificationHandler extends EventTarget { + private notifications = new Map(); + + public sendNotification(notification: NotificationPropsWithId): () => void { + this.notifications.set(notification.id, notification); + this.dispatchEvent(new CustomEvent("rpNotificationUpdate")); + return () => { + this.notifications.delete(notification.id); + this.dispatchEvent(new CustomEvent("rpNotificationUpdate")); + }; + } + + public getNotifications(): NotificationPropsWithId[] { + return Array.from(this.notifications.values()); + } + + public closeNotification(id: string): void { + this.notifications.delete(id); + this.dispatchEvent(new CustomEvent("rpNotificationUpdate")); + } +} +export const NotificationHandler = new RPNotificationHandler(); + +export class NotificationAPI { + public origin: string; + public name: string; + public color: string; + private notifications: Array<() => void> = []; + + public constructor(origin: string, name: string, color?: string) { + this.origin = origin; + this.name = name; + this.color = color ?? "#5864f2"; + } + + public notify(notification: NotificationProps): () => void { + notification.name = this.name; + notification.origin = this.origin; + notification.type ??= "info"; + notification.color ??= this.color; + notification.id = `${this.origin}-${this.name}-${notification.header}-${ + notification.type + } -${Date.now()}`; + const dismiss = NotificationHandler.sendNotification(notification as NotificationPropsWithId); + this.notifications.push(dismiss); + return () => { + dismiss(); + this.notifications = this.notifications.filter((f) => f !== dismiss); + }; + } + + public dismissAll(): void { + for (const dismiss of this.notifications) { + dismiss(); + } + this.notifications = []; + } + + public static api(name: string, color?: string): NotificationAPI { + return new NotificationAPI("API", name, color); + } + + public static coremod(name: string, color?: string): NotificationAPI { + return new NotificationAPI("Coremod", name, color); + } + + public static plugin(name: string, color?: string): NotificationAPI { + return new NotificationAPI("Plugin", name, color); + } +} diff --git a/src/renderer/coremods/notification/icons/Close.tsx b/src/renderer/coremods/notification/icons/Close.tsx new file mode 100644 index 000000000..53c4f8b1d --- /dev/null +++ b/src/renderer/coremods/notification/icons/Close.tsx @@ -0,0 +1,9 @@ +import { React } from "@common"; +export default React.memo((props: React.SVGProps) => ( + + + +)); diff --git a/src/renderer/coremods/notification/icons/Danger.tsx b/src/renderer/coremods/notification/icons/Danger.tsx new file mode 100644 index 000000000..b27dacf97 --- /dev/null +++ b/src/renderer/coremods/notification/icons/Danger.tsx @@ -0,0 +1,9 @@ +import { React } from "@common"; +export default React.memo((props: React.SVGProps) => ( + + + +)); diff --git a/src/renderer/coremods/notification/icons/Info.tsx b/src/renderer/coremods/notification/icons/Info.tsx new file mode 100644 index 000000000..8669c58ef --- /dev/null +++ b/src/renderer/coremods/notification/icons/Info.tsx @@ -0,0 +1,9 @@ +import { React } from "@common"; +export default React.memo((props: React.SVGProps) => ( + + + +)); diff --git a/src/renderer/coremods/notification/icons/Success.tsx b/src/renderer/coremods/notification/icons/Success.tsx new file mode 100644 index 000000000..7a2bd09a1 --- /dev/null +++ b/src/renderer/coremods/notification/icons/Success.tsx @@ -0,0 +1,9 @@ +import { React } from "@common"; +export default React.memo((props: React.SVGProps) => ( + + + +)); diff --git a/src/renderer/coremods/notification/icons/Warning.tsx b/src/renderer/coremods/notification/icons/Warning.tsx new file mode 100644 index 000000000..59754d496 --- /dev/null +++ b/src/renderer/coremods/notification/icons/Warning.tsx @@ -0,0 +1,9 @@ +import { React } from "@common"; +export default React.memo((props: React.SVGProps) => ( + + + +)); diff --git a/src/renderer/coremods/notification/icons/index.ts b/src/renderer/coremods/notification/icons/index.ts new file mode 100644 index 000000000..cb8d92b68 --- /dev/null +++ b/src/renderer/coremods/notification/icons/index.ts @@ -0,0 +1,13 @@ +import Close from "./Close"; +import Danger from "./Danger"; +import Info from "./Info"; +import Success from "./Success"; +import Warning from "./Warning"; + +export default { + Close, + Danger, + Info, + Success, + Warning, +}; diff --git a/src/renderer/coremods/notification/index.tsx b/src/renderer/coremods/notification/index.tsx new file mode 100644 index 000000000..4c5542f6a --- /dev/null +++ b/src/renderer/coremods/notification/index.tsx @@ -0,0 +1,5 @@ +import NotificationContainer from "./notification"; + +export function _renderNotification(): React.ReactElement { + return ; +} diff --git a/src/renderer/coremods/notification/notification.css b/src/renderer/coremods/notification/notification.css new file mode 100644 index 000000000..b4f373e60 --- /dev/null +++ b/src/renderer/coremods/notification/notification.css @@ -0,0 +1,187 @@ +/*======== CSS Variables ========*/ +.theme-dark { + --replugged-notification-box-shadow: rgba(0, 0, 0, 0.2); + --replugged-notification-border: rgba(28, 36, 43, 0.6); +} +.theme-light { + --replugged-notification-box-shadow: rgba(255, 255, 255, 0.1); + --replugged-notification-border: rgba(199, 199, 199, 0.06); +} +/*======== Toast Styling ========*/ + +.replugged-notification-container { + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: flex-end; + position: fixed; + bottom: 25px; + right: 25px; + z-index: 999; +} +.replugged-notification { + display: flex; + flex-direction: column; + margin-bottom: 10px; + background-color: var(--background-secondary); + border: 1px solid var(--replugged-notification-border); + box-shadow: 0 8px 16px 0 var(--replugged-notification-box-shadow); + border-radius: 8px; + max-width: 600px; + min-width: 223px; + width: 320px; + position: relative; + animation: + slide-in 0.5s ease, + shake 1.4s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; +} + +.replugged-notification .header { + display: flex; + color: var(--header-primary); + font-weight: 600; + font-size: 24px; + line-height: 1.2; + background-color: var(--background-primary); + border-radius: 8px 8px 0 0; + padding: 12px 20px; + justify-content: center; + align-items: center; /* Center items vertically */ +} + +.replugged-notification .header span:has(.icon) { + display: flex; + align-items: center; + justify-content: center; + margin-right: 8px; + margin-top: 3px; +} + +.replugged-notification .header .icon { + display: flex; + align-items: center; + justify-content: center; +} + +.replugged-notification .header .icon img { + width: 18px; + height: 18px; +} + +.replugged-notification .header .dismiss { + width: 16px; + height: 16px; + opacity: 0.5; + transition: opacity 0.2s; + margin-left: auto; + cursor: pointer; + font-size: 12px; + margin-bottom: 5px; +} + +.replugged-notification .contents { + display: flex; + border-radius: 0 0 8px 8px; + text-align: center; + justify-content: flex-end; + flex-direction: column; + padding: 10px; +} + +.replugged-notification .contents .inner { + color: var(--text-normal); + font-size: 14px; + line-height: 1.4; + background-color: var(--background-tertiary); + border-bottom: 1px solid solid var(--replugged-notification-border); + border-radius: 8px; + padding: 10px; + margin-bottom: 6px; +} + +.replugged-notification .buttons { + display: flex; + flex-wrap: wrap; + width: 95%; + padding: 10px; +} + +.replugged-notification .buttons button { + box-sizing: border-box; + min-width: calc(50% - 10px); + padding: 8px; + margin: 8px 4px; + flex: 1; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.replugged-notification .buttons button[class*="lookGhost"] { + opacity: 0.8; + transition: + background-color 0.17s ease, + color 0.17s ease, + opacity 0.17s ease, + transform 0.17s ease; +} + +.replugged-notification .buttons button[class*="lookGhost"]:hover { + opacity: 1; +} + +.replugged-notification.leaving { + animation: slide-out 0.7s ease-out; +} + +/*========= Header Types =========*/ +.replugged-notification.info .icon { + color: var(--blurple); +} + +.replugged-notification.warning .icon { + color: var(--info-warning-foreground); +} + +.replugged-notification.danger .icon { + color: var(--info-danger-foreground); +} + +.replugged-notification.success .icon { + color: var(--info-positive-foreground); +} + +/*========== Animations ==========*/ +@keyframes shake { + 10%, + 90% { + transform: translate3d(1px, 0, 0); + } + 20%, + 80% { + transform: translate3d(2px, 0, 0); + } + 30%, + 50%, + 70% { + transform: translate3d(-4px, 0, 0); + } + 40%, + 60% { + transform: translate3d(4px, 0, 0); + } +} + +@keyframes slide-in { + from { + margin-right: -500px; + opacity: 0; + } +} + +@keyframes slide-out { + to { + margin-right: -500px; + opacity: 0; + } +} diff --git a/src/renderer/coremods/notification/notification.tsx b/src/renderer/coremods/notification/notification.tsx new file mode 100644 index 000000000..1738abd69 --- /dev/null +++ b/src/renderer/coremods/notification/notification.tsx @@ -0,0 +1,160 @@ +import { React } from "@common"; +import { notification } from "@replugged"; +import { Button, Clickable, Progress, Tooltip } from "@components"; +import Icons from "./icons"; +import "./notification.css"; +import type { NotificationPropsWithId } from "../../apis/notification"; +const predefinedIcons: Record< + string, + React.MemoExoticComponent<(props: React.SVGProps) => React.ReactElement> +> = { + danger: Icons.Danger, + info: Icons.Info, + success: Icons.Success, + warning: Icons.Warning, +}; + +function NotificationGradient(hex: string): string[] { + const hexWithoutHash = hex.replace(/^#/, ""); + const num = parseInt(hexWithoutHash, 16); + const r = (num >> 16) & 0xff; + const g = (num >> 8) & 0xff; + const b = num & 0xff; + const luminance = + 0.2126 * (r / 255) ** 2.2 + 0.7152 * (g / 255) ** 2.2 + 0.0722 * (b / 255) ** 2.2; + const lightenDarken = luminance > 0.5 ? -175 : 175; + const newR = Math.min(Math.max(0, r + lightenDarken), 255); + const newG = Math.min(Math.max(0, g + lightenDarken), 255); + const newB = Math.min(Math.max(0, b + lightenDarken), 255); + const newHex = `#${((newR << 16) | (newG << 8) | newB).toString(16).padStart(6, "0")}`; + return luminance > 0.5 ? [newHex, hex] : [hex, newHex]; +} + +function Notification(props: NotificationPropsWithId): React.ReactElement | null { + const [leaving, setLeaving] = React.useState(false); + const [timeoutState, setTimeoutState] = React.useState(); + const [progress, setProgress] = React.useState(100); + const [progressState, setProgressState] = React.useState(); + const [timeLeft, setTimeLeft] = React.useState(props.timeout); + const Icon = props.icon ?? (props.type ? predefinedIcons[props.type] : predefinedIcons.info); + React.useEffect(() => { + if (!isNaN(props.timeout)) { + const timeout = setTimeout(() => { + setLeaving(true); + notification.NotificationHandler.closeNotification(props.id); + }, props.timeout); + setTimeoutState(timeout); + setProgressState( + setInterval(() => { + setTimeLeft((prev) => prev - 1000); + }, 1e3), + ); + } + return () => { + clearTimeout(timeoutState); + clearInterval(progressState); + }; + }, []); + React.useEffect(() => { + setProgress((timeLeft / props.timeout) * 100); + }, [timeLeft]); + + return ( +
+ {props.header && ( +
+ {props.icon !== false && ( + + `${text.charAt(0).toUpperCase()}${text.substring(1).toLowerCase()}`, + ) + : "Info" + }`}> +
+ {props.image ? ( + + ) : ( + Icon && + )} +
+
+ )} + {props.header} + { + setLeaving(true); + notification.NotificationHandler.closeNotification(props.id); + }}> + + +
+ )} + {props.content && ( +
+
{props.content}
+
+ )} + {props.buttons && Array.isArray(props.buttons) && ( +
+ {props.buttons.map(({ onClick, ...buttonProps }, index: number) => { + return ( +
+ )} + {timeoutState && !props.hideProgressBar && ( + + )} +
+ ); +} + +export default function notificationContainer(): React.ReactElement | null { + const [toasts, setToasts] = React.useState([]); + + const toastsUpdate = (): void => setToasts(notification.NotificationHandler.getNotifications()); + + React.useEffect(() => { + notification.NotificationHandler.addEventListener("rpNotificationUpdate", toastsUpdate); + toastsUpdate(); + + return () => { + notification.NotificationHandler.removeEventListener("rpNotificationUpdate", toastsUpdate); + }; + }, []); + + return ( +
+ {Boolean(toasts.length) && toasts.map((props) => )} +
+ ); +} diff --git a/src/renderer/coremods/notification/plaintextPatches.ts b/src/renderer/coremods/notification/plaintextPatches.ts new file mode 100644 index 000000000..80b125e40 --- /dev/null +++ b/src/renderer/coremods/notification/plaintextPatches.ts @@ -0,0 +1,14 @@ +import type { PlaintextPatch } from "src/types"; + +export default [ + { + find: "Shakeable is shaken when not mounted", + replacements: [ + { + match: /(\.DnDKeyboardHelpBar.{20,40})]/, + replace: (_, prefix) => + `${prefix},replugged.coremods?.coremods?.notification?._renderNotification?.()]`, + }, + ], + }, +] as PlaintextPatch[]; diff --git a/src/renderer/modules/common/components.ts b/src/renderer/modules/common/components.ts index 6e0bf66e2..f4f15aa64 100644 --- a/src/renderer/modules/common/components.ts +++ b/src/renderer/modules/common/components.ts @@ -1,4 +1,4 @@ -import type { LoaderType } from "@components"; +import type { LoaderType, ProgressType } from "@components"; import type { ClickableCompType } from "@components/Clickable"; import type { OriginalTextType } from "@components/Text"; import type { ButtonType } from "../components/ButtonItem"; @@ -47,6 +47,7 @@ interface DiscordComponents { Select: SelectCompType; showToast: ShowToast; Slider: SliderCompType; + Progress: ProgressType; Spinner: LoaderType; Switch: SwitchType; Text: OriginalTextType; diff --git a/src/renderer/modules/components/ButtonItem.tsx b/src/renderer/modules/components/ButtonItem.tsx index e72ade372..3f22ae61b 100644 --- a/src/renderer/modules/components/ButtonItem.tsx +++ b/src/renderer/modules/components/ButtonItem.tsx @@ -101,7 +101,7 @@ const classes = "dividerDefault", ); -interface ButtonItemProps { +export interface ButtonItemProps { onClick?: React.MouseEventHandler; button?: string; note?: string; diff --git a/src/renderer/modules/components/Progress.tsx b/src/renderer/modules/components/Progress.tsx new file mode 100644 index 000000000..3ad1ca964 --- /dev/null +++ b/src/renderer/modules/components/Progress.tsx @@ -0,0 +1,14 @@ +import type React from "react"; +import components from "../common/components"; + +interface ProgressProps { + animate?: boolean; + className?: string; + itemClassName?: string; + style?: React.CSSProperties; + percent: number; + foregroundGradientColor?: string[]; +} + +export type ProgressType = React.FC; +export default components.Progress; diff --git a/src/renderer/modules/components/index.ts b/src/renderer/modules/components/index.ts index d5244f228..869f1bb40 100644 --- a/src/renderer/modules/components/index.ts +++ b/src/renderer/modules/components/index.ts @@ -163,6 +163,11 @@ export type { NoticeType }; export let Notice: NoticeType; importTimeout("Notice", import("./Notice"), (mod) => (Notice = mod.default)); +import type { ProgressType } from "./Progress"; +export type { ProgressType }; +export let Progress: ProgressType; +importTimeout("Progress", import("./Progress"), (mod) => (Progress = mod.default)); + /** * @internal * @hidden diff --git a/src/renderer/replugged.ts b/src/renderer/replugged.ts index 534e0d7ec..5140e0aa3 100644 --- a/src/renderer/replugged.ts +++ b/src/renderer/replugged.ts @@ -16,6 +16,9 @@ export { Injector } from "./modules/injector"; export * as logger from "./modules/logger"; export { Logger } from "./modules/logger"; +export * as notification from "./apis/notification"; +export { NotificationAPI } from "./apis/notification"; + export * as webpack from "./modules/webpack"; export * as common from "./modules/common"; export * as components from "./modules/components"; From f6f61d644da306d6c7759320e76cd6799923dfa6 Mon Sep 17 00:00:00 2001 From: Tharki-God Date: Tue, 9 Apr 2024 07:55:41 +0530 Subject: [PATCH 2/9] coremod --- src/renderer/managers/coremods.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/renderer/managers/coremods.ts b/src/renderer/managers/coremods.ts index cafd75fb7..1565c7685 100644 --- a/src/renderer/managers/coremods.ts +++ b/src/renderer/managers/coremods.ts @@ -6,6 +6,7 @@ import { default as notrackPlaintext } from "../coremods/notrack/plaintextPatche import { default as noDevtoolsWarningPlaintext } from "../coremods/noDevtoolsWarning/plaintextPatches"; import { default as messagePopover } from "../coremods/messagePopover/plaintextPatches"; import { default as notices } from "../coremods/notices/plaintextPatches"; +import { default as notification } from "../coremods/notification/plaintextPatches"; import { default as contextMenu } from "../coremods/contextMenu/plaintextPatches"; import { default as languagePlaintext } from "../coremods/language/plaintextPatches"; import { default as commandsPlaintext } from "../coremods/commands/plaintextPatches"; @@ -28,6 +29,7 @@ export namespace coremods { export let installer: Coremod; export let messagePopover: Coremod; export let notices: Coremod; + export let notification: Coremod; export let contextMenu: Coremod; export let language: Coremod; export let rpc: Coremod; @@ -53,6 +55,7 @@ export async function startAll(): Promise { coremods.installer = await import("../coremods/installer"); coremods.messagePopover = await import("../coremods/messagePopover"); coremods.notices = await import("../coremods/notices"); + coremods.notification = await import("../coremods/notification"); coremods.contextMenu = await import("../coremods/contextMenu"); coremods.language = await import("../coremods/language"); coremods.rpc = await import("../coremods/rpc"); @@ -83,6 +86,7 @@ export function runPlaintextPatches(): Promise { noDevtoolsWarningPlaintext, messagePopover, notices, + notification, contextMenu, languagePlaintext, commandsPlaintext, From 1b715b495091262073a41d9cf9aaa75c5f3bedc0 Mon Sep 17 00:00:00 2001 From: yofukashino Date: Tue, 8 Apr 2025 17:04:52 +0530 Subject: [PATCH 3/9] chore: lint --- src/renderer/apis/notification.ts | 6 ++---- src/renderer/coremods/notification/notification.tsx | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/renderer/apis/notification.ts b/src/renderer/apis/notification.ts index 8985a0508..1bd62c1d1 100644 --- a/src/renderer/apis/notification.ts +++ b/src/renderer/apis/notification.ts @@ -1,4 +1,4 @@ -import { ButtonItemProps } from "@components/ButtonItem"; +import type { ButtonItemProps } from "../modules/components/ButtonItem"; export interface NotificationProps { id?: string; timeout: number; @@ -63,9 +63,7 @@ export class NotificationAPI { notification.origin = this.origin; notification.type ??= "info"; notification.color ??= this.color; - notification.id = `${this.origin}-${this.name}-${notification.header}-${ - notification.type - } -${Date.now()}`; + notification.id = `${this.origin}-${this.name}-${notification.type} -${Date.now()}`; const dismiss = NotificationHandler.sendNotification(notification as NotificationPropsWithId); this.notifications.push(dismiss); return () => { diff --git a/src/renderer/coremods/notification/notification.tsx b/src/renderer/coremods/notification/notification.tsx index 1738abd69..4fa0d5d56 100644 --- a/src/renderer/coremods/notification/notification.tsx +++ b/src/renderer/coremods/notification/notification.tsx @@ -154,7 +154,7 @@ export default function notificationContainer(): React.ReactElement | null { return (
- {Boolean(toasts.length) && toasts.map((props) => )} + {Boolean(toasts.length) && toasts.map((props, i) => )}
); } From 8640686faeeb831c1345803b26b4c35e2ef277ff Mon Sep 17 00:00:00 2001 From: yofukashino Date: Tue, 8 Apr 2025 17:05:01 +0530 Subject: [PATCH 4/9] fix: swc --- src/renderer/coremods/notification/plaintextPatches.ts | 2 +- src/renderer/modules/components/Progress.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/renderer/coremods/notification/plaintextPatches.ts b/src/renderer/coremods/notification/plaintextPatches.ts index 80b125e40..aacedd3ad 100644 --- a/src/renderer/coremods/notification/plaintextPatches.ts +++ b/src/renderer/coremods/notification/plaintextPatches.ts @@ -5,7 +5,7 @@ export default [ find: "Shakeable is shaken when not mounted", replacements: [ { - match: /(\.DnDKeyboardHelpBar.{20,40})]/, + match: /(\.app,children:\[.+?)\]/, replace: (_, prefix) => `${prefix},replugged.coremods?.coremods?.notification?._renderNotification?.()]`, }, diff --git a/src/renderer/modules/components/Progress.tsx b/src/renderer/modules/components/Progress.tsx index 3ad1ca964..e2da1b452 100644 --- a/src/renderer/modules/components/Progress.tsx +++ b/src/renderer/modules/components/Progress.tsx @@ -1,5 +1,6 @@ import type React from "react"; import components from "../common/components"; +import { getFunctionBySource } from "@webpack"; interface ProgressProps { animate?: boolean; @@ -11,4 +12,4 @@ interface ProgressProps { } export type ProgressType = React.FC; -export default components.Progress; +export default getFunctionBySource(components, ".progressBar")!; From 754784e78c36425d1f6456a79695f46b9749f94b Mon Sep 17 00:00:00 2001 From: yofukashino Date: Sat, 16 Aug 2025 02:30:37 +0530 Subject: [PATCH 5/9] feat: refresh --- src/renderer/apis/notification.ts | 4 +- .../coremods/notification/notification.css | 38 ++++++------------- .../coremods/notification/notification.tsx | 20 +++++++--- 3 files changed, 28 insertions(+), 34 deletions(-) diff --git a/src/renderer/apis/notification.ts b/src/renderer/apis/notification.ts index 1bd62c1d1..7bde52d62 100644 --- a/src/renderer/apis/notification.ts +++ b/src/renderer/apis/notification.ts @@ -1,3 +1,4 @@ +import { DISCORD_BLURPLE } from "src/constants"; import type { ButtonItemProps } from "../modules/components/ButtonItem"; export interface NotificationProps { id?: string; @@ -5,6 +6,7 @@ export interface NotificationProps { origin?: string; name?: string; color?: string; + gradient?: [string, string]; iconColor?: string; imageClassName?: string | undefined; header: React.ReactNode; @@ -55,7 +57,7 @@ export class NotificationAPI { public constructor(origin: string, name: string, color?: string) { this.origin = origin; this.name = name; - this.color = color ?? "#5864f2"; + this.color = color ?? DISCORD_BLURPLE; } public notify(notification: NotificationProps): () => void { diff --git a/src/renderer/coremods/notification/notification.css b/src/renderer/coremods/notification/notification.css index b4f373e60..e66be304a 100644 --- a/src/renderer/coremods/notification/notification.css +++ b/src/renderer/coremods/notification/notification.css @@ -1,12 +1,3 @@ -/*======== CSS Variables ========*/ -.theme-dark { - --replugged-notification-box-shadow: rgba(0, 0, 0, 0.2); - --replugged-notification-border: rgba(28, 36, 43, 0.6); -} -.theme-light { - --replugged-notification-box-shadow: rgba(255, 255, 255, 0.1); - --replugged-notification-border: rgba(199, 199, 199, 0.06); -} /*======== Toast Styling ========*/ .replugged-notification-container { @@ -23,9 +14,9 @@ display: flex; flex-direction: column; margin-bottom: 10px; - background-color: var(--background-secondary); - border: 1px solid var(--replugged-notification-border); - box-shadow: 0 8px 16px 0 var(--replugged-notification-box-shadow); + background-color: var(--background-base-lower); + border: 1px solid var(--border-normal); + box-shadow: 0 8px 16px 0 var(--border-subtle); border-radius: 8px; max-width: 600px; min-width: 223px; @@ -41,20 +32,17 @@ color: var(--header-primary); font-weight: 600; font-size: 24px; - line-height: 1.2; - background-color: var(--background-primary); - border-radius: 8px 8px 0 0; - padding: 12px 20px; - justify-content: center; - align-items: center; /* Center items vertically */ + background-color: var(--background-base-lowest); + border-radius: 8px; + padding: 14px; + align-items: center; } -.replugged-notification .header span:has(.icon) { +.replugged-notification .header span:has(> .icon) { display: flex; align-items: center; justify-content: center; margin-right: 8px; - margin-top: 3px; } .replugged-notification .header .icon { @@ -69,14 +57,10 @@ } .replugged-notification .header .dismiss { - width: 16px; - height: 16px; opacity: 0.5; transition: opacity 0.2s; margin-left: auto; cursor: pointer; - font-size: 12px; - margin-bottom: 5px; } .replugged-notification .contents { @@ -89,11 +73,11 @@ } .replugged-notification .contents .inner { - color: var(--text-normal); + color: var(--text-primary); font-size: 14px; line-height: 1.4; - background-color: var(--background-tertiary); - border-bottom: 1px solid solid var(--replugged-notification-border); + background-color: var(--background-base-low); + border-bottom: 1px solid solid var(--border-normal); border-radius: 8px; padding: 10px; margin-bottom: 6px; diff --git a/src/renderer/coremods/notification/notification.tsx b/src/renderer/coremods/notification/notification.tsx index 4fa0d5d56..63530bf01 100644 --- a/src/renderer/coremods/notification/notification.tsx +++ b/src/renderer/coremods/notification/notification.tsx @@ -4,6 +4,7 @@ import { Button, Clickable, Progress, Tooltip } from "@components"; import Icons from "./icons"; import "./notification.css"; import type { NotificationPropsWithId } from "../../apis/notification"; +import { DISCORD_BLURPLE } from "src/constants"; const predefinedIcons: Record< string, React.MemoExoticComponent<(props: React.SVGProps) => React.ReactElement> @@ -30,7 +31,9 @@ function NotificationGradient(hex: string): string[] { return luminance > 0.5 ? [newHex, hex] : [hex, newHex]; } -function Notification(props: NotificationPropsWithId): React.ReactElement | null { +const Notification = React.memo(function Notification( + props: NotificationPropsWithId, +): React.ReactElement | null { const [leaving, setLeaving] = React.useState(false); const [timeoutState, setTimeoutState] = React.useState(); const [progress, setProgress] = React.useState(100); @@ -131,14 +134,19 @@ function Notification(props: NotificationPropsWithId): React.ReactElement | null )} ); -} +}); -export default function notificationContainer(): React.ReactElement | null { +export default React.memo(function notificationContainer(): React.ReactElement | null { const [toasts, setToasts] = React.useState([]); const toastsUpdate = (): void => setToasts(notification.NotificationHandler.getNotifications()); @@ -154,7 +162,7 @@ export default function notificationContainer(): React.ReactElement | null { return (
- {Boolean(toasts.length) && toasts.map((props, i) => )} + {Boolean(toasts.length) && toasts.map((props) => )}
); -} +}); From 0491445d2f5494a821b774f5523fe319278268ab Mon Sep 17 00:00:00 2001 From: yofukashino Date: Sat, 16 Aug 2025 02:32:39 +0530 Subject: [PATCH 6/9] chore: switch to classNames --- src/renderer/coremods/notification/notification.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/renderer/coremods/notification/notification.tsx b/src/renderer/coremods/notification/notification.tsx index 63530bf01..f1bfd6041 100644 --- a/src/renderer/coremods/notification/notification.tsx +++ b/src/renderer/coremods/notification/notification.tsx @@ -1,10 +1,12 @@ -import { React } from "@common"; +import { React, classNames } from "@common"; import { notification } from "@replugged"; import { Button, Clickable, Progress, Tooltip } from "@components"; import Icons from "./icons"; -import "./notification.css"; import type { NotificationPropsWithId } from "../../apis/notification"; import { DISCORD_BLURPLE } from "src/constants"; + +import "./notification.css"; + const predefinedIcons: Record< string, React.MemoExoticComponent<(props: React.SVGProps) => React.ReactElement> @@ -65,9 +67,9 @@ const Notification = React.memo(function Notification( return (
{props.header && (
From ab37547f8f49f0227227fdc624b115bd579911d4 Mon Sep 17 00:00:00 2001 From: yofukashino Date: Sat, 16 Aug 2025 02:33:32 +0530 Subject: [PATCH 7/9] chore: prettier --- src/renderer/managers/coremods.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/renderer/managers/coremods.ts b/src/renderer/managers/coremods.ts index 6d10acace..8ee4b7f2e 100644 --- a/src/renderer/managers/coremods.ts +++ b/src/renderer/managers/coremods.ts @@ -9,14 +9,13 @@ import experimentsPlaintext from "../coremods/experiments/plaintextPatches"; import languagePlaintext from "../coremods/language/plaintextPatches"; import messagePopoverPlaintext from "../coremods/messagePopover/plaintextPatches"; import noDevtoolsWarningPlaintext from "../coremods/noDevtoolsWarning/plaintextPatches"; -import notificationPlaintext from "../coremods/notification/plaintextPatches"; +import notificationPlaintext from "../coremods/notification/plaintextPatches"; import noticesPlaintext from "../coremods/notices/plaintextPatches"; import notrackPlaintext from "../coremods/notrack/plaintextPatches"; import popoutThemingPlaintext from "../coremods/popoutTheming/plaintextPatches"; import settingsPlaintext from "../coremods/settings/plaintextPatches"; import titleBarPlaintext from "../coremods/titleBar/plaintextPatches"; - const logger = Logger.api("Coremods"); interface Coremod { From ff04240545497a5fdea004e26ec4d9e796aebed6 Mon Sep 17 00:00:00 2001 From: yofukashino Date: Sat, 23 Aug 2025 02:18:59 +0530 Subject: [PATCH 8/9] feat: jsdocs --- src/renderer/apis/notification.ts | 64 +++++++++++++++++++ src/renderer/coremods/notification/index.tsx | 4 ++ .../coremods/notification/notification.css | 17 ++++- .../coremods/notification/notification.tsx | 6 +- 4 files changed, 86 insertions(+), 5 deletions(-) diff --git a/src/renderer/apis/notification.ts b/src/renderer/apis/notification.ts index 7bde52d62..1a30819aa 100644 --- a/src/renderer/apis/notification.ts +++ b/src/renderer/apis/notification.ts @@ -1,5 +1,6 @@ import { DISCORD_BLURPLE } from "src/constants"; import type { ButtonItemProps } from "../modules/components/ButtonItem"; + export interface NotificationProps { id?: string; timeout: number; @@ -25,6 +26,7 @@ export interface NotificationPropsWithId extends NotificationProps { name: string; color: string; } + class RPNotificationHandler extends EventTarget { private notifications = new Map(); @@ -46,20 +48,58 @@ class RPNotificationHandler extends EventTarget { this.dispatchEvent(new CustomEvent("rpNotificationUpdate")); } } + +/** + * @internal + * @hidden + */ export const NotificationHandler = new RPNotificationHandler(); +/** + * Send an in-app notification on discord. + * + * @example + * ``` + * import { NotificationAPI } from "replugged"; + * + * const notification = NotificationAPI.coremod("Notification"); + * + * export async function start() { + * notification.notify({ + * header: "Example", + timeout: 10000, + content: "This is an example notification!" + }) + * } + * + * export function stop() { + * notification.dismissAll(); + * } + * ``` + */ export class NotificationAPI { public origin: string; public name: string; public color: string; private notifications: Array<() => void> = []; + /** + * + * @param origin Origin of the context (e.g. API, Plugin, Coremod...) + * @param name Name of the context (e.g. Notices, SilentTyping, Badges...) + * @param color Color of the prefix as hex or a CSS color + */ public constructor(origin: string, name: string, color?: string) { this.origin = origin; this.name = name; this.color = color ?? DISCORD_BLURPLE; } + /** + * A function to send in app notification. + * @param notification The notification details to show + * @returns A callback to dismiss the notification + */ public notify(notification: NotificationProps): () => void { notification.name = this.name; notification.origin = this.origin; @@ -74,6 +114,9 @@ export class NotificationAPI { }; } + /** + * Dismiss all notifications made by from this origin + */ public dismissAll(): void { for (const dismiss of this.notifications) { dismiss(); @@ -81,14 +124,35 @@ export class NotificationAPI { this.notifications = []; } + /** + * Convenience method to create a new {@link NotificationAPI} for an API. + * @internal + * @param name Name of the API + * @param color Color of the prefix as hex or a CSS color (default: blurple) + * @returns {@link NotificationAPI} with origin "API" + */ public static api(name: string, color?: string): NotificationAPI { return new NotificationAPI("API", name, color); } + /** + * Convenience method to create a new {@link NotificationAPI} for an coremod. + * @internal + * @param name Name of the Coremod + * @param color Color of the prefix as hex or a CSS color (default: blurple) + * @returns {@link NotificationAPI} with origin "Coremod" + */ public static coremod(name: string, color?: string): NotificationAPI { return new NotificationAPI("Coremod", name, color); } + /** + * Convenience method to create a new {@link NotificationAPI} for an Plugin. + * @internal + * @param name Name of the Plugin + * @param color Color of the prefix as hex or a CSS color (default: blurple) + * @returns {@link NotificationAPI} with origin "Plugin" + */ public static plugin(name: string, color?: string): NotificationAPI { return new NotificationAPI("Plugin", name, color); } diff --git a/src/renderer/coremods/notification/index.tsx b/src/renderer/coremods/notification/index.tsx index 4c5542f6a..f8c75200e 100644 --- a/src/renderer/coremods/notification/index.tsx +++ b/src/renderer/coremods/notification/index.tsx @@ -1,5 +1,9 @@ import NotificationContainer from "./notification"; +/** + * @internal + * @hidden + */ export function _renderNotification(): React.ReactElement { return ; } diff --git a/src/renderer/coremods/notification/notification.css b/src/renderer/coremods/notification/notification.css index e66be304a..a9ac2cacc 100644 --- a/src/renderer/coremods/notification/notification.css +++ b/src/renderer/coremods/notification/notification.css @@ -4,19 +4,32 @@ display: flex; flex-direction: column; align-items: flex-end; - justify-content: flex-end; position: fixed; bottom: 25px; right: 25px; z-index: 999; + max-height: 69%; + overflow: hidden scroll; + scroll-snap-type: y proximity; +} + +.replugged-notification-container::-webkit-scrollbar, +.replugged-notification-container::-webkit-scrollbar-track, +.replugged-notification-container::-webkit-scrollbar-thumb, +.replugged-notification-container::-webkit-scrollbar-corner { + opacity: 0; } + +.replugged-notification-container > .replugged-notification:last-child { + scroll-snap-align: end; +} + .replugged-notification { display: flex; flex-direction: column; margin-bottom: 10px; background-color: var(--background-base-lower); border: 1px solid var(--border-normal); - box-shadow: 0 8px 16px 0 var(--border-subtle); border-radius: 8px; max-width: 600px; min-width: 223px; diff --git a/src/renderer/coremods/notification/notification.tsx b/src/renderer/coremods/notification/notification.tsx index f1bfd6041..4d4016bec 100644 --- a/src/renderer/coremods/notification/notification.tsx +++ b/src/renderer/coremods/notification/notification.tsx @@ -33,9 +33,9 @@ function NotificationGradient(hex: string): string[] { return luminance > 0.5 ? [newHex, hex] : [hex, newHex]; } -const Notification = React.memo(function Notification( +const Notification = React.memo(( props: NotificationPropsWithId, -): React.ReactElement | null { +): React.ReactElement | null => { const [leaving, setLeaving] = React.useState(false); const [timeoutState, setTimeoutState] = React.useState(); const [progress, setProgress] = React.useState(100); @@ -148,7 +148,7 @@ const Notification = React.memo(function Notification( ); }); -export default React.memo(function notificationContainer(): React.ReactElement | null { +export default React.memo((): React.ReactElement | null => { const [toasts, setToasts] = React.useState([]); const toastsUpdate = (): void => setToasts(notification.NotificationHandler.getNotifications()); From babcb6efe45d53e5799421dac1c631a01b80a68d Mon Sep 17 00:00:00 2001 From: yofukashino Date: Sat, 23 Aug 2025 02:30:39 +0530 Subject: [PATCH 9/9] style: prettier sucks dick --- src/renderer/coremods/notification/notification.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/renderer/coremods/notification/notification.tsx b/src/renderer/coremods/notification/notification.tsx index 4d4016bec..473983cd2 100644 --- a/src/renderer/coremods/notification/notification.tsx +++ b/src/renderer/coremods/notification/notification.tsx @@ -33,9 +33,7 @@ function NotificationGradient(hex: string): string[] { return luminance > 0.5 ? [newHex, hex] : [hex, newHex]; } -const Notification = React.memo(( - props: NotificationPropsWithId, -): React.ReactElement | null => { +const Notification = React.memo((props: NotificationPropsWithId): React.ReactElement | null => { const [leaving, setLeaving] = React.useState(false); const [timeoutState, setTimeoutState] = React.useState(); const [progress, setProgress] = React.useState(100); @@ -148,7 +146,7 @@ const Notification = React.memo(( ); }); -export default React.memo((): React.ReactElement | null => { +export default React.memo((): React.ReactElement | null => { const [toasts, setToasts] = React.useState([]); const toastsUpdate = (): void => setToasts(notification.NotificationHandler.getNotifications());