diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index c1e6d802dd..abb5b560f0 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -348,6 +348,21 @@ impl ChannelStore { .is_some_and(|state| state.has_new_messages()) } + pub fn last_acknowledge_message_id(&self, channel_id: ChannelId) -> Option { + self.channel_states.get(&channel_id).and_then(|state| { + if let Some(last_message_id) = state.latest_chat_message { + if state + .last_acknowledged_message_id() + .is_some_and(|id| id < last_message_id) + { + return state.last_acknowledged_message_id(); + } + } + + None + }) + } + pub fn acknowledge_message_id( &mut self, channel_id: ChannelId, @@ -1152,6 +1167,10 @@ impl ChannelState { }) } + fn last_acknowledged_message_id(&self) -> Option { + self.observed_chat_message + } + fn acknowledge_message_id(&mut self, message_id: u64) { let observed = self.observed_chat_message.get_or_insert(message_id); *observed = (*observed).max(message_id); diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 0181ed8587..afbfe6a476 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -63,6 +63,7 @@ pub struct ChatPanel { focus_handle: FocusHandle, open_context_menu: Option<(u64, Subscription)>, highlighted_message: Option<(u64, Task<()>)>, + last_acknowledged_message_id: Option, } #[derive(Serialize, Deserialize)] @@ -126,6 +127,7 @@ impl ChatPanel { focus_handle: cx.focus_handle(), open_context_menu: None, highlighted_message: None, + last_acknowledged_message_id: None, }; if let Some(channel_id) = ActiveCall::global(cx) @@ -281,6 +283,13 @@ impl ChatPanel { fn acknowledge_last_message(&mut self, cx: &mut ViewContext) { if self.active && self.is_scrolled_to_bottom { if let Some((chat, _)) = &self.active_chat { + if let Some(channel_id) = self.channel_id(cx) { + self.last_acknowledged_message_id = self + .channel_store + .read(cx) + .last_acknowledge_message_id(channel_id); + } + chat.update(cx, |chat, cx| { chat.acknowledge_last_message(cx); }); @@ -454,120 +463,145 @@ impl ChatPanel { cx.theme().colors().panel_background }; - v_flex().w_full().relative().child( - div() - .bg(background) - .rounded_md() - .overflow_hidden() - .px_1() - .py_0p5() - .when(!is_continuation_from_previous, |this| { - this.mt_2().child( - h_flex() - .text_ui_sm() - .child(div().absolute().child( - Avatar::new(message.sender.avatar_uri.clone()).size(rems(1.)), - )) - .child( - div() - .pl(cx.rem_size() + px(6.0)) - .pr(px(8.0)) - .font_weight(FontWeight::BOLD) - .child(Label::new(message.sender.github_login.clone())), - ) - .child( - Label::new(format_timestamp( - OffsetDateTime::now_utc(), - message.timestamp, - self.local_timezone, - None, + v_flex() + .w_full() + .relative() + .child( + div() + .bg(background) + .rounded_md() + .overflow_hidden() + .px_1() + .py_0p5() + .when(!is_continuation_from_previous, |this| { + this.mt_2().child( + h_flex() + .text_ui_sm() + .child(div().absolute().child( + Avatar::new(message.sender.avatar_uri.clone()).size(rems(1.)), )) - .size(LabelSize::Small) - .color(Color::Muted), - ), + .child( + div() + .pl(cx.rem_size() + px(6.0)) + .pr(px(8.0)) + .font_weight(FontWeight::BOLD) + .child(Label::new(message.sender.github_login.clone())), + ) + .child( + Label::new(format_timestamp( + OffsetDateTime::now_utc(), + message.timestamp, + self.local_timezone, + None, + )) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + }) + .when( + message.reply_to_message_id.is_some() && reply_to_message.is_none(), + |this| { + const MESSAGE_DELETED: &str = "Message has been deleted"; + + let body_text = StyledText::new(MESSAGE_DELETED).with_highlights( + &cx.text_style(), + vec![( + 0..MESSAGE_DELETED.len(), + HighlightStyle { + font_style: Some(FontStyle::Italic), + ..Default::default() + }, + )], + ); + + this.child( + div() + .border_l_2() + .text_ui_xs() + .border_color(cx.theme().colors().border) + .px_1() + .py_0p5() + .child(body_text), + ) + }, ) - }) - .when( - message.reply_to_message_id.is_some() && reply_to_message.is_none(), - |this| { - const MESSAGE_DELETED: &str = "Message has been deleted"; - - let body_text = StyledText::new(MESSAGE_DELETED).with_highlights( - &cx.text_style(), - vec![( - 0..MESSAGE_DELETED.len(), - HighlightStyle { - font_style: Some(FontStyle::Italic), - ..Default::default() - }, - )], - ); - - this.child( - div() - .border_l_2() - .text_ui_xs() - .border_color(cx.theme().colors().border) - .px_1() - .py_0p5() - .child(body_text), + .when_some(reply_to_message, |el, reply_to_message| { + el.child(self.render_replied_to_message( + Some(message.id), + &reply_to_message, + cx, + )) + }) + .when(mentioning_you || replied_to_you, |this| this.my_0p5()) + .map(|el| { + let text = self.markdown_data.entry(message.id).or_insert_with(|| { + Self::render_markdown_with_mentions( + &self.languages, + self.client.id(), + &message, + ) + }); + el.child( + v_flex() + .w_full() + .text_ui_sm() + .id(element_id) + .group("") + .child(text.element("body".into(), cx)) + .child( + div() + .absolute() + .z_index(1) + .right_0() + .w_6() + .bg(background) + .when(!self.has_open_menu(message_id), |el| { + el.visible_on_hover("") + }) + .when_some(message_id, |el, message_id| { + el.child( + popover_menu(("menu", message_id)) + .trigger(IconButton::new( + ("trigger", message_id), + IconName::Ellipsis, + )) + .menu(move |cx| { + Some(Self::render_message_menu( + &this, + message_id, + can_delete_message, + cx, + )) + }), + ) + }), + ), ) - }, - ) - .when_some(reply_to_message, |el, reply_to_message| { - el.child(self.render_replied_to_message( - Some(message.id), - &reply_to_message, - cx, - )) - }) - .when(mentioning_you || replied_to_you, |this| this.my_0p5()) - .map(|el| { - let text = self.markdown_data.entry(message.id).or_insert_with(|| { - Self::render_markdown_with_mentions( - &self.languages, - self.client.id(), - &message, - ) - }); - el.child( - v_flex() - .w_full() - .text_ui_sm() - .id(element_id) - .group("") - .child(text.element("body".into(), cx)) + }), + ) + .when( + self.last_acknowledged_message_id + .is_some_and(|l| Some(l) == message_id), + |this| { + this.child( + h_flex() + .py_2() + .gap_1() + .items_center() + .child(div().w_full().h_0p5().bg(cx.theme().colors().border)) .child( div() - .absolute() - .z_index(1) - .right_0() - .w_6() - .bg(background) - .when(!self.has_open_menu(message_id), |el| { - el.visible_on_hover("") - }) - .when_some(message_id, |el, message_id| { - el.child( - popover_menu(("menu", message_id)) - .trigger(IconButton::new( - ("trigger", message_id), - IconName::Ellipsis, - )) - .menu(move |cx| { - Some(Self::render_message_menu( - &this, - message_id, - can_delete_message, - cx, - )) - }), - ) - }), - ), + .px_1() + .rounded_md() + .text_ui_xs() + .bg(cx.theme().colors().background) + .child("New messages"), + ) + .child(div().w_full().h_0p5().bg(cx.theme().colors().border)), ) - }), - ) + }, + ) } fn has_open_menu(&self, message_id: Option) -> bool { @@ -682,8 +716,11 @@ impl ChatPanel { cx.spawn(|this, mut cx| async move { let chat = open_chat.await?; - this.update(&mut cx, |this, cx| { + let highlight_message_id = scroll_to_message_id; + let scroll_to_message_id = this.update(&mut cx, |this, cx| { this.set_active_chat(chat.clone(), cx); + + scroll_to_message_id.or_else(|| this.last_acknowledged_message_id) })?; if let Some(message_id) = scroll_to_message_id { @@ -691,21 +728,22 @@ impl ChatPanel { ChannelChat::load_history_since_message(chat.clone(), message_id, (*cx).clone()) .await { - let task = cx.spawn({ - let this = this.clone(); - - |mut cx| async move { - cx.background_executor().timer(Duration::from_secs(2)).await; - this.update(&mut cx, |this, cx| { - this.highlighted_message.take(); - cx.notify(); - }) - .ok(); - } - }); - this.update(&mut cx, |this, cx| { - this.highlighted_message = Some((message_id, task)); + if let Some(highlight_message_id) = highlight_message_id { + let task = cx.spawn({ + |this, mut cx| async move { + cx.background_executor().timer(Duration::from_secs(2)).await; + this.update(&mut cx, |this, cx| { + this.highlighted_message.take(); + cx.notify(); + }) + .ok(); + } + }); + + this.highlighted_message = Some((highlight_message_id, task)); + } + if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) { this.message_list.scroll_to(ListOffset { item_ix,