mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-28 04:27:35 +02:00
add zap tab to user profile
fix twitter embed
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
"light-bolt11-decoder": "^2.1.0",
|
||||
"moment": "^2.29.4",
|
||||
"noble-secp256k1": "^1.2.14",
|
||||
"nostr-tools": "^1.4.1",
|
||||
"nostr-tools": "^1.5.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
|
@@ -26,6 +26,7 @@ import { LoginNip05View } from "./views/login/nip05";
|
||||
import { Button, Flex, Spinner, Text } from "@chakra-ui/react";
|
||||
import { deleteDatabase } from "./services/db";
|
||||
import { LoginNsecView } from "./views/login/nsec";
|
||||
import UserZapsTab from "./views/user/zaps";
|
||||
|
||||
const RequireCurrentAccount = ({ children }: { children: JSX.Element }) => {
|
||||
let location = useLocation();
|
||||
@@ -85,6 +86,7 @@ const router = createBrowserRouter([
|
||||
children: [
|
||||
{ path: "", element: <UserNotesTab /> },
|
||||
{ path: "notes", element: <UserNotesTab /> },
|
||||
{ path: "zaps", element: <UserZapsTab /> },
|
||||
{ path: "followers", element: <UserFollowersTab /> },
|
||||
{ path: "following", element: <UserFollowingTab /> },
|
||||
{ path: "relays", element: <UserRelaysTab /> },
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box, Button, ButtonGroup, IconButton, Text } from "@chakra-ui/react";
|
||||
import { requestProvider } from "webln";
|
||||
import { getReadableAmount, parsePaymentRequest } from "../helpers/bolt11";
|
||||
import { readableAmountInSats, parsePaymentRequest } from "../helpers/bolt11";
|
||||
import { useAsync } from "react-use";
|
||||
import { ClipboardIcon } from "./icons";
|
||||
import moment from "moment";
|
||||
@@ -62,7 +62,7 @@ export const InlineInvoiceCard = ({ paymentRequest }: InvoiceButtonProps) => {
|
||||
<ButtonGroup>
|
||||
<IconButton icon={<ClipboardIcon />} title="Copy to clipboard" aria-label="copy invoice" variant="outline" />
|
||||
<Button as="a" variant="outline" onClick={handleClick} isLoading={loading} href={`lightning:${paymentRequest}`}>
|
||||
⚡ Pay {invoice.amount ? getReadableAmount(invoice.amount) : ""}
|
||||
⚡ Pay {invoice.amount ? readableAmountInSats(invoice.amount) : ""}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
|
@@ -47,7 +47,7 @@ const embeds: EmbedType[] = [
|
||||
},
|
||||
// Twitter tweet
|
||||
{
|
||||
regexp: /^https?:\/\/twitter\.com\/(?:\#!\/)?(\w+)\/status(es)?\/(\d+)[^\s]*/im,
|
||||
regexp: /https?:\/\/twitter\.com\/(?:\#!\/)?(\w+)\/status(es)?\/(\d+)[^\s]*/im,
|
||||
render: (match) => <TweetEmbed href={match[0]} conversation={false} />,
|
||||
name: "Tweet",
|
||||
isMedia: true,
|
||||
|
@@ -4,7 +4,11 @@ import { LightningIcon } from "./icons";
|
||||
import { useState } from "react";
|
||||
import { encodeText } from "../helpers/bech32";
|
||||
|
||||
export const UserTipButton = ({ pubkey, ...props }: { pubkey: string } & Omit<IconButtonProps, "aria-label">) => {
|
||||
export const UserTipButton = ({
|
||||
pubkey,
|
||||
eventId,
|
||||
...props
|
||||
}: { pubkey: string; eventId?: string } & Omit<IconButtonProps, "aria-label">) => {
|
||||
const metadata = useUserMetadata(pubkey);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const toast = useToast();
|
||||
@@ -59,7 +63,7 @@ export const UserTipButton = ({ pubkey, ...props }: { pubkey: string } & Omit<Ic
|
||||
title="Send Tip"
|
||||
icon={<LightningIcon />}
|
||||
isLoading={loading}
|
||||
color="yellow.300"
|
||||
color="yellow.400"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
24
src/components/zap-button.tsx
Normal file
24
src/components/zap-button.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Button } from "@chakra-ui/react";
|
||||
import { useState } from "react";
|
||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||
import {} from "nostr-tools/nip57";
|
||||
|
||||
const ZapButton = ({ pubkey, noteId }: { noteId: string; pubkey: string }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const metadata = useUserMetadata(pubkey);
|
||||
|
||||
const handleClick = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
} catch (e) {}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button size="sm" onClick={handleClick} isLoading={loading}>
|
||||
Zap
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ZapButton;
|
@@ -32,7 +32,7 @@ export function parsePaymentRequest(paymentRequest: string): ParsedInvoice {
|
||||
};
|
||||
}
|
||||
|
||||
export function getReadableAmount(amount: number) {
|
||||
export function readableAmountInSats(amount: number) {
|
||||
const amountInSats = amount / 1000;
|
||||
if (amountInSats > 1000000) {
|
||||
return `${amountInSats / 1000000}M sats`;
|
||||
|
66
src/helpers/nip57.ts
Normal file
66
src/helpers/nip57.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { utf8Decoder } from "nostr-tools/utils";
|
||||
|
||||
import { bech32 } from "@scure/base";
|
||||
import { isETag, isPTag, NostrEvent } from "../types/nostr-event";
|
||||
import { parsePaymentRequest } from "./bolt11";
|
||||
|
||||
import { Kind0ParsedContent } from "./user-metadata";
|
||||
import { validateZapRequest } from "nostr-tools/nip57";
|
||||
|
||||
// based on https://github.com/nbd-wtf/nostr-tools/blob/master/nip57.ts
|
||||
export async function getZapEndpoint(metadata: Kind0ParsedContent): Promise<null | string> {
|
||||
try {
|
||||
let lnurl: string = "";
|
||||
let { lud06, lud16 } = metadata;
|
||||
if (lud06) {
|
||||
let { words } = bech32.decode(lud06, 1000);
|
||||
let data = bech32.fromWords(words);
|
||||
lnurl = utf8Decoder.decode(data);
|
||||
} else if (lud16) {
|
||||
let [name, domain] = lud16.split("@");
|
||||
lnurl = `https://${domain}/.well-known/lnurlp/${name}`;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
let res = await fetch(lnurl);
|
||||
let body = await res.json();
|
||||
|
||||
if (body.allowsNostr && body.nostrPubkey) {
|
||||
return body.callback;
|
||||
}
|
||||
} catch (err) {
|
||||
/*-*/
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isNoteZap(event: NostrEvent) {
|
||||
return event.tags.some(isETag);
|
||||
}
|
||||
export function isProfileZap(event: NostrEvent) {
|
||||
return !isNoteZap(event) && event.tags.some(isPTag);
|
||||
}
|
||||
|
||||
export function parseZapNote(event: NostrEvent) {
|
||||
const zapRequestStr = event.tags.find(([t, v]) => t === "description")?.[1];
|
||||
if (!zapRequestStr) throw new Error("no description tag");
|
||||
|
||||
const bolt11 = event.tags.find((t) => t[0] === "bolt11")?.[1];
|
||||
if (!bolt11) throw new Error("missing bolt11 invoice");
|
||||
|
||||
const error = validateZapRequest(zapRequestStr);
|
||||
if (error) throw new Error(error);
|
||||
|
||||
const zapRequest = JSON.parse(zapRequestStr) as NostrEvent;
|
||||
const payment = parsePaymentRequest(bolt11);
|
||||
|
||||
const eventId = zapRequest.tags.find(isETag)?.[1];
|
||||
|
||||
return {
|
||||
request: zapRequest,
|
||||
payment,
|
||||
eventId,
|
||||
};
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
|
||||
import { Account } from "./account";
|
||||
import { signEvent, getEventHash, getPublicKey } from "nostr-tools";
|
||||
import { getPublicKey } from "nostr-tools/keys";
|
||||
import { signEvent, getEventHash } from "nostr-tools/event";
|
||||
import db from "./db";
|
||||
|
||||
class SigningService {
|
||||
|
@@ -1,14 +1,7 @@
|
||||
export type ETag = ["e", string] | ["e", string, string] | ["e", string, string, string];
|
||||
export type PTag = ["p", string] | ["p", string, string];
|
||||
export type RTag = ["r", string] | ["r", string, string];
|
||||
export type Tag =
|
||||
| [string]
|
||||
| [string, string]
|
||||
| [string, string, string]
|
||||
| [string, string, string, string]
|
||||
| ETag
|
||||
| PTag
|
||||
| RTag;
|
||||
export type Tag = string[] | ETag | PTag | RTag;
|
||||
|
||||
export type NostrEvent = {
|
||||
id: string;
|
||||
|
@@ -21,7 +21,7 @@ import { RelayUrlInput } from "../../components/relay-url-input";
|
||||
import { Bech32Prefix, normalizeToBech32, normalizeToHex } from "../../helpers/nip-19";
|
||||
import accountService from "../../services/account";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
import { generatePrivateKey, getPublicKey } from "nostr-tools";
|
||||
import { generatePrivateKey, getPublicKey } from "nostr-tools/keys";
|
||||
import signingService from "../../services/signing";
|
||||
|
||||
export const LoginNsecView = () => {
|
||||
|
@@ -30,6 +30,7 @@ import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
|
||||
const tabs = [
|
||||
{ label: "Notes", path: "notes" },
|
||||
{ label: "Zaps", path: "zaps" },
|
||||
{ label: "Followers", path: "followers" },
|
||||
{ label: "Following", path: "following" },
|
||||
{ label: "Relays", path: "relays" },
|
||||
|
77
src/views/user/zaps.tsx
Normal file
77
src/views/user/zaps.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Box, Button, Flex, Spinner, Text, useDisclosure } from "@chakra-ui/react";
|
||||
import moment from "moment";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { ErrorBoundary, ErrorFallback } from "../../components/error-boundary";
|
||||
import QuoteNote from "../../components/note/quote-note";
|
||||
import { UserAvatarLink } from "../../components/user-avatar-link";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import { readableAmountInSats } from "../../helpers/bolt11";
|
||||
import { convertTimestampToDate } from "../../helpers/date";
|
||||
import { isProfileZap, parseZapNote } from "../../helpers/nip57";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
|
||||
const ZapNote = ({ zapEvent }: { zapEvent: NostrEvent }) => {
|
||||
const { isOpen, onToggle } = useDisclosure();
|
||||
try {
|
||||
const { request, payment, eventId } = parseZapNote(zapEvent);
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderWidth="1px"
|
||||
borderRadius="lg"
|
||||
overflow="hidden"
|
||||
padding="2"
|
||||
display="flex"
|
||||
gap="2"
|
||||
flexDirection="column"
|
||||
>
|
||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||
<UserAvatarLink pubkey={request.pubkey} size="xs" />
|
||||
<UserLink pubkey={request.pubkey} />
|
||||
{payment.amount && <Text>{readableAmountInSats(payment.amount)}</Text>}
|
||||
{request.content && (
|
||||
<Button variant="link" onClick={onToggle}>
|
||||
Show message
|
||||
</Button>
|
||||
)}
|
||||
<Text ml="auto">{moment(convertTimestampToDate(request.created_at)).fromNow()}</Text>
|
||||
</Flex>
|
||||
{request.content && isOpen && <Text>{request.content}</Text>}
|
||||
{eventId && <QuoteNote noteId={eventId} />}
|
||||
</Box>
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
return <ErrorFallback error={e} resetErrorBoundary={() => {}} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const UserZapsTab = () => {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
const readRelays = useReadRelayUrls();
|
||||
|
||||
const { events, loading, loadMore } = useTimelineLoader(
|
||||
`${pubkey}-zaps`,
|
||||
readRelays,
|
||||
{ "#p": [pubkey], kinds: [9735], since: moment().subtract(1, "day").unix() },
|
||||
{ pageSize: moment.duration(1, "day").asSeconds() }
|
||||
);
|
||||
const timeline = events.filter(isProfileZap);
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2" pr="2" pl="2">
|
||||
{timeline.map((event) => (
|
||||
<ErrorBoundary key={event.id}>
|
||||
<ZapNote zapEvent={event} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserZapsTab;
|
@@ -3483,10 +3483,10 @@ node-releases@^2.0.6:
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae"
|
||||
integrity sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==
|
||||
|
||||
nostr-tools@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.4.1.tgz#9a8ba4ae7b23e78fa495b1b98b9b26992f41f791"
|
||||
integrity sha512-pFAbVNtRMfW5ducUmk0f20IZv4a6pXYchBQKuA6D3x6sg83KoWipuiAie/gfOgrkoCsBLV14DmpWsVmo3N7WXQ==
|
||||
nostr-tools@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.5.0.tgz#b8b8d4d9e122c4acbdfe8b580e82f539f0866b42"
|
||||
integrity sha512-9zZdS3OF1hC8Tzpx2ycFLFx0AYSXBkLZ5EaXcPWIdZlQgSkpDrHl/TUYk2E8K7OocK+3VNbo4+7K2N38qdCjsg==
|
||||
dependencies:
|
||||
"@noble/hashes" "1.0.0"
|
||||
"@noble/secp256k1" "^1.7.1"
|
||||
|
Reference in New Issue
Block a user