Notification Log: fix lag if there are too many entries (#3584)

Use Discord's lazy list implementation for only rendering what's on screen
This commit is contained in:
V 2025-08-05 19:20:28 +02:00 committed by GitHub
parent 1ebd412392
commit 6380111f32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 106 additions and 40 deletions

View file

@ -474,19 +474,66 @@ export type MaskedLink = ComponentType<PropsWithChildren<{
channelId?: string;
}>>;
export type ScrollerThin = ComponentType<PropsWithChildren<{
export interface ScrollerBaseProps {
className?: string;
style?: CSSProperties;
dir?: "ltr";
orientation?: "horizontal" | "vertical" | "auto";
paddingFix?: boolean;
fade?: boolean;
onClose?(): void;
onScroll?(): void;
}
export type ScrollerThin = ComponentType<PropsWithChildren<ScrollerBaseProps & {
orientation?: "horizontal" | "vertical" | "auto";
fade?: boolean;
}>>;
interface BaseListItem {
anchorId: any;
listIndex: number;
offsetTop: number;
section: number;
}
interface ListSection extends BaseListItem {
type: "section";
}
interface ListRow extends BaseListItem {
type: "row";
row: number;
rowIndex: number;
}
export type ListScrollerThin = ComponentType<ScrollerBaseProps & {
sections: number[];
renderSection?: (item: ListSection) => React.ReactNode;
renderRow: (item: ListRow) => React.ReactNode;
renderFooter?: (item: any) => React.ReactNode;
renderSidebar?: (listVisible: boolean, sidebarVisible: boolean) => React.ReactNode;
wrapSection?: (section: number, children: React.ReactNode) => React.ReactNode;
sectionHeight: number;
rowHeight: number;
footerHeight?: number;
sidebarHeight?: number;
chunkSize?: number;
paddingTop?: number;
paddingBottom?: number;
fade?: boolean;
onResize?: Function;
getAnchorId?: any;
innerTag?: string;
innerId?: string;
innerClassName?: string;
innerRole?: string;
innerAriaLabel?: string;
// Yes, Discord uses this casing
innerAriaMultiselectable?: boolean;
innerAriaOrientation?: "vertical" | "horizontal";
}>;
export type Clickable = <T extends "a" | "div" | "span" | "li" = "div">(props: PropsWithChildren<ComponentPropsWithRef<T>> & {
tag?: T;
}) => ReactNode;

View file

@ -104,9 +104,7 @@ export default ErrorBoundary.wrap(function NotificationComponent({
</svg>
</button>
</div>
<div>
{richBody ?? <p className="vc-notification-p">{body}</p>}
</div>
{richBody ?? <p className="vc-notification-p">{body}</p>}
</div>
</div>
{image && <img className="vc-notification-img" src={image} alt="" />}

View file

@ -21,9 +21,9 @@ import { Settings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Flex } from "@components/Flex";
import { openNotificationSettingsModal } from "@components/settings/tabs/vencord/NotificationSettings";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { closeModal, ModalCloseButton, 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 { Alerts, Button, Forms, ListScrollerThin, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
import { nanoid } from "nanoid";
import type { DispatchWithoutAction } from "react";
@ -103,21 +103,9 @@ export function useLogs() {
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}>
<div className={cl("wrapper", { removing })}>
<NotificationComponent
{...data}
permanent={true}
@ -129,13 +117,13 @@ function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
setTimeout(() => deleteNotification(data.timestamp), 200);
}}
richBody={
<div className={cl("body")}>
{data.body}
<div className={cl("body-wrapper")}>
<div className={cl("body")}>{data.body}</div>
<Timestamp timestamp={new Date(data.timestamp)} className={cl("timestamp")} />
</div>
}
/>
</div>
</div >
);
}
@ -151,9 +139,14 @@ export function NotificationLog({ log, pending }: { log: PersistentNotificationD
);
return (
<div className={cl("container")}>
{log.map(n => <NotificationEntry data={n} key={n.id} />)}
</div>
<ListScrollerThin
className={cl("container")}
sections={[log.length]}
sectionHeight={0}
rowHeight={120}
renderSection={() => null}
renderRow={item => <NotificationEntry data={log[item.row]} key={log[item.row].id} />}
/>
);
}
@ -161,15 +154,15 @@ function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void
const [log, pending] = useLogs();
return (
<ModalRoot {...modalProps} size={ModalSize.LARGE}>
<ModalRoot {...modalProps} size={ModalSize.LARGE} className={cl("modal")}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Log</Text>
<ModalCloseButton onClick={close} />
</ModalHeader>
<ModalContent>
<div style={{ width: "100%" }}>
<NotificationLog log={log} pending={pending} />
</ModalContent>
</div>
<ModalFooter>
<Flex>

View file

@ -32,6 +32,7 @@
.vc-notification-content {
width: 100%;
overflow: hidden;
}
.vc-notification-header {
@ -81,6 +82,11 @@
width: 100%;
}
.vc-notification-log-modal {
max-width: 962px;
width: clamp(var(--modal-width-large, 800px), 962px, 85vw);
}
.vc-notification-log-empty {
height: 218px;
background: url("/assets/b36de980b174d7b798c89f35c116e5c6.svg") center no-repeat;
@ -88,19 +94,23 @@
}
.vc-notification-log-container {
display: flex;
flex-direction: column;
padding: 1em;
overflow: hidden;
max-height: min(750px, 75vh);
width: 100%;
}
.vc-notification-log-wrapper {
height: 120px;
width: 100%;
padding-bottom: 16px;
box-sizing: border-box;
transition: 200ms ease;
transition-property: height, opacity;
}
.vc-notification-log-wrapper:not(:last-child) {
margin-bottom: 1em;
/* stylelint-disable-next-line no-descending-specificity */
.vc-notification-root {
height: 104px;
}
}
.vc-notification-log-removing {
@ -109,9 +119,18 @@
margin-bottom: 1em;
}
.vc-notification-log-body {
.vc-notification-log-body-wrapper {
display: flex;
flex-direction: column;
width: 100%;
box-sizing: border-box;
}
.vc-notification-log-body {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
line-height: 1.2em;
}
.vc-notification-log-timestamp {
@ -123,4 +142,4 @@
.vc-notification-log-danger-btn {
color: var(--white-500);
background-color: var(--button-danger-background);
}
}

View file

@ -70,14 +70,23 @@ export const ColorPicker = waitForComponent<t.ColorPicker>("ColorPicker", filter
export const UserSummaryItem = waitForComponent("UserSummaryItem", filters.componentByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
export let createScroller: (scrollbarClassName: string, fadeClassName: string, customThemeClassName: string) => t.ScrollerThin;
export let createListScroller: (scrollBarClassName: string, fadeClassName: string, someOtherClassIdkMan: string, resizeObserverClass: typeof ResizeObserver) => t.ListScrollerThin;
export let scrollerClasses: Record<string, string>;
export let listScrollerClasses: Record<string, string>;
waitFor(filters.byCode('="ltr",orientation:', "customTheme:", "forwardRef"), m => createScroller = m);
waitFor(filters.byCode("getScrollerNode:", "resizeObserver:", "sectionHeight:"), m => createListScroller = m);
waitFor(["thin", "auto", "customTheme"], m => scrollerClasses = m);
waitFor(m => m.thin && m.auto && !m.customTheme, m => listScrollerClasses = m);
export const ScrollerNone = LazyComponent(() => createScroller(scrollerClasses.none, scrollerClasses.fade, scrollerClasses.customTheme));
export const ScrollerThin = LazyComponent(() => createScroller(scrollerClasses.thin, scrollerClasses.fade, scrollerClasses.customTheme));
export const ScrollerAuto = LazyComponent(() => createScroller(scrollerClasses.auto, scrollerClasses.fade, scrollerClasses.customTheme));
export const ListScrollerNone = LazyComponent(() => createListScroller(listScrollerClasses.none, listScrollerClasses.fade, "", ResizeObserver));
export const ListScrollerThin = LazyComponent(() => createListScroller(listScrollerClasses.thin, listScrollerClasses.fade, "", ResizeObserver));
export const ListScrollerAuto = LazyComponent(() => createListScroller(listScrollerClasses.auto, listScrollerClasses.fade, "", ResizeObserver));
const { FocusLock_ } = mapMangledModuleLazy('document.getElementById("app-mount"))', {
FocusLock_: filters.componentByCode(".containerRef")
}) as {