chore: improve ui consistency (#115)

* .

* .
This commit is contained in:
reya
2025-08-09 14:58:01 +07:00
committed by GitHub
parent be660cb14b
commit 17f92d767e
6 changed files with 184 additions and 241 deletions

View File

@@ -27,8 +27,10 @@ use ui::button::{Button, ButtonVariants};
use ui::dock_area::dock::DockPlacement; use ui::dock_area::dock::DockPlacement;
use ui::dock_area::panel::PanelView; use ui::dock_area::panel::PanelView;
use ui::dock_area::{ClosePanel, DockArea, DockItem}; use ui::dock_area::{ClosePanel, DockArea, DockItem};
use ui::indicator::Indicator;
use ui::modal::ModalButtonProps; use ui::modal::ModalButtonProps;
use ui::popup_menu::PopupMenuExt; use ui::popup_menu::PopupMenuExt;
use ui::tooltip::Tooltip;
use ui::{h_flex, ContextModal, IconName, Root, Sizable, StyledExt}; use ui::{h_flex, ContextModal, IconName, Root, Sizable, StyledExt};
use crate::views::compose::compose_button; use crate::views::compose::compose_button;
@@ -327,11 +329,31 @@ impl ChatSpace {
fn render_titlebar_left_side( fn render_titlebar_left_side(
&mut self, &mut self,
_window: &mut Window, _window: &mut Window,
_cx: &Context<Self>, cx: &Context<Self>,
) -> impl IntoElement { ) -> 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( fn render_titlebar_right_side(

View File

@@ -9,10 +9,10 @@ use global::nostr_client;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, img, list, px, red, rems, white, Action, AnyElement, App, AppContext, ClipboardItem, div, img, list, px, red, rems, white, Action, AnyElement, App, AppContext, ClipboardItem,
Context, Element, Empty, Entity, EventEmitter, Flatten, FocusHandle, Focusable, Context, Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement,
InteractiveElement, IntoElement, ListAlignment, ListState, MouseButton, ObjectFit, IntoElement, ListAlignment, ListState, MouseButton, ObjectFit, ParentElement,
ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString, PathPromptOptions, Render, RetainAllImageCache, SharedString, StatefulInteractiveElement,
StatefulInteractiveElement, Styled, StyledImage, Subscription, Window, Styled, StyledImage, Subscription, Window,
}; };
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use i18n::t; use i18n::t;
@@ -712,7 +712,7 @@ impl Chat {
window.open_modal(cx, move |this, _window, cx| { window.open_modal(cx, move |this, _window, cx| {
this.title(SharedString::new(t!("chat.logs_title"))).child( this.title(SharedString::new(t!("chat.logs_title"))).child(
div() div()
.w_full() .pb_4()
.flex() .flex()
.flex_col() .flex_col()
.gap_2() .gap_2()

View File

@@ -15,6 +15,7 @@ use ui::actions::OpenProfile;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::context_menu::ContextMenuExt; use ui::context_menu::ContextMenuExt;
use ui::modal::ModalButtonProps; use ui::modal::ModalButtonProps;
use ui::skeleton::Skeleton;
use ui::{h_flex, ContextModal, StyledExt}; use ui::{h_flex, ContextModal, StyledExt};
use crate::views::screening; use crate::views::screening;
@@ -23,60 +24,109 @@ use crate::views::screening;
pub struct RoomListItem { pub struct RoomListItem {
ix: usize, ix: usize,
base: Div, base: Div,
room_id: u64, room_id: Option<u64>,
public_key: PublicKey, public_key: Option<PublicKey>,
name: SharedString, name: Option<SharedString>,
avatar: SharedString, avatar: Option<SharedString>,
created_at: SharedString, created_at: Option<SharedString>,
kind: RoomKind, kind: Option<RoomKind>,
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
handler: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>, handler: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
} }
impl RoomListItem { impl RoomListItem {
pub fn new( pub fn new(ix: usize) -> Self {
ix: usize,
room_id: u64,
public_key: PublicKey,
name: SharedString,
avatar: SharedString,
created_at: SharedString,
kind: RoomKind,
) -> Self {
Self { Self {
ix, ix,
public_key, base: h_flex().h_9().w_full().px_1p5().gap_2(),
room_id, room_id: None,
name, public_key: None,
avatar, name: None,
created_at, avatar: None,
kind, created_at: None,
base: h_flex().h_9().w_full().px_1p5(), kind: None,
handler: Rc::new(|_, _, _| {}), 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( pub fn on_click(
mut self, mut self,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
self.handler = Rc::new(handler); self.handler = Some(Rc::new(handler));
self self
} }
} }
impl RenderOnce for RoomListItem { impl RenderOnce for RoomListItem {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { 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 hide_avatar = AppSettings::get_hide_user_avatars(cx);
let require_screening = AppSettings::get_screening(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 self.base
.id(self.ix) .id(self.ix)
.gap_2()
.text_sm() .text_sm()
.rounded(cx.theme().radius) .rounded(cx.theme().radius)
.when(!hide_avatar, |this| { .when(!hide_avatar, |this| {
@@ -86,7 +136,7 @@ impl RenderOnce for RoomListItem {
.size_6() .size_6()
.rounded_full() .rounded_full()
.overflow_hidden() .overflow_hidden()
.child(Avatar::new(self.avatar).size(rems(1.5))), .child(Avatar::new(avatar).size(rems(1.5))),
) )
}) })
.child( .child(
@@ -102,14 +152,14 @@ impl RenderOnce for RoomListItem {
.text_ellipsis() .text_ellipsis()
.truncate() .truncate()
.font_medium() .font_medium()
.child(self.name), .child(name),
) )
.child( .child(
div() div()
.flex_shrink_0() .flex_shrink_0()
.text_xs() .text_xs()
.text_color(cx.theme().text_placeholder) .text_color(cx.theme().text_placeholder)
.child(self.created_at), .child(created_at),
), ),
) )
.context_menu(move |this, _window, _cx| { .context_menu(move |this, _window, _cx| {

View File

@@ -9,9 +9,9 @@ use global::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS};
use global::nostr_client; use global::nostr_client;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter, div, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
FocusHandle, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, Styled,
Styled, Subscription, Task, Window, Subscription, Task, Window,
}; };
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use i18n::t; use i18n::t;
@@ -26,16 +26,15 @@ use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonRounded, ButtonVariants}; use ui::button::{Button, ButtonRounded, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock_area::panel::{Panel, PanelEvent};
use ui::indicator::Indicator;
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
use ui::popup_menu::PopupMenu; use ui::popup_menu::PopupMenu;
use ui::skeleton::Skeleton;
use ui::{v_flex, ContextModal, IconName, Selectable, Sizable, StyledExt}; use ui::{v_flex, ContextModal, IconName, Selectable, Sizable, StyledExt};
mod list_item; mod list_item;
const FIND_DELAY: u64 = 600; const FIND_DELAY: u64 = 600;
const FIND_LIMIT: usize = 10; const FIND_LIMIT: usize = 10;
const TOTAL_SKELETONS: usize = 3;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
Sidebar::new(window, cx) Sidebar::new(window, cx)
@@ -547,61 +546,6 @@ impl Sidebar {
}); });
} }
fn open_loading_modal(&self, window: &mut Window, cx: &mut Context<Self>) {
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<Item = impl IntoElement> {
(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( fn list_items(
&self, &self,
rooms: &[Entity<Room>], rooms: &[Entity<Room>],
@@ -622,17 +566,17 @@ impl Sidebar {
}); });
items.push( items.push(
RoomListItem::new( RoomListItem::new(ix)
ix, .room_id(room_id)
room_id, .name(this.display_name(cx))
this.members[0], .avatar(this.display_image(proxy, cx))
this.display_name(cx), .created_at(this.ago())
this.display_image(proxy, cx), .public_key(this.members[0])
this.ago(), .kind(this.kind)
this.kind, .on_click(handler),
)
.on_click(handler),
) )
} else {
items.push(RoomListItem::new(ix));
} }
} }
@@ -669,6 +613,7 @@ impl Focusable for Sidebar {
impl Render for Sidebar { impl Render for Sidebar {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let registry = Registry::read_global(cx); let registry = Registry::read_global(cx);
let loading = registry.loading;
// Get rooms from either search results or the chat registry // Get rooms from either search results or the chat registry
let rooms = if let Some(results) = self.local_result.read(cx).as_ref() { 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() v_flex()
.image_cache(self.image_cache.clone()) .image_cache(self.image_cache.clone())
.size_full() .size_full()
@@ -723,76 +677,55 @@ impl Render for Sidebar {
.overflow_y_hidden() .overflow_y_hidden()
.child( .child(
div() div()
.flex_none()
.px_1() .px_1()
.w_full() .h_flex()
.h_9() .gap_2()
.flex() .flex_none()
.items_center()
.justify_between()
.child( .child(
div() Button::new("all")
.flex() .label(t!("sidebar.all_button"))
.items_center() .tooltip(t!("sidebar.all_conversations_tooltip"))
.gap_2() .when_some(self.indicator.read(cx).as_ref(), |this, kind| {
.child( this.when(kind == &RoomKind::Ongoing, |this| {
Button::new("all") this.child(
.label(t!("sidebar.all_button")) div().size_1().rounded_full().bg(cx.theme().cursor),
.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() .small()
.rounded(ButtonRounded::Full) .bold()
.selected(self.filter(&RoomKind::Ongoing, cx)) .secondary()
.on_click(cx.listener(|this, _, _, cx| { .rounded(ButtonRounded::Full)
this.set_filter(RoomKind::Ongoing, cx); .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")) .child(
.tooltip(t!("sidebar.requests_tooltip")) Button::new("requests")
.when_some( .label(t!("sidebar.requests_button"))
self.indicator.read(cx).as_ref(), .tooltip(t!("sidebar.requests_tooltip"))
|this, kind| { .when_some(self.indicator.read(cx).as_ref(), |this, kind| {
this.when(kind != &RoomKind::Ongoing, |this| { this.when(kind != &RoomKind::Ongoing, |this| {
this.child( this.child(
div() div().size_1().rounded_full().bg(cx.theme().cursor),
.size_1()
.rounded_full()
.bg(cx.theme().cursor),
)
})
},
) )
.small() })
.bold() })
.secondary() .small()
.rounded(ButtonRounded::Full) .bold()
.selected(!self.filter(&RoomKind::Ongoing, cx)) .secondary()
.on_click(cx.listener(|this, _, _, cx| { .rounded(ButtonRounded::Full)
this.set_filter(RoomKind::default(), cx); .selected(!self.filter(&RoomKind::Ongoing, cx))
})), .on_click(cx.listener(|this, _, _, cx| {
), this.set_filter(RoomKind::default(), cx);
})),
), ),
) )
.child( .child(
uniform_list( uniform_list(
"rooms", "rooms",
rooms.len(), total_rooms,
cx.processor(move |this, range, _window, cx| { cx.processor(move |this, range, _window, cx| {
this.list_items(&rooms, range, cx) this.list_items(&rooms, range, cx)
}), }),
@@ -800,59 +733,5 @@ impl Render for Sidebar {
.h_full(), .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)
})),
),
),
)
})
} }
} }

View File

@@ -48,7 +48,7 @@ pub const WAIT_FOR_FINISH: u64 = 60;
pub const DEFAULT_MODAL_WIDTH: f32 = 420.; pub const DEFAULT_MODAL_WIDTH: f32 = 420.;
/// Default width of the sidebar. /// Default width of the sidebar.
pub const DEFAULT_SIDEBAR_WIDTH: f32 = 280.; pub const DEFAULT_SIDEBAR_WIDTH: f32 = 240.;
/// Image Resize Service /// Image Resize Service
pub const IMAGE_RESIZE_SERVICE: &str = "https://wsrv.nl"; pub const IMAGE_RESIZE_SERVICE: &str = "https://wsrv.nl";

View File

@@ -335,17 +335,9 @@ sidebar:
en: "Incoming new conversations" en: "Incoming new conversations"
trusted_contacts_tooltip: trusted_contacts_tooltip:
en: "Only show rooms from trusted contacts" en: "Only show rooms from trusted contacts"
retrieving_messages:
en: "Retrieving messages" loading:
retrieving_messages_description: label:
en: "This may take some time" en: "Downloading messages"
why_seeing_this_tooltip: tooltip:
en: "Why you're seeing this" en: "This may take a while. Please be patient."
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"