diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg index 76a3f55..7286461 100644 --- a/assets/icons/copy.svg +++ b/assets/icons/copy.svg @@ -1,3 +1,3 @@ - + diff --git a/crates/coop/src/panels/greeter.rs b/crates/coop/src/panels/greeter.rs index bdee671..5ad7e1c 100644 --- a/crates/coop/src/panels/greeter.rs +++ b/crates/coop/src/panels/greeter.rs @@ -40,7 +40,7 @@ impl GreeterPanel { cx.update(|window, cx| { Workspace::add_panel( profile::init(public_key, window, cx), - DockPlacement::Center, + DockPlacement::Right, window, cx, ); diff --git a/crates/coop/src/panels/messaging_relays.rs b/crates/coop/src/panels/messaging_relays.rs index 6b98b4a..979ce17 100644 --- a/crates/coop/src/panels/messaging_relays.rs +++ b/crates/coop/src/panels/messaging_relays.rs @@ -296,7 +296,7 @@ impl Render for MessagingRelayPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .p_3() - .gap_2() + .gap_3() .w_full() .child( div() @@ -367,9 +367,11 @@ impl Render for MessagingRelayPanel { }) .child( Button::new("submit") + .icon(IconName::CheckCircle) .label("Update") .primary() .small() + .font_semibold() .loading(self.updating) .disabled(self.updating) .on_click(cx.listener(move |this, _ev, window, cx| { diff --git a/crates/coop/src/panels/profile.rs b/crates/coop/src/panels/profile.rs index b6f418e..b0817e7 100644 --- a/crates/coop/src/panels/profile.rs +++ b/crates/coop/src/panels/profile.rs @@ -20,7 +20,7 @@ use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::input::{InputState, TextInput}; use ui::notification::Notification; -use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension}; +use ui::{h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension}; pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| ProfilePanel::new(public_key, window, cx)) @@ -51,6 +51,12 @@ pub struct ProfilePanel { /// Copied states copied: bool, + + /// Updating state + updating: bool, + + /// Tasks + tasks: Vec>>, } impl ProfilePanel { @@ -58,6 +64,7 @@ impl ProfilePanel { let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice")); let website_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me")); let avatar_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me/a.jpg")); + // Use multi-line input for bio let bio_input = cx.new(|cx| { InputState::new(window, cx) @@ -68,10 +75,7 @@ impl ProfilePanel { // Get user's profile and update inputs cx.defer_in(window, move |this, window, cx| { - let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(&public_key, cx); - // Set all input's values with current profile - this.set_profile(profile, window, cx); + this.set_profile(window, cx); }); Self { @@ -84,11 +88,15 @@ impl ProfilePanel { website_input, uploading: false, copied: false, + updating: false, + tasks: vec![], } } - fn set_profile(&mut self, person: Person, window: &mut Window, cx: &mut Context) { - let metadata = person.metadata(); + fn set_profile(&mut self, window: &mut Window, cx: &mut Context) { + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get(&self.public_key, cx); + let metadata = profile.metadata(); self.avatar_input.update(cx, |this, cx| { if let Some(avatar) = metadata.picture.as_ref() { @@ -205,6 +213,11 @@ impl ProfilePanel { .detach(); } + fn set_updating(&mut self, updating: bool, cx: &mut Context) { + self.updating = updating; + cx.notify(); + } + /// Set the metadata for the current user fn publish(&self, metadata: &Metadata, cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); @@ -253,26 +266,34 @@ impl ProfilePanel { // Set the metadata let task = self.publish(&new_metadata, cx); - cx.spawn_in(window, async move |_this, cx| { + // Set the updating state + self.set_updating(true, cx); + + self.tasks.push(cx.spawn_in(window, async move |this, cx| { match task.await { Ok(_) => { - cx.update(|window, cx| { + this.update_in(cx, |this, window, cx| { + // Update the registry persons.update(cx, |this, cx| { this.insert(Person::new(public_key, new_metadata), cx); }); + + // Update current panel + this.set_updating(false, cx); + this.set_profile(window, cx); + window.push_notification("Profile updated successfully", cx); - }) - .ok(); + })?; } Err(e) => { cx.update(|window, cx| { window.push_notification(Notification::error(e.to_string()), cx); - }) - .ok(); + })?; } }; - }) - .detach(); + + Ok(()) + })); } } @@ -296,123 +317,126 @@ impl Focusable for ProfilePanel { impl Render for ProfilePanel { fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context) -> impl IntoElement { - let shorten_pkey = SharedString::from(shorten_pubkey(self.public_key, 8)); + let avatar_input = self.avatar_input.read(cx).value(); // Get the avatar - let avatar_input = self.avatar_input.read(cx).value(); let avatar = if avatar_input.is_empty() { "brand/avatar.png" } else { avatar_input.as_str() }; + // Get the public key as short string + let shorten_pkey = SharedString::from(shorten_pubkey(self.public_key, 8)); + v_flex() - .size_full() - .items_center() - .justify_center() - .p_2() + .p_3() + .gap_3() + .w_full() .child( v_flex() - .gap_2() - .w_112() + .h_40() + .w_full() + .items_center() + .justify_center() + .gap_4() + .child(Avatar::new(avatar).size(rems(4.25))) .child( - v_flex() - .h_40() - .w_full() - .items_center() - .justify_center() - .gap_4() - .child(Avatar::new(avatar).size(rems(4.25))) - .child( - Button::new("upload") - .icon(IconName::PlusCircle) - .label("Add an avatar") - .xsmall() - .ghost() - .rounded() - .disabled(self.uploading) - .loading(self.uploading) - .on_click(cx.listener(move |this, _, window, cx| { - this.upload(window, cx); - })), - ), - ) - .child( - v_flex() - .gap_1() - .text_sm() - .text_color(cx.theme().text_muted) - .child(SharedString::from("What should people call you?")) - .child(TextInput::new(&self.name_input).small()), - ) - .child( - v_flex() - .gap_1() - .text_sm() - .text_color(cx.theme().text_muted) - .child(SharedString::from("A short introduction about you:")) - .child(TextInput::new(&self.bio_input).small()), - ) - .child( - v_flex() - .gap_1() - .text_sm() - .text_color(cx.theme().text_muted) - .child(SharedString::from("Website:")) - .child(TextInput::new(&self.website_input).small()), - ) - .child(divider(cx)) - .child( - v_flex() - .gap_1() - .child( - div() - .font_semibold() - .text_xs() - .text_color(cx.theme().text_placeholder) - .child(SharedString::from("Public Key:")), - ) - .child( - h_flex() - .h_8() - .w_full() - .justify_center() - .gap_2() - .bg(cx.theme().surface_background) - .rounded(cx.theme().radius) - .text_sm() - .child(shorten_pkey) - .child( - Button::new("copy") - .icon({ - if self.copied { - IconName::CheckCircle - } else { - IconName::Copy - } - }) - .xsmall() - .ghost() - .on_click(cx.listener(move |this, _ev, window, cx| { - this.copy( - this.public_key.to_bech32().unwrap(), - window, - cx, - ); - })), - ), - ), - ) - .child(divider(cx)) - .child( - Button::new("submit") - .label("Update") - .primary() + Button::new("upload") + .icon(IconName::PlusCircle) + .label("Add an avatar") + .xsmall() + .ghost() + .rounded() .disabled(self.uploading) - .on_click(cx.listener(move |this, _ev, window, cx| { - this.update(window, cx); + .loading(self.uploading) + .on_click(cx.listener(move |this, _, window, cx| { + this.upload(window, cx); })), ), ) + .child( + v_flex() + .gap_1p5() + .child( + div() + .text_sm() + .text_color(cx.theme().text_muted) + .child(SharedString::from("What should people call you?")), + ) + .child(TextInput::new(&self.name_input).bordered(false).small()), + ) + .child( + v_flex() + .gap_1p5() + .child( + div() + .text_sm() + .text_color(cx.theme().text_muted) + .child(SharedString::from("A short introduction about you:")), + ) + .child(TextInput::new(&self.bio_input).bordered(false).small()), + ) + .child( + v_flex() + .gap_1p5() + .child( + div() + .text_sm() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Website:")), + ) + .child(TextInput::new(&self.website_input).bordered(false).small()), + ) + .child( + v_flex() + .gap_1p5() + .child( + div() + .text_sm() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Public Key:")), + ) + .child( + h_flex() + .h_8() + .w_full() + .justify_center() + .gap_3() + .rounded(cx.theme().radius) + .bg(cx.theme().secondary_background) + .text_sm() + .text_color(cx.theme().secondary_foreground) + .child(shorten_pkey) + .child( + Button::new("copy") + .icon({ + if self.copied { + IconName::CheckCircle + } else { + IconName::Copy + } + }) + .xsmall() + .secondary() + .on_click(cx.listener(move |this, _ev, window, cx| { + this.copy(this.public_key.to_bech32().unwrap(), window, cx); + })), + ), + ), + ) + .child( + Button::new("submit") + .icon(IconName::CheckCircle) + .label("Update") + .primary() + .small() + .font_semibold() + .loading(self.updating) + .disabled(self.updating) + .on_click(cx.listener(move |this, _ev, window, cx| { + this.update(window, cx); + })), + ) } } diff --git a/crates/coop/src/panels/relay_list.rs b/crates/coop/src/panels/relay_list.rs index 5633b30..3207dad 100644 --- a/crates/coop/src/panels/relay_list.rs +++ b/crates/coop/src/panels/relay_list.rs @@ -337,7 +337,7 @@ impl Render for RelayListPanel { v_flex() .on_action(cx.listener(Self::set_metadata)) .p_3() - .gap_2() + .gap_3() .w_full() .child( div() @@ -426,9 +426,11 @@ impl Render for RelayListPanel { }) .child( Button::new("submit") + .icon(IconName::CheckCircle) .label("Update") .primary() .small() + .font_semibold() .loading(self.updating) .disabled(self.updating) .on_click(cx.listener(move |this, _ev, window, cx| { diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index 8f35e1e..5441e20 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -513,7 +513,7 @@ impl ButtonVariant { fn bg_color(&self, cx: &App) -> Hsla { match self { ButtonVariant::Primary => cx.theme().element_background, - ButtonVariant::Secondary => cx.theme().elevated_surface_background, + ButtonVariant::Secondary => cx.theme().secondary_background, ButtonVariant::Danger => cx.theme().danger_background, ButtonVariant::Warning => cx.theme().warning_background, ButtonVariant::Ghost { alt } => { @@ -531,7 +531,7 @@ impl ButtonVariant { fn text_color(&self, cx: &App) -> Hsla { match self { ButtonVariant::Primary => cx.theme().element_foreground, - ButtonVariant::Secondary => cx.theme().text_muted, + ButtonVariant::Secondary => cx.theme().secondary_foreground, ButtonVariant::Danger => cx.theme().danger_foreground, ButtonVariant::Warning => cx.theme().warning_foreground, ButtonVariant::Transparent => cx.theme().text_placeholder,