Position and style the channel editor correctly

Mikayla created

Fix a bug where some channel updates would be lost
Add channel name sanitization before storing in the database

Change summary

crates/client/src/channel_store.rs    | 34 ++++++++++++++++++++------
crates/collab/src/db.rs               |  1 
crates/collab_ui/src/collab_panel.rs  | 36 ++++++++++++++++++++++++----
crates/theme/src/theme.rs             |  2 +
crates/util/src/util.rs               | 15 ++++++++++++
styles/src/style_tree/collab_panel.ts |  7 ++++
6 files changed, 81 insertions(+), 14 deletions(-)

Detailed changes

crates/client/src/channel_store.rs 🔗

@@ -4,8 +4,10 @@ use anyhow::Result;
 use collections::HashMap;
 use collections::HashSet;
 use futures::Future;
+use futures::StreamExt;
 use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
 use rpc::{proto, TypedEnvelope};
+use std::mem;
 use std::sync::Arc;
 
 pub type ChannelId = u64;
@@ -19,6 +21,7 @@ pub struct ChannelStore {
     client: Arc<Client>,
     user_store: ModelHandle<UserStore>,
     _rpc_subscription: Subscription,
+    _maintain_user: Task<()>,
 }
 
 #[derive(Clone, Debug, PartialEq)]
@@ -55,6 +58,20 @@ impl ChannelStore {
         let rpc_subscription =
             client.add_message_handler(cx.handle(), Self::handle_update_channels);
 
+        let mut current_user = user_store.read(cx).watch_current_user();
+        let maintain_user = cx.spawn(|this, mut cx| async move {
+            while let Some(current_user) = current_user.next().await {
+                if current_user.is_none() {
+                    this.update(&mut cx, |this, cx| {
+                        this.channels.clear();
+                        this.channel_invitations.clear();
+                        this.channel_participants.clear();
+                        this.outgoing_invites.clear();
+                        cx.notify();
+                    });
+                }
+            }
+        });
         Self {
             channels: vec![],
             channel_invitations: vec![],
@@ -63,6 +80,7 @@ impl ChannelStore {
             client,
             user_store,
             _rpc_subscription: rpc_subscription,
+            _maintain_user: maintain_user,
         }
     }
 
@@ -301,10 +319,10 @@ impl ChannelStore {
                 .iter_mut()
                 .find(|c| c.id == channel.id)
             {
-                let existing_channel = Arc::get_mut(existing_channel)
-                    .expect("channel is shared, update would have been lost");
-                existing_channel.name = channel.name;
-                existing_channel.user_is_admin = channel.user_is_admin;
+                util::make_arc_mut(existing_channel, |new_existing_channel| {
+                    new_existing_channel.name = channel.name;
+                    new_existing_channel.user_is_admin = channel.user_is_admin;
+                });
                 continue;
             }
 
@@ -322,10 +340,10 @@ impl ChannelStore {
 
         for channel in payload.channels {
             if let Some(existing_channel) = self.channels.iter_mut().find(|c| c.id == channel.id) {
-                let existing_channel = Arc::get_mut(existing_channel)
-                    .expect("channel is shared, update would have been lost");
-                existing_channel.name = channel.name;
-                existing_channel.user_is_admin = channel.user_is_admin;
+                util::make_arc_mut(existing_channel, |new_existing_channel| {
+                    new_existing_channel.name = channel.name;
+                    new_existing_channel.user_is_admin = channel.user_is_admin;
+                });
                 continue;
             }
 

crates/collab/src/db.rs 🔗

@@ -3155,6 +3155,7 @@ impl Database {
         live_kit_room: &str,
         creator_id: UserId,
     ) -> Result<ChannelId> {
+        let name = name.trim().trim_start_matches('#');
         self.transaction(move |tx| async move {
             if let Some(parent) = parent {
                 self.check_user_is_channel_admin(parent, creator_id, &*tx)

crates/collab_ui/src/collab_panel.rs 🔗

@@ -308,7 +308,7 @@ impl CollabPanel {
                             cx,
                         ),
                         ListEntry::ChannelEditor { depth } => {
-                            this.render_channel_editor(&theme.collab_panel, *depth, cx)
+                            this.render_channel_editor(&theme, *depth, cx)
                         }
                     }
                 });
@@ -1280,11 +1280,37 @@ impl CollabPanel {
 
     fn render_channel_editor(
         &self,
-        _theme: &theme::CollabPanel,
-        _depth: usize,
+        theme: &theme::Theme,
+        depth: usize,
         cx: &AppContext,
     ) -> AnyElement<Self> {
-        ChildView::new(&self.channel_name_editor, cx).into_any()
+        Flex::row()
+            .with_child(
+                Svg::new("icons/channel_hash.svg")
+                    .with_color(theme.collab_panel.channel_hash.color)
+                    .constrained()
+                    .with_width(theme.collab_panel.channel_hash.width)
+                    .aligned()
+                    .left(),
+            )
+            .with_child(
+                ChildView::new(&self.channel_name_editor, cx)
+                    .contained()
+                    .with_style(theme.collab_panel.channel_editor)
+                    .flex(1.0, true),
+            )
+            .align_children_center()
+            .contained()
+            .with_padding_left(
+                theme.collab_panel.contact_row.default_style().padding.left
+                    + theme.collab_panel.channel_indent * depth as f32,
+            )
+            .contained()
+            .with_style(gpui::elements::ContainerStyle {
+                background_color: Some(theme.editor.background),
+                ..Default::default()
+            })
+            .into_any()
     }
 
     fn render_channel(
@@ -1331,7 +1357,7 @@ impl CollabPanel {
                 .constrained()
                 .with_height(theme.row_height)
                 .contained()
-                .with_style(*theme.contact_row.in_state(is_selected).style_for(state))
+                .with_style(*theme.contact_row.style_for(is_selected, state))
                 .with_padding_left(
                     theme.contact_row.default_style().padding.left
                         + theme.channel_indent * channel.depth as f32,

crates/theme/src/theme.rs 🔗

@@ -221,6 +221,7 @@ pub struct CollabPanel {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub log_in_button: Interactive<ContainedText>,
+    pub channel_editor: ContainerStyle,
     pub channel_hash: Icon,
     pub channel_modal: ChannelModal,
     pub user_query_editor: FieldEditor,
@@ -885,6 +886,7 @@ impl<T> Toggleable<T> {
     pub fn active_state(&self) -> &T {
         self.in_state(true)
     }
+
     pub fn inactive_state(&self) -> &T {
         self.in_state(false)
     }

crates/util/src/util.rs 🔗

@@ -9,9 +9,11 @@ pub mod test;
 use std::{
     borrow::Cow,
     cmp::{self, Ordering},
+    mem,
     ops::{AddAssign, Range, RangeInclusive},
     panic::Location,
     pin::Pin,
+    sync::Arc,
     task::{Context, Poll},
 };
 
@@ -118,6 +120,19 @@ pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut se
     }
 }
 
+/// Mutates through the arc if no other references exist,
+/// otherwise clones the value and swaps out the reference with a new Arc
+/// Useful for mutating the elements of a list while using iter_mut()
+pub fn make_arc_mut<T: Clone>(arc: &mut Arc<T>, mutate: impl FnOnce(&mut T)) {
+    if let Some(t) = Arc::get_mut(arc) {
+        mutate(t);
+        return;
+    }
+    let mut new_t = (**arc).clone();
+    mutate(&mut new_t);
+    mem::swap(&mut Arc::new(new_t), arc);
+}
+
 pub trait ResultExt<E> {
     type Ok;
 

styles/src/style_tree/collab_panel.ts 🔗

@@ -316,6 +316,11 @@ export default function contacts_panel(): any {
                 },
             },
         }),
-        face_overlap: 8
+        face_overlap: 8,
+        channel_editor: {
+            padding: {
+                left: 8,
+            }
+        }
     }
 }