mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-14 06:39:19 +02:00
add people list select
This commit is contained in:
parent
5c061cad48
commit
68001bbe77
5
.changeset/rotten-donuts-reflect.md
Normal file
5
.changeset/rotten-donuts-reflect.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add people list context and selector
|
@ -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() {
|
||||||
|
@ -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>;
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user