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