From 9e9a4c7945ce3383300ee7de52e023fec63b284c Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 28 Jan 2026 08:26:49 +0700 Subject: [PATCH] add profile panel --- assets/icons/boom.svg | 2 +- assets/icons/caret-down.svg | 2 +- assets/icons/caret-up.svg | 2 +- assets/icons/check-circle.svg | 2 +- assets/icons/check.svg | 2 +- assets/icons/close-circle-fill.svg | 2 +- assets/icons/close-circle.svg | 2 +- assets/icons/close.svg | 2 +- assets/icons/copy.svg | 2 +- assets/icons/door.svg | 2 +- assets/icons/ellipsis.svg | 2 +- assets/icons/emoji.svg | 2 +- assets/icons/eye.svg | 2 +- assets/icons/info.svg | 2 +- assets/icons/link.svg | 2 +- assets/icons/moon.svg | 2 +- assets/icons/panel-left.svg | 2 +- assets/icons/panel-right-open.svg | 3 + assets/icons/panel-right.svg | 3 + assets/icons/plus-circle.svg | 2 +- assets/icons/plus.svg | 2 +- assets/icons/relay.svg | 2 +- assets/icons/reply.svg | 2 +- assets/icons/search.svg | 2 +- assets/icons/settings.svg | 2 +- assets/icons/sun.svg | 2 +- assets/icons/upload.svg | 2 +- assets/icons/warning.svg | 2 +- crates/coop/src/panels/connect.rs | 1 + crates/coop/src/panels/greeter.rs | 104 +++++--- crates/coop/src/panels/import.rs | 1 + crates/coop/src/panels/mod.rs | 1 + crates/coop/src/panels/profile.rs | 413 +++++++++++++++++++++++++++++ crates/coop/src/workspace.rs | 4 +- crates/dock/src/tab/mod.rs | 2 +- crates/dock/src/tab/tab_bar.rs | 15 -- crates/dock/src/tab_panel.rs | 42 +-- crates/state/src/lib.rs | 21 ++ crates/ui/src/styled.rs | 2 +- 39 files changed, 544 insertions(+), 120 deletions(-) create mode 100644 assets/icons/panel-right-open.svg create mode 100644 assets/icons/panel-right.svg create mode 100644 crates/coop/src/panels/profile.rs diff --git a/assets/icons/boom.svg b/assets/icons/boom.svg index 3f18d96..dcaf940 100644 --- a/assets/icons/boom.svg +++ b/assets/icons/boom.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/caret-down.svg b/assets/icons/caret-down.svg index a5717ec..36be93e 100644 --- a/assets/icons/caret-down.svg +++ b/assets/icons/caret-down.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/caret-up.svg b/assets/icons/caret-up.svg index 6ffbd8b..6e3a8a1 100644 --- a/assets/icons/caret-up.svg +++ b/assets/icons/caret-up.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/check-circle.svg b/assets/icons/check-circle.svg index 74e45c3..6b1b8d0 100644 --- a/assets/icons/check-circle.svg +++ b/assets/icons/check-circle.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/check.svg b/assets/icons/check.svg index 5105a0d..299e001 100644 --- a/assets/icons/check.svg +++ b/assets/icons/check.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/close-circle-fill.svg b/assets/icons/close-circle-fill.svg index 27c42e3..079ddbb 100644 --- a/assets/icons/close-circle-fill.svg +++ b/assets/icons/close-circle-fill.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/close-circle.svg b/assets/icons/close-circle.svg index b622fb0..201fc93 100644 --- a/assets/icons/close-circle.svg +++ b/assets/icons/close-circle.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/close.svg b/assets/icons/close.svg index debd9cb..484ffce 100644 --- a/assets/icons/close.svg +++ b/assets/icons/close.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg index 2daf6a6..76a3f55 100644 --- a/assets/icons/copy.svg +++ b/assets/icons/copy.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/door.svg b/assets/icons/door.svg index df03a31..b9cb553 100644 --- a/assets/icons/door.svg +++ b/assets/icons/door.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/ellipsis.svg b/assets/icons/ellipsis.svg index 2ab4cc7..411f545 100644 --- a/assets/icons/ellipsis.svg +++ b/assets/icons/ellipsis.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/emoji.svg b/assets/icons/emoji.svg index fd52a5f..2df5b84 100644 --- a/assets/icons/emoji.svg +++ b/assets/icons/emoji.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/eye.svg b/assets/icons/eye.svg index 907673c..44d426a 100644 --- a/assets/icons/eye.svg +++ b/assets/icons/eye.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/info.svg b/assets/icons/info.svg index 48994c5..7c816dc 100644 --- a/assets/icons/info.svg +++ b/assets/icons/info.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/link.svg b/assets/icons/link.svg index 88ae017..f5217bf 100644 --- a/assets/icons/link.svg +++ b/assets/icons/link.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/moon.svg b/assets/icons/moon.svg index 1684e5f..68dd15a 100644 --- a/assets/icons/moon.svg +++ b/assets/icons/moon.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/panel-left.svg b/assets/icons/panel-left.svg index c18c700..814388d 100644 --- a/assets/icons/panel-left.svg +++ b/assets/icons/panel-left.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/panel-right-open.svg b/assets/icons/panel-right-open.svg new file mode 100644 index 0000000..2d3e722 --- /dev/null +++ b/assets/icons/panel-right-open.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/panel-right.svg b/assets/icons/panel-right.svg new file mode 100644 index 0000000..ea70e43 --- /dev/null +++ b/assets/icons/panel-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/plus-circle.svg b/assets/icons/plus-circle.svg index a615ad7..96ac654 100644 --- a/assets/icons/plus-circle.svg +++ b/assets/icons/plus-circle.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg index f9e4c8f..ed27feb 100644 --- a/assets/icons/plus.svg +++ b/assets/icons/plus.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/relay.svg b/assets/icons/relay.svg index cd47109..ea4e7b4 100644 --- a/assets/icons/relay.svg +++ b/assets/icons/relay.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/reply.svg b/assets/icons/reply.svg index 8a36e4c..499a40a 100644 --- a/assets/icons/reply.svg +++ b/assets/icons/reply.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/search.svg b/assets/icons/search.svg index c519f15..01ed3cc 100644 --- a/assets/icons/search.svg +++ b/assets/icons/search.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/settings.svg b/assets/icons/settings.svg index 01426e5..cb2a6ac 100644 --- a/assets/icons/settings.svg +++ b/assets/icons/settings.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/sun.svg b/assets/icons/sun.svg index 07e5909..b9d5dbd 100644 --- a/assets/icons/sun.svg +++ b/assets/icons/sun.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/upload.svg b/assets/icons/upload.svg index ffa5b34..cfb5e30 100644 --- a/assets/icons/upload.svg +++ b/assets/icons/upload.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/warning.svg b/assets/icons/warning.svg index 0854261..07eeb04 100644 --- a/assets/icons/warning.svg +++ b/assets/icons/warning.svg @@ -1,3 +1,3 @@ - + diff --git a/crates/coop/src/panels/connect.rs b/crates/coop/src/panels/connect.rs index 5a8ea69..efd98e9 100644 --- a/crates/coop/src/panels/connect.rs +++ b/crates/coop/src/panels/connect.rs @@ -97,6 +97,7 @@ impl Render for ConnectPanel { .size_full() .items_center() .justify_center() + .p_2() .gap_10() .child( v_flex() diff --git a/crates/coop/src/panels/greeter.rs b/crates/coop/src/panels/greeter.rs index e0eb227..230f569 100644 --- a/crates/coop/src/panels/greeter.rs +++ b/crates/coop/src/panels/greeter.rs @@ -1,3 +1,4 @@ +use dock::dock::DockPlacement; use dock::panel::{Panel, PanelEvent}; use gpui::prelude::FluentBuilder; use gpui::{ @@ -9,7 +10,7 @@ use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt}; -use crate::panels::{connect, import}; +use crate::panels::{connect, import, profile}; use crate::workspace::Workspace; pub fn init(window: &mut Window, cx: &mut App) -> Entity { @@ -71,20 +72,21 @@ impl Render for GreeterPanel { let identity = nostr.read(cx).identity(); h_flex() - .relative() .size_full() .items_center() .justify_center() + .p_2() .child( v_flex() - .gap_10() + .gap_3() .h_full() .items_center() .justify_center() .child( h_flex() - .w_96() + .mb_7() .gap_2() + .w_96() .child( svg() .path("brand/coop.svg") @@ -108,6 +110,60 @@ impl Render for GreeterPanel { ), ), ) + .when(!identity.read(cx).owned, |this| { + this.child( + v_flex() + .gap_2() + .w_96() + .child( + h_flex() + .gap_1() + .w_full() + .text_sm() + .font_semibold() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Use your own identity")) + .child(div().flex_1().h_px().bg(cx.theme().border)), + ) + .child( + v_flex() + .w_full() + .items_start() + .justify_start() + .gap_2() + .child( + Button::new("connect") + .icon(Icon::new(IconName::Door)) + .label("Connect account via Nostr Connect") + .ghost() + .small() + .on_click(move |_ev, window, cx| { + Workspace::add_panel( + connect::init(window, cx), + DockPlacement::Center, + window, + cx, + ); + }), + ) + .child( + Button::new("import") + .icon(Icon::new(IconName::Usb)) + .label("Import a secret key or bunker") + .ghost() + .small() + .on_click(move |_ev, window, cx| { + Workspace::add_panel( + import::init(window, cx), + DockPlacement::Center, + window, + cx, + ); + }), + ), + ), + ) + }) .child( v_flex() .gap_2() @@ -128,42 +184,20 @@ impl Render for GreeterPanel { .items_start() .justify_start() .gap_2() - .when(!identity.read(cx).owned, |this| { - this.child( - Button::new("connect") - .icon(Icon::new(IconName::Door)) - .label("Connect account via Nostr Connect") - .ghost() - .small() - .on_click(move |_ev, window, cx| { - Workspace::add_panel( - connect::init(window, cx), - window, - cx, - ); - }), - ) - .child( - Button::new("import") - .icon(Icon::new(IconName::Usb)) - .label("Import a secret key or bunker") - .ghost() - .small() - .on_click(move |_ev, window, cx| { - Workspace::add_panel( - import::init(window, cx), - window, - cx, - ); - }), - ) - }) .child( Button::new("profile") .icon(Icon::new(IconName::Profile)) .label("Update profile") .ghost() - .small(), + .small() + .on_click(move |_ev, window, cx| { + Workspace::add_panel( + profile::init(window, cx), + DockPlacement::Center, + window, + cx, + ); + }), ) .child( Button::new("changelog") diff --git a/crates/coop/src/panels/import.rs b/crates/coop/src/panels/import.rs index f73ef3a..f133f0c 100644 --- a/crates/coop/src/panels/import.rs +++ b/crates/coop/src/panels/import.rs @@ -292,6 +292,7 @@ impl Render for ImportPanel { .size_full() .items_center() .justify_center() + .p_2() .gap_10() .child( div() diff --git a/crates/coop/src/panels/mod.rs b/crates/coop/src/panels/mod.rs index 5237dc2..ab20f31 100644 --- a/crates/coop/src/panels/mod.rs +++ b/crates/coop/src/panels/mod.rs @@ -1,3 +1,4 @@ pub mod connect; pub mod greeter; pub mod import; +pub mod profile; diff --git a/crates/coop/src/panels/profile.rs b/crates/coop/src/panels/profile.rs new file mode 100644 index 0000000..04cf01a --- /dev/null +++ b/crates/coop/src/panels/profile.rs @@ -0,0 +1,413 @@ +use std::str::FromStr; +use std::time::Duration; + +use anyhow::anyhow; +use common::{nip96_upload, shorten_pubkey}; +use dock::panel::{Panel, PanelEvent}; +use gpui::{ + div, rems, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, + FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, + Styled, Window, +}; +use gpui_tokio::Tokio; +use nostr_sdk::prelude::*; +use person::{Person, PersonRegistry}; +use settings::AppSettings; +use smol::fs; +use state::NostrRegistry; +use theme::ActiveTheme; +use ui::avatar::Avatar; +use ui::button::{Button, ButtonVariants}; +use ui::input::{InputState, TextInput}; +use ui::notification::Notification; +use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension}; + +pub fn init(window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| ProfilePanel::new(window, cx)) +} + +#[derive(Debug)] +pub struct ProfilePanel { + name: SharedString, + focus_handle: FocusHandle, + + /// 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, +} + +impl ProfilePanel { + fn new(window: &mut Window, cx: &mut Context) -> Self { + 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")); + + // Hidden input for avatar url + 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) + .multi_line() + .auto_grow(3, 8) + .placeholder("A short introduce about you.") + }); + + cx.defer_in(window, move |this, window, cx| { + let nostr = NostrRegistry::global(cx); + let public_key = nostr.read(cx).identity().read(cx).public_key(); + + 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); + }); + + Self { + name: "Update Profile".into(), + focus_handle: cx.focus_handle(), + name_input, + avatar_input, + bio_input, + website_input, + uploading: false, + copied: false, + } + } + + fn set_profile(&mut self, person: Person, window: &mut Window, cx: &mut Context) { + let metadata = person.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); + } + }); + } + + 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 { + cx.spawn_in(window, async move |this, cx| { + cx.background_executor().timer(Duration::from_secs(2)).await; + + // Reset the copied state after a delay + cx.update(|window, cx| { + this.update(cx, |this, cx| { + this.set_copied(false, window, cx); + }) + .ok(); + }) + .ok(); + }) + .detach(); + } + } + + 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_file_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 paths.await { + Ok(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")) + } + } + _ => Err(anyhow!("Error")), + } + }); + + cx.spawn_in(window, async move |this, cx| { + let result = task.await; + + 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(); + } + + fn set_metadata(&mut self, window: &mut Window, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let public_key = nostr.read(cx).identity().read(cx).public_key(); + + // Get the old metadata + let persons = PersonRegistry::global(cx); + let old_metadata = persons.read(cx).get(&public_key, cx).metadata(); + + // Extract all new metadata fields + let avatar = self.avatar_input.read(cx).value(); + let name = self.name_input.read(cx).value(); + let bio = self.bio_input.read(cx).value(); + let website = self.website_input.read(cx).value(); + + // Construct the new metadata + let mut new_metadata = old_metadata + .display_name(name.as_ref()) + .name(name.as_ref()) + .about(bio.as_ref()); + + // Verify the avatar URL before adding it + if let Ok(url) = Url::from_str(&avatar) { + new_metadata = new_metadata.picture(url); + } + + // Verify the website URL before adding it + if let Ok(url) = Url::from_str(&website) { + new_metadata = new_metadata.website(url); + } + + // Set the metadata + let task = nostr.read(cx).set_metadata(&new_metadata, cx); + + cx.spawn_in(window, async move |this, cx| { + match task.await { + Ok(()) => { + cx.update(|window, cx| { + persons.update(cx, |this, cx| { + this.insert(Person::new(public_key, new_metadata), cx); + }); + + this.update(cx, |this, cx| { + this.set_metadata(window, cx); + }) + .ok(); + + 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(); + } +} + +impl Panel for ProfilePanel { + fn panel_id(&self) -> SharedString { + self.name.clone() + } + + fn title(&self, _cx: &App) -> AnyElement { + self.name.clone().into_any_element() + } +} + +impl EventEmitter for ProfilePanel {} + +impl Focusable for ProfilePanel { + fn focus_handle(&self, _: &App) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for ProfilePanel { + fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context) -> impl IntoElement { + let nostr = NostrRegistry::global(cx); + let public_key = nostr.read(cx).identity().read(cx).public_key(); + let shorten_pkey = SharedString::from(shorten_pubkey(public_key, 8)); + + // 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() + }; + + v_flex() + .size_full() + .items_center() + .justify_center() + .p_2() + .child( + v_flex() + .gap_2() + .w_112() + .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( + public_key.to_bech32().unwrap(), + window, + cx, + ); + })), + ), + ), + ) + .child(divider(cx)) + .child( + Button::new("submit") + .label("Continue") + .primary() + .disabled(self.uploading) + .on_click(cx.listener(move |this, _ev, window, cx| { + this.set_metadata(window, cx); + })), + ), + ) + } +} diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index 484ce6f..7dda3fa 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -109,7 +109,7 @@ impl Workspace { } } - pub fn add_panel

(panel: P, window: &mut Window, cx: &mut App) + pub fn add_panel

(panel: P, placement: DockPlacement, window: &mut Window, cx: &mut App) where P: PanelView, { @@ -117,7 +117,7 @@ impl Workspace { if let Ok(workspace) = root.read(cx).view().clone().downcast::() { workspace.update(cx, |this, cx| { this.dock.update(cx, |this, cx| { - this.add_panel(Arc::new(panel), DockPlacement::Center, window, cx); + this.add_panel(Arc::new(panel), placement, window, cx); }); }); } diff --git a/crates/dock/src/tab/mod.rs b/crates/dock/src/tab/mod.rs index 5a5c6df..cda8983 100644 --- a/crates/dock/src/tab/mod.rs +++ b/crates/dock/src/tab/mod.rs @@ -136,7 +136,7 @@ impl RenderOnce for Tab { self.base .id(self.ix) .h(TITLEBAR_HEIGHT) - .px_2() + .px_4() .relative() .flex() .items_center() diff --git a/crates/dock/src/tab/tab_bar.rs b/crates/dock/src/tab/tab_bar.rs index b7597ae..24b296a 100644 --- a/crates/dock/src/tab/tab_bar.rs +++ b/crates/dock/src/tab/tab_bar.rs @@ -9,9 +9,6 @@ use smallvec::SmallVec; use theme::ActiveTheme; use ui::{h_flex, Sizable, Size, StyledExt}; -use crate::platforms::linux::LinuxWindowControls; -use crate::platforms::windows::WindowsWindowControls; - #[derive(IntoElement)] pub struct TabBar { base: Div, @@ -132,17 +129,5 @@ impl RenderOnce for TabBar { }), ) .when_some(self.suffix, |this, suffix| this.child(suffix)) - .when( - !cx.theme().platform.is_mac() && !window.is_fullscreen(), - |this| match cx.theme().platform { - theme::PlatformKind::Linux => { - this.child(div().px_2().child(LinuxWindowControls::new())) - } - theme::PlatformKind::Windows => { - this.child(WindowsWindowControls::new(Self::height(window))) - } - _ => this, - }, - ) } } diff --git a/crates/dock/src/tab_panel.rs b/crates/dock/src/tab_panel.rs index 3895556..21dc572 100644 --- a/crates/dock/src/tab_panel.rs +++ b/crates/dock/src/tab_panel.rs @@ -5,9 +5,9 @@ use gpui::{ div, px, rems, App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString, - StatefulInteractiveElement, Styled, WeakEntity, Window, WindowControlArea, + StatefulInteractiveElement, Styled, WeakEntity, Window, }; -use theme::{ActiveTheme, PlatformKind, CLIENT_SIDE_DECORATION_ROUNDING, TITLEBAR_HEIGHT}; +use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, TITLEBAR_HEIGHT}; use ui::button::{Button, ButtonVariants as _}; use ui::popup_menu::{PopupMenu, PopupMenuExt}; use ui::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt}; @@ -88,9 +88,6 @@ pub struct TabPanel { /// Whether the tab panel is collapsed collapsed: bool, - /// Whether window is moving - window_move: bool, - /// When drag move, will get the placement of the panel to be split will_split_placement: Option, } @@ -160,7 +157,6 @@ impl TabPanel { will_split_placement: None, zoomed: false, collapsed: false, - window_move: false, closable: true, } } @@ -577,8 +573,6 @@ impl TabPanel { return div().into_any_element(); }; - #[cfg(target_os = "linux")] - let supported_controls = window.window_controls(); let left_dock_button = self.render_dock_toggle_button(DockPlacement::Left, window, cx); let bottom_dock_button = self.render_dock_toggle_button(DockPlacement::Bottom, window, cx); let right_dock_button = self.render_dock_toggle_button(DockPlacement::Right, window, cx); @@ -737,40 +731,9 @@ impl TabPanel { // empty space to allow move to last tab right div() .id("tab-bar-empty-space") - .window_control_area(WindowControlArea::Drag) .h_full() .flex_grow() .min_w_16() - .when(!window.is_fullscreen(), |this| match cx.theme().platform { - PlatformKind::Linux => this - .when(supported_controls.window_menu, |this| { - this.on_mouse_down(MouseButton::Right, move |ev, window, _| { - window.show_window_menu(ev.position) - }) - }) - .on_mouse_move(cx.listener(move |this, _ev, window, _| { - if this.window_move { - this.window_move = false; - window.start_window_move(); - } - })) - .on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| { - this.window_move = false; - })) - .on_mouse_up( - MouseButton::Left, - cx.listener(move |this, _ev, _window, _cx| { - this.window_move = false; - }), - ) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, _ev, _window, _cx| { - this.window_move = true; - }), - ), - _ => this, - }) .when(state.droppable, |this| { let view = cx.entity(); @@ -804,7 +767,6 @@ impl TabPanel { .border_color(cx.theme().border) .border_l_1() .border_b_1() - .when(!cx.theme().platform.is_mac(), |this| this.border_r_1()) .child(self.render_toolbar(state, window, cx)) .when_some(right_dock_button, |this, btn| this.child(btn)), ) diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 31fdc60..8311f1a 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -615,6 +615,27 @@ impl NostrRegistry { })); } + /// Set the metadata for the current user + pub fn set_metadata(&self, metadata: &Metadata, cx: &App) -> Task> { + let client = self.client(); + let public_key = self.identity().read(cx).public_key(); + let write_relays = self.write_relays(&public_key, cx); + let metadata = metadata.clone(); + + cx.background_spawn(async move { + let urls = write_relays.await; + let signer = client.signer().await?; + + // Sign the new metadata event + let event = EventBuilder::metadata(&metadata).sign(&signer).await?; + + // Send event to user's write relayss + client.send_event_to(urls, &event).await?; + + Ok(()) + }) + } + /// Get local stored identity fn get_identity(&mut self, cx: &mut Context) { let read_credential = cx.read_credentials(CLIENT_NAME); diff --git a/crates/ui/src/styled.rs b/crates/ui/src/styled.rs index 8e40768..6cc9480 100644 --- a/crates/ui/src/styled.rs +++ b/crates/ui/src/styled.rs @@ -18,7 +18,7 @@ pub fn v_flex() -> Div { /// Returns a `Div` as divider. pub fn divider(cx: &App) -> Div { - div().my_2().w_full().h_px().bg(cx.theme().border) + div().my_2().w_full().h_px().bg(cx.theme().border_variant) } macro_rules! font_weight {