Start changing Avatar to use URI

Julia and Antonio Scandurra created

Co-Authored-By: Antonio Scandurra <antonio@zed.dev>

Change summary

crates/channel2/src/channel_store_tests.rs           |   2 
crates/client2/src/test.rs                           |   4 
crates/client2/src/user.rs                           |  32 ++--
crates/collab2/src/tests/integration_tests.rs        |   4 
crates/collab2/src/tests/test_server.rs              |   2 
crates/collab_ui2/src/chat_panel.rs                  |  10 -
crates/collab_ui2/src/chat_panel/message_editor.rs   |   6 
crates/collab_ui2/src/collab_panel.rs                |  85 +++++-----
crates/collab_ui2/src/collab_panel/contact_finder.rs |   4 
crates/collab_ui2/src/collab_titlebar_item.rs        | 112 ++++++-------
crates/collab_ui2/src/face_pile.rs                   |  13 
crates/collab_ui2/src/notification_panel.rs          |  83 +++++----
crates/project2/src/project2.rs                      |   2 
crates/ui2/src/components/avatar.rs                  |  12 -
crates/ui2/src/components/list/list_item.rs          |   2 
crates/ui2/src/components/stories/avatar.rs          |   8 
crates/workspace2/src/workspace2.rs                  |   2 
crates/zed2/src/main.rs                              |   2 
crates/zed2/src/zed2.rs                              |   8 
19 files changed, 190 insertions(+), 203 deletions(-)

Detailed changes

crates/channel2/src/channel_store_tests.rs 🔗

@@ -345,7 +345,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
 fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
     let http = FakeHttpClient::with_404_response();
     let client = Client::new(http.clone(), cx);
-    let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx));
+    let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx));
 
     let settings_store = SettingsStore::test(cx);
     cx.set_global(settings_store);

crates/client2/src/test.rs 🔗

@@ -8,7 +8,6 @@ use rpc::{
     ConnectionId, Peer, Receipt, TypedEnvelope,
 };
 use std::sync::Arc;
-use util::http::FakeHttpClient;
 
 pub struct FakeServer {
     peer: Arc<Peer>,
@@ -195,8 +194,7 @@ impl FakeServer {
         client: Arc<Client>,
         cx: &mut TestAppContext,
     ) -> Model<UserStore> {
-        let http_client = FakeHttpClient::with_404_response();
-        let user_store = cx.build_model(|cx| UserStore::new(client, http_client, cx));
+        let user_store = cx.build_model(|cx| UserStore::new(client, cx));
         assert_eq!(
             self.receive::<proto::GetUsers>()
                 .await

crates/client2/src/user.rs 🔗

@@ -2,8 +2,8 @@ use super::{proto, Client, Status, TypedEnvelope};
 use anyhow::{anyhow, Context, Result};
 use collections::{hash_map::Entry, HashMap, HashSet};
 use feature_flags::FeatureFlagAppExt;
-use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
-use gpui::{AsyncAppContext, EventEmitter, ImageData, Model, ModelContext, Task};
+use futures::{channel::mpsc, AsyncReadExt, Future, StreamExt};
+use gpui::{AsyncAppContext, EventEmitter, ImageData, Model, ModelContext, SharedString, Task};
 use postage::{sink::Sink, watch};
 use rpc::proto::{RequestMessage, UsersResponse};
 use std::sync::{Arc, Weak};
@@ -20,7 +20,7 @@ pub struct ParticipantIndex(pub u32);
 pub struct User {
     pub id: UserId,
     pub github_login: String,
-    pub avatar: Option<Arc<ImageData>>,
+    pub avatar_uri: SharedString,
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -76,7 +76,6 @@ pub struct UserStore {
     pending_contact_requests: HashMap<u64, usize>,
     invite_info: Option<InviteInfo>,
     client: Weak<Client>,
-    http: Arc<dyn HttpClient>,
     _maintain_contacts: Task<()>,
     _maintain_current_user: Task<Result<()>>,
 }
@@ -114,7 +113,6 @@ enum UpdateContacts {
 impl UserStore {
     pub fn new(
         client: Arc<Client>,
-        http: Arc<dyn HttpClient>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
         let (mut current_user_tx, current_user_rx) = watch::channel();
@@ -134,7 +132,6 @@ impl UserStore {
             invite_info: None,
             client: Arc::downgrade(&client),
             update_contacts_tx,
-            http,
             _maintain_contacts: cx.spawn(|this, mut cx| async move {
                 let _subscriptions = rpc_subscriptions;
                 while let Some(message) = update_contacts_rx.next().await {
@@ -445,6 +442,12 @@ impl UserStore {
         self.perform_contact_request(user_id, proto::RemoveContact { user_id }, cx)
     }
 
+    pub fn has_incoming_contact_request(&self, user_id: u64) -> bool {
+        self.incoming_contact_requests
+            .iter()
+            .any(|user| user.id == user_id)
+    }
+
     pub fn respond_to_contact_request(
         &mut self,
         requester_id: u64,
@@ -616,17 +619,14 @@ impl UserStore {
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<Arc<User>>>> {
         let client = self.client.clone();
-        let http = self.http.clone();
         cx.spawn(|this, mut cx| async move {
             if let Some(rpc) = client.upgrade() {
                 let response = rpc.request(request).await.context("error loading users")?;
-                let users = future::join_all(
-                    response
-                        .users
-                        .into_iter()
-                        .map(|user| User::new(user, http.as_ref())),
-                )
-                .await;
+                let users = response
+                    .users
+                    .into_iter()
+                    .map(|user| User::new(user))
+                    .collect::<Vec<_>>();
 
                 this.update(&mut cx, |this, _| {
                     for user in &users {
@@ -659,11 +659,11 @@ impl UserStore {
 }
 
 impl User {
-    async fn new(message: proto::User, http: &dyn HttpClient) -> Arc<Self> {
+    fn new(message: proto::User) -> Arc<Self> {
         Arc::new(User {
             id: message.id,
             github_login: message.github_login,
-            avatar: fetch_avatar(http, &message.avatar_url).warn_on_err().await,
+            avatar_uri: message.avatar_url.into(),
         })
     }
 }

crates/collab2/src/tests/integration_tests.rs 🔗

@@ -1823,7 +1823,7 @@ async fn test_active_call_events(
             owner: Arc::new(User {
                 id: client_a.user_id().unwrap(),
                 github_login: "user_a".to_string(),
-                avatar: None,
+                avatar_uri: "avatar_a".into(),
             }),
             project_id: project_a_id,
             worktree_root_names: vec!["a".to_string()],
@@ -1841,7 +1841,7 @@ async fn test_active_call_events(
             owner: Arc::new(User {
                 id: client_b.user_id().unwrap(),
                 github_login: "user_b".to_string(),
-                avatar: None,
+                avatar_uri: "avatar_b".into(),
             }),
             project_id: project_b_id,
             worktree_root_names: vec!["b".to_string()]

crates/collab2/src/tests/test_server.rs 🔗

@@ -209,7 +209,7 @@ impl TestServer {
             });
 
         let fs = FakeFs::new(cx.executor());
-        let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx));
+        let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx));
         let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx));
         let mut language_registry = LanguageRegistry::test();
         language_registry.set_executor(cx.executor());

crates/collab_ui2/src/chat_panel.rs 🔗

@@ -364,13 +364,7 @@ impl ChatPanel {
         if !is_continuation {
             result = result.child(
                 h_stack()
-                    .children(
-                        message
-                            .sender
-                            .avatar
-                            .clone()
-                            .map(|avatar| Avatar::data(avatar)),
-                    )
+                    .child(Avatar::new(message.sender.avatar_uri.clone()))
                     .child(Label::new(message.sender.github_login.clone()))
                     .child(Label::new(format_timestamp(
                         message.timestamp,
@@ -659,7 +653,7 @@ mod tests {
             timestamp: OffsetDateTime::now_utc(),
             sender: Arc::new(client::User {
                 github_login: "fgh".into(),
-                avatar: None,
+                avatar_uri: "avatar_fgh".into(),
                 id: 103,
             }),
             nonce: 5,

crates/collab_ui2/src/chat_panel/message_editor.rs 🔗

@@ -234,7 +234,7 @@ mod tests {
                         user: Arc::new(User {
                             github_login: "a-b".into(),
                             id: 101,
-                            avatar: None,
+                            avatar_uri: "avatar_a-b".into(),
                         }),
                         kind: proto::channel_member::Kind::Member,
                         role: proto::ChannelRole::Member,
@@ -243,7 +243,7 @@ mod tests {
                         user: Arc::new(User {
                             github_login: "C_D".into(),
                             id: 102,
-                            avatar: None,
+                            avatar_uri: "avatar_C_D".into(),
                         }),
                         kind: proto::channel_member::Kind::Member,
                         role: proto::ChannelRole::Member,
@@ -275,7 +275,7 @@ mod tests {
         cx.update(|cx| {
             let http = FakeHttpClient::with_404_response();
             let client = Client::new(http.clone(), cx);
-            let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx));
+            let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx));
             let settings = SettingsStore::test(cx);
             cx.set_global(settings);
             theme::init(theme::LoadThemes::JustBase, cx);

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -1155,7 +1155,7 @@ impl CollabPanel {
         let tooltip = format!("Follow {}", user.github_login);
 
         ListItem::new(SharedString::from(user.github_login.clone()))
-            .left_child(Avatar::data(user.avatar.clone().unwrap()))
+            .left_child(Avatar::new(user.avatar_uri.clone()))
             .child(
                 h_stack()
                     .w_full()
@@ -2365,44 +2365,45 @@ impl CollabPanel {
         let busy = contact.busy || calling;
         let user_id = contact.user.id;
         let github_login = SharedString::from(contact.user.github_login.clone());
-        let mut item = ListItem::new(github_login.clone())
-            .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
-            .child(
-                h_stack()
-                    .w_full()
-                    .justify_between()
-                    .child(Label::new(github_login.clone()))
-                    .when(calling, |el| {
-                        el.child(Label::new("Calling").color(Color::Muted))
-                    })
-                    .when(!calling, |el| {
-                        el.child(
-                            div()
-                                .id("remove_contact")
-                                .invisible()
-                                .group_hover("", |style| style.visible())
-                                .child(
-                                    IconButton::new("remove_contact", Icon::Close)
-                                        .icon_color(Color::Muted)
-                                        .tooltip(|cx| Tooltip::text("Remove Contact", cx))
-                                        .on_click(cx.listener({
-                                            let github_login = github_login.clone();
-                                            move |this, _, cx| {
-                                                this.remove_contact(user_id, &github_login, cx);
-                                            }
-                                        })),
-                                ),
-                        )
-                    }),
-            )
-            .left_child(
-                // todo!() handle contacts with no avatar
-                Avatar::data(contact.user.avatar.clone().unwrap())
-                    .availability_indicator(if online { Some(!busy) } else { None }),
-            )
-            .when(online && !busy, |el| {
-                el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
-            });
+        let mut item =
+            ListItem::new(github_login.clone())
+                .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
+                .child(
+                    h_stack()
+                        .w_full()
+                        .justify_between()
+                        .child(Label::new(github_login.clone()))
+                        .when(calling, |el| {
+                            el.child(Label::new("Calling").color(Color::Muted))
+                        })
+                        .when(!calling, |el| {
+                            el.child(
+                                div()
+                                    .id("remove_contact")
+                                    .invisible()
+                                    .group_hover("", |style| style.visible())
+                                    .child(
+                                        IconButton::new("remove_contact", Icon::Close)
+                                            .icon_color(Color::Muted)
+                                            .tooltip(|cx| Tooltip::text("Remove Contact", cx))
+                                            .on_click(cx.listener({
+                                                let github_login = github_login.clone();
+                                                move |this, _, cx| {
+                                                    this.remove_contact(user_id, &github_login, cx);
+                                                }
+                                            })),
+                                    ),
+                            )
+                        }),
+                )
+                .left_child(
+                    // todo!() handle contacts with no avatar
+                    Avatar::new(contact.user.avatar_uri.clone())
+                        .availability_indicator(if online { Some(!busy) } else { None }),
+                )
+                .when(online && !busy, |el| {
+                    el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
+                });
 
         div()
             .id(github_login.clone())
@@ -2474,7 +2475,7 @@ impl CollabPanel {
                     .child(Label::new(github_login.clone()))
                     .child(h_stack().children(controls)),
             )
-            .when_some(user.avatar.clone(), |el, avatar| el.left_avatar(avatar))
+            .left_avatar(user.avatar_uri.clone())
     }
 
     fn render_contact_placeholder(
@@ -2532,7 +2533,9 @@ impl CollabPanel {
             let result = FacePile {
                 faces: participants
                     .iter()
-                    .filter_map(|user| Some(Avatar::data(user.avatar.clone()?).into_any_element()))
+                    .filter_map(|user| {
+                        Some(Avatar::new(user.avatar_uri.clone()).into_any_element())
+                    })
                     .take(FACEPILE_LIMIT)
                     .chain(if extra_count > 0 {
                         // todo!() @nate - this label looks wrong.

crates/collab_ui2/src/collab_panel/contact_finder.rs 🔗

@@ -7,7 +7,7 @@ use gpui::{
 use picker::{Picker, PickerDelegate};
 use std::sync::Arc;
 use theme::ActiveTheme as _;
-use ui::prelude::*;
+use ui::{prelude::*, Avatar};
 use util::{ResultExt as _, TryFutureExt};
 use workspace::ModalView;
 
@@ -187,7 +187,7 @@ impl PickerDelegate for ContactFinderDelegate {
             div()
                 .flex_1()
                 .justify_between()
-                .children(user.avatar.clone().map(|avatar| img(avatar)))
+                .child(Avatar::new(user.avatar_uri.clone()))
                 .child(Label::new(user.github_login.clone()))
                 .children(icon_path.map(|icon_path| svg().path(icon_path))),
         )

crates/collab_ui2/src/collab_titlebar_item.rs 🔗

@@ -232,43 +232,41 @@ impl Render for CollabTitlebarItem {
                     })
                     .child(h_stack().px_1p5().map(|this| {
                         if let Some(user) = current_user {
-                            this.when_some(user.avatar.clone(), |this, avatar| {
-                                // TODO: Finish implementing user menu popover
-                                //
-                                this.child(
-                                    popover_menu("user-menu")
-                                        .menu(|cx| {
-                                            ContextMenu::build(cx, |menu, _| menu.header("ADADA"))
-                                        })
-                                        .trigger(
-                                            ButtonLike::new("user-menu")
-                                                .child(
-                                                    h_stack()
-                                                        .gap_0p5()
-                                                        .child(Avatar::data(avatar))
-                                                        .child(
-                                                            IconElement::new(Icon::ChevronDown)
-                                                                .color(Color::Muted),
-                                                        ),
-                                                )
-                                                .style(ButtonStyle::Subtle)
-                                                .tooltip(move |cx| {
-                                                    Tooltip::text("Toggle User Menu", cx)
-                                                }),
-                                        )
-                                        .anchor(gpui::AnchorCorner::TopRight),
-                                )
-                                // this.child(
-                                //     ButtonLike::new("user-menu")
-                                //         .child(
-                                //             h_stack().gap_0p5().child(Avatar::data(avatar)).child(
-                                //                 IconElement::new(Icon::ChevronDown).color(Color::Muted),
-                                //             ),
-                                //         )
-                                //         .style(ButtonStyle::Subtle)
-                                //         .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
-                                // )
-                            })
+                            // TODO: Finish implementing user menu popover
+                            //
+                            this.child(
+                                popover_menu("user-menu")
+                                    .menu(|cx| {
+                                        ContextMenu::build(cx, |menu, _| menu.header("ADADA"))
+                                    })
+                                    .trigger(
+                                        ButtonLike::new("user-menu")
+                                            .child(
+                                                h_stack()
+                                                    .gap_0p5()
+                                                    .child(Avatar::new(user.avatar_uri.clone()))
+                                                    .child(
+                                                        IconElement::new(Icon::ChevronDown)
+                                                            .color(Color::Muted),
+                                                    ),
+                                            )
+                                            .style(ButtonStyle::Subtle)
+                                            .tooltip(move |cx| {
+                                                Tooltip::text("Toggle User Menu", cx)
+                                            }),
+                                    )
+                                    .anchor(gpui::AnchorCorner::TopRight),
+                            )
+                            // this.child(
+                            //     ButtonLike::new("user-menu")
+                            //         .child(
+                            //             h_stack().gap_0p5().child(Avatar::data(avatar)).child(
+                            //                 IconElement::new(Icon::ChevronDown).color(Color::Muted),
+                            //             ),
+                            //         )
+                            //         .style(ButtonStyle::Subtle)
+                            //         .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
+                            // )
                         } else {
                             this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| {
                                 let client = client.clone();
@@ -425,26 +423,20 @@ impl CollabTitlebarItem {
     ) -> Option<FacePile> {
         let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id));
         let mut pile = FacePile::default();
-        pile.extend(
-            user.avatar
-                .clone()
-                .map(|avatar| {
-                    div()
-                        .child(
-                            Avatar::data(avatar.clone())
-                                .grayscale(!is_present)
-                                .border_color(if is_speaking {
-                                    gpui::blue()
-                                } else if is_muted {
-                                    gpui::red()
-                                } else {
-                                    Hsla::default()
-                                }),
-                        )
-                        .into_any_element()
-                })
-                .into_iter()
-                .chain(followers.iter().filter_map(|follower_peer_id| {
+        pile.child(
+            div()
+                .child(
+                    Avatar::new(user.avatar_uri.clone())
+                        .grayscale(!is_present)
+                        .border_color(if is_speaking {
+                            gpui::blue()
+                        } else if is_muted {
+                            gpui::red()
+                        } else {
+                            Hsla::default()
+                        }),
+                )
+                .children(followers.iter().filter_map(|follower_peer_id| {
                     let follower = room
                         .remote_participants()
                         .values()
@@ -454,10 +446,8 @@ impl CollabTitlebarItem {
                                 .then_some(current_user)
                         })?
                         .clone();
-                    follower
-                        .avatar
-                        .clone()
-                        .map(|avatar| div().child(Avatar::data(avatar.clone())).into_any_element())
+
+                    Some(div().child(Avatar::new(follower.avatar_uri.clone())))
                 })),
         );
         Some(pile)

crates/collab_ui2/src/face_pile.rs 🔗

@@ -1,11 +1,12 @@
 use gpui::{
-    div, AnyElement, Div, ElementId, IntoElement, ParentElement as _, RenderOnce, Styled,
-    WindowContext,
+    div, AnyElement, Div, ElementId, IntoElement, ParentElement as _, ParentElement, RenderOnce,
+    Styled, WindowContext,
 };
+use smallvec::SmallVec;
 
 #[derive(Default, IntoElement)]
 pub struct FacePile {
-    pub faces: Vec<AnyElement>,
+    pub faces: SmallVec<[AnyElement; 2]>,
 }
 
 impl RenderOnce for FacePile {
@@ -25,8 +26,8 @@ impl RenderOnce for FacePile {
     }
 }
 
-impl Extend<AnyElement> for FacePile {
-    fn extend<T: IntoIterator<Item = AnyElement>>(&mut self, children: T) {
-        self.faces.extend(children);
+impl ParentElement for FacePile {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+        &mut self.faces
     }
 }

crates/collab_ui2/src/notification_panel.rs 🔗

@@ -6,11 +6,11 @@ use collections::HashMap;
 use db::kvp::KEY_VALUE_STORE;
 use futures::StreamExt;
 use gpui::{
-    actions, div, img, serde_json, svg, AnyElement, AnyView, AppContext, AsyncAppContext, Context,
-    CursorStyle, Div, Entity, EventEmitter, Flatten, FocusHandle, FocusableView,
-    InteractiveElement, IntoElement, ListAlignment, ListState, Model, MouseButton, ParentElement,
-    Render, Stateful, StatefulInteractiveElement, Task, View, ViewContext, VisualContext, WeakView,
-    WindowContext,
+    actions, div, img, px, serde_json, svg, AnyElement, AnyView, AppContext, AsyncAppContext,
+    AsyncWindowContext, Context, CursorStyle, Div, Element, Entity, EventEmitter, Flatten,
+    FocusHandle, FocusableView, InteractiveElement, IntoElement, ListAlignment, ListScrollEvent,
+    ListState, Model, MouseButton, ParentElement, Render, Stateful, StatefulInteractiveElement,
+    Task, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
 use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
 use project::Fs;
@@ -80,7 +80,9 @@ impl NotificationPanel {
         let user_store = workspace.app_state().user_store.clone();
         let workspace_handle = workspace.weak_handle();
 
-        cx.build_view(|cx| {
+        cx.build_view(|cx: &mut ViewContext<Self>| {
+            let view = cx.view().clone();
+
             let mut status = client.status();
             cx.spawn(|this, mut cx| async move {
                 while let Some(_) = status.next().await {
@@ -97,25 +99,30 @@ impl NotificationPanel {
             .detach();
 
             let mut notification_list =
-                ListState::new(0, ListAlignment::Top, 1000., move |this, ix, cx| {
-                    this.render_notification(ix, cx).unwrap_or_else(|| div())
+                ListState::new(0, ListAlignment::Top, px(1000.), move |ix, cx| {
+                    view.update(cx, |this, cx| {
+                        this.render_notification(ix, cx)
+                            .unwrap_or_else(|| div().into_any())
+                    })
                 });
-            notification_list.set_scroll_handler(|visible_range, count, this, cx| {
-                if count.saturating_sub(visible_range.end) < LOADING_THRESHOLD {
-                    if let Some(task) = this
-                        .notification_store
-                        .update(cx, |store, cx| store.load_more_notifications(false, cx))
-                    {
-                        task.detach();
+            notification_list.set_scroll_handler(cx.listener(
+                |this, event: &ListScrollEvent, cx| {
+                    if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD {
+                        if let Some(task) = this
+                            .notification_store
+                            .update(cx, |store, cx| store.load_more_notifications(false, cx))
+                        {
+                            task.detach();
+                        }
                     }
-                }
-            });
+                },
+            ));
 
             let mut this = Self {
                 fs,
                 client,
                 user_store,
-                local_timezone: cx.platform().local_timezone(),
+                local_timezone: cx.local_timezone(),
                 channel_store: ChannelStore::global(cx),
                 notification_store: NotificationStore::global(cx),
                 notification_list,
@@ -146,7 +153,10 @@ impl NotificationPanel {
         })
     }
 
-    pub fn load(workspace: WeakView<Workspace>, cx: AsyncAppContext) -> Task<Result<View<Self>>> {
+    pub fn load(
+        workspace: WeakView<Workspace>,
+        cx: AsyncWindowContext,
+    ) -> Task<Result<View<Self>>> {
         cx.spawn(|mut cx| async move {
             let serialized_panel = if let Some(panel) = cx
                 .background_executor()
@@ -160,24 +170,22 @@ impl NotificationPanel {
                 None
             };
 
-            Flatten::flatten(cx.update(|cx| {
-                workspace.update(cx, |workspace, cx| {
-                    let panel = Self::new(workspace, cx);
-                    if let Some(serialized_panel) = serialized_panel {
-                        panel.update(cx, |panel, cx| {
-                            panel.width = serialized_panel.width;
-                            cx.notify();
-                        });
-                    }
-                    panel
-                })
-            }))
+            workspace.update(&mut cx, |workspace, cx| {
+                let panel = Self::new(workspace, cx);
+                if let Some(serialized_panel) = serialized_panel {
+                    panel.update(cx, |panel, cx| {
+                        panel.width = serialized_panel.width;
+                        cx.notify();
+                    });
+                }
+                panel
+            })
         })
     }
 
     fn serialize(&mut self, cx: &mut ViewContext<Self>) {
         let width = self.width;
-        self.pending_serialization = cx.background().spawn(
+        self.pending_serialization = cx.background_executor().spawn(
             async move {
                 KEY_VALUE_STORE
                     .write_kvp(
@@ -217,17 +225,17 @@ impl NotificationPanel {
                 .child(
                     v_stack().child(Label::new(text)).child(
                         h_stack()
-                            .child(Label::from(format_timestamp(
+                            .child(Label::new(format_timestamp(
                                 timestamp,
                                 now,
                                 self.local_timezone,
                             )))
                             .children(if let Some(is_accepted) = response {
-                                Some(Label::new(if is_accepted {
+                                Some(div().child(Label::new(if is_accepted {
                                     "You accepted"
                                 } else {
                                     "You declined"
-                                }))
+                                })))
                             } else if needs_response {
                                 Some(
                                     h_stack()
@@ -262,7 +270,8 @@ impl NotificationPanel {
                                 None
                             }),
                     ),
-                ),
+                )
+                .into_any(),
         )
     }
 
@@ -355,7 +364,7 @@ impl NotificationPanel {
                 .or_insert_with(|| {
                     let client = self.client.clone();
                     cx.spawn(|this, mut cx| async move {
-                        cx.background().timer(MARK_AS_READ_DELAY).await;
+                        cx.background_executor().timer(MARK_AS_READ_DELAY).await;
                         client
                             .request(proto::MarkNotificationRead { notification_id })
                             .await?;

crates/project2/src/project2.rs 🔗

@@ -867,7 +867,7 @@ impl Project {
         languages.set_executor(cx.executor());
         let http_client = util::http::FakeHttpClient::with_404_response();
         let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
-        let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx));
+        let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx));
         let project = cx.update(|cx| {
             Project::local(
                 client,

crates/ui2/src/components/avatar.rs 🔗

@@ -58,16 +58,8 @@ impl RenderOnce for Avatar {
 }
 
 impl Avatar {
-    pub fn uri(src: impl Into<SharedString>) -> Self {
-        Self::source(src.into().into())
-    }
-
-    pub fn data(src: Arc<ImageData>) -> Self {
-        Self::source(src.into())
-    }
-
-    pub fn source(src: ImageSource) -> Self {
-        Self {
+    pub fn new(src: impl Into<ImageSource>) -> Self {
+        Avatar {
             image: img(src),
             is_available: None,
             border_color: None,

crates/ui2/src/components/list/list_item.rs 🔗

@@ -107,7 +107,7 @@ impl ListItem {
     }
 
     pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self {
-        self.left_slot = Some(Avatar::source(left_avatar.into()).into_any_element());
+        self.left_slot = Some(Avatar::new(left_avatar).into_any_element());
         self
     }
 }

crates/ui2/src/components/stories/avatar.rs 🔗

@@ -13,18 +13,18 @@ impl Render for AvatarStory {
         Story::container()
             .child(Story::title_for::<Avatar>())
             .child(Story::label("Default"))
-            .child(Avatar::uri(
+            .child(Avatar::new(
                 "https://avatars.githubusercontent.com/u/1714999?v=4",
             ))
-            .child(Avatar::uri(
+            .child(Avatar::new(
                 "https://avatars.githubusercontent.com/u/326587?v=4",
             ))
             .child(
-                Avatar::uri("https://avatars.githubusercontent.com/u/326587?v=4")
+                Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
                     .availability_indicator(true),
             )
             .child(
-                Avatar::uri("https://avatars.githubusercontent.com/u/326587?v=4")
+                Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
                     .availability_indicator(false),
             )
     }

crates/workspace2/src/workspace2.rs 🔗

@@ -360,7 +360,7 @@ impl AppState {
         let languages = Arc::new(LanguageRegistry::test());
         let http_client = util::http::FakeHttpClient::with_404_response();
         let client = Client::new(http_client.clone(), cx);
-        let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx));
+        let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx));
         let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx));
 
         theme::init(theme::LoadThemes::JustBase, cx);

crates/zed2/src/main.rs 🔗

@@ -143,7 +143,7 @@ fn main() {
 
         language::init(cx);
         languages::init(languages.clone(), node_runtime.clone(), cx);
-        let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
+        let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx));
         let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx));
 
         cx.set_global(client.clone());

crates/zed2/src/zed2.rs 🔗

@@ -165,10 +165,10 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
                 collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
             let chat_panel =
                 collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
-            // let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
-            //     workspace_handle.clone(),
-            //     cx.clone(),
-            // );
+            let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
+                workspace_handle.clone(),
+                cx.clone(),
+            );
             let (
                 project_panel,
                 terminal_panel,