From 53f9f6240ffc0b68b2664978a04f8268d156acf3 Mon Sep 17 00:00:00 2001 From: Cody Tseng Date: Sat, 9 Nov 2024 22:28:10 +0800 Subject: [PATCH] feat: post & reply (#5) --- src/renderer/src/components/Content/index.tsx | 2 +- .../src/components/NoteStats/LikeButton.tsx | 2 +- .../NoteStats/NoteOptions/index.tsx | 20 ++- .../src/components/NoteStats/ReplyButton.tsx | 17 ++- .../src/components/NoteStats/RepostButton.tsx | 4 +- .../src/components/PostDialog/Metions.tsx | 47 +++++++ .../src/components/PostDialog/Preview.tsx | 21 +++ .../src/components/PostDialog/index.tsx | 131 ++++++++++++++++++ .../src/components/ReplyNote/index.tsx | 22 +-- .../src/components/ReplyNoteList/index.tsx | 2 +- .../src/components/Titlebar/index.tsx | 2 +- src/renderer/src/components/ui/button.tsx | 2 +- src/renderer/src/components/ui/dialog.tsx | 67 ++++----- .../src/components/ui/dropdown-menu.tsx | 4 +- src/renderer/src/components/ui/textarea.tsx | 23 +++ src/renderer/src/hooks/useFetchEvent.tsx | 37 +++-- .../PrimaryPageLayout/AccountButton.tsx | 5 +- .../layouts/PrimaryPageLayout/PostButton.tsx | 17 +++ .../src/layouts/PrimaryPageLayout/index.tsx | 6 +- .../src/layouts/SecondaryPageLayout/index.tsx | 4 +- src/renderer/src/lib/draft-event.ts | 34 ++++- src/renderer/src/lib/event.ts | 94 ++++++++++++- .../src/pages/secondary/ProfilePage/index.tsx | 6 +- .../src/providers/NoteStatsProvider.tsx | 8 +- src/renderer/src/services/client.service.ts | 4 +- 25 files changed, 483 insertions(+), 98 deletions(-) create mode 100644 src/renderer/src/components/PostDialog/Metions.tsx create mode 100644 src/renderer/src/components/PostDialog/Preview.tsx create mode 100644 src/renderer/src/components/PostDialog/index.tsx create mode 100644 src/renderer/src/components/ui/textarea.tsx create mode 100644 src/renderer/src/layouts/PrimaryPageLayout/PostButton.tsx diff --git a/src/renderer/src/components/Content/index.tsx b/src/renderer/src/components/Content/index.tsx index 5c423074..4dd566d5 100644 --- a/src/renderer/src/components/Content/index.tsx +++ b/src/renderer/src/components/Content/index.tsx @@ -97,7 +97,7 @@ function preprocess(content: string) { }) const embeddedNotes: string[] = [] - const embeddedNoteRegex = /(nostr:note1[a-z0-9]{58}|nostr:nevent1[a-z0-9]+)/g + const embeddedNoteRegex = /nostr:(note1[a-z0-9]{58}|nevent1[a-z0-9]+|naddr1[a-z0-9]+)/g ;(c.match(embeddedNoteRegex) || []).forEach((note) => { c = c.replace(note, '').trim() embeddedNotes.push(note) diff --git a/src/renderer/src/components/NoteStats/LikeButton.tsx b/src/renderer/src/components/NoteStats/LikeButton.tsx index d4ef0b56..23887dc7 100644 --- a/src/renderer/src/components/NoteStats/LikeButton.tsx +++ b/src/renderer/src/components/NoteStats/LikeButton.tsx @@ -53,7 +53,7 @@ export default function LikeButton({ const targetRelayList = await client.fetchRelayList(event.pubkey) const reaction = createReactionDraftEvent(event) - await publish(reaction, targetRelayList.read) + await publish(reaction, targetRelayList.read.slice(0, 3)) markNoteAsLiked(event.id) } catch (error) { console.error('like failed', error) diff --git a/src/renderer/src/components/NoteStats/NoteOptions/index.tsx b/src/renderer/src/components/NoteStats/NoteOptions/index.tsx index f502091a..4bc35972 100644 --- a/src/renderer/src/components/NoteStats/NoteOptions/index.tsx +++ b/src/renderer/src/components/NoteStats/NoteOptions/index.tsx @@ -4,7 +4,8 @@ import { DropdownMenuItem, DropdownMenuTrigger } from '@renderer/components/ui/dropdown-menu' -import { Ellipsis } from 'lucide-react' +import { getSharableEventId } from '@renderer/lib/event' +import { Code, Copy, Ellipsis } from 'lucide-react' import { Event } from 'nostr-tools' import { useState } from 'react' import RawEventDialog from './RawEventDialog' @@ -22,7 +23,22 @@ export default function NoteOptions({ event }: { event: Event }) { /> - setIsRawEventDialogOpen(true)}> + { + e.stopPropagation() + navigator.clipboard.writeText('nostr:' + getSharableEventId(event)) + }} + > + + copy embedded code + + { + e.stopPropagation() + setIsRawEventDialogOpen(true) + }} + > + raw event diff --git a/src/renderer/src/components/NoteStats/ReplyButton.tsx b/src/renderer/src/components/NoteStats/ReplyButton.tsx index d426fd02..0ca09942 100644 --- a/src/renderer/src/components/NoteStats/ReplyButton.tsx +++ b/src/renderer/src/components/NoteStats/ReplyButton.tsx @@ -1,17 +1,26 @@ +import { useNostr } from '@renderer/providers/NostrProvider' import { useNoteStats } from '@renderer/providers/NoteStatsProvider' import { MessageCircle } from 'lucide-react' import { Event } from 'nostr-tools' import { useMemo } from 'react' +import PostDialog from '../PostDialog' import { formatCount } from './utils' export default function ReplyButton({ event }: { event: Event }) { const { noteStatsMap } = useNoteStats() + const { pubkey } = useNostr() const { replyCount } = useMemo(() => noteStatsMap.get(event.id) ?? {}, [noteStatsMap, event.id]) return ( -
- -
{formatCount(replyCount)}
-
+ + + ) } diff --git a/src/renderer/src/components/NoteStats/RepostButton.tsx b/src/renderer/src/components/NoteStats/RepostButton.tsx index 42de65e3..bc66f7d8 100644 --- a/src/renderer/src/components/NoteStats/RepostButton.tsx +++ b/src/renderer/src/components/NoteStats/RepostButton.tsx @@ -63,7 +63,7 @@ export default function RepostButton({ const targetRelayList = await client.fetchRelayList(event.pubkey) const repost = createRepostDraftEvent(event) - await publish(repost, targetRelayList.read) + await publish(repost, targetRelayList.read.slice(0, 3)) markNoteAsReposted(event.id) } catch (error) { console.error('repost failed', error) @@ -97,7 +97,7 @@ export default function RepostButton({ - Cancel + e.stopPropagation()}>Cancel Repost diff --git a/src/renderer/src/components/PostDialog/Metions.tsx b/src/renderer/src/components/PostDialog/Metions.tsx new file mode 100644 index 00000000..2a358663 --- /dev/null +++ b/src/renderer/src/components/PostDialog/Metions.tsx @@ -0,0 +1,47 @@ +import { Button } from '@renderer/components/ui/button' +import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover' +import { extractMentions } from '@renderer/lib/event' +import { useEffect, useState } from 'react' +import UserAvatar from '../UserAvatar' +import Username from '../Username' +import { Event } from 'nostr-tools' + +export default function Mentions({ + content, + parentEvent +}: { + content: string + parentEvent?: Event +}) { + const [pubkeys, setPubkeys] = useState([]) + + useEffect(() => { + extractMentions(content, parentEvent).then(({ pubkeys }) => setPubkeys(pubkeys)) + }, [content]) + + return ( + + + + + +
+
Mentions:
+ {pubkeys.map((pubkey, index) => ( +
+ + +
+ ))} +
+
+
+ ) +} diff --git a/src/renderer/src/components/PostDialog/Preview.tsx b/src/renderer/src/components/PostDialog/Preview.tsx new file mode 100644 index 00000000..12244ee8 --- /dev/null +++ b/src/renderer/src/components/PostDialog/Preview.tsx @@ -0,0 +1,21 @@ +import { Card } from '@renderer/components/ui/card' +import dayjs from 'dayjs' +import Content from '../Content' + +export default function Preview({ content }: { content: string }) { + return ( + + + + ) +} diff --git a/src/renderer/src/components/PostDialog/index.tsx b/src/renderer/src/components/PostDialog/index.tsx new file mode 100644 index 00000000..32c90877 --- /dev/null +++ b/src/renderer/src/components/PostDialog/index.tsx @@ -0,0 +1,131 @@ +import { Button } from '@renderer/components/ui/button' +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger +} from '@renderer/components/ui/dialog' +import { ScrollArea } from '@renderer/components/ui/scroll-area' +import { Textarea } from '@renderer/components/ui/textarea' +import { useToast } from '@renderer/hooks/use-toast' +import { createShortTextNoteDraftEvent } from '@renderer/lib/draft-event' +import { useNostr } from '@renderer/providers/NostrProvider' +import client from '@renderer/services/client.service' +import { LoaderCircle } from 'lucide-react' +import { Event } from 'nostr-tools' +import { useState } from 'react' +import UserAvatar from '../UserAvatar' +import Mentions from './Metions' +import Preview from './Preview' + +export default function PostDialog({ + children, + parentEvent +}: { + children: React.ReactNode + parentEvent?: Event +}) { + const { toast } = useToast() + const { pubkey, publish } = useNostr() + const [open, setOpen] = useState(false) + const [content, setContent] = useState('') + const [posting, setPosting] = useState(false) + + const handleTextareaChange = (e: React.ChangeEvent) => { + setContent(e.target.value) + } + + const post = async (e: React.MouseEvent) => { + e.stopPropagation() + if (!content || !pubkey || posting) { + setOpen(false) + return + } + + setPosting(true) + try { + const additionalRelayUrls: string[] = [] + if (parentEvent) { + const relayList = await client.fetchRelayList(parentEvent.pubkey) + additionalRelayUrls.push(...relayList.read.slice(0, 5)) + } + const draftEvent = await createShortTextNoteDraftEvent(content, parentEvent) + await publish(draftEvent, additionalRelayUrls) + setContent('') + setOpen(false) + } catch (error) { + if (error instanceof AggregateError) { + error.errors.forEach((e) => + toast({ + variant: 'destructive', + title: 'Failed to post', + description: e.message + }) + ) + } else if (error instanceof Error) { + toast({ + variant: 'destructive', + title: 'Failed to post', + description: error.message + }) + } + console.error(error) + return + } finally { + setPosting(false) + } + toast({ + title: 'Post successful', + description: 'Your post has been published' + }) + } + + return ( + + {children} + + +
+ + + {parentEvent ? ( +
+
Reply to
+ +
{parentEvent.content}
+
+ ) : ( + 'New post' + )} +
+
+