diff --git a/packages/app/src/Element/Feed/Timeline.tsx b/packages/app/src/Element/Feed/Timeline.tsx
index 28dae201..6922526a 100644
--- a/packages/app/src/Element/Feed/Timeline.tsx
+++ b/packages/app/src/Element/Feed/Timeline.tsx
@@ -8,6 +8,7 @@ import useTimelineFeed, { TimelineFeed, TimelineSubject } from "Feed/TimelineFee
import useModeration from "Hooks/useModeration";
import { LiveStreams } from "Element/LiveStreams";
import { TimelineRenderer } from "./TimelineFragment";
+import { unixNow } from "@snort/shared";
export interface TimelineProps {
postsOnly: boolean;
@@ -70,7 +71,10 @@ const Timeline = (props: TimelineProps) => {
<>
onShowLatest(t)}
diff --git a/packages/app/src/Element/Feed/TimelineFollows.tsx b/packages/app/src/Element/Feed/TimelineFollows.tsx
index f982637b..08a22dc3 100644
--- a/packages/app/src/Element/Feed/TimelineFollows.tsx
+++ b/packages/app/src/Element/Feed/TimelineFollows.tsx
@@ -1,7 +1,6 @@
import "./Timeline.css";
import { ReactNode, useCallback, useContext, useMemo, useState, useSyncExternalStore } from "react";
-import { FormattedMessage } from "react-intl";
-import { EventKind, NostrEvent, NostrLink } from "@snort/system";
+import { EventKind, NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system";
import { unixNow } from "@snort/shared";
import { SnortContext, useReactions } from "@snort/system-react";
@@ -9,9 +8,10 @@ import { dedupeByPubkey, findTag, orderDescending } from "SnortUtils";
import useModeration from "Hooks/useModeration";
import { FollowsFeed } from "Cache";
import { LiveStreams } from "Element/LiveStreams";
-import AsyncButton from "../AsyncButton";
import useLogin from "Hooks/useLogin";
-import { TimelineRenderer } from "./TimelineFragment";
+import { TimelineFragment, TimelineRenderer } from "./TimelineFragment";
+import useHashtagsFeed from "Feed/HashtagsFeed";
+import { ShowMoreInView } from "Element/Event/ShowMore";
export interface TimelineFollowsProps {
postsOnly: boolean;
@@ -42,19 +42,39 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
const sortedFeed = useMemo(() => orderDescending(feed), [feed]);
+ const postsOnly = useCallback((a: NostrEvent) => props.postsOnly ? !a.tags.some(b => b[0] === "e" || b[0] === "a") : true, [props.postsOnly]);
+
const filterPosts = useCallback(
function (nts: Array) {
const a = nts.filter(a => a.kind !== EventKind.LiveEvent);
return a
- ?.filter(a => (props.postsOnly ? !a.tags.some(b => b[0] === "e" || b[0] === "a") : true))
+ ?.filter(postsOnly)
.filter(a => !isMuted(a.pubkey) && login.follows.item.includes(a.pubkey) && (props.noteFilter?.(a) ?? true));
},
- [props.postsOnly, muted, login.follows.timestamp],
+ [postsOnly, muted, login.follows.timestamp],
);
+
+ const mixin = useHashtagsFeed();
const mainFeed = useMemo(() => {
return filterPosts((sortedFeed ?? []).filter(a => a.created_at <= latest));
- }, [sortedFeed, filterPosts, latest, login.follows.timestamp]);
+ }, [sortedFeed, filterPosts, latest, login.follows.timestamp, mixin]);
+
+ const hashTagsGroups = useMemo(() => {
+ const mainFeedIds = new Set(mainFeed.map(a => a.id));
+ const included = new Set();
+ return (mixin.data.data ?? []).filter(a => !mainFeedIds.has(a.id) && postsOnly(a)).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>)
+ }, [mixin, mainFeed, postsOnly]);
const latestFeed = useMemo(() => {
return filterPosts((sortedFeed ?? []).filter(a => a.created_at > latest));
@@ -79,26 +99,44 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
<>
{(props.liveStreams ?? true) && }
onShowLatest(t)}
noteOnClick={props.noteOnClick}
noteRenderer={props.noteRenderer}
/>
-
-
{
- await FollowsFeed.loadMore(system, login, sortedFeed[sortedFeed.length - 1].created_at);
- }}>
-
-
-
+ await FollowsFeed.loadMore(system, login, sortedFeed[sortedFeed.length - 1].created_at)} />
>
);
};
export default TimelineFollows;
+
+
+function weaveTimeline(main: Array, hashtags: Record>): Array {
+ // always skip 5 posts from start to avoid heavy handed weaving
+ const skip = 5;
+
+ const frags = Object.entries(hashtags).map(([k, v]) => {
+ const take = v.slice(0, 5);
+ return {
+ title:
+
#{k}
+
,
+ events: take,
+ refTime: Math.min(main[skip].created_at, take.reduce((acc, v) => acc > v.created_at ? acc : v.created_at, 0))
+ } as TimelineFragment;
+ });
+
+ 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);
+}
\ No newline at end of file
diff --git a/packages/app/src/Element/Feed/TimelineFragment.tsx b/packages/app/src/Element/Feed/TimelineFragment.tsx
index ac54a846..f4047cc7 100644
--- a/packages/app/src/Element/Feed/TimelineFragment.tsx
+++ b/packages/app/src/Element/Feed/TimelineFragment.tsx
@@ -9,6 +9,7 @@ import { FormattedMessage } from "react-intl";
export interface TimelineFragment {
events: Array;
+ refTime: number;
title?: ReactNode;
}
diff --git a/packages/app/src/Feed/HashtagsFeed.ts b/packages/app/src/Feed/HashtagsFeed.ts
new file mode 100644
index 00000000..aa30da9d
--- /dev/null
+++ b/packages/app/src/Feed/HashtagsFeed.ts
@@ -0,0 +1,24 @@
+import { useMemo } from "react";
+import { EventKind, NoteCollection, RequestBuilder } from "@snort/system";
+import { unixNow } from "@snort/shared";
+import { useRequestBuilder } from "@snort/system-react";
+
+import useLogin from "Hooks/useLogin";
+import { Hour } from "Const";
+
+export default function useHashtagsFeed() {
+ const { hashtags } = useLogin(s => ({ hashtags: s.tags.item }));
+ const sub = useMemo(() => {
+ const rb = new RequestBuilder("hashtags-feed");
+ rb.withFilter()
+ .kinds([EventKind.TextNote])
+ .tag("t", hashtags)
+ .since(unixNow() - Hour * 4);
+ return rb;
+ }, [hashtags]);
+
+ return {
+ data: useRequestBuilder(NoteCollection, sub),
+ hashtags,
+ };
+}