This commit is contained in:
2026-09-01 11:04:16 +07:00
parent 3cdc623583
commit a4c12723de
10 changed files with 76 additions and 103 deletions
+3
View File
@@ -14,6 +14,7 @@
//! [`PopupMenu`], wired through `gpui_base::Popover` //! [`PopupMenu`], wired through `gpui_base::Popover`
//! - [`SegmentButton`] / [`CountBadge`] — segmented filter/tab button with an //! - [`SegmentButton`] / [`CountBadge`] — segmented filter/tab button with an
//! optional count badge //! optional count badge
//! - [`UserAvatar`] — user picture avatar with a name-initials fallback
//! - [`status_badge`] — NIP-34 issue/PR status badge //! - [`status_badge`] — NIP-34 issue/PR status badge
//! - [`placeholder`] — centered muted placeholder message //! - [`placeholder`] — centered muted placeholder message
//! - [`copy_row`] / [`menu_copy_row`] — rows with a copy-to-clipboard button //! - [`copy_row`] / [`menu_copy_row`] — rows with a copy-to-clipboard button
@@ -31,6 +32,7 @@ mod segment_button;
mod status_badge; mod status_badge;
mod title_bar; mod title_bar;
mod tree_row; mod tree_row;
mod user_avatar;
pub mod copy_row; pub mod copy_row;
pub mod image_cache; pub mod image_cache;
@@ -46,4 +48,5 @@ pub use segment_button::{CountBadge, SegmentButton};
pub use status_badge::status_badge; pub use status_badge::status_badge;
pub use title_bar::title_bar_drag_handlers; pub use title_bar::title_bar_drag_handlers;
pub use tree_row::tree_row; pub use tree_row::tree_row;
pub use user_avatar::UserAvatar;
pub use util::middle_truncate; pub use util::middle_truncate;
+48
View File
@@ -0,0 +1,48 @@
use gpui::prelude::*;
use gpui::{App, SharedString, StyleRefinement, Window};
use gpui_component::avatar::Avatar;
use gpui_component::{ActiveTheme, Sizable, StyledExt};
/// A user avatar: the gpui-component [`Avatar`] sized small and rounded with
/// the theme radius, showing the user's picture or a name-initials fallback.
#[derive(IntoElement)]
pub struct UserAvatar {
name: SharedString,
picture: Option<SharedString>,
style: StyleRefinement,
}
impl UserAvatar {
/// Create an avatar for `name`; the name seeds the initials fallback
/// shown when no picture is set.
pub fn new(name: impl Into<SharedString>) -> Self {
Self {
name: name.into(),
picture: None,
style: StyleRefinement::default(),
}
}
/// The user's picture URL, if known.
pub fn picture(mut self, picture: Option<impl Into<SharedString>>) -> Self {
self.picture = picture.map(Into::into);
self
}
}
impl Styled for UserAvatar {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for UserAvatar {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
Avatar::new()
.name(self.name)
.when_some(self.picture, |this, url| this.src(url))
.rounded(cx.theme().radius)
.refine_style(&self.style)
.small()
}
}
@@ -1,12 +1,11 @@
use gpui::prelude::*; use gpui::prelude::*;
use gpui::{AnyElement, App, SharedString, Window, div, px}; use gpui::{AnyElement, App, SharedString, Window, div, px};
use gpui_component::avatar::Avatar;
use gpui_component::clipboard::Clipboard; use gpui_component::clipboard::Clipboard;
use gpui_component::{ActiveTheme, Sizable, StyledExt, WindowExt, h_flex, v_flex}; use gpui_component::{ActiveTheme, StyledExt, WindowExt, h_flex, v_flex};
use nostr::prelude::PublicKey; use nostr::prelude::PublicKey;
use signed_core::Announcement; use signed_core::Announcement;
use signed_state::ProfileStore; use signed_state::ProfileStore;
use signed_ui::middle_truncate; use signed_ui::{UserAvatar, middle_truncate};
/// Open the "About" dialog: every field of the repository's announcement /// Open the "About" dialog: every field of the repository's announcement
/// event (NIP-34, kind 30617), as parsed into [`Announcement`]. /// event (NIP-34, kind 30617), as parsed into [`Announcement`].
@@ -174,13 +173,7 @@ fn maintainers(maintainers: &[PublicKey], cx: &App) -> AnyElement {
.gap_2() .gap_2()
.items_center() .items_center()
.min_w_0() .min_w_0()
.child( .child(UserAvatar::new(name.clone()).picture(picture))
Avatar::new()
.name(name.clone())
.when_some(picture, |this, url| this.src(url))
.rounded(cx.theme().radius)
.small(),
)
.child( .child(
div() div()
.flex_1() .flex_1()
@@ -7,7 +7,6 @@ use gpui::{
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString,
Window, div, px, relative, Window, div, px, relative,
}; };
use gpui_component::avatar::Avatar;
use gpui_component::button::{Button, ButtonVariants}; use gpui_component::button::{Button, ButtonVariants};
use gpui_component::input::{Textarea, TextareaState}; use gpui_component::input::{Textarea, TextareaState};
use gpui_component::scroll::ScrollableElement; use gpui_component::scroll::ScrollableElement;
@@ -17,7 +16,7 @@ use nostr::prelude::{Event, EventId, PublicKey};
use signed_core::activity_subject; use signed_core::activity_subject;
use signed_state::{ProfileStore, RepoStore}; use signed_state::{ProfileStore, RepoStore};
use signed_ui::image_cache::{MAX_IMAGES, image_cache}; use signed_ui::image_cache::{MAX_IMAGES, image_cache};
use signed_ui::{placeholder, status_badge}; use signed_ui::{UserAvatar, placeholder, status_badge};
use utils::relative_time; use utils::relative_time;
/// Detail panel of a single issue. /// Detail panel of a single issue.
@@ -91,13 +90,7 @@ impl IssueDetailView {
h_flex() h_flex()
.gap_1() .gap_1()
.items_center() .items_center()
.child( .child(UserAvatar::new(name.clone()).picture(picture))
Avatar::new()
.name(name.clone())
.when_some(picture, |this, url| this.src(url))
.rounded(cx.theme().radius)
.small(),
)
.child(div().text_sm().truncate().text_ellipsis().child(name)) .child(div().text_sm().truncate().text_ellipsis().child(name))
.into_any_element() .into_any_element()
})), })),
@@ -169,13 +162,7 @@ impl IssueDetailView {
.child( .child(
h_flex() h_flex()
.gap_1() .gap_1()
.child( .child(UserAvatar::new(author.clone()).picture(picture))
Avatar::new()
.name(author.clone())
.when_some(picture, |this, url| this.src(url))
.rounded(cx.theme().radius)
.small(),
)
.child(author), .child(author),
) )
.child( .child(
@@ -351,13 +338,8 @@ impl Render for IssueDetailView {
h_flex() h_flex()
.gap_1() .gap_1()
.child( .child(
Avatar::new() UserAvatar::new(author.clone())
.when_some(picture, |this, url| { .picture(picture),
this.src(url)
})
.name(author.clone())
.rounded(cx.theme().radius)
.small(),
) )
.child(author), .child(author),
) )
@@ -7,20 +7,19 @@ use gpui::{
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render, AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render,
SharedString, Size, WeakEntity, Window, div, px, size, SharedString, Size, WeakEntity, Window, div, px, size,
}; };
use gpui_component::avatar::Avatar;
use gpui_component::button::{Button, ButtonVariants}; use gpui_component::button::{Button, ButtonVariants};
use gpui_component::dialog::{DialogDescription, DialogFooter, DialogHeader, DialogTitle}; use gpui_component::dialog::{DialogDescription, DialogFooter, DialogHeader, DialogTitle};
use gpui_component::form::{field, v_form}; use gpui_component::form::{field, v_form};
use gpui_component::input::{Input, InputState, Textarea, TextareaState}; use gpui_component::input::{Input, InputState, Textarea, TextareaState};
use gpui_component::scroll::Scrollbar; use gpui_component::scroll::Scrollbar;
use gpui_component::{ use gpui_component::{
ActiveTheme, Icon, Sizable, VirtualListScrollHandle, WindowExt, h_flex, v_flex, v_virtual_list, ActiveTheme, Icon, VirtualListScrollHandle, WindowExt, h_flex, v_flex, v_virtual_list,
}; };
use nostr::prelude::EventId; use nostr::prelude::EventId;
use signed_core::{RepoStatus, activity_subject}; use signed_core::{RepoStatus, activity_subject};
use signed_state::{ProfileStore, RepoStore}; use signed_state::{ProfileStore, RepoStore};
use signed_ui::image_cache::{MAX_IMAGES, image_cache}; use signed_ui::image_cache::{MAX_IMAGES, image_cache};
use signed_ui::{SegmentButton, placeholder, status_badge}; use signed_ui::{SegmentButton, UserAvatar, placeholder, status_badge};
use utils::relative_time; use utils::relative_time;
use super::issue_detail::IssueDetailView; use super::issue_detail::IssueDetailView;
@@ -167,13 +166,7 @@ impl IssuesView {
.child( .child(
h_flex() h_flex()
.gap_1() .gap_1()
.child( .child(UserAvatar::new(author.clone()).picture(picture))
Avatar::new()
.name(author.clone())
.when_some(picture, |this, url| this.src(url))
.rounded(cx.theme().radius)
.small(),
)
.child(div().child(author)), .child(div().child(author)),
) )
.child(SharedString::from("opened")) .child(SharedString::from("opened"))
@@ -14,7 +14,6 @@ use gpui::{
}; };
use gpui_base::{Button as BaseButton, Disableable, Popover}; use gpui_base::{Button as BaseButton, Disableable, Popover};
use gpui_component::alert::Alert; use gpui_component::alert::Alert;
use gpui_component::avatar::Avatar;
use gpui_component::button::{Button, ButtonVariants}; use gpui_component::button::{Button, ButtonVariants};
use gpui_component::combobox::{ use gpui_component::combobox::{
Caret, Combobox, ComboboxEvent, ComboboxState, ComboboxTriggerContext, Caret, Combobox, ComboboxEvent, ComboboxState, ComboboxTriggerContext,
@@ -31,7 +30,7 @@ use signed_core::Announcement;
use signed_git::{CommitList, FileCommit}; use signed_git::{CommitList, FileCommit};
use signed_state::{Backend, GitStore, LocalReposStore, ProfileStore, RepoStore}; use signed_state::{Backend, GitStore, LocalReposStore, ProfileStore, RepoStore};
use signed_ui::image_cache::{MAX_IMAGES, image_cache}; use signed_ui::image_cache::{MAX_IMAGES, image_cache};
use signed_ui::{DropdownButton, PixelAvatar, copy_row}; use signed_ui::{DropdownButton, PixelAvatar, UserAvatar, copy_row};
mod about; mod about;
mod browser; mod browser;
@@ -1883,13 +1882,7 @@ impl RepoDetailView {
.child( .child(
h_flex() h_flex()
.gap_1() .gap_1()
.child( .child(UserAvatar::new(owner_name.clone()).picture(owner_picture))
Avatar::new()
.name(owner_name.clone())
.when_some(owner_picture, |this, url| this.src(url))
.rounded(cx.theme().radius)
.small(),
)
.child(div().text_xs().whitespace_nowrap().child(owner_name)), .child(div().text_xs().whitespace_nowrap().child(owner_name)),
) )
.when(!rest.is_empty(), |this| { .when(!rest.is_empty(), |this| {
@@ -10,7 +10,6 @@ use gpui::{
ScrollStrategy, SharedString, Size, Subscription, Task, WeakEntity, Window, div, px, relative, ScrollStrategy, SharedString, Size, Subscription, Task, WeakEntity, Window, div, px, relative,
size, size,
}; };
use gpui_component::avatar::Avatar;
use gpui_component::button::{Button, ButtonVariants}; use gpui_component::button::{Button, ButtonVariants};
use gpui_component::clipboard::Clipboard; use gpui_component::clipboard::Clipboard;
use gpui_component::input::{Textarea, TextareaState}; use gpui_component::input::{Textarea, TextareaState};
@@ -28,7 +27,7 @@ use signed_core::{activity_subject, pull_request_patch};
use signed_git::{CommitDiff, FileCommit, FileDiff, patch_commits, patch_diffs}; use signed_git::{CommitDiff, FileCommit, FileDiff, patch_commits, patch_diffs};
use signed_state::{GitStore, ProfileStore, RepoStore}; use signed_state::{GitStore, ProfileStore, RepoStore};
use signed_ui::image_cache::{MAX_IMAGES, image_cache}; use signed_ui::image_cache::{MAX_IMAGES, image_cache};
use signed_ui::{placeholder, status_badge, tree_row}; use signed_ui::{UserAvatar, placeholder, status_badge, tree_row};
use utils::{relative_time, relative_time_secs}; use utils::{relative_time, relative_time_secs};
use super::diff::CommitDiffView; use super::diff::CommitDiffView;
@@ -668,13 +667,8 @@ impl PullRequestDetailView {
h_flex() h_flex()
.gap_1() .gap_1()
.child( .child(
Avatar::new() UserAvatar::new(author.clone())
.name(author.clone()) .picture(picture),
.when_some(picture, |this, url| {
this.src(url)
})
.rounded(cx.theme().radius)
.small(),
) )
.child(author), .child(author),
) )
@@ -741,13 +735,7 @@ impl PullRequestDetailView {
h_flex() h_flex()
.gap_1() .gap_1()
.items_center() .items_center()
.child( .child(UserAvatar::new(name.clone()).picture(picture))
Avatar::new()
.name(name.clone())
.when_some(picture, |this, url| this.src(url))
.rounded(cx.theme().radius)
.small(),
)
.child(div().text_sm().truncate().text_ellipsis().child(name)) .child(div().text_sm().truncate().text_ellipsis().child(name))
.into_any_element() .into_any_element()
})), })),
@@ -937,13 +925,7 @@ impl PullRequestDetailView {
.child( .child(
h_flex() h_flex()
.gap_1() .gap_1()
.child( .child(UserAvatar::new(author.clone()).picture(picture))
Avatar::new()
.name(author.clone())
.when_some(picture, |this, url| this.src(url))
.rounded(cx.theme().radius)
.small(),
)
.child(author), .child(author),
) )
.child( .child(
@@ -7,20 +7,19 @@ use gpui::{
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render, AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render,
SharedString, Size, WeakEntity, Window, div, px, size, SharedString, Size, WeakEntity, Window, div, px, size,
}; };
use gpui_component::avatar::Avatar;
use gpui_component::button::{Button, ButtonVariants}; use gpui_component::button::{Button, ButtonVariants};
use gpui_component::dialog::{DialogDescription, DialogFooter, DialogHeader, DialogTitle}; use gpui_component::dialog::{DialogDescription, DialogFooter, DialogHeader, DialogTitle};
use gpui_component::form::{field, v_form}; use gpui_component::form::{field, v_form};
use gpui_component::input::{Input, InputState, Textarea, TextareaState}; use gpui_component::input::{Input, InputState, Textarea, TextareaState};
use gpui_component::scroll::Scrollbar; use gpui_component::scroll::Scrollbar;
use gpui_component::{ use gpui_component::{
ActiveTheme, Icon, Sizable, VirtualListScrollHandle, WindowExt, h_flex, v_flex, v_virtual_list, ActiveTheme, Icon, VirtualListScrollHandle, WindowExt, h_flex, v_flex, v_virtual_list,
}; };
use nostr::prelude::{EventId, Kind}; use nostr::prelude::{EventId, Kind};
use signed_core::{RepoStatus, activity_subject}; use signed_core::{RepoStatus, activity_subject};
use signed_state::{ProfileStore, RepoStore}; use signed_state::{ProfileStore, RepoStore};
use signed_ui::image_cache::{MAX_IMAGES, image_cache}; use signed_ui::image_cache::{MAX_IMAGES, image_cache};
use signed_ui::{SegmentButton, placeholder, status_badge}; use signed_ui::{SegmentButton, UserAvatar, placeholder, status_badge};
use utils::relative_time; use utils::relative_time;
use super::pull_request_detail::PullRequestDetailView; use super::pull_request_detail::PullRequestDetailView;
@@ -187,13 +186,7 @@ impl PullRequestsView {
.child( .child(
h_flex() h_flex()
.gap_1() .gap_1()
.child( .child(UserAvatar::new(author.clone()).picture(picture))
Avatar::new()
.name(author.clone())
.when_some(picture, |this, url| this.src(url))
.rounded(cx.theme().radius)
.small(),
)
.child(div().child(author)), .child(div().child(author)),
) )
.child(SharedString::from("opened")) .child(SharedString::from("opened"))
+2 -9
View File
@@ -7,7 +7,6 @@ use gpui::{
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render, AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render,
SharedString, Size, Subscription, WeakEntity, Window, div, px, size, SharedString, Size, Subscription, WeakEntity, Window, div, px, size,
}; };
use gpui_component::avatar::Avatar;
use gpui_component::input::{Input, InputEvent, InputState}; use gpui_component::input::{Input, InputEvent, InputState};
use gpui_component::scroll::Scrollbar; use gpui_component::scroll::Scrollbar;
use gpui_component::{ use gpui_component::{
@@ -16,8 +15,8 @@ use gpui_component::{
}; };
use signed_core::Announcement; use signed_core::Announcement;
use signed_state::{ProfileStore, RepoListStore, Timestamp}; use signed_state::{ProfileStore, RepoListStore, Timestamp};
use signed_ui::SegmentButton;
use signed_ui::image_cache::{MAX_IMAGES, image_cache}; use signed_ui::image_cache::{MAX_IMAGES, image_cache};
use signed_ui::{SegmentButton, UserAvatar};
use utils::relative_time; use utils::relative_time;
use super::RepoDetailView; use super::RepoDetailView;
@@ -255,13 +254,7 @@ impl RepoListView {
h_flex() h_flex()
.gap_2() .gap_2()
.items_center() .items_center()
.child( .child(UserAvatar::new(owner.name()).picture(owner.picture()))
Avatar::new()
.name(owner.name())
.when_some(owner.picture(), |this, url| this.src(url))
.rounded(cx.theme().radius)
.small(),
)
.child( .child(
div() div()
.text_xs() .text_xs()
+2 -9
View File
@@ -11,14 +11,13 @@ use gpui::{
SharedString, Subscription, WeakEntity, Window, div, img, px, uniform_list, SharedString, Subscription, WeakEntity, Window, div, img, px, uniform_list,
}; };
use gpui_base::Button as BaseButton; use gpui_base::Button as BaseButton;
use gpui_component::avatar::Avatar;
use gpui_component::button::{Button, ButtonVariants}; use gpui_component::button::{Button, ButtonVariants};
use gpui_component::input::InputState; use gpui_component::input::InputState;
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, StyledExt, h_flex, v_flex}; use gpui_component::{ActiveTheme, Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
use signed_core::{Announcement, identifier_from_name}; use signed_core::{Announcement, identifier_from_name};
use signed_state::{Backend, BackendEvent, LocalReposStore, Profile, ProfileStore, RepoListStore}; use signed_state::{Backend, BackendEvent, LocalReposStore, Profile, ProfileStore, RepoListStore};
use signed_ui::image_cache::{MAX_IMAGES, image_cache}; use signed_ui::image_cache::{MAX_IMAGES, image_cache};
use signed_ui::{NavItem, PixelAvatar, title_bar_drag_handlers}; use signed_ui::{NavItem, PixelAvatar, UserAvatar, title_bar_drag_handlers};
use super::{RepoDetailView, RepoListView}; use super::{RepoDetailView, RepoListView};
@@ -397,13 +396,7 @@ impl SidebarPanel {
Button::new("user").text().dropdown_caret(true).child( Button::new("user").text().dropdown_caret(true).child(
h_flex() h_flex()
.gap_1() .gap_1()
.child( .child(UserAvatar::new(name.clone()).picture(picture))
Avatar::new()
.name(name.clone())
.when_some(picture, |this, url| this.src(url))
.rounded(cx.theme().radius)
.small(),
)
.child(div().text_xs().font_semibold().child(name)), .child(div().text_xs().font_semibold().child(name)),
), ),
), ),