zed2 notification panel (#3603)

Julia created

Release Notes:

- N/A

Change summary

Cargo.lock                                                         |    1 
crates/channel2/src/channel_store_tests.rs                         |    2 
crates/client2/src/test.rs                                         |    4 
crates/client2/src/user.rs                                         |   60 
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                              |   88 
crates/collab_ui2/src/collab_panel/contact_finder.rs               |    4 
crates/collab_ui2/src/collab_titlebar_item.rs                      |  115 
crates/collab_ui2/src/collab_ui.rs                                 |    1 
crates/collab_ui2/src/face_pile.rs                                 |   12 
crates/collab_ui2/src/notification_panel.rs                        | 1600 
crates/collab_ui2/src/notifications/incoming_call_notification.rs  |    9 
crates/collab_ui2/src/notifications/project_shared_notification.rs |    7 
crates/project2/src/project2.rs                                    |    2 
crates/ui2/src/components/avatar.rs                                |   15 
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/Cargo.toml                                             |    1 
crates/zed2/src/main.rs                                            |    3 
crates/zed2/src/zed2.rs                                            |   14 
24 files changed, 874 insertions(+), 1,098 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -12071,6 +12071,7 @@ dependencies = [
  "lsp2",
  "menu2",
  "node_runtime",
+ "notifications2",
  "num_cpus",
  "outline2",
  "parking_lot 0.11.2",

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,13 +2,12 @@ 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, Future, StreamExt};
+use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, Task};
 use postage::{sink::Sink, watch};
 use rpc::proto::{RequestMessage, UsersResponse};
 use std::sync::{Arc, Weak};
 use text::ReplicaId;
-use util::http::HttpClient;
 use util::TryFutureExt as _;
 
 pub type UserId = u64;
@@ -20,7 +19,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 +75,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<()>>,
 }
@@ -112,11 +110,7 @@ enum UpdateContacts {
 }
 
 impl UserStore {
-    pub fn new(
-        client: Arc<Client>,
-        http: Arc<dyn HttpClient>,
-        cx: &mut ModelContext<Self>,
-    ) -> Self {
+    pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
         let (mut current_user_tx, current_user_rx) = watch::channel();
         let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded();
         let rpc_subscriptions = vec![
@@ -134,7 +128,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 +438,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 +615,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 +655,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(),
         })
     }
 }
@@ -696,25 +692,3 @@ impl Collaborator {
         })
     }
 }
-
-// todo!("we probably don't need this now that we fetch")
-async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
-    let mut response = http
-        .get(url, Default::default(), true)
-        .await
-        .map_err(|e| anyhow!("failed to send user avatar request: {}", e))?;
-
-    if !response.status().is_success() {
-        return Err(anyhow!("avatar request failed {:?}", response.status()));
-    }
-
-    let mut body = Vec::new();
-    response
-        .body_mut()
-        .read_to_end(&mut body)
-        .await
-        .map_err(|e| anyhow!("failed to read user avatar response body: {}", e))?;
-    let format = image::guess_format(&body)?;
-    let image = image::load_from_memory_with_format(&body, format)?.into_bgra8();
-    Ok(Arc::new(ImageData::new(image)))
-}

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 🔗

@@ -19,6 +19,7 @@ mod contact_finder;
 use contact_finder::ContactFinder;
 use menu::{Cancel, Confirm, SelectNext, SelectPrev};
 use rpc::proto::{self, PeerId};
+use smallvec::SmallVec;
 use theme::{ActiveTheme, ThemeSettings};
 // use context_menu::{ContextMenu, ContextMenuItem};
 // use db::kvp::KEY_VALUE_STORE;
@@ -1155,7 +1156,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()
@@ -2349,44 +2350,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())
@@ -2458,7 +2460,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(
@@ -2516,7 +2518,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.
@@ -2524,7 +2528,7 @@ impl CollabPanel {
                     } else {
                         None
                     })
-                    .collect::<Vec<_>>(),
+                    .collect::<SmallVec<_>>(),
             };
 
             Some(result)

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();
@@ -424,27 +422,21 @@ impl CollabTitlebarItem {
         current_user: &Arc<User>,
     ) -> 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| {
+
+        let pile = FacePile::default().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,12 +446,11 @@ 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/collab_ui.rs 🔗

@@ -39,6 +39,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     collab_panel::init(cx);
     channel_view::init(cx);
     chat_panel::init(cx);
+    notification_panel::init(cx);
     notifications::init(&app_state, cx);
 
     // cx.add_global_action(toggle_screen_sharing);

crates/collab_ui2/src/face_pile.rs 🔗

@@ -1,11 +1,11 @@
 use gpui::{
-    div, AnyElement, Div, ElementId, IntoElement, ParentElement as _, RenderOnce, Styled,
-    WindowContext,
+    div, AnyElement, Div, ElementId, IntoElement, 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 +25,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 🔗

@@ -1,884 +1,716 @@
-// use crate::{chat_panel::ChatPanel, render_avatar, NotificationPanelSettings};
-// use anyhow::Result;
-// use channel::ChannelStore;
-// use client::{Client, Notification, User, UserStore};
-// use collections::HashMap;
-// use db::kvp::KEY_VALUE_STORE;
-// use futures::StreamExt;
-// use gpui::{
-//     actions,
-//     elements::*,
-//     platform::{CursorStyle, MouseButton},
-//     serde_json, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View,
-//     ViewContext, ViewHandle, WeakViewHandle, WindowContext,
-// };
-// use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
-// use project::Fs;
-// use rpc::proto;
-// use serde::{Deserialize, Serialize};
-// use settings::SettingsStore;
-// use std::{sync::Arc, time::Duration};
-// use theme::{ui, Theme};
-// use time::{OffsetDateTime, UtcOffset};
-// use util::{ResultExt, TryFutureExt};
-// use workspace::{
-//     dock::{DockPosition, Panel},
-//     Workspace,
-// };
-
-// const LOADING_THRESHOLD: usize = 30;
-// const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
-// const TOAST_DURATION: Duration = Duration::from_secs(5);
-// const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";
-
-// pub struct NotificationPanel {
-//     client: Arc<Client>,
-//     user_store: ModelHandle<UserStore>,
-//     channel_store: ModelHandle<ChannelStore>,
-//     notification_store: ModelHandle<NotificationStore>,
-//     fs: Arc<dyn Fs>,
-//     width: Option<f32>,
-//     active: bool,
-//     notification_list: ListState<Self>,
-//     pending_serialization: Task<Option<()>>,
-//     subscriptions: Vec<gpui::Subscription>,
-//     workspace: WeakViewHandle<Workspace>,
-//     current_notification_toast: Option<(u64, Task<()>)>,
-//     local_timezone: UtcOffset,
-//     has_focus: bool,
-//     mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
-// }
-
-// #[derive(Serialize, Deserialize)]
-// struct SerializedNotificationPanel {
-//     width: Option<f32>,
-// }
-
-// #[derive(Debug)]
-// pub enum Event {
-//     DockPositionChanged,
-//     Focus,
-//     Dismissed,
-// }
-
-// pub struct NotificationPresenter {
-//     pub actor: Option<Arc<client::User>>,
-//     pub text: String,
-//     pub icon: &'static str,
-//     pub needs_response: bool,
-//     pub can_navigate: bool,
-// }
-
-// actions!(notification_panel, [ToggleFocus]);
-
-// pub fn init(_cx: &mut AppContext) {}
-
-// impl NotificationPanel {
-//     pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
-//         let fs = workspace.app_state().fs.clone();
-//         let client = workspace.app_state().client.clone();
-//         let user_store = workspace.app_state().user_store.clone();
-//         let workspace_handle = workspace.weak_handle();
-
-//         cx.add_view(|cx| {
-//             let mut status = client.status();
-//             cx.spawn(|this, mut cx| async move {
-//                 while let Some(_) = status.next().await {
-//                     if this
-//                         .update(&mut cx, |_, cx| {
-//                             cx.notify();
-//                         })
-//                         .is_err()
-//                     {
-//                         break;
-//                     }
-//                 }
-//             })
-//             .detach();
-
-//             let mut notification_list =
-//                 ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
-//                     this.render_notification(ix, cx)
-//                         .unwrap_or_else(|| Empty::new().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();
-//                     }
-//                 }
-//             });
-
-//             let mut this = Self {
-//                 fs,
-//                 client,
-//                 user_store,
-//                 local_timezone: cx.platform().local_timezone(),
-//                 channel_store: ChannelStore::global(cx),
-//                 notification_store: NotificationStore::global(cx),
-//                 notification_list,
-//                 pending_serialization: Task::ready(None),
-//                 workspace: workspace_handle,
-//                 has_focus: false,
-//                 current_notification_toast: None,
-//                 subscriptions: Vec::new(),
-//                 active: false,
-//                 mark_as_read_tasks: HashMap::default(),
-//                 width: None,
-//             };
-
-//             let mut old_dock_position = this.position(cx);
-//             this.subscriptions.extend([
-//                 cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
-//                 cx.subscribe(&this.notification_store, Self::on_notification_event),
-//                 cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
-//                     let new_dock_position = this.position(cx);
-//                     if new_dock_position != old_dock_position {
-//                         old_dock_position = new_dock_position;
-//                         cx.emit(Event::DockPositionChanged);
-//                     }
-//                     cx.notify();
-//                 }),
-//             ]);
-//             this
-//         })
-//     }
-
-//     pub fn load(
-//         workspace: WeakViewHandle<Workspace>,
-//         cx: AsyncAppContext,
-//     ) -> Task<Result<ViewHandle<Self>>> {
-//         cx.spawn(|mut cx| async move {
-//             let serialized_panel = if let Some(panel) = cx
-//                 .background()
-//                 .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
-//                 .await
-//                 .log_err()
-//                 .flatten()
-//             {
-//                 Some(serde_json::from_str::<SerializedNotificationPanel>(&panel)?)
-//             } else {
-//                 None
-//             };
-
-//             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(
-//             async move {
-//                 KEY_VALUE_STORE
-//                     .write_kvp(
-//                         NOTIFICATION_PANEL_KEY.into(),
-//                         serde_json::to_string(&SerializedNotificationPanel { width })?,
-//                     )
-//                     .await?;
-//                 anyhow::Ok(())
-//             }
-//             .log_err(),
-//         );
-//     }
-
-//     fn render_notification(
-//         &mut self,
-//         ix: usize,
-//         cx: &mut ViewContext<Self>,
-//     ) -> Option<AnyElement<Self>> {
-//         let entry = self.notification_store.read(cx).notification_at(ix)?;
-//         let notification_id = entry.id;
-//         let now = OffsetDateTime::now_utc();
-//         let timestamp = entry.timestamp;
-//         let NotificationPresenter {
-//             actor,
-//             text,
-//             needs_response,
-//             can_navigate,
-//             ..
-//         } = self.present_notification(entry, cx)?;
-
-//         let theme = theme::current(cx);
-//         let style = &theme.notification_panel;
-//         let response = entry.response;
-//         let notification = entry.notification.clone();
-
-//         let message_style = if entry.is_read {
-//             style.read_text.clone()
-//         } else {
-//             style.unread_text.clone()
-//         };
-
-//         if self.active && !entry.is_read {
-//             self.did_render_notification(notification_id, &notification, cx);
-//         }
-
-//         enum Decline {}
-//         enum Accept {}
-
-//         Some(
-//             MouseEventHandler::new::<NotificationEntry, _>(ix, cx, |_, cx| {
-//                 let container = message_style.container;
-
-//                 Flex::row()
-//                     .with_children(actor.map(|actor| {
-//                         render_avatar(actor.avatar.clone(), &style.avatar, style.avatar_container)
-//                     }))
-//                     .with_child(
-//                         Flex::column()
-//                             .with_child(Text::new(text, message_style.text.clone()))
-//                             .with_child(
-//                                 Flex::row()
-//                                     .with_child(
-//                                         Label::new(
-//                                             format_timestamp(timestamp, now, self.local_timezone),
-//                                             style.timestamp.text.clone(),
-//                                         )
-//                                         .contained()
-//                                         .with_style(style.timestamp.container),
-//                                     )
-//                                     .with_children(if let Some(is_accepted) = response {
-//                                         Some(
-//                                             Label::new(
-//                                                 if is_accepted {
-//                                                     "You accepted"
-//                                                 } else {
-//                                                     "You declined"
-//                                                 },
-//                                                 style.read_text.text.clone(),
-//                                             )
-//                                             .flex_float()
-//                                             .into_any(),
-//                                         )
-//                                     } else if needs_response {
-//                                         Some(
-//                                             Flex::row()
-//                                                 .with_children([
-//                                                     MouseEventHandler::new::<Decline, _>(
-//                                                         ix,
-//                                                         cx,
-//                                                         |state, _| {
-//                                                             let button =
-//                                                                 style.button.style_for(state);
-//                                                             Label::new(
-//                                                                 "Decline",
-//                                                                 button.text.clone(),
-//                                                             )
-//                                                             .contained()
-//                                                             .with_style(button.container)
-//                                                         },
-//                                                     )
-//                                                     .with_cursor_style(CursorStyle::PointingHand)
-//                                                     .on_click(MouseButton::Left, {
-//                                                         let notification = notification.clone();
-//                                                         move |_, view, cx| {
-//                                                             view.respond_to_notification(
-//                                                                 notification.clone(),
-//                                                                 false,
-//                                                                 cx,
-//                                                             );
-//                                                         }
-//                                                     }),
-//                                                     MouseEventHandler::new::<Accept, _>(
-//                                                         ix,
-//                                                         cx,
-//                                                         |state, _| {
-//                                                             let button =
-//                                                                 style.button.style_for(state);
-//                                                             Label::new(
-//                                                                 "Accept",
-//                                                                 button.text.clone(),
-//                                                             )
-//                                                             .contained()
-//                                                             .with_style(button.container)
-//                                                         },
-//                                                     )
-//                                                     .with_cursor_style(CursorStyle::PointingHand)
-//                                                     .on_click(MouseButton::Left, {
-//                                                         let notification = notification.clone();
-//                                                         move |_, view, cx| {
-//                                                             view.respond_to_notification(
-//                                                                 notification.clone(),
-//                                                                 true,
-//                                                                 cx,
-//                                                             );
-//                                                         }
-//                                                     }),
-//                                                 ])
-//                                                 .flex_float()
-//                                                 .into_any(),
-//                                         )
-//                                     } else {
-//                                         None
-//                                     }),
-//                             )
-//                             .flex(1.0, true),
-//                     )
-//                     .contained()
-//                     .with_style(container)
-//                     .into_any()
-//             })
-//             .with_cursor_style(if can_navigate {
-//                 CursorStyle::PointingHand
-//             } else {
-//                 CursorStyle::default()
-//             })
-//             .on_click(MouseButton::Left, {
-//                 let notification = notification.clone();
-//                 move |_, this, cx| this.did_click_notification(&notification, cx)
-//             })
-//             .into_any(),
-//         )
-//     }
-
-//     fn present_notification(
-//         &self,
-//         entry: &NotificationEntry,
-//         cx: &AppContext,
-//     ) -> Option<NotificationPresenter> {
-//         let user_store = self.user_store.read(cx);
-//         let channel_store = self.channel_store.read(cx);
-//         match entry.notification {
-//             Notification::ContactRequest { sender_id } => {
-//                 let requester = user_store.get_cached_user(sender_id)?;
-//                 Some(NotificationPresenter {
-//                     icon: "icons/plus.svg",
-//                     text: format!("{} wants to add you as a contact", requester.github_login),
-//                     needs_response: user_store.has_incoming_contact_request(requester.id),
-//                     actor: Some(requester),
-//                     can_navigate: false,
-//                 })
-//             }
-//             Notification::ContactRequestAccepted { responder_id } => {
-//                 let responder = user_store.get_cached_user(responder_id)?;
-//                 Some(NotificationPresenter {
-//                     icon: "icons/plus.svg",
-//                     text: format!("{} accepted your contact invite", responder.github_login),
-//                     needs_response: false,
-//                     actor: Some(responder),
-//                     can_navigate: false,
-//                 })
-//             }
-//             Notification::ChannelInvitation {
-//                 ref channel_name,
-//                 channel_id,
-//                 inviter_id,
-//             } => {
-//                 let inviter = user_store.get_cached_user(inviter_id)?;
-//                 Some(NotificationPresenter {
-//                     icon: "icons/hash.svg",
-//                     text: format!(
-//                         "{} invited you to join the #{channel_name} channel",
-//                         inviter.github_login
-//                     ),
-//                     needs_response: channel_store.has_channel_invitation(channel_id),
-//                     actor: Some(inviter),
-//                     can_navigate: false,
-//                 })
-//             }
-//             Notification::ChannelMessageMention {
-//                 sender_id,
-//                 channel_id,
-//                 message_id,
-//             } => {
-//                 let sender = user_store.get_cached_user(sender_id)?;
-//                 let channel = channel_store.channel_for_id(channel_id)?;
-//                 let message = self
-//                     .notification_store
-//                     .read(cx)
-//                     .channel_message_for_id(message_id)?;
-//                 Some(NotificationPresenter {
-//                     icon: "icons/conversations.svg",
-//                     text: format!(
-//                         "{} mentioned you in #{}:\n{}",
-//                         sender.github_login, channel.name, message.body,
-//                     ),
-//                     needs_response: false,
-//                     actor: Some(sender),
-//                     can_navigate: true,
-//                 })
-//             }
-//         }
-//     }
-
-//     fn did_render_notification(
-//         &mut self,
-//         notification_id: u64,
-//         notification: &Notification,
-//         cx: &mut ViewContext<Self>,
-//     ) {
-//         let should_mark_as_read = match notification {
-//             Notification::ContactRequestAccepted { .. } => true,
-//             Notification::ContactRequest { .. }
-//             | Notification::ChannelInvitation { .. }
-//             | Notification::ChannelMessageMention { .. } => false,
-//         };
-
-//         if should_mark_as_read {
-//             self.mark_as_read_tasks
-//                 .entry(notification_id)
-//                 .or_insert_with(|| {
-//                     let client = self.client.clone();
-//                     cx.spawn(|this, mut cx| async move {
-//                         cx.background().timer(MARK_AS_READ_DELAY).await;
-//                         client
-//                             .request(proto::MarkNotificationRead { notification_id })
-//                             .await?;
-//                         this.update(&mut cx, |this, _| {
-//                             this.mark_as_read_tasks.remove(&notification_id);
-//                         })?;
-//                         Ok(())
-//                     })
-//                 });
-//         }
-//     }
-
-//     fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
-//         if let Notification::ChannelMessageMention {
-//             message_id,
-//             channel_id,
-//             ..
-//         } = notification.clone()
-//         {
-//             if let Some(workspace) = self.workspace.upgrade(cx) {
-//                 cx.app_context().defer(move |cx| {
-//                     workspace.update(cx, |workspace, cx| {
-//                         if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
-//                             panel.update(cx, |panel, cx| {
-//                                 panel
-//                                     .select_channel(channel_id, Some(message_id), cx)
-//                                     .detach_and_log_err(cx);
-//                             });
-//                         }
-//                     });
-//                 });
-//             }
-//         }
-//     }
-
-//     fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool {
-//         if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
-//             if let Some(workspace) = self.workspace.upgrade(cx) {
-//                 return workspace
-//                     .read_with(cx, |workspace, cx| {
-//                         if let Some(panel) = workspace.panel::<ChatPanel>(cx) {
-//                             return panel.read_with(cx, |panel, cx| {
-//                                 panel.is_scrolled_to_bottom()
-//                                     && panel.active_chat().map_or(false, |chat| {
-//                                         chat.read(cx).channel_id == *channel_id
-//                                     })
-//                             });
-//                         }
-//                         false
-//                     })
-//                     .unwrap_or_default();
-//             }
-//         }
-
-//         false
-//     }
-
-//     fn render_sign_in_prompt(
-//         &self,
-//         theme: &Arc<Theme>,
-//         cx: &mut ViewContext<Self>,
-//     ) -> AnyElement<Self> {
-//         enum SignInPromptLabel {}
-
-//         MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
-//             Label::new(
-//                 "Sign in to view your notifications".to_string(),
-//                 theme
-//                     .chat_panel
-//                     .sign_in_prompt
-//                     .style_for(mouse_state)
-//                     .clone(),
-//             )
-//         })
-//         .with_cursor_style(CursorStyle::PointingHand)
-//         .on_click(MouseButton::Left, move |_, this, cx| {
-//             let client = this.client.clone();
-//             cx.spawn(|_, cx| async move {
-//                 client.authenticate_and_connect(true, &cx).log_err().await;
-//             })
-//             .detach();
-//         })
-//         .aligned()
-//         .into_any()
-//     }
-
-//     fn render_empty_state(
-//         &self,
-//         theme: &Arc<Theme>,
-//         _cx: &mut ViewContext<Self>,
-//     ) -> AnyElement<Self> {
-//         Label::new(
-//             "You have no notifications".to_string(),
-//             theme.chat_panel.sign_in_prompt.default.clone(),
-//         )
-//         .aligned()
-//         .into_any()
-//     }
-
-//     fn on_notification_event(
-//         &mut self,
-//         _: ModelHandle<NotificationStore>,
-//         event: &NotificationEvent,
-//         cx: &mut ViewContext<Self>,
-//     ) {
-//         match event {
-//             NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
-//             NotificationEvent::NotificationRemoved { entry }
-//             | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx),
-//             NotificationEvent::NotificationsUpdated {
-//                 old_range,
-//                 new_count,
-//             } => {
-//                 self.notification_list.splice(old_range.clone(), *new_count);
-//                 cx.notify();
-//             }
-//         }
-//     }
-
-//     fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
-//         if self.is_showing_notification(&entry.notification, cx) {
-//             return;
-//         }
-
-//         let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
-//         else {
-//             return;
-//         };
-
-//         let notification_id = entry.id;
-//         self.current_notification_toast = Some((
-//             notification_id,
-//             cx.spawn(|this, mut cx| async move {
-//                 cx.background().timer(TOAST_DURATION).await;
-//                 this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
-//                     .ok();
-//             }),
-//         ));
-
-//         self.workspace
-//             .update(cx, |workspace, cx| {
-//                 workspace.dismiss_notification::<NotificationToast>(0, cx);
-//                 workspace.show_notification(0, cx, |cx| {
-//                     let workspace = cx.weak_handle();
-//                     cx.add_view(|_| NotificationToast {
-//                         notification_id,
-//                         actor,
-//                         text,
-//                         workspace,
-//                     })
-//                 })
-//             })
-//             .ok();
-//     }
-
-//     fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext<Self>) {
-//         if let Some((current_id, _)) = &self.current_notification_toast {
-//             if *current_id == notification_id {
-//                 self.current_notification_toast.take();
-//                 self.workspace
-//                     .update(cx, |workspace, cx| {
-//                         workspace.dismiss_notification::<NotificationToast>(0, cx)
-//                     })
-//                     .ok();
-//             }
-//         }
-//     }
-
-//     fn respond_to_notification(
-//         &mut self,
-//         notification: Notification,
-//         response: bool,
-//         cx: &mut ViewContext<Self>,
-//     ) {
-//         self.notification_store.update(cx, |store, cx| {
-//             store.respond_to_notification(notification, response, cx);
-//         });
-//     }
-// }
-
-// impl Entity for NotificationPanel {
-//     type Event = Event;
-// }
-
-// impl View for NotificationPanel {
-//     fn ui_name() -> &'static str {
-//         "NotificationPanel"
-//     }
-
-//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-//         let theme = theme::current(cx);
-//         let style = &theme.notification_panel;
-//         let element = if self.client.user_id().is_none() {
-//             self.render_sign_in_prompt(&theme, cx)
-//         } else if self.notification_list.item_count() == 0 {
-//             self.render_empty_state(&theme, cx)
-//         } else {
-//             Flex::column()
-//                 .with_child(
-//                     Flex::row()
-//                         .with_child(Label::new("Notifications", style.title.text.clone()))
-//                         .with_child(ui::svg(&style.title_icon).flex_float())
-//                         .align_children_center()
-//                         .contained()
-//                         .with_style(style.title.container)
-//                         .constrained()
-//                         .with_height(style.title_height),
-//                 )
-//                 .with_child(
-//                     List::new(self.notification_list.clone())
-//                         .contained()
-//                         .with_style(style.list)
-//                         .flex(1., true),
-//                 )
-//                 .into_any()
-//         };
-//         element
-//             .contained()
-//             .with_style(style.container)
-//             .constrained()
-//             .with_min_width(150.)
-//             .into_any()
-//     }
-
-//     fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
-//         self.has_focus = true;
-//     }
-
-//     fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
-//         self.has_focus = false;
-//     }
-// }
-
-// impl Panel for NotificationPanel {
-//     fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
-//         settings::get::<NotificationPanelSettings>(cx).dock
-//     }
-
-//     fn position_is_valid(&self, position: DockPosition) -> bool {
-//         matches!(position, DockPosition::Left | DockPosition::Right)
-//     }
-
-//     fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
-//         settings::update_settings_file::<NotificationPanelSettings>(
-//             self.fs.clone(),
-//             cx,
-//             move |settings| settings.dock = Some(position),
-//         );
-//     }
-
-//     fn size(&self, cx: &gpui::WindowContext) -> f32 {
-//         self.width
-//             .unwrap_or_else(|| settings::get::<NotificationPanelSettings>(cx).default_width)
-//     }
-
-//     fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
-//         self.width = size;
-//         self.serialize(cx);
-//         cx.notify();
-//     }
-
-//     fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
-//         self.active = active;
-//         if self.notification_store.read(cx).notification_count() == 0 {
-//             cx.emit(Event::Dismissed);
-//         }
-//     }
-
-//     fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
-//         (settings::get::<NotificationPanelSettings>(cx).button
-//             && self.notification_store.read(cx).notification_count() > 0)
-//             .then(|| "icons/bell.svg")
-//     }
-
-//     fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
-//         (
-//             "Notification Panel".to_string(),
-//             Some(Box::new(ToggleFocus)),
-//         )
-//     }
-
-//     fn icon_label(&self, cx: &WindowContext) -> Option<String> {
-//         let count = self.notification_store.read(cx).unread_notification_count();
-//         if count == 0 {
-//             None
-//         } else {
-//             Some(count.to_string())
-//         }
-//     }
-
-//     fn should_change_position_on_event(event: &Self::Event) -> bool {
-//         matches!(event, Event::DockPositionChanged)
-//     }
-
-//     fn should_close_on_event(event: &Self::Event) -> bool {
-//         matches!(event, Event::Dismissed)
-//     }
-
-//     fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
-//         self.has_focus
-//     }
-
-//     fn is_focus_event(event: &Self::Event) -> bool {
-//         matches!(event, Event::Focus)
-//     }
-// }
-
-// pub struct NotificationToast {
-//     notification_id: u64,
-//     actor: Option<Arc<User>>,
-//     text: String,
-//     workspace: WeakViewHandle<Workspace>,
-// }
-
-// pub enum ToastEvent {
-//     Dismiss,
-// }
-
-// impl NotificationToast {
-//     fn focus_notification_panel(&self, cx: &mut AppContext) {
-//         let workspace = self.workspace.clone();
-//         let notification_id = self.notification_id;
-//         cx.defer(move |cx| {
-//             workspace
-//                 .update(cx, |workspace, cx| {
-//                     if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
-//                         panel.update(cx, |panel, cx| {
-//                             let store = panel.notification_store.read(cx);
-//                             if let Some(entry) = store.notification_for_id(notification_id) {
-//                                 panel.did_click_notification(&entry.clone().notification, cx);
-//                             }
-//                         });
-//                     }
-//                 })
-//                 .ok();
-//         })
-//     }
-// }
-
-// impl Entity for NotificationToast {
-//     type Event = ToastEvent;
-// }
-
-// impl View for NotificationToast {
-//     fn ui_name() -> &'static str {
-//         "ContactNotification"
-//     }
-
-//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-//         let user = self.actor.clone();
-//         let theme = theme::current(cx).clone();
-//         let theme = &theme.contact_notification;
-
-//         MouseEventHandler::new::<Self, _>(0, cx, |_, cx| {
-//             Flex::row()
-//                 .with_children(user.and_then(|user| {
-//                     Some(
-//                         Image::from_data(user.avatar.clone()?)
-//                             .with_style(theme.header_avatar)
-//                             .aligned()
-//                             .constrained()
-//                             .with_height(
-//                                 cx.font_cache()
-//                                     .line_height(theme.header_message.text.font_size),
-//                             )
-//                             .aligned()
-//                             .top(),
-//                     )
-//                 }))
-//                 .with_child(
-//                     Text::new(self.text.clone(), theme.header_message.text.clone())
-//                         .contained()
-//                         .with_style(theme.header_message.container)
-//                         .aligned()
-//                         .top()
-//                         .left()
-//                         .flex(1., true),
-//                 )
-//                 .with_child(
-//                     MouseEventHandler::new::<ToastEvent, _>(0, cx, |state, _| {
-//                         let style = theme.dismiss_button.style_for(state);
-//                         Svg::new("icons/x.svg")
-//                             .with_color(style.color)
-//                             .constrained()
-//                             .with_width(style.icon_width)
-//                             .aligned()
-//                             .contained()
-//                             .with_style(style.container)
-//                             .constrained()
-//                             .with_width(style.button_width)
-//                             .with_height(style.button_width)
-//                     })
-//                     .with_cursor_style(CursorStyle::PointingHand)
-//                     .with_padding(Padding::uniform(5.))
-//                     .on_click(MouseButton::Left, move |_, _, cx| {
-//                         cx.emit(ToastEvent::Dismiss)
-//                     })
-//                     .aligned()
-//                     .constrained()
-//                     .with_height(
-//                         cx.font_cache()
-//                             .line_height(theme.header_message.text.font_size),
-//                     )
-//                     .aligned()
-//                     .top()
-//                     .flex_float(),
-//                 )
-//                 .contained()
-//         })
-//         .with_cursor_style(CursorStyle::PointingHand)
-//         .on_click(MouseButton::Left, move |_, this, cx| {
-//             this.focus_notification_panel(cx);
-//             cx.emit(ToastEvent::Dismiss);
-//         })
-//         .into_any()
-//     }
-// }
-
-// impl workspace::notifications::Notification for NotificationToast {
-//     fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
-//         matches!(event, ToastEvent::Dismiss)
-//     }
-// }
-
-// fn format_timestamp(
-//     mut timestamp: OffsetDateTime,
-//     mut now: OffsetDateTime,
-//     local_timezone: UtcOffset,
-// ) -> String {
-//     timestamp = timestamp.to_offset(local_timezone);
-//     now = now.to_offset(local_timezone);
-
-//     let today = now.date();
-//     let date = timestamp.date();
-//     if date == today {
-//         let difference = now - timestamp;
-//         if difference >= Duration::from_secs(3600) {
-//             format!("{}h", difference.whole_seconds() / 3600)
-//         } else if difference >= Duration::from_secs(60) {
-//             format!("{}m", difference.whole_seconds() / 60)
-//         } else {
-//             "just now".to_string()
-//         }
-//     } else if date.next_day() == Some(today) {
-//         format!("yesterday")
-//     } else {
-//         format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
-//     }
-// }
+use crate::{chat_panel::ChatPanel, NotificationPanelSettings};
+use anyhow::Result;
+use channel::ChannelStore;
+use client::{Client, Notification, User, UserStore};
+use collections::HashMap;
+use db::kvp::KEY_VALUE_STORE;
+use futures::StreamExt;
+use gpui::{
+    actions, div, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext, CursorStyle,
+    DismissEvent, Div, Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
+    IntoElement, ListAlignment, ListScrollEvent, ListState, Model, ParentElement, Render, Stateful,
+    StatefulInteractiveElement, Styled, Task, View, ViewContext, VisualContext, WeakView,
+    WindowContext,
+};
+use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
+use project::Fs;
+use rpc::proto;
+use serde::{Deserialize, Serialize};
+use settings::{Settings, SettingsStore};
+use std::{sync::Arc, time::Duration};
+use time::{OffsetDateTime, UtcOffset};
+use ui::{h_stack, v_stack, Avatar, Button, Clickable, Icon, IconButton, IconElement, Label};
+use util::{ResultExt, TryFutureExt};
+use workspace::{
+    dock::{DockPosition, Panel, PanelEvent},
+    Workspace,
+};
+
+const LOADING_THRESHOLD: usize = 30;
+const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
+const TOAST_DURATION: Duration = Duration::from_secs(5);
+const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";
+
+pub struct NotificationPanel {
+    client: Arc<Client>,
+    user_store: Model<UserStore>,
+    channel_store: Model<ChannelStore>,
+    notification_store: Model<NotificationStore>,
+    fs: Arc<dyn Fs>,
+    width: Option<f32>,
+    active: bool,
+    notification_list: ListState,
+    pending_serialization: Task<Option<()>>,
+    subscriptions: Vec<gpui::Subscription>,
+    workspace: WeakView<Workspace>,
+    current_notification_toast: Option<(u64, Task<()>)>,
+    local_timezone: UtcOffset,
+    focus_handle: FocusHandle,
+    mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct SerializedNotificationPanel {
+    width: Option<f32>,
+}
+
+#[derive(Debug)]
+pub enum Event {
+    DockPositionChanged,
+    Focus,
+    Dismissed,
+}
+
+pub struct NotificationPresenter {
+    pub actor: Option<Arc<client::User>>,
+    pub text: String,
+    pub icon: &'static str,
+    pub needs_response: bool,
+    pub can_navigate: bool,
+}
+
+actions!(notification_panel, [ToggleFocus]);
+
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(|workspace: &mut Workspace, _| {
+        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
+            workspace.toggle_panel_focus::<NotificationPanel>(cx);
+        });
+    })
+    .detach();
+}
+
+impl NotificationPanel {
+    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
+        let fs = workspace.app_state().fs.clone();
+        let client = workspace.app_state().client.clone();
+        let user_store = workspace.app_state().user_store.clone();
+        let workspace_handle = workspace.weak_handle();
+
+        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 {
+                    if this
+                        .update(&mut cx, |_, cx| {
+                            cx.notify();
+                        })
+                        .is_err()
+                    {
+                        break;
+                    }
+                }
+            })
+            .detach();
+
+            let notification_list =
+                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(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.local_timezone(),
+                channel_store: ChannelStore::global(cx),
+                notification_store: NotificationStore::global(cx),
+                notification_list,
+                pending_serialization: Task::ready(None),
+                workspace: workspace_handle,
+                focus_handle: cx.focus_handle(),
+                current_notification_toast: None,
+                subscriptions: Vec::new(),
+                active: false,
+                mark_as_read_tasks: HashMap::default(),
+                width: None,
+            };
+
+            let mut old_dock_position = this.position(cx);
+            this.subscriptions.extend([
+                cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
+                cx.subscribe(&this.notification_store, Self::on_notification_event),
+                cx.observe_global::<SettingsStore>(move |this: &mut Self, cx| {
+                    let new_dock_position = this.position(cx);
+                    if new_dock_position != old_dock_position {
+                        old_dock_position = new_dock_position;
+                        cx.emit(Event::DockPositionChanged);
+                    }
+                    cx.notify();
+                }),
+            ]);
+            this
+        })
+    }
+
+    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()
+                .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
+                .await
+                .log_err()
+                .flatten()
+            {
+                Some(serde_json::from_str::<SerializedNotificationPanel>(&panel)?)
+            } else {
+                None
+            };
+
+            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_executor().spawn(
+            async move {
+                KEY_VALUE_STORE
+                    .write_kvp(
+                        NOTIFICATION_PANEL_KEY.into(),
+                        serde_json::to_string(&SerializedNotificationPanel { width })?,
+                    )
+                    .await?;
+                anyhow::Ok(())
+            }
+            .log_err(),
+        );
+    }
+
+    fn render_notification(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
+        let entry = self.notification_store.read(cx).notification_at(ix)?;
+        let notification_id = entry.id;
+        let now = OffsetDateTime::now_utc();
+        let timestamp = entry.timestamp;
+        let NotificationPresenter {
+            actor,
+            text,
+            needs_response,
+            can_navigate,
+            ..
+        } = self.present_notification(entry, cx)?;
+
+        let response = entry.response;
+        let notification = entry.notification.clone();
+
+        if self.active && !entry.is_read {
+            self.did_render_notification(notification_id, &notification, cx);
+        }
+
+        Some(
+            div()
+                .id(ix)
+                .child(
+                    h_stack()
+                        .children(actor.map(|actor| Avatar::new(actor.avatar_uri.clone())))
+                        .child(
+                            v_stack().child(Label::new(text)).child(
+                                h_stack()
+                                    .child(Label::new(format_timestamp(
+                                        timestamp,
+                                        now,
+                                        self.local_timezone,
+                                    )))
+                                    .children(if let Some(is_accepted) = response {
+                                        Some(div().child(Label::new(if is_accepted {
+                                            "You accepted"
+                                        } else {
+                                            "You declined"
+                                        })))
+                                    } else if needs_response {
+                                        Some(
+                                            h_stack()
+                                                .child(Button::new("decline", "Decline").on_click(
+                                                    {
+                                                        let notification = notification.clone();
+                                                        let view = cx.view().clone();
+                                                        move |_, cx| {
+                                                            view.update(cx, |this, cx| {
+                                                                this.respond_to_notification(
+                                                                    notification.clone(),
+                                                                    false,
+                                                                    cx,
+                                                                )
+                                                            });
+                                                        }
+                                                    },
+                                                ))
+                                                .child(Button::new("accept", "Accept").on_click({
+                                                    let notification = notification.clone();
+                                                    let view = cx.view().clone();
+                                                    move |_, cx| {
+                                                        view.update(cx, |this, cx| {
+                                                            this.respond_to_notification(
+                                                                notification.clone(),
+                                                                true,
+                                                                cx,
+                                                            )
+                                                        });
+                                                    }
+                                                })),
+                                        )
+                                    } else {
+                                        None
+                                    }),
+                            ),
+                        ),
+                )
+                .when(can_navigate, |el| {
+                    el.cursor(CursorStyle::PointingHand).on_click({
+                        let notification = notification.clone();
+                        cx.listener(move |this, _, cx| {
+                            this.did_click_notification(&notification, cx)
+                        })
+                    })
+                })
+                .into_any(),
+        )
+    }
+
+    fn present_notification(
+        &self,
+        entry: &NotificationEntry,
+        cx: &AppContext,
+    ) -> Option<NotificationPresenter> {
+        let user_store = self.user_store.read(cx);
+        let channel_store = self.channel_store.read(cx);
+        match entry.notification {
+            Notification::ContactRequest { sender_id } => {
+                let requester = user_store.get_cached_user(sender_id)?;
+                Some(NotificationPresenter {
+                    icon: "icons/plus.svg",
+                    text: format!("{} wants to add you as a contact", requester.github_login),
+                    needs_response: user_store.has_incoming_contact_request(requester.id),
+                    actor: Some(requester),
+                    can_navigate: false,
+                })
+            }
+            Notification::ContactRequestAccepted { responder_id } => {
+                let responder = user_store.get_cached_user(responder_id)?;
+                Some(NotificationPresenter {
+                    icon: "icons/plus.svg",
+                    text: format!("{} accepted your contact invite", responder.github_login),
+                    needs_response: false,
+                    actor: Some(responder),
+                    can_navigate: false,
+                })
+            }
+            Notification::ChannelInvitation {
+                ref channel_name,
+                channel_id,
+                inviter_id,
+            } => {
+                let inviter = user_store.get_cached_user(inviter_id)?;
+                Some(NotificationPresenter {
+                    icon: "icons/hash.svg",
+                    text: format!(
+                        "{} invited you to join the #{channel_name} channel",
+                        inviter.github_login
+                    ),
+                    needs_response: channel_store.has_channel_invitation(channel_id),
+                    actor: Some(inviter),
+                    can_navigate: false,
+                })
+            }
+            Notification::ChannelMessageMention {
+                sender_id,
+                channel_id,
+                message_id,
+            } => {
+                let sender = user_store.get_cached_user(sender_id)?;
+                let channel = channel_store.channel_for_id(channel_id)?;
+                let message = self
+                    .notification_store
+                    .read(cx)
+                    .channel_message_for_id(message_id)?;
+                Some(NotificationPresenter {
+                    icon: "icons/conversations.svg",
+                    text: format!(
+                        "{} mentioned you in #{}:\n{}",
+                        sender.github_login, channel.name, message.body,
+                    ),
+                    needs_response: false,
+                    actor: Some(sender),
+                    can_navigate: true,
+                })
+            }
+        }
+    }
+
+    fn did_render_notification(
+        &mut self,
+        notification_id: u64,
+        notification: &Notification,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let should_mark_as_read = match notification {
+            Notification::ContactRequestAccepted { .. } => true,
+            Notification::ContactRequest { .. }
+            | Notification::ChannelInvitation { .. }
+            | Notification::ChannelMessageMention { .. } => false,
+        };
+
+        if should_mark_as_read {
+            self.mark_as_read_tasks
+                .entry(notification_id)
+                .or_insert_with(|| {
+                    let client = self.client.clone();
+                    cx.spawn(|this, mut cx| async move {
+                        cx.background_executor().timer(MARK_AS_READ_DELAY).await;
+                        client
+                            .request(proto::MarkNotificationRead { notification_id })
+                            .await?;
+                        this.update(&mut cx, |this, _| {
+                            this.mark_as_read_tasks.remove(&notification_id);
+                        })?;
+                        Ok(())
+                    })
+                });
+        }
+    }
+
+    fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
+        if let Notification::ChannelMessageMention {
+            message_id,
+            channel_id,
+            ..
+        } = notification.clone()
+        {
+            if let Some(workspace) = self.workspace.upgrade() {
+                cx.window_context().defer(move |cx| {
+                    workspace.update(cx, |workspace, cx| {
+                        if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
+                            panel.update(cx, |panel, cx| {
+                                panel
+                                    .select_channel(channel_id, Some(message_id), cx)
+                                    .detach_and_log_err(cx);
+                            });
+                        }
+                    });
+                });
+            }
+        }
+    }
+
+    fn is_showing_notification(&self, notification: &Notification, cx: &ViewContext<Self>) -> bool {
+        if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
+            if let Some(workspace) = self.workspace.upgrade() {
+                return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(cx) {
+                    let panel = panel.read(cx);
+                    panel.is_scrolled_to_bottom()
+                        && panel
+                            .active_chat()
+                            .map_or(false, |chat| chat.read(cx).channel_id == *channel_id)
+                } else {
+                    false
+                };
+            }
+        }
+
+        false
+    }
+
+    fn render_sign_in_prompt(&self) -> AnyElement {
+        Button::new(
+            "sign_in_prompt_button",
+            "Sign in to view your notifications",
+        )
+        .on_click({
+            let client = self.client.clone();
+            move |_, cx| {
+                let client = client.clone();
+                cx.spawn(move |cx| async move {
+                    client.authenticate_and_connect(true, &cx).log_err().await;
+                })
+                .detach()
+            }
+        })
+        .into_any_element()
+    }
+
+    fn render_empty_state(&self) -> AnyElement {
+        Label::new("You have no notifications").into_any_element()
+    }
+
+    fn on_notification_event(
+        &mut self,
+        _: Model<NotificationStore>,
+        event: &NotificationEvent,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
+            NotificationEvent::NotificationRemoved { entry }
+            | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx),
+            NotificationEvent::NotificationsUpdated {
+                old_range,
+                new_count,
+            } => {
+                self.notification_list.splice(old_range.clone(), *new_count);
+                cx.notify();
+            }
+        }
+    }
+
+    fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
+        if self.is_showing_notification(&entry.notification, cx) {
+            return;
+        }
+
+        let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
+        else {
+            return;
+        };
+
+        let notification_id = entry.id;
+        self.current_notification_toast = Some((
+            notification_id,
+            cx.spawn(|this, mut cx| async move {
+                cx.background_executor().timer(TOAST_DURATION).await;
+                this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
+                    .ok();
+            }),
+        ));
+
+        self.workspace
+            .update(cx, |workspace, cx| {
+                workspace.dismiss_notification::<NotificationToast>(0, cx);
+                workspace.show_notification(0, cx, |cx| {
+                    let workspace = cx.view().downgrade();
+                    cx.build_view(|_| NotificationToast {
+                        notification_id,
+                        actor,
+                        text,
+                        workspace,
+                    })
+                })
+            })
+            .ok();
+    }
+
+    fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext<Self>) {
+        if let Some((current_id, _)) = &self.current_notification_toast {
+            if *current_id == notification_id {
+                self.current_notification_toast.take();
+                self.workspace
+                    .update(cx, |workspace, cx| {
+                        workspace.dismiss_notification::<NotificationToast>(0, cx)
+                    })
+                    .ok();
+            }
+        }
+    }
+
+    fn respond_to_notification(
+        &mut self,
+        notification: Notification,
+        response: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.notification_store.update(cx, |store, cx| {
+            store.respond_to_notification(notification, response, cx);
+        });
+    }
+}
+
+impl Render for NotificationPanel {
+    type Element = AnyElement;
+
+    fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement {
+        if self.client.user_id().is_none() {
+            self.render_sign_in_prompt()
+        } else if self.notification_list.item_count() == 0 {
+            self.render_empty_state()
+        } else {
+            v_stack()
+                .bg(gpui::red())
+                .child(
+                    h_stack()
+                        .child(Label::new("Notifications"))
+                        .child(IconElement::new(Icon::Envelope)),
+                )
+                .child(list(self.notification_list.clone()).size_full())
+                .size_full()
+                .into_any_element()
+        }
+    }
+}
+
+impl FocusableView for NotificationPanel {
+    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl EventEmitter<Event> for NotificationPanel {}
+impl EventEmitter<PanelEvent> for NotificationPanel {}
+
+impl Panel for NotificationPanel {
+    fn persistent_name() -> &'static str {
+        "NotificationPanel"
+    }
+
+    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+        NotificationPanelSettings::get_global(cx).dock
+    }
+
+    fn position_is_valid(&self, position: DockPosition) -> bool {
+        matches!(position, DockPosition::Left | DockPosition::Right)
+    }
+
+    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+        settings::update_settings_file::<NotificationPanelSettings>(
+            self.fs.clone(),
+            cx,
+            move |settings| settings.dock = Some(position),
+        );
+    }
+
+    fn size(&self, cx: &gpui::WindowContext) -> f32 {
+        self.width
+            .unwrap_or_else(|| NotificationPanelSettings::get_global(cx).default_width)
+    }
+
+    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+        self.width = size;
+        self.serialize(cx);
+        cx.notify();
+    }
+
+    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
+        self.active = active;
+        if self.notification_store.read(cx).notification_count() == 0 {
+            cx.emit(Event::Dismissed);
+        }
+    }
+
+    fn icon(&self, cx: &gpui::WindowContext) -> Option<Icon> {
+        (NotificationPanelSettings::get_global(cx).button
+            && self.notification_store.read(cx).notification_count() > 0)
+            .then(|| Icon::Bell)
+    }
+
+    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
+        let count = self.notification_store.read(cx).unread_notification_count();
+        if count == 0 {
+            None
+        } else {
+            Some(count.to_string())
+        }
+    }
+
+    fn toggle_action(&self) -> Box<dyn gpui::Action> {
+        Box::new(ToggleFocus)
+    }
+}
+
+pub struct NotificationToast {
+    notification_id: u64,
+    actor: Option<Arc<User>>,
+    text: String,
+    workspace: WeakView<Workspace>,
+}
+
+pub enum ToastEvent {
+    Dismiss,
+}
+
+impl NotificationToast {
+    fn focus_notification_panel(&self, cx: &mut ViewContext<Self>) {
+        let workspace = self.workspace.clone();
+        let notification_id = self.notification_id;
+        cx.window_context().defer(move |cx| {
+            workspace
+                .update(cx, |workspace, cx| {
+                    if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
+                        panel.update(cx, |panel, cx| {
+                            let store = panel.notification_store.read(cx);
+                            if let Some(entry) = store.notification_for_id(notification_id) {
+                                panel.did_click_notification(&entry.clone().notification, cx);
+                            }
+                        });
+                    }
+                })
+                .ok();
+        })
+    }
+}
+
+impl Render for NotificationToast {
+    type Element = Stateful<Div>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        let user = self.actor.clone();
+
+        h_stack()
+            .id("notification_panel_toast")
+            .children(user.map(|user| Avatar::new(user.avatar_uri.clone())))
+            .child(Label::new(self.text.clone()))
+            .child(
+                IconButton::new("close", Icon::Close)
+                    .on_click(cx.listener(|_, _, cx| cx.emit(ToastEvent::Dismiss))),
+            )
+            .on_click(cx.listener(|this, _, cx| {
+                this.focus_notification_panel(cx);
+                cx.emit(ToastEvent::Dismiss);
+            }))
+    }
+}
+
+impl EventEmitter<ToastEvent> for NotificationToast {}
+impl EventEmitter<DismissEvent> for NotificationToast {}
+
+fn format_timestamp(
+    mut timestamp: OffsetDateTime,
+    mut now: OffsetDateTime,
+    local_timezone: UtcOffset,
+) -> String {
+    timestamp = timestamp.to_offset(local_timezone);
+    now = now.to_offset(local_timezone);
+
+    let today = now.date();
+    let date = timestamp.date();
+    if date == today {
+        let difference = now - timestamp;
+        if difference >= Duration::from_secs(3600) {
+            format!("{}h", difference.whole_seconds() / 3600)
+        } else if difference >= Duration::from_secs(60) {
+            format!("{}m", difference.whole_seconds() / 60)
+        } else {
+            "just now".to_string()
+        }
+    } else if date.next_day() == Some(today) {
+        format!("yesterday")
+    } else {
+        format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
+    }
+}

crates/collab_ui2/src/notifications/incoming_call_notification.rs 🔗

@@ -114,14 +114,7 @@ impl IncomingCallNotification {
     }
     fn render_caller(&self, cx: &mut ViewContext<Self>) -> impl Element {
         h_stack()
-            .children(
-                self.state
-                    .call
-                    .calling_user
-                    .avatar
-                    .as_ref()
-                    .map(|avatar| Avatar::data(avatar.clone())),
-            )
+            .child(Avatar::new(self.state.call.calling_user.avatar_uri.clone()))
             .child(
                 v_stack()
                     .child(Label::new(format!(

crates/collab_ui2/src/notifications/project_shared_notification.rs 🔗

@@ -119,12 +119,7 @@ impl ProjectSharedNotification {
 
     fn render_owner(&self) -> impl Element {
         h_stack()
-            .children(
-                self.owner
-                    .avatar
-                    .clone()
-                    .map(|avatar| Avatar::data(avatar.clone())),
-            )
+            .child(Avatar::new(self.owner.avatar_uri.clone()))
             .child(
                 v_stack()
                     .child(Label::new(self.owner.github_login.clone()))

crates/project2/src/project2.rs 🔗

@@ -868,7 +868,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 🔗

@@ -1,6 +1,5 @@
 use crate::prelude::*;
-use gpui::{img, Div, Hsla, ImageData, ImageSource, Img, IntoElement, Styled};
-use std::sync::Arc;
+use gpui::{img, Div, Hsla, ImageSource, Img, IntoElement, Styled};
 
 #[derive(Debug, Default, PartialEq, Clone)]
 pub enum Shape {
@@ -58,16 +57,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 🔗

@@ -111,7 +111,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 🔗

@@ -363,7 +363,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/Cargo.toml 🔗

@@ -49,6 +49,7 @@ lsp = { package = "lsp2", path = "../lsp2" }
 menu = { package = "menu2", path = "../menu2" }
 # language_tools = { path = "../language_tools" }
 node_runtime = { path = "../node_runtime" }
+notifications = { package = "notifications2", path = "../notifications2" }
 assistant = { package = "assistant2", path = "../assistant2" }
 outline = { package = "outline2", path = "../outline2" }
 # plugin_runtime = { path = "../plugin_runtime",optional = true }

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());
@@ -220,6 +220,7 @@ fn main() {
         // activity_indicator::init(cx);
         // language_tools::init(cx);
         call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
+        notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
         collab_ui::init(&app_state, cx);
         feedback::init(cx);
         welcome::init(cx);

crates/zed2/src/zed2.rs 🔗

@@ -164,24 +164,24 @@ 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,
                 assistant_panel,
                 channels_panel,
                 chat_panel,
-                //     notification_panel,
+                notification_panel,
             ) = futures::try_join!(
                 project_panel,
                 terminal_panel,
                 assistant_panel,
                 channels_panel,
                 chat_panel,
-                //     notification_panel,
+                notification_panel,
             )?;
 
             workspace_handle.update(&mut cx, |workspace, cx| {
@@ -191,7 +191,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
                 workspace.add_panel(assistant_panel, cx);
                 workspace.add_panel(channels_panel, cx);
                 workspace.add_panel(chat_panel, cx);
-                //     workspace.add_panel(notification_panel, cx);
+                workspace.add_panel(notification_panel, cx);
 
                 // if !was_deserialized
                 //     && workspace