Put channel call participants back in channel row

Max Brunsfeld created

Open both the channel notes and the channel chat when clicking a channel

Change summary

crates/channel/src/channel_store.rs  |  42 ++--
crates/collab_ui/src/channel_view.rs |   2 
crates/collab_ui/src/chat_panel.rs   |  35 ++-
crates/collab_ui/src/collab_panel.rs | 217 +++++++++--------------------
4 files changed, 109 insertions(+), 187 deletions(-)

Detailed changes

crates/channel/src/channel_store.rs 🔗

@@ -196,10 +196,15 @@ impl ChannelStore {
         )
     }
 
+    /// Asynchronously open a given resource associated with a channel.
+    ///
+    /// Make sure that the resource is only opened once, even if this method
+    /// is called multiple times with the same channel id while the first task
+    /// is still running.
     fn open_channel_resource<T: Entity, F, Fut>(
         &mut self,
         channel_id: ChannelId,
-        map: fn(&mut Self) -> &mut HashMap<ChannelId, OpenedModelHandle<T>>,
+        get_map: fn(&mut Self) -> &mut HashMap<ChannelId, OpenedModelHandle<T>>,
         load: F,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<ModelHandle<T>>>
@@ -207,21 +212,20 @@ impl ChannelStore {
         F: 'static + FnOnce(Arc<Channel>, AsyncAppContext) -> Fut,
         Fut: Future<Output = Result<ModelHandle<T>>>,
     {
-        // Make sure that a given channel resource is only opened once per
-        // app instance, even if this method is called multiple times
-        // with the same channel id while the first task is still running.
         let task = loop {
-            match map(self).entry(channel_id) {
+            match get_map(self).entry(channel_id) {
                 hash_map::Entry::Occupied(e) => match e.get() {
-                    OpenedModelHandle::Open(buffer) => {
-                        if let Some(buffer) = buffer.upgrade(cx) {
-                            break Task::ready(Ok(buffer)).shared();
+                    OpenedModelHandle::Open(model) => {
+                        if let Some(model) = model.upgrade(cx) {
+                            break Task::ready(Ok(model)).shared();
                         } else {
-                            map(self).remove(&channel_id);
+                            get_map(self).remove(&channel_id);
                             continue;
                         }
                     }
-                    OpenedModelHandle::Loading(task) => break task.clone(),
+                    OpenedModelHandle::Loading(task) => {
+                        break task.clone();
+                    }
                 },
                 hash_map::Entry::Vacant(e) => {
                     let task = cx
@@ -235,25 +239,21 @@ impl ChannelStore {
                             load(channel, cx).await.map_err(Arc::new)
                         })
                         .shared();
+
                     e.insert(OpenedModelHandle::Loading(task.clone()));
                     cx.spawn({
                         let task = task.clone();
                         |this, mut cx| async move {
                             let result = task.await;
-                            this.update(&mut cx, |this, cx| match result {
-                                Ok(buffer) => {
-                                    cx.observe_release(&buffer, move |this, _, _| {
-                                        this.opened_buffers.remove(&channel_id);
-                                    })
-                                    .detach();
-                                    map(this).insert(
+                            this.update(&mut cx, |this, _| match result {
+                                Ok(model) => {
+                                    get_map(this).insert(
                                         channel_id,
-                                        OpenedModelHandle::Open(buffer.downgrade()),
+                                        OpenedModelHandle::Open(model.downgrade()),
                                     );
                                 }
-                                Err(error) => {
-                                    log::error!("failed to open channel buffer {error:?}");
-                                    map(this).remove(&channel_id);
+                                Err(_) => {
+                                    get_map(this).remove(&channel_id);
                                 }
                             });
                         }

crates/collab_ui/src/channel_view.rs 🔗

@@ -53,6 +53,7 @@ impl ChannelView {
 
         cx.spawn(|mut cx| async move {
             let channel_buffer = channel_buffer.await?;
+
             let markdown = markdown.await?;
             channel_buffer.update(&mut cx, |buffer, cx| {
                 buffer.buffer().update(cx, |buffer, cx| {
@@ -75,7 +76,6 @@ impl ChannelView {
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let buffer = channel_buffer.read(cx).buffer();
-        // buffer.update(cx, |buffer, cx| buffer.set_language(language, cx));
         let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx));
         let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
 

crates/collab_ui/src/chat_panel.rs 🔗

@@ -33,7 +33,7 @@ const CHAT_PANEL_KEY: &'static str = "ChatPanel";
 pub struct ChatPanel {
     client: Arc<Client>,
     channel_store: ModelHandle<ChannelStore>,
-    active_channel: Option<(ModelHandle<ChannelChat>, Subscription)>,
+    active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
     message_list: ListState<ChatPanel>,
     input_editor: ViewHandle<Editor>,
     channel_select: ViewHandle<Select>,
@@ -105,7 +105,7 @@ impl ChatPanel {
 
         let mut message_list =
             ListState::<Self>::new(0, Orientation::Bottom, 1000., move |this, ix, cx| {
-                let message = this.active_channel.as_ref().unwrap().0.read(cx).message(ix);
+                let message = this.active_chat.as_ref().unwrap().0.read(cx).message(ix);
                 this.render_message(message, cx)
             });
         message_list.set_scroll_handler(|visible_range, this, cx| {
@@ -119,7 +119,7 @@ impl ChatPanel {
                 fs,
                 client,
                 channel_store,
-                active_channel: Default::default(),
+                active_chat: Default::default(),
                 pending_serialization: Task::ready(None),
                 message_list,
                 input_editor,
@@ -151,6 +151,7 @@ impl ChatPanel {
 
             cx.observe(&this.channel_select, |this, channel_select, cx| {
                 let selected_ix = channel_select.read(cx).selected_index();
+
                 let selected_channel_id = this
                     .channel_store
                     .read(cx)
@@ -216,14 +217,14 @@ impl ChatPanel {
     fn init_active_channel(&mut self, cx: &mut ViewContext<Self>) {
         let channel_count = self.channel_store.read(cx).channel_count();
         self.message_list.reset(0);
-        self.active_channel = None;
+        self.active_chat = None;
         self.channel_select.update(cx, |select, cx| {
             select.set_item_count(channel_count, cx);
         });
     }
 
-    fn set_active_channel(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
-        if self.active_channel.as_ref().map(|e| &e.0) != Some(&chat) {
+    fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
+        if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
             let id = chat.read(cx).channel().id;
             {
                 let chat = chat.read(cx);
@@ -234,7 +235,7 @@ impl ChatPanel {
                 });
             }
             let subscription = cx.subscribe(&chat, Self::channel_did_change);
-            self.active_channel = Some((chat, subscription));
+            self.active_chat = Some((chat, subscription));
             self.channel_select.update(cx, |select, cx| {
                 if let Some(ix) = self.channel_store.read(cx).index_of_channel(id) {
                     select.set_selected_index(ix, cx);
@@ -275,7 +276,7 @@ impl ChatPanel {
     }
 
     fn render_active_channel_messages(&self) -> AnyElement<Self> {
-        let messages = if self.active_channel.is_some() {
+        let messages = if self.active_chat.is_some() {
             List::new(self.message_list.clone()).into_any()
         } else {
             Empty::new().into_any()
@@ -396,15 +397,15 @@ impl ChatPanel {
     }
 
     fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        if let Some((channel, _)) = self.active_channel.as_ref() {
+        if let Some((chat, _)) = self.active_chat.as_ref() {
             let body = self.input_editor.update(cx, |editor, cx| {
                 let body = editor.text(cx);
                 editor.clear(cx);
                 body
             });
 
-            if let Some(task) = channel
-                .update(cx, |channel, cx| channel.send_message(body, cx))
+            if let Some(task) = chat
+                .update(cx, |chat, cx| chat.send_message(body, cx))
                 .log_err()
             {
                 task.detach();
@@ -413,8 +414,8 @@ impl ChatPanel {
     }
 
     fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
-        if let Some((channel, _)) = self.active_channel.as_ref() {
-            channel.update(cx, |channel, cx| {
+        if let Some((chat, _)) = self.active_chat.as_ref() {
+            chat.update(cx, |channel, cx| {
                 channel.load_more_messages(cx);
             })
         }
@@ -425,13 +426,19 @@ impl ChatPanel {
         selected_channel_id: u64,
         cx: &mut ViewContext<ChatPanel>,
     ) -> Task<Result<()>> {
+        if let Some((chat, _)) = &self.active_chat {
+            if chat.read(cx).channel().id == selected_channel_id {
+                return Task::ready(Ok(()));
+            }
+        }
+
         let open_chat = self.channel_store.update(cx, |store, cx| {
             store.open_channel_chat(selected_channel_id, cx)
         });
         cx.spawn(|this, mut cx| async move {
             let chat = open_chat.await?;
             this.update(&mut cx, |this, cx| {
-                this.set_active_channel(chat, cx);
+                this.set_active_chat(chat, cx);
             })
         })
     }

crates/collab_ui/src/collab_panel.rs 🔗

@@ -80,7 +80,7 @@ struct RenameChannel {
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct OpenChannelBuffer {
+struct OpenChannelNotes {
     channel_id: u64,
 }
 
@@ -104,7 +104,7 @@ impl_actions!(
         ManageMembers,
         RenameChannel,
         ToggleCollapse,
-        OpenChannelBuffer
+        OpenChannelNotes
     ]
 );
 
@@ -130,7 +130,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(CollabPanel::toggle_channel_collapsed);
     cx.add_action(CollabPanel::collapse_selected_channel);
     cx.add_action(CollabPanel::expand_selected_channel);
-    cx.add_action(CollabPanel::open_channel_buffer);
+    cx.add_action(CollabPanel::open_channel_notes);
 }
 
 #[derive(Debug)]
@@ -226,10 +226,6 @@ enum ListEntry {
         channel: Arc<Channel>,
         depth: usize,
     },
-    ChannelCall {
-        channel: Arc<Channel>,
-        depth: usize,
-    },
     ChannelNotes {
         channel_id: ChannelId,
     },
@@ -369,13 +365,6 @@ impl CollabPanel {
                                 return channel_row;
                             }
                         }
-                        ListEntry::ChannelCall { channel, depth } => this.render_channel_call(
-                            &*channel,
-                            *depth,
-                            &theme.collab_panel,
-                            is_selected,
-                            cx,
-                        ),
                         ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
                             *channel_id,
                             &theme.collab_panel,
@@ -751,12 +740,6 @@ impl CollabPanel {
                                 channel: channel.clone(),
                                 depth,
                             });
-                            if !channel_store.channel_participants(channel.id).is_empty() {
-                                self.entries.push(ListEntry::ChannelCall {
-                                    channel: channel.clone(),
-                                    depth,
-                                });
-                            }
                         }
                     }
                 }
@@ -1566,10 +1549,23 @@ impl CollabPanel {
         let disclosed =
             has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok());
 
+        let is_active = iife!({
+            let call_channel = ActiveCall::global(cx)
+                .read(cx)
+                .room()?
+                .read(cx)
+                .channel_id()?;
+            Some(call_channel == channel_id)
+        })
+        .unwrap_or(false);
+
+        const FACEPILE_LIMIT: usize = 3;
+
         enum ChannelCall {}
-        enum ChannelNotes {}
 
         MouseEventHandler::new::<Channel, _>(channel.id as usize, cx, |state, cx| {
+            let row_hovered = state.hovered();
+
             Flex::<Self>::row()
                 .with_child(
                     Svg::new("icons/hash.svg")
@@ -1588,29 +1584,49 @@ impl CollabPanel {
                         .flex(1., true),
                 )
                 .with_child(
-                    MouseEventHandler::new::<ChannelCall, _>(channel_id as usize, cx, |_, _| {
-                        Svg::new("icons/radix/speaker-loud.svg")
-                            .with_color(theme.channel_hash.color)
-                            .constrained()
-                            .with_width(theme.channel_hash.width)
-                            .aligned()
-                            .right()
-                    })
-                    .on_click(MouseButton::Left, move |_, this, cx| {
-                        this.join_channel_call(channel_id, cx)
-                    }),
-                )
-                .with_child(
-                    MouseEventHandler::new::<ChannelNotes, _>(channel_id as usize, cx, |_, _| {
-                        Svg::new("icons/radix/file.svg")
-                            .with_color(theme.channel_hash.color)
-                            .constrained()
-                            .with_width(theme.channel_hash.width)
-                            .aligned()
-                            .right()
-                    })
+                    MouseEventHandler::new::<ChannelCall, _>(
+                        channel.id as usize,
+                        cx,
+                        move |_, cx| {
+                            let participants =
+                                self.channel_store.read(cx).channel_participants(channel_id);
+                            if !participants.is_empty() {
+                                let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
+
+                                FacePile::new(theme.face_overlap)
+                                    .with_children(
+                                        participants
+                                            .iter()
+                                            .filter_map(|user| {
+                                                Some(
+                                                    Image::from_data(user.avatar.clone()?)
+                                                        .with_style(theme.channel_avatar),
+                                                )
+                                            })
+                                            .take(FACEPILE_LIMIT),
+                                    )
+                                    .with_children((extra_count > 0).then(|| {
+                                        Label::new(
+                                            format!("+{}", extra_count),
+                                            theme.extra_participant_label.text.clone(),
+                                        )
+                                        .contained()
+                                        .with_style(theme.extra_participant_label.container)
+                                    }))
+                                    .into_any()
+                            } else if row_hovered {
+                                Svg::new("icons/radix/speaker-loud.svg")
+                                    .with_color(theme.channel_hash.color)
+                                    .constrained()
+                                    .with_width(theme.channel_hash.width)
+                                    .into_any()
+                            } else {
+                                Empty::new().into_any()
+                            }
+                        },
+                    )
                     .on_click(MouseButton::Left, move |_, this, cx| {
-                        this.open_channel_buffer(&OpenChannelBuffer { channel_id }, cx);
+                        this.join_channel_call(channel_id, cx);
                     }),
                 )
                 .align_children_center()
@@ -1622,7 +1638,7 @@ impl CollabPanel {
                 .constrained()
                 .with_height(theme.row_height)
                 .contained()
-                .with_style(*theme.channel_row.style_for(is_selected, state))
+                .with_style(*theme.channel_row.style_for(is_selected || is_active, state))
                 .with_padding_left(
                     theme.channel_row.default_style().padding.left
                         + theme.channel_indent * depth as f32,
@@ -1638,94 +1654,6 @@ impl CollabPanel {
         .into_any()
     }
 
-    fn render_channel_call(
-        &self,
-        channel: &Channel,
-        depth: usize,
-        theme: &theme::CollabPanel,
-        is_selected: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let channel_id = channel.id;
-
-        let is_active = iife!({
-            let call_channel = ActiveCall::global(cx)
-                .read(cx)
-                .room()?
-                .read(cx)
-                .channel_id()?;
-            Some(call_channel == channel_id)
-        })
-        .unwrap_or(false);
-
-        const FACEPILE_LIMIT: usize = 5;
-
-        enum ChannelCall {}
-
-        let host_avatar_width = theme
-            .contact_avatar
-            .width
-            .or(theme.contact_avatar.height)
-            .unwrap_or(0.);
-
-        MouseEventHandler::new::<ChannelCall, _>(channel.id as usize, cx, |state, cx| {
-            let participants = self.channel_store.read(cx).channel_participants(channel_id);
-            let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
-            let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
-            let row = theme.project_row.in_state(is_selected).style_for(state);
-
-            Flex::<Self>::row()
-                .with_child(render_tree_branch(
-                    tree_branch,
-                    &row.name.text,
-                    true,
-                    vec2f(host_avatar_width, theme.row_height),
-                    cx.font_cache(),
-                ))
-                .with_child(
-                    FacePile::new(theme.face_overlap)
-                        .with_children(
-                            participants
-                                .iter()
-                                .filter_map(|user| {
-                                    Some(
-                                        Image::from_data(user.avatar.clone()?)
-                                            .with_style(theme.channel_avatar),
-                                    )
-                                })
-                                .take(FACEPILE_LIMIT),
-                        )
-                        .with_children((extra_count > 0).then(|| {
-                            Label::new(
-                                format!("+{}", extra_count),
-                                theme.extra_participant_label.text.clone(),
-                            )
-                            .contained()
-                            .with_style(theme.extra_participant_label.container)
-                        })),
-                )
-                .align_children_center()
-                .constrained()
-                .with_height(theme.row_height)
-                .aligned()
-                .left()
-                .contained()
-                .with_style(*theme.channel_row.style_for(is_selected || is_active, state))
-                .with_padding_left(
-                    theme.channel_row.default_style().padding.left
-                        + theme.channel_indent * (depth + 1) as f32,
-                )
-        })
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            this.join_channel_call(channel_id, cx);
-        })
-        .on_click(MouseButton::Right, move |e, this, cx| {
-            this.deploy_channel_context_menu(Some(e.position), channel_id, cx);
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .into_any()
-    }
-
     fn render_channel_notes(
         &self,
         channel_id: ChannelId,
@@ -1775,7 +1703,7 @@ impl CollabPanel {
                 .with_padding_left(theme.channel_row.default_style().padding.left)
         })
         .on_click(MouseButton::Left, move |_, this, cx| {
-            this.open_channel_buffer(&OpenChannelBuffer { channel_id }, cx);
+            this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
         })
         .with_cursor_style(CursorStyle::PointingHand)
         .into_any()
@@ -1987,7 +1915,7 @@ impl CollabPanel {
 
             let mut items = vec![
                 ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }),
-                ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }),
+                ContextMenuItem::action("Open Notes", OpenChannelNotes { channel_id }),
             ];
 
             if self.channel_store.read(cx).is_user_admin(channel_id) {
@@ -2114,9 +2042,6 @@ impl CollabPanel {
                     ListEntry::Channel { channel, .. } => {
                         self.join_channel_chat(channel.id, cx);
                     }
-                    ListEntry::ChannelCall { channel, .. } => {
-                        self.join_channel_call(channel.id, cx);
-                    }
                     ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
                     _ => {}
                 }
@@ -2325,7 +2250,7 @@ impl CollabPanel {
         }
     }
 
-    fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext<Self>) {
+    fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
         if let Some(workspace) = self.workspace.upgrade(cx) {
             let pane = workspace.read(cx).active_pane().clone();
             let channel_id = action.channel_id;
@@ -2510,7 +2435,9 @@ impl CollabPanel {
             .detach_and_log_err(cx);
     }
 
-    fn join_channel_chat(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
+    fn join_channel_chat(&mut self, channel_id: u64, cx: &mut ViewContext<Self>) {
+        self.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
+
         if let Some(workspace) = self.workspace.upgrade(cx) {
             cx.app_context().defer(move |cx| {
                 workspace.update(cx, |workspace, cx| {
@@ -2757,18 +2684,6 @@ impl PartialEq for ListEntry {
                     return channel_1.id == channel_2.id && depth_1 == depth_2;
                 }
             }
-            ListEntry::ChannelCall {
-                channel: channel_1,
-                depth: depth_1,
-            } => {
-                if let ListEntry::ChannelCall {
-                    channel: channel_2,
-                    depth: depth_2,
-                } = other
-                {
-                    return channel_1.id == channel_2.id && depth_1 == depth_2;
-                }
-            }
             ListEntry::ChannelNotes { channel_id } => {
                 if let ListEntry::ChannelNotes {
                     channel_id: other_id,