From 51bfb0e48dc44092502c57eaaef18e2479f5dd36 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Fri, 22 May 2026 13:17:33 +0200 Subject: [PATCH] bug-fixes --- src/components/NormalFeed/index.tsx | 10 +++- src/components/NoteCard/MainNoteCard.tsx | 5 +- src/components/NoteCard/RepostNoteCard.tsx | 5 +- src/components/NoteCard/index.tsx | 7 ++- src/components/NoteList/index.tsx | 36 +++++++++++- src/components/NoteStats/SeenOnButton.tsx | 18 ++++-- src/components/NoteStats/index.tsx | 21 +++++-- src/lib/relay-allowlist.test.ts | 34 +++++++++++ src/lib/relay-allowlist.ts | 58 +++++++++++++++++++ src/lib/relay-url-priority.ts | 2 +- src/pages/primary/NoteListPage/RelaysFeed.tsx | 2 + src/services/note-stats.service.ts | 46 +++++++++++++-- 12 files changed, 224 insertions(+), 20 deletions(-) create mode 100644 src/lib/relay-allowlist.test.ts create mode 100644 src/lib/relay-allowlist.ts diff --git a/src/components/NormalFeed/index.tsx b/src/components/NormalFeed/index.tsx index 48f803e0..7e3a5ec7 100644 --- a/src/components/NormalFeed/index.tsx +++ b/src/components/NormalFeed/index.tsx @@ -124,6 +124,10 @@ const NormalFeed = forwardRef(function NormalFeed( { subRequests, @@ -160,7 +164,9 @@ const NormalFeed = forwardRef ) : null} {!embedded && bottomNoteLabel ? ( diff --git a/src/components/NoteCard/RepostNoteCard.tsx b/src/components/NoteCard/RepostNoteCard.tsx index 6dca60b7..3ae72254 100644 --- a/src/components/NoteCard/RepostNoteCard.tsx +++ b/src/components/NoteCard/RepostNoteCard.tsx @@ -16,7 +16,8 @@ export default function RepostNoteCard({ filterMutedNotes = true, pinned = false, bottomNoteLabel, - deferAuthorAvatar = true + deferAuthorAvatar = true, + seenOnAllowlist }: { event: Event className?: string @@ -24,6 +25,7 @@ export default function RepostNoteCard({ pinned?: boolean bottomNoteLabel?: string deferAuthorAvatar?: boolean + seenOnAllowlist?: readonly string[] }) { const { mutePubkeySet } = useMuteList() const { hideContentMentioningMutedUsers } = useContentPolicy() @@ -102,6 +104,7 @@ export default function RepostNoteCard({ pinned={pinned} bottomNoteLabel={bottomNoteLabel} deferAuthorAvatar={deferAuthorAvatar} + seenOnAllowlist={seenOnAllowlist} /> ) } diff --git a/src/components/NoteCard/index.tsx b/src/components/NoteCard/index.tsx index 7ce571fc..6021c00d 100644 --- a/src/components/NoteCard/index.tsx +++ b/src/components/NoteCard/index.tsx @@ -18,7 +18,8 @@ const NoteCard = memo(function NoteCard({ bottomNoteLabel, fetchNoteStatsIfMissing = true, deferAuthorAvatar = true, - searchListPreview = false + searchListPreview = false, + seenOnAllowlist }: { event: Event className?: string @@ -31,6 +32,7 @@ const NoteCard = memo(function NoteCard({ fetchNoteStatsIfMissing?: boolean deferAuthorAvatar?: boolean searchListPreview?: boolean + seenOnAllowlist?: readonly string[] }) { const { mutePubkeySet } = useMuteList() const { hideContentMentioningMutedUsers } = useContentPolicy() @@ -57,6 +59,7 @@ const NoteCard = memo(function NoteCard({ pinned={pinned} bottomNoteLabel={bottomNoteLabel} deferAuthorAvatar={deferAuthorAvatar} + seenOnAllowlist={seenOnAllowlist} /> ) } @@ -70,6 +73,7 @@ const NoteCard = memo(function NoteCard({ fetchNoteStatsIfMissing={fetchNoteStatsIfMissing} deferAuthorAvatar={deferAuthorAvatar} searchListPreview={searchListPreview} + seenOnAllowlist={seenOnAllowlist} /> ) }, (prevProps, nextProps) => { @@ -83,6 +87,7 @@ const NoteCard = memo(function NoteCard({ prevProps.hideParentNotePreview === nextProps.hideParentNotePreview && prevProps.bottomNoteLabel === nextProps.bottomNoteLabel && prevProps.fetchNoteStatsIfMissing === nextProps.fetchNoteStatsIfMissing && + prevProps.seenOnAllowlist === nextProps.seenOnAllowlist && prevProps.deferAuthorAvatar === nextProps.deferAuthorAvatar && prevProps.searchListPreview === nextProps.searchListPreview ) diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index 1b63e4f9..2c2ecbf2 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -23,6 +23,7 @@ import { isSpellSubRequestsSameFiltersDifferentRelays } from '@/lib/spell-feed-request-identity' import logger from '@/lib/logger' +import { eventSeenOnMatchesAllowlist } from '@/lib/relay-allowlist' import { isLocalNetworkUrl, normalizeUrl } from '@/lib/url' import { eventPassesNoteListKindPicker } from '@/lib/feed-kind-filter' import { collectLocalEventsForTextSearch } from '@/lib/local-nip50-search-merge' @@ -671,6 +672,10 @@ const NoteList = forwardRef( * unrelated picker churn — stale grid + refresh feeling broken. */ homeFeedListMode, + /** Home favorites: relays allowed for “Seen on” + stats on the Notes tab (favorites + trending). */ + homeFeedSeenOnAllowlistOp, + /** Home favorites: wider stack for Replies / Gallery (adds NIP-65, cache, HTTP index). */ + homeFeedSeenOnAllowlistReplies, /** Spells page: bumps when user picks a feed; used with {@link onSpellFeedFirstPaint}. */ spellFeedInstrumentToken, /** Spells page: fired once when the filtered list first has rows after a picker change. */ @@ -788,6 +793,8 @@ const NoteList = forwardRef( followingFeedDeltaSubRequests?: TFeedSubRequest[] feedTimelineScopeKey?: string homeFeedListMode?: TNoteListMode + homeFeedSeenOnAllowlistOp?: string[] + homeFeedSeenOnAllowlistReplies?: string[] spellFeedInstrumentToken?: number onSpellFeedFirstPaint?: (detail: { eventCount: number; firstEventId: string }) => void timelineLoadingSafetyTimeoutMs?: number @@ -1041,6 +1048,19 @@ const NoteList = forwardRef( const timelineSubscriptionKey = feedSubscriptionKey ?? subRequestsKey + const homeFeedActiveSeenOnAllowlist = useMemo(() => { + if (feedSubscriptionKey !== 'home-all-favorites') return undefined + if (homeFeedListMode === 'postsAndReplies' || homeFeedListMode === 'media') { + return homeFeedSeenOnAllowlistReplies?.length ? homeFeedSeenOnAllowlistReplies : undefined + } + return homeFeedSeenOnAllowlistOp?.length ? homeFeedSeenOnAllowlistOp : undefined + }, [ + feedSubscriptionKey, + homeFeedListMode, + homeFeedSeenOnAllowlistOp, + homeFeedSeenOnAllowlistReplies + ]) + const prevSubRequestsKeyForTimelineRef = useRef(null) const feedTimelineScopePrevRef = useRef(undefined) /** Detect pull-to-refresh so preserve-mode feeds still clear; unrelated dep changes must not clear. */ @@ -1241,6 +1261,17 @@ const NoteList = forwardRef( if (extraShouldHideEvent?.(evt)) return true + if ( + homeFeedActiveSeenOnAllowlist && + homeFeedListMode === 'posts' && + !eventSeenOnMatchesAllowlist( + client.getSeenEventRelayUrls(evt.id), + homeFeedActiveSeenOnAllowlist + ) + ) { + return true + } + return false }, [ @@ -1252,7 +1283,9 @@ const NoteList = forwardRef( pinnedEventIds, isEventDeleted, zapReplyThreshold, - extraShouldHideEvent + extraShouldHideEvent, + homeFeedActiveSeenOnAllowlist, + homeFeedListMode ] ) @@ -4481,6 +4514,7 @@ const NoteList = forwardRef( filterMutedNotes={filterMutedNotes} bottomNoteLabel={eventReasonLabelMap.get(event.id)} deferAuthorAvatar + seenOnAllowlist={homeFeedActiveSeenOnAllowlist} /> )) )} diff --git a/src/components/NoteStats/SeenOnButton.tsx b/src/components/NoteStats/SeenOnButton.tsx index 3d94a7bd..a5cceb00 100644 --- a/src/components/NoteStats/SeenOnButton.tsx +++ b/src/components/NoteStats/SeenOnButton.tsx @@ -10,6 +10,7 @@ import { DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { toRelay } from '@/lib/link' +import { filterRelaysToUserAllowlist } from '@/lib/relay-allowlist' import { simplifyUrl } from '@/lib/url' import { useScreenSize } from '@/providers/ScreenSizeProvider' import client from '@/services/client.service' @@ -19,7 +20,14 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import RelayIcon from '../RelayIcon' -export default function SeenOnButton({ event }: { event: Event }) { +export default function SeenOnButton({ + event, + /** When set (home favorites feed), only list relays from the feed allowlist. */ + allowedRelays +}: { + event: Event + allowedRelays?: readonly string[] +}) { const { t } = useTranslation() const { isSmallScreen } = useScreenSize() const { push } = useSecondaryPage() @@ -32,8 +40,10 @@ export default function SeenOnButton({ event }: { event: Event }) { const maxAttempts = 20 const apply = () => { const seenOn = client.getSeenEventRelayUrls(event.id) - if (!cancelled) setRelays(seenOn) - return seenOn.length > 0 + const visible = + allowedRelays?.length ? filterRelaysToUserAllowlist(seenOn, allowedRelays) : seenOn + if (!cancelled) setRelays(visible) + return visible.length > 0 } if (apply()) return const id = setInterval(() => { @@ -45,7 +55,7 @@ export default function SeenOnButton({ event }: { event: Event }) { cancelled = true clearInterval(id) } - }, [event.id]) + }, [event.id, allowedRelays]) const trigger = (