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