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}