diff --git a/assets/icons/group.svg b/assets/icons/group.svg new file mode 100644 index 0000000..7f94e3c --- /dev/null +++ b/assets/icons/group.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index 24fa1b4..dd908d4 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -16,8 +16,8 @@ use global::constants::{ use global::{app_state, nostr_client, AuthRequest, Notice, SignalKind, UnwrappingStatus}; use gpui::prelude::FluentBuilder; use gpui::{ - deferred, div, px, rems, App, AppContext, AsyncWindowContext, Axis, Context, Entity, - InteractiveElement, IntoElement, ParentElement, Render, SharedString, + deferred, div, px, rems, App, AppContext, AsyncWindowContext, Axis, ClipboardItem, Context, + Entity, InteractiveElement, IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window, }; use i18n::{shared_t, t}; @@ -30,7 +30,7 @@ use signer_proxy::{BrowserSignerProxy, BrowserSignerProxyOptions}; use smallvec::{smallvec, SmallVec}; use theme::{ActiveTheme, Theme, ThemeMode}; use title_bar::TitleBar; -use ui::actions::OpenProfile; +use ui::actions::{CopyPublicKey, OpenPublicKey}; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::dock_area::dock::DockPlacement; @@ -1177,7 +1177,7 @@ impl ChatSpace { .detach(); } - fn on_open_profile(&mut self, ev: &OpenProfile, window: &mut Window, cx: &mut Context) { + 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); @@ -1195,6 +1195,12 @@ impl ChatSpace { }); } + fn on_copy_pubkey(&mut self, ev: &CopyPublicKey, window: &mut Window, cx: &mut Context) { + let Ok(bech32) = ev.0.to_bech32(); + cx.write_to_clipboard(ClipboardItem::new_string(bech32)); + window.push_notification(t!("common.copied"), cx); + } + fn render_proxy_modal(&mut self, window: &mut Window, cx: &mut App) { window.open_modal(cx, |this, _window, _cx| { this.overlay_closable(false) @@ -1496,7 +1502,8 @@ impl Render for ChatSpace { .on_action(cx.listener(Self::on_settings)) .on_action(cx.listener(Self::on_dark_mode)) .on_action(cx.listener(Self::on_sign_out)) - .on_action(cx.listener(Self::on_open_profile)) + .on_action(cx.listener(Self::on_open_pubkey)) + .on_action(cx.listener(Self::on_copy_pubkey)) .on_action(cx.listener(Self::on_reload_metadata)) .relative() .size_full() diff --git a/crates/coop/src/views/chat/mod.rs b/crates/coop/src/views/chat/mod.rs index 5883fad..ed7846f 100644 --- a/crates/coop/src/views/chat/mod.rs +++ b/crates/coop/src/views/chat/mod.rs @@ -24,8 +24,10 @@ use settings::AppSettings; use smallvec::{smallvec, SmallVec}; use smol::fs; use theme::ActiveTheme; +use ui::actions::{CopyPublicKey, OpenPublicKey}; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; +use ui::context_menu::ContextMenuExt; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::emoji_picker::EmojiPicker; use ui::input::{InputEvent, InputState, TextInput}; @@ -693,6 +695,7 @@ impl Chat { let id = message.id; let author = self.profile(&message.author, cx); + let public_key = author.public_key(); let replies = message.replies_to.as_slice(); let has_replies = !replies.is_empty(); @@ -715,7 +718,18 @@ impl Chat { .flex() .gap_3() .when(!hide_avatar, |this| { - this.child(Avatar::new(author.avatar(proxy)).size(rems(2.))) + this.child( + div() + .id(SharedString::from(format!("{ix}-avatar"))) + .child(Avatar::new(author.avatar(proxy)).size(rems(2.))) + .context_menu(move |this, _window, _cx| { + let view = Box::new(OpenPublicKey(public_key)); + let copy = Box::new(CopyPublicKey(public_key)); + + this.menu(t!("profile.view"), view) + .menu(t!("profile.copy"), copy) + }), + ) }) .child( v_flex() @@ -1318,9 +1332,7 @@ impl Panel for Chat { let label = this.display_name(cx); let url = this.display_image(proxy, cx); - div() - .flex() - .items_center() + h_flex() .gap_1p5() .child(Avatar::new(url).size(rems(1.25))) .child(label) diff --git a/crates/coop/src/views/sidebar/list_item.rs b/crates/coop/src/views/sidebar/list_item.rs index 5a607ac..33363b1 100644 --- a/crates/coop/src/views/sidebar/list_item.rs +++ b/crates/coop/src/views/sidebar/list_item.rs @@ -11,7 +11,9 @@ use registry::room::RoomKind; use registry::Registry; use settings::AppSettings; use theme::ActiveTheme; +use ui::actions::{CopyPublicKey, OpenPublicKey}; use ui::avatar::Avatar; +use ui::context_menu::ContextMenuExt; use ui::modal::ModalButtonProps; use ui::skeleton::Skeleton; use ui::{h_flex, ContextModal, StyledExt}; @@ -166,6 +168,10 @@ impl RenderOnce for RoomListItem { ), ) .hover(|this| this.bg(cx.theme().elevated_surface_background)) + .context_menu(move |this, _window, _cx| { + this.menu(t!("profile.view"), Box::new(OpenPublicKey(public_key))) + .menu(t!("profile.copy"), Box::new(CopyPublicKey(public_key))) + }) .on_click(move |event, window, cx| { handler(event, window, cx); diff --git a/crates/coop/src/views/user_profile.rs b/crates/coop/src/views/user_profile.rs index 81d61e7..c144762 100644 --- a/crates/coop/src/views/user_profile.rs +++ b/crates/coop/src/views/user_profile.rs @@ -17,7 +17,7 @@ use smallvec::{smallvec, SmallVec}; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; -use ui::{h_flex, v_flex, Disableable, Icon, IconName, Sizable, StyledExt}; +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)) @@ -32,27 +32,24 @@ pub struct UserProfile { } impl UserProfile { - pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context) -> Self { + pub fn new(target: PublicKey, window: &mut Window, cx: &mut Context) -> Self { let registry = Registry::read_global(cx); - let identity = registry.identity(cx).public_key(); - let profile = registry.get_person(&public_key, cx); + let profile = registry.get_person(&target, cx); let mut tasks = smallvec![]; - let check_follow: Task = cx.background_spawn(async move { + let check_follow: Task> = cx.background_spawn(async move { let client = nostr_client(); - let filter = Filter::new() - .kind(Kind::ContactList) - .author(identity) - .pubkey(public_key) - .limit(1); + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + let contact_list = client.database().contacts_public_keys(public_key).await?; - client.database().count(filter).await.unwrap_or(0) >= 1 + Ok(contact_list.contains(&target)) }); let verify_nip05 = if let Some(address) = profile.metadata().nip05 { Some(Tokio::spawn(cx, async move { - nip05_verify(public_key, &address).await.unwrap_or(false) + nip05_verify(target, &address).await.unwrap_or(false) })) } else { None @@ -61,7 +58,7 @@ impl UserProfile { tasks.push( // Load user profile data cx.spawn_in(window, async move |this, cx| { - let followed = check_follow.await; + let followed = check_follow.await.unwrap_or(false); // Update the followed status this.update(cx, |this, cx| { @@ -133,6 +130,7 @@ impl Render for UserProfile { v_flex() .gap_4() + .text_sm() .child( v_flex() .gap_3() @@ -188,10 +186,8 @@ impl Render for UserProfile { .child( v_flex() .gap_1() - .text_sm() .child( div() - .block() .text_color(cx.theme().text_muted) .child(SharedString::from("Public Key:")), ) @@ -201,12 +197,13 @@ impl Render for UserProfile { .child( div() .p_2() - .h_9() + .h_7() .rounded_md() .bg(cx.theme().elevated_surface_background) .truncate() .text_ellipsis() .line_clamp(1) + .line_height(relative(1.)) .child(shared_bech32), ) .child( @@ -218,8 +215,8 @@ impl Render for UserProfile { IconName::Copy } }) - .ghost() - .disabled(self.copied) + .cta() + .ghost_alt() .on_click(cx.listener(move |this, _e, window, cx| { this.copy_pubkey(window, cx); })), @@ -229,7 +226,6 @@ impl Render for UserProfile { .child( v_flex() .gap_1() - .text_sm() .child( div() .text_color(cx.theme().text_muted) diff --git a/crates/ui/src/actions.rs b/crates/ui/src/actions.rs index 3de2809..0abc6c8 100644 --- a/crates/ui/src/actions.rs +++ b/crates/ui/src/actions.rs @@ -2,10 +2,15 @@ use gpui::{actions, Action}; use nostr_sdk::prelude::PublicKey; use serde::Deserialize; -/// Define a open profile action +/// Define a open public key action #[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)] -#[action(namespace = profile, no_json)] -pub struct OpenProfile(pub PublicKey); +#[action(namespace = pubkey, no_json)] +pub struct OpenPublicKey(pub PublicKey); + +/// Define a copy inline public key action +#[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)] +#[action(namespace = pubkey, no_json)] +pub struct CopyPublicKey(pub PublicKey); /// Define a custom confirm action #[derive(Clone, Action, PartialEq, Eq, Deserialize)] diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index 143fbbb..940e190 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -45,6 +45,7 @@ pub enum IconName { Plus, PlusFill, PlusCircleFill, + Group, ResizeCorner, Reply, Report, @@ -106,6 +107,7 @@ impl IconName { Self::Plus => "icons/plus.svg", Self::PlusFill => "icons/plus-fill.svg", Self::PlusCircleFill => "icons/plus-circle-fill.svg", + Self::Group => "icons/group.svg", Self::ResizeCorner => "icons/resize-corner.svg", Self::Reply => "icons/reply.svg", Self::Report => "icons/report.svg", diff --git a/crates/ui/src/text.rs b/crates/ui/src/text.rs index 57e0190..a36c1d9 100644 --- a/crates/ui/src/text.rs +++ b/crates/ui/src/text.rs @@ -13,7 +13,7 @@ use regex::Regex; use registry::Registry; use theme::ActiveTheme; -use crate::actions::OpenProfile; +use crate::actions::OpenPublicKey; static URL_REGEX: Lazy = Lazy::new(|| { Regex::new(r"^(?:[a-zA-Z]+://)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d+)?(/.*)?$").unwrap() @@ -140,7 +140,7 @@ impl RenderedText { log::error!("Failed to parse public key from: {clean_url}"); return; }; - window.dispatch_action(Box::new(OpenProfile(public_key)), cx); + window.dispatch_action(Box::new(OpenPublicKey(public_key)), cx); } else if is_url(token) { if !token.starts_with("http") { cx.open_url(&format!("https://{token}")); diff --git a/locales/app.yml b/locales/app.yml index 297ed2f..f21b0db 100644 --- a/locales/app.yml +++ b/locales/app.yml @@ -272,9 +272,11 @@ profile: unknown: en: "Unknown contact" njump: - en: "Open in njump.me" + en: "View on njump.me" no_bio: en: "No bio." + copy: + en: "Copy Public Key" preferences: account_header: