27 changed files with 654 additions and 156 deletions
@ -0,0 +1,89 @@ |
|||||||
|
import { useSecondaryPage } from '@/PageManager' |
||||||
|
import { useNoteStatsById } from '@/hooks/useNoteStatsById' |
||||||
|
import { toProfile } from '@/lib/link' |
||||||
|
import { useScreenSize } from '@/providers/ScreenSizeProvider' |
||||||
|
import { useUserTrust } from '@/providers/UserTrustProvider' |
||||||
|
import { Event } from 'nostr-tools' |
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react' |
||||||
|
import { useTranslation } from 'react-i18next' |
||||||
|
import Emoji from '../Emoji' |
||||||
|
import { FormattedTimestamp } from '../FormattedTimestamp' |
||||||
|
import Nip05 from '../Nip05' |
||||||
|
import UserAvatar from '../UserAvatar' |
||||||
|
import Username from '../Username' |
||||||
|
|
||||||
|
const SHOW_COUNT = 20 |
||||||
|
|
||||||
|
export default function ReactionList({ event }: { event: Event }) { |
||||||
|
const { t } = useTranslation() |
||||||
|
const { push } = useSecondaryPage() |
||||||
|
const { isSmallScreen } = useScreenSize() |
||||||
|
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust() |
||||||
|
const noteStats = useNoteStatsById(event.id) |
||||||
|
const filteredLikes = useMemo(() => { |
||||||
|
return (noteStats?.likes ?? []) |
||||||
|
.filter((like) => !hideUntrustedInteractions || isUserTrusted(like.pubkey)) |
||||||
|
.sort((a, b) => b.created_at - a.created_at) |
||||||
|
}, [noteStats, event.id, hideUntrustedInteractions, isUserTrusted]) |
||||||
|
|
||||||
|
const [showCount, setShowCount] = useState(SHOW_COUNT) |
||||||
|
const bottomRef = useRef<HTMLDivElement | null>(null) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!bottomRef.current || filteredLikes.length <= showCount) return |
||||||
|
const obs = new IntersectionObserver( |
||||||
|
([entry]) => { |
||||||
|
if (entry.isIntersecting) setShowCount((c) => c + SHOW_COUNT) |
||||||
|
}, |
||||||
|
{ rootMargin: '10px', threshold: 0.1 } |
||||||
|
) |
||||||
|
obs.observe(bottomRef.current) |
||||||
|
return () => obs.disconnect() |
||||||
|
}, [filteredLikes.length, showCount]) |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="min-h-[80vh]"> |
||||||
|
{filteredLikes.slice(0, showCount).map((like) => ( |
||||||
|
<div |
||||||
|
key={like.id} |
||||||
|
className="px-4 py-3 border-b transition-colors clickable flex items-center gap-3" |
||||||
|
onClick={() => push(toProfile(like.pubkey))} |
||||||
|
> |
||||||
|
<div className="w-6 flex flex-col items-center"> |
||||||
|
<Emoji |
||||||
|
emoji={like.emoji} |
||||||
|
classNames={{ |
||||||
|
text: 'text-xl', |
||||||
|
img: 'size-5' |
||||||
|
}} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<UserAvatar userId={like.pubkey} size="medium" className="shrink-0" /> |
||||||
|
|
||||||
|
<div className="flex-1 w-0"> |
||||||
|
<Username |
||||||
|
userId={like.pubkey} |
||||||
|
className="text-sm font-semibold text-muted-foreground hover:text-foreground max-w-fit truncate" |
||||||
|
skeletonClassName="h-3" |
||||||
|
/> |
||||||
|
<div className="flex items-center gap-1 text-sm text-muted-foreground"> |
||||||
|
<Nip05 pubkey={like.pubkey} append="·" /> |
||||||
|
<FormattedTimestamp |
||||||
|
timestamp={like.created_at} |
||||||
|
className="shrink-0" |
||||||
|
short={isSmallScreen} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
|
||||||
|
<div ref={bottomRef} /> |
||||||
|
|
||||||
|
<div className="text-sm mt-2 text-center text-muted-foreground"> |
||||||
|
{filteredLikes.length > 0 ? t('No more reactions') : t('No reactions yet')} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,81 @@ |
|||||||
|
import { useSecondaryPage } from '@/PageManager' |
||||||
|
import { useNoteStatsById } from '@/hooks/useNoteStatsById' |
||||||
|
import { toProfile } from '@/lib/link' |
||||||
|
import { useScreenSize } from '@/providers/ScreenSizeProvider' |
||||||
|
import { useUserTrust } from '@/providers/UserTrustProvider' |
||||||
|
import { Repeat } from 'lucide-react' |
||||||
|
import { Event } from 'nostr-tools' |
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react' |
||||||
|
import { useTranslation } from 'react-i18next' |
||||||
|
import { FormattedTimestamp } from '../FormattedTimestamp' |
||||||
|
import Nip05 from '../Nip05' |
||||||
|
import UserAvatar from '../UserAvatar' |
||||||
|
import Username from '../Username' |
||||||
|
|
||||||
|
const SHOW_COUNT = 20 |
||||||
|
|
||||||
|
export default function RepostList({ event }: { event: Event }) { |
||||||
|
const { t } = useTranslation() |
||||||
|
const { push } = useSecondaryPage() |
||||||
|
const { isSmallScreen } = useScreenSize() |
||||||
|
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust() |
||||||
|
const noteStats = useNoteStatsById(event.id) |
||||||
|
const filteredReposts = useMemo(() => { |
||||||
|
return (noteStats?.reposts ?? []) |
||||||
|
.filter((repost) => !hideUntrustedInteractions || isUserTrusted(repost.pubkey)) |
||||||
|
.sort((a, b) => b.created_at - a.created_at) |
||||||
|
}, [noteStats, event.id, hideUntrustedInteractions, isUserTrusted]) |
||||||
|
|
||||||
|
const [showCount, setShowCount] = useState(SHOW_COUNT) |
||||||
|
const bottomRef = useRef<HTMLDivElement | null>(null) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!bottomRef.current || filteredReposts.length <= showCount) return |
||||||
|
const obs = new IntersectionObserver( |
||||||
|
([entry]) => { |
||||||
|
if (entry.isIntersecting) setShowCount((c) => c + SHOW_COUNT) |
||||||
|
}, |
||||||
|
{ rootMargin: '10px', threshold: 0.1 } |
||||||
|
) |
||||||
|
obs.observe(bottomRef.current) |
||||||
|
return () => obs.disconnect() |
||||||
|
}, [filteredReposts.length, showCount]) |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="min-h-[80vh]"> |
||||||
|
{filteredReposts.slice(0, showCount).map((repost) => ( |
||||||
|
<div |
||||||
|
key={repost.id} |
||||||
|
className="px-4 py-3 border-b transition-colors clickable flex items-center gap-3" |
||||||
|
onClick={() => push(toProfile(repost.pubkey))} |
||||||
|
> |
||||||
|
<Repeat className="text-green-400 size-5" /> |
||||||
|
|
||||||
|
<UserAvatar userId={repost.pubkey} size="medium" className="shrink-0" /> |
||||||
|
|
||||||
|
<div className="flex-1 w-0"> |
||||||
|
<Username |
||||||
|
userId={repost.pubkey} |
||||||
|
className="text-sm font-semibold text-muted-foreground hover:text-foreground max-w-fit truncate" |
||||||
|
skeletonClassName="h-3" |
||||||
|
/> |
||||||
|
<div className="flex items-center gap-1 text-sm text-muted-foreground"> |
||||||
|
<Nip05 pubkey={repost.pubkey} append="·" /> |
||||||
|
<FormattedTimestamp |
||||||
|
timestamp={repost.created_at} |
||||||
|
className="shrink-0" |
||||||
|
short={isSmallScreen} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
|
||||||
|
<div ref={bottomRef} /> |
||||||
|
|
||||||
|
<div className="text-sm mt-2 text-center text-muted-foreground"> |
||||||
|
{filteredReposts.length > 0 ? t('No more reposts') : t('No reposts yet')} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,84 @@ |
|||||||
|
import { useSecondaryPage } from '@/PageManager' |
||||||
|
import { useNoteStatsById } from '@/hooks/useNoteStatsById' |
||||||
|
import { formatAmount } from '@/lib/lightning' |
||||||
|
import { toProfile } from '@/lib/link' |
||||||
|
import { useScreenSize } from '@/providers/ScreenSizeProvider' |
||||||
|
import { Zap } from 'lucide-react' |
||||||
|
import { Event } from 'nostr-tools' |
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react' |
||||||
|
import { useTranslation } from 'react-i18next' |
||||||
|
import Content from '../Content' |
||||||
|
import { FormattedTimestamp } from '../FormattedTimestamp' |
||||||
|
import Nip05 from '../Nip05' |
||||||
|
import UserAvatar from '../UserAvatar' |
||||||
|
import Username from '../Username' |
||||||
|
|
||||||
|
const SHOW_COUNT = 20 |
||||||
|
|
||||||
|
export default function ZapList({ event }: { event: Event }) { |
||||||
|
const { t } = useTranslation() |
||||||
|
const { push } = useSecondaryPage() |
||||||
|
const { isSmallScreen } = useScreenSize() |
||||||
|
const noteStats = useNoteStatsById(event.id) |
||||||
|
const filteredZaps = useMemo(() => { |
||||||
|
return (noteStats?.zaps ?? []).sort((a, b) => b.amount - a.amount) |
||||||
|
}, [noteStats, event.id]) |
||||||
|
|
||||||
|
const [showCount, setShowCount] = useState(SHOW_COUNT) |
||||||
|
const bottomRef = useRef<HTMLDivElement | null>(null) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!bottomRef.current || filteredZaps.length <= showCount) return |
||||||
|
const obs = new IntersectionObserver( |
||||||
|
([entry]) => { |
||||||
|
if (entry.isIntersecting) setShowCount((c) => c + SHOW_COUNT) |
||||||
|
}, |
||||||
|
{ rootMargin: '10px', threshold: 0.1 } |
||||||
|
) |
||||||
|
obs.observe(bottomRef.current) |
||||||
|
return () => obs.disconnect() |
||||||
|
}, [filteredZaps.length, showCount]) |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="min-h-[80vh]"> |
||||||
|
{filteredZaps.slice(0, showCount).map((zap) => ( |
||||||
|
<div |
||||||
|
key={zap.pr} |
||||||
|
className="px-4 py-3 border-b transition-colors clickable flex gap-2" |
||||||
|
onClick={() => push(toProfile(zap.pubkey))} |
||||||
|
> |
||||||
|
<div className="w-8 flex flex-col items-center mt-0.5"> |
||||||
|
<Zap className="text-yellow-400 size-5" /> |
||||||
|
<div className="text-sm font-semibold text-yellow-400">{formatAmount(zap.amount)}</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="flex space-x-2 items-start"> |
||||||
|
<UserAvatar userId={zap.pubkey} size="medium" className="shrink-0 mt-0.5" /> |
||||||
|
<div className="flex-1"> |
||||||
|
<Username |
||||||
|
userId={zap.pubkey} |
||||||
|
className="text-sm font-semibold text-muted-foreground hover:text-foreground max-w-fit truncate" |
||||||
|
skeletonClassName="h-3" |
||||||
|
/> |
||||||
|
<div className="flex items-center gap-1 text-sm text-muted-foreground"> |
||||||
|
<Nip05 pubkey={zap.pubkey} append="·" /> |
||||||
|
<FormattedTimestamp |
||||||
|
timestamp={zap.created_at} |
||||||
|
className="shrink-0" |
||||||
|
short={isSmallScreen} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<Content className="mt-2" content={zap.comment} /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
|
||||||
|
<div ref={bottomRef} /> |
||||||
|
|
||||||
|
<div className="text-sm mt-2 text-center text-muted-foreground"> |
||||||
|
{filteredZaps.length > 0 ? t('No more zaps') : t('No zaps yet')} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
Loading…
Reference in new issue