More progress on collab panel

Conrad Irwin created

Change summary

crates/collab_ui2/src/collab_panel.rs    | 163 +++++++++++++++++++++----
crates/collab_ui2/src/face_pile.rs       |   4 
crates/gpui2/src/elements/overlay.rs     |   1 
crates/ui2/src/components/icon.rs        |   2 
crates/ui2/src/components/icon_button.rs |   4 
5 files changed, 142 insertions(+), 32 deletions(-)

Detailed changes

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -17,6 +17,7 @@ mod contact_finder;
 //     Client, Contact, User, UserStore,
 // };
 use contact_finder::ContactFinder;
+use rpc::proto;
 // use context_menu::{ContextMenu, ContextMenuItem};
 // use db::kvp::KEY_VALUE_STORE;
 // use drag_and_drop::{DragAndDrop, Draggable};
@@ -166,15 +167,17 @@ use editor::Editor;
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions, div, img, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle,
-    Focusable, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render,
-    RenderOnce, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
+    actions, div, img, prelude::*, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter,
+    FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model, ParentElement,
+    Render, RenderOnce, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
+    WeakView,
 };
 use project::Fs;
 use serde_derive::{Deserialize, Serialize};
 use settings::Settings;
 use ui::{
-    h_stack, v_stack, Avatar, Button, Icon, IconButton, Label, List, ListHeader, ListItem, Tooltip,
+    h_stack, v_stack, Avatar, Button, Color, Icon, IconButton, Label, List, ListHeader, ListItem,
+    Toggle, Tooltip,
 };
 use util::{maybe, ResultExt};
 use workspace::{
@@ -183,7 +186,7 @@ use workspace::{
     Workspace,
 };
 
-use crate::CollaborationPanelSettings;
+use crate::{face_pile::FacePile, CollaborationPanelSettings};
 
 pub fn init(cx: &mut AppContext) {
     cx.observe_new_views(|workspace: &mut Workspace, _| {
@@ -309,7 +312,7 @@ pub struct CollabPanel {
     // channel_name_editor: ViewHandle<Editor>,
     channel_editing_state: Option<ChannelEditingState>,
     entries: Vec<ListEntry>,
-    // selection: Option<usize>,
+    selection: Option<usize>,
     channel_store: Model<ChannelStore>,
     user_store: Model<UserStore>,
     client: Arc<Client>,
@@ -600,7 +603,7 @@ impl CollabPanel {
                 filter_editor,
                 entries: Vec::default(),
                 channel_editing_state: None,
-                //                 selection: None,
+                selection: None,
                 channel_store: ChannelStore::global(cx),
                 user_store: workspace.user_store().clone(),
                 //                 project: workspace.project().clone(),
@@ -2357,14 +2360,14 @@ impl CollabPanel {
     //         self.deploy_channel_context_menu(None, &channel.clone(), self.selection.unwrap(), cx);
     //     }
 
-    //     fn selected_channel(&self) -> Option<&Arc<Channel>> {
-    //         self.selection
-    //             .and_then(|ix| self.entries.get(ix))
-    //             .and_then(|entry| match entry {
-    //                 ListEntry::Channel { channel, .. } => Some(channel),
-    //                 _ => None,
-    //             })
-    //     }
+    fn selected_channel(&self) -> Option<&Arc<Channel>> {
+        self.selection
+            .and_then(|ix| self.entries.get(ix))
+            .and_then(|entry| match entry {
+                ListEntry::Channel { channel, .. } => Some(channel),
+                _ => None,
+            })
+    }
 
     //     fn show_channel_modal(
     //         &mut self,
@@ -3019,7 +3022,123 @@ impl CollabPanel {
         cx: &mut ViewContext<Self>,
     ) -> impl IntoElement {
         let channel_id = channel.id;
-        ListItem::new(channel_id as usize).child(Label::new(channel.name.clone()))
+
+        let is_public = self
+            .channel_store
+            .read(cx)
+            .channel_for_id(channel_id)
+            .map(|channel| channel.visibility)
+            == Some(proto::ChannelVisibility::Public);
+        let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
+        let disclosed = has_children
+            .then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok())
+            .unwrap_or(false);
+
+        let is_active = maybe!({
+            let call_channel = ActiveCall::global(cx)
+                .read(cx)
+                .room()?
+                .read(cx)
+                .channel_id()?;
+            Some(call_channel == channel_id)
+        })
+        .unwrap_or(false);
+
+        let has_messages_notification = channel.unseen_message_id.is_some() || true;
+        let has_notes_notification = channel.unseen_note_version.is_some();
+
+        const FACEPILE_LIMIT: usize = 3;
+        let participants = self.channel_store.read(cx).channel_participants(channel_id);
+
+        let face_pile = if !participants.is_empty() {
+            let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
+
+            let result = FacePile {
+                faces: participants
+                    .iter()
+                    .filter_map(|user| Some(Avatar::data(user.avatar.clone()?).into_any_element()))
+                    .take(FACEPILE_LIMIT)
+                    .chain(if extra_count > 0 {
+                        Some(Label::new(format!("+{}", extra_count)).into_any_element())
+                    } else {
+                        None
+                    })
+                    .collect::<Vec<_>>(),
+            };
+
+            Some(result)
+        } else {
+            None
+        };
+
+        div().group("").child(
+            ListItem::new(channel_id as usize)
+                .indent_level(depth)
+                .left_icon(if is_public { Icon::Public } else { Icon::Hash })
+                .selected(is_selected || is_active)
+                .child(
+                    h_stack()
+                        .w_full()
+                        .justify_between()
+                        .child(
+                            div()
+                                .id(channel_id as usize)
+                                .child(Label::new(channel.name.clone()))
+                                .children(face_pile.map(|face_pile| face_pile.render(cx)))
+                                .tooltip(|cx| Tooltip::text("Join channel", cx)),
+                        )
+                        .child(
+                            h_stack()
+                                .child(
+                                    div()
+                                        .id("channel_chat")
+                                        .bg(gpui::blue())
+                                        .when(!has_messages_notification, |el| el.invisible())
+                                        .group_hover("", |style| style.visible())
+                                        .child(
+                                            IconButton::new("test_chat", Icon::MessageBubbles)
+                                                .color(if has_messages_notification {
+                                                    Color::Default
+                                                } else {
+                                                    Color::Muted
+                                                }),
+                                        )
+                                        .tooltip(|cx| Tooltip::text("Open channel chat", cx)),
+                                )
+                                .child(
+                                    div()
+                                        .id("channel_notes")
+                                        .when(!has_notes_notification, |el| el.invisible())
+                                        .group_hover("", |style| style.visible())
+                                        .child(
+                                            div().child("Notes").id("test_notes").tooltip(|cx| {
+                                                Tooltip::text("Open channel notes", cx)
+                                            }),
+                                        ), // .child(
+                                           //     IconButton::new("channel_notes", Icon::File)
+                                           //         .color(if has_notes_notification {
+                                           //             Color::Default
+                                           //         } else {
+                                           //             Color::Muted
+                                           //         })
+                                           //         .tooltip(|cx| {
+                                           //             Tooltip::text("Open channel notes", cx)
+                                           //         }),
+                                           // ),
+                                ),
+                        ),
+                )
+                .toggle(if has_children {
+                    Toggle::Toggled(disclosed)
+                } else {
+                    Toggle::NotToggleable
+                })
+                .on_click(cx.listener(|this, _, cx| todo!()))
+                .on_secondary_mouse_down(cx.listener(|this, _, cx| {
+                    todo!() // open context menu
+                })),
+        )
+
         // let channel_id = channel.id;
         // let collab_theme = &theme.collab_panel;
         // let is_public = self
@@ -3032,18 +3151,6 @@ impl CollabPanel {
         // let disclosed =
         //     has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
 
-        // let is_active = maybe!({
-        //     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 ChannelNote {}
         // enum NotesTooltip {}

crates/collab_ui2/src/face_pile.rs 🔗

@@ -3,8 +3,8 @@ use gpui::{
 };
 
 #[derive(Default)]
-pub(crate) struct FacePile {
-    faces: Vec<AnyElement>,
+pub struct FacePile {
+    pub faces: Vec<AnyElement>,
 }
 
 impl RenderOnce for FacePile {

crates/gpui2/src/elements/overlay.rs 🔗

@@ -105,7 +105,6 @@ impl Element for Overlay {
             origin: Point::zero(),
             size: cx.viewport_size(),
         };
-        dbg!(limits);
 
         match self.fit_mode {
             OverlayFitMode::SnapToWindow => {

crates/ui2/src/components/icon.rs 🔗

@@ -63,6 +63,7 @@ pub enum Icon {
     Mic,
     MicMute,
     Plus,
+    Public,
     Quote,
     Replace,
     ReplaceAll,
@@ -134,6 +135,7 @@ impl Icon {
             Icon::Mic => "icons/mic.svg",
             Icon::MicMute => "icons/mic-mute.svg",
             Icon::Plus => "icons/plus.svg",
+            Icon::Public => "icons/public.svg",
             Icon::Quote => "icons/quote.svg",
             Icon::Replace => "icons/replace.svg",
             Icon::ReplaceAll => "icons/replace_all.svg",

crates/ui2/src/components/icon_button.rs 🔗

@@ -65,7 +65,9 @@ impl RenderOnce for IconButton {
             }
         }
 
-        button
+        // todo! this extra wrapping div is to work around a bug
+        // where tooltips otherwise don't show up.
+        div().id(self.id.clone()).child(button)
     }
 }