finish thread view

fix bugs with mentions being considered legacy tags
This commit is contained in:
hzrd149 2023-02-07 17:04:18 -06:00
parent 5d3f269b43
commit cea88afb21
8 changed files with 146 additions and 85 deletions

View File

@ -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,
});

View File

@ -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>
);
});

View File

@ -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 {

View File

@ -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;

View File

@ -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,

View File

@ -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
View 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>;
};

View 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>
);
};