mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-12 21:59:16 +02:00
fix timeline load more bug
This commit is contained in:
parent
f52cf23963
commit
982cc9ffa5
@ -13,7 +13,6 @@ import { GlobalTab } from "./views/home/global-tab";
|
||||
import { normalizeToHex } from "./helpers/nip-19";
|
||||
import UserView from "./views/user";
|
||||
import UserNotesTab from "./views/user/notes";
|
||||
import UserRepliesTab from "./views/user/replies";
|
||||
import UserFollowersTab from "./views/user/followers";
|
||||
import UserRelaysTab from "./views/user/relays";
|
||||
import UserFollowingTab from "./views/user/following";
|
||||
@ -86,7 +85,6 @@ const router = createBrowserRouter([
|
||||
children: [
|
||||
{ path: "", element: <UserNotesTab /> },
|
||||
{ path: "notes", element: <UserNotesTab /> },
|
||||
{ path: "replies", element: <UserRepliesTab /> },
|
||||
{ path: "followers", element: <UserFollowersTab /> },
|
||||
{ path: "following", element: <UserFollowingTab /> },
|
||||
{ path: "relays", element: <UserRelaysTab /> },
|
||||
|
@ -7,7 +7,7 @@ import createDefer from "./deferred";
|
||||
|
||||
let lastId = 0;
|
||||
|
||||
const REQUEST_DEFAULT_TIMEOUT = 1000 * 20;
|
||||
const REQUEST_DEFAULT_TIMEOUT = 1000 * 5;
|
||||
export class NostrRequest {
|
||||
static IDLE = "idle";
|
||||
static RUNNING = "running";
|
||||
@ -16,7 +16,6 @@ export class NostrRequest {
|
||||
id: string;
|
||||
timeout: number;
|
||||
relays: Set<Relay>;
|
||||
relayCleanup = new Map<Relay, Function>();
|
||||
state = NostrRequest.IDLE;
|
||||
onEvent = new Subject<NostrEvent>();
|
||||
onComplete = createDefer<void>();
|
||||
@ -27,76 +26,64 @@ export class NostrRequest {
|
||||
this.relays = new Set(relayUrls.map((url) => relayPoolService.requestRelay(url)));
|
||||
|
||||
for (const relay of this.relays) {
|
||||
const handleEOSE = (event: IncomingEOSE) => {
|
||||
if (event.subId === this.id) {
|
||||
this.handleEndOfEvents(relay);
|
||||
}
|
||||
};
|
||||
relay.onEOSE.subscribe(handleEOSE);
|
||||
|
||||
const handleEvent = (event: IncomingEvent) => {
|
||||
if (this.state === NostrRequest.RUNNING && event.subId === this.id && !this.seenEvents.has(event.body.id)) {
|
||||
this.onEvent.next(event.body);
|
||||
this.seenEvents.add(event.body.id);
|
||||
}
|
||||
};
|
||||
relay.onEvent.subscribe(handleEvent);
|
||||
|
||||
this.relayCleanup.set(relay, () => {
|
||||
relay.onEOSE.unsubscribe(handleEOSE);
|
||||
relay.onEvent.unsubscribe(handleEvent);
|
||||
});
|
||||
relay.onEOSE.subscribe(this.handleEOSE, this);
|
||||
relay.onEvent.subscribe(this.handleEvent, this);
|
||||
}
|
||||
|
||||
this.timeout = timeout ?? REQUEST_DEFAULT_TIMEOUT;
|
||||
}
|
||||
|
||||
handleEndOfEvents(relay: Relay) {
|
||||
this.relays.delete(relay);
|
||||
relay.send(["CLOSE", this.id]);
|
||||
handleEOSE(eose: IncomingEOSE) {
|
||||
if (eose.subId === this.id) {
|
||||
const relay = eose.relay;
|
||||
this.relays.delete(relay);
|
||||
relay.send(["CLOSE", this.id]);
|
||||
|
||||
const cleanup = this.relayCleanup.get(relay);
|
||||
if (cleanup) cleanup();
|
||||
relay.onEOSE.unsubscribe(this.handleEOSE, this);
|
||||
relay.onEvent.unsubscribe(this.handleEvent, this);
|
||||
|
||||
if (this.relays.size === 0) {
|
||||
this.state = NostrRequest.COMPLETE;
|
||||
this.onComplete.resolve();
|
||||
if (this.relays.size === 0) {
|
||||
this.state = NostrRequest.COMPLETE;
|
||||
this.onComplete.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
handleEvent(incomingEvent: IncomingEvent) {
|
||||
if (
|
||||
this.state === NostrRequest.RUNNING &&
|
||||
incomingEvent.subId === this.id &&
|
||||
!this.seenEvents.has(incomingEvent.body.id)
|
||||
) {
|
||||
this.onEvent.next(incomingEvent.body);
|
||||
this.seenEvents.add(incomingEvent.body.id);
|
||||
}
|
||||
}
|
||||
|
||||
start(query: NostrQuery) {
|
||||
if (this.state !== NostrRequest.IDLE) return this;
|
||||
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]);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
console.log(`NostrRequest: ${this.id} timed out`);
|
||||
this.cancel();
|
||||
}, this.timeout);
|
||||
|
||||
console.log(`NostrRequest: ${this.id} started`);
|
||||
setTimeout(() => this.complete(), this.timeout);
|
||||
|
||||
return this;
|
||||
}
|
||||
cancel() {
|
||||
if (this.state !== NostrRequest.COMPLETE) return this;
|
||||
complete() {
|
||||
if (this.state === NostrRequest.COMPLETE) return this;
|
||||
|
||||
this.state = NostrRequest.COMPLETE;
|
||||
for (const relay of this.relays) {
|
||||
relay.send(["CLOSE", this.id]);
|
||||
relay.onEOSE.unsubscribe(this.handleEOSE, this);
|
||||
relay.onEvent.unsubscribe(this.handleEvent, this);
|
||||
}
|
||||
for (const [relay, cleanup] of this.relayCleanup) {
|
||||
if (cleanup) cleanup();
|
||||
}
|
||||
this.relayCleanup = new Map();
|
||||
this.relays = new Set();
|
||||
this.onComplete.resolve();
|
||||
|
||||
console.log(`NostrRequest: ${this.id} complete`);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
@ -6,14 +6,17 @@ export type IncomingEvent = {
|
||||
type: "EVENT";
|
||||
subId: string;
|
||||
body: NostrEvent;
|
||||
relay: Relay;
|
||||
};
|
||||
export type IncomingNotice = {
|
||||
type: "NOTICE";
|
||||
message: string;
|
||||
relay: Relay;
|
||||
};
|
||||
export type IncomingEOSE = {
|
||||
type: "EOSE";
|
||||
subId: string;
|
||||
relay: Relay;
|
||||
};
|
||||
// NIP-20
|
||||
export type IncomingCommandResult = {
|
||||
@ -21,6 +24,7 @@ export type IncomingCommandResult = {
|
||||
eventId: string;
|
||||
status: boolean;
|
||||
message?: string;
|
||||
relay: Relay;
|
||||
};
|
||||
|
||||
export enum RelayMode {
|
||||
@ -125,16 +129,16 @@ export class Relay {
|
||||
|
||||
switch (type) {
|
||||
case "EVENT":
|
||||
this.onEvent.next({ type, subId: data[1], body: data[2] });
|
||||
this.onEvent.next({ relay: this, type, subId: data[1], body: data[2] });
|
||||
break;
|
||||
case "NOTICE":
|
||||
this.onNotice.next({ type, message: data[1] });
|
||||
this.onNotice.next({ relay: this, type, message: data[1] });
|
||||
break;
|
||||
case "EOSE":
|
||||
this.onEOSE.next({ type, subId: data[1] });
|
||||
this.onEOSE.next({ relay: this, type, subId: data[1] });
|
||||
break;
|
||||
case "OK":
|
||||
this.onCommandResult.next({ type, eventId: data[1], status: data[2], message: data[3] });
|
||||
this.onCommandResult.next({ relay: this, type, eventId: data[1], status: data[2], message: data[3] });
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -33,7 +33,7 @@ export class ThreadLoader {
|
||||
|
||||
this.checkAndUpdateRoot();
|
||||
|
||||
request.cancel();
|
||||
request.complete();
|
||||
this.loading.next(false);
|
||||
});
|
||||
request.start({ ids: [this.focusId.value] });
|
||||
@ -60,7 +60,7 @@ export class ThreadLoader {
|
||||
request.onEvent.subscribe((event) => {
|
||||
this.events.next({ ...this.events.value, [event.id]: event });
|
||||
|
||||
request.cancel();
|
||||
request.complete();
|
||||
});
|
||||
request.start({ ids: [this.rootId.value] });
|
||||
}
|
||||
|
@ -19,7 +19,8 @@ export class TimelineLoader {
|
||||
events = new PersistentSubject<NostrEvent[]>([]);
|
||||
loading = new PersistentSubject(false);
|
||||
page = new PersistentSubject(0);
|
||||
private seenEvents = new Set<string>();
|
||||
|
||||
private eventDir = new Map<string, NostrEvent>();
|
||||
private subscription: NostrMultiSubscription;
|
||||
private opts: Options = { pageSize: moment.duration(1, "hour").asSeconds() };
|
||||
|
||||
@ -48,9 +49,9 @@ export class TimelineLoader {
|
||||
}
|
||||
|
||||
private handleEvent(event: NostrEvent) {
|
||||
if (!this.seenEvents.has(event.id)) {
|
||||
this.events.next(this.events.value.concat(event).sort((a, b) => b.created_at - a.created_at));
|
||||
this.seenEvents.add(event.id);
|
||||
if (!this.eventDir.has(event.id)) {
|
||||
this.eventDir.set(event.id, event);
|
||||
this.events.next(Array.from(this.eventDir.values()).sort((a, b) => b.created_at - a.created_at));
|
||||
if (this.loading.value) this.loading.next(false);
|
||||
}
|
||||
}
|
||||
@ -69,7 +70,7 @@ export class TimelineLoader {
|
||||
loadMore() {
|
||||
if (this.loading.value) return;
|
||||
|
||||
const query = { ...this.query, ...this.getPageDates(this.page.value ?? 0) };
|
||||
const query = { ...this.query, ...this.getPageDates(this.page.value) };
|
||||
const request = new NostrRequest(this.relays);
|
||||
request.onEvent.subscribe(this.handleEvent, this);
|
||||
request.onComplete.then(() => {
|
||||
@ -78,12 +79,12 @@ export class TimelineLoader {
|
||||
request.start(query);
|
||||
|
||||
this.loading.next(true);
|
||||
this.page.next(this.page.value ?? 0 + 1);
|
||||
this.page.next(this.page.value + 1);
|
||||
}
|
||||
|
||||
forgetEvents() {
|
||||
this.events.next([]);
|
||||
this.seenEvents.clear();
|
||||
this.eventDir.clear();
|
||||
this.subscription.forgetEvents();
|
||||
}
|
||||
open() {
|
||||
|
@ -14,10 +14,11 @@ import { InlineInvoiceCard } from "../inline-invoice-card";
|
||||
import { TweetEmbed } from "../tweet-embed";
|
||||
import { UserLink } from "../user-link";
|
||||
import { normalizeToHex } from "../../helpers/nip-19";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||
import { NoteLink } from "../note-link";
|
||||
import settings from "../../services/settings";
|
||||
import styled from "@emotion/styled";
|
||||
import QuoteNote from "./quote-note";
|
||||
// import { ExternalLinkIcon } from "../icons";
|
||||
|
||||
const BlurredImage = (props: ImageProps) => {
|
||||
@ -31,7 +32,7 @@ const BlurredImage = (props: ImageProps) => {
|
||||
|
||||
type EmbedType = {
|
||||
regexp: RegExp;
|
||||
render: (match: RegExpMatchArray, event?: NostrEvent, trusted?: boolean) => JSX.Element | string;
|
||||
render: (match: RegExpMatchArray, event?: NostrEvent | DraftNostrEvent, trusted?: boolean) => JSX.Element | string;
|
||||
name?: string;
|
||||
isMedia: boolean;
|
||||
};
|
||||
@ -202,7 +203,7 @@ const embeds: EmbedType[] = [
|
||||
return key ? <UserLink color="blue.500" pubkey={key} showAt /> : match[0];
|
||||
case "note1":
|
||||
const noteId = normalizeToHex(match[1]);
|
||||
return noteId ? <NoteLink noteId={noteId} /> : match[0];
|
||||
return noteId ? <QuoteNote noteId={noteId} /> : match[0];
|
||||
default:
|
||||
return match[0];
|
||||
}
|
||||
@ -251,7 +252,11 @@ const MediaEmbed = ({ children, type }: { children: JSX.Element | string; type:
|
||||
);
|
||||
};
|
||||
|
||||
function embedContent(content: string, event?: NostrEvent, trusted: boolean = false): (string | JSX.Element)[] {
|
||||
function embedContent(
|
||||
content: string,
|
||||
event?: NostrEvent | DraftNostrEvent,
|
||||
trusted: boolean = false
|
||||
): (string | JSX.Element)[] {
|
||||
for (const embedType of embeds) {
|
||||
const match = content.match(embedType.regexp);
|
||||
|
||||
@ -278,7 +283,7 @@ const GradientOverlay = styled.div`
|
||||
`;
|
||||
|
||||
export type NoteContentsProps = {
|
||||
event: NostrEvent;
|
||||
event: NostrEvent | DraftNostrEvent;
|
||||
trusted?: boolean;
|
||||
maxHeight?: number;
|
||||
};
|
||||
|
10
src/components/note/quote-note.tsx
Normal file
10
src/components/note/quote-note.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { NoteLink } from "../note-link";
|
||||
|
||||
const QuoteNote = ({ noteId, relay }: { noteId: string; relay?: string }) => {
|
||||
const relays = useReadRelayUrls(relay ? [relay] : []);
|
||||
|
||||
return <NoteLink noteId={noteId} />;
|
||||
};
|
||||
|
||||
export default QuoteNote;
|
@ -22,6 +22,8 @@ const FollowingSideNav = () => {
|
||||
export const Page = ({ children }: { children: React.ReactNode }) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
console.log(isMobile);
|
||||
|
||||
return (
|
||||
<Container
|
||||
size="lg"
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
Button,
|
||||
Textarea,
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
@ -19,6 +20,7 @@ import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||
import { useSigningContext } from "../../providers/signing-provider";
|
||||
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||
import { NoteLink } from "../note-link";
|
||||
import { NoteContents } from "../note/note-contents";
|
||||
import { PostResults } from "./post-results";
|
||||
|
||||
function emptyDraft(): DraftNostrEvent {
|
||||
@ -37,14 +39,12 @@ type PostModalProps = {
|
||||
};
|
||||
|
||||
export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
const pad = isMobile ? "2" : "4";
|
||||
|
||||
const { requestSignature } = useSigningContext();
|
||||
const writeRelays = useWriteRelayUrls();
|
||||
const [waiting, setWaiting] = useState(false);
|
||||
const [signedEvent, setSignedEvent] = useState<NostrEvent | null>(null);
|
||||
const [results, resultsActions] = useList<PostResult>();
|
||||
const { isOpen: showPreview, onToggle: togglePreview } = useDisclosure();
|
||||
const [draft, setDraft] = useState<DraftNostrEvent>(() => Object.assign(emptyDraft(), initialDraft));
|
||||
|
||||
const handleContentChange: React.ChangeEventHandler<HTMLTextAreaElement> = (event) => {
|
||||
@ -71,32 +71,27 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
|
||||
|
||||
const renderContent = () => {
|
||||
if (signedEvent) {
|
||||
return (
|
||||
<ModalBody padding="4">
|
||||
<PostResults event={signedEvent} results={results} onClose={onClose} />
|
||||
</ModalBody>
|
||||
);
|
||||
return <PostResults event={signedEvent} results={results} onClose={onClose} />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<ModalBody pr={pad} pl={pad}>
|
||||
{refs.replyId && (
|
||||
<Text mb="2">
|
||||
Replying to: <NoteLink noteId={refs.replyId} />
|
||||
</Text>
|
||||
)}
|
||||
{refs.replyId && (
|
||||
<Text mb="2">
|
||||
Replying to: <NoteLink noteId={refs.replyId} />
|
||||
</Text>
|
||||
)}
|
||||
{showPreview ? (
|
||||
<NoteContents event={draft} trusted />
|
||||
) : (
|
||||
<Textarea autoFocus mb="2" value={draft.content} onChange={handleContentChange} rows={5} />
|
||||
</ModalBody>
|
||||
<ModalFooter pr={pad} pl={pad} pb={pad} pt="0">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<Button onClick={onClose} isDisabled={waiting} ml="auto">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="blue" type="submit" isLoading={waiting} onClick={handleSubmit} isDisabled={!canSubmit}>
|
||||
Post
|
||||
</Button>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
)}
|
||||
<Flex gap="2" alignItems="center" justifyContent="flex-end">
|
||||
{draft.content.length > 0 && <Button onClick={togglePreview}>Preview</Button>}
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button colorScheme="blue" type="submit" isLoading={waiting} onClick={handleSubmit} isDisabled={!canSubmit}>
|
||||
Post
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -104,7 +99,9 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="4xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>{renderContent()}</ModalContent>
|
||||
<ModalContent>
|
||||
<ModalBody padding="4">{renderContent()}</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,14 @@
|
||||
import { useMediaQuery } from "@chakra-ui/react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile] = useMediaQuery("(max-width: 1000px)");
|
||||
const match = useMemo(() => window.matchMedia("(max-width: 1000px)"), []);
|
||||
const [matches, setMatches] = useState(match.matches);
|
||||
|
||||
return isMobile;
|
||||
useEffect(() => {
|
||||
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
|
||||
match.addEventListener("change", listener);
|
||||
return () => match.removeEventListener("change", listener);
|
||||
}, [match]);
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useDeepCompareEffect, useUnmount } from "react-use";
|
||||
import { useUnmount } from "react-use";
|
||||
import { NostrQueryWithStart, TimelineLoader, TimelineLoaderOptions } from "../classes/timeline-loader";
|
||||
import useSubject from "./use-subject";
|
||||
|
||||
@ -18,9 +18,9 @@ export function useTimelineLoader(key: string, relays: string[], query: NostrQue
|
||||
loader.setQuery(query);
|
||||
}, [key]);
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
useEffect(() => {
|
||||
loader.setRelays(relays);
|
||||
}, [relays]);
|
||||
}, [relays.join("|")]);
|
||||
|
||||
const enabled = opts?.enabled ?? true;
|
||||
useEffect(() => {
|
||||
|
@ -73,7 +73,7 @@ export const DiscoverTab = () => {
|
||||
}, [discover]);
|
||||
|
||||
const { events, loading, loadMore } = useTimelineLoader(
|
||||
`discover`,
|
||||
`${account.pubkey}-discover`,
|
||||
relays,
|
||||
{ authors: throttledPubkeys, kinds: [1], since: moment().subtract(1, "hour").unix() },
|
||||
{ pageSize: moment.duration(1, "hour").asSeconds(), enabled: throttledPubkeys.length > 0 }
|
||||
|
@ -24,7 +24,7 @@ export const FollowingTab = () => {
|
||||
|
||||
const following = contacts?.contacts || [];
|
||||
const { events, loading, loadMore } = useTimelineLoader(
|
||||
`following-posts`,
|
||||
`${account.pubkey}-following-posts`,
|
||||
relays,
|
||||
{ authors: following, kinds: [1], since: moment().subtract(2, "hour").unix() },
|
||||
{ pageSize: moment.duration(2, "hour").asSeconds(), enabled: following.length > 0 }
|
||||
|
@ -30,7 +30,6 @@ import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
|
||||
const tabs = [
|
||||
{ label: "Notes", path: "notes" },
|
||||
{ label: "Replies", path: "replies" },
|
||||
{ label: "Followers", path: "followers" },
|
||||
{ label: "Following", path: "following" },
|
||||
{ label: "Relays", path: "relays" },
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Button, Flex, Spinner } from "@chakra-ui/react";
|
||||
import { Button, Flex, FormControl, FormLabel, Spinner, Switch, useDisclosure } from "@chakra-ui/react";
|
||||
import moment from "moment";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { Note } from "../../components/note";
|
||||
@ -9,17 +9,24 @@ import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
const UserNotesTab = () => {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
const relays = useReadRelayUrls();
|
||||
const { isOpen: showReplies, onToggle: toggleReplies } = useDisclosure();
|
||||
|
||||
const { events, loading, loadMore } = useTimelineLoader(
|
||||
`${pubkey} notes`,
|
||||
`${pubkey}-notes`,
|
||||
relays,
|
||||
{ authors: [pubkey], kinds: [1], since: moment().subtract(1, "day").unix() },
|
||||
{ pageSize: moment.duration(1, "day").asSeconds() }
|
||||
);
|
||||
const timeline = events.filter(isNote);
|
||||
const timeline = showReplies ? events : events.filter(isNote);
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2" pr="2" pl="2">
|
||||
<FormControl display="flex" alignItems="center">
|
||||
<FormLabel htmlFor="show-replies" mb="0">
|
||||
Show Replies
|
||||
</FormLabel>
|
||||
<Switch id="show-replies" isChecked={showReplies} onChange={toggleReplies} />
|
||||
</FormControl>
|
||||
{timeline.map((event) => (
|
||||
<Note key={event.id} event={event} maxHeight={300} />
|
||||
))}
|
||||
|
@ -1,31 +0,0 @@
|
||||
import { Button, Flex, Spinner } from "@chakra-ui/react";
|
||||
import moment from "moment";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { Note } from "../../components/note";
|
||||
import { isReply } from "../../helpers/nostr-event";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
|
||||
const UserRepliesTab = () => {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
const relays = useReadRelayUrls();
|
||||
|
||||
const { events, loading, loadMore } = useTimelineLoader(
|
||||
`${pubkey} replies`,
|
||||
relays,
|
||||
{ authors: [pubkey], kinds: [1], since: moment().subtract(4, "hours").unix() },
|
||||
{ pageSize: moment.duration(1, "day").asSeconds() }
|
||||
);
|
||||
const timeline = events.filter(isReply);
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2" pr="2" pl="2">
|
||||
{timeline.map((event) => (
|
||||
<Note key={event.id} event={event} maxHeight={300} />
|
||||
))}
|
||||
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserRepliesTab;
|
Loading…
x
Reference in New Issue
Block a user