From c67b223a53a5040f28d2acb2269c3ff256e019e9 Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Tue, 16 Sep 2025 19:59:03 +0700 Subject: [PATCH] chore: add missing ui elements (#153) * add empty state * . * update welcome panel --- crates/coop/src/views/chat/mod.rs | 83 +++++++++++++-------------- crates/coop/src/views/compose.rs | 4 +- crates/coop/src/views/new_account.rs | 4 +- crates/coop/src/views/preferences.rs | 4 +- crates/coop/src/views/screening.rs | 10 ++-- crates/coop/src/views/setup_relay.rs | 4 +- crates/coop/src/views/sidebar/mod.rs | 68 +++++++++++++++++++--- crates/coop/src/views/welcome.rs | 64 +++++++++++++-------- crates/title_bar/src/platforms/mac.rs | 2 +- crates/ui/src/button.rs | 19 +++--- crates/ui/src/dock_area/tab_panel.rs | 20 ++++--- crates/ui/src/resizable/panel.rs | 3 +- locales/app.yml | 8 +++ 13 files changed, 183 insertions(+), 110 deletions(-) diff --git a/crates/coop/src/views/chat/mod.rs b/crates/coop/src/views/chat/mod.rs index 24f3bd3..1a34120 100644 --- a/crates/coop/src/views/chat/mod.rs +++ b/crates/coop/src/views/chat/mod.rs @@ -26,7 +26,7 @@ use smallvec::{smallvec, SmallVec}; use smol::fs; use theme::ActiveTheme; use ui::avatar::Avatar; -use ui::button::{Button, ButtonRounded, ButtonVariants}; +use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::emoji_picker::EmojiPicker; use ui::input::{InputEvent, InputState, TextInput}; @@ -761,7 +761,7 @@ impl Chat { .label(t!("common.resend")) .danger() .xsmall() - .rounded(ButtonRounded::Full) + .rounded() .on_click(cx.listener( move |this, _, window, cx| { this.resend_message(&id, window, cx); @@ -1031,45 +1031,6 @@ impl Chat { } fn render_actions(&self, id: &EventId, cx: &Context) -> impl IntoElement { - let reply = Button::new("reply") - .icon(IconName::Reply) - .tooltip(t!("chat.reply_button")) - .small() - .ghost() - .on_click({ - let id = id.to_owned(); - cx.listener(move |this, _event, _window, cx| { - this.reply_to(&id, cx); - }) - }) - .into_any_element(); - - let copy = Button::new("copy") - .icon(IconName::Copy) - .tooltip(t!("chat.copy_message_button")) - .small() - .ghost() - .on_click({ - let id = id.to_owned(); - cx.listener(move |this, _event, _window, cx| { - this.copy_message(&id, cx); - }) - }) - .into_any_element(); - - let more = Button::new("seen-on") - .icon(IconName::Ellipsis) - .small() - .ghost() - .popup_menu({ - let id = id.to_owned(); - move |this, _window, _cx| { - // TODO: add more actions - this.menu(t!("common.seen_on"), Box::new(SeenOn(id))) - } - }) - .into_any_element(); - h_flex() .p_0p5() .gap_1() @@ -1082,7 +1043,45 @@ impl Chat { .border_1() .border_color(cx.theme().border) .bg(cx.theme().background) - .children(vec![reply, copy, more]) + .child( + Button::new("reply") + .icon(IconName::Reply) + .tooltip(t!("chat.reply_button")) + .small() + .ghost() + .on_click({ + let id = id.to_owned(); + cx.listener(move |this, _event, _window, cx| { + this.reply_to(&id, cx); + }) + }), + ) + .child( + Button::new("copy") + .icon(IconName::Copy) + .tooltip(t!("chat.copy_message_button")) + .small() + .ghost() + .on_click({ + let id = id.to_owned(); + cx.listener(move |this, _event, _window, cx| { + this.copy_message(&id, cx); + }) + }), + ) + .child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border)) + .child( + Button::new("seen-on") + .icon(IconName::Ellipsis) + .small() + .ghost() + .popup_menu({ + let id = id.to_owned(); + move |this, _window, _cx| { + this.menu(t!("common.seen_on"), Box::new(SeenOn(id))) + } + }), + ) .group_hover("", |this| this.visible()) } diff --git a/crates/coop/src/views/compose.rs b/crates/coop/src/views/compose.rs index 39c94ef..111c9a6 100644 --- a/crates/coop/src/views/compose.rs +++ b/crates/coop/src/views/compose.rs @@ -22,7 +22,7 @@ use smallvec::{smallvec, SmallVec}; use smol::Timer; use theme::ActiveTheme; use ui::avatar::Avatar; -use ui::button::{Button, ButtonRounded, ButtonVariants}; +use ui::button::{Button, ButtonVariants}; use ui::input::{InputEvent, InputState, TextInput}; use ui::notification::Notification; use ui::{h_flex, v_flex, ContextModal, Disableable, Icon, IconName, Sizable, StyledExt}; @@ -34,7 +34,7 @@ pub fn compose_button() -> impl IntoElement { .ghost_alt() .cta() .small() - .rounded(ButtonRounded::Full) + .rounded() .on_click(move |_, window, cx| { let compose = cx.new(|cx| Compose::new(window, cx)); let title = SharedString::new(t!("sidebar.direct_messages")); diff --git a/crates/coop/src/views/new_account.rs b/crates/coop/src/views/new_account.rs index 4ebd37c..79c4dff 100644 --- a/crates/coop/src/views/new_account.rs +++ b/crates/coop/src/views/new_account.rs @@ -14,7 +14,7 @@ use settings::AppSettings; use smol::fs; use theme::ActiveTheme; use ui::avatar::Avatar; -use ui::button::{Button, ButtonRounded, ButtonVariants}; +use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::input::{InputState, TextInput}; use ui::modal::ModalButtonProps; @@ -352,7 +352,7 @@ impl Render for NewAccount { .label(t!("common.upload")) .ghost() .small() - .rounded(ButtonRounded::Full) + .rounded() .disabled(self.submitting || self.uploading) .loading(self.uploading) .on_click(cx.listener(move |this, _, window, cx| { diff --git a/crates/coop/src/views/preferences.rs b/crates/coop/src/views/preferences.rs index da842d9..5638e14 100644 --- a/crates/coop/src/views/preferences.rs +++ b/crates/coop/src/views/preferences.rs @@ -10,7 +10,7 @@ use registry::Registry; use settings::AppSettings; use theme::ActiveTheme; use ui::avatar::Avatar; -use ui::button::{Button, ButtonRounded, ButtonVariants}; +use ui::button::{Button, ButtonVariants}; use ui::input::{InputState, TextInput}; use ui::modal::ModalButtonProps; use ui::switch::Switch; @@ -169,7 +169,7 @@ impl Render for Preferences { .label("Messaging Relays") .xsmall() .ghost_alt() - .rounded(ButtonRounded::Full) + .rounded() .on_click(cx.listener(move |this, _e, window, cx| { this.open_relays(window, cx); })), diff --git a/crates/coop/src/views/screening.rs b/crates/coop/src/views/screening.rs index 34c739b..42bb685 100644 --- a/crates/coop/src/views/screening.rs +++ b/crates/coop/src/views/screening.rs @@ -17,7 +17,7 @@ use settings::AppSettings; use smallvec::{smallvec, SmallVec}; use theme::ActiveTheme; use ui::avatar::Avatar; -use ui::button::{Button, ButtonRounded, ButtonVariants}; +use ui::button::{Button, ButtonVariants}; use ui::indicator::Indicator; use ui::{h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt}; @@ -268,7 +268,7 @@ impl Render for Screening { .label(t!("profile.njump")) .secondary() .small() - .rounded(ButtonRounded::Full) + .rounded() .on_click(cx.listener(move |this, _e, window, cx| { this.open_njump(window, cx); })), @@ -278,7 +278,7 @@ impl Render for Screening { .tooltip(t!("screening.report")) .icon(IconName::Report) .danger() - .rounded(ButtonRounded::Full) + .rounded() .on_click(cx.listener(move |this, _e, window, cx| { this.report(window, cx); })), @@ -330,7 +330,7 @@ impl Render for Screening { .icon(IconName::Info) .xsmall() .ghost() - .rounded(ButtonRounded::Full) + .rounded() .tooltip(t!("screening.active_tooltip")), ), ) @@ -402,7 +402,7 @@ impl Render for Screening { .icon(IconName::Info) .xsmall() .ghost() - .rounded(ButtonRounded::Full) + .rounded() .on_click(cx.listener( move |this, _, window, cx| { this.mutual_contacts(window, cx); diff --git a/crates/coop/src/views/setup_relay.rs b/crates/coop/src/views/setup_relay.rs index b8ccb15..275087a 100644 --- a/crates/coop/src/views/setup_relay.rs +++ b/crates/coop/src/views/setup_relay.rs @@ -14,7 +14,7 @@ use nostr_sdk::prelude::*; use registry::Registry; use smallvec::{smallvec, SmallVec}; use theme::ActiveTheme; -use ui::button::{Button, ButtonRounded, ButtonVariants}; +use ui::button::{Button, ButtonVariants}; use ui::input::{InputEvent, InputState, TextInput}; use ui::modal::ModalButtonProps; use ui::{h_flex, v_flex, ContextModal, IconName, Sizable, StyledExt}; @@ -33,7 +33,7 @@ where .label(label) .warning() .xsmall() - .rounded(ButtonRounded::Full) + .rounded() .on_click(move |_, window, cx| { let view = cx.new(|cx| SetupRelay::new(Kind::InboxRelays, window, cx)); let weak_view = view.downgrade(); diff --git a/crates/coop/src/views/sidebar/mod.rs b/crates/coop/src/views/sidebar/mod.rs index d170e10..f61175d 100644 --- a/crates/coop/src/views/sidebar/mod.rs +++ b/crates/coop/src/views/sidebar/mod.rs @@ -9,9 +9,9 @@ use global::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS}; use global::{css, nostr_client, UnwrappingStatus}; use gpui::prelude::FluentBuilder; use gpui::{ - div, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, - Focusable, InteractiveElement, IntoElement, ParentElement, Render, RetainAllImageCache, - SharedString, Styled, Subscription, Task, Window, + div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter, + FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, + RetainAllImageCache, SharedString, Styled, Subscription, Task, Window, }; use gpui_tokio::Tokio; use i18n::{shared_t, t}; @@ -23,7 +23,7 @@ use registry::{Registry, RegistryEvent}; use settings::AppSettings; use smallvec::{smallvec, SmallVec}; use theme::ActiveTheme; -use ui::button::{Button, ButtonRounded, ButtonVariants}; +use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::input::{InputEvent, InputState, TextInput}; use ui::popup_menu::{PopupMenu, PopupMenuExt}; @@ -669,6 +669,7 @@ impl Focusable for Sidebar { impl Render for Sidebar { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let registry = Registry::read_global(cx); + let loading = registry.unwrapping_status.read(cx) != &UnwrappingStatus::Complete; // Get rooms from either search results or the chat registry let rooms = if let Some(results) = self.local_result.read(cx).as_ref() { @@ -688,7 +689,7 @@ impl Render for Sidebar { let mut total_rooms = rooms.len(); // Add 3 dummy rooms to display as skeletons - if registry.unwrapping_status.read(cx) != &UnwrappingStatus::Complete { + if loading { total_rooms += 3 } @@ -752,7 +753,7 @@ impl Render for Sidebar { .cta() .bold() .secondary() - .rounded(ButtonRounded::Full) + .rounded() .selected(self.filter(&RoomKind::Ongoing, cx)) .on_click(cx.listener(|this, _, _, cx| { this.set_filter(RoomKind::Ongoing, cx); @@ -773,7 +774,7 @@ impl Render for Sidebar { .cta() .bold() .secondary() - .rounded(ButtonRounded::Full) + .rounded() .selected(!self.filter(&RoomKind::Ongoing, cx)) .on_click(cx.listener(|this, _, _, cx| { this.set_filter(RoomKind::default(), cx); @@ -791,7 +792,7 @@ impl Render for Sidebar { .icon(IconName::Ellipsis) .xsmall() .ghost() - .rounded(ButtonRounded::Full) + .rounded() .popup_menu(move |this, _window, _cx| { this.menu( t!("sidebar.reload_menu"), @@ -805,6 +806,57 @@ impl Render for Sidebar { ), ), ) + .when(!loading && total_rooms == 0, |this| { + this.map(|this| { + if self.filter(&RoomKind::Ongoing, cx) { + this.child( + v_flex() + .py_2() + .gap_1p5() + .items_center() + .justify_center() + .text_center() + .child( + div() + .text_sm() + .font_semibold() + .line_height(relative(1.25)) + .child(shared_t!("sidebar.no_conversations")), + ) + .child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .line_height(relative(1.25)) + .child(shared_t!("sidebar.no_conversations_label")), + ), + ) + } else { + this.child( + v_flex() + .py_2() + .gap_1p5() + .items_center() + .justify_center() + .text_center() + .child( + div() + .text_sm() + .font_semibold() + .line_height(relative(1.25)) + .child(shared_t!("sidebar.no_requests")), + ) + .child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .line_height(relative(1.25)) + .child(shared_t!("sidebar.no_requests_label")), + ), + ) + } + }) + }) .child( uniform_list( "rooms", diff --git a/crates/coop/src/views/welcome.rs b/crates/coop/src/views/welcome.rs index 7b8f5d3..5731326 100644 --- a/crates/coop/src/views/welcome.rs +++ b/crates/coop/src/views/welcome.rs @@ -1,12 +1,13 @@ use gpui::{ div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, - IntoElement, ParentElement, Render, SharedString, Styled, Window, + InteractiveElement, IntoElement, ParentElement, Render, SharedString, + StatefulInteractiveElement, Styled, Window, }; use theme::ActiveTheme; use ui::button::Button; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::popup_menu::PopupMenu; -use ui::StyledExt; +use ui::{v_flex, StyledExt}; pub fn init(window: &mut Window, cx: &mut App) -> Entity { Welcome::new(window, cx) @@ -14,8 +15,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity { pub struct Welcome { name: SharedString, - closable: bool, - zoomable: bool, + version: SharedString, focus_handle: FocusHandle, } @@ -25,10 +25,11 @@ impl Welcome { } fn view(_window: &mut Window, cx: &mut Context) -> Self { + let version = SharedString::from(format!("Version: {}", env!("CARGO_PKG_VERSION"))); + Self { + version, name: "Welcome".into(), - closable: true, - zoomable: true, focus_handle: cx.focus_handle(), } } @@ -39,16 +40,15 @@ impl Panel for Welcome { self.name.clone() } - fn title(&self, _cx: &App) -> AnyElement { - "👋".into_any_element() - } - - fn closable(&self, _cx: &App) -> bool { - self.closable - } - - fn zoomable(&self, _cx: &App) -> bool { - self.zoomable + fn title(&self, cx: &App) -> AnyElement { + div() + .child( + svg() + .path("brand/coop.svg") + .size_4() + .text_color(cx.theme().element_background), + ) + .into_any_element() } fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu { @@ -76,11 +76,10 @@ impl Render for Welcome { .items_center() .justify_center() .child( - div() - .flex() - .flex_col() + v_flex() + .gap_2() .items_center() - .gap_1() + .justify_center() .child( svg() .path("brand/coop.svg") @@ -88,11 +87,26 @@ impl Render for Welcome { .text_color(cx.theme().elevated_surface_background), ) .child( - div() - .child("coop on nostr") - .text_color(cx.theme().text_placeholder) - .font_semibold() - .text_sm(), + v_flex() + .items_center() + .justify_center() + .text_center() + .child( + div() + .font_semibold() + .text_color(cx.theme().text_muted) + .child("coop on nostr"), + ) + .child( + div() + .id("version") + .text_color(cx.theme().text_placeholder) + .text_xs() + .child(self.version.clone()) + .on_click(|_, _window, cx| { + cx.open_url("https://github.com/lumehq/coop/releases"); + }), + ), ), ) } diff --git a/crates/title_bar/src/platforms/mac.rs b/crates/title_bar/src/platforms/mac.rs index c7becde..526e245 100644 --- a/crates/title_bar/src/platforms/mac.rs +++ b/crates/title_bar/src/platforms/mac.rs @@ -3,4 +3,4 @@ /// /// Magic number: There is one extra pixel of padding on the left side due to /// the 1px border around the window on macOS apps. -pub const TRAFFIC_LIGHT_PADDING: f32 = 71.; +pub const TRAFFIC_LIGHT_PADDING: f32 = 80.; diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index 3b73e84..f629847 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -10,11 +10,6 @@ use crate::indicator::Indicator; use crate::tooltip::Tooltip; use crate::{h_flex, Disableable, Icon, Selectable, Sizable, Size, StyledExt}; -pub enum ButtonRounded { - Normal, - Full, -} - #[derive(Clone, Copy, PartialEq, Eq)] pub struct ButtonCustomVariant { color: Hsla, @@ -130,7 +125,7 @@ pub struct Button { children: Vec, variant: ButtonVariant, - rounded: ButtonRounded, + rounded: bool, size: Size, disabled: bool, @@ -163,7 +158,7 @@ impl Button { disabled: false, selected: false, variant: ButtonVariant::default(), - rounded: ButtonRounded::Normal, + rounded: false, size: Size::Medium, tooltip: None, on_click: None, @@ -177,9 +172,9 @@ impl Button { } } - /// Set the border radius of the Button. - pub fn rounded(mut self, rounded: impl Into) -> Self { - self.rounded = rounded.into(); + /// Make the button rounded. + pub fn rounded(mut self) -> Self { + self.rounded = true; self } @@ -315,8 +310,8 @@ impl RenderOnce for Button { .cursor_default() .overflow_hidden() .map(|this| match self.rounded { - ButtonRounded::Normal => this.rounded(cx.theme().radius), - ButtonRounded::Full => this.rounded_full(), + false => this.rounded(cx.theme().radius), + true => this.rounded_full(), }) .map(|this| { if self.label.is_none() && self.children.is_empty() { diff --git a/crates/ui/src/dock_area/tab_panel.rs b/crates/ui/src/dock_area/tab_panel.rs index d301751..ccd707f 100644 --- a/crates/ui/src/dock_area/tab_panel.rs +++ b/crates/ui/src/dock_area/tab_panel.rs @@ -412,16 +412,15 @@ impl TabPanel { let is_zoomed = self.is_zoomed && state.zoomable; let view = cx.entity().clone(); let build_popup_menu = move |this, cx: &App| view.read(cx).popup_menu(this, cx); + let toolbar = self.toolbar_buttons(window, cx); + let has_toolbar = !toolbar.is_empty(); h_flex() + .p_0p5() .gap_1() .occlude() - .items_center() - .children( - self.toolbar_buttons(window, cx) - .into_iter() - .map(|btn| btn.small().ghost()), - ) + .rounded_full() + .children(toolbar.into_iter().map(|btn| btn.small().ghost().rounded())) .when(self.is_zoomed, |this| { this.child( Button::new("zoom") @@ -434,11 +433,16 @@ impl TabPanel { })), ) }) + .when(has_toolbar, |this| { + this.bg(cx.theme().surface_background) + .child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border)) + }) .child( Button::new("menu") .icon(IconName::Ellipsis) .small() .ghost() + .rounded() .popup_menu({ let zoomable = state.zoomable; let closable = state.closable; @@ -647,7 +651,7 @@ impl TabPanel { .child( div() .size_full() - .rounded_lg() + .rounded_xl() .shadow_sm() .when(cx.theme().mode.is_dark(), |this| this.shadow_lg()) .bg(cx.theme().panel_background) @@ -667,7 +671,7 @@ impl TabPanel { .p_1() .child( div() - .rounded_lg() + .rounded_xl() .border_1() .border_color(cx.theme().element_disabled) .bg(cx.theme().drop_target_background) diff --git a/crates/ui/src/resizable/panel.rs b/crates/ui/src/resizable/panel.rs index 33d68f0..24e8a03 100644 --- a/crates/ui/src/resizable/panel.rs +++ b/crates/ui/src/resizable/panel.rs @@ -405,13 +405,14 @@ impl Render for ResizablePanel { return div(); } - let view = cx.entity().clone(); let total_size = self .group .as_ref() .and_then(|group| group.upgrade()) .map(|group| group.read(cx).total_size()); + let view = cx.entity(); + div() .flex() .flex_grow() diff --git a/locales/app.yml b/locales/app.yml index eb66b2e..1d1c791 100644 --- a/locales/app.yml +++ b/locales/app.yml @@ -401,6 +401,14 @@ sidebar: en: "Incoming new conversations" trusted_contacts_tooltip: en: "Only show rooms from trusted contacts" + no_requests: + en: "No message requests" + no_requests_label: + en: "New message requests from people you don't know will appear here." + no_conversations: + en: "No conversations" + no_conversations_label: + en: "Start a conversation with someone to get started." loading: label: