collab_panel.rs

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