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