107 channel touch ups (#3087)

Mikayla Maki created

Release Notes:

- Add user avatars to channel chat messages
- Group messages by sender
- Fix visual bugs in new chat and note buttons

Change summary

crates/collab_ui/src/chat_panel.rs      | 151 +++++++++++++++++++-------
crates/collab_ui/src/collab_panel.rs    |  35 +++++-
crates/theme/src/theme.rs               |   2 
styles/src/style_tree/chat_panel.ts     |  14 ++
styles/src/style_tree/component_test.ts |   1 
5 files changed, 154 insertions(+), 49 deletions(-)

Detailed changes

crates/collab_ui/src/chat_panel.rs 🔗

@@ -130,6 +130,7 @@ impl ChatPanel {
                 fs,
                 client,
                 channel_store,
+
                 active_chat: Default::default(),
                 pending_serialization: Task::ready(None),
                 message_list,
@@ -328,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
         };
@@ -349,49 +364,103 @@ impl ChatPanel {
         enum DeleteMessage {}
 
         let body = message.body.clone();
-        Flex::column()
-            .with_child(
-                Flex::row()
-                    .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()
-                    })),
-            )
-            .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/collab_ui/src/collab_panel.rs 🔗

@@ -1937,6 +1937,8 @@ impl CollabPanel {
             is_dragged_over = true;
         }
 
+        let has_messages_notification = channel.unseen_message_id.is_some();
+
         MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
             let row_hovered = state.hovered();
 
@@ -2022,24 +2024,33 @@ impl CollabPanel {
                         .flex(1., true)
                 })
                 .with_child(
-                    MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |_, _| {
+                    MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| {
+                        let container_style = collab_theme
+                            .disclosure
+                            .button
+                            .style_for(mouse_state)
+                            .container;
+
                         if channel.unseen_message_id.is_some() {
                             Svg::new("icons/conversations.svg")
                                 .with_color(collab_theme.channel_note_active_color)
                                 .constrained()
                                 .with_width(collab_theme.channel_hash.width)
+                                .contained()
+                                .with_style(container_style)
+                                .with_uniform_padding(4.)
                                 .into_any()
                         } else if row_hovered {
                             Svg::new("icons/conversations.svg")
                                 .with_color(collab_theme.channel_hash.color)
                                 .constrained()
                                 .with_width(collab_theme.channel_hash.width)
+                                .contained()
+                                .with_style(container_style)
+                                .with_uniform_padding(4.)
                                 .into_any()
                         } else {
-                            Empty::new()
-                                .constrained()
-                                .with_width(collab_theme.channel_hash.width)
-                                .into_any()
+                            Empty::new().into_any()
                         }
                     })
                     .on_click(MouseButton::Left, move |_, this, cx| {
@@ -2056,7 +2067,12 @@ impl CollabPanel {
                     .with_margin_right(4.),
                 )
                 .with_child(
-                    MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |_, cx| {
+                    MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| {
+                        let container_style = collab_theme
+                            .disclosure
+                            .button
+                            .style_for(mouse_state)
+                            .container;
                         if row_hovered || channel.unseen_note_version.is_some() {
                             Svg::new("icons/file.svg")
                                 .with_color(if channel.unseen_note_version.is_some() {
@@ -2067,6 +2083,8 @@ impl CollabPanel {
                                 .constrained()
                                 .with_width(collab_theme.channel_hash.width)
                                 .contained()
+                                .with_style(container_style)
+                                .with_uniform_padding(4.)
                                 .with_margin_right(collab_theme.channel_hash.container.margin.left)
                                 .with_tooltip::<NotesTooltip>(
                                     ix as usize,
@@ -2076,13 +2094,16 @@ impl CollabPanel {
                                     cx,
                                 )
                                 .into_any()
-                        } else {
+                        } else if has_messages_notification {
                             Empty::new()
                                 .constrained()
                                 .with_width(collab_theme.channel_hash.width)
                                 .contained()
+                                .with_uniform_padding(4.)
                                 .with_margin_right(collab_theme.channel_hash.container.margin.left)
                                 .into_any()
+                        } else {
+                            Empty::new().into_any()
                         }
                     })
                     .on_click(MouseButton::Left, move |_, this, cx| {

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"),

styles/src/style_tree/component_test.ts 🔗

@@ -21,6 +21,7 @@ export default function contacts_panel(): any {
             ...text(theme.lowest, "sans", "base"),
             button: icon_button({ variant: "ghost" }),
             spacing: 4,
+            padding: 4,
         },
     }
 }