import { Client, CredentialManager } from "@atcute/client"; import { Nsid } from "@atcute/lexicons"; import { A, useLocation, useNavigate } from "@solidjs/router"; import { createResource, createSignal, For, onCleanup, onMount, Show } from "solid-js"; import { isTouchDevice } from "../layout"; import { resolveLexiconAuthority } from "../utils/api"; import { appHandleLink, appList, appName, AppUrl } from "../utils/app-urls"; import { createDebouncedValue } from "../utils/hooks/debounced"; import { Modal } from "./modal"; export const [showSearch, setShowSearch] = createSignal(false); const SearchButton = () => { onMount(() => window.addEventListener("keydown", keyEvent)); onCleanup(() => window.removeEventListener("keydown", keyEvent)); const keyEvent = (ev: KeyboardEvent) => { if (document.querySelector("[data-modal]")) return; if ((ev.ctrlKey || ev.metaKey) && ev.key == "k") { ev.preventDefault(); setShowSearch(!showSearch()); } else if (ev.key == "Escape") { ev.preventDefault(); setShowSearch(false); } }; return ( ); }; const Search = () => { const navigate = useNavigate(); let searchInput!: HTMLInputElement; const rpc = new Client({ handler: new CredentialManager({ service: "https://public.api.bsky.app" }), }); onMount(() => { if (!isTouchDevice || useLocation().pathname !== "/") searchInput.focus(); }); const fetchTypeahead = async (input: string) => { if (!input.length) return []; const res = await rpc.get("app.bsky.actor.searchActorsTypeahead", { params: { q: input, limit: 5 }, }); if (res.ok) { return res.data.actors; } return []; }; const [input, setInput] = createSignal(); const [selectedIndex, setSelectedIndex] = createSignal(-1); const [search] = createResource(createDebouncedValue(input, 250), fetchTypeahead); const processInput = async (input: string) => { input = input.trim().replace(/^@/, ""); if (!input.length) return; const index = selectedIndex() >= 0 ? selectedIndex() : 0; setShowSearch(false); setInput(undefined); if (search()?.length && selectedIndex() !== -1) { navigate(`/at://${search()![index].did}`); } else if (input.startsWith("https://") || input.startsWith("http://")) { const hostLength = input.indexOf("/", 8); const host = input.slice(0, hostLength).replace("https://", "").replace("http://", ""); if (!(host in appList)) { navigate(`/${input.replace("https://", "").replace("http://", "").replace("/", "")}`); } else { const app = appList[host as AppUrl]; const path = input.slice(hostLength + 1).split("/"); const uri = appHandleLink[app](path); navigate(`/${uri}`); } } else if (input.startsWith("lex:")) { const nsid = input.replace("lex:", "") as Nsid; const res = await resolveLexiconAuthority(nsid); navigate(`/at://${res}/com.atproto.lexicon.schema/${nsid}`); } else { navigate(`/at://${input.replace("at://", "")}`); } setSelectedIndex(-1); }; return (
{ e.preventDefault(); processInput(searchInput.value); }} >
{ setInput(e.currentTarget.value); setSelectedIndex(-1); }} onKeyDown={(e) => { const results = search(); if (!results?.length) return; if (e.key === "ArrowDown") { e.preventDefault(); setSelectedIndex((prev) => (prev === -1 ? 0 : (prev + 1) % results.length)); } else if (e.key === "ArrowUp") { e.preventDefault(); setSelectedIndex((prev) => prev === -1 ? results.length - 1 : (prev - 1 + results.length) % results.length, ); } }} />
); }; const ListUrlsTooltip = () => { const [openList, setOpenList] = createSignal(false); let urls: Record = {}; for (const [appUrl, appView] of Object.entries(appList)) { if (!urls[appView]) urls[appView] = [appUrl as AppUrl]; else urls[appView].push(appUrl as AppUrl); } return ( <> setOpenList(false)}>
Supported URLs
Links that will be parsed automatically, as long as all the data necessary is on the URL.
{([appView, name]) => { return (

{name}

{(url) => ( {url} )}
); }}
); }; export { Search, SearchButton };