diff --git a/crates/chat_ui/src/actions.rs b/crates/chat_ui/src/actions.rs index 915f212..c2ed059 100644 --- a/crates/chat_ui/src/actions.rs +++ b/crates/chat_ui/src/actions.rs @@ -6,22 +6,13 @@ use settings::SignerKind; #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = chat, no_json)] pub enum Command { - Insert(&'static str), ChangeSubject(&'static str), ChangeSigner(SignerKind), ToggleBackup, + + Insert(&'static str), + + Copy(PublicKey), + Relays(PublicKey), + Njump(PublicKey), } - -#[derive(Action, Clone, PartialEq, Eq, Deserialize)] -#[action(namespace = chat, no_json)] -pub struct SeenOn(pub EventId); - -/// Define a open public key action -#[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)] -#[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); diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 2ffaf8a..8d44a42 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -27,7 +27,7 @@ use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::indicator::Indicator; use ui::input::{InputEvent, InputState, TextInput}; -use ui::menu::{ContextMenuExt, DropdownMenu}; +use ui::menu::DropdownMenu; use ui::notification::Notification; use ui::scroll::Scrollbar; use ui::{ @@ -505,10 +505,21 @@ impl ChatPanel { } } - fn copy_message(&self, id: &EventId, cx: &Context) { - if let Some(message) = self.message(id) { - cx.write_to_clipboard(ClipboardItem::new_string(message.content.to_string())); - } + fn copy_author(&self, public_key: &PublicKey, cx: &App) { + let content = public_key.to_bech32().unwrap(); + let item = ClipboardItem::new_string(content); + + cx.write_to_clipboard(item); + } + + fn copy_message(&self, id: &EventId, cx: &App) { + let Some(message) = self.message(id) else { + return; + }; + let content = message.content.to_string(); + let item = ClipboardItem::new_string(content); + + cx.write_to_clipboard(item); } fn reply_to(&mut self, id: &EventId, cx: &mut Context) { @@ -588,7 +599,7 @@ impl ChatPanel { }); } - fn profile(&self, public_key: &PublicKey, cx: &Context) -> Person { + fn profile(&self, public_key: &PublicKey, cx: &App) -> Person { let persons = PersonRegistry::global(cx); persons.read(cx).get(public_key, cx) } @@ -631,9 +642,53 @@ impl ChatPanel { window.push_notification(Notification::error("Failed to toggle backup"), cx); } } + Command::Copy(public_key) => { + self.copy_author(public_key, cx); + } + Command::Relays(public_key) => { + self.open_relays(public_key, window, cx); + } + Command::Njump(public_key) => { + self.open_njump(public_key, cx); + } } } + fn open_relays(&mut self, public_key: &PublicKey, window: &mut Window, cx: &mut Context) { + let profile = self.profile(public_key, cx); + + window.open_modal(cx, move |this, _window, cx| { + let relays = profile.messaging_relays(); + + this.title("Messaging Relays") + .show_close(true) + .child(v_flex().gap_1().children({ + let mut items = vec![]; + + for url in relays.iter() { + items.push( + h_flex() + .h_7() + .px_2() + .gap_2() + .bg(cx.theme().elevated_surface_background) + .rounded(cx.theme().radius) + .text_sm() + .child(div().size_1p5().rounded_full().bg(gpui::green())) + .child(SharedString::from(url.to_string())), + ); + } + + items + })) + }); + } + + fn open_njump(&mut self, public_key: &PublicKey, cx: &mut Context) { + let content = format!("https://njump.me/{}", public_key.to_bech32().unwrap()); + cx.open_url(&content); + } + fn render_announcement(&self, ix: usize, cx: &Context) -> AnyElement { v_flex() .id(ix) @@ -758,18 +813,14 @@ impl ChatPanel { .flex() .gap_3() .when(!hide_avatar, |this| { - this.child( - div() - .id(SharedString::from(format!("{ix}-avatar"))) - .child(Avatar::new(author.avatar())) - .context_menu(move |this, _window, _cx| { - let view = Box::new(OpenPublicKey(public_key)); - let copy = Box::new(CopyPublicKey(public_key)); - - this.menu("View Profile", view) - .menu("Copy Public Key", copy) - }), - ) + this.child(Avatar::new(author.avatar()).dropdown_menu( + move |this, _window, _cx| { + this.menu("Copy Public Key", Box::new(Command::Copy(public_key))) + .menu("View Relays", Box::new(Command::Relays(public_key))) + .separator() + .menu("View on njump.me", Box::new(Command::Njump(public_key))) + }, + )) }) .child( v_flex() @@ -807,8 +858,17 @@ impl ChatPanel { }), ), ) - .child(self.render_border(cx)) - .child(self.render_actions(&id, cx)) + .child( + div() + .group_hover("", |this| this.bg(cx.theme().element_active)) + .absolute() + .left_0() + .top_0() + .w(px(2.)) + .h_full() + .bg(cx.theme().border_transparent), + ) + .child(self.render_actions(&id, &public_key, cx)) .on_mouse_down( MouseButton::Middle, cx.listener(move |this, _, _window, cx| { @@ -911,7 +971,7 @@ impl ChatPanel { window.open_modal(cx, move |this, _window, cx| { this.show_close(true) .title(SharedString::from("Sent Reports")) - .child(v_flex().gap_4().pb_4().w_full().children({ + .child(v_flex().gap_4().w_full().children({ let mut items = Vec::with_capacity(reports.len()); for report in reports.iter() { @@ -1030,18 +1090,12 @@ impl ChatPanel { }) } - fn render_border(&self, cx: &Context) -> impl IntoElement { - div() - .group_hover("", |this| this.bg(cx.theme().element_active)) - .absolute() - .left_0() - .top_0() - .w(px(2.)) - .h_full() - .bg(cx.theme().border_transparent) - } - - fn render_actions(&self, id: &EventId, cx: &Context) -> impl IntoElement { + fn render_actions( + &self, + id: &EventId, + public_key: &PublicKey, + cx: &Context, + ) -> impl IntoElement { h_flex() .p_0p5() .gap_1() @@ -1082,13 +1136,22 @@ impl ChatPanel { ) .child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border)) .child( - Button::new("seen-on") + Button::new("advance") .icon(IconName::Ellipsis) .small() .ghost() .dropdown_menu({ - let id = id.to_owned(); - move |this, _window, _cx| this.menu("Seen on", Box::new(SeenOn(id))) + let public_key = *public_key; + let _id = *id; + move |this, _window, _cx| { + this.menu("Copy author", Box::new(Command::Copy(public_key))) + /* + .menu( + "Trace", + Box::new(Command::Trace(id)), + ) + */ + } }), ) .group_hover("", |this| this.visible()) diff --git a/crates/ui/src/avatar.rs b/crates/ui/src/avatar.rs index 2d79545..7ceaaa8 100644 --- a/crates/ui/src/avatar.rs +++ b/crates/ui/src/avatar.rs @@ -1,12 +1,12 @@ use gpui::prelude::FluentBuilder; use gpui::{ - div, img, px, AbsoluteLength, App, Div, Hsla, ImageSource, Img, InteractiveElement, - Interactivity, IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage, - Window, + AbsoluteLength, App, Div, Hsla, ImageSource, Img, InteractiveElement, Interactivity, + IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage, Window, div, img, + px, }; use theme::ActiveTheme; -use crate::{Sizable, Size}; +use crate::{Selectable, Sizable, Size}; /// Returns the size of the avatar based on the given [`Size`]. pub(super) fn avatar_size(size: Size) -> AbsoluteLength { @@ -37,6 +37,7 @@ pub struct Avatar { style: StyleRefinement, size: Size, border_color: Option, + selected: bool, } impl Avatar { @@ -48,6 +49,7 @@ impl Avatar { style: StyleRefinement::default(), size: Size::Medium, border_color: None, + selected: false, } } @@ -89,6 +91,17 @@ impl Styled for Avatar { } } +impl Selectable for Avatar { + fn is_selected(&self) -> bool { + self.selected + } + + fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } +} + impl InteractiveElement for Avatar { fn interactivity(&mut self) -> &mut Interactivity { self.base.interactivity() diff --git a/crates/ui/src/menu/dropdown_menu.rs b/crates/ui/src/menu/dropdown_menu.rs index c5938cc..a053291 100644 --- a/crates/ui/src/menu/dropdown_menu.rs +++ b/crates/ui/src/menu/dropdown_menu.rs @@ -5,10 +5,11 @@ use gpui::{ RenderOnce, SharedString, StyleRefinement, Styled, Window, }; +use crate::Selectable; +use crate::avatar::Avatar; use crate::button::Button; use crate::menu::PopupMenu; use crate::popover::Popover; -use crate::Selectable; /// A dropdown menu trait for buttons and other interactive elements pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement + 'static { @@ -35,6 +36,8 @@ pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement + impl DropdownMenu for Button {} +impl DropdownMenu for Avatar {} + #[derive(IntoElement)] pub struct DropdownMenuPopover { id: ElementId, diff --git a/crates/ui/src/modal.rs b/crates/ui/src/modal.rs index d30e9a2..a974ab2 100644 --- a/crates/ui/src/modal.rs +++ b/crates/ui/src/modal.rs @@ -3,10 +3,9 @@ use std::time::Duration; use gpui::prelude::FluentBuilder; use gpui::{ - anchored, div, hsla, point, px, Animation, AnimationExt as _, AnyElement, App, Bounds, - BoxShadow, ClickEvent, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding, - MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, StyleRefinement, Styled, - Window, + Animation, AnimationExt as _, AnyElement, App, Bounds, BoxShadow, ClickEvent, Div, FocusHandle, + InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, + RenderOnce, SharedString, StyleRefinement, Styled, Window, anchored, div, hsla, point, px, }; use theme::ActiveTheme; @@ -14,7 +13,7 @@ use crate::actions::{Cancel, Confirm}; use crate::animation::cubic_bezier; use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _}; use crate::scroll::ScrollableElement; -use crate::{h_flex, v_flex, IconName, Root, Sizable, StyledExt, WindowExtension}; +use crate::{IconName, Root, Sizable, StyledExt, WindowExtension, h_flex, v_flex}; const CONTEXT: &str = "Modal"; @@ -500,6 +499,7 @@ impl RenderOnce for Modal { .child(self.content), ), ) + .when_none(&self.footer, |this| this.child(div().pt(padding_left))) .when_some(self.footer, |this, footer| { this.child( h_flex()