update avatar

This commit is contained in:
2026-09-25 14:56:49 +07:00
parent 192a0585b6
commit a29ce9f827
17 changed files with 94 additions and 72 deletions
+2 -1
View File
@@ -11,11 +11,12 @@
### Changed ### Changed
- Migrate the GPUI foundation to the published `gpui-pre` crates and GPUI Kit 0.6, off the zed and gpui-component git pins - Migrate the GPUI foundation to the published `gpui-pre` crates and GPUI Kit 0.6, off the zed and gpui-component git pins
- Use the pixel avatar as the single fallback for a missing picture, sized and rounded to match the other avatars
- Redesign the dock tab bar, using muted grey active tab, added close buttons, double-click to zoom, and removed panel toolbar - Redesign the dock tab bar, using muted grey active tab, added close buttons, double-click to zoom, and removed panel toolbar
### Fixed ### Fixed
- Honor an explicit size on the pixel-art avatar, so the repository header's avatar is no longer stuck at the 16px default - Render every avatar at one consistent size, where a surrounding border had shrunk pictures by two pixels and the pixel avatar ignored an explicit size
### Removed ### Removed
+6 -6
View File
@@ -77,9 +77,9 @@
"success.foreground": "#052E16", "success.foreground": "#052E16",
"switch.background": "#CECECE", "switch.background": "#CECECE",
"switch.thumb.background": "#FFFFFF", "switch.thumb.background": "#FFFFFF",
"tab.background": "#00000000", "tab.background": "#E8E8E8",
"tab.active.background": "#EBFFC1", "tab.active.background": "#F5F5F5",
"tab.active.foreground": "#3F6212", "tab.active.foreground": "#18181B",
"tab.foreground": "#646464", "tab.foreground": "#646464",
"tab_bar.background": "#F0F0F0", "tab_bar.background": "#F0F0F0",
"tab_bar.segmented.background": "#F0F0F0", "tab_bar.segmented.background": "#F0F0F0",
@@ -286,9 +286,9 @@
"success.foreground": "#052E16", "success.foreground": "#052E16",
"switch.background": "#484848", "switch.background": "#484848",
"switch.thumb.background": "#0A0A0A", "switch.thumb.background": "#0A0A0A",
"tab.background": "#00000000", "tab.background": "#2E2E2E",
"tab.active.background": "#19200A", "tab.active.background": "#2A2A2A",
"tab.active.foreground": "#C6FF4D", "tab.active.foreground": "#FAFAFA",
"tab.foreground": "#B4B4B4", "tab.foreground": "#B4B4B4",
"tab_bar.background": "#191919", "tab_bar.background": "#191919",
"tab_bar.segmented.background": "#191919", "tab_bar.segmented.background": "#191919",
+5 -7
View File
@@ -352,15 +352,13 @@ impl SignedTabGroupSkin {
.items_center() .items_center()
.flex_shrink_0() .flex_shrink_0()
.h_7() .h_7()
.min_w_32() .min_w_40()
.gap_1() .gap_1()
.px_1p5() .px_1p5()
.text_sm() .text_sm()
.overflow_hidden() .overflow_hidden()
.rounded(cx.theme().radius) .rounded(cx.theme().radius)
.text_color(cx.theme().tab_foreground) .text_color(cx.theme().tab_foreground)
// The title claims the free space and starts at the left, so a name
// narrower than the tab leaves blank space before the close button.
.child( .child(
h_flex() h_flex()
.flex_1() .flex_1()
@@ -406,16 +404,16 @@ impl SignedTabGroupSkin {
.styles(|styles| { .styles(|styles| {
styles.selected(|style| { styles.selected(|style| {
style style
.text_color(cx.theme().foreground) .text_color(cx.theme().tab_active_foreground)
.bg(cx.theme().secondary) .bg(cx.theme().tab_active)
}) })
}) })
.hover(|this| { .hover(|this| {
if active { if active {
this this
} else { } else {
this.text_color(cx.theme().foreground) this.text_color(cx.theme().tab_foreground)
.bg(cx.theme().secondary_hover) .bg(cx.theme().tab)
} }
}) })
.selected(active) .selected(active)
@@ -1,21 +1,26 @@
use gpui::prelude::*; use gpui::prelude::*;
use gpui::{App, SharedString, StyleRefinement, Window}; use gpui::{App, SharedString, StyleRefinement, Window};
use gpui_component::avatar::Avatar; use gpui_base::{Avatar as BaseAvatar, AvatarFallback, AvatarImage};
use gpui_component::{ActiveTheme, Sizable, Size, StyledExt}; use gpui_component::{ActiveTheme, Sizable, Size, StyledExt};
/// A small user avatar from gpui-component [`Avatar`], rounded with the theme radius. use crate::pixel_avatar::{PixelAvatar, side_length};
/// It shows the user's picture or falls back to name initials.
/// A user avatar built on the unstyled [`gpui_base::Avatar`].
///
/// It shows the user's picture when one is set, and falls back to the
/// deterministic pixel avatar seeded from the name otherwise.
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct UserAvatar { pub struct Avatar {
name: SharedString, name: SharedString,
picture: Option<SharedString>, picture: Option<SharedString>,
size: Size, size: Size,
style: StyleRefinement, style: StyleRefinement,
} }
impl UserAvatar { impl Avatar {
/// Create an avatar for `name`. /// Create an avatar for `name`.
/// The name seeds the initials fallback shown when no picture is set. ///
/// The name seeds the pixel fallback shown when no picture is set.
pub fn new(name: impl Into<SharedString>) -> Self { pub fn new(name: impl Into<SharedString>) -> Self {
Self { Self {
name: name.into(), name: name.into(),
@@ -32,26 +37,35 @@ impl UserAvatar {
} }
} }
impl Sizable for UserAvatar { impl Sizable for Avatar {
fn with_size(mut self, size: impl Into<Size>) -> Self { fn with_size(mut self, size: impl Into<Size>) -> Self {
self.size = size.into(); self.size = size.into();
self self
} }
} }
impl Styled for UserAvatar { impl Styled for Avatar {
fn style(&mut self) -> &mut StyleRefinement { fn style(&mut self) -> &mut StyleRefinement {
&mut self.style &mut self.style
} }
} }
impl RenderOnce for UserAvatar { impl RenderOnce for Avatar {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
Avatar::new() let fallback = AvatarFallback::new()
.name(self.name) .size_full()
.when_some(self.picture, |this, url| this.src(url)) .child(PixelAvatar::new(self.name.clone()).size_full());
BaseAvatar::new()
.size(side_length(self.size))
.flex_shrink_0()
.rounded(cx.theme().radius) .rounded(cx.theme().radius)
.overflow_hidden()
.bg(cx.theme().secondary)
.when_some(self.picture, |this, url| {
this.image(AvatarImage::new(url).size_full().rounded(cx.theme().radius))
})
.fallback(fallback)
.refine_style(&self.style) .refine_style(&self.style)
.with_size(self.size)
} }
} }
+2 -2
View File
@@ -1,3 +1,4 @@
mod avatar;
mod dropdown_button; mod dropdown_button;
mod nav_item; mod nav_item;
mod pixel_avatar; mod pixel_avatar;
@@ -8,11 +9,11 @@ mod setting;
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 util; pub mod util;
pub use avatar::Avatar;
pub use copy_row::{copy_row, menu_copy_row}; pub use copy_row::{copy_row, menu_copy_row};
pub use dropdown_button::DropdownButton; pub use dropdown_button::DropdownButton;
pub use nav_item::NavItem; pub use nav_item::NavItem;
@@ -24,5 +25,4 @@ pub use setting::{SelectOption, setting_block, setting_row};
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;
+21 -13
View File
@@ -5,7 +5,10 @@ use gpui_component::{ActiveTheme, Colorize, Sizable, Size};
/// Number of rows and columns in the pixel grid. /// Number of rows and columns in the pixel grid.
const GRID_SIZE: usize = 8; const GRID_SIZE: usize = 8;
/// Probability that a cell in the left half is filled. /// Empty cells kept between the pattern and the avatar edge, so the art
/// gathers in the center instead of filling the whole avatar.
const MARGIN: usize = 1;
/// Probability that a cell in the left half of the pattern area is filled.
const FILL_PROBABILITY: f32 = 0.42; const FILL_PROBABILITY: f32 = 0.42;
/// Probability that a filled cell uses the accent shade instead of the main color. /// Probability that a filled cell uses the accent shade instead of the main color.
const ACCENT_PROBABILITY: f32 = 0.25; const ACCENT_PROBABILITY: f32 = 0.25;
@@ -22,12 +25,10 @@ pub struct PixelAvatar {
impl PixelAvatar { impl PixelAvatar {
/// Create an avatar seeded from `seed`. /// Create an avatar seeded from `seed`.
///
/// The seed should be a stable string unique to the entity the avatar represents.
pub fn new(seed: impl AsRef<str>) -> Self { pub fn new(seed: impl AsRef<str>) -> Self {
Self { Self {
seed: fnv1a(seed.as_ref().as_bytes()), seed: fnv1a(seed.as_ref().as_bytes()),
size: Size::XSmall, size: Size::Small,
style: StyleRefinement::default(), style: StyleRefinement::default(),
} }
} }
@@ -84,6 +85,7 @@ impl RenderOnce for PixelAvatar {
.grid_rows(GRID_SIZE as u16) .grid_rows(GRID_SIZE as u16)
.size(side_length(self.size)) .size(side_length(self.size))
.flex_shrink_0() .flex_shrink_0()
.rounded(theme.radius)
.overflow_hidden() .overflow_hidden()
.bg(main.opacity(0.16)) .bg(main.opacity(0.16))
.children(cells) .children(cells)
@@ -91,7 +93,8 @@ impl RenderOnce for PixelAvatar {
} }
} }
fn side_length(size: Size) -> Pixels { /// The rendered side length of an avatar at `size`, shared with [`Avatar`].
pub(crate) fn side_length(size: Size) -> Pixels {
match size { match size {
Size::XSmall => px(16.), Size::XSmall => px(16.),
Size::Small => px(24.), Size::Small => px(24.),
@@ -106,8 +109,13 @@ fn pattern(seed: u64) -> [u8; GRID_SIZE * GRID_SIZE] {
let mut pattern = [0u8; GRID_SIZE * GRID_SIZE]; let mut pattern = [0u8; GRID_SIZE * GRID_SIZE];
let mut filled = 0usize; let mut filled = 0usize;
for row in 0..GRID_SIZE { // Only the inner rows and the inner left half are candidates; mirroring
for col in 0..GRID_SIZE / 2 { // then keeps the art within the same inset, leaving the outer ring empty.
let art_rows = GRID_SIZE - 2 * MARGIN;
let art_columns = GRID_SIZE / 2 - MARGIN;
for row in MARGIN..GRID_SIZE - MARGIN {
for col in MARGIN..GRID_SIZE / 2 {
if rng.chance(FILL_PROBABILITY) { if rng.chance(FILL_PROBABILITY) {
let accent = rng.chance(ACCENT_PROBABILITY); let accent = rng.chance(ACCENT_PROBABILITY);
set_cell(&mut pattern, row, col, if accent { 2 } else { 1 }); set_cell(&mut pattern, row, col, if accent { 2 } else { 1 });
@@ -117,17 +125,17 @@ fn pattern(seed: u64) -> [u8; GRID_SIZE * GRID_SIZE] {
} }
if filled < MIN_FILLED { if filled < MIN_FILLED {
let half = GRID_SIZE * GRID_SIZE / 2; let total = art_rows * art_columns;
let start = (rng.next() % half as u64) as usize; let start = (rng.next() % total as u64) as usize;
for offset in 0..half { for offset in 0..total {
if filled >= MIN_FILLED { if filled >= MIN_FILLED {
break; break;
} }
let ix = (start + offset) % half; let ix = (start + offset) % total;
let row = ix / (GRID_SIZE / 2); let row = MARGIN + ix / art_columns;
let col = ix % (GRID_SIZE / 2); let col = MARGIN + ix % art_columns;
if pattern[row * GRID_SIZE + col] == 0 { if pattern[row * GRID_SIZE + col] == 0 {
set_cell(&mut pattern, row, col, 1); set_cell(&mut pattern, row, col, 1);
+3 -3
View File
@@ -7,7 +7,7 @@ use gpui_component::tag::Tag;
use gpui_component::{ActiveTheme, Icon, Sizable, StyledExt, h_flex, v_flex}; use gpui_component::{ActiveTheme, Icon, Sizable, StyledExt, h_flex, v_flex};
use nostr::prelude::{Event, EventId, PublicKey}; use nostr::prelude::{Event, EventId, PublicKey};
use signed_state::{ProfileStore, RepoStore}; use signed_state::{ProfileStore, RepoStore};
use signed_ui::UserAvatar; use signed_ui::Avatar;
use utils::relative_time; use utils::relative_time;
pub(crate) fn issue_roots(store: &RepoStore) -> &[Event] { pub(crate) fn issue_roots(store: &RepoStore) -> &[Event] {
@@ -71,7 +71,7 @@ pub(crate) fn sidebar_section(
h_flex() h_flex()
.gap_1() .gap_1()
.items_center() .items_center()
.child(UserAvatar::new(name.clone()).picture(picture)) .child(Avatar::new(name.clone()).picture(picture))
.child(div().text_sm().truncate().text_ellipsis().child(name)) .child(div().text_sm().truncate().text_ellipsis().child(name))
.into_any_element() .into_any_element()
})), })),
@@ -137,7 +137,7 @@ pub(crate) fn comments_section(store: &Entity<RepoStore>, root: EventId, cx: &Ap
.child( .child(
h_flex() h_flex()
.gap_1() .gap_1()
.child(UserAvatar::new(author.clone()).picture(picture)) .child(Avatar::new(author.clone()).picture(picture))
.child(author), .child(author),
) )
.child( .child(
+2 -2
View File
@@ -15,7 +15,7 @@ use signed_core::{COVER_NOTE_KIND, InboxItem, InboxReadState, RepoAddr, filters}
use signed_state::{ use signed_state::{
Backend, BackendEvent, ProfileStore, RefreshGate, RefreshRequest, RepoListStore, query_inbox, Backend, BackendEvent, ProfileStore, RefreshGate, RefreshRequest, RepoListStore, query_inbox,
}; };
use signed_ui::{CountBadge, UserAvatar}; use signed_ui::{Avatar, CountBadge};
use utils::relative_time; use utils::relative_time;
use super::{RepoItem, open_repo_item, panel_avatar, tab_title}; use super::{RepoItem, open_repo_item, panel_avatar, tab_title};
@@ -553,7 +553,7 @@ fn sub_activity(event: &Event, me: Option<PublicKey>, cx: &App) -> AnyElement {
.items_center() .items_center()
.text_xs() .text_xs()
.child( .child(
UserAvatar::new(name.clone()) Avatar::new(name.clone())
.picture(profile.picture()) .picture(profile.picture())
.xsmall(), .xsmall(),
) )
+2 -4
View File
@@ -10,7 +10,7 @@ use gpui_component::{ActiveTheme, StyledExt, h_flex, v_flex};
use nostr::prelude::EventId; use nostr::prelude::EventId;
use signed_core::activity_subject; use signed_core::activity_subject;
use signed_state::{ProfileStore, RepoStore}; use signed_state::{ProfileStore, RepoStore};
use signed_ui::{UserAvatar, placeholder, status_badge}; use signed_ui::{Avatar, placeholder, status_badge};
use utils::relative_time; use utils::relative_time;
use crate::views::discussion::{comment_form, comments_section, issue_roots, sidebar_section}; use crate::views::discussion::{comment_form, comments_section, issue_roots, sidebar_section};
@@ -153,9 +153,7 @@ impl Render for IssueDetailView {
.child( .child(
h_flex() h_flex()
.gap_1() .gap_1()
.child( .child(Avatar::new(&author).picture(picture))
UserAvatar::new(&author).picture(picture),
)
.child(author), .child(author),
) )
.child( .child(
+2 -2
View File
@@ -18,7 +18,7 @@ use gpui_component::{
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::{SegmentButton, UserAvatar, placeholder, status_badge}; use signed_ui::{Avatar, SegmentButton, placeholder, status_badge};
use utils::relative_time; use utils::relative_time;
pub(super) mod detail; pub(super) mod detail;
@@ -189,7 +189,7 @@ impl IssuesView {
.child( .child(
h_flex() h_flex()
.gap_1() .gap_1()
.child(UserAvatar::new(author.clone()).picture(picture)) .child(Avatar::new(author.clone()).picture(picture))
.child(div().child(author)), .child(div().child(author)),
) )
.child(SharedString::from("opened")) .child(SharedString::from("opened"))
+3 -3
View File
@@ -20,11 +20,11 @@ pub(crate) use repo::{RepoItem, open_repo_item, open_repo_panel};
pub use repo_list::RepoListView; pub use repo_list::RepoListView;
pub use sidebar::SidebarPanel; pub use sidebar::SidebarPanel;
use signed_state::{ProfileStore, RepoStore}; use signed_state::{ProfileStore, RepoStore};
use signed_ui::{PixelAvatar, UserAvatar}; use signed_ui::{Avatar, PixelAvatar};
pub(crate) fn tab_title(avatar: AnyElement, label: impl IntoElement) -> impl IntoElement { pub(crate) fn tab_title(avatar: AnyElement, label: impl IntoElement) -> impl IntoElement {
h_flex() h_flex()
.gap_1() .gap_1p5()
.items_center() .items_center()
.child(div().flex_shrink_0().child(avatar)) .child(div().flex_shrink_0().child(avatar))
.child(label) .child(label)
@@ -54,7 +54,7 @@ pub(crate) fn repo_tab_avatar(store: &RepoStore, cx: &App) -> AnyElement {
.map(|announcement| ProfileStore::global(cx).read(cx).get(&announcement.owner)); .map(|announcement| ProfileStore::global(cx).read(cx).get(&announcement.owner));
match owner.and_then(|profile| profile.picture().map(|picture| (profile.name(), picture))) { match owner.and_then(|profile| profile.picture().map(|picture| (profile.name(), picture))) {
Some((name, picture)) => UserAvatar::new(name) Some((name, picture)) => Avatar::new(name)
.picture(Some(picture)) .picture(Some(picture))
.xsmall() .xsmall()
.into_any_element(), .into_any_element(),
@@ -27,7 +27,7 @@ use signed_core::{
}; };
use signed_git::{FileCommit, patch_commits, patch_diffs}; use signed_git::{FileCommit, patch_commits, patch_diffs};
use signed_state::{Backend, ProfileStore, RepoStore, ensure_repo_mirror}; use signed_state::{Backend, ProfileStore, RepoStore, ensure_repo_mirror};
use signed_ui::{CountBadge, UserAvatar, placeholder, status_badge}; use signed_ui::{Avatar, CountBadge, placeholder, status_badge};
use utils::{relative_time, relative_time_secs}; use utils::{relative_time, relative_time_secs};
use crate::views::commit_diff::{CommitDiffView, DiffPane}; use crate::views::commit_diff::{CommitDiffView, DiffPane};
@@ -453,7 +453,7 @@ impl PullRequestDetailView {
h_flex() h_flex()
.gap_1() .gap_1()
.child( .child(
UserAvatar::new(author.clone()) Avatar::new(author.clone())
.picture(picture), .picture(picture),
) )
.child(author), .child(author),
@@ -16,7 +16,7 @@ use gpui_component::{
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::{DropdownButton, SegmentButton, UserAvatar, placeholder, status_badge}; use signed_ui::{Avatar, DropdownButton, SegmentButton, placeholder, status_badge};
use utils::relative_time; use utils::relative_time;
pub(super) mod detail; pub(super) mod detail;
@@ -203,7 +203,7 @@ impl PullRequestsView {
.child( .child(
h_flex() h_flex()
.gap_1() .gap_1()
.child(UserAvatar::new(author.clone()).picture(picture)) .child(Avatar::new(author.clone()).picture(picture))
.child(div().child(author)), .child(div().child(author)),
) )
.child(SharedString::from("opened")) .child(SharedString::from("opened"))
+2 -2
View File
@@ -5,7 +5,7 @@ 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::{UserAvatar, middle_truncate}; use signed_ui::{Avatar, middle_truncate};
pub(super) fn open_about_dialog(announcement: Announcement, window: &mut Window, cx: &mut App) { pub(super) fn open_about_dialog(announcement: Announcement, window: &mut Window, cx: &mut App) {
window.open_dialog(cx, move |dialog, _window, cx| { window.open_dialog(cx, move |dialog, _window, cx| {
@@ -173,7 +173,7 @@ fn maintainers(maintainers: &[PublicKey], cx: &App) -> AnyElement {
.gap_2() .gap_2()
.items_center() .items_center()
.min_w_0() .min_w_0()
.child(UserAvatar::new(name.clone()).picture(picture)) .child(Avatar::new(name.clone()).picture(picture))
.child( .child(
div() div()
.flex_1() .flex_1()
+3 -3
View File
@@ -31,7 +31,7 @@ use signed_state::{
pr_proposes_checkout, pr_proposes_checkout,
}; };
use signed_ui::{ use signed_ui::{
CountBadge, DropdownButton, PixelAvatar, UserAvatar, copy_row, menu_copy_row, middle_truncate, Avatar, CountBadge, DropdownButton, PixelAvatar, copy_row, menu_copy_row, middle_truncate,
ref_selector_trigger, ref_selector_trigger,
}; };
@@ -1491,7 +1491,7 @@ impl RepoDetailView {
.child( .child(
h_flex() h_flex()
.gap_1() .gap_1()
.child(UserAvatar::new(owner_name.clone()).picture(owner_picture)) .child(Avatar::new(owner_name.clone()).picture(owner_picture))
.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| {
@@ -2032,7 +2032,7 @@ fn bound_repo_label(binding: &Nip34Binding, cx: &App) -> AnyElement {
h_flex() h_flex()
.gap_1() .gap_1()
.text_color(cx.theme().foreground) .text_color(cx.theme().foreground)
.child(UserAvatar::new(profile.name()).picture(profile.picture())) .child(Avatar::new(profile.name()).picture(profile.picture()))
.child(profile.name()), .child(profile.name()),
) )
}) })
+2 -2
View File
@@ -16,7 +16,7 @@ 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, UserAvatar}; use signed_ui::{Avatar, SegmentButton};
use utils::relative_time; use utils::relative_time;
use super::{open_repo_panel, panel_avatar, tab_title}; use super::{open_repo_panel, panel_avatar, tab_title};
@@ -289,7 +289,7 @@ impl RepoListView {
h_flex() h_flex()
.gap_2() .gap_2()
.items_center() .items_center()
.child(UserAvatar::new(owner.name()).picture(owner.picture())) .child(Avatar::new(owner.name()).picture(owner.picture()))
.child( .child(
div() div()
.text_xs() .text_xs()
+8 -5
View File
@@ -23,7 +23,7 @@ use signed_state::{
Backend, BackendEvent, CheckoutsStore, LocalReposStore, Nip34Binding, Nip34Kind, Profile, Backend, BackendEvent, CheckoutsStore, LocalReposStore, Nip34Binding, Nip34Kind, Profile,
ProfileStore, RepoListStore, ResolvedLocalRepo, resolve_local_repos, ProfileStore, RepoListStore, ResolvedLocalRepo, resolve_local_repos,
}; };
use signed_ui::{NavItem, PixelAvatar, UserAvatar, title_bar_drag_handlers}; use signed_ui::{Avatar, NavItem, PixelAvatar, title_bar_drag_handlers};
use super::{InboxView, RepoDetailView, RepoListView, open_repo_panel}; use super::{InboxView, RepoDetailView, RepoListView, open_repo_panel};
@@ -447,7 +447,9 @@ impl SidebarPanel {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
let name = announcement.name().map(SharedString::from); let name = announcement.name().map(SharedString::from);
let avatar = PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id)); // The sidebar rows are compact, so the avatar matches the 16px nav icons.
let avatar =
PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id)).xsmall();
let announcement = announcement.clone(); let announcement = announcement.clone();
let unpushed = self let unpushed = self
@@ -552,7 +554,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(UserAvatar::new(name.clone()).picture(picture)) .child(Avatar::new(name.clone()).picture(picture))
.child(div().text_xs().font_semibold().child(name)), .child(div().text_xs().font_semibold().child(name)),
), ),
), ),
@@ -654,7 +656,8 @@ pub(super) fn server_host(relay: &RelayUrl) -> SharedString {
/// The repository's pixel avatar, with the bound owner's avatar at its bottom right. /// The repository's pixel avatar, with the bound owner's avatar at its bottom right.
fn local_avatar(entry: &ResolvedLocalRepo, cx: &App) -> AnyElement { fn local_avatar(entry: &ResolvedLocalRepo, cx: &App) -> AnyElement {
let avatar = PixelAvatar::new(entry.path.to_string_lossy()); // Matches the other sidebar rows, and the badge below sits at its corner.
let avatar = PixelAvatar::new(entry.path.to_string_lossy()).xsmall();
let Some(owner) = entry.nip34.as_ref().and_then(|binding| binding.owner) else { let Some(owner) = entry.nip34.as_ref().and_then(|binding| binding.owner) else {
return avatar.into_any_element(); return avatar.into_any_element();
@@ -668,7 +671,7 @@ fn local_avatar(entry: &ResolvedLocalRepo, cx: &App) -> AnyElement {
.child(avatar) .child(avatar)
.child( .child(
div().absolute().bottom_neg_0p5().right_neg_0p5().child( div().absolute().bottom_neg_0p5().right_neg_0p5().child(
UserAvatar::new(profile.name()) Avatar::new(profile.name())
.picture(profile.picture()) .picture(profile.picture())
.size(px(14.)), .size(px(14.)),
), ),