channel_modal.rs

  1use channel::{ChannelId, ChannelMembership, ChannelStore};
  2use client::{
  3    proto::{self, ChannelRole, ChannelVisibility},
  4    User, UserId, UserStore,
  5};
  6use fuzzy::{match_strings, StringMatchCandidate};
  7use gpui::{
  8    actions, div, AppContext, ClipboardItem, DismissEvent, Div, Entity, EventEmitter,
  9    FocusableView, Model, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext,
 10    WeakView,
 11};
 12use picker::{Picker, PickerDelegate};
 13use std::sync::Arc;
 14use ui::v_stack;
 15use util::TryFutureExt;
 16
 17actions!(
 18    SelectNextControl,
 19    ToggleMode,
 20    ToggleMemberAdmin,
 21    RemoveMember
 22);
 23
 24// pub fn init(cx: &mut AppContext) {
 25//     Picker::<ChannelModalDelegate>::init(cx);
 26//     cx.add_action(ChannelModal::toggle_mode);
 27//     cx.add_action(ChannelModal::toggle_member_admin);
 28//     cx.add_action(ChannelModal::remove_member);
 29//     cx.add_action(ChannelModal::dismiss);
 30// }
 31
 32pub struct ChannelModal {
 33    picker: View<Picker<ChannelModalDelegate>>,
 34    channel_store: Model<ChannelStore>,
 35    channel_id: ChannelId,
 36    has_focus: bool,
 37}
 38
 39impl ChannelModal {
 40    pub fn new(
 41        user_store: Model<UserStore>,
 42        channel_store: Model<ChannelStore>,
 43        channel_id: ChannelId,
 44        mode: Mode,
 45        members: Vec<ChannelMembership>,
 46        cx: &mut ViewContext<Self>,
 47    ) -> Self {
 48        cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
 49        let channel_modal = cx.view().downgrade();
 50        let picker = cx.build_view(|cx| {
 51            Picker::new(
 52                ChannelModalDelegate {
 53                    channel_modal,
 54                    matching_users: Vec::new(),
 55                    matching_member_indices: Vec::new(),
 56                    selected_index: 0,
 57                    user_store: user_store.clone(),
 58                    channel_store: channel_store.clone(),
 59                    channel_id,
 60                    match_candidates: Vec::new(),
 61                    members,
 62                    mode,
 63                    // context_menu: cx.add_view(|cx| {
 64                    //     let mut menu = ContextMenu::new(cx.view_id(), cx);
 65                    //     menu.set_position_mode(OverlayPositionMode::Local);
 66                    //     menu
 67                    // }),
 68                },
 69                cx,
 70            )
 71        });
 72
 73        let has_focus = picker.focus_handle(cx).contains_focused(cx);
 74
 75        Self {
 76            picker,
 77            channel_store,
 78            channel_id,
 79            has_focus,
 80        }
 81    }
 82
 83    fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
 84        let mode = match self.picker.read(cx).delegate.mode {
 85            Mode::ManageMembers => Mode::InviteMembers,
 86            Mode::InviteMembers => Mode::ManageMembers,
 87        };
 88        self.set_mode(mode, cx);
 89    }
 90
 91    fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
 92        let channel_store = self.channel_store.clone();
 93        let channel_id = self.channel_id;
 94        cx.spawn(|this, mut cx| async move {
 95            if mode == Mode::ManageMembers {
 96                let mut members = channel_store
 97                    .update(&mut cx, |channel_store, cx| {
 98                        channel_store.get_channel_member_details(channel_id, cx)
 99                    })?
100                    .await?;
101
102                members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
103
104                this.update(&mut cx, |this, cx| {
105                    this.picker
106                        .update(cx, |picker, _| picker.delegate.members = members);
107                })?;
108            }
109
110            this.update(&mut cx, |this, cx| {
111                this.picker.update(cx, |picker, cx| {
112                    let delegate = &mut picker.delegate;
113                    delegate.mode = mode;
114                    delegate.selected_index = 0;
115                    picker.set_query("", cx);
116                    picker.update_matches(picker.query(cx), cx);
117                    cx.notify()
118                });
119                cx.notify()
120            })
121        })
122        .detach();
123    }
124
125    fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
126        self.picker.update(cx, |picker, cx| {
127            picker.delegate.toggle_selected_member_admin(cx);
128        })
129    }
130
131    fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
132        self.picker.update(cx, |picker, cx| {
133            picker.delegate.remove_selected_member(cx);
134        });
135    }
136
137    fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
138        cx.emit(DismissEvent);
139    }
140}
141
142impl EventEmitter<DismissEvent> for ChannelModal {}
143
144impl FocusableView for ChannelModal {
145    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
146        self.picker.focus_handle(cx)
147    }
148}
149
150impl Render for ChannelModal {
151    type Element = Div;
152
153    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
154        v_stack().min_w_96().child(self.picker.clone())
155        // let theme = &theme::current(cx).collab_panel.tabbed_modal;
156
157        // let mode = self.picker.read(cx).delegate().mode;
158        // let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
159        //     return Empty::new().into_any();
160        // };
161
162        // enum InviteMembers {}
163        // enum ManageMembers {}
164
165        // fn render_mode_button<T: 'static>(
166        //     mode: Mode,
167        //     text: &'static str,
168        //     current_mode: Mode,
169        //     theme: &theme::TabbedModal,
170        //     cx: &mut ViewContext<ChannelModal>,
171        // ) -> AnyElement<ChannelModal> {
172        //     let active = mode == current_mode;
173        //     MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
174        //         let contained_text = theme.tab_button.style_for(active, state);
175        //         Label::new(text, contained_text.text.clone())
176        //             .contained()
177        //             .with_style(contained_text.container.clone())
178        //     })
179        //     .on_click(MouseButton::Left, move |_, this, cx| {
180        //         if !active {
181        //             this.set_mode(mode, cx);
182        //         }
183        //     })
184        //     .with_cursor_style(CursorStyle::PointingHand)
185        //     .into_any()
186        // }
187
188        // fn render_visibility(
189        //     channel_id: ChannelId,
190        //     visibility: ChannelVisibility,
191        //     theme: &theme::TabbedModal,
192        //     cx: &mut ViewContext<ChannelModal>,
193        // ) -> AnyElement<ChannelModal> {
194        //     enum TogglePublic {}
195
196        //     if visibility == ChannelVisibility::Members {
197        //         return Flex::row()
198        //             .with_child(
199        //                 MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
200        //                     let style = theme.visibility_toggle.style_for(state);
201        //                     Label::new(format!("{}", "Public access: OFF"), style.text.clone())
202        //                         .contained()
203        //                         .with_style(style.container.clone())
204        //                 })
205        //                 .on_click(MouseButton::Left, move |_, this, cx| {
206        //                     this.channel_store
207        //                         .update(cx, |channel_store, cx| {
208        //                             channel_store.set_channel_visibility(
209        //                                 channel_id,
210        //                                 ChannelVisibility::Public,
211        //                                 cx,
212        //                             )
213        //                         })
214        //                         .detach_and_log_err(cx);
215        //                 })
216        //                 .with_cursor_style(CursorStyle::PointingHand),
217        //             )
218        //             .into_any();
219        //     }
220
221        //     Flex::row()
222        //         .with_child(
223        //             MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
224        //                 let style = theme.visibility_toggle.style_for(state);
225        //                 Label::new(format!("{}", "Public access: ON"), style.text.clone())
226        //                     .contained()
227        //                     .with_style(style.container.clone())
228        //             })
229        //             .on_click(MouseButton::Left, move |_, this, cx| {
230        //                 this.channel_store
231        //                     .update(cx, |channel_store, cx| {
232        //                         channel_store.set_channel_visibility(
233        //                             channel_id,
234        //                             ChannelVisibility::Members,
235        //                             cx,
236        //                         )
237        //                     })
238        //                     .detach_and_log_err(cx);
239        //             })
240        //             .with_cursor_style(CursorStyle::PointingHand),
241        //         )
242        //         .with_spacing(14.0)
243        //         .with_child(
244        //             MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
245        //                 let style = theme.channel_link.style_for(state);
246        //                 Label::new(format!("{}", "copy link"), style.text.clone())
247        //                     .contained()
248        //                     .with_style(style.container.clone())
249        //             })
250        //             .on_click(MouseButton::Left, move |_, this, cx| {
251        //                 if let Some(channel) =
252        //                     this.channel_store.read(cx).channel_for_id(channel_id)
253        //                 {
254        //                     let item = ClipboardItem::new(channel.link());
255        //                     cx.write_to_clipboard(item);
256        //                 }
257        //             })
258        //             .with_cursor_style(CursorStyle::PointingHand),
259        //         )
260        //         .into_any()
261        // }
262
263        // Flex::column()
264        //     .with_child(
265        //         Flex::column()
266        //             .with_child(
267        //                 Label::new(format!("#{}", channel.name), theme.title.text.clone())
268        //                     .contained()
269        //                     .with_style(theme.title.container.clone()),
270        //             )
271        //             .with_child(render_visibility(channel.id, channel.visibility, theme, cx))
272        //             .with_child(Flex::row().with_children([
273        //                 render_mode_button::<InviteMembers>(
274        //                     Mode::InviteMembers,
275        //                     "Invite members",
276        //                     mode,
277        //                     theme,
278        //                     cx,
279        //                 ),
280        //                 render_mode_button::<ManageMembers>(
281        //                     Mode::ManageMembers,
282        //                     "Manage members",
283        //                     mode,
284        //                     theme,
285        //                     cx,
286        //                 ),
287        //             ]))
288        //             .expanded()
289        //             .contained()
290        //             .with_style(theme.header),
291        //     )
292        //     .with_child(
293        //         ChildView::new(&self.picker, cx)
294        //             .contained()
295        //             .with_style(theme.body),
296        //     )
297        //     .constrained()
298        //     .with_max_height(theme.max_height)
299        //     .with_max_width(theme.max_width)
300        //     .contained()
301        //     .with_style(theme.modal)
302        //     .into_any()
303    }
304
305    // fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
306    //     self.has_focus = true;
307    //     if cx.is_self_focused() {
308    //         cx.focus(&self.picker)
309    //     }
310    // }
311
312    // fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
313    //     self.has_focus = false;
314    // }
315}
316
317#[derive(Copy, Clone, PartialEq)]
318pub enum Mode {
319    ManageMembers,
320    InviteMembers,
321}
322
323pub struct ChannelModalDelegate {
324    channel_modal: WeakView<ChannelModal>,
325    matching_users: Vec<Arc<User>>,
326    matching_member_indices: Vec<usize>,
327    user_store: Model<UserStore>,
328    channel_store: Model<ChannelStore>,
329    channel_id: ChannelId,
330    selected_index: usize,
331    mode: Mode,
332    match_candidates: Vec<StringMatchCandidate>,
333    members: Vec<ChannelMembership>,
334    // context_menu: ViewHandle<ContextMenu>,
335}
336
337impl PickerDelegate for ChannelModalDelegate {
338    type ListItem = Div;
339
340    fn placeholder_text(&self) -> Arc<str> {
341        "Search collaborator by username...".into()
342    }
343
344    fn match_count(&self) -> usize {
345        match self.mode {
346            Mode::ManageMembers => self.matching_member_indices.len(),
347            Mode::InviteMembers => self.matching_users.len(),
348        }
349    }
350
351    fn selected_index(&self) -> usize {
352        self.selected_index
353    }
354
355    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
356        self.selected_index = ix;
357    }
358
359    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
360        match self.mode {
361            Mode::ManageMembers => {
362                self.match_candidates.clear();
363                self.match_candidates
364                    .extend(self.members.iter().enumerate().map(|(id, member)| {
365                        StringMatchCandidate {
366                            id,
367                            string: member.user.github_login.clone(),
368                            char_bag: member.user.github_login.chars().collect(),
369                        }
370                    }));
371
372                let matches = cx.background_executor().block(match_strings(
373                    &self.match_candidates,
374                    &query,
375                    true,
376                    usize::MAX,
377                    &Default::default(),
378                    cx.background_executor().clone(),
379                ));
380
381                cx.spawn(|picker, mut cx| async move {
382                    picker
383                        .update(&mut cx, |picker, cx| {
384                            let delegate = &mut picker.delegate;
385                            delegate.matching_member_indices.clear();
386                            delegate
387                                .matching_member_indices
388                                .extend(matches.into_iter().map(|m| m.candidate_id));
389                            cx.notify();
390                        })
391                        .ok();
392                })
393            }
394            Mode::InviteMembers => {
395                let search_users = self
396                    .user_store
397                    .update(cx, |store, cx| store.fuzzy_search_users(query, cx));
398                cx.spawn(|picker, mut cx| async move {
399                    async {
400                        let users = search_users.await?;
401                        picker.update(&mut cx, |picker, cx| {
402                            picker.delegate.matching_users = users;
403                            cx.notify();
404                        })?;
405                        anyhow::Ok(())
406                    }
407                    .log_err()
408                    .await;
409                })
410            }
411        }
412    }
413
414    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
415        if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
416            match self.mode {
417                Mode::ManageMembers => {
418                    self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
419                }
420                Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
421                    Some(proto::channel_member::Kind::Invitee) => {
422                        self.remove_selected_member(cx);
423                    }
424                    Some(proto::channel_member::Kind::AncestorMember) | None => {
425                        self.invite_member(selected_user, cx)
426                    }
427                    Some(proto::channel_member::Kind::Member) => {}
428                },
429            }
430        }
431    }
432
433    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
434        self.channel_modal
435            .update(cx, |_, cx| {
436                cx.emit(DismissEvent);
437            })
438            .ok();
439    }
440
441    fn render_match(
442        &self,
443        ix: usize,
444        selected: bool,
445        cx: &mut ViewContext<Picker<Self>>,
446    ) -> Option<Self::ListItem> {
447        None
448        //     let full_theme = &theme::current(cx);
449        //     let theme = &full_theme.collab_panel.channel_modal;
450        //     let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
451        //     let (user, role) = self.user_at_index(ix).unwrap();
452        //     let request_status = self.member_status(user.id, cx);
453
454        //     let style = tabbed_modal
455        //         .picker
456        //         .item
457        //         .in_state(selected)
458        //         .style_for(mouse_state);
459
460        //     let in_manage = matches!(self.mode, Mode::ManageMembers);
461
462        //     let mut result = Flex::row()
463        //         .with_children(user.avatar.clone().map(|avatar| {
464        //             Image::from_data(avatar)
465        //                 .with_style(theme.contact_avatar)
466        //                 .aligned()
467        //                 .left()
468        //         }))
469        //         .with_child(
470        //             Label::new(user.github_login.clone(), style.label.clone())
471        //                 .contained()
472        //                 .with_style(theme.contact_username)
473        //                 .aligned()
474        //                 .left(),
475        //         )
476        //         .with_children({
477        //             (in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
478        //                 || {
479        //                     Label::new("Invited", theme.member_tag.text.clone())
480        //                         .contained()
481        //                         .with_style(theme.member_tag.container)
482        //                         .aligned()
483        //                         .left()
484        //                 },
485        //             )
486        //         })
487        //         .with_children(if in_manage && role == Some(ChannelRole::Admin) {
488        //             Some(
489        //                 Label::new("Admin", theme.member_tag.text.clone())
490        //                     .contained()
491        //                     .with_style(theme.member_tag.container)
492        //                     .aligned()
493        //                     .left(),
494        //             )
495        //         } else if in_manage && role == Some(ChannelRole::Guest) {
496        //             Some(
497        //                 Label::new("Guest", theme.member_tag.text.clone())
498        //                     .contained()
499        //                     .with_style(theme.member_tag.container)
500        //                     .aligned()
501        //                     .left(),
502        //             )
503        //         } else {
504        //             None
505        //         })
506        //         .with_children({
507        //             let svg = match self.mode {
508        //                 Mode::ManageMembers => Some(
509        //                     Svg::new("icons/ellipsis.svg")
510        //                         .with_color(theme.member_icon.color)
511        //                         .constrained()
512        //                         .with_width(theme.member_icon.icon_width)
513        //                         .aligned()
514        //                         .constrained()
515        //                         .with_width(theme.member_icon.button_width)
516        //                         .with_height(theme.member_icon.button_width)
517        //                         .contained()
518        //                         .with_style(theme.member_icon.container),
519        //                 ),
520        //                 Mode::InviteMembers => match request_status {
521        //                     Some(proto::channel_member::Kind::Member) => Some(
522        //                         Svg::new("icons/check.svg")
523        //                             .with_color(theme.member_icon.color)
524        //                             .constrained()
525        //                             .with_width(theme.member_icon.icon_width)
526        //                             .aligned()
527        //                             .constrained()
528        //                             .with_width(theme.member_icon.button_width)
529        //                             .with_height(theme.member_icon.button_width)
530        //                             .contained()
531        //                             .with_style(theme.member_icon.container),
532        //                     ),
533        //                     Some(proto::channel_member::Kind::Invitee) => Some(
534        //                         Svg::new("icons/check.svg")
535        //                             .with_color(theme.invitee_icon.color)
536        //                             .constrained()
537        //                             .with_width(theme.invitee_icon.icon_width)
538        //                             .aligned()
539        //                             .constrained()
540        //                             .with_width(theme.invitee_icon.button_width)
541        //                             .with_height(theme.invitee_icon.button_width)
542        //                             .contained()
543        //                             .with_style(theme.invitee_icon.container),
544        //                     ),
545        //                     Some(proto::channel_member::Kind::AncestorMember) | None => None,
546        //                 },
547        //             };
548
549        //             svg.map(|svg| svg.aligned().flex_float().into_any())
550        //         })
551        //         .contained()
552        //         .with_style(style.container)
553        //         .constrained()
554        //         .with_height(tabbed_modal.row_height)
555        //         .into_any();
556
557        //     if selected {
558        //         result = Stack::new()
559        //             .with_child(result)
560        //             .with_child(
561        //                 ChildView::new(&self.context_menu, cx)
562        //                     .aligned()
563        //                     .top()
564        //                     .right(),
565        //             )
566        //             .into_any();
567        //     }
568
569        //     result
570    }
571}
572
573impl ChannelModalDelegate {
574    fn member_status(
575        &self,
576        user_id: UserId,
577        cx: &AppContext,
578    ) -> Option<proto::channel_member::Kind> {
579        self.members
580            .iter()
581            .find_map(|membership| (membership.user.id == user_id).then_some(membership.kind))
582            .or_else(|| {
583                self.channel_store
584                    .read(cx)
585                    .has_pending_channel_invite(self.channel_id, user_id)
586                    .then_some(proto::channel_member::Kind::Invitee)
587            })
588    }
589
590    fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<ChannelRole>)> {
591        match self.mode {
592            Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
593                let channel_membership = self.members.get(*ix)?;
594                Some((
595                    channel_membership.user.clone(),
596                    Some(channel_membership.role),
597                ))
598            }),
599            Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
600        }
601    }
602
603    fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
604        let (user, role) = self.user_at_index(self.selected_index)?;
605        let new_role = if role == Some(ChannelRole::Admin) {
606            ChannelRole::Member
607        } else {
608            ChannelRole::Admin
609        };
610        let update = self.channel_store.update(cx, |store, cx| {
611            store.set_member_role(self.channel_id, user.id, new_role, cx)
612        });
613        cx.spawn(|picker, mut cx| async move {
614            update.await?;
615            picker.update(&mut cx, |picker, cx| {
616                let this = &mut picker.delegate;
617                if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
618                    member.role = new_role;
619                }
620                cx.focus_self();
621                cx.notify();
622            })
623        })
624        .detach_and_log_err(cx);
625        Some(())
626    }
627
628    fn remove_selected_member(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
629        let (user, _) = self.user_at_index(self.selected_index)?;
630        let user_id = user.id;
631        let update = self.channel_store.update(cx, |store, cx| {
632            store.remove_member(self.channel_id, user_id, cx)
633        });
634        cx.spawn(|picker, mut cx| async move {
635            update.await?;
636            picker.update(&mut cx, |picker, cx| {
637                let this = &mut picker.delegate;
638                if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
639                    this.members.remove(ix);
640                    this.matching_member_indices.retain_mut(|member_ix| {
641                        if *member_ix == ix {
642                            return false;
643                        } else if *member_ix > ix {
644                            *member_ix -= 1;
645                        }
646                        true
647                    })
648                }
649
650                this.selected_index = this
651                    .selected_index
652                    .min(this.matching_member_indices.len().saturating_sub(1));
653
654                cx.focus_self();
655                cx.notify();
656            })
657        })
658        .detach_and_log_err(cx);
659        Some(())
660    }
661
662    fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
663        let invite_member = self.channel_store.update(cx, |store, cx| {
664            store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
665        });
666
667        cx.spawn(|this, mut cx| async move {
668            invite_member.await?;
669
670            this.update(&mut cx, |this, cx| {
671                let new_member = ChannelMembership {
672                    user,
673                    kind: proto::channel_member::Kind::Invitee,
674                    role: ChannelRole::Member,
675                };
676                let members = &mut this.delegate.members;
677                match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
678                    Ok(ix) | Err(ix) => members.insert(ix, new_member),
679                }
680
681                cx.notify();
682            })
683        })
684        .detach_and_log_err(cx);
685    }
686
687    fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
688        // self.context_menu.update(cx, |context_menu, cx| {
689        //     context_menu.show(
690        //         Default::default(),
691        //         AnchorCorner::TopRight,
692        //         vec![
693        //             ContextMenuItem::action("Remove", RemoveMember),
694        //             ContextMenuItem::action(
695        //                 if role == ChannelRole::Admin {
696        //                     "Make non-admin"
697        //                 } else {
698        //                     "Make admin"
699        //                 },
700        //                 ToggleMemberAdmin,
701        //             ),
702        //         ],
703        //         cx,
704        //     )
705        // })
706    }
707}