From a331e43b4e7efe25095a0e9d3e637ac2fa744675 Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Wed, 18 Oct 2023 12:50:03 +0300 Subject: [PATCH] search dropdown --- packages/app/src/Element/SearchBox.css | 9 +- packages/app/src/Element/SearchBox.tsx | 135 +++++++++++++++++--- packages/app/src/Pages/NostrLinkHandler.tsx | 3 +- packages/app/src/lang.json | 3 + packages/app/src/translations/en.json | 1 + 5 files changed, 132 insertions(+), 19 deletions(-) diff --git a/packages/app/src/Element/SearchBox.css b/packages/app/src/Element/SearchBox.css index 51192345..88e63a06 100644 --- a/packages/app/src/Element/SearchBox.css +++ b/packages/app/src/Element/SearchBox.css @@ -1,7 +1,6 @@ .search { flex-grow: 1; display: flex; - padding: 9px 16px; background: var(--gray-superdark); border-radius: 1000px; } @@ -16,7 +15,11 @@ border-radius: 0 !important; font-size: 15px; line-height: 21px; - padding: 0 !important; + padding: 9px 16px; +} + +.search > svg { + margin: 9px 16px; } @media (max-width: 768px) { @@ -28,4 +31,4 @@ .search input { display: none; } -} \ No newline at end of file +} diff --git a/packages/app/src/Element/SearchBox.tsx b/packages/app/src/Element/SearchBox.tsx index b1068ae5..9f1e03e3 100644 --- a/packages/app/src/Element/SearchBox.tsx +++ b/packages/app/src/Element/SearchBox.tsx @@ -1,19 +1,70 @@ import "./SearchBox.css"; import Spinner from "../Icons/Spinner"; import Icon from "../Icons/Icon"; -import { useIntl } from "react-intl"; +import { FormattedMessage, useIntl } from "react-intl"; import { fetchNip05Pubkey } from "../Nip05/Verifier"; -import { useState } from "react"; -import { NostrLink, NostrPrefix, tryParseNostrLink } from "@snort/system"; -import { useNavigate } from "react-router-dom"; +import { ChangeEvent, useEffect, useRef, useState } from "react"; +import { NostrLink, tryParseNostrLink } from "@snort/system"; +import { useLocation, useNavigate } from "react-router-dom"; +import { unixNow } from "@snort/shared"; +import useTimelineFeed, { TimelineFeedOptions, TimelineSubject } from "../Feed/TimelineFeed"; +import Note from "./Event/Note"; + +const MAX_RESULTS = 3; export default function SearchBox() { const { formatMessage } = useIntl(); const [search, setSearch] = useState(""); const [searching, setSearching] = useState(false); const navigate = useNavigate(); + const location = useLocation(); - async function searchThing() { + const [activeIndex, setActiveIndex] = useState(-1); + const resultListRef = useRef(null); + + const options: TimelineFeedOptions = { + method: "LIMIT_UNTIL", + window: undefined, + now: unixNow(), + }; + + const subject: TimelineSubject = { + type: "profile_keyword", + discriminator: search, + items: [search], + relay: undefined, + streams: false, + }; + + const { main } = useTimelineFeed(subject, options); + + useEffect(() => { + const handleGlobalKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + setSearch(""); + } + }; + const inputElement = resultListRef.current?.previousSibling as HTMLElement; + + inputElement?.addEventListener("focus", () => { + document.addEventListener("keydown", handleGlobalKeyDown); + }); + inputElement?.addEventListener("blur", () => { + document.removeEventListener("keydown", handleGlobalKeyDown); + }); + + return () => { + document.removeEventListener("keydown", handleGlobalKeyDown); + }; + }, []); + + useEffect(() => { + // Close the search on navigation + setSearch(""); + setActiveIndex(-1); + }, [location]); + + const executeSearch = async () => { try { setSearching(true); const link = tryParseNostrLink(search); @@ -25,36 +76,90 @@ export default function SearchBox() { const [handle, domain] = search.split("@"); const pk = await fetchNip05Pubkey(handle, domain); if (pk) { - navigate(`/${new NostrLink(NostrPrefix.PublicKey, pk).encode()}`); + navigate(`/${new NostrLink(CONFIG.profileLinkPrefix, pk).encode()}`); return; } } navigate(`/search/${encodeURIComponent(search)}`); } finally { - setSearch(""); setSearching(false); } - } + }; + + const handleChange = (e: ChangeEvent) => { + if (!e.target.value.match(/nsec1[a-zA-Z0-9]{20,65}/gi)) { + setSearch(e.target.value); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + switch (e.key) { + case "Enter": + if (activeIndex === 0) { + navigate(`/search/${encodeURIComponent(search)}`); + } else if (activeIndex > 0 && main) { + const selectedResult = main[activeIndex - 1]; + navigate(`/${new NostrLink(CONFIG.profileLinkPrefix, selectedResult.pubkey).encode()}`); + } else { + executeSearch(); + } + break; + case "ArrowDown": + e.preventDefault(); + setActiveIndex(prev => Math.min(prev + 1, Math.min(MAX_RESULTS, main ? main.length : 0))); + break; + case "ArrowUp": + e.preventDefault(); + setActiveIndex(prev => Math.max(prev - 1, 0)); + break; + default: + break; + } + }; return ( -
+
setSearch(e.target.value)} - onKeyDown={async e => { - if (e.key === "Enter") { - await searchThing(); - } - }} + onChange={handleChange} + onKeyDown={handleKeyDown} /> {searching ? ( ) : ( navigate("/search")} /> )} + {search && !searching && ( +
+
setActiveIndex(0)} + onClick={() => navigate(`/search/${encodeURIComponent(search)}`, { state: { forceRefresh: true } })}> + : {search} +
+ {main?.slice(0, MAX_RESULTS).map((result, idx) => ( +
setActiveIndex(idx + 1)}> + +
+ ))} +
+ )}
); } diff --git a/packages/app/src/Pages/NostrLinkHandler.tsx b/packages/app/src/Pages/NostrLinkHandler.tsx index c8a74c2e..fd1596eb 100644 --- a/packages/app/src/Pages/NostrLinkHandler.tsx +++ b/packages/app/src/Pages/NostrLinkHandler.tsx @@ -22,7 +22,8 @@ export default function NostrLinkHandler() { if (nav.type === NostrPrefix.Event || nav.type === NostrPrefix.Note || nav.type === NostrPrefix.Address) { setRenderComponent(); // Directly render ThreadRoute } else if (nav.type === NostrPrefix.PublicKey || nav.type === NostrPrefix.Profile) { - setRenderComponent(); // Directly render ProfilePage + const id = nav.encode(); + setRenderComponent(); // Directly render ProfilePage } } else { if (state) { diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json index 4bdc210d..d81759d3 100644 --- a/packages/app/src/lang.json +++ b/packages/app/src/lang.json @@ -401,6 +401,9 @@ "E8a4yq": { "defaultMessage": "Follow some popular accounts" }, + "EJbFi7": { + "defaultMessage": "Search notes" + }, "ELbg9p": { "defaultMessage": "Data Providers" }, diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index f25a316b..1b65d164 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -131,6 +131,7 @@ "Dn82AL": "Live", "DtYelJ": "Transfer", "E8a4yq": "Follow some popular accounts", + "EJbFi7": "Search notes", "ELbg9p": "Data Providers", "EPYwm7": "Your private key is your password. If you lose this key, you will lose access to your account! Copy it and keep it in a safe place. There is no way to reset your private key.", "EQKRE4": "Show badges on profile pages",