From 17f92d767e2092220b5dc87341765d19e95163a5 Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Sat, 9 Aug 2025 14:58:01 +0700 Subject: [PATCH] chore: improve ui consistency (#115) * . * . --- crates/coop/src/chatspace.rs | 28 ++- crates/coop/src/views/chat/mod.rs | 10 +- crates/coop/src/views/sidebar/list_item.rs | 116 +++++++--- crates/coop/src/views/sidebar/mod.rs | 249 ++++++--------------- crates/global/src/constants.rs | 2 +- locales/app.yml | 20 +- 6 files changed, 184 insertions(+), 241 deletions(-) diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index 3901c0a..c0889b8 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -27,8 +27,10 @@ use ui::button::{Button, ButtonVariants}; use ui::dock_area::dock::DockPlacement; use ui::dock_area::panel::PanelView; use ui::dock_area::{ClosePanel, DockArea, DockItem}; +use ui::indicator::Indicator; use ui::modal::ModalButtonProps; use ui::popup_menu::PopupMenuExt; +use ui::tooltip::Tooltip; use ui::{h_flex, ContextModal, IconName, Root, Sizable, StyledExt}; use crate::views::compose::compose_button; @@ -327,11 +329,31 @@ impl ChatSpace { fn render_titlebar_left_side( &mut self, _window: &mut Window, - _cx: &Context, + cx: &Context, ) -> impl IntoElement { - let compose_button = compose_button().into_any_element(); + let registry = Registry::read_global(cx); + let loading = registry.loading; - h_flex().gap_1().child(compose_button) + h_flex() + .gap_2() + .child(compose_button()) + .when(loading, |this| { + this.child( + h_flex() + .id("downloading") + .px_4() + .h_6() + .gap_1() + .text_xs() + .rounded_full() + .bg(cx.theme().elevated_surface_background) + .child(shared_t!("loading.label")) + .child(Indicator::new().xsmall()) + .tooltip(|window, cx| { + Tooltip::new(t!("loading.tooltip"), window, cx).into() + }), + ) + }) } fn render_titlebar_right_side( diff --git a/crates/coop/src/views/chat/mod.rs b/crates/coop/src/views/chat/mod.rs index 3a8c1b3..efdd99d 100644 --- a/crates/coop/src/views/chat/mod.rs +++ b/crates/coop/src/views/chat/mod.rs @@ -9,10 +9,10 @@ use global::nostr_client; use gpui::prelude::FluentBuilder; use gpui::{ div, img, list, px, red, rems, white, Action, AnyElement, App, AppContext, ClipboardItem, - Context, Element, Empty, Entity, EventEmitter, Flatten, FocusHandle, Focusable, - InteractiveElement, IntoElement, ListAlignment, ListState, MouseButton, ObjectFit, - ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString, - StatefulInteractiveElement, Styled, StyledImage, Subscription, Window, + Context, Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement, + IntoElement, ListAlignment, ListState, MouseButton, ObjectFit, ParentElement, + PathPromptOptions, Render, RetainAllImageCache, SharedString, StatefulInteractiveElement, + Styled, StyledImage, Subscription, Window, }; use gpui_tokio::Tokio; use i18n::t; @@ -712,7 +712,7 @@ impl Chat { window.open_modal(cx, move |this, _window, cx| { this.title(SharedString::new(t!("chat.logs_title"))).child( div() - .w_full() + .pb_4() .flex() .flex_col() .gap_2() diff --git a/crates/coop/src/views/sidebar/list_item.rs b/crates/coop/src/views/sidebar/list_item.rs index 00ad558..a21a3f3 100644 --- a/crates/coop/src/views/sidebar/list_item.rs +++ b/crates/coop/src/views/sidebar/list_item.rs @@ -15,6 +15,7 @@ use ui::actions::OpenProfile; use ui::avatar::Avatar; use ui::context_menu::ContextMenuExt; use ui::modal::ModalButtonProps; +use ui::skeleton::Skeleton; use ui::{h_flex, ContextModal, StyledExt}; use crate::views::screening; @@ -23,60 +24,109 @@ use crate::views::screening; pub struct RoomListItem { ix: usize, base: Div, - room_id: u64, - public_key: PublicKey, - name: SharedString, - avatar: SharedString, - created_at: SharedString, - kind: RoomKind, + room_id: Option, + public_key: Option, + name: Option, + avatar: Option, + created_at: Option, + kind: Option, #[allow(clippy::type_complexity)] - handler: Rc, + handler: Option>, } impl RoomListItem { - pub fn new( - ix: usize, - room_id: u64, - public_key: PublicKey, - name: SharedString, - avatar: SharedString, - created_at: SharedString, - kind: RoomKind, - ) -> Self { + pub fn new(ix: usize) -> Self { Self { ix, - public_key, - room_id, - name, - avatar, - created_at, - kind, - base: h_flex().h_9().w_full().px_1p5(), - handler: Rc::new(|_, _, _| {}), + base: h_flex().h_9().w_full().px_1p5().gap_2(), + room_id: None, + public_key: None, + name: None, + avatar: None, + created_at: None, + kind: None, + handler: None, } } + pub fn room_id(mut self, room_id: u64) -> Self { + self.room_id = Some(room_id); + self + } + + pub fn public_key(mut self, public_key: PublicKey) -> Self { + self.public_key = Some(public_key); + self + } + + pub fn name(mut self, name: SharedString) -> Self { + self.name = Some(name); + self + } + + pub fn avatar(mut self, avatar: SharedString) -> Self { + self.avatar = Some(avatar); + self + } + + pub fn created_at(mut self, created_at: SharedString) -> Self { + self.created_at = Some(created_at); + self + } + + pub fn kind(mut self, kind: RoomKind) -> Self { + self.kind = Some(kind); + self + } + pub fn on_click( mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, ) -> Self { - self.handler = Rc::new(handler); + self.handler = Some(Rc::new(handler)); self } } impl RenderOnce for RoomListItem { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let public_key = self.public_key; - let room_id = self.room_id; - let kind = self.kind; - let handler = self.handler.clone(); let hide_avatar = AppSettings::get_hide_user_avatars(cx); let require_screening = AppSettings::get_screening(cx); + let ( + Some(public_key), + Some(room_id), + Some(name), + Some(avatar), + Some(created_at), + Some(kind), + Some(handler), + ) = ( + self.public_key, + self.room_id, + self.name, + self.avatar, + self.created_at, + self.kind, + self.handler, + ) + else { + return self + .base + .id(self.ix) + .child(Skeleton::new().flex_shrink_0().size_6().rounded_full()) + .child( + div() + .flex_1() + .flex() + .justify_between() + .child(Skeleton::new().w_32().h_2p5().rounded_sm()) + .child(Skeleton::new().w_6().h_2p5().rounded_sm()), + ); + }; + self.base .id(self.ix) - .gap_2() .text_sm() .rounded(cx.theme().radius) .when(!hide_avatar, |this| { @@ -86,7 +136,7 @@ impl RenderOnce for RoomListItem { .size_6() .rounded_full() .overflow_hidden() - .child(Avatar::new(self.avatar).size(rems(1.5))), + .child(Avatar::new(avatar).size(rems(1.5))), ) }) .child( @@ -102,14 +152,14 @@ impl RenderOnce for RoomListItem { .text_ellipsis() .truncate() .font_medium() - .child(self.name), + .child(name), ) .child( div() .flex_shrink_0() .text_xs() .text_color(cx.theme().text_placeholder) - .child(self.created_at), + .child(created_at), ), ) .context_menu(move |this, _window, _cx| { diff --git a/crates/coop/src/views/sidebar/mod.rs b/crates/coop/src/views/sidebar/mod.rs index 4c42d8d..dbd995b 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::nostr_client; use gpui::prelude::FluentBuilder; use gpui::{ - div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter, - FocusHandle, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, - Styled, Subscription, Task, Window, + div, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, + Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, Styled, + Subscription, Task, Window, }; use gpui_tokio::Tokio; use i18n::t; @@ -26,16 +26,15 @@ use smallvec::{smallvec, SmallVec}; use theme::ActiveTheme; use ui::button::{Button, ButtonRounded, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; -use ui::indicator::Indicator; use ui::input::{InputEvent, InputState, TextInput}; use ui::popup_menu::PopupMenu; -use ui::skeleton::Skeleton; use ui::{v_flex, ContextModal, IconName, Selectable, Sizable, StyledExt}; mod list_item; const FIND_DELAY: u64 = 600; const FIND_LIMIT: usize = 10; +const TOTAL_SKELETONS: usize = 3; pub fn init(window: &mut Window, cx: &mut App) -> Entity { Sidebar::new(window, cx) @@ -547,61 +546,6 @@ impl Sidebar { }); } - fn open_loading_modal(&self, window: &mut Window, cx: &mut Context) { - let title = SharedString::new(t!("sidebar.loading_modal_title")); - let text_1 = SharedString::new(t!("sidebar.loading_modal_body_1")); - let text_2 = SharedString::new(t!("sidebar.loading_modal_body_2")); - let desc = SharedString::new(t!("sidebar.loading_modal_description")); - - window.open_modal(cx, move |this, _window, cx| { - this.show_close(true) - .keyboard(true) - .title(title.clone()) - .child( - v_flex() - .pb_4() - .gap_2() - .child( - div() - .flex() - .flex_col() - .gap_2() - .text_sm() - .child(text_1.clone()) - .child(text_2.clone()), - ) - .child( - div() - .text_xs() - .text_color(cx.theme().text_muted) - .child(desc.clone()), - ), - ) - }); - } - - #[allow(dead_code)] - fn skeletons(&self, total: i32) -> impl IntoIterator { - (0..total).map(|_| { - div() - .h_9() - .w_full() - .px_1p5() - .flex() - .items_center() - .gap_2() - .child(Skeleton::new().flex_shrink_0().size_6().rounded_full()) - .child( - div() - .flex_1() - .flex() - .justify_between() - .child(Skeleton::new().w_32().h_2p5().rounded_sm()) - .child(Skeleton::new().w_6().h_2p5().rounded_sm()), - ) - }) - } - fn list_items( &self, rooms: &[Entity], @@ -622,17 +566,17 @@ impl Sidebar { }); items.push( - RoomListItem::new( - ix, - room_id, - this.members[0], - this.display_name(cx), - this.display_image(proxy, cx), - this.ago(), - this.kind, - ) - .on_click(handler), + RoomListItem::new(ix) + .room_id(room_id) + .name(this.display_name(cx)) + .avatar(this.display_image(proxy, cx)) + .created_at(this.ago()) + .public_key(this.members[0]) + .kind(this.kind) + .on_click(handler), ) + } else { + items.push(RoomListItem::new(ix)); } } @@ -669,6 +613,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.loading; // Get rooms from either search results or the chat registry let rooms = if let Some(results) = self.local_result.read(cx).as_ref() { @@ -684,6 +629,15 @@ impl Render for Sidebar { } }; + // Get total rooms count + let mut total_rooms = rooms.len(); + + // If loading in progress + // Add 3 skeletons to the room list + if loading { + total_rooms += TOTAL_SKELETONS; + } + v_flex() .image_cache(self.image_cache.clone()) .size_full() @@ -723,76 +677,55 @@ impl Render for Sidebar { .overflow_y_hidden() .child( div() - .flex_none() .px_1() - .w_full() - .h_9() - .flex() - .items_center() - .justify_between() + .h_flex() + .gap_2() + .flex_none() .child( - div() - .flex() - .items_center() - .gap_2() - .child( - Button::new("all") - .label(t!("sidebar.all_button")) - .tooltip(t!("sidebar.all_conversations_tooltip")) - .when_some( - self.indicator.read(cx).as_ref(), - |this, kind| { - this.when(kind == &RoomKind::Ongoing, |this| { - this.child( - div() - .size_1() - .rounded_full() - .bg(cx.theme().cursor), - ) - }) - }, + Button::new("all") + .label(t!("sidebar.all_button")) + .tooltip(t!("sidebar.all_conversations_tooltip")) + .when_some(self.indicator.read(cx).as_ref(), |this, kind| { + this.when(kind == &RoomKind::Ongoing, |this| { + this.child( + div().size_1().rounded_full().bg(cx.theme().cursor), ) - .small() - .bold() - .secondary() - .rounded(ButtonRounded::Full) - .selected(self.filter(&RoomKind::Ongoing, cx)) - .on_click(cx.listener(|this, _, _, cx| { - this.set_filter(RoomKind::Ongoing, cx); - })), - ) - .child( - Button::new("requests") - .label(t!("sidebar.requests_button")) - .tooltip(t!("sidebar.requests_tooltip")) - .when_some( - self.indicator.read(cx).as_ref(), - |this, kind| { - this.when(kind != &RoomKind::Ongoing, |this| { - this.child( - div() - .size_1() - .rounded_full() - .bg(cx.theme().cursor), - ) - }) - }, + }) + }) + .small() + .bold() + .secondary() + .rounded(ButtonRounded::Full) + .selected(self.filter(&RoomKind::Ongoing, cx)) + .on_click(cx.listener(|this, _, _, cx| { + this.set_filter(RoomKind::Ongoing, cx); + })), + ) + .child( + Button::new("requests") + .label(t!("sidebar.requests_button")) + .tooltip(t!("sidebar.requests_tooltip")) + .when_some(self.indicator.read(cx).as_ref(), |this, kind| { + this.when(kind != &RoomKind::Ongoing, |this| { + this.child( + div().size_1().rounded_full().bg(cx.theme().cursor), ) - .small() - .bold() - .secondary() - .rounded(ButtonRounded::Full) - .selected(!self.filter(&RoomKind::Ongoing, cx)) - .on_click(cx.listener(|this, _, _, cx| { - this.set_filter(RoomKind::default(), cx); - })), - ), + }) + }) + .small() + .bold() + .secondary() + .rounded(ButtonRounded::Full) + .selected(!self.filter(&RoomKind::Ongoing, cx)) + .on_click(cx.listener(|this, _, _, cx| { + this.set_filter(RoomKind::default(), cx); + })), ), ) .child( uniform_list( "rooms", - rooms.len(), + total_rooms, cx.processor(move |this, range, _window, cx| { this.list_items(&rooms, range, cx) }), @@ -800,59 +733,5 @@ impl Render for Sidebar { .h_full(), ), ) - .when(registry.loading, |this| { - let title = SharedString::new(t!("sidebar.retrieving_messages")); - let desc = SharedString::new(t!("sidebar.retrieving_messages_description")); - - this.child( - div().absolute().bottom_3().px_3().w_full().child( - div() - .p_1() - .w_full() - .rounded_full() - .flex() - .items_center() - .justify_between() - .bg(cx.theme().panel_background) - .shadow_sm() - // Loading - .child(div().flex_shrink_0().pl_1().child(Indicator::new().small())) - // Title - .child( - v_flex() - .flex_1() - .items_center() - .justify_center() - .text_center() - .child( - div() - .text_sm() - .font_semibold() - .line_height(relative(1.2)) - .child(title.clone()), - ) - .child( - div() - .text_xs() - .text_color(cx.theme().text_muted) - .child(desc.clone()), - ), - ) - // Info button - .child( - Button::new("help") - .icon(IconName::Info) - .tooltip(t!("sidebar.why_seeing_this_tooltip")) - .small() - .ghost() - .rounded(ButtonRounded::Full) - .flex_shrink_0() - .on_click(cx.listener(move |this, _, window, cx| { - this.open_loading_modal(window, cx) - })), - ), - ), - ) - }) } } diff --git a/crates/global/src/constants.rs b/crates/global/src/constants.rs index ec2103a..ac0db9c 100644 --- a/crates/global/src/constants.rs +++ b/crates/global/src/constants.rs @@ -48,7 +48,7 @@ pub const WAIT_FOR_FINISH: u64 = 60; pub const DEFAULT_MODAL_WIDTH: f32 = 420.; /// Default width of the sidebar. -pub const DEFAULT_SIDEBAR_WIDTH: f32 = 280.; +pub const DEFAULT_SIDEBAR_WIDTH: f32 = 240.; /// Image Resize Service pub const IMAGE_RESIZE_SERVICE: &str = "https://wsrv.nl"; diff --git a/locales/app.yml b/locales/app.yml index 8d77a6b..0e99f85 100644 --- a/locales/app.yml +++ b/locales/app.yml @@ -335,17 +335,9 @@ sidebar: en: "Incoming new conversations" trusted_contacts_tooltip: en: "Only show rooms from trusted contacts" - retrieving_messages: - en: "Retrieving messages" - retrieving_messages_description: - en: "This may take some time" - why_seeing_this_tooltip: - en: "Why you're seeing this" - loading_modal_title: - en: "Retrieving Your Messages" - loading_modal_body_1: - en: "Coop is downloading all your messages from the messaging relays. Depending on your total number of messages, this process may take up to 15 minutes if you're using Nostr Connect." - loading_modal_body_2: - en: "Please be patient - you only need to do this full download once. Next time, Coop will only download new messages." - loading_modal_description: - en: "You still can use the app normally while messages are processing in the background" + +loading: + label: + en: "Downloading messages" + tooltip: + en: "This may take a while. Please be patient."