feat: add edit profile panel
This commit is contained in:
@@ -133,11 +133,13 @@ impl AppView {
|
||||
}
|
||||
}
|
||||
PanelKind::Profile => {
|
||||
let panel = Arc::new(profile::init(window, cx));
|
||||
if let Some(profile) = cx.global::<AppRegistry>().user() {
|
||||
let panel = Arc::new(profile::init(profile, window, cx));
|
||||
|
||||
self.dock.update(cx, |dock_area, cx| {
|
||||
dock_area.add_panel(panel, action.position, window, cx);
|
||||
});
|
||||
self.dock.update(cx, |dock_area, cx| {
|
||||
dock_area.add_panel(panel, action.position, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
PanelKind::Contacts => {
|
||||
let panel = Arc::new(contacts::init(window, cx));
|
||||
|
||||
@@ -370,8 +370,7 @@ impl Chat {
|
||||
});
|
||||
|
||||
// Show loading spinner
|
||||
self.is_uploading = true;
|
||||
cx.notify();
|
||||
self.set_loading(true, cx);
|
||||
|
||||
// TODO: support multiple upload
|
||||
cx.spawn(move |this, mut async_cx| async move {
|
||||
@@ -394,8 +393,7 @@ impl Chat {
|
||||
// Stop loading spinner
|
||||
if let Some(view) = this.upgrade() {
|
||||
_ = async_cx.update_entity(&view, |this, cx| {
|
||||
this.is_uploading = false;
|
||||
cx.notify();
|
||||
this.set_loading(false, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -427,6 +425,11 @@ impl Chat {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.is_uploading = status;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Chat {
|
||||
|
||||
@@ -1,18 +1,37 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use async_utility::task::spawn;
|
||||
use common::{constants::IMAGE_SERVICE, profile::NostrProfile, utils::nip96_upload};
|
||||
use gpui::{
|
||||
div, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
||||
div, img, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten, FocusHandle,
|
||||
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smol::fs;
|
||||
use state::get_client;
|
||||
use tokio::sync::oneshot;
|
||||
use ui::{
|
||||
button::Button,
|
||||
button::{Button, ButtonVariants},
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
input::TextInput,
|
||||
popup_menu::PopupMenu,
|
||||
ContextModal, Disableable, Sizable, Size,
|
||||
};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Profile> {
|
||||
Profile::new(window, cx)
|
||||
pub fn init(profile: NostrProfile, window: &mut Window, cx: &mut App) -> Entity<Profile> {
|
||||
Profile::new(profile, window, cx)
|
||||
}
|
||||
|
||||
pub struct Profile {
|
||||
profile: NostrProfile,
|
||||
// Form
|
||||
name_input: Entity<TextInput>,
|
||||
avatar_input: Entity<TextInput>,
|
||||
bio_input: Entity<TextInput>,
|
||||
website_input: Entity<TextInput>,
|
||||
is_loading: bool,
|
||||
is_submitting: bool,
|
||||
// Panel
|
||||
name: SharedString,
|
||||
closable: bool,
|
||||
zoomable: bool,
|
||||
@@ -20,14 +39,177 @@ pub struct Profile {
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
pub fn new(_window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
pub fn new(mut profile: NostrProfile, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let name_input = cx.new(|cx| {
|
||||
let mut input = TextInput::new(window, cx).text_size(Size::XSmall);
|
||||
if let Some(name) = profile.metadata().display_name.as_ref() {
|
||||
input.set_text(name, window, cx);
|
||||
}
|
||||
input
|
||||
});
|
||||
let avatar_input = cx.new(|cx| {
|
||||
let mut input = TextInput::new(window, cx).text_size(Size::XSmall).small();
|
||||
if let Some(picture) = profile.metadata().picture.as_ref() {
|
||||
input.set_text(picture, window, cx);
|
||||
}
|
||||
input
|
||||
});
|
||||
let bio_input = cx.new(|cx| {
|
||||
let mut input = TextInput::new(window, cx)
|
||||
.text_size(Size::XSmall)
|
||||
.multi_line();
|
||||
if let Some(about) = profile.metadata().about.as_ref() {
|
||||
input.set_text(about, window, cx);
|
||||
} else {
|
||||
input.set_placeholder("A short introduce about you.");
|
||||
}
|
||||
input
|
||||
});
|
||||
let website_input = cx.new(|cx| {
|
||||
let mut input = TextInput::new(window, cx).text_size(Size::XSmall);
|
||||
if let Some(website) = profile.metadata().website.as_ref() {
|
||||
input.set_text(website, window, cx);
|
||||
} else {
|
||||
input.set_placeholder("https://your-website.com");
|
||||
}
|
||||
input
|
||||
});
|
||||
|
||||
cx.new(|cx| Self {
|
||||
profile,
|
||||
name_input,
|
||||
avatar_input,
|
||||
bio_input,
|
||||
website_input,
|
||||
is_loading: false,
|
||||
is_submitting: false,
|
||||
name: "Profile".into(),
|
||||
closable: true,
|
||||
zoomable: true,
|
||||
focus_handle: cx.focus_handle(),
|
||||
})
|
||||
}
|
||||
|
||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let avatar_input = self.avatar_input.downgrade();
|
||||
let window_handle = window.window_handle();
|
||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
directories: false,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
// Show loading spinner
|
||||
self.set_loading(true, cx);
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
|
||||
Ok(Some(mut paths)) => {
|
||||
let path = paths.pop().unwrap();
|
||||
|
||||
if let Ok(file_data) = fs::read(path).await {
|
||||
let (tx, rx) = oneshot::channel::<Url>();
|
||||
|
||||
spawn(async move {
|
||||
let client = get_client();
|
||||
|
||||
if let Ok(url) = nip96_upload(client, file_data).await {
|
||||
_ = tx.send(url);
|
||||
}
|
||||
});
|
||||
|
||||
if let Ok(url) = rx.await {
|
||||
// Stop loading spinner
|
||||
if let Some(view) = this.upgrade() {
|
||||
cx.update_entity(&view, |this, cx| {
|
||||
this.set_loading(false, cx);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Update avatar input
|
||||
if let Some(input) = avatar_input.upgrade() {
|
||||
cx.update_window(window_handle, |_, window, cx| {
|
||||
cx.update_entity(&input, |this, cx| {
|
||||
this.set_text(url.to_string(), window, cx);
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(_) => {}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.is_loading = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Show loading spinner
|
||||
self.set_submitting(true, cx);
|
||||
|
||||
let avatar = self.avatar_input.read(cx).text().to_string();
|
||||
let name = self.name_input.read(cx).text().to_string();
|
||||
let bio = self.bio_input.read(cx).text().to_string();
|
||||
let website = self.website_input.read(cx).text().to_string();
|
||||
|
||||
let mut new_metadata = self
|
||||
.profile
|
||||
.metadata()
|
||||
.to_owned()
|
||||
.display_name(name)
|
||||
.about(bio);
|
||||
|
||||
if let Ok(url) = Url::from_str(&avatar) {
|
||||
new_metadata = new_metadata.picture(url);
|
||||
};
|
||||
|
||||
if let Ok(url) = Url::from_str(&website) {
|
||||
new_metadata = new_metadata.website(url);
|
||||
}
|
||||
|
||||
let window_handle = window.window_handle();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let client = get_client();
|
||||
let (tx, rx) = oneshot::channel::<EventId>();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(output) = client.set_metadata(&new_metadata).await {
|
||||
_ = tx.send(output.val);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
if rx.await.is_ok() {
|
||||
if let Some(profile) = this.upgrade() {
|
||||
cx.update_window(window_handle, |_, window, cx| {
|
||||
cx.update_entity(&profile, |this, cx| {
|
||||
this.set_submitting(false, cx);
|
||||
window.push_notification(
|
||||
"Your profile has been updated successfully",
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_submitting(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.is_submitting = status;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Profile {
|
||||
@@ -65,12 +247,91 @@ impl Focusable for Profile {
|
||||
}
|
||||
|
||||
impl Render for Profile {
|
||||
fn render(&mut self, _window: &mut gpui::Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.px_2()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child("Profile")
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_end()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.h_24()
|
||||
.child(
|
||||
img(format!(
|
||||
"{}/?url={}&w=100&h=100&fit=cover&mask=circle&n=-1",
|
||||
IMAGE_SERVICE,
|
||||
self.avatar_input.read(cx).text()
|
||||
))
|
||||
.size_10()
|
||||
.rounded_full()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.w_full()
|
||||
.child(self.avatar_input.clone())
|
||||
.child(
|
||||
Button::new("upload")
|
||||
.label("Upload")
|
||||
.ghost()
|
||||
.small()
|
||||
.disabled(self.is_submitting)
|
||||
.loading(self.is_loading)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.upload(window, cx);
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.child("Name:")
|
||||
.child(self.name_input.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.child("Bio:")
|
||||
.child(self.bio_input.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.child("Website:")
|
||||
.child(self.website_input.clone()),
|
||||
)
|
||||
.child(
|
||||
div().flex().items_center().justify_end().child(
|
||||
Button::new("submit")
|
||||
.label("Update")
|
||||
.primary()
|
||||
.small()
|
||||
.disabled(self.is_loading)
|
||||
.loading(self.is_submitting)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.submit(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user