feat: sign up flow v2

This commit is contained in:
Kieran 2023-11-08 14:47:35 +00:00
parent a2bcb936ef
commit d119a5f626
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
27 changed files with 815 additions and 1379 deletions

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { ReactNode, useEffect, useState } from "react";
import { HexKey } from "@snort/system";
import FollowListBase from "Element/User/FollowListBase";
@ -6,7 +6,7 @@ import PageSpinner from "Element/PageSpinner";
import NostrBandApi from "External/NostrBand";
import { ErrorOrOffline } from "./ErrorOrOffline";
export default function TrendingUsers() {
export default function TrendingUsers({ title }: { title?: ReactNode }) {
const [userList, setUserList] = useState<HexKey[]>();
const [error, setError] = useState<Error>();
@ -28,5 +28,5 @@ export default function TrendingUsers() {
if (error) return <ErrorOrOffline error={error} onRetry={loadTrendingUsers} className="p" />;
if (!userList) return <PageSpinner />;
return <FollowListBase pubkeys={userList} showAbout={true} />;
return <FollowListBase pubkeys={userList} showAbout={true} title={title} />;
}

View File

@ -45,19 +45,21 @@ export default function FollowListBase({
}
return (
<div className={className}>
<div className="flex flex-col g8">
{(showFollowAll ?? true) && (
<div className="flex mt10 mb10">
<div className="grow bold">{title}</div>
<div className="flex items-center">
<div className="grow font-bold">{title}</div>
{actions}
<AsyncButton className="transparent" type="button" onClick={() => followAll()} disabled={login.readonly}>
<FormattedMessage {...messages.FollowAll} />
</AsyncButton>
</div>
)}
{pubkeys?.map(a => (
<ProfilePreview pubkey={a} key={a} options={{ about: showAbout }} actions={profileActions?.(a)} />
))}
<div className={className}>
{pubkeys?.map(a => (
<ProfilePreview pubkey={a} key={a} options={{ about: showAbout }} actions={profileActions?.(a)} />
))}
</div>
</div>
);
}

View File

@ -49,7 +49,7 @@ export default function ProfilePreview(props: ProfilePreviewProps) {
showProfileCard={options.profileCards}
/>
{props.actions ?? (
<div className="follow-button-container">
<div className="whitespace-nowrap">
<FollowButton pubkey={pubkey} />
</div>
)}

View File

@ -1,8 +1,9 @@
import { useEffect, useState, type ReactNode } from "react";
import { useEffect, useState, type ReactNode, useSyncExternalStore } from "react";
import { IntlProvider as ReactIntlProvider } from "react-intl";
import enMessages from "translations/en.json";
import useLogin from "Hooks/useLogin";
import { ExternalStore } from "@snort/shared";
const DefaultLocale = "en-US";
@ -80,9 +81,35 @@ const getMessages = (locale: string) => {
return matchLang(locale) ?? matchLang(truncatedLocale) ?? Promise.resolve(enMessages);
};
export const IntlProvider = ({ children }: { children: ReactNode }) => {
class LangStore extends ExternalStore<string> {
setLang(s: string) {
localStorage.setItem("lang", s);
this.notifyChange();
}
takeSnapshot(): string {
return localStorage.getItem("lang") ?? DefaultLocale;
}
}
const LangOverride = new LangStore();
export function useLocale() {
const { language } = useLogin(s => ({ language: s.preferences.language }));
const locale = language ?? getLocale();
const loggedOutLang = useSyncExternalStore(
c => LangOverride.hook(c),
() => LangOverride.snapshot(),
);
const locale = language ?? loggedOutLang ?? getLocale();
return {
locale,
lang: locale.toLowerCase().split(/[_-]+/)[0],
setOverride: (s: string) => LangOverride.setLang(s),
};
}
export const IntlProvider = ({ children }: { children: ReactNode }) => {
const { locale } = useLocale();
const [messages, setMessages] = useState<Record<string, string>>(enMessages);
useEffect(() => {
@ -93,7 +120,7 @@ export const IntlProvider = ({ children }: { children: ReactNode }) => {
}
})
.catch(console.error);
}, [language]);
}, [locale]);
return (
<ReactIntlProvider locale={locale} messages={messages}>

View File

@ -6,6 +6,7 @@ import {
PrivateKeySigner,
KeyStorage,
SystemInterface,
UserMetadata,
} from "@snort/system";
import { unixNowMs } from "@snort/shared";
import * as secp from "@noble/curves/secp256k1";
@ -79,7 +80,11 @@ export function clearEntropy(state: LoginSession) {
/**
* Generate a new key and login with this generated key
*/
export async function generateNewLogin(system: SystemInterface, pin: (key: string) => Promise<KeyStorage>) {
export async function generateNewLogin(
system: SystemInterface,
pin: (key: string) => Promise<KeyStorage>,
profile: UserMetadata,
) {
const ent = generateBip39Entropy();
const entropy = utils.bytesToHex(ent);
const privateKey = entropyToPrivateKey(ent);
@ -99,6 +104,10 @@ export async function generateNewLogin(system: SystemInterface, pin: (key: strin
const ev2 = await publisher.relayList(newRelays);
await system.BroadcastEvent(ev2);
// Publish new profile
const ev3 = await publisher.metadata(profile);
await system.BroadcastEvent(ev3);
LoginStore.loginWithPrivateKey(await pin(privateKey), entropy, newRelays);
}

View File

@ -1,388 +0,0 @@
import "./LoginPage.css";
import { CSSProperties, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useIntl, FormattedMessage } from "react-intl";
import { HexKey, Nip46Signer, NotEncrypted, PinEncrypted, PrivateKeySigner } from "@snort/system";
import { bech32ToHex, getPublicKey, unwrap } from "SnortUtils";
import ZapButton from "Element/Event/ZapButton";
import useImgProxy from "Hooks/useImgProxy";
import Icon from "Icons/Icon";
import { generateNewLogin, LoginSessionType, LoginStore } from "Login";
import AsyncButton from "Element/AsyncButton";
import useLoginHandler from "Hooks/useLoginHandler";
import { secp256k1 } from "@noble/curves/secp256k1";
import { bytesToHex } from "@noble/curves/abstract/utils";
import Modal from "Element/Modal";
import QrCode from "Element/QrCode";
import Copy from "Element/Copy";
import { delay } from "SnortUtils";
import { PinPrompt } from "Element/PinPrompt";
import useEventPublisher from "Hooks/useEventPublisher";
import { isHex } from "@snort/shared";
declare global {
interface Window {
plausible?: (tag: string) => void;
}
}
interface ArtworkEntry {
name: string;
pubkey: HexKey;
link: string;
}
const KarnageKey = bech32ToHex("npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac");
// todo: fill more
const Artwork: Array<ArtworkEntry> = [
{
name: "",
pubkey: KarnageKey,
link: "https://void.cat/d/VKhPayp9ekeXYZGzAL9CxP",
},
{
name: "",
pubkey: KarnageKey,
link: "https://void.cat/d/3H2h8xxc3aEN6EVeobd8tw",
},
{
name: "",
pubkey: KarnageKey,
link: "https://void.cat/d/7i9W9PXn3TV86C4RUefNC9",
},
{
name: "",
pubkey: KarnageKey,
link: "https://void.cat/d/KtoX4ei6RYHY7HESg3Ve3k",
},
];
export default function LoginPage() {
const navigate = useNavigate();
const [key, setKey] = useState("");
const [nip46Key, setNip46Key] = useState("");
const [error, setError] = useState("");
const [pin, setPin] = useState(false);
const [art, setArt] = useState<ArtworkEntry>();
const [isMasking, setMasking] = useState(true);
const { formatMessage } = useIntl();
const { proxy } = useImgProxy();
const loginHandler = useLoginHandler();
const hasNip7 = "nostr" in window;
const { system } = useEventPublisher();
const hasSubtleCrypto = window.crypto.subtle !== undefined;
const [nostrConnect, setNostrConnect] = useState("");
useEffect(() => {
const ret = unwrap(Artwork.at(Artwork.length * Math.random()));
const url = proxy(ret.link);
setArt({ ...ret, link: url });
}, []);
async function makeKeyStore(key: string, pin?: string) {
if (pin) {
return await PinEncrypted.create(key, pin);
} else {
return new NotEncrypted(key);
}
}
async function doLogin(pin?: string) {
setError("");
try {
await loginHandler.doLogin(key, key => makeKeyStore(key, pin));
navigate("/");
} catch (e) {
if (e instanceof Error) {
setError(e.message);
} else {
setError(
formatMessage({
defaultMessage: "Unknown login error",
}),
);
}
console.error(e);
}
}
async function makeRandomKey(pin?: string) {
try {
await generateNewLogin(system, key => makeKeyStore(key, pin));
window.plausible?.("Generate Account");
navigate("/new");
} catch (e) {
if (e instanceof Error) {
setError(e.message);
}
}
}
async function doNip07Login() {
const relays =
"getRelays" in unwrap(window.nostr) ? await unwrap(window.nostr?.getRelays).call(window.nostr) : undefined;
const pubKey = await unwrap(window.nostr).getPublicKey();
LoginStore.loginWithPubkey(pubKey, LoginSessionType.Nip7, relays);
navigate("/");
}
function generateNip46() {
const meta = {
name: CONFIG.appNameCapitalized,
url: window.location.href,
};
const newKey = bytesToHex(secp256k1.utils.randomPrivateKey());
const relays = ["wss://relay.damus.io"].map(a => `relay=${encodeURIComponent(a)}`);
const connectUrl = `nostrconnect://${getPublicKey(newKey)}?${[
...relays,
`metadata=${encodeURIComponent(JSON.stringify(meta))}`,
].join("&")}`;
setNostrConnect(connectUrl);
setNip46Key(newKey);
}
async function startNip46(pin?: string) {
if (!nostrConnect || !nip46Key) return;
const signer = new Nip46Signer(nostrConnect, new PrivateKeySigner(nip46Key));
await signer.init();
await delay(500);
await signer.describe();
LoginStore.loginWithPubkey(
await signer.getPubKey(),
LoginSessionType.Nip46,
undefined,
["wss://relay.damus.io"],
await makeKeyStore(nip46Key, pin),
);
navigate("/");
}
function nip46Buttons() {
return (
<>
<AsyncButton
type="button"
onClick={() => {
generateNip46();
setPin(true);
}}>
<FormattedMessage defaultMessage="Nostr Connect" description="Login button for NIP-46 signer app" />
</AsyncButton>
{nostrConnect && !pin && (
<Modal id="nostr-connect" onClose={() => setNostrConnect("")}>
<>
<h2>
<FormattedMessage defaultMessage="Nostr Connect" />
</h2>
<p>
<FormattedMessage defaultMessage="Scan this QR code with your signer app to get started" />
</p>
<div className="flex flex-col items-center g12">
<QrCode data={nostrConnect} />
<Copy text={nostrConnect} />
</div>
</>
</Modal>
)}
</>
);
}
function altLogins() {
if (!hasNip7) {
return;
}
return (
<>
<AsyncButton type="button" onClick={doNip07Login}>
<FormattedMessage
defaultMessage="Nostr Extension"
description="Login button for NIP7 key manager extension"
/>
</AsyncButton>
{nip46Buttons()}
</>
);
}
function installExtension() {
if (hasSubtleCrypto) return;
return (
<>
<div className="flex login-or">
<FormattedMessage defaultMessage="OR" description="Seperator text for Login / Generate Key" />
<div className="divider w-max"></div>
</div>
<h1 dir="auto">
<FormattedMessage
defaultMessage="Install Extension"
description="Heading for install key manager extension"
/>
</h1>
<p>
<FormattedMessage defaultMessage="Key manager extensions are more secure and allow you to easily login to any Nostr client, here are some well known extensions:" />
</p>
<ul>
<li>
<a href="https://getalby.com/" target="_blank" rel="noreferrer">
Alby
</a>
</li>
<li>
<a
href="https://chrome.google.com/webstore/detail/nos2x/kpgefcfmnafjgpblomihpgmejjdanjjp"
target="_blank"
rel="noreferrer">
nos2x
</a>
</li>
</ul>
<p>
<FormattedMessage
defaultMessage="If you want to try out some others, check out {link} for more!"
values={{
link: <a href="https://github.com/aljazceru/awesome-nostr#browser-extensions">awesome-nostr</a>,
}}
/>
</p>
<p>
<FormattedMessage defaultMessage="Once you setup your key manager extension and generated a key, you can follow our new users flow to setup your profile and help you find some interesting people on Nostr to follow." />
</p>
{hasNip7 ? (
<div className="login-actions">
<button type="button" onClick={() => doNip07Login().then(() => navigate("/new/username"))}>
<FormattedMessage defaultMessage="Setup Profile" />
</button>
</div>
) : (
<b className="error">
<FormattedMessage defaultMessage="Hmm, can't find a key manager extension.. try reloading the page." />
</b>
)}
</>
);
}
return (
<div className="login">
<div>
<div className="login-container">
<h1 className="logo" onClick={() => navigate("/")}>
{CONFIG.appName}
</h1>
<h1 dir="auto">
<FormattedMessage defaultMessage="Login" description="Login header" />
</h1>
<p dir="auto">
<FormattedMessage defaultMessage="Your key" description="Label for key input" />
</p>
<div className="flex items-center g8">
<input
dir="auto"
type={isMasking ? "password" : "text"}
placeholder={formatMessage({
defaultMessage: "nsec, npub, nip-05, hex, mnemonic",
})}
className="grow"
onChange={e => setKey(e.target.value)}
/>
<Icon
name={isMasking ? "openeye" : "closedeye"}
size={30}
className="highlight pointer"
onClick={() => setMasking(!isMasking)}
/>
</div>
{error.length > 0 ? <b className="error">{error}</b> : null}
<p>
<FormattedMessage
defaultMessage="Only the secret key can be used to publish (sign events), everything else logs you in read-only mode."
description="Explanation for public key only login is read-only"
/>
</p>
<div dir="auto" className="login-actions">
<AsyncButton
type="button"
onClick={async () => {
if (key.startsWith("nsec") || (key.length === 64 && isHex(key))) {
setPin(true);
} else {
await doLogin();
}
}}>
<FormattedMessage defaultMessage="Login" description="Login button" />
</AsyncButton>
<AsyncButton onClick={() => setPin(true)}>
<FormattedMessage defaultMessage="Create Account" />
</AsyncButton>
{pin && (
<PinPrompt
subTitle={
<>
<p>
<FormattedMessage
defaultMessage="Secure your private key with a PIN, ensuring enhanced protection on {site}. You'll be prompted to enter this PIN each time you access the site."
values={{
site: CONFIG.appNameCapitalized,
}}
/>
</p>
<p>
<FormattedMessage defaultMessage="Alternatively, you may choose to store your private key without a PIN by selecting 'Cancel.'" />
</p>
<p>
<FormattedMessage defaultMessage="After submitting the pin there may be a slight delay as we encrypt the key." />
</p>
</>
}
onResult={async pin => {
setPin(false);
if (key) {
await doLogin(pin);
} else if (nostrConnect) {
await startNip46(pin);
} else {
await makeRandomKey(pin);
}
}}
onCancel={async () => {
setPin(false);
if (key) {
await doLogin();
} else if (nostrConnect) {
await startNip46();
} else {
await makeRandomKey();
}
}}
/>
)}
{altLogins()}
</div>
{installExtension()}
</div>
</div>
<div>
<div className="artwork" style={{ ["--img-src"]: `url('${art?.link}')` } as CSSProperties}>
<div className="attribution">
<FormattedMessage
defaultMessage="Art by {name}"
description="Artwork attribution label"
values={{
name: <span className="artist">Karnage</span>,
}}
/>
<ZapButton pubkey={art?.pubkey ?? ""} />
</div>
</div>
</div>
</div>
);
}

View File

@ -1,55 +0,0 @@
import { useIntl, FormattedMessage } from "react-intl";
import { useNavigate, Link } from "react-router-dom";
import { DeveloperAccounts } from "Const";
import Logo from "Element/Logo";
import FollowListBase from "Element/User/FollowListBase";
import { clearEntropy } from "Login";
import useLogin from "Hooks/useLogin";
import TrendingUsers from "Element/TrendingUsers";
import messages from "./messages";
export default function DiscoverFollows() {
const { formatMessage } = useIntl();
const login = useLogin();
const navigate = useNavigate();
async function clearEntropyAndGo() {
clearEntropy(login);
navigate("/");
}
return (
<div className="main-content new-user p" dir="auto">
<Logo />
<div className="progress-bar">
<div className="progress"></div>
</div>
<h1>
<FormattedMessage {...messages.Ready} />
</h1>
<p>
<FormattedMessage {...messages.Share} values={{ link: <Link to="/">{formatMessage(messages.World)}</Link> }} />
</p>
<div className="next-actions continue-actions">
<button type="button" onClick={() => clearEntropyAndGo()}>
<FormattedMessage {...messages.Done} />{" "}
</button>
</div>
<h3>
<FormattedMessage
defaultMessage="{site_name} Developers"
values={{
site_name: CONFIG.appNameCapitalized,
}}
/>
</h3>
{DeveloperAccounts.length > 0 && <FollowListBase pubkeys={DeveloperAccounts} showAbout={true} />}
<h3>
<FormattedMessage defaultMessage="Trending Users" />
</h3>
<TrendingUsers />
</div>
);
}

View File

@ -1,115 +0,0 @@
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { useUserProfile } from "@snort/system-react";
import Logo from "Element/Logo";
import { Nip5Services } from "Pages/NostrAddressPage";
import Nip5Service from "Element/Nip5Service";
import ProfileImage from "Element/User/ProfileImage";
import useLogin from "Hooks/useLogin";
import messages from "./messages";
export default function GetVerified() {
const navigate = useNavigate();
const { publicKey } = useLogin();
const user = useUserProfile(publicKey);
const [isVerified, setIsVerified] = useState(false);
const name = user?.name || "nostrich";
const [nip05, setNip05] = useState(`${name}@snort.social`);
const onNext = async () => {
navigate("/new/import");
};
return (
<div className="main-content new-user" dir="auto">
<Logo />
<div className="progress-bar">
<div className="progress progress-third"></div>
</div>
<h1>
<FormattedMessage {...messages.Identifier} />
</h1>
<div className="next-actions continue-actions">
<button className="secondary" type="button" onClick={onNext}>
<FormattedMessage {...messages.Skip} />
</button>
</div>
<h4>
<FormattedMessage {...messages.PreviewOnSnort} />
</h4>
<div className="profile-preview-nip">
{publicKey && <ProfileImage pubkey={publicKey} defaultNip={nip05} verifyNip={false} />}
</div>
<p>
<FormattedMessage {...messages.IdentifierHelp} />
</p>
<ul>
<li>
<FormattedMessage {...messages.PreventFakes} />
</li>
<li>
<FormattedMessage {...messages.EasierToFind} />
</li>
<li>
<FormattedMessage {...messages.Funding} />
</li>
</ul>
<p className="warning">
<FormattedMessage {...messages.NameSquatting} />
</p>
{!isVerified && (
<>
<h2>
<FormattedMessage {...messages.GetSnortId} />
</h2>
<p>
<FormattedMessage {...messages.GetSnortIdHelp} />
</p>
<div className="nip-container">
<Nip5Service
key="snort"
{...Nip5Services[0]}
helpText={false}
onChange={setNip05}
onSuccess={() => setIsVerified(true)}
/>
</div>
</>
)}
{!isVerified && (
<>
<h2>
<FormattedMessage {...messages.GetPartnerId} />
</h2>
<p>
<FormattedMessage {...messages.GetPartnerIdHelp} />
</p>
<div className="nip-container">
<Nip5Service
key="nostrplebs"
{...Nip5Services[1]}
helpText={false}
onChange={setNip05}
onSuccess={() => setIsVerified(true)}
/>
</div>
</>
)}
<div className="next-actions">
{!isVerified && (
<button type="button" className="transparent" onClick={onNext}>
<FormattedMessage {...messages.Skip} />
</button>
)}
{isVerified && (
<button type="button" onClick={onNext}>
<FormattedMessage {...messages.Next} />
</button>
)}
</div>
</div>
);
}

View File

@ -1,147 +0,0 @@
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import Logo from "Element/Logo";
import { CollapsedSection } from "Element/Collapsed";
import useLogin from "Hooks/useLogin";
import { PROFILE } from ".";
import { DefaultPreferences, LoginStore, updatePreferences } from "Login";
import { AllLanguageCodes } from "Pages/settings/Preferences";
import messages from "./messages";
import ExportKeys from "Pages/settings/Keys";
const WhatIsSnort = () => {
return (
<CollapsedSection
title={
<h3>
<FormattedMessage {...messages.WhatIsSnort} />
</h3>
}>
<p>
<FormattedMessage {...messages.WhatIsSnortIntro} />
</p>
<p>
<FormattedMessage {...messages.WhatIsSnortNotes} />
</p>
<p>
<FormattedMessage {...messages.WhatIsSnortExperience} />
</p>
</CollapsedSection>
);
};
const HowDoKeysWork = () => {
return (
<CollapsedSection
title={
<h3>
<FormattedMessage {...messages.HowKeysWork} />
</h3>
}>
<p>
<FormattedMessage {...messages.DigitalSignatures} />
</p>
<p>
<FormattedMessage {...messages.TamperProof} />
</p>
<p>
<FormattedMessage {...messages.Bitcoin} />
</p>
</CollapsedSection>
);
};
const Extensions = () => {
const { preferences } = useLogin();
return (
<CollapsedSection
title={
<h3>
<FormattedMessage {...messages.ImproveSecurity} />
</h3>
}>
<p>
<FormattedMessage {...messages.Extensions} />
</p>
<ul>
<li>
<a href="https://getalby.com/" target="_blank" rel="noreferrer">
Alby
</a>
{(preferences.language === "ru" || preferences.language === "ru-RU") && (
<a href="https://nostr.21ideas.org/docs/guides/Alby.html" target="_blank" rel="noreferrer">
(Tony's Guide)
</a>
)}
</li>
<li>
<a href="https://github.com/fiatjaf/nos2x" target="_blank" rel="noreferrer">
nos2x
</a>
</li>
</ul>
<p>
<FormattedMessage {...messages.ExtensionsNostr} />
</p>
</CollapsedSection>
);
};
export default function NewUserFlow() {
const login = useLogin();
const navigate = useNavigate();
return (
<div className="main-content new-user p" dir="auto">
<Logo />
<div className="progress-bar">
<div className="progress progress-first"></div>
</div>
<h1>
<FormattedMessage {...messages.SaveKeys} />
</h1>
<div className="flex justify-between">
<FormattedMessage defaultMessage="Language" />
<select
value={login.preferences.language || DefaultPreferences.language}
onChange={e =>
updatePreferences(login, {
...login.preferences,
language: e.target.value,
})
}
style={{ textTransform: "capitalize" }}>
{AllLanguageCodes.sort().map(a => (
<option value={a}>
{new Intl.DisplayNames([a], {
type: "language",
}).of(a)}
</option>
))}
</select>
</div>
<p>
<FormattedMessage {...messages.SaveKeysHelp} />
</p>
<ExportKeys />
<div className="next-actions">
<button
type="button"
onClick={() => {
LoginStore.updateSession({
...login,
generatedEntropy: undefined,
});
navigate(PROFILE);
}}>
<FormattedMessage {...messages.KeysSaved} />{" "}
</button>
</div>
<WhatIsSnort />
<HowDoKeysWork />
<Extensions />
</div>
);
}

View File

@ -1,84 +0,0 @@
import { useEffect, useState } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { mapEventToProfile } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import Logo from "Element/Logo";
import useEventPublisher from "Hooks/useEventPublisher";
import useLogin from "Hooks/useLogin";
import { UserCache } from "Cache";
import AvatarEditor from "Element/User/AvatarEditor";
import { DISCOVER } from ".";
import messages from "./messages";
export default function ProfileSetup() {
const login = useLogin();
const myProfile = useUserProfile(login.publicKey);
const [username, setUsername] = useState("");
const [picture, setPicture] = useState("");
const { formatMessage } = useIntl();
const { publisher, system } = useEventPublisher();
const navigate = useNavigate();
useEffect(() => {
if (myProfile) {
setUsername(myProfile.name ?? "");
setPicture(myProfile.picture ?? "");
}
}, [myProfile]);
const onNext = async () => {
if ((username.length > 0 || picture.length > 0) && publisher) {
const ev = await publisher.metadata({
...myProfile,
name: username,
picture,
});
system.BroadcastEvent(ev);
const profile = mapEventToProfile(ev);
if (profile) {
UserCache.set(profile);
}
}
navigate(DISCOVER);
};
return (
<div className="main-content new-user p" dir="auto">
<Logo />
<div className="progress-bar">
<div className="progress progress-second"></div>
</div>
<h1>
<FormattedMessage defaultMessage="Setup profile" />
</h1>
<h2>
<FormattedMessage defaultMessage="Profile picture" />
</h2>
<AvatarEditor picture={picture} onPictureChange={p => setPicture(p)} />
<h2>
<FormattedMessage defaultMessage="Username" />
</h2>
<input
className="username"
placeholder={formatMessage(messages.UsernamePlaceholder)}
type="text"
value={username}
onChange={ev => setUsername(ev.target.value)}
/>
<div className="help-text">
<FormattedMessage defaultMessage="You can change your username at any point." />
</div>
<div className="next-actions">
<button type="button" className="transparent" onClick={() => navigate(DISCOVER)}>
<FormattedMessage {...messages.Skip} />
</button>
<button type="button" onClick={onNext}>
<FormattedMessage {...messages.Next} />
</button>
</div>
</div>
);
}

View File

@ -1,179 +0,0 @@
.new-user {
color: var(--font-secondary-color);
}
.new-user input {
font-size: 16px;
}
.new-user p {
font-weight: 400;
font-size: 16px;
line-height: 24px;
}
.new-user p > a {
color: var(--highlight);
}
.new-user li {
line-height: 24px;
}
.new-user li > a {
color: var(--highlight);
}
.new-user .nip-handle {
max-width: 120px;
}
.new-user h1 {
color: var(--font-color);
font-weight: 700;
font-size: 32px;
line-height: 39px;
}
.new-user h2 {
margin-top: 24px;
margin-bottom: 16px;
color: var(--font-color);
font-weight: 600;
font-size: 16px;
line-height: 19px;
}
.new-user h3 {
color: var(--font-color);
font-weight: 700;
font-size: 21px;
line-height: 25px;
}
.new-user h4 {
color: var(--font-secondary-color);
font-weight: 600;
font-size: 12px;
line-height: 19px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.progress-bar {
width: 100%;
height: 7px;
background: var(--gray-secondary);
border-radius: 53px;
}
.progress-bar .progress {
height: 7px;
background: var(--snort-gradient);
border-radius: 53px;
}
.progress.progress-first {
width: 20%;
}
.progress.progress-second {
width: 50%;
}
.progress.progress-third {
width: 75%;
}
.progress.progress-last {
width: 95%;
}
.new-user .next-actions {
margin-top: 32px;
margin-bottom: 64px;
width: 100%;
display: flex;
justify-content: flex-end;
}
.new-user .next-actions button:not(:last-child) {
margin-right: 12px;
}
.new-user .next-actions.continue-actions {
margin-bottom: 0;
}
.new-user > .copy {
padding: 12px 16px;
border: 2px dashed #222222;
border-radius: 16px;
}
.light .new-user > .copy {
border: 2px dashed #aaaaaa;
}
.new-user > .copy .body {
font-size: 16px;
}
@media (max-width: 520px) {
.new-user > .copy .body {
font-size: 12px;
}
}
.new-user > .copy .icon {
margin-left: auto;
}
.new-user > .copy .icon svg {
width: 16px;
height: 16px;
}
.new-user input {
width: 100%;
max-width: 568px;
background: #222;
border: none;
}
@media (max-width: 720px) {
.new-user input {
width: calc(100vw - 40px);
}
}
.light .new-user input {
background: none;
}
.new-user .warning {
font-weight: 400;
font-size: 14px;
line-height: 19px;
color: #fc6e1e;
}
.profile-preview-nip {
padding: 12px 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
}
.light .profile-preview-nip {
border: 1px solid rgba(0, 0, 0, 0.1);
}
.new-user .nip-container input[type="text"] {
width: 166px;
}
.new-user .help-text {
margin-top: 6px;
font-weight: 400;
font-size: 14px;
line-height: 24px;
}

View File

@ -1,30 +0,0 @@
import "./index.css";
import { RouteObject } from "react-router-dom";
import GetVerified from "Pages/new/GetVerified";
import ProfileSetup from "Pages/new/ProfileSetup";
import NewUserFlow from "Pages/new/NewUserFlow";
import DiscoverFollows from "Pages/new/DiscoverFollows";
export const PROFILE = "/new/profile";
export const DISCOVER = "/new/discover";
export const VERIFY = "/new/verify";
export const NewUserRoutes: RouteObject[] = [
{
path: "/new",
element: <NewUserFlow />,
},
{
path: PROFILE,
element: <ProfileSetup />,
},
{
path: VERIFY,
element: <GetVerified />,
},
{
path: DISCOVER,
element: <DiscoverFollows />,
},
];

View File

@ -1,86 +0,0 @@
import { defineMessages } from "react-intl";
export default defineMessages({
SaveKeys: {
defaultMessage: "Save your keys!",
},
SaveKeysHelp: {
defaultMessage:
"Your private key is your password. If you lose this key, you will lose access to your account! Copy it and keep it in a safe place. There is no way to reset your private key.",
},
YourPubkey: { defaultMessage: "Your public key" },
YourPrivkey: { defaultMessage: "Your private key" },
YourMnemonic: { defaultMessage: "Your mnemonic phrase" },
KeysSaved: { defaultMessage: "I have saved my keys, continue" },
WhatIsSnort: {
defaultMessage: "What is {site} and how does it work?",
values: { site: CONFIG.appNameCapitalized },
},
WhatIsSnortIntro: {
defaultMessage: `{site} is a Nostr UI, nostr is a decentralised protocol for saving and distributing "notes".`,
values: { site: CONFIG.appNameCapitalized },
},
WhatIsSnortNotes: {
defaultMessage: `Notes hold text content, the most popular usage of these notes is to store "tweet like" messages.`,
},
WhatIsSnortExperience: {
defaultMessage: "{site} is designed to have a similar experience to Twitter.",
values: { site: CONFIG.appNameCapitalized },
},
HowKeysWork: { defaultMessage: "How do keys work?" },
DigitalSignatures: {
defaultMessage: `Nostr uses digital signature technology to provide tamper proof notes which can safely be replicated to many relays to provide redundant storage of your content.`,
},
TamperProof: {
defaultMessage: `This means that nobody can modify notes which you have created and everybody can easily verify that the notes they are reading are created by you.`,
},
Bitcoin: {
defaultMessage: `This is the same technology which is used by Bitcoin and has been proven to be extremely secure.`,
},
Extensions: {
defaultMessage: `It is recommended to use one of the following browser extensions if you are on a desktop computer to secure your key:`,
},
ExtensionsNostr: { defaultMessage: `You can also use these extensions to login to most Nostr sites.` },
ImproveSecurity: { defaultMessage: "Improve login security with browser extensions" },
PickUsername: { defaultMessage: "Pick a username" },
Username: { defaultMessage: "Username" },
UsernamePlaceholder: { defaultMessage: "e.g. Jack" },
PopularAccounts: { defaultMessage: "Follow some popular accounts" },
Skip: { defaultMessage: "Skip" },
Done: { defaultMessage: "Done!" },
ImportTwitter: { defaultMessage: "Import Twitter Follows" },
TwitterPlaceholder: { defaultMessage: "Twitter username..." },
FindYourFollows: { defaultMessage: "Find your twitter follows on nostr (Data provided by {provider})" },
TwitterUsername: { defaultMessage: "Twitter username" },
FollowsOnNostr: { defaultMessage: "{username}'s Follows on Nostr" },
NoUsersFound: { defaultMessage: "No nostr users found for {twitterUsername}" },
FailedToLoad: { defaultMessage: "Failed to load follows, please try again later" },
Check: { defaultMessage: "Check" },
Next: { defaultMessage: "Next" },
SetupProfile: { defaultMessage: "Setup your Profile" },
Identifier: { defaultMessage: "Get an identifier" },
IdentifierHelp: {
defaultMessage:
"Getting an identifier helps confirm the real you to people who know you. Many people can have a username @jack, but there is only one jack@cash.app.",
},
PreventFakes: { defaultMessage: "Prevent fake accounts from imitating you" },
EasierToFind: { defaultMessage: "Make your profile easier to find and share" },
Funding: { defaultMessage: "Fund developers and platforms providing NIP-05 verification services" },
NameSquatting: {
defaultMessage:
"Name-squatting and impersonation is not allowed. {site} and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule.",
values: { site: CONFIG.appNameCapitalized },
},
PreviewOnSnort: { defaultMessage: "Preview on {site}", values: { site: CONFIG.appNameCapitalized } },
GetSnortId: { defaultMessage: "Get a Snort identifier" },
GetSnortIdHelp: {
defaultMessage:
"Only Snort and our integration partner identifier gives you a colorful domain name, but you are welcome to use other services too.",
},
GetPartnerId: { defaultMessage: "Get a partner identifier" },
GetPartnerIdHelp: { defaultMessage: "We have also partnered with nostrplebs.com to give you more options" },
Ready: { defaultMessage: "You're ready!" },
Share: { defaultMessage: "Share your thoughts with {link}" },
World: { defaultMessage: "the world" },
});

View File

@ -0,0 +1,42 @@
import { FormattedMessage } from "react-intl";
import { useLocation, useNavigate } from "react-router-dom";
import AsyncButton from "Element/AsyncButton";
import { NewUserState } from ".";
import TrendingUsers from "Element/TrendingUsers";
export function Discover() {
const location = useLocation();
const navigate = useNavigate();
const state = location.state as NewUserState;
return (
<div className="flex flex-col g24">
<h1 className="text-center">
<FormattedMessage
defaultMessage="{site} is more fun together!"
values={{
site: CONFIG.appNameCapitalized,
}}
/>
</h1>
<div className="new-trending">
<TrendingUsers
title={
<h3>
<FormattedMessage defaultMessage="Trending Users" />
</h3>
}
/>
</div>
<AsyncButton
className="primary"
onClick={() =>
navigate("/login/sign-up/moderation", {
state,
})
}>
<FormattedMessage defaultMessage="Next" />
</AsyncButton>
</div>
);
}

View File

@ -0,0 +1,50 @@
.onboarding-modal {
clear: both;
margin-top: 10vh;
}
@media (min-width: 640px) {
.onboarding-modal {
padding: 40px 48px;
border-radius: 24px;
background-color: var(--gray-superdark);
margin-left: auto;
margin-right: auto;
margin-top: 10vh;
width: 460px;
}
}
.onboarding-modal h1,
.onboarding-modal h2,
.onboarding-modal h3,
.onboarding-modal h4,
.onboarding-modal h5 {
margin: 0;
}
.onboarding-modal button.secondary:hover {
background-color: var(--gray-medium);
}
.onboarding-modal {
--border-color: #3a3a3a;
}
.new-username {
padding: 10px 16px !important;
align-self: stretch;
border-radius: 100px !important;
background-color: var(--gray-superlight) !important;
box-shadow: 0px 0px 0px 4px transparent !important;
color: var(--gray-ultradark) !important;
}
.new-username:focus {
box-shadow: 0px 0px 0px 4px rgba(172, 136, 255, 0.8) !important;
}
.new-trending {
max-height: 30vh;
overflow-y: scroll;
}

View File

@ -0,0 +1,74 @@
import "./index.css";
import { Outlet, RouteObject } from "react-router-dom";
import { SignIn, SignUp } from "./start";
import { AllLanguageCodes } from "Pages/settings/Preferences";
import Icon from "Icons/Icon";
import { Profile } from "./profile";
import { Topics } from "./topics";
import { Discover } from "./discover";
import { useLocale } from "IntlProvider";
import { Moderation } from "./moderation";
export interface NewUserState {
name?: string;
picture?: string;
topics?: Array<string>;
muteLists?: Array<string>;
}
function OnboardingLayout() {
const { lang, setOverride } = useLocale();
return (
<div className="p24">
<div className="float-right flex g8 items-center">
<Icon name="translate" />
<select value={lang} onChange={e => setOverride(e.target.value)} className="capitalize">
{AllLanguageCodes.sort().map(a => (
<option value={a}>
{new Intl.DisplayNames([a], {
type: "language",
}).of(a)}
</option>
))}
</select>
</div>
<div className="onboarding-modal">
<Outlet />
</div>
</div>
);
}
export const OnboardingRoutes = [
{
path: "/login",
element: <OnboardingLayout />,
children: [
{
path: "",
element: <SignIn />,
},
{
path: "sign-up",
element: <SignUp />,
},
{
path: "sign-up/profile",
element: <Profile />,
},
{
path: "sign-up/topics",
element: <Topics />,
},
{
path: "sign-up/discover",
element: <Discover />,
},
{
path: "sign-up/moderation",
element: <Moderation />,
},
],
},
] as Array<RouteObject>;

View File

@ -0,0 +1,156 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { ReactNode, useState } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import AsyncButton from "Element/AsyncButton";
import classNames from "classnames";
import { appendDedupe } from "SnortUtils";
import useEventPublisher from "Hooks/useEventPublisher";
import { setMuted } from "Login";
import { ToggleSwitch } from "Icons/Toggle";
export const FixedModeration = {
hateSpeech: {
title: <FormattedMessage defaultMessage="Hate Speech" />,
words: [],
canEdit: false,
},
derogatory: {
title: <FormattedMessage defaultMessage="Derogatory" />,
words: [],
canEdit: false,
},
nsfw: {
title: <FormattedMessage defaultMessage="NSFW" />,
words: [
"adult content",
"explicit",
"mature audiences",
"18+",
"sensitive content",
"graphic content",
"age-restricted",
"explicit material",
"adult material",
"nsfw",
"explicit images",
"adult film",
"adult video",
"mature themes",
"sexual content",
"graphic violence",
"strong language",
"explicit language",
"adult-only",
"mature language",
],
canEdit: false,
},
crypto: {
title: <FormattedMessage defaultMessage="Crypto" />,
words: [
"bitcoin",
"btc",
"satoshi",
"crypto",
"blockchain",
"mining",
"wallet",
"exchange",
"halving",
"hash rate",
"ledger",
"crypto trading",
"digital currency",
"virtual currency",
"cryptocurrency investment",
"altcoin",
"decentralized finance",
"defi",
"token",
"ico",
"crypto wallet",
"satoshi nakamoto",
],
canEdit: true,
},
politics: {
title: <FormattedMessage defaultMessage="Politics" />,
words: [],
canEdit: true,
},
};
export function Moderation() {
const { publisher, system } = useEventPublisher();
const [topics, setTopics] = useState<Array<string>>(Object.keys(FixedModeration));
const navigate = useNavigate();
return (
<div className="flex flex-col g24">
<div className="flex flex-col g8 text-center">
<h1>
<FormattedMessage defaultMessage="Clean up your feed" />
</h1>
<FormattedMessage defaultMessage="Your space the way you want it 😌" />
</div>
<div className="flex flex-col g8">
<div className="flex g8 items-center">
<small className="grow uppercase font-semibold">
<FormattedMessage defaultMessage="Lists to mute:" />
</small>
<span className="font-medium">
<FormattedMessage defaultMessage="Toggle all" />
</span>
<ToggleSwitch
size={50}
onClick={() =>
topics.length === Object.keys(FixedModeration).length
? setTopics([])
: setTopics(Object.keys(FixedModeration))
}
className={topics.length === Object.keys(FixedModeration).length ? "active" : ""}
/>
</div>
{Object.entries(FixedModeration).map(([k, v]) => (
<div className="flex g8 items-center bb" key={k}>
<div className="font-semibold grow">{v.title}</div>
{v.canEdit && (
<div>
<FormattedMessage defaultMessage="edit" />
</div>
)}
<ToggleSwitch
size={50}
className={topics.includes(k) ? "active" : ""}
onClick={() => setTopics(s => (topics.includes(k) ? s.filter(a => a !== k) : appendDedupe(s, [k])))}
/>
</div>
))}
</div>
<div className="flex flex-col g8">
<span className="font-semibold">
<FormattedMessage defaultMessage="Additional Terms:" />
</span>
<small className="font-medium">
<FormattedMessage defaultMessage="Use commas to separate words e.g. word1, word2, word3" />
</small>
<textarea></textarea>
</div>
<AsyncButton
className="primary"
onClick={async () => {
const words = Object.entries(FixedModeration)
.filter(([k]) => topics.includes(k))
.map(([, v]) => v.words)
.flat();
if (words.length > 0) {
// no
}
navigate("/");
}}>
<FormattedMessage defaultMessage="Finish" />
</AsyncButton>
</div>
);
}

View File

@ -0,0 +1,47 @@
import AsyncButton from "Element/AsyncButton";
import AvatarEditor from "Element/User/AvatarEditor";
import { useContext, useState } from "react";
import { FormattedMessage } from "react-intl";
import { useLocation, useNavigate } from "react-router-dom";
import { generateNewLogin } from "Login";
import { SnortContext } from "@snort/system-react";
import { NotEncrypted } from "@snort/system";
import { NewUserState } from ".";
export function Profile() {
const system = useContext(SnortContext);
const [picture, setPicture] = useState<string>();
const [error, setError] = useState("");
const navigate = useNavigate();
const location = useLocation();
const state = location.state as NewUserState;
async function makeRandomKey() {
try {
setError("");
await generateNewLogin(system, key => Promise.resolve(new NotEncrypted(key)), {
name: state.name,
picture,
});
window.plausible?.("Generate Account");
navigate("/login/sign-up/topics");
} catch (e) {
if (e instanceof Error) {
setError(e.message);
}
}
}
return (
<div className="flex flex-col g24 text-center">
<h1>
<FormattedMessage defaultMessage="Profile Image" />
</h1>
<AvatarEditor picture={picture} onPictureChange={p => setPicture(p)} />
<AsyncButton className="primary" onClick={() => makeRandomKey()}>
<FormattedMessage defaultMessage="Next" />
</AsyncButton>
{error && <b className="error">{error}</b>}
</div>
);
}

View File

@ -0,0 +1,151 @@
import { FormattedMessage, useIntl } from "react-intl";
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { unwrap } from "@snort/shared";
import AsyncButton from "Element/AsyncButton";
import Icon from "Icons/Icon";
import { NewUserState } from ".";
import { LoginSessionType, LoginStore } from "Login";
import useLoginHandler from "Hooks/useLoginHandler";
import { NotEncrypted } from "@snort/system";
import classNames from "classnames";
export function SignIn() {
const navigate = useNavigate();
const { formatMessage } = useIntl();
const [key, setKey] = useState("");
const [error, setError] = useState("");
const [useKey, setUseKey] = useState(false);
const loginHandler = useLoginHandler();
const hasNip7 = "nostr" in window;
async function doNip07Login() {
const relays =
"getRelays" in unwrap(window.nostr) ? await unwrap(window.nostr?.getRelays).call(window.nostr) : undefined;
const pubKey = await unwrap(window.nostr).getPublicKey();
LoginStore.loginWithPubkey(pubKey, LoginSessionType.Nip7, relays);
navigate("/");
}
async function doLogin() {
setError("");
try {
await loginHandler.doLogin(key, key => Promise.resolve(new NotEncrypted(key)));
navigate("/");
} catch (e) {
if (e instanceof Error) {
setError(e.message);
} else {
setError(
formatMessage({
defaultMessage: "Unknown login error",
}),
);
}
console.error(e);
}
}
const nip7Login = hasNip7 && !useKey;
return (
<div className="flex flex-col g24">
<img src={CONFIG.appleTouchIconUrl} width={48} height={48} className="br mr-auto ml-auto" />
<div className="flex flex-col g16 items-center">
<h1>
<FormattedMessage defaultMessage="Sign In" />
</h1>
{nip7Login && <FormattedMessage defaultMessage="Use a nostr signer extension to sign in" />}
</div>
<div className={classNames("flex flex-col g16", { "items-center": nip7Login })}>
{hasNip7 && !useKey && (
<>
<AsyncButton onClick={doNip07Login}>
<div className="circle bg-warning p12 text-white">
<Icon name="key" />
</div>
<FormattedMessage defaultMessage="Sign in with Nostr Extension" />
</AsyncButton>
<Link to="" className="highlight">
<FormattedMessage defaultMessage="Supported Extensions" />
</Link>
<AsyncButton onClick={() => setUseKey(true)}>
<FormattedMessage defaultMessage="Sign in with key" />
</AsyncButton>
</>
)}
{(!hasNip7 || useKey) && (
<>
<input
type="text"
placeholder={formatMessage({
defaultMessage: "nsec, npub, nip-05, hex, mnemonic",
})}
value={key}
onChange={e => setKey(e.target.value)}
className="new-username"
/>
{error && <b className="error">{error}</b>}
<AsyncButton onClick={doLogin} className="primary">
<FormattedMessage defaultMessage="Login" />
</AsyncButton>
</>
)}
</div>
<div className="flex flex-col g16 items-center">
<FormattedMessage defaultMessage="Don't have an account?" />
<AsyncButton className="secondary" onClick={() => navigate("/login/sign-up")}>
<FormattedMessage defaultMessage="Sign Up" />
</AsyncButton>
</div>
</div>
);
}
export function SignUp() {
const { formatMessage } = useIntl();
const navigate = useNavigate();
const [name, setName] = useState("");
return (
<div className="flex flex-col g24">
<img src={CONFIG.appleTouchIconUrl} width={48} height={48} className="br mr-auto ml-auto" />
<div className="flex flex-col g16 items-center">
<h1>
<FormattedMessage defaultMessage="Sign Up" />
</h1>
<FormattedMessage defaultMessage="What should we call you?" />
</div>
<div className="flex flex-col g16">
<input
type="text"
autoFocus={true}
placeholder={formatMessage({
defaultMessage: "Name or nym",
})}
value={name}
onChange={e => setName(e.target.value)}
className="new-username"
/>
<AsyncButton
className="primary"
disabled={name.length === 0}
onClick={() =>
navigate("/login/sign-up/profile", {
state: {
name: name,
} as NewUserState,
})
}>
<FormattedMessage defaultMessage="Next" />
</AsyncButton>
</div>
<div className="flex flex-col g16 items-center">
<FormattedMessage defaultMessage="Already have an account?" />
<AsyncButton className="secondary" onClick={() => navigate("/login")}>
<FormattedMessage defaultMessage="Sign In" />
</AsyncButton>
</div>
</div>
);
}

View File

@ -0,0 +1,85 @@
import { ReactNode, useState } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import AsyncButton from "Element/AsyncButton";
import classNames from "classnames";
import { appendDedupe } from "SnortUtils";
import useEventPublisher from "Hooks/useEventPublisher";
export const FixedTopics = {
life: {
text: <FormattedMessage defaultMessage="Life" />,
tags: ["life"],
},
science: {
text: <FormattedMessage defaultMessage="Science" />,
tags: ["science"],
},
nature: {
text: <FormattedMessage defaultMessage="Nature" />,
tags: ["nature"],
},
business: {
text: <FormattedMessage defaultMessage="Business" />,
tags: ["business"],
},
game: {
text: <FormattedMessage defaultMessage="Game" />,
tags: ["game", "gaming"],
},
sport: {
text: <FormattedMessage defaultMessage="Sport" />,
tags: ["sport"],
},
photography: {
text: <FormattedMessage defaultMessage="Photography" />,
tags: ["photography"],
},
bitcoin: {
text: <FormattedMessage defaultMessage="Bitcoin" />,
tags: ["bitcoin"],
},
};
export function Topics() {
const { publisher, system } = useEventPublisher();
const [topics, setTopics] = useState<Array<string>>([]);
const navigate = useNavigate();
function tab(name: string, text: ReactNode) {
const active = topics.includes(name);
return (
<div
className={classNames("tab", { active })}
onClick={() => setTopics(s => (active ? s.filter(a => a !== name) : appendDedupe(s, [name])))}>
{text}
</div>
);
}
return (
<div className="flex flex-col g24 text-center">
<h1>
<FormattedMessage defaultMessage="Pick a few topics of interest" />
</h1>
<div className="tabs flex-wrap justify-center">{Object.entries(FixedTopics).map(([k, v]) => tab(k, v.text))}</div>
<AsyncButton
className="primary"
onClick={async () => {
const tags = Object.entries(FixedTopics)
.filter(([k]) => topics.includes(k))
.map(([, v]) => v.tags)
.flat();
if (tags.length > 0) {
const ev = await publisher?.tags(tags);
if (ev) {
await system.BroadcastEvent(ev);
}
}
navigate("/login/sign-up/discover");
}}>
<FormattedMessage defaultMessage="Next" />
</AsyncButton>
</div>
);
}

View File

@ -2,9 +2,10 @@ import "./Preferences.css";
import { FormattedMessage, useIntl } from "react-intl";
import useLogin from "Hooks/useLogin";
import { DefaultPreferences, updatePreferences, UserPreferences } from "Login";
import { updatePreferences, UserPreferences } from "Login";
import { DefaultImgProxy } from "Const";
import { unwrap } from "SnortUtils";
import { useLocale } from "IntlProvider";
import messages from "./messages";
@ -36,6 +37,7 @@ const PreferencesPage = () => {
const { formatMessage } = useIntl();
const login = useLogin();
const perf = login.preferences;
const { lang } = useLocale();
return (
<div className="preferences flex flex-col g24">
@ -49,7 +51,7 @@ const PreferencesPage = () => {
</h4>
<div>
<select
value={perf.language || DefaultPreferences.language}
value={lang}
onChange={e =>
updatePreferences(login, {
...perf,

View File

@ -660,6 +660,10 @@ div.form-col {
color: var(--warning);
}
.bg-warning {
background-color: var(--warning);
}
.bg-error {
background-color: var(--error);
}

View File

@ -26,7 +26,6 @@ import * as serviceWorkerRegistration from "serviceWorkerRegistration";
import { IntlProvider } from "IntlProvider";
import { getCountry, unwrap } from "SnortUtils";
import Layout from "Pages/Layout";
import LoginPage from "Pages/LoginPage";
import ProfilePage from "Pages/Profile/ProfilePage";
import { RootRoutes, RootTabRoutes } from "Pages/Root";
import NotificationsPage from "Pages/Notifications";
@ -37,7 +36,6 @@ import MessagesPage from "Pages/MessagesPage";
import DonatePage from "Pages/DonatePage";
import SearchPage from "Pages/SearchPage";
import HelpPage from "Pages/HelpPage";
import { NewUserRoutes } from "Pages/new";
import { WalletRoutes } from "Pages/WalletPage";
import NostrLinkHandler from "Pages/NostrLinkHandler";
import { ThreadRoute } from "Element/Event/Thread";
@ -51,6 +49,13 @@ import FreeNostrAddressPage from "./Pages/FreeNostrAddressPage";
import { ListFeedPage } from "Pages/ListFeedPage";
import { updateRelayConnections } from "Hooks/useLoginRelays";
import { AboutPage } from "Pages/About";
import { OnboardingRoutes } from "Pages/onboarding";
declare global {
interface Window {
plausible?: (tag: string) => void;
}
}
const WasmQueryOptimizer = {
expandFilter: (f: ReqFilter) => {
@ -165,10 +170,6 @@ async function initSite() {
let didInit = false;
const mainRoutes = [
...RootRoutes,
{
path: "/login",
element: <LoginPage />,
},
{
path: "/help",
element: <HelpPage />,
@ -218,7 +219,7 @@ const mainRoutes = [
path: "/about",
element: <AboutPage />,
},
...NewUserRoutes,
...OnboardingRoutes,
...WalletRoutes,
] as Array<RouteObject>;

View File

@ -23,11 +23,14 @@
"+vVZ/G": {
"defaultMessage": "Connect"
},
"+vj0U3": {
"defaultMessage": "edit"
},
"+xliwN": {
"defaultMessage": "{name} reposted"
},
"/4tOwT": {
"defaultMessage": "Skip"
"/B8zwF": {
"defaultMessage": "Your space the way you want it 😌"
},
"/GCoTA": {
"defaultMessage": "Clear"
@ -39,9 +42,6 @@
"defaultMessage": "Public",
"description": "Public Zap"
},
"/RD0e2": {
"defaultMessage": "Nostr uses digital signature technology to provide tamper proof notes which can safely be replicated to many relays to provide redundant storage of your content."
},
"/Xf4UW": {
"defaultMessage": "Send anonymous usage metrics"
},
@ -72,6 +72,9 @@
"0mch2Y": {
"defaultMessage": "name has disallowed characters"
},
"0siT4z": {
"defaultMessage": "Politics"
},
"0uoY11": {
"defaultMessage": "Show Status"
},
@ -93,6 +96,9 @@
"1o2BgB": {
"defaultMessage": "Check Signatures"
},
"1ozeyg": {
"defaultMessage": "Nature"
},
"1udzha": {
"defaultMessage": "Conversations"
},
@ -102,12 +108,18 @@
"25V4l1": {
"defaultMessage": "Banner"
},
"25WwxF": {
"defaultMessage": "Don't have an account?"
},
"2IFGap": {
"defaultMessage": "Donate"
},
"2LbrkB": {
"defaultMessage": "Enter password"
},
"2O2sfp": {
"defaultMessage": "Finish"
},
"2a2YiP": {
"defaultMessage": "{n} Bookmarks"
},
@ -120,6 +132,9 @@
"2zJXeA": {
"defaultMessage": "Profiles"
},
"39AHJm": {
"defaultMessage": "Sign Up"
},
"3KNMbJ": {
"defaultMessage": "Articles"
},
@ -138,16 +153,9 @@
"3tVy+Z": {
"defaultMessage": "{n} Followers"
},
"3xCwbZ": {
"defaultMessage": "OR",
"description": "Seperator text for Login / Generate Key"
},
"3yk8fB": {
"defaultMessage": "Wallet"
},
"40VR6s": {
"defaultMessage": "Nostr Connect"
},
"450Fty": {
"defaultMessage": "None"
},
@ -163,6 +171,9 @@
"4MBtMa": {
"defaultMessage": "Name must be between 1 and 32 characters"
},
"4MjsHk": {
"defaultMessage": "Life"
},
"4OB335": {
"defaultMessage": "Dislike"
},
@ -181,15 +192,9 @@
"5CB6zB": {
"defaultMessage": "Zap Splits"
},
"5JcXdV": {
"defaultMessage": "Create Account"
},
"5oTnfy": {
"defaultMessage": "Buy Handle"
},
"5rOdPG": {
"defaultMessage": "Once you setup your key manager extension and generated a key, you can follow our new users flow to setup your profile and help you find some interesting people on Nostr to follow."
},
"5u6iEc": {
"defaultMessage": "Transfer to Pubkey"
},
@ -217,9 +222,6 @@
"6TfgXX": {
"defaultMessage": "{site} is an open source project built by passionate people in their free time"
},
"6Yfvvp": {
"defaultMessage": "Get an identifier"
},
"6bgpn+": {
"defaultMessage": "Not all clients support this, you may still receive some zaps as if zap splits was not configured"
},
@ -232,9 +234,6 @@
"7+Domh": {
"defaultMessage": "Notes"
},
"7/h1jn": {
"defaultMessage": "After submitting the pin there may be a slight delay as we encrypt the key."
},
"712i26": {
"defaultMessage": "Proxy uses HODL invoices to forward the payment, which hides the pubkey of your node"
},
@ -256,9 +255,6 @@
"8ED/4u": {
"defaultMessage": "Reply To"
},
"8Kboo2": {
"defaultMessage": "Scan this QR code with your signer app to get started"
},
"8QDesP": {
"defaultMessage": "Zap {n} sats"
},
@ -274,10 +270,6 @@
"8v1NN+": {
"defaultMessage": "Pairing phrase"
},
"8xNnhi": {
"defaultMessage": "Nostr Extension",
"description": "Login button for NIP7 key manager extension"
},
"9+Ddtu": {
"defaultMessage": "Next"
},
@ -290,10 +282,6 @@
"9WRlF4": {
"defaultMessage": "Send"
},
"9gqH2W": {
"defaultMessage": "Login",
"description": "Login button"
},
"9kSari": {
"defaultMessage": "Retry publishing"
},
@ -316,15 +304,15 @@
"ASRK0S": {
"defaultMessage": "This author has been muted"
},
"Adk34V": {
"defaultMessage": "Setup your Profile"
},
"Ai8VHU": {
"defaultMessage": "Unlimited note retention on Snort relay"
},
"AkCxS/": {
"defaultMessage": "Reason"
},
"Am8glJ": {
"defaultMessage": "Game"
},
"AnLrRC": {
"defaultMessage": "Non-Zap",
"description": "Non-Zap, Regular LN payment"
@ -347,15 +335,9 @@
"BGCM48": {
"defaultMessage": "Write access to Snort relay, with 1 year of event retention"
},
"BOUMjw": {
"defaultMessage": "No nostr users found for {twitterUsername}"
},
"BWpuKl": {
"defaultMessage": "Update"
},
"BcGMo+": {
"defaultMessage": "Notes hold text content, the most popular usage of these notes is to store \"tweet like\" messages."
},
"BjNwZW": {
"defaultMessage": "Nostr address (nip05)"
},
@ -380,9 +362,6 @@
"CmZ9ls": {
"defaultMessage": "{n} Muted"
},
"CoVXRS": {
"defaultMessage": "Alternatively, you may choose to store your private key without a PIN by selecting 'Cancel.'"
},
"CsCUYo": {
"defaultMessage": "{n} sats"
},
@ -416,8 +395,8 @@
"DtYelJ": {
"defaultMessage": "Transfer"
},
"E8a4yq": {
"defaultMessage": "Follow some popular accounts"
"Dx4ey3": {
"defaultMessage": "Toggle all"
},
"EJbFi7": {
"defaultMessage": "Search notes"
@ -425,9 +404,6 @@
"ELbg9p": {
"defaultMessage": "Data Providers"
},
"EPYwm7": {
"defaultMessage": "Your private key is your password. If you lose this key, you will lose access to your account! Copy it and keep it in a safe place. There is no way to reset your private key."
},
"EQKRE4": {
"defaultMessage": "Show badges on profile pages"
},
@ -452,12 +428,6 @@
"EnCOBJ": {
"defaultMessage": "Buy"
},
"Eqjl5K": {
"defaultMessage": "Only Snort and our integration partner identifier gives you a colorful domain name, but you are welcome to use other services too."
},
"F+B3x1": {
"defaultMessage": "We have also partnered with nostrplebs.com to give you more options"
},
"F3l7xL": {
"defaultMessage": "Add Account"
},
@ -467,9 +437,6 @@
"FMfjrl": {
"defaultMessage": "Show status messages on profile pages"
},
"FS3b54": {
"defaultMessage": "Done!"
},
"FSYL8G": {
"defaultMessage": "Trending Users"
},
@ -549,12 +516,6 @@
"IKKHqV": {
"defaultMessage": "Follows"
},
"INSqIz": {
"defaultMessage": "Twitter username..."
},
"IUZC+0": {
"defaultMessage": "This means that nobody can modify notes which you have created and everybody can easily verify that the notes they are reading are created by you."
},
"IVbtTS": {
"defaultMessage": "Zap all {n} sats"
},
@ -573,6 +534,9 @@
"J+dIsA": {
"defaultMessage": "Subscriptions"
},
"J2HeQ+": {
"defaultMessage": "Use commas to separate words e.g. word1, word2, word3"
},
"JCIgkj": {
"defaultMessage": "Username"
},
@ -582,6 +546,9 @@
"JHEHCk": {
"defaultMessage": "Zaps ({n})"
},
"JIVWWA": {
"defaultMessage": "Sport"
},
"JPFYIM": {
"defaultMessage": "No lightning address"
},
@ -616,9 +583,6 @@
"KQvWvD": {
"defaultMessage": "Deleted"
},
"KWuDfz": {
"defaultMessage": "I have saved my keys, continue"
},
"KahimY": {
"defaultMessage": "Unknown event kind: {kind}"
},
@ -649,10 +613,6 @@
"LwYmVi": {
"defaultMessage": "Zaps on this note will be split to the following users."
},
"M10zFV": {
"defaultMessage": "Nostr Connect",
"description": "Login button for NIP-46 signer app"
},
"M3Oirc": {
"defaultMessage": "Debug Menus"
},
@ -666,9 +626,6 @@
"defaultMessage": "Wallet password",
"description": "Wallet password input placeholder"
},
"MRp6Ly": {
"defaultMessage": "Twitter username"
},
"MWTx65": {
"defaultMessage": "Default Page"
},
@ -696,27 +653,18 @@
"NAuFNH": {
"defaultMessage": "You already have a subscription of this type, please renew or pay"
},
"NNSu3d": {
"defaultMessage": "Import Twitter Follows"
},
"NdOYJJ": {
"defaultMessage": "Hmm nothing here.. Checkout {newUsersPage} to follow some recommended nostrich's!"
},
"NepkXH": {
"defaultMessage": "Can't vote with {amount} sats, please set a different default zap amount"
},
"NfNk2V": {
"defaultMessage": "Your private key"
},
"NndBJE": {
"defaultMessage": "New users page"
},
"O8Z8t9": {
"defaultMessage": "Show More"
},
"O9GTIc": {
"defaultMessage": "Profile picture"
},
"OEW7yJ": {
"defaultMessage": "Zaps"
},
@ -735,12 +683,6 @@
"ORGv1Q": {
"defaultMessage": "Created"
},
"Oq/kVn": {
"defaultMessage": "Name-squatting and impersonation is not allowed. {site} and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule."
},
"P/xrLk": {
"defaultMessage": "Secure your private key with a PIN, ensuring enhanced protection on {site}. You'll be prompted to enter this PIN each time you access the site."
},
"P61BTu": {
"defaultMessage": "Copy Event JSON"
},
@ -757,12 +699,6 @@
"defaultMessage": "Summary",
"description": "Notifications summary"
},
"PLSbmL": {
"defaultMessage": "Your mnemonic phrase"
},
"PaN7t3": {
"defaultMessage": "Preview on {site}"
},
"PamNxw": {
"defaultMessage": "Unknown file header: {name}"
},
@ -778,13 +714,6 @@
"QWhotP": {
"defaultMessage": "Zap Pool only works if you use one of the supported wallet connections (WebLN, LNC, LNDHub or Nostr Wallet Connect)"
},
"QawghE": {
"defaultMessage": "You can change your username at any point."
},
"QxCuTo": {
"defaultMessage": "Art by {name}",
"description": "Artwork attribution label"
},
"Qxv0B2": {
"defaultMessage": "You currently have {number} sats in your zap pool."
},
@ -794,9 +723,6 @@
"R81upa": {
"defaultMessage": "People you follow"
},
"RDZVQL": {
"defaultMessage": "Check"
},
"RSr2uB": {
"defaultMessage": "Username must only contain lowercase letters and numbers"
},
@ -813,6 +739,9 @@
"defaultMessage": "Recent",
"description": "Sort order name"
},
"RkW5we": {
"defaultMessage": "Bitcoin"
},
"RoOyAh": {
"defaultMessage": "Relays"
},
@ -844,6 +773,9 @@
"Sjo1P4": {
"defaultMessage": "Custom"
},
"SmuYUd": {
"defaultMessage": "What should we call you?"
},
"Ss0sWu": {
"defaultMessage": "Pay Now"
},
@ -859,6 +791,12 @@
"TP/cMX": {
"defaultMessage": "Ended"
},
"TaeBqw": {
"defaultMessage": "Sign in with Nostr Extension"
},
"TdtZQ5": {
"defaultMessage": "Crypto"
},
"TpgeGw": {
"defaultMessage": "Hex Salt..",
"description": "Hexidecimal 'salt' input for imgproxy"
@ -884,15 +822,15 @@
"UUPFlt": {
"defaultMessage": "Users must accept the content warning to show the content of your note."
},
"Ub+AGc": {
"defaultMessage": "Sign In"
},
"Up5U7K": {
"defaultMessage": "Block"
},
"UrKTqQ": {
"defaultMessage": "You have an active iris.to account"
},
"VBadwB": {
"defaultMessage": "Hmm, can't find a key manager extension.. try reloading the page."
},
"VN0+Fz": {
"defaultMessage": "Balance: {amount} sats"
},
@ -914,9 +852,6 @@
"VvaJst": {
"defaultMessage": "View Wallets"
},
"Vx7Zm2": {
"defaultMessage": "How do keys work?"
},
"W1yoZY": {
"defaultMessage": "It looks like you dont have any subscriptions, you can get one {link}"
},
@ -926,17 +861,14 @@
"W9355R": {
"defaultMessage": "Unmute"
},
"WONP5O": {
"defaultMessage": "Find your twitter follows on nostr (Data provided by {provider})"
},
"WmZhfL": {
"defaultMessage": "Automatically translate notes to your local language"
},
"WvGmZT": {
"defaultMessage": "npub / nprofile / nostr address"
},
"WxthCV": {
"defaultMessage": "e.g. Jack"
"X6tipZ": {
"defaultMessage": "Sign in with key"
},
"X7xU8J": {
"defaultMessage": "nsec, npub, nip-05, hex, mnemonic"
@ -957,9 +889,6 @@
"defaultMessage": "Redeem",
"description": "Button: Redeem Cashu token"
},
"XzF0aC": {
"defaultMessage": "Key manager extensions are more secure and allow you to easily login to any Nostr client, here are some well known extensions:"
},
"YDURw6": {
"defaultMessage": "Service URL"
},
@ -981,8 +910,8 @@
"Zff6lu": {
"defaultMessage": "Username iris.to/<b>{name}</b> is reserved for you!"
},
"Zr5TMx": {
"defaultMessage": "Setup profile"
"a+6cHB": {
"defaultMessage": "Derogatory"
},
"a5UPxh": {
"defaultMessage": "Fund developers and platforms providing NIP-05 verification services"
@ -990,6 +919,12 @@
"a7TDNm": {
"defaultMessage": "Notes will stream in real time into global and notes tab"
},
"aHje0o": {
"defaultMessage": "Name or nym"
},
"aMaLBK": {
"defaultMessage": "Supported Extensions"
},
"aWpBzj": {
"defaultMessage": "Show more"
},
@ -1011,19 +946,12 @@
"bfvyfs": {
"defaultMessage": "Anon"
},
"brAXSu": {
"defaultMessage": "Pick a username"
},
"bxv59V": {
"defaultMessage": "Just now"
},
"c+JYNI": {
"defaultMessage": "No thanks"
},
"c+oiJe": {
"defaultMessage": "Install Extension",
"description": "Heading for install key manager extension"
},
"c35bj2": {
"defaultMessage": "If you have an enquiry about your NIP-05 order please DM {link}"
},
@ -1033,6 +961,9 @@
"cFbU1B": {
"defaultMessage": "Using Alby? Go to {link} to get your NWC config!"
},
"cHCwbF": {
"defaultMessage": "Photography"
},
"cPIKU2": {
"defaultMessage": "Following"
},
@ -1055,6 +986,9 @@
"cyR7Kh": {
"defaultMessage": "Back"
},
"d+6YsV": {
"defaultMessage": "Lists to mute:"
},
"d6CyG5": {
"defaultMessage": "History",
"description": "Wallet transation history"
@ -1080,6 +1014,9 @@
"e7qqly": {
"defaultMessage": "Mark All Read"
},
"eF0Re7": {
"defaultMessage": "Use a nostr signer extension to sign in"
},
"eHAneD": {
"defaultMessage": "Reaction emoji"
},
@ -1104,6 +1041,9 @@
"fWZYP5": {
"defaultMessage": "Pinned"
},
"fX5RYm": {
"defaultMessage": "Pick a few topics of interest"
},
"filwqD": {
"defaultMessage": "Read"
},
@ -1125,9 +1065,6 @@
"g985Wp": {
"defaultMessage": "Failed to send vote"
},
"gBdUXk": {
"defaultMessage": "Save your keys!"
},
"gDzDRs": {
"defaultMessage": "Emoji to send when reactiong to a note"
},
@ -1146,12 +1083,12 @@
"grQ+mI": {
"defaultMessage": "Proof of Work"
},
"h7jvCs": {
"defaultMessage": "{site} is more fun together!"
},
"h8XMJL": {
"defaultMessage": "Badges"
},
"hK5ZDk": {
"defaultMessage": "the world"
},
"hMzcSq": {
"defaultMessage": "Messages"
},
@ -1176,9 +1113,6 @@
"iCqGww": {
"defaultMessage": "Reactions ({n})"
},
"iDGAbc": {
"defaultMessage": "Get a Snort identifier"
},
"iEoXYx": {
"defaultMessage": "DeepL translations"
},
@ -1209,6 +1143,9 @@
"jAmfGl": {
"defaultMessage": "Your {site_name} subscription is expired"
},
"jHa/ko": {
"defaultMessage": "Clean up your feed"
},
"jMzO1S": {
"defaultMessage": "Internal error: {msg}"
},
@ -1216,9 +1153,6 @@
"defaultMessage": "Back",
"description": "Navigate back button on threads view"
},
"juhqvW": {
"defaultMessage": "Improve login security with browser extensions"
},
"jvo0vs": {
"defaultMessage": "Save"
},
@ -1228,6 +1162,9 @@
"k2veDA": {
"defaultMessage": "Write"
},
"k7+5Ny": {
"defaultMessage": "Hate Speech"
},
"k7sKNy": {
"defaultMessage": "Our very own NIP-05 verification service, help support the development of this site and get a shiny special badge on our site!"
},
@ -1237,9 +1174,6 @@
"kJYo0u": {
"defaultMessage": "{n,plural,=0{{name} reposted} other{{name} & {n} others reposted}}"
},
"kTLGM2": {
"defaultMessage": "{site} is designed to have a similar experience to Twitter."
},
"kaaf1E": {
"defaultMessage": "now"
},
@ -1249,9 +1183,6 @@
"l+ikU1": {
"defaultMessage": "Everything in {plan}"
},
"lBboHo": {
"defaultMessage": "If you want to try out some others, check out {link} for more!"
},
"lCILNz": {
"defaultMessage": "Buy Now"
},
@ -1264,9 +1195,6 @@
"lTbT3s": {
"defaultMessage": "Wallet password"
},
"lVKH7C": {
"defaultMessage": "What is {site} and how does it work?"
},
"lgg1KN": {
"defaultMessage": "account page"
},
@ -1313,15 +1241,6 @@
"nGBrvw": {
"defaultMessage": "Bookmarks"
},
"nN9XTz": {
"defaultMessage": "Share your thoughts with {link}"
},
"nOaArs": {
"defaultMessage": "Setup Profile"
},
"ncbgUU": {
"defaultMessage": "{site} is a Nostr UI, nostr is a decentralised protocol for saving and distributing \"notes\"."
},
"nihgfo": {
"defaultMessage": "Listen to this article"
},
@ -1331,10 +1250,6 @@
"nwZXeh": {
"defaultMessage": "{n} blocked"
},
"o6Uy3d": {
"defaultMessage": "Only the secret key can be used to publish (sign events), everything else logs you in read-only mode.",
"description": "Explanation for public key only login is read-only"
},
"o7e+nJ": {
"defaultMessage": "{n} followers"
},
@ -1344,19 +1259,9 @@
"odFwjL": {
"defaultMessage": "Follows only"
},
"odhABf": {
"defaultMessage": "Login",
"description": "Login header"
},
"ojzbwv": {
"defaultMessage": "Hey, it looks like you dont have a Nostr Address yet, you should get one! Check out {link}"
},
"osUr8O": {
"defaultMessage": "You can also use these extensions to login to most Nostr sites."
},
"oxCa4R": {
"defaultMessage": "Getting an identifier helps confirm the real you to people who know you. Many people can have a username @jack, but there is only one jack@cash.app."
},
"p4N05H": {
"defaultMessage": "Upload"
},
@ -1402,6 +1307,9 @@
"qtWLmt": {
"defaultMessage": "Like"
},
"qydxOd": {
"defaultMessage": "Science"
},
"qz9fty": {
"defaultMessage": "Incorrect pin"
},
@ -1417,21 +1325,12 @@
"rbrahO": {
"defaultMessage": "Close"
},
"reJ6SM": {
"defaultMessage": "It is recommended to use one of the following browser extensions if you are on a desktop computer to secure your key:"
},
"rfuMjE": {
"defaultMessage": "(Default)"
},
"rmdsT4": {
"defaultMessage": "{n} days"
},
"rrfdTe": {
"defaultMessage": "This is the same technology which is used by Bitcoin and has been proven to be extremely secure."
},
"rudscU": {
"defaultMessage": "Failed to load follows, please try again later"
},
"rx1i0i": {
"defaultMessage": "Short link"
},
@ -1471,6 +1370,9 @@
"u4bHcR": {
"defaultMessage": "Check out the code here: {link}"
},
"uCk8r+": {
"defaultMessage": "Already have an account?"
},
"uKqSN+": {
"defaultMessage": "Follows Feed"
},
@ -1486,15 +1388,15 @@
"usAvMr": {
"defaultMessage": "Edit Profile"
},
"ut+2Cd": {
"defaultMessage": "Get a partner identifier"
},
"v8lolG": {
"defaultMessage": "Start chat"
},
"vB3oQ/": {
"defaultMessage": "Must be a contact list or pubkey list"
},
"vN5UH8": {
"defaultMessage": "Profile Image"
},
"vOKedj": {
"defaultMessage": "{n,plural,=1{& {n} other} other{& {n} others}}"
},
@ -1513,13 +1415,15 @@
"vxwnbh": {
"defaultMessage": "Amount of work to apply to all published events"
},
"w1Fanr": {
"defaultMessage": "Business"
},
"w6qrwX": {
"defaultMessage": "NSFW"
},
"wEQDC6": {
"defaultMessage": "Edit"
},
"wLtRCF": {
"defaultMessage": "Your key",
"description": "Label for key input"
},
"wSZR47": {
"defaultMessage": "Submit"
},
@ -1539,9 +1443,6 @@
"wtLjP6": {
"defaultMessage": "Copy ID"
},
"wuMvI5": {
"defaultMessage": "{site_name} Developers"
},
"x/Fx2P": {
"defaultMessage": "Fund the services that you use by splitting a portion of all your zaps into a pool of funds!"
},
@ -1554,12 +1455,6 @@
"xIoGG9": {
"defaultMessage": "Go to"
},
"xJ9n2N": {
"defaultMessage": "Your public key"
},
"xKflGN": {
"defaultMessage": "{username}''s Follows on Nostr"
},
"xQtL3v": {
"defaultMessage": "Unlock",
"description": "Unlock wallet"
@ -1573,6 +1468,9 @@
"xhQMeQ": {
"defaultMessage": "Expires"
},
"xl4s/X": {
"defaultMessage": "Additional Terms:"
},
"xmcVZ0": {
"defaultMessage": "Search"
},
@ -1600,9 +1498,6 @@
"zcaOTs": {
"defaultMessage": "Zap amount in sats"
},
"zjJZBd": {
"defaultMessage": "You're ready!"
},
"zm6qS1": {
"defaultMessage": "{n} mins to read"
},

View File

@ -7,12 +7,12 @@
"+vA//S": "Logins",
"+vIQlC": "Please make sure to save the following password in order to manage your handle in the future",
"+vVZ/G": "Connect",
"+vj0U3": "edit",
"+xliwN": "{name} reposted",
"/4tOwT": "Skip",
"/B8zwF": "Your space the way you want it 😌",
"/GCoTA": "Clear",
"/JE/X+": "Account Support",
"/PCavi": "Public",
"/RD0e2": "Nostr uses digital signature technology to provide tamper proof notes which can safely be replicated to many relays to provide redundant storage of your content.",
"/Xf4UW": "Send anonymous usage metrics",
"/clOBU": "Weekly",
"/d6vEc": "Make your profile easier to find and share",
@ -23,6 +23,7 @@
"0BUTMv": "Search...",
"0jOEtS": "Invalid LNURL",
"0mch2Y": "name has disallowed characters",
"0siT4z": "Politics",
"0uoY11": "Show Status",
"0yO7wF": "{n} secs",
"1Mo59U": "Are you sure you want to remove this note from bookmarks?",
@ -30,38 +31,39 @@
"1c4YST": "Connected to: {node} 🎉",
"1nYUGC": "{n} Following",
"1o2BgB": "Check Signatures",
"1ozeyg": "Nature",
"1udzha": "Conversations",
"2/2yg+": "Add",
"25V4l1": "Banner",
"25WwxF": "Don't have an account?",
"2IFGap": "Donate",
"2LbrkB": "Enter password",
"2O2sfp": "Finish",
"2a2YiP": "{n} Bookmarks",
"2k0Cv+": "Dislikes ({n})",
"2ukA4d": "{n} hours",
"2zJXeA": "Profiles",
"39AHJm": "Sign Up",
"3KNMbJ": "Articles",
"3cc4Ct": "Light",
"3gOsZq": "Translators",
"3qnJlS": "You are voting with {amount} sats",
"3t3kok": "{n,plural,=1{{n} new note} other{{n} new notes}}",
"3tVy+Z": "{n} Followers",
"3xCwbZ": "OR",
"3yk8fB": "Wallet",
"40VR6s": "Nostr Connect",
"450Fty": "None",
"47FYwb": "Cancel",
"4IPzdn": "Primary Developers",
"4L2vUY": "Your new NIP-05 handle is:",
"4MBtMa": "Name must be between 1 and 32 characters",
"4MjsHk": "Life",
"4OB335": "Dislike",
"4Vmpt4": "Nostr Plebs is one of the first NIP-05 providers in the space and offers a good collection of domains at reasonable prices",
"4Z3t5i": "Use imgproxy to compress images",
"4rYCjn": "Note to Self",
"5BVs2e": "zap",
"5CB6zB": "Zap Splits",
"5JcXdV": "Create Account",
"5oTnfy": "Buy Handle",
"5rOdPG": "Once you setup your key manager extension and generated a key, you can follow our new users flow to setup your profile and help you find some interesting people on Nostr to follow.",
"5u6iEc": "Transfer to Pubkey",
"5vMmmR": "Usernames are not unique on Nostr. The nostr address is your unique human-readable address that is unique to you upon registration.",
"5ykRmX": "Send zap",
@ -71,12 +73,10 @@
"65BmHb": "Failed to proxy image from {host}, click here to load directly",
"6OSOXl": "Reason: <i>{reason}</i>",
"6TfgXX": "{site} is an open source project built by passionate people in their free time",
"6Yfvvp": "Get an identifier",
"6bgpn+": "Not all clients support this, you may still receive some zaps as if zap splits was not configured",
"6ewQqw": "Likes ({n})",
"6uMqL1": "Unpaid",
"7+Domh": "Notes",
"7/h1jn": "After submitting the pin there may be a slight delay as we encrypt the key.",
"712i26": "Proxy uses HODL invoices to forward the payment, which hides the pubkey of your node",
"7BX/yC": "Account Switcher",
"7UOvbT": "Offline",
@ -84,18 +84,15 @@
"8/vBbP": "Reposts ({n})",
"89q5wc": "Confirm Reposts",
"8ED/4u": "Reply To",
"8Kboo2": "Scan this QR code with your signer app to get started",
"8QDesP": "Zap {n} sats",
"8Rkoyb": "Recipient",
"8Y6bZQ": "Invalid zap split: {input}",
"8g2vyB": "name too long",
"8v1NN+": "Pairing phrase",
"8xNnhi": "Nostr Extension",
"9+Ddtu": "Next",
"9HU8vw": "Reply",
"9SvQep": "Follows {n}",
"9WRlF4": "Send",
"9gqH2W": "Login",
"9kSari": "Retry publishing",
"9pMqYs": "Nostr Address",
"9wO4wJ": "Lightning Invoice",
@ -103,9 +100,9 @@
"ADmfQT": "Parent",
"AN0Z7Q": "Muted Words",
"ASRK0S": "This author has been muted",
"Adk34V": "Setup your Profile",
"Ai8VHU": "Unlimited note retention on Snort relay",
"AkCxS/": "Reason",
"Am8glJ": "Game",
"AnLrRC": "Non-Zap",
"AxDOiG": "Months",
"AyGauy": "Login",
@ -113,9 +110,7 @@
"B6+XJy": "zapped",
"B6H7eJ": "nsec, npub, nip-05, hex",
"BGCM48": "Write access to Snort relay, with 1 year of event retention",
"BOUMjw": "No nostr users found for {twitterUsername}",
"BWpuKl": "Update",
"BcGMo+": "Notes hold text content, the most popular usage of these notes is to store \"tweet like\" messages.",
"BjNwZW": "Nostr address (nip05)",
"C1LjMx": "Lightning Donation",
"C7642/": "Quote Repost",
@ -124,7 +119,6 @@
"CHTbO3": "Failed to load invoice",
"CVWeJ6": "Trending People",
"CmZ9ls": "{n} Muted",
"CoVXRS": "Alternatively, you may choose to store your private key without a PIN by selecting 'Cancel.'",
"CsCUYo": "{n} sats",
"Cu/K85": "Translated from {lang}",
"D+KzKd": "Automatically zap every note when loaded",
@ -136,10 +130,9 @@
"Dh3hbq": "Auto Zap",
"Dn82AL": "Live",
"DtYelJ": "Transfer",
"E8a4yq": "Follow some popular accounts",
"Dx4ey3": "Toggle all",
"EJbFi7": "Search notes",
"ELbg9p": "Data Providers",
"EPYwm7": "Your private key is your password. If you lose this key, you will lose access to your account! Copy it and keep it in a safe place. There is no way to reset your private key.",
"EQKRE4": "Show badges on profile pages",
"EWyQH5": "Global",
"Ebl/B2": "Translate to {lang}",
@ -148,12 +141,9 @@
"EcglP9": "Key",
"EjFyoR": "On-chain Donation Address",
"EnCOBJ": "Buy",
"Eqjl5K": "Only Snort and our integration partner identifier gives you a colorful domain name, but you are welcome to use other services too.",
"F+B3x1": "We have also partnered with nostrplebs.com to give you more options",
"F3l7xL": "Add Account",
"FDguSC": "{n} Zaps",
"FMfjrl": "Show status messages on profile pages",
"FS3b54": "Done!",
"FSYL8G": "Trending Users",
"FcNSft": "Redirect issues HTTP redirect to the supplied lightning address",
"FdhSU2": "Claim Now",
@ -180,17 +170,17 @@
"HhcAVH": "You don't follow this person, click here to load media from <i>{link}</i>, or update <a><i>your preferences</i></a> to always load media from everybody.",
"IEwZvs": "Are you sure you want to unpin this note?",
"IKKHqV": "Follows",
"INSqIz": "Twitter username...",
"IUZC+0": "This means that nobody can modify notes which you have created and everybody can easily verify that the notes they are reading are created by you.",
"IVbtTS": "Zap all {n} sats",
"IWz1ta": "Auto Translate",
"Ig9/a1": "Sent {n} sats to {name}",
"IoQq+a": "Click here to load anyway",
"Ix8l+B": "Trending Notes",
"J+dIsA": "Subscriptions",
"J2HeQ+": "Use commas to separate words e.g. word1, word2, word3",
"JCIgkj": "Username",
"JGrt9q": "Send sats to {name}",
"JHEHCk": "Zaps ({n})",
"JIVWWA": "Sport",
"JPFYIM": "No lightning address",
"JSx7y9": "Subscribe to {site_name} {plan} for {price} and receive the following rewards",
"JeoS4y": "Repost",
@ -202,7 +192,6 @@
"KAhAcM": "Enter LNDHub config",
"KHK8B9": "Relay",
"KQvWvD": "Deleted",
"KWuDfz": "I have saved my keys, continue",
"KahimY": "Unknown event kind: {kind}",
"KoFlZg": "Enter mint URL",
"KtsyO0": "Enter Pin",
@ -213,12 +202,10 @@
"Lu5/Bj": "Open on Zapstr",
"Lw+I+J": "{n,plural,=0{{name} zapped} other{{name} & {n} others zapped}}",
"LwYmVi": "Zaps on this note will be split to the following users.",
"M10zFV": "Nostr Connect",
"M3Oirc": "Debug Menus",
"MBAYRO": "Shows \"Copy ID\" and \"Copy Event JSON\" in the context menu on each message",
"MI2jkA": "Not available:",
"MP54GY": "Wallet password",
"MRp6Ly": "Twitter username",
"MWTx65": "Default Page",
"MiMipu": "Set as primary Nostr address (nip05)",
"Mrpkot": "Pay for subscription",
@ -228,44 +215,35 @@
"N2IrpM": "Confirm",
"NAidKb": "Notifications",
"NAuFNH": "You already have a subscription of this type, please renew or pay",
"NNSu3d": "Import Twitter Follows",
"NdOYJJ": "Hmm nothing here.. Checkout {newUsersPage} to follow some recommended nostrich's!",
"NepkXH": "Can't vote with {amount} sats, please set a different default zap amount",
"NfNk2V": "Your private key",
"NndBJE": "New users page",
"O8Z8t9": "Show More",
"O9GTIc": "Profile picture",
"OEW7yJ": "Zaps",
"OKhRC6": "Share",
"OLEm6z": "Unknown login error",
"OQSOJF": "Get a free nostr address",
"OQXnew": "You subscription is still active, you can't renew yet",
"ORGv1Q": "Created",
"Oq/kVn": "Name-squatting and impersonation is not allowed. {site} and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule.",
"P/xrLk": "Secure your private key with a PIN, ensuring enhanced protection on {site}. You'll be prompted to enter this PIN each time you access the site.",
"P61BTu": "Copy Event JSON",
"P7FD0F": "System (Default)",
"P7nJT9": "Total today (UTC): {amount} sats",
"PCSt5T": "Preferences",
"PJeJFc": "Summary",
"PLSbmL": "Your mnemonic phrase",
"PaN7t3": "Preview on {site}",
"PamNxw": "Unknown file header: {name}",
"Pe0ogR": "Theme",
"PrsIg7": "Reactions will be shown on every page, if disabled no reactions will be shown",
"QDFTjG": "{n} Relays",
"QWhotP": "Zap Pool only works if you use one of the supported wallet connections (WebLN, LNC, LNDHub or Nostr Wallet Connect)",
"QawghE": "You can change your username at any point.",
"QxCuTo": "Art by {name}",
"Qxv0B2": "You currently have {number} sats in your zap pool.",
"R/6nsx": "Subscription",
"R81upa": "People you follow",
"RDZVQL": "Check",
"RSr2uB": "Username must only contain lowercase letters and numbers",
"RahCRH": "Expired",
"RfhLwC": "By: {author}",
"RhDAoS": "Are you sure you want to delete {id}",
"RjpoYG": "Recent",
"RkW5we": "Bitcoin",
"RoOyAh": "Relays",
"Rs4kCE": "Bookmark",
"RwFaYs": "Sort",
@ -276,11 +254,14 @@
"SYQtZ7": "LN Address Proxy",
"ShdEie": "Mark all read",
"Sjo1P4": "Custom",
"SmuYUd": "What should we call you?",
"Ss0sWu": "Pay Now",
"StKzTE": "The author has marked this note as a <i>sensitive topic</i>",
"TDR5ge": "Media in notes will automatically be shown for selected people, otherwise only the link will show",
"TJo5E6": "Preview",
"TP/cMX": "Ended",
"TaeBqw": "Sign in with Nostr Extension",
"TdtZQ5": "Crypto",
"TpgeGw": "Hex Salt..",
"Tpy00S": "People",
"U1aPPi": "Stop listening",
@ -289,9 +270,9 @@
"UNjfWJ": "Check all event signatures received from relays",
"UT7Nkj": "New Chat",
"UUPFlt": "Users must accept the content warning to show the content of your note.",
"Ub+AGc": "Sign In",
"Up5U7K": "Block",
"UrKTqQ": "You have an active iris.to account",
"VBadwB": "Hmm, can't find a key manager extension.. try reloading the page.",
"VN0+Fz": "Balance: {amount} sats",
"VOjC1i": "Pick which upload service you want to upload attachments to",
"VR5eHw": "Public key (npub/nprofile)",
@ -299,21 +280,18 @@
"VlJkSk": "{n} muted",
"VnXp8Z": "Avatar",
"VvaJst": "View Wallets",
"Vx7Zm2": "How do keys work?",
"W1yoZY": "It looks like you dont have any subscriptions, you can get one {link}",
"W2PiAr": "{n} Blocked",
"W9355R": "Unmute",
"WONP5O": "Find your twitter follows on nostr (Data provided by {provider})",
"WmZhfL": "Automatically translate notes to your local language",
"WvGmZT": "npub / nprofile / nostr address",
"WxthCV": "e.g. Jack",
"X6tipZ": "Sign in with key",
"X7xU8J": "nsec, npub, nip-05, hex, mnemonic",
"XECMfW": "Send usage metrics",
"XICsE8": "File hosts",
"XgWvGA": "Reactions",
"Xopqkl": "Your default zap amount is {number} sats, example values are calculated from this.",
"XrSk2j": "Redeem",
"XzF0aC": "Key manager extensions are more secure and allow you to easily login to any Nostr client, here are some well known extensions:",
"YDURw6": "Service URL",
"YXA3AH": "Enable reactions",
"Z4BMCZ": "Enter pairing phrase",
@ -321,9 +299,11 @@
"ZLmyG9": "Contributors",
"ZS+jRE": "Send zap splits to",
"Zff6lu": "Username iris.to/<b>{name}</b> is reserved for you!",
"Zr5TMx": "Setup profile",
"a+6cHB": "Derogatory",
"a5UPxh": "Fund developers and platforms providing NIP-05 verification services",
"a7TDNm": "Notes will stream in real time into global and notes tab",
"aHje0o": "Name or nym",
"aMaLBK": "Supported Extensions",
"aWpBzj": "Show more",
"b12Goz": "Mnemonic",
"b5vAk0": "Your handle will act like a lightning address and will redirect to your chosen LNURL or Lightning address",
@ -331,13 +311,12 @@
"bQdA2k": "Sensitive Content",
"bep9C3": "Public Key",
"bfvyfs": "Anon",
"brAXSu": "Pick a username",
"bxv59V": "Just now",
"c+JYNI": "No thanks",
"c+oiJe": "Install Extension",
"c35bj2": "If you have an enquiry about your NIP-05 order please DM {link}",
"c3g2hL": "Broadcast Again",
"cFbU1B": "Using Alby? Go to {link} to get your NWC config!",
"cHCwbF": "Photography",
"cPIKU2": "Following",
"cQfLWb": "URL..",
"cWx9t8": "Mute all",
@ -345,6 +324,7 @@
"cuP16y": "Multi account support",
"cuV2gK": "name is registered",
"cyR7Kh": "Back",
"d+6YsV": "Lists to mute:",
"d6CyG5": "History",
"d7d0/x": "LN Address",
"dOQCL8": "Display name",
@ -353,6 +333,7 @@
"e61Jf3": "Coming soon",
"e7VmYP": "Enter pin to unlock your private key",
"e7qqly": "Mark All Read",
"eF0Re7": "Use a nostr signer extension to sign in",
"eHAneD": "Reaction emoji",
"eJj8HD": "Get Verified",
"eSzf2G": "A single zap of {nIn} sats will allocate {nOut} sats to the zap pool.",
@ -361,6 +342,7 @@
"fBlba3": "Thanks for using {site}, please consider donating if you can.",
"fOksnD": "Can't vote because LNURL service does not support zaps",
"fWZYP5": "Pinned",
"fX5RYm": "Pick a few topics of interest",
"filwqD": "Read",
"fjAcWo": "Gift Wraps",
"flnGvv": "What's on your mind?",
@ -368,15 +350,14 @@
"fsB/4p": "Saved",
"g5pX+a": "About",
"g985Wp": "Failed to send vote",
"gBdUXk": "Save your keys!",
"gDzDRs": "Emoji to send when reactiong to a note",
"gXgY3+": "Not all clients support this yet",
"gczcC5": "Subscribe",
"geppt8": "{count} ({count2} in memory)",
"gjBiyj": "Loading...",
"grQ+mI": "Proof of Work",
"h7jvCs": "{site} is more fun together!",
"h8XMJL": "Badges",
"hK5ZDk": "the world",
"hMzcSq": "Messages",
"hRTfTR": "PRO",
"hY4lzx": "Supports",
@ -385,7 +366,6 @@
"hniz8Z": "here",
"i/dBAR": "Zap Pool",
"iCqGww": "Reactions ({n})",
"iDGAbc": "Get a Snort identifier",
"iEoXYx": "DeepL translations",
"iGT1eE": "Prevent fake accounts from imitating you",
"iNWbVV": "Handle",
@ -396,25 +376,23 @@
"izWS4J": "Unfollow",
"jA3OE/": "{n,plural,=1{{n} sat} other{{n} sats}}",
"jAmfGl": "Your {site_name} subscription is expired",
"jHa/ko": "Clean up your feed",
"jMzO1S": "Internal error: {msg}",
"jfV8Wr": "Back",
"juhqvW": "Improve login security with browser extensions",
"jvo0vs": "Save",
"jzgQ2z": "{n} Reactions",
"k2veDA": "Write",
"k7+5Ny": "Hate Speech",
"k7sKNy": "Our very own NIP-05 verification service, help support the development of this site and get a shiny special badge on our site!",
"kEZUR8": "Register an Iris username",
"kJYo0u": "{n,plural,=0{{name} reposted} other{{name} & {n} others reposted}}",
"kTLGM2": "{site} is designed to have a similar experience to Twitter.",
"kaaf1E": "now",
"kuPHYE": "{n,plural,=0{{name} liked} other{{name} & {n} others liked}}",
"l+ikU1": "Everything in {plan}",
"lBboHo": "If you want to try out some others, check out {link} for more!",
"lCILNz": "Buy Now",
"lD3+8a": "Pay",
"lPWASz": "Snort nostr address",
"lTbT3s": "Wallet password",
"lVKH7C": "What is {site} and how does it work?",
"lgg1KN": "account page",
"ll3xBp": "Image proxy service",
"lnaT9F": "Following {n}",
@ -430,20 +408,13 @@
"n1Whvj": "Switch",
"nDejmx": "Unblock",
"nGBrvw": "Bookmarks",
"nN9XTz": "Share your thoughts with {link}",
"nOaArs": "Setup Profile",
"ncbgUU": "{site} is a Nostr UI, nostr is a decentralised protocol for saving and distributing \"notes\".",
"nihgfo": "Listen to this article",
"nn1qb3": "Your donations are greatly appreciated",
"nwZXeh": "{n} blocked",
"o6Uy3d": "Only the secret key can be used to publish (sign events), everything else logs you in read-only mode.",
"o7e+nJ": "{n} followers",
"oJ+JJN": "Nothing found :/",
"odFwjL": "Follows only",
"odhABf": "Login",
"ojzbwv": "Hey, it looks like you dont have a Nostr Address yet, you should get one! Check out {link}",
"osUr8O": "You can also use these extensions to login to most Nostr sites.",
"oxCa4R": "Getting an identifier helps confirm the real you to people who know you. Many people can have a username @jack, but there is only one jack@cash.app.",
"p4N05H": "Upload",
"p85Uwy": "Active Subscriptions",
"pI+77w": "Downloadable backups from Snort relay",
@ -459,16 +430,14 @@
"qkvYUb": "Add to Profile",
"qmJ8kD": "Translation failed",
"qtWLmt": "Like",
"qydxOd": "Science",
"qz9fty": "Incorrect pin",
"r3C4x/": "Software",
"r5srDR": "Enter wallet password",
"rT14Ow": "Add Relays",
"rbrahO": "Close",
"reJ6SM": "It is recommended to use one of the following browser extensions if you are on a desktop computer to secure your key:",
"rfuMjE": "(Default)",
"rmdsT4": "{n} days",
"rrfdTe": "This is the same technology which is used by Bitcoin and has been proven to be extremely secure.",
"rudscU": "Failed to load follows, please try again later",
"rx1i0i": "Short link",
"sKDn4e": "Show Badges",
"sUNhQE": "user",
@ -482,39 +451,39 @@
"u+LyXc": "Interactions",
"u/vOPu": "Paid",
"u4bHcR": "Check out the code here: {link}",
"uCk8r+": "Already have an account?",
"uKqSN+": "Follows Feed",
"uSV4Ti": "Reposts need to be manually confirmed",
"uc0din": "Send sats splits to",
"ugyJnE": "Sending notes and other stuff",
"usAvMr": "Edit Profile",
"ut+2Cd": "Get a partner identifier",
"v8lolG": "Start chat",
"vB3oQ/": "Must be a contact list or pubkey list",
"vN5UH8": "Profile Image",
"vOKedj": "{n,plural,=1{& {n} other} other{& {n} others}}",
"vZ4quW": "NIP-05 is a DNS based verification spec which helps to validate you as a real user.",
"vhlWFg": "Poll Options",
"vlbWtt": "Get a free one",
"vrTOHJ": "{amount} sats",
"vxwnbh": "Amount of work to apply to all published events",
"w1Fanr": "Business",
"w6qrwX": "NSFW",
"wEQDC6": "Edit",
"wLtRCF": "Your key",
"wSZR47": "Submit",
"wWLwvh": "Anon",
"wih7iJ": "name is blocked",
"wofVHy": "Moderation",
"wqyN/i": "Find out more info about {service} at {link}",
"wtLjP6": "Copy ID",
"wuMvI5": "{site_name} Developers",
"x/Fx2P": "Fund the services that you use by splitting a portion of all your zaps into a pool of funds!",
"x82IOl": "Mute",
"xIcAOU": "Votes by {type}",
"xIoGG9": "Go to",
"xJ9n2N": "Your public key",
"xKflGN": "{username}''s Follows on Nostr",
"xQtL3v": "Unlock",
"xaj9Ba": "Provider",
"xbVgIm": "Automatically load media",
"xhQMeQ": "Expires",
"xl4s/X": "Additional Terms:",
"xmcVZ0": "Search",
"y1Z3or": "Language",
"yCLnBC": "LNURL or Lightning Address",
@ -524,7 +493,6 @@
"zINlao": "Owner",
"zQvVDJ": "All",
"zcaOTs": "Zap amount in sats",
"zjJZBd": "You're ready!",
"zm6qS1": "{n} mins to read",
"zonsdq": "Failed to load LNURL service",
"zvCDao": "Automatically show latest notes",

View File

@ -36,7 +36,7 @@
"@snort/shared": "^1.0.7",
"@stablelib/xchacha20": "^1.0.1",
"debug": "^4.3.4",
"events": "^3.3.0",
"eventemitter3": "^5.0.1",
"isomorphic-ws": "^5.0.0",
"uuid": "^9.0.0",
"ws": "^8.14.0"

View File

@ -3243,7 +3243,7 @@ __metadata:
"@types/uuid": ^9.0.2
"@types/ws": ^8.5.5
debug: ^4.3.4
events: ^3.3.0
eventemitter3: ^5.0.1
isomorphic-ws: ^5.0.0
jest: ^29.5.0
jest-environment-jsdom: ^29.5.0
@ -7272,6 +7272,13 @@ __metadata:
languageName: node
linkType: hard
"eventemitter3@npm:^5.0.1":
version: 5.0.1
resolution: "eventemitter3@npm:5.0.1"
checksum: 543d6c858ab699303c3c32e0f0f47fc64d360bf73c3daf0ac0b5079710e340d6fe9f15487f94e66c629f5f82cd1a8678d692f3dbb6f6fcd1190e1b97fcad36f8
languageName: node
linkType: hard
"events@npm:^3.2.0, events@npm:^3.3.0":
version: 3.3.0
resolution: "events@npm:3.3.0"