collab_panel.rs

   1mod channel_modal;
   2mod contact_finder;
   3
   4use self::channel_modal::ChannelModal;
   5use crate::{CollaborationPanelSettings, channel_view::ChannelView};
   6use anyhow::Context as _;
   7use call::ActiveCall;
   8use channel::{Channel, ChannelEvent, ChannelStore};
   9use client::{ChannelId, Client, Contact, User, UserStore};
  10use collections::{HashMap, HashSet};
  11use contact_finder::ContactFinder;
  12use db::kvp::KeyValueStore;
  13use editor::{Editor, EditorElement, EditorStyle};
  14use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
  15use gpui::{
  16    AnyElement, App, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, DismissEvent, Div,
  17    Empty, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, KeyContext, ListOffset,
  18    ListState, MouseDownEvent, Pixels, Point, PromptLevel, SharedString, Subscription, Task,
  19    TextStyle, WeakEntity, Window, actions, anchored, canvas, deferred, div, fill, list, point,
  20    prelude::*, px,
  21};
  22
  23use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrevious};
  24use project::{Fs, Project};
  25use rpc::{
  26    ErrorCode, ErrorExt,
  27    proto::{self, ChannelVisibility, PeerId, reorder_channel::Direction},
  28};
  29use serde::{Deserialize, Serialize};
  30use settings::Settings;
  31use smallvec::SmallVec;
  32use std::{mem, sync::Arc};
  33use theme::ActiveTheme;
  34use theme_settings::ThemeSettings;
  35use ui::{
  36    Avatar, AvatarAvailabilityIndicator, ContextMenu, CopyButton, Facepile, HighlightedLabel,
  37    IconButtonShape, Indicator, ListHeader, ListItem, Tab, Tooltip, prelude::*, tooltip_container,
  38};
  39use util::{ResultExt, TryFutureExt, maybe};
  40use workspace::{
  41    CopyRoomId, Deafen, LeaveCall, MultiWorkspace, Mute, OpenChannelNotes, OpenChannelNotesById,
  42    ScreenShare, ShareProject, Workspace,
  43    dock::{DockPosition, Panel, PanelEvent},
  44    notifications::{DetachAndPromptErr, NotifyResultExt},
  45};
  46
  47const FILTER_OCCUPIED_CHANNELS_KEY: &str = "filter_occupied_channels";
  48const FAVORITE_CHANNELS_KEY: &str = "favorite_channels";
  49
  50actions!(
  51    collab_panel,
  52    [
  53        /// Toggles the collab panel.
  54        Toggle,
  55        /// Toggles focus on the collaboration panel.
  56        ToggleFocus,
  57        /// Removes the selected channel or contact.
  58        Remove,
  59        /// Opens the context menu for the selected item.
  60        Secondary,
  61        /// Collapses the selected channel in the tree view.
  62        CollapseSelectedChannel,
  63        /// Expands the selected channel in the tree view.
  64        ExpandSelectedChannel,
  65        /// Opens the meeting notes for the selected channel in the panel.
  66        ///
  67        /// Use `collab::OpenChannelNotes` to open the channel notes for the current call.
  68        OpenSelectedChannelNotes,
  69        /// Toggles whether the selected channel is in the Favorites section.
  70        ToggleSelectedChannelFavorite,
  71        /// Starts moving a channel to a new location.
  72        StartMoveChannel,
  73        /// Moves the selected item to the current location.
  74        MoveSelected,
  75        /// Inserts a space character in the filter input.
  76        InsertSpace,
  77        /// Moves the selected channel up in the list.
  78        MoveChannelUp,
  79        /// Moves the selected channel down in the list.
  80        MoveChannelDown,
  81    ]
  82);
  83
  84#[derive(Debug, Copy, Clone, PartialEq, Eq)]
  85struct ChannelMoveClipboard {
  86    channel_id: ChannelId,
  87}
  88
  89const COLLABORATION_PANEL_KEY: &str = "CollaborationPanel";
  90
  91pub fn init(cx: &mut App) {
  92    cx.observe_new(|workspace: &mut Workspace, _, _| {
  93        workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
  94            workspace.toggle_panel_focus::<CollabPanel>(window, cx);
  95            if let Some(collab_panel) = workspace.panel::<CollabPanel>(cx) {
  96                collab_panel.update(cx, |panel, cx| {
  97                    panel.filter_editor.update(cx, |editor, cx| {
  98                        if editor.snapshot(window, cx).is_focused() {
  99                            editor.select_all(&Default::default(), window, cx);
 100                        }
 101                    });
 102                })
 103            }
 104        });
 105        workspace.register_action(|workspace, _: &Toggle, window, cx| {
 106            if !workspace.toggle_panel_focus::<CollabPanel>(window, cx) {
 107                workspace.close_panel::<CollabPanel>(window, cx);
 108            }
 109        });
 110        workspace.register_action(|_, _: &OpenChannelNotes, window, cx| {
 111            let channel_id = ActiveCall::global(cx)
 112                .read(cx)
 113                .room()
 114                .and_then(|room| room.read(cx).channel_id());
 115
 116            if let Some(channel_id) = channel_id {
 117                let workspace = cx.entity();
 118                window.defer(cx, move |window, cx| {
 119                    ChannelView::open(channel_id, None, workspace, window, cx)
 120                        .detach_and_log_err(cx)
 121                });
 122            }
 123        });
 124        workspace.register_action(|_, action: &OpenChannelNotesById, window, cx| {
 125            let channel_id = client::ChannelId(action.channel_id);
 126            let workspace = cx.entity();
 127            window.defer(cx, move |window, cx| {
 128                ChannelView::open(channel_id, None, workspace, window, cx).detach_and_log_err(cx)
 129            });
 130        });
 131        // TODO: make it possible to bind this one to a held key for push to talk?
 132        // how to make "toggle_on_modifiers_press" contextual?
 133        workspace.register_action(|_, _: &Mute, _, cx| title_bar::collab::toggle_mute(cx));
 134        workspace.register_action(|_, _: &Deafen, _, cx| title_bar::collab::toggle_deafen(cx));
 135        workspace.register_action(|_, _: &LeaveCall, window, cx| {
 136            CollabPanel::leave_call(window, cx);
 137        });
 138        workspace.register_action(|workspace, _: &CopyRoomId, window, cx| {
 139            use workspace::notifications::{NotificationId, NotifyTaskExt as _};
 140
 141            struct RoomIdCopiedToast;
 142
 143            if let Some(room) = ActiveCall::global(cx).read(cx).room() {
 144                let romo_id_fut = room.read(cx).room_id();
 145                let workspace_handle = cx.weak_entity();
 146                cx.spawn(async move |workspace, cx| {
 147                    let room_id = romo_id_fut.await.context("Failed to get livekit room")?;
 148                    workspace.update(cx, |workspace, cx| {
 149                        cx.write_to_clipboard(ClipboardItem::new_string(room_id));
 150                        workspace.show_toast(
 151                            workspace::Toast::new(
 152                                NotificationId::unique::<RoomIdCopiedToast>(),
 153                                "Room ID copied to clipboard",
 154                            )
 155                            .autohide(),
 156                            cx,
 157                        );
 158                    })
 159                })
 160                .detach_and_notify_err(workspace_handle, window, cx);
 161            } else {
 162                workspace.show_error(&"There’s no active call; join one first.", cx);
 163            }
 164        });
 165        workspace.register_action(|workspace, _: &ShareProject, window, cx| {
 166            let project = workspace.project().clone();
 167            println!("{project:?}");
 168            window.defer(cx, move |_window, cx| {
 169                ActiveCall::global(cx).update(cx, move |call, cx| {
 170                    if let Some(room) = call.room() {
 171                        println!("{room:?}");
 172                        if room.read(cx).is_sharing_project() {
 173                            call.unshare_project(project, cx).ok();
 174                        } else {
 175                            call.share_project(project, cx).detach_and_log_err(cx);
 176                        }
 177                    }
 178                });
 179            });
 180        });
 181        // TODO(jk): Is this action ever triggered?
 182        workspace.register_action(|_, _: &ScreenShare, window, cx| {
 183            let room = ActiveCall::global(cx).read(cx).room().cloned();
 184            if let Some(room) = room {
 185                window.defer(cx, move |_window, cx| {
 186                    room.update(cx, |room, cx| {
 187                        if room.is_sharing_screen() {
 188                            room.unshare_screen(true, cx).ok();
 189                        } else {
 190                            #[cfg(target_os = "linux")]
 191                            let is_wayland = gpui::guess_compositor() == "Wayland";
 192                            #[cfg(not(target_os = "linux"))]
 193                            let is_wayland = false;
 194
 195                            #[cfg(target_os = "linux")]
 196                            {
 197                                if is_wayland {
 198                                    room.share_screen_wayland(cx).detach_and_log_err(cx);
 199                                }
 200                            }
 201                            if !is_wayland {
 202                                let sources = cx.screen_capture_sources();
 203
 204                                cx.spawn(async move |room, cx| {
 205                                    let sources = sources.await??;
 206                                    let first = sources.into_iter().next();
 207                                    if let Some(first) = first {
 208                                        room.update(cx, |room, cx| room.share_screen(first, cx))?
 209                                            .await
 210                                    } else {
 211                                        Ok(())
 212                                    }
 213                                })
 214                                .detach_and_log_err(cx);
 215                            }
 216                        };
 217                    });
 218                });
 219            }
 220        });
 221    })
 222    .detach();
 223}
 224
 225#[derive(Debug)]
 226pub enum ChannelEditingState {
 227    Create {
 228        location: Option<ChannelId>,
 229        pending_name: Option<String>,
 230    },
 231    Rename {
 232        location: ChannelId,
 233        pending_name: Option<String>,
 234    },
 235}
 236
 237impl ChannelEditingState {
 238    fn pending_name(&self) -> Option<String> {
 239        match self {
 240            ChannelEditingState::Create { pending_name, .. } => pending_name.clone(),
 241            ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(),
 242        }
 243    }
 244}
 245
 246pub struct CollabPanel {
 247    fs: Arc<dyn Fs>,
 248    focus_handle: FocusHandle,
 249    channel_clipboard: Option<ChannelMoveClipboard>,
 250    pending_panel_serialization: Task<Option<()>>,
 251    pending_favorites_serialization: Task<Option<()>>,
 252    pending_filter_serialization: Task<Option<()>>,
 253    context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
 254    list_state: ListState,
 255    filter_editor: Entity<Editor>,
 256    channel_name_editor: Entity<Editor>,
 257    channel_editing_state: Option<ChannelEditingState>,
 258    entries: Vec<ListEntry>,
 259    selection: Option<usize>,
 260    channel_store: Entity<ChannelStore>,
 261    user_store: Entity<UserStore>,
 262    client: Arc<Client>,
 263    project: Entity<Project>,
 264    match_candidates: Vec<StringMatchCandidate>,
 265    subscriptions: Vec<Subscription>,
 266    collapsed_sections: Vec<Section>,
 267    collapsed_channels: Vec<ChannelId>,
 268    filter_occupied_channels: bool,
 269    workspace: WeakEntity<Workspace>,
 270}
 271
 272#[derive(Serialize, Deserialize)]
 273struct SerializedCollabPanel {
 274    collapsed_channels: Option<Vec<u64>>,
 275}
 276
 277#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
 278enum Section {
 279    ActiveCall,
 280    FavoriteChannels,
 281    Channels,
 282    ChannelInvites,
 283    ContactRequests,
 284    Contacts,
 285    Online,
 286    Offline,
 287}
 288
 289#[derive(Clone, Debug)]
 290enum ListEntry {
 291    Header(Section),
 292    CallParticipant {
 293        user: Arc<User>,
 294        peer_id: Option<PeerId>,
 295        is_pending: bool,
 296        role: proto::ChannelRole,
 297    },
 298    ParticipantProject {
 299        project_id: u64,
 300        worktree_root_names: Vec<String>,
 301        host_user_id: u64,
 302        is_last: bool,
 303    },
 304    ParticipantScreen {
 305        peer_id: Option<PeerId>,
 306        is_last: bool,
 307    },
 308    IncomingRequest(Arc<User>),
 309    OutgoingRequest(Arc<User>),
 310    ChannelInvite(Arc<Channel>),
 311    Channel {
 312        channel: Arc<Channel>,
 313        depth: usize,
 314        has_children: bool,
 315        is_favorite: bool,
 316        // `None` when the channel is a parent of a matched channel.
 317        string_match: Option<StringMatch>,
 318    },
 319    ChannelNotes {
 320        channel_id: ChannelId,
 321    },
 322    ChannelEditor {
 323        depth: usize,
 324    },
 325    Contact {
 326        contact: Arc<Contact>,
 327        calling: bool,
 328    },
 329    ContactPlaceholder,
 330}
 331
 332impl CollabPanel {
 333    pub fn new(
 334        workspace: &mut Workspace,
 335        window: &mut Window,
 336        cx: &mut Context<Workspace>,
 337    ) -> Entity<Self> {
 338        cx.new(|cx| {
 339            let filter_editor = cx.new(|cx| {
 340                let mut editor = Editor::single_line(window, cx);
 341                editor.set_placeholder_text("Search channels…", window, cx);
 342                editor
 343            });
 344
 345            cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {
 346                if let editor::EditorEvent::BufferEdited = event {
 347                    let query = this.filter_editor.read(cx).text(cx);
 348                    if !query.is_empty() {
 349                        this.selection.take();
 350                    }
 351                    this.update_entries(true, cx);
 352                    if !query.is_empty() {
 353                        this.selection = this
 354                            .entries
 355                            .iter()
 356                            .position(|entry| !matches!(entry, ListEntry::Header(_)));
 357                    }
 358                }
 359            })
 360            .detach();
 361
 362            let channel_name_editor = cx.new(|cx| Editor::single_line(window, cx));
 363
 364            cx.subscribe_in(
 365                &channel_name_editor,
 366                window,
 367                |this: &mut Self, _, event, window, cx| {
 368                    if let editor::EditorEvent::Blurred = event {
 369                        if let Some(state) = &this.channel_editing_state
 370                            && state.pending_name().is_some()
 371                        {
 372                            return;
 373                        }
 374                        this.take_editing_state(window, cx);
 375                        this.update_entries(false, cx);
 376                        cx.notify();
 377                    }
 378                },
 379            )
 380            .detach();
 381
 382            let mut this = Self {
 383                focus_handle: cx.focus_handle(),
 384                channel_clipboard: None,
 385                fs: workspace.app_state().fs.clone(),
 386                pending_panel_serialization: Task::ready(None),
 387                pending_favorites_serialization: Task::ready(None),
 388                pending_filter_serialization: Task::ready(None),
 389                context_menu: None,
 390                list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),
 391                channel_name_editor,
 392                filter_editor,
 393                entries: Vec::default(),
 394                channel_editing_state: None,
 395                selection: None,
 396                channel_store: ChannelStore::global(cx),
 397                user_store: workspace.user_store().clone(),
 398                project: workspace.project().clone(),
 399                subscriptions: Vec::default(),
 400                match_candidates: Vec::default(),
 401                collapsed_sections: vec![Section::Offline],
 402                collapsed_channels: Vec::default(),
 403                filter_occupied_channels: false,
 404                workspace: workspace.weak_handle(),
 405                client: workspace.app_state().client.clone(),
 406            };
 407
 408            this.update_entries(false, cx);
 409
 410            let active_call = ActiveCall::global(cx);
 411            this.subscriptions
 412                .push(cx.observe(&this.user_store, |this, _, cx| {
 413                    this.update_entries(true, cx)
 414                }));
 415            this.subscriptions
 416                .push(cx.observe(&this.channel_store, move |this, _, cx| {
 417                    this.update_entries(true, cx)
 418                }));
 419            this.subscriptions
 420                .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
 421            this.subscriptions.push(cx.subscribe_in(
 422                &this.channel_store,
 423                window,
 424                |this, _channel_store, e, window, cx| match e {
 425                    ChannelEvent::ChannelCreated(channel_id)
 426                    | ChannelEvent::ChannelRenamed(channel_id) => {
 427                        if this.take_editing_state(window, cx) {
 428                            this.update_entries(false, cx);
 429                            this.selection = this.entries.iter().position(|entry| {
 430                                if let ListEntry::Channel { channel, .. } = entry {
 431                                    channel.id == *channel_id
 432                                } else {
 433                                    false
 434                                }
 435                            });
 436                        }
 437                    }
 438                },
 439            ));
 440
 441            this
 442        })
 443    }
 444
 445    pub async fn load(
 446        workspace: WeakEntity<Workspace>,
 447        mut cx: AsyncWindowContext,
 448    ) -> anyhow::Result<Entity<Self>> {
 449        let serialized_panel = match workspace
 450            .read_with(&cx, |workspace, _| {
 451                CollabPanel::serialization_key(workspace)
 452            })
 453            .ok()
 454            .flatten()
 455        {
 456            Some(serialization_key) => {
 457                let kvp = cx.update(|_, cx| KeyValueStore::global(cx))?;
 458                kvp.read_kvp(&serialization_key)
 459                    .context("reading collaboration panel from key value store")
 460                    .log_err()
 461                    .flatten()
 462                    .map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel))
 463                    .transpose()
 464                    .log_err()
 465                    .flatten()
 466            }
 467            None => None,
 468        };
 469
 470        workspace.update_in(&mut cx, |workspace, window, cx| {
 471            let panel = CollabPanel::new(workspace, window, cx);
 472            if let Some(serialized_panel) = serialized_panel {
 473                panel.update(cx, |panel, cx| {
 474                    panel.collapsed_channels = serialized_panel
 475                        .collapsed_channels
 476                        .unwrap_or_default()
 477                        .iter()
 478                        .map(|cid| ChannelId(*cid))
 479                        .collect();
 480                    cx.notify();
 481                });
 482            }
 483
 484            let filter_occupied_channels = KeyValueStore::global(cx)
 485                .read_kvp(FILTER_OCCUPIED_CHANNELS_KEY)
 486                .ok()
 487                .flatten()
 488                .is_some();
 489
 490            panel.update(cx, |panel, cx| {
 491                panel.filter_occupied_channels = filter_occupied_channels;
 492
 493                if filter_occupied_channels {
 494                    panel.update_entries(false, cx);
 495                }
 496            });
 497
 498            let favorites: Vec<ChannelId> = KeyValueStore::global(cx)
 499                .read_kvp(FAVORITE_CHANNELS_KEY)
 500                .ok()
 501                .flatten()
 502                .and_then(|json| serde_json::from_str::<Vec<u64>>(&json).ok())
 503                .unwrap_or_default()
 504                .into_iter()
 505                .map(ChannelId)
 506                .collect();
 507
 508            if !favorites.is_empty() {
 509                panel.update(cx, |panel, cx| {
 510                    panel.channel_store.update(cx, |store, cx| {
 511                        store.set_favorite_channel_ids(favorites, cx);
 512                    });
 513                });
 514            }
 515
 516            panel
 517        })
 518    }
 519
 520    fn serialization_key(workspace: &Workspace) -> Option<String> {
 521        workspace
 522            .database_id()
 523            .map(|id| i64::from(id).to_string())
 524            .or(workspace.session_id())
 525            .map(|id| format!("{}-{:?}", COLLABORATION_PANEL_KEY, id))
 526    }
 527
 528    fn serialize(&mut self, cx: &mut Context<Self>) {
 529        let Some(serialization_key) = self
 530            .workspace
 531            .read_with(cx, |workspace, _| CollabPanel::serialization_key(workspace))
 532            .ok()
 533            .flatten()
 534        else {
 535            return;
 536        };
 537        let collapsed_channels = if self.collapsed_channels.is_empty() {
 538            None
 539        } else {
 540            Some(self.collapsed_channels.iter().map(|id| id.0).collect())
 541        };
 542
 543        let kvp = KeyValueStore::global(cx);
 544        self.pending_panel_serialization = cx.background_spawn(
 545            async move {
 546                kvp.write_kvp(
 547                    serialization_key,
 548                    serde_json::to_string(&SerializedCollabPanel { collapsed_channels })?,
 549                )
 550                .await?;
 551                anyhow::Ok(())
 552            }
 553            .log_err(),
 554        );
 555    }
 556
 557    fn scroll_to_item(&mut self, ix: usize) {
 558        self.list_state.scroll_to_reveal_item(ix)
 559    }
 560
 561    fn update_entries(&mut self, select_same_item: bool, cx: &mut Context<Self>) {
 562        let query = self.filter_editor.read(cx).text(cx);
 563        let fg_executor = cx.foreground_executor().clone();
 564        let executor = cx.background_executor().clone();
 565
 566        let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
 567        let old_entries = mem::take(&mut self.entries);
 568        let mut scroll_to_top = false;
 569
 570        if let Some(room) = ActiveCall::global(cx).read(cx).room() {
 571            self.entries.push(ListEntry::Header(Section::ActiveCall));
 572            if !old_entries
 573                .iter()
 574                .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
 575            {
 576                scroll_to_top = true;
 577            }
 578
 579            if !self.collapsed_sections.contains(&Section::ActiveCall) {
 580                let room = room.read(cx);
 581
 582                if query.is_empty()
 583                    && let Some(channel_id) = room.channel_id()
 584                {
 585                    self.entries.push(ListEntry::ChannelNotes { channel_id });
 586                }
 587
 588                // Populate the active user.
 589                if let Some(user) = self.user_store.read(cx).current_user() {
 590                    self.match_candidates.clear();
 591                    self.match_candidates
 592                        .push(StringMatchCandidate::new(0, &user.github_login));
 593                    let matches = fg_executor.block_on(match_strings(
 594                        &self.match_candidates,
 595                        &query,
 596                        true,
 597                        true,
 598                        usize::MAX,
 599                        &Default::default(),
 600                        executor.clone(),
 601                    ));
 602                    if !matches.is_empty() {
 603                        let user_id = user.id;
 604                        self.entries.push(ListEntry::CallParticipant {
 605                            user,
 606                            peer_id: None,
 607                            is_pending: false,
 608                            role: room.local_participant().role,
 609                        });
 610                        let mut projects = room.local_participant().projects.iter().peekable();
 611                        while let Some(project) = projects.next() {
 612                            self.entries.push(ListEntry::ParticipantProject {
 613                                project_id: project.id,
 614                                worktree_root_names: project.worktree_root_names.clone(),
 615                                host_user_id: user_id,
 616                                is_last: projects.peek().is_none() && !room.is_sharing_screen(),
 617                            });
 618                        }
 619                        if room.is_sharing_screen() {
 620                            self.entries.push(ListEntry::ParticipantScreen {
 621                                peer_id: None,
 622                                is_last: true,
 623                            });
 624                        }
 625                    }
 626                }
 627
 628                // Populate remote participants.
 629                self.match_candidates.clear();
 630                self.match_candidates
 631                    .extend(room.remote_participants().values().map(|participant| {
 632                        StringMatchCandidate::new(
 633                            participant.user.id as usize,
 634                            &participant.user.github_login,
 635                        )
 636                    }));
 637                let mut matches = fg_executor.block_on(match_strings(
 638                    &self.match_candidates,
 639                    &query,
 640                    true,
 641                    true,
 642                    usize::MAX,
 643                    &Default::default(),
 644                    executor.clone(),
 645                ));
 646                matches.sort_by(|a, b| {
 647                    let a_is_guest = room.role_for_user(a.candidate_id as u64)
 648                        == Some(proto::ChannelRole::Guest);
 649                    let b_is_guest = room.role_for_user(b.candidate_id as u64)
 650                        == Some(proto::ChannelRole::Guest);
 651                    a_is_guest
 652                        .cmp(&b_is_guest)
 653                        .then_with(|| a.string.cmp(&b.string))
 654                });
 655                for mat in matches {
 656                    let user_id = mat.candidate_id as u64;
 657                    let participant = &room.remote_participants()[&user_id];
 658                    self.entries.push(ListEntry::CallParticipant {
 659                        user: participant.user.clone(),
 660                        peer_id: Some(participant.peer_id),
 661                        is_pending: false,
 662                        role: participant.role,
 663                    });
 664                    let mut projects = participant.projects.iter().peekable();
 665                    while let Some(project) = projects.next() {
 666                        self.entries.push(ListEntry::ParticipantProject {
 667                            project_id: project.id,
 668                            worktree_root_names: project.worktree_root_names.clone(),
 669                            host_user_id: participant.user.id,
 670                            is_last: projects.peek().is_none() && !participant.has_video_tracks(),
 671                        });
 672                    }
 673                    if participant.has_video_tracks() {
 674                        self.entries.push(ListEntry::ParticipantScreen {
 675                            peer_id: Some(participant.peer_id),
 676                            is_last: true,
 677                        });
 678                    }
 679                }
 680
 681                // Populate pending participants.
 682                self.match_candidates.clear();
 683                self.match_candidates
 684                    .extend(room.pending_participants().iter().enumerate().map(
 685                        |(id, participant)| {
 686                            StringMatchCandidate::new(id, &participant.github_login)
 687                        },
 688                    ));
 689                let matches = fg_executor.block_on(match_strings(
 690                    &self.match_candidates,
 691                    &query,
 692                    true,
 693                    true,
 694                    usize::MAX,
 695                    &Default::default(),
 696                    executor.clone(),
 697                ));
 698                self.entries
 699                    .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
 700                        user: room.pending_participants()[mat.candidate_id].clone(),
 701                        peer_id: None,
 702                        is_pending: true,
 703                        role: proto::ChannelRole::Member,
 704                    }));
 705            }
 706        }
 707
 708        let mut request_entries = Vec::new();
 709
 710        let channel_store = self.channel_store.read(cx);
 711        let user_store = self.user_store.read(cx);
 712
 713        let favorite_ids = channel_store.favorite_channel_ids();
 714        if !favorite_ids.is_empty() {
 715            let favorite_channels: Vec<_> = favorite_ids
 716                .iter()
 717                .filter_map(|id| channel_store.channel_for_id(*id))
 718                .collect();
 719
 720            self.match_candidates.clear();
 721            self.match_candidates.extend(
 722                favorite_channels
 723                    .iter()
 724                    .enumerate()
 725                    .map(|(ix, channel)| StringMatchCandidate::new(ix, &channel.name)),
 726            );
 727
 728            let matches = fg_executor.block_on(match_strings(
 729                &self.match_candidates,
 730                &query,
 731                true,
 732                true,
 733                usize::MAX,
 734                &Default::default(),
 735                executor.clone(),
 736            ));
 737
 738            if !matches.is_empty() || query.is_empty() {
 739                self.entries
 740                    .push(ListEntry::Header(Section::FavoriteChannels));
 741
 742                let matches_by_candidate: HashMap<usize, &StringMatch> =
 743                    matches.iter().map(|mat| (mat.candidate_id, mat)).collect();
 744
 745                for (ix, channel) in favorite_channels.iter().enumerate() {
 746                    if !query.is_empty() && !matches_by_candidate.contains_key(&ix) {
 747                        continue;
 748                    }
 749                    self.entries.push(ListEntry::Channel {
 750                        channel: (*channel).clone(),
 751                        depth: 0,
 752                        has_children: false,
 753                        is_favorite: true,
 754                        string_match: matches_by_candidate.get(&ix).cloned().cloned(),
 755                    });
 756                }
 757            }
 758        }
 759
 760        self.entries.push(ListEntry::Header(Section::Channels));
 761
 762        if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
 763            self.match_candidates.clear();
 764            self.match_candidates.extend(
 765                channel_store
 766                    .ordered_channels()
 767                    .enumerate()
 768                    .map(|(ix, (_, channel))| StringMatchCandidate::new(ix, &channel.name)),
 769            );
 770            let mut channels = channel_store
 771                .ordered_channels()
 772                .map(|(_, chan)| chan)
 773                .collect::<Vec<_>>();
 774            let matches = fg_executor.block_on(match_strings(
 775                &self.match_candidates,
 776                &query,
 777                true,
 778                true,
 779                usize::MAX,
 780                &Default::default(),
 781                executor.clone(),
 782            ));
 783
 784            let matches_by_id: HashMap<_, _> = matches
 785                .iter()
 786                .map(|mat| (channels[mat.candidate_id].id, mat.clone()))
 787                .collect();
 788
 789            let channel_ids_of_matches_or_parents: HashSet<_> = matches
 790                .iter()
 791                .flat_map(|mat| {
 792                    let match_channel = channels[mat.candidate_id];
 793
 794                    match_channel
 795                        .parent_path
 796                        .iter()
 797                        .copied()
 798                        .chain(Some(match_channel.id))
 799                })
 800                .collect();
 801
 802            channels.retain(|chan| channel_ids_of_matches_or_parents.contains(&chan.id));
 803
 804            if self.filter_occupied_channels {
 805                let occupied_channel_ids_or_ancestors: HashSet<_> = channel_store
 806                    .ordered_channels()
 807                    .map(|(_, channel)| channel)
 808                    .filter(|channel| !channel_store.channel_participants(channel.id).is_empty())
 809                    .flat_map(|channel| channel.parent_path.iter().copied().chain(Some(channel.id)))
 810                    .collect();
 811                channels.retain(|channel| occupied_channel_ids_or_ancestors.contains(&channel.id));
 812            }
 813
 814            if let Some(state) = &self.channel_editing_state
 815                && matches!(state, ChannelEditingState::Create { location: None, .. })
 816            {
 817                self.entries.push(ListEntry::ChannelEditor { depth: 0 });
 818            }
 819
 820            let should_respect_collapse = query.is_empty() && !self.filter_occupied_channels;
 821            let mut collapse_depth = None;
 822
 823            for (idx, channel) in channels.into_iter().enumerate() {
 824                let depth = channel.parent_path.len();
 825
 826                if should_respect_collapse {
 827                    if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
 828                        collapse_depth = Some(depth);
 829                    } else if let Some(collapsed_depth) = collapse_depth {
 830                        if depth > collapsed_depth {
 831                            continue;
 832                        }
 833                        if self.is_channel_collapsed(channel.id) {
 834                            collapse_depth = Some(depth);
 835                        } else {
 836                            collapse_depth = None;
 837                        }
 838                    }
 839                }
 840
 841                let has_children = channel_store
 842                    .channel_at_index(idx + 1)
 843                    .is_some_and(|next_channel| next_channel.parent_path.ends_with(&[channel.id]));
 844
 845                match &self.channel_editing_state {
 846                    Some(ChannelEditingState::Create {
 847                        location: parent_id,
 848                        ..
 849                    }) if *parent_id == Some(channel.id) => {
 850                        self.entries.push(ListEntry::Channel {
 851                            channel: channel.clone(),
 852                            depth,
 853                            has_children: false,
 854                            is_favorite: false,
 855                            string_match: matches_by_id.get(&channel.id).map(|mat| (*mat).clone()),
 856                        });
 857                        self.entries
 858                            .push(ListEntry::ChannelEditor { depth: depth + 1 });
 859                    }
 860                    Some(ChannelEditingState::Rename {
 861                        location: parent_id,
 862                        ..
 863                    }) if parent_id == &channel.id => {
 864                        self.entries.push(ListEntry::ChannelEditor { depth });
 865                    }
 866                    _ => {
 867                        self.entries.push(ListEntry::Channel {
 868                            channel: channel.clone(),
 869                            depth,
 870                            has_children,
 871                            is_favorite: false,
 872                            string_match: matches_by_id.get(&channel.id).map(|mat| (*mat).clone()),
 873                        });
 874                    }
 875                }
 876            }
 877        }
 878
 879        let channel_invites = channel_store.channel_invitations();
 880        if !channel_invites.is_empty() {
 881            self.match_candidates.clear();
 882            self.match_candidates.extend(
 883                channel_invites
 884                    .iter()
 885                    .enumerate()
 886                    .map(|(ix, channel)| StringMatchCandidate::new(ix, &channel.name)),
 887            );
 888            let matches = fg_executor.block_on(match_strings(
 889                &self.match_candidates,
 890                &query,
 891                true,
 892                true,
 893                usize::MAX,
 894                &Default::default(),
 895                executor.clone(),
 896            ));
 897            request_entries.extend(
 898                matches
 899                    .iter()
 900                    .map(|mat| ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())),
 901            );
 902
 903            if !request_entries.is_empty() {
 904                self.entries
 905                    .push(ListEntry::Header(Section::ChannelInvites));
 906                if !self.collapsed_sections.contains(&Section::ChannelInvites) {
 907                    self.entries.append(&mut request_entries);
 908                }
 909            }
 910        }
 911
 912        self.entries.push(ListEntry::Header(Section::Contacts));
 913
 914        request_entries.clear();
 915        let incoming = user_store.incoming_contact_requests();
 916        if !incoming.is_empty() {
 917            self.match_candidates.clear();
 918            self.match_candidates.extend(
 919                incoming
 920                    .iter()
 921                    .enumerate()
 922                    .map(|(ix, user)| StringMatchCandidate::new(ix, &user.github_login)),
 923            );
 924            let matches = fg_executor.block_on(match_strings(
 925                &self.match_candidates,
 926                &query,
 927                true,
 928                true,
 929                usize::MAX,
 930                &Default::default(),
 931                executor.clone(),
 932            ));
 933            request_entries.extend(
 934                matches
 935                    .iter()
 936                    .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
 937            );
 938        }
 939
 940        let outgoing = user_store.outgoing_contact_requests();
 941        if !outgoing.is_empty() {
 942            self.match_candidates.clear();
 943            self.match_candidates.extend(
 944                outgoing
 945                    .iter()
 946                    .enumerate()
 947                    .map(|(ix, user)| StringMatchCandidate::new(ix, &user.github_login)),
 948            );
 949            let matches = fg_executor.block_on(match_strings(
 950                &self.match_candidates,
 951                &query,
 952                true,
 953                true,
 954                usize::MAX,
 955                &Default::default(),
 956                executor.clone(),
 957            ));
 958            request_entries.extend(
 959                matches
 960                    .iter()
 961                    .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
 962            );
 963        }
 964
 965        if !request_entries.is_empty() {
 966            self.entries
 967                .push(ListEntry::Header(Section::ContactRequests));
 968            if !self.collapsed_sections.contains(&Section::ContactRequests) {
 969                self.entries.append(&mut request_entries);
 970            }
 971        }
 972
 973        let contacts = user_store.contacts();
 974        if !contacts.is_empty() {
 975            self.match_candidates.clear();
 976            self.match_candidates.extend(
 977                contacts
 978                    .iter()
 979                    .enumerate()
 980                    .map(|(ix, contact)| StringMatchCandidate::new(ix, &contact.user.github_login)),
 981            );
 982
 983            let matches = fg_executor.block_on(match_strings(
 984                &self.match_candidates,
 985                &query,
 986                true,
 987                true,
 988                usize::MAX,
 989                &Default::default(),
 990                executor,
 991            ));
 992
 993            let (online_contacts, offline_contacts) = matches
 994                .iter()
 995                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
 996
 997            for (matches, section) in [
 998                (online_contacts, Section::Online),
 999                (offline_contacts, Section::Offline),
1000            ] {
1001                if !matches.is_empty() {
1002                    self.entries.push(ListEntry::Header(section));
1003                    if !self.collapsed_sections.contains(&section) {
1004                        let active_call = &ActiveCall::global(cx).read(cx);
1005                        for mat in matches {
1006                            let contact = &contacts[mat.candidate_id];
1007                            self.entries.push(ListEntry::Contact {
1008                                contact: contact.clone(),
1009                                calling: active_call.pending_invites().contains(&contact.user.id),
1010                            });
1011                        }
1012                    }
1013                }
1014            }
1015        }
1016
1017        if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
1018            self.entries.push(ListEntry::ContactPlaceholder);
1019        }
1020
1021        if select_same_item {
1022            if let Some(prev_selected_entry) = prev_selected_entry {
1023                let prev_selection = self.selection.take();
1024                for (ix, entry) in self.entries.iter().enumerate() {
1025                    if *entry == prev_selected_entry {
1026                        self.selection = Some(ix);
1027                        break;
1028                    }
1029                }
1030                if self.selection.is_none() {
1031                    self.selection = prev_selection.and_then(|prev_ix| {
1032                        if self.entries.is_empty() {
1033                            None
1034                        } else {
1035                            Some(prev_ix.min(self.entries.len() - 1))
1036                        }
1037                    });
1038                }
1039            }
1040        } else {
1041            self.selection = self.selection.and_then(|prev_selection| {
1042                if self.entries.is_empty() {
1043                    None
1044                } else {
1045                    Some(prev_selection.min(self.entries.len() - 1))
1046                }
1047            });
1048        }
1049
1050        let old_scroll_top = self.list_state.logical_scroll_top();
1051        self.list_state.reset(self.entries.len());
1052
1053        if scroll_to_top {
1054            self.list_state.scroll_to(ListOffset::default());
1055        } else {
1056            // Attempt to maintain the same scroll position.
1057            if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
1058                let new_scroll_top = self
1059                    .entries
1060                    .iter()
1061                    .position(|entry| entry == old_top_entry)
1062                    .map(|item_ix| ListOffset {
1063                        item_ix,
1064                        offset_in_item: old_scroll_top.offset_in_item,
1065                    })
1066                    .or_else(|| {
1067                        let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
1068                        let item_ix = self
1069                            .entries
1070                            .iter()
1071                            .position(|entry| entry == entry_after_old_top)?;
1072                        Some(ListOffset {
1073                            item_ix,
1074                            offset_in_item: Pixels::ZERO,
1075                        })
1076                    })
1077                    .or_else(|| {
1078                        let entry_before_old_top =
1079                            old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
1080                        let item_ix = self
1081                            .entries
1082                            .iter()
1083                            .position(|entry| entry == entry_before_old_top)?;
1084                        Some(ListOffset {
1085                            item_ix,
1086                            offset_in_item: Pixels::ZERO,
1087                        })
1088                    });
1089
1090                self.list_state
1091                    .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
1092            }
1093        }
1094
1095        cx.notify();
1096    }
1097
1098    fn render_call_participant(
1099        &self,
1100        user: &Arc<User>,
1101        peer_id: Option<PeerId>,
1102        is_pending: bool,
1103        role: proto::ChannelRole,
1104        is_selected: bool,
1105        cx: &mut Context<Self>,
1106    ) -> ListItem {
1107        let user_id = user.id;
1108        let is_current_user =
1109            self.user_store.read(cx).current_user().map(|user| user.id) == Some(user_id);
1110        let tooltip = format!("Follow {}", user.github_login);
1111
1112        let is_call_admin = ActiveCall::global(cx).read(cx).room().is_some_and(|room| {
1113            room.read(cx).local_participant().role == proto::ChannelRole::Admin
1114        });
1115
1116        let end_slot = if is_pending {
1117            Label::new("Calling").color(Color::Muted).into_any_element()
1118        } else if is_current_user {
1119            IconButton::new("leave-call", IconName::Exit)
1120                .icon_size(IconSize::Small)
1121                .tooltip(Tooltip::text("Leave Call"))
1122                .on_click(move |_, window, cx| Self::leave_call(window, cx))
1123                .into_any_element()
1124        } else if role == proto::ChannelRole::Guest {
1125            Label::new("Guest").color(Color::Muted).into_any_element()
1126        } else if role == proto::ChannelRole::Talker {
1127            Label::new("Mic only")
1128                .color(Color::Muted)
1129                .into_any_element()
1130        } else {
1131            Empty.into_any_element()
1132        };
1133
1134        ListItem::new(user.github_login.clone())
1135            .start_slot(Avatar::new(user.avatar_uri.clone()))
1136            .child(render_participant_name_and_handle(user))
1137            .toggle_state(is_selected)
1138            .end_slot(end_slot)
1139            .tooltip(Tooltip::text("Click to Follow"))
1140            .when_some(peer_id, |el, peer_id| {
1141                if role == proto::ChannelRole::Guest {
1142                    return el;
1143                }
1144                el.tooltip(Tooltip::text(tooltip.clone()))
1145                    .on_click(cx.listener(move |this, _, window, cx| {
1146                        this.workspace
1147                            .update(cx, |workspace, cx| workspace.follow(peer_id, window, cx))
1148                            .ok();
1149                    }))
1150            })
1151            .when(is_call_admin, |el| {
1152                el.on_secondary_mouse_down(cx.listener(
1153                    move |this, event: &MouseDownEvent, window, cx| {
1154                        this.deploy_participant_context_menu(
1155                            event.position,
1156                            user_id,
1157                            role,
1158                            window,
1159                            cx,
1160                        )
1161                    },
1162                ))
1163            })
1164    }
1165
1166    fn render_participant_project(
1167        &self,
1168        project_id: u64,
1169        worktree_root_names: &[String],
1170        host_user_id: u64,
1171        is_last: bool,
1172        is_selected: bool,
1173        window: &mut Window,
1174        cx: &mut Context<Self>,
1175    ) -> impl IntoElement {
1176        let project_name: SharedString = if worktree_root_names.is_empty() {
1177            "untitled".to_string()
1178        } else {
1179            worktree_root_names.join(", ")
1180        }
1181        .into();
1182
1183        ListItem::new(project_id as usize)
1184            .toggle_state(is_selected)
1185            .on_click(cx.listener(move |this, _, window, cx| {
1186                this.workspace
1187                    .update(cx, |workspace, cx| {
1188                        let app_state = workspace.app_state().clone();
1189                        workspace::join_in_room_project(project_id, host_user_id, app_state, cx)
1190                            .detach_and_prompt_err(
1191                                "Failed to join project",
1192                                window,
1193                                cx,
1194                                |error, _, _| Some(format!("{error:#}")),
1195                            );
1196                    })
1197                    .ok();
1198            }))
1199            .start_slot(
1200                h_flex()
1201                    .gap_1p5()
1202                    .child(render_tree_branch(is_last, false, window, cx))
1203                    .child(
1204                        Icon::new(IconName::Folder)
1205                            .size(IconSize::Small)
1206                            .color(Color::Muted),
1207                    ),
1208            )
1209            .child(Label::new(project_name.clone()))
1210            .tooltip(Tooltip::text(format!("Open {}", project_name)))
1211    }
1212
1213    fn render_participant_screen(
1214        &self,
1215        peer_id: Option<PeerId>,
1216        is_last: bool,
1217        is_selected: bool,
1218        window: &mut Window,
1219        cx: &mut Context<Self>,
1220    ) -> impl IntoElement {
1221        let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
1222
1223        ListItem::new(("screen", id))
1224            .toggle_state(is_selected)
1225            .start_slot(
1226                h_flex()
1227                    .gap_1p5()
1228                    .child(render_tree_branch(is_last, false, window, cx))
1229                    .child(
1230                        Icon::new(IconName::Screen)
1231                            .size(IconSize::Small)
1232                            .color(Color::Muted),
1233                    ),
1234            )
1235            .child(Label::new("Screen"))
1236            .when_some(peer_id, |this, _| {
1237                this.on_click(cx.listener(move |this, _, window, cx| {
1238                    this.workspace
1239                        .update(cx, |workspace, cx| {
1240                            workspace.open_shared_screen(peer_id.unwrap(), window, cx)
1241                        })
1242                        .ok();
1243                }))
1244                .tooltip(Tooltip::text("Open Shared Screen"))
1245            })
1246    }
1247
1248    fn take_editing_state(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1249        if self.channel_editing_state.take().is_some() {
1250            self.channel_name_editor.update(cx, |editor, cx| {
1251                editor.set_text("", window, cx);
1252            });
1253            true
1254        } else {
1255            false
1256        }
1257    }
1258
1259    fn render_channel_notes(
1260        &self,
1261        channel_id: ChannelId,
1262        is_selected: bool,
1263        window: &mut Window,
1264        cx: &mut Context<Self>,
1265    ) -> impl IntoElement {
1266        let channel_store = self.channel_store.read(cx);
1267        let has_channel_buffer_changed = channel_store.has_channel_buffer_changed(channel_id);
1268
1269        ListItem::new("channel-notes")
1270            .toggle_state(is_selected)
1271            .on_click(cx.listener(move |this, _, window, cx| {
1272                this.open_channel_notes(channel_id, window, cx);
1273            }))
1274            .start_slot(
1275                h_flex()
1276                    .relative()
1277                    .gap_1p5()
1278                    .child(render_tree_branch(false, true, window, cx))
1279                    .child(
1280                        h_flex()
1281                            .child(
1282                                Icon::new(IconName::Reader)
1283                                    .size(IconSize::Small)
1284                                    .color(Color::Muted),
1285                            )
1286                            .when(has_channel_buffer_changed, |this| {
1287                                this.child(
1288                                    div()
1289                                        .absolute()
1290                                        .top_neg_0p5()
1291                                        .right_0()
1292                                        .child(Indicator::dot().color(Color::Info)),
1293                                )
1294                            }),
1295                    ),
1296            )
1297            .child(Label::new("notes"))
1298            .tooltip(Tooltip::text("Open Channel Notes"))
1299    }
1300
1301    fn has_subchannels(&self, ix: usize) -> bool {
1302        self.entries.get(ix).is_some_and(|entry| {
1303            if let ListEntry::Channel { has_children, .. } = entry {
1304                *has_children
1305            } else {
1306                false
1307            }
1308        })
1309    }
1310
1311    fn deploy_participant_context_menu(
1312        &mut self,
1313        position: Point<Pixels>,
1314        user_id: u64,
1315        role: proto::ChannelRole,
1316        window: &mut Window,
1317        cx: &mut Context<Self>,
1318    ) {
1319        let this = cx.entity();
1320        if !(role == proto::ChannelRole::Guest
1321            || role == proto::ChannelRole::Talker
1322            || role == proto::ChannelRole::Member)
1323        {
1324            return;
1325        }
1326
1327        let context_menu = ContextMenu::build(window, cx, |mut context_menu, window, _| {
1328            if role == proto::ChannelRole::Guest {
1329                context_menu = context_menu.entry(
1330                    "Grant Mic Access",
1331                    None,
1332                    window.handler_for(&this, move |_, window, cx| {
1333                        ActiveCall::global(cx)
1334                            .update(cx, |call, cx| {
1335                                let Some(room) = call.room() else {
1336                                    return Task::ready(Ok(()));
1337                                };
1338                                room.update(cx, |room, cx| {
1339                                    room.set_participant_role(
1340                                        user_id,
1341                                        proto::ChannelRole::Talker,
1342                                        cx,
1343                                    )
1344                                })
1345                            })
1346                            .detach_and_prompt_err(
1347                                "Failed to grant mic access",
1348                                window,
1349                                cx,
1350                                |_, _, _| None,
1351                            )
1352                    }),
1353                );
1354            }
1355            if role == proto::ChannelRole::Guest || role == proto::ChannelRole::Talker {
1356                context_menu = context_menu.entry(
1357                    "Grant Write Access",
1358                    None,
1359                    window.handler_for(&this, move |_, window, cx| {
1360                        ActiveCall::global(cx)
1361                            .update(cx, |call, cx| {
1362                                let Some(room) = call.room() else {
1363                                    return Task::ready(Ok(()));
1364                                };
1365                                room.update(cx, |room, cx| {
1366                                    room.set_participant_role(
1367                                        user_id,
1368                                        proto::ChannelRole::Member,
1369                                        cx,
1370                                    )
1371                                })
1372                            })
1373                            .detach_and_prompt_err("Failed to grant write access", window, cx, |e, _, _| {
1374                                match e.error_code() {
1375                                    ErrorCode::NeedsCla => Some("This user has not yet signed the CLA at https://zed.dev/cla.".into()),
1376                                    _ => None,
1377                                }
1378                            })
1379                    }),
1380                );
1381            }
1382            if role == proto::ChannelRole::Member || role == proto::ChannelRole::Talker {
1383                let label = if role == proto::ChannelRole::Talker {
1384                    "Mute"
1385                } else {
1386                    "Revoke Access"
1387                };
1388                context_menu = context_menu.entry(
1389                    label,
1390                    None,
1391                    window.handler_for(&this, move |_, window, cx| {
1392                        ActiveCall::global(cx)
1393                            .update(cx, |call, cx| {
1394                                let Some(room) = call.room() else {
1395                                    return Task::ready(Ok(()));
1396                                };
1397                                room.update(cx, |room, cx| {
1398                                    room.set_participant_role(
1399                                        user_id,
1400                                        proto::ChannelRole::Guest,
1401                                        cx,
1402                                    )
1403                                })
1404                            })
1405                            .detach_and_prompt_err(
1406                                "Failed to revoke access",
1407                                window,
1408                                cx,
1409                                |_, _, _| None,
1410                            )
1411                    }),
1412                );
1413            }
1414
1415            context_menu
1416        });
1417
1418        window.focus(&context_menu.focus_handle(cx), cx);
1419        let subscription = cx.subscribe_in(
1420            &context_menu,
1421            window,
1422            |this, _, _: &DismissEvent, window, cx| {
1423                if this.context_menu.as_ref().is_some_and(|context_menu| {
1424                    context_menu.0.focus_handle(cx).contains_focused(window, cx)
1425                }) {
1426                    cx.focus_self(window);
1427                }
1428                this.context_menu.take();
1429                cx.notify();
1430            },
1431        );
1432        self.context_menu = Some((context_menu, position, subscription));
1433    }
1434
1435    fn deploy_channel_context_menu(
1436        &mut self,
1437        position: Point<Pixels>,
1438        channel_id: ChannelId,
1439        ix: usize,
1440        window: &mut Window,
1441        cx: &mut Context<Self>,
1442    ) {
1443        let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
1444            self.channel_store
1445                .read(cx)
1446                .channel_for_id(clipboard.channel_id)
1447                .map(|channel| channel.name.clone())
1448        });
1449        let this = cx.entity();
1450
1451        let context_menu = ContextMenu::build(window, cx, |mut context_menu, window, cx| {
1452            if self.has_subchannels(ix) {
1453                let expand_action_name = if self.is_channel_collapsed(channel_id) {
1454                    "Expand Subchannels"
1455                } else {
1456                    "Collapse Subchannels"
1457                };
1458                context_menu = context_menu.entry(
1459                    expand_action_name,
1460                    None,
1461                    window.handler_for(&this, move |this, window, cx| {
1462                        this.toggle_channel_collapsed(channel_id, window, cx)
1463                    }),
1464                );
1465            }
1466
1467            context_menu = context_menu
1468                .entry(
1469                    "Open Notes",
1470                    None,
1471                    window.handler_for(&this, move |this, window, cx| {
1472                        this.open_channel_notes(channel_id, window, cx)
1473                    }),
1474                )
1475                .entry(
1476                    "Copy Channel Link",
1477                    None,
1478                    window.handler_for(&this, move |this, _, cx| {
1479                        this.copy_channel_link(channel_id, cx)
1480                    }),
1481                )
1482                .entry(
1483                    "Copy Channel Notes Link",
1484                    None,
1485                    window.handler_for(&this, move |this, _, cx| {
1486                        this.copy_channel_notes_link(channel_id, cx)
1487                    }),
1488                )
1489                .separator()
1490                .entry(
1491                    if self.is_channel_favorited(channel_id, cx) {
1492                        "Remove from Favorites"
1493                    } else {
1494                        "Add to Favorites"
1495                    },
1496                    None,
1497                    window.handler_for(&this, move |this, _window, cx| {
1498                        this.toggle_favorite_channel(channel_id, cx)
1499                    }),
1500                );
1501
1502            let mut has_destructive_actions = false;
1503            if self.channel_store.read(cx).is_channel_admin(channel_id) {
1504                has_destructive_actions = true;
1505                context_menu = context_menu
1506                    .separator()
1507                    .entry(
1508                        "New Subchannel",
1509                        None,
1510                        window.handler_for(&this, move |this, window, cx| {
1511                            this.new_subchannel(channel_id, window, cx)
1512                        }),
1513                    )
1514                    .entry(
1515                        "Rename",
1516                        Some(Box::new(SecondaryConfirm)),
1517                        window.handler_for(&this, move |this, window, cx| {
1518                            this.rename_channel(channel_id, window, cx)
1519                        }),
1520                    );
1521
1522                if let Some(channel_name) = clipboard_channel_name {
1523                    context_menu = context_menu.separator().entry(
1524                        format!("Move '#{}' here", channel_name),
1525                        None,
1526                        window.handler_for(&this, move |this, window, cx| {
1527                            this.move_channel_on_clipboard(channel_id, window, cx)
1528                        }),
1529                    );
1530                }
1531
1532                if self.channel_store.read(cx).is_root_channel(channel_id) {
1533                    context_menu = context_menu.separator().entry(
1534                        "Manage Members",
1535                        None,
1536                        window.handler_for(&this, move |this, window, cx| {
1537                            this.manage_members(channel_id, window, cx)
1538                        }),
1539                    )
1540                } else {
1541                    context_menu = context_menu.entry(
1542                        "Move this channel",
1543                        None,
1544                        window.handler_for(&this, move |this, window, cx| {
1545                            this.start_move_channel(channel_id, window, cx)
1546                        }),
1547                    );
1548                    if self.channel_store.read(cx).is_public_channel(channel_id) {
1549                        context_menu = context_menu.separator().entry(
1550                            "Make Channel Private",
1551                            None,
1552                            window.handler_for(&this, move |this, window, cx| {
1553                                this.set_channel_visibility(
1554                                    channel_id,
1555                                    ChannelVisibility::Members,
1556                                    window,
1557                                    cx,
1558                                )
1559                            }),
1560                        )
1561                    } else {
1562                        context_menu = context_menu.separator().entry(
1563                            "Make Channel Public",
1564                            None,
1565                            window.handler_for(&this, move |this, window, cx| {
1566                                this.set_channel_visibility(
1567                                    channel_id,
1568                                    ChannelVisibility::Public,
1569                                    window,
1570                                    cx,
1571                                )
1572                            }),
1573                        )
1574                    }
1575                }
1576
1577                context_menu = context_menu.entry(
1578                    "Delete",
1579                    None,
1580                    window.handler_for(&this, move |this, window, cx| {
1581                        this.remove_channel(channel_id, window, cx)
1582                    }),
1583                );
1584            }
1585
1586            if self.channel_store.read(cx).is_root_channel(channel_id) {
1587                if !has_destructive_actions {
1588                    context_menu = context_menu.separator()
1589                }
1590                context_menu = context_menu.entry(
1591                    "Leave Channel",
1592                    None,
1593                    window.handler_for(&this, move |this, window, cx| {
1594                        this.leave_channel(channel_id, window, cx)
1595                    }),
1596                );
1597            }
1598
1599            context_menu
1600        });
1601
1602        window.focus(&context_menu.focus_handle(cx), cx);
1603        let subscription = cx.subscribe_in(
1604            &context_menu,
1605            window,
1606            |this, _, _: &DismissEvent, window, cx| {
1607                if this.context_menu.as_ref().is_some_and(|context_menu| {
1608                    context_menu.0.focus_handle(cx).contains_focused(window, cx)
1609                }) {
1610                    cx.focus_self(window);
1611                }
1612                this.context_menu.take();
1613                cx.notify();
1614            },
1615        );
1616        self.context_menu = Some((context_menu, position, subscription));
1617
1618        cx.notify();
1619    }
1620
1621    fn deploy_contact_context_menu(
1622        &mut self,
1623        position: Point<Pixels>,
1624        contact: Arc<Contact>,
1625        window: &mut Window,
1626        cx: &mut Context<Self>,
1627    ) {
1628        let this = cx.entity();
1629        let in_room = ActiveCall::global(cx).read(cx).room().is_some();
1630
1631        let context_menu = ContextMenu::build(window, cx, |mut context_menu, _, _| {
1632            let user_id = contact.user.id;
1633
1634            if contact.online && !contact.busy {
1635                let label = if in_room {
1636                    format!("Invite {} to join", contact.user.github_login)
1637                } else {
1638                    format!("Call {}", contact.user.github_login)
1639                };
1640                context_menu = context_menu.entry(label, None, {
1641                    let this = this.clone();
1642                    move |window, cx| {
1643                        this.update(cx, |this, cx| {
1644                            this.call(user_id, window, cx);
1645                        });
1646                    }
1647                });
1648            }
1649
1650            context_menu.entry("Remove Contact", None, {
1651                let this = this.clone();
1652                move |window, cx| {
1653                    this.update(cx, |this, cx| {
1654                        this.remove_contact(
1655                            contact.user.id,
1656                            &contact.user.github_login,
1657                            window,
1658                            cx,
1659                        );
1660                    });
1661                }
1662            })
1663        });
1664
1665        window.focus(&context_menu.focus_handle(cx), cx);
1666        let subscription = cx.subscribe_in(
1667            &context_menu,
1668            window,
1669            |this, _, _: &DismissEvent, window, cx| {
1670                if this.context_menu.as_ref().is_some_and(|context_menu| {
1671                    context_menu.0.focus_handle(cx).contains_focused(window, cx)
1672                }) {
1673                    cx.focus_self(window);
1674                }
1675                this.context_menu.take();
1676                cx.notify();
1677            },
1678        );
1679        self.context_menu = Some((context_menu, position, subscription));
1680
1681        cx.notify();
1682    }
1683
1684    fn reset_filter_editor_text(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1685        self.filter_editor.update(cx, |editor, cx| {
1686            if editor.buffer().read(cx).len(cx).0 > 0 {
1687                editor.set_text("", window, cx);
1688                true
1689            } else {
1690                false
1691            }
1692        })
1693    }
1694
1695    fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
1696        if cx.stop_active_drag(window) {
1697            return;
1698        } else if self.take_editing_state(window, cx) {
1699            window.focus(&self.filter_editor.focus_handle(cx), cx);
1700        } else if !self.reset_filter_editor_text(window, cx) {
1701            self.focus_handle.focus(window, cx);
1702        }
1703
1704        if self.context_menu.is_some() {
1705            self.context_menu.take();
1706            cx.notify();
1707        }
1708
1709        self.update_entries(false, cx);
1710    }
1711
1712    pub fn select_next(&mut self, _: &SelectNext, _: &mut Window, cx: &mut Context<Self>) {
1713        let ix = self.selection.map_or(0, |ix| ix + 1);
1714        if ix < self.entries.len() {
1715            self.selection = Some(ix);
1716        }
1717
1718        if let Some(ix) = self.selection {
1719            self.scroll_to_item(ix)
1720        }
1721        cx.notify();
1722    }
1723
1724    pub fn select_previous(&mut self, _: &SelectPrevious, _: &mut Window, cx: &mut Context<Self>) {
1725        let ix = self.selection.take().unwrap_or(0);
1726        if ix > 0 {
1727            self.selection = Some(ix - 1);
1728        }
1729
1730        if let Some(ix) = self.selection {
1731            self.scroll_to_item(ix)
1732        }
1733        cx.notify();
1734    }
1735
1736    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
1737        if self.confirm_channel_edit(window, cx) {
1738            return;
1739        }
1740
1741        if let Some(selection) = self.selection
1742            && let Some(entry) = self.entries.get(selection)
1743        {
1744            match entry {
1745                ListEntry::Header(section) => match section {
1746                    Section::ActiveCall => Self::leave_call(window, cx),
1747                    Section::Channels => self.new_root_channel(window, cx),
1748                    Section::Contacts => self.toggle_contact_finder(window, cx),
1749                    Section::FavoriteChannels
1750                    | Section::ContactRequests
1751                    | Section::Online
1752                    | Section::Offline
1753                    | Section::ChannelInvites => {
1754                        self.toggle_section_expanded(*section, cx);
1755                    }
1756                },
1757                ListEntry::Contact { contact, calling } => {
1758                    if contact.online && !contact.busy && !calling {
1759                        self.call(contact.user.id, window, cx);
1760                    }
1761                }
1762                ListEntry::ParticipantProject {
1763                    project_id,
1764                    host_user_id,
1765                    ..
1766                } => {
1767                    if let Some(workspace) = self.workspace.upgrade() {
1768                        let app_state = workspace.read(cx).app_state().clone();
1769                        workspace::join_in_room_project(*project_id, *host_user_id, app_state, cx)
1770                            .detach_and_prompt_err(
1771                                "Failed to join project",
1772                                window,
1773                                cx,
1774                                |error, _, _| Some(format!("{error:#}")),
1775                            );
1776                    }
1777                }
1778                ListEntry::ParticipantScreen { peer_id, .. } => {
1779                    let Some(peer_id) = peer_id else {
1780                        return;
1781                    };
1782                    if let Some(workspace) = self.workspace.upgrade() {
1783                        workspace.update(cx, |workspace, cx| {
1784                            workspace.open_shared_screen(*peer_id, window, cx)
1785                        });
1786                    }
1787                }
1788                ListEntry::Channel { channel, .. } => {
1789                    let is_active = maybe!({
1790                        let call_channel = ActiveCall::global(cx)
1791                            .read(cx)
1792                            .room()?
1793                            .read(cx)
1794                            .channel_id()?;
1795
1796                        Some(call_channel == channel.id)
1797                    })
1798                    .unwrap_or(false);
1799                    if is_active {
1800                        self.open_channel_notes(channel.id, window, cx)
1801                    } else {
1802                        self.join_channel(channel.id, window, cx)
1803                    }
1804                }
1805                ListEntry::ContactPlaceholder => self.toggle_contact_finder(window, cx),
1806                ListEntry::CallParticipant { user, peer_id, .. } => {
1807                    if Some(user) == self.user_store.read(cx).current_user().as_ref() {
1808                        Self::leave_call(window, cx);
1809                    } else if let Some(peer_id) = peer_id {
1810                        self.workspace
1811                            .update(cx, |workspace, cx| workspace.follow(*peer_id, window, cx))
1812                            .ok();
1813                    }
1814                }
1815                ListEntry::IncomingRequest(user) => {
1816                    self.respond_to_contact_request(user.id, true, window, cx)
1817                }
1818                ListEntry::ChannelInvite(channel) => {
1819                    self.respond_to_channel_invite(channel.id, true, cx)
1820                }
1821                ListEntry::ChannelNotes { channel_id } => {
1822                    self.open_channel_notes(*channel_id, window, cx)
1823                }
1824                ListEntry::OutgoingRequest(_) => {}
1825                ListEntry::ChannelEditor { .. } => {}
1826            }
1827        }
1828    }
1829
1830    fn insert_space(&mut self, _: &InsertSpace, window: &mut Window, cx: &mut Context<Self>) {
1831        if self.channel_editing_state.is_some() {
1832            self.channel_name_editor.update(cx, |editor, cx| {
1833                editor.insert(" ", window, cx);
1834            });
1835        } else if self.filter_editor.focus_handle(cx).is_focused(window) {
1836            self.filter_editor.update(cx, |editor, cx| {
1837                editor.insert(" ", window, cx);
1838            });
1839        }
1840    }
1841
1842    fn confirm_channel_edit(&mut self, window: &mut Window, cx: &mut Context<CollabPanel>) -> bool {
1843        if let Some(editing_state) = &mut self.channel_editing_state {
1844            match editing_state {
1845                ChannelEditingState::Create {
1846                    location,
1847                    pending_name,
1848                    ..
1849                } => {
1850                    if pending_name.is_some() {
1851                        return false;
1852                    }
1853                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1854
1855                    *pending_name = Some(channel_name.clone());
1856
1857                    let create = self.channel_store.update(cx, |channel_store, cx| {
1858                        channel_store.create_channel(&channel_name, *location, cx)
1859                    });
1860                    if location.is_none() {
1861                        cx.spawn_in(window, async move |this, cx| {
1862                            let channel_id = create.await?;
1863                            this.update_in(cx, |this, window, cx| {
1864                                this.show_channel_modal(
1865                                    channel_id,
1866                                    channel_modal::Mode::InviteMembers,
1867                                    window,
1868                                    cx,
1869                                )
1870                            })
1871                        })
1872                        .detach_and_prompt_err(
1873                            "Failed to create channel",
1874                            window,
1875                            cx,
1876                            |_, _, _| None,
1877                        );
1878                    } else {
1879                        create.detach_and_prompt_err(
1880                            "Failed to create channel",
1881                            window,
1882                            cx,
1883                            |_, _, _| None,
1884                        );
1885                    }
1886                    cx.notify();
1887                }
1888                ChannelEditingState::Rename {
1889                    location,
1890                    pending_name,
1891                } => {
1892                    if pending_name.is_some() {
1893                        return false;
1894                    }
1895                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1896                    *pending_name = Some(channel_name.clone());
1897
1898                    self.channel_store
1899                        .update(cx, |channel_store, cx| {
1900                            channel_store.rename(*location, &channel_name, cx)
1901                        })
1902                        .detach();
1903                    cx.notify();
1904                }
1905            }
1906            cx.focus_self(window);
1907            true
1908        } else {
1909            false
1910        }
1911    }
1912
1913    fn toggle_section_expanded(&mut self, section: Section, cx: &mut Context<Self>) {
1914        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
1915            self.collapsed_sections.remove(ix);
1916        } else {
1917            self.collapsed_sections.push(section);
1918        }
1919        self.update_entries(false, cx);
1920    }
1921
1922    fn collapse_selected_channel(
1923        &mut self,
1924        _: &CollapseSelectedChannel,
1925        window: &mut Window,
1926        cx: &mut Context<Self>,
1927    ) {
1928        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
1929            return;
1930        };
1931
1932        if self.is_channel_collapsed(channel_id) {
1933            return;
1934        }
1935
1936        self.toggle_channel_collapsed(channel_id, window, cx);
1937    }
1938
1939    fn expand_selected_channel(
1940        &mut self,
1941        _: &ExpandSelectedChannel,
1942        window: &mut Window,
1943        cx: &mut Context<Self>,
1944    ) {
1945        let Some(id) = self.selected_channel().map(|channel| channel.id) else {
1946            return;
1947        };
1948
1949        if !self.is_channel_collapsed(id) {
1950            return;
1951        }
1952
1953        self.toggle_channel_collapsed(id, window, cx)
1954    }
1955
1956    fn toggle_channel_collapsed(
1957        &mut self,
1958        channel_id: ChannelId,
1959        window: &mut Window,
1960        cx: &mut Context<Self>,
1961    ) {
1962        match self.collapsed_channels.binary_search(&channel_id) {
1963            Ok(ix) => {
1964                self.collapsed_channels.remove(ix);
1965            }
1966            Err(ix) => {
1967                self.collapsed_channels.insert(ix, channel_id);
1968            }
1969        };
1970        self.serialize(cx);
1971        self.update_entries(true, cx);
1972        cx.notify();
1973        cx.focus_self(window);
1974    }
1975
1976    fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
1977        self.collapsed_channels.binary_search(&channel_id).is_ok()
1978    }
1979
1980    pub fn toggle_favorite_channel(&mut self, channel_id: ChannelId, cx: &mut Context<Self>) {
1981        self.channel_store.update(cx, |store, cx| {
1982            store.toggle_favorite_channel(channel_id, cx);
1983        });
1984        self.persist_favorites(cx);
1985    }
1986
1987    fn is_channel_favorited(&self, channel_id: ChannelId, cx: &App) -> bool {
1988        self.channel_store.read(cx).is_channel_favorited(channel_id)
1989    }
1990
1991    fn persist_filter_occupied_channels(&mut self, cx: &mut Context<Self>) {
1992        let is_enabled = self.filter_occupied_channels;
1993        let kvp_store = KeyValueStore::global(cx);
1994        self.pending_filter_serialization = cx.background_spawn(
1995            async move {
1996                if is_enabled {
1997                    kvp_store
1998                        .write_kvp(FILTER_OCCUPIED_CHANNELS_KEY.to_string(), "1".to_string())
1999                        .await?;
2000                } else {
2001                    kvp_store
2002                        .delete_kvp(FILTER_OCCUPIED_CHANNELS_KEY.to_string())
2003                        .await?;
2004                }
2005                anyhow::Ok(())
2006            }
2007            .log_err(),
2008        );
2009    }
2010
2011    fn persist_favorites(&mut self, cx: &mut Context<Self>) {
2012        let favorite_ids: Vec<u64> = self
2013            .channel_store
2014            .read(cx)
2015            .favorite_channel_ids()
2016            .iter()
2017            .map(|id| id.0)
2018            .collect();
2019        let kvp_store = KeyValueStore::global(cx);
2020        self.pending_favorites_serialization = cx.background_spawn(
2021            async move {
2022                let json = serde_json::to_string(&favorite_ids)?;
2023                kvp_store
2024                    .write_kvp(FAVORITE_CHANNELS_KEY.to_string(), json)
2025                    .await?;
2026                anyhow::Ok(())
2027            }
2028            .log_err(),
2029        );
2030    }
2031
2032    fn leave_call(window: &mut Window, cx: &mut App) {
2033        ActiveCall::global(cx)
2034            .update(cx, |call, cx| call.hang_up(cx))
2035            .detach_and_prompt_err("Failed to hang up", window, cx, |_, _, _| None);
2036    }
2037
2038    fn toggle_contact_finder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2039        if let Some(workspace) = self.workspace.upgrade() {
2040            workspace.update(cx, |workspace, cx| {
2041                workspace.toggle_modal(window, cx, |window, cx| {
2042                    let mut finder = ContactFinder::new(self.user_store.clone(), window, cx);
2043                    finder.set_query(self.filter_editor.read(cx).text(cx), window, cx);
2044                    finder
2045                });
2046            });
2047        }
2048    }
2049
2050    fn new_root_channel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2051        self.channel_editing_state = Some(ChannelEditingState::Create {
2052            location: None,
2053            pending_name: None,
2054        });
2055        self.update_entries(false, cx);
2056        self.select_channel_editor();
2057        window.focus(&self.channel_name_editor.focus_handle(cx), cx);
2058        cx.notify();
2059    }
2060
2061    fn select_channel_editor(&mut self) {
2062        self.selection = self
2063            .entries
2064            .iter()
2065            .position(|entry| matches!(entry, ListEntry::ChannelEditor { .. }));
2066    }
2067
2068    fn new_subchannel(
2069        &mut self,
2070        channel_id: ChannelId,
2071        window: &mut Window,
2072        cx: &mut Context<Self>,
2073    ) {
2074        self.collapsed_channels
2075            .retain(|channel| *channel != channel_id);
2076        self.channel_editing_state = Some(ChannelEditingState::Create {
2077            location: Some(channel_id),
2078            pending_name: None,
2079        });
2080        self.update_entries(false, cx);
2081        self.select_channel_editor();
2082        window.focus(&self.channel_name_editor.focus_handle(cx), cx);
2083        cx.notify();
2084    }
2085
2086    fn manage_members(
2087        &mut self,
2088        channel_id: ChannelId,
2089        window: &mut Window,
2090        cx: &mut Context<Self>,
2091    ) {
2092        self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, window, cx);
2093    }
2094
2095    fn remove_selected_channel(&mut self, _: &Remove, window: &mut Window, cx: &mut Context<Self>) {
2096        if let Some(channel) = self.selected_channel() {
2097            self.remove_channel(channel.id, window, cx)
2098        }
2099    }
2100
2101    fn rename_selected_channel(
2102        &mut self,
2103        _: &SecondaryConfirm,
2104        window: &mut Window,
2105        cx: &mut Context<Self>,
2106    ) {
2107        if let Some(channel) = self.selected_channel() {
2108            self.rename_channel(channel.id, window, cx);
2109        }
2110    }
2111
2112    fn rename_channel(
2113        &mut self,
2114        channel_id: ChannelId,
2115        window: &mut Window,
2116        cx: &mut Context<Self>,
2117    ) {
2118        let channel_store = self.channel_store.read(cx);
2119        if !channel_store.is_channel_admin(channel_id) {
2120            return;
2121        }
2122        if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() {
2123            self.channel_editing_state = Some(ChannelEditingState::Rename {
2124                location: channel_id,
2125                pending_name: None,
2126            });
2127            self.channel_name_editor.update(cx, |editor, cx| {
2128                editor.set_text(channel.name.clone(), window, cx);
2129                editor.select_all(&Default::default(), window, cx);
2130            });
2131            window.focus(&self.channel_name_editor.focus_handle(cx), cx);
2132            self.update_entries(false, cx);
2133            self.select_channel_editor();
2134        }
2135    }
2136
2137    fn open_selected_channel_notes(
2138        &mut self,
2139        _: &OpenSelectedChannelNotes,
2140        window: &mut Window,
2141        cx: &mut Context<Self>,
2142    ) {
2143        if let Some(channel) = self.selected_channel() {
2144            self.open_channel_notes(channel.id, window, cx);
2145        }
2146    }
2147
2148    pub fn toggle_selected_channel_favorite(
2149        &mut self,
2150        _: &ToggleSelectedChannelFavorite,
2151        _window: &mut Window,
2152        cx: &mut Context<Self>,
2153    ) {
2154        if let Some(channel) = self.selected_channel() {
2155            self.toggle_favorite_channel(channel.id, cx);
2156        }
2157    }
2158
2159    fn set_channel_visibility(
2160        &mut self,
2161        channel_id: ChannelId,
2162        visibility: ChannelVisibility,
2163        window: &mut Window,
2164        cx: &mut Context<Self>,
2165    ) {
2166        self.channel_store
2167            .update(cx, |channel_store, cx| {
2168                channel_store.set_channel_visibility(channel_id, visibility, cx)
2169            })
2170            .detach_and_prompt_err("Failed to set channel visibility", window, cx, |e, _, _| match e.error_code() {
2171                ErrorCode::BadPublicNesting =>
2172                    if e.error_tag("direction") == Some("parent") {
2173                        Some("To make a channel public, its parent channel must be public.".to_string())
2174                    } else {
2175                        Some("To make a channel private, all of its subchannels must be private.".to_string())
2176                    },
2177                _ => None
2178            });
2179    }
2180
2181    fn start_move_channel(
2182        &mut self,
2183        channel_id: ChannelId,
2184        _window: &mut Window,
2185        _cx: &mut Context<Self>,
2186    ) {
2187        self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
2188    }
2189
2190    fn start_move_selected_channel(
2191        &mut self,
2192        _: &StartMoveChannel,
2193        window: &mut Window,
2194        cx: &mut Context<Self>,
2195    ) {
2196        if let Some(channel) = self.selected_channel() {
2197            self.start_move_channel(channel.id, window, cx);
2198        }
2199    }
2200
2201    fn move_channel_on_clipboard(
2202        &mut self,
2203        to_channel_id: ChannelId,
2204        window: &mut Window,
2205        cx: &mut Context<CollabPanel>,
2206    ) {
2207        if let Some(clipboard) = self.channel_clipboard.take() {
2208            self.move_channel(clipboard.channel_id, to_channel_id, window, cx)
2209        }
2210    }
2211
2212    fn move_channel(
2213        &self,
2214        channel_id: ChannelId,
2215        to: ChannelId,
2216        window: &mut Window,
2217        cx: &mut Context<Self>,
2218    ) {
2219        self.channel_store
2220            .update(cx, |channel_store, cx| {
2221                channel_store.move_channel(channel_id, to, cx)
2222            })
2223            .detach_and_prompt_err("Failed to move channel", window, cx, |e, _, _| {
2224                match e.error_code() {
2225                    ErrorCode::BadPublicNesting => {
2226                        Some("Public channels must have public parents".into())
2227                    }
2228                    ErrorCode::CircularNesting => {
2229                        Some("You cannot move a channel into itself".into())
2230                    }
2231                    ErrorCode::WrongMoveTarget => {
2232                        Some("You cannot move a channel into a different root channel".into())
2233                    }
2234                    _ => None,
2235                }
2236            })
2237    }
2238
2239    pub fn move_channel_up(
2240        &mut self,
2241        _: &MoveChannelUp,
2242        window: &mut Window,
2243        cx: &mut Context<Self>,
2244    ) {
2245        self.reorder_selected_channel(Direction::Up, window, cx);
2246    }
2247
2248    pub fn move_channel_down(
2249        &mut self,
2250        _: &MoveChannelDown,
2251        window: &mut Window,
2252        cx: &mut Context<Self>,
2253    ) {
2254        self.reorder_selected_channel(Direction::Down, window, cx);
2255    }
2256
2257    fn reorder_selected_channel(
2258        &mut self,
2259        direction: Direction,
2260        window: &mut Window,
2261        cx: &mut Context<Self>,
2262    ) {
2263        if let Some(channel) = self.selected_channel().cloned() {
2264            if self.selected_entry_is_favorite() {
2265                self.reorder_favorite(channel.id, direction, cx);
2266                return;
2267            }
2268
2269            self.channel_store.update(cx, |store, cx| {
2270                store
2271                    .reorder_channel(channel.id, direction, cx)
2272                    .detach_and_prompt_err(
2273                        match direction {
2274                            Direction::Up => "Failed to move channel up",
2275                            Direction::Down => "Failed to move channel down",
2276                        },
2277                        window,
2278                        cx,
2279                        |_, _, _| None,
2280                    )
2281            });
2282        }
2283    }
2284
2285    pub fn reorder_favorite(
2286        &mut self,
2287        channel_id: ChannelId,
2288        direction: Direction,
2289        cx: &mut Context<Self>,
2290    ) {
2291        self.channel_store.update(cx, |store, cx| {
2292            let favorite_ids = store.favorite_channel_ids();
2293            let Some(channel_index) = favorite_ids.iter().position(|id| *id == channel_id) else {
2294                return;
2295            };
2296            let target_channel_index = match direction {
2297                Direction::Up => channel_index.checked_sub(1),
2298                Direction::Down => {
2299                    let next = channel_index + 1;
2300                    (next < favorite_ids.len()).then_some(next)
2301                }
2302            };
2303            if let Some(target_channel_index) = target_channel_index {
2304                let mut new_ids = favorite_ids.to_vec();
2305                new_ids.swap(channel_index, target_channel_index);
2306                store.set_favorite_channel_ids(new_ids, cx);
2307            }
2308        });
2309        self.persist_favorites(cx);
2310    }
2311
2312    fn open_channel_notes(
2313        &mut self,
2314        channel_id: ChannelId,
2315        window: &mut Window,
2316        cx: &mut Context<Self>,
2317    ) {
2318        if let Some(workspace) = self.workspace.upgrade() {
2319            ChannelView::open(channel_id, None, workspace, window, cx).detach();
2320        }
2321    }
2322
2323    fn show_inline_context_menu(
2324        &mut self,
2325        _: &Secondary,
2326        window: &mut Window,
2327        cx: &mut Context<Self>,
2328    ) {
2329        let Some(bounds) = self
2330            .selection
2331            .and_then(|ix| self.list_state.bounds_for_item(ix))
2332        else {
2333            return;
2334        };
2335
2336        if let Some(channel) = self.selected_channel() {
2337            self.deploy_channel_context_menu(
2338                bounds.center(),
2339                channel.id,
2340                self.selection.unwrap(),
2341                window,
2342                cx,
2343            );
2344            cx.stop_propagation();
2345            return;
2346        };
2347
2348        if let Some(contact) = self.selected_contact() {
2349            self.deploy_contact_context_menu(bounds.center(), contact, window, cx);
2350            cx.stop_propagation();
2351        }
2352    }
2353
2354    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
2355        let mut dispatch_context = KeyContext::new_with_defaults();
2356        dispatch_context.add("CollabPanel");
2357        dispatch_context.add("menu");
2358
2359        let identifier = if self.channel_name_editor.focus_handle(cx).is_focused(window)
2360            || self.filter_editor.focus_handle(cx).is_focused(window)
2361        {
2362            "editing"
2363        } else {
2364            "not_editing"
2365        };
2366
2367        dispatch_context.add(identifier);
2368        dispatch_context
2369    }
2370
2371    fn selected_channel(&self) -> Option<&Arc<Channel>> {
2372        self.selection
2373            .and_then(|ix| self.entries.get(ix))
2374            .and_then(|entry| match entry {
2375                ListEntry::Channel { channel, .. } => Some(channel),
2376                _ => None,
2377            })
2378    }
2379
2380    fn selected_entry_is_favorite(&self) -> bool {
2381        self.selection
2382            .and_then(|ix| self.entries.get(ix))
2383            .is_some_and(|entry| {
2384                matches!(
2385                    entry,
2386                    ListEntry::Channel {
2387                        is_favorite: true,
2388                        ..
2389                    }
2390                )
2391            })
2392    }
2393
2394    fn selected_contact(&self) -> Option<Arc<Contact>> {
2395        self.selection
2396            .and_then(|ix| self.entries.get(ix))
2397            .and_then(|entry| match entry {
2398                ListEntry::Contact { contact, .. } => Some(contact.clone()),
2399                _ => None,
2400            })
2401    }
2402
2403    fn show_channel_modal(
2404        &mut self,
2405        channel_id: ChannelId,
2406        mode: channel_modal::Mode,
2407        window: &mut Window,
2408        cx: &mut Context<Self>,
2409    ) {
2410        let workspace = self.workspace.clone();
2411        let user_store = self.user_store.clone();
2412        let channel_store = self.channel_store.clone();
2413
2414        cx.spawn_in(window, async move |_, cx| {
2415            workspace.update_in(cx, |workspace, window, cx| {
2416                workspace.toggle_modal(window, cx, |window, cx| {
2417                    ChannelModal::new(
2418                        user_store.clone(),
2419                        channel_store.clone(),
2420                        channel_id,
2421                        mode,
2422                        window,
2423                        cx,
2424                    )
2425                });
2426            })
2427        })
2428        .detach();
2429    }
2430
2431    fn leave_channel(&self, channel_id: ChannelId, window: &mut Window, cx: &mut Context<Self>) {
2432        let Some(user_id) = self.user_store.read(cx).current_user().map(|u| u.id) else {
2433            return;
2434        };
2435        let Some(channel) = self.channel_store.read(cx).channel_for_id(channel_id) else {
2436            return;
2437        };
2438        let prompt_message = format!("Are you sure you want to leave \"#{}\"?", channel.name);
2439        let answer = window.prompt(
2440            PromptLevel::Warning,
2441            &prompt_message,
2442            None,
2443            &["Leave", "Cancel"],
2444            cx,
2445        );
2446        cx.spawn_in(window, async move |this, cx| {
2447            if answer.await? != 0 {
2448                return Ok(());
2449            }
2450            this.update(cx, |this, cx| {
2451                this.channel_store.update(cx, |channel_store, cx| {
2452                    channel_store.remove_member(channel_id, user_id, cx)
2453                })
2454            })?
2455            .await
2456        })
2457        .detach_and_prompt_err("Failed to leave channel", window, cx, |_, _, _| None)
2458    }
2459
2460    fn remove_channel(
2461        &mut self,
2462        channel_id: ChannelId,
2463        window: &mut Window,
2464        cx: &mut Context<Self>,
2465    ) {
2466        let channel_store = self.channel_store.clone();
2467        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
2468            let prompt_message = format!(
2469                "Are you sure you want to remove the channel \"{}\"?",
2470                channel.name
2471            );
2472            let answer = window.prompt(
2473                PromptLevel::Warning,
2474                &prompt_message,
2475                None,
2476                &["Remove", "Cancel"],
2477                cx,
2478            );
2479            let workspace = self.workspace.clone();
2480            cx.spawn_in(window, async move |this, mut cx| {
2481                if answer.await? == 0 {
2482                    channel_store
2483                        .update(cx, |channels, _| channels.remove_channel(channel_id))
2484                        .await
2485                        .notify_workspace_async_err(workspace, &mut cx);
2486                    this.update_in(cx, |_, window, cx| cx.focus_self(window))
2487                        .ok();
2488                }
2489                anyhow::Ok(())
2490            })
2491            .detach();
2492        }
2493    }
2494
2495    fn remove_contact(
2496        &mut self,
2497        user_id: u64,
2498        github_login: &str,
2499        window: &mut Window,
2500        cx: &mut Context<Self>,
2501    ) {
2502        let user_store = self.user_store.clone();
2503        let prompt_message = format!(
2504            "Are you sure you want to remove \"{}\" from your contacts?",
2505            github_login
2506        );
2507        let answer = window.prompt(
2508            PromptLevel::Warning,
2509            &prompt_message,
2510            None,
2511            &["Remove", "Cancel"],
2512            cx,
2513        );
2514        let workspace = self.workspace.clone();
2515        cx.spawn_in(window, async move |_, mut cx| {
2516            if answer.await? == 0 {
2517                user_store
2518                    .update(cx, |store, cx| store.remove_contact(user_id, cx))
2519                    .await
2520                    .notify_workspace_async_err(workspace, &mut cx);
2521            }
2522            anyhow::Ok(())
2523        })
2524        .detach_and_prompt_err("Failed to remove contact", window, cx, |_, _, _| None);
2525    }
2526
2527    fn respond_to_contact_request(
2528        &mut self,
2529        user_id: u64,
2530        accept: bool,
2531        window: &mut Window,
2532        cx: &mut Context<Self>,
2533    ) {
2534        self.user_store
2535            .update(cx, |store, cx| {
2536                store.respond_to_contact_request(user_id, accept, cx)
2537            })
2538            .detach_and_prompt_err(
2539                "Failed to respond to contact request",
2540                window,
2541                cx,
2542                |_, _, _| None,
2543            );
2544    }
2545
2546    fn respond_to_channel_invite(
2547        &mut self,
2548        channel_id: ChannelId,
2549        accept: bool,
2550        cx: &mut Context<Self>,
2551    ) {
2552        self.channel_store
2553            .update(cx, |store, cx| {
2554                store.respond_to_channel_invite(channel_id, accept, cx)
2555            })
2556            .detach();
2557    }
2558
2559    fn call(&mut self, recipient_user_id: u64, window: &mut Window, cx: &mut Context<Self>) {
2560        ActiveCall::global(cx)
2561            .update(cx, |call, cx| {
2562                call.invite(recipient_user_id, Some(self.project.clone()), cx)
2563            })
2564            .detach_and_prompt_err("Call failed", window, cx, |_, _, _| None);
2565    }
2566
2567    fn join_channel(&self, channel_id: ChannelId, window: &mut Window, cx: &mut Context<Self>) {
2568        let Some(workspace) = self.workspace.upgrade() else {
2569            return;
2570        };
2571
2572        let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() else {
2573            return;
2574        };
2575        workspace::join_channel(
2576            channel_id,
2577            workspace.read(cx).app_state().clone(),
2578            Some(handle),
2579            Some(self.workspace.clone()),
2580            cx,
2581        )
2582        .detach_and_prompt_err("Failed to join channel", window, cx, |_, _, _| None)
2583    }
2584
2585    fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut Context<Self>) {
2586        let channel_store = self.channel_store.read(cx);
2587        let Some(channel) = channel_store.channel_for_id(channel_id) else {
2588            return;
2589        };
2590        let item = ClipboardItem::new_string(channel.link(cx));
2591        cx.write_to_clipboard(item)
2592    }
2593
2594    fn copy_channel_notes_link(&mut self, channel_id: ChannelId, cx: &mut Context<Self>) {
2595        let channel_store = self.channel_store.read(cx);
2596        let Some(channel) = channel_store.channel_for_id(channel_id) else {
2597            return;
2598        };
2599        let item = ClipboardItem::new_string(channel.notes_link(None, cx));
2600        cx.write_to_clipboard(item)
2601    }
2602
2603    fn render_signed_out(&mut self, cx: &mut Context<Self>) -> Div {
2604        let collab_blurb = "Work with your team in realtime with collaborative editing, voice, shared notes and more.";
2605
2606        // Two distinct "not connected" states:
2607        //   - Authenticated (has credentials): user just needs to connect.
2608        //   - Unauthenticated (no credentials): user needs to sign in via GitHub.
2609        let is_authenticated = self.client.user_id().is_some();
2610        let status = *self.client.status().borrow();
2611        let is_busy = status.is_signing_in();
2612
2613        let (button_id, button_label, button_icon) = if is_authenticated {
2614            (
2615                "connect",
2616                if is_busy { "Connecting…" } else { "Connect" },
2617                IconName::Public,
2618            )
2619        } else {
2620            (
2621                "sign_in",
2622                if is_busy {
2623                    "Signing in…"
2624                } else {
2625                    "Sign In with GitHub"
2626                },
2627                IconName::Github,
2628            )
2629        };
2630
2631        v_flex()
2632            .p_4()
2633            .gap_4()
2634            .size_full()
2635            .text_center()
2636            .justify_center()
2637            .child(Label::new(collab_blurb))
2638            .child(
2639                Button::new(button_id, button_label)
2640                    .full_width()
2641                    .start_icon(Icon::new(button_icon).color(Color::Muted))
2642                    .style(ButtonStyle::Outlined)
2643                    .disabled(is_busy)
2644                    .on_click(cx.listener(|this, _, window, cx| {
2645                        let client = this.client.clone();
2646                        let workspace = this.workspace.clone();
2647                        cx.spawn_in(window, async move |_, mut cx| {
2648                            client
2649                                .connect(true, &mut cx)
2650                                .await
2651                                .into_response()
2652                                .notify_workspace_async_err(workspace, &mut cx);
2653                        })
2654                        .detach()
2655                    })),
2656            )
2657    }
2658
2659    fn render_list_entry(
2660        &mut self,
2661        ix: usize,
2662        window: &mut Window,
2663        cx: &mut Context<Self>,
2664    ) -> AnyElement {
2665        let entry = &self.entries[ix];
2666
2667        let is_selected = self.selection == Some(ix);
2668        match entry {
2669            ListEntry::Header(section) => {
2670                let is_collapsed = self.collapsed_sections.contains(section);
2671                self.render_header(*section, is_selected, is_collapsed, cx)
2672                    .into_any_element()
2673            }
2674            ListEntry::Contact { contact, calling } => self
2675                .render_contact(contact, *calling, is_selected, cx)
2676                .into_any_element(),
2677            ListEntry::ContactPlaceholder => self
2678                .render_contact_placeholder(is_selected, cx)
2679                .into_any_element(),
2680            ListEntry::IncomingRequest(user) => self
2681                .render_contact_request(user, true, is_selected, cx)
2682                .into_any_element(),
2683            ListEntry::OutgoingRequest(user) => self
2684                .render_contact_request(user, false, is_selected, cx)
2685                .into_any_element(),
2686            ListEntry::Channel {
2687                channel,
2688                depth,
2689                has_children,
2690                string_match,
2691                ..
2692            } => self
2693                .render_channel(
2694                    channel,
2695                    *depth,
2696                    *has_children,
2697                    is_selected,
2698                    ix,
2699                    string_match.as_ref(),
2700                    cx,
2701                )
2702                .into_any_element(),
2703            ListEntry::ChannelEditor { depth } => self
2704                .render_channel_editor(*depth, window, cx)
2705                .into_any_element(),
2706            ListEntry::ChannelInvite(channel) => self
2707                .render_channel_invite(channel, is_selected, cx)
2708                .into_any_element(),
2709            ListEntry::CallParticipant {
2710                user,
2711                peer_id,
2712                is_pending,
2713                role,
2714            } => self
2715                .render_call_participant(user, *peer_id, *is_pending, *role, is_selected, cx)
2716                .into_any_element(),
2717            ListEntry::ParticipantProject {
2718                project_id,
2719                worktree_root_names,
2720                host_user_id,
2721                is_last,
2722            } => self
2723                .render_participant_project(
2724                    *project_id,
2725                    worktree_root_names,
2726                    *host_user_id,
2727                    *is_last,
2728                    is_selected,
2729                    window,
2730                    cx,
2731                )
2732                .into_any_element(),
2733            ListEntry::ParticipantScreen { peer_id, is_last } => self
2734                .render_participant_screen(*peer_id, *is_last, is_selected, window, cx)
2735                .into_any_element(),
2736            ListEntry::ChannelNotes { channel_id } => self
2737                .render_channel_notes(*channel_id, is_selected, window, cx)
2738                .into_any_element(),
2739        }
2740    }
2741
2742    fn render_signed_in(&mut self, _: &mut Window, cx: &mut Context<Self>) -> Div {
2743        self.channel_store.update(cx, |channel_store, _| {
2744            channel_store.initialize();
2745        });
2746
2747        let has_query = !self.filter_editor.read(cx).text(cx).is_empty();
2748
2749        v_flex()
2750            .size_full()
2751            .gap_1()
2752            .child(
2753                h_flex()
2754                    .p_2()
2755                    .h(Tab::container_height(cx))
2756                    .gap_1p5()
2757                    .border_b_1()
2758                    .border_color(cx.theme().colors().border)
2759                    .child(
2760                        Icon::new(IconName::MagnifyingGlass)
2761                            .size(IconSize::Small)
2762                            .color(Color::Muted),
2763                    )
2764                    .child(self.render_filter_input(&self.filter_editor, cx))
2765                    .when(has_query, |this| {
2766                        this.pr_2p5().child(
2767                            IconButton::new("clear_filter", IconName::Close)
2768                                .shape(IconButtonShape::Square)
2769                                .tooltip(Tooltip::text("Clear Filter"))
2770                                .on_click(cx.listener(|this, _, window, cx| {
2771                                    this.reset_filter_editor_text(window, cx);
2772                                    cx.notify();
2773                                })),
2774                        )
2775                    }),
2776            )
2777            .child(
2778                list(
2779                    self.list_state.clone(),
2780                    cx.processor(Self::render_list_entry),
2781                )
2782                .size_full(),
2783            )
2784    }
2785
2786    fn render_filter_input(
2787        &self,
2788        editor: &Entity<Editor>,
2789        cx: &mut Context<Self>,
2790    ) -> impl IntoElement {
2791        let settings = ThemeSettings::get_global(cx);
2792        let text_style = TextStyle {
2793            color: if editor.read(cx).read_only(cx) {
2794                cx.theme().colors().text_disabled
2795            } else {
2796                cx.theme().colors().text
2797            },
2798            font_family: settings.ui_font.family.clone(),
2799            font_features: settings.ui_font.features.clone(),
2800            font_fallbacks: settings.ui_font.fallbacks.clone(),
2801            font_size: rems(0.875).into(),
2802            font_weight: settings.ui_font.weight,
2803            font_style: FontStyle::Normal,
2804            line_height: relative(1.3),
2805            ..Default::default()
2806        };
2807
2808        EditorElement::new(
2809            editor,
2810            EditorStyle {
2811                local_player: cx.theme().players().local(),
2812                text: text_style,
2813                ..Default::default()
2814            },
2815        )
2816    }
2817
2818    fn render_header(
2819        &self,
2820        section: Section,
2821        is_selected: bool,
2822        is_collapsed: bool,
2823        cx: &mut Context<Self>,
2824    ) -> impl IntoElement {
2825        let mut channel_link = None;
2826        let mut channel_tooltip_text = None;
2827        let mut channel_icon = None;
2828
2829        let text = match section {
2830            Section::ActiveCall => {
2831                let channel_name = maybe!({
2832                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
2833
2834                    let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
2835
2836                    channel_link = Some(channel.link(cx));
2837                    (channel_icon, channel_tooltip_text) = match channel.visibility {
2838                        proto::ChannelVisibility::Public => {
2839                            (Some("icons/public.svg"), Some("Copy public channel link."))
2840                        }
2841                        proto::ChannelVisibility::Members => {
2842                            (Some("icons/hash.svg"), Some("Copy private channel link."))
2843                        }
2844                    };
2845
2846                    Some(channel.name.as_ref())
2847                });
2848
2849                if let Some(name) = channel_name {
2850                    SharedString::from(name.to_string())
2851                } else {
2852                    SharedString::from("Current Call")
2853                }
2854            }
2855            Section::FavoriteChannels => SharedString::from("Favorites"),
2856            Section::ContactRequests => SharedString::from("Requests"),
2857            Section::Contacts => SharedString::from("Contacts"),
2858            Section::Channels => SharedString::from("Channels"),
2859            Section::ChannelInvites => SharedString::from("Invites"),
2860            Section::Online => SharedString::from("Online"),
2861            Section::Offline => SharedString::from("Offline"),
2862        };
2863
2864        let button = match section {
2865            Section::ActiveCall => channel_link.map(|channel_link| {
2866                CopyButton::new("copy-channel-link", channel_link)
2867                    .visible_on_hover("section-header")
2868                    .tooltip_label("Copy Channel Link")
2869                    .into_any_element()
2870            }),
2871            Section::Contacts => Some(
2872                IconButton::new("add-contact", IconName::Plus)
2873                    .icon_size(IconSize::Small)
2874                    .on_click(
2875                        cx.listener(|this, _, window, cx| this.toggle_contact_finder(window, cx)),
2876                    )
2877                    .tooltip(Tooltip::text("Search for new contact"))
2878                    .into_any_element(),
2879            ),
2880            Section::Channels => {
2881                Some(
2882                    h_flex()
2883                        .child(
2884                            IconButton::new("filter-occupied-channels", IconName::ListFilter)
2885                                .icon_size(IconSize::Small)
2886                                .toggle_state(self.filter_occupied_channels)
2887                                .on_click(cx.listener(|this, _, _window, cx| {
2888                                    this.filter_occupied_channels = !this.filter_occupied_channels;
2889                                    this.update_entries(true, cx);
2890                                    this.persist_filter_occupied_channels(cx);
2891                                }))
2892                                .tooltip(Tooltip::text(if self.filter_occupied_channels {
2893                                    "Show All Channels"
2894                                } else {
2895                                    "Show Occupied Channels"
2896                                })),
2897                        )
2898                        .child(
2899                            IconButton::new("add-channel", IconName::Plus)
2900                                .icon_size(IconSize::Small)
2901                                .on_click(cx.listener(|this, _, window, cx| {
2902                                    this.new_root_channel(window, cx)
2903                                }))
2904                                .tooltip(Tooltip::text("Create Channel")),
2905                        )
2906                        .into_any_element(),
2907                )
2908            }
2909            _ => None,
2910        };
2911
2912        let can_collapse = match section {
2913            Section::ActiveCall
2914            | Section::Channels
2915            | Section::Contacts
2916            | Section::FavoriteChannels => false,
2917
2918            Section::ChannelInvites
2919            | Section::ContactRequests
2920            | Section::Online
2921            | Section::Offline => true,
2922        };
2923
2924        h_flex().w_full().group("section-header").child(
2925            ListHeader::new(text)
2926                .when(can_collapse, |header| {
2927                    header.toggle(Some(!is_collapsed)).on_toggle(cx.listener(
2928                        move |this, _, _, cx| {
2929                            this.toggle_section_expanded(section, cx);
2930                        },
2931                    ))
2932                })
2933                .inset(true)
2934                .end_slot::<AnyElement>(button)
2935                .toggle_state(is_selected),
2936        )
2937    }
2938
2939    fn render_contact(
2940        &self,
2941        contact: &Arc<Contact>,
2942        calling: bool,
2943        is_selected: bool,
2944        cx: &mut Context<Self>,
2945    ) -> impl IntoElement {
2946        let online = contact.online;
2947        let busy = contact.busy || calling;
2948        let github_login = contact.user.github_login.clone();
2949        let item = ListItem::new(github_login.clone())
2950            .indent_level(1)
2951            .indent_step_size(px(20.))
2952            .toggle_state(is_selected)
2953            .child(
2954                h_flex()
2955                    .w_full()
2956                    .justify_between()
2957                    .child(render_participant_name_and_handle(&contact.user))
2958                    .when(calling, |el| {
2959                        el.child(Label::new("Calling").color(Color::Muted))
2960                    })
2961                    .when(!calling, |el| {
2962                        el.child(
2963                            IconButton::new("contact context menu", IconName::Ellipsis)
2964                                .icon_color(Color::Muted)
2965                                .visible_on_hover("")
2966                                .on_click(cx.listener({
2967                                    let contact = contact.clone();
2968                                    move |this, event: &ClickEvent, window, cx| {
2969                                        this.deploy_contact_context_menu(
2970                                            event.position(),
2971                                            contact.clone(),
2972                                            window,
2973                                            cx,
2974                                        );
2975                                    }
2976                                })),
2977                        )
2978                    }),
2979            )
2980            .on_secondary_mouse_down(cx.listener({
2981                let contact = contact.clone();
2982                move |this, event: &MouseDownEvent, window, cx| {
2983                    this.deploy_contact_context_menu(event.position, contact.clone(), window, cx);
2984                }
2985            }))
2986            .start_slot(
2987                // todo handle contacts with no avatar
2988                Avatar::new(contact.user.avatar_uri.clone())
2989                    .indicator::<AvatarAvailabilityIndicator>(if online {
2990                        Some(AvatarAvailabilityIndicator::new(match busy {
2991                            true => ui::CollaboratorAvailability::Busy,
2992                            false => ui::CollaboratorAvailability::Free,
2993                        }))
2994                    } else {
2995                        None
2996                    }),
2997            );
2998
2999        div()
3000            .id(github_login.clone())
3001            .group("")
3002            .child(item)
3003            .tooltip(move |_, cx| {
3004                let text = if !online {
3005                    format!(" {} is offline", &github_login)
3006                } else if busy {
3007                    format!(" {} is on a call", &github_login)
3008                } else {
3009                    let room = ActiveCall::global(cx).read(cx).room();
3010                    if room.is_some() {
3011                        format!("Invite {} to join call", &github_login)
3012                    } else {
3013                        format!("Call {}", &github_login)
3014                    }
3015                };
3016                Tooltip::simple(text, cx)
3017            })
3018    }
3019
3020    fn render_contact_request(
3021        &self,
3022        user: &Arc<User>,
3023        is_incoming: bool,
3024        is_selected: bool,
3025        cx: &mut Context<Self>,
3026    ) -> impl IntoElement {
3027        let github_login = user.github_login.clone();
3028        let user_id = user.id;
3029        let is_response_pending = self.user_store.read(cx).is_contact_request_pending(user);
3030        let color = if is_response_pending {
3031            Color::Muted
3032        } else {
3033            Color::Default
3034        };
3035
3036        let controls = if is_incoming {
3037            vec![
3038                IconButton::new("decline-contact", IconName::Close)
3039                    .on_click(cx.listener(move |this, _, window, cx| {
3040                        this.respond_to_contact_request(user_id, false, window, cx);
3041                    }))
3042                    .icon_color(color)
3043                    .tooltip(Tooltip::text("Decline invite")),
3044                IconButton::new("accept-contact", IconName::Check)
3045                    .on_click(cx.listener(move |this, _, window, cx| {
3046                        this.respond_to_contact_request(user_id, true, window, cx);
3047                    }))
3048                    .icon_color(color)
3049                    .tooltip(Tooltip::text("Accept invite")),
3050            ]
3051        } else {
3052            let github_login = github_login.clone();
3053            vec![
3054                IconButton::new("remove_contact", IconName::Close)
3055                    .on_click(cx.listener(move |this, _, window, cx| {
3056                        this.remove_contact(user_id, &github_login, window, cx);
3057                    }))
3058                    .icon_color(color)
3059                    .tooltip(Tooltip::text("Cancel invite")),
3060            ]
3061        };
3062
3063        ListItem::new(github_login.clone())
3064            .indent_level(1)
3065            .indent_step_size(px(20.))
3066            .toggle_state(is_selected)
3067            .child(
3068                h_flex()
3069                    .w_full()
3070                    .justify_between()
3071                    .child(Label::new(github_login))
3072                    .child(h_flex().children(controls)),
3073            )
3074            .start_slot(Avatar::new(user.avatar_uri.clone()))
3075    }
3076
3077    fn render_channel_invite(
3078        &self,
3079        channel: &Arc<Channel>,
3080        is_selected: bool,
3081        cx: &mut Context<Self>,
3082    ) -> ListItem {
3083        let channel_id = channel.id;
3084        let response_is_pending = self
3085            .channel_store
3086            .read(cx)
3087            .has_pending_channel_invite_response(channel);
3088        let color = if response_is_pending {
3089            Color::Muted
3090        } else {
3091            Color::Default
3092        };
3093
3094        let controls = [
3095            IconButton::new("reject-invite", IconName::Close)
3096                .on_click(cx.listener(move |this, _, _, cx| {
3097                    this.respond_to_channel_invite(channel_id, false, cx);
3098                }))
3099                .icon_color(color)
3100                .tooltip(Tooltip::text("Decline invite")),
3101            IconButton::new("accept-invite", IconName::Check)
3102                .on_click(cx.listener(move |this, _, _, cx| {
3103                    this.respond_to_channel_invite(channel_id, true, cx);
3104                }))
3105                .icon_color(color)
3106                .tooltip(Tooltip::text("Accept invite")),
3107        ];
3108
3109        ListItem::new(("channel-invite", channel.id.0 as usize))
3110            .toggle_state(is_selected)
3111            .child(
3112                h_flex()
3113                    .w_full()
3114                    .justify_between()
3115                    .child(Label::new(channel.name.clone()))
3116                    .child(h_flex().children(controls)),
3117            )
3118            .start_slot(
3119                Icon::new(IconName::Hash)
3120                    .size(IconSize::Small)
3121                    .color(Color::Muted),
3122            )
3123    }
3124
3125    fn render_contact_placeholder(&self, is_selected: bool, cx: &mut Context<Self>) -> ListItem {
3126        ListItem::new("contact-placeholder")
3127            .child(Icon::new(IconName::Plus))
3128            .child(Label::new("Add a Contact"))
3129            .toggle_state(is_selected)
3130            .on_click(cx.listener(|this, _, window, cx| this.toggle_contact_finder(window, cx)))
3131    }
3132
3133    fn render_channel(
3134        &self,
3135        channel: &Channel,
3136        depth: usize,
3137        has_children: bool,
3138        is_selected: bool,
3139        ix: usize,
3140        string_match: Option<&StringMatch>,
3141        cx: &mut Context<Self>,
3142    ) -> impl IntoElement {
3143        let channel_id = channel.id;
3144
3145        let is_active = maybe!({
3146            let call_channel = ActiveCall::global(cx)
3147                .read(cx)
3148                .room()?
3149                .read(cx)
3150                .channel_id()?;
3151            Some(call_channel == channel_id)
3152        })
3153        .unwrap_or(false);
3154        let channel_store = self.channel_store.read(cx);
3155        let is_public = channel_store
3156            .channel_for_id(channel_id)
3157            .map(|channel| channel.visibility)
3158            == Some(proto::ChannelVisibility::Public);
3159        let disclosed =
3160            has_children.then(|| self.collapsed_channels.binary_search(&channel.id).is_err());
3161
3162        let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);
3163
3164        const FACEPILE_LIMIT: usize = 3;
3165        let participants = self.channel_store.read(cx).channel_participants(channel_id);
3166
3167        let face_pile = if participants.is_empty() {
3168            None
3169        } else {
3170            let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
3171            let result = Facepile::new(
3172                participants
3173                    .iter()
3174                    .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
3175                    .take(FACEPILE_LIMIT)
3176                    .chain(if extra_count > 0 {
3177                        Some(
3178                            Label::new(format!("+{extra_count}"))
3179                                .ml_2()
3180                                .into_any_element(),
3181                        )
3182                    } else {
3183                        None
3184                    })
3185                    .collect::<SmallVec<_>>(),
3186            );
3187
3188            Some(result)
3189        };
3190
3191        let width = self
3192            .workspace
3193            .read_with(cx, |workspace, cx| {
3194                workspace
3195                    .panel_size_state::<Self>(cx)
3196                    .and_then(|size_state| size_state.size)
3197            })
3198            .ok()
3199            .flatten()
3200            .unwrap_or(px(240.));
3201        let root_id = channel.root_id();
3202
3203        let is_favorited = self.is_channel_favorited(channel_id, cx);
3204        let (favorite_icon, favorite_color, favorite_tooltip) = if is_favorited {
3205            (IconName::StarFilled, Color::Accent, "Remove from Favorites")
3206        } else {
3207            (IconName::Star, Color::Default, "Add to Favorites")
3208        };
3209
3210        h_flex()
3211            .id(ix)
3212            .group("")
3213            .w_full()
3214            .overflow_hidden()
3215            .when(!channel.is_root_channel(), |el| {
3216                el.on_drag(channel.clone(), move |channel, _, _, cx| {
3217                    cx.new(|_| DraggedChannelView {
3218                        channel: channel.clone(),
3219                        width,
3220                    })
3221                })
3222            })
3223            .drag_over::<Channel>({
3224                move |style, dragged_channel: &Channel, _window, cx| {
3225                    if dragged_channel.root_id() == root_id {
3226                        style.bg(cx.theme().colors().ghost_element_hover)
3227                    } else {
3228                        style
3229                    }
3230                }
3231            })
3232            .on_drop(
3233                cx.listener(move |this, dragged_channel: &Channel, window, cx| {
3234                    if dragged_channel.root_id() != root_id {
3235                        return;
3236                    }
3237                    this.move_channel(dragged_channel.id, channel_id, window, cx);
3238                }),
3239            )
3240            .child(
3241                ListItem::new(ix)
3242                    // Add one level of depth for the disclosure arrow.
3243                    .indent_level(depth + 1)
3244                    .indent_step_size(px(20.))
3245                    .toggle_state(is_selected || is_active)
3246                    .toggle(disclosed)
3247                    .on_toggle(cx.listener(move |this, _, window, cx| {
3248                        this.toggle_channel_collapsed(channel_id, window, cx)
3249                    }))
3250                    .on_click(cx.listener(move |this, _, window, cx| {
3251                        if is_active {
3252                            this.open_channel_notes(channel_id, window, cx)
3253                        } else {
3254                            this.join_channel(channel_id, window, cx)
3255                        }
3256                    }))
3257                    .on_secondary_mouse_down(cx.listener(
3258                        move |this, event: &MouseDownEvent, window, cx| {
3259                            this.deploy_channel_context_menu(
3260                                event.position,
3261                                channel_id,
3262                                ix,
3263                                window,
3264                                cx,
3265                            )
3266                        },
3267                    ))
3268                    .child(
3269                        h_flex()
3270                            .id(format!("inside-{}", channel_id.0))
3271                            .w_full()
3272                            .gap_1()
3273                            .child(
3274                                div()
3275                                    .relative()
3276                                    .child(
3277                                        Icon::new(if is_public {
3278                                            IconName::Public
3279                                        } else {
3280                                            IconName::Hash
3281                                        })
3282                                        .size(IconSize::Small)
3283                                        .color(Color::Muted),
3284                                    )
3285                                    .children(has_notes_notification.then(|| {
3286                                        div()
3287                                            .w_1p5()
3288                                            .absolute()
3289                                            .right(px(-1.))
3290                                            .top(px(-1.))
3291                                            .child(Indicator::dot().color(Color::Info))
3292                                    })),
3293                            )
3294                            .child(
3295                                h_flex()
3296                                    .id(channel_id.0 as usize)
3297                                    .child(match string_match {
3298                                        None => Label::new(channel.name.clone()).into_any_element(),
3299                                        Some(string_match) => HighlightedLabel::new(
3300                                            channel.name.clone(),
3301                                            string_match.positions.clone(),
3302                                        )
3303                                        .into_any_element(),
3304                                    })
3305                                    .children(face_pile.map(|face_pile| face_pile.p_1())),
3306                            )
3307                            .tooltip({
3308                                let channel_store = self.channel_store.clone();
3309                                move |_window, cx| {
3310                                    cx.new(|_| JoinChannelTooltip {
3311                                        channel_store: channel_store.clone(),
3312                                        channel_id,
3313                                        has_notes_notification,
3314                                    })
3315                                    .into()
3316                                }
3317                            }),
3318                    ),
3319            )
3320            .child(
3321                h_flex()
3322                    .visible_on_hover("")
3323                    .h_full()
3324                    .absolute()
3325                    .right_0()
3326                    .px_1()
3327                    .gap_px()
3328                    .rounded_l_md()
3329                    .bg(cx.theme().colors().background)
3330                    .child({
3331                        let focus_handle = self.focus_handle.clone();
3332                        IconButton::new("channel_favorite", favorite_icon)
3333                            .icon_size(IconSize::Small)
3334                            .icon_color(favorite_color)
3335                            .on_click(cx.listener(move |this, _, _window, cx| {
3336                                this.toggle_favorite_channel(channel_id, cx)
3337                            }))
3338                            .tooltip(move |_window, cx| {
3339                                Tooltip::for_action_in(
3340                                    favorite_tooltip,
3341                                    &ToggleSelectedChannelFavorite,
3342                                    &focus_handle,
3343                                    cx,
3344                                )
3345                            })
3346                    })
3347                    .child({
3348                        let focus_handle = self.focus_handle.clone();
3349                        IconButton::new("channel_notes", IconName::Reader)
3350                            .icon_size(IconSize::Small)
3351                            .on_click(cx.listener(move |this, _, window, cx| {
3352                                this.open_channel_notes(channel_id, window, cx)
3353                            }))
3354                            .tooltip(move |_window, cx| {
3355                                Tooltip::for_action_in(
3356                                    "Open Channel Notes",
3357                                    &OpenSelectedChannelNotes,
3358                                    &focus_handle,
3359                                    cx,
3360                                )
3361                            })
3362                    }),
3363            )
3364    }
3365
3366    fn render_channel_editor(
3367        &self,
3368        depth: usize,
3369        _window: &mut Window,
3370        _cx: &mut Context<Self>,
3371    ) -> impl IntoElement {
3372        let item = ListItem::new("channel-editor")
3373            .inset(false)
3374            // Add one level of depth for the disclosure arrow.
3375            .indent_level(depth + 1)
3376            .indent_step_size(px(20.))
3377            .start_slot(
3378                Icon::new(IconName::Hash)
3379                    .size(IconSize::Small)
3380                    .color(Color::Muted),
3381            );
3382
3383        if let Some(pending_name) = self
3384            .channel_editing_state
3385            .as_ref()
3386            .and_then(|state| state.pending_name())
3387        {
3388            item.child(Label::new(pending_name))
3389        } else {
3390            item.child(self.channel_name_editor.clone())
3391        }
3392    }
3393}
3394
3395fn render_tree_branch(
3396    is_last: bool,
3397    overdraw: bool,
3398    window: &mut Window,
3399    cx: &mut App,
3400) -> impl IntoElement {
3401    let rem_size = window.rem_size();
3402    let line_height = window.text_style().line_height_in_pixels(rem_size);
3403    let thickness = px(1.);
3404    let color = cx.theme().colors().icon_disabled;
3405
3406    canvas(
3407        |_, _, _| {},
3408        move |bounds, _, window, _| {
3409            let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
3410            let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
3411            let right = bounds.right();
3412            let top = bounds.top();
3413
3414            window.paint_quad(fill(
3415                Bounds::from_corners(
3416                    point(start_x, top),
3417                    point(
3418                        start_x + thickness,
3419                        if is_last {
3420                            start_y
3421                        } else {
3422                            bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
3423                        },
3424                    ),
3425                ),
3426                color,
3427            ));
3428            window.paint_quad(fill(
3429                Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
3430                color,
3431            ));
3432        },
3433    )
3434    .w(rem_size)
3435    .h(line_height - px(2.))
3436}
3437
3438fn render_participant_name_and_handle(user: &User) -> impl IntoElement {
3439    Label::new(if let Some(ref display_name) = user.name {
3440        format!("{display_name} ({})", user.github_login)
3441    } else {
3442        user.github_login.to_string()
3443    })
3444}
3445
3446impl Render for CollabPanel {
3447    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3448        let status = *self.client.status().borrow();
3449
3450        v_flex()
3451            .key_context(self.dispatch_context(window, cx))
3452            .on_action(cx.listener(CollabPanel::cancel))
3453            .on_action(cx.listener(CollabPanel::select_next))
3454            .on_action(cx.listener(CollabPanel::select_previous))
3455            .on_action(cx.listener(CollabPanel::confirm))
3456            .on_action(cx.listener(CollabPanel::insert_space))
3457            .on_action(cx.listener(CollabPanel::remove_selected_channel))
3458            .on_action(cx.listener(CollabPanel::show_inline_context_menu))
3459            .on_action(cx.listener(CollabPanel::rename_selected_channel))
3460            .on_action(cx.listener(CollabPanel::open_selected_channel_notes))
3461            .on_action(cx.listener(CollabPanel::toggle_selected_channel_favorite))
3462            .on_action(cx.listener(CollabPanel::collapse_selected_channel))
3463            .on_action(cx.listener(CollabPanel::expand_selected_channel))
3464            .on_action(cx.listener(CollabPanel::start_move_selected_channel))
3465            .on_action(cx.listener(CollabPanel::move_channel_up))
3466            .on_action(cx.listener(CollabPanel::move_channel_down))
3467            .track_focus(&self.focus_handle)
3468            .size_full()
3469            .child(if !status.is_or_was_connected() || status.is_signing_in() {
3470                self.render_signed_out(cx)
3471            } else {
3472                self.render_signed_in(window, cx)
3473            })
3474            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
3475                deferred(
3476                    anchored()
3477                        .position(*position)
3478                        .anchor(gpui::Corner::TopLeft)
3479                        .child(menu.clone()),
3480                )
3481                .with_priority(1)
3482            }))
3483    }
3484}
3485
3486impl EventEmitter<PanelEvent> for CollabPanel {}
3487
3488impl Panel for CollabPanel {
3489    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
3490        CollaborationPanelSettings::get_global(cx).dock
3491    }
3492
3493    fn position_is_valid(&self, position: DockPosition) -> bool {
3494        matches!(position, DockPosition::Left | DockPosition::Right)
3495    }
3496
3497    fn set_position(
3498        &mut self,
3499        position: DockPosition,
3500        _window: &mut Window,
3501        cx: &mut Context<Self>,
3502    ) {
3503        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
3504            settings.collaboration_panel.get_or_insert_default().dock = Some(position.into())
3505        });
3506    }
3507
3508    fn default_size(&self, _window: &Window, cx: &App) -> Pixels {
3509        CollaborationPanelSettings::get_global(cx).default_width
3510    }
3511
3512    fn icon(&self, _window: &Window, cx: &App) -> Option<ui::IconName> {
3513        CollaborationPanelSettings::get_global(cx)
3514            .button
3515            .then_some(ui::IconName::UserGroup)
3516    }
3517
3518    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
3519        Some("Collab Panel")
3520    }
3521
3522    fn toggle_action(&self) -> Box<dyn gpui::Action> {
3523        Box::new(ToggleFocus)
3524    }
3525
3526    fn persistent_name() -> &'static str {
3527        "CollabPanel"
3528    }
3529
3530    fn panel_key() -> &'static str {
3531        COLLABORATION_PANEL_KEY
3532    }
3533
3534    fn activation_priority(&self) -> u32 {
3535        5
3536    }
3537}
3538
3539impl Focusable for CollabPanel {
3540    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
3541        self.filter_editor.focus_handle(cx)
3542    }
3543}
3544
3545impl PartialEq for ListEntry {
3546    fn eq(&self, other: &Self) -> bool {
3547        match self {
3548            ListEntry::Header(section_1) => {
3549                if let ListEntry::Header(section_2) = other {
3550                    return section_1 == section_2;
3551                }
3552            }
3553            ListEntry::CallParticipant { user: user_1, .. } => {
3554                if let ListEntry::CallParticipant { user: user_2, .. } = other {
3555                    return user_1.id == user_2.id;
3556                }
3557            }
3558            ListEntry::ParticipantProject {
3559                project_id: project_id_1,
3560                ..
3561            } => {
3562                if let ListEntry::ParticipantProject {
3563                    project_id: project_id_2,
3564                    ..
3565                } = other
3566                {
3567                    return project_id_1 == project_id_2;
3568                }
3569            }
3570            ListEntry::ParticipantScreen {
3571                peer_id: peer_id_1, ..
3572            } => {
3573                if let ListEntry::ParticipantScreen {
3574                    peer_id: peer_id_2, ..
3575                } = other
3576                {
3577                    return peer_id_1 == peer_id_2;
3578                }
3579            }
3580            ListEntry::Channel {
3581                channel: channel_1,
3582                is_favorite: is_favorite_1,
3583                ..
3584            } => {
3585                if let ListEntry::Channel {
3586                    channel: channel_2,
3587                    is_favorite: is_favorite_2,
3588                    ..
3589                } = other
3590                {
3591                    return channel_1.id == channel_2.id && is_favorite_1 == is_favorite_2;
3592                }
3593            }
3594            ListEntry::ChannelNotes { channel_id } => {
3595                if let ListEntry::ChannelNotes {
3596                    channel_id: other_id,
3597                } = other
3598                {
3599                    return channel_id == other_id;
3600                }
3601            }
3602            ListEntry::ChannelInvite(channel_1) => {
3603                if let ListEntry::ChannelInvite(channel_2) = other {
3604                    return channel_1.id == channel_2.id;
3605                }
3606            }
3607            ListEntry::IncomingRequest(user_1) => {
3608                if let ListEntry::IncomingRequest(user_2) = other {
3609                    return user_1.id == user_2.id;
3610                }
3611            }
3612            ListEntry::OutgoingRequest(user_1) => {
3613                if let ListEntry::OutgoingRequest(user_2) = other {
3614                    return user_1.id == user_2.id;
3615                }
3616            }
3617            ListEntry::Contact {
3618                contact: contact_1, ..
3619            } => {
3620                if let ListEntry::Contact {
3621                    contact: contact_2, ..
3622                } = other
3623                {
3624                    return contact_1.user.id == contact_2.user.id;
3625                }
3626            }
3627            ListEntry::ChannelEditor { depth } => {
3628                if let ListEntry::ChannelEditor { depth: other_depth } = other {
3629                    return depth == other_depth;
3630                }
3631            }
3632            ListEntry::ContactPlaceholder => {
3633                if let ListEntry::ContactPlaceholder = other {
3634                    return true;
3635                }
3636            }
3637        }
3638        false
3639    }
3640}
3641
3642struct DraggedChannelView {
3643    channel: Channel,
3644    width: Pixels,
3645}
3646
3647impl Render for DraggedChannelView {
3648    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3649        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
3650        h_flex()
3651            .font_family(ui_font)
3652            .bg(cx.theme().colors().background)
3653            .w(self.width)
3654            .p_1()
3655            .gap_1()
3656            .child(
3657                Icon::new(
3658                    if self.channel.visibility == proto::ChannelVisibility::Public {
3659                        IconName::Public
3660                    } else {
3661                        IconName::Hash
3662                    },
3663                )
3664                .size(IconSize::Small)
3665                .color(Color::Muted),
3666            )
3667            .child(Label::new(self.channel.name.clone()))
3668    }
3669}
3670
3671struct JoinChannelTooltip {
3672    channel_store: Entity<ChannelStore>,
3673    channel_id: ChannelId,
3674    #[allow(unused)]
3675    has_notes_notification: bool,
3676}
3677
3678impl Render for JoinChannelTooltip {
3679    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3680        tooltip_container(cx, |container, cx| {
3681            let participants = self
3682                .channel_store
3683                .read(cx)
3684                .channel_participants(self.channel_id);
3685
3686            container
3687                .child(Label::new("Join Channel"))
3688                .children(participants.iter().map(|participant| {
3689                    h_flex()
3690                        .gap_2()
3691                        .child(Avatar::new(participant.avatar_uri.clone()))
3692                        .child(render_participant_name_and_handle(participant))
3693                }))
3694        })
3695    }
3696}
3697
3698#[cfg(any(test, feature = "test-support"))]
3699impl CollabPanel {
3700    pub fn entries_as_strings(&self) -> Vec<String> {
3701        let mut string_entries = Vec::new();
3702        for (index, entry) in self.entries.iter().enumerate() {
3703            let selected_marker = if self.selection == Some(index) {
3704                "  <== selected"
3705            } else {
3706                ""
3707            };
3708            match entry {
3709                ListEntry::Header(section) => {
3710                    let name = match section {
3711                        Section::ActiveCall => "Active Call",
3712                        Section::FavoriteChannels => "Favorites",
3713                        Section::Channels => "Channels",
3714                        Section::ChannelInvites => "Channel Invites",
3715                        Section::ContactRequests => "Contact Requests",
3716                        Section::Contacts => "Contacts",
3717                        Section::Online => "Online",
3718                        Section::Offline => "Offline",
3719                    };
3720                    string_entries.push(format!("[{name}]"));
3721                }
3722                ListEntry::Channel {
3723                    channel,
3724                    depth,
3725                    has_children,
3726                    ..
3727                } => {
3728                    let indent = "  ".repeat(*depth + 1);
3729                    let icon = if *has_children {
3730                        "v "
3731                    } else if channel.visibility == proto::ChannelVisibility::Public {
3732                        "🛜 "
3733                    } else {
3734                        "#️⃣ "
3735                    };
3736                    string_entries.push(format!("{indent}{icon}{}{selected_marker}", channel.name));
3737                }
3738                ListEntry::ChannelNotes { .. } => {
3739                    string_entries.push(format!("  (notes){selected_marker}"));
3740                }
3741                ListEntry::ChannelEditor { depth } => {
3742                    let indent = "  ".repeat(*depth + 1);
3743                    string_entries.push(format!("{indent}[editor]{selected_marker}"));
3744                }
3745                ListEntry::ChannelInvite(channel) => {
3746                    string_entries.push(format!("  (invite) #{}{selected_marker}", channel.name));
3747                }
3748                ListEntry::CallParticipant { user, .. } => {
3749                    string_entries.push(format!("  {}{selected_marker}", user.github_login));
3750                }
3751                ListEntry::ParticipantProject {
3752                    worktree_root_names,
3753                    ..
3754                } => {
3755                    string_entries.push(format!(
3756                        "    {}{selected_marker}",
3757                        worktree_root_names.join(", ")
3758                    ));
3759                }
3760                ListEntry::ParticipantScreen { .. } => {
3761                    string_entries.push(format!("    (screen){selected_marker}"));
3762                }
3763                ListEntry::IncomingRequest(user) => {
3764                    string_entries.push(format!(
3765                        "  (incoming) {}{selected_marker}",
3766                        user.github_login
3767                    ));
3768                }
3769                ListEntry::OutgoingRequest(user) => {
3770                    string_entries.push(format!(
3771                        "  (outgoing) {}{selected_marker}",
3772                        user.github_login
3773                    ));
3774                }
3775                ListEntry::Contact { contact, .. } => {
3776                    string_entries
3777                        .push(format!("  {}{selected_marker}", contact.user.github_login));
3778                }
3779                ListEntry::ContactPlaceholder => {}
3780            }
3781        }
3782        string_entries
3783    }
3784}