refactor EditProfile (#255)

This commit is contained in:
Bullish Bear 2023-10-23 21:57:58 +08:00 committed by GitHub
parent 114cf7a2c7
commit 82c9457ed2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 251 additions and 321 deletions

View File

@ -502,11 +502,11 @@ export function AppComponent(props: {
<div
class={tw`max-w-[35rem] h-full m-auto`}
>
{EditProfile({
emit: app.eventBus.emit,
myProfile: model.myProfile,
newProfileField: model.newProfileField,
})}
<EditProfile
ctx={model.app.ctx}
profileGetter={app.database}
emit={props.eventBus.emit}
/>
</div>
</div>
{dmVNode}

View File

@ -12,7 +12,6 @@ import { DirectedMessageController, sendDMandImages } from "../features/dm.ts";
import { notify } from "./notification.ts";
import { emitFunc, EventBus } from "../event-bus.ts";
import { ContactUpdate } from "./conversation-list.tsx";
import { MyProfileUpdate } from "./edit-profile.tsx";
import { EditorEvent, EditorModel, new_DM_EditorModel, SendMessage } from "./editor.tsx";
import { DirectMessagePanelUpdate } from "./message-panel.tsx";
import { NavigationUpdate } from "./nav.tsx";
@ -46,6 +45,7 @@ import { GroupMessageController } from "../features/gm.ts";
import { ChatMessage } from "./message.ts";
import { InviteUsersToGroup } from "./invite-button.tsx";
import { IS_BETA_VERSION } from "./config.js";
import { SaveProfile } from "./edit-profile.tsx";
export type UI_Interaction_Event =
| SearchUpdate
@ -54,7 +54,7 @@ export type UI_Interaction_Event =
| NavigationUpdate
| DirectMessagePanelUpdate
| BackToContactList
| MyProfileUpdate
| SaveProfile
| PinConversation
| UnpinConversation
| SignInEvent
@ -243,23 +243,14 @@ export async function* UI_Interaction_Update(args: {
console.log(editor);
} //
//
// MyProfile
// Profile
//
else if (event.type == "EditMyProfile") {
model.myProfile = Object.assign(model.myProfile || {}, event.profile);
} else if (event.type == "SaveMyProfile") {
InsertNewProfileField(app.model);
else if (event.type == "SaveProfile") {
await saveProfile(
event.profile,
app.ctx,
event.ctx,
pool,
);
} else if (event.type == "EditNewProfileFieldKey") {
model.newProfileField.key = event.key;
} else if (event.type == "EditNewProfileFieldValue") {
model.newProfileField.value = event.value;
} else if (event.type == "InsertNewProfileField") {
InsertNewProfileField(app.model);
} //
//
// Navigation
@ -662,18 +653,6 @@ export async function* Database_Update(
}
}
function InsertNewProfileField(model: Model) {
if (model.newProfileField.key && model.newProfileField.value) {
model.myProfile = Object.assign(model.myProfile || {}, {
[model.newProfileField.key]: model.newProfileField.value,
});
model.newProfileField = {
key: "",
value: "",
};
}
}
export async function handle_SendMessage(
event: SendMessage,
ctx: NostrAccountContext,

View File

@ -120,7 +120,7 @@ export class ConversationList extends Component<Props, State> {
class={tw`w-full h-10 ${CenterClass} text-sm text-[${PrimaryTextColor}] !hover:bg-transparent hover:font-bold group`}
>
<ChatIcon
class={tw`w-4 h-4m mr-1 text-[${PrimaryTextColor}] stroke-current`}
class={tw`w-4 h-4 mr-1 text-[${PrimaryTextColor}] stroke-current`}
style={{
fill: "none",
}}

View File

@ -1,13 +1,10 @@
/** @jsx h */
import { Fragment, h } from "https://esm.sh/preact@10.17.1";
import { createRef, Fragment, h } from "https://esm.sh/preact@10.17.1";
import { tw } from "https://esm.sh/twind@0.16.16";
import { Avatar } from "./components/avatar.tsx";
import { emitFunc, EventEmitter } from "../event-bus.ts";
import {
ButtonClass,
CenterClass,
DividerClass,
inputBorderClass,
InputClass,
LinearGradientsClass,
NoOutlineClass,
@ -15,309 +12,263 @@ import {
import { ProfileData } from "../features/profile.ts";
import {
DividerBackgroundColor,
ErrorColor,
HintLinkColor,
HintTextColor,
HoverButtonBackgroudColor,
PlaceholderColor,
PrimaryTextColor,
} from "./style/colors.ts";
import { Component, ComponentChildren } from "https://esm.sh/preact@10.11.3";
import { ProfileGetter } from "./search.tsx";
import { NostrAccountContext } from "../lib/nostr-ts/nostr.ts";
import { emitFunc } from "../event-bus.ts";
export type MyProfileUpdate =
| Edit
| Save
| EditNewProfileFieldKey
| EditNewProfileFieldValue
| InsertNewProfileField;
export type Edit = {
type: "EditMyProfile";
export type SaveProfile = {
type: "SaveProfile";
profile: ProfileData;
ctx: NostrAccountContext;
};
type Save = {
type: "SaveMyProfile";
profile: ProfileData;
};
export type EditNewProfileFieldKey = {
type: "EditNewProfileFieldKey";
type profileItem = {
key: string;
value?: string;
hint?: ComponentChildren;
};
export type EditNewProfileFieldValue = {
type: "EditNewProfileFieldValue";
value: string;
type Props = {
ctx: NostrAccountContext;
profileGetter: ProfileGetter;
emit: emitFunc<SaveProfile>;
};
export type InsertNewProfileField = {
type: "InsertNewProfileField";
type State = {
profile: ProfileData | undefined;
newFieldKeyError: string;
};
export function EditProfile(props: {
emit: emitFunc<MyProfileUpdate>;
myProfile: ProfileData | undefined;
newProfileField: {
key: string;
value: string;
export class EditProfile extends Component<Props, State> {
styles = {
container: tw`py-4`,
banner: {
container: tw`h-72 w-full rounded-lg mb-20 relative`,
avatar:
tw`w-24 h-24 m-auto absolute top-60 left-1/2 box-border border-2 border-[${PrimaryTextColor}] -translate-x-2/4`,
},
avatar: tw`w-24 h-24 m-auto box-border border-2 border-[${PrimaryTextColor}]`,
field: {
title: tw`text-[${PrimaryTextColor}] mt-8`,
input: tw`${InputClass}`,
hint: {
text: tw`text-sm text-[${HintTextColor}]`,
link: tw`text-[${HintLinkColor}]`,
},
},
addButton:
tw`w-full mt-6 p-3 rounded-lg ${NoOutlineClass} text-[${PrimaryTextColor}] bg-[${DividerBackgroundColor}] hover:bg-[${HoverButtonBackgroudColor}] ${CenterClass}`,
submitButton:
tw`w-full p-3 rounded-lg ${NoOutlineClass} text-[${PrimaryTextColor}] ${CenterClass} ${LinearGradientsClass} hover:bg-gradient-to-l`,
divider: tw`${DividerClass}`,
custom: {
title: tw`text-[${PrimaryTextColor}] font-bold text-sm`,
text: tw`text-[${HintTextColor}] text-sm`,
error: tw`text-sm text-[${ErrorColor}]`,
},
};
}) {
return (
<div class={tw`py-[3rem]`}>
{props.myProfile?.banner
? (
<div
class={tw`h-[18.75rem] w-full rounded-lg relative`}
style={{
background: `url(${
props.myProfile?.banner ? props.myProfile.banner : "default-bg.png"
}) no-repeat center center / cover`,
}}
>
<Avatar
picture={props.myProfile?.picture}
class={tw`w-[6.25rem] h-[6.25rem] m-auto absolute top-[15.62rem] left-[50%] box-border border-[3px] border-[${PrimaryTextColor}]`}
style={{
transform: "translate(-50%, 0%)",
}}
/>
</div>
)
: (
<Avatar
picture={props.myProfile?.picture}
class={tw`w-[6.25rem] h-[6.25rem] m-auto box-border border-[3px] border-[${PrimaryTextColor}]`}
/>
)}
<h3 class={tw`text-[${PrimaryTextColor}] mt-[4.5rem]`}>
Name
</h3>
<textarea
placeholder="Name"
rows={props.myProfile?.name?.split("\n")?.length || 1}
value={props.myProfile?.name}
onInput={(e) => {
props.emit({
type: "EditMyProfile",
profile: {
name: e.currentTarget.value,
},
});
const lines = e.currentTarget.value.split("\n");
e.currentTarget.setAttribute(
"rows",
`${lines.length}`,
);
}}
type="text"
class={tw`${InputClass}`}
>
</textarea>
<h3 class={tw`mt-[1.5rem] text-[${PrimaryTextColor}]`}>
Picture
</h3>
<textarea
placeholder="Profile Image URL"
rows={props.myProfile?.picture?.split("\n")?.length || 1}
value={props.myProfile?.picture}
onInput={(e) => {
props.emit({
type: "EditMyProfile",
profile: {
picture: e.currentTarget.value,
},
});
const lines = e.currentTarget.value.split("\n");
e.currentTarget.setAttribute(
"rows",
`${lines.length}`,
);
}}
type="text"
class={tw`${InputClass}`}
>
</textarea>
<span class={tw`text-[0.875rem] text-[${HintTextColor}]`}>
You can upload your images on websites like{" "}
<a class={tw`text-[${HintLinkColor}]`} href="https://nostr.build/" target="_blank">
nostr.build
</a>
</span>
<h3 class={tw`mt-[1.5rem] text-[${PrimaryTextColor}]`}>
About
</h3>
<textarea
placeholder="About"
rows={props.myProfile?.about?.split("\n")?.length || 1}
value={props.myProfile?.about}
onInput={(e) => {
props.emit({
type: "EditMyProfile",
profile: {
about: e.currentTarget.value,
},
});
const lines = e.currentTarget.value.split("\n");
e.currentTarget.setAttribute(
"rows",
`${lines.length}`,
);
}}
type="text"
class={tw`${InputClass}`}
>
</textarea>
<h3 class={tw`mt-[1.5rem] text-[${PrimaryTextColor}]`}>
Website
</h3>
<textarea
placeholder="Website"
rows={props.myProfile?.website?.split("\n")?.length || 1}
value={props.myProfile?.website}
onInput={(e) => {
props.emit({
type: "EditMyProfile",
profile: {
website: e.currentTarget.value,
},
});
const lines = e.currentTarget.value.split("\n");
e.currentTarget.setAttribute(
"rows",
`${lines.length}`,
);
}}
type="text"
class={tw`${InputClass}`}
>
</textarea>
<h3 class={tw`mt-[1.5rem] text-[${PrimaryTextColor}]`}>
Banner
</h3>
<textarea
placeholder="Banner Image Url"
rows={props.myProfile?.banner?.split("\n")?.length || 1}
value={props.myProfile?.banner}
onInput={(e) => {
props.emit({
type: "EditMyProfile",
profile: {
banner: e.currentTarget.value,
},
});
const lines = e.currentTarget.value.split("\n");
e.currentTarget.setAttribute(
"rows",
`${lines.length}`,
);
}}
type="text"
class={tw`${InputClass}`}
>
</textarea>
{props.myProfile
? Object.entries(props.myProfile).map(([key, value]) => {
if (["name", "picture", "about", "website", "banner"].includes(key) || !value) {
return undefined;
}
return (
<Fragment>
<h3 class={tw`mt-[1.5rem] text-[${PrimaryTextColor}]`}>
{key}
</h3>
<textarea
placeholder={key}
rows={value.toString().split("\n").length}
value={value}
onInput={(e) => {
props.emit({
type: "EditMyProfile",
profile: {
[key]: e.currentTarget.value,
},
});
const lines = e.currentTarget.value.split("\n");
e.currentTarget.setAttribute(
"rows",
`${lines.length}`,
);
}}
type="text"
class={tw`${InputClass}`}
>
</textarea>
</Fragment>
);
})
: undefined}
<div class={tw`${DividerClass}`}></div>
<p class={tw`text-[${PrimaryTextColor}] font-blod text-[0.8125rem]`}>Custom Fields</p>
<span class={tw`text-[${HintTextColor}] text-[0.875rem]`}>
Create your own custom fields, anything goes!
</span>
<h3 class={tw`text-[${PrimaryTextColor}] mt-[1.5rem]`}>
Field name
</h3>
<input
placeholder="e.g. hobbies"
value={props.newProfileField.key}
onInput={(e) => {
props.emit({
type: "EditNewProfileFieldKey",
key: e.currentTarget.value,
});
}}
type="text"
class={tw`${InputClass}`}
/>
<h3 class={tw`mt-[1.5rem] text-[${PrimaryTextColor}]`}>
Field value
</h3>
<textarea
placeholder="e.g. Sports, Reading, Design"
rows={1}
value={props.newProfileField.value}
onInput={(e) => {
props.emit({
type: "EditNewProfileFieldValue",
value: e.currentTarget.value,
});
const lines = e.currentTarget.value.split("\n");
e.currentTarget.setAttribute(
"rows",
`${lines.length}`,
);
}}
type="text"
class={tw`${InputClass}`}
>
</textarea>
<button
class={tw`w-full mt-[1.5rem] p-[0.75rem] rounded-lg ${NoOutlineClass} text-[${PrimaryTextColor}] bg-[${DividerBackgroundColor}] hover:bg-[${HoverButtonBackgroudColor}] ${CenterClass}`}
onClick={() => {
props.emit({
type: "InsertNewProfileField",
});
}}
>
Add Field
</button>
<div class={tw`${DividerClass}`}></div>
<div class={tw`mt-[1.5rem] flex justify-end`}>
<button
class={tw`w-full p-[0.75rem] rounded-lg ${NoOutlineClass} text-[${PrimaryTextColor}] ${CenterClass} ${LinearGradientsClass} hover:bg-gradient-to-l`}
onClick={async () => {
if (props.myProfile) {
props.emit({
type: "SaveMyProfile",
profile: props.myProfile,
});
}
componentDidMount() {
const { ctx, profileGetter } = this.props;
this.setState({
profile: profileGetter.getProfilesByPublicKey(ctx.publicKey)?.profile,
});
}
shouldComponentUpdate(_: Readonly<Props>, nextState: Readonly<State>, __: any): boolean {
return JSON.stringify(this.state.profile) != JSON.stringify(nextState.profile);
}
newFieldKey = createRef<HTMLInputElement>();
newFieldValue = createRef<HTMLTextAreaElement>();
onInput = (e: h.JSX.TargetedEvent<HTMLTextAreaElement, Event>, key?: string) => {
const lines = e.currentTarget.value.split("\n");
e.currentTarget.setAttribute(
"rows",
`${lines.length}`,
);
if (key) {
const value = e.currentTarget.value;
this.setState({
profile: {
...this.state.profile,
[key]: value,
},
});
}
};
addField = () => {
if (!this.newFieldKey.current || !this.newFieldValue.current) {
return;
}
if (this.newFieldKey.current.value.trim() == "") {
this.setState({
newFieldKeyError: "Key is required.",
});
return;
}
this.setState({
profile: {
...this.state.profile,
[this.newFieldKey.current.value]: this.newFieldValue.current.value,
},
newFieldKeyError: "",
});
this.newFieldKey.current.value = "";
this.newFieldValue.current.value = "";
};
onSubmit = () => {
if (!this.state.profile) {
return;
}
this.props.emit({
type: "SaveProfile",
ctx: this.props.ctx,
profile: this.state.profile,
});
};
render() {
const profileItems: profileItem[] = [
{
key: "name",
value: this.state.profile?.name,
},
{
key: "picture",
value: this.state.profile?.picture,
hint: (
<span class={this.styles.field.hint.text}>
You can upload your images on websites like{" "}
<a class={this.styles.field.hint.link} href="https://nostr.build/" target="_blank">
nostr.build
</a>
</span>
),
},
{
key: "about",
value: this.state.profile?.about,
},
{
key: "website",
value: this.state.profile?.website,
},
{
key: "banner",
value: this.state.profile?.banner,
},
];
if (this.state.profile) {
for (const [key, value] of Object.entries(this.state.profile)) {
if (["name", "picture", "about", "website", "banner"].includes(key) || !value) {
continue;
}
profileItems.push({
key: key,
value: value,
});
}
}
const banner = this.state.profile?.banner
? (
<div
class={this.styles.banner.container}
style={{
background: `url(${
this.state.profile?.banner ? this.state.profile.banner : "default-bg.png"
}) no-repeat center center / cover`,
}}
>
Update Profile
</button>
<Avatar
picture={this.state.profile?.picture}
class={this.styles.banner.avatar}
/>
</div>
)
: (
<Avatar
picture={this.state.profile?.picture}
class={this.styles.avatar}
/>
);
const items = profileItems.map((item) => (
<Fragment>
<h3 class={this.styles.field.title} style={{ textTransform: "capitalize" }}>
{item.key}
</h3>
<textarea
placeholder={item.key}
rows={item.value?.split("\n")?.length || 1}
value={item.value}
onInput={(e) => this.onInput(e, item.key)}
type="text"
class={this.styles.field.input}
>
</textarea>
{item.hint}
</Fragment>
));
return (
<div class={this.styles.container}>
{banner}
{items}
<div class={this.styles.divider}></div>
<p class={this.styles.custom.title}>Custom Fields</p>
<span class={this.styles.custom.text}>
Create your own custom fields, anything goes!
</span>
<h3 class={this.styles.field.title}>
Field name
</h3>
<input
ref={this.newFieldKey}
placeholder="e.g. hobbies"
type="text"
class={this.styles.field.input}
/>
<span class={this.styles.custom.error}>{this.state.newFieldKeyError}</span>
<h3 class={this.styles.field.title}>
Field value
</h3>
<textarea
ref={this.newFieldValue}
placeholder="e.g. Sports, Reading, Design"
rows={1}
onInput={(e) => this.onInput(e)}
type="text"
class={this.styles.field.input}
>
</textarea>
<button class={this.styles.addButton} onClick={this.addField}>Add Field</button>
<div class={tw`${DividerClass}`}></div>
<button class={this.styles.submitButton} onClick={this.onSubmit}>Update Profile</button>
</div>
</div>
);
);
}
}

View File

@ -22,7 +22,7 @@ export function ProfileCard(props: {
profile: {
container: tw`flex`,
avatar: tw`w-10 h-10`,
name: tw`text-[1.2rem] font-blod leading-10 truncate ml-2`,
name: tw`text-[1.2rem] font-bold leading-10 truncate ml-2`,
},
divider: tw`${DividerClass} my-[0.5rem]`,
about: tw`text-[0.8rem]`,