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