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
16 changed files with 417 additions and 186 deletions

View File

@@ -167,14 +167,16 @@ export default function Nip22ReplyNoteList({
return (
<>
<div
className={`text-sm text-center text-muted-foreground ${!loading ? 'hover:text-foreground cursor-pointer' : ''}`}
onClick={loadMore}
>
{loading ? t('loading...') : until ? t('load more older replies') : null}
</div>
{replies.length > 0 && (loading || until) && <Separator className="my-2" />}
<div className={cn('mb-4', className)}>
{(loading || until) && (
<div
className={`text-sm text-center text-muted-foreground mt-2 ${!loading ? 'hover:text-foreground cursor-pointer' : ''}`}
onClick={loadMore}
>
{loading ? t('loading...') : t('load more older replies')}
</div>
)}
{replies.length > 0 && (loading || until) && <Separator className="mt-2" />}
<div className={cn('mb-2', className)}>
{replies.map((reply) => {
const info = replyMap[reply.id]
return (

View File

@@ -6,6 +6,7 @@ import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import Content from '../Content'
import { FormattedTimestamp } from '../FormattedTimestamp'
import NoteOptions from '../NoteOptions'
import NoteStats from '../NoteStats'
import ParentNotePreview from '../ParentNotePreview'
import UserAvatar from '../UserAvatar'
@@ -36,25 +37,28 @@ export default function Note({
return (
<div className={className}>
<div className="flex items-center space-x-2">
<UserAvatar userId={event.pubkey} size={size === 'small' ? 'small' : 'normal'} />
<div
className={`flex-1 w-0 ${size === 'small' ? 'flex space-x-2 items-center overflow-hidden' : ''}`}
>
<div className="flex gap-2 items-center">
<Username
userId={event.pubkey}
className={`font-semibold flex truncate ${size === 'small' ? 'text-sm' : ''}`}
skeletonClassName={size === 'small' ? 'h-3' : 'h-4'}
/>
{usingClient && size === 'normal' && (
<div className="text-xs text-muted-foreground shrink-0">using {usingClient}</div>
)}
</div>
<div className="text-xs text-muted-foreground shrink-0">
<FormattedTimestamp timestamp={event.created_at} />
<div className="flex justify-between items-start gap-2">
<div className="flex items-center space-x-2 flex-1">
<UserAvatar userId={event.pubkey} size={size === 'small' ? 'small' : 'normal'} />
<div
className={`flex-1 w-0 ${size === 'small' ? 'flex space-x-2 items-center overflow-hidden' : ''}`}
>
<div className="flex gap-2 items-center">
<Username
userId={event.pubkey}
className={`font-semibold flex truncate ${size === 'small' ? 'text-sm' : ''}`}
skeletonClassName={size === 'small' ? 'h-3' : 'h-4'}
/>
{usingClient && size === 'normal' && (
<div className="text-xs text-muted-foreground shrink-0">using {usingClient}</div>
)}
</div>
<div className="text-xs text-muted-foreground shrink-0">
<FormattedTimestamp timestamp={event.created_at} />
</div>
</div>
</div>
{size === 'normal' && <NoteOptions event={event} className="shrink-0 [&_svg]:size-5" />}
</div>
{parentEventId && (
<ParentNotePreview

View File

@@ -0,0 +1,152 @@
import { Button } from '@/components/ui/button'
import { Drawer, DrawerContent, DrawerOverlay } from '@/components/ui/drawer'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
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 { useScreenSize } from '@/providers/ScreenSizeProvider'
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, className }: { event: Event; className?: string }) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { pubkey } = useNostr()
const [isRawEventDialogOpen, setIsRawEventDialogOpen] = useState(false)
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const { mutePubkey, unmutePubkey, mutePubkeys } = useMuteList()
const isMuted = useMemo(() => mutePubkeys.includes(event.pubkey), [mutePubkeys, event])
const trigger = (
<button
className="flex items-center text-muted-foreground hover:text-foreground pl-3 h-full"
onClick={() => setIsDrawerOpen(true)}
>
<Ellipsis />
</button>
)
const rawEventDialog = (
<RawEventDialog
event={event}
isOpen={isRawEventDialogOpen}
onClose={() => setIsRawEventDialogOpen(false)}
/>
)
if (isSmallScreen) {
return (
<div className={className} onClick={(e) => e.stopPropagation()}>
{trigger}
<Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>
<DrawerOverlay onClick={() => setIsDrawerOpen(false)} />
<DrawerContent hideOverlay>
<div className="py-2">
<Button
onClick={() => {
setIsDrawerOpen(false)
navigator.clipboard.writeText(getSharableEventId(event))
}}
className="w-full p-6 justify-start text-lg gap-4 [&_svg]:size-5"
variant="ghost"
>
<Copy />
{t('Copy event ID')}
</Button>
<Button
onClick={() => {
navigator.clipboard.writeText(pubkeyToNpub(event.pubkey) ?? '')
setIsDrawerOpen(false)
}}
className="w-full p-6 justify-start text-lg gap-4 [&_svg]:size-5"
variant="ghost"
>
<Copy />
{t('Copy user ID')}
</Button>
<Button
onClick={() => {
setIsDrawerOpen(false)
setIsRawEventDialogOpen(true)
}}
className="w-full p-6 justify-start text-lg gap-4 [&_svg]:size-5"
variant="ghost"
>
<Code />
{t('View raw event')}
</Button>
{pubkey && (
<Button
onClick={() => {
setIsDrawerOpen(false)
if (isMuted) {
unmutePubkey(event.pubkey)
} else {
mutePubkey(event.pubkey)
}
}}
className="w-full p-6 justify-start text-destructive text-lg gap-4 [&_svg]:size-5 focus:text-destructive"
variant="ghost"
>
{isMuted ? <Bell /> : <BellOff />}
{isMuted ? t('Unmute user') : t('Mute user')}
</Button>
)}
</div>
</DrawerContent>
</Drawer>
{rawEventDialog}
</div>
)
}
return (
<div className={className} onClick={(e) => e.stopPropagation()}>
<DropdownMenu>
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent collisionPadding={8} className="min-w-52">
<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>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setIsRawEventDialogOpen(true)}>
<Code />
{t('View raw event')}
</DropdownMenuItem>
{pubkey && (
<>
<DropdownMenuSeparator />
<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}
</div>
)
}

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,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>

View File

@@ -6,6 +6,7 @@ import NoteStats from '../NoteStats'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
import PictureContent from '../PictureContent'
import NoteOptions from '../NoteOptions'
export default function PictureNote({
event,
@@ -22,23 +23,26 @@ export default function PictureNote({
return (
<div className={className}>
<div className="px-4 flex items-center space-x-2">
<UserAvatar userId={event.pubkey} />
<div className="flex-1 w-0">
<div className="flex gap-2 items-center">
<Username
userId={event.pubkey}
className="font-semibold flex"
skeletonClassName="h-4"
/>
{usingClient && (
<div className="text-xs text-muted-foreground truncate">using {usingClient}</div>
)}
</div>
<div className="text-xs text-muted-foreground line-clamp-1">
<FormattedTimestamp timestamp={event.created_at} />
<div className="px-4 flex justify-between items-start gap-2">
<div className="flex items-center space-x-2 flex-1">
<UserAvatar userId={event.pubkey} />
<div className="flex-1 w-0">
<div className="flex gap-2 items-center">
<Username
userId={event.pubkey}
className="font-semibold flex truncate"
skeletonClassName="h-4"
/>
{usingClient && (
<div className="text-xs text-muted-foreground truncate">using {usingClient}</div>
)}
</div>
<div className="text-xs text-muted-foreground line-clamp-1">
<FormattedTimestamp timestamp={event.created_at} />
</div>
</div>
</div>
<NoteOptions event={event} className="shrink-0 [&_svg]:size-5" />
</div>
<PictureContent className="mt-2" event={event} />
{!hideStats && (

View File

@@ -5,6 +5,7 @@ import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Content from '../Content'
import { FormattedTimestamp } from '../FormattedTimestamp'
import NoteOptions from '../NoteOptions'
import NoteStats from '../NoteStats'
import ParentNotePreview from '../ParentNotePreview'
import UserAvatar from '../UserAvatar'
@@ -31,19 +32,22 @@ export default function ReplyNote({
return (
<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 px-4 py-3 border-b transition-colors duration-500 ${highlight ? 'bg-highlight/50' : ''}`}
>
<UserAvatar userId={event.pubkey} size="small" className="shrink-0" />
<div className="w-full overflow-hidden">
<div className="flex gap-2 items-center">
<Username
userId={event.pubkey}
className="text-sm font-semibold text-muted-foreground hover:text-foreground truncate"
skeletonClassName="h-3"
/>
<div className="text-xs text-muted-foreground shrink-0">
<FormattedTimestamp timestamp={event.created_at} />
<div className="flex items-start justify-between gap-2">
<div className="flex gap-2 items-center flex-1">
<Username
userId={event.pubkey}
className="text-sm font-semibold text-muted-foreground hover:text-foreground truncate"
skeletonClassName="h-3"
/>
<div className="text-xs text-muted-foreground shrink-0">
<FormattedTimestamp timestamp={event.created_at} />
</div>
</div>
<NoteOptions event={event} className="shrink-0 [&_svg]:size-5" />
</div>
{parentEvent && (
<ParentNotePreview
@@ -58,7 +62,12 @@ export default function ReplyNote({
{show ? (
<>
<Content className="mt-1" event={event} size="small" />
<NoteStats className="mt-2" event={event} variant="reply" />
<NoteStats
className="mt-2"
classNames={{ buttonBar: 'justify-start gap-1' }}
event={event}
variant="reply"
/>
</>
) : (
<Button

View File

@@ -178,17 +178,19 @@ export default function ReplyNoteList({ event, className }: { event: NEvent; cla
return (
<>
<div
className={`text-sm text-center text-muted-foreground ${!loading ? 'hover:text-foreground cursor-pointer' : ''}`}
onClick={loadMore}
>
{loading ? t('loading...') : until ? t('load more older replies') : null}
</div>
{replies.length === 0 && !loading && !until && (
<div className="text-sm text-center text-muted-foreground">{t('no replies')}</div>
{(loading || until) && (
<div
className={`text-sm text-center text-muted-foreground mt-2 ${!loading ? 'hover:text-foreground cursor-pointer' : ''}`}
onClick={loadMore}
>
{loading ? t('loading...') : t('load more older replies')}
</div>
)}
{replies.length > 0 && (loading || until) && <Separator className="my-2" />}
<div className={cn('mb-4', className)}>
{replies.length === 0 && !loading && !until && (
<div className="text-sm mt-2 text-center text-muted-foreground">{t('no replies')}</div>
)}
{replies.length > 0 && (loading || until) && <Separator className="mt-2" />}
<div className={cn('mb-2', className)}>
{replies.map((reply) => {
const info = replyMap[reply.id]
return (

View File

@@ -31,10 +31,10 @@ DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> & { hideOverlay?: boolean }
>(({ className, children, hideOverlay = false, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
{!hideOverlay && <DrawerOverlay />}
<DrawerPrimitive.Content
ref={ref}
className={cn(

View File

@@ -58,12 +58,8 @@ const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref
return (
<SecondaryPageLayout ref={ref} index={index} title={t('Note')} displayScrollToTopButton>
<PictureNote key={`note-${event.id}`} event={event} fetchNoteStats />
<Separator className="mb-2 mt-4" />
<Nip22ReplyNoteList
key={`nip22-reply-note-list-${event.id}`}
event={event}
className="px-2"
/>
<Separator className="mt-4" />
<Nip22ReplyNoteList key={`nip22-reply-note-list-${event.id}`} event={event} />
</SecondaryPageLayout>
)
}
@@ -77,15 +73,11 @@ const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref
<ParentNote key={`parent-note-${event.id}`} eventId={parentEventId} />
<Note key={`note-${event.id}`} event={event} fetchNoteStats hideParentNotePreview />
</div>
<Separator className="mb-2 mt-4" />
<Separator className="mt-4" />
{event.kind === kinds.ShortTextNote ? (
<ReplyNoteList key={`reply-note-list-${event.id}`} event={event} className="px-2" />
<ReplyNoteList key={`reply-note-list-${event.id}`} event={event} />
) : isPictureEvent(event) ? (
<Nip22ReplyNoteList
key={`nip22-reply-note-list-${event.id}`}
event={event}
className="px-2"
/>
<Nip22ReplyNoteList key={`nip22-reply-note-list-${event.id}`} event={event} />
) : null}
</SecondaryPageLayout>
)