perf: optimize list rendering
This commit is contained in:
@@ -20,9 +20,9 @@ import PullToRefresh from 'react-simple-pull-to-refresh'
|
|||||||
import NoteCard from '../NoteCard'
|
import NoteCard from '../NoteCard'
|
||||||
import PictureNoteCard from '../PictureNoteCard'
|
import PictureNoteCard from '../PictureNoteCard'
|
||||||
|
|
||||||
const NORMAL_RELAY_LIMIT = 100
|
const LIMIT = 100
|
||||||
const ALGO_RELAY_LIMIT = 500
|
const ALGO_LIMIT = 500
|
||||||
const PICTURE_NOTE_LIMIT = 30
|
const SHOW_COUNT = 20
|
||||||
|
|
||||||
export default function NoteList({
|
export default function NoteList({
|
||||||
relayUrls,
|
relayUrls,
|
||||||
@@ -45,22 +45,17 @@ export default function NoteList({
|
|||||||
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
|
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
|
||||||
const [events, setEvents] = useState<Event[]>([])
|
const [events, setEvents] = useState<Event[]>([])
|
||||||
const [newEvents, setNewEvents] = useState<Event[]>([])
|
const [newEvents, setNewEvents] = useState<Event[]>([])
|
||||||
|
const [showCount, setShowCount] = useState(SHOW_COUNT)
|
||||||
const [hasMore, setHasMore] = useState<boolean>(true)
|
const [hasMore, setHasMore] = useState<boolean>(true)
|
||||||
const [refreshing, setRefreshing] = useState(true)
|
const [refreshing, setRefreshing] = useState(true)
|
||||||
const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode())
|
const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode())
|
||||||
const bottomRef = useRef<HTMLDivElement | null>(null)
|
const bottomRef = useRef<HTMLDivElement | null>(null)
|
||||||
const isPictures = useMemo(() => listMode === 'pictures', [listMode])
|
const isPictures = useMemo(() => listMode === 'pictures', [listMode])
|
||||||
const noteFilter = useMemo(() => {
|
const noteFilter = useMemo(() => {
|
||||||
if (isPictures) {
|
|
||||||
return {
|
return {
|
||||||
kinds: [PICTURE_EVENT_KIND],
|
kinds: isPictures
|
||||||
limit: PICTURE_NOTE_LIMIT,
|
? [PICTURE_EVENT_KIND]
|
||||||
...filter
|
: [kinds.ShortTextNote, kinds.Repost, PICTURE_EVENT_KIND],
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
kinds: [kinds.ShortTextNote, kinds.Repost, PICTURE_EVENT_KIND],
|
|
||||||
limit: NORMAL_RELAY_LIMIT,
|
|
||||||
...filter
|
...filter
|
||||||
}
|
}
|
||||||
}, [JSON.stringify(filter), isPictures])
|
}, [JSON.stringify(filter), isPictures])
|
||||||
@@ -80,12 +75,11 @@ export default function NoteList({
|
|||||||
const relayInfos = await relayInfoService.getRelayInfos(relayUrls)
|
const relayInfos = await relayInfoService.getRelayInfos(relayUrls)
|
||||||
areAlgoRelays = relayInfos.every((relayInfo) => checkAlgoRelay(relayInfo))
|
areAlgoRelays = relayInfos.every((relayInfo) => checkAlgoRelay(relayInfo))
|
||||||
}
|
}
|
||||||
const filter = areAlgoRelays ? { ...noteFilter, limit: ALGO_RELAY_LIMIT } : noteFilter
|
|
||||||
|
|
||||||
let eventCount = 0
|
let eventCount = 0
|
||||||
const { closer, timelineKey } = await client.subscribeTimeline(
|
const { closer, timelineKey } = await client.subscribeTimeline(
|
||||||
[...relayUrls],
|
[...relayUrls],
|
||||||
filter,
|
{ ...noteFilter, limit: areAlgoRelays ? ALGO_LIMIT : LIMIT },
|
||||||
{
|
{
|
||||||
onEvents: (events, eosed) => {
|
onEvents: (events, eosed) => {
|
||||||
if (eventCount > events.length) return
|
if (eventCount > events.length) return
|
||||||
@@ -124,23 +118,25 @@ export default function NoteList({
|
|||||||
}, [JSON.stringify(relayUrls), noteFilter, refreshCount])
|
}, [JSON.stringify(relayUrls), noteFilter, refreshCount])
|
||||||
|
|
||||||
const loadMore = useCallback(async () => {
|
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(
|
const newEvents = await client.loadMoreTimeline(
|
||||||
timelineKey,
|
timelineKey,
|
||||||
events.length ? events[events.length - 1].created_at - 1 : dayjs().unix(),
|
events.length ? events[events.length - 1].created_at - 1 : dayjs().unix(),
|
||||||
noteFilter.limit
|
LIMIT
|
||||||
)
|
)
|
||||||
if (newEvents.length === 0) {
|
if (newEvents.length === 0) {
|
||||||
setHasMore(false)
|
setHasMore(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setEvents((oldEvents) => [...oldEvents, ...newEvents])
|
setEvents((oldEvents) => [...oldEvents, ...newEvents])
|
||||||
}, [timelineKey, refreshing, hasMore, events, noteFilter])
|
}, [timelineKey, refreshing, hasMore, events, noteFilter, showCount])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (refreshing) return
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
root: null,
|
root: null,
|
||||||
rootMargin: '10px',
|
rootMargin: '10px',
|
||||||
@@ -164,7 +160,7 @@ export default function NoteList({
|
|||||||
observerInstance.unobserve(currentBottomRef)
|
observerInstance.unobserve(currentBottomRef)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [refreshing, loadMore])
|
}, [loadMore])
|
||||||
|
|
||||||
const showNewEvents = () => {
|
const showNewEvents = () => {
|
||||||
setEvents((oldEvents) => [...newEvents, ...oldEvents])
|
setEvents((oldEvents) => [...newEvents, ...oldEvents])
|
||||||
@@ -177,6 +173,7 @@ export default function NoteList({
|
|||||||
listMode={listMode}
|
listMode={listMode}
|
||||||
setListMode={(listMode) => {
|
setListMode={(listMode) => {
|
||||||
setListMode(listMode)
|
setListMode(listMode)
|
||||||
|
setShowCount(SHOW_COUNT)
|
||||||
topRef.current?.scrollIntoView({ behavior: 'instant', block: 'end' })
|
topRef.current?.scrollIntoView({ behavior: 'instant', block: 'end' })
|
||||||
storage.setNoteListMode(listMode)
|
storage.setNoteListMode(listMode)
|
||||||
}}
|
}}
|
||||||
@@ -190,7 +187,8 @@ export default function NoteList({
|
|||||||
pullingContent=""
|
pullingContent=""
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{newEvents.filter((event: Event) => {
|
{events.length > 0 &&
|
||||||
|
newEvents.filter((event: Event) => {
|
||||||
return (
|
return (
|
||||||
(!filterMutedNotes || !mutePubkeys.includes(event.pubkey)) &&
|
(!filterMutedNotes || !mutePubkeys.includes(event.pubkey)) &&
|
||||||
(listMode !== 'posts' || !isReplyNoteEvent(event))
|
(listMode !== 'posts' || !isReplyNoteEvent(event))
|
||||||
@@ -206,11 +204,12 @@ export default function NoteList({
|
|||||||
<PictureNoteCardMasonry
|
<PictureNoteCardMasonry
|
||||||
className="px-2 sm:px-4 mt-2"
|
className="px-2 sm:px-4 mt-2"
|
||||||
columnCount={isLargeScreen ? 3 : 2}
|
columnCount={isLargeScreen ? 3 : 2}
|
||||||
events={events}
|
events={events.slice(0, showCount)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{events
|
{events
|
||||||
|
.slice(0, showCount)
|
||||||
.filter((event: Event) => listMode !== 'posts' || !isReplyNoteEvent(event))
|
.filter((event: Event) => listMode !== 'posts' || !isReplyNoteEvent(event))
|
||||||
.map((event) => (
|
.map((event) => (
|
||||||
<NoteCard
|
<NoteCard
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { FormattedTimestamp } from '../FormattedTimestamp'
|
|||||||
import UserAvatar from '../UserAvatar'
|
import UserAvatar from '../UserAvatar'
|
||||||
|
|
||||||
const LIMIT = 100
|
const LIMIT = 100
|
||||||
|
const SHOW_COUNT = 30
|
||||||
|
|
||||||
const NotificationList = forwardRef((_, ref) => {
|
const NotificationList = forwardRef((_, ref) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -34,6 +35,7 @@ const NotificationList = forwardRef((_, ref) => {
|
|||||||
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
|
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
|
||||||
const [refreshing, setRefreshing] = useState(true)
|
const [refreshing, setRefreshing] = useState(true)
|
||||||
const [notifications, setNotifications] = useState<Event[]>([])
|
const [notifications, setNotifications] = useState<Event[]>([])
|
||||||
|
const [showCount, setShowCount] = useState(SHOW_COUNT)
|
||||||
const [until, setUntil] = useState<number | undefined>(dayjs().unix())
|
const [until, setUntil] = useState<number | undefined>(dayjs().unix())
|
||||||
const bottomRef = useRef<HTMLDivElement | null>(null)
|
const bottomRef = useRef<HTMLDivElement | null>(null)
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
@@ -70,15 +72,23 @@ const NotificationList = forwardRef((_, ref) => {
|
|||||||
onEvents: (events, eosed) => {
|
onEvents: (events, eosed) => {
|
||||||
if (eventCount > events.length) return
|
if (eventCount > events.length) return
|
||||||
eventCount = events.length
|
eventCount = events.length
|
||||||
setUntil(events.length >= LIMIT ? events[events.length - 1].created_at - 1 : undefined)
|
|
||||||
setNotifications(events.filter((event) => event.pubkey !== pubkey))
|
setNotifications(events.filter((event) => event.pubkey !== pubkey))
|
||||||
if (eosed) {
|
if (eosed) {
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
|
setUntil(events.length >= 0 ? events[events.length - 1].created_at - 1 : undefined)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onNew: (event) => {
|
onNew: (event) => {
|
||||||
if (event.pubkey === pubkey) return
|
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])
|
}, [pubkey, refreshCount])
|
||||||
|
|
||||||
const loadMore = useCallback(async () => {
|
const loadMore = useCallback(async () => {
|
||||||
|
if (showCount < notifications.length) {
|
||||||
|
setShowCount((count) => count + SHOW_COUNT)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!pubkey || !timelineKey || !until || refreshing) 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)
|
setUntil(undefined)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notifications.length > 0) {
|
if (newNotifications.length > 0) {
|
||||||
setNotifications((oldNotifications) => [
|
setNotifications((oldNotifications) => [
|
||||||
...oldNotifications,
|
...oldNotifications,
|
||||||
...notifications.filter((event) => event.pubkey !== pubkey)
|
...newNotifications.filter((event) => event.pubkey !== pubkey)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
setUntil(notifications[notifications.length - 1].created_at - 1)
|
setUntil(newNotifications[newNotifications.length - 1].created_at - 1)
|
||||||
}, [pubkey, timelineKey, until, refreshing])
|
}, [pubkey, timelineKey, until, refreshing, showCount, notifications])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (refreshing) return
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
root: null,
|
root: null,
|
||||||
rootMargin: '10px',
|
rootMargin: '10px',
|
||||||
@@ -136,7 +150,7 @@ const NotificationList = forwardRef((_, ref) => {
|
|||||||
observerInstance.unobserve(currentBottomRef)
|
observerInstance.unobserve(currentBottomRef)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [refreshing, loadMore])
|
}, [loadMore])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PullToRefresh
|
<PullToRefresh
|
||||||
@@ -147,7 +161,7 @@ const NotificationList = forwardRef((_, ref) => {
|
|||||||
pullingContent=""
|
pullingContent=""
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{notifications.map((notification) => (
|
{notifications.slice(0, showCount).map((notification) => (
|
||||||
<NotificationItem key={notification.id} notification={notification} />
|
<NotificationItem key={notification.id} notification={notification} />
|
||||||
))}
|
))}
|
||||||
<div className="text-center text-sm text-muted-foreground">
|
<div className="text-center text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -320,7 +320,8 @@ class ClientService extends EventTarget {
|
|||||||
timeline.refs = newRefs.concat(timeline.refs)
|
timeline.refs = newRefs.concat(timeline.refs)
|
||||||
onEvents(newEvents.concat(cachedEvents), true)
|
onEvents(newEvents.concat(cachedEvents), true)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
eoseTimeout: 10000 // 10s
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user