feat: add highlights to quotes
This commit is contained in:
@@ -2,10 +2,11 @@ import { cn } from '@/lib/utils'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useRef, useEffect, useState } from 'react'
|
import { useRef, useEffect, useState } from 'react'
|
||||||
|
|
||||||
export type TTabValue = 'replies' | 'reactions'
|
export type TTabValue = 'replies' | 'reactions' | 'quotes'
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ value: 'replies', label: 'Replies' },
|
{ value: 'replies', label: 'Replies' },
|
||||||
{ value: 'reactions', label: 'Reactions' }
|
{ value: 'reactions', label: 'Reactions' },
|
||||||
|
{ value: 'quotes', label: 'Quotes' }
|
||||||
] as { value: TTabValue; label: string }[]
|
] as { value: TTabValue; label: string }[]
|
||||||
|
|
||||||
export function Tabs({
|
export function Tabs({
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
|
|||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import HideUntrustedContentButton from '../HideUntrustedContentButton'
|
import HideUntrustedContentButton from '../HideUntrustedContentButton'
|
||||||
|
import QuoteList from '../QuoteList'
|
||||||
|
import ReactionList from '../ReactionList'
|
||||||
import ReplyNoteList from '../ReplyNoteList'
|
import ReplyNoteList from '../ReplyNoteList'
|
||||||
import { Tabs, TTabValue } from './Tabs'
|
import { Tabs, TTabValue } from './Tabs'
|
||||||
import ReactionList from '../ReactionList'
|
|
||||||
|
|
||||||
export default function ExternalContentInteractions({
|
export default function ExternalContentInteractions({
|
||||||
pageIndex,
|
pageIndex,
|
||||||
@@ -22,6 +23,9 @@ export default function ExternalContentInteractions({
|
|||||||
case 'reactions':
|
case 'reactions':
|
||||||
list = <ReactionList stuff={externalContent} />
|
list = <ReactionList stuff={externalContent} />
|
||||||
break
|
break
|
||||||
|
case 'quotes':
|
||||||
|
list = <QuoteList stuff={externalContent} />
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function NoteInteractions({
|
|||||||
list = <ReplyNoteList index={pageIndex} stuff={event} />
|
list = <ReplyNoteList index={pageIndex} stuff={event} />
|
||||||
break
|
break
|
||||||
case 'quotes':
|
case 'quotes':
|
||||||
list = <QuoteList event={event} />
|
list = <QuoteList stuff={event} />
|
||||||
break
|
break
|
||||||
case 'reactions':
|
case 'reactions':
|
||||||
list = <ReactionList stuff={event} />
|
list = <ReactionList stuff={event} />
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const NoteList = forwardRef(
|
|||||||
showNewNotesDirectly = false
|
showNewNotesDirectly = false
|
||||||
}: {
|
}: {
|
||||||
subRequests: TFeedSubRequest[]
|
subRequests: TFeedSubRequest[]
|
||||||
showKinds: number[]
|
showKinds?: number[]
|
||||||
filterMutedNotes?: boolean
|
filterMutedNotes?: boolean
|
||||||
hideReplies?: boolean
|
hideReplies?: boolean
|
||||||
hideUntrustedNotes?: boolean
|
hideUntrustedNotes?: boolean
|
||||||
@@ -236,7 +236,7 @@ const NoteList = forwardRef(
|
|||||||
setNewEvents([])
|
setNewEvents([])
|
||||||
setHasMore(true)
|
setHasMore(true)
|
||||||
|
|
||||||
if (showKinds.length === 0) {
|
if (showKinds?.length === 0 && subRequests.every(({ filter }) => !filter.kinds)) {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
setHasMore(false)
|
setHasMore(false)
|
||||||
return () => {}
|
return () => {}
|
||||||
@@ -246,7 +246,7 @@ const NoteList = forwardRef(
|
|||||||
subRequests.map(({ urls, filter }) => ({
|
subRequests.map(({ urls, filter }) => ({
|
||||||
urls,
|
urls,
|
||||||
filter: {
|
filter: {
|
||||||
kinds: showKinds,
|
kinds: showKinds ?? [],
|
||||||
...filter,
|
...filter,
|
||||||
limit: areAlgoRelays ? ALGO_LIMIT : LIMIT
|
limit: areAlgoRelays ? ALGO_LIMIT : LIMIT
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,156 +1,61 @@
|
|||||||
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
|
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
|
||||||
|
import { useStuff } from '@/hooks/useStuff'
|
||||||
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
|
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
|
||||||
import { useNostr } from '@/providers/NostrProvider'
|
|
||||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
|
||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import dayjs from 'dayjs'
|
import { TFeedSubRequest } from '@/types'
|
||||||
import { Event, kinds } from 'nostr-tools'
|
import { Event, Filter, kinds } from 'nostr-tools'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import NoteList from '../NoteList'
|
||||||
import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard'
|
|
||||||
|
|
||||||
const LIMIT = 100
|
export default function QuoteList({ stuff }: { stuff: Event | string }) {
|
||||||
const SHOW_COUNT = 10
|
const { event, externalContent } = useStuff(stuff)
|
||||||
|
const [subRequests, setSubRequests] = useState<TFeedSubRequest[]>([])
|
||||||
export default function QuoteList({ event, className }: { event: Event; className?: string }) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { startLogin } = useNostr()
|
|
||||||
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
|
|
||||||
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
|
|
||||||
const [events, setEvents] = useState<Event[]>([])
|
|
||||||
const [showCount, setShowCount] = useState(SHOW_COUNT)
|
|
||||||
const [hasMore, setHasMore] = useState<boolean>(true)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const bottomRef = useRef<HTMLDivElement | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function init() {
|
async function init() {
|
||||||
setLoading(true)
|
const relaySet = new Set(BIG_RELAY_URLS)
|
||||||
setEvents([])
|
const filters: Filter[] = []
|
||||||
setHasMore(true)
|
if (event) {
|
||||||
|
|
||||||
const relayList = await client.fetchRelayList(event.pubkey)
|
const relayList = await client.fetchRelayList(event.pubkey)
|
||||||
const relayUrls = relayList.read.concat(BIG_RELAY_URLS)
|
relayList.read.slice(0, 5).forEach((url) => relaySet.add(url))
|
||||||
const seenOn = client.getSeenEventRelayUrls(event.id)
|
const seenOn = client.getSeenEventRelayUrls(event.id)
|
||||||
relayUrls.unshift(...seenOn)
|
seenOn.forEach((url) => relaySet.add(url))
|
||||||
|
|
||||||
const { closer, timelineKey } = await client.subscribeTimeline(
|
const isReplaceable = isReplaceableEvent(event.kind)
|
||||||
[
|
const key = isReplaceable ? getReplaceableCoordinateFromEvent(event) : event.id
|
||||||
{
|
filters.push({
|
||||||
urls: relayUrls,
|
'#q': [key],
|
||||||
filter: {
|
|
||||||
'#q': [
|
|
||||||
isReplaceableEvent(event.kind) ? getReplaceableCoordinateFromEvent(event) : event.id
|
|
||||||
],
|
|
||||||
kinds: [
|
kinds: [
|
||||||
kinds.ShortTextNote,
|
kinds.ShortTextNote,
|
||||||
kinds.Highlights,
|
|
||||||
kinds.LongFormArticle,
|
kinds.LongFormArticle,
|
||||||
ExtendedKind.COMMENT,
|
ExtendedKind.COMMENT,
|
||||||
ExtendedKind.POLL
|
ExtendedKind.POLL
|
||||||
],
|
]
|
||||||
limit: LIMIT
|
})
|
||||||
|
if (isReplaceable) {
|
||||||
|
filters.push({
|
||||||
|
'#a': [key],
|
||||||
|
kinds: [kinds.Highlights]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
filters.push({
|
||||||
|
'#e': [key],
|
||||||
|
kinds: [kinds.Highlights]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
if (externalContent) {
|
||||||
{
|
filters.push({
|
||||||
onEvents: (events, eosed) => {
|
'#r': [externalContent],
|
||||||
if (events.length > 0) {
|
kinds: [kinds.Highlights]
|
||||||
setEvents(events)
|
})
|
||||||
}
|
}
|
||||||
if (eosed) {
|
const urls = Array.from(relaySet)
|
||||||
setLoading(false)
|
setSubRequests(filters.map((filter) => ({ urls, filter })))
|
||||||
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()
|
init()
|
||||||
return () => {
|
|
||||||
promise.then((closer) => closer())
|
|
||||||
}
|
|
||||||
}, [event])
|
}, [event])
|
||||||
|
|
||||||
useEffect(() => {
|
return <NoteList subRequests={subRequests} />
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
setEvents((oldEvents) => [...oldEvents, ...newEvents])
|
|
||||||
}
|
|
||||||
|
|
||||||
const observerInstance = new IntersectionObserver((entries) => {
|
|
||||||
if (entries[0].isIntersecting && hasMore) {
|
|
||||||
loadMore()
|
|
||||||
}
|
|
||||||
}, options)
|
|
||||||
|
|
||||||
const currentBottomRef = bottomRef.current
|
|
||||||
|
|
||||||
if (currentBottomRef) {
|
|
||||||
observerInstance.observe(currentBottomRef)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (observerInstance && currentBottomRef) {
|
|
||||||
observerInstance.unobserve(currentBottomRef)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [timelineKey, loading, hasMore, events, showCount])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<div className="min-h-[80vh]">
|
|
||||||
<div>
|
|
||||||
{events.slice(0, showCount).map((event) => {
|
|
||||||
if (hideUntrustedInteractions && !isUserTrusted(event.pubkey)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return <NoteCard key={event.id} className="w-full" event={event} />
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{hasMore || loading ? (
|
|
||||||
<div ref={bottomRef}>
|
|
||||||
<NoteCardLoadingSkeleton />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center text-sm text-muted-foreground mt-2">{t('no more notes')}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="h-40" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user