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