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