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 ( setShowSearch(!showSearch())} class={`flex items-center gap-0.5 rounded-lg ${isTouchDevice ? "p-1 text-xl hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" : "dark:bg-dark-100/70 box-border h-7 border-[0.5px] border-neutral-300 bg-neutral-100/70 p-1.5 text-xs hover:bg-neutral-200 active:bg-neutral-300 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"}`} > {/Mac/i.test(navigator.platform) ? "⌘" : "⌃"}K ); }; 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); }} > PDS URL, AT URI, NSID, DID, or handle { 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, ); } }} /> setInput(undefined)} > {(actor, index) => ( setShowSearch(false)} > {actor.handle} )} ); }; 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} )} ); }} setOpenList(true)} > > ); }; export { Search, SearchButton };
{name}