Add channel note indicator and clear changed status

Mikayla created

Change summary

crates/channel/src/channel_store.rs               | 20 +++-
crates/channel/src/channel_store/channel_index.rs | 12 ++
crates/collab/src/tests/channel_buffer_tests.rs   | 63 ++++++++++++++++
crates/collab_ui/src/collab_panel.rs              | 33 ++++++++
crates/theme/src/theme.rs                         |  1 
styles/src/style_tree/collab_panel.ts             |  1 
6 files changed, 119 insertions(+), 11 deletions(-)

Detailed changes

crates/channel/src/channel_store.rs 🔗

@@ -43,7 +43,7 @@ pub type ChannelData = (Channel, ChannelPath);
 pub struct Channel {
     pub id: ChannelId,
     pub name: String,
-    pub has_changed: bool,
+    pub has_note_changed: bool,
 }
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
@@ -200,19 +200,27 @@ impl ChannelStore {
     ) -> Task<Result<ModelHandle<ChannelBuffer>>> {
         let client = self.client.clone();
         let user_store = self.user_store.clone();
-        self.open_channel_resource(
+        let open_channel_buffer = self.open_channel_resource(
             channel_id,
             |this| &mut this.opened_buffers,
             |channel, cx| ChannelBuffer::new(channel, client, user_store, cx),
             cx,
-        )
+        );
+        cx.spawn(|this, mut cx| async move {
+            let buffer = open_channel_buffer.await?;
+            this.update(&mut cx, |this, cx| {
+                this.channel_index.clear_note_changed(channel_id);
+                cx.notify();
+            });
+            Ok(buffer)
+        })
     }
 
     pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> Option<bool> {
         self.channel_index
             .by_id()
             .get(&channel_id)
-            .map(|channel| channel.has_changed)
+            .map(|channel| channel.has_note_changed)
     }
 
     pub fn open_channel_chat(
@@ -787,7 +795,7 @@ impl ChannelStore {
                     Arc::new(Channel {
                         id: channel.id,
                         name: channel.name,
-                        has_changed: false,
+                        has_note_changed: false,
                     }),
                 ),
             }
@@ -825,7 +833,7 @@ impl ChannelStore {
             }
 
             for id_changed in payload.notes_changed {
-                index.has_changed(id_changed);
+                index.note_changed(id_changed);
             }
 
             for edge in payload.insert_edge {

crates/channel/src/channel_store/channel_index.rs 🔗

@@ -38,6 +38,12 @@ impl ChannelIndex {
             channels_by_id: &mut self.channels_by_id,
         }
     }
+
+    pub fn clear_note_changed(&mut self, channel_id: ChannelId) {
+        if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
+            Arc::make_mut(channel).has_note_changed = false;
+        }
+    }
 }
 
 impl Deref for ChannelIndex {
@@ -76,9 +82,9 @@ impl<'a> ChannelPathsInsertGuard<'a> {
         }
     }
 
-    pub fn has_changed(&mut self, channel_id: ChannelId) {
+    pub fn note_changed(&mut self, channel_id: ChannelId) {
         if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
-            Arc::make_mut(channel).has_changed = true;
+            Arc::make_mut(channel).has_note_changed = true;
         }
     }
 
@@ -91,7 +97,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
                 Arc::new(Channel {
                     id: channel_proto.id,
                     name: channel_proto.name,
-                    has_changed: false,
+                    has_note_changed: false,
                 }),
             );
             self.insert_root(channel_proto.id);

crates/collab/src/tests/channel_buffer_tests.rs 🔗

@@ -445,7 +445,7 @@ fn channel(id: u64, name: &'static str) -> Channel {
     Channel {
         id,
         name: name.to_string(),
-        has_changed: false,
+        has_note_changed: false,
     }
 }
 
@@ -786,6 +786,7 @@ async fn test_channel_buffer_changes(
         .await
         .unwrap();
 
+    // Client A makes an edit, and client B should see that the note has changed.
     channel_buffer_a.update(cx_a, |buffer, cx| {
         buffer.buffer().update(cx, |buffer, cx| {
             buffer.edit([(0..0, "1")], None, cx);
@@ -802,6 +803,66 @@ async fn test_channel_buffer_changes(
     });
 
     assert!(has_buffer_changed);
+
+    // Opening the buffer should clear the changed flag.
+    let channel_buffer_b = client_b
+        .channel_store()
+        .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+
+    let has_buffer_changed = cx_b.read(|cx| {
+        client_b
+            .channel_store()
+            .read(cx)
+            .has_channel_buffer_changed(channel_id)
+            .unwrap()
+    });
+
+    assert!(!has_buffer_changed);
+
+    // Editing the channel while the buffer is open shuold not show that the buffer has changed.
+    channel_buffer_a.update(cx_a, |buffer, cx| {
+        buffer.buffer().update(cx, |buffer, cx| {
+            buffer.edit([(0..0, "2")], None, cx);
+        })
+    });
+    deterministic.run_until_parked();
+
+    let has_buffer_changed = cx_b.read(|cx| {
+        client_b
+            .channel_store()
+            .read(cx)
+            .has_channel_buffer_changed(channel_id)
+            .unwrap()
+    });
+
+    assert!(!has_buffer_changed);
+
+    // Closing the buffer should re-enable change tracking
+    cx_b.update(|_| {
+        drop(channel_buffer_b);
+    });
+
+    deterministic.run_until_parked();
+
+    channel_buffer_a.update(cx_a, |buffer, cx| {
+        buffer.buffer().update(cx, |buffer, cx| {
+            buffer.edit([(0..0, "3")], None, cx);
+        })
+    });
+    deterministic.run_until_parked();
+
+    let has_buffer_changed = cx_b.read(|cx| {
+        client_b
+            .channel_store()
+            .read(cx)
+            .has_channel_buffer_changed(channel_id)
+            .unwrap()
+    });
+
+    assert!(has_buffer_changed);
 }
 
 #[track_caller]

crates/collab_ui/src/collab_panel.rs 🔗

@@ -1774,6 +1774,7 @@ impl CollabPanel {
         const FACEPILE_LIMIT: usize = 3;
 
         enum ChannelCall {}
+        enum ChannelNote {}
 
         let mut is_dragged_over = false;
         if cx
@@ -1820,7 +1821,7 @@ impl CollabPanel {
                         channel.name.clone(),
                         theme
                             .channel_name
-                            .in_state(channel.has_changed)
+                            .in_state(channel.has_note_changed)
                             .text
                             .clone(),
                     )
@@ -1863,6 +1864,8 @@ impl CollabPanel {
                                 .with_color(theme.channel_hash.color)
                                 .constrained()
                                 .with_width(theme.channel_hash.width)
+                                .contained()
+                                .with_margin_right(theme.channel_hash.container.margin.left)
                                 .into_any()
                         } else {
                             Empty::new().into_any()
@@ -1872,6 +1875,34 @@ impl CollabPanel {
                         this.join_channel_call(channel_id, cx);
                     }),
                 )
+                .with_child(
+                    MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |_, cx| {
+                        let participants =
+                            self.channel_store.read(cx).channel_participants(channel_id);
+                        if participants.is_empty() {
+                            if channel.has_note_changed {
+                                Svg::new("icons/terminal.svg")
+                                    .with_color(theme.channel_note_active_color)
+                                    .constrained()
+                                    .with_width(theme.channel_hash.width)
+                                    .into_any()
+                            } else if row_hovered {
+                                Svg::new("icons/terminal.svg")
+                                    .with_color(theme.channel_hash.color)
+                                    .constrained()
+                                    .with_width(theme.channel_hash.width)
+                                    .into_any()
+                            } else {
+                                Empty::new().into_any()
+                            }
+                        } else {
+                            Empty::new().into_any()
+                        }
+                    })
+                    .on_click(MouseButton::Left, move |_, this, cx| {
+                        this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
+                    }),
+                )
                 .align_children_center()
                 .styleable_component()
                 .disclosable(

crates/theme/src/theme.rs 🔗

@@ -238,6 +238,7 @@ pub struct CollabPanel {
     pub log_in_button: Interactive<ContainedText>,
     pub channel_editor: ContainerStyle,
     pub channel_hash: Icon,
+    pub channel_note_active_color: Color,
     pub tabbed_modal: TabbedModal,
     pub contact_finder: ContactFinder,
     pub channel_modal: ChannelModal,

styles/src/style_tree/collab_panel.ts 🔗

@@ -194,6 +194,7 @@ export default function contacts_panel(): any {
         },
         user_query_editor: filter_input,
         channel_hash: icon_style,
+        channel_note_active_color: foreground(layer, "active"),
         user_query_editor_height: 33,
         add_contact_button: header_icon_button,
         add_channel_button: header_icon_button,