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