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

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
import { Event } from 'nostr-tools'
import { formatTimestamp } from '@renderer/lib/timestamp'
import { Event } from 'nostr-tools'
import { useState } from 'react'
import Content from '../Content'
import LikeButton from '../NoteStats/LikeButton'
import ParentNotePreview from '../ParentNotePreview'
import PostDialog from '../PostDialog'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
import LikeButton from '../NoteStats/LikeButton'
import PostDialog from '../PostDialog'
import ParentNotePreview from '../ParentNotePreview'
export default function ReplyNote({
event,
@@ -18,6 +19,8 @@ export default function ReplyNote({
onClickParent?: (eventId: string) => void
highlight?: boolean
}) {
const [isPostDialogOpen, setIsPostDialogOpen] = useState(false)
return (
<div
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" />
<div className="flex gap-2 text-xs">
<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>
</PostDialog>
<div className="text-muted-foreground hover:text-primary cursor-pointer">reply</div>
</div>
</div>
<LikeButton event={event} variant="reply" />
<PostDialog parentEvent={event} open={isPostDialogOpen} setOpen={setIsPostDialogOpen} />
</div>
)
}

View File

@@ -40,8 +40,8 @@ class ClientService {
this.eventBatchLoadFn.bind(this),
{ cache: false }
)
private profileCache = new LRUCache<string, Promise<TProfile | undefined>>({ max: 10000 })
private profileDataloader = new DataLoader<string, TProfile | undefined>(
private profileCache = new LRUCache<string, Promise<TProfile>>({ max: 10000 })
private profileDataloader = new DataLoader<string, TProfile>(
(ids) => Promise.all(ids.map((id) => this._fetchProfileByBench32Id(id))),
{ cacheMap: this.profileCache }
)
@@ -237,7 +237,15 @@ class ClientService {
let event: NEvent | undefined
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 {
event = await this.tryHarderToFetchEvent(relays, filter)
}
@@ -271,6 +279,14 @@ class ClientService {
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)
if (profileFromBigRelays) {
return profileFromBigRelays