Adjust channel rendering to group related messages

Mikayla created

Change summary

crates/collab_ui/src/chat_panel.rs  | 178 +++++++++++++++++++-----------
crates/theme/src/theme.rs           |   2 
styles/src/style_tree/chat_panel.ts |  14 ++
3 files changed, 126 insertions(+), 68 deletions(-)

Detailed changes

crates/collab_ui/src/chat_panel.rs 🔗

@@ -2,7 +2,7 @@ use crate::{channel_view::ChannelView, ChatPanelSettings};
 use anyhow::Result;
 use call::ActiveCall;
 use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
-use client::{Client, UserStore};
+use client::Client;
 use db::kvp::KEY_VALUE_STORE;
 use editor::Editor;
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
@@ -35,7 +35,6 @@ const CHAT_PANEL_KEY: &'static str = "ChatPanel";
 pub struct ChatPanel {
     client: Arc<Client>,
     channel_store: ModelHandle<ChannelStore>,
-    user_store: ModelHandle<UserStore>,
     active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
     message_list: ListState<ChatPanel>,
     input_editor: ViewHandle<Editor>,
@@ -79,7 +78,6 @@ impl ChatPanel {
         let fs = workspace.app_state().fs.clone();
         let client = workspace.app_state().client.clone();
         let channel_store = workspace.app_state().channel_store.clone();
-        let user_store = workspace.app_state().user_store.clone();
 
         let input_editor = cx.add_view(|cx| {
             let mut editor = Editor::auto_height(
@@ -132,7 +130,7 @@ impl ChatPanel {
                 fs,
                 client,
                 channel_store,
-                user_store,
+
                 active_chat: Default::default(),
                 pending_serialization: Task::ready(None),
                 message_list,
@@ -331,12 +329,26 @@ impl ChatPanel {
     }
 
     fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let message = self.active_chat.as_ref().unwrap().0.read(cx).message(ix);
+        let (message, is_continuation, is_last) = {
+            let active_chat = self.active_chat.as_ref().unwrap().0.read(cx);
+            let last_message = active_chat.message(ix.saturating_sub(1));
+            let this_message = active_chat.message(ix);
+            let is_continuation = last_message.id != this_message.id
+                && this_message.sender.id == last_message.sender.id;
+
+            (
+                active_chat.message(ix),
+                is_continuation,
+                active_chat.message_count() == ix + 1,
+            )
+        };
 
         let now = OffsetDateTime::now_utc();
         let theme = theme::current(cx);
         let style = if message.is_pending() {
             &theme.chat_panel.pending_message
+        } else if is_continuation {
+            &theme.chat_panel.continuation_message
         } else {
             &theme.chat_panel.message
         };
@@ -352,71 +364,103 @@ impl ChatPanel {
         enum DeleteMessage {}
 
         let body = message.body.clone();
-        Flex::column()
-            .with_child(
-                Flex::row()
-                    .with_child(
-                        message
-                            .sender
-                            .avatar
-                            .clone()
-                            .map(|avatar| {
-                                Image::from_data(avatar)
-                                    .with_style(theme.collab_panel.channel_avatar)
-                                    .into_any()
-                            })
-                            .unwrap_or_else(|| {
-                                Empty::new()
-                                    .constrained()
-                                    .with_width(
-                                        theme.collab_panel.channel_avatar.width.unwrap_or(12.),
-                                    )
-                                    .into_any()
-                            })
-                            .contained()
-                            .with_margin_right(4.),
-                    )
-                    .with_child(
-                        Label::new(
-                            message.sender.github_login.clone(),
-                            style.sender.text.clone(),
+        if is_continuation {
+            Flex::row()
+                .with_child(Text::new(body, style.body.clone()))
+                .with_children(message_id_to_remove.map(|id| {
+                    MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
+                        let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
+                        render_icon_button(button_style, "icons/x.svg")
+                            .aligned()
+                            .into_any()
+                    })
+                    .with_padding(Padding::uniform(2.))
+                    .with_cursor_style(CursorStyle::PointingHand)
+                    .on_click(MouseButton::Left, move |_, this, cx| {
+                        this.remove_message(id, cx);
+                    })
+                    .flex_float()
+                }))
+                .contained()
+                .with_style(style.container)
+                .with_margin_bottom(if is_last {
+                    theme.chat_panel.last_message_bottom_spacing
+                } else {
+                    0.
+                })
+                .into_any()
+        } else {
+            Flex::column()
+                .with_child(
+                    Flex::row()
+                        .with_child(
+                            message
+                                .sender
+                                .avatar
+                                .clone()
+                                .map(|avatar| {
+                                    Image::from_data(avatar)
+                                        .with_style(theme.collab_panel.channel_avatar)
+                                        .into_any()
+                                })
+                                .unwrap_or_else(|| {
+                                    Empty::new()
+                                        .constrained()
+                                        .with_width(
+                                            theme.collab_panel.channel_avatar.width.unwrap_or(12.),
+                                        )
+                                        .into_any()
+                                })
+                                .contained()
+                                .with_margin_right(4.),
                         )
-                        .contained()
-                        .with_style(style.sender.container),
-                    )
-                    .with_child(
-                        Label::new(
-                            format_timestamp(message.timestamp, now, self.local_timezone),
-                            style.timestamp.text.clone(),
+                        .with_child(
+                            Label::new(
+                                message.sender.github_login.clone(),
+                                style.sender.text.clone(),
+                            )
+                            .contained()
+                            .with_style(style.sender.container),
                         )
-                        .contained()
-                        .with_style(style.timestamp.container),
-                    )
-                    .with_children(message_id_to_remove.map(|id| {
-                        MouseEventHandler::new::<DeleteMessage, _>(
-                            id as usize,
-                            cx,
-                            |mouse_state, _| {
-                                let button_style =
-                                    theme.chat_panel.icon_button.style_for(mouse_state);
-                                render_icon_button(button_style, "icons/x.svg")
-                                    .aligned()
-                                    .into_any()
-                            },
+                        .with_child(
+                            Label::new(
+                                format_timestamp(message.timestamp, now, self.local_timezone),
+                                style.timestamp.text.clone(),
+                            )
+                            .contained()
+                            .with_style(style.timestamp.container),
                         )
-                        .with_padding(Padding::uniform(2.))
-                        .with_cursor_style(CursorStyle::PointingHand)
-                        .on_click(MouseButton::Left, move |_, this, cx| {
-                            this.remove_message(id, cx);
-                        })
-                        .flex_float()
-                    }))
-                    .align_children_center(),
-            )
-            .with_child(Text::new(body, style.body.clone()))
-            .contained()
-            .with_style(style.container)
-            .into_any()
+                        .with_children(message_id_to_remove.map(|id| {
+                            MouseEventHandler::new::<DeleteMessage, _>(
+                                id as usize,
+                                cx,
+                                |mouse_state, _| {
+                                    let button_style =
+                                        theme.chat_panel.icon_button.style_for(mouse_state);
+                                    render_icon_button(button_style, "icons/x.svg")
+                                        .aligned()
+                                        .into_any()
+                                },
+                            )
+                            .with_padding(Padding::uniform(2.))
+                            .with_cursor_style(CursorStyle::PointingHand)
+                            .on_click(MouseButton::Left, move |_, this, cx| {
+                                this.remove_message(id, cx);
+                            })
+                            .flex_float()
+                        }))
+                        .align_children_center(),
+                )
+                .with_child(Text::new(body, style.body.clone()))
+                .contained()
+                .with_style(style.container)
+                .with_margin_bottom(if is_last {
+                    theme.chat_panel.last_message_bottom_spacing
+                } else {
+                    0.
+                })
+                .into_any()
+        }
     }
 
     fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {

crates/theme/src/theme.rs 🔗

@@ -635,6 +635,8 @@ pub struct ChatPanel {
     pub channel_select: ChannelSelect,
     pub input_editor: FieldEditor,
     pub message: ChatMessage,
+    pub continuation_message: ChatMessage,
+    pub last_message_bottom_spacing: f32,
     pub pending_message: ChatMessage,
     pub sign_in_prompt: Interactive<TextStyle>,
     pub icon_button: Interactive<IconButton>,

styles/src/style_tree/chat_panel.ts 🔗

@@ -87,7 +87,19 @@ export default function chat_panel(): any {
                 ...text(layer, "sans", "base", { weight: "bold" }),
             },
             timestamp: text(layer, "sans", "base", "disabled"),
-            margin: { bottom: SPACING }
+            margin: { top: SPACING }
+        },
+        last_message_bottom_spacing: SPACING,
+        continuation_message: {
+            body: text(layer, "sans", "base"),
+            sender: {
+                margin: {
+                    right: 8,
+                },
+                ...text(layer, "sans", "base", { weight: "bold" }),
+            },
+            timestamp: text(layer, "sans", "base", "disabled"),
+
         },
         pending_message: {
             body: text(layer, "sans", "base"),