Add an indicator to the channel chat to see all the messages that you missed (#7781)

This pull requests add the following features:
- Show indicator before first unseen message
- Scroll to last unseen message

<img width="241" alt="Screenshot 2024-02-14 at 18 10 35"
src="https://github.com/zed-industries/zed/assets/62463826/ca396daf-7102-4eac-ae50-7d0b5ba9b6d5">


https://github.com/zed-industries/zed/assets/62463826/3a5c4afb-aea7-4e7b-98f6-515c027ef83b

### Questions: 
1. Should we hide the indicator after a couple of seconds? Now the
indicator will hide when you close/reopen the channel chat, because when
the last unseen channel message ID is not smaller than the last message
we will not show it.

Release Notes:
- Added unseen messages indicator for the channel chat.
This commit is contained in:
Remco Smits 2024-02-20 03:20:02 +01:00 committed by GitHub
parent 0422d43798
commit aad7761038
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 180 additions and 123 deletions

View file

@ -348,6 +348,21 @@ impl ChannelStore {
.is_some_and(|state| state.has_new_messages()) .is_some_and(|state| state.has_new_messages())
} }
pub fn last_acknowledge_message_id(&self, channel_id: ChannelId) -> Option<u64> {
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( pub fn acknowledge_message_id(
&mut self, &mut self,
channel_id: ChannelId, channel_id: ChannelId,
@ -1152,6 +1167,10 @@ impl ChannelState {
}) })
} }
fn last_acknowledged_message_id(&self) -> Option<u64> {
self.observed_chat_message
}
fn acknowledge_message_id(&mut self, message_id: u64) { fn acknowledge_message_id(&mut self, message_id: u64) {
let observed = self.observed_chat_message.get_or_insert(message_id); let observed = self.observed_chat_message.get_or_insert(message_id);
*observed = (*observed).max(message_id); *observed = (*observed).max(message_id);

View file

@ -63,6 +63,7 @@ pub struct ChatPanel {
focus_handle: FocusHandle, focus_handle: FocusHandle,
open_context_menu: Option<(u64, Subscription)>, open_context_menu: Option<(u64, Subscription)>,
highlighted_message: Option<(u64, Task<()>)>, highlighted_message: Option<(u64, Task<()>)>,
last_acknowledged_message_id: Option<u64>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -126,6 +127,7 @@ impl ChatPanel {
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
open_context_menu: None, open_context_menu: None,
highlighted_message: None, highlighted_message: None,
last_acknowledged_message_id: None,
}; };
if let Some(channel_id) = ActiveCall::global(cx) if let Some(channel_id) = ActiveCall::global(cx)
@ -281,6 +283,13 @@ impl ChatPanel {
fn acknowledge_last_message(&mut self, cx: &mut ViewContext<Self>) { fn acknowledge_last_message(&mut self, cx: &mut ViewContext<Self>) {
if self.active && self.is_scrolled_to_bottom { if self.active && self.is_scrolled_to_bottom {
if let Some((chat, _)) = &self.active_chat { 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.update(cx, |chat, cx| {
chat.acknowledge_last_message(cx); chat.acknowledge_last_message(cx);
}); });
@ -454,120 +463,145 @@ impl ChatPanel {
cx.theme().colors().panel_background cx.theme().colors().panel_background
}; };
v_flex().w_full().relative().child( v_flex()
div() .w_full()
.bg(background) .relative()
.rounded_md() .child(
.overflow_hidden() div()
.px_1() .bg(background)
.py_0p5() .rounded_md()
.when(!is_continuation_from_previous, |this| { .overflow_hidden()
this.mt_2().child( .px_1()
h_flex() .py_0p5()
.text_ui_sm() .when(!is_continuation_from_previous, |this| {
.child(div().absolute().child( this.mt_2().child(
Avatar::new(message.sender.avatar_uri.clone()).size(rems(1.)), h_flex()
)) .text_ui_sm()
.child( .child(div().absolute().child(
div() Avatar::new(message.sender.avatar_uri.clone()).size(rems(1.)),
.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) .child(
.color(Color::Muted), 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_some(reply_to_message, |el, reply_to_message| {
.when( el.child(self.render_replied_to_message(
message.reply_to_message_id.is_some() && reply_to_message.is_none(), Some(message.id),
|this| { &reply_to_message,
const MESSAGE_DELETED: &str = "Message has been deleted"; cx,
))
let body_text = StyledText::new(MESSAGE_DELETED).with_highlights( })
&cx.text_style(), .when(mentioning_you || replied_to_you, |this| this.my_0p5())
vec![( .map(|el| {
0..MESSAGE_DELETED.len(), let text = self.markdown_data.entry(message.id).or_insert_with(|| {
HighlightStyle { Self::render_markdown_with_mentions(
font_style: Some(FontStyle::Italic), &self.languages,
..Default::default() self.client.id(),
}, &message,
)], )
); });
el.child(
this.child( v_flex()
div() .w_full()
.border_l_2() .text_ui_sm()
.text_ui_xs() .id(element_id)
.border_color(cx.theme().colors().border) .group("")
.px_1() .child(text.element("body".into(), cx))
.py_0p5() .child(
.child(body_text), 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| { .when(
el.child(self.render_replied_to_message( self.last_acknowledged_message_id
Some(message.id), .is_some_and(|l| Some(l) == message_id),
&reply_to_message, |this| {
cx, this.child(
)) h_flex()
}) .py_2()
.when(mentioning_you || replied_to_you, |this| this.my_0p5()) .gap_1()
.map(|el| { .items_center()
let text = self.markdown_data.entry(message.id).or_insert_with(|| { .child(div().w_full().h_0p5().bg(cx.theme().colors().border))
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( .child(
div() div()
.absolute() .px_1()
.z_index(1) .rounded_md()
.right_0() .text_ui_xs()
.w_6() .bg(cx.theme().colors().background)
.bg(background) .child("New messages"),
.when(!self.has_open_menu(message_id), |el| { )
el.visible_on_hover("") .child(div().w_full().h_0p5().bg(cx.theme().colors().border)),
})
.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,
))
}),
)
}),
),
) )
}), },
) )
} }
fn has_open_menu(&self, message_id: Option<u64>) -> bool { fn has_open_menu(&self, message_id: Option<u64>) -> bool {
@ -682,8 +716,11 @@ impl ChatPanel {
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let chat = open_chat.await?; 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); 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 { 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()) ChannelChat::load_history_since_message(chat.clone(), message_id, (*cx).clone())
.await .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.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) { if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
this.message_list.scroll_to(ListOffset { this.message_list.scroll_to(ListOffset {
item_ix, item_ix,