From 0ec3955d3cbb4637a7bf44c24a6c4bc97033a380 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Mon, 6 Mar 2023 19:42:09 -0600 Subject: [PATCH] handle nostr: links --- README.md | 4 +- src/app.tsx | 32 ++++--------- src/components/note/note-menu.tsx | 18 ++++++- src/components/zap-modal.tsx | 12 ++--- src/index.tsx | 8 ++++ src/views/link/index.tsx | 72 ++++++++++++++++++++++++++++ src/views/user/components/header.tsx | 21 +++++++- 7 files changed, 132 insertions(+), 35 deletions(-) create mode 100644 src/views/link/index.tsx diff --git a/README.md b/README.md index ec3c0b876..2207b0e13 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ I would recomend you use a browser extension like [Alby](https://getalby.com/) o - [x] [NIP-15](https://github.com/nostr-protocol/nips/blob/master/15.md): End of Stored Events Notice - [x] [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md): bech32-encoded entities - [ ] [NIP-20](https://github.com/nostr-protocol/nips/blob/master/20.md): Command Results -- [ ] [NIP-21](https://github.com/nostr-protocol/nips/blob/master/21.md): `nostr:` URL scheme +- [x] [NIP-21](https://github.com/nostr-protocol/nips/blob/master/21.md): `nostr:` URL scheme - [x] [NIP-25](https://github.com/nostr-protocol/nips/blob/master/25.md): Reactions - [ ] [NIP-26](https://github.com/nostr-protocol/nips/blob/master/26.md): Delegated Event Signing - [ ] [NIP-33](https://github.com/nostr-protocol/nips/blob/master/33.md): Parameterized Replaceable Events @@ -78,10 +78,8 @@ I would recomend you use a browser extension like [Alby](https://getalby.com/) o - add `client` tag to published events - Save note drafts and let users manage them - make app a valid web share target https://developer.chrome.com/articles/web-share-target/ - - handle `nostr:` links - handle image share - implement NIP-56 and blocking -- block notes based on content - allow user to select relay or following list when fetching replies (default to my relays + following?) - massive thread note1dapvuu8fl09yjtg2gyr2h6nypaffl2sq0aj5raz86463qk5kpyzqlxvtc3 diff --git a/src/app.tsx b/src/app.tsx index 748260721..36c489b4a 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -30,6 +30,7 @@ const LoginNsecView = React.lazy(() => import("./views/login/nsec")); const UserZapsTab = React.lazy(() => import("./views/user/zaps")); const DirectMessagesView = React.lazy(() => import("./views/dm")); const DirectMessageChatView = React.lazy(() => import("./views/dm/chat")); +const NostrLinkView = React.lazy(() => import("./views/link")); const RequireCurrentAccount = ({ children }: { children: JSX.Element }) => { let location = useLocation(); @@ -107,30 +108,13 @@ const router = createBrowserRouter([ }, element: , }, - { - path: "settings", - element: , - }, - { - path: "relays", - element: , - }, - { - path: "notifications", - element: , - }, - { - path: "dm", - element: , - }, - { - path: "dm/:key", - element: , - }, - { - path: "profile", - element: , - }, + { path: "settings", element: }, + { path: "relays", element: }, + { path: "notifications", element: }, + { path: "dm", element: }, + { path: "dm/:key", element: }, + { path: "profile", element: }, + { path: "nostr-link", element: }, { path: "", element: , diff --git a/src/components/note/note-menu.tsx b/src/components/note/note-menu.tsx index 0ad9e93e2..c81d4447b 100644 --- a/src/components/note/note-menu.tsx +++ b/src/components/note/note-menu.tsx @@ -9,14 +9,27 @@ import { ModalCloseButton, } from "@chakra-ui/react"; import { useCopyToClipboard } from "react-use"; +import { nip19 } from "nostr-tools"; import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19"; import { NostrEvent } from "../../types/nostr-event"; import { MenuIconButton, MenuIconButtonProps } from "../menu-icon-button"; -import { ClipboardIcon, CodeIcon, LikeIcon } from "../icons"; +import { ClipboardIcon, CodeIcon, LikeIcon, ShareIcon } from "../icons"; import { getReferences } from "../../helpers/nostr-event"; import NoteReactionsModal from "./note-reactions-modal"; +import { getEventRelays } from "../../services/event-relays"; +import relayScoreboardService from "../../services/relay-scoreboard"; + +function getShareLink(eventId: string) { + const relays = getEventRelays(eventId).value; + const ranked = relayScoreboardService.getRankedRelays(relays); + const onlyTwo = ranked.slice(0, 2); + + if (onlyTwo.length > 0) { + return nip19.neventEncode({ id: eventId, relays: onlyTwo }); + } else return nip19.noteEncode(eventId); +} export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit) => { const infoModal = useDisclosure(); @@ -30,6 +43,9 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit}> Reactions + copyToClipboard("nostr:" + getShareLink(event.id))} icon={}> + Copy Share Link + {noteId && ( copyToClipboard(noteId)} icon={}> Copy Note ID diff --git a/src/components/zap-modal.tsx b/src/components/zap-modal.tsx index fc48a1288..5e3716248 100644 --- a/src/components/zap-modal.tsx +++ b/src/components/zap-modal.tsx @@ -148,14 +148,14 @@ export default function ZapModal({ const payWithApp = async () => { window.open("lightning:" + invoice); - window.addEventListener( - "focus", - () => { + const listener = () => { + if (document.visibilityState === "visible") { if (onPaid) onPaid(); onClose(); - }, - { once: true } - ); + document.removeEventListener("visibilitychange", listener); + } + }; + document.addEventListener("visibilitychange", listener); }; const handleClose = () => { diff --git a/src/index.tsx b/src/index.tsx index ec8db3a02..74b346c6f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,6 +3,14 @@ import { createRoot } from "react-dom/client"; import { App } from "./app"; import { Providers } from "./providers"; +// register nostr: protocol handler +try { + navigator.registerProtocolHandler("web+nostr", new URL("/nostr-link?q=%s", location.origin).toString()); +} catch (e) { + console.log("Failed to register handler"); + console.log(e); +} + const element = document.getElementById("root"); if (!element) throw new Error("missing mount point"); const root = createRoot(element); diff --git a/src/views/link/index.tsx b/src/views/link/index.tsx new file mode 100644 index 000000000..ca53ed9e3 --- /dev/null +++ b/src/views/link/index.tsx @@ -0,0 +1,72 @@ +import { Alert, AlertIcon, AlertTitle, Spinner } from "@chakra-ui/react"; +import { Navigate, useSearchParams } from "react-router-dom"; +import { Kind, nip19 } from "nostr-tools"; +import { useUserMetadata } from "../../hooks/use-user-metadata"; +import useSingleEvent from "../../hooks/use-single-event"; +import { useReadRelayUrls } from "../../hooks/use-client-relays"; +import { EventPointer, ProfilePointer } from "nostr-tools/lib/nip19"; + +export function NpubLinkHandler({ pubkey, relays }: { pubkey: string; relays?: string[] }) { + const readRelays = useReadRelayUrls(relays); + const metadata = useUserMetadata(pubkey, readRelays); + if (!metadata) return ; + return ; +} + +export function NoteLinkHandler({ eventId, relays }: { eventId: string; relays?: string[] }) { + const readRelays = useReadRelayUrls(relays); + const { event, loading } = useSingleEvent(eventId, readRelays); + if (loading) return ; + + if (!event) + return ( + + + Failed to find event + + ); + + if (event.kind !== Kind.Text) + return ( + + + Cant handle event kind {event.kind} + + ); + + return ; +} + +export default function NostrLinkView() { + const [searchParams] = useSearchParams(); + const rawLink = searchParams.get("q"); + + if (!rawLink) + return ( + + + No link provided + + ); + + const cleanLink = rawLink.replace(/(web\+)?nostr:/, ""); + const decoded = nip19.decode(cleanLink); + + if ((decoded.type = "npub")) return ; + if (decoded.type === "nprofile") { + const data = decoded.data as ProfilePointer; + return ; + } + if (decoded.type === "note") return ; + if (decoded.type === "nevent") { + const data = decoded.data as EventPointer; + return ; + } + + return ( + + + Unknown type "{decoded.type}" + + ); +} diff --git a/src/views/user/components/header.tsx b/src/views/user/components/header.tsx index 90d1e64b1..4e8a7929d 100644 --- a/src/views/user/components/header.tsx +++ b/src/views/user/components/header.tsx @@ -1,5 +1,8 @@ import { Flex, Heading, SkeletonText, Text, Link, IconButton } from "@chakra-ui/react"; +import { nip19 } from "nostr-tools"; +import { useMemo } from "react"; import { useNavigate, Link as RouterLink } from "react-router-dom"; +import { RelayMode } from "../../../classes/relay"; import { CopyIconButton } from "../../../components/copy-icon-button"; import { ChatIcon, ExternalLinkIcon, KeyIcon, SettingsIcon } from "../../../components/icons"; import { QrIconButton } from "../../../components/qr-icon-button"; @@ -12,9 +15,23 @@ import { truncatedId } from "../../../helpers/nostr-event"; import { fixWebsiteUrl, getUserDisplayName } from "../../../helpers/user-metadata"; import { useCurrentAccount } from "../../../hooks/use-current-account"; import { useIsMobile } from "../../../hooks/use-is-mobile"; +import useMergedUserRelays from "../../../hooks/use-merged-user-relays"; import { useUserMetadata } from "../../../hooks/use-user-metadata"; +import relayScoreboardService from "../../../services/relay-scoreboard"; import { UserProfileMenu } from "./user-profile-menu"; +function useUserShareLink(pubkey: string) { + const userRelays = useMergedUserRelays(pubkey); + + return useMemo(() => { + const writeUrls = userRelays.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url); + const ranked = relayScoreboardService.getRankedRelays(writeUrls); + const onlyTwo = ranked.slice(0, 2); + + return onlyTwo.length > 0 ? nip19.nprofileEncode({ pubkey, relays: onlyTwo }) : nip19.npubEncode(pubkey); + }, [userRelays]); +} + export default function Header({ pubkey }: { pubkey: string }) { const isMobile = useIsMobile(); const navigate = useNavigate(); @@ -24,6 +41,8 @@ export default function Header({ pubkey }: { pubkey: string }) { const account = useCurrentAccount(); const isSelf = pubkey === account.pubkey; + const shareLink = useUserShareLink(pubkey); + return ( @@ -56,7 +75,7 @@ export default function Header({ pubkey }: { pubkey: string }) { {truncatedId(npub, 10)} - + )}