commit view: Reuse avatar asset (#44554)

Agus Zubiaga created

Change summary

crates/git_ui/src/blame_ui.rs       | 12 ++-
crates/git_ui/src/commit_tooltip.rs | 46 +++++++++++------
crates/git_ui/src/commit_view.rs    | 80 +++++-------------------------
3 files changed, 51 insertions(+), 87 deletions(-)

Detailed changes

crates/git_ui/src/blame_ui.rs 🔗

@@ -47,11 +47,13 @@ impl BlameRenderer for GitBlameRenderer {
         let name = util::truncate_and_trailoff(author_name, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED);
 
         let avatar = if ProjectSettings::get_global(cx).git.blame.show_avatar {
-            CommitAvatar::new(
-                &blame_entry.sha.to_string().into(),
-                details.as_ref().and_then(|it| it.remote.as_ref()),
+            Some(
+                CommitAvatar::new(
+                    &blame_entry.sha.to_string().into(),
+                    details.as_ref().and_then(|it| it.remote.as_ref()),
+                )
+                .render(window, cx),
             )
-            .render(window, cx)
         } else {
             None
         };
@@ -264,7 +266,7 @@ impl BlameRenderer for GitBlameRenderer {
                                     .flex_wrap()
                                     .border_b_1()
                                     .border_color(cx.theme().colors().border_variant)
-                                    .children(avatar)
+                                    .child(avatar)
                                     .child(author)
                                     .when(!author_email.is_empty(), |this| {
                                         this.child(

crates/git_ui/src/commit_tooltip.rs 🔗

@@ -29,11 +29,16 @@ pub struct CommitDetails {
 pub struct CommitAvatar<'a> {
     sha: &'a SharedString,
     remote: Option<&'a GitRemote>,
+    size: Option<IconSize>,
 }
 
 impl<'a> CommitAvatar<'a> {
     pub fn new(sha: &'a SharedString, remote: Option<&'a GitRemote>) -> Self {
-        Self { sha, remote }
+        Self {
+            sha,
+            remote,
+            size: None,
+        }
     }
 
     pub fn from_commit_details(details: &'a CommitDetails) -> Self {
@@ -43,28 +48,37 @@ impl<'a> CommitAvatar<'a> {
                 .message
                 .as_ref()
                 .and_then(|details| details.remote.as_ref()),
+            size: None,
         }
     }
-}
 
-impl<'a> CommitAvatar<'a> {
-    pub fn render(&'a self, window: &mut Window, cx: &mut App) -> Option<impl IntoElement + use<>> {
+    pub fn size(mut self, size: IconSize) -> Self {
+        self.size = Some(size);
+        self
+    }
+
+    pub fn render(&'a self, window: &mut Window, cx: &mut App) -> AnyElement {
+        match self.avatar(window, cx) {
+            // Loading or no avatar found
+            None => Icon::new(IconName::Person)
+                .color(Color::Muted)
+                .when_some(self.size, |this, size| this.size(size))
+                .into_any_element(),
+            // Found
+            Some(avatar) => avatar
+                .when_some(self.size, |this, size| this.size(size.rems()))
+                .into_any_element(),
+        }
+    }
+
+    pub fn avatar(&'a self, window: &mut Window, cx: &mut App) -> Option<Avatar> {
         let remote = self
             .remote
             .filter(|remote| remote.host_supports_avatars())?;
-
         let avatar_url = CommitAvatarAsset::new(remote.clone(), self.sha.clone());
 
-        let element = match window.use_asset::<CommitAvatarAsset>(&avatar_url, cx) {
-            // Loading or no avatar found
-            None | Some(None) => Icon::new(IconName::Person)
-                .color(Color::Muted)
-                .into_element()
-                .into_any(),
-            // Found
-            Some(Some(url)) => Avatar::new(url.to_string()).into_element().into_any(),
-        };
-        Some(element)
+        let url = window.use_asset::<CommitAvatarAsset>(&avatar_url, cx)??;
+        Some(Avatar::new(url.to_string()))
     }
 }
 
@@ -253,7 +267,7 @@ impl Render for CommitTooltip {
                                 .gap_x_2()
                                 .overflow_x_hidden()
                                 .flex_wrap()
-                                .children(avatar)
+                                .child(avatar)
                                 .child(author)
                                 .when(!author_email.is_empty(), |this| {
                                     this.child(

crates/git_ui/src/commit_view.rs 🔗

@@ -5,8 +5,8 @@ use editor::{Editor, EditorEvent, ExcerptRange, MultiBuffer, multibuffer_context
 use git::repository::{CommitDetails, CommitDiff, RepoPath};
 use git::{GitHostingProviderRegistry, GitRemote, parse_git_remote_url};
 use gpui::{
-    AnyElement, App, AppContext as _, Asset, AsyncApp, AsyncWindowContext, Context, Element,
-    Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,
+    AnyElement, App, AppContext as _, AsyncApp, AsyncWindowContext, Context, Element, Entity,
+    EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,
     PromptLevel, Render, Styled, Task, WeakEntity, Window, actions,
 };
 use language::{
@@ -21,7 +21,7 @@ use std::{
     sync::Arc,
 };
 use theme::ActiveTheme;
-use ui::{Avatar, DiffStat, Tooltip, prelude::*};
+use ui::{DiffStat, Tooltip, prelude::*};
 use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};
 use workspace::item::TabTooltipContent;
 use workspace::{
@@ -33,6 +33,7 @@ use workspace::{
     searchable::SearchableItemHandle,
 };
 
+use crate::commit_tooltip::CommitAvatar;
 use crate::git_panel::GitPanel;
 
 actions!(git, [ApplyCurrentStash, PopCurrentStash, DropCurrentStash,]);
@@ -318,17 +319,7 @@ impl CommitView {
         cx: &mut App,
     ) -> AnyElement {
         let size = size.into();
-        let remote = self.remote.as_ref().filter(|r| r.host_supports_avatars());
-
-        if let Some(remote) = remote {
-            let avatar_asset = CommitAvatarAsset::new(remote.clone(), sha.clone());
-            if let Some(Some(url)) = window.use_asset::<CommitAvatarAsset>(&avatar_asset, cx) {
-                return Avatar::new(url.to_string())
-                    .size(size)
-                    .into_element()
-                    .into_any();
-            }
-        }
+        let avatar = CommitAvatar::new(sha, self.remote.as_ref());
 
         v_flex()
             .w(size)
@@ -339,10 +330,15 @@ impl CommitView {
             .justify_center()
             .items_center()
             .child(
-                Icon::new(IconName::Person)
-                    .color(Color::Muted)
-                    .size(IconSize::Medium)
-                    .into_element(),
+                avatar
+                    .avatar(window, cx)
+                    .map(|a| a.size(size).into_any_element())
+                    .unwrap_or_else(|| {
+                        Icon::new(IconName::Person)
+                            .color(Color::Muted)
+                            .size(IconSize::Medium)
+                            .into_any_element()
+                    }),
             )
             .into_any()
     }
@@ -647,54 +643,6 @@ impl CommitView {
     }
 }
 
-#[derive(Clone, Debug)]
-struct CommitAvatarAsset {
-    sha: SharedString,
-    remote: GitRemote,
-}
-
-impl std::hash::Hash for CommitAvatarAsset {
-    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-        self.sha.hash(state);
-        self.remote.host.name().hash(state);
-    }
-}
-
-impl CommitAvatarAsset {
-    fn new(remote: GitRemote, sha: SharedString) -> Self {
-        Self { remote, sha }
-    }
-}
-
-impl Asset for CommitAvatarAsset {
-    type Source = Self;
-    type Output = Option<SharedString>;
-
-    fn load(
-        source: Self::Source,
-        cx: &mut App,
-    ) -> impl Future<Output = Self::Output> + Send + 'static {
-        let client = cx.http_client();
-        async move {
-            match source
-                .remote
-                .host
-                .commit_author_avatar_url(
-                    &source.remote.owner,
-                    &source.remote.repo,
-                    source.sha.clone(),
-                    client,
-                )
-                .await
-            {
-                Ok(Some(url)) => Some(SharedString::from(url.to_string())),
-                Ok(None) => None,
-                Err(_) => None,
-            }
-        }
-    }
-}
-
 impl language::File for GitBlob {
     fn as_local(&self) -> Option<&dyn language::LocalFile> {
         None