style: adjust the style of NoteStats (#222)
This commit is contained in:
parent
71895e3a0f
commit
accf3319e7
16 changed files with 417 additions and 186 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue