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