mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 21:31:43 +01:00
create thread loader
fix youtube embeds add more video embeds add queue to relay
This commit is contained in:
parent
23d3639e83
commit
34598b49f5
103
src/classes/thread-loader.ts
Normal file
103
src/classes/thread-loader.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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 />
|
||||
|
@ -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];
|
||||
|
@ -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;
|
||||
|
46
src/hooks/use-thread-loader.ts
Normal file
46
src/hooks/use-thread-loader.ts
Normal 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,
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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>;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user