feat: new user flow profile editor

This commit is contained in:
Kieran 2023-05-10 11:41:38 +01:00
parent c36a3e0bc1
commit 59038d118e
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
15 changed files with 128 additions and 61 deletions

View File

@ -145,5 +145,12 @@
<symbol id="user" viewBox="0 0 18 20" fill="none">
<path d="M17 19C17 17.6044 17 16.9067 16.8278 16.3389C16.44 15.0605 15.4395 14.06 14.1611 13.6722C13.5933 13.5 12.8956 13.5 11.5 13.5H6.5C5.10444 13.5 4.40665 13.5 3.83886 13.6722C2.56045 14.06 1.56004 15.0605 1.17224 16.3389C1 16.9067 1 17.6044 1 19M13.5 5.5C13.5 7.98528 11.4853 10 9 10C6.51472 10 4.5 7.98528 4.5 5.5C4.5 3.01472 6.51472 1 9 1C11.4853 1 13.5 3.01472 13.5 5.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="camera-plus" viewBox="0 0 22 21" fill="none">
<path d="M21 10.5V13.6C21 15.8402 21 16.9603 20.564 17.816C20.1805 18.5686 19.5686 19.1805 18.816 19.564C17.9603 20 16.8402 20 14.6 20H7.4C5.15979 20 4.03969 20 3.18404 19.564C2.43139 19.1805 1.81947 18.5686 1.43597 17.816C1 16.9603 1 15.8402 1 13.6V8.4C1 6.15979 1 5.03969 1.43597 4.18404C1.81947 3.43139 2.43139 2.81947 3.18404 2.43597C4.03969 2 5.15979 2 7.4 2H11.5M18 7V1M15 4H21M15 11C15 13.2091 13.2091 15 11 15C8.79086 15 7 13.2091 7 11C7 8.79086 8.79086 7 11 7C13.2091 7 15 8.79086 15 11Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="edit" viewBox="0 0 23 23" fill="none">
<path d="M10 3.99998H5.8C4.11984 3.99998 3.27976 3.99998 2.63803 4.32696C2.07354 4.61458 1.6146 5.07353 1.32698 5.63801C1 6.27975 1 7.11983 1 8.79998V17.2C1 18.8801 1 19.7202 1.32698 20.362C1.6146 20.9264 2.07354 21.3854 2.63803 21.673C3.27976 22 4.11984 22 5.8 22H14.2C15.8802 22 16.7202 22 17.362 21.673C17.9265 21.3854 18.3854 20.9264 18.673 20.362C19 19.7202 19 18.8801 19 17.2V13M6.99997 16H8.67452C9.1637 16 9.40829 16 9.63846 15.9447C9.84254 15.8957 10.0376 15.8149 10.2166 15.7053C10.4184 15.5816 10.5914 15.4086 10.9373 15.0627L20.5 5.49998C21.3284 4.67156 21.3284 3.32841 20.5 2.49998C19.6716 1.67156 18.3284 1.67155 17.5 2.49998L7.93723 12.0627C7.59133 12.4086 7.41838 12.5816 7.29469 12.7834C7.18504 12.9624 7.10423 13.1574 7.05523 13.3615C6.99997 13.5917 6.99997 13.8363 6.99997 14.3255V16Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -8,6 +8,7 @@
background-clip: content-box, border-box;
background-size: cover;
box-sizing: border-box;
background-color: var(--gray);
}
.avatar[data-domain="snort.social"] {

View File

@ -0,0 +1,49 @@
import Icon from "Icons/Icon";
import { useState } from "react";
import useFileUpload from "Upload";
import { openFile, unwrap } from "Util";
interface AvatarEditorProps {
picture?: string;
onPictureChange?: (newPicture: string) => void;
}
export default function AvatarEditor({ picture, onPictureChange }: AvatarEditorProps) {
const uploader = useFileUpload();
const [error, setError] = useState("");
async function uploadFile() {
setError("");
try {
const f = await openFile();
if (f) {
const rsp = await uploader.upload(f, f.name);
console.log(rsp);
if (typeof rsp?.error === "string") {
setError(`Upload failed: ${rsp.error}`);
} else {
onPictureChange?.(unwrap(rsp.url));
}
}
} catch (e) {
if (e instanceof Error) {
setError(`Upload failed: ${e.message}`);
} else {
setError(`Upload failed`);
}
}
}
return (
<>
<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"} />
</div>
</div>
</div>
{error && <b className="error">{error}</b>}
</>
);
}

View File

@ -1,12 +1,12 @@
import { ReactNode } from "react";
import { FormattedMessage } from "react-intl";
import { HexKey } from "@snort/nostr";
import useEventPublisher from "Feed/EventPublisher";
import { HexKey } from "@snort/nostr";
import ProfilePreview from "Element/ProfilePreview";
import useLogin from "Hooks/useLogin";
import messages from "./messages";
import useLogin from "Hooks/useLogin";
export interface FollowListBaseProps {
pubkeys: HexKey[];
@ -26,7 +26,7 @@ export default function FollowListBase({ pubkeys, title, showFollowAll, showAbou
}
return (
<div className="main-content">
<>
{(showFollowAll ?? true) && (
<div className="flex mt10 mb10">
<div className="f-grow bold">{title}</div>
@ -38,6 +38,6 @@ export default function FollowListBase({ pubkeys, title, showFollowAll, showAbou
{pubkeys?.map(a => (
<ProfilePreview pubkey={a} key={a} options={{ about: showAbout }} />
))}
</div>
</>
);
}

View File

@ -1,3 +1,7 @@
.hashtag {
color: var(--highlight);
}
.hashtag > a {
text-decoration: none;
}

View File

@ -8,6 +8,10 @@
cursor: pointer;
}
.link-preview-container > a {
text-decoration: none;
}
.link-preview-title {
padding: 0 10px 10px 10px;
}

View File

@ -264,14 +264,7 @@ export default function ProfilePage() {
}
case FOLLOWS: {
if (isMe) {
return (
<>
<button onClick={() => navigate("/new/import")} className="mb10">
<FormattedMessage defaultMessage="Find Twitter follows" />
</button>
<FollowsList pubkeys={follows} showFollowAll={!isMe} showAbout={false} />;
</>
);
return <FollowsList pubkeys={follows} showFollowAll={!isMe} showAbout={false} />;
} else {
return <FollowsTab id={id} />;
}
@ -379,7 +372,7 @@ export default function ProfilePage() {
{isMe && blocked.length > 0 && renderTab(ProfileTab.Blocked)}
</div>
</div>
{tabContent()}
<div className="main-content">{tabContent()}</div>
</>
);
}

View File

@ -44,7 +44,7 @@ export default function DiscoverFollows() {
<h3>
<FormattedMessage {...messages.PopularAccounts} />
</h3>
<div>{sortedReccomends.length > 0 && <FollowListBase pubkeys={sortedReccomends} showAbout={true} />}</div>
{sortedReccomends.length > 0 && <FollowListBase pubkeys={sortedReccomends} showAbout={true} />}
<TrendingUsers />
</div>
);

View File

@ -9,6 +9,7 @@ import { hexToMnemonic } from "nip6";
import useLogin from "Hooks/useLogin";
import messages from "./messages";
import { PROFILE } from ".";
const WhatIsSnort = () => {
return (
@ -107,7 +108,7 @@ export default function NewUserFlow() {
</h2>
<Copy text={hexToMnemonic(generatedEntropy ?? "")} />
<div className="next-actions">
<button type="button" onClick={() => navigate("/new/username")}>
<button type="button" onClick={() => navigate(PROFILE)}>
<FormattedMessage {...messages.KeysSaved} />{" "}
</button>
</div>

View File

@ -1,26 +1,47 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import Logo from "Element/Logo";
import useEventPublisher from "Feed/EventPublisher";
import useLogin from "Hooks/useLogin";
import { useUserProfile } from "Hooks/useUserProfile";
import { mapEventToProfile, UserCache } from "Cache";
import AvatarEditor from "Element/AvatarEditor";
import messages from "./messages";
import { DISCOVER } from ".";
export default function NewUserName() {
export default function ProfileSetup() {
const login = useLogin();
const myProfile = useUserProfile(login.publicKey);
const [username, setUsername] = useState("");
const [picture, setPicture] = useState("");
const { formatMessage } = useIntl();
const publisher = useEventPublisher();
const navigate = useNavigate();
const nextPage = "/new/discover";
useEffect(() => {
if (myProfile) {
setUsername(myProfile.name ?? "");
setPicture(myProfile.picture ?? "");
}
}, [myProfile]);
const onNext = async () => {
if (username.length > 0 && publisher) {
const ev = await publisher.metadata({ name: username });
if ((username.length > 0 || picture.length > 0) && publisher) {
const ev = await publisher.metadata({
...myProfile,
name: username,
picture,
});
publisher.broadcast(ev);
const profile = mapEventToProfile(ev);
if (profile) {
UserCache.set(profile);
}
}
navigate(nextPage);
navigate(DISCOVER);
};
return (
@ -30,13 +51,14 @@ export default function NewUserName() {
<div className="progress progress-second"></div>
</div>
<h1>
<FormattedMessage {...messages.PickUsername} />
<FormattedMessage defaultMessage="Setup profile" />
</h1>
<p>
<FormattedMessage {...messages.UsernameHelp} />
</p>
<h2>
<FormattedMessage {...messages.Username} />
<FormattedMessage defaultMessage="Profile picture" />
</h2>
<AvatarEditor picture={picture} onPictureChange={p => setPicture(p)} />
<h2>
<FormattedMessage defaultMessage="Username" />
</h2>
<input
className="username"
@ -49,7 +71,7 @@ export default function NewUserName() {
<FormattedMessage defaultMessage="You can change your username at any point." />
</div>
<div className="next-actions">
<button type="button" className="transparent" onClick={() => navigate(nextPage)}>
<button type="button" className="transparent" onClick={() => navigate(DISCOVER)}>
<FormattedMessage {...messages.Skip} />
</button>
<button type="button" onClick={onNext}>

View File

@ -2,15 +2,15 @@ import "./index.css";
import { RouteObject } from "react-router-dom";
import GetVerified from "Pages/new/GetVerified";
import NewUserName from "Pages/new/NewUsername";
import ProfileSetup from "Pages/new/ProfileSetup";
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 PROFILE = "/new/profile";
export const IMPORT = "/new/import";
export const DISCOVER = "/new/discover";
export const VERIFY = "/new/verify";
export const NewUserRoutes: RouteObject[] = [
{
@ -18,8 +18,8 @@ export const NewUserRoutes: RouteObject[] = [
element: <NewUserFlow />,
},
{
path: USERNAME,
element: <NewUserName />,
path: PROFILE,
element: <ProfileSetup />,
},
{
path: IMPORT,

View File

@ -37,10 +37,6 @@ export default defineMessages({
ExtensionsNostr: { defaultMessage: `You can also use these extensions to login to most Nostr sites.` },
ImproveSecurity: { defaultMessage: "Improve login security with browser extensions" },
PickUsername: { defaultMessage: "Pick a username" },
UsernameHelp: {
defaultMessage:
"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: { defaultMessage: "Username" },
UsernamePlaceholder: { defaultMessage: "e.g. Jack" },
PopularAccounts: { defaultMessage: "Follow some popular accounts" },

View File

@ -22,29 +22,31 @@
.settings .image-setting {
display: flex;
justify-content: space-between;
}
.settings .image-setting > div:first-child {
align-self: center;
}
.settings .avatar,
.settings .banner {
margin-left: auto;
}
.settings .avatar .edit,
.settings .banner .edit {
.avatar .edit,
.banner .edit {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
opacity: 0;
background-color: var(--bg-color);
cursor: pointer;
opacity: 0;
border-radius: 100%;
}
.settings .avatar .edit:hover {
.avatar .edit.new {
opacity: 0.5;
}
.avatar .edit:hover {
opacity: 0.5;
}

View File

@ -15,6 +15,7 @@ import { mapEventToProfile, UserCache } from "Cache";
import useLogin from "Hooks/useLogin";
import messages from "./messages";
import AvatarEditor from "Element/AvatarEditor";
export interface ProfileSettingsProps {
avatar?: boolean;
@ -36,7 +37,6 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
const [website, setWebsite] = useState<string>();
const [nip05, setNip05] = useState<string>();
const [lud16, setLud16] = useState<string>();
const [reactions, setReactions] = useState<boolean>();
const avatarPicture = (picture?.length ?? 0) === 0 ? Nostrich : picture;
@ -98,13 +98,6 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
}
}
async function setNewAvatar() {
const rsp = await uploadFile();
if (rsp) {
setPicture(rsp);
}
}
async function setNewBanner() {
const rsp = await uploadFile();
if (rsp) {
@ -189,11 +182,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
<div>
<FormattedMessage {...messages.Avatar} />:
</div>
<div style={{ backgroundImage: `url(${avatarPicture})` }} className="avatar">
<div className="edit" onClick={() => setNewAvatar()}>
<FormattedMessage {...messages.Edit} />
</div>
</div>
<AvatarEditor picture={avatarPicture} onPictureChange={p => setPicture(p)} />
</div>
)}
{(props.banner ?? true) && (

View File

@ -374,7 +374,6 @@ input:disabled {
a {
color: inherit;
line-height: 1.3em;
text-decoration: none;
}
a.ext {