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 (
+
+
+
+
+
+ {searchResults && (
+
+ Find what you where looking for?
+ } size="sm" onClick={openDonate} flexShrink={0}>
+ Support Creator
+
+ {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"