refactor: search
This commit is contained in:
parent
88567c2c13
commit
0153465e29
24 changed files with 785 additions and 345 deletions
|
|
@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const RelayPage = forwardRef(({ url }: { url?: string }, ref) => {
|
|||
displayScrollToTopButton
|
||||
ref={ref}
|
||||
>
|
||||
<Relay url={normalizedUrl} className="pt-3" />
|
||||
<Relay url={normalizedUrl} />
|
||||
</PrimaryPageLayout>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
42
src/pages/primary/SearchPage/index.tsx
Normal file
42
src/pages/primary/SearchPage/index.tsx
Normal 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
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
66
src/pages/secondary/SearchPage/index.tsx
Normal file
66
src/pages/secondary/SearchPage/index.tsx
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue