-
+
{type === 'replies' && isDiscussion && (
diff --git a/src/components/NoteStats/Likes.tsx b/src/components/NoteStats/Likes.tsx
index 314b3127..6c01a60b 100644
--- a/src/components/NoteStats/Likes.tsx
+++ b/src/components/NoteStats/Likes.tsx
@@ -1,20 +1,25 @@
+import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
+import { ExtendedKind } from '@/constants'
import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { shouldHideInteractions } from '@/lib/event-filtering'
import { createReactionDraftEvent } from '@/lib/draft-event'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
+import { useUserTrust } from '@/providers/UserTrustProvider'
import noteStatsService from '@/services/note-stats.service'
import { TEmoji } from '@/types'
import { Loader } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo, useRef, useState } from 'react'
import Emoji from '../Emoji'
+import Username from '../Username'
import logger from '@/lib/logger'
export default function Likes({ event }: { event: Event }) {
const inQuietMode = shouldHideInteractions(event)
const { pubkey, checkLogin, publish } = useNostr()
+ const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const noteStats = useNoteStatsById(event.id)
const [liking, setLiking] = useState
(null)
const longPressTimerRef = useRef(null)
@@ -22,9 +27,16 @@ export default function Likes({ event }: { event: Event }) {
const [isCompleted, setIsCompleted] = useState(null)
const likes = useMemo(() => {
- const _likes = noteStats?.likes
+ let _likes = noteStats?.likes
if (!_likes) return []
+ if (event.kind === ExtendedKind.DISCUSSION) {
+ _likes = _likes.filter((item) => item.emoji === '⬆️' || item.emoji === '⬇️')
+ }
+ if (hideUntrustedInteractions) {
+ _likes = _likes.filter((item) => isUserTrusted(item.pubkey))
+ }
+
const stats = new Map }>()
_likes.forEach((item) => {
// In quiet mode, normalize all emojis to "+" to prevent trolling with funny emojis
@@ -42,8 +54,10 @@ export default function Likes({ event }: { event: Event }) {
stats.get(key)?.pubkeys.add(item.pubkey)
}
})
- return Array.from(stats.values()).sort((a, b) => b.pubkeys.size - a.pubkeys.size)
- }, [noteStats, event, inQuietMode])
+ return Array.from(stats.values())
+ .filter((g) => g.pubkeys.size > 0)
+ .sort((a, b) => b.pubkeys.size - a.pubkeys.size)
+ }, [noteStats, event, inQuietMode, hideUntrustedInteractions, isUserTrusted])
if (!likes.length) return null
@@ -123,53 +137,78 @@ export default function Likes({ event }: { event: Event }) {
return (
- {likes.map(({ key, emoji, pubkeys }) => (
-
e.stopPropagation()}
- onMouseDown={() => handleMouseDown(key)}
- onMouseUp={handleMouseUp}
- onMouseLeave={handleMouseLeave}
- onTouchStart={() => handleMouseDown(key)}
- onTouchMove={handleTouchMove}
- onTouchEnd={handleMouseUp}
- onTouchCancel={handleMouseLeave}
- >
- {(isLongPressing === key || isCompleted === key) && (
-
- )}
-
- {liking === key ? (
-
- ) : (
+ {likes.map(({ key, emoji, pubkeys }) => {
+ const contributorIds = Array.from(pubkeys).sort()
+ return (
+
+
e.stopPropagation()}
+ onMouseDown={() => handleMouseDown(key)}
+ onMouseUp={handleMouseUp}
+ onMouseLeave={handleMouseLeave}
+ onTouchStart={() => handleMouseDown(key)}
+ onTouchMove={handleTouchMove}
+ onTouchEnd={handleMouseUp}
+ onTouchCancel={handleMouseLeave}
>
-
+ {(isLongPressing === key || isCompleted === key) && (
+
+ )}
+
+ {liking === key ? (
+
+ ) : (
+
+
+
+ )}
+
{pubkeys.size}
+
- )}
- {pubkeys.size}
-
-
- ))}
+
+
e.stopPropagation()}
+ >
+
+
+ {contributorIds.map((userId) => (
+
+ ))}
+
+
+
+
+ )
+ })}
diff --git a/src/components/ReactionList/index.tsx b/src/components/ReactionList/index.tsx
deleted file mode 100644
index f3f848de..00000000
--- a/src/components/ReactionList/index.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import { useSecondaryPage } from '@/PageManager'
-import { ExtendedKind } from '@/constants'
-import { useNoteStatsById } from '@/hooks/useNoteStatsById'
-import { shouldHideInteractions } from '@/lib/event-filtering'
-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 inQuietMode = shouldHideInteractions(event)
- const { t } = useTranslation()
- const { push } = useSecondaryPage()
- const { isSmallScreen } = useScreenSize()
- const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
- const noteStats = useNoteStatsById(event.id)
- const filteredLikes = useMemo(() => {
- let likes = noteStats?.likes ?? []
-
- // For discussion events (kind 11), only show up/down arrow reactions
- if (event.kind === ExtendedKind.DISCUSSION) {
- likes = likes.filter(like => like.emoji === '⬆️' || like.emoji === '⬇️')
- }
-
- return likes
- .filter((like) => !hideUntrustedInteractions || isUserTrusted(like.pubkey))
- .sort((a, b) => b.created_at - a.created_at)
- }, [noteStats, event.id, hideUntrustedInteractions, isUserTrusted, event.kind])
-
- const [showCount, setShowCount] = useState(SHOW_COUNT)
- const bottomRef = useRef(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 (
-
- {filteredLikes.slice(0, showCount).map((like) => (
-
push(toProfile(like.pubkey))}
- >
-
-
-
-
-
-
-
-
- ))}
-
-
-
-
- {filteredLikes.length > 0 ? t('No more reactions') : t('No reactions yet')}
-
-
- )
-}
diff --git a/src/components/ReplyNoteList/ZapReplyFeedRow.tsx b/src/components/ReplyNoteList/ZapReplyFeedRow.tsx
new file mode 100644
index 00000000..4267afb9
--- /dev/null
+++ b/src/components/ReplyNoteList/ZapReplyFeedRow.tsx
@@ -0,0 +1,56 @@
+import Content from '@/components/Content'
+import { FormattedTimestamp } from '@/components/FormattedTimestamp'
+import Nip05 from '@/components/Nip05'
+import UserAvatar from '@/components/UserAvatar'
+import Username from '@/components/Username'
+import { formatAmount } from '@/lib/lightning'
+import { toProfile } from '@/lib/link'
+import { useSecondaryPage } from '@/PageManager'
+import { useScreenSize } from '@/providers/ScreenSizeProvider'
+import type { TNoteStats } from '@/services/note-stats.service'
+import { Zap } from 'lucide-react'
+import { useTranslation } from 'react-i18next'
+
+export type TZapFeedEntry = TNoteStats['zaps'][number]
+
+export default function ZapReplyFeedRow({ zap }: { zap: TZapFeedEntry }) {
+ const { t } = useTranslation()
+ const { push } = useSecondaryPage()
+ const { isSmallScreen } = useScreenSize()
+
+ return (
+ push(toProfile(zap.pubkey))}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ {formatAmount(zap.amount)} {t('sats')}
+
+
+ ·
+
+
+
+
+
+
+ {zap.comment ?
: null}
+
+
+
+ )
+}
diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx
index 65147571..d256c6a0 100644
--- a/src/components/ReplyNoteList/index.tsx
+++ b/src/components/ReplyNoteList/index.tsx
@@ -9,6 +9,7 @@ import {
isReplaceableEvent,
isReplyNoteEvent
} from '@/lib/event'
+import { shouldHideInteractions } from '@/lib/event-filtering'
import logger from '@/lib/logger'
import { toNote } from '@/lib/link'
import { generateBech32IdFromETag, tagNameEquals } from '@/lib/tag'
@@ -25,10 +26,12 @@ import noteStatsService from '@/services/note-stats.service'
import discussionFeedCache from '@/services/discussion-feed-cache.service'
import { buildReplyReadRelayList } from '@/lib/relay-list-builder'
import { Filter, Event as NEvent, kinds } from 'nostr-tools'
+import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { LoadingBar } from '../LoadingBar'
import ReplyNote, { ReplyNoteSkeleton } from '../ReplyNote'
+import ZapReplyFeedRow from './ZapReplyFeedRow'
type TRootInfo =
| { type: 'E'; id: string; pubkey: string }
@@ -43,6 +46,7 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
const { navigateToNote } = useSmartNoteNavigation()
const { currentIndex } = useSecondaryPage()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
+ const noteStats = useNoteStatsById(event.id)
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const { relayList: userRelayList, pubkey: userPubkey } = useNostr()
@@ -179,6 +183,14 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
return replyEvents.sort((a, b) => b.created_at - a.created_at)
}
}, [event.id, repliesMap, mutePubkeySet, hideContentMentioningMutedUsers, sort])
+
+ const zapsForFeed = useMemo(() => {
+ if (shouldHideInteractions(event)) return []
+ const raw = noteStats?.zaps ?? []
+ const filtered = hideUntrustedInteractions ? raw.filter((z) => isUserTrusted(z.pubkey)) : raw
+ return [...filtered].sort((a, b) => b.amount - a.amount)
+ }, [event, noteStats, hideUntrustedInteractions, isUserTrusted])
+
const [timelineKey] = useState(undefined)
const [until, setUntil] = useState(undefined)
const [loading, setLoading] = useState(false)
@@ -470,6 +482,9 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
return (
{loading &&
}
+ {zapsForFeed.map((zap) => (
+
+ ))}
{!loading && until && (
{
- 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
(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 (
-
- {filteredReposts.slice(0, showCount).map((repost) => (
-
push(toProfile(repost.pubkey))}
- >
-
-
-
-
-
-
- ))}
-
-
-
-
- {filteredReposts.length > 0 ? t('No more boosts') : t('No boosts yet')}
-
-
- )
-}
diff --git a/src/components/ZapList/index.tsx b/src/components/ZapList/index.tsx
deleted file mode 100644
index 4eb22fc4..00000000
--- a/src/components/ZapList/index.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import { useSecondaryPage } from '@/PageManager'
-import { useNoteStatsById } from '@/hooks/useNoteStatsById'
-import { shouldHideInteractions } from '@/lib/event-filtering'
-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 inQuietMode = shouldHideInteractions(event)
-
- // Hide zap receipts in quiet mode as they contain emojis and text
- if (inQuietMode) {
- return null
- }
- 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(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 (
-
- {filteredZaps.slice(0, showCount).map((zap) => (
-
push(toProfile(zap.pubkey))}
- >
-
-
-
{formatAmount(zap.amount)}
-
-
-
-
- ))}
-
-
-
-
- {filteredZaps.length > 0 ? t('No more zaps') : t('No zaps yet')}
-
-
- )
-}
diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts
index 7273d475..25c8cb35 100644
--- a/src/i18n/locales/de.ts
+++ b/src/i18n/locales/de.ts
@@ -498,6 +498,7 @@ export default {
'No zaps yet': 'Noch keine Zaps',
'No more boosts': 'Keine weiteren Boosts',
'No boosts yet': 'Noch keine Boosts',
+ 'n more boosts': '{{count}} weitere Boosts',
Boosts: 'Boosts',
FollowListNotFoundConfirmation:
'Folgeliste nicht gefunden. Möchten Sie eine neue erstellen? Wenn Sie zuvor Benutzer gefolgt haben, bestätigen Sie bitte NICHT, da diese Operation dazu führt, dass Sie Ihre vorherige Folgeliste verlieren.',
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 98a64a80..9779a724 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -568,6 +568,7 @@ export default {
'No zaps yet': 'No zaps yet',
'No more boosts': 'No more boosts',
'No boosts yet': 'No boosts yet',
+ 'n more boosts': '{{count}} more boosts',
Boosts: 'Boosts',
FollowListNotFoundConfirmation:
'Follow list not found. Do you want to create a new one? If you have followed users before, please DO NOT confirm as this operation will cause you to lose your previous follow list.',
diff --git a/src/pages/secondary/NotePage/index.tsx b/src/pages/secondary/NotePage/index.tsx
index 413309b0..1de951da 100644
--- a/src/pages/secondary/NotePage/index.tsx
+++ b/src/pages/secondary/NotePage/index.tsx
@@ -493,6 +493,7 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false, initialEvent }:
: undefined
}
/>
+