Merge pull request 'Eject CRA' (#565) from eject into main
Reviewed-on: #565
This commit is contained in:
@ -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();
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 && (
|
||||
<>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -31,6 +31,8 @@ a.pfp {
|
||||
|
||||
.pfp .profile-name {
|
||||
max-width: stretch;
|
||||
max-width: -webkit-fill-available;
|
||||
max-width: -moz-available;
|
||||
}
|
||||
|
||||
.pfp a {
|
||||
|
@ -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(),
|
||||
})}
|
||||
|
||||
<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>
|
||||
|
@ -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 }));
|
||||
};
|
||||
|
@ -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() {
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
);
|
||||
|
34
packages/app/src/Nip05/Verifier.ts
Normal file
34
packages/app/src/Nip05/Verifier.ts
Normal 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;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import "./Login.css";
|
||||
import "./LoginPage.css";
|
||||
|
||||
import { CSSProperties, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
@ -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() {
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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" />
|
||||
<FormattedMessage {...messages.Buy} />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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],
|
||||
},
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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";
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
37
packages/app/src/serviceWorkerRegistration.ts
Normal file
37
packages/app/src/serviceWorkerRegistration.ts
Normal 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();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user