Preferences page

This commit is contained in:
2023-08-21 14:58:57 +01:00
parent 35423cc91b
commit 976f841d0b
45 changed files with 484 additions and 467 deletions

View File

@ -11,19 +11,21 @@ interface AvatarProps {
user?: UserMetadata;
onClick?: () => void;
size?: number;
image?: string;
}
const Avatar = ({ user, size, onClick }: AvatarProps) => {
const Avatar = ({ user, size, onClick, image }: AvatarProps) => {
const [url, setUrl] = useState<string>(Nostrich);
const { proxy } = useImgProxy();
useEffect(() => {
if (user?.picture) {
const url = proxy(user.picture, size ?? 120);
setUrl(url);
const url = image ?? user?.picture;
if (url) {
const proxyUrl = proxy(url, size ?? 120);
setUrl(proxyUrl);
} else {
setUrl(Nostrich);
}
}, [user]);
}, [user, image]);
const backgroundImage = `url(${url})`;
const style = { "--img-url": backgroundImage } as CSSProperties;

View File

@ -0,0 +1,20 @@
.avatar .edit,
.banner .edit {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background-color: var(--bg-color);
cursor: pointer;
opacity: 0;
border-radius: 100%;
}
.avatar .edit.new {
opacity: 0.5;
}
.avatar .edit:hover {
opacity: 0.5;
}

View File

@ -1,7 +1,9 @@
import "./AvatarEditor.css";
import Icon from "Icons/Icon";
import { useState } from "react";
import useFileUpload from "Upload";
import { openFile, unwrap } from "SnortUtils";
import Spinner from "Icons/Spinner";
interface AvatarEditorProps {
picture?: string;
@ -11,9 +13,11 @@ interface AvatarEditorProps {
export default function AvatarEditor({ picture, onPictureChange }: AvatarEditorProps) {
const uploader = useFileUpload();
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
async function uploadFile() {
setError("");
setLoading(true);
try {
const f = await openFile();
if (f) {
@ -32,6 +36,7 @@ export default function AvatarEditor({ picture, onPictureChange }: AvatarEditorP
setError(`Upload failed`);
}
}
setLoading(false);
}
return (
@ -39,7 +44,7 @@ export default function AvatarEditor({ picture, onPictureChange }: AvatarEditorP
<div className="flex f-center">
<div style={{ backgroundImage: `url(${picture})` }} className="avatar">
<div className={`edit${picture ? "" : " new"}`} onClick={() => uploadFile().catch(console.error)}>
<Icon name={picture ? "edit" : "camera-plus"} />
{loading ? <Spinner /> : <Icon name={picture ? "edit" : "camera-plus"} />}
</div>
</div>
</div>

View File

@ -41,6 +41,8 @@ export function updatePreferences(state: LoginSession, p: UserPreferences) {
export function logout(k: HexKey) {
LoginStore.removeSession(k);
//TODO: delete giftwarps for:k
//TODO: delete notifications for:k
}
export function markNotificationsRead(state: LoginSession) {

View File

@ -158,6 +158,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
const pk = unwrap(s.publicKey);
if (this.#accounts.has(pk)) {
this.#accounts.set(pk, s);
console.debug("SET SESSION", s);
this.#save();
}
}
@ -175,7 +176,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
const s = this.#activeAccount ? this.#accounts.get(this.#activeAccount) : undefined;
if (!s) return LoggedOut;
return s;
return { ...s };
}
#createPublisher(l: LoginSession) {

View File

@ -74,7 +74,7 @@ const DonatePage = () => {
}
return (
<div className="main-content m5">
<div className="main-content p">
<h2>
<FormattedMessage defaultMessage="Help fund the development of Snort" />
</h2>

View File

@ -5,8 +5,6 @@ import Nip5Service from "Element/Nip5Service";
import messages from "./messages";
import "./Verification.css";
export const SnortNostrAddressService = {
name: "Snort",
service: `${ApiHost}/api/v1/n5sp`,
@ -26,11 +24,11 @@ export const Nip5Services = [
},
];
export default function VerificationPage() {
export default function NostrAddressPage() {
return (
<div className="main-content verification">
<div className="main-content p">
<h2>
<FormattedMessage {...messages.GetVerified} />
<FormattedMessage defaultMessage="Buy nostr address" />
</h2>
<p>
<FormattedMessage {...messages.Nip05} />

View File

@ -86,7 +86,7 @@ export default function NotificationsPage() {
const timeGrouped = useMemo(() => {
return orderDescending([...notifications])
.filter(a => !isMuted(a.pubkey))
.filter(a => !isMuted(a.pubkey) && findTag(a, "p") === login.publicKey)
.reduce((acc, v) => {
const key = `${timeKey(v)}:${notificationContext(v as TaggedRawEvent)?.encode()}:${v.kind}`;
if (acc.has(key)) {

View File

@ -333,11 +333,19 @@ export const RootRoutes = [
},
{
path: "trending/people",
element: <TrendingUsers />,
element: (
<div className="p">
<TrendingUsers />
</div>
),
},
{
path: "suggested",
element: <SuggestedProfiles />,
element: (
<div className="p">
<SuggestedProfiles />
</div>
),
},
{
path: "/t/:tag",

View File

@ -29,31 +29,33 @@ export const SettingsRoutes: RouteObject[] = [
{
path: "",
element: <SettingsIndex />,
children: [
{
path: "profile",
element: <Profile />,
},
{
path: "relays",
element: <Relay />,
},
{
path: "relays/:id",
element: <RelayInfo />,
},
{
path: "preferences",
element: <Preferences />,
},
{
path: "accounts",
element: <AccountsPage />,
},
{
path: "keys",
element: <ExportKeys />,
},
...ManageHandleRoutes,
...WalletSettingsRoutes,
],
},
{
path: "profile",
element: <Profile />,
},
{
path: "relays",
element: <Relay />,
},
{
path: "relays/:id",
element: <RelayInfo />,
},
{
path: "preferences",
element: <Preferences />,
},
{
path: "accounts",
element: <AccountsPage />,
},
{
path: "keys",
element: <ExportKeys />,
},
...ManageHandleRoutes,
...WalletSettingsRoutes,
];

View File

@ -1,3 +0,0 @@
.verification a {
color: var(--highlight);
}

View File

@ -204,15 +204,11 @@ export default function WalletPage() {
}
return (
<div className="main-content">
<div className="main-content p">
{error && <b className="error">{error}</b>}
{walletList()}
{unlockWallet()}
{walletInfo()}
<button onClick={() => Wallets.remove(unwrap(walletState.config).id)}>
<FormattedMessage defaultMessage="Delete Account" />
</button>
</div>
);
}

View File

@ -1,3 +1,7 @@
.zap-pool input[type="range"] {
width: 200px;
}
.zap-pool h4 {
margin: 0;
}

View File

@ -92,7 +92,7 @@ export default function ZapPoolPage() {
const sumPending = zapPool.reduce((acc, v) => acc + v.sum, 0);
return (
<div className="zap-pool main-content">
<div className="zap-pool main-content p">
<h1>
<FormattedMessage defaultMessage="Zap Pool" />
</h1>
@ -150,7 +150,7 @@ export default function ZapPoolPage() {
</AsyncButton>
)}
</p>
<div className="card">
<div>
<ZapTarget
target={
zapPool.find(b => b.pubkey === bech32ToHex(SnortPubKey) && b.type === ZapPoolRecipientType.Generic) ?? {
@ -166,7 +166,7 @@ export default function ZapPoolPage() {
<FormattedMessage defaultMessage="Relays" />
</h3>
{relayConnections.map(a => (
<div className="card">
<div>
<h4>{getRelayName(a.address)}</h4>
<ZapTarget
target={
@ -184,7 +184,7 @@ export default function ZapPoolPage() {
<FormattedMessage defaultMessage="File hosts" />
</h3>
{UploaderServices.map(a => (
<div className="card">
<div>
<h4>{a.name}</h4>
<ZapTarget
target={
@ -202,7 +202,7 @@ export default function ZapPoolPage() {
<FormattedMessage defaultMessage="Data Providers" />
</h3>
{DataProviders.map(a => (
<div className="card">
<div>
<h4>{a.name}</h4>
<ZapTarget
target={

View File

@ -25,7 +25,7 @@ export default function DiscoverFollows() {
}
return (
<div className="main-content new-user" dir="auto">
<div className="main-content new-user p" dir="auto">
<Logo />
<div className="progress-bar">
<div className="progress"></div>
@ -45,6 +45,9 @@ export default function DiscoverFollows() {
<FormattedMessage {...messages.PopularAccounts} />
</h3>
{sortedReccomends.length > 0 && <FollowListBase pubkeys={sortedReccomends} showAbout={true} />}
<h3>
<FormattedMessage defaultMessage="Trending Users" />
</h3>
<TrendingUsers />
</div>
);

View File

@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom";
import { useUserProfile } from "@snort/system-react";
import Logo from "Element/Logo";
import { Nip5Services } from "Pages/Verification";
import { Nip5Services } from "Pages/NostrAddressPage";
import Nip5Service from "Element/Nip5Service";
import ProfileImage from "Element/ProfileImage";
import useLogin from "Hooks/useLogin";

View File

@ -90,7 +90,7 @@ export default function NewUserFlow() {
const navigate = useNavigate();
return (
<div className="main-content new-user" dir="auto">
<div className="main-content new-user p" dir="auto">
<Logo />
<div className="progress-bar">
<div className="progress progress-first"></div>
@ -98,31 +98,25 @@ export default function NewUserFlow() {
<h1>
<FormattedMessage {...messages.SaveKeys} />
</h1>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<FormattedMessage defaultMessage="Language" />
</div>
</div>
<div>
<select
value={login.preferences.language || DefaultPreferences.language}
onChange={e =>
updatePreferences(login, {
...login.preferences,
language: e.target.value,
})
}
style={{ textTransform: "capitalize" }}>
{AllLanguageCodes.sort().map(a => (
<option value={a}>
{new Intl.DisplayNames([a], {
type: "language",
}).of(a)}
</option>
))}
</select>
</div>
<div className="flex f-space">
<FormattedMessage defaultMessage="Language" />
<select
value={login.preferences.language || DefaultPreferences.language}
onChange={e =>
updatePreferences(login, {
...login.preferences,
language: e.target.value,
})
}
style={{ textTransform: "capitalize" }}>
{AllLanguageCodes.sort().map(a => (
<option value={a}>
{new Intl.DisplayNames([a], {
type: "language",
}).of(a)}
</option>
))}
</select>
</div>
<p>
<FormattedMessage {...messages.SaveKeysHelp} />

View File

@ -47,7 +47,7 @@ export default function ProfileSetup() {
};
return (
<div className="main-content new-user" dir="auto">
<div className="main-content new-user p" dir="auto">
<Logo />
<div className="progress-bar">
<div className="progress progress-second"></div>

View File

@ -1,4 +1,4 @@
.export-keys > .copy {
.copy.dashed {
padding: 12px 16px;
border: 2px dashed #222222;
border-radius: 16px;

View File

@ -10,18 +10,18 @@ import { hexToBech32 } from "SnortUtils";
export default function ExportKeys() {
const { publicKey, privateKey, generatedEntropy } = useLogin();
return (
<div className="export-keys">
<div className="flex-column g12">
<h3>
<FormattedMessage defaultMessage="Public Key" />
</h3>
<Copy text={hexToBech32("npub", publicKey ?? "")} maxSize={48} className="mb10" />
<Copy text={encodeTLV(NostrPrefix.Profile, publicKey ?? "")} maxSize={48} />
<Copy text={hexToBech32("npub", publicKey ?? "")} className="dashed" />
<Copy text={encodeTLV(NostrPrefix.Profile, publicKey ?? "")} className="dashed" />
{privateKey && (
<>
<h3>
<FormattedMessage defaultMessage="Private Key" />
</h3>
<Copy text={hexToBech32("nsec", privateKey)} maxSize={48} />
<Copy text={hexToBech32("nsec", privateKey)} className="dashed" />
</>
)}
{generatedEntropy && (
@ -29,7 +29,7 @@ export default function ExportKeys() {
<h3>
<FormattedMessage defaultMessage="Mnemonic" />
</h3>
<Copy text={hexToMnemonic(generatedEntropy ?? "")} maxSize={48} />
<Copy text={hexToMnemonic(generatedEntropy ?? "")} className="dashed" />
</>
)}
</div>

View File

@ -1,8 +1,14 @@
.preferences small {
margin-top: 0.5em;
color: var(--font-secondary-color);
}
.preferences select {
min-width: 100px;
}
.preferences h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
line-height: 22px; /* 137.5% */
}

View File

@ -32,6 +32,7 @@ export const AllLanguageCodes = [
const PreferencesPage = () => {
const { formatMessage } = useIntl();
const login = useLogin();
console.debug(login);
const perf = login.preferences;
const [emoji, setEmoji] = useState<Array<{ name: string; char: string }>>([]);
@ -42,17 +43,15 @@ const PreferencesPage = () => {
}, []);
return (
<div className="preferences">
<div className="preferences flex-column g24">
<h3>
<FormattedMessage {...messages.Preferences} />
</h3>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<FormattedMessage defaultMessage="Language" />
</div>
</div>
<div className="flex f-space w-max">
<h4>
<FormattedMessage defaultMessage="Language" />
</h4>
<div>
<select
value={perf.language || DefaultPreferences.language}
@ -73,12 +72,10 @@ const PreferencesPage = () => {
</select>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<FormattedMessage {...messages.Theme} />
</div>
</div>
<div className="flex f-space w-max">
<h4>
<FormattedMessage {...messages.Theme} />
</h4>
<div>
<select
value={perf.theme}
@ -100,12 +97,10 @@ const PreferencesPage = () => {
</select>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<FormattedMessage {...messages.DefaultRootTab} />
</div>
</div>
<div className="flex f-space w-max">
<h4>
<FormattedMessage {...messages.DefaultRootTab} />
</h4>
<div>
<select
value={perf.defaultRootTab}
@ -127,16 +122,17 @@ const PreferencesPage = () => {
</select>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<div className="flex w-max">
<div className="flex-column g8">
<h4>
<FormattedMessage {...messages.AutoloadMedia} />
</div>
</h4>
<small>
<FormattedMessage {...messages.AutoloadMediaHelp} />
</small>
<div className="mt10">
<div className="w-max">
<select
className="w-max"
value={perf.autoLoadMedia}
onChange={e =>
updatePreferences(login, {
@ -157,11 +153,11 @@ const PreferencesPage = () => {
</div>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<div className="flex f-space w-max">
<div className="flex-column g8">
<h4>
<FormattedMessage defaultMessage="Proof of Work" />
</div>
</h4>
<small>
<FormattedMessage defaultMessage="Amount of work to apply to all published events" />
</small>
@ -175,12 +171,10 @@ const PreferencesPage = () => {
/>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<FormattedMessage defaultMessage="Default Zap amount" />
</div>
</div>
<div className="flex f-space w-max">
<h4>
<FormattedMessage defaultMessage="Default Zap amount" />
</h4>
<div>
<input
type="number"
@ -190,11 +184,11 @@ const PreferencesPage = () => {
/>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<div className="flex f-space w-max">
<div className="flex-column g8">
<h4>
<FormattedMessage defaultMessage="Auto Zap" />
</div>
</h4>
<small>
<FormattedMessage defaultMessage="Automatically zap every note when loaded" />
</small>
@ -207,12 +201,12 @@ const PreferencesPage = () => {
/>
</div>
</div>
<div className="card flex f-col">
<div className="flex w-max">
<div className="flex f-col f-grow">
<div>
<div className="flex-column">
<div className="flex f-space">
<div className="flex-column g8">
<h4>
<FormattedMessage {...messages.ImgProxy} />
</div>
</h4>
<small>
<FormattedMessage {...messages.ImgProxyHelp} />
</small>
@ -231,7 +225,7 @@ const PreferencesPage = () => {
</div>
</div>
{perf.imgProxyConfig && (
<div className="w-max mt10 form">
<div className="w-max form">
<div className="form-group">
<div>
<FormattedMessage {...messages.ServiceUrl} />
@ -307,11 +301,11 @@ const PreferencesPage = () => {
</div>
)}
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<div className="flex f-space w-max">
<div className="flex-column g8">
<h4>
<FormattedMessage {...messages.EnableReactions} />
</div>
</h4>
<small>
<FormattedMessage {...messages.EnableReactionsHelp} />
</small>
@ -324,43 +318,39 @@ const PreferencesPage = () => {
/>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<FormattedMessage {...messages.ReactionEmoji} />
</div>
<small>
<FormattedMessage {...messages.ReactionEmojiHelp} />
</small>
<div className="mt10">
<select
className="emoji-selector"
value={perf.reactionEmoji}
onChange={e =>
updatePreferences(login, {
...perf,
reactionEmoji: e.target.value,
})
}>
<option value="+">
+ <FormattedMessage {...messages.Default} />
<div className="flex-column g8">
<h4>
<FormattedMessage {...messages.ReactionEmoji} />
</h4>
<small>
<FormattedMessage {...messages.ReactionEmojiHelp} />
</small>
<select
className="emoji-selector"
value={perf.reactionEmoji}
onChange={e =>
updatePreferences(login, {
...perf,
reactionEmoji: e.target.value,
})
}>
<option value="+">
+ <FormattedMessage {...messages.Default} />
</option>
{emoji.map(({ name, char }) => {
return (
<option value={char}>
{name} {char}
</option>
{emoji.map(({ name, char }) => {
return (
<option value={char}>
{name} {char}
</option>
);
})}
</select>
</div>
</div>
);
})}
</select>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<div className="flex f-space">
<div className="flex-column g8">
<h4>
<FormattedMessage {...messages.ConfirmReposts} />
</div>
</h4>
<small>
<FormattedMessage {...messages.ConfirmRepostsHelp} />
</small>
@ -373,11 +363,11 @@ const PreferencesPage = () => {
/>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<div className="flex f-space">
<div className="flex-column g8">
<h4>
<FormattedMessage {...messages.ShowLatest} />
</div>
</h4>
<small>
<FormattedMessage {...messages.ShowLatestHelp} />
</small>
@ -390,37 +380,33 @@ const PreferencesPage = () => {
/>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<FormattedMessage {...messages.FileUpload} />
</div>
<small>
<FormattedMessage {...messages.FileUploadHelp} />
</small>
<div className="mt10">
<select
value={perf.fileUploader}
onChange={e =>
updatePreferences(login, {
...perf,
fileUploader: e.target.value,
} as UserPreferences)
}>
<option value="void.cat">
void.cat <FormattedMessage {...messages.Default} />
</option>
<option value="nostr.build">nostr.build</option>
<option value="nostrimg.com">nostrimg.com</option>
</select>
</div>
</div>
<div className="flex-column g8">
<h4>
<FormattedMessage {...messages.FileUpload} />
</h4>
<small>
<FormattedMessage {...messages.FileUploadHelp} />
</small>
<select
value={perf.fileUploader}
onChange={e =>
updatePreferences(login, {
...perf,
fileUploader: e.target.value,
} as UserPreferences)
}>
<option value="void.cat">
void.cat <FormattedMessage {...messages.Default} />
</option>
<option value="nostr.build">nostr.build</option>
<option value="nostrimg.com">nostrimg.com</option>
</select>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<div className="flex f-space">
<div className="flex-column g8">
<h4>
<FormattedMessage {...messages.DebugMenus} />
</div>
</h4>
<small>
<FormattedMessage {...messages.DebugMenusHelp} />
</small>

View File

@ -1,53 +1,51 @@
.settings h4 {
font-size: 16px;
font-weight: 600;
margin: 0;
}
.settings .avatar {
width: 256px;
height: 256px;
width: 64px;
height: 64px;
background-size: cover;
border-radius: 100%;
cursor: pointer;
margin-bottom: 20px;
}
.settings .banner {
width: 300px;
height: 150px;
width: 100%;
height: 100%;
background-size: cover;
margin-bottom: 20px;
border-radius: 12px;
}
.settings .image-settings {
display: block;
align-items: center;
justify-content: center;
}
.settings .image-setting {
display: flex;
justify-content: space-between;
}
.settings .image-setting > div:first-child {
align-self: center;
}
.avatar .edit,
.banner .edit {
display: flex;
align-items: center;
justify-content: center;
position: relative;
width: 100%;
height: 100%;
background-color: var(--bg-color);
cursor: pointer;
opacity: 0;
border-radius: 100%;
height: 145px;
margin-block-end: 45px;
}
.avatar .edit.new {
opacity: 0.5;
.settings .image-settings > div {
position: absolute;
}
.avatar .edit:hover {
opacity: 0.5;
.settings .image-settings .avatar-stack {
bottom: -32px;
}
.settings .image-settings .avatar-stack .btn-rnd {
position: absolute;
padding: 0;
width: 32px;
height: 32px;
right: -10px;
bottom: -10px;
}
.settings .image-settings .banner button {
right: 16px;
top: 12px;
position: absolute;
}
.settings .editor textarea {
@ -59,3 +57,8 @@
.settings .actions {
margin-top: 16px;
}
.settings small {
font-size: 14px;
color: var(--font-secondary-color);
}

View File

@ -13,10 +13,8 @@ import useFileUpload from "Upload";
import AsyncButton from "Element/AsyncButton";
import { UserCache } from "Cache";
import useLogin from "Hooks/useLogin";
import AvatarEditor from "Element/AvatarEditor";
import Icon from "Icons/Icon";
import messages from "./messages";
import Avatar from "Element/Avatar";
export interface ProfileSettingsProps {
avatar?: boolean;
@ -31,7 +29,6 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
const uploader = useFileUpload();
const [name, setName] = useState<string>();
const [displayName, setDisplayName] = useState<string>();
const [picture, setPicture] = useState<string>();
const [banner, setBanner] = useState<string>();
const [about, setAbout] = useState<string>();
@ -39,12 +36,9 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
const [nip05, setNip05] = useState<string>();
const [lud16, setLud16] = useState<string>();
const avatarPicture = (picture?.length ?? 0) === 0 ? Nostrich : picture;
useEffect(() => {
if (user) {
setName(user.name);
setDisplayName(user.display_name);
setPicture(user.picture);
setBanner(user.banner);
setAbout(user.about);
@ -59,7 +53,6 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
const userCopy = {
...user,
name,
display_name: displayName,
about,
picture,
banner,
@ -107,69 +100,62 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
}
}
async function setNewAvatar() {
const rsp = await uploadFile();
if (rsp) {
setPicture(rsp);
}
}
function editor() {
return (
<div className="editor form">
<div className="form-group card">
<div>
<FormattedMessage {...messages.Name} />:
</div>
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<div className="flex f-col g24">
<div className="flex f-col w-max g8">
<h4>
<FormattedMessage defaultMessage="Name" />
</h4>
<input className="w-max" type="text" value={name} onChange={e => setName(e.target.value)} />
</div>
<div className="flex f-col w-max g8">
<h4>
<FormattedMessage defaultMessage="About" />
</h4>
<textarea className="w-max" onChange={e => setAbout(e.target.value)} value={about}></textarea>
</div>
<div className="flex f-col w-max g8">
<h4>
<FormattedMessage defaultMessage="Website" />
</h4>
<input className="w-max" type="text" value={website} onChange={e => setWebsite(e.target.value)} />
</div>
<div className="flex f-col w-max g8">
<h4>
<FormattedMessage defaultMessage="Nostr Address" />
</h4>
<div className="flex f-col g8 w-max">
<input type="text" className="w-max" value={nip05} onChange={e => setNip05(e.target.value)} />
<small>
<FormattedMessage defaultMessage="Usernames are not unique on Nostr. The nostr address is your unique human-readable address that is unique to you upon registration." />
</small>
<div className="flex g12">
<button className="flex f-center" type="button" onClick={() => navigate("/nostr-address")}>
<FormattedMessage defaultMessage="Buy nostr address" />
</button>
<button className="flex f-center secondary" type="button" onClick={() => navigate("/nostr-address")}>
<FormattedMessage defaultMessage="Get a free one" />
</button>
</div>
</div>
</div>
<div className="form-group card">
<div>
<FormattedMessage {...messages.DisplayName} />:
</div>
<div>
<input type="text" value={displayName} onChange={e => setDisplayName(e.target.value)} />
</div>
</div>
<div className="form-group card">
<div>
<FormattedMessage {...messages.About} />:
</div>
<div className="w-max">
<textarea className="w-max" onChange={e => setAbout(e.target.value)} value={about}></textarea>
</div>
</div>
<div className="form-group card">
<div>
<FormattedMessage {...messages.Website} />:
</div>
<div>
<input type="text" value={website} onChange={e => setWebsite(e.target.value)} />
</div>
</div>
<div className="form-group card">
<div>
<FormattedMessage {...messages.Nip05} />:
</div>
<div>
<input type="text" className="mr10" value={nip05} onChange={e => setNip05(e.target.value)} />
<button type="button" onClick={() => navigate("/verification")}>
<Icon name="shopping-bag" />
&nbsp; <FormattedMessage {...messages.Buy} />
</button>
</div>
</div>
<div className="form-group card">
<div>
<FormattedMessage {...messages.LnAddress} />:
</div>
<div>
<input type="text" value={lud16} onChange={e => setLud16(e.target.value)} />
</div>
</div>
<div className="form-group card">
<div></div>
<div>
<AsyncButton onClick={() => saveProfile()}>
<FormattedMessage {...messages.Save} />
</AsyncButton>
</div>
<div className="flex f-col w-max g8">
<h4>
<FormattedMessage defaultMessage="Lightning Address" />
</h4>
<input className="w-max" type="text" value={lud16} onChange={e => setLud16(e.target.value)} />
</div>
<AsyncButton onClick={() => saveProfile()}>
<FormattedMessage defaultMessage="Save" />
</AsyncButton>
</div>
);
}
@ -179,28 +165,23 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
return (
<>
<div className="flex f-center image-settings">
{(props.avatar ?? true) && (
<div className="image-setting card">
<div>
<FormattedMessage {...messages.Avatar} />:
</div>
<AvatarEditor picture={avatarPicture} onPictureChange={p => setPicture(p)} />
{(props.banner ?? true) && (
<div
style={{
backgroundImage: `url(${(banner?.length ?? 0) === 0 ? Nostrich : banner})`,
}}
className="banner">
<AsyncButton type="button" onClick={() => setNewBanner()}>
<FormattedMessage defaultMessage="Upload" />
</AsyncButton>
</div>
)}
{(props.banner ?? true) && (
<div className="image-setting card">
<div>
<FormattedMessage {...messages.Banner} />:
</div>
<div
style={{
backgroundImage: `url(${(banner?.length ?? 0) === 0 ? Nostrich : banner})`,
}}
className="banner">
<div className="edit" onClick={() => setNewBanner()}>
<FormattedMessage {...messages.Edit} />
</div>
</div>
{(props.avatar ?? true) && (
<div className="avatar-stack">
<Avatar user={user} image={picture} />
<AsyncButton type="button" className="btn-rnd" onClick={() => setNewAvatar()}>
<Icon name="upload-01" />
</AsyncButton>
</div>
)}
</div>
@ -209,12 +190,5 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
);
}
return (
<div className="settings">
<h3>
<FormattedMessage {...messages.EditProfile} />
</h3>
{settings()}
</div>
);
return <div className="settings">{settings()}</div>;
}

View File

@ -22,7 +22,7 @@ const RelayInfo = () => {
<h3 className="pointer" onClick={() => navigate("/settings/relays")}>
<FormattedMessage {...messages.Relays} />
</h3>
<div className="card">
<div>
<h3>{stats?.info?.name}</h3>
<p>{stats?.info?.description}</p>

View File

@ -1,19 +1,29 @@
.settings-nav {
display: grid;
grid-template-columns: 237px auto;
}
.settings-nav > div {
border: 1px solid var(--gray-superdark);
}
.settings-nav > div:nth-child(2) {
padding: 12px 16px;
}
.settings-nav .card {
cursor: pointer;
}
.settings-row {
display: grid;
grid-template-columns: 22px 1fr 8px;
grid-template-columns: 24px 1fr 24px;
align-items: center;
font-weight: 600;
font-size: 16px;
padding: 0.8em 1em;
background: var(--note-bg);
border-radius: 10px;
cursor: pointer;
gap: 10px;
margin-bottom: 5px;
padding: 12px 16px;
gap: 8px;
font-size: 16px;
font-weight: 600;
}
.settings-row.inner {
@ -24,12 +34,12 @@
}
.settings-group-header {
font-weight: 600;
align-items: center;
cursor: pointer;
padding: 12px 16px;
gap: 8px;
font-size: 16px;
padding: 0.8em 1em;
background-color: var(--note-bg);
border-radius: 10px;
margin-bottom: 5px;
font-weight: 600;
}
.settings-row:hover,
@ -41,11 +51,6 @@
margin-left: auto;
}
.settings-row svg {
width: 100%;
height: 100%;
}
.settings-group-header .collapse-icon > svg {
width: 8px;
}

View File

@ -1,12 +1,11 @@
import "./Root.css";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { Outlet, useNavigate } from "react-router-dom";
import Icon from "Icons/Icon";
import { LoginStore, logout } from "Login";
import useLogin from "Hooks/useLogin";
import { unwrap } from "SnortUtils";
import { getCurrentSubscription } from "Subscription";
import { CollapsedSection } from "Element/Collapsed";
import messages from "./messages";
@ -21,80 +20,72 @@ const SettingsIndex = () => {
}
return (
<>
<div className="settings-nav">
<CollapsedSection
title={
<div className="flex">
<Icon name="user" className="mr10" />
<FormattedMessage defaultMessage="Account" />
</div>
}
className="settings-group-header">
<div className="card">
<div className="settings-row inner" onClick={() => navigate("profile")}>
<Icon name="profile" />
<FormattedMessage {...messages.Profile} />
<Icon name="arrowFront" />
</div>
<div className="settings-row inner" onClick={() => navigate("relays")}>
<Icon name="relay" />
<FormattedMessage {...messages.Relays} />
<Icon name="arrowFront" />
</div>
<div className="settings-row inner" onClick={() => navigate("keys")}>
<Icon name="key" />
<FormattedMessage defaultMessage="Export Keys" />
<Icon name="arrowFront" />
</div>
<div className="settings-row inner" onClick={() => navigate("handle")}>
<Icon name="badge" />
<FormattedMessage defaultMessage="Nostr Adddress" />
<Icon name="arrowFront" />
</div>
<div className="settings-row inner" onClick={() => navigate("/subscribe/manage")}>
<Icon name="diamond" />
<FormattedMessage defaultMessage="Subscription" />
<Icon name="arrowFront" />
</div>
{sub && (
<div className="settings-row inner" onClick={() => navigate("accounts")}>
<Icon name="code-circle" />
<FormattedMessage defaultMessage="Account Switcher" />
<Icon name="arrowFront" />
</div>
)}
<div className="settings-nav">
<div>
<div className="settings-row" onClick={() => navigate("profile")}>
<Icon name="profile" size={24} />
<FormattedMessage {...messages.Profile} />
<Icon name="arrowFront" size={16} />
</div>
<div className="settings-row" onClick={() => navigate("relays")}>
<Icon name="relay" size={24} />
<FormattedMessage {...messages.Relays} />
<Icon name="arrowFront" size={16} />
</div>
<div className="settings-row" onClick={() => navigate("keys")}>
<Icon name="key" size={24} />
<FormattedMessage defaultMessage="Export Keys" />
<Icon name="arrowFront" size={16} />
</div>
<div className="settings-row" onClick={() => navigate("handle")}>
<Icon name="badge" size={24} />
<FormattedMessage defaultMessage="Nostr Adddress" />
<Icon name="arrowFront" size={16} />
</div>
<div className="settings-row" onClick={() => navigate("/subscribe/manage")}>
<Icon name="diamond" size={24} />
<FormattedMessage defaultMessage="Subscription" />
<Icon name="arrowFront" size={16} />
</div>
{sub && (
<div className="settings-row" onClick={() => navigate("accounts")}>
<Icon name="code-circle" size={24} />
<FormattedMessage defaultMessage="Account Switcher" />
<Icon name="arrowFront" size={16} />
</div>
</CollapsedSection>
)}
<div className="settings-row" onClick={() => navigate("preferences")}>
<Icon name="gear" />
<Icon name="gear" size={24} />
<FormattedMessage {...messages.Preferences} />
<Icon name="arrowFront" />
<Icon name="arrowFront" size={16} />
</div>
<div className="settings-row" onClick={() => navigate("wallet")}>
<Icon name="wallet" />
<Icon name="wallet" size={24} />
<FormattedMessage defaultMessage="Wallet" />
<Icon name="arrowFront" />
<Icon name="arrowFront" size={16} />
</div>
<div className="settings-row" onClick={() => navigate("/donate")}>
<Icon name="heart" />
<Icon name="heart" size={24} />
<FormattedMessage {...messages.Donate} />
<Icon name="arrowFront" />
<Icon name="arrowFront" size={16} />
</div>
<div className="settings-row" onClick={() => navigate("/zap-pool")}>
<Icon name="piggy-bank" />
<Icon name="piggy-bank" size={24} />
<FormattedMessage defaultMessage="Zap Pool" />
<Icon name="arrowFront" />
<Icon name="arrowFront" size={16} />
</div>
<div className="settings-row" onClick={handleLogout}>
<Icon name="logout" />
<Icon name="logout" size={24} />
<FormattedMessage {...messages.LogOut} />
<Icon name="arrowFront" />
<Icon name="arrowFront" size={16} />
</div>
</div>
</>
<div>
<Outlet />
</div>
</div>
);
};

View File

@ -5,10 +5,10 @@
grid-gap: 10px;
}
.wallet-grid .card {
.wallet-grid > div {
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
justify-content: space-between;
}

View File

@ -1,7 +1,7 @@
import "./WalletSettings.css";
import LndLogo from "lnd-logo.png";
import { FormattedMessage } from "react-intl";
import { RouteObject, useNavigate } from "react-router-dom";
import { Link, RouteObject, useNavigate } from "react-router-dom";
import BlueWallet from "Icons/BlueWallet";
import ConnectLNC from "Pages/settings/wallet/LNC";
@ -15,21 +15,26 @@ const WalletSettings = () => {
const navigate = useNavigate();
return (
<>
<Link to="/wallet">
<button type="button">
<FormattedMessage defaultMessage="View Wallets" />
</button>
</Link>
<h3>
<FormattedMessage defaultMessage="Connect Wallet" />
</h3>
<div className="wallet-grid">
<div className="card" onClick={() => navigate("/settings/wallet/lnc")}>
<div onClick={() => navigate("/settings/wallet/lnc")}>
<img src={LndLogo} width={100} />
<h3 className="f-end">LND with LNC</h3>
<h3>LND with LNC</h3>
</div>
<div className="card" onClick={() => navigate("/settings/wallet/lndhub")}>
<div onClick={() => navigate("/settings/wallet/lndhub")}>
<BlueWallet width={100} height={100} />
<h3 className="f-end">LNDHub</h3>
<h3>LNDHub</h3>
</div>
<div className="card" onClick={() => navigate("/settings/wallet/nwc")}>
<div onClick={() => navigate("/settings/wallet/nwc")}>
<NostrIcon width={100} height={100} />
<h3 className="f-end">Nostr Wallet Connect</h3>
<h3>Nostr Wallet Connect</h3>
</div>
</div>
</>

View File

@ -42,7 +42,7 @@ export default function LNForwardAddress({ handle }: { handle: ManageHandle }) {
}
return (
<div className="card">
<div>
<h4>
<FormattedMessage defaultMessage="Update Lightning Address" />
</h4>

View File

@ -29,37 +29,33 @@ export default function ListHandles() {
defaultMessage="It looks like you dont have any, check {link} to buy one!"
values={{
link: (
<Link to="/verification">
<FormattedMessage defaultMessage="Verification" />
<Link to="/nostr-address">
<FormattedMessage defaultMessage="Buy Handle" />
</Link>
),
}}
/>
)}
{handles.map(a => (
<div className="card flex" key={a.id}>
<div className="f-grow">
<h4 className="nip05">
{a.handle}@
<span className="domain" data-domain={a.domain?.toLowerCase()}>
{a.domain}
</span>
</h4>
</div>
<div>
<button
onClick={() =>
navigate("manage", {
state: a,
})
}>
<FormattedMessage defaultMessage="Manage" />
</button>
</div>
<div className="flex f-space" key={a.id}>
<h4 className="nip05">
{a.handle}@
<span className="domain" data-domain={a.domain?.toLowerCase()}>
{a.domain}
</span>
</h4>
<button
onClick={() =>
navigate("manage", {
state: a,
})
}>
<FormattedMessage defaultMessage="Manage" />
</button>
</div>
))}
{handles.length > 0 && (
<button onClick={() => navigate("/verification")}>
<button onClick={() => navigate("/nostr-address")}>
<FormattedMessage defaultMessage="Buy Handle" />
</button>
)}

View File

@ -28,7 +28,7 @@ export default function TransferHandle({ handle }: { handle: ManageHandle }) {
}
return (
<div className="card">
<div>
<h4>
<FormattedMessage defaultMessage="Transfer to Pubkey" />
</h4>

View File

@ -31,7 +31,7 @@ const ConnectCashu = () => {
data: mintUrl,
} as WalletConfig;
Wallets.add(newWallet);
navigate("/wallet");
navigate("/settings/wallet");
} catch (e) {
if (e instanceof Error) {
setError((e as Error).message);

View File

@ -46,7 +46,7 @@ const ConnectLNC = () => {
active: true,
info: unwrap(walletInfo),
});
navigate("/wallet");
navigate("/settings/wallet");
}
function flowConnect() {

View File

@ -29,7 +29,7 @@ const ConnectLNDHub = () => {
} as WalletConfig;
Wallets.add(newWallet);
navigate("/wallet");
navigate("/settings/wallet");
} catch (e) {
if (e instanceof Error) {
setError((e as Error).message);

View File

@ -29,7 +29,7 @@ const ConnectNostrWallet = () => {
} as WalletConfig;
Wallets.add(newWallet);
navigate("/wallet");
navigate("/settings/wallet");
} catch (e) {
if (e instanceof Error) {
setError((e as Error).message);

View File

@ -33,7 +33,7 @@ export default function ManageSubscriptionPage() {
return <PageSpinner />;
}
return (
<>
<div className="main-content p flex-column g24">
<h2>
<FormattedMessage defaultMessage="Subscriptions" />
</h2>
@ -60,6 +60,6 @@ export default function ManageSubscriptionPage() {
</p>
)}
{error && <b className="error">{mapSubscriptionErrorCode(error)}</b>}
</>
</div>
);
}

View File

@ -8,7 +8,7 @@ import Icon from "Icons/Icon";
import useEventPublisher from "Feed/EventPublisher";
import SendSats from "Element/SendSats";
import Nip5Service from "Element/Nip5Service";
import { SnortNostrAddressService } from "Pages/Verification";
import { SnortNostrAddressService } from "Pages/NostrAddressPage";
import Nip05 from "Element/Nip05";
export default function SubscriptionCard({ sub }: { sub: Subscription }) {
@ -62,7 +62,7 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
return (
<>
<div className="card">
<div className="p">
<div className="flex card-title">
<Icon name="badge" className="mr5" size={25} />
{mapPlanName(sub.type)}

View File

@ -1,4 +1,4 @@
.subscribe-page > div.card {
.subscribe-page > div {
margin: 5px;
min-height: 400px;
user-select: none;
@ -18,7 +18,7 @@
flex-direction: column;
}
.subscribe-page > div.card {
.subscribe-page > div {
flex: unset;
}
}

View File

@ -77,11 +77,11 @@ export function SubscribePage() {
return (
<>
<div className="flex subscribe-page">
<div className="flex subscribe-page main-content">
{Plans.map(a => {
const lower = Plans.filter(b => b.id < a.id);
return (
<div className={`card flex f-col${a.disabled ? " disabled" : ""}`}>
<div className={`p flex-column${a.disabled ? " disabled" : ""}`}>
<div className="f-grow">
<h2>{mapPlanName(a.id)}</h2>
<p>

View File

@ -27,14 +27,18 @@ export async function openFile(): Promise<File | undefined> {
return new Promise(resolve => {
const elm = document.createElement("input");
elm.type = "file";
elm.onchange = (e: Event) => {
const handleInput = (e: Event) => {
console.debug(e);
const elm = e.target as HTMLInputElement;
if (elm.files) {
resolve(elm.files[0]);
if ((elm.files?.length ?? 0) > 0) {
resolve(elm.files![0]);
} else {
resolve(undefined);
}
};
elm.onchange = e => handleInput(e);
elm.onblur = e => handleInput(e);
elm.click();
});
}

View File

@ -14,11 +14,11 @@ export class Nip5Task extends BaseUITask {
return (
<p>
<FormattedMessage
defaultMessage="Hey, it looks like you dont have a NIP-05 handle yet, you should get one! Check out {link}"
defaultMessage="Hey, it looks like you dont have a Nostr Address yet, you should get one! Check out {link}"
values={{
link: (
<Link to="/verification">
<FormattedMessage defaultMessage="NIP-05 Shop" />
<Link to="/nostr-address">
<FormattedMessage defaultMessage="Buy nostr address" />
</Link>
),
}}

View File

@ -125,6 +125,10 @@ body #root > div:not(.page) header {
}
}
.p {
padding: 12px 16px;
}
.card {
padding: 12px 16px;
border-bottom: 1px solid var(--gray-superdark);
@ -158,7 +162,6 @@ button {
padding: 6px 12px;
font-weight: 600;
color: white;
min-height: 35px;
font-size: var(--font-size);
background-color: var(--highlight);
border: none;
@ -296,12 +299,13 @@ input[type="password"],
input[type="number"],
select,
textarea {
padding: 12px;
padding: 12px 16px;
color: var(--font-color);
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
outline: none;
line-height: 24px; /* 150% */
}
.light input[type="text"],
@ -338,6 +342,11 @@ input:disabled {
min-width: 0;
}
.flex-column {
display: flex;
flex-direction: column;
}
.f-center {
justify-content: center;
}

View File

@ -19,7 +19,7 @@ import { RootRoutes } from "Pages/Root";
import NotificationsPage from "Pages/Notifications";
import SettingsPage, { SettingsRoutes } from "Pages/SettingsPage";
import ErrorPage from "Pages/ErrorPage";
import VerificationPage from "Pages/Verification";
import NostrAddressPage from "Pages/NostrAddressPage";
import MessagesPage from "Pages/MessagesPage";
import DonatePage from "Pages/DonatePage";
import SearchPage from "Pages/SearchPage";
@ -125,8 +125,8 @@ export const router = createBrowserRouter([
children: SettingsRoutes,
},
{
path: "/verification",
element: <VerificationPage />,
path: "/nostr-address",
element: <NostrAddressPage />,
},
{
path: "/messages/:id?",