feat: quote
This commit is contained in:
@@ -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} />
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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..."
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user