mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-29 11:12:12 +01:00
Show unavailable events in threads
This commit is contained in:
parent
151f3c71af
commit
fd6ce3ec31
5
.changeset/fast-bats-jump.md
Normal file
5
.changeset/fast-bats-jump.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Show unavailable events in threads
|
@ -5,7 +5,7 @@ import { NostrEvent } from "../types/nostr-event";
|
||||
import { NostrOutgoingRequest, NostrRequestFilter, RelayQueryMap } from "../types/nostr-query";
|
||||
import Relay, { IncomingEvent } from "./relay";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import { isFilterEqual } from "../helpers/nostr/filter";
|
||||
import { isFilterEqual, isQueryMapEqual } from "../helpers/nostr/filter";
|
||||
|
||||
export default class NostrMultiSubscription {
|
||||
static INIT = "initial";
|
||||
@ -58,7 +58,7 @@ export default class NostrMultiSubscription {
|
||||
}
|
||||
|
||||
setQueryMap(queryMap: RelayQueryMap) {
|
||||
if (isFilterEqual(this.queryMap, queryMap)) return;
|
||||
if (isQueryMapEqual(this.queryMap, queryMap)) return;
|
||||
|
||||
// add and remove relays
|
||||
for (const url of Object.keys(queryMap)) {
|
||||
|
@ -12,7 +12,13 @@ import EventStore from "./event-store";
|
||||
import { isReplaceable } from "../helpers/nostr/events";
|
||||
import replaceableEventLoaderService from "../services/replaceable-event-requester";
|
||||
import deleteEventService from "../services/delete-events";
|
||||
import { addQueryToFilter, isFilterEqual, mapQueryMap, stringifyFilter } from "../helpers/nostr/filter";
|
||||
import {
|
||||
addQueryToFilter,
|
||||
isFilterEqual,
|
||||
isQueryMapEqual,
|
||||
mapQueryMap,
|
||||
stringifyFilter,
|
||||
} from "../helpers/nostr/filter";
|
||||
import { localCacheRelay } from "../services/local-cache-relay";
|
||||
import { SimpleSubscription } from "nostr-idb";
|
||||
import { relayRequest } from "../helpers/relay";
|
||||
@ -178,7 +184,7 @@ export default class TimelineLoader {
|
||||
}
|
||||
|
||||
setQueryMap(queryMap: RelayQueryMap) {
|
||||
if (isFilterEqual(this.queryMap, queryMap)) return;
|
||||
if (isQueryMapEqual(this.queryMap, queryMap)) return;
|
||||
|
||||
this.log("set query map", queryMap);
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Suspense, lazy } from "react";
|
||||
import type { DecodeResult } from "nostr-tools/lib/types/nip19";
|
||||
import { CardProps, Spinner } from "@chakra-ui/react";
|
||||
import { Button, CardProps, Spinner } from "@chakra-ui/react";
|
||||
import { kinds, nip19 } from "nostr-tools";
|
||||
|
||||
import EmbeddedNote from "./event-types/embedded-note";
|
||||
@ -40,6 +40,7 @@ import EmbeddedTorrentComment from "./event-types/embedded-torrent-comment";
|
||||
import EmbeddedChannel from "./event-types/embedded-channel";
|
||||
import { FLARE_VIDEO_KIND } from "../../helpers/nostr/flare";
|
||||
import EmbeddedFlareVideo from "./event-types/embedded-flare-video";
|
||||
import LoadingNostrLink from "../loading-nostr-link";
|
||||
const EmbeddedStemstrTrack = lazy(() => import("./event-types/embedded-stemstr-track"));
|
||||
|
||||
export type EmbedProps = {
|
||||
@ -101,17 +102,17 @@ export function EmbedEventPointer({ pointer, ...props }: { pointer: DecodeResult
|
||||
switch (pointer.type) {
|
||||
case "note": {
|
||||
const event = useSingleEvent(pointer.data);
|
||||
if (event === undefined) return <NoteLink noteId={pointer.data} />;
|
||||
if (!event) return <LoadingNostrLink link={pointer} />;
|
||||
return <EmbedEvent event={event} {...props} />;
|
||||
}
|
||||
case "nevent": {
|
||||
const event = useSingleEvent(pointer.data.id, pointer.data.relays);
|
||||
if (event === undefined) return <NoteLink noteId={pointer.data.id} />;
|
||||
if (!event) return <LoadingNostrLink link={pointer} />;
|
||||
return <EmbedEvent event={event} {...props} />;
|
||||
}
|
||||
case "naddr": {
|
||||
const event = useReplaceableEvent(pointer.data);
|
||||
if (!event) return <span>{nip19.naddrEncode(pointer.data)}</span>;
|
||||
const event = useReplaceableEvent(pointer.data, pointer.data.relays);
|
||||
if (!event) return <LoadingNostrLink link={pointer} />;
|
||||
return <EmbedEvent event={event} {...props} />;
|
||||
}
|
||||
case "nrelay":
|
||||
|
86
src/components/loading-nostr-link.tsx
Normal file
86
src/components/loading-nostr-link.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { Box, Button, ButtonGroup, Link, Text, useDisclosure } from "@chakra-ui/react";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import { ExternalLinkIcon, SearchIcon } from "./icons";
|
||||
import { buildAppSelectUrl } from "../helpers/nostr/apps";
|
||||
import UserLink from "./user-link";
|
||||
import { encodeDecodeResult } from "../helpers/nip19";
|
||||
|
||||
export default function LoadingNostrLink({ link }: { link: nip19.DecodeResult }) {
|
||||
const encoded = encodeDecodeResult(link);
|
||||
const details = useDisclosure();
|
||||
|
||||
const renderDetails = () => {
|
||||
switch (link.type) {
|
||||
case "note":
|
||||
return <Text>ID: {link.data}</Text>;
|
||||
case "nevent":
|
||||
return (
|
||||
<>
|
||||
<Text>ID: {link.data.id}</Text>
|
||||
{link.data.kind && <Text>Kind: {link.data.kind}</Text>}
|
||||
{link.data.author && (
|
||||
<Text>
|
||||
Pubkey: <UserLink pubkey={link.data.author} />
|
||||
</Text>
|
||||
)}
|
||||
{link.data.relays && <Text>Relays: {link.data.relays.join(", ")}</Text>}
|
||||
</>
|
||||
);
|
||||
case "npub":
|
||||
return <Text>Pubkey: {link.data}</Text>;
|
||||
case "nprofile":
|
||||
return (
|
||||
<>
|
||||
<Text>Pubkey: {link.data.pubkey}</Text>
|
||||
{link.data.relays && <Text>Relays: {link.data.relays.join(", ")}</Text>}
|
||||
</>
|
||||
);
|
||||
case "naddr":
|
||||
return (
|
||||
<>
|
||||
<Text>Kind: {link.data.kind}</Text>
|
||||
<Text>
|
||||
Pubkey: <UserLink pubkey={link.data.pubkey} />
|
||||
</Text>
|
||||
<Text>Identifier: {link.data.identifier}</Text>
|
||||
{link.data.relays && link.data.relays.length > 0 && <Text>Relays: {link.data.relays.join(", ")}</Text>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="link"
|
||||
color="GrayText"
|
||||
maxW="lg"
|
||||
textAlign="left"
|
||||
fontFamily="monospace"
|
||||
whiteSpace="pre"
|
||||
onClick={details.onToggle}
|
||||
>
|
||||
[{details.isOpen ? "-" : "+"}]
|
||||
<Text as="span" isTruncated>
|
||||
{encoded}
|
||||
</Text>
|
||||
</Button>
|
||||
{details.isOpen && (
|
||||
<Box px="2" fontFamily="monospace" color="GrayText" fontWeight="bold" fontSize="sm">
|
||||
<Text>Type: {link.type}</Text>
|
||||
{renderDetails()}
|
||||
<ButtonGroup variant="link" size="sm" my="1">
|
||||
<Button leftIcon={<SearchIcon />} colorScheme="primary" isDisabled>
|
||||
Find
|
||||
</Button>
|
||||
<Button as={Link} leftIcon={<ExternalLinkIcon />} href={buildAppSelectUrl(encoded)} isExternal>
|
||||
Open
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,39 +1,37 @@
|
||||
import { useRef } from "react";
|
||||
import { Flex, Heading, Link, SkeletonText, Text } from "@chakra-ui/react";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { Flex, Heading, Link, Text } from "@chakra-ui/react";
|
||||
import { kinds, nip18 } from "nostr-tools";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { isETag, NostrEvent } from "../../../types/nostr-event";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
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/local/trust";
|
||||
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
|
||||
import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer";
|
||||
import useSingleEvent from "../../../hooks/use-single-event";
|
||||
import { EmbedEvent } from "../../embed-event";
|
||||
import useUserMuteFilter from "../../../hooks/use-user-mute-filter";
|
||||
import { parseHardcodedNoteContent } from "../../../helpers/nostr/events";
|
||||
import { getEventCommunityPointer } from "../../../helpers/nostr/communities";
|
||||
import LoadingNostrLink from "../../loading-nostr-link";
|
||||
|
||||
export default function RepostNote({ event }: { event: NostrEvent }) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, event.id);
|
||||
|
||||
const muteFilter = useUserMuteFilter();
|
||||
const hardCodedNote = parseHardcodedNoteContent(event);
|
||||
|
||||
const [_, eventId, relay] = event.tags.find(isETag) ?? [];
|
||||
const readRelays = useReadRelayUrls(relay ? [relay] : []);
|
||||
|
||||
const loadedNote = useSingleEvent(eventId, readRelays);
|
||||
const pointer = nip18.getRepostedEventPointer(event);
|
||||
const loadedNote = useSingleEvent(pointer?.id, pointer?.relays);
|
||||
const note = hardCodedNote || loadedNote;
|
||||
|
||||
const communityCoordinate = getEventCommunityPointer(event);
|
||||
|
||||
if (note && muteFilter(note)) return;
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, event.id);
|
||||
|
||||
if ((note && muteFilter(note)) || !pointer) return null;
|
||||
|
||||
return (
|
||||
<TrustProvider event={event}>
|
||||
@ -59,7 +57,7 @@ export default function RepostNote({ event }: { event: NostrEvent }) {
|
||||
<NoteMenu event={event} size="sm" variant="link" aria-label="note options" ml="auto" />
|
||||
</Flex>
|
||||
{!note ? (
|
||||
<SkeletonText />
|
||||
<LoadingNostrLink link={{ type: "nevent", data: pointer }} />
|
||||
) : note.kind === kinds.ShortTextNote ? (
|
||||
// NOTE: tell the note not to register itself with the intersection observer. since this is an older note it will break the order of the timeline
|
||||
<Note event={note} showReplyButton registerIntersectionEntity={false} />
|
||||
|
@ -51,22 +51,22 @@ export function getSharableEventAddress(event: NostrEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
export function encodePointer(pointer: nip19.DecodeResult) {
|
||||
switch (pointer.type) {
|
||||
export function encodeDecodeResult(result: nip19.DecodeResult) {
|
||||
switch (result.type) {
|
||||
case "naddr":
|
||||
return nip19.naddrEncode(pointer.data);
|
||||
return nip19.naddrEncode(result.data);
|
||||
case "nprofile":
|
||||
return nip19.nprofileEncode(pointer.data);
|
||||
return nip19.nprofileEncode(result.data);
|
||||
case "nevent":
|
||||
return nip19.neventEncode(pointer.data);
|
||||
return nip19.neventEncode(result.data);
|
||||
case "nrelay":
|
||||
return nip19.nrelayEncode(pointer.data);
|
||||
return nip19.nrelayEncode(result.data);
|
||||
case "nsec":
|
||||
return nip19.nsecEncode(pointer.data);
|
||||
return nip19.nsecEncode(result.data);
|
||||
case "npub":
|
||||
return nip19.npubEncode(pointer.data);
|
||||
return nip19.npubEncode(result.data);
|
||||
case "note":
|
||||
return nip19.noteEncode(pointer.data);
|
||||
return nip19.noteEncode(result.data);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,10 @@ export function isFilterEqual(a: NostrRequestFilter, b: NostrRequestFilter) {
|
||||
return stringifyFilter(a) === stringifyFilter(b);
|
||||
}
|
||||
|
||||
export function isQueryMapEqual(a: RelayQueryMap, b: RelayQueryMap) {
|
||||
return stringify(a) === stringify(b);
|
||||
}
|
||||
|
||||
export function mapQueryMap(queryMap: RelayQueryMap, fn: (filter: NostrRequestFilter) => NostrRequestFilter) {
|
||||
const newMap: RelayQueryMap = {};
|
||||
for (const [relay, filter] of Object.entries(queryMap)) newMap[relay] = fn(filter);
|
||||
|
@ -15,17 +15,17 @@ export default function useThreadTimelineLoader(
|
||||
kind: number = kinds.ShortTextNote,
|
||||
) {
|
||||
const refs = focusedEvent && getReferences(focusedEvent);
|
||||
const rootId = refs?.root?.e?.id || focusedEvent?.id;
|
||||
const rootPointer = refs?.root?.e || (focusedEvent && { id: focusedEvent?.id });
|
||||
|
||||
const readRelays = unique([...relays, ...(refs?.root?.e?.relays ?? [])]);
|
||||
const readRelays = unique([...relays, ...(rootPointer?.relays ?? [])]);
|
||||
|
||||
const timelineId = `${rootId}-replies`;
|
||||
const timelineId = `${rootPointer?.id}-replies`;
|
||||
const timeline = useTimelineLoader(
|
||||
timelineId,
|
||||
readRelays,
|
||||
rootId
|
||||
rootPointer
|
||||
? {
|
||||
"#e": [rootId],
|
||||
"#e": [rootPointer.id],
|
||||
kinds: [kind],
|
||||
}
|
||||
: undefined,
|
||||
@ -38,7 +38,7 @@ export default function useThreadTimelineLoader(
|
||||
for (const e of events) singleEventService.handleEvent(e);
|
||||
}, [events]);
|
||||
|
||||
const rootEvent = useSingleEvent(refs?.root?.e?.id, refs?.root?.e?.relays);
|
||||
const rootEvent = useSingleEvent(rootPointer?.id, rootPointer?.relays);
|
||||
const allEvents = useMemo(() => {
|
||||
const arr = Array.from(events);
|
||||
if (focusedEvent) arr.push(focusedEvent);
|
||||
@ -46,5 +46,5 @@ export default function useThreadTimelineLoader(
|
||||
return arr;
|
||||
}, [events, rootEvent, focusedEvent]);
|
||||
|
||||
return { events: allEvents, rootEvent, rootId, timeline };
|
||||
return { events: allEvents, rootEvent, rootPointer, timeline };
|
||||
}
|
||||
|
@ -11,12 +11,6 @@ import { NostrEvent } from "../types/nostr-event";
|
||||
import { LOCAL_CACHE_RELAY, LOCAL_CACHE_RELAY_ENABLED } from "./local-cache-relay";
|
||||
|
||||
function hashFilter(filter: NostrRequestFilter) {
|
||||
// const encoder = new TextEncoder();
|
||||
// const data = encoder.encode(stringify(filter));
|
||||
// const hash = await window.crypto.subtle.digest("SHA-256", data);
|
||||
// const hashArray = Array.from(new Uint8Array(hash));
|
||||
// const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
// return hashHex;
|
||||
return stringify(filter);
|
||||
}
|
||||
|
||||
|
@ -1,48 +1,16 @@
|
||||
import { Filter } from "nostr-tools";
|
||||
import { NostrEvent } from "./nostr-event";
|
||||
|
||||
export type NostrOutgoingEvent = ["EVENT", NostrEvent];
|
||||
export type NostrOutgoingRequest = ["REQ", string, ...NostrQuery[]];
|
||||
export type NostrOutgoingCount = ["COUNT", string, ...NostrQuery[]];
|
||||
export type NostrOutgoingRequest = ["REQ", string, ...Filter[]];
|
||||
export type NostrOutgoingCount = ["COUNT", string, ...Filter[]];
|
||||
export type NostrOutgoingClose = ["CLOSE", string];
|
||||
|
||||
export type NostrOutgoingMessage = NostrOutgoingEvent | NostrOutgoingRequest | NostrOutgoingClose | NostrOutgoingCount;
|
||||
|
||||
export type NostrQuery = {
|
||||
ids?: string[];
|
||||
authors?: string[];
|
||||
kinds?: number[];
|
||||
"#a"?: string[];
|
||||
"#b"?: string[];
|
||||
"#c"?: string[];
|
||||
"#d"?: string[];
|
||||
"#e"?: string[];
|
||||
"#f"?: string[];
|
||||
"#g"?: string[];
|
||||
"#h"?: string[];
|
||||
"#i"?: string[];
|
||||
"#j"?: string[];
|
||||
"#k"?: string[];
|
||||
"#l"?: string[];
|
||||
"#m"?: string[];
|
||||
"#n"?: string[];
|
||||
"#o"?: string[];
|
||||
"#p"?: string[];
|
||||
"#q"?: string[];
|
||||
"#r"?: string[];
|
||||
"#s"?: string[];
|
||||
"#t"?: string[];
|
||||
"#u"?: string[];
|
||||
"#v"?: string[];
|
||||
"#w"?: string[];
|
||||
"#x"?: string[];
|
||||
"#y"?: string[];
|
||||
"#z"?: string[];
|
||||
since?: number;
|
||||
until?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
};
|
||||
/** @deprecated use Filter instead */
|
||||
export type NostrQuery = Filter;
|
||||
|
||||
export type NostrRequestFilter = NostrQuery | NostrQuery[];
|
||||
export type NostrRequestFilter = Filter | Filter[];
|
||||
|
||||
export type RelayQueryMap = Record<string, NostrRequestFilter>;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { EmbedEventPointer } from "../../../components/embed-event";
|
||||
import { getGoalEventPointers, getGoalLinks } from "../../../helpers/nostr/goal";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { encodePointer } from "../../../helpers/nip19";
|
||||
import { encodeDecodeResult } from "../../../helpers/nip19";
|
||||
import OpenGraphCard from "../../../components/open-graph-card";
|
||||
|
||||
export default function GoalContents({ goal }: { goal: NostrEvent }) {
|
||||
@ -11,7 +11,7 @@ export default function GoalContents({ goal }: { goal: NostrEvent }) {
|
||||
return (
|
||||
<>
|
||||
{pointers.map((pointer) => (
|
||||
<EmbedEventPointer key={encodePointer(pointer)} pointer={pointer} />
|
||||
<EmbedEventPointer key={encodeDecodeResult(pointer)} pointer={pointer} />
|
||||
))}
|
||||
{links.map((link) => (
|
||||
<OpenGraphCard url={new URL(link)} />
|
||||
|
@ -26,7 +26,7 @@ import ListFeedButton from "../components/list-feed-button";
|
||||
import VerticalPageLayout from "../../../components/vertical-page-layout";
|
||||
import { COMMUNITY_DEFINITION_KIND } from "../../../helpers/nostr/communities";
|
||||
import { EmbedEvent, EmbedEventPointer } from "../../../components/embed-event";
|
||||
import { encodePointer } from "../../../helpers/nip19";
|
||||
import { encodeDecodeResult } from "../../../helpers/nip19";
|
||||
import useSingleEvent from "../../../hooks/use-single-event";
|
||||
import UserAvatarLink from "../../../components/user-avatar-link";
|
||||
import useParamsAddressPointer from "../../../hooks/use-params-address-pointer";
|
||||
@ -136,7 +136,7 @@ function ListPage({ list }: { list: NostrEvent }) {
|
||||
<Flex gap="2" direction="column">
|
||||
{articles.map((pointer) => {
|
||||
const decode: DecodeResult = { type: "naddr", data: pointer };
|
||||
return <EmbedEventPointer key={encodePointer(decode)} pointer={decode} />;
|
||||
return <EmbedEventPointer key={encodeDecodeResult(decode)} pointer={decode} />;
|
||||
})}
|
||||
</Flex>
|
||||
</>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { useMemo } from "react";
|
||||
import { Button, Heading, Spinner } from "@chakra-ui/react";
|
||||
import { ReactNode, useMemo } from "react";
|
||||
import { Card, Flex, Heading, Link, Spinner } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import Note from "../../components/note";
|
||||
import { getSharableEventAddress } from "../../helpers/nip19";
|
||||
import { ThreadPost } from "./components/thread-post";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
@ -13,12 +13,57 @@ import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-
|
||||
import useThreadTimelineLoader from "../../hooks/use-thread-timeline-loader";
|
||||
import useSingleEvent from "../../hooks/use-single-event";
|
||||
import useParamsEventPointer from "../../hooks/use-params-event-pointer";
|
||||
import LoadingNostrLink from "../../components/loading-nostr-link";
|
||||
import UserName from "../../components/user-name";
|
||||
import { getSharableEventAddress } from "../../helpers/nip19";
|
||||
import UserAvatarLink from "../../components/user-avatar-link";
|
||||
import { ReplyIcon } from "../../components/icons";
|
||||
|
||||
function ThreadPage({ thread, rootId, focusId }: { thread: Map<string, ThreadItem>; rootId: string; focusId: string }) {
|
||||
const isRoot = rootId === focusId;
|
||||
function CollapsedReplies({
|
||||
pointer,
|
||||
thread,
|
||||
root,
|
||||
}: {
|
||||
pointer: nip19.EventPointer;
|
||||
thread: Map<string, ThreadItem>;
|
||||
root: nip19.EventPointer;
|
||||
}) {
|
||||
const post = thread.get(pointer.id);
|
||||
if (!post) return <LoadingNostrLink link={{ type: "nevent", data: pointer }} />;
|
||||
|
||||
let reply: ReactNode = null;
|
||||
if (post.refs.reply?.e && post.refs.reply.e.id !== root.id) {
|
||||
reply = <CollapsedReplies pointer={post.refs.reply.e} thread={thread} root={root} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{reply}
|
||||
<Card gap="2" overflow="hidden" px="2" display="flex" flexDirection="row" p="2">
|
||||
<UserAvatarLink pubkey={post.event.pubkey} size="xs" />
|
||||
<UserName pubkey={post.event.pubkey} fontWeight="bold" />
|
||||
{root.id !== pointer.id && <ReplyIcon />}
|
||||
<Link as={RouterLink} to={`/n/${getSharableEventAddress(post.event)}`} isTruncated>
|
||||
{post.event.content}
|
||||
</Link>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ThreadPage({
|
||||
thread,
|
||||
rootPointer,
|
||||
focusId,
|
||||
}: {
|
||||
thread: Map<string, ThreadItem>;
|
||||
rootPointer: nip19.EventPointer;
|
||||
focusId: string;
|
||||
}) {
|
||||
const isRoot = rootPointer.id === focusId;
|
||||
|
||||
const focusedPost = thread.get(focusId);
|
||||
const rootPost = thread.get(rootId);
|
||||
const rootPost = thread.get(rootPointer.id);
|
||||
if (isRoot && rootPost) {
|
||||
return <ThreadPost post={rootPost} initShowReplies focusId={focusId} />;
|
||||
}
|
||||
@ -34,29 +79,22 @@ function ThreadPage({ thread, rootId, focusId }: { thread: Map<string, ThreadIte
|
||||
}
|
||||
}
|
||||
|
||||
const grandparentPointer = focusedPost.replyingTo?.refs.reply?.e;
|
||||
|
||||
return (
|
||||
<>
|
||||
{parentPosts.length > 1 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
h="4rem"
|
||||
w="full"
|
||||
as={RouterLink}
|
||||
to={`/n/${getSharableEventAddress(parentPosts[0].event)}`}
|
||||
>
|
||||
View full thread ({parentPosts.length - 1})
|
||||
</Button>
|
||||
{rootPointer && focusedPost.refs.reply?.e?.id !== rootPointer.id && (
|
||||
<CollapsedReplies pointer={rootPointer} thread={thread} root={rootPointer} />
|
||||
)}
|
||||
{focusedPost.replyingTo && (
|
||||
<Note
|
||||
key={focusedPost.replyingTo.event.id + "-rely"}
|
||||
event={focusedPost.replyingTo.event}
|
||||
hideDrawerButton
|
||||
showReplyLine={false}
|
||||
/>
|
||||
{grandparentPointer && grandparentPointer.id !== rootPointer.id && (
|
||||
<CollapsedReplies pointer={grandparentPointer} thread={thread} root={rootPointer} />
|
||||
)}
|
||||
<ThreadPost key={focusedPost.event.id} post={focusedPost} initShowReplies focusId={focusId} />
|
||||
{focusedPost.replyingTo ? (
|
||||
<Note event={focusedPost.replyingTo.event} hideDrawerButton showReplyLine={false} />
|
||||
) : (
|
||||
focusedPost.refs.reply?.e && <LoadingNostrLink link={{ type: "nevent", data: focusedPost.refs.reply.e }} />
|
||||
)}
|
||||
<ThreadPost post={focusedPost} initShowReplies focusId={focusId} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -66,7 +104,7 @@ export default function ThreadView() {
|
||||
const readRelays = useReadRelayUrls(pointer.relays);
|
||||
|
||||
const focusedEvent = useSingleEvent(pointer.id, pointer.relays);
|
||||
const { rootId, events, timeline } = useThreadTimelineLoader(focusedEvent, readRelays);
|
||||
const { rootPointer, events, timeline } = useThreadTimelineLoader(focusedEvent, readRelays);
|
||||
const thread = useMemo(() => buildThread(events), [events]);
|
||||
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
@ -74,15 +112,16 @@ export default function ThreadView() {
|
||||
return (
|
||||
<VerticalPageLayout px={{ base: 0, md: "2" }}>
|
||||
{!focusedEvent && (
|
||||
<Heading mx="auto" my="4">
|
||||
<Spinner /> Loading note
|
||||
</Heading>
|
||||
<>
|
||||
<Heading my="4">
|
||||
<Spinner /> Loading note
|
||||
</Heading>
|
||||
<LoadingNostrLink link={{ type: "nevent", data: pointer }} />
|
||||
</>
|
||||
)}
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
{focusedEvent && rootId ? (
|
||||
<ThreadPage thread={thread} rootId={rootId} focusId={focusedEvent.id} />
|
||||
) : (
|
||||
<Spinner />
|
||||
{focusedEvent && rootPointer && (
|
||||
<ThreadPage thread={thread} rootPointer={rootPointer} focusId={focusedEvent.id} />
|
||||
)}
|
||||
</IntersectionObserverProvider>
|
||||
</VerticalPageLayout>
|
||||
|
Loading…
x
Reference in New Issue
Block a user