fix bug with threading

lots more cleanup
This commit is contained in:
hzrd149 2023-02-07 17:04:18 -06:00
parent e0a1b2ed1b
commit f0a89ac59e
19 changed files with 108 additions and 76 deletions

View File

@ -41,7 +41,7 @@ export const App = () => {
}
/>
<Route
path="/e/:id"
path="/n/:id"
element={
<RequireSetup>
<EventPage />

View File

@ -0,0 +1,18 @@
import { Link, LinkProps } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { Bech32Prefix, normalizeToBech32 } from "../helpers/nip-19";
import { truncatedId } from "../helpers/nostr-event";
export type NoteLinkProps = LinkProps & {
noteId: string;
};
export const NoteLink = ({ noteId, ...props }: NoteLinkProps) => {
const note1 = normalizeToBech32(noteId, Bech32Prefix.Note) ?? noteId;
return (
<Link as={RouterLink} to={`/n/${note1}`} {...props}>
{truncatedId(note1)}
</Link>
);
};

View File

@ -22,19 +22,19 @@ import { UserAvatarLink } from "../user-avatar-link";
import { getUserDisplayName } from "../../helpers/user-metadata";
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
import { PostContents } from "./post-contents";
import { PostMenu } from "./post-menu";
import { PostCC } from "./post-cc";
import { NoteContents } from "./note-contents";
import { NoteMenu } from "./note-menu";
import { NoteCC } from "./note-cc";
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 = {
export type NoteProps = {
event: NostrEvent;
};
export const Post = React.memo(({ event }: PostProps) => {
export const Note = React.memo(({ event }: NoteProps) => {
const metadata = useUserMetadata(event.pubkey);
const pubkey = useSubject(identity.pubkey);
@ -54,27 +54,25 @@ export const Post = React.memo(({ event }: PostProps) => {
{getUserDisplayName(metadata, event.pubkey)}
</Link>
</Heading>
<Link as={RouterLink} to={`/e/${normalizeToBech32(event.id, Bech32Prefix.Note)}`} ml="2">
<Link as={RouterLink} to={`/n/${normalizeToBech32(event.id, Bech32Prefix.Note)}`} ml="2">
{moment(event.created_at * 1000).fromNow()}
</Link>
{isReply(event) && <PostCC event={event} />}
{isReply(event) && <NoteCC event={event} />}
</Box>
</Flex>
<PostMenu event={event} />
<NoteMenu event={event} />
</HStack>
</CardHeader>
<CardBody padding="0">
<VStack alignItems="flex-start" justifyContent="stretch">
<Box overflow="hidden" width="100%">
<PostContents event={event} trusted={following.includes(event.pubkey)} />
</Box>
</VStack>
<Box overflow="hidden" width="100%">
<NoteContents event={event} trusted={following.includes(event.pubkey)} />
</Box>
</CardBody>
{/* <CardFooter padding="0" gap="2"> */}
{/* <Button
size="sm"
variant="link"
onClick={() => navigate(`/e/${normalizeToBech32(event.id, Bech32Prefix.Note)}`)}
onClick={() => navigate(`/n/${normalizeToBech32(event.id, Bech32Prefix.Note)}`)}
>
Replies
</Button> */}

View File

@ -2,7 +2,7 @@ import { Text } from "@chakra-ui/react";
import { isPTag, NostrEvent } from "../../types/nostr-event";
import { UserLink } from "../user-link";
export const PostCC = ({ event }: { event: NostrEvent }) => {
export const NoteCC = ({ event }: { event: NostrEvent }) => {
const hasCC = event.tags.some(isPTag);
if (!hasCC) return null;

View File

@ -5,6 +5,7 @@ import { TweetEmbed } from "../tweet-embed";
import { UserLink } from "../user-link";
import { normalizeToHex } from "../../helpers/nip-19";
import { NostrEvent } from "../../types/nostr-event";
import { NoteLink } from "../note-link";
const BlurredImage = (props: ImageProps) => {
const { isOpen, onToggle } = useDisclosure();
@ -18,12 +19,12 @@ const embeds: {
// Lightning Invoice
{
regexp: /(lightning:)?(LNBC[A-Za-z0-9]+)/im,
render: (match) => <InlineInvoiceCard key={match[0]} paymentRequest={match[2]} />,
render: (match) => <InlineInvoiceCard paymentRequest={match[2]} />,
},
// Twitter tweet
{
regexp: /^https?:\/\/twitter\.com\/(?:\#!\/)?(\w+)\/status(es)?\/(\d+)/im,
render: (match) => <TweetEmbed key={match[0]} href={match[0]} conversation={false} />,
render: (match) => <TweetEmbed href={match[0]} conversation={false} />,
},
// Youtube Video
{
@ -89,7 +90,7 @@ const embeds: {
regexp: /(https?:\/\/)([\da-z\.-]+\.[a-z\.]{2,6})([\/\w\.-]+\.(svg|gif|png|jpg|jpeg|webp|avif))[^\s]*/im,
render: (match, trusted) => {
const ImageComponent = trusted ? Image : BlurredImage;
return <ImageComponent key={match[0]} src={match[0]} width="100%" maxWidth="30rem" />;
return <ImageComponent src={match[0]} width="100%" maxWidth="30rem" />;
},
},
// Video
@ -97,7 +98,7 @@ const embeds: {
regexp: /(https?:\/\/)([\da-z\.-]+\.[a-z\.]{2,6})([\/\w\.-]+\.(mp4|mkv|webm|mov))[^\s]*/im,
render: (match) => (
<AspectRatio ratio={16 / 9} maxWidth="30rem">
<video key={match[0]} src={match[0]} controls />
<video src={match[0]} controls />
</AspectRatio>
),
},
@ -105,7 +106,7 @@ const embeds: {
{
regexp: /(https?:\/\/[^\s]+)/im,
render: (match) => (
<Link key={match[0]} color="blue.500" href={match[0]} target="_blank">
<Link color="blue.500" href={match[0]} target="_blank">
{match[0]}
</Link>
),
@ -123,15 +124,20 @@ const embeds: {
}
},
},
// Nostr Embeds
// Nostr Mention Links
{
regexp: /#\[(\d+)\]/,
render: (match, event) => {
const index = parseInt(match[1]);
const tag = event?.tags[index];
if (tag && tag[0] === "p" && tag[1]) {
return <UserLink color="blue.500" pubkey={tag[1]} />;
if (tag) {
if (tag[0] === "p" && tag[1]) {
return <UserLink color="blue.500" pubkey={tag[1]} />;
}
if (tag[0] === "e" && tag[1]) {
return <NoteLink color="blue.500" noteId={tag[1]} />;
}
}
return match[0];
@ -161,15 +167,19 @@ function embedContent(content: string, event?: NostrEvent, trusted: boolean = fa
return [content];
}
export type PostContentsProps = {
export type NoteContentsProps = {
event: NostrEvent;
trusted?: boolean;
};
export const PostContents = React.memo(({ event, trusted }: PostContentsProps) => {
export const NoteContents = React.memo(({ event, trusted }: NoteContentsProps) => {
const parts = embedContent(event.content, event, trusted ?? false);
return (
<Box whiteSpace="pre-wrap">{parts.map((part) => (typeof part === "string" ? <span>{part}</span> : part))}</Box>
<Box whiteSpace="pre-wrap">
{parts.map((part, i) => (
<span key={"part-" + i}>{part}</span>
))}
</Box>
);
});

View File

@ -16,8 +16,9 @@ import { NostrEvent } from "../../types/nostr-event";
import { MenuIconButton } from "../menu-icon-button";
import { ClipboardIcon, CodeIcon, IMAGE_ICONS } from "../icons";
import { getReferences } from "../../helpers/nostr-event";
export const PostMenu = ({ event }: { event: NostrEvent }) => {
export const NoteMenu = ({ event }: { event: NostrEvent }) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
const noteId = normalizeToBech32(event.id, Bech32Prefix.Note);
@ -74,6 +75,7 @@ export const PostMenu = ({ event }: { event: NostrEvent }) => {
<ModalCloseButton />
<ModalBody overflow="auto">
<pre>{JSON.stringify(event, null, 2)}</pre>
<pre>{JSON.stringify(getReferences(event), null, 2)}</pre>
</ModalBody>
</ModalContent>
</Modal>

View File

@ -10,9 +10,10 @@ export type UserLinkProps = LinkProps & {
export const UserLink = ({ pubkey, ...props }: UserLinkProps) => {
const metadata = useUserMetadata(pubkey);
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
return (
<Link as={RouterLink} to={`/u/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`} {...props}>
<Link as={RouterLink} to={`/u/${npub}`} {...props}>
@{getUserDisplayName(metadata, pubkey)}
</Link>
);

View File

@ -4,7 +4,7 @@ export function isReply(event: NostrEvent) {
return !!event.tags.find((tag) => isETag(tag) && tag[3] !== "mention");
}
export function isPost(event: NostrEvent) {
export function isNote(event: NostrEvent) {
return !isReply(event);
}

View File

@ -20,10 +20,6 @@ export function linkEvents(events: NostrEvent[]) {
idToChildren[refs.replyId] = idToChildren[refs.replyId] || [];
idToChildren[refs.replyId].push(event);
}
if (refs.rootId && refs.rootId !== refs.replyId) {
idToChildren[refs.rootId] = idToChildren[refs.rootId] || [];
idToChildren[refs.rootId].push(event);
}
replies.set(event.id, {
event,

View File

@ -1,5 +1,5 @@
import { Kind0ParsedContent } from "../types/nostr-event";
import { normalizeToBech32 } from "./nip-19";
import { Bech32Prefix, normalizeToBech32 } from "./nip-19";
import { truncatedId } from "./nostr-event";
export function getUserDisplayName(metadata: Kind0ParsedContent | undefined, pubkey: string) {
@ -8,5 +8,5 @@ export function getUserDisplayName(metadata: Kind0ParsedContent | undefined, pub
} else if (metadata?.name) {
return metadata.name;
}
return truncatedId(normalizeToBech32(pubkey) ?? pubkey);
return truncatedId(normalizeToBech32(pubkey, Bech32Prefix.Pubkey) ?? pubkey);
}

View File

@ -2,7 +2,7 @@ import { Alert, AlertDescription, AlertIcon, AlertTitle, Flex, Spinner, Text } f
import { Page } from "../../components/page";
import { useParams } from "react-router-dom";
import { normalizeToHex } from "../../helpers/nip-19";
import { Post } from "../../components/post";
import { Note } from "../../components/note";
import { useThreadLoader } from "../../hooks/use-thread-loader";
import { ThreadPost } from "./thread-post";
@ -39,10 +39,12 @@ export const EventView = ({ eventId }: EventViewProps) => {
if (loading) return <Spinner />;
let pageContent = <span>Missing Event</span>;
const isRoot = rootId === focusId;
const rootPost = thread.get(rootId);
if (isRoot && rootPost) {
return <ThreadPost post={rootPost} initShowReplies />;
pageContent = <ThreadPost post={rootPost} initShowReplies />;
}
const post = thread.get(focusId);
@ -56,16 +58,21 @@ export const EventView = ({ eventId }: EventViewProps) => {
}
}
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} />
pageContent = (
<>
{parentPosts.map((parent) => (
<Note key={parent.event.id + "-rely"} event={parent.event} />
))}
<ThreadPost key={post.event.id} post={post} initShowReplies />
</Flex>
</>
);
} else if (events[focusId]) {
return <Post event={events[focusId]} />;
pageContent = <Note event={events[focusId]} />;
}
return <span>Missing Event</span>;
return (
<Flex direction="column" gap="4" overflow="auto" flex={1} pb="4" pt="4" pl="1" pr="1">
{pageContent}
</Flex>
);
};

View File

@ -1,6 +1,6 @@
import { Button, Flex, useDisclosure } from "@chakra-ui/react";
import { useState } from "react";
import { Post } from "../../components/post";
import { Note } from "../../components/note";
import { ThreadItem as ThreadItemData } from "../../helpers/thread";
export type ThreadItemProps = {
@ -14,7 +14,7 @@ export const ThreadPost = ({ post, initShowReplies }: ThreadItemProps) => {
return (
<Flex direction="column" gap="2">
<Post event={post.event} />
<Note event={post.event} />
{post.children.length > 0 && (
<>
<Button variant="link" size="sm" alignSelf="flex-start" onClick={toggle}>
@ -23,7 +23,7 @@ export const ThreadPost = ({ post, initShowReplies }: ThreadItemProps) => {
{showReplies && (
<Flex direction="column" gap="2" pl="4" borderLeftColor="gray.500" borderLeftWidth="1px">
{post.children.map((child) => (
<ThreadPost key={post.event.id} post={child} />
<ThreadPost key={child.event.id} post={child} />
))}
</Flex>
)}

View File

@ -2,13 +2,13 @@ import { useEffect, useState } from "react";
import { Button, Flex, Spinner } from "@chakra-ui/react";
import moment from "moment";
import { mergeAll, from } from "rxjs";
import { Post } from "../../components/post";
import { Note } from "../../components/note";
import useSubject from "../../hooks/use-subject";
import { useUserContacts } from "../../hooks/use-user-contacts";
import identity from "../../services/identity";
import userContactsService from "../../services/user-contacts";
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
import { isPost } from "../../helpers/nostr-event";
import { isNote } from "../../helpers/nostr-event";
function useExtendedContacts(pubkey: string) {
const [extendedContacts, setExtendedContacts] = useState<string[]>([]);
@ -42,17 +42,17 @@ export const DiscoverTab = () => {
const contactsOfContacts = useExtendedContacts(pubkey);
const { events, loading, loadMore } = useTimelineLoader(
`discover-posts`,
`discover`,
{ authors: contactsOfContacts, kinds: [1], since: moment().subtract(1, "hour").unix() },
{ pageSize: moment.duration(1, "hour").asSeconds(), enabled: contactsOfContacts.length > 0 }
);
const timeline = events.filter(isPost);
const timeline = events.filter(isNote);
return (
<Flex direction="column" overflow="auto" gap="2">
{timeline.map((event) => (
<Post key={event.id} event={event} />
<Note key={event.id} event={event} />
))}
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
</Flex>

View File

@ -1,14 +1,14 @@
import { Button, Flex, FormControl, FormLabel, Spinner, Switch, useDisclosure } from "@chakra-ui/react";
import { useSearchParams } from "react-router-dom";
import moment from "moment";
import { Post } from "../../components/post";
import { isPost } from "../../helpers/nostr-event";
import { Note } from "../../components/note";
import { isNote } from "../../helpers/nostr-event";
import useSubject from "../../hooks/use-subject";
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
import { useUserContacts } from "../../hooks/use-user-contacts";
import identity from "../../services/identity";
export const FollowingPostsTab = () => {
export const FollowingTab = () => {
const pubkey = useSubject(identity.pubkey);
const contacts = useUserContacts(pubkey);
const [search, setSearch] = useSearchParams();
@ -24,7 +24,7 @@ export const FollowingPostsTab = () => {
{ pageSize: moment.duration(2, "hour").asSeconds(), enabled: following.length > 0 }
);
const timeline = showReplies ? events : events.filter(isPost);
const timeline = showReplies ? events : events.filter(isNote);
return (
<Flex direction="column" overflow="auto" gap="2">
@ -35,7 +35,7 @@ export const FollowingPostsTab = () => {
<Switch id="show-replies" isChecked={showReplies} onChange={onToggle} />
</FormControl>
{timeline.map((event) => (
<Post key={event.id} event={event} />
<Note key={event.id} event={event} />
))}
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
</Flex>

View File

@ -1,22 +1,22 @@
import { Button, Flex, Spinner } from "@chakra-ui/react";
import moment from "moment";
import { Post } from "../../components/post";
import { isPost } from "../../helpers/nostr-event";
import { Note } from "../../components/note";
import { isNote } from "../../helpers/nostr-event";
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
export const GlobalTab = () => {
const { events, loading, loadMore } = useTimelineLoader(
`global-posts`,
`global`,
{ kinds: [1], since: moment().subtract(5, "minutes").unix() },
{ pageSize: moment.duration(5, "minutes").asSeconds() }
);
const timeline = events.filter(isPost);
const timeline = events.filter(isNote);
return (
<Flex direction="column" overflow="auto" gap="2">
{timeline.map((event) => (
<Post key={event.id} event={event} />
<Note key={event.id} event={event} />
))}
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
</Flex>

View File

@ -1,7 +1,7 @@
import { Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
// import { useMatch, useNavigate } from "react-router-dom";
import { DiscoverTab } from "./discover-tab";
import { FollowingPostsTab } from "./following-posts-tab";
import { FollowingTab } from "./following-tab";
import { GlobalTab } from "./global-tab";
export const HomeView = () => {
@ -28,7 +28,7 @@ export const HomeView = () => {
</TabList>
<TabPanels overflow="auto" height="100%">
<TabPanel pr={0} pl={0}>
<FollowingPostsTab />
<FollowingTab />
</TabPanel>
<TabPanel pr={0} pl={0}>
<DiscoverTab />

View File

@ -15,7 +15,7 @@ import {
Box,
} from "@chakra-ui/react";
import { useParams } from "react-router-dom";
import { UserPostsTab } from "./posts";
import { UserNotesTab } from "./notes";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import { UserAvatar } from "../../components/user-avatar";
import { getUserDisplayName } from "../../helpers/user-metadata";
@ -80,7 +80,7 @@ export const UserView = ({ pubkey }: UserViewProps) => {
{header}
<Tabs display="flex" flexDirection="column" flexGrow="1" overflow={isMobile ? undefined : "hidden"} isLazy>
<TabList overflow={isMobile ? "auto" : undefined}>
<Tab>Posts</Tab>
<Tab>Notes</Tab>
<Tab>Replies</Tab>
<Tab>Followers</Tab>
<Tab>Following</Tab>
@ -89,7 +89,7 @@ export const UserView = ({ pubkey }: UserViewProps) => {
<TabPanels overflow={isMobile ? undefined : "auto"} height="100%">
<TabPanel pr={0} pl={0}>
<UserPostsTab pubkey={pubkey} />
<UserNotesTab pubkey={pubkey} />
</TabPanel>
<TabPanel pr={0} pl={0}>
<UserRepliesTab pubkey={pubkey} />

View File

@ -1,21 +1,21 @@
import { Button, Flex, Spinner } from "@chakra-ui/react";
import moment from "moment";
import { Post } from "../../components/post";
import { isPost } from "../../helpers/nostr-event";
import { Note } from "../../components/note";
import { isNote } from "../../helpers/nostr-event";
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
export const UserPostsTab = ({ pubkey }: { pubkey: string }) => {
export const UserNotesTab = ({ pubkey }: { pubkey: string }) => {
const { events, loading, loadMore } = useTimelineLoader(
`${pubkey} posts`,
`${pubkey} notes`,
{ authors: [pubkey], kinds: [1], since: moment().subtract(1, "day").unix() },
{ pageSize: moment.duration(1, "day").asSeconds() }
);
const timeline = events.filter(isPost);
const timeline = events.filter(isNote);
return (
<Flex direction="column" gap="2" pr="2" pl="2">
{timeline.map((event) => (
<Post key={event.id} event={event} />
<Note key={event.id} event={event} />
))}
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
</Flex>

View File

@ -1,6 +1,6 @@
import { Button, Flex, Spinner } from "@chakra-ui/react";
import moment from "moment";
import { Post } from "../../components/post";
import { Note } from "../../components/note";
import { isReply } from "../../helpers/nostr-event";
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
@ -15,7 +15,7 @@ export const UserRepliesTab = ({ pubkey }: { pubkey: string }) => {
return (
<Flex direction="column" gap="2" pr="2" pl="2">
{timeline.map((event) => (
<Post key={event.id} event={event} />
<Note key={event.id} event={event} />
))}
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
</Flex>