aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Vencord.ts2
-rw-r--r--src/api/Notifications/NotificationComponent.tsx12
-rw-r--r--src/api/Notifications/Notifications.tsx7
-rw-r--r--src/api/Notifications/notificationLog.tsx203
-rw-r--r--src/api/Notifications/styles.css58
-rw-r--r--src/api/settings.ts4
-rw-r--r--src/components/VencordSettings/VencordTab.tsx28
-rw-r--r--src/plugins/crashHandler.ts2
-rw-r--r--src/plugins/devCompanion.dev.tsx6
-rw-r--r--src/webpack/common/react.ts4
10 files changed, 311 insertions, 15 deletions
diff --git a/src/Vencord.ts b/src/Vencord.ts
index af6ca08..00f8a58 100644
--- a/src/Vencord.ts
+++ b/src/Vencord.ts
@@ -54,6 +54,7 @@ async function init() {
title: "Vencord has been updated!",
body: "Click here to restart",
permanent: true,
+ noPersist: true,
onClick() {
if (needsFullRestart)
window.DiscordNative.app.relaunch();
@@ -69,6 +70,7 @@ async function init() {
title: "A Vencord update is available!",
body: "Click here to view the update",
permanent: true,
+ noPersist: true,
onClick() {
SettingsRouter.open("VencordUpdater");
}
diff --git a/src/api/Notifications/NotificationComponent.tsx b/src/api/Notifications/NotificationComponent.tsx
index 53c1b81..542c29b 100644
--- a/src/api/Notifications/NotificationComponent.tsx
+++ b/src/api/Notifications/NotificationComponent.tsx
@@ -20,6 +20,7 @@ import "./styles.css";
import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
+import { classes } from "@utils/misc";
import { React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
import { NotificationData } from "./Notifications";
@@ -33,8 +34,10 @@ export default ErrorBoundary.wrap(function NotificationComponent({
onClick,
onClose,
image,
- permanent
-}: NotificationData) {
+ permanent,
+ className,
+ dismissOnClick
+}: NotificationData & { className?: string; }) {
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
@@ -61,11 +64,12 @@ export default ErrorBoundary.wrap(function NotificationComponent({
return (
<button
- className="vc-notification-root"
+ className={classes("vc-notification-root", className)}
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
onClick={() => {
- onClose!();
onClick?.();
+ if (dismissOnClick !== false)
+ onClose!();
}}
onContextMenu={e => {
e.preventDefault();
diff --git a/src/api/Notifications/Notifications.tsx b/src/api/Notifications/Notifications.tsx
index a6b2ccd..c842ec8 100644
--- a/src/api/Notifications/Notifications.tsx
+++ b/src/api/Notifications/Notifications.tsx
@@ -23,6 +23,7 @@ import type { ReactNode } from "react";
import type { Root } from "react-dom/client";
import NotificationComponent from "./NotificationComponent";
+import { persistNotification } from "./notificationLog";
const NotificationQueue = new Queue();
@@ -56,6 +57,10 @@ export interface NotificationData {
color?: string;
/** Whether this notification should not have a timeout */
permanent?: boolean;
+ /** Whether this notification should not be persisted in the Notification Log */
+ noPersist?: boolean;
+ /** Whether this notification should be dismissed when clicked (defaults to true) */
+ dismissOnClick?: boolean;
}
function _showNotification(notification: NotificationData, id: number) {
@@ -86,6 +91,8 @@ export async function requestPermission() {
}
export async function showNotification(data: NotificationData) {
+ persistNotification(data);
+
if (shouldBeNative() && await requestPermission()) {
const { title, body, icon, image, onClick = null, onClose = null } = data;
const n = new Notification(title, {
diff --git a/src/api/Notifications/notificationLog.tsx b/src/api/Notifications/notificationLog.tsx
new file mode 100644
index 0000000..72f09ac
--- /dev/null
+++ b/src/api/Notifications/notificationLog.tsx
@@ -0,0 +1,203 @@
+/*
+ * 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 * as DataStore from "@api/DataStore";
+import { Settings } from "@api/settings";
+import { classNameFactory } from "@api/Styles";
+import { useAwaiter } from "@utils/misc";
+import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
+import { Alerts, Button, Forms, moment, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
+import { nanoid } from "nanoid";
+import type { DispatchWithoutAction } from "react";
+
+import NotificationComponent from "./NotificationComponent";
+import type { NotificationData } from "./Notifications";
+
+interface PersistentNotificationData extends Pick<NotificationData, "title" | "body" | "image" | "icon" | "color"> {
+ timestamp: number;
+ id: string;
+}
+
+const KEY = "notification-log";
+
+const getLog = async () => {
+ const log = await DataStore.get(KEY) as PersistentNotificationData[] | undefined;
+ return log ?? [];
+};
+
+const cl = classNameFactory("vc-notification-log-");
+const signals = new Set<DispatchWithoutAction>();
+
+export async function persistNotification(notification: NotificationData) {
+ if (notification.noPersist) return;
+
+ const limit = Settings.notifications.logLimit;
+ if (limit === 0) return;
+
+ await DataStore.update(KEY, (old: PersistentNotificationData[] | undefined) => {
+ const log = old ?? [];
+
+ // Omit stuff we don't need
+ const {
+ onClick, onClose, richBody, permanent, noPersist, dismissOnClick,
+ ...pureNotification
+ } = notification;
+
+ log.unshift({
+ ...pureNotification,
+ timestamp: Date.now(),
+ id: nanoid()
+ });
+
+ if (log.length > limit && limit !== 200)
+ log.length = limit;
+
+ return log;
+ });
+
+ signals.forEach(x => x());
+}
+
+export async function deleteNotification(timestamp: number) {
+ const log = await getLog();
+ const index = log.findIndex(x => x.timestamp === timestamp);
+ if (index === -1) return;
+
+ log.splice(index, 1);
+ await DataStore.set(KEY, log);
+ signals.forEach(x => x());
+}
+
+export function useLogs() {
+ const [signal, setSignal] = useReducer(x => x + 1, 0);
+
+ useEffect(() => {
+ signals.add(setSignal);
+ return () => void signals.delete(setSignal);
+ }, []);
+
+ const [log, _, pending] = useAwaiter(getLog, {
+ fallbackValue: [],
+ deps: [signal]
+ });
+
+ return [log, pending] as const;
+}
+
+function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
+ const [removing, setRemoving] = useState(false);
+ const ref = React.useRef<HTMLDivElement>(null);
+
+ useEffect(() => {
+ const div = ref.current!;
+
+ const setHeight = () => {
+ if (div.clientHeight === 0) return requestAnimationFrame(setHeight);
+ div.style.height = `${div.clientHeight}px`;
+ };
+
+ setHeight();
+ }, []);
+
+ return (
+ <div className={cl("wrapper", { removing })} ref={ref}>
+ <NotificationComponent
+ {...data}
+ permanent={true}
+ dismissOnClick={false}
+ onClose={() => {
+ if (removing) return;
+ setRemoving(true);
+
+ setTimeout(() => deleteNotification(data.timestamp), 200);
+ }}
+ richBody={
+ <div className={cl("body")}>
+ {data.body}
+ <Timestamp timestamp={moment(data.timestamp)} className={cl("timestamp")} />
+ </div>
+ }
+ />
+ </div>
+ );
+}
+
+export function NotificationLog({ log, pending }: { log: PersistentNotificationData[], pending: boolean; }) {
+ if (!log.length && !pending)
+ return (
+ <div className={cl("container")}>
+ <div className={cl("empty")} />
+ <Forms.FormText style={{ textAlign: "center" }}>
+ No notifications yet
+ </Forms.FormText>
+ </div>
+ );
+
+ return (
+ <div className={cl("container")}>
+ {log.map(n => <NotificationEntry data={n} key={n.id} />)}
+ </div>
+ );
+}
+
+function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void; }) {
+ const [log, pending] = useLogs();
+
+ return (
+ <ModalRoot {...modalProps} size={ModalSize.LARGE}>
+ <ModalHeader>
+ <Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Log</Text>
+ <ModalCloseButton onClick={close} />
+ </ModalHeader>
+
+ <ModalContent>
+ <NotificationLog log={log} pending={pending} />
+ </ModalContent>
+
+ <ModalFooter>
+ <Button
+ disabled={log.length === 0}
+ onClick={() => {
+ Alerts.show({
+ title: "Are you sure?",
+ body: `This will permanently remove ${log.length} notification${log.length === 1 ? "" : "s"}. This action cannot be undone.`,
+ async onConfirm() {
+ await DataStore.set(KEY, []);
+ signals.forEach(x => x());
+ },
+ confirmText: "Do it!",
+ confirmColor: "vc-notification-log-danger-btn",
+ cancelText: "Nevermind"
+ });
+ }}
+ >
+ Clear Notification Log
+ </Button>
+ </ModalFooter>
+ </ModalRoot>
+ );
+}
+
+export function openNotificationLogModal() {
+ const key = openModal(modalProps => (
+ <LogModal
+ modalProps={modalProps}
+ close={() => closeModal(key)}
+ />
+ ));
+}
diff --git a/src/api/Notifications/styles.css b/src/api/Notifications/styles.css
index cd37142..98dff6d 100644
--- a/src/api/Notifications/styles.css
+++ b/src/api/Notifications/styles.css
@@ -3,16 +3,20 @@
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;
+ width: 100%;
+}
+
+.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
+ position: absolute;
+ z-index: 2147483647;
+ right: 1rem;
+ width: 25vw;
+ min-height: 10vh;
}
.vc-notification {
@@ -72,3 +76,47 @@
.vc-notification-img {
width: 100%;
}
+
+.vc-notification-log-empty {
+ height: 218px;
+ background: url("/assets/b36de980b174d7b798c89f35c116e5c6.svg") center no-repeat;
+ margin-bottom: 40px;
+}
+
+.vc-notification-log-container {
+ display: flex;
+ flex-direction: column;
+ padding: 1em;
+ overflow: hidden;
+}
+
+.vc-notification-log-wrapper {
+ transition: 200ms ease;
+ transition-property: height, opacity;
+}
+
+.vc-notification-log-wrapper:not(:last-child) {
+ margin-bottom: 1em;
+}
+
+.vc-notification-log-removing {
+ height: 0 !important;
+ opacity: 0;
+ margin-bottom: 1em;
+}
+
+.vc-notification-log-body {
+ display: flex;
+ flex-direction: column;
+}
+
+.vc-notification-log-timestamp {
+ margin-left: auto;
+ font-size: 0.8em;
+ font-weight: lighter;
+}
+
+.vc-notification-log-danger-btn {
+ color: var(--white-500);
+ background-color: var(--button-danger-background);
+}
diff --git a/src/api/settings.ts b/src/api/settings.ts
index 0aaa490..321a4c4 100644
--- a/src/api/settings.ts
+++ b/src/api/settings.ts
@@ -47,6 +47,7 @@ export interface Settings {
timeout: number;
position: "top-right" | "bottom-right";
useNative: "always" | "never" | "not-focused";
+ logLimit: number;
};
}
@@ -66,7 +67,8 @@ const DefaultSettings: Settings = {
notifications: {
timeout: 5000,
position: "bottom-right",
- useNative: "not-focused"
+ useNative: "not-focused",
+ logLimit: 50
}
};
diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx
index 120f4c5..7113421 100644
--- a/src/components/VencordSettings/VencordTab.tsx
+++ b/src/components/VencordSettings/VencordTab.tsx
@@ -17,6 +17,7 @@
*/
+import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { useSettings } from "@api/settings";
import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton";
@@ -165,7 +166,7 @@ function VencordSettings() {
{ 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>>}
+ ] satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>}
closeOnSelect={true}
select={v => notifSettings.useNative = v}
isSelected={v => v === notifSettings.useNative}
@@ -179,7 +180,7 @@ function VencordSettings() {
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>>}
+ ] satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>}
select={v => notifSettings.position = v}
isSelected={v => v === notifSettings.position}
serialize={identity}
@@ -198,6 +199,29 @@ function VencordSettings() {
onMarkerRender={v => (v / 1000) + "s"}
stickToMarkers={false}
/>
+
+ <Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Log Limit</Forms.FormTitle>
+ <Forms.FormText className={Margins.bottom16}>
+ The amount of notifications to save in the log until old ones are removed.
+ Set to <code>0</code> to disable Notification log and <code>∞</code> to never automatically remove old Notifications
+ </Forms.FormText>
+ <Slider
+ markers={[0, 25, 50, 75, 100, 200]}
+ minValue={0}
+ maxValue={200}
+ stickToMarkers={true}
+ initialValue={notifSettings.logLimit}
+ onValueChange={v => notifSettings.logLimit = v}
+ onValueRender={v => v === 200 ? "∞" : v}
+ onMarkerRender={v => v === 200 ? "∞" : v}
+ />
+
+ <Button
+ onClick={openNotificationLogModal}
+ disabled={notifSettings.logLimit === 0}
+ >
+ Open Notification Log
+ </Button>
</React.Fragment>
);
}
diff --git a/src/plugins/crashHandler.ts b/src/plugins/crashHandler.ts
index 6457e09..61b06b3 100644
--- a/src/plugins/crashHandler.ts
+++ b/src/plugins/crashHandler.ts
@@ -78,6 +78,7 @@ export default definePlugin({
color: "#eed202",
title: "Discord has crashed!",
body: "Awn :( Discord has crashed more than five times, not attempting to recover.",
+ noPersist: true,
});
} catch { }
@@ -111,6 +112,7 @@ export default definePlugin({
color: "#eed202",
title: "Discord has crashed!",
body: "Attempting to recover...",
+ noPersist: true,
});
} catch { }
}
diff --git a/src/plugins/devCompanion.dev.tsx b/src/plugins/devCompanion.dev.tsx
index b9a7e4d..c675fc2 100644
--- a/src/plugins/devCompanion.dev.tsx
+++ b/src/plugins/devCompanion.dev.tsx
@@ -116,7 +116,8 @@ function initWs(isManual = false) {
showNotification({
title: "Dev Companion Error",
body: (e as ErrorEvent).message || "No Error Message",
- color: "var(--status-danger, red)"
+ color: "var(--status-danger, red)",
+ noPersist: true,
});
});
@@ -128,7 +129,8 @@ function initWs(isManual = false) {
showNotification({
title: "Dev Companion Disconnected",
body: e.reason || "No Reason provided",
- color: "var(--status-danger, red)"
+ color: "var(--status-danger, red)",
+ noPersist: true,
});
});
diff --git a/src/webpack/common/react.ts b/src/webpack/common/react.ts
index d73a3df..1719613 100644
--- a/src/webpack/common/react.ts
+++ b/src/webpack/common/react.ts
@@ -24,10 +24,12 @@ export let useState: typeof React.useState;
export let useEffect: typeof React.useEffect;
export let useMemo: typeof React.useMemo;
export let useRef: typeof React.useRef;
+export let useReducer: typeof React.useReducer;
+export let useCallback: typeof React.useCallback;
export const ReactDOM: typeof import("react-dom") & typeof import("react-dom/client") = findByPropsLazy("createPortal", "render");
waitFor("useState", m => {
React = m;
- ({ useEffect, useState, useMemo, useRef } = React);
+ ({ useEffect, useState, useMemo, useRef, useReducer, useCallback } = React);
});