panel.rs

   1mod channel_modal;
   2mod contact_finder;
   3mod panel_settings;
   4
   5use anyhow::Result;
   6use call::ActiveCall;
   7use client::{proto::PeerId, Channel, ChannelStore, Client, Contact, User, UserStore};
   8use contact_finder::build_contact_finder;
   9use context_menu::{ContextMenu, ContextMenuItem};
  10use db::kvp::KEY_VALUE_STORE;
  11use editor::{Cancel, Editor};
  12use futures::StreamExt;
  13use fuzzy::{match_strings, StringMatchCandidate};
  14use gpui::{
  15    actions,
  16    elements::{
  17        Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState,
  18        MouseEventHandler, Orientation, Padding, ParentElement, Stack, Svg,
  19    },
  20    geometry::{
  21        rect::RectF,
  22        vector::{vec2f, Vector2F},
  23    },
  24    impl_actions,
  25    platform::{CursorStyle, MouseButton, PromptLevel},
  26    serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle,
  27    Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
  28};
  29use menu::{Confirm, SelectNext, SelectPrev};
  30use panel_settings::{ChannelsPanelDockPosition, ChannelsPanelSettings};
  31use project::{Fs, Project};
  32use serde_derive::{Deserialize, Serialize};
  33use settings::SettingsStore;
  34use std::{mem, sync::Arc};
  35use theme::IconButton;
  36use util::{ResultExt, TryFutureExt};
  37use workspace::{
  38    dock::{DockPosition, Panel},
  39    item::ItemHandle,
  40    Workspace,
  41};
  42
  43#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  44struct RemoveChannel {
  45    channel_id: u64,
  46}
  47
  48#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  49struct NewChannel {
  50    channel_id: u64,
  51}
  52
  53actions!(collab_panel, [ToggleFocus]);
  54
  55impl_actions!(collab_panel, [RemoveChannel, NewChannel]);
  56
  57const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel";
  58
  59pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
  60    settings::register::<panel_settings::ChannelsPanelSettings>(cx);
  61    contact_finder::init(cx);
  62    channel_modal::init(cx);
  63
  64    cx.add_action(CollabPanel::cancel);
  65    cx.add_action(CollabPanel::select_next);
  66    cx.add_action(CollabPanel::select_prev);
  67    cx.add_action(CollabPanel::confirm);
  68    cx.add_action(CollabPanel::remove_channel);
  69    cx.add_action(CollabPanel::new_subchannel);
  70}
  71
  72#[derive(Debug, Default)]
  73pub struct ChannelEditingState {
  74    parent_id: Option<u64>,
  75}
  76
  77pub struct CollabPanel {
  78    width: Option<f32>,
  79    fs: Arc<dyn Fs>,
  80    has_focus: bool,
  81    pending_serialization: Task<Option<()>>,
  82    context_menu: ViewHandle<ContextMenu>,
  83    filter_editor: ViewHandle<Editor>,
  84    channel_name_editor: ViewHandle<Editor>,
  85    channel_editing_state: Option<ChannelEditingState>,
  86    entries: Vec<ListEntry>,
  87    selection: Option<usize>,
  88    user_store: ModelHandle<UserStore>,
  89    channel_store: ModelHandle<ChannelStore>,
  90    project: ModelHandle<Project>,
  91    match_candidates: Vec<StringMatchCandidate>,
  92    list_state: ListState<Self>,
  93    subscriptions: Vec<Subscription>,
  94    collapsed_sections: Vec<Section>,
  95    workspace: WeakViewHandle<Workspace>,
  96}
  97
  98#[derive(Serialize, Deserialize)]
  99struct SerializedChannelsPanel {
 100    width: Option<f32>,
 101}
 102
 103#[derive(Debug)]
 104pub enum Event {
 105    DockPositionChanged,
 106    Focus,
 107    Dismissed,
 108}
 109
 110#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
 111enum Section {
 112    ActiveCall,
 113    Channels,
 114    Requests,
 115    Contacts,
 116    Online,
 117    Offline,
 118}
 119
 120#[derive(Clone, Debug)]
 121enum ListEntry {
 122    Header(Section, usize),
 123    CallParticipant {
 124        user: Arc<User>,
 125        is_pending: bool,
 126    },
 127    ParticipantProject {
 128        project_id: u64,
 129        worktree_root_names: Vec<String>,
 130        host_user_id: u64,
 131        is_last: bool,
 132    },
 133    ParticipantScreen {
 134        peer_id: PeerId,
 135        is_last: bool,
 136    },
 137    IncomingRequest(Arc<User>),
 138    OutgoingRequest(Arc<User>),
 139    ChannelInvite(Arc<Channel>),
 140    Channel(Arc<Channel>),
 141    ChannelEditor {
 142        depth: usize,
 143    },
 144    Contact {
 145        contact: Arc<Contact>,
 146        calling: bool,
 147    },
 148}
 149
 150impl Entity for CollabPanel {
 151    type Event = Event;
 152}
 153
 154impl CollabPanel {
 155    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
 156        cx.add_view::<Self, _>(|cx| {
 157            let view_id = cx.view_id();
 158
 159            let filter_editor = cx.add_view(|cx| {
 160                let mut editor = Editor::single_line(
 161                    Some(Arc::new(|theme| {
 162                        theme.collab_panel.user_query_editor.clone()
 163                    })),
 164                    cx,
 165                );
 166                editor.set_placeholder_text("Filter channels, contacts", cx);
 167                editor
 168            });
 169
 170            cx.subscribe(&filter_editor, |this, _, event, cx| {
 171                if let editor::Event::BufferEdited = event {
 172                    let query = this.filter_editor.read(cx).text(cx);
 173                    if !query.is_empty() {
 174                        this.selection.take();
 175                    }
 176                    this.update_entries(cx);
 177                    if !query.is_empty() {
 178                        this.selection = this
 179                            .entries
 180                            .iter()
 181                            .position(|entry| !matches!(entry, ListEntry::Header(_, _)));
 182                    }
 183                }
 184            })
 185            .detach();
 186
 187            let channel_name_editor = cx.add_view(|cx| {
 188                Editor::single_line(
 189                    Some(Arc::new(|theme| {
 190                        theme.collab_panel.user_query_editor.clone()
 191                    })),
 192                    cx,
 193                )
 194            });
 195
 196            cx.subscribe(&channel_name_editor, |this, _, event, cx| {
 197                if let editor::Event::Blurred = event {
 198                    this.take_editing_state(cx);
 199                    this.update_entries(cx);
 200                    cx.notify();
 201                }
 202            })
 203            .detach();
 204
 205            let list_state =
 206                ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
 207                    let theme = theme::current(cx).clone();
 208                    let is_selected = this.selection == Some(ix);
 209                    let current_project_id = this.project.read(cx).remote_id();
 210
 211                    match &this.entries[ix] {
 212                        ListEntry::Header(section, depth) => {
 213                            let is_collapsed = this.collapsed_sections.contains(section);
 214                            this.render_header(
 215                                *section,
 216                                &theme,
 217                                *depth,
 218                                is_selected,
 219                                is_collapsed,
 220                                cx,
 221                            )
 222                        }
 223                        ListEntry::CallParticipant { user, is_pending } => {
 224                            Self::render_call_participant(
 225                                user,
 226                                *is_pending,
 227                                is_selected,
 228                                &theme.collab_panel,
 229                            )
 230                        }
 231                        ListEntry::ParticipantProject {
 232                            project_id,
 233                            worktree_root_names,
 234                            host_user_id,
 235                            is_last,
 236                        } => Self::render_participant_project(
 237                            *project_id,
 238                            worktree_root_names,
 239                            *host_user_id,
 240                            Some(*project_id) == current_project_id,
 241                            *is_last,
 242                            is_selected,
 243                            &theme.collab_panel,
 244                            cx,
 245                        ),
 246                        ListEntry::ParticipantScreen { peer_id, is_last } => {
 247                            Self::render_participant_screen(
 248                                *peer_id,
 249                                *is_last,
 250                                is_selected,
 251                                &theme.collab_panel,
 252                                cx,
 253                            )
 254                        }
 255                        ListEntry::Channel(channel) => {
 256                            Self::render_channel(&*channel, &theme.collab_panel, is_selected, cx)
 257                        }
 258                        ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
 259                            channel.clone(),
 260                            this.channel_store.clone(),
 261                            &theme.collab_panel,
 262                            is_selected,
 263                            cx,
 264                        ),
 265                        ListEntry::IncomingRequest(user) => Self::render_contact_request(
 266                            user.clone(),
 267                            this.user_store.clone(),
 268                            &theme.collab_panel,
 269                            true,
 270                            is_selected,
 271                            cx,
 272                        ),
 273                        ListEntry::OutgoingRequest(user) => Self::render_contact_request(
 274                            user.clone(),
 275                            this.user_store.clone(),
 276                            &theme.collab_panel,
 277                            false,
 278                            is_selected,
 279                            cx,
 280                        ),
 281                        ListEntry::Contact { contact, calling } => Self::render_contact(
 282                            contact,
 283                            *calling,
 284                            &this.project,
 285                            &theme.collab_panel,
 286                            is_selected,
 287                            cx,
 288                        ),
 289                        ListEntry::ChannelEditor { depth } => {
 290                            this.render_channel_editor(&theme.collab_panel, *depth, cx)
 291                        }
 292                    }
 293                });
 294
 295            let mut this = Self {
 296                width: None,
 297                has_focus: false,
 298                fs: workspace.app_state().fs.clone(),
 299                pending_serialization: Task::ready(None),
 300                context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
 301                channel_name_editor,
 302                filter_editor,
 303                entries: Vec::default(),
 304                channel_editing_state: None,
 305                selection: None,
 306                user_store: workspace.user_store().clone(),
 307                channel_store: workspace.app_state().channel_store.clone(),
 308                project: workspace.project().clone(),
 309                subscriptions: Vec::default(),
 310                match_candidates: Vec::default(),
 311                collapsed_sections: Vec::default(),
 312                workspace: workspace.weak_handle(),
 313                list_state,
 314            };
 315            this.update_entries(cx);
 316
 317            // Update the dock position when the setting changes.
 318            let mut old_dock_position = this.position(cx);
 319            this.subscriptions
 320                .push(
 321                    cx.observe_global::<SettingsStore, _>(move |this: &mut CollabPanel, cx| {
 322                        let new_dock_position = this.position(cx);
 323                        if new_dock_position != old_dock_position {
 324                            old_dock_position = new_dock_position;
 325                            cx.emit(Event::DockPositionChanged);
 326                        }
 327                    }),
 328                );
 329
 330            let active_call = ActiveCall::global(cx);
 331            this.subscriptions
 332                .push(cx.observe(&this.user_store, |this, _, cx| this.update_entries(cx)));
 333            this.subscriptions
 334                .push(cx.observe(&this.channel_store, |this, _, cx| this.update_entries(cx)));
 335            this.subscriptions
 336                .push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx)));
 337
 338            this
 339        })
 340    }
 341
 342    pub fn load(
 343        workspace: WeakViewHandle<Workspace>,
 344        cx: AsyncAppContext,
 345    ) -> Task<Result<ViewHandle<Self>>> {
 346        cx.spawn(|mut cx| async move {
 347            let serialized_panel = if let Some(panel) = cx
 348                .background()
 349                .spawn(async move { KEY_VALUE_STORE.read_kvp(CHANNELS_PANEL_KEY) })
 350                .await
 351                .log_err()
 352                .flatten()
 353            {
 354                Some(serde_json::from_str::<SerializedChannelsPanel>(&panel)?)
 355            } else {
 356                None
 357            };
 358
 359            workspace.update(&mut cx, |workspace, cx| {
 360                let panel = CollabPanel::new(workspace, cx);
 361                if let Some(serialized_panel) = serialized_panel {
 362                    panel.update(cx, |panel, cx| {
 363                        panel.width = serialized_panel.width;
 364                        cx.notify();
 365                    });
 366                }
 367                panel
 368            })
 369        })
 370    }
 371
 372    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 373        let width = self.width;
 374        self.pending_serialization = cx.background().spawn(
 375            async move {
 376                KEY_VALUE_STORE
 377                    .write_kvp(
 378                        CHANNELS_PANEL_KEY.into(),
 379                        serde_json::to_string(&SerializedChannelsPanel { width })?,
 380                    )
 381                    .await?;
 382                anyhow::Ok(())
 383            }
 384            .log_err(),
 385        );
 386    }
 387
 388    fn update_entries(&mut self, cx: &mut ViewContext<Self>) {
 389        let channel_store = self.channel_store.read(cx);
 390        let user_store = self.user_store.read(cx);
 391        let query = self.filter_editor.read(cx).text(cx);
 392        let executor = cx.background().clone();
 393
 394        let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
 395        let old_entries = mem::take(&mut self.entries);
 396
 397        if let Some(room) = ActiveCall::global(cx).read(cx).room() {
 398            let room = room.read(cx);
 399            let mut participant_entries = Vec::new();
 400
 401            // Populate the active user.
 402            if let Some(user) = user_store.current_user() {
 403                self.match_candidates.clear();
 404                self.match_candidates.push(StringMatchCandidate {
 405                    id: 0,
 406                    string: user.github_login.clone(),
 407                    char_bag: user.github_login.chars().collect(),
 408                });
 409                let matches = executor.block(match_strings(
 410                    &self.match_candidates,
 411                    &query,
 412                    true,
 413                    usize::MAX,
 414                    &Default::default(),
 415                    executor.clone(),
 416                ));
 417                if !matches.is_empty() {
 418                    let user_id = user.id;
 419                    participant_entries.push(ListEntry::CallParticipant {
 420                        user,
 421                        is_pending: false,
 422                    });
 423                    let mut projects = room.local_participant().projects.iter().peekable();
 424                    while let Some(project) = projects.next() {
 425                        participant_entries.push(ListEntry::ParticipantProject {
 426                            project_id: project.id,
 427                            worktree_root_names: project.worktree_root_names.clone(),
 428                            host_user_id: user_id,
 429                            is_last: projects.peek().is_none(),
 430                        });
 431                    }
 432                }
 433            }
 434
 435            // Populate remote participants.
 436            self.match_candidates.clear();
 437            self.match_candidates
 438                .extend(room.remote_participants().iter().map(|(_, participant)| {
 439                    StringMatchCandidate {
 440                        id: participant.user.id as usize,
 441                        string: participant.user.github_login.clone(),
 442                        char_bag: participant.user.github_login.chars().collect(),
 443                    }
 444                }));
 445            let matches = executor.block(match_strings(
 446                &self.match_candidates,
 447                &query,
 448                true,
 449                usize::MAX,
 450                &Default::default(),
 451                executor.clone(),
 452            ));
 453            for mat in matches {
 454                let user_id = mat.candidate_id as u64;
 455                let participant = &room.remote_participants()[&user_id];
 456                participant_entries.push(ListEntry::CallParticipant {
 457                    user: participant.user.clone(),
 458                    is_pending: false,
 459                });
 460                let mut projects = participant.projects.iter().peekable();
 461                while let Some(project) = projects.next() {
 462                    participant_entries.push(ListEntry::ParticipantProject {
 463                        project_id: project.id,
 464                        worktree_root_names: project.worktree_root_names.clone(),
 465                        host_user_id: participant.user.id,
 466                        is_last: projects.peek().is_none() && participant.video_tracks.is_empty(),
 467                    });
 468                }
 469                if !participant.video_tracks.is_empty() {
 470                    participant_entries.push(ListEntry::ParticipantScreen {
 471                        peer_id: participant.peer_id,
 472                        is_last: true,
 473                    });
 474                }
 475            }
 476
 477            // Populate pending participants.
 478            self.match_candidates.clear();
 479            self.match_candidates
 480                .extend(
 481                    room.pending_participants()
 482                        .iter()
 483                        .enumerate()
 484                        .map(|(id, participant)| StringMatchCandidate {
 485                            id,
 486                            string: participant.github_login.clone(),
 487                            char_bag: participant.github_login.chars().collect(),
 488                        }),
 489                );
 490            let matches = executor.block(match_strings(
 491                &self.match_candidates,
 492                &query,
 493                true,
 494                usize::MAX,
 495                &Default::default(),
 496                executor.clone(),
 497            ));
 498            participant_entries.extend(matches.iter().map(|mat| ListEntry::CallParticipant {
 499                user: room.pending_participants()[mat.candidate_id].clone(),
 500                is_pending: true,
 501            }));
 502
 503            if !participant_entries.is_empty() {
 504                self.entries.push(ListEntry::Header(Section::ActiveCall, 0));
 505                if !self.collapsed_sections.contains(&Section::ActiveCall) {
 506                    self.entries.extend(participant_entries);
 507                }
 508            }
 509        }
 510
 511        self.entries.push(ListEntry::Header(Section::Channels, 0));
 512
 513        let channels = channel_store.channels();
 514        if !(channels.is_empty() && self.channel_editing_state.is_none()) {
 515            self.match_candidates.clear();
 516            self.match_candidates
 517                .extend(
 518                    channels
 519                        .iter()
 520                        .enumerate()
 521                        .map(|(ix, channel)| StringMatchCandidate {
 522                            id: ix,
 523                            string: channel.name.clone(),
 524                            char_bag: channel.name.chars().collect(),
 525                        }),
 526                );
 527            let matches = executor.block(match_strings(
 528                &self.match_candidates,
 529                &query,
 530                true,
 531                usize::MAX,
 532                &Default::default(),
 533                executor.clone(),
 534            ));
 535            if let Some(state) = &self.channel_editing_state {
 536                if state.parent_id.is_none() {
 537                    self.entries.push(ListEntry::ChannelEditor { depth: 0 });
 538                }
 539            }
 540            for mat in matches {
 541                let channel = &channels[mat.candidate_id];
 542                self.entries.push(ListEntry::Channel(channel.clone()));
 543                if let Some(state) = &self.channel_editing_state {
 544                    if state.parent_id == Some(channel.id) {
 545                        self.entries.push(ListEntry::ChannelEditor {
 546                            depth: channel.depth + 1,
 547                        });
 548                    }
 549                }
 550            }
 551        }
 552
 553        self.entries.push(ListEntry::Header(Section::Contacts, 0));
 554
 555        let mut request_entries = Vec::new();
 556        let channel_invites = channel_store.channel_invitations();
 557        if !channel_invites.is_empty() {
 558            self.match_candidates.clear();
 559            self.match_candidates
 560                .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
 561                    StringMatchCandidate {
 562                        id: ix,
 563                        string: channel.name.clone(),
 564                        char_bag: channel.name.chars().collect(),
 565                    }
 566                }));
 567            let matches = executor.block(match_strings(
 568                &self.match_candidates,
 569                &query,
 570                true,
 571                usize::MAX,
 572                &Default::default(),
 573                executor.clone(),
 574            ));
 575            request_entries.extend(
 576                matches
 577                    .iter()
 578                    .map(|mat| ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())),
 579            );
 580        }
 581
 582        let incoming = user_store.incoming_contact_requests();
 583        if !incoming.is_empty() {
 584            self.match_candidates.clear();
 585            self.match_candidates
 586                .extend(
 587                    incoming
 588                        .iter()
 589                        .enumerate()
 590                        .map(|(ix, user)| StringMatchCandidate {
 591                            id: ix,
 592                            string: user.github_login.clone(),
 593                            char_bag: user.github_login.chars().collect(),
 594                        }),
 595                );
 596            let matches = executor.block(match_strings(
 597                &self.match_candidates,
 598                &query,
 599                true,
 600                usize::MAX,
 601                &Default::default(),
 602                executor.clone(),
 603            ));
 604            request_entries.extend(
 605                matches
 606                    .iter()
 607                    .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
 608            );
 609        }
 610
 611        let outgoing = user_store.outgoing_contact_requests();
 612        if !outgoing.is_empty() {
 613            self.match_candidates.clear();
 614            self.match_candidates
 615                .extend(
 616                    outgoing
 617                        .iter()
 618                        .enumerate()
 619                        .map(|(ix, user)| StringMatchCandidate {
 620                            id: ix,
 621                            string: user.github_login.clone(),
 622                            char_bag: user.github_login.chars().collect(),
 623                        }),
 624                );
 625            let matches = executor.block(match_strings(
 626                &self.match_candidates,
 627                &query,
 628                true,
 629                usize::MAX,
 630                &Default::default(),
 631                executor.clone(),
 632            ));
 633            request_entries.extend(
 634                matches
 635                    .iter()
 636                    .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
 637            );
 638        }
 639
 640        if !request_entries.is_empty() {
 641            self.entries.push(ListEntry::Header(Section::Requests, 1));
 642            if !self.collapsed_sections.contains(&Section::Requests) {
 643                self.entries.append(&mut request_entries);
 644            }
 645        }
 646
 647        let contacts = user_store.contacts();
 648        if !contacts.is_empty() {
 649            self.match_candidates.clear();
 650            self.match_candidates
 651                .extend(
 652                    contacts
 653                        .iter()
 654                        .enumerate()
 655                        .map(|(ix, contact)| StringMatchCandidate {
 656                            id: ix,
 657                            string: contact.user.github_login.clone(),
 658                            char_bag: contact.user.github_login.chars().collect(),
 659                        }),
 660                );
 661
 662            let matches = executor.block(match_strings(
 663                &self.match_candidates,
 664                &query,
 665                true,
 666                usize::MAX,
 667                &Default::default(),
 668                executor.clone(),
 669            ));
 670
 671            let (mut online_contacts, offline_contacts) = matches
 672                .iter()
 673                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
 674            if let Some(room) = ActiveCall::global(cx).read(cx).room() {
 675                let room = room.read(cx);
 676                online_contacts.retain(|contact| {
 677                    let contact = &contacts[contact.candidate_id];
 678                    !room.contains_participant(contact.user.id)
 679                });
 680            }
 681
 682            for (matches, section) in [
 683                (online_contacts, Section::Online),
 684                (offline_contacts, Section::Offline),
 685            ] {
 686                if !matches.is_empty() {
 687                    self.entries.push(ListEntry::Header(section, 1));
 688                    if !self.collapsed_sections.contains(&section) {
 689                        let active_call = &ActiveCall::global(cx).read(cx);
 690                        for mat in matches {
 691                            let contact = &contacts[mat.candidate_id];
 692                            self.entries.push(ListEntry::Contact {
 693                                contact: contact.clone(),
 694                                calling: active_call.pending_invites().contains(&contact.user.id),
 695                            });
 696                        }
 697                    }
 698                }
 699            }
 700        }
 701
 702        if let Some(prev_selected_entry) = prev_selected_entry {
 703            self.selection.take();
 704            for (ix, entry) in self.entries.iter().enumerate() {
 705                if *entry == prev_selected_entry {
 706                    self.selection = Some(ix);
 707                    break;
 708                }
 709            }
 710        }
 711
 712        let old_scroll_top = self.list_state.logical_scroll_top();
 713        self.list_state.reset(self.entries.len());
 714
 715        // Attempt to maintain the same scroll position.
 716        if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
 717            let new_scroll_top = self
 718                .entries
 719                .iter()
 720                .position(|entry| entry == old_top_entry)
 721                .map(|item_ix| ListOffset {
 722                    item_ix,
 723                    offset_in_item: old_scroll_top.offset_in_item,
 724                })
 725                .or_else(|| {
 726                    let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
 727                    let item_ix = self
 728                        .entries
 729                        .iter()
 730                        .position(|entry| entry == entry_after_old_top)?;
 731                    Some(ListOffset {
 732                        item_ix,
 733                        offset_in_item: 0.,
 734                    })
 735                })
 736                .or_else(|| {
 737                    let entry_before_old_top =
 738                        old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
 739                    let item_ix = self
 740                        .entries
 741                        .iter()
 742                        .position(|entry| entry == entry_before_old_top)?;
 743                    Some(ListOffset {
 744                        item_ix,
 745                        offset_in_item: 0.,
 746                    })
 747                });
 748
 749            self.list_state
 750                .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
 751        }
 752
 753        cx.notify();
 754    }
 755
 756    fn render_call_participant(
 757        user: &User,
 758        is_pending: bool,
 759        is_selected: bool,
 760        theme: &theme::CollabPanel,
 761    ) -> AnyElement<Self> {
 762        Flex::row()
 763            .with_children(user.avatar.clone().map(|avatar| {
 764                Image::from_data(avatar)
 765                    .with_style(theme.contact_avatar)
 766                    .aligned()
 767                    .left()
 768            }))
 769            .with_child(
 770                Label::new(
 771                    user.github_login.clone(),
 772                    theme.contact_username.text.clone(),
 773                )
 774                .contained()
 775                .with_style(theme.contact_username.container)
 776                .aligned()
 777                .left()
 778                .flex(1., true),
 779            )
 780            .with_children(if is_pending {
 781                Some(
 782                    Label::new("Calling", theme.calling_indicator.text.clone())
 783                        .contained()
 784                        .with_style(theme.calling_indicator.container)
 785                        .aligned(),
 786                )
 787            } else {
 788                None
 789            })
 790            .constrained()
 791            .with_height(theme.row_height)
 792            .contained()
 793            .with_style(
 794                *theme
 795                    .contact_row
 796                    .in_state(is_selected)
 797                    .style_for(&mut Default::default()),
 798            )
 799            .into_any()
 800    }
 801
 802    fn render_participant_project(
 803        project_id: u64,
 804        worktree_root_names: &[String],
 805        host_user_id: u64,
 806        is_current: bool,
 807        is_last: bool,
 808        is_selected: bool,
 809        theme: &theme::CollabPanel,
 810        cx: &mut ViewContext<Self>,
 811    ) -> AnyElement<Self> {
 812        enum JoinProject {}
 813
 814        let font_cache = cx.font_cache();
 815        let host_avatar_height = theme
 816            .contact_avatar
 817            .width
 818            .or(theme.contact_avatar.height)
 819            .unwrap_or(0.);
 820        let row = &theme.project_row.inactive_state().default;
 821        let tree_branch = theme.tree_branch;
 822        let line_height = row.name.text.line_height(font_cache);
 823        let cap_height = row.name.text.cap_height(font_cache);
 824        let baseline_offset =
 825            row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
 826        let project_name = if worktree_root_names.is_empty() {
 827            "untitled".to_string()
 828        } else {
 829            worktree_root_names.join(", ")
 830        };
 831
 832        MouseEventHandler::<JoinProject, Self>::new(project_id as usize, cx, |mouse_state, _| {
 833            let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
 834            let row = theme
 835                .project_row
 836                .in_state(is_selected)
 837                .style_for(mouse_state);
 838
 839            Flex::row()
 840                .with_child(
 841                    Stack::new()
 842                        .with_child(Canvas::new(move |scene, bounds, _, _, _| {
 843                            let start_x =
 844                                bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.);
 845                            let end_x = bounds.max_x();
 846                            let start_y = bounds.min_y();
 847                            let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
 848
 849                            scene.push_quad(gpui::Quad {
 850                                bounds: RectF::from_points(
 851                                    vec2f(start_x, start_y),
 852                                    vec2f(
 853                                        start_x + tree_branch.width,
 854                                        if is_last { end_y } else { bounds.max_y() },
 855                                    ),
 856                                ),
 857                                background: Some(tree_branch.color),
 858                                border: gpui::Border::default(),
 859                                corner_radius: 0.,
 860                            });
 861                            scene.push_quad(gpui::Quad {
 862                                bounds: RectF::from_points(
 863                                    vec2f(start_x, end_y),
 864                                    vec2f(end_x, end_y + tree_branch.width),
 865                                ),
 866                                background: Some(tree_branch.color),
 867                                border: gpui::Border::default(),
 868                                corner_radius: 0.,
 869                            });
 870                        }))
 871                        .constrained()
 872                        .with_width(host_avatar_height),
 873                )
 874                .with_child(
 875                    Label::new(project_name, row.name.text.clone())
 876                        .aligned()
 877                        .left()
 878                        .contained()
 879                        .with_style(row.name.container)
 880                        .flex(1., false),
 881                )
 882                .constrained()
 883                .with_height(theme.row_height)
 884                .contained()
 885                .with_style(row.container)
 886        })
 887        .with_cursor_style(if !is_current {
 888            CursorStyle::PointingHand
 889        } else {
 890            CursorStyle::Arrow
 891        })
 892        .on_click(MouseButton::Left, move |_, this, cx| {
 893            if !is_current {
 894                if let Some(workspace) = this.workspace.upgrade(cx) {
 895                    let app_state = workspace.read(cx).app_state().clone();
 896                    workspace::join_remote_project(project_id, host_user_id, app_state, cx)
 897                        .detach_and_log_err(cx);
 898                }
 899            }
 900        })
 901        .into_any()
 902    }
 903
 904    fn render_participant_screen(
 905        peer_id: PeerId,
 906        is_last: bool,
 907        is_selected: bool,
 908        theme: &theme::CollabPanel,
 909        cx: &mut ViewContext<Self>,
 910    ) -> AnyElement<Self> {
 911        enum OpenSharedScreen {}
 912
 913        let font_cache = cx.font_cache();
 914        let host_avatar_height = theme
 915            .contact_avatar
 916            .width
 917            .or(theme.contact_avatar.height)
 918            .unwrap_or(0.);
 919        let row = &theme.project_row.inactive_state().default;
 920        let tree_branch = theme.tree_branch;
 921        let line_height = row.name.text.line_height(font_cache);
 922        let cap_height = row.name.text.cap_height(font_cache);
 923        let baseline_offset =
 924            row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
 925
 926        MouseEventHandler::<OpenSharedScreen, Self>::new(
 927            peer_id.as_u64() as usize,
 928            cx,
 929            |mouse_state, _| {
 930                let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
 931                let row = theme
 932                    .project_row
 933                    .in_state(is_selected)
 934                    .style_for(mouse_state);
 935
 936                Flex::row()
 937                    .with_child(
 938                        Stack::new()
 939                            .with_child(Canvas::new(move |scene, bounds, _, _, _| {
 940                                let start_x = bounds.min_x() + (bounds.width() / 2.)
 941                                    - (tree_branch.width / 2.);
 942                                let end_x = bounds.max_x();
 943                                let start_y = bounds.min_y();
 944                                let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
 945
 946                                scene.push_quad(gpui::Quad {
 947                                    bounds: RectF::from_points(
 948                                        vec2f(start_x, start_y),
 949                                        vec2f(
 950                                            start_x + tree_branch.width,
 951                                            if is_last { end_y } else { bounds.max_y() },
 952                                        ),
 953                                    ),
 954                                    background: Some(tree_branch.color),
 955                                    border: gpui::Border::default(),
 956                                    corner_radius: 0.,
 957                                });
 958                                scene.push_quad(gpui::Quad {
 959                                    bounds: RectF::from_points(
 960                                        vec2f(start_x, end_y),
 961                                        vec2f(end_x, end_y + tree_branch.width),
 962                                    ),
 963                                    background: Some(tree_branch.color),
 964                                    border: gpui::Border::default(),
 965                                    corner_radius: 0.,
 966                                });
 967                            }))
 968                            .constrained()
 969                            .with_width(host_avatar_height),
 970                    )
 971                    .with_child(
 972                        Svg::new("icons/disable_screen_sharing_12.svg")
 973                            .with_color(row.icon.color)
 974                            .constrained()
 975                            .with_width(row.icon.width)
 976                            .aligned()
 977                            .left()
 978                            .contained()
 979                            .with_style(row.icon.container),
 980                    )
 981                    .with_child(
 982                        Label::new("Screen", row.name.text.clone())
 983                            .aligned()
 984                            .left()
 985                            .contained()
 986                            .with_style(row.name.container)
 987                            .flex(1., false),
 988                    )
 989                    .constrained()
 990                    .with_height(theme.row_height)
 991                    .contained()
 992                    .with_style(row.container)
 993            },
 994        )
 995        .with_cursor_style(CursorStyle::PointingHand)
 996        .on_click(MouseButton::Left, move |_, this, cx| {
 997            if let Some(workspace) = this.workspace.upgrade(cx) {
 998                workspace.update(cx, |workspace, cx| {
 999                    workspace.open_shared_screen(peer_id, cx)
1000                });
1001            }
1002        })
1003        .into_any()
1004    }
1005
1006    fn take_editing_state(
1007        &mut self,
1008        cx: &mut ViewContext<Self>,
1009    ) -> Option<(ChannelEditingState, String)> {
1010        let result = self
1011            .channel_editing_state
1012            .take()
1013            .map(|state| (state, self.channel_name_editor.read(cx).text(cx)));
1014
1015        self.channel_name_editor
1016            .update(cx, |editor, cx| editor.set_text("", cx));
1017
1018        result
1019    }
1020
1021    fn render_header(
1022        &self,
1023        section: Section,
1024        theme: &theme::Theme,
1025        depth: usize,
1026        is_selected: bool,
1027        is_collapsed: bool,
1028        cx: &mut ViewContext<Self>,
1029    ) -> AnyElement<Self> {
1030        enum Header {}
1031        enum LeaveCallContactList {}
1032        enum AddChannel {}
1033
1034        let tooltip_style = &theme.tooltip;
1035        let text = match section {
1036            Section::ActiveCall => "Current Call",
1037            Section::Requests => "Requests",
1038            Section::Contacts => "Contacts",
1039            Section::Channels => "Channels",
1040            Section::Online => "Online",
1041            Section::Offline => "Offline",
1042        };
1043
1044        enum AddContact {}
1045        let button = match section {
1046            Section::ActiveCall => Some(
1047                MouseEventHandler::<AddContact, Self>::new(0, cx, |_, _| {
1048                    render_icon_button(
1049                        &theme.collab_panel.leave_call_button,
1050                        "icons/radix/exit.svg",
1051                    )
1052                })
1053                .with_cursor_style(CursorStyle::PointingHand)
1054                .on_click(MouseButton::Left, |_, _, cx| {
1055                    ActiveCall::global(cx)
1056                        .update(cx, |call, cx| call.hang_up(cx))
1057                        .detach_and_log_err(cx);
1058                })
1059                .with_tooltip::<AddContact>(
1060                    0,
1061                    "Leave call".into(),
1062                    None,
1063                    tooltip_style.clone(),
1064                    cx,
1065                ),
1066            ),
1067            Section::Contacts => Some(
1068                MouseEventHandler::<LeaveCallContactList, Self>::new(0, cx, |_, _| {
1069                    render_icon_button(
1070                        &theme.collab_panel.add_contact_button,
1071                        "icons/user_plus_16.svg",
1072                    )
1073                })
1074                .with_cursor_style(CursorStyle::PointingHand)
1075                .on_click(MouseButton::Left, |_, this, cx| {
1076                    this.toggle_contact_finder(cx);
1077                })
1078                .with_tooltip::<LeaveCallContactList>(
1079                    0,
1080                    "Search for new contact".into(),
1081                    None,
1082                    tooltip_style.clone(),
1083                    cx,
1084                ),
1085            ),
1086            Section::Channels => Some(
1087                MouseEventHandler::<AddChannel, Self>::new(0, cx, |_, _| {
1088                    render_icon_button(&theme.collab_panel.add_contact_button, "icons/plus_16.svg")
1089                })
1090                .with_cursor_style(CursorStyle::PointingHand)
1091                .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx))
1092                .with_tooltip::<AddChannel>(
1093                    0,
1094                    "Add or join a channel".into(),
1095                    None,
1096                    tooltip_style.clone(),
1097                    cx,
1098                ),
1099            ),
1100            _ => None,
1101        };
1102
1103        let can_collapse = depth > 0;
1104        let icon_size = (&theme.collab_panel).section_icon_size;
1105        MouseEventHandler::<Header, Self>::new(section as usize, cx, |state, _| {
1106            let header_style = if can_collapse {
1107                theme
1108                    .collab_panel
1109                    .subheader_row
1110                    .in_state(is_selected)
1111                    .style_for(state)
1112            } else {
1113                &theme.collab_panel.header_row
1114            };
1115
1116            Flex::row()
1117                .with_children(if can_collapse {
1118                    Some(
1119                        Svg::new(if is_collapsed {
1120                            "icons/chevron_right_8.svg"
1121                        } else {
1122                            "icons/chevron_down_8.svg"
1123                        })
1124                        .with_color(header_style.text.color)
1125                        .constrained()
1126                        .with_max_width(icon_size)
1127                        .with_max_height(icon_size)
1128                        .aligned()
1129                        .constrained()
1130                        .with_width(icon_size)
1131                        .contained()
1132                        .with_margin_right(
1133                            theme.collab_panel.contact_username.container.margin.left,
1134                        ),
1135                    )
1136                } else {
1137                    None
1138                })
1139                .with_child(
1140                    Label::new(text, header_style.text.clone())
1141                        .aligned()
1142                        .left()
1143                        .flex(1., true),
1144                )
1145                .with_children(button.map(|button| button.aligned().right()))
1146                .constrained()
1147                .with_height(theme.collab_panel.row_height)
1148                .contained()
1149                .with_style(header_style.container)
1150        })
1151        .with_cursor_style(CursorStyle::PointingHand)
1152        .on_click(MouseButton::Left, move |_, this, cx| {
1153            if can_collapse {
1154                this.toggle_expanded(section, cx);
1155            }
1156        })
1157        .into_any()
1158    }
1159
1160    fn render_contact(
1161        contact: &Contact,
1162        calling: bool,
1163        project: &ModelHandle<Project>,
1164        theme: &theme::CollabPanel,
1165        is_selected: bool,
1166        cx: &mut ViewContext<Self>,
1167    ) -> AnyElement<Self> {
1168        let online = contact.online;
1169        let busy = contact.busy || calling;
1170        let user_id = contact.user.id;
1171        let github_login = contact.user.github_login.clone();
1172        let initial_project = project.clone();
1173        let mut event_handler =
1174            MouseEventHandler::<Contact, Self>::new(contact.user.id as usize, cx, |state, cx| {
1175                Flex::row()
1176                    .with_children(contact.user.avatar.clone().map(|avatar| {
1177                        let status_badge = if contact.online {
1178                            Some(
1179                                Empty::new()
1180                                    .collapsed()
1181                                    .contained()
1182                                    .with_style(if busy {
1183                                        theme.contact_status_busy
1184                                    } else {
1185                                        theme.contact_status_free
1186                                    })
1187                                    .aligned(),
1188                            )
1189                        } else {
1190                            None
1191                        };
1192                        Stack::new()
1193                            .with_child(
1194                                Image::from_data(avatar)
1195                                    .with_style(theme.contact_avatar)
1196                                    .aligned()
1197                                    .left(),
1198                            )
1199                            .with_children(status_badge)
1200                    }))
1201                    .with_child(
1202                        Label::new(
1203                            contact.user.github_login.clone(),
1204                            theme.contact_username.text.clone(),
1205                        )
1206                        .contained()
1207                        .with_style(theme.contact_username.container)
1208                        .aligned()
1209                        .left()
1210                        .flex(1., true),
1211                    )
1212                    .with_child(
1213                        MouseEventHandler::<Cancel, Self>::new(
1214                            contact.user.id as usize,
1215                            cx,
1216                            |mouse_state, _| {
1217                                let button_style = theme.contact_button.style_for(mouse_state);
1218                                render_icon_button(button_style, "icons/x_mark_8.svg")
1219                                    .aligned()
1220                                    .flex_float()
1221                            },
1222                        )
1223                        .with_padding(Padding::uniform(2.))
1224                        .with_cursor_style(CursorStyle::PointingHand)
1225                        .on_click(MouseButton::Left, move |_, this, cx| {
1226                            this.remove_contact(user_id, &github_login, cx);
1227                        })
1228                        .flex_float(),
1229                    )
1230                    .with_children(if calling {
1231                        Some(
1232                            Label::new("Calling", theme.calling_indicator.text.clone())
1233                                .contained()
1234                                .with_style(theme.calling_indicator.container)
1235                                .aligned(),
1236                        )
1237                    } else {
1238                        None
1239                    })
1240                    .constrained()
1241                    .with_height(theme.row_height)
1242                    .contained()
1243                    .with_style(*theme.contact_row.in_state(is_selected).style_for(state))
1244            })
1245            .on_click(MouseButton::Left, move |_, this, cx| {
1246                if online && !busy {
1247                    this.call(user_id, Some(initial_project.clone()), cx);
1248                }
1249            });
1250
1251        if online {
1252            event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand);
1253        }
1254
1255        event_handler.into_any()
1256    }
1257
1258    fn render_channel_editor(
1259        &self,
1260        theme: &theme::CollabPanel,
1261        depth: usize,
1262        cx: &AppContext,
1263    ) -> AnyElement<Self> {
1264        ChildView::new(&self.channel_name_editor, cx).into_any()
1265    }
1266
1267    fn render_channel(
1268        channel: &Channel,
1269        theme: &theme::CollabPanel,
1270        is_selected: bool,
1271        cx: &mut ViewContext<Self>,
1272    ) -> AnyElement<Self> {
1273        let channel_id = channel.id;
1274        MouseEventHandler::<Channel, Self>::new(channel.id as usize, cx, |state, _cx| {
1275            Flex::row()
1276                .with_child({
1277                    Svg::new("icons/file_icons/hash.svg")
1278                        // .with_style(theme.contact_avatar)
1279                        .aligned()
1280                        .left()
1281                })
1282                .with_child(
1283                    Label::new(channel.name.clone(), theme.contact_username.text.clone())
1284                        .contained()
1285                        .with_style(theme.contact_username.container)
1286                        .aligned()
1287                        .left()
1288                        .flex(1., true),
1289                )
1290                .constrained()
1291                .with_height(theme.row_height)
1292                .contained()
1293                .with_style(*theme.contact_row.in_state(is_selected).style_for(state))
1294                .with_margin_left(20. * channel.depth as f32)
1295        })
1296        .on_click(MouseButton::Left, move |_, this, cx| {
1297            this.join_channel(channel_id, cx);
1298        })
1299        .on_click(MouseButton::Right, move |e, this, cx| {
1300            this.deploy_channel_context_menu(e.position, channel_id, cx);
1301        })
1302        .into_any()
1303    }
1304
1305    fn render_channel_invite(
1306        channel: Arc<Channel>,
1307        channel_store: ModelHandle<ChannelStore>,
1308        theme: &theme::CollabPanel,
1309        is_selected: bool,
1310        cx: &mut ViewContext<Self>,
1311    ) -> AnyElement<Self> {
1312        enum Decline {}
1313        enum Accept {}
1314
1315        let channel_id = channel.id;
1316        let is_invite_pending = channel_store.read(cx).is_channel_invite_pending(&channel);
1317        let button_spacing = theme.contact_button_spacing;
1318
1319        Flex::row()
1320            .with_child({
1321                Svg::new("icons/file_icons/hash.svg")
1322                    // .with_style(theme.contact_avatar)
1323                    .aligned()
1324                    .left()
1325            })
1326            .with_child(
1327                Label::new(channel.name.clone(), theme.contact_username.text.clone())
1328                    .contained()
1329                    .with_style(theme.contact_username.container)
1330                    .aligned()
1331                    .left()
1332                    .flex(1., true),
1333            )
1334            .with_child(
1335                MouseEventHandler::<Decline, Self>::new(
1336                    channel.id as usize,
1337                    cx,
1338                    |mouse_state, _| {
1339                        let button_style = if is_invite_pending {
1340                            &theme.disabled_button
1341                        } else {
1342                            theme.contact_button.style_for(mouse_state)
1343                        };
1344                        render_icon_button(button_style, "icons/x_mark_8.svg").aligned()
1345                    },
1346                )
1347                .with_cursor_style(CursorStyle::PointingHand)
1348                .on_click(MouseButton::Left, move |_, this, cx| {
1349                    this.respond_to_channel_invite(channel_id, false, cx);
1350                })
1351                .contained()
1352                .with_margin_right(button_spacing),
1353            )
1354            .with_child(
1355                MouseEventHandler::<Accept, Self>::new(
1356                    channel.id as usize,
1357                    cx,
1358                    |mouse_state, _| {
1359                        let button_style = if is_invite_pending {
1360                            &theme.disabled_button
1361                        } else {
1362                            theme.contact_button.style_for(mouse_state)
1363                        };
1364                        render_icon_button(button_style, "icons/check_8.svg")
1365                            .aligned()
1366                            .flex_float()
1367                    },
1368                )
1369                .with_cursor_style(CursorStyle::PointingHand)
1370                .on_click(MouseButton::Left, move |_, this, cx| {
1371                    this.respond_to_channel_invite(channel_id, true, cx);
1372                }),
1373            )
1374            .constrained()
1375            .with_height(theme.row_height)
1376            .contained()
1377            .with_style(
1378                *theme
1379                    .contact_row
1380                    .in_state(is_selected)
1381                    .style_for(&mut Default::default()),
1382            )
1383            .into_any()
1384    }
1385
1386    fn render_contact_request(
1387        user: Arc<User>,
1388        user_store: ModelHandle<UserStore>,
1389        theme: &theme::CollabPanel,
1390        is_incoming: bool,
1391        is_selected: bool,
1392        cx: &mut ViewContext<Self>,
1393    ) -> AnyElement<Self> {
1394        enum Decline {}
1395        enum Accept {}
1396        enum Cancel {}
1397
1398        let mut row = Flex::row()
1399            .with_children(user.avatar.clone().map(|avatar| {
1400                Image::from_data(avatar)
1401                    .with_style(theme.contact_avatar)
1402                    .aligned()
1403                    .left()
1404            }))
1405            .with_child(
1406                Label::new(
1407                    user.github_login.clone(),
1408                    theme.contact_username.text.clone(),
1409                )
1410                .contained()
1411                .with_style(theme.contact_username.container)
1412                .aligned()
1413                .left()
1414                .flex(1., true),
1415            );
1416
1417        let user_id = user.id;
1418        let github_login = user.github_login.clone();
1419        let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
1420        let button_spacing = theme.contact_button_spacing;
1421
1422        if is_incoming {
1423            row.add_child(
1424                MouseEventHandler::<Decline, Self>::new(user.id as usize, cx, |mouse_state, _| {
1425                    let button_style = if is_contact_request_pending {
1426                        &theme.disabled_button
1427                    } else {
1428                        theme.contact_button.style_for(mouse_state)
1429                    };
1430                    render_icon_button(button_style, "icons/x_mark_8.svg").aligned()
1431                })
1432                .with_cursor_style(CursorStyle::PointingHand)
1433                .on_click(MouseButton::Left, move |_, this, cx| {
1434                    this.respond_to_contact_request(user_id, false, cx);
1435                })
1436                .contained()
1437                .with_margin_right(button_spacing),
1438            );
1439
1440            row.add_child(
1441                MouseEventHandler::<Accept, Self>::new(user.id as usize, cx, |mouse_state, _| {
1442                    let button_style = if is_contact_request_pending {
1443                        &theme.disabled_button
1444                    } else {
1445                        theme.contact_button.style_for(mouse_state)
1446                    };
1447                    render_icon_button(button_style, "icons/check_8.svg")
1448                        .aligned()
1449                        .flex_float()
1450                })
1451                .with_cursor_style(CursorStyle::PointingHand)
1452                .on_click(MouseButton::Left, move |_, this, cx| {
1453                    this.respond_to_contact_request(user_id, true, cx);
1454                }),
1455            );
1456        } else {
1457            row.add_child(
1458                MouseEventHandler::<Cancel, Self>::new(user.id as usize, cx, |mouse_state, _| {
1459                    let button_style = if is_contact_request_pending {
1460                        &theme.disabled_button
1461                    } else {
1462                        theme.contact_button.style_for(mouse_state)
1463                    };
1464                    render_icon_button(button_style, "icons/x_mark_8.svg")
1465                        .aligned()
1466                        .flex_float()
1467                })
1468                .with_padding(Padding::uniform(2.))
1469                .with_cursor_style(CursorStyle::PointingHand)
1470                .on_click(MouseButton::Left, move |_, this, cx| {
1471                    this.remove_contact(user_id, &github_login, cx);
1472                })
1473                .flex_float(),
1474            );
1475        }
1476
1477        row.constrained()
1478            .with_height(theme.row_height)
1479            .contained()
1480            .with_style(
1481                *theme
1482                    .contact_row
1483                    .in_state(is_selected)
1484                    .style_for(&mut Default::default()),
1485            )
1486            .into_any()
1487    }
1488
1489    fn deploy_channel_context_menu(
1490        &mut self,
1491        position: Vector2F,
1492        channel_id: u64,
1493        cx: &mut ViewContext<Self>,
1494    ) {
1495        self.context_menu.update(cx, |context_menu, cx| {
1496            context_menu.show(
1497                position,
1498                gpui::elements::AnchorCorner::BottomLeft,
1499                vec![
1500                    ContextMenuItem::action("New Channel", NewChannel { channel_id }),
1501                    ContextMenuItem::action("Remove Channel", RemoveChannel { channel_id }),
1502                ],
1503                cx,
1504            );
1505        });
1506    }
1507
1508    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
1509        let mut did_clear = self.filter_editor.update(cx, |editor, cx| {
1510            if editor.buffer().read(cx).len(cx) > 0 {
1511                editor.set_text("", cx);
1512                true
1513            } else {
1514                false
1515            }
1516        });
1517
1518        did_clear |= self.take_editing_state(cx).is_some();
1519
1520        if !did_clear {
1521            cx.emit(Event::Dismissed);
1522        }
1523    }
1524
1525    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
1526        let mut ix = self.selection.map_or(0, |ix| ix + 1);
1527        while let Some(entry) = self.entries.get(ix) {
1528            if entry.is_selectable() {
1529                self.selection = Some(ix);
1530                break;
1531            }
1532            ix += 1;
1533        }
1534
1535        self.list_state.reset(self.entries.len());
1536        if let Some(ix) = self.selection {
1537            self.list_state.scroll_to(ListOffset {
1538                item_ix: ix,
1539                offset_in_item: 0.,
1540            });
1541        }
1542        cx.notify();
1543    }
1544
1545    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
1546        if let Some(mut ix) = self.selection.take() {
1547            while ix > 0 {
1548                ix -= 1;
1549                if let Some(entry) = self.entries.get(ix) {
1550                    if entry.is_selectable() {
1551                        self.selection = Some(ix);
1552                        break;
1553                    }
1554                }
1555            }
1556        }
1557
1558        self.list_state.reset(self.entries.len());
1559        if let Some(ix) = self.selection {
1560            self.list_state.scroll_to(ListOffset {
1561                item_ix: ix,
1562                offset_in_item: 0.,
1563            });
1564        }
1565        cx.notify();
1566    }
1567
1568    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
1569        if let Some(selection) = self.selection {
1570            if let Some(entry) = self.entries.get(selection) {
1571                match entry {
1572                    ListEntry::Header(section, _) => {
1573                        self.toggle_expanded(*section, cx);
1574                    }
1575                    ListEntry::Contact { contact, calling } => {
1576                        if contact.online && !contact.busy && !calling {
1577                            self.call(contact.user.id, Some(self.project.clone()), cx);
1578                        }
1579                    }
1580                    ListEntry::ParticipantProject {
1581                        project_id,
1582                        host_user_id,
1583                        ..
1584                    } => {
1585                        if let Some(workspace) = self.workspace.upgrade(cx) {
1586                            let app_state = workspace.read(cx).app_state().clone();
1587                            workspace::join_remote_project(
1588                                *project_id,
1589                                *host_user_id,
1590                                app_state,
1591                                cx,
1592                            )
1593                            .detach_and_log_err(cx);
1594                        }
1595                    }
1596                    ListEntry::ParticipantScreen { peer_id, .. } => {
1597                        if let Some(workspace) = self.workspace.upgrade(cx) {
1598                            workspace.update(cx, |workspace, cx| {
1599                                workspace.open_shared_screen(*peer_id, cx)
1600                            });
1601                        }
1602                    }
1603                    _ => {}
1604                }
1605            }
1606        } else if let Some((editing_state, channel_name)) = self.take_editing_state(cx) {
1607            let create_channel = self.channel_store.update(cx, |channel_store, cx| {
1608                channel_store.create_channel(&channel_name, editing_state.parent_id)
1609            });
1610
1611            cx.foreground()
1612                .spawn(async move {
1613                    create_channel.await.ok();
1614                })
1615                .detach();
1616        }
1617    }
1618
1619    fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
1620        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
1621            self.collapsed_sections.remove(ix);
1622        } else {
1623            self.collapsed_sections.push(section);
1624        }
1625        self.update_entries(cx);
1626    }
1627
1628    fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
1629        if let Some(workspace) = self.workspace.upgrade(cx) {
1630            workspace.update(cx, |workspace, cx| {
1631                workspace.toggle_modal(cx, |_, cx| {
1632                    cx.add_view(|cx| {
1633                        let finder = build_contact_finder(self.user_store.clone(), cx);
1634                        finder.set_query(self.filter_editor.read(cx).text(cx), cx);
1635                        finder
1636                    })
1637                });
1638            });
1639        }
1640    }
1641
1642    fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
1643        if self.channel_editing_state.is_none() {
1644            self.channel_editing_state = Some(ChannelEditingState { parent_id: None });
1645            self.update_entries(cx);
1646        }
1647
1648        cx.focus(self.channel_name_editor.as_any());
1649        cx.notify();
1650    }
1651
1652    fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
1653        if self.channel_editing_state.is_none() {
1654            self.channel_editing_state = Some(ChannelEditingState {
1655                parent_id: Some(action.channel_id),
1656            });
1657            self.update_entries(cx);
1658        }
1659
1660        cx.focus(self.channel_name_editor.as_any());
1661        cx.notify();
1662    }
1663
1664    fn remove_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
1665        let channel_id = action.channel_id;
1666        let channel_store = self.channel_store.clone();
1667        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
1668            let prompt_message = format!(
1669                "Are you sure you want to remove the channel \"{}\"?",
1670                channel.name
1671            );
1672            let mut answer =
1673                cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
1674            let window_id = cx.window_id();
1675            cx.spawn(|_, mut cx| async move {
1676                if answer.next().await == Some(0) {
1677                    if let Err(e) = channel_store
1678                        .update(&mut cx, |channels, cx| channels.remove_channel(channel_id))
1679                        .await
1680                    {
1681                        cx.prompt(
1682                            window_id,
1683                            PromptLevel::Info,
1684                            &format!("Failed to remove channel: {}", e),
1685                            &["Ok"],
1686                        );
1687                    }
1688                }
1689            })
1690            .detach();
1691        }
1692    }
1693
1694    fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
1695        let user_store = self.user_store.clone();
1696        let prompt_message = format!(
1697            "Are you sure you want to remove \"{}\" from your contacts?",
1698            github_login
1699        );
1700        let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
1701        let window_id = cx.window_id();
1702        cx.spawn(|_, mut cx| async move {
1703            if answer.next().await == Some(0) {
1704                if let Err(e) = user_store
1705                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
1706                    .await
1707                {
1708                    cx.prompt(
1709                        window_id,
1710                        PromptLevel::Info,
1711                        &format!("Failed to remove contact: {}", e),
1712                        &["Ok"],
1713                    );
1714                }
1715            }
1716        })
1717        .detach();
1718    }
1719
1720    fn respond_to_contact_request(
1721        &mut self,
1722        user_id: u64,
1723        accept: bool,
1724        cx: &mut ViewContext<Self>,
1725    ) {
1726        self.user_store
1727            .update(cx, |store, cx| {
1728                store.respond_to_contact_request(user_id, accept, cx)
1729            })
1730            .detach();
1731    }
1732
1733    fn respond_to_channel_invite(
1734        &mut self,
1735        channel_id: u64,
1736        accept: bool,
1737        cx: &mut ViewContext<Self>,
1738    ) {
1739        let respond = self.channel_store.update(cx, |store, _| {
1740            store.respond_to_channel_invite(channel_id, accept)
1741        });
1742        cx.foreground().spawn(respond).detach();
1743    }
1744
1745    fn call(
1746        &mut self,
1747        recipient_user_id: u64,
1748        initial_project: Option<ModelHandle<Project>>,
1749        cx: &mut ViewContext<Self>,
1750    ) {
1751        ActiveCall::global(cx)
1752            .update(cx, |call, cx| {
1753                call.invite(recipient_user_id, initial_project, cx)
1754            })
1755            .detach_and_log_err(cx);
1756    }
1757
1758    fn join_channel(&self, channel: u64, cx: &mut ViewContext<Self>) {
1759        ActiveCall::global(cx)
1760            .update(cx, |call, cx| call.join_channel(channel, cx))
1761            .detach_and_log_err(cx);
1762    }
1763}
1764
1765impl View for CollabPanel {
1766    fn ui_name() -> &'static str {
1767        "CollabPanel"
1768    }
1769
1770    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
1771        if !self.has_focus {
1772            self.has_focus = true;
1773            cx.emit(Event::Focus);
1774        }
1775    }
1776
1777    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
1778        self.has_focus = false;
1779    }
1780
1781    fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
1782        let theme = &theme::current(cx).collab_panel;
1783
1784        enum PanelFocus {}
1785        MouseEventHandler::<PanelFocus, _>::new(0, cx, |_, cx| {
1786            Stack::new()
1787                .with_child(
1788                    Flex::column()
1789                        .with_child(
1790                            Flex::row()
1791                                .with_child(
1792                                    ChildView::new(&self.filter_editor, cx)
1793                                        .contained()
1794                                        .with_style(theme.user_query_editor.container)
1795                                        .flex(1.0, true),
1796                                )
1797                                .constrained()
1798                                .with_width(self.size(cx)),
1799                        )
1800                        .with_child(
1801                            List::new(self.list_state.clone())
1802                                .constrained()
1803                                .with_width(self.size(cx))
1804                                .flex(1., true)
1805                                .into_any(),
1806                        )
1807                        .contained()
1808                        .with_style(theme.container)
1809                        .constrained()
1810                        .with_width(self.size(cx))
1811                        .into_any(),
1812                )
1813                .with_child(ChildView::new(&self.context_menu, cx))
1814                .into_any()
1815        })
1816        .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
1817        .into_any_named("channels panel")
1818    }
1819}
1820
1821impl Panel for CollabPanel {
1822    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
1823        match settings::get::<ChannelsPanelSettings>(cx).dock {
1824            ChannelsPanelDockPosition::Left => DockPosition::Left,
1825            ChannelsPanelDockPosition::Right => DockPosition::Right,
1826        }
1827    }
1828
1829    fn position_is_valid(&self, position: DockPosition) -> bool {
1830        matches!(position, DockPosition::Left | DockPosition::Right)
1831    }
1832
1833    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1834        settings::update_settings_file::<ChannelsPanelSettings>(
1835            self.fs.clone(),
1836            cx,
1837            move |settings| {
1838                let dock = match position {
1839                    DockPosition::Left | DockPosition::Bottom => ChannelsPanelDockPosition::Left,
1840                    DockPosition::Right => ChannelsPanelDockPosition::Right,
1841                };
1842                settings.dock = Some(dock);
1843            },
1844        );
1845    }
1846
1847    fn size(&self, cx: &gpui::WindowContext) -> f32 {
1848        self.width
1849            .unwrap_or_else(|| settings::get::<ChannelsPanelSettings>(cx).default_width)
1850    }
1851
1852    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
1853        self.width = Some(size);
1854        self.serialize(cx);
1855        cx.notify();
1856    }
1857
1858    fn icon_path(&self) -> &'static str {
1859        "icons/radix/person.svg"
1860    }
1861
1862    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
1863        ("Channels Panel".to_string(), Some(Box::new(ToggleFocus)))
1864    }
1865
1866    fn should_change_position_on_event(event: &Self::Event) -> bool {
1867        matches!(event, Event::DockPositionChanged)
1868    }
1869
1870    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
1871        self.has_focus
1872    }
1873
1874    fn is_focus_event(event: &Self::Event) -> bool {
1875        matches!(event, Event::Focus)
1876    }
1877}
1878
1879impl ListEntry {
1880    fn is_selectable(&self) -> bool {
1881        if let ListEntry::Header(_, 0) = self {
1882            false
1883        } else {
1884            true
1885        }
1886    }
1887}
1888
1889impl PartialEq for ListEntry {
1890    fn eq(&self, other: &Self) -> bool {
1891        match self {
1892            ListEntry::Header(section_1, depth_1) => {
1893                if let ListEntry::Header(section_2, depth_2) = other {
1894                    return section_1 == section_2 && depth_1 == depth_2;
1895                }
1896            }
1897            ListEntry::CallParticipant { user: user_1, .. } => {
1898                if let ListEntry::CallParticipant { user: user_2, .. } = other {
1899                    return user_1.id == user_2.id;
1900                }
1901            }
1902            ListEntry::ParticipantProject {
1903                project_id: project_id_1,
1904                ..
1905            } => {
1906                if let ListEntry::ParticipantProject {
1907                    project_id: project_id_2,
1908                    ..
1909                } = other
1910                {
1911                    return project_id_1 == project_id_2;
1912                }
1913            }
1914            ListEntry::ParticipantScreen {
1915                peer_id: peer_id_1, ..
1916            } => {
1917                if let ListEntry::ParticipantScreen {
1918                    peer_id: peer_id_2, ..
1919                } = other
1920                {
1921                    return peer_id_1 == peer_id_2;
1922                }
1923            }
1924            ListEntry::Channel(channel_1) => {
1925                if let ListEntry::Channel(channel_2) = other {
1926                    return channel_1.id == channel_2.id;
1927                }
1928            }
1929            ListEntry::ChannelInvite(channel_1) => {
1930                if let ListEntry::ChannelInvite(channel_2) = other {
1931                    return channel_1.id == channel_2.id;
1932                }
1933            }
1934            ListEntry::IncomingRequest(user_1) => {
1935                if let ListEntry::IncomingRequest(user_2) = other {
1936                    return user_1.id == user_2.id;
1937                }
1938            }
1939            ListEntry::OutgoingRequest(user_1) => {
1940                if let ListEntry::OutgoingRequest(user_2) = other {
1941                    return user_1.id == user_2.id;
1942                }
1943            }
1944            ListEntry::Contact {
1945                contact: contact_1, ..
1946            } => {
1947                if let ListEntry::Contact {
1948                    contact: contact_2, ..
1949                } = other
1950                {
1951                    return contact_1.user.id == contact_2.user.id;
1952                }
1953            }
1954            ListEntry::ChannelEditor { depth } => {
1955                if let ListEntry::ChannelEditor { depth: other_depth } = other {
1956                    return depth == other_depth;
1957                }
1958            }
1959        }
1960        false
1961    }
1962}
1963
1964fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
1965    Svg::new(svg_path)
1966        .with_color(style.color)
1967        .constrained()
1968        .with_width(style.icon_width)
1969        .aligned()
1970        .constrained()
1971        .with_width(style.button_width)
1972        .with_height(style.button_width)
1973        .contained()
1974        .with_style(style.container)
1975}