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