From b7d2c599e12e3b33def0e97e525e22109139b408 Mon Sep 17 00:00:00 2001 From: kieran Date: Fri, 26 Apr 2024 14:06:21 +0100 Subject: [PATCH] feat: new timeline render flow --- packages/app/src/Components/Feed/Timeline.tsx | 3 +- .../app/src/Components/Feed/TimelineChunk.tsx | 44 +++++++ .../src/Components/Feed/TimelineFollows.tsx | 120 ++++++------------ packages/app/src/Hooks/useTimelineChunks.ts | 30 +++++ 4 files changed, 116 insertions(+), 81 deletions(-) create mode 100644 packages/app/src/Components/Feed/TimelineChunk.tsx create mode 100644 packages/app/src/Hooks/useTimelineChunks.ts diff --git a/packages/app/src/Components/Feed/Timeline.tsx b/packages/app/src/Components/Feed/Timeline.tsx index 2d377ca3..e98b2405 100644 --- a/packages/app/src/Components/Feed/Timeline.tsx +++ b/packages/app/src/Components/Feed/Timeline.tsx @@ -1,5 +1,6 @@ import "./Timeline.css"; +import { unixNow } from "@snort/shared"; import { socialGraphInstance, TaggedNostrEvent } from "@snort/system"; import { useCallback, useMemo, useState } from "react"; @@ -28,7 +29,7 @@ export interface TimelineProps { */ const Timeline = (props: TimelineProps) => { const login = useLogin(); - const [openedAt] = useHistoryState(Math.floor(Date.now() / 1000), "openedAt"); + const [openedAt] = useHistoryState(unixNow(), "openedAt"); const feedOptions = useMemo( () => ({ method: props.method, diff --git a/packages/app/src/Components/Feed/TimelineChunk.tsx b/packages/app/src/Components/Feed/TimelineChunk.tsx new file mode 100644 index 00000000..8d5dbf87 --- /dev/null +++ b/packages/app/src/Components/Feed/TimelineChunk.tsx @@ -0,0 +1,44 @@ +import { NostrEvent, RequestBuilder } from "@snort/system"; +import { useRequestBuilder } from "@snort/system-react"; +import { ReactNode, useMemo } from "react"; + +import { WindowChunk } from "@/Hooks/useTimelineChunks"; + +import { DisplayAs } from "./DisplayAsSelector"; +import { TimelineFragment } from "./TimelineFragment"; + +export interface TimelineChunkProps { + id: string; + chunk: WindowChunk, + builder: (rb: RequestBuilder) => void; + noteFilter?: (ev: NostrEvent) => boolean; + noteRenderer?: (ev: NostrEvent) => ReactNode; + noteOnClick?: (ev: NostrEvent) => void; + displayAs?: DisplayAs; + showDisplayAsSelector?: boolean; +} + +/** + * Simple chunk of a timeline using absoliute time ranges + */ +export default function TimelineChunk(props: TimelineChunkProps) { + const sub = useMemo(() => { + const rb = new RequestBuilder(`timeline-chunk:${props.id}:${props.chunk.since}-${props.chunk.until}`); + props.builder(rb); + for (const f of rb.filterBuilders) { + f.since(props.chunk.since).until(props.chunk.until); + } + return rb; + }, [props.id, props.chunk, props.builder]); + + const feed = useRequestBuilder(sub); + + return props.noteFilter?.(a) ?? true), + refTime: props.chunk.until + }} + noteOnClick={props.noteOnClick} + noteRenderer={props.noteRenderer} + /> +} \ No newline at end of file diff --git a/packages/app/src/Components/Feed/TimelineFollows.tsx b/packages/app/src/Components/Feed/TimelineFollows.tsx index 667996b2..a987373d 100644 --- a/packages/app/src/Components/Feed/TimelineFollows.tsx +++ b/packages/app/src/Components/Feed/TimelineFollows.tsx @@ -1,16 +1,18 @@ import "./Timeline.css"; -import { EventKind, NostrEvent, TaggedNostrEvent } from "@snort/system"; -import { ReactNode, useCallback, useMemo, useState } from "react"; -import { Link } from "react-router-dom"; +import { unixNow } from "@snort/shared"; +import { EventKind, NostrEvent, RequestBuilder } from "@snort/system"; +import { ReactNode, useState } from "react"; import { DisplayAs, DisplayAsSelector } from "@/Components/Feed/DisplayAsSelector"; -import { TimelineRenderer } from "@/Components/Feed/TimelineRenderer"; -import useTimelineFeed, { TimelineFeedOptions, TimelineSubject } from "@/Feed/TimelineFeed"; import useFollowsControls from "@/Hooks/useFollowControls"; import useHistoryState from "@/Hooks/useHistoryState"; import useLogin from "@/Hooks/useLogin"; -import { dedupeByPubkey } from "@/Utils"; +import useTimelineChunks from "@/Hooks/useTimelineChunks"; +import { Hour } from "@/Utils/Const"; + +import { AutoLoadMore } from "../Event/LoadMore"; +import TimelineChunk from "./TimelineChunk"; export interface TimelineFollowsProps { postsOnly: boolean; @@ -23,7 +25,7 @@ export interface TimelineFollowsProps { } /** - * A list of notes by "subject" + * A list of notes by your follows */ const TimelineFollows = (props: TimelineFollowsProps) => { const login = useLogin(s => ({ @@ -33,83 +35,41 @@ const TimelineFollows = (props: TimelineFollowsProps) => { })); const displayAsInitial = props.displayAs ?? login.feedDisplayAs ?? "list"; const [displayAs, setDisplayAs] = useState(displayAsInitial); - const [openedAt] = useHistoryState(Math.floor(Date.now() / 1000), "openedAt"); + const [openedAt] = useHistoryState(unixNow(), "openedAt"); const { isFollowing, followList } = useFollowsControls(); - const subject = useMemo( - () => - ({ - type: "pubkey", - items: followList, - discriminator: login.publicKey?.slice(0, 12), - extra: rb => { - if (login.tags.length > 0) { - rb.withFilter().kinds([EventKind.TextNote, EventKind.Repost]).tags(login.tags); - } - }, - }) as TimelineSubject, - [login.publicKey, followList, login.tags], - ); - const feed = useTimelineFeed(subject, { method: "TIME_RANGE", now: openedAt } as TimelineFeedOptions); + const { chunks, showMore } = useTimelineChunks({ + now: openedAt, + firstChunkSize: Hour * 2 + }); - // TODO allow reposts: - const postsOnly = useCallback( - (a: NostrEvent) => (props.postsOnly ? !a.tags.some(b => b[0] === "e" || b[0] === "a") : true), - [props.postsOnly], - ); + const builder = (rb: RequestBuilder) => { + rb.withFilter() + .authors(followList) + .kinds([EventKind.TextNote, EventKind.Repost, EventKind.Polls]); + }; - const filterPosts = useCallback( - (nts: Array) => { - const a = nts.filter(a => a.kind !== EventKind.LiveEvent); - return a - ?.filter(postsOnly) - .filter(a => props.noteFilter?.(a) ?? true) - .filter(a => isFollowing(a.pubkey) || a.tags.filter(a => a[0] === "t").length < 5); - }, - [postsOnly, props.noteFilter, isFollowing], - ); + const filterEvents = (a: NostrEvent) => + (props.noteFilter?.(a) ?? true) + && (props.postsOnly ? !a.tags.some(b => b[0] === "e" || b[0] === "a") : true) + && (isFollowing(a.pubkey) || a.tags.filter(a => a[0] === "t").length < 5); - const mainFeed = useMemo(() => { - return filterPosts(feed.main ?? []); - }, [feed.main, filterPosts]); - - const latestFeed = useMemo(() => { - return filterPosts(feed.latest ?? []); - }, [feed.latest]); - - const latestAuthors = useMemo(() => { - return dedupeByPubkey(latestFeed).map(e => e.pubkey); - }, [latestFeed]); - - function onShowLatest(scrollToTop = false) { - feed.showLatest(); - if (scrollToTop) { - window.scrollTo(0, 0); - } - } - - return ( - <> - setDisplayAs(displayAs)} - /> - onShowLatest(t)} - noteOnClick={props.noteOnClick} - noteRenderer={props.noteRenderer} - noteContext={e => { - if (typeof e.context === "string") { - return {`#${e.context}`}; - } - }} - displayAs={displayAs} - loadMore={() => feed.loadMore()} - /> - - ); + return <> + setDisplayAs(displayAs)} + /> + {chunks.map(c => )} + showMore()} /> + ; }; export default TimelineFollows; diff --git a/packages/app/src/Hooks/useTimelineChunks.ts b/packages/app/src/Hooks/useTimelineChunks.ts new file mode 100644 index 00000000..bff3a00a --- /dev/null +++ b/packages/app/src/Hooks/useTimelineChunks.ts @@ -0,0 +1,30 @@ +import { useState } from "react"; + +export interface WindowChunk { + since: number; + until: number; +} + +export default function useTimelineChunks(opt: { window?: number; firstChunkSize?: number, now: number }) { + const [windowSize] = useState(opt.window ?? (60 * 60 * 2)); + const [windows, setWindows] = useState(1); + + const chunks: Array = []; + for (let x = 0; x < windows; x++) { + // offset from now going backwards in time + const offset = opt.now - (windowSize * (x - 1)); + const size = x === 0 && opt.firstChunkSize ? opt.firstChunkSize : windowSize; + chunks.push({ + since: offset - size, + until: offset + }); + } + + return { + now: opt.now, + chunks, + showMore: () => { + setWindows(s => s + 1); + }, + }; +}