panel.rs

   1mod contact_finder;
   2mod panel_settings;
   3
   4use anyhow::Result;
   5use call::ActiveCall;
   6use client::{proto::PeerId, Client, Contact, User, UserStore};
   7use contact_finder::{build_contact_finder, ContactFinder};
   8use context_menu::ContextMenu;
   9use db::kvp::KEY_VALUE_STORE;
  10use editor::{Cancel, Editor};
  11use futures::StreamExt;
  12use fuzzy::{match_strings, StringMatchCandidate};
  13use gpui::{
  14    actions,
  15    elements::{
  16        Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState,
  17        MouseEventHandler, Orientation, Padding, ParentElement, Stack, Svg,
  18    },
  19    geometry::{rect::RectF, vector::vec2f},
  20    platform::{CursorStyle, MouseButton, PromptLevel},
  21    serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle,
  22    Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
  23};
  24use menu::{Confirm, SelectNext, SelectPrev};
  25use panel_settings::{ChannelsPanelDockPosition, ChannelsPanelSettings};
  26use project::{Fs, Project};
  27use serde_derive::{Deserialize, Serialize};
  28use settings::SettingsStore;
  29use std::{mem, sync::Arc};
  30use theme::IconButton;
  31use util::{ResultExt, TryFutureExt};
  32use workspace::{
  33    dock::{DockPosition, Panel},
  34    Workspace,
  35};
  36
  37actions!(collab_panel, [ToggleFocus]);
  38
  39const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel";
  40
  41pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
  42    settings::register::<panel_settings::ChannelsPanelSettings>(cx);
  43    contact_finder::init(cx);
  44
  45    cx.add_action(CollabPanel::cancel);
  46    cx.add_action(CollabPanel::select_next);
  47    cx.add_action(CollabPanel::select_prev);
  48    cx.add_action(CollabPanel::confirm);
  49}
  50
  51pub struct CollabPanel {
  52    width: Option<f32>,
  53    fs: Arc<dyn Fs>,
  54    has_focus: bool,
  55    pending_serialization: Task<Option<()>>,
  56    context_menu: ViewHandle<ContextMenu>,
  57    contact_finder: Option<ViewHandle<ContactFinder>>,
  58
  59    // from contacts list
  60    filter_editor: ViewHandle<Editor>,
  61    entries: Vec<ContactEntry>,
  62    selection: Option<usize>,
  63    user_store: ModelHandle<UserStore>,
  64    project: ModelHandle<Project>,
  65    match_candidates: Vec<StringMatchCandidate>,
  66    list_state: ListState<Self>,
  67    subscriptions: Vec<Subscription>,
  68    collapsed_sections: Vec<Section>,
  69    workspace: WeakViewHandle<Workspace>,
  70}
  71
  72#[derive(Serialize, Deserialize)]
  73struct SerializedChannelsPanel {
  74    width: Option<f32>,
  75}
  76
  77#[derive(Debug)]
  78pub enum Event {
  79    DockPositionChanged,
  80    Focus,
  81    Dismissed,
  82}
  83
  84#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
  85enum Section {
  86    ActiveCall,
  87    Requests,
  88    Online,
  89    Offline,
  90}
  91
  92#[derive(Clone)]
  93enum ContactEntry {
  94    Header(Section),
  95    CallParticipant {
  96        user: Arc<User>,
  97        is_pending: bool,
  98    },
  99    ParticipantProject {
 100        project_id: u64,
 101        worktree_root_names: Vec<String>,
 102        host_user_id: u64,
 103        is_last: bool,
 104    },
 105    ParticipantScreen {
 106        peer_id: PeerId,
 107        is_last: bool,
 108    },
 109    IncomingRequest(Arc<User>),
 110    OutgoingRequest(Arc<User>),
 111    Contact {
 112        contact: Arc<Contact>,
 113        calling: bool,
 114    },
 115}
 116
 117impl Entity for CollabPanel {
 118    type Event = Event;
 119}
 120
 121impl CollabPanel {
 122    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
 123        cx.add_view::<Self, _>(|cx| {
 124            let view_id = cx.view_id();
 125
 126            let filter_editor = cx.add_view(|cx| {
 127                let mut editor = Editor::single_line(
 128                    Some(Arc::new(|theme| {
 129                        theme.contact_list.user_query_editor.clone()
 130                    })),
 131                    cx,
 132                );
 133                editor.set_placeholder_text("Filter contacts", cx);
 134                editor
 135            });
 136
 137            cx.subscribe(&filter_editor, |this, _, event, cx| {
 138                if let editor::Event::BufferEdited = event {
 139                    let query = this.filter_editor.read(cx).text(cx);
 140                    if !query.is_empty() {
 141                        this.selection.take();
 142                    }
 143                    this.update_entries(cx);
 144                    if !query.is_empty() {
 145                        this.selection = this
 146                            .entries
 147                            .iter()
 148                            .position(|entry| !matches!(entry, ContactEntry::Header(_)));
 149                    }
 150                }
 151            })
 152            .detach();
 153
 154            let list_state =
 155                ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
 156                    let theme = theme::current(cx).clone();
 157                    let is_selected = this.selection == Some(ix);
 158                    let current_project_id = this.project.read(cx).remote_id();
 159
 160                    match &this.entries[ix] {
 161                        ContactEntry::Header(section) => {
 162                            let is_collapsed = this.collapsed_sections.contains(section);
 163                            Self::render_header(
 164                                *section,
 165                                &theme.contact_list,
 166                                is_selected,
 167                                is_collapsed,
 168                                cx,
 169                            )
 170                        }
 171                        ContactEntry::CallParticipant { user, is_pending } => {
 172                            Self::render_call_participant(
 173                                user,
 174                                *is_pending,
 175                                is_selected,
 176                                &theme.contact_list,
 177                            )
 178                        }
 179                        ContactEntry::ParticipantProject {
 180                            project_id,
 181                            worktree_root_names,
 182                            host_user_id,
 183                            is_last,
 184                        } => Self::render_participant_project(
 185                            *project_id,
 186                            worktree_root_names,
 187                            *host_user_id,
 188                            Some(*project_id) == current_project_id,
 189                            *is_last,
 190                            is_selected,
 191                            &theme.contact_list,
 192                            cx,
 193                        ),
 194                        ContactEntry::ParticipantScreen { peer_id, is_last } => {
 195                            Self::render_participant_screen(
 196                                *peer_id,
 197                                *is_last,
 198                                is_selected,
 199                                &theme.contact_list,
 200                                cx,
 201                            )
 202                        }
 203                        ContactEntry::IncomingRequest(user) => Self::render_contact_request(
 204                            user.clone(),
 205                            this.user_store.clone(),
 206                            &theme.contact_list,
 207                            true,
 208                            is_selected,
 209                            cx,
 210                        ),
 211                        ContactEntry::OutgoingRequest(user) => Self::render_contact_request(
 212                            user.clone(),
 213                            this.user_store.clone(),
 214                            &theme.contact_list,
 215                            false,
 216                            is_selected,
 217                            cx,
 218                        ),
 219                        ContactEntry::Contact { contact, calling } => Self::render_contact(
 220                            contact,
 221                            *calling,
 222                            &this.project,
 223                            &theme.contact_list,
 224                            is_selected,
 225                            cx,
 226                        ),
 227                    }
 228                });
 229
 230            let mut this = Self {
 231                width: None,
 232                has_focus: false,
 233                fs: workspace.app_state().fs.clone(),
 234                pending_serialization: Task::ready(None),
 235                context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
 236                filter_editor,
 237                contact_finder: None,
 238                entries: Vec::default(),
 239                selection: None,
 240                user_store: workspace.user_store().clone(),
 241                project: workspace.project().clone(),
 242                subscriptions: Vec::default(),
 243                match_candidates: Vec::default(),
 244                collapsed_sections: Vec::default(),
 245                workspace: workspace.weak_handle(),
 246                list_state,
 247            };
 248            this.update_entries(cx);
 249
 250            // Update the dock position when the setting changes.
 251            let mut old_dock_position = this.position(cx);
 252            this.subscriptions
 253                .push(
 254                    cx.observe_global::<SettingsStore, _>(move |this: &mut CollabPanel, cx| {
 255                        let new_dock_position = this.position(cx);
 256                        if new_dock_position != old_dock_position {
 257                            old_dock_position = new_dock_position;
 258                            cx.emit(Event::DockPositionChanged);
 259                        }
 260                    }),
 261                );
 262
 263            let active_call = ActiveCall::global(cx);
 264            this.subscriptions
 265                .push(cx.observe(&this.user_store, |this, _, cx| this.update_entries(cx)));
 266            this.subscriptions
 267                .push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx)));
 268
 269            this
 270        })
 271    }
 272
 273    pub fn load(
 274        workspace: WeakViewHandle<Workspace>,
 275        cx: AsyncAppContext,
 276    ) -> Task<Result<ViewHandle<Self>>> {
 277        cx.spawn(|mut cx| async move {
 278            let serialized_panel = if let Some(panel) = cx
 279                .background()
 280                .spawn(async move { KEY_VALUE_STORE.read_kvp(CHANNELS_PANEL_KEY) })
 281                .await
 282                .log_err()
 283                .flatten()
 284            {
 285                Some(serde_json::from_str::<SerializedChannelsPanel>(&panel)?)
 286            } else {
 287                None
 288            };
 289
 290            workspace.update(&mut cx, |workspace, cx| {
 291                let panel = CollabPanel::new(workspace, cx);
 292                if let Some(serialized_panel) = serialized_panel {
 293                    panel.update(cx, |panel, cx| {
 294                        panel.width = serialized_panel.width;
 295                        cx.notify();
 296                    });
 297                }
 298                panel
 299            })
 300        })
 301    }
 302
 303    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 304        let width = self.width;
 305        self.pending_serialization = cx.background().spawn(
 306            async move {
 307                KEY_VALUE_STORE
 308                    .write_kvp(
 309                        CHANNELS_PANEL_KEY.into(),
 310                        serde_json::to_string(&SerializedChannelsPanel { width })?,
 311                    )
 312                    .await?;
 313                anyhow::Ok(())
 314            }
 315            .log_err(),
 316        );
 317    }
 318
 319    fn update_entries(&mut self, cx: &mut ViewContext<Self>) {
 320        let user_store = self.user_store.read(cx);
 321        let query = self.filter_editor.read(cx).text(cx);
 322        let executor = cx.background().clone();
 323
 324        let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
 325        let old_entries = mem::take(&mut self.entries);
 326
 327        if let Some(room) = ActiveCall::global(cx).read(cx).room() {
 328            let room = room.read(cx);
 329            let mut participant_entries = Vec::new();
 330
 331            // Populate the active user.
 332            if let Some(user) = user_store.current_user() {
 333                self.match_candidates.clear();
 334                self.match_candidates.push(StringMatchCandidate {
 335                    id: 0,
 336                    string: user.github_login.clone(),
 337                    char_bag: user.github_login.chars().collect(),
 338                });
 339                let matches = executor.block(match_strings(
 340                    &self.match_candidates,
 341                    &query,
 342                    true,
 343                    usize::MAX,
 344                    &Default::default(),
 345                    executor.clone(),
 346                ));
 347                if !matches.is_empty() {
 348                    let user_id = user.id;
 349                    participant_entries.push(ContactEntry::CallParticipant {
 350                        user,
 351                        is_pending: false,
 352                    });
 353                    let mut projects = room.local_participant().projects.iter().peekable();
 354                    while let Some(project) = projects.next() {
 355                        participant_entries.push(ContactEntry::ParticipantProject {
 356                            project_id: project.id,
 357                            worktree_root_names: project.worktree_root_names.clone(),
 358                            host_user_id: user_id,
 359                            is_last: projects.peek().is_none(),
 360                        });
 361                    }
 362                }
 363            }
 364
 365            // Populate remote participants.
 366            self.match_candidates.clear();
 367            self.match_candidates
 368                .extend(room.remote_participants().iter().map(|(_, participant)| {
 369                    StringMatchCandidate {
 370                        id: participant.user.id as usize,
 371                        string: participant.user.github_login.clone(),
 372                        char_bag: participant.user.github_login.chars().collect(),
 373                    }
 374                }));
 375            let matches = executor.block(match_strings(
 376                &self.match_candidates,
 377                &query,
 378                true,
 379                usize::MAX,
 380                &Default::default(),
 381                executor.clone(),
 382            ));
 383            for mat in matches {
 384                let user_id = mat.candidate_id as u64;
 385                let participant = &room.remote_participants()[&user_id];
 386                participant_entries.push(ContactEntry::CallParticipant {
 387                    user: participant.user.clone(),
 388                    is_pending: false,
 389                });
 390                let mut projects = participant.projects.iter().peekable();
 391                while let Some(project) = projects.next() {
 392                    participant_entries.push(ContactEntry::ParticipantProject {
 393                        project_id: project.id,
 394                        worktree_root_names: project.worktree_root_names.clone(),
 395                        host_user_id: participant.user.id,
 396                        is_last: projects.peek().is_none() && participant.video_tracks.is_empty(),
 397                    });
 398                }
 399                if !participant.video_tracks.is_empty() {
 400                    participant_entries.push(ContactEntry::ParticipantScreen {
 401                        peer_id: participant.peer_id,
 402                        is_last: true,
 403                    });
 404                }
 405            }
 406
 407            // Populate pending participants.
 408            self.match_candidates.clear();
 409            self.match_candidates
 410                .extend(
 411                    room.pending_participants()
 412                        .iter()
 413                        .enumerate()
 414                        .map(|(id, participant)| StringMatchCandidate {
 415                            id,
 416                            string: participant.github_login.clone(),
 417                            char_bag: participant.github_login.chars().collect(),
 418                        }),
 419                );
 420            let matches = executor.block(match_strings(
 421                &self.match_candidates,
 422                &query,
 423                true,
 424                usize::MAX,
 425                &Default::default(),
 426                executor.clone(),
 427            ));
 428            participant_entries.extend(matches.iter().map(|mat| ContactEntry::CallParticipant {
 429                user: room.pending_participants()[mat.candidate_id].clone(),
 430                is_pending: true,
 431            }));
 432
 433            if !participant_entries.is_empty() {
 434                self.entries.push(ContactEntry::Header(Section::ActiveCall));
 435                if !self.collapsed_sections.contains(&Section::ActiveCall) {
 436                    self.entries.extend(participant_entries);
 437                }
 438            }
 439        }
 440
 441        let mut request_entries = Vec::new();
 442        let incoming = user_store.incoming_contact_requests();
 443        if !incoming.is_empty() {
 444            self.match_candidates.clear();
 445            self.match_candidates
 446                .extend(
 447                    incoming
 448                        .iter()
 449                        .enumerate()
 450                        .map(|(ix, user)| StringMatchCandidate {
 451                            id: ix,
 452                            string: user.github_login.clone(),
 453                            char_bag: user.github_login.chars().collect(),
 454                        }),
 455                );
 456            let matches = executor.block(match_strings(
 457                &self.match_candidates,
 458                &query,
 459                true,
 460                usize::MAX,
 461                &Default::default(),
 462                executor.clone(),
 463            ));
 464            request_entries.extend(
 465                matches
 466                    .iter()
 467                    .map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
 468            );
 469        }
 470
 471        let outgoing = user_store.outgoing_contact_requests();
 472        if !outgoing.is_empty() {
 473            self.match_candidates.clear();
 474            self.match_candidates
 475                .extend(
 476                    outgoing
 477                        .iter()
 478                        .enumerate()
 479                        .map(|(ix, user)| StringMatchCandidate {
 480                            id: ix,
 481                            string: user.github_login.clone(),
 482                            char_bag: user.github_login.chars().collect(),
 483                        }),
 484                );
 485            let matches = executor.block(match_strings(
 486                &self.match_candidates,
 487                &query,
 488                true,
 489                usize::MAX,
 490                &Default::default(),
 491                executor.clone(),
 492            ));
 493            request_entries.extend(
 494                matches
 495                    .iter()
 496                    .map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
 497            );
 498        }
 499
 500        if !request_entries.is_empty() {
 501            self.entries.push(ContactEntry::Header(Section::Requests));
 502            if !self.collapsed_sections.contains(&Section::Requests) {
 503                self.entries.append(&mut request_entries);
 504            }
 505        }
 506
 507        let contacts = user_store.contacts();
 508        if !contacts.is_empty() {
 509            self.match_candidates.clear();
 510            self.match_candidates
 511                .extend(
 512                    contacts
 513                        .iter()
 514                        .enumerate()
 515                        .map(|(ix, contact)| StringMatchCandidate {
 516                            id: ix,
 517                            string: contact.user.github_login.clone(),
 518                            char_bag: contact.user.github_login.chars().collect(),
 519                        }),
 520                );
 521
 522            let matches = executor.block(match_strings(
 523                &self.match_candidates,
 524                &query,
 525                true,
 526                usize::MAX,
 527                &Default::default(),
 528                executor.clone(),
 529            ));
 530
 531            let (mut online_contacts, offline_contacts) = matches
 532                .iter()
 533                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
 534            if let Some(room) = ActiveCall::global(cx).read(cx).room() {
 535                let room = room.read(cx);
 536                online_contacts.retain(|contact| {
 537                    let contact = &contacts[contact.candidate_id];
 538                    !room.contains_participant(contact.user.id)
 539                });
 540            }
 541
 542            for (matches, section) in [
 543                (online_contacts, Section::Online),
 544                (offline_contacts, Section::Offline),
 545            ] {
 546                if !matches.is_empty() {
 547                    self.entries.push(ContactEntry::Header(section));
 548                    if !self.collapsed_sections.contains(&section) {
 549                        let active_call = &ActiveCall::global(cx).read(cx);
 550                        for mat in matches {
 551                            let contact = &contacts[mat.candidate_id];
 552                            self.entries.push(ContactEntry::Contact {
 553                                contact: contact.clone(),
 554                                calling: active_call.pending_invites().contains(&contact.user.id),
 555                            });
 556                        }
 557                    }
 558                }
 559            }
 560        }
 561
 562        if let Some(prev_selected_entry) = prev_selected_entry {
 563            self.selection.take();
 564            for (ix, entry) in self.entries.iter().enumerate() {
 565                if *entry == prev_selected_entry {
 566                    self.selection = Some(ix);
 567                    break;
 568                }
 569            }
 570        }
 571
 572        let old_scroll_top = self.list_state.logical_scroll_top();
 573        self.list_state.reset(self.entries.len());
 574
 575        // Attempt to maintain the same scroll position.
 576        if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
 577            let new_scroll_top = self
 578                .entries
 579                .iter()
 580                .position(|entry| entry == old_top_entry)
 581                .map(|item_ix| ListOffset {
 582                    item_ix,
 583                    offset_in_item: old_scroll_top.offset_in_item,
 584                })
 585                .or_else(|| {
 586                    let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
 587                    let item_ix = self
 588                        .entries
 589                        .iter()
 590                        .position(|entry| entry == entry_after_old_top)?;
 591                    Some(ListOffset {
 592                        item_ix,
 593                        offset_in_item: 0.,
 594                    })
 595                })
 596                .or_else(|| {
 597                    let entry_before_old_top =
 598                        old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
 599                    let item_ix = self
 600                        .entries
 601                        .iter()
 602                        .position(|entry| entry == entry_before_old_top)?;
 603                    Some(ListOffset {
 604                        item_ix,
 605                        offset_in_item: 0.,
 606                    })
 607                });
 608
 609            self.list_state
 610                .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
 611        }
 612
 613        cx.notify();
 614    }
 615
 616    fn render_call_participant(
 617        user: &User,
 618        is_pending: bool,
 619        is_selected: bool,
 620        theme: &theme::ContactList,
 621    ) -> AnyElement<Self> {
 622        Flex::row()
 623            .with_children(user.avatar.clone().map(|avatar| {
 624                Image::from_data(avatar)
 625                    .with_style(theme.contact_avatar)
 626                    .aligned()
 627                    .left()
 628            }))
 629            .with_child(
 630                Label::new(
 631                    user.github_login.clone(),
 632                    theme.contact_username.text.clone(),
 633                )
 634                .contained()
 635                .with_style(theme.contact_username.container)
 636                .aligned()
 637                .left()
 638                .flex(1., true),
 639            )
 640            .with_children(if is_pending {
 641                Some(
 642                    Label::new("Calling", theme.calling_indicator.text.clone())
 643                        .contained()
 644                        .with_style(theme.calling_indicator.container)
 645                        .aligned(),
 646                )
 647            } else {
 648                None
 649            })
 650            .constrained()
 651            .with_height(theme.row_height)
 652            .contained()
 653            .with_style(
 654                *theme
 655                    .contact_row
 656                    .in_state(is_selected)
 657                    .style_for(&mut Default::default()),
 658            )
 659            .into_any()
 660    }
 661
 662    fn render_participant_project(
 663        project_id: u64,
 664        worktree_root_names: &[String],
 665        host_user_id: u64,
 666        is_current: bool,
 667        is_last: bool,
 668        is_selected: bool,
 669        theme: &theme::ContactList,
 670        cx: &mut ViewContext<Self>,
 671    ) -> AnyElement<Self> {
 672        enum JoinProject {}
 673
 674        let font_cache = cx.font_cache();
 675        let host_avatar_height = theme
 676            .contact_avatar
 677            .width
 678            .or(theme.contact_avatar.height)
 679            .unwrap_or(0.);
 680        let row = &theme.project_row.inactive_state().default;
 681        let tree_branch = theme.tree_branch;
 682        let line_height = row.name.text.line_height(font_cache);
 683        let cap_height = row.name.text.cap_height(font_cache);
 684        let baseline_offset =
 685            row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
 686        let project_name = if worktree_root_names.is_empty() {
 687            "untitled".to_string()
 688        } else {
 689            worktree_root_names.join(", ")
 690        };
 691
 692        MouseEventHandler::<JoinProject, Self>::new(project_id as usize, cx, |mouse_state, _| {
 693            let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
 694            let row = theme
 695                .project_row
 696                .in_state(is_selected)
 697                .style_for(mouse_state);
 698
 699            Flex::row()
 700                .with_child(
 701                    Stack::new()
 702                        .with_child(Canvas::new(move |scene, bounds, _, _, _| {
 703                            let start_x =
 704                                bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.);
 705                            let end_x = bounds.max_x();
 706                            let start_y = bounds.min_y();
 707                            let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
 708
 709                            scene.push_quad(gpui::Quad {
 710                                bounds: RectF::from_points(
 711                                    vec2f(start_x, start_y),
 712                                    vec2f(
 713                                        start_x + tree_branch.width,
 714                                        if is_last { end_y } else { bounds.max_y() },
 715                                    ),
 716                                ),
 717                                background: Some(tree_branch.color),
 718                                border: gpui::Border::default(),
 719                                corner_radius: 0.,
 720                            });
 721                            scene.push_quad(gpui::Quad {
 722                                bounds: RectF::from_points(
 723                                    vec2f(start_x, end_y),
 724                                    vec2f(end_x, end_y + tree_branch.width),
 725                                ),
 726                                background: Some(tree_branch.color),
 727                                border: gpui::Border::default(),
 728                                corner_radius: 0.,
 729                            });
 730                        }))
 731                        .constrained()
 732                        .with_width(host_avatar_height),
 733                )
 734                .with_child(
 735                    Label::new(project_name, row.name.text.clone())
 736                        .aligned()
 737                        .left()
 738                        .contained()
 739                        .with_style(row.name.container)
 740                        .flex(1., false),
 741                )
 742                .constrained()
 743                .with_height(theme.row_height)
 744                .contained()
 745                .with_style(row.container)
 746        })
 747        .with_cursor_style(if !is_current {
 748            CursorStyle::PointingHand
 749        } else {
 750            CursorStyle::Arrow
 751        })
 752        .on_click(MouseButton::Left, move |_, this, cx| {
 753            if !is_current {
 754                if let Some(workspace) = this.workspace.upgrade(cx) {
 755                    let app_state = workspace.read(cx).app_state().clone();
 756                    workspace::join_remote_project(project_id, host_user_id, app_state, cx)
 757                        .detach_and_log_err(cx);
 758                }
 759            }
 760        })
 761        .into_any()
 762    }
 763
 764    fn render_participant_screen(
 765        peer_id: PeerId,
 766        is_last: bool,
 767        is_selected: bool,
 768        theme: &theme::ContactList,
 769        cx: &mut ViewContext<Self>,
 770    ) -> AnyElement<Self> {
 771        enum OpenSharedScreen {}
 772
 773        let font_cache = cx.font_cache();
 774        let host_avatar_height = theme
 775            .contact_avatar
 776            .width
 777            .or(theme.contact_avatar.height)
 778            .unwrap_or(0.);
 779        let row = &theme.project_row.inactive_state().default;
 780        let tree_branch = theme.tree_branch;
 781        let line_height = row.name.text.line_height(font_cache);
 782        let cap_height = row.name.text.cap_height(font_cache);
 783        let baseline_offset =
 784            row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
 785
 786        MouseEventHandler::<OpenSharedScreen, Self>::new(
 787            peer_id.as_u64() as usize,
 788            cx,
 789            |mouse_state, _| {
 790                let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
 791                let row = theme
 792                    .project_row
 793                    .in_state(is_selected)
 794                    .style_for(mouse_state);
 795
 796                Flex::row()
 797                    .with_child(
 798                        Stack::new()
 799                            .with_child(Canvas::new(move |scene, bounds, _, _, _| {
 800                                let start_x = bounds.min_x() + (bounds.width() / 2.)
 801                                    - (tree_branch.width / 2.);
 802                                let end_x = bounds.max_x();
 803                                let start_y = bounds.min_y();
 804                                let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
 805
 806                                scene.push_quad(gpui::Quad {
 807                                    bounds: RectF::from_points(
 808                                        vec2f(start_x, start_y),
 809                                        vec2f(
 810                                            start_x + tree_branch.width,
 811                                            if is_last { end_y } else { bounds.max_y() },
 812                                        ),
 813                                    ),
 814                                    background: Some(tree_branch.color),
 815                                    border: gpui::Border::default(),
 816                                    corner_radius: 0.,
 817                                });
 818                                scene.push_quad(gpui::Quad {
 819                                    bounds: RectF::from_points(
 820                                        vec2f(start_x, end_y),
 821                                        vec2f(end_x, end_y + tree_branch.width),
 822                                    ),
 823                                    background: Some(tree_branch.color),
 824                                    border: gpui::Border::default(),
 825                                    corner_radius: 0.,
 826                                });
 827                            }))
 828                            .constrained()
 829                            .with_width(host_avatar_height),
 830                    )
 831                    .with_child(
 832                        Svg::new("icons/disable_screen_sharing_12.svg")
 833                            .with_color(row.icon.color)
 834                            .constrained()
 835                            .with_width(row.icon.width)
 836                            .aligned()
 837                            .left()
 838                            .contained()
 839                            .with_style(row.icon.container),
 840                    )
 841                    .with_child(
 842                        Label::new("Screen", row.name.text.clone())
 843                            .aligned()
 844                            .left()
 845                            .contained()
 846                            .with_style(row.name.container)
 847                            .flex(1., false),
 848                    )
 849                    .constrained()
 850                    .with_height(theme.row_height)
 851                    .contained()
 852                    .with_style(row.container)
 853            },
 854        )
 855        .with_cursor_style(CursorStyle::PointingHand)
 856        .on_click(MouseButton::Left, move |_, this, cx| {
 857            if let Some(workspace) = this.workspace.upgrade(cx) {
 858                workspace.update(cx, |workspace, cx| {
 859                    workspace.open_shared_screen(peer_id, cx)
 860                });
 861            }
 862        })
 863        .into_any()
 864    }
 865
 866    fn render_header(
 867        section: Section,
 868        theme: &theme::ContactList,
 869        is_selected: bool,
 870        is_collapsed: bool,
 871        cx: &mut ViewContext<Self>,
 872    ) -> AnyElement<Self> {
 873        enum Header {}
 874        enum LeaveCallContactList {}
 875
 876        let header_style = theme
 877            .header_row
 878            .in_state(is_selected)
 879            .style_for(&mut Default::default());
 880        let text = match section {
 881            Section::ActiveCall => "Collaborators",
 882            Section::Requests => "Contact Requests",
 883            Section::Online => "Online",
 884            Section::Offline => "Offline",
 885        };
 886        let leave_call = if section == Section::ActiveCall {
 887            Some(
 888                MouseEventHandler::<LeaveCallContactList, Self>::new(0, cx, |state, _| {
 889                    let style = theme.leave_call.style_for(state);
 890                    Label::new("Leave Call", style.text.clone())
 891                        .contained()
 892                        .with_style(style.container)
 893                })
 894                .on_click(MouseButton::Left, |_, _, cx| {
 895                    ActiveCall::global(cx)
 896                        .update(cx, |call, cx| call.hang_up(cx))
 897                        .detach_and_log_err(cx);
 898                })
 899                .aligned(),
 900            )
 901        } else {
 902            None
 903        };
 904
 905        let icon_size = theme.section_icon_size;
 906        MouseEventHandler::<Header, Self>::new(section as usize, cx, |_, _| {
 907            Flex::row()
 908                .with_child(
 909                    Svg::new(if is_collapsed {
 910                        "icons/chevron_right_8.svg"
 911                    } else {
 912                        "icons/chevron_down_8.svg"
 913                    })
 914                    .with_color(header_style.text.color)
 915                    .constrained()
 916                    .with_max_width(icon_size)
 917                    .with_max_height(icon_size)
 918                    .aligned()
 919                    .constrained()
 920                    .with_width(icon_size),
 921                )
 922                .with_child(
 923                    Label::new(text, header_style.text.clone())
 924                        .aligned()
 925                        .left()
 926                        .contained()
 927                        .with_margin_left(theme.contact_username.container.margin.left)
 928                        .flex(1., true),
 929                )
 930                .with_children(leave_call)
 931                .constrained()
 932                .with_height(theme.row_height)
 933                .contained()
 934                .with_style(header_style.container)
 935        })
 936        .with_cursor_style(CursorStyle::PointingHand)
 937        .on_click(MouseButton::Left, move |_, this, cx| {
 938            this.toggle_expanded(section, cx);
 939        })
 940        .into_any()
 941    }
 942
 943    fn render_contact(
 944        contact: &Contact,
 945        calling: bool,
 946        project: &ModelHandle<Project>,
 947        theme: &theme::ContactList,
 948        is_selected: bool,
 949        cx: &mut ViewContext<Self>,
 950    ) -> AnyElement<Self> {
 951        let online = contact.online;
 952        let busy = contact.busy || calling;
 953        let user_id = contact.user.id;
 954        let github_login = contact.user.github_login.clone();
 955        let initial_project = project.clone();
 956        let mut event_handler =
 957            MouseEventHandler::<Contact, Self>::new(contact.user.id as usize, cx, |_, cx| {
 958                Flex::row()
 959                    .with_children(contact.user.avatar.clone().map(|avatar| {
 960                        let status_badge = if contact.online {
 961                            Some(
 962                                Empty::new()
 963                                    .collapsed()
 964                                    .contained()
 965                                    .with_style(if busy {
 966                                        theme.contact_status_busy
 967                                    } else {
 968                                        theme.contact_status_free
 969                                    })
 970                                    .aligned(),
 971                            )
 972                        } else {
 973                            None
 974                        };
 975                        Stack::new()
 976                            .with_child(
 977                                Image::from_data(avatar)
 978                                    .with_style(theme.contact_avatar)
 979                                    .aligned()
 980                                    .left(),
 981                            )
 982                            .with_children(status_badge)
 983                    }))
 984                    .with_child(
 985                        Label::new(
 986                            contact.user.github_login.clone(),
 987                            theme.contact_username.text.clone(),
 988                        )
 989                        .contained()
 990                        .with_style(theme.contact_username.container)
 991                        .aligned()
 992                        .left()
 993                        .flex(1., true),
 994                    )
 995                    .with_child(
 996                        MouseEventHandler::<Cancel, Self>::new(
 997                            contact.user.id as usize,
 998                            cx,
 999                            |mouse_state, _| {
1000                                let button_style = theme.contact_button.style_for(mouse_state);
1001                                render_icon_button(button_style, "icons/x_mark_8.svg")
1002                                    .aligned()
1003                                    .flex_float()
1004                            },
1005                        )
1006                        .with_padding(Padding::uniform(2.))
1007                        .with_cursor_style(CursorStyle::PointingHand)
1008                        .on_click(MouseButton::Left, move |_, this, cx| {
1009                            this.remove_contact(user_id, &github_login, cx);
1010                        })
1011                        .flex_float(),
1012                    )
1013                    .with_children(if calling {
1014                        Some(
1015                            Label::new("Calling", theme.calling_indicator.text.clone())
1016                                .contained()
1017                                .with_style(theme.calling_indicator.container)
1018                                .aligned(),
1019                        )
1020                    } else {
1021                        None
1022                    })
1023                    .constrained()
1024                    .with_height(theme.row_height)
1025                    .contained()
1026                    .with_style(
1027                        *theme
1028                            .contact_row
1029                            .in_state(is_selected)
1030                            .style_for(&mut Default::default()),
1031                    )
1032            })
1033            .on_click(MouseButton::Left, move |_, this, cx| {
1034                if online && !busy {
1035                    this.call(user_id, Some(initial_project.clone()), cx);
1036                }
1037            });
1038
1039        if online {
1040            event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand);
1041        }
1042
1043        event_handler.into_any()
1044    }
1045
1046    fn render_contact_request(
1047        user: Arc<User>,
1048        user_store: ModelHandle<UserStore>,
1049        theme: &theme::ContactList,
1050        is_incoming: bool,
1051        is_selected: bool,
1052        cx: &mut ViewContext<Self>,
1053    ) -> AnyElement<Self> {
1054        enum Decline {}
1055        enum Accept {}
1056        enum Cancel {}
1057
1058        let mut row = Flex::row()
1059            .with_children(user.avatar.clone().map(|avatar| {
1060                Image::from_data(avatar)
1061                    .with_style(theme.contact_avatar)
1062                    .aligned()
1063                    .left()
1064            }))
1065            .with_child(
1066                Label::new(
1067                    user.github_login.clone(),
1068                    theme.contact_username.text.clone(),
1069                )
1070                .contained()
1071                .with_style(theme.contact_username.container)
1072                .aligned()
1073                .left()
1074                .flex(1., true),
1075            );
1076
1077        let user_id = user.id;
1078        let github_login = user.github_login.clone();
1079        let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
1080        let button_spacing = theme.contact_button_spacing;
1081
1082        if is_incoming {
1083            row.add_child(
1084                MouseEventHandler::<Decline, Self>::new(user.id as usize, cx, |mouse_state, _| {
1085                    let button_style = if is_contact_request_pending {
1086                        &theme.disabled_button
1087                    } else {
1088                        theme.contact_button.style_for(mouse_state)
1089                    };
1090                    render_icon_button(button_style, "icons/x_mark_8.svg").aligned()
1091                })
1092                .with_cursor_style(CursorStyle::PointingHand)
1093                .on_click(MouseButton::Left, move |_, this, cx| {
1094                    this.respond_to_contact_request(user_id, false, cx);
1095                })
1096                .contained()
1097                .with_margin_right(button_spacing),
1098            );
1099
1100            row.add_child(
1101                MouseEventHandler::<Accept, Self>::new(user.id as usize, cx, |mouse_state, _| {
1102                    let button_style = if is_contact_request_pending {
1103                        &theme.disabled_button
1104                    } else {
1105                        theme.contact_button.style_for(mouse_state)
1106                    };
1107                    render_icon_button(button_style, "icons/check_8.svg")
1108                        .aligned()
1109                        .flex_float()
1110                })
1111                .with_cursor_style(CursorStyle::PointingHand)
1112                .on_click(MouseButton::Left, move |_, this, cx| {
1113                    this.respond_to_contact_request(user_id, true, cx);
1114                }),
1115            );
1116        } else {
1117            row.add_child(
1118                MouseEventHandler::<Cancel, Self>::new(user.id as usize, cx, |mouse_state, _| {
1119                    let button_style = if is_contact_request_pending {
1120                        &theme.disabled_button
1121                    } else {
1122                        theme.contact_button.style_for(mouse_state)
1123                    };
1124                    render_icon_button(button_style, "icons/x_mark_8.svg")
1125                        .aligned()
1126                        .flex_float()
1127                })
1128                .with_padding(Padding::uniform(2.))
1129                .with_cursor_style(CursorStyle::PointingHand)
1130                .on_click(MouseButton::Left, move |_, this, cx| {
1131                    this.remove_contact(user_id, &github_login, cx);
1132                })
1133                .flex_float(),
1134            );
1135        }
1136
1137        row.constrained()
1138            .with_height(theme.row_height)
1139            .contained()
1140            .with_style(
1141                *theme
1142                    .contact_row
1143                    .in_state(is_selected)
1144                    .style_for(&mut Default::default()),
1145            )
1146            .into_any()
1147    }
1148
1149    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
1150        if self.contact_finder.take().is_some() {
1151            cx.notify();
1152            return;
1153        }
1154
1155        let did_clear = self.filter_editor.update(cx, |editor, cx| {
1156            if editor.buffer().read(cx).len(cx) > 0 {
1157                editor.set_text("", cx);
1158                true
1159            } else {
1160                false
1161            }
1162        });
1163
1164        if !did_clear {
1165            cx.emit(Event::Dismissed);
1166        }
1167    }
1168
1169    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
1170        if let Some(ix) = self.selection {
1171            if self.entries.len() > ix + 1 {
1172                self.selection = Some(ix + 1);
1173            }
1174        } else if !self.entries.is_empty() {
1175            self.selection = Some(0);
1176        }
1177        self.list_state.reset(self.entries.len());
1178        if let Some(ix) = self.selection {
1179            self.list_state.scroll_to(ListOffset {
1180                item_ix: ix,
1181                offset_in_item: 0.,
1182            });
1183        }
1184        cx.notify();
1185    }
1186
1187    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
1188        if let Some(ix) = self.selection {
1189            if ix > 0 {
1190                self.selection = Some(ix - 1);
1191            } else {
1192                self.selection = None;
1193            }
1194        }
1195        self.list_state.reset(self.entries.len());
1196        if let Some(ix) = self.selection {
1197            self.list_state.scroll_to(ListOffset {
1198                item_ix: ix,
1199                offset_in_item: 0.,
1200            });
1201        }
1202        cx.notify();
1203    }
1204
1205    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
1206        if let Some(selection) = self.selection {
1207            if let Some(entry) = self.entries.get(selection) {
1208                match entry {
1209                    ContactEntry::Header(section) => {
1210                        self.toggle_expanded(*section, cx);
1211                    }
1212                    ContactEntry::Contact { contact, calling } => {
1213                        if contact.online && !contact.busy && !calling {
1214                            self.call(contact.user.id, Some(self.project.clone()), cx);
1215                        }
1216                    }
1217                    ContactEntry::ParticipantProject {
1218                        project_id,
1219                        host_user_id,
1220                        ..
1221                    } => {
1222                        if let Some(workspace) = self.workspace.upgrade(cx) {
1223                            let app_state = workspace.read(cx).app_state().clone();
1224                            workspace::join_remote_project(
1225                                *project_id,
1226                                *host_user_id,
1227                                app_state,
1228                                cx,
1229                            )
1230                            .detach_and_log_err(cx);
1231                        }
1232                    }
1233                    ContactEntry::ParticipantScreen { peer_id, .. } => {
1234                        if let Some(workspace) = self.workspace.upgrade(cx) {
1235                            workspace.update(cx, |workspace, cx| {
1236                                workspace.open_shared_screen(*peer_id, cx)
1237                            });
1238                        }
1239                    }
1240                    _ => {}
1241                }
1242            }
1243        }
1244    }
1245
1246    fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
1247        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
1248            self.collapsed_sections.remove(ix);
1249        } else {
1250            self.collapsed_sections.push(section);
1251        }
1252        self.update_entries(cx);
1253    }
1254
1255    fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
1256        if self.contact_finder.take().is_none() {
1257            let child = cx.add_view(|cx| {
1258                let finder = build_contact_finder(self.user_store.clone(), cx);
1259                finder.set_query(self.filter_editor.read(cx).text(cx), cx);
1260                finder
1261            });
1262            cx.focus(&child);
1263            // self.subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
1264            //     // PickerEvent::Dismiss => cx.emit(Event::Dismissed),
1265            // }));
1266            self.contact_finder = Some(child);
1267        }
1268        cx.notify();
1269    }
1270
1271    fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
1272        let user_store = self.user_store.clone();
1273        let prompt_message = format!(
1274            "Are you sure you want to remove \"{}\" from your contacts?",
1275            github_login
1276        );
1277        let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
1278        let window_id = cx.window_id();
1279        cx.spawn(|_, mut cx| async move {
1280            if answer.next().await == Some(0) {
1281                if let Err(e) = user_store
1282                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
1283                    .await
1284                {
1285                    cx.prompt(
1286                        window_id,
1287                        PromptLevel::Info,
1288                        &format!("Failed to remove contact: {}", e),
1289                        &["Ok"],
1290                    );
1291                }
1292            }
1293        })
1294        .detach();
1295    }
1296
1297    fn respond_to_contact_request(
1298        &mut self,
1299        user_id: u64,
1300        accept: bool,
1301        cx: &mut ViewContext<Self>,
1302    ) {
1303        self.user_store
1304            .update(cx, |store, cx| {
1305                store.respond_to_contact_request(user_id, accept, cx)
1306            })
1307            .detach();
1308    }
1309
1310    fn call(
1311        &mut self,
1312        recipient_user_id: u64,
1313        initial_project: Option<ModelHandle<Project>>,
1314        cx: &mut ViewContext<Self>,
1315    ) {
1316        ActiveCall::global(cx)
1317            .update(cx, |call, cx| {
1318                call.invite(recipient_user_id, initial_project, cx)
1319            })
1320            .detach_and_log_err(cx);
1321    }
1322}
1323
1324impl View for CollabPanel {
1325    fn ui_name() -> &'static str {
1326        "CollabPanel"
1327    }
1328
1329    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
1330        if !self.has_focus {
1331            self.has_focus = true;
1332            cx.emit(Event::Focus);
1333        }
1334    }
1335
1336    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
1337        self.has_focus = false;
1338    }
1339
1340    fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
1341        enum AddContact {}
1342        let theme = theme::current(cx).clone();
1343
1344        Stack::new()
1345            .with_child(if let Some(finder) = &self.contact_finder {
1346                ChildView::new(&finder, cx).into_any()
1347            } else {
1348                Flex::column()
1349                    .with_child(
1350                        Flex::row()
1351                            .with_child(
1352                                ChildView::new(&self.filter_editor, cx)
1353                                    .contained()
1354                                    .with_style(theme.contact_list.user_query_editor.container)
1355                                    .flex(1.0, true),
1356                            )
1357                            .with_child(
1358                                MouseEventHandler::<AddContact, Self>::new(0, cx, |_, _| {
1359                                    render_icon_button(
1360                                        &theme.contact_list.add_contact_button,
1361                                        "icons/user_plus_16.svg",
1362                                    )
1363                                })
1364                                .with_cursor_style(CursorStyle::PointingHand)
1365                                .on_click(MouseButton::Left, |_, this, cx| {
1366                                    this.toggle_contact_finder(cx);
1367                                })
1368                                .with_tooltip::<AddContact>(
1369                                    0,
1370                                    "Search for new contact".into(),
1371                                    None,
1372                                    theme.tooltip.clone(),
1373                                    cx,
1374                                )
1375                                .constrained()
1376                                .with_height(theme.contact_list.user_query_editor_height)
1377                                .with_width(theme.contact_list.user_query_editor_height),
1378                            )
1379                            .constrained()
1380                            .with_width(self.size(cx)),
1381                    )
1382                    .with_child(
1383                        List::new(self.list_state.clone())
1384                            .constrained()
1385                            .with_width(self.size(cx))
1386                            .flex(1., true)
1387                            .into_any(),
1388                    )
1389                    .constrained()
1390                    .with_width(self.size(cx))
1391                    .into_any()
1392            })
1393            .with_child(ChildView::new(&self.context_menu, cx))
1394            .into_any_named("channels panel")
1395            .into_any()
1396    }
1397}
1398
1399impl Panel for CollabPanel {
1400    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
1401        match settings::get::<ChannelsPanelSettings>(cx).dock {
1402            ChannelsPanelDockPosition::Left => DockPosition::Left,
1403            ChannelsPanelDockPosition::Right => DockPosition::Right,
1404        }
1405    }
1406
1407    fn position_is_valid(&self, position: DockPosition) -> bool {
1408        matches!(position, DockPosition::Left | DockPosition::Right)
1409    }
1410
1411    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1412        settings::update_settings_file::<ChannelsPanelSettings>(
1413            self.fs.clone(),
1414            cx,
1415            move |settings| {
1416                let dock = match position {
1417                    DockPosition::Left | DockPosition::Bottom => ChannelsPanelDockPosition::Left,
1418                    DockPosition::Right => ChannelsPanelDockPosition::Right,
1419                };
1420                settings.dock = Some(dock);
1421            },
1422        );
1423    }
1424
1425    fn size(&self, cx: &gpui::WindowContext) -> f32 {
1426        self.width
1427            .unwrap_or_else(|| settings::get::<ChannelsPanelSettings>(cx).default_width)
1428    }
1429
1430    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
1431        self.width = Some(size);
1432        self.serialize(cx);
1433        cx.notify();
1434    }
1435
1436    fn icon_path(&self) -> &'static str {
1437        "icons/radix/person.svg"
1438    }
1439
1440    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
1441        ("Channels Panel".to_string(), Some(Box::new(ToggleFocus)))
1442    }
1443
1444    fn should_change_position_on_event(event: &Self::Event) -> bool {
1445        matches!(event, Event::DockPositionChanged)
1446    }
1447
1448    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
1449        self.has_focus
1450    }
1451
1452    fn is_focus_event(event: &Self::Event) -> bool {
1453        matches!(event, Event::Focus)
1454    }
1455}
1456
1457impl PartialEq for ContactEntry {
1458    fn eq(&self, other: &Self) -> bool {
1459        match self {
1460            ContactEntry::Header(section_1) => {
1461                if let ContactEntry::Header(section_2) = other {
1462                    return section_1 == section_2;
1463                }
1464            }
1465            ContactEntry::CallParticipant { user: user_1, .. } => {
1466                if let ContactEntry::CallParticipant { user: user_2, .. } = other {
1467                    return user_1.id == user_2.id;
1468                }
1469            }
1470            ContactEntry::ParticipantProject {
1471                project_id: project_id_1,
1472                ..
1473            } => {
1474                if let ContactEntry::ParticipantProject {
1475                    project_id: project_id_2,
1476                    ..
1477                } = other
1478                {
1479                    return project_id_1 == project_id_2;
1480                }
1481            }
1482            ContactEntry::ParticipantScreen {
1483                peer_id: peer_id_1, ..
1484            } => {
1485                if let ContactEntry::ParticipantScreen {
1486                    peer_id: peer_id_2, ..
1487                } = other
1488                {
1489                    return peer_id_1 == peer_id_2;
1490                }
1491            }
1492            ContactEntry::IncomingRequest(user_1) => {
1493                if let ContactEntry::IncomingRequest(user_2) = other {
1494                    return user_1.id == user_2.id;
1495                }
1496            }
1497            ContactEntry::OutgoingRequest(user_1) => {
1498                if let ContactEntry::OutgoingRequest(user_2) = other {
1499                    return user_1.id == user_2.id;
1500                }
1501            }
1502            ContactEntry::Contact {
1503                contact: contact_1, ..
1504            } => {
1505                if let ContactEntry::Contact {
1506                    contact: contact_2, ..
1507                } = other
1508                {
1509                    return contact_1.user.id == contact_2.user.id;
1510                }
1511            }
1512        }
1513        false
1514    }
1515}
1516
1517fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
1518    Svg::new(svg_path)
1519        .with_color(style.color)
1520        .constrained()
1521        .with_width(style.icon_width)
1522        .aligned()
1523        .contained()
1524        .with_style(style.container)
1525        .constrained()
1526        .with_width(style.button_width)
1527        .with_height(style.button_width)
1528}