diff --git a/src/PageManager.tsx b/src/PageManager.tsx index d97f8bd5..794f79ad 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -106,9 +106,6 @@ const PrimaryPinListPageLazy = lazy(() => import('@/pages/secondary/PinListPage' const PrimaryInterestListPageLazy = lazy(() => import('@/pages/secondary/InterestListPage')) const PrimaryUserEmojiListPageLazy = lazy(() => import('@/pages/secondary/UserEmojiListPage')) const PrimaryOthersRelaySettingsPageLazy = lazy(() => import('@/pages/secondary/OthersRelaySettingsPage')) -const PrimaryProfileInteractionDiagramPageLazy = lazy( - () => import('@/pages/secondary/ProfileInteractionDiagramPage') -) const SecondaryRelayPageLazy = lazy(() => import('@/pages/secondary/RelayPage')) function suspensePrimaryPage(page: ReactElement) { @@ -924,29 +921,6 @@ export function useSmartOthersRelaySettingsNavigation() { return { navigateToOthersRelaySettings } } -export function useSmartProfileInteractionsNavigation() { - const { setPrimaryNoteView } = usePrimaryNoteView() - const { push: pushSecondaryPage } = useSecondaryPage() - const { isSmallScreen } = useScreenSize() - - const navigateToProfileInteractions = (url: string) => { - if (isSmallScreen) { - const profileId = url.replace('/users/', '').replace('/interactions', '') - window.history.pushState(null, '', url) - setPrimaryNoteView( - suspensePrimaryPage( - - ), - 'profile-interactions' - ) - } else { - pushSecondaryPage(url) - } - } - - return { navigateToProfileInteractions } -} - /** Settings index is a normal primary page; sub-routes open on the secondary stack (panel / drawer). */ export function useSmartSettingsNavigation() { const { navigate: navigatePrimary } = usePrimaryPage() @@ -1887,8 +1861,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { } if ( primaryViewType === 'following' || - primaryViewType === 'others-relay-settings' || - primaryViewType === 'profile-interactions' + primaryViewType === 'others-relay-settings' ) { const currentPath = window.location.pathname.split('?')[0].split('#')[0] const segs = currentPath.split('/').filter(Boolean) diff --git a/src/components/Explore/ExploreFavoriteRelays.tsx b/src/components/Explore/ExploreFavoriteRelays.tsx index 2a373da8..266ac175 100644 --- a/src/components/Explore/ExploreFavoriteRelays.tsx +++ b/src/components/Explore/ExploreFavoriteRelays.tsx @@ -6,7 +6,6 @@ import { toRelay, toRelaySettings } from '@/lib/link' import { normalizeUrl, simplifyUrl } from '@/lib/url' import { usePrimaryPage } from '@/contexts/primary-page-context' import { useSecondaryPage, useSmartRelayNavigation } from '@/PageManager' -import { useFeed } from '@/providers/FeedProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { cn } from '@/lib/utils' import { Newspaper, Settings } from 'lucide-react' @@ -61,7 +60,6 @@ export default function ExploreFavoriteRelays() { const { t } = useTranslation() const { navigate } = usePrimaryPage() const { push } = useSecondaryPage() - const { switchFeed } = useFeed() const { favoriteRelays, blockedRelays } = useFavoriteRelays() const blockedSet = useMemo( @@ -99,9 +97,7 @@ export default function ExploreFavoriteRelays() { variant="outline" size="sm" className="h-8 gap-1.5 px-2.5 font-medium" - onClick={() => { - void switchFeed('all-favorites').then(() => navigate('feed')) - }} + onClick={() => navigate('feed')} > {t('Favorite Relays')} diff --git a/src/components/PostEditor/PostContent.tsx b/src/components/PostEditor/PostContent.tsx index c78091fd..93555f0b 100644 --- a/src/components/PostEditor/PostContent.tsx +++ b/src/components/PostEditor/PostContent.tsx @@ -45,10 +45,9 @@ import { } from '@/constants' import { cn } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' -import { useFeed } from '@/providers/FeedProvider' import { useReply } from '@/providers/ReplyProvider' import { canonicalizeRssArticleUrl, getArticleUrlFromCommentITags } from '@/lib/rss-article' -import { cleanUrl, normalizeUrl, rewritePlainTextHttpUrls } from '@/lib/url' +import { cleanUrl, rewritePlainTextHttpUrls } from '@/lib/url' import logger from '@/lib/logger' import { LoginRequiredError } from '@/lib/nostr-errors' import postEditorCache from '@/services/post-editor-cache.service' @@ -196,7 +195,6 @@ export default function PostContent({ const { t, i18n } = useTranslation() const { pubkey, publish, checkLogin } = useNostr() const { userGroups } = useGroupList() - const { feedInfo } = useFeed() const { addReplies } = useReply() const mergePublishedReplyIntoThread = useCallback( @@ -1363,21 +1361,6 @@ export default function PostContent({ }) // console.log('Published event:', newEvent) - // Check if we need to refresh the current relay view - if (feedInfo.feedType === 'relay' && feedInfo.id) { - const currentRelayUrl = normalizeUrl(feedInfo.id) - const publishedRelays = additionalRelayUrls - - // If we published to the current relay being viewed, trigger a refresh after a short delay - if (publishedRelays.some(url => normalizeUrl(url) === currentRelayUrl)) { - setTimeout(() => { - // Trigger a page refresh by dispatching a custom event that the relay view can listen to - window.dispatchEvent(new CustomEvent('relay-refresh-needed', { - detail: { relayUrl: currentRelayUrl } - })) - }, 1000) // 1 second delay to allow the event to propagate - } - } // Show publishing feedback if ((newEvent as any).relayStatuses) { diff --git a/src/components/Profile/ProfileBadgeDetailDialog.tsx b/src/components/Profile/ProfileBadgeDetailDialog.tsx deleted file mode 100644 index 7e6bf61e..00000000 --- a/src/components/Profile/ProfileBadgeDetailDialog.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import UserAvatar from '@/components/UserAvatar' -import Username from '@/components/Username' -import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle -} from '@/components/ui/dialog' -import { ScrollArea } from '@/components/ui/scroll-area' -import type { TProfileBadge } from '@/hooks/useProfileBadges' -import { fetchBadgeRecipientPubkeys } from '@/lib/fetch-badge-recipient-pubkeys' -import { toNote, toProfile } from '@/lib/link' -import { hexPubkeysEqual } from '@/lib/pubkey' -import { useSecondaryPage } from '@/PageManager' -import { useEffect, useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' - -function parseIssuerPubkeyFromATag(aTag: string): string | undefined { - const parts = aTag.split(':') - if (parts.length < 2) return undefined - const pk = parts[1] - return /^[0-9a-f]{64}$/i.test(pk) ? pk.toLowerCase() : undefined -} - -export default function ProfileBadgeDetailDialog({ - open, - onOpenChange, - badge, - profilePubkey, - relayUrls -}: { - open: boolean - onOpenChange: (open: boolean) => void - badge: TProfileBadge | null - profilePubkey: string - relayUrls: string[] -}) { - const { t } = useTranslation() - const { push } = useSecondaryPage() - /** Secondary panel is below dialog z-index; close modal before navigating. */ - const pushSecondaryAndClose = (path: string) => { - onOpenChange(false) - push(path) - } - const [recipientPubkeys, setRecipientPubkeys] = useState([]) - const [recipientsLoading, setRecipientsLoading] = useState(false) - const [recipientsError, setRecipientsError] = useState(false) - - const issuerPubkey = useMemo(() => (badge ? parseIssuerPubkeyFromATag(badge.a) : undefined), [badge]) - - const displayImage = badge?.image ?? badge?.thumb - const displayThumb = badge?.thumb ?? badge?.image - const label = badge?.name ?? badge?.a.split(':').pop() ?? '' - - useEffect(() => { - if (!open || !badge) { - setRecipientPubkeys([]) - setRecipientsError(false) - setRecipientsLoading(false) - return - } - - if (relayUrls.length === 0) { - setRecipientPubkeys([]) - setRecipientsError(true) - return - } - - let cancelled = false - setRecipientsLoading(true) - setRecipientsError(false) - fetchBadgeRecipientPubkeys(relayUrls, badge.a) - .then((pubkeys) => { - if (cancelled) return - pubkeys.sort((a, b) => a.localeCompare(b)) - setRecipientPubkeys(pubkeys) - }) - .catch(() => { - if (!cancelled) { - setRecipientsError(true) - setRecipientPubkeys([]) - } - }) - .finally(() => { - if (!cancelled) setRecipientsLoading(false) - }) - - return () => { - cancelled = true - } - }, [open, badge, relayUrls]) - - const otherRecipients = useMemo( - () => recipientPubkeys.filter((pk) => !hexPubkeysEqual(pk, profilePubkey)), - [recipientPubkeys, profilePubkey] - ) - - if (!badge) return null - - return ( - - - - {t('Badge details')} - {label} - - -
- {displayImage || displayThumb ? ( - {label} - ) : ( -
- {label.slice(0, 3)} -
- )} -
{label}
-
- - {badge.description ? ( -

- {badge.description} -

- ) : null} - - {badge.awardCreatedAt != null ? ( -

- {t('Awarded on', { defaultValue: 'Awarded on' })}{' '} - {new Date(badge.awardCreatedAt * 1000).toLocaleString(undefined, { - dateStyle: 'medium', - timeStyle: 'short' - })} -

- ) : null} - - {issuerPubkey ? ( -
-
{t('Issued by')}
- -
- ) : null} - -
-
{t('Other recipients')}
- {recipientsLoading ? ( -
{t('Loading...')}
- ) : recipientsError ? ( -
{t('Recipients could not be loaded')}
- ) : otherRecipients.length === 0 ? ( -
{t('No other recipients found')}
- ) : ( - -
    - {otherRecipients.map((pk) => ( -
  • - -
  • - ))} -
-
- )} -
- - -
-
- ) -} diff --git a/src/components/Profile/ProfileHeaderInteractions.tsx b/src/components/Profile/ProfileHeaderInteractions.tsx deleted file mode 100644 index ac42d143..00000000 --- a/src/components/Profile/ProfileHeaderInteractions.tsx +++ /dev/null @@ -1,352 +0,0 @@ -import Content from '@/components/Content' -import ReactionEmojiDisplay from '@/components/Note/ReactionEmojiDisplay' -import UserAvatar from '@/components/UserAvatar' -import Username from '@/components/Username' -import ProfileBadgeDetailDialog from './ProfileBadgeDetailDialog' -import { replaceableEventDedupeKey } from '@/lib/event' -import { formatAmount } from '@/lib/lightning' -import { cn } from '@/lib/utils' -import { toNote, toProfile } from '@/lib/link' -import { useSecondaryPage } from '@/PageManager' -import type { TProfileZap } from '@/hooks/useProfileInteractions' -import type { TProfileBadge } from '@/hooks/useProfileBadges' -import type { TProfileFollowPack } from '@/hooks/useProfileFollowPacks' -import { Flag, Zap, MessageCircle, ThumbsDown, ThumbsUp, Users } from 'lucide-react' -import { Skeleton } from '@/components/ui/skeleton' -import { useTranslation } from 'react-i18next' -import { useState } from 'react' -import { Event } from 'nostr-tools' - -type Props = { - profilePubkey: string - badgeRelayUrls: string[] - zaps: TProfileZap[] - reactions: Event[] - comments: Event[] - badges: TProfileBadge[] - followPacks: TProfileFollowPack[] - reports: Event[] - loading: boolean - badgesLoading: boolean - followPacksLoading: boolean - reportsLoading: boolean - /** When false (logged out), the Reports section is omitted — reports use the viewer’s relays only. */ - reportsEnabled: boolean -} - -const ZAPS_PER_ROW = 4 -const ZAP_ROWS = 3 -const MAX_ZAPS = ZAPS_PER_ROW * ZAP_ROWS -const LIKES_GRID_COLS = 4 -const LIKES_GRID_ROWS = 3 -const MAX_LIKES = LIKES_GRID_COLS * LIKES_GRID_ROWS -const BADGES_PER_ROW = 6 -const BADGE_ROWS = 2 -const MAX_BADGES = BADGES_PER_ROW * BADGE_ROWS -const BADGE_TILE_PX = 96 -const MAX_FOLLOW_PACKS = 8 -const MAX_REPORTS = 12 - -function reportSummaryFromEvent(event: Event): string { - const reportTag = event.tags.find((t) => t[0] === 'report') - const reason = reportTag?.[1]?.trim() - if (reason) return reason - const text = event.content.trim().replace(/\s+/g, ' ') - if (text) return text.length > 48 ? `${text.slice(0, 45)}…` : text - return '—' -} - -function ZapBadge({ zap }: { zap: TProfileZap }) { - const { push } = useSecondaryPage() - return ( - - ) -} - -function ReactionBadge({ event }: { event: Event }) { - const { push } = useSecondaryPage() - const raw = event.content.trim() - const isPlus = raw === '+' - const isMinus = raw === '-' - return ( - - ) -} - -function CommentBadge({ event }: { event: Event }) { - const { push } = useSecondaryPage() - return ( - - ) -} - -function ReportBadge({ event }: { event: Event }) { - const { push } = useSecondaryPage() - const summary = reportSummaryFromEvent(event) - return ( - - ) -} - -function FollowPackBadge({ pack }: { pack: TProfileFollowPack }) { - const { t } = useTranslation() - const { push } = useSecondaryPage() - const authorPk = pack.event.pubkey - return ( - - ) -} - -function BadgeItem({ - badge, - onOpenDetail -}: { - badge: TProfileBadge - onOpenDetail: (b: TProfileBadge) => void -}) { - const { t } = useTranslation() - const imageUrl = badge.thumb ?? badge.image - const label = badge.name ?? badge.a.split(':').pop() ?? '' - return ( - - ) -} - -export default function ProfileHeaderInteractions({ - profilePubkey, - badgeRelayUrls, - zaps, - reactions, - comments, - badges, - followPacks, - reports, - loading, - badgesLoading, - followPacksLoading, - reportsLoading, - reportsEnabled -}: Props) { - const { t } = useTranslation() - const [badgeDialogOpen, setBadgeDialogOpen] = useState(false) - const [selectedBadge, setSelectedBadge] = useState(null) - - const displayZaps = zaps.slice(0, MAX_ZAPS) - const displayReactions = reactions.slice(0, MAX_LIKES) - const displayBadges = badges.slice(0, MAX_BADGES) - const displayFollowPacks = followPacks.slice(0, MAX_FOLLOW_PACKS) - const displayReports = reports.slice(0, MAX_REPORTS) - - const Section = ({ - title, - isEmpty, - isLoading, - children, - skeletonCount = 6, - skeletonItemClassName, - skeletonGridClassName - }: { - title: string - isEmpty: boolean - isLoading: boolean - children: React.ReactNode - skeletonCount?: number - skeletonItemClassName?: string - skeletonGridClassName?: string - }) => ( -
-
{title}
- {isLoading && isEmpty ? ( -
- {Array.from({ length: skeletonCount }).map((_, i) => ( - - ))} -
- ) : isEmpty ? ( -
{t('None')}
- ) : ( - children - )} -
- ) - - return ( -
-
-
- {displayZaps.map((item) => ( - - ))} -
-
-
-
- {displayReactions.map((item) => ( - - ))} -
-
-
-
- {comments.map((item) => ( - - ))} -
-
-
-
- {displayBadges.map((badge, index) => ( - { - setSelectedBadge(b) - setBadgeDialogOpen(true) - }} - /> - ))} -
-
- { - setBadgeDialogOpen(o) - if (!o) setSelectedBadge(null) - }} - badge={selectedBadge} - profilePubkey={profilePubkey} - relayUrls={badgeRelayUrls} - /> -
-
- {displayFollowPacks.map((pack) => ( - - ))} -
-
- {reportsEnabled ? ( -
-
- {displayReports.map((item) => ( - - ))} -
-
- ) : null} -
- ) -} diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index 4479203d..2a4d9fc1 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -15,11 +15,11 @@ import { kinds, type NostrEvent } from 'nostr-tools' import { createReactionDraftEvent } from '@/lib/draft-event' import { getPaymentInfoFromEvent } from '@/lib/event-metadata' import { showSimplePublishSuccess, toastPublishPromise } from '@/lib/publishing-feedback' -import { toProfileEditor, toProfileInteractionMap } from '@/lib/link' +import { toProfileEditor } from '@/lib/link' import { generateImageByPubkey } from '@/lib/pubkey' import { isVideo } from '@/lib/url' import { usePrimaryPage } from '@/contexts/primary-page-context' -import { useSecondaryPage, useSmartProfileInteractionsNavigation } from '@/PageManager' +import { useSecondaryPage } from '@/PageManager' import { useNostr } from '@/providers/NostrProvider' import client from '@/services/client.service' import { replaceableEventService } from '@/services/client.service' @@ -42,7 +42,6 @@ import { Link, MessageCircle, ThumbsUp, - LayoutGrid } from 'lucide-react' import { useEffect, @@ -191,7 +190,6 @@ export default function Profile({ }) { const { t } = useTranslation() const { push } = useSecondaryPage() - const { navigateToProfileInteractions } = useSmartProfileInteractionsNavigation() const { navigate: navigatePrimary } = usePrimaryPage() const internalFeedRef = useRef<{ refresh: () => void }>(null) const profileFeedRef = feedRef ?? internalFeedRef @@ -536,12 +534,6 @@ export default function Profile({ {t('Follow Packs')} - navigateToProfileInteractions(toProfileInteractionMap(pubkey))} - > - - {t('interactionMapMenu')} - push(toProfileEditor())}> {t('Edit')} diff --git a/src/components/ProfileOptions/index.tsx b/src/components/ProfileOptions/index.tsx index 0ede6287..a023ae34 100644 --- a/src/components/ProfileOptions/index.tsx +++ b/src/components/ProfileOptions/index.tsx @@ -28,16 +28,13 @@ import { MessageCircle, Send, SatelliteDish, - Video, - LayoutGrid + Video } from 'lucide-react' import { useMemo, useState, useEffect } from 'react' import { createReactionDraftEvent } from '@/lib/draft-event' import PostEditor from '@/components/PostEditor' import { showSimplePublishSuccess, toastPublishPromise } from '@/lib/publishing-feedback' import { useTranslation } from 'react-i18next' -import { useSmartProfileInteractionsNavigation } from '@/PageManager' -import { toProfileInteractionMap } from '@/lib/link' import { toast } from 'sonner' import { Event, kinds } from 'nostr-tools' @@ -56,7 +53,6 @@ export default function ProfileOptions({ onSendCallInvite?: (url: string) => void }) { const { t } = useTranslation() - const { navigateToProfileInteractions } = useSmartProfileInteractionsNavigation() const { pubkey: accountPubkey, profile, publish, checkLogin } = useNostr() const { mutePubkeySet, mutePubkeyPrivately, mutePubkeyPublicly, unmutePubkey } = useMuteList() const { relayUrls: currentBrowsingRelayUrls } = useCurrentRelays() @@ -82,7 +78,7 @@ export default function ProfileOptions({ if (event) { setLocalProfileEvent(event) } - } catch (error) { + } catch { // Silently fail: reply/like stay hidden until the event loads } } @@ -248,12 +244,6 @@ export default function ProfileOptions({ )} - navigateToProfileInteractions(toProfileInteractionMap(pubkey))} - > - - {t('interactionMapMenu')} - navigator.clipboard.writeText('nostr:' + pubkeyToNpub(pubkey))} > diff --git a/src/constants.ts b/src/constants.ts index 3e6df5a0..4275bdf4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -278,7 +278,6 @@ export const StorageKey = { DEFAULT_ZAP_COMMENT: 'defaultZapComment', QUICK_ZAP: 'quickZap', ZAP_REPLY_THRESHOLD: 'zapReplyThreshold', - ACCOUNT_FEED_INFO_MAP: 'accountFeedInfoMap', /** Per-pubkey ms timestamps: last full network hydrate (see ACCOUNT_SESSION_NETWORK_HYDRATE_MIN_INTERVAL_MS). */ ACCOUNT_NETWORK_HYDRATE_AT_MAP: 'accountNetworkHydrateAtMap', AUTOPLAY: 'autoplay', diff --git a/src/contexts/primary-note-view-context.tsx b/src/contexts/primary-note-view-context.tsx index 2c4b560b..348b3dbd 100644 --- a/src/contexts/primary-note-view-context.tsx +++ b/src/contexts/primary-note-view-context.tsx @@ -8,7 +8,6 @@ export type TPrimaryOverlayViewType = | 'hashtag' | 'relay' | 'following' - | 'profile-interactions' | 'mute' | 'bookmarks' | 'pins' diff --git a/src/hooks/useProfileBadges.tsx b/src/hooks/useProfileBadges.tsx deleted file mode 100644 index b8fe0752..00000000 --- a/src/hooks/useProfileBadges.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { ExtendedKind } from '@/constants' -import { extractBadgeDefinitionMedia } from '@/lib/badge-definition-media' -import { - fetchNip58BadgeAward, - fetchNip58BadgeDefinition, - mergeNip58BadgeRelayPool -} from '@/lib/fetch-badge-nip58' -import indexedDb from '@/services/indexed-db.service' -import { Event } from 'nostr-tools' -import { tagNameEquals } from '@/lib/tag' - -export type TProfileBadge = { - /** Badge definition coordinate (e.g. "30009:alice:bravery") */ - a: string - /** Badge award event id */ - awardId: string - /** Human-readable name from definition */ - name?: string - /** High-res image URL */ - image?: string - /** Thumbnail URL (prefer thumb over image for grid display) */ - thumb?: string - /** From badge definition (NIP-58) */ - description?: string - /** Kind 8 award `created_at` when loaded */ - awardCreatedAt?: number -} - -/** Parse a-tag "30009:pubkey:d" into { kind, pubkey, d } */ -function parseATag(aTag: string): { kind: number; pubkey: string; d: string } | null { - const parts = aTag.split(':') - if (parts.length < 3) return null - const kind = parseInt(parts[0], 10) - if (isNaN(kind)) return null - const pk = parts[1] - if (!/^[0-9a-fA-F]{64}$/.test(pk)) return null - const d = parts.slice(2).join(':') - if (!d) return null - return { kind, pubkey: pk.toLowerCase(), d } -} - -function mergeProfileBadgesByAwardId(seed: TProfileBadge[], fresh: TProfileBadge[]): TProfileBadge[] { - const m = new Map() - for (const b of seed) m.set(b.awardId, b) - for (const b of fresh) m.set(b.awardId, b) - return [...m.values()] -} - -export async function enrichBadgesFromIndexedDb(badges: TProfileBadge[]): Promise { - return Promise.all( - badges.map(async (b) => { - if (b.thumb || b.image) return b - const parsed = parseATag(b.a) - if (!parsed || parsed.kind !== ExtendedKind.BADGE_DEFINITION) return b - try { - const def = await indexedDb.getReplaceableEvent(parsed.pubkey, parsed.kind, parsed.d) - if (!def) return b - const name = def.tags.find(tagNameEquals('name'))?.[1] - const description = def.tags.find(tagNameEquals('description'))?.[1] - const media = extractBadgeDefinitionMedia(def) - return { - ...b, - name: name ?? b.name ?? parsed.d, - image: media.image, - thumb: media.thumb ?? media.image, - description: description ?? b.description - } - } catch { - return b - } - }) - ) -} - -/** - * Resolves NIP-58 badge definitions/awards for the newest kind-30008 `profile_badges` event. - * Used by profile accordion bundle fetch. - */ -export async function resolveProfileBadgeList( - profileBadgesEvent: Event | undefined, - urls: string[], - blockedRelays: string[], - seedBadges: TProfileBadge[] | null | undefined -): Promise { - if (!profileBadgesEvent) { - return seedBadges?.length ? [...seedBadges] : [] - } - - const tags = profileBadgesEvent.tags - const pairs: { a: string; e: string; eRelayHint?: string }[] = [] - for (let i = 0; i < tags.length - 1; i++) { - const ta = tags[i] - const te = tags[i + 1] - if ( - ta[0] === 'a' && - te[0] === 'e' && - ta[1] && - te[1] && - /^[a-f0-9]{64}$/i.test(te[1]) - ) { - pairs.push({ a: ta[1], e: te[1], eRelayHint: te[2] }) - } - } - - if (pairs.length === 0) { - return seedBadges?.length ? [...seedBadges] : [] - } - - const result: TProfileBadge[] = await Promise.all( - pairs.map(async ({ a, e, eRelayHint }) => { - const parsed = parseATag(a) - if (!parsed || parsed.kind !== ExtendedKind.BADGE_DEFINITION) { - return { a, awardId: e } - } - - const relayPool = mergeNip58BadgeRelayPool(urls, eRelayHint, blockedRelays) - const [defEvent, awardEvent] = await Promise.all([ - fetchNip58BadgeDefinition(parsed.pubkey, parsed.d, relayPool), - fetchNip58BadgeAward(e, relayPool) - ]) - - const awardATag = awardEvent?.tags.find(tagNameEquals('a'))?.[1] - const awardMatchesDefinition = !awardEvent || awardATag === a - const awardCreatedAt = - awardMatchesDefinition && awardEvent ? awardEvent.created_at : undefined - - if (defEvent) { - try { - await indexedDb.putReplaceableEvent(defEvent) - } catch { - /* ignore */ - } - } - - if (!defEvent) { - return { a, awardId: e, awardCreatedAt } - } - - const name = defEvent.tags.find(tagNameEquals('name'))?.[1] - const description = defEvent.tags.find(tagNameEquals('description'))?.[1] - const media = extractBadgeDefinitionMedia(defEvent) - - return { - a, - awardId: e, - name: name ?? parsed.d, - image: media.image, - thumb: media.thumb ?? media.image, - description, - awardCreatedAt - } - }) - ) - - return mergeProfileBadgesByAwardId(seedBadges ?? [], result) -} diff --git a/src/hooks/useProfileFollowPacks.tsx b/src/hooks/useProfileFollowPacks.tsx deleted file mode 100644 index bae8382d..00000000 --- a/src/hooks/useProfileFollowPacks.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { Event } from 'nostr-tools' - -export type TProfileFollowPack = { - event: Event - title: string -} diff --git a/src/hooks/useProfileInteractions.tsx b/src/hooks/useProfileInteractions.tsx deleted file mode 100644 index dbf97ceb..00000000 --- a/src/hooks/useProfileInteractions.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export type TProfileZap = { - pr: string - pubkey: string - amount: number - created_at: number - comment?: string -} diff --git a/src/hooks/useProfileRelayUrls.tsx b/src/hooks/useProfileRelayUrls.tsx deleted file mode 100644 index b2e8ad59..00000000 --- a/src/hooks/useProfileRelayUrls.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { - profileAccordionGetCachedRelayUrls, - profileAccordionRelayUrlsKey, - profileAccordionSetRelayUrls -} from '@/lib/profile-accordion-session-cache' -import { buildProfileRelayUrls, getProfileRelayUrlsProvisional } from '@/lib/profile-relay-urls' -import { useCallback, useEffect, useRef, useState } from 'react' -import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' - -/** Returns profile relay URLs (outboxes + PROFILE_FETCH). Use for sharing relays across profile fetches. */ -export function useProfileRelayUrls(pubkey: string | undefined, enabled: boolean) { - const { blockedRelays } = useFavoriteRelays() - const blockedRelaysRef = useRef(blockedRelays) - blockedRelaysRef.current = blockedRelays - const blockedRelaysKey = profileAccordionRelayUrlsKey(blockedRelays) - - const [relayUrls, setRelayUrls] = useState([]) - const [loading, setLoading] = useState(false) - /** Stale-while-revalidate: avoid accordion skeleton when refreshing relays but URLs already visible */ - const relayUrlsRef = useRef([]) - relayUrlsRef.current = relayUrls - - const fetch = useCallback( - async (force = false): Promise => { - if (!pubkey) { - setRelayUrls((prev) => (prev.length === 0 ? prev : [])) - setLoading(false) - return [] - } - - if (!force) { - const cached = profileAccordionGetCachedRelayUrls(pubkey) - if (cached?.length) { - setRelayUrls(cached) - setLoading(false) - return cached - } - } - - const provisional = getProfileRelayUrlsProvisional(blockedRelaysRef.current) - const revalidateWithVisibleUrls = force && relayUrlsRef.current.length > 0 - if (!revalidateWithVisibleUrls) { - if (provisional.length > 0) { - profileAccordionSetRelayUrls(pubkey, provisional) - setRelayUrls(provisional) - setLoading(false) - } else { - setLoading(true) - } - } else { - setLoading(true) - } - try { - const urls = await buildProfileRelayUrls(pubkey, blockedRelaysRef.current) - profileAccordionSetRelayUrls(pubkey, urls) - setRelayUrls(urls) - return urls - } catch { - setRelayUrls((prev) => (prev.length === 0 ? prev : [])) - return [] - } finally { - setLoading(false) - } - }, - [pubkey, blockedRelaysKey] - ) - - const refresh = useCallback(() => { - if (!pubkey) return Promise.resolve([] as string[]) - /** Do not invalidate: that wipes interactions/badges/follow-packs cache and forces empty refetches */ - return fetch(true) - }, [pubkey, fetch]) - - useEffect(() => { - if (!pubkey) { - setRelayUrls((prev) => (prev.length === 0 ? prev : [])) - setLoading(false) - return - } - if (!enabled) { - const cached = profileAccordionGetCachedRelayUrls(pubkey) - setRelayUrls((prev) => { - if (cached && cached.length > 0) return cached - if (prev.length === 0) return prev - return [] - }) - setLoading(false) - return - } - void fetch(false) - }, [pubkey, enabled, fetch]) - - return { relayUrls, loading, refresh } -} diff --git a/src/i18n/locales/cs.ts b/src/i18n/locales/cs.ts index 259d6102..a904c411 100644 --- a/src/i18n/locales/cs.ts +++ b/src/i18n/locales/cs.ts @@ -611,7 +611,6 @@ export default { successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", - feedStarting: "Starting feeds and relays… This can take a few seconds after login.", singleRelayKindFallbackNotice: "This relay returned no events for an open-ended request (no kinds in the filter). The feed below uses your usual kind filter instead.", refreshCacheButtonExplainer: "Refresh Cache runs an IndexedDB upgrade check, re-fetches your relay lists and profile-related events from the network (same work as the automatic startup sync), syncs kind-5 deletions into tombstones and removes deleted items from the local cache, then refreshes the store counts below.", "eventArchive.sectionTitle": "Notes & feed archive", diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 5ed601f7..dd3c43ab 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -35,26 +35,6 @@ export default { Profile: "Profil", Logout: "Abmelden", Following: "Folgende", - interactionMapMenu: "Interaktionskarte", - interactionMapTitle: "Interaktionskarte", - interactionMapSubtitle: - "Personen, die dieser Nutzer in Notizen markiert, die lokal schon vorliegen (Sitzungs‑LRU + IndexedDB‑Archiv). Kräftigere Farbe ≈ häufiger erwähnt; hellerer Rand ≈ zuletzt. Nicht vollständig.", - interactionMapSessionNotes: "Sitzungscache: {{count}} ihrer Notizen", - interactionMapArchiveNotes: "Archiv‑Scan: {{count}} ihrer Notizen (begrenzt)", - interactionMapEmpty: - "In gecachten Notizen noch keine markierten Personen. Timeline öffnen oder Feeds lesen, damit das Archiv füllt.", - interactionMapRefresh: "Cache erneut scannen", - interactionMapCellTitle: "{{count}} Erwähnungen · zuletzt {{when}}", - interactionMapIncludeFollows: "Alle meine Follows einblenden", - interactionMapIncludeFollowsHint: - "Zeigt deine komplette Follow-Liste zusammen mit Personen aus ihren gecachten Tags. Bei langen Listen scrollen.", - interactionMapIncludeFollowsBreakdown: - "{{total}} angezeigt — {{fromTags}} aus ihren gecachten Tags, {{fromFollowsOnly}} nur aus deiner Follow-Liste", - interactionMapCellTitleFollowOnly: "Nicht in ihren lokalen Tags — nur deine Follow-Liste", - interactionMapFollowingCheckbox: "Folge ich", - interactionMapMentionsShort: "×{{count}}", - interactionMapRecencyUnknown: "—", - interactionMapScore: "Score {{score}}", followings: "Folgekonten", boosted: "geboostet", "Boosted by:": "Geboostet von:", @@ -631,7 +611,6 @@ export default { successes: "Erfolge", None: "Keine", "Cache & offline storage": "Cache & Offline-Speicher", - feedStarting: "Starting feeds and relays… This can take a few seconds after login.", singleRelayKindFallbackNotice: "Dieses Relay hat auf eine offene Anfrage (ohne kinds im Filter) keine Events geliefert. Der Feed unten nutzt stattdessen deinen gewohnten Kind-Filter.", refreshCacheButtonExplainer: "Refresh Cache runs an IndexedDB upgrade check, re-fetches your relay lists and profile-related events from the network (same work as the automatic startup sync), syncs kind-5 deletions into tombstones and removes deleted items from the local cache, then refreshes the store counts below.", "eventArchive.sectionTitle": "Notes & feed archive", diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index ae452004..1b9adf8c 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -33,26 +33,6 @@ export default { Profile: "Profile", Logout: "Logout", Following: "Following", - interactionMapMenu: "Interaction map", - interactionMapTitle: "Interaction map", - interactionMapSubtitle: - "People this user tags in notes and replies we already have locally (in-memory session + IndexedDB archive). Stronger color ≈ more mentions; brighter border ≈ more recent. Not exhaustive.", - interactionMapSessionNotes: "Session cache: {{count}} of their notes", - interactionMapArchiveNotes: "Archive scan: {{count}} of their notes (capped)", - interactionMapEmpty: - "No tagged people found in cached notes yet. Open their timeline or browse feeds so notes land in the archive.", - interactionMapRefresh: "Rescan cache", - interactionMapCellTitle: "{{count}} mentions · last {{when}}", - interactionMapIncludeFollows: "Include everyone I follow", - interactionMapIncludeFollowsHint: - "Shows your full follow list merged with people from their cached tags. Scroll when the list is long.", - interactionMapIncludeFollowsBreakdown: - "{{total}} shown — {{fromTags}} from their cached tags, {{fromFollowsOnly}} from your follows only", - interactionMapCellTitleFollowOnly: "Not in their cached tags — your follow list only", - interactionMapFollowingCheckbox: "Following", - interactionMapMentionsShort: "×{{count}}", - interactionMapRecencyUnknown: "—", - interactionMapScore: "Score {{score}}", followings: "followings", boosted: "boosted", "Boosted by:": "Boosted by:", @@ -635,7 +615,6 @@ export default { successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", - feedStarting: "Starting feeds and relays… This can take a few seconds after login.", singleRelayKindFallbackNotice: "This relay returned no events for an open-ended request (no kinds in the filter). The feed below uses your usual kind filter instead.", refreshCacheButtonExplainer: "Refresh Cache runs an IndexedDB upgrade check, re-fetches your relay lists and profile-related events from the network (same work as the automatic startup sync), syncs kind-5 deletions into tombstones and removes deleted items from the local cache, then refreshes the store counts below.", "eventArchive.sectionTitle": "Notes & feed archive", diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 7ea0f6cb..6f6e2c28 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -611,7 +611,6 @@ export default { successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", - feedStarting: "Starting feeds and relays… This can take a few seconds after login.", singleRelayKindFallbackNotice: "This relay returned no events for an open-ended request (no kinds in the filter). The feed below uses your usual kind filter instead.", refreshCacheButtonExplainer: "Refresh Cache runs an IndexedDB upgrade check, re-fetches your relay lists and profile-related events from the network (same work as the automatic startup sync), syncs kind-5 deletions into tombstones and removes deleted items from the local cache, then refreshes the store counts below.", "eventArchive.sectionTitle": "Notes & feed archive", diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 4a98186c..a0c485b8 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -611,7 +611,6 @@ export default { successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", - feedStarting: "Starting feeds and relays… This can take a few seconds after login.", singleRelayKindFallbackNotice: "This relay returned no events for an open-ended request (no kinds in the filter). The feed below uses your usual kind filter instead.", refreshCacheButtonExplainer: "Refresh Cache runs an IndexedDB upgrade check, re-fetches your relay lists and profile-related events from the network (same work as the automatic startup sync), syncs kind-5 deletions into tombstones and removes deleted items from the local cache, then refreshes the store counts below.", "eventArchive.sectionTitle": "Notes & feed archive", diff --git a/src/i18n/locales/nl.ts b/src/i18n/locales/nl.ts index 9cd7fbcb..9845b0b9 100644 --- a/src/i18n/locales/nl.ts +++ b/src/i18n/locales/nl.ts @@ -611,7 +611,6 @@ export default { successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", - feedStarting: "Starting feeds and relays… This can take a few seconds after login.", singleRelayKindFallbackNotice: "This relay returned no events for an open-ended request (no kinds in the filter). The feed below uses your usual kind filter instead.", refreshCacheButtonExplainer: "Refresh Cache runs an IndexedDB upgrade check, re-fetches your relay lists and profile-related events from the network (same work as the automatic startup sync), syncs kind-5 deletions into tombstones and removes deleted items from the local cache, then refreshes the store counts below.", "eventArchive.sectionTitle": "Notes & feed archive", diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index c8db277f..bcb7eea6 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -611,7 +611,6 @@ export default { successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", - feedStarting: "Starting feeds and relays… This can take a few seconds after login.", singleRelayKindFallbackNotice: "This relay returned no events for an open-ended request (no kinds in the filter). The feed below uses your usual kind filter instead.", refreshCacheButtonExplainer: "Refresh Cache runs an IndexedDB upgrade check, re-fetches your relay lists and profile-related events from the network (same work as the automatic startup sync), syncs kind-5 deletions into tombstones and removes deleted items from the local cache, then refreshes the store counts below.", "eventArchive.sectionTitle": "Notes & feed archive", diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 92d8179a..2174ffc2 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -611,7 +611,6 @@ export default { successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", - feedStarting: "Starting feeds and relays… This can take a few seconds after login.", singleRelayKindFallbackNotice: "This relay returned no events for an open-ended request (no kinds in the filter). The feed below uses your usual kind filter instead.", refreshCacheButtonExplainer: "Refresh Cache runs an IndexedDB upgrade check, re-fetches your relay lists and profile-related events from the network (same work as the automatic startup sync), syncs kind-5 deletions into tombstones and removes deleted items from the local cache, then refreshes the store counts below.", "eventArchive.sectionTitle": "Notes & feed archive", diff --git a/src/i18n/locales/tr.ts b/src/i18n/locales/tr.ts index eeb20647..e8d4b007 100644 --- a/src/i18n/locales/tr.ts +++ b/src/i18n/locales/tr.ts @@ -611,7 +611,6 @@ export default { successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", - feedStarting: "Starting feeds and relays… This can take a few seconds after login.", singleRelayKindFallbackNotice: "This relay returned no events for an open-ended request (no kinds in the filter). The feed below uses your usual kind filter instead.", refreshCacheButtonExplainer: "Refresh Cache runs an IndexedDB upgrade check, re-fetches your relay lists and profile-related events from the network (same work as the automatic startup sync), syncs kind-5 deletions into tombstones and removes deleted items from the local cache, then refreshes the store counts below.", "eventArchive.sectionTitle": "Notes & feed archive", diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index cb8884f4..3511b668 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -611,7 +611,6 @@ export default { successes: "successes", None: "None", "Cache & offline storage": "Cache & offline storage", - feedStarting: "Starting feeds and relays… This can take a few seconds after login.", singleRelayKindFallbackNotice: "This relay returned no events for an open-ended request (no kinds in the filter). The feed below uses your usual kind filter instead.", refreshCacheButtonExplainer: "Refresh Cache runs an IndexedDB upgrade check, re-fetches your relay lists and profile-related events from the network (same work as the automatic startup sync), syncs kind-5 deletions into tombstones and removes deleted items from the local cache, then refreshes the store counts below.", "eventArchive.sectionTitle": "Notes & feed archive", diff --git a/src/lib/link.ts b/src/lib/link.ts index 932d79db..50dda137 100644 --- a/src/lib/link.ts +++ b/src/lib/link.ts @@ -106,11 +106,6 @@ export const toOthersRelaySettings = (pubkey: string) => { const npub = nip19.npubEncode(pubkey) return `/users/${npub}/relays` } -/** Cached note mentions / tags — session + IndexedDB archive scan (see profile interaction map page). */ -export const toProfileInteractionMap = (pubkeyHex: string) => { - const npub = nip19.npubEncode(pubkeyHex) - return `/users/${npub}/interactions` -} export const toSearch = (params?: TSearchParams) => { if (!params) return '/search' const query = new URLSearchParams() diff --git a/src/lib/profile-accordion-fetch.ts b/src/lib/profile-accordion-fetch.ts deleted file mode 100644 index 6015bfed..00000000 --- a/src/lib/profile-accordion-fetch.ts +++ /dev/null @@ -1,422 +0,0 @@ -/** - * Orchestrated fetch for the profile interactions accordion: phase 1 (zaps, notes, follow packs, - * profile_badges list), then separate batches for comments on notes, comments on profile (#a), and - * profile reactions (#e + #a); badge NIP-58 resolution and reports run after. `onPartial` fires as - * relays return events (coalesced per microtask). Session cache writes stay at completion only. - * Ordering matches the former standalone profile-interactions hook (removed; logic lives here). - */ - -import { ExtendedKind } from '@/constants' -import { getZapInfoFromEvent } from '@/lib/event-metadata' -import { buildProfileReportRelayUrls } from '@/lib/profile-report-relay-urls' -import { - profileAccordionGetCachedBadges, - profileAccordionGetCachedFollowPacks, - profileAccordionGetCachedInteractions, - profileAccordionGetCachedReports, - profileAccordionRelayUrlsKey, - profileAccordionSetBadges, - profileAccordionSetFollowPacks, - profileAccordionSetInteractions, - profileAccordionSetReports -} from '@/lib/profile-accordion-session-cache' -import type { TProfileBadge } from '@/hooks/useProfileBadges' -import { enrichBadgesFromIndexedDb, resolveProfileBadgeList } from '@/hooks/useProfileBadges' -import type { TProfileFollowPack } from '@/hooks/useProfileFollowPacks' -import type { TProfileZap } from '@/hooks/useProfileInteractions' -import { replaceableEventDedupeKey } from '@/lib/event' -import { hexPubkeysEqual } from '@/lib/pubkey' -import { queryService, replaceableEventService } from '@/services/client.service' -import { Event, Filter, kinds } from 'nostr-tools' - -const NOTE_IDS_FOR_COMMENTS = 50 -const REPORT_LIMIT = 50 - -const QUERY_OPTS = { - eoseTimeout: 2500, - globalTimeout: 18_000, - firstRelayResultGraceMs: false -} as const - -export type ProfileAccordionBundle = { - zaps: TProfileZap[] - reactions: Event[] - comments: Event[] - badges: TProfileBadge[] - followPacks: TProfileFollowPack[] - reports: Event[] -} - -function getPackTitle(event: Event): string { - const titleTag = event.tags.find((tag) => tag[0] === 'title' || tag[0] === 'name') - return titleTag?.[1] || 'Follow Pack' -} - -function isProfileBadgesListEvent(pubkey: string, e: Event): boolean { - if (e.kind !== ExtendedKind.PROFILE_BADGES) return false - if (!hexPubkeysEqual(e.pubkey, pubkey)) return false - return e.tags.some((t) => t[0] === 'd' && t[1] === 'profile_badges') -} - -function cacheHydrated( - pubkey: string, - relayKey: string, - viewerPubkey: string | null | undefined -): ProfileAccordionBundle | null { - const zi = profileAccordionGetCachedInteractions(pubkey, relayKey) - const zb = profileAccordionGetCachedBadges(pubkey, relayKey) - const zf = profileAccordionGetCachedFollowPacks(pubkey, relayKey) - const viewer = viewerPubkey?.trim() - const reportsReady = !viewer || profileAccordionGetCachedReports(pubkey, viewer) !== undefined - if (!zi || zb === undefined || zf === undefined || !reportsReady) return null - const reports = - viewer ? profileAccordionGetCachedReports(pubkey, viewer) ?? [] : [] - return { - zaps: zi.zaps, - reactions: zi.reactions, - comments: zi.comments, - badges: zb, - followPacks: zf, - reports - } -} - -function bundleSnapshot(args: { - collectedZaps: TProfileZap[] - reactionsByPubkey: Map - collectedComments: Event[] - packByDedupeKey: Map - badgesForUi: TProfileBadge[] - reports: Event[] -}): ProfileAccordionBundle { - const zaps = [...args.collectedZaps].sort((a, b) => b.amount - a.amount) - const reactions = Array.from(args.reactionsByPubkey.values()).sort( - (a, b) => b.created_at - a.created_at - ) - const comments = [...args.collectedComments].sort((a, b) => b.created_at - a.created_at) - const followPacks = [...args.packByDedupeKey.values()].sort( - (a, b) => b.event.created_at - a.event.created_at - ) - return { - zaps, - reactions, - comments, - badges: args.badgesForUi, - followPacks, - reports: args.reports - } -} - -export async function fetchProfileAccordionBundle(args: { - pubkey: string - urls: string[] - viewerPubkey: string | null | undefined - favoriteRelays: string[] - blockedRelays: string[] - force: boolean - /** Called as relays return events so the UI can render incrementally (not only after full EOSE). */ - onPartial?: (bundle: ProfileAccordionBundle) => void -}): Promise { - const { pubkey, urls, viewerPubkey, favoriteRelays, blockedRelays, force, onPartial } = args - const relayKey = profileAccordionRelayUrlsKey(urls) - const viewer = viewerPubkey?.trim() - - if (!force) { - const hit = cacheHydrated(pubkey, relayKey, viewer) - if (hit) return hit - } - - const profileReactionATags = new Set([`0:${pubkey}:`, `0:${pubkey}:profile`]) - const profileAddrs = [`0:${pubkey}:`, `0:${pubkey}:profile`] - - const seedBadges = force ? undefined : profileAccordionGetCachedBadges(pubkey, relayKey) - let resolvedBadges: TProfileBadge[] | null = null - let reportsSoFar: Event[] = [] - - const collectedZaps: TProfileZap[] = [] - const seenZaps = new Set() - const noteIdSet = new Set() - const packByDedupeKey = new Map() - const reactionsByPubkey = new Map() - const seenProfileReactionEventIds = new Set() - const collectedComments: Event[] = [] - const seenCommentIds = new Set() - let profileBadgesEvent: Event | undefined - let profileMetaEvent: Event | undefined - - const emit = () => { - if (!onPartial) return - const badgesForUi = resolvedBadges ?? seedBadges ?? [] - onPartial( - bundleSnapshot({ - collectedZaps, - reactionsByPubkey, - collectedComments, - packByDedupeKey, - badgesForUi, - reports: reportsSoFar - }) - ) - } - - let emitCoalesce = false - const scheduleEmit = () => { - if (!onPartial || emitCoalesce) return - emitCoalesce = true - queueMicrotask(() => { - emitCoalesce = false - emit() - }) - } - - const reactionTargetsKind0Profile = (evt: Event): boolean => { - if (evt.kind !== kinds.Reaction) return false - const aHit = evt.tags.some((t) => t[0] === 'a' && t[1] && profileReactionATags.has(t[1])) - if (aHit) return true - const pid = profileMetaEvent?.id - if (!pid) return false - return evt.tags.some((t) => t[0] === 'e' && t[1] && hexPubkeysEqual(t[1], pid)) - } - - const ingestProfileReaction = (evt: Event) => { - if (!reactionTargetsKind0Profile(evt)) return - if (hexPubkeysEqual(evt.pubkey, pubkey)) return - if (seenProfileReactionEventIds.has(evt.id)) return - seenProfileReactionEventIds.add(evt.id) - const existing = reactionsByPubkey.get(evt.pubkey) - if (!existing || evt.created_at > existing.created_at) { - reactionsByPubkey.set(evt.pubkey, evt) - } - } - - const ingestComment = (evt: Event) => { - if (evt.kind !== ExtendedKind.COMMENT) return - if (hexPubkeysEqual(evt.pubkey, pubkey)) return - if (seenCommentIds.has(evt.id)) return - seenCommentIds.add(evt.id) - collectedComments.push(evt) - } - - const ingestPhase1Event = (evt: Event) => { - if (evt.kind === kinds.Zap) { - const info = getZapInfoFromEvent(evt) - if (!info || !hexPubkeysEqual(info.recipientPubkey ?? '', pubkey) || !info.amount || info.amount <= 0) - return - const sender = info.senderPubkey ?? evt.pubkey - if (hexPubkeysEqual(sender, pubkey)) return - if (seenZaps.has(evt.id)) return - seenZaps.add(evt.id) - collectedZaps.push({ - pr: evt.id, - pubkey: sender, - amount: info.amount, - created_at: evt.created_at, - comment: info.comment - }) - } else if (evt.kind === kinds.ShortTextNote) { - noteIdSet.add(evt.id) - } else if (evt.kind === ExtendedKind.FOLLOW_PACK) { - const key = replaceableEventDedupeKey(evt) - const next: TProfileFollowPack = { event: evt, title: getPackTitle(evt) } - const prev = packByDedupeKey.get(key) - if (!prev || evt.created_at > prev.event.created_at) { - packByDedupeKey.set(key, next) - } - } else if (isProfileBadgesListEvent(pubkey, evt)) { - if (!profileBadgesEvent || evt.created_at > profileBadgesEvent.created_at) { - profileBadgesEvent = evt - } - } - } - - // Keep phase 1 free of #a reaction/comment: many relays handle those poorly when batched with - // zaps/notes/badges. Same ordering as interactions hook — dedicated REQ(s) for profile comments - // and reactions after we have note ids + kind-0 id. - const phase1Filters: Filter[] = [ - { '#p': [pubkey], kinds: [kinds.Zap], limit: 100 }, - { authors: [pubkey], kinds: [kinds.ShortTextNote], limit: NOTE_IDS_FOR_COMMENTS }, - { '#p': [pubkey], kinds: [ExtendedKind.FOLLOW_PACK], limit: 50 }, - { - authors: [pubkey], - kinds: [ExtendedKind.PROFILE_BADGES], - '#d': ['profile_badges'], - limit: 5 - } - ] - - const phase1Opts = { - ...QUERY_OPTS, - onevent: (evt: Event) => { - ingestPhase1Event(evt) - scheduleEmit() - } - } - - const [metaEv, _phase1Events] = await Promise.all([ - replaceableEventService.fetchReplaceableEvent(pubkey, kinds.Metadata, undefined, urls), - queryService.fetchEvents(urls, phase1Filters, phase1Opts) - ]) - profileMetaEvent = metaEv - emit() - - const noteIds = [...noteIdSet].slice(0, NOTE_IDS_FOR_COMMENTS) - - if (noteIds.length > 0) { - await queryService.fetchEvents( - urls, - [{ '#e': noteIds, kinds: [ExtendedKind.COMMENT], limit: 50 }], - { - ...QUERY_OPTS, - onevent: (evt: Event) => { - if (evt.kind === ExtendedKind.COMMENT) ingestComment(evt) - scheduleEmit() - } - } - ) - } - - await queryService.fetchEvents( - urls, - [{ '#a': profileAddrs, kinds: [ExtendedKind.COMMENT], limit: 120 }], - { - ...QUERY_OPTS, - onevent: (evt: Event) => { - if (evt.kind === ExtendedKind.COMMENT) ingestComment(evt) - scheduleEmit() - } - } - ) - - const reactionFilters: Filter[] = [] - if (profileMetaEvent?.id) { - reactionFilters.push({ '#e': [profileMetaEvent.id], kinds: [kinds.Reaction], limit: 80 }) - } - reactionFilters.push({ - '#a': [...profileReactionATags], - kinds: [kinds.Reaction], - limit: 80 - }) - await queryService.fetchEvents(urls, reactionFilters, { - ...QUERY_OPTS, - onevent: (evt: Event) => { - if (evt.kind === kinds.Reaction) ingestProfileReaction(evt) - scheduleEmit() - } - }) - - collectedZaps.sort((a, b) => b.amount - a.amount) - const reactions = Array.from(reactionsByPubkey.values()).sort((a, b) => b.created_at - a.created_at) - collectedComments.sort((a, b) => b.created_at - a.created_at) - const followPacks = [...packByDedupeKey.values()].sort((a, b) => b.event.created_at - a.event.created_at) - - let badges = await resolveProfileBadgeList(profileBadgesEvent, urls, blockedRelays, seedBadges) - badges = await enrichBadgesFromIndexedDb(badges) - resolvedBadges = badges - emit() - - let reports: Event[] = [] - if (viewer) { - const reportUrls = await buildProfileReportRelayUrls({ - viewerPubkey: viewer, - favoriteRelays, - blockedRelays - }) - if (reportUrls.length > 0) { - const seenReportIds = new Set() - reports = await queryService.fetchEvents( - reportUrls, - [{ '#p': [pubkey], kinds: [ExtendedKind.REPORT], limit: REPORT_LIMIT }], - { - ...QUERY_OPTS, - onevent: (evt: Event) => { - if (evt.kind !== ExtendedKind.REPORT || seenReportIds.has(evt.id)) return - seenReportIds.add(evt.id) - reportsSoFar.push(evt) - reportsSoFar.sort((a, b) => b.created_at - a.created_at) - scheduleEmit() - } - } - ) - } - profileAccordionSetReports(pubkey, viewer, reports) - } - reportsSoFar = reports - - profileAccordionSetInteractions(pubkey, relayKey, { - zaps: collectedZaps, - reactions, - comments: collectedComments - }) - profileAccordionSetBadges(pubkey, relayKey, badges) - profileAccordionSetFollowPacks(pubkey, relayKey, followPacks) - - emit() - - return { - zaps: collectedZaps, - reactions, - comments: collectedComments, - badges, - followPacks, - reports - } -} - -export function profileAccordionBundleCacheKey(urls: string[]): string { - return profileAccordionRelayUrlsKey(urls) -} - -function badgeMergeKey(b: TProfileBadge): string { - return `${b.a}|${b.awardId}` -} - -/** Merge two accordion bundles (e.g. provisional relays + delta-only second fetch). */ -export function mergeProfileAccordionBundles( - base: ProfileAccordionBundle, - add: ProfileAccordionBundle -): ProfileAccordionBundle { - const zapByPr = new Map(base.zaps.map((z) => [z.pr, z])) - for (const z of add.zaps) { - if (!zapByPr.has(z.pr)) zapByPr.set(z.pr, z) - } - const zaps = [...zapByPr.values()].sort((a, b) => b.amount - a.amount) - - const reactionsByPubkey = new Map() - for (const e of base.reactions) { - reactionsByPubkey.set(e.pubkey, e) - } - for (const e of add.reactions) { - const prev = reactionsByPubkey.get(e.pubkey) - if (!prev || e.created_at > prev.created_at) reactionsByPubkey.set(e.pubkey, e) - } - const reactions = [...reactionsByPubkey.values()].sort((a, b) => b.created_at - a.created_at) - - const commentById = new Map(base.comments.map((c) => [c.id, c])) - for (const c of add.comments) { - if (!commentById.has(c.id)) commentById.set(c.id, c) - } - const comments = [...commentById.values()].sort((a, b) => b.created_at - a.created_at) - - const packByKey = new Map(base.followPacks.map((p) => [replaceableEventDedupeKey(p.event), p])) - for (const p of add.followPacks) { - const k = replaceableEventDedupeKey(p.event) - const prev = packByKey.get(k) - if (!prev || p.event.created_at > prev.event.created_at) packByKey.set(k, p) - } - const followPacks = [...packByKey.values()].sort((a, b) => b.event.created_at - a.event.created_at) - - const badgeByKey = new Map(base.badges.map((b) => [badgeMergeKey(b), b])) - for (const b of add.badges) { - const k = badgeMergeKey(b) - if (!badgeByKey.has(k)) badgeByKey.set(k, b) - } - const badges = [...badgeByKey.values()] - - const reportById = new Map(base.reports.map((r) => [r.id, r])) - for (const r of add.reports) { - if (!reportById.has(r.id)) reportById.set(r.id, r) - } - const reports = [...reportById.values()].sort((a, b) => b.created_at - a.created_at) - - return { zaps, reactions, comments, badges, followPacks, reports } -} diff --git a/src/lib/profile-accordion-session-cache.ts b/src/lib/profile-accordion-session-cache.ts deleted file mode 100644 index e4787aad..00000000 --- a/src/lib/profile-accordion-session-cache.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * In-memory session cache for profile accordion fetches (per viewed profile pubkey). - * Survives collapsing/reopening the accordion; cleared on full page reload. - */ - -import type { TProfileZap } from '@/hooks/useProfileInteractions' -import type { TProfileBadge } from '@/hooks/useProfileBadges' -import type { TProfileFollowPack } from '@/hooks/useProfileFollowPacks' -import type { Event } from 'nostr-tools' - -export type ProfileAccordionInteractionsSnapshot = { - zaps: TProfileZap[] - reactions: Event[] - comments: Event[] -} - -type Entry = { - relayUrls?: string[] - /** Fingerprint of profile relay list from {@link profileAccordionSetRelayUrls} (invalidates slices when it changes) */ - relayUrlsKey?: string - interactions?: ProfileAccordionInteractionsSnapshot - /** Relay key used for the last interactions fetch (per-slice; avoids races with badges / follow packs) */ - interactionsRelayKey?: string - badges?: TProfileBadge[] - badgesRelayKey?: string - followPacks?: TProfileFollowPack[] - followPacksRelayKey?: string - /** viewer hex pubkey → reports */ - reportsByViewer?: Record -} - -const store = new Map() - -export function profileAccordionRelayUrlsKey(urls: string[]): string { - if (urls.length === 0) return '' - return [...urls].sort().join('|') -} - -function getEntry(pubkey: string): Entry { - let e = store.get(pubkey) - if (!e) { - e = {} - store.set(pubkey, e) - } - return e -} - -export function profileAccordionGetCachedRelayUrls(pubkey: string): string[] | undefined { - const urls = getEntry(pubkey).relayUrls - return urls?.length ? urls : undefined -} - -export function profileAccordionSetRelayUrls(pubkey: string, urls: string[]): void { - const e = getEntry(pubkey) - const key = profileAccordionRelayUrlsKey(urls) - if (e.relayUrlsKey && e.relayUrlsKey !== key) { - delete e.interactions - delete e.interactionsRelayKey - delete e.badges - delete e.badgesRelayKey - delete e.followPacks - delete e.followPacksRelayKey - } - e.relayUrls = urls - e.relayUrlsKey = key -} - -export function profileAccordionGetCachedInteractions( - pubkey: string, - relayKey: string -): ProfileAccordionInteractionsSnapshot | undefined { - const e = store.get(pubkey) - if (!e?.interactions || e.interactionsRelayKey !== relayKey) return undefined - return e.interactions -} - -export function profileAccordionSetInteractions( - pubkey: string, - relayKey: string, - data: ProfileAccordionInteractionsSnapshot -): void { - const e = getEntry(pubkey) - e.interactions = data - e.interactionsRelayKey = relayKey -} - -export function profileAccordionGetCachedBadges(pubkey: string, relayKey: string): TProfileBadge[] | undefined { - const e = store.get(pubkey) - if (!e?.badges || e.badgesRelayKey !== relayKey) return undefined - return e.badges -} - -export function profileAccordionSetBadges(pubkey: string, relayKey: string, badges: TProfileBadge[]): void { - const e = getEntry(pubkey) - e.badges = badges - e.badgesRelayKey = relayKey -} - -export function profileAccordionGetCachedFollowPacks( - pubkey: string, - relayKey: string -): TProfileFollowPack[] | undefined { - const e = store.get(pubkey) - if (!e?.followPacks || e.followPacksRelayKey !== relayKey) return undefined - return e.followPacks -} - -export function profileAccordionSetFollowPacks( - pubkey: string, - relayKey: string, - packs: TProfileFollowPack[] -): void { - const e = getEntry(pubkey) - e.followPacks = packs - e.followPacksRelayKey = relayKey -} - -export function profileAccordionGetCachedReports(profilePubkey: string, viewerPubkey: string): Event[] | undefined { - return getEntry(profilePubkey).reportsByViewer?.[viewerPubkey] -} - -export function profileAccordionSetReports( - profilePubkey: string, - viewerPubkey: string, - reports: Event[] -): void { - const e = getEntry(profilePubkey) - if (!e.reportsByViewer) e.reportsByViewer = {} - e.reportsByViewer[viewerPubkey] = reports -} - diff --git a/src/lib/profile-interaction-partners.ts b/src/lib/profile-interaction-partners.ts deleted file mode 100644 index 9c122255..00000000 --- a/src/lib/profile-interaction-partners.ts +++ /dev/null @@ -1,178 +0,0 @@ -import type { Event } from 'nostr-tools' - -const HEX64 = /^[0-9a-f]{64}$/i - -/** Pubkeys this author tags with `p` or references via `a` (kind:pubkey:…), excluding self. */ -export function extractPartnerPubkeysFromEvent(event: Event, authorPubkeyLower: string): string[] { - const self = authorPubkeyLower.toLowerCase() - const found = new Set() - for (const t of event.tags ?? []) { - const name = t[0] - if (name === 'p' || name === 'P') { - const pk = (t[1] ?? '').trim().toLowerCase() - if (HEX64.test(pk) && pk !== self) found.add(pk) - continue - } - if (name === 'a' || name === 'A') { - const coord = (t[1] ?? '').trim() - const parts = coord.split(':') - if (parts.length >= 2) { - const pk = parts[1]!.toLowerCase() - if (HEX64.test(pk) && pk !== self) found.add(pk) - } - } - } - return [...found] -} - -export type TInteractionPartnerStat = { - pubkey: string - /** How often this pubkey appears in p / a references on the author's events */ - mentionCount: number - /** Latest event created_at among those references */ - lastReferencedAt: number -} - -/** Same recency horizon as the interaction map UI (≈ half a year). */ -export const INTERACTION_MAP_RECENCY_MAX_AGE_SEC = 180 * 86400 - -export type TRankedInteractionPartner = { - stat: TInteractionPartnerStat - /** 0–100: more mentions and more recent references rank higher (matches map “heat” weights). */ - score: number -} - -/** - * Sort by combined frequency + recency. Uses `nowSec` and `maxAgeSec` like the map card shading - * (55% mention density vs max in list, 45% recency within the age window). - */ -export function rankInteractionPartnersByRecencyAndFrequency( - partners: TInteractionPartnerStat[], - nowSec: number, - maxAgeSec: number = INTERACTION_MAP_RECENCY_MAX_AGE_SEC -): TRankedInteractionPartner[] { - if (partners.length === 0) return [] - const age = Math.max(1, maxAgeSec) - const maxM = Math.max(1, ...partners.map((p) => p.mentionCount)) - - const scoreFor = (p: TInteractionPartnerStat): number => { - const countNorm = Math.min(1, p.mentionCount / maxM) - const recencyNorm = - p.lastReferencedAt > 0 - ? 1 - Math.min(1, Math.max(0, nowSec - p.lastReferencedAt) / age) - : 0 - return 100 * (0.55 * countNorm + 0.45 * recencyNorm) - } - - return [...partners] - .map((stat) => ({ stat, score: scoreFor(stat) })) - .sort( - (a, b) => - b.score - a.score || - b.stat.mentionCount - a.stat.mentionCount || - b.stat.lastReferencedAt - a.stat.lastReferencedAt || - a.stat.pubkey.localeCompare(b.stat.pubkey) - ) -} - -/** - * Rows for the interaction map grid: ranked by frequency/recency, with optional merge of the viewer’s - * follows. When `includeAllFollows` is true, returns **every** merged row (no row cap): people from cached - * tags first (by score), then everyone who appears only from your follow list (stable pubkey order). - */ -export function rankInteractionMapGridRows( - partners: TInteractionPartnerStat[], - opts: { - includeAllFollows: boolean - followings: string[] - nowSec: number - maxAgeSec?: number - /** Max rows when `includeAllFollows` is false (interaction-only view). Ignored when including follows. */ - gridCap?: number - } -): TRankedInteractionPartner[] { - const { - includeAllFollows, - followings, - nowSec, - maxAgeSec = INTERACTION_MAP_RECENCY_MAX_AGE_SEC, - gridCap = 72 - } = opts - - if (!includeAllFollows) { - return rankInteractionPartnersByRecencyAndFrequency(partners, nowSec, maxAgeSec).slice(0, gridCap) - } - - const merged = mergeInteractionPartnersWithFollowings(partners, followings) - if (merged.length === 0) return [] - - const tagged = merged.filter((p) => p.mentionCount > 0 || p.lastReferencedAt > 0) - const followOnly = merged.filter((p) => p.mentionCount === 0 && p.lastReferencedAt === 0) - - const rankedTagged = rankInteractionPartnersByRecencyAndFrequency(tagged, nowSec, maxAgeSec) - const extrasSorted = [...followOnly].sort((a, b) => a.pubkey.localeCompare(b.pubkey)) - const extraRows: TRankedInteractionPartner[] = extrasSorted.map((stat) => ({ stat, score: 0 })) - return [...rankedTagged, ...extraRows] -} - -export function buildInteractionPartnerStats(events: Event[], authorPubkey: string): TInteractionPartnerStat[] { - const author = authorPubkey.trim().toLowerCase() - if (!HEX64.test(author)) return [] - - const byPk = new Map() - - for (const ev of events) { - if (!ev?.pubkey || ev.pubkey.toLowerCase() !== author) continue - const ts = typeof ev.created_at === 'number' ? ev.created_at : 0 - for (const pk of extractPartnerPubkeysFromEvent(ev, author)) { - const cur = byPk.get(pk) ?? { count: 0, lastAt: 0 } - cur.count += 1 - cur.lastAt = Math.max(cur.lastAt, ts) - byPk.set(pk, cur) - } - } - - return [...byPk.entries()] - .map(([pubkey, v]) => ({ - pubkey, - mentionCount: v.count, - lastReferencedAt: v.lastAt - })) - .sort((a, b) => b.mentionCount - a.mentionCount || b.lastReferencedAt - a.lastReferencedAt) -} - -export function mergeEventsById(events: Event[]): Event[] { - const m = new Map() - for (const e of events) { - if (!e?.id) continue - const prev = m.get(e.id) - if (!prev || e.created_at > prev.created_at) m.set(e.id, e) - } - return [...m.values()] -} - -/** Adds follow pubkeys not already present so the viewer can manage follows from the interaction grid. */ -export function mergeInteractionPartnersWithFollowings( - partners: TInteractionPartnerStat[], - followedPubkeys: string[] -): TInteractionPartnerStat[] { - const map = new Map() - for (const p of partners) { - const k = p.pubkey.trim().toLowerCase() - if (!HEX64.test(k)) continue - map.set(k, { pubkey: k, mentionCount: p.mentionCount, lastReferencedAt: p.lastReferencedAt }) - } - for (const raw of followedPubkeys) { - const k = raw.trim().toLowerCase() - if (!HEX64.test(k)) continue - if (!map.has(k)) { - map.set(k, { pubkey: k, mentionCount: 0, lastReferencedAt: 0 }) - } - } - return [...map.values()].sort( - (a, b) => - b.mentionCount - a.mentionCount || - b.lastReferencedAt - a.lastReferencedAt || - a.pubkey.localeCompare(b.pubkey) - ) -} diff --git a/src/lib/profile-relay-urls.ts b/src/lib/profile-relay-urls.ts deleted file mode 100644 index 77ba3788..00000000 --- a/src/lib/profile-relay-urls.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Build relay URLs for profile-related fetches (zaps, likes, comments, badges, follow packs). - * Uses profile owner's outboxes + PROFILE_FETCH_RELAY_URLS. - */ - -import { E_TAG_FILTER_BLOCKED_RELAY_URLS, PROFILE_FETCH_RELAY_URLS } from '@/constants' -import client from '@/services/client.service' -import { normalizeUrl } from '@/lib/url' - -/** - * Immediate relay stack before NIP-65 outboxes resolve (accordion / fast first paint). - */ -export function getProfileRelayUrlsProvisional(blockedRelays: string[] = []): string[] { - const blocked = new Set( - [...blockedRelays, ...E_TAG_FILTER_BLOCKED_RELAY_URLS].map((u) => (normalizeUrl(u) || u).toLowerCase()) - ) - const out: string[] = [] - const seen = new Set() - for (const u of PROFILE_FETCH_RELAY_URLS) { - const n = normalizeUrl(u) || u - if (!n || blocked.has(n.toLowerCase()) || seen.has(n)) continue - seen.add(n) - out.push(n) - } - return out -} - -export async function buildProfileRelayUrls( - pubkey: string, - blockedRelays: string[] = [] -): Promise { - const blocked = new Set( - [...blockedRelays, ...E_TAG_FILTER_BLOCKED_RELAY_URLS].map((u) => (normalizeUrl(u) || u).toLowerCase()) - ) - const addRelay = (url: string | undefined, out: Set) => { - if (!url) return - const n = normalizeUrl(url) || url - if (!n || blocked.has(n.toLowerCase())) return - out.add(n) - } - - const relayUrlsSet = new Set() - const relayList = await client.fetchRelayList(pubkey).catch(() => ({ write: [] as string[], read: [] as string[] })) - ;(relayList?.write ?? []).filter((u): u is string => !!u).forEach((u) => addRelay(u, relayUrlsSet)) - PROFILE_FETCH_RELAY_URLS.forEach((u) => addRelay(u, relayUrlsSet)) - return Array.from(relayUrlsSet) -} diff --git a/src/lib/profile-report-relay-urls.ts b/src/lib/profile-report-relay-urls.ts deleted file mode 100644 index a808f509..00000000 --- a/src/lib/profile-report-relay-urls.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Relays for profile NIP-56 reports (kind 1984): only the viewer’s favorite tier and read (inbox) - * relays — no profile outboxes or global read mirrors, to limit abusive report spam. - */ - -import { feedRelayPolicyUrls } from '@/features/feed/relay-policy' -import { getFavoritesFeedRelayUrls } from '@/lib/favorites-feed-relays' -import { relayUrlsLocalsFirst } from '@/lib/relay-url-priority' -import { normalizeUrl } from '@/lib/url' -import client from '@/services/client.service' - -const MAX_PROFILE_REPORT_RELAYS = 28 - -export async function buildProfileReportRelayUrls(options: { - viewerPubkey: string - favoriteRelays: string[] - blockedRelays: string[] -}): Promise { - const { viewerPubkey, favoriteRelays, blockedRelays } = options - const list = await client.fetchRelayList(viewerPubkey).catch(() => ({ read: [] as string[], write: [] as string[] })) - const inbox = relayUrlsLocalsFirst(list.read ?? []) - .map((u) => normalizeUrl(u) || u) - .filter(Boolean) as string[] - const favorites = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays) - return feedRelayPolicyUrls([ - { source: 'favorites', urls: favorites }, - { source: 'viewer-read', urls: inbox } - ], { - operation: 'read', - blockedRelays, - maxRelays: MAX_PROFILE_REPORT_RELAYS, - applySocialKindBlockedFilter: false, - allowThirdPartyLocalRelays: true - }) -} diff --git a/src/pages/primary/NoteListPage/RelaysFeed.tsx b/src/pages/primary/NoteListPage/RelaysFeed.tsx index e528179f..1890b8ce 100644 --- a/src/pages/primary/NoteListPage/RelaysFeed.tsx +++ b/src/pages/primary/NoteListPage/RelaysFeed.tsx @@ -1,11 +1,7 @@ import NormalFeed from '@/components/NormalFeed' import type { TNoteListRef } from '@/components/NoteList' import { checkAlgoRelay } from '@/lib/relay' -import { - isWispTrendingNotesRelayUrl, - WISP_TRENDING_FEED_KINDS -} from '@/lib/wisp-trending-relay' -import { normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url' +import { normalizeUrl } from '@/lib/url' import { useFeed } from '@/providers/FeedProvider' import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider' import relayInfoService from '@/services/relay-info.service' @@ -21,7 +17,7 @@ const RelaysFeed = forwardRef< kindsOverride?: number[] } >(function RelaysFeed({ setSubHeader, onSubHeaderRefresh, kindsOverride }, ref) { - const { feedInfo, relayUrls } = useFeed() + const { relayUrls } = useFeed() const { showKinds } = useKindFilterOrDefaults() const [areAlgoRelays, setAreAlgoRelays] = useState(false) @@ -57,7 +53,7 @@ const RelaysFeed = forwardRef< if (cancelled) return const areAlgo = relayInfos.every((relayInfo) => checkAlgoRelay(relayInfo)) setAreAlgoRelays(areAlgo) - } catch (_error) { + } catch { if (!cancelled) setAreAlgoRelays(false) } } @@ -76,47 +72,11 @@ const RelaysFeed = forwardRef< return fallbackNoteKinds }, [kindsOverride, showKinds, fallbackNoteKinds]) - const canRenderFeed = - (feedInfo.feedType === 'relay' || - feedInfo.feedType === 'relays' || - feedInfo.feedType === 'all-favorites') && - relayUrls.length > 0 - - /** Distinguishes home relay chips so we do not keep the previous timeline on single→all-favorites (strict superset). */ - const feedTimelineScopeKey = useMemo(() => { - if (feedInfo.feedType === 'all-favorites') return 'all-favorites' - if (feedInfo.feedType === 'relays') return `relays:${feedInfo.id ?? ''}` - if (feedInfo.feedType === 'relay') { - /** Same canonical URL identity as {@link NoteList} `subRequestsKey` (not `normalizeUrl` alone — HTTP index relays differ). */ - const urlsKey = [...relayUrls] - .map((u) => normalizeAnyRelayUrl(u) || u) - .filter(Boolean) - .sort() - .join('|') - if (urlsKey) return `relay:${urlsKey}` - const id = feedInfo.id ? normalizeAnyRelayUrl(feedInfo.id) || feedInfo.id : '' - return `relay:${id}` - } - return undefined - }, [feedInfo.feedType, feedInfo.id, relayUrls]) - - const wispTrendingSingleRelay = - feedInfo.feedType === 'relay' && - relayUrls.length === 1 && - !!relayUrls[0] && - isWispTrendingNotesRelayUrl(relayUrls[0]) + const canRenderFeed = relayUrls.length > 0 // Hooks must run every render — never place useMemo after conditional returns. const subRequests = useMemo(() => { if (!canRenderFeed) return [] - if (wispTrendingSingleRelay) { - return [ - { - urls: relayUrls, - filter: { kinds: [...WISP_TRENDING_FEED_KINDS], limit: 100 } - } - ] - } return [ { urls: relayUrls, @@ -125,37 +85,26 @@ const RelaysFeed = forwardRef< } } ] - }, [canRenderFeed, relayUrls, defaultKinds, wispTrendingSingleRelay]) + }, [canRenderFeed, relayUrls, defaultKinds]) if (!canRenderFeed) { return null } - // preserveTimeline: merge when relay list grows (e.g. all-favorites list fills in). Do not use - // mergeTimelineWhenSubRequestFiltersMatch here — same kinds + different URLs would keep the old - // timeline when switching home feed chips (all-favorites ↔ set ↔ single relay). + // preserveTimeline: merge when relay list grows (e.g. all-favorites list fills in). return ( 1) - } + timelinePublicReadFallback /> ) }) diff --git a/src/pages/primary/NoteListPage/index.tsx b/src/pages/primary/NoteListPage/index.tsx index 75ba1c23..575ff22a 100644 --- a/src/pages/primary/NoteListPage/index.tsx +++ b/src/pages/primary/NoteListPage/index.tsx @@ -6,7 +6,6 @@ import { useFeed } from '@/providers/FeedProvider' import { useNostr } from '@/providers/NostrProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' import type { TNoteListRef } from '@/components/NoteList' -import { NoteCardLoadingSkeleton } from '@/components/NoteCard' import { TPageRef } from '@/types' import { Calendar, Compass, Flame } from 'lucide-react' import React, { @@ -30,15 +29,10 @@ const NoteListPage = forwardRef((_, ref) => { const { addRelayUrls, removeRelayUrls } = useCurrentRelays() const layoutRef = useRef(null) const feedRef = useRef(null) - const { feedInfo, relayUrls, isReady } = useFeed() + const { relayUrls } = useFeed() const { isSmallScreen } = useScreenSize() const [homeSubHeader, setHomeSubHeader] = useState(null) - const usesSubHeader = - feedInfo.feedType === 'all-favorites' || - feedInfo.feedType === 'relay' || - feedInfo.feedType === 'relays' - const runFeedRefresh = useCallback(() => { feedRef.current?.refresh() }, []) @@ -56,10 +50,6 @@ const NoteListPage = forwardRef((_, ref) => { setHomeSubHeader(node) }, []) - useEffect(() => { - if (!usesSubHeader) setHomeSubHeader(null) - }, [usesSubHeader]) - // REMOVED: Scroll-to-top logic - feed should NEVER scroll to top when drawer opens/closes // The feed stays mounted and maintains scroll position at all times @@ -72,37 +62,6 @@ const NoteListPage = forwardRef((_, ref) => { } }, [relayUrls]) - let content: React.ReactNode = null - if (!isReady) { - content = ( -
-

- {t('feedStarting', { - defaultValue: 'Starting feeds and relays… This can take a few seconds after login.' - })} -

- {Array.from({ length: 5 }).map((_, i) => ( - - ))} -
- ) - } else { - content = ( - <> - - - ) - } - const feedPageTitle = t('Favorite Relays') const subHeader = ( @@ -116,7 +75,7 @@ const NoteListPage = forwardRef((_, ref) => { ) /** Desktop: nav/logo/account live in titlebar only on small screens; refresh moves to subheader when present. Omit empty h-12 strip. */ - const showNoteListTitlebar = isSmallScreen || !usesSubHeader + const showNoteListTitlebar = isSmallScreen return ( ((_, ref) => { suppressMobileDefaultActiveRelaysButton titlebar={ showNoteListTitlebar ? ( - + ) : null } subHeader={subHeader} displayScrollToTopButton >
- {content} +
) diff --git a/src/pages/primary/SpellsPage/RelayThreadHeatMap.tsx b/src/pages/primary/SpellsPage/RelayThreadHeatMap.tsx index 917fc797..02868e88 100644 --- a/src/pages/primary/SpellsPage/RelayThreadHeatMap.tsx +++ b/src/pages/primary/SpellsPage/RelayThreadHeatMap.tsx @@ -4,9 +4,8 @@ import { ExtendedKind } from '@/constants' import { eventPassesNoteListKindPicker } from '@/lib/feed-kind-filter' import { filterEventsExcludingTombstones } from '@/lib/event' import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadRelaysWithHttp } from '@/lib/favorites-feed-relays' -import { toNote, toProfileInteractionMap } from '@/lib/link' +import { toNote } from '@/lib/link' import logger from '@/lib/logger' -import { mergeEventsById } from '@/lib/profile-interaction-partners' import { parseRelayThreadHeatMapCache, relayThreadHeatMapSettingKey, @@ -22,14 +21,14 @@ import { type TRelayThreadHeatEdge } from '@/lib/relay-thread-heat' import { usePrimaryPage } from '@/contexts/primary-page-context' -import { useSmartNoteNavigation, useSmartProfileInteractionsNavigation } from '@/PageManager' +import { useSmartNoteNavigation } from '@/PageManager' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider' import { useNostr } from '@/providers/NostrProvider' import client, { eventService } from '@/services/client.service' import indexedDb from '@/services/indexed-db.service' import { cn } from '@/lib/utils' -import { LayoutGrid, Loader2, RefreshCw } from 'lucide-react' +import { Loader2, RefreshCw } from 'lucide-react' import type { Event } from 'nostr-tools' import { kinds, verifyEvent } from 'nostr-tools' import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' @@ -44,6 +43,18 @@ const SESSION_HEAT_LIMIT = 2500 const ARCHIVE_HEAT_MAX_SCAN = 30_000 const ARCHIVE_HEAT_MAX_MATCHES = 2000 +function mergeEventsById(events: Event[]): Event[] { + const eventsById = new Map() + for (const event of events) { + if (!event?.id) continue + const existing = eventsById.get(event.id) + if (!existing || event.created_at > existing.created_at) { + eventsById.set(event.id, event) + } + } + return Array.from(eventsById.values()) +} + const HEAT_KINDS = [kinds.ShortTextNote, ExtendedKind.DISCUSSION] as const const ARCHIVE_SCAN_TIMEOUT_MS = 22_000 @@ -87,7 +98,6 @@ export default function RelayThreadHeatMap({ followPubkeys, refreshKey }: Props) const { t } = useTranslation() const { navigate: navigatePrimary } = usePrimaryPage() const { navigateToNote } = useSmartNoteNavigation() - const { navigateToProfileInteractions } = useSmartProfileInteractionsNavigation() const { pubkey, relayList } = useNostr() const { favoriteRelays, blockedRelays } = useFavoriteRelays() const { showKinds, showKind1OPs, showKind1Replies, showKind1111 } = useKindFilterOrDefaults() @@ -437,16 +447,6 @@ export default function RelayThreadHeatMap({ followPubkeys, refreshKey }: Props) )} {t('heatMapRescan')} - - - - - ) -}) - -ProfileInteractionDiagramPage.displayName = 'ProfileInteractionDiagramPage' -export default ProfileInteractionDiagramPage diff --git a/src/providers/FeedProvider.tsx b/src/providers/FeedProvider.tsx index f1b86995..3482a81b 100644 --- a/src/providers/FeedProvider.tsx +++ b/src/providers/FeedProvider.tsx @@ -1,14 +1,10 @@ import { feedRelayPolicyUrls } from '@/features/feed/relay-policy' import { getFavoritesFeedRelayUrls } from '@/lib/favorites-feed-relays' -import { getRelaySetFromEvent, getRelayListFromEvent, getHttpRelayListFromEvent } from '@/lib/event-metadata' +import { getRelayListFromEvent, getHttpRelayListFromEvent } from '@/lib/event-metadata' import logger from '@/lib/logger' -import { isHttpRelayUrl, isWebsocketUrl, normalizeAnyRelayUrl } from '@/lib/url' +import { normalizeAnyRelayUrl } from '@/lib/url' import { buildWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay' -import indexedDb from '@/services/indexed-db.service' -import storage from '@/services/local-storage.service' -import { TFeedInfo, TFeedType } from '@/types' -import { kinds } from 'nostr-tools' -import { useEffect, useMemo, useRef, useState, useCallback } from 'react' +import { useEffect, useMemo, useState, useCallback } from 'react' import { FeedContext } from './feed-context' import { useFavoriteRelays } from './FavoriteRelaysProvider' import { useNostr } from './NostrProvider' @@ -42,8 +38,8 @@ function buildAllFavoritesFeedRelayUrls( } export function FeedProvider({ children }: { children: React.ReactNode }) { - const { pubkey, isInitialized, cacheRelayListEvent, httpRelayListEvent } = useNostr() - const { relaySets, favoriteRelays, blockedRelays } = useFavoriteRelays() + const { isInitialized, cacheRelayListEvent, httpRelayListEvent } = useNostr() + const { favoriteRelays, blockedRelays } = useFavoriteRelays() /** * Extra relay URLs always merged into the all-favorites feed: @@ -67,11 +63,6 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { const [relayUrls, setRelayUrls] = useState(() => buildAllFavoritesFeedRelayUrls([], [], [buildWispTrendingNotesRelayUrl()]) ) - const [isReady, setIsReady] = useState(true) - const [feedInfo, setFeedInfo] = useState({ - feedType: 'all-favorites' - }) - const feedInfoRef = useRef(feedInfo) /** Same logical relay policy result — reuse array ref so NoteList does not re-subscribe. */ const setRelayUrlsIfChanged = useCallback((next: string[]) => { setRelayUrls((prev) => { @@ -80,98 +71,11 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { }) }, []) - const switchFeed = useCallback(async ( - feedType: TFeedType, - options: { - activeRelaySetId?: string | null - pubkey?: string | null - relay?: string | null - } = {} - ) => { - logger.debug('switchFeed called:', { feedType, options }) - if (feedType === 'relay') { - const normalizedUrl = normalizeAnyRelayUrl(options.relay ?? '') - const isRelayFeedUrl = - !!normalizedUrl && (isHttpRelayUrl(normalizedUrl) || isWebsocketUrl(normalizedUrl)) - logger.debug('Relay switchFeed:', { normalizedUrl, isRelayFeedUrl, blockedRelays }) - - if (!isRelayFeedUrl) { - logger.debug('Invalid relay URL, setting isReady to true') - setIsReady(true) - return - } - - // Don't allow selecting a blocked relay as feed - if (blockedRelays.includes(normalizedUrl)) { - logger.warn('Cannot select blocked relay as feed:', normalizedUrl) - setIsReady(true) - return - } - - const newFeedInfo = { feedType, id: normalizedUrl } - logger.component('FeedProvider', 'Setting relay feed info', newFeedInfo) - setFeedInfo(newFeedInfo) - feedInfoRef.current = newFeedInfo - setRelayUrlsIfChanged([normalizedUrl]) - logger.component('FeedProvider', 'Set relayUrls', { relayUrls: [normalizedUrl] }) - storage.setFeedInfo(newFeedInfo, pubkey) - // Reset note list mode to 'posts' when switching to relay feed to ensure main content is shown - storage.setNoteListMode('posts') - setIsReady(true) - logger.component('FeedProvider', 'Relay feed setup complete, isReady set to true') - return - } - if (feedType === 'relays') { - const relaySetId = options.activeRelaySetId ?? (relaySets.length > 0 ? relaySets[0].id : null) - if (!relaySetId || !pubkey) { - setIsReady(true) - return - } - - let relaySet = - relaySets.find((set) => set.id === relaySetId) ?? - (relaySets.length > 0 ? relaySets[0] : null) - if (!relaySet) { - const storedRelaySetEvent = await indexedDb.getReplaceableEvent( - pubkey, - kinds.Relaysets, - relaySetId - ) - if (storedRelaySetEvent) { - relaySet = getRelaySetFromEvent(storedRelaySetEvent, blockedRelays) - } - } - if (relaySet) { - const newFeedInfo = { feedType, id: relaySet.id } - setFeedInfo(newFeedInfo) - feedInfoRef.current = newFeedInfo - setRelayUrlsIfChanged(relaySet.relayUrls) - storage.setFeedInfo(newFeedInfo, pubkey) - // Reset note list mode to 'posts' when switching to relay set to ensure main content is shown - storage.setNoteListMode('posts') - setIsReady(true) - } - setIsReady(true) - return - } - if (feedType === 'all-favorites') { - const finalRelays = buildAllFavoritesFeedRelayUrls(favoriteRelays, blockedRelays, extraFeedRelayUrls) - logger.debug('Switching to all-favorites, finalRelays:', finalRelays) - const newFeedInfo = { feedType } - setFeedInfo(newFeedInfo) - feedInfoRef.current = newFeedInfo - setRelayUrlsIfChanged(finalRelays) - storage.setFeedInfo(newFeedInfo, pubkey) - // Reset note list mode to 'posts' when switching to all-favorites to ensure main content is shown - storage.setNoteListMode('posts') - setIsReady(true) - return - } - setIsReady(true) - }, [pubkey, favoriteRelays, blockedRelays, relaySets, extraFeedRelayUrls, setRelayUrlsIfChanged]) - - const switchFeedRef = useRef(switchFeed) - switchFeedRef.current = switchFeed + const updateFeedRelayUrls = useCallback(() => { + const finalRelays = buildAllFavoritesFeedRelayUrls(favoriteRelays, blockedRelays, extraFeedRelayUrls) + logger.debug('Updating all-favorites relay URLs:', finalRelays) + setRelayUrlsIfChanged(finalRelays) + }, [favoriteRelays, blockedRelays, extraFeedRelayUrls, setRelayUrlsIfChanged]) const favoriteRelaysIdentity = useMemo( () => @@ -191,79 +95,24 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { .join('|'), [blockedRelays] ) - const relaySetsIdentity = useMemo( - () => - relaySets - .map((s) => { - const urls = [...s.relayUrls] - .map((u) => normalizeAnyRelayUrl(u) || u.trim()) - .filter(Boolean) - .sort() - .join(',') - return `${s.id}:${urls}` - }) - .sort() - .join('\n'), - [relaySets] - ) - useEffect(() => { - const init = async () => { - logger.debug('FeedProvider init:', { isInitialized, pubkey, favoriteRelays: favoriteRelays.length, blockedRelays: blockedRelays.length }) - - // Wait for favoriteRelays to be initialized (should have at least default relays) - // If favoriteRelays is empty, it might not be initialized yet, so wait - if (favoriteRelays.length === 0 && !pubkey) { - // For anonymous users, favoriteRelays should be initialized from FAST_READ_RELAY_URLS - // If it's still empty, something is wrong, but we'll use defaults - logger.debug('FeedProvider: favoriteRelays is empty, using defaults') - } - - let stored: TFeedInfo | null = null - if (pubkey) { - const fromStorage = storage.getFeedInfo(pubkey) - logger.debug('Stored feed info:', fromStorage) - if (fromStorage) stored = fromStorage - } - - const storedFeedType = (stored as { feedType?: string } | null)?.feedType - const migrateHomeToCombo = - storedFeedType === 'following' || - storedFeedType === 'bookmarks' || - storedFeedType === 'relay' || - storedFeedType === 'relays' - - if (migrateHomeToCombo && pubkey) { - const migrated: TFeedInfo = { feedType: 'all-favorites' } - storage.setFeedInfo(migrated, pubkey) - logger.info('[FeedProvider] Home feed uses combo (all-favorites); migrated stored selection', { - previous: storedFeedType - }) - } + logger.debug('FeedProvider relay init:', { + isInitialized, + favoriteRelays: favoriteRelays.length, + blockedRelays: blockedRelays.length + }) - return await switchFeedRef.current('all-favorites') + if (favoriteRelays.length === 0) { + logger.debug('FeedProvider: favoriteRelays is empty, using defaults') } - void init() - }, [pubkey, isInitialized, favoriteRelaysIdentity, blockedRelaysIdentity, relaySetsIdentity]) - - // Update relay URLs when favoriteRelays, blocked, or extra relay lists change while in all-favorites mode - useEffect(() => { - if (feedInfo.feedType !== 'all-favorites') return - const finalRelays = buildAllFavoritesFeedRelayUrls(favoriteRelays, blockedRelays, extraFeedRelayUrls) - logger.debug('Updating relay URLs for all-favorites:', finalRelays) - // Same logical list can be merged into a new array each run; keep the previous reference so - // feed consumers (RelaysFeed → NoteList relay subscription) do not re-enter effects in a tight loop. - setRelayUrlsIfChanged(finalRelays) - }, [feedInfo.feedType, favoriteRelays, blockedRelays, extraFeedRelayUrls, setRelayUrlsIfChanged]) + updateFeedRelayUrls() + }, [isInitialized, favoriteRelaysIdentity, blockedRelaysIdentity, updateFeedRelayUrls]) return ( {children} diff --git a/src/providers/feed-context.tsx b/src/providers/feed-context.tsx index b353c91f..d52bf8fc 100644 --- a/src/providers/feed-context.tsx +++ b/src/providers/feed-context.tsx @@ -2,21 +2,10 @@ * Standalone React context for feed state so HMR on `FeedProvider.tsx` does not recreate * `createContext()` (which breaks `useFeed` after Fast Refresh). */ -import { TFeedInfo, TFeedType } from '@/types' import { createContext, useContext } from 'react' export type TFeedContext = { - feedInfo: TFeedInfo relayUrls: string[] - isReady: boolean - switchFeed: ( - feedType: TFeedType, - options?: { - activeRelaySetId?: string | null - pubkey?: string | null - relay?: string | null - } - ) => Promise } export const FeedContext = createContext(undefined) diff --git a/src/routes.tsx b/src/routes.tsx index 6559207f..5da100a0 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -22,7 +22,6 @@ const PostSettingsPageLazy = lazy(() => import('./pages/secondary/PostSettingsPa const ProfileEditorPageLazy = lazy(() => import('./pages/secondary/ProfileEditorPage')) const ProfileListPageLazy = lazy(() => import('./pages/secondary/ProfileListPage')) const ProfilePageLazy = lazy(() => import('./pages/secondary/ProfilePage')) -const ProfileInteractionDiagramPageLazy = lazy(() => import('./pages/secondary/ProfileInteractionDiagramPage')) const RelayPageLazy = lazy(() => import('./pages/secondary/RelayPage')) const RelayReviewsPageLazy = lazy(() => import('./pages/secondary/RelayReviewsPage')) const RelaySettingsPageLazy = lazy(() => import('./pages/secondary/RelaySettingsPage')) @@ -73,7 +72,6 @@ const ROUTES = [ { path: '/users', element: SR(ProfileListPageLazy) }, { path: '/users/:id/following', element: SR(FollowingListPageLazy) }, { path: '/users/:id/relays', element: SR(OthersRelaySettingsPageLazy) }, - { path: '/users/:id/interactions', element: SR(ProfileInteractionDiagramPageLazy) }, { path: '/users/:id', element: SR(ProfilePageLazy) }, { path: '/relays/:url/reviews', element: SR(RelayReviewsPageLazy) }, { path: '/relays/:url', element: SR(RelayPageLazy) }, diff --git a/src/services/client-query.service.ts b/src/services/client-query.service.ts index 515eb0ac..454f4209 100644 --- a/src/services/client-query.service.ts +++ b/src/services/client-query.service.ts @@ -38,7 +38,8 @@ import type { ISigner, TSignerType } from '@/types' /** NIP-01 filter keys only; NIP-50 adds `search` which non-searchable relays reject. */ function filterForRelay(f: Filter, relaySupportsSearch: boolean): Filter { if (relaySupportsSearch) return f - const { search: _search, ...rest } = f + const rest = { ...f } + delete rest.search return rest as Filter } @@ -357,7 +358,6 @@ export class QueryService { let feedFirstResultGraceTimeoutId: ReturnType | null = null let replaceableRaceTimeoutId: ReturnType | null = null let allEosed = false - let eventCount = 0 let resolved = false let firstResultTime: number | null = null let globalTimeoutId: ReturnType | null = null @@ -372,7 +372,6 @@ export class QueryService { const evts = await queryIndexRelay(base, effectiveFilter, { signal: abortHttp.signal }) for (const evt of evts) { if (resolved) return - eventCount++ onevent?.(evt) events.push(evt) this.trackEventSeenOnByUrl(evt.id, base) @@ -461,7 +460,6 @@ export class QueryService { effectiveFilter, { onevent: (evt) => { - eventCount++ onevent?.(evt) events.push(evt) // Session cache: ingest as events arrive (reactions/replies/zaps from note-stats, etc.), diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 02a9af93..5b05afec 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -37,7 +37,8 @@ import { /** NIP-01 filter keys only; NIP-50 adds `search` which non-searchable relays reject. */ function filterForRelay(f: Filter, relaySupportsSearch: boolean): Filter { if (relaySupportsSearch) return f - const { search: _search, ...rest } = f + const rest = { ...f } + delete rest.search return rest as Filter } @@ -1004,7 +1005,7 @@ class ClientService extends EventTarget { } const bootstrapExtras: string[] = [...(additionalRelayUrls ?? [])] - let authorInboxFromContext: string[] = [] + const authorInboxFromContext: string[] = [] const shouldMergeContextInboxes = !specifiedRelayUrls?.length && ![kinds.Contacts, kinds.Mutelist, ExtendedKind.FOLLOW_SET].includes(event.kind) @@ -3140,7 +3141,7 @@ class ClientService extends EventTarget { return } const chunk = followings.slice(i * chunkSize, (i + 1) * chunkSize) - const [relayListEvents, contactsEvents, _profiles] = await Promise.all([ + const [relayListEvents, contactsEvents] = await Promise.all([ this.replaceableEventService.fetchReplaceableEventsFromProfileFetchRelays(chunk, kinds.RelayList), this.replaceableEventService.fetchReplaceableEventsFromProfileFetchRelays(chunk, kinds.Contacts), Promise.all(chunk.map((pk) => this.fetchProfileEvent(pk))) diff --git a/src/services/local-storage.service.ts b/src/services/local-storage.service.ts index 64ba7eff..76ab3bbc 100644 --- a/src/services/local-storage.service.ts +++ b/src/services/local-storage.service.ts @@ -12,7 +12,6 @@ import { randomString } from '@/lib/random' import { TAccount, TAccountPointer, - TFeedInfo, TFontSize, TMediaAutoLoadPolicy, TMediaUploadServiceConfig, @@ -38,7 +37,6 @@ const SETTINGS_KEYS = [ StorageKey.DEFAULT_ZAP_COMMENT, StorageKey.QUICK_ZAP, StorageKey.ZAP_REPLY_THRESHOLD, - StorageKey.ACCOUNT_FEED_INFO_MAP, StorageKey.AUTOPLAY, StorageKey.HIDE_UNTRUSTED_INTERACTIONS, StorageKey.HIDE_UNTRUSTED_NOTIFICATIONS, @@ -85,7 +83,6 @@ class LocalStorageService { private defaultZapComment: string = 'Zap!' private quickZap: boolean = false private zapReplyThreshold: number = 1 - private accountFeedInfoMap: Record = {} private mediaUploadService: string = DEFAULT_NIP_96_SERVICE private autoplay: boolean = true private hideUntrustedInteractions: boolean = false @@ -191,10 +188,6 @@ class LocalStorageService { } } - const accountFeedInfoMapStr = - window.localStorage.getItem(StorageKey.ACCOUNT_FEED_INFO_MAP) ?? '{}' - this.accountFeedInfoMap = JSON.parse(accountFeedInfoMapStr) - // deprecated this.mediaUploadService = window.localStorage.getItem(StorageKey.MEDIA_UPLOAD_SERVICE) ?? DEFAULT_NIP_96_SERVICE @@ -574,8 +567,6 @@ class LocalStorageService { const num = parseInt(zapReplyStr) if (!isNaN(num)) this.zapReplyThreshold = num } - const accountFeedInfoStr = get(StorageKey.ACCOUNT_FEED_INFO_MAP) - if (accountFeedInfoStr != null) this.accountFeedInfoMap = JSON.parse(accountFeedInfoStr) as Record this.autoplay = get(StorageKey.AUTOPLAY) !== 'false' const hideInteractions = get(StorageKey.HIDE_UNTRUSTED_INTERACTIONS) if (hideInteractions != null) this.hideUntrustedInteractions = hideInteractions === 'true' @@ -792,18 +783,6 @@ class LocalStorageService { this.persistSetting(StorageKey.ZAP_REPLY_THRESHOLD, sats.toString()) } - getFeedInfo(pubkey: string) { - return this.accountFeedInfoMap[pubkey] - } - - setFeedInfo(info: TFeedInfo, pubkey?: string | null) { - this.accountFeedInfoMap[pubkey ?? 'default'] = info - this.persistSetting( - StorageKey.ACCOUNT_FEED_INFO_MAP, - JSON.stringify(this.accountFeedInfoMap) - ) - } - getAutoplay() { return this.autoplay } diff --git a/src/services/navigation.service.ts b/src/services/navigation.service.ts index c22d4cb4..fb2464a9 100644 --- a/src/services/navigation.service.ts +++ b/src/services/navigation.service.ts @@ -41,7 +41,6 @@ export type ViewType = | 'hashtag' | 'relay' | 'following' - | 'profile-interactions' | 'mute' | 'bookmarks' | 'pins' @@ -277,10 +276,8 @@ export class NavigationService { if (viewType === 'profile') { if (pathname.includes('/following')) return 'Following' if (pathname.includes('/relays')) return 'Relays and Storage Settings' - if (pathname.includes('/interactions')) return 'Interaction map' return 'Profile' } - if (viewType === 'profile-interactions') return 'Interaction map' if (viewType === 'hashtag') return 'Hashtag' if (viewType === 'relay') return 'Relay' if (viewType === 'note') { diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 9f0d2141..035ba92a 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -168,9 +168,6 @@ export type TAccount = { export type TAccountPointer = Pick -export type TFeedType = 'relays' | 'relay' | 'all-favorites' -export type TFeedInfo = { feedType: TFeedType; id?: string } - export type TImetaInfo = { url: string blurHash?: string