handle nostr: links

This commit is contained in:
hzrd149 2023-03-06 19:42:09 -06:00
parent 25f3201ac8
commit 0ec3955d3c
7 changed files with 132 additions and 35 deletions

View File

@ -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

View File

@ -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: <NoteView />,
},
{
path: "settings",
element: <SettingsView />,
},
{
path: "relays",
element: <RelaysView />,
},
{
path: "notifications",
element: <NotificationsView />,
},
{
path: "dm",
element: <DirectMessagesView />,
},
{
path: "dm/:key",
element: <DirectMessageChatView />,
},
{
path: "profile",
element: <ProfileView />,
},
{ path: "settings", element: <SettingsView /> },
{ path: "relays", element: <RelaysView /> },
{ path: "notifications", element: <NotificationsView /> },
{ path: "dm", element: <DirectMessagesView /> },
{ path: "dm/:key", element: <DirectMessageChatView /> },
{ path: "profile", element: <ProfileView /> },
{ path: "nostr-link", element: <NostrLinkView /> },
{
path: "",
element: <HomeView />,

View File

@ -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<MenuIconButtonProps, "children">) => {
const infoModal = useDisclosure();
@ -30,6 +43,9 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
<MenuItem onClick={reactionsModal.onOpen} icon={<LikeIcon />}>
Reactions
</MenuItem>
<MenuItem onClick={() => copyToClipboard("nostr:" + getShareLink(event.id))} icon={<ShareIcon />}>
Copy Share Link
</MenuItem>
{noteId && (
<MenuItem onClick={() => copyToClipboard(noteId)} icon={<ClipboardIcon />}>
Copy Note ID

View File

@ -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 = () => {

View File

@ -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);

72
src/views/link/index.tsx Normal file
View File

@ -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 <Spinner />;
return <Navigate to={`/u/${pubkey}`} replace />;
}
export function NoteLinkHandler({ eventId, relays }: { eventId: string; relays?: string[] }) {
const readRelays = useReadRelayUrls(relays);
const { event, loading } = useSingleEvent(eventId, readRelays);
if (loading) return <Spinner />;
if (!event)
return (
<Alert status="error">
<AlertIcon />
<AlertTitle>Failed to find event</AlertTitle>
</Alert>
);
if (event.kind !== Kind.Text)
return (
<Alert status="error">
<AlertIcon />
<AlertTitle>Cant handle event kind {event.kind}</AlertTitle>
</Alert>
);
return <Navigate to={`/n/${eventId}`} replace />;
}
export default function NostrLinkView() {
const [searchParams] = useSearchParams();
const rawLink = searchParams.get("q");
if (!rawLink)
return (
<Alert status="warning">
<AlertIcon />
<AlertTitle>No link provided</AlertTitle>
</Alert>
);
const cleanLink = rawLink.replace(/(web\+)?nostr:/, "");
const decoded = nip19.decode(cleanLink);
if ((decoded.type = "npub")) return <NpubLinkHandler pubkey={decoded.data as string} />;
if (decoded.type === "nprofile") {
const data = decoded.data as ProfilePointer;
return <NpubLinkHandler pubkey={data.pubkey} relays={data.relays} />;
}
if (decoded.type === "note") return <NoteLinkHandler eventId={decoded.data as string} />;
if (decoded.type === "nevent") {
const data = decoded.data as EventPointer;
return <NoteLinkHandler eventId={data.id} relays={data.relays} />;
}
return (
<Alert status="warning">
<AlertIcon />
<AlertTitle>Unknown type "{decoded.type}"</AlertTitle>
</Alert>
);
}

View File

@ -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 (
<Flex direction="column" gap="2" px="2" pt="2">
<Flex gap="4">
@ -56,7 +75,7 @@ export default function Header({ pubkey }: { pubkey: string }) {
<KeyIcon />
<Text>{truncatedId(npub, 10)}</Text>
<CopyIconButton text={npub} title="Copy npub" aria-label="Copy npub" size="xs" />
<QrIconButton content={"nostr:" + npub} title="Show QrCode" aria-label="Show QrCode" size="xs" />
<QrIconButton content={"nostr:" + shareLink} title="Show QrCode" aria-label="Show QrCode" size="xs" />
</Flex>
)}
<Flex gap="2" ml="auto">