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:
parent
0422d43798
commit
aad7761038
2 changed files with 180 additions and 123 deletions
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue