diff --git a/src/app.tsx b/src/app.tsx index 937ded1ee..e8ae02926 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -41,7 +41,7 @@ export const App = () => { } /> diff --git a/src/components/note-link.tsx b/src/components/note-link.tsx new file mode 100644 index 000000000..9861dfc8b --- /dev/null +++ b/src/components/note-link.tsx @@ -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 ( + + {truncatedId(note1)} + + ); +}; diff --git a/src/components/post/index.tsx b/src/components/note/index.tsx similarity index 77% rename from src/components/post/index.tsx rename to src/components/note/index.tsx index e4700aa21..f470a8365 100644 --- a/src/components/post/index.tsx +++ b/src/components/note/index.tsx @@ -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)} - + {moment(event.created_at * 1000).fromNow()} - {isReply(event) && } + {isReply(event) && } - + - - - - - + + + {/* */} {/* */} diff --git a/src/components/post/post-cc.tsx b/src/components/note/note-cc.tsx similarity index 91% rename from src/components/post/post-cc.tsx rename to src/components/note/note-cc.tsx index 3b32bb7f5..653356e18 100644 --- a/src/components/post/post-cc.tsx +++ b/src/components/note/note-cc.tsx @@ -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; diff --git a/src/components/post/post-contents.tsx b/src/components/note/note-contents.tsx similarity index 84% rename from src/components/post/post-contents.tsx rename to src/components/note/note-contents.tsx index cc068c015..84e36aff1 100644 --- a/src/components/post/post-contents.tsx +++ b/src/components/note/note-contents.tsx @@ -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) => , + render: (match) => , }, // Twitter tweet { regexp: /^https?:\/\/twitter\.com\/(?:\#!\/)?(\w+)\/status(es)?\/(\d+)/im, - render: (match) => , + render: (match) => , }, // 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 ; + return ; }, }, // Video @@ -97,7 +98,7 @@ const embeds: { regexp: /(https?:\/\/)([\da-z\.-]+\.[a-z\.]{2,6})([\/\w\.-]+\.(mp4|mkv|webm|mov))[^\s]*/im, render: (match) => ( - ), }, @@ -105,7 +106,7 @@ const embeds: { { regexp: /(https?:\/\/[^\s]+)/im, render: (match) => ( - + {match[0]} ), @@ -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 ; + if (tag) { + if (tag[0] === "p" && tag[1]) { + return ; + } + if (tag[0] === "e" && tag[1]) { + return ; + } } 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 ( - {parts.map((part) => (typeof part === "string" ? {part} : part))} + + {parts.map((part, i) => ( + {part} + ))} + ); }); diff --git a/src/components/post/post-menu.tsx b/src/components/note/note-menu.tsx similarity index 92% rename from src/components/post/post-menu.tsx rename to src/components/note/note-menu.tsx index 12fdf9081..783dd8ff9 100644 --- a/src/components/post/post-menu.tsx +++ b/src/components/note/note-menu.tsx @@ -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 }) => {
{JSON.stringify(event, null, 2)}
+
{JSON.stringify(getReferences(event), null, 2)}
diff --git a/src/components/user-link.tsx b/src/components/user-link.tsx index c2b39f4d4..ada501edd 100644 --- a/src/components/user-link.tsx +++ b/src/components/user-link.tsx @@ -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 ( - + @{getUserDisplayName(metadata, pubkey)} ); diff --git a/src/helpers/nostr-event.ts b/src/helpers/nostr-event.ts index 9196a0736..9d2b8cc18 100644 --- a/src/helpers/nostr-event.ts +++ b/src/helpers/nostr-event.ts @@ -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); } diff --git a/src/helpers/thread.ts b/src/helpers/thread.ts index 13f7e79e2..5a29175e7 100644 --- a/src/helpers/thread.ts +++ b/src/helpers/thread.ts @@ -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, diff --git a/src/helpers/user-metadata.ts b/src/helpers/user-metadata.ts index 9f0fe2327..12e989250 100644 --- a/src/helpers/user-metadata.ts +++ b/src/helpers/user-metadata.ts @@ -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); } diff --git a/src/views/event/index.tsx b/src/views/event/index.tsx index 5b4dcd679..757a227fb 100644 --- a/src/views/event/index.tsx +++ b/src/views/event/index.tsx @@ -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 ; + let pageContent = Missing Event; + const isRoot = rootId === focusId; const rootPost = thread.get(rootId); if (isRoot && rootPost) { - return ; + pageContent = ; } const post = thread.get(focusId); @@ -56,16 +58,21 @@ export const EventView = ({ eventId }: EventViewProps) => { } } - return ( - - {parentPosts.map((post) => ( - + pageContent = ( + <> + {parentPosts.map((parent) => ( + ))} - + ); } else if (events[focusId]) { - return ; + pageContent = ; } - return Missing Event; + + return ( + + {pageContent} + + ); }; diff --git a/src/views/event/thread-post.tsx b/src/views/event/thread-post.tsx index 6dee28f78..400b4e215 100644 --- a/src/views/event/thread-post.tsx +++ b/src/views/event/thread-post.tsx @@ -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 ( - + {post.children.length > 0 && ( <> } diff --git a/src/views/home/following-posts-tab.tsx b/src/views/home/following-tab.tsx similarity index 85% rename from src/views/home/following-posts-tab.tsx rename to src/views/home/following-tab.tsx index 9684ec837..c1658ead7 100644 --- a/src/views/home/following-posts-tab.tsx +++ b/src/views/home/following-tab.tsx @@ -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 ( @@ -35,7 +35,7 @@ export const FollowingPostsTab = () => { {timeline.map((event) => ( - + ))} {loading ? : } diff --git a/src/views/home/global-tab.tsx b/src/views/home/global-tab.tsx index 5d4e47000..0f664b3af 100644 --- a/src/views/home/global-tab.tsx +++ b/src/views/home/global-tab.tsx @@ -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 ( {timeline.map((event) => ( - + ))} {loading ? : } diff --git a/src/views/home/index.tsx b/src/views/home/index.tsx index fc15b508f..f4848e5f0 100644 --- a/src/views/home/index.tsx +++ b/src/views/home/index.tsx @@ -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 = () => { - + diff --git a/src/views/user/index.tsx b/src/views/user/index.tsx index 5bf35eb9a..7a3166045 100644 --- a/src/views/user/index.tsx +++ b/src/views/user/index.tsx @@ -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} - Posts + Notes Replies Followers Following @@ -89,7 +89,7 @@ export const UserView = ({ pubkey }: UserViewProps) => { - + diff --git a/src/views/user/posts.tsx b/src/views/user/notes.tsx similarity index 69% rename from src/views/user/posts.tsx rename to src/views/user/notes.tsx index 7a3703755..8adf78041 100644 --- a/src/views/user/posts.tsx +++ b/src/views/user/notes.tsx @@ -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 ( {timeline.map((event) => ( - + ))} {loading ? : } diff --git a/src/views/user/replies.tsx b/src/views/user/replies.tsx index 0942741cf..c3656bd46 100644 --- a/src/views/user/replies.tsx +++ b/src/views/user/replies.tsx @@ -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 ( {timeline.map((event) => ( - + ))} {loading ? : }