From ee21e196250703bdf347e53419c17f643dc31101 Mon Sep 17 00:00:00 2001 From: codytseng Date: Thu, 23 Jan 2025 15:53:02 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=F0=9F=8F=97=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Nip22ReplyNoteList/index.tsx | 41 +++++++++-------- src/components/NoteList/index.tsx | 49 +++++++++++---------- src/components/NotificationList/index.tsx | 47 ++++++++++---------- src/components/ReplyNoteList/index.tsx | 41 +++++++++-------- src/providers/FeedProvider.tsx | 44 ++++++++++-------- src/services/client.service.ts | 2 +- 6 files changed, 120 insertions(+), 104 deletions(-) diff --git a/src/components/Nip22ReplyNoteList/index.tsx b/src/components/Nip22ReplyNoteList/index.tsx index 26596f16..4342938b 100644 --- a/src/components/Nip22ReplyNoteList/index.tsx +++ b/src/components/Nip22ReplyNoteList/index.tsx @@ -7,7 +7,7 @@ import { useNoteStats } from '@/providers/NoteStatsProvider' import client from '@/services/client.service' import dayjs from 'dayjs' import { Event as NEvent } from 'nostr-tools' -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import ReplyNote from '../ReplyNote' import { isCommentEvent } from '@/lib/event' @@ -116,7 +116,7 @@ export default function Nip22ReplyNoteList({ setReplyMap(replyMap) }, [replies, event.id, updateNoteReplyCount]) - const loadMore = async () => { + const loadMore = useCallback(async () => { if (loading || !until || !timelineKey) return setLoading(true) @@ -127,24 +127,27 @@ export default function Nip22ReplyNoteList({ } setUntil(events.length ? events[events.length - 1].created_at - 1 : undefined) setLoading(false) - } + }, [loading, until, timelineKey]) - const onNewReply = (evt: NEvent) => { - setReplies((pre) => { - if (pre.some((reply) => reply.id === evt.id)) return pre - return [...pre, evt] - }) - if (evt.pubkey === pubkey) { - setTimeout(() => { - if (bottomRef.current) { - bottomRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) - } - highlightReply(evt.id, false) - }, 100) - } - } + const onNewReply = useCallback( + (evt: NEvent) => { + setReplies((pre) => { + if (pre.some((reply) => reply.id === evt.id)) return pre + return [...pre, evt] + }) + if (evt.pubkey === pubkey) { + setTimeout(() => { + if (bottomRef.current) { + bottomRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) + } + highlightReply(evt.id, false) + }, 100) + } + }, + [pubkey] + ) - const highlightReply = (eventId: string, scrollTo = true) => { + const highlightReply = useCallback((eventId: string, scrollTo = true) => { if (scrollTo) { const ref = replyRefs.current[eventId] if (ref) { @@ -155,7 +158,7 @@ export default function Nip22ReplyNoteList({ setTimeout(() => { setHighlightReplyId((pre) => (pre === eventId ? undefined : pre)) }, 1500) - } + }, []) return ( <> diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index 3201e680..e11558fb 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -1,7 +1,7 @@ import { Button } from '@/components/ui/button' import { PICTURE_EVENT_KIND } from '@/constants' -import { useFetchRelayInfos } from '@/hooks' import { isReplyNoteEvent } from '@/lib/event' +import { checkAlgoRelay } from '@/lib/relay' import { cn } from '@/lib/utils' import { useMuteList } from '@/providers/MuteListProvider' import { useNostr } from '@/providers/NostrProvider' @@ -11,7 +11,7 @@ import storage from '@/services/storage.service' import { TNoteListMode } from '@/types' import dayjs from 'dayjs' import { Event, Filter, kinds } from 'nostr-tools' -import { ReactNode, useEffect, useMemo, useRef, useState } from 'react' +import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import PullToRefresh from 'react-simple-pull-to-refresh' import NoteCard from '../NoteCard' @@ -36,7 +36,6 @@ export default function NoteList({ const { isLargeScreen } = useScreenSize() const { signEvent, checkLogin } = useNostr() const { mutePubkeys } = useMuteList() - const { areAlgoRelays } = useFetchRelayInfos([...relayUrls]) const [refreshCount, setRefreshCount] = useState(0) const [timelineKey, setTimelineKey] = useState(undefined) const [events, setEvents] = useState([]) @@ -56,10 +55,10 @@ export default function NoteList({ } return { kinds: [kinds.ShortTextNote, kinds.Repost, PICTURE_EVENT_KIND], - limit: areAlgoRelays ? ALGO_RELAY_LIMIT : NORMAL_RELAY_LIMIT, + limit: NORMAL_RELAY_LIMIT, ...filter } - }, [JSON.stringify(filter), areAlgoRelays, isPictures]) + }, [JSON.stringify(filter), isPictures]) useEffect(() => { if (relayUrls.length === 0) return @@ -70,10 +69,14 @@ export default function NoteList({ setNewEvents([]) setHasMore(true) + const relayInfos = await client.fetchRelayInfos(relayUrls) + const 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], - noteFilter, + filter, { onEvents: (events, eosed) => { if (eventCount > events.length) return @@ -112,7 +115,22 @@ export default function NoteList({ return () => { promise.then((closer) => closer()) } - }, [JSON.stringify(relayUrls), noteFilter, areAlgoRelays, refreshCount]) + }, [JSON.stringify(relayUrls), noteFilter, refreshCount]) + + const loadMore = useCallback(async () => { + if (!timelineKey || refreshing || !hasMore) return + + const newEvents = await client.loadMoreTimeline( + timelineKey, + events.length ? events[events.length - 1].created_at - 1 : dayjs().unix(), + noteFilter.limit + ) + if (newEvents.length === 0) { + setHasMore(false) + return + } + setEvents((oldEvents) => [...oldEvents, ...newEvents]) + }, [timelineKey, refreshing, hasMore, events, noteFilter]) useEffect(() => { if (refreshing) return @@ -140,22 +158,7 @@ export default function NoteList({ observerInstance.unobserve(currentBottomRef) } } - }, [refreshing, hasMore, events, timelineKey, bottomRef]) - - const loadMore = async () => { - if (!timelineKey || refreshing) return - - const newEvents = await client.loadMoreTimeline( - timelineKey, - events.length ? events[events.length - 1].created_at - 1 : dayjs().unix(), - noteFilter.limit - ) - if (newEvents.length === 0) { - setHasMore(false) - return - } - setEvents((oldEvents) => [...oldEvents, ...newEvents]) - } + }, [refreshing, loadMore]) const showNewEvents = () => { setEvents((oldEvents) => [...newEvents, ...oldEvents]) diff --git a/src/components/NotificationList/index.tsx b/src/components/NotificationList/index.tsx index 641f3a69..24ed9c19 100644 --- a/src/components/NotificationList/index.tsx +++ b/src/components/NotificationList/index.tsx @@ -9,7 +9,7 @@ import client from '@/services/client.service' import dayjs from 'dayjs' import { Heart, MessageCircle, Repeat, ThumbsUp } from 'lucide-react' import { Event, kinds, nip19, validateEvent } from 'nostr-tools' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import PullToRefresh from 'react-simple-pull-to-refresh' import { embedded, embeddedNostrNpubRenderer, embeddedNostrProfileRenderer } from '../Embedded' @@ -20,7 +20,7 @@ const LIMIT = 100 export default function NotificationList() { const { t } = useTranslation() - const { pubkey, relayList } = useNostr() + const { pubkey } = useNostr() const [refreshCount, setRefreshCount] = useState(0) const [timelineKey, setTimelineKey] = useState(undefined) const [refreshing, setRefreshing] = useState(true) @@ -29,13 +29,14 @@ export default function NotificationList() { const bottomRef = useRef(null) useEffect(() => { - if (!pubkey || !relayList) { + if (!pubkey) { setUntil(undefined) return } const init = async () => { setRefreshing(true) + const relayList = await client.fetchRelayList(pubkey) let eventCount = 0 const { closer, timelineKey } = await client.subscribeTimeline( relayList.read.length >= 4 @@ -70,7 +71,25 @@ export default function NotificationList() { return () => { promise.then((closer) => closer?.()) } - }, [pubkey, refreshCount, relayList]) + }, [pubkey, refreshCount]) + + const loadMore = useCallback(async () => { + if (!pubkey || !timelineKey || !until || refreshing) return + const notifications = await client.loadMoreTimeline(timelineKey, until, LIMIT) + if (notifications.length === 0) { + setUntil(undefined) + return + } + + if (notifications.length > 0) { + setNotifications((oldNotifications) => [ + ...oldNotifications, + ...notifications.filter((event) => event.pubkey !== pubkey) + ]) + } + + setUntil(notifications[notifications.length - 1].created_at - 1) + }, [pubkey, timelineKey, until, refreshing]) useEffect(() => { if (refreshing) return @@ -98,25 +117,7 @@ export default function NotificationList() { observerInstance.unobserve(currentBottomRef) } } - }, [until, refreshing, timelineKey]) - - const loadMore = async () => { - if (!pubkey || !timelineKey || !until || refreshing) return - const notifications = await client.loadMoreTimeline(timelineKey, until, LIMIT) - if (notifications.length === 0) { - setUntil(undefined) - return - } - - if (notifications.length > 0) { - setNotifications((oldNotifications) => [ - ...oldNotifications, - ...notifications.filter((event) => event.pubkey !== pubkey) - ]) - } - - setUntil(notifications[notifications.length - 1].created_at - 1) - } + }, [refreshing, loadMore]) return ( { + const loadMore = useCallback(async () => { if (loading || !until || !timelineKey) return setLoading(true) @@ -138,24 +138,27 @@ export default function ReplyNoteList({ event, className }: { event: NEvent; cla } setUntil(events.length ? events[events.length - 1].created_at - 1 : undefined) setLoading(false) - } + }, [loading, until, timelineKey]) - const onNewReply = (evt: NEvent) => { - setReplies((pre) => { - if (pre.some((reply) => reply.id === evt.id)) return pre - return [...pre, evt] - }) - if (evt.pubkey === pubkey) { - setTimeout(() => { - if (bottomRef.current) { - bottomRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) - } - highlightReply(evt.id, false) - }, 100) - } - } + const onNewReply = useCallback( + (evt: NEvent) => { + setReplies((pre) => { + if (pre.some((reply) => reply.id === evt.id)) return pre + return [...pre, evt] + }) + if (evt.pubkey === pubkey) { + setTimeout(() => { + if (bottomRef.current) { + bottomRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) + } + highlightReply(evt.id, false) + }, 100) + } + }, + [pubkey] + ) - const highlightReply = (eventId: string, scrollTo = true) => { + const highlightReply = useCallback((eventId: string, scrollTo = true) => { if (scrollTo) { const ref = replyRefs.current[eventId] if (ref) { @@ -166,7 +169,7 @@ export default function ReplyNoteList({ event, className }: { event: NEvent; cla setTimeout(() => { setHighlightReplyId((pre) => (pre === eventId ? undefined : pre)) }, 1500) - } + }, []) return ( <> diff --git a/src/providers/FeedProvider.tsx b/src/providers/FeedProvider.tsx index 8e1b1235..6a800689 100644 --- a/src/providers/FeedProvider.tsx +++ b/src/providers/FeedProvider.tsx @@ -3,7 +3,7 @@ import { isWebsocketUrl, normalizeUrl } from '@/lib/url' import storage from '@/services/storage.service' import { TFeedType } from '@/types' import { Filter } from 'nostr-tools' -import { createContext, useContext, useEffect, useState } from 'react' +import { createContext, useContext, useEffect, useRef, useState } from 'react' import { useFollowList } from './FollowListProvider' import { useNostr } from './NostrProvider' import { useRelaySets } from './RelaySetsProvider' @@ -32,10 +32,11 @@ export const useFeed = () => { } export function FeedProvider({ children }: { children: React.ReactNode }) { + const isFirstRenderRef = useRef(true) const { pubkey, getRelayList } = useNostr() const { getFollowings } = useFollowList() const { relaySets } = useRelaySets() - const [feedType, setFeedType] = useState(storage.getFeedType()) + const feedTypeRef = useRef(storage.getFeedType()) const [relayUrls, setRelayUrls] = useState([]) const [temporaryRelayUrls, setTemporaryRelayUrls] = useState([]) const [filter, setFilter] = useState({}) @@ -46,26 +47,31 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { useEffect(() => { const init = async () => { - // temporary relay urls from query params - const searchParams = new URLSearchParams(window.location.search) - const temporaryRelayUrls = searchParams - .getAll('r') - .map((url) => normalizeUrl(url)) - .filter((url) => isWebsocketUrl(url)) - if (temporaryRelayUrls.length) { - return await switchFeed('temporary', { temporaryRelayUrls }) + const isFirstRender = isFirstRenderRef.current + isFirstRenderRef.current = false + if (isFirstRender) { + // temporary relay urls from query params + const searchParams = new URLSearchParams(window.location.search) + const temporaryRelayUrls = searchParams + .getAll('r') + .map((url) => normalizeUrl(url)) + .filter((url) => isWebsocketUrl(url)) + if (temporaryRelayUrls.length) { + return await switchFeed('temporary', { temporaryRelayUrls }) + } + + if (feedTypeRef.current === 'relays') { + return await switchFeed('relays', { activeRelaySetId }) + } } - if (feedType === 'following') { - if (!pubkey) return + if (feedTypeRef.current === 'following' && pubkey) { return await switchFeed('following', { pubkey }) - } else { - await switchFeed('relays', { activeRelaySetId }) } } init() - }, [pubkey]) + }, [pubkey, feedTypeRef]) const switchFeed = async ( feedType: TFeedType, @@ -86,7 +92,7 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { relaySets.find((set) => set.id === options.activeRelaySetId) ?? (relaySets.length > 0 ? relaySets[0] : null) if (relaySet) { - setFeedType(feedType) + feedTypeRef.current = feedType setRelayUrls(relaySet.relayUrls) setActiveRelaySetId(relaySet.id) setFilter({}) @@ -99,7 +105,7 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { if (!options.pubkey) { return setIsReady(true) } - setFeedType(feedType) + feedTypeRef.current = feedType setActiveRelaySetId(null) const [relayList, followings] = await Promise.all([ getRelayList(options.pubkey), @@ -118,7 +124,7 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { return setIsReady(true) } - setFeedType(feedType) + feedTypeRef.current = feedType setTemporaryRelayUrls(urls) setRelayUrls(urls) setActiveRelaySetId(null) @@ -131,7 +137,7 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { return ( this.eventCache.get(id))