diff --git a/_headers b/_headers
new file mode 100644
index 00000000..aa6bc2c8
--- /dev/null
+++ b/_headers
@@ -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;
\ No newline at end of file
diff --git a/packages/app/.babelrc b/packages/app/.babelrc
deleted file mode 100644
index a05f7d9c..00000000
--- a/packages/app/.babelrc
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "plugins": [["formatjs"]]
-}
diff --git a/packages/app/.eslintrc.cjs b/packages/app/.eslintrc.cjs
index 3bdddba6..01143548 100644
--- a/packages/app/.eslintrc.cjs
+++ b/packages/app/.eslintrc.cjs
@@ -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,
},
};
diff --git a/packages/app/babel.config.json b/packages/app/babel.config.json
new file mode 100644
index 00000000..37fbacf0
--- /dev/null
+++ b/packages/app/babel.config.json
@@ -0,0 +1,11 @@
+{
+ "presets": [
+ "@babel/preset-env",
+ "@babel/preset-react"
+ ],
+ "plugins": [
+ [
+ "formatjs"
+ ]
+ ]
+}
\ No newline at end of file
diff --git a/packages/app/config-overrides.js b/packages/app/config-overrides.js
deleted file mode 100644
index 0cc66159..00000000
--- a/packages/app/config-overrides.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/* eslint-disable @typescript-eslint/no-var-requires */
-const { useBabelRc, override } = require("customize-cra");
-
-module.exports = override(useBabelRc());
diff --git a/packages/app/d.ts b/packages/app/custom.d.ts
similarity index 100%
rename from packages/app/d.ts
rename to packages/app/custom.d.ts
diff --git a/packages/app/package.json b/packages/app/package.json
index 0b1d7307..305768eb 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -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"
diff --git a/packages/app/public/icons.svg b/packages/app/public/icons.svg
index 806a5df3..bd320118 100644
--- a/packages/app/public/icons.svg
+++ b/packages/app/public/icons.svg
@@ -157,6 +157,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/app/public/index.html b/packages/app/public/index.html
index 14de6246..5af92c66 100644
--- a/packages/app/public/index.html
+++ b/packages/app/public/index.html
@@ -2,24 +2,19 @@
-
-
-
+
+
-
-
- snort.social - Nostr interface
+
+
+ Snort - Nostr
-
-
diff --git a/packages/app/src/Cache/UserCache.ts b/packages/app/src/Cache/UserCache.ts
index 79bf2b4b..ab6feade 100644
--- a/packages/app/src/Cache/UserCache.ts
+++ b/packages/app/src/Cache/UserCache.ts
@@ -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 {
#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 {
});
}
}
+ if (m.nip05) {
+ this.#nip5Queue.push({
+ pubkey: m.pubkey,
+ nip05: m.nip05,
+ });
+ }
}
return updateType;
}
@@ -98,27 +107,68 @@ class UserProfileCache extends FeedCache {
}
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(queue: Array, proc: (v: T) => Promise, 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();
diff --git a/packages/app/src/Cache/index.ts b/packages/app/src/Cache/index.ts
index e194af9b..b8ac6bc4 100644
--- a/packages/app/src/Cache/index.ts
+++ b/packages/app/src/Cache/index.ts
@@ -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) {
diff --git a/packages/app/src/Element/CashuNuts.tsx b/packages/app/src/Element/CashuNuts.tsx
index 0f2a6130..bf1a2a9d 100644
--- a/packages/app/src/Element/CashuNuts.tsx
+++ b/packages/app/src/Element/CashuNuts.tsx
@@ -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();
+ 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
}
diff --git a/packages/app/src/Element/Nip05.tsx b/packages/app/src/Element/Nip05.tsx
index 0583837a..4d1fb286 100644
--- a/packages/app/src/Element/Nip05.tsx
+++ b/packages/app/src/Element/Nip05.tsx
@@ -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;
-}
-
-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 (
-
+
{!isDefaultUser && isVerified &&
{`${name}@`}}
{isVerified && (
<>
diff --git a/packages/app/src/Element/Note.css b/packages/app/src/Element/Note.css
index 28806a27..b20a6ece 100644
--- a/packages/app/src/Element/Note.css
+++ b/packages/app/src/Element/Note.css
@@ -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;
}
diff --git a/packages/app/src/Element/NoteCreator.css b/packages/app/src/Element/NoteCreator.css
index b19f6d48..45605d6d 100644
--- a/packages/app/src/Element/NoteCreator.css
+++ b/packages/app/src/Element/NoteCreator.css
@@ -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 {
diff --git a/packages/app/src/Element/NoteToSelf.css b/packages/app/src/Element/NoteToSelf.css
index dab13721..d18040c7 100644
--- a/packages/app/src/Element/NoteToSelf.css
+++ b/packages/app/src/Element/NoteToSelf.css
@@ -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;
}
diff --git a/packages/app/src/Element/NoteToSelf.tsx b/packages/app/src/Element/NoteToSelf.tsx
index 3a0133ba..348372dd 100644
--- a/packages/app/src/Element/NoteToSelf.tsx
+++ b/packages/app/src/Element/NoteToSelf.tsx
@@ -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 (
-
+
);
}
@@ -35,7 +34,7 @@ export default function NoteToSelf({ pubkey, clickable, className, link }: NoteT
diff --git a/packages/app/src/Element/ProfileImage.css b/packages/app/src/Element/ProfileImage.css
index 5a586fe6..8f346215 100644
--- a/packages/app/src/Element/ProfileImage.css
+++ b/packages/app/src/Element/ProfileImage.css
@@ -31,6 +31,8 @@ a.pfp {
.pfp .profile-name {
max-width: stretch;
+ max-width: -webkit-fill-available;
+ max-width: -moz-available;
}
.pfp a {
diff --git a/packages/app/src/Element/Relay.tsx b/packages/app/src/Element/Relay.tsx
index 989f22cd..5d9e6afc 100644
--- a/packages/app/src/Element/Relay.tsx
+++ b/packages/app/src/Element/Relay.tsx
@@ -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) {
<>
-
+
@@ -65,7 +55,7 @@ export default function Relay(props: RelayProps) {
read: relaySettings.read,
})
}>
-
+
@@ -78,28 +68,15 @@ export default function Relay(props: RelayProps) {
read: !relaySettings.read,
})
}>
-
+
-
-
- {latency > 2000
- ? formatMessage(messages.Seconds, {
- n: (latency / 1000).toFixed(0),
- })
- : formatMessage(messages.Milliseconds, {
- n: latency.toLocaleString(),
- })}
-
- {state?.disconnects}
-
- {state?.pendingRequests?.length}
-
+
navigate(state?.id ?? "")}>
-
+
diff --git a/packages/app/src/Element/Textarea.tsx b/packages/app/src/Element/Textarea.tsx
index edd8d7e5..fff173bb 100644
--- a/packages/app/src/Element/Textarea.tsx
+++ b/packages/app/src/Element/Textarea.tsx
@@ -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 }));
};
diff --git a/packages/app/src/Hooks/useLoginHandler.tsx b/packages/app/src/Hooks/useLoginHandler.tsx
index 156844df..94865c5c 100644
--- a/packages/app/src/Hooks/useLoginHandler.tsx
+++ b/packages/app/src/Hooks/useLoginHandler.tsx
@@ -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() {
diff --git a/packages/app/src/Icons/Icon.tsx b/packages/app/src/Icons/Icon.tsx
index 6d9c6400..677a6306 100644
--- a/packages/app/src/Icons/Icon.tsx
+++ b/packages/app/src/Icons/Icon.tsx
@@ -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 (