add lightning tip button

This commit is contained in:
hzrd149 2023-02-07 17:04:18 -06:00
parent 7cec112571
commit aa5febcdfc
13 changed files with 132 additions and 31 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
View 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);
}

View File

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

View File

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

View File

@ -6,6 +6,7 @@ declare global {
webln?: WebLNProvider & {
enabled?: boolean;
isEnabled?: boolean;
lnurl?: (lnurl: string) => Promise<{ paymentHash: string; preimage: string }>;
};
}
}

View File

@ -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" />}

View File

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

View File

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