refactor: remove electron-related code

This commit is contained in:
codytseng 2024-12-21 23:20:30 +08:00
parent bed8df06e8
commit 2b1e6fe8f5
200 changed files with 2771 additions and 8432 deletions

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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} />
</>
)
}

View 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)}
/>
</>
)
}

View 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>
)
}

View file

@ -0,0 +1,4 @@
export function formatCount(count?: number) {
if (count === undefined || count <= 0) return ''
return count >= 100 ? '99+' : count
}