mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-10-09 20:33:03 +02:00
129 lines
4.4 KiB
TypeScript
129 lines
4.4 KiB
TypeScript
import { ReactNode, memo, useEffect, useState } from "react";
|
|
import { Box, Button, Text } from "@chakra-ui/react";
|
|
import { Kind } from "nostr-tools";
|
|
import dayjs from "dayjs";
|
|
|
|
import useSubject from "../../../hooks/use-subject";
|
|
import TimelineLoader from "../../../classes/timeline-loader";
|
|
import RepostNote from "./repost-note";
|
|
import { Note } from "../../note";
|
|
import { NostrEvent } from "../../../types/nostr-event";
|
|
import { STREAM_KIND } from "../../../helpers/nostr/stream";
|
|
import StreamNote from "./stream-note";
|
|
import { ErrorBoundary } from "../../error-boundary";
|
|
import EmbeddedArticle from "../../embed-event/event-types/embedded-article";
|
|
import { getEventUID, isReply } from "../../../helpers/nostr/events";
|
|
import ReplyNote from "./reply-note";
|
|
import RelayRecommendation from "./relay-recommendation";
|
|
import { ExtendedIntersectionObserverEntry, useIntersectionObserver } from "../../../providers/intersection-observer";
|
|
import BadgeAwardCard from "../../../views/badges/components/badge-award-card";
|
|
|
|
function RenderEvent({ event }: { event: NostrEvent }) {
|
|
let content: ReactNode | null = null;
|
|
switch (event.kind) {
|
|
case Kind.Text:
|
|
content = isReply(event) ? <ReplyNote event={event} /> : <Note event={event} showReplyButton />;
|
|
break;
|
|
case Kind.Repost:
|
|
content = <RepostNote event={event} />;
|
|
break;
|
|
case Kind.Article:
|
|
content = <EmbeddedArticle article={event} />;
|
|
break;
|
|
case STREAM_KIND:
|
|
content = <StreamNote event={event} />;
|
|
break;
|
|
case Kind.RecommendRelay:
|
|
content = <RelayRecommendation event={event} />;
|
|
break;
|
|
case Kind.BadgeAward:
|
|
content = <BadgeAwardCard award={event} />;
|
|
default:
|
|
content = <Text>Unknown event kind: {event.kind}</Text>;
|
|
break;
|
|
}
|
|
|
|
return content && <ErrorBoundary>{content}</ErrorBoundary>;
|
|
}
|
|
const RenderEventMemo = memo(RenderEvent);
|
|
|
|
const PRELOAD_NOTES = 5;
|
|
function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
|
|
const notesArray = useSubject(timeline.timeline);
|
|
const [latest, setLatest] = useState(() => dayjs().unix());
|
|
const { subject } = useIntersectionObserver();
|
|
|
|
const [minDate, setMinDate] = useState(timeline.timeline.value[PRELOAD_NOTES]?.created_at ?? 0);
|
|
|
|
// reset the latest and minDate when timeline changes
|
|
useEffect(() => {
|
|
setLatest(dayjs().unix());
|
|
setMinDate(timeline.timeline.value[PRELOAD_NOTES]?.created_at ?? 0);
|
|
}, [timeline, setMinDate, setLatest]);
|
|
|
|
const newNotes: NostrEvent[] = [];
|
|
const notes: NostrEvent[] = [];
|
|
for (const note of notesArray) {
|
|
if (note.created_at > latest) newNotes.push(note);
|
|
else if (note.created_at > minDate) notes.push(note);
|
|
}
|
|
|
|
const [intersectionEntryCache] = useState(() => new Map<string, IntersectionObserverEntry>());
|
|
useEffect(() => {
|
|
const listener = (entities: ExtendedIntersectionObserverEntry[]) => {
|
|
for (const entity of entities) entity.id && intersectionEntryCache.set(entity.id, entity.entry);
|
|
|
|
let min: number = Infinity;
|
|
let preload = PRELOAD_NOTES;
|
|
let foundVisible = false;
|
|
for (const event of timeline.timeline.value) {
|
|
if (event.created_at > latest) continue;
|
|
const entry = intersectionEntryCache.get(getEventUID(event));
|
|
if (!entry || !entry.isIntersecting) {
|
|
if (foundVisible) {
|
|
// found and event below the view
|
|
if (preload-- < 0) break;
|
|
if (event.created_at < min) min = event.created_at;
|
|
} else {
|
|
// found and event above the view
|
|
continue;
|
|
}
|
|
} else {
|
|
// found visible event
|
|
foundVisible = true;
|
|
}
|
|
}
|
|
|
|
setMinDate((v) => Math.min(v, min));
|
|
};
|
|
|
|
subject.subscribe(listener);
|
|
return () => {
|
|
subject.unsubscribe(listener);
|
|
};
|
|
}, [setMinDate, intersectionEntryCache, latest, timeline]);
|
|
|
|
return (
|
|
<>
|
|
{newNotes.length > 0 && (
|
|
<Box h="0" overflow="visible" w="full" zIndex={100} display="flex" position="relative">
|
|
<Button
|
|
onClick={() => setLatest(timeline.timeline.value[0].created_at + 10)}
|
|
colorScheme="primary"
|
|
size="lg"
|
|
mx="auto"
|
|
w={["50%", null, "30%"]}
|
|
>
|
|
Show {newNotes.length} new notes
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
{notes.map((note) => (
|
|
<RenderEventMemo key={note.id} event={note} />
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default memo(GenericNoteTimeline);
|