From 7065015462a09c5ff7902316ef570bd9c39ceabc Mon Sep 17 00:00:00 2001 From: codytseng Date: Mon, 17 Nov 2025 22:37:04 +0800 Subject: [PATCH] feat: add highlights to quotes --- .../ExternalContentInteractions/Tabs.tsx | 5 +- .../ExternalContentInteractions/index.tsx | 6 +- src/components/NoteInteractions/index.tsx | 2 +- src/components/NoteList/index.tsx | 6 +- src/components/QuoteList/index.tsx | 187 +++++------------- 5 files changed, 58 insertions(+), 148 deletions(-) diff --git a/src/components/ExternalContentInteractions/Tabs.tsx b/src/components/ExternalContentInteractions/Tabs.tsx index fc336032..365f09b1 100644 --- a/src/components/ExternalContentInteractions/Tabs.tsx +++ b/src/components/ExternalContentInteractions/Tabs.tsx @@ -2,10 +2,11 @@ import { cn } from '@/lib/utils' import { useTranslation } from 'react-i18next' import { useRef, useEffect, useState } from 'react' -export type TTabValue = 'replies' | 'reactions' +export type TTabValue = 'replies' | 'reactions' | 'quotes' const TABS = [ { value: 'replies', label: 'Replies' }, - { value: 'reactions', label: 'Reactions' } + { value: 'reactions', label: 'Reactions' }, + { value: 'quotes', label: 'Quotes' } ] as { value: TTabValue; label: string }[] export function Tabs({ diff --git a/src/components/ExternalContentInteractions/index.tsx b/src/components/ExternalContentInteractions/index.tsx index 1e3caee3..023df2e4 100644 --- a/src/components/ExternalContentInteractions/index.tsx +++ b/src/components/ExternalContentInteractions/index.tsx @@ -2,9 +2,10 @@ import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' import { Separator } from '@/components/ui/separator' import { useState } from 'react' import HideUntrustedContentButton from '../HideUntrustedContentButton' +import QuoteList from '../QuoteList' +import ReactionList from '../ReactionList' import ReplyNoteList from '../ReplyNoteList' import { Tabs, TTabValue } from './Tabs' -import ReactionList from '../ReactionList' export default function ExternalContentInteractions({ pageIndex, @@ -22,6 +23,9 @@ export default function ExternalContentInteractions({ case 'reactions': list = break + case 'quotes': + list = + break default: break } diff --git a/src/components/NoteInteractions/index.tsx b/src/components/NoteInteractions/index.tsx index 0734ced2..385899a1 100644 --- a/src/components/NoteInteractions/index.tsx +++ b/src/components/NoteInteractions/index.tsx @@ -24,7 +24,7 @@ export default function NoteInteractions({ list = break case 'quotes': - list = + list = break case 'reactions': list = diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index d4a9bbc9..04be8a4f 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -47,7 +47,7 @@ const NoteList = forwardRef( showNewNotesDirectly = false }: { subRequests: TFeedSubRequest[] - showKinds: number[] + showKinds?: number[] filterMutedNotes?: boolean hideReplies?: boolean hideUntrustedNotes?: boolean @@ -236,7 +236,7 @@ const NoteList = forwardRef( setNewEvents([]) setHasMore(true) - if (showKinds.length === 0) { + if (showKinds?.length === 0 && subRequests.every(({ filter }) => !filter.kinds)) { setLoading(false) setHasMore(false) return () => {} @@ -246,7 +246,7 @@ const NoteList = forwardRef( subRequests.map(({ urls, filter }) => ({ urls, filter: { - kinds: showKinds, + kinds: showKinds ?? [], ...filter, limit: areAlgoRelays ? ALGO_LIMIT : LIMIT } diff --git a/src/components/QuoteList/index.tsx b/src/components/QuoteList/index.tsx index 37f6eec1..eb031b99 100644 --- a/src/components/QuoteList/index.tsx +++ b/src/components/QuoteList/index.tsx @@ -1,156 +1,61 @@ import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' +import { useStuff } from '@/hooks/useStuff' import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event' -import { useNostr } from '@/providers/NostrProvider' -import { useUserTrust } from '@/providers/UserTrustProvider' import client from '@/services/client.service' -import dayjs from 'dayjs' -import { Event, kinds } from 'nostr-tools' -import { useEffect, useRef, useState } from 'react' -import { useTranslation } from 'react-i18next' -import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard' +import { TFeedSubRequest } from '@/types' +import { Event, Filter, kinds } from 'nostr-tools' +import { useEffect, useState } from 'react' +import NoteList from '../NoteList' -const LIMIT = 100 -const SHOW_COUNT = 10 - -export default function QuoteList({ event, className }: { event: Event; className?: string }) { - const { t } = useTranslation() - const { startLogin } = useNostr() - const { hideUntrustedInteractions, isUserTrusted } = useUserTrust() - const [timelineKey, setTimelineKey] = useState(undefined) - const [events, setEvents] = useState([]) - const [showCount, setShowCount] = useState(SHOW_COUNT) - const [hasMore, setHasMore] = useState(true) - const [loading, setLoading] = useState(true) - const bottomRef = useRef(null) +export default function QuoteList({ stuff }: { stuff: Event | string }) { + const { event, externalContent } = useStuff(stuff) + const [subRequests, setSubRequests] = useState([]) useEffect(() => { async function init() { - setLoading(true) - setEvents([]) - setHasMore(true) + const relaySet = new Set(BIG_RELAY_URLS) + const filters: Filter[] = [] + if (event) { + const relayList = await client.fetchRelayList(event.pubkey) + relayList.read.slice(0, 5).forEach((url) => relaySet.add(url)) + const seenOn = client.getSeenEventRelayUrls(event.id) + seenOn.forEach((url) => relaySet.add(url)) - const relayList = await client.fetchRelayList(event.pubkey) - const relayUrls = relayList.read.concat(BIG_RELAY_URLS) - const seenOn = client.getSeenEventRelayUrls(event.id) - relayUrls.unshift(...seenOn) - - const { closer, timelineKey } = await client.subscribeTimeline( - [ - { - urls: relayUrls, - filter: { - '#q': [ - isReplaceableEvent(event.kind) ? getReplaceableCoordinateFromEvent(event) : event.id - ], - kinds: [ - kinds.ShortTextNote, - kinds.Highlights, - kinds.LongFormArticle, - ExtendedKind.COMMENT, - ExtendedKind.POLL - ], - limit: LIMIT - } - } - ], - { - onEvents: (events, eosed) => { - if (events.length > 0) { - setEvents(events) - } - if (eosed) { - setLoading(false) - setHasMore(events.length > 0) - } - }, - onNew: (event) => { - setEvents((oldEvents) => - [event, ...oldEvents].sort((a, b) => b.created_at - a.created_at) - ) - } - }, - { startLogin } - ) - setTimelineKey(timelineKey) - return closer - } - - const promise = init() - return () => { - promise.then((closer) => closer()) - } - }, [event]) - - useEffect(() => { - const options = { - root: null, - rootMargin: '10px', - threshold: 0.1 - } - - const loadMore = async () => { - if (showCount < events.length) { - setShowCount((prev) => prev + SHOW_COUNT) - // preload more - if (events.length - showCount > LIMIT / 2) { - return + const isReplaceable = isReplaceableEvent(event.kind) + const key = isReplaceable ? getReplaceableCoordinateFromEvent(event) : event.id + filters.push({ + '#q': [key], + kinds: [ + kinds.ShortTextNote, + kinds.LongFormArticle, + ExtendedKind.COMMENT, + ExtendedKind.POLL + ] + }) + if (isReplaceable) { + filters.push({ + '#a': [key], + kinds: [kinds.Highlights] + }) + } else { + filters.push({ + '#e': [key], + kinds: [kinds.Highlights] + }) } } - - if (!timelineKey || loading || !hasMore) return - setLoading(true) - const newEvents = await client.loadMoreTimeline( - timelineKey, - events.length ? events[events.length - 1].created_at - 1 : dayjs().unix(), - LIMIT - ) - setLoading(false) - if (newEvents.length === 0) { - setHasMore(false) - return + if (externalContent) { + filters.push({ + '#r': [externalContent], + kinds: [kinds.Highlights] + }) } - setEvents((oldEvents) => [...oldEvents, ...newEvents]) + const urls = Array.from(relaySet) + setSubRequests(filters.map((filter) => ({ urls, filter }))) } - const observerInstance = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting && hasMore) { - loadMore() - } - }, options) + init() + }, [event]) - const currentBottomRef = bottomRef.current - - if (currentBottomRef) { - observerInstance.observe(currentBottomRef) - } - - return () => { - if (observerInstance && currentBottomRef) { - observerInstance.unobserve(currentBottomRef) - } - } - }, [timelineKey, loading, hasMore, events, showCount]) - - return ( -
-
-
- {events.slice(0, showCount).map((event) => { - if (hideUntrustedInteractions && !isUserTrusted(event.pubkey)) { - return null - } - return - })} -
- {hasMore || loading ? ( -
- -
- ) : ( -
{t('no more notes')}
- )} -
-
-
- ) + return }