diff --git a/.changeset/stale-eels-think.md b/.changeset/stale-eels-think.md new file mode 100644 index 000000000..665ab5d14 --- /dev/null +++ b/.changeset/stale-eels-think.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Add NIP-22 comments on articles diff --git a/src/components/embed-event/event-types/embedded-stemstr-track.tsx b/src/components/embed-event/event-types/embedded-stemstr-track.tsx index e70db7bfb..629c9ff9c 100644 --- a/src/components/embed-event/event-types/embedded-stemstr-track.tsx +++ b/src/components/embed-event/event-types/embedded-stemstr-track.tsx @@ -22,7 +22,7 @@ import TrackStemstrButton from "../../../views/tracks/components/track-stemstr-b import TrackDownloadButton from "../../../views/tracks/components/track-download-button"; import TrackPlayer from "../../../views/tracks/components/track-player"; import QuoteEventButton from "../../note/quote-event-button"; -import NoteZapButton from "../../note/note-zap-button"; +import EventZapButton from "../../zap/event-zap-button"; // example nevent1qqst32cnyhhs7jt578u7vp3y047dduuwjquztpvwqc43f3nvg8dh28gpzamhxue69uhhyetvv9ujuum5v4khxarj9eshquq4rxdxa export default function EmbeddedStemstrTrack({ track, ...props }: Omit & { track: NostrEvent }) { @@ -54,7 +54,7 @@ export default function EmbeddedStemstrTrack({ track, ...props }: Omit - + diff --git a/src/components/message/message-bubble.tsx b/src/components/message/message-bubble.tsx index 8c59f8d20..c5b43d8ea 100644 --- a/src/components/message/message-bubble.tsx +++ b/src/components/message/message-bubble.tsx @@ -9,7 +9,7 @@ import useEventReactions from "../../hooks/use-event-reactions"; import EventReactionButtons from "../event-reactions/event-reactions"; import { IconThreadButton } from "./thread-button"; import AddReactionButton from "../note/timeline-note/components/add-reaction-button"; -import NoteZapButton from "../note/note-zap-button"; +import EventZapButton from "../zap/event-zap-button"; import useEventIntersectionRef from "../../hooks/use-event-intersection-ref"; export type MessageBubbleProps = { @@ -36,7 +36,7 @@ export default function MessageBubble({ const actions = ( <> - + {showThreadButton && } diff --git a/src/components/note/timeline-note/index.tsx b/src/components/note/timeline-note/index.tsx index 142f69582..00c09fbdf 100644 --- a/src/components/note/timeline-note/index.tsx +++ b/src/components/note/timeline-note/index.tsx @@ -20,7 +20,7 @@ import { useObservable } from "applesauce-react/hooks"; import NoteMenu from "../note-menu"; import UserLink from "../../user/user-link"; -import NoteZapButton from "../note-zap-button"; +import EventZapButton from "../../zap/event-zap-button"; import { ExpandProvider } from "../../../providers/local/expanded"; import EventVerificationIcon from "../../common-event/event-verification-icon"; import ShareButton from "./components/share-button"; @@ -132,7 +132,7 @@ export function TimelineNote({ )} - + {!showReactionsOnNewLine && reactionButtons} diff --git a/src/components/note/note-zap-button.tsx b/src/components/zap/event-zap-button.tsx similarity index 95% rename from src/components/note/note-zap-button.tsx rename to src/components/zap/event-zap-button.tsx index 6381077cf..7c866977e 100644 --- a/src/components/note/note-zap-button.tsx +++ b/src/components/zap/event-zap-button.tsx @@ -19,7 +19,7 @@ export type NoteZapButtonProps = Omit & { showEventPreview?: boolean; }; -export default function NoteZapButton({ event, allowComment, showEventPreview, ...props }: NoteZapButtonProps) { +export default function EventZapButton({ event, allowComment, showEventPreview, ...props }: NoteZapButtonProps) { const account = useCurrentAccount(); const { metadata } = useUserLNURLMetadata(event.pubkey); const zaps = useEventZaps(getEventUID(event)) ?? []; diff --git a/src/views/articles/article.tsx b/src/views/articles/article.tsx index 8cb1d5c9f..3c78b05cf 100644 --- a/src/views/articles/article.tsx +++ b/src/views/articles/article.tsx @@ -1,5 +1,5 @@ import { NostrEvent } from "nostr-tools"; -import { Box, Flex, Heading, Image, Spinner, Text } from "@chakra-ui/react"; +import { Box, Button, Flex, Heading, Image, Spinner, Text, useDisclosure } from "@chakra-ui/react"; import dayjs from "dayjs"; import useParamsAddressPointer from "../../hooks/use-params-address-pointer"; @@ -18,10 +18,13 @@ import MarkdownContent from "../../components/markdown/markdown"; import ArticleMenu from "./components/article-menu"; import ArticleTags from "./components/article-tags"; import NoteReactions from "../../components/note/timeline-note/components/note-reactions"; -import NoteZapButton from "../../components/note/note-zap-button"; +import EventZapButton from "../../components/zap/event-zap-button"; import ZapBubbles from "../../components/note/timeline-note/components/zap-bubbles"; import BookmarkEventButton from "../../components/note/bookmark-event"; import QuoteEventButton from "../../components/note/quote-event-button"; +import { ArticleComments } from "./components/article-comments"; +import ArticleCommentForm from "./components/article-comment-form"; +import { ThreadIcon } from "../../components/icons"; function ArticlePage({ article }: { article: NostrEvent }) { const image = getArticleImage(article); @@ -29,8 +32,10 @@ function ArticlePage({ article }: { article: NostrEvent }) { const published = getArticlePublishDate(article); const summary = getArticleSummary(article); + const comment = useDisclosure(); + return ( - + {title} @@ -46,10 +51,10 @@ function ArticlePage({ article }: { article: NostrEvent }) { {image && } - + - + @@ -57,11 +62,22 @@ function ArticlePage({ article }: { article: NostrEvent }) { - + + + {comment.isOpen ? ( + + ) : ( + + )} + + + ); } diff --git a/src/views/articles/components/article-comment-form.tsx b/src/views/articles/components/article-comment-form.tsx new file mode 100644 index 000000000..7a4ec12a7 --- /dev/null +++ b/src/views/articles/components/article-comment-form.tsx @@ -0,0 +1,100 @@ +import { useRef } from "react"; +import { NostrEvent } from "nostr-tools"; +import { useEventFactory } from "applesauce-react/hooks"; +import { getEventUID } from "applesauce-core/helpers"; +import { useForm } from "react-hook-form"; +import { useAsync, useThrottle } from "react-use"; + +import { usePublishEvent } from "../../../providers/global/publish-provider"; +import { useContextEmojis } from "../../../providers/global/emoji-provider"; +import useTextAreaUploadFile, { useTextAreaInsertTextWithForm } from "../../../hooks/use-textarea-upload-file"; +import MagicTextArea, { RefType } from "../../../components/magic-textarea"; +import useCacheForm from "../../../hooks/use-cache-form"; +import { Box, Button, ButtonGroup, Flex } from "@chakra-ui/react"; +import InsertImageButton from "../../new/note/insert-image-button"; +import InsertGifButton from "../../../components/gif/insert-gif-button"; +import { TrustProvider } from "../../../providers/local/trust-provider"; +import TextNoteContents from "../../../components/note/timeline-note/text-note-contents"; + +export default function ArticleCommentForm({ + event, + onSubmitted, + onCancel, +}: { + event: NostrEvent; + onSubmitted?: (comment: NostrEvent) => void; + onCancel?: () => void; +}) { + const publish = usePublishEvent(); + const factory = useEventFactory(); + const emojis = useContextEmojis(); + + const { setValue, getValues, watch, handleSubmit, formState, reset } = useForm({ + defaultValues: { + content: "", + }, + mode: "all", + }); + + const clearCache = useCacheForm<{ content: string }>(`comment-${getEventUID(event)}`, getValues, reset, formState); + + watch("content"); + + const textAreaRef = useRef(null); + const insertText = useTextAreaInsertTextWithForm(textAreaRef, getValues, setValue); + const { onPaste } = useTextAreaUploadFile(insertText); + + const submit = handleSubmit(async (values) => { + const draft = await factory.comment(event, values.content, { emojis }); + + const pub = await publish("Comment", draft); + + if (pub && onSubmitted) onSubmitted(pub.event); + clearCache(); + }); + + const formRef = useRef(null); + + // throttle preview + const throttleValues = useThrottle(getValues(), 500); + const { value: preview } = useAsync( + () => factory.comment(event, throttleValues.content, { emojis }), + [throttleValues, emojis], + ); + + return ( + + setValue("content", e.target.value, { shouldDirty: true })} + instanceRef={(inst) => (textAreaRef.current = inst)} + onPaste={onPaste} + onKeyDown={(e) => { + if ((e.ctrlKey || e.metaKey) && e.key === "Enter" && formRef.current) formRef.current.requestSubmit(); + }} + /> + + + + + {onCancel && } + + + + {preview && preview.content.length > 0 && ( + + + + + + )} + + ); +} diff --git a/src/views/articles/components/article-comments.tsx b/src/views/articles/components/article-comments.tsx new file mode 100644 index 000000000..bfa390fab --- /dev/null +++ b/src/views/articles/components/article-comments.tsx @@ -0,0 +1,110 @@ +import { + Box, + Button, + ButtonGroup, + Card, + CardBody, + CardFooter, + CardHeader, + Flex, + IconButton, + useDisclosure, +} from "@chakra-ui/react"; +import { NostrEvent } from "nostr-tools"; +import { COMMENT_KIND, getEventUID } from "applesauce-core/helpers"; +import { useStoreQuery } from "applesauce-react/hooks"; +import { CommentsQuery, RepliesQuery } from "applesauce-core/queries"; + +import Timestamp from "../../../components/timestamp"; +import DebugEventButton from "../../../components/debug-modal/debug-event-button"; +import UserLink from "../../../components/user/user-link"; +import TextNoteContents from "../../../components/note/timeline-note/text-note-contents"; +import { useReadRelays } from "../../../hooks/use-client-relays"; +import useTimelineLoader from "../../../hooks/use-timeline-loader"; +import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback"; +import IntersectionObserverProvider from "../../../providers/local/intersection-observer"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import UserDnsIdentity from "../../../components/user/user-dns-identity"; +import ArticleCommentForm from "./article-comment-form"; +import NoteReactions from "../../../components/note/timeline-note/components/note-reactions"; +import { ChevronDownIcon, ChevronUpIcon, ReplyIcon } from "../../../components/icons"; +import EventZapButton from "../../../components/zap/event-zap-button"; + +function Comment({ comment }: { comment: NostrEvent }) { + const reply = useDisclosure(); + const replies = useStoreQuery(RepliesQuery, [comment]); + const expand = useDisclosure({ defaultIsOpen: true }); + const all = useDisclosure(); + + return ( + <> + + + + + + +
+ +
+ + + + + +
+ + + + + {!reply.isOpen && ( + + )} + + {replies && replies.length > 0 && ( + : } + aria-label="Expand" + size="sm" + variant="ghost" + onClick={expand.onToggle} + /> + )} + +
+ {reply.isOpen && } + {replies && replies.length > 2 && expand.isOpen && !all.isOpen && ( + + )} + {replies && replies.length > 0 && expand.isOpen && ( + + {(replies.length > 2 && !all.isOpen ? replies.slice(0, 2) : replies).map((reply) => ( + + ))} + + )} + + ); +} + +export function ArticleComments({ article }: { article: NostrEvent }) { + const readRelays = useReadRelays(); + const { loader } = useTimelineLoader(`${getEventUID(article)}-comments`, readRelays, { + kinds: [COMMENT_KIND], + "#A": [getEventUID(article)], + }); + + const comments = useStoreQuery(CommentsQuery, [article]); + const callback = useTimelineCurserIntersectionCallback(loader); + + return ( + + {comments?.map((comment) => )} + + ); +} diff --git a/src/views/discovery/dvm-feed/components/feed-status.tsx b/src/views/discovery/dvm-feed/components/feed-status.tsx index fa01a8e14..01c0c4983 100644 --- a/src/views/discovery/dvm-feed/components/feed-status.tsx +++ b/src/views/discovery/dvm-feed/components/feed-status.tsx @@ -31,7 +31,7 @@ import UserAvatar from "../../../../components/user/user-avatar"; import UserLink from "../../../../components/user/user-link"; import UserDnsIdentity from "../../../../components/user/user-dns-identity"; import DebugEventButton from "../../../../components/debug-modal/debug-event-button"; -import NoteZapButton from "../../../../components/note/note-zap-button"; +import EventZapButton from "../../../../components/zap/event-zap-button"; function NextPageButton({ chain, pointer }: { pointer: AddressPointer; chain: ChainedDVMJob[] }) { const publish = usePublishEvent(); diff --git a/src/views/emoji-packs/components/emoji-pack-card.tsx b/src/views/emoji-packs/components/emoji-pack-card.tsx index bb46aeac4..a9fcadb07 100644 --- a/src/views/emoji-packs/components/emoji-pack-card.tsx +++ b/src/views/emoji-packs/components/emoji-pack-card.tsx @@ -18,7 +18,7 @@ import UserLink from "../../../components/user/user-link"; import { NostrEvent } from "../../../types/nostr-event"; import EmojiPackFavoriteButton from "./emoji-pack-favorite-button"; import EmojiPackMenu from "./emoji-pack-menu"; -import NoteZapButton from "../../../components/note/note-zap-button"; +import EventZapButton from "../../../components/zap/event-zap-button"; import HoverLinkOverlay from "../../../components/hover-link-overlay"; import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref"; import useShareableEventAddress from "../../../hooks/use-shareable-event-address"; @@ -53,7 +53,7 @@ export default function EmojiPackCard({ pack, ...props }: Omit - + diff --git a/src/views/emoji-packs/emoji-pack.tsx b/src/views/emoji-packs/emoji-pack.tsx index 11e38fc1b..adf464c3e 100644 --- a/src/views/emoji-packs/emoji-pack.tsx +++ b/src/views/emoji-packs/emoji-pack.tsx @@ -34,7 +34,7 @@ import UserAvatarLink from "../../components/user/user-avatar-link"; import Timestamp from "../../components/timestamp"; import useParamsAddressPointer from "../../hooks/use-params-address-pointer"; import { usePublishEvent } from "../../providers/global/publish-provider"; -import NoteZapButton from "../../components/note/note-zap-button"; +import EventZapButton from "../../components/zap/event-zap-button"; import QuoteEventButton from "../../components/note/quote-event-button"; function AddEmojiForm({ onAdd }: { onAdd: (values: { name: string; url: string }) => void }) { @@ -175,7 +175,7 @@ function EmojiPackPage({ pack }: { pack: NostrEvent }) { - + diff --git a/src/views/files/index.tsx b/src/views/files/index.tsx index 1e0e1282f..15e435122 100644 --- a/src/views/files/index.tsx +++ b/src/views/files/index.tsx @@ -16,7 +16,7 @@ import MimeTypePicker from "./mime-type-picker"; import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status"; import VerticalPageLayout from "../../components/vertical-page-layout"; import Timestamp from "../../components/timestamp"; -import NoteZapButton from "../../components/note/note-zap-button"; +import EventZapButton from "../../components/zap/event-zap-button"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import { useReadRelays } from "../../hooks/use-client-relays"; @@ -65,7 +65,7 @@ function ImageFile({ event }: { event: NostrEvent }) { - + ); diff --git a/src/views/lists/components/list-card.tsx b/src/views/lists/components/list-card.tsx index 2cfb62d6f..1829aac72 100644 --- a/src/views/lists/components/list-card.tsx +++ b/src/views/lists/components/list-card.tsx @@ -32,7 +32,7 @@ import ListMenu from "./list-menu"; import { NotesIcon } from "../../../components/icons"; import User01 from "../../../components/icons/user-01"; import HoverLinkOverlay from "../../../components/hover-link-overlay"; -import NoteZapButton from "../../../components/note/note-zap-button"; +import EventZapButton from "../../../components/zap/event-zap-button"; import Link01 from "../../../components/icons/link-01"; import File02 from "../../../components/icons/file-02"; import SimpleLikeButton from "../../../components/event-reactions/simple-like-button"; @@ -113,7 +113,7 @@ function ListCardRender({ - {!isSpecialList && } + {!isSpecialList && } {!isSpecialList && } diff --git a/src/views/streams/stream/stream-chat/chat-message.tsx b/src/views/streams/stream/stream-chat/chat-message.tsx index 7ce51c3da..c36596972 100644 --- a/src/views/streams/stream/stream-chat/chat-message.tsx +++ b/src/views/streams/stream/stream-chat/chat-message.tsx @@ -6,7 +6,7 @@ import UserAvatar from "../../../../components/user/user-avatar"; import UserLink from "../../../../components/user/user-link"; import { TrustProvider } from "../../../../providers/local/trust-provider"; import ChatMessageContent from "./chat-message-content"; -import NoteZapButton from "../../../../components/note/note-zap-button"; +import EventZapButton from "../../../../components/zap/event-zap-button"; import useEventIntersectionRef from "../../../../hooks/use-event-intersection-ref"; import { getStreamHost } from "../../../../helpers/nostr/stream"; @@ -23,7 +23,7 @@ function ChatMessage({ event, stream }: { event: NostrEvent; stream: NostrEvent {": "} - } /> - + {!showReactionsOnNewLine && reactionButtons} diff --git a/src/views/torrents/components/torrent-table-row.tsx b/src/views/torrents/components/torrent-table-row.tsx index 6a363991d..70a564e8a 100644 --- a/src/views/torrents/components/torrent-table-row.tsx +++ b/src/views/torrents/components/torrent-table-row.tsx @@ -9,7 +9,7 @@ import UserLink from "../../../components/user/user-link"; import Magnet from "../../../components/icons/magnet"; import { formatBytes } from "../../../helpers/number"; import TorrentMenu from "./torrent-menu"; -import NoteZapButton from "../../../components/note/note-zap-button"; +import EventZapButton from "../../../components/zap/event-zap-button"; import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref"; import useShareableEventAddress from "../../../hooks/use-shareable-event-address"; @@ -70,7 +70,7 @@ function TorrentTableRow({ torrent }: { torrent: NostrEvent }) { - + } aria-label="Magnet URI" isExternal href={magnetLink} /> diff --git a/src/views/torrents/components/torrents-comments.tsx b/src/views/torrents/components/torrents-comments.tsx index a1c9cf0ee..c49c56c9e 100644 --- a/src/views/torrents/components/torrents-comments.tsx +++ b/src/views/torrents/components/torrents-comments.tsx @@ -34,7 +34,7 @@ import ReplyForm from "../../thread/components/reply-form"; import useThreadColorLevelProps from "../../../hooks/use-thread-color-level-props"; import TorrentCommentMenu from "./torrent-comment-menu"; import NoteReactions from "../../../components/note/timeline-note/components/note-reactions"; -import NoteZapButton from "../../../components/note/note-zap-button"; +import EventZapButton from "../../../components/zap/event-zap-button"; import { TextNoteContents } from "../../../components/note/timeline-note/text-note-contents"; import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref"; @@ -104,7 +104,7 @@ export const ThreadPost = memo(({ post, level = -1 }: { post: ThreadItem; level? } /> - + {!showReactionsOnNewLine && reactionButtons} diff --git a/src/views/torrents/torrent.tsx b/src/views/torrents/torrent.tsx index 26e08b8d0..4a2330d79 100644 --- a/src/views/torrents/torrent.tsx +++ b/src/views/torrents/torrent.tsx @@ -41,7 +41,7 @@ import ReplyForm from "../thread/components/reply-form"; import { getThreadReferences } from "../../helpers/nostr/event"; import MessageTextCircle01 from "../../components/icons/message-text-circle-01"; import useParamsEventPointer from "../../hooks/use-params-event-pointer"; -import NoteZapButton from "../../components/note/note-zap-button"; +import EventZapButton from "../../components/zap/event-zap-button"; import QuoteEventButton from "../../components/note/quote-event-button"; import { TextNoteContents } from "../../components/note/timeline-note/text-note-contents"; @@ -74,7 +74,7 @@ function TorrentDetailsPage({ torrent }: { torrent: NostrEvent }) { ))} - + - + diff --git a/src/views/videos/video.tsx b/src/views/videos/video.tsx index da4afeacb..e366a9905 100644 --- a/src/views/videos/video.tsx +++ b/src/views/videos/video.tsx @@ -26,7 +26,7 @@ import VideoCard from "./components/video-card"; import UserName from "../../components/user/user-name"; import { useBreakpointValue } from "../../providers/global/breakpoint-provider"; import SimpleBookmarkButton from "../../components/simple-bookmark-button"; -import NoteZapButton from "../../components/note/note-zap-button"; +import EventZapButton from "../../components/zap/event-zap-button"; import QuoteEventButton from "../../components/note/quote-event-button"; function VideoRecommendations({ video }: { video: NostrEvent }) { @@ -56,7 +56,7 @@ function VideoDetailsPage({ video }: { video: NostrEvent }) { {title} - + diff --git a/src/views/wiki/page.tsx b/src/views/wiki/page.tsx index 02ebf8d1e..11de48ac9 100644 --- a/src/views/wiki/page.tsx +++ b/src/views/wiki/page.tsx @@ -29,7 +29,7 @@ import { WIKI_RELAYS } from "../../const"; import GitBranch01 from "../../components/icons/git-branch-01"; import { ExternalLinkIcon } from "../../components/icons"; import FileSearch01 from "../../components/icons/file-search-01"; -import NoteZapButton from "../../components/note/note-zap-button"; +import EventZapButton from "../../components/zap/event-zap-button"; import ZapBubbles from "../../components/note/timeline-note/components/zap-bubbles"; import QuoteEventButton from "../../components/note/quote-event-button"; import WikiPageMenu from "./components/wiki-page-menu"; @@ -120,7 +120,7 @@ export function WikiPagePage({ page }: { page: NostrEvent }) { - +