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