review / cleanup

This commit is contained in:
Kieran 2023-02-09 12:26:54 +00:00
parent a03b385e55
commit dbae89837f
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
129 changed files with 678 additions and 2303 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
node_modules/
.github/
.vscode/
build/
yarn-error.log

View File

@ -1 +1,5 @@
{}
{
"printWidth": 120,
"bracketSameLine": true,
"arrowParens": "avoid"
}

0
Dockerfile Normal file
View File

14
d.ts
View File

@ -12,3 +12,17 @@ declare module "*.webp" {
const value: string;
export default value;
}
declare module "light-bolt11-decoder" {
export function decode(pr?: string): ParsedInvoice;
export interface ParsedInvoice {
paymentRequest: string;
sections: Section[];
}
export interface Section {
name: string;
value: string | Uint8Array | number | undefined;
}
}

View File

@ -3,16 +3,12 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Fast nostr web ui" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; child-src 'none'; worker-src 'self'; frame-src youtube.com www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com https://open.spotify.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src wss://* 'self' https://*; img-src * data:; font-src https://fonts.gstatic.com; media-src *; script-src 'self' https://static.cloudflareinsights.com https://platform.twitter.com https://embed.tidal.com;"
/>
content="default-src 'self'; child-src 'none'; worker-src 'self'; frame-src youtube.com www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com https://open.spotify.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src wss://* 'self' https://*; img-src * data:; font-src https://fonts.gstatic.com; media-src *; script-src 'self' https://static.cloudflareinsights.com https://platform.twitter.com https://embed.tidal.com;" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/nostrich_512.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

View File

@ -18,14 +18,12 @@ export const VoidCatHost = "https://void.cat";
/**
* Kierans pubkey
*/
export const KieranPubKey =
"npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49";
export const KieranPubKey = "npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49";
/**
* Official snort account
*/
export const SnortPubKey =
"npub1sn0rtcjcf543gj4wsg7fa59s700d5ztys5ctj0g69g2x6802npjqhjjtws";
export const SnortPubKey = "npub1sn0rtcjcf543gj4wsg7fa59s700d5ztys5ctj0g69g2x6802npjqhjjtws";
/**
* Websocket re-connect timeout
@ -49,9 +47,7 @@ export const DefaultRelays = new Map<string, RelaySettings>([
/**
* Default search relays
*/
export const SearchRelays = new Map<string, RelaySettings>([
["wss://relay.nostr.band", { read: true, write: false }],
]);
export const SearchRelays = new Map<string, RelaySettings>([["wss://relay.nostr.band", { read: true, write: false }]]);
/**
* List of recommended follows for new users
@ -118,8 +114,7 @@ export const YoutubeUrlRegex =
/**
* Tweet Regex
*/
export const TweetUrlRegex =
/https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)/;
export const TweetUrlRegex = /https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)/;
/**
* Hashtag regex
@ -135,15 +130,12 @@ export const TidalRegex = /tidal\.com\/(?:browse\/)?(\w+)\/([a-z0-9-]+)/i;
/**
* SoundCloud regex
*/
export const SoundCloudRegex =
/soundcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/;
export const SoundCloudRegex = /soundcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/;
/**
* Mixcloud regex
*/
export const MixCloudRegex =
/mixcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/;
export const MixCloudRegex = /mixcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/;
export const SpotifyRegex =
/open\.spotify\.com\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/;
export const SpotifyRegex = /open\.spotify\.com\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/;

View File

@ -28,11 +28,11 @@ export class SnortDB extends Dexie {
super(NAME);
this.version(VERSION)
.stores(STORES)
.upgrade(async (tx) => {
.upgrade(async tx => {
await tx
.table("users")
.toCollection()
.modify((user) => {
.modify(user => {
user.npub = hexToBech32("npub", user.pubkey);
});
});

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

View File

@ -16,9 +16,7 @@ declare global {
nostr: {
getPublicKey: () => Promise<HexKey>;
signEvent: (event: RawEvent) => Promise<RawEvent>;
getRelays: () => Promise<
Record<string, { read: boolean; write: boolean }>
>;
getRelays: () => Promise<Record<string, { read: boolean; write: boolean }>>;
nip04: {
encrypt: (pubkey: HexKey, content: string) => Promise<string>;
decrypt: (pubkey: HexKey, content: string) => Promise<string>;
@ -28,26 +26,17 @@ declare global {
}
export default function useEventPublisher() {
const pubKey = useSelector<RootState, HexKey | undefined>(
(s) => s.login.publicKey
);
const privKey = useSelector<RootState, HexKey | undefined>(
(s) => s.login.privateKey
);
const follows = useSelector<RootState, HexKey[]>((s) => s.login.follows);
const pubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
const privKey = useSelector<RootState, HexKey | undefined>(s => s.login.privateKey);
const follows = useSelector<RootState, HexKey[]>(s => s.login.follows);
const relays = useSelector((s: RootState) => s.login.relays);
const hasNip07 = "nostr" in window;
async function signEvent(ev: NEvent): Promise<NEvent> {
if (hasNip07 && !privKey) {
ev.Id = await ev.CreateId();
const tmpEv = (await barrierNip07(() =>
window.nostr.signEvent(ev.ToObject())
)) as TaggedRawEvent;
if (!tmpEv.relays) {
tmpEv.relays = [];
}
return new NEvent(tmpEv);
const tmpEv = (await barrierNip07(() => window.nostr.signEvent(ev.ToObject()))) as RawEvent;
return new NEvent(tmpEv as TaggedRawEvent);
} else if (privKey) {
await ev.Sign(privKey);
} else {
@ -125,17 +114,15 @@ export default function useEventPublisher() {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.Lists;
ev.Tags.push(new Tag(["d", Lists.Muted], ev.Tags.length));
keys.forEach((p) => {
keys.forEach(p => {
ev.Tags.push(new Tag(["p", p], ev.Tags.length));
});
let content = "";
if (priv.length > 0) {
const ps = priv.map((p) => ["p", p]);
const ps = priv.map(p => ["p", p]);
const plaintext = JSON.stringify(ps);
if (hasNip07 && !privKey) {
content = await barrierNip07(() =>
window.nostr.nip04.encrypt(pubKey, plaintext)
);
content = await barrierNip07(() => window.nostr.nip04.encrypt(pubKey, plaintext));
} else if (privKey) {
content = await ev.EncryptData(plaintext, pubKey, privKey);
}
@ -165,11 +152,11 @@ export default function useEventPublisher() {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.ZapRequest;
if (note) {
ev.Tags.push(new Tag(["e", note], 0));
ev.Tags.push(new Tag(["e", note], ev.Tags.length));
}
ev.Tags.push(new Tag(["p", author], 0));
ev.Tags.push(new Tag(["p", author], ev.Tags.length));
const relayTag = ["relays", ...Object.keys(relays).slice(0, 10)];
ev.Tags.push(new Tag(relayTag, 0));
ev.Tags.push(new Tag(relayTag, ev.Tags.length));
processContent(ev, msg || "");
return await signEvent(ev);
}
@ -185,17 +172,7 @@ export default function useEventPublisher() {
const thread = replyTo.Thread;
if (thread) {
if (thread.Root || thread.ReplyTo) {
ev.Tags.push(
new Tag(
[
"e",
thread.Root?.Event ?? thread.ReplyTo?.Event ?? "",
"",
"root",
],
ev.Tags.length
)
);
ev.Tags.push(new Tag(["e", thread.Root?.Event ?? thread.ReplyTo?.Event ?? "", "", "root"], ev.Tags.length));
}
ev.Tags.push(new Tag(["e", replyTo.Id, "", "reply"], ev.Tags.length));
@ -243,17 +220,14 @@ export default function useEventPublisher() {
return await signEvent(ev);
}
},
addFollow: async (
pkAdd: HexKey | HexKey[],
newRelays?: Record<string, RelaySettings>
) => {
addFollow: async (pkAdd: HexKey | HexKey[], newRelays?: Record<string, RelaySettings>) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.ContactList;
ev.Content = JSON.stringify(newRelays ?? relays);
const temp = new Set(follows);
if (Array.isArray(pkAdd)) {
pkAdd.forEach((a) => temp.add(a));
pkAdd.forEach(a => temp.add(a));
} else {
temp.add(pkAdd);
}
@ -309,21 +283,14 @@ export default function useEventPublisher() {
},
decryptDm: async (note: NEvent): Promise<string | undefined> => {
if (pubKey) {
if (
note.PubKey !== pubKey &&
!note.Tags.some((a) => a.PubKey === pubKey)
) {
if (note.PubKey !== pubKey && !note.Tags.some(a => a.PubKey === pubKey)) {
return "<CANT DECRYPT>";
}
try {
const otherPubKey =
note.PubKey === pubKey
? unwrap(note.Tags.filter((a) => a.Key === "p")[0].PubKey)
: note.PubKey;
note.PubKey === pubKey ? unwrap(note.Tags.filter(a => a.Key === "p")[0].PubKey) : note.PubKey;
if (hasNip07 && !privKey) {
return await barrierNip07(() =>
window.nostr.nip04.decrypt(otherPubKey, note.Content)
);
return await barrierNip07(() => window.nostr.nip04.decrypt(otherPubKey, note.Content));
} else if (privKey) {
await note.DecryptDm(privKey, otherPubKey);
return note.Content;
@ -343,9 +310,7 @@ export default function useEventPublisher() {
try {
if (hasNip07 && !privKey) {
const cx: string = await barrierNip07(() =>
window.nostr.nip04.encrypt(to, content)
);
const cx: string = await barrierNip07(() => window.nostr.nip04.encrypt(to, content));
ev.Content = cx;
return await signEvent(ev);
} else if (privKey) {
@ -363,7 +328,7 @@ export default function useEventPublisher() {
let isNip07Busy = false;
const delay = (t: number) => {
return new Promise((resolve) => {
return new Promise(resolve => {
setTimeout(resolve, t);
});
};

View File

@ -18,11 +18,7 @@ export default function useFollowsFeed(pubkey: HexKey) {
}
export function getFollowers(feed: NoteStore, pubkey: HexKey) {
const contactLists = feed?.notes.filter(
(a) => a.kind === EventKind.ContactList && a.pubkey === pubkey
);
const pTags = contactLists?.map((a) =>
a.tags.filter((b) => b[0] === "p").map((c) => c[1])
);
const contactLists = feed?.notes.filter(a => a.kind === EventKind.ContactList && a.pubkey === pubkey);
const pTags = contactLists?.map(a => a.tags.filter(b => b[0] === "p").map(c => c[1]));
return [...new Set(pTags?.flat())];
}

View File

@ -11,9 +11,7 @@ export interface ImgProxySettings {
}
export default function useImgProxy() {
const settings = useSelector(
(s: RootState) => s.login.preferences.imgProxyConfig
);
const settings = useSelector((s: RootState) => s.login.preferences.imgProxyConfig);
const te = new TextEncoder();
function urlSafe(s: string) {
@ -34,9 +32,7 @@ export default function useImgProxy() {
if (!settings) return url;
const opt = resize ? `rs:fit:${resize}:${resize}` : "";
const urlBytes = te.encode(url);
const urlEncoded = urlSafe(
base64.encode(urlBytes, 0, urlBytes.byteLength)
);
const urlEncoded = urlSafe(base64.encode(urlBytes, 0, urlBytes.byteLength));
const path = `/${opt}/${urlEncoded}`;
const sig = await signUrl(path);
return `${new URL(settings.url).toString()}${sig}${path}`;

View File

@ -23,6 +23,7 @@ import { barrierNip07 } from "Feed/EventPublisher";
import { getMutedKeys, getNewest } from "Feed/MuteList";
import useModeration from "Hooks/useModeration";
import { unwrap } from "Util";
import { AnyAction, ThunkDispatch } from "@reduxjs/toolkit";
/**
* Managed loading data for the current logged in user
@ -103,23 +104,19 @@ export default function useLoginFeed() {
const mutedFeed = useSubscription(subMuted, { leaveOpen: true, cache: true });
useEffect(() => {
const contactList = metadataFeed.store.notes.filter(
(a) => a.kind === EventKind.ContactList
);
const metadata = metadataFeed.store.notes.filter(
(a) => a.kind === EventKind.SetMetadata
);
const contactList = metadataFeed.store.notes.filter(a => a.kind === EventKind.ContactList);
const metadata = metadataFeed.store.notes.filter(a => a.kind === EventKind.SetMetadata);
const profiles = metadata
.map((a) => mapEventToProfile(a))
.filter((a) => a !== undefined)
.map((a) => unwrap(a));
.map(a => mapEventToProfile(a))
.filter(a => a !== undefined)
.map(a => unwrap(a));
for (const cl of contactList) {
if (cl.content !== "" && cl.content !== "{}") {
const relays = JSON.parse(cl.content);
dispatch(setRelays({ relays, createdAt: cl.created_at }));
}
const pTags = cl.tags.filter((a) => a[0] === "p").map((a) => a[1]);
const pTags = cl.tags.filter(a => a[0] === "p").map(a => a[1]);
dispatch(setFollows({ keys: pTags, createdAt: cl.created_at }));
}
@ -145,17 +142,13 @@ export default function useLoginFeed() {
useEffect(() => {
const replies = notificationFeed.store.notes.filter(
(a) =>
a.kind === EventKind.TextNote &&
!isMuted(a.pubkey) &&
a.created_at > readNotifications
a => a.kind === EventKind.TextNote && !isMuted(a.pubkey) && a.created_at > readNotifications
);
replies.forEach((nx) => {
replies.forEach(nx => {
dispatch(setLatestNotifications(nx.created_at));
makeNotification(db, nx).then((notification) => {
makeNotification(db, nx).then(notification => {
if (notification) {
// @ts-expect-error This is typed wrong, but I don't have the time to fix it right now
dispatch(sendNotification(notification));
(dispatch as ThunkDispatch<RootState, undefined, AnyAction>)(sendNotification(notification));
}
});
});
@ -166,19 +159,12 @@ export default function useLoginFeed() {
dispatch(setMuted(muted));
const newest = getNewest(mutedFeed.store.notes);
if (
newest &&
newest.content.length > 0 &&
pubKey &&
newest.created_at > latestMuted
) {
if (newest && newest.content.length > 0 && pubKey && newest.created_at > latestMuted) {
decryptBlocked(newest, pubKey, privKey)
.then((plaintext) => {
.then(plaintext => {
try {
const blocked = JSON.parse(plaintext);
const keys = blocked
.filter((p: string) => p && p.length === 2 && p[0] === "p")
.map((p: string) => p[1]);
const keys = blocked.filter((p: string) => p && p.length === 2 && p[0] === "p").map((p: string) => p[1]);
dispatch(
setBlocked({
keys,
@ -189,29 +175,21 @@ export default function useLoginFeed() {
console.debug("Couldn't parse JSON");
}
})
.catch((error) => console.warn(error));
.catch(error => console.warn(error));
}
}, [dispatch, mutedFeed.store]);
useEffect(() => {
const dms = dmsFeed.store.notes.filter(
(a) => a.kind === EventKind.DirectMessage
);
const dms = dmsFeed.store.notes.filter(a => a.kind === EventKind.DirectMessage);
dispatch(addDirectMessage(dms));
}, [dispatch, dmsFeed.store]);
}
async function decryptBlocked(
raw: TaggedRawEvent,
pubKey: HexKey,
privKey?: HexKey
) {
async function decryptBlocked(raw: TaggedRawEvent, pubKey: HexKey, privKey?: HexKey) {
const ev = new Event(raw);
if (pubKey && privKey) {
return await ev.DecryptData(raw.content, privKey, pubKey);
} else {
return await barrierNip07(() =>
window.nostr.nip04.decrypt(pubKey, raw.content)
);
return await barrierNip07(() => window.nostr.nip04.decrypt(pubKey, raw.content));
}
}

View File

@ -34,7 +34,7 @@ export function getMutedKeys(rawNotes: TaggedRawEvent[]): {
const newest = getNewest(rawNotes);
if (newest) {
const { created_at, tags } = newest;
const keys = tags.filter((t) => t[0] === "p").map((t) => t[1]);
const keys = tags.filter(t => t[0] === "p").map(t => t[1]);
return {
keys,
createdAt: created_at,
@ -44,8 +44,6 @@ export function getMutedKeys(rawNotes: TaggedRawEvent[]): {
}
export function getMuted(feed: NoteStore, pubkey: HexKey): HexKey[] {
const lists = feed?.notes.filter(
(a) => a.kind === EventKind.Lists && a.pubkey === pubkey
);
const lists = feed?.notes.filter(a => a.kind === EventKind.Lists && a.pubkey === pubkey);
return getMutedKeys(lists).keys;
}

View File

@ -4,7 +4,7 @@ import { useKey, useKeys } from "State/Users/Hooks";
import { HexKey } from "Nostr";
import { System } from "Nostr/System";
export function useUserProfile(pubKey: HexKey): MetadataCache | undefined {
export function useUserProfile(pubKey?: HexKey): MetadataCache | undefined {
const users = useKey(pubKey);
useEffect(() => {
@ -17,9 +17,7 @@ export function useUserProfile(pubKey: HexKey): MetadataCache | undefined {
return users;
}
export function useUserProfiles(
pubKeys: Array<HexKey>
): Map<HexKey, MetadataCache> | undefined {
export function useUserProfiles(pubKeys?: Array<HexKey>): Map<HexKey, MetadataCache> | undefined {
const users = useKeys(pubKeys);
useEffect(() => {

View File

@ -25,7 +25,7 @@ function notesReducer(state: NoteStore, arg: ReducerArg) {
if (arg.type === "END") {
return {
notes: state.notes,
end: arg.end ?? false,
end: arg.end ?? true,
} as NoteStore;
}
@ -40,8 +40,8 @@ function notesReducer(state: NoteStore, arg: ReducerArg) {
if (!(evs instanceof Array)) {
evs = evs === undefined ? [] : [evs];
}
const existingIds = new Set(state.notes.map((a) => a.id));
evs = evs.filter((a) => !existingIds.has(a.id));
const existingIds = new Set(state.notes.map(a => a.id));
evs = evs.filter(a => !existingIds.has(a.id));
if (evs.length === 0) {
return state;
}
@ -99,7 +99,7 @@ export default function useSubscription(
if (useCache) {
// preload notes from db
PreloadNotes(subDebounce.Id)
.then((ev) => {
.then(ev => {
dispatch({
type: "EVENT",
ev: ev,
@ -107,7 +107,7 @@ export default function useSubscription(
})
.catch(console.warn);
}
subDebounce.OnEvent = (e) => {
subDebounce.OnEvent = e => {
dispatch({
type: "EVENT",
ev: e,
@ -117,7 +117,7 @@ export default function useSubscription(
}
};
subDebounce.OnEnd = (c) => {
subDebounce.OnEnd = c => {
if (!(options?.leaveOpen ?? false)) {
c.RemoveSubscription(subDebounce.Id);
if (subDebounce.IsFinished()) {
@ -149,7 +149,7 @@ export default function useSubscription(
useEffect(() => {
return debounce(DebounceMs, () => {
setDebounceOutput((s) => (s += 1));
setDebounceOutput(s => (s += 1));
});
}, [state]);
@ -175,23 +175,15 @@ const PreloadNotes = async (id: string): Promise<TaggedRawEvent[]> => {
const feed = await db.feeds.get(id);
if (feed) {
const events = await db.events.bulkGet(feed.ids);
return events.filter((a) => a !== undefined).map((a) => unwrap(a));
return events.filter(a => a !== undefined).map(a => unwrap(a));
}
return [];
};
const TrackNotesInFeed = async (id: string, notes: TaggedRawEvent[]) => {
const existing = await db.feeds.get(id);
const ids = Array.from(
new Set([...(existing?.ids || []), ...notes.map((a) => a.id)])
);
const since = notes.reduce(
(acc, v) => (acc > v.created_at ? v.created_at : acc),
+Infinity
);
const until = notes.reduce(
(acc, v) => (acc < v.created_at ? v.created_at : acc),
-Infinity
);
const ids = Array.from(new Set([...(existing?.ids || []), ...notes.map(a => a.id)]));
const since = notes.reduce((acc, v) => (acc > v.created_at ? v.created_at : acc), +Infinity);
const until = notes.reduce((acc, v) => (acc < v.created_at ? v.created_at : acc), -Infinity);
await db.feeds.put({ id, ids, since, until });
};

View File

@ -10,14 +10,12 @@ import { debounce } from "Util";
export default function useThreadFeed(id: u256) {
const [trackingEvents, setTrackingEvent] = useState<u256[]>([id]);
const pref = useSelector<RootState, UserPreferences>(
(s) => s.login.preferences
);
const pref = useSelector<RootState, UserPreferences>(s => s.login.preferences);
function addId(id: u256[]) {
setTrackingEvent((s) => {
setTrackingEvent(s => {
const orig = new Set(s);
if (id.some((a) => !orig.has(a))) {
if (id.some(a => !orig.has(a))) {
const tmp = new Set([...s, ...id]);
return Array.from(tmp);
} else {
@ -35,13 +33,7 @@ export default function useThreadFeed(id: u256) {
const subRelated = new Subscriptions();
subRelated.Kinds = new Set(
pref.enableReactions
? [
EventKind.Reaction,
EventKind.TextNote,
EventKind.Deletion,
EventKind.Repost,
EventKind.ZapReceipt,
]
? [EventKind.Reaction, EventKind.TextNote, EventKind.Deletion, EventKind.Repost, EventKind.ZapReceipt]
: [EventKind.TextNote]
);
subRelated.ETags = thisSub.Ids;
@ -55,15 +47,13 @@ export default function useThreadFeed(id: u256) {
useEffect(() => {
if (main.store) {
return debounce(200, () => {
const mainNotes = main.store.notes.filter(
(a) => a.kind === EventKind.TextNote
);
const mainNotes = main.store.notes.filter(a => a.kind === EventKind.TextNote);
const eTags = mainNotes
.filter((a) => a.kind === EventKind.TextNote)
.map((a) => a.tags.filter((b) => b[0] === "e").map((b) => b[1]))
.filter(a => a.kind === EventKind.TextNote)
.map(a => a.tags.filter(b => b[0] === "e").map(b => b[1]))
.flat();
const ids = mainNotes.map((a) => a.id);
const ids = mainNotes.map(a => a.id);
const allEvents = new Set([...eTags, ...ids]);
addId(Array.from(allEvents));
});

View File

@ -19,19 +19,14 @@ export interface TimelineSubject {
items: string[];
}
export default function useTimelineFeed(
subject: TimelineSubject,
options: TimelineFeedOptions
) {
export default function useTimelineFeed(subject: TimelineSubject, options: TimelineFeedOptions) {
const now = unixNow();
const [window] = useState<number>(options.window ?? 60 * 60);
const [until, setUntil] = useState<number>(now);
const [since, setSince] = useState<number>(now - window);
const [trackingEvents, setTrackingEvent] = useState<u256[]>([]);
const [trackingParentEvents, setTrackingParentEvents] = useState<u256[]>([]);
const pref = useSelector<RootState, UserPreferences>(
(s) => s.login.preferences
);
const pref = useSelector<RootState, UserPreferences>(s => s.login.preferences);
const createSub = useCallback(() => {
if (subject.type !== "global" && subject.items.length === 0) {
@ -116,12 +111,7 @@ export default function useTimelineFeed(
if (trackingEvents.length > 0 && pref.enableReactions) {
sub = new Subscriptions();
sub.Id = `timeline-related:${subject.type}`;
sub.Kinds = new Set([
EventKind.Reaction,
EventKind.Repost,
EventKind.Deletion,
EventKind.ZapReceipt,
]);
sub.Kinds = new Set([EventKind.Reaction, EventKind.Repost, EventKind.Deletion, EventKind.ZapReceipt]);
sub.ETags = new Set(trackingEvents);
}
return sub ?? null;
@ -143,21 +133,21 @@ export default function useTimelineFeed(
useEffect(() => {
if (main.store.notes.length > 0) {
setTrackingEvent((s) => {
const ids = main.store.notes.map((a) => a.id);
if (ids.some((a) => !s.includes(a))) {
setTrackingEvent(s => {
const ids = main.store.notes.map(a => a.id);
if (ids.some(a => !s.includes(a))) {
return Array.from(new Set([...s, ...ids]));
}
return s;
});
const reposts = main.store.notes
.filter((a) => a.kind === EventKind.Repost && a.content === "")
.map((a) => a.tags.find((b) => b[0] === "e"))
.filter((a) => a)
.map((a) => unwrap(a)[1]);
.filter(a => a.kind === EventKind.Repost && a.content === "")
.map(a => a.tags.find(b => b[0] === "e"))
.filter(a => a)
.map(a => unwrap(a)[1]);
if (reposts.length > 0) {
setTrackingParentEvents((s) => {
if (reposts.some((a) => !s.includes(a))) {
setTrackingParentEvents(s => {
if (reposts.some(a => !s.includes(a))) {
const temp = new Set([...s, ...reposts]);
return Array.from(temp);
}
@ -175,14 +165,11 @@ export default function useTimelineFeed(
loadMore: () => {
console.debug("Timeline load more!");
if (options.method === "LIMIT_UNTIL") {
const oldest = main.store.notes.reduce(
(acc, v) => (acc = v.created_at < acc ? v.created_at : acc),
unixNow()
);
const oldest = main.store.notes.reduce((acc, v) => (acc = v.created_at < acc ? v.created_at : acc), unixNow());
setUntil(oldest);
} else {
setUntil((s) => s - window);
setSince((s) => s - window);
setUntil(s => s - window);
setSince(s => s - window);
}
},
showLatest: () => {

View File

@ -29,7 +29,7 @@ export default function useModeration() {
}
function unmute(id: HexKey) {
const newMuted = muted.filter((p) => p !== id);
const newMuted = muted.filter(p => p !== id);
dispatch(
setMuted({
createdAt: new Date().getTime(),
@ -40,7 +40,7 @@ export default function useModeration() {
}
function unblock(id: HexKey) {
const newBlocked = blocked.filter((p) => p !== id);
const newBlocked = blocked.filter(p => p !== id);
dispatch(
setBlocked({
createdAt: new Date().getTime(),

View File

@ -1,12 +1,6 @@
const ArrowBack = () => {
return (
<svg
width="16"
height="13"
viewBox="0 0 16 13"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="16" height="13" viewBox="0 0 16 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M14.6667 6.5H1.33334M1.33334 6.5L6.33334 11.5M1.33334 6.5L6.33334 1.5"
stroke="currentColor"

View File

@ -1,19 +1,7 @@
const ArrowFront = () => {
return (
<svg
width="8"
height="14"
viewBox="0 0 8 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1 13L7 7L1 1"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<svg width="8" height="14" viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 13L7 7L1 1" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
};

View File

@ -1,12 +1,6 @@
const Attachment = () => {
return (
<svg
width="21"
height="22"
viewBox="0 0 21 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="21" height="22" viewBox="0 0 21 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M19.1525 9.89945L10.1369 18.9151C8.08662 20.9653 4.7625 20.9653 2.71225 18.9151C0.661997 16.8648 0.661998 13.5407 2.71225 11.4904L11.7279 2.47483C13.0947 1.108 15.3108 1.108 16.6776 2.47483C18.0444 3.84167 18.0444 6.05775 16.6776 7.42458L8.01555 16.0866C7.33213 16.7701 6.22409 16.7701 5.54068 16.0866C4.85726 15.4032 4.85726 14.2952 5.54068 13.6118L13.1421 6.01037"
stroke="currentColor"

View File

@ -1,12 +1,6 @@
const Bell = () => {
return (
<svg
width="20"
height="23"
viewBox="0 0 20 23"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="20" height="23" viewBox="0 0 20 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7.35419 20.5C8.05933 21.1224 8.98557 21.5 10 21.5C11.0145 21.5 11.9407 21.1224 12.6458 20.5M16 7.5C16 5.9087 15.3679 4.38258 14.2427 3.25736C13.1174 2.13214 11.5913 1.5 10 1.5C8.40872 1.5 6.8826 2.13214 5.75738 3.25736C4.63216 4.38258 4.00002 5.9087 4.00002 7.5C4.00002 10.5902 3.22049 12.706 2.34968 14.1054C1.61515 15.2859 1.24788 15.8761 1.26134 16.0408C1.27626 16.2231 1.31488 16.2926 1.46179 16.4016C1.59448 16.5 2.19261 16.5 3.38887 16.5H16.6112C17.8074 16.5 18.4056 16.5 18.5382 16.4016C18.6852 16.2926 18.7238 16.2231 18.7387 16.0408C18.7522 15.8761 18.3849 15.2859 17.6504 14.1054C16.7795 12.706 16 10.5902 16 7.5Z"
stroke="currentColor"

View File

@ -2,21 +2,8 @@ import IconProps from "./IconProps";
const Check = (props: IconProps) => {
return (
<svg
width="18"
height="13"
viewBox="0 0 18 13"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M17 1L6 12L1 7"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<svg width="18" height="13" viewBox="0 0 18 13" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M17 1L6 12L1 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
};

View File

@ -2,14 +2,7 @@ import IconProps from "./IconProps";
const Close = (props: IconProps) => {
return (
<svg
width="8"
height="8"
viewBox="0 0 8 8"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M7.33332 0.666992L0.666656 7.33366M0.666656 0.666992L7.33332 7.33366"
stroke="currentColor"

View File

@ -2,14 +2,7 @@ import IconProps from "./IconProps";
const Copy = (props: IconProps) => {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M5.33331 5.33398V3.46732C5.33331 2.72058 5.33331 2.34721 5.47864 2.062C5.60647 1.81111 5.81044 1.60714 6.06133 1.47931C6.34654 1.33398 6.71991 1.33398 7.46665 1.33398H12.5333C13.28 1.33398 13.6534 1.33398 13.9386 1.47931C14.1895 1.60714 14.3935 1.81111 14.5213 2.062C14.6666 2.34721 14.6666 2.72058 14.6666 3.46732V8.53398C14.6666 9.28072 14.6666 9.65409 14.5213 9.9393C14.3935 10.1902 14.1895 10.3942 13.9386 10.522C13.6534 10.6673 13.28 10.6673 12.5333 10.6673H10.6666M3.46665 14.6673H8.53331C9.28005 14.6673 9.65342 14.6673 9.93863 14.522C10.1895 14.3942 10.3935 14.1902 10.5213 13.9393C10.6666 13.6541 10.6666 13.2807 10.6666 12.534V7.46732C10.6666 6.72058 10.6666 6.34721 10.5213 6.062C10.3935 5.81111 10.1895 5.60714 9.93863 5.47931C9.65342 5.33398 9.28005 5.33398 8.53331 5.33398H3.46665C2.71991 5.33398 2.34654 5.33398 2.06133 5.47931C1.81044 5.60714 1.60647 5.81111 1.47864 6.062C1.33331 6.34721 1.33331 6.72058 1.33331 7.46732V12.534C1.33331 13.2807 1.33331 13.6541 1.47864 13.9393C1.60647 14.1902 1.81044 14.3942 2.06133 14.522C2.34654 14.6673 2.71991 14.6673 3.46665 14.6673Z"
stroke="currentColor"

View File

@ -2,14 +2,7 @@ import IconProps from "./IconProps";
const Dislike = (props: IconProps) => {
return (
<svg
width="19"
height="20"
viewBox="0 0 19 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg width="19" height="20" viewBox="0 0 19 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M13.1667 1.66667V10.8333M17.3333 8.16667V4.33334C17.3333 3.39992 17.3333 2.93321 17.1517 2.57669C16.9919 2.26308 16.7369 2.00812 16.4233 1.84833C16.0668 1.66667 15.6001 1.66667 14.6667 1.66667H5.76501C4.54711 1.66667 3.93816 1.66667 3.44632 1.88953C3.01284 2.08595 2.64442 2.40202 2.38437 2.8006C2.08931 3.25283 1.99672 3.8547 1.81153 5.05844L1.37563 7.89178C1.13137 9.47943 1.00925 10.2733 1.24484 10.8909C1.45162 11.4331 1.84054 11.8864 2.34494 12.1732C2.91961 12.5 3.72278 12.5 5.32912 12.5H6C6.46671 12.5 6.70007 12.5 6.87833 12.5908C7.03513 12.6707 7.16261 12.7982 7.24251 12.955C7.33334 13.1333 7.33334 13.3666 7.33334 13.8333V16.2785C7.33334 17.4133 8.25333 18.3333 9.3882 18.3333C9.65889 18.3333 9.90419 18.1739 10.0141 17.9266L12.8148 11.6252C12.9421 11.3385 13.0058 11.1952 13.1065 11.0902C13.1955 10.9973 13.3048 10.9263 13.4258 10.8827C13.5627 10.8333 13.7195 10.8333 14.0332 10.8333H14.6667C15.6001 10.8333 16.0668 10.8333 16.4233 10.6517C16.7369 10.4919 16.9919 10.2369 17.1517 9.92332C17.3333 9.5668 17.3333 9.10009 17.3333 8.16667Z"
stroke="currentColor"

View File

@ -1,12 +1,6 @@
const Dots = () => {
return (
<svg
width="4"
height="16"
viewBox="0 0 4 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="4" height="16" viewBox="0 0 4 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M1.99996 8.86865C2.4602 8.86865 2.83329 8.49556 2.83329 8.03532C2.83329 7.57508 2.4602 7.20199 1.99996 7.20199C1.53972 7.20199 1.16663 7.57508 1.16663 8.03532C1.16663 8.49556 1.53972 8.86865 1.99996 8.86865Z"
stroke="currentColor"

View File

@ -2,14 +2,7 @@ import type IconProps from "./IconProps";
const Envelope = (props: IconProps) => {
return (
<svg
width="22"
height="19"
viewBox="0 0 22 19"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg width="22" height="19" viewBox="0 0 22 19" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M1 4.5L9.16492 10.2154C9.82609 10.6783 10.1567 10.9097 10.5163 10.9993C10.8339 11.0785 11.1661 11.0785 11.4837 10.9993C11.8433 10.9097 12.1739 10.6783 12.8351 10.2154L21 4.5M5.8 17.5H16.2C17.8802 17.5 18.7202 17.5 19.362 17.173C19.9265 16.8854 20.3854 16.4265 20.673 15.862C21 15.2202 21 14.3802 21 12.7V6.3C21 4.61984 21 3.77976 20.673 3.13803C20.3854 2.57354 19.9265 2.1146 19.362 1.82698C18.7202 1.5 17.8802 1.5 16.2 1.5H5.8C4.11984 1.5 3.27976 1.5 2.63803 1.82698C2.07354 2.1146 1.6146 2.57354 1.32698 3.13803C1 3.77976 1 4.61984 1 6.3V12.7C1 14.3802 1 15.2202 1.32698 15.862C1.6146 16.4265 2.07354 16.8854 2.63803 17.173C3.27976 17.5 4.11984 17.5 5.8 17.5Z"
stroke="currentColor"

View File

@ -2,14 +2,7 @@ import IconProps from "./IconProps";
const Gear = (props: IconProps) => {
return (
<svg
width="20"
height="22"
viewBox="0 0 20 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M7.39504 18.3711L7.97949 19.6856C8.15323 20.0768 8.43676 20.4093 8.79571 20.6426C9.15466 20.8759 9.5736 21.0001 10.0017 21C10.4298 21.0001 10.8488 20.8759 11.2077 20.6426C11.5667 20.4093 11.8502 20.0768 12.0239 19.6856L12.6084 18.3711C12.8164 17.9047 13.1664 17.5159 13.6084 17.26C14.0532 17.0034 14.5677 16.8941 15.0784 16.9478L16.5084 17.1C16.934 17.145 17.3636 17.0656 17.7451 16.8713C18.1265 16.6771 18.4434 16.3763 18.6573 16.0056C18.8714 15.635 18.9735 15.2103 18.951 14.7829C18.9285 14.3555 18.7825 13.9438 18.5306 13.5978L17.6839 12.4344C17.3825 12.0171 17.2214 11.5148 17.2239 11C17.2238 10.4866 17.3864 9.98635 17.6884 9.57111L18.535 8.40778C18.7869 8.06175 18.933 7.65007 18.9554 7.22267C18.9779 6.79528 18.8759 6.37054 18.6617 6C18.4478 5.62923 18.1309 5.32849 17.7495 5.13423C17.3681 4.93997 16.9385 4.86053 16.5128 4.90556L15.0828 5.05778C14.5722 5.11141 14.0576 5.00212 13.6128 4.74556C13.1699 4.48825 12.8199 4.09736 12.6128 3.62889L12.0239 2.31444C11.8502 1.92317 11.5667 1.59072 11.2077 1.3574C10.8488 1.12408 10.4298 0.99993 10.0017 1C9.5736 0.99993 9.15466 1.12408 8.79571 1.3574C8.43676 1.59072 8.15323 1.92317 7.97949 2.31444L7.39504 3.62889C7.18797 4.09736 6.83792 4.48825 6.39504 4.74556C5.95026 5.00212 5.43571 5.11141 4.92504 5.05778L3.4906 4.90556C3.06493 4.86053 2.63534 4.93997 2.25391 5.13423C1.87249 5.32849 1.55561 5.62923 1.34171 6C1.12753 6.37054 1.02549 6.79528 1.04798 7.22267C1.07046 7.65007 1.2165 8.06175 1.46838 8.40778L2.31504 9.57111C2.61698 9.98635 2.77958 10.4866 2.77949 11C2.77958 11.5134 2.61698 12.0137 2.31504 12.4289L1.46838 13.5922C1.2165 13.9382 1.07046 14.3499 1.04798 14.7773C1.02549 15.2047 1.12753 15.6295 1.34171 16C1.55582 16.3706 1.87274 16.6712 2.25411 16.8654C2.63548 17.0596 3.06496 17.1392 3.4906 17.0944L4.9206 16.9422C5.43127 16.8886 5.94581 16.9979 6.3906 17.2544C6.83513 17.511 7.18681 17.902 7.39504 18.3711Z"
stroke="currentColor"

View File

@ -2,14 +2,7 @@ import IconProps from "./IconProps";
const Heart = (props: IconProps) => {
return (
<svg
width="20"
height="18"
viewBox="0 0 20 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"

View File

@ -1,12 +1,6 @@
const Link = () => {
return (
<svg
width="22"
height="22"
viewBox="0 0 22 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M8.99996 12C9.42941 12.5742 9.97731 13.0492 10.6065 13.393C11.2357 13.7367 11.9315 13.9411 12.6466 13.9924C13.3617 14.0436 14.0795 13.9404 14.7513 13.6898C15.4231 13.4392 16.0331 13.0471 16.54 12.54L19.54 9.54003C20.4507 8.59702 20.9547 7.334 20.9433 6.02302C20.9319 4.71204 20.4061 3.45797 19.479 2.53093C18.552 1.60389 17.2979 1.07805 15.987 1.06666C14.676 1.05526 13.413 1.55924 12.47 2.47003L10.75 4.18003M13 10C12.5705 9.4259 12.0226 8.95084 11.3934 8.60709C10.7642 8.26333 10.0684 8.05891 9.3533 8.00769C8.63816 7.95648 7.92037 8.05966 7.24861 8.31025C6.57685 8.56083 5.96684 8.95296 5.45996 9.46003L2.45996 12.46C1.54917 13.403 1.04519 14.666 1.05659 15.977C1.06798 17.288 1.59382 18.5421 2.52086 19.4691C3.4479 20.3962 4.70197 20.922 6.01295 20.9334C7.32393 20.9448 8.58694 20.4408 9.52995 19.53L11.24 17.82"
stroke="currentColor"

View File

@ -1,12 +1,6 @@
const Logout = () => {
return (
<svg
width="22"
height="20"
viewBox="0 0 22 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M17 6L21 10M21 10L17 14M21 10H8M14 2.20404C12.7252 1.43827 11.2452 1 9.66667 1C4.8802 1 1 5.02944 1 10C1 14.9706 4.8802 19 9.66667 19C11.2452 19 12.7252 18.5617 14 17.796"
stroke="currentColor"

View File

@ -1,19 +1,7 @@
const Plus = () => {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 1V15M1 8H15"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 1V15M1 8H15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
};

View File

@ -2,14 +2,7 @@ import IconProps from "./IconProps";
const Profile = (props: IconProps) => {
return (
<svg
width="22"
height="22"
viewBox="0 0 22 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M14 8H14.01M8 8H8.01M21 11C21 16.5228 16.5228 21 11 21C5.47715 21 1 16.5228 1 11C1 5.47715 5.47715 1 11 1C16.5228 1 21 5.47715 21 11ZM14.5 8C14.5 8.27614 14.2761 8.5 14 8.5C13.7239 8.5 13.5 8.27614 13.5 8C13.5 7.72386 13.7239 7.5 14 7.5C14.2761 7.5 14.5 7.72386 14.5 8ZM8.5 8C8.5 8.27614 8.27614 8.5 8 8.5C7.72386 8.5 7.5 8.27614 7.5 8C7.5 7.72386 7.72386 7.5 8 7.5C8.27614 7.5 8.5 7.72386 8.5 8ZM11 16.5C13.5005 16.5 15.5 14.667 15.5 13H6.5C6.5 14.667 8.4995 16.5 11 16.5Z"
stroke="currentColor"

View File

@ -2,14 +2,7 @@ import IconProps from "./IconProps";
const Qr = (props: IconProps) => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M4.5 4.5H4.51M15.5 4.5H15.51M4.5 15.5H4.51M11 11H11.01M15.5 15.5H15.51M15 19H19V15M12 14.5V19M19 12H14.5M13.6 8H17.4C17.9601 8 18.2401 8 18.454 7.89101C18.6422 7.79513 18.7951 7.64215 18.891 7.45399C19 7.24008 19 6.96005 19 6.4V2.6C19 2.03995 19 1.75992 18.891 1.54601C18.7951 1.35785 18.6422 1.20487 18.454 1.10899C18.2401 1 17.9601 1 17.4 1H13.6C13.0399 1 12.7599 1 12.546 1.10899C12.3578 1.20487 12.2049 1.35785 12.109 1.54601C12 1.75992 12 2.03995 12 2.6V6.4C12 6.96005 12 7.24008 12.109 7.45399C12.2049 7.64215 12.3578 7.79513 12.546 7.89101C12.7599 8 13.0399 8 13.6 8ZM2.6 8H6.4C6.96005 8 7.24008 8 7.45399 7.89101C7.64215 7.79513 7.79513 7.64215 7.89101 7.45399C8 7.24008 8 6.96005 8 6.4V2.6C8 2.03995 8 1.75992 7.89101 1.54601C7.79513 1.35785 7.64215 1.20487 7.45399 1.10899C7.24008 1 6.96005 1 6.4 1H2.6C2.03995 1 1.75992 1 1.54601 1.10899C1.35785 1.20487 1.20487 1.35785 1.10899 1.54601C1 1.75992 1 2.03995 1 2.6V6.4C1 6.96005 1 7.24008 1.10899 7.45399C1.20487 7.64215 1.35785 7.79513 1.54601 7.89101C1.75992 8 2.03995 8 2.6 8ZM2.6 19H6.4C6.96005 19 7.24008 19 7.45399 18.891C7.64215 18.7951 7.79513 18.6422 7.89101 18.454C8 18.2401 8 17.9601 8 17.4V13.6C8 13.0399 8 12.7599 7.89101 12.546C7.79513 12.3578 7.64215 12.2049 7.45399 12.109C7.24008 12 6.96005 12 6.4 12H2.6C2.03995 12 1.75992 12 1.54601 12.109C1.35785 12.2049 1.20487 12.3578 1.10899 12.546C1 12.7599 1 13.0399 1 13.6V17.4C1 17.9601 1 18.2401 1.10899 18.454C1.20487 18.6422 1.35785 18.7951 1.54601 18.891C1.75992 19 2.03995 19 2.6 19Z"
stroke="currentColor"

View File

@ -2,14 +2,7 @@ import IconProps from "./IconProps";
const Relay = (props: IconProps) => {
return (
<svg
width="22"
height="22"
viewBox="0 0 22 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M21 9.5L20.5256 5.70463C20.3395 4.21602 20.2465 3.47169 19.8961 2.9108C19.5875 2.41662 19.1416 2.02301 18.613 1.77804C18.013 1.5 17.2629 1.5 15.7626 1.5H6.23735C4.73714 1.5 3.98704 1.5 3.38702 1.77804C2.85838 2.02301 2.4125 2.41662 2.10386 2.9108C1.75354 3.47169 1.6605 4.21601 1.47442 5.70463L1 9.5M4.5 13.5H17.5M4.5 13.5C2.567 13.5 1 11.933 1 10C1 8.067 2.567 6.5 4.5 6.5H17.5C19.433 6.5 21 8.067 21 10C21 11.933 19.433 13.5 17.5 13.5M4.5 13.5C2.567 13.5 1 15.067 1 17C1 18.933 2.567 20.5 4.5 20.5H17.5C19.433 20.5 21 18.933 21 17C21 15.067 19.433 13.5 17.5 13.5M5 10H5.01M5 17H5.01M11 10H17M11 17H17"
stroke="currentColor"

View File

@ -1,12 +1,6 @@
const Reply = () => {
return (
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M1.5 5.5C1.5 4.09987 1.5 3.3998 1.77248 2.86502C2.01217 2.39462 2.39462 2.01217 2.86502 1.77248C3.3998 1.5 4.09987 1.5 5.5 1.5H12.5C13.9001 1.5 14.6002 1.5 15.135 1.77248C15.6054 2.01217 15.9878 2.39462 16.2275 2.86502C16.5 3.3998 16.5 4.09987 16.5 5.5V10C16.5 11.4001 16.5 12.1002 16.2275 12.635C15.9878 13.1054 15.6054 13.4878 15.135 13.7275C14.6002 14 13.9001 14 12.5 14H10.4031C9.88308 14 9.62306 14 9.37435 14.051C9.15369 14.0963 8.94017 14.1712 8.73957 14.2737C8.51347 14.3892 8.31043 14.5517 7.90434 14.8765L5.91646 16.4668C5.56973 16.7442 5.39636 16.8829 5.25045 16.8831C5.12356 16.8832 5.00352 16.8255 4.92436 16.7263C4.83333 16.6123 4.83333 16.3903 4.83333 15.9463V14C4.05836 14 3.67087 14 3.35295 13.9148C2.49022 13.6836 1.81635 13.0098 1.58519 12.147C1.5 11.8291 1.5 11.4416 1.5 10.6667V5.5Z"
stroke="currentColor"

View File

@ -1,12 +1,6 @@
const Search = () => {
return (
<svg
width="20"
height="21"
viewBox="0 0 20 21"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M19 19.5L14.65 15.15M17 9.5C17 13.9183 13.4183 17.5 9 17.5C4.58172 17.5 1 13.9183 1 9.5C1 5.08172 4.58172 1.5 9 1.5C13.4183 1.5 17 5.08172 17 9.5Z"
stroke="currentColor"

View File

@ -2,14 +2,7 @@ import type IconProps from "./IconProps";
const Zap = (props: IconProps) => {
return (
<svg
width="16"
height="20"
viewBox="0 0 16 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M8.8333 1.70166L1.41118 10.6082C1.12051 10.957 0.975169 11.1314 0.972948 11.2787C0.971017 11.4068 1.02808 11.5286 1.12768 11.6091C1.24226 11.7017 1.46928 11.7017 1.92333 11.7017H7.99997L7.16663 18.3683L14.5888 9.46178C14.8794 9.11297 15.0248 8.93857 15.027 8.79128C15.0289 8.66323 14.9719 8.54141 14.8723 8.46092C14.7577 8.36833 14.5307 8.36833 14.0766 8.36833H7.99997L8.8333 1.70166Z"
stroke="currentColor"

View File

@ -2,14 +2,7 @@ import type IconProps from "./IconProps";
const ZapCircle = (props: IconProps) => {
return (
<svg
width="33"
height="32"
viewBox="0 0 33 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"

View File

@ -26,11 +26,7 @@ const getMessages = (locale: string) => {
export const IntlProvider = ({ children }: { children: ReactNode }) => {
const getLocale = () => {
return (
(navigator.languages && navigator.languages[0]) ||
navigator.language ||
DEFAULT_LOCALE
);
return (navigator.languages && navigator.languages[0]) || navigator.language || DEFAULT_LOCALE;
};
const locale = getLocale();

View File

@ -69,21 +69,14 @@ export class ServiceProvider {
return await this._GetJson("/config.json");
}
async CheckAvailable(
handle: string,
domain: string
): Promise<HandleAvailability | ServiceError> {
async CheckAvailable(handle: string, domain: string): Promise<HandleAvailability | ServiceError> {
return await this._GetJson("/registration/availability", "POST", {
name: handle,
domain,
});
}
async RegisterHandle(
handle: string,
domain: string,
pubkey: string
): Promise<HandleRegisterResponse | ServiceError> {
async RegisterHandle(handle: string, domain: string, pubkey: string): Promise<HandleRegisterResponse | ServiceError> {
return await this._GetJson("/registration/register", "PUT", {
name: handle,
domain,
@ -92,17 +85,10 @@ export class ServiceProvider {
});
}
async CheckRegistration(
token: string
): Promise<CheckRegisterResponse | ServiceError> {
return await this._GetJson(
"/registration/register/check",
"POST",
undefined,
{
authorization: token,
}
);
async CheckRegistration(token: string): Promise<CheckRegisterResponse | ServiceError> {
return await this._GetJson("/registration/register/check", "POST", undefined, {
authorization: token,
});
}
async _GetJson<T>(
path: string,

View File

@ -5,7 +5,7 @@ import { Subscriptions } from "Nostr/Subscriptions";
import { default as NEvent } from "Nostr/Event";
import { DefaultConnectTimeout } from "Const";
import { ConnectionStats } from "Nostr/ConnectionStats";
import { RawEvent, TaggedRawEvent, u256 } from "Nostr";
import { RawEvent, RawReqFilter, TaggedRawEvent, u256 } from "Nostr";
import { RelayInfo } from "./RelayInfo";
import Nips from "./Nips";
import { System } from "./System";
@ -40,7 +40,7 @@ export default class Connection {
Id: string;
Address: string;
Socket: WebSocket | null;
Pending: Subscriptions[];
Pending: Array<RawReqFilter>;
Subscriptions: Map<string, Subscriptions>;
Settings: RelaySettings;
Info?: RelayInfo;
@ -116,9 +116,9 @@ export default class Connection {
this.IsClosed = false;
this.Socket = new WebSocket(this.Address);
this.Socket.onopen = () => this.OnOpen();
this.Socket.onmessage = (e) => this.OnMessage(e);
this.Socket.onerror = (e) => this.OnError(e);
this.Socket.onclose = (e) => this.OnClose(e);
this.Socket.onmessage = e => this.OnMessage(e);
this.Socket.onerror = e => this.OnError(e);
this.Socket.onclose = e => this.OnClose(e);
}
Close() {
@ -141,9 +141,7 @@ export default class Connection {
if (!this.IsClosed) {
this.ConnectTimeout = this.ConnectTimeout * 2;
console.log(
`[${this.Address}] Closed (${e.reason}), trying again in ${(
this.ConnectTimeout / 1000
)
`[${this.Address}] Closed (${e.reason}), trying again in ${(this.ConnectTimeout / 1000)
.toFixed(0)
.toLocaleString()} sec`
);
@ -224,7 +222,7 @@ export default class Connection {
* Send event on this connection and wait for OK response
*/
async SendAsync(e: NEvent, timeout = 5000) {
return new Promise<void>((resolve) => {
return new Promise<void>(resolve => {
if (!this.Settings.write) {
resolve();
return;
@ -304,7 +302,7 @@ export default class Connection {
* Using relay document to determine if this relay supports a feature
*/
SupportsNip(n: number) {
return this.Info?.supported_nips?.some((a) => a === n) ?? false;
return this.Info?.supported_nips?.some(a => a === n) ?? false;
}
_UpdateState() {
@ -312,10 +310,7 @@ export default class Connection {
this.CurrentState.events.received = this.Stats.EventsReceived;
this.CurrentState.events.send = this.Stats.EventsSent;
this.CurrentState.avgLatency =
this.Stats.Latency.length > 0
? this.Stats.Latency.reduce((acc, v) => acc + v, 0) /
this.Stats.Latency.length
: 0;
this.Stats.Latency.length > 0 ? this.Stats.Latency.reduce((acc, v) => acc + v, 0) / this.Stats.Latency.length : 0;
this.CurrentState.disconnects = this.Stats.Disconnects;
this.CurrentState.info = this.Info;
this.CurrentState.id = this.Id;
@ -346,21 +341,20 @@ export default class Connection {
_SendSubscription(sub: Subscriptions) {
if (!this.Authed && this.AwaitingAuth.size > 0) {
this.Pending.push(sub);
this.Pending.push(sub.ToObject());
return;
}
let req = ["REQ", sub.Id, sub.ToObject()];
if (sub.OrSubs.length > 0) {
req = [...req, ...sub.OrSubs.map((o) => o.ToObject())];
req = [...req, ...sub.OrSubs.map(o => o.ToObject())];
}
sub.Started.set(this.Address, new Date().getTime());
this._SendJson(req);
}
_SendJson(obj: Subscriptions | object) {
_SendJson(obj: object) {
if (this.Socket?.readyState !== WebSocket.OPEN) {
// @ts-expect-error TODO @v0l please figure this out... what the hell is going on
this.Pending.push(obj);
return;
}
@ -388,7 +382,7 @@ export default class Connection {
};
this.AwaitingAuth.set(challenge, true);
const authEvent = await System.nip42Auth(challenge, this.Address);
return new Promise((resolve) => {
return new Promise(resolve => {
if (!authEvent) {
authCleanup();
return Promise.reject("no event");
@ -425,11 +419,7 @@ export default class Connection {
if (started) {
const responseTime = now - started;
if (responseTime > 10_000) {
console.warn(
`[${this.Address}][${subId}] Slow response time ${(
responseTime / 1000
).toFixed(1)} seconds`
);
console.warn(`[${this.Address}][${subId}] Slow response time ${(responseTime / 1000).toFixed(1)} seconds`);
}
this.Stats.Latency.push(responseTime);
} else {

View File

@ -67,7 +67,7 @@ export default class Event {
* Get the pub key of the creator of this event NIP-26
*/
get RootPubKey() {
const delegation = this.Tags.find((a) => a.Key === "delegation");
const delegation = this.Tags.find(a => a.Key === "delegation");
if (delegation?.PubKey) {
return delegation.PubKey;
}
@ -103,7 +103,7 @@ export default class Event {
this.PubKey,
this.CreatedAt,
this.Kind,
this.Tags.map((a) => a.ToObject()).filter((a) => a !== null),
this.Tags.map(a => a.ToObject()).filter(a => a !== null),
this.Content,
];
@ -124,8 +124,8 @@ export default class Event {
created_at: this.CreatedAt,
kind: this.Kind,
tags: <string[][]>this.Tags.sort((a, b) => a.Index - b.Index)
.map((a) => a.ToObject())
.filter((a) => a !== null),
.map(a => a.ToObject())
.filter(a => a !== null),
content: this.Content,
sig: this.Signature,
};
@ -156,11 +156,7 @@ export default class Event {
data
);
const uData = new Uint8Array(result);
return `${base64.encode(uData, 0, result.byteLength)}?iv=${base64.encode(
iv,
0,
16
)}`;
return `${base64.encode(uData, 0, result.byteLength)}?iv=${base64.encode(iv, 0, 16)}`;
}
/**
@ -203,12 +199,6 @@ export default class Event {
async _GetDmSharedKey(pubkey: HexKey, privkey: HexKey) {
const sharedPoint = secp.getSharedSecret(privkey, "02" + pubkey);
const sharedX = sharedPoint.slice(1, 33);
return await window.crypto.subtle.importKey(
"raw",
sharedX,
{ name: "AES-CBC" },
false,
["encrypt", "decrypt"]
);
return await window.crypto.subtle.importKey("raw", sharedX, { name: "AES-CBC" }, false, ["encrypt", "decrypt"]);
}
}

View File

@ -133,7 +133,7 @@ export class NostrSystem {
* Request/Response pattern
*/
RequestSubscription(sub: Subscriptions) {
return new Promise<TaggedRawEvent[]>((resolve) => {
return new Promise<TaggedRawEvent[]>(resolve => {
const events: TaggedRawEvent[] = [];
// force timeout returning current results
@ -143,14 +143,14 @@ export class NostrSystem {
}, 10_000);
const onEventPassthrough = sub.OnEvent;
sub.OnEvent = (ev) => {
sub.OnEvent = ev => {
if (typeof onEventPassthrough === "function") {
onEventPassthrough(ev);
}
if (!events.some((a) => a.id === ev.id)) {
if (!events.some(a => a.id === ev.id)) {
events.push(ev);
} else {
const existing = events.find((a) => a.id === ev.id);
const existing = events.find(a => a.id === ev.id);
if (existing) {
for (const v of ev.relays) {
existing.relays.push(v);
@ -158,7 +158,7 @@ export class NostrSystem {
}
}
};
sub.OnEnd = (c) => {
sub.OnEnd = c => {
c.RemoveSubscription(sub.Id);
if (sub.IsFinished()) {
clearInterval(timeout);
@ -176,7 +176,7 @@ export class NostrSystem {
const meta = await this.UserDb.bulkGet(Array.from(this.WantsMetadata));
const expire = new Date().getTime() - ProfileCacheExpire;
for (const pk of this.WantsMetadata) {
const m = meta.find((a) => a?.pubkey === pk);
const m = meta.find(a => a?.pubkey === pk);
if (!m || m.loaded < expire) {
missing.add(pk);
// cap 100 missing profiles
@ -193,7 +193,7 @@ export class NostrSystem {
sub.Id = `profiles:${sub.Id.slice(0, 8)}`;
sub.Kinds = new Set([EventKind.SetMetadata]);
sub.Authors = missing;
sub.OnEvent = async (e) => {
sub.OnEvent = async e => {
const profile = mapEventToProfile(e);
const userDb = unwrap(this.UserDb);
if (profile) {
@ -208,19 +208,17 @@ export class NostrSystem {
}
};
const results = await this.RequestSubscription(sub);
const couldNotFetch = Array.from(missing).filter(
(a) => !results.some((b) => b.pubkey === a)
);
const couldNotFetch = Array.from(missing).filter(a => !results.some(b => b.pubkey === a));
console.debug("No profiles: ", couldNotFetch);
if (couldNotFetch.length > 0) {
const updates = couldNotFetch
.map((a) => {
.map(a => {
return {
pubkey: a,
loaded: new Date().getTime(),
};
})
.map((a) => unwrap(this.UserDb).update(a.pubkey, a));
.map(a => unwrap(this.UserDb).update(a.pubkey, a));
await Promise.all(updates);
}
}
@ -228,8 +226,7 @@ export class NostrSystem {
setTimeout(() => this._FetchMetadata(), 500);
}
nip42Auth: (challenge: string, relay: string) => Promise<Event | undefined> =
async () => undefined;
nip42Auth: (challenge: string, relay: string) => Promise<Event | undefined> = async () => undefined;
}
export const System = new NostrSystem();

View File

@ -1,4 +1,5 @@
import { HexKey, u256 } from "Nostr";
import { unwrap } from "Util";
export default class Tag {
Original: string[];
@ -56,7 +57,7 @@ export default class Tag {
switch (this.Key) {
case "e": {
let ret = ["e", this.Event, this.Relay, this.Marker];
const trimEnd = ret.reverse().findIndex((a) => a !== undefined);
const trimEnd = ret.reverse().findIndex(a => a !== undefined);
ret = ret.reverse().slice(0, ret.length - trimEnd);
return <string[]>ret;
}
@ -64,10 +65,10 @@ export default class Tag {
return this.PubKey ? ["p", this.PubKey] : null;
}
case "t": {
return ["t", this.Hashtag ?? ""];
return ["t", unwrap(this.Hashtag)];
}
case "d": {
return ["d", this.DTag ?? ""];
return ["d", unwrap(this.DTag)];
}
default: {
return this.Original;

View File

@ -19,15 +19,15 @@ export default class Thread {
* @param ev Event to extract thread from
*/
static ExtractThread(ev: NEvent) {
const isThread = ev.Tags.some((a) => a.Key === "e");
const isThread = ev.Tags.some(a => a.Key === "e");
if (!isThread) {
return null;
}
const shouldWriteMarkers = ev.Kind === EventKind.TextNote;
const ret = new Thread();
const eTags = ev.Tags.filter((a) => a.Key === "e");
const marked = eTags.some((a) => a.Marker !== undefined);
const eTags = ev.Tags.filter(a => a.Key === "e");
const marked = eTags.some(a => a.Marker !== undefined);
if (!marked) {
ret.Root = eTags[0];
ret.Root.Marker = shouldWriteMarkers ? "root" : undefined;
@ -38,19 +38,17 @@ export default class Thread {
if (eTags.length > 2) {
ret.Mentions = eTags.slice(2);
if (shouldWriteMarkers) {
ret.Mentions.forEach((a) => (a.Marker = "mention"));
ret.Mentions.forEach(a => (a.Marker = "mention"));
}
}
} else {
const root = eTags.find((a) => a.Marker === "root");
const reply = eTags.find((a) => a.Marker === "reply");
const root = eTags.find(a => a.Marker === "root");
const reply = eTags.find(a => a.Marker === "reply");
ret.Root = root;
ret.ReplyTo = reply;
ret.Mentions = eTags.filter((a) => a.Marker === "mention");
ret.Mentions = eTags.filter(a => a.Marker === "mention");
}
ret.PubKeys = Array.from(
new Set(ev.Tags.filter((a) => a.Key === "p").map((a) => <u256>a.PubKey))
);
ret.PubKeys = Array.from(new Set(ev.Tags.filter(a => a.Key === "p").map(a => <u256>a.PubKey)));
return ret;
}
}

View File

@ -20,6 +20,11 @@ export interface TaggedRawEvent extends RawEvent {
*/
export type HexKey = string;
/**
* Optinally undefined HexKey
*/
export type MaybeHexKey = HexKey | undefined;
/**
* A 256bit hex id
*/

View File

@ -7,18 +7,12 @@ import { MetadataCache, UsersDb } from "State/Users";
import { getDisplayName } from "Element/ProfileImage";
import { MentionRegex } from "Const";
export async function makeNotification(
db: UsersDb,
ev: TaggedRawEvent
): Promise<NotificationRequest | null> {
export async function makeNotification(db: UsersDb, ev: TaggedRawEvent): Promise<NotificationRequest | null> {
switch (ev.kind) {
case EventKind.TextNote: {
const pubkeys = new Set([
ev.pubkey,
...ev.tags.filter((a) => a[0] === "p").map((a) => a[1]),
]);
const pubkeys = new Set([ev.pubkey, ...ev.tags.filter(a => a[0] === "p").map(a => a[1])]);
const users = await db.bulkGet(Array.from(pubkeys));
const fromUser = users.find((a) => a?.pubkey === ev.pubkey);
const fromUser = users.find(a => a?.pubkey === ev.pubkey);
const name = getDisplayName(fromUser, ev.pubkey);
const avatarUrl = fromUser?.picture || Nostrich;
return {
@ -35,13 +29,13 @@ export async function makeNotification(
function replaceTagsWithUser(ev: TaggedRawEvent, users: MetadataCache[]) {
return ev.content
.split(MentionRegex)
.map((match) => {
.map(match => {
const matchTag = match.match(/#\[(\d+)\]/);
if (matchTag && matchTag.length === 2) {
const idx = parseInt(matchTag[1]);
const ref = ev.tags[idx];
if (ref && ref[0] === "p" && ref.length > 1) {
const u = users.find((a) => a.pubkey === ref[1]);
const u = users.find(a => a.pubkey === ref[1]);
return `@${getDisplayName(u, ref[1])}`;
}
}

View File

@ -9,40 +9,30 @@ import { bech32ToHex } from "Util";
import useEventPublisher from "Feed/EventPublisher";
import DM from "Element/DM";
import { RawEvent, TaggedRawEvent } from "Nostr";
import { TaggedRawEvent } from "Nostr";
import { dmsInChat, isToSelf } from "Pages/MessagesPage";
import NoteToSelf from "Element/NoteToSelf";
import { RootState } from "State/Store";
type RouterParams = {
id: string;
};
interface State {
login: {
dms: TaggedRawEvent[];
};
}
export default function ChatPage() {
const params = useParams<RouterParams>();
const publisher = useEventPublisher();
const id = bech32ToHex(params.id ?? "");
const pubKey = useSelector<{ login: { publicKey: string } }>(
(s) => s.login.publicKey
);
const dms = useSelector<State, RawEvent[]>((s) => filterDms(s.login.dms));
const pubKey = useSelector((s: RootState) => s.login.publicKey);
const dms = useSelector((s: RootState) => filterDms(s.login.dms));
const [content, setContent] = useState<string>();
const { ref, inView } = useInView();
const dmListRef = useRef<HTMLDivElement>(null);
function filterDms(dms: TaggedRawEvent[]) {
return dmsInChat(
id === pubKey ? dms.filter((d) => isToSelf(d, pubKey)) : dms,
id
);
return dmsInChat(id === pubKey ? dms.filter(d => isToSelf(d, pubKey)) : dms, id);
}
const sortedDms = useMemo<RawEvent[]>(() => {
const sortedDms = useMemo(() => {
return [...dms].sort((a, b) => a.created_at - b.created_at);
}, [dms]);
@ -70,13 +60,12 @@ export default function ChatPage() {
return (
<>
{(id === pubKey && (
<NoteToSelf className="f-grow mb-10" pubkey={id} />
)) || <ProfileImage pubkey={id} className="f-grow mb10" />}
{(id === pubKey && <NoteToSelf className="f-grow mb-10" pubkey={id} />) || (
<ProfileImage pubkey={id} className="f-grow mb10" />
)}
<div className="dm-list" ref={dmListRef}>
<div>
{/* TODO I need to look into this again, something's being bricked with the RawEvent and TaggedRawEvent */}
{sortedDms.map((a) => (
{sortedDms.map(a => (
<DM data={a as TaggedRawEvent} key={a.id} />
))}
<div ref={ref} className="mb10"></div>
@ -87,9 +76,8 @@ export default function ChatPage() {
<textarea
className="f-grow mr10"
value={content}
onChange={(e) => setContent(e.target.value)}
onKeyDown={(e) => onEnter(e)}
></textarea>
onChange={e => setContent(e.target.value)}
onKeyDown={e => onEnter(e)}></textarea>
<button type="button" onClick={() => sendDm()}>
Send
</button>

View File

@ -7,27 +7,15 @@ import { bech32ToHex } from "Util";
const Developers = [
bech32ToHex(KieranPubKey), // kieran
bech32ToHex(
"npub107jk7htfv243u0x5ynn43scq9wrxtaasmrwwa8lfu2ydwag6cx2quqncxg"
), // verbiricha
bech32ToHex(
"npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac"
), // Karnage
bech32ToHex("npub107jk7htfv243u0x5ynn43scq9wrxtaasmrwwa8lfu2ydwag6cx2quqncxg"), // verbiricha
bech32ToHex("npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac"), // Karnage
];
const Contributors = [
bech32ToHex(
"npub10djxr5pvdu97rjkde7tgcsjxzpdzmdguwacfjwlchvj7t88dl7nsdl54nf"
), // ivan
bech32ToHex(
"npub148jmlutaa49y5wl5mcll003ftj59v79vf7wuv3apcwpf75hx22vs7kk9ay"
), // liran cohen
bech32ToHex(
"npub1xdtducdnjerex88gkg2qk2atsdlqsyxqaag4h05jmcpyspqt30wscmntxy"
), // artur
bech32ToHex(
"npub1vp8fdcyejd4pqjyrjk9sgz68vuhq7pyvnzk8j0ehlljvwgp8n6eqsrnpsw"
), // samsamskies
bech32ToHex("npub10djxr5pvdu97rjkde7tgcsjxzpdzmdguwacfjwlchvj7t88dl7nsdl54nf"), // ivan
bech32ToHex("npub148jmlutaa49y5wl5mcll003ftj59v79vf7wuv3apcwpf75hx22vs7kk9ay"), // liran cohen
bech32ToHex("npub1xdtducdnjerex88gkg2qk2atsdlqsyxqaag4h05jmcpyspqt30wscmntxy"), // artur
bech32ToHex("npub1vp8fdcyejd4pqjyrjk9sgz68vuhq7pyvnzk8j0ehlljvwgp8n6eqsrnpsw"), // samsamskies
];
interface Splits {
@ -60,7 +48,7 @@ const DonatePage = () => {
}, []);
function actions(pk: HexKey) {
const split = splits.find((a) => bech32ToHex(a.pubKey) === pk);
const split = splits.find(a => bech32ToHex(a.pubKey) === pk);
if (split) {
return <>{(100 * split.split).toLocaleString()}%</>;
}
@ -70,44 +58,29 @@ const DonatePage = () => {
return (
<div className="main-content m5">
<h2>Help fund the development of Snort</h2>
<p>
Snort is an open source project built by passionate people in their free
time
</p>
<p>Snort is an open source project built by passionate people in their free time</p>
<p>Your donations are greatly appreciated</p>
<p>
Check out the code here:{" "}
<a
className="highlight"
href="https://github.com/v0l/snort"
rel="noreferrer"
target="_blank"
>
<a className="highlight" href="https://github.com/v0l/snort" rel="noreferrer" target="_blank">
https://github.com/v0l/snort
</a>
</p>
<p>
Each contributor will get paid a percentage of all donations and NIP-05
orders, you can see the split amounts below
Each contributor will get paid a percentage of all donations and NIP-05 orders, you can see the split amounts
below
</p>
<div className="flex">
<div className="mr10">Lightning Donation: </div>
<ZapButton
pubkey={bech32ToHex(SnortPubKey)}
svc={"donate@snort.social"}
/>
<ZapButton pubkey={bech32ToHex(SnortPubKey)} svc={"donate@snort.social"} />
</div>
{today && (
<small>
Total today (UTC): {today.donations.toLocaleString()} sats
</small>
)}
{today && <small>Total today (UTC): {today.donations.toLocaleString()} sats</small>}
<h3>Primary Developers</h3>
{Developers.map((a) => (
{Developers.map(a => (
<ProfilePreview pubkey={a} key={a} actions={actions(a)} />
))}
<h4>Contributors</h4>
{Contributors.map((a) => (
{Contributors.map(a => (
<ProfilePreview pubkey={a} key={a} actions={actions(a)} />
))}
</div>

View File

@ -1,10 +1,9 @@
import { useParams } from "react-router-dom";
import Timeline from "Element/Timeline";
import { unwrap } from "Util";
const HashTagsPage = () => {
const params = useParams();
const tag = unwrap(params.tag).toLowerCase();
const tag = (params.tag ?? "").toLowerCase();
return (
<>

Some files were not shown because too many files have changed in this diff Show More