contact_finder.rs

  1use client::{ContactRequestStatus, User, UserStore};
  2use gpui::{
  3    elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
  4};
  5use picker::{Picker, PickerDelegate, PickerEvent};
  6use std::sync::Arc;
  7use util::TryFutureExt;
  8use workspace::Modal;
  9
 10pub fn init(cx: &mut AppContext) {
 11    Picker::<ContactFinderDelegate>::init(cx);
 12    cx.add_action(ContactFinder::dismiss)
 13}
 14
 15pub struct ContactFinder {
 16    picker: ViewHandle<Picker<ContactFinderDelegate>>,
 17    has_focus: bool,
 18}
 19
 20impl ContactFinder {
 21    pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
 22        let picker = cx.add_view(|cx| {
 23            Picker::new(
 24                ContactFinderDelegate {
 25                    user_store,
 26                    potential_contacts: Arc::from([]),
 27                    selected_index: 0,
 28                },
 29                cx,
 30            )
 31            .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
 32        });
 33
 34        cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
 35
 36        Self {
 37            picker,
 38            has_focus: false,
 39        }
 40    }
 41
 42    pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
 43        self.picker.update(cx, |picker, cx| {
 44            picker.set_query(query, cx);
 45        });
 46    }
 47
 48    fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
 49        cx.emit(PickerEvent::Dismiss);
 50    }
 51}
 52
 53impl Entity for ContactFinder {
 54    type Event = PickerEvent;
 55}
 56
 57impl View for ContactFinder {
 58    fn ui_name() -> &'static str {
 59        "ContactFinder"
 60    }
 61
 62    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
 63        let full_theme = &theme::current(cx);
 64        let theme = &full_theme.collab_panel.tabbed_modal;
 65
 66        fn render_mode_button(
 67            text: &'static str,
 68            theme: &theme::TabbedModal,
 69            _cx: &mut ViewContext<ContactFinder>,
 70        ) -> AnyElement<ContactFinder> {
 71            let contained_text = &theme.tab_button.active_state().default;
 72            Label::new(text, contained_text.text.clone())
 73                .contained()
 74                .with_style(contained_text.container.clone())
 75                .into_any()
 76        }
 77
 78        Flex::column()
 79            .with_child(
 80                Flex::column()
 81                    .with_child(
 82                        Label::new("Contacts", theme.title.text.clone())
 83                            .contained()
 84                            .with_style(theme.title.container.clone()),
 85                    )
 86                    .with_child(Flex::row().with_children([render_mode_button(
 87                        "Invite new contacts",
 88                        &theme,
 89                        cx,
 90                    )]))
 91                    .expanded()
 92                    .contained()
 93                    .with_style(theme.header),
 94            )
 95            .with_child(
 96                ChildView::new(&self.picker, cx)
 97                    .contained()
 98                    .with_style(theme.body),
 99            )
100            .constrained()
101            .with_max_height(theme.max_height)
102            .with_max_width(theme.max_width)
103            .contained()
104            .with_style(theme.modal)
105            .into_any()
106    }
107
108    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
109        self.has_focus = true;
110        if cx.is_self_focused() {
111            cx.focus(&self.picker)
112        }
113    }
114
115    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
116        self.has_focus = false;
117    }
118}
119
120impl Modal for ContactFinder {
121    fn has_focus(&self) -> bool {
122        self.has_focus
123    }
124
125    fn dismiss_on_event(event: &Self::Event) -> bool {
126        match event {
127            PickerEvent::Dismiss => true,
128        }
129    }
130}
131
132pub struct ContactFinderDelegate {
133    potential_contacts: Arc<[Arc<User>]>,
134    user_store: ModelHandle<UserStore>,
135    selected_index: usize,
136}
137
138impl PickerDelegate for ContactFinderDelegate {
139    fn placeholder_text(&self) -> Arc<str> {
140        "Search collaborator by username...".into()
141    }
142
143    fn match_count(&self) -> usize {
144        self.potential_contacts.len()
145    }
146
147    fn selected_index(&self) -> usize {
148        self.selected_index
149    }
150
151    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
152        self.selected_index = ix;
153    }
154
155    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
156        let search_users = self
157            .user_store
158            .update(cx, |store, cx| store.fuzzy_search_users(query, cx));
159
160        cx.spawn(|picker, mut cx| async move {
161            async {
162                let potential_contacts = search_users.await?;
163                picker.update(&mut cx, |picker, cx| {
164                    picker.delegate_mut().potential_contacts = potential_contacts.into();
165                    cx.notify();
166                })?;
167                anyhow::Ok(())
168            }
169            .log_err()
170            .await;
171        })
172    }
173
174    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
175        if let Some(user) = self.potential_contacts.get(self.selected_index) {
176            let user_store = self.user_store.read(cx);
177            match user_store.contact_request_status(user) {
178                ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
179                    self.user_store
180                        .update(cx, |store, cx| store.request_contact(user.id, cx))
181                        .detach();
182                }
183                ContactRequestStatus::RequestSent => {
184                    self.user_store
185                        .update(cx, |store, cx| store.remove_contact(user.id, cx))
186                        .detach();
187                }
188                _ => {}
189            }
190        }
191    }
192
193    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
194        cx.emit(PickerEvent::Dismiss);
195    }
196
197    fn render_match(
198        &self,
199        ix: usize,
200        mouse_state: &mut MouseState,
201        selected: bool,
202        cx: &gpui::AppContext,
203    ) -> AnyElement<Picker<Self>> {
204        let full_theme = &theme::current(cx);
205        let theme = &full_theme.collab_panel.contact_finder;
206        let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
207        let user = &self.potential_contacts[ix];
208        let request_status = self.user_store.read(cx).contact_request_status(user);
209
210        let icon_path = match request_status {
211            ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
212                Some("icons/check.svg")
213            }
214            ContactRequestStatus::RequestSent => Some("icons/x.svg"),
215            ContactRequestStatus::RequestAccepted => None,
216        };
217        let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
218            &theme.disabled_contact_button
219        } else {
220            &theme.contact_button
221        };
222        let style = tabbed_modal
223            .picker
224            .item
225            .in_state(selected)
226            .style_for(mouse_state);
227        Flex::row()
228            .with_children(user.avatar.clone().map(|avatar| {
229                Image::from_data(avatar)
230                    .with_style(theme.contact_avatar)
231                    .aligned()
232                    .left()
233            }))
234            .with_child(
235                Label::new(user.github_login.clone(), style.label.clone())
236                    .contained()
237                    .with_style(theme.contact_username)
238                    .aligned()
239                    .left(),
240            )
241            .with_children(icon_path.map(|icon_path| {
242                Svg::new(icon_path)
243                    .with_color(button_style.color)
244                    .constrained()
245                    .with_width(button_style.icon_width)
246                    .aligned()
247                    .contained()
248                    .with_style(button_style.container)
249                    .constrained()
250                    .with_width(button_style.button_width)
251                    .with_height(button_style.button_width)
252                    .aligned()
253                    .flex_float()
254            }))
255            .contained()
256            .with_style(style.container)
257            .constrained()
258            .with_height(tabbed_modal.row_height)
259            .into_any()
260    }
261}