From 68001bbe774822c88454d3bb4e058256c1a9f2d7 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Sun, 30 Jul 2023 11:45:58 -0500 Subject: [PATCH] add people list select --- .changeset/rotten-donuts-reflect.md | 5 ++ src/classes/timeline-loader.ts | 21 +++++- .../people-list-provider.tsx | 74 +++++++++++++++++++ .../people-list-selection.tsx | 25 +++++++ .../generic-note-timeline/stream-note.tsx | 11 +-- src/views/streams/components/stream-card.tsx | 5 +- src/views/streams/index.tsx | 21 +++++- 7 files changed, 146 insertions(+), 16 deletions(-) create mode 100644 .changeset/rotten-donuts-reflect.md create mode 100644 src/components/people-list-selection/people-list-provider.tsx create mode 100644 src/components/people-list-selection/people-list-selection.tsx diff --git a/.changeset/rotten-donuts-reflect.md b/.changeset/rotten-donuts-reflect.md new file mode 100644 index 000000000..8690ed1b6 --- /dev/null +++ b/.changeset/rotten-donuts-reflect.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Add people list context and selector diff --git a/src/classes/timeline-loader.ts b/src/classes/timeline-loader.ts index 24502a3ff..b04fc99db 100644 --- a/src/classes/timeline-loader.ts +++ b/src/classes/timeline-loader.ts @@ -1,10 +1,12 @@ import dayjs from "dayjs"; import { utils } from "nostr-tools"; +import debug, { Debug, Debugger } from "debug"; import { NostrEvent } from "../types/nostr-event"; import { NostrQuery, NostrRequestFilter } from "../types/nostr-query"; import { NostrRequest } from "./nostr-request"; import { NostrMultiSubscription } from "./nostr-multi-subscription"; import Subject, { PersistentSubject } from "./subject"; +import { logger } from "../helpers/debug"; function addToQuery(filter: NostrRequestFilter, query: NostrQuery) { if (Array.isArray(filter)) { @@ -23,6 +25,7 @@ class RelayTimelineLoader { blockSize = BLOCK_SIZE; private name?: string; private requestId = 0; + private log: Debugger; loading = false; events: NostrEvent[] = []; @@ -32,17 +35,19 @@ class RelayTimelineLoader { onEvent = new Subject(); onBlockFinish = new Subject(); - constructor(relay: string, query: NostrRequestFilter, name?: string) { + constructor(relay: string, query: NostrRequestFilter, name: string, log?: Debugger) { this.relay = relay; this.query = query; this.name = name; + + this.log = log || logger.extend(name); } loadNextBlock() { this.loading = true; let query: NostrRequestFilter = addToQuery(this.query, { limit: this.blockSize }); if (this.events[this.events.length - 1]) { - query = addToQuery(query, { until: this.events[this.events.length - 1].created_at + 1 }); + query = addToQuery(query, { until: this.events[this.events.length - 1].created_at - 1 }); } const request = new NostrRequest([this.relay], undefined, this.name + "-" + this.requestId++); @@ -56,6 +61,7 @@ class RelayTimelineLoader { request.onComplete.then(() => { this.loading = false; if (gotEvents === 0) this.complete = true; + this.log(`Got ${gotEvents} events`); this.onBlockFinish.next(); }); @@ -95,11 +101,15 @@ export class TimelineLoader { loadNextBlockBuffer = 2; eventFilter?: (event: NostrEvent) => boolean; + private name: string; + private log: Debugger; private subscription: NostrMultiSubscription; private relayTimelineLoaders = new Map(); - constructor(name?: string) { + constructor(name: string) { + this.name = name; + this.log = logger.extend("TimelineLoader:" + name); this.subscription = new NostrMultiSubscription([], undefined, name); this.subscription.onEvent.subscribe(this.handleEvent, this); } @@ -121,7 +131,7 @@ export class TimelineLoader { for (const relay of this.relays) { if (!this.relayTimelineLoaders.has(relay)) { - const loader = new RelayTimelineLoader(relay, this.query, this.subscription.name); + const loader = new RelayTimelineLoader(relay, this.query, this.name, this.log.extend(relay)); this.relayTimelineLoaders.set(relay, loader); loader.onEvent.subscribe(this.handleEvent, this); loader.onBlockFinish.subscribe(this.updateLoading, this); @@ -195,10 +205,13 @@ export class TimelineLoader { } /** @deprecated */ loadMore() { + let triggeredLoad = false; for (const [relay, loader] of this.relayTimelineLoaders) { if (loader.complete || loader.loading) continue; loader.loadNextBlock(); + triggeredLoad = true; } + if (triggeredLoad) this.updateLoading(); } private updateLoading() { diff --git a/src/components/people-list-selection/people-list-provider.tsx b/src/components/people-list-selection/people-list-provider.tsx new file mode 100644 index 000000000..477733538 --- /dev/null +++ b/src/components/people-list-selection/people-list-provider.tsx @@ -0,0 +1,74 @@ +import { PropsWithChildren, createContext, useContext, useMemo, useState } from "react"; +import { nip19 } from "nostr-tools"; +import { useReadRelayUrls } from "../../hooks/use-client-relays"; +import { useCurrentAccount } from "../../hooks/use-current-account"; +import { useUserContacts } from "../../hooks/use-user-contacts"; +import { isPTag } from "../../types/nostr-event"; +import replaceableEventLoaderService from "../../services/replaceable-event-requester"; +import useSubject from "../../hooks/use-subject"; +import clientFollowingService from "../../services/client-following"; + +export type ListIdentifier = "following" | "global" | string; + +export function useParsedNaddr(naddr?: string) { + if (!naddr) return; + try { + const parsed = nip19.decode(naddr); + + if (parsed.type === "naddr") { + return parsed.data; + } + } catch (e) {} +} + +export function useList(naddr?: string) { + const parsed = useMemo(() => useParsedNaddr(naddr), [naddr]); + const readRelays = useReadRelayUrls(parsed?.relays ?? []); + + const sub = useMemo(() => { + if (!parsed) return; + return replaceableEventLoaderService.requestEvent(readRelays, parsed.kind, parsed.pubkey, parsed.identifier); + }, [parsed]); + + return useSubject(sub); +} + +export function useListPeople(list: ListIdentifier) { + const contacts = useSubject(clientFollowingService.following); + + const listEvent = useList(list); + + if (list === "following") return contacts.map((t) => t[1]); + if (listEvent) { + return listEvent.tags.filter(isPTag).map((t) => t[1]); + } + return []; +} + +export type PeopleListContextType = { + list: string; + people: string[]; + setList: (list: string) => void; +}; +const PeopleListContext = createContext({ list: "following", setList: () => {}, people: [] }); + +export function usePeopleListContext() { + return useContext(PeopleListContext); +} + +export default function PeopleListProvider({ children }: PropsWithChildren) { + const account = useCurrentAccount(); + const [list, setList] = useState(account ? "following" : "global"); + + const people = useListPeople(list); + const context = useMemo( + () => ({ + people, + list, + setList, + }), + [list, setList] + ); + + return {children}; +} diff --git a/src/components/people-list-selection/people-list-selection.tsx b/src/components/people-list-selection/people-list-selection.tsx new file mode 100644 index 000000000..c42d63c8c --- /dev/null +++ b/src/components/people-list-selection/people-list-selection.tsx @@ -0,0 +1,25 @@ +import { Select, SelectProps, useDisclosure } from "@chakra-ui/react"; +import { usePeopleListContext } from "./people-list-provider"; + +export default function PeopleListSelection({ + hideGlobalOption = false, + ...props +}: { + hideGlobalOption?: boolean; +} & Omit) { + const { people, list, setList } = usePeopleListContext(); + const { isOpen, onOpen, onClose } = useDisclosure(); + + return ( + + ); +} diff --git a/src/components/timeline-page/generic-note-timeline/stream-note.tsx b/src/components/timeline-page/generic-note-timeline/stream-note.tsx index af1b0c02b..3323e3f42 100644 --- a/src/components/timeline-page/generic-note-timeline/stream-note.tsx +++ b/src/components/timeline-page/generic-note-timeline/stream-note.tsx @@ -1,7 +1,6 @@ import { useMemo, useRef } from "react"; import { Badge, - Box, Card, CardBody, CardFooter, @@ -25,10 +24,10 @@ import { UserAvatar } from "../../user-avatar"; import { UserLink } from "../../user-link"; import StreamStatusBadge from "../../../views/streams/components/status-badge"; import { NoteRelays } from "../../note/note-relays"; +import { useAsync } from "react-use"; export default function StreamNote({ event, ...props }: CardProps & { event: NostrEvent }) { - const stream = useMemo(() => parseStreamEvent(event), [event]); - const { title, image } = stream; + const { value: stream, error } = useAsync(async () => parseStreamEvent(event), [event]); // if there is a parent intersection observer, register this card const ref = useRef(null); @@ -36,6 +35,8 @@ export default function StreamNote({ event, ...props }: CardProps & { event: Nos const naddr = useEventNaddr(event); + if (!stream || error) return null; + return ( @@ -47,10 +48,10 @@ export default function StreamNote({ event, ...props }: CardProps & { event: Nos - {image && {title}} + {stream.image && {stream.title}} - {title} + {stream.title} diff --git a/src/views/streams/components/stream-card.tsx b/src/views/streams/components/stream-card.tsx index fa9d6e868..99a86514b 100644 --- a/src/views/streams/components/stream-card.tsx +++ b/src/views/streams/components/stream-card.tsx @@ -27,9 +27,6 @@ import { Link as RouterLink } from "react-router-dom"; import { UserAvatar } from "../../../components/user-avatar"; import { UserLink } from "../../../components/user-link"; import dayjs from "dayjs"; -import relayScoreboardService from "../../../services/relay-scoreboard"; -import { getEventRelays } from "../../../services/event-relays"; -import { nip19 } from "nostr-tools"; import StreamStatusBadge from "./status-badge"; import { CodeIcon } from "../../../components/icons"; import RawValue from "../../../components/debug-modals/raw-value"; @@ -71,7 +68,7 @@ export default function StreamCard({ stream, ...props }: CardProps & { stream: P ))} )} - Updated: {dayjs.unix(stream.updated).fromNow()} + {stream.starts && Started: {dayjs.unix(stream.starts).fromNow()}} diff --git a/src/views/streams/index.tsx b/src/views/streams/index.tsx index 88ede7af5..38285ad40 100644 --- a/src/views/streams/index.tsx +++ b/src/views/streams/index.tsx @@ -10,6 +10,9 @@ import { NostrEvent } from "../../types/nostr-event"; import RelaySelectionButton from "../../components/relay-selection/relay-selection-button"; import RelaySelectionProvider, { useRelaySelectionRelays } from "../../providers/relay-selection-provider"; import useRelaysChanged from "../../hooks/use-relays-changed"; +import PeopleListSelection from "../../components/people-list-selection/people-list-selection"; +import PeopleListProvider, { usePeopleListContext } from "../../components/people-list-selection/people-list-provider"; +import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status"; function StreamsPage() { // hard code damus and snort relays for finding streams @@ -27,7 +30,15 @@ function StreamsPage() { [filterStatus] ); - const timeline = useTimelineLoader(`streams`, readRelays, { kinds: [STREAM_KIND] }, { eventFilter }); + const { people } = usePeopleListContext(); + const query = + people.length > 0 + ? [ + { authors: people, kinds: [STREAM_KIND] }, + { "#p": people, kinds: [STREAM_KIND] }, + ] + : { kinds: [STREAM_KIND] }; + const timeline = useTimelineLoader(`streams`, readRelays, query, { eventFilter }); useRelaysChanged(readRelays, () => timeline.reset()); @@ -45,12 +56,13 @@ function StreamsPage() { } } catch (e) {} } - return Array.from(Object.values(parsedStreams)).sort((a, b) => b.updated - a.updated); + return Array.from(Object.values(parsedStreams)).sort((a, b) => (b.starts ?? 0) - (a.starts ?? 0)); }, [events]); return ( +