diff --git a/assets/icons/paperclip.svg b/assets/icons/paperclip.svg new file mode 100644 index 00000000..b1e60469 --- /dev/null +++ b/assets/icons/paperclip.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/assets/tests/icons.rs b/crates/assets/tests/icons.rs deleted file mode 100644 index f9be2375..00000000 --- a/crates/assets/tests/icons.rs +++ /dev/null @@ -1,39 +0,0 @@ -use gpui::AssetSource; - -#[test] -fn icons_resolve_from_the_vendored_folder() { - // Icons coop's code and the kit components draw, served by the vendored - // folder (signed's set, filled with the kit bundle where coop needs more). - let vendored = [ - "icons/search.svg", - "icons/close.svg", - "icons/panel-left.svg", - "icons/window-minimize.svg", - "icons/settings-2.svg", - "icons/x.svg", - "icons/compass.svg", - "icons/radio-tower.svg", - ]; - for path in vendored { - let own = assets::Assets - .load(path) - .unwrap_or_else(|e| panic!("{path} errored in own embed: {e}")); - assert!(own.is_some(), "own embed is missing {path}"); - - let combined = assets::CombinedAssets.load(path).unwrap(); - assert!(combined.is_some(), "CombinedAssets fails to serve {path}"); - } - - // Coop's own files still load through the combined source. - let own = [ - "brand/coop.svg", - "brand/bg1.jpg", - "fonts/Inter/Inter-Medium.ttf", - ]; - for path in own { - assert!( - matches!(assets::CombinedAssets.load(path), Ok(Some(_))), - "CombinedAssets fails to serve coop asset {path}" - ); - } -} diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 28e0f739..e339002b 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -1390,21 +1390,26 @@ impl ChatPanel { } let reply_id = *id; - let copy_id = *id; let dropdown_id = *id; let public_key = *public_key; - actions - .reply(cx.listener(move |this, _event, _window, cx| { - this.reply_to(&reply_id, cx); - })) - .copy(cx.listener(move |this, _event, _window, cx| { - this.copy_message(©_id, cx); - })) - .dropdown(move |menu, _window, _cx| { - menu.menu("Copy author", Box::new(Command::Copy(public_key))) - .menu("Seen on", Box::new(Command::Trace(dropdown_id))) - }) + let actions = actions.reply(cx.listener(move |this, _event, _window, cx| { + this.reply_to(&reply_id, cx); + })); + + let actions = if let Some(message) = self.message(id) { + actions.copy( + SharedString::from(format!("message-copy-{id}")), + message.preview(), + ) + } else { + actions + }; + + actions.dropdown(move |menu, _window, _cx| { + menu.menu("Copy author", Box::new(Command::Copy(public_key))) + .menu("Seen on", Box::new(Command::Trace(dropdown_id))) + }) } fn render_attachment(&self, url: &Url, cx: &Context) -> impl IntoElement { @@ -1879,7 +1884,7 @@ impl Render for ChatPanel { .items_end() .child( Button::new("upload") - .icon(IconName::Plus) + .icon(IconName::Paperclip) .tooltip("Upload media") .loading(self.uploading) .disabled(self.uploading) @@ -1888,7 +1893,14 @@ impl Render for ChatPanel { this.upload(window, cx); })), ) - .child(Textarea::new(&self.input).appearance(false).flex_1()) + .child( + Textarea::new(&self.input) + .appearance(false) + .small() + .min_h_8() + .py_1() + .flex_1(), + ) .child( h_flex() .pl_1() diff --git a/crates/nip29_ui/src/lib.rs b/crates/nip29_ui/src/lib.rs index 2d2b187a..cb2dbbd4 100644 --- a/crates/nip29_ui/src/lib.rs +++ b/crates/nip29_ui/src/lib.rs @@ -1055,8 +1055,8 @@ impl GroupPanel { .items_end() .child( Button::new("upload") - .icon(IconName::Upload) - .tooltip("Upload file") + .icon(ui::IconName::Paperclip) + .tooltip("Upload media") .loading(self.uploading) .disabled(self.uploading) .ghost() @@ -1064,12 +1064,21 @@ impl GroupPanel { this.upload(window, cx); })), ) - .child(Textarea::new(&self.input).appearance(false).flex_1()) + .child( + Textarea::new(&self.input) + .appearance(false) + .bg(cx.theme().input) + .small() + .min_h_8() + .py_1() + .flex_1(), + ) .child( Button::new("send") .icon(ui::IconName::PaperPlaneFill) .tooltip("Send") .ghost() + .disabled(self.uploading) .on_click(cx.listener(|this, _event, window, cx| { this.send(window, cx); })), diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index 20b9effb..2d723053 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -20,6 +20,7 @@ pub enum IconName { Link, Lock, Message, + Paperclip, PaperPlaneFill, Plus, PlusChat, @@ -51,6 +52,7 @@ impl IconNamed for IconName { Self::Link => "icons/link.svg", Self::Lock => "icons/lock.svg", Self::Message => "icons/message.svg", + Self::Paperclip => "icons/paperclip.svg", Self::PaperPlaneFill => "icons/paper-plane-fill.svg", Self::Plus => "icons/plus.svg", Self::PlusChat => "icons/plus-chat.svg", diff --git a/crates/ui/src/message.rs b/crates/ui/src/message.rs index 6c61bc98..745f5108 100644 --- a/crates/ui/src/message.rs +++ b/crates/ui/src/message.rs @@ -10,6 +10,7 @@ use gpui::{ }; use gpui_kit::assets::IconName; use gpui_kit::component::button::{Button, ButtonVariants as _}; +use gpui_kit::component::clipboard::Clipboard; use gpui_kit::component::menu::{DropdownMenu as _, PopupMenu, PopupMenuItem}; use gpui_kit::component::{ ActiveTheme, InteractiveElementExt as _, Sizable as _, StyledExt as _, h_flex, v_flex, @@ -602,7 +603,7 @@ pub struct MessageActions { style: StyleRefinement, reactions: SmallVec<[(&'static str, SharedClickListener); 4]>, reply: Option, - copy: Option, + copy: Option<(ElementId, SharedString)>, dropdown: Option, } @@ -634,9 +635,10 @@ impl MessageActions { self } - /// Add the copy button. - pub fn copy(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self { - self.copy = Some(Rc::new(handler)); + /// Add the copy button for the given value. + /// The id must be unique per bar, the clipboard component keys its copied state on it. + pub fn copy(mut self, id: impl Into, value: impl Into) -> Self { + self.copy = Some((id.into(), value.into())); self } @@ -707,15 +709,8 @@ impl RenderOnce for MessageActions { .on_click(move |event, window, cx| reply(event, window, cx)), ) }) - .when_some(copy, |this, copy| { - this.child( - Button::new("copy") - .icon(IconName::Copy) - .tooltip("Copy") - .small() - .ghost() - .on_click(move |event, window, cx| copy(event, window, cx)), - ) + .when_some(copy, |this, (id, value)| { + this.child(Clipboard::new(id).value(value).tooltip("Copy").small()) }) .when_some(dropdown, |this, dropdown| { this.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border)) diff --git a/crates/workspace/src/dialogs/new_chat.rs b/crates/workspace/src/dialogs/new_chat.rs index e46bbf52..81d92ad0 100644 --- a/crates/workspace/src/dialogs/new_chat.rs +++ b/crates/workspace/src/dialogs/new_chat.rs @@ -10,7 +10,6 @@ use gpui::{ Window, div, px, uniform_list, }; use gpui_kit::assets::IconName; -use gpui_kit::base::StyledExt as _; use gpui_kit::component::button::{Button, ButtonVariants}; use gpui_kit::component::dock::{DockArea, DockPlacement, PanelHandle}; use gpui_kit::component::input::{Input, InputEvent, InputState}; @@ -205,6 +204,7 @@ impl NewChat { Ok(()) })); + return; } @@ -369,15 +369,13 @@ impl NewChat { .p_0p5() .rounded_full() .bg(cx.theme().secondary) - .text_xs() - .font_medium() .hover(|this| this.bg(cx.theme().primary)) .child( Avatar::new(profile.avatar()) .seed(profile.avatar_seed()) .xsmall(), ) - .child(div().max_w_32().truncate().child(profile.name())) + .child(div().max_w_32().truncate().text_xs().child(profile.name())) .on_click(cx.listener(move |this, _event, _window, cx| { this.toggle_member(&public_key, cx); })) diff --git a/crates/workspace/src/dialogs/profile.rs b/crates/workspace/src/dialogs/profile.rs index 82a67fd8..b3c83eb2 100644 --- a/crates/workspace/src/dialogs/profile.rs +++ b/crates/workspace/src/dialogs/profile.rs @@ -4,17 +4,17 @@ use anyhow::Error; use chat::{ChatRegistry, Room, RoomKind}; use gpui::prelude::FluentBuilder; use gpui::{ - App, AppContext, ClipboardItem, Context, Div, Entity, IntoElement, ParentElement, Render, - SharedString, Styled, Task, Window, div, retain_all, + App, AppContext, Context, Div, Entity, IntoElement, ParentElement, Render, SharedString, + Styled, Task, Window, div, retain_all, }; use gpui_kit::assets::IconName; use gpui_kit::base::StyledExt as _; use gpui_kit::component::button::{Button, ButtonVariants}; +use gpui_kit::component::clipboard::Clipboard; use gpui_kit::component::dock::DockPlacement; use gpui_kit::component::input::{Input, InputState, Textarea, TextareaState}; use gpui_kit::component::notification::Notification; use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex}; -use instant::Duration; use nostr_sdk::prelude::*; use person::{Directory, Person, PersonRegistry, shorten_pubkey}; use settings::AppSettings; @@ -49,8 +49,6 @@ struct ProfileDialog { website_input: Entity, /// Uploading state uploading: bool, - /// Copied states - copied: bool, /// Updating state updating: bool, /// Tasks @@ -92,7 +90,6 @@ impl ProfileDialog { bio_input, website_input, uploading: false, - copied: false, updating: false, tasks: vec![], } @@ -141,35 +138,6 @@ impl ProfileDialog { cx.notify(); } - 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 { - self.tasks.push(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(); - - Ok(()) - })); - } - } - fn set_uploading(&mut self, status: bool, cx: &mut Context) { self.uploading = status; cx.notify(); @@ -368,7 +336,8 @@ impl Render for ProfileDialog { .w_full() .items_center() .justify_center() - .bg(cx.theme().sidebar) + .rounded(cx.theme().radius_lg) + .bg(cx.theme().muted) .child(Avatar::new(avatar).seed(profile.avatar_seed()).large()) .when(self.editing, |this| { this.child( @@ -393,25 +362,13 @@ impl Render for ProfileDialog { .justify_center() .gap_3() .rounded(cx.theme().radius) - .bg(cx.theme().sidebar) + .bg(cx.theme().muted) .text_xs() .child(shorten_pkey) - .child( - Button::new("copy") - .icon({ - if self.copied { - IconName::CircleCheck - } else { - IconName::Copy - } - }) - .xsmall() - .secondary() - .on_click(cx.listener(move |this, _ev, window, cx| { - let pk = this.public_key.to_bech32().unwrap(); - this.copy(pk, window, cx) - })), - ), + .child(Clipboard::new("copy").value_fn({ + let public_key = self.public_key; + move |_, _| public_key.to_bech32().unwrap_or_default().into() + })), ), ) .when(self.editing, |this| { @@ -544,7 +501,7 @@ fn field( }; v_flex() - .gap_1p5() + .gap_1() .child(label(text, cx)) - .child(div().text_xs().text_color(color).child(content)) + .child(div().text_sm().text_color(color).child(content)) } diff --git a/crates/workspace/src/dialogs/relays.rs b/crates/workspace/src/dialogs/relays.rs index 4ec18ec2..620a3b94 100644 --- a/crates/workspace/src/dialogs/relays.rs +++ b/crates/workspace/src/dialogs/relays.rs @@ -264,7 +264,7 @@ impl RelayManager { fn render_tab_bar(&self, cx: &mut Context) -> impl IntoElement { TabBar::new("relays-tabs") - .segmented() + .pill() .selected_index(self.active_tab) .on_click(cx.listener(|this, ix: &usize, _window, cx| { this.active_tab = *ix; diff --git a/crates/workspace/src/panels/backup.rs b/crates/workspace/src/panels/backup.rs index 8a0bbcc5..1e20353a 100644 --- a/crates/workspace/src/panels/backup.rs +++ b/crates/workspace/src/panels/backup.rs @@ -1,16 +1,14 @@ use anyhow::Error; use gpui::{ - App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, - IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div, + App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, + ParentElement, Render, SharedString, Styled, Task, Window, div, }; -use gpui_kit::assets::IconName; use gpui_kit::base::StyledExt as _; -use gpui_kit::component::button::{Button, ButtonVariants}; +use gpui_kit::component::clipboard::Clipboard; use gpui_kit::component::dock::{Panel, PanelEvent}; use gpui_kit::component::input::{Input, InputState}; use gpui_kit::component::separator::Separator; -use gpui_kit::component::{ActiveTheme, Sizable, v_flex}; -use instant::Duration; +use gpui_kit::component::{ActiveTheme, v_flex}; use nostr_sdk::prelude::*; use state::USER_KEYRING; @@ -32,9 +30,6 @@ pub struct BackupPanel { /// Secret key input nsec_input: Entity, - /// Copied status - copied: bool, - /// Background tasks tasks: Vec>>, } @@ -54,7 +49,6 @@ impl BackupPanel { focus_handle: cx.focus_handle(), npub_input, nsec_input, - copied: false, tasks: vec![], } } @@ -81,31 +75,6 @@ impl BackupPanel { Ok(()) })); } - - fn copy_secret(&mut self, cx: &mut Context) { - let value = self.nsec_input.read(cx).value(); - let item = ClipboardItem::new_string(value.to_string()); - cx.write_to_clipboard(item); - - // Set the copied status to true - self.set_copied(true, cx); - } - - fn set_copied(&mut self, status: bool, cx: &mut Context) { - self.copied = status; - cx.notify(); - - self.tasks.push(cx.spawn(async move |this, cx| { - cx.background_executor().timer(Duration::from_secs(2)).await; - - // Clear the error message after a delay - this.update(cx, |this, cx| { - this.set_copied(false, cx); - })?; - - Ok(()) - })); - } } impl gpui_kit::component::dock::BasePanel for BackupPanel { @@ -174,20 +143,12 @@ impl Render for BackupPanel { .child(Input::new(&self.nsec_input).disabled(true)), ) .child( - Button::new("copy") - .icon(IconName::Copy) - .label({ - if self.copied { - "Copied" - } else { - "Copy secret key" - } - }) - .primary() - .small() - .on_click(cx.listener(move |this, _ev, _window, cx| { - this.copy_secret(cx); - })), + Clipboard::new("copy-secret") + .accessibility_label("Copy secret key") + .value_fn({ + let nsec_input = self.nsec_input.clone(); + move |_, cx| nsec_input.read(cx).value() + }), ), ) } diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index 640008f7..83b21fe5 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -340,7 +340,7 @@ impl Sidebar { ) .child( div() - .text_sm() + .text_xs() .text_color(cx.theme().sidebar_foreground) .child(label), )