contact_finder.rs

  1use client::{ContactRequestStatus, User, UserStore};
  2use gpui::{
  3    AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Model,
  4    ParentElement as _, 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};
 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.build_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>) -> Self::Element {
 39        v_stack()
 40            .elevation_3(cx)
 41            .child(
 42                v_stack()
 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_stack().child(Label::new("Invite new contacts"))),
 50            )
 51            .child(self.picker.clone())
 52            .w(rems(34.))
 53    }
 54
 55    type Element = Div;
 56}
 57
 58pub struct ContactFinderDelegate {
 59    parent: WeakView<ContactFinder>,
 60    potential_contacts: Arc<[Arc<User>]>,
 61    user_store: Model<UserStore>,
 62    selected_index: usize,
 63}
 64
 65impl EventEmitter<DismissEvent> for ContactFinder {}
 66impl ModalView for ContactFinder {}
 67
 68impl FocusableView for ContactFinder {
 69    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
 70        self.picker.focus_handle(cx)
 71    }
 72}
 73
 74impl PickerDelegate for ContactFinderDelegate {
 75    type ListItem = ListItem;
 76
 77    fn match_count(&self) -> usize {
 78        self.potential_contacts.len()
 79    }
 80
 81    fn selected_index(&self) -> usize {
 82        self.selected_index
 83    }
 84
 85    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
 86        self.selected_index = ix;
 87    }
 88
 89    fn placeholder_text(&self) -> Arc<str> {
 90        "Search collaborator by username...".into()
 91    }
 92
 93    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
 94        let search_users = self
 95            .user_store
 96            .update(cx, |store, cx| store.fuzzy_search_users(query, cx));
 97
 98        cx.spawn(|picker, mut cx| async move {
 99            async {
100                let potential_contacts = search_users.await?;
101                picker.update(&mut cx, |picker, cx| {
102                    picker.delegate.potential_contacts = potential_contacts.into();
103                    cx.notify();
104                })?;
105                anyhow::Ok(())
106            }
107            .log_err()
108            .await;
109        })
110    }
111
112    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
113        if let Some(user) = self.potential_contacts.get(self.selected_index) {
114            let user_store = self.user_store.read(cx);
115            match user_store.contact_request_status(user) {
116                ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
117                    self.user_store
118                        .update(cx, |store, cx| store.request_contact(user.id, cx))
119                        .detach();
120                }
121                ContactRequestStatus::RequestSent => {
122                    self.user_store
123                        .update(cx, |store, cx| store.remove_contact(user.id, cx))
124                        .detach();
125                }
126                _ => {}
127            }
128        }
129    }
130
131    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
132        self.parent
133            .update(cx, |_, cx| cx.emit(DismissEvent))
134            .log_err();
135    }
136
137    fn render_match(
138        &self,
139        ix: usize,
140        selected: bool,
141        cx: &mut ViewContext<Picker<Self>>,
142    ) -> Option<Self::ListItem> {
143        let user = &self.potential_contacts[ix];
144        let request_status = self.user_store.read(cx).contact_request_status(user);
145
146        let icon_path = match request_status {
147            ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
148                Some("icons/check.svg")
149            }
150            ContactRequestStatus::RequestSent => Some("icons/x.svg"),
151            ContactRequestStatus::RequestAccepted => None,
152        };
153        Some(
154            ListItem::new(ix)
155                .inset(true)
156                .selected(selected)
157                .start_slot(Avatar::new(user.avatar_uri.clone()))
158                .child(Label::new(user.github_login.clone()))
159                .end_slot::<IconElement>(
160                    icon_path.map(|icon_path| IconElement::from_path(icon_path)),
161                ),
162        )
163    }
164}