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),
)