use crate::{collab_panel, ChatPanelSettings};use anyhow::Result;use call::{room, ActiveCall};use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, ChannelStore};use client::{ChannelId, Client};use collections::HashMap;use db::kvp::KEY_VALUE_STORE;use editor::{actions, Editor};use gpui::{ actions, div, list, prelude::*, px, Action, AppContext, AsyncWindowContext, ClipboardItem, CursorStyle, DismissEvent, ElementId, EventEmitter, FocusHandle, FocusableView, FontWeight, HighlightStyle, ListOffset, ListScrollEvent, ListState, Model, Render, Stateful, Subscription, Task, View, ViewContext, VisualContext, WeakView,};use language::LanguageRegistry;use menu::Confirm;use message_editor::MessageEditor;use project::Fs;use rich_text::{Highlight, RichText};use serde::{Deserialize, Serialize};use settings::Settings;use std::{sync::Arc, time::Duration};use time::{OffsetDateTime, UtcOffset};use ui::{ prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label, PopoverMenu, Tab, TabBar, Tooltip,};use util::{ResultExt, TryFutureExt};use workspace::{ dock::{DockPosition, Panel, PanelEvent}, Workspace,}; mod message_editor; const MESSAGE_LOADING_THRESHOLD: usize = 50;const CHAT_PANEL_KEY: &str = "ChatPanel"; pub fn init(cx: &mut AppContext) { cx.observe_new_views(|workspace: &mut Workspace, _| { workspace.register_action(|workspace, _: &ToggleFocus, cx| { workspace.toggle_panel_focus::<ChatPanel>(cx); }); }) .detach();} pub struct ChatPanel { client: Arc<Client>, channel_store: Model<ChannelStore>, languages: Arc<LanguageRegistry>, message_list: ListState, active_chat: Option<(Model<ChannelChat>, Subscription)>, message_editor: View<MessageEditor>, local_timezone: UtcOffset, fs: Arc<dyn Fs>, width: Option<Pixels>, active: bool, pending_serialization: Task<Option<()>>, subscriptions: Vec<gpui::Subscription>, is_scrolled_to_bottom: bool, markdown_data: HashMap<ChannelMessageId, RichText>, focus_handle: FocusHandle, open_context_menu: Option<(u64, Subscription)>, highlighted_message: Option<(u64, Task<()>)>, last_acknowledged_message_id: Option<u64>,} #[derive(Serialize, Deserialize)]struct SerializedChatPanel { width: Option<Pixels>,} actions!(chat_panel, [ToggleFocus]); impl ChatPanel { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> { let fs = workspace.app_state().fs.clone(); let client = workspace.app_state().client.clone(); let channel_store = ChannelStore::global(cx); let user_store = workspace.app_state().user_store.clone(); let languages = workspace.app_state().languages.clone(); let input_editor = cx.new_view(|cx| { MessageEditor::new( languages.clone(), user_store.clone(), None, cx.new_view(|cx| Editor::auto_height(4, cx)), cx, ) }); cx.new_view(|cx: &mut ViewContext<Self>| { let view = cx.view().downgrade(); let message_list = ListState::new(0, gpui::ListAlignment::Bottom, px(1000.), move |ix, cx| { if let Some(view) = view.upgrade() { view.update(cx, |view, cx| { view.render_message(ix, cx).into_any_element() }) } else { div().into_any() } }); message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, cx| { if event.visible_range.start < MESSAGE_LOADING_THRESHOLD { this.load_more_messages(cx); } this.is_scrolled_to_bottom = !event.is_scrolled; })); let local_offset = chrono::Local::now().offset().local_minus_utc(); let mut this = Self { fs, client, channel_store, languages, message_list, active_chat: Default::default(), pending_serialization: Task::ready(None), message_editor: input_editor, local_timezone: UtcOffset::from_whole_seconds(local_offset).unwrap(), subscriptions: Vec::new(), is_scrolled_to_bottom: true, active: false, width: None, markdown_data: Default::default(), 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) .read(cx) .room() .and_then(|room| room.read(cx).channel_id()) { this.select_channel(channel_id, None, cx) .detach_and_log_err(cx); } this.subscriptions.push(cx.subscribe( &ActiveCall::global(cx), move |this: &mut Self, call, event: &room::Event, cx| match event { room::Event::RoomJoined { channel_id } => { if let Some(channel_id) = channel_id { this.select_channel(*channel_id, None, cx) .detach_and_log_err(cx); if call .read(cx) .room() .is_some_and(|room| room.read(cx).contains_guests()) { cx.emit(PanelEvent::Activate) } } } room::Event::RoomLeft { channel_id } => { if channel_id == &this.channel_id(cx) { cx.emit(PanelEvent::Close) } } _ => {} }, )); this }) } pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> { self.active_chat .as_ref() .map(|(chat, _)| chat.read(cx).channel_id) } pub fn is_scrolled_to_bottom(&self) -> bool { self.is_scrolled_to_bottom } pub fn active_chat(&self) -> Option<Model<ChannelChat>> { self.active_chat.as_ref().map(|(chat, _)| chat.clone()) } pub fn load( workspace: WeakView<Workspace>, cx: AsyncWindowContext, ) -> Task<Result<View<Self>>> { cx.spawn(|mut cx| async move { let serialized_panel = if let Some(panel) = cx .background_executor() .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) }) .await .log_err() .flatten() { Some(serde_json::from_str::<SerializedChatPanel>(&panel)?) } else { None }; workspace.update(&mut cx, |workspace, cx| { let panel = Self::new(workspace, cx); if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width.map(|r| r.round()); cx.notify(); }); } panel }) }) } fn serialize(&mut self, cx: &mut ViewContext<Self>) { let width = self.width; self.pending_serialization = cx.background_executor().spawn( async move { KEY_VALUE_STORE .write_kvp( CHAT_PANEL_KEY.into(), serde_json::to_string(&SerializedChatPanel { width })?, ) .await?; anyhow::Ok(()) } .log_err(), ); } fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) { if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) { self.markdown_data.clear(); self.message_list.reset(chat.read(cx).message_count()); self.message_editor.update(cx, |editor, cx| { editor.set_channel_chat(chat.clone(), cx); editor.clear_reply_to_message_id(); }); let subscription = cx.subscribe(&chat, Self::channel_did_change); self.active_chat = Some((chat, subscription)); self.acknowledge_last_message(cx); cx.notify(); } } fn channel_did_change( &mut self, _: Model<ChannelChat>, event: &ChannelChatEvent, cx: &mut ViewContext<Self>, ) { match event { ChannelChatEvent::MessagesUpdated { old_range, new_count, } => { self.message_list.splice(old_range.clone(), *new_count); if self.active { self.acknowledge_last_message(cx); } } ChannelChatEvent::UpdateMessage { message_id, message_ix, } => { self.message_list.splice(*message_ix..*message_ix + 1, 1); self.markdown_data.remove(message_id); } ChannelChatEvent::NewMessage { channel_id, message_id, } => { if !self.active { self.channel_store.update(cx, |store, cx| { store.update_latest_message_id(*channel_id, *message_id, cx) }) } } } cx.notify(); } fn acknowledge_last_message(&mut self, cx: &mut ViewContext<Self>) { 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); }); } } } fn render_replied_to_message( &mut self, message_id: Option<ChannelMessageId>, reply_to_message: &Option<ChannelMessage>, cx: &mut ViewContext<Self>, ) -> impl IntoElement { let reply_to_message = match reply_to_message { None => { return div().child( h_flex() .text_ui_xs(cx) .my_0p5() .px_0p5() .gap_x_1() .rounded_md() .child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted)) .when(reply_to_message.is_none(), |el| { el.child( Label::new("Message has been deleted...") .size(LabelSize::XSmall) .color(Color::Muted), ) }), ) } Some(val) => val, }; let user_being_replied_to = reply_to_message.sender.clone(); let message_being_replied_to = reply_to_message.clone(); let message_element_id: ElementId = match message_id { Some(ChannelMessageId::Saved(id)) => ("reply-to-saved-message-container", id).into(), Some(ChannelMessageId::Pending(id)) => { ("reply-to-pending-message-container", id).into() } // This should never happen None => ("composing-reply-container").into(), }; let current_channel_id = self.channel_id(cx); let reply_to_message_id = reply_to_message.id; div().child( h_flex() .id(message_element_id) .text_ui_xs(cx) .my_0p5() .px_0p5() .gap_x_1() .rounded_md() .overflow_hidden() .hover(|style| style.bg(cx.theme().colors().element_background)) .child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted)) .child(Avatar::new(user_being_replied_to.avatar_uri.clone()).size(rems(0.7))) .child( Label::new(format!("@{}", user_being_replied_to.github_login)) .size(LabelSize::XSmall) .weight(FontWeight::SEMIBOLD) .color(Color::Muted), ) .child( div().overflow_y_hidden().child( Label::new(message_being_replied_to.body.replace('\n', " ")) .size(LabelSize::XSmall) .color(Color::Default), ), ) .cursor(CursorStyle::PointingHand) .tooltip(|cx| Tooltip::text("Go to message", cx)) .on_click(cx.listener(move |chat_panel, _, cx| { if let Some(channel_id) = current_channel_id { chat_panel .select_channel(channel_id, reply_to_message_id.into(), cx) .detach_and_log_err(cx) } })), ) } fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement { let active_chat = &self.active_chat.as_ref().unwrap().0; let (message, is_continuation_from_previous, is_admin) = active_chat.update(cx, |active_chat, cx| { let is_admin = self .channel_store .read(cx) .is_channel_admin(active_chat.channel_id); let last_message = active_chat.message(ix.saturating_sub(1)); let this_message = active_chat.message(ix).clone(); let duration_since_last_message = this_message.timestamp - last_message.timestamp; let is_continuation_from_previous = last_message.sender.id == this_message.sender.id && last_message.id != this_message.id && duration_since_last_message < Duration::from_secs(5 * 60); if let ChannelMessageId::Saved(id) = this_message.id { if this_message .mentions .iter() .any(|(_, user_id)| Some(*user_id) == self.client.user_id()) { active_chat.acknowledge_message(id); } } (this_message, is_continuation_from_previous, is_admin) }); let _is_pending = message.is_pending(); let belongs_to_user = Some(message.sender.id) == self.client.user_id(); let can_delete_message = belongs_to_user || is_admin; let can_edit_message = belongs_to_user; let element_id: ElementId = match message.id { ChannelMessageId::Saved(id) => ("saved-message", id).into(), ChannelMessageId::Pending(id) => ("pending-message", id).into(), }; let mentioning_you = message .mentions .iter() .any(|m| Some(m.1) == self.client.user_id()); let message_id = match message.id { ChannelMessageId::Saved(id) => Some(id), ChannelMessageId::Pending(_) => None, }; let reply_to_message = message .reply_to_message_id .and_then(|id| active_chat.read(cx).find_loaded_message(id)) .cloned(); let replied_to_you = reply_to_message.as_ref().map(|m| m.sender.id) == self.client.user_id(); let is_highlighted_message = self .highlighted_message .as_ref() .is_some_and(|(id, _)| Some(id) == message_id.as_ref()); let background = if is_highlighted_message { cx.theme().status().info_background } else if mentioning_you || replied_to_you { cx.theme().colors().background } else { cx.theme().colors().panel_background }; let reply_to_message_id = self.message_editor.read(cx).reply_to_message_id(); v_flex() .w_full() .relative() .group("") .when(!is_continuation_from_previous, |this| this.pt_2()) .child( div() .group("") .bg(background) .rounded_md() .overflow_hidden() .px_1p5() .py_0p5() .when_some(reply_to_message_id, |el, reply_id| { el.when_some(message_id, |el, message_id| { el.when(reply_id == message_id, |el| { el.bg(cx.theme().colors().element_selected) }) }) }) .when(!self.has_open_menu(message_id), |this| { this.hover(|style| style.bg(cx.theme().colors().element_hover)) }) .when(message.reply_to_message_id.is_some(), |el| { el.child(self.render_replied_to_message( Some(message.id), &reply_to_message, cx, )) .when(is_continuation_from_previous, |this| this.mt_2()) }) .when( !is_continuation_from_previous || message.reply_to_message_id.is_some(), |this| { this.child( h_flex() .gap_2() .text_ui_sm(cx) .child( Avatar::new(message.sender.avatar_uri.clone()) .size(rems(1.)), ) .child( Label::new(message.sender.github_login.clone())Lines 1–500 of 1337. Load more
use crate::{collab_panel, ChatPanelSettings}; use anyhow::Result; use call::{room, ActiveCall}; use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, ChannelStore}; use client::{ChannelId, Client}; use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use editor::{actions, Editor}; use gpui::{ actions, div, list, prelude::*, px, Action, AppContext, AsyncWindowContext, ClipboardItem, CursorStyle, DismissEvent, ElementId, EventEmitter, FocusHandle, FocusableView, FontWeight, HighlightStyle, ListOffset, ListScrollEvent, ListState, Model, Render, Stateful, Subscription, Task, View, ViewContext, VisualContext, WeakView, }; use language::LanguageRegistry; use menu::Confirm; use message_editor::MessageEditor; use project::Fs; use rich_text::{Highlight, RichText}; use serde::{Deserialize, Serialize}; use settings::Settings; use std::{sync::Arc, time::Duration}; use time::{OffsetDateTime, UtcOffset}; use ui::{ prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label, PopoverMenu, Tab, TabBar, Tooltip, }; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, Workspace, }; mod message_editor; const MESSAGE_LOADING_THRESHOLD: usize = 50; const CHAT_PANEL_KEY: &str = "ChatPanel"; pub fn init(cx: &mut AppContext) { cx.observe_new_views(|workspace: &mut Workspace, _| { workspace.register_action(|workspace, _: &ToggleFocus, cx| { workspace.toggle_panel_focus::<ChatPanel>(cx); }); }) .detach(); } pub struct ChatPanel { client: Arc<Client>, channel_store: Model<ChannelStore>, languages: Arc<LanguageRegistry>, message_list: ListState, active_chat: Option<(Model<ChannelChat>, Subscription)>, message_editor: View<MessageEditor>, local_timezone: UtcOffset, fs: Arc<dyn Fs>, width: Option<Pixels>, active: bool, pending_serialization: Task<Option<()>>, subscriptions: Vec<gpui::Subscription>, is_scrolled_to_bottom: bool, markdown_data: HashMap<ChannelMessageId, RichText>, focus_handle: FocusHandle, open_context_menu: Option<(u64, Subscription)>, highlighted_message: Option<(u64, Task<()>)>, last_acknowledged_message_id: Option<u64>, } #[derive(Serialize, Deserialize)] struct SerializedChatPanel { width: Option<Pixels>, } actions!(chat_panel, [ToggleFocus]); impl ChatPanel { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> { let fs = workspace.app_state().fs.clone(); let client = workspace.app_state().client.clone(); let channel_store = ChannelStore::global(cx); let user_store = workspace.app_state().user_store.clone(); let languages = workspace.app_state().languages.clone(); let input_editor = cx.new_view(|cx| { MessageEditor::new( languages.clone(), user_store.clone(), None, cx.new_view(|cx| Editor::auto_height(4, cx)), cx, ) }); cx.new_view(|cx: &mut ViewContext<Self>| { let view = cx.view().downgrade(); let message_list = ListState::new(0, gpui::ListAlignment::Bottom, px(1000.), move |ix, cx| { if let Some(view) = view.upgrade() { view.update(cx, |view, cx| { view.render_message(ix, cx).into_any_element() }) } else { div().into_any() } }); message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, cx| { if event.visible_range.start < MESSAGE_LOADING_THRESHOLD { this.load_more_messages(cx); } this.is_scrolled_to_bottom = !event.is_scrolled; })); let local_offset = chrono::Local::now().offset().local_minus_utc(); let mut this = Self { fs, client, channel_store, languages, message_list, active_chat: Default::default(), pending_serialization: Task::ready(None), message_editor: input_editor, local_timezone: UtcOffset::from_whole_seconds(local_offset).unwrap(), subscriptions: Vec::new(), is_scrolled_to_bottom: true, active: false, width: None, markdown_data: Default::default(), 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) .read(cx) .room() .and_then(|room| room.read(cx).channel_id()) { this.select_channel(channel_id, None, cx) .detach_and_log_err(cx); } this.subscriptions.push(cx.subscribe( &ActiveCall::global(cx), move |this: &mut Self, call, event: &room::Event, cx| match event { room::Event::RoomJoined { channel_id } => { if let Some(channel_id) = channel_id { this.select_channel(*channel_id, None, cx) .detach_and_log_err(cx); if call .read(cx) .room() .is_some_and(|room| room.read(cx).contains_guests()) { cx.emit(PanelEvent::Activate) } } } room::Event::RoomLeft { channel_id } => { if channel_id == &this.channel_id(cx) { cx.emit(PanelEvent::Close) } } _ => {} }, )); this }) } pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> { self.active_chat .as_ref() .map(|(chat, _)| chat.read(cx).channel_id) } pub fn is_scrolled_to_bottom(&self) -> bool { self.is_scrolled_to_bottom } pub fn active_chat(&self) -> Option<Model<ChannelChat>> { self.active_chat.as_ref().map(|(chat, _)| chat.clone()) } pub fn load( workspace: WeakView<Workspace>, cx: AsyncWindowContext, ) -> Task<Result<View<Self>>> { cx.spawn(|mut cx| async move { let serialized_panel = if let Some(panel) = cx .background_executor() .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) }) .await .log_err() .flatten() { Some(serde_json::from_str::<SerializedChatPanel>(&panel)?) } else { None }; workspace.update(&mut cx, |workspace, cx| { let panel = Self::new(workspace, cx); if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width.map(|r| r.round()); cx.notify(); }); } panel }) }) } fn serialize(&mut self, cx: &mut ViewContext<Self>) { let width = self.width; self.pending_serialization = cx.background_executor().spawn( async move { KEY_VALUE_STORE .write_kvp( CHAT_PANEL_KEY.into(), serde_json::to_string(&SerializedChatPanel { width })?, ) .await?; anyhow::Ok(()) } .log_err(), ); } fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) { if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) { self.markdown_data.clear(); self.message_list.reset(chat.read(cx).message_count()); self.message_editor.update(cx, |editor, cx| { editor.set_channel_chat(chat.clone(), cx); editor.clear_reply_to_message_id(); }); let subscription = cx.subscribe(&chat, Self::channel_did_change); self.active_chat = Some((chat, subscription)); self.acknowledge_last_message(cx); cx.notify(); } } fn channel_did_change( &mut self, _: Model<ChannelChat>, event: &ChannelChatEvent, cx: &mut ViewContext<Self>, ) { match event { ChannelChatEvent::MessagesUpdated { old_range, new_count, } => { self.message_list.splice(old_range.clone(), *new_count); if self.active { self.acknowledge_last_message(cx); } } ChannelChatEvent::UpdateMessage { message_id, message_ix, } => { self.message_list.splice(*message_ix..*message_ix + 1, 1); self.markdown_data.remove(message_id); } ChannelChatEvent::NewMessage { channel_id, message_id, } => { if !self.active { self.channel_store.update(cx, |store, cx| { store.update_latest_message_id(*channel_id, *message_id, cx) }) } } } cx.notify(); } fn acknowledge_last_message(&mut self, cx: &mut ViewContext<Self>) { 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); }); } } } fn render_replied_to_message( &mut self, message_id: Option<ChannelMessageId>, reply_to_message: &Option<ChannelMessage>, cx: &mut ViewContext<Self>, ) -> impl IntoElement { let reply_to_message = match reply_to_message { None => { return div().child( h_flex() .text_ui_xs(cx) .my_0p5() .px_0p5() .gap_x_1() .rounded_md() .child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted)) .when(reply_to_message.is_none(), |el| { el.child( Label::new("Message has been deleted...") .size(LabelSize::XSmall) .color(Color::Muted), ) }), ) } Some(val) => val, }; let user_being_replied_to = reply_to_message.sender.clone(); let message_being_replied_to = reply_to_message.clone(); let message_element_id: ElementId = match message_id { Some(ChannelMessageId::Saved(id)) => ("reply-to-saved-message-container", id).into(), Some(ChannelMessageId::Pending(id)) => { ("reply-to-pending-message-container", id).into() } // This should never happen None => ("composing-reply-container").into(), }; let current_channel_id = self.channel_id(cx); let reply_to_message_id = reply_to_message.id; div().child( h_flex() .id(message_element_id) .text_ui_xs(cx) .my_0p5() .px_0p5() .gap_x_1() .rounded_md() .overflow_hidden() .hover(|style| style.bg(cx.theme().colors().element_background)) .child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted)) .child(Avatar::new(user_being_replied_to.avatar_uri.clone()).size(rems(0.7))) .child( Label::new(format!("@{}", user_being_replied_to.github_login)) .size(LabelSize::XSmall) .weight(FontWeight::SEMIBOLD) .color(Color::Muted), ) .child( div().overflow_y_hidden().child( Label::new(message_being_replied_to.body.replace('\n', " ")) .size(LabelSize::XSmall) .color(Color::Default), ), ) .cursor(CursorStyle::PointingHand) .tooltip(|cx| Tooltip::text("Go to message", cx)) .on_click(cx.listener(move |chat_panel, _, cx| { if let Some(channel_id) = current_channel_id { chat_panel .select_channel(channel_id, reply_to_message_id.into(), cx) .detach_and_log_err(cx) } })), ) } fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement { let active_chat = &self.active_chat.as_ref().unwrap().0; let (message, is_continuation_from_previous, is_admin) = active_chat.update(cx, |active_chat, cx| { let is_admin = self .channel_store .read(cx) .is_channel_admin(active_chat.channel_id); let last_message = active_chat.message(ix.saturating_sub(1)); let this_message = active_chat.message(ix).clone(); let duration_since_last_message = this_message.timestamp - last_message.timestamp; let is_continuation_from_previous = last_message.sender.id == this_message.sender.id && last_message.id != this_message.id && duration_since_last_message < Duration::from_secs(5 * 60); if let ChannelMessageId::Saved(id) = this_message.id { if this_message .mentions .iter() .any(|(_, user_id)| Some(*user_id) == self.client.user_id()) { active_chat.acknowledge_message(id); } } (this_message, is_continuation_from_previous, is_admin) }); let _is_pending = message.is_pending(); let belongs_to_user = Some(message.sender.id) == self.client.user_id(); let can_delete_message = belongs_to_user || is_admin; let can_edit_message = belongs_to_user; let element_id: ElementId = match message.id { ChannelMessageId::Saved(id) => ("saved-message", id).into(), ChannelMessageId::Pending(id) => ("pending-message", id).into(), }; let mentioning_you = message .mentions .iter() .any(|m| Some(m.1) == self.client.user_id()); let message_id = match message.id { ChannelMessageId::Saved(id) => Some(id), ChannelMessageId::Pending(_) => None, }; let reply_to_message = message .reply_to_message_id .and_then(|id| active_chat.read(cx).find_loaded_message(id)) .cloned(); let replied_to_you = reply_to_message.as_ref().map(|m| m.sender.id) == self.client.user_id(); let is_highlighted_message = self .highlighted_message .as_ref() .is_some_and(|(id, _)| Some(id) == message_id.as_ref()); let background = if is_highlighted_message { cx.theme().status().info_background } else if mentioning_you || replied_to_you { cx.theme().colors().background } else { cx.theme().colors().panel_background }; let reply_to_message_id = self.message_editor.read(cx).reply_to_message_id(); v_flex() .w_full() .relative() .group("") .when(!is_continuation_from_previous, |this| this.pt_2()) .child( div() .group("") .bg(background) .rounded_md() .overflow_hidden() .px_1p5() .py_0p5() .when_some(reply_to_message_id, |el, reply_id| { el.when_some(message_id, |el, message_id| { el.when(reply_id == message_id, |el| { el.bg(cx.theme().colors().element_selected) }) }) }) .when(!self.has_open_menu(message_id), |this| { this.hover(|style| style.bg(cx.theme().colors().element_hover)) }) .when(message.reply_to_message_id.is_some(), |el| { el.child(self.render_replied_to_message( Some(message.id), &reply_to_message, cx, )) .when(is_continuation_from_previous, |this| this.mt_2()) }) .when( !is_continuation_from_previous || message.reply_to_message_id.is_some(), |this| { this.child( h_flex() .gap_2() .text_ui_sm(cx) .child( Avatar::new(message.sender.avatar_uri.clone()) .size(rems(1.)), ) .child( Label::new(message.sender.github_login.clone())