From 0a1d5906570ee9cbf85b6580fa9f702244c7391e Mon Sep 17 00:00:00 2001 From: Michael Hall Date: Thu, 31 Aug 2023 11:31:06 -0400 Subject: [PATCH 01/17] add comments drawer --- components/CommentsDrawer/CommentsDrawer.tsx | 77 ++++++++++++++++ components/CommentsDrawer/CommentsFeed.tsx | 26 ++++++ components/Drawer/Drawer.styles.js | 19 ++++ components/Drawer/Drawer.tsx | 23 ++++- components/NoteAction/NoteActionComment.tsx | 34 ++++--- components/ZapWizard/ZapDrawer.tsx | 27 +----- ndk/NDKCommentsProvider.tsx | 96 ++++++++++++++++++++ 7 files changed, 254 insertions(+), 48 deletions(-) create mode 100644 components/CommentsDrawer/CommentsDrawer.tsx create mode 100644 components/CommentsDrawer/CommentsFeed.tsx create mode 100644 components/Drawer/Drawer.styles.js create mode 100644 ndk/NDKCommentsProvider.tsx diff --git a/components/CommentsDrawer/CommentsDrawer.tsx b/components/CommentsDrawer/CommentsDrawer.tsx new file mode 100644 index 00000000..1caf4f40 --- /dev/null +++ b/components/CommentsDrawer/CommentsDrawer.tsx @@ -0,0 +1,77 @@ +import { Avatar, Box, Group, Text, TextInput } from "@mantine/core"; +import Drawer, { DrawerProps } from "components/Drawer/Drawer"; +import useAuth from "hooks/useAuth"; +import useProfilePicSrc from "ndk/hooks/useProfilePicSrc"; +import { useUser } from "ndk/hooks/useUser"; +import CommentsFeed from "./CommentsFeed"; +import { useEvent } from "ndk/NDKEventProvider"; +import { CommentsProvider, useComments } from "ndk/NDKCommentsProvider"; + +export default function CommentsDrawer(props: DrawerProps) { + const { authState } = useAuth(); + const user = useUser(authState.pk); + const src = useProfilePicSrc(user); + const { event } = useEvent(); + + return ( + + + + + + ({ + border: `1px solid ${theme.colors.gray[4]}`, + })} + /> + + + + + ); +} + +const CommentsDrawerHeader = () => { + const { comments } = useComments(); + const commentsCount = + (comments?.length ?? 0) + + (comments?.reduce((prev, curr) => prev + curr.children.length, 0) ?? 0); + + return ( + ({ + borderBottom: `1px solid ${theme.colors.gray[6]}`, + })} + > + + Comments{" "} + {comments && ( + + {commentsCount} + + )} + + + ); +}; diff --git a/components/CommentsDrawer/CommentsFeed.tsx b/components/CommentsDrawer/CommentsFeed.tsx new file mode 100644 index 00000000..012fb46c --- /dev/null +++ b/components/CommentsDrawer/CommentsFeed.tsx @@ -0,0 +1,26 @@ +import { Box, Center, Loader, Text } from "@mantine/core"; +import { useComments } from "ndk/NDKCommentsProvider"; + +export default function CommentsFeed() { + const { comments } = useComments(); + return ( + ({ + borderBottom: `1px solid ${theme.colors.gray[6]}`, + })} + > + {comments ? ( + comments.map((comment) => ( + + {comment.event.content} ({comment.children.length}) + + )) + ) : ( +
+ +
+ )} +
+ ); +} diff --git a/components/Drawer/Drawer.styles.js b/components/Drawer/Drawer.styles.js new file mode 100644 index 00000000..b5c82600 --- /dev/null +++ b/components/Drawer/Drawer.styles.js @@ -0,0 +1,19 @@ +import { createStyles } from "@mantine/core"; + +const useStyles = createStyles((theme, _params, getRef) => ({ + overlay: { + backgroundColor: `${theme.colors.dark[7]} !important`, + backdropFilter: "blur(16px)", + opacity: `${0.5} !important`, + }, + drawer: { + backgroundColor: theme.colors.dark[8], + borderTopLeftRadius: 40, + borderTopRightRadius: 40, + maxWidth: 600, + margin: "auto", + padding: `0 16px 24px 16px !important`, + }, +})); + +export default useStyles; diff --git a/components/Drawer/Drawer.tsx b/components/Drawer/Drawer.tsx index 49888f64..1e8a08b3 100644 --- a/components/Drawer/Drawer.tsx +++ b/components/Drawer/Drawer.tsx @@ -2,7 +2,6 @@ import { Drawer as BaseDrawer, type DrawerProps as BaseDrawerProp, } from "@mantine/core"; -import withStopClickPropagation from "../../utils/hoc/withStopClickPropagation"; import { type PropsWithChildren, type SyntheticEvent, @@ -12,8 +11,7 @@ import { import DrawerHandle from "./DrawerHandle"; import { noop } from "../../utils/common"; import { useMediaQuery } from "@mantine/hooks"; - -const MantineDrawer = withStopClickPropagation(BaseDrawer); +import useStyles from "components/Drawer/Drawer.styles"; export interface DrawerProps extends BaseDrawerProp { onDragEnd?: () => void; @@ -22,6 +20,9 @@ export interface DrawerProps extends BaseDrawerProp { const Drawer = ({ onDragEnd = noop, children, + withCloseButton, + position, + trapFocus, ...rest }: PropsWithChildren) => { const [isDragging, setIsDragging] = useState(false); @@ -31,6 +32,7 @@ const Drawer = ({ const isOpened = rest.opened; const containerRef = useRef(null); const [size, setSize] = useState("auto"); + const { classes } = useStyles(); const getClientY = (event: SyntheticEvent) => { if (event.nativeEvent instanceof TouchEvent) { @@ -80,7 +82,18 @@ const Drawer = ({ }; return ( - + e.stopPropagation()} + position={position || "bottom"} + withCloseButton={withCloseButton || false} + trapFocus={trapFocus || false} + classNames={{ + overlay: classes.overlay, + drawer: classes.drawer, + }} + {...rest} + size={size} + >
{children}
-
+ ); }; diff --git a/components/NoteAction/NoteActionComment.tsx b/components/NoteAction/NoteActionComment.tsx index 9d76c71f..444738f6 100644 --- a/components/NoteAction/NoteActionComment.tsx +++ b/components/NoteAction/NoteActionComment.tsx @@ -2,16 +2,17 @@ import { Group, Text } from "@mantine/core"; import { CommentIcon } from "icons/StemstrIcon"; import NoteAction from "./NoteAction"; import { useEvent } from "../../ndk/NDKEventProvider"; -import { openSheet } from "../../store/Sheets"; -import { useDispatch, useSelector } from "react-redux"; +import { useSelector } from "react-redux"; import useAuth from "hooks/useAuth"; import { AppState } from "../../store/Store"; import { selectNoteState } from "../../store/Notes"; +import { useDisclosure } from "@mantine/hooks"; +import CommentsDrawer from "components/CommentsDrawer/CommentsDrawer"; const NoteActionComment = () => { const { event } = useEvent(); - const dispatch = useDispatch(); const { guardAuth, guardSubscribed } = useAuth(); + const [opened, { open, close }] = useDisclosure(false); const { commentCount } = useSelector((state: AppState) => selectNoteState(state, event.id) ); @@ -19,24 +20,21 @@ const NoteActionComment = () => { const handleClickComment = () => { if (!guardAuth()) return; if (!guardSubscribed()) return; - - dispatch( - openSheet({ - sheetKey: "postSheet", - replyingTo: event.rawEvent(), - }) - ); + open(); }; return ( - - - {" "} - - {commentCount > 0 ? commentCount : ""} - - - + <> + + + + {" "} + + {commentCount > 0 ? commentCount : ""} + + + + ); }; diff --git a/components/ZapWizard/ZapDrawer.tsx b/components/ZapWizard/ZapDrawer.tsx index 6d58daad..79f9efc3 100644 --- a/components/ZapWizard/ZapDrawer.tsx +++ b/components/ZapWizard/ZapDrawer.tsx @@ -1,7 +1,6 @@ import { type PropsWithChildren } from "react"; import { useZapWizard } from "./ZapWizardProvider"; import Drawer from "../Drawer/Drawer"; -import { MantineTheme } from "@mantine/core"; interface ZapDrawerProps { isOpen: boolean; @@ -13,32 +12,10 @@ const ZapDrawer = ({ onClose, children, }: PropsWithChildren) => { - const { end, willShowCloseButton } = useZapWizard(); + const { end } = useZapWizard(); return ( - ({ - overlay: { - backgroundColor: `${theme.colors.dark[7]} !important`, - backdropFilter: "blur(16px)", - opacity: `${0.5} !important`, - }, - drawer: { - backgroundColor: theme.colors.dark[8], - borderTopLeftRadius: 40, - borderTopRightRadius: 40, - maxWidth: 600, - margin: "auto", - padding: `0 16px ${willShowCloseButton ? 48 : 24}px 16px !important`, - }, - })} - onDragEnd={end} - > + {children} ); diff --git a/ndk/NDKCommentsProvider.tsx b/ndk/NDKCommentsProvider.tsx new file mode 100644 index 00000000..45da6814 --- /dev/null +++ b/ndk/NDKCommentsProvider.tsx @@ -0,0 +1,96 @@ +import { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk"; +import { Kind } from "nostr-tools"; +import { + createContext, + type PropsWithChildren, + useContext, + useState, + useEffect, +} from "react"; +import { useNDK } from "./NDKProvider"; +import { parseEventTags } from "./utils"; + +type Comment = { + event: NDKEvent; + children: NDKEvent[]; +}; + +interface CommentsContextProps { + noteId: string; + enabled: boolean; +} + +type CommentsContextType = { + comments?: Comment[]; +}; + +const CommentsContext = createContext({}); + +export const CommentsProvider = ({ + children, + noteId, + enabled, +}: PropsWithChildren) => { + const { ndk } = useNDK(); + const [comments, setComments] = useState(); + + useEffect(() => { + if (enabled && ndk) { + const filter: NDKFilter = { + kinds: [Kind.Text], + "#e": [noteId], + }; + ndk.fetchEvents(filter).then((eventSet) => { + const events = Array.from(eventSet.values()).sort( + (a, b) => b.created_at! - a.created_at! + ); + console.log(events); + let comments: Comment[] = events + .filter((event) => { + const { root, reply } = parseEventTags(event); + return root && !reply && root[1] === noteId; + }) + .map((event) => { + const children = events.filter((child) => { + return eventIsDescendantOf(child, event, events); + }); + return { event, children }; + }); + setComments(comments); + }); + } else { + setComments(undefined); + } + }, [enabled, ndk]); + + const contextValue: CommentsContextType = { + comments, + }; + + return ( + + {children} + + ); +}; + +const eventIsDescendantOf = ( + child: NDKEvent, + ancestor: NDKEvent, + events: NDKEvent[] +): boolean => { + const { reply } = parseEventTags(child); + if (!reply) return false; + if (reply[1] === ancestor.id) return true; + const parent = events.find((event) => event.id === reply[1]); + if (!parent) return false; + return eventIsDescendantOf(parent, ancestor, events); +}; + +export const useComments = () => { + const context = useContext(CommentsContext); + if (context === undefined) { + throw new Error("useComments must be used within a CommentsProvider"); + } + return context; +}; From ced0183230dbe591eb6336f8390299ed0704c1bc Mon Sep 17 00:00:00 2001 From: Michael Hall Date: Thu, 31 Aug 2023 16:38:57 -0400 Subject: [PATCH 02/17] comment ui --- components/CommentsDrawer/CommentGroup.tsx | 113 +++++++++++++++++++++ components/CommentsDrawer/CommentsFeed.tsx | 8 +- components/NoteAction/NoteActionLike.tsx | 4 +- components/NoteAction/NoteActionMore.tsx | 19 +++- components/NoteActionZap/NoteActionZap.tsx | 10 +- components/NoteContent/NoteContent.tsx | 8 +- components/NoteHeader/NoteHeader.js | 12 ++- ndk/NDKCommentsProvider.tsx | 14 +-- 8 files changed, 158 insertions(+), 30 deletions(-) create mode 100644 components/CommentsDrawer/CommentGroup.tsx diff --git a/components/CommentsDrawer/CommentGroup.tsx b/components/CommentsDrawer/CommentGroup.tsx new file mode 100644 index 00000000..d808d7a0 --- /dev/null +++ b/components/CommentsDrawer/CommentGroup.tsx @@ -0,0 +1,113 @@ +import { Box, Button, DefaultProps, Group, Space } from "@mantine/core"; +import { NDKEvent } from "@nostr-dev-kit/ndk"; +import NoteActionLike from "components/NoteAction/NoteActionLike"; +import NoteActionMore from "components/NoteAction/NoteActionMore"; +import NoteActionZap from "components/NoteActionZap/NoteActionZap"; +import NoteContent from "components/NoteContent/NoteContent"; +import { + RelativeTime, + UserDetailsAvatar, + UserDetailsDisplayName, + UserDetailsName, +} from "components/NoteHeader/NoteHeader"; +import { Comment } from "ndk/NDKCommentsProvider"; +import { EventProvider, useEvent } from "ndk/NDKEventProvider"; +import { useState } from "react"; + +type CommentProps = { + comment: Comment; +}; + +export default function CommentGroup({ comment }: CommentProps) { + return ( + + + + + + + ); +} + +const CommentChildren = ({ events }: { events: NDKEvent[] }) => { + const [isShowingReplies, setIsShowingReplies] = useState(false); + + const handleShowRepliesClick = () => { + setIsShowingReplies(true); + }; + + if (!events.length) return null; + + return isShowingReplies ? ( + + {events.map((event) => ( + + + + ))} + + ) : ( + + ); +}; + +const CommentView = ({ isReply = false }: { isReply?: boolean }) => { + const { event } = useEvent(); + + return ( + + + + + + + + ); +}; + +const CommentHeader = ({ + isReply, + ...rest +}: DefaultProps & { isReply: boolean }) => { + return ( + + + + + + + + + + ); +}; + +const CommentContent = () => { + const { event } = useEvent(); + + return ; +}; + +const CommentActions = () => { + return ( + + + + + + ); +}; diff --git a/components/CommentsDrawer/CommentsFeed.tsx b/components/CommentsDrawer/CommentsFeed.tsx index 012fb46c..9d53ffb2 100644 --- a/components/CommentsDrawer/CommentsFeed.tsx +++ b/components/CommentsDrawer/CommentsFeed.tsx @@ -1,5 +1,6 @@ -import { Box, Center, Loader, Text } from "@mantine/core"; +import { Box, Center, Loader } from "@mantine/core"; import { useComments } from "ndk/NDKCommentsProvider"; +import CommentGroup from "./CommentGroup"; export default function CommentsFeed() { const { comments } = useComments(); @@ -8,13 +9,12 @@ export default function CommentsFeed() { h={375} sx={(theme) => ({ borderBottom: `1px solid ${theme.colors.gray[6]}`, + overflowY: "scroll", })} > {comments ? ( comments.map((comment) => ( - - {comment.event.content} ({comment.children.length}) - + )) ) : (
diff --git a/components/NoteAction/NoteActionLike.tsx b/components/NoteAction/NoteActionLike.tsx index ff4905da..e808efb7 100644 --- a/components/NoteAction/NoteActionLike.tsx +++ b/components/NoteAction/NoteActionLike.tsx @@ -13,7 +13,7 @@ import useAuth from "hooks/useAuth"; import { currentUserLikedNote, selectNoteState } from "../../store/Notes"; import { AppState } from "../../store/Store"; -const NoteActionLike = () => { +const NoteActionLike = ({ size = 18 }: { size?: number }) => { const dispatch = useDispatch(); const { ndk } = useNDK(); const { event } = useEvent(); @@ -79,7 +79,7 @@ const NoteActionLike = () => { noWrap > - + {" "} {reactionCount > 0 && {reactionCount}} diff --git a/components/NoteAction/NoteActionMore.tsx b/components/NoteAction/NoteActionMore.tsx index fdff77b4..b6640b83 100644 --- a/components/NoteAction/NoteActionMore.tsx +++ b/components/NoteAction/NoteActionMore.tsx @@ -13,12 +13,15 @@ import { MoreIcon, ShareIcon, } from "icons/StemstrIcon"; -import withStopClickPropagation from "utils/hoc/withStopClickPropagation"; import { useEvent } from "../../ndk/NDKEventProvider"; import { selectUserPreferencesState } from "store/UserPreferences"; import { useSelector } from "react-redux"; -const NoteActionMore = () => { +type NoteActionMoreProps = { + size: number; +}; + +const NoteActionMore = ({ size = 24 }: NoteActionMoreProps) => { const [opened, { open, close }] = useDisclosure(false); const { userPreferences } = useSelector(selectUserPreferencesState); @@ -79,8 +82,14 @@ const NoteActionMore = () => { Close -
({ cursor: "pointer" })}> - +
{ + e.stopPropagation(); + open(); + }} + sx={(theme) => ({ cursor: "pointer" })} + > +
); @@ -187,4 +196,4 @@ const NoteActionMoreShare = () => { ); }; -export default withStopClickPropagation(NoteActionMore); +export default NoteActionMore; diff --git a/components/NoteActionZap/NoteActionZap.tsx b/components/NoteActionZap/NoteActionZap.tsx index 3d41b864..b07a9edf 100644 --- a/components/NoteActionZap/NoteActionZap.tsx +++ b/components/NoteActionZap/NoteActionZap.tsx @@ -1,4 +1,4 @@ -import { Space, Text, Group } from "@mantine/core"; +import { Text, Group } from "@mantine/core"; import { ZapIcon } from "icons/StemstrIcon"; import NoteAction from "components/NoteAction/NoteAction"; import { useEvent } from "ndk/NDKEventProvider"; @@ -41,7 +41,7 @@ const ZapsAmountTotal = () => { ) : null; }; -const NoteActionContentWithZapWizard = () => { +const NoteActionContentWithZapWizard = ({ size = 18 }: { size?: number }) => { const { start } = useZapWizard(); const { event } = useEvent(); const { isZappedByCurrentUser } = useSelector((state: AppState) => @@ -60,7 +60,7 @@ const NoteActionContentWithZapWizard = () => { })} noWrap > - + @@ -68,7 +68,7 @@ const NoteActionContentWithZapWizard = () => { ); }; -const NoteActionZap = () => { +const NoteActionZap = ({ size = 18 }: { size?: number }) => { const { event } = useEvent(); const zapRecipient = useUser(event.pubkey); let isZappable: boolean | undefined = false; @@ -82,7 +82,7 @@ const NoteActionZap = () => { return isZappable && zapRecipient ? ( - + ) : null; }; diff --git a/components/NoteContent/NoteContent.tsx b/components/NoteContent/NoteContent.tsx index 93ea549c..0079d6a0 100644 --- a/components/NoteContent/NoteContent.tsx +++ b/components/NoteContent/NoteContent.tsx @@ -1,5 +1,5 @@ import { Fragment, type MouseEvent } from "react"; -import { Text, Anchor } from "@mantine/core"; +import { Text, Anchor, DefaultProps } from "@mantine/core"; import MentionLink from "./MentionLink"; import { NPUB_NOSTR_URI_REGEX } from "../../constants"; import useStyles from "./NoteContent.styles"; @@ -7,7 +7,9 @@ import useStyles from "./NoteContent.styles"; const HYPERLINK_REGEX = /(https?:\/\/[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|])/; -export const NoteContent = ({ content }: { content: string }) => { +type NoteContentProps = DefaultProps & { content: string }; + +export const NoteContent = ({ content, ...rest }: NoteContentProps) => { const { classes } = useStyles(); const formattingRegEx = new RegExp( `(?:${NPUB_NOSTR_URI_REGEX.source}|${HYPERLINK_REGEX.source})`, @@ -42,7 +44,7 @@ export const NoteContent = ({ content }: { content: string }) => { }); return ( - + {formattingRegEx.test(content) ? formattedContent : content} ); diff --git a/components/NoteHeader/NoteHeader.js b/components/NoteHeader/NoteHeader.js index 000ab822..b9c1824e 100644 --- a/components/NoteHeader/NoteHeader.js +++ b/components/NoteHeader/NoteHeader.js @@ -43,15 +43,17 @@ const UserDetailsNip05 = () => { ) : null; }; -const UserDetailsAvatar = () => { +export const UserDetailsAvatar = ({ size = 42 }) => { const { event } = useEvent(); const user = useUser(event.pubkey); const src = useProfilePicSrc(user); - return ; + return ( + + ); }; -const UserDetailsDisplayName = (props) => { +export const UserDetailsDisplayName = (props) => { const { event } = useEvent(); const user = useUser(event.pubkey); @@ -62,7 +64,7 @@ const UserDetailsDisplayName = (props) => { ); }; -const UserDetailsName = (props) => { +export const UserDetailsName = (props) => { const { event } = useEvent(); const user = useUser(event.pubkey); const willDisplay = user?.profile?.name && user?.profile?.displayName; @@ -79,7 +81,7 @@ const UserDetailsName = (props) => { ); }; -const RelativeTime = (props) => { +export const RelativeTime = (props) => { const { event } = useEvent(); return ( diff --git a/ndk/NDKCommentsProvider.tsx b/ndk/NDKCommentsProvider.tsx index 45da6814..8774ac2a 100644 --- a/ndk/NDKCommentsProvider.tsx +++ b/ndk/NDKCommentsProvider.tsx @@ -10,7 +10,7 @@ import { import { useNDK } from "./NDKProvider"; import { parseEventTags } from "./utils"; -type Comment = { +export type Comment = { event: NDKEvent; children: NDKEvent[]; }; @@ -44,16 +44,18 @@ export const CommentsProvider = ({ const events = Array.from(eventSet.values()).sort( (a, b) => b.created_at! - a.created_at! ); - console.log(events); let comments: Comment[] = events .filter((event) => { const { root, reply } = parseEventTags(event); - return root && !reply && root[1] === noteId; + if (reply && reply[1] === noteId) return true; + return !reply && root && root[1] === noteId; }) .map((event) => { - const children = events.filter((child) => { - return eventIsDescendantOf(child, event, events); - }); + const children = events + .filter((child) => { + return eventIsDescendantOf(child, event, events); + }) + .sort((a, b) => a.created_at! - b.created_at!); return { event, children }; }); setComments(comments); From f9043473d1b1b451e3767b96314c9ab772c28286 Mon Sep 17 00:00:00 2001 From: Michael Hall Date: Thu, 31 Aug 2023 17:06:35 -0400 Subject: [PATCH 03/17] fix like/zap colors --- components/CommentsDrawer/CommentGroup.tsx | 4 ++-- components/NoteAction/NoteActionLike.tsx | 12 +++++++----- components/NoteActionZap/NoteActionZap.tsx | 18 +++++++++--------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/components/CommentsDrawer/CommentGroup.tsx b/components/CommentsDrawer/CommentGroup.tsx index d808d7a0..706cbba7 100644 --- a/components/CommentsDrawer/CommentGroup.tsx +++ b/components/CommentsDrawer/CommentGroup.tsx @@ -106,8 +106,8 @@ const CommentActions = () => { - - + + ); }; diff --git a/components/NoteAction/NoteActionLike.tsx b/components/NoteAction/NoteActionLike.tsx index e808efb7..db62a4ab 100644 --- a/components/NoteAction/NoteActionLike.tsx +++ b/components/NoteAction/NoteActionLike.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Group, Text } from "@mantine/core"; +import { DefaultProps, Group, Text } from "@mantine/core"; import { motion, useAnimation } from "framer-motion"; import { useDispatch, useSelector } from "react-redux"; import { Kind } from "nostr-tools"; @@ -13,7 +13,11 @@ import useAuth from "hooks/useAuth"; import { currentUserLikedNote, selectNoteState } from "../../store/Notes"; import { AppState } from "../../store/Store"; -const NoteActionLike = ({ size = 18 }: { size?: number }) => { +type NoteActionLikeProps = DefaultProps & { + size?: number; +}; + +const NoteActionLike = ({ size = 18, c = "gray.1" }: NoteActionLikeProps) => { const dispatch = useDispatch(); const { ndk } = useNDK(); const { event } = useEvent(); @@ -70,11 +74,9 @@ const NoteActionLike = ({ size = 18 }: { size?: number }) => { ({ transition: "color 1s ease", - color: isLikedByCurrentUser - ? theme.colors.red[5] - : theme.colors.gray[1], })} noWrap > diff --git a/components/NoteActionZap/NoteActionZap.tsx b/components/NoteActionZap/NoteActionZap.tsx index b07a9edf..8b176f2e 100644 --- a/components/NoteActionZap/NoteActionZap.tsx +++ b/components/NoteActionZap/NoteActionZap.tsx @@ -1,4 +1,4 @@ -import { Text, Group } from "@mantine/core"; +import { Text, Group, DefaultProps } from "@mantine/core"; import { ZapIcon } from "icons/StemstrIcon"; import NoteAction from "components/NoteAction/NoteAction"; import { useEvent } from "ndk/NDKEventProvider"; @@ -41,7 +41,7 @@ const ZapsAmountTotal = () => { ) : null; }; -const NoteActionContentWithZapWizard = ({ size = 18 }: { size?: number }) => { +const NoteActionContentWithZapWizard = ({ size, c }: NoteActionZapProps) => { const { start } = useZapWizard(); const { event } = useEvent(); const { isZappedByCurrentUser } = useSelector((state: AppState) => @@ -53,11 +53,7 @@ const NoteActionContentWithZapWizard = ({ size = 18 }: { size?: number }) => { ({ - color: isZappedByCurrentUser - ? theme.colors.orange[5] - : theme.colors.gray[1], - })} + c={isZappedByCurrentUser ? "orange.5" : c} noWrap > @@ -68,7 +64,11 @@ const NoteActionContentWithZapWizard = ({ size = 18 }: { size?: number }) => { ); }; -const NoteActionZap = ({ size = 18 }: { size?: number }) => { +type NoteActionZapProps = DefaultProps & { + size?: number; +}; + +const NoteActionZap = ({ size = 18, c = "gray.1" }: NoteActionZapProps) => { const { event } = useEvent(); const zapRecipient = useUser(event.pubkey); let isZappable: boolean | undefined = false; @@ -82,7 +82,7 @@ const NoteActionZap = ({ size = 18 }: { size?: number }) => { return isZappable && zapRecipient ? ( - + ) : null; }; From 76bdfdf83eeaacb12d7addc5dd8035c6e0593422 Mon Sep 17 00:00:00 2001 From: Michael Hall Date: Fri, 1 Sep 2023 09:46:22 -0400 Subject: [PATCH 04/17] link comment header to profile --- components/CommentsDrawer/CommentGroup.tsx | 5 +++-- components/NoteHeader/NoteHeader.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/components/CommentsDrawer/CommentGroup.tsx b/components/CommentsDrawer/CommentGroup.tsx index 706cbba7..7e4847a7 100644 --- a/components/CommentsDrawer/CommentGroup.tsx +++ b/components/CommentsDrawer/CommentGroup.tsx @@ -6,6 +6,7 @@ import NoteActionZap from "components/NoteActionZap/NoteActionZap"; import NoteContent from "components/NoteContent/NoteContent"; import { RelativeTime, + UserDetailsAnchorWrapper, UserDetailsAvatar, UserDetailsDisplayName, UserDetailsName, @@ -83,12 +84,12 @@ const CommentHeader = ({ }: DefaultProps & { isReply: boolean }) => { return ( - + - + ); diff --git a/components/NoteHeader/NoteHeader.js b/components/NoteHeader/NoteHeader.js index b9c1824e..e0ca959f 100644 --- a/components/NoteHeader/NoteHeader.js +++ b/components/NoteHeader/NoteHeader.js @@ -12,7 +12,7 @@ import useNip05 from "ndk/hooks/useNip05"; import { Nip05Status } from "store/Nip05"; import useProfilePicSrc from "ndk/hooks/useProfilePicSrc"; -const UserDetailsAnchorWrapper = ({ children }) => { +export const UserDetailsAnchorWrapper = ({ children }) => { const { event } = useEvent(); return ( From a048fbb0b8bede7ac2cae1e8fc7ef122f9538710 Mon Sep 17 00:00:00 2001 From: Michael Hall Date: Fri, 1 Sep 2023 09:56:18 -0400 Subject: [PATCH 05/17] remove whitespace from empty note tags --- components/NoteTags/NoteTags.js | 43 ++++++++++++++++----------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/components/NoteTags/NoteTags.js b/components/NoteTags/NoteTags.js index 3b5204b0..4a7465c7 100644 --- a/components/NoteTags/NoteTags.js +++ b/components/NoteTags/NoteTags.js @@ -1,5 +1,4 @@ import { Chip, Group } from "@mantine/core"; -import withStopClickPropagation from "../../utils/hoc/withStopClickPropagation"; import { useEvent } from "../../ndk/NDKEventProvider"; import { useRouter } from "next/router"; import { Route } from "../../enums"; @@ -7,9 +6,11 @@ import { Route } from "../../enums"; const NoteTags = ({ classes, ...rest }) => { const { event } = useEvent(); const router = useRouter(); + const tags = event?.tags?.filter((tag) => tag[0] == "t"); - return ( + return tags.length ? ( e.stopPropagation()} position="left" spacing={12} sx={(theme) => ({ @@ -20,26 +21,24 @@ const NoteTags = ({ classes, ...rest }) => { })} {...rest} > - {event?.tags - ?.filter((tag) => tag[0] == "t") - .map((tag, index) => ( - - router.push({ - pathname: Route.Tag, - query: { tag: tag[1] }, - }) - } - > - #{tag[1]} - - ))} + {tags.map((tag, index) => ( + + router.push({ + pathname: Route.Tag, + query: { tag: tag[1] }, + }) + } + > + #{tag[1]} + + ))} - ); + ) : null; }; -export default withStopClickPropagation(NoteTags); +export default NoteTags; From 450aeafc64f05eb17b31d3f50098fc46a9b28fc5 Mon Sep 17 00:00:00 2001 From: Michael Hall Date: Fri, 1 Sep 2023 10:15:06 -0400 Subject: [PATCH 06/17] show likes and zaps in comments --- components/NoteActionRow/NoteActionRow.tsx | 118 -------------------- ndk/NDKEventProvider.tsx | 119 ++++++++++++++++++++- 2 files changed, 118 insertions(+), 119 deletions(-) diff --git a/components/NoteActionRow/NoteActionRow.tsx b/components/NoteActionRow/NoteActionRow.tsx index ec1e7cb7..ffa9a90a 100644 --- a/components/NoteActionRow/NoteActionRow.tsx +++ b/components/NoteActionRow/NoteActionRow.tsx @@ -1,128 +1,10 @@ import { Group } from "@mantine/core"; -import { useMemo, useEffect, useCallback, useState } from "react"; import NoteActionComment from "../NoteAction/NoteActionComment"; import NoteActionLike from "../NoteAction/NoteActionLike"; import NoteActionZap from "../NoteActionZap/NoteActionZap"; -import { useEvent } from "../../ndk/NDKEventProvider"; -import { Kind } from "nostr-tools"; -import { - NDKEvent, - NDKZapInvoice, - zapInvoiceFromEvent, -} from "@nostr-dev-kit/ndk"; -import { createRelaySet } from "../../ndk/utils"; -import { useNDK } from "../../ndk/NDKProvider"; -import { useDispatch, useSelector } from "react-redux"; -import { - setReactionCount, - setIsLikedByCurrentUser, - setCommentCount, - setZapsAmountTotal, - selectNoteState, - setIsZappedByCurrentUser, - setRepostCount, - setIsRepostedByCurrentUser, -} from "../../store/Notes"; -import { selectAuthState } from "../../store/Auth"; -import { DEFAULT_RELAY_URLS } from "../../constants"; -import { AppState } from "../../store/Store"; import NoteActionRepost from "components/NoteAction/NoteActionRepost"; const NoteActionRow = () => { - const dispatch = useDispatch(); - const auth = useSelector(selectAuthState); - const { ndk } = useNDK(); - const { event } = useEvent(); - const noteId = event.id; - const { zapsAmountTotal, reactionCount, commentCount } = useSelector( - (state: AppState) => selectNoteState(state, noteId) - ); - const filter = useMemo( - () => ({ - kinds: [Kind.Text, 1808 as Kind, Kind.Reaction, Kind.Zap, 6, 16], - "#e": [noteId], - }), - [noteId] - ); - const [hasFetchedMetaEvents, sethasFetchedMetaEvents] = useState(false); - const dispatchData = useCallback( - async (events: NDKEvent[]) => { - const reactions = events.filter(({ kind }) => kind === Kind.Reaction); - const reposts = events.filter( - ({ kind }) => kind && [6, 16].includes(kind) - ); - const newCommentCount = events.filter(({ kind, content }) => - [Kind.Text, 1808].includes(kind as Kind) - ).length; - const validZapInvoices = events - .map(zapInvoiceFromEvent) - .filter((zi) => zi !== null) as NDKZapInvoice[]; - const getZapsAmountTotal = () => { - return validZapInvoices.reduce((acc, { amount: amountInMillisats }) => { - const amount = amountInMillisats ? amountInMillisats / 1000 : 0; - - return acc + amount; - }, 0); - }; - const newZapsAmountTotal = getZapsAmountTotal(); - - // sometimes the subscription is not returning the right amounts after initial load. - // this makes sure it doesn't override the previously loaded values since the new values should not be less - // than the previous values. - if ( - commentCount > newCommentCount || - reactionCount > reactions.length || - zapsAmountTotal > newZapsAmountTotal - ) { - return; - } - - dispatch(setReactionCount({ id: noteId, value: reactions.length })); - dispatch( - setIsLikedByCurrentUser({ - id: noteId, - value: Boolean(reactions.find((ev) => ev.pubkey === auth.pk)), - }) - ); - dispatch(setRepostCount({ id: noteId, value: reposts.length })); - dispatch( - setIsRepostedByCurrentUser({ - id: noteId, - value: Boolean(reposts.find((ev) => ev.pubkey === auth.pk)), - }) - ); - dispatch(setCommentCount({ id: noteId, value: newCommentCount })); - dispatch(setZapsAmountTotal({ id: noteId, value: newZapsAmountTotal })); - dispatch( - setIsZappedByCurrentUser({ - id: noteId, - value: Boolean( - validZapInvoices.find(({ zappee }) => zappee === auth.pk) - ), - }) - ); - }, - [auth.pk, dispatch, noteId, commentCount, reactionCount, zapsAmountTotal] - ); - - useEffect(() => { - if (!ndk || hasFetchedMetaEvents) { - return; - } - sethasFetchedMetaEvents(true); - - const relaySet = createRelaySet( - [...DEFAULT_RELAY_URLS, process.env.NEXT_PUBLIC_STEMSTR_RELAY as string], - ndk - ); - - ndk - .fetchEvents(filter, {}, relaySet) - .then((events) => Array.from(events)) - .then(dispatchData) - .catch(console.error); - }, [ndk, filter, dispatchData, hasFetchedMetaEvents]); - return ( diff --git a/ndk/NDKEventProvider.tsx b/ndk/NDKEventProvider.tsx index 44ba3d26..a3debf98 100644 --- a/ndk/NDKEventProvider.tsx +++ b/ndk/NDKEventProvider.tsx @@ -3,10 +3,35 @@ import { type PropsWithChildren, useContext, useMemo, + useState, + useCallback, + useEffect, } from "react"; -import { NDKUser, NDKEvent, NostrEvent } from "@nostr-dev-kit/ndk"; +import { + NDKUser, + NDKEvent, + NostrEvent, + zapInvoiceFromEvent, + NDKZapInvoice, +} from "@nostr-dev-kit/ndk"; import { useUser } from "./hooks/useUser"; import { useNDK } from "./NDKProvider"; +import { useDispatch, useSelector } from "react-redux"; +import { selectAuthState } from "store/Auth"; +import { AppState } from "store/Store"; +import { + selectNoteState, + setCommentCount, + setIsLikedByCurrentUser, + setIsRepostedByCurrentUser, + setIsZappedByCurrentUser, + setReactionCount, + setRepostCount, + setZapsAmountTotal, +} from "store/Notes"; +import { Kind } from "nostr-tools"; +import { createRelaySet } from "./utils"; +import { DEFAULT_RELAY_URLS } from "../constants"; interface EventContextProps { event: NDKEvent; @@ -36,6 +61,98 @@ export const EventProvider = ({ return event; }, [event.kind]); + const dispatch = useDispatch(); + const auth = useSelector(selectAuthState); + const noteId = event.id; + const { zapsAmountTotal, reactionCount, commentCount } = useSelector( + (state: AppState) => selectNoteState(state, noteId) + ); + const filter = useMemo( + () => ({ + kinds: [Kind.Text, 1808 as Kind, Kind.Reaction, Kind.Zap, 6, 16], + "#e": [noteId], + }), + [noteId] + ); + const [hasFetchedMetaEvents, sethasFetchedMetaEvents] = useState(false); + const dispatchData = useCallback( + async (events: NDKEvent[]) => { + const reactions = events.filter(({ kind }) => kind === Kind.Reaction); + const reposts = events.filter( + ({ kind }) => kind && [6, 16].includes(kind) + ); + const newCommentCount = events.filter(({ kind, content }) => + [Kind.Text, 1808].includes(kind as Kind) + ).length; + const validZapInvoices = events + .map(zapInvoiceFromEvent) + .filter((zi) => zi !== null) as NDKZapInvoice[]; + const getZapsAmountTotal = () => { + return validZapInvoices.reduce((acc, { amount: amountInMillisats }) => { + const amount = amountInMillisats ? amountInMillisats / 1000 : 0; + + return acc + amount; + }, 0); + }; + const newZapsAmountTotal = getZapsAmountTotal(); + + // sometimes the subscription is not returning the right amounts after initial load. + // this makes sure it doesn't override the previously loaded values since the new values should not be less + // than the previous values. + if ( + commentCount > newCommentCount || + reactionCount > reactions.length || + zapsAmountTotal > newZapsAmountTotal + ) { + return; + } + + dispatch(setReactionCount({ id: noteId, value: reactions.length })); + dispatch( + setIsLikedByCurrentUser({ + id: noteId, + value: Boolean(reactions.find((ev) => ev.pubkey === auth.pk)), + }) + ); + dispatch(setRepostCount({ id: noteId, value: reposts.length })); + dispatch( + setIsRepostedByCurrentUser({ + id: noteId, + value: Boolean(reposts.find((ev) => ev.pubkey === auth.pk)), + }) + ); + dispatch(setCommentCount({ id: noteId, value: newCommentCount })); + dispatch(setZapsAmountTotal({ id: noteId, value: newZapsAmountTotal })); + dispatch( + setIsZappedByCurrentUser({ + id: noteId, + value: Boolean( + validZapInvoices.find(({ zappee }) => zappee === auth.pk) + ), + }) + ); + }, + [auth.pk, dispatch, noteId, commentCount, reactionCount, zapsAmountTotal] + ); + + useEffect(() => { + if (!ndk || hasFetchedMetaEvents) { + return; + } + sethasFetchedMetaEvents(true); + + const relaySet = createRelaySet( + [...DEFAULT_RELAY_URLS, process.env.NEXT_PUBLIC_STEMSTR_RELAY as string], + ndk + ); + + ndk + .fetchEvents(filter, {}, relaySet) + .then((events) => Array.from(events)) + .then(dispatchData) + .catch(console.error); + }, [ndk, filter, dispatchData, hasFetchedMetaEvents]); + return ( {children} From de4ca414a045b5af2f5e74d2ab4c3e3db875b2d2 Mon Sep 17 00:00:00 2001 From: Michael Hall Date: Fri, 1 Sep 2023 10:22:16 -0400 Subject: [PATCH 07/17] fix comment header overflow --- components/CommentsDrawer/CommentGroup.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/CommentsDrawer/CommentGroup.tsx b/components/CommentsDrawer/CommentGroup.tsx index 7e4847a7..a6ba1be2 100644 --- a/components/CommentsDrawer/CommentGroup.tsx +++ b/components/CommentsDrawer/CommentGroup.tsx @@ -83,11 +83,11 @@ const CommentHeader = ({ ...rest }: DefaultProps & { isReply: boolean }) => { return ( - + - + From d99338533b0273a34e354497b21c40577ddd28cd Mon Sep 17 00:00:00 2001 From: Michael Hall Date: Sat, 2 Sep 2023 13:38:40 -0400 Subject: [PATCH 08/17] add ability to comment --- components/CommentsDrawer/CommentForm.tsx | 89 ++++++++++++++++++++ components/CommentsDrawer/CommentGroup.tsx | 12 ++- components/CommentsDrawer/CommentsDrawer.tsx | 24 +----- ndk/NDKCommentsProvider.tsx | 81 +++++++++++++----- ndk/utils.tsx | 59 +++++++++++++ 5 files changed, 223 insertions(+), 42 deletions(-) create mode 100644 components/CommentsDrawer/CommentForm.tsx diff --git a/components/CommentsDrawer/CommentForm.tsx b/components/CommentsDrawer/CommentForm.tsx new file mode 100644 index 00000000..16f319a6 --- /dev/null +++ b/components/CommentsDrawer/CommentForm.tsx @@ -0,0 +1,89 @@ +import { Avatar, Button, Group, Textarea } from "@mantine/core"; +import { useForm } from "@mantine/form"; +import useAuth from "hooks/useAuth"; +import { useComments } from "ndk/NDKCommentsProvider"; +import { useNDK } from "ndk/NDKProvider"; +import useProfilePicSrc from "ndk/hooks/useProfilePicSrc"; +import { useUser } from "ndk/hooks/useUser"; +import { createKind1Event } from "ndk/utils"; +import { ChangeEventHandler } from "react"; + +type CommentFormValues = { + comment: string; +}; + +export default function CommentForm() { + const { ndk, stemstrRelaySet } = useNDK(); + const { replyingTo, rootEvent, setHighlightedEvent } = useComments(); + const isShowingName = replyingTo?.id !== rootEvent?.id; + const replyingToUser = useUser( + isShowingName ? replyingTo?.pubkey : undefined + ); + const { authState } = useAuth(); + const user = useUser(authState.pk); + const src = useProfilePicSrc(user); + const form = useForm({ + initialValues: { + comment: "", + }, + validate: {}, + }); + + const handleChange: ChangeEventHandler = (e) => { + form.setFieldValue("comment", e.target.value); + }; + + const handleSubmit = (values: CommentFormValues) => { + if (ndk && replyingTo) { + const event = createKind1Event(ndk, values.comment, { + replyingTo: replyingTo.rawEvent(), + }); + event.publish(stemstrRelaySet).then(() => { + setHighlightedEvent(event); + }); + } + }; + + return ( +
+ + ({ + border: `1px solid ${theme.colors.gray[4]}`, + })} + /> +