refactor: inline hashtag posts
This commit is contained in:
parent
de1b982e93
commit
f2a41cb474
@ -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;
|
||||
|
@ -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 && (
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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!"
|
||||
},
|
||||
|
@ -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}}",
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user