review / cleanup

This commit is contained in:
2023-02-09 12:26:54 +00:00
parent a03b385e55
commit dbae89837f
129 changed files with 678 additions and 2303 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@ export default function LoadMore({
useEffect(() => {
const t = setInterval(() => {
setTick((x) => (x += 1));
setTick(x => (x += 1));
}, 500);
return () => clearInterval(t);
}, []);

View File

@ -16,8 +16,7 @@ export default function LogoutButton() {
onClick={() => {
dispatch(logout());
navigate("/");
}}
>
}}>
<FormattedMessage {...messages.Logout} />
</button>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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())}
/>
&nbsp;@&nbsp;
<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>
)}

View File

@ -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:&nbsp;
@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]}&nbsp;</>}
{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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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