contact_finder.rs

  1use client::{ContactRequestStatus, User, UserStore};
  2use gpui::{
  3    AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, ParentElement as _,
  4    Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
  5};
  6use picker::{Picker, PickerDelegate};
  7use std::sync::Arc;
  8use theme::ActiveTheme as _;
  9use ui::{prelude::*, Avatar, ListItem, ListItemSpacing};
 10use util::{ResultExt as _, TryFutureExt};
 11use workspace::ModalView;
 12
 13pub struct ContactFinder {
 14    picker: View<Picker<ContactFinderDelegate>>,
 15}
 16
 17impl ContactFinder {
 18    pub fn new(user_store: Model<UserStore>, cx: &mut ViewContext<Self>) -> Self {
 19        let delegate = ContactFinderDelegate {
 20            parent: cx.view().downgrade(),
 21            user_store,
 22            potential_contacts: Arc::from([]),
 23            selected_index: 0,
 24        };
 25        let picker = cx.new_view(|cx| Picker::new(delegate, cx).modal(false));
 26
 27        Self { picker }
 28    }
 29
 30    pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
 31        self.picker.update(cx, |picker, cx| {
 32            picker.set_query(query, cx);
 33        });
 34    }
 35}
 36
 37impl Render for ContactFinder {
 38    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 39        v_flex()
 40            .elevation_3(cx)
 41            .child(
 42                v_flex()
 43                    .px_2()
 44                    .py_1()
 45                    .bg(cx.theme().colors().element_background)
 46                    // HACK: Prevent the background color from overflowing the parent container.
 47                    .rounded_t(px(8.))
 48                    .child(Label::new("Contacts"))
 49                    .child(h_flex().child(Label::new("Invite new contacts"))),
 50            )
 51            .child(self.picker.clone())
 52            .w(rems(34.))
 53    }
 54}
 55
 56pub struct ContactFinderDelegate {
 57    parent: WeakView<ContactFinder>,
 58    potential_contacts: Arc<[Arc<User>]>,
 59    user_store: Model<UserStore>,
 60    selected_index: usize,
 61}
 62
 63impl EventEmitter<DismissEvent> for ContactFinder {}
 64impl ModalView for ContactFinder {}
 65
 66impl FocusableView for ContactFinder {
 67    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
 68        self.picker.focus_handle(cx)
 69    }
 70}
 71
 72impl PickerDelegate for ContactFinderDelegate {
 73    type ListItem = ListItem;
 74
 75    fn match_count(&self) -> usize {
 76        self.potential_contacts.len()
 77    }
 78
 79    fn selected_index(&self) -> usize {
 80        self.selected_index
 81    }
 82
 83    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
 84        self.selected_index = ix;
 85    }
 86
 87    fn placeholder_text(&self) -> Arc<str> {
 88        "Search collaborator by username...".into()
 89    }
 90
 91    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
 92        let search_users = self
 93            .user_store
 94            .update(cx, |store, cx| store.fuzzy_search_users(query, cx));
 95
 96        cx.spawn(|picker, mut cx| async move {
 97            async {
 98                let potential_contacts = search_users.await?;
 99                picker.update(&mut cx, |picker, cx| {
100                    picker.delegate.potential_contacts = potential_contacts.into();
101                    cx.notify();
102                })?;
103                anyhow::Ok(())
104            }
105            .log_err()
106            .await;
107        })
108    }
109
110    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
111        if let Some(user) = self.potential_contacts.get(self.selected_index) {
112            let user_store = self.user_store.read(cx);
113            match user_store.contact_request_status(user) {
114                ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
115                    self.user_store
116                        .update(cx, |store, cx| store.request_contact(user.id, cx))
117                        .detach();
118                }
119                ContactRequestStatus::RequestSent => {
120                    self.user_store
121                        .update(cx, |store, cx| store.remove_contact(user.id, cx))
122                        .detach();
123                }
124                _ => {}
125            }
126        }
127    }
128
129    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
130        self.parent
131            .update(cx, |_, cx| cx.emit(DismissEvent))
132            .log_err();
133    }
134
135    fn render_match(
136        &self,
137        ix: usize,
138        selected: bool,
139        cx: &mut ViewContext<Picker<Self>>,
140    ) -> Option<Self::ListItem> {
141        let user = &self.potential_contacts[ix];
142        let request_status = self.user_store.read(cx).contact_request_status(user);
143
144        let icon_path = match request_status {
145            ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
146                Some("icons/check.svg")
147            }
148            ContactRequestStatus::RequestSent => Some("icons/x.svg"),
149            ContactRequestStatus::RequestAccepted => None,
150        };
151        Some(
152            ListItem::new(ix)
153                .inset(true)
154                .spacing(ListItemSpacing::Sparse)
155                .selected(selected)
156                .start_slot(Avatar::new(user.avatar_uri.clone()))
157                .child(Label::new(user.github_login.clone()))
158                .end_slot::<Icon>(icon_path.map(|icon_path| Icon::from_path(icon_path))),
159        )
160    }
161}