212 lines
7 KiB
TypeScript
212 lines
7 KiB
TypeScript
/*
|
|
* 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 { Flex } from "@components/Flex";
|
|
import { openNotificationSettingsModal } from "@components/VencordSettings/NotificationSettings";
|
|
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
|
import { useAwaiter } from "@utils/react";
|
|
import { Alerts, Button, Forms, 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={new Date(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>
|
|
<Flex>
|
|
<Button onClick={openNotificationSettingsModal}>
|
|
Notification Settings
|
|
</Button>
|
|
|
|
<Button
|
|
disabled={log.length === 0}
|
|
color={Button.Colors.RED}
|
|
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>
|
|
</Flex>
|
|
</ModalFooter>
|
|
</ModalRoot>
|
|
);
|
|
}
|
|
|
|
export function openNotificationLogModal() {
|
|
const key = openModal(modalProps => (
|
|
<LogModal
|
|
modalProps={modalProps}
|
|
close={() => closeModal(key)}
|
|
/>
|
|
));
|
|
}
|