collab_panel.rs

   1mod channel_modal;
   2mod contact_finder;
   3
   4use self::channel_modal::ChannelModal;
   5use crate::{
   6    channel_view::ChannelView, chat_panel::ChatPanel, face_pile::FacePile,
   7    CollaborationPanelSettings,
   8};
   9use call::ActiveCall;
  10use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
  11use client::{Client, Contact, User, UserStore};
  12use contact_finder::ContactFinder;
  13use db::kvp::KEY_VALUE_STORE;
  14use editor::{Editor, EditorElement, EditorStyle};
  15use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
  16use fuzzy::{match_strings, StringMatchCandidate};
  17use gpui::{
  18    actions, canvas, div, fill, list, overlay, point, prelude::*, px, AnyElement, AppContext,
  19    AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle,
  20    FocusableView, FontStyle, FontWeight, InteractiveElement, IntoElement, ListOffset, ListState,
  21    Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce,
  22    SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext,
  23    WeakView, WhiteSpace,
  24};
  25use menu::{Cancel, Confirm, SelectNext, SelectPrev};
  26use project::{Fs, Project};
  27use rpc::proto::{self, PeerId};
  28use serde_derive::{Deserialize, Serialize};
  29use settings::{Settings, SettingsStore};
  30use smallvec::SmallVec;
  31use std::{mem, sync::Arc};
  32use theme::{ActiveTheme, ThemeSettings};
  33use ui::{
  34    prelude::*, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconName, IconSize, Label,
  35    ListHeader, ListItem, Tooltip,
  36};
  37use util::{maybe, ResultExt, TryFutureExt};
  38use workspace::{
  39    dock::{DockPosition, Panel, PanelEvent},
  40    notifications::{NotifyResultExt, NotifyTaskExt},
  41    Workspace,
  42};
  43
  44actions!(
  45    collab_panel,
  46    [
  47        ToggleFocus,
  48        Remove,
  49        Secondary,
  50        CollapseSelectedChannel,
  51        ExpandSelectedChannel,
  52        StartMoveChannel,
  53        MoveSelected,
  54        InsertSpace,
  55    ]
  56);
  57
  58#[derive(Debug, Copy, Clone, PartialEq, Eq)]
  59struct ChannelMoveClipboard {
  60    channel_id: ChannelId,
  61}
  62
  63const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
  64
  65pub fn init(cx: &mut AppContext) {
  66    cx.observe_new_views(|workspace: &mut Workspace, _| {
  67        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
  68            workspace.toggle_panel_focus::<CollabPanel>(cx);
  69        });
  70    })
  71    .detach();
  72}
  73
  74#[derive(Debug)]
  75pub enum ChannelEditingState {
  76    Create {
  77        location: Option<ChannelId>,
  78        pending_name: Option<String>,
  79    },
  80    Rename {
  81        location: ChannelId,
  82        pending_name: Option<String>,
  83    },
  84}
  85
  86impl ChannelEditingState {
  87    fn pending_name(&self) -> Option<String> {
  88        match self {
  89            ChannelEditingState::Create { pending_name, .. } => pending_name.clone(),
  90            ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(),
  91        }
  92    }
  93}
  94
  95pub struct CollabPanel {
  96    width: Option<Pixels>,
  97    fs: Arc<dyn Fs>,
  98    focus_handle: FocusHandle,
  99    channel_clipboard: Option<ChannelMoveClipboard>,
 100    pending_serialization: Task<Option<()>>,
 101    context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
 102    list_state: ListState,
 103    filter_editor: View<Editor>,
 104    channel_name_editor: View<Editor>,
 105    channel_editing_state: Option<ChannelEditingState>,
 106    entries: Vec<ListEntry>,
 107    selection: Option<usize>,
 108    channel_store: Model<ChannelStore>,
 109    user_store: Model<UserStore>,
 110    client: Arc<Client>,
 111    project: Model<Project>,
 112    match_candidates: Vec<StringMatchCandidate>,
 113    subscriptions: Vec<Subscription>,
 114    collapsed_sections: Vec<Section>,
 115    collapsed_channels: Vec<ChannelId>,
 116    workspace: WeakView<Workspace>,
 117}
 118
 119#[derive(Serialize, Deserialize)]
 120struct SerializedCollabPanel {
 121    width: Option<Pixels>,
 122    collapsed_channels: Option<Vec<u64>>,
 123}
 124
 125#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
 126enum Section {
 127    ActiveCall,
 128    Channels,
 129    ChannelInvites,
 130    ContactRequests,
 131    Contacts,
 132    Online,
 133    Offline,
 134}
 135
 136#[derive(Clone, Debug)]
 137enum ListEntry {
 138    Header(Section),
 139    CallParticipant {
 140        user: Arc<User>,
 141        peer_id: Option<PeerId>,
 142        is_pending: bool,
 143        role: proto::ChannelRole,
 144    },
 145    ParticipantProject {
 146        project_id: u64,
 147        worktree_root_names: Vec<String>,
 148        host_user_id: u64,
 149        is_last: bool,
 150    },
 151    ParticipantScreen {
 152        peer_id: Option<PeerId>,
 153        is_last: bool,
 154    },
 155    IncomingRequest(Arc<User>),
 156    OutgoingRequest(Arc<User>),
 157    ChannelInvite(Arc<Channel>),
 158    Channel {
 159        channel: Arc<Channel>,
 160        depth: usize,
 161        has_children: bool,
 162    },
 163    ChannelNotes {
 164        channel_id: ChannelId,
 165    },
 166    ChannelChat {
 167        channel_id: ChannelId,
 168    },
 169    ChannelEditor {
 170        depth: usize,
 171    },
 172    Contact {
 173        contact: Arc<Contact>,
 174        calling: bool,
 175    },
 176    ContactPlaceholder,
 177}
 178
 179impl CollabPanel {
 180    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
 181        cx.new_view(|cx| {
 182            let filter_editor = cx.new_view(|cx| {
 183                let mut editor = Editor::single_line(cx);
 184                editor.set_placeholder_text("Filter...", cx);
 185                editor
 186            });
 187
 188            cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {
 189                if let editor::EditorEvent::BufferEdited = event {
 190                    let query = this.filter_editor.read(cx).text(cx);
 191                    if !query.is_empty() {
 192                        this.selection.take();
 193                    }
 194                    this.update_entries(true, cx);
 195                    if !query.is_empty() {
 196                        this.selection = this
 197                            .entries
 198                            .iter()
 199                            .position(|entry| !matches!(entry, ListEntry::Header(_)));
 200                    }
 201                }
 202            })
 203            .detach();
 204
 205            let channel_name_editor = cx.new_view(|cx| Editor::single_line(cx));
 206
 207            cx.subscribe(&channel_name_editor, |this: &mut Self, _, event, cx| {
 208                if let editor::EditorEvent::Blurred = event {
 209                    if let Some(state) = &this.channel_editing_state {
 210                        if state.pending_name().is_some() {
 211                            return;
 212                        }
 213                    }
 214                    this.take_editing_state(cx);
 215                    this.update_entries(false, cx);
 216                    cx.notify();
 217                }
 218            })
 219            .detach();
 220
 221            let view = cx.view().downgrade();
 222            let list_state =
 223                ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| {
 224                    if let Some(view) = view.upgrade() {
 225                        view.update(cx, |view, cx| view.render_list_entry(ix, cx))
 226                    } else {
 227                        div().into_any()
 228                    }
 229                });
 230
 231            let mut this = Self {
 232                width: None,
 233                focus_handle: cx.focus_handle(),
 234                channel_clipboard: None,
 235                fs: workspace.app_state().fs.clone(),
 236                pending_serialization: Task::ready(None),
 237                context_menu: None,
 238                list_state,
 239                channel_name_editor,
 240                filter_editor,
 241                entries: Vec::default(),
 242                channel_editing_state: None,
 243                selection: None,
 244                channel_store: ChannelStore::global(cx),
 245                user_store: workspace.user_store().clone(),
 246                project: workspace.project().clone(),
 247                subscriptions: Vec::default(),
 248                match_candidates: Vec::default(),
 249                collapsed_sections: vec![Section::Offline],
 250                collapsed_channels: Vec::default(),
 251                workspace: workspace.weak_handle(),
 252                client: workspace.app_state().client.clone(),
 253            };
 254
 255            this.update_entries(false, cx);
 256
 257            // Update the dock position when the setting changes.
 258            let mut old_dock_position = this.position(cx);
 259            this.subscriptions.push(cx.observe_global::<SettingsStore>(
 260                move |this: &mut Self, cx| {
 261                    let new_dock_position = this.position(cx);
 262                    if new_dock_position != old_dock_position {
 263                        old_dock_position = new_dock_position;
 264                        cx.emit(PanelEvent::ChangePosition);
 265                    }
 266                    cx.notify();
 267                },
 268            ));
 269
 270            let active_call = ActiveCall::global(cx);
 271            this.subscriptions
 272                .push(cx.observe(&this.user_store, |this, _, cx| {
 273                    this.update_entries(true, cx)
 274                }));
 275            this.subscriptions
 276                .push(cx.observe(&this.channel_store, |this, _, cx| {
 277                    this.update_entries(true, cx)
 278                }));
 279            this.subscriptions
 280                .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
 281            this.subscriptions
 282                .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
 283                    this.update_entries(true, cx)
 284                }));
 285            this.subscriptions.push(cx.subscribe(
 286                &this.channel_store,
 287                |this, _channel_store, e, cx| match e {
 288                    ChannelEvent::ChannelCreated(channel_id)
 289                    | ChannelEvent::ChannelRenamed(channel_id) => {
 290                        if this.take_editing_state(cx) {
 291                            this.update_entries(false, cx);
 292                            this.selection = this.entries.iter().position(|entry| {
 293                                if let ListEntry::Channel { channel, .. } = entry {
 294                                    channel.id == *channel_id
 295                                } else {
 296                                    false
 297                                }
 298                            });
 299                        }
 300                    }
 301                },
 302            ));
 303
 304            this
 305        })
 306    }
 307
 308    pub async fn load(
 309        workspace: WeakView<Workspace>,
 310        mut cx: AsyncWindowContext,
 311    ) -> anyhow::Result<View<Self>> {
 312        let serialized_panel = cx
 313            .background_executor()
 314            .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
 315            .await
 316            .map_err(|_| anyhow::anyhow!("Failed to read collaboration panel from key value store"))
 317            .log_err()
 318            .flatten()
 319            .map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel))
 320            .transpose()
 321            .log_err()
 322            .flatten();
 323
 324        workspace.update(&mut cx, |workspace, cx| {
 325            let panel = CollabPanel::new(workspace, cx);
 326            if let Some(serialized_panel) = serialized_panel {
 327                panel.update(cx, |panel, cx| {
 328                    panel.width = serialized_panel.width;
 329                    panel.collapsed_channels = serialized_panel
 330                        .collapsed_channels
 331                        .unwrap_or_else(|| Vec::new());
 332                    cx.notify();
 333                });
 334            }
 335            panel
 336        })
 337    }
 338
 339    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 340        let width = self.width;
 341        let collapsed_channels = self.collapsed_channels.clone();
 342        self.pending_serialization = cx.background_executor().spawn(
 343            async move {
 344                KEY_VALUE_STORE
 345                    .write_kvp(
 346                        COLLABORATION_PANEL_KEY.into(),
 347                        serde_json::to_string(&SerializedCollabPanel {
 348                            width,
 349                            collapsed_channels: Some(collapsed_channels),
 350                        })?,
 351                    )
 352                    .await?;
 353                anyhow::Ok(())
 354            }
 355            .log_err(),
 356        );
 357    }
 358
 359    fn scroll_to_item(&mut self, ix: usize) {
 360        self.list_state.scroll_to_reveal_item(ix)
 361    }
 362
 363    fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
 364        let channel_store = self.channel_store.read(cx);
 365        let user_store = self.user_store.read(cx);
 366        let query = self.filter_editor.read(cx).text(cx);
 367        let executor = cx.background_executor().clone();
 368
 369        let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
 370        let old_entries = mem::take(&mut self.entries);
 371        let mut scroll_to_top = false;
 372
 373        if let Some(room) = ActiveCall::global(cx).read(cx).room() {
 374            self.entries.push(ListEntry::Header(Section::ActiveCall));
 375            if !old_entries
 376                .iter()
 377                .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
 378            {
 379                scroll_to_top = true;
 380            }
 381
 382            if !self.collapsed_sections.contains(&Section::ActiveCall) {
 383                let room = room.read(cx);
 384
 385                if let Some(channel_id) = room.channel_id() {
 386                    self.entries.push(ListEntry::ChannelNotes { channel_id });
 387                    self.entries.push(ListEntry::ChannelChat { channel_id });
 388                }
 389
 390                // Populate the active user.
 391                if let Some(user) = user_store.current_user() {
 392                    self.match_candidates.clear();
 393                    self.match_candidates.push(StringMatchCandidate {
 394                        id: 0,
 395                        string: user.github_login.clone(),
 396                        char_bag: user.github_login.chars().collect(),
 397                    });
 398                    let matches = executor.block(match_strings(
 399                        &self.match_candidates,
 400                        &query,
 401                        true,
 402                        usize::MAX,
 403                        &Default::default(),
 404                        executor.clone(),
 405                    ));
 406                    if !matches.is_empty() {
 407                        let user_id = user.id;
 408                        self.entries.push(ListEntry::CallParticipant {
 409                            user,
 410                            peer_id: None,
 411                            is_pending: false,
 412                            role: room.local_participant().role,
 413                        });
 414                        let mut projects = room.local_participant().projects.iter().peekable();
 415                        while let Some(project) = projects.next() {
 416                            self.entries.push(ListEntry::ParticipantProject {
 417                                project_id: project.id,
 418                                worktree_root_names: project.worktree_root_names.clone(),
 419                                host_user_id: user_id,
 420                                is_last: projects.peek().is_none() && !room.is_screen_sharing(),
 421                            });
 422                        }
 423                        if room.is_screen_sharing() {
 424                            self.entries.push(ListEntry::ParticipantScreen {
 425                                peer_id: None,
 426                                is_last: true,
 427                            });
 428                        }
 429                    }
 430                }
 431
 432                // Populate remote participants.
 433                self.match_candidates.clear();
 434                self.match_candidates
 435                    .extend(
 436                        room.remote_participants()
 437                            .iter()
 438                            .filter_map(|(_, participant)| {
 439                                Some(StringMatchCandidate {
 440                                    id: participant.user.id as usize,
 441                                    string: participant.user.github_login.clone(),
 442                                    char_bag: participant.user.github_login.chars().collect(),
 443                                })
 444                            }),
 445                    );
 446                let mut matches = executor.block(match_strings(
 447                    &self.match_candidates,
 448                    &query,
 449                    true,
 450                    usize::MAX,
 451                    &Default::default(),
 452                    executor.clone(),
 453                ));
 454                matches.sort_by(|a, b| {
 455                    let a_is_guest = room.role_for_user(a.candidate_id as u64)
 456                        == Some(proto::ChannelRole::Guest);
 457                    let b_is_guest = room.role_for_user(b.candidate_id as u64)
 458                        == Some(proto::ChannelRole::Guest);
 459                    a_is_guest
 460                        .cmp(&b_is_guest)
 461                        .then_with(|| a.string.cmp(&b.string))
 462                });
 463                for mat in matches {
 464                    let user_id = mat.candidate_id as u64;
 465                    let participant = &room.remote_participants()[&user_id];
 466                    self.entries.push(ListEntry::CallParticipant {
 467                        user: participant.user.clone(),
 468                        peer_id: Some(participant.peer_id),
 469                        is_pending: false,
 470                        role: participant.role,
 471                    });
 472                    let mut projects = participant.projects.iter().peekable();
 473                    while let Some(project) = projects.next() {
 474                        self.entries.push(ListEntry::ParticipantProject {
 475                            project_id: project.id,
 476                            worktree_root_names: project.worktree_root_names.clone(),
 477                            host_user_id: participant.user.id,
 478                            is_last: projects.peek().is_none()
 479                                && participant.video_tracks.is_empty(),
 480                        });
 481                    }
 482                    if !participant.video_tracks.is_empty() {
 483                        self.entries.push(ListEntry::ParticipantScreen {
 484                            peer_id: Some(participant.peer_id),
 485                            is_last: true,
 486                        });
 487                    }
 488                }
 489
 490                // Populate pending participants.
 491                self.match_candidates.clear();
 492                self.match_candidates
 493                    .extend(room.pending_participants().iter().enumerate().map(
 494                        |(id, participant)| StringMatchCandidate {
 495                            id,
 496                            string: participant.github_login.clone(),
 497                            char_bag: participant.github_login.chars().collect(),
 498                        },
 499                    ));
 500                let matches = executor.block(match_strings(
 501                    &self.match_candidates,
 502                    &query,
 503                    true,
 504                    usize::MAX,
 505                    &Default::default(),
 506                    executor.clone(),
 507                ));
 508                self.entries
 509                    .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
 510                        user: room.pending_participants()[mat.candidate_id].clone(),
 511                        peer_id: None,
 512                        is_pending: true,
 513                        role: proto::ChannelRole::Member,
 514                    }));
 515            }
 516        }
 517
 518        let mut request_entries = Vec::new();
 519
 520        if cx.has_flag::<ChannelsAlpha>() {
 521            self.entries.push(ListEntry::Header(Section::Channels));
 522
 523            if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
 524                self.match_candidates.clear();
 525                self.match_candidates
 526                    .extend(channel_store.ordered_channels().enumerate().map(
 527                        |(ix, (_, channel))| StringMatchCandidate {
 528                            id: ix,
 529                            string: channel.name.clone().into(),
 530                            char_bag: channel.name.chars().collect(),
 531                        },
 532                    ));
 533                let matches = executor.block(match_strings(
 534                    &self.match_candidates,
 535                    &query,
 536                    true,
 537                    usize::MAX,
 538                    &Default::default(),
 539                    executor.clone(),
 540                ));
 541                if let Some(state) = &self.channel_editing_state {
 542                    if matches!(state, ChannelEditingState::Create { location: None, .. }) {
 543                        self.entries.push(ListEntry::ChannelEditor { depth: 0 });
 544                    }
 545                }
 546                let mut collapse_depth = None;
 547                for mat in matches {
 548                    let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
 549                    let depth = channel.parent_path.len();
 550
 551                    if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
 552                        collapse_depth = Some(depth);
 553                    } else if let Some(collapsed_depth) = collapse_depth {
 554                        if depth > collapsed_depth {
 555                            continue;
 556                        }
 557                        if self.is_channel_collapsed(channel.id) {
 558                            collapse_depth = Some(depth);
 559                        } else {
 560                            collapse_depth = None;
 561                        }
 562                    }
 563
 564                    let has_children = channel_store
 565                        .channel_at_index(mat.candidate_id + 1)
 566                        .map_or(false, |next_channel| {
 567                            next_channel.parent_path.ends_with(&[channel.id])
 568                        });
 569
 570                    match &self.channel_editing_state {
 571                        Some(ChannelEditingState::Create {
 572                            location: parent_id,
 573                            ..
 574                        }) if *parent_id == Some(channel.id) => {
 575                            self.entries.push(ListEntry::Channel {
 576                                channel: channel.clone(),
 577                                depth,
 578                                has_children: false,
 579                            });
 580                            self.entries
 581                                .push(ListEntry::ChannelEditor { depth: depth + 1 });
 582                        }
 583                        Some(ChannelEditingState::Rename {
 584                            location: parent_id,
 585                            ..
 586                        }) if parent_id == &channel.id => {
 587                            self.entries.push(ListEntry::ChannelEditor { depth });
 588                        }
 589                        _ => {
 590                            self.entries.push(ListEntry::Channel {
 591                                channel: channel.clone(),
 592                                depth,
 593                                has_children,
 594                            });
 595                        }
 596                    }
 597                }
 598            }
 599
 600            let channel_invites = channel_store.channel_invitations();
 601            if !channel_invites.is_empty() {
 602                self.match_candidates.clear();
 603                self.match_candidates
 604                    .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
 605                        StringMatchCandidate {
 606                            id: ix,
 607                            string: channel.name.clone().into(),
 608                            char_bag: channel.name.chars().collect(),
 609                        }
 610                    }));
 611                let matches = executor.block(match_strings(
 612                    &self.match_candidates,
 613                    &query,
 614                    true,
 615                    usize::MAX,
 616                    &Default::default(),
 617                    executor.clone(),
 618                ));
 619                request_entries.extend(matches.iter().map(|mat| {
 620                    ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
 621                }));
 622
 623                if !request_entries.is_empty() {
 624                    self.entries
 625                        .push(ListEntry::Header(Section::ChannelInvites));
 626                    if !self.collapsed_sections.contains(&Section::ChannelInvites) {
 627                        self.entries.append(&mut request_entries);
 628                    }
 629                }
 630            }
 631        }
 632
 633        self.entries.push(ListEntry::Header(Section::Contacts));
 634
 635        request_entries.clear();
 636        let incoming = user_store.incoming_contact_requests();
 637        if !incoming.is_empty() {
 638            self.match_candidates.clear();
 639            self.match_candidates
 640                .extend(
 641                    incoming
 642                        .iter()
 643                        .enumerate()
 644                        .map(|(ix, user)| StringMatchCandidate {
 645                            id: ix,
 646                            string: user.github_login.clone(),
 647                            char_bag: user.github_login.chars().collect(),
 648                        }),
 649                );
 650            let matches = executor.block(match_strings(
 651                &self.match_candidates,
 652                &query,
 653                true,
 654                usize::MAX,
 655                &Default::default(),
 656                executor.clone(),
 657            ));
 658            request_entries.extend(
 659                matches
 660                    .iter()
 661                    .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
 662            );
 663        }
 664
 665        let outgoing = user_store.outgoing_contact_requests();
 666        if !outgoing.is_empty() {
 667            self.match_candidates.clear();
 668            self.match_candidates
 669                .extend(
 670                    outgoing
 671                        .iter()
 672                        .enumerate()
 673                        .map(|(ix, user)| StringMatchCandidate {
 674                            id: ix,
 675                            string: user.github_login.clone(),
 676                            char_bag: user.github_login.chars().collect(),
 677                        }),
 678                );
 679            let matches = executor.block(match_strings(
 680                &self.match_candidates,
 681                &query,
 682                true,
 683                usize::MAX,
 684                &Default::default(),
 685                executor.clone(),
 686            ));
 687            request_entries.extend(
 688                matches
 689                    .iter()
 690                    .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
 691            );
 692        }
 693
 694        if !request_entries.is_empty() {
 695            self.entries
 696                .push(ListEntry::Header(Section::ContactRequests));
 697            if !self.collapsed_sections.contains(&Section::ContactRequests) {
 698                self.entries.append(&mut request_entries);
 699            }
 700        }
 701
 702        let contacts = user_store.contacts();
 703        if !contacts.is_empty() {
 704            self.match_candidates.clear();
 705            self.match_candidates
 706                .extend(
 707                    contacts
 708                        .iter()
 709                        .enumerate()
 710                        .map(|(ix, contact)| StringMatchCandidate {
 711                            id: ix,
 712                            string: contact.user.github_login.clone(),
 713                            char_bag: contact.user.github_login.chars().collect(),
 714                        }),
 715                );
 716
 717            let matches = executor.block(match_strings(
 718                &self.match_candidates,
 719                &query,
 720                true,
 721                usize::MAX,
 722                &Default::default(),
 723                executor.clone(),
 724            ));
 725
 726            let (online_contacts, offline_contacts) = matches
 727                .iter()
 728                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
 729
 730            for (matches, section) in [
 731                (online_contacts, Section::Online),
 732                (offline_contacts, Section::Offline),
 733            ] {
 734                if !matches.is_empty() {
 735                    self.entries.push(ListEntry::Header(section));
 736                    if !self.collapsed_sections.contains(&section) {
 737                        let active_call = &ActiveCall::global(cx).read(cx);
 738                        for mat in matches {
 739                            let contact = &contacts[mat.candidate_id];
 740                            self.entries.push(ListEntry::Contact {
 741                                contact: contact.clone(),
 742                                calling: active_call.pending_invites().contains(&contact.user.id),
 743                            });
 744                        }
 745                    }
 746                }
 747            }
 748        }
 749
 750        if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
 751            self.entries.push(ListEntry::ContactPlaceholder);
 752        }
 753
 754        if select_same_item {
 755            if let Some(prev_selected_entry) = prev_selected_entry {
 756                self.selection.take();
 757                for (ix, entry) in self.entries.iter().enumerate() {
 758                    if *entry == prev_selected_entry {
 759                        self.selection = Some(ix);
 760                        break;
 761                    }
 762                }
 763            }
 764        } else {
 765            self.selection = self.selection.and_then(|prev_selection| {
 766                if self.entries.is_empty() {
 767                    None
 768                } else {
 769                    Some(prev_selection.min(self.entries.len() - 1))
 770                }
 771            });
 772        }
 773
 774        let old_scroll_top = self.list_state.logical_scroll_top();
 775        self.list_state.reset(self.entries.len());
 776
 777        if scroll_to_top {
 778            self.list_state.scroll_to(ListOffset::default());
 779        } else {
 780            // Attempt to maintain the same scroll position.
 781            if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
 782                let new_scroll_top = self
 783                    .entries
 784                    .iter()
 785                    .position(|entry| entry == old_top_entry)
 786                    .map(|item_ix| ListOffset {
 787                        item_ix,
 788                        offset_in_item: old_scroll_top.offset_in_item,
 789                    })
 790                    .or_else(|| {
 791                        let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
 792                        let item_ix = self
 793                            .entries
 794                            .iter()
 795                            .position(|entry| entry == entry_after_old_top)?;
 796                        Some(ListOffset {
 797                            item_ix,
 798                            offset_in_item: Pixels::ZERO,
 799                        })
 800                    })
 801                    .or_else(|| {
 802                        let entry_before_old_top =
 803                            old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
 804                        let item_ix = self
 805                            .entries
 806                            .iter()
 807                            .position(|entry| entry == entry_before_old_top)?;
 808                        Some(ListOffset {
 809                            item_ix,
 810                            offset_in_item: Pixels::ZERO,
 811                        })
 812                    });
 813
 814                self.list_state
 815                    .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
 816            }
 817        }
 818
 819        cx.notify();
 820    }
 821
 822    fn render_call_participant(
 823        &self,
 824        user: &Arc<User>,
 825        peer_id: Option<PeerId>,
 826        is_pending: bool,
 827        role: proto::ChannelRole,
 828        is_selected: bool,
 829        cx: &mut ViewContext<Self>,
 830    ) -> ListItem {
 831        let user_id = user.id;
 832        let is_current_user =
 833            self.user_store.read(cx).current_user().map(|user| user.id) == Some(user_id);
 834        let tooltip = format!("Follow {}", user.github_login);
 835
 836        let is_call_admin = ActiveCall::global(cx).read(cx).room().is_some_and(|room| {
 837            room.read(cx).local_participant().role == proto::ChannelRole::Admin
 838        });
 839
 840        ListItem::new(SharedString::from(user.github_login.clone()))
 841            .start_slot(Avatar::new(user.avatar_uri.clone()))
 842            .child(Label::new(user.github_login.clone()))
 843            .selected(is_selected)
 844            .end_slot(if is_pending {
 845                Label::new("Calling").color(Color::Muted).into_any_element()
 846            } else if is_current_user {
 847                IconButton::new("leave-call", IconName::Exit)
 848                    .style(ButtonStyle::Subtle)
 849                    .on_click(move |_, cx| Self::leave_call(cx))
 850                    .tooltip(|cx| Tooltip::text("Leave Call", cx))
 851                    .into_any_element()
 852            } else if role == proto::ChannelRole::Guest {
 853                Label::new("Guest").color(Color::Muted).into_any_element()
 854            } else {
 855                div().into_any_element()
 856            })
 857            .when_some(peer_id, |el, peer_id| {
 858                if role == proto::ChannelRole::Guest {
 859                    return el;
 860                }
 861                el.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
 862                    .on_click(cx.listener(move |this, _, cx| {
 863                        this.workspace
 864                            .update(cx, |workspace, cx| workspace.follow(peer_id, cx))
 865                            .ok();
 866                    }))
 867            })
 868            .when(is_call_admin, |el| {
 869                el.on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| {
 870                    this.deploy_participant_context_menu(event.position, user_id, role, cx)
 871                }))
 872            })
 873    }
 874
 875    fn render_participant_project(
 876        &self,
 877        project_id: u64,
 878        worktree_root_names: &[String],
 879        host_user_id: u64,
 880        is_last: bool,
 881        is_selected: bool,
 882        cx: &mut ViewContext<Self>,
 883    ) -> impl IntoElement {
 884        let project_name: SharedString = if worktree_root_names.is_empty() {
 885            "untitled".to_string()
 886        } else {
 887            worktree_root_names.join(", ")
 888        }
 889        .into();
 890
 891        ListItem::new(project_id as usize)
 892            .selected(is_selected)
 893            .on_click(cx.listener(move |this, _, cx| {
 894                this.workspace
 895                    .update(cx, |workspace, cx| {
 896                        let app_state = workspace.app_state().clone();
 897                        workspace::join_remote_project(project_id, host_user_id, app_state, cx)
 898                            .detach_and_log_err(cx);
 899                    })
 900                    .ok();
 901            }))
 902            .start_slot(
 903                h_stack()
 904                    .gap_1()
 905                    .child(render_tree_branch(is_last, false, cx))
 906                    .child(IconButton::new(0, IconName::Folder)),
 907            )
 908            .child(Label::new(project_name.clone()))
 909            .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
 910    }
 911
 912    fn render_participant_screen(
 913        &self,
 914        peer_id: Option<PeerId>,
 915        is_last: bool,
 916        is_selected: bool,
 917        cx: &mut ViewContext<Self>,
 918    ) -> impl IntoElement {
 919        let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
 920
 921        ListItem::new(("screen", id))
 922            .selected(is_selected)
 923            .start_slot(
 924                h_stack()
 925                    .gap_1()
 926                    .child(render_tree_branch(is_last, false, cx))
 927                    .child(IconButton::new(0, IconName::Screen)),
 928            )
 929            .child(Label::new("Screen"))
 930            .when_some(peer_id, |this, _| {
 931                this.on_click(cx.listener(move |this, _, cx| {
 932                    this.workspace
 933                        .update(cx, |workspace, cx| {
 934                            workspace.open_shared_screen(peer_id.unwrap(), cx)
 935                        })
 936                        .ok();
 937                }))
 938                .tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx))
 939            })
 940    }
 941
 942    fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
 943        if let Some(_) = self.channel_editing_state.take() {
 944            self.channel_name_editor.update(cx, |editor, cx| {
 945                editor.set_text("", cx);
 946            });
 947            true
 948        } else {
 949            false
 950        }
 951    }
 952
 953    fn render_channel_notes(
 954        &self,
 955        channel_id: ChannelId,
 956        is_selected: bool,
 957        cx: &mut ViewContext<Self>,
 958    ) -> impl IntoElement {
 959        ListItem::new("channel-notes")
 960            .selected(is_selected)
 961            .on_click(cx.listener(move |this, _, cx| {
 962                this.open_channel_notes(channel_id, cx);
 963            }))
 964            .start_slot(
 965                h_stack()
 966                    .gap_1()
 967                    .child(render_tree_branch(false, true, cx))
 968                    .child(IconButton::new(0, IconName::File)),
 969            )
 970            .child(div().h_7().w_full().child(Label::new("notes")))
 971            .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
 972    }
 973
 974    fn render_channel_chat(
 975        &self,
 976        channel_id: ChannelId,
 977        is_selected: bool,
 978        cx: &mut ViewContext<Self>,
 979    ) -> impl IntoElement {
 980        ListItem::new("channel-chat")
 981            .selected(is_selected)
 982            .on_click(cx.listener(move |this, _, cx| {
 983                this.join_channel_chat(channel_id, cx);
 984            }))
 985            .start_slot(
 986                h_stack()
 987                    .gap_1()
 988                    .child(render_tree_branch(false, false, cx))
 989                    .child(IconButton::new(0, IconName::MessageBubbles)),
 990            )
 991            .child(Label::new("chat"))
 992            .tooltip(move |cx| Tooltip::text("Open Chat", cx))
 993    }
 994
 995    fn has_subchannels(&self, ix: usize) -> bool {
 996        self.entries.get(ix).map_or(false, |entry| {
 997            if let ListEntry::Channel { has_children, .. } = entry {
 998                *has_children
 999            } else {
1000                false
1001            }
1002        })
1003    }
1004
1005    fn deploy_participant_context_menu(
1006        &mut self,
1007        position: Point<Pixels>,
1008        user_id: u64,
1009        role: proto::ChannelRole,
1010        cx: &mut ViewContext<Self>,
1011    ) {
1012        let this = cx.view().clone();
1013        if !(role == proto::ChannelRole::Guest || role == proto::ChannelRole::Member) {
1014            return;
1015        }
1016
1017        let context_menu = ContextMenu::build(cx, |context_menu, cx| {
1018            if role == proto::ChannelRole::Guest {
1019                context_menu.entry(
1020                    "Grant Write Access",
1021                    None,
1022                    cx.handler_for(&this, move |_, cx| {
1023                        ActiveCall::global(cx)
1024                            .update(cx, |call, cx| {
1025                                let Some(room) = call.room() else {
1026                                    return Task::ready(Ok(()));
1027                                };
1028                                room.update(cx, |room, cx| {
1029                                    room.set_participant_role(
1030                                        user_id,
1031                                        proto::ChannelRole::Member,
1032                                        cx,
1033                                    )
1034                                })
1035                            })
1036                            .detach_and_notify_err(cx)
1037                    }),
1038                )
1039            } else if role == proto::ChannelRole::Member {
1040                context_menu.entry(
1041                    "Revoke Write Access",
1042                    None,
1043                    cx.handler_for(&this, move |_, cx| {
1044                        ActiveCall::global(cx)
1045                            .update(cx, |call, cx| {
1046                                let Some(room) = call.room() else {
1047                                    return Task::ready(Ok(()));
1048                                };
1049                                room.update(cx, |room, cx| {
1050                                    room.set_participant_role(
1051                                        user_id,
1052                                        proto::ChannelRole::Guest,
1053                                        cx,
1054                                    )
1055                                })
1056                            })
1057                            .detach_and_notify_err(cx)
1058                    }),
1059                )
1060            } else {
1061                unreachable!()
1062            }
1063        });
1064
1065        cx.focus_view(&context_menu);
1066        let subscription =
1067            cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
1068                if this.context_menu.as_ref().is_some_and(|context_menu| {
1069                    context_menu.0.focus_handle(cx).contains_focused(cx)
1070                }) {
1071                    cx.focus_self();
1072                }
1073                this.context_menu.take();
1074                cx.notify();
1075            });
1076        self.context_menu = Some((context_menu, position, subscription));
1077    }
1078
1079    fn deploy_channel_context_menu(
1080        &mut self,
1081        position: Point<Pixels>,
1082        channel_id: ChannelId,
1083        ix: usize,
1084        cx: &mut ViewContext<Self>,
1085    ) {
1086        let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
1087            self.channel_store
1088                .read(cx)
1089                .channel_for_id(clipboard.channel_id)
1090                .map(|channel| channel.name.clone())
1091        });
1092        let this = cx.view().clone();
1093
1094        let context_menu = ContextMenu::build(cx, |mut context_menu, cx| {
1095            if self.has_subchannels(ix) {
1096                let expand_action_name = if self.is_channel_collapsed(channel_id) {
1097                    "Expand Subchannels"
1098                } else {
1099                    "Collapse Subchannels"
1100                };
1101                context_menu = context_menu.entry(
1102                    expand_action_name,
1103                    None,
1104                    cx.handler_for(&this, move |this, cx| {
1105                        this.toggle_channel_collapsed(channel_id, cx)
1106                    }),
1107                );
1108            }
1109
1110            context_menu = context_menu
1111                .entry(
1112                    "Open Notes",
1113                    None,
1114                    cx.handler_for(&this, move |this, cx| {
1115                        this.open_channel_notes(channel_id, cx)
1116                    }),
1117                )
1118                .entry(
1119                    "Open Chat",
1120                    None,
1121                    cx.handler_for(&this, move |this, cx| {
1122                        this.join_channel_chat(channel_id, cx)
1123                    }),
1124                )
1125                .entry(
1126                    "Copy Channel Link",
1127                    None,
1128                    cx.handler_for(&this, move |this, cx| {
1129                        this.copy_channel_link(channel_id, cx)
1130                    }),
1131                );
1132
1133            if self.channel_store.read(cx).is_channel_admin(channel_id) {
1134                context_menu = context_menu
1135                    .separator()
1136                    .entry(
1137                        "New Subchannel",
1138                        None,
1139                        cx.handler_for(&this, move |this, cx| this.new_subchannel(channel_id, cx)),
1140                    )
1141                    .entry(
1142                        "Rename",
1143                        None,
1144                        cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)),
1145                    )
1146                    .entry(
1147                        "Move this channel",
1148                        None,
1149                        cx.handler_for(&this, move |this, cx| {
1150                            this.start_move_channel(channel_id, cx)
1151                        }),
1152                    );
1153
1154                if let Some(channel_name) = clipboard_channel_name {
1155                    context_menu = context_menu.separator().entry(
1156                        format!("Move '#{}' here", channel_name),
1157                        None,
1158                        cx.handler_for(&this, move |this, cx| {
1159                            this.move_channel_on_clipboard(channel_id, cx)
1160                        }),
1161                    );
1162                }
1163
1164                context_menu = context_menu
1165                    .separator()
1166                    .entry(
1167                        "Invite Members",
1168                        None,
1169                        cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)),
1170                    )
1171                    .entry(
1172                        "Manage Members",
1173                        None,
1174                        cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
1175                    )
1176                    .entry(
1177                        "Delete",
1178                        None,
1179                        cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
1180                    );
1181            }
1182
1183            context_menu
1184        });
1185
1186        cx.focus_view(&context_menu);
1187        let subscription =
1188            cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
1189                if this.context_menu.as_ref().is_some_and(|context_menu| {
1190                    context_menu.0.focus_handle(cx).contains_focused(cx)
1191                }) {
1192                    cx.focus_self();
1193                }
1194                this.context_menu.take();
1195                cx.notify();
1196            });
1197        self.context_menu = Some((context_menu, position, subscription));
1198
1199        cx.notify();
1200    }
1201
1202    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
1203        if self.take_editing_state(cx) {
1204            cx.focus_view(&self.filter_editor);
1205        } else {
1206            self.filter_editor.update(cx, |editor, cx| {
1207                if editor.buffer().read(cx).len(cx) > 0 {
1208                    editor.set_text("", cx);
1209                }
1210            });
1211        }
1212
1213        self.update_entries(false, cx);
1214    }
1215
1216    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
1217        let ix = self.selection.map_or(0, |ix| ix + 1);
1218        if ix < self.entries.len() {
1219            self.selection = Some(ix);
1220        }
1221
1222        if let Some(ix) = self.selection {
1223            self.scroll_to_item(ix)
1224        }
1225        cx.notify();
1226    }
1227
1228    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
1229        let ix = self.selection.take().unwrap_or(0);
1230        if ix > 0 {
1231            self.selection = Some(ix - 1);
1232        }
1233
1234        if let Some(ix) = self.selection {
1235            self.scroll_to_item(ix)
1236        }
1237        cx.notify();
1238    }
1239
1240    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
1241        if self.confirm_channel_edit(cx) {
1242            return;
1243        }
1244
1245        if let Some(selection) = self.selection {
1246            if let Some(entry) = self.entries.get(selection) {
1247                match entry {
1248                    ListEntry::Header(section) => match section {
1249                        Section::ActiveCall => Self::leave_call(cx),
1250                        Section::Channels => self.new_root_channel(cx),
1251                        Section::Contacts => self.toggle_contact_finder(cx),
1252                        Section::ContactRequests
1253                        | Section::Online
1254                        | Section::Offline
1255                        | Section::ChannelInvites => {
1256                            self.toggle_section_expanded(*section, cx);
1257                        }
1258                    },
1259                    ListEntry::Contact { contact, calling } => {
1260                        if contact.online && !contact.busy && !calling {
1261                            self.call(contact.user.id, cx);
1262                        }
1263                    }
1264                    ListEntry::ParticipantProject {
1265                        project_id,
1266                        host_user_id,
1267                        ..
1268                    } => {
1269                        if let Some(workspace) = self.workspace.upgrade() {
1270                            let app_state = workspace.read(cx).app_state().clone();
1271                            workspace::join_remote_project(
1272                                *project_id,
1273                                *host_user_id,
1274                                app_state,
1275                                cx,
1276                            )
1277                            .detach_and_log_err(cx);
1278                        }
1279                    }
1280                    ListEntry::ParticipantScreen { peer_id, .. } => {
1281                        let Some(peer_id) = peer_id else {
1282                            return;
1283                        };
1284                        if let Some(workspace) = self.workspace.upgrade() {
1285                            workspace.update(cx, |workspace, cx| {
1286                                workspace.open_shared_screen(*peer_id, cx)
1287                            });
1288                        }
1289                    }
1290                    ListEntry::Channel { channel, .. } => {
1291                        let is_active = maybe!({
1292                            let call_channel = ActiveCall::global(cx)
1293                                .read(cx)
1294                                .room()?
1295                                .read(cx)
1296                                .channel_id()?;
1297
1298                            Some(call_channel == channel.id)
1299                        })
1300                        .unwrap_or(false);
1301                        if is_active {
1302                            self.open_channel_notes(channel.id, cx)
1303                        } else {
1304                            self.join_channel(channel.id, cx)
1305                        }
1306                    }
1307                    ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
1308                    ListEntry::CallParticipant { user, peer_id, .. } => {
1309                        if Some(user) == self.user_store.read(cx).current_user().as_ref() {
1310                            Self::leave_call(cx);
1311                        } else if let Some(peer_id) = peer_id {
1312                            self.workspace
1313                                .update(cx, |workspace, cx| workspace.follow(*peer_id, cx))
1314                                .ok();
1315                        }
1316                    }
1317                    ListEntry::IncomingRequest(user) => {
1318                        self.respond_to_contact_request(user.id, true, cx)
1319                    }
1320                    ListEntry::ChannelInvite(channel) => {
1321                        self.respond_to_channel_invite(channel.id, true, cx)
1322                    }
1323                    ListEntry::ChannelNotes { channel_id } => {
1324                        self.open_channel_notes(*channel_id, cx)
1325                    }
1326                    ListEntry::ChannelChat { channel_id } => {
1327                        self.join_channel_chat(*channel_id, cx)
1328                    }
1329
1330                    ListEntry::OutgoingRequest(_) => {}
1331                    ListEntry::ChannelEditor { .. } => {}
1332                }
1333            }
1334        }
1335    }
1336
1337    fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
1338        if self.channel_editing_state.is_some() {
1339            self.channel_name_editor.update(cx, |editor, cx| {
1340                editor.insert(" ", cx);
1341            });
1342        }
1343    }
1344
1345    fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
1346        if let Some(editing_state) = &mut self.channel_editing_state {
1347            match editing_state {
1348                ChannelEditingState::Create {
1349                    location,
1350                    pending_name,
1351                    ..
1352                } => {
1353                    if pending_name.is_some() {
1354                        return false;
1355                    }
1356                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1357
1358                    *pending_name = Some(channel_name.clone());
1359
1360                    self.channel_store
1361                        .update(cx, |channel_store, cx| {
1362                            channel_store.create_channel(&channel_name, *location, cx)
1363                        })
1364                        .detach();
1365                    cx.notify();
1366                }
1367                ChannelEditingState::Rename {
1368                    location,
1369                    pending_name,
1370                } => {
1371                    if pending_name.is_some() {
1372                        return false;
1373                    }
1374                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1375                    *pending_name = Some(channel_name.clone());
1376
1377                    self.channel_store
1378                        .update(cx, |channel_store, cx| {
1379                            channel_store.rename(*location, &channel_name, cx)
1380                        })
1381                        .detach();
1382                    cx.notify();
1383                }
1384            }
1385            cx.focus_self();
1386            true
1387        } else {
1388            false
1389        }
1390    }
1391
1392    fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
1393        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
1394            self.collapsed_sections.remove(ix);
1395        } else {
1396            self.collapsed_sections.push(section);
1397        }
1398        self.update_entries(false, cx);
1399    }
1400
1401    fn collapse_selected_channel(
1402        &mut self,
1403        _: &CollapseSelectedChannel,
1404        cx: &mut ViewContext<Self>,
1405    ) {
1406        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
1407            return;
1408        };
1409
1410        if self.is_channel_collapsed(channel_id) {
1411            return;
1412        }
1413
1414        self.toggle_channel_collapsed(channel_id, cx);
1415    }
1416
1417    fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
1418        let Some(id) = self.selected_channel().map(|channel| channel.id) else {
1419            return;
1420        };
1421
1422        if !self.is_channel_collapsed(id) {
1423            return;
1424        }
1425
1426        self.toggle_channel_collapsed(id, cx)
1427    }
1428
1429    //     fn toggle_channel_collapsed_action(
1430    //         &mut self,
1431    //         action: &ToggleCollapse,
1432    //         cx: &mut ViewContext<Self>,
1433    //     ) {
1434    //         self.toggle_channel_collapsed(action.location, cx);
1435    //     }
1436
1437    fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1438        match self.collapsed_channels.binary_search(&channel_id) {
1439            Ok(ix) => {
1440                self.collapsed_channels.remove(ix);
1441            }
1442            Err(ix) => {
1443                self.collapsed_channels.insert(ix, channel_id);
1444            }
1445        };
1446        self.serialize(cx);
1447        self.update_entries(true, cx);
1448        cx.notify();
1449        cx.focus_self();
1450    }
1451
1452    fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
1453        self.collapsed_channels.binary_search(&channel_id).is_ok()
1454    }
1455
1456    fn leave_call(cx: &mut WindowContext) {
1457        ActiveCall::global(cx)
1458            .update(cx, |call, cx| call.hang_up(cx))
1459            .detach_and_log_err(cx);
1460    }
1461
1462    fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
1463        if let Some(workspace) = self.workspace.upgrade() {
1464            workspace.update(cx, |workspace, cx| {
1465                workspace.toggle_modal(cx, |cx| {
1466                    let mut finder = ContactFinder::new(self.user_store.clone(), cx);
1467                    finder.set_query(self.filter_editor.read(cx).text(cx), cx);
1468                    finder
1469                });
1470            });
1471        }
1472    }
1473
1474    fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
1475        self.channel_editing_state = Some(ChannelEditingState::Create {
1476            location: None,
1477            pending_name: None,
1478        });
1479        self.update_entries(false, cx);
1480        self.select_channel_editor();
1481        cx.focus_view(&self.channel_name_editor);
1482        cx.notify();
1483    }
1484
1485    fn select_channel_editor(&mut self) {
1486        self.selection = self.entries.iter().position(|entry| match entry {
1487            ListEntry::ChannelEditor { .. } => true,
1488            _ => false,
1489        });
1490    }
1491
1492    fn new_subchannel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1493        self.collapsed_channels
1494            .retain(|channel| *channel != channel_id);
1495        self.channel_editing_state = Some(ChannelEditingState::Create {
1496            location: Some(channel_id),
1497            pending_name: None,
1498        });
1499        self.update_entries(false, cx);
1500        self.select_channel_editor();
1501        cx.focus_view(&self.channel_name_editor);
1502        cx.notify();
1503    }
1504
1505    fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1506        self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
1507    }
1508
1509    fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1510        self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
1511    }
1512
1513    fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
1514        if let Some(channel) = self.selected_channel() {
1515            self.remove_channel(channel.id, cx)
1516        }
1517    }
1518
1519    fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
1520        if let Some(channel) = self.selected_channel() {
1521            self.rename_channel(channel.id, cx);
1522        }
1523    }
1524
1525    fn rename_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1526        let channel_store = self.channel_store.read(cx);
1527        if !channel_store.is_channel_admin(channel_id) {
1528            return;
1529        }
1530        if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() {
1531            self.channel_editing_state = Some(ChannelEditingState::Rename {
1532                location: channel_id,
1533                pending_name: None,
1534            });
1535            self.channel_name_editor.update(cx, |editor, cx| {
1536                editor.set_text(channel.name.clone(), cx);
1537                editor.select_all(&Default::default(), cx);
1538            });
1539            cx.focus_view(&self.channel_name_editor);
1540            self.update_entries(false, cx);
1541            self.select_channel_editor();
1542        }
1543    }
1544
1545    fn start_move_channel(&mut self, channel_id: ChannelId, _cx: &mut ViewContext<Self>) {
1546        self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
1547    }
1548
1549    fn start_move_selected_channel(&mut self, _: &StartMoveChannel, cx: &mut ViewContext<Self>) {
1550        if let Some(channel) = self.selected_channel() {
1551            self.start_move_channel(channel.id, cx);
1552        }
1553    }
1554
1555    fn move_channel_on_clipboard(
1556        &mut self,
1557        to_channel_id: ChannelId,
1558        cx: &mut ViewContext<CollabPanel>,
1559    ) {
1560        if let Some(clipboard) = self.channel_clipboard.take() {
1561            self.channel_store.update(cx, |channel_store, cx| {
1562                channel_store
1563                    .move_channel(clipboard.channel_id, Some(to_channel_id), cx)
1564                    .detach_and_log_err(cx)
1565            })
1566        }
1567    }
1568
1569    fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1570        if let Some(workspace) = self.workspace.upgrade() {
1571            ChannelView::open(channel_id, workspace, cx).detach();
1572        }
1573    }
1574
1575    fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
1576        let Some(channel) = self.selected_channel() else {
1577            return;
1578        };
1579        let Some(bounds) = self
1580            .selection
1581            .and_then(|ix| self.list_state.bounds_for_item(ix))
1582        else {
1583            return;
1584        };
1585
1586        self.deploy_channel_context_menu(bounds.center(), channel.id, self.selection.unwrap(), cx);
1587        cx.stop_propagation();
1588    }
1589
1590    fn selected_channel(&self) -> Option<&Arc<Channel>> {
1591        self.selection
1592            .and_then(|ix| self.entries.get(ix))
1593            .and_then(|entry| match entry {
1594                ListEntry::Channel { channel, .. } => Some(channel),
1595                _ => None,
1596            })
1597    }
1598
1599    fn show_channel_modal(
1600        &mut self,
1601        channel_id: ChannelId,
1602        mode: channel_modal::Mode,
1603        cx: &mut ViewContext<Self>,
1604    ) {
1605        let workspace = self.workspace.clone();
1606        let user_store = self.user_store.clone();
1607        let channel_store = self.channel_store.clone();
1608        let members = self.channel_store.update(cx, |channel_store, cx| {
1609            channel_store.get_channel_member_details(channel_id, cx)
1610        });
1611
1612        cx.spawn(|_, mut cx| async move {
1613            let members = members.await?;
1614            workspace.update(&mut cx, |workspace, cx| {
1615                workspace.toggle_modal(cx, |cx| {
1616                    ChannelModal::new(
1617                        user_store.clone(),
1618                        channel_store.clone(),
1619                        channel_id,
1620                        mode,
1621                        members,
1622                        cx,
1623                    )
1624                });
1625            })
1626        })
1627        .detach();
1628    }
1629
1630    fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1631        let channel_store = self.channel_store.clone();
1632        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
1633            let prompt_message = format!(
1634                "Are you sure you want to remove the channel \"{}\"?",
1635                channel.name
1636            );
1637            let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
1638            cx.spawn(|this, mut cx| async move {
1639                if answer.await? == 0 {
1640                    channel_store
1641                        .update(&mut cx, |channels, _| channels.remove_channel(channel_id))?
1642                        .await
1643                        .notify_async_err(&mut cx);
1644                    this.update(&mut cx, |_, cx| cx.focus_self()).ok();
1645                }
1646                anyhow::Ok(())
1647            })
1648            .detach();
1649        }
1650    }
1651
1652    fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
1653        let user_store = self.user_store.clone();
1654        let prompt_message = format!(
1655            "Are you sure you want to remove \"{}\" from your contacts?",
1656            github_login
1657        );
1658        let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
1659        cx.spawn(|_, mut cx| async move {
1660            if answer.await? == 0 {
1661                user_store
1662                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))?
1663                    .await
1664                    .notify_async_err(&mut cx);
1665            }
1666            anyhow::Ok(())
1667        })
1668        .detach_and_log_err(cx);
1669    }
1670
1671    fn respond_to_contact_request(
1672        &mut self,
1673        user_id: u64,
1674        accept: bool,
1675        cx: &mut ViewContext<Self>,
1676    ) {
1677        self.user_store
1678            .update(cx, |store, cx| {
1679                store.respond_to_contact_request(user_id, accept, cx)
1680            })
1681            .detach_and_log_err(cx);
1682    }
1683
1684    fn respond_to_channel_invite(
1685        &mut self,
1686        channel_id: u64,
1687        accept: bool,
1688        cx: &mut ViewContext<Self>,
1689    ) {
1690        self.channel_store
1691            .update(cx, |store, cx| {
1692                store.respond_to_channel_invite(channel_id, accept, cx)
1693            })
1694            .detach();
1695    }
1696
1697    fn call(&mut self, recipient_user_id: u64, cx: &mut ViewContext<Self>) {
1698        ActiveCall::global(cx)
1699            .update(cx, |call, cx| {
1700                call.invite(recipient_user_id, Some(self.project.clone()), cx)
1701            })
1702            .detach_and_log_err(cx);
1703    }
1704
1705    fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
1706        let Some(workspace) = self.workspace.upgrade() else {
1707            return;
1708        };
1709        let Some(handle) = cx.window_handle().downcast::<Workspace>() else {
1710            return;
1711        };
1712        workspace::join_channel(
1713            channel_id,
1714            workspace.read(cx).app_state().clone(),
1715            Some(handle),
1716            cx,
1717        )
1718        .detach_and_log_err(cx)
1719    }
1720
1721    fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1722        let Some(workspace) = self.workspace.upgrade() else {
1723            return;
1724        };
1725        cx.window_context().defer(move |cx| {
1726            workspace.update(cx, |workspace, cx| {
1727                if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
1728                    panel.update(cx, |panel, cx| {
1729                        panel
1730                            .select_channel(channel_id, None, cx)
1731                            .detach_and_log_err(cx);
1732                    });
1733                }
1734            });
1735        });
1736    }
1737
1738    fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1739        let channel_store = self.channel_store.read(cx);
1740        let Some(channel) = channel_store.channel_for_id(channel_id) else {
1741            return;
1742        };
1743        let item = ClipboardItem::new(channel.link());
1744        cx.write_to_clipboard(item)
1745    }
1746
1747    fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div {
1748        let collab_blurb = "Work with your team in realtime with collaborative editing, voice, shared notes and more.";
1749
1750        v_stack()
1751            .gap_6()
1752            .p_4()
1753            .child(Label::new(collab_blurb))
1754            .child(
1755                v_stack()
1756                    .gap_2()
1757                    .child(
1758                        Button::new("sign_in", "Sign in")
1759                            .icon_color(Color::Muted)
1760                            .icon(IconName::Github)
1761                            .icon_position(IconPosition::Start)
1762                            .style(ButtonStyle::Filled)
1763                            .full_width()
1764                            .on_click(cx.listener(|this, _, cx| {
1765                                let client = this.client.clone();
1766                                cx.spawn(|_, mut cx| async move {
1767                                    client
1768                                        .authenticate_and_connect(true, &cx)
1769                                        .await
1770                                        .notify_async_err(&mut cx);
1771                                })
1772                                .detach()
1773                            })),
1774                    )
1775                    .child(
1776                        div().flex().w_full().items_center().child(
1777                            Label::new("Sign in to enable collaboration.")
1778                                .color(Color::Muted)
1779                                .size(LabelSize::Small),
1780                        ),
1781                    ),
1782            )
1783    }
1784
1785    fn render_list_entry(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
1786        let entry = &self.entries[ix];
1787
1788        let is_selected = self.selection == Some(ix);
1789        match entry {
1790            ListEntry::Header(section) => {
1791                let is_collapsed = self.collapsed_sections.contains(section);
1792                self.render_header(*section, is_selected, is_collapsed, cx)
1793                    .into_any_element()
1794            }
1795            ListEntry::Contact { contact, calling } => self
1796                .render_contact(contact, *calling, is_selected, cx)
1797                .into_any_element(),
1798            ListEntry::ContactPlaceholder => self
1799                .render_contact_placeholder(is_selected, cx)
1800                .into_any_element(),
1801            ListEntry::IncomingRequest(user) => self
1802                .render_contact_request(user, true, is_selected, cx)
1803                .into_any_element(),
1804            ListEntry::OutgoingRequest(user) => self
1805                .render_contact_request(user, false, is_selected, cx)
1806                .into_any_element(),
1807            ListEntry::Channel {
1808                channel,
1809                depth,
1810                has_children,
1811            } => self
1812                .render_channel(channel, *depth, *has_children, is_selected, ix, cx)
1813                .into_any_element(),
1814            ListEntry::ChannelEditor { depth } => {
1815                self.render_channel_editor(*depth, cx).into_any_element()
1816            }
1817            ListEntry::ChannelInvite(channel) => self
1818                .render_channel_invite(channel, is_selected, cx)
1819                .into_any_element(),
1820            ListEntry::CallParticipant {
1821                user,
1822                peer_id,
1823                is_pending,
1824                role,
1825            } => self
1826                .render_call_participant(user, *peer_id, *is_pending, *role, is_selected, cx)
1827                .into_any_element(),
1828            ListEntry::ParticipantProject {
1829                project_id,
1830                worktree_root_names,
1831                host_user_id,
1832                is_last,
1833            } => self
1834                .render_participant_project(
1835                    *project_id,
1836                    &worktree_root_names,
1837                    *host_user_id,
1838                    *is_last,
1839                    is_selected,
1840                    cx,
1841                )
1842                .into_any_element(),
1843            ListEntry::ParticipantScreen { peer_id, is_last } => self
1844                .render_participant_screen(*peer_id, *is_last, is_selected, cx)
1845                .into_any_element(),
1846            ListEntry::ChannelNotes { channel_id } => self
1847                .render_channel_notes(*channel_id, is_selected, cx)
1848                .into_any_element(),
1849            ListEntry::ChannelChat { channel_id } => self
1850                .render_channel_chat(*channel_id, is_selected, cx)
1851                .into_any_element(),
1852        }
1853    }
1854
1855    fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
1856        v_stack()
1857            .size_full()
1858            .child(list(self.list_state.clone()).full())
1859            .child(
1860                v_stack()
1861                    .child(div().mx_2().border_primary(cx).border_t())
1862                    .child(
1863                        v_stack()
1864                            .p_2()
1865                            .child(self.render_filter_input(&self.filter_editor, cx)),
1866                    ),
1867            )
1868    }
1869
1870    fn render_filter_input(
1871        &self,
1872        editor: &View<Editor>,
1873        cx: &mut ViewContext<Self>,
1874    ) -> impl IntoElement {
1875        let settings = ThemeSettings::get_global(cx);
1876        let text_style = TextStyle {
1877            color: if editor.read(cx).read_only(cx) {
1878                cx.theme().colors().text_disabled
1879            } else {
1880                cx.theme().colors().text
1881            },
1882            font_family: settings.ui_font.family.clone(),
1883            font_features: settings.ui_font.features,
1884            font_size: rems(0.875).into(),
1885            font_weight: FontWeight::NORMAL,
1886            font_style: FontStyle::Normal,
1887            line_height: relative(1.3).into(),
1888            background_color: None,
1889            underline: None,
1890            white_space: WhiteSpace::Normal,
1891        };
1892
1893        EditorElement::new(
1894            editor,
1895            EditorStyle {
1896                local_player: cx.theme().players().local(),
1897                text: text_style,
1898                ..Default::default()
1899            },
1900        )
1901    }
1902
1903    fn render_header(
1904        &self,
1905        section: Section,
1906        is_selected: bool,
1907        is_collapsed: bool,
1908        cx: &ViewContext<Self>,
1909    ) -> impl IntoElement {
1910        let mut channel_link = None;
1911        let mut channel_tooltip_text = None;
1912        let mut channel_icon = None;
1913        // let mut is_dragged_over = false;
1914
1915        let text = match section {
1916            Section::ActiveCall => {
1917                let channel_name = maybe!({
1918                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
1919
1920                    let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
1921
1922                    channel_link = Some(channel.link());
1923                    (channel_icon, channel_tooltip_text) = match channel.visibility {
1924                        proto::ChannelVisibility::Public => {
1925                            (Some("icons/public.svg"), Some("Copy public channel link."))
1926                        }
1927                        proto::ChannelVisibility::Members => {
1928                            (Some("icons/hash.svg"), Some("Copy private channel link."))
1929                        }
1930                    };
1931
1932                    Some(channel.name.as_ref())
1933                });
1934
1935                if let Some(name) = channel_name {
1936                    SharedString::from(format!("{}", name))
1937                } else {
1938                    SharedString::from("Current Call")
1939                }
1940            }
1941            Section::ContactRequests => SharedString::from("Requests"),
1942            Section::Contacts => SharedString::from("Contacts"),
1943            Section::Channels => SharedString::from("Channels"),
1944            Section::ChannelInvites => SharedString::from("Invites"),
1945            Section::Online => SharedString::from("Online"),
1946            Section::Offline => SharedString::from("Offline"),
1947        };
1948
1949        let button = match section {
1950            Section::ActiveCall => channel_link.map(|channel_link| {
1951                let channel_link_copy = channel_link.clone();
1952                IconButton::new("channel-link", IconName::Copy)
1953                    .icon_size(IconSize::Small)
1954                    .size(ButtonSize::None)
1955                    .visible_on_hover("section-header")
1956                    .on_click(move |_, cx| {
1957                        let item = ClipboardItem::new(channel_link_copy.clone());
1958                        cx.write_to_clipboard(item)
1959                    })
1960                    .tooltip(|cx| Tooltip::text("Copy channel link", cx))
1961                    .into_any_element()
1962            }),
1963            Section::Contacts => Some(
1964                IconButton::new("add-contact", IconName::Plus)
1965                    .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
1966                    .tooltip(|cx| Tooltip::text("Search for new contact", cx))
1967                    .into_any_element(),
1968            ),
1969            Section::Channels => Some(
1970                IconButton::new("add-channel", IconName::Plus)
1971                    .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
1972                    .tooltip(|cx| Tooltip::text("Create a channel", cx))
1973                    .into_any_element(),
1974            ),
1975            _ => None,
1976        };
1977
1978        let can_collapse = match section {
1979            Section::ActiveCall | Section::Channels | Section::Contacts => false,
1980            Section::ChannelInvites
1981            | Section::ContactRequests
1982            | Section::Online
1983            | Section::Offline => true,
1984        };
1985
1986        h_stack()
1987            .w_full()
1988            .group("section-header")
1989            .child(
1990                ListHeader::new(text)
1991                    .when(can_collapse, |header| {
1992                        header.toggle(Some(!is_collapsed)).on_toggle(cx.listener(
1993                            move |this, _, cx| {
1994                                this.toggle_section_expanded(section, cx);
1995                            },
1996                        ))
1997                    })
1998                    .inset(true)
1999                    .end_slot::<AnyElement>(button)
2000                    .selected(is_selected),
2001            )
2002            .when(section == Section::Channels, |el| {
2003                el.drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
2004                    .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
2005                        this.channel_store
2006                            .update(cx, |channel_store, cx| {
2007                                channel_store.move_channel(dragged_channel.id, None, cx)
2008                            })
2009                            .detach_and_log_err(cx)
2010                    }))
2011            })
2012    }
2013
2014    fn render_contact(
2015        &self,
2016        contact: &Contact,
2017        calling: bool,
2018        is_selected: bool,
2019        cx: &mut ViewContext<Self>,
2020    ) -> impl IntoElement {
2021        let online = contact.online;
2022        let busy = contact.busy || calling;
2023        let user_id = contact.user.id;
2024        let github_login = SharedString::from(contact.user.github_login.clone());
2025        let item =
2026            ListItem::new(github_login.clone())
2027                .indent_level(1)
2028                .indent_step_size(px(20.))
2029                .selected(is_selected)
2030                .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
2031                .child(
2032                    h_stack()
2033                        .w_full()
2034                        .justify_between()
2035                        .child(Label::new(github_login.clone()))
2036                        .when(calling, |el| {
2037                            el.child(Label::new("Calling").color(Color::Muted))
2038                        })
2039                        .when(!calling, |el| {
2040                            el.child(
2041                                IconButton::new("remove_contact", IconName::Close)
2042                                    .icon_color(Color::Muted)
2043                                    .visible_on_hover("")
2044                                    .tooltip(|cx| Tooltip::text("Remove Contact", cx))
2045                                    .on_click(cx.listener({
2046                                        let github_login = github_login.clone();
2047                                        move |this, _, cx| {
2048                                            this.remove_contact(user_id, &github_login, cx);
2049                                        }
2050                                    })),
2051                            )
2052                        }),
2053                )
2054                .start_slot(
2055                    // todo!() handle contacts with no avatar
2056                    Avatar::new(contact.user.avatar_uri.clone())
2057                        .availability_indicator(if online { Some(!busy) } else { None }),
2058                )
2059                .when(online && !busy, |el| {
2060                    el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
2061                });
2062
2063        div()
2064            .id(github_login.clone())
2065            .group("")
2066            .child(item)
2067            .tooltip(move |cx| {
2068                let text = if !online {
2069                    format!(" {} is offline", &github_login)
2070                } else if busy {
2071                    format!(" {} is on a call", &github_login)
2072                } else {
2073                    let room = ActiveCall::global(cx).read(cx).room();
2074                    if room.is_some() {
2075                        format!("Invite {} to join call", &github_login)
2076                    } else {
2077                        format!("Call {}", &github_login)
2078                    }
2079                };
2080                Tooltip::text(text, cx)
2081            })
2082    }
2083
2084    fn render_contact_request(
2085        &self,
2086        user: &Arc<User>,
2087        is_incoming: bool,
2088        is_selected: bool,
2089        cx: &mut ViewContext<Self>,
2090    ) -> impl IntoElement {
2091        let github_login = SharedString::from(user.github_login.clone());
2092        let user_id = user.id;
2093        let is_response_pending = self.user_store.read(cx).is_contact_request_pending(&user);
2094        let color = if is_response_pending {
2095            Color::Muted
2096        } else {
2097            Color::Default
2098        };
2099
2100        let controls = if is_incoming {
2101            vec![
2102                IconButton::new("decline-contact", IconName::Close)
2103                    .on_click(cx.listener(move |this, _, cx| {
2104                        this.respond_to_contact_request(user_id, false, cx);
2105                    }))
2106                    .icon_color(color)
2107                    .tooltip(|cx| Tooltip::text("Decline invite", cx)),
2108                IconButton::new("accept-contact", IconName::Check)
2109                    .on_click(cx.listener(move |this, _, cx| {
2110                        this.respond_to_contact_request(user_id, true, cx);
2111                    }))
2112                    .icon_color(color)
2113                    .tooltip(|cx| Tooltip::text("Accept invite", cx)),
2114            ]
2115        } else {
2116            let github_login = github_login.clone();
2117            vec![IconButton::new("remove_contact", IconName::Close)
2118                .on_click(cx.listener(move |this, _, cx| {
2119                    this.remove_contact(user_id, &github_login, cx);
2120                }))
2121                .icon_color(color)
2122                .tooltip(|cx| Tooltip::text("Cancel invite", cx))]
2123        };
2124
2125        ListItem::new(github_login.clone())
2126            .indent_level(1)
2127            .indent_step_size(px(20.))
2128            .selected(is_selected)
2129            .child(
2130                h_stack()
2131                    .w_full()
2132                    .justify_between()
2133                    .child(Label::new(github_login.clone()))
2134                    .child(h_stack().children(controls)),
2135            )
2136            .start_slot(Avatar::new(user.avatar_uri.clone()))
2137    }
2138
2139    fn render_channel_invite(
2140        &self,
2141        channel: &Arc<Channel>,
2142        is_selected: bool,
2143        cx: &mut ViewContext<Self>,
2144    ) -> ListItem {
2145        let channel_id = channel.id;
2146        let response_is_pending = self
2147            .channel_store
2148            .read(cx)
2149            .has_pending_channel_invite_response(&channel);
2150        let color = if response_is_pending {
2151            Color::Muted
2152        } else {
2153            Color::Default
2154        };
2155
2156        let controls = [
2157            IconButton::new("reject-invite", IconName::Close)
2158                .on_click(cx.listener(move |this, _, cx| {
2159                    this.respond_to_channel_invite(channel_id, false, cx);
2160                }))
2161                .icon_color(color)
2162                .tooltip(|cx| Tooltip::text("Decline invite", cx)),
2163            IconButton::new("accept-invite", IconName::Check)
2164                .on_click(cx.listener(move |this, _, cx| {
2165                    this.respond_to_channel_invite(channel_id, true, cx);
2166                }))
2167                .icon_color(color)
2168                .tooltip(|cx| Tooltip::text("Accept invite", cx)),
2169        ];
2170
2171        ListItem::new(("channel-invite", channel.id as usize))
2172            .selected(is_selected)
2173            .child(
2174                h_stack()
2175                    .w_full()
2176                    .justify_between()
2177                    .child(Label::new(channel.name.clone()))
2178                    .child(h_stack().children(controls)),
2179            )
2180            .start_slot(
2181                Icon::new(IconName::Hash)
2182                    .size(IconSize::Small)
2183                    .color(Color::Muted),
2184            )
2185    }
2186
2187    fn render_contact_placeholder(
2188        &self,
2189        is_selected: bool,
2190        cx: &mut ViewContext<Self>,
2191    ) -> ListItem {
2192        ListItem::new("contact-placeholder")
2193            .child(Icon::new(IconName::Plus))
2194            .child(Label::new("Add a Contact"))
2195            .selected(is_selected)
2196            .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
2197    }
2198
2199    fn render_channel(
2200        &self,
2201        channel: &Channel,
2202        depth: usize,
2203        has_children: bool,
2204        is_selected: bool,
2205        ix: usize,
2206        cx: &mut ViewContext<Self>,
2207    ) -> impl IntoElement {
2208        let channel_id = channel.id;
2209
2210        let is_active = maybe!({
2211            let call_channel = ActiveCall::global(cx)
2212                .read(cx)
2213                .room()?
2214                .read(cx)
2215                .channel_id()?;
2216            Some(call_channel == channel_id)
2217        })
2218        .unwrap_or(false);
2219        let is_public = self
2220            .channel_store
2221            .read(cx)
2222            .channel_for_id(channel_id)
2223            .map(|channel| channel.visibility)
2224            == Some(proto::ChannelVisibility::Public);
2225        let disclosed =
2226            has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
2227
2228        let has_messages_notification = channel.unseen_message_id.is_some();
2229        let has_notes_notification = channel.unseen_note_version.is_some();
2230
2231        const FACEPILE_LIMIT: usize = 3;
2232        let participants = self.channel_store.read(cx).channel_participants(channel_id);
2233
2234        let face_pile = if !participants.is_empty() {
2235            let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2236            let result = FacePile {
2237                faces: participants
2238                    .iter()
2239                    .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
2240                    .take(FACEPILE_LIMIT)
2241                    .chain(if extra_count > 0 {
2242                        Some(
2243                            div()
2244                                .ml_1()
2245                                .child(Label::new(format!("+{extra_count}")))
2246                                .into_any_element(),
2247                        )
2248                    } else {
2249                        None
2250                    })
2251                    .collect::<SmallVec<_>>(),
2252            };
2253
2254            Some(result)
2255        } else {
2256            None
2257        };
2258
2259        let width = self.width.unwrap_or(px(240.));
2260
2261        div()
2262            .id(channel_id as usize)
2263            .group("")
2264            .flex()
2265            .w_full()
2266            .on_drag(channel.clone(), move |channel, cx| {
2267                cx.new_view(|_| DraggedChannelView {
2268                    channel: channel.clone(),
2269                    width,
2270                })
2271            })
2272            .drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
2273            .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
2274                this.channel_store
2275                    .update(cx, |channel_store, cx| {
2276                        channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
2277                    })
2278                    .detach_and_log_err(cx)
2279            }))
2280            .child(
2281                ListItem::new(channel_id as usize)
2282                    // Add one level of depth for the disclosure arrow.
2283                    .indent_level(depth + 1)
2284                    .indent_step_size(px(20.))
2285                    .selected(is_selected || is_active)
2286                    .toggle(disclosed)
2287                    .on_toggle(
2288                        cx.listener(move |this, _, cx| {
2289                            this.toggle_channel_collapsed(channel_id, cx)
2290                        }),
2291                    )
2292                    .on_click(cx.listener(move |this, _, cx| {
2293                        if is_active {
2294                            this.open_channel_notes(channel_id, cx)
2295                        } else {
2296                            this.join_channel(channel_id, cx)
2297                        }
2298                    }))
2299                    .on_secondary_mouse_down(cx.listener(
2300                        move |this, event: &MouseDownEvent, cx| {
2301                            this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
2302                        },
2303                    ))
2304                    .start_slot(
2305                        Icon::new(if is_public {
2306                            IconName::Public
2307                        } else {
2308                            IconName::Hash
2309                        })
2310                        .size(IconSize::Small)
2311                        .color(Color::Muted),
2312                    )
2313                    .child(
2314                        h_stack()
2315                            .id(channel_id as usize)
2316                            .child(Label::new(channel.name.clone()))
2317                            .children(face_pile.map(|face_pile| face_pile.render(cx))),
2318                    ),
2319            )
2320            .child(
2321                h_stack()
2322                    .absolute()
2323                    .right(rems(0.))
2324                    .h_full()
2325                    // HACK: Without this the channel name clips on top of the icons, but I'm not sure why.
2326                    .z_index(10)
2327                    .child(
2328                        h_stack()
2329                            .h_full()
2330                            .gap_1()
2331                            .px_1()
2332                            .child(
2333                                IconButton::new("channel_chat", IconName::MessageBubbles)
2334                                    .style(ButtonStyle::Filled)
2335                                    .size(ButtonSize::Compact)
2336                                    .icon_size(IconSize::Small)
2337                                    .icon_color(if has_messages_notification {
2338                                        Color::Default
2339                                    } else {
2340                                        Color::Muted
2341                                    })
2342                                    .on_click(cx.listener(move |this, _, cx| {
2343                                        this.join_channel_chat(channel_id, cx)
2344                                    }))
2345                                    .tooltip(|cx| Tooltip::text("Open channel chat", cx))
2346                                    .when(!has_messages_notification, |this| {
2347                                        this.visible_on_hover("")
2348                                    }),
2349                            )
2350                            .child(
2351                                IconButton::new("channel_notes", IconName::File)
2352                                    .style(ButtonStyle::Filled)
2353                                    .size(ButtonSize::Compact)
2354                                    .icon_size(IconSize::Small)
2355                                    .icon_color(if has_notes_notification {
2356                                        Color::Default
2357                                    } else {
2358                                        Color::Muted
2359                                    })
2360                                    .on_click(cx.listener(move |this, _, cx| {
2361                                        this.open_channel_notes(channel_id, cx)
2362                                    }))
2363                                    .tooltip(|cx| Tooltip::text("Open channel notes", cx))
2364                                    .when(!has_notes_notification, |this| {
2365                                        this.visible_on_hover("")
2366                                    }),
2367                            ),
2368                    ),
2369            )
2370            .tooltip(|cx| Tooltip::text("Join channel", cx))
2371    }
2372
2373    fn render_channel_editor(&self, depth: usize, _cx: &mut ViewContext<Self>) -> impl IntoElement {
2374        let item = ListItem::new("channel-editor")
2375            .inset(false)
2376            // Add one level of depth for the disclosure arrow.
2377            .indent_level(depth + 1)
2378            .indent_step_size(px(20.))
2379            .start_slot(
2380                Icon::new(IconName::Hash)
2381                    .size(IconSize::Small)
2382                    .color(Color::Muted),
2383            );
2384
2385        if let Some(pending_name) = self
2386            .channel_editing_state
2387            .as_ref()
2388            .and_then(|state| state.pending_name())
2389        {
2390            item.child(Label::new(pending_name))
2391        } else {
2392            item.child(self.channel_name_editor.clone())
2393        }
2394    }
2395}
2396
2397fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) -> impl IntoElement {
2398    let rem_size = cx.rem_size();
2399    let line_height = cx.text_style().line_height_in_pixels(rem_size);
2400    let width = rem_size * 1.5;
2401    let thickness = px(1.);
2402    let color = cx.theme().colors().text;
2403
2404    canvas(move |bounds, cx| {
2405        let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
2406        let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
2407        let right = bounds.right();
2408        let top = bounds.top();
2409
2410        cx.paint_quad(fill(
2411            Bounds::from_corners(
2412                point(start_x, top),
2413                point(
2414                    start_x + thickness,
2415                    if is_last {
2416                        start_y
2417                    } else {
2418                        bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
2419                    },
2420                ),
2421            ),
2422            color,
2423        ));
2424        cx.paint_quad(fill(
2425            Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
2426            color,
2427        ));
2428    })
2429    .w(width)
2430    .h(line_height)
2431}
2432
2433impl Render for CollabPanel {
2434    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2435        v_stack()
2436            .key_context("CollabPanel")
2437            .on_action(cx.listener(CollabPanel::cancel))
2438            .on_action(cx.listener(CollabPanel::select_next))
2439            .on_action(cx.listener(CollabPanel::select_prev))
2440            .on_action(cx.listener(CollabPanel::confirm))
2441            .on_action(cx.listener(CollabPanel::insert_space))
2442            .on_action(cx.listener(CollabPanel::remove_selected_channel))
2443            .on_action(cx.listener(CollabPanel::show_inline_context_menu))
2444            .on_action(cx.listener(CollabPanel::rename_selected_channel))
2445            .on_action(cx.listener(CollabPanel::collapse_selected_channel))
2446            .on_action(cx.listener(CollabPanel::expand_selected_channel))
2447            .on_action(cx.listener(CollabPanel::start_move_selected_channel))
2448            .track_focus(&self.focus_handle)
2449            .size_full()
2450            .child(if self.user_store.read(cx).current_user().is_none() {
2451                self.render_signed_out(cx)
2452            } else {
2453                self.render_signed_in(cx)
2454            })
2455            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
2456                overlay()
2457                    .position(*position)
2458                    .anchor(gpui::AnchorCorner::TopLeft)
2459                    .child(menu.clone())
2460            }))
2461    }
2462}
2463
2464impl EventEmitter<PanelEvent> for CollabPanel {}
2465
2466impl Panel for CollabPanel {
2467    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
2468        CollaborationPanelSettings::get_global(cx).dock
2469    }
2470
2471    fn position_is_valid(&self, position: DockPosition) -> bool {
2472        matches!(position, DockPosition::Left | DockPosition::Right)
2473    }
2474
2475    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
2476        settings::update_settings_file::<CollaborationPanelSettings>(
2477            self.fs.clone(),
2478            cx,
2479            move |settings| settings.dock = Some(position),
2480        );
2481    }
2482
2483    fn size(&self, cx: &gpui::WindowContext) -> Pixels {
2484        self.width
2485            .unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width)
2486    }
2487
2488    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
2489        self.width = size;
2490        self.serialize(cx);
2491        cx.notify();
2492    }
2493
2494    fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::IconName> {
2495        CollaborationPanelSettings::get_global(cx)
2496            .button
2497            .then(|| ui::IconName::Collab)
2498    }
2499
2500    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
2501        Some("Collab Panel")
2502    }
2503
2504    fn toggle_action(&self) -> Box<dyn gpui::Action> {
2505        Box::new(ToggleFocus)
2506    }
2507
2508    fn persistent_name() -> &'static str {
2509        "CollabPanel"
2510    }
2511}
2512
2513impl FocusableView for CollabPanel {
2514    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
2515        self.filter_editor.focus_handle(cx).clone()
2516    }
2517}
2518
2519impl PartialEq for ListEntry {
2520    fn eq(&self, other: &Self) -> bool {
2521        match self {
2522            ListEntry::Header(section_1) => {
2523                if let ListEntry::Header(section_2) = other {
2524                    return section_1 == section_2;
2525                }
2526            }
2527            ListEntry::CallParticipant { user: user_1, .. } => {
2528                if let ListEntry::CallParticipant { user: user_2, .. } = other {
2529                    return user_1.id == user_2.id;
2530                }
2531            }
2532            ListEntry::ParticipantProject {
2533                project_id: project_id_1,
2534                ..
2535            } => {
2536                if let ListEntry::ParticipantProject {
2537                    project_id: project_id_2,
2538                    ..
2539                } = other
2540                {
2541                    return project_id_1 == project_id_2;
2542                }
2543            }
2544            ListEntry::ParticipantScreen {
2545                peer_id: peer_id_1, ..
2546            } => {
2547                if let ListEntry::ParticipantScreen {
2548                    peer_id: peer_id_2, ..
2549                } = other
2550                {
2551                    return peer_id_1 == peer_id_2;
2552                }
2553            }
2554            ListEntry::Channel {
2555                channel: channel_1, ..
2556            } => {
2557                if let ListEntry::Channel {
2558                    channel: channel_2, ..
2559                } = other
2560                {
2561                    return channel_1.id == channel_2.id;
2562                }
2563            }
2564            ListEntry::ChannelNotes { channel_id } => {
2565                if let ListEntry::ChannelNotes {
2566                    channel_id: other_id,
2567                } = other
2568                {
2569                    return channel_id == other_id;
2570                }
2571            }
2572            ListEntry::ChannelChat { channel_id } => {
2573                if let ListEntry::ChannelChat {
2574                    channel_id: other_id,
2575                } = other
2576                {
2577                    return channel_id == other_id;
2578                }
2579            }
2580            ListEntry::ChannelInvite(channel_1) => {
2581                if let ListEntry::ChannelInvite(channel_2) = other {
2582                    return channel_1.id == channel_2.id;
2583                }
2584            }
2585            ListEntry::IncomingRequest(user_1) => {
2586                if let ListEntry::IncomingRequest(user_2) = other {
2587                    return user_1.id == user_2.id;
2588                }
2589            }
2590            ListEntry::OutgoingRequest(user_1) => {
2591                if let ListEntry::OutgoingRequest(user_2) = other {
2592                    return user_1.id == user_2.id;
2593                }
2594            }
2595            ListEntry::Contact {
2596                contact: contact_1, ..
2597            } => {
2598                if let ListEntry::Contact {
2599                    contact: contact_2, ..
2600                } = other
2601                {
2602                    return contact_1.user.id == contact_2.user.id;
2603                }
2604            }
2605            ListEntry::ChannelEditor { depth } => {
2606                if let ListEntry::ChannelEditor { depth: other_depth } = other {
2607                    return depth == other_depth;
2608                }
2609            }
2610            ListEntry::ContactPlaceholder => {
2611                if let ListEntry::ContactPlaceholder = other {
2612                    return true;
2613                }
2614            }
2615        }
2616        false
2617    }
2618}
2619
2620struct DraggedChannelView {
2621    channel: Channel,
2622    width: Pixels,
2623}
2624
2625impl Render for DraggedChannelView {
2626    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
2627        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
2628        h_stack()
2629            .font(ui_font)
2630            .bg(cx.theme().colors().background)
2631            .w(self.width)
2632            .p_1()
2633            .gap_1()
2634            .child(
2635                Icon::new(
2636                    if self.channel.visibility == proto::ChannelVisibility::Public {
2637                        IconName::Public
2638                    } else {
2639                        IconName::Hash
2640                    },
2641                )
2642                .size(IconSize::Small)
2643                .color(Color::Muted),
2644            )
2645            .child(Label::new(self.channel.name.clone()))
2646    }
2647}