use channel::{ChannelMembership, ChannelStore};use client::{ proto::{self, ChannelRole, ChannelVisibility}, ChannelId, User, UserId, UserStore,};use fuzzy::{match_strings, StringMatchCandidate};use gpui::{ actions, anchored, deferred, div, App, ClipboardItem, Context, DismissEvent, Entity, EventEmitter, Focusable, ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window,};use picker::{Picker, PickerDelegate};use std::sync::Arc;use ui::{prelude::*, Avatar, CheckboxWithLabel, ContextMenu, ListItem, ListItemSpacing};use util::TryFutureExt;use workspace::{notifications::DetachAndPromptErr, ModalView}; actions!( channel_modal, [ SelectNextControl, ToggleMode, ToggleMemberAdmin, RemoveMember ]); pub struct ChannelModal { picker: Entity<Picker<ChannelModalDelegate>>, channel_store: Entity<ChannelStore>, channel_id: ChannelId,} impl ChannelModal { pub fn new( user_store: Entity<UserStore>, channel_store: Entity<ChannelStore>, channel_id: ChannelId, mode: Mode, window: &mut Window, cx: &mut Context<Self>, ) -> Self { cx.observe(&channel_store, |_, _, cx| cx.notify()).detach(); let channel_modal = cx.entity().downgrade(); let picker = cx.new(|cx| { Picker::uniform_list( ChannelModalDelegate { channel_modal, matching_users: Vec::new(), matching_member_indices: Vec::new(), selected_index: 0, user_store: user_store.clone(), channel_store: channel_store.clone(), channel_id, match_candidates: Vec::new(), context_menu: None, members: Vec::new(), has_all_members: false, mode, }, window, cx, ) .modal(false) }); Self { picker, channel_store, channel_id, } } fn toggle_mode(&mut self, _: &ToggleMode, window: &mut Window, cx: &mut Context<Self>) { let mode = match self.picker.read(cx).delegate.mode { Mode::ManageMembers => Mode::InviteMembers, Mode::InviteMembers => Mode::ManageMembers, }; self.set_mode(mode, window, cx); } fn set_mode(&mut self, mode: Mode, window: &mut Window, cx: &mut Context<Self>) { self.picker.update(cx, |picker, cx| { let delegate = &mut picker.delegate; delegate.mode = mode; delegate.selected_index = 0; picker.set_query("", window, cx); picker.update_matches(picker.query(cx), window, cx); cx.notify() }); cx.notify() } fn set_channel_visibility( &mut self, selection: &ToggleState, _: &mut Window, cx: &mut Context<Self>, ) { self.channel_store.update(cx, |channel_store, cx| { channel_store .set_channel_visibility( self.channel_id, match selection { ToggleState::Unselected => ChannelVisibility::Members, ToggleState::Selected => ChannelVisibility::Public, ToggleState::Indeterminate => return, }, cx, ) .detach_and_log_err(cx) }); } fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) { cx.emit(DismissEvent); }} impl EventEmitter<DismissEvent> for ChannelModal {}impl ModalView for ChannelModal {} impl Focusable for ChannelModal { fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { self.picker.focus_handle(cx) }} impl Render for ChannelModal { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let channel_store = self.channel_store.read(cx); let Some(channel) = channel_store.channel_for_id(self.channel_id) else { return div(); }; let channel_name = channel.name.clone(); let channel_id = channel.id; let visibility = channel.visibility; let mode = self.picker.read(cx).delegate.mode; v_flex() .key_context("ChannelModal") .on_action(cx.listener(Self::toggle_mode)) .on_action(cx.listener(Self::dismiss)) .elevation_3(cx) .w(rems(34.)) .child( v_flex() .px_2() .py_1() .gap_2() .child( h_flex() .w_px() .flex_1() .gap_1() .child(Icon::new(IconName::Hash).size(IconSize::Medium)) .child(Label::new(channel_name)), ) .child( h_flex() .w_full() .h(rems_from_px(22.)) .justify_between() .line_height(rems(1.25)) .child(CheckboxWithLabel::new( "is-public", Label::new("Public").size(LabelSize::Small), if visibility == ChannelVisibility::Public { ui::ToggleState::Selected } else { ui::ToggleState::Unselected }, cx.listener(Self::set_channel_visibility), )) .children( Some( Button::new("copy-link", "Copy Link") .label_size(LabelSize::Small) .on_click(cx.listener(move |this, _, _, cx| { if let Some(channel) = this .channel_store .read(cx) .channel_for_id(channel_id) { let item = ClipboardItem::new_string(channel.link(cx)); cx.write_to_clipboard(item); } })), ) .filter(|_| visibility == ChannelVisibility::Public), ), ) .child( h_flex() .child( div() .id("manage-members") .px_2() .py_1() .cursor_pointer() .border_b_2() .when(mode == Mode::ManageMembers, |this| { this.border_color(cx.theme().colors().border) }) .child(Label::new("Manage Members")) .on_click(cx.listener(|this, _, window, cx| { this.set_mode(Mode::ManageMembers, window, cx); })), ) .child( div() .id("invite-members") .px_2() .py_1() .cursor_pointer() .border_b_2() .when(mode == Mode::InviteMembers, |this| { this.border_color(cx.theme().colors().border) }) .child(Label::new("Invite Members")) .on_click(cx.listener(|this, _, window, cx| { this.set_mode(Mode::InviteMembers, window, cx); })), ), ), ) .child(self.picker.clone()) }} #[derive(Copy, Clone, PartialEq)]pub enum Mode { ManageMembers, InviteMembers,} pub struct ChannelModalDelegate { channel_modal: WeakEntity<ChannelModal>, matching_users: Vec<Arc<User>>, matching_member_indices: Vec<usize>, user_store: Entity<UserStore>, channel_store: Entity<ChannelStore>, channel_id: ChannelId, selected_index: usize, mode: Mode, match_candidates: Vec<StringMatchCandidate>, members: Vec<ChannelMembership>, has_all_members: bool, context_menu: Option<(Entity<ContextMenu>, Subscription)>,} impl PickerDelegate for ChannelModalDelegate { type ListItem = ListItem; fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> { "Search collaborator by username...".into() } fn match_count(&self) -> usize { match self.mode { Mode::ManageMembers => self.matching_member_indices.len(), Mode::InviteMembers => self.matching_users.len(), } } fn selected_index(&self) -> usize { self.selected_index } fn set_selected_index( &mut self, ix: usize, _window: &mut Window, _: &mut Context<Picker<Self>>, ) { self.selected_index = ix; } fn update_matches( &mut self, query: String, window: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Task<()> { match self.mode { Mode::ManageMembers => { if self.has_all_members { self.match_candidates.clear(); self.match_candidates .extend(self.members.iter().enumerate().map(|(id, member)| { StringMatchCandidate::new(id, &member.user.github_login) })); let matches = cx.background_executor().block(match_strings( &self.match_candidates, &query, true, usize::MAX, &Default::default(), cx.background_executor().clone(), )); cx.spawn_in(window, |picker, mut cx| async move { picker .update(&mut cx, |picker, cx| { let delegate = &mut picker.delegate; delegate.matching_member_indices.clear(); delegate .matching_member_indices .extend(matches.into_iter().map(|m| m.candidate_id)); cx.notify(); }) .ok(); }) } else { let search_members = self.channel_store.update(cx, |store, cx| { store.fuzzy_search_members(self.channel_id, query.clone(), 100, cx) }); cx.spawn_in(window, |picker, mut cx| async move { async { let members = search_members.await?; picker.update(&mut cx, |picker, cx| { picker.delegate.has_all_members = query.is_empty() && members.len() < 100; picker.delegate.matching_member_indices = (0..members.len()).collect(); picker.delegate.members = members; cx.notify(); })?; anyhow::Ok(()) } .log_err() .await; }) } } Mode::InviteMembers => { let search_users = self .user_store .update(cx, |store, cx| store.fuzzy_search_users(query, cx)); cx.spawn_in(window, |picker, mut cx| async move { async { let users = search_users.await?; picker.update(&mut cx, |picker, cx| { picker.delegate.matching_users = users; cx.notify(); })?; anyhow::Ok(()) } .log_err() .await; }) } } } fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) { if let Some(selected_user) = self.user_at_index(self.selected_index) { if Some(selected_user.id) == self.user_store.read(cx).current_user().map(|user| user.id) { return; } match self.mode { Mode::ManageMembers => self.show_context_menu(self.selected_index, window, cx), Mode::InviteMembers => match self.member_status(selected_user.id, cx) { Some(proto::channel_member::Kind::Invitee) => { self.remove_member(selected_user.id, window, cx); } Some(proto::channel_member::Kind::Member) => {} None => self.invite_member(selected_user, window, cx), }, } } } fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) { if self.context_menu.is_none() { self.channel_modal .update(cx, |_, cx| { cx.emit(DismissEvent); }) .ok(); } } fn render_match( &self, ix: usize, selected: bool, _: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Option<Self::ListItem> { let user = self.user_at_index(ix)?; let membership = self.member_at_index(ix); let request_status = self.member_status(user.id, cx); let is_me = self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id); Some( ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) .toggle_state(selected) .start_slot(Avatar::new(user.avatar_uri.clone())) .child(Label::new(user.github_login.clone())) .end_slot(h_flex().gap_2().map(|slot| { match self.mode { Mode::ManageMembers => slot .children( if request_status == Some(proto::channel_member::Kind::Invitee) { Some(Label::new("Invited")) } else { None }, ) .children(match membership.map(|m| m.role) { Some(ChannelRole::Admin) => Some(Label::new("Admin")), Some(ChannelRole::Guest) => Some(Label::new("Guest")), _ => None, }) .when(!is_me, |el| { el.child(IconButton::new("ellipsis", IconName::Ellipsis)) }) .when(is_me, |el| el.child(Label::new("You").color(Color::Muted))) .children( if let (Some((menu, _)), true) = (&self.context_menu, selected) { Some( deferred( anchored() .anchor(gpui::Corner::TopRight) .child(menu.clone()), ) .with_priority(1), ) } else { None }, ), Mode::InviteMembers => match request_status { Some(proto::channel_member::Kind::Invitee) => { slot.children(Some(Label::new("Invited"))) } Some(proto::channel_member::Kind::Member) => { slot.children(Some(Label::new("Member"))) } _ => slot, }, } })), ) }} impl ChannelModalDelegate { fn member_status(&self, user_id: UserId, cx: &App) -> Option<proto::channel_member::Kind> { self.members .iter() .find_map(|membership| (membership.user.id == user_id).then_some(membership.kind)) .or_else(|| { self.channel_store .read(cx) .has_pending_channel_invite(self.channel_id, user_id) .then_some(proto::channel_member::Kind::Invitee) }) } fn member_at_index(&self, ix: usize) -> Option<&ChannelMembership> { self.matching_member_indices .get(ix) .and_then(|ix| self.members.get(*ix)) } fn user_at_index(&self, ix: usize) -> Option<Arc<User>> { match self.mode { Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| { let channel_membership = self.members.get(*ix)?; Some(channel_membership.user.clone()) }), Mode::InviteMembers => self.matching_users.get(ix).cloned(), } } fn set_user_role( &mut self, user_id: UserId, new_role: ChannelRole, window: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Option<()> { let update = self.channel_store.update(cx, |store, cx| { store.set_member_role(self.channel_id, user_id, new_role, cx) }); cx.spawn_in(window, |picker, mut cx| async move { update.await?; picker.update_in(&mut cx, |picker, window, cx| { let this = &mut picker.delegate; if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user_id) { member.role = new_role; } cx.focus_self(window); cx.notify();Lines 1–500 of 650. Load more
use channel::{ChannelMembership, ChannelStore}; use client::{ proto::{self, ChannelRole, ChannelVisibility}, ChannelId, User, UserId, UserStore, }; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, anchored, deferred, div, App, ClipboardItem, Context, DismissEvent, Entity, EventEmitter, Focusable, ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, }; use picker::{Picker, PickerDelegate}; use std::sync::Arc; use ui::{prelude::*, Avatar, CheckboxWithLabel, ContextMenu, ListItem, ListItemSpacing}; use util::TryFutureExt; use workspace::{notifications::DetachAndPromptErr, ModalView}; actions!( channel_modal, [ SelectNextControl, ToggleMode, ToggleMemberAdmin, RemoveMember ] ); pub struct ChannelModal { picker: Entity<Picker<ChannelModalDelegate>>, channel_store: Entity<ChannelStore>, channel_id: ChannelId, } impl ChannelModal { pub fn new( user_store: Entity<UserStore>, channel_store: Entity<ChannelStore>, channel_id: ChannelId, mode: Mode, window: &mut Window, cx: &mut Context<Self>, ) -> Self { cx.observe(&channel_store, |_, _, cx| cx.notify()).detach(); let channel_modal = cx.entity().downgrade(); let picker = cx.new(|cx| { Picker::uniform_list( ChannelModalDelegate { channel_modal, matching_users: Vec::new(), matching_member_indices: Vec::new(), selected_index: 0, user_store: user_store.clone(), channel_store: channel_store.clone(), channel_id, match_candidates: Vec::new(), context_menu: None, members: Vec::new(), has_all_members: false, mode, }, window, cx, ) .modal(false) }); Self { picker, channel_store, channel_id, } } fn toggle_mode(&mut self, _: &ToggleMode, window: &mut Window, cx: &mut Context<Self>) { let mode = match self.picker.read(cx).delegate.mode { Mode::ManageMembers => Mode::InviteMembers, Mode::InviteMembers => Mode::ManageMembers, }; self.set_mode(mode, window, cx); } fn set_mode(&mut self, mode: Mode, window: &mut Window, cx: &mut Context<Self>) { self.picker.update(cx, |picker, cx| { let delegate = &mut picker.delegate; delegate.mode = mode; delegate.selected_index = 0; picker.set_query("", window, cx); picker.update_matches(picker.query(cx), window, cx); cx.notify() }); cx.notify() } fn set_channel_visibility( &mut self, selection: &ToggleState, _: &mut Window, cx: &mut Context<Self>, ) { self.channel_store.update(cx, |channel_store, cx| { channel_store .set_channel_visibility( self.channel_id, match selection { ToggleState::Unselected => ChannelVisibility::Members, ToggleState::Selected => ChannelVisibility::Public, ToggleState::Indeterminate => return, }, cx, ) .detach_and_log_err(cx) }); } fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) { cx.emit(DismissEvent); } } impl EventEmitter<DismissEvent> for ChannelModal {} impl ModalView for ChannelModal {} impl Focusable for ChannelModal { fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { self.picker.focus_handle(cx) } } impl Render for ChannelModal { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let channel_store = self.channel_store.read(cx); let Some(channel) = channel_store.channel_for_id(self.channel_id) else { return div(); }; let channel_name = channel.name.clone(); let channel_id = channel.id; let visibility = channel.visibility; let mode = self.picker.read(cx).delegate.mode; v_flex() .key_context("ChannelModal") .on_action(cx.listener(Self::toggle_mode)) .on_action(cx.listener(Self::dismiss)) .elevation_3(cx) .w(rems(34.)) .child( v_flex() .px_2() .py_1() .gap_2() .child( h_flex() .w_px() .flex_1() .gap_1() .child(Icon::new(IconName::Hash).size(IconSize::Medium)) .child(Label::new(channel_name)), ) .child( h_flex() .w_full() .h(rems_from_px(22.)) .justify_between() .line_height(rems(1.25)) .child(CheckboxWithLabel::new( "is-public", Label::new("Public").size(LabelSize::Small), if visibility == ChannelVisibility::Public { ui::ToggleState::Selected } else { ui::ToggleState::Unselected }, cx.listener(Self::set_channel_visibility), )) .children( Some( Button::new("copy-link", "Copy Link") .label_size(LabelSize::Small) .on_click(cx.listener(move |this, _, _, cx| { if let Some(channel) = this .channel_store .read(cx) .channel_for_id(channel_id) { let item = ClipboardItem::new_string(channel.link(cx)); cx.write_to_clipboard(item); } })), ) .filter(|_| visibility == ChannelVisibility::Public), ), ) .child( h_flex() .child( div() .id("manage-members") .px_2() .py_1() .cursor_pointer() .border_b_2() .when(mode == Mode::ManageMembers, |this| { this.border_color(cx.theme().colors().border) }) .child(Label::new("Manage Members")) .on_click(cx.listener(|this, _, window, cx| { this.set_mode(Mode::ManageMembers, window, cx); })), ) .child( div() .id("invite-members") .px_2() .py_1() .cursor_pointer() .border_b_2() .when(mode == Mode::InviteMembers, |this| { this.border_color(cx.theme().colors().border) }) .child(Label::new("Invite Members")) .on_click(cx.listener(|this, _, window, cx| { this.set_mode(Mode::InviteMembers, window, cx); })), ), ), ) .child(self.picker.clone()) } } #[derive(Copy, Clone, PartialEq)] pub enum Mode { ManageMembers, InviteMembers, } pub struct ChannelModalDelegate { channel_modal: WeakEntity<ChannelModal>, matching_users: Vec<Arc<User>>, matching_member_indices: Vec<usize>, user_store: Entity<UserStore>, channel_store: Entity<ChannelStore>, channel_id: ChannelId, selected_index: usize, mode: Mode, match_candidates: Vec<StringMatchCandidate>, members: Vec<ChannelMembership>, has_all_members: bool, context_menu: Option<(Entity<ContextMenu>, Subscription)>, } impl PickerDelegate for ChannelModalDelegate { type ListItem = ListItem; fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> { "Search collaborator by username...".into() } fn match_count(&self) -> usize { match self.mode { Mode::ManageMembers => self.matching_member_indices.len(), Mode::InviteMembers => self.matching_users.len(), } } fn selected_index(&self) -> usize { self.selected_index } fn set_selected_index( &mut self, ix: usize, _window: &mut Window, _: &mut Context<Picker<Self>>, ) { self.selected_index = ix; } fn update_matches( &mut self, query: String, window: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Task<()> { match self.mode { Mode::ManageMembers => { if self.has_all_members { self.match_candidates.clear(); self.match_candidates .extend(self.members.iter().enumerate().map(|(id, member)| { StringMatchCandidate::new(id, &member.user.github_login) })); let matches = cx.background_executor().block(match_strings( &self.match_candidates, &query, true, usize::MAX, &Default::default(), cx.background_executor().clone(), )); cx.spawn_in(window, |picker, mut cx| async move { picker .update(&mut cx, |picker, cx| { let delegate = &mut picker.delegate; delegate.matching_member_indices.clear(); delegate .matching_member_indices .extend(matches.into_iter().map(|m| m.candidate_id)); cx.notify(); }) .ok(); }) } else { let search_members = self.channel_store.update(cx, |store, cx| { store.fuzzy_search_members(self.channel_id, query.clone(), 100, cx) }); cx.spawn_in(window, |picker, mut cx| async move { async { let members = search_members.await?; picker.update(&mut cx, |picker, cx| { picker.delegate.has_all_members = query.is_empty() && members.len() < 100; picker.delegate.matching_member_indices = (0..members.len()).collect(); picker.delegate.members = members; cx.notify(); })?; anyhow::Ok(()) } .log_err() .await; }) } } Mode::InviteMembers => { let search_users = self .user_store .update(cx, |store, cx| store.fuzzy_search_users(query, cx)); cx.spawn_in(window, |picker, mut cx| async move { async { let users = search_users.await?; picker.update(&mut cx, |picker, cx| { picker.delegate.matching_users = users; cx.notify(); })?; anyhow::Ok(()) } .log_err() .await; }) } } } fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) { if let Some(selected_user) = self.user_at_index(self.selected_index) { if Some(selected_user.id) == self.user_store.read(cx).current_user().map(|user| user.id) { return; } match self.mode { Mode::ManageMembers => self.show_context_menu(self.selected_index, window, cx), Mode::InviteMembers => match self.member_status(selected_user.id, cx) { Some(proto::channel_member::Kind::Invitee) => { self.remove_member(selected_user.id, window, cx); } Some(proto::channel_member::Kind::Member) => {} None => self.invite_member(selected_user, window, cx), }, } } } fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) { if self.context_menu.is_none() { self.channel_modal .update(cx, |_, cx| { cx.emit(DismissEvent); }) .ok(); } } fn render_match( &self, ix: usize, selected: bool, _: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Option<Self::ListItem> { let user = self.user_at_index(ix)?; let membership = self.member_at_index(ix); let request_status = self.member_status(user.id, cx); let is_me = self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id); Some( ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) .toggle_state(selected) .start_slot(Avatar::new(user.avatar_uri.clone())) .child(Label::new(user.github_login.clone())) .end_slot(h_flex().gap_2().map(|slot| { match self.mode { Mode::ManageMembers => slot .children( if request_status == Some(proto::channel_member::Kind::Invitee) { Some(Label::new("Invited")) } else { None }, ) .children(match membership.map(|m| m.role) { Some(ChannelRole::Admin) => Some(Label::new("Admin")), Some(ChannelRole::Guest) => Some(Label::new("Guest")), _ => None, }) .when(!is_me, |el| { el.child(IconButton::new("ellipsis", IconName::Ellipsis)) }) .when(is_me, |el| el.child(Label::new("You").color(Color::Muted))) .children( if let (Some((menu, _)), true) = (&self.context_menu, selected) { Some( deferred( anchored() .anchor(gpui::Corner::TopRight) .child(menu.clone()), ) .with_priority(1), ) } else { None }, ), Mode::InviteMembers => match request_status { Some(proto::channel_member::Kind::Invitee) => { slot.children(Some(Label::new("Invited"))) } Some(proto::channel_member::Kind::Member) => { slot.children(Some(Label::new("Member"))) } _ => slot, }, } })), ) } } impl ChannelModalDelegate { fn member_status(&self, user_id: UserId, cx: &App) -> Option<proto::channel_member::Kind> { self.members .iter() .find_map(|membership| (membership.user.id == user_id).then_some(membership.kind)) .or_else(|| { self.channel_store .read(cx) .has_pending_channel_invite(self.channel_id, user_id) .then_some(proto::channel_member::Kind::Invitee) }) } fn member_at_index(&self, ix: usize) -> Option<&ChannelMembership> { self.matching_member_indices .get(ix) .and_then(|ix| self.members.get(*ix)) } fn user_at_index(&self, ix: usize) -> Option<Arc<User>> { match self.mode { Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| { let channel_membership = self.members.get(*ix)?; Some(channel_membership.user.clone()) }), Mode::InviteMembers => self.matching_users.get(ix).cloned(), } } fn set_user_role( &mut self, user_id: UserId, new_role: ChannelRole, window: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Option<()> { let update = self.channel_store.update(cx, |store, cx| { store.set_member_role(self.channel_id, user_id, new_role, cx) }); cx.spawn_in(window, |picker, mut cx| async move { update.await?; picker.update_in(&mut cx, |picker, window, cx| { let this = &mut picker.delegate; if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user_id) { member.role = new_role; } cx.focus_self(window); cx.notify();