diff --git a/src/routes/Search.tsx b/src/routes/Search.tsx index 40781c15..8b84c612 100644 --- a/src/routes/Search.tsx +++ b/src/routes/Search.tsx @@ -7,9 +7,11 @@ import { createResource, createSignal, For, + Match, onMount, Show, - Suspense + Suspense, + Switch } from "solid-js"; import close from "~/assets/icons/close.svg"; @@ -19,6 +21,7 @@ import { ContactEditor, ContactFormValues, LabelCircle, + LoadingShimmer, NavBar, showToast } from "~/components"; @@ -31,6 +34,13 @@ import { } from "~/components/layout"; import { useI18n } from "~/i18n/context"; import { useMegaStore } from "~/state/megaStore"; +import { + actuallyFetchNostrProfile, + hexpubFromNpub, + profileToPseudoContact, + PseudoContact, + searchProfiles +} from "~/utils"; export function Search() { return ( @@ -65,7 +75,6 @@ function ActualSearch() { async function contactsFetcher() { try { - console.log("getting contacts"); const contacts: TagItem[] = state.mutiny_wallet?.get_contacts_sorted(); return contacts || []; @@ -78,11 +87,10 @@ function ActualSearch() { const [contacts] = createResource(contactsFetcher); const filteredContacts = createMemo(() => { + const s = searchValue().toLowerCase(); return ( contacts()?.filter((c) => { - const s = searchValue().toLowerCase(); return ( - // c.ln_address && (c.name.toLowerCase().includes(s) || c.ln_address?.toLowerCase().includes(s) || @@ -92,6 +100,14 @@ function ActualSearch() { ); }); + const foundNpubs = createMemo(() => { + return ( + filteredContacts() + ?.map((c) => c.npub) + .filter((n) => !!n) || [] + ); + }); + const showSendButton = createMemo(() => { if (searchValue() === "") { return false; @@ -104,12 +120,10 @@ function ActualSearch() { let success = false; actions.handleIncomingString( text, - (error) => { - // showToast(error); - console.log("error", error); + (_error) => { + // noop }, - (result) => { - console.log("result", result); + (_result) => { success = true; } ); @@ -258,7 +272,7 @@ function ActualSearch() { Continue -
+

Contacts

@@ -290,8 +304,162 @@ function ActualSearch() { + + +

Global Search

+ }> + + +
); } + +function GlobalSearch(props: { + searchValue: string; + sendToContact: (contact: TagItem) => void; + foundNpubs: (string | undefined)[]; +}) { + const hexpubs = createMemo(() => { + const hexpubs: string[] = []; + for (const npub of props.foundNpubs) { + hexpubFromNpub(npub) + .then((h) => { + if (h) { + hexpubs.push(h); + } + }) + .catch((e) => { + console.error(e); + }); + } + return hexpubs; + }); + + async function searchFetcher(args: { value?: string; hexpubs?: string[] }) { + try { + // Handling case when value starts with "npub" + if (args.value?.startsWith("npub")) { + const hexpub = await hexpubFromNpub(args.value); + if (!hexpub) return []; + + const profile = await actuallyFetchNostrProfile(hexpub); + if (!profile) return []; + + const contact = profileToPseudoContact(profile); + return contact.ln_address ? [contact] : []; + } + + // Handling case for other values (name, nip-05, whatever else primal searches) + const contacts = await searchProfiles(args.value!.toLowerCase()); + return contacts.filter( + (c) => c.ln_address && !args.hexpubs?.includes(c.hexpub) + ); + } catch (e) { + console.error(e); + return []; + } + } + + const searchArgs = createMemo(() => { + if (props.searchValue) { + return { + value: props.searchValue, + hexpubs: hexpubs() + }; + } else { + return { + value: "", + hexpubs: undefined + }; + } + }); + + const [searchResults] = createResource(searchArgs, searchFetcher); + + return ( + + +

+ No results found for "{props.searchValue}" +

+
+ + + {(contact) => ( + + )} + + +
+ ); +} + +function SingleContact(props: { + contact: PseudoContact; + sendToContact: (contact: TagItem) => void; +}) { + const [state, _actions] = useMegaStore(); + async function createContactFromSearchResult(contact: PseudoContact) { + try { + const contactId = await state.mutiny_wallet?.create_new_contact( + contact.name, + contact.hexpub ? contact.hexpub : undefined, + contact.ln_address ? contact.ln_address : undefined, + undefined, + contact.image_url ? contact.image_url : undefined + ); + + if (!contactId) { + throw new Error("no contact id returned"); + } + + const tagItem = await state.mutiny_wallet?.get_tag_item(contactId); + + if (!tagItem) { + throw new Error("no contact returned"); + } + + props.sendToContact(tagItem); + } catch (e) { + console.error(e); + } + } + + return ( + + ); +} diff --git a/src/utils/fetchZaps.ts b/src/utils/fetchZaps.ts index 5da453c0..d5df0b25 100644 --- a/src/utils/fetchZaps.ts +++ b/src/utils/fetchZaps.ts @@ -28,7 +28,7 @@ type SimpleZapItem = { content?: string; }; -type NostrProfile = { +export type NostrProfile = { id: string; pubkey: string; created_at: number; @@ -288,6 +288,10 @@ export const fetchNostrProfile: ResourceFetcher< string, NostrProfile | undefined > = async (hexpub, _info) => { + return await actuallyFetchNostrProfile(hexpub); +}; + +export async function actuallyFetchNostrProfile(hexpub: string) { try { if (!PRIMAL_API) throw new Error("Missing PRIMAL_API environment variable"); @@ -315,4 +319,59 @@ export const fetchNostrProfile: ResourceFetcher< console.error("Failed to load profile: ", e); throw new Error("Failed to load profile"); } +} + +// Search results from primal have some of the stuff we want for a TagItem contact +export type PseudoContact = { + name: string; + hexpub: string; + ln_address?: string; + image_url?: string; }; + +export async function searchProfiles(query: string): Promise { + console.log("searching profiles..."); + const response = await fetch(PRIMAL_API, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify([ + "user_search", + { query: query.trim(), limit: 10 } + ]) + }); + + if (!response.ok) { + throw new Error(`Failed to search`); + } + + const data = await response.json(); + + const users: PseudoContact[] = []; + + for (const object of data) { + if (object.kind === 0) { + try { + const profile = object as NostrProfile; + const contact = profileToPseudoContact(profile); + users.push(contact); + } catch (e) { + console.error("Failed to parse content: ", object.content); + } + } + } + + return users; +} + +export function profileToPseudoContact(profile: NostrProfile): PseudoContact { + const content = JSON.parse(profile.content); + const contact: Partial = { + hexpub: profile.pubkey + }; + contact.name = content.display_name || content.name || profile.pubkey; + contact.ln_address = content.lud16 || undefined; + contact.image_url = content.picture || undefined; + return contact as PseudoContact; +}