review / cleanup
This commit is contained in:
@ -1,7 +1,6 @@
|
||||
import { useState } from "react";
|
||||
|
||||
interface AsyncButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
interface AsyncButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
onClick(e: React.MouseEvent): Promise<void> | void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
@ -4,20 +4,14 @@ import { CSSProperties, useEffect, useState } from "react";
|
||||
import type { UserMetadata } from "Nostr";
|
||||
import useImgProxy from "Feed/ImgProxy";
|
||||
|
||||
const Avatar = ({
|
||||
user,
|
||||
...rest
|
||||
}: {
|
||||
user?: UserMetadata;
|
||||
onClick?: () => void;
|
||||
}) => {
|
||||
const Avatar = ({ user, ...rest }: { user?: UserMetadata; onClick?: () => void }) => {
|
||||
const [url, setUrl] = useState<string>(Nostrich);
|
||||
const { proxy } = useImgProxy();
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.picture) {
|
||||
proxy(user.picture, 120)
|
||||
.then((a) => setUrl(a))
|
||||
.then(a => setUrl(a))
|
||||
.catch(console.warn);
|
||||
}
|
||||
}, [user]);
|
||||
@ -25,14 +19,7 @@ const Avatar = ({
|
||||
const backgroundImage = `url(${url})`;
|
||||
const style = { "--img-url": backgroundImage } as CSSProperties;
|
||||
const domain = user?.nip05 && user.nip05.split("@")[1];
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
style={style}
|
||||
className="avatar"
|
||||
data-domain={domain?.toLowerCase()}
|
||||
></div>
|
||||
);
|
||||
return <div {...rest} style={style} className="avatar" data-domain={domain?.toLowerCase()}></div>;
|
||||
};
|
||||
|
||||
export default Avatar;
|
||||
|
@ -18,39 +18,21 @@ export default function BlockList({ variant }: BlockListProps) {
|
||||
{variant === "muted" && (
|
||||
<>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
{...messages.MuteCount}
|
||||
values={{ n: muted.length }}
|
||||
/>
|
||||
<FormattedMessage {...messages.MuteCount} values={{ n: muted.length }} />
|
||||
</h4>
|
||||
{muted.map((a) => {
|
||||
return (
|
||||
<ProfilePreview
|
||||
actions={<MuteButton pubkey={a} />}
|
||||
pubkey={a}
|
||||
options={{ about: false }}
|
||||
key={a}
|
||||
/>
|
||||
);
|
||||
{muted.map(a => {
|
||||
return <ProfilePreview actions={<MuteButton pubkey={a} />} pubkey={a} options={{ about: false }} key={a} />;
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{variant === "blocked" && (
|
||||
<>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
{...messages.BlockCount}
|
||||
values={{ n: blocked.length }}
|
||||
/>
|
||||
<FormattedMessage {...messages.BlockCount} values={{ n: blocked.length }} />
|
||||
</h4>
|
||||
{blocked.map((a) => {
|
||||
{blocked.map(a => {
|
||||
return (
|
||||
<ProfilePreview
|
||||
actions={<BlockButton pubkey={a} />}
|
||||
pubkey={a}
|
||||
options={{ about: false }}
|
||||
key={a}
|
||||
/>
|
||||
<ProfilePreview actions={<BlockButton pubkey={a} />} pubkey={a} options={{ about: false }} key={a} />
|
||||
);
|
||||
})}
|
||||
</>
|
||||
|
@ -9,12 +9,7 @@ interface CollapsedProps {
|
||||
setCollapsed(b: boolean): void;
|
||||
}
|
||||
|
||||
const Collapsed = ({
|
||||
text,
|
||||
children,
|
||||
collapsed,
|
||||
setCollapsed,
|
||||
}: CollapsedProps) => {
|
||||
const Collapsed = ({ text, children, collapsed, setCollapsed }: CollapsedProps) => {
|
||||
return collapsed ? (
|
||||
<div className="collapsed">
|
||||
<ShowMore text={text} onClick={() => setCollapsed(false)} />
|
||||
|
@ -10,23 +10,13 @@ export interface CopyProps {
|
||||
export default function Copy({ text, maxSize = 32 }: CopyProps) {
|
||||
const { copy, copied } = useCopy();
|
||||
const sliceLength = maxSize / 2;
|
||||
const trimmed =
|
||||
text.length > maxSize
|
||||
? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}`
|
||||
: text;
|
||||
const trimmed = text.length > maxSize ? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}` : text;
|
||||
|
||||
return (
|
||||
<div className="flex flex-row copy" onClick={() => copy(text)}>
|
||||
<span className="body">{trimmed}</span>
|
||||
<span
|
||||
className="icon"
|
||||
style={{ color: copied ? "var(--success)" : "var(--highlight)" }}
|
||||
>
|
||||
{copied ? (
|
||||
<Check width={13} height={13} />
|
||||
) : (
|
||||
<CopyIcon width={13} height={13} />
|
||||
)}
|
||||
<span className="icon" style={{ color: copied ? "var(--success)" : "var(--highlight)" }}>
|
||||
{copied ? <Check width={13} height={13} /> : <CopyIcon width={13} height={13} />}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
@ -22,18 +22,14 @@ export type DMProps = {
|
||||
|
||||
export default function DM(props: DMProps) {
|
||||
const dispatch = useDispatch();
|
||||
const pubKey = useSelector<RootState, HexKey | undefined>(
|
||||
(s) => s.login.publicKey
|
||||
);
|
||||
const pubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const publisher = useEventPublisher();
|
||||
const [content, setContent] = useState("Loading...");
|
||||
const [decrypted, setDecrypted] = useState(false);
|
||||
const { ref, inView } = useInView();
|
||||
const { formatMessage } = useIntl();
|
||||
const isMe = props.data.pubkey === pubKey;
|
||||
const otherPubkey = isMe
|
||||
? pubKey
|
||||
: unwrap(props.data.tags.find((a) => a[0] === "p")?.[1]);
|
||||
const otherPubkey = isMe ? pubKey : unwrap(props.data.tags.find(a => a[0] === "p")?.[1]);
|
||||
|
||||
async function decrypt() {
|
||||
const e = new Event(props.data);
|
||||
@ -55,18 +51,10 @@ export default function DM(props: DMProps) {
|
||||
return (
|
||||
<div className={`flex dm f-col${isMe ? " me" : ""}`} ref={ref}>
|
||||
<div>
|
||||
<NoteTime
|
||||
from={props.data.created_at * 1000}
|
||||
fallback={formatMessage(messages.JustNow)}
|
||||
/>
|
||||
<NoteTime from={props.data.created_at * 1000} fallback={formatMessage(messages.JustNow)} />
|
||||
</div>
|
||||
<div className="w-max">
|
||||
<Text
|
||||
content={content}
|
||||
tags={[]}
|
||||
users={new Map()}
|
||||
creator={otherPubkey}
|
||||
/>
|
||||
<Text content={content} tags={[]} users={new Map()} creator={otherPubkey} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -15,9 +15,7 @@ export interface FollowButtonProps {
|
||||
export default function FollowButton(props: FollowButtonProps) {
|
||||
const pubkey = parseId(props.pubkey);
|
||||
const publiser = useEventPublisher();
|
||||
const isFollowing = useSelector<RootState, boolean>(
|
||||
(s) => s.login.follows?.includes(pubkey) ?? false
|
||||
);
|
||||
const isFollowing = useSelector<RootState, boolean>(s => s.login.follows?.includes(pubkey) ?? false);
|
||||
const baseClassname = `${props.className} follow-button`;
|
||||
|
||||
async function follow(pubkey: HexKey) {
|
||||
@ -34,13 +32,8 @@ export default function FollowButton(props: FollowButtonProps) {
|
||||
<button
|
||||
type="button"
|
||||
className={isFollowing ? `${baseClassname} secondary` : baseClassname}
|
||||
onClick={() => (isFollowing ? unfollow(pubkey) : follow(pubkey))}
|
||||
>
|
||||
{isFollowing ? (
|
||||
<FormattedMessage {...messages.Unfollow} />
|
||||
) : (
|
||||
<FormattedMessage {...messages.Follow} />
|
||||
)}
|
||||
onClick={() => (isFollowing ? unfollow(pubkey) : follow(pubkey))}>
|
||||
{isFollowing ? <FormattedMessage {...messages.Unfollow} /> : <FormattedMessage {...messages.Follow} />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -10,10 +10,7 @@ export interface FollowListBaseProps {
|
||||
pubkeys: HexKey[];
|
||||
title?: string;
|
||||
}
|
||||
export default function FollowListBase({
|
||||
pubkeys,
|
||||
title,
|
||||
}: FollowListBaseProps) {
|
||||
export default function FollowListBase({ pubkeys, title }: FollowListBaseProps) {
|
||||
const publisher = useEventPublisher();
|
||||
|
||||
async function followAll() {
|
||||
@ -25,15 +22,11 @@ export default function FollowListBase({
|
||||
<div className="main-content">
|
||||
<div className="flex mt10 mb10">
|
||||
<div className="f-grow bold">{title}</div>
|
||||
<button
|
||||
className="transparent"
|
||||
type="button"
|
||||
onClick={() => followAll()}
|
||||
>
|
||||
<button className="transparent" type="button" onClick={() => followAll()}>
|
||||
<FormattedMessage {...messages.FollowAll} />
|
||||
</button>
|
||||
</div>
|
||||
{pubkeys?.map((a) => (
|
||||
{pubkeys?.map(a => (
|
||||
<ProfilePreview pubkey={a} key={a} />
|
||||
))}
|
||||
</div>
|
||||
|
@ -18,17 +18,10 @@ export default function FollowersList({ pubkey }: FollowersListProps) {
|
||||
|
||||
const pubkeys = useMemo(() => {
|
||||
const contactLists = feed?.store.notes.filter(
|
||||
(a) =>
|
||||
a.kind === EventKind.ContactList &&
|
||||
a.tags.some((b) => b[0] === "p" && b[1] === pubkey)
|
||||
a => a.kind === EventKind.ContactList && a.tags.some(b => b[0] === "p" && b[1] === pubkey)
|
||||
);
|
||||
return [...new Set(contactLists?.map((a) => a.pubkey))];
|
||||
return [...new Set(contactLists?.map(a => a.pubkey))];
|
||||
}, [feed, pubkey]);
|
||||
|
||||
return (
|
||||
<FollowListBase
|
||||
pubkeys={pubkeys}
|
||||
title={formatMessage(messages.FollowerCount, { n: pubkeys?.length })}
|
||||
/>
|
||||
);
|
||||
return <FollowListBase pubkeys={pubkeys} title={formatMessage(messages.FollowerCount, { n: pubkeys?.length })} />;
|
||||
}
|
||||
|
@ -20,10 +20,5 @@ export default function FollowsList({ pubkey }: FollowsListProps) {
|
||||
return getFollowers(feed.store, pubkey);
|
||||
}, [feed, pubkey]);
|
||||
|
||||
return (
|
||||
<FollowListBase
|
||||
pubkeys={pubkeys}
|
||||
title={formatMessage(messages.FollowingCount, { n: pubkeys?.length })}
|
||||
/>
|
||||
);
|
||||
return <FollowListBase pubkeys={pubkeys} title={formatMessage(messages.FollowingCount, { n: pubkeys?.length })} />;
|
||||
}
|
||||
|
@ -17,9 +17,7 @@ export interface FollowsYouProps {
|
||||
export default function FollowsYou({ pubkey }: FollowsYouProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const feed = useFollowsFeed(pubkey);
|
||||
const loginPubKey = useSelector<RootState, HexKey | undefined>(
|
||||
(s) => s.login.publicKey
|
||||
);
|
||||
const loginPubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
|
||||
const pubkeys = useMemo(() => {
|
||||
return getFollowers(feed.store, pubkey);
|
||||
@ -27,7 +25,5 @@ export default function FollowsYou({ pubkey }: FollowsYouProps) {
|
||||
|
||||
const followsMe = loginPubKey ? pubkeys.includes(loginPubKey) : false;
|
||||
|
||||
return followsMe ? (
|
||||
<span className="follows-you">{formatMessage(messages.FollowsYou)}</span>
|
||||
) : null;
|
||||
return followsMe ? <span className="follows-you">{formatMessage(messages.FollowsYou)}</span> : null;
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import "./Hashtag.css";
|
||||
const Hashtag = ({ tag }: { tag: string }) => {
|
||||
return (
|
||||
<span className="hashtag">
|
||||
<Link to={`/t/${tag}`} onClick={(e) => e.stopPropagation()}>
|
||||
<Link to={`/t/${tag}`} onClick={e => e.stopPropagation()}>
|
||||
#{tag}
|
||||
</Link>
|
||||
</span>
|
||||
|
@ -19,30 +19,17 @@ import TidalEmbed from "Element/TidalEmbed";
|
||||
import { ProxyImg } from "Element/ProxyImg";
|
||||
import { HexKey } from "Nostr";
|
||||
|
||||
export default function HyperText({
|
||||
link,
|
||||
creator,
|
||||
}: {
|
||||
link: string;
|
||||
creator: HexKey;
|
||||
}) {
|
||||
export default function HyperText({ link, creator }: { link: string; creator: HexKey }) {
|
||||
const pref = useSelector((s: RootState) => s.login.preferences);
|
||||
const follows = useSelector((s: RootState) => s.login.follows);
|
||||
|
||||
const render = useCallback(() => {
|
||||
const a = link;
|
||||
try {
|
||||
const hideNonFollows =
|
||||
pref.autoLoadMedia === "follows-only" && !follows.includes(creator);
|
||||
const hideNonFollows = pref.autoLoadMedia === "follows-only" && !follows.includes(creator);
|
||||
if (pref.autoLoadMedia === "none" || hideNonFollows) {
|
||||
return (
|
||||
<a
|
||||
href={a}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="ext"
|
||||
>
|
||||
<a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||
{a}
|
||||
</a>
|
||||
);
|
||||
@ -54,8 +41,7 @@ export default function HyperText({
|
||||
const soundcloundId = SoundCloudRegex.test(a) && RegExp.$1;
|
||||
const mixcloudId = MixCloudRegex.test(a) && RegExp.$1;
|
||||
const spotifyId = SpotifyRegex.test(a);
|
||||
const extension =
|
||||
FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
|
||||
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
|
||||
if (extension) {
|
||||
switch (extension) {
|
||||
case "gif":
|
||||
@ -83,11 +69,10 @@ export default function HyperText({
|
||||
<a
|
||||
key={url.toString()}
|
||||
href={url.toString()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onClick={e => e.stopPropagation()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="ext"
|
||||
>
|
||||
className="ext">
|
||||
{url.toString()}
|
||||
</a>
|
||||
);
|
||||
@ -124,13 +109,7 @@ export default function HyperText({
|
||||
return <SpotifyEmbed link={a} />;
|
||||
} else {
|
||||
return (
|
||||
<a
|
||||
href={a}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="ext"
|
||||
>
|
||||
<a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||
{a}
|
||||
</a>
|
||||
);
|
||||
@ -139,13 +118,7 @@ export default function HyperText({
|
||||
// Ignore the error.
|
||||
}
|
||||
return (
|
||||
<a
|
||||
href={a}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="ext"
|
||||
>
|
||||
<a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||
{a}
|
||||
</a>
|
||||
);
|
||||
|
@ -1,7 +1,6 @@
|
||||
import "./Invoice.css";
|
||||
import { useState } from "react";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
// @ts-expect-error No types available
|
||||
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
||||
import { useMemo } from "react";
|
||||
import SendSats from "Element/SendSats";
|
||||
@ -14,10 +13,6 @@ export interface InvoiceProps {
|
||||
invoice: string;
|
||||
}
|
||||
|
||||
interface Section {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default function Invoice(props: InvoiceProps) {
|
||||
const invoice = props.invoice;
|
||||
const webln = useWebln();
|
||||
@ -28,22 +23,19 @@ export default function Invoice(props: InvoiceProps) {
|
||||
try {
|
||||
const parsed = invoiceDecode(invoice);
|
||||
|
||||
const amount = parseInt(
|
||||
parsed.sections.find((a: Section) => a.name === "amount")?.value
|
||||
);
|
||||
const timestamp = parseInt(
|
||||
parsed.sections.find((a: Section) => a.name === "timestamp")?.value
|
||||
);
|
||||
const expire = parseInt(
|
||||
parsed.sections.find((a: Section) => a.name === "expiry")?.value
|
||||
);
|
||||
const description = parsed.sections.find(
|
||||
(a: Section) => a.name === "description"
|
||||
)?.value;
|
||||
const amountSection = parsed.sections.find(a => a.name === "amount");
|
||||
const amount = amountSection ? (amountSection.value as number) : NaN;
|
||||
|
||||
const timestampSection = parsed.sections.find(a => a.name === "timestamp");
|
||||
const timestamp = timestampSection ? (timestampSection.value as number) : NaN;
|
||||
|
||||
const expirySection = parsed.sections.find(a => a.name === "expiry");
|
||||
const expire = expirySection ? (expirySection.value as number) : NaN;
|
||||
const descriptionSection = parsed.sections.find(a => a.name === "description")?.value;
|
||||
const ret = {
|
||||
amount: !isNaN(amount) ? amount / 1000 : 0,
|
||||
expire: !isNaN(timestamp) && !isNaN(expire) ? timestamp + expire : null,
|
||||
description,
|
||||
description: descriptionSection as string | undefined,
|
||||
expired: false,
|
||||
};
|
||||
if (ret.expire) {
|
||||
@ -93,18 +85,13 @@ export default function Invoice(props: InvoiceProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`note-invoice flex ${isExpired ? "expired" : ""} ${
|
||||
isPaid ? "paid" : ""
|
||||
}`}
|
||||
>
|
||||
<div className={`note-invoice flex ${isExpired ? "expired" : ""} ${isPaid ? "paid" : ""}`}>
|
||||
<div className="invoice-header">{header()}</div>
|
||||
|
||||
<p className="invoice-amount">
|
||||
{amount > 0 && (
|
||||
<>
|
||||
{amount.toLocaleString()}{" "}
|
||||
<span className="sats">sat{amount === 1 ? "" : "s"}</span>
|
||||
{amount.toLocaleString()} <span className="sats">sat{amount === 1 ? "" : "s"}</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
@ -117,11 +104,7 @@ export default function Invoice(props: InvoiceProps) {
|
||||
</div>
|
||||
) : (
|
||||
<button disabled={isExpired} type="button" onClick={payInvoice}>
|
||||
{isExpired ? (
|
||||
<FormattedMessage {...messages.Expired} />
|
||||
) : (
|
||||
<FormattedMessage {...messages.Pay} />
|
||||
)}
|
||||
{isExpired ? <FormattedMessage {...messages.Expired} /> : <FormattedMessage {...messages.Pay} />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,59 +0,0 @@
|
||||
.lnurl-tip {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lnurl-tip .btn {
|
||||
background-color: inherit;
|
||||
width: 210px;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.lnurl-tip .btn:hover {
|
||||
background-color: var(--gray);
|
||||
}
|
||||
|
||||
.sat-amount {
|
||||
display: inline-block;
|
||||
background-color: var(--gray-secondary);
|
||||
color: var(--font-color);
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
user-select: none;
|
||||
margin: 2px 5px;
|
||||
}
|
||||
|
||||
.sat-amount:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sat-amount.active {
|
||||
font-weight: bold;
|
||||
color: var(--note-bg);
|
||||
background-color: var(--font-color);
|
||||
}
|
||||
|
||||
.lnurl-tip .invoice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lnurl-tip .invoice .actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lnurl-tip .invoice .actions .copy-action {
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
.lnurl-tip .invoice .actions .pay-actions {
|
||||
margin: 10px auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
@ -1,301 +0,0 @@
|
||||
import "./LNURLTip.css";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { bech32ToText, unwrap } from "Util";
|
||||
import { HexKey } from "Nostr";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import Modal from "Element/Modal";
|
||||
import QrCode from "Element/QrCode";
|
||||
import Copy from "Element/Copy";
|
||||
import useWebln from "Hooks/useWebln";
|
||||
|
||||
interface LNURLService {
|
||||
nostrPubkey?: HexKey;
|
||||
minSendable?: number;
|
||||
maxSendable?: number;
|
||||
metadata: string;
|
||||
callback: string;
|
||||
commentAllowed?: number;
|
||||
}
|
||||
|
||||
interface LNURLInvoice {
|
||||
pr: string;
|
||||
successAction?: LNURLSuccessAction;
|
||||
}
|
||||
|
||||
interface LNURLSuccessAction {
|
||||
description?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface LNURLTipProps {
|
||||
onClose?: () => void;
|
||||
svc?: string;
|
||||
show?: boolean;
|
||||
invoice?: string; // shortcut to invoice qr tab
|
||||
title?: string;
|
||||
notice?: string;
|
||||
note?: HexKey;
|
||||
author?: HexKey;
|
||||
}
|
||||
|
||||
export default function LNURLTip(props: LNURLTipProps) {
|
||||
const onClose = props.onClose || (() => undefined);
|
||||
const service = props.svc;
|
||||
const show = props.show || false;
|
||||
const { note, author } = props;
|
||||
const amounts = [50, 100, 500, 1_000, 5_000, 10_000, 50_000];
|
||||
const [payService, setPayService] = useState<LNURLService>();
|
||||
const [amount, setAmount] = useState<number>();
|
||||
const [customAmount, setCustomAmount] = useState<number>(0);
|
||||
const [invoice, setInvoice] = useState<LNURLInvoice>();
|
||||
const [comment, setComment] = useState<string>();
|
||||
const [error, setError] = useState<string>();
|
||||
const [success, setSuccess] = useState<LNURLSuccessAction>();
|
||||
const webln = useWebln(show);
|
||||
const publisher = useEventPublisher();
|
||||
|
||||
useEffect(() => {
|
||||
if (show && !props.invoice) {
|
||||
loadService()
|
||||
.then((a) => setPayService(unwrap(a)))
|
||||
.catch(() => setError("Failed to load LNURL service"));
|
||||
} else {
|
||||
setPayService(undefined);
|
||||
setError(undefined);
|
||||
setInvoice(props.invoice ? { pr: props.invoice } : undefined);
|
||||
setAmount(undefined);
|
||||
setComment(undefined);
|
||||
setSuccess(undefined);
|
||||
}
|
||||
}, [show, service]);
|
||||
|
||||
const serviceAmounts = useMemo(() => {
|
||||
if (payService) {
|
||||
const min = (payService.minSendable ?? 0) / 1000;
|
||||
const max = (payService.maxSendable ?? 0) / 1000;
|
||||
return amounts.filter((a) => a >= min && a <= max);
|
||||
}
|
||||
return [];
|
||||
}, [payService]);
|
||||
|
||||
const metadata = useMemo(() => {
|
||||
if (payService) {
|
||||
const meta: string[][] = JSON.parse(payService.metadata);
|
||||
const desc = meta.find((a) => a[0] === "text/plain");
|
||||
const image = meta.find((a) => a[0] === "image/png;base64");
|
||||
return {
|
||||
description: desc ? desc[1] : null,
|
||||
image: image ? image[1] : null,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}, [payService]);
|
||||
|
||||
const selectAmount = (a: number) => {
|
||||
setError(undefined);
|
||||
setInvoice(undefined);
|
||||
setAmount(a);
|
||||
};
|
||||
|
||||
async function fetchJson<T>(url: string) {
|
||||
const rsp = await fetch(url);
|
||||
if (rsp.ok) {
|
||||
const data: T = await rsp.json();
|
||||
console.log(data);
|
||||
setError(undefined);
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function loadService(): Promise<LNURLService | null> {
|
||||
if (service) {
|
||||
const isServiceUrl = service.toLowerCase().startsWith("lnurl");
|
||||
if (isServiceUrl) {
|
||||
const serviceUrl = bech32ToText(service);
|
||||
return await fetchJson(serviceUrl);
|
||||
} else {
|
||||
const ns = service.split("@");
|
||||
return await fetchJson(`https://${ns[1]}/.well-known/lnurlp/${ns[0]}`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function loadInvoice() {
|
||||
if (!amount || !payService) return null;
|
||||
let url = "";
|
||||
const amountParam = `amount=${Math.floor(amount * 1000)}`;
|
||||
const commentParam = comment
|
||||
? `&comment=${encodeURIComponent(comment)}`
|
||||
: "";
|
||||
if (payService.nostrPubkey && author) {
|
||||
const ev = await publisher.zap(author, note, comment);
|
||||
const nostrParam =
|
||||
ev && `&nostr=${encodeURIComponent(JSON.stringify(ev.ToObject()))}`;
|
||||
url = `${payService.callback}?${amountParam}${commentParam}${nostrParam}`;
|
||||
} else {
|
||||
url = `${payService.callback}?${amountParam}${commentParam}`;
|
||||
}
|
||||
try {
|
||||
const rsp = await fetch(url);
|
||||
if (rsp.ok) {
|
||||
const data = await rsp.json();
|
||||
console.log(data);
|
||||
if (data.status === "ERROR") {
|
||||
setError(data.reason);
|
||||
} else {
|
||||
setInvoice(data);
|
||||
setError("");
|
||||
payWebLNIfEnabled(data);
|
||||
}
|
||||
} else {
|
||||
setError("Failed to load invoice");
|
||||
}
|
||||
} catch (e) {
|
||||
setError("Failed to load invoice");
|
||||
}
|
||||
}
|
||||
|
||||
function custom() {
|
||||
const min = (payService?.minSendable ?? 0) / 1000;
|
||||
const max = (payService?.maxSendable ?? 21_000_000_000) / 1000;
|
||||
return (
|
||||
<div className="flex mb10">
|
||||
<input
|
||||
type="number"
|
||||
min={min}
|
||||
max={max}
|
||||
className="f-grow mr10"
|
||||
value={customAmount}
|
||||
onChange={(e) => setCustomAmount(parseInt(e.target.value))}
|
||||
/>
|
||||
<div className="btn" onClick={() => selectAmount(customAmount)}>
|
||||
Confirm
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function payWebLNIfEnabled(invoice: LNURLInvoice) {
|
||||
try {
|
||||
if (webln?.enabled) {
|
||||
const res = await webln.sendPayment(invoice.pr);
|
||||
console.log(res);
|
||||
setSuccess(invoice.successAction || {});
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
console.warn(e);
|
||||
if (e instanceof Error) {
|
||||
setError(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function invoiceForm() {
|
||||
if (invoice) return null;
|
||||
return (
|
||||
<>
|
||||
<div className="f-ellipsis mb10">
|
||||
{metadata?.description ?? service}
|
||||
</div>
|
||||
<div className="flex">
|
||||
{(payService?.commentAllowed ?? 0) > 0 ? (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Comment"
|
||||
className="mb10 f-grow"
|
||||
maxLength={payService?.commentAllowed}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb10">
|
||||
{serviceAmounts.map((a) => (
|
||||
<span
|
||||
className={`sat-amount ${amount === a ? "active" : ""}`}
|
||||
key={a}
|
||||
onClick={() => selectAmount(a)}
|
||||
>
|
||||
{a.toLocaleString()}
|
||||
</span>
|
||||
))}
|
||||
{payService ? (
|
||||
<span
|
||||
className={`sat-amount ${amount === -1 ? "active" : ""}`}
|
||||
onClick={() => selectAmount(-1)}
|
||||
>
|
||||
Custom
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{amount === -1 ? custom() : null}
|
||||
{(amount ?? 0) > 0 && (
|
||||
<button type="button" className="mb10" onClick={() => loadInvoice()}>
|
||||
Get Invoice
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function payInvoice() {
|
||||
if (success) return null;
|
||||
const pr = invoice?.pr;
|
||||
return (
|
||||
<>
|
||||
<div className="invoice">
|
||||
{props.notice && <b className="error">{props.notice}</b>}
|
||||
<QrCode data={pr} link={`lightning:${pr}`} />
|
||||
<div className="actions">
|
||||
{pr && (
|
||||
<>
|
||||
<div className="copy-action">
|
||||
<Copy text={pr} maxSize={26} />
|
||||
</div>
|
||||
<div className="pay-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.open(`lightning:${pr}`)}
|
||||
>
|
||||
Open Wallet
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function successAction() {
|
||||
if (!success) return null;
|
||||
return (
|
||||
<>
|
||||
<p>{success?.description ?? "Paid!"}</p>
|
||||
{success.url ? (
|
||||
<a href={success.url} rel="noreferrer" target="_blank">
|
||||
{success.url}
|
||||
</a>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const defaultTitle = payService?.nostrPubkey
|
||||
? "⚡️ Send Zap!"
|
||||
: "⚡️ Send sats";
|
||||
if (!show) return null;
|
||||
return (
|
||||
<Modal onClose={onClose}>
|
||||
<div className="lnurl-tip" onClick={(e) => e.stopPropagation()}>
|
||||
<h2>{props.title || defaultTitle}</h2>
|
||||
{invoiceForm()}
|
||||
{error ? <p className="error">{error}</p> : null}
|
||||
{payInvoice()}
|
||||
{successAction()}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -24,7 +24,7 @@ export default function LoadMore({
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => {
|
||||
setTick((x) => (x += 1));
|
||||
setTick(x => (x += 1));
|
||||
}, 500);
|
||||
return () => clearInterval(t);
|
||||
}, []);
|
||||
|
@ -16,8 +16,7 @@ export default function LogoutButton() {
|
||||
onClick={() => {
|
||||
dispatch(logout());
|
||||
navigate("/");
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<FormattedMessage {...messages.Logout} />
|
||||
</button>
|
||||
);
|
||||
|
@ -18,7 +18,7 @@ export default function Mention({ pubkey }: { pubkey: HexKey }) {
|
||||
}, [user, pubkey]);
|
||||
|
||||
return (
|
||||
<Link to={profileLink(pubkey)} onClick={(e) => e.stopPropagation()}>
|
||||
<Link to={profileLink(pubkey)} onClick={e => e.stopPropagation()}>
|
||||
@{name}
|
||||
</Link>
|
||||
);
|
||||
|
@ -3,14 +3,9 @@ import { useSelector } from "react-redux";
|
||||
import { RootState } from "State/Store";
|
||||
|
||||
const MixCloudEmbed = ({ link }: { link: string }) => {
|
||||
const feedPath =
|
||||
(MixCloudRegex.test(link) && RegExp.$1) +
|
||||
"%2F" +
|
||||
(MixCloudRegex.test(link) && RegExp.$2);
|
||||
const feedPath = (MixCloudRegex.test(link) && RegExp.$1) + "%2F" + (MixCloudRegex.test(link) && RegExp.$2);
|
||||
|
||||
const lightTheme = useSelector<RootState, boolean>(
|
||||
(s) => s.login.preferences.theme === "light"
|
||||
);
|
||||
const lightTheme = useSelector<RootState, boolean>(s => s.login.preferences.theme === "light");
|
||||
|
||||
const lightParams = lightTheme ? "light=1" : "light=0";
|
||||
|
||||
|
@ -8,10 +8,7 @@ export interface ModalProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function useOnClickOutside(
|
||||
ref: React.MutableRefObject<Element | null>,
|
||||
onClickOutside: () => void
|
||||
) {
|
||||
function useOnClickOutside(ref: React.MutableRefObject<Element | null>, onClickOutside: () => void) {
|
||||
useEffect(() => {
|
||||
function handleClickOutside(ev: MouseEvent) {
|
||||
if (ref && ref.current && !ref.current.contains(ev.target as Node)) {
|
||||
|
@ -24,29 +24,18 @@ export default function MutedList({ pubkey }: MutedListProps) {
|
||||
<div className="main-content">
|
||||
<div className="flex mt10">
|
||||
<div className="f-grow bold">
|
||||
<FormattedMessage
|
||||
{...messages.MuteCount}
|
||||
values={{ n: pubkeys?.length }}
|
||||
/>
|
||||
<FormattedMessage {...messages.MuteCount} values={{ n: pubkeys?.length }} />
|
||||
</div>
|
||||
<button
|
||||
disabled={hasAllMuted || pubkeys.length === 0}
|
||||
className="transparent"
|
||||
type="button"
|
||||
onClick={() => muteAll(pubkeys)}
|
||||
>
|
||||
onClick={() => muteAll(pubkeys)}>
|
||||
<FormattedMessage {...messages.MuteAll} />
|
||||
</button>
|
||||
</div>
|
||||
{pubkeys?.map((a) => {
|
||||
return (
|
||||
<ProfilePreview
|
||||
actions={<MuteButton pubkey={a} />}
|
||||
pubkey={a}
|
||||
options={{ about: false }}
|
||||
key={a}
|
||||
/>
|
||||
);
|
||||
{pubkeys?.map(a => {
|
||||
return <ProfilePreview actions={<MuteButton pubkey={a} />} pubkey={a} options={{ about: false }} key={a} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,11 +1,7 @@
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faCircleCheck,
|
||||
faSpinner,
|
||||
faTriangleExclamation,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCircleCheck, faSpinner, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import "./Nip05.css";
|
||||
import { HexKey } from "Nostr";
|
||||
@ -19,13 +15,9 @@ async function fetchNip05Pubkey(name: string, domain: string) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(
|
||||
name
|
||||
)}`
|
||||
);
|
||||
const res = await fetch(`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`);
|
||||
const data: NostrJson = await res.json();
|
||||
const match = Object.keys(data.names).find((n) => {
|
||||
const match = Object.keys(data.names).find(n => {
|
||||
return n.toLowerCase() === name.toLowerCase();
|
||||
});
|
||||
return match ? data.names[match] : undefined;
|
||||
@ -39,16 +31,12 @@ const VERIFICATION_STALE_TIMEOUT = 10 * 60 * 1000;
|
||||
|
||||
export function useIsVerified(pubkey: HexKey, nip05?: string) {
|
||||
const [name, domain] = nip05 ? nip05.split("@") : [];
|
||||
const { isError, isSuccess, data } = useQuery(
|
||||
["nip05", nip05],
|
||||
() => fetchNip05Pubkey(name, domain),
|
||||
{
|
||||
retry: false,
|
||||
retryOnMount: false,
|
||||
cacheTime: VERIFICATION_CACHE_TIME,
|
||||
staleTime: VERIFICATION_STALE_TIMEOUT,
|
||||
}
|
||||
);
|
||||
const { isError, isSuccess, data } = useQuery(["nip05", nip05], () => fetchNip05Pubkey(name, domain), {
|
||||
retry: false,
|
||||
retryOnMount: false,
|
||||
cacheTime: VERIFICATION_CACHE_TIME,
|
||||
staleTime: VERIFICATION_STALE_TIMEOUT,
|
||||
});
|
||||
const isVerified = isSuccess && data === pubkey;
|
||||
const cantVerify = isSuccess && data !== pubkey;
|
||||
return { isVerified, couldNotVerify: isError || cantVerify };
|
||||
@ -62,42 +50,18 @@ export interface Nip05Params {
|
||||
const Nip05 = (props: Nip05Params) => {
|
||||
const [name, domain] = props.nip05 ? props.nip05.split("@") : [];
|
||||
const isDefaultUser = name === "_";
|
||||
const { isVerified, couldNotVerify } = useIsVerified(
|
||||
props.pubkey,
|
||||
props.nip05
|
||||
);
|
||||
const { isVerified, couldNotVerify } = useIsVerified(props.pubkey, props.nip05);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex nip05${couldNotVerify ? " failed" : ""}`}
|
||||
onClick={(ev) => ev.stopPropagation()}
|
||||
>
|
||||
<div className={`flex nip05${couldNotVerify ? " failed" : ""}`} onClick={ev => ev.stopPropagation()}>
|
||||
{!isDefaultUser && <div className="nick">{`${name}@`}</div>}
|
||||
<span className="domain" data-domain={domain?.toLowerCase()}>
|
||||
{domain}
|
||||
</span>
|
||||
<span className="badge">
|
||||
{isVerified && (
|
||||
<FontAwesomeIcon
|
||||
color={"var(--highlight)"}
|
||||
icon={faCircleCheck}
|
||||
size="xs"
|
||||
/>
|
||||
)}
|
||||
{!isVerified && !couldNotVerify && (
|
||||
<FontAwesomeIcon
|
||||
color={"var(--fg-color)"}
|
||||
icon={faSpinner}
|
||||
size="xs"
|
||||
/>
|
||||
)}
|
||||
{couldNotVerify && (
|
||||
<FontAwesomeIcon
|
||||
color={"var(--error)"}
|
||||
icon={faTriangleExclamation}
|
||||
size="xs"
|
||||
/>
|
||||
)}
|
||||
{isVerified && <FontAwesomeIcon color={"var(--highlight)"} icon={faCircleCheck} size="xs" />}
|
||||
{!isVerified && !couldNotVerify && <FontAwesomeIcon color={"var(--fg-color)"} icon={faSpinner} size="xs" />}
|
||||
{couldNotVerify && <FontAwesomeIcon color={"var(--error)"} icon={faTriangleExclamation} size="xs" />}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
@ -20,6 +20,7 @@ import { debounce, hexToBech32 } from "Util";
|
||||
import { UserMetadata } from "Nostr";
|
||||
|
||||
import messages from "./messages";
|
||||
import { RootState } from "State/Store";
|
||||
|
||||
type Nip05ServiceProps = {
|
||||
name: string;
|
||||
@ -29,47 +30,34 @@ type Nip05ServiceProps = {
|
||||
supportLink: string;
|
||||
};
|
||||
|
||||
interface ReduxStore {
|
||||
login: { publicKey: string };
|
||||
}
|
||||
|
||||
export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
const navigate = useNavigate();
|
||||
const { formatMessage } = useIntl();
|
||||
const pubkey = useSelector<ReduxStore, string>((s) => s.login.publicKey);
|
||||
const pubkey = useSelector((s: RootState) => s.login.publicKey);
|
||||
const user = useUserProfile(pubkey);
|
||||
const publisher = useEventPublisher();
|
||||
const svc = useMemo(
|
||||
() => new ServiceProvider(props.service),
|
||||
[props.service]
|
||||
);
|
||||
const svc = useMemo(() => new ServiceProvider(props.service), [props.service]);
|
||||
const [serviceConfig, setServiceConfig] = useState<ServiceConfig>();
|
||||
const [error, setError] = useState<ServiceError>();
|
||||
const [handle, setHandle] = useState<string>("");
|
||||
const [domain, setDomain] = useState<string>("");
|
||||
const [availabilityResponse, setAvailabilityResponse] =
|
||||
useState<HandleAvailability>();
|
||||
const [registerResponse, setRegisterResponse] =
|
||||
useState<HandleRegisterResponse>();
|
||||
const [availabilityResponse, setAvailabilityResponse] = useState<HandleAvailability>();
|
||||
const [registerResponse, setRegisterResponse] = useState<HandleRegisterResponse>();
|
||||
const [showInvoice, setShowInvoice] = useState<boolean>(false);
|
||||
const [registerStatus, setRegisterStatus] = useState<CheckRegisterResponse>();
|
||||
|
||||
const domainConfig = useMemo(
|
||||
() => serviceConfig?.domains.find((a) => a.name === domain),
|
||||
[domain, serviceConfig]
|
||||
);
|
||||
const domainConfig = useMemo(() => serviceConfig?.domains.find(a => a.name === domain), [domain, serviceConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
svc
|
||||
.GetConfig()
|
||||
.then((a) => {
|
||||
.then(a => {
|
||||
if ("error" in a) {
|
||||
setError(a as ServiceError);
|
||||
} else {
|
||||
const svc = a as ServiceConfig;
|
||||
setServiceConfig(svc);
|
||||
const defaultDomain =
|
||||
svc.domains.find((a) => a.default)?.name || svc.domains[0].name;
|
||||
const defaultDomain = svc.domains.find(a => a.default)?.name || svc.domains[0].name;
|
||||
setDomain(defaultDomain);
|
||||
}
|
||||
})
|
||||
@ -88,10 +76,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
setAvailabilityResponse({ available: false, why: "TOO_LONG" });
|
||||
return;
|
||||
}
|
||||
const rx = new RegExp(
|
||||
domainConfig?.regex[0] ?? "",
|
||||
domainConfig?.regex[1] ?? ""
|
||||
);
|
||||
const rx = new RegExp(domainConfig?.regex[0] ?? "", domainConfig?.regex[1] ?? "");
|
||||
if (!rx.test(handle)) {
|
||||
setAvailabilityResponse({ available: false, why: "REGEX" });
|
||||
return;
|
||||
@ -99,7 +84,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
return debounce(500, () => {
|
||||
svc
|
||||
.CheckAvailable(handle, domain)
|
||||
.then((a) => {
|
||||
.then(a => {
|
||||
if ("error" in a) {
|
||||
setError(a as ServiceError);
|
||||
} else {
|
||||
@ -133,10 +118,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
}
|
||||
}, [registerResponse, showInvoice, svc]);
|
||||
|
||||
function mapError(
|
||||
e: ServiceErrorCode | undefined,
|
||||
t: string | null
|
||||
): string | undefined {
|
||||
function mapError(e: ServiceErrorCode | undefined, t: string | null): string | undefined {
|
||||
if (e === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
@ -152,8 +134,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
}
|
||||
|
||||
async function startBuy(handle: string, domain: string) {
|
||||
if (registerResponse) {
|
||||
setShowInvoice(true);
|
||||
if (!pubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -202,11 +183,11 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
type="text"
|
||||
placeholder="Handle"
|
||||
value={handle}
|
||||
onChange={(e) => setHandle(e.target.value.toLowerCase())}
|
||||
onChange={e => setHandle(e.target.value.toLowerCase())}
|
||||
/>
|
||||
@
|
||||
<select value={domain} onChange={(e) => setDomain(e.target.value)}>
|
||||
{serviceConfig?.domains.map((a) => (
|
||||
<select value={domain} onChange={e => setDomain(e.target.value)}>
|
||||
{serviceConfig?.domains.map(a => (
|
||||
<option key={a.name}>{a.name}</option>
|
||||
))}
|
||||
</select>
|
||||
@ -215,10 +196,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
{availabilityResponse?.available && !registerStatus && (
|
||||
<div className="flex">
|
||||
<div className="mr10">
|
||||
<FormattedMessage
|
||||
{...messages.Sats}
|
||||
values={{ n: availabilityResponse.quote?.price }}
|
||||
/>
|
||||
<FormattedMessage {...messages.Sats} values={{ n: availabilityResponse.quote?.price }} />
|
||||
<br />
|
||||
<small>{availabilityResponse.quote?.data.type}</small>
|
||||
</div>
|
||||
@ -238,10 +216,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
<div className="flex">
|
||||
<b className="error">
|
||||
<FormattedMessage {...messages.NotAvailable} />{" "}
|
||||
{mapError(
|
||||
availabilityResponse.why,
|
||||
availabilityResponse.reasonTag || null
|
||||
)}
|
||||
{mapError(availabilityResponse.why, availabilityResponse.reasonTag || null)}
|
||||
</b>
|
||||
</div>
|
||||
)}
|
||||
|
@ -1,11 +1,5 @@
|
||||
import "./Note.css";
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
useLayoutEffect,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
import { useCallback, useMemo, useState, useLayoutEffect, ReactNode } from "react";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
@ -58,21 +52,11 @@ const HiddenNote = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
export default function Note(props: NoteProps) {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
data,
|
||||
related,
|
||||
highlight,
|
||||
options: opt,
|
||||
["data-ev"]: parsedEvent,
|
||||
ignoreModeration = false,
|
||||
} = props;
|
||||
const { data, related, highlight, options: opt, ["data-ev"]: parsedEvent, ignoreModeration = false } = props;
|
||||
const ev = useMemo(() => parsedEvent ?? new NEvent(data), [data]);
|
||||
const pubKeys = useMemo(() => ev.Thread?.PubKeys || [], [ev]);
|
||||
const users = useUserProfiles(pubKeys);
|
||||
const deletions = useMemo(
|
||||
() => getReactions(related, ev.Id, EventKind.Deletion),
|
||||
[related]
|
||||
);
|
||||
const deletions = useMemo(() => getReactions(related, ev.Id, EventKind.Deletion), [related]);
|
||||
const { isMuted } = useModeration();
|
||||
const isOpMuted = isMuted(ev.PubKey);
|
||||
const { ref, inView, entry } = useInView({ triggerOnce: true });
|
||||
@ -99,14 +83,7 @@ export default function Note(props: NoteProps) {
|
||||
</b>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text
|
||||
content={body}
|
||||
tags={ev.Tags}
|
||||
users={users || new Map()}
|
||||
creator={ev.PubKey}
|
||||
/>
|
||||
);
|
||||
return <Text content={body} tags={ev.Tags} users={users || new Map()} creator={ev.PubKey} />;
|
||||
}, [ev]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@ -139,9 +116,7 @@ export default function Note(props: NoteProps) {
|
||||
mentions.push({
|
||||
pk,
|
||||
name: u.name ?? shortNpub,
|
||||
link: (
|
||||
<Link to={`/p/${npub}`}>{u.name ? `@${u.name}` : shortNpub}</Link>
|
||||
),
|
||||
link: <Link to={`/p/${npub}`}>{u.name ? `@${u.name}` : shortNpub}</Link>,
|
||||
});
|
||||
} else {
|
||||
mentions.push({
|
||||
@ -151,7 +126,7 @@ export default function Note(props: NoteProps) {
|
||||
});
|
||||
}
|
||||
}
|
||||
mentions.sort((a) => (a.name.startsWith("npub") ? 1 : -1));
|
||||
mentions.sort(a => (a.name.startsWith("npub") ? 1 : -1));
|
||||
const othersLength = mentions.length - maxMentions;
|
||||
const renderMention = (m: { link: React.ReactNode }, idx: number) => {
|
||||
return (
|
||||
@ -162,13 +137,8 @@ export default function Note(props: NoteProps) {
|
||||
);
|
||||
};
|
||||
const pubMentions =
|
||||
mentions.length > maxMentions
|
||||
? mentions?.slice(0, maxMentions).map(renderMention)
|
||||
: mentions?.map(renderMention);
|
||||
const others =
|
||||
mentions.length > maxMentions
|
||||
? formatMessage(messages.Others, { n: othersLength })
|
||||
: "";
|
||||
mentions.length > maxMentions ? mentions?.slice(0, maxMentions).map(renderMention) : mentions?.map(renderMention);
|
||||
const others = mentions.length > maxMentions ? formatMessage(messages.Others, { n: othersLength }) : "";
|
||||
return (
|
||||
<div className="reply">
|
||||
re:
|
||||
@ -178,11 +148,7 @@ export default function Note(props: NoteProps) {
|
||||
{others}
|
||||
</>
|
||||
) : (
|
||||
replyId && (
|
||||
<Link to={eventLink(replyId)}>
|
||||
{hexToBech32("note", replyId)?.substring(0, 12)}
|
||||
</Link>
|
||||
)
|
||||
replyId && <Link to={eventLink(replyId)}>{hexToBech32("note", replyId)?.substring(0, 12)}</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@ -192,10 +158,7 @@ export default function Note(props: NoteProps) {
|
||||
return (
|
||||
<>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
{...messages.UnknownEventKind}
|
||||
values={{ kind: ev.Kind }}
|
||||
/>
|
||||
<FormattedMessage {...messages.UnknownEventKind} values={{ kind: ev.Kind }} />
|
||||
</h4>
|
||||
<pre>{JSON.stringify(ev.ToObject(), undefined, " ")}</pre>
|
||||
</>
|
||||
@ -207,10 +170,7 @@ export default function Note(props: NoteProps) {
|
||||
return (
|
||||
<>
|
||||
<p className="highlight">
|
||||
<FormattedMessage
|
||||
{...messages.TranslatedFrom}
|
||||
values={{ lang: translated.fromLanguage }}
|
||||
/>
|
||||
<FormattedMessage {...messages.TranslatedFrom} values={{ lang: translated.fromLanguage }} />
|
||||
</p>
|
||||
{translated.text}
|
||||
</>
|
||||
@ -230,10 +190,7 @@ export default function Note(props: NoteProps) {
|
||||
<>
|
||||
{options.showHeader && (
|
||||
<div className="header flex">
|
||||
<ProfileImage
|
||||
pubkey={ev.RootPubKey}
|
||||
subHeader={replyTag() ?? undefined}
|
||||
/>
|
||||
<ProfileImage pubkey={ev.RootPubKey} subHeader={replyTag() ?? undefined} />
|
||||
{options.showTime && (
|
||||
<div className="info">
|
||||
<NoteTime from={ev.CreatedAt * 1000} />
|
||||
@ -241,43 +198,27 @@ export default function Note(props: NoteProps) {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="body" onClick={(e) => goToEvent(e, ev.Id)}>
|
||||
<div className="body" onClick={e => goToEvent(e, ev.Id)}>
|
||||
{transformBody()}
|
||||
{translation()}
|
||||
</div>
|
||||
{extendable && !showMore && (
|
||||
<span
|
||||
className="expand-note mt10 flex f-center"
|
||||
onClick={() => setShowMore(true)}
|
||||
>
|
||||
<span className="expand-note mt10 flex f-center" onClick={() => setShowMore(true)}>
|
||||
<FormattedMessage {...messages.ShowMore} />
|
||||
</span>
|
||||
)}
|
||||
{options.showFooter && (
|
||||
<NoteFooter
|
||||
ev={ev}
|
||||
related={related}
|
||||
onTranslated={(t) => setTranslated(t)}
|
||||
/>
|
||||
)}
|
||||
{options.showFooter && <NoteFooter ev={ev} related={related} onTranslated={t => setTranslated(t)} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const note = (
|
||||
<div
|
||||
className={`${baseClassName}${highlight ? " active " : " "}${
|
||||
extendable && !showMore ? " note-expand" : ""
|
||||
}`}
|
||||
ref={ref}
|
||||
>
|
||||
className={`${baseClassName}${highlight ? " active " : " "}${extendable && !showMore ? " note-expand" : ""}`}
|
||||
ref={ref}>
|
||||
{content()}
|
||||
</div>
|
||||
);
|
||||
|
||||
return !ignoreModeration && isOpMuted ? (
|
||||
<HiddenNote>{note}</HiddenNote>
|
||||
) : (
|
||||
note
|
||||
);
|
||||
return !ignoreModeration && isOpMuted ? <HiddenNote>{note}</HiddenNote> : note;
|
||||
}
|
||||
|
@ -48,9 +48,7 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
|
||||
async function sendNote() {
|
||||
if (note) {
|
||||
const ev = replyTo
|
||||
? await publisher.reply(replyTo, note)
|
||||
: await publisher.note(note);
|
||||
const ev = replyTo ? await publisher.reply(replyTo, note) : await publisher.note(note);
|
||||
console.debug("Sending note: ", ev);
|
||||
publisher.broadcast(ev);
|
||||
setNote("");
|
||||
@ -68,7 +66,7 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
if (file) {
|
||||
const rx = await uploader.upload(file, file.name);
|
||||
if (rx.url) {
|
||||
setNote((n) => `${n ? `${n}\n` : ""}${rx.url}`);
|
||||
setNote(n => `${n ? `${n}\n` : ""}${rx.url}`);
|
||||
} else if (rx?.error) {
|
||||
setError(rx.error);
|
||||
}
|
||||
@ -125,11 +123,7 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
<FormattedMessage {...messages.Cancel} />
|
||||
</button>
|
||||
<button type="button" onClick={onSubmit}>
|
||||
{replyTo ? (
|
||||
<FormattedMessage {...messages.Reply} />
|
||||
) : (
|
||||
<FormattedMessage {...messages.Send} />
|
||||
)}
|
||||
{replyTo ? <FormattedMessage {...messages.Reply} /> : <FormattedMessage {...messages.Send} />}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -21,13 +21,7 @@ import Zap from "Icons/Zap";
|
||||
import Reply from "Icons/Reply";
|
||||
import { formatShort } from "Number";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import {
|
||||
getReactions,
|
||||
dedupeByPubkey,
|
||||
hexToBech32,
|
||||
normalizeReaction,
|
||||
Reaction,
|
||||
} from "Util";
|
||||
import { getReactions, dedupeByPubkey, hexToBech32, normalizeReaction, Reaction } from "Util";
|
||||
import { NoteCreator } from "Element/NoteCreator";
|
||||
import Reactions from "Element/Reactions";
|
||||
import SendSats from "Element/SendSats";
|
||||
@ -58,13 +52,9 @@ export interface NoteFooterProps {
|
||||
export default function NoteFooter(props: NoteFooterProps) {
|
||||
const { related, ev } = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const login = useSelector<RootState, HexKey | undefined>(
|
||||
(s) => s.login.publicKey
|
||||
);
|
||||
const login = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const { mute, block } = useModeration();
|
||||
const prefs = useSelector<RootState, UserPreferences>(
|
||||
(s) => s.login.preferences
|
||||
);
|
||||
const prefs = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||
const author = useUserProfile(ev.RootPubKey);
|
||||
const publisher = useEventPublisher();
|
||||
const [reply, setReply] = useState(false);
|
||||
@ -75,29 +65,23 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
const langNames = new Intl.DisplayNames([...window.navigator.languages], {
|
||||
type: "language",
|
||||
});
|
||||
const reactions = useMemo(
|
||||
() => getReactions(related, ev.Id, EventKind.Reaction),
|
||||
[related, ev]
|
||||
);
|
||||
const reposts = useMemo(
|
||||
() => dedupeByPubkey(getReactions(related, ev.Id, EventKind.Repost)),
|
||||
[related, ev]
|
||||
);
|
||||
const reactions = useMemo(() => getReactions(related, ev.Id, EventKind.Reaction), [related, ev]);
|
||||
const reposts = useMemo(() => dedupeByPubkey(getReactions(related, ev.Id, EventKind.Repost)), [related, ev]);
|
||||
const zaps = useMemo(() => {
|
||||
const sortedZaps = getReactions(related, ev.Id, EventKind.ZapReceipt)
|
||||
.map(parseZap)
|
||||
.filter((z) => z.valid && z.zapper !== ev.PubKey);
|
||||
.filter(z => z.valid && z.zapper !== ev.PubKey);
|
||||
sortedZaps.sort((a, b) => b.amount - a.amount);
|
||||
return sortedZaps;
|
||||
}, [related]);
|
||||
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
||||
const didZap = zaps.some((a) => a.zapper === login);
|
||||
const didZap = zaps.some(a => a.zapper === login);
|
||||
const groupReactions = useMemo(() => {
|
||||
const result = reactions?.reduce(
|
||||
(acc, reaction) => {
|
||||
const kind = normalizeReaction(reaction.content);
|
||||
const rs = acc[kind] || [];
|
||||
if (rs.map((e) => e.pubkey).includes(reaction.pubkey)) {
|
||||
if (rs.map(e => e.pubkey).includes(reaction.pubkey)) {
|
||||
return acc;
|
||||
}
|
||||
return { ...acc, [kind]: [...rs, reaction] };
|
||||
@ -116,14 +100,11 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
const negative = groupReactions[Reaction.Negative];
|
||||
|
||||
function hasReacted(emoji: string) {
|
||||
return reactions?.some(
|
||||
({ pubkey, content }) =>
|
||||
normalizeReaction(content) === emoji && pubkey === login
|
||||
);
|
||||
return reactions?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === login);
|
||||
}
|
||||
|
||||
function hasReposted() {
|
||||
return reposts.some((a) => a.pubkey === login);
|
||||
return reposts.some(a => a.pubkey === login);
|
||||
}
|
||||
|
||||
async function react(content: string) {
|
||||
@ -134,11 +115,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
}
|
||||
|
||||
async function deleteEvent() {
|
||||
if (
|
||||
window.confirm(
|
||||
formatMessage(messages.ConfirmDeletion, { id: ev.Id.substring(0, 8) })
|
||||
)
|
||||
) {
|
||||
if (window.confirm(formatMessage(messages.ConfirmDeletion, { id: ev.Id.substring(0, 8) }))) {
|
||||
const evDelete = await publisher.delete(ev.Id);
|
||||
publisher.broadcast(evDelete);
|
||||
}
|
||||
@ -146,10 +123,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
|
||||
async function repost() {
|
||||
if (!hasReposted()) {
|
||||
if (
|
||||
!prefs.confirmReposts ||
|
||||
window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id }))
|
||||
) {
|
||||
if (!prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id }))) {
|
||||
const evRepost = await publisher.repost(ev);
|
||||
publisher.broadcast(evRepost);
|
||||
}
|
||||
@ -161,18 +135,11 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
if (service) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`reaction-pill ${didZap ? "reacted" : ""}`}
|
||||
onClick={() => setTip(true)}
|
||||
>
|
||||
<div className={`reaction-pill ${didZap ? "reacted" : ""}`} onClick={() => setTip(true)}>
|
||||
<div className="reaction-pill-icon">
|
||||
<Zap />
|
||||
</div>
|
||||
{zapTotal > 0 && (
|
||||
<div className="reaction-pill-number">
|
||||
{formatShort(zapTotal)}
|
||||
</div>
|
||||
)}
|
||||
{zapTotal > 0 && <div className="reaction-pill-number">{formatShort(zapTotal)}</div>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@ -182,18 +149,11 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
|
||||
function repostIcon() {
|
||||
return (
|
||||
<div
|
||||
className={`reaction-pill ${hasReposted() ? "reacted" : ""}`}
|
||||
onClick={() => repost()}
|
||||
>
|
||||
<div className={`reaction-pill ${hasReposted() ? "reacted" : ""}`} onClick={() => repost()}>
|
||||
<div className="reaction-pill-icon">
|
||||
<FontAwesomeIcon icon={faRepeat} />
|
||||
</div>
|
||||
{reposts.length > 0 && (
|
||||
<div className="reaction-pill-number">
|
||||
{formatShort(reposts.length)}
|
||||
</div>
|
||||
)}
|
||||
{reposts.length > 0 && <div className="reaction-pill-number">{formatShort(reposts.length)}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -204,16 +164,11 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`reaction-pill ${hasReacted("+") ? "reacted" : ""} `}
|
||||
onClick={() => react("+")}
|
||||
>
|
||||
<div className={`reaction-pill ${hasReacted("+") ? "reacted" : ""} `} onClick={() => react("+")}>
|
||||
<div className="reaction-pill-icon">
|
||||
<Heart />
|
||||
</div>
|
||||
<div className="reaction-pill-number">
|
||||
{formatShort(positive.length)}
|
||||
</div>
|
||||
<div className="reaction-pill-number">{formatShort(positive.length)}</div>
|
||||
</div>
|
||||
{repostIcon()}
|
||||
</>
|
||||
@ -221,9 +176,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
}
|
||||
|
||||
async function share() {
|
||||
const url = `${window.location.protocol}//${
|
||||
window.location.host
|
||||
}/e/${hexToBech32("note", ev.Id)}`;
|
||||
const url = `${window.location.protocol}//${window.location.host}/e/${hexToBech32("note", ev.Id)}`;
|
||||
if ("share" in window.navigator) {
|
||||
await window.navigator.share({
|
||||
title: "Snort",
|
||||
@ -262,9 +215,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
}
|
||||
|
||||
async function copyEvent() {
|
||||
await navigator.clipboard.writeText(
|
||||
JSON.stringify(ev.Original, undefined, " ")
|
||||
);
|
||||
await navigator.clipboard.writeText(JSON.stringify(ev.Original, undefined, " "));
|
||||
}
|
||||
|
||||
function menuItems() {
|
||||
@ -291,10 +242,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
{prefs.enableReactions && (
|
||||
<MenuItem onClick={() => react("-")}>
|
||||
<Dislike />
|
||||
<FormattedMessage
|
||||
{...messages.Dislike}
|
||||
values={{ n: negative.length }}
|
||||
/>
|
||||
<FormattedMessage {...messages.Dislike} values={{ n: negative.length }} />
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={() => block(ev.PubKey)}>
|
||||
@ -303,10 +251,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => translate()}>
|
||||
<FontAwesomeIcon icon={faLanguage} />
|
||||
<FormattedMessage
|
||||
{...messages.TranslateTo}
|
||||
values={{ lang: langNames.of(lang.split("-")[0]) }}
|
||||
/>
|
||||
<FormattedMessage {...messages.TranslateTo} values={{ lang: langNames.of(lang.split("-")[0]) }} />
|
||||
</MenuItem>
|
||||
{prefs.showDebugMenus && (
|
||||
<MenuItem onClick={() => copyEvent()}>
|
||||
@ -330,10 +275,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
<div className="footer-reactions">
|
||||
{tipButton()}
|
||||
{reactionIcons()}
|
||||
<div
|
||||
className={`reaction-pill ${reply ? "reacted" : ""}`}
|
||||
onClick={() => setReply((s) => !s)}
|
||||
>
|
||||
<div className={`reaction-pill ${reply ? "reacted" : ""}`} onClick={() => setReply(s => !s)}>
|
||||
<div className="reaction-pill-icon">
|
||||
<Reply />
|
||||
</div>
|
||||
@ -346,18 +288,11 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
menuClassName="ctx-menu"
|
||||
>
|
||||
menuClassName="ctx-menu">
|
||||
{menuItems()}
|
||||
</Menu>
|
||||
</div>
|
||||
<NoteCreator
|
||||
autoFocus={true}
|
||||
replyTo={ev}
|
||||
onSend={() => setReply(false)}
|
||||
show={reply}
|
||||
setShow={setReply}
|
||||
/>
|
||||
<NoteCreator autoFocus={true} replyTo={ev} onSend={() => setReply(false)} show={reply} setShow={setReply} />
|
||||
<Reactions
|
||||
show={showReactions}
|
||||
setShow={setShowReactions}
|
||||
|
@ -23,7 +23,7 @@ export default function NoteReaction(props: NoteReactionProps) {
|
||||
|
||||
const refEvent = useMemo(() => {
|
||||
if (ev) {
|
||||
const eTags = ev.Tags.filter((a) => a.Key === "e");
|
||||
const eTags = ev.Tags.filter(a => a.Key === "e");
|
||||
if (eTags.length > 0) {
|
||||
return eTags[0].Event;
|
||||
}
|
||||
@ -39,11 +39,7 @@ export default function NoteReaction(props: NoteReactionProps) {
|
||||
* Some clients embed the reposted note in the content
|
||||
*/
|
||||
function extractRoot() {
|
||||
if (
|
||||
ev?.Kind === EventKind.Repost &&
|
||||
ev.Content.length > 0 &&
|
||||
ev.Content !== "#[0]"
|
||||
) {
|
||||
if (ev?.Kind === EventKind.Repost && ev.Content.length > 0 && ev.Content !== "#[0]") {
|
||||
try {
|
||||
const r: RawEvent = JSON.parse(ev.Content);
|
||||
return r as TaggedRawEvent;
|
||||
@ -73,9 +69,7 @@ export default function NoteReaction(props: NoteReactionProps) {
|
||||
{root ? <Note data={root} options={opt} related={[]} /> : null}
|
||||
{!root && refEvent ? (
|
||||
<p>
|
||||
<Link to={eventLink(refEvent)}>
|
||||
#{hexToBech32("note", refEvent).substring(0, 12)}
|
||||
</Link>
|
||||
<Link to={eventLink(refEvent)}>#{hexToBech32("note", refEvent).substring(0, 12)}</Link>
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -31,10 +31,7 @@ export default function NoteTime(props: NoteTimeProps) {
|
||||
weekday: "short",
|
||||
});
|
||||
} else if (absAgo > HourInMs) {
|
||||
return `${fromDate.getHours().toString().padStart(2, "0")}:${fromDate
|
||||
.getMinutes()
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
return `${fromDate.getHours().toString().padStart(2, "0")}:${fromDate.getMinutes().toString().padStart(2, "0")}`;
|
||||
} else if (absAgo < MinuteInMs) {
|
||||
return fallback;
|
||||
} else {
|
||||
@ -49,7 +46,7 @@ export default function NoteTime(props: NoteTimeProps) {
|
||||
useEffect(() => {
|
||||
setTime(calcTime());
|
||||
const t = setInterval(() => {
|
||||
setTime((s) => {
|
||||
setTime(s => {
|
||||
const newTime = calcTime();
|
||||
if (newTime !== s) {
|
||||
return newTime;
|
||||
|
@ -20,19 +20,13 @@ function NoteLabel({ pubkey }: NoteToSelfProps) {
|
||||
const user = useUserProfile(pubkey);
|
||||
return (
|
||||
<div>
|
||||
<FormattedMessage {...messages.NoteToSelf} />{" "}
|
||||
<FontAwesomeIcon icon={faCertificate} size="xs" />
|
||||
<FormattedMessage {...messages.NoteToSelf} /> <FontAwesomeIcon icon={faCertificate} size="xs" />
|
||||
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NoteToSelf({
|
||||
pubkey,
|
||||
clickable,
|
||||
className,
|
||||
link,
|
||||
}: NoteToSelfProps) {
|
||||
export default function NoteToSelf({ pubkey, clickable, className, link }: NoteToSelfProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const clickLink = () => {
|
||||
@ -45,12 +39,7 @@ export default function NoteToSelf({
|
||||
<div className={`nts${className ? ` ${className}` : ""}`}>
|
||||
<div className="avatar-wrapper">
|
||||
<div className={`avatar${clickable ? " clickable" : ""}`}>
|
||||
<FontAwesomeIcon
|
||||
onClick={clickLink}
|
||||
className="note-to-self"
|
||||
icon={faBook}
|
||||
size="2xl"
|
||||
/>
|
||||
<FontAwesomeIcon onClick={clickLink} className="note-to-self" icon={faBook} size="2xl" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="f-grow">
|
||||
|
@ -17,13 +17,7 @@ export interface ProfileImageProps {
|
||||
link?: string;
|
||||
}
|
||||
|
||||
export default function ProfileImage({
|
||||
pubkey,
|
||||
subHeader,
|
||||
showUsername = true,
|
||||
className,
|
||||
link,
|
||||
}: ProfileImageProps) {
|
||||
export default function ProfileImage({ pubkey, subHeader, showUsername = true, className, link }: ProfileImageProps) {
|
||||
const navigate = useNavigate();
|
||||
const user = useUserProfile(pubkey);
|
||||
|
||||
@ -34,19 +28,12 @@ export default function ProfileImage({
|
||||
return (
|
||||
<div className={`pfp${className ? ` ${className}` : ""}`}>
|
||||
<div className="avatar-wrapper">
|
||||
<Avatar
|
||||
user={user}
|
||||
onClick={() => navigate(link ?? profileLink(pubkey))}
|
||||
/>
|
||||
<Avatar user={user} onClick={() => navigate(link ?? profileLink(pubkey))} />
|
||||
</div>
|
||||
{showUsername && (
|
||||
<div className="profile-name f-grow">
|
||||
<div className="username">
|
||||
<Link
|
||||
className="display-name"
|
||||
key={pubkey}
|
||||
to={link ?? profileLink(pubkey)}
|
||||
>
|
||||
<Link className="display-name" key={pubkey} to={link ?? profileLink(pubkey)}>
|
||||
{name}
|
||||
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
|
||||
</Link>
|
||||
@ -58,10 +45,7 @@ export default function ProfileImage({
|
||||
);
|
||||
}
|
||||
|
||||
export function getDisplayName(
|
||||
user: MetadataCache | undefined,
|
||||
pubkey: HexKey
|
||||
) {
|
||||
export function getDisplayName(user: MetadataCache | undefined, pubkey: HexKey) {
|
||||
let name = hexToBech32("npub", pubkey).substring(0, 12);
|
||||
if (user?.display_name !== undefined && user.display_name.length > 0) {
|
||||
name = user.display_name;
|
||||
|
@ -25,21 +25,12 @@ export default function ProfilePreview(props: ProfilePreviewProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`profile-preview${
|
||||
props.className ? ` ${props.className}` : ""
|
||||
}`}
|
||||
ref={ref}
|
||||
>
|
||||
<div className={`profile-preview${props.className ? ` ${props.className}` : ""}`} ref={ref}>
|
||||
{inView && (
|
||||
<>
|
||||
<ProfileImage
|
||||
pubkey={pubkey}
|
||||
subHeader={
|
||||
options.about ? (
|
||||
<div className="f-ellipsis about">{user?.about}</div>
|
||||
) : undefined
|
||||
}
|
||||
subHeader={options.about ? <div className="f-ellipsis about">{user?.about}</div> : undefined}
|
||||
/>
|
||||
{props.actions ?? (
|
||||
<div className="follow-button-container">
|
||||
|
@ -1,11 +1,7 @@
|
||||
import useImgProxy from "Feed/ImgProxy";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface ProxyImgProps
|
||||
extends React.DetailedHTMLProps<
|
||||
React.ImgHTMLAttributes<HTMLImageElement>,
|
||||
HTMLImageElement
|
||||
> {
|
||||
interface ProxyImgProps extends React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement> {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
@ -17,7 +13,7 @@ export const ProxyImg = (props: ProxyImgProps) => {
|
||||
useEffect(() => {
|
||||
if (src) {
|
||||
proxy(src, size)
|
||||
.then((a) => setUrl(a))
|
||||
.then(a => setUrl(a))
|
||||
.catch(console.warn);
|
||||
}
|
||||
}, [src]);
|
||||
|
@ -28,14 +28,7 @@ interface ReactionsProps {
|
||||
zaps: ParsedZap[];
|
||||
}
|
||||
|
||||
const Reactions = ({
|
||||
show,
|
||||
setShow,
|
||||
positive,
|
||||
negative,
|
||||
reposts,
|
||||
zaps,
|
||||
}: ReactionsProps) => {
|
||||
const Reactions = ({ show, setShow, positive, negative, reposts, zaps }: ReactionsProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const onClose = () => setShow(false);
|
||||
const likes = useMemo(() => {
|
||||
@ -48,8 +41,7 @@ const Reactions = ({
|
||||
sorted.sort((a, b) => b.created_at - a.created_at);
|
||||
return sorted;
|
||||
}, [negative]);
|
||||
const total =
|
||||
positive.length + negative.length + zaps.length + reposts.length;
|
||||
const total = positive.length + negative.length + zaps.length + reposts.length;
|
||||
const defaultTabs: Tab[] = [
|
||||
{
|
||||
text: formatMessage(messages.Likes, { n: likes.length }),
|
||||
@ -93,24 +85,17 @@ const Reactions = ({
|
||||
</div>
|
||||
<div className="reactions-header">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
{...messages.ReactionsCount}
|
||||
values={{ n: total }}
|
||||
/>
|
||||
<FormattedMessage {...messages.ReactionsCount} values={{ n: total }} />
|
||||
</h2>
|
||||
</div>
|
||||
<Tabs tabs={tabs} tab={tab} setTab={setTab} />
|
||||
<div className="body" key={tab.value}>
|
||||
{tab.value === 0 &&
|
||||
likes.map((ev) => {
|
||||
likes.map(ev => {
|
||||
return (
|
||||
<div key={ev.id} className="reactions-item">
|
||||
<div className="reaction-icon">
|
||||
{ev.content === "+" ? (
|
||||
<Heart width={20} height={18} />
|
||||
) : (
|
||||
ev.content
|
||||
)}
|
||||
{ev.content === "+" ? <Heart width={20} height={18} /> : ev.content}
|
||||
</div>
|
||||
<ProfileImage pubkey={ev.pubkey} />
|
||||
<FollowButton pubkey={ev.pubkey} />
|
||||
@ -118,23 +103,22 @@ const Reactions = ({
|
||||
);
|
||||
})}
|
||||
{tab.value === 1 &&
|
||||
zaps.map((z) => {
|
||||
zaps.map(z => {
|
||||
return (
|
||||
<div key={z.id} className="reactions-item">
|
||||
<div className="zap-reaction-icon">
|
||||
<ZapIcon width={17} height={20} />
|
||||
<span className="zap-amount">{formatShort(z.amount)}</span>
|
||||
z.zapper && (
|
||||
<div key={z.id} className="reactions-item">
|
||||
<div className="zap-reaction-icon">
|
||||
<ZapIcon width={17} height={20} />
|
||||
<span className="zap-amount">{formatShort(z.amount)}</span>
|
||||
</div>
|
||||
<ProfileImage pubkey={z.zapper} subHeader={<>{z.content}</>} />
|
||||
<FollowButton pubkey={z.zapper} />
|
||||
</div>
|
||||
<ProfileImage
|
||||
pubkey={z.zapper ?? ""}
|
||||
subHeader={<>{z.content}</>}
|
||||
/>
|
||||
<FollowButton pubkey={z.zapper ?? ""} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
{tab.value === 2 &&
|
||||
reposts.map((ev) => {
|
||||
reposts.map(ev => {
|
||||
return (
|
||||
<div key={ev.id} className="reactions-item">
|
||||
<div className="reaction-icon">
|
||||
@ -146,7 +130,7 @@ const Reactions = ({
|
||||
);
|
||||
})}
|
||||
{tab.value === 3 &&
|
||||
dislikes.map((ev) => {
|
||||
dislikes.map(ev => {
|
||||
return (
|
||||
<div key={ev.id} className="reactions-item">
|
||||
<div className="reaction-icon">
|
||||
|
@ -27,10 +27,7 @@ export default function Relay(props: RelayProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { formatMessage } = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const allRelaySettings = useSelector<
|
||||
RootState,
|
||||
Record<string, RelaySettings>
|
||||
>((s) => s.login.relays);
|
||||
const allRelaySettings = useSelector<RootState, Record<string, RelaySettings>>(s => s.login.relays);
|
||||
const relaySettings = allRelaySettings[props.addr];
|
||||
const state = useRelayState(props.addr);
|
||||
const name = useMemo(() => new URL(props.addr).host, [props.addr]);
|
||||
@ -66,11 +63,8 @@ export default function Relay(props: RelayProps) {
|
||||
write: !relaySettings.write,
|
||||
read: relaySettings.read,
|
||||
})
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={relaySettings.write ? faSquareCheck : faSquareXmark}
|
||||
/>
|
||||
}>
|
||||
<FontAwesomeIcon icon={relaySettings.write ? faSquareCheck : faSquareXmark} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="f-1">
|
||||
@ -82,11 +76,8 @@ export default function Relay(props: RelayProps) {
|
||||
write: relaySettings.write,
|
||||
read: !relaySettings.read,
|
||||
})
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={relaySettings.read ? faSquareCheck : faSquareXmark}
|
||||
/>
|
||||
}>
|
||||
<FontAwesomeIcon icon={relaySettings.read ? faSquareCheck : faSquareXmark} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -104,10 +95,7 @@ export default function Relay(props: RelayProps) {
|
||||
<FontAwesomeIcon icon={faPlugCircleXmark} /> {state?.disconnects}
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
className="icon-btn"
|
||||
onClick={() => navigate(state?.id ?? "")}
|
||||
>
|
||||
<span className="icon-btn" onClick={() => navigate(state?.id ?? "")}>
|
||||
<FontAwesomeIcon icon={faGear} />
|
||||
</span>
|
||||
</div>
|
||||
|
@ -54,9 +54,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
const service = props.svc;
|
||||
const show = props.show || false;
|
||||
const { note, author, target } = props;
|
||||
const amounts = [
|
||||
500, 1_000, 5_000, 10_000, 20_000, 50_000, 100_000, 1_000_000,
|
||||
];
|
||||
const amounts = [500, 1_000, 5_000, 10_000, 20_000, 50_000, 100_000, 1_000_000];
|
||||
const emojis: Record<number, string> = {
|
||||
1_000: "👍",
|
||||
5_000: "💜",
|
||||
@ -77,13 +75,12 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const publisher = useEventPublisher();
|
||||
const horizontalScroll = useHorizontalScroll();
|
||||
const canComment =
|
||||
(payService?.commentAllowed ?? 0) > 0 || payService?.nostrPubkey;
|
||||
const canComment = (payService?.commentAllowed ?? 0) > 0 || payService?.nostrPubkey;
|
||||
|
||||
useEffect(() => {
|
||||
if (show && !props.invoice) {
|
||||
loadService()
|
||||
.then((a) => setPayService(a ?? undefined))
|
||||
.then(a => setPayService(a ?? undefined))
|
||||
.catch(() => setError(formatMessage(messages.LNURLFail)));
|
||||
} else {
|
||||
setPayService(undefined);
|
||||
@ -99,13 +96,11 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
if (payService) {
|
||||
const min = (payService.minSendable ?? 0) / 1000;
|
||||
const max = (payService.maxSendable ?? 0) / 1000;
|
||||
return amounts.filter((a) => a >= min && a <= max);
|
||||
return amounts.filter(a => a >= min && a <= max);
|
||||
}
|
||||
return [];
|
||||
}, [payService]);
|
||||
|
||||
// TODO Why was this never used? I think this might be a bug, or was it just an oversight?
|
||||
|
||||
const selectAmount = (a: number) => {
|
||||
setError(undefined);
|
||||
setInvoice(undefined);
|
||||
@ -141,14 +136,10 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
if (!amount || !payService) return null;
|
||||
let url = "";
|
||||
const amountParam = `amount=${Math.floor(amount * 1000)}`;
|
||||
const commentParam =
|
||||
comment && payService?.commentAllowed
|
||||
? `&comment=${encodeURIComponent(comment)}`
|
||||
: "";
|
||||
const commentParam = comment && payService?.commentAllowed ? `&comment=${encodeURIComponent(comment)}` : "";
|
||||
if (payService.nostrPubkey && author) {
|
||||
const ev = await publisher.zap(author, note, comment);
|
||||
const nostrParam =
|
||||
ev && `&nostr=${encodeURIComponent(JSON.stringify(ev.ToObject()))}`;
|
||||
const nostrParam = ev && `&nostr=${encodeURIComponent(JSON.stringify(ev.ToObject()))}`;
|
||||
url = `${payService.callback}?${amountParam}${commentParam}${nostrParam}`;
|
||||
} else {
|
||||
url = `${payService.callback}?${amountParam}${commentParam}`;
|
||||
@ -185,14 +176,13 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
className="f-grow mr10"
|
||||
placeholder={formatMessage(messages.Custom)}
|
||||
value={customAmount}
|
||||
onChange={(e) => setCustomAmount(parseInt(e.target.value))}
|
||||
onChange={e => setCustomAmount(parseInt(e.target.value))}
|
||||
/>
|
||||
<button
|
||||
className="secondary"
|
||||
type="button"
|
||||
disabled={!customAmount}
|
||||
onClick={() => selectAmount(customAmount ?? 0)}
|
||||
>
|
||||
onClick={() => selectAmount(customAmount ?? 0)}>
|
||||
<FormattedMessage {...messages.Confirm} />
|
||||
</button>
|
||||
</div>
|
||||
@ -222,12 +212,8 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
<FormattedMessage {...messages.ZapAmount} />
|
||||
</h3>
|
||||
<div className="amounts" ref={horizontalScroll}>
|
||||
{serviceAmounts.map((a) => (
|
||||
<span
|
||||
className={`sat-amount ${amount === a ? "active" : ""}`}
|
||||
key={a}
|
||||
onClick={() => selectAmount(a)}
|
||||
>
|
||||
{serviceAmounts.map(a => (
|
||||
<span className={`sat-amount ${amount === a ? "active" : ""}`} key={a} onClick={() => selectAmount(a)}>
|
||||
{emojis[a] && <>{emojis[a]} </>}
|
||||
{formatShort(a)}
|
||||
</span>
|
||||
@ -241,28 +227,18 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
placeholder={formatMessage(messages.Comment)}
|
||||
className="f-grow"
|
||||
maxLength={payService?.commentAllowed || 120}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
onChange={e => setComment(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{(amount ?? 0) > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="zap-action"
|
||||
onClick={() => loadInvoice()}
|
||||
>
|
||||
<button type="button" className="zap-action" onClick={() => loadInvoice()}>
|
||||
<div className="zap-action-container">
|
||||
<Zap />
|
||||
{target ? (
|
||||
<FormattedMessage
|
||||
{...messages.ZapTarget}
|
||||
values={{ target, n: formatShort(amount) }}
|
||||
/>
|
||||
<FormattedMessage {...messages.ZapTarget} values={{ target, n: formatShort(amount) }} />
|
||||
) : (
|
||||
<FormattedMessage
|
||||
{...messages.ZapSats}
|
||||
values={{ n: formatShort(amount) }}
|
||||
/>
|
||||
<FormattedMessage {...messages.ZapSats} values={{ n: formatShort(amount) }} />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
@ -285,11 +261,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
<div className="copy-action">
|
||||
<Copy text={pr} maxSize={26} />
|
||||
</div>
|
||||
<button
|
||||
className="wallet-action"
|
||||
type="button"
|
||||
onClick={() => window.open(`lightning:${pr}`)}
|
||||
>
|
||||
<button className="wallet-action" type="button" onClick={() => window.open(`lightning:${pr}`)}>
|
||||
<FormattedMessage {...messages.OpenWallet} />
|
||||
</button>
|
||||
</>
|
||||
@ -319,9 +291,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const defaultTitle = payService?.nostrPubkey
|
||||
? formatMessage(messages.SendZap)
|
||||
: formatMessage(messages.SendSats);
|
||||
const defaultTitle = payService?.nostrPubkey ? formatMessage(messages.SendZap) : formatMessage(messages.SendSats);
|
||||
const title = target
|
||||
? formatMessage(messages.ToTarget, {
|
||||
action: defaultTitle,
|
||||
@ -331,7 +301,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
if (!show) return null;
|
||||
return (
|
||||
<Modal className="lnurl-modal" onClose={onClose}>
|
||||
<div className="lnurl-tip" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="lnurl-tip" onClick={e => e.stopPropagation()}>
|
||||
<div className="close" onClick={onClose}>
|
||||
<Close />
|
||||
</div>
|
||||
|
@ -37,12 +37,6 @@
|
||||
}
|
||||
|
||||
.skeleton::after {
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
#50535a 0%,
|
||||
#656871 20%,
|
||||
#50535a 40%,
|
||||
#50535a 100%
|
||||
);
|
||||
background-image: linear-gradient(90deg, #50535a 0%, #656871 20%, #50535a 40%, #50535a 100%);
|
||||
}
|
||||
}
|
||||
|
@ -8,22 +8,13 @@ interface ISkepetonProps {
|
||||
margin?: string;
|
||||
}
|
||||
|
||||
export default function Skeleton({
|
||||
children,
|
||||
width,
|
||||
height,
|
||||
margin,
|
||||
loading = true,
|
||||
}: ISkepetonProps) {
|
||||
export default function Skeleton({ children, width, height, margin, loading = true }: ISkepetonProps) {
|
||||
if (!loading) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="skeleton"
|
||||
style={{ width: width, height: height, margin: margin }}
|
||||
>
|
||||
<div className="skeleton" style={{ width: width, height: height, margin: margin }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
@ -5,8 +5,7 @@ const SoundCloudEmbed = ({ link }: { link: string }) => {
|
||||
height="166"
|
||||
scrolling="no"
|
||||
allow="autoplay"
|
||||
src={`https://w.soundcloud.com/player/?url=${link}`}
|
||||
></iframe>
|
||||
src={`https://w.soundcloud.com/player/?url=${link}`}></iframe>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,8 +1,5 @@
|
||||
const SpotifyEmbed = ({ link }: { link: string }) => {
|
||||
const convertedUrl = link.replace(
|
||||
/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/,
|
||||
"/embed/$1/$2"
|
||||
);
|
||||
const convertedUrl = link.replace(/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/, "/embed/$1/$2");
|
||||
|
||||
return (
|
||||
<iframe
|
||||
@ -12,8 +9,7 @@ const SpotifyEmbed = ({ link }: { link: string }) => {
|
||||
height="352"
|
||||
frameBorder="0"
|
||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||
loading="lazy"
|
||||
></iframe>
|
||||
loading="lazy"></iframe>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -20,11 +20,8 @@ interface TabElementProps extends Omit<TabsProps, "tabs"> {
|
||||
export const TabElement = ({ t, tab, setTab }: TabElementProps) => {
|
||||
return (
|
||||
<div
|
||||
className={`tab ${tab.value === t.value ? "active" : ""} ${
|
||||
t.disabled ? "disabled" : ""
|
||||
}`}
|
||||
onClick={() => !t.disabled && setTab(t)}
|
||||
>
|
||||
className={`tab ${tab.value === t.value ? "active" : ""} ${t.disabled ? "disabled" : ""}`}
|
||||
onClick={() => !t.disabled && setTab(t)}>
|
||||
{t.text}
|
||||
</div>
|
||||
);
|
||||
@ -33,7 +30,7 @@ export const TabElement = ({ t, tab, setTab }: TabElementProps) => {
|
||||
const Tabs = ({ tabs, tab, setTab }: TabsProps) => {
|
||||
return (
|
||||
<div className="tabs">
|
||||
{tabs.map((t) => (
|
||||
{tabs.map(t => (
|
||||
<TabElement tab={tab} setTab={setTab} t={t} />
|
||||
))}
|
||||
</div>
|
||||
|
@ -34,9 +34,9 @@ export interface TextProps {
|
||||
export default function Text({ content, tags, creator, users }: TextProps) {
|
||||
function extractLinks(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map((f) => {
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(UrlRegex).map((a) => {
|
||||
return f.split(UrlRegex).map(a => {
|
||||
if (a.startsWith("http")) {
|
||||
return <HyperText link={a} creator={creator} />;
|
||||
}
|
||||
@ -50,29 +50,22 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
||||
|
||||
function extractMentions(frag: TextFragment) {
|
||||
return frag.body
|
||||
.map((f) => {
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(MentionRegex).map((match) => {
|
||||
return f.split(MentionRegex).map(match => {
|
||||
const matchTag = match.match(/#\[(\d+)\]/);
|
||||
if (matchTag && matchTag.length === 2) {
|
||||
const idx = parseInt(matchTag[1]);
|
||||
const ref = frag.tags?.find((a) => a.Index === idx);
|
||||
const ref = frag.tags?.find(a => a.Index === idx);
|
||||
if (ref) {
|
||||
switch (ref.Key) {
|
||||
case "p": {
|
||||
return <Mention pubkey={ref.PubKey ?? ""} />;
|
||||
}
|
||||
case "e": {
|
||||
const eText = hexToBech32(
|
||||
"note",
|
||||
ref.Event ?? ""
|
||||
).substring(0, 12);
|
||||
const eText = hexToBech32("note", ref.Event).substring(0, 12);
|
||||
return (
|
||||
<Link
|
||||
key={ref.Event}
|
||||
to={eventLink(ref.Event ?? "")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Link key={ref.Event} to={eventLink(ref.Event ?? "")} onClick={e => e.stopPropagation()}>
|
||||
#{eText}
|
||||
</Link>
|
||||
);
|
||||
@ -95,9 +88,9 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
||||
|
||||
function extractInvoices(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map((f) => {
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(InvoiceRegex).map((i) => {
|
||||
return f.split(InvoiceRegex).map(i => {
|
||||
if (i.toLowerCase().startsWith("lnbc")) {
|
||||
return <Invoice key={i} invoice={i} />;
|
||||
} else {
|
||||
@ -112,9 +105,9 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
||||
|
||||
function extractHashtags(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map((f) => {
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(HashtagRegex).map((i) => {
|
||||
return f.split(HashtagRegex).map(i => {
|
||||
if (i.toLowerCase().startsWith("#")) {
|
||||
return <Hashtag tag={i.substring(1)} />;
|
||||
} else {
|
||||
@ -134,7 +127,7 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
||||
|
||||
function transformParagraph(frag: TextFragment) {
|
||||
const fragments = transformText(frag);
|
||||
if (fragments.every((f) => typeof f === "string")) {
|
||||
if (fragments.every(f => typeof f === "string")) {
|
||||
return <p>{fragments}</p>;
|
||||
}
|
||||
return <>{fragments}</>;
|
||||
@ -150,13 +143,9 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
||||
|
||||
const components = useMemo(() => {
|
||||
return {
|
||||
p: (x: { children?: React.ReactNode[] }) =>
|
||||
transformParagraph({ body: x.children ?? [], tags, users }),
|
||||
a: (x: { href?: string }) => (
|
||||
<HyperText link={x.href ?? ""} creator={creator} />
|
||||
),
|
||||
li: (x: { children?: Fragment[] }) =>
|
||||
transformLi({ body: x.children ?? [], tags, users }),
|
||||
p: (x: { children?: React.ReactNode[] }) => transformParagraph({ body: x.children ?? [], tags, users }),
|
||||
a: (x: { href?: string }) => <HyperText link={x.href ?? ""} creator={creator} />,
|
||||
li: (x: { children?: Fragment[] }) => transformLi({ body: x.children ?? [], tags, users }),
|
||||
};
|
||||
}, [content]);
|
||||
|
||||
@ -178,9 +167,7 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
||||
) {
|
||||
node.type = "text";
|
||||
const position = unwrap(node.position);
|
||||
node.value = content
|
||||
.slice(position.start.offset, position.end.offset)
|
||||
.replace(/\)$/, " )");
|
||||
node.value = content.slice(position.start.offset, position.end.offset).replace(/\)$/, " )");
|
||||
return SKIP;
|
||||
}
|
||||
});
|
||||
@ -188,11 +175,7 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
||||
[content]
|
||||
);
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className="text"
|
||||
components={components}
|
||||
remarkPlugins={[disableMarkdownLinks]}
|
||||
>
|
||||
<ReactMarkdown className="text" components={components} remarkPlugins={[disableMarkdownLinks]}>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
|
@ -85,11 +85,8 @@ const Textarea = (props: TextareaProps) => {
|
||||
"@": {
|
||||
afterWhitespace: true,
|
||||
dataProvider: userDataProvider,
|
||||
component: (props: { entity: MetadataCache }) => (
|
||||
<UserItem {...props.entity} />
|
||||
),
|
||||
output: (item: { pubkey: string }) =>
|
||||
`@${hexToBech32("npub", item.pubkey)}`,
|
||||
component: (props: { entity: MetadataCache }) => <UserItem {...props.entity} />,
|
||||
output: (item: { pubkey: string }) => `@${hexToBech32("npub", item.pubkey)}`,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
@ -87,8 +87,7 @@
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.subthread-container.subthread-mid:not(.subthread-last)
|
||||
.line-container:after {
|
||||
.subthread-container.subthread-mid:not(.subthread-last) .line-container:after {
|
||||
left: 48px;
|
||||
}
|
||||
}
|
||||
@ -103,8 +102,7 @@
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.subthread-container.subthread-mid:not(.subthread-last)
|
||||
.line-container:after {
|
||||
.subthread-container.subthread-mid:not(.subthread-last) .line-container:after {
|
||||
left: 48px;
|
||||
}
|
||||
}
|
||||
|
@ -13,12 +13,9 @@ import NoteGhost from "Element/NoteGhost";
|
||||
import Collapsed from "Element/Collapsed";
|
||||
import messages from "./messages";
|
||||
|
||||
function getParent(
|
||||
ev: HexKey,
|
||||
chains: Map<HexKey, NEvent[]>
|
||||
): HexKey | undefined {
|
||||
function getParent(ev: HexKey, chains: Map<HexKey, NEvent[]>): HexKey | undefined {
|
||||
for (const [k, vs] of chains.entries()) {
|
||||
const fs = vs.map((a) => a.Id);
|
||||
const fs = vs.map(a => a.Id);
|
||||
if (fs.includes(ev)) {
|
||||
return k;
|
||||
}
|
||||
@ -49,30 +46,17 @@ interface SubthreadProps {
|
||||
onNavigate: (e: u256) => void;
|
||||
}
|
||||
|
||||
const Subthread = ({
|
||||
active,
|
||||
path,
|
||||
notes,
|
||||
related,
|
||||
chains,
|
||||
onNavigate,
|
||||
}: SubthreadProps) => {
|
||||
const Subthread = ({ active, path, notes, related, chains, onNavigate }: SubthreadProps) => {
|
||||
const renderSubthread = (a: NEvent, idx: number) => {
|
||||
const isLastSubthread = idx === notes.length - 1;
|
||||
const replies = getReplies(a.Id, chains);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`subthread-container ${
|
||||
replies.length > 0 ? "subthread-multi" : ""
|
||||
}`}
|
||||
>
|
||||
<div className={`subthread-container ${replies.length > 0 ? "subthread-multi" : ""}`}>
|
||||
<Divider />
|
||||
<Note
|
||||
highlight={active === a.Id}
|
||||
className={`thread-note ${
|
||||
isLastSubthread && replies.length === 0 ? "is-last-note" : ""
|
||||
}`}
|
||||
className={`thread-note ${isLastSubthread && replies.length === 0 ? "is-last-note" : ""}`}
|
||||
data-ev={a}
|
||||
key={a.Id}
|
||||
related={related}
|
||||
@ -116,13 +100,11 @@ const ThreadNote = ({
|
||||
}: ThreadNoteProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const replies = getReplies(note.Id, chains);
|
||||
const activeInReplies = replies.map((r) => r.Id).includes(active);
|
||||
const activeInReplies = replies.map(r => r.Id).includes(active);
|
||||
const [collapsed, setCollapsed] = useState(!activeInReplies);
|
||||
const hasMultipleNotes = replies.length > 0;
|
||||
const isLastVisibleNote = isLastSubthread && isLast && !hasMultipleNotes;
|
||||
const className = `subthread-container ${
|
||||
isLast && collapsed ? "subthread-last" : "subthread-multi subthread-mid"
|
||||
}`;
|
||||
const className = `subthread-container ${isLast && collapsed ? "subthread-last" : "subthread-multi subthread-mid"}`;
|
||||
return (
|
||||
<>
|
||||
<div className={className}>
|
||||
@ -149,11 +131,7 @@ const ThreadNote = ({
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
) : (
|
||||
<Collapsed
|
||||
text={formatMessage(messages.ShowReplies)}
|
||||
collapsed={collapsed}
|
||||
setCollapsed={setCollapsed}
|
||||
>
|
||||
<Collapsed text={formatMessage(messages.ShowReplies)} collapsed={collapsed} setCollapsed={setCollapsed}>
|
||||
<TierThree
|
||||
active={active}
|
||||
path={path}
|
||||
@ -170,16 +148,7 @@ const ThreadNote = ({
|
||||
);
|
||||
};
|
||||
|
||||
const TierTwo = ({
|
||||
active,
|
||||
isLastSubthread,
|
||||
path,
|
||||
from,
|
||||
notes,
|
||||
related,
|
||||
chains,
|
||||
onNavigate,
|
||||
}: SubthreadProps) => {
|
||||
const TierTwo = ({ active, isLastSubthread, path, from, notes, related, chains, onNavigate }: SubthreadProps) => {
|
||||
const [first, ...rest] = notes;
|
||||
|
||||
return (
|
||||
@ -216,36 +185,22 @@ const TierTwo = ({
|
||||
);
|
||||
};
|
||||
|
||||
const TierThree = ({
|
||||
active,
|
||||
path,
|
||||
isLastSubthread,
|
||||
from,
|
||||
notes,
|
||||
related,
|
||||
chains,
|
||||
onNavigate,
|
||||
}: SubthreadProps) => {
|
||||
const TierThree = ({ active, path, isLastSubthread, from, notes, related, chains, onNavigate }: SubthreadProps) => {
|
||||
const [first, ...rest] = notes;
|
||||
const replies = getReplies(first.Id, chains);
|
||||
const activeInReplies =
|
||||
notes.map((r) => r.Id).includes(active) ||
|
||||
replies.map((r) => r.Id).includes(active);
|
||||
const activeInReplies = notes.map(r => r.Id).includes(active) || replies.map(r => r.Id).includes(active);
|
||||
const hasMultipleNotes = rest.length > 0 || replies.length > 0;
|
||||
const isLast = replies.length === 0 && rest.length === 0;
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`subthread-container ${
|
||||
hasMultipleNotes ? "subthread-multi" : ""
|
||||
} ${isLast ? "subthread-last" : "subthread-mid"}`}
|
||||
>
|
||||
className={`subthread-container ${hasMultipleNotes ? "subthread-multi" : ""} ${
|
||||
isLast ? "subthread-last" : "subthread-mid"
|
||||
}`}>
|
||||
<Divider variant="small" />
|
||||
<Note
|
||||
highlight={active === first.Id}
|
||||
className={`thread-note ${
|
||||
isLastSubthread && isLast ? "is-last-note" : ""
|
||||
}`}
|
||||
className={`thread-note ${isLastSubthread && isLast ? "is-last-note" : ""}`}
|
||||
data-ev={first}
|
||||
key={first.Id}
|
||||
related={related}
|
||||
@ -256,11 +211,7 @@ const TierThree = ({
|
||||
{path.length <= 1 || !activeInReplies
|
||||
? replies.length > 0 && (
|
||||
<div className="show-more-container">
|
||||
<button
|
||||
className="show-more"
|
||||
type="button"
|
||||
onClick={() => onNavigate(from)}
|
||||
>
|
||||
<button className="show-more" type="button" onClick={() => onNavigate(from)}>
|
||||
<FormattedMessage {...messages.ShowReplies} />
|
||||
</button>
|
||||
</div>
|
||||
@ -284,10 +235,9 @@ const TierThree = ({
|
||||
return (
|
||||
<div
|
||||
key={r.Id}
|
||||
className={`subthread-container ${
|
||||
lastReply ? "" : "subthread-multi"
|
||||
} ${lastReply ? "subthread-last" : "subthread-mid"}`}
|
||||
>
|
||||
className={`subthread-container ${lastReply ? "" : "subthread-multi"} ${
|
||||
lastReply ? "subthread-last" : "subthread-mid"
|
||||
}`}>
|
||||
<Divider variant="small" />
|
||||
<Note
|
||||
className={`thread-note ${lastNote ? "is-last-note" : ""}`}
|
||||
@ -311,22 +261,15 @@ export interface ThreadProps {
|
||||
|
||||
export default function Thread(props: ThreadProps) {
|
||||
const notes = props.notes ?? [];
|
||||
const parsedNotes = notes.map((a) => new NEvent(a));
|
||||
const parsedNotes = notes.map(a => new NEvent(a));
|
||||
// root note has no thread info
|
||||
const root = useMemo(
|
||||
() => parsedNotes.find((a) => a.Thread === null),
|
||||
[notes]
|
||||
);
|
||||
const root = useMemo(() => parsedNotes.find(a => a.Thread === null), [notes]);
|
||||
const [path, setPath] = useState<HexKey[]>([]);
|
||||
const currentId = path.length > 0 && path[path.length - 1];
|
||||
const currentRoot = useMemo(
|
||||
() => parsedNotes.find((a) => a.Id === currentId),
|
||||
[notes, currentId]
|
||||
);
|
||||
const currentRoot = useMemo(() => parsedNotes.find(a => a.Id === currentId), [notes, currentId]);
|
||||
const [navigated, setNavigated] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const isSingleNote =
|
||||
parsedNotes.filter((a) => a.Kind === EventKind.TextNote).length === 1;
|
||||
const isSingleNote = parsedNotes.filter(a => a.Kind === EventKind.TextNote).length === 1;
|
||||
const location = useLocation();
|
||||
const urlNoteId = location?.pathname.slice(3);
|
||||
const urlNoteHex = urlNoteId && bech32ToHex(urlNoteId);
|
||||
@ -334,9 +277,9 @@ export default function Thread(props: ThreadProps) {
|
||||
const chains = useMemo(() => {
|
||||
const chains = new Map<u256, NEvent[]>();
|
||||
parsedNotes
|
||||
?.filter((a) => a.Kind === EventKind.TextNote)
|
||||
?.filter(a => a.Kind === EventKind.TextNote)
|
||||
.sort((a, b) => b.CreatedAt - a.CreatedAt)
|
||||
.forEach((v) => {
|
||||
.forEach(v => {
|
||||
const replyTo = v.Thread?.ReplyTo?.Event ?? v.Thread?.Root?.Event;
|
||||
if (replyTo) {
|
||||
if (!chains.has(replyTo)) {
|
||||
@ -378,28 +321,15 @@ export default function Thread(props: ThreadProps) {
|
||||
}, [root, navigated, urlNoteHex, chains]);
|
||||
|
||||
const brokenChains = useMemo(() => {
|
||||
return Array.from(chains?.keys()).filter(
|
||||
(a) => !parsedNotes?.some((b) => b.Id === a)
|
||||
);
|
||||
return Array.from(chains?.keys()).filter(a => !parsedNotes?.some(b => b.Id === a));
|
||||
}, [chains]);
|
||||
|
||||
function renderRoot(note: NEvent) {
|
||||
const className = `thread-root ${isSingleNote ? "thread-root-single" : ""}`;
|
||||
if (note) {
|
||||
return (
|
||||
<Note
|
||||
className={className}
|
||||
key={note.Id}
|
||||
data-ev={note}
|
||||
related={notes}
|
||||
/>
|
||||
);
|
||||
return <Note className={className} key={note.Id} data-ev={note} related={notes} />;
|
||||
} else {
|
||||
return (
|
||||
<NoteGhost className={className}>
|
||||
Loading thread root.. ({notes?.length} notes loaded)
|
||||
</NoteGhost>
|
||||
);
|
||||
return <NoteGhost className={className}>Loading thread root.. ({notes?.length} notes loaded)</NoteGhost>;
|
||||
}
|
||||
}
|
||||
|
||||
@ -438,25 +368,18 @@ export default function Thread(props: ThreadProps) {
|
||||
|
||||
return (
|
||||
<div className="main-content mt10">
|
||||
<BackButton
|
||||
onClick={goBack}
|
||||
text={path?.length > 1 ? "Parent" : "Back"}
|
||||
/>
|
||||
<BackButton onClick={goBack} text={path?.length > 1 ? "Parent" : "Back"} />
|
||||
<div className="thread-container">
|
||||
{currentRoot && renderRoot(currentRoot)}
|
||||
{currentRoot && renderChain(currentRoot.Id)}
|
||||
{currentRoot === root && (
|
||||
<>
|
||||
{brokenChains.length > 0 && <h3>Other replies</h3>}
|
||||
{brokenChains.map((a) => {
|
||||
{brokenChains.map(a => {
|
||||
return (
|
||||
<div className="mb10">
|
||||
<NoteGhost
|
||||
className={`thread-note thread-root ghost-root`}
|
||||
key={a}
|
||||
>
|
||||
Missing event{" "}
|
||||
<Link to={eventLink(a)}>{a.substring(0, 8)}</Link>
|
||||
<NoteGhost className={`thread-note thread-root ghost-root`} key={a}>
|
||||
Missing event <Link to={eventLink(a)}>{a.substring(0, 8)}</Link>
|
||||
</NoteGhost>
|
||||
{renderChain(a)}
|
||||
</div>
|
||||
|
@ -34,13 +34,11 @@ async function oembedLookup(link: string) {
|
||||
const TidalEmbed = ({ link }: { link: string }) => {
|
||||
const [source, setSource] = useState<string>();
|
||||
const [height, setHeight] = useState<number>();
|
||||
const extraStyles = link.includes("video")
|
||||
? { aspectRatio: "16 / 9" }
|
||||
: { height };
|
||||
const extraStyles = link.includes("video") ? { aspectRatio: "16 / 9" } : { height };
|
||||
|
||||
useEffect(() => {
|
||||
oembedLookup(link)
|
||||
.then((data) => {
|
||||
.then(data => {
|
||||
setSource(data.source || undefined);
|
||||
setHeight(data.height);
|
||||
})
|
||||
@ -49,25 +47,11 @@ const TidalEmbed = ({ link }: { link: string }) => {
|
||||
|
||||
if (!source)
|
||||
return (
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="ext"
|
||||
>
|
||||
<a href={link} target="_blank" rel="noreferrer" onClick={e => e.stopPropagation()} className="ext">
|
||||
{link}
|
||||
</a>
|
||||
);
|
||||
return (
|
||||
<iframe
|
||||
src={source}
|
||||
style={extraStyles}
|
||||
width="100%"
|
||||
title="TIDAL Embed"
|
||||
frameBorder={0}
|
||||
/>
|
||||
);
|
||||
return <iframe src={source} style={extraStyles} width="100%" title="TIDAL Embed" frameBorder={0} />;
|
||||
};
|
||||
|
||||
export default TidalEmbed;
|
||||
|
@ -36,18 +36,17 @@ export default function Timeline({
|
||||
window,
|
||||
}: TimelineProps) {
|
||||
const { muted, isMuted } = useModeration();
|
||||
const { main, related, latest, parent, loadMore, showLatest } =
|
||||
useTimelineFeed(subject, {
|
||||
method,
|
||||
window: window,
|
||||
});
|
||||
const { main, related, latest, parent, loadMore, showLatest } = useTimelineFeed(subject, {
|
||||
method,
|
||||
window: window,
|
||||
});
|
||||
|
||||
const filterPosts = useCallback(
|
||||
(nts: TaggedRawEvent[]) => {
|
||||
return [...nts]
|
||||
.sort((a, b) => b.created_at - a.created_at)
|
||||
?.filter((a) => (postsOnly ? !a.tags.some((b) => b[0] === "e") : true))
|
||||
.filter((a) => ignoreModeration || !isMuted(a.pubkey));
|
||||
?.filter(a => (postsOnly ? !a.tags.some(b => b[0] === "e") : true))
|
||||
.filter(a => ignoreModeration || !isMuted(a.pubkey));
|
||||
},
|
||||
[postsOnly, muted]
|
||||
);
|
||||
@ -57,9 +56,7 @@ export default function Timeline({
|
||||
}, [main, filterPosts]);
|
||||
|
||||
const latestFeed = useMemo(() => {
|
||||
return filterPosts(latest.notes).filter(
|
||||
(a) => !mainFeed.some((b) => b.id === a.id)
|
||||
);
|
||||
return filterPosts(latest.notes).filter(a => !mainFeed.some(b => b.id === a.id));
|
||||
}, [latest, mainFeed, filterPosts]);
|
||||
|
||||
function eventElement(e: TaggedRawEvent) {
|
||||
@ -68,14 +65,7 @@ export default function Timeline({
|
||||
return <ProfilePreview pubkey={e.pubkey} className="card" />;
|
||||
}
|
||||
case EventKind.TextNote: {
|
||||
return (
|
||||
<Note
|
||||
key={e.id}
|
||||
data={e}
|
||||
related={related.notes}
|
||||
ignoreModeration={ignoreModeration}
|
||||
/>
|
||||
);
|
||||
return <Note key={e.id} data={e} related={related.notes} ignoreModeration={ignoreModeration} />;
|
||||
}
|
||||
case EventKind.ZapReceipt: {
|
||||
const zap = parseZap(e);
|
||||
@ -83,14 +73,8 @@ export default function Timeline({
|
||||
}
|
||||
case EventKind.Reaction:
|
||||
case EventKind.Repost: {
|
||||
const eRef = e.tags.find((a) => a[0] === "e")?.at(1);
|
||||
return (
|
||||
<NoteReaction
|
||||
data={e}
|
||||
key={e.id}
|
||||
root={parent.notes.find((a) => a.id === eRef)}
|
||||
/>
|
||||
);
|
||||
const eRef = e.tags.find(a => a[0] === "e")?.at(1);
|
||||
return <NoteReaction data={e} key={e.id} root={parent.notes.find(a => a.id === eRef)} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -100,10 +84,7 @@ export default function Timeline({
|
||||
{latestFeed.length > 1 && (
|
||||
<div className="card latest-notes pointer" onClick={() => showLatest()}>
|
||||
<FontAwesomeIcon icon={faForward} size="xl" />{" "}
|
||||
<FormattedMessage
|
||||
{...messages.ShowLatest}
|
||||
values={{ n: latestFeed.length - 1 }}
|
||||
/>
|
||||
<FormattedMessage {...messages.ShowLatest} values={{ n: latestFeed.length - 1 }} />
|
||||
</div>
|
||||
)}
|
||||
{mainFeed.map(eventElement)}
|
||||
|
@ -2,12 +2,9 @@ import "./Zap.css";
|
||||
import { useMemo } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useSelector } from "react-redux";
|
||||
// @ts-expect-error No types available
|
||||
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
||||
import { bytesToHex } from "@noble/hashes/utils";
|
||||
import { sha256, unwrap } from "Util";
|
||||
|
||||
//import { sha256 } from "Util";
|
||||
import { formatShort } from "Number";
|
||||
import { HexKey, TaggedRawEvent } from "Nostr";
|
||||
import Event from "Nostr/Event";
|
||||
@ -18,28 +15,20 @@ import { RootState } from "State/Store";
|
||||
import messages from "./messages";
|
||||
|
||||
function findTag(e: TaggedRawEvent, tag: string) {
|
||||
const maybeTag = e.tags.find((evTag) => {
|
||||
const maybeTag = e.tags.find(evTag => {
|
||||
return evTag[0] === tag;
|
||||
});
|
||||
return maybeTag && maybeTag[1];
|
||||
}
|
||||
|
||||
interface Section {
|
||||
name: string;
|
||||
}
|
||||
|
||||
function getInvoice(zap: TaggedRawEvent) {
|
||||
const bolt11 = findTag(zap, "bolt11");
|
||||
const decoded = invoiceDecode(bolt11);
|
||||
|
||||
const amount = decoded.sections.find(
|
||||
(section: Section) => section.name === "amount"
|
||||
)?.value;
|
||||
const hash = decoded.sections.find(
|
||||
(section: Section) => section.name === "description_hash"
|
||||
)?.value;
|
||||
const amount = decoded.sections.find(section => section.name === "amount")?.value;
|
||||
const hash = decoded.sections.find(section => section.name === "description_hash")?.value;
|
||||
|
||||
return { amount, hash: hash ? bytesToHex(hash) : undefined };
|
||||
return { amount, hash: hash ? bytesToHex(hash as Uint8Array) : undefined };
|
||||
}
|
||||
|
||||
interface Zapper {
|
||||
@ -88,13 +77,7 @@ export function parseZap(zap: TaggedRawEvent): ParsedZap {
|
||||
};
|
||||
}
|
||||
|
||||
const Zap = ({
|
||||
zap,
|
||||
showZapped = true,
|
||||
}: {
|
||||
zap: ParsedZap;
|
||||
showZapped?: boolean;
|
||||
}) => {
|
||||
const Zap = ({ zap, showZapped = true }: { zap: ParsedZap; showZapped?: boolean }) => {
|
||||
const { amount, content, zapper, valid, p } = zap;
|
||||
const pubKey = useSelector((s: RootState) => s.login.publicKey);
|
||||
|
||||
@ -105,21 +88,13 @@ const Zap = ({
|
||||
{p !== pubKey && showZapped && <ProfileImage pubkey={p} />}
|
||||
<div className="amount">
|
||||
<span className="amount-number">
|
||||
<FormattedMessage
|
||||
{...messages.Sats}
|
||||
values={{ n: formatShort(amount) }}
|
||||
/>
|
||||
<FormattedMessage {...messages.Sats} values={{ n: formatShort(amount) }} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{content.length > 0 && zapper && (
|
||||
<div className="body">
|
||||
<Text
|
||||
creator={zapper}
|
||||
content={content}
|
||||
tags={[]}
|
||||
users={new Map()}
|
||||
/>
|
||||
<Text creator={zapper} content={content} tags={[]} users={new Map()} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -132,8 +107,8 @@ interface ZapsSummaryProps {
|
||||
|
||||
export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
|
||||
const sortedZaps = useMemo(() => {
|
||||
const pub = [...zaps.filter((z) => z.zapper && z.valid)];
|
||||
const priv = [...zaps.filter((z) => !z.zapper && z.valid)];
|
||||
const pub = [...zaps.filter(z => z.zapper && z.valid)];
|
||||
const priv = [...zaps.filter(z => !z.zapper && z.valid)];
|
||||
pub.sort((a, b) => b.amount - a.amount);
|
||||
return pub.concat(priv);
|
||||
}, [zaps]);
|
||||
@ -151,16 +126,8 @@ export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
|
||||
<div className={`top-zap`}>
|
||||
<div className="summary">
|
||||
{zapper && <ProfileImage pubkey={zapper} />}
|
||||
{restZaps.length > 0 && (
|
||||
<FormattedMessage
|
||||
{...messages.Others}
|
||||
values={{ n: restZaps.length }}
|
||||
/>
|
||||
)}{" "}
|
||||
<FormattedMessage
|
||||
{...messages.OthersZapped}
|
||||
values={{ n: restZaps.length }}
|
||||
/>
|
||||
{restZaps.length > 0 && <FormattedMessage {...messages.Others} values={{ n: restZaps.length }} />}{" "}
|
||||
<FormattedMessage {...messages.OthersZapped} values={{ n: restZaps.length }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -89,8 +89,7 @@ const messages = defineMessages({
|
||||
AccountSupport: "Account Support",
|
||||
GoTo: "Go to",
|
||||
FindMore: "Find out more info about {service} at {link}",
|
||||
SavePassword:
|
||||
"Please make sure to save the following password in order to manage your handle in the future",
|
||||
SavePassword: "Please make sure to save the following password in order to manage your handle in the future",
|
||||
});
|
||||
|
||||
export default addIdAndDefaultMessageToMessages(messages, "Element");
|
||||
|
Reference in New Issue
Block a user