refactor: search

This commit is contained in:
codytseng 2025-08-31 22:43:47 +08:00
parent 88567c2c13
commit 0153465e29
24 changed files with 785 additions and 345 deletions

View file

@ -1,17 +0,0 @@
import { SearchDialog } from '@/components/SearchDialog'
import { Button } from '@/components/ui/button'
import { Search } from 'lucide-react'
import { useState } from 'react'
export default function SearchButton() {
const [open, setOpen] = useState(false)
return (
<>
<Button variant="ghost" size="titlebar-icon" onClick={() => setOpen(true)}>
<Search />
</Button>
<SearchDialog open={open} setOpen={setOpen} />
</>
)
}

View file

@ -1,18 +1,19 @@
import { useSecondaryPage } from '@/PageManager'
import BookmarkList from '@/components/BookmarkList'
import PostEditor from '@/components/PostEditor'
import { Button } from '@/components/ui/button'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { toSearch } from '@/lib/link'
import { useFeed } from '@/providers/FeedProvider'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { TPageRef } from '@/types'
import { PencilLine } from 'lucide-react'
import { PencilLine, Search } from 'lucide-react'
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import FeedButton from './FeedButton'
import FollowingFeed from './FollowingFeed'
import RelaysFeed from './RelaysFeed'
import SearchButton from './SearchButton'
const NoteListPage = forwardRef((_, ref) => {
const { t } = useTranslation()
@ -76,10 +77,12 @@ function NoteListPageTitlebar() {
return (
<div className="flex gap-1 items-center h-full justify-between">
<FeedButton className="flex-1 max-w-fit w-0" />
<div className="shrink-0 flex gap-1 items-center">
<SearchButton />
{isSmallScreen && <PostButton />}
</div>
{isSmallScreen && (
<div className="shrink-0 flex gap-1 items-center">
<SearchButton />
<PostButton />
</div>
)}
</div>
)
}
@ -106,3 +109,13 @@ function PostButton() {
</>
)
}
function SearchButton() {
const { push } = useSecondaryPage()
return (
<Button variant="ghost" size="titlebar-icon" onClick={() => push(toSearch())}>
<Search />
</Button>
)
}

View file

@ -23,7 +23,7 @@ const RelayPage = forwardRef(({ url }: { url?: string }, ref) => {
displayScrollToTopButton
ref={ref}
>
<Relay url={normalizedUrl} className="pt-3" />
<Relay url={normalizedUrl} />
</PrimaryPageLayout>
)
})

View file

@ -0,0 +1,42 @@
import SearchBar, { TSearchBarRef } from '@/components/SearchBar'
import SearchResult from '@/components/SearchResult'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { usePrimaryPage } from '@/PageManager'
import { TSearchParams } from '@/types'
import { forwardRef, useEffect, useMemo, useRef, useState } from 'react'
const SearchPage = forwardRef((_, ref) => {
const { current, display } = usePrimaryPage()
const [input, setInput] = useState('')
const [searchParams, setSearchParams] = useState<TSearchParams | null>(null)
const searchBarRef = useRef<TSearchBarRef>(null)
const isActive = useMemo(() => current === 'search' && display, [current, display])
useEffect(() => {
if (isActive) {
searchBarRef.current?.focus()
}
}, [isActive])
const onSearch = (params: TSearchParams | null) => {
setSearchParams(params)
if (params?.input) {
setInput(params.input)
}
}
return (
<PrimaryPageLayout
ref={ref}
pageName="search"
titlebar={
<SearchBar ref={searchBarRef} onSearch={onSearch} input={input} setInput={setInput} />
}
displayScrollToTopButton
>
<SearchResult searchParams={searchParams} />
</PrimaryPageLayout>
)
})
SearchPage.displayName = 'SearchPage'
export default SearchPage

View file

@ -1,18 +1,11 @@
import { Favicon } from '@/components/Favicon'
import ProfileList from '@/components/ProfileList'
import UserItem from '@/components/UserItem'
import { SEARCHABLE_RELAY_URLS } from '@/constants'
import { useFetchRelayInfos } from '@/hooks'
import { ProfileListBySearch } from '@/components/ProfileListBySearch'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { fetchPubkeysFromDomain } from '@/lib/nip05'
import { useFeed } from '@/providers/FeedProvider'
import client from '@/services/client.service'
import dayjs from 'dayjs'
import { forwardRef, useEffect, useMemo, useRef, useState } from 'react'
import { forwardRef, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
const LIMIT = 50
const ProfileListPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t } = useTranslation()
const [title, setTitle] = useState<React.ReactNode>()
@ -72,69 +65,3 @@ function ProfileListByDomain({ domain }: { domain: string }) {
return <ProfileList pubkeys={pubkeys} />
}
function ProfileListBySearch({ search }: { search: string }) {
const { relayUrls } = useFeed()
const { searchableRelayUrls } = useFetchRelayInfos(relayUrls)
const [until, setUntil] = useState<number>(() => dayjs().unix())
const [hasMore, setHasMore] = useState<boolean>(true)
const [pubkeySet, setPubkeySet] = useState(new Set<string>())
const bottomRef = useRef<HTMLDivElement>(null)
const filter = { until, search }
const urls = useMemo(() => {
return filter.search ? searchableRelayUrls.concat(SEARCHABLE_RELAY_URLS).slice(0, 4) : relayUrls
}, [relayUrls, searchableRelayUrls, filter])
useEffect(() => {
if (!hasMore) return
const options = {
root: null,
rootMargin: '10px',
threshold: 1
}
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)
}
}
}, [hasMore, filter, urls])
async function loadMore() {
if (urls.length === 0) {
return setHasMore(false)
}
const profiles = await client.searchProfiles(urls, { ...filter, limit: LIMIT })
const newPubkeySet = new Set<string>()
profiles.forEach((profile) => {
if (!pubkeySet.has(profile.pubkey)) {
newPubkeySet.add(profile.pubkey)
}
})
setPubkeySet((prev) => new Set([...prev, ...newPubkeySet]))
setHasMore(profiles.length >= LIMIT)
const lastProfileCreatedAt = profiles[profiles.length - 1].created_at
setUntil(lastProfileCreatedAt ? lastProfileCreatedAt - 1 : 0)
}
return (
<div className="px-4">
{Array.from(pubkeySet).map((pubkey, index) => (
<UserItem key={`${index}-${pubkey}`} pubkey={pubkey} />
))}
{hasMore && <div ref={bottomRef} />}
</div>
)
}

View file

@ -21,7 +21,7 @@ const RelayPage = forwardRef(({ url, index }: { url?: string; index?: number },
controls={<RelayPageControls url={normalizedUrl} />}
displayScrollToTopButton
>
<Relay url={normalizedUrl} className="pt-3" />
<Relay url={normalizedUrl} />
</SecondaryPageLayout>
)
})

View file

@ -0,0 +1,66 @@
import SearchBar, { TSearchBarRef } from '@/components/SearchBar'
import SearchResult from '@/components/SearchResult'
import { Button } from '@/components/ui/button'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { toSearch } from '@/lib/link'
import { useSecondaryPage } from '@/PageManager'
import { TSearchParams } from '@/types'
import { ChevronLeft } from 'lucide-react'
import { forwardRef, useEffect, useMemo, useRef, useState } from 'react'
const SearchPage = forwardRef(({ index }: { index?: number }, ref) => {
const { push, pop } = useSecondaryPage()
const [input, setInput] = useState('')
const searchBarRef = useRef<TSearchBarRef>(null)
const searchParams = useMemo(() => {
const params = new URLSearchParams(window.location.search)
const type = params.get('t')
if (
type !== 'profile' &&
type !== 'profiles' &&
type !== 'notes' &&
type !== 'hashtag' &&
type !== 'relay'
) {
return null
}
const search = params.get('q')
if (!search) {
return null
}
const input = params.get('i') ?? ''
setInput(input || search)
return { type, search, input } as TSearchParams
}, [])
useEffect(() => {
if (!window.location.search) {
searchBarRef.current?.focus()
}
}, [])
const onSearch = (params: TSearchParams | null) => {
if (params) {
push(toSearch(params))
}
}
return (
<SecondaryPageLayout
ref={ref}
index={index}
titlebar={
<div className="flex items-center gap-1 h-full">
<Button variant="ghost" size="titlebar-icon" onClick={() => pop()}>
<ChevronLeft />
</Button>
<SearchBar ref={searchBarRef} input={input} setInput={setInput} onSearch={onSearch} />
</div>
}
>
<SearchResult searchParams={searchParams} />
</SecondaryPageLayout>
)
})
SearchPage.displayName = 'SearchPage'
export default SearchPage