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