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 ( diff --git a/packages/app/src/IntlProvider.tsx b/packages/app/src/IntlProvider.tsx index a3889c57..52edda61 100644 --- a/packages/app/src/IntlProvider.tsx +++ b/packages/app/src/IntlProvider.tsx @@ -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>(enMessages); + + useEffect(() => { + getMessages(locale) + .then(x => { + if (x) { + setMessages(x); + } + }) + .catch(console.error); + }, [language]); return ( - + {children} ); diff --git a/packages/app/src/Nip05/Verifier.ts b/packages/app/src/Nip05/Verifier.ts new file mode 100644 index 00000000..5d02204f --- /dev/null +++ b/packages/app/src/Nip05/Verifier.ts @@ -0,0 +1,34 @@ +import DnsOverHttpResolver from "dns-over-http-resolver"; +import { bech32ToHex } from "Util"; + +const resolver = new DnsOverHttpResolver(); +interface NostrJson { + names: Record; +} + +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; +} diff --git a/packages/app/src/Pages/Login.css b/packages/app/src/Pages/LoginPage.css similarity index 100% rename from packages/app/src/Pages/Login.css rename to packages/app/src/Pages/LoginPage.css diff --git a/packages/app/src/Pages/Login.tsx b/packages/app/src/Pages/LoginPage.tsx similarity index 99% rename from packages/app/src/Pages/Login.tsx rename to packages/app/src/Pages/LoginPage.tsx index c1e2282e..89165021 100644 --- a/packages/app/src/Pages/Login.tsx +++ b/packages/app/src/Pages/LoginPage.tsx @@ -1,4 +1,4 @@ -import "./Login.css"; +import "./LoginPage.css"; import { CSSProperties, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; diff --git a/packages/app/src/Pages/NostrLinkHandler.tsx b/packages/app/src/Pages/NostrLinkHandler.tsx index 8cb2223f..195f0867 100644 --- a/packages/app/src/Pages/NostrLinkHandler.tsx +++ b/packages/app/src/Pages/NostrLinkHandler.tsx @@ -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() { diff --git a/packages/app/src/Pages/ProfilePage.tsx b/packages/app/src/Pages/ProfilePage.tsx index 5ed1c59f..1db6305c 100644 --- a/packages/app/src/Pages/ProfilePage.tsx +++ b/packages/app/src/Pages/ProfilePage.tsx @@ -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"; diff --git a/packages/app/src/Pages/WalletPage.tsx b/packages/app/src/Pages/WalletPage.tsx index 56701d8c..17167562 100644 --- a/packages/app/src/Pages/WalletPage.tsx +++ b/packages/app/src/Pages/WalletPage.tsx @@ -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 ; + return ; case WalletInvoiceState.Paid: - return ; + return ; case WalletInvoiceState.Expired: - return ; + return ; } } diff --git a/packages/app/src/Pages/settings/Preferences.tsx b/packages/app/src/Pages/settings/Preferences.tsx index 47877023..9c255de0 100644 --- a/packages/app/src/Pages/settings/Preferences.tsx +++ b/packages/app/src/Pages/settings/Preferences.tsx @@ -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>([]); + + useEffect(() => { + (async () => { + const emoji = await import("@jukben/emoji-search"); + setEmoji(emoji.default("").map(a => ({ name: a.name, char: a.char }))); + })(); + }, []); return (
@@ -319,7 +326,7 @@ const PreferencesPage = () => { - {emoji("").map(({ name, char }) => { + {emoji.map(({ name, char }) => { return (