Cargo.lock 🔗
@@ -12071,6 +12071,7 @@ dependencies = [
"lsp2",
"menu2",
"node_runtime",
+ "notifications2",
"num_cpus",
"outline2",
"parking_lot 0.11.2",
Julia created
Release Notes:
- N/A
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(-)
@@ -12071,6 +12071,7 @@ dependencies = [
"lsp2",
"menu2",
"node_runtime",
+ "notifications2",
"num_cpus",
"outline2",
"parking_lot 0.11.2",
@@ -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);
@@ -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
@@ -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)))
-}
@@ -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()]
@@ -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());
@@ -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,
@@ -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);
@@ -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)
@@ -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))),
)
@@ -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)
}
@@ -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);
@@ -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
}
}
@@ -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, ¬ification, 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(¬ification, 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(¬ification_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, .. } = ¬ification {
-// 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, ¬ification, 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(¬ification, 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(¬ification_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, .. } = ¬ification {
+ 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())
+ }
+}
@@ -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!(
@@ -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()))
@@ -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,
@@ -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,
@@ -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
}
}
@@ -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),
)
}
@@ -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);
@@ -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 }
@@ -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);
@@ -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