Merge pull request #233 from v0l/onboarding

feat: onboarding
This commit is contained in:
Kieran 2023-02-10 10:46:12 +00:00 committed by GitHub
commit f4bfa978ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1192 additions and 209 deletions

View File

@ -1,7 +1,7 @@
name: Docker build
on:
push:
branches: [ main ]
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
@ -15,4 +15,4 @@ jobs:
- uses: docker/setup-qemu-action@v1
- uses: docker/setup-buildx-action@v1
- name: Build the Docker image
run: docker buildx build -t ghcr.io/${{ github.repository_owner }}/snort:latest --platform linux/amd64 --push .
run: docker buildx build -t ghcr.io/${{ github.repository_owner }}/snort:latest --platform linux/amd64 --push .

View File

@ -11,9 +11,7 @@
content="default-src 'self'; 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; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src wss://* 'self' https://*; img-src * data:; font-src https://fonts.gstatic.com; media-src *; script-src 'self' https://static.cloudflareinsights.com https://platform.twitter.com https://embed.tidal.com;" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@400;600;700&display=swap"
rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/nostrich_512.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>snort.social - Nostr interface</title>

View File

@ -1,5 +1,6 @@
import { ReactNode } from "react";
import { useState, ReactNode } from "react";
import ChevronDown from "Icons/ChevronDown";
import ShowMore from "Element/ShowMore";
interface CollapsedProps {
@ -19,4 +20,43 @@ const Collapsed = ({ text, children, collapsed, setCollapsed }: CollapsedProps)
);
};
interface CollapsedIconProps {
icon: ReactNode;
children: ReactNode;
collapsed: boolean;
}
export const CollapsedIcon = ({ icon, children, collapsed }: CollapsedIconProps) => {
return collapsed ? (
<div className="collapsed">{icon}</div>
) : (
<div className="uncollapsed">
{icon}
{children}
</div>
);
};
interface CollapsedSectionProps {
title: ReactNode;
children: ReactNode;
}
export const CollapsedSection = ({ title, children }: CollapsedSectionProps) => {
const [collapsed, setCollapsed] = useState(true);
const icon = (
<div className={`collapse-icon ${collapsed ? "" : "flip"}`} onClick={() => setCollapsed(!collapsed)}>
<ChevronDown />
</div>
);
return (
<div className="collapsable-section">
<h3 onClick={() => setCollapsed(!collapsed)}>{title}</h3>
<CollapsedIcon icon={icon} collapsed={collapsed}>
{children}
</CollapsedIcon>
</div>
);
};
export default Collapsed;

View File

@ -1,3 +1,4 @@
import { ReactNode } from "react";
import { FormattedMessage } from "react-intl";
import useEventPublisher from "Feed/EventPublisher";
@ -8,7 +9,7 @@ import messages from "./messages";
export interface FollowListBaseProps {
pubkeys: HexKey[];
title?: string;
title?: ReactNode | string;
}
export default function FollowListBase({ pubkeys, title }: FollowListBaseProps) {
const publisher = useEventPublisher();

View File

@ -5,10 +5,6 @@
font-weight: normal;
}
.nip05.failed {
text-decoration: line-through;
}
.nip05 .domain {
color: var(--font-secondary-color);
background-color: var(--font-secondary-color);

View File

@ -29,14 +29,18 @@ async function fetchNip05Pubkey(name: string, domain: string) {
const VERIFICATION_CACHE_TIME = 24 * 60 * 60 * 1000;
const VERIFICATION_STALE_TIMEOUT = 10 * 60 * 1000;
export function useIsVerified(pubkey: HexKey, nip05?: string) {
export function useIsVerified(pubkey: HexKey, nip05?: string, bypassCheck?: boolean) {
const [name, domain] = nip05 ? nip05.split("@") : [];
const { isError, isSuccess, data } = useQuery(["nip05", nip05], () => fetchNip05Pubkey(name, domain), {
retry: false,
retryOnMount: false,
cacheTime: VERIFICATION_CACHE_TIME,
staleTime: VERIFICATION_STALE_TIMEOUT,
});
const { isError, isSuccess, data } = useQuery(
["nip05", nip05],
() => (bypassCheck ? Promise.resolve(pubkey) : fetchNip05Pubkey(name, domain)),
{
retry: false,
retryOnMount: false,
cacheTime: VERIFICATION_CACHE_TIME,
staleTime: VERIFICATION_STALE_TIMEOUT,
}
);
const isVerified = isSuccess && data === pubkey;
const cantVerify = isSuccess && data !== pubkey;
return { isVerified, couldNotVerify: isError || cantVerify };
@ -45,12 +49,13 @@ export function useIsVerified(pubkey: HexKey, nip05?: string) {
export interface Nip05Params {
nip05?: string;
pubkey: HexKey;
verifyNip?: boolean;
}
const Nip05 = (props: Nip05Params) => {
const [name, domain] = props.nip05 ? props.nip05.split("@") : [];
const Nip05 = ({ nip05, pubkey, verifyNip = true }: Nip05Params) => {
const [name, domain] = nip05 ? nip05.split("@") : [];
const isDefaultUser = name === "_";
const { isVerified, couldNotVerify } = useIsVerified(props.pubkey, props.nip05);
const { isVerified, couldNotVerify } = useIsVerified(pubkey, nip05, !verifyNip);
return (
<div className={`flex nip05${couldNotVerify ? " failed" : ""}`} onClick={ev => ev.stopPropagation()}>

View File

@ -1,7 +1,10 @@
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState, ChangeEvent } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { unwrap } from "Util";
import { formatShort } from "Number";
import {
ServiceProvider,
ServiceConfig,
@ -28,10 +31,15 @@ type Nip05ServiceProps = {
about: JSX.Element;
link: string;
supportLink: string;
helpText?: boolean;
autoUpdate?: boolean;
onChange?(h: string): void;
onSuccess?(h: string): void;
};
export default function Nip5Service(props: Nip05ServiceProps) {
const navigate = useNavigate();
const { helpText = true, autoUpdate = true } = props;
const { formatMessage } = useIntl();
const pubkey = useSelector((s: RootState) => s.login.publicKey);
const user = useUserProfile(pubkey);
@ -46,6 +54,22 @@ export default function Nip5Service(props: Nip05ServiceProps) {
const [showInvoice, setShowInvoice] = useState<boolean>(false);
const [registerStatus, setRegisterStatus] = useState<CheckRegisterResponse>();
const onHandleChange = (e: ChangeEvent<HTMLInputElement>) => {
const h = e.target.value.toLowerCase();
setHandle(h);
if (props.onChange) {
props.onChange(`${h}@${domain}`);
}
};
const onDomainChange = (e: ChangeEvent<HTMLSelectElement>) => {
const d = e.target.value;
setDomain(d);
if (props.onChange) {
props.onChange(`${handle}@${d}`);
}
};
const domainConfig = useMemo(() => serviceConfig?.domains.find(a => a.name === domain), [domain, serviceConfig]);
useEffect(() => {
@ -111,6 +135,9 @@ export default function Nip5Service(props: Nip05ServiceProps) {
setRegisterStatus(status);
setRegisterResponse(undefined);
setError(undefined);
if (autoUpdate) {
updateProfile(handle, domain);
}
}
}
}, 2_000);
@ -149,44 +176,47 @@ export default function Nip5Service(props: Nip05ServiceProps) {
async function updateProfile(handle: string, domain: string) {
if (user) {
const nip05 = `${handle}@${domain}`;
const newProfile = {
...user,
nip05: `${handle}@${domain}`,
nip05,
} as UserMetadata;
const ev = await publisher.metadata(newProfile);
publisher.broadcast(ev);
navigate("/settings");
if (props.onSuccess) {
props.onSuccess(nip05);
}
if (helpText) {
navigate("/settings");
}
}
}
return (
<>
<h3>{props.name}</h3>
{props.about}
<p>
<FormattedMessage
{...messages.FindMore}
values={{
service: props.name,
link: (
<a href={props.link} target="_blank" rel="noreferrer">
{props.link}
</a>
),
}}
/>
</p>
{helpText && <h3>{props.name}</h3>}
{helpText && props.about}
{helpText && (
<p>
<FormattedMessage
{...messages.FindMore}
values={{
service: props.name,
link: (
<a href={props.link} target="_blank" rel="noreferrer">
{props.link}
</a>
),
}}
/>
</p>
)}
{error && <b className="error">{error.error}</b>}
{!registerStatus && (
<div className="flex mb10">
<input
type="text"
placeholder="Handle"
value={handle}
onChange={e => setHandle(e.target.value.toLowerCase())}
/>
<input type="text" placeholder={formatMessage(messages.Handle)} value={handle} onChange={onHandleChange} />
&nbsp;@&nbsp;
<select value={domain} onChange={e => setDomain(e.target.value)}>
<select value={domain} onChange={onDomainChange}>
{serviceConfig?.domains.map(a => (
<option key={a.name}>{a.name}</option>
))}
@ -196,17 +226,22 @@ export default function Nip5Service(props: Nip05ServiceProps) {
{availabilityResponse?.available && !registerStatus && (
<div className="flex">
<div className="mr10">
<FormattedMessage {...messages.Sats} values={{ n: availabilityResponse.quote?.price }} />
<FormattedMessage
{...messages.Sats}
values={{ n: formatShort(unwrap(availabilityResponse.quote?.price)) }}
/>
<br />
<small>{availabilityResponse.quote?.data.type}</small>
</div>
<input
type="text"
className="f-grow mr10"
placeholder="pubkey"
value={hexToBech32("npub", pubkey)}
disabled
/>
{!autoUpdate && (
<input
type="text"
className="f-grow mr10"
placeholder="pubkey"
value={hexToBech32("npub", pubkey)}
disabled
/>
)}
<AsyncButton onClick={() => startBuy(handle, domain)}>
<FormattedMessage {...messages.BuyNow} />
</AsyncButton>
@ -250,12 +285,16 @@ export default function Nip5Service(props: Nip05ServiceProps) {
<FormattedMessage {...messages.AccountPage} />
</a>
</p>
<h4>
<FormattedMessage {...messages.ActivateNow} />
</h4>
<AsyncButton onClick={() => updateProfile(handle, domain)}>
<FormattedMessage {...messages.AddToProfile} />
</AsyncButton>
{!autoUpdate && (
<>
<h4>
<FormattedMessage {...messages.ActivateNow} />
</h4>
<AsyncButton onClick={() => updateProfile(handle, domain)}>
<FormattedMessage {...messages.AddToProfile} />
</AsyncButton>
</>
)}
</div>
)}
</>

View File

@ -50,18 +50,51 @@
}
.note > .footer .ctx-menu {
background-color: var(--note-bg);
color: var(--font-secondary-color);
border: 1px solid var(--font-secondary-color);
border-radius: 16px;
background: transparent;
box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.4);
min-width: 0;
margin: 0;
padding: 0;
border-radius: 16px;
}
.light .note > .footer .ctx-menu {
}
.note > .footer .ctx-menu li {
background: #1e1e1e;
padding-top: 8px;
padding-bottom: 8px;
display: grid;
grid-template-columns: 2rem auto;
}
.light .note > .footer .ctx-menu li {
background: var(--note-bg);
}
.note > .footer .ctx-menu li:first-of-type {
padding-top: 12px;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
}
.note > .footer .ctx-menu li:last-of-type {
padding-bottom: 12px;
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
}
.light .note > .footer .ctx-menu li:hover {
color: white;
background: #2a2a2a;
}
.note > .footer .ctx-menu li:hover {
color: white;
background: var(--font-secondary-color);
}
.ctx-menu .red {
color: var(--error);
}

View File

@ -1,19 +1,16 @@
import { useMemo, useState } from "react";
import { useSelector } from "react-redux";
import { useIntl, FormattedMessage } from "react-intl";
import {
faTrash,
faHeart,
faRepeat,
faShareNodes,
faCopy,
faCommentSlash,
faBan,
faLanguage,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Menu, MenuItem } from "@szhsin/react-menu";
import Json from "Icons/Json";
import Repost from "Icons/Repost";
import Trash from "Icons/Trash";
import Translate from "Icons/Translate";
import Block from "Icons/Block";
import Mute from "Icons/Mute";
import Share from "Icons/Share";
import Copy from "Icons/Copy";
import Dislike from "Icons/Dislike";
import Heart from "Icons/Heart";
import Dots from "Icons/Dots";
@ -151,7 +148,7 @@ export default function NoteFooter(props: NoteFooterProps) {
return (
<div className={`reaction-pill ${hasReposted() ? "reacted" : ""}`} onClick={() => repost()}>
<div className="reaction-pill-icon">
<FontAwesomeIcon icon={faRepeat} />
<Repost width={18} height={16} />
</div>
{reposts.length > 0 && <div className="reaction-pill-number">{formatShort(reposts.length)}</div>}
</div>
@ -223,45 +220,45 @@ export default function NoteFooter(props: NoteFooterProps) {
<>
{prefs.enableReactions && (
<MenuItem onClick={() => setShowReactions(true)}>
<FontAwesomeIcon icon={faHeart} />
<Heart />
<FormattedMessage {...messages.Reactions} />
</MenuItem>
)}
<MenuItem onClick={() => share()}>
<FontAwesomeIcon icon={faShareNodes} />
<Share />
<FormattedMessage {...messages.Share} />
</MenuItem>
<MenuItem onClick={() => copyId()}>
<FontAwesomeIcon icon={faCopy} />
<Copy />
<FormattedMessage {...messages.CopyID} />
</MenuItem>
<MenuItem onClick={() => mute(ev.PubKey)}>
<FontAwesomeIcon icon={faCommentSlash} />
<Mute />
<FormattedMessage {...messages.Mute} />
</MenuItem>
{prefs.enableReactions && (
<MenuItem onClick={() => react("-")}>
<Dislike />
<FormattedMessage {...messages.Dislike} values={{ n: negative.length }} />
<FormattedMessage {...messages.DislikeAction} />
</MenuItem>
)}
<MenuItem onClick={() => block(ev.PubKey)}>
<FontAwesomeIcon icon={faBan} />
<Block />
<FormattedMessage {...messages.Block} />
</MenuItem>
<MenuItem onClick={() => translate()}>
<FontAwesomeIcon icon={faLanguage} />
<Translate />
<FormattedMessage {...messages.TranslateTo} values={{ lang: langNames.of(lang.split("-")[0]) }} />
</MenuItem>
{prefs.showDebugMenus && (
<MenuItem onClick={() => copyEvent()}>
<FontAwesomeIcon icon={faCopy} />
<Json />
<FormattedMessage {...messages.CopyJSON} />
</MenuItem>
)}
{isMine && (
<MenuItem onClick={() => deleteEvent()}>
<FontAwesomeIcon icon={faTrash} className="red" />
<Trash className="red" />
<FormattedMessage {...messages.Delete} />
</MenuItem>
)}

View File

@ -15,11 +15,22 @@ export interface ProfileImageProps {
showUsername?: boolean;
className?: string;
link?: string;
defaultNip?: string;
verifyNip?: boolean;
}
export default function ProfileImage({ pubkey, subHeader, showUsername = true, className, link }: ProfileImageProps) {
export default function ProfileImage({
pubkey,
subHeader,
showUsername = true,
className,
link,
defaultNip,
verifyNip,
}: ProfileImageProps) {
const navigate = useNavigate();
const user = useUserProfile(pubkey);
const nip05 = defaultNip ? defaultNip : user?.nip05;
const name = useMemo(() => {
return getDisplayName(user, pubkey);
@ -35,7 +46,7 @@ export default function ProfileImage({ pubkey, subHeader, showUsername = true, c
<div className="username">
<Link className="display-name" key={pubkey} to={link ?? profileLink(pubkey)}>
{name}
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
{nip05 && <Nip05 nip05={nip05} pubkey={pubkey} verifyNip={verifyNip} />}
</Link>
</div>
<div className="subheader">{subHeader}</div>

View File

@ -10,6 +10,10 @@
position: relative;
}
.light .reactions-view {
background-color: var(--note-bg);
}
@media (max-width: 720px) {
.reactions-view {
padding: 12px 16px;

View File

@ -45,6 +45,7 @@ export default defineMessages({
CopyID: "Copy ID",
CopyJSON: "Copy Event JSON",
Dislike: "{n} Dislike",
DislikeAction: "Dislike",
Sats: `{n} {n, plural, =1 {sat} other {sats}}`,
Zapped: "zapped",
OthersZapped: `{n, plural, =0 {} =1 {zapped} other {zapped}}`,
@ -89,4 +90,5 @@ export default defineMessages({
GoTo: "Go to",
FindMore: "Find out more info about {service} at {link}",
SavePassword: "Please make sure to save the following password in order to manage your handle in the future",
Handle: "Handle",
});

15
src/Icons/Block.tsx Normal file
View File

@ -0,0 +1,15 @@
const Block = () => {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M4.10829 4.10768L15.8916 15.891M18.3333 9.99935C18.3333 14.6017 14.6023 18.3327 9.99996 18.3327C5.39759 18.3327 1.66663 14.6017 1.66663 9.99935C1.66663 5.39698 5.39759 1.66602 9.99996 1.66602C14.6023 1.66602 18.3333 5.39698 18.3333 9.99935Z"
stroke="currentColor"
strokeWidth="1.66667"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export default Block;

14
src/Icons/ChevronDown.tsx Normal file
View File

@ -0,0 +1,14 @@
const ChevronDown = () => {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.29289 8.29289C5.68342 7.90237 6.31658 7.90237 6.70711 8.29289L12 13.5858L17.2929 8.29289C17.6834 7.90237 18.3166 7.90237 18.7071 8.29289C19.0976 8.68342 19.0976 9.31658 18.7071 9.70711L12.7071 15.7071C12.3166 16.0976 11.6834 16.0976 11.2929 15.7071L5.29289 9.70711C4.90237 9.31658 4.90237 8.68342 5.29289 8.29289Z"
fill="currentColor"
/>
</svg>
);
};
export default ChevronDown;

View File

@ -2,11 +2,11 @@ import IconProps from "./IconProps";
const Copy = (props: IconProps) => {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M5.33331 5.33398V3.46732C5.33331 2.72058 5.33331 2.34721 5.47864 2.062C5.60647 1.81111 5.81044 1.60714 6.06133 1.47931C6.34654 1.33398 6.71991 1.33398 7.46665 1.33398H12.5333C13.28 1.33398 13.6534 1.33398 13.9386 1.47931C14.1895 1.60714 14.3935 1.81111 14.5213 2.062C14.6666 2.34721 14.6666 2.72058 14.6666 3.46732V8.53398C14.6666 9.28072 14.6666 9.65409 14.5213 9.9393C14.3935 10.1902 14.1895 10.3942 13.9386 10.522C13.6534 10.6673 13.28 10.6673 12.5333 10.6673H10.6666M3.46665 14.6673H8.53331C9.28005 14.6673 9.65342 14.6673 9.93863 14.522C10.1895 14.3942 10.3935 14.1902 10.5213 13.9393C10.6666 13.6541 10.6666 13.2807 10.6666 12.534V7.46732C10.6666 6.72058 10.6666 6.34721 10.5213 6.062C10.3935 5.81111 10.1895 5.60714 9.93863 5.47931C9.65342 5.33398 9.28005 5.33398 8.53331 5.33398H3.46665C2.71991 5.33398 2.34654 5.33398 2.06133 5.47931C1.81044 5.60714 1.60647 5.81111 1.47864 6.062C1.33331 6.34721 1.33331 6.72058 1.33331 7.46732V12.534C1.33331 13.2807 1.33331 13.6541 1.47864 13.9393C1.60647 14.1902 1.81044 14.3942 2.06133 14.522C2.34654 14.6673 2.71991 14.6673 3.46665 14.6673Z"
d="M13.3333 13.3327V15.666C13.3333 16.5994 13.3333 17.0661 13.1516 17.4227C12.9918 17.7363 12.7369 17.9912 12.4233 18.151C12.0668 18.3327 11.6 18.3327 10.6666 18.3327H4.33329C3.39987 18.3327 2.93316 18.3327 2.57664 18.151C2.26304 17.9912 2.00807 17.7363 1.84828 17.4227C1.66663 17.0661 1.66663 16.5994 1.66663 15.666V9.33268C1.66663 8.39926 1.66663 7.93255 1.84828 7.57603C2.00807 7.26243 2.26304 7.00746 2.57664 6.84767C2.93316 6.66602 3.39987 6.66602 4.33329 6.66602H6.66663M9.33329 13.3327H15.6666C16.6 13.3327 17.0668 13.3327 17.4233 13.151C17.7369 12.9912 17.9918 12.7363 18.1516 12.4227C18.3333 12.0661 18.3333 11.5994 18.3333 10.666V4.33268C18.3333 3.39926 18.3333 2.93255 18.1516 2.57603C17.9918 2.26243 17.7369 2.00746 17.4233 1.84767C17.0668 1.66602 16.6 1.66602 15.6666 1.66602H9.33329C8.39987 1.66602 7.93316 1.66602 7.57664 1.84767C7.26304 2.00746 7.00807 2.26243 6.84828 2.57603C6.66663 2.93255 6.66663 3.39926 6.66663 4.33268V10.666C6.66663 11.5994 6.66663 12.0661 6.84828 12.4227C7.00807 12.7363 7.26304 12.9912 7.57664 13.151C7.93316 13.3327 8.39987 13.3327 9.33329 13.3327Z"
stroke="currentColor"
strokeWidth="1.33333"
strokeWidth="1.66667"
strokeLinecap="round"
strokeLinejoin="round"
/>

15
src/Icons/Json.tsx Normal file
View File

@ -0,0 +1,15 @@
const Json = () => {
return (
<svg width="22" height="18" viewBox="0 0 22 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M17.5708 17C18.8328 17 19.8568 15.977 19.8568 14.714V10.143L20.9998 9L19.8568 7.857V3.286C19.8568 2.023 18.8338 1 17.5708 1M4.429 1C3.166 1 2.143 2.023 2.143 3.286V7.857L1 9L2.143 10.143V14.714C2.143 15.977 3.166 17 4.429 17"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export default Json;

15
src/Icons/Mute.tsx Normal file
View File

@ -0,0 +1,15 @@
const Mute = () => {
return (
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.75 12.3333L17.9166 16.5M17.9166 12.3333L13.75 16.5M9.99996 11.9167H6.24996C5.08699 11.9167 4.5055 11.9167 4.03234 12.0602C2.96701 12.3834 2.13333 13.217 1.81016 14.2824C1.66663 14.7555 1.66663 15.337 1.66663 16.5M12.0833 5.25C12.0833 7.32107 10.4044 9 8.33329 9C6.26222 9 4.58329 7.32107 4.58329 5.25C4.58329 3.17893 6.26222 1.5 8.33329 1.5C10.4044 1.5 12.0833 3.17893 12.0833 5.25Z"
stroke="currentColor"
strokeWidth="1.66667"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export default Mute;

17
src/Icons/Repost.tsx Normal file
View File

@ -0,0 +1,17 @@
import IconProps from "./IconProps";
const Repost = (props: IconProps) => {
return (
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M1 12C1 12 1.12132 12.8492 4.63604 16.364C8.15076 19.8787 13.8492 19.8787 17.364 16.364C18.6092 15.1187 19.4133 13.5993 19.7762 12M1 12V18M1 12H7M21 8C21 8 20.8787 7.15076 17.364 3.63604C13.8492 0.12132 8.15076 0.12132 4.63604 3.63604C3.39076 4.88131 2.58669 6.40072 2.22383 8M21 8V2M21 8H15"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export default Repost;

15
src/Icons/Share.tsx Normal file
View File

@ -0,0 +1,15 @@
const Share = () => {
return (
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M16.3261 8.50715C16.5296 8.33277 16.6313 8.24559 16.6686 8.14184C16.7013 8.05078 16.7013 7.95117 16.6686 7.86011C16.6313 7.75636 16.5296 7.66918 16.3261 7.4948L9.26719 1.44429C8.917 1.14412 8.74191 0.99404 8.59367 0.990363C8.46483 0.987167 8.34177 1.04377 8.26035 1.14367C8.16667 1.25861 8.16667 1.48923 8.16667 1.95045V5.52984C6.38777 5.84105 4.75966 6.74244 3.54976 8.09586C2.23069 9.5714 1.50103 11.4809 1.5 13.4601V13.9701C2.37445 12.9167 3.46626 12.0647 4.70063 11.4726C5.78891 10.9505 6.96535 10.6412 8.16667 10.5597V14.0515C8.16667 14.5127 8.16667 14.7433 8.26035 14.8583C8.34177 14.9582 8.46483 15.0148 8.59367 15.0116C8.74191 15.0079 8.917 14.8578 9.26719 14.5577L16.3261 8.50715Z"
stroke="currentColor"
strokeWidth="1.66667"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export default Share;

15
src/Icons/Translate.tsx Normal file
View File

@ -0,0 +1,15 @@
const Translate = () => {
return (
<svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M10.7608 13.1667H16.7391M10.7608 13.1667L9.16663 16.5M10.7608 13.1667L13.1485 8.17419C13.3409 7.77189 13.4371 7.57075 13.5688 7.50718C13.6832 7.4519 13.8167 7.4519 13.9311 7.50718C14.0628 7.57075 14.159 7.77189 14.3514 8.17419L16.7391 13.1667M16.7391 13.1667L18.3333 16.5M1.66663 3.16667H6.66663M6.66663 3.16667H9.58329M6.66663 3.16667V1.5M9.58329 3.16667H11.6666M9.58329 3.16667C9.16984 5.63107 8.21045 7.86349 6.80458 9.73702M8.33329 10.6667C7.82285 10.4373 7.30217 10.1184 6.80458 9.73702M6.80458 9.73702C5.67748 8.87314 4.66893 7.68886 4.16663 6.5M6.80458 9.73702C5.46734 11.5191 3.72615 12.9765 1.66663 14"
stroke="currentColor"
strokeWidth="1.66667"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export default Translate;

17
src/Icons/Trash.tsx Normal file
View File

@ -0,0 +1,17 @@
import IconProps from "./IconProps";
const Trash = (props: IconProps) => {
return (
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M14 5V4.2C14 3.0799 14 2.51984 13.782 2.09202C13.5903 1.71569 13.2843 1.40973 12.908 1.21799C12.4802 1 11.9201 1 10.8 1H9.2C8.07989 1 7.51984 1 7.09202 1.21799C6.71569 1.40973 6.40973 1.71569 6.21799 2.09202C6 2.51984 6 3.0799 6 4.2V5M1 5H19M17 5V16.2C17 17.8802 17 18.7202 16.673 19.362C16.3854 19.9265 15.9265 20.3854 15.362 20.673C14.7202 21 13.8802 21 12.2 21H7.8C6.11984 21 5.27976 21 4.63803 20.673C4.07354 20.3854 3.6146 19.9265 3.32698 19.362C3 18.7202 3 17.8802 3 16.2V5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export default Trash;

View File

@ -14,7 +14,7 @@
.write-dm {
position: fixed;
bottom: 0;
background-color: var(--gray-light);
background-color: var(--gray);
width: inherit;
border-radius: 5px 5px 0 0;
}

View File

@ -7,24 +7,24 @@ import messages from "./messages";
import "./Verification.css";
export default function VerificationPage() {
const services = [
{
name: "Snort",
service: `${ApiHost}/api/v1/n5sp`,
link: "https://snort.social/",
supportLink: "https://snort.social/help",
about: <FormattedMessage {...messages.SnortSocialNip} />,
},
{
name: "Nostr Plebs",
service: "https://nostrplebs.com/api/v1",
link: "https://nostrplebs.com/",
supportLink: "https://nostrplebs.com/manage",
about: <FormattedMessage {...messages.NostrPlebsNip} />,
},
];
export const services = [
{
name: "Snort",
service: `${ApiHost}/api/v1/n5sp`,
link: "https://snort.social/",
supportLink: "https://snort.social/help",
about: <FormattedMessage {...messages.SnortSocialNip} />,
},
{
name: "Nostr Plebs",
service: "https://nostrplebs.com/api/v1",
link: "https://nostrplebs.com/",
supportLink: "https://nostrplebs.com/manage",
about: <FormattedMessage {...messages.NostrPlebsNip} />,
},
];
export default function VerificationPage() {
return (
<div className="main-content verification">
<h2>

View File

@ -1,21 +1,32 @@
import { useIntl, FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";
import { RecommendedFollows } from "Const";
import FollowListBase from "Element/FollowListBase";
import { useMemo } from "react";
import { useNavigate } from "react-router-dom";
import messages from "./messages";
export default function DiscoverFollows() {
const navigate = useNavigate();
const sortedRecommends = useMemo(() => {
const { formatMessage } = useIntl();
const sortedReccomends = useMemo(() => {
return RecommendedFollows.sort(() => (Math.random() >= 0.5 ? -1 : 1));
}, []);
return (
<>
<h2>Follow some popular accounts</h2>
<button onClick={() => navigate("/")}>Skip</button>
{sortedRecommends.length > 0 && <FollowListBase pubkeys={sortedRecommends} />}
<button onClick={() => navigate("/")}>Done!</button>
</>
<div className="main-content new-user">
<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>
<h3>
<FormattedMessage {...messages.PopularAccounts} />
</h3>
{sortedReccomends.length > 0 && <FollowListBase pubkeys={sortedReccomends} />}
</div>
);
}

View File

@ -0,0 +1,109 @@
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { services } from "Pages/Verification";
import Nip5Service from "Element/Nip5Service";
import ProfileImage from "Element/ProfileImage";
import type { RootState } from "State/Store";
import { useUserProfile } from "Feed/ProfileFeed";
import messages from "./messages";
export default function GetVerified() {
const navigate = useNavigate();
const { publicKey } = useSelector((s: RootState) => s.login);
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">
<div className="progress-bar">
<div className="progress progress-third"></div>
</div>
<h1>
<FormattedMessage {...messages.Identifier} />
</h1>
<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"
{...services[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"
{...services[1]}
helpText={false}
onChange={setNip05}
onSuccess={() => setIsVerified(true)}
/>
</div>
</>
)}
<div className="next-actions">
{!isVerified && (
<button type="button" className="transparent" onClick={onNext}>
<FormattedMessage {...messages.Skip} />
</button>
)}
{isVerified && (
<button type="button" onClick={onNext}>
<FormattedMessage {...messages.Next} />
</button>
)}
</div>
</div>
);
}

View File

@ -1,5 +1,6 @@
import { useMemo, useState } from "react";
import { useSelector } from "react-redux";
import { useIntl, FormattedMessage } from "react-intl";
import { ApiHost } from "Const";
import AsyncButton from "Element/AsyncButton";
@ -8,11 +9,14 @@ import { RootState } from "State/Store";
import { bech32ToHex } from "Util";
import { useNavigate } from "react-router-dom";
import messages from "./messages";
const TwitterFollowsApi = `${ApiHost}/api/v1/twitter/follows-for-nostr`;
export default function ImportFollows() {
const navigate = useNavigate();
const currentFollows = useSelector((s: RootState) => s.login.follows);
const { formatMessage } = useIntl();
const [twitterUsername, setTwitterUsername] = useState<string>("");
const [follows, setFollows] = useState<string[]>([]);
const [error, setError] = useState<string>("");
@ -29,45 +33,76 @@ export default function ImportFollows() {
const data = await rsp.json();
if (rsp.ok) {
if (Array.isArray(data) && data.length === 0) {
setError(`No nostr users found for "${twitterUsername}"`);
setError(formatMessage(messages.NoUsersFound, { twitterUsername }));
} else {
setFollows(data);
}
} else if ("error" in data) {
setError(data.error);
} else {
setError("Failed to load follows, please try again later");
setError(formatMessage(messages.FailedToLoad));
}
} catch (e) {
console.warn(e);
setError("Failed to load follows, please try again later");
setError(formatMessage(messages.FailedToLoad));
}
}
return (
<>
<h2>Import Twitter Follows</h2>
<div className="main-content new-user">
<div className="progress-bar">
<div className="progress progress-last"></div>
</div>
<h1>
<FormattedMessage {...messages.ImportTwitter} />
</h1>
<p>
Find your twitter follows on nostr (Data provided by{" "}
<a href="https://nostr.directory" target="_blank" rel="noreferrer">
nostr.directory
</a>
)
<FormattedMessage
{...messages.FindYourFollows}
values={{
provider: (
<a href="https://nostr.directory" target="_blank" rel="noreferrer">
nostr.directory
</a>
),
}}
/>
</p>
<h2>
<FormattedMessage {...messages.TwitterUsername} />
</h2>
<div className="flex">
<input
type="text"
placeholder="Twitter username.."
placeholder={formatMessage(messages.TwitterPlaceholder)}
className="f-grow mr10"
value={twitterUsername}
onChange={e => setTwitterUsername(e.target.value)}
/>
<AsyncButton onClick={loadFollows}>Check</AsyncButton>
<AsyncButton type="button" className="secondary tall" onClick={loadFollows}>
<FormattedMessage {...messages.Check} />
</AsyncButton>
</div>
{error.length > 0 && <b className="error">{error}</b>}
{sortedTwitterFollows.length > 0 && <FollowListBase pubkeys={sortedTwitterFollows} />}
{sortedTwitterFollows.length > 0 && (
<FollowListBase
title={
<h2>
<FormattedMessage {...messages.FollowsOnNostr} values={{ username: twitterUsername }} />
</h2>
}
pubkeys={sortedTwitterFollows}
/>
)}
<button onClick={() => navigate("/new/discover")}>Next</button>
</>
<div className="next-actions">
<button className="secondary" type="button" onClick={() => navigate("/new/discover")}>
<FormattedMessage {...messages.Skip} />
</button>
<button type="button" onClick={() => navigate("/new/discover")}>
<FormattedMessage {...messages.Next} />
</button>
</div>
</div>
);
}

View File

@ -1,13 +0,0 @@
import ProfileSettings from "Pages/settings/Profile";
import { useNavigate } from "react-router-dom";
export default function NewUserProfile() {
const navigate = useNavigate();
return (
<>
<h1>Setup your Profile</h1>
<ProfileSettings privateKey={false} banner={false} />
<button onClick={() => navigate("/new/import")}>Next</button>
</>
);
}

View File

@ -0,0 +1,102 @@
import { useSelector } from "react-redux";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { CollapsedSection } from "Element/Collapsed";
import Copy from "Element/Copy";
import { RootState } from "State/Store";
import { hexToBech32 } from "Util";
import messages from "./messages";
const WhatIsSnort = () => {
return (
<CollapsedSection title={<FormattedMessage {...messages.WhatIsSnort} />}>
<p>
<FormattedMessage {...messages.WhatIsSnortIntro} />
</p>
<p>
<FormattedMessage {...messages.WhatIsSnortNotes} />
</p>
<p>
<FormattedMessage {...messages.WhatIsSnortExperience} />
</p>
</CollapsedSection>
);
};
const HowDoKeysWork = () => {
return (
<CollapsedSection title={<FormattedMessage {...messages.HowKeysWork} />}>
<p>
<FormattedMessage {...messages.DigitalSignatures} />
</p>
<p>
<FormattedMessage {...messages.TamperProof} />
</p>
<p>
<FormattedMessage {...messages.Bitcoin} />
</p>
</CollapsedSection>
);
};
const Extensions = () => {
return (
<CollapsedSection title={<FormattedMessage {...messages.ImproveSecurity} />}>
<p>
<FormattedMessage {...messages.Extensions} />
</p>
<ul>
<li>
<a href="https://getalby.com/" target="_blank" rel="noreferrer">
Alby
</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 { publicKey, privateKey } = useSelector((s: RootState) => s.login);
const navigate = useNavigate();
return (
<div className="main-content new-user">
<div className="progress-bar">
<div className="progress progress-first"></div>
</div>
<h1>
<FormattedMessage {...messages.SaveKeys} />
</h1>
<p>
<FormattedMessage {...messages.SaveKeysHelp} />
</p>
<h2>
<FormattedMessage {...messages.YourPubkey} />
</h2>
<Copy text={hexToBech32("npub", publicKey ?? "")} />
<h2>
<FormattedMessage {...messages.YourPrivkey} />
</h2>
<Copy text={hexToBech32("nsec", privateKey ?? "")} />
<div className="next-actions">
<button type="button" onClick={() => navigate("/new/username")}>
<FormattedMessage {...messages.KeysSaved} />{" "}
</button>
</div>
<WhatIsSnort />
<HowDoKeysWork />
<Extensions />
</div>
);
}

View File

@ -0,0 +1,55 @@
import { useState } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import useEventPublisher from "Feed/EventPublisher";
import messages from "./messages";
export default function NewUserName() {
const [username, setUsername] = useState("");
const { formatMessage } = useIntl();
const publisher = useEventPublisher();
const navigate = useNavigate();
const onNext = async () => {
if (username.length > 0) {
const ev = await publisher.metadata({ name: username });
console.debug(ev);
publisher.broadcast(ev);
}
navigate("/new/verify");
};
return (
<div className="main-content new-user">
<div className="progress-bar">
<div className="progress progress-second"></div>
</div>
<h1>
<FormattedMessage {...messages.PickUsername} />
</h1>
<p>
<FormattedMessage {...messages.UsernameHelp} />
</p>
<h2>
<FormattedMessage {...messages.Username} />
</h2>
<input
className="username"
placeholder={formatMessage(messages.UsernamePlaceholder)}
type="text"
value={username}
onChange={ev => setUsername(ev.target.value)}
/>
<div className="next-actions">
<button type="button" className="transparent" onClick={() => navigate("/new/verify")}>
<FormattedMessage {...messages.Skip} />
</button>
<button type="button" onClick={onNext}>
<FormattedMessage {...messages.Next} />
</button>
</div>
</div>
);
}

151
src/Pages/new/index.css Normal file
View File

@ -0,0 +1,151 @@
.new-user p {
color: var(--font-secondary-color);
}
.new-user li {
color: var(--font-secondary-color);
}
.new-user p > a {
color: var(--highlight);
}
.new-user li > a {
color: var(--highlight);
}
.new-user h1 {
color: var(--font-color);
font-weight: 700;
font-size: 32px;
line-height: 39px;
}
.new-user h2 {
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 > .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;
}

View File

@ -1,81 +1,36 @@
import { useSelector } from "react-redux";
import { RouteObject, useNavigate } from "react-router-dom";
import "./index.css";
import { RouteObject } from "react-router-dom";
import Copy from "Element/Copy";
import { RootState } from "State/Store";
import { hexToBech32 } from "Util";
import NewUserProfile from "Pages/new//NewProfile";
import GetVerified from "Pages/new/GetVerified";
import NewUserName from "Pages/new/NewUsername";
import NewUserFlow from "Pages/new/NewUserFlow";
import ImportFollows from "Pages/new/ImportFollows";
import DiscoverFollows from "Pages/new/DiscoverFollows";
const USERNAME = "/new/username";
const IMPORT = "/new/import";
const DISCOVER = "/new/discover";
const VERIFY = "/new/verify";
export const NewUserRoutes: RouteObject[] = [
{
path: "/new",
element: <NewUserFlow />,
},
{
path: "/new/profile",
element: <NewUserProfile />,
path: USERNAME,
element: <NewUserName />,
},
{
path: "/new/import",
path: IMPORT,
element: <ImportFollows />,
},
{
path: "/new/discover",
path: VERIFY,
element: <GetVerified />,
},
{
path: DISCOVER,
element: <DiscoverFollows />,
},
];
export default function NewUserFlow() {
const { privateKey } = useSelector((s: RootState) => s.login);
const navigate = useNavigate();
return (
<>
<h1>Welcome to Snort!</h1>
<p>Snort is a Nostr UI, nostr is a decentralised protocol for saving and distributing "notes".</p>
<p>Notes hold text content, the most popular usage of these notes is to store "tweet like" messages.</p>
<p>Snort is designed to have a similar experience to Twitter.</p>
<h2>Keys</h2>
<p>
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.
</p>
<p>
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.
</p>
<p>This is the same technology which is used by Bitcoin and has been proven to be extremely secure.</p>
<h2>Your Key</h2>
<p>
When you want to author new notes you need to sign them with your private key, as with Bitcoin private keys
these need to be kept secure.
</p>
<p>Please now copy your private key and save it somewhere secure:</p>
<div className="card">
<Copy text={hexToBech32("nsec", privateKey ?? "")} />
</div>
<p>
It is also recommended to use one of the following browser extensions if you are on a desktop computer to secure
your key:
</p>
<ul>
<li>
<a href="https://getalby.com/" target="_blank" rel="noreferrer">
Alby
</a>
</li>
<li>
<a href="https://github.com/fiatjaf/nos2x" target="_blank" rel="noreferrer">
nos2x
</a>
</li>
</ul>
<p>You can also use these extensions to login to most Nostr sites.</p>
<button onClick={() => navigate("/new/profile")}>Next</button>
</>
);
}

57
src/Pages/new/messages.js Normal file
View File

@ -0,0 +1,57 @@
import { defineMessages } from "react-intl";
export default defineMessages({
SaveKeys: "Save your keys!",
SaveKeysHelp:
"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: "Your public key",
YourPrivkey: "Your private key",
KeysSaved: "I have saved my keys, continue",
WhatIsSnort: "What is Snort and how does it work?",
WhatIsSnortIntro: `Snort is a Nostr UI, nostr is a decentralised protocol for saving and distributing "notes".`,
WhatIsSnortNotes: `Notes hold text content, the most popular usage of these notes is to store "tweet like" messages.`,
WhatIsSnortExperience: "Snort is designed to have a similar experience to Twitter.",
HowKeysWork: "How do keys work?",
DigitalSignatures: `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: `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: `This is the same technology which is used by Bitcoin and has been proven to be extremely secure.`,
Extensions: `It is recommended to use one of the following browser extensions if you are on a desktop computer to secure your key:`,
ExtensionsNostr: `You can also use these extensions to login to most Nostr sites.`,
ImproveSecurity: "Improve login security with browser extensions",
PickUsername: "Pick a username",
UsernameHelp:
"On Nostr, many people have the same username. User names and identity are separate things. You can get a unique identifier in the next step.",
Username: "Username",
UsernamePlaceholder: "e.g. Jack",
PopularAccounts: "Follow some popular accounts",
Skip: "Skip",
Done: "Done!",
ImportTwitter: "Import Twitter Follows (optional)",
TwitterPlaceholder: "Twitter username...",
FindYourFollows: "Find your twitter follows on nostr (Data provided by {provider})",
TwitterUsername: "Twitter username",
FollowsOnNostr: "{username}'s Follows on Nostr",
NoUsersFound: "No nostr users found for {twitterUsername}",
FailedToLoad: "Failed to load follows, please try again later",
Check: "Check",
Next: "Next",
SetupProfile: "Setup your Profile",
Identifier: "Get an identifier (optional)",
IdentifierHelp:
"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: "Prevent fake accounts from imitating you",
EasierToFind: "Make your profile easier to find and share",
Funding: "Fund developers and platforms providing NIP-05 verification services",
NameSquatting:
"Name-squatting and impersonation is not allowed. Snort and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule.",
PreviewOnSnort: "Preview on snort",
GetSnortId: "Get a Snort identifier",
GetSnortIdHelp:
"Only Snort and our integration partner identifier gives you a colorful domain name, but you are welcome to use other services too.",
GetPartnerId: "Get a partner identifier",
GetPartnerIdHelp: "We have also partnered with nostrplebs.com to give you more options",
Ready: "You're ready!",
Share: "Share your thoughts with {link}",
World: "the world",
});

View File

@ -65,7 +65,7 @@ html.light {
body {
margin: 0;
font-family: "Be Vietnam Pro", sans-serif;
font-family: "Inter", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--bg-color);
@ -167,6 +167,8 @@ button.secondary {
}
button.transparent {
font-weight: 400;
color: var(--font-secondary);
background-color: transparent;
border: 1px solid var(--gray-superdark);
}
@ -543,3 +545,28 @@ body.scroll-lock {
.main-content .profile-preview {
margin-bottom: 16px;
}
button.tall {
height: 40px;
}
.collapsable-section {
position: relative;
}
.collapsable-section h3,
.collapsable-section svg {
cursor: pointer;
}
.collapsable-section .collapse-icon {
position: absolute;
top: 0;
right: 0;
transition: transform 300ms ease-in-out;
}
.collapsable-section .collapse-icon.flip {
transform: rotate(180deg);
transition: transform 300ms ease-in-out;
}

View File

@ -21,6 +21,7 @@
"Element.DisalledLater": "name will be available later",
"Element.Disallowed": "name is blocked",
"Element.Dislike": "{n} Dislike",
"Element.DislikeAction": "Dislike",
"Element.Dislikes": "Dislikes ({n})",
"Element.Expired": "Expired",
"Element.FindMore": "Find out more info about {service} at {link}",
@ -30,6 +31,7 @@
"Element.FollowingCount": "Follows {n}",
"Element.FollowsYou": "follows you",
"Element.GoTo": "Go to",
"Element.Handle": "Handle",
"Element.Invoice": "Lightning Invoice",
"Element.InvoiceFail": "Failed to load invoice",
"Element.JustNow": "Just now",
@ -115,6 +117,53 @@
"Pages.Settings": "Settings",
"Pages.SnortSocialNip": "Our very own NIP-05 verification service, help support the development of this site and get a shiny special badge on our site!",
"Pages.Zaps": "Zaps",
"Pages.new.Bitcoin": "This is the same technology which is used by Bitcoin and has been proven to be extremely secure.",
"Pages.new.Check": "Check",
"Pages.new.DigitalSignatures": "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.",
"Pages.new.Done": "Done!",
"Pages.new.EasierToFind": "Make your profile easier to find and share",
"Pages.new.Extensions": "It is recommended to use one of the following browser extensions if you are on a desktop computer to secure your key:",
"Pages.new.ExtensionsNostr": "You can also use these extensions to login to most Nostr sites.",
"Pages.new.FailedToLoad": "Failed to load follows, please try again later",
"Pages.new.FindYourFollows": "Find your twitter follows on nostr (Data provided by {provider})",
"Pages.new.FollowsOnNostr": "{username}'s Follows on Nostr",
"Pages.new.Funding": "Fund developers and platforms providing NIP-05 verification services",
"Pages.new.GetPartnerId": "Get a partner identifier",
"Pages.new.GetPartnerIdHelp": "We have also partnered with nostrplebs.com to give you more options",
"Pages.new.GetSnortId": "Get a Snort identifier",
"Pages.new.GetSnortIdHelp": "Only Snort and our integration partner identifier gives you a colorful domain name, but you are welcome to use other services too.",
"Pages.new.HowKeysWork": "How do keys work?",
"Pages.new.Identifier": "Get an identifier (optional)",
"Pages.new.IdentifierHelp": "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.",
"Pages.new.ImportTwitter": "Import Twitter Follows (optional)",
"Pages.new.ImproveSecurity": "Improve login security with browser extensions",
"Pages.new.KeysSaved": "I have saved my keys, continue",
"Pages.new.NameSquatting": "Name-squatting and impersonation is not allowed. Snort and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule.",
"Pages.new.Next": "Next",
"Pages.new.NoUsersFound": "No nostr users found for {twitterUsername}",
"Pages.new.PickUsername": "Pick a username",
"Pages.new.PopularAccounts": "Follow some popular accounts",
"Pages.new.PreventFakes": "Prevent fake accounts from imitating you",
"Pages.new.PreviewOnSnort": "Preview on snort",
"Pages.new.Ready": "You're ready!",
"Pages.new.SaveKeys": "Save your keys!",
"Pages.new.SaveKeysHelp": "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.",
"Pages.new.SetupProfile": "Setup your Profile",
"Pages.new.Share": "Share your thoughts with {link}",
"Pages.new.Skip": "Skip",
"Pages.new.TamperProof": "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.",
"Pages.new.TwitterPlaceholder": "Twitter username...",
"Pages.new.TwitterUsername": "Twitter username",
"Pages.new.Username": "Username",
"Pages.new.UsernameHelp": "On Nostr, many people have the same username. User names and identity are separate things. You can get a unique identifier in the next step.",
"Pages.new.UsernamePlaceholder": "e.g. Jack",
"Pages.new.WhatIsSnort": "What is Snort and how does it work?",
"Pages.new.WhatIsSnortExperience": "Snort is designed to have a similar experience to Twitter.",
"Pages.new.WhatIsSnortIntro": "Snort is a Nostr UI, nostr is a decentralised protocol for saving and distributing \"notes\".",
"Pages.new.WhatIsSnortNotes": "Notes hold text content, the most popular usage of these notes is to store \"tweet like\" messages.",
"Pages.new.World": "the world",
"Pages.new.YourPrivkey": "Your private key",
"Pages.new.YourPubkey": "Your public key",
"Pages.settings.About": "About",
"Pages.settings.Add": "Add",
"Pages.settings.AddRelays": "Add Relays",

View File

@ -21,6 +21,7 @@
"Element.DisalledLater": "el nombre estará disponible más tarde",
"Element.Disallowed": "el nombre no está permitido",
"Element.Dislike": "{n} No me gusta",
"Element.DislikeAction": "",
"Element.Dislikes": "No me gusta ({n})",
"Element.Expired": "Caducada",
"Element.FindMore": "Aprende más sobre {service} en {link}",
@ -30,6 +31,7 @@
"Element.FollowingCount": "Siguiendo a {n}",
"Element.FollowsYou": "te sigue",
"Element.GoTo": "Ir a",
"Element.Handle": "",
"Element.Invoice": "Factura Lightning",
"Element.InvoiceFail": "Error al cargar factura",
"Element.JustNow": "Justo ahora",
@ -115,6 +117,53 @@
"Pages.Settings": "Configuración",
"Pages.SnortSocialNip": "Nuestro servicio de verificación NIP-05, apoya el desarrollo de este proyecto y obtén una apariencia especial en nuestra web!",
"Pages.Zaps": "Zaps",
"Pages.new.Bitcoin": "",
"Pages.new.Check": "",
"Pages.new.DigitalSignatures": "",
"Pages.new.Done": "",
"Pages.new.EasierToFind": "",
"Pages.new.Extensions": "",
"Pages.new.ExtensionsNostr": "",
"Pages.new.FailedToLoad": "",
"Pages.new.FindYourFollows": "",
"Pages.new.FollowsOnNostr": "",
"Pages.new.Funding": "",
"Pages.new.GetPartnerId": "",
"Pages.new.GetPartnerIdHelp": "",
"Pages.new.GetSnortId": "",
"Pages.new.GetSnortIdHelp": "",
"Pages.new.HowKeysWork": "",
"Pages.new.Identifier": "",
"Pages.new.IdentifierHelp": "",
"Pages.new.ImportTwitter": "",
"Pages.new.ImproveSecurity": "",
"Pages.new.KeysSaved": "",
"Pages.new.NameSquatting": "",
"Pages.new.Next": "",
"Pages.new.NoUsersFound": "",
"Pages.new.PickUsername": "",
"Pages.new.PopularAccounts": "",
"Pages.new.PreventFakes": "",
"Pages.new.PreviewOnSnort": "",
"Pages.new.Ready": "",
"Pages.new.SaveKeys": "",
"Pages.new.SaveKeysHelp": "",
"Pages.new.SetupProfile": "",
"Pages.new.Share": "",
"Pages.new.Skip": "",
"Pages.new.TamperProof": "",
"Pages.new.TwitterPlaceholder": "",
"Pages.new.TwitterUsername": "",
"Pages.new.Username": "",
"Pages.new.UsernameHelp": "",
"Pages.new.UsernamePlaceholder": "",
"Pages.new.WhatIsSnort": "",
"Pages.new.WhatIsSnortExperience": "",
"Pages.new.WhatIsSnortIntro": "",
"Pages.new.WhatIsSnortNotes": "",
"Pages.new.World": "",
"Pages.new.YourPrivkey": "",
"Pages.new.YourPubkey": "",
"Pages.settings.About": "Bio",
"Pages.settings.Add": "Añadir",
"Pages.settings.AddRelays": "Añadir Relays",

View File

@ -21,6 +21,7 @@
"Element.DisalledLater": "le nom sera disponible plus tard",
"Element.Disallowed": "le nom est bloqué",
"Element.Dislike": "{n} Disliker",
"Element.DislikeAction": "",
"Element.Dislikes": "Dislikes ({n})",
"Element.Expired": "Expiré",
"Element.FindMore": "En savoir plus sur {service} sur {link}",
@ -30,6 +31,7 @@
"Element.FollowingCount": "Follow {n}",
"Element.FollowsYou": "vous follow",
"Element.GoTo": "Aller à",
"Element.Handle": "",
"Element.Invoice": "Facture Lightning",
"Element.InvoiceFail": "Échec du chargement de la facture",
"Element.JustNow": "Juste maintenant",
@ -115,6 +117,53 @@
"Pages.Settings": "Paramètres",
"Pages.SnortSocialNip": "Notre propre service de vérification NIP-05, aidez à soutenir le développement de ce site et obtenez un badge spécial brillant sur notre site !",
"Pages.Zaps": "Zaps",
"Pages.new.Bitcoin": "",
"Pages.new.Check": "",
"Pages.new.DigitalSignatures": "",
"Pages.new.Done": "",
"Pages.new.EasierToFind": "",
"Pages.new.Extensions": "",
"Pages.new.ExtensionsNostr": "",
"Pages.new.FailedToLoad": "",
"Pages.new.FindYourFollows": "",
"Pages.new.FollowsOnNostr": "",
"Pages.new.Funding": "",
"Pages.new.GetPartnerId": "",
"Pages.new.GetPartnerIdHelp": "",
"Pages.new.GetSnortId": "",
"Pages.new.GetSnortIdHelp": "",
"Pages.new.HowKeysWork": "",
"Pages.new.Identifier": "",
"Pages.new.IdentifierHelp": "",
"Pages.new.ImportTwitter": "",
"Pages.new.ImproveSecurity": "",
"Pages.new.KeysSaved": "",
"Pages.new.NameSquatting": "",
"Pages.new.Next": "",
"Pages.new.NoUsersFound": "",
"Pages.new.PickUsername": "",
"Pages.new.PopularAccounts": "",
"Pages.new.PreventFakes": "",
"Pages.new.PreviewOnSnort": "",
"Pages.new.Ready": "",
"Pages.new.SaveKeys": "",
"Pages.new.SaveKeysHelp": "",
"Pages.new.SetupProfile": "",
"Pages.new.Share": "",
"Pages.new.Skip": "",
"Pages.new.TamperProof": "",
"Pages.new.TwitterPlaceholder": "",
"Pages.new.TwitterUsername": "",
"Pages.new.Username": "",
"Pages.new.UsernameHelp": "",
"Pages.new.UsernamePlaceholder": "",
"Pages.new.WhatIsSnort": "",
"Pages.new.WhatIsSnortExperience": "",
"Pages.new.WhatIsSnortIntro": "",
"Pages.new.WhatIsSnortNotes": "",
"Pages.new.World": "",
"Pages.new.YourPrivkey": "",
"Pages.new.YourPubkey": "",
"Pages.settings.About": "About",
"Pages.settings.Add": "Ajouter",
"Pages.settings.AddRelays": "Ajouter Relais",

View File

@ -21,6 +21,7 @@
"Element.DisalledLater": "名前は後で使用できるようになります",
"Element.Disallowed": "名前がブロックされています",
"Element.Dislike": "{n}イヤ",
"Element.DislikeAction": "",
"Element.Dislikes": "イヤ ({n})",
"Element.Expired": "失効",
"Element.FindMore": " {service} の詳細を {link}で",
@ -30,6 +31,7 @@
"Element.FollowingCount": "フォロー {n}",
"Element.FollowsYou": "はあなたをフォローしています",
"Element.GoTo": "開く:",
"Element.Handle": "",
"Element.Invoice": "Lightningインボイス",
"Element.InvoiceFail": "インボイスの読み込みに失敗しました",
"Element.JustNow": "たった今",
@ -115,6 +117,53 @@
"Pages.Settings": "設定",
"Pages.SnortSocialNip": "私たち独自のNIP-05認証サービスです。このサイトの開発を支援し、ピカピカの特別なバッジを私たちのサイトで使えるようになります",
"Pages.Zaps": "Zap",
"Pages.new.Bitcoin": "",
"Pages.new.Check": "",
"Pages.new.DigitalSignatures": "",
"Pages.new.Done": "",
"Pages.new.EasierToFind": "",
"Pages.new.Extensions": "",
"Pages.new.ExtensionsNostr": "",
"Pages.new.FailedToLoad": "",
"Pages.new.FindYourFollows": "",
"Pages.new.FollowsOnNostr": "",
"Pages.new.Funding": "",
"Pages.new.GetPartnerId": "",
"Pages.new.GetPartnerIdHelp": "",
"Pages.new.GetSnortId": "",
"Pages.new.GetSnortIdHelp": "",
"Pages.new.HowKeysWork": "",
"Pages.new.Identifier": "",
"Pages.new.IdentifierHelp": "",
"Pages.new.ImportTwitter": "",
"Pages.new.ImproveSecurity": "",
"Pages.new.KeysSaved": "",
"Pages.new.NameSquatting": "",
"Pages.new.Next": "",
"Pages.new.NoUsersFound": "",
"Pages.new.PickUsername": "",
"Pages.new.PopularAccounts": "",
"Pages.new.PreventFakes": "",
"Pages.new.PreviewOnSnort": "",
"Pages.new.Ready": "",
"Pages.new.SaveKeys": "",
"Pages.new.SaveKeysHelp": "",
"Pages.new.SetupProfile": "",
"Pages.new.Share": "",
"Pages.new.Skip": "",
"Pages.new.TamperProof": "",
"Pages.new.TwitterPlaceholder": "",
"Pages.new.TwitterUsername": "",
"Pages.new.Username": "",
"Pages.new.UsernameHelp": "",
"Pages.new.UsernamePlaceholder": "",
"Pages.new.WhatIsSnort": "",
"Pages.new.WhatIsSnortExperience": "",
"Pages.new.WhatIsSnortIntro": "",
"Pages.new.WhatIsSnortNotes": "",
"Pages.new.World": "",
"Pages.new.YourPrivkey": "",
"Pages.new.YourPubkey": "",
"Pages.settings.About": "自己紹介",
"Pages.settings.Add": "追加",
"Pages.settings.AddRelays": "リレーを追加する",

View File

@ -21,6 +21,7 @@
"Element.DisalledLater": "",
"Element.Disallowed": "",
"Element.Dislike": "",
"Element.DislikeAction": "",
"Element.Dislikes": "",
"Element.Expired": "",
"Element.FindMore": "",
@ -30,6 +31,7 @@
"Element.FollowingCount": "",
"Element.FollowsYou": "",
"Element.GoTo": "",
"Element.Handle": "",
"Element.Invoice": "",
"Element.InvoiceFail": "",
"Element.JustNow": "",
@ -115,6 +117,53 @@
"Pages.Settings": "",
"Pages.SnortSocialNip": "",
"Pages.Zaps": "",
"Pages.new.Bitcoin": "",
"Pages.new.Check": "",
"Pages.new.DigitalSignatures": "",
"Pages.new.Done": "",
"Pages.new.EasierToFind": "",
"Pages.new.Extensions": "",
"Pages.new.ExtensionsNostr": "",
"Pages.new.FailedToLoad": "",
"Pages.new.FindYourFollows": "",
"Pages.new.FollowsOnNostr": "",
"Pages.new.Funding": "",
"Pages.new.GetPartnerId": "",
"Pages.new.GetPartnerIdHelp": "",
"Pages.new.GetSnortId": "",
"Pages.new.GetSnortIdHelp": "",
"Pages.new.HowKeysWork": "",
"Pages.new.Identifier": "",
"Pages.new.IdentifierHelp": "",
"Pages.new.ImportTwitter": "",
"Pages.new.ImproveSecurity": "",
"Pages.new.KeysSaved": "",
"Pages.new.NameSquatting": "",
"Pages.new.Next": "",
"Pages.new.NoUsersFound": "",
"Pages.new.PickUsername": "",
"Pages.new.PopularAccounts": "",
"Pages.new.PreventFakes": "",
"Pages.new.PreviewOnSnort": "",
"Pages.new.Ready": "",
"Pages.new.SaveKeys": "",
"Pages.new.SaveKeysHelp": "",
"Pages.new.SetupProfile": "",
"Pages.new.Share": "",
"Pages.new.Skip": "",
"Pages.new.TamperProof": "",
"Pages.new.TwitterPlaceholder": "",
"Pages.new.TwitterUsername": "",
"Pages.new.Username": "",
"Pages.new.UsernameHelp": "",
"Pages.new.UsernamePlaceholder": "",
"Pages.new.WhatIsSnort": "",
"Pages.new.WhatIsSnortExperience": "",
"Pages.new.WhatIsSnortIntro": "",
"Pages.new.WhatIsSnortNotes": "",
"Pages.new.World": "",
"Pages.new.YourPrivkey": "",
"Pages.new.YourPubkey": "",
"Pages.settings.About": "",
"Pages.settings.Add": "",
"Pages.settings.AddRelays": "",