mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-26 17:52:18 +01:00
timeline and relay selection cleanup
This commit is contained in:
parent
facb287433
commit
e6b773980a
@ -36,8 +36,9 @@ import Nip19ToolsView from "./views/tools/nip19";
|
||||
import UserAboutTab from "./views/user/about";
|
||||
import UserLikesTab from "./views/user/likes";
|
||||
import useSetColorMode from "./hooks/use-set-color-mode";
|
||||
import UserStreamsTab from "./views/user/streams";
|
||||
|
||||
const LiveStreamsTab = React.lazy(() => import("./views/streams"));
|
||||
const StreamsView = React.lazy(() => import("./views/streams"));
|
||||
const StreamView = React.lazy(() => import("./views/streams/stream"));
|
||||
const SearchView = React.lazy(() => import("./views/search"));
|
||||
|
||||
@ -78,6 +79,7 @@ const router = createHashRouter([
|
||||
{ path: "about", element: <UserAboutTab /> },
|
||||
{ path: "notes", element: <UserNotesTab /> },
|
||||
{ path: "media", element: <UserMediaTab /> },
|
||||
{ path: "streams", element: <UserStreamsTab /> },
|
||||
{ path: "zaps", element: <UserZapsTab /> },
|
||||
{ path: "likes", element: <UserLikesTab /> },
|
||||
{ path: "followers", element: <UserFollowersTab /> },
|
||||
@ -106,7 +108,7 @@ const router = createHashRouter([
|
||||
},
|
||||
{
|
||||
path: "streams",
|
||||
element: <LiveStreamsTab />,
|
||||
element: <StreamsView />,
|
||||
},
|
||||
{ path: "l/:link", element: <NostrLinkView /> },
|
||||
{ path: "t/:hashtag", element: <HashTagView /> },
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Subject } from "./subject";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { NostrOutgoingMessage, NostrQuery } from "../types/nostr-query";
|
||||
import { NostrOutgoingMessage, NostrRequestFilter } from "../types/nostr-query";
|
||||
import { IncomingEvent, Relay } from "./relay";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
|
||||
@ -13,14 +13,14 @@ export class NostrMultiSubscription {
|
||||
|
||||
id: string;
|
||||
name?: string;
|
||||
query?: NostrQuery;
|
||||
query?: NostrRequestFilter;
|
||||
relayUrls: string[];
|
||||
relays: Relay[];
|
||||
state = NostrMultiSubscription.INIT;
|
||||
onEvent = new Subject<NostrEvent>();
|
||||
seenEvents = new Set<string>();
|
||||
|
||||
constructor(relayUrls: string[], query?: NostrQuery, name?: string) {
|
||||
constructor(relayUrls: string[], query?: NostrRequestFilter, name?: string) {
|
||||
this.id = String(name || lastId++);
|
||||
this.query = query;
|
||||
this.name = name;
|
||||
@ -66,16 +66,20 @@ export class NostrMultiSubscription {
|
||||
if (this.state === NostrMultiSubscription.OPEN) return this;
|
||||
|
||||
this.state = NostrMultiSubscription.OPEN;
|
||||
this.send(["REQ", this.id, this.query]);
|
||||
if (Array.isArray(this.query)) {
|
||||
this.send(["REQ", this.id, ...this.query]);
|
||||
} else this.send(["REQ", this.id, this.query]);
|
||||
|
||||
this.subscribeToRelays();
|
||||
|
||||
return this;
|
||||
}
|
||||
setQuery(query: NostrQuery) {
|
||||
setQuery(query: NostrRequestFilter) {
|
||||
this.query = query;
|
||||
if (this.state === NostrMultiSubscription.OPEN) {
|
||||
this.send(["REQ", this.id, this.query]);
|
||||
if (Array.isArray(this.query)) {
|
||||
this.send(["REQ", this.id, ...this.query]);
|
||||
} else this.send(["REQ", this.id, this.query]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@ -97,7 +101,9 @@ export class NostrMultiSubscription {
|
||||
// if the subscription is open and it has a query
|
||||
if (this.state === NostrMultiSubscription.OPEN && this.query) {
|
||||
// open a connection to this relay
|
||||
relay.send(["REQ", this.id, this.query]);
|
||||
if (Array.isArray(this.query)) {
|
||||
relay.send(["REQ", this.id, ...this.query]);
|
||||
} else relay.send(["REQ", this.id, this.query]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { NostrQuery } from "../types/nostr-query";
|
||||
import { NostrRequestFilter } from "../types/nostr-query";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import { IncomingEOSE, IncomingEvent, Relay } from "./relay";
|
||||
import Subject from "./subject";
|
||||
@ -59,14 +59,16 @@ export class NostrRequest {
|
||||
}
|
||||
}
|
||||
|
||||
start(query: NostrQuery) {
|
||||
start(filter: NostrRequestFilter) {
|
||||
if (this.state !== NostrRequest.IDLE) {
|
||||
throw new Error("cant restart a nostr request");
|
||||
}
|
||||
|
||||
this.state = NostrRequest.RUNNING;
|
||||
for (const relay of this.relays) {
|
||||
relay.send(["REQ", this.id, query]);
|
||||
if (Array.isArray(filter)) {
|
||||
relay.send(["REQ", this.id, ...filter]);
|
||||
} else relay.send(["REQ", this.id, filter]);
|
||||
}
|
||||
|
||||
setTimeout(() => this.complete(), this.timeout);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { NostrOutgoingMessage, NostrQuery } from "../types/nostr-query";
|
||||
import { NostrOutgoingMessage, NostrRequestFilter } from "../types/nostr-query";
|
||||
import { IncomingEOSE, Relay } from "./relay";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import { Subject } from "./subject";
|
||||
@ -13,13 +13,13 @@ export class NostrSubscription {
|
||||
|
||||
id: string;
|
||||
name?: string;
|
||||
query?: NostrQuery;
|
||||
query?: NostrRequestFilter;
|
||||
relay: Relay;
|
||||
state = NostrSubscription.INIT;
|
||||
onEvent = new Subject<NostrEvent>();
|
||||
onEOSE = new Subject<IncomingEOSE>();
|
||||
|
||||
constructor(relayUrl: string, query?: NostrQuery, name?: string) {
|
||||
constructor(relayUrl: string, query?: NostrRequestFilter, name?: string) {
|
||||
this.id = String(name || lastId++);
|
||||
this.query = query;
|
||||
this.name = name;
|
||||
@ -43,16 +43,20 @@ export class NostrSubscription {
|
||||
if (this.state === NostrSubscription.OPEN) return this;
|
||||
|
||||
this.state = NostrSubscription.OPEN;
|
||||
this.send(["REQ", this.id, this.query]);
|
||||
if (Array.isArray(this.query)) {
|
||||
this.send(["REQ", this.id, ...this.query]);
|
||||
} else this.send(["REQ", this.id, this.query]);
|
||||
|
||||
relayPoolService.addClaim(this.relay.url, this);
|
||||
|
||||
return this;
|
||||
}
|
||||
setQuery(query: NostrQuery) {
|
||||
setQuery(query: NostrRequestFilter) {
|
||||
this.query = query;
|
||||
if (this.state === NostrSubscription.OPEN) {
|
||||
this.send(["REQ", this.id, this.query]);
|
||||
if (Array.isArray(this.query)) {
|
||||
this.send(["REQ", this.id, ...this.query]);
|
||||
} else this.send(["REQ", this.id, this.query]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
@ -1,18 +1,25 @@
|
||||
import dayjs from "dayjs";
|
||||
import { utils } from "nostr-tools";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { NostrQuery } from "../types/nostr-query";
|
||||
import { NostrQuery, NostrRequestFilter } from "../types/nostr-query";
|
||||
import { NostrRequest } from "./nostr-request";
|
||||
import { NostrMultiSubscription } from "./nostr-multi-subscription";
|
||||
import Subject, { PersistentSubject } from "./subject";
|
||||
|
||||
function addToQuery(filter: NostrRequestFilter, query: NostrQuery) {
|
||||
if (Array.isArray(filter)) {
|
||||
return filter.map((f) => ({ ...f, ...query }));
|
||||
}
|
||||
return { ...filter, ...query };
|
||||
}
|
||||
|
||||
const BLOCK_SIZE = 20;
|
||||
|
||||
type EventFilter = (event: NostrEvent) => boolean;
|
||||
|
||||
class RelayTimelineLoader {
|
||||
relay: string;
|
||||
query: NostrQuery;
|
||||
query: NostrRequestFilter;
|
||||
blockSize = BLOCK_SIZE;
|
||||
private name?: string;
|
||||
private requestId = 0;
|
||||
@ -25,7 +32,7 @@ class RelayTimelineLoader {
|
||||
onEvent = new Subject<NostrEvent>();
|
||||
onBlockFinish = new Subject<void>();
|
||||
|
||||
constructor(relay: string, query: NostrQuery, name?: string) {
|
||||
constructor(relay: string, query: NostrRequestFilter, name?: string) {
|
||||
this.relay = relay;
|
||||
this.query = query;
|
||||
this.name = name;
|
||||
@ -33,9 +40,9 @@ class RelayTimelineLoader {
|
||||
|
||||
loadNextBlock() {
|
||||
this.loading = true;
|
||||
const query: NostrQuery = { ...this.query, limit: this.blockSize };
|
||||
let query: NostrRequestFilter = addToQuery(this.query, { limit: this.blockSize });
|
||||
if (this.events[this.events.length - 1]) {
|
||||
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++);
|
||||
@ -77,7 +84,7 @@ class RelayTimelineLoader {
|
||||
|
||||
export class TimelineLoader {
|
||||
cursor = dayjs().unix();
|
||||
query?: NostrQuery;
|
||||
query?: NostrRequestFilter;
|
||||
relays: string[] = [];
|
||||
|
||||
events = new PersistentSubject<NostrEvent[]>([]);
|
||||
@ -145,7 +152,7 @@ export class TimelineLoader {
|
||||
this.subscription.setRelays(relays);
|
||||
this.updateComplete();
|
||||
}
|
||||
setQuery(query: NostrQuery) {
|
||||
setQuery(query: NostrRequestFilter) {
|
||||
if (JSON.stringify(this.query) === JSON.stringify(query)) return;
|
||||
|
||||
this.removeLoaders();
|
||||
@ -160,7 +167,7 @@ export class TimelineLoader {
|
||||
|
||||
// update the subscription
|
||||
this.subscription.forgetEvents();
|
||||
this.subscription.setQuery({ ...query, limit: BLOCK_SIZE / 2 });
|
||||
this.subscription.setQuery(addToQuery(query, { limit: BLOCK_SIZE / 2 }));
|
||||
}
|
||||
setFilter(filter?: (event: NostrEvent) => boolean) {
|
||||
this.eventFilter = filter;
|
||||
@ -221,6 +228,12 @@ export class TimelineLoader {
|
||||
this.subscription.close();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.cursor = dayjs().unix();
|
||||
this.relayTimelineLoaders.clear();
|
||||
this.forgetEvents();
|
||||
}
|
||||
|
||||
// TODO: this is only needed because the current logic dose not remove events when the relay they where fetched from is removed
|
||||
/** @deprecated */
|
||||
forgetEvents() {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Box, Code, Flex, Heading } from "@chakra-ui/react";
|
||||
import { CopyIconButton } from "../copy-icon-button";
|
||||
|
||||
export default function RawValue({ value, heading }: { heading: string; value: string }) {
|
||||
export default function RawValue({ value, heading }: { heading: string; value?: string | null }) {
|
||||
return (
|
||||
<Box>
|
||||
<Heading size="sm" mb="2">
|
||||
@ -11,7 +11,7 @@ export default function RawValue({ value, heading }: { heading: string; value: s
|
||||
<Code fontSize="md" wordBreak="break-all">
|
||||
{value}
|
||||
</Code>
|
||||
<CopyIconButton text={value} size="xs" aria-label="copy" />
|
||||
<CopyIconButton text={String(value)} size="xs" aria-label="copy" />
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
|
@ -1,23 +0,0 @@
|
||||
import React from "react";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
import { TimelineLoader } from "../classes/timeline-loader";
|
||||
import RepostNote from "./repost-note";
|
||||
import { Note } from "./note";
|
||||
|
||||
const GenericNoteTimeline = React.memo(({ timeline }: { timeline: TimelineLoader }) => {
|
||||
const notes = useSubject(timeline.timeline);
|
||||
|
||||
return (
|
||||
<>
|
||||
{notes.map((note) =>
|
||||
note.kind === 6 ? (
|
||||
<RepostNote key={note.id} event={note} maxHeight={1200} />
|
||||
) : (
|
||||
<Note key={note.id} event={note} maxHeight={1200} />
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default GenericNoteTimeline;
|
15
src/components/relay-selection/relay-selection-button.tsx
Normal file
15
src/components/relay-selection/relay-selection-button.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { Button, ButtonProps } from "@chakra-ui/react";
|
||||
import { RelayIcon } from "../icons";
|
||||
import { useRelaySelectionContext } from "../../providers/relay-selection-provider";
|
||||
|
||||
export default function RelaySelectionButton({ ...props }: ButtonProps) {
|
||||
const { openModal, relays } = useRelaySelectionContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button leftIcon={<RelayIcon />} onClick={openModal} {...props}>
|
||||
{relays.length} {relays.length === 1 ? "Relay" : "Relays"}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
@ -15,8 +15,8 @@ import {
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { RelayFavicon } from "../../components/relay-favicon";
|
||||
import { RelayUrlInput } from "../../components/relay-url-input";
|
||||
import { RelayFavicon } from "../relay-favicon";
|
||||
import { RelayUrlInput } from "../relay-url-input";
|
||||
import { normalizeRelayUrl } from "../../helpers/url";
|
||||
import { unique } from "../../helpers/array";
|
||||
import relayScoreboardService from "../../services/relay-scoreboard";
|
||||
@ -63,7 +63,7 @@ export default function RelaySelectionModal({
|
||||
const relays = useReadRelayUrls([...selected, ...newSelected, ...Array.from(manuallyAddedRelays)]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} onClose={onClose} closeOnOverlayClick={false}>
|
||||
<Modal isOpen={true} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Select Relays</ModalHeader>
|
||||
@ -108,7 +108,7 @@ export default function RelaySelectionModal({
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Save
|
||||
Set relays
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
37
src/components/timeline/generic-note-timeline.tsx
Normal file
37
src/components/timeline/generic-note-timeline.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { TimelineLoader } from "../../classes/timeline-loader";
|
||||
import RepostNote from "./repost-note";
|
||||
import { Note } from "../note";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { Text } from "@chakra-ui/react";
|
||||
import { Kind } from "nostr-tools";
|
||||
import { STREAM_KIND } from "../../helpers/nostr/stream";
|
||||
import StreamNote from "./stream-note";
|
||||
|
||||
const RenderEvent = React.memo(({ event }: { event: NostrEvent }) => {
|
||||
switch (event.kind) {
|
||||
case Kind.Text:
|
||||
return <Note event={event} maxHeight={1200} />;
|
||||
case Kind.Repost:
|
||||
return <RepostNote event={event} maxHeight={1200} />;
|
||||
case STREAM_KIND:
|
||||
return <StreamNote event={event} />;
|
||||
default:
|
||||
return <Text>Unknown event kind: {event.kind}</Text>;
|
||||
}
|
||||
});
|
||||
|
||||
const GenericNoteTimeline = React.memo(({ timeline }: { timeline: TimelineLoader }) => {
|
||||
const notes = useSubject(timeline.timeline);
|
||||
|
||||
return (
|
||||
<>
|
||||
{notes.map((note) => (
|
||||
<RenderEvent key={note.id} event={note} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default GenericNoteTimeline;
|
@ -1,19 +1,19 @@
|
||||
import { useRef } from "react";
|
||||
import { Flex, Heading, SkeletonText, Text } from "@chakra-ui/react";
|
||||
import { useAsync } from "react-use";
|
||||
import singleEventService from "../services/single-event";
|
||||
import { isETag, NostrEvent } from "../types/nostr-event";
|
||||
import { ErrorFallback } from "./error-boundary";
|
||||
import { Note } from "./note";
|
||||
import { NoteMenu } from "./note/note-menu";
|
||||
import { UserAvatar } from "./user-avatar";
|
||||
import { UserDnsIdentityIcon } from "./user-dns-identity-icon";
|
||||
import { UserLink } from "./user-link";
|
||||
import { TrustProvider } from "../providers/trust";
|
||||
import { safeJson } from "../helpers/parse";
|
||||
import singleEventService from "../../services/single-event";
|
||||
import { isETag, NostrEvent } from "../../types/nostr-event";
|
||||
import { ErrorFallback } from "../error-boundary";
|
||||
import { Note } from "../note";
|
||||
import { NoteMenu } from "../note/note-menu";
|
||||
import { UserAvatar } from "../user-avatar";
|
||||
import { UserDnsIdentityIcon } from "../user-dns-identity-icon";
|
||||
import { UserLink } from "../user-link";
|
||||
import { TrustProvider } from "../../providers/trust";
|
||||
import { safeJson } from "../../helpers/parse";
|
||||
import { verifySignature } from "nostr-tools";
|
||||
import { useReadRelayUrls } from "../hooks/use-client-relays";
|
||||
import { useRegisterIntersectionEntity } from "../providers/intersection-observer";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
||||
|
||||
function parseHardcodedNoteContent(event: NostrEvent): NostrEvent | null {
|
||||
const json = safeJson(event.content, null);
|
75
src/components/timeline/stream-note.tsx
Normal file
75
src/components/timeline/stream-note.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { useMemo, useRef } from "react";
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Card,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
CardProps,
|
||||
Divider,
|
||||
Flex,
|
||||
Heading,
|
||||
Image,
|
||||
LinkBox,
|
||||
LinkOverlay,
|
||||
Spacer,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import dayjs from "dayjs";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { parseStreamEvent } from "../../helpers/nostr/stream";
|
||||
import useEventNaddr from "../../hooks/use-event-naddr";
|
||||
import { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
||||
import { UserAvatar } from "../user-avatar";
|
||||
import { UserLink } from "../user-link";
|
||||
import StreamStatusBadge from "../../views/streams/components/status-badge";
|
||||
import { NoteRelays } from "../note/note-relays";
|
||||
|
||||
export default function StreamNote({ event, ...props }: CardProps & { event: NostrEvent }) {
|
||||
const stream = useMemo(() => parseStreamEvent(event), [event]);
|
||||
const { title, image } = stream;
|
||||
|
||||
// if there is a parent intersection observer, register this card
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, event.id);
|
||||
|
||||
const naddr = useEventNaddr(event);
|
||||
|
||||
return (
|
||||
<Card {...props} ref={ref}>
|
||||
<LinkBox as={CardBody} p="2" display="flex" flexDirection="column" gap="2">
|
||||
<Flex gap="2">
|
||||
<Flex gap="2" direction="column">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<UserAvatar pubkey={stream.host} size="sm" noProxy />
|
||||
<Heading size="sm">
|
||||
<UserLink pubkey={stream.host} />
|
||||
</Heading>
|
||||
</Flex>
|
||||
{image && <Image src={image} alt={title} borderRadius="lg" maxH="15rem" />}
|
||||
<Heading size="md">
|
||||
<LinkOverlay as={RouterLink} to={`/streams/${naddr}`}>
|
||||
{title}
|
||||
</LinkOverlay>
|
||||
</Heading>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{stream.tags.length > 0 && (
|
||||
<Flex gap="2" wrap="wrap">
|
||||
{stream.tags.map((tag) => (
|
||||
<Badge key={tag}>{tag}</Badge>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
<Text>Updated: {dayjs.unix(stream.updated).fromNow()}</Text>
|
||||
</LinkBox>
|
||||
<Divider />
|
||||
<CardFooter p="2" display="flex" gap="2" alignItems="center">
|
||||
<StreamStatusBadge stream={stream} />
|
||||
<Spacer />
|
||||
<NoteRelays event={stream.event} />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
@ -2,11 +2,14 @@ import dayjs from "dayjs";
|
||||
import { DraftNostrEvent, NostrEvent, isPTag } from "../../types/nostr-event";
|
||||
import { unique } from "../array";
|
||||
|
||||
export const STREAM_KIND = 30311;
|
||||
export const STREAM_CHAT_MESSAGE_KIND = 1311;
|
||||
|
||||
export type ParsedStream = {
|
||||
event: NostrEvent;
|
||||
author: string;
|
||||
host: string;
|
||||
title: string;
|
||||
title?: string;
|
||||
summary?: string;
|
||||
image?: string;
|
||||
updated: number;
|
||||
@ -30,7 +33,6 @@ export function parseStreamEvent(stream: NostrEvent): ParsedStream {
|
||||
const startTime = starts ? parseInt(starts) : stream.created_at;
|
||||
const endTime = endsTag ? parseInt(endsTag) : dayjs(startTime).add(4, "hour").unix();
|
||||
|
||||
if (!title) throw new Error("missing title");
|
||||
if (!identifier) throw new Error("missing identifier");
|
||||
if (!streaming) throw new Error("missing streaming");
|
||||
|
||||
@ -73,7 +75,7 @@ export function buildChatMessage(stream: ParsedStream, content: string) {
|
||||
tags: [["a", getATag(stream), "", "root"]],
|
||||
content,
|
||||
created_at: dayjs().unix(),
|
||||
kind: 1311,
|
||||
kind: STREAM_CHAT_MESSAGE_KIND,
|
||||
};
|
||||
|
||||
return template;
|
||||
|
23
src/hooks/use-event-naddr.ts
Normal file
23
src/hooks/use-event-naddr.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { useMemo } from "react";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { getEventRelays } from "../services/event-relays";
|
||||
import relayScoreboardService from "../services/relay-scoreboard";
|
||||
|
||||
export default function useEventNaddr(event: NostrEvent) {
|
||||
return useMemo(() => {
|
||||
const identifier = event.tags.find((t) => t[0] === "d" && t[1])?.[1];
|
||||
const relays = getEventRelays(event.id).value;
|
||||
const ranked = relayScoreboardService.getRankedRelays(relays);
|
||||
const onlyTwo = ranked.slice(0, 2);
|
||||
|
||||
if (!identifier) return null;
|
||||
|
||||
return nip19.naddrEncode({
|
||||
identifier,
|
||||
relays: onlyTwo,
|
||||
pubkey: event.pubkey,
|
||||
kind: event.kind,
|
||||
});
|
||||
}, [event]);
|
||||
}
|
15
src/hooks/use-relays-changed.ts
Normal file
15
src/hooks/use-relays-changed.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { usePrevious } from "react-use";
|
||||
|
||||
export default function useRelaysChanged(relays: string[], cb: (relays: string[]) => void) {
|
||||
const callback = useRef(cb);
|
||||
callback.current = cb;
|
||||
|
||||
const prev = usePrevious(relays);
|
||||
useEffect(() => {
|
||||
if (!!prev && prev?.join(",") !== relays.join(",")) {
|
||||
// always call the latest callback
|
||||
callback.current(relays);
|
||||
}
|
||||
}, [relays.join(",")]);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useUnmount } from "react-use";
|
||||
import { NostrQuery } from "../types/nostr-query";
|
||||
import { NostrRequestFilter } from "../types/nostr-query";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import timelineCacheService from "../services/timeline-cache";
|
||||
|
||||
@ -10,7 +10,7 @@ type Options = {
|
||||
cursor?: number;
|
||||
};
|
||||
|
||||
export function useTimelineLoader(key: string, relays: string[], query: NostrQuery, opts?: Options) {
|
||||
export function useTimelineLoader(key: string, relays: string[], query: NostrRequestFilter, opts?: Options) {
|
||||
const timeline = useMemo(() => timelineCacheService.createTimeline(key), [key]);
|
||||
|
||||
useEffect(() => {
|
||||
|
62
src/providers/relay-selection-provider.tsx
Normal file
62
src/providers/relay-selection-provider.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { PropsWithChildren, createContext, useCallback, useContext, useMemo } from "react";
|
||||
import { useReadRelayUrls } from "../hooks/use-client-relays";
|
||||
import { useDisclosure } from "@chakra-ui/react";
|
||||
import RelaySelectionModal from "../components/relay-selection/relay-selection-modal";
|
||||
import { unique } from "../helpers/array";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
type RelaySelectionContextType = {
|
||||
relays: string[];
|
||||
setSelected: (relays: string[]) => void;
|
||||
openModal: () => void;
|
||||
};
|
||||
|
||||
export const RelaySelectionContext = createContext<RelaySelectionContextType>({
|
||||
relays: [],
|
||||
setSelected: () => {},
|
||||
openModal: () => {},
|
||||
});
|
||||
|
||||
export function useRelaySelectionContext() {
|
||||
return useContext(RelaySelectionContext);
|
||||
}
|
||||
export function useRelaySelectionRelays() {
|
||||
return useContext(RelaySelectionContext).relays;
|
||||
}
|
||||
|
||||
export type RelaySelectionProviderProps = PropsWithChildren & {
|
||||
overrideDefault?: string[];
|
||||
additionalDefaults?: string[];
|
||||
};
|
||||
|
||||
export default function RelaySelectionProvider({
|
||||
children,
|
||||
overrideDefault,
|
||||
additionalDefaults,
|
||||
}: RelaySelectionProviderProps) {
|
||||
const relaysModal = useDisclosure();
|
||||
const { state } = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const userReadRelays = useReadRelayUrls();
|
||||
const relays = useMemo(() => {
|
||||
if (state?.relays) return state.relays;
|
||||
if (overrideDefault) return overrideDefault;
|
||||
if (additionalDefaults) return unique([...userReadRelays, ...additionalDefaults]);
|
||||
return userReadRelays;
|
||||
}, [state?.relays, overrideDefault, userReadRelays, additionalDefaults]);
|
||||
|
||||
const setSelected = useCallback((relays: string[]) => {
|
||||
navigate(".", { state: { relays }, replace: true });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<RelaySelectionContext.Provider value={{ relays, setSelected, openModal: relaysModal.onOpen }}>
|
||||
{children}
|
||||
|
||||
{relaysModal.isOpen && (
|
||||
<RelaySelectionModal selected={relays} onSubmit={setSelected} onClose={relaysModal.onClose} />
|
||||
)}
|
||||
</RelaySelectionContext.Provider>
|
||||
);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { NostrEvent } from "./nostr-event";
|
||||
|
||||
export type NostrOutgoingEvent = ["EVENT", NostrEvent];
|
||||
export type NostrOutgoingRequest = ["REQ", string, NostrQuery];
|
||||
export type NostrOutgoingRequest = ["REQ", string, ...NostrQuery[]];
|
||||
export type NostrOutgoingClose = ["CLOSE", string];
|
||||
|
||||
export type NostrOutgoingMessage = NostrOutgoingEvent | NostrOutgoingRequest | NostrOutgoingClose;
|
||||
@ -19,3 +19,5 @@ export type NostrQuery = {
|
||||
until?: number;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export type NostrRequestFilter = NostrQuery | NostrQuery[];
|
||||
|
@ -21,13 +21,16 @@ import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import { isReply } from "../../helpers/nostr-event";
|
||||
import { CheckIcon, EditIcon, RelayIcon } from "../../components/icons";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import RelaySelectionModal from "./relay-selection-modal";
|
||||
import RelaySelectionModal from "../../components/relay-selection/relay-selection-modal";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import TimelineActionAndStatus from "../../components/timeline-action-and-status";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import GenericNoteTimeline from "../../components/generic-note-timeline";
|
||||
import GenericNoteTimeline from "../../components/timeline/generic-note-timeline";
|
||||
import { unique } from "../../helpers/array";
|
||||
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
|
||||
import RelaySelectionProvider, { useRelaySelectionRelays } from "../../providers/relay-selection-provider";
|
||||
import useRelaysChanged from "../../hooks/use-relays-changed";
|
||||
|
||||
function EditableControls() {
|
||||
const { isEditing, getSubmitButtonProps, getCancelButtonProps, getEditButtonProps } = useEditableControls();
|
||||
@ -42,7 +45,7 @@ function EditableControls() {
|
||||
);
|
||||
}
|
||||
|
||||
export default function HashTagView() {
|
||||
function HashTagPage() {
|
||||
const navigate = useNavigate();
|
||||
const { hashtag } = useParams() as { hashtag: string };
|
||||
const [editableHashtag, setEditableHashtag] = useState(hashtag);
|
||||
@ -50,15 +53,7 @@ export default function HashTagView() {
|
||||
|
||||
useAppTitle("#" + hashtag);
|
||||
|
||||
const defaultRelays = useReadRelayUrls();
|
||||
const [selectedRelays, setSelectedRelays] = useState(defaultRelays);
|
||||
|
||||
// add the default relays to the selection when they load
|
||||
useEffect(() => {
|
||||
setSelectedRelays((a) => unique([...a, ...defaultRelays]));
|
||||
}, [defaultRelays.join("|")]);
|
||||
|
||||
const relaysModal = useDisclosure();
|
||||
const readRelays = useRelaySelectionRelays();
|
||||
const { isOpen: showReplies, onToggle } = useDisclosure();
|
||||
|
||||
const eventFilter = useCallback(
|
||||
@ -69,11 +64,13 @@ export default function HashTagView() {
|
||||
);
|
||||
const timeline = useTimelineLoader(
|
||||
`${hashtag}-hashtag`,
|
||||
selectedRelays,
|
||||
readRelays,
|
||||
{ kinds: [1], "#t": [hashtag] },
|
||||
{ eventFilter }
|
||||
);
|
||||
|
||||
useRelaysChanged(readRelays, () => timeline.reset());
|
||||
|
||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
@ -111,9 +108,7 @@ export default function HashTagView() {
|
||||
<Input as={EditableInput} maxW="md" />
|
||||
<EditableControls />
|
||||
</Editable>
|
||||
<Button leftIcon={<RelayIcon />} onClick={relaysModal.onOpen}>
|
||||
{selectedRelays.length} Relays
|
||||
</Button>
|
||||
<RelaySelectionButton />
|
||||
<FormControl display="flex" alignItems="center" w="auto">
|
||||
<Switch id="show-replies" isChecked={showReplies} onChange={onToggle} mr="2" />
|
||||
<FormLabel htmlFor="show-replies" mb="0">
|
||||
@ -126,17 +121,14 @@ export default function HashTagView() {
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
</Flex>
|
||||
</IntersectionObserverProvider>
|
||||
|
||||
{relaysModal.isOpen && (
|
||||
<RelaySelectionModal
|
||||
selected={selectedRelays}
|
||||
onSubmit={(relays) => {
|
||||
setSelectedRelays(relays);
|
||||
timeline.forgetEvents();
|
||||
}}
|
||||
onClose={relaysModal.onClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HashTagView() {
|
||||
return (
|
||||
<RelaySelectionProvider>
|
||||
<HashTagPage />
|
||||
</RelaySelectionProvider>
|
||||
);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import { NostrEvent } from "../../types/nostr-event";
|
||||
import TimelineActionAndStatus from "../../components/timeline-action-and-status";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import GenericNoteTimeline from "../../components/generic-note-timeline";
|
||||
import GenericNoteTimeline from "../../components/timeline/generic-note-timeline";
|
||||
|
||||
function FollowingTabBody() {
|
||||
const account = useCurrentAccount()!;
|
||||
|
@ -1,30 +1,22 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
import { Flex, FormControl, FormLabel, Select, Switch, useDisclosure } from "@chakra-ui/react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { unique } from "../../helpers/array";
|
||||
import { Flex, FormControl, FormLabel, Switch, useDisclosure } from "@chakra-ui/react";
|
||||
import { isReply } from "../../helpers/nostr-event";
|
||||
import { useAppTitle } from "../../hooks/use-app-title";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import TimelineActionAndStatus from "../../components/timeline-action-and-status";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import GenericNoteTimeline from "../../components/generic-note-timeline";
|
||||
import GenericNoteTimeline from "../../components/timeline/generic-note-timeline";
|
||||
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
|
||||
import RelaySelectionProvider, { useRelaySelectionRelays } from "../../providers/relay-selection-provider";
|
||||
import useRelaysChanged from "../../hooks/use-relays-changed";
|
||||
|
||||
export default function GlobalTab() {
|
||||
useAppTitle("global");
|
||||
const defaultRelays = useReadRelayUrls();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const selectedRelay = searchParams.get("relay") ?? "";
|
||||
const setSelectedRelay = (url: string) => {
|
||||
if (url) {
|
||||
setSearchParams({ relay: url });
|
||||
} else setSearchParams({});
|
||||
};
|
||||
function GlobalPage() {
|
||||
const readRelays = useRelaySelectionRelays();
|
||||
const { isOpen: showReplies, onToggle } = useDisclosure();
|
||||
|
||||
const availableRelays = unique([...defaultRelays, selectedRelay]).filter(Boolean);
|
||||
useAppTitle("global");
|
||||
|
||||
const eventFilter = useCallback(
|
||||
(event: NostrEvent) => {
|
||||
@ -33,12 +25,9 @@ export default function GlobalTab() {
|
||||
},
|
||||
[showReplies]
|
||||
);
|
||||
const timeline = useTimelineLoader(
|
||||
[`global`, selectedRelay].join(","),
|
||||
selectedRelay ? [selectedRelay] : [],
|
||||
{ kinds: [1] },
|
||||
{ eventFilter }
|
||||
);
|
||||
const timeline = useTimelineLoader(`global`, readRelays, { kinds: [1] }, { eventFilter });
|
||||
|
||||
useRelaysChanged(readRelays, () => timeline.reset());
|
||||
|
||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
@ -47,20 +36,7 @@ export default function GlobalTab() {
|
||||
<IntersectionObserverProvider callback={callback} root={scrollBox}>
|
||||
<Flex py="4" direction="column" gap="2" overflowY="auto" overflowX="hidden" ref={scrollBox}>
|
||||
<Flex gap="2">
|
||||
<Select
|
||||
placeholder="Select Relay"
|
||||
maxWidth="250"
|
||||
value={selectedRelay}
|
||||
onChange={(e) => {
|
||||
setSelectedRelay(e.target.value);
|
||||
}}
|
||||
>
|
||||
{availableRelays.map((url) => (
|
||||
<option key={url} value={url}>
|
||||
{url}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<RelaySelectionButton />
|
||||
<FormControl display="flex" alignItems="center">
|
||||
<Switch id="show-replies" isChecked={showReplies} onChange={onToggle} mr="2" />
|
||||
<FormLabel htmlFor="show-replies" mb="0">
|
||||
@ -75,3 +51,11 @@ export default function GlobalTab() {
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
||||
export default function GlobalTab() {
|
||||
// wrap the global page with another relay selection so it dose not effect the rest of the app
|
||||
return (
|
||||
<RelaySelectionProvider overrideDefault={["wss://welcome.nostr.wine"]}>
|
||||
<GlobalPage />
|
||||
</RelaySelectionProvider>
|
||||
);
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ import RawValue from "../../../components/debug-modals/raw-value";
|
||||
import RawJson from "../../../components/debug-modals/raw-json";
|
||||
import { NoteRelays } from "../../../components/note/note-relays";
|
||||
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||
import useEventNaddr from "../../../hooks/use-event-naddr";
|
||||
|
||||
export default function StreamCard({ stream, ...props }: CardProps & { stream: ParsedStream }) {
|
||||
const { title, identifier, image } = stream;
|
||||
@ -45,18 +46,7 @@ export default function StreamCard({ stream, ...props }: CardProps & { stream: P
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, stream.event.id);
|
||||
|
||||
const naddr = useMemo(() => {
|
||||
const relays = getEventRelays(stream.event.id).value;
|
||||
const ranked = relayScoreboardService.getRankedRelays(relays);
|
||||
const onlyTwo = ranked.slice(0, 2);
|
||||
|
||||
return nip19.naddrEncode({
|
||||
identifier,
|
||||
relays: onlyTwo,
|
||||
pubkey: stream.author,
|
||||
kind: stream.event.kind,
|
||||
});
|
||||
}, [identifier]);
|
||||
const naddr = useEventNaddr(stream.event);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -1,18 +1,19 @@
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { Flex, Select } from "@chakra-ui/react";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import StreamCard from "./components/stream-card";
|
||||
import { ParsedStream, getATag, parseStreamEvent } from "../../helpers/nostr/stream";
|
||||
import { ParsedStream, STREAM_KIND, getATag, parseStreamEvent } from "../../helpers/nostr/stream";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { RelayIconStack } from "../../components/relay-icon-stack";
|
||||
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
|
||||
import RelaySelectionProvider, { useRelaySelectionRelays } from "../../providers/relay-selection-provider";
|
||||
import useRelaysChanged from "../../hooks/use-relays-changed";
|
||||
|
||||
export default function LiveStreamsTab() {
|
||||
function StreamsPage() {
|
||||
// hard code damus and snort relays for finding streams
|
||||
const readRelays = useReadRelayUrls(["wss://relay.damus.io", "wss://relay.snort.social"]);
|
||||
const readRelays = useRelaySelectionRelays(); //useReadRelayUrls(["wss://relay.damus.io", "wss://relay.snort.social"]);
|
||||
const [filterStatus, setFilterStatus] = useState<string>("live");
|
||||
|
||||
const eventFilter = useCallback(
|
||||
@ -25,7 +26,11 @@ export default function LiveStreamsTab() {
|
||||
},
|
||||
[filterStatus]
|
||||
);
|
||||
const timeline = useTimelineLoader(`streams`, readRelays, { kinds: [30311] }, { eventFilter });
|
||||
|
||||
const timeline = useTimelineLoader(`streams`, readRelays, { kinds: [STREAM_KIND] }, { eventFilter });
|
||||
|
||||
useRelaysChanged(readRelays, () => timeline.reset());
|
||||
|
||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
@ -46,10 +51,13 @@ export default function LiveStreamsTab() {
|
||||
|
||||
return (
|
||||
<Flex p="2" gap="2" overflow="hidden" direction="column">
|
||||
<Select maxW="sm" value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)}>
|
||||
<option value="live">Live</option>
|
||||
<option value="ended">Ended</option>
|
||||
</Select>
|
||||
<Flex gap="2">
|
||||
<Select maxW="sm" value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)}>
|
||||
<option value="live">Live</option>
|
||||
<option value="ended">Ended</option>
|
||||
</Select>
|
||||
<RelaySelectionButton ml="auto" />
|
||||
</Flex>
|
||||
<IntersectionObserverProvider callback={callback} root={scrollBox}>
|
||||
<Flex gap="2" wrap="wrap" overflowY="auto" overflowX="hidden" ref={scrollBox}>
|
||||
{streams.map((stream) => (
|
||||
@ -60,3 +68,12 @@ export default function LiveStreamsTab() {
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
export default function StreamsView() {
|
||||
return (
|
||||
<RelaySelectionProvider
|
||||
additionalDefaults={["wss://nos.lol", "wss://relay.damus.io", "wss://relay.snort.social", "wss://nostr.wine"]}
|
||||
>
|
||||
<StreamsPage />
|
||||
</RelaySelectionProvider>
|
||||
);
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { Link as RouterLink, useParams, Navigate, useSearchParams } from "react-
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { Global, css } from "@emotion/react";
|
||||
|
||||
import { ParsedStream, parseStreamEvent } from "../../../helpers/nostr/stream";
|
||||
import { ParsedStream, STREAM_KIND, parseStreamEvent } from "../../../helpers/nostr/stream";
|
||||
import { NostrRequest } from "../../../classes/nostr-request";
|
||||
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
|
||||
import { unique } from "../../../helpers/array";
|
||||
@ -138,7 +138,7 @@ export default function StreamView() {
|
||||
try {
|
||||
const parsed = nip19.decode(naddr);
|
||||
if (parsed.type !== "naddr") throw new Error("Invalid stream address");
|
||||
if (parsed.data.kind !== 30311) throw new Error("Invalid stream kind");
|
||||
if (parsed.data.kind !== STREAM_KIND) throw new Error("Invalid stream kind");
|
||||
|
||||
const request = new NostrRequest(unique([...readRelays, ...(parsed.data.relays ?? [])]));
|
||||
request.onEvent.subscribe((event) => {
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
import { ParsedStream, buildChatMessage, getATag } from "../../../../helpers/nostr/stream";
|
||||
import { ParsedStream, STREAM_CHAT_MESSAGE_KIND, buildChatMessage, getATag } from "../../../../helpers/nostr/stream";
|
||||
import { useAdditionalRelayContext } from "../../../../providers/additional-relay-context";
|
||||
import { useReadRelayUrls } from "../../../../hooks/use-client-relays";
|
||||
import { useUserRelays } from "../../../../hooks/use-user-relays";
|
||||
@ -38,6 +38,7 @@ import { truncatedId } from "../../../../helpers/nostr-event";
|
||||
import { css } from "@emotion/react";
|
||||
import TopZappers from "./top-zappers";
|
||||
import { parseZapEvent } from "../../../../helpers/zaps";
|
||||
import { Kind } from "nostr-tools";
|
||||
|
||||
const hideScrollbar = css`
|
||||
scrollbar-width: 0;
|
||||
@ -64,7 +65,7 @@ export default function StreamChat({
|
||||
|
||||
const timeline = useTimelineLoader(`${truncatedId(stream.event.id)}-chat`, readRelays, {
|
||||
"#a": [getATag(stream)],
|
||||
kinds: [1311, 9735],
|
||||
kinds: [STREAM_CHAT_MESSAGE_KIND, Kind.Zap],
|
||||
});
|
||||
|
||||
const events = useSubject(timeline.timeline).sort((a, b) => b.created_at - a.created_at);
|
||||
@ -131,7 +132,7 @@ export default function StreamChat({
|
||||
css={isChatLog && hideScrollbar}
|
||||
>
|
||||
{events.map((event) =>
|
||||
event.kind === 1311 ? (
|
||||
event.kind === STREAM_CHAT_MESSAGE_KIND ? (
|
||||
<ChatMessage key={event.id} event={event} stream={stream} />
|
||||
) : (
|
||||
<ZapMessage key={event.id} zap={event} stream={stream} />
|
||||
|
@ -44,6 +44,7 @@ const tabs = [
|
||||
{ label: "About", path: "about" },
|
||||
{ label: "Notes", path: "notes" },
|
||||
{ label: "Media", path: "media" },
|
||||
{ label: "Streams", path: "streams" },
|
||||
{ label: "Zaps", path: "zaps" },
|
||||
{ label: "Following", path: "following" },
|
||||
{ label: "Likes", path: "likes" },
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
import { Flex, FormControl, FormLabel, Switch, useDisclosure } from "@chakra-ui/react";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { Kind } from "nostr-tools";
|
||||
import { isReply, isRepost, truncatedId } from "../../helpers/nostr-event";
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
import { RelayIconStack } from "../../components/relay-icon-stack";
|
||||
@ -8,8 +9,9 @@ import { NostrEvent } from "../../types/nostr-event";
|
||||
import TimelineActionAndStatus from "../../components/timeline-action-and-status";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import GenericNoteTimeline from "../../components/generic-note-timeline";
|
||||
import GenericNoteTimeline from "../../components/timeline/generic-note-timeline";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import { STREAM_KIND } from "../../helpers/nostr/stream";
|
||||
|
||||
const UserNotesTab = () => {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
@ -31,7 +33,7 @@ const UserNotesTab = () => {
|
||||
readRelays,
|
||||
{
|
||||
authors: [pubkey],
|
||||
kinds: [1, 6],
|
||||
kinds: [Kind.Text, Kind.Repost, STREAM_KIND],
|
||||
},
|
||||
{ eventFilter }
|
||||
);
|
||||
|
36
src/views/user/streams.tsx
Normal file
36
src/views/user/streams.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { useRef } from "react";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { truncatedId } from "../../helpers/nostr-event";
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
import TimelineActionAndStatus from "../../components/timeline-action-and-status";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import GenericNoteTimeline from "../../components/timeline/generic-note-timeline";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import { STREAM_KIND } from "../../helpers/nostr/stream";
|
||||
|
||||
export default function UserStreamsTab() {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
const readRelays = useAdditionalRelayContext();
|
||||
|
||||
const timeline = useTimelineLoader(truncatedId(pubkey) + "-streams", readRelays, [
|
||||
{
|
||||
authors: [pubkey],
|
||||
kinds: [STREAM_KIND],
|
||||
},
|
||||
{ "#p": [pubkey], kinds: [STREAM_KIND] },
|
||||
]);
|
||||
|
||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider<string> root={scrollBox} callback={callback}>
|
||||
<Flex direction="column" gap="2" pt="4" pb="8" h="full" overflowY="auto" overflowX="hidden" ref={scrollBox}>
|
||||
<GenericNoteTimeline timeline={timeline} />
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
</Flex>
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user