diff --git a/CHANGELOG.md b/CHANGELOG.md index cc2c145..3019471 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - Restructure the core crate into focused modules for announcements, pull requests, git events, inbox, and upstream state - Publish nostr events directly through the client with relay acceptance required, removing the grasp push helper and its per-server retry loop so failures surface immediately - Poll the repository sync drift check every 180 seconds instead of 60 +- Merge the `PixelAvatar` into the `Avatar` component. Seed user pixel avatars from the public key instead of the display name, so avatars stay stable when a user renames ### Fixed diff --git a/crates/signed_ui/src/avatar.rs b/crates/signed_ui/src/avatar.rs index a866b33..b2fb2e8 100644 --- a/crates/signed_ui/src/avatar.rs +++ b/crates/signed_ui/src/avatar.rs @@ -1,37 +1,62 @@ +use std::fmt::Display; + use gpui::prelude::*; -use gpui::{App, SharedString, StyleRefinement, Window}; -use gpui_base::{Avatar as BaseAvatar, AvatarFallback, AvatarImage}; -use gpui_component::{ActiveTheme, Sizable, Size, StyledExt}; +use gpui::{App, Pixels, SharedString, StyleRefinement, Window, div, px}; +use gpui_base::{AvatarImage, StyledExt}; +use gpui_component::{ActiveTheme, Colorize, Sizable, Size}; -use crate::pixel_avatar::{PixelAvatar, side_length}; +/// Number of rows and columns in the pixel grid. +const GRID_SIZE: usize = 8; +/// Empty cells kept between the pattern and the avatar edge. +const MARGIN: usize = 1; +/// Probability that a cell in the left half of the pattern area is filled. +const FILL_PROBABILITY: f32 = 0.42; +/// Probability that a filled cell uses the accent shade instead of the main color. +const ACCENT_PROBABILITY: f32 = 0.25; +/// Minimum number of filled left-half cells. +const MIN_FILLED: usize = 5; -/// 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. +/// An avatar for a user or other entity. #[derive(IntoElement)] pub struct Avatar { - name: SharedString, + id: SharedString, + name: Option, picture: Option, size: Size, style: StyleRefinement, } impl Avatar { - /// Create an avatar for `name`. + /// Create an avatar for `id`, a public key or any other stable string. /// - /// The name seeds the pixel fallback shown when no picture is set. - pub fn new(name: impl Into) -> Self { + /// The id seeds the pixel fallback shown when no picture is set. + pub fn new(id: T) -> Self + where + T: Display, + { Self { - name: name.into(), + id: id.to_string().into(), + name: None, picture: None, size: Size::Small, style: StyleRefinement::default(), } } + /// The display name of the avatar's subject, if known. + pub fn name(mut self, name: T) -> Self + where + T: Into>, + { + self.name = name.into(); + self + } + /// The user's picture URL, if known. - pub fn picture(mut self, picture: Option>) -> Self { + pub fn picture(mut self, picture: Option) -> Self + where + T: Into, + { self.picture = picture.map(Into::into); self } @@ -52,20 +77,176 @@ impl Styled for Avatar { impl RenderOnce for Avatar { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let fallback = AvatarFallback::new() - .size_full() - .child(PixelAvatar::new(self.name.clone()).size_full()); + let radius = cx.theme().radius; - BaseAvatar::new() - .size(side_length(self.size)) + match self.picture { + Some(url) => div() + .size(side_length(self.size)) + .flex_shrink_0() + .rounded(radius) + .overflow_hidden() + .bg(cx.theme().secondary) + .child(AvatarImage::new(url).size_full().rounded(radius)) + .refine_style(&self.style) + .into_any_element(), + None => PixelAvatar { + seed: fnv1a(self.id.as_bytes()), + side: side_length(self.size), + style: self.style, + } + .into_any_element(), + } + } +} + +/// A deterministic, offline pixel-art avatar. +#[derive(IntoElement)] +struct PixelAvatar { + seed: u64, + side: Pixels, + style: StyleRefinement, +} + +impl Styled for PixelAvatar { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + +impl RenderOnce for PixelAvatar { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let theme = cx.theme(); + let pattern = pattern(self.seed); + let mut cells = Vec::new(); + + let hue = self.seed as f32 / u64::MAX as f32; + let main = theme.blue.hue(hue); + + let shade = if theme.is_dark() { + main.lightness((main.l * 1.6).min(0.95)) + } else { + main.lightness((main.l * 0.45).max(0.18)) + }; + + for row in 0..GRID_SIZE { + for col in 0..GRID_SIZE { + let value = pattern[row * GRID_SIZE + col]; + if value != 0 { + let color = if value == 2 { shade } else { main }; + cells.push( + div() + .row_start(row as i16 + 1) + .row_end(row as i16 + 2) + .col_start(col as i16 + 1) + .col_end(col as i16 + 2) + .bg(color), + ); + } + } + } + + div() + .grid() + .grid_cols(GRID_SIZE as u16) + .grid_rows(GRID_SIZE as u16) + .size(self.side) .flex_shrink_0() - .rounded(cx.theme().radius) + .rounded(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) + .bg(main.opacity(0.16)) + .children(cells) .refine_style(&self.style) } } + +/// The rendered side length of an avatar at `size`. +fn side_length(size: Size) -> Pixels { + match size { + Size::XSmall => px(16.), + Size::Small => px(24.), + Size::Medium => px(48.), + Size::Large => px(80.), + Size::Size(size) => size, + } +} + +fn pattern(seed: u64) -> [u8; GRID_SIZE * GRID_SIZE] { + let mut rng = PixelRng::new(seed); + let mut pattern = [0u8; GRID_SIZE * GRID_SIZE]; + let mut filled = 0usize; + + // Only the inner rows and the inner left half are candidates; mirroring + // 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) { + let accent = rng.chance(ACCENT_PROBABILITY); + set_cell(&mut pattern, row, col, if accent { 2 } else { 1 }); + filled += 1; + } + } + } + + if filled < MIN_FILLED { + let total = art_rows * art_columns; + let start = (rng.next() % total as u64) as usize; + + for offset in 0..total { + if filled >= MIN_FILLED { + break; + } + + let ix = (start + offset) % total; + let row = MARGIN + ix / art_columns; + let col = MARGIN + ix % art_columns; + + if pattern[row * GRID_SIZE + col] == 0 { + set_cell(&mut pattern, row, col, 1); + filled += 1; + } + } + } + + pattern +} + +/// Fill `cell (row, col)` and its horizontal mirror. +fn set_cell(pattern: &mut [u8; GRID_SIZE * GRID_SIZE], row: usize, col: usize, value: u8) { + pattern[row * GRID_SIZE + col] = value; + pattern[row * GRID_SIZE + (GRID_SIZE - 1 - col)] = value; +} + +/// FNV-1a 64-bit hash, stable across platforms and runs. +fn fnv1a(bytes: &[u8]) -> u64 { + let mut hash = 0xcbf2_9ce4_8422_2325u64; + for &byte in bytes { + hash ^= byte as u64; + hash = hash.wrapping_mul(0x0000_0100_0000_01b3); + } + hash +} + +/// Tiny xorshift64* PRNG for deriving the pattern from the seed. +struct PixelRng(u64); + +impl PixelRng { + fn new(seed: u64) -> Self { + Self(seed.max(1)) + } + + fn next(&mut self) -> u64 { + let mut x = self.0; + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + self.0 = x; + x.wrapping_mul(0x2545_f491_4f6c_dd1d) + } + + fn chance(&mut self, probability: f32) -> bool { + self.next() as f32 / (u64::MAX as f32) < probability + } +} diff --git a/crates/signed_ui/src/lib.rs b/crates/signed_ui/src/lib.rs index e3ab674..905a8e7 100644 --- a/crates/signed_ui/src/lib.rs +++ b/crates/signed_ui/src/lib.rs @@ -1,7 +1,6 @@ mod avatar; mod dropdown_button; mod nav_item; -mod pixel_avatar; mod placeholder; mod ref_selector; mod segment_button; @@ -16,7 +15,6 @@ pub use avatar::Avatar; pub use copy_row::{copy_row, menu_copy_row}; pub use dropdown_button::DropdownButton; pub use nav_item::NavItem; -pub use pixel_avatar::PixelAvatar; pub use placeholder::placeholder; pub use ref_selector::ref_selector_trigger; pub use segment_button::{CountBadge, SegmentButton}; diff --git a/crates/signed_ui/src/pixel_avatar.rs b/crates/signed_ui/src/pixel_avatar.rs deleted file mode 100644 index 22dad72..0000000 --- a/crates/signed_ui/src/pixel_avatar.rs +++ /dev/null @@ -1,219 +0,0 @@ -use gpui::prelude::*; -use gpui::{App, Pixels, StyleRefinement, Window, div, px}; -use gpui_base::StyledExt; -use gpui_component::{ActiveTheme, Colorize, Sizable, Size}; - -/// Number of rows and columns in the pixel grid. -const GRID_SIZE: usize = 8; -/// 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; -/// Probability that a filled cell uses the accent shade instead of the main color. -const ACCENT_PROBABILITY: f32 = 0.25; -/// Minimum number of filled left-half cells. -const MIN_FILLED: usize = 5; - -/// A deterministic, offline pixel-art avatar. -#[derive(IntoElement)] -pub struct PixelAvatar { - seed: u64, - size: Size, - style: StyleRefinement, -} - -impl PixelAvatar { - /// Create an avatar seeded from `seed`. - pub fn new(seed: impl AsRef) -> Self { - Self { - seed: fnv1a(seed.as_ref().as_bytes()), - size: Size::Small, - style: StyleRefinement::default(), - } - } -} - -impl Sizable for PixelAvatar { - fn with_size(mut self, size: impl Into) -> Self { - self.size = size.into(); - self - } -} - -impl Styled for PixelAvatar { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.style - } -} - -impl RenderOnce for PixelAvatar { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let theme = cx.theme(); - let pattern = pattern(self.seed); - let mut cells = Vec::new(); - - let hue = self.seed as f32 / u64::MAX as f32; - let main = theme.blue.hue(hue); - - let shade = if theme.is_dark() { - main.lightness((main.l * 1.6).min(0.95)) - } else { - main.lightness((main.l * 0.45).max(0.18)) - }; - - for row in 0..GRID_SIZE { - for col in 0..GRID_SIZE { - let value = pattern[row * GRID_SIZE + col]; - if value != 0 { - let color = if value == 2 { shade } else { main }; - cells.push( - div() - .row_start(row as i16 + 1) - .row_end(row as i16 + 2) - .col_start(col as i16 + 1) - .col_end(col as i16 + 2) - .bg(color), - ); - } - } - } - - div() - .grid() - .grid_cols(GRID_SIZE as u16) - .grid_rows(GRID_SIZE as u16) - .size(side_length(self.size)) - .flex_shrink_0() - .rounded(theme.radius) - .overflow_hidden() - .bg(main.opacity(0.16)) - .children(cells) - .refine_style(&self.style) - } -} - -/// The rendered side length of an avatar at `size`, shared with [`Avatar`]. -pub(crate) fn side_length(size: Size) -> Pixels { - match size { - Size::XSmall => px(16.), - Size::Small => px(24.), - Size::Medium => px(48.), - Size::Large => px(80.), - Size::Size(size) => size, - } -} - -fn pattern(seed: u64) -> [u8; GRID_SIZE * GRID_SIZE] { - let mut rng = PixelRng::new(seed); - let mut pattern = [0u8; GRID_SIZE * GRID_SIZE]; - let mut filled = 0usize; - - // Only the inner rows and the inner left half are candidates; mirroring - // 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) { - let accent = rng.chance(ACCENT_PROBABILITY); - set_cell(&mut pattern, row, col, if accent { 2 } else { 1 }); - filled += 1; - } - } - } - - if filled < MIN_FILLED { - let total = art_rows * art_columns; - let start = (rng.next() % total as u64) as usize; - - for offset in 0..total { - if filled >= MIN_FILLED { - break; - } - - let ix = (start + offset) % total; - let row = MARGIN + ix / art_columns; - let col = MARGIN + ix % art_columns; - - if pattern[row * GRID_SIZE + col] == 0 { - set_cell(&mut pattern, row, col, 1); - filled += 1; - } - } - } - - pattern -} - -/// Fill `cell (row, col)` and its horizontal mirror. -fn set_cell(pattern: &mut [u8; GRID_SIZE * GRID_SIZE], row: usize, col: usize, value: u8) { - pattern[row * GRID_SIZE + col] = value; - pattern[row * GRID_SIZE + (GRID_SIZE - 1 - col)] = value; -} - -/// FNV-1a 64-bit hash, stable across platforms and runs. -fn fnv1a(bytes: &[u8]) -> u64 { - let mut hash = 0xcbf2_9ce4_8422_2325u64; - for &byte in bytes { - hash ^= byte as u64; - hash = hash.wrapping_mul(0x0000_0100_0000_01b3); - } - hash -} - -/// Tiny xorshift64* PRNG for deriving the pattern from the seed. -struct PixelRng(u64); - -impl PixelRng { - fn new(seed: u64) -> Self { - Self(seed.max(1)) - } - - fn next(&mut self) -> u64 { - let mut x = self.0; - x ^= x >> 12; - x ^= x << 25; - x ^= x >> 27; - self.0 = x; - x.wrapping_mul(0x2545_f491_4f6c_dd1d) - } - - fn chance(&mut self, probability: f32) -> bool { - self.next() as f32 / (u64::MAX as f32) < probability - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn count_filled(pattern: &[u8; GRID_SIZE * GRID_SIZE]) -> usize { - pattern.iter().filter(|&&cell| cell != 0).count() - } - - #[test] - fn pattern_properties() { - for seed in 0..50 { - let pattern = pattern(seed); - assert!( - count_filled(&pattern) >= MIN_FILLED * 2, - "pattern too sparse for seed {seed}" - ); - for row in 0..GRID_SIZE { - for col in 0..GRID_SIZE { - assert_eq!( - pattern[row * GRID_SIZE + col], - pattern[row * GRID_SIZE + (GRID_SIZE - 1 - col)], - "asymmetric pattern for seed {seed} at ({row}, {col})" - ); - } - } - } - for seed in [0, 1, 42, u64::MAX] { - assert_eq!(pattern(seed), pattern(seed)); - } - assert_ne!(pattern(42), pattern(43)); - } -} diff --git a/crates/workspace/src/views/discussion.rs b/crates/workspace/src/views/discussion.rs index 771c44e..db1945d 100644 --- a/crates/workspace/src/views/discussion.rs +++ b/crates/workspace/src/views/discussion.rs @@ -71,7 +71,7 @@ pub(crate) fn sidebar_section( h_flex() .gap_1() .items_center() - .child(Avatar::new(name.clone()).picture(picture)) + .child(Avatar::new(*pubkey).name(name.clone()).picture(picture)) .child(div().text_sm().truncate().text_ellipsis().child(name)) .into_any_element() })), @@ -137,7 +137,11 @@ pub(crate) fn comments_section(store: &Entity, root: EventId, cx: &Ap .child( h_flex() .gap_1() - .child(Avatar::new(author.clone()).picture(picture)) + .child( + Avatar::new(comment.pubkey) + .name(author.clone()) + .picture(picture), + ) .child(author), ) .child( diff --git a/crates/workspace/src/views/inbox.rs b/crates/workspace/src/views/inbox.rs index 6761735..dfd69ce 100644 --- a/crates/workspace/src/views/inbox.rs +++ b/crates/workspace/src/views/inbox.rs @@ -544,7 +544,8 @@ fn sub_activity(event: &Event, me: Option, cx: &App) -> AnyElement { .items_center() .text_xs() .child( - Avatar::new(name.clone()) + Avatar::new(event.pubkey) + .name(name.clone()) .picture(profile.picture()) .xsmall(), ) diff --git a/crates/workspace/src/views/issues/detail.rs b/crates/workspace/src/views/issues/detail.rs index c5a7131..e4e5e13 100644 --- a/crates/workspace/src/views/issues/detail.rs +++ b/crates/workspace/src/views/issues/detail.rs @@ -93,7 +93,7 @@ impl Render for IssueDetailView { }; }; - let (title, author, picture, status, age, issue_id, content) = { + let (title, author, picture, status, age, issue_id, content, pubkey) = { let profile_store = ProfileStore::global(cx); let profile = profile_store.read(cx).get(&issue.pubkey); let content = if issue.content.is_empty() { @@ -110,6 +110,7 @@ impl Render for IssueDetailView { relative_time(issue.created_at), issue.id, content, + issue.pubkey, ) }; @@ -153,7 +154,7 @@ impl Render for IssueDetailView { .child( h_flex() .gap_1() - .child(Avatar::new(&author).picture(picture)) + .child(Avatar::new(pubkey).name(author.clone()).picture(picture)) .child(author), ) .child( diff --git a/crates/workspace/src/views/issues/mod.rs b/crates/workspace/src/views/issues/mod.rs index edfba02..db8c869 100644 --- a/crates/workspace/src/views/issues/mod.rs +++ b/crates/workspace/src/views/issues/mod.rs @@ -189,7 +189,11 @@ impl IssuesView { .child( h_flex() .gap_1() - .child(Avatar::new(author.clone()).picture(picture)) + .child( + Avatar::new(issue.pubkey) + .name(author.clone()) + .picture(picture), + ) .child(div().child(author)), ) .child(SharedString::from("opened")) diff --git a/crates/workspace/src/views/mod.rs b/crates/workspace/src/views/mod.rs index c235cac..e85aed5 100644 --- a/crates/workspace/src/views/mod.rs +++ b/crates/workspace/src/views/mod.rs @@ -12,6 +12,8 @@ pub(crate) mod sidebar; mod status_list; pub(crate) mod tree; +use std::fmt::Display; + use gpui::prelude::*; use gpui::{AnyElement, App, div}; use gpui_component::{Sizable as _, h_flex}; @@ -22,7 +24,7 @@ pub(crate) use repo::{RepoItem, open_repo_item, open_repo_panel}; pub use repo_list::RepoListView; pub use sidebar::SidebarPanel; use signed_state::{ProfileStore, RepoStore}; -use signed_ui::{Avatar, PixelAvatar}; +use signed_ui::Avatar; pub(crate) fn tab_title(avatar: AnyElement, label: impl IntoElement) -> impl IntoElement { h_flex() @@ -32,8 +34,8 @@ pub(crate) fn tab_title(avatar: AnyElement, label: impl IntoElement) -> impl Int .child(label) } -pub(crate) fn panel_avatar(seed: impl AsRef) -> AnyElement { - PixelAvatar::new(seed).xsmall().into_any_element() +pub(crate) fn panel_avatar(seed: impl Display) -> AnyElement { + Avatar::new(seed).xsmall().into_any_element() } pub(crate) fn repo_tab_avatar(store: &RepoStore, cx: &App) -> AnyElement { @@ -55,11 +57,13 @@ pub(crate) fn repo_tab_avatar(store: &RepoStore, cx: &App) -> AnyElement { .as_ref() .map(|announcement| ProfileStore::global(cx).read(cx).get(&announcement.owner)); - match owner.and_then(|profile| profile.picture().map(|picture| (profile.name(), picture))) { - Some((name, picture)) => Avatar::new(name) - .picture(Some(picture)) - .xsmall() - .into_any_element(), - None => PixelAvatar::new(seed).xsmall().into_any_element(), - } + let (name, picture) = owner + .map(|profile| (Some(profile.name()), profile.picture())) + .unwrap_or((None, None)); + + Avatar::new(seed) + .name(name) + .picture(picture) + .xsmall() + .into_any_element() } diff --git a/crates/workspace/src/views/pull_requests/detail.rs b/crates/workspace/src/views/pull_requests/detail.rs index 58b3b1c..6f78b4f 100644 --- a/crates/workspace/src/views/pull_requests/detail.rs +++ b/crates/workspace/src/views/pull_requests/detail.rs @@ -407,7 +407,7 @@ impl PullRequestDetailView { return placeholder(&error, cx); } - let (author, picture, age, root_id) = { + let (author, picture, age, root_id, pubkey) = { let store = self.store.read(cx); let Some(root) = store .pull_requests @@ -422,6 +422,7 @@ impl PullRequestDetailView { profile.picture(), relative_time(root.created_at), root.id, + root.pubkey, ) }; @@ -451,7 +452,8 @@ impl PullRequestDetailView { h_flex() .gap_1() .child( - Avatar::new(author.clone()) + Avatar::new(pubkey) + .name(author.clone()) .picture(picture), ) .child(author), diff --git a/crates/workspace/src/views/pull_requests/mod.rs b/crates/workspace/src/views/pull_requests/mod.rs index beb3a93..a31b1b6 100644 --- a/crates/workspace/src/views/pull_requests/mod.rs +++ b/crates/workspace/src/views/pull_requests/mod.rs @@ -203,7 +203,11 @@ impl PullRequestsView { .child( h_flex() .gap_1() - .child(Avatar::new(author.clone()).picture(picture)) + .child( + Avatar::new(pr.pubkey) + .name(author.clone()) + .picture(picture), + ) .child(div().child(author)), ) .child(SharedString::from("opened")) diff --git a/crates/workspace/src/views/repo/about.rs b/crates/workspace/src/views/repo/about.rs index 145bcdf..b73495f 100644 --- a/crates/workspace/src/views/repo/about.rs +++ b/crates/workspace/src/views/repo/about.rs @@ -174,7 +174,7 @@ fn maintainers(maintainers: &[PublicKey], cx: &App) -> AnyElement { .gap_2() .items_center() .min_w_0() - .child(Avatar::new(name.clone()).picture(picture)) + .child(Avatar::new(*pubkey).name(name.clone()).picture(picture)) .child( div() .flex_1() diff --git a/crates/workspace/src/views/repo/mod.rs b/crates/workspace/src/views/repo/mod.rs index 463fd87..c0f29b1 100644 --- a/crates/workspace/src/views/repo/mod.rs +++ b/crates/workspace/src/views/repo/mod.rs @@ -30,7 +30,7 @@ use signed_state::{ SyncStatusStore, }; use signed_ui::{ - Avatar, CountBadge, DropdownButton, PixelAvatar, copy_row, menu_copy_row, ref_selector_trigger, + Avatar, CountBadge, DropdownButton, copy_row, menu_copy_row, ref_selector_trigger, }; use utils::middle_truncate; @@ -896,7 +896,7 @@ impl RepoDetailView { let name = repo_display_name(self.store.read(cx)); let description = announcement.description(); - let avatar = PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id)); + let avatar = Avatar::new(format!("{}:{}", announcement.owner, announcement.id)); v_flex() .on_action( @@ -1253,7 +1253,7 @@ impl RepoDetailView { .as_ref() .map(|path| path.display().to_string()) .unwrap_or_default(); - let avatar = PixelAvatar::new(path.clone()); + let avatar = Avatar::new(path.clone()); // A repository already bound to a coordinate is not offered for publishing again. let bound = self.store.read(cx).nip34.clone(); @@ -1481,7 +1481,9 @@ impl RepoDetailView { .child( h_flex() .gap_1() - .child(Avatar::new(owner_name.clone()).picture(owner_picture)) + .child(Avatar::new(announcement.owner) + .name(owner_name.clone()) + .picture(owner_picture)) .child(div().text_xs().whitespace_nowrap().child(owner_name)), ) .when(!rest.is_empty(), |this| { @@ -2180,12 +2182,17 @@ fn bound_repo_label(binding: &Nip34Binding, cx: &App) -> AnyElement { .child("Initialized by") .text_color(cx.theme().muted_foreground) .when_some(profile, |this, profile| { + let name = profile.name(); this.child( h_flex() .gap_1() .text_color(cx.theme().foreground) - .child(Avatar::new(profile.name()).picture(profile.picture())) - .child(profile.name()), + .child( + Avatar::new(profile.public_key()) + .name(name.clone()) + .picture(profile.picture()), + ) + .child(name), ) }) .when_some(binding.identifier.as_deref(), |this, ident| { diff --git a/crates/workspace/src/views/repo_list.rs b/crates/workspace/src/views/repo_list.rs index 4c33393..17bfc46 100644 --- a/crates/workspace/src/views/repo_list.rs +++ b/crates/workspace/src/views/repo_list.rs @@ -274,7 +274,11 @@ impl RepoListView { h_flex() .gap_2() .items_center() - .child(Avatar::new(owner.name()).picture(owner.picture())) + .child( + Avatar::new(announcement.owner) + .name(owner.name()) + .picture(owner.picture()), + ) .child( div() .text_xs() diff --git a/crates/workspace/src/views/sidebar/mod.rs b/crates/workspace/src/views/sidebar/mod.rs index 4b0165f..ea71ee4 100644 --- a/crates/workspace/src/views/sidebar/mod.rs +++ b/crates/workspace/src/views/sidebar/mod.rs @@ -26,7 +26,7 @@ use signed_state::{ Backend, BackendEvent, CheckoutsStore, LocalReposStore, Nip34Binding, Nip34Kind, Profile, ProfileStore, RepoListStore, ResolvedLocalRepo, SyncStatusStore, }; -use signed_ui::{Avatar, NavItem, PixelAvatar, title_bar_drag_handlers}; +use signed_ui::{Avatar, NavItem, title_bar_drag_handlers}; use super::{InboxView, PlaceholderPanel, RepoDetailView, RepoListView, open_repo_panel}; @@ -510,8 +510,7 @@ impl SidebarPanel { ) -> impl IntoElement { let name = announcement.name().map(SharedString::from); // The sidebar rows are compact, so the avatar matches the 16px nav icons. - let avatar = - PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id)).xsmall(); + let avatar = Avatar::new(format!("{}:{}", announcement.owner, announcement.id)).xsmall(); let announcement = announcement.clone(); let addr = announcement.addr(); @@ -636,7 +635,11 @@ impl SidebarPanel { .child( h_flex() .gap_1() - .child(Avatar::new(name.clone()).picture(picture)) + .child( + Avatar::new(profile.public_key()) + .name(name.clone()) + .picture(picture), + ) .child(div().text_xs().font_semibold().child(name)), ) .dropdown_menu(move |menu, _, _| { @@ -768,7 +771,7 @@ pub(super) fn server_host(relay: &RelayUrl) -> SharedString { /// The repository's pixel avatar, with the bound owner's avatar at its bottom right. fn local_avatar(entry: &ResolvedLocalRepo, cx: &App) -> AnyElement { // Matches the other sidebar rows, and the badge below sits at its corner. - let avatar = PixelAvatar::new(entry.path.to_string_lossy()).xsmall(); + let avatar = Avatar::new(entry.path.to_string_lossy()).xsmall(); let Some(owner) = entry.nip34.as_ref().and_then(|binding| binding.owner) else { return avatar.into_any_element(); @@ -782,7 +785,8 @@ fn local_avatar(entry: &ResolvedLocalRepo, cx: &App) -> AnyElement { .child(avatar) .child( div().absolute().bottom_neg_0p5().right_neg_0p5().child( - Avatar::new(profile.name()) + Avatar::new(owner) + .name(profile.name()) .picture(profile.picture()) .size(px(14.)), ),