Fix articles view freezing on load

This commit is contained in:
hzrd149
2025-04-22 10:33:59 -05:00
parent 0c9756d415
commit 598b424c2b
4 changed files with 148 additions and 64 deletions

View File

@@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Fix articles view freezing on load

View File

@@ -1,28 +1,91 @@
import { useLayoutEffect, useRef } from "react";
import { logger } from "applesauce-core";
import { RefCallback, useCallback, useEffect, useLayoutEffect, useRef } from "react";
import { useLocation } from "react-router-dom";
import { FixedSizeList } from "react-window";
export default function useScrollRestoreRef<T extends HTMLElement = HTMLDivElement>(name = "default") {
const ref = useRef<T | null>(null);
const log = logger.extend("useScrollRestoreRef");
function useScrollKey(name = "default") {
const location = useLocation();
const key = "scroll_" + name + "_" + location.key + location.pathname;
useLayoutEffect(() => {
const container = ref.current;
// Restore scroll position on mount
if (container) {
const savedScroll = sessionStorage.getItem(key);
if (savedScroll) container.scrollTop = parseInt(savedScroll, 10);
return "scroll_" + name + "_" + location.key + location.pathname;
}
// Save scroll position before unmounting
// Save the scroll position when the component unmounts
function useSaveScrollPosition<T extends HTMLElement = HTMLDivElement>(key: string, ref: React.RefObject<T>) {
useLayoutEffect(() => {
return () => {
if (!container) return;
if (ref.current) {
const position = ref.current?.scrollTop;
const y = container?.scrollTop;
if (y) sessionStorage.setItem(key, String(y));
else sessionStorage.removeItem(key);
if (position) {
log("Saving scroll position", key, position);
sessionStorage.setItem(key, String(position));
} else {
log("Removing scroll position", key);
sessionStorage.removeItem(key);
}
}
};
}, [key]);
return ref;
}
/** Scroll restoration hook for a html element */
export default function useScrollRestoreRef<T extends HTMLElement = HTMLDivElement>(
name = "default",
): RefCallback<T | null> {
const ref = useRef<T | null>(null);
const key = useScrollKey(name);
// Save the scroll position when the component unmounts
useSaveScrollPosition(key, ref);
// Return ref callback for restoring scroll position on mount
return useCallback(
(container: T | null) => {
ref.current = container;
if (container) {
const savedScroll = sessionStorage.getItem(key);
if (savedScroll) {
log("Restoring scroll position", container, parseInt(savedScroll, 10));
container.scrollTop = parseInt(savedScroll, 10);
}
}
},
[key],
);
}
/** Scroll restoration hook for react-window lists */
export function useVirtualListScrollRestore(name = "default") {
const ref = useRef<HTMLDivElement | null>(null);
const key = useScrollKey(name);
// Save the scroll position when the component unmounts
useSaveScrollPosition(key, ref);
// Callback for saving the scroll element
const outerRef = useCallback(
(container: HTMLDivElement | null) => {
ref.current = container;
if (container) log("Found scroll container", container);
},
[key],
);
// Restore scroll position on mount
const listRef = useCallback(
(list: FixedSizeList | null) => {
if (list) {
const savedScroll = sessionStorage.getItem(key);
if (savedScroll) {
log("Restoring scroll position", list, parseInt(savedScroll, 10));
list.scrollTo(parseInt(savedScroll, 10));
}
}
},
[key],
);
return { outerRef, ref: listRef };
}

View File

@@ -1,24 +1,24 @@
import { memo } from "react";
import { Box, Card, Flex, Heading, LinkBox, Spacer, Text } from "@chakra-ui/react";
import { Box, Card, CardProps, Flex, Heading, LinkBox, Spacer, Text } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import { memo } from "react";
import { Link as RouterLink } from "react-router-dom";
import HoverLinkOverlay from "../../../components/hover-link-overlay";
import ZapBubbles from "../../../components/note/timeline-note/components/zap-bubbles";
import Timestamp from "../../../components/timestamp";
import UserAvatar from "../../../components/user/user-avatar";
import UserName from "../../../components/user/user-name";
import {
getArticleImage,
getArticlePublishDate,
getArticleSummary,
getArticleTitle,
} from "../../../helpers/nostr/long-form";
import UserAvatar from "../../../components/user/user-avatar";
import UserName from "../../../components/user/user-name";
import Timestamp from "../../../components/timestamp";
import HoverLinkOverlay from "../../../components/hover-link-overlay";
import useShareableEventAddress from "../../../hooks/use-shareable-event-address";
import ArticleTags from "./article-tags";
import ArticleMenu from "./article-menu";
import ZapBubbles from "../../../components/note/timeline-note/components/zap-bubbles";
import ArticleTags from "./article-tags";
const ArticleCard = memo(({ article }: { article: NostrEvent }) => {
const ArticleCard = memo(({ article, ...props }: { article: NostrEvent } & Omit<CardProps, "children">) => {
const image = getArticleImage(article);
const title = getArticleTitle(article);
const published = getArticlePublishDate(article);
@@ -27,7 +27,7 @@ const ArticleCard = memo(({ article }: { article: NostrEvent }) => {
const naddr = useShareableEventAddress(article);
return (
<Card as={LinkBox} display="block" p="2" position="relative" variant="ghost">
<Card as={LinkBox} display="block" p="2" position="relative" variant="ghost" overflow="hidden" {...props}>
<Flex gap="2" alignItems="center" mb="2">
<UserAvatar pubkey={article.pubkey} size="sm" />
<UserName pubkey={article.pubkey} />
@@ -38,12 +38,13 @@ const ArticleCard = memo(({ article }: { article: NostrEvent }) => {
{image && (
<Box
aspectRatio={16 / 9}
aspectRatio={{ base: 3, lg: 16 / 9 }}
backgroundImage={image}
backgroundPosition="center"
backgroundRepeat="no-repeat"
backgroundSize="cover"
float={{ base: undefined, lg: "right" }}
w={{ base: "full", lg: "initial" }}
mx={{ base: "auto", lg: 2 }}
mb={{ base: "2", lg: undefined }}
minH="10rem"
@@ -55,7 +56,7 @@ const ArticleCard = memo(({ article }: { article: NostrEvent }) => {
{title}
</HoverLinkOverlay>
</Heading>
<Text noOfLines={5}>{summary}</Text>
<Text noOfLines={4}>{summary}</Text>
<ArticleTags article={article} />

View File

@@ -1,25 +1,38 @@
import { useCallback, useMemo } from "react";
import { Filter, kinds, NostrEvent } from "nostr-tools";
import { Flex, Heading, Spacer } from "@chakra-ui/react";
import { Box, Flex } from "@chakra-ui/react";
import { getEventUID } from "applesauce-core/helpers";
import { Filter, kinds, NostrEvent } from "nostr-tools";
import { useCallback, useMemo } from "react";
import AutoSizer from "react-virtualized-auto-sizer";
import { FixedSizeList as List, ListChildComponentProps } from "react-window";
import VerticalPageLayout from "../../components/vertical-page-layout";
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
import { useReadRelays } from "../../hooks/use-client-relays";
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status";
import ArticleCard from "./components/article-card";
import { ErrorBoundary } from "../../components/error-boundary";
import SimpleView from "../../components/layout/presets/simple-view";
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
import { getArticleTitle } from "../../helpers/nostr/long-form";
import { ErrorBoundary } from "../../components/error-boundary";
import useMaxPageWidth from "../../hooks/use-max-page-width";
import { useReadRelays } from "../../hooks/use-client-relays";
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
import { useVirtualListScrollRestore } from "../../hooks/use-scroll-restore";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
import ArticleCard from "./components/article-card";
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
function ArticleRow({ index, style, data }: ListChildComponentProps<NostrEvent[]>) {
return (
<Box style={style} pb="2">
<ErrorBoundary key={getEventUID(data[index])}>
<ArticleCard article={data[index]} h="full" mx="auto" maxW="6xl" w="full" />
</ErrorBoundary>
</Box>
);
}
function ArticlesHomePage() {
const relays = useReadRelays();
const userMuteFilter = useClientSideMuteFilter();
const scroll = useVirtualListScrollRestore("manual");
const eventFilter = useCallback(
(event: NostrEvent) => {
@@ -42,28 +55,30 @@ function ArticlesHomePage() {
});
const callback = useTimelineCurserIntersectionCallback(loader);
const maxWidth = useMaxPageWidth();
const rowHight = useBreakpointValue({ base: 400, lg: 250 }) || 250;
return (
<VerticalPageLayout maxW={maxWidth} mx="auto">
<Flex gap="2">
<Heading>Articles</Heading>
<PeopleListSelection />
<Spacer />
{/* <Button as={RouterLink} to="/articles/new" colorScheme="primary" leftIcon={<Plus boxSize={6} />}>
New
</Button> */}
</Flex>
<IntersectionObserverProvider callback={callback}>
{articles.map((article) => (
<ErrorBoundary key={getEventUID(article)}>
<ArticleCard article={article} />
</ErrorBoundary>
))}
<TimelineActionAndStatus loader={loader} />
<SimpleView title="Articles" scroll={false} actions={<PeopleListSelection ms="auto" />} flush>
{/* Container */}
<Flex direction="column" flex={1}>
<AutoSizer>
{({ height, width }) => (
<List
itemCount={articles.length}
itemSize={rowHight}
itemData={articles}
width={width}
height={height}
{...scroll}
>
{ArticleRow}
</List>
)}
</AutoSizer>
</Flex>
</SimpleView>
</IntersectionObserverProvider>
</VerticalPageLayout>
);
}