From 9a0bbb8b74d31e5b0fe4c4fb90d60d304fadece3 Mon Sep 17 00:00:00 2001 From: Kieran Date: Wed, 24 Jan 2024 11:43:51 +0000 Subject: [PATCH] refactor: hashtags timeline weaver to worker relay --- packages/app/src/Components/Feed/Timeline.tsx | 13 +--- .../src/Components/Feed/TimelineFollows.tsx | 76 +++++++------------ .../src/Components/LiveStream/LiveStreams.tsx | 23 +++--- packages/app/src/Feed/TimelineFeed.ts | 67 ++++++---------- packages/app/src/Feed/WorkerRelayView.ts | 18 ----- packages/app/src/Pages/Layout/NavSidebar.tsx | 22 +++--- packages/app/src/lang.json | 6 ++ packages/app/src/translations/en.json | 2 + packages/system/src/request-builder.ts | 4 + 9 files changed, 92 insertions(+), 139 deletions(-) diff --git a/packages/app/src/Components/Feed/Timeline.tsx b/packages/app/src/Components/Feed/Timeline.tsx index bda996d8..cc44326f 100644 --- a/packages/app/src/Components/Feed/Timeline.tsx +++ b/packages/app/src/Components/Feed/Timeline.tsx @@ -7,11 +7,9 @@ import { FormattedMessage } from "react-intl"; import { DisplayAs, DisplayAsSelector } from "@/Components/Feed/DisplayAsSelector"; import { TimelineRenderer } from "@/Components/Feed/TimelineRenderer"; -import { LiveStreams } from "@/Components/LiveStream/LiveStreams"; import useTimelineFeed, { TimelineFeed, TimelineSubject } from "@/Feed/TimelineFeed"; import useLogin from "@/Hooks/useLogin"; -import useModeration from "@/Hooks/useModeration"; -import { dedupeByPubkey, findTag } from "@/Utils"; +import { dedupeByPubkey } from "@/Utils"; export interface TimelineProps { postsOnly: boolean; @@ -43,7 +41,6 @@ const Timeline = (props: TimelineProps) => { const displayAsInitial = props.displayAs ?? login.feedDisplayAs ?? "list"; const [displayAs, setDisplayAs] = useState(displayAsInitial); - const { muted, isEventMuted } = useModeration(); const filterPosts = useCallback( (nts: readonly TaggedNostrEvent[]) => { const checkFollowDistance = (a: TaggedNostrEvent) => { @@ -56,9 +53,9 @@ const Timeline = (props: TimelineProps) => { const a = [...nts.filter(a => a.kind !== EventKind.LiveEvent)]; return a ?.filter(a => (props.postsOnly ? !a.tags.some(b => b[0] === "e") : true)) - .filter(a => (props.ignoreModeration || !isEventMuted(a)) && checkFollowDistance(a)); + .filter(a => props.ignoreModeration && checkFollowDistance(a)); }, - [props.postsOnly, muted, props.ignoreModeration, props.followDistance], + [props.postsOnly, props.ignoreModeration, props.followDistance], ); const mainFeed = useMemo(() => { @@ -67,9 +64,6 @@ const Timeline = (props: TimelineProps) => { const latestFeed = useMemo(() => { return filterPosts(feed.latest ?? []).filter(a => !mainFeed.some(b => b.id === a.id)); }, [feed, filterPosts]); - const liveStreams = useMemo(() => { - return (feed.main ?? []).filter(a => a.kind === EventKind.LiveEvent && findTag(a, "status") === "live"); - }, [feed]); const latestAuthors = useMemo(() => { return dedupeByPubkey(latestFeed).map(e => e.pubkey); @@ -84,7 +78,6 @@ const Timeline = (props: TimelineProps) => { return ( <> - { const login = useLogin(); const displayAsInitial = props.displayAs ?? login.feedDisplayAs ?? "list"; const [displayAs, setDisplayAs] = useState(displayAsInitial); - const [latest, setLatest] = useHistoryState(unixNow(), "TimelineFollowsLatest"); - const [limit, setLimit] = useState(50); - const feed = useFollowsTimelineView(limit); - const { muted, isEventMuted } = useModeration(); - - const oldest = useMemo(() => feed.at(-1)?.created_at, [feed]); + const subject = useMemo( + () => + ({ + type: "pubkey", + items: login.follows.item, + discriminator: login.publicKey?.slice(0, 12), + extra: rb => { + if (login.tags.item.length > 0) { + rb.withFilter().kinds([EventKind.TextNote]).tag("t", login.tags.item); + } + }, + }) as TimelineSubject, + [login.follows.item, login.tags.item], + ); + const feed = useTimelineFeed(subject, { method: "TIME_RANGE" } as TimelineFeedOptions); const postsOnly = useCallback( (a: NostrEvent) => (props.postsOnly ? !a.tags.some(b => b[0] === "e" || b[0] === "a") : true), @@ -50,49 +55,26 @@ const TimelineFollows = (props: TimelineFollowsProps) => { const a = nts.filter(a => a.kind !== EventKind.LiveEvent); return a ?.filter(postsOnly) - .filter(a => !isEventMuted(a) && login.follows.item.includes(a.pubkey) && (props.noteFilter?.(a) ?? true)); + .filter(a => props.noteFilter?.(a) ?? true) + .filter(a => login.follows.item.includes(a.pubkey) || a.tags.filter(a => a[0] === "t").length < 5); }, - [postsOnly, muted, login.follows.timestamp], + [postsOnly, props.noteFilter, login.follows.timestamp], ); - const mixin = useHashtagsFeed(); const mainFeed = useMemo(() => { - return filterPosts((feed ?? []).filter(a => a.created_at <= latest)); - }, [feed, filterPosts, latest, login.follows.timestamp]); - - const findHashTagContext = (a: NostrEvent) => { - const tag = a.tags.filter(a => a[0] === "t").find(a => login.tags.item.includes(a[1].toLowerCase()))?.[1]; - return tag; - }; - const mixinFiltered = useMemo(() => { - const mainFeedIds = new Set(mainFeed.map(a => a.id)); - return (mixin.data ?? []) - .filter(a => !mainFeedIds.has(a.id) && postsOnly(a) && !isEventMuted(a)) - .filter(a => a.tags.filter(a => a[0] === "t").length < 5) - .filter(a => !oldest || a.created_at >= oldest) - .map( - a => - ({ - ...a, - context: findHashTagContext(a), - }) as TaggedNostrEvent, - ); - }, [mixin, mainFeed, postsOnly, isEventMuted]); + return filterPosts(feed.main ?? []); + }, [feed.main, filterPosts]); const latestFeed = useMemo(() => { - return filterPosts((feed ?? []).filter(a => a.created_at > latest)); - }, [feed, latest]); - - const liveStreams = useMemo(() => { - return (feed ?? []).filter(a => a.kind === EventKind.LiveEvent && findTag(a, "status") === "live"); - }, [feed]); + return filterPosts(feed.latest ?? []); + }, [feed.latest]); const latestAuthors = useMemo(() => { return dedupeByPubkey(latestFeed).map(e => e.pubkey); }, [latestFeed]); function onShowLatest(scrollToTop = false) { - setLatest(unixNow()); + feed.showLatest(); if (scrollToTop) { window.scrollTo(0, 0); } @@ -100,14 +82,14 @@ const TimelineFollows = (props: TimelineFollowsProps) => { return ( <> - {(props.liveStreams ?? true) && } + {(props.liveStreams ?? true) && } setDisplayAs(displayAs)} /> onShowLatest(t)} noteOnClick={props.noteOnClick} @@ -119,10 +101,10 @@ const TimelineFollows = (props: TimelineFollowsProps) => { }} displayAs={displayAs} /> - {feed.length > 0 && ( + {(feed.main?.length ?? 0) > 0 && ( { - setLimit(s => s + 20); + onShowLatest(false); }} /> )} diff --git a/packages/app/src/Components/LiveStream/LiveStreams.tsx b/packages/app/src/Components/LiveStream/LiveStreams.tsx index f8ef34ae..bfe307ce 100644 --- a/packages/app/src/Components/LiveStream/LiveStreams.tsx +++ b/packages/app/src/Components/LiveStream/LiveStreams.tsx @@ -1,22 +1,27 @@ import "./LiveStreams.css"; -import { NostrEvent, NostrLink } from "@snort/system"; +import { unixNow } from "@snort/shared"; +import { EventKind, NostrEvent, NostrLink, RequestBuilder } from "@snort/system"; +import { useRequestBuilder } from "@snort/system-react"; import { CSSProperties, useMemo } from "react"; import { Link } from "react-router-dom"; import Icon from "@/Components/Icons/Icon"; import useImgProxy from "@/Hooks/useImgProxy"; +import useLogin from "@/Hooks/useLogin"; import { findTag } from "@/Utils"; -export function LiveStreams({ evs }: { evs: Array }) { - const streams = useMemo(() => { - return [...evs].sort((a, b) => { - const aStarts = Number(findTag(a, "starts") ?? a.created_at); - const bStarts = Number(findTag(b, "starts") ?? b.created_at); - return aStarts > bStarts ? -1 : 1; - }); - }, [evs]); +export function LiveStreams() { + const follows = useLogin(s => s.follows.item); + const sub = useMemo(() => { + const since = unixNow() - 60 * 60 * 24; + const rb = new RequestBuilder("follows:streams"); + rb.withFilter().kinds([EventKind.LiveEvent]).authors(follows).since(since); + rb.withFilter().kinds([EventKind.LiveEvent]).tag("p", follows).since(since); + return rb; + }, [follows]); + const streams = useRequestBuilder(sub); if (streams.length === 0) return null; return ( diff --git a/packages/app/src/Feed/TimelineFeed.ts b/packages/app/src/Feed/TimelineFeed.ts index d8ff37fc..89ed5ebd 100644 --- a/packages/app/src/Feed/TimelineFeed.ts +++ b/packages/app/src/Feed/TimelineFeed.ts @@ -4,6 +4,7 @@ import { useRequestBuilderAdvanced } from "@snort/system-react"; import { useCallback, useMemo, useSyncExternalStore } from "react"; import useLogin from "@/Hooks/useLogin"; +import useModeration from "@/Hooks/useModeration"; import useTimelineWindow from "@/Hooks/useTimelineWindow"; import { SearchRelays } from "@/Utils/Const"; @@ -18,7 +19,7 @@ export interface TimelineSubject { discriminator: string; items: string[]; relay?: Array; - streams?: boolean; + extra?: (rb: RequestBuilder) => void; } export type TimelineFeed = ReturnType; @@ -29,6 +30,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel now: options.now ?? unixNow(), }); const pref = useLogin(s => s.appData.item.preferences); + const { isEventMuted } = useModeration(); const createBuilder = useCallback(() => { if (subject.type !== "global" && subject.items.length === 0) { @@ -71,49 +73,24 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel break; } } - if (subject.streams && subject.type === "pubkey") { - b.withFilter() - .kinds([EventKind.LiveEvent]) - .authors(subject.items) - .since(now - 60 * 60 * 24); - b.withFilter().kinds([EventKind.LiveEvent]).tag("p", subject.items); - } - return { - builder: b, - filter: f, - }; - }, [subject.type, subject.items, subject.discriminator]); + subject.extra?.(b); + return b; + }, [subject.type, subject.items, subject.discriminator, subject.extra]); const sub = useMemo(() => { const rb = createBuilder(); - console.debug(rb?.builder.id, options); if (rb) { - if (options.method === "LIMIT_UNTIL") { - rb.filter.until(until).limit(50); - } else { - rb.filter.since(since).until(until); - if (since === undefined) { - rb.filter.limit(50); + for (const filter of rb.filterBuilders) { + if (options.method === "LIMIT_UNTIL") { + filter.until(until).limit(50); + } else { + filter.since(since).until(until); + if (since === undefined) { + filter.limit(50); + } } } - - if (pref.autoShowLatest) { - // copy properties of main sub but with limit 0 - // this will put latest directly into main feed - rb.builder - .withOptions({ - leaveOpen: true, - }) - .withFilter() - .authors(rb.filter.filter.authors) - .kinds(rb.filter.filter.kinds) - .tag("p", rb.filter.filter["#p"]) - .tag("t", rb.filter.filter["#t"]) - .search(rb.filter.filter.search) - .limit(1) - .since(now); - } - return rb.builder; + return rb; } }, [until, since, options.method, pref, createBuilder]); @@ -131,12 +108,14 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel const subRealtime = useMemo(() => { const rb = createBuilder(); if (rb && !pref.autoShowLatest && options.method !== "LIMIT_UNTIL") { - rb.builder.withOptions({ + rb.withOptions({ leaveOpen: true, }); - rb.builder.id = `${rb.builder.id}:latest`; - rb.filter.limit(1).since(now); - return rb.builder; + rb.id = `${rb.id}:latest`; + for (const filter of rb.filterBuilders) { + filter.limit(1).since(now); + } + return rb; } }, [pref.autoShowLatest, createBuilder]); @@ -152,8 +131,8 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel ); return { - main: main, - latest: latest, + main: main?.filter(a => !isEventMuted(a)), + latest: latest?.filter(a => !isEventMuted(a)), loadMore: () => { if (main) { console.debug("Timeline load more!"); diff --git a/packages/app/src/Feed/WorkerRelayView.ts b/packages/app/src/Feed/WorkerRelayView.ts index 3d6ecd18..789a0e8e 100644 --- a/packages/app/src/Feed/WorkerRelayView.ts +++ b/packages/app/src/Feed/WorkerRelayView.ts @@ -2,26 +2,8 @@ import { EventKind, RequestBuilder } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; import { useMemo } from "react"; -//import { LRUCache } from "typescript-lru-cache"; import useLogin from "@/Hooks/useLogin"; -//const cache = new LRUCache({ maxSize: 100 }); - -export function useFollowsTimelineView(limit = 20) { - const follows = useLogin(s => s.follows.item); - const kinds = [EventKind.TextNote, EventKind.Repost, EventKind.Polls]; - - const req = useMemo(() => { - const rb = new RequestBuilder("follows-timeline"); - rb.withOptions({ - leaveOpen: true, - }); - rb.withFilter().kinds(kinds).authors(follows).limit(limit); - return rb; - }, [follows, limit]); - return useRequestBuilder(req); -} - export function useNotificationsView() { const publicKey = useLogin(s => s.publicKey); const kinds = [EventKind.TextNote, EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt]; diff --git a/packages/app/src/Pages/Layout/NavSidebar.tsx b/packages/app/src/Pages/Layout/NavSidebar.tsx index f064c4ef..833b5d90 100644 --- a/packages/app/src/Pages/Layout/NavSidebar.tsx +++ b/packages/app/src/Pages/Layout/NavSidebar.tsx @@ -9,56 +9,56 @@ import Icon from "@/Components/Icons/Icon"; import Avatar from "@/Components/User/Avatar"; import { ProfileLink } from "@/Components/User/ProfileLink"; import useEventPublisher from "@/Hooks/useEventPublisher"; +import useLogin from "@/Hooks/useLogin"; import { HasNotificationsMarker } from "@/Pages/Layout/HasNotificationsMarker"; import { WalletBalance } from "@/Pages/Layout/WalletBalance"; import { subscribeToNotifications } from "@/Utils/Notifications"; import { getCurrentSubscription } from "@/Utils/Subscription"; -import useLogin from "../../Hooks/useLogin"; import { LogoHeader } from "./LogoHeader"; const MENU_ITEMS = [ { - label: "Home", + label: , icon: "home", link: "/", nonLoggedIn: true, }, { - label: "Search", + label: , icon: "search", link: "/search", nonLoggedIn: true, }, { - label: "Notifications", + label: , icon: "bell", link: "/notifications", }, { - label: "Messages", + label: , icon: "mail", link: "/messages", hideReadOnly: true, }, { - label: "Deck", + label: , icon: "deck", link: "/deck", }, { - label: "Social Graph", + label: , icon: "graph", link: "/graph", }, { - label: "About", + label: , icon: "info", link: "/donate", nonLoggedIn: true, }, { - label: "Settings", + label: , icon: "settings", link: "/settings", }, @@ -126,7 +126,7 @@ export default function NavSidebar({ narrow = false }: { narrow: boolean }) { return ""; } const onClick = () => { - if (item.label === "Notifications" && publisher) { + if (item.link === "/notifications" && publisher) { subscribeToNotifications(publisher); } }; @@ -138,7 +138,7 @@ export default function NavSidebar({ narrow = false }: { narrow: boolean }) { className={({ isActive }) => getNavLinkClass(isActive, narrow)}> - {item.label === "Notifications" && } + {item.link === "/notifications" && } {!narrow && {item.label}} ); diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json index fc594874..0592deb2 100644 --- a/packages/app/src/lang.json +++ b/packages/app/src/lang.json @@ -1214,6 +1214,9 @@ "egib+2": { "defaultMessage": "{n,plural,=1{& {n} other} other{& {n} others}}" }, + "ejEGdx": { + "defaultMessage": "Home" + }, "f1OxTe": { "defaultMessage": "Community leaders are individuals who grow the nostr ecosystem by being active in their local communities and helping onboard new users. Anyone can become a community leader, but few hold the current honorary title." }, @@ -1482,6 +1485,9 @@ "nwZXeh": { "defaultMessage": "{n} blocked" }, + "o/gK53": { + "defaultMessage": "Deck" + }, "o7e+nJ": { "defaultMessage": "{n} followers" }, diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index c0d24dd0..713314e2 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -400,6 +400,7 @@ "eSzf2G": "A single zap of {nIn} sats will allocate {nOut} sats to the zap pool.", "eXT2QQ": "Group Chat", "egib+2": "{n,plural,=1{& {n} other} other{& {n} others}}", + "ejEGdx": "Home", "f1OxTe": "Community leaders are individuals who grow the nostr ecosystem by being active in their local communities and helping onboard new users. Anyone can become a community leader, but few hold the current honorary title.", "f2CAxA": "Dump", "fBI91o": "Zap", @@ -489,6 +490,7 @@ "nUT0Lv": "Tools", "nihgfo": "Listen to this article", "nwZXeh": "{n} blocked", + "o/gK53": "Deck", "o7e+nJ": "{n} followers", "oJ+JJN": "Nothing found :/", "odFwjL": "Follows only", diff --git a/packages/system/src/request-builder.ts b/packages/system/src/request-builder.ts index 6cf65147..82c2d733 100644 --- a/packages/system/src/request-builder.ts +++ b/packages/system/src/request-builder.ts @@ -92,6 +92,10 @@ export class RequestBuilder { return this.#builders.length; } + get filterBuilders() { + return this.#builders; + } + get options() { return this.#options; }