perf: optimize list rendering

This commit is contained in:
codytseng
2025-02-14 21:44:13 +08:00
parent 41d46b1a13
commit 978244dd22
3 changed files with 60 additions and 46 deletions

View File

@@ -20,9 +20,9 @@ import PullToRefresh from 'react-simple-pull-to-refresh'
import NoteCard from '../NoteCard'
import PictureNoteCard from '../PictureNoteCard'
const NORMAL_RELAY_LIMIT = 100
const ALGO_RELAY_LIMIT = 500
const PICTURE_NOTE_LIMIT = 30
const LIMIT = 100
const ALGO_LIMIT = 500
const SHOW_COUNT = 20
export default function NoteList({
relayUrls,
@@ -45,22 +45,17 @@ export default function NoteList({
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
const [events, setEvents] = useState<Event[]>([])
const [newEvents, setNewEvents] = useState<Event[]>([])
const [showCount, setShowCount] = useState(SHOW_COUNT)
const [hasMore, setHasMore] = useState<boolean>(true)
const [refreshing, setRefreshing] = useState(true)
const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode())
const bottomRef = useRef<HTMLDivElement | null>(null)
const isPictures = useMemo(() => listMode === 'pictures', [listMode])
const noteFilter = useMemo(() => {
if (isPictures) {
return {
kinds: [PICTURE_EVENT_KIND],
limit: PICTURE_NOTE_LIMIT,
...filter
}
}
return {
kinds: [kinds.ShortTextNote, kinds.Repost, PICTURE_EVENT_KIND],
limit: NORMAL_RELAY_LIMIT,
kinds: isPictures
? [PICTURE_EVENT_KIND]
: [kinds.ShortTextNote, kinds.Repost, PICTURE_EVENT_KIND],
...filter
}
}, [JSON.stringify(filter), isPictures])
@@ -80,12 +75,11 @@ export default function NoteList({
const relayInfos = await relayInfoService.getRelayInfos(relayUrls)
areAlgoRelays = relayInfos.every((relayInfo) => checkAlgoRelay(relayInfo))
}
const filter = areAlgoRelays ? { ...noteFilter, limit: ALGO_RELAY_LIMIT } : noteFilter
let eventCount = 0
const { closer, timelineKey } = await client.subscribeTimeline(
[...relayUrls],
filter,
{ ...noteFilter, limit: areAlgoRelays ? ALGO_LIMIT : LIMIT },
{
onEvents: (events, eosed) => {
if (eventCount > events.length) return
@@ -124,23 +118,25 @@ export default function NoteList({
}, [JSON.stringify(relayUrls), noteFilter, refreshCount])
const loadMore = useCallback(async () => {
if (!timelineKey || refreshing || !hasMore) return
if (showCount < events.length) {
setShowCount((prev) => prev + SHOW_COUNT)
return
}
if (!timelineKey || refreshing || !hasMore) return
const newEvents = await client.loadMoreTimeline(
timelineKey,
events.length ? events[events.length - 1].created_at - 1 : dayjs().unix(),
noteFilter.limit
LIMIT
)
if (newEvents.length === 0) {
setHasMore(false)
return
}
setEvents((oldEvents) => [...oldEvents, ...newEvents])
}, [timelineKey, refreshing, hasMore, events, noteFilter])
}, [timelineKey, refreshing, hasMore, events, noteFilter, showCount])
useEffect(() => {
if (refreshing) return
const options = {
root: null,
rootMargin: '10px',
@@ -164,7 +160,7 @@ export default function NoteList({
observerInstance.unobserve(currentBottomRef)
}
}
}, [refreshing, loadMore])
}, [loadMore])
const showNewEvents = () => {
setEvents((oldEvents) => [...newEvents, ...oldEvents])
@@ -177,6 +173,7 @@ export default function NoteList({
listMode={listMode}
setListMode={(listMode) => {
setListMode(listMode)
setShowCount(SHOW_COUNT)
topRef.current?.scrollIntoView({ behavior: 'instant', block: 'end' })
storage.setNoteListMode(listMode)
}}
@@ -190,27 +187,29 @@ export default function NoteList({
pullingContent=""
>
<div>
{newEvents.filter((event: Event) => {
return (
(!filterMutedNotes || !mutePubkeys.includes(event.pubkey)) &&
(listMode !== 'posts' || !isReplyNoteEvent(event))
)
}).length > 0 && (
<div className="flex justify-center w-full my-2">
<Button size="lg" onClick={showNewEvents}>
{t('show new notes')}
</Button>
</div>
)}
{events.length > 0 &&
newEvents.filter((event: Event) => {
return (
(!filterMutedNotes || !mutePubkeys.includes(event.pubkey)) &&
(listMode !== 'posts' || !isReplyNoteEvent(event))
)
}).length > 0 && (
<div className="flex justify-center w-full my-2">
<Button size="lg" onClick={showNewEvents}>
{t('show new notes')}
</Button>
</div>
)}
{isPictures ? (
<PictureNoteCardMasonry
className="px-2 sm:px-4 mt-2"
columnCount={isLargeScreen ? 3 : 2}
events={events}
events={events.slice(0, showCount)}
/>
) : (
<div>
{events
.slice(0, showCount)
.filter((event: Event) => listMode !== 'posts' || !isReplyNoteEvent(event))
.map((event) => (
<NoteCard

View File

@@ -26,6 +26,7 @@ import { FormattedTimestamp } from '../FormattedTimestamp'
import UserAvatar from '../UserAvatar'
const LIMIT = 100
const SHOW_COUNT = 30
const NotificationList = forwardRef((_, ref) => {
const { t } = useTranslation()
@@ -34,6 +35,7 @@ const NotificationList = forwardRef((_, ref) => {
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
const [refreshing, setRefreshing] = useState(true)
const [notifications, setNotifications] = useState<Event[]>([])
const [showCount, setShowCount] = useState(SHOW_COUNT)
const [until, setUntil] = useState<number | undefined>(dayjs().unix())
const bottomRef = useRef<HTMLDivElement | null>(null)
useImperativeHandle(
@@ -70,15 +72,23 @@ const NotificationList = forwardRef((_, ref) => {
onEvents: (events, eosed) => {
if (eventCount > events.length) return
eventCount = events.length
setUntil(events.length >= LIMIT ? events[events.length - 1].created_at - 1 : undefined)
setNotifications(events.filter((event) => event.pubkey !== pubkey))
if (eosed) {
setRefreshing(false)
setUntil(events.length >= 0 ? events[events.length - 1].created_at - 1 : undefined)
}
},
onNew: (event) => {
if (event.pubkey === pubkey) return
setNotifications((oldEvents) => [event, ...oldEvents])
setNotifications((oldEvents) => {
const index = oldEvents.findIndex(
(oldEvent) => oldEvent.created_at < event.created_at
)
if (index === -1) {
return [...oldEvents, event]
}
return [...oldEvents.slice(0, index), event, ...oldEvents.slice(index)]
})
}
}
)
@@ -93,26 +103,30 @@ const NotificationList = forwardRef((_, ref) => {
}, [pubkey, refreshCount])
const loadMore = useCallback(async () => {
if (showCount < notifications.length) {
setShowCount((count) => count + SHOW_COUNT)
return
}
if (!pubkey || !timelineKey || !until || refreshing) return
const notifications = await client.loadMoreTimeline(timelineKey, until, LIMIT)
if (notifications.length === 0) {
const newNotifications = await client.loadMoreTimeline(timelineKey, until, LIMIT)
if (newNotifications.length === 0) {
setUntil(undefined)
return
}
if (notifications.length > 0) {
if (newNotifications.length > 0) {
setNotifications((oldNotifications) => [
...oldNotifications,
...notifications.filter((event) => event.pubkey !== pubkey)
...newNotifications.filter((event) => event.pubkey !== pubkey)
])
}
setUntil(notifications[notifications.length - 1].created_at - 1)
}, [pubkey, timelineKey, until, refreshing])
setUntil(newNotifications[newNotifications.length - 1].created_at - 1)
}, [pubkey, timelineKey, until, refreshing, showCount, notifications])
useEffect(() => {
if (refreshing) return
const options = {
root: null,
rootMargin: '10px',
@@ -136,7 +150,7 @@ const NotificationList = forwardRef((_, ref) => {
observerInstance.unobserve(currentBottomRef)
}
}
}, [refreshing, loadMore])
}, [loadMore])
return (
<PullToRefresh
@@ -147,7 +161,7 @@ const NotificationList = forwardRef((_, ref) => {
pullingContent=""
>
<div>
{notifications.map((notification) => (
{notifications.slice(0, showCount).map((notification) => (
<NotificationItem key={notification.id} notification={notification} />
))}
<div className="text-center text-sm text-muted-foreground">

View File

@@ -320,7 +320,8 @@ class ClientService extends EventTarget {
timeline.refs = newRefs.concat(timeline.refs)
onEvents(newEvents.concat(cachedEvents), true)
}
}
},
eoseTimeout: 10000 // 10s
})
}
})