Eject CRA #565

Merged
Kieran merged 7 commits from eject into main 2023-05-18 08:48:08 +00:00
46 changed files with 3282 additions and 4680 deletions

2
_headers Normal file
View File

@ -0,0 +1,2 @@
/*
Content-Security-Policy: default-src 'self'; manifest-src *; 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 https://player.twitch.tv https://embed.music.apple.com https://nostrnests.com https://embed.wavlake.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src *; img-src * data:; font-src https://fonts.gstatic.com; media-src *; script-src 'self' 'wasm-unsafe-eval' https://static.cloudflareinsights.com https://platform.twitter.com https://embed.tidal.com;

View File

@ -1,3 +0,0 @@
{
"plugins": [["formatjs"]]
}

View File

@ -3,11 +3,11 @@ module.exports = {
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
root: true,
ignorePatterns: ["build/", "*.test.ts"],
ignorePatterns: ["build/", "*.test.ts", "*.js"],
env: {
browser: true,
worker: true,
commonjs: true,
node: true,
node: false,
},
};

View File

@ -0,0 +1,11 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
[
"formatjs"
]
]
}

View File

@ -1,4 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { useBabelRc, override } = require("customize-cra");
module.exports = override(useBabelRc());

View File

@ -4,16 +4,13 @@
"private": true,
"dependencies": {
"@cashu/cashu-ts": "^0.6.1",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@jukben/emoji-search": "^2.0.1",
"@lightninglabs/lnc-web": "^0.2.3-alpha",
"@noble/curves": "^1.0.0",
"@noble/hashes": "^1.2.0",
"@protobufjs/base64": "^1.1.2",
"@reduxjs/toolkit": "^1.9.1",
"@scure/bip32": "^1.1.5",
"@scure/bip32": "^1.3.0",
"@scure/bip39": "^1.1.1",
"@snort/nostr": "^1.0.0",
"@szhsin/react-menu": "^3.3.1",
@ -29,14 +26,10 @@
"react-dom": "^18.2.0",
"react-intersection-observer": "^9.4.1",
"react-intl": "^6.2.8",
"react-markdown": "^8.0.4",
"react-query": "^3.39.2",
"react-redux": "^8.0.5",
"react-router-dom": "^6.5.0",
"react-textarea-autosize": "^8.4.0",
"react-twitter-embed": "^4.0.4",
"throttle-debounce": "^5.0.0",
"unist-util-visit": "^4.1.2",
"use-long-press": "^2.0.3",
"workbox-background-sync": "^6.4.2",
"workbox-broadcast-update": "^6.4.2",
@ -52,10 +45,9 @@
"workbox-streams": "^6.4.2"
},
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject",
"start": "webpack serve",
"build": "webpack --node-env=production",
"test": "",
"intl-extract": "formatjs extract 'src/**/*.ts*' --ignore='**/*.d.ts' --out-file src/lang.json --flatten true",
"intl-compile": "formatjs compile src/lang.json --out-file src/translations/en.json",
"format": "prettier --write .",
@ -83,22 +75,38 @@
},
"devDependencies": {
"@babel/plugin-syntax-import-assertions": "^7.20.0",
"@babel/preset-env": "^7.21.5",
"@babel/preset-react": "^7.18.6",
"@formatjs/cli": "^6.0.1",
"@formatjs/ts-transformer": "^3.13.1",
"@types/jest": "^29.2.5",
"@types/node": "^18.11.18",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"@types/webscopeio__react-textarea-autocomplete": "^4.7.2",
"@types/webtorrent": "^0.109.3",
"@webpack-cli/generators": "^3.0.4",
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
"babel-plugin-formatjs": "^10.3.36",
"babel-loader": "^9.1.2",
"babel-plugin-formatjs": "^10.5.1",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.3",
"css-minimizer-webpack-plugin": "^5.0.0",
"customize-cra": "^1.0.0",
"eslint-plugin-formatjs": "^4.10.1",
"eslint-webpack-plugin": "^4.0.1",
"html-webpack-plugin": "^5.5.1",
"husky": ">=6",
"lint-staged": ">=10",
"mini-css-extract-plugin": "^2.7.5",
"prettier": "2.8.3",
"react-app-rewired": "^2.2.1",
"react-scripts": "5.0.1",
"typescript": "^4.9.4"
"ts-loader": "^9.4.2",
"typescript": "^5.0.4",
"webpack": "^5.82.1",
"webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^5.1.1",
"webpack-dev-server": "^4.15.0",
"workbox-webpack-plugin": "^6.5.4"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,css,md}": "prettier --write"

View File

@ -157,6 +157,18 @@
<symbol id="piggy-bank" viewBox="0 0 22 20" fill="none">
<path d="M3.99993 11C3.99993 12.6484 4.66466 14.1415 5.74067 15.226C5.84445 15.3305 5.89633 15.3828 5.92696 15.4331C5.95619 15.4811 5.9732 15.5224 5.98625 15.5771C5.99993 15.6343 5.99993 15.6995 5.99993 15.8298V18.2C5.99993 18.48 5.99993 18.62 6.05443 18.727C6.10236 18.8211 6.17885 18.8976 6.27293 18.9455C6.37989 19 6.5199 19 6.79993 19H8.69993C8.97996 19 9.11997 19 9.22693 18.9455C9.32101 18.8976 9.3975 18.8211 9.44543 18.727C9.49993 18.62 9.49993 18.48 9.49993 18.2V17.8C9.49993 17.52 9.49993 17.38 9.55443 17.273C9.60236 17.1789 9.67885 17.1024 9.77293 17.0545C9.87989 17 10.0199 17 10.2999 17H11.6999C11.98 17 12.12 17 12.2269 17.0545C12.321 17.1024 12.3975 17.1789 12.4454 17.273C12.4999 17.38 12.4999 17.52 12.4999 17.8V18.2C12.4999 18.48 12.4999 18.62 12.5544 18.727C12.6024 18.8211 12.6789 18.8976 12.7729 18.9455C12.8799 19 13.0199 19 13.2999 19H15.2C15.48 19 15.62 19 15.727 18.9455C15.8211 18.8976 15.8976 18.8211 15.9455 18.727C16 18.62 16 18.48 16 18.2V17.2243C16 17.0223 16 16.9212 16.0288 16.8401C16.0563 16.7624 16.0911 16.708 16.15 16.6502C16.2114 16.59 16.3155 16.5417 16.5237 16.445C17.5059 15.989 18.344 15.2751 18.9511 14.3902C19.0579 14.2346 19.1112 14.1568 19.1683 14.1108C19.2228 14.0668 19.2717 14.0411 19.3387 14.021C19.4089 14 19.4922 14 19.6587 14H20.2C20.48 14 20.62 14 20.727 13.9455C20.8211 13.8976 20.8976 13.8211 20.9455 13.727C21 13.62 21 13.48 21 13.2V9.78575C21 9.51916 21 9.38586 20.9505 9.28303C20.9013 9.181 20.819 9.09867 20.717 9.04953C20.6141 9 20.4808 9 20.2143 9C20.0213 9 19.9248 9 19.8471 8.9738C19.7633 8.94556 19.7045 8.90798 19.6437 8.84377C19.5874 8.78422 19.5413 8.68464 19.4493 8.48547C19.1538 7.84622 18.7492 7.26777 18.2593 6.77404C18.1555 6.66945 18.1036 6.61716 18.073 6.56687C18.0437 6.51889 18.0267 6.47759 18.0137 6.42294C18 6.36567 18 6.30051 18 6.17018V5.06058C18 4.70053 18 4.52051 17.925 4.39951C17.8593 4.29351 17.7564 4.21588 17.6365 4.18184C17.4995 4.14299 17.3264 4.19245 16.9802 4.29136L14.6077 4.96922C14.5673 4.98074 14.5472 4.9865 14.5267 4.99054C14.5085 4.99414 14.4901 4.99671 14.4716 4.99826C14.4508 5 14.4298 5 14.3879 5H13.959M3.99993 11C3.99993 8.69594 5.29864 6.6952 7.20397 5.6899M3.99993 11H3C1.89543 11 1 10.1046 1 9C1 8.25972 1.4022 7.61337 2 7.26756M14 4.5C14 6.433 12.433 8 10.5 8C8.567 8 7 6.433 7 4.5C7 2.567 8.567 1 10.5 1C12.433 1 14 2.567 14 4.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="clock" viewBox="0 0 22 22" fill="none">
<path d="M11 5V11L15 13M21 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 11Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="book-closed" viewBox="0 0 18 22" fill="none">
<path d="M17 18V15H4C2.34315 15 1 16.3431 1 18M5.8 21H13.8C14.9201 21 15.4802 21 15.908 20.782C16.2843 20.5903 16.5903 20.2843 16.782 19.908C17 19.4802 17 18.9201 17 17.8V4.2C17 3.07989 17 2.51984 16.782 2.09202C16.5903 1.71569 16.2843 1.40973 15.908 1.21799C15.4802 1 14.9201 1 13.8 1H5.8C4.11984 1 3.27976 1 2.63803 1.32698C2.07354 1.6146 1.6146 2.07354 1.32698 2.63803C1 3.27976 1 4.11984 1 5.8V16.2C1 17.8802 1 18.7202 1.32698 19.362C1.6146 19.9265 2.07354 20.3854 2.63803 20.673C3.27976 21 4.11984 21 5.8 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="shopping-bag" viewBox="0 0 22 22" fill="none">
<path d="M15.0004 8V5C15.0004 2.79086 13.2095 1 11.0004 1C8.79123 1 7.00037 2.79086 7.00037 5V8M2.59237 9.35196L1.99237 15.752C1.82178 17.5717 1.73648 18.4815 2.03842 19.1843C2.30367 19.8016 2.76849 20.3121 3.35839 20.6338C4.0299 21 4.94374 21 6.77142 21H15.2293C17.057 21 17.9708 21 18.6423 20.6338C19.2322 20.3121 19.6971 19.8016 19.9623 19.1843C20.2643 18.4815 20.179 17.5717 20.0084 15.752L19.4084 9.35197C19.2643 7.81535 19.1923 7.04704 18.8467 6.46616C18.5424 5.95458 18.0927 5.54511 17.555 5.28984C16.9444 5 16.1727 5 14.6293 5L7.37142 5C5.82806 5 5.05638 5 4.44579 5.28984C3.90803 5.54511 3.45838 5.95458 3.15403 6.46616C2.80846 7.04704 2.73643 7.81534 2.59237 9.35196Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="wifi" viewBox="0 0 24 18" fill="none">
<path d="M12 16.5H12.01M22.8064 5.70076C19.9595 3.09199 16.1656 1.5 11.9999 1.5C7.83414 1.5 4.04023 3.09199 1.19336 5.70076M4.73193 9.24297C6.67006 7.53566 9.21407 6.5 12 6.5C14.7859 6.5 17.3299 7.53566 19.268 9.24297M15.6983 12.7751C14.6792 11.9763 13.3952 11.5 11.9999 11.5C10.5835 11.5 9.28172 11.9908 8.25537 12.8116" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -2,24 +2,19 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Fast nostr web ui" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; manifest-src *; 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 https://player.twitch.tv https://embed.music.apple.com https://nostrnests.com https://embed.wavlake.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src *; img-src * data:; font-src https://fonts.gstatic.com; media-src *; script-src 'self' 'wasm-unsafe-eval' https://static.cloudflareinsights.com https://platform.twitter.com https://embed.tidal.com;" />
<meta name="description" content="Feature packed nostr client" />
<meta name="keywords" content="nostr snort fast decentralized social media censorship resistant open source software" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://imgproxy.snort.social" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/nostrich_512.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>snort.social - Nostr interface</title>
<link rel="apple-touch-icon" href="/nostrich_512.png" />
<link rel="manifest" href="/manifest.json" />
<title>Snort - Nostr</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@ -2,13 +2,16 @@ import FeedCache from "Cache/FeedCache";
import { db } from "Db";
import { MetadataCache } from "Cache";
import { LNURL } from "LNURL";
import { fetchNip05Pubkey } from "Nip05/Verifier";
class UserProfileCache extends FeedCache<MetadataCache> {
#zapperQueue: Array<{ pubkey: string; lnurl: string }> = [];
#nip5Queue: Array<{ pubkey: string; nip05: string }> = [];
constructor() {
super("UserCache", db.users);
this.#processZapperQueue();
this.#processNip5Queue();
}
key(of: MetadataCache): string {
@ -80,6 +83,12 @@ class UserProfileCache extends FeedCache<MetadataCache> {
});
}
}
if (m.nip05) {
this.#nip5Queue.push({
pubkey: m.pubkey,
nip05: m.nip05,
});
}
}
return updateType;
}
@ -98,27 +107,68 @@ class UserProfileCache extends FeedCache<MetadataCache> {
}
async #processZapperQueue() {
while (this.#zapperQueue.length > 0) {
const i = this.#zapperQueue.shift();
if (i) {
try {
const svc = new LNURL(i.lnurl);
await svc.load();
const p = this.getFromCache(i.pubkey);
if (p) {
this.#setItem({
...p,
zapService: svc.zapperPubkey,
});
}
} catch {
console.warn("Failed to load LNURL for zapper pubkey", i.lnurl);
await this.#batchQueue(
this.#zapperQueue,
async i => {
const svc = new LNURL(i.lnurl);
await svc.load();
const p = this.getFromCache(i.pubkey);
if (p) {
this.#setItem({
...p,
zapService: svc.zapperPubkey,
});
}
}
}
},
5
);
setTimeout(() => this.#processZapperQueue(), 1_000);
}
async #processNip5Queue() {
await this.#batchQueue(
this.#nip5Queue,
async i => {
const [name, domain] = i.nip05.split("@");
const nip5pk = await fetchNip05Pubkey(name, domain);
const p = this.getFromCache(i.pubkey);
if (p) {
this.#setItem({
...p,
isNostrAddressValid: i.pubkey === nip5pk,
});
}
},
5
);
setTimeout(() => this.#processNip5Queue(), 1_000);
}
async #batchQueue<T>(queue: Array<T>, proc: (v: T) => Promise<void>, batchSize = 3) {
const batch = [];
while (queue.length > 0) {
const i = queue.shift();
if (i) {
batch.push(
(async () => {
try {
await proc(i);
} catch {
console.warn("Failed to process item", i);
}
batch.pop(); // pop any
})()
);
if (batch.length === batchSize) {
await Promise.all(batch);
}
} else {
await Promise.all(batch);
}
}
}
}
export const UserCache = new UserProfileCache();

View File

@ -29,6 +29,11 @@ export interface MetadataCache extends UserMetadata {
* Pubkey of zapper service
*/
zapService?: HexKey;
/**
* If the nip05 is valid for this user
*/
isNostrAddressValid: boolean;
}
export function mapEventToProfile(ev: RawEvent) {

View File

@ -1,10 +1,19 @@
import { getDecodedToken } from "@cashu/cashu-ts";
import { useMemo } from "react";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import useLogin from "Hooks/useLogin";
import { useUserProfile } from "Hooks/useUserProfile";
interface Token {
token: Array<{
mint: string;
proofs: Array<{
amount: number;
}>;
}>;
memo?: string;
}
export default function CashuNuts({ token }: { token: string }) {
const login = useLogin();
const profile = useUserProfile(login.publicKey);
@ -22,12 +31,16 @@ export default function CashuNuts({ token }: { token: string }) {
window.open(url, "_blank");
}
const cashu = useMemo(() => {
const [cashu, setCashu] = useState<Token>();
useEffect(() => {
try {
if (!token.startsWith("cashuA") || token.length < 10) {
return;
}
return getDecodedToken(token);
import("@cashu/cashu-ts").then(({ getDecodedToken }) => {
const tkn = getDecodedToken(token);
setCashu(tkn);
});
} catch {
// ignored
}

View File

@ -1,59 +1,12 @@
import "./Nip05.css";
import { useQuery } from "react-query";
import { HexKey } from "@snort/nostr";
import DnsOverHttpResolver from "dns-over-http-resolver";
import Icon from "Icons/Icon";
import { bech32ToHex } from "Util";
import { useUserProfile } from "Hooks/useUserProfile";
interface NostrJson {
names: Record<string, string>;
}
const resolver = new DnsOverHttpResolver();
async function fetchNip05Pubkey(name: string, domain: string) {
if (!name || !domain) {
return undefined;
}
try {
const res = await fetch(`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`);
const data: NostrJson = await res.json();
const match = Object.keys(data.names).find(n => {
return n.toLowerCase() === name.toLowerCase();
});
return match ? data.names[match] : undefined;
} catch {
// ignored
}
// Check as DoH TXT entry
try {
const resDns = await resolver.resolveTxt(`${name}._nostr.${domain}`);
return bech32ToHex(resDns[0][0]);
} catch {
// ignored
}
return undefined;
}
const VERIFICATION_CACHE_TIME = 24 * 60 * 60 * 1000;
const VERIFICATION_STALE_TIMEOUT = 10 * 60 * 1000;
export function useIsVerified(pubkey: HexKey, nip05?: string, bypassCheck?: boolean) {
const [name, domain] = nip05 ? nip05.split("@") : [];
const { isError, isSuccess, data } = useQuery(
["nip05", nip05],
() => (bypassCheck ? Promise.resolve(pubkey) : fetchNip05Pubkey(name, domain)),
{
retry: false,
retryOnMount: false,
cacheTime: VERIFICATION_CACHE_TIME,
staleTime: VERIFICATION_STALE_TIMEOUT,
}
);
const isVerified = isSuccess && data === pubkey;
const cantVerify = isSuccess && data !== pubkey;
return { isVerified, couldNotVerify: isError || cantVerify };
export function useIsVerified(pubkey: HexKey, bypassCheck?: boolean) {
const profile = useUserProfile(pubkey);
return { isVerified: bypassCheck || profile?.isNostrAddressValid };
}
export interface Nip05Params {
@ -65,10 +18,10 @@ export interface Nip05Params {
const Nip05 = ({ nip05, pubkey, verifyNip = true }: Nip05Params) => {
const [name, domain] = nip05 ? nip05.split("@") : [];
const isDefaultUser = name === "_";
const { isVerified, couldNotVerify } = useIsVerified(pubkey, nip05, !verifyNip);
const { isVerified } = useIsVerified(pubkey, !verifyNip);
return (
<div className={`flex nip05${couldNotVerify ? " failed" : ""}`}>
<div className={`flex nip05${!isVerified ? " failed" : ""}`}>
{!isDefaultUser && isVerified && <span className="nick">{`${name}@`}</span>}
{isVerified && (
<>

View File

@ -180,6 +180,8 @@
.note .poll-body > div > .progress {
background-color: var(--gray);
height: stretch;
height: -webkit-fill-available;
height: -moz-available;
position: absolute;
z-index: 1;
}

View File

@ -18,8 +18,9 @@
background-color: var(--note-bg);
border-radius: 10px 10px 0 0;
min-height: 100px;
max-width: stretch;
min-width: stretch;
width: stretch;
width: -webkit-fill-available;
width: -moz-available;
max-height: 210px;
}
@ -57,6 +58,8 @@
display: flex;
justify-content: flex-end;
width: stretch;
width: -webkit-fill-available;
width: -moz-available;
}
.note-creator .insert > button {

View File

@ -3,20 +3,19 @@
align-items: center;
}
.note-to-self {
margin-left: 5px;
margin-top: 3px;
}
.nts .avatar-wrapper {
margin-right: 8px;
}
.nts .avatar {
border-width: 1px;
width: 40px;
height: 40px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
}
.nts .avatar.clickable {
cursor: pointer;
}

View File

@ -1,11 +1,10 @@
import "./NoteToSelf.css";
import { Link, useNavigate } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBook, faCertificate } from "@fortawesome/free-solid-svg-icons";
import { profileLink } from "Util";
import messages from "./messages";
import Icon from "Icons/Icon";
export interface NoteToSelfProps {
pubkey: string;
@ -17,7 +16,7 @@ export interface NoteToSelfProps {
function NoteLabel() {
return (
<div>
<FormattedMessage {...messages.NoteToSelf} /> <FontAwesomeIcon icon={faCertificate} size="xs" />
<FormattedMessage {...messages.NoteToSelf} /> <Icon name="badge" size={15} />
</div>
);
}
@ -35,7 +34,7 @@ export default function NoteToSelf({ pubkey, clickable, className, link }: NoteT
<div className={`nts${className ? ` ${className}` : ""}`}>
<div className="avatar-wrapper">
<div className={`avatar${clickable ? " clickable" : ""}`}>
<FontAwesomeIcon onClick={clickLink} className="note-to-self" icon={faBook} size="2xl" />
<Icon onClick={clickLink} name="book-closed" size={20} />
</div>
</div>
<div className="f-grow">

View File

@ -31,6 +31,8 @@ a.pfp {
.pfp .profile-name {
max-width: stretch;
max-width: -webkit-fill-available;
max-width: -moz-available;
}
.pfp a {

View File

@ -1,33 +1,23 @@
import "./Relay.css";
import { useMemo } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faPlug,
faSquareCheck,
faSquareXmark,
faWifi,
faPlugCircleXmark,
faGear,
faWarning,
} from "@fortawesome/free-solid-svg-icons";
import { RelaySettings } from "@snort/nostr";
import useRelayState from "Feed/RelayState";
import { System } from "System";
import { getRelayName, unixNowMs, unwrap } from "Util";
import messages from "./messages";
import useLogin from "Hooks/useLogin";
import { setRelays } from "Login";
import Icon from "Icons/Icon";
import messages from "./messages";
export interface RelayProps {
addr: string;
}
export default function Relay(props: RelayProps) {
const { formatMessage } = useIntl();
const navigate = useNavigate();
const login = useLogin();
const relaySettings = unwrap(login.relays.item[props.addr] ?? System.Sockets.get(props.addr)?.Settings ?? {});
@ -50,7 +40,7 @@ export default function Relay(props: RelayProps) {
<>
<div className={`relay w-max`}>
<div className={`flex ${state?.connected ? "bg-success" : "bg-error"}`}>
<FontAwesomeIcon icon={faPlug} />
<Icon name="wifi" />
</div>
<div className="f-grow f-col">
<div className="flex mb10">
@ -65,7 +55,7 @@ export default function Relay(props: RelayProps) {
read: relaySettings.read,
})
}>
<FontAwesomeIcon icon={relaySettings.write ? faSquareCheck : faSquareXmark} />
<Icon name={relaySettings.write ? "check" : "close"} size={12} />
</span>
</div>
<div className="f-1">
@ -78,28 +68,15 @@ export default function Relay(props: RelayProps) {
read: !relaySettings.read,
})
}>
<FontAwesomeIcon icon={relaySettings.read ? faSquareCheck : faSquareXmark} />
<Icon name={relaySettings.read ? "check" : "close"} size={12} />
</span>
</div>
</div>
<div className="flex">
<div className="f-grow">
<FontAwesomeIcon icon={faWifi} className="mr5 ml5" />
{latency > 2000
? formatMessage(messages.Seconds, {
n: (latency / 1000).toFixed(0),
})
: formatMessage(messages.Milliseconds, {
n: latency.toLocaleString(),
})}
&nbsp;
<FontAwesomeIcon icon={faPlugCircleXmark} className="mr5 ml5" /> {state?.disconnects}
<FontAwesomeIcon icon={faWarning} className="mr5 ml5" />
{state?.pendingRequests?.length}
</div>
<div className="f-grow"></div>
<div>
<span className="icon-btn" onClick={() => navigate(state?.id ?? "")}>
<FontAwesomeIcon icon={faGear} />
<Icon name="gear" size={12} />
</span>
</div>
</div>

View File

@ -3,7 +3,6 @@ import "./Textarea.css";
import { useIntl } from "react-intl";
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
import emoji from "@jukben/emoji-search";
import TextareaAutosize from "react-textarea-autosize";
import { NostrPrefix } from "@snort/nostr";
@ -61,8 +60,10 @@ const Textarea = (props: TextareaProps) => {
return await UserCache.search(token);
};
const emojiDataProvider = (token: string) => {
return emoji(token)
const emojiDataProvider = async (token: string) => {
const emoji = await import("@jukben/emoji-search");
return emoji
.default(token)
.slice(0, 5)
.map(({ name, char }) => ({ name, char }));
};

View File

@ -3,7 +3,7 @@ import { useIntl } from "react-intl";
import { EmailRegex, MnemonicRegex } from "Const";
import { LoginStore } from "Login";
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
import { getNip05PubKey } from "Pages/Login";
import { getNip05PubKey } from "Pages/LoginPage";
import { bech32ToHex } from "Util";
export default function useLoginHandler() {

View File

@ -1,4 +1,5 @@
import { MouseEventHandler } from "react";
import IconsSvg from "public/icons.svg";
type Props = {
name: string;
@ -9,7 +10,7 @@ type Props = {
const Icon = (props: Props) => {
const size = props.size || 20;
const href = "/icons.svg#" + props.name;
const href = `${IconsSvg}#` + props.name;
return (
<svg width={size} height={size} className={props.className} onClick={props.onClick}>

View File

@ -1,22 +1,7 @@
import { type ReactNode } from "react";
import { useEffect, useState, type ReactNode } from "react";
import { IntlProvider as ReactIntlProvider } from "react-intl";
import enMessages from "translations/en.json";
import esMessages from "translations/es_ES.json";
import zhMessages from "translations/zh_CN.json";
import twMessages from "translations/zh_TW.json";
import jaMessages from "translations/ja_JP.json";
import frMessages from "translations/fr_FR.json";
import huMessages from "translations/hu_HU.json";
import idMessages from "translations/id_ID.json";
import arMessages from "translations/ar_SA.json";
import itMessages from "translations/it_IT.json";
import deMessages from "translations/de_DE.json";
import ruMessages from "translations/ru_RU.json";
import svMessages from "translations/sv_SE.json";
import hrMessages from "translations/hr_HR.json";
import taINMessages from "translations/ta_IN.json";
import useLogin from "Hooks/useLogin";
const DefaultLocale = "en-US";
@ -24,65 +9,76 @@ const DefaultLocale = "en-US";
const getMessages = (locale: string) => {
const truncatedLocale = locale.toLowerCase().split(/[_-]+/)[0];
const matchLang = (lng: string) => {
const matchLang = async (lng: string) => {
switch (lng) {
case "es-ES":
case "es":
return esMessages;
return (await import("translations/es_ES.json")).default;
case "zh-CN":
case "zh-Hans-CN":
case "zh":
return zhMessages;
return (await import("translations/zh_CN.json")).default;
case "zh-TW":
return twMessages;
return (await import("translations/zh_TW.json")).default;
case "ja-JP":
case "ja":
return jaMessages;
return (await import("translations/ja_JP.json")).default;
case "fr-FR":
case "fr":
return frMessages;
return (await import("translations/fr_FR.json")).default;
case "hu-HU":
case "hu":
return huMessages;
return (await import("translations/hu_HU.json")).default;
case "id-ID":
case "id":
return idMessages;
return (await import("translations/id_ID.json")).default;
case "ar-SA":
case "ar":
return arMessages;
return (await import("translations/ar_SA.json")).default;
case "it-IT":
case "it":
return itMessages;
return (await import("translations/it_IT.json")).default;
case "de-DE":
case "de":
return deMessages;
return (await import("translations/de_DE.json")).default;
case "ru-RU":
case "ru":
return ruMessages;
return (await import("translations/ru_RU.json")).default;
case "sv-SE":
case "sv":
return svMessages;
return (await import("translations/sv_SE.json")).default;
case "hr-HR":
case "hr":
return hrMessages;
return (await import("translations/hr_HR.json")).default;
case "ta-IN":
case "ta":
return taINMessages;
return (await import("translations/ta_IN.json")).default;
case DefaultLocale:
case "en":
return enMessages;
}
};
return matchLang(locale) ?? matchLang(truncatedLocale) ?? enMessages;
return matchLang(locale) ?? matchLang(truncatedLocale) ?? Promise.resolve(enMessages);
};
export const IntlProvider = ({ children }: { children: ReactNode }) => {
const { language } = useLogin().preferences;
const locale = language ?? getLocale();
const [messages, setMessages] = useState<Record<string, string>>(enMessages);
useEffect(() => {
getMessages(locale)
.then(x => {
if (x) {
setMessages(x);
}
})
.catch(console.error);
}, [language]);
return (
<ReactIntlProvider locale={locale} messages={getMessages(locale)}>
<ReactIntlProvider locale={locale} messages={messages}>
{children}
</ReactIntlProvider>
);

View File

@ -0,0 +1,34 @@
import DnsOverHttpResolver from "dns-over-http-resolver";
import { bech32ToHex } from "Util";
const resolver = new DnsOverHttpResolver();
interface NostrJson {
names: Record<string, string>;
}
export async function fetchNip05Pubkey(name: string, domain: string, timeout = 2_000) {
if (!name || !domain) {
return undefined;
}
try {
const res = await fetch(`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`, {
signal: AbortSignal.timeout(timeout),
});
const data: NostrJson = await res.json();
const match = Object.keys(data.names).find(n => {
return n.toLowerCase() === name.toLowerCase();
});
return match ? data.names[match] : undefined;
} catch {
// ignored
}
// Check as DoH TXT entry
try {
const resDns = await resolver.resolveTxt(`${name}._nostr.${domain}`);
return bech32ToHex(resDns[0][0]);
} catch {
// ignored
}
return undefined;
}

View File

@ -1,4 +1,4 @@
import "./Login.css";
import "./LoginPage.css";
import { CSSProperties, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";

View File

@ -5,7 +5,7 @@ import { useNavigate, useParams } from "react-router-dom";
import Spinner from "Icons/Spinner";
import { parseNostrLink, profileLink } from "Util";
import { getNip05PubKey } from "Pages/Login";
import { getNip05PubKey } from "Pages/LoginPage";
import { System } from "System";
export default function NostrLinkHandler() {

View File

@ -42,7 +42,7 @@ import BadgeList from "Element/BadgeList";
import { ProxyImg } from "Element/ProxyImg";
import useHorizontalScroll from "Hooks/useHorizontalScroll";
import { EmailRegex } from "Const";
import { getNip05PubKey } from "Pages/Login";
import { getNip05PubKey } from "Pages/LoginPage";
import { LNURL } from "LNURL";
import useLogin from "Hooks/useLogin";

View File

@ -3,14 +3,13 @@ import "./WalletPage.css";
import { useEffect, useState } from "react";
import { RouteObject, useNavigate } from "react-router-dom";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheck, faClock, faXmark } from "@fortawesome/free-solid-svg-icons";
import NoteTime from "Element/NoteTime";
import { WalletInvoice, Sats, WalletInfo, WalletInvoiceState, useWallet, LNWallet, Wallets, WalletKind } from "Wallet";
import AsyncButton from "Element/AsyncButton";
import { unwrap } from "Util";
import { WebLNWallet } from "Wallet/WebLN";
import Icon from "Icons/Icon";
export const WalletRoutes: RouteObject[] = [
{
@ -63,11 +62,11 @@ export default function WalletPage() {
function stateIcon(s: WalletInvoiceState) {
switch (s) {
case WalletInvoiceState.Pending:
return <FontAwesomeIcon icon={faClock} className="mr5" />;
return <Icon name="clock" className="mr5" size={15} />;
case WalletInvoiceState.Paid:
return <FontAwesomeIcon icon={faCheck} className="mr5" />;
return <Icon name="check" className="mr5" size={15} />;
case WalletInvoiceState.Expired:
return <FontAwesomeIcon icon={faXmark} className="mr5" />;
return <Icon name="close" className="mr5" size={15} />;
}
}

View File

@ -1,8 +1,7 @@
import "./Preferences.css";
import { FormattedMessage, useIntl } from "react-intl";
import emoji from "@jukben/emoji-search";
import { useEffect, useState } from "react";
import useLogin from "Hooks/useLogin";
import { DefaultPreferences, updatePreferences, UserPreferences } from "Login";
import { DefaultImgProxy } from "Const";
@ -32,6 +31,14 @@ const PreferencesPage = () => {
const { formatMessage } = useIntl();
const login = useLogin();
const perf = login.preferences;
const [emoji, setEmoji] = useState<Array<{ name: string; char: string }>>([]);
useEffect(() => {
(async () => {
const emoji = await import("@jukben/emoji-search");
setEmoji(emoji.default("").map(a => ({ name: a.name, char: a.char })));
})();
}, []);
return (
<div className="preferences">
@ -319,7 +326,7 @@ const PreferencesPage = () => {
<option value="+">
+ <FormattedMessage {...messages.Default} />
</option>
{emoji("").map(({ name, char }) => {
{emoji.map(({ name, char }) => {
return (
<option value={char}>
{name} {char}

View File

@ -3,8 +3,6 @@ import Nostrich from "nostrich.webp";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faShop } from "@fortawesome/free-solid-svg-icons";
import useEventPublisher from "Feed/EventPublisher";
import { useUserProfile } from "Hooks/useUserProfile";
@ -13,9 +11,10 @@ import useFileUpload from "Upload";
import AsyncButton from "Element/AsyncButton";
import { mapEventToProfile, UserCache } from "Cache";
import useLogin from "Hooks/useLogin";
import AvatarEditor from "Element/AvatarEditor";
import Icon from "Icons/Icon";
import messages from "./messages";
import AvatarEditor from "Element/AvatarEditor";
export interface ProfileSettingsProps {
avatar?: boolean;
@ -65,13 +64,14 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
website,
nip05,
lud16,
} as Record<string, string | number | undefined>;
} as Record<string, string | number | undefined | boolean>;
delete userCopy["loaded"];
delete userCopy["created"];
delete userCopy["pubkey"];
delete userCopy["npub"];
delete userCopy["deleted"];
delete userCopy["zapService"];
delete userCopy["isNostrAddressValid"];
console.debug(userCopy);
if (publisher) {
@ -147,7 +147,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
<div>
<input type="text" className="mr10" value={nip05} onChange={e => setNip05(e.target.value)} />
<button type="button" onClick={() => navigate("/verification")}>
<FontAwesomeIcon icon={faShop} />
<Icon name="shopping-bag" />
&nbsp; <FormattedMessage {...messages.Buy} />
</button>
</div>

View File

@ -4,7 +4,6 @@ import { v4 as uuid } from "uuid";
import AsyncButton from "Element/AsyncButton";
import { unwrap } from "Util";
import { CashuWallet } from "Wallet/Cashu";
import { WalletConfig, WalletKind, Wallets } from "Wallet";
import { useNavigate } from "react-router-dom";
@ -19,6 +18,8 @@ const ConnectCashu = () => {
if (!mintUrl) {
throw new Error("Mint URL is required");
}
const { CashuWallet } = await import("Wallet/Cashu");
const connection = new CashuWallet(config);
await connection.login();
const info = await connection.getInfo();

View File

@ -4,8 +4,7 @@ import { useNavigate } from "react-router-dom";
import { v4 as uuid } from "uuid";
import AsyncButton from "Element/AsyncButton";
import { LNCWallet } from "Wallet/LNCWallet";
import { WalletInfo, WalletKind, Wallets } from "Wallet";
import { LNWallet, WalletInfo, WalletKind, Wallets } from "Wallet";
import { unwrap } from "Util";
const ConnectLNC = () => {
@ -13,12 +12,13 @@ const ConnectLNC = () => {
const navigate = useNavigate();
const [pairingPhrase, setPairingPhrase] = useState<string>();
const [error, setError] = useState<string>();
const [connectedLNC, setConnectedLNC] = useState<LNCWallet>();
const [connectedLNC, setConnectedLNC] = useState<LNWallet & { setPassword(pw: string): void }>();
const [walletInfo, setWalletInfo] = useState<WalletInfo>();
const [walletPassword, setWalletPassword] = useState<string>();
async function tryConnect(cfg: string) {
try {
const { LNCWallet } = await import("Wallet/LNCWallet");
const lnc = await LNCWallet.Initialize(cfg);
const info = await lnc.getInfo();

View File

@ -1,4 +1,4 @@
import { LNWallet, Sats, WalletError, WalletErrorCode, WalletInfo, WalletInvoice } from "Wallet";
import { InvoiceRequest, LNWallet, Sats, WalletError, WalletErrorCode, WalletInfo, WalletInvoice } from "Wallet";
import { CashuMint, CashuWallet as TheCashuWallet, Proof } from "@cashu/cashu-ts";

View File

@ -1,4 +1,4 @@
import { Connection, RawEvent } from "@snort/nostr";
import { Connection, EventKind, RawEvent } from "@snort/nostr";
import { EventBuilder } from "System";
import { EventExt } from "System/EventExt";
import { LNWallet, WalletError, WalletErrorCode, WalletInfo, WalletInvoice, WalletInvoiceState } from "Wallet";
@ -156,7 +156,7 @@ export class NostrConnectWallet implements LNWallet {
params,
});
const eb = new EventBuilder();
eb.kind(23194)
eb.kind(23194 as EventKind)
.content(await EventExt.encryptData(payload, this.#config.walletPubkey, this.#config.secret))
.tag(["p", this.#config.walletPubkey]);
@ -166,7 +166,7 @@ export class NostrConnectWallet implements LNWallet {
"REQ",
evCommand.id.slice(0, 12),
{
kinds: [23195],
kinds: [23195 as EventKind],
authors: [this.#config.walletPubkey],
["#e"]: [evCommand.id],
},

View File

@ -1,10 +1,8 @@
import { useSyncExternalStore } from "react";
import { decodeInvoice, unwrap } from "Util";
import { LNCWallet } from "./LNCWallet";
import LNDHubWallet from "./LNDHub";
import { NostrConnectWallet } from "./NostrWalletConnect";
import { CashuWallet } from "./Cashu";
import { setupWebLNWalletConfig, WebLNWallet } from "./WebLN";
export enum WalletKind {
@ -171,7 +169,13 @@ export class WalletStore {
} else {
const w = this.#activateWallet(activeConfig);
if (w) {
this.#instance.set(activeConfig.id, w);
if ("then" in w) {
w.then(wx => {
this.#instance.set(activeConfig.id, wx);
this.snapshotState();
});
return undefined;
}
return w;
} else {
throw new Error("Unable to activate wallet config");
@ -238,11 +242,10 @@ export class WalletStore {
}
}
#activateWallet(cfg: WalletConfig): LNWallet | undefined {
#activateWallet(cfg: WalletConfig): LNWallet | Promise<LNWallet> | undefined {
switch (cfg.kind) {
case WalletKind.LNC: {
const w = LNCWallet.Empty();
return w;
return import("./LNCWallet").then(({ LNCWallet }) => LNCWallet.Empty());
}
case WalletKind.WebLN: {
return new WebLNWallet();
@ -254,7 +257,7 @@ export class WalletStore {
return new NostrConnectWallet(unwrap(cfg.data));
}
case WalletKind.Cashu: {
return new CashuWallet(unwrap(cfg.data));
return import("./Cashu").then(({ CashuWallet }) => new CashuWallet(unwrap(cfg.data)));
}
}
}

View File

@ -366,11 +366,15 @@ input:disabled {
.w-max {
width: 100%;
width: stretch;
width: -webkit-fill-available;
width: -moz-available;
}
.w-max-w {
max-width: 100%;
max-width: stretch;
max-width: -webkit-fill-available;
max-width: -moz-available;
}
a {

View File

@ -1,8 +1,8 @@
import "./index.css";
import "@szhsin/react-menu/dist/index.css";
import "public/manifest.json";
import { StrictMode } from "react";
import { QueryClient, QueryClientProvider } from "react-query";
import * as ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
@ -12,7 +12,7 @@ import { IntlProvider } from "IntlProvider";
import { unwrap } from "Util";
import Store from "State/Store";
import Layout from "Pages/Layout";
import LoginPage from "Pages/Login";
import LoginPage from "Pages/LoginPage";
import ProfilePage from "Pages/ProfilePage";
import { RootRoutes } from "Pages/Root";
import NotificationsPage from "Pages/Notifications";
@ -32,10 +32,8 @@ import Thread from "Element/Thread";
import { SubscribeRoutes } from "Pages/subscribe";
import ZapPoolPage from "Pages/ZapPool";
/**
* HTTP query provider
*/
const HTTP = new QueryClient();
// @ts-ignore
window.__webpack_nonce__ = "ZmlhdGphZiBzYWlkIHNub3J0LnNvY2lhbCBpcyBwcmV0dHkgZ29vZCwgd2UgbWFkZSBpdCE=";
serviceWorkerRegistration.register();
@ -113,11 +111,9 @@ const root = ReactDOM.createRoot(unwrap(document.getElementById("root")));
root.render(
<StrictMode>
<Provider store={Store}>
<QueryClientProvider client={HTTP}>
<IntlProvider>
<RouterProvider router={router} />
</IntlProvider>
</QueryClientProvider>
<IntlProvider>
<RouterProvider router={router} />
</IntlProvider>
</Provider>
</StrictMode>
);

View File

@ -1,4 +1,6 @@
/* eslint-disable no-restricted-globals */
/// <reference lib="webworker" />
import {} from ".";
declare var self: ServiceWorkerGlobalScope;
import { clientsClaim } from "workbox-core";
import { ExpirationPlugin } from "workbox-expiration";

View File

@ -1,134 +0,0 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://cra.link/PWA
const isLocalhost = Boolean(
window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address.
window.location.hostname === "[::1]" ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
);
export function register(config) {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
"This web app is being served cache-first by a service " +
"worker. To learn more, visit https://cra.link/PWA"
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
"New content is available and will be used when all " +
"tabs for this page are closed. See https://cra.link/PWA."
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log("Content is cached for offline use.");
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error("Error during service worker registration:", error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { "Service-Worker": "script" },
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get("content-type");
if (response.status === 404 || (contentType != null && contentType.indexOf("javascript") === -1)) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log("No internet connection found. App is running in offline mode.");
});
}
export function unregister() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
})
.catch(error => {
console.error(error.message);
});
}
}

View File

@ -0,0 +1,37 @@
export function register() {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
window.addEventListener("load", () => {
registerValidSW("/service-worker.js");
});
}
}
async function registerValidSW(swUrl: string) {
try {
const registration = await navigator.serviceWorker.register(swUrl);
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
console.log("Service worker updated, pending reload");
} else {
console.log("Content is cached for offline use.");
}
}
};
};
} catch (e) {
console.error("Error during service worker registration:", e);
}
}
export async function unregister() {
if ("serviceWorker" in navigator) {
const registration = await navigator.serviceWorker.ready;
await registration.unregister();
}
}

View File

@ -2,6 +2,7 @@
"compilerOptions": {
"baseUrl": "src",
"target": "es2020",
"module": "es2020",
"jsx": "react-jsx",
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,

View File

@ -0,0 +1,138 @@
// Generated using webpack-cli https://github.com/webpack/webpack-cli
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const ESLintPlugin = require("eslint-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
const TsTransformer = require("@formatjs/ts-transformer");
const isProduction = process.env.NODE_ENV == "production";
const config = {
entry: {
main: "./src/index.tsx",
},
target: "browserslist",
devtool: isProduction ? "source-map" : "eval",
output: {
publicPath: "/",
path: path.resolve(__dirname, "build"),
filename: ({ runtime }) => {
if (runtime === "sw") {
return "[name].js";
}
return isProduction ? "[name].[chunkhash].js" : "[name].js";
},
clean: isProduction,
},
devServer: {
open: true,
host: "localhost",
historyApiFallback: true,
},
plugins: [
new CopyPlugin({
patterns: [
{ from: "public/manifest.json" },
{ from: "public/robots.txt" },
{ from: "public/nostrich_512.png" },
{ from: "public/nostrich_256.png" },
],
}),
new HtmlWebpackPlugin({
template: "public/index.html",
favicon: "public/favicon.ico",
excludeChunks: ["sw"],
}),
new ESLintPlugin(),
new MiniCssExtractPlugin({
filename: isProduction ? "[name].[chunkhash].css" : "[name].css",
}),
],
module: {
rules: [
{
test: /\.(ts|tsx)$/i,
use: [
"babel-loader",
{
loader: "ts-loader",
options: {
getCustomTransformers() {
return {
before: [
TsTransformer.transform({
overrideIdFn: "[sha512:contenthash:base64:6]",
}),
],
};
},
},
},
],
exclude: ["/node_modules/"],
},
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
{
test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif|webp)$/i,
type: "asset",
},
],
},
optimization: {
usedExports: true,
chunkIds: "deterministic",
minimize: isProduction,
minimizer: [
"...",
// same as https://github.com/facebook/create-react-app/blob/main/packages/react-scripts/config/webpack.config.js
new TerserPlugin({
terserOptions: {
parse: {
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
comparisons: false,
inline: 2,
},
mangle: {
safari10: true,
},
keep_classnames: isProduction,
keep_fnames: isProduction,
output: {
ecma: 5,
comments: false,
ascii_only: true,
},
},
}),
new CssMinimizerPlugin(),
],
},
resolve: {
extensions: [".tsx", ".ts", ".jsx", ".js", "..."],
modules: ["node_modules", __dirname, path.resolve(__dirname, "src")],
},
};
module.exports = () => {
if (isProduction) {
config.mode = "production";
config.entry.sw = {
import: "./src/service-worker.ts",
filename: "service-worker.js",
};
} else {
config.mode = "development";
}
return config;
};

View File

@ -4,8 +4,8 @@
"main": "dist/lib.js",
"types": "dist/src/index.d.ts",
"scripts": {
"build": "rm -rf dist && webpack",
"watch": "rm -rf dist && webpack -w",
"build": "webpack --node-env=production",
"watch": "webpack -w",
"clean": "rm -rf dist",
"test": "ts-mocha --type-check -j 1 --timeout 5s test/test.*.ts",
"test-browser": "ts-node test/browser/server.ts",

View File

@ -1,19 +1,23 @@
const fs = require("fs")
const isProduction = process.env.NODE_ENV == "production";
const entry = {
lib: "./src/index.ts",
}
for (const file of fs.readdirSync("./test/")) {
if (/.ts$/.test(file)) {
const name = file.replace(/.ts$/, "")
entry[`test/${name}`] = `./test/${file}`
if (!isProduction) {
for (const file of fs.readdirSync("./test/")) {
if (/.ts$/.test(file)) {
const name = file.replace(/.ts$/, "")
entry[`test/${name}`] = `./test/${file}`
}
}
}
module.exports = {
mode: process.env.NODE_ENV || "development",
devtool: "inline-source-map",
target: "browserslist",
devtool: isProduction ? "source-map" : "eval",
entry,
resolve: {
extensions: [".ts", ".js"],
@ -27,9 +31,13 @@ module.exports = {
output: {
filename: "[name].js",
path: `${__dirname}/dist`,
clean: true,
library: {
type: "umd",
name: "Nostr",
},
},
optimization: {
usedExports: true,
},
}

7090
yarn.lock

File diff suppressed because it is too large Load Diff