feat: new timeline render flow
This commit is contained in:
@ -1,5 +1,6 @@
|
|||||||
import "./Timeline.css";
|
import "./Timeline.css";
|
||||||
|
|
||||||
|
import { unixNow } from "@snort/shared";
|
||||||
import { socialGraphInstance, TaggedNostrEvent } from "@snort/system";
|
import { socialGraphInstance, TaggedNostrEvent } from "@snort/system";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ export interface TimelineProps {
|
|||||||
*/
|
*/
|
||||||
const Timeline = (props: TimelineProps) => {
|
const Timeline = (props: TimelineProps) => {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const [openedAt] = useHistoryState(Math.floor(Date.now() / 1000), "openedAt");
|
const [openedAt] = useHistoryState(unixNow(), "openedAt");
|
||||||
const feedOptions = useMemo(
|
const feedOptions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
method: props.method,
|
method: props.method,
|
||||||
|
44
packages/app/src/Components/Feed/TimelineChunk.tsx
Normal file
44
packages/app/src/Components/Feed/TimelineChunk.tsx
Normal file
@ -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 <TimelineFragment
|
||||||
|
frag={{
|
||||||
|
events: feed.filter(a => props.noteFilter?.(a) ?? true),
|
||||||
|
refTime: props.chunk.until
|
||||||
|
}}
|
||||||
|
noteOnClick={props.noteOnClick}
|
||||||
|
noteRenderer={props.noteRenderer}
|
||||||
|
/>
|
||||||
|
}
|
@ -1,16 +1,18 @@
|
|||||||
import "./Timeline.css";
|
import "./Timeline.css";
|
||||||
|
|
||||||
import { EventKind, NostrEvent, TaggedNostrEvent } from "@snort/system";
|
import { unixNow } from "@snort/shared";
|
||||||
import { ReactNode, useCallback, useMemo, useState } from "react";
|
import { EventKind, NostrEvent, RequestBuilder } from "@snort/system";
|
||||||
import { Link } from "react-router-dom";
|
import { ReactNode, useState } from "react";
|
||||||
|
|
||||||
import { DisplayAs, DisplayAsSelector } from "@/Components/Feed/DisplayAsSelector";
|
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 useFollowsControls from "@/Hooks/useFollowControls";
|
||||||
import useHistoryState from "@/Hooks/useHistoryState";
|
import useHistoryState from "@/Hooks/useHistoryState";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
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 {
|
export interface TimelineFollowsProps {
|
||||||
postsOnly: boolean;
|
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 TimelineFollows = (props: TimelineFollowsProps) => {
|
||||||
const login = useLogin(s => ({
|
const login = useLogin(s => ({
|
||||||
@ -33,83 +35,41 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
|
|||||||
}));
|
}));
|
||||||
const displayAsInitial = props.displayAs ?? login.feedDisplayAs ?? "list";
|
const displayAsInitial = props.displayAs ?? login.feedDisplayAs ?? "list";
|
||||||
const [displayAs, setDisplayAs] = useState<DisplayAs>(displayAsInitial);
|
const [displayAs, setDisplayAs] = useState<DisplayAs>(displayAsInitial);
|
||||||
const [openedAt] = useHistoryState(Math.floor(Date.now() / 1000), "openedAt");
|
const [openedAt] = useHistoryState(unixNow(), "openedAt");
|
||||||
const { isFollowing, followList } = useFollowsControls();
|
const { isFollowing, followList } = useFollowsControls();
|
||||||
const subject = useMemo(
|
const { chunks, showMore } = useTimelineChunks({
|
||||||
() =>
|
now: openedAt,
|
||||||
({
|
firstChunkSize: Hour * 2
|
||||||
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);
|
|
||||||
|
|
||||||
// TODO allow reposts:
|
const builder = (rb: RequestBuilder) => {
|
||||||
const postsOnly = useCallback(
|
rb.withFilter()
|
||||||
(a: NostrEvent) => (props.postsOnly ? !a.tags.some(b => b[0] === "e" || b[0] === "a") : true),
|
.authors(followList)
|
||||||
[props.postsOnly],
|
.kinds([EventKind.TextNote, EventKind.Repost, EventKind.Polls]);
|
||||||
);
|
};
|
||||||
|
|
||||||
const filterPosts = useCallback(
|
const filterEvents = (a: NostrEvent) =>
|
||||||
(nts: Array<TaggedNostrEvent>) => {
|
(props.noteFilter?.(a) ?? true)
|
||||||
const a = nts.filter(a => a.kind !== EventKind.LiveEvent);
|
&& (props.postsOnly ? !a.tags.some(b => b[0] === "e" || b[0] === "a") : true)
|
||||||
return a
|
&& (isFollowing(a.pubkey) || a.tags.filter(a => a[0] === "t").length < 5);
|
||||||
?.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 mainFeed = useMemo(() => {
|
return <>
|
||||||
return filterPosts(feed.main ?? []);
|
<DisplayAsSelector
|
||||||
}, [feed.main, filterPosts]);
|
show={props.showDisplayAsSelector}
|
||||||
|
activeSelection={displayAs}
|
||||||
const latestFeed = useMemo(() => {
|
onSelect={(displayAs: DisplayAs) => setDisplayAs(displayAs)}
|
||||||
return filterPosts(feed.latest ?? []);
|
/>
|
||||||
}, [feed.latest]);
|
{chunks.map(c => <TimelineChunk
|
||||||
|
key={c.until}
|
||||||
const latestAuthors = useMemo(() => {
|
id="follows"
|
||||||
return dedupeByPubkey(latestFeed).map(e => e.pubkey);
|
chunk={c}
|
||||||
}, [latestFeed]);
|
builder={builder}
|
||||||
|
noteFilter={filterEvents}
|
||||||
function onShowLatest(scrollToTop = false) {
|
noteOnClick={props.noteOnClick}
|
||||||
feed.showLatest();
|
noteRenderer={props.noteRenderer}
|
||||||
if (scrollToTop) {
|
/>)}
|
||||||
window.scrollTo(0, 0);
|
<AutoLoadMore onClick={() => showMore()} />
|
||||||
}
|
</>;
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DisplayAsSelector
|
|
||||||
show={props.showDisplayAsSelector}
|
|
||||||
activeSelection={displayAs}
|
|
||||||
onSelect={(displayAs: DisplayAs) => setDisplayAs(displayAs)}
|
|
||||||
/>
|
|
||||||
<TimelineRenderer
|
|
||||||
frags={[{ events: mainFeed, refTime: 0 }]}
|
|
||||||
latest={latestAuthors}
|
|
||||||
showLatest={t => onShowLatest(t)}
|
|
||||||
noteOnClick={props.noteOnClick}
|
|
||||||
noteRenderer={props.noteRenderer}
|
|
||||||
noteContext={e => {
|
|
||||||
if (typeof e.context === "string") {
|
|
||||||
return <Link to={`/t/${e.context}`}>{`#${e.context}`}</Link>;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
displayAs={displayAs}
|
|
||||||
loadMore={() => feed.loadMore()}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TimelineFollows;
|
export default TimelineFollows;
|
||||||
|
30
packages/app/src/Hooks/useTimelineChunks.ts
Normal file
30
packages/app/src/Hooks/useTimelineChunks.ts
Normal file
@ -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<WindowChunk> = [];
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
Reference in New Issue
Block a user