mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-25 01:34:02 +00:00
Navigate to chat messages when clicking them in the notification panel
This commit is contained in:
parent
d62f114c02
commit
5b90507310
6 changed files with 177 additions and 70 deletions
|
@ -8,7 +8,12 @@ use client::{
|
||||||
use futures::lock::Mutex;
|
use futures::lock::Mutex;
|
||||||
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
|
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use std::{collections::HashSet, mem, ops::Range, sync::Arc};
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
mem,
|
||||||
|
ops::{ControlFlow, Range},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
use sum_tree::{Bias, SumTree};
|
use sum_tree::{Bias, SumTree};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use util::{post_inc, ResultExt as _, TryFutureExt};
|
use util::{post_inc, ResultExt as _, TryFutureExt};
|
||||||
|
@ -201,18 +206,16 @@ impl ChannelChat {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) -> bool {
|
pub fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<Option<()>>> {
|
||||||
if !self.loaded_all_messages {
|
if self.loaded_all_messages {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let rpc = self.rpc.clone();
|
let rpc = self.rpc.clone();
|
||||||
let user_store = self.user_store.clone();
|
let user_store = self.user_store.clone();
|
||||||
let channel_id = self.channel.id;
|
let channel_id = self.channel.id;
|
||||||
if let Some(before_message_id) =
|
let before_message_id = self.first_loaded_message_id()?;
|
||||||
self.messages.first().and_then(|message| match message.id {
|
Some(cx.spawn(|this, mut cx| {
|
||||||
ChannelMessageId::Saved(id) => Some(id),
|
|
||||||
ChannelMessageId::Pending(_) => None,
|
|
||||||
})
|
|
||||||
{
|
|
||||||
cx.spawn(|this, mut cx| {
|
|
||||||
async move {
|
async move {
|
||||||
let response = rpc
|
let response = rpc
|
||||||
.request(proto::GetChannelMessages {
|
.request(proto::GetChannelMessages {
|
||||||
|
@ -221,8 +224,7 @@ impl ChannelChat {
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
let loaded_all_messages = response.done;
|
let loaded_all_messages = response.done;
|
||||||
let messages =
|
let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
|
||||||
messages_from_proto(response.messages, &user_store, &mut cx).await?;
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.loaded_all_messages = loaded_all_messages;
|
this.loaded_all_messages = loaded_all_messages;
|
||||||
this.insert_messages(messages, cx);
|
this.insert_messages(messages, cx);
|
||||||
|
@ -230,12 +232,42 @@ impl ChannelChat {
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
.log_err()
|
.log_err()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn first_loaded_message_id(&mut self) -> Option<u64> {
|
||||||
|
self.messages.first().and_then(|message| match message.id {
|
||||||
|
ChannelMessageId::Saved(id) => Some(id),
|
||||||
|
ChannelMessageId::Pending(_) => None,
|
||||||
})
|
})
|
||||||
.detach();
|
}
|
||||||
return true;
|
|
||||||
|
pub async fn load_history_since_message(
|
||||||
|
chat: ModelHandle<Self>,
|
||||||
|
message_id: u64,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Option<usize> {
|
||||||
|
loop {
|
||||||
|
let step = chat.update(&mut cx, |chat, cx| {
|
||||||
|
if let Some(first_id) = chat.first_loaded_message_id() {
|
||||||
|
if first_id <= message_id {
|
||||||
|
let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>();
|
||||||
|
let message_id = ChannelMessageId::Saved(message_id);
|
||||||
|
cursor.seek(&message_id, Bias::Left, &());
|
||||||
|
return ControlFlow::Break(if cursor.start().0 == message_id {
|
||||||
|
Some(cursor.start().1 .0)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ControlFlow::Continue(chat.load_more_messages(cx))
|
||||||
|
});
|
||||||
|
match step {
|
||||||
|
ControlFlow::Break(ix) => return ix,
|
||||||
|
ControlFlow::Continue(task) => task?.await?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn acknowledge_last_message(&mut self, cx: &mut ModelContext<Self>) {
|
pub fn acknowledge_last_message(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
|
|
|
@ -295,7 +295,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
// Scroll up to view older messages.
|
// Scroll up to view older messages.
|
||||||
channel.update(cx, |channel, cx| {
|
channel.update(cx, |channel, cx| {
|
||||||
assert!(channel.load_more_messages(cx));
|
channel.load_more_messages(cx).unwrap().detach();
|
||||||
});
|
});
|
||||||
let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
|
let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
|
||||||
assert_eq!(get_messages.payload.channel_id, 5);
|
assert_eq!(get_messages.payload.channel_id, 5);
|
||||||
|
|
|
@ -332,7 +332,7 @@ async fn test_channel_message_changes(
|
||||||
chat_panel_b
|
chat_panel_b
|
||||||
.update(cx_b, |chat_panel, cx| {
|
.update(cx_b, |chat_panel, cx| {
|
||||||
chat_panel.set_active(true, cx);
|
chat_panel.set_active(true, cx);
|
||||||
chat_panel.select_channel(channel_id, cx)
|
chat_panel.select_channel(channel_id, None, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -188,7 +188,7 @@ impl ChatPanel {
|
||||||
.channel_at(selected_ix)
|
.channel_at(selected_ix)
|
||||||
.map(|e| e.id);
|
.map(|e| e.id);
|
||||||
if let Some(selected_channel_id) = selected_channel_id {
|
if let Some(selected_channel_id) = selected_channel_id {
|
||||||
this.select_channel(selected_channel_id, cx)
|
this.select_channel(selected_channel_id, None, cx)
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -622,7 +622,9 @@ impl ChatPanel {
|
||||||
fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
|
fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
|
||||||
if let Some((chat, _)) = self.active_chat.as_ref() {
|
if let Some((chat, _)) = self.active_chat.as_ref() {
|
||||||
chat.update(cx, |channel, cx| {
|
chat.update(cx, |channel, cx| {
|
||||||
channel.load_more_messages(cx);
|
if let Some(task) = channel.load_more_messages(cx) {
|
||||||
|
task.detach();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -630,6 +632,7 @@ impl ChatPanel {
|
||||||
pub fn select_channel(
|
pub fn select_channel(
|
||||||
&mut self,
|
&mut self,
|
||||||
selected_channel_id: u64,
|
selected_channel_id: u64,
|
||||||
|
scroll_to_message_id: Option<u64>,
|
||||||
cx: &mut ViewContext<ChatPanel>,
|
cx: &mut ViewContext<ChatPanel>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
if let Some((chat, _)) = &self.active_chat {
|
if let Some((chat, _)) = &self.active_chat {
|
||||||
|
@ -645,8 +648,23 @@ impl ChatPanel {
|
||||||
let chat = open_chat.await?;
|
let chat = open_chat.await?;
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.markdown_data = Default::default();
|
this.markdown_data = Default::default();
|
||||||
this.set_active_chat(chat, cx);
|
this.set_active_chat(chat.clone(), cx);
|
||||||
})
|
})?;
|
||||||
|
|
||||||
|
if let Some(message_id) = scroll_to_message_id {
|
||||||
|
if let Some(item_ix) =
|
||||||
|
ChannelChat::load_history_since_message(chat, message_id, cx.clone()).await
|
||||||
|
{
|
||||||
|
this.update(&mut cx, |this, _| {
|
||||||
|
this.message_list.scroll_to(ListOffset {
|
||||||
|
item_ix,
|
||||||
|
offset_in_item: 0.,
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3366,7 +3366,9 @@ impl CollabPanel {
|
||||||
workspace.update(cx, |workspace, cx| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
|
if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.select_channel(channel_id, cx).detach_and_log_err(cx);
|
panel
|
||||||
|
.select_channel(channel_id, None, cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
format_timestamp, is_channels_feature_enabled, render_avatar, NotificationPanelSettings,
|
chat_panel::ChatPanel, format_timestamp, is_channels_feature_enabled, render_avatar,
|
||||||
|
NotificationPanelSettings,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use channel::ChannelStore;
|
use channel::ChannelStore;
|
||||||
|
@ -58,6 +59,14 @@ pub enum Event {
|
||||||
Dismissed,
|
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]);
|
actions!(notification_panel, [ToggleFocus]);
|
||||||
|
|
||||||
pub fn init(_cx: &mut AppContext) {}
|
pub fn init(_cx: &mut AppContext) {}
|
||||||
|
@ -178,7 +187,13 @@ impl NotificationPanel {
|
||||||
let entry = self.notification_store.read(cx).notification_at(ix)?;
|
let entry = self.notification_store.read(cx).notification_at(ix)?;
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
let timestamp = entry.timestamp;
|
let timestamp = entry.timestamp;
|
||||||
let (actor, text, icon, needs_response) = self.present_notification(entry, cx)?;
|
let NotificationPresenter {
|
||||||
|
actor,
|
||||||
|
text,
|
||||||
|
icon,
|
||||||
|
needs_response,
|
||||||
|
can_navigate,
|
||||||
|
} = self.present_notification(entry, cx)?;
|
||||||
|
|
||||||
let theme = theme::current(cx);
|
let theme = theme::current(cx);
|
||||||
let style = &theme.notification_panel;
|
let style = &theme.notification_panel;
|
||||||
|
@ -280,6 +295,15 @@ impl NotificationPanel {
|
||||||
.with_style(container)
|
.with_style(container)
|
||||||
.into_any()
|
.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(),
|
.into_any(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -288,27 +312,29 @@ impl NotificationPanel {
|
||||||
&self,
|
&self,
|
||||||
entry: &NotificationEntry,
|
entry: &NotificationEntry,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Option<(Option<Arc<client::User>>, String, &'static str, bool)> {
|
) -> Option<NotificationPresenter> {
|
||||||
let user_store = self.user_store.read(cx);
|
let user_store = self.user_store.read(cx);
|
||||||
let channel_store = self.channel_store.read(cx);
|
let channel_store = self.channel_store.read(cx);
|
||||||
let icon;
|
|
||||||
let text;
|
|
||||||
let actor;
|
|
||||||
let needs_response;
|
|
||||||
match entry.notification {
|
match entry.notification {
|
||||||
Notification::ContactRequest { sender_id } => {
|
Notification::ContactRequest { sender_id } => {
|
||||||
let requester = user_store.get_cached_user(sender_id)?;
|
let requester = user_store.get_cached_user(sender_id)?;
|
||||||
icon = "icons/plus.svg";
|
Some(NotificationPresenter {
|
||||||
text = format!("{} wants to add you as a contact", requester.github_login);
|
icon: "icons/plus.svg",
|
||||||
needs_response = user_store.is_contact_request_pending(&requester);
|
text: format!("{} wants to add you as a contact", requester.github_login),
|
||||||
actor = Some(requester);
|
needs_response: user_store.is_contact_request_pending(&requester),
|
||||||
|
actor: Some(requester),
|
||||||
|
can_navigate: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
Notification::ContactRequestAccepted { responder_id } => {
|
Notification::ContactRequestAccepted { responder_id } => {
|
||||||
let responder = user_store.get_cached_user(responder_id)?;
|
let responder = user_store.get_cached_user(responder_id)?;
|
||||||
icon = "icons/plus.svg";
|
Some(NotificationPresenter {
|
||||||
text = format!("{} accepted your contact invite", responder.github_login);
|
icon: "icons/plus.svg",
|
||||||
needs_response = false;
|
text: format!("{} accepted your contact invite", responder.github_login),
|
||||||
actor = Some(responder);
|
needs_response: false,
|
||||||
|
actor: Some(responder),
|
||||||
|
can_navigate: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
Notification::ChannelInvitation {
|
Notification::ChannelInvitation {
|
||||||
ref channel_name,
|
ref channel_name,
|
||||||
|
@ -316,13 +342,16 @@ impl NotificationPanel {
|
||||||
inviter_id,
|
inviter_id,
|
||||||
} => {
|
} => {
|
||||||
let inviter = user_store.get_cached_user(inviter_id)?;
|
let inviter = user_store.get_cached_user(inviter_id)?;
|
||||||
icon = "icons/hash.svg";
|
Some(NotificationPresenter {
|
||||||
text = format!(
|
icon: "icons/hash.svg",
|
||||||
|
text: format!(
|
||||||
"{} invited you to join the #{channel_name} channel",
|
"{} invited you to join the #{channel_name} channel",
|
||||||
inviter.github_login
|
inviter.github_login
|
||||||
);
|
),
|
||||||
needs_response = channel_store.has_channel_invitation(channel_id);
|
needs_response: channel_store.has_channel_invitation(channel_id),
|
||||||
actor = Some(inviter);
|
actor: Some(inviter),
|
||||||
|
can_navigate: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
Notification::ChannelMessageMention {
|
Notification::ChannelMessageMention {
|
||||||
sender_id,
|
sender_id,
|
||||||
|
@ -335,16 +364,41 @@ impl NotificationPanel {
|
||||||
.notification_store
|
.notification_store
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.channel_message_for_id(message_id)?;
|
.channel_message_for_id(message_id)?;
|
||||||
icon = "icons/conversations.svg";
|
Some(NotificationPresenter {
|
||||||
text = format!(
|
icon: "icons/conversations.svg",
|
||||||
|
text: format!(
|
||||||
"{} mentioned you in the #{} channel:\n{}",
|
"{} mentioned you in the #{} channel:\n{}",
|
||||||
sender.github_login, channel.name, message.body,
|
sender.github_login, channel.name, message.body,
|
||||||
);
|
),
|
||||||
needs_response = false;
|
needs_response: false,
|
||||||
actor = Some(sender);
|
actor: Some(sender),
|
||||||
|
can_navigate: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some((actor, text, icon, needs_response))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_sign_in_prompt(
|
fn render_sign_in_prompt(
|
||||||
|
@ -410,7 +464,8 @@ impl NotificationPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
|
fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
|
||||||
let Some((actor, text, _, _)) = self.present_notification(entry, cx) else {
|
let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
|
||||||
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue