refactor: remove electron-related code
This commit is contained in:
parent
bed8df06e8
commit
2b1e6fe8f5
200 changed files with 2771 additions and 8432 deletions
89
src/components/NoteStats/LikeButton.tsx
Normal file
89
src/components/NoteStats/LikeButton.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { createReactionDraftEvent } from '@/lib/draft-event'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useNoteStats } from '@/providers/NoteStatsProvider'
|
||||
import client from '@/services/client.service'
|
||||
import { Heart, Loader } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { formatCount } from './utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function LikeButton({
|
||||
event,
|
||||
variant = 'normal',
|
||||
canFetch = false
|
||||
}: {
|
||||
event: Event
|
||||
variant?: 'normal' | 'reply'
|
||||
canFetch?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { publish, checkLogin } = useNostr()
|
||||
const { noteStatsMap, fetchNoteLikedStatus, fetchNoteLikeCount, markNoteAsLiked } = useNoteStats()
|
||||
const [liking, setLiking] = useState(false)
|
||||
const { likeCount, hasLiked } = useMemo(
|
||||
() => noteStatsMap.get(event.id) ?? {},
|
||||
[noteStatsMap, event.id]
|
||||
)
|
||||
const canLike = !hasLiked && !liking
|
||||
|
||||
useEffect(() => {
|
||||
if (!canFetch) return
|
||||
|
||||
if (likeCount === undefined) {
|
||||
fetchNoteLikeCount(event)
|
||||
}
|
||||
if (hasLiked === undefined) {
|
||||
fetchNoteLikedStatus(event)
|
||||
}
|
||||
}, [canFetch, event])
|
||||
|
||||
const like = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
checkLogin(async () => {
|
||||
if (!canLike) return
|
||||
|
||||
setLiking(true)
|
||||
const timer = setTimeout(() => setLiking(false), 5000)
|
||||
|
||||
try {
|
||||
const [liked] = await Promise.all([
|
||||
hasLiked === undefined ? fetchNoteLikedStatus(event) : hasLiked,
|
||||
likeCount === undefined ? fetchNoteLikeCount(event) : likeCount
|
||||
])
|
||||
if (liked) return
|
||||
|
||||
const targetRelayList = await client.fetchRelayList(event.pubkey)
|
||||
const reaction = createReactionDraftEvent(event)
|
||||
await publish(reaction, targetRelayList.read.slice(0, 3))
|
||||
markNoteAsLiked(event.id)
|
||||
} catch (error) {
|
||||
console.error('like failed', error)
|
||||
} finally {
|
||||
setLiking(false)
|
||||
clearTimeout(timer)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'flex items-center enabled:hover:text-red-400',
|
||||
variant === 'normal' ? 'gap-1' : 'flex-col',
|
||||
hasLiked ? 'text-red-400' : 'text-muted-foreground'
|
||||
)}
|
||||
onClick={like}
|
||||
disabled={!canLike}
|
||||
title={t('Like')}
|
||||
>
|
||||
{liking ? (
|
||||
<Loader className="animate-spin" size={16} />
|
||||
) : (
|
||||
<Heart size={16} className={hasLiked ? 'fill-red-400' : ''} />
|
||||
)}
|
||||
<div className="text-sm">{formatCount(likeCount)}</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
34
src/components/NoteStats/NoteOptions/RawEventDialog.tsx
Normal file
34
src/components/NoteStats/NoteOptions/RawEventDialog.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '@/components/ui/dialog'
|
||||
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
|
||||
import { Event } from 'nostr-tools'
|
||||
|
||||
export default function RawEventDialog({
|
||||
event,
|
||||
isOpen,
|
||||
onClose
|
||||
}: {
|
||||
event: Event
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}) {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="h-[60vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Raw Event</DialogTitle>
|
||||
<DialogDescription className="hidden" />
|
||||
</DialogHeader>
|
||||
<ScrollArea className="h-full">
|
||||
<pre className="text-sm text-muted-foreground">{JSON.stringify(event, null, 2)}</pre>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
47
src/components/NoteStats/NoteOptions/index.tsx
Normal file
47
src/components/NoteStats/NoteOptions/index.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { getSharableEventId } from '@/lib/event'
|
||||
import { Code, Copy, Ellipsis } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import RawEventDialog from './RawEventDialog'
|
||||
|
||||
export default function NoteOptions({ event }: { event: Event }) {
|
||||
const { t } = useTranslation()
|
||||
const [isRawEventDialogOpen, setIsRawEventDialogOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="h-4" onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Ellipsis
|
||||
size={16}
|
||||
className="text-muted-foreground hover:text-foreground cursor-pointer"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent collisionPadding={8}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigator.clipboard.writeText('nostr:' + getSharableEventId(event))}
|
||||
>
|
||||
<Copy />
|
||||
{t('copy embedded code')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setIsRawEventDialogOpen(true)}>
|
||||
<Code />
|
||||
{t('raw event')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<RawEventDialog
|
||||
event={event}
|
||||
isOpen={isRawEventDialogOpen}
|
||||
onClose={() => setIsRawEventDialogOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
34
src/components/NoteStats/ReplyButton.tsx
Normal file
34
src/components/NoteStats/ReplyButton.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useNoteStats } from '@/providers/NoteStatsProvider'
|
||||
import { MessageCircle } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo, useState } from 'react'
|
||||
import PostDialog from '../PostDialog'
|
||||
import { formatCount } from './utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function ReplyButton({ event }: { event: Event }) {
|
||||
const { t } = useTranslation()
|
||||
const { noteStatsMap } = useNoteStats()
|
||||
const { pubkey } = useNostr()
|
||||
const { replyCount } = useMemo(() => noteStatsMap.get(event.id) ?? {}, [noteStatsMap, event.id])
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="flex gap-1 items-center text-muted-foreground enabled:hover:text-blue-400"
|
||||
disabled={!pubkey}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setOpen(true)
|
||||
}}
|
||||
title={t('Reply')}
|
||||
>
|
||||
<MessageCircle size={16} />
|
||||
<div className="text-sm">{formatCount(replyCount)}</div>
|
||||
</button>
|
||||
<PostDialog parentEvent={event} open={open} setOpen={setOpen} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
121
src/components/NoteStats/RepostButton.tsx
Normal file
121
src/components/NoteStats/RepostButton.tsx
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { createRepostDraftEvent } from '@/lib/draft-event'
|
||||
import { getSharableEventId } from '@/lib/event'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useNoteStats } from '@/providers/NoteStatsProvider'
|
||||
import client from '@/services/client.service'
|
||||
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'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function RepostButton({
|
||||
event,
|
||||
canFetch = false
|
||||
}: {
|
||||
event: Event
|
||||
canFetch?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { publish, checkLogin } = useNostr()
|
||||
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]
|
||||
)
|
||||
const canRepost = !hasReposted && !reposting
|
||||
|
||||
useEffect(() => {
|
||||
if (!canFetch) return
|
||||
|
||||
if (repostCount === undefined) {
|
||||
fetchNoteRepostCount(event)
|
||||
}
|
||||
if (hasReposted === undefined) {
|
||||
fetchNoteRepostedStatus(event)
|
||||
}
|
||||
}, [canFetch, event])
|
||||
|
||||
const repost = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
checkLogin(async () => {
|
||||
if (!canRepost) return
|
||||
|
||||
setReposting(true)
|
||||
const timer = setTimeout(() => setReposting(false), 5000)
|
||||
|
||||
try {
|
||||
const [reposted] = await Promise.all([
|
||||
hasReposted === undefined ? fetchNoteRepostedStatus(event) : hasReposted,
|
||||
repostCount === undefined ? fetchNoteRepostCount(event) : repostCount
|
||||
])
|
||||
if (reposted) return
|
||||
|
||||
const targetRelayList = await client.fetchRelayList(event.pubkey)
|
||||
const repost = createRepostDraftEvent(event)
|
||||
await publish(repost, targetRelayList.read.slice(0, 5))
|
||||
markNoteAsReposted(event.id)
|
||||
} catch (error) {
|
||||
console.error('repost failed', error)
|
||||
} finally {
|
||||
setReposting(false)
|
||||
clearTimeout(timer)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<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={t('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()
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem onClick={repost}>
|
||||
<Repeat /> {t('Repost')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setIsPostDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<PencilLine /> {t('Quote')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<PostDialog
|
||||
open={isPostDialogOpen}
|
||||
setOpen={setIsPostDialogOpen}
|
||||
defaultContent={'\nnostr:' + getSharableEventId(event)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
27
src/components/NoteStats/index.tsx
Normal file
27
src/components/NoteStats/index.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { cn } from '@/lib/utils'
|
||||
import { Event } from 'nostr-tools'
|
||||
import LikeButton from './LikeButton'
|
||||
import NoteOptions from './NoteOptions'
|
||||
import ReplyButton from './ReplyButton'
|
||||
import RepostButton from './RepostButton'
|
||||
|
||||
export default function NoteStats({
|
||||
event,
|
||||
className,
|
||||
fetchIfNotExisting = false
|
||||
}: {
|
||||
event: Event
|
||||
className?: string
|
||||
fetchIfNotExisting?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('flex justify-between', className)}>
|
||||
<div className="flex gap-4 h-4 items-center">
|
||||
<ReplyButton event={event} />
|
||||
<RepostButton event={event} canFetch={fetchIfNotExisting} />
|
||||
<LikeButton event={event} canFetch={fetchIfNotExisting} />
|
||||
</div>
|
||||
<NoteOptions event={event} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
4
src/components/NoteStats/utils.ts
Normal file
4
src/components/NoteStats/utils.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export function formatCount(count?: number) {
|
||||
if (count === undefined || count <= 0) return ''
|
||||
return count >= 100 ? '99+' : count
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue