mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-08-08 17:23:18 +02:00
Fix articles view freezing on load
This commit is contained in:
5
.changeset/tricky-tigers-care.md
Normal file
5
.changeset/tricky-tigers-care.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Fix articles view freezing on load
|
@@ -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 };
|
||||
}
|
||||
|
@@ -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} />
|
||||
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user