refactor: inline hashtag posts

This commit is contained in:
Kieran 2023-11-14 10:38:02 +00:00
parent de1b982e93
commit f2a41cb474
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
7 changed files with 79 additions and 109 deletions

View File

@ -1,5 +1,5 @@
import "./Note.css";
import React from "react";
import { ReactNode } from "react";
import { EventKind, NostrEvent, TaggedNostrEvent } from "@snort/system";
import { NostrFileElement } from "Element/Event/NostrFileHeader";
import ZapstrEmbed from "Element/Embed/ZapstrEmbed";
@ -21,6 +21,7 @@ export interface NoteProps {
depth?: number;
searchedValue?: string;
threadChains?: Map<string, Array<NostrEvent>>;
context?: ReactNode;
options?: {
showHeader?: boolean;
showContextMenu?: boolean;

View File

@ -290,6 +290,7 @@ export function NoteInner(props: NoteProps) {
link={opt?.canClick === undefined ? undefined : ""}
/>
<div className="info">
{props.context}
{(options.showTime || options.showBookmarked) && (
<>
{options.showBookmarked && (

View File

@ -1,5 +1,6 @@
import "./Timeline.css";
import { ReactNode, useCallback, useContext, useMemo, useState, useSyncExternalStore } from "react";
import { Link } from "react-router-dom";
import { EventKind, NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system";
import { unixNow } from "@snort/shared";
import { SnortContext, useReactions } from "@snort/system-react";
@ -9,10 +10,9 @@ import useModeration from "Hooks/useModeration";
import { FollowsFeed } from "Cache";
import { LiveStreams } from "Element/LiveStreams";
import useLogin from "Hooks/useLogin";
import { TimelineFragment, TimelineRenderer } from "./TimelineFragment";
import { TimelineRenderer } from "./TimelineFragment";
import useHashtagsFeed from "Feed/HashtagsFeed";
import { ShowMoreInView } from "Element/Event/ShowMore";
import { HashTagHeader } from "Pages/HashTagsPage";
export interface TimelineFollowsProps {
postsOnly: boolean;
@ -42,6 +42,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
const { muted, isEventMuted } = useModeration();
const sortedFeed = useMemo(() => orderDescending(feed), [feed]);
const oldest = useMemo(() => sortedFeed.at(-1)?.created_at, [sortedFeed]);
const postsOnly = useCallback(
(a: NostrEvent) => (props.postsOnly ? !a.tags.some(b => b[0] === "e" || b[0] === "a") : true),
@ -61,32 +62,28 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
const mixin = useHashtagsFeed();
const mainFeed = useMemo(() => {
return filterPosts((sortedFeed ?? []).filter(a => a.created_at <= latest));
}, [sortedFeed, filterPosts, latest, login.follows.timestamp, mixin]);
}, [sortedFeed, filterPosts, latest, login.follows.timestamp]);
const hashTagsGroups = useMemo(() => {
const findHashTagContext = (a: NostrEvent) => {
const tag = a.tags.filter(a => a[0] === "t").find(a => login.tags.item.includes(a[1].toLowerCase()))?.[1];
if (tag) {
return <Link to={`/t/${tag}`}>{`#${tag}`}</Link>;
}
};
const mixinFiltered = useMemo(() => {
const mainFeedIds = new Set(mainFeed.map(a => a.id));
const included = new Set<string>();
return (mixin.data.data ?? [])
.filter(a => !mainFeedIds.has(a.id) && postsOnly(a))
.filter(a => !mainFeedIds.has(a.id) && postsOnly(a) && !isEventMuted(a))
.filter(a => a.tags.filter(a => a[0] === "t").length < 5)
.reduce(
(acc, v) => {
if (included.has(v.id)) return acc;
const tags = v.tags
.filter(a => a[0] === "t")
.map(v => v[1].toLocaleLowerCase())
.filter(a => mixin.hashtags.includes(a));
for (const t of tags) {
acc[t] ??= [];
acc[t].push(v);
break;
}
included.add(v.id);
return acc;
},
{} as Record<string, Array<TaggedNostrEvent>>,
.filter(a => a.created_at >= (oldest ?? unixNow()))
.map(
a =>
({
...a,
context: findHashTagContext(a),
}) as TaggedNostrEvent,
);
}, [mixin, mainFeed, postsOnly]);
}, [mixin, mainFeed, postsOnly, isEventMuted]);
const latestFeed = useMemo(() => {
return filterPosts((sortedFeed ?? []).filter(a => a.created_at > latest));
@ -111,7 +108,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
<>
{(props.liveStreams ?? true) && <LiveStreams evs={liveStreams} />}
<TimelineRenderer
frags={weaveTimeline(mainFeed, hashTagsGroups)}
frags={[{ events: orderDescending(mainFeed.concat(mixinFiltered)), refTime: latest }]}
related={reactions.data ?? []}
latest={latestAuthors}
showLatest={t => onShowLatest(t)}
@ -119,55 +116,9 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
noteRenderer={props.noteRenderer}
/>
{sortedFeed.length > 0 && (
<ShowMoreInView
onClick={async () => await FollowsFeed.loadMore(system, login, sortedFeed[sortedFeed.length - 1].created_at)}
/>
<ShowMoreInView onClick={async () => await FollowsFeed.loadMore(system, login, oldest ?? unixNow())} />
)}
</>
);
};
export default TimelineFollows;
function weaveTimeline(
main: Array<TaggedNostrEvent>,
hashtags: Record<string, Array<TaggedNostrEvent>>,
): Array<TimelineFragment> {
// always skip 5 posts from start to avoid heavy handed weaving
let skip = 5;
if (main.length < skip) {
skip = Math.min(skip, main.length - 1);
}
const frags = Object.entries(hashtags).map(([k, v]) => {
const take = v.slice(0, 5);
return {
title: (
<div className="bb p">
<HashTagHeader tag={k} />
</div>
),
events: take,
refTime: Math.min(
main.at(skip)?.created_at ?? unixNow(),
take.reduce((acc, v) => (acc > v.created_at ? acc : v.created_at), 0),
),
} as TimelineFragment;
});
if (main.length === 0) {
return frags;
}
return [
{
events: main.slice(0, skip),
refTime: main[0].created_at,
},
...frags,
{
events: main.slice(skip),
refTime: main[skip].created_at,
},
].sort((a, b) => (a.refTime > b.refTime ? -1 : 1));
}

View File

@ -1,11 +1,12 @@
import { ReactNode, useCallback } from "react";
import { FormattedMessage } from "react-intl";
import { useInView } from "react-intersection-observer";
import { TaggedNostrEvent } from "@snort/system";
import Note from "Element/Event/Note";
import ProfileImage from "Element/User/ProfileImage";
import Icon from "Icons/Icon";
import { findTag } from "SnortUtils";
import { ReactNode, useCallback } from "react";
import { useInView } from "react-intersection-observer";
import { FormattedMessage } from "react-intl";
export interface TimelineFragment {
events: Array<TaggedNostrEvent>;
@ -13,7 +14,7 @@ export interface TimelineFragment {
title?: ReactNode;
}
export interface TimelineFragmentProps {
export interface TimelineRendererProps {
frags: Array<TimelineFragment>;
related: Array<TaggedNostrEvent>;
/**
@ -25,14 +26,8 @@ export interface TimelineFragmentProps {
noteOnClick?: (ev: TaggedNostrEvent) => void;
}
export function TimelineRenderer(props: TimelineFragmentProps) {
export function TimelineRenderer(props: TimelineRendererProps) {
const { ref, inView } = useInView();
const relatedFeed = useCallback(
(id: string) => {
return props.related.filter(a => findTag(a, "e") === id);
},
[props.related],
);
return (
<>
@ -64,19 +59,48 @@ export function TimelineRenderer(props: TimelineFragmentProps) {
)}
</>
)}
{props.frags.map(f => {
return (
<>
{f.title}
{f.events.map(
e =>
props.noteRenderer?.(e) ?? (
<Note data={e} related={relatedFeed(e.id)} key={e.id} depth={0} onClick={props.noteOnClick} />
),
)}
</>
);
})}
{props.frags.map(f => (
<TimelineFragment
frag={f}
related={props.related}
noteRenderer={props.noteRenderer}
noteOnClick={props.noteOnClick}
/>
))}
</>
);
}
export interface TimelineFragProps {
frag: TimelineFragment;
related: Array<TaggedNostrEvent>;
noteRenderer?: (ev: TaggedNostrEvent) => ReactNode;
noteOnClick?: (ev: TaggedNostrEvent) => void;
}
export function TimelineFragment(props: TimelineFragProps) {
const relatedFeed = useCallback(
(id: string) => {
return props.related.filter(a => findTag(a, "e") === id);
},
[props.related],
);
return (
<>
{props.frag.title}
{props.frag.events.map(
e =>
props.noteRenderer?.(e) ?? (
<Note
data={e}
related={relatedFeed(e.id)}
key={e.id}
depth={0}
onClick={props.noteOnClick}
context={e.context}
/>
),
)}
</>
);
}

View File

@ -739,9 +739,6 @@
"defaultMessage": "Recent",
"description": "Sort order name"
},
"RkW5we": {
"defaultMessage": "Bitcoin"
},
"RoOyAh": {
"defaultMessage": "Relays"
},
@ -910,9 +907,6 @@
"Zff6lu": {
"defaultMessage": "Username iris.to/<b>{name}</b> is reserved for you!"
},
"a+6cHB": {
"defaultMessage": "Derogatory"
},
"a5UPxh": {
"defaultMessage": "Fund developers and platforms providing NIP-05 verification services"
},
@ -1162,9 +1156,6 @@
"k2veDA": {
"defaultMessage": "Write"
},
"k7+5Ny": {
"defaultMessage": "Hate Speech"
},
"k7sKNy": {
"defaultMessage": "Our very own NIP-05 verification service, help support the development of this site and get a shiny special badge on our site!"
},

View File

@ -243,7 +243,6 @@
"RfhLwC": "By: {author}",
"RhDAoS": "Are you sure you want to delete {id}",
"RjpoYG": "Recent",
"RkW5we": "Bitcoin",
"RoOyAh": "Relays",
"Rs4kCE": "Bookmark",
"RwFaYs": "Sort",
@ -299,7 +298,6 @@
"ZLmyG9": "Contributors",
"ZS+jRE": "Send zap splits to",
"Zff6lu": "Username iris.to/<b>{name}</b> is reserved for you!",
"a+6cHB": "Derogatory",
"a5UPxh": "Fund developers and platforms providing NIP-05 verification services",
"a7TDNm": "Notes will stream in real time into global and notes tab",
"aHje0o": "Name or nym",
@ -382,7 +380,6 @@
"jvo0vs": "Save",
"jzgQ2z": "{n} Reactions",
"k2veDA": "Write",
"k7+5Ny": "Hate Speech",
"k7sKNy": "Our very own NIP-05 verification service, help support the development of this site and get a shiny special badge on our site!",
"kEZUR8": "Register an Iris username",
"kJYo0u": "{n,plural,=0{{name} reposted} other{{name} & {n} others reposted}}",

View File

@ -14,7 +14,12 @@ export interface TaggedNostrEvent extends NostrEvent {
/**
* A list of relays this event was seen on
*/
relays: string[];
relays: Array<string>;
/**
* Additional context
*/
context?: never;
}
/**