Navigate to chat messages when clicking them in the notification panel

This commit is contained in:
Max Brunsfeld 2023-10-19 12:31:45 -07:00
parent d62f114c02
commit 5b90507310
6 changed files with 177 additions and 70 deletions

View file

@ -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,41 +206,68 @@ 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 {
let rpc = self.rpc.clone(); return None;
let user_store = self.user_store.clone(); }
let channel_id = self.channel.id;
if let Some(before_message_id) = let rpc = self.rpc.clone();
self.messages.first().and_then(|message| match message.id { let user_store = self.user_store.clone();
ChannelMessageId::Saved(id) => Some(id), let channel_id = self.channel.id;
ChannelMessageId::Pending(_) => None, let before_message_id = self.first_loaded_message_id()?;
}) Some(cx.spawn(|this, mut cx| {
{ async move {
cx.spawn(|this, mut cx| { let response = rpc
async move { .request(proto::GetChannelMessages {
let response = rpc channel_id,
.request(proto::GetChannelMessages { before_message_id,
channel_id, })
before_message_id, .await?;
}) let loaded_all_messages = response.done;
.await?; let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
let loaded_all_messages = response.done; this.update(&mut cx, |this, cx| {
let messages = this.loaded_all_messages = loaded_all_messages;
messages_from_proto(response.messages, &user_store, &mut cx).await?; this.insert_messages(messages, cx);
this.update(&mut cx, |this, cx| { });
this.loaded_all_messages = loaded_all_messages; anyhow::Ok(())
this.insert_messages(messages, cx); }
.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,
})
}
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
}); });
anyhow::Ok(())
} }
.log_err() }
}) ControlFlow::Continue(chat.load_more_messages(cx))
.detach(); });
return true; 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>) {

View file

@ -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);

View file

@ -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();

View file

@ -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(())
}) })
} }

View file

@ -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);
}); });
} }
}); });

View file

@ -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(&notification, 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",
"{} invited you to join the #{channel_name} channel", text: format!(
inviter.github_login "{} invited you to join the #{channel_name} channel",
); inviter.github_login
needs_response = channel_store.has_channel_invitation(channel_id); ),
actor = Some(inviter); needs_response: channel_store.has_channel_invitation(channel_id),
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",
"{} mentioned you in the #{} channel:\n{}", text: format!(
sender.github_login, channel.name, message.body, "{} mentioned you in the #{} channel:\n{}",
); sender.github_login, channel.name, message.body,
needs_response = false; ),
actor = Some(sender); needs_response: false,
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;
}; };