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 { 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<NostrEvent>();
onBlockFinish = new Subject<void>();
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<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.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() {

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 {
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<HTMLDivElement | null>(null);
@ -36,6 +35,8 @@ export default function StreamNote({ event, ...props }: CardProps & { event: Nos
const naddr = useEventNaddr(event);
if (!stream || error) return null;
return (
<Card {...props} ref={ref}>
<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} />
</Heading>
</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">
<LinkOverlay as={RouterLink} to={`/streams/${naddr}`}>
{title}
{stream.title}
</LinkOverlay>
</Heading>
</Flex>

View File

@ -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
))}
</Flex>
)}
<Text>Updated: {dayjs.unix(stream.updated).fromNow()}</Text>
{stream.starts && <Text>Started: {dayjs.unix(stream.starts).fromNow()}</Text>}
</LinkBox>
<Divider />
<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 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 (
<Flex p="2" gap="2" overflow="hidden" direction="column">
<Flex gap="2">
<PeopleListSelection maxW="sm" />
<Select maxW="sm" value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)}>
<option value="live">Live</option>
<option value="ended">Ended</option>
@ -62,6 +74,7 @@ function StreamsPage() {
{streams.map((stream) => (
<StreamCard key={stream.event.id} stream={stream} w="sm" />
))}
<TimelineActionAndStatus timeline={timeline} />
</Flex>
</IntersectionObserverProvider>
</Flex>
@ -72,7 +85,9 @@ export default function StreamsView() {
<RelaySelectionProvider
additionalDefaults={["wss://nos.lol", "wss://relay.damus.io", "wss://relay.snort.social", "wss://nostr.wine"]}
>
<StreamsPage />
<PeopleListProvider>
<StreamsPage />
</PeopleListProvider>
</RelaySelectionProvider>
);
}