collab_panel.rs

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