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