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; const value: string;
export default value; 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> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta name="description" content="Fast nostr web ui" /> <meta name="description" content="Fast nostr web ui" />
<meta <meta
http-equiv="Content-Security-Policy" 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="apple-touch-icon" href="%PUBLIC_URL%/nostrich_512.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

View File

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

View File

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

View File

@ -1,7 +1,6 @@
import { useState } from "react"; import { useState } from "react";
interface AsyncButtonProps interface AsyncButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
onClick(e: React.MouseEvent): Promise<void> | void; onClick(e: React.MouseEvent): Promise<void> | void;
children?: React.ReactNode; children?: React.ReactNode;
} }

View File

@ -4,20 +4,14 @@ import { CSSProperties, useEffect, useState } from "react";
import type { UserMetadata } from "Nostr"; import type { UserMetadata } from "Nostr";
import useImgProxy from "Feed/ImgProxy"; import useImgProxy from "Feed/ImgProxy";
const Avatar = ({ const Avatar = ({ user, ...rest }: { user?: UserMetadata; onClick?: () => void }) => {
user,
...rest
}: {
user?: UserMetadata;
onClick?: () => void;
}) => {
const [url, setUrl] = useState<string>(Nostrich); const [url, setUrl] = useState<string>(Nostrich);
const { proxy } = useImgProxy(); const { proxy } = useImgProxy();
useEffect(() => { useEffect(() => {
if (user?.picture) { if (user?.picture) {
proxy(user.picture, 120) proxy(user.picture, 120)
.then((a) => setUrl(a)) .then(a => setUrl(a))
.catch(console.warn); .catch(console.warn);
} }
}, [user]); }, [user]);
@ -25,14 +19,7 @@ const Avatar = ({
const backgroundImage = `url(${url})`; const backgroundImage = `url(${url})`;
const style = { "--img-url": backgroundImage } as CSSProperties; const style = { "--img-url": backgroundImage } as CSSProperties;
const domain = user?.nip05 && user.nip05.split("@")[1]; const domain = user?.nip05 && user.nip05.split("@")[1];
return ( return <div {...rest} style={style} className="avatar" data-domain={domain?.toLowerCase()}></div>;
<div
{...rest}
style={style}
className="avatar"
data-domain={domain?.toLowerCase()}
></div>
);
}; };
export default Avatar; export default Avatar;

View File

@ -18,39 +18,21 @@ export default function BlockList({ variant }: BlockListProps) {
{variant === "muted" && ( {variant === "muted" && (
<> <>
<h4> <h4>
<FormattedMessage <FormattedMessage {...messages.MuteCount} values={{ n: muted.length }} />
{...messages.MuteCount}
values={{ n: muted.length }}
/>
</h4> </h4>
{muted.map((a) => { {muted.map(a => {
return ( return <ProfilePreview actions={<MuteButton pubkey={a} />} pubkey={a} options={{ about: false }} key={a} />;
<ProfilePreview
actions={<MuteButton pubkey={a} />}
pubkey={a}
options={{ about: false }}
key={a}
/>
);
})} })}
</> </>
)} )}
{variant === "blocked" && ( {variant === "blocked" && (
<> <>
<h4> <h4>
<FormattedMessage <FormattedMessage {...messages.BlockCount} values={{ n: blocked.length }} />
{...messages.BlockCount}
values={{ n: blocked.length }}
/>
</h4> </h4>
{blocked.map((a) => { {blocked.map(a => {
return ( return (
<ProfilePreview <ProfilePreview actions={<BlockButton pubkey={a} />} pubkey={a} options={{ about: false }} key={a} />
actions={<BlockButton pubkey={a} />}
pubkey={a}
options={{ about: false }}
key={a}
/>
); );
})} })}
</> </>

View File

@ -9,12 +9,7 @@ interface CollapsedProps {
setCollapsed(b: boolean): void; setCollapsed(b: boolean): void;
} }
const Collapsed = ({ const Collapsed = ({ text, children, collapsed, setCollapsed }: CollapsedProps) => {
text,
children,
collapsed,
setCollapsed,
}: CollapsedProps) => {
return collapsed ? ( return collapsed ? (
<div className="collapsed"> <div className="collapsed">
<ShowMore text={text} onClick={() => setCollapsed(false)} /> <ShowMore text={text} onClick={() => setCollapsed(false)} />

View File

@ -10,23 +10,13 @@ export interface CopyProps {
export default function Copy({ text, maxSize = 32 }: CopyProps) { export default function Copy({ text, maxSize = 32 }: CopyProps) {
const { copy, copied } = useCopy(); const { copy, copied } = useCopy();
const sliceLength = maxSize / 2; const sliceLength = maxSize / 2;
const trimmed = const trimmed = text.length > maxSize ? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}` : text;
text.length > maxSize
? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}`
: text;
return ( return (
<div className="flex flex-row copy" onClick={() => copy(text)}> <div className="flex flex-row copy" onClick={() => copy(text)}>
<span className="body">{trimmed}</span> <span className="body">{trimmed}</span>
<span <span className="icon" style={{ color: copied ? "var(--success)" : "var(--highlight)" }}>
className="icon" {copied ? <Check width={13} height={13} /> : <CopyIcon width={13} height={13} />}
style={{ color: copied ? "var(--success)" : "var(--highlight)" }}
>
{copied ? (
<Check width={13} height={13} />
) : (
<CopyIcon width={13} height={13} />
)}
</span> </span>
</div> </div>
); );

View File

@ -22,18 +22,14 @@ export type DMProps = {
export default function DM(props: DMProps) { export default function DM(props: DMProps) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const pubKey = useSelector<RootState, HexKey | undefined>( const pubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
(s) => s.login.publicKey
);
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const [content, setContent] = useState("Loading..."); const [content, setContent] = useState("Loading...");
const [decrypted, setDecrypted] = useState(false); const [decrypted, setDecrypted] = useState(false);
const { ref, inView } = useInView(); const { ref, inView } = useInView();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const isMe = props.data.pubkey === pubKey; const isMe = props.data.pubkey === pubKey;
const otherPubkey = isMe const otherPubkey = isMe ? pubKey : unwrap(props.data.tags.find(a => a[0] === "p")?.[1]);
? pubKey
: unwrap(props.data.tags.find((a) => a[0] === "p")?.[1]);
async function decrypt() { async function decrypt() {
const e = new Event(props.data); const e = new Event(props.data);
@ -55,18 +51,10 @@ export default function DM(props: DMProps) {
return ( return (
<div className={`flex dm f-col${isMe ? " me" : ""}`} ref={ref}> <div className={`flex dm f-col${isMe ? " me" : ""}`} ref={ref}>
<div> <div>
<NoteTime <NoteTime from={props.data.created_at * 1000} fallback={formatMessage(messages.JustNow)} />
from={props.data.created_at * 1000}
fallback={formatMessage(messages.JustNow)}
/>
</div> </div>
<div className="w-max"> <div className="w-max">
<Text <Text content={content} tags={[]} users={new Map()} creator={otherPubkey} />
content={content}
tags={[]}
users={new Map()}
creator={otherPubkey}
/>
</div> </div>
</div> </div>
); );

View File

@ -15,9 +15,7 @@ export interface FollowButtonProps {
export default function FollowButton(props: FollowButtonProps) { export default function FollowButton(props: FollowButtonProps) {
const pubkey = parseId(props.pubkey); const pubkey = parseId(props.pubkey);
const publiser = useEventPublisher(); const publiser = useEventPublisher();
const isFollowing = useSelector<RootState, boolean>( const isFollowing = useSelector<RootState, boolean>(s => s.login.follows?.includes(pubkey) ?? false);
(s) => s.login.follows?.includes(pubkey) ?? false
);
const baseClassname = `${props.className} follow-button`; const baseClassname = `${props.className} follow-button`;
async function follow(pubkey: HexKey) { async function follow(pubkey: HexKey) {
@ -34,13 +32,8 @@ export default function FollowButton(props: FollowButtonProps) {
<button <button
type="button" type="button"
className={isFollowing ? `${baseClassname} secondary` : baseClassname} className={isFollowing ? `${baseClassname} secondary` : baseClassname}
onClick={() => (isFollowing ? unfollow(pubkey) : follow(pubkey))} onClick={() => (isFollowing ? unfollow(pubkey) : follow(pubkey))}>
> {isFollowing ? <FormattedMessage {...messages.Unfollow} /> : <FormattedMessage {...messages.Follow} />}
{isFollowing ? (
<FormattedMessage {...messages.Unfollow} />
) : (
<FormattedMessage {...messages.Follow} />
)}
</button> </button>
); );
} }

View File

@ -10,10 +10,7 @@ export interface FollowListBaseProps {
pubkeys: HexKey[]; pubkeys: HexKey[];
title?: string; title?: string;
} }
export default function FollowListBase({ export default function FollowListBase({ pubkeys, title }: FollowListBaseProps) {
pubkeys,
title,
}: FollowListBaseProps) {
const publisher = useEventPublisher(); const publisher = useEventPublisher();
async function followAll() { async function followAll() {
@ -25,15 +22,11 @@ export default function FollowListBase({
<div className="main-content"> <div className="main-content">
<div className="flex mt10 mb10"> <div className="flex mt10 mb10">
<div className="f-grow bold">{title}</div> <div className="f-grow bold">{title}</div>
<button <button className="transparent" type="button" onClick={() => followAll()}>
className="transparent"
type="button"
onClick={() => followAll()}
>
<FormattedMessage {...messages.FollowAll} /> <FormattedMessage {...messages.FollowAll} />
</button> </button>
</div> </div>
{pubkeys?.map((a) => ( {pubkeys?.map(a => (
<ProfilePreview pubkey={a} key={a} /> <ProfilePreview pubkey={a} key={a} />
))} ))}
</div> </div>

View File

@ -18,17 +18,10 @@ export default function FollowersList({ pubkey }: FollowersListProps) {
const pubkeys = useMemo(() => { const pubkeys = useMemo(() => {
const contactLists = feed?.store.notes.filter( const contactLists = feed?.store.notes.filter(
(a) => a => a.kind === EventKind.ContactList && a.tags.some(b => b[0] === "p" && b[1] === pubkey)
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]); }, [feed, pubkey]);
return ( return <FollowListBase pubkeys={pubkeys} title={formatMessage(messages.FollowerCount, { n: pubkeys?.length })} />;
<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); return getFollowers(feed.store, pubkey);
}, [feed, pubkey]); }, [feed, pubkey]);
return ( return <FollowListBase pubkeys={pubkeys} title={formatMessage(messages.FollowingCount, { n: pubkeys?.length })} />;
<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) { export default function FollowsYou({ pubkey }: FollowsYouProps) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const feed = useFollowsFeed(pubkey); const feed = useFollowsFeed(pubkey);
const loginPubKey = useSelector<RootState, HexKey | undefined>( const loginPubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
(s) => s.login.publicKey
);
const pubkeys = useMemo(() => { const pubkeys = useMemo(() => {
return getFollowers(feed.store, pubkey); return getFollowers(feed.store, pubkey);
@ -27,7 +25,5 @@ export default function FollowsYou({ pubkey }: FollowsYouProps) {
const followsMe = loginPubKey ? pubkeys.includes(loginPubKey) : false; const followsMe = loginPubKey ? pubkeys.includes(loginPubKey) : false;
return followsMe ? ( return followsMe ? <span className="follows-you">{formatMessage(messages.FollowsYou)}</span> : null;
<span className="follows-you">{formatMessage(messages.FollowsYou)}</span>
) : null;
} }

View File

@ -4,7 +4,7 @@ import "./Hashtag.css";
const Hashtag = ({ tag }: { tag: string }) => { const Hashtag = ({ tag }: { tag: string }) => {
return ( return (
<span className="hashtag"> <span className="hashtag">
<Link to={`/t/${tag}`} onClick={(e) => e.stopPropagation()}> <Link to={`/t/${tag}`} onClick={e => e.stopPropagation()}>
#{tag} #{tag}
</Link> </Link>
</span> </span>

View File

@ -19,30 +19,17 @@ import TidalEmbed from "Element/TidalEmbed";
import { ProxyImg } from "Element/ProxyImg"; import { ProxyImg } from "Element/ProxyImg";
import { HexKey } from "Nostr"; import { HexKey } from "Nostr";
export default function HyperText({ export default function HyperText({ link, creator }: { link: string; creator: HexKey }) {
link,
creator,
}: {
link: string;
creator: HexKey;
}) {
const pref = useSelector((s: RootState) => s.login.preferences); const pref = useSelector((s: RootState) => s.login.preferences);
const follows = useSelector((s: RootState) => s.login.follows); const follows = useSelector((s: RootState) => s.login.follows);
const render = useCallback(() => { const render = useCallback(() => {
const a = link; const a = link;
try { try {
const hideNonFollows = const hideNonFollows = pref.autoLoadMedia === "follows-only" && !follows.includes(creator);
pref.autoLoadMedia === "follows-only" && !follows.includes(creator);
if (pref.autoLoadMedia === "none" || hideNonFollows) { if (pref.autoLoadMedia === "none" || hideNonFollows) {
return ( return (
<a <a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
href={a}
onClick={(e) => e.stopPropagation()}
target="_blank"
rel="noreferrer"
className="ext"
>
{a} {a}
</a> </a>
); );
@ -54,8 +41,7 @@ export default function HyperText({
const soundcloundId = SoundCloudRegex.test(a) && RegExp.$1; const soundcloundId = SoundCloudRegex.test(a) && RegExp.$1;
const mixcloudId = MixCloudRegex.test(a) && RegExp.$1; const mixcloudId = MixCloudRegex.test(a) && RegExp.$1;
const spotifyId = SpotifyRegex.test(a); const spotifyId = SpotifyRegex.test(a);
const extension = const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
if (extension) { if (extension) {
switch (extension) { switch (extension) {
case "gif": case "gif":
@ -83,11 +69,10 @@ export default function HyperText({
<a <a
key={url.toString()} key={url.toString()}
href={url.toString()} href={url.toString()}
onClick={(e) => e.stopPropagation()} onClick={e => e.stopPropagation()}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="ext" className="ext">
>
{url.toString()} {url.toString()}
</a> </a>
); );
@ -124,13 +109,7 @@ export default function HyperText({
return <SpotifyEmbed link={a} />; return <SpotifyEmbed link={a} />;
} else { } else {
return ( return (
<a <a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
href={a}
onClick={(e) => e.stopPropagation()}
target="_blank"
rel="noreferrer"
className="ext"
>
{a} {a}
</a> </a>
); );
@ -139,13 +118,7 @@ export default function HyperText({
// Ignore the error. // Ignore the error.
} }
return ( return (
<a <a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
href={a}
onClick={(e) => e.stopPropagation()}
target="_blank"
rel="noreferrer"
className="ext"
>
{a} {a}
</a> </a>
); );

View File

@ -1,7 +1,6 @@
import "./Invoice.css"; import "./Invoice.css";
import { useState } from "react"; import { useState } from "react";
import { useIntl, FormattedMessage } from "react-intl"; import { useIntl, FormattedMessage } from "react-intl";
// @ts-expect-error No types available
import { decode as invoiceDecode } from "light-bolt11-decoder"; import { decode as invoiceDecode } from "light-bolt11-decoder";
import { useMemo } from "react"; import { useMemo } from "react";
import SendSats from "Element/SendSats"; import SendSats from "Element/SendSats";
@ -14,10 +13,6 @@ export interface InvoiceProps {
invoice: string; invoice: string;
} }
interface Section {
name: string;
}
export default function Invoice(props: InvoiceProps) { export default function Invoice(props: InvoiceProps) {
const invoice = props.invoice; const invoice = props.invoice;
const webln = useWebln(); const webln = useWebln();
@ -28,22 +23,19 @@ export default function Invoice(props: InvoiceProps) {
try { try {
const parsed = invoiceDecode(invoice); const parsed = invoiceDecode(invoice);
const amount = parseInt( const amountSection = parsed.sections.find(a => a.name === "amount");
parsed.sections.find((a: Section) => a.name === "amount")?.value const amount = amountSection ? (amountSection.value as number) : NaN;
);
const timestamp = parseInt( const timestampSection = parsed.sections.find(a => a.name === "timestamp");
parsed.sections.find((a: Section) => a.name === "timestamp")?.value const timestamp = timestampSection ? (timestampSection.value as number) : NaN;
);
const expire = parseInt( const expirySection = parsed.sections.find(a => a.name === "expiry");
parsed.sections.find((a: Section) => a.name === "expiry")?.value const expire = expirySection ? (expirySection.value as number) : NaN;
); const descriptionSection = parsed.sections.find(a => a.name === "description")?.value;
const description = parsed.sections.find(
(a: Section) => a.name === "description"
)?.value;
const ret = { const ret = {
amount: !isNaN(amount) ? amount / 1000 : 0, amount: !isNaN(amount) ? amount / 1000 : 0,
expire: !isNaN(timestamp) && !isNaN(expire) ? timestamp + expire : null, expire: !isNaN(timestamp) && !isNaN(expire) ? timestamp + expire : null,
description, description: descriptionSection as string | undefined,
expired: false, expired: false,
}; };
if (ret.expire) { if (ret.expire) {
@ -93,18 +85,13 @@ export default function Invoice(props: InvoiceProps) {
return ( return (
<> <>
<div <div className={`note-invoice flex ${isExpired ? "expired" : ""} ${isPaid ? "paid" : ""}`}>
className={`note-invoice flex ${isExpired ? "expired" : ""} ${
isPaid ? "paid" : ""
}`}
>
<div className="invoice-header">{header()}</div> <div className="invoice-header">{header()}</div>
<p className="invoice-amount"> <p className="invoice-amount">
{amount > 0 && ( {amount > 0 && (
<> <>
{amount.toLocaleString()}{" "} {amount.toLocaleString()} <span className="sats">sat{amount === 1 ? "" : "s"}</span>
<span className="sats">sat{amount === 1 ? "" : "s"}</span>
</> </>
)} )}
</p> </p>
@ -117,11 +104,7 @@ export default function Invoice(props: InvoiceProps) {
</div> </div>
) : ( ) : (
<button disabled={isExpired} type="button" onClick={payInvoice}> <button disabled={isExpired} type="button" onClick={payInvoice}>
{isExpired ? ( {isExpired ? <FormattedMessage {...messages.Expired} /> : <FormattedMessage {...messages.Pay} />}
<FormattedMessage {...messages.Expired} />
) : (
<FormattedMessage {...messages.Pay} />
)}
</button> </button>
)} )}
</div> </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(() => { useEffect(() => {
const t = setInterval(() => { const t = setInterval(() => {
setTick((x) => (x += 1)); setTick(x => (x += 1));
}, 500); }, 500);
return () => clearInterval(t); return () => clearInterval(t);
}, []); }, []);

View File

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

View File

@ -18,7 +18,7 @@ export default function Mention({ pubkey }: { pubkey: HexKey }) {
}, [user, pubkey]); }, [user, pubkey]);
return ( return (
<Link to={profileLink(pubkey)} onClick={(e) => e.stopPropagation()}> <Link to={profileLink(pubkey)} onClick={e => e.stopPropagation()}>
@{name} @{name}
</Link> </Link>
); );

View File

@ -3,14 +3,9 @@ import { useSelector } from "react-redux";
import { RootState } from "State/Store"; import { RootState } from "State/Store";
const MixCloudEmbed = ({ link }: { link: string }) => { const MixCloudEmbed = ({ link }: { link: string }) => {
const feedPath = const feedPath = (MixCloudRegex.test(link) && RegExp.$1) + "%2F" + (MixCloudRegex.test(link) && RegExp.$2);
(MixCloudRegex.test(link) && RegExp.$1) +
"%2F" +
(MixCloudRegex.test(link) && RegExp.$2);
const lightTheme = useSelector<RootState, boolean>( const lightTheme = useSelector<RootState, boolean>(s => s.login.preferences.theme === "light");
(s) => s.login.preferences.theme === "light"
);
const lightParams = lightTheme ? "light=1" : "light=0"; const lightParams = lightTheme ? "light=1" : "light=0";

View File

@ -8,10 +8,7 @@ export interface ModalProps {
children: React.ReactNode; children: React.ReactNode;
} }
function useOnClickOutside( function useOnClickOutside(ref: React.MutableRefObject<Element | null>, onClickOutside: () => void) {
ref: React.MutableRefObject<Element | null>,
onClickOutside: () => void
) {
useEffect(() => { useEffect(() => {
function handleClickOutside(ev: MouseEvent) { function handleClickOutside(ev: MouseEvent) {
if (ref && ref.current && !ref.current.contains(ev.target as Node)) { 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="main-content">
<div className="flex mt10"> <div className="flex mt10">
<div className="f-grow bold"> <div className="f-grow bold">
<FormattedMessage <FormattedMessage {...messages.MuteCount} values={{ n: pubkeys?.length }} />
{...messages.MuteCount}
values={{ n: pubkeys?.length }}
/>
</div> </div>
<button <button
disabled={hasAllMuted || pubkeys.length === 0} disabled={hasAllMuted || pubkeys.length === 0}
className="transparent" className="transparent"
type="button" type="button"
onClick={() => muteAll(pubkeys)} onClick={() => muteAll(pubkeys)}>
>
<FormattedMessage {...messages.MuteAll} /> <FormattedMessage {...messages.MuteAll} />
</button> </button>
</div> </div>
{pubkeys?.map((a) => { {pubkeys?.map(a => {
return ( return <ProfilePreview actions={<MuteButton pubkey={a} />} pubkey={a} options={{ about: false }} key={a} />;
<ProfilePreview
actions={<MuteButton pubkey={a} />}
pubkey={a}
options={{ about: false }}
key={a}
/>
);
})} })}
</div> </div>
); );

View File

@ -1,11 +1,7 @@
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { faCircleCheck, faSpinner, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
faCircleCheck,
faSpinner,
faTriangleExclamation,
} from "@fortawesome/free-solid-svg-icons";
import "./Nip05.css"; import "./Nip05.css";
import { HexKey } from "Nostr"; import { HexKey } from "Nostr";
@ -19,13 +15,9 @@ async function fetchNip05Pubkey(name: string, domain: string) {
return undefined; return undefined;
} }
try { try {
const res = await fetch( const res = await fetch(`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`);
`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(
name
)}`
);
const data: NostrJson = await res.json(); 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 n.toLowerCase() === name.toLowerCase();
}); });
return match ? data.names[match] : undefined; return match ? data.names[match] : undefined;
@ -39,16 +31,12 @@ const VERIFICATION_STALE_TIMEOUT = 10 * 60 * 1000;
export function useIsVerified(pubkey: HexKey, nip05?: string) { export function useIsVerified(pubkey: HexKey, nip05?: string) {
const [name, domain] = nip05 ? nip05.split("@") : []; const [name, domain] = nip05 ? nip05.split("@") : [];
const { isError, isSuccess, data } = useQuery( const { isError, isSuccess, data } = useQuery(["nip05", nip05], () => fetchNip05Pubkey(name, domain), {
["nip05", nip05], retry: false,
() => fetchNip05Pubkey(name, domain), retryOnMount: false,
{ cacheTime: VERIFICATION_CACHE_TIME,
retry: false, staleTime: VERIFICATION_STALE_TIMEOUT,
retryOnMount: false, });
cacheTime: VERIFICATION_CACHE_TIME,
staleTime: VERIFICATION_STALE_TIMEOUT,
}
);
const isVerified = isSuccess && data === pubkey; const isVerified = isSuccess && data === pubkey;
const cantVerify = isSuccess && data !== pubkey; const cantVerify = isSuccess && data !== pubkey;
return { isVerified, couldNotVerify: isError || cantVerify }; return { isVerified, couldNotVerify: isError || cantVerify };
@ -62,42 +50,18 @@ export interface Nip05Params {
const Nip05 = (props: Nip05Params) => { const Nip05 = (props: Nip05Params) => {
const [name, domain] = props.nip05 ? props.nip05.split("@") : []; const [name, domain] = props.nip05 ? props.nip05.split("@") : [];
const isDefaultUser = name === "_"; const isDefaultUser = name === "_";
const { isVerified, couldNotVerify } = useIsVerified( const { isVerified, couldNotVerify } = useIsVerified(props.pubkey, props.nip05);
props.pubkey,
props.nip05
);
return ( return (
<div <div className={`flex nip05${couldNotVerify ? " failed" : ""}`} onClick={ev => ev.stopPropagation()}>
className={`flex nip05${couldNotVerify ? " failed" : ""}`}
onClick={(ev) => ev.stopPropagation()}
>
{!isDefaultUser && <div className="nick">{`${name}@`}</div>} {!isDefaultUser && <div className="nick">{`${name}@`}</div>}
<span className="domain" data-domain={domain?.toLowerCase()}> <span className="domain" data-domain={domain?.toLowerCase()}>
{domain} {domain}
</span> </span>
<span className="badge"> <span className="badge">
{isVerified && ( {isVerified && <FontAwesomeIcon color={"var(--highlight)"} icon={faCircleCheck} size="xs" />}
<FontAwesomeIcon {!isVerified && !couldNotVerify && <FontAwesomeIcon color={"var(--fg-color)"} icon={faSpinner} size="xs" />}
color={"var(--highlight)"} {couldNotVerify && <FontAwesomeIcon color={"var(--error)"} icon={faTriangleExclamation} size="xs" />}
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> </span>
</div> </div>
); );

View File

@ -20,6 +20,7 @@ import { debounce, hexToBech32 } from "Util";
import { UserMetadata } from "Nostr"; import { UserMetadata } from "Nostr";
import messages from "./messages"; import messages from "./messages";
import { RootState } from "State/Store";
type Nip05ServiceProps = { type Nip05ServiceProps = {
name: string; name: string;
@ -29,47 +30,34 @@ type Nip05ServiceProps = {
supportLink: string; supportLink: string;
}; };
interface ReduxStore {
login: { publicKey: string };
}
export default function Nip5Service(props: Nip05ServiceProps) { export default function Nip5Service(props: Nip05ServiceProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const { formatMessage } = useIntl(); 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 user = useUserProfile(pubkey);
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const svc = useMemo( const svc = useMemo(() => new ServiceProvider(props.service), [props.service]);
() => new ServiceProvider(props.service),
[props.service]
);
const [serviceConfig, setServiceConfig] = useState<ServiceConfig>(); const [serviceConfig, setServiceConfig] = useState<ServiceConfig>();
const [error, setError] = useState<ServiceError>(); const [error, setError] = useState<ServiceError>();
const [handle, setHandle] = useState<string>(""); const [handle, setHandle] = useState<string>("");
const [domain, setDomain] = useState<string>(""); const [domain, setDomain] = useState<string>("");
const [availabilityResponse, setAvailabilityResponse] = const [availabilityResponse, setAvailabilityResponse] = useState<HandleAvailability>();
useState<HandleAvailability>(); const [registerResponse, setRegisterResponse] = useState<HandleRegisterResponse>();
const [registerResponse, setRegisterResponse] =
useState<HandleRegisterResponse>();
const [showInvoice, setShowInvoice] = useState<boolean>(false); const [showInvoice, setShowInvoice] = useState<boolean>(false);
const [registerStatus, setRegisterStatus] = useState<CheckRegisterResponse>(); const [registerStatus, setRegisterStatus] = useState<CheckRegisterResponse>();
const domainConfig = useMemo( const domainConfig = useMemo(() => serviceConfig?.domains.find(a => a.name === domain), [domain, serviceConfig]);
() => serviceConfig?.domains.find((a) => a.name === domain),
[domain, serviceConfig]
);
useEffect(() => { useEffect(() => {
svc svc
.GetConfig() .GetConfig()
.then((a) => { .then(a => {
if ("error" in a) { if ("error" in a) {
setError(a as ServiceError); setError(a as ServiceError);
} else { } else {
const svc = a as ServiceConfig; const svc = a as ServiceConfig;
setServiceConfig(svc); setServiceConfig(svc);
const defaultDomain = const defaultDomain = svc.domains.find(a => a.default)?.name || svc.domains[0].name;
svc.domains.find((a) => a.default)?.name || svc.domains[0].name;
setDomain(defaultDomain); setDomain(defaultDomain);
} }
}) })
@ -88,10 +76,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
setAvailabilityResponse({ available: false, why: "TOO_LONG" }); setAvailabilityResponse({ available: false, why: "TOO_LONG" });
return; return;
} }
const rx = new RegExp( const rx = new RegExp(domainConfig?.regex[0] ?? "", domainConfig?.regex[1] ?? "");
domainConfig?.regex[0] ?? "",
domainConfig?.regex[1] ?? ""
);
if (!rx.test(handle)) { if (!rx.test(handle)) {
setAvailabilityResponse({ available: false, why: "REGEX" }); setAvailabilityResponse({ available: false, why: "REGEX" });
return; return;
@ -99,7 +84,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
return debounce(500, () => { return debounce(500, () => {
svc svc
.CheckAvailable(handle, domain) .CheckAvailable(handle, domain)
.then((a) => { .then(a => {
if ("error" in a) { if ("error" in a) {
setError(a as ServiceError); setError(a as ServiceError);
} else { } else {
@ -133,10 +118,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
} }
}, [registerResponse, showInvoice, svc]); }, [registerResponse, showInvoice, svc]);
function mapError( function mapError(e: ServiceErrorCode | undefined, t: string | null): string | undefined {
e: ServiceErrorCode | undefined,
t: string | null
): string | undefined {
if (e === undefined) { if (e === undefined) {
return undefined; return undefined;
} }
@ -152,8 +134,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
} }
async function startBuy(handle: string, domain: string) { async function startBuy(handle: string, domain: string) {
if (registerResponse) { if (!pubkey) {
setShowInvoice(true);
return; return;
} }
@ -202,11 +183,11 @@ export default function Nip5Service(props: Nip05ServiceProps) {
type="text" type="text"
placeholder="Handle" placeholder="Handle"
value={handle} value={handle}
onChange={(e) => setHandle(e.target.value.toLowerCase())} onChange={e => setHandle(e.target.value.toLowerCase())}
/> />
&nbsp;@&nbsp; &nbsp;@&nbsp;
<select value={domain} onChange={(e) => setDomain(e.target.value)}> <select value={domain} onChange={e => setDomain(e.target.value)}>
{serviceConfig?.domains.map((a) => ( {serviceConfig?.domains.map(a => (
<option key={a.name}>{a.name}</option> <option key={a.name}>{a.name}</option>
))} ))}
</select> </select>
@ -215,10 +196,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
{availabilityResponse?.available && !registerStatus && ( {availabilityResponse?.available && !registerStatus && (
<div className="flex"> <div className="flex">
<div className="mr10"> <div className="mr10">
<FormattedMessage <FormattedMessage {...messages.Sats} values={{ n: availabilityResponse.quote?.price }} />
{...messages.Sats}
values={{ n: availabilityResponse.quote?.price }}
/>
<br /> <br />
<small>{availabilityResponse.quote?.data.type}</small> <small>{availabilityResponse.quote?.data.type}</small>
</div> </div>
@ -238,10 +216,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
<div className="flex"> <div className="flex">
<b className="error"> <b className="error">
<FormattedMessage {...messages.NotAvailable} />{" "} <FormattedMessage {...messages.NotAvailable} />{" "}
{mapError( {mapError(availabilityResponse.why, availabilityResponse.reasonTag || null)}
availabilityResponse.why,
availabilityResponse.reasonTag || null
)}
</b> </b>
</div> </div>
)} )}

View File

@ -1,11 +1,5 @@
import "./Note.css"; import "./Note.css";
import { import { useCallback, useMemo, useState, useLayoutEffect, ReactNode } from "react";
useCallback,
useMemo,
useState,
useLayoutEffect,
ReactNode,
} from "react";
import { useNavigate, Link } from "react-router-dom"; import { useNavigate, Link } from "react-router-dom";
import { useInView } from "react-intersection-observer"; import { useInView } from "react-intersection-observer";
import { useIntl, FormattedMessage } from "react-intl"; import { useIntl, FormattedMessage } from "react-intl";
@ -58,21 +52,11 @@ const HiddenNote = ({ children }: { children: React.ReactNode }) => {
export default function Note(props: NoteProps) { export default function Note(props: NoteProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const { const { data, related, highlight, options: opt, ["data-ev"]: parsedEvent, ignoreModeration = false } = props;
data,
related,
highlight,
options: opt,
["data-ev"]: parsedEvent,
ignoreModeration = false,
} = props;
const ev = useMemo(() => parsedEvent ?? new NEvent(data), [data]); const ev = useMemo(() => parsedEvent ?? new NEvent(data), [data]);
const pubKeys = useMemo(() => ev.Thread?.PubKeys || [], [ev]); const pubKeys = useMemo(() => ev.Thread?.PubKeys || [], [ev]);
const users = useUserProfiles(pubKeys); const users = useUserProfiles(pubKeys);
const deletions = useMemo( const deletions = useMemo(() => getReactions(related, ev.Id, EventKind.Deletion), [related]);
() => getReactions(related, ev.Id, EventKind.Deletion),
[related]
);
const { isMuted } = useModeration(); const { isMuted } = useModeration();
const isOpMuted = isMuted(ev.PubKey); const isOpMuted = isMuted(ev.PubKey);
const { ref, inView, entry } = useInView({ triggerOnce: true }); const { ref, inView, entry } = useInView({ triggerOnce: true });
@ -99,14 +83,7 @@ export default function Note(props: NoteProps) {
</b> </b>
); );
} }
return ( return <Text content={body} tags={ev.Tags} users={users || new Map()} creator={ev.PubKey} />;
<Text
content={body}
tags={ev.Tags}
users={users || new Map()}
creator={ev.PubKey}
/>
);
}, [ev]); }, [ev]);
useLayoutEffect(() => { useLayoutEffect(() => {
@ -139,9 +116,7 @@ export default function Note(props: NoteProps) {
mentions.push({ mentions.push({
pk, pk,
name: u.name ?? shortNpub, name: u.name ?? shortNpub,
link: ( link: <Link to={`/p/${npub}`}>{u.name ? `@${u.name}` : shortNpub}</Link>,
<Link to={`/p/${npub}`}>{u.name ? `@${u.name}` : shortNpub}</Link>
),
}); });
} else { } else {
mentions.push({ 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 othersLength = mentions.length - maxMentions;
const renderMention = (m: { link: React.ReactNode }, idx: number) => { const renderMention = (m: { link: React.ReactNode }, idx: number) => {
return ( return (
@ -162,13 +137,8 @@ export default function Note(props: NoteProps) {
); );
}; };
const pubMentions = const pubMentions =
mentions.length > maxMentions mentions.length > maxMentions ? mentions?.slice(0, maxMentions).map(renderMention) : mentions?.map(renderMention);
? mentions?.slice(0, maxMentions).map(renderMention) const others = mentions.length > maxMentions ? formatMessage(messages.Others, { n: othersLength }) : "";
: mentions?.map(renderMention);
const others =
mentions.length > maxMentions
? formatMessage(messages.Others, { n: othersLength })
: "";
return ( return (
<div className="reply"> <div className="reply">
re:&nbsp; re:&nbsp;
@ -178,11 +148,7 @@ export default function Note(props: NoteProps) {
{others} {others}
</> </>
) : ( ) : (
replyId && ( replyId && <Link to={eventLink(replyId)}>{hexToBech32("note", replyId)?.substring(0, 12)}</Link>
<Link to={eventLink(replyId)}>
{hexToBech32("note", replyId)?.substring(0, 12)}
</Link>
)
)} )}
</div> </div>
); );
@ -192,10 +158,7 @@ export default function Note(props: NoteProps) {
return ( return (
<> <>
<h4> <h4>
<FormattedMessage <FormattedMessage {...messages.UnknownEventKind} values={{ kind: ev.Kind }} />
{...messages.UnknownEventKind}
values={{ kind: ev.Kind }}
/>
</h4> </h4>
<pre>{JSON.stringify(ev.ToObject(), undefined, " ")}</pre> <pre>{JSON.stringify(ev.ToObject(), undefined, " ")}</pre>
</> </>
@ -207,10 +170,7 @@ export default function Note(props: NoteProps) {
return ( return (
<> <>
<p className="highlight"> <p className="highlight">
<FormattedMessage <FormattedMessage {...messages.TranslatedFrom} values={{ lang: translated.fromLanguage }} />
{...messages.TranslatedFrom}
values={{ lang: translated.fromLanguage }}
/>
</p> </p>
{translated.text} {translated.text}
</> </>
@ -230,10 +190,7 @@ export default function Note(props: NoteProps) {
<> <>
{options.showHeader && ( {options.showHeader && (
<div className="header flex"> <div className="header flex">
<ProfileImage <ProfileImage pubkey={ev.RootPubKey} subHeader={replyTag() ?? undefined} />
pubkey={ev.RootPubKey}
subHeader={replyTag() ?? undefined}
/>
{options.showTime && ( {options.showTime && (
<div className="info"> <div className="info">
<NoteTime from={ev.CreatedAt * 1000} /> <NoteTime from={ev.CreatedAt * 1000} />
@ -241,43 +198,27 @@ export default function Note(props: NoteProps) {
)} )}
</div> </div>
)} )}
<div className="body" onClick={(e) => goToEvent(e, ev.Id)}> <div className="body" onClick={e => goToEvent(e, ev.Id)}>
{transformBody()} {transformBody()}
{translation()} {translation()}
</div> </div>
{extendable && !showMore && ( {extendable && !showMore && (
<span <span className="expand-note mt10 flex f-center" onClick={() => setShowMore(true)}>
className="expand-note mt10 flex f-center"
onClick={() => setShowMore(true)}
>
<FormattedMessage {...messages.ShowMore} /> <FormattedMessage {...messages.ShowMore} />
</span> </span>
)} )}
{options.showFooter && ( {options.showFooter && <NoteFooter ev={ev} related={related} onTranslated={t => setTranslated(t)} />}
<NoteFooter
ev={ev}
related={related}
onTranslated={(t) => setTranslated(t)}
/>
)}
</> </>
); );
} }
const note = ( const note = (
<div <div
className={`${baseClassName}${highlight ? " active " : " "}${ className={`${baseClassName}${highlight ? " active " : " "}${extendable && !showMore ? " note-expand" : ""}`}
extendable && !showMore ? " note-expand" : "" ref={ref}>
}`}
ref={ref}
>
{content()} {content()}
</div> </div>
); );
return !ignoreModeration && isOpMuted ? ( return !ignoreModeration && isOpMuted ? <HiddenNote>{note}</HiddenNote> : note;
<HiddenNote>{note}</HiddenNote>
) : (
note
);
} }

View File

@ -48,9 +48,7 @@ export function NoteCreator(props: NoteCreatorProps) {
async function sendNote() { async function sendNote() {
if (note) { if (note) {
const ev = replyTo const ev = replyTo ? await publisher.reply(replyTo, note) : await publisher.note(note);
? await publisher.reply(replyTo, note)
: await publisher.note(note);
console.debug("Sending note: ", ev); console.debug("Sending note: ", ev);
publisher.broadcast(ev); publisher.broadcast(ev);
setNote(""); setNote("");
@ -68,7 +66,7 @@ export function NoteCreator(props: NoteCreatorProps) {
if (file) { if (file) {
const rx = await uploader.upload(file, file.name); const rx = await uploader.upload(file, file.name);
if (rx.url) { if (rx.url) {
setNote((n) => `${n ? `${n}\n` : ""}${rx.url}`); setNote(n => `${n ? `${n}\n` : ""}${rx.url}`);
} else if (rx?.error) { } else if (rx?.error) {
setError(rx.error); setError(rx.error);
} }
@ -125,11 +123,7 @@ export function NoteCreator(props: NoteCreatorProps) {
<FormattedMessage {...messages.Cancel} /> <FormattedMessage {...messages.Cancel} />
</button> </button>
<button type="button" onClick={onSubmit}> <button type="button" onClick={onSubmit}>
{replyTo ? ( {replyTo ? <FormattedMessage {...messages.Reply} /> : <FormattedMessage {...messages.Send} />}
<FormattedMessage {...messages.Reply} />
) : (
<FormattedMessage {...messages.Send} />
)}
</button> </button>
</div> </div>
</Modal> </Modal>

View File

@ -21,13 +21,7 @@ import Zap from "Icons/Zap";
import Reply from "Icons/Reply"; import Reply from "Icons/Reply";
import { formatShort } from "Number"; import { formatShort } from "Number";
import useEventPublisher from "Feed/EventPublisher"; import useEventPublisher from "Feed/EventPublisher";
import { import { getReactions, dedupeByPubkey, hexToBech32, normalizeReaction, Reaction } from "Util";
getReactions,
dedupeByPubkey,
hexToBech32,
normalizeReaction,
Reaction,
} from "Util";
import { NoteCreator } from "Element/NoteCreator"; import { NoteCreator } from "Element/NoteCreator";
import Reactions from "Element/Reactions"; import Reactions from "Element/Reactions";
import SendSats from "Element/SendSats"; import SendSats from "Element/SendSats";
@ -58,13 +52,9 @@ export interface NoteFooterProps {
export default function NoteFooter(props: NoteFooterProps) { export default function NoteFooter(props: NoteFooterProps) {
const { related, ev } = props; const { related, ev } = props;
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const login = useSelector<RootState, HexKey | undefined>( const login = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
(s) => s.login.publicKey
);
const { mute, block } = useModeration(); const { mute, block } = useModeration();
const prefs = useSelector<RootState, UserPreferences>( const prefs = useSelector<RootState, UserPreferences>(s => s.login.preferences);
(s) => s.login.preferences
);
const author = useUserProfile(ev.RootPubKey); const author = useUserProfile(ev.RootPubKey);
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const [reply, setReply] = useState(false); const [reply, setReply] = useState(false);
@ -75,29 +65,23 @@ export default function NoteFooter(props: NoteFooterProps) {
const langNames = new Intl.DisplayNames([...window.navigator.languages], { const langNames = new Intl.DisplayNames([...window.navigator.languages], {
type: "language", type: "language",
}); });
const reactions = useMemo( const reactions = useMemo(() => getReactions(related, ev.Id, EventKind.Reaction), [related, ev]);
() => getReactions(related, ev.Id, EventKind.Reaction), const reposts = useMemo(() => dedupeByPubkey(getReactions(related, ev.Id, EventKind.Repost)), [related, ev]);
[related, ev]
);
const reposts = useMemo(
() => dedupeByPubkey(getReactions(related, ev.Id, EventKind.Repost)),
[related, ev]
);
const zaps = useMemo(() => { const zaps = useMemo(() => {
const sortedZaps = getReactions(related, ev.Id, EventKind.ZapReceipt) const sortedZaps = getReactions(related, ev.Id, EventKind.ZapReceipt)
.map(parseZap) .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); sortedZaps.sort((a, b) => b.amount - a.amount);
return sortedZaps; return sortedZaps;
}, [related]); }, [related]);
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0); 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 groupReactions = useMemo(() => {
const result = reactions?.reduce( const result = reactions?.reduce(
(acc, reaction) => { (acc, reaction) => {
const kind = normalizeReaction(reaction.content); const kind = normalizeReaction(reaction.content);
const rs = acc[kind] || []; 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;
} }
return { ...acc, [kind]: [...rs, reaction] }; return { ...acc, [kind]: [...rs, reaction] };
@ -116,14 +100,11 @@ export default function NoteFooter(props: NoteFooterProps) {
const negative = groupReactions[Reaction.Negative]; const negative = groupReactions[Reaction.Negative];
function hasReacted(emoji: string) { function hasReacted(emoji: string) {
return reactions?.some( return reactions?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === login);
({ pubkey, content }) =>
normalizeReaction(content) === emoji && pubkey === login
);
} }
function hasReposted() { function hasReposted() {
return reposts.some((a) => a.pubkey === login); return reposts.some(a => a.pubkey === login);
} }
async function react(content: string) { async function react(content: string) {
@ -134,11 +115,7 @@ export default function NoteFooter(props: NoteFooterProps) {
} }
async function deleteEvent() { async function deleteEvent() {
if ( if (window.confirm(formatMessage(messages.ConfirmDeletion, { id: ev.Id.substring(0, 8) }))) {
window.confirm(
formatMessage(messages.ConfirmDeletion, { id: ev.Id.substring(0, 8) })
)
) {
const evDelete = await publisher.delete(ev.Id); const evDelete = await publisher.delete(ev.Id);
publisher.broadcast(evDelete); publisher.broadcast(evDelete);
} }
@ -146,10 +123,7 @@ export default function NoteFooter(props: NoteFooterProps) {
async function repost() { async function repost() {
if (!hasReposted()) { if (!hasReposted()) {
if ( if (!prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id }))) {
!prefs.confirmReposts ||
window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id }))
) {
const evRepost = await publisher.repost(ev); const evRepost = await publisher.repost(ev);
publisher.broadcast(evRepost); publisher.broadcast(evRepost);
} }
@ -161,18 +135,11 @@ export default function NoteFooter(props: NoteFooterProps) {
if (service) { if (service) {
return ( return (
<> <>
<div <div className={`reaction-pill ${didZap ? "reacted" : ""}`} onClick={() => setTip(true)}>
className={`reaction-pill ${didZap ? "reacted" : ""}`}
onClick={() => setTip(true)}
>
<div className="reaction-pill-icon"> <div className="reaction-pill-icon">
<Zap /> <Zap />
</div> </div>
{zapTotal > 0 && ( {zapTotal > 0 && <div className="reaction-pill-number">{formatShort(zapTotal)}</div>}
<div className="reaction-pill-number">
{formatShort(zapTotal)}
</div>
)}
</div> </div>
</> </>
); );
@ -182,18 +149,11 @@ export default function NoteFooter(props: NoteFooterProps) {
function repostIcon() { function repostIcon() {
return ( return (
<div <div className={`reaction-pill ${hasReposted() ? "reacted" : ""}`} onClick={() => repost()}>
className={`reaction-pill ${hasReposted() ? "reacted" : ""}`}
onClick={() => repost()}
>
<div className="reaction-pill-icon"> <div className="reaction-pill-icon">
<FontAwesomeIcon icon={faRepeat} /> <FontAwesomeIcon icon={faRepeat} />
</div> </div>
{reposts.length > 0 && ( {reposts.length > 0 && <div className="reaction-pill-number">{formatShort(reposts.length)}</div>}
<div className="reaction-pill-number">
{formatShort(reposts.length)}
</div>
)}
</div> </div>
); );
} }
@ -204,16 +164,11 @@ export default function NoteFooter(props: NoteFooterProps) {
} }
return ( return (
<> <>
<div <div className={`reaction-pill ${hasReacted("+") ? "reacted" : ""} `} onClick={() => react("+")}>
className={`reaction-pill ${hasReacted("+") ? "reacted" : ""} `}
onClick={() => react("+")}
>
<div className="reaction-pill-icon"> <div className="reaction-pill-icon">
<Heart /> <Heart />
</div> </div>
<div className="reaction-pill-number"> <div className="reaction-pill-number">{formatShort(positive.length)}</div>
{formatShort(positive.length)}
</div>
</div> </div>
{repostIcon()} {repostIcon()}
</> </>
@ -221,9 +176,7 @@ export default function NoteFooter(props: NoteFooterProps) {
} }
async function share() { async function share() {
const url = `${window.location.protocol}//${ const url = `${window.location.protocol}//${window.location.host}/e/${hexToBech32("note", ev.Id)}`;
window.location.host
}/e/${hexToBech32("note", ev.Id)}`;
if ("share" in window.navigator) { if ("share" in window.navigator) {
await window.navigator.share({ await window.navigator.share({
title: "Snort", title: "Snort",
@ -262,9 +215,7 @@ export default function NoteFooter(props: NoteFooterProps) {
} }
async function copyEvent() { async function copyEvent() {
await navigator.clipboard.writeText( await navigator.clipboard.writeText(JSON.stringify(ev.Original, undefined, " "));
JSON.stringify(ev.Original, undefined, " ")
);
} }
function menuItems() { function menuItems() {
@ -291,10 +242,7 @@ export default function NoteFooter(props: NoteFooterProps) {
{prefs.enableReactions && ( {prefs.enableReactions && (
<MenuItem onClick={() => react("-")}> <MenuItem onClick={() => react("-")}>
<Dislike /> <Dislike />
<FormattedMessage <FormattedMessage {...messages.Dislike} values={{ n: negative.length }} />
{...messages.Dislike}
values={{ n: negative.length }}
/>
</MenuItem> </MenuItem>
)} )}
<MenuItem onClick={() => block(ev.PubKey)}> <MenuItem onClick={() => block(ev.PubKey)}>
@ -303,10 +251,7 @@ export default function NoteFooter(props: NoteFooterProps) {
</MenuItem> </MenuItem>
<MenuItem onClick={() => translate()}> <MenuItem onClick={() => translate()}>
<FontAwesomeIcon icon={faLanguage} /> <FontAwesomeIcon icon={faLanguage} />
<FormattedMessage <FormattedMessage {...messages.TranslateTo} values={{ lang: langNames.of(lang.split("-")[0]) }} />
{...messages.TranslateTo}
values={{ lang: langNames.of(lang.split("-")[0]) }}
/>
</MenuItem> </MenuItem>
{prefs.showDebugMenus && ( {prefs.showDebugMenus && (
<MenuItem onClick={() => copyEvent()}> <MenuItem onClick={() => copyEvent()}>
@ -330,10 +275,7 @@ export default function NoteFooter(props: NoteFooterProps) {
<div className="footer-reactions"> <div className="footer-reactions">
{tipButton()} {tipButton()}
{reactionIcons()} {reactionIcons()}
<div <div className={`reaction-pill ${reply ? "reacted" : ""}`} onClick={() => setReply(s => !s)}>
className={`reaction-pill ${reply ? "reacted" : ""}`}
onClick={() => setReply((s) => !s)}
>
<div className="reaction-pill-icon"> <div className="reaction-pill-icon">
<Reply /> <Reply />
</div> </div>
@ -346,18 +288,11 @@ export default function NoteFooter(props: NoteFooterProps) {
</div> </div>
</div> </div>
} }
menuClassName="ctx-menu" menuClassName="ctx-menu">
>
{menuItems()} {menuItems()}
</Menu> </Menu>
</div> </div>
<NoteCreator <NoteCreator autoFocus={true} replyTo={ev} onSend={() => setReply(false)} show={reply} setShow={setReply} />
autoFocus={true}
replyTo={ev}
onSend={() => setReply(false)}
show={reply}
setShow={setReply}
/>
<Reactions <Reactions
show={showReactions} show={showReactions}
setShow={setShowReactions} setShow={setShowReactions}

View File

@ -23,7 +23,7 @@ export default function NoteReaction(props: NoteReactionProps) {
const refEvent = useMemo(() => { const refEvent = useMemo(() => {
if (ev) { if (ev) {
const eTags = ev.Tags.filter((a) => a.Key === "e"); const eTags = ev.Tags.filter(a => a.Key === "e");
if (eTags.length > 0) { if (eTags.length > 0) {
return eTags[0].Event; return eTags[0].Event;
} }
@ -39,11 +39,7 @@ export default function NoteReaction(props: NoteReactionProps) {
* Some clients embed the reposted note in the content * Some clients embed the reposted note in the content
*/ */
function extractRoot() { function extractRoot() {
if ( if (ev?.Kind === EventKind.Repost && ev.Content.length > 0 && ev.Content !== "#[0]") {
ev?.Kind === EventKind.Repost &&
ev.Content.length > 0 &&
ev.Content !== "#[0]"
) {
try { try {
const r: RawEvent = JSON.parse(ev.Content); const r: RawEvent = JSON.parse(ev.Content);
return r as TaggedRawEvent; return r as TaggedRawEvent;
@ -73,9 +69,7 @@ export default function NoteReaction(props: NoteReactionProps) {
{root ? <Note data={root} options={opt} related={[]} /> : null} {root ? <Note data={root} options={opt} related={[]} /> : null}
{!root && refEvent ? ( {!root && refEvent ? (
<p> <p>
<Link to={eventLink(refEvent)}> <Link to={eventLink(refEvent)}>#{hexToBech32("note", refEvent).substring(0, 12)}</Link>
#{hexToBech32("note", refEvent).substring(0, 12)}
</Link>
</p> </p>
) : null} ) : null}
</div> </div>

View File

@ -31,10 +31,7 @@ export default function NoteTime(props: NoteTimeProps) {
weekday: "short", weekday: "short",
}); });
} else if (absAgo > HourInMs) { } else if (absAgo > HourInMs) {
return `${fromDate.getHours().toString().padStart(2, "0")}:${fromDate return `${fromDate.getHours().toString().padStart(2, "0")}:${fromDate.getMinutes().toString().padStart(2, "0")}`;
.getMinutes()
.toString()
.padStart(2, "0")}`;
} else if (absAgo < MinuteInMs) { } else if (absAgo < MinuteInMs) {
return fallback; return fallback;
} else { } else {
@ -49,7 +46,7 @@ export default function NoteTime(props: NoteTimeProps) {
useEffect(() => { useEffect(() => {
setTime(calcTime()); setTime(calcTime());
const t = setInterval(() => { const t = setInterval(() => {
setTime((s) => { setTime(s => {
const newTime = calcTime(); const newTime = calcTime();
if (newTime !== s) { if (newTime !== s) {
return newTime; return newTime;

View File

@ -20,19 +20,13 @@ function NoteLabel({ pubkey }: NoteToSelfProps) {
const user = useUserProfile(pubkey); const user = useUserProfile(pubkey);
return ( return (
<div> <div>
<FormattedMessage {...messages.NoteToSelf} />{" "} <FormattedMessage {...messages.NoteToSelf} /> <FontAwesomeIcon icon={faCertificate} size="xs" />
<FontAwesomeIcon icon={faCertificate} size="xs" />
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />} {user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
</div> </div>
); );
} }
export default function NoteToSelf({ export default function NoteToSelf({ pubkey, clickable, className, link }: NoteToSelfProps) {
pubkey,
clickable,
className,
link,
}: NoteToSelfProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const clickLink = () => { const clickLink = () => {
@ -45,12 +39,7 @@ export default function NoteToSelf({
<div className={`nts${className ? ` ${className}` : ""}`}> <div className={`nts${className ? ` ${className}` : ""}`}>
<div className="avatar-wrapper"> <div className="avatar-wrapper">
<div className={`avatar${clickable ? " clickable" : ""}`}> <div className={`avatar${clickable ? " clickable" : ""}`}>
<FontAwesomeIcon <FontAwesomeIcon onClick={clickLink} className="note-to-self" icon={faBook} size="2xl" />
onClick={clickLink}
className="note-to-self"
icon={faBook}
size="2xl"
/>
</div> </div>
</div> </div>
<div className="f-grow"> <div className="f-grow">

View File

@ -17,13 +17,7 @@ export interface ProfileImageProps {
link?: string; link?: string;
} }
export default function ProfileImage({ export default function ProfileImage({ pubkey, subHeader, showUsername = true, className, link }: ProfileImageProps) {
pubkey,
subHeader,
showUsername = true,
className,
link,
}: ProfileImageProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const user = useUserProfile(pubkey); const user = useUserProfile(pubkey);
@ -34,19 +28,12 @@ export default function ProfileImage({
return ( return (
<div className={`pfp${className ? ` ${className}` : ""}`}> <div className={`pfp${className ? ` ${className}` : ""}`}>
<div className="avatar-wrapper"> <div className="avatar-wrapper">
<Avatar <Avatar user={user} onClick={() => navigate(link ?? profileLink(pubkey))} />
user={user}
onClick={() => navigate(link ?? profileLink(pubkey))}
/>
</div> </div>
{showUsername && ( {showUsername && (
<div className="profile-name f-grow"> <div className="profile-name f-grow">
<div className="username"> <div className="username">
<Link <Link className="display-name" key={pubkey} to={link ?? profileLink(pubkey)}>
className="display-name"
key={pubkey}
to={link ?? profileLink(pubkey)}
>
{name} {name}
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />} {user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
</Link> </Link>
@ -58,10 +45,7 @@ export default function ProfileImage({
); );
} }
export function getDisplayName( export function getDisplayName(user: MetadataCache | undefined, pubkey: HexKey) {
user: MetadataCache | undefined,
pubkey: HexKey
) {
let name = hexToBech32("npub", pubkey).substring(0, 12); let name = hexToBech32("npub", pubkey).substring(0, 12);
if (user?.display_name !== undefined && user.display_name.length > 0) { if (user?.display_name !== undefined && user.display_name.length > 0) {
name = user.display_name; name = user.display_name;

View File

@ -25,21 +25,12 @@ export default function ProfilePreview(props: ProfilePreviewProps) {
}; };
return ( return (
<div <div className={`profile-preview${props.className ? ` ${props.className}` : ""}`} ref={ref}>
className={`profile-preview${
props.className ? ` ${props.className}` : ""
}`}
ref={ref}
>
{inView && ( {inView && (
<> <>
<ProfileImage <ProfileImage
pubkey={pubkey} pubkey={pubkey}
subHeader={ subHeader={options.about ? <div className="f-ellipsis about">{user?.about}</div> : undefined}
options.about ? (
<div className="f-ellipsis about">{user?.about}</div>
) : undefined
}
/> />
{props.actions ?? ( {props.actions ?? (
<div className="follow-button-container"> <div className="follow-button-container">

View File

@ -1,11 +1,7 @@
import useImgProxy from "Feed/ImgProxy"; import useImgProxy from "Feed/ImgProxy";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
interface ProxyImgProps interface ProxyImgProps extends React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement> {
extends React.DetailedHTMLProps<
React.ImgHTMLAttributes<HTMLImageElement>,
HTMLImageElement
> {
size?: number; size?: number;
} }
@ -17,7 +13,7 @@ export const ProxyImg = (props: ProxyImgProps) => {
useEffect(() => { useEffect(() => {
if (src) { if (src) {
proxy(src, size) proxy(src, size)
.then((a) => setUrl(a)) .then(a => setUrl(a))
.catch(console.warn); .catch(console.warn);
} }
}, [src]); }, [src]);

View File

@ -28,14 +28,7 @@ interface ReactionsProps {
zaps: ParsedZap[]; zaps: ParsedZap[];
} }
const Reactions = ({ const Reactions = ({ show, setShow, positive, negative, reposts, zaps }: ReactionsProps) => {
show,
setShow,
positive,
negative,
reposts,
zaps,
}: ReactionsProps) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const onClose = () => setShow(false); const onClose = () => setShow(false);
const likes = useMemo(() => { const likes = useMemo(() => {
@ -48,8 +41,7 @@ const Reactions = ({
sorted.sort((a, b) => b.created_at - a.created_at); sorted.sort((a, b) => b.created_at - a.created_at);
return sorted; return sorted;
}, [negative]); }, [negative]);
const total = const total = positive.length + negative.length + zaps.length + reposts.length;
positive.length + negative.length + zaps.length + reposts.length;
const defaultTabs: Tab[] = [ const defaultTabs: Tab[] = [
{ {
text: formatMessage(messages.Likes, { n: likes.length }), text: formatMessage(messages.Likes, { n: likes.length }),
@ -93,24 +85,17 @@ const Reactions = ({
</div> </div>
<div className="reactions-header"> <div className="reactions-header">
<h2> <h2>
<FormattedMessage <FormattedMessage {...messages.ReactionsCount} values={{ n: total }} />
{...messages.ReactionsCount}
values={{ n: total }}
/>
</h2> </h2>
</div> </div>
<Tabs tabs={tabs} tab={tab} setTab={setTab} /> <Tabs tabs={tabs} tab={tab} setTab={setTab} />
<div className="body" key={tab.value}> <div className="body" key={tab.value}>
{tab.value === 0 && {tab.value === 0 &&
likes.map((ev) => { likes.map(ev => {
return ( return (
<div key={ev.id} className="reactions-item"> <div key={ev.id} className="reactions-item">
<div className="reaction-icon"> <div className="reaction-icon">
{ev.content === "+" ? ( {ev.content === "+" ? <Heart width={20} height={18} /> : ev.content}
<Heart width={20} height={18} />
) : (
ev.content
)}
</div> </div>
<ProfileImage pubkey={ev.pubkey} /> <ProfileImage pubkey={ev.pubkey} />
<FollowButton pubkey={ev.pubkey} /> <FollowButton pubkey={ev.pubkey} />
@ -118,23 +103,22 @@ const Reactions = ({
); );
})} })}
{tab.value === 1 && {tab.value === 1 &&
zaps.map((z) => { zaps.map(z => {
return ( return (
<div key={z.id} className="reactions-item"> z.zapper && (
<div className="zap-reaction-icon"> <div key={z.id} className="reactions-item">
<ZapIcon width={17} height={20} /> <div className="zap-reaction-icon">
<span className="zap-amount">{formatShort(z.amount)}</span> <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> </div>
<ProfileImage )
pubkey={z.zapper ?? ""}
subHeader={<>{z.content}</>}
/>
<FollowButton pubkey={z.zapper ?? ""} />
</div>
); );
})} })}
{tab.value === 2 && {tab.value === 2 &&
reposts.map((ev) => { reposts.map(ev => {
return ( return (
<div key={ev.id} className="reactions-item"> <div key={ev.id} className="reactions-item">
<div className="reaction-icon"> <div className="reaction-icon">
@ -146,7 +130,7 @@ const Reactions = ({
); );
})} })}
{tab.value === 3 && {tab.value === 3 &&
dislikes.map((ev) => { dislikes.map(ev => {
return ( return (
<div key={ev.id} className="reactions-item"> <div key={ev.id} className="reactions-item">
<div className="reaction-icon"> <div className="reaction-icon">

View File

@ -27,10 +27,7 @@ export default function Relay(props: RelayProps) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const navigate = useNavigate(); const navigate = useNavigate();
const allRelaySettings = useSelector< const allRelaySettings = useSelector<RootState, Record<string, RelaySettings>>(s => s.login.relays);
RootState,
Record<string, RelaySettings>
>((s) => s.login.relays);
const relaySettings = allRelaySettings[props.addr]; const relaySettings = allRelaySettings[props.addr];
const state = useRelayState(props.addr); const state = useRelayState(props.addr);
const name = useMemo(() => new URL(props.addr).host, [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, write: !relaySettings.write,
read: relaySettings.read, read: relaySettings.read,
}) })
} }>
> <FontAwesomeIcon icon={relaySettings.write ? faSquareCheck : faSquareXmark} />
<FontAwesomeIcon
icon={relaySettings.write ? faSquareCheck : faSquareXmark}
/>
</span> </span>
</div> </div>
<div className="f-1"> <div className="f-1">
@ -82,11 +76,8 @@ export default function Relay(props: RelayProps) {
write: relaySettings.write, write: relaySettings.write,
read: !relaySettings.read, read: !relaySettings.read,
}) })
} }>
> <FontAwesomeIcon icon={relaySettings.read ? faSquareCheck : faSquareXmark} />
<FontAwesomeIcon
icon={relaySettings.read ? faSquareCheck : faSquareXmark}
/>
</span> </span>
</div> </div>
</div> </div>
@ -104,10 +95,7 @@ export default function Relay(props: RelayProps) {
<FontAwesomeIcon icon={faPlugCircleXmark} /> {state?.disconnects} <FontAwesomeIcon icon={faPlugCircleXmark} /> {state?.disconnects}
</div> </div>
<div> <div>
<span <span className="icon-btn" onClick={() => navigate(state?.id ?? "")}>
className="icon-btn"
onClick={() => navigate(state?.id ?? "")}
>
<FontAwesomeIcon icon={faGear} /> <FontAwesomeIcon icon={faGear} />
</span> </span>
</div> </div>

View File

@ -54,9 +54,7 @@ export default function LNURLTip(props: LNURLTipProps) {
const service = props.svc; const service = props.svc;
const show = props.show || false; const show = props.show || false;
const { note, author, target } = props; const { note, author, target } = props;
const amounts = [ const amounts = [500, 1_000, 5_000, 10_000, 20_000, 50_000, 100_000, 1_000_000];
500, 1_000, 5_000, 10_000, 20_000, 50_000, 100_000, 1_000_000,
];
const emojis: Record<number, string> = { const emojis: Record<number, string> = {
1_000: "👍", 1_000: "👍",
5_000: "💜", 5_000: "💜",
@ -77,13 +75,12 @@ export default function LNURLTip(props: LNURLTipProps) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const horizontalScroll = useHorizontalScroll(); const horizontalScroll = useHorizontalScroll();
const canComment = const canComment = (payService?.commentAllowed ?? 0) > 0 || payService?.nostrPubkey;
(payService?.commentAllowed ?? 0) > 0 || payService?.nostrPubkey;
useEffect(() => { useEffect(() => {
if (show && !props.invoice) { if (show && !props.invoice) {
loadService() loadService()
.then((a) => setPayService(a ?? undefined)) .then(a => setPayService(a ?? undefined))
.catch(() => setError(formatMessage(messages.LNURLFail))); .catch(() => setError(formatMessage(messages.LNURLFail)));
} else { } else {
setPayService(undefined); setPayService(undefined);
@ -99,13 +96,11 @@ export default function LNURLTip(props: LNURLTipProps) {
if (payService) { if (payService) {
const min = (payService.minSendable ?? 0) / 1000; const min = (payService.minSendable ?? 0) / 1000;
const max = (payService.maxSendable ?? 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 []; return [];
}, [payService]); }, [payService]);
// TODO Why was this never used? I think this might be a bug, or was it just an oversight?
const selectAmount = (a: number) => { const selectAmount = (a: number) => {
setError(undefined); setError(undefined);
setInvoice(undefined); setInvoice(undefined);
@ -141,14 +136,10 @@ export default function LNURLTip(props: LNURLTipProps) {
if (!amount || !payService) return null; if (!amount || !payService) return null;
let url = ""; let url = "";
const amountParam = `amount=${Math.floor(amount * 1000)}`; const amountParam = `amount=${Math.floor(amount * 1000)}`;
const commentParam = const commentParam = comment && payService?.commentAllowed ? `&comment=${encodeURIComponent(comment)}` : "";
comment && payService?.commentAllowed
? `&comment=${encodeURIComponent(comment)}`
: "";
if (payService.nostrPubkey && author) { if (payService.nostrPubkey && author) {
const ev = await publisher.zap(author, note, comment); const ev = await publisher.zap(author, note, comment);
const nostrParam = const nostrParam = ev && `&nostr=${encodeURIComponent(JSON.stringify(ev.ToObject()))}`;
ev && `&nostr=${encodeURIComponent(JSON.stringify(ev.ToObject()))}`;
url = `${payService.callback}?${amountParam}${commentParam}${nostrParam}`; url = `${payService.callback}?${amountParam}${commentParam}${nostrParam}`;
} else { } else {
url = `${payService.callback}?${amountParam}${commentParam}`; url = `${payService.callback}?${amountParam}${commentParam}`;
@ -185,14 +176,13 @@ export default function LNURLTip(props: LNURLTipProps) {
className="f-grow mr10" className="f-grow mr10"
placeholder={formatMessage(messages.Custom)} placeholder={formatMessage(messages.Custom)}
value={customAmount} value={customAmount}
onChange={(e) => setCustomAmount(parseInt(e.target.value))} onChange={e => setCustomAmount(parseInt(e.target.value))}
/> />
<button <button
className="secondary" className="secondary"
type="button" type="button"
disabled={!customAmount} disabled={!customAmount}
onClick={() => selectAmount(customAmount ?? 0)} onClick={() => selectAmount(customAmount ?? 0)}>
>
<FormattedMessage {...messages.Confirm} /> <FormattedMessage {...messages.Confirm} />
</button> </button>
</div> </div>
@ -222,12 +212,8 @@ export default function LNURLTip(props: LNURLTipProps) {
<FormattedMessage {...messages.ZapAmount} /> <FormattedMessage {...messages.ZapAmount} />
</h3> </h3>
<div className="amounts" ref={horizontalScroll}> <div className="amounts" ref={horizontalScroll}>
{serviceAmounts.map((a) => ( {serviceAmounts.map(a => (
<span <span className={`sat-amount ${amount === a ? "active" : ""}`} key={a} onClick={() => selectAmount(a)}>
className={`sat-amount ${amount === a ? "active" : ""}`}
key={a}
onClick={() => selectAmount(a)}
>
{emojis[a] && <>{emojis[a]}&nbsp;</>} {emojis[a] && <>{emojis[a]}&nbsp;</>}
{formatShort(a)} {formatShort(a)}
</span> </span>
@ -241,28 +227,18 @@ export default function LNURLTip(props: LNURLTipProps) {
placeholder={formatMessage(messages.Comment)} placeholder={formatMessage(messages.Comment)}
className="f-grow" className="f-grow"
maxLength={payService?.commentAllowed || 120} maxLength={payService?.commentAllowed || 120}
onChange={(e) => setComment(e.target.value)} onChange={e => setComment(e.target.value)}
/> />
)} )}
</div> </div>
{(amount ?? 0) > 0 && ( {(amount ?? 0) > 0 && (
<button <button type="button" className="zap-action" onClick={() => loadInvoice()}>
type="button"
className="zap-action"
onClick={() => loadInvoice()}
>
<div className="zap-action-container"> <div className="zap-action-container">
<Zap /> <Zap />
{target ? ( {target ? (
<FormattedMessage <FormattedMessage {...messages.ZapTarget} values={{ target, n: formatShort(amount) }} />
{...messages.ZapTarget}
values={{ target, n: formatShort(amount) }}
/>
) : ( ) : (
<FormattedMessage <FormattedMessage {...messages.ZapSats} values={{ n: formatShort(amount) }} />
{...messages.ZapSats}
values={{ n: formatShort(amount) }}
/>
)} )}
</div> </div>
</button> </button>
@ -285,11 +261,7 @@ export default function LNURLTip(props: LNURLTipProps) {
<div className="copy-action"> <div className="copy-action">
<Copy text={pr} maxSize={26} /> <Copy text={pr} maxSize={26} />
</div> </div>
<button <button className="wallet-action" type="button" onClick={() => window.open(`lightning:${pr}`)}>
className="wallet-action"
type="button"
onClick={() => window.open(`lightning:${pr}`)}
>
<FormattedMessage {...messages.OpenWallet} /> <FormattedMessage {...messages.OpenWallet} />
</button> </button>
</> </>
@ -319,9 +291,7 @@ export default function LNURLTip(props: LNURLTipProps) {
); );
} }
const defaultTitle = payService?.nostrPubkey const defaultTitle = payService?.nostrPubkey ? formatMessage(messages.SendZap) : formatMessage(messages.SendSats);
? formatMessage(messages.SendZap)
: formatMessage(messages.SendSats);
const title = target const title = target
? formatMessage(messages.ToTarget, { ? formatMessage(messages.ToTarget, {
action: defaultTitle, action: defaultTitle,
@ -331,7 +301,7 @@ export default function LNURLTip(props: LNURLTipProps) {
if (!show) return null; if (!show) return null;
return ( return (
<Modal className="lnurl-modal" onClose={onClose}> <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}> <div className="close" onClick={onClose}>
<Close /> <Close />
</div> </div>

View File

@ -37,12 +37,6 @@
} }
.skeleton::after { .skeleton::after {
background-image: linear-gradient( background-image: linear-gradient(90deg, #50535a 0%, #656871 20%, #50535a 40%, #50535a 100%);
90deg,
#50535a 0%,
#656871 20%,
#50535a 40%,
#50535a 100%
);
} }
} }

View File

@ -8,22 +8,13 @@ interface ISkepetonProps {
margin?: string; margin?: string;
} }
export default function Skeleton({ export default function Skeleton({ children, width, height, margin, loading = true }: ISkepetonProps) {
children,
width,
height,
margin,
loading = true,
}: ISkepetonProps) {
if (!loading) { if (!loading) {
return <>{children}</>; return <>{children}</>;
} }
return ( return (
<div <div className="skeleton" style={{ width: width, height: height, margin: margin }}>
className="skeleton"
style={{ width: width, height: height, margin: margin }}
>
{children} {children}
</div> </div>
); );

View File

@ -5,8 +5,7 @@ const SoundCloudEmbed = ({ link }: { link: string }) => {
height="166" height="166"
scrolling="no" scrolling="no"
allow="autoplay" allow="autoplay"
src={`https://w.soundcloud.com/player/?url=${link}`} src={`https://w.soundcloud.com/player/?url=${link}`}></iframe>
></iframe>
); );
}; };

View File

@ -1,8 +1,5 @@
const SpotifyEmbed = ({ link }: { link: string }) => { const SpotifyEmbed = ({ link }: { link: string }) => {
const convertedUrl = link.replace( const convertedUrl = link.replace(/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/, "/embed/$1/$2");
/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/,
"/embed/$1/$2"
);
return ( return (
<iframe <iframe
@ -12,8 +9,7 @@ const SpotifyEmbed = ({ link }: { link: string }) => {
height="352" height="352"
frameBorder="0" frameBorder="0"
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy" loading="lazy"></iframe>
></iframe>
); );
}; };

View File

@ -20,11 +20,8 @@ interface TabElementProps extends Omit<TabsProps, "tabs"> {
export const TabElement = ({ t, tab, setTab }: TabElementProps) => { export const TabElement = ({ t, tab, setTab }: TabElementProps) => {
return ( return (
<div <div
className={`tab ${tab.value === t.value ? "active" : ""} ${ className={`tab ${tab.value === t.value ? "active" : ""} ${t.disabled ? "disabled" : ""}`}
t.disabled ? "disabled" : "" onClick={() => !t.disabled && setTab(t)}>
}`}
onClick={() => !t.disabled && setTab(t)}
>
{t.text} {t.text}
</div> </div>
); );
@ -33,7 +30,7 @@ export const TabElement = ({ t, tab, setTab }: TabElementProps) => {
const Tabs = ({ tabs, tab, setTab }: TabsProps) => { const Tabs = ({ tabs, tab, setTab }: TabsProps) => {
return ( return (
<div className="tabs"> <div className="tabs">
{tabs.map((t) => ( {tabs.map(t => (
<TabElement tab={tab} setTab={setTab} t={t} /> <TabElement tab={tab} setTab={setTab} t={t} />
))} ))}
</div> </div>

View File

@ -34,9 +34,9 @@ export interface TextProps {
export default function Text({ content, tags, creator, users }: TextProps) { export default function Text({ content, tags, creator, users }: TextProps) {
function extractLinks(fragments: Fragment[]) { function extractLinks(fragments: Fragment[]) {
return fragments return fragments
.map((f) => { .map(f => {
if (typeof f === "string") { if (typeof f === "string") {
return f.split(UrlRegex).map((a) => { return f.split(UrlRegex).map(a => {
if (a.startsWith("http")) { if (a.startsWith("http")) {
return <HyperText link={a} creator={creator} />; return <HyperText link={a} creator={creator} />;
} }
@ -50,29 +50,22 @@ export default function Text({ content, tags, creator, users }: TextProps) {
function extractMentions(frag: TextFragment) { function extractMentions(frag: TextFragment) {
return frag.body return frag.body
.map((f) => { .map(f => {
if (typeof f === "string") { if (typeof f === "string") {
return f.split(MentionRegex).map((match) => { return f.split(MentionRegex).map(match => {
const matchTag = match.match(/#\[(\d+)\]/); const matchTag = match.match(/#\[(\d+)\]/);
if (matchTag && matchTag.length === 2) { if (matchTag && matchTag.length === 2) {
const idx = parseInt(matchTag[1]); 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) { if (ref) {
switch (ref.Key) { switch (ref.Key) {
case "p": { case "p": {
return <Mention pubkey={ref.PubKey ?? ""} />; return <Mention pubkey={ref.PubKey ?? ""} />;
} }
case "e": { case "e": {
const eText = hexToBech32( const eText = hexToBech32("note", ref.Event).substring(0, 12);
"note",
ref.Event ?? ""
).substring(0, 12);
return ( return (
<Link <Link key={ref.Event} to={eventLink(ref.Event ?? "")} onClick={e => e.stopPropagation()}>
key={ref.Event}
to={eventLink(ref.Event ?? "")}
onClick={(e) => e.stopPropagation()}
>
#{eText} #{eText}
</Link> </Link>
); );
@ -95,9 +88,9 @@ export default function Text({ content, tags, creator, users }: TextProps) {
function extractInvoices(fragments: Fragment[]) { function extractInvoices(fragments: Fragment[]) {
return fragments return fragments
.map((f) => { .map(f => {
if (typeof f === "string") { if (typeof f === "string") {
return f.split(InvoiceRegex).map((i) => { return f.split(InvoiceRegex).map(i => {
if (i.toLowerCase().startsWith("lnbc")) { if (i.toLowerCase().startsWith("lnbc")) {
return <Invoice key={i} invoice={i} />; return <Invoice key={i} invoice={i} />;
} else { } else {
@ -112,9 +105,9 @@ export default function Text({ content, tags, creator, users }: TextProps) {
function extractHashtags(fragments: Fragment[]) { function extractHashtags(fragments: Fragment[]) {
return fragments return fragments
.map((f) => { .map(f => {
if (typeof f === "string") { if (typeof f === "string") {
return f.split(HashtagRegex).map((i) => { return f.split(HashtagRegex).map(i => {
if (i.toLowerCase().startsWith("#")) { if (i.toLowerCase().startsWith("#")) {
return <Hashtag tag={i.substring(1)} />; return <Hashtag tag={i.substring(1)} />;
} else { } else {
@ -134,7 +127,7 @@ export default function Text({ content, tags, creator, users }: TextProps) {
function transformParagraph(frag: TextFragment) { function transformParagraph(frag: TextFragment) {
const fragments = transformText(frag); const fragments = transformText(frag);
if (fragments.every((f) => typeof f === "string")) { if (fragments.every(f => typeof f === "string")) {
return <p>{fragments}</p>; return <p>{fragments}</p>;
} }
return <>{fragments}</>; return <>{fragments}</>;
@ -150,13 +143,9 @@ export default function Text({ content, tags, creator, users }: TextProps) {
const components = useMemo(() => { const components = useMemo(() => {
return { return {
p: (x: { children?: React.ReactNode[] }) => p: (x: { children?: React.ReactNode[] }) => transformParagraph({ body: x.children ?? [], tags, users }),
transformParagraph({ body: x.children ?? [], tags, users }), a: (x: { href?: string }) => <HyperText link={x.href ?? ""} creator={creator} />,
a: (x: { href?: string }) => ( li: (x: { children?: Fragment[] }) => transformLi({ body: x.children ?? [], tags, users }),
<HyperText link={x.href ?? ""} creator={creator} />
),
li: (x: { children?: Fragment[] }) =>
transformLi({ body: x.children ?? [], tags, users }),
}; };
}, [content]); }, [content]);
@ -178,9 +167,7 @@ export default function Text({ content, tags, creator, users }: TextProps) {
) { ) {
node.type = "text"; node.type = "text";
const position = unwrap(node.position); const position = unwrap(node.position);
node.value = content node.value = content.slice(position.start.offset, position.end.offset).replace(/\)$/, " )");
.slice(position.start.offset, position.end.offset)
.replace(/\)$/, " )");
return SKIP; return SKIP;
} }
}); });
@ -188,11 +175,7 @@ export default function Text({ content, tags, creator, users }: TextProps) {
[content] [content]
); );
return ( return (
<ReactMarkdown <ReactMarkdown className="text" components={components} remarkPlugins={[disableMarkdownLinks]}>
className="text"
components={components}
remarkPlugins={[disableMarkdownLinks]}
>
{content} {content}
</ReactMarkdown> </ReactMarkdown>
); );

View File

@ -85,11 +85,8 @@ const Textarea = (props: TextareaProps) => {
"@": { "@": {
afterWhitespace: true, afterWhitespace: true,
dataProvider: userDataProvider, dataProvider: userDataProvider,
component: (props: { entity: MetadataCache }) => ( component: (props: { entity: MetadataCache }) => <UserItem {...props.entity} />,
<UserItem {...props.entity} /> output: (item: { pubkey: string }) => `@${hexToBech32("npub", item.pubkey)}`,
),
output: (item: { pubkey: string }) =>
`@${hexToBech32("npub", item.pubkey)}`,
}, },
}} }}
/> />

View File

@ -87,8 +87,7 @@
} }
@media (min-width: 720px) { @media (min-width: 720px) {
.subthread-container.subthread-mid:not(.subthread-last) .subthread-container.subthread-mid:not(.subthread-last) .line-container:after {
.line-container:after {
left: 48px; left: 48px;
} }
} }
@ -103,8 +102,7 @@
} }
@media (min-width: 720px) { @media (min-width: 720px) {
.subthread-container.subthread-mid:not(.subthread-last) .subthread-container.subthread-mid:not(.subthread-last) .line-container:after {
.line-container:after {
left: 48px; left: 48px;
} }
} }

View File

@ -13,12 +13,9 @@ import NoteGhost from "Element/NoteGhost";
import Collapsed from "Element/Collapsed"; import Collapsed from "Element/Collapsed";
import messages from "./messages"; import messages from "./messages";
function getParent( function getParent(ev: HexKey, chains: Map<HexKey, NEvent[]>): HexKey | undefined {
ev: HexKey,
chains: Map<HexKey, NEvent[]>
): HexKey | undefined {
for (const [k, vs] of chains.entries()) { 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)) { if (fs.includes(ev)) {
return k; return k;
} }
@ -49,30 +46,17 @@ interface SubthreadProps {
onNavigate: (e: u256) => void; onNavigate: (e: u256) => void;
} }
const Subthread = ({ const Subthread = ({ active, path, notes, related, chains, onNavigate }: SubthreadProps) => {
active,
path,
notes,
related,
chains,
onNavigate,
}: SubthreadProps) => {
const renderSubthread = (a: NEvent, idx: number) => { const renderSubthread = (a: NEvent, idx: number) => {
const isLastSubthread = idx === notes.length - 1; const isLastSubthread = idx === notes.length - 1;
const replies = getReplies(a.Id, chains); const replies = getReplies(a.Id, chains);
return ( return (
<> <>
<div <div className={`subthread-container ${replies.length > 0 ? "subthread-multi" : ""}`}>
className={`subthread-container ${
replies.length > 0 ? "subthread-multi" : ""
}`}
>
<Divider /> <Divider />
<Note <Note
highlight={active === a.Id} highlight={active === a.Id}
className={`thread-note ${ className={`thread-note ${isLastSubthread && replies.length === 0 ? "is-last-note" : ""}`}
isLastSubthread && replies.length === 0 ? "is-last-note" : ""
}`}
data-ev={a} data-ev={a}
key={a.Id} key={a.Id}
related={related} related={related}
@ -116,13 +100,11 @@ const ThreadNote = ({
}: ThreadNoteProps) => { }: ThreadNoteProps) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const replies = getReplies(note.Id, chains); 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 [collapsed, setCollapsed] = useState(!activeInReplies);
const hasMultipleNotes = replies.length > 0; const hasMultipleNotes = replies.length > 0;
const isLastVisibleNote = isLastSubthread && isLast && !hasMultipleNotes; const isLastVisibleNote = isLastSubthread && isLast && !hasMultipleNotes;
const className = `subthread-container ${ const className = `subthread-container ${isLast && collapsed ? "subthread-last" : "subthread-multi subthread-mid"}`;
isLast && collapsed ? "subthread-last" : "subthread-multi subthread-mid"
}`;
return ( return (
<> <>
<div className={className}> <div className={className}>
@ -149,11 +131,7 @@ const ThreadNote = ({
onNavigate={onNavigate} onNavigate={onNavigate}
/> />
) : ( ) : (
<Collapsed <Collapsed text={formatMessage(messages.ShowReplies)} collapsed={collapsed} setCollapsed={setCollapsed}>
text={formatMessage(messages.ShowReplies)}
collapsed={collapsed}
setCollapsed={setCollapsed}
>
<TierThree <TierThree
active={active} active={active}
path={path} path={path}
@ -170,16 +148,7 @@ const ThreadNote = ({
); );
}; };
const TierTwo = ({ const TierTwo = ({ active, isLastSubthread, path, from, notes, related, chains, onNavigate }: SubthreadProps) => {
active,
isLastSubthread,
path,
from,
notes,
related,
chains,
onNavigate,
}: SubthreadProps) => {
const [first, ...rest] = notes; const [first, ...rest] = notes;
return ( return (
@ -216,36 +185,22 @@ const TierTwo = ({
); );
}; };
const TierThree = ({ const TierThree = ({ active, path, isLastSubthread, from, notes, related, chains, onNavigate }: SubthreadProps) => {
active,
path,
isLastSubthread,
from,
notes,
related,
chains,
onNavigate,
}: SubthreadProps) => {
const [first, ...rest] = notes; const [first, ...rest] = notes;
const replies = getReplies(first.Id, chains); const replies = getReplies(first.Id, chains);
const activeInReplies = const activeInReplies = notes.map(r => r.Id).includes(active) || replies.map(r => r.Id).includes(active);
notes.map((r) => r.Id).includes(active) ||
replies.map((r) => r.Id).includes(active);
const hasMultipleNotes = rest.length > 0 || replies.length > 0; const hasMultipleNotes = rest.length > 0 || replies.length > 0;
const isLast = replies.length === 0 && rest.length === 0; const isLast = replies.length === 0 && rest.length === 0;
return ( return (
<> <>
<div <div
className={`subthread-container ${ className={`subthread-container ${hasMultipleNotes ? "subthread-multi" : ""} ${
hasMultipleNotes ? "subthread-multi" : "" isLast ? "subthread-last" : "subthread-mid"
} ${isLast ? "subthread-last" : "subthread-mid"}`} }`}>
>
<Divider variant="small" /> <Divider variant="small" />
<Note <Note
highlight={active === first.Id} highlight={active === first.Id}
className={`thread-note ${ className={`thread-note ${isLastSubthread && isLast ? "is-last-note" : ""}`}
isLastSubthread && isLast ? "is-last-note" : ""
}`}
data-ev={first} data-ev={first}
key={first.Id} key={first.Id}
related={related} related={related}
@ -256,11 +211,7 @@ const TierThree = ({
{path.length <= 1 || !activeInReplies {path.length <= 1 || !activeInReplies
? replies.length > 0 && ( ? replies.length > 0 && (
<div className="show-more-container"> <div className="show-more-container">
<button <button className="show-more" type="button" onClick={() => onNavigate(from)}>
className="show-more"
type="button"
onClick={() => onNavigate(from)}
>
<FormattedMessage {...messages.ShowReplies} /> <FormattedMessage {...messages.ShowReplies} />
</button> </button>
</div> </div>
@ -284,10 +235,9 @@ const TierThree = ({
return ( return (
<div <div
key={r.Id} key={r.Id}
className={`subthread-container ${ className={`subthread-container ${lastReply ? "" : "subthread-multi"} ${
lastReply ? "" : "subthread-multi" lastReply ? "subthread-last" : "subthread-mid"
} ${lastReply ? "subthread-last" : "subthread-mid"}`} }`}>
>
<Divider variant="small" /> <Divider variant="small" />
<Note <Note
className={`thread-note ${lastNote ? "is-last-note" : ""}`} className={`thread-note ${lastNote ? "is-last-note" : ""}`}
@ -311,22 +261,15 @@ export interface ThreadProps {
export default function Thread(props: ThreadProps) { export default function Thread(props: ThreadProps) {
const notes = props.notes ?? []; 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 // root note has no thread info
const root = useMemo( const root = useMemo(() => parsedNotes.find(a => a.Thread === null), [notes]);
() => parsedNotes.find((a) => a.Thread === null),
[notes]
);
const [path, setPath] = useState<HexKey[]>([]); const [path, setPath] = useState<HexKey[]>([]);
const currentId = path.length > 0 && path[path.length - 1]; const currentId = path.length > 0 && path[path.length - 1];
const currentRoot = useMemo( const currentRoot = useMemo(() => parsedNotes.find(a => a.Id === currentId), [notes, currentId]);
() => parsedNotes.find((a) => a.Id === currentId),
[notes, currentId]
);
const [navigated, setNavigated] = useState(false); const [navigated, setNavigated] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const isSingleNote = const isSingleNote = parsedNotes.filter(a => a.Kind === EventKind.TextNote).length === 1;
parsedNotes.filter((a) => a.Kind === EventKind.TextNote).length === 1;
const location = useLocation(); const location = useLocation();
const urlNoteId = location?.pathname.slice(3); const urlNoteId = location?.pathname.slice(3);
const urlNoteHex = urlNoteId && bech32ToHex(urlNoteId); const urlNoteHex = urlNoteId && bech32ToHex(urlNoteId);
@ -334,9 +277,9 @@ export default function Thread(props: ThreadProps) {
const chains = useMemo(() => { const chains = useMemo(() => {
const chains = new Map<u256, NEvent[]>(); const chains = new Map<u256, NEvent[]>();
parsedNotes parsedNotes
?.filter((a) => a.Kind === EventKind.TextNote) ?.filter(a => a.Kind === EventKind.TextNote)
.sort((a, b) => b.CreatedAt - a.CreatedAt) .sort((a, b) => b.CreatedAt - a.CreatedAt)
.forEach((v) => { .forEach(v => {
const replyTo = v.Thread?.ReplyTo?.Event ?? v.Thread?.Root?.Event; const replyTo = v.Thread?.ReplyTo?.Event ?? v.Thread?.Root?.Event;
if (replyTo) { if (replyTo) {
if (!chains.has(replyTo)) { if (!chains.has(replyTo)) {
@ -378,28 +321,15 @@ export default function Thread(props: ThreadProps) {
}, [root, navigated, urlNoteHex, chains]); }, [root, navigated, urlNoteHex, chains]);
const brokenChains = useMemo(() => { const brokenChains = useMemo(() => {
return Array.from(chains?.keys()).filter( return Array.from(chains?.keys()).filter(a => !parsedNotes?.some(b => b.Id === a));
(a) => !parsedNotes?.some((b) => b.Id === a)
);
}, [chains]); }, [chains]);
function renderRoot(note: NEvent) { function renderRoot(note: NEvent) {
const className = `thread-root ${isSingleNote ? "thread-root-single" : ""}`; const className = `thread-root ${isSingleNote ? "thread-root-single" : ""}`;
if (note) { if (note) {
return ( return <Note className={className} key={note.Id} data-ev={note} related={notes} />;
<Note
className={className}
key={note.Id}
data-ev={note}
related={notes}
/>
);
} else { } else {
return ( return <NoteGhost className={className}>Loading thread root.. ({notes?.length} notes loaded)</NoteGhost>;
<NoteGhost className={className}>
Loading thread root.. ({notes?.length} notes loaded)
</NoteGhost>
);
} }
} }
@ -438,25 +368,18 @@ export default function Thread(props: ThreadProps) {
return ( return (
<div className="main-content mt10"> <div className="main-content mt10">
<BackButton <BackButton onClick={goBack} text={path?.length > 1 ? "Parent" : "Back"} />
onClick={goBack}
text={path?.length > 1 ? "Parent" : "Back"}
/>
<div className="thread-container"> <div className="thread-container">
{currentRoot && renderRoot(currentRoot)} {currentRoot && renderRoot(currentRoot)}
{currentRoot && renderChain(currentRoot.Id)} {currentRoot && renderChain(currentRoot.Id)}
{currentRoot === root && ( {currentRoot === root && (
<> <>
{brokenChains.length > 0 && <h3>Other replies</h3>} {brokenChains.length > 0 && <h3>Other replies</h3>}
{brokenChains.map((a) => { {brokenChains.map(a => {
return ( return (
<div className="mb10"> <div className="mb10">
<NoteGhost <NoteGhost className={`thread-note thread-root ghost-root`} key={a}>
className={`thread-note thread-root ghost-root`} Missing event <Link to={eventLink(a)}>{a.substring(0, 8)}</Link>
key={a}
>
Missing event{" "}
<Link to={eventLink(a)}>{a.substring(0, 8)}</Link>
</NoteGhost> </NoteGhost>
{renderChain(a)} {renderChain(a)}
</div> </div>

View File

@ -34,13 +34,11 @@ async function oembedLookup(link: string) {
const TidalEmbed = ({ link }: { link: string }) => { const TidalEmbed = ({ link }: { link: string }) => {
const [source, setSource] = useState<string>(); const [source, setSource] = useState<string>();
const [height, setHeight] = useState<number>(); const [height, setHeight] = useState<number>();
const extraStyles = link.includes("video") const extraStyles = link.includes("video") ? { aspectRatio: "16 / 9" } : { height };
? { aspectRatio: "16 / 9" }
: { height };
useEffect(() => { useEffect(() => {
oembedLookup(link) oembedLookup(link)
.then((data) => { .then(data => {
setSource(data.source || undefined); setSource(data.source || undefined);
setHeight(data.height); setHeight(data.height);
}) })
@ -49,25 +47,11 @@ const TidalEmbed = ({ link }: { link: string }) => {
if (!source) if (!source)
return ( return (
<a <a href={link} target="_blank" rel="noreferrer" onClick={e => e.stopPropagation()} className="ext">
href={link}
target="_blank"
rel="noreferrer"
onClick={(e) => e.stopPropagation()}
className="ext"
>
{link} {link}
</a> </a>
); );
return ( return <iframe src={source} style={extraStyles} width="100%" title="TIDAL Embed" frameBorder={0} />;
<iframe
src={source}
style={extraStyles}
width="100%"
title="TIDAL Embed"
frameBorder={0}
/>
);
}; };
export default TidalEmbed; export default TidalEmbed;

View File

@ -36,18 +36,17 @@ export default function Timeline({
window, window,
}: TimelineProps) { }: TimelineProps) {
const { muted, isMuted } = useModeration(); const { muted, isMuted } = useModeration();
const { main, related, latest, parent, loadMore, showLatest } = const { main, related, latest, parent, loadMore, showLatest } = useTimelineFeed(subject, {
useTimelineFeed(subject, { method,
method, window: window,
window: window, });
});
const filterPosts = useCallback( const filterPosts = useCallback(
(nts: TaggedRawEvent[]) => { (nts: TaggedRawEvent[]) => {
return [...nts] return [...nts]
.sort((a, b) => b.created_at - a.created_at) .sort((a, b) => b.created_at - a.created_at)
?.filter((a) => (postsOnly ? !a.tags.some((b) => b[0] === "e") : true)) ?.filter(a => (postsOnly ? !a.tags.some(b => b[0] === "e") : true))
.filter((a) => ignoreModeration || !isMuted(a.pubkey)); .filter(a => ignoreModeration || !isMuted(a.pubkey));
}, },
[postsOnly, muted] [postsOnly, muted]
); );
@ -57,9 +56,7 @@ export default function Timeline({
}, [main, filterPosts]); }, [main, filterPosts]);
const latestFeed = useMemo(() => { const latestFeed = useMemo(() => {
return filterPosts(latest.notes).filter( return filterPosts(latest.notes).filter(a => !mainFeed.some(b => b.id === a.id));
(a) => !mainFeed.some((b) => b.id === a.id)
);
}, [latest, mainFeed, filterPosts]); }, [latest, mainFeed, filterPosts]);
function eventElement(e: TaggedRawEvent) { function eventElement(e: TaggedRawEvent) {
@ -68,14 +65,7 @@ export default function Timeline({
return <ProfilePreview pubkey={e.pubkey} className="card" />; return <ProfilePreview pubkey={e.pubkey} className="card" />;
} }
case EventKind.TextNote: { case EventKind.TextNote: {
return ( return <Note key={e.id} data={e} related={related.notes} ignoreModeration={ignoreModeration} />;
<Note
key={e.id}
data={e}
related={related.notes}
ignoreModeration={ignoreModeration}
/>
);
} }
case EventKind.ZapReceipt: { case EventKind.ZapReceipt: {
const zap = parseZap(e); const zap = parseZap(e);
@ -83,14 +73,8 @@ export default function Timeline({
} }
case EventKind.Reaction: case EventKind.Reaction:
case EventKind.Repost: { case EventKind.Repost: {
const eRef = e.tags.find((a) => a[0] === "e")?.at(1); const eRef = e.tags.find(a => a[0] === "e")?.at(1);
return ( return <NoteReaction data={e} key={e.id} root={parent.notes.find(a => a.id === eRef)} />;
<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 && ( {latestFeed.length > 1 && (
<div className="card latest-notes pointer" onClick={() => showLatest()}> <div className="card latest-notes pointer" onClick={() => showLatest()}>
<FontAwesomeIcon icon={faForward} size="xl" />{" "} <FontAwesomeIcon icon={faForward} size="xl" />{" "}
<FormattedMessage <FormattedMessage {...messages.ShowLatest} values={{ n: latestFeed.length - 1 }} />
{...messages.ShowLatest}
values={{ n: latestFeed.length - 1 }}
/>
</div> </div>
)} )}
{mainFeed.map(eventElement)} {mainFeed.map(eventElement)}

View File

@ -2,12 +2,9 @@ import "./Zap.css";
import { useMemo } from "react"; import { useMemo } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
// @ts-expect-error No types available
import { decode as invoiceDecode } from "light-bolt11-decoder"; import { decode as invoiceDecode } from "light-bolt11-decoder";
import { bytesToHex } from "@noble/hashes/utils"; import { bytesToHex } from "@noble/hashes/utils";
import { sha256, unwrap } from "Util"; import { sha256, unwrap } from "Util";
//import { sha256 } from "Util";
import { formatShort } from "Number"; import { formatShort } from "Number";
import { HexKey, TaggedRawEvent } from "Nostr"; import { HexKey, TaggedRawEvent } from "Nostr";
import Event from "Nostr/Event"; import Event from "Nostr/Event";
@ -18,28 +15,20 @@ import { RootState } from "State/Store";
import messages from "./messages"; import messages from "./messages";
function findTag(e: TaggedRawEvent, tag: string) { function findTag(e: TaggedRawEvent, tag: string) {
const maybeTag = e.tags.find((evTag) => { const maybeTag = e.tags.find(evTag => {
return evTag[0] === tag; return evTag[0] === tag;
}); });
return maybeTag && maybeTag[1]; return maybeTag && maybeTag[1];
} }
interface Section {
name: string;
}
function getInvoice(zap: TaggedRawEvent) { function getInvoice(zap: TaggedRawEvent) {
const bolt11 = findTag(zap, "bolt11"); const bolt11 = findTag(zap, "bolt11");
const decoded = invoiceDecode(bolt11); const decoded = invoiceDecode(bolt11);
const amount = decoded.sections.find( const amount = decoded.sections.find(section => section.name === "amount")?.value;
(section: Section) => section.name === "amount" const hash = decoded.sections.find(section => section.name === "description_hash")?.value;
)?.value;
const hash = decoded.sections.find(
(section: Section) => section.name === "description_hash"
)?.value;
return { amount, hash: hash ? bytesToHex(hash) : undefined }; return { amount, hash: hash ? bytesToHex(hash as Uint8Array) : undefined };
} }
interface Zapper { interface Zapper {
@ -88,13 +77,7 @@ export function parseZap(zap: TaggedRawEvent): ParsedZap {
}; };
} }
const Zap = ({ const Zap = ({ zap, showZapped = true }: { zap: ParsedZap; showZapped?: boolean }) => {
zap,
showZapped = true,
}: {
zap: ParsedZap;
showZapped?: boolean;
}) => {
const { amount, content, zapper, valid, p } = zap; const { amount, content, zapper, valid, p } = zap;
const pubKey = useSelector((s: RootState) => s.login.publicKey); const pubKey = useSelector((s: RootState) => s.login.publicKey);
@ -105,21 +88,13 @@ const Zap = ({
{p !== pubKey && showZapped && <ProfileImage pubkey={p} />} {p !== pubKey && showZapped && <ProfileImage pubkey={p} />}
<div className="amount"> <div className="amount">
<span className="amount-number"> <span className="amount-number">
<FormattedMessage <FormattedMessage {...messages.Sats} values={{ n: formatShort(amount) }} />
{...messages.Sats}
values={{ n: formatShort(amount) }}
/>
</span> </span>
</div> </div>
</div> </div>
{content.length > 0 && zapper && ( {content.length > 0 && zapper && (
<div className="body"> <div className="body">
<Text <Text creator={zapper} content={content} tags={[]} users={new Map()} />
creator={zapper}
content={content}
tags={[]}
users={new Map()}
/>
</div> </div>
)} )}
</div> </div>
@ -132,8 +107,8 @@ interface ZapsSummaryProps {
export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => { export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
const sortedZaps = useMemo(() => { const sortedZaps = useMemo(() => {
const pub = [...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)]; const priv = [...zaps.filter(z => !z.zapper && z.valid)];
pub.sort((a, b) => b.amount - a.amount); pub.sort((a, b) => b.amount - a.amount);
return pub.concat(priv); return pub.concat(priv);
}, [zaps]); }, [zaps]);
@ -151,16 +126,8 @@ export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
<div className={`top-zap`}> <div className={`top-zap`}>
<div className="summary"> <div className="summary">
{zapper && <ProfileImage pubkey={zapper} />} {zapper && <ProfileImage pubkey={zapper} />}
{restZaps.length > 0 && ( {restZaps.length > 0 && <FormattedMessage {...messages.Others} values={{ n: restZaps.length }} />}{" "}
<FormattedMessage <FormattedMessage {...messages.OthersZapped} values={{ n: restZaps.length }} />
{...messages.Others}
values={{ n: restZaps.length }}
/>
)}{" "}
<FormattedMessage
{...messages.OthersZapped}
values={{ n: restZaps.length }}
/>
</div> </div>
</div> </div>
)} )}

View File

@ -89,8 +89,7 @@ const messages = defineMessages({
AccountSupport: "Account Support", AccountSupport: "Account Support",
GoTo: "Go to", GoTo: "Go to",
FindMore: "Find out more info about {service} at {link}", FindMore: "Find out more info about {service} at {link}",
SavePassword: SavePassword: "Please make sure to save the following password in order to manage your handle in the future",
"Please make sure to save the following password in order to manage your handle in the future",
}); });
export default addIdAndDefaultMessageToMessages(messages, "Element"); export default addIdAndDefaultMessageToMessages(messages, "Element");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,6 @@
const Attachment = () => { const Attachment = () => {
return ( return (
<svg <svg width="21" height="22" viewBox="0 0 21 22" fill="none" xmlns="http://www.w3.org/2000/svg">
width="21"
height="22"
viewBox="0 0 21 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <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" 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" stroke="currentColor"

View File

@ -1,12 +1,6 @@
const Bell = () => { const Bell = () => {
return ( return (
<svg <svg width="20" height="23" viewBox="0 0 20 23" fill="none" xmlns="http://www.w3.org/2000/svg">
width="20"
height="23"
viewBox="0 0 20 23"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <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" 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" stroke="currentColor"

View File

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

View File

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

View File

@ -2,14 +2,7 @@ import IconProps from "./IconProps";
const Copy = (props: IconProps) => { const Copy = (props: IconProps) => {
return ( return (
<svg <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <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" 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" stroke="currentColor"

View File

@ -2,14 +2,7 @@ import IconProps from "./IconProps";
const Dislike = (props: IconProps) => { const Dislike = (props: IconProps) => {
return ( return (
<svg <svg width="19" height="20" viewBox="0 0 19 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
width="19"
height="20"
viewBox="0 0 19 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <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" 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" stroke="currentColor"

View File

@ -1,12 +1,6 @@
const Dots = () => { const Dots = () => {
return ( return (
<svg <svg width="4" height="16" viewBox="0 0 4 16" fill="none" xmlns="http://www.w3.org/2000/svg">
width="4"
height="16"
viewBox="0 0 4 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <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" 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" stroke="currentColor"

View File

@ -2,14 +2,7 @@ import type IconProps from "./IconProps";
const Envelope = (props: IconProps) => { const Envelope = (props: IconProps) => {
return ( return (
<svg <svg width="22" height="19" viewBox="0 0 22 19" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
width="22"
height="19"
viewBox="0 0 22 19"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <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" 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" stroke="currentColor"

View File

@ -2,14 +2,7 @@ import IconProps from "./IconProps";
const Gear = (props: IconProps) => { const Gear = (props: IconProps) => {
return ( return (
<svg <svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
width="20"
height="22"
viewBox="0 0 20 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <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" 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" stroke="currentColor"

View File

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

View File

@ -1,12 +1,6 @@
const Link = () => { const Link = () => {
return ( return (
<svg <svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
width="22"
height="22"
viewBox="0 0 22 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <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" 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" stroke="currentColor"

View File

@ -1,12 +1,6 @@
const Logout = () => { const Logout = () => {
return ( return (
<svg <svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
width="22"
height="20"
viewBox="0 0 22 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <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" 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" stroke="currentColor"

View File

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

View File

@ -2,14 +2,7 @@ import IconProps from "./IconProps";
const Profile = (props: IconProps) => { const Profile = (props: IconProps) => {
return ( return (
<svg <svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
width="22"
height="22"
viewBox="0 0 22 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <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" 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" stroke="currentColor"

View File

@ -2,14 +2,7 @@ import IconProps from "./IconProps";
const Qr = (props: IconProps) => { const Qr = (props: IconProps) => {
return ( return (
<svg <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <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" 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" stroke="currentColor"

View File

@ -2,14 +2,7 @@ import IconProps from "./IconProps";
const Relay = (props: IconProps) => { const Relay = (props: IconProps) => {
return ( return (
<svg <svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
width="22"
height="22"
viewBox="0 0 22 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <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" 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" stroke="currentColor"

View File

@ -1,12 +1,6 @@
const Reply = () => { const Reply = () => {
return ( return (
<svg <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <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" 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" stroke="currentColor"

View File

@ -1,12 +1,6 @@
const Search = () => { const Search = () => {
return ( return (
<svg <svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
width="20"
height="21"
viewBox="0 0 20 21"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <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" 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" stroke="currentColor"

View File

@ -2,14 +2,7 @@ import type IconProps from "./IconProps";
const Zap = (props: IconProps) => { const Zap = (props: IconProps) => {
return ( return (
<svg <svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
width="16"
height="20"
viewBox="0 0 16 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <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" 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" stroke="currentColor"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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