diff --git a/Cargo.lock b/Cargo.lock index 4d2aaf7c..67e509f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1242,7 +1242,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6" +source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1674,7 +1674,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6" +source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" dependencies = [ "proc-macro2", "quote", @@ -2614,7 +2614,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6" +source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2713,7 +2713,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6" +source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2724,7 +2724,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6" +source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" dependencies = [ "anyhow", "gpui", @@ -2953,7 +2953,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6" +source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" dependencies = [ "anyhow", "async-compression", @@ -2979,7 +2979,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6" +source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3775,7 +3775,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6" +source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" dependencies = [ "anyhow", "bindgen 0.71.1", @@ -4025,7 +4025,7 @@ dependencies = [ [[package]] name = "nostr" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#a7c768a8c1e6ecb3853ab17090766dbc779a5da9" +source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56" dependencies = [ "aes", "base64", @@ -4049,7 +4049,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#a7c768a8c1e6ecb3853ab17090766dbc779a5da9" +source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56" dependencies = [ "async-utility", "nostr", @@ -4061,7 +4061,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#a7c768a8c1e6ecb3853ab17090766dbc779a5da9" +source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56" dependencies = [ "flatbuffers", "lru", @@ -4072,7 +4072,7 @@ dependencies = [ [[package]] name = "nostr-gossip" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#a7c768a8c1e6ecb3853ab17090766dbc779a5da9" +source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56" dependencies = [ "nostr", ] @@ -4080,7 +4080,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#a7c768a8c1e6ecb3853ab17090766dbc779a5da9" +source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56" dependencies = [ "async-utility", "flume", @@ -4094,7 +4094,7 @@ dependencies = [ [[package]] name = "nostr-relay-pool" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#a7c768a8c1e6ecb3853ab17090766dbc779a5da9" +source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56" dependencies = [ "async-utility", "async-wsocket", @@ -4111,7 +4111,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#a7c768a8c1e6ecb3853ab17090766dbc779a5da9" +source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56" dependencies = [ "async-utility", "nostr", @@ -4628,7 +4628,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6" +source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" dependencies = [ "collections", "serde", @@ -5254,7 +5254,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6" +source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" dependencies = [ "derive_refineable", ] @@ -5352,7 +5352,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6" +source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" dependencies = [ "anyhow", "bytes", @@ -5406,7 +5406,7 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6" +source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" dependencies = [ "arrayvec", "log", @@ -5872,7 +5872,7 @@ checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6" +source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" dependencies = [ "anyhow", "serde", @@ -6327,7 +6327,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6" +source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" dependencies = [ "arrayvec", "log", @@ -7295,7 +7295,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6" +source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" dependencies = [ "anyhow", "async-fs", @@ -7331,7 +7331,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6" +source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" dependencies = [ "perf", "quote", diff --git a/crates/coop/src/actions.rs b/crates/coop/src/actions.rs index fc478cb6..4fa8e1c4 100644 --- a/crates/coop/src/actions.rs +++ b/crates/coop/src/actions.rs @@ -5,9 +5,23 @@ use key_store::{KeyItem, KeyStore}; use nostr_connect::prelude::*; use state::NostrRegistry; -actions!(coop, [KeyringPopup, DarkMode, Settings, Logout, Quit]); +// Sidebar actions actions!(sidebar, [Reload, RelayStatus]); +// User actions +actions!( + coop, + [ + KeyringPopup, + DarkMode, + ViewProfile, + ViewRelays, + Settings, + Logout, + Quit + ] +); + #[derive(Debug, Clone)] pub struct CoopAuthUrlHandler; diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index 8a7254a4..51b620f6 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -32,10 +32,11 @@ use ui::popover::{Popover, PopoverContent}; use ui::popup_menu::PopupMenuExt; use ui::{h_flex, v_flex, ContextModal, IconName, Root, Sizable, StyledExt}; -use crate::actions::{reset, DarkMode, KeyringPopup, Logout, Settings}; +use crate::actions::{reset, DarkMode, KeyringPopup, Logout, Settings, ViewProfile, ViewRelays}; +use crate::user::viewer; use crate::views::compose::compose_button; -use crate::views::{onboarding, preferences, sidebar, startup, user_profile, welcome}; -use crate::{login, new_identity}; +use crate::views::{onboarding, preferences, setup_relay, sidebar, startup, welcome}; +use crate::{login, new_identity, user}; pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| ChatSpace::new(window, cx)) @@ -228,11 +229,82 @@ impl ChatSpace { window.open_modal(cx, move |modal, _window, _cx| { modal .title(shared_t!("common.preferences")) - .width(px(580.)) + .width(px(520.)) .child(view.clone()) }); } + fn on_profile(&mut self, _ev: &ViewProfile, window: &mut Window, cx: &mut Context) { + let view = user::init(window, cx); + let entity = view.downgrade(); + + window.open_modal(cx, move |modal, _window, _cx| { + let entity = entity.clone(); + + modal + .title("Profile") + .confirm() + .child(view.clone()) + .button_props(ModalButtonProps::default().ok_text("Update")) + .on_ok(move |_, window, cx| { + entity + .update(cx, |this, cx| { + let persons = PersonRegistry::global(cx); + let set_metadata = this.set_metadata(cx); + + cx.spawn_in(window, async move |this, cx| { + let result = set_metadata.await; + + this.update_in(cx, |_, window, cx| { + match result { + Ok(profile) => { + persons.update(cx, |this, cx| { + this.insert_or_update_person(profile, cx); + // Close the edit profile modal + window.close_all_modals(cx); + }); + } + Err(e) => { + window.push_notification(e.to_string(), cx); + } + }; + }) + .ok(); + }) + .detach(); + }) + .ok(); + + // false to keep the modal open + false + }) + }); + } + + fn on_relays(&mut self, _ev: &ViewRelays, window: &mut Window, cx: &mut Context) { + let view = setup_relay::init(window, cx); + let entity = view.downgrade(); + + window.open_modal(cx, move |this, _window, _cx| { + let entity = entity.clone(); + + this.confirm() + .title(shared_t!("relays.modal")) + .child(view.clone()) + .button_props(ModalButtonProps::default().ok_text(t!("common.update"))) + .on_ok(move |_, window, cx| { + entity + .update(cx, |this, cx| { + this.set_relays(window, cx); + }) + .ok(); + + // false to keep the modal open + false + }) + }); + } + fn on_dark_mode(&mut self, _ev: &DarkMode, window: &mut Window, cx: &mut Context) { if cx.theme().mode.is_dark() { Theme::change(ThemeMode::Light, Some(window), cx); @@ -247,17 +319,22 @@ impl ChatSpace { fn on_open_pubkey(&mut self, ev: &OpenPublicKey, window: &mut Window, cx: &mut Context) { let public_key = ev.0; - let profile = user_profile::init(public_key, window, cx); + let view = viewer::init(public_key, window, cx); window.open_modal(cx, move |this, _window, _cx| { this.alert() .show_close(true) .overlay_closable(true) - .child(profile.clone()) - .button_props(ModalButtonProps::default().ok_text(t!("profile.njump"))) + .child(view.clone()) + .button_props(ModalButtonProps::default().ok_text("View on njump.me")) .on_ok(move |_, _window, cx| { - let Ok(bech32) = public_key.to_bech32(); - cx.open_url(&format!("https://njump.me/{bech32}")); + let bech32 = public_key.to_bech32().unwrap(); + let url = format!("https://njump.me/{bech32}"); + + // Open the URL in the default browser + cx.open_url(&url); + + // false to keep the modal open false }) }); @@ -467,14 +544,14 @@ impl ChatSpace { .popup_menu(move |this, _window, _cx| { this.label(profile.display_name()) .menu_with_icon( - t!("user.dark_mode"), - IconName::Sun, - Box::new(DarkMode), + "Profile", + IconName::EmojiFill, + Box::new(ViewProfile), ) .menu_with_icon( - t!("user.settings"), - IconName::Settings, - Box::new(Settings), + "Messaging Relays", + IconName::Server, + Box::new(ViewRelays), ) .separator() .label(SharedString::from("Keyring Service")) @@ -486,7 +563,17 @@ impl ChatSpace { ) .separator() .menu_with_icon( - t!("user.sign_out"), + "Dark Mode", + IconName::Sun, + Box::new(DarkMode), + ) + .menu_with_icon( + "Settings", + IconName::Settings, + Box::new(Settings), + ) + .menu_with_icon( + "Sign Out", IconName::Logout, Box::new(Logout), ) @@ -556,6 +643,8 @@ impl Render for ChatSpace { div() .id(SharedString::from("chatspace")) .on_action(cx.listener(Self::on_settings)) + .on_action(cx.listener(Self::on_profile)) + .on_action(cx.listener(Self::on_relays)) .on_action(cx.listener(Self::on_dark_mode)) .on_action(cx.listener(Self::on_sign_out)) .on_action(cx.listener(Self::on_open_pubkey)) diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index e541473e..8f496507 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -15,6 +15,7 @@ mod actions; mod chatspace; mod login; mod new_identity; +mod user; mod views; i18n::init!(); diff --git a/crates/coop/src/new_identity/mod.rs b/crates/coop/src/new_identity/mod.rs index 13e4cc1a..25f96bca 100644 --- a/crates/coop/src/new_identity/mod.rs +++ b/crates/coop/src/new_identity/mod.rs @@ -244,20 +244,18 @@ impl NewAccount { this.update_in(cx, |this, window, cx| { match result { Ok(Ok(url)) => { - this.uploading(false, cx); this.avatar_input.update(cx, |this, cx| { this.set_value(url.to_string(), window, cx); }); } Ok(Err(e)) => { window.push_notification(e.to_string(), cx); - this.uploading(false, cx); } Err(e) => { log::warn!("Failed to upload avatar: {e}"); - this.uploading(false, cx); } }; + this.uploading(false, cx); }) .expect("Entity has been released"); }) diff --git a/crates/coop/src/user/mod.rs b/crates/coop/src/user/mod.rs new file mode 100644 index 00000000..d4f58402 --- /dev/null +++ b/crates/coop/src/user/mod.rs @@ -0,0 +1,393 @@ +use std::str::FromStr; +use std::time::Duration; + +use anyhow::{anyhow, Error}; +use common::{nip96_upload, shorten_pubkey}; +use gpui::prelude::FluentBuilder; +use gpui::{ + div, img, App, AppContext, ClipboardItem, Context, Entity, Flatten, IntoElement, ParentElement, + PathPromptOptions, Render, SharedString, Styled, Task, Window, +}; +use gpui_tokio::Tokio; +use nostr_sdk::prelude::*; +use settings::AppSettings; +use smallvec::{smallvec, SmallVec}; +use smol::fs; +use state::NostrRegistry; +use theme::ActiveTheme; +use ui::button::{Button, ButtonVariants}; +use ui::input::{InputState, TextInput}; +use ui::{h_flex, v_flex, ContextModal, Disableable, IconName, Sizable, StyledExt}; + +pub mod viewer; + +pub fn init(window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| UserProfile::new(window, cx)) +} + +#[derive(Debug)] +pub struct UserProfile { + /// User profile + profile: Option, + + /// User's name text input + name_input: Entity, + + /// User's avatar url text input + avatar_input: Entity, + + /// User's bio multi line input + bio_input: Entity, + + /// User's website url text input + website_input: Entity, + + /// Uploading state + uploading: bool, + + /// Copied states + copied: bool, + + /// Async operations + _tasks: SmallVec<[Task<()>; 1]>, +} + +impl UserProfile { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice")); + let avatar_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me/a.jpg")); + let website_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me")); + + // Use multi-line input for bio + let bio_input = cx.new(|cx| { + InputState::new(window, cx) + .multi_line() + .auto_grow(3, 8) + .placeholder("A short introduce about you.") + }); + + let get_profile = Self::get_profile(cx); + let mut tasks = smallvec![]; + + tasks.push( + // Get metadata in the background + cx.spawn_in(window, async move |this, cx| { + if let Ok(profile) = get_profile.await { + this.update_in(cx, |this, window, cx| { + this.set_profile(profile, window, cx); + }) + .ok(); + } + }), + ); + + Self { + profile: None, + name_input, + avatar_input, + bio_input, + website_input, + uploading: false, + copied: false, + _tasks: tasks, + } + } + + fn get_profile(cx: &App) -> Task> { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + + cx.background_spawn(async move { + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + + let metadata = client + .database() + .metadata(public_key) + .await? + .unwrap_or_default(); + + Ok(Profile::new(public_key, metadata)) + }) + } + + fn set_profile(&mut self, profile: Profile, window: &mut Window, cx: &mut Context) { + let metadata = profile.metadata(); + + self.avatar_input.update(cx, |this, cx| { + if let Some(avatar) = metadata.picture.as_ref() { + this.set_value(avatar, window, cx); + } + }); + + self.bio_input.update(cx, |this, cx| { + if let Some(bio) = metadata.about.as_ref() { + this.set_value(bio, window, cx); + } + }); + + self.name_input.update(cx, |this, cx| { + if let Some(display_name) = metadata.display_name.as_ref() { + this.set_value(display_name, window, cx); + } + }); + + self.website_input.update(cx, |this, cx| { + if let Some(website) = metadata.website.as_ref() { + this.set_value(website, window, cx); + } + }); + + self.profile = Some(profile); + cx.notify(); + } + + fn copy(&mut self, value: String, window: &mut Window, cx: &mut Context) { + let item = ClipboardItem::new_string(value); + cx.write_to_clipboard(item); + + self.set_copied(true, window, cx); + } + + fn set_copied(&mut self, status: bool, window: &mut Window, cx: &mut Context) { + self.copied = status; + cx.notify(); + + if status { + self._tasks.push( + // Reset the copied state after a delay + cx.spawn_in(window, async move |this, cx| { + cx.background_executor().timer(Duration::from_secs(2)).await; + cx.update(|window, cx| { + this.update(cx, |this, cx| { + this.set_copied(false, window, cx); + }) + .ok(); + }) + .ok(); + }), + ); + } + } + + fn uploading(&mut self, status: bool, cx: &mut Context) { + self.uploading = status; + cx.notify(); + } + + fn upload(&mut self, window: &mut Window, cx: &mut Context) { + self.uploading(true, cx); + + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + + // Get the user's configured NIP96 server + let nip96_server = AppSettings::get_media_server(cx); + + // Open native file dialog + let paths = cx.prompt_for_paths(PathPromptOptions { + files: true, + directories: false, + multiple: false, + prompt: None, + }); + + let task = Tokio::spawn(cx, async move { + match Flatten::flatten(paths.await.map_err(|e| e.into())) { + Ok(Some(mut paths)) => { + if let Some(path) = paths.pop() { + let file = fs::read(path).await?; + let url = nip96_upload(&client, &nip96_server, file).await?; + + Ok(url) + } else { + Err(anyhow!("Path not found")) + } + } + Ok(None) => Err(anyhow!("User cancelled")), + Err(e) => Err(anyhow!("File dialog error: {e}")), + } + }); + + cx.spawn_in(window, async move |this, cx| { + let result = Flatten::flatten(task.await.map_err(|e| e.into())); + + this.update_in(cx, |this, window, cx| { + match result { + Ok(Ok(url)) => { + this.avatar_input.update(cx, |this, cx| { + this.set_value(url.to_string(), window, cx); + }); + } + Ok(Err(e)) => { + window.push_notification(e.to_string(), cx); + } + Err(e) => { + log::warn!("Failed to upload avatar: {e}"); + } + }; + this.uploading(false, cx); + }) + .expect("Entity has been released"); + }) + .detach(); + } + + pub fn set_metadata(&mut self, cx: &mut Context) -> Task> { + let avatar = self.avatar_input.read(cx).value().to_string(); + let name = self.name_input.read(cx).value().to_string(); + let bio = self.bio_input.read(cx).value().to_string(); + let website = self.website_input.read(cx).value().to_string(); + + // Get the current profile metadata + let old_metadata = self + .profile + .as_ref() + .map(|profile| profile.metadata()) + .unwrap_or_default(); + + // Construct the new metadata + 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 nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + let gossip = nostr.read(cx).gossip(); + + cx.background_spawn(async move { + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + + let gossip = gossip.read().await; + let write_relays = gossip.inbox_relays(&public_key); + + // Ensure connections to the write relays + gossip.ensure_connections(&client, &write_relays).await; + + // Sign the new metadata event + let event = EventBuilder::metadata(&new_metadata).sign(&signer).await?; + + // Send event to user's write relayss + client.send_event_to(write_relays, &event).await?; + + // Return the updated profile + let metadata = Metadata::from_json(&event.content).unwrap_or_default(); + let profile = Profile::new(event.pubkey, metadata); + + Ok(profile) + }) + } +} + +impl Render for UserProfile { + fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .gap_3() + .child( + v_flex() + .relative() + .w_full() + .h_32() + .items_center() + .justify_center() + .gap_2() + .bg(cx.theme().surface_background) + .rounded(cx.theme().radius) + .map(|this| { + let picture = self.avatar_input.read(cx).value(); + let source = if picture.is_empty() { + "brand/avatar.png" + } else { + picture.as_str() + }; + this.child(img(source).rounded_full().size_10().flex_shrink_0()) + }) + .child( + Button::new("upload") + .icon(IconName::Upload) + .label("Change") + .ghost() + .small() + .disabled(self.uploading) + .on_click(cx.listener(move |this, _, window, cx| { + this.upload(window, cx); + })), + ), + ) + .child( + v_flex() + .gap_1() + .text_sm() + .child(SharedString::from("Name:")) + .child(TextInput::new(&self.name_input).small()), + ) + .child( + v_flex() + .gap_1() + .text_sm() + .child(SharedString::from("Bio:")) + .child(TextInput::new(&self.bio_input).small()), + ) + .child( + v_flex() + .gap_1() + .text_sm() + .child(SharedString::from("Website:")) + .child(TextInput::new(&self.website_input).small()), + ) + .when_some(self.profile.as_ref(), |this, profile| { + let public_key = profile.public_key(); + let display = SharedString::from(shorten_pubkey(profile.public_key(), 8)); + + this.child(div().my_1().h_px().w_full().bg(cx.theme().border)) + .child( + v_flex() + .gap_1() + .child( + div() + .text_xs() + .text_color(cx.theme().text_placeholder) + .font_semibold() + .child(SharedString::from("Public Key:")), + ) + .child( + h_flex() + .gap_2() + .w_full() + .h_12() + .justify_center() + .bg(cx.theme().surface_background) + .rounded(cx.theme().radius) + .text_sm() + .child(display) + .child( + Button::new("copy") + .icon({ + if self.copied { + IconName::CheckCircleFill + } else { + IconName::Copy + } + }) + .xsmall() + .ghost() + .on_click(cx.listener(move |this, _e, window, cx| { + this.copy( + public_key.to_bech32().unwrap(), + window, + cx, + ); + })), + ), + ), + ) + }) + } +} diff --git a/crates/coop/src/views/user_profile.rs b/crates/coop/src/user/viewer.rs similarity index 79% rename from crates/coop/src/views/user_profile.rs rename to crates/coop/src/user/viewer.rs index 425f5c64..a88029da 100644 --- a/crates/coop/src/views/user_profile.rs +++ b/crates/coop/src/user/viewer.rs @@ -1,250 +1,259 @@ -use std::time::Duration; - -use common::{nip05_verify, RenderedProfile}; -use gpui::prelude::FluentBuilder; -use gpui::{ - div, relative, rems, App, AppContext, ClipboardItem, Context, Entity, IntoElement, - ParentElement, Render, SharedString, Styled, Task, Window, -}; -use gpui_tokio::Tokio; -use i18n::{shared_t, t}; -use nostr_sdk::prelude::*; -use person::PersonRegistry; -use settings::AppSettings; -use smallvec::{smallvec, SmallVec}; -use state::NostrRegistry; -use theme::ActiveTheme; -use ui::avatar::Avatar; -use ui::button::{Button, ButtonVariants}; -use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt}; - -pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| UserProfile::new(public_key, window, cx)) -} - -pub struct UserProfile { - profile: Profile, - followed: bool, - verified: bool, - copied: bool, - _tasks: SmallVec<[Task<()>; 1]>, -} - -impl UserProfile { - pub fn new(target: PublicKey, window: &mut Window, cx: &mut Context) -> Self { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - - let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get_person(&target, cx); - - let mut tasks = smallvec![]; - - let check_follow: Task> = cx.background_spawn(async move { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - let contact_list = client.database().contacts_public_keys(public_key).await?; - - Ok(contact_list.contains(&target)) - }); - - let verify_nip05 = if let Some(address) = profile.metadata().nip05 { - Some(Tokio::spawn(cx, async move { - nip05_verify(target, &address).await.unwrap_or(false) - })) - } else { - None - }; - - tasks.push( - // Load user profile data - cx.spawn_in(window, async move |this, cx| { - let followed = check_follow.await.unwrap_or(false); - - // Update the followed status - this.update(cx, |this, cx| { - this.followed = followed; - cx.notify(); - }) - .ok(); - - // Update the NIP05 verification status if user has NIP05 address - if let Some(task) = verify_nip05 { - if let Ok(verified) = task.await { - this.update(cx, |this, cx| { - this.verified = verified; - cx.notify(); - }) - .ok(); - } - } - }), - ); - - Self { - profile, - followed: false, - verified: false, - copied: false, - _tasks: tasks, - } - } - - fn address(&self, _cx: &Context) -> Option { - self.profile.metadata().nip05 - } - - fn copy_pubkey(&mut self, window: &mut Window, cx: &mut Context) { - let Ok(bech32) = self.profile.public_key().to_bech32(); - let item = ClipboardItem::new_string(bech32); - cx.write_to_clipboard(item); - - self.set_copied(true, window, cx); - } - - fn set_copied(&mut self, status: bool, window: &mut Window, cx: &mut Context) { - self.copied = status; - cx.notify(); - - // Reset the copied state after a delay - if status { - cx.spawn_in(window, async move |this, cx| { - cx.background_executor().timer(Duration::from_secs(2)).await; - cx.update(|window, cx| { - this.update(cx, |this, cx| { - this.set_copied(false, window, cx); - }) - .ok(); - }) - .ok(); - }) - .detach(); - } - } -} - -impl Render for UserProfile { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let proxy = AppSettings::get_proxy_user_avatars(cx); - let bech32 = self.profile.public_key().to_bech32().unwrap(); - let shared_bech32 = SharedString::from(bech32); - - v_flex() - .gap_4() - .text_sm() - .child( - v_flex() - .gap_3() - .items_center() - .justify_center() - .text_center() - .child(Avatar::new(self.profile.avatar(proxy)).size(rems(4.))) - .child( - v_flex() - .child( - div() - .font_semibold() - .line_height(relative(1.25)) - .child(self.profile.display_name()), - ) - .when_some(self.address(cx), |this, address| { - this.child( - h_flex() - .justify_center() - .gap_1() - .text_xs() - .text_color(cx.theme().text_muted) - .child(address) - .when(self.verified, |this| { - this.child( - div() - .relative() - .text_color(cx.theme().text_accent) - .child( - Icon::new(IconName::CheckCircleFill) - .small() - .block(), - ), - ) - }), - ) - }), - ) - .when(!self.followed, |this| { - this.child( - div() - .flex_none() - .w_32() - .p_1() - .rounded_full() - .bg(cx.theme().elevated_surface_background) - .text_xs() - .font_semibold() - .child(shared_t!("profile.unknown")), - ) - }), - ) - .child( - v_flex() - .gap_1() - .child( - div() - .text_color(cx.theme().text_muted) - .child(SharedString::from("Public Key:")), - ) - .child( - h_flex() - .gap_1() - .child( - div() - .p_2() - .h_7() - .rounded_md() - .bg(cx.theme().elevated_surface_background) - .truncate() - .text_ellipsis() - .line_clamp(1) - .line_height(relative(1.)) - .child(shared_bech32), - ) - .child( - Button::new("copy") - .icon({ - if self.copied { - IconName::CheckCircleFill - } else { - IconName::Copy - } - }) - .cta() - .ghost_alt() - .on_click(cx.listener(move |this, _e, window, cx| { - this.copy_pubkey(window, cx); - })), - ), - ), - ) - .child( - v_flex() - .gap_1() - .child( - div() - .text_color(cx.theme().text_muted) - .child(shared_t!("profile.label_bio")), - ) - .child( - div() - .p_2() - .rounded_md() - .bg(cx.theme().elevated_surface_background) - .child( - self.profile - .metadata() - .about - .map(SharedString::from) - .unwrap_or(shared_t!("profile.no_bio")), - ), - ), - ) - } -} +use std::time::Duration; + +use common::{nip05_verify, shorten_pubkey, RenderedProfile}; +use gpui::prelude::FluentBuilder; +use gpui::{ + div, relative, rems, App, AppContext, ClipboardItem, Context, Entity, IntoElement, + ParentElement, Render, SharedString, Styled, Task, Window, +}; +use gpui_tokio::Tokio; +use nostr_sdk::prelude::*; +use person::PersonRegistry; +use settings::AppSettings; +use smallvec::{smallvec, SmallVec}; +use state::NostrRegistry; +use theme::ActiveTheme; +use ui::avatar::Avatar; +use ui::button::{Button, ButtonVariants}; +use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt}; + +pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| ProfileViewer::new(public_key, window, cx)) +} + +#[derive(Debug)] +pub struct ProfileViewer { + profile: Profile, + + /// Follow status + followed: bool, + + /// Verification status + verified: bool, + + /// Copy status + copied: bool, + + /// Async operations + _tasks: SmallVec<[Task<()>; 1]>, +} + +impl ProfileViewer { + pub fn new(target: PublicKey, window: &mut Window, cx: &mut Context) -> Self { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get_person(&target, cx); + + let mut tasks = smallvec![]; + + let check_follow: Task> = cx.background_spawn(async move { + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + let contact_list = client.database().contacts_public_keys(public_key).await?; + + Ok(contact_list.contains(&target)) + }); + + let verify_nip05 = if let Some(address) = profile.metadata().nip05 { + Some(Tokio::spawn(cx, async move { + nip05_verify(target, &address).await.unwrap_or(false) + })) + } else { + None + }; + + tasks.push( + // Load user profile data + cx.spawn_in(window, async move |this, cx| { + let followed = check_follow.await.unwrap_or(false); + + // Update the followed status + this.update(cx, |this, cx| { + this.followed = followed; + cx.notify(); + }) + .ok(); + + // Update the NIP05 verification status if user has NIP05 address + if let Some(task) = verify_nip05 { + if let Ok(verified) = task.await { + this.update(cx, |this, cx| { + this.verified = verified; + cx.notify(); + }) + .ok(); + } + } + }), + ); + + Self { + profile, + followed: false, + verified: false, + copied: false, + _tasks: tasks, + } + } + + fn address(&self, _cx: &Context) -> Option { + self.profile.metadata().nip05 + } + + fn copy_pubkey(&mut self, window: &mut Window, cx: &mut Context) { + let Ok(bech32) = self.profile.public_key().to_bech32(); + let item = ClipboardItem::new_string(bech32); + cx.write_to_clipboard(item); + + self.set_copied(true, window, cx); + } + + fn set_copied(&mut self, status: bool, window: &mut Window, cx: &mut Context) { + self.copied = status; + cx.notify(); + + if status { + self._tasks.push( + // Reset the copied state after a delay + cx.spawn_in(window, async move |this, cx| { + cx.background_executor().timer(Duration::from_secs(2)).await; + cx.update(|window, cx| { + this.update(cx, |this, cx| { + this.set_copied(false, window, cx); + }) + .ok(); + }) + .ok(); + }), + ); + } + } +} + +impl Render for ProfileViewer { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let proxy = AppSettings::get_proxy_user_avatars(cx); + let bech32 = shorten_pubkey(self.profile.public_key(), 16); + let shared_bech32 = SharedString::from(bech32); + + v_flex() + .gap_4() + .text_sm() + .child( + v_flex() + .gap_3() + .items_center() + .justify_center() + .text_center() + .child(Avatar::new(self.profile.avatar(proxy)).size(rems(4.))) + .child( + v_flex() + .child( + div() + .font_semibold() + .line_height(relative(1.25)) + .child(self.profile.display_name()), + ) + .when_some(self.address(cx), |this, address| { + this.child( + h_flex() + .justify_center() + .gap_1() + .text_xs() + .text_color(cx.theme().text_muted) + .child(address) + .when(self.verified, |this| { + this.child( + div() + .relative() + .text_color(cx.theme().text_accent) + .child( + Icon::new(IconName::CheckCircleFill) + .small() + .block(), + ), + ) + }), + ) + }), + ) + .when(!self.followed, |this| { + this.child( + div() + .flex_none() + .w_32() + .p_1() + .rounded_full() + .bg(cx.theme().elevated_surface_background) + .text_xs() + .font_semibold() + .child(SharedString::from("Unknown contact")), + ) + }), + ) + .child( + v_flex() + .gap_1() + .text_sm() + .child( + div() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Bio:")), + ) + .child( + div() + .p_2() + .min_h_16() + .rounded(cx.theme().radius) + .bg(cx.theme().elevated_surface_background) + .child( + self.profile + .metadata() + .about + .map(SharedString::from) + .unwrap_or(SharedString::from("No bio.")), + ), + ), + ) + .child(div().my_1().h_px().w_full().bg(cx.theme().border)) + .child( + v_flex() + .gap_1() + .child( + div() + .text_xs() + .text_color(cx.theme().text_placeholder) + .font_semibold() + .child(SharedString::from("Public Key:")), + ) + .child( + h_flex() + .gap_2() + .w_full() + .h_12() + .justify_center() + .bg(cx.theme().surface_background) + .rounded(cx.theme().radius) + .text_sm() + .child(shared_bech32) + .child( + Button::new("copy") + .icon({ + if self.copied { + IconName::CheckCircleFill + } else { + IconName::Copy + } + }) + .xsmall() + .ghost() + .on_click(cx.listener(move |this, _e, window, cx| { + this.copy_pubkey(window, cx); + })), + ), + ), + ) + } +} diff --git a/crates/coop/src/views/edit_profile.rs b/crates/coop/src/views/edit_profile.rs deleted file mode 100644 index 186b54b9..00000000 --- a/crates/coop/src/views/edit_profile.rs +++ /dev/null @@ -1,306 +0,0 @@ -use std::str::FromStr; -use std::time::Duration; - -use anyhow::Error; -use common::nip96_upload; -use gpui::prelude::FluentBuilder; -use gpui::{ - div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement, - PathPromptOptions, Render, SharedString, Styled, Task, Window, -}; -use i18n::{shared_t, t}; -use nostr_sdk::prelude::*; -use settings::AppSettings; -use smol::fs; -use state::NostrRegistry; -use theme::ActiveTheme; -use ui::button::{Button, ButtonVariants}; -use ui::input::{InputState, TextInput}; -use ui::{v_flex, Disableable, IconName, Sizable}; - -pub fn init(window: &mut Window, cx: &mut App) -> Entity { - EditProfile::new(window, cx) -} - -pub struct EditProfile { - profile: Option, - name_input: Entity, - avatar_input: Entity, - bio_input: Entity, - website_input: Entity, - is_loading: bool, - is_submitting: bool, -} - -impl EditProfile { - pub fn new(window: &mut Window, cx: &mut App) -> Entity { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - - let name_input = - cx.new(|cx| InputState::new(window, cx).placeholder(t!("profile.placeholder_name"))); - - let avatar_input = - cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.jpg")); - - let website_input = - cx.new(|cx| InputState::new(window, cx).placeholder("https://your-website.com")); - - let bio_input = cx.new(|cx| { - InputState::new(window, cx) - .multi_line() - .placeholder(t!("profile.placeholder_bio")) - }); - - cx.new(|cx| { - let this = Self { - name_input, - avatar_input, - bio_input, - website_input, - profile: None, - is_loading: false, - is_submitting: false, - }; - - 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_in(window, async move |this, cx| { - if let Ok(Some(metadata)) = task.await { - this.update_in(cx, |this: &mut EditProfile, window, cx| { - this.avatar_input.update(cx, |this, cx| { - if let Some(avatar) = metadata.picture.as_ref() { - this.set_value(avatar, window, cx); - } - }); - this.bio_input.update(cx, |this, cx| { - if let Some(bio) = metadata.about.as_ref() { - this.set_value(bio, window, cx); - } - }); - this.name_input.update(cx, |this, cx| { - if let Some(display_name) = metadata.display_name.as_ref() { - this.set_value(display_name, window, cx); - } - }); - this.website_input.update(cx, |this, cx| { - if let Some(website) = metadata.website.as_ref() { - this.set_value(website, window, cx); - } - }); - this.profile = Some(metadata); - cx.notify(); - }) - .ok(); - } - }) - .detach(); - - this - }) - } - - fn upload(&mut self, window: &mut Window, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - - let nip96 = AppSettings::get_media_server(cx); - let avatar_input = self.avatar_input.downgrade(); - - let paths = cx.prompt_for_paths(PathPromptOptions { - files: true, - directories: false, - multiple: false, - prompt: None, - }); - - // Show loading spinner - self.set_loading(true, cx); - - cx.spawn_in(window, async move |this, cx| { - 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::(); - - nostr_sdk::async_utility::task::spawn(async move { - if let Ok(url) = nip96_upload(&client, &nip96, file_data).await { - _ = tx.send(url); - } - }); - - if let Ok(url) = rx.await { - cx.update(|window, cx| { - // Stop loading spinner - this.update(cx, |this, cx| { - this.set_loading(false, cx); - }) - .ok(); - - // Set avatar input - avatar_input - .update(cx, |this, cx| { - this.set_value(url.to_string(), window, cx); - }) - .ok(); - }) - .ok(); - } - } - } - Ok(None) => { - cx.update(|_, cx| { - // Stop loading spinner - this.update(cx, |this, cx| { - this.set_loading(false, cx); - }) - .ok(); - }) - .ok(); - } - Err(_) => {} - } - }) - .detach(); - } - - pub fn set_metadata(&mut self, cx: &mut Context) -> Task> { - let avatar = self.avatar_input.read(cx).value().to_string(); - let name = self.name_input.read(cx).value().to_string(); - let bio = self.bio_input.read(cx).value().to_string(); - let website = self.website_input.read(cx).value().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 nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - let gossip = nostr.read(cx).gossip(); - - cx.background_spawn(async move { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - let gossip = gossip.read().await; - let write_relays = gossip.inbox_relays(&public_key); - - // Ensure connections to the write relays - gossip.ensure_connections(&client, &write_relays).await; - - // Sign the new metadata event - let event = EventBuilder::metadata(&new_metadata).sign(&signer).await?; - - // Send event to user's write relayss - client.send_event_to(write_relays, &event).await?; - - // Return the updated profile - let metadata = Metadata::from_json(&event.content).unwrap_or_default(); - let profile = Profile::new(event.pubkey, metadata); - - Ok(profile) - }) - } - - fn set_loading(&mut self, status: bool, cx: &mut Context) { - self.is_loading = status; - cx.notify(); - } -} - -impl Render for EditProfile { - fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .gap_3() - .child( - div() - .w_full() - .h_32() - .bg(cx.theme().surface_background) - .rounded(cx.theme().radius) - .flex() - .flex_col() - .items_center() - .justify_center() - .gap_2() - .map(|this| { - let picture = self.avatar_input.read(cx).value(); - if picture.is_empty() { - this.child( - img("brand/avatar.png") - .rounded_full() - .size_10() - .flex_shrink_0(), - ) - } else { - this.child( - img(picture.clone()) - .rounded_full() - .size_10() - .flex_shrink_0(), - ) - } - }) - .child( - Button::new("upload") - .icon(IconName::Upload) - .label(t!("common.change")) - .ghost() - .small() - .disabled(self.is_loading || 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_sm() - .child(shared_t!("profile.label_name")) - .child(TextInput::new(&self.name_input).small()), - ) - .child( - div() - .flex() - .flex_col() - .gap_1() - .text_sm() - .child(shared_t!("profile.label_website")) - .child(TextInput::new(&self.website_input).small()), - ) - .child( - div() - .flex() - .flex_col() - .gap_1() - .text_sm() - .child(shared_t!("profile.label_bio")) - .child(TextInput::new(&self.bio_input).small()), - ) - } -} diff --git a/crates/coop/src/views/mod.rs b/crates/coop/src/views/mod.rs index 8a759493..eb878ebf 100644 --- a/crates/coop/src/views/mod.rs +++ b/crates/coop/src/views/mod.rs @@ -1,10 +1,8 @@ pub mod compose; -pub mod edit_profile; pub mod onboarding; pub mod preferences; pub mod screening; pub mod setup_relay; pub mod sidebar; pub mod startup; -pub mod user_profile; pub mod welcome; diff --git a/crates/coop/src/views/preferences.rs b/crates/coop/src/views/preferences.rs index 8c9a0430..766bcdf8 100644 --- a/crates/coop/src/views/preferences.rs +++ b/crates/coop/src/views/preferences.rs @@ -1,22 +1,15 @@ -use account::Account; -use common::RenderedProfile; use gpui::http_client::Url; use gpui::{ - div, px, relative, rems, App, AppContext, Context, Entity, InteractiveElement, IntoElement, - ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window, + div, px, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, + Styled, Window, }; use i18n::{shared_t, t}; -use person::PersonRegistry; use settings::AppSettings; use theme::ActiveTheme; -use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::input::{InputState, TextInput}; -use ui::modal::ModalButtonProps; use ui::switch::Switch; -use ui::{h_flex, v_flex, ContextModal, IconName, Sizable, Size, StyledExt}; - -use crate::views::{edit_profile, setup_relay}; +use ui::{h_flex, v_flex, IconName, Sizable, Size, StyledExt}; pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Preferences::new(window, cx)) @@ -37,73 +30,6 @@ impl Preferences { Self { media_input } } - - fn open_edit_profile(&self, window: &mut Window, cx: &mut Context) { - let view = edit_profile::init(window, cx); - let weak_view = view.downgrade(); - - window.open_modal(cx, move |modal, _window, _cx| { - let weak_view = weak_view.clone(); - - modal - .confirm() - .title(shared_t!("profile.title")) - .child(view.clone()) - .button_props(ModalButtonProps::default().ok_text(t!("common.update"))) - .on_ok(move |_, window, cx| { - weak_view - .update(cx, |this, cx| { - let persons = PersonRegistry::global(cx); - let set_metadata = this.set_metadata(cx); - - cx.spawn_in(window, async move |this, cx| { - let result = set_metadata.await; - - this.update_in(cx, |_, window, cx| { - match result { - Ok(profile) => { - persons.update(cx, |this, cx| { - this.insert_or_update_person(profile, cx); - }); - } - Err(e) => { - window.push_notification(e.to_string(), cx); - } - }; - }) - .ok(); - }) - .detach(); - }) - .ok(); - // true to close the modal - true - }) - }); - } - - fn open_relays(&self, window: &mut Window, cx: &mut Context) { - let view = setup_relay::init(window, cx); - let weak_view = view.downgrade(); - - window.open_modal(cx, move |this, _window, _cx| { - let weak_view = weak_view.clone(); - - this.confirm() - .title(shared_t!("relays.modal")) - .child(view.clone()) - .button_props(ModalButtonProps::default().ok_text(t!("common.update"))) - .on_ok(move |_, window, cx| { - weak_view - .update(cx, |this, cx| { - this.set_relays(window, cx); - }) - .ok(); - // true to close the modal - false - }) - }); - } } impl Render for Preferences { @@ -115,68 +41,9 @@ impl Render for Preferences { let proxy = AppSettings::get_proxy_user_avatars(cx); let hide = AppSettings::get_hide_user_avatars(cx); - let persons = PersonRegistry::global(cx); - let account = Account::global(cx); - let public_key = account.read(cx).public_key(); - let profile = persons.read(cx).get_person(&public_key, cx); - let input_state = self.media_input.downgrade(); v_flex() - .child( - v_flex() - .pb_2() - .gap_2() - .child( - div() - .text_sm() - .text_color(cx.theme().text_placeholder) - .font_semibold() - .child(shared_t!("preferences.account_header")), - ) - .child( - h_flex() - .w_full() - .justify_between() - .child( - h_flex() - .id("user") - .gap_2() - .child(Avatar::new(profile.avatar(proxy)).size(rems(2.4))) - .child( - div() - .flex_1() - .text_sm() - .child( - div() - .font_semibold() - .line_height(relative(1.3)) - .child(profile.display_name()), - ) - .child( - div() - .text_xs() - .text_color(cx.theme().text_muted) - .line_height(relative(1.3)) - .child(shared_t!("preferences.account_btn")), - ), - ) - .on_click(cx.listener(move |this, _e, window, cx| { - this.open_edit_profile(window, cx); - })), - ) - .child( - Button::new("relays") - .label("Messaging Relays") - .xsmall() - .ghost_alt() - .rounded() - .on_click(cx.listener(move |this, _e, window, cx| { - this.open_relays(window, cx); - })), - ), - ), - ) .child( v_flex() .py_2() diff --git a/crates/coop/src/views/startup.rs b/crates/coop/src/views/startup.rs index 8829f08b..39cde991 100644 --- a/crates/coop/src/views/startup.rs +++ b/crates/coop/src/views/startup.rs @@ -61,7 +61,7 @@ impl Startup { Self { credential, loading: false, - name: "Continue".into(), + name: "Onboarding".into(), focus_handle: cx.focus_handle(), image_cache: RetainAllImageCache::new(cx), _subscriptions: subscriptions,