From c7d9a0476750783326a1bf7280164aecb8bea622 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Tue, 1 Aug 2023 20:52:36 -0500 Subject: [PATCH] rebuild search view to use NIP-50 --- .changeset/eighty-phones-switch.md | 5 + src/components/note/index.tsx | 4 +- src/components/note/note-relays.tsx | 2 +- .../generic-note-timeline/stream-note.tsx | 4 +- src/types/nostr-query.ts | 1 + src/views/search/index.tsx | 266 ++++++++---------- src/views/streams/components/stream-card.tsx | 4 +- src/views/streams/index.tsx | 12 +- src/views/user/about.tsx | 18 +- src/views/user/components/header.tsx | 4 +- src/views/user/components/user-card.tsx | 6 +- 11 files changed, 150 insertions(+), 176 deletions(-) create mode 100644 .changeset/eighty-phones-switch.md diff --git a/.changeset/eighty-phones-switch.md b/.changeset/eighty-phones-switch.md new file mode 100644 index 000000000..ddb44530e --- /dev/null +++ b/.changeset/eighty-phones-switch.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Rebuild search view to use NIP-50 diff --git a/src/components/note/index.tsx b/src/components/note/index.tsx index 6e77d0eba..a87bae9ae 100644 --- a/src/components/note/index.tsx +++ b/src/components/note/index.tsx @@ -17,7 +17,7 @@ import { NostrEvent } from "../../types/nostr-event"; import { UserAvatarLink } from "../user-avatar-link"; import { NoteMenu } from "./note-menu"; -import { NoteRelays } from "./note-relays"; +import { EventRelays } from "./note-relays"; import { useIsMobile } from "../../hooks/use-is-mobile"; import { UserLink } from "../user-link"; import { UserDnsIdentityIcon } from "../user-dns-identity-icon"; @@ -90,7 +90,7 @@ export const Note = React.memo(({ event, variant = "outline" }: NoteProps) => { target="_blank" /> )} - + diff --git a/src/components/note/note-relays.tsx b/src/components/note/note-relays.tsx index 1e7f0629f..77a42b857 100644 --- a/src/components/note/note-relays.tsx +++ b/src/components/note/note-relays.tsx @@ -10,7 +10,7 @@ export type NoteRelaysProps = { event: NostrEvent; }; -export const NoteRelays = memo(({ event }: NoteRelaysProps) => { +export const EventRelays = memo(({ event }: NoteRelaysProps) => { const isMobile = useIsMobile(); const eventRelays = useSubject(getEventRelays(getEventUID(event))); diff --git a/src/components/timeline-page/generic-note-timeline/stream-note.tsx b/src/components/timeline-page/generic-note-timeline/stream-note.tsx index 3323e3f42..95cc11df4 100644 --- a/src/components/timeline-page/generic-note-timeline/stream-note.tsx +++ b/src/components/timeline-page/generic-note-timeline/stream-note.tsx @@ -23,7 +23,7 @@ import { useRegisterIntersectionEntity } from "../../../providers/intersection-o import { UserAvatar } from "../../user-avatar"; import { UserLink } from "../../user-link"; import StreamStatusBadge from "../../../views/streams/components/status-badge"; -import { NoteRelays } from "../../note/note-relays"; +import { EventRelays } from "../../note/note-relays"; import { useAsync } from "react-use"; export default function StreamNote({ event, ...props }: CardProps & { event: NostrEvent }) { @@ -69,7 +69,7 @@ export default function StreamNote({ event, ...props }: CardProps & { event: Nos - + ); diff --git a/src/types/nostr-query.ts b/src/types/nostr-query.ts index 51fabedba..017590b4e 100644 --- a/src/types/nostr-query.ts +++ b/src/types/nostr-query.ts @@ -18,6 +18,7 @@ export type NostrQuery = { since?: number; until?: number; limit?: number; + search?: string; }; export type NostrRequestFilter = NostrQuery | NostrQuery[]; diff --git a/src/views/search/index.tsx b/src/views/search/index.tsx index 2d08e3802..cd3da4738 100644 --- a/src/views/search/index.tsx +++ b/src/views/search/index.tsx @@ -1,100 +1,129 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; import { + Box, Button, Card, CardBody, CardFooter, CardHeader, Flex, - Heading, IconButton, Input, - Text, + Link, + SimpleGrid, useDisclosure, } from "@chakra-ui/react"; -import dayjs from "dayjs"; -import { useCallback, useEffect, useState } from "react"; import { useSearchParams, Link as RouterLink, useNavigate } from "react-router-dom"; -import { useAsync } from "react-use"; -import { ClipboardIcon, LightningIcon, QrCodeIcon } from "../../components/icons"; -import { UserAvatarLink } from "../../components/user-avatar-link"; -import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon"; -import ZapModal from "../../components/zap-modal"; -import { truncatedId } from "../../helpers/nostr/event"; +import { ClipboardIcon, QrCodeIcon } from "../../components/icons"; import QrScannerModal from "../../components/qr-scanner-modal"; import { safeDecode } from "../../helpers/nip19"; -import { useInvoiceModalContext } from "../../providers/invoice-modal"; import { matchHashtag } from "../../helpers/regexp"; +import RelaySelectionButton from "../../components/relay-selection/relay-selection-button"; +import RelaySelectionProvider, { useRelaySelectionRelays } from "../../providers/relay-selection-provider"; +import { useTimelineLoader } from "../../hooks/use-timeline-loader"; +import { Kind, nip19 } from "nostr-tools"; +import useSubject from "../../hooks/use-subject"; +import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; +import IntersectionObserverProvider from "../../providers/intersection-observer"; +import { NostrEvent } from "../../types/nostr-event"; +import { getUserDisplayName, parseKind0Event } from "../../helpers/user-metadata"; +import { UserAvatar } from "../../components/user-avatar"; +import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon"; +import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status"; +import { EventRelays } from "../../components/note/note-relays"; +import { EmbedableContent, embedUrls } from "../../helpers/embeds"; +import { embedNostrLinks, renderGenericUrl } from "../../components/embed-types"; +import { getEventRelays } from "../../services/event-relays"; +import relayScoreboardService from "../../services/relay-scoreboard"; -type relay = string; -type NostrBandSearchResults = { - query: string; - page: number; - page_size: number; - nip05_count: number; - timeline: any[]; - page_count: number; - result_count: number; - serp: any[]; - people_count: number; - people: [ - { - i: number; - pubkey: string; - name: string; - about: string; - picture: string; - nip05: string; - nip05_verified: boolean; - website: string; - display_name: string; - lud06: string; - lud16: string; - lud06_url: string; - first_tm: number; - last_tm: number; - last_tm_str: string; - followed_count: number; - following_count: number; - zappers: number; - zap_amount: number; - zapped_pubkeys: number; - zap_amount_sent: number; - zap_amount_processed: number; - zapped_pubkeys_processed: number; - zappers_processed: number; - twitter?: { - verified: boolean; - verify_event: string; - handle: string; - name: string; - bio: string; - picture: string; - followers: number; - tweet: string; - }; - relays: number[]; - } - ]; - relays: Record; -}; +function buildDescriptionContent(description: string) { + let content: EmbedableContent = [description.trim()]; -export default function SearchView() { + content = embedNostrLinks(content); + content = embedUrls(content, [renderGenericUrl]); + + return content; +} + +function ProfileResult({ event }: { event: NostrEvent }) { + const metadata = parseKind0Event(event); + + const aboutContent = metadata.about && buildDescriptionContent(metadata.about); + const nprofile = useMemo(() => { + const relays = getEventRelays(event.id).value; + const ranked = relayScoreboardService.getRankedRelays(relays).slice(2); + return nip19.nprofileEncode({ pubkey: event.pubkey, relays: ranked }); + }, [event.id]); + + return ( + + + + + + {getUserDisplayName(metadata, event.pubkey)} + + + + + + {aboutContent && ( + + {aboutContent} + + )} + + + + + + ); +} + +function SearchResults({ search }: { search: string }) { + const searchRelays = useRelaySelectionRelays(); + + const timeline = useTimelineLoader( + `search`, + searchRelays, + { search: search || "", kinds: [Kind.Metadata] }, + { enabled: !!search } + ); + + const events = useSubject(timeline?.timeline) ?? []; + + const callback = useTimelineCurserIntersectionCallback(timeline); + + return ( + + + {events.map((event) => ( + + ))} + + + + + ); +} + +export function SearchPage() { const navigate = useNavigate(); - const { isOpen: donateOpen, onOpen: openDonate, onClose: closeDonate } = useDisclosure(); - const { isOpen: qrScannerOpen, onOpen: openScanner, onClose: closeScanner } = useDisclosure(); + const qrScannerModal = useDisclosure(); const [searchParams, setSearchParams] = useSearchParams(); - const [search, setSearch] = useState(searchParams.get("q") ?? ""); - const { requestPay } = useInvoiceModalContext(); + const [searchInput, setSearchInput] = useState(searchParams.get("q") ?? ""); + + const search = searchParams.get("q"); // update the input value when search changes useEffect(() => { - setSearch(searchParams.get("q") ?? ""); + setSearchInput(searchParams.get("q") ?? ""); }, [searchParams]); const handleSearchText = (text: string) => { const cleanText = text.trim(); - if (cleanText.startsWith("nostr:") || cleanText.startsWith("web+nostr:") || safeDecode(search)) { + if (cleanText.startsWith("nostr:") || cleanText.startsWith("web+nostr:") || safeDecode(text)) { navigate({ pathname: "/l/" + encodeURIComponent(text) }, { replace: true }); return; } @@ -115,92 +144,41 @@ export default function SearchView() { // set the search when the form is submitted const handleSubmit = (e: React.SyntheticEvent) => { e.preventDefault(); - handleSearchText(search); + handleSearchText(searchInput); }; - // fetch search data from nostr.band - const { value: searchResults, loading } = useAsync(async () => { - if (!searchParams.has("q")) return; - return await fetch(`https://nostr.realsearch.cc/nostr?method=search&count=10&q=${searchParams.get("q")}`).then( - (res) => res.json() as Promise - ); - }, [searchParams.get("q")]); - - // handle data from qr code scanner - const handleQrCodeData = handleSearchText; - return ( - - + +
- } aria-label="Qr Scanner" /> + } aria-label="Qr Scanner" /> {!!navigator.clipboard.readText && ( } aria-label="Read clipboard" /> )} - setSearch(e.target.value)} /> - + setSearchInput(e.target.value)} /> + +
- {searchResults && ( - - Find what you where looking for? - - {donateOpen && ( - { - closeDonate(); - await requestPay(invoice); - }} - /> - )} - - )} - - - {searchResults?.people.map((person) => ( - - - - - - {person.name || truncatedId(person.pubkey)} - - - - - - - {person.about} - - - {person.followed_count} Followers - Created: {dayjs.unix(person.first_tm).toString()} - - - ))} - + {search && }
); } + +// TODO: remove this when there is a good way to allow the user to select from a list of filtered relays that support NIP-50 +const searchRelays = ["wss://relay.nostr.band", "wss://search.nos.today"]; +export default function SearchView() { + // const { value: searchRelays = ["wss://relay.nostr.band"] } = useAsync(async () => { + // const relays: string[] = await fetch("https://api.nostr.watch/v1/nip/50").then((res) => res.json()); + // return relays; + // }); + + return ( + + + + ); +} diff --git a/src/views/streams/components/stream-card.tsx b/src/views/streams/components/stream-card.tsx index c67448910..37af36693 100644 --- a/src/views/streams/components/stream-card.tsx +++ b/src/views/streams/components/stream-card.tsx @@ -20,7 +20,7 @@ import { UserAvatar } from "../../../components/user-avatar"; import { UserLink } from "../../../components/user-link"; import dayjs from "dayjs"; import StreamStatusBadge from "./status-badge"; -import { NoteRelays } from "../../../components/note/note-relays"; +import { EventRelays } from "../../../components/note/note-relays"; import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer"; import useEventNaddr from "../../../hooks/use-event-naddr"; import StreamDebugButton from "./stream-debug-button"; @@ -62,7 +62,7 @@ export default function StreamCard({ stream, ...props }: CardProps & { stream: P - + diff --git a/src/views/streams/index.tsx b/src/views/streams/index.tsx index edc39596c..fabe1f34e 100644 --- a/src/views/streams/index.tsx +++ b/src/views/streams/index.tsx @@ -1,5 +1,5 @@ -import { useCallback, useMemo, useRef, useState } from "react"; -import { Flex, Select } from "@chakra-ui/react"; +import { useCallback, useMemo, useState } from "react"; +import { Flex, Select, SimpleGrid } from "@chakra-ui/react"; import { useTimelineLoader } from "../../hooks/use-timeline-loader"; import IntersectionObserverProvider from "../../providers/intersection-observer"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; @@ -66,12 +66,12 @@ function StreamsPage() {
- + {streams.map((stream) => ( - + ))} - - + + ); diff --git a/src/views/user/about.tsx b/src/views/user/about.tsx index 4ffbbf2a5..cee07d032 100644 --- a/src/views/user/about.tsx +++ b/src/views/user/about.tsx @@ -100,8 +100,8 @@ export default function UserAboutTab() { alignItems={isMobile ? "flex-start" : "flex-end"} > - - {getUserDisplayName(metadata, pubkey)} + + {getUserDisplayName(metadata, pubkey)} @@ -115,19 +115,7 @@ export default function UserAboutTab() { position="absolute" /> - {aboutContent && ( - - {aboutContent.map((part, i) => - typeof part === "string" ? ( - - {part} - - ) : ( - React.cloneElement(part, { key: "part-" + i }) - ) - )} - - )} + {aboutContent && {aboutContent}} {metadata?.lud16 && ( diff --git a/src/views/user/components/header.tsx b/src/views/user/components/header.tsx index 8ab8e66ab..c2a380158 100644 --- a/src/views/user/components/header.tsx +++ b/src/views/user/components/header.tsx @@ -31,7 +31,9 @@ export default function Header({ - {getUserDisplayName(metadata, pubkey)} + + {getUserDisplayName(metadata, pubkey)} + {isSelf && ( diff --git a/src/views/user/components/user-card.tsx b/src/views/user/components/user-card.tsx index ff00bbe7f..8aca42d45 100644 --- a/src/views/user/components/user-card.tsx +++ b/src/views/user/components/user-card.tsx @@ -1,5 +1,5 @@ -import { Box, Flex, FlexProps, Heading, Input, Link } from "@chakra-ui/react"; -import { Link as ReactRouterLink } from "react-router-dom"; +import { Flex, FlexProps, Heading, Link } from "@chakra-ui/react"; +import { Link as RouterLink } from "react-router-dom"; import { useUserMetadata } from "../../../hooks/use-user-metadata"; import { getUserDisplayName } from "../../../helpers/user-metadata"; @@ -28,7 +28,7 @@ export const UserCard = ({ pubkey, relay, ...props }: UserCardProps) => { > - + {getUserDisplayName(metadata, pubkey)}