contact_finder.rs

  1use client::{ContactRequestStatus, User, UserStore};
  2use gpui::{
  3    App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ParentElement as _,
  4    Render, Styled, Task, WeakEntity, Window,
  5};
  6use picker::{Picker, PickerDelegate};
  7use std::sync::Arc;
  8use ui::{Avatar, ListItem, ListItemSpacing, prelude::*};
  9use util::{ResultExt as _, TryFutureExt};
 10use workspace::ModalView;
 11
 12pub struct ContactFinder {
 13    picker: Entity<Picker<ContactFinderDelegate>>,
 14}
 15
 16impl ContactFinder {
 17    pub fn new(user_store: Entity<UserStore>, window: &mut Window, cx: &mut Context<Self>) -> Self {
 18        let delegate = ContactFinderDelegate {
 19            parent: cx.entity().downgrade(),
 20            user_store,
 21            potential_contacts: Arc::from([]),
 22            selected_index: 0,
 23        };
 24        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
 25
 26        Self { picker }
 27    }
 28
 29    pub fn set_query(&mut self, query: String, window: &mut Window, cx: &mut Context<Self>) {
 30        self.picker.update(cx, |picker, cx| {
 31            picker.set_query(query, window, cx);
 32        });
 33    }
 34}
 35
 36impl Render for ContactFinder {
 37    fn render(&mut self, _: &mut Window, cx: &mut Context<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: WeakEntity<ContactFinder>,
 57    potential_contacts: Arc<[Arc<User>]>,
 58    user_store: Entity<UserStore>,
 59    selected_index: usize,
 60}
 61
 62impl EventEmitter<DismissEvent> for ContactFinder {}
 63impl ModalView for ContactFinder {}
 64
 65impl Focusable for ContactFinder {
 66    fn focus_handle(&self, cx: &App) -> 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(
 83        &mut self,
 84        ix: usize,
 85        _window: &mut Window,
 86        _: &mut Context<Picker<Self>>,
 87    ) {
 88        self.selected_index = ix;
 89    }
 90
 91    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 92        "Search collaborator by username...".into()
 93    }
 94
 95    fn update_matches(
 96        &mut self,
 97        query: String,
 98        window: &mut Window,
 99        cx: &mut Context<Picker<Self>>,
100    ) -> Task<()> {
101        let search_users = self
102            .user_store
103            .update(cx, |store, cx| store.fuzzy_search_users(query, cx));
104
105        cx.spawn_in(window, async move |picker, cx| {
106            async {
107                let potential_contacts = search_users.await?;
108                picker.update(cx, |picker, cx| {
109                    picker.delegate.potential_contacts = potential_contacts.into();
110                    cx.notify();
111                })?;
112                anyhow::Ok(())
113            }
114            .log_err()
115            .await;
116        })
117    }
118
119    fn confirm(&mut self, _: bool, _: &mut Window, cx: &mut Context<Picker<Self>>) {
120        if let Some(user) = self.potential_contacts.get(self.selected_index) {
121            let user_store = self.user_store.read(cx);
122            match user_store.contact_request_status(user) {
123                ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
124                    self.user_store
125                        .update(cx, |store, cx| store.request_contact(user.id, cx))
126                        .detach();
127                }
128                ContactRequestStatus::RequestSent => {
129                    self.user_store
130                        .update(cx, |store, cx| store.remove_contact(user.id, cx))
131                        .detach();
132                }
133                _ => {}
134            }
135        }
136    }
137
138    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
139        self.parent
140            .update(cx, |_, cx| cx.emit(DismissEvent))
141            .log_err();
142    }
143
144    fn render_match(
145        &self,
146        ix: usize,
147        selected: bool,
148        _: &mut Window,
149        cx: &mut Context<Picker<Self>>,
150    ) -> Option<Self::ListItem> {
151        let user = &self.potential_contacts[ix];
152        let request_status = self.user_store.read(cx).contact_request_status(user);
153
154        let icon_path = match request_status {
155            ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
156                Some("icons/check.svg")
157            }
158            ContactRequestStatus::RequestSent => Some("icons/x.svg"),
159            ContactRequestStatus::RequestAccepted => None,
160        };
161        Some(
162            ListItem::new(ix)
163                .inset(true)
164                .spacing(ListItemSpacing::Sparse)
165                .toggle_state(selected)
166                .start_slot(Avatar::new(user.avatar_uri.clone()))
167                .child(Label::new(user.github_login.clone()))
168                .end_slot::<Icon>(icon_path.map(Icon::from_path)),
169        )
170    }
171}