mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-02 08:58:36 +02:00
add lightning tip button
This commit is contained in:
parent
7cec112571
commit
aa5febcdfc
@ -13,7 +13,7 @@
|
||||
"@chakra-ui/react": "^2.4.4",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"bech32-buffer": "^0.2.1",
|
||||
"bech32": "^2.0.0",
|
||||
"framer-motion": "^7.10.3",
|
||||
"idb": "^7.1.1",
|
||||
"identicon.js": "^2.3.3",
|
||||
|
@ -74,8 +74,20 @@ export const AddIcon = createIcon({
|
||||
defaultProps,
|
||||
});
|
||||
|
||||
export const ArrowDownS = createIcon({
|
||||
export const ArrowDownSIcon = createIcon({
|
||||
displayName: "arrow-down-s",
|
||||
d: "M12 13.172l4.95-4.95 1.414 1.414L12 16 5.636 9.636 7.05 8.222z",
|
||||
defaultProps,
|
||||
});
|
||||
|
||||
export const LinkItem = createIcon({
|
||||
displayName: "ri-link",
|
||||
d: "M18.364 15.536L16.95 14.12l1.414-1.414a5 5 0 1 0-7.071-7.071L9.879 7.05 8.464 5.636 9.88 4.222a7 7 0 0 1 9.9 9.9l-1.415 1.414zm-2.828 2.828l-1.415 1.414a7 7 0 0 1-9.9-9.9l1.415-1.414L7.05 9.88l-1.414 1.414a5 5 0 1 0 7.071 7.071l1.414-1.414 1.415 1.414zm-.708-10.607l1.415 1.415-7.071 7.07-1.415-1.414 7.071-7.07z",
|
||||
defaultProps,
|
||||
});
|
||||
|
||||
export const LightningIcon = createIcon({
|
||||
displayName: "lightning",
|
||||
d: "M13 10h7l-9 13v-9H4l9-13z",
|
||||
defaultProps,
|
||||
});
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { Menu, MenuButton, MenuList, IconButton, MenuListProps } from "@chakra-ui/react";
|
||||
import { Menu, MenuButton, MenuList, IconButton, MenuListProps, MenuButtonProps } from "@chakra-ui/react";
|
||||
import { MoreIcon } from "./icons";
|
||||
|
||||
export type MenuIconButtonProps = {
|
||||
export type MenuIconButtonProps = MenuButtonProps & {
|
||||
children: MenuListProps["children"];
|
||||
};
|
||||
|
||||
export const MenuIconButton = ({ children }: MenuIconButtonProps) => (
|
||||
export const MenuIconButton = ({ children, ...props }: MenuIconButtonProps) => (
|
||||
<Menu isLazy>
|
||||
<MenuButton as={IconButton} icon={<MoreIcon />} aria-label="view raw" title="view raw" size="xs" />
|
||||
<MenuButton as={IconButton} icon={<MoreIcon />} aria-label="view raw" title="view raw" size="xs" {...props} />
|
||||
<MenuList>{children}</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
|
@ -29,7 +29,8 @@ import { isReply } from "../../helpers/nostr-event";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import identity from "../../services/identity";
|
||||
import { useUserContacts } from "../../hooks/use-user-contacts";
|
||||
import { ArrowDownS } from "../icons";
|
||||
import { ArrowDownSIcon } from "../icons";
|
||||
import { UserTipButton } from "../user-tip-button";
|
||||
|
||||
export type NoteProps = {
|
||||
event: NostrEvent;
|
||||
@ -44,7 +45,7 @@ export const Note = React.memo(({ event }: NoteProps) => {
|
||||
return (
|
||||
<Card padding="2" variant="outline">
|
||||
<CardHeader padding="0" mb="2">
|
||||
<HStack spacing="4">
|
||||
<Flex gap="2">
|
||||
<Flex flex="1" gap="2">
|
||||
<UserAvatarLink pubkey={event.pubkey} size="sm" />
|
||||
|
||||
@ -60,8 +61,9 @@ export const Note = React.memo(({ event }: NoteProps) => {
|
||||
{isReply(event) && <NoteCC event={event} />}
|
||||
</Box>
|
||||
</Flex>
|
||||
<UserTipButton pubkey={event.pubkey} size="xs" />
|
||||
<NoteMenu event={event} />
|
||||
</HStack>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
<CardBody padding="0">
|
||||
<Box overflow="hidden" width="100%">
|
||||
|
@ -23,7 +23,7 @@ const embeds: {
|
||||
},
|
||||
// Twitter tweet
|
||||
{
|
||||
regexp: /^https?:\/\/twitter\.com\/(?:\#!\/)?(\w+)\/status(es)?\/(\d+)/im,
|
||||
regexp: /^https?:\/\/twitter\.com\/(?:\#!\/)?(\w+)\/status(es)?\/(\d+)[^\s]+/im,
|
||||
render: (match) => <TweetEmbed href={match[0]} conversation={false} />,
|
||||
},
|
||||
// Youtube Video
|
||||
|
53
src/components/user-tip-button.tsx
Normal file
53
src/components/user-tip-button.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { IconButton, IconButtonProps, useToast } from "@chakra-ui/react";
|
||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||
import { LightningIcon } from "./icons";
|
||||
import { useState } from "react";
|
||||
import { encodeText } from "../helpers/bech32";
|
||||
|
||||
export const UserTipButton = ({ pubkey, ...props }: { pubkey: string } & Omit<IconButtonProps, "aria-label">) => {
|
||||
const metadata = useUserMetadata(pubkey);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const toast = useToast();
|
||||
if (!metadata) return null;
|
||||
|
||||
let lnurl = metadata.lud06;
|
||||
if (metadata.lud16 && !lnurl) {
|
||||
const parts = metadata.lud16.split("@");
|
||||
lnurl = encodeText("lnurl", `https://${parts[1]}/.well-known/lnurlp/${parts[0]}`);
|
||||
}
|
||||
|
||||
if (!lnurl) return null;
|
||||
|
||||
const handleClick = async () => {
|
||||
if (!lnurl) return;
|
||||
setLoading(true);
|
||||
|
||||
if (window.webln && window.webln.lnurl) {
|
||||
try {
|
||||
if (!window.webln.enabled) await window.webln.enable();
|
||||
await window.webln.lnurl(lnurl);
|
||||
|
||||
toast({
|
||||
title: "Tip sent",
|
||||
status: "success",
|
||||
duration: 1000,
|
||||
});
|
||||
} catch (e) {}
|
||||
} else {
|
||||
window.open(`lnurl:${lnurl}`);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
onClick={handleClick}
|
||||
aria-label="Send Tip"
|
||||
title="Send Tip"
|
||||
icon={<LightningIcon />}
|
||||
isLoading={loading}
|
||||
color="yellow.300"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
6
src/helpers/bech32.ts
Normal file
6
src/helpers/bech32.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { bech32 } from "bech32";
|
||||
|
||||
export function encodeText(prefix: string, text: string) {
|
||||
const words = bech32.toWords(new TextEncoder().encode(text));
|
||||
return bech32.encode(prefix, words, Infinity);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { decode, encode } from "bech32-buffer";
|
||||
import { bech32 } from "bech32";
|
||||
|
||||
export function isHex(key?: string) {
|
||||
if (key?.toLowerCase()?.match(/^[0-9a-f]{64}$/)) return true;
|
||||
@ -11,29 +11,29 @@ export enum Bech32Prefix {
|
||||
Note = "note",
|
||||
}
|
||||
|
||||
export function isBech32Key(key: string) {
|
||||
export function isBech32Key(bech32String: string) {
|
||||
try {
|
||||
let { prefix } = decode(key.toLowerCase());
|
||||
const { prefix } = bech32.decode(bech32String.toLowerCase());
|
||||
if (!["npub", "nsec", "note"].includes(prefix)) return false;
|
||||
if (!isHex(bech32ToHex(key))) return false;
|
||||
if (!isHex(bech32ToHex(bech32String))) return false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function bech32ToHex(key: string) {
|
||||
export function bech32ToHex(bech32String: string) {
|
||||
try {
|
||||
let { data } = decode(key);
|
||||
return toHexString(data);
|
||||
const { words } = bech32.decode(bech32String);
|
||||
return toHexString(new Uint8Array(bech32.fromWords(words)));
|
||||
} catch (error) {}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function hexToBech32(hex: string, prefix: Bech32Prefix) {
|
||||
try {
|
||||
let buffer = fromHexString(hex);
|
||||
return buffer && encode(prefix, buffer, "bech32");
|
||||
const hexArray = hexStringToUint8(hex);
|
||||
return hexArray && bech32.encode(prefix, bech32.toWords(hexArray));
|
||||
} catch (error) {
|
||||
// continue
|
||||
}
|
||||
@ -48,7 +48,7 @@ export function toHexString(buffer: Uint8Array) {
|
||||
}, "");
|
||||
}
|
||||
|
||||
export function fromHexString(str: string) {
|
||||
export function hexStringToUint8(str: string) {
|
||||
if (str.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(str)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -26,6 +26,9 @@ export type Kind0ParsedContent = {
|
||||
about?: string;
|
||||
picture?: string;
|
||||
banner?: string;
|
||||
website?: string;
|
||||
lud16?: string;
|
||||
lud06?: string;
|
||||
};
|
||||
|
||||
export function isETag(tag: Tag): tag is ETag {
|
||||
|
1
src/types/webln.d.ts
vendored
1
src/types/webln.d.ts
vendored
@ -6,6 +6,7 @@ declare global {
|
||||
webln?: WebLNProvider & {
|
||||
enabled?: boolean;
|
||||
isEnabled?: boolean;
|
||||
lnurl?: (lnurl: string) => Promise<{ paymentHash: string; preimage: string }>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { Avatar, MenuItem } from "@chakra-ui/react";
|
||||
import { MenuIconButton } from "../../../components/menu-icon-button";
|
||||
import { MenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||
|
||||
import { ClipboardIcon, IMAGE_ICONS } from "../../../components/icons";
|
||||
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip-19";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
import { truncatedId } from "../../../helpers/nostr-event";
|
||||
|
||||
export const UserProfileMenu = ({ pubkey }: { pubkey: string }) => {
|
||||
export const UserProfileMenu = ({ pubkey, ...props }: { pubkey: string } & Omit<MenuIconButtonProps, "children">) => {
|
||||
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
||||
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
|
||||
|
||||
return (
|
||||
<MenuIconButton>
|
||||
<MenuIconButton {...props}>
|
||||
<MenuItem
|
||||
as="a"
|
||||
icon={<Avatar src={IMAGE_ICONS.nostrGuruIcon} size="xs" />}
|
||||
|
@ -1,10 +1,25 @@
|
||||
import { Flex, Heading, SkeletonText, Tab, TabList, TabPanel, TabPanels, Tabs, Text, Box } from "@chakra-ui/react";
|
||||
import {
|
||||
Flex,
|
||||
Heading,
|
||||
SkeletonText,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Tabs,
|
||||
Text,
|
||||
Box,
|
||||
Link,
|
||||
IconButton,
|
||||
} from "@chakra-ui/react";
|
||||
import { Outlet, useLoaderData, useMatches, useNavigate } from "react-router-dom";
|
||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||
import { UserAvatar } from "../../components/user-avatar";
|
||||
import { getUserDisplayName } from "../../helpers/user-metadata";
|
||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||
import { UserProfileMenu } from "./components/user-profile-menu";
|
||||
import { LinkIcon } from "@chakra-ui/icons";
|
||||
import { UserTipButton } from "../../components/user-tip-button";
|
||||
|
||||
const tabs = [
|
||||
{ label: "Notes", path: "notes" },
|
||||
@ -32,10 +47,19 @@ const UserView = () => {
|
||||
<Flex direction="column" gap={isMobile ? 0 : 2}>
|
||||
<Heading size={isMobile ? "md" : "lg"}>{getUserDisplayName(metadata, pubkey)}</Heading>
|
||||
{!metadata ? <SkeletonText /> : <Text>{metadata?.about}</Text>}
|
||||
{metadata?.website && (
|
||||
<Text>
|
||||
<LinkIcon />{" "}
|
||||
<Link href={metadata.website} target="_blank" color="blue.500">
|
||||
{metadata.website}
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
<Box ml="auto">
|
||||
<Flex ml="auto" gap="2">
|
||||
<UserTipButton pubkey={pubkey} size="xs" />
|
||||
<UserProfileMenu pubkey={pubkey} />
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
|
10
yarn.lock
10
yarn.lock
@ -2719,16 +2719,16 @@ base64-js@^1.3.1:
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
bech32-buffer@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/bech32-buffer/-/bech32-buffer-0.2.1.tgz#8106f2f51bcb2ba1d9fb7718905c3042c5be2fcd"
|
||||
integrity sha512-fCG1TyZuCN48Sdw97p/IR39fvqpFlWDVpG7qnuU1Uc3+Xtc/0uqAp8U7bMW/bGuVF5CcNVIXwxQsWwUr6un6FQ==
|
||||
|
||||
bech32@^1.1.2:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9"
|
||||
integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==
|
||||
|
||||
bech32@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355"
|
||||
integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==
|
||||
|
||||
better-path-resolve@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/better-path-resolve/-/better-path-resolve-1.0.0.tgz#13a35a1104cdd48a7b74bf8758f96a1ee613f99d"
|
||||
|
Loading…
x
Reference in New Issue
Block a user