Detailed changes
@@ -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<Self>) -> 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<Self>) -> Option<Task<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<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()
- })
- .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<Self>) {
@@ -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::<proto::GetChannelMessages>().await.unwrap();
assert_eq!(get_messages.payload.channel_id, 5);
@@ -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();
@@ -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<Self>) {
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<u64>,
cx: &mut ViewContext<ChatPanel>,
) -> Task<Result<()>> {
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(())
})
}
@@ -3366,7 +3366,9 @@ impl CollabPanel {
workspace.update(cx, |workspace, cx| {
if let Some(panel) = workspace.focus_panel::<ChatPanel>(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::{
- 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<Arc<client::User>>,
+ pub text: String,
+ pub icon: &'static str,
+ pub needs_response: bool,
+ pub can_navigate: bool,
+}
+
actions!(notification_panel, [ToggleFocus]);
pub fn init(_cx: &mut AppContext) {}
@@ -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<Arc<client::User>>, String, &'static str, bool)> {
+ ) -> Option<NotificationPresenter> {
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<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(
@@ -410,7 +464,8 @@ impl NotificationPanel {
}
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;
};