From 5b90507310b0817f6a155ec6557b1a165a94c611 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 19 Oct 2023 12:31:45 -0700 Subject: [PATCH] Navigate to chat messages when clicking them in the notification panel --- crates/channel/src/channel_chat.rs | 96 +++++++++----- crates/channel/src/channel_store_tests.rs | 2 +- .../collab/src/tests/channel_message_tests.rs | 2 +- crates/collab_ui/src/chat_panel.rs | 26 +++- crates/collab_ui/src/collab_panel.rs | 4 +- crates/collab_ui/src/notification_panel.rs | 117 +++++++++++++----- 6 files changed, 177 insertions(+), 70 deletions(-) diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index a07b7d395d..5f256f2f29 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -8,7 +8,12 @@ use client::{ use futures::lock::Mutex; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; 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 time::OffsetDateTime; use util::{post_inc, ResultExt as _, TryFutureExt}; @@ -201,41 +206,68 @@ impl ChannelChat { }) } - pub fn load_more_messages(&mut self, cx: &mut ModelContext) -> bool { - if !self.loaded_all_messages { - let rpc = self.rpc.clone(); - let user_store = self.user_store.clone(); - let channel_id = self.channel.id; - if let Some(before_message_id) = - self.messages.first().and_then(|message| match message.id { - ChannelMessageId::Saved(id) => Some(id), - ChannelMessageId::Pending(_) => None, - }) - { - cx.spawn(|this, mut cx| { - async move { - let response = rpc - .request(proto::GetChannelMessages { - channel_id, - before_message_id, - }) - .await?; - let loaded_all_messages = response.done; - let messages = - messages_from_proto(response.messages, &user_store, &mut cx).await?; - this.update(&mut cx, |this, cx| { - this.loaded_all_messages = loaded_all_messages; - this.insert_messages(messages, cx); + pub fn load_more_messages(&mut self, cx: &mut ModelContext) -> Option>> { + if self.loaded_all_messages { + return None; + } + + let rpc = self.rpc.clone(); + let user_store = self.user_store.clone(); + let channel_id = self.channel.id; + let before_message_id = self.first_loaded_message_id()?; + Some(cx.spawn(|this, mut cx| { + async move { + let response = rpc + .request(proto::GetChannelMessages { + channel_id, + before_message_id, + }) + .await?; + let loaded_all_messages = response.done; + let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?; + this.update(&mut cx, |this, cx| { + this.loaded_all_messages = loaded_all_messages; + this.insert_messages(messages, cx); + }); + anyhow::Ok(()) + } + .log_err() + })) + } + + pub fn first_loaded_message_id(&mut self) -> Option { + self.messages.first().and_then(|message| match message.id { + ChannelMessageId::Saved(id) => Some(id), + ChannelMessageId::Pending(_) => None, + }) + } + + pub async fn load_history_since_message( + chat: ModelHandle, + message_id: u64, + mut cx: AsyncAppContext, + ) -> Option { + 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 }); - anyhow::Ok(()) } - .log_err() - }) - .detach(); - return true; + } + 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) { diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 8ad8f21224..8cc9cb73da 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -295,7 +295,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) { // Scroll up to view older messages. channel.update(cx, |channel, cx| { - assert!(channel.load_more_messages(cx)); + channel.load_more_messages(cx).unwrap().detach(); }); let get_messages = server.receive::().await.unwrap(); assert_eq!(get_messages.payload.channel_id, 5); diff --git a/crates/collab/src/tests/channel_message_tests.rs b/crates/collab/src/tests/channel_message_tests.rs index 0e63f96bf9..918eb053d3 100644 --- a/crates/collab/src/tests/channel_message_tests.rs +++ b/crates/collab/src/tests/channel_message_tests.rs @@ -332,7 +332,7 @@ async fn test_channel_message_changes( chat_panel_b .update(cx_b, |chat_panel, cx| { chat_panel.set_active(true, cx); - chat_panel.select_channel(channel_id, cx) + chat_panel.select_channel(channel_id, None, cx) }) .await .unwrap(); diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index c36ea8d363..912ac936d6 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -188,7 +188,7 @@ impl ChatPanel { .channel_at(selected_ix) .map(|e| e.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); } }) @@ -622,7 +622,9 @@ impl ChatPanel { fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext) { if let Some((chat, _)) = self.active_chat.as_ref() { 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( &mut self, selected_channel_id: u64, + scroll_to_message_id: Option, cx: &mut ViewContext, ) -> Task> { if let Some((chat, _)) = &self.active_chat { @@ -645,8 +648,23 @@ impl ChatPanel { let chat = open_chat.await?; this.update(&mut cx, |this, cx| { 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(()) }) } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 49ab3b15b3..e907127ca4 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -3366,7 +3366,9 @@ impl CollabPanel { workspace.update(cx, |workspace, cx| { if let Some(panel) = workspace.focus_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); }); } }); diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 93ba05a671..9e8016a439 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -1,5 +1,6 @@ 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 channel::ChannelStore; @@ -58,6 +59,14 @@ pub enum Event { Dismissed, } +pub struct NotificationPresenter { + pub actor: Option>, + 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) {} @@ -178,7 +187,13 @@ impl NotificationPanel { let entry = self.notification_store.read(cx).notification_at(ix)?; let now = OffsetDateTime::now_utc(); 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 style = &theme.notification_panel; @@ -280,6 +295,15 @@ impl NotificationPanel { .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(), ) } @@ -288,27 +312,29 @@ impl NotificationPanel { &self, entry: &NotificationEntry, cx: &AppContext, - ) -> Option<(Option>, String, &'static str, bool)> { + ) -> Option { let user_store = self.user_store.read(cx); let channel_store = self.channel_store.read(cx); - let icon; - let text; - let actor; - let needs_response; match entry.notification { Notification::ContactRequest { sender_id } => { let requester = user_store.get_cached_user(sender_id)?; - icon = "icons/plus.svg"; - text = format!("{} wants to add you as a contact", requester.github_login); - needs_response = user_store.is_contact_request_pending(&requester); - actor = Some(requester); + Some(NotificationPresenter { + icon: "icons/plus.svg", + text: format!("{} wants to add you as a contact", requester.github_login), + needs_response: user_store.is_contact_request_pending(&requester), + actor: Some(requester), + can_navigate: false, + }) } Notification::ContactRequestAccepted { responder_id } => { let responder = user_store.get_cached_user(responder_id)?; - icon = "icons/plus.svg"; - text = format!("{} accepted your contact invite", responder.github_login); - needs_response = false; - actor = Some(responder); + 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, @@ -316,13 +342,16 @@ impl NotificationPanel { inviter_id, } => { let inviter = user_store.get_cached_user(inviter_id)?; - 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); + 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, @@ -335,16 +364,41 @@ impl NotificationPanel { .notification_store .read(cx) .channel_message_for_id(message_id)?; - icon = "icons/conversations.svg"; - text = format!( - "{} mentioned you in the #{} channel:\n{}", - sender.github_login, channel.name, message.body, - ); - needs_response = false; - actor = Some(sender); + Some(NotificationPresenter { + icon: "icons/conversations.svg", + text: format!( + "{} mentioned you in the #{} channel:\n{}", + sender.github_login, channel.name, message.body, + ), + needs_response: false, + actor: Some(sender), + can_navigate: true, + }) + } + } + } + + fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext) { + 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::(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( @@ -410,7 +464,8 @@ impl NotificationPanel { } fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext) { - let Some((actor, text, _, _)) = self.present_notification(entry, cx) else { + let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx) + else { return; };