Merge pull request 'Iris nip-05 registration' (#638) from mmalmi/snort:main into main
Reviewed-on: Kieran/snort#638
This commit is contained in:
commit
0a0c67dc89
@ -1,2 +1,2 @@
|
|||||||
/*
|
/*
|
||||||
Content-Security-Policy: default-src 'self'; manifest-src *; child-src 'none'; worker-src 'self'; frame-src youtube.com www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com https://open.spotify.com https://player.twitch.tv https://embed.music.apple.com https://nostrnests.com https://embed.wavlake.com; style-src 'self' 'unsafe-inline'; connect-src *; img-src * data: blob:; font-src 'self'; media-src * blob:; script-src 'self' 'wasm-unsafe-eval' https://analytics.v0l.io https://platform.twitter.com https://embed.tidal.com;
|
Content-Security-Policy: default-src 'self'; manifest-src *; child-src 'none'; worker-src 'self'; frame-src youtube.com www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com https://open.spotify.com https://player.twitch.tv https://embed.music.apple.com https://nostrnests.com https://embed.wavlake.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; connect-src *; img-src * data: blob:; font-src 'self'; media-src * blob:; script-src 'self' 'wasm-unsafe-eval' https://analytics.v0l.io https://platform.twitter.com https://embed.tidal.com https://challenges.cloudflare.com;
|
@ -15,6 +15,11 @@ export const Day = Hour * 24;
|
|||||||
*/
|
*/
|
||||||
export const ApiHost = "https://api.snort.social";
|
export const ApiHost = "https://api.snort.social";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iris api for free nip05 names
|
||||||
|
*/
|
||||||
|
export const IrisHost = "https://api.iris.to";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LibreTranslate endpoint
|
* LibreTranslate endpoint
|
||||||
*/
|
*/
|
||||||
|
31
packages/app/src/Element/IrisAccount/AccountName.tsx
Normal file
31
packages/app/src/Element/IrisAccount/AccountName.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import FormattedMessage from "Element/FormattedMessage";
|
||||||
|
|
||||||
|
export default function AccountName({ name = "", link = true }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<FormattedMessage defaultMessage="Username" />: <b>{name}</b>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<FormattedMessage defaultMessage="Short link" />:{" "}
|
||||||
|
{link ? (
|
||||||
|
<a
|
||||||
|
href={`https://iris.to/${name}`}
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate(`/${name}`);
|
||||||
|
}}>
|
||||||
|
iris.to/{name}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<>iris.to/{name}</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<FormattedMessage defaultMessage="Nostr address (nip05)" />: <b>{name}@iris.to</b>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
74
packages/app/src/Element/IrisAccount/ActiveAccount.tsx
Normal file
74
packages/app/src/Element/IrisAccount/ActiveAccount.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import AccountName from "./AccountName";
|
||||||
|
import useLogin from "../../Hooks/useLogin";
|
||||||
|
import { useUserProfile } from "@snort/system-react";
|
||||||
|
import { System } from "../../index";
|
||||||
|
import { UserCache } from "../../Cache";
|
||||||
|
import useEventPublisher from "../../Hooks/useEventPublisher";
|
||||||
|
import { mapEventToProfile } from "@snort/system";
|
||||||
|
import FormattedMessage from "Element/FormattedMessage";
|
||||||
|
|
||||||
|
export default function ActiveAccount({ name = "", setAsPrimary = () => {} }) {
|
||||||
|
const { publicKey, readonly } = useLogin(s => ({
|
||||||
|
publicKey: s.publicKey,
|
||||||
|
readonly: s.readonly,
|
||||||
|
}));
|
||||||
|
const profile = useUserProfile(publicKey);
|
||||||
|
const publisher = useEventPublisher();
|
||||||
|
|
||||||
|
async function saveProfile(nip05: string) {
|
||||||
|
if (readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// copy user object and delete internal fields
|
||||||
|
const userCopy = {
|
||||||
|
...(profile || {}),
|
||||||
|
nip05,
|
||||||
|
} as Record<string, string | number | undefined | boolean>;
|
||||||
|
delete userCopy["loaded"];
|
||||||
|
delete userCopy["created"];
|
||||||
|
delete userCopy["pubkey"];
|
||||||
|
delete userCopy["npub"];
|
||||||
|
delete userCopy["deleted"];
|
||||||
|
delete userCopy["zapService"];
|
||||||
|
delete userCopy["isNostrAddressValid"];
|
||||||
|
console.debug(userCopy);
|
||||||
|
|
||||||
|
if (publisher) {
|
||||||
|
const ev = await publisher.metadata(userCopy);
|
||||||
|
System.BroadcastEvent(ev);
|
||||||
|
|
||||||
|
const newProfile = mapEventToProfile(ev);
|
||||||
|
if (newProfile) {
|
||||||
|
await UserCache.update(newProfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
const newNip = name + "@iris.to";
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
saveProfile(newNip);
|
||||||
|
}, 2000);
|
||||||
|
if (profile) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (profile.nip05 !== newNip) {
|
||||||
|
saveProfile(newNip);
|
||||||
|
setAsPrimary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="negative">
|
||||||
|
<FormattedMessage defaultMessage="You have an active iris.to account" />:
|
||||||
|
<AccountName name={name} />
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<button type="button" onClick={onClick}>
|
||||||
|
<FormattedMessage defaultMessage="Set as primary Nostr address (nip05)" />
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
307
packages/app/src/Element/IrisAccount/IrisAccount.tsx
Normal file
307
packages/app/src/Element/IrisAccount/IrisAccount.tsx
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { Component, FormEvent } from "react";
|
||||||
|
import { LoginStore } from "Login";
|
||||||
|
|
||||||
|
import AccountName from "./AccountName";
|
||||||
|
import ActiveAccount from "./ActiveAccount";
|
||||||
|
import ReservedAccount from "./ReservedAccount";
|
||||||
|
import { ProfileLoader } from "../../index";
|
||||||
|
import FormattedMessage from "Element/FormattedMessage";
|
||||||
|
import { injectIntl } from "react-intl";
|
||||||
|
import messages from "Element/messages";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
cf_turnstile_callback: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
intl: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO split into smaller components
|
||||||
|
class IrisAccount extends Component<Props> {
|
||||||
|
state = {
|
||||||
|
irisToActive: false,
|
||||||
|
existing: null as any,
|
||||||
|
profile: null as any,
|
||||||
|
newUserName: "",
|
||||||
|
newUserNameValid: false,
|
||||||
|
error: null as any,
|
||||||
|
showChallenge: false,
|
||||||
|
invalidUsernameMessage: null as any,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let view: any;
|
||||||
|
|
||||||
|
if (this.state.irisToActive) {
|
||||||
|
const username = this.state.profile?.nip05.split("@")[0];
|
||||||
|
view = <AccountName name={username} />;
|
||||||
|
} else if (this.state.existing && this.state.existing.confirmed) {
|
||||||
|
view = (
|
||||||
|
<ActiveAccount name={this.state.existing.name} setAsPrimary={() => this.setState({ irisToActive: true })} />
|
||||||
|
);
|
||||||
|
} else if (this.state.existing) {
|
||||||
|
view = (
|
||||||
|
<ReservedAccount
|
||||||
|
name={this.state.existing.name}
|
||||||
|
enableReserved={() => this.enableReserved()}
|
||||||
|
declineReserved={() => this.declineReserved()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (this.state.error) {
|
||||||
|
view = <div className="error">Error: {this.state.error}</div>;
|
||||||
|
} else if (this.state.showChallenge) {
|
||||||
|
window.cf_turnstile_callback = (token: any) => this.register(token);
|
||||||
|
view = (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="cf-turnstile"
|
||||||
|
data-sitekey={
|
||||||
|
["iris.to", "beta.iris.to", "snort.social"].includes(window.location.hostname)
|
||||||
|
? "0x4AAAAAAACsEd8XuwpPTFwz"
|
||||||
|
: "3x00000000000000000000FF"
|
||||||
|
}
|
||||||
|
data-callback="cf_turnstile_callback"></div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
view = (
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Register an Iris username" /> (iris.to/username)
|
||||||
|
</p>
|
||||||
|
<form onSubmit={e => this.showChallenge(e)}>
|
||||||
|
<div className="flex g8">
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
value={this.state.newUserName}
|
||||||
|
onInput={e => this.onNewUserNameChange(e)}
|
||||||
|
/>
|
||||||
|
<button type="submit">
|
||||||
|
<FormattedMessage defaultMessage="Register" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{this.state.newUserNameValid ? (
|
||||||
|
<>
|
||||||
|
<span className="success">
|
||||||
|
<FormattedMessage defaultMessage="Username is available" />
|
||||||
|
</span>
|
||||||
|
<AccountName name={this.state.newUserName} link={false} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="error">{this.state.invalidUsernameMessage}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
<FormattedMessage defaultMessage="Iris.to account" />
|
||||||
|
</h3>
|
||||||
|
{view}
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/irislib/faq#iris-username">FAQ</a>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onNewUserNameChange(e: any) {
|
||||||
|
const newUserName = e.target.value;
|
||||||
|
if (newUserName.length === 0) {
|
||||||
|
this.setState({
|
||||||
|
newUserName,
|
||||||
|
newUserNameValid: false,
|
||||||
|
invalidUsernameMessage: "",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newUserName.length < 8 || newUserName.length > 15) {
|
||||||
|
this.setState({
|
||||||
|
newUserName,
|
||||||
|
newUserNameValid: false,
|
||||||
|
invalidUsernameMessage: this.props.intl.formatMessage(messages.IrisUserNameLengthError),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!newUserName.match(/^[a-z0-9_.]+$/)) {
|
||||||
|
this.setState({
|
||||||
|
newUserName,
|
||||||
|
newUserNameValid: false,
|
||||||
|
invalidUsernameMessage: this.props.intl.formatMessage(messages.IrisUserNameFormatError),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
newUserName,
|
||||||
|
invalidUsernameMessage: "",
|
||||||
|
});
|
||||||
|
this.checkAvailabilityFromAPI(newUserName);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAvailabilityFromAPI = async (name: string) => {
|
||||||
|
const res = await fetch(`https://api.iris.to/user/available?name=${encodeURIComponent(name)}`);
|
||||||
|
if (name !== this.state.newUserName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.status < 500) {
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.available) {
|
||||||
|
this.setState({ newUserNameValid: true });
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
newUserNameValid: false,
|
||||||
|
invalidUsernameMessage: json.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
newUserNameValid: false,
|
||||||
|
invalidUsernameMessage: "Error checking username availability",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
showChallenge(e: FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!this.state.newUserNameValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ showChallenge: true }, () => {
|
||||||
|
// Dynamically injecting Cloudflare script
|
||||||
|
if (!document.querySelector('script[src="https://challenges.cloudflare.com/turnstile/v0/api.js"]')) {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
|
||||||
|
script.async = true;
|
||||||
|
script.defer = true;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async register(cfToken: any) {
|
||||||
|
console.log("register", cfToken);
|
||||||
|
const login = LoginStore.snapshot();
|
||||||
|
const publisher = LoginStore.getPublisher(login.id);
|
||||||
|
const event = await publisher?.note(`iris.to/${this.state.newUserName}`);
|
||||||
|
// post signed event as request body to https://api.iris.to/user/confirm_user
|
||||||
|
const res = await fetch("https://api.iris.to/user/signup", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ event, cfToken }),
|
||||||
|
});
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.setState({
|
||||||
|
error: null,
|
||||||
|
existing: {
|
||||||
|
confirmed: true,
|
||||||
|
name: this.state.newUserName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
delete window.cf_turnstile_callback;
|
||||||
|
} else {
|
||||||
|
res
|
||||||
|
.json()
|
||||||
|
.then(json => {
|
||||||
|
this.setState({ error: json.message || "error" });
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.setState({ error: "error" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async enableReserved() {
|
||||||
|
const login = LoginStore.snapshot();
|
||||||
|
const publisher = LoginStore.getPublisher(login.id);
|
||||||
|
const event = await publisher?.note(`iris.to/${this.state.newUserName}`);
|
||||||
|
// post signed event as request body to https://api.iris.to/user/confirm_user
|
||||||
|
const res = await fetch("https://api.iris.to/user/confirm_user", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(event),
|
||||||
|
});
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.setState({
|
||||||
|
error: null,
|
||||||
|
existing: { confirmed: true, name: this.state.existing.name },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res
|
||||||
|
.json()
|
||||||
|
.then(json => {
|
||||||
|
this.setState({ error: json.message || "error" });
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.setState({ error: "error" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async declineReserved() {
|
||||||
|
if (!confirm(`Are you sure you want to decline iris.to/${name}?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const login = LoginStore.snapshot();
|
||||||
|
const publisher = LoginStore.getPublisher(login.id);
|
||||||
|
const event = await publisher?.note(`decline iris.to/${this.state.newUserName}`);
|
||||||
|
const res = await fetch("https://api.iris.to/user/decline_user", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(event),
|
||||||
|
});
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.setState({ confirmSuccess: false, error: null, existing: null });
|
||||||
|
} else {
|
||||||
|
res
|
||||||
|
.json()
|
||||||
|
.then(json => {
|
||||||
|
this.setState({ error: json.message || "error" });
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.setState({ error: "error" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const session = LoginStore.snapshot();
|
||||||
|
const myPub = session.publicKey;
|
||||||
|
ProfileLoader.Cache.hook(() => {
|
||||||
|
const profile = ProfileLoader.Cache.getFromCache(myPub);
|
||||||
|
const irisToActive = profile && profile.nip05 && profile.nip05.endsWith("@iris.to");
|
||||||
|
this.setState({ profile, irisToActive });
|
||||||
|
if (profile && !irisToActive) {
|
||||||
|
this.checkExistingAccount(myPub);
|
||||||
|
}
|
||||||
|
}, myPub);
|
||||||
|
this.checkExistingAccount(myPub);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkExistingAccount(pub: any) {
|
||||||
|
const res = await fetch(`https://api.iris.to/user/find?public_key=${pub}`);
|
||||||
|
if (res.status === 200) {
|
||||||
|
const json = await res.json();
|
||||||
|
this.setState({ existing: json });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(IrisAccount);
|
26
packages/app/src/Element/IrisAccount/ReservedAccount.tsx
Normal file
26
packages/app/src/Element/IrisAccount/ReservedAccount.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import AccountName from "./AccountName";
|
||||||
|
import FormattedMessage from "Element/FormattedMessage";
|
||||||
|
|
||||||
|
export default function ReservedAccount({ name = "", enableReserved = () => {}, declineReserved = () => {} }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className="success">
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Username iris.to/<b>{name}</b> is reserved for you!"
|
||||||
|
values={{ name, b: s => <b>{s}</b> }}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<AccountName name={name} link={false} />
|
||||||
|
<p>
|
||||||
|
<button className="btn btn-sm btn-primary" onClick={() => enableReserved()}>
|
||||||
|
<FormattedMessage defaultMessage="Yes please" />
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button className="btn btn-sm btn-neutral" onClick={() => declineReserved()}>
|
||||||
|
<FormattedMessage defaultMessage="No thanks" />
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -98,4 +98,6 @@ export default defineMessages({
|
|||||||
ConfirmUnpin: { defaultMessage: "Are you sure you want to unpin this note?" },
|
ConfirmUnpin: { defaultMessage: "Are you sure you want to unpin this note?" },
|
||||||
ReactionsLink: { defaultMessage: "{n} Reactions" },
|
ReactionsLink: { defaultMessage: "{n} Reactions" },
|
||||||
ReBroadcast: { defaultMessage: "Broadcast Again" },
|
ReBroadcast: { defaultMessage: "Broadcast Again" },
|
||||||
|
IrisUserNameLengthError: { defaultMessage: "Name must be between 1 and 32 characters" },
|
||||||
|
IrisUserNameFormatError: { defaultMessage: "Username must only contain lowercase letters and numbers" },
|
||||||
});
|
});
|
||||||
|
38
packages/app/src/Pages/FreeNostrAddressPage.tsx
Normal file
38
packages/app/src/Pages/FreeNostrAddressPage.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import FormattedMessage from "@snort/app/src/Element/FormattedMessage";
|
||||||
|
|
||||||
|
/*
|
||||||
|
import { IrisHost } from "Const";
|
||||||
|
import Nip5Service from "Element/Nip5Service";
|
||||||
|
*/
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
import IrisAccount from "../Element/IrisAccount/IrisAccount";
|
||||||
|
|
||||||
|
export default function FreeNostrAddressPage() {
|
||||||
|
return (
|
||||||
|
<div className="main-content p">
|
||||||
|
<h2>
|
||||||
|
<FormattedMessage defaultMessage="Get a free nostr address" />
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage {...messages.Nip05} />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage {...messages.Nip05Pros} />
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<FormattedMessage {...messages.AvoidImpersonators} />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<FormattedMessage {...messages.EasierToFind} />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<FormattedMessage {...messages.Funding} />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<IrisAccount />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -162,7 +162,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
<button className="flex f-center" type="button" onClick={() => navigate("/nostr-address")}>
|
<button className="flex f-center" type="button" onClick={() => navigate("/nostr-address")}>
|
||||||
<FormattedMessage defaultMessage="Buy nostr address" />
|
<FormattedMessage defaultMessage="Buy nostr address" />
|
||||||
</button>
|
</button>
|
||||||
<button className="flex f-center secondary" type="button" onClick={() => navigate("/nostr-address")}>
|
<button className="flex f-center secondary" type="button" onClick={() => navigate("/free-nostr-address")}>
|
||||||
<FormattedMessage defaultMessage="Get a free one" />
|
<FormattedMessage defaultMessage="Get a free one" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,6 +45,7 @@ import { db } from "Db";
|
|||||||
import { preload, RelayMetrics, UserCache, UserRelays } from "Cache";
|
import { preload, RelayMetrics, UserCache, UserRelays } from "Cache";
|
||||||
import { LoginStore } from "Login";
|
import { LoginStore } from "Login";
|
||||||
import { SnortDeckLayout } from "Pages/DeckLayout";
|
import { SnortDeckLayout } from "Pages/DeckLayout";
|
||||||
|
import FreeNostrAddressPage from "./Pages/FreeNostrAddressPage";
|
||||||
|
|
||||||
const WasmQueryOptimizer = {
|
const WasmQueryOptimizer = {
|
||||||
expandFilter: (f: ReqFilter) => {
|
expandFilter: (f: ReqFilter) => {
|
||||||
@ -163,6 +164,10 @@ export const router = createBrowserRouter([
|
|||||||
element: <SettingsPage />,
|
element: <SettingsPage />,
|
||||||
children: SettingsRoutes,
|
children: SettingsRoutes,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/free-nostr-address",
|
||||||
|
element: <FreeNostrAddressPage />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/nostr-address",
|
path: "/nostr-address",
|
||||||
element: <NostrAddressPage />,
|
element: <NostrAddressPage />,
|
||||||
|
@ -157,6 +157,9 @@
|
|||||||
"4L2vUY": {
|
"4L2vUY": {
|
||||||
"defaultMessage": "Your new NIP-05 handle is:"
|
"defaultMessage": "Your new NIP-05 handle is:"
|
||||||
},
|
},
|
||||||
|
"4MBtMa": {
|
||||||
|
"defaultMessage": "Name must be between 1 and 32 characters"
|
||||||
|
},
|
||||||
"4OB335": {
|
"4OB335": {
|
||||||
"defaultMessage": "Dislike"
|
"defaultMessage": "Dislike"
|
||||||
},
|
},
|
||||||
@ -329,6 +332,9 @@
|
|||||||
"BcGMo+": {
|
"BcGMo+": {
|
||||||
"defaultMessage": "Notes hold text content, the most popular usage of these notes is to store \"tweet like\" messages."
|
"defaultMessage": "Notes hold text content, the most popular usage of these notes is to store \"tweet like\" messages."
|
||||||
},
|
},
|
||||||
|
"BjNwZW": {
|
||||||
|
"defaultMessage": "Nostr address (nip05)"
|
||||||
|
},
|
||||||
"C1LjMx": {
|
"C1LjMx": {
|
||||||
"defaultMessage": "Lightning Donation"
|
"defaultMessage": "Lightning Donation"
|
||||||
},
|
},
|
||||||
@ -401,6 +407,9 @@
|
|||||||
"EcZF24": {
|
"EcZF24": {
|
||||||
"defaultMessage": "Custom Relays"
|
"defaultMessage": "Custom Relays"
|
||||||
},
|
},
|
||||||
|
"EcfIwB": {
|
||||||
|
"defaultMessage": "Username is available"
|
||||||
|
},
|
||||||
"EcglP9": {
|
"EcglP9": {
|
||||||
"defaultMessage": "Key"
|
"defaultMessage": "Key"
|
||||||
},
|
},
|
||||||
@ -617,6 +626,9 @@
|
|||||||
"MWTx65": {
|
"MWTx65": {
|
||||||
"defaultMessage": "Default Page"
|
"defaultMessage": "Default Page"
|
||||||
},
|
},
|
||||||
|
"MiMipu": {
|
||||||
|
"defaultMessage": "Set as primary Nostr address (nip05)"
|
||||||
|
},
|
||||||
"Mrpkot": {
|
"Mrpkot": {
|
||||||
"defaultMessage": "Pay for subscription"
|
"defaultMessage": "Pay for subscription"
|
||||||
},
|
},
|
||||||
@ -626,6 +638,9 @@
|
|||||||
"MzRYWH": {
|
"MzRYWH": {
|
||||||
"defaultMessage": "Buying {item}"
|
"defaultMessage": "Buying {item}"
|
||||||
},
|
},
|
||||||
|
"Mzizei": {
|
||||||
|
"defaultMessage": "Iris.to account"
|
||||||
|
},
|
||||||
"N2IrpM": {
|
"N2IrpM": {
|
||||||
"defaultMessage": "Confirm"
|
"defaultMessage": "Confirm"
|
||||||
},
|
},
|
||||||
@ -662,6 +677,9 @@
|
|||||||
"OLEm6z": {
|
"OLEm6z": {
|
||||||
"defaultMessage": "Unknown login error"
|
"defaultMessage": "Unknown login error"
|
||||||
},
|
},
|
||||||
|
"OQSOJF": {
|
||||||
|
"defaultMessage": "Get a free nostr address"
|
||||||
|
},
|
||||||
"OQXnew": {
|
"OQXnew": {
|
||||||
"defaultMessage": "You subscription is still active, you can't renew yet"
|
"defaultMessage": "You subscription is still active, you can't renew yet"
|
||||||
},
|
},
|
||||||
@ -717,6 +735,9 @@
|
|||||||
"RDZVQL": {
|
"RDZVQL": {
|
||||||
"defaultMessage": "Check"
|
"defaultMessage": "Check"
|
||||||
},
|
},
|
||||||
|
"RSr2uB": {
|
||||||
|
"defaultMessage": "Username must only contain lowercase letters and numbers"
|
||||||
|
},
|
||||||
"RahCRH": {
|
"RahCRH": {
|
||||||
"defaultMessage": "Expired"
|
"defaultMessage": "Expired"
|
||||||
},
|
},
|
||||||
@ -789,6 +810,9 @@
|
|||||||
"Up5U7K": {
|
"Up5U7K": {
|
||||||
"defaultMessage": "Block"
|
"defaultMessage": "Block"
|
||||||
},
|
},
|
||||||
|
"UrKTqQ": {
|
||||||
|
"defaultMessage": "You have an active iris.to account"
|
||||||
|
},
|
||||||
"VBadwB": {
|
"VBadwB": {
|
||||||
"defaultMessage": "Hmm, can't find a key manager extension.. try reloading the page."
|
"defaultMessage": "Hmm, can't find a key manager extension.. try reloading the page."
|
||||||
},
|
},
|
||||||
@ -801,6 +825,9 @@
|
|||||||
"VR5eHw": {
|
"VR5eHw": {
|
||||||
"defaultMessage": "Public key (npub/nprofile)"
|
"defaultMessage": "Public key (npub/nprofile)"
|
||||||
},
|
},
|
||||||
|
"VcwrfF": {
|
||||||
|
"defaultMessage": "Yes please"
|
||||||
|
},
|
||||||
"VlJkSk": {
|
"VlJkSk": {
|
||||||
"defaultMessage": "{n} muted"
|
"defaultMessage": "{n} muted"
|
||||||
},
|
},
|
||||||
@ -877,6 +904,9 @@
|
|||||||
"ZS+jRE": {
|
"ZS+jRE": {
|
||||||
"defaultMessage": "Send zap splits to"
|
"defaultMessage": "Send zap splits to"
|
||||||
},
|
},
|
||||||
|
"Zff6lu": {
|
||||||
|
"defaultMessage": "Username iris.to/<b>{name}</b> is reserved for you!"
|
||||||
|
},
|
||||||
"Zr5TMx": {
|
"Zr5TMx": {
|
||||||
"defaultMessage": "Setup profile"
|
"defaultMessage": "Setup profile"
|
||||||
},
|
},
|
||||||
@ -913,6 +943,9 @@
|
|||||||
"bxv59V": {
|
"bxv59V": {
|
||||||
"defaultMessage": "Just now"
|
"defaultMessage": "Just now"
|
||||||
},
|
},
|
||||||
|
"c+JYNI": {
|
||||||
|
"defaultMessage": "No thanks"
|
||||||
|
},
|
||||||
"c+oiJe": {
|
"c+oiJe": {
|
||||||
"defaultMessage": "Install Extension",
|
"defaultMessage": "Install Extension",
|
||||||
"description": "Heading for install key manager extension"
|
"description": "Heading for install key manager extension"
|
||||||
@ -961,6 +994,9 @@
|
|||||||
"dOQCL8": {
|
"dOQCL8": {
|
||||||
"defaultMessage": "Display name"
|
"defaultMessage": "Display name"
|
||||||
},
|
},
|
||||||
|
"deEeEI": {
|
||||||
|
"defaultMessage": "Register"
|
||||||
|
},
|
||||||
"e61Jf3": {
|
"e61Jf3": {
|
||||||
"defaultMessage": "Coming soon"
|
"defaultMessage": "Coming soon"
|
||||||
},
|
},
|
||||||
@ -1109,6 +1145,9 @@
|
|||||||
"k7sKNy": {
|
"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!"
|
"defaultMessage": "Our very own NIP-05 verification service, help support the development of this site and get a shiny special badge on our site!"
|
||||||
},
|
},
|
||||||
|
"kEZUR8": {
|
||||||
|
"defaultMessage": "Register an Iris username"
|
||||||
|
},
|
||||||
"kJYo0u": {
|
"kJYo0u": {
|
||||||
"defaultMessage": "{n,plural,=0{{name} reposted} other{{name} & {n} others reposted}}"
|
"defaultMessage": "{n,plural,=0{{name} reposted} other{{name} & {n} others reposted}}"
|
||||||
},
|
},
|
||||||
@ -1292,6 +1331,9 @@
|
|||||||
"rudscU": {
|
"rudscU": {
|
||||||
"defaultMessage": "Failed to load follows, please try again later"
|
"defaultMessage": "Failed to load follows, please try again later"
|
||||||
},
|
},
|
||||||
|
"rx1i0i": {
|
||||||
|
"defaultMessage": "Short link"
|
||||||
|
},
|
||||||
"sKDn4e": {
|
"sKDn4e": {
|
||||||
"defaultMessage": "Show Badges"
|
"defaultMessage": "Show Badges"
|
||||||
},
|
},
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
"47FYwb": "Cancel",
|
"47FYwb": "Cancel",
|
||||||
"4IPzdn": "Primary Developers",
|
"4IPzdn": "Primary Developers",
|
||||||
"4L2vUY": "Your new NIP-05 handle is:",
|
"4L2vUY": "Your new NIP-05 handle is:",
|
||||||
|
"4MBtMa": "Name must be between 1 and 32 characters",
|
||||||
"4OB335": "Dislike",
|
"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",
|
"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",
|
"4Z3t5i": "Use imgproxy to compress images",
|
||||||
@ -107,6 +108,7 @@
|
|||||||
"BOr9z/": "Snort is an open source project built by passionate people in their free time",
|
"BOr9z/": "Snort is an open source project built by passionate people in their free time",
|
||||||
"BWpuKl": "Update",
|
"BWpuKl": "Update",
|
||||||
"BcGMo+": "Notes hold text content, the most popular usage of these notes is to store \"tweet like\" messages.",
|
"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",
|
"C1LjMx": "Lightning Donation",
|
||||||
"C5xzTC": "Premium",
|
"C5xzTC": "Premium",
|
||||||
"C81/uG": "Logout",
|
"C81/uG": "Logout",
|
||||||
@ -131,6 +133,7 @@
|
|||||||
"EWyQH5": "Global",
|
"EWyQH5": "Global",
|
||||||
"Ebl/B2": "Translate to {lang}",
|
"Ebl/B2": "Translate to {lang}",
|
||||||
"EcZF24": "Custom Relays",
|
"EcZF24": "Custom Relays",
|
||||||
|
"EcfIwB": "Username is available",
|
||||||
"EcglP9": "Key",
|
"EcglP9": "Key",
|
||||||
"EjFyoR": "On-chain Donation Address",
|
"EjFyoR": "On-chain Donation Address",
|
||||||
"EnCOBJ": "Buy",
|
"EnCOBJ": "Buy",
|
||||||
@ -202,9 +205,11 @@
|
|||||||
"MP54GY": "Wallet password",
|
"MP54GY": "Wallet password",
|
||||||
"MRp6Ly": "Twitter username",
|
"MRp6Ly": "Twitter username",
|
||||||
"MWTx65": "Default Page",
|
"MWTx65": "Default Page",
|
||||||
|
"MiMipu": "Set as primary Nostr address (nip05)",
|
||||||
"Mrpkot": "Pay for subscription",
|
"Mrpkot": "Pay for subscription",
|
||||||
"MuVeKe": "Buy nostr address",
|
"MuVeKe": "Buy nostr address",
|
||||||
"MzRYWH": "Buying {item}",
|
"MzRYWH": "Buying {item}",
|
||||||
|
"Mzizei": "Iris.to account",
|
||||||
"N2IrpM": "Confirm",
|
"N2IrpM": "Confirm",
|
||||||
"NAidKb": "Notifications",
|
"NAidKb": "Notifications",
|
||||||
"NAuFNH": "You already have a subscription of this type, please renew or pay",
|
"NAuFNH": "You already have a subscription of this type, please renew or pay",
|
||||||
@ -217,6 +222,7 @@
|
|||||||
"OEW7yJ": "Zaps",
|
"OEW7yJ": "Zaps",
|
||||||
"OKhRC6": "Share",
|
"OKhRC6": "Share",
|
||||||
"OLEm6z": "Unknown login error",
|
"OLEm6z": "Unknown login error",
|
||||||
|
"OQSOJF": "Get a free nostr address",
|
||||||
"OQXnew": "You subscription is still active, you can't renew yet",
|
"OQXnew": "You subscription is still active, you can't renew yet",
|
||||||
"ORGv1Q": "Created",
|
"ORGv1Q": "Created",
|
||||||
"P61BTu": "Copy Event JSON",
|
"P61BTu": "Copy Event JSON",
|
||||||
@ -235,6 +241,7 @@
|
|||||||
"R/6nsx": "Subscription",
|
"R/6nsx": "Subscription",
|
||||||
"R81upa": "People you follow",
|
"R81upa": "People you follow",
|
||||||
"RDZVQL": "Check",
|
"RDZVQL": "Check",
|
||||||
|
"RSr2uB": "Username must only contain lowercase letters and numbers",
|
||||||
"RahCRH": "Expired",
|
"RahCRH": "Expired",
|
||||||
"RfhLwC": "By: {author}",
|
"RfhLwC": "By: {author}",
|
||||||
"RhDAoS": "Are you sure you want to delete {id}",
|
"RhDAoS": "Are you sure you want to delete {id}",
|
||||||
@ -258,10 +265,12 @@
|
|||||||
"UT7Nkj": "New Chat",
|
"UT7Nkj": "New Chat",
|
||||||
"UUPFlt": "Users must accept the content warning to show the content of your note.",
|
"UUPFlt": "Users must accept the content warning to show the content of your note.",
|
||||||
"Up5U7K": "Block",
|
"Up5U7K": "Block",
|
||||||
|
"UrKTqQ": "You have an active iris.to account",
|
||||||
"VBadwB": "Hmm, can't find a key manager extension.. try reloading the page.",
|
"VBadwB": "Hmm, can't find a key manager extension.. try reloading the page.",
|
||||||
"VN0+Fz": "Balance: {amount} sats",
|
"VN0+Fz": "Balance: {amount} sats",
|
||||||
"VOjC1i": "Pick which upload service you want to upload attachments to",
|
"VOjC1i": "Pick which upload service you want to upload attachments to",
|
||||||
"VR5eHw": "Public key (npub/nprofile)",
|
"VR5eHw": "Public key (npub/nprofile)",
|
||||||
|
"VcwrfF": "Yes please",
|
||||||
"VlJkSk": "{n} muted",
|
"VlJkSk": "{n} muted",
|
||||||
"VnXp8Z": "Avatar",
|
"VnXp8Z": "Avatar",
|
||||||
"VvaJst": "View Wallets",
|
"VvaJst": "View Wallets",
|
||||||
@ -287,6 +296,7 @@
|
|||||||
"ZKORll": "Activate Now",
|
"ZKORll": "Activate Now",
|
||||||
"ZLmyG9": "Contributors",
|
"ZLmyG9": "Contributors",
|
||||||
"ZS+jRE": "Send zap splits to",
|
"ZS+jRE": "Send zap splits to",
|
||||||
|
"Zff6lu": "Username iris.to/<b>{name}</b> is reserved for you!",
|
||||||
"Zr5TMx": "Setup profile",
|
"Zr5TMx": "Setup profile",
|
||||||
"a5UPxh": "Fund developers and platforms providing NIP-05 verification services",
|
"a5UPxh": "Fund developers and platforms providing NIP-05 verification services",
|
||||||
"a7TDNm": "Notes will stream in real time into global and notes tab",
|
"a7TDNm": "Notes will stream in real time into global and notes tab",
|
||||||
@ -299,6 +309,7 @@
|
|||||||
"bfvyfs": "Anon",
|
"bfvyfs": "Anon",
|
||||||
"brAXSu": "Pick a username",
|
"brAXSu": "Pick a username",
|
||||||
"bxv59V": "Just now",
|
"bxv59V": "Just now",
|
||||||
|
"c+JYNI": "No thanks",
|
||||||
"c+oiJe": "Install Extension",
|
"c+oiJe": "Install Extension",
|
||||||
"c2DTVd": "Enter a pin to encrypt your private key, you must enter this pin every time you open Snort.",
|
"c2DTVd": "Enter a pin to encrypt your private key, you must enter this pin every time you open Snort.",
|
||||||
"c35bj2": "If you have an enquiry about your NIP-05 order please DM {link}",
|
"c35bj2": "If you have an enquiry about your NIP-05 order please DM {link}",
|
||||||
@ -314,6 +325,7 @@
|
|||||||
"d6CyG5": "History",
|
"d6CyG5": "History",
|
||||||
"d7d0/x": "LN Address",
|
"d7d0/x": "LN Address",
|
||||||
"dOQCL8": "Display name",
|
"dOQCL8": "Display name",
|
||||||
|
"deEeEI": "Register",
|
||||||
"e61Jf3": "Coming soon",
|
"e61Jf3": "Coming soon",
|
||||||
"e7VmYP": "Enter pin to unlock your private key",
|
"e7VmYP": "Enter pin to unlock your private key",
|
||||||
"e7qqly": "Mark All Read",
|
"e7qqly": "Mark All Read",
|
||||||
@ -363,6 +375,7 @@
|
|||||||
"jzgQ2z": "{n} Reactions",
|
"jzgQ2z": "{n} Reactions",
|
||||||
"k2veDA": "Write",
|
"k2veDA": "Write",
|
||||||
"k7sKNy": "Our very own NIP-05 verification service, help support the development of this site and get a shiny special badge on our site!",
|
"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}}",
|
"kJYo0u": "{n,plural,=0{{name} reposted} other{{name} & {n} others reposted}}",
|
||||||
"kaaf1E": "now",
|
"kaaf1E": "now",
|
||||||
"kuPHYE": "{n,plural,=0{{name} liked} other{{name} & {n} others liked}}",
|
"kuPHYE": "{n,plural,=0{{name} liked} other{{name} & {n} others liked}}",
|
||||||
@ -423,6 +436,7 @@
|
|||||||
"rmdsT4": "{n} days",
|
"rmdsT4": "{n} days",
|
||||||
"rrfdTe": "This is the same technology which is used by Bitcoin and has been proven to be extremely secure.",
|
"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",
|
"rudscU": "Failed to load follows, please try again later",
|
||||||
|
"rx1i0i": "Short link",
|
||||||
"sKDn4e": "Show Badges",
|
"sKDn4e": "Show Badges",
|
||||||
"sUNhQE": "user",
|
"sUNhQE": "user",
|
||||||
"sWnYKw": "Snort is designed to have a similar experience to Twitter.",
|
"sWnYKw": "Snort is designed to have a similar experience to Twitter.",
|
||||||
|
@ -109,16 +109,24 @@ export abstract class FeedCache<TCached> {
|
|||||||
const k = this.key(obj);
|
const k = this.key(obj);
|
||||||
this.cache.set(k, obj);
|
this.cache.set(k, obj);
|
||||||
if (this.table) {
|
if (this.table) {
|
||||||
|
try {
|
||||||
await this.table.put(obj);
|
await this.table.put(obj);
|
||||||
this.onTable.add(k);
|
this.onTable.add(k);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.notifyChange([k]);
|
this.notifyChange([k]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkSet(obj: Array<TCached> | Readonly<Array<TCached>>) {
|
async bulkSet(obj: Array<TCached> | Readonly<Array<TCached>>) {
|
||||||
if (this.table) {
|
if (this.table) {
|
||||||
|
try {
|
||||||
await this.table.bulkPut(obj);
|
await this.table.bulkPut(obj);
|
||||||
obj.forEach(a => this.onTable.add(this.key(a)));
|
obj.forEach(a => this.onTable.add(this.key(a)));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
obj.forEach(v => this.cache.set(this.key(v), v));
|
obj.forEach(v => this.cache.set(this.key(v), v));
|
||||||
this.notifyChange(obj.map(a => this.key(a)));
|
this.notifyChange(obj.map(a => this.key(a)));
|
||||||
|
Loading…
Reference in New Issue
Block a user