import './search.css'; import { forwardRef } from 'preact/compat'; import { useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, } from 'preact/hooks'; import { InView } from 'react-intersection-observer'; import { useParams, useSearchParams } from 'react-router-dom'; import AccountBlock from '../components/account-block'; import Icon from '../components/icon'; import Link from '../components/link'; import Loader from '../components/loader'; import NavMenu from '../components/nav-menu'; import Status from '../components/status'; import { api } from '../utils/api'; import useTitle from '../utils/useTitle'; const SHORT_LIMIT = 5; const LIMIT = 40; function Search(props) { const params = useParams(); const { masto, instance, authenticated } = api({ instance: params.instance, }); const [uiState, setUiState] = useState('default'); const [searchParams] = useSearchParams(); const searchFormRef = useRef(); const q = props?.query || searchParams.get('q'); const type = props?.type || searchParams.get('type'); useTitle( q ? `Search: ${q}${ type ? ` (${ { statuses: 'Posts', accounts: 'Accounts', hashtags: 'Hashtags', }[type] })` : '' }` : 'Search', `/search`, ); const [showMore, setShowMore] = useState(false); const offsetRef = useRef(0); useEffect(() => { offsetRef.current = 0; }, [type]); const scrollableRef = useRef(); useLayoutEffect(() => { scrollableRef.current?.scrollTo?.(0, 0); }, [q, type]); const [statusResults, setStatusResults] = useState([]); const [accountResults, setAccountResults] = useState([]); const [hashtagResults, setHashtagResults] = useState([]); function loadResults(firstLoad) { setUiState('loading'); if (firstLoad && !type) { setStatusResults(statusResults.slice(0, SHORT_LIMIT)); setAccountResults(accountResults.slice(0, SHORT_LIMIT)); setHashtagResults(hashtagResults.slice(0, SHORT_LIMIT)); } (async () => { const params = { q, resolve: authenticated, limit: SHORT_LIMIT, }; if (type) { params.limit = LIMIT; params.type = type; params.offset = offsetRef.current; } try { const results = await masto.v2.search(params); console.log(results); if (type) { if (type === 'statuses') { setStatusResults((prev) => [...prev, ...results.statuses]); } else if (type === 'accounts') { setAccountResults((prev) => [...prev, ...results.accounts]); } else if (type === 'hashtags') { setHashtagResults((prev) => [...prev, ...results.hashtags]); } offsetRef.current = offsetRef.current + LIMIT; setShowMore(!!results[type]?.length); } else { setStatusResults(results.statuses); setAccountResults(results.accounts); setHashtagResults(results.hashtags); } setUiState('default'); } catch (err) { console.error(err); setUiState('error'); } })(); } useEffect(() => { // searchFieldRef.current?.focus?.(); // searchFormRef.current?.focus?.(); if (q) { // searchFieldRef.current.value = q; searchFormRef.current?.setValue?.(q); loadResults(true); } }, [q, type, instance]); return (
 
{!!q && (
{!!type && ‹ All} {[ { label: 'Accounts', type: 'accounts', to: `/search?q=${q}&type=accounts`, }, { label: 'Hashtags', type: 'hashtags', to: `/search?q=${q}&type=hashtags`, }, { label: 'Posts', type: 'statuses', to: `/search?q=${q}&type=statuses`, }, ] .sort((a, b) => { if (a.type === type) return -1; if (b.type === type) return 1; return 0; }) .map((link) => ( {link.label} ))}
)} {!!q ? ( <> {(!type || type === 'accounts') && ( <> {type !== 'accounts' && (

Accounts

)} {accountResults.length > 0 ? ( <>
    {accountResults.map((account) => (
  • ))}
{type !== 'accounts' && (
See more accounts
)} ) : uiState === 'loading' ? (

) : (

No accounts found.

)} )} {(!type || type === 'hashtags') && ( <> {type !== 'hashtags' && (

Hashtags

)} {hashtagResults.length > 0 ? ( <> {type !== 'hashtags' && (
See more hashtags
)} ) : uiState === 'loading' ? (

) : (

No hashtags found.

)} )} {(!type || type === 'statuses') && ( <> {type !== 'statuses' && (

Posts

)} {statusResults.length > 0 ? ( <>
    {statusResults.map((status) => (
  • ))}
{type !== 'statuses' && (
See more posts
)} ) : uiState === 'loading' ? (

) : (

No posts found.

)} )} {!!type && (uiState === 'default' ? ( showMore ? ( { if (inView) { loadResults(); } }} > ) : (

The end.

) ) : ( !!( hashtagResults.length || accountResults.length || statusResults.length ) && (

) ))} ) : uiState === 'loading' ? (

) : (

Enter your search term or paste a URL above to get started.

)}
); } export default Search; const SearchForm = forwardRef((props, ref) => { const { instance } = api(); const [searchParams, setSearchParams] = useSearchParams(); const [searchMenuOpen, setSearchMenuOpen] = useState(false); const [query, setQuery] = useState(searchParams.q || ''); const formRef = useRef(null); const searchFieldRef = useRef(null); useImperativeHandle(ref, () => ({ setValue: (value) => { setQuery(value); }, focus: () => { searchFieldRef.current.focus(); }, })); return (
{ e.preventDefault(); if (query) { setSearchParams({ q: query, }); } else { setSearchParams({}); } }} > { if (!e.target.value) { setSearchParams({}); } }} onInput={(e) => { setQuery(e.target.value); setSearchMenuOpen(true); }} onFocus={() => { setSearchMenuOpen(true); }} onBlur={() => { setTimeout(() => { setSearchMenuOpen(false); }, 100); formRef.current ?.querySelector('.search-popover-item.focus') ?.classList.remove('focus'); }} onKeyDown={(e) => { const { key } = e; switch (key) { case 'Escape': setSearchMenuOpen(false); break; case 'Down': case 'ArrowDown': e.preventDefault(); if (searchMenuOpen) { const focusItem = formRef.current.querySelector( '.search-popover-item.focus', ); if (focusItem) { let nextItem = focusItem.nextElementSibling; while (nextItem && nextItem.hidden) { nextItem = nextItem.nextElementSibling; } if (nextItem) { nextItem.classList.add('focus'); const siblings = Array.from( nextItem.parentElement.children, ).filter((el) => el !== nextItem); siblings.forEach((el) => { el.classList.remove('focus'); }); } } else { const firstItem = formRef.current.querySelector( '.search-popover-item', ); if (firstItem) { firstItem.classList.add('focus'); } } } break; case 'Up': case 'ArrowUp': e.preventDefault(); if (searchMenuOpen) { const focusItem = document.querySelector( '.search-popover-item.focus', ); if (focusItem) { let prevItem = focusItem.previousElementSibling; while (prevItem && prevItem.hidden) { prevItem = prevItem.previousElementSibling; } if (prevItem) { prevItem.classList.add('focus'); const siblings = Array.from( prevItem.parentElement.children, ).filter((el) => el !== prevItem); siblings.forEach((el) => { el.classList.remove('focus'); }); } } else { const lastItem = document.querySelector( '.search-popover-item:last-child', ); if (lastItem) { lastItem.classList.add('focus'); } } } break; case 'Enter': if (searchMenuOpen) { const focusItem = document.querySelector( '.search-popover-item.focus', ); if (focusItem) { e.preventDefault(); focusItem.click(); } setSearchMenuOpen(false); } break; } }} />
); });