mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-27 20:17:05 +02:00
improve invoice integration
add simple followers list on desktop
This commit is contained in:
@@ -27,7 +27,7 @@ export const App = () => {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<LoginView />} />
|
<Route path="/login" element={<LoginView />} />
|
||||||
<Route
|
<Route
|
||||||
path="/user/:pubkey"
|
path="/u/:pubkey"
|
||||||
element={
|
element={
|
||||||
<RequireSetup>
|
<RequireSetup>
|
||||||
<UserPage />
|
<UserPage />
|
||||||
|
45
src/components/following-list.tsx
Normal file
45
src/components/following-list.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Box, Button, Flex, SkeletonText } from "@chakra-ui/react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { Bech32Prefix, normalizeToBech32 } from "../helpers/nip-19";
|
||||||
|
import { getUserDisplayName } from "../helpers/user-metadata";
|
||||||
|
import useSubject from "../hooks/use-subject";
|
||||||
|
import { useUserContacts } from "../hooks/use-user-contacts";
|
||||||
|
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||||
|
import identity from "../services/identity";
|
||||||
|
import { UserAvatar } from "./user-avatar";
|
||||||
|
|
||||||
|
const FollowingListItem = ({ pubkey }: { pubkey: string }) => {
|
||||||
|
const { metadata, loading } = useUserMetadata(pubkey);
|
||||||
|
|
||||||
|
if (loading || !metadata) return <SkeletonText />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
as={Link}
|
||||||
|
leftIcon={<UserAvatar pubkey={pubkey} size="xs" />}
|
||||||
|
overflow="hidden"
|
||||||
|
variant="outline"
|
||||||
|
to={`/u/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`}
|
||||||
|
justifyContent="flex-start"
|
||||||
|
>
|
||||||
|
{getUserDisplayName(metadata, pubkey)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FollowingList = () => {
|
||||||
|
const pubkey = useSubject(identity.pubkey);
|
||||||
|
const { contacts, loading } = useUserContacts(pubkey);
|
||||||
|
|
||||||
|
if (loading || !contacts) return <SkeletonText />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box overflow="auto" pr="2" pb="4" pt="2">
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
{contacts.contacts.map((contact) => (
|
||||||
|
<FollowingListItem key={contact.pubkey} pubkey={contact.pubkey} />
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@@ -44,3 +44,8 @@ export const ProfileIcon = createIcon({
|
|||||||
displayName: "user-line",
|
displayName: "user-line",
|
||||||
d: "M4 22a8 8 0 1 1 16 0h-2a6 6 0 1 0-12 0H4zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4z",
|
d: "M4 22a8 8 0 1 1 16 0h-2a6 6 0 1 0-12 0H4zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4z",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ClipboardIcon = createIcon({
|
||||||
|
displayName: "clipboard-line",
|
||||||
|
d: "M7 4V2h10v2h3.007c.548 0 .993.445.993.993v16.014a.994.994 0 0 1-.993.993H3.993A.994.994 0 0 1 3 21.007V4.993C3 4.445 3.445 4 3.993 4H7zm0 2H5v14h14V6h-2v2H7V6zm2-2v2h6V4H9z",
|
||||||
|
});
|
||||||
|
90
src/components/inline-invoice-card.tsx
Normal file
90
src/components/inline-invoice-card.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Heading,
|
||||||
|
IconButton,
|
||||||
|
Text,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { requestProvider } from "webln";
|
||||||
|
import { getReadableAmount, parsePaymentRequest } from "../helpers/bolt11";
|
||||||
|
import { useAsync } from "react-use";
|
||||||
|
import { ClipboardIcon } from "./icons";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
export type InvoiceButtonProps = {
|
||||||
|
paymentRequest: string;
|
||||||
|
};
|
||||||
|
export const InlineInvoiceCard = ({ paymentRequest }: InvoiceButtonProps) => {
|
||||||
|
const { value: invoice, error } = useAsync(async () =>
|
||||||
|
parsePaymentRequest(paymentRequest)
|
||||||
|
);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const handleClick = async (event: React.SyntheticEvent) => {
|
||||||
|
if (!window.webln) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const provider = await requestProvider();
|
||||||
|
const response = await provider.sendPayment(paymentRequest);
|
||||||
|
if (response.preimage) {
|
||||||
|
console.log("Paid");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to pay invoice");
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
<>{paymentRequest}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!invoice) return <>Loading Invoice...</>;
|
||||||
|
|
||||||
|
const isExpired = moment(invoice.expiry).isBefore(moment());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
padding="3"
|
||||||
|
borderColor="yellow.300"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderRadius="md"
|
||||||
|
display="flex"
|
||||||
|
gap="4"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Box flexGrow={1}>
|
||||||
|
<Text fontWeight="bold">Lightning Invoice</Text>
|
||||||
|
<Text>{invoice.description}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text color={isExpired ? "red.400" : undefined}>
|
||||||
|
{isExpired ? "Expired" : "Expires"}:{" "}
|
||||||
|
{moment(invoice.expiry).fromNow()}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<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) : ""}
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@@ -1,46 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Button } from "@chakra-ui/react";
|
|
||||||
import { requestProvider } from "webln";
|
|
||||||
import { getReadableAmount, parsePaymentRequest } from "../helpers/bolt11";
|
|
||||||
import { useAsync } from "react-use";
|
|
||||||
|
|
||||||
export type InvoiceButtonProps = {
|
|
||||||
paymentRequest: string;
|
|
||||||
};
|
|
||||||
export const InvoiceButton = ({ paymentRequest }: InvoiceButtonProps) => {
|
|
||||||
const { value: invoice, error } = useAsync(async () =>
|
|
||||||
parsePaymentRequest(paymentRequest)
|
|
||||||
);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const handleClick = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const provider = await requestProvider();
|
|
||||||
await provider.enable();
|
|
||||||
const response = await provider.sendPayment(paymentRequest);
|
|
||||||
if (response.preimage) {
|
|
||||||
console.log("Paid");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Failed to pay invoice");
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
<>{paymentRequest}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
colorScheme="yellow"
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleClick}
|
|
||||||
isLoading={loading}
|
|
||||||
>
|
|
||||||
⚡ Invoice for{" "}
|
|
||||||
{invoice?.amount ? getReadableAmount(invoice.amount) : "♾️"}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -1,12 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button, Container, Flex, IconButton, VStack } from "@chakra-ui/react";
|
import {
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
IconButton,
|
||||||
|
VStack,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { ErrorBoundary } from "./error-boundary";
|
|
||||||
import { ConnectedRelays } from "./connected-relays";
|
|
||||||
|
|
||||||
import { useIsMobile } from "../hooks/use-is-mobile";
|
|
||||||
import { ProfileButton } from "./profile-button";
|
|
||||||
import identity from "../services/identity";
|
|
||||||
import {
|
import {
|
||||||
GlobalIcon,
|
GlobalIcon,
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
@@ -14,6 +15,13 @@ import {
|
|||||||
ProfileIcon,
|
ProfileIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
} from "./icons";
|
} from "./icons";
|
||||||
|
import { ErrorBoundary } from "./error-boundary";
|
||||||
|
import { ConnectedRelays } from "./connected-relays";
|
||||||
|
|
||||||
|
import { useIsMobile } from "../hooks/use-is-mobile";
|
||||||
|
import { ProfileButton } from "./profile-button";
|
||||||
|
import identity from "../services/identity";
|
||||||
|
import { FollowingList } from "./following-list";
|
||||||
|
|
||||||
const MobileLayout = ({ children }: { children: React.ReactNode }) => {
|
const MobileLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -90,7 +98,8 @@ const DesktopLayout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
<ErrorBoundary>{children}</ErrorBoundary>
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
</Flex>
|
</Flex>
|
||||||
<VStack width="15rem" pt="2" alignItems="stretch" flexShrink={0}>
|
<VStack width="15rem" pt="2" alignItems="stretch" flexShrink={0}>
|
||||||
<Button onClick={() => navigate("/")}>Manage Follows</Button>
|
<Heading size="md">Following</Heading>
|
||||||
|
<FollowingList />
|
||||||
</VStack>
|
</VStack>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
@@ -12,10 +12,8 @@ import remarkImages from "remark-images";
|
|||||||
import remarkUnwrapImages from "remark-unwrap-images";
|
import remarkUnwrapImages from "remark-unwrap-images";
|
||||||
import rehypeExternalLinks from "rehype-external-links";
|
import rehypeExternalLinks from "rehype-external-links";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// import rehypeTruncate from "rehype-truncate";
|
|
||||||
// @ts-ignore
|
|
||||||
import linkifyRegex from "remark-linkify-regex";
|
import linkifyRegex from "remark-linkify-regex";
|
||||||
import { InvoiceButton } from "./invoice-button";
|
import { InlineInvoiceCard } from "./inline-invoice-card";
|
||||||
import { TweetEmbed } from "./tweet-embed";
|
import { TweetEmbed } from "./tweet-embed";
|
||||||
|
|
||||||
const lightningInvoiceRegExp = /(lightning:)?LNBC[A-Za-z0-9]+/i;
|
const lightningInvoiceRegExp = /(lightning:)?LNBC[A-Za-z0-9]+/i;
|
||||||
@@ -41,7 +39,9 @@ const HandleLinkTypes = (props: LinkProps) => {
|
|||||||
|
|
||||||
if (href) {
|
if (href) {
|
||||||
if (lightningInvoiceRegExp.test(href)) {
|
if (lightningInvoiceRegExp.test(href)) {
|
||||||
return <InvoiceButton paymentRequest={href.replace(/lightning:/i, "")} />;
|
return (
|
||||||
|
<InlineInvoiceCard paymentRequest={href.replace(/lightning:/i, "")} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (youtubeVideoLink.test(href)) {
|
if (youtubeVideoLink.test(href)) {
|
||||||
const parts = youtubeVideoLink.exec(href);
|
const parts = youtubeVideoLink.exec(href);
|
||||||
@@ -74,29 +74,23 @@ const components = {
|
|||||||
|
|
||||||
export type PostContentsProps = {
|
export type PostContentsProps = {
|
||||||
content: string;
|
content: string;
|
||||||
maxChars?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PostContents = React.memo(
|
export const PostContents = React.memo(({ content }: PostContentsProps) => {
|
||||||
({ content, maxChars }: PostContentsProps) => {
|
const fixedLines = content.replace(/(?<! )\n/g, " \n");
|
||||||
const fixedLines = content.replace(/(?<! )\n/g, " \n");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[
|
remarkPlugins={[
|
||||||
remarkImages,
|
remarkImages,
|
||||||
remarkUnwrapImages,
|
remarkUnwrapImages,
|
||||||
remarkGfm,
|
remarkGfm,
|
||||||
linkifyRegex(lightningInvoiceRegExp),
|
linkifyRegex(lightningInvoiceRegExp),
|
||||||
]}
|
]}
|
||||||
rehypePlugins={[
|
rehypePlugins={[[rehypeExternalLinks, { target: "_blank" }]]}
|
||||||
[rehypeExternalLinks, { target: "_blank" }],
|
components={components}
|
||||||
// [rehypeTruncate, { maxChars, disable: !maxChars }],
|
>
|
||||||
]}
|
{fixedLines}
|
||||||
components={components}
|
</ReactMarkdown>
|
||||||
>
|
);
|
||||||
{fixedLines}
|
});
|
||||||
</ReactMarkdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
@@ -20,7 +20,7 @@ import { PostModal } from "../post-modal";
|
|||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||||
import { UserAvatarLink } from "../user-avatar-link";
|
import { UserAvatarLink } from "../user-avatar-link";
|
||||||
import { getUserFullName } from "../../helpers/user-metadata";
|
import { getUserDisplayName } from "../../helpers/user-metadata";
|
||||||
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
|
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
|
||||||
|
|
||||||
import { PostContents } from "../post-contents";
|
import { PostContents } from "../post-contents";
|
||||||
@@ -35,9 +35,7 @@ export const Post = React.memo(({ event }: PostProps) => {
|
|||||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||||
const { metadata } = useUserMetadata(event.pubkey);
|
const { metadata } = useUserMetadata(event.pubkey);
|
||||||
|
|
||||||
const username = metadata
|
const username = metadata && getUserDisplayName(metadata, event.pubkey);
|
||||||
? getUserFullName(metadata) || event.pubkey
|
|
||||||
: event.pubkey;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card padding="2" variant="outline">
|
<Card padding="2" variant="outline">
|
||||||
@@ -49,7 +47,7 @@ export const Post = React.memo(({ event }: PostProps) => {
|
|||||||
<Box>
|
<Box>
|
||||||
<Heading size="sm">
|
<Heading size="sm">
|
||||||
<Link
|
<Link
|
||||||
to={`/user/${normalizeToBech32(
|
to={`/u/${normalizeToBech32(
|
||||||
event.pubkey,
|
event.pubkey,
|
||||||
Bech32Prefix.Pubkey
|
Bech32Prefix.Pubkey
|
||||||
)}`}
|
)}`}
|
||||||
@@ -66,7 +64,7 @@ export const Post = React.memo(({ event }: PostProps) => {
|
|||||||
<CardBody padding="0" mb="2">
|
<CardBody padding="0" mb="2">
|
||||||
<VStack alignItems="flex-start" justifyContent="stretch">
|
<VStack alignItems="flex-start" justifyContent="stretch">
|
||||||
<Box overflow="hidden" width="100%">
|
<Box overflow="hidden" width="100%">
|
||||||
<PostContents content={event.content} maxChars={300} />
|
<PostContents content={event.content} />
|
||||||
</Box>
|
</Box>
|
||||||
</VStack>
|
</VStack>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
@@ -4,23 +4,18 @@ import { Link } from "react-router-dom";
|
|||||||
import { Bech32Prefix, normalizeToBech32 } from "../helpers/nip-19";
|
import { Bech32Prefix, normalizeToBech32 } from "../helpers/nip-19";
|
||||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||||
import { UserAvatar, UserAvatarProps } from "./user-avatar";
|
import { UserAvatar, UserAvatarProps } from "./user-avatar";
|
||||||
|
import { getUserDisplayName } from "../helpers/user-metadata";
|
||||||
|
|
||||||
export const UserAvatarLink = React.memo(
|
export const UserAvatarLink = React.memo(
|
||||||
({ pubkey, ...props }: UserAvatarProps) => {
|
({ pubkey, ...props }: UserAvatarProps) => {
|
||||||
const { metadata } = useUserMetadata(pubkey);
|
const { metadata } = useUserMetadata(pubkey);
|
||||||
|
const label = metadata
|
||||||
let label = "Loading...";
|
? getUserDisplayName(metadata, pubkey)
|
||||||
if (metadata?.display_name && metadata?.name) {
|
: "Loading...";
|
||||||
label = `${metadata.display_name} (${metadata.name})`;
|
|
||||||
} else if (metadata?.name) {
|
|
||||||
label = metadata.name;
|
|
||||||
} else {
|
|
||||||
label = normalizeToBech32(pubkey) ?? pubkey;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={label}>
|
<Tooltip label={label}>
|
||||||
<Link to={`/user/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`}>
|
<Link to={`/u/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`}>
|
||||||
<UserAvatar pubkey={pubkey} {...props} />
|
<UserAvatar pubkey={pubkey} {...props} />
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@@ -3,12 +3,16 @@ import {
|
|||||||
Section,
|
Section,
|
||||||
AmountSection,
|
AmountSection,
|
||||||
DescriptionSection,
|
DescriptionSection,
|
||||||
|
TimestampSection,
|
||||||
} from "light-bolt11-decoder";
|
} from "light-bolt11-decoder";
|
||||||
|
import { convertTimestampToDate } from "./date";
|
||||||
|
|
||||||
export type ParsedInvoice = {
|
export type ParsedInvoice = {
|
||||||
paymentRequest: string;
|
paymentRequest: string;
|
||||||
description: string;
|
description: string;
|
||||||
amount?: number;
|
amount?: number;
|
||||||
|
timestamp: Date;
|
||||||
|
expiry: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
function isDescription(section: Section): section is DescriptionSection {
|
function isDescription(section: Section): section is DescriptionSection {
|
||||||
@@ -17,14 +21,20 @@ function isDescription(section: Section): section is DescriptionSection {
|
|||||||
function isAmount(section: Section): section is AmountSection {
|
function isAmount(section: Section): section is AmountSection {
|
||||||
return section.name === "amount";
|
return section.name === "amount";
|
||||||
}
|
}
|
||||||
|
function isTimestamp(section: Section): section is TimestampSection {
|
||||||
|
return section.name === "timestamp";
|
||||||
|
}
|
||||||
|
|
||||||
export function parsePaymentRequest(paymentRequest: string): ParsedInvoice {
|
export function parsePaymentRequest(paymentRequest: string): ParsedInvoice {
|
||||||
const decoded = decode(paymentRequest);
|
const decoded = decode(paymentRequest);
|
||||||
|
const timestamp = decoded.sections.find(isTimestamp)?.value ?? 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paymentRequest: decoded.paymentRequest,
|
paymentRequest: decoded.paymentRequest,
|
||||||
description: decoded.sections.find(isDescription)?.value ?? "",
|
description: decoded.sections.find(isDescription)?.value ?? "",
|
||||||
amount: decoded.sections.find(isAmount)?.value,
|
amount: decoded.sections.find(isAmount)?.value,
|
||||||
|
timestamp: convertTimestampToDate(timestamp),
|
||||||
|
expiry: convertTimestampToDate(timestamp + decoded.expiry),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,9 +1,15 @@
|
|||||||
import { Kind0ParsedContent } from "../types/nostr-event";
|
import { Kind0ParsedContent } from "../types/nostr-event";
|
||||||
|
import { normalizeToBech32 } from "./nip-19";
|
||||||
|
import { truncatedId } from "./nostr-event";
|
||||||
|
|
||||||
export function getUserFullName(metadata: Kind0ParsedContent) {
|
export function getUserDisplayName(
|
||||||
|
metadata: Kind0ParsedContent,
|
||||||
|
pubkey: string
|
||||||
|
) {
|
||||||
if (metadata?.display_name && metadata?.name) {
|
if (metadata?.display_name && metadata?.name) {
|
||||||
return `${metadata.display_name} (${metadata.name})`;
|
return `${metadata.display_name} (${metadata.name})`;
|
||||||
} else if (metadata?.name) {
|
} else if (metadata?.name) {
|
||||||
return metadata.name;
|
return metadata.name;
|
||||||
}
|
}
|
||||||
|
return truncatedId(normalizeToBech32(pubkey) ?? pubkey);
|
||||||
}
|
}
|
||||||
|
18
src/hooks/use-user-contacts.ts
Normal file
18
src/hooks/use-user-contacts.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import settings from "../services/settings";
|
||||||
|
import userContacts from "../services/user-contacts";
|
||||||
|
import useSubject from "./use-subject";
|
||||||
|
|
||||||
|
export function useUserContacts(pubkey: string) {
|
||||||
|
const relays = useSubject(settings.relays);
|
||||||
|
const observable = useMemo(
|
||||||
|
() => userContacts.requestUserContacts(pubkey, relays),
|
||||||
|
[pubkey, relays]
|
||||||
|
);
|
||||||
|
const contacts = useSubject(observable) ?? undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading: !contacts,
|
||||||
|
contacts,
|
||||||
|
};
|
||||||
|
}
|
11
src/types/webln.d.ts
vendored
Normal file
11
src/types/webln.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { NostrEvent } from "./nostr-event";
|
||||||
|
import { WebLNProvider } from "webln";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
webln?: WebLNProvider & {
|
||||||
|
enabled?: boolean;
|
||||||
|
isEnabled?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@@ -18,12 +18,12 @@ import { useParams } from "react-router-dom";
|
|||||||
import { UserPostsTab } from "./posts";
|
import { UserPostsTab } from "./posts";
|
||||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||||
import { UserAvatar } from "../../components/user-avatar";
|
import { UserAvatar } from "../../components/user-avatar";
|
||||||
import { getUserFullName } from "../../helpers/user-metadata";
|
import { getUserDisplayName } from "../../helpers/user-metadata";
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||||
import { UserRelaysTab } from "./relays";
|
import { UserRelaysTab } from "./relays";
|
||||||
import { UserFollowingTab } from "./following";
|
import { UserFollowingTab } from "./following";
|
||||||
import { UserRepliesTab } from "./replies";
|
import { UserRepliesTab } from "./replies";
|
||||||
import { normalizeToBech32, normalizeToHex } from "../../helpers/nip-19";
|
import { normalizeToHex } from "../../helpers/nip-19";
|
||||||
import { Page } from "../../components/page";
|
import { Page } from "../../components/page";
|
||||||
import { UserProfileMenu } from "./user-profile-menu";
|
import { UserProfileMenu } from "./user-profile-menu";
|
||||||
|
|
||||||
@@ -60,8 +60,7 @@ export const UserView = ({ pubkey }: UserViewProps) => {
|
|||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const { metadata, loading: loadingMetadata } = useUserMetadata(pubkey, true);
|
const { metadata, loading: loadingMetadata } = useUserMetadata(pubkey, true);
|
||||||
const bech32Key = normalizeToBech32(pubkey);
|
const label = metadata && getUserDisplayName(metadata, pubkey);
|
||||||
const label = metadata ? getUserFullName(metadata) || bech32Key : bech32Key;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
|
Reference in New Issue
Block a user