From 98f6a686b067866460534d034fb4379b56e8ead0 Mon Sep 17 00:00:00 2001 From: Kieran Date: Fri, 10 Nov 2023 09:43:02 +0000 Subject: [PATCH] feat: mixin hashtags --- packages/app/src/Element/Feed/Timeline.tsx | 6 +- .../app/src/Element/Feed/TimelineFollows.tsx | 78 ++++++++++++++----- .../app/src/Element/Feed/TimelineFragment.tsx | 1 + packages/app/src/Feed/HashtagsFeed.ts | 24 ++++++ 4 files changed, 88 insertions(+), 21 deletions(-) create mode 100644 packages/app/src/Feed/HashtagsFeed.ts 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, + }; +}