diff --git a/package.json b/package.json index 5471823e7..f2d22b8ce 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "react-dom": "^18.2.0", "react-error-boundary": "^3.1.4", "react-hook-form": "^7.43.1", + "react-qr-barcode-scanner": "^1.0.6", "react-router-dom": "^6.5.0", "react-singleton-hook": "^4.0.1", "react-use": "^17.4.0", diff --git a/src/app.tsx b/src/app.tsx index f97d53e8c..5b54ad137 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,4 +1,4 @@ -import { Suspense } from "react"; +import React, { Suspense } from "react"; import { createBrowserRouter, Navigate, Outlet, RouterProvider, useLocation } from "react-router-dom"; import { Button, Flex, Spinner, Text } from "@chakra-ui/react"; import { ErrorBoundary } from "./components/error-boundary"; @@ -32,6 +32,8 @@ import DirectMessagesView from "./views/dm"; import DirectMessageChatView from "./views/dm/chat"; import NostrLinkView from "./views/link"; import UserReportsTab from "./views/user/reports"; +// code split search view because QrScanner library is 400kB +const SearchView = React.lazy(() => import("./views/search")); const RequireCurrentAccount = ({ children }: { children: JSX.Element }) => { let location = useLocation(); @@ -113,6 +115,7 @@ const router = createBrowserRouter([ { path: "settings", element: }, { path: "relays", element: }, { path: "notifications", element: }, + { path: "search", element: }, { path: "dm", element: }, { path: "dm/:key", element: }, { path: "profile", element: }, diff --git a/src/components/page/desktop-side-nav.tsx b/src/components/page/desktop-side-nav.tsx index c59a42faf..b3894d5e9 100644 --- a/src/components/page/desktop-side-nav.tsx +++ b/src/components/page/desktop-side-nav.tsx @@ -4,7 +4,7 @@ import { Link, useNavigate } from "react-router-dom"; import { useCurrentAccount } from "../../hooks/use-current-account"; import accountService from "../../services/account"; import { ConnectedRelays } from "../connected-relays"; -import { ChatIcon, FeedIcon, LogoutIcon, NotificationIcon, ProfileIcon, RelayIcon } from "../icons"; +import { ChatIcon, FeedIcon, LogoutIcon, NotificationIcon, ProfileIcon, RelayIcon, SearchIcon } from "../icons"; import { ProfileButton } from "../profile-button"; import AccountSwitcher from "./account-switcher"; @@ -30,6 +30,9 @@ export default function DesktopSideNav() { + diff --git a/src/components/page/mobile-bottom-nav.tsx b/src/components/page/mobile-bottom-nav.tsx index 1b4edd3e3..7b426ce33 100644 --- a/src/components/page/mobile-bottom-nav.tsx +++ b/src/components/page/mobile-bottom-nav.tsx @@ -15,7 +15,6 @@ export default function MobileBottomNav() { onClick={() => navigate(`/search`)} flexGrow="1" size="md" - disabled /> } aria-label="Messages" onClick={() => navigate(`/dm`)} flexGrow="1" size="md" /> void } & Pick) { + const [stopStream, setStopStream] = useState(false); + const handleClose = () => { + // Stop the QR Reader stream (fixes issue where the browser freezes when closing the modal) and then dismiss the modal one tick later + setStopStream(true); + setTimeout(() => onClose(), 0); + }; + + return ( + + + + + { + if (result && result.getText()) { + handleClose(); + // wait for steam to be stopped before returning data + setTimeout(() => onData(result.getText()), 0); + } + }} + /> + + + + + + + + ); +} diff --git a/src/components/zap-modal.tsx b/src/components/zap-modal.tsx index 225cae25b..fd348bf6c 100644 --- a/src/components/zap-modal.tsx +++ b/src/components/zap-modal.tsx @@ -39,13 +39,23 @@ type FormValues = { comment: string; }; +export type ZapModalProps = Omit & { + event?: NostrEvent; + pubkey: string; + onPaid?: () => void; + initialComment?: string; + initialAmount?: number; +}; + export default function ZapModal({ event, pubkey, onClose, onPaid, + initialComment, + initialAmount, ...props -}: { event?: NostrEvent; pubkey: string; onPaid?: () => void } & Omit) { +}: ZapModalProps) { const metadata = useUserMetadata(pubkey); const { requestSignature } = useSigningContext(); const toast = useToast(); @@ -62,8 +72,8 @@ export default function ZapModal({ } = useForm({ mode: "onBlur", defaultValues: { - amount: 10, - comment: "", + amount: initialAmount ?? 10, + comment: initialComment ?? "", }, }); diff --git a/src/views/link/index.tsx b/src/views/link/index.tsx index ca53ed9e3..82d7f989c 100644 --- a/src/views/link/index.tsx +++ b/src/views/link/index.tsx @@ -52,7 +52,7 @@ export default function NostrLinkView() { const cleanLink = rawLink.replace(/(web\+)?nostr:/, ""); const decoded = nip19.decode(cleanLink); - if ((decoded.type = "npub")) return ; + if (decoded.type === "npub") return ; if (decoded.type === "nprofile") { const data = decoded.data as ProfilePointer; return ; diff --git a/src/views/search/index.tsx b/src/views/search/index.tsx new file mode 100644 index 000000000..3f9be07d4 --- /dev/null +++ b/src/views/search/index.tsx @@ -0,0 +1,181 @@ +import { + Button, + Card, + CardBody, + CardFooter, + CardHeader, + Flex, + Heading, + IconButton, + Input, + Text, + useDisclosure, +} from "@chakra-ui/react"; +import { useEffect, useState } from "react"; +import { useSearchParams, Link as RouterLink, useNavigate } from "react-router-dom"; +import { useAsync } from "react-use"; +import { LightningIcon, QrCodeIcon } from "../../components/icons"; +import { UserAvatarLink } from "../../components/user-avatar-link"; +import { UserDnsIdentityIcon } from "../../components/user-dns-identity"; +import ZapModal from "../../components/zap-modal"; +import { convertTimestampToDate } from "../../helpers/date"; +import { truncatedId } from "../../helpers/nostr-event"; +import QrScannerModal from "../../components/qr-scanner-modal"; + +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; +}; + +export default function SearchView() { + const navigate = useNavigate(); + const { isOpen: donateOpen, onOpen: openDonate, onClose: closeDonate } = useDisclosure(); + const { isOpen: qrScannerOpen, onOpen: openScanner, onClose: closeScanner } = useDisclosure(); + const [searchParams, setSearchParams] = useSearchParams(); + const [search, setSearch] = useState(searchParams.get("q") ?? ""); + + // update the input value when search changes + useEffect(() => { + setSearch(searchParams.get("q") ?? ""); + }, [searchParams]); + + // set the search when the form is submitted + const handleSubmit = (e: React.SyntheticEvent) => { + e.preventDefault(); + setSearchParams({ q: search }, { replace: true }); + }; + + // 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 = (text: string) => { + // if its a nostr: link pass it on the the link handler + if (text.startsWith("nostr:")) { + navigate({ pathname: "/nostr-link", search: `q=${text}` }, { replace: true }); + } else { + setSearchParams({ q: text }, { replace: true }); + } + }; + + return ( + + + +
+ + } aria-label="Qr Scanner" /> + setSearch(e.target.value)} /> + + +
+ + {searchResults && ( + + Find what you where looking for? + + {donateOpen && ( + + )} + + )} + + + {searchResults?.people.map((person) => ( + + + + + + {person.name || truncatedId(person.pubkey)} + + + + + + + {person.about} + + + {person.followed_count} Followers + Created: {convertTimestampToDate(person.first_tm).toLocaleDateString()} + + + ))} + +
+ ); +} diff --git a/src/views/user/components/header.tsx b/src/views/user/components/header.tsx index 4b56c78d4..5a61d0d08 100644 --- a/src/views/user/components/header.tsx +++ b/src/views/user/components/header.tsx @@ -1,4 +1,4 @@ -import { Flex, Heading, SkeletonText, Text, Link, IconButton } from "@chakra-ui/react"; +import { Flex, Heading, SkeletonText, Text, Link, IconButton, Image } from "@chakra-ui/react"; import { nip19 } from "nostr-tools"; import { useMemo } from "react"; import { useNavigate, Link as RouterLink } from "react-router-dom"; diff --git a/yarn.lock b/yarn.lock index a16232217..418266cf4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2331,6 +2331,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^16.9.35": + version "16.14.35" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.35.tgz#9d3cf047d85aca8006c4776693124a5be90ee429" + integrity sha512-NUEiwmSS1XXtmBcsm1NyRRPYjoZF2YTE89/5QiLt5mlGffYK9FQqOKuOLuXNrjPQV04oQgaZG+Yq02ZfHoFyyg== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -2374,6 +2383,20 @@ resolved "https://registry.yarnpkg.com/@zag-js/focus-visible/-/focus-visible-0.1.0.tgz#9777bbaff8316d0b3a14a9095631e1494f69dbc7" integrity sha512-PeaBcTmdZWcFf7n1aM+oiOdZc+sy14qi0emPIeUuGMTjbP0xLGrZu43kdpHnWSXy7/r4Ubp/vlg50MCV8+9Isg== +"@zxing/library@^0.17.0": + version "0.17.1" + resolved "https://registry.yarnpkg.com/@zxing/library/-/library-0.17.1.tgz#4c82bf401391c2b79bfbab0a6b1143da6d8feb1a" + integrity sha512-RuiBZuteGaFXCle/b0X+g3peN8UpDc3pGe/J7hZBzKWaMZLbjensR7ja3vy47xWhXU4e8MICGqegPMxc2V2sow== + dependencies: + ts-custom-error "^3.0.0" + optionalDependencies: + "@zxing/text-encoding" "~0.9.0" + +"@zxing/text-encoding@~0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b" + integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA== + acorn@^8.5.0: version "8.8.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" @@ -3673,6 +3696,15 @@ react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-qr-barcode-scanner@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/react-qr-barcode-scanner/-/react-qr-barcode-scanner-1.0.6.tgz#1df7ac3f3cb839ad673e8b619e0e93b4bdddc4e3" + integrity sha512-DdalO4oqHyxWPa4cIjiHeMS19HbIvKq+oo/PAglAsxmfhAUGC8sM1mJnzo0zPQM1yw9ZNpjrtqHz+rs86Mu7Ww== + dependencies: + "@types/react" "^16.9.35" + "@zxing/library" "^0.17.0" + react-webcam "^5.0.1" + react-refresh@^0.14.0: version "0.14.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" @@ -3751,6 +3783,11 @@ react-use@^17.4.0: ts-easing "^0.2.0" tslib "^2.1.0" +react-webcam@^5.0.1: + version "5.2.4" + resolved "https://registry.yarnpkg.com/react-webcam/-/react-webcam-5.2.4.tgz#714b4460ea43ac7ed081824299cd2a580f764478" + integrity sha512-Qqj14t68Ke1eoEYjFde+N48HtuIJg0ePIQRpFww9eZt5oBcDpe/l60h+m3VRFJAR5/E3dOhSU5R8EJEcdCq/Eg== + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" @@ -4132,6 +4169,11 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" +ts-custom-error@^3.0.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/ts-custom-error/-/ts-custom-error-3.3.1.tgz#8bd3c8fc6b8dc8e1cb329267c45200f1e17a65d1" + integrity sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A== + ts-easing@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec"