aboutsummaryrefslogtreecommitdiff
path: root/src/api/Notifications/notificationLog.tsx
diff options
context:
space:
mode:
authorV <vendicated@riseup.net>2023-04-01 02:47:49 +0200
committerGitHub <noreply@github.com>2023-04-01 02:47:49 +0200
commit6960a439c9c2af261517b00b0b16a7fc5756c48b (patch)
tree4e1832c5cc295b26046e193ac5cf60bcd2590568 /src/api/Notifications/notificationLog.tsx
parent4dff1c5bd5b16e926bc628acf11118344832a374 (diff)
downloadVencord-6960a439c9c2af261517b00b0b16a7fc5756c48b.tar.gz
Vencord-6960a439c9c2af261517b00b0b16a7fc5756c48b.tar.bz2
Vencord-6960a439c9c2af261517b00b0b16a7fc5756c48b.zip
Add Notification log (#745)
Diffstat (limited to 'src/api/Notifications/notificationLog.tsx')
-rw-r--r--src/api/Notifications/notificationLog.tsx203
1 files changed, 203 insertions, 0 deletions
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)}
+ />
+ ));
+}