1mod channel_modal;
2mod contact_finder;
3mod panel_settings;
4
5use anyhow::Result;
6use call::ActiveCall;
7use client::{
8 proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore,
9};
10
11use context_menu::{ContextMenu, ContextMenuItem};
12use db::kvp::KEY_VALUE_STORE;
13use editor::{Cancel, Editor};
14use futures::StreamExt;
15use fuzzy::{match_strings, StringMatchCandidate};
16use gpui::{
17 actions,
18 elements::{
19 Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState,
20 MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable,
21 Stack, Svg,
22 },
23 geometry::{
24 rect::RectF,
25 vector::{vec2f, Vector2F},
26 },
27 impl_actions,
28 platform::{CursorStyle, MouseButton, PromptLevel},
29 serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle,
30 Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
31};
32use menu::{Confirm, SelectNext, SelectPrev};
33use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings};
34use project::{Fs, Project};
35use serde_derive::{Deserialize, Serialize};
36use settings::SettingsStore;
37use staff_mode::StaffMode;
38use std::{borrow::Cow, mem, sync::Arc};
39use theme::{components::ComponentExt, IconButton};
40use util::{iife, ResultExt, TryFutureExt};
41use workspace::{
42 dock::{DockPosition, Panel},
43 item::ItemHandle,
44 Workspace,
45};
46
47use crate::face_pile::FacePile;
48use channel_modal::ChannelModal;
49
50use self::contact_finder::ContactFinder;
51
52#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
53struct RemoveChannel {
54 channel_id: u64,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
58struct ToggleCollapse {
59 channel_id: u64,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
63struct NewChannel {
64 channel_id: u64,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
68struct InviteMembers {
69 channel_id: u64,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
73struct ManageMembers {
74 channel_id: u64,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
78struct RenameChannel {
79 channel_id: u64,
80}
81
82actions!(
83 collab_panel,
84 [
85 ToggleFocus,
86 Remove,
87 Secondary,
88 CollapseSelectedChannel,
89 ExpandSelectedChannel
90 ]
91);
92
93impl_actions!(
94 collab_panel,
95 [
96 RemoveChannel,
97 NewChannel,
98 InviteMembers,
99 ManageMembers,
100 RenameChannel,
101 ToggleCollapse
102 ]
103);
104
105const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
106
107pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
108 settings::register::<panel_settings::CollaborationPanelSettings>(cx);
109 contact_finder::init(cx);
110 channel_modal::init(cx);
111
112 cx.add_action(CollabPanel::cancel);
113 cx.add_action(CollabPanel::select_next);
114 cx.add_action(CollabPanel::select_prev);
115 cx.add_action(CollabPanel::confirm);
116 cx.add_action(CollabPanel::remove);
117 cx.add_action(CollabPanel::remove_selected_channel);
118 cx.add_action(CollabPanel::show_inline_context_menu);
119 cx.add_action(CollabPanel::new_subchannel);
120 cx.add_action(CollabPanel::invite_members);
121 cx.add_action(CollabPanel::manage_members);
122 cx.add_action(CollabPanel::rename_selected_channel);
123 cx.add_action(CollabPanel::rename_channel);
124 cx.add_action(CollabPanel::toggle_channel_collapsed);
125 cx.add_action(CollabPanel::collapse_selected_channel);
126 cx.add_action(CollabPanel::expand_selected_channel)
127}
128
129#[derive(Debug)]
130pub enum ChannelEditingState {
131 Create {
132 parent_id: Option<u64>,
133 pending_name: Option<String>,
134 },
135 Rename {
136 channel_id: u64,
137 pending_name: Option<String>,
138 },
139}
140
141impl ChannelEditingState {
142 fn pending_name(&self) -> Option<&str> {
143 match self {
144 ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
145 ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
146 }
147 }
148}
149
150pub struct CollabPanel {
151 width: Option<f32>,
152 fs: Arc<dyn Fs>,
153 has_focus: bool,
154 pending_serialization: Task<Option<()>>,
155 context_menu: ViewHandle<ContextMenu>,
156 filter_editor: ViewHandle<Editor>,
157 channel_name_editor: ViewHandle<Editor>,
158 channel_editing_state: Option<ChannelEditingState>,
159 entries: Vec<ListEntry>,
160 selection: Option<usize>,
161 user_store: ModelHandle<UserStore>,
162 client: Arc<Client>,
163 channel_store: ModelHandle<ChannelStore>,
164 project: ModelHandle<Project>,
165 match_candidates: Vec<StringMatchCandidate>,
166 list_state: ListState<Self>,
167 subscriptions: Vec<Subscription>,
168 collapsed_sections: Vec<Section>,
169 collapsed_channels: Vec<ChannelId>,
170 workspace: WeakViewHandle<Workspace>,
171 context_menu_on_selected: bool,
172}
173
174#[derive(Serialize, Deserialize)]
175struct SerializedChannelsPanel {
176 width: Option<f32>,
177}
178
179#[derive(Debug)]
180pub enum Event {
181 DockPositionChanged,
182 Focus,
183 Dismissed,
184}
185
186#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
187enum Section {
188 ActiveCall,
189 Channels,
190 ChannelInvites,
191 ContactRequests,
192 Contacts,
193 Online,
194 Offline,
195}
196
197#[derive(Clone, Debug)]
198enum ListEntry {
199 Header(Section, usize),
200 CallParticipant {
201 user: Arc<User>,
202 is_pending: bool,
203 },
204 ParticipantProject {
205 project_id: u64,
206 worktree_root_names: Vec<String>,
207 host_user_id: u64,
208 is_last: bool,
209 },
210 ParticipantScreen {
211 peer_id: PeerId,
212 is_last: bool,
213 },
214 IncomingRequest(Arc<User>),
215 OutgoingRequest(Arc<User>),
216 ChannelInvite(Arc<Channel>),
217 Channel {
218 channel: Arc<Channel>,
219 depth: usize,
220 },
221 ChannelEditor {
222 depth: usize,
223 },
224 Contact {
225 contact: Arc<Contact>,
226 calling: bool,
227 },
228 ContactPlaceholder,
229}
230
231impl Entity for CollabPanel {
232 type Event = Event;
233}
234
235impl CollabPanel {
236 pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
237 cx.add_view::<Self, _>(|cx| {
238 let view_id = cx.view_id();
239
240 let filter_editor = cx.add_view(|cx| {
241 let mut editor = Editor::single_line(
242 Some(Arc::new(|theme| {
243 theme.collab_panel.user_query_editor.clone()
244 })),
245 cx,
246 );
247 editor.set_placeholder_text("Filter channels, contacts", cx);
248 editor
249 });
250
251 cx.subscribe(&filter_editor, |this, _, event, cx| {
252 if let editor::Event::BufferEdited = event {
253 let query = this.filter_editor.read(cx).text(cx);
254 if !query.is_empty() {
255 this.selection.take();
256 }
257 this.update_entries(true, cx);
258 if !query.is_empty() {
259 this.selection = this
260 .entries
261 .iter()
262 .position(|entry| !matches!(entry, ListEntry::Header(_, _)));
263 }
264 }
265 })
266 .detach();
267
268 let channel_name_editor = cx.add_view(|cx| {
269 Editor::single_line(
270 Some(Arc::new(|theme| {
271 theme.collab_panel.user_query_editor.clone()
272 })),
273 cx,
274 )
275 });
276
277 cx.subscribe(&channel_name_editor, |this, _, event, cx| {
278 if let editor::Event::Blurred = event {
279 if let Some(state) = &this.channel_editing_state {
280 if state.pending_name().is_some() {
281 return;
282 }
283 }
284 this.take_editing_state(cx);
285 this.update_entries(false, cx);
286 cx.notify();
287 }
288 })
289 .detach();
290
291 let list_state =
292 ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
293 let theme = theme::current(cx).clone();
294 let is_selected = this.selection == Some(ix);
295 let current_project_id = this.project.read(cx).remote_id();
296
297 match &this.entries[ix] {
298 ListEntry::Header(section, depth) => {
299 let is_collapsed = this.collapsed_sections.contains(section);
300 this.render_header(
301 *section,
302 &theme,
303 *depth,
304 is_selected,
305 is_collapsed,
306 cx,
307 )
308 }
309 ListEntry::CallParticipant { user, is_pending } => {
310 Self::render_call_participant(
311 user,
312 *is_pending,
313 is_selected,
314 &theme.collab_panel,
315 )
316 }
317 ListEntry::ParticipantProject {
318 project_id,
319 worktree_root_names,
320 host_user_id,
321 is_last,
322 } => Self::render_participant_project(
323 *project_id,
324 worktree_root_names,
325 *host_user_id,
326 Some(*project_id) == current_project_id,
327 *is_last,
328 is_selected,
329 &theme.collab_panel,
330 cx,
331 ),
332 ListEntry::ParticipantScreen { peer_id, is_last } => {
333 Self::render_participant_screen(
334 *peer_id,
335 *is_last,
336 is_selected,
337 &theme.collab_panel,
338 cx,
339 )
340 }
341 ListEntry::Channel { channel, depth } => {
342 let channel_row = this.render_channel(
343 &*channel,
344 *depth,
345 &theme.collab_panel,
346 is_selected,
347 cx,
348 );
349
350 if is_selected && this.context_menu_on_selected {
351 Stack::new()
352 .with_child(channel_row)
353 .with_child(
354 ChildView::new(&this.context_menu, cx)
355 .aligned()
356 .bottom()
357 .right(),
358 )
359 .into_any()
360 } else {
361 return channel_row;
362 }
363 }
364 ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
365 channel.clone(),
366 this.channel_store.clone(),
367 &theme.collab_panel,
368 is_selected,
369 cx,
370 ),
371 ListEntry::IncomingRequest(user) => Self::render_contact_request(
372 user.clone(),
373 this.user_store.clone(),
374 &theme.collab_panel,
375 true,
376 is_selected,
377 cx,
378 ),
379 ListEntry::OutgoingRequest(user) => Self::render_contact_request(
380 user.clone(),
381 this.user_store.clone(),
382 &theme.collab_panel,
383 false,
384 is_selected,
385 cx,
386 ),
387 ListEntry::Contact { contact, calling } => Self::render_contact(
388 contact,
389 *calling,
390 &this.project,
391 &theme.collab_panel,
392 is_selected,
393 cx,
394 ),
395 ListEntry::ChannelEditor { depth } => {
396 this.render_channel_editor(&theme, *depth, cx)
397 }
398 ListEntry::ContactPlaceholder => {
399 this.render_contact_placeholder(&theme.collab_panel, is_selected, cx)
400 }
401 }
402 });
403
404 let mut this = Self {
405 width: None,
406 has_focus: false,
407 fs: workspace.app_state().fs.clone(),
408 pending_serialization: Task::ready(None),
409 context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
410 channel_name_editor,
411 filter_editor,
412 entries: Vec::default(),
413 channel_editing_state: None,
414 selection: None,
415 user_store: workspace.user_store().clone(),
416 channel_store: workspace.app_state().channel_store.clone(),
417 project: workspace.project().clone(),
418 subscriptions: Vec::default(),
419 match_candidates: Vec::default(),
420 collapsed_sections: vec![Section::Offline],
421 collapsed_channels: Vec::default(),
422 workspace: workspace.weak_handle(),
423 client: workspace.app_state().client.clone(),
424 context_menu_on_selected: true,
425 list_state,
426 };
427
428 this.update_entries(false, cx);
429
430 // Update the dock position when the setting changes.
431 let mut old_dock_position = this.position(cx);
432 this.subscriptions
433 .push(
434 cx.observe_global::<SettingsStore, _>(move |this: &mut CollabPanel, cx| {
435 let new_dock_position = this.position(cx);
436 if new_dock_position != old_dock_position {
437 old_dock_position = new_dock_position;
438 cx.emit(Event::DockPositionChanged);
439 }
440 cx.notify();
441 }),
442 );
443
444 let active_call = ActiveCall::global(cx);
445 this.subscriptions
446 .push(cx.observe(&this.user_store, |this, _, cx| {
447 this.update_entries(true, cx)
448 }));
449 this.subscriptions
450 .push(cx.observe(&this.channel_store, |this, _, cx| {
451 this.update_entries(true, cx)
452 }));
453 this.subscriptions
454 .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
455 this.subscriptions.push(
456 cx.observe_global::<StaffMode, _>(move |this, cx| this.update_entries(true, cx)),
457 );
458 this.subscriptions.push(cx.subscribe(
459 &this.channel_store,
460 |this, _channel_store, e, cx| match e {
461 ChannelEvent::ChannelCreated(channel_id)
462 | ChannelEvent::ChannelRenamed(channel_id) => {
463 if this.take_editing_state(cx) {
464 this.update_entries(false, cx);
465 this.selection = this.entries.iter().position(|entry| {
466 if let ListEntry::Channel { channel, .. } = entry {
467 channel.id == *channel_id
468 } else {
469 false
470 }
471 });
472 }
473 }
474 },
475 ));
476
477 this
478 })
479 }
480
481 pub fn load(
482 workspace: WeakViewHandle<Workspace>,
483 cx: AsyncAppContext,
484 ) -> Task<Result<ViewHandle<Self>>> {
485 cx.spawn(|mut cx| async move {
486 let serialized_panel = if let Some(panel) = cx
487 .background()
488 .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
489 .await
490 .log_err()
491 .flatten()
492 {
493 Some(serde_json::from_str::<SerializedChannelsPanel>(&panel)?)
494 } else {
495 None
496 };
497
498 workspace.update(&mut cx, |workspace, cx| {
499 let panel = CollabPanel::new(workspace, cx);
500 if let Some(serialized_panel) = serialized_panel {
501 panel.update(cx, |panel, cx| {
502 panel.width = serialized_panel.width;
503 cx.notify();
504 });
505 }
506 panel
507 })
508 })
509 }
510
511 fn serialize(&mut self, cx: &mut ViewContext<Self>) {
512 let width = self.width;
513 self.pending_serialization = cx.background().spawn(
514 async move {
515 KEY_VALUE_STORE
516 .write_kvp(
517 COLLABORATION_PANEL_KEY.into(),
518 serde_json::to_string(&SerializedChannelsPanel { width })?,
519 )
520 .await?;
521 anyhow::Ok(())
522 }
523 .log_err(),
524 );
525 }
526
527 fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
528 let channel_store = self.channel_store.read(cx);
529 let user_store = self.user_store.read(cx);
530 let query = self.filter_editor.read(cx).text(cx);
531 let executor = cx.background().clone();
532
533 let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
534 let old_entries = mem::take(&mut self.entries);
535
536 if let Some(room) = ActiveCall::global(cx).read(cx).room() {
537 self.entries.push(ListEntry::Header(Section::ActiveCall, 0));
538
539 if !self.collapsed_sections.contains(&Section::ActiveCall) {
540 let room = room.read(cx);
541
542 // Populate the active user.
543 if let Some(user) = user_store.current_user() {
544 self.match_candidates.clear();
545 self.match_candidates.push(StringMatchCandidate {
546 id: 0,
547 string: user.github_login.clone(),
548 char_bag: user.github_login.chars().collect(),
549 });
550 let matches = executor.block(match_strings(
551 &self.match_candidates,
552 &query,
553 true,
554 usize::MAX,
555 &Default::default(),
556 executor.clone(),
557 ));
558 if !matches.is_empty() {
559 let user_id = user.id;
560 self.entries.push(ListEntry::CallParticipant {
561 user,
562 is_pending: false,
563 });
564 let mut projects = room.local_participant().projects.iter().peekable();
565 while let Some(project) = projects.next() {
566 self.entries.push(ListEntry::ParticipantProject {
567 project_id: project.id,
568 worktree_root_names: project.worktree_root_names.clone(),
569 host_user_id: user_id,
570 is_last: projects.peek().is_none(),
571 });
572 }
573 }
574 }
575
576 // Populate remote participants.
577 self.match_candidates.clear();
578 self.match_candidates
579 .extend(room.remote_participants().iter().map(|(_, participant)| {
580 StringMatchCandidate {
581 id: participant.user.id as usize,
582 string: participant.user.github_login.clone(),
583 char_bag: participant.user.github_login.chars().collect(),
584 }
585 }));
586 let matches = executor.block(match_strings(
587 &self.match_candidates,
588 &query,
589 true,
590 usize::MAX,
591 &Default::default(),
592 executor.clone(),
593 ));
594 for mat in matches {
595 let user_id = mat.candidate_id as u64;
596 let participant = &room.remote_participants()[&user_id];
597 self.entries.push(ListEntry::CallParticipant {
598 user: participant.user.clone(),
599 is_pending: false,
600 });
601 let mut projects = participant.projects.iter().peekable();
602 while let Some(project) = projects.next() {
603 self.entries.push(ListEntry::ParticipantProject {
604 project_id: project.id,
605 worktree_root_names: project.worktree_root_names.clone(),
606 host_user_id: participant.user.id,
607 is_last: projects.peek().is_none()
608 && participant.video_tracks.is_empty(),
609 });
610 }
611 if !participant.video_tracks.is_empty() {
612 self.entries.push(ListEntry::ParticipantScreen {
613 peer_id: participant.peer_id,
614 is_last: true,
615 });
616 }
617 }
618
619 // Populate pending participants.
620 self.match_candidates.clear();
621 self.match_candidates
622 .extend(room.pending_participants().iter().enumerate().map(
623 |(id, participant)| StringMatchCandidate {
624 id,
625 string: participant.github_login.clone(),
626 char_bag: participant.github_login.chars().collect(),
627 },
628 ));
629 let matches = executor.block(match_strings(
630 &self.match_candidates,
631 &query,
632 true,
633 usize::MAX,
634 &Default::default(),
635 executor.clone(),
636 ));
637 self.entries
638 .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
639 user: room.pending_participants()[mat.candidate_id].clone(),
640 is_pending: true,
641 }));
642 }
643 }
644
645 let mut request_entries = Vec::new();
646 if self.include_channels_section(cx) {
647 self.entries.push(ListEntry::Header(Section::Channels, 0));
648
649 if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
650 self.match_candidates.clear();
651 self.match_candidates
652 .extend(
653 channel_store
654 .channels()
655 .enumerate()
656 .map(|(ix, (_, channel))| StringMatchCandidate {
657 id: ix,
658 string: channel.name.clone(),
659 char_bag: channel.name.chars().collect(),
660 }),
661 );
662 let matches = executor.block(match_strings(
663 &self.match_candidates,
664 &query,
665 true,
666 usize::MAX,
667 &Default::default(),
668 executor.clone(),
669 ));
670 if let Some(state) = &self.channel_editing_state {
671 if matches!(
672 state,
673 ChannelEditingState::Create {
674 parent_id: None,
675 ..
676 }
677 ) {
678 self.entries.push(ListEntry::ChannelEditor { depth: 0 });
679 }
680 }
681 let mut collapse_depth = None;
682 for mat in matches {
683 let (depth, channel) =
684 channel_store.channel_at_index(mat.candidate_id).unwrap();
685
686 if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
687 collapse_depth = Some(depth);
688 } else if let Some(collapsed_depth) = collapse_depth {
689 if depth > collapsed_depth {
690 continue;
691 }
692 if self.is_channel_collapsed(channel.id) {
693 collapse_depth = Some(depth);
694 } else {
695 collapse_depth = None;
696 }
697 }
698
699 match &self.channel_editing_state {
700 Some(ChannelEditingState::Create { parent_id, .. })
701 if *parent_id == Some(channel.id) =>
702 {
703 self.entries.push(ListEntry::Channel {
704 channel: channel.clone(),
705 depth,
706 });
707 self.entries
708 .push(ListEntry::ChannelEditor { depth: depth + 1 });
709 }
710 Some(ChannelEditingState::Rename { channel_id, .. })
711 if *channel_id == channel.id =>
712 {
713 self.entries.push(ListEntry::ChannelEditor { depth });
714 }
715 _ => {
716 self.entries.push(ListEntry::Channel {
717 channel: channel.clone(),
718 depth,
719 });
720 }
721 }
722 }
723 }
724
725 let channel_invites = channel_store.channel_invitations();
726 if !channel_invites.is_empty() {
727 self.match_candidates.clear();
728 self.match_candidates
729 .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
730 StringMatchCandidate {
731 id: ix,
732 string: channel.name.clone(),
733 char_bag: channel.name.chars().collect(),
734 }
735 }));
736 let matches = executor.block(match_strings(
737 &self.match_candidates,
738 &query,
739 true,
740 usize::MAX,
741 &Default::default(),
742 executor.clone(),
743 ));
744 request_entries.extend(matches.iter().map(|mat| {
745 ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
746 }));
747
748 if !request_entries.is_empty() {
749 self.entries
750 .push(ListEntry::Header(Section::ChannelInvites, 1));
751 if !self.collapsed_sections.contains(&Section::ChannelInvites) {
752 self.entries.append(&mut request_entries);
753 }
754 }
755 }
756 }
757
758 self.entries.push(ListEntry::Header(Section::Contacts, 0));
759
760 request_entries.clear();
761 let incoming = user_store.incoming_contact_requests();
762 if !incoming.is_empty() {
763 self.match_candidates.clear();
764 self.match_candidates
765 .extend(
766 incoming
767 .iter()
768 .enumerate()
769 .map(|(ix, user)| StringMatchCandidate {
770 id: ix,
771 string: user.github_login.clone(),
772 char_bag: user.github_login.chars().collect(),
773 }),
774 );
775 let matches = executor.block(match_strings(
776 &self.match_candidates,
777 &query,
778 true,
779 usize::MAX,
780 &Default::default(),
781 executor.clone(),
782 ));
783 request_entries.extend(
784 matches
785 .iter()
786 .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
787 );
788 }
789
790 let outgoing = user_store.outgoing_contact_requests();
791 if !outgoing.is_empty() {
792 self.match_candidates.clear();
793 self.match_candidates
794 .extend(
795 outgoing
796 .iter()
797 .enumerate()
798 .map(|(ix, user)| StringMatchCandidate {
799 id: ix,
800 string: user.github_login.clone(),
801 char_bag: user.github_login.chars().collect(),
802 }),
803 );
804 let matches = executor.block(match_strings(
805 &self.match_candidates,
806 &query,
807 true,
808 usize::MAX,
809 &Default::default(),
810 executor.clone(),
811 ));
812 request_entries.extend(
813 matches
814 .iter()
815 .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
816 );
817 }
818
819 if !request_entries.is_empty() {
820 self.entries
821 .push(ListEntry::Header(Section::ContactRequests, 1));
822 if !self.collapsed_sections.contains(&Section::ContactRequests) {
823 self.entries.append(&mut request_entries);
824 }
825 }
826
827 let contacts = user_store.contacts();
828 if !contacts.is_empty() {
829 self.match_candidates.clear();
830 self.match_candidates
831 .extend(
832 contacts
833 .iter()
834 .enumerate()
835 .map(|(ix, contact)| StringMatchCandidate {
836 id: ix,
837 string: contact.user.github_login.clone(),
838 char_bag: contact.user.github_login.chars().collect(),
839 }),
840 );
841
842 let matches = executor.block(match_strings(
843 &self.match_candidates,
844 &query,
845 true,
846 usize::MAX,
847 &Default::default(),
848 executor.clone(),
849 ));
850
851 let (online_contacts, offline_contacts) = matches
852 .iter()
853 .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
854
855 for (matches, section) in [
856 (online_contacts, Section::Online),
857 (offline_contacts, Section::Offline),
858 ] {
859 if !matches.is_empty() {
860 self.entries.push(ListEntry::Header(section, 1));
861 if !self.collapsed_sections.contains(§ion) {
862 let active_call = &ActiveCall::global(cx).read(cx);
863 for mat in matches {
864 let contact = &contacts[mat.candidate_id];
865 self.entries.push(ListEntry::Contact {
866 contact: contact.clone(),
867 calling: active_call.pending_invites().contains(&contact.user.id),
868 });
869 }
870 }
871 }
872 }
873 }
874
875 if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
876 self.entries.push(ListEntry::ContactPlaceholder);
877 }
878
879 if select_same_item {
880 if let Some(prev_selected_entry) = prev_selected_entry {
881 self.selection.take();
882 for (ix, entry) in self.entries.iter().enumerate() {
883 if *entry == prev_selected_entry {
884 self.selection = Some(ix);
885 break;
886 }
887 }
888 }
889 } else {
890 self.selection = self.selection.and_then(|prev_selection| {
891 if self.entries.is_empty() {
892 None
893 } else {
894 Some(prev_selection.min(self.entries.len() - 1))
895 }
896 });
897 }
898
899 let old_scroll_top = self.list_state.logical_scroll_top();
900 self.list_state.reset(self.entries.len());
901
902 // Attempt to maintain the same scroll position.
903 if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
904 let new_scroll_top = self
905 .entries
906 .iter()
907 .position(|entry| entry == old_top_entry)
908 .map(|item_ix| ListOffset {
909 item_ix,
910 offset_in_item: old_scroll_top.offset_in_item,
911 })
912 .or_else(|| {
913 let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
914 let item_ix = self
915 .entries
916 .iter()
917 .position(|entry| entry == entry_after_old_top)?;
918 Some(ListOffset {
919 item_ix,
920 offset_in_item: 0.,
921 })
922 })
923 .or_else(|| {
924 let entry_before_old_top =
925 old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
926 let item_ix = self
927 .entries
928 .iter()
929 .position(|entry| entry == entry_before_old_top)?;
930 Some(ListOffset {
931 item_ix,
932 offset_in_item: 0.,
933 })
934 });
935
936 self.list_state
937 .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
938 }
939
940 cx.notify();
941 }
942
943 fn render_call_participant(
944 user: &User,
945 is_pending: bool,
946 is_selected: bool,
947 theme: &theme::CollabPanel,
948 ) -> AnyElement<Self> {
949 Flex::row()
950 .with_children(user.avatar.clone().map(|avatar| {
951 Image::from_data(avatar)
952 .with_style(theme.contact_avatar)
953 .aligned()
954 .left()
955 }))
956 .with_child(
957 Label::new(
958 user.github_login.clone(),
959 theme.contact_username.text.clone(),
960 )
961 .contained()
962 .with_style(theme.contact_username.container)
963 .aligned()
964 .left()
965 .flex(1., true),
966 )
967 .with_children(if is_pending {
968 Some(
969 Label::new("Calling", theme.calling_indicator.text.clone())
970 .contained()
971 .with_style(theme.calling_indicator.container)
972 .aligned(),
973 )
974 } else {
975 None
976 })
977 .constrained()
978 .with_height(theme.row_height)
979 .contained()
980 .with_style(
981 *theme
982 .contact_row
983 .in_state(is_selected)
984 .style_for(&mut Default::default()),
985 )
986 .into_any()
987 }
988
989 fn render_participant_project(
990 project_id: u64,
991 worktree_root_names: &[String],
992 host_user_id: u64,
993 is_current: bool,
994 is_last: bool,
995 is_selected: bool,
996 theme: &theme::CollabPanel,
997 cx: &mut ViewContext<Self>,
998 ) -> AnyElement<Self> {
999 enum JoinProject {}
1000
1001 let font_cache = cx.font_cache();
1002 let host_avatar_height = theme
1003 .contact_avatar
1004 .width
1005 .or(theme.contact_avatar.height)
1006 .unwrap_or(0.);
1007 let row = &theme.project_row.inactive_state().default;
1008 let tree_branch = theme.tree_branch;
1009 let line_height = row.name.text.line_height(font_cache);
1010 let cap_height = row.name.text.cap_height(font_cache);
1011 let baseline_offset =
1012 row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
1013 let project_name = if worktree_root_names.is_empty() {
1014 "untitled".to_string()
1015 } else {
1016 worktree_root_names.join(", ")
1017 };
1018
1019 MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, _| {
1020 let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
1021 let row = theme
1022 .project_row
1023 .in_state(is_selected)
1024 .style_for(mouse_state);
1025
1026 Flex::row()
1027 .with_child(
1028 Stack::new()
1029 .with_child(Canvas::new(move |scene, bounds, _, _, _| {
1030 let start_x =
1031 bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.);
1032 let end_x = bounds.max_x();
1033 let start_y = bounds.min_y();
1034 let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
1035
1036 scene.push_quad(gpui::Quad {
1037 bounds: RectF::from_points(
1038 vec2f(start_x, start_y),
1039 vec2f(
1040 start_x + tree_branch.width,
1041 if is_last { end_y } else { bounds.max_y() },
1042 ),
1043 ),
1044 background: Some(tree_branch.color),
1045 border: gpui::Border::default(),
1046 corner_radii: (0.).into(),
1047 });
1048 scene.push_quad(gpui::Quad {
1049 bounds: RectF::from_points(
1050 vec2f(start_x, end_y),
1051 vec2f(end_x, end_y + tree_branch.width),
1052 ),
1053 background: Some(tree_branch.color),
1054 border: gpui::Border::default(),
1055 corner_radii: (0.).into(),
1056 });
1057 }))
1058 .constrained()
1059 .with_width(host_avatar_height),
1060 )
1061 .with_child(
1062 Label::new(project_name, row.name.text.clone())
1063 .aligned()
1064 .left()
1065 .contained()
1066 .with_style(row.name.container)
1067 .flex(1., false),
1068 )
1069 .constrained()
1070 .with_height(theme.row_height)
1071 .contained()
1072 .with_style(row.container)
1073 })
1074 .with_cursor_style(if !is_current {
1075 CursorStyle::PointingHand
1076 } else {
1077 CursorStyle::Arrow
1078 })
1079 .on_click(MouseButton::Left, move |_, this, cx| {
1080 if !is_current {
1081 if let Some(workspace) = this.workspace.upgrade(cx) {
1082 let app_state = workspace.read(cx).app_state().clone();
1083 workspace::join_remote_project(project_id, host_user_id, app_state, cx)
1084 .detach_and_log_err(cx);
1085 }
1086 }
1087 })
1088 .into_any()
1089 }
1090
1091 fn render_participant_screen(
1092 peer_id: PeerId,
1093 is_last: bool,
1094 is_selected: bool,
1095 theme: &theme::CollabPanel,
1096 cx: &mut ViewContext<Self>,
1097 ) -> AnyElement<Self> {
1098 enum OpenSharedScreen {}
1099
1100 let font_cache = cx.font_cache();
1101 let host_avatar_height = theme
1102 .contact_avatar
1103 .width
1104 .or(theme.contact_avatar.height)
1105 .unwrap_or(0.);
1106 let row = &theme.project_row.inactive_state().default;
1107 let tree_branch = theme.tree_branch;
1108 let line_height = row.name.text.line_height(font_cache);
1109 let cap_height = row.name.text.cap_height(font_cache);
1110 let baseline_offset =
1111 row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
1112
1113 MouseEventHandler::new::<OpenSharedScreen, _>(
1114 peer_id.as_u64() as usize,
1115 cx,
1116 |mouse_state, _| {
1117 let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
1118 let row = theme
1119 .project_row
1120 .in_state(is_selected)
1121 .style_for(mouse_state);
1122
1123 Flex::row()
1124 .with_child(
1125 Stack::new()
1126 .with_child(Canvas::new(move |scene, bounds, _, _, _| {
1127 let start_x = bounds.min_x() + (bounds.width() / 2.)
1128 - (tree_branch.width / 2.);
1129 let end_x = bounds.max_x();
1130 let start_y = bounds.min_y();
1131 let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
1132
1133 scene.push_quad(gpui::Quad {
1134 bounds: RectF::from_points(
1135 vec2f(start_x, start_y),
1136 vec2f(
1137 start_x + tree_branch.width,
1138 if is_last { end_y } else { bounds.max_y() },
1139 ),
1140 ),
1141 background: Some(tree_branch.color),
1142 border: gpui::Border::default(),
1143 corner_radii: (0.).into(),
1144 });
1145 scene.push_quad(gpui::Quad {
1146 bounds: RectF::from_points(
1147 vec2f(start_x, end_y),
1148 vec2f(end_x, end_y + tree_branch.width),
1149 ),
1150 background: Some(tree_branch.color),
1151 border: gpui::Border::default(),
1152 corner_radii: (0.).into(),
1153 });
1154 }))
1155 .constrained()
1156 .with_width(host_avatar_height),
1157 )
1158 .with_child(
1159 Svg::new("icons/disable_screen_sharing_12.svg")
1160 .with_color(row.icon.color)
1161 .constrained()
1162 .with_width(row.icon.width)
1163 .aligned()
1164 .left()
1165 .contained()
1166 .with_style(row.icon.container),
1167 )
1168 .with_child(
1169 Label::new("Screen", row.name.text.clone())
1170 .aligned()
1171 .left()
1172 .contained()
1173 .with_style(row.name.container)
1174 .flex(1., false),
1175 )
1176 .constrained()
1177 .with_height(theme.row_height)
1178 .contained()
1179 .with_style(row.container)
1180 },
1181 )
1182 .with_cursor_style(CursorStyle::PointingHand)
1183 .on_click(MouseButton::Left, move |_, this, cx| {
1184 if let Some(workspace) = this.workspace.upgrade(cx) {
1185 workspace.update(cx, |workspace, cx| {
1186 workspace.open_shared_screen(peer_id, cx)
1187 });
1188 }
1189 })
1190 .into_any()
1191 }
1192
1193 fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
1194 if let Some(_) = self.channel_editing_state.take() {
1195 self.channel_name_editor.update(cx, |editor, cx| {
1196 editor.set_text("", cx);
1197 });
1198 true
1199 } else {
1200 false
1201 }
1202 }
1203
1204 fn render_header(
1205 &self,
1206 section: Section,
1207 theme: &theme::Theme,
1208 depth: usize,
1209 is_selected: bool,
1210 is_collapsed: bool,
1211 cx: &mut ViewContext<Self>,
1212 ) -> AnyElement<Self> {
1213 enum Header {}
1214 enum LeaveCallContactList {}
1215 enum AddChannel {}
1216
1217 let tooltip_style = &theme.tooltip;
1218 let text = match section {
1219 Section::ActiveCall => {
1220 let channel_name = iife!({
1221 let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
1222
1223 let name = self
1224 .channel_store
1225 .read(cx)
1226 .channel_for_id(channel_id)?
1227 .name
1228 .as_str();
1229
1230 Some(name)
1231 });
1232
1233 if let Some(name) = channel_name {
1234 Cow::Owned(format!("Current Call - #{}", name))
1235 } else {
1236 Cow::Borrowed("Current Call")
1237 }
1238 }
1239 Section::ContactRequests => Cow::Borrowed("Requests"),
1240 Section::Contacts => Cow::Borrowed("Contacts"),
1241 Section::Channels => Cow::Borrowed("Channels"),
1242 Section::ChannelInvites => Cow::Borrowed("Invites"),
1243 Section::Online => Cow::Borrowed("Online"),
1244 Section::Offline => Cow::Borrowed("Offline"),
1245 };
1246
1247 enum AddContact {}
1248 let button = match section {
1249 Section::ActiveCall => Some(
1250 MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
1251 render_icon_button(
1252 theme
1253 .collab_panel
1254 .leave_call_button
1255 .style_for(is_selected, state),
1256 "icons/exit.svg",
1257 )
1258 })
1259 .with_cursor_style(CursorStyle::PointingHand)
1260 .on_click(MouseButton::Left, |_, _, cx| {
1261 Self::leave_call(cx);
1262 })
1263 .with_tooltip::<AddContact>(
1264 0,
1265 "Leave call",
1266 None,
1267 tooltip_style.clone(),
1268 cx,
1269 ),
1270 ),
1271 Section::Contacts => Some(
1272 MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
1273 render_icon_button(
1274 theme
1275 .collab_panel
1276 .add_contact_button
1277 .style_for(is_selected, state),
1278 "icons/plus_16.svg",
1279 )
1280 })
1281 .with_cursor_style(CursorStyle::PointingHand)
1282 .on_click(MouseButton::Left, |_, this, cx| {
1283 this.toggle_contact_finder(cx);
1284 })
1285 .with_tooltip::<LeaveCallContactList>(
1286 0,
1287 "Search for new contact",
1288 None,
1289 tooltip_style.clone(),
1290 cx,
1291 ),
1292 ),
1293 Section::Channels => Some(
1294 MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| {
1295 render_icon_button(
1296 theme
1297 .collab_panel
1298 .add_contact_button
1299 .style_for(is_selected, state),
1300 "icons/plus.svg",
1301 )
1302 })
1303 .with_cursor_style(CursorStyle::PointingHand)
1304 .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx))
1305 .with_tooltip::<AddChannel>(
1306 0,
1307 "Create a channel",
1308 None,
1309 tooltip_style.clone(),
1310 cx,
1311 ),
1312 ),
1313 _ => None,
1314 };
1315
1316 let can_collapse = depth > 0;
1317 let icon_size = (&theme.collab_panel).section_icon_size;
1318 let mut result = MouseEventHandler::new::<Header, _>(section as usize, cx, |state, _| {
1319 let header_style = if can_collapse {
1320 theme
1321 .collab_panel
1322 .subheader_row
1323 .in_state(is_selected)
1324 .style_for(state)
1325 } else {
1326 &theme.collab_panel.header_row
1327 };
1328
1329 Flex::row()
1330 .with_children(if can_collapse {
1331 Some(
1332 Svg::new(if is_collapsed {
1333 "icons/chevron_right.svg"
1334 } else {
1335 "icons/chevron_down.svg"
1336 })
1337 .with_color(header_style.text.color)
1338 .constrained()
1339 .with_max_width(icon_size)
1340 .with_max_height(icon_size)
1341 .aligned()
1342 .constrained()
1343 .with_width(icon_size)
1344 .contained()
1345 .with_margin_right(
1346 theme.collab_panel.contact_username.container.margin.left,
1347 ),
1348 )
1349 } else {
1350 None
1351 })
1352 .with_child(
1353 Label::new(text, header_style.text.clone())
1354 .aligned()
1355 .left()
1356 .flex(1., true),
1357 )
1358 .with_children(button.map(|button| button.aligned().right()))
1359 .constrained()
1360 .with_height(theme.collab_panel.row_height)
1361 .contained()
1362 .with_style(header_style.container)
1363 });
1364
1365 if can_collapse {
1366 result = result
1367 .with_cursor_style(CursorStyle::PointingHand)
1368 .on_click(MouseButton::Left, move |_, this, cx| {
1369 if can_collapse {
1370 this.toggle_section_expanded(section, cx);
1371 }
1372 })
1373 }
1374
1375 result.into_any()
1376 }
1377
1378 fn render_contact(
1379 contact: &Contact,
1380 calling: bool,
1381 project: &ModelHandle<Project>,
1382 theme: &theme::CollabPanel,
1383 is_selected: bool,
1384 cx: &mut ViewContext<Self>,
1385 ) -> AnyElement<Self> {
1386 let online = contact.online;
1387 let busy = contact.busy || calling;
1388 let user_id = contact.user.id;
1389 let github_login = contact.user.github_login.clone();
1390 let initial_project = project.clone();
1391 let mut event_handler =
1392 MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| {
1393 Flex::row()
1394 .with_children(contact.user.avatar.clone().map(|avatar| {
1395 let status_badge = if contact.online {
1396 Some(
1397 Empty::new()
1398 .collapsed()
1399 .contained()
1400 .with_style(if busy {
1401 theme.contact_status_busy
1402 } else {
1403 theme.contact_status_free
1404 })
1405 .aligned(),
1406 )
1407 } else {
1408 None
1409 };
1410 Stack::new()
1411 .with_child(
1412 Image::from_data(avatar)
1413 .with_style(theme.contact_avatar)
1414 .aligned()
1415 .left(),
1416 )
1417 .with_children(status_badge)
1418 }))
1419 .with_child(
1420 Label::new(
1421 contact.user.github_login.clone(),
1422 theme.contact_username.text.clone(),
1423 )
1424 .contained()
1425 .with_style(theme.contact_username.container)
1426 .aligned()
1427 .left()
1428 .flex(1., true),
1429 )
1430 .with_child(
1431 MouseEventHandler::new::<Cancel, _>(
1432 contact.user.id as usize,
1433 cx,
1434 |mouse_state, _| {
1435 let button_style = theme.contact_button.style_for(mouse_state);
1436 render_icon_button(button_style, "icons/x.svg")
1437 .aligned()
1438 .flex_float()
1439 },
1440 )
1441 .with_padding(Padding::uniform(2.))
1442 .with_cursor_style(CursorStyle::PointingHand)
1443 .on_click(MouseButton::Left, move |_, this, cx| {
1444 this.remove_contact(user_id, &github_login, cx);
1445 })
1446 .flex_float(),
1447 )
1448 .with_children(if calling {
1449 Some(
1450 Label::new("Calling", theme.calling_indicator.text.clone())
1451 .contained()
1452 .with_style(theme.calling_indicator.container)
1453 .aligned(),
1454 )
1455 } else {
1456 None
1457 })
1458 .constrained()
1459 .with_height(theme.row_height)
1460 .contained()
1461 .with_style(*theme.contact_row.in_state(is_selected).style_for(state))
1462 })
1463 .on_click(MouseButton::Left, move |_, this, cx| {
1464 if online && !busy {
1465 this.call(user_id, Some(initial_project.clone()), cx);
1466 }
1467 });
1468
1469 if online {
1470 event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand);
1471 }
1472
1473 event_handler.into_any()
1474 }
1475
1476 fn render_contact_placeholder(
1477 &self,
1478 theme: &theme::CollabPanel,
1479 is_selected: bool,
1480 cx: &mut ViewContext<Self>,
1481 ) -> AnyElement<Self> {
1482 enum AddContacts {}
1483 MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
1484 let style = theme.list_empty_state.style_for(is_selected, state);
1485 Flex::row()
1486 .with_child(
1487 Svg::new("icons/plus.svg")
1488 .with_color(theme.list_empty_icon.color)
1489 .constrained()
1490 .with_width(theme.list_empty_icon.width)
1491 .aligned()
1492 .left(),
1493 )
1494 .with_child(
1495 Label::new("Add a contact", style.text.clone())
1496 .contained()
1497 .with_style(theme.list_empty_label_container),
1498 )
1499 .align_children_center()
1500 .contained()
1501 .with_style(style.container)
1502 .into_any()
1503 })
1504 .on_click(MouseButton::Left, |_, this, cx| {
1505 this.toggle_contact_finder(cx);
1506 })
1507 .into_any()
1508 }
1509
1510 fn render_channel_editor(
1511 &self,
1512 theme: &theme::Theme,
1513 depth: usize,
1514 cx: &AppContext,
1515 ) -> AnyElement<Self> {
1516 Flex::row()
1517 .with_child(
1518 Empty::new()
1519 .constrained()
1520 .with_width(theme.collab_panel.disclosure.button_space()),
1521 )
1522 .with_child(
1523 Svg::new("icons/hash.svg")
1524 .with_color(theme.collab_panel.channel_hash.color)
1525 .constrained()
1526 .with_width(theme.collab_panel.channel_hash.width)
1527 .aligned()
1528 .left(),
1529 )
1530 .with_child(
1531 if let Some(pending_name) = self
1532 .channel_editing_state
1533 .as_ref()
1534 .and_then(|state| state.pending_name())
1535 {
1536 Label::new(
1537 pending_name.to_string(),
1538 theme.collab_panel.contact_username.text.clone(),
1539 )
1540 .contained()
1541 .with_style(theme.collab_panel.contact_username.container)
1542 .aligned()
1543 .left()
1544 .flex(1., true)
1545 .into_any()
1546 } else {
1547 ChildView::new(&self.channel_name_editor, cx)
1548 .aligned()
1549 .left()
1550 .contained()
1551 .with_style(theme.collab_panel.channel_editor)
1552 .flex(1.0, true)
1553 .into_any()
1554 },
1555 )
1556 .align_children_center()
1557 .constrained()
1558 .with_height(theme.collab_panel.row_height)
1559 .contained()
1560 .with_style(gpui::elements::ContainerStyle {
1561 background_color: Some(theme.editor.background),
1562 ..*theme.collab_panel.contact_row.default_style()
1563 })
1564 .with_padding_left(
1565 theme.collab_panel.contact_row.default_style().padding.left
1566 + theme.collab_panel.channel_indent * depth as f32,
1567 )
1568 .into_any()
1569 }
1570
1571 fn render_channel(
1572 &self,
1573 channel: &Channel,
1574 depth: usize,
1575 theme: &theme::CollabPanel,
1576 is_selected: bool,
1577 cx: &mut ViewContext<Self>,
1578 ) -> AnyElement<Self> {
1579 let channel_id = channel.id;
1580 let has_children = self.channel_store.read(cx).has_children(channel_id);
1581 let disclosed =
1582 has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok());
1583
1584 let is_active = iife!({
1585 let call_channel = ActiveCall::global(cx)
1586 .read(cx)
1587 .room()?
1588 .read(cx)
1589 .channel_id()?;
1590 Some(call_channel == channel_id)
1591 })
1592 .unwrap_or(false);
1593
1594 const FACEPILE_LIMIT: usize = 3;
1595
1596 MouseEventHandler::new::<Channel, _>(channel.id as usize, cx, |state, cx| {
1597 Flex::<Self>::row()
1598 .with_child(
1599 Svg::new("icons/hash.svg")
1600 .with_color(theme.channel_hash.color)
1601 .constrained()
1602 .with_width(theme.channel_hash.width)
1603 .aligned()
1604 .left(),
1605 )
1606 .with_child(
1607 Label::new(channel.name.clone(), theme.channel_name.text.clone())
1608 .contained()
1609 .with_style(theme.channel_name.container)
1610 .aligned()
1611 .left()
1612 .flex(1., true),
1613 )
1614 .with_children({
1615 let participants = self.channel_store.read(cx).channel_participants(channel_id);
1616 if !participants.is_empty() {
1617 let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
1618
1619 Some(
1620 FacePile::new(theme.face_overlap)
1621 .with_children(
1622 participants
1623 .iter()
1624 .filter_map(|user| {
1625 Some(
1626 Image::from_data(user.avatar.clone()?)
1627 .with_style(theme.channel_avatar),
1628 )
1629 })
1630 .take(FACEPILE_LIMIT),
1631 )
1632 .with_children((extra_count > 0).then(|| {
1633 Label::new(
1634 format!("+{}", extra_count),
1635 theme.extra_participant_label.text.clone(),
1636 )
1637 .contained()
1638 .with_style(theme.extra_participant_label.container)
1639 })),
1640 )
1641 } else {
1642 None
1643 }
1644 })
1645 .align_children_center()
1646 .styleable_component()
1647 .disclosable(disclosed, Box::new(ToggleCollapse { channel_id }))
1648 .with_id(channel_id as usize)
1649 .with_style(theme.disclosure.clone())
1650 .element()
1651 .constrained()
1652 .with_height(theme.row_height)
1653 .contained()
1654 .with_style(*theme.channel_row.style_for(is_selected || is_active, state))
1655 .with_padding_left(
1656 theme.channel_row.default_style().padding.left
1657 + theme.channel_indent * depth as f32,
1658 )
1659 })
1660 .on_click(MouseButton::Left, move |_, this, cx| {
1661 this.join_channel(channel_id, cx);
1662 })
1663 .on_click(MouseButton::Right, move |e, this, cx| {
1664 this.deploy_channel_context_menu(Some(e.position), channel_id, cx);
1665 })
1666 .with_cursor_style(CursorStyle::PointingHand)
1667 .into_any()
1668 }
1669
1670 fn render_channel_invite(
1671 channel: Arc<Channel>,
1672 channel_store: ModelHandle<ChannelStore>,
1673 theme: &theme::CollabPanel,
1674 is_selected: bool,
1675 cx: &mut ViewContext<Self>,
1676 ) -> AnyElement<Self> {
1677 enum Decline {}
1678 enum Accept {}
1679
1680 let channel_id = channel.id;
1681 let is_invite_pending = channel_store
1682 .read(cx)
1683 .has_pending_channel_invite_response(&channel);
1684 let button_spacing = theme.contact_button_spacing;
1685
1686 Flex::row()
1687 .with_child(
1688 Svg::new("icons/hash.svg")
1689 .with_color(theme.channel_hash.color)
1690 .constrained()
1691 .with_width(theme.channel_hash.width)
1692 .aligned()
1693 .left(),
1694 )
1695 .with_child(
1696 Label::new(channel.name.clone(), theme.contact_username.text.clone())
1697 .contained()
1698 .with_style(theme.contact_username.container)
1699 .aligned()
1700 .left()
1701 .flex(1., true),
1702 )
1703 .with_child(
1704 MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
1705 let button_style = if is_invite_pending {
1706 &theme.disabled_button
1707 } else {
1708 theme.contact_button.style_for(mouse_state)
1709 };
1710 render_icon_button(button_style, "icons/x.svg").aligned()
1711 })
1712 .with_cursor_style(CursorStyle::PointingHand)
1713 .on_click(MouseButton::Left, move |_, this, cx| {
1714 this.respond_to_channel_invite(channel_id, false, cx);
1715 })
1716 .contained()
1717 .with_margin_right(button_spacing),
1718 )
1719 .with_child(
1720 MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
1721 let button_style = if is_invite_pending {
1722 &theme.disabled_button
1723 } else {
1724 theme.contact_button.style_for(mouse_state)
1725 };
1726 render_icon_button(button_style, "icons/check.svg")
1727 .aligned()
1728 .flex_float()
1729 })
1730 .with_cursor_style(CursorStyle::PointingHand)
1731 .on_click(MouseButton::Left, move |_, this, cx| {
1732 this.respond_to_channel_invite(channel_id, true, cx);
1733 }),
1734 )
1735 .constrained()
1736 .with_height(theme.row_height)
1737 .contained()
1738 .with_style(
1739 *theme
1740 .contact_row
1741 .in_state(is_selected)
1742 .style_for(&mut Default::default()),
1743 )
1744 .with_padding_left(
1745 theme.contact_row.default_style().padding.left + theme.channel_indent,
1746 )
1747 .into_any()
1748 }
1749
1750 fn render_contact_request(
1751 user: Arc<User>,
1752 user_store: ModelHandle<UserStore>,
1753 theme: &theme::CollabPanel,
1754 is_incoming: bool,
1755 is_selected: bool,
1756 cx: &mut ViewContext<Self>,
1757 ) -> AnyElement<Self> {
1758 enum Decline {}
1759 enum Accept {}
1760 enum Cancel {}
1761
1762 let mut row = Flex::row()
1763 .with_children(user.avatar.clone().map(|avatar| {
1764 Image::from_data(avatar)
1765 .with_style(theme.contact_avatar)
1766 .aligned()
1767 .left()
1768 }))
1769 .with_child(
1770 Label::new(
1771 user.github_login.clone(),
1772 theme.contact_username.text.clone(),
1773 )
1774 .contained()
1775 .with_style(theme.contact_username.container)
1776 .aligned()
1777 .left()
1778 .flex(1., true),
1779 );
1780
1781 let user_id = user.id;
1782 let github_login = user.github_login.clone();
1783 let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
1784 let button_spacing = theme.contact_button_spacing;
1785
1786 if is_incoming {
1787 row.add_child(
1788 MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| {
1789 let button_style = if is_contact_request_pending {
1790 &theme.disabled_button
1791 } else {
1792 theme.contact_button.style_for(mouse_state)
1793 };
1794 render_icon_button(button_style, "icons/x.svg").aligned()
1795 })
1796 .with_cursor_style(CursorStyle::PointingHand)
1797 .on_click(MouseButton::Left, move |_, this, cx| {
1798 this.respond_to_contact_request(user_id, false, cx);
1799 })
1800 .contained()
1801 .with_margin_right(button_spacing),
1802 );
1803
1804 row.add_child(
1805 MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| {
1806 let button_style = if is_contact_request_pending {
1807 &theme.disabled_button
1808 } else {
1809 theme.contact_button.style_for(mouse_state)
1810 };
1811 render_icon_button(button_style, "icons/check.svg")
1812 .aligned()
1813 .flex_float()
1814 })
1815 .with_cursor_style(CursorStyle::PointingHand)
1816 .on_click(MouseButton::Left, move |_, this, cx| {
1817 this.respond_to_contact_request(user_id, true, cx);
1818 }),
1819 );
1820 } else {
1821 row.add_child(
1822 MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| {
1823 let button_style = if is_contact_request_pending {
1824 &theme.disabled_button
1825 } else {
1826 theme.contact_button.style_for(mouse_state)
1827 };
1828 render_icon_button(button_style, "icons/x.svg")
1829 .aligned()
1830 .flex_float()
1831 })
1832 .with_padding(Padding::uniform(2.))
1833 .with_cursor_style(CursorStyle::PointingHand)
1834 .on_click(MouseButton::Left, move |_, this, cx| {
1835 this.remove_contact(user_id, &github_login, cx);
1836 })
1837 .flex_float(),
1838 );
1839 }
1840
1841 row.constrained()
1842 .with_height(theme.row_height)
1843 .contained()
1844 .with_style(
1845 *theme
1846 .contact_row
1847 .in_state(is_selected)
1848 .style_for(&mut Default::default()),
1849 )
1850 .into_any()
1851 }
1852
1853 fn include_channels_section(&self, cx: &AppContext) -> bool {
1854 if cx.has_global::<StaffMode>() {
1855 cx.global::<StaffMode>().0
1856 } else {
1857 false
1858 }
1859 }
1860
1861 fn deploy_channel_context_menu(
1862 &mut self,
1863 position: Option<Vector2F>,
1864 channel_id: u64,
1865 cx: &mut ViewContext<Self>,
1866 ) {
1867 if self.channel_store.read(cx).is_user_admin(channel_id) {
1868 self.context_menu_on_selected = position.is_none();
1869
1870 self.context_menu.update(cx, |context_menu, cx| {
1871 context_menu.set_position_mode(if self.context_menu_on_selected {
1872 OverlayPositionMode::Local
1873 } else {
1874 OverlayPositionMode::Window
1875 });
1876
1877 let expand_action_name = if self.is_channel_collapsed(channel_id) {
1878 "Expand Subchannels"
1879 } else {
1880 "Collapse Subchannels"
1881 };
1882
1883 context_menu.show(
1884 position.unwrap_or_default(),
1885 if self.context_menu_on_selected {
1886 gpui::elements::AnchorCorner::TopRight
1887 } else {
1888 gpui::elements::AnchorCorner::BottomLeft
1889 },
1890 vec![
1891 ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }),
1892 ContextMenuItem::action("New Subchannel", NewChannel { channel_id }),
1893 ContextMenuItem::Separator,
1894 ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }),
1895 ContextMenuItem::Separator,
1896 ContextMenuItem::action("Rename", RenameChannel { channel_id }),
1897 ContextMenuItem::action("Manage", ManageMembers { channel_id }),
1898 ContextMenuItem::Separator,
1899 ContextMenuItem::action("Delete", RemoveChannel { channel_id }),
1900 ],
1901 cx,
1902 );
1903 });
1904
1905 cx.notify();
1906 }
1907 }
1908
1909 fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
1910 if self.take_editing_state(cx) {
1911 cx.focus(&self.filter_editor);
1912 } else {
1913 self.filter_editor.update(cx, |editor, cx| {
1914 if editor.buffer().read(cx).len(cx) > 0 {
1915 editor.set_text("", cx);
1916 }
1917 });
1918 }
1919
1920 self.update_entries(false, cx);
1921 }
1922
1923 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
1924 let ix = self.selection.map_or(0, |ix| ix + 1);
1925 if ix < self.entries.len() {
1926 self.selection = Some(ix);
1927 }
1928
1929 self.list_state.reset(self.entries.len());
1930 if let Some(ix) = self.selection {
1931 self.list_state.scroll_to(ListOffset {
1932 item_ix: ix,
1933 offset_in_item: 0.,
1934 });
1935 }
1936 cx.notify();
1937 }
1938
1939 fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
1940 let ix = self.selection.take().unwrap_or(0);
1941 if ix > 0 {
1942 self.selection = Some(ix - 1);
1943 }
1944
1945 self.list_state.reset(self.entries.len());
1946 if let Some(ix) = self.selection {
1947 self.list_state.scroll_to(ListOffset {
1948 item_ix: ix,
1949 offset_in_item: 0.,
1950 });
1951 }
1952 cx.notify();
1953 }
1954
1955 fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
1956 if self.confirm_channel_edit(cx) {
1957 return;
1958 }
1959
1960 if let Some(selection) = self.selection {
1961 if let Some(entry) = self.entries.get(selection) {
1962 match entry {
1963 ListEntry::Header(section, _) => match section {
1964 Section::ActiveCall => Self::leave_call(cx),
1965 Section::Channels => self.new_root_channel(cx),
1966 Section::Contacts => self.toggle_contact_finder(cx),
1967 Section::ContactRequests
1968 | Section::Online
1969 | Section::Offline
1970 | Section::ChannelInvites => {
1971 self.toggle_section_expanded(*section, cx);
1972 }
1973 },
1974 ListEntry::Contact { contact, calling } => {
1975 if contact.online && !contact.busy && !calling {
1976 self.call(contact.user.id, Some(self.project.clone()), cx);
1977 }
1978 }
1979 ListEntry::ParticipantProject {
1980 project_id,
1981 host_user_id,
1982 ..
1983 } => {
1984 if let Some(workspace) = self.workspace.upgrade(cx) {
1985 let app_state = workspace.read(cx).app_state().clone();
1986 workspace::join_remote_project(
1987 *project_id,
1988 *host_user_id,
1989 app_state,
1990 cx,
1991 )
1992 .detach_and_log_err(cx);
1993 }
1994 }
1995 ListEntry::ParticipantScreen { peer_id, .. } => {
1996 if let Some(workspace) = self.workspace.upgrade(cx) {
1997 workspace.update(cx, |workspace, cx| {
1998 workspace.open_shared_screen(*peer_id, cx)
1999 });
2000 }
2001 }
2002 ListEntry::Channel { channel, .. } => {
2003 self.join_channel(channel.id, cx);
2004 }
2005 ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
2006 _ => {}
2007 }
2008 }
2009 }
2010 }
2011
2012 fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
2013 if let Some(editing_state) = &mut self.channel_editing_state {
2014 match editing_state {
2015 ChannelEditingState::Create {
2016 parent_id,
2017 pending_name,
2018 ..
2019 } => {
2020 if pending_name.is_some() {
2021 return false;
2022 }
2023 let channel_name = self.channel_name_editor.read(cx).text(cx);
2024
2025 *pending_name = Some(channel_name.clone());
2026
2027 self.channel_store
2028 .update(cx, |channel_store, cx| {
2029 channel_store.create_channel(&channel_name, *parent_id, cx)
2030 })
2031 .detach();
2032 cx.notify();
2033 }
2034 ChannelEditingState::Rename {
2035 channel_id,
2036 pending_name,
2037 } => {
2038 if pending_name.is_some() {
2039 return false;
2040 }
2041 let channel_name = self.channel_name_editor.read(cx).text(cx);
2042 *pending_name = Some(channel_name.clone());
2043
2044 self.channel_store
2045 .update(cx, |channel_store, cx| {
2046 channel_store.rename(*channel_id, &channel_name, cx)
2047 })
2048 .detach();
2049 cx.notify();
2050 }
2051 }
2052 cx.focus_self();
2053 true
2054 } else {
2055 false
2056 }
2057 }
2058
2059 fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
2060 if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
2061 self.collapsed_sections.remove(ix);
2062 } else {
2063 self.collapsed_sections.push(section);
2064 }
2065 self.update_entries(false, cx);
2066 }
2067
2068 fn collapse_selected_channel(
2069 &mut self,
2070 _: &CollapseSelectedChannel,
2071 cx: &mut ViewContext<Self>,
2072 ) {
2073 let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
2074 return;
2075 };
2076
2077 if self.is_channel_collapsed(channel_id) {
2078 return;
2079 }
2080
2081 self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
2082 }
2083
2084 fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
2085 let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
2086 return;
2087 };
2088
2089 if !self.is_channel_collapsed(channel_id) {
2090 return;
2091 }
2092
2093 self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
2094 }
2095
2096 fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext<Self>) {
2097 let channel_id = action.channel_id;
2098
2099 match self.collapsed_channels.binary_search(&channel_id) {
2100 Ok(ix) => {
2101 self.collapsed_channels.remove(ix);
2102 }
2103 Err(ix) => {
2104 self.collapsed_channels.insert(ix, channel_id);
2105 }
2106 };
2107 self.update_entries(true, cx);
2108 cx.notify();
2109 cx.focus_self();
2110 }
2111
2112 fn is_channel_collapsed(&self, channel: ChannelId) -> bool {
2113 self.collapsed_channels.binary_search(&channel).is_ok()
2114 }
2115
2116 fn leave_call(cx: &mut ViewContext<Self>) {
2117 ActiveCall::global(cx)
2118 .update(cx, |call, cx| call.hang_up(cx))
2119 .detach_and_log_err(cx);
2120 }
2121
2122 fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
2123 if let Some(workspace) = self.workspace.upgrade(cx) {
2124 workspace.update(cx, |workspace, cx| {
2125 workspace.toggle_modal(cx, |_, cx| {
2126 cx.add_view(|cx| {
2127 let mut finder = ContactFinder::new(self.user_store.clone(), cx);
2128 finder.set_query(self.filter_editor.read(cx).text(cx), cx);
2129 finder
2130 })
2131 });
2132 });
2133 }
2134 }
2135
2136 fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
2137 self.channel_editing_state = Some(ChannelEditingState::Create {
2138 parent_id: None,
2139 pending_name: None,
2140 });
2141 self.update_entries(false, cx);
2142 self.select_channel_editor();
2143 cx.focus(self.channel_name_editor.as_any());
2144 cx.notify();
2145 }
2146
2147 fn select_channel_editor(&mut self) {
2148 self.selection = self.entries.iter().position(|entry| match entry {
2149 ListEntry::ChannelEditor { .. } => true,
2150 _ => false,
2151 });
2152 }
2153
2154 fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
2155 self.collapsed_channels
2156 .retain(|&channel| channel != action.channel_id);
2157 self.channel_editing_state = Some(ChannelEditingState::Create {
2158 parent_id: Some(action.channel_id),
2159 pending_name: None,
2160 });
2161 self.update_entries(false, cx);
2162 self.select_channel_editor();
2163 cx.focus(self.channel_name_editor.as_any());
2164 cx.notify();
2165 }
2166
2167 fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext<Self>) {
2168 self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx);
2169 }
2170
2171 fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext<Self>) {
2172 self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx);
2173 }
2174
2175 fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
2176 if let Some(channel) = self.selected_channel() {
2177 self.remove_channel(channel.id, cx)
2178 }
2179 }
2180
2181 fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
2182 if let Some(channel) = self.selected_channel() {
2183 self.rename_channel(
2184 &RenameChannel {
2185 channel_id: channel.id,
2186 },
2187 cx,
2188 );
2189 }
2190 }
2191
2192 fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
2193 let channel_store = self.channel_store.read(cx);
2194 if !channel_store.is_user_admin(action.channel_id) {
2195 return;
2196 }
2197 if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() {
2198 self.channel_editing_state = Some(ChannelEditingState::Rename {
2199 channel_id: action.channel_id,
2200 pending_name: None,
2201 });
2202 self.channel_name_editor.update(cx, |editor, cx| {
2203 editor.set_text(channel.name.clone(), cx);
2204 editor.select_all(&Default::default(), cx);
2205 });
2206 cx.focus(self.channel_name_editor.as_any());
2207 self.update_entries(false, cx);
2208 self.select_channel_editor();
2209 }
2210 }
2211
2212 fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
2213 let Some(channel) = self.selected_channel() else {
2214 return;
2215 };
2216
2217 self.deploy_channel_context_menu(None, channel.id, cx);
2218 }
2219
2220 fn selected_channel(&self) -> Option<&Arc<Channel>> {
2221 self.selection
2222 .and_then(|ix| self.entries.get(ix))
2223 .and_then(|entry| match entry {
2224 ListEntry::Channel { channel, .. } => Some(channel),
2225 _ => None,
2226 })
2227 }
2228
2229 fn show_channel_modal(
2230 &mut self,
2231 channel_id: ChannelId,
2232 mode: channel_modal::Mode,
2233 cx: &mut ViewContext<Self>,
2234 ) {
2235 let workspace = self.workspace.clone();
2236 let user_store = self.user_store.clone();
2237 let channel_store = self.channel_store.clone();
2238 let members = self.channel_store.update(cx, |channel_store, cx| {
2239 channel_store.get_channel_member_details(channel_id, cx)
2240 });
2241
2242 cx.spawn(|_, mut cx| async move {
2243 let members = members.await?;
2244 workspace.update(&mut cx, |workspace, cx| {
2245 workspace.toggle_modal(cx, |_, cx| {
2246 cx.add_view(|cx| {
2247 ChannelModal::new(
2248 user_store.clone(),
2249 channel_store.clone(),
2250 channel_id,
2251 mode,
2252 members,
2253 cx,
2254 )
2255 })
2256 });
2257 })
2258 })
2259 .detach();
2260 }
2261
2262 fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
2263 self.remove_channel(action.channel_id, cx)
2264 }
2265
2266 fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2267 let channel_store = self.channel_store.clone();
2268 if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
2269 let prompt_message = format!(
2270 "Are you sure you want to remove the channel \"{}\"?",
2271 channel.name
2272 );
2273 let mut answer =
2274 cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
2275 let window = cx.window();
2276 cx.spawn(|this, mut cx| async move {
2277 if answer.next().await == Some(0) {
2278 if let Err(e) = channel_store
2279 .update(&mut cx, |channels, _| channels.remove_channel(channel_id))
2280 .await
2281 {
2282 window.prompt(
2283 PromptLevel::Info,
2284 &format!("Failed to remove channel: {}", e),
2285 &["Ok"],
2286 &mut cx,
2287 );
2288 }
2289 this.update(&mut cx, |_, cx| cx.focus_self()).ok();
2290 }
2291 })
2292 .detach();
2293 }
2294 }
2295
2296 // Should move to the filter editor if clicking on it
2297 // Should move selection to the channel editor if activating it
2298
2299 fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
2300 let user_store = self.user_store.clone();
2301 let prompt_message = format!(
2302 "Are you sure you want to remove \"{}\" from your contacts?",
2303 github_login
2304 );
2305 let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
2306 let window = cx.window();
2307 cx.spawn(|_, mut cx| async move {
2308 if answer.next().await == Some(0) {
2309 if let Err(e) = user_store
2310 .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
2311 .await
2312 {
2313 window.prompt(
2314 PromptLevel::Info,
2315 &format!("Failed to remove contact: {}", e),
2316 &["Ok"],
2317 &mut cx,
2318 );
2319 }
2320 }
2321 })
2322 .detach();
2323 }
2324
2325 fn respond_to_contact_request(
2326 &mut self,
2327 user_id: u64,
2328 accept: bool,
2329 cx: &mut ViewContext<Self>,
2330 ) {
2331 self.user_store
2332 .update(cx, |store, cx| {
2333 store.respond_to_contact_request(user_id, accept, cx)
2334 })
2335 .detach();
2336 }
2337
2338 fn respond_to_channel_invite(
2339 &mut self,
2340 channel_id: u64,
2341 accept: bool,
2342 cx: &mut ViewContext<Self>,
2343 ) {
2344 let respond = self.channel_store.update(cx, |store, _| {
2345 store.respond_to_channel_invite(channel_id, accept)
2346 });
2347 cx.foreground().spawn(respond).detach();
2348 }
2349
2350 fn call(
2351 &mut self,
2352 recipient_user_id: u64,
2353 initial_project: Option<ModelHandle<Project>>,
2354 cx: &mut ViewContext<Self>,
2355 ) {
2356 ActiveCall::global(cx)
2357 .update(cx, |call, cx| {
2358 call.invite(recipient_user_id, initial_project, cx)
2359 })
2360 .detach_and_log_err(cx);
2361 }
2362
2363 fn join_channel(&self, channel: u64, cx: &mut ViewContext<Self>) {
2364 ActiveCall::global(cx)
2365 .update(cx, |call, cx| call.join_channel(channel, cx))
2366 .detach_and_log_err(cx);
2367 }
2368}
2369
2370impl View for CollabPanel {
2371 fn ui_name() -> &'static str {
2372 "CollabPanel"
2373 }
2374
2375 fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
2376 if !self.has_focus {
2377 self.has_focus = true;
2378 if !self.context_menu.is_focused(cx) {
2379 if let Some(editing_state) = &self.channel_editing_state {
2380 if editing_state.pending_name().is_none() {
2381 cx.focus(&self.channel_name_editor);
2382 } else {
2383 cx.focus(&self.filter_editor);
2384 }
2385 } else {
2386 cx.focus(&self.filter_editor);
2387 }
2388 }
2389 cx.emit(Event::Focus);
2390 }
2391 }
2392
2393 fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
2394 self.has_focus = false;
2395 }
2396
2397 fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
2398 let theme = &theme::current(cx).collab_panel;
2399
2400 if self.user_store.read(cx).current_user().is_none() {
2401 enum LogInButton {}
2402
2403 return Flex::column()
2404 .with_child(
2405 MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
2406 let button = theme.log_in_button.style_for(state);
2407 Label::new("Sign in to collaborate", button.text.clone())
2408 .aligned()
2409 .left()
2410 .contained()
2411 .with_style(button.container)
2412 })
2413 .on_click(MouseButton::Left, |_, this, cx| {
2414 let client = this.client.clone();
2415 cx.spawn(|_, cx| async move {
2416 client.authenticate_and_connect(true, &cx).await.log_err();
2417 })
2418 .detach();
2419 })
2420 .with_cursor_style(CursorStyle::PointingHand),
2421 )
2422 .contained()
2423 .with_style(theme.container)
2424 .into_any();
2425 }
2426
2427 enum PanelFocus {}
2428 MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
2429 Stack::new()
2430 .with_child(
2431 Flex::column()
2432 .with_child(
2433 Flex::row()
2434 .with_child(
2435 ChildView::new(&self.filter_editor, cx)
2436 .contained()
2437 .with_style(theme.user_query_editor.container)
2438 .flex(1.0, true),
2439 )
2440 .constrained()
2441 .with_width(self.size(cx)),
2442 )
2443 .with_child(
2444 List::new(self.list_state.clone())
2445 .constrained()
2446 .with_width(self.size(cx))
2447 .flex(1., true)
2448 .into_any(),
2449 )
2450 .contained()
2451 .with_style(theme.container)
2452 .constrained()
2453 .with_width(self.size(cx))
2454 .into_any(),
2455 )
2456 .with_children(
2457 (!self.context_menu_on_selected)
2458 .then(|| ChildView::new(&self.context_menu, cx)),
2459 )
2460 .into_any()
2461 })
2462 .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
2463 .into_any_named("collab panel")
2464 }
2465}
2466
2467impl Panel for CollabPanel {
2468 fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
2469 match settings::get::<CollaborationPanelSettings>(cx).dock {
2470 CollaborationPanelDockPosition::Left => DockPosition::Left,
2471 CollaborationPanelDockPosition::Right => DockPosition::Right,
2472 }
2473 }
2474
2475 fn position_is_valid(&self, position: DockPosition) -> bool {
2476 matches!(position, DockPosition::Left | DockPosition::Right)
2477 }
2478
2479 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
2480 settings::update_settings_file::<CollaborationPanelSettings>(
2481 self.fs.clone(),
2482 cx,
2483 move |settings| {
2484 let dock = match position {
2485 DockPosition::Left | DockPosition::Bottom => {
2486 CollaborationPanelDockPosition::Left
2487 }
2488 DockPosition::Right => CollaborationPanelDockPosition::Right,
2489 };
2490 settings.dock = Some(dock);
2491 },
2492 );
2493 }
2494
2495 fn size(&self, cx: &gpui::WindowContext) -> f32 {
2496 self.width
2497 .unwrap_or_else(|| settings::get::<CollaborationPanelSettings>(cx).default_width)
2498 }
2499
2500 fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
2501 self.width = size;
2502 self.serialize(cx);
2503 cx.notify();
2504 }
2505
2506 fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
2507 settings::get::<CollaborationPanelSettings>(cx)
2508 .button
2509 .then(|| "icons/conversations.svg")
2510 }
2511
2512 fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
2513 (
2514 "Collaboration Panel".to_string(),
2515 Some(Box::new(ToggleFocus)),
2516 )
2517 }
2518
2519 fn should_change_position_on_event(event: &Self::Event) -> bool {
2520 matches!(event, Event::DockPositionChanged)
2521 }
2522
2523 fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
2524 self.has_focus
2525 }
2526
2527 fn is_focus_event(event: &Self::Event) -> bool {
2528 matches!(event, Event::Focus)
2529 }
2530}
2531
2532impl PartialEq for ListEntry {
2533 fn eq(&self, other: &Self) -> bool {
2534 match self {
2535 ListEntry::Header(section_1, depth_1) => {
2536 if let ListEntry::Header(section_2, depth_2) = other {
2537 return section_1 == section_2 && depth_1 == depth_2;
2538 }
2539 }
2540 ListEntry::CallParticipant { user: user_1, .. } => {
2541 if let ListEntry::CallParticipant { user: user_2, .. } = other {
2542 return user_1.id == user_2.id;
2543 }
2544 }
2545 ListEntry::ParticipantProject {
2546 project_id: project_id_1,
2547 ..
2548 } => {
2549 if let ListEntry::ParticipantProject {
2550 project_id: project_id_2,
2551 ..
2552 } = other
2553 {
2554 return project_id_1 == project_id_2;
2555 }
2556 }
2557 ListEntry::ParticipantScreen {
2558 peer_id: peer_id_1, ..
2559 } => {
2560 if let ListEntry::ParticipantScreen {
2561 peer_id: peer_id_2, ..
2562 } = other
2563 {
2564 return peer_id_1 == peer_id_2;
2565 }
2566 }
2567 ListEntry::Channel {
2568 channel: channel_1,
2569 depth: depth_1,
2570 } => {
2571 if let ListEntry::Channel {
2572 channel: channel_2,
2573 depth: depth_2,
2574 } = other
2575 {
2576 return channel_1.id == channel_2.id && depth_1 == depth_2;
2577 }
2578 }
2579 ListEntry::ChannelInvite(channel_1) => {
2580 if let ListEntry::ChannelInvite(channel_2) = other {
2581 return channel_1.id == channel_2.id;
2582 }
2583 }
2584 ListEntry::IncomingRequest(user_1) => {
2585 if let ListEntry::IncomingRequest(user_2) = other {
2586 return user_1.id == user_2.id;
2587 }
2588 }
2589 ListEntry::OutgoingRequest(user_1) => {
2590 if let ListEntry::OutgoingRequest(user_2) = other {
2591 return user_1.id == user_2.id;
2592 }
2593 }
2594 ListEntry::Contact {
2595 contact: contact_1, ..
2596 } => {
2597 if let ListEntry::Contact {
2598 contact: contact_2, ..
2599 } = other
2600 {
2601 return contact_1.user.id == contact_2.user.id;
2602 }
2603 }
2604 ListEntry::ChannelEditor { depth } => {
2605 if let ListEntry::ChannelEditor { depth: other_depth } = other {
2606 return depth == other_depth;
2607 }
2608 }
2609 ListEntry::ContactPlaceholder => {
2610 if let ListEntry::ContactPlaceholder = other {
2611 return true;
2612 }
2613 }
2614 }
2615 false
2616 }
2617}
2618
2619fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
2620 Svg::new(svg_path)
2621 .with_color(style.color)
2622 .constrained()
2623 .with_width(style.icon_width)
2624 .aligned()
2625 .constrained()
2626 .with_width(style.button_width)
2627 .with_height(style.button_width)
2628 .contained()
2629 .with_style(style.container)
2630}