add people list select

This commit is contained in:
hzrd149 2023-07-30 11:45:58 -05:00
parent 5c061cad48
commit 68001bbe77
7 changed files with 146 additions and 16 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add people list context and selector

View File

@ -1,10 +1,12 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { utils } from "nostr-tools"; import { utils } from "nostr-tools";
import debug, { Debug, Debugger } from "debug";
import { NostrEvent } from "../types/nostr-event"; import { NostrEvent } from "../types/nostr-event";
import { NostrQuery, NostrRequestFilter } from "../types/nostr-query"; import { NostrQuery, NostrRequestFilter } from "../types/nostr-query";
import { NostrRequest } from "./nostr-request"; import { NostrRequest } from "./nostr-request";
import { NostrMultiSubscription } from "./nostr-multi-subscription"; import { NostrMultiSubscription } from "./nostr-multi-subscription";
import Subject, { PersistentSubject } from "./subject"; import Subject, { PersistentSubject } from "./subject";
import { logger } from "../helpers/debug";
function addToQuery(filter: NostrRequestFilter, query: NostrQuery) { function addToQuery(filter: NostrRequestFilter, query: NostrQuery) {
if (Array.isArray(filter)) { if (Array.isArray(filter)) {
@ -23,6 +25,7 @@ class RelayTimelineLoader {
blockSize = BLOCK_SIZE; blockSize = BLOCK_SIZE;
private name?: string; private name?: string;
private requestId = 0; private requestId = 0;
private log: Debugger;
loading = false; loading = false;
events: NostrEvent[] = []; events: NostrEvent[] = [];
@ -32,17 +35,19 @@ class RelayTimelineLoader {
onEvent = new Subject<NostrEvent>(); onEvent = new Subject<NostrEvent>();
onBlockFinish = new Subject<void>(); onBlockFinish = new Subject<void>();
constructor(relay: string, query: NostrRequestFilter, name?: string) { constructor(relay: string, query: NostrRequestFilter, name: string, log?: Debugger) {
this.relay = relay; this.relay = relay;
this.query = query; this.query = query;
this.name = name; this.name = name;
this.log = log || logger.extend(name);
} }
loadNextBlock() { loadNextBlock() {
this.loading = true; this.loading = true;
let query: NostrRequestFilter = addToQuery(this.query, { limit: this.blockSize }); let query: NostrRequestFilter = addToQuery(this.query, { limit: this.blockSize });
if (this.events[this.events.length - 1]) { 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++); const request = new NostrRequest([this.relay], undefined, this.name + "-" + this.requestId++);
@ -56,6 +61,7 @@ class RelayTimelineLoader {
request.onComplete.then(() => { request.onComplete.then(() => {
this.loading = false; this.loading = false;
if (gotEvents === 0) this.complete = true; if (gotEvents === 0) this.complete = true;
this.log(`Got ${gotEvents} events`);
this.onBlockFinish.next(); this.onBlockFinish.next();
}); });
@ -95,11 +101,15 @@ export class TimelineLoader {
loadNextBlockBuffer = 2; loadNextBlockBuffer = 2;
eventFilter?: (event: NostrEvent) => boolean; eventFilter?: (event: NostrEvent) => boolean;
private name: string;
private log: Debugger;
private subscription: NostrMultiSubscription; private subscription: NostrMultiSubscription;
private relayTimelineLoaders = new Map<string, RelayTimelineLoader>(); private relayTimelineLoaders = new Map<string, RelayTimelineLoader>();
constructor(name?: string) { constructor(name: string) {
this.name = name;
this.log = logger.extend("TimelineLoader:" + name);
this.subscription = new NostrMultiSubscription([], undefined, name); this.subscription = new NostrMultiSubscription([], undefined, name);
this.subscription.onEvent.subscribe(this.handleEvent, this); this.subscription.onEvent.subscribe(this.handleEvent, this);
} }
@ -121,7 +131,7 @@ export class TimelineLoader {
for (const relay of this.relays) { for (const relay of this.relays) {
if (!this.relayTimelineLoaders.has(relay)) { 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); this.relayTimelineLoaders.set(relay, loader);
loader.onEvent.subscribe(this.handleEvent, this); loader.onEvent.subscribe(this.handleEvent, this);
loader.onBlockFinish.subscribe(this.updateLoading, this); loader.onBlockFinish.subscribe(this.updateLoading, this);
@ -195,10 +205,13 @@ export class TimelineLoader {
} }
/** @deprecated */ /** @deprecated */
loadMore() { loadMore() {
let triggeredLoad = false;
for (const [relay, loader] of this.relayTimelineLoaders) { for (const [relay, loader] of this.relayTimelineLoaders) {
if (loader.complete || loader.loading) continue; if (loader.complete || loader.loading) continue;
loader.loadNextBlock(); loader.loadNextBlock();
triggeredLoad = true;
} }
if (triggeredLoad) this.updateLoading();
} }
private updateLoading() { private updateLoading() {

View File

@ -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<PeopleListContextType>({ 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 <PeopleListContext.Provider value={context}>{children}</PeopleListContext.Provider>;
}

View File

@ -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<SelectProps, "value" | "onChange" | "children">) {
const { people, list, setList } = usePeopleListContext();
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<Select
value={list}
onChange={(e) => {
setList(e.target.value);
}}
{...props}
>
<option value="following">Following</option>
{!hideGlobalOption && <option value="global">Global</option>}
</Select>
);
}

View File

@ -1,7 +1,6 @@
import { useMemo, useRef } from "react"; import { useMemo, useRef } from "react";
import { import {
Badge, Badge,
Box,
Card, Card,
CardBody, CardBody,
CardFooter, CardFooter,
@ -25,10 +24,10 @@ import { UserAvatar } from "../../user-avatar";
import { UserLink } from "../../user-link"; import { UserLink } from "../../user-link";
import StreamStatusBadge from "../../../views/streams/components/status-badge"; import StreamStatusBadge from "../../../views/streams/components/status-badge";
import { NoteRelays } from "../../note/note-relays"; import { NoteRelays } from "../../note/note-relays";
import { useAsync } from "react-use";
export default function StreamNote({ event, ...props }: CardProps & { event: NostrEvent }) { export default function StreamNote({ event, ...props }: CardProps & { event: NostrEvent }) {
const stream = useMemo(() => parseStreamEvent(event), [event]); const { value: stream, error } = useAsync(async () => parseStreamEvent(event), [event]);
const { title, image } = stream;
// if there is a parent intersection observer, register this card // if there is a parent intersection observer, register this card
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
@ -36,6 +35,8 @@ export default function StreamNote({ event, ...props }: CardProps & { event: Nos
const naddr = useEventNaddr(event); const naddr = useEventNaddr(event);
if (!stream || error) return null;
return ( return (
<Card {...props} ref={ref}> <Card {...props} ref={ref}>
<LinkBox as={CardBody} p="2" display="flex" flexDirection="column" gap="2"> <LinkBox as={CardBody} p="2" display="flex" flexDirection="column" gap="2">
@ -47,10 +48,10 @@ export default function StreamNote({ event, ...props }: CardProps & { event: Nos
<UserLink pubkey={stream.host} /> <UserLink pubkey={stream.host} />
</Heading> </Heading>
</Flex> </Flex>
{image && <Image src={image} alt={title} borderRadius="lg" maxH="15rem" />} {stream.image && <Image src={stream.image} alt={stream.title} borderRadius="lg" maxH="15rem" />}
<Heading size="md"> <Heading size="md">
<LinkOverlay as={RouterLink} to={`/streams/${naddr}`}> <LinkOverlay as={RouterLink} to={`/streams/${naddr}`}>
{title} {stream.title}
</LinkOverlay> </LinkOverlay>
</Heading> </Heading>
</Flex> </Flex>

View File

@ -27,9 +27,6 @@ import { Link as RouterLink } from "react-router-dom";
import { UserAvatar } from "../../../components/user-avatar"; import { UserAvatar } from "../../../components/user-avatar";
import { UserLink } from "../../../components/user-link"; import { UserLink } from "../../../components/user-link";
import dayjs from "dayjs"; 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 StreamStatusBadge from "./status-badge";
import { CodeIcon } from "../../../components/icons"; import { CodeIcon } from "../../../components/icons";
import RawValue from "../../../components/debug-modals/raw-value"; import RawValue from "../../../components/debug-modals/raw-value";
@ -71,7 +68,7 @@ export default function StreamCard({ stream, ...props }: CardProps & { stream: P
))} ))}
</Flex> </Flex>
)} )}
<Text>Updated: {dayjs.unix(stream.updated).fromNow()}</Text> {stream.starts && <Text>Started: {dayjs.unix(stream.starts).fromNow()}</Text>}
</LinkBox> </LinkBox>
<Divider /> <Divider />
<CardFooter p="2" display="flex" gap="2" alignItems="center"> <CardFooter p="2" display="flex" gap="2" alignItems="center">

View File

@ -10,6 +10,9 @@ import { NostrEvent } from "../../types/nostr-event";
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button"; import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
import RelaySelectionProvider, { useRelaySelectionRelays } from "../../providers/relay-selection-provider"; import RelaySelectionProvider, { useRelaySelectionRelays } from "../../providers/relay-selection-provider";
import useRelaysChanged from "../../hooks/use-relays-changed"; 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() { function StreamsPage() {
// hard code damus and snort relays for finding streams // hard code damus and snort relays for finding streams
@ -27,7 +30,15 @@ function StreamsPage() {
[filterStatus] [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()); useRelaysChanged(readRelays, () => timeline.reset());
@ -45,12 +56,13 @@ function StreamsPage() {
} }
} catch (e) {} } 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]); }, [events]);
return ( return (
<Flex p="2" gap="2" overflow="hidden" direction="column"> <Flex p="2" gap="2" overflow="hidden" direction="column">
<Flex gap="2"> <Flex gap="2">
<PeopleListSelection maxW="sm" />
<Select maxW="sm" value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)}> <Select maxW="sm" value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)}>
<option value="live">Live</option> <option value="live">Live</option>
<option value="ended">Ended</option> <option value="ended">Ended</option>
@ -62,6 +74,7 @@ function StreamsPage() {
{streams.map((stream) => ( {streams.map((stream) => (
<StreamCard key={stream.event.id} stream={stream} w="sm" /> <StreamCard key={stream.event.id} stream={stream} w="sm" />
))} ))}
<TimelineActionAndStatus timeline={timeline} />
</Flex> </Flex>
</IntersectionObserverProvider> </IntersectionObserverProvider>
</Flex> </Flex>
@ -72,7 +85,9 @@ export default function StreamsView() {
<RelaySelectionProvider <RelaySelectionProvider
additionalDefaults={["wss://nos.lol", "wss://relay.damus.io", "wss://relay.snort.social", "wss://nostr.wine"]} additionalDefaults={["wss://nos.lol", "wss://relay.damus.io", "wss://relay.snort.social", "wss://nostr.wine"]}
> >
<PeopleListProvider>
<StreamsPage /> <StreamsPage />
</PeopleListProvider>
</RelaySelectionProvider> </RelaySelectionProvider>
); );
} }