From 9e549521bfdd5c37f94b915e8a5950df754b0403 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 21 Mar 2026 09:34:54 +0100 Subject: [PATCH] more refactoring --- src/components/NoteBoostBadges/index.tsx | 4 +- src/components/NoteInteractions/Tabs.tsx | 9 +- src/components/NoteInteractions/index.tsx | 14 +- src/components/NoteStats/Likes.tsx | 133 +++++++++++------- src/components/ReactionList/index.tsx | 98 ------------- .../ReplyNoteList/ZapReplyFeedRow.tsx | 56 ++++++++ src/components/ReplyNoteList/index.tsx | 15 ++ src/components/RepostList/index.tsx | 81 ----------- src/components/ZapList/index.tsx | 91 ------------ src/i18n/locales/de.ts | 1 + src/i18n/locales/en.ts | 1 + src/pages/secondary/NotePage/index.tsx | 1 + 12 files changed, 165 insertions(+), 339 deletions(-) delete mode 100644 src/components/ReactionList/index.tsx create mode 100644 src/components/ReplyNoteList/ZapReplyFeedRow.tsx delete mode 100644 src/components/RepostList/index.tsx delete mode 100644 src/components/ZapList/index.tsx diff --git a/src/components/NoteBoostBadges/index.tsx b/src/components/NoteBoostBadges/index.tsx index d2139ecd..0b1fb5b3 100644 --- a/src/components/NoteBoostBadges/index.tsx +++ b/src/components/NoteBoostBadges/index.tsx @@ -1,7 +1,7 @@ +import { ExtendedKind } from '@/constants' import { useNoteStatsById } from '@/hooks/useNoteStatsById' import { shouldHideInteractions } from '@/lib/event-filtering' import { cn } from '@/lib/utils' -import { ExtendedKind } from '@/constants' import { useUserTrust } from '@/providers/UserTrustProvider' import { Event } from 'nostr-tools' import { useMemo } from 'react' @@ -51,7 +51,7 @@ export default function NoteBoostBadges({ event, className }: { event: Event; cl {overflow > 0 ? ( +{overflow} diff --git a/src/components/NoteInteractions/Tabs.tsx b/src/components/NoteInteractions/Tabs.tsx index 897621bb..64543b54 100644 --- a/src/components/NoteInteractions/Tabs.tsx +++ b/src/components/NoteInteractions/Tabs.tsx @@ -2,11 +2,9 @@ import { cn } from '@/lib/utils' import { useTranslation } from 'react-i18next' import { useRef, useEffect, useState } from 'react' -export type TTabValue = 'replies' | 'quotes' | 'reactions' | 'zaps' +export type TTabValue = 'replies' | 'quotes' const TABS = [ { value: 'replies', label: 'Replies' }, - { value: 'zaps', label: 'Zaps' }, - { value: 'reactions', label: 'Reactions' }, { value: 'quotes', label: 'Quotes' } ] as { value: TTabValue; label: string }[] @@ -25,10 +23,7 @@ export function Tabs({ const containerRef = useRef(null) const [indicatorStyle, setIndicatorStyle] = useState({ width: 0, left: 0, top: 0 }) - // Filter tabs based on hideBoostsAndQuotes - const visibleTabs = hideBoostsAndQuotes - ? TABS.filter((tab) => tab.value !== 'boosts' && tab.value !== 'quotes') - : TABS + const visibleTabs = hideQuotesForDiscussion ? TABS.filter((tab) => tab.value !== 'quotes') : TABS useEffect(() => { setTimeout(() => { diff --git a/src/components/NoteInteractions/index.tsx b/src/components/NoteInteractions/index.tsx index dc5cc6ca..e751a5f9 100644 --- a/src/components/NoteInteractions/index.tsx +++ b/src/components/NoteInteractions/index.tsx @@ -5,9 +5,7 @@ import { Event } from 'nostr-tools' import { useState } from 'react' import HideUntrustedContentButton from '../HideUntrustedContentButton' import QuoteList from '../QuoteList' -import ReactionList from '../ReactionList' import ReplyNoteList from '../ReplyNoteList' -import ZapList from '../ZapList' import { Tabs, TTabValue } from './Tabs' import ReplySort, { ReplySortOption } from './ReplySort' @@ -36,16 +34,6 @@ export default function NoteInteractions({ if (isDiscussion) return null // Hide quotes for discussions list = break - case 'reactions': - list = - break - case 'boosts': - if (isDiscussion) return null // Hide boosts for discussions - list = - break - case 'zaps': - list = - break default: break } @@ -54,7 +42,7 @@ export default function NoteInteractions({ <>
- +
{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 } /> +