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