Add Notification log (#745)
This commit is contained in:
parent
4dff1c5bd5
commit
6960a439c9
14 changed files with 333 additions and 19 deletions
|
@ -34,11 +34,13 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vap/core": "0.0.12",
|
"@vap/core": "0.0.12",
|
||||||
"@vap/shiki": "0.10.3",
|
"@vap/shiki": "0.10.3",
|
||||||
"fflate": "^0.7.4"
|
"fflate": "^0.7.4",
|
||||||
|
"nanoid": "^4.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/diff": "^5.0.2",
|
"@types/diff": "^5.0.2",
|
||||||
"@types/lodash": "^4.14.191",
|
"@types/lodash": "^4.14.191",
|
||||||
|
"@types/nanoid": "^3.0.0",
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
"@types/react": "^18.0.27",
|
"@types/react": "^18.0.27",
|
||||||
"@types/react-dom": "^18.0.10",
|
"@types/react-dom": "^18.0.10",
|
||||||
|
|
|
@ -11,6 +11,7 @@ patchedDependencies:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@types/diff': ^5.0.2
|
'@types/diff': ^5.0.2
|
||||||
'@types/lodash': ^4.14.191
|
'@types/lodash': ^4.14.191
|
||||||
|
'@types/nanoid': ^3.0.0
|
||||||
'@types/node': ^18.11.18
|
'@types/node': ^18.11.18
|
||||||
'@types/react': ^18.0.27
|
'@types/react': ^18.0.27
|
||||||
'@types/react-dom': ^18.0.10
|
'@types/react-dom': ^18.0.10
|
||||||
|
@ -31,6 +32,7 @@ specifiers:
|
||||||
fflate: ^0.7.4
|
fflate: ^0.7.4
|
||||||
highlight.js: 10.6.0
|
highlight.js: 10.6.0
|
||||||
moment: ^2.29.4
|
moment: ^2.29.4
|
||||||
|
nanoid: ^4.0.2
|
||||||
puppeteer-core: ^19.6.0
|
puppeteer-core: ^19.6.0
|
||||||
standalone-electron-types: ^1.0.0
|
standalone-electron-types: ^1.0.0
|
||||||
stylelint: ^14.16.1
|
stylelint: ^14.16.1
|
||||||
|
@ -43,10 +45,12 @@ dependencies:
|
||||||
'@vap/core': 0.0.12
|
'@vap/core': 0.0.12
|
||||||
'@vap/shiki': 0.10.3
|
'@vap/shiki': 0.10.3
|
||||||
fflate: 0.7.4
|
fflate: 0.7.4
|
||||||
|
nanoid: 4.0.2
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/diff': 5.0.2
|
'@types/diff': 5.0.2
|
||||||
'@types/lodash': 4.14.191
|
'@types/lodash': 4.14.191
|
||||||
|
'@types/nanoid': 3.0.0
|
||||||
'@types/node': 18.11.18
|
'@types/node': 18.11.18
|
||||||
'@types/react': 18.0.27
|
'@types/react': 18.0.27
|
||||||
'@types/react-dom': 18.0.10
|
'@types/react-dom': 18.0.10
|
||||||
|
@ -417,6 +421,13 @@ packages:
|
||||||
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
|
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/nanoid/3.0.0:
|
||||||
|
resolution: {integrity: sha512-UXitWSmXCwhDmAKe7D3hNQtQaHeHt5L8LO1CB8GF8jlYVzOv5cBWDNqiJ+oPEWrWei3i3dkZtHY/bUtd0R/uOQ==}
|
||||||
|
deprecated: This is a stub types definition. nanoid provides its own type definitions, so you do not need this installed.
|
||||||
|
dependencies:
|
||||||
|
nanoid: 4.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/node/18.11.18:
|
/@types/node/18.11.18:
|
||||||
resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==}
|
resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -2245,6 +2256,11 @@ packages:
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/nanoid/4.0.2:
|
||||||
|
resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
|
||||||
|
engines: {node: ^14 || ^16 || >=18}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
/nanomatch/1.2.13:
|
/nanomatch/1.2.13:
|
||||||
resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
|
resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
|
@ -36,7 +36,7 @@ const commonOptions = {
|
||||||
entryPoints: ["browser/Vencord.ts"],
|
entryPoints: ["browser/Vencord.ts"],
|
||||||
globalName: "Vencord",
|
globalName: "Vencord",
|
||||||
format: "iife",
|
format: "iife",
|
||||||
external: ["plugins", "git-hash"],
|
external: ["plugins", "git-hash", "/assets/*"],
|
||||||
plugins: [
|
plugins: [
|
||||||
globPlugins,
|
globPlugins,
|
||||||
...commonOpts.plugins,
|
...commonOpts.plugins,
|
||||||
|
|
|
@ -193,7 +193,7 @@ export const commonOpts = {
|
||||||
legalComments: "linked",
|
legalComments: "linked",
|
||||||
banner,
|
banner,
|
||||||
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
||||||
external: ["~plugins", "~git-hash", "~git-remote"],
|
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
|
||||||
inject: ["./scripts/build/inject/react.mjs"],
|
inject: ["./scripts/build/inject/react.mjs"],
|
||||||
jsxFactory: "VencordCreateElement",
|
jsxFactory: "VencordCreateElement",
|
||||||
jsxFragment: "VencordFragment",
|
jsxFragment: "VencordFragment",
|
||||||
|
|
|
@ -54,6 +54,7 @@ async function init() {
|
||||||
title: "Vencord has been updated!",
|
title: "Vencord has been updated!",
|
||||||
body: "Click here to restart",
|
body: "Click here to restart",
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
noPersist: true,
|
||||||
onClick() {
|
onClick() {
|
||||||
if (needsFullRestart)
|
if (needsFullRestart)
|
||||||
window.DiscordNative.app.relaunch();
|
window.DiscordNative.app.relaunch();
|
||||||
|
@ -69,6 +70,7 @@ async function init() {
|
||||||
title: "A Vencord update is available!",
|
title: "A Vencord update is available!",
|
||||||
body: "Click here to view the update",
|
body: "Click here to view the update",
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
noPersist: true,
|
||||||
onClick() {
|
onClick() {
|
||||||
SettingsRouter.open("VencordUpdater");
|
SettingsRouter.open("VencordUpdater");
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import "./styles.css";
|
||||||
|
|
||||||
import { useSettings } from "@api/settings";
|
import { useSettings } from "@api/settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { classes } from "@utils/misc";
|
||||||
import { React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
|
import { React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
|
||||||
|
|
||||||
import { NotificationData } from "./Notifications";
|
import { NotificationData } from "./Notifications";
|
||||||
|
@ -33,8 +34,10 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
||||||
onClick,
|
onClick,
|
||||||
onClose,
|
onClose,
|
||||||
image,
|
image,
|
||||||
permanent
|
permanent,
|
||||||
}: NotificationData) {
|
className,
|
||||||
|
dismissOnClick
|
||||||
|
}: NotificationData & { className?: string; }) {
|
||||||
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
|
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
|
||||||
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
|
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
|
||||||
|
|
||||||
|
@ -61,11 +64,12 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="vc-notification-root"
|
className={classes("vc-notification-root", className)}
|
||||||
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
|
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose!();
|
|
||||||
onClick?.();
|
onClick?.();
|
||||||
|
if (dismissOnClick !== false)
|
||||||
|
onClose!();
|
||||||
}}
|
}}
|
||||||
onContextMenu={e => {
|
onContextMenu={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
@ -23,6 +23,7 @@ import type { ReactNode } from "react";
|
||||||
import type { Root } from "react-dom/client";
|
import type { Root } from "react-dom/client";
|
||||||
|
|
||||||
import NotificationComponent from "./NotificationComponent";
|
import NotificationComponent from "./NotificationComponent";
|
||||||
|
import { persistNotification } from "./notificationLog";
|
||||||
|
|
||||||
const NotificationQueue = new Queue();
|
const NotificationQueue = new Queue();
|
||||||
|
|
||||||
|
@ -56,6 +57,10 @@ export interface NotificationData {
|
||||||
color?: string;
|
color?: string;
|
||||||
/** Whether this notification should not have a timeout */
|
/** Whether this notification should not have a timeout */
|
||||||
permanent?: boolean;
|
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) {
|
function _showNotification(notification: NotificationData, id: number) {
|
||||||
|
@ -86,6 +91,8 @@ export async function requestPermission() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function showNotification(data: NotificationData) {
|
export async function showNotification(data: NotificationData) {
|
||||||
|
persistNotification(data);
|
||||||
|
|
||||||
if (shouldBeNative() && await requestPermission()) {
|
if (shouldBeNative() && await requestPermission()) {
|
||||||
const { title, body, icon, image, onClick = null, onClose = null } = data;
|
const { title, body, icon, image, onClick = null, onClose = null } = data;
|
||||||
const n = new Notification(title, {
|
const n = new Notification(title, {
|
||||||
|
|
203
src/api/Notifications/notificationLog.tsx
Normal file
203
src/api/Notifications/notificationLog.tsx
Normal file
|
@ -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)}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
|
@ -3,16 +3,20 @@
|
||||||
all: unset;
|
all: unset;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 25vw;
|
|
||||||
min-height: 10vh;
|
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
background-color: var(--background-secondary-alt);
|
background-color: var(--background-secondary-alt);
|
||||||
position: absolute;
|
|
||||||
z-index: 2147483647;
|
|
||||||
right: 1rem;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
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 {
|
.vc-notification {
|
||||||
|
@ -72,3 +76,47 @@
|
||||||
.vc-notification-img {
|
.vc-notification-img {
|
||||||
width: 100%;
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ export interface Settings {
|
||||||
timeout: number;
|
timeout: number;
|
||||||
position: "top-right" | "bottom-right";
|
position: "top-right" | "bottom-right";
|
||||||
useNative: "always" | "never" | "not-focused";
|
useNative: "always" | "never" | "not-focused";
|
||||||
|
logLimit: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +67,8 @@ const DefaultSettings: Settings = {
|
||||||
notifications: {
|
notifications: {
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
position: "bottom-right",
|
position: "bottom-right",
|
||||||
useNative: "not-focused"
|
useNative: "not-focused",
|
||||||
|
logLimit: 50
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
|
||||||
import { useSettings } from "@api/settings";
|
import { useSettings } from "@api/settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import DonateButton from "@components/DonateButton";
|
import DonateButton from "@components/DonateButton";
|
||||||
|
@ -198,6 +199,29 @@ function VencordSettings() {
|
||||||
onMarkerRender={v => (v / 1000) + "s"}
|
onMarkerRender={v => (v / 1000) + "s"}
|
||||||
stickToMarkers={false}
|
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>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,7 @@ export default definePlugin({
|
||||||
color: "#eed202",
|
color: "#eed202",
|
||||||
title: "Discord has crashed!",
|
title: "Discord has crashed!",
|
||||||
body: "Awn :( Discord has crashed more than five times, not attempting to recover.",
|
body: "Awn :( Discord has crashed more than five times, not attempting to recover.",
|
||||||
|
noPersist: true,
|
||||||
});
|
});
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|
||||||
|
@ -111,6 +112,7 @@ export default definePlugin({
|
||||||
color: "#eed202",
|
color: "#eed202",
|
||||||
title: "Discord has crashed!",
|
title: "Discord has crashed!",
|
||||||
body: "Attempting to recover...",
|
body: "Attempting to recover...",
|
||||||
|
noPersist: true,
|
||||||
});
|
});
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,7 +116,8 @@ function initWs(isManual = false) {
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Dev Companion Error",
|
title: "Dev Companion Error",
|
||||||
body: (e as ErrorEvent).message || "No Error Message",
|
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({
|
showNotification({
|
||||||
title: "Dev Companion Disconnected",
|
title: "Dev Companion Disconnected",
|
||||||
body: e.reason || "No Reason provided",
|
body: e.reason || "No Reason provided",
|
||||||
color: "var(--status-danger, red)"
|
color: "var(--status-danger, red)",
|
||||||
|
noPersist: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -24,10 +24,12 @@ export let useState: typeof React.useState;
|
||||||
export let useEffect: typeof React.useEffect;
|
export let useEffect: typeof React.useEffect;
|
||||||
export let useMemo: typeof React.useMemo;
|
export let useMemo: typeof React.useMemo;
|
||||||
export let useRef: typeof React.useRef;
|
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");
|
export const ReactDOM: typeof import("react-dom") & typeof import("react-dom/client") = findByPropsLazy("createPortal", "render");
|
||||||
|
|
||||||
waitFor("useState", m => {
|
waitFor("useState", m => {
|
||||||
React = m;
|
React = m;
|
||||||
({ useEffect, useState, useMemo, useRef } = React);
|
({ useEffect, useState, useMemo, useRef, useReducer, useCallback } = React);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue