Merge branch 'main' into editor-integration
This commit is contained in:
Generated
+21
@@ -1748,6 +1748,7 @@ dependencies = [
|
||||
"lsp2",
|
||||
"nanoid",
|
||||
"node_runtime",
|
||||
"notifications2",
|
||||
"parking_lot 0.11.2",
|
||||
"pretty_assertions",
|
||||
"project2",
|
||||
@@ -5484,6 +5485,26 @@ dependencies = [
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notifications2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"channel2",
|
||||
"client2",
|
||||
"clock",
|
||||
"collections",
|
||||
"db2",
|
||||
"feature_flags2",
|
||||
"gpui2",
|
||||
"rpc2",
|
||||
"settings2",
|
||||
"sum_tree",
|
||||
"text2",
|
||||
"time",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.3.7"
|
||||
|
||||
@@ -67,6 +67,7 @@ members = [
|
||||
"crates/multi_buffer2",
|
||||
"crates/node_runtime",
|
||||
"crates/notifications",
|
||||
"crates/notifications2",
|
||||
"crates/outline",
|
||||
"crates/picker",
|
||||
"crates/picker2",
|
||||
|
||||
@@ -72,10 +72,8 @@ fs = { package = "fs2", path = "../fs2", features = ["test-support"] }
|
||||
git = { package = "git3", path = "../git3", features = ["test-support"] }
|
||||
live_kit_client = { package = "live_kit_client2", path = "../live_kit_client2", features = ["test-support"] }
|
||||
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
|
||||
|
||||
node_runtime = { path = "../node_runtime" }
|
||||
#todo!(notifications)
|
||||
#notifications = { path = "../notifications", features = ["test-support"] }
|
||||
notifications = { package = "notifications2", path = "../notifications2", features = ["test-support"] }
|
||||
|
||||
project = { package = "project2", path = "../project2", features = ["test-support"] }
|
||||
rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
|
||||
|
||||
@@ -1,115 +1,115 @@
|
||||
use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
|
||||
use channel::{ChannelChat, ChannelMessageId};
|
||||
use channel::{ChannelChat, ChannelMessageId, MessageParams};
|
||||
use gpui::{BackgroundExecutor, Model, TestAppContext};
|
||||
use rpc::Notification;
|
||||
|
||||
// todo!(notifications)
|
||||
// #[gpui::test]
|
||||
// async fn test_basic_channel_messages(
|
||||
// executor: BackgroundExecutor,
|
||||
// mut cx_a: &mut TestAppContext,
|
||||
// mut cx_b: &mut TestAppContext,
|
||||
// mut cx_c: &mut TestAppContext,
|
||||
// ) {
|
||||
// let mut server = TestServer::start(executor.clone()).await;
|
||||
// let client_a = server.create_client(cx_a, "user_a").await;
|
||||
// let client_b = server.create_client(cx_b, "user_b").await;
|
||||
// let client_c = server.create_client(cx_c, "user_c").await;
|
||||
#[gpui::test]
|
||||
async fn test_basic_channel_messages(
|
||||
executor: BackgroundExecutor,
|
||||
mut cx_a: &mut TestAppContext,
|
||||
mut cx_b: &mut TestAppContext,
|
||||
mut cx_c: &mut TestAppContext,
|
||||
) {
|
||||
let mut server = TestServer::start(executor.clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
|
||||
// let channel_id = server
|
||||
// .make_channel(
|
||||
// "the-channel",
|
||||
// None,
|
||||
// (&client_a, cx_a),
|
||||
// &mut [(&client_b, cx_b), (&client_c, cx_c)],
|
||||
// )
|
||||
// .await;
|
||||
let channel_id = server
|
||||
.make_channel(
|
||||
"the-channel",
|
||||
None,
|
||||
(&client_a, cx_a),
|
||||
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
||||
)
|
||||
.await;
|
||||
|
||||
// let channel_chat_a = client_a
|
||||
// .channel_store()
|
||||
// .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
// let channel_chat_b = client_b
|
||||
// .channel_store()
|
||||
// .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
let channel_chat_a = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_chat_b = client_b
|
||||
.channel_store()
|
||||
.update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// let message_id = channel_chat_a
|
||||
// .update(cx_a, |c, cx| {
|
||||
// c.send_message(
|
||||
// MessageParams {
|
||||
// text: "hi @user_c!".into(),
|
||||
// mentions: vec![(3..10, client_c.id())],
|
||||
// },
|
||||
// cx,
|
||||
// )
|
||||
// .unwrap()
|
||||
// })
|
||||
// .await
|
||||
// .unwrap();
|
||||
// channel_chat_a
|
||||
// .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap())
|
||||
// .await
|
||||
// .unwrap();
|
||||
let message_id = channel_chat_a
|
||||
.update(cx_a, |c, cx| {
|
||||
c.send_message(
|
||||
MessageParams {
|
||||
text: "hi @user_c!".into(),
|
||||
mentions: vec![(3..10, client_c.id())],
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
channel_chat_a
|
||||
.update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// executor.run_until_parked();
|
||||
// channel_chat_b
|
||||
// .update(cx_b, |c, cx| c.send_message("three".into(), cx).unwrap())
|
||||
// .await
|
||||
// .unwrap();
|
||||
executor.run_until_parked();
|
||||
channel_chat_b
|
||||
.update(cx_b, |c, cx| c.send_message("three".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// executor.run_until_parked();
|
||||
executor.run_until_parked();
|
||||
|
||||
// let channel_chat_c = client_c
|
||||
// .channel_store()
|
||||
// .update(cx_c, |store, cx| store.open_channel_chat(channel_id, cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
let channel_chat_c = client_c
|
||||
.channel_store()
|
||||
.update(cx_c, |store, cx| store.open_channel_chat(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// for (chat, cx) in [
|
||||
// (&channel_chat_a, &mut cx_a),
|
||||
// (&channel_chat_b, &mut cx_b),
|
||||
// (&channel_chat_c, &mut cx_c),
|
||||
// ] {
|
||||
// chat.update(*cx, |c, _| {
|
||||
// assert_eq!(
|
||||
// c.messages()
|
||||
// .iter()
|
||||
// .map(|m| (m.body.as_str(), m.mentions.as_slice()))
|
||||
// .collect::<Vec<_>>(),
|
||||
// vec![
|
||||
// ("hi @user_c!", [(3..10, client_c.id())].as_slice()),
|
||||
// ("two", &[]),
|
||||
// ("three", &[])
|
||||
// ],
|
||||
// "results for user {}",
|
||||
// c.client().id(),
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
for (chat, cx) in [
|
||||
(&channel_chat_a, &mut cx_a),
|
||||
(&channel_chat_b, &mut cx_b),
|
||||
(&channel_chat_c, &mut cx_c),
|
||||
] {
|
||||
chat.update(*cx, |c, _| {
|
||||
assert_eq!(
|
||||
c.messages()
|
||||
.iter()
|
||||
.map(|m| (m.body.as_str(), m.mentions.as_slice()))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
("hi @user_c!", [(3..10, client_c.id())].as_slice()),
|
||||
("two", &[]),
|
||||
("three", &[])
|
||||
],
|
||||
"results for user {}",
|
||||
c.client().id(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// client_c.notification_store().update(cx_c, |store, _| {
|
||||
// assert_eq!(store.notification_count(), 2);
|
||||
// assert_eq!(store.unread_notification_count(), 1);
|
||||
// assert_eq!(
|
||||
// store.notification_at(0).unwrap().notification,
|
||||
// Notification::ChannelMessageMention {
|
||||
// message_id,
|
||||
// sender_id: client_a.id(),
|
||||
// channel_id,
|
||||
// }
|
||||
// );
|
||||
// assert_eq!(
|
||||
// store.notification_at(1).unwrap().notification,
|
||||
// Notification::ChannelInvitation {
|
||||
// channel_id,
|
||||
// channel_name: "the-channel".to_string(),
|
||||
// inviter_id: client_a.id()
|
||||
// }
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
client_c.notification_store().update(cx_c, |store, _| {
|
||||
assert_eq!(store.notification_count(), 2);
|
||||
assert_eq!(store.unread_notification_count(), 1);
|
||||
assert_eq!(
|
||||
store.notification_at(0).unwrap().notification,
|
||||
Notification::ChannelMessageMention {
|
||||
message_id,
|
||||
sender_id: client_a.id(),
|
||||
channel_id,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
store.notification_at(1).unwrap().notification,
|
||||
Notification::ChannelInvitation {
|
||||
channel_id,
|
||||
channel_name: "the-channel".to_string(),
|
||||
inviter_id: client_a.id()
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rejoin_channel_chat(
|
||||
|
||||
@@ -1128,6 +1128,8 @@ async fn test_channel_link_notifications(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
// the members-only channel is still shown for c, but hidden for b
|
||||
assert_channels_list_shape(
|
||||
client_b.channel_store(),
|
||||
|
||||
@@ -1,160 +1,160 @@
|
||||
//todo!(notifications)
|
||||
// use crate::tests::TestServer;
|
||||
// use gpui::{executor::Deterministic, TestAppContext};
|
||||
// use notifications::NotificationEvent;
|
||||
// use parking_lot::Mutex;
|
||||
// use rpc::{proto, Notification};
|
||||
// use std::sync::Arc;
|
||||
use std::sync::Arc;
|
||||
|
||||
// #[gpui::test]
|
||||
// async fn test_notifications(
|
||||
// deterministic: Arc<Deterministic>,
|
||||
// cx_a: &mut TestAppContext,
|
||||
// cx_b: &mut TestAppContext,
|
||||
// ) {
|
||||
// deterministic.forbid_parking();
|
||||
// let mut server = TestServer::start(&deterministic).await;
|
||||
// let client_a = server.create_client(cx_a, "user_a").await;
|
||||
// let client_b = server.create_client(cx_b, "user_b").await;
|
||||
use gpui::{BackgroundExecutor, TestAppContext};
|
||||
use notifications::NotificationEvent;
|
||||
use parking_lot::Mutex;
|
||||
use rpc::{proto, Notification};
|
||||
|
||||
// let notification_events_a = Arc::new(Mutex::new(Vec::new()));
|
||||
// let notification_events_b = Arc::new(Mutex::new(Vec::new()));
|
||||
// client_a.notification_store().update(cx_a, |_, cx| {
|
||||
// let events = notification_events_a.clone();
|
||||
// cx.subscribe(&cx.handle(), move |_, _, event, _| {
|
||||
// events.lock().push(event.clone());
|
||||
// })
|
||||
// .detach()
|
||||
// });
|
||||
// client_b.notification_store().update(cx_b, |_, cx| {
|
||||
// let events = notification_events_b.clone();
|
||||
// cx.subscribe(&cx.handle(), move |_, _, event, _| {
|
||||
// events.lock().push(event.clone());
|
||||
// })
|
||||
// .detach()
|
||||
// });
|
||||
use crate::tests::TestServer;
|
||||
|
||||
// // Client A sends a contact request to client B.
|
||||
// client_a
|
||||
// .user_store()
|
||||
// .update(cx_a, |store, cx| store.request_contact(client_b.id(), cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
#[gpui::test]
|
||||
async fn test_notifications(
|
||||
executor: BackgroundExecutor,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
let mut server = TestServer::start(executor.clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
// // Client B receives a contact request notification and responds to the
|
||||
// // request, accepting it.
|
||||
// deterministic.run_until_parked();
|
||||
// client_b.notification_store().update(cx_b, |store, cx| {
|
||||
// assert_eq!(store.notification_count(), 1);
|
||||
// assert_eq!(store.unread_notification_count(), 1);
|
||||
let notification_events_a = Arc::new(Mutex::new(Vec::new()));
|
||||
let notification_events_b = Arc::new(Mutex::new(Vec::new()));
|
||||
client_a.notification_store().update(cx_a, |_, cx| {
|
||||
let events = notification_events_a.clone();
|
||||
cx.subscribe(&cx.handle(), move |_, _, event, _| {
|
||||
events.lock().push(event.clone());
|
||||
})
|
||||
.detach()
|
||||
});
|
||||
client_b.notification_store().update(cx_b, |_, cx| {
|
||||
let events = notification_events_b.clone();
|
||||
cx.subscribe(&cx.handle(), move |_, _, event, _| {
|
||||
events.lock().push(event.clone());
|
||||
})
|
||||
.detach()
|
||||
});
|
||||
|
||||
// let entry = store.notification_at(0).unwrap();
|
||||
// assert_eq!(
|
||||
// entry.notification,
|
||||
// Notification::ContactRequest {
|
||||
// sender_id: client_a.id()
|
||||
// }
|
||||
// );
|
||||
// assert!(!entry.is_read);
|
||||
// assert_eq!(
|
||||
// ¬ification_events_b.lock()[0..],
|
||||
// &[
|
||||
// NotificationEvent::NewNotification {
|
||||
// entry: entry.clone(),
|
||||
// },
|
||||
// NotificationEvent::NotificationsUpdated {
|
||||
// old_range: 0..0,
|
||||
// new_count: 1
|
||||
// }
|
||||
// ]
|
||||
// );
|
||||
// Client A sends a contact request to client B.
|
||||
client_a
|
||||
.user_store()
|
||||
.update(cx_a, |store, cx| store.request_contact(client_b.id(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// store.respond_to_notification(entry.notification.clone(), true, cx);
|
||||
// });
|
||||
// Client B receives a contact request notification and responds to the
|
||||
// request, accepting it.
|
||||
executor.run_until_parked();
|
||||
client_b.notification_store().update(cx_b, |store, cx| {
|
||||
assert_eq!(store.notification_count(), 1);
|
||||
assert_eq!(store.unread_notification_count(), 1);
|
||||
|
||||
// // Client B sees the notification is now read, and that they responded.
|
||||
// deterministic.run_until_parked();
|
||||
// client_b.notification_store().read_with(cx_b, |store, _| {
|
||||
// assert_eq!(store.notification_count(), 1);
|
||||
// assert_eq!(store.unread_notification_count(), 0);
|
||||
let entry = store.notification_at(0).unwrap();
|
||||
assert_eq!(
|
||||
entry.notification,
|
||||
Notification::ContactRequest {
|
||||
sender_id: client_a.id()
|
||||
}
|
||||
);
|
||||
assert!(!entry.is_read);
|
||||
assert_eq!(
|
||||
¬ification_events_b.lock()[0..],
|
||||
&[
|
||||
NotificationEvent::NewNotification {
|
||||
entry: entry.clone(),
|
||||
},
|
||||
NotificationEvent::NotificationsUpdated {
|
||||
old_range: 0..0,
|
||||
new_count: 1
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
// let entry = store.notification_at(0).unwrap();
|
||||
// assert!(entry.is_read);
|
||||
// assert_eq!(entry.response, Some(true));
|
||||
// assert_eq!(
|
||||
// ¬ification_events_b.lock()[2..],
|
||||
// &[
|
||||
// NotificationEvent::NotificationRead {
|
||||
// entry: entry.clone(),
|
||||
// },
|
||||
// NotificationEvent::NotificationsUpdated {
|
||||
// old_range: 0..1,
|
||||
// new_count: 1
|
||||
// }
|
||||
// ]
|
||||
// );
|
||||
// });
|
||||
store.respond_to_notification(entry.notification.clone(), true, cx);
|
||||
});
|
||||
|
||||
// // Client A receives a notification that client B accepted their request.
|
||||
// client_a.notification_store().read_with(cx_a, |store, _| {
|
||||
// assert_eq!(store.notification_count(), 1);
|
||||
// assert_eq!(store.unread_notification_count(), 1);
|
||||
// Client B sees the notification is now read, and that they responded.
|
||||
executor.run_until_parked();
|
||||
client_b.notification_store().read_with(cx_b, |store, _| {
|
||||
assert_eq!(store.notification_count(), 1);
|
||||
assert_eq!(store.unread_notification_count(), 0);
|
||||
|
||||
// let entry = store.notification_at(0).unwrap();
|
||||
// assert_eq!(
|
||||
// entry.notification,
|
||||
// Notification::ContactRequestAccepted {
|
||||
// responder_id: client_b.id()
|
||||
// }
|
||||
// );
|
||||
// assert!(!entry.is_read);
|
||||
// });
|
||||
let entry = store.notification_at(0).unwrap();
|
||||
assert!(entry.is_read);
|
||||
assert_eq!(entry.response, Some(true));
|
||||
assert_eq!(
|
||||
¬ification_events_b.lock()[2..],
|
||||
&[
|
||||
NotificationEvent::NotificationRead {
|
||||
entry: entry.clone(),
|
||||
},
|
||||
NotificationEvent::NotificationsUpdated {
|
||||
old_range: 0..1,
|
||||
new_count: 1
|
||||
}
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// // Client A creates a channel and invites client B to be a member.
|
||||
// let channel_id = client_a
|
||||
// .channel_store()
|
||||
// .update(cx_a, |store, cx| {
|
||||
// store.create_channel("the-channel", None, cx)
|
||||
// })
|
||||
// .await
|
||||
// .unwrap();
|
||||
// client_a
|
||||
// .channel_store()
|
||||
// .update(cx_a, |store, cx| {
|
||||
// store.invite_member(channel_id, client_b.id(), proto::ChannelRole::Member, cx)
|
||||
// })
|
||||
// .await
|
||||
// .unwrap();
|
||||
// Client A receives a notification that client B accepted their request.
|
||||
client_a.notification_store().read_with(cx_a, |store, _| {
|
||||
assert_eq!(store.notification_count(), 1);
|
||||
assert_eq!(store.unread_notification_count(), 1);
|
||||
|
||||
// // Client B receives a channel invitation notification and responds to the
|
||||
// // invitation, accepting it.
|
||||
// deterministic.run_until_parked();
|
||||
// client_b.notification_store().update(cx_b, |store, cx| {
|
||||
// assert_eq!(store.notification_count(), 2);
|
||||
// assert_eq!(store.unread_notification_count(), 1);
|
||||
let entry = store.notification_at(0).unwrap();
|
||||
assert_eq!(
|
||||
entry.notification,
|
||||
Notification::ContactRequestAccepted {
|
||||
responder_id: client_b.id()
|
||||
}
|
||||
);
|
||||
assert!(!entry.is_read);
|
||||
});
|
||||
|
||||
// let entry = store.notification_at(0).unwrap();
|
||||
// assert_eq!(
|
||||
// entry.notification,
|
||||
// Notification::ChannelInvitation {
|
||||
// channel_id,
|
||||
// channel_name: "the-channel".to_string(),
|
||||
// inviter_id: client_a.id()
|
||||
// }
|
||||
// );
|
||||
// assert!(!entry.is_read);
|
||||
// Client A creates a channel and invites client B to be a member.
|
||||
let channel_id = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| {
|
||||
store.create_channel("the-channel", None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| {
|
||||
store.invite_member(channel_id, client_b.id(), proto::ChannelRole::Member, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// store.respond_to_notification(entry.notification.clone(), true, cx);
|
||||
// });
|
||||
// Client B receives a channel invitation notification and responds to the
|
||||
// invitation, accepting it.
|
||||
executor.run_until_parked();
|
||||
client_b.notification_store().update(cx_b, |store, cx| {
|
||||
assert_eq!(store.notification_count(), 2);
|
||||
assert_eq!(store.unread_notification_count(), 1);
|
||||
|
||||
// // Client B sees the notification is now read, and that they responded.
|
||||
// deterministic.run_until_parked();
|
||||
// client_b.notification_store().read_with(cx_b, |store, _| {
|
||||
// assert_eq!(store.notification_count(), 2);
|
||||
// assert_eq!(store.unread_notification_count(), 0);
|
||||
let entry = store.notification_at(0).unwrap();
|
||||
assert_eq!(
|
||||
entry.notification,
|
||||
Notification::ChannelInvitation {
|
||||
channel_id,
|
||||
channel_name: "the-channel".to_string(),
|
||||
inviter_id: client_a.id()
|
||||
}
|
||||
);
|
||||
assert!(!entry.is_read);
|
||||
|
||||
// let entry = store.notification_at(0).unwrap();
|
||||
// assert!(entry.is_read);
|
||||
// assert_eq!(entry.response, Some(true));
|
||||
// });
|
||||
// }
|
||||
store.respond_to_notification(entry.notification.clone(), true, cx);
|
||||
});
|
||||
|
||||
// Client B sees the notification is now read, and that they responded.
|
||||
executor.run_until_parked();
|
||||
client_b.notification_store().read_with(cx_b, |store, _| {
|
||||
assert_eq!(store.notification_count(), 2);
|
||||
assert_eq!(store.unread_notification_count(), 0);
|
||||
|
||||
let entry = store.notification_at(0).unwrap();
|
||||
assert!(entry.is_read);
|
||||
assert_eq!(entry.response, Some(true));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -220,14 +220,6 @@ impl RandomizedTest for RandomChannelBufferTest {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn on_client_added(client: &Rc<TestClient>, cx: &mut TestAppContext) {
|
||||
let channel_store = client.channel_store();
|
||||
while channel_store.read_with(cx, |store, _| store.channel_count() == 0) {
|
||||
// todo!(notifications)
|
||||
// channel_store.next_notification(cx).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_quiesce(server: &mut TestServer, clients: &mut [(Rc<TestClient>, TestAppContext)]) {
|
||||
let channels = server.app_state.db.all_channels().await.unwrap();
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ pub trait RandomizedTest: 'static + Sized {
|
||||
|
||||
async fn initialize(server: &mut TestServer, users: &[UserTestPlan]);
|
||||
|
||||
async fn on_client_added(client: &Rc<TestClient>, cx: &mut TestAppContext);
|
||||
async fn on_client_added(_client: &Rc<TestClient>, _cx: &mut TestAppContext) {}
|
||||
|
||||
async fn on_quiesce(server: &mut TestServer, client: &mut [(Rc<TestClient>, TestAppContext)]);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use gpui::{BackgroundExecutor, Context, Model, TestAppContext, WindowHandle};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
|
||||
use notifications::NotificationStore;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, WorktreeId};
|
||||
use rpc::{proto::ChannelRole, RECEIVE_TIMEOUT};
|
||||
@@ -47,8 +48,7 @@ pub struct TestClient {
|
||||
pub username: String,
|
||||
pub app_state: Arc<workspace::AppState>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
// todo!(notifications)
|
||||
// notification_store: Model<NotificationStore>,
|
||||
notification_store: Model<NotificationStore>,
|
||||
state: RefCell<TestClientState>,
|
||||
}
|
||||
|
||||
@@ -234,8 +234,7 @@ impl TestServer {
|
||||
audio::init((), cx);
|
||||
call::init(client.clone(), user_store.clone(), cx);
|
||||
channel::init(&client, user_store.clone(), cx);
|
||||
//todo(notifications)
|
||||
// notifications::init(client.clone(), user_store, cx);
|
||||
notifications::init(client.clone(), user_store, cx);
|
||||
});
|
||||
|
||||
client
|
||||
@@ -247,8 +246,7 @@ impl TestServer {
|
||||
app_state,
|
||||
username: name.to_string(),
|
||||
channel_store: cx.read(ChannelStore::global).clone(),
|
||||
// todo!(notifications)
|
||||
// notification_store: cx.read(NotificationStore::global).clone(),
|
||||
notification_store: cx.read(NotificationStore::global).clone(),
|
||||
state: Default::default(),
|
||||
};
|
||||
client.wait_for_current_user(cx).await;
|
||||
@@ -456,10 +454,9 @@ impl TestClient {
|
||||
&self.channel_store
|
||||
}
|
||||
|
||||
// todo!(notifications)
|
||||
// pub fn notification_store(&self) -> &Model<NotificationStore> {
|
||||
// &self.notification_store
|
||||
// }
|
||||
pub fn notification_store(&self) -> &Model<NotificationStore> {
|
||||
&self.notification_store
|
||||
}
|
||||
|
||||
pub fn user_store(&self) -> &Model<UserStore> {
|
||||
&self.app_state.user_store
|
||||
|
||||
@@ -9580,7 +9580,7 @@ impl Render for Editor {
|
||||
|
||||
impl InputHandler for Editor {
|
||||
fn text_for_range(
|
||||
&self,
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<String> {
|
||||
@@ -9593,7 +9593,7 @@ impl InputHandler for Editor {
|
||||
)
|
||||
}
|
||||
|
||||
fn selected_text_range(&self, cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
|
||||
fn selected_text_range(&mut self, cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
|
||||
// Prevent the IME menu from appearing when holding down an alphabetic key
|
||||
// while input is disabled.
|
||||
if !self.input_enabled {
|
||||
|
||||
@@ -17,11 +17,10 @@ use collections::{BTreeMap, HashMap};
|
||||
use gpui::{
|
||||
black, hsla, point, px, relative, size, transparent_black, Action, AnyElement,
|
||||
BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchContext, DispatchPhase,
|
||||
Edges, Element, ElementId, Entity, FocusHandle, GlobalElementId, Hsla, InputHandler,
|
||||
InputHandlerView, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers,
|
||||
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent,
|
||||
ShapedGlyph, Size, Style, TextRun, TextStyle, TextSystem, ViewContext, WindowContext,
|
||||
WrappedLineLayout,
|
||||
Edges, Element, ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla,
|
||||
InputHandler, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, MouseButton,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, ShapedGlyph, Size,
|
||||
Style, TextRun, TextStyle, TextSystem, ViewContext, WindowContext, WrappedLineLayout,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::language_settings::ShowWhitespaceSetting;
|
||||
@@ -2517,16 +2516,10 @@ impl Element<Editor> for EditorElement {
|
||||
self.paint_gutter(gutter_bounds, &layout, editor, cx);
|
||||
}
|
||||
self.paint_text(text_bounds, &layout, editor, cx);
|
||||
let input_handler = ElementInputHandler::new(bounds, cx);
|
||||
cx.handle_input(&editor.focus_handle, input_handler);
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_text_input<'a>(
|
||||
&self,
|
||||
editor: &'a mut Editor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<(Box<dyn InputHandlerView>, &'a FocusHandle)> {
|
||||
Some((Box::new(cx.view()), &editor.focus_handle))
|
||||
}
|
||||
}
|
||||
|
||||
// impl EditorElement {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use crate::{
|
||||
BorrowWindow, Bounds, ElementId, FocusHandle, InputHandlerView, LayoutId, Pixels, ViewContext,
|
||||
WindowInputHandler,
|
||||
};
|
||||
use crate::{BorrowWindow, Bounds, ElementId, LayoutId, Pixels, ViewContext};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
pub(crate) use smallvec::SmallVec;
|
||||
use std::{any::Any, mem};
|
||||
@@ -34,14 +31,6 @@ pub trait Element<V: 'static> {
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
);
|
||||
|
||||
fn handle_text_input<'a>(
|
||||
&self,
|
||||
_view_state: &'a mut V,
|
||||
_cx: &mut ViewContext<V>,
|
||||
) -> Option<(Box<dyn InputHandlerView>, &'a FocusHandle)> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
@@ -165,18 +154,6 @@ where
|
||||
mut frame_state,
|
||||
} => {
|
||||
let bounds = cx.layout_bounds(layout_id);
|
||||
if let Some((input_handler, focus_handle)) =
|
||||
self.element.handle_text_input(view_state, cx)
|
||||
{
|
||||
if focus_handle.is_focused(cx) {
|
||||
cx.window.requested_input_handler = Some(Box::new(WindowInputHandler {
|
||||
cx: cx.app.this.clone(),
|
||||
window: cx.window_handle(),
|
||||
input_handler,
|
||||
element_bounds: bounds,
|
||||
}));
|
||||
}
|
||||
}
|
||||
if let Some(id) = self.element.id() {
|
||||
cx.with_element_state(id, |element_state, cx| {
|
||||
let mut element_state = element_state.unwrap();
|
||||
|
||||
@@ -9,6 +9,7 @@ mod executor;
|
||||
mod focusable;
|
||||
mod geometry;
|
||||
mod image_cache;
|
||||
mod input;
|
||||
mod interactive;
|
||||
mod keymap;
|
||||
mod platform;
|
||||
@@ -24,7 +25,6 @@ mod text_system;
|
||||
mod util;
|
||||
mod view;
|
||||
mod window;
|
||||
mod window_input_handler;
|
||||
|
||||
mod private {
|
||||
/// A mechanism for restricting implementations of a trait to only those in GPUI.
|
||||
@@ -45,6 +45,7 @@ pub use focusable::*;
|
||||
pub use geometry::*;
|
||||
pub use gpui2_macros::*;
|
||||
pub use image_cache::*;
|
||||
pub use input::*;
|
||||
pub use interactive::*;
|
||||
pub use keymap::*;
|
||||
pub use platform::*;
|
||||
@@ -66,7 +67,6 @@ pub use text_system::*;
|
||||
pub use util::arc_cow::ArcCow;
|
||||
pub use view::*;
|
||||
pub use window::*;
|
||||
pub use window_input_handler::*;
|
||||
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use std::{
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
use crate::{AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext};
|
||||
use std::ops::Range;
|
||||
|
||||
/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc.
|
||||
///
|
||||
/// Once your view `V` implements this trait, you can use it to construct an [ElementInputHandler<V>].
|
||||
/// This input handler can then be assigned during paint by calling [WindowContext::handle_input].
|
||||
pub trait InputHandler: 'static + Sized {
|
||||
fn text_for_range(&mut self, range: Range<usize>, cx: &mut ViewContext<Self>)
|
||||
-> Option<String>;
|
||||
fn selected_text_range(&mut self, cx: &mut ViewContext<Self>) -> Option<Range<usize>>;
|
||||
fn marked_text_range(&self, cx: &mut ViewContext<Self>) -> Option<Range<usize>>;
|
||||
fn unmark_text(&mut self, cx: &mut ViewContext<Self>);
|
||||
fn replace_text_in_range(
|
||||
&mut self,
|
||||
range: Option<Range<usize>>,
|
||||
text: &str,
|
||||
cx: &mut ViewContext<Self>,
|
||||
);
|
||||
fn replace_and_mark_text_in_range(
|
||||
&mut self,
|
||||
range: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
new_selected_range: Option<Range<usize>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
);
|
||||
fn bounds_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
element_bounds: Bounds<Pixels>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Bounds<Pixels>>;
|
||||
}
|
||||
|
||||
/// The canonical implementation of `PlatformInputHandler`. Call `WindowContext::handle_input`
|
||||
/// with an instance during your element's paint.
|
||||
pub struct ElementInputHandler<V> {
|
||||
view: View<V>,
|
||||
element_bounds: Bounds<Pixels>,
|
||||
cx: AsyncWindowContext,
|
||||
}
|
||||
|
||||
impl<V: 'static> ElementInputHandler<V> {
|
||||
/// Used in [Element::paint] with the element's bounds and a view context for its
|
||||
/// containing view.
|
||||
pub fn new(element_bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) -> Self {
|
||||
ElementInputHandler {
|
||||
view: cx.view(),
|
||||
element_bounds,
|
||||
cx: cx.to_async(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: InputHandler> PlatformInputHandler for ElementInputHandler<V> {
|
||||
fn selected_text_range(&mut self) -> Option<Range<usize>> {
|
||||
self.view
|
||||
.update(&mut self.cx, |view, cx| view.selected_text_range(cx))
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn marked_text_range(&mut self) -> Option<Range<usize>> {
|
||||
self.view
|
||||
.update(&mut self.cx, |view, cx| view.marked_text_range(cx))
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn text_for_range(&mut self, range_utf16: Range<usize>) -> Option<String> {
|
||||
self.view
|
||||
.update(&mut self.cx, |view, cx| {
|
||||
view.text_for_range(range_utf16, cx)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
|
||||
self.view
|
||||
.update(&mut self.cx, |view, cx| {
|
||||
view.replace_text_in_range(replacement_range, text, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn replace_and_mark_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
new_selected_range: Option<Range<usize>>,
|
||||
) {
|
||||
self.view
|
||||
.update(&mut self.cx, |view, cx| {
|
||||
view.replace_and_mark_text_in_range(range_utf16, new_text, new_selected_range, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self) {
|
||||
self.view
|
||||
.update(&mut self.cx, |view, cx| view.unmark_text(cx))
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
|
||||
self.view
|
||||
.update(&mut self.cx, |view, cx| {
|
||||
view.bounds_for_range(range_utf16, self.element_bounds, cx)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
@@ -293,10 +293,10 @@ impl From<TileId> for etagere::AllocId {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PlatformInputHandler {
|
||||
fn selected_text_range(&self) -> Option<Range<usize>>;
|
||||
fn marked_text_range(&self) -> Option<Range<usize>>;
|
||||
fn text_for_range(&self, range_utf16: Range<usize>) -> Option<String>;
|
||||
pub trait PlatformInputHandler: 'static {
|
||||
fn selected_text_range(&mut self) -> Option<Range<usize>>;
|
||||
fn marked_text_range(&mut self) -> Option<Range<usize>>;
|
||||
fn text_for_range(&mut self, range_utf16: Range<usize>) -> Option<String>;
|
||||
fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str);
|
||||
fn replace_and_mark_text_in_range(
|
||||
&mut self,
|
||||
@@ -305,7 +305,7 @@ pub trait PlatformInputHandler {
|
||||
new_selected_range: Option<Range<usize>>,
|
||||
);
|
||||
fn unmark_text(&mut self);
|
||||
fn bounds_for_range(&self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>>;
|
||||
fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -211,7 +211,6 @@ pub struct Window {
|
||||
default_prevented: bool,
|
||||
mouse_position: Point<Pixels>,
|
||||
requested_cursor_style: Option<CursorStyle>,
|
||||
pub(crate) requested_input_handler: Option<Box<dyn PlatformInputHandler>>,
|
||||
scale_factor: f32,
|
||||
bounds: WindowBounds,
|
||||
bounds_observers: SubscriberSet<(), AnyObserver>,
|
||||
@@ -311,7 +310,6 @@ impl Window {
|
||||
default_prevented: true,
|
||||
mouse_position,
|
||||
requested_cursor_style: None,
|
||||
requested_input_handler: None,
|
||||
scale_factor,
|
||||
bounds,
|
||||
bounds_observers: SubscriberSet::new(),
|
||||
@@ -1052,9 +1050,6 @@ impl<'a> WindowContext<'a> {
|
||||
.take()
|
||||
.unwrap_or(CursorStyle::Arrow);
|
||||
self.platform.set_cursor_style(cursor_style);
|
||||
if let Some(handler) = self.window.requested_input_handler.take() {
|
||||
self.window.platform_window.set_input_handler(handler);
|
||||
}
|
||||
|
||||
self.window.dirty = false;
|
||||
}
|
||||
@@ -2182,6 +2177,21 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/// Set an input handler, such as [ElementInputHandler], which interfaces with the
|
||||
/// platform to receive textual input with proper integration with concerns such
|
||||
/// as IME interactions.
|
||||
pub fn handle_input(
|
||||
&mut self,
|
||||
focus_handle: &FocusHandle,
|
||||
input_handler: impl PlatformInputHandler,
|
||||
) {
|
||||
if focus_handle.is_focused(self) {
|
||||
self.window
|
||||
.platform_window
|
||||
.set_input_handler(Box::new(input_handler));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> ViewContext<'_, V> {
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
use crate::{
|
||||
AnyWindowHandle, AppCell, Bounds, Context, Pixels, PlatformInputHandler, View, ViewContext,
|
||||
WindowContext,
|
||||
};
|
||||
use std::{ops::Range, rc::Weak};
|
||||
|
||||
pub struct WindowInputHandler {
|
||||
pub cx: Weak<AppCell>,
|
||||
pub input_handler: Box<dyn InputHandlerView>,
|
||||
pub window: AnyWindowHandle,
|
||||
pub element_bounds: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
pub trait InputHandlerView {
|
||||
fn text_for_range(&self, range: Range<usize>, cx: &mut WindowContext) -> Option<String>;
|
||||
fn selected_text_range(&self, cx: &mut WindowContext) -> Option<Range<usize>>;
|
||||
fn marked_text_range(&self, cx: &mut WindowContext) -> Option<Range<usize>>;
|
||||
fn unmark_text(&self, cx: &mut WindowContext);
|
||||
fn replace_text_in_range(
|
||||
&self,
|
||||
range: Option<Range<usize>>,
|
||||
text: &str,
|
||||
cx: &mut WindowContext,
|
||||
);
|
||||
fn replace_and_mark_text_in_range(
|
||||
&self,
|
||||
range: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
new_selected_range: Option<Range<usize>>,
|
||||
cx: &mut WindowContext,
|
||||
);
|
||||
fn bounds_for_range(
|
||||
&self,
|
||||
range_utf16: std::ops::Range<usize>,
|
||||
element_bounds: crate::Bounds<Pixels>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<crate::Bounds<Pixels>>;
|
||||
}
|
||||
|
||||
pub trait InputHandler: Sized {
|
||||
fn text_for_range(&self, range: Range<usize>, cx: &mut ViewContext<Self>) -> Option<String>;
|
||||
fn selected_text_range(&self, cx: &mut ViewContext<Self>) -> Option<Range<usize>>;
|
||||
fn marked_text_range(&self, cx: &mut ViewContext<Self>) -> Option<Range<usize>>;
|
||||
fn unmark_text(&mut self, cx: &mut ViewContext<Self>);
|
||||
fn replace_text_in_range(
|
||||
&mut self,
|
||||
range: Option<Range<usize>>,
|
||||
text: &str,
|
||||
cx: &mut ViewContext<Self>,
|
||||
);
|
||||
fn replace_and_mark_text_in_range(
|
||||
&mut self,
|
||||
range: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
new_selected_range: Option<Range<usize>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
);
|
||||
fn bounds_for_range(
|
||||
&mut self,
|
||||
range_utf16: std::ops::Range<usize>,
|
||||
element_bounds: crate::Bounds<Pixels>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<crate::Bounds<Pixels>>;
|
||||
}
|
||||
|
||||
impl<V: InputHandler + 'static> InputHandlerView for View<V> {
|
||||
fn text_for_range(&self, range: Range<usize>, cx: &mut WindowContext) -> Option<String> {
|
||||
self.update(cx, |this, cx| this.text_for_range(range, cx))
|
||||
}
|
||||
|
||||
fn selected_text_range(&self, cx: &mut WindowContext) -> Option<Range<usize>> {
|
||||
self.update(cx, |this, cx| this.selected_text_range(cx))
|
||||
}
|
||||
|
||||
fn marked_text_range(&self, cx: &mut WindowContext) -> Option<Range<usize>> {
|
||||
self.update(cx, |this, cx| this.marked_text_range(cx))
|
||||
}
|
||||
|
||||
fn unmark_text(&self, cx: &mut WindowContext) {
|
||||
self.update(cx, |this, cx| this.unmark_text(cx))
|
||||
}
|
||||
|
||||
fn replace_text_in_range(
|
||||
&self,
|
||||
range: Option<Range<usize>>,
|
||||
text: &str,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
self.update(cx, |this, cx| this.replace_text_in_range(range, text, cx))
|
||||
}
|
||||
|
||||
fn replace_and_mark_text_in_range(
|
||||
&self,
|
||||
range: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
new_selected_range: Option<Range<usize>>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
self.update(cx, |this, cx| {
|
||||
this.replace_and_mark_text_in_range(range, new_text, new_selected_range, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn bounds_for_range(
|
||||
&self,
|
||||
range_utf16: std::ops::Range<usize>,
|
||||
element_bounds: crate::Bounds<Pixels>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<crate::Bounds<Pixels>> {
|
||||
self.update(cx, |this, cx| {
|
||||
this.bounds_for_range(range_utf16, element_bounds, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformInputHandler for WindowInputHandler {
|
||||
fn selected_text_range(&self) -> Option<Range<usize>> {
|
||||
self.update(|handler, cx| handler.selected_text_range(cx))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn marked_text_range(&self) -> Option<Range<usize>> {
|
||||
self.update(|handler, cx| handler.marked_text_range(cx))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn text_for_range(&self, range_utf16: Range<usize>) -> Option<String> {
|
||||
self.update(|handler, cx| handler.text_for_range(range_utf16, cx))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
|
||||
self.update(|handler, cx| handler.replace_text_in_range(replacement_range, text, cx));
|
||||
}
|
||||
|
||||
fn replace_and_mark_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
new_selected_range: Option<Range<usize>>,
|
||||
) {
|
||||
self.update(|handler, cx| {
|
||||
handler.replace_and_mark_text_in_range(range_utf16, new_text, new_selected_range, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self) {
|
||||
self.update(|handler, cx| handler.unmark_text(cx));
|
||||
}
|
||||
|
||||
fn bounds_for_range(&self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
|
||||
self.update(|handler, cx| handler.bounds_for_range(range_utf16, self.element_bounds, cx))
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowInputHandler {
|
||||
fn update<R>(
|
||||
&self,
|
||||
f: impl FnOnce(&dyn InputHandlerView, &mut WindowContext) -> R,
|
||||
) -> Option<R> {
|
||||
let cx = self.cx.upgrade()?;
|
||||
let mut cx = cx.borrow_mut();
|
||||
cx.update_window(self.window, |_, cx| f(&*self.input_handler, cx))
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "notifications2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/notification_store2.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"channel/test-support",
|
||||
"collections/test-support",
|
||||
"gpui/test-support",
|
||||
"rpc/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
channel = { package = "channel2", path = "../channel2" }
|
||||
client = { package = "client2", path = "../client2" }
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
db = { package = "db2", path = "../db2" }
|
||||
feature_flags = { package = "feature_flags2", path = "../feature_flags2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
rpc = { package = "rpc2", path = "../rpc2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
text = { package = "text2", path = "../text2" }
|
||||
util = { path = "../util" }
|
||||
|
||||
anyhow.workspace = true
|
||||
time.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { package = "client2", path = "../client2", features = ["test-support"] }
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
|
||||
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
@@ -0,0 +1,466 @@
|
||||
use anyhow::{Context, Result};
|
||||
use channel::{ChannelMessage, ChannelMessageId, ChannelStore};
|
||||
use client::{Client, UserStore};
|
||||
use collections::HashMap;
|
||||
use db::smol::stream::StreamExt;
|
||||
use gpui::{AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, Task};
|
||||
use rpc::{proto, Notification, TypedEnvelope};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use sum_tree::{Bias, SumTree};
|
||||
use time::OffsetDateTime;
|
||||
use util::ResultExt;
|
||||
|
||||
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
|
||||
let notification_store = cx.build_model(|cx| NotificationStore::new(client, user_store, cx));
|
||||
cx.set_global(notification_store);
|
||||
}
|
||||
|
||||
pub struct NotificationStore {
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
channel_messages: HashMap<u64, ChannelMessage>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
notifications: SumTree<NotificationEntry>,
|
||||
loaded_all_notifications: bool,
|
||||
_watch_connection_status: Task<Option<()>>,
|
||||
_subscriptions: Vec<client::Subscription>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum NotificationEvent {
|
||||
NotificationsUpdated {
|
||||
old_range: Range<usize>,
|
||||
new_count: usize,
|
||||
},
|
||||
NewNotification {
|
||||
entry: NotificationEntry,
|
||||
},
|
||||
NotificationRemoved {
|
||||
entry: NotificationEntry,
|
||||
},
|
||||
NotificationRead {
|
||||
entry: NotificationEntry,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct NotificationEntry {
|
||||
pub id: u64,
|
||||
pub notification: Notification,
|
||||
pub timestamp: OffsetDateTime,
|
||||
pub is_read: bool,
|
||||
pub response: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct NotificationSummary {
|
||||
max_id: u64,
|
||||
count: usize,
|
||||
unread_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct Count(usize);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct UnreadCount(usize);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct NotificationId(u64);
|
||||
|
||||
impl NotificationStore {
|
||||
pub fn global(cx: &AppContext) -> Model<Self> {
|
||||
cx.global::<Model<Self>>().clone()
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let mut connection_status = client.status();
|
||||
let watch_connection_status = cx.spawn(|this, mut cx| async move {
|
||||
while let Some(status) = connection_status.next().await {
|
||||
let this = this.upgrade()?;
|
||||
match status {
|
||||
client::Status::Connected { .. } => {
|
||||
if let Some(task) = this
|
||||
.update(&mut cx, |this, cx| this.handle_connect(cx))
|
||||
.log_err()?
|
||||
{
|
||||
task.await.log_err()?;
|
||||
}
|
||||
}
|
||||
_ => this
|
||||
.update(&mut cx, |this, cx| this.handle_disconnect(cx))
|
||||
.log_err()?,
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
});
|
||||
|
||||
Self {
|
||||
channel_store: ChannelStore::global(cx),
|
||||
notifications: Default::default(),
|
||||
loaded_all_notifications: false,
|
||||
channel_messages: Default::default(),
|
||||
_watch_connection_status: watch_connection_status,
|
||||
_subscriptions: vec![
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_new_notification),
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_delete_notification),
|
||||
],
|
||||
user_store,
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notification_count(&self) -> usize {
|
||||
self.notifications.summary().count
|
||||
}
|
||||
|
||||
pub fn unread_notification_count(&self) -> usize {
|
||||
self.notifications.summary().unread_count
|
||||
}
|
||||
|
||||
pub fn channel_message_for_id(&self, id: u64) -> Option<&ChannelMessage> {
|
||||
self.channel_messages.get(&id)
|
||||
}
|
||||
|
||||
// Get the nth newest notification.
|
||||
pub fn notification_at(&self, ix: usize) -> Option<&NotificationEntry> {
|
||||
let count = self.notifications.summary().count;
|
||||
if ix >= count {
|
||||
return None;
|
||||
}
|
||||
let ix = count - 1 - ix;
|
||||
let mut cursor = self.notifications.cursor::<Count>();
|
||||
cursor.seek(&Count(ix), Bias::Right, &());
|
||||
cursor.item()
|
||||
}
|
||||
|
||||
pub fn notification_for_id(&self, id: u64) -> Option<&NotificationEntry> {
|
||||
let mut cursor = self.notifications.cursor::<NotificationId>();
|
||||
cursor.seek(&NotificationId(id), Bias::Left, &());
|
||||
if let Some(item) = cursor.item() {
|
||||
if item.id == id {
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn load_more_notifications(
|
||||
&self,
|
||||
clear_old: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
if self.loaded_all_notifications && !clear_old {
|
||||
return None;
|
||||
}
|
||||
|
||||
let before_id = if clear_old {
|
||||
None
|
||||
} else {
|
||||
self.notifications.first().map(|entry| entry.id)
|
||||
};
|
||||
let request = self.client.request(proto::GetNotifications { before_id });
|
||||
Some(cx.spawn(|this, mut cx| async move {
|
||||
let this = this
|
||||
.upgrade()
|
||||
.context("Notification store was dropped while loading notifications")?;
|
||||
|
||||
let response = request.await?;
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.loaded_all_notifications = response.done
|
||||
})?;
|
||||
Self::add_notifications(
|
||||
this,
|
||||
response.notifications,
|
||||
AddNotificationsOptions {
|
||||
is_new: false,
|
||||
clear_old,
|
||||
includes_first: response.done,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<Result<()>>> {
|
||||
self.notifications = Default::default();
|
||||
self.channel_messages = Default::default();
|
||||
cx.notify();
|
||||
self.load_more_notifications(true, cx)
|
||||
}
|
||||
|
||||
fn handle_disconnect(&mut self, cx: &mut ModelContext<Self>) {
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
async fn handle_new_notification(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::AddNotification>,
|
||||
_: Arc<Client>,
|
||||
cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
Self::add_notifications(
|
||||
this,
|
||||
envelope.payload.notification.into_iter().collect(),
|
||||
AddNotificationsOptions {
|
||||
is_new: true,
|
||||
clear_old: false,
|
||||
includes_first: false,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn handle_delete_notification(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::DeleteNotification>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.splice_notifications([(envelope.payload.notification_id, None)], false, cx);
|
||||
Ok(())
|
||||
})?
|
||||
}
|
||||
|
||||
async fn add_notifications(
|
||||
this: Model<Self>,
|
||||
notifications: Vec<proto::Notification>,
|
||||
options: AddNotificationsOptions,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let mut user_ids = Vec::new();
|
||||
let mut message_ids = Vec::new();
|
||||
|
||||
let notifications = notifications
|
||||
.into_iter()
|
||||
.filter_map(|message| {
|
||||
Some(NotificationEntry {
|
||||
id: message.id,
|
||||
is_read: message.is_read,
|
||||
timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)
|
||||
.ok()?,
|
||||
notification: Notification::from_proto(&message)?,
|
||||
response: message.response,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if notifications.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for entry in ¬ifications {
|
||||
match entry.notification {
|
||||
Notification::ChannelInvitation { inviter_id, .. } => {
|
||||
user_ids.push(inviter_id);
|
||||
}
|
||||
Notification::ContactRequest {
|
||||
sender_id: requester_id,
|
||||
} => {
|
||||
user_ids.push(requester_id);
|
||||
}
|
||||
Notification::ContactRequestAccepted {
|
||||
responder_id: contact_id,
|
||||
} => {
|
||||
user_ids.push(contact_id);
|
||||
}
|
||||
Notification::ChannelMessageMention {
|
||||
sender_id,
|
||||
message_id,
|
||||
..
|
||||
} => {
|
||||
user_ids.push(sender_id);
|
||||
message_ids.push(message_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (user_store, channel_store) = this.read_with(&cx, |this, _| {
|
||||
(this.user_store.clone(), this.channel_store.clone())
|
||||
})?;
|
||||
|
||||
user_store
|
||||
.update(&mut cx, |store, cx| store.get_users(user_ids, cx))?
|
||||
.await?;
|
||||
let messages = channel_store
|
||||
.update(&mut cx, |store, cx| {
|
||||
store.fetch_channel_messages(message_ids, cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if options.clear_old {
|
||||
cx.emit(NotificationEvent::NotificationsUpdated {
|
||||
old_range: 0..this.notifications.summary().count,
|
||||
new_count: 0,
|
||||
});
|
||||
this.notifications = SumTree::default();
|
||||
this.channel_messages.clear();
|
||||
this.loaded_all_notifications = false;
|
||||
}
|
||||
|
||||
if options.includes_first {
|
||||
this.loaded_all_notifications = true;
|
||||
}
|
||||
|
||||
this.channel_messages
|
||||
.extend(messages.into_iter().filter_map(|message| {
|
||||
if let ChannelMessageId::Saved(id) = message.id {
|
||||
Some((id, message))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}));
|
||||
|
||||
this.splice_notifications(
|
||||
notifications
|
||||
.into_iter()
|
||||
.map(|notification| (notification.id, Some(notification))),
|
||||
options.is_new,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn splice_notifications(
|
||||
&mut self,
|
||||
notifications: impl IntoIterator<Item = (u64, Option<NotificationEntry>)>,
|
||||
is_new: bool,
|
||||
cx: &mut ModelContext<'_, NotificationStore>,
|
||||
) {
|
||||
let mut cursor = self.notifications.cursor::<(NotificationId, Count)>();
|
||||
let mut new_notifications = SumTree::new();
|
||||
let mut old_range = 0..0;
|
||||
|
||||
for (i, (id, new_notification)) in notifications.into_iter().enumerate() {
|
||||
new_notifications.append(cursor.slice(&NotificationId(id), Bias::Left, &()), &());
|
||||
|
||||
if i == 0 {
|
||||
old_range.start = cursor.start().1 .0;
|
||||
}
|
||||
|
||||
let old_notification = cursor.item();
|
||||
if let Some(old_notification) = old_notification {
|
||||
if old_notification.id == id {
|
||||
cursor.next(&());
|
||||
|
||||
if let Some(new_notification) = &new_notification {
|
||||
if new_notification.is_read {
|
||||
cx.emit(NotificationEvent::NotificationRead {
|
||||
entry: new_notification.clone(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
cx.emit(NotificationEvent::NotificationRemoved {
|
||||
entry: old_notification.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if let Some(new_notification) = &new_notification {
|
||||
if is_new {
|
||||
cx.emit(NotificationEvent::NewNotification {
|
||||
entry: new_notification.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(notification) = new_notification {
|
||||
new_notifications.push(notification, &());
|
||||
}
|
||||
}
|
||||
|
||||
old_range.end = cursor.start().1 .0;
|
||||
let new_count = new_notifications.summary().count - old_range.start;
|
||||
new_notifications.append(cursor.suffix(&()), &());
|
||||
drop(cursor);
|
||||
|
||||
self.notifications = new_notifications;
|
||||
cx.emit(NotificationEvent::NotificationsUpdated {
|
||||
old_range,
|
||||
new_count,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn respond_to_notification(
|
||||
&mut self,
|
||||
notification: Notification,
|
||||
response: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match notification {
|
||||
Notification::ContactRequest { sender_id } => {
|
||||
self.user_store
|
||||
.update(cx, |store, cx| {
|
||||
store.respond_to_contact_request(sender_id, response, cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
Notification::ChannelInvitation { channel_id, .. } => {
|
||||
self.channel_store
|
||||
.update(cx, |store, cx| {
|
||||
store.respond_to_channel_invite(channel_id, response, cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<NotificationEvent> for NotificationStore {}
|
||||
|
||||
impl sum_tree::Item for NotificationEntry {
|
||||
type Summary = NotificationSummary;
|
||||
|
||||
fn summary(&self) -> Self::Summary {
|
||||
NotificationSummary {
|
||||
max_id: self.id,
|
||||
count: 1,
|
||||
unread_count: if self.is_read { 0 } else { 1 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for NotificationSummary {
|
||||
type Context = ();
|
||||
|
||||
fn add_summary(&mut self, summary: &Self, _: &()) {
|
||||
self.max_id = self.max_id.max(summary.max_id);
|
||||
self.count += summary.count;
|
||||
self.unread_count += summary.unread_count;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, NotificationSummary> for NotificationId {
|
||||
fn add_summary(&mut self, summary: &NotificationSummary, _: &()) {
|
||||
debug_assert!(summary.max_id > self.0);
|
||||
self.0 = summary.max_id;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, NotificationSummary> for Count {
|
||||
fn add_summary(&mut self, summary: &NotificationSummary, _: &()) {
|
||||
self.0 += summary.count;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, NotificationSummary> for UnreadCount {
|
||||
fn add_summary(&mut self, summary: &NotificationSummary, _: &()) {
|
||||
self.0 += summary.unread_count;
|
||||
}
|
||||
}
|
||||
|
||||
struct AddNotificationsOptions {
|
||||
is_new: bool,
|
||||
clear_old: bool,
|
||||
includes_first: bool,
|
||||
}
|
||||
Reference in New Issue
Block a user