mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-10 04:39:19 +02:00
handle nostr: links
This commit is contained in:
parent
25f3201ac8
commit
0ec3955d3c
@ -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
|
||||
|
||||
|
32
src/app.tsx
32
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: <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 />,
|
||||
|
@ -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
|
||||
|
@ -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 = () => {
|
||||
|
@ -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
72
src/views/link/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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">
|
||||
|
Loading…
x
Reference in New Issue
Block a user