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}