fix eslint warnings
This commit is contained in:
@ -1,14 +1,20 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export default function AsyncButton(props: any) {
|
||||
interface AsyncButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
onClick(e: React.MouseEvent): Promise<void> | void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function AsyncButton(props: AsyncButtonProps) {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
async function handle(e: any) {
|
||||
async function handle(e: React.MouseEvent) {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
if (typeof props.onClick === "function") {
|
||||
let f = props.onClick(e);
|
||||
const f = props.onClick(e);
|
||||
if (f instanceof Promise) {
|
||||
await f;
|
||||
}
|
||||
@ -19,12 +25,7 @@ export default function AsyncButton(props: any) {
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled={loading}
|
||||
{...props}
|
||||
onClick={(e) => handle(e)}
|
||||
>
|
||||
<button type="button" disabled={loading} {...props} onClick={handle}>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
|
@ -1,13 +1,7 @@
|
||||
import { useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { HexKey } from "Nostr";
|
||||
import type { RootState } from "State/Store";
|
||||
import MuteButton from "Element/MuteButton";
|
||||
import BlockButton from "Element/BlockButton";
|
||||
import ProfilePreview from "Element/ProfilePreview";
|
||||
import useMutedFeed, { getMuted } from "Feed/MuteList";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
|
||||
import messages from "./messages";
|
||||
@ -17,7 +11,6 @@ interface BlockListProps {
|
||||
}
|
||||
|
||||
export default function BlockList({ variant }: BlockListProps) {
|
||||
const { publicKey } = useSelector((s: RootState) => s.login);
|
||||
const { blocked, muted } = useModeration();
|
||||
|
||||
return (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState, ReactNode } from "react";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import ShowMore from "Element/ShowMore";
|
||||
|
||||
|
@ -8,7 +8,7 @@ export interface CopyProps {
|
||||
maxSize?: number;
|
||||
}
|
||||
export default function Copy({ text, maxSize = 32 }: CopyProps) {
|
||||
const { copy, copied, error } = useCopy();
|
||||
const { copy, copied } = useCopy();
|
||||
const sliceLength = maxSize / 2;
|
||||
const trimmed =
|
||||
text.length > maxSize
|
||||
|
@ -12,6 +12,7 @@ import { setLastReadDm } from "Pages/MessagesPage";
|
||||
import { RootState } from "State/Store";
|
||||
import { HexKey, TaggedRawEvent } from "Nostr";
|
||||
import { incDmInteraction } from "State/Login";
|
||||
import { unwrap } from "Util";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
@ -32,11 +33,11 @@ export default function DM(props: DMProps) {
|
||||
const isMe = props.data.pubkey === pubKey;
|
||||
const otherPubkey = isMe
|
||||
? pubKey
|
||||
: props.data.tags.find((a) => a[0] === "p")![1];
|
||||
: unwrap(props.data.tags.find((a) => a[0] === "p")?.[1]);
|
||||
|
||||
async function decrypt() {
|
||||
let e = new Event(props.data);
|
||||
let decrypted = await publisher.decryptDm(e);
|
||||
const e = new Event(props.data);
|
||||
const decrypted = await publisher.decryptDm(e);
|
||||
setContent(decrypted || "<ERROR>");
|
||||
if (!isMe) {
|
||||
setLastReadDm(e.PubKey);
|
||||
|
@ -21,12 +21,12 @@ export default function FollowButton(props: FollowButtonProps) {
|
||||
const baseClassname = `${props.className} follow-button`;
|
||||
|
||||
async function follow(pubkey: HexKey) {
|
||||
let ev = await publiser.addFollow(pubkey);
|
||||
const ev = await publiser.addFollow(pubkey);
|
||||
publiser.broadcast(ev);
|
||||
}
|
||||
|
||||
async function unfollow(pubkey: HexKey) {
|
||||
let ev = await publiser.removeFollow(pubkey);
|
||||
const ev = await publiser.removeFollow(pubkey);
|
||||
publiser.broadcast(ev);
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ export default function FollowListBase({
|
||||
const publisher = useEventPublisher();
|
||||
|
||||
async function followAll() {
|
||||
let ev = await publisher.addFollow(pubkeys);
|
||||
const ev = await publisher.addFollow(pubkeys);
|
||||
publisher.broadcast(ev);
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ export default function FollowersList({ pubkey }: FollowersListProps) {
|
||||
const feed = useFollowersFeed(pubkey);
|
||||
|
||||
const pubkeys = useMemo(() => {
|
||||
let contactLists = feed?.store.notes.filter(
|
||||
const contactLists = feed?.store.notes.filter(
|
||||
(a) =>
|
||||
a.kind === EventKind.ContactList &&
|
||||
a.tags.some((b) => b[0] === "p" && b[1] === pubkey)
|
||||
|
@ -25,7 +25,7 @@ export default function FollowsYou({ pubkey }: FollowsYouProps) {
|
||||
return getFollowers(feed.store, pubkey);
|
||||
}, [feed, pubkey]);
|
||||
|
||||
const followsMe = pubkeys.includes(loginPubKey!) ?? false;
|
||||
const followsMe = loginPubKey ? pubkeys.includes(loginPubKey) : false;
|
||||
|
||||
return followsMe ? (
|
||||
<span className="follows-you">{formatMessage(messages.FollowsYou)}</span>
|
||||
|
@ -135,7 +135,9 @@ export default function HyperText({
|
||||
</a>
|
||||
);
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
// Ignore the error.
|
||||
}
|
||||
return (
|
||||
<a
|
||||
href={a}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import "./Invoice.css";
|
||||
import { useState } from "react";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error No types available
|
||||
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
||||
import { useMemo } from "react";
|
||||
import SendSats from "Element/SendSats";
|
||||
@ -13,6 +13,11 @@ import messages from "./messages";
|
||||
export interface InvoiceProps {
|
||||
invoice: string;
|
||||
}
|
||||
|
||||
interface Section {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default function Invoice(props: InvoiceProps) {
|
||||
const invoice = props.invoice;
|
||||
const webln = useWebln();
|
||||
@ -21,21 +26,21 @@ export default function Invoice(props: InvoiceProps) {
|
||||
|
||||
const info = useMemo(() => {
|
||||
try {
|
||||
let parsed = invoiceDecode(invoice);
|
||||
const parsed = invoiceDecode(invoice);
|
||||
|
||||
let amount = parseInt(
|
||||
parsed.sections.find((a: any) => a.name === "amount")?.value
|
||||
const amount = parseInt(
|
||||
parsed.sections.find((a: Section) => a.name === "amount")?.value
|
||||
);
|
||||
let timestamp = parseInt(
|
||||
parsed.sections.find((a: any) => a.name === "timestamp")?.value
|
||||
const timestamp = parseInt(
|
||||
parsed.sections.find((a: Section) => a.name === "timestamp")?.value
|
||||
);
|
||||
let expire = parseInt(
|
||||
parsed.sections.find((a: any) => a.name === "expiry")?.value
|
||||
const expire = parseInt(
|
||||
parsed.sections.find((a: Section) => a.name === "expiry")?.value
|
||||
);
|
||||
let description = parsed.sections.find(
|
||||
(a: any) => a.name === "description"
|
||||
const description = parsed.sections.find(
|
||||
(a: Section) => a.name === "description"
|
||||
)?.value;
|
||||
let ret = {
|
||||
const ret = {
|
||||
amount: !isNaN(amount) ? amount / 1000 : 0,
|
||||
expire: !isNaN(timestamp) && !isNaN(expire) ? timestamp + expire : null,
|
||||
description,
|
||||
@ -72,7 +77,7 @@ export default function Invoice(props: InvoiceProps) {
|
||||
);
|
||||
}
|
||||
|
||||
async function payInvoice(e: any) {
|
||||
async function payInvoice(e: React.MouseEvent<HTMLButtonElement>) {
|
||||
e.stopPropagation();
|
||||
if (webln?.enabled) {
|
||||
try {
|
||||
|
59
src/Element/LNURLTip.css
Normal file
59
src/Element/LNURLTip.css
Normal file
@ -0,0 +1,59 @@
|
||||
.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;
|
||||
}
|
301
src/Element/LNURLTip.tsx
Normal file
301
src/Element/LNURLTip.tsx
Normal file
@ -0,0 +1,301 @@
|
||||
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>
|
||||
);
|
||||
}
|
@ -23,7 +23,7 @@ export default function LoadMore({
|
||||
}, [inView, shouldLoadMore, tick]);
|
||||
|
||||
useEffect(() => {
|
||||
let t = setInterval(() => {
|
||||
const t = setInterval(() => {
|
||||
setTick((x) => (x += 1));
|
||||
}, 500);
|
||||
return () => clearInterval(t);
|
||||
|
@ -9,10 +9,10 @@ export default function Mention({ pubkey }: { pubkey: HexKey }) {
|
||||
|
||||
const name = useMemo(() => {
|
||||
let name = hexToBech32("npub", pubkey).substring(0, 12);
|
||||
if ((user?.display_name?.length ?? 0) > 0) {
|
||||
name = user!.display_name!;
|
||||
} else if ((user?.name?.length ?? 0) > 0) {
|
||||
name = user!.name!;
|
||||
if (user?.display_name !== undefined && user.display_name.length > 0) {
|
||||
name = user.display_name;
|
||||
} else if (user?.name !== undefined && user.name.length > 0) {
|
||||
name = user.name;
|
||||
}
|
||||
return name;
|
||||
}, [user, pubkey]);
|
||||
|
@ -8,10 +8,13 @@ export interface ModalProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function useOnClickOutside(ref: any, onClickOutside: () => void) {
|
||||
function useOnClickOutside(
|
||||
ref: React.MutableRefObject<Element | null>,
|
||||
onClickOutside: () => void
|
||||
) {
|
||||
useEffect(() => {
|
||||
function handleClickOutside(ev: any) {
|
||||
if (ref && ref.current && !ref.current.contains(ev.target)) {
|
||||
function handleClickOutside(ev: MouseEvent) {
|
||||
if (ref && ref.current && !ref.current.contains(ev.target as Node)) {
|
||||
onClickOutside();
|
||||
}
|
||||
}
|
||||
@ -24,7 +27,7 @@ function useOnClickOutside(ref: any, onClickOutside: () => void) {
|
||||
|
||||
export default function Modal(props: ModalProps) {
|
||||
const ref = useRef(null);
|
||||
const onClose = props.onClose || (() => {});
|
||||
const onClose = props.onClose || (() => undefined);
|
||||
const className = props.className || "";
|
||||
useOnClickOutside(ref, onClose);
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { HexKey } from "Nostr";
|
||||
import MuteButton from "Element/MuteButton";
|
||||
import ProfilePreview from "Element/ProfilePreview";
|
||||
|
@ -29,7 +29,9 @@ type Nip05ServiceProps = {
|
||||
supportLink: string;
|
||||
};
|
||||
|
||||
type ReduxStore = any;
|
||||
interface ReduxStore {
|
||||
login: { publicKey: string };
|
||||
}
|
||||
|
||||
export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
const navigate = useNavigate();
|
||||
@ -64,9 +66,9 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
if ("error" in a) {
|
||||
setError(a as ServiceError);
|
||||
} else {
|
||||
let svc = a as ServiceConfig;
|
||||
const svc = a as ServiceConfig;
|
||||
setServiceConfig(svc);
|
||||
let defaultDomain =
|
||||
const defaultDomain =
|
||||
svc.domains.find((a) => a.default)?.name || svc.domains[0].name;
|
||||
setDomain(defaultDomain);
|
||||
}
|
||||
@ -86,7 +88,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
setAvailabilityResponse({ available: false, why: "TOO_LONG" });
|
||||
return;
|
||||
}
|
||||
let rx = new RegExp(
|
||||
const rx = new RegExp(
|
||||
domainConfig?.regex[0] ?? "",
|
||||
domainConfig?.regex[1] ?? ""
|
||||
);
|
||||
@ -111,14 +113,14 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (registerResponse && showInvoice) {
|
||||
let t = setInterval(async () => {
|
||||
let status = await svc.CheckRegistration(registerResponse.token);
|
||||
const t = setInterval(async () => {
|
||||
const status = await svc.CheckRegistration(registerResponse.token);
|
||||
if ("error" in status) {
|
||||
setError(status);
|
||||
setRegisterResponse(undefined);
|
||||
setShowInvoice(false);
|
||||
} else {
|
||||
let result: CheckRegisterResponse = status;
|
||||
const result: CheckRegisterResponse = status;
|
||||
if (result.available && result.paid) {
|
||||
setShowInvoice(false);
|
||||
setRegisterStatus(status);
|
||||
@ -131,8 +133,14 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
}
|
||||
}, [registerResponse, showInvoice, svc]);
|
||||
|
||||
function mapError(e: ServiceErrorCode, t: string | null): string | undefined {
|
||||
let whyMap = new Map([
|
||||
function mapError(
|
||||
e: ServiceErrorCode | undefined,
|
||||
t: string | null
|
||||
): string | undefined {
|
||||
if (e === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const whyMap = new Map([
|
||||
["TOO_SHORT", formatMessage(messages.TooShort)],
|
||||
["TOO_LONG", formatMessage(messages.TooLong)],
|
||||
["REGEX", formatMessage(messages.Regex)],
|
||||
@ -149,7 +157,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
let rsp = await svc.RegisterHandle(handle, domain, pubkey);
|
||||
const rsp = await svc.RegisterHandle(handle, domain, pubkey);
|
||||
if ("error" in rsp) {
|
||||
setError(rsp);
|
||||
} else {
|
||||
@ -160,11 +168,11 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
|
||||
async function updateProfile(handle: string, domain: string) {
|
||||
if (user) {
|
||||
let newProfile = {
|
||||
const newProfile = {
|
||||
...user,
|
||||
nip05: `${handle}@${domain}`,
|
||||
} as UserMetadata;
|
||||
let ev = await publisher.metadata(newProfile);
|
||||
const ev = await publisher.metadata(newProfile);
|
||||
publisher.broadcast(ev);
|
||||
navigate("/settings");
|
||||
}
|
||||
@ -231,7 +239,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
<b className="error">
|
||||
<FormattedMessage {...messages.NotAvailable} />{" "}
|
||||
{mapError(
|
||||
availabilityResponse.why!,
|
||||
availabilityResponse.why,
|
||||
availabilityResponse.reasonTag || null
|
||||
)}
|
||||
</b>
|
||||
|
@ -17,7 +17,6 @@ import Text from "Element/Text";
|
||||
import { eventLink, getReactions, hexToBech32 } from "Util";
|
||||
import NoteFooter, { Translation } from "Element/NoteFooter";
|
||||
import NoteTime from "Element/NoteTime";
|
||||
import ShowMore from "Element/ShowMore";
|
||||
import EventKind from "Nostr/EventKind";
|
||||
import { useUserProfiles } from "Feed/ProfileFeed";
|
||||
import { TaggedRawEvent, u256 } from "Nostr";
|
||||
@ -39,10 +38,10 @@ export interface NoteProps {
|
||||
["data-ev"]?: NEvent;
|
||||
}
|
||||
|
||||
const HiddenNote = ({ children }: any) => {
|
||||
const HiddenNote = ({ children }: { children: React.ReactNode }) => {
|
||||
const [show, setShow] = useState(false);
|
||||
return show ? (
|
||||
children
|
||||
<>{children}</>
|
||||
) : (
|
||||
<div className="card note hidden-note">
|
||||
<div className="header">
|
||||
@ -61,7 +60,6 @@ export default function Note(props: NoteProps) {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
data,
|
||||
className,
|
||||
related,
|
||||
highlight,
|
||||
options: opt,
|
||||
@ -80,9 +78,9 @@ export default function Note(props: NoteProps) {
|
||||
const { ref, inView, entry } = useInView({ triggerOnce: true });
|
||||
const [extendable, setExtendable] = useState<boolean>(false);
|
||||
const [showMore, setShowMore] = useState<boolean>(false);
|
||||
const baseClassname = `note card ${props.className ? props.className : ""}`;
|
||||
const baseClassName = `note card ${props.className ? props.className : ""}`;
|
||||
const [translated, setTranslated] = useState<Translation>();
|
||||
const replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event;
|
||||
// TODO Why was this unused? Was this a mistake?
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const options = {
|
||||
@ -93,7 +91,7 @@ export default function Note(props: NoteProps) {
|
||||
};
|
||||
|
||||
const transformBody = useCallback(() => {
|
||||
let body = ev?.Content ?? "";
|
||||
const body = ev?.Content ?? "";
|
||||
if (deletions?.length > 0) {
|
||||
return (
|
||||
<b className="error">
|
||||
@ -113,14 +111,14 @@ export default function Note(props: NoteProps) {
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (entry && inView && extendable === false) {
|
||||
let h = entry?.target.clientHeight ?? 0;
|
||||
const h = entry?.target.clientHeight ?? 0;
|
||||
if (h > 650) {
|
||||
setExtendable(true);
|
||||
}
|
||||
}
|
||||
}, [inView, entry, extendable]);
|
||||
|
||||
function goToEvent(e: any, id: u256) {
|
||||
function goToEvent(e: React.MouseEvent, id: u256) {
|
||||
e.stopPropagation();
|
||||
navigate(eventLink(id));
|
||||
}
|
||||
@ -131,9 +129,9 @@ export default function Note(props: NoteProps) {
|
||||
}
|
||||
|
||||
const maxMentions = 2;
|
||||
let replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event;
|
||||
let mentions: { pk: string; name: string; link: ReactNode }[] = [];
|
||||
for (let pk of ev.Thread?.PubKeys) {
|
||||
const replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event;
|
||||
const mentions: { pk: string; name: string; link: ReactNode }[] = [];
|
||||
for (const pk of ev.Thread?.PubKeys ?? []) {
|
||||
const u = users?.get(pk);
|
||||
const npub = hexToBech32("npub", pk);
|
||||
const shortNpub = npub.substring(0, 12);
|
||||
@ -153,9 +151,9 @@ export default function Note(props: NoteProps) {
|
||||
});
|
||||
}
|
||||
}
|
||||
mentions.sort((a, b) => (a.name.startsWith("npub") ? 1 : -1));
|
||||
let othersLength = mentions.length - maxMentions;
|
||||
const renderMention = (m: any, idx: number) => {
|
||||
mentions.sort((a) => (a.name.startsWith("npub") ? 1 : -1));
|
||||
const othersLength = mentions.length - maxMentions;
|
||||
const renderMention = (m: { link: React.ReactNode }, idx: number) => {
|
||||
return (
|
||||
<>
|
||||
{idx > 0 && ", "}
|
||||
@ -268,7 +266,7 @@ export default function Note(props: NoteProps) {
|
||||
|
||||
const note = (
|
||||
<div
|
||||
className={`${baseClassname}${highlight ? " active " : " "}${
|
||||
className={`${baseClassName}${highlight ? " active " : " "}${
|
||||
extendable && !showMore ? " note-expand" : ""
|
||||
}`}
|
||||
ref={ref}
|
||||
|
@ -33,14 +33,14 @@ export interface NoteCreatorProps {
|
||||
show: boolean;
|
||||
setShow: (s: boolean) => void;
|
||||
replyTo?: NEvent;
|
||||
onSend?: Function;
|
||||
onSend?: () => void;
|
||||
autoFocus: boolean;
|
||||
}
|
||||
|
||||
export function NoteCreator(props: NoteCreatorProps) {
|
||||
const { show, setShow, replyTo, onSend, autoFocus } = props;
|
||||
const publisher = useEventPublisher();
|
||||
const [note, setNote] = useState<string>();
|
||||
const [note, setNote] = useState<string>("");
|
||||
const [error, setError] = useState<string>();
|
||||
const [active, setActive] = useState<boolean>(false);
|
||||
const uploader = useFileUpload();
|
||||
@ -48,7 +48,7 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
|
||||
async function sendNote() {
|
||||
if (note) {
|
||||
let ev = replyTo
|
||||
const ev = replyTo
|
||||
? await publisher.reply(replyTo, note)
|
||||
: await publisher.note(note);
|
||||
console.debug("Sending note: ", ev);
|
||||
@ -64,21 +64,23 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
|
||||
async function attachFile() {
|
||||
try {
|
||||
let file = await openFile();
|
||||
const file = await openFile();
|
||||
if (file) {
|
||||
let rx = await uploader.upload(file, file.name);
|
||||
const rx = await uploader.upload(file, file.name);
|
||||
if (rx.url) {
|
||||
setNote((n) => `${n ? `${n}\n` : ""}${rx.url}`);
|
||||
} else if (rx?.error) {
|
||||
setError(rx.error);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error?.message);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
setError(error?.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onChange(ev: any) {
|
||||
function onChange(ev: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||
const { value } = ev.target;
|
||||
setNote(value);
|
||||
if (value) {
|
||||
@ -88,7 +90,7 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
}
|
||||
}
|
||||
|
||||
function cancel(ev: any) {
|
||||
function cancel() {
|
||||
setShow(false);
|
||||
setNote("");
|
||||
}
|
||||
@ -112,11 +114,7 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
value={note}
|
||||
onFocus={() => setActive(true)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="attachment"
|
||||
onClick={(e) => attachFile()}
|
||||
>
|
||||
<button type="button" className="attachment" onClick={attachFile}>
|
||||
<Attachment />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -95,7 +95,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
const groupReactions = useMemo(() => {
|
||||
const result = reactions?.reduce(
|
||||
(acc, reaction) => {
|
||||
let kind = normalizeReaction(reaction.content);
|
||||
const kind = normalizeReaction(reaction.content);
|
||||
const rs = acc[kind] || [];
|
||||
if (rs.map((e) => e.pubkey).includes(reaction.pubkey)) {
|
||||
return acc;
|
||||
@ -128,7 +128,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
|
||||
async function react(content: string) {
|
||||
if (!hasReacted(content)) {
|
||||
let evLike = await publisher.react(ev, content);
|
||||
const evLike = await publisher.react(ev, content);
|
||||
publisher.broadcast(evLike);
|
||||
}
|
||||
}
|
||||
@ -139,7 +139,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
formatMessage(messages.ConfirmDeletion, { id: ev.Id.substring(0, 8) })
|
||||
)
|
||||
) {
|
||||
let evDelete = await publisher.delete(ev.Id);
|
||||
const evDelete = await publisher.delete(ev.Id);
|
||||
publisher.broadcast(evDelete);
|
||||
}
|
||||
}
|
||||
@ -150,14 +150,14 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
!prefs.confirmReposts ||
|
||||
window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id }))
|
||||
) {
|
||||
let evRepost = await publisher.repost(ev);
|
||||
const evRepost = await publisher.repost(ev);
|
||||
publisher.broadcast(evRepost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tipButton() {
|
||||
let service = author?.lud16 || author?.lud06;
|
||||
const service = author?.lud16 || author?.lud06;
|
||||
if (service) {
|
||||
return (
|
||||
<>
|
||||
@ -246,7 +246,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
let result = await res.json();
|
||||
const result = await res.json();
|
||||
if (typeof props.onTranslated === "function" && result) {
|
||||
props.onTranslated({
|
||||
text: result.translatedText,
|
||||
@ -332,7 +332,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
{reactionIcons()}
|
||||
<div
|
||||
className={`reaction-pill ${reply ? "reacted" : ""}`}
|
||||
onClick={(e) => setReply((s) => !s)}
|
||||
onClick={() => setReply((s) => !s)}
|
||||
>
|
||||
<div className="reaction-pill-icon">
|
||||
<Reply />
|
||||
|
@ -1,8 +1,13 @@
|
||||
import "./Note.css";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
|
||||
export default function NoteGhost(props: any) {
|
||||
const className = `note card ${props.className ? props.className : ""}`;
|
||||
interface NoteGhostProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function NoteGhost(props: NoteGhostProps) {
|
||||
const className = `note card ${props.className ?? ""}`;
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="header">
|
||||
|
@ -23,7 +23,7 @@ export default function NoteReaction(props: NoteReactionProps) {
|
||||
|
||||
const refEvent = useMemo(() => {
|
||||
if (ev) {
|
||||
let 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;
|
||||
}
|
||||
@ -45,7 +45,7 @@ export default function NoteReaction(props: NoteReactionProps) {
|
||||
ev.Content !== "#[0]"
|
||||
) {
|
||||
try {
|
||||
let r: RawEvent = JSON.parse(ev.Content);
|
||||
const r: RawEvent = JSON.parse(ev.Content);
|
||||
return r as TaggedRawEvent;
|
||||
} catch (e) {
|
||||
console.error("Could not load reposted content", e);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedRelativeTime } from "react-intl";
|
||||
|
||||
const MinuteInMs = 1_000 * 60;
|
||||
const HourInMs = MinuteInMs * 60;
|
||||
@ -19,10 +18,11 @@ export default function NoteTime(props: NoteTimeProps) {
|
||||
}).format(from);
|
||||
const fromDate = new Date(from);
|
||||
const isoDate = fromDate.toISOString();
|
||||
const ago = new Date().getTime() - from;
|
||||
const absAgo = Math.abs(ago);
|
||||
|
||||
function calcTime() {
|
||||
const fromDate = new Date(from);
|
||||
const ago = new Date().getTime() - from;
|
||||
const absAgo = Math.abs(ago);
|
||||
if (absAgo > DayInMs) {
|
||||
return fromDate.toLocaleDateString(undefined, {
|
||||
year: "2-digit",
|
||||
@ -38,7 +38,7 @@ export default function NoteTime(props: NoteTimeProps) {
|
||||
} else if (absAgo < MinuteInMs) {
|
||||
return fallback;
|
||||
} else {
|
||||
let mins = Math.floor(absAgo / MinuteInMs);
|
||||
const mins = Math.floor(absAgo / MinuteInMs);
|
||||
if (ago < 0) {
|
||||
return `in ${mins}m`;
|
||||
}
|
||||
@ -48,9 +48,9 @@ export default function NoteTime(props: NoteTimeProps) {
|
||||
|
||||
useEffect(() => {
|
||||
setTime(calcTime());
|
||||
let t = setInterval(() => {
|
||||
const t = setInterval(() => {
|
||||
setTime((s) => {
|
||||
let newTime = calcTime();
|
||||
const newTime = calcTime();
|
||||
if (newTime !== s) {
|
||||
return newTime;
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ export interface NoteToSelfProps {
|
||||
link?: string;
|
||||
}
|
||||
|
||||
function NoteLabel({ pubkey, link }: NoteToSelfProps) {
|
||||
function NoteLabel({ pubkey }: NoteToSelfProps) {
|
||||
const user = useUserProfile(pubkey);
|
||||
return (
|
||||
<div>
|
||||
|
@ -63,10 +63,10 @@ export function getDisplayName(
|
||||
pubkey: HexKey
|
||||
) {
|
||||
let name = hexToBech32("npub", pubkey).substring(0, 12);
|
||||
if ((user?.display_name?.length ?? 0) > 0) {
|
||||
name = user!.display_name!;
|
||||
} else if ((user?.name?.length ?? 0) > 0) {
|
||||
name = user!.name!;
|
||||
if (user?.display_name !== undefined && user.display_name.length > 0) {
|
||||
name = user.display_name;
|
||||
} else if (user?.name !== undefined && user.name.length > 0) {
|
||||
name = user.name;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
@ -1,7 +1,15 @@
|
||||
import useImgProxy from "Feed/ImgProxy";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const ProxyImg = (props: any) => {
|
||||
interface ProxyImgProps
|
||||
extends React.DetailedHTMLProps<
|
||||
React.ImgHTMLAttributes<HTMLImageElement>,
|
||||
HTMLImageElement
|
||||
> {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export const ProxyImg = (props: ProxyImgProps) => {
|
||||
const { src, size, ...rest } = props;
|
||||
const [url, setUrl] = useState<string>();
|
||||
const { proxy } = useImgProxy();
|
||||
|
@ -15,7 +15,7 @@ export default function QrCode(props: QrCodeProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if ((props.data?.length ?? 0) > 0 && qrRef.current) {
|
||||
let qr = new QRCodeStyling({
|
||||
const qr = new QRCodeStyling({
|
||||
width: props.width || 256,
|
||||
height: props.height || 256,
|
||||
data: props.data,
|
||||
@ -35,9 +35,9 @@ export default function QrCode(props: QrCodeProps) {
|
||||
qrRef.current.innerHTML = "";
|
||||
qr.append(qrRef.current);
|
||||
if (props.link) {
|
||||
qrRef.current.onclick = function (e) {
|
||||
let elm = document.createElement("a");
|
||||
elm.href = props.link!;
|
||||
qrRef.current.onclick = function () {
|
||||
const elm = document.createElement("a");
|
||||
elm.href = props.link ?? "";
|
||||
elm.click();
|
||||
};
|
||||
}
|
||||
@ -46,10 +46,5 @@ export default function QrCode(props: QrCodeProps) {
|
||||
}
|
||||
}, [props.data, props.link]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`qr${props.className ? ` ${props.className}` : ""}`}
|
||||
ref={qrRef}
|
||||
></div>
|
||||
);
|
||||
return <div className={`qr${props.className ?? ""}`} ref={qrRef}></div>;
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ export default function Relay(props: RelayProps) {
|
||||
);
|
||||
}
|
||||
|
||||
let latency = Math.floor(state?.avgLatency ?? 0);
|
||||
const latency = Math.floor(state?.avgLatency ?? 0);
|
||||
return (
|
||||
<>
|
||||
<div className={`relay w-max`}>
|
||||
@ -104,7 +104,10 @@ 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>
|
||||
|
@ -50,7 +50,7 @@ export interface LNURLTipProps {
|
||||
}
|
||||
|
||||
export default function LNURLTip(props: LNURLTipProps) {
|
||||
const onClose = props.onClose || (() => {});
|
||||
const onClose = props.onClose || (() => undefined);
|
||||
const service = props.svc;
|
||||
const show = props.show || false;
|
||||
const { note, author, target } = props;
|
||||
@ -83,7 +83,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
useEffect(() => {
|
||||
if (show && !props.invoice) {
|
||||
loadService()
|
||||
.then((a) => setPayService(a!))
|
||||
.then((a) => setPayService(a ?? undefined))
|
||||
.catch(() => setError(formatMessage(messages.LNURLFail)));
|
||||
} else {
|
||||
setPayService(undefined);
|
||||
@ -97,25 +97,14 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
|
||||
const serviceAmounts = useMemo(() => {
|
||||
if (payService) {
|
||||
let min = (payService.minSendable ?? 0) / 1000;
|
||||
let max = (payService.maxSendable ?? 0) / 1000;
|
||||
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) {
|
||||
let meta: string[][] = JSON.parse(payService.metadata);
|
||||
let desc = meta.find((a) => a[0] === "text/plain");
|
||||
let image = meta.find((a) => a[0] === "image/png;base64");
|
||||
return {
|
||||
description: desc ? desc[1] : null,
|
||||
image: image ? image[1] : null,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}, [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);
|
||||
@ -124,9 +113,9 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
};
|
||||
|
||||
async function fetchJson<T>(url: string) {
|
||||
let rsp = await fetch(url);
|
||||
const rsp = await fetch(url);
|
||||
if (rsp.ok) {
|
||||
let data: T = await rsp.json();
|
||||
const data: T = await rsp.json();
|
||||
console.log(data);
|
||||
setError(undefined);
|
||||
return data;
|
||||
@ -136,12 +125,12 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
|
||||
async function loadService(): Promise<LNURLService | null> {
|
||||
if (service) {
|
||||
let isServiceUrl = service.toLowerCase().startsWith("lnurl");
|
||||
const isServiceUrl = service.toLowerCase().startsWith("lnurl");
|
||||
if (isServiceUrl) {
|
||||
let serviceUrl = bech32ToText(service);
|
||||
const serviceUrl = bech32ToText(service);
|
||||
return await fetchJson(serviceUrl);
|
||||
} else {
|
||||
let ns = service.split("@");
|
||||
const ns = service.split("@");
|
||||
return await fetchJson(`https://${ns[1]}/.well-known/lnurlp/${ns[0]}`);
|
||||
}
|
||||
}
|
||||
@ -165,9 +154,9 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
url = `${payService.callback}?${amountParam}${commentParam}`;
|
||||
}
|
||||
try {
|
||||
let rsp = await fetch(url);
|
||||
const rsp = await fetch(url);
|
||||
if (rsp.ok) {
|
||||
let data = await rsp.json();
|
||||
const data = await rsp.json();
|
||||
console.log(data);
|
||||
if (data.status === "ERROR") {
|
||||
setError(data.reason);
|
||||
@ -185,8 +174,8 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
}
|
||||
|
||||
function custom() {
|
||||
let min = (payService?.minSendable ?? 1000) / 1000;
|
||||
let max = (payService?.maxSendable ?? 21_000_000_000) / 1000;
|
||||
const min = (payService?.minSendable ?? 1000) / 1000;
|
||||
const max = (payService?.maxSendable ?? 21_000_000_000) / 1000;
|
||||
return (
|
||||
<div className="custom-amount flex">
|
||||
<input
|
||||
@ -201,8 +190,8 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
<button
|
||||
className="secondary"
|
||||
type="button"
|
||||
disabled={!Boolean(customAmount)}
|
||||
onClick={() => selectAmount(customAmount!)}
|
||||
disabled={!customAmount}
|
||||
onClick={() => selectAmount(customAmount ?? 0)}
|
||||
>
|
||||
<FormattedMessage {...messages.Confirm} />
|
||||
</button>
|
||||
@ -213,13 +202,15 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
async function payWebLNIfEnabled(invoice: LNURLInvoice) {
|
||||
try {
|
||||
if (webln?.enabled) {
|
||||
let res = await webln.sendPayment(invoice!.pr);
|
||||
const res = await webln.sendPayment(invoice?.pr ?? "");
|
||||
console.log(res);
|
||||
setSuccess(invoice!.successAction || {});
|
||||
setSuccess(invoice?.successAction ?? {});
|
||||
}
|
||||
} catch (e: any) {
|
||||
setError(e.toString());
|
||||
} catch (e: unknown) {
|
||||
console.warn(e);
|
||||
if (e instanceof Error) {
|
||||
setError(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ import ReactMarkdown from "react-markdown";
|
||||
import { visit, SKIP } from "unist-util-visit";
|
||||
|
||||
import { UrlRegex, MentionRegex, InvoiceRegex, HashtagRegex } from "Const";
|
||||
import { eventLink, hexToBech32 } from "Util";
|
||||
import { eventLink, hexToBech32, unwrap } from "Util";
|
||||
import Invoice from "Element/Invoice";
|
||||
import Hashtag from "Element/Hashtag";
|
||||
|
||||
@ -14,11 +14,12 @@ import { MetadataCache } from "State/Users";
|
||||
import Mention from "Element/Mention";
|
||||
import HyperText from "Element/HyperText";
|
||||
import { HexKey } from "Nostr";
|
||||
import * as unist from "unist";
|
||||
|
||||
export type Fragment = string | JSX.Element;
|
||||
export type Fragment = string | React.ReactNode;
|
||||
|
||||
export interface TextFragment {
|
||||
body: Fragment[];
|
||||
body: React.ReactNode[];
|
||||
tags: Tag[];
|
||||
users: Map<string, MetadataCache>;
|
||||
}
|
||||
@ -52,24 +53,24 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
||||
.map((f) => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(MentionRegex).map((match) => {
|
||||
let matchTag = match.match(/#\[(\d+)\]/);
|
||||
const matchTag = match.match(/#\[(\d+)\]/);
|
||||
if (matchTag && matchTag.length === 2) {
|
||||
let idx = parseInt(matchTag[1]);
|
||||
let ref = frag.tags?.find((a) => a.Index === idx);
|
||||
const idx = parseInt(matchTag[1]);
|
||||
const ref = frag.tags?.find((a) => a.Index === idx);
|
||||
if (ref) {
|
||||
switch (ref.Key) {
|
||||
case "p": {
|
||||
return <Mention pubkey={ref.PubKey!} />;
|
||||
return <Mention pubkey={ref.PubKey ?? ""} />;
|
||||
}
|
||||
case "e": {
|
||||
let 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!)}
|
||||
to={eventLink(ref.Event ?? "")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
#{eText}
|
||||
@ -77,7 +78,7 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
||||
);
|
||||
}
|
||||
case "t": {
|
||||
return <Hashtag tag={ref.Hashtag!} />;
|
||||
return <Hashtag tag={ref.Hashtag ?? ""} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -127,7 +128,7 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
||||
}
|
||||
|
||||
function transformLi(frag: TextFragment) {
|
||||
let fragments = transformText(frag);
|
||||
const fragments = transformText(frag);
|
||||
return <li>{fragments}</li>;
|
||||
}
|
||||
|
||||
@ -140,9 +141,6 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
||||
}
|
||||
|
||||
function transformText(frag: TextFragment) {
|
||||
if (frag.body === undefined) {
|
||||
debugger;
|
||||
}
|
||||
let fragments = extractMentions(frag);
|
||||
fragments = extractLinks(fragments);
|
||||
fragments = extractInvoices(fragments);
|
||||
@ -152,15 +150,22 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
||||
|
||||
const components = useMemo(() => {
|
||||
return {
|
||||
p: (x: any) =>
|
||||
p: (x: { children?: React.ReactNode[] }) =>
|
||||
transformParagraph({ body: x.children ?? [], tags, users }),
|
||||
a: (x: any) => <HyperText link={x.href} creator={creator} />,
|
||||
li: (x: any) => transformLi({ 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]);
|
||||
|
||||
interface Node extends unist.Node<unist.Data> {
|
||||
value: string;
|
||||
}
|
||||
|
||||
const disableMarkdownLinks = useCallback(
|
||||
() => (tree: any) => {
|
||||
() => (tree: Node) => {
|
||||
visit(tree, (node, index, parent) => {
|
||||
if (
|
||||
parent &&
|
||||
@ -172,8 +177,9 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
||||
node.type === "definition")
|
||||
) {
|
||||
node.type = "text";
|
||||
const position = unwrap(node.position);
|
||||
node.value = content
|
||||
.slice(node.position.start.offset, node.position.end.offset)
|
||||
.slice(position.start.offset, position.end.offset)
|
||||
.replace(/\)$/, " )");
|
||||
return SKIP;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import "@webscopeio/react-textarea-autocomplete/style.css";
|
||||
import "./Textarea.css";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { useIntl } from "react-intl";
|
||||
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
|
||||
import emoji from "@jukben/emoji-search";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
@ -30,7 +30,7 @@ const EmojiItem = ({ entity: { name, char } }: { entity: EmojiItemProps }) => {
|
||||
};
|
||||
|
||||
const UserItem = (metadata: MetadataCache) => {
|
||||
const { pubkey, display_name, picture, nip05, ...rest } = metadata;
|
||||
const { pubkey, display_name, nip05, ...rest } = metadata;
|
||||
return (
|
||||
<div key={pubkey} className="user-item">
|
||||
<div className="user-picture">
|
||||
@ -44,7 +44,15 @@ const UserItem = (metadata: MetadataCache) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Textarea = ({ users, onChange, ...rest }: any) => {
|
||||
interface TextareaProps {
|
||||
autoFocus: boolean;
|
||||
className: string;
|
||||
onChange(ev: React.ChangeEvent<HTMLTextAreaElement>): void;
|
||||
value: string;
|
||||
onFocus(): void;
|
||||
}
|
||||
|
||||
const Textarea = (props: TextareaProps) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
@ -52,7 +60,7 @@ const Textarea = ({ users, onChange, ...rest }: any) => {
|
||||
|
||||
const userDataProvider = (token: string) => {
|
||||
setQuery(token);
|
||||
return allUsers;
|
||||
return allUsers ?? [];
|
||||
};
|
||||
|
||||
const emojiDataProvider = (token: string) => {
|
||||
@ -62,23 +70,26 @@ const Textarea = ({ users, onChange, ...rest }: any) => {
|
||||
};
|
||||
|
||||
return (
|
||||
// @ts-expect-error If anybody can figure out how to type this, please do
|
||||
<ReactTextareaAutocomplete
|
||||
{...rest}
|
||||
loadingComponent={() => <span>Loading....</span>}
|
||||
{...props}
|
||||
loadingComponent={() => <span>Loading...</span>}
|
||||
placeholder={formatMessage(messages.NotePlaceholder)}
|
||||
onChange={onChange}
|
||||
textAreaComponent={TextareaAutosize}
|
||||
trigger={{
|
||||
":": {
|
||||
dataProvider: emojiDataProvider,
|
||||
component: EmojiItem,
|
||||
output: (item: EmojiItemProps, trigger) => item.char,
|
||||
output: (item: EmojiItemProps) => item.char,
|
||||
},
|
||||
"@": {
|
||||
afterWhitespace: true,
|
||||
dataProvider: userDataProvider,
|
||||
component: (props: any) => <UserItem {...props.entity} />,
|
||||
output: (item: any) => `@${hexToBech32("npub", item.pubkey)}`,
|
||||
component: (props: { entity: MetadataCache }) => (
|
||||
<UserItem {...props.entity} />
|
||||
),
|
||||
output: (item: { pubkey: string }) =>
|
||||
`@${hexToBech32("npub", item.pubkey)}`,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
@ -6,19 +6,18 @@ import { useNavigate, useLocation, Link } from "react-router-dom";
|
||||
import { TaggedRawEvent, u256, HexKey } from "Nostr";
|
||||
import { default as NEvent } from "Nostr/Event";
|
||||
import EventKind from "Nostr/EventKind";
|
||||
import { eventLink, hexToBech32, bech32ToHex } from "Util";
|
||||
import { eventLink, bech32ToHex, unwrap } from "Util";
|
||||
import BackButton from "Element/BackButton";
|
||||
import Note from "Element/Note";
|
||||
import NoteGhost from "Element/NoteGhost";
|
||||
import Collapsed from "Element/Collapsed";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
function getParent(
|
||||
ev: HexKey,
|
||||
chains: Map<HexKey, NEvent[]>
|
||||
): HexKey | undefined {
|
||||
for (let [k, vs] of chains.entries()) {
|
||||
for (const [k, vs] of chains.entries()) {
|
||||
const fs = vs.map((a) => a.Id);
|
||||
if (fs.includes(ev)) {
|
||||
return k;
|
||||
@ -53,7 +52,6 @@ interface SubthreadProps {
|
||||
const Subthread = ({
|
||||
active,
|
||||
path,
|
||||
from,
|
||||
notes,
|
||||
related,
|
||||
chains,
|
||||
@ -332,20 +330,19 @@ export default function Thread(props: ThreadProps) {
|
||||
const location = useLocation();
|
||||
const urlNoteId = location?.pathname.slice(3);
|
||||
const urlNoteHex = urlNoteId && bech32ToHex(urlNoteId);
|
||||
const rootNoteId = root && hexToBech32("note", root.Id);
|
||||
|
||||
const chains = useMemo(() => {
|
||||
let chains = new Map<u256, NEvent[]>();
|
||||
const chains = new Map<u256, NEvent[]>();
|
||||
parsedNotes
|
||||
?.filter((a) => a.Kind === EventKind.TextNote)
|
||||
.sort((a, b) => b.CreatedAt - a.CreatedAt)
|
||||
.forEach((v) => {
|
||||
let replyTo = v.Thread?.ReplyTo?.Event ?? v.Thread?.Root?.Event;
|
||||
const replyTo = v.Thread?.ReplyTo?.Event ?? v.Thread?.Root?.Event;
|
||||
if (replyTo) {
|
||||
if (!chains.has(replyTo)) {
|
||||
chains.set(replyTo, [v]);
|
||||
} else {
|
||||
chains.get(replyTo)!.push(v);
|
||||
unwrap(chains.get(replyTo)).push(v);
|
||||
}
|
||||
} else if (v.Tags.length > 0) {
|
||||
console.log("Not replying to anything: ", v);
|
||||
@ -370,7 +367,7 @@ export default function Thread(props: ThreadProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
let subthreadPath = [];
|
||||
const subthreadPath = [];
|
||||
let parent = getParent(urlNoteHex, chains);
|
||||
while (parent) {
|
||||
subthreadPath.unshift(parent);
|
||||
@ -414,7 +411,7 @@ export default function Thread(props: ThreadProps) {
|
||||
if (!from || !chains) {
|
||||
return;
|
||||
}
|
||||
let replies = chains.get(from);
|
||||
const replies = chains.get(from);
|
||||
if (replies) {
|
||||
return (
|
||||
<Subthread
|
||||
@ -476,6 +473,6 @@ function getReplies(from: u256, chains?: Map<u256, NEvent[]>): NEvent[] {
|
||||
if (!from || !chains) {
|
||||
return [];
|
||||
}
|
||||
let replies = chains.get(from);
|
||||
const replies = chains.get(from);
|
||||
return replies ? replies : [];
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CSSProperties, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TidalRegex } from "Const";
|
||||
|
||||
// Re-use dom parser across instances of TidalEmbed
|
||||
|
@ -83,7 +83,7 @@ export default function Timeline({
|
||||
}
|
||||
case EventKind.Reaction:
|
||||
case EventKind.Repost: {
|
||||
let eRef = e.tags.find((a) => a[0] === "e")?.at(1);
|
||||
const eRef = e.tags.find((a) => a[0] === "e")?.at(1);
|
||||
return (
|
||||
<NoteReaction
|
||||
data={e}
|
||||
|
@ -2,10 +2,10 @@ import "./Zap.css";
|
||||
import { useMemo } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useSelector } from "react-redux";
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error No types available
|
||||
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
||||
import { bytesToHex } from "@noble/hashes/utils";
|
||||
import { sha256 } from "Util";
|
||||
import { sha256, unwrap } from "Util";
|
||||
|
||||
//import { sha256 } from "Util";
|
||||
import { formatShort } from "Number";
|
||||
@ -24,15 +24,19 @@ function findTag(e: TaggedRawEvent, tag: string) {
|
||||
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: any) => section.name === "amount"
|
||||
(section: Section) => section.name === "amount"
|
||||
)?.value;
|
||||
const hash = decoded.sections.find(
|
||||
(section: any) => section.name === "description_hash"
|
||||
(section: Section) => section.name === "description_hash"
|
||||
)?.value;
|
||||
|
||||
return { amount, hash: hash ? bytesToHex(hash) : undefined };
|
||||
@ -72,7 +76,7 @@ export function parseZap(zap: TaggedRawEvent): ParsedZap {
|
||||
const { amount, hash } = getInvoice(zap);
|
||||
const zapper = hash ? getZapper(zap, hash) : { isValid: false };
|
||||
const e = findTag(zap, "e");
|
||||
const p = findTag(zap, "p")!;
|
||||
const p = unwrap(findTag(zap, "p"));
|
||||
return {
|
||||
id: zap.id,
|
||||
e,
|
||||
|
@ -6,8 +6,8 @@ import { useUserProfile } from "Feed/ProfileFeed";
|
||||
import { HexKey } from "Nostr";
|
||||
import SendSats from "Element/SendSats";
|
||||
|
||||
const ZapButton = ({ pubkey, svc }: { pubkey?: HexKey; svc?: string }) => {
|
||||
const profile = useUserProfile(pubkey!);
|
||||
const ZapButton = ({ pubkey, svc }: { pubkey: HexKey; svc?: string }) => {
|
||||
const profile = useUserProfile(pubkey);
|
||||
const [zap, setZap] = useState(false);
|
||||
const service = svc ?? (profile?.lud16 || profile?.lud06);
|
||||
|
||||
@ -15,7 +15,7 @@ const ZapButton = ({ pubkey, svc }: { pubkey?: HexKey; svc?: string }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="zap-button" onClick={(e) => setZap(true)}>
|
||||
<div className="zap-button" onClick={() => setZap(true)}>
|
||||
<FontAwesomeIcon icon={faBolt} />
|
||||
</div>
|
||||
<SendSats
|
||||
|
Reference in New Issue
Block a user