use async_utility::task::spawn; use common::utils::nip96_upload; use global::{constants::IMAGE_SERVICE, get_client}; use gpui::{ div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task, Window, }; use nostr_sdk::prelude::*; use smol::fs; use std::{str::FromStr, sync::Arc, time::Duration}; use ui::{ 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) -> Arc> { Arc::new(Profile::new(window, cx)) } pub struct Profile { profile: Option, // Form name_input: Entity, avatar_input: Entity, bio_input: Entity, website_input: Entity, is_loading: bool, is_submitting: bool, // Panel name: SharedString, focus_handle: FocusHandle, } impl Profile { pub fn new(window: &mut Window, cx: &mut App) -> Entity { let window_handle = window.window_handle(); let name_input = cx.new(|cx| { TextInput::new(window, cx) .text_size(Size::XSmall) .placeholder("Alice") }); let avatar_input = cx.new(|cx| { TextInput::new(window, cx) .text_size(Size::XSmall) .small() .placeholder("https://example.com/avatar.jpg") }); let website_input = cx.new(|cx| { TextInput::new(window, cx) .text_size(Size::XSmall) .placeholder("https://your-website.com") }); let bio_input = cx.new(|cx| { TextInput::new(window, cx) .text_size(Size::XSmall) .multi_line() .placeholder("A short introduce about you.") }); cx.new(|cx| { let this = Self { name_input, avatar_input, bio_input, website_input, profile: None, is_loading: false, is_submitting: false, name: "Profile".into(), focus_handle: cx.focus_handle(), }; let client = get_client(); let task: Task, Error>> = cx.background_spawn(async move { let signer = client.signer().await?; let public_key = signer.get_public_key().await?; let metadata = client .fetch_metadata(public_key, Duration::from_secs(2)) .await?; Ok(metadata) }); cx.spawn(|this, mut cx| async move { if let Ok(Some(metadata)) = task.await { _ = cx.update_window(window_handle, |_, window, cx| { _ = this.update(cx, |this: &mut Profile, cx| { this.avatar_input.update(cx, |this, cx| { if let Some(avatar) = metadata.picture.as_ref() { this.set_text(avatar, window, cx); } }); this.bio_input.update(cx, |this, cx| { if let Some(bio) = metadata.about.as_ref() { this.set_text(bio, window, cx); } }); this.name_input.update(cx, |this, cx| { if let Some(display_name) = metadata.display_name.as_ref() { this.set_text(display_name, window, cx); } }); this.website_input.update(cx, |this, cx| { if let Some(website) = metadata.website.as_ref() { this.set_text(website, window, cx); } }); this.profile = Some(metadata); cx.notify(); }); }); } }) .detach(); this }) } fn upload(&mut self, window: &mut Window, cx: &mut Context) { 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::(); 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 { cx.update_window(window_handle, |_, window, cx| { // Stop loading spinner this.update(cx, |this, cx| { this.set_loading(false, cx); }) .unwrap(); // Set avatar input avatar_input .update(cx, |this, cx| { this.set_text(url.to_string(), window, cx); }) .unwrap(); }) .unwrap(); } } } Ok(None) => { // Stop loading spinner if let Some(view) = this.upgrade() { cx.update_entity(&view, |this, cx| { this.set_loading(false, cx); }) .unwrap(); } } Err(_) => {} } }) .detach(); } fn set_loading(&mut self, status: bool, cx: &mut Context) { self.is_loading = status; cx.notify(); } fn submit(&mut self, window: &mut Window, cx: &mut Context) { // 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 old_metadata = if let Some(metadata) = self.profile.as_ref() { metadata.clone() } else { Metadata::default() }; let mut new_metadata = old_metadata.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::(); 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() { cx.update_window(window_handle, |_, window, cx| { this.update(cx, |this, cx| { this.set_submitting(false, cx); window.push_notification("Your profile has been updated successfully", cx); }) .unwrap() }) .unwrap(); } }) .detach(); } fn set_submitting(&mut self, status: bool, cx: &mut Context) { self.is_submitting = status; cx.notify(); } } impl Panel for Profile { fn panel_id(&self) -> SharedString { "ProfilePanel".into() } fn title(&self, _cx: &App) -> AnyElement { self.name.clone().into_any_element() } fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu { menu.track_focus(&self.focus_handle) } fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec