create thread loader

fix youtube embeds
add more video embeds
add queue to relay
This commit is contained in:
hzrd149 2023-02-07 17:04:18 -06:00
parent 23d3639e83
commit 34598b49f5
7 changed files with 195 additions and 41 deletions

View File

@ -0,0 +1,103 @@
import { BehaviorSubject } from "rxjs";
import { getReferences } from "../helpers/nostr-event";
import { NostrEvent } from "../types/nostr-event";
import { NostrRequest } from "./nostr-request";
import { NostrSubscription } from "./nostr-subscription";
export class ThreadLoader {
loading = new BehaviorSubject(false);
focusId = new BehaviorSubject("");
rootId = new BehaviorSubject("");
events = new BehaviorSubject<Record<string, NostrEvent>>({});
private relays: string[];
private subscription: NostrSubscription;
constructor(relays: string[], eventId: string) {
this.relays = relays;
this.subscription = new NostrSubscription(relays);
this.subscription.onEvent.subscribe((event) => {
this.events.next({ ...this.events.value, [event.id]: event });
});
this.updateEventId(eventId);
}
loadEvent() {
this.loading.next(true);
const request = new NostrRequest(this.relays);
request.onEvent.subscribe((event) => {
this.events.next({ ...this.events.value, [event.id]: event });
this.checkAndUpdateRoot();
request.cancel();
this.loading.next(false);
});
request.start({ ids: [this.focusId.value] });
}
private checkAndUpdateRoot() {
const event = this.events.value[this.focusId.value];
if (event) {
const refs = getReferences(event);
const rootId = refs.rootId || event.id;
// only update the root if its different
if (rootId !== this.rootId.value) {
this.rootId.next(rootId);
this.loadRoot();
this.updateSubscription();
}
}
}
loadRoot() {
if (this.rootId.value) {
const request = new NostrRequest(this.relays);
request.onEvent.subscribe((event) => {
this.events.next({ ...this.events.value, [event.id]: event });
request.cancel();
});
request.start({ ids: [this.rootId.value] });
}
}
private updateSubscription() {
if (this.rootId.value) {
this.subscription.update({ "#e": [this.rootId.value], kinds: [1] });
if (this.subscription.state !== NostrSubscription.OPEN) {
this.subscription.open();
}
}
}
updateEventId(eventId: string) {
if (this.loading.value) {
console.warn("trying to set eventId while loading");
return;
}
this.focusId.next(eventId);
const event = this.events.value[eventId];
if (!event) {
this.loadEvent();
} else {
this.checkAndUpdateRoot();
}
}
open() {
if (!this.loading.value && this.events.value[this.focusId.value]) {
this.loadEvent();
}
this.updateSubscription();
}
close() {
this.subscription.close();
}
}

View File

@ -26,7 +26,7 @@ const embeds: { regexp: RegExp; render: (match: RegExpMatchArray, trusted: boole
render: (match) => (
<AspectRatio ratio={16 / 10} maxWidth="30rem">
<iframe
src={`https://www.youtube.com/embed/${match[6]}`}
src={`https://www.youtube.com/embed/${match[5]}`}
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
@ -46,7 +46,7 @@ const embeds: { regexp: RegExp; render: (match: RegExpMatchArray, trusted: boole
},
// Video
{
regexp: /(https?:\/\/)([\da-z\.-]+\.[a-z\.]{2,6})([\/\w\.-]+\.(mp4|mkv|webm))/im,
regexp: /(https?:\/\/)([\da-z\.-]+\.[a-z\.]{2,6})([\/\w\.-]+\.(mp4|mkv|webm|mov))/im,
render: (match) => (
<AspectRatio ratio={16 / 9} maxWidth="30rem">
<video key={match[0]} src={match[0]} controls />

View File

@ -23,16 +23,19 @@ export function getReferences(event: NostrEvent) {
let replyId = eTags.find((t) => t[3] === "reply")?.[1];
let rootId = eTags.find((t) => t[3] === "root")?.[1];
if (rootId && !replyId) {
if (!rootId || !replyId) {
// a direct reply dose not need a "reply" reference
// https://github.com/nostr-protocol/nips/blob/master/10.md
replyId = rootId;
// this is not necessarily to spec. but if there is only one id (root or reply) then assign it to both
// this handles the cases where a client only set a "reply" tag and no root
rootId = replyId = rootId || replyId;
}
// legacy behavior
// https://github.com/nostr-protocol/nips/blob/master/10.md#positional-e-tags-deprecated
if (!rootId && !replyId && eTags.length >= 1) {
console.warn(`Using legacy threading behavior for ${event.id}`, event);
// console.info(`Using legacy threading behavior for ${event.id}`, event);
// first tag is the root
rootId = eTags[0][1];

View File

@ -9,7 +9,7 @@ export type LinkedEvent = {
children: LinkedEvent[];
};
export function linkEvents(events: NostrEvent[], rootId: string) {
export function linkEvents(events: NostrEvent[]) {
const idToChildren: Record<string, NostrEvent[]> = {};
const replies = new Map<string, LinkedEvent>();
@ -35,12 +35,9 @@ export function linkEvents(events: NostrEvent[], rootId: string) {
for (const [id, reply] of replies) {
reply.root = reply.refs.rootId ? replies.get(reply.refs.rootId) : undefined;
reply.reply = reply.refs.replyId
? replies.get(reply.refs.replyId)
: undefined;
reply.reply = reply.refs.replyId ? replies.get(reply.refs.replyId) : undefined;
reply.children =
idToChildren[id]?.map((e) => replies.get(e.id) as LinkedEvent) ?? [];
reply.children = idToChildren[id]?.map((e) => replies.get(e.id) as LinkedEvent) ?? [];
}
return replies;

View File

@ -0,0 +1,46 @@
import { useEffect, useMemo, useRef } from "react";
import { useUnmount } from "react-use";
import { ThreadLoader } from "../classes/thread-loader";
import { linkEvents } from "../helpers/thread";
import settings from "../services/settings";
import useSubject from "./use-subject";
type Options = {
enabled?: boolean;
};
export function useThreadLoader(eventId: string, opts?: Options) {
const relays = useSubject(settings.relays);
const ref = useRef<ThreadLoader | null>(null);
const loader = (ref.current = ref.current || new ThreadLoader(relays, eventId));
useEffect(() => {
if (eventId !== loader.focusId.value) loader.updateEventId(eventId);
}, [eventId]);
const enabled = opts?.enabled ?? true;
useEffect(() => {
if (enabled) loader.open();
else loader.close();
}, [enabled]);
useUnmount(() => {
loader.close();
});
const events = useSubject(loader.events);
const loading = useSubject(loader.loading);
const rootId = useSubject(loader.rootId);
const focusId = useSubject(loader.focusId);
const linked = useMemo(() => linkEvents(Object.values(events)), [events]);
return {
loader,
events,
linked,
rootId,
focusId,
loading,
};
}

View File

@ -33,6 +33,8 @@ export class Relay {
ws?: WebSocket;
permission: Permission = Permission.ALL;
private queue: NostrOutgoingMessage[] = [];
constructor(url: string, permission: Permission = Permission.ALL) {
this.url = url;
@ -52,6 +54,8 @@ export class Relay {
this.ws.onopen = () => {
this.onOpen.next(this);
this.sendQueued();
if (import.meta.env.DEV) {
console.info(`Relay: ${this.url} connected`);
}
@ -69,13 +73,25 @@ export class Relay {
if (this.permission & Permission.WRITE) {
if (this.connected) {
this.ws?.send(JSON.stringify(json));
}
} else this.queue.push(json);
}
}
close() {
this.ws?.close();
}
private sendQueued() {
if (this.connected) {
if (import.meta.env.DEV) {
console.info(`Relay: ${this.url} sending ${this.queue.length} queued messages`);
}
for (const message of this.queue) {
this.send(message);
}
this.queue = [];
}
}
get okay() {
return this.connected || this.connecting;
}

View File

@ -1,11 +1,9 @@
import { Alert, AlertDescription, AlertIcon, AlertTitle, Flex } from "@chakra-ui/react";
import useSubject from "../hooks/use-subject";
import settings from "../services/settings";
import { Alert, AlertDescription, AlertIcon, AlertTitle, Flex, Spinner } from "@chakra-ui/react";
import { Page } from "../components/page";
import { useParams } from "react-router-dom";
import { normalizeToHex } from "../helpers/nip-19";
import { Post } from "../components/post";
import { useMemo } from "react";
import { useThreadLoader } from "../hooks/use-thread-loader";
export const EventPage = () => {
const params = useParams();
@ -30,37 +28,28 @@ export const EventPage = () => {
);
};
// function useEvent(id: string, relays: string[]) {
// const sub = useMemo(() => eventsService.requestEvent(id, relays), [id]);
// const event = useSubject(sub);
// return event;
// }
export type EventViewProps = {
/** id of event in hex format */
eventId: string;
};
export const EventView = ({ eventId }: EventViewProps) => {
// const relays = useSubject(settings.relays);
const id = normalizeToHex(eventId) ?? "";
const { linked, events, rootId, focusId, loading } = useThreadLoader(id, { enabled: !!id });
// const event = useEvent(eventId, relays);
if (loading) return <Spinner />;
// const replySub = useSubscription(relays, { "#e": [eventId], kinds: [1] });
// const { events } = useEventDir(replySub);
const entry = linked.get(focusId);
if (entry) {
const isRoot = rootId === focusId;
// const timeline = Object.values(events).sort(
// (a, b) => b.created_at - a.created_at
// );
// return (
// <Flex direction="column" gap="2" flexGrow="1" overflow="auto">
// {event && <Post event={event} />}
// {timeline.map((event) => (
// <Post key={event.id} event={event} />
// ))}
// </Flex>
// );
return <h1>coming soon</h1>;
return (
<Flex direction="column" gap="4">
{!isRoot && (entry.root ? <Post event={entry.root.event} /> : <span>Missing Root</span>)}
<Post event={entry.event} />
</Flex>
);
} else if (events[focusId]) {
return <Post event={events[focusId]} />;
}
return <span>Missing Event</span>;
};