style: adjust the style of NoteStats (#222)

This commit is contained in:
Cody Tseng 2025-03-07 23:39:46 +08:00 committed by GitHub
parent 71895e3a0f
commit accf3319e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 417 additions and 186 deletions

View file

@ -55,7 +55,7 @@ export default function LikeButton({ event }: { event: Event }) {
return (
<button
className={cn(
'flex items-center enabled:hover:text-red-400 gap-1',
'flex items-center enabled:hover:text-red-400 gap-1 px-3 h-full',
hasLiked ? 'text-red-400' : 'text-muted-foreground'
)}
onClick={like}
@ -63,9 +63,9 @@ export default function LikeButton({ event }: { event: Event }) {
title={t('Like')}
>
{liking ? (
<Loader className="animate-spin" size={16} />
<Loader className="animate-spin" />
) : (
<Heart size={16} className={hasLiked ? 'fill-red-400' : ''} />
<Heart className={hasLiked ? 'fill-red-400' : ''} />
)}
{!!likeCount && <div className="text-sm">{formatCount(likeCount)}</div>}
</button>

View file

@ -1,34 +0,0 @@
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

@ -1,68 +0,0 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { getSharableEventId } from '@/lib/event'
import { pubkeyToNpub } from '@/lib/pubkey'
import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import { Bell, BellOff, Code, Copy, Ellipsis } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import RawEventDialog from './RawEventDialog'
export default function NoteOptions({ event }: { event: Event }) {
const { t } = useTranslation()
const { pubkey } = useNostr()
const [isRawEventDialogOpen, setIsRawEventDialogOpen] = useState(false)
const { mutePubkey, unmutePubkey, mutePubkeys } = useMuteList()
const isMuted = useMemo(() => mutePubkeys.includes(event.pubkey), [mutePubkeys, event])
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(getSharableEventId(event))}
>
<Copy />
{t('Copy event ID')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(pubkeyToNpub(event.pubkey) ?? '')}
>
<Copy />
{t('Copy user ID')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsRawEventDialogOpen(true)}>
<Code />
{t('View raw event')}
</DropdownMenuItem>
{pubkey && (
<DropdownMenuItem
onClick={() => (isMuted ? unmutePubkey(event.pubkey) : mutePubkey(event.pubkey))}
className="text-destructive focus:text-destructive"
>
{isMuted ? <Bell /> : <BellOff />}
{isMuted ? t('Unmute user') : t('Mute user')}
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
<RawEventDialog
event={event}
isOpen={isRawEventDialogOpen}
onClose={() => setIsRawEventDialogOpen(false)}
/>
</div>
)
}

View file

@ -26,7 +26,7 @@ export default function ReplyButton({
return (
<>
<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 pr-3 h-full"
onClick={(e) => {
e.stopPropagation()
checkLogin(() => {
@ -35,7 +35,7 @@ export default function ReplyButton({
}}
title={t('Reply')}
>
<MessageCircle size={16} />
<MessageCircle />
{variant !== 'reply' && !!replyCount && (
<div className="text-sm">{formatCount(replyCount)}</div>
)}

View file

@ -1,3 +1,5 @@
import { Button } from '@/components/ui/button'
import { Drawer, DrawerContent, DrawerOverlay } from '@/components/ui/drawer'
import {
DropdownMenu,
DropdownMenuContent,
@ -9,6 +11,7 @@ import { getSharableEventId } from '@/lib/event'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
import { useNoteStats } from '@/providers/NoteStatsProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import client from '@/services/client.service'
import { Loader, PencilLine, Repeat } from 'lucide-react'
import { Event } from 'nostr-tools'
@ -19,10 +22,12 @@ import { formatCount } from './utils'
export default function RepostButton({ event }: { event: Event }) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { publish, checkLogin, pubkey } = useNostr()
const { noteStatsMap, updateNoteStatsByEvents, fetchNoteStats } = useNoteStats()
const [reposting, setReposting] = useState(false)
const [isPostDialogOpen, setIsPostDialogOpen] = useState(false)
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const { repostCount, hasReposted } = useMemo(() => {
const stats = noteStatsMap.get(event.id) || {}
return {
@ -62,22 +67,74 @@ export default function RepostButton({ event }: { event: Event }) {
})
}
const trigger = (
<button
className={cn(
'flex gap-1 items-center enabled:hover:text-lime-500 px-3 h-full',
hasReposted ? 'text-lime-500' : 'text-muted-foreground'
)}
title={t('Repost')}
onClick={() => {
if (isSmallScreen) {
setIsDrawerOpen(true)
}
}}
>
{reposting ? <Loader className="animate-spin" /> : <Repeat />}
{!!repostCount && <div className="text-sm">{formatCount(repostCount)}</div>}
</button>
)
const postEditor = (
<PostEditor
open={isPostDialogOpen}
setOpen={setIsPostDialogOpen}
defaultContent={'\nnostr:' + getSharableEventId(event)}
/>
)
if (isSmallScreen) {
return (
<>
{trigger}
<Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>
<DrawerOverlay onClick={() => setIsDrawerOpen(false)} />
<DrawerContent hideOverlay>
<div className="py-2">
<Button
onClick={repost}
disabled={!canRepost}
className="w-full p-6 justify-start text-lg gap-4 [&_svg]:size-5"
variant="ghost"
>
<Repeat /> {t('Repost')}
</Button>
<Button
onClick={(e) => {
e.stopPropagation()
checkLogin(() => {
setIsDrawerOpen(false)
setIsPostDialogOpen(true)
})
}}
className="w-full p-6 justify-start text-lg gap-4 [&_svg]:size-5"
variant="ghost"
>
<PencilLine /> {t('Quote')}
</Button>
</div>
</DrawerContent>
</Drawer>
{postEditor}
</>
)
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className={cn(
'flex gap-1 items-center enabled:hover:text-lime-500',
hasReposted ? 'text-lime-500' : 'text-muted-foreground'
)}
title={t('Repost')}
>
{reposting ? <Loader className="animate-spin" size={16} /> : <Repeat size={16} />}
{!!repostCount && <div className="text-sm">{formatCount(repostCount)}</div>}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-44">
<DropdownMenuItem onClick={repost} disabled={!canRepost}>
<Repeat /> {t('Repost')}
</DropdownMenuItem>
@ -93,11 +150,7 @@ export default function RepostButton({ event }: { event: Event }) {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<PostEditor
open={isPostDialogOpen}
setOpen={setIsPostDialogOpen}
defaultContent={'\nnostr:' + getSharableEventId(event)}
/>
{postEditor}
</>
)
}

View file

@ -1,4 +1,6 @@
import { useSecondaryPage } from '@/PageManager'
import { Button } from '@/components/ui/button'
import { Drawer, DrawerContent, DrawerOverlay } from '@/components/ui/drawer'
import {
DropdownMenu,
DropdownMenuContent,
@ -9,39 +11,78 @@ import {
} from '@/components/ui/dropdown-menu'
import { toRelay } from '@/lib/link'
import { simplifyUrl } from '@/lib/url'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import client from '@/services/client.service'
import { Server } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import RelayIcon from '../RelayIcon'
export default function SeenOnButton({ event }: { event: Event }) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { push } = useSecondaryPage()
const [relays, setRelays] = useState<string[]>([])
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
useEffect(() => {
const seenOn = client.getSeenEventRelayUrls(event.id)
setRelays(seenOn)
}, [])
const trigger = (
<button
className="flex gap-1 items-center text-muted-foreground enabled:hover:text-primary pl-3 h-full"
title={t('Seen on')}
disabled={relays.length === 0}
onClick={() => {
if (isSmallScreen) {
setIsDrawerOpen(true)
}
}}
>
<Server />
{relays.length > 0 && <div className="text-sm">{relays.length}</div>}
</button>
)
if (isSmallScreen) {
return (
<>
{trigger}
<Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>
<DrawerOverlay onClick={() => setIsDrawerOpen(false)} />
<DrawerContent hideOverlay>
<div className="py-2">
{relays.map((relay) => (
<Button
className="w-full p-6 justify-start text-lg gap-4"
variant="ghost"
key={relay}
onClick={() => {
setIsDrawerOpen(false)
push(toRelay(relay))
}}
>
<RelayIcon url={relay} /> {simplifyUrl(relay)}
</Button>
))}
</div>
</DrawerContent>
</Drawer>
</>
)
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="flex gap-1 items-center text-muted-foreground enabled:hover:text-primary"
title={t('Seen on')}
disabled={relays.length === 0}
>
<Server size={16} />
{relays.length > 0 && <div className="text-sm">{relays.length}</div>}
</button>
</DropdownMenuTrigger>
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent collisionPadding={8}>
<DropdownMenuLabel>{t('Seen on')}</DropdownMenuLabel>
<DropdownMenuSeparator />
{relays.map((relay) => (
<DropdownMenuItem key={relay} onClick={() => push(toRelay(relay))}>
<DropdownMenuItem key={relay} onClick={() => push(toRelay(relay))} className="min-w-52">
<RelayIcon url={relay} />
{simplifyUrl(relay)}
</DropdownMenuItem>
))}

View file

@ -18,6 +18,7 @@ export default function ZapButton({ event }: { event: Event }) {
const { checkLogin, pubkey } = useNostr()
const { noteStatsMap, addZap } = useNoteStats()
const { defaultZapSats, defaultZapComment, quickZap } = useZap()
const [touchStart, setTouchStart] = useState<{ x: number; y: number } | null>(null)
const [openZapDialog, setOpenZapDialog] = useState(false)
const [zapping, setZapping] = useState(false)
const { zapAmount, hasZapped } = useMemo(() => {
@ -71,6 +72,11 @@ export default function ZapButton({ event }: { event: Event }) {
e.preventDefault()
isLongPressRef.current = false
if ('touches' in e) {
const touch = e.touches[0]
setTouchStart({ x: touch.clientX, y: touch.clientY })
}
if (quickZap) {
timerRef.current = setTimeout(() => {
isLongPressRef.current = true
@ -89,6 +95,15 @@ export default function ZapButton({ event }: { event: Event }) {
clearTimeout(timerRef.current)
}
if ('touches' in e) {
setTouchStart(null)
if (!touchStart) return
const touch = e.changedTouches[0]
const diffX = Math.abs(touch.clientX - touchStart.x)
const diffY = Math.abs(touch.clientY - touchStart.y)
if (diffX > 10 || diffY > 10) return
}
if (!quickZap) {
checkLogin(() => {
setOpenZapDialog(true)
@ -110,7 +125,7 @@ export default function ZapButton({ event }: { event: Event }) {
<>
<button
className={cn(
'flex items-center enabled:hover:text-yellow-400 gap-1 select-none',
'flex items-center enabled:hover:text-yellow-400 gap-1 select-none px-3 h-full',
hasZapped ? 'text-yellow-400' : 'text-muted-foreground'
)}
title={t('Zap')}
@ -121,9 +136,9 @@ export default function ZapButton({ event }: { event: Event }) {
onTouchEnd={handleClickEnd}
>
{zapping ? (
<Loader className="animate-spin" size={16} />
<Loader className="animate-spin" />
) : (
<Zap size={16} className={hasZapped ? 'fill-yellow-400' : ''} />
<Zap className={hasZapped ? 'fill-yellow-400' : ''} />
)}
{!!zapAmount && <div className="text-sm">{formatAmount(zapAmount)}</div>}
</button>

View file

@ -1,9 +1,9 @@
import { cn } from '@/lib/utils'
import { useNoteStats } from '@/providers/NoteStatsProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Event } from 'nostr-tools'
import { useEffect } from 'react'
import LikeButton from './LikeButton'
import NoteOptions from './NoteOptions'
import ReplyButton from './ReplyButton'
import RepostButton from './RepostButton'
import SeenOnButton from './SeenOnButton'
@ -13,14 +13,19 @@ import ZapButton from './ZapButton'
export default function NoteStats({
event,
className,
classNames,
fetchIfNotExisting = false,
variant = 'note'
}: {
event: Event
className?: string
classNames?: {
buttonBar?: string
}
fetchIfNotExisting?: boolean
variant?: 'note' | 'reply'
}) {
const { isSmallScreen } = useScreenSize()
const { fetchNoteStats } = useNoteStats()
useEffect(() => {
@ -28,19 +33,39 @@ export default function NoteStats({
fetchNoteStats(event)
}, [event, fetchIfNotExisting])
if (isSmallScreen) {
return (
<div className={cn('select-none', className)}>
<TopZaps event={event} />
<div
className={cn(
'flex justify-between items-center h-5 [&_svg]:size-5',
classNames?.buttonBar
)}
onClick={(e) => e.stopPropagation()}
>
<ReplyButton event={event} variant={variant} />
<RepostButton event={event} />
<LikeButton event={event} />
<ZapButton event={event} />
<SeenOnButton event={event} />
</div>
</div>
)
}
return (
<div className={cn('select-none', className)}>
<TopZaps event={event} />
<div className="flex justify-between">
<div className="flex gap-5 h-4 items-center" onClick={(e) => e.stopPropagation()}>
<div className="flex justify-between h-5 [&_svg]:size-4">
<div className="flex items-center" onClick={(e) => e.stopPropagation()}>
<ReplyButton event={event} variant={variant} />
<RepostButton event={event} />
<LikeButton event={event} />
<ZapButton event={event} />
</div>
<div className="flex gap-5 h-4 items-center" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center" onClick={(e) => e.stopPropagation()}>
<SeenOnButton event={event} />
<NoteOptions event={event} />
</div>
</div>
</div>