From 07c5d58f8e53c5c1406ddf4025c1e861204ff31b Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 4 Feb 2026 08:42:03 +0700 Subject: [PATCH] . --- Cargo.lock | 135 ++++++++ assets/icons/shield.svg | 3 + crates/chat/src/lib.rs | 33 +- crates/common/src/display.rs | 8 +- crates/coop/src/dialogs/compose.rs | 510 ----------------------------- crates/coop/src/panels/greeter.rs | 44 ++- crates/state/Cargo.toml | 1 + crates/state/src/lib.rs | 26 +- crates/ui/src/icon.rs | 2 + 9 files changed, 199 insertions(+), 563 deletions(-) create mode 100644 assets/icons/shield.svg delete mode 100644 crates/coop/src/dialogs/compose.rs diff --git a/Cargo.lock b/Cargo.lock index 0509fa7..8777f3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,6 +94,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -1097,6 +1147,46 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.5.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + [[package]] name = "cmake" version = "0.1.57" @@ -1192,6 +1282,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "combine" version = "4.6.7" @@ -3274,6 +3370,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.13.0" @@ -4246,6 +4348,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "oneshot" version = "0.1.13" @@ -4508,6 +4616,20 @@ dependencies = [ "state", ] +[[package]] +name = "petname" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd31dcfdbbd7431a807ef4df6edd6473228e94d5c805e8cf671227a21bad068" +dependencies = [ + "anyhow", + "clap", + "itertools 0.14.0", + "proc-macro2", + "quote", + "rand 0.8.5", +] + [[package]] name = "phf" version = "0.11.3" @@ -6074,6 +6196,7 @@ dependencies = [ "nostr-connect", "nostr-lmdb", "nostr-sdk", + "petname", "reqwest", "rustls", "serde", @@ -6097,6 +6220,12 @@ dependencies = [ "float-cmp", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.26.3" @@ -7102,6 +7231,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "util" version = "0.1.0" diff --git a/assets/icons/shield.svg b/assets/icons/shield.svg new file mode 100644 index 0000000..dacf3b7 --- /dev/null +++ b/assets/icons/shield.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index 6a38bd6..ea02060 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -104,7 +104,7 @@ impl ChatRegistry { let device_signer = device.read(cx).device_signer.clone(); // A flag to indicate if the registry is loading - let tracking_flag = Arc::new(AtomicBool::new(true)); + let tracking_flag = Arc::new(AtomicBool::new(false)); // Channel for communication between nostr and gpui let (tx, rx) = flume::bounded::(2048); @@ -166,7 +166,7 @@ impl ChatRegistry { Self { rooms: vec![], - loading: true, + loading: false, sender: tx.clone(), tracking_flag, tracking: None, @@ -253,37 +253,18 @@ impl ChatRegistry { /// Tracking the status of unwrapping gift wrap events. fn tracking(&mut self, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - let status = self.tracking_flag.clone(); let tx = self.sender.clone(); self.tracking = Some(cx.background_spawn(async move { let loop_duration = Duration::from_secs(12); - let mut total_loops = 0; loop { - if client.has_signer().await { - total_loops += 1; - - if status.load(Ordering::Acquire) { - // Reset gift wrap processing flag - _ = status.compare_exchange( - true, - false, - Ordering::Release, - Ordering::Relaxed, - ); - tx.send_async(NostrEvent::Unwrapping(true)).await.ok(); - } else { - // Wait at least 2 loops to prevent exiting early while events are still being processed - if total_loops >= 2 { - tx.send_async(NostrEvent::Unwrapping(false)).await.ok(); - // Reset the counter - total_loops = 0; - } - } + if status.load(Ordering::Acquire) { + _ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed); + tx.send_async(NostrEvent::Unwrapping(true)).await.ok(); + } else { + tx.send_async(NostrEvent::Unwrapping(false)).await.ok(); } smol::Timer::after(loop_duration).await; } diff --git a/crates/common/src/display.rs b/crates/common/src/display.rs index 3f967ff..ab7c215 100644 --- a/crates/common/src/display.rs +++ b/crates/common/src/display.rs @@ -12,6 +12,7 @@ const SECONDS_IN_MINUTE: i64 = 60; const MINUTES_IN_HOUR: i64 = 60; const HOURS_IN_DAY: i64 = 24; const DAYS_IN_MONTH: i64 = 30; +const IMAGE_RESIZER: &str = "https://wsrv.nl"; pub trait RenderedProfile { fn avatar(&self) -> SharedString; @@ -24,7 +25,12 @@ impl RenderedProfile for Profile { .picture .as_ref() .filter(|picture| !picture.is_empty()) - .map(|picture| picture.into()) + .map(|picture| { + let url = format!( + "{IMAGE_RESIZER}/?url={picture}&w=100&h=100&fit=cover&mask=circle&n=-1" + ); + url.into() + }) .unwrap_or_else(|| "brand/avatar.png".into()) } diff --git a/crates/coop/src/dialogs/compose.rs b/crates/coop/src/dialogs/compose.rs deleted file mode 100644 index f7af756..0000000 --- a/crates/coop/src/dialogs/compose.rs +++ /dev/null @@ -1,510 +0,0 @@ -use std::ops::Range; -use std::time::Duration; - -use anyhow::{anyhow, Error}; -use chat::{ChatRegistry, Room}; -use common::{TextUtils, BOOTSTRAP_RELAYS}; -use gpui::prelude::FluentBuilder; -use gpui::{ - div, px, relative, rems, uniform_list, App, AppContext, Context, Entity, InteractiveElement, - IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, - StatefulInteractiveElement, Styled, Subscription, Task, Window, -}; -use gpui_tokio::Tokio; -use nostr_sdk::prelude::*; -use person::PersonRegistry; -use smallvec::{smallvec, SmallVec}; -use state::{NostrAddress, NostrRegistry}; -use theme::ActiveTheme; -use ui::avatar::Avatar; -use ui::button::{Button, ButtonVariants}; -use ui::input::{InputEvent, InputState, TextInput}; -use ui::modal::ModalButtonProps; -use ui::notification::Notification; -use ui::{h_flex, v_flex, Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension}; - -pub fn compose_button() -> impl IntoElement { - div().child( - Button::new("compose") - .icon(IconName::Plus) - .ghost_alt() - .cta() - .small() - .rounded() - .on_click(move |_, window, cx| { - let compose = cx.new(|cx| Compose::new(window, cx)); - let weak_view = compose.downgrade(); - - window.open_modal(cx, move |modal, _window, cx| { - let weak_view = weak_view.clone(); - let label = if compose.read(cx).selected(cx).len() > 1 { - SharedString::from("Create Group DM") - } else { - SharedString::from("Create DM") - }; - - modal - .alert() - .overlay_closable(true) - .keyboard(true) - .show_close(true) - .button_props(ModalButtonProps::default().ok_text(label)) - .title(SharedString::from("Direct Messages")) - .child(compose.clone()) - .on_ok(move |_, window, cx| { - weak_view - .update(cx, |this, cx| { - this.submit(window, cx); - }) - .ok(); - - // false to prevent the modal from closing - false - }) - }) - }), - ) -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] -struct Contact { - public_key: PublicKey, - selected: bool, -} - -impl AsRef for Contact { - fn as_ref(&self) -> &PublicKey { - &self.public_key - } -} - -impl Contact { - pub fn new(public_key: PublicKey) -> Self { - Self { - public_key, - selected: false, - } - } - - pub fn selected(mut self) -> Self { - self.selected = true; - self - } -} - -pub struct Compose { - /// Input for the room's subject - title_input: Entity, - - /// Input for the room's members - user_input: Entity, - - /// User's contacts - contacts: Entity>, - - /// Error message - error_message: Entity>, - - image_cache: Entity, - _subscriptions: SmallVec<[Subscription; 2]>, - _tasks: SmallVec<[Task<()>; 1]>, -} - -impl Compose { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - - let contacts = cx.new(|_| vec![]); - let error_message = cx.new(|_| None); - - let user_input = - cx.new(|cx| InputState::new(window, cx).placeholder("npub or nprofile...")); - - let title_input = - cx.new(|cx| InputState::new(window, cx).placeholder("Family...(Optional)")); - - let mut subscriptions = smallvec![]; - let mut tasks = smallvec![]; - - let get_contacts: Task, Error>> = cx.background_spawn(async move { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - let profiles = client.database().contacts(public_key).await?; - let contacts: Vec = profiles - .into_iter() - .map(|profile| Contact::new(profile.public_key())) - .collect(); - - Ok(contacts) - }); - - tasks.push( - // Load all contacts - cx.spawn_in(window, async move |this, cx| { - match get_contacts.await { - Ok(contacts) => { - this.update(cx, |this, cx| { - this.extend_contacts(contacts, cx); - }) - .ok(); - } - Err(e) => { - cx.update(|window, cx| { - window.push_notification(Notification::error(e.to_string()), cx); - }) - .ok(); - } - }; - }), - ); - - subscriptions.push( - // Clear the image cache when sidebar is closed - cx.on_release_in(window, move |this, window, cx| { - this.image_cache.update(cx, |this, cx| { - this.clear(window, cx); - }) - }), - ); - - subscriptions.push( - // Handle Enter event for user input - cx.subscribe_in( - &user_input, - window, - move |this, _input, event, window, cx| { - if let InputEvent::PressEnter { .. } = event { - this.add_and_select_contact(window, cx) - }; - }, - ), - ); - - Self { - title_input, - user_input, - error_message, - contacts, - image_cache: RetainAllImageCache::new(cx), - _subscriptions: subscriptions, - _tasks: tasks, - } - } - - async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> { - let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); - let kinds = vec![Kind::Metadata, Kind::ContactList]; - let filter = Filter::new().author(public_key).kinds(kinds).limit(10); - - client - .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) - .await?; - - Ok(()) - } - - fn extend_contacts(&mut self, contacts: I, cx: &mut Context) - where - I: IntoIterator, - { - self.contacts.update(cx, |this, cx| { - this.extend(contacts); - cx.notify(); - }); - } - - fn push_contact(&mut self, contact: Contact, window: &mut Window, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - let pk = contact.public_key; - - if !self.contacts.read(cx).iter().any(|c| c.public_key == pk) { - self._tasks.push(cx.background_spawn(async move { - Self::request_metadata(&client, pk).await.ok(); - })); - - cx.defer_in(window, |this, window, cx| { - this.contacts.update(cx, |this, cx| { - this.insert(0, contact); - cx.notify(); - }); - this.user_input.update(cx, |this, cx| { - this.set_value("", window, cx); - this.set_loading(false, cx); - }); - }); - } else { - self.set_error("Contact already added", cx); - } - } - - fn select_contact(&mut self, public_key: PublicKey, cx: &mut Context) { - self.contacts.update(cx, |this, cx| { - if let Some(contact) = this.iter_mut().find(|c| c.public_key == public_key) { - contact.selected = true; - } - cx.notify(); - }); - } - - fn add_and_select_contact(&mut self, window: &mut Window, cx: &mut Context) { - let content = self.user_input.read(cx).value().to_string(); - let http_client = cx.http_client(); - - // Show loading indicator in the input - self.user_input.update(cx, |this, cx| { - this.set_loading(true, cx); - }); - - if let Ok(public_key) = content.to_public_key() { - let contact = Contact::new(public_key).selected(); - self.push_contact(contact, window, cx); - } else if let Ok(addr) = Nip05Address::parse(&content) { - let task = Tokio::spawn(cx, async move { - if let Ok(profile) = addr.profile(&http_client).await { - let public_key = profile.public_key; - let contact = Contact::new(public_key).selected(); - - Ok(contact) - } else { - Err(anyhow!("Not found")) - } - }); - - cx.spawn_in(window, async move |this, cx| { - match task.await { - Ok(Ok(contact)) => { - this.update_in(cx, |this, window, cx| { - this.push_contact(contact, window, cx); - }) - .ok(); - } - Ok(Err(e)) => { - this.update(cx, |this, cx| { - this.set_error(e.to_string(), cx); - }) - .ok(); - } - Err(e) => { - log::error!("Tokio error: {e}"); - } - }; - }) - .detach(); - } - } - - fn selected(&self, cx: &App) -> Vec { - self.contacts - .read(cx) - .iter() - .filter_map(|contact| { - if contact.selected { - Some(contact.public_key) - } else { - None - } - }) - .collect() - } - - fn submit(&mut self, window: &mut Window, cx: &mut Context) { - let chat = ChatRegistry::global(cx); - let nostr = NostrRegistry::global(cx); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - - let receivers: Vec = self.selected(cx); - let subject_input = self.title_input.read(cx).value(); - let subject = (!subject_input.is_empty()).then(|| subject_input.to_string()); - - if !self.user_input.read(cx).value().is_empty() { - self.add_and_select_contact(window, cx); - return; - }; - - chat.update(cx, |this, cx| { - let room = cx.new(|_| Room::new(subject, public_key, receivers)); - this.emit_room(room.downgrade(), cx); - }); - - window.close_modal(cx); - } - - fn set_error(&mut self, error: impl Into, cx: &mut Context) { - // Unlock the user input - self.user_input.update(cx, |this, cx| { - this.set_loading(false, cx); - }); - - // Update error message - self.error_message.update(cx, |this, cx| { - *this = Some(error.into()); - cx.notify(); - }); - - // Dismiss error after 2 seconds - cx.spawn(async move |this, cx| { - cx.background_executor().timer(Duration::from_secs(2)).await; - - this.update(cx, |this, cx| { - this.error_message.update(cx, |this, cx| { - *this = None; - cx.notify(); - }); - }) - .ok(); - }) - .detach(); - } - - fn list_items(&self, range: Range, cx: &Context) -> Vec { - let persons = PersonRegistry::global(cx); - let mut items = Vec::with_capacity(self.contacts.read(cx).len()); - - for ix in range { - let Some(contact) = self.contacts.read(cx).get(ix) else { - continue; - }; - - let public_key = contact.public_key; - let profile = persons.read(cx).get(&public_key, cx); - - items.push( - h_flex() - .id(ix) - .px_2() - .h_11() - .w_full() - .justify_between() - .rounded(cx.theme().radius) - .child( - h_flex() - .gap_1p5() - .text_sm() - .child(Avatar::new(profile.avatar()).size(rems(1.75))) - .child(profile.name()), - ) - .when(contact.selected, |this| { - this.child( - Icon::new(IconName::CheckCircle) - .small() - .text_color(cx.theme().text_accent), - ) - }) - .hover(|this| this.bg(cx.theme().elevated_surface_background)) - .on_click(cx.listener(move |this, _, _window, cx| { - this.select_contact(public_key, cx); - })), - ); - } - - items - } -} - -impl Render for Compose { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let error = self.error_message.read(cx).as_ref(); - let loading = self.user_input.read(cx).loading; - let contacts = self.contacts.read(cx); - - v_flex() - .image_cache(self.image_cache.clone()) - .gap_2() - .child( - div() - .text_sm() - .text_color(cx.theme().text_muted) - .child(SharedString::from("Start a conversation with someone using their npub or NIP-05 (like foo@bar.com).")), - ) - .when_some(error, |this, msg| { - this.child( - div() - .italic() - .text_sm() - .text_color(cx.theme().danger_foreground) - .child(msg.clone()), - ) - }) - .child( - h_flex() - .gap_1() - .h_10() - .border_b_1() - .border_color(cx.theme().border) - .child( - div() - .text_sm() - .font_semibold() - .child(SharedString::from("Subject:")), - ) - .child(TextInput::new(&self.title_input).small().appearance(false)), - ) - .child( - v_flex() - .pt_1() - .gap_2() - .child( - v_flex() - .gap_2() - .child( - div() - .text_sm() - .font_semibold() - .child(SharedString::from("To:")), - ) - .child( - TextInput::new(&self.user_input) - .small() - .disabled(loading) - .suffix( - Button::new("add") - .icon(IconName::PlusCircle) - .transparent() - .small() - .disabled(loading) - .on_click(cx.listener(move |this, _, window, cx| { - this.add_and_select_contact(window, cx); - })), - ), - ), - ) - .map(|this| { - if contacts.is_empty() { - this.child( - v_flex() - .h_24() - .w_full() - .items_center() - .justify_center() - .text_center() - .text_xs() - .child( - div() - .font_semibold() - .line_height(relative(1.2)) - .child(SharedString::from("No contacts")), - ) - .child( - div() - .text_color(cx.theme().text_muted) - .child(SharedString::from("Your recently contacts will appear here.")), - ), - ) - } else { - this.child( - uniform_list( - "contacts", - contacts.len(), - cx.processor(move |this, range, _window, cx| { - this.list_items(range, cx) - }), - ) - .h(px(300.)), - ) - } - }), - ) - } -} diff --git a/crates/coop/src/panels/greeter.rs b/crates/coop/src/panels/greeter.rs index 1a9798d..04541c9 100644 --- a/crates/coop/src/panels/greeter.rs +++ b/crates/coop/src/panels/greeter.rs @@ -114,7 +114,6 @@ impl Render for GreeterPanel { v_flex() .gap_2() .w_full() - .items_start() .child( h_flex() .gap_1() @@ -127,17 +126,16 @@ impl Render for GreeterPanel { ) .child( v_flex() - .w_full() - .items_start() - .justify_start() .gap_2() + .w_full() .when(relay_list_state == RelayState::NotSet, |this| { this.child( Button::new("relaylist") - .icon(Icon::new(IconName::Door)) + .icon(Icon::new(IconName::Relay)) .label("Set up relay list") .ghost() .small() + .no_center() .on_click(move |_ev, window, cx| { Workspace::add_panel( relay_list::init(window, cx), @@ -153,10 +151,11 @@ impl Render for GreeterPanel { |this| { this.child( Button::new("import") - .icon(Icon::new(IconName::Usb)) + .icon(Icon::new(IconName::Relay)) .label("Set up messaging relays") .ghost() .small() + .no_center() .on_click(move |_ev, window, cx| { Workspace::add_panel( messaging_relays::init(window, cx), @@ -176,7 +175,6 @@ impl Render for GreeterPanel { v_flex() .gap_2() .w_full() - .items_start() .child( h_flex() .gap_1() @@ -189,16 +187,15 @@ impl Render for GreeterPanel { ) .child( v_flex() - .w_full() - .items_start() - .justify_start() .gap_2() + .w_full() .child( Button::new("connect") .icon(Icon::new(IconName::Door)) .label("Connect account via Nostr Connect") .ghost() .small() + .no_center() .on_click(move |_ev, window, cx| { Workspace::add_panel( connect::init(window, cx), @@ -214,6 +211,7 @@ impl Render for GreeterPanel { .label("Import a secret key or bunker") .ghost() .small() + .no_center() .on_click(move |_ev, window, cx| { Workspace::add_panel( import::init(window, cx), @@ -230,7 +228,6 @@ impl Render for GreeterPanel { v_flex() .gap_2() .w_full() - .items_start() .child( h_flex() .gap_1() @@ -243,16 +240,23 @@ impl Render for GreeterPanel { ) .child( v_flex() - .w_full() - .items_start() - .justify_start() .gap_2() + .w_full() + .child( + Button::new("backup") + .icon(Icon::new(IconName::Shield)) + .label("Backup account") + .ghost() + .small() + .no_center(), + ) .child( Button::new("profile") .icon(Icon::new(IconName::Profile)) .label("Update profile") .ghost() .small() + .no_center() .on_click(move |_ev, window, cx| { Workspace::add_panel( profile::init(window, cx), @@ -262,19 +266,13 @@ impl Render for GreeterPanel { ); }), ) - .child( - Button::new("changelog") - .icon(Icon::new(IconName::Ship)) - .label("Keep up to date") - .ghost() - .small(), - ) .child( Button::new("invite") .icon(Icon::new(IconName::Invite)) - .label("Invite people") + .label("Invite friends") .ghost() - .small(), + .small() + .no_center(), ), ), ), diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index d6af627..abeea36 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -23,3 +23,4 @@ serde.workspace = true serde_json.workspace = true rustls = "0.23" +petname = "2.0.2" diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 63f85b5..6e34fe4 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -37,6 +37,8 @@ pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app"; pub const DEVICE_GIFTWRAP: &str = "device-gift-wraps"; /// Default subscription id for user gift wrap events pub const USER_GIFTWRAP: &str = "user-gift-wraps"; +/// Default avatar for new users +pub const DEFAULT_AVATAR: &str = "https://image.nostr.build/93bb6084457a42620849b6827f3f34f111ae5a4ac728638a989d4ed4b4bb3ac8.png"; /// Default vertex relays pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"]; /// Default search relays @@ -726,7 +728,12 @@ impl NostrRegistry { /// Create a new identity fn create_identity(&mut self, cx: &mut Context) { + let client = self.client(); + + // Generate new keys let keys = Keys::generate(); + + // Get write credential task let write_credential = cx.write_credentials( CLIENT_NAME, &keys.public_key().to_hex(), @@ -736,10 +743,23 @@ impl NostrRegistry { // Update the signer self.set_signer(keys, false, cx); - // TODO: set metadata - - // Spawn a task to write the credentials + // Spawn a task to set metadata and write the credentials cx.background_spawn(async move { + let name = petname::petname(2, "-").unwrap_or("Cooper".to_string()); + let avatar = Url::parse(DEFAULT_AVATAR).unwrap(); + + // Construct metadata for the identity + let metadata = Metadata::new() + .display_name(&name) + .name(&name) + .picture(avatar); + + // Set metadata for the identity + if let Err(e) = client.set_metadata(&metadata).await { + log::error!("Failed to set metadata: {}", e); + } + + // Write the credentials if let Err(e) = write_credential.await { log::error!("Failed to write credentials: {}", e); } diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index 6e67c3d..01eb604 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -42,6 +42,7 @@ pub enum IconName { Settings, Sun, Ship, + Shield, Upload, Usb, PanelLeft, @@ -96,6 +97,7 @@ impl IconName { Self::Settings => "icons/settings.svg", Self::Sun => "icons/sun.svg", Self::Ship => "icons/ship.svg", + Self::Shield => "icons/shield.svg", Self::Upload => "icons/upload.svg", Self::Usb => "icons/usb.svg", Self::PanelLeft => "icons/panel-left.svg",