refactor: inline hashtag posts
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
import "./Note.css";
|
import "./Note.css";
|
||||||
import React from "react";
|
import { ReactNode } from "react";
|
||||||
import { EventKind, NostrEvent, TaggedNostrEvent } from "@snort/system";
|
import { EventKind, NostrEvent, TaggedNostrEvent } from "@snort/system";
|
||||||
import { NostrFileElement } from "Element/Event/NostrFileHeader";
|
import { NostrFileElement } from "Element/Event/NostrFileHeader";
|
||||||
import ZapstrEmbed from "Element/Embed/ZapstrEmbed";
|
import ZapstrEmbed from "Element/Embed/ZapstrEmbed";
|
||||||
@ -21,6 +21,7 @@ export interface NoteProps {
|
|||||||
depth?: number;
|
depth?: number;
|
||||||
searchedValue?: string;
|
searchedValue?: string;
|
||||||
threadChains?: Map<string, Array<NostrEvent>>;
|
threadChains?: Map<string, Array<NostrEvent>>;
|
||||||
|
context?: ReactNode;
|
||||||
options?: {
|
options?: {
|
||||||
showHeader?: boolean;
|
showHeader?: boolean;
|
||||||
showContextMenu?: boolean;
|
showContextMenu?: boolean;
|
||||||
|
@ -290,6 +290,7 @@ export function NoteInner(props: NoteProps) {
|
|||||||
link={opt?.canClick === undefined ? undefined : ""}
|
link={opt?.canClick === undefined ? undefined : ""}
|
||||||
/>
|
/>
|
||||||
<div className="info">
|
<div className="info">
|
||||||
|
{props.context}
|
||||||
{(options.showTime || options.showBookmarked) && (
|
{(options.showTime || options.showBookmarked) && (
|
||||||
<>
|
<>
|
||||||
{options.showBookmarked && (
|
{options.showBookmarked && (
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import "./Timeline.css";
|
import "./Timeline.css";
|
||||||
import { ReactNode, useCallback, useContext, useMemo, useState, useSyncExternalStore } from "react";
|
import { ReactNode, useCallback, useContext, useMemo, useState, useSyncExternalStore } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import { EventKind, NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system";
|
import { EventKind, NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
import { unixNow } from "@snort/shared";
|
import { unixNow } from "@snort/shared";
|
||||||
import { SnortContext, useReactions } from "@snort/system-react";
|
import { SnortContext, useReactions } from "@snort/system-react";
|
||||||
@ -9,10 +10,9 @@ import useModeration from "Hooks/useModeration";
|
|||||||
import { FollowsFeed } from "Cache";
|
import { FollowsFeed } from "Cache";
|
||||||
import { LiveStreams } from "Element/LiveStreams";
|
import { LiveStreams } from "Element/LiveStreams";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import { TimelineFragment, TimelineRenderer } from "./TimelineFragment";
|
import { TimelineRenderer } from "./TimelineFragment";
|
||||||
import useHashtagsFeed from "Feed/HashtagsFeed";
|
import useHashtagsFeed from "Feed/HashtagsFeed";
|
||||||
import { ShowMoreInView } from "Element/Event/ShowMore";
|
import { ShowMoreInView } from "Element/Event/ShowMore";
|
||||||
import { HashTagHeader } from "Pages/HashTagsPage";
|
|
||||||
|
|
||||||
export interface TimelineFollowsProps {
|
export interface TimelineFollowsProps {
|
||||||
postsOnly: boolean;
|
postsOnly: boolean;
|
||||||
@ -42,6 +42,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
|
|||||||
const { muted, isEventMuted } = useModeration();
|
const { muted, isEventMuted } = useModeration();
|
||||||
|
|
||||||
const sortedFeed = useMemo(() => orderDescending(feed), [feed]);
|
const sortedFeed = useMemo(() => orderDescending(feed), [feed]);
|
||||||
|
const oldest = useMemo(() => sortedFeed.at(-1)?.created_at, [sortedFeed]);
|
||||||
|
|
||||||
const postsOnly = useCallback(
|
const postsOnly = useCallback(
|
||||||
(a: NostrEvent) => (props.postsOnly ? !a.tags.some(b => b[0] === "e" || b[0] === "a") : true),
|
(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 mixin = useHashtagsFeed();
|
||||||
const mainFeed = useMemo(() => {
|
const mainFeed = useMemo(() => {
|
||||||
return filterPosts((sortedFeed ?? []).filter(a => a.created_at <= latest));
|
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 mainFeedIds = new Set(mainFeed.map(a => a.id));
|
||||||
const included = new Set<string>();
|
|
||||||
return (mixin.data.data ?? [])
|
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)
|
.filter(a => a.tags.filter(a => a[0] === "t").length < 5)
|
||||||
.reduce(
|
.filter(a => a.created_at >= (oldest ?? unixNow()))
|
||||||
(acc, v) => {
|
.map(
|
||||||
if (included.has(v.id)) return acc;
|
a =>
|
||||||
const tags = v.tags
|
({
|
||||||
.filter(a => a[0] === "t")
|
...a,
|
||||||
.map(v => v[1].toLocaleLowerCase())
|
context: findHashTagContext(a),
|
||||||
.filter(a => mixin.hashtags.includes(a));
|
}) as TaggedNostrEvent,
|
||||||
for (const t of tags) {
|
|
||||||
acc[t] ??= [];
|
|
||||||
acc[t].push(v);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
included.add(v.id);
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, Array<TaggedNostrEvent>>,
|
|
||||||
);
|
);
|
||||||
}, [mixin, mainFeed, postsOnly]);
|
}, [mixin, mainFeed, postsOnly, isEventMuted]);
|
||||||
|
|
||||||
const latestFeed = useMemo(() => {
|
const latestFeed = useMemo(() => {
|
||||||
return filterPosts((sortedFeed ?? []).filter(a => a.created_at > latest));
|
return filterPosts((sortedFeed ?? []).filter(a => a.created_at > latest));
|
||||||
@ -111,7 +108,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
|
|||||||
<>
|
<>
|
||||||
{(props.liveStreams ?? true) && <LiveStreams evs={liveStreams} />}
|
{(props.liveStreams ?? true) && <LiveStreams evs={liveStreams} />}
|
||||||
<TimelineRenderer
|
<TimelineRenderer
|
||||||
frags={weaveTimeline(mainFeed, hashTagsGroups)}
|
frags={[{ events: orderDescending(mainFeed.concat(mixinFiltered)), refTime: latest }]}
|
||||||
related={reactions.data ?? []}
|
related={reactions.data ?? []}
|
||||||
latest={latestAuthors}
|
latest={latestAuthors}
|
||||||
showLatest={t => onShowLatest(t)}
|
showLatest={t => onShowLatest(t)}
|
||||||
@ -119,55 +116,9 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
|
|||||||
noteRenderer={props.noteRenderer}
|
noteRenderer={props.noteRenderer}
|
||||||
/>
|
/>
|
||||||
{sortedFeed.length > 0 && (
|
{sortedFeed.length > 0 && (
|
||||||
<ShowMoreInView
|
<ShowMoreInView onClick={async () => await FollowsFeed.loadMore(system, login, oldest ?? unixNow())} />
|
||||||
onClick={async () => await FollowsFeed.loadMore(system, login, sortedFeed[sortedFeed.length - 1].created_at)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default TimelineFollows;
|
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 { TaggedNostrEvent } from "@snort/system";
|
||||||
|
|
||||||
import Note from "Element/Event/Note";
|
import Note from "Element/Event/Note";
|
||||||
import ProfileImage from "Element/User/ProfileImage";
|
import ProfileImage from "Element/User/ProfileImage";
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import { findTag } from "SnortUtils";
|
import { findTag } from "SnortUtils";
|
||||||
import { ReactNode, useCallback } from "react";
|
|
||||||
import { useInView } from "react-intersection-observer";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
|
||||||
|
|
||||||
export interface TimelineFragment {
|
export interface TimelineFragment {
|
||||||
events: Array<TaggedNostrEvent>;
|
events: Array<TaggedNostrEvent>;
|
||||||
@ -13,7 +14,7 @@ export interface TimelineFragment {
|
|||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimelineFragmentProps {
|
export interface TimelineRendererProps {
|
||||||
frags: Array<TimelineFragment>;
|
frags: Array<TimelineFragment>;
|
||||||
related: Array<TaggedNostrEvent>;
|
related: Array<TaggedNostrEvent>;
|
||||||
/**
|
/**
|
||||||
@ -25,14 +26,8 @@ export interface TimelineFragmentProps {
|
|||||||
noteOnClick?: (ev: TaggedNostrEvent) => void;
|
noteOnClick?: (ev: TaggedNostrEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TimelineRenderer(props: TimelineFragmentProps) {
|
export function TimelineRenderer(props: TimelineRendererProps) {
|
||||||
const { ref, inView } = useInView();
|
const { ref, inView } = useInView();
|
||||||
const relatedFeed = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
return props.related.filter(a => findTag(a, "e") === id);
|
|
||||||
},
|
|
||||||
[props.related],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -64,19 +59,48 @@ export function TimelineRenderer(props: TimelineFragmentProps) {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{props.frags.map(f => {
|
{props.frags.map(f => (
|
||||||
return (
|
<TimelineFragment
|
||||||
<>
|
frag={f}
|
||||||
{f.title}
|
related={props.related}
|
||||||
{f.events.map(
|
noteRenderer={props.noteRenderer}
|
||||||
e =>
|
noteOnClick={props.noteOnClick}
|
||||||
props.noteRenderer?.(e) ?? (
|
/>
|
||||||
<Note data={e} related={relatedFeed(e.id)} key={e.id} depth={0} onClick={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",
|
"defaultMessage": "Recent",
|
||||||
"description": "Sort order name"
|
"description": "Sort order name"
|
||||||
},
|
},
|
||||||
"RkW5we": {
|
|
||||||
"defaultMessage": "Bitcoin"
|
|
||||||
},
|
|
||||||
"RoOyAh": {
|
"RoOyAh": {
|
||||||
"defaultMessage": "Relays"
|
"defaultMessage": "Relays"
|
||||||
},
|
},
|
||||||
@ -910,9 +907,6 @@
|
|||||||
"Zff6lu": {
|
"Zff6lu": {
|
||||||
"defaultMessage": "Username iris.to/<b>{name}</b> is reserved for you!"
|
"defaultMessage": "Username iris.to/<b>{name}</b> is reserved for you!"
|
||||||
},
|
},
|
||||||
"a+6cHB": {
|
|
||||||
"defaultMessage": "Derogatory"
|
|
||||||
},
|
|
||||||
"a5UPxh": {
|
"a5UPxh": {
|
||||||
"defaultMessage": "Fund developers and platforms providing NIP-05 verification services"
|
"defaultMessage": "Fund developers and platforms providing NIP-05 verification services"
|
||||||
},
|
},
|
||||||
@ -1162,9 +1156,6 @@
|
|||||||
"k2veDA": {
|
"k2veDA": {
|
||||||
"defaultMessage": "Write"
|
"defaultMessage": "Write"
|
||||||
},
|
},
|
||||||
"k7+5Ny": {
|
|
||||||
"defaultMessage": "Hate Speech"
|
|
||||||
},
|
|
||||||
"k7sKNy": {
|
"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!"
|
"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}",
|
"RfhLwC": "By: {author}",
|
||||||
"RhDAoS": "Are you sure you want to delete {id}",
|
"RhDAoS": "Are you sure you want to delete {id}",
|
||||||
"RjpoYG": "Recent",
|
"RjpoYG": "Recent",
|
||||||
"RkW5we": "Bitcoin",
|
|
||||||
"RoOyAh": "Relays",
|
"RoOyAh": "Relays",
|
||||||
"Rs4kCE": "Bookmark",
|
"Rs4kCE": "Bookmark",
|
||||||
"RwFaYs": "Sort",
|
"RwFaYs": "Sort",
|
||||||
@ -299,7 +298,6 @@
|
|||||||
"ZLmyG9": "Contributors",
|
"ZLmyG9": "Contributors",
|
||||||
"ZS+jRE": "Send zap splits to",
|
"ZS+jRE": "Send zap splits to",
|
||||||
"Zff6lu": "Username iris.to/<b>{name}</b> is reserved for you!",
|
"Zff6lu": "Username iris.to/<b>{name}</b> is reserved for you!",
|
||||||
"a+6cHB": "Derogatory",
|
|
||||||
"a5UPxh": "Fund developers and platforms providing NIP-05 verification services",
|
"a5UPxh": "Fund developers and platforms providing NIP-05 verification services",
|
||||||
"a7TDNm": "Notes will stream in real time into global and notes tab",
|
"a7TDNm": "Notes will stream in real time into global and notes tab",
|
||||||
"aHje0o": "Name or nym",
|
"aHje0o": "Name or nym",
|
||||||
@ -382,7 +380,6 @@
|
|||||||
"jvo0vs": "Save",
|
"jvo0vs": "Save",
|
||||||
"jzgQ2z": "{n} Reactions",
|
"jzgQ2z": "{n} Reactions",
|
||||||
"k2veDA": "Write",
|
"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!",
|
"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",
|
"kEZUR8": "Register an Iris username",
|
||||||
"kJYo0u": "{n,plural,=0{{name} reposted} other{{name} & {n} others reposted}}",
|
"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
|
* A list of relays this event was seen on
|
||||||
*/
|
*/
|
||||||
relays: string[];
|
relays: Array<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional context
|
||||||
|
*/
|
||||||
|
context?: never;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user