feat: add context menu for quick profile viewing (#170)
* add profile context menu * add context menu for avatar
This commit is contained in:
@@ -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<Self>) {
|
||||
fn on_open_pubkey(&mut self, ev: &OpenPublicKey, window: &mut Window, cx: &mut Context<Self>) {
|
||||
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<Self>) {
|
||||
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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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<UserProfile> {
|
||||
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>) -> Self {
|
||||
pub fn new(target: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> 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<bool> = cx.background_spawn(async move {
|
||||
let check_follow: Task<Result<bool, Error>> = 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)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<Regex> = 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}"));
|
||||
|
||||
Reference in New Issue
Block a user