contacts_popover.rs

  1use std::sync::Arc;
  2
  3use crate::contact_finder;
  4use call::ActiveCall;
  5use client::{Contact, User, UserStore};
  6use editor::{Cancel, Editor};
  7use fuzzy::{match_strings, StringMatchCandidate};
  8use gpui::{
  9    elements::*, impl_actions, impl_internal_actions, keymap, AppContext, ClipboardItem,
 10    CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription,
 11    View, ViewContext, ViewHandle,
 12};
 13use menu::{Confirm, SelectNext, SelectPrev};
 14use project::Project;
 15use serde::Deserialize;
 16use settings::Settings;
 17use theme::IconButton;
 18
 19impl_actions!(contacts_popover, [RemoveContact, RespondToContactRequest]);
 20impl_internal_actions!(contacts_popover, [ToggleExpanded, Call]);
 21
 22pub fn init(cx: &mut MutableAppContext) {
 23    cx.add_action(ContactsPopover::remove_contact);
 24    cx.add_action(ContactsPopover::respond_to_contact_request);
 25    cx.add_action(ContactsPopover::clear_filter);
 26    cx.add_action(ContactsPopover::select_next);
 27    cx.add_action(ContactsPopover::select_prev);
 28    cx.add_action(ContactsPopover::confirm);
 29    cx.add_action(ContactsPopover::toggle_expanded);
 30    cx.add_action(ContactsPopover::call);
 31}
 32
 33#[derive(Clone, PartialEq)]
 34struct ToggleExpanded(Section);
 35
 36#[derive(Clone, PartialEq)]
 37struct Call {
 38    recipient_user_id: u64,
 39    initial_project: Option<ModelHandle<Project>>,
 40}
 41
 42#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
 43enum Section {
 44    Requests,
 45    Online,
 46    Offline,
 47}
 48
 49#[derive(Clone)]
 50enum ContactEntry {
 51    Header(Section),
 52    IncomingRequest(Arc<User>),
 53    OutgoingRequest(Arc<User>),
 54    Contact(Arc<Contact>),
 55}
 56
 57impl PartialEq for ContactEntry {
 58    fn eq(&self, other: &Self) -> bool {
 59        match self {
 60            ContactEntry::Header(section_1) => {
 61                if let ContactEntry::Header(section_2) = other {
 62                    return section_1 == section_2;
 63                }
 64            }
 65            ContactEntry::IncomingRequest(user_1) => {
 66                if let ContactEntry::IncomingRequest(user_2) = other {
 67                    return user_1.id == user_2.id;
 68                }
 69            }
 70            ContactEntry::OutgoingRequest(user_1) => {
 71                if let ContactEntry::OutgoingRequest(user_2) = other {
 72                    return user_1.id == user_2.id;
 73                }
 74            }
 75            ContactEntry::Contact(contact_1) => {
 76                if let ContactEntry::Contact(contact_2) = other {
 77                    return contact_1.user.id == contact_2.user.id;
 78                }
 79            }
 80        }
 81        false
 82    }
 83}
 84
 85#[derive(Clone, Deserialize, PartialEq)]
 86pub struct RequestContact(pub u64);
 87
 88#[derive(Clone, Deserialize, PartialEq)]
 89pub struct RemoveContact(pub u64);
 90
 91#[derive(Clone, Deserialize, PartialEq)]
 92pub struct RespondToContactRequest {
 93    pub user_id: u64,
 94    pub accept: bool,
 95}
 96
 97pub enum Event {
 98    Dismissed,
 99}
100
101pub struct ContactsPopover {
102    entries: Vec<ContactEntry>,
103    match_candidates: Vec<StringMatchCandidate>,
104    list_state: ListState,
105    project: ModelHandle<Project>,
106    user_store: ModelHandle<UserStore>,
107    filter_editor: ViewHandle<Editor>,
108    collapsed_sections: Vec<Section>,
109    selection: Option<usize>,
110    _subscriptions: Vec<Subscription>,
111}
112
113impl ContactsPopover {
114    pub fn new(
115        project: ModelHandle<Project>,
116        user_store: ModelHandle<UserStore>,
117        cx: &mut ViewContext<Self>,
118    ) -> Self {
119        let filter_editor = cx.add_view(|cx| {
120            let mut editor = Editor::single_line(
121                Some(|theme| theme.contacts_popover.user_query_editor.clone()),
122                cx,
123            );
124            editor.set_placeholder_text("Filter contacts", cx);
125            editor
126        });
127
128        cx.subscribe(&filter_editor, |this, _, event, cx| {
129            if let editor::Event::BufferEdited = event {
130                let query = this.filter_editor.read(cx).text(cx);
131                if !query.is_empty() {
132                    this.selection.take();
133                }
134                this.update_entries(cx);
135                if !query.is_empty() {
136                    this.selection = this
137                        .entries
138                        .iter()
139                        .position(|entry| !matches!(entry, ContactEntry::Header(_)));
140                }
141            }
142        })
143        .detach();
144
145        let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| {
146            let theme = cx.global::<Settings>().theme.clone();
147            let is_selected = this.selection == Some(ix);
148
149            match &this.entries[ix] {
150                ContactEntry::Header(section) => {
151                    let is_collapsed = this.collapsed_sections.contains(section);
152                    Self::render_header(
153                        *section,
154                        &theme.contacts_popover,
155                        is_selected,
156                        is_collapsed,
157                        cx,
158                    )
159                }
160                ContactEntry::IncomingRequest(user) => Self::render_contact_request(
161                    user.clone(),
162                    this.user_store.clone(),
163                    &theme.contacts_popover,
164                    true,
165                    is_selected,
166                    cx,
167                ),
168                ContactEntry::OutgoingRequest(user) => Self::render_contact_request(
169                    user.clone(),
170                    this.user_store.clone(),
171                    &theme.contacts_popover,
172                    false,
173                    is_selected,
174                    cx,
175                ),
176                ContactEntry::Contact(contact) => Self::render_contact(
177                    contact,
178                    &this.project,
179                    &theme.contacts_popover,
180                    is_selected,
181                    cx,
182                ),
183            }
184        });
185
186        let active_call = ActiveCall::global(cx);
187        let mut subscriptions = Vec::new();
188        subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx)));
189        subscriptions.push(cx.observe(&active_call, |_, _, cx| cx.notify()));
190
191        let mut this = Self {
192            list_state,
193            selection: None,
194            collapsed_sections: Default::default(),
195            entries: Default::default(),
196            match_candidates: Default::default(),
197            filter_editor,
198            _subscriptions: subscriptions,
199            project,
200            user_store,
201        };
202        this.update_entries(cx);
203        this
204    }
205
206    fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext<Self>) {
207        self.user_store
208            .update(cx, |store, cx| store.remove_contact(request.0, cx))
209            .detach();
210    }
211
212    fn respond_to_contact_request(
213        &mut self,
214        action: &RespondToContactRequest,
215        cx: &mut ViewContext<Self>,
216    ) {
217        self.user_store
218            .update(cx, |store, cx| {
219                store.respond_to_contact_request(action.user_id, action.accept, cx)
220            })
221            .detach();
222    }
223
224    fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
225        let did_clear = self.filter_editor.update(cx, |editor, cx| {
226            if editor.buffer().read(cx).len(cx) > 0 {
227                editor.set_text("", cx);
228                true
229            } else {
230                false
231            }
232        });
233        if !did_clear {
234            cx.emit(Event::Dismissed);
235        }
236    }
237
238    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
239        if let Some(ix) = self.selection {
240            if self.entries.len() > ix + 1 {
241                self.selection = Some(ix + 1);
242            }
243        } else if !self.entries.is_empty() {
244            self.selection = Some(0);
245        }
246        cx.notify();
247        self.list_state.reset(self.entries.len());
248    }
249
250    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
251        if let Some(ix) = self.selection {
252            if ix > 0 {
253                self.selection = Some(ix - 1);
254            } else {
255                self.selection = None;
256            }
257        }
258        cx.notify();
259        self.list_state.reset(self.entries.len());
260    }
261
262    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
263        if let Some(selection) = self.selection {
264            if let Some(entry) = self.entries.get(selection) {
265                match entry {
266                    ContactEntry::Header(section) => {
267                        let section = *section;
268                        self.toggle_expanded(&ToggleExpanded(section), cx);
269                    }
270                    _ => {}
271                }
272            }
273        }
274    }
275
276    fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext<Self>) {
277        let section = action.0;
278        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
279            self.collapsed_sections.remove(ix);
280        } else {
281            self.collapsed_sections.push(section);
282        }
283        self.update_entries(cx);
284    }
285
286    fn update_entries(&mut self, cx: &mut ViewContext<Self>) {
287        let user_store = self.user_store.read(cx);
288        let query = self.filter_editor.read(cx).text(cx);
289        let executor = cx.background().clone();
290
291        let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
292        self.entries.clear();
293
294        let mut request_entries = Vec::new();
295        let incoming = user_store.incoming_contact_requests();
296        if !incoming.is_empty() {
297            self.match_candidates.clear();
298            self.match_candidates
299                .extend(
300                    incoming
301                        .iter()
302                        .enumerate()
303                        .map(|(ix, user)| StringMatchCandidate {
304                            id: ix,
305                            string: user.github_login.clone(),
306                            char_bag: user.github_login.chars().collect(),
307                        }),
308                );
309            let matches = executor.block(match_strings(
310                &self.match_candidates,
311                &query,
312                true,
313                usize::MAX,
314                &Default::default(),
315                executor.clone(),
316            ));
317            request_entries.extend(
318                matches
319                    .iter()
320                    .map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
321            );
322        }
323
324        let outgoing = user_store.outgoing_contact_requests();
325        if !outgoing.is_empty() {
326            self.match_candidates.clear();
327            self.match_candidates
328                .extend(
329                    outgoing
330                        .iter()
331                        .enumerate()
332                        .map(|(ix, user)| StringMatchCandidate {
333                            id: ix,
334                            string: user.github_login.clone(),
335                            char_bag: user.github_login.chars().collect(),
336                        }),
337                );
338            let matches = executor.block(match_strings(
339                &self.match_candidates,
340                &query,
341                true,
342                usize::MAX,
343                &Default::default(),
344                executor.clone(),
345            ));
346            request_entries.extend(
347                matches
348                    .iter()
349                    .map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
350            );
351        }
352
353        if !request_entries.is_empty() {
354            self.entries.push(ContactEntry::Header(Section::Requests));
355            if !self.collapsed_sections.contains(&Section::Requests) {
356                self.entries.append(&mut request_entries);
357            }
358        }
359
360        let contacts = user_store.contacts();
361        if !contacts.is_empty() {
362            // Always put the current user first.
363            self.match_candidates.clear();
364            self.match_candidates
365                .extend(
366                    contacts
367                        .iter()
368                        .enumerate()
369                        .map(|(ix, contact)| StringMatchCandidate {
370                            id: ix,
371                            string: contact.user.github_login.clone(),
372                            char_bag: contact.user.github_login.chars().collect(),
373                        }),
374                );
375
376            let matches = executor.block(match_strings(
377                &self.match_candidates,
378                &query,
379                true,
380                usize::MAX,
381                &Default::default(),
382                executor.clone(),
383            ));
384
385            let (online_contacts, offline_contacts) = matches
386                .iter()
387                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
388
389            for (matches, section) in [
390                (online_contacts, Section::Online),
391                (offline_contacts, Section::Offline),
392            ] {
393                if !matches.is_empty() {
394                    self.entries.push(ContactEntry::Header(section));
395                    if !self.collapsed_sections.contains(&section) {
396                        for mat in matches {
397                            let contact = &contacts[mat.candidate_id];
398                            self.entries.push(ContactEntry::Contact(contact.clone()));
399                        }
400                    }
401                }
402            }
403        }
404
405        if let Some(prev_selected_entry) = prev_selected_entry {
406            self.selection.take();
407            for (ix, entry) in self.entries.iter().enumerate() {
408                if *entry == prev_selected_entry {
409                    self.selection = Some(ix);
410                    break;
411                }
412            }
413        }
414
415        self.list_state.reset(self.entries.len());
416        cx.notify();
417    }
418
419    fn render_active_call(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
420        let room = ActiveCall::global(cx).read(cx).room()?;
421        let theme = &cx.global::<Settings>().theme.contacts_popover;
422
423        Some(
424            Flex::column()
425                .with_children(room.read(cx).pending_users().iter().map(|user| {
426                    Flex::row()
427                        .with_children(user.avatar.clone().map(|avatar| {
428                            Image::new(avatar)
429                                .with_style(theme.contact_avatar)
430                                .aligned()
431                                .left()
432                                .boxed()
433                        }))
434                        .with_child(
435                            Label::new(
436                                user.github_login.clone(),
437                                theme.contact_username.text.clone(),
438                            )
439                            .contained()
440                            .with_style(theme.contact_username.container)
441                            .aligned()
442                            .left()
443                            .flex(1., true)
444                            .boxed(),
445                        )
446                        .constrained()
447                        .with_height(theme.row_height)
448                        .contained()
449                        .with_style(theme.contact_row.default)
450                        .boxed()
451                }))
452                .boxed(),
453        )
454    }
455
456    fn render_header(
457        section: Section,
458        theme: &theme::ContactsPopover,
459        is_selected: bool,
460        is_collapsed: bool,
461        cx: &mut RenderContext<Self>,
462    ) -> ElementBox {
463        enum Header {}
464
465        let header_style = theme.header_row.style_for(Default::default(), is_selected);
466        let text = match section {
467            Section::Requests => "Requests",
468            Section::Online => "Online",
469            Section::Offline => "Offline",
470        };
471        let icon_size = theme.section_icon_size;
472        MouseEventHandler::<Header>::new(section as usize, cx, |_, _| {
473            Flex::row()
474                .with_child(
475                    Svg::new(if is_collapsed {
476                        "icons/chevron_right_8.svg"
477                    } else {
478                        "icons/chevron_down_8.svg"
479                    })
480                    .with_color(header_style.text.color)
481                    .constrained()
482                    .with_max_width(icon_size)
483                    .with_max_height(icon_size)
484                    .aligned()
485                    .constrained()
486                    .with_width(icon_size)
487                    .boxed(),
488                )
489                .with_child(
490                    Label::new(text.to_string(), header_style.text.clone())
491                        .aligned()
492                        .left()
493                        .contained()
494                        .with_margin_left(theme.contact_username.container.margin.left)
495                        .flex(1., true)
496                        .boxed(),
497                )
498                .constrained()
499                .with_height(theme.row_height)
500                .contained()
501                .with_style(header_style.container)
502                .boxed()
503        })
504        .with_cursor_style(CursorStyle::PointingHand)
505        .on_click(MouseButton::Left, move |_, cx| {
506            cx.dispatch_action(ToggleExpanded(section))
507        })
508        .boxed()
509    }
510
511    fn render_contact(
512        contact: &Contact,
513        project: &ModelHandle<Project>,
514        theme: &theme::ContactsPopover,
515        is_selected: bool,
516        cx: &mut RenderContext<Self>,
517    ) -> ElementBox {
518        let online = contact.online;
519        let user_id = contact.user.id;
520        let initial_project = project.clone();
521        let mut element =
522            MouseEventHandler::<Contact>::new(contact.user.id as usize, cx, |_, _| {
523                Flex::row()
524                    .with_children(contact.user.avatar.clone().map(|avatar| {
525                        let status_badge = if contact.online {
526                            Some(
527                                Empty::new()
528                                    .collapsed()
529                                    .contained()
530                                    .with_style(if contact.busy {
531                                        theme.contact_status_busy
532                                    } else {
533                                        theme.contact_status_free
534                                    })
535                                    .aligned()
536                                    .boxed(),
537                            )
538                        } else {
539                            None
540                        };
541                        Stack::new()
542                            .with_child(
543                                Image::new(avatar)
544                                    .with_style(theme.contact_avatar)
545                                    .aligned()
546                                    .left()
547                                    .boxed(),
548                            )
549                            .with_children(status_badge)
550                            .boxed()
551                    }))
552                    .with_child(
553                        Label::new(
554                            contact.user.github_login.clone(),
555                            theme.contact_username.text.clone(),
556                        )
557                        .contained()
558                        .with_style(theme.contact_username.container)
559                        .aligned()
560                        .left()
561                        .flex(1., true)
562                        .boxed(),
563                    )
564                    .constrained()
565                    .with_height(theme.row_height)
566                    .contained()
567                    .with_style(*theme.contact_row.style_for(Default::default(), is_selected))
568                    .boxed()
569            })
570            .on_click(MouseButton::Left, move |_, cx| {
571                if online {
572                    cx.dispatch_action(Call {
573                        recipient_user_id: user_id,
574                        initial_project: Some(initial_project.clone()),
575                    });
576                }
577            });
578
579        if online {
580            element = element.with_cursor_style(CursorStyle::PointingHand);
581        }
582
583        element.boxed()
584    }
585
586    fn render_contact_request(
587        user: Arc<User>,
588        user_store: ModelHandle<UserStore>,
589        theme: &theme::ContactsPopover,
590        is_incoming: bool,
591        is_selected: bool,
592        cx: &mut RenderContext<Self>,
593    ) -> ElementBox {
594        enum Decline {}
595        enum Accept {}
596        enum Cancel {}
597
598        let mut row = Flex::row()
599            .with_children(user.avatar.clone().map(|avatar| {
600                Image::new(avatar)
601                    .with_style(theme.contact_avatar)
602                    .aligned()
603                    .left()
604                    .boxed()
605            }))
606            .with_child(
607                Label::new(
608                    user.github_login.clone(),
609                    theme.contact_username.text.clone(),
610                )
611                .contained()
612                .with_style(theme.contact_username.container)
613                .aligned()
614                .left()
615                .flex(1., true)
616                .boxed(),
617            );
618
619        let user_id = user.id;
620        let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
621        let button_spacing = theme.contact_button_spacing;
622
623        if is_incoming {
624            row.add_children([
625                MouseEventHandler::<Decline>::new(user.id as usize, cx, |mouse_state, _| {
626                    let button_style = if is_contact_request_pending {
627                        &theme.disabled_button
628                    } else {
629                        theme.contact_button.style_for(mouse_state, false)
630                    };
631                    render_icon_button(button_style, "icons/x_mark_8.svg")
632                        .aligned()
633                        .boxed()
634                })
635                .with_cursor_style(CursorStyle::PointingHand)
636                .on_click(MouseButton::Left, move |_, cx| {
637                    cx.dispatch_action(RespondToContactRequest {
638                        user_id,
639                        accept: false,
640                    })
641                })
642                .contained()
643                .with_margin_right(button_spacing)
644                .boxed(),
645                MouseEventHandler::<Accept>::new(user.id as usize, cx, |mouse_state, _| {
646                    let button_style = if is_contact_request_pending {
647                        &theme.disabled_button
648                    } else {
649                        theme.contact_button.style_for(mouse_state, false)
650                    };
651                    render_icon_button(button_style, "icons/check_8.svg")
652                        .aligned()
653                        .flex_float()
654                        .boxed()
655                })
656                .with_cursor_style(CursorStyle::PointingHand)
657                .on_click(MouseButton::Left, move |_, cx| {
658                    cx.dispatch_action(RespondToContactRequest {
659                        user_id,
660                        accept: true,
661                    })
662                })
663                .boxed(),
664            ]);
665        } else {
666            row.add_child(
667                MouseEventHandler::<Cancel>::new(user.id as usize, cx, |mouse_state, _| {
668                    let button_style = if is_contact_request_pending {
669                        &theme.disabled_button
670                    } else {
671                        theme.contact_button.style_for(mouse_state, false)
672                    };
673                    render_icon_button(button_style, "icons/x_mark_8.svg")
674                        .aligned()
675                        .flex_float()
676                        .boxed()
677                })
678                .with_padding(Padding::uniform(2.))
679                .with_cursor_style(CursorStyle::PointingHand)
680                .on_click(MouseButton::Left, move |_, cx| {
681                    cx.dispatch_action(RemoveContact(user_id))
682                })
683                .flex_float()
684                .boxed(),
685            );
686        }
687
688        row.constrained()
689            .with_height(theme.row_height)
690            .contained()
691            .with_style(*theme.contact_row.style_for(Default::default(), is_selected))
692            .boxed()
693    }
694
695    fn call(&mut self, action: &Call, cx: &mut ViewContext<Self>) {
696        ActiveCall::global(cx)
697            .update(cx, |active_call, cx| {
698                active_call.invite(action.recipient_user_id, action.initial_project.clone(), cx)
699            })
700            .detach_and_log_err(cx);
701    }
702}
703
704impl Entity for ContactsPopover {
705    type Event = Event;
706}
707
708impl View for ContactsPopover {
709    fn ui_name() -> &'static str {
710        "ContactsPopover"
711    }
712
713    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
714        let mut cx = Self::default_keymap_context();
715        cx.set.insert("menu".into());
716        cx
717    }
718
719    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
720        enum AddContact {}
721        let theme = cx.global::<Settings>().theme.clone();
722
723        Flex::column()
724            .with_child(
725                Flex::row()
726                    .with_child(
727                        ChildView::new(self.filter_editor.clone())
728                            .contained()
729                            .with_style(theme.contacts_popover.user_query_editor.container)
730                            .flex(1., true)
731                            .boxed(),
732                    )
733                    .with_child(
734                        MouseEventHandler::<AddContact>::new(0, cx, |_, _| {
735                            Svg::new("icons/user_plus_16.svg")
736                                .with_color(theme.contacts_popover.add_contact_button.color)
737                                .constrained()
738                                .with_height(16.)
739                                .contained()
740                                .with_style(theme.contacts_popover.add_contact_button.container)
741                                .aligned()
742                                .boxed()
743                        })
744                        .with_cursor_style(CursorStyle::PointingHand)
745                        .on_click(MouseButton::Left, |_, cx| {
746                            cx.dispatch_action(contact_finder::Toggle)
747                        })
748                        .boxed(),
749                    )
750                    .constrained()
751                    .with_height(theme.contacts_popover.user_query_editor_height)
752                    .boxed(),
753            )
754            .with_children(self.render_active_call(cx))
755            .with_child(List::new(self.list_state.clone()).flex(1., false).boxed())
756            .with_children(
757                self.user_store
758                    .read(cx)
759                    .invite_info()
760                    .cloned()
761                    .and_then(|info| {
762                        enum InviteLink {}
763
764                        if info.count > 0 {
765                            Some(
766                                MouseEventHandler::<InviteLink>::new(0, cx, |state, cx| {
767                                    let style = theme
768                                        .contacts_popover
769                                        .invite_row
770                                        .style_for(state, false)
771                                        .clone();
772
773                                    let copied = cx.read_from_clipboard().map_or(false, |item| {
774                                        item.text().as_str() == info.url.as_ref()
775                                    });
776
777                                    Label::new(
778                                        format!(
779                                            "{} invite link ({} left)",
780                                            if copied { "Copied" } else { "Copy" },
781                                            info.count
782                                        ),
783                                        style.label.clone(),
784                                    )
785                                    .aligned()
786                                    .left()
787                                    .constrained()
788                                    .with_height(theme.contacts_popover.row_height)
789                                    .contained()
790                                    .with_style(style.container)
791                                    .boxed()
792                                })
793                                .with_cursor_style(CursorStyle::PointingHand)
794                                .on_click(MouseButton::Left, move |_, cx| {
795                                    cx.write_to_clipboard(ClipboardItem::new(info.url.to_string()));
796                                    cx.notify();
797                                })
798                                .boxed(),
799                            )
800                        } else {
801                            None
802                        }
803                    }),
804            )
805            .contained()
806            .with_style(theme.contacts_popover.container)
807            .constrained()
808            .with_width(theme.contacts_popover.width)
809            .with_height(theme.contacts_popover.height)
810            .boxed()
811    }
812
813    fn on_focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
814        if !self.filter_editor.is_focused(cx) {
815            cx.focus(&self.filter_editor);
816        }
817    }
818
819    fn on_focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
820        if !self.filter_editor.is_focused(cx) {
821            cx.emit(Event::Dismissed);
822        }
823    }
824}
825
826fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element {
827    Svg::new(svg_path)
828        .with_color(style.color)
829        .constrained()
830        .with_width(style.icon_width)
831        .aligned()
832        .contained()
833        .with_style(style.container)
834        .constrained()
835        .with_width(style.button_width)
836        .with_height(style.button_width)
837}