mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 21:31:43 +01:00
finish thread view
fix bugs with mentions being considered legacy tags
This commit is contained in:
parent
5d3f269b43
commit
cea88afb21
@ -73,3 +73,9 @@ export const AddIcon = createIcon({
|
||||
d: "M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z",
|
||||
defaultProps,
|
||||
});
|
||||
|
||||
export const ArrowDownS = createIcon({
|
||||
displayName: "arrow-down-s",
|
||||
d: "M12 13.172l4.95-4.95 1.414 1.414L12 16 5.636 9.636 7.05 8.222z",
|
||||
defaultProps,
|
||||
});
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||
import moment from "moment";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
@ -11,8 +12,9 @@ import {
|
||||
Flex,
|
||||
Heading,
|
||||
HStack,
|
||||
Text,
|
||||
IconButton,
|
||||
VStack,
|
||||
Link,
|
||||
} from "@chakra-ui/react";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||
@ -27,12 +29,12 @@ import { isReply } from "../../helpers/nostr-event";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import identity from "../../services/identity";
|
||||
import { useUserContacts } from "../../hooks/use-user-contacts";
|
||||
import { ArrowDownS } from "../icons";
|
||||
|
||||
export type PostProps = {
|
||||
event: NostrEvent;
|
||||
};
|
||||
export const Post = React.memo(({ event }: PostProps) => {
|
||||
const navigate = useNavigate();
|
||||
const metadata = useUserMetadata(event.pubkey);
|
||||
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
@ -48,37 +50,39 @@ export const Post = React.memo(({ event }: PostProps) => {
|
||||
|
||||
<Box>
|
||||
<Heading size="sm" display="inline">
|
||||
<Link to={`/u/${normalizeToBech32(event.pubkey, Bech32Prefix.Pubkey)}`}>
|
||||
<Link as={RouterLink} to={`/u/${normalizeToBech32(event.pubkey, Bech32Prefix.Pubkey)}`}>
|
||||
{getUserDisplayName(metadata, event.pubkey)}
|
||||
</Link>
|
||||
</Heading>
|
||||
<Text display="inline" ml="2">
|
||||
<Link as={RouterLink} to={`/e/${normalizeToBech32(event.id, Bech32Prefix.Note)}`} ml="2">
|
||||
{moment(event.created_at * 1000).fromNow()}
|
||||
</Text>
|
||||
</Link>
|
||||
{isReply(event) && <PostCC event={event} />}
|
||||
</Box>
|
||||
</Flex>
|
||||
<PostMenu event={event} />
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody padding="0" mb="2">
|
||||
<CardBody padding="0">
|
||||
<VStack alignItems="flex-start" justifyContent="stretch">
|
||||
<Box overflow="hidden" width="100%">
|
||||
<PostContents event={event} trusted={following.includes(event.pubkey)} />
|
||||
</Box>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
<CardFooter padding="0">
|
||||
<Flex gap="2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
onClick={() => navigate(`/e/${normalizeToBech32(event.id, Bech32Prefix.Note)}`)}
|
||||
>
|
||||
Replies
|
||||
</Button>
|
||||
</Flex>
|
||||
</CardFooter>
|
||||
{/* <CardFooter padding="0" gap="2"> */}
|
||||
{/* <Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
onClick={() => navigate(`/e/${normalizeToBech32(event.id, Bech32Prefix.Note)}`)}
|
||||
>
|
||||
Replies
|
||||
</Button> */}
|
||||
{/* <ButtonGroup size="sm" isAttached variant="outline" ml="auto">
|
||||
<Button>Like</Button>
|
||||
<IconButton aria-label="Show Likes" icon={<ArrowDownS />} />
|
||||
</ButtonGroup> */}
|
||||
{/* </CardFooter> */}
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
@ -34,13 +34,14 @@ export function getReferences(event: NostrEvent) {
|
||||
|
||||
// legacy behavior
|
||||
// https://github.com/nostr-protocol/nips/blob/master/10.md#positional-e-tags-deprecated
|
||||
if (!rootId && !replyId && eTags.length >= 1) {
|
||||
const legacyTags = eTags.filter((t) => !t[3]);
|
||||
if (!rootId && !replyId && legacyTags.length >= 1) {
|
||||
// console.info(`Using legacy threading behavior for ${event.id}`, event);
|
||||
|
||||
// first tag is the root
|
||||
rootId = eTags[0][1];
|
||||
rootId = legacyTags[0][1];
|
||||
// last tag is reply
|
||||
replyId = eTags[eTags.length - 1][1] ?? rootId;
|
||||
replyId = legacyTags[legacyTags.length - 1][1] ?? rootId;
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -1,18 +1,18 @@
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { EventReferences, getReferences } from "./nostr-event";
|
||||
|
||||
export type LinkedEvent = {
|
||||
export type ThreadItem = {
|
||||
event: NostrEvent;
|
||||
root?: LinkedEvent;
|
||||
reply?: LinkedEvent;
|
||||
root?: ThreadItem;
|
||||
reply?: ThreadItem;
|
||||
refs: EventReferences;
|
||||
children: LinkedEvent[];
|
||||
children: ThreadItem[];
|
||||
};
|
||||
|
||||
export function linkEvents(events: NostrEvent[]) {
|
||||
const idToChildren: Record<string, NostrEvent[]> = {};
|
||||
|
||||
const replies = new Map<string, LinkedEvent>();
|
||||
const replies = new Map<string, ThreadItem>();
|
||||
for (const event of events) {
|
||||
const refs = getReferences(event);
|
||||
|
||||
@ -20,7 +20,7 @@ export function linkEvents(events: NostrEvent[]) {
|
||||
idToChildren[refs.replyId] = idToChildren[refs.replyId] || [];
|
||||
idToChildren[refs.replyId].push(event);
|
||||
}
|
||||
if (refs.rootId) {
|
||||
if (refs.rootId && refs.rootId !== refs.replyId) {
|
||||
idToChildren[refs.rootId] = idToChildren[refs.rootId] || [];
|
||||
idToChildren[refs.rootId].push(event);
|
||||
}
|
||||
@ -37,7 +37,7 @@ export function linkEvents(events: NostrEvent[]) {
|
||||
|
||||
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 ThreadItem) ?? [];
|
||||
}
|
||||
|
||||
return replies;
|
||||
|
@ -33,12 +33,12 @@ export function useThreadLoader(eventId: string, opts?: Options) {
|
||||
const loading = useSubject(loader.loading);
|
||||
const rootId = useSubject(loader.rootId);
|
||||
const focusId = useSubject(loader.focusId);
|
||||
const linked = useMemo(() => linkEvents(Object.values(events)), [events]);
|
||||
const thread = useMemo(() => linkEvents(Object.values(events)), [events]);
|
||||
|
||||
return {
|
||||
loader,
|
||||
events,
|
||||
linked,
|
||||
thread,
|
||||
rootId,
|
||||
focusId,
|
||||
loading,
|
||||
|
@ -1,55 +0,0 @@
|
||||
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 { useThreadLoader } from "../hooks/use-thread-loader";
|
||||
|
||||
export const EventPage = () => {
|
||||
const params = useParams();
|
||||
let id = normalizeToHex(params.id ?? "");
|
||||
|
||||
if (!id) {
|
||||
return (
|
||||
<Page>
|
||||
<Alert status="error">
|
||||
<AlertIcon />
|
||||
<AlertTitle>Invalid event id</AlertTitle>
|
||||
<AlertDescription>"{params.id}" dose not look like a valid event id</AlertDescription>
|
||||
</Alert>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<EventView eventId={id} />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export type EventViewProps = {
|
||||
eventId: string;
|
||||
};
|
||||
|
||||
export const EventView = ({ eventId }: EventViewProps) => {
|
||||
const id = normalizeToHex(eventId) ?? "";
|
||||
const { linked, events, rootId, focusId, loading } = useThreadLoader(id, { enabled: !!id });
|
||||
|
||||
if (loading) return <Spinner />;
|
||||
|
||||
const entry = linked.get(focusId);
|
||||
if (entry) {
|
||||
const isRoot = rootId === focusId;
|
||||
|
||||
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>;
|
||||
};
|
71
src/views/event/index.tsx
Normal file
71
src/views/event/index.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { Alert, AlertDescription, AlertIcon, AlertTitle, Flex, Spinner, Text } 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 { useThreadLoader } from "../../hooks/use-thread-loader";
|
||||
import { ThreadPost } from "./thread-post";
|
||||
|
||||
export const EventPage = () => {
|
||||
const params = useParams();
|
||||
let id = normalizeToHex(params.id ?? "");
|
||||
|
||||
if (!id) {
|
||||
return (
|
||||
<Page>
|
||||
<Alert status="error">
|
||||
<AlertIcon />
|
||||
<AlertTitle>Invalid event id</AlertTitle>
|
||||
<AlertDescription>"{params.id}" dose not look like a valid event id</AlertDescription>
|
||||
</Alert>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<EventView eventId={id} />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export type EventViewProps = {
|
||||
eventId: string;
|
||||
};
|
||||
|
||||
export const EventView = ({ eventId }: EventViewProps) => {
|
||||
const id = normalizeToHex(eventId) ?? "";
|
||||
const { thread, events, rootId, focusId, loading } = useThreadLoader(id, { enabled: !!id });
|
||||
|
||||
if (loading) return <Spinner />;
|
||||
|
||||
const isRoot = rootId === focusId;
|
||||
const rootPost = thread.get(rootId);
|
||||
if (isRoot && rootPost) {
|
||||
return <ThreadPost post={rootPost} initShowReplies />;
|
||||
}
|
||||
|
||||
const post = thread.get(focusId);
|
||||
if (post) {
|
||||
const parentPosts = [];
|
||||
if (post.reply) {
|
||||
let p = post;
|
||||
while (p.reply) {
|
||||
parentPosts.unshift(p.reply);
|
||||
p = p.reply;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="4" overflow="auto" flex={1} pb="4" pt="4">
|
||||
{parentPosts.map((post) => (
|
||||
<Post key={post.event.id} event={post.event} />
|
||||
))}
|
||||
<ThreadPost key={post.event.id} post={post} initShowReplies />
|
||||
</Flex>
|
||||
);
|
||||
} else if (events[focusId]) {
|
||||
return <Post event={events[focusId]} />;
|
||||
}
|
||||
return <span>Missing Event</span>;
|
||||
};
|
34
src/views/event/thread-post.tsx
Normal file
34
src/views/event/thread-post.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Button, Flex, useDisclosure } from "@chakra-ui/react";
|
||||
import { useState } from "react";
|
||||
import { Post } from "../../components/post";
|
||||
import { ThreadItem as ThreadItemData } from "../../helpers/thread";
|
||||
|
||||
export type ThreadItemProps = {
|
||||
post: ThreadItemData;
|
||||
initShowReplies?: boolean;
|
||||
};
|
||||
|
||||
export const ThreadPost = ({ post, initShowReplies }: ThreadItemProps) => {
|
||||
const [showReplies, setShowReplies] = useState(!!initShowReplies);
|
||||
const toggle = () => setShowReplies((v) => !v);
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
<Post event={post.event} />
|
||||
{post.children.length > 0 && (
|
||||
<>
|
||||
<Button variant="link" size="sm" alignSelf="flex-start" onClick={toggle}>
|
||||
{post.children.length} {post.children.length > 1 ? "Replies" : "Reply"}
|
||||
</Button>
|
||||
{showReplies && (
|
||||
<Flex direction="column" gap="2" pl="4" borderLeftColor="gray.500" borderLeftWidth="1px">
|
||||
{post.children.map((child) => (
|
||||
<ThreadPost key={post.event.id} post={child} />
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user