feat: sign up flow v2
This commit is contained in:
parent
a2bcb936ef
commit
d119a5f626
@ -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} />;
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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}>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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 />,
|
||||
},
|
||||
];
|
@ -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" },
|
||||
});
|
42
packages/app/src/Pages/onboarding/discover.tsx
Normal file
42
packages/app/src/Pages/onboarding/discover.tsx
Normal 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>
|
||||
);
|
||||
}
|
50
packages/app/src/Pages/onboarding/index.css
Normal file
50
packages/app/src/Pages/onboarding/index.css
Normal 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;
|
||||
}
|
74
packages/app/src/Pages/onboarding/index.tsx
Normal file
74
packages/app/src/Pages/onboarding/index.tsx
Normal 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>;
|
156
packages/app/src/Pages/onboarding/moderation.tsx
Normal file
156
packages/app/src/Pages/onboarding/moderation.tsx
Normal 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>
|
||||
);
|
||||
}
|
47
packages/app/src/Pages/onboarding/profile.tsx
Normal file
47
packages/app/src/Pages/onboarding/profile.tsx
Normal 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>
|
||||
);
|
||||
}
|
151
packages/app/src/Pages/onboarding/start.tsx
Normal file
151
packages/app/src/Pages/onboarding/start.tsx
Normal 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>
|
||||
);
|
||||
}
|
85
packages/app/src/Pages/onboarding/topics.tsx
Normal file
85
packages/app/src/Pages/onboarding/topics.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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,
|
||||
|
@ -660,6 +660,10 @@ div.form-col {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: var(--warning);
|
||||
}
|
||||
|
||||
.bg-error {
|
||||
background-color: var(--error);
|
||||
}
|
||||
|
@ -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>;
|
||||
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user