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