feat: quote

This commit is contained in:
codytseng
2024-11-18 23:14:08 +08:00
parent 26439f9d6b
commit 2a36b1bcf8
6 changed files with 113 additions and 65 deletions

View File

@@ -2,7 +2,7 @@ import { useNostr } from '@renderer/providers/NostrProvider'
import { useNoteStats } from '@renderer/providers/NoteStatsProvider' import { useNoteStats } from '@renderer/providers/NoteStatsProvider'
import { MessageCircle } from 'lucide-react' import { MessageCircle } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo, useState } from 'react'
import PostDialog from '../PostDialog' import PostDialog from '../PostDialog'
import { formatCount } from './utils' import { formatCount } from './utils'
@@ -10,17 +10,22 @@ export default function ReplyButton({ event }: { event: Event }) {
const { noteStatsMap } = useNoteStats() const { noteStatsMap } = useNoteStats()
const { pubkey } = useNostr() const { pubkey } = useNostr()
const { replyCount } = useMemo(() => noteStatsMap.get(event.id) ?? {}, [noteStatsMap, event.id]) const { replyCount } = useMemo(() => noteStatsMap.get(event.id) ?? {}, [noteStatsMap, event.id])
const [open, setOpen] = useState(false)
return ( return (
<PostDialog parentEvent={event}> <>
<button <button
className="flex gap-1 items-center text-muted-foreground enabled:hover:text-blue-400" className="flex gap-1 items-center text-muted-foreground enabled:hover:text-blue-400"
disabled={!pubkey} disabled={!pubkey}
onClick={(e) => e.stopPropagation()} onClick={(e) => {
e.stopPropagation()
setOpen(true)
}}
> >
<MessageCircle size={16} /> <MessageCircle size={16} />
<div className="text-sm">{formatCount(replyCount)}</div> <div className="text-sm">{formatCount(replyCount)}</div>
</button> </button>
</PostDialog> <PostDialog parentEvent={event} open={open} setOpen={setOpen} />
</>
) )
} }

View File

@@ -1,22 +1,19 @@
import { import {
AlertDialog, DropdownMenu,
AlertDialogAction, DropdownMenuContent,
AlertDialogCancel, DropdownMenuItem,
AlertDialogContent, DropdownMenuTrigger
AlertDialogDescription, } from '@renderer/components/ui/dropdown-menu'
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger
} from '@renderer/components/ui/alert-dialog'
import { createRepostDraftEvent } from '@renderer/lib/draft-event' import { createRepostDraftEvent } from '@renderer/lib/draft-event'
import { getSharableEventId } from '@renderer/lib/event'
import { cn } from '@renderer/lib/utils' import { cn } from '@renderer/lib/utils'
import { useNostr } from '@renderer/providers/NostrProvider' import { useNostr } from '@renderer/providers/NostrProvider'
import { useNoteStats } from '@renderer/providers/NoteStatsProvider' import { useNoteStats } from '@renderer/providers/NoteStatsProvider'
import client from '@renderer/services/client.service' import client from '@renderer/services/client.service'
import { Loader, Repeat } from 'lucide-react' import { Loader, PencilLine, Repeat } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import PostDialog from '../PostDialog'
import { formatCount } from './utils' import { formatCount } from './utils'
export default function RepostButton({ export default function RepostButton({
@@ -30,6 +27,7 @@ export default function RepostButton({
const { noteStatsMap, fetchNoteRepostCount, fetchNoteRepostedStatus, markNoteAsReposted } = const { noteStatsMap, fetchNoteRepostCount, fetchNoteRepostedStatus, markNoteAsReposted } =
useNoteStats() useNoteStats()
const [reposting, setReposting] = useState(false) const [reposting, setReposting] = useState(false)
const [isPostDialogOpen, setIsPostDialogOpen] = useState(false)
const { repostCount, hasReposted } = useMemo( const { repostCount, hasReposted } = useMemo(
() => noteStatsMap.get(event.id) ?? {}, () => noteStatsMap.get(event.id) ?? {},
[noteStatsMap, event.id] [noteStatsMap, event.id]
@@ -64,7 +62,7 @@ export default function RepostButton({
const targetRelayList = await client.fetchRelayList(event.pubkey) const targetRelayList = await client.fetchRelayList(event.pubkey)
const repost = createRepostDraftEvent(event) const repost = createRepostDraftEvent(event)
await publish(repost, targetRelayList.read.slice(0, 3)) await publish(repost, targetRelayList.read.slice(0, 5))
markNoteAsReposted(event.id) markNoteAsReposted(event.id)
} catch (error) { } catch (error) {
console.error('repost failed', error) console.error('repost failed', error)
@@ -76,8 +74,9 @@ export default function RepostButton({
} }
return ( return (
<AlertDialog> <>
<AlertDialogTrigger asChild> <DropdownMenu>
<DropdownMenuTrigger asChild>
<button <button
className={cn( className={cn(
'flex gap-1 items-center enabled:hover:text-lime-500', 'flex gap-1 items-center enabled:hover:text-lime-500',
@@ -90,19 +89,31 @@ export default function RepostButton({
{reposting ? <Loader className="animate-spin" size={16} /> : <Repeat size={16} />} {reposting ? <Loader className="animate-spin" size={16} /> : <Repeat size={16} />}
<div className="text-sm">{formatCount(repostCount)}</div> <div className="text-sm">{formatCount(repostCount)}</div>
</button> </button>
</AlertDialogTrigger> </DropdownMenuTrigger>
<AlertDialogContent> <DropdownMenuContent
<AlertDialogHeader> onClick={(e) => {
<AlertDialogTitle>Repost Note</AlertDialogTitle> e.stopPropagation()
<AlertDialogDescription> e.preventDefault()
Are you sure you want to repost this note? }}
</AlertDialogDescription> >
</AlertDialogHeader> <DropdownMenuItem onClick={repost}>
<AlertDialogFooter> <Repeat /> Repost
<AlertDialogCancel onClick={(e) => e.stopPropagation()}>Cancel</AlertDialogCancel> </DropdownMenuItem>
<AlertDialogAction onClick={repost}>Repost</AlertDialogAction> <DropdownMenuItem
</AlertDialogFooter> onClick={(e) => {
</AlertDialogContent> e.stopPropagation()
</AlertDialog> setIsPostDialogOpen(true)
}}
>
<PencilLine /> Quote
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<PostDialog
open={isPostDialogOpen}
setOpen={setIsPostDialogOpen}
defaultContent={'\nnostr:' + getSharableEventId(event)}
/>
</>
) )
} }

View File

@@ -1,14 +1,26 @@
import PostDialog from '@renderer/components/PostDialog' import PostDialog from '@renderer/components/PostDialog'
import { Button } from '@renderer/components/ui/button' import { Button } from '@renderer/components/ui/button'
import { PencilLine } from 'lucide-react' import { PencilLine } from 'lucide-react'
import { useState } from 'react'
export default function PostButton({ variant = 'titlebar' }: { variant?: 'titlebar' | 'sidebar' }) { export default function PostButton({ variant = 'titlebar' }: { variant?: 'titlebar' | 'sidebar' }) {
const [open, setOpen] = useState(false)
return ( return (
<PostDialog> <>
<Button variant={variant} size={variant} title="new post"> <Button
variant={variant}
size={variant}
title="new post"
onClick={(e) => {
e.stopPropagation()
setOpen(true)
}}
>
<PencilLine /> <PencilLine />
{variant === 'sidebar' && <div>Post</div>} {variant === 'sidebar' && <div>Post</div>}
</Button> </Button>
</PostDialog> <PostDialog open={open} setOpen={setOpen} />
</>
) )
} }

View File

@@ -4,8 +4,7 @@ import {
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle
DialogTrigger
} from '@renderer/components/ui/dialog' } from '@renderer/components/ui/dialog'
import { ScrollArea } from '@renderer/components/ui/scroll-area' import { ScrollArea } from '@renderer/components/ui/scroll-area'
import { Textarea } from '@renderer/components/ui/textarea' import { Textarea } from '@renderer/components/ui/textarea'
@@ -15,23 +14,26 @@ import { useNostr } from '@renderer/providers/NostrProvider'
import client from '@renderer/services/client.service' import client from '@renderer/services/client.service'
import { LoaderCircle } from 'lucide-react' import { LoaderCircle } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useState } from 'react' import { Dispatch, useState } from 'react'
import UserAvatar from '../UserAvatar' import UserAvatar from '../UserAvatar'
import Mentions from './Metions' import Mentions from './Metions'
import Preview from './Preview' import Preview from './Preview'
import Uploader from './Uploader' import Uploader from './Uploader'
export default function PostDialog({ export default function PostDialog({
children, defaultContent = '',
parentEvent parentEvent,
open,
setOpen
}: { }: {
children: React.ReactNode defaultContent?: string
parentEvent?: Event parentEvent?: Event
open: boolean
setOpen: Dispatch<boolean>
}) { }) {
const { toast } = useToast() const { toast } = useToast()
const { publish, checkLogin } = useNostr() const { publish, checkLogin } = useNostr()
const [open, setOpen] = useState(false) const [content, setContent] = useState(defaultContent)
const [content, setContent] = useState('')
const [posting, setPosting] = useState(false) const [posting, setPosting] = useState(false)
const canPost = !!content && !posting const canPost = !!content && !posting
@@ -88,7 +90,6 @@ export default function PostDialog({
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="p-0" withoutClose> <DialogContent className="p-0" withoutClose>
<ScrollArea className="px-4 h-full max-h-screen"> <ScrollArea className="px-4 h-full max-h-screen">
<div className="space-y-4 px-2 py-6"> <div className="space-y-4 px-2 py-6">
@@ -107,6 +108,7 @@ export default function PostDialog({
<DialogDescription /> <DialogDescription />
</DialogHeader> </DialogHeader>
<Textarea <Textarea
className="h-32"
onChange={handleTextareaChange} onChange={handleTextareaChange}
value={content} value={content}
placeholder="Write something..." placeholder="Write something..."

View File

@@ -1,11 +1,12 @@
import { Event } from 'nostr-tools'
import { formatTimestamp } from '@renderer/lib/timestamp' import { formatTimestamp } from '@renderer/lib/timestamp'
import { Event } from 'nostr-tools'
import { useState } from 'react'
import Content from '../Content' import Content from '../Content'
import LikeButton from '../NoteStats/LikeButton'
import ParentNotePreview from '../ParentNotePreview'
import PostDialog from '../PostDialog'
import UserAvatar from '../UserAvatar' import UserAvatar from '../UserAvatar'
import Username from '../Username' import Username from '../Username'
import LikeButton from '../NoteStats/LikeButton'
import PostDialog from '../PostDialog'
import ParentNotePreview from '../ParentNotePreview'
export default function ReplyNote({ export default function ReplyNote({
event, event,
@@ -18,6 +19,8 @@ export default function ReplyNote({
onClickParent?: (eventId: string) => void onClickParent?: (eventId: string) => void
highlight?: boolean highlight?: boolean
}) { }) {
const [isPostDialogOpen, setIsPostDialogOpen] = useState(false)
return ( return (
<div <div
className={`flex space-x-2 items-start rounded-lg p-2 transition-colors duration-500 ${highlight ? 'bg-highlight/50' : ''}`} className={`flex space-x-2 items-start rounded-lg p-2 transition-colors duration-500 ${highlight ? 'bg-highlight/50' : ''}`}
@@ -35,12 +38,11 @@ export default function ReplyNote({
<Content event={event} size="small" /> <Content event={event} size="small" />
<div className="flex gap-2 text-xs"> <div className="flex gap-2 text-xs">
<div className="text-muted-foreground/60">{formatTimestamp(event.created_at)}</div> <div className="text-muted-foreground/60">{formatTimestamp(event.created_at)}</div>
<PostDialog parentEvent={event}>
<div className="text-muted-foreground hover:text-primary cursor-pointer">reply</div> <div className="text-muted-foreground hover:text-primary cursor-pointer">reply</div>
</PostDialog>
</div> </div>
</div> </div>
<LikeButton event={event} variant="reply" /> <LikeButton event={event} variant="reply" />
<PostDialog parentEvent={event} open={isPostDialogOpen} setOpen={setIsPostDialogOpen} />
</div> </div>
) )
} }

View File

@@ -40,8 +40,8 @@ class ClientService {
this.eventBatchLoadFn.bind(this), this.eventBatchLoadFn.bind(this),
{ cache: false } { cache: false }
) )
private profileCache = new LRUCache<string, Promise<TProfile | undefined>>({ max: 10000 }) private profileCache = new LRUCache<string, Promise<TProfile>>({ max: 10000 })
private profileDataloader = new DataLoader<string, TProfile | undefined>( private profileDataloader = new DataLoader<string, TProfile>(
(ids) => Promise.all(ids.map((id) => this._fetchProfileByBench32Id(id))), (ids) => Promise.all(ids.map((id) => this._fetchProfileByBench32Id(id))),
{ cacheMap: this.profileCache } { cacheMap: this.profileCache }
) )
@@ -237,7 +237,15 @@ class ClientService {
let event: NEvent | undefined let event: NEvent | undefined
if (filter.ids) { if (filter.ids) {
event = await this.fetchEventById(relays, filter.ids[0]) const eventId = filter.ids[0]
if (eventId !== id) {
const cache = this.eventCache.get(eventId)
if (cache) {
this.eventDataLoader.prime(id, cache)
return cache
}
}
event = await this.fetchEventById(relays, eventId)
} else { } else {
event = await this.tryHarderToFetchEvent(relays, filter) event = await this.tryHarderToFetchEvent(relays, filter)
} }
@@ -271,6 +279,14 @@ class ClientService {
throw new Error('Invalid id') throw new Error('Invalid id')
} }
if (pubkey !== id) {
const cache = this.profileCache.get(pubkey)
if (cache) {
this.profileDataloader.prime(id, cache)
return cache
}
}
const profileFromBigRelays = this.fetchProfileFromBigRelaysDataloader.load(pubkey) const profileFromBigRelays = this.fetchProfileFromBigRelaysDataloader.load(pubkey)
if (profileFromBigRelays) { if (profileFromBigRelays) {
return profileFromBigRelays return profileFromBigRelays