Merge pull request #223 from v0l/eslint

Eslint
This commit is contained in:
Kieran 2023-02-09 12:33:39 +00:00 committed by GitHub
commit e745ead7ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
145 changed files with 1248 additions and 2404 deletions

5
.dockerignore Normal file
View File

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

13
.eslintrc.cjs Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
root: true,
ignorePatterns: ["build/"],
env: {
browser: true,
worker: true,
commonjs: true,
node: true,
},
};

20
.github/workflows/eslint.yaml vendored Normal file
View File

@ -0,0 +1,20 @@
name: Linting
on:
pull_request:
push:
branches: [main]
jobs:
formatting:
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- name: Install Dependencies
run: yarn install
- name: Check Eslint
run: yarn eslint

18
.github/workflows/formatting.yaml vendored Normal file
View File

@ -0,0 +1,18 @@
name: Formatting
on:
pull_request:
jobs:
formatting:
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- name: Install Dependencies
run: yarn install
- name: Check Formatting
run: yarn prettier --check .

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
build/

View File

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

0
Dockerfile Normal file
View File

20
d.ts
View File

@ -1,14 +1,28 @@
declare module "*.jpg" { declare module "*.jpg" {
const value: any; const value: unknown;
export default value; export default value;
} }
declare module "*.svg" { declare module "*.svg" {
const value: any; const value: unknown;
export default value; export default value;
} }
declare module "*.webp" { declare module "*.webp" {
const value: any; 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

@ -56,7 +56,9 @@
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"generate-messages": "extract-messages -l=en,es,zh,ja -o src/translations -d en --flat true **/messages.js" "generate-messages": "extract-messages -l=en,es,zh,ja -o src/translations -d en --flat true **/messages.js",
"format": "prettier --write .",
"eslint": "eslint ."
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [

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
@ -83,17 +79,20 @@ export const RecommendedFollows = [
* Regex to match email address * Regex to match email address
*/ */
export const EmailRegex = export const EmailRegex =
// eslint-disable-next-line no-useless-escape
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
/** /**
* Generic URL regex * Generic URL regex
*/ */
export const UrlRegex = export const UrlRegex =
// eslint-disable-next-line no-useless-escape
/((?:http|ftp|https):\/\/(?:[\w+?\.\w+])+(?:[a-zA-Z0-9\~\!\@\#\$\%\^\&\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?)/i; /((?:http|ftp|https):\/\/(?:[\w+?\.\w+])+(?:[a-zA-Z0-9\~\!\@\#\$\%\^\&\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?)/i;
/** /**
* Extract file extensions regex * Extract file extensions regex
*/ */
// eslint-disable-next-line no-useless-escape
export const FileExtensionRegex = /\.([\w]+)$/i; export const FileExtensionRegex = /\.([\w]+)$/i;
/** /**
@ -115,12 +114,12 @@ 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
*/ */
// eslint-disable-next-line no-useless-escape
export const HashtagRegex = /(#[^\s!@#$%^&*()=+.\/,\[{\]};:'"?><]+)/; export const HashtagRegex = /(#[^\s!@#$%^&*()=+.\/,\[{\]};:'"?><]+)/;
/** /**
@ -131,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,14 +1,19 @@
import { useState } from "react"; import { useState } from "react";
export default function AsyncButton(props: any) { interface AsyncButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
onClick(e: React.MouseEvent): Promise<void> | void;
children?: React.ReactNode;
}
export default function AsyncButton(props: AsyncButtonProps) {
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
async function handle(e: any) { async function handle(e: React.MouseEvent) {
if (loading) return; if (loading) return;
setLoading(true); setLoading(true);
try { try {
if (typeof props.onClick === "function") { if (typeof props.onClick === "function") {
let f = props.onClick(e); const f = props.onClick(e);
if (f instanceof Promise) { if (f instanceof Promise) {
await f; await f;
} }
@ -19,12 +24,7 @@ export default function AsyncButton(props: any) {
} }
return ( return (
<button <button type="button" disabled={loading} {...props} onClick={handle}>
type="button"
disabled={loading}
{...props}
onClick={(e) => handle(e)}
>
{props.children} {props.children}
</button> </button>
); );

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

@ -1,13 +1,7 @@
import { useMemo } from "react";
import { useSelector } from "react-redux";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { HexKey } from "Nostr";
import type { RootState } from "State/Store";
import MuteButton from "Element/MuteButton"; import MuteButton from "Element/MuteButton";
import BlockButton from "Element/BlockButton"; import BlockButton from "Element/BlockButton";
import ProfilePreview from "Element/ProfilePreview"; import ProfilePreview from "Element/ProfilePreview";
import useMutedFeed, { getMuted } from "Feed/MuteList";
import useModeration from "Hooks/useModeration"; import useModeration from "Hooks/useModeration";
import messages from "./messages"; import messages from "./messages";
@ -17,7 +11,6 @@ interface BlockListProps {
} }
export default function BlockList({ variant }: BlockListProps) { export default function BlockList({ variant }: BlockListProps) {
const { publicKey } = useSelector((s: RootState) => s.login);
const { blocked, muted } = useModeration(); const { blocked, muted } = useModeration();
return ( return (
@ -25,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

@ -1,4 +1,4 @@
import { useState, ReactNode } from "react"; import { ReactNode } from "react";
import ShowMore from "Element/ShowMore"; import ShowMore from "Element/ShowMore";
@ -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

@ -8,25 +8,15 @@ export interface CopyProps {
maxSize?: number; maxSize?: number;
} }
export default function Copy({ text, maxSize = 32 }: CopyProps) { export default function Copy({ text, maxSize = 32 }: CopyProps) {
const { copy, copied, error } = 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

@ -12,6 +12,7 @@ import { setLastReadDm } from "Pages/MessagesPage";
import { RootState } from "State/Store"; import { RootState } from "State/Store";
import { HexKey, TaggedRawEvent } from "Nostr"; import { HexKey, TaggedRawEvent } from "Nostr";
import { incDmInteraction } from "State/Login"; import { incDmInteraction } from "State/Login";
import { unwrap } from "Util";
import messages from "./messages"; import messages from "./messages";
@ -21,22 +22,18 @@ 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
: props.data.tags.find((a) => a[0] === "p")![1];
async function decrypt() { async function decrypt() {
let e = new Event(props.data); const e = new Event(props.data);
let decrypted = await publisher.decryptDm(e); const decrypted = await publisher.decryptDm(e);
setContent(decrypted || "<ERROR>"); setContent(decrypted || "<ERROR>");
if (!isMe) { if (!isMe) {
setLastReadDm(e.PubKey); setLastReadDm(e.PubKey);
@ -54,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,18 +15,16 @@ 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) {
let ev = await publiser.addFollow(pubkey); const ev = await publiser.addFollow(pubkey);
publiser.broadcast(ev); publiser.broadcast(ev);
} }
async function unfollow(pubkey: HexKey) { async function unfollow(pubkey: HexKey) {
let ev = await publiser.removeFollow(pubkey); const ev = await publiser.removeFollow(pubkey);
publiser.broadcast(ev); publiser.broadcast(ev);
} }
@ -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,14 +10,11 @@ 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() {
let ev = await publisher.addFollow(pubkeys); const ev = await publisher.addFollow(pubkeys);
publisher.broadcast(ev); publisher.broadcast(ev);
} }
@ -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

@ -17,18 +17,11 @@ export default function FollowersList({ pubkey }: FollowersListProps) {
const feed = useFollowersFeed(pubkey); const feed = useFollowersFeed(pubkey);
const pubkeys = useMemo(() => { const pubkeys = useMemo(() => {
let 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,17 +17,13 @@ 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);
}, [feed, pubkey]); }, [feed, pubkey]);
const followsMe = 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,26 +109,16 @@ 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>
); );
} }
} catch (error) {} } catch (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
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";
@ -13,6 +12,7 @@ import messages from "./messages";
export interface InvoiceProps { export interface InvoiceProps {
invoice: string; invoice: 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();
@ -21,24 +21,21 @@ export default function Invoice(props: InvoiceProps) {
const info = useMemo(() => { const info = useMemo(() => {
try { try {
let parsed = invoiceDecode(invoice); const parsed = invoiceDecode(invoice);
let amount = parseInt( const amountSection = parsed.sections.find(a => a.name === "amount");
parsed.sections.find((a: any) => a.name === "amount")?.value const amount = amountSection ? (amountSection.value as number) : NaN;
);
let timestamp = parseInt( const timestampSection = parsed.sections.find(a => a.name === "timestamp");
parsed.sections.find((a: any) => a.name === "timestamp")?.value const timestamp = timestampSection ? (timestampSection.value as number) : NaN;
);
let expire = parseInt( const expirySection = parsed.sections.find(a => a.name === "expiry");
parsed.sections.find((a: any) => a.name === "expiry")?.value const expire = expirySection ? (expirySection.value as number) : NaN;
); const descriptionSection = parsed.sections.find(a => a.name === "description")?.value;
let description = parsed.sections.find( const ret = {
(a: any) => a.name === "description"
)?.value;
let 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) {
@ -72,7 +69,7 @@ export default function Invoice(props: InvoiceProps) {
); );
} }
async function payInvoice(e: any) { async function payInvoice(e: React.MouseEvent<HTMLButtonElement>) {
e.stopPropagation(); e.stopPropagation();
if (webln?.enabled) { if (webln?.enabled) {
try { try {
@ -88,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>
@ -112,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

@ -23,8 +23,8 @@ export default function LoadMore({
}, [inView, shouldLoadMore, tick]); }, [inView, shouldLoadMore, tick]);
useEffect(() => { useEffect(() => {
let 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

@ -9,16 +9,16 @@ export default function Mention({ pubkey }: { pubkey: HexKey }) {
const name = useMemo(() => { const name = useMemo(() => {
let name = hexToBech32("npub", pubkey).substring(0, 12); let name = hexToBech32("npub", pubkey).substring(0, 12);
if ((user?.display_name?.length ?? 0) > 0) { if (user?.display_name !== undefined && user.display_name.length > 0) {
name = user!.display_name!; name = user.display_name;
} else if ((user?.name?.length ?? 0) > 0) { } else if (user?.name !== undefined && user.name.length > 0) {
name = user!.name!; name = user.name;
} }
return name; return name;
}, [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,10 @@ export interface ModalProps {
children: React.ReactNode; children: React.ReactNode;
} }
function useOnClickOutside(ref: any, onClickOutside: () => void) { function useOnClickOutside(ref: React.MutableRefObject<Element | null>, onClickOutside: () => void) {
useEffect(() => { useEffect(() => {
function handleClickOutside(ev: any) { function handleClickOutside(ev: MouseEvent) {
if (ref && ref.current && !ref.current.contains(ev.target)) { if (ref && ref.current && !ref.current.contains(ev.target as Node)) {
onClickOutside(); onClickOutside();
} }
} }
@ -24,7 +24,7 @@ function useOnClickOutside(ref: any, onClickOutside: () => void) {
export default function Modal(props: ModalProps) { export default function Modal(props: ModalProps) {
const ref = useRef(null); const ref = useRef(null);
const onClose = props.onClose || (() => {}); const onClose = props.onClose || (() => undefined);
const className = props.className || ""; const className = props.className || "";
useOnClickOutside(ref, onClose); useOnClickOutside(ref, onClose);

View File

@ -1,6 +1,5 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { HexKey } from "Nostr"; import { HexKey } from "Nostr";
import MuteButton from "Element/MuteButton"; import MuteButton from "Element/MuteButton";
import ProfilePreview from "Element/ProfilePreview"; import ProfilePreview from "Element/ProfilePreview";
@ -25,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,45 +30,34 @@ type Nip05ServiceProps = {
supportLink: string; supportLink: string;
}; };
type ReduxStore = any;
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 {
let svc = a as ServiceConfig; const svc = a as ServiceConfig;
setServiceConfig(svc); setServiceConfig(svc);
let 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);
} }
}) })
@ -86,10 +76,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
setAvailabilityResponse({ available: false, why: "TOO_LONG" }); setAvailabilityResponse({ available: false, why: "TOO_LONG" });
return; return;
} }
let 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;
@ -97,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 {
@ -111,14 +98,14 @@ export default function Nip5Service(props: Nip05ServiceProps) {
useEffect(() => { useEffect(() => {
if (registerResponse && showInvoice) { if (registerResponse && showInvoice) {
let t = setInterval(async () => { const t = setInterval(async () => {
let status = await svc.CheckRegistration(registerResponse.token); const status = await svc.CheckRegistration(registerResponse.token);
if ("error" in status) { if ("error" in status) {
setError(status); setError(status);
setRegisterResponse(undefined); setRegisterResponse(undefined);
setShowInvoice(false); setShowInvoice(false);
} else { } else {
let result: CheckRegisterResponse = status; const result: CheckRegisterResponse = status;
if (result.available && result.paid) { if (result.available && result.paid) {
setShowInvoice(false); setShowInvoice(false);
setRegisterStatus(status); setRegisterStatus(status);
@ -131,8 +118,11 @@ export default function Nip5Service(props: Nip05ServiceProps) {
} }
}, [registerResponse, showInvoice, svc]); }, [registerResponse, showInvoice, svc]);
function mapError(e: ServiceErrorCode, t: string | null): string | undefined { function mapError(e: ServiceErrorCode | undefined, t: string | null): string | undefined {
let whyMap = new Map([ if (e === undefined) {
return undefined;
}
const whyMap = new Map([
["TOO_SHORT", formatMessage(messages.TooShort)], ["TOO_SHORT", formatMessage(messages.TooShort)],
["TOO_LONG", formatMessage(messages.TooLong)], ["TOO_LONG", formatMessage(messages.TooLong)],
["REGEX", formatMessage(messages.Regex)], ["REGEX", formatMessage(messages.Regex)],
@ -144,12 +134,11 @@ 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;
} }
let rsp = await svc.RegisterHandle(handle, domain, pubkey); const rsp = await svc.RegisterHandle(handle, domain, pubkey);
if ("error" in rsp) { if ("error" in rsp) {
setError(rsp); setError(rsp);
} else { } else {
@ -160,11 +149,11 @@ export default function Nip5Service(props: Nip05ServiceProps) {
async function updateProfile(handle: string, domain: string) { async function updateProfile(handle: string, domain: string) {
if (user) { if (user) {
let newProfile = { const newProfile = {
...user, ...user,
nip05: `${handle}@${domain}`, nip05: `${handle}@${domain}`,
} as UserMetadata; } as UserMetadata;
let ev = await publisher.metadata(newProfile); const ev = await publisher.metadata(newProfile);
publisher.broadcast(ev); publisher.broadcast(ev);
navigate("/settings"); navigate("/settings");
} }
@ -194,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>
@ -207,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>
@ -230,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";
@ -17,7 +11,6 @@ import Text from "Element/Text";
import { eventLink, getReactions, hexToBech32 } from "Util"; import { eventLink, getReactions, hexToBech32 } from "Util";
import NoteFooter, { Translation } from "Element/NoteFooter"; import NoteFooter, { Translation } from "Element/NoteFooter";
import NoteTime from "Element/NoteTime"; import NoteTime from "Element/NoteTime";
import ShowMore from "Element/ShowMore";
import EventKind from "Nostr/EventKind"; import EventKind from "Nostr/EventKind";
import { useUserProfiles } from "Feed/ProfileFeed"; import { useUserProfiles } from "Feed/ProfileFeed";
import { TaggedRawEvent, u256 } from "Nostr"; import { TaggedRawEvent, u256 } from "Nostr";
@ -39,10 +32,10 @@ export interface NoteProps {
["data-ev"]?: NEvent; ["data-ev"]?: NEvent;
} }
const HiddenNote = ({ children }: any) => { const HiddenNote = ({ children }: { children: React.ReactNode }) => {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
return show ? ( return show ? (
children <>{children}</>
) : ( ) : (
<div className="card note hidden-note"> <div className="card note hidden-note">
<div className="header"> <div className="header">
@ -59,30 +52,19 @@ const HiddenNote = ({ children }: any) => {
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,
className,
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 });
const [extendable, setExtendable] = useState<boolean>(false); const [extendable, setExtendable] = useState<boolean>(false);
const [showMore, setShowMore] = useState<boolean>(false); const [showMore, setShowMore] = useState<boolean>(false);
const baseClassname = `note card ${props.className ? props.className : ""}`; const baseClassName = `note card ${props.className ? props.className : ""}`;
const [translated, setTranslated] = useState<Translation>(); const [translated, setTranslated] = useState<Translation>();
const replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event; // TODO Why was this unused? Was this a mistake?
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const options = { const options = {
@ -93,7 +75,7 @@ export default function Note(props: NoteProps) {
}; };
const transformBody = useCallback(() => { const transformBody = useCallback(() => {
let body = ev?.Content ?? ""; const body = ev?.Content ?? "";
if (deletions?.length > 0) { if (deletions?.length > 0) {
return ( return (
<b className="error"> <b className="error">
@ -101,26 +83,19 @@ 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(() => {
if (entry && inView && extendable === false) { if (entry && inView && extendable === false) {
let h = entry?.target.clientHeight ?? 0; const h = entry?.target.clientHeight ?? 0;
if (h > 650) { if (h > 650) {
setExtendable(true); setExtendable(true);
} }
} }
}, [inView, entry, extendable]); }, [inView, entry, extendable]);
function goToEvent(e: any, id: u256) { function goToEvent(e: React.MouseEvent, id: u256) {
e.stopPropagation(); e.stopPropagation();
navigate(eventLink(id)); navigate(eventLink(id));
} }
@ -131,9 +106,9 @@ export default function Note(props: NoteProps) {
} }
const maxMentions = 2; const maxMentions = 2;
let replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event; const replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event;
let mentions: { pk: string; name: string; link: ReactNode }[] = []; const mentions: { pk: string; name: string; link: ReactNode }[] = [];
for (let pk of ev.Thread?.PubKeys) { for (const pk of ev.Thread?.PubKeys ?? []) {
const u = users?.get(pk); const u = users?.get(pk);
const npub = hexToBech32("npub", pk); const npub = hexToBech32("npub", pk);
const shortNpub = npub.substring(0, 12); const shortNpub = npub.substring(0, 12);
@ -141,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({
@ -153,9 +126,9 @@ export default function Note(props: NoteProps) {
}); });
} }
} }
mentions.sort((a, b) => (a.name.startsWith("npub") ? 1 : -1)); mentions.sort(a => (a.name.startsWith("npub") ? 1 : -1));
let othersLength = mentions.length - maxMentions; const othersLength = mentions.length - maxMentions;
const renderMention = (m: any, idx: number) => { const renderMention = (m: { link: React.ReactNode }, idx: number) => {
return ( return (
<> <>
{idx > 0 && ", "} {idx > 0 && ", "}
@ -164,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;
@ -180,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>
); );
@ -194,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>
</> </>
@ -209,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}
</> </>
@ -232,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} />
@ -243,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

@ -33,14 +33,14 @@ export interface NoteCreatorProps {
show: boolean; show: boolean;
setShow: (s: boolean) => void; setShow: (s: boolean) => void;
replyTo?: NEvent; replyTo?: NEvent;
onSend?: Function; onSend?: () => void;
autoFocus: boolean; autoFocus: boolean;
} }
export function NoteCreator(props: NoteCreatorProps) { export function NoteCreator(props: NoteCreatorProps) {
const { show, setShow, replyTo, onSend, autoFocus } = props; const { show, setShow, replyTo, onSend, autoFocus } = props;
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const [note, setNote] = useState<string>(); const [note, setNote] = useState<string>("");
const [error, setError] = useState<string>(); const [error, setError] = useState<string>();
const [active, setActive] = useState<boolean>(false); const [active, setActive] = useState<boolean>(false);
const uploader = useFileUpload(); const uploader = useFileUpload();
@ -48,9 +48,7 @@ export function NoteCreator(props: NoteCreatorProps) {
async function sendNote() { async function sendNote() {
if (note) { if (note) {
let 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("");
@ -64,21 +62,23 @@ export function NoteCreator(props: NoteCreatorProps) {
async function attachFile() { async function attachFile() {
try { try {
let file = await openFile(); const file = await openFile();
if (file) { if (file) {
let 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);
} }
} }
} catch (error: any) { } catch (error: unknown) {
setError(error?.message); if (error instanceof Error) {
setError(error?.message);
}
} }
} }
function onChange(ev: any) { function onChange(ev: React.ChangeEvent<HTMLTextAreaElement>) {
const { value } = ev.target; const { value } = ev.target;
setNote(value); setNote(value);
if (value) { if (value) {
@ -88,7 +88,7 @@ export function NoteCreator(props: NoteCreatorProps) {
} }
} }
function cancel(ev: any) { function cancel() {
setShow(false); setShow(false);
setNote(""); setNote("");
} }
@ -112,11 +112,7 @@ export function NoteCreator(props: NoteCreatorProps) {
value={note} value={note}
onFocus={() => setActive(true)} onFocus={() => setActive(true)}
/> />
<button <button type="button" className="attachment" onClick={attachFile}>
type="button"
className="attachment"
onClick={(e) => attachFile()}
>
<Attachment /> <Attachment />
</button> </button>
</div> </div>
@ -127,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,17 +21,11 @@ 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";
import { parseZap, ParsedZap, ZapsSummary } from "Element/Zap"; import { parseZap, ZapsSummary } from "Element/Zap";
import { useUserProfile } from "Feed/ProfileFeed"; import { useUserProfile } from "Feed/ProfileFeed";
import { default as NEvent } from "Nostr/Event"; import { default as NEvent } from "Nostr/Event";
import { RootState } from "State/Store"; import { RootState } from "State/Store";
@ -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) => {
let 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,63 +100,46 @@ 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) {
if (!hasReacted(content)) { if (!hasReacted(content)) {
let evLike = await publisher.react(ev, content); const evLike = await publisher.react(ev, content);
publisher.broadcast(evLike); publisher.broadcast(evLike);
} }
} }
async function deleteEvent() { async function deleteEvent() {
if ( if (window.confirm(formatMessage(messages.ConfirmDeletion, { id: ev.Id.substring(0, 8) }))) {
window.confirm( const evDelete = await publisher.delete(ev.Id);
formatMessage(messages.ConfirmDeletion, { id: ev.Id.substring(0, 8) })
)
) {
let evDelete = await publisher.delete(ev.Id);
publisher.broadcast(evDelete); publisher.broadcast(evDelete);
} }
} }
async function repost() { async function repost() {
if (!hasReposted()) { if (!hasReposted()) {
if ( if (!prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id }))) {
!prefs.confirmReposts || const evRepost = await publisher.repost(ev);
window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id }))
) {
let evRepost = await publisher.repost(ev);
publisher.broadcast(evRepost); publisher.broadcast(evRepost);
} }
} }
} }
function tipButton() { function tipButton() {
let service = author?.lud16 || author?.lud06; const service = author?.lud16 || author?.lud06;
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",
@ -246,7 +199,7 @@ export default function NoteFooter(props: NoteFooterProps) {
}); });
if (res.ok) { if (res.ok) {
let result = await res.json(); const result = await res.json();
if (typeof props.onTranslated === "function" && result) { if (typeof props.onTranslated === "function" && result) {
props.onTranslated({ props.onTranslated({
text: result.translatedText, text: result.translatedText,
@ -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={(e) => 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

@ -1,8 +1,13 @@
import "./Note.css"; import "./Note.css";
import ProfileImage from "Element/ProfileImage"; import ProfileImage from "Element/ProfileImage";
export default function NoteGhost(props: any) { interface NoteGhostProps {
const className = `note card ${props.className ? props.className : ""}`; className?: string;
children: React.ReactNode;
}
export default function NoteGhost(props: NoteGhostProps) {
const className = `note card ${props.className ?? ""}`;
return ( return (
<div className={className}> <div className={className}>
<div className="header"> <div className="header">

View File

@ -23,7 +23,7 @@ export default function NoteReaction(props: NoteReactionProps) {
const refEvent = useMemo(() => { const refEvent = useMemo(() => {
if (ev) { if (ev) {
let 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,13 +39,9 @@ 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 {
let r: RawEvent = JSON.parse(ev.Content); const r: RawEvent = JSON.parse(ev.Content);
return r as TaggedRawEvent; return r as TaggedRawEvent;
} catch (e) { } catch (e) {
console.error("Could not load reposted content", e); console.error("Could not load reposted content", e);
@ -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

@ -1,5 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FormattedRelativeTime } from "react-intl";
const MinuteInMs = 1_000 * 60; const MinuteInMs = 1_000 * 60;
const HourInMs = MinuteInMs * 60; const HourInMs = MinuteInMs * 60;
@ -19,10 +18,11 @@ export default function NoteTime(props: NoteTimeProps) {
}).format(from); }).format(from);
const fromDate = new Date(from); const fromDate = new Date(from);
const isoDate = fromDate.toISOString(); const isoDate = fromDate.toISOString();
const ago = new Date().getTime() - from;
const absAgo = Math.abs(ago);
function calcTime() { function calcTime() {
const fromDate = new Date(from);
const ago = new Date().getTime() - from;
const absAgo = Math.abs(ago);
if (absAgo > DayInMs) { if (absAgo > DayInMs) {
return fromDate.toLocaleDateString(undefined, { return fromDate.toLocaleDateString(undefined, {
year: "2-digit", year: "2-digit",
@ -31,14 +31,11 @@ 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 {
let mins = Math.floor(absAgo / MinuteInMs); const mins = Math.floor(absAgo / MinuteInMs);
if (ago < 0) { if (ago < 0) {
return `in ${mins}m`; return `in ${mins}m`;
} }
@ -48,9 +45,9 @@ export default function NoteTime(props: NoteTimeProps) {
useEffect(() => { useEffect(() => {
setTime(calcTime()); setTime(calcTime());
let t = setInterval(() => { const t = setInterval(() => {
setTime((s) => { setTime(s => {
let newTime = calcTime(); const newTime = calcTime();
if (newTime !== s) { if (newTime !== s) {
return newTime; return newTime;
} }

View File

@ -16,23 +16,17 @@ export interface NoteToSelfProps {
link?: string; link?: string;
} }
function NoteLabel({ pubkey, link }: NoteToSelfProps) { 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,15 +45,12 @@ 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?.length ?? 0) > 0) { if (user?.display_name !== undefined && user.display_name.length > 0) {
name = user!.display_name!; name = user.display_name;
} else if ((user?.name?.length ?? 0) > 0) { } else if (user?.name !== undefined && user.name.length > 0) {
name = user!.name!; name = user.name;
} }
return name; return 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,7 +1,11 @@
import useImgProxy from "Feed/ImgProxy"; import useImgProxy from "Feed/ImgProxy";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export const ProxyImg = (props: any) => { interface ProxyImgProps extends React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement> {
size?: number;
}
export const ProxyImg = (props: ProxyImgProps) => {
const { src, size, ...rest } = props; const { src, size, ...rest } = props;
const [url, setUrl] = useState<string>(); const [url, setUrl] = useState<string>();
const { proxy } = useImgProxy(); const { proxy } = useImgProxy();
@ -9,7 +13,7 @@ export const ProxyImg = (props: any) => {
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

@ -15,7 +15,7 @@ export default function QrCode(props: QrCodeProps) {
useEffect(() => { useEffect(() => {
if ((props.data?.length ?? 0) > 0 && qrRef.current) { if ((props.data?.length ?? 0) > 0 && qrRef.current) {
let qr = new QRCodeStyling({ const qr = new QRCodeStyling({
width: props.width || 256, width: props.width || 256,
height: props.height || 256, height: props.height || 256,
data: props.data, data: props.data,
@ -35,9 +35,9 @@ export default function QrCode(props: QrCodeProps) {
qrRef.current.innerHTML = ""; qrRef.current.innerHTML = "";
qr.append(qrRef.current); qr.append(qrRef.current);
if (props.link) { if (props.link) {
qrRef.current.onclick = function (e) { qrRef.current.onclick = function () {
let elm = document.createElement("a"); const elm = document.createElement("a");
elm.href = props.link!; elm.href = props.link ?? "";
elm.click(); elm.click();
}; };
} }
@ -46,10 +46,5 @@ export default function QrCode(props: QrCodeProps) {
} }
}, [props.data, props.link]); }, [props.data, props.link]);
return ( return <div className={`qr${props.className ?? ""}`} ref={qrRef}></div>;
<div
className={`qr${props.className ? ` ${props.className}` : ""}`}
ref={qrRef}
></div>
);
} }

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]);
@ -47,7 +44,7 @@ export default function Relay(props: RelayProps) {
); );
} }
let latency = Math.floor(state?.avgLatency ?? 0); const latency = Math.floor(state?.avgLatency ?? 0);
return ( return (
<> <>
<div className={`relay w-max`}> <div className={`relay w-max`}>
@ -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,7 +95,7 @@ export default function Relay(props: RelayProps) {
<FontAwesomeIcon icon={faPlugCircleXmark} /> {state?.disconnects} <FontAwesomeIcon icon={faPlugCircleXmark} /> {state?.disconnects}
</div> </div>
<div> <div>
<span className="icon-btn" onClick={() => navigate(state!.id)}> <span className="icon-btn" onClick={() => navigate(state?.id ?? "")}>
<FontAwesomeIcon icon={faGear} /> <FontAwesomeIcon icon={faGear} />
</span> </span>
</div> </div>

View File

@ -50,13 +50,11 @@ export interface LNURLTipProps {
} }
export default function LNURLTip(props: LNURLTipProps) { export default function LNURLTip(props: LNURLTipProps) {
const onClose = props.onClose || (() => {}); const onClose = props.onClose || (() => undefined);
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!)) .then(a => setPayService(a ?? undefined))
.catch(() => setError(formatMessage(messages.LNURLFail))); .catch(() => setError(formatMessage(messages.LNURLFail)));
} else { } else {
setPayService(undefined); setPayService(undefined);
@ -97,26 +94,13 @@ export default function LNURLTip(props: LNURLTipProps) {
const serviceAmounts = useMemo(() => { const serviceAmounts = useMemo(() => {
if (payService) { if (payService) {
let min = (payService.minSendable ?? 0) / 1000; const min = (payService.minSendable ?? 0) / 1000;
let 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]);
const metadata = useMemo(() => {
if (payService) {
let meta: string[][] = JSON.parse(payService.metadata);
let desc = meta.find((a) => a[0] === "text/plain");
let image = meta.find((a) => a[0] === "image/png;base64");
return {
description: desc ? desc[1] : null,
image: image ? image[1] : null,
};
}
return null;
}, [payService]);
const selectAmount = (a: number) => { const selectAmount = (a: number) => {
setError(undefined); setError(undefined);
setInvoice(undefined); setInvoice(undefined);
@ -124,9 +108,9 @@ export default function LNURLTip(props: LNURLTipProps) {
}; };
async function fetchJson<T>(url: string) { async function fetchJson<T>(url: string) {
let rsp = await fetch(url); const rsp = await fetch(url);
if (rsp.ok) { if (rsp.ok) {
let data: T = await rsp.json(); const data: T = await rsp.json();
console.log(data); console.log(data);
setError(undefined); setError(undefined);
return data; return data;
@ -136,12 +120,12 @@ export default function LNURLTip(props: LNURLTipProps) {
async function loadService(): Promise<LNURLService | null> { async function loadService(): Promise<LNURLService | null> {
if (service) { if (service) {
let isServiceUrl = service.toLowerCase().startsWith("lnurl"); const isServiceUrl = service.toLowerCase().startsWith("lnurl");
if (isServiceUrl) { if (isServiceUrl) {
let serviceUrl = bech32ToText(service); const serviceUrl = bech32ToText(service);
return await fetchJson(serviceUrl); return await fetchJson(serviceUrl);
} else { } else {
let ns = service.split("@"); const ns = service.split("@");
return await fetchJson(`https://${ns[1]}/.well-known/lnurlp/${ns[0]}`); return await fetchJson(`https://${ns[1]}/.well-known/lnurlp/${ns[0]}`);
} }
} }
@ -152,22 +136,18 @@ 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}`;
} }
try { try {
let rsp = await fetch(url); const rsp = await fetch(url);
if (rsp.ok) { if (rsp.ok) {
let data = await rsp.json(); const data = await rsp.json();
console.log(data); console.log(data);
if (data.status === "ERROR") { if (data.status === "ERROR") {
setError(data.reason); setError(data.reason);
@ -185,8 +165,8 @@ export default function LNURLTip(props: LNURLTipProps) {
} }
function custom() { function custom() {
let min = (payService?.minSendable ?? 1000) / 1000; const min = (payService?.minSendable ?? 1000) / 1000;
let max = (payService?.maxSendable ?? 21_000_000_000) / 1000; const max = (payService?.maxSendable ?? 21_000_000_000) / 1000;
return ( return (
<div className="custom-amount flex"> <div className="custom-amount flex">
<input <input
@ -196,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={!Boolean(customAmount)} disabled={!customAmount}
onClick={() => selectAmount(customAmount!)} onClick={() => selectAmount(customAmount ?? 0)}>
>
<FormattedMessage {...messages.Confirm} /> <FormattedMessage {...messages.Confirm} />
</button> </button>
</div> </div>
@ -213,13 +192,15 @@ export default function LNURLTip(props: LNURLTipProps) {
async function payWebLNIfEnabled(invoice: LNURLInvoice) { async function payWebLNIfEnabled(invoice: LNURLInvoice) {
try { try {
if (webln?.enabled) { if (webln?.enabled) {
let res = await webln.sendPayment(invoice!.pr); const res = await webln.sendPayment(invoice?.pr ?? "");
console.log(res); console.log(res);
setSuccess(invoice!.successAction || {}); setSuccess(invoice?.successAction ?? {});
} }
} catch (e: any) { } catch (e: unknown) {
setError(e.toString());
console.warn(e); console.warn(e);
if (e instanceof Error) {
setError(e.toString());
}
} }
} }
@ -231,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>
@ -250,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>
@ -294,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>
</> </>
@ -328,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,
@ -340,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

@ -5,7 +5,7 @@ import ReactMarkdown from "react-markdown";
import { visit, SKIP } from "unist-util-visit"; import { visit, SKIP } from "unist-util-visit";
import { UrlRegex, MentionRegex, InvoiceRegex, HashtagRegex } from "Const"; import { UrlRegex, MentionRegex, InvoiceRegex, HashtagRegex } from "Const";
import { eventLink, hexToBech32 } from "Util"; import { eventLink, hexToBech32, unwrap } from "Util";
import Invoice from "Element/Invoice"; import Invoice from "Element/Invoice";
import Hashtag from "Element/Hashtag"; import Hashtag from "Element/Hashtag";
@ -14,11 +14,12 @@ import { MetadataCache } from "State/Users";
import Mention from "Element/Mention"; import Mention from "Element/Mention";
import HyperText from "Element/HyperText"; import HyperText from "Element/HyperText";
import { HexKey } from "Nostr"; import { HexKey } from "Nostr";
import * as unist from "unist";
export type Fragment = string | JSX.Element; export type Fragment = string | React.ReactNode;
export interface TextFragment { export interface TextFragment {
body: Fragment[]; body: React.ReactNode[];
tags: Tag[]; tags: Tag[];
users: Map<string, MetadataCache>; users: Map<string, MetadataCache>;
} }
@ -33,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} />;
} }
@ -49,35 +50,28 @@ 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 => {
let matchTag = match.match(/#\[(\d+)\]/); const matchTag = match.match(/#\[(\d+)\]/);
if (matchTag && matchTag.length === 2) { if (matchTag && matchTag.length === 2) {
let idx = parseInt(matchTag[1]); const idx = parseInt(matchTag[1]);
let 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": {
let eText = hexToBech32("note", ref.Event!).substring( const eText = hexToBech32("note", ref.Event).substring(0, 12);
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>
); );
} }
case "t": { case "t": {
return <Hashtag tag={ref.Hashtag!} />; return <Hashtag tag={ref.Hashtag ?? ""} />;
} }
} }
} }
@ -94,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 {
@ -111,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 {
@ -127,22 +121,19 @@ export default function Text({ content, tags, creator, users }: TextProps) {
} }
function transformLi(frag: TextFragment) { function transformLi(frag: TextFragment) {
let fragments = transformText(frag); const fragments = transformText(frag);
return <li>{fragments}</li>; return <li>{fragments}</li>;
} }
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}</>;
} }
function transformText(frag: TextFragment) { function transformText(frag: TextFragment) {
if (frag.body === undefined) {
debugger;
}
let fragments = extractMentions(frag); let fragments = extractMentions(frag);
fragments = extractLinks(fragments); fragments = extractLinks(fragments);
fragments = extractInvoices(fragments); fragments = extractInvoices(fragments);
@ -152,15 +143,18 @@ export default function Text({ content, tags, creator, users }: TextProps) {
const components = useMemo(() => { const components = useMemo(() => {
return { return {
p: (x: any) => 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: any) => <HyperText link={x.href} creator={creator} />, li: (x: { children?: Fragment[] }) => transformLi({ body: x.children ?? [], tags, users }),
li: (x: any) => transformLi({ body: x.children ?? [], tags, users }),
}; };
}, [content]); }, [content]);
interface Node extends unist.Node<unist.Data> {
value: string;
}
const disableMarkdownLinks = useCallback( const disableMarkdownLinks = useCallback(
() => (tree: any) => { () => (tree: Node) => {
visit(tree, (node, index, parent) => { visit(tree, (node, index, parent) => {
if ( if (
parent && parent &&
@ -172,9 +166,8 @@ export default function Text({ content, tags, creator, users }: TextProps) {
node.type === "definition") node.type === "definition")
) { ) {
node.type = "text"; node.type = "text";
node.value = content const position = unwrap(node.position);
.slice(node.position.start.offset, node.position.end.offset) node.value = content.slice(position.start.offset, position.end.offset).replace(/\)$/, " )");
.replace(/\)$/, " )");
return SKIP; return SKIP;
} }
}); });
@ -182,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

@ -2,7 +2,7 @@ import "@webscopeio/react-textarea-autocomplete/style.css";
import "./Textarea.css"; import "./Textarea.css";
import { useState } from "react"; import { useState } from "react";
import { useIntl, FormattedMessage } from "react-intl"; import { useIntl } from "react-intl";
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete"; import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
import emoji from "@jukben/emoji-search"; import emoji from "@jukben/emoji-search";
import TextareaAutosize from "react-textarea-autosize"; import TextareaAutosize from "react-textarea-autosize";
@ -30,7 +30,7 @@ const EmojiItem = ({ entity: { name, char } }: { entity: EmojiItemProps }) => {
}; };
const UserItem = (metadata: MetadataCache) => { const UserItem = (metadata: MetadataCache) => {
const { pubkey, display_name, picture, nip05, ...rest } = metadata; const { pubkey, display_name, nip05, ...rest } = metadata;
return ( return (
<div key={pubkey} className="user-item"> <div key={pubkey} className="user-item">
<div className="user-picture"> <div className="user-picture">
@ -44,7 +44,15 @@ const UserItem = (metadata: MetadataCache) => {
); );
}; };
const Textarea = ({ users, onChange, ...rest }: any) => { interface TextareaProps {
autoFocus: boolean;
className: string;
onChange(ev: React.ChangeEvent<HTMLTextAreaElement>): void;
value: string;
onFocus(): void;
}
const Textarea = (props: TextareaProps) => {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@ -52,7 +60,7 @@ const Textarea = ({ users, onChange, ...rest }: any) => {
const userDataProvider = (token: string) => { const userDataProvider = (token: string) => {
setQuery(token); setQuery(token);
return allUsers; return allUsers ?? [];
}; };
const emojiDataProvider = (token: string) => { const emojiDataProvider = (token: string) => {
@ -62,23 +70,23 @@ const Textarea = ({ users, onChange, ...rest }: any) => {
}; };
return ( return (
// @ts-expect-error If anybody can figure out how to type this, please do
<ReactTextareaAutocomplete <ReactTextareaAutocomplete
{...rest} {...props}
loadingComponent={() => <span>Loading....</span>} loadingComponent={() => <span>Loading...</span>}
placeholder={formatMessage(messages.NotePlaceholder)} placeholder={formatMessage(messages.NotePlaceholder)}
onChange={onChange}
textAreaComponent={TextareaAutosize} textAreaComponent={TextareaAutosize}
trigger={{ trigger={{
":": { ":": {
dataProvider: emojiDataProvider, dataProvider: emojiDataProvider,
component: EmojiItem, component: EmojiItem,
output: (item: EmojiItemProps, trigger) => item.char, output: (item: EmojiItemProps) => item.char,
}, },
"@": { "@": {
afterWhitespace: true, afterWhitespace: true,
dataProvider: userDataProvider, dataProvider: userDataProvider,
component: (props: any) => <UserItem {...props.entity} />, component: (props: { entity: MetadataCache }) => <UserItem {...props.entity} />,
output: (item: any) => `@${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

@ -6,20 +6,16 @@ import { useNavigate, useLocation, Link } from "react-router-dom";
import { TaggedRawEvent, u256, HexKey } from "Nostr"; import { TaggedRawEvent, u256, HexKey } from "Nostr";
import { default as NEvent } from "Nostr/Event"; import { default as NEvent } from "Nostr/Event";
import EventKind from "Nostr/EventKind"; import EventKind from "Nostr/EventKind";
import { eventLink, hexToBech32, bech32ToHex } from "Util"; import { eventLink, bech32ToHex, unwrap } from "Util";
import BackButton from "Element/BackButton"; import BackButton from "Element/BackButton";
import Note from "Element/Note"; import Note from "Element/Note";
import NoteGhost from "Element/NoteGhost"; 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, for (const [k, vs] of chains.entries()) {
chains: Map<HexKey, NEvent[]> const fs = vs.map(a => a.Id);
): HexKey | undefined {
for (let [k, vs] of chains.entries()) {
const fs = vs.map((a) => a.Id);
if (fs.includes(ev)) { if (fs.includes(ev)) {
return k; return k;
} }
@ -50,31 +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,
from,
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}
@ -118,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}>
@ -151,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}
@ -172,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 (
@ -218,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}
@ -258,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>
@ -286,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" : ""}`}
@ -313,39 +261,31 @@ 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);
const rootNoteId = root && hexToBech32("note", root.Id);
const chains = useMemo(() => { const chains = useMemo(() => {
let 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 => {
let 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)) {
chains.set(replyTo, [v]); chains.set(replyTo, [v]);
} else { } else {
chains.get(replyTo)!.push(v); unwrap(chains.get(replyTo)).push(v);
} }
} else if (v.Tags.length > 0) { } else if (v.Tags.length > 0) {
console.log("Not replying to anything: ", v); console.log("Not replying to anything: ", v);
@ -370,7 +310,7 @@ export default function Thread(props: ThreadProps) {
return; return;
} }
let subthreadPath = []; const subthreadPath = [];
let parent = getParent(urlNoteHex, chains); let parent = getParent(urlNoteHex, chains);
while (parent) { while (parent) {
subthreadPath.unshift(parent); subthreadPath.unshift(parent);
@ -381,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>
);
} }
} }
@ -414,7 +341,7 @@ export default function Thread(props: ThreadProps) {
if (!from || !chains) { if (!from || !chains) {
return; return;
} }
let replies = chains.get(from); const replies = chains.get(from);
if (replies) { if (replies) {
return ( return (
<Subthread <Subthread
@ -441,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>
@ -476,6 +396,6 @@ function getReplies(from: u256, chains?: Map<u256, NEvent[]>): NEvent[] {
if (!from || !chains) { if (!from || !chains) {
return []; return [];
} }
let replies = chains.get(from); const replies = chains.get(from);
return replies ? replies : []; return replies ? replies : [];
} }

View File

@ -1,4 +1,4 @@
import { CSSProperties, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { TidalRegex } from "Const"; import { TidalRegex } from "Const";
// Re-use dom parser across instances of TidalEmbed // Re-use dom parser across instances of TidalEmbed
@ -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: {
let 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
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 } 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,7 +15,7 @@ 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];
@ -28,14 +25,10 @@ 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: any) => section.name === "amount" const hash = decoded.sections.find(section => section.name === "description_hash")?.value;
)?.value;
const hash = decoded.sections.find(
(section: any) => 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 {
@ -72,7 +65,7 @@ export function parseZap(zap: TaggedRawEvent): ParsedZap {
const { amount, hash } = getInvoice(zap); const { amount, hash } = getInvoice(zap);
const zapper = hash ? getZapper(zap, hash) : { isValid: false }; const zapper = hash ? getZapper(zap, hash) : { isValid: false };
const e = findTag(zap, "e"); const e = findTag(zap, "e");
const p = findTag(zap, "p")!; const p = unwrap(findTag(zap, "p"));
return { return {
id: zap.id, id: zap.id,
e, e,
@ -84,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);
@ -101,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>
@ -128,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]);
@ -147,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

@ -6,8 +6,8 @@ import { useUserProfile } from "Feed/ProfileFeed";
import { HexKey } from "Nostr"; import { HexKey } from "Nostr";
import SendSats from "Element/SendSats"; import SendSats from "Element/SendSats";
const ZapButton = ({ pubkey, svc }: { pubkey?: HexKey; svc?: string }) => { const ZapButton = ({ pubkey, svc }: { pubkey: HexKey; svc?: string }) => {
const profile = useUserProfile(pubkey!); const profile = useUserProfile(pubkey);
const [zap, setZap] = useState(false); const [zap, setZap] = useState(false);
const service = svc ?? (profile?.lud16 || profile?.lud06); const service = svc ?? (profile?.lud16 || profile?.lud06);
@ -15,7 +15,7 @@ const ZapButton = ({ pubkey, svc }: { pubkey?: HexKey; svc?: string }) => {
return ( return (
<> <>
<div className="zap-button" onClick={(e) => setZap(true)}> <div className="zap-button" onClick={() => setZap(true)}>
<FontAwesomeIcon icon={faBolt} /> <FontAwesomeIcon icon={faBolt} />
</div> </div>
<SendSats <SendSats

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

@ -1,12 +1,13 @@
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { TaggedRawEvent } from "Nostr";
import { System } from "Nostr/System"; import { System } from "Nostr/System";
import { default as NEvent } from "Nostr/Event"; import { default as NEvent } from "Nostr/Event";
import EventKind from "Nostr/EventKind"; import EventKind from "Nostr/EventKind";
import Tag from "Nostr/Tag"; import Tag from "Nostr/Tag";
import { RootState } from "State/Store"; import { RootState } from "State/Store";
import { HexKey, RawEvent, u256, UserMetadata, Lists } from "Nostr"; import { HexKey, RawEvent, u256, UserMetadata, Lists } from "Nostr";
import { bech32ToHex } from "Util"; import { bech32ToHex, unwrap } from "Util";
import { DefaultRelays, HashtagRegex } from "Const"; import { DefaultRelays, HashtagRegex } from "Const";
import { RelaySettings } from "Nostr/Connection"; import { RelaySettings } from "Nostr/Connection";
@ -15,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>;
@ -27,23 +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();
let tmpEv = await barierNip07(() => const tmpEv = (await barrierNip07(() => window.nostr.signEvent(ev.ToObject()))) as RawEvent;
window.nostr.signEvent(ev.ToObject()) return new NEvent(tmpEv as TaggedRawEvent);
);
return new NEvent(tmpEv);
} else if (privKey) { } else if (privKey) {
await ev.Sign(privKey); await ev.Sign(privKey);
} else { } else {
@ -111,27 +104,25 @@ export default function useEventPublisher() {
*/ */
broadcastForBootstrap: (ev: NEvent | undefined) => { broadcastForBootstrap: (ev: NEvent | undefined) => {
if (ev) { if (ev) {
for (let [k, _] of DefaultRelays) { for (const [k] of DefaultRelays) {
System.WriteOnceToRelay(k, ev); System.WriteOnceToRelay(k, ev);
} }
} }
}, },
muted: async (keys: HexKey[], priv: HexKey[]) => { muted: async (keys: HexKey[], priv: HexKey[]) => {
if (pubKey) { if (pubKey) {
let 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 barierNip07(() => 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);
} }
@ -142,7 +133,7 @@ export default function useEventPublisher() {
}, },
metadata: async (obj: UserMetadata) => { metadata: async (obj: UserMetadata) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.SetMetadata; ev.Kind = EventKind.SetMetadata;
ev.Content = JSON.stringify(obj); ev.Content = JSON.stringify(obj);
return await signEvent(ev); return await signEvent(ev);
@ -150,7 +141,7 @@ export default function useEventPublisher() {
}, },
note: async (msg: string) => { note: async (msg: string) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.TextNote; ev.Kind = EventKind.TextNote;
processContent(ev, msg); processContent(ev, msg);
return await signEvent(ev); return await signEvent(ev);
@ -158,18 +149,14 @@ export default function useEventPublisher() {
}, },
zap: async (author: HexKey, note?: HexKey, msg?: string) => { zap: async (author: HexKey, note?: HexKey, msg?: string) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.ZapRequest; ev.Kind = EventKind.ZapRequest;
if (note) { if (note) {
// @ts-ignore ev.Tags.push(new Tag(["e", note], ev.Tags.length));
ev.Tags.push(new Tag(["e", note]));
} }
// @ts-ignore ev.Tags.push(new Tag(["p", author], ev.Tags.length));
ev.Tags.push(new Tag(["p", author]));
// @ts-ignore
const relayTag = ["relays", ...Object.keys(relays).slice(0, 10)]; const relayTag = ["relays", ...Object.keys(relays).slice(0, 10)];
// @ts-ignore ev.Tags.push(new Tag(relayTag, ev.Tags.length));
ev.Tags.push(new Tag(relayTag));
processContent(ev, msg || ""); processContent(ev, msg || "");
return await signEvent(ev); return await signEvent(ev);
} }
@ -179,18 +166,13 @@ export default function useEventPublisher() {
*/ */
reply: async (replyTo: NEvent, msg: string) => { reply: async (replyTo: NEvent, msg: string) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.TextNote; ev.Kind = EventKind.TextNote;
let 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));
@ -199,7 +181,7 @@ export default function useEventPublisher() {
ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length)); ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length));
} }
for (let pk of thread.PubKeys) { for (const pk of thread.PubKeys) {
if (pk === pubKey) { if (pk === pubKey) {
continue; // dont tag self in replies continue; // dont tag self in replies
} }
@ -218,7 +200,7 @@ export default function useEventPublisher() {
}, },
react: async (evRef: NEvent, content = "+") => { react: async (evRef: NEvent, content = "+") => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.Reaction; ev.Kind = EventKind.Reaction;
ev.Content = content; ev.Content = content;
ev.Tags.push(new Tag(["e", evRef.Id], 0)); ev.Tags.push(new Tag(["e", evRef.Id], 0));
@ -228,31 +210,28 @@ export default function useEventPublisher() {
}, },
saveRelays: async () => { saveRelays: async () => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.ContactList; ev.Kind = EventKind.ContactList;
ev.Content = JSON.stringify(relays); ev.Content = JSON.stringify(relays);
for (let pk of follows) { for (const pk of follows) {
ev.Tags.push(new Tag(["p", pk], ev.Tags.length)); ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
} }
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) {
let 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);
let 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);
} }
for (let pk of temp) { for (const pk of temp) {
if (pk.length !== 64) { if (pk.length !== 64) {
continue; continue;
} }
@ -264,10 +243,10 @@ export default function useEventPublisher() {
}, },
removeFollow: async (pkRemove: HexKey) => { removeFollow: async (pkRemove: HexKey) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.ContactList; ev.Kind = EventKind.ContactList;
ev.Content = JSON.stringify(relays); ev.Content = JSON.stringify(relays);
for (let pk of follows) { for (const pk of follows) {
if (pk === pkRemove || pk.length !== 64) { if (pk === pkRemove || pk.length !== 64) {
continue; continue;
} }
@ -282,7 +261,7 @@ export default function useEventPublisher() {
*/ */
delete: async (id: u256) => { delete: async (id: u256) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.Deletion; ev.Kind = EventKind.Deletion;
ev.Content = ""; ev.Content = "";
ev.Tags.push(new Tag(["e", id], 0)); ev.Tags.push(new Tag(["e", id], 0));
@ -290,11 +269,11 @@ export default function useEventPublisher() {
} }
}, },
/** /**
* Respot a note (NIP-18) * Repost a note (NIP-18)
*/ */
repost: async (note: NEvent) => { repost: async (note: NEvent) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.Repost; ev.Kind = EventKind.Repost;
ev.Content = JSON.stringify(note.Original); ev.Content = JSON.stringify(note.Original);
ev.Tags.push(new Tag(["e", note.Id], 0)); ev.Tags.push(new Tag(["e", note.Id], 0));
@ -304,43 +283,34 @@ 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 {
let otherPubKey = const otherPubKey =
note.PubKey === pubKey note.PubKey === pubKey ? unwrap(note.Tags.filter(a => a.Key === "p")[0].PubKey) : note.PubKey;
? note.Tags.filter((a) => a.Key === "p")[0].PubKey!
: note.PubKey;
if (hasNip07 && !privKey) { if (hasNip07 && !privKey) {
return await barierNip07(() => 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;
} }
} catch (e) { } catch (e) {
console.error("Decyrption failed", e); console.error("Decryption failed", e);
return "<DECRYPTION FAILED>"; return "<DECRYPTION FAILED>";
} }
} }
}, },
sendDm: async (content: string, to: HexKey) => { sendDm: async (content: string, to: HexKey) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.DirectMessage; ev.Kind = EventKind.DirectMessage;
ev.Content = content; ev.Content = content;
ev.Tags.push(new Tag(["p", to], 0)); ev.Tags.push(new Tag(["p", to], 0));
try { try {
if (hasNip07 && !privKey) { if (hasNip07 && !privKey) {
let cx: string = await barierNip07(() => 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) {
@ -358,12 +328,12 @@ export default function useEventPublisher() {
let isNip07Busy = false; let isNip07Busy = false;
const delay = (t: number) => { const delay = (t: number) => {
return new Promise((resolve, reject) => { return new Promise(resolve => {
setTimeout(resolve, t); setTimeout(resolve, t);
}); });
}; };
export const barierNip07 = async (then: () => Promise<any>) => { export const barrierNip07 = async <T>(then: () => Promise<T>): Promise<T> => {
while (isNip07Busy) { while (isNip07Busy) {
await delay(10); await delay(10);
} }

View File

@ -6,7 +6,7 @@ import useSubscription from "Feed/Subscription";
export default function useFollowersFeed(pubkey: HexKey) { export default function useFollowersFeed(pubkey: HexKey) {
const sub = useMemo(() => { const sub = useMemo(() => {
let x = new Subscriptions(); const x = new Subscriptions();
x.Id = `followers:${pubkey.slice(0, 12)}`; x.Id = `followers:${pubkey.slice(0, 12)}`;
x.Kinds = new Set([EventKind.ContactList]); x.Kinds = new Set([EventKind.ContactList]);
x.PTags = new Set([pubkey]); x.PTags = new Set([pubkey]);

View File

@ -6,7 +6,7 @@ import useSubscription, { NoteStore } from "Feed/Subscription";
export default function useFollowsFeed(pubkey: HexKey) { export default function useFollowsFeed(pubkey: HexKey) {
const sub = useMemo(() => { const sub = useMemo(() => {
let x = new Subscriptions(); const x = new Subscriptions();
x.Id = `follows:${pubkey.slice(0, 12)}`; x.Id = `follows:${pubkey.slice(0, 12)}`;
x.Kinds = new Set([EventKind.ContactList]); x.Kinds = new Set([EventKind.ContactList]);
x.Authors = new Set([pubkey]); x.Authors = new Set([pubkey]);
@ -18,11 +18,7 @@ export default function useFollowsFeed(pubkey: HexKey) {
} }
export function getFollowers(feed: NoteStore, pubkey: HexKey) { export function getFollowers(feed: NoteStore, pubkey: HexKey) {
let 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]));
);
let 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

@ -2,6 +2,7 @@ import * as secp from "@noble/secp256k1";
import * as base64 from "@protobufjs/base64"; import * as base64 from "@protobufjs/base64";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "State/Store"; import { RootState } from "State/Store";
import { unwrap } from "Util";
export interface ImgProxySettings { export interface ImgProxySettings {
url: string; url: string;
@ -10,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) {
@ -21,8 +20,8 @@ export default function useImgProxy() {
async function signUrl(u: string) { async function signUrl(u: string) {
const result = await secp.utils.hmacSha256( const result = await secp.utils.hmacSha256(
secp.utils.hexToBytes(settings!.key), secp.utils.hexToBytes(unwrap(settings).key),
secp.utils.hexToBytes(settings!.salt), secp.utils.hexToBytes(unwrap(settings).salt),
te.encode(u) te.encode(u)
); );
return urlSafe(base64.encode(result, 0, result.byteLength)); return urlSafe(base64.encode(result, 0, result.byteLength));
@ -33,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

@ -19,9 +19,11 @@ import { RootState } from "State/Store";
import { mapEventToProfile, MetadataCache } from "State/Users"; import { mapEventToProfile, MetadataCache } from "State/Users";
import { useDb } from "State/Users/Db"; import { useDb } from "State/Users/Db";
import useSubscription from "Feed/Subscription"; import useSubscription from "Feed/Subscription";
import { barierNip07 } from "Feed/EventPublisher"; 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 { AnyAction, ThunkDispatch } from "@reduxjs/toolkit";
/** /**
* Managed loading data for the current logged in user * Managed loading data for the current logged in user
@ -40,7 +42,7 @@ export default function useLoginFeed() {
const subMetadata = useMemo(() => { const subMetadata = useMemo(() => {
if (!pubKey) return null; if (!pubKey) return null;
let sub = new Subscriptions(); const sub = new Subscriptions();
sub.Id = `login:meta`; sub.Id = `login:meta`;
sub.Authors = new Set([pubKey]); sub.Authors = new Set([pubKey]);
sub.Kinds = new Set([EventKind.ContactList, EventKind.SetMetadata]); sub.Kinds = new Set([EventKind.ContactList, EventKind.SetMetadata]);
@ -52,7 +54,7 @@ export default function useLoginFeed() {
const subNotification = useMemo(() => { const subNotification = useMemo(() => {
if (!pubKey) return null; if (!pubKey) return null;
let sub = new Subscriptions(); const sub = new Subscriptions();
sub.Id = "login:notifications"; sub.Id = "login:notifications";
// todo: add zaps // todo: add zaps
sub.Kinds = new Set([EventKind.TextNote]); sub.Kinds = new Set([EventKind.TextNote]);
@ -64,7 +66,7 @@ export default function useLoginFeed() {
const subMuted = useMemo(() => { const subMuted = useMemo(() => {
if (!pubKey) return null; if (!pubKey) return null;
let sub = new Subscriptions(); const sub = new Subscriptions();
sub.Id = "login:muted"; sub.Id = "login:muted";
sub.Kinds = new Set([EventKind.Lists]); sub.Kinds = new Set([EventKind.Lists]);
sub.Authors = new Set([pubKey]); sub.Authors = new Set([pubKey]);
@ -77,12 +79,12 @@ export default function useLoginFeed() {
const subDms = useMemo(() => { const subDms = useMemo(() => {
if (!pubKey) return null; if (!pubKey) return null;
let dms = new Subscriptions(); const dms = new Subscriptions();
dms.Id = "login:dms"; dms.Id = "login:dms";
dms.Kinds = new Set([EventKind.DirectMessage]); dms.Kinds = new Set([EventKind.DirectMessage]);
dms.PTags = new Set([pubKey]); dms.PTags = new Set([pubKey]);
let dmsFromME = new Subscriptions(); const dmsFromME = new Subscriptions();
dmsFromME.Authors = new Set([pubKey]); dmsFromME.Authors = new Set([pubKey]);
dmsFromME.Kinds = new Set([EventKind.DirectMessage]); dmsFromME.Kinds = new Set([EventKind.DirectMessage]);
dms.AddSubscription(dmsFromME); dms.AddSubscription(dmsFromME);
@ -102,28 +104,24 @@ export default function useLoginFeed() {
const mutedFeed = useSubscription(subMuted, { leaveOpen: true, cache: true }); const mutedFeed = useSubscription(subMuted, { leaveOpen: true, cache: true });
useEffect(() => { useEffect(() => {
let 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 profiles = metadata
let metadata = metadataFeed.store.notes.filter( .map(a => mapEventToProfile(a))
(a) => a.kind === EventKind.SetMetadata .filter(a => a !== undefined)
); .map(a => unwrap(a));
let profiles = metadata
.map((a) => mapEventToProfile(a))
.filter((a) => a !== undefined)
.map((a) => a!);
for (let cl of contactList) { for (const cl of contactList) {
if (cl.content !== "" && cl.content !== "{}") { if (cl.content !== "" && cl.content !== "{}") {
let 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 }));
} }
let 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 }));
} }
(async () => { (async () => {
let maxProfile = profiles.reduce( const maxProfile = profiles.reduce(
(acc, v) => { (acc, v) => {
if (v.created > acc.created) { if (v.created > acc.created) {
acc.profile = v; acc.profile = v;
@ -134,7 +132,7 @@ export default function useLoginFeed() {
{ created: 0, profile: null as MetadataCache | null } { created: 0, profile: null as MetadataCache | null }
); );
if (maxProfile.profile) { if (maxProfile.profile) {
let existing = await db.find(maxProfile.profile.pubkey); const existing = await db.find(maxProfile.profile.pubkey);
if ((existing?.created ?? 0) < maxProfile.created) { if ((existing?.created ?? 0) < maxProfile.created) {
await db.put(maxProfile.profile); await db.put(maxProfile.profile);
} }
@ -144,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-ignore (dispatch as ThunkDispatch<RootState, undefined, AnyAction>)(sendNotification(notification));
dispatch(sendNotification(notification));
} }
}); });
}); });
@ -165,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: any) => p && p.length === 2 && p[0] === "p")
.map((p: any) => p[1]);
dispatch( dispatch(
setBlocked({ setBlocked({
keys, keys,
@ -188,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(() => {
let 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 barierNip07(() => return await barrierNip07(() => window.nostr.nip04.decrypt(pubKey, raw.content));
window.nostr.nip04.decrypt(pubKey, raw.content)
);
} }
} }

View File

@ -7,7 +7,7 @@ import useSubscription, { NoteStore } from "Feed/Subscription";
export default function useMutedFeed(pubkey: HexKey) { export default function useMutedFeed(pubkey: HexKey) {
const sub = useMemo(() => { const sub = useMemo(() => {
let sub = new Subscriptions(); const sub = new Subscriptions();
sub.Id = `muted:${pubkey.slice(0, 12)}`; sub.Id = `muted:${pubkey.slice(0, 12)}`;
sub.Kinds = new Set([EventKind.Lists]); sub.Kinds = new Set([EventKind.Lists]);
sub.Authors = new Set([pubkey]); sub.Authors = new Set([pubkey]);
@ -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[] {
let 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

@ -1,16 +1,16 @@
import { useSyncExternalStore } from "react"; import { useSyncExternalStore } from "react";
import { System } from "Nostr/System"; import { System } from "Nostr/System";
import { CustomHook, StateSnapshot } from "Nostr/Connection"; import { StateSnapshot } from "Nostr/Connection";
const noop = (f: CustomHook) => { const noop = () => {
return () => {}; return () => undefined;
}; };
const noopState = (): StateSnapshot | undefined => { const noopState = (): StateSnapshot | undefined => {
return undefined; return undefined;
}; };
export default function useRelayState(addr: string) { export default function useRelayState(addr: string) {
let c = System.Sockets.get(addr); const c = System.Sockets.get(addr);
return useSyncExternalStore<StateSnapshot | undefined>( return useSyncExternalStore<StateSnapshot | undefined>(
c?.StatusHook.bind(c) ?? noop, c?.StatusHook.bind(c) ?? noop,
c?.GetState.bind(c) ?? noopState c?.GetState.bind(c) ?? noopState

View File

@ -2,7 +2,7 @@ import { useEffect, useMemo, useReducer, useState } from "react";
import { System } from "Nostr/System"; import { System } from "Nostr/System";
import { TaggedRawEvent } from "Nostr"; import { TaggedRawEvent } from "Nostr";
import { Subscriptions } from "Nostr/Subscriptions"; import { Subscriptions } from "Nostr/Subscriptions";
import { debounce } from "Util"; import { debounce, unwrap } from "Util";
import { db } from "Db"; import { db } from "Db";
export type NoteStore = { export type NoteStore = {
@ -17,7 +17,7 @@ export type UseSubscriptionOptions = {
interface ReducerArg { interface ReducerArg {
type: "END" | "EVENT" | "CLEAR"; type: "END" | "EVENT" | "CLEAR";
ev?: TaggedRawEvent | Array<TaggedRawEvent>; ev?: TaggedRawEvent | TaggedRawEvent[];
end?: boolean; end?: boolean;
} }
@ -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!, end: arg.end ?? true,
} as NoteStore; } as NoteStore;
} }
@ -36,12 +36,12 @@ function notesReducer(state: NoteStore, arg: ReducerArg) {
} as NoteStore; } as NoteStore;
} }
let evs = arg.ev!; let evs = arg.ev;
if (!Array.isArray(evs)) { if (!(evs instanceof Array)) {
evs = [evs]; evs = evs === undefined ? [] : [evs];
} }
let 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) => 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,15 +10,13 @@ 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 => {
let orig = new Set(s); const orig = new Set(s);
if (id.some((a) => !orig.has(a))) { if (id.some(a => !orig.has(a))) {
let tmp = new Set([...s, ...id]); const tmp = new Set([...s, ...id]);
return Array.from(tmp); return Array.from(tmp);
} else { } else {
return s; return s;
@ -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,16 +47,14 @@ export default function useThreadFeed(id: u256) {
useEffect(() => { useEffect(() => {
if (main.store) { if (main.store) {
return debounce(200, () => { return debounce(200, () => {
let mainNotes = main.store.notes.filter( const mainNotes = main.store.notes.filter(a => a.kind === EventKind.TextNote);
(a) => a.kind === EventKind.TextNote
);
let 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();
let ids = mainNotes.map((a) => a.id); const ids = mainNotes.map(a => a.id);
let allEvents = new Set([...eTags, ...ids]); const allEvents = new Set([...eTags, ...ids]);
addId(Array.from(allEvents)); addId(Array.from(allEvents));
}); });
} }

View File

@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { u256 } from "Nostr"; import { u256 } from "Nostr";
import EventKind from "Nostr/EventKind"; import EventKind from "Nostr/EventKind";
import { Subscriptions } from "Nostr/Subscriptions"; import { Subscriptions } from "Nostr/Subscriptions";
import { unixNow } from "Util"; import { unixNow, unwrap } from "Util";
import useSubscription from "Feed/Subscription"; import useSubscription from "Feed/Subscription";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "State/Store"; import { RootState } from "State/Store";
@ -19,26 +19,21 @@ 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) {
return null; return null;
} }
let sub = new Subscriptions(); const sub = new Subscriptions();
sub.Id = `timeline:${subject.type}:${subject.discriminator}`; sub.Id = `timeline:${subject.type}:${subject.discriminator}`;
sub.Kinds = new Set([EventKind.TextNote, EventKind.Repost]); sub.Kinds = new Set([EventKind.TextNote, EventKind.Repost]);
switch (subject.type) { switch (subject.type) {
@ -64,7 +59,7 @@ export default function useTimelineFeed(
}, [subject.type, subject.items, subject.discriminator]); }, [subject.type, subject.items, subject.discriminator]);
const sub = useMemo(() => { const sub = useMemo(() => {
let sub = createSub(); const sub = createSub();
if (sub) { if (sub) {
if (options.method === "LIMIT_UNTIL") { if (options.method === "LIMIT_UNTIL") {
sub.Until = until; sub.Until = until;
@ -80,7 +75,7 @@ export default function useTimelineFeed(
if (pref.autoShowLatest) { if (pref.autoShowLatest) {
// copy properties of main sub but with limit 0 // copy properties of main sub but with limit 0
// this will put latest directly into main feed // this will put latest directly into main feed
let latestSub = new Subscriptions(); const latestSub = new Subscriptions();
latestSub.Authors = sub.Authors; latestSub.Authors = sub.Authors;
latestSub.HashTags = sub.HashTags; latestSub.HashTags = sub.HashTags;
latestSub.PTags = sub.PTags; latestSub.PTags = sub.PTags;
@ -97,7 +92,7 @@ export default function useTimelineFeed(
const main = useSubscription(sub, { leaveOpen: true, cache: true }); const main = useSubscription(sub, { leaveOpen: true, cache: true });
const subRealtime = useMemo(() => { const subRealtime = useMemo(() => {
let subLatest = createSub(); const subLatest = createSub();
if (subLatest && !pref.autoShowLatest) { if (subLatest && !pref.autoShowLatest) {
subLatest.Id = `${subLatest.Id}:latest`; subLatest.Id = `${subLatest.Id}:latest`;
subLatest.Limit = 1; subLatest.Limit = 1;
@ -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;
@ -131,7 +121,7 @@ export default function useTimelineFeed(
const subParents = useMemo(() => { const subParents = useMemo(() => {
if (trackingParentEvents.length > 0) { if (trackingParentEvents.length > 0) {
let parents = new Subscriptions(); const parents = new Subscriptions();
parents.Id = `timeline-parent:${subject.type}`; parents.Id = `timeline-parent:${subject.type}`;
parents.Ids = new Set(trackingParentEvents); parents.Ids = new Set(trackingParentEvents);
return parents; return parents;
@ -143,22 +133,22 @@ export default function useTimelineFeed(
useEffect(() => { useEffect(() => {
if (main.store.notes.length > 0) { if (main.store.notes.length > 0) {
setTrackingEvent((s) => { setTrackingEvent(s => {
let 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;
}); });
let 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) => 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))) {
let temp = new Set([...s, ...reposts]); const temp = new Set([...s, ...reposts]);
return Array.from(temp); return Array.from(temp);
} }
return s; return s;
@ -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") {
let 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

@ -6,7 +6,7 @@ import useSubscription from "./Subscription";
export default function useZapsFeed(pubkey: HexKey) { export default function useZapsFeed(pubkey: HexKey) {
const sub = useMemo(() => { const sub = useMemo(() => {
let x = new Subscriptions(); const x = new Subscriptions();
x.Id = `zaps:${pubkey.slice(0, 12)}`; x.Id = `zaps:${pubkey.slice(0, 12)}`;
x.Kinds = new Set([EventKind.ZapReceipt]); x.Kinds = new Set([EventKind.ZapReceipt]);
x.PTags = new Set([pubkey]); x.PTags = new Set([pubkey]);

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, WheelEvent, LegacyRef } from "react"; import { useEffect, useRef, LegacyRef } from "react";
function useHorizontalScroll() { function useHorizontalScroll() {
const elRef = useRef<HTMLDivElement>(); const elRef = useRef<HTMLDivElement>();
@ -10,9 +10,7 @@ function useHorizontalScroll() {
ev.preventDefault(); ev.preventDefault();
el.scrollTo({ left: el.scrollLeft + ev.deltaY, behavior: "smooth" }); el.scrollTo({ left: el.scrollLeft + ev.deltaY, behavior: "smooth" });
}; };
// @ts-ignore
el.addEventListener("wheel", onWheel); el.addEventListener("wheel", onWheel);
// @ts-ignore
return () => el.removeEventListener("wheel", onWheel); return () => el.removeEventListener("wheel", onWheel);
} }
}, []); }, []);

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

@ -5,7 +5,7 @@ declare global {
webln?: { webln?: {
enabled: boolean; enabled: boolean;
enable: () => Promise<void>; enable: () => Promise<void>;
sendPayment: (pr: string) => Promise<any>; sendPayment: (pr: string) => Promise<unknown>;
}; };
} }
} }
@ -15,7 +15,7 @@ export default function useWebln(enable = true) {
useEffect(() => { useEffect(() => {
if (maybeWebLn && !maybeWebLn.enabled && enable) { if (maybeWebLn && !maybeWebLn.enabled && enable) {
maybeWebLn.enable().catch((error) => { maybeWebLn.enable().catch(() => {
console.debug("Couldn't enable WebLN"); console.debug("Couldn't enable WebLN");
}); });
} }

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,14 +1,6 @@
import IconProps from "./IconProps"; const Attachment = () => {
const Attachment = (props: IconProps) => {
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,14 +1,6 @@
import IconProps from "./IconProps"; const Logout = () => {
const Logout = (props: IconProps) => {
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,14 +1,6 @@
import IconProps from "./IconProps";
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

@ -1,42 +1,38 @@
import { type ReactNode } from 'react' import { type ReactNode } from "react";
import { IntlProvider as ReactIntlProvider } from 'react-intl' import { IntlProvider as ReactIntlProvider } from "react-intl";
import enMessages from 'translations/en.json' import enMessages from "translations/en.json";
import esMessages from 'translations/es.json' import esMessages from "translations/es.json";
import zhMessages from 'translations/zh.json' import zhMessages from "translations/zh.json";
import jaMessages from 'translations/ja.json' import jaMessages from "translations/ja.json";
const DEFAULT_LOCALE = 'en-US' const DEFAULT_LOCALE = "en-US";
const getMessages = (locale: string) => { const getMessages = (locale: string) => {
const truncatedLocale = locale.toLowerCase().split(/[_-]+/)[0] const truncatedLocale = locale.toLowerCase().split(/[_-]+/)[0];
switch (truncatedLocale) { switch (truncatedLocale) {
case 'en': case "en":
return enMessages return enMessages;
case 'es': case "es":
return esMessages return esMessages;
case 'zh': case "zh":
return zhMessages return zhMessages;
case 'ja': case "ja":
return jaMessages return jaMessages;
default: default:
return enMessages return enMessages;
} }
} };
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 || const locale = getLocale();
DEFAULT_LOCALE
)
}
const locale = getLocale()
return ( return (
<ReactIntlProvider locale={locale} messages={getMessages(locale)}> <ReactIntlProvider locale={locale} messages={getMessages(locale)}>
{children} {children}
</ReactIntlProvider> </ReactIntlProvider>
) );
} };

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