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(
2163 self.entries
2164 .clone()
2165 .into_iter()
2166 .enumerate()
2167 .map(|(ix, entry)| {
2168 let is_selected = self.selection == Some(ix);
2169 match entry {
2170 ListEntry::Header(section) => {
2171 let is_collapsed =
2172 self.collapsed_sections.contains(§ion);
2173 self.render_header(section, is_selected, is_collapsed, cx)
2174 .into_any_element()
2175 }
2176 ListEntry::Contact { contact, calling } => self
2177 .render_contact(&*contact, calling, is_selected, cx)
2178 .into_any_element(),
2179 ListEntry::ContactPlaceholder => self
2180 .render_contact_placeholder(is_selected, cx)
2181 .into_any_element(),
2182 ListEntry::IncomingRequest(user) => self
2183 .render_contact_request(user, true, is_selected, cx)
2184 .into_any_element(),
2185 ListEntry::OutgoingRequest(user) => self
2186 .render_contact_request(user, false, is_selected, cx)
2187 .into_any_element(),
2188 ListEntry::Channel {
2189 channel,
2190 depth,
2191 has_children,
2192 } => self
2193 .render_channel(
2194 &*channel,
2195 depth,
2196 has_children,
2197 is_selected,
2198 ix,
2199 cx,
2200 )
2201 .into_any_element(),
2202 ListEntry::ChannelEditor { depth } => {
2203 self.render_channel_editor(depth, cx).into_any_element()
2204 }
2205 ListEntry::CallParticipant {
2206 user,
2207 peer_id,
2208 is_pending,
2209 } => self
2210 .render_call_participant(user, peer_id, is_pending, cx)
2211 .into_any_element(),
2212 ListEntry::ParticipantProject {
2213 project_id,
2214 worktree_root_names,
2215 host_user_id,
2216 is_last,
2217 } => self
2218 .render_participant_project(
2219 project_id,
2220 &worktree_root_names,
2221 host_user_id,
2222 is_last,
2223 cx,
2224 )
2225 .into_any_element(),
2226 ListEntry::ParticipantScreen { peer_id, is_last } => self
2227 .render_participant_screen(peer_id, is_last, cx)
2228 .into_any_element(),
2229 ListEntry::ChannelNotes { channel_id } => {
2230 self.render_channel_notes(channel_id, cx).into_any_element()
2231 }
2232 ListEntry::ChannelChat { channel_id } => {
2233 self.render_channel_chat(channel_id, cx).into_any_element()
2234 }
2235 }
2236 }),
2237 ),
2238 )
2239 }
2240
2241 fn render_header(
2242 &mut self,
2243 section: Section,
2244 is_selected: bool,
2245 is_collapsed: bool,
2246 cx: &ViewContext<Self>,
2247 ) -> impl IntoElement {
2248 let mut channel_link = None;
2249 let mut channel_tooltip_text = None;
2250 let mut channel_icon = None;
2251 // let mut is_dragged_over = false;
2252
2253 let text = match section {
2254 Section::ActiveCall => {
2255 let channel_name = maybe!({
2256 let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
2257
2258 let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
2259
2260 channel_link = Some(channel.link());
2261 (channel_icon, channel_tooltip_text) = match channel.visibility {
2262 proto::ChannelVisibility::Public => {
2263 (Some("icons/public.svg"), Some("Copy public channel link."))
2264 }
2265 proto::ChannelVisibility::Members => {
2266 (Some("icons/hash.svg"), Some("Copy private channel link."))
2267 }
2268 };
2269
2270 Some(channel.name.as_ref())
2271 });
2272
2273 if let Some(name) = channel_name {
2274 SharedString::from(format!("{}", name))
2275 } else {
2276 SharedString::from("Current Call")
2277 }
2278 }
2279 Section::ContactRequests => SharedString::from("Requests"),
2280 Section::Contacts => SharedString::from("Contacts"),
2281 Section::Channels => SharedString::from("Channels"),
2282 Section::ChannelInvites => SharedString::from("Invites"),
2283 Section::Online => SharedString::from("Online"),
2284 Section::Offline => SharedString::from("Offline"),
2285 };
2286
2287 let button = match section {
2288 Section::ActiveCall => channel_link.map(|channel_link| {
2289 let channel_link_copy = channel_link.clone();
2290 IconButton::new("channel-link", Icon::Copy)
2291 .on_click(move |_, cx| {
2292 let item = ClipboardItem::new(channel_link_copy.clone());
2293 cx.write_to_clipboard(item)
2294 })
2295 .tooltip(|cx| Tooltip::text("Copy channel link", cx))
2296 }),
2297 Section::Contacts => Some(
2298 IconButton::new("add-contact", Icon::Plus)
2299 .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
2300 .tooltip(|cx| Tooltip::text("Search for new contact", cx)),
2301 ),
2302 Section::Channels => Some(
2303 IconButton::new("add-channel", Icon::Plus)
2304 .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
2305 .tooltip(|cx| Tooltip::text("Create a channel", cx)),
2306 ),
2307 _ => None,
2308 };
2309
2310 let can_collapse = match section {
2311 Section::ActiveCall | Section::Channels | Section::Contacts => false,
2312 Section::ChannelInvites
2313 | Section::ContactRequests
2314 | Section::Online
2315 | Section::Offline => true,
2316 };
2317
2318 h_stack()
2319 .w_full()
2320 .map(|el| {
2321 if can_collapse {
2322 el.child(
2323 ListItem::new(text.clone())
2324 .child(div().w_full().child(Label::new(text)))
2325 .selected(is_selected)
2326 .toggle(Some(!is_collapsed))
2327 .on_click(cx.listener(move |this, _, cx| {
2328 this.toggle_section_expanded(section, cx)
2329 })),
2330 )
2331 } else {
2332 el.child(
2333 ListHeader::new(text)
2334 .when_some(button, |el, button| el.meta(button))
2335 .selected(is_selected),
2336 )
2337 }
2338 })
2339 .when(section == Section::Channels, |el| {
2340 el.drag_over::<DraggedChannelView>(|style| {
2341 style.bg(cx.theme().colors().ghost_element_hover)
2342 })
2343 .on_drop(cx.listener(
2344 move |this, view: &View<DraggedChannelView>, cx| {
2345 this.channel_store
2346 .update(cx, |channel_store, cx| {
2347 channel_store.move_channel(view.read(cx).channel.id, None, cx)
2348 })
2349 .detach_and_log_err(cx)
2350 },
2351 ))
2352 })
2353 }
2354
2355 fn render_contact(
2356 &mut self,
2357 contact: &Contact,
2358 calling: bool,
2359 is_selected: bool,
2360 cx: &mut ViewContext<Self>,
2361 ) -> impl IntoElement {
2362 enum ContactTooltip {}
2363
2364 let online = contact.online;
2365 let busy = contact.busy || calling;
2366 let user_id = contact.user.id;
2367 let github_login = SharedString::from(contact.user.github_login.clone());
2368 let mut item = ListItem::new(github_login.clone())
2369 .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
2370 .child(
2371 h_stack()
2372 .w_full()
2373 .justify_between()
2374 .child(Label::new(github_login.clone()))
2375 .when(calling, |el| {
2376 el.child(Label::new("Calling").color(Color::Muted))
2377 })
2378 .when(!calling, |el| {
2379 el.child(
2380 div()
2381 .id("remove_contact")
2382 .invisible()
2383 .group_hover("", |style| style.visible())
2384 .child(
2385 IconButton::new("remove_contact", Icon::Close)
2386 .icon_color(Color::Muted)
2387 .tooltip(|cx| Tooltip::text("Remove Contact", cx))
2388 .on_click(cx.listener({
2389 let github_login = github_login.clone();
2390 move |this, _, cx| {
2391 this.remove_contact(user_id, &github_login, cx);
2392 }
2393 })),
2394 ),
2395 )
2396 }),
2397 )
2398 .left_child(
2399 // todo!() handle contacts with no avatar
2400 Avatar::data(contact.user.avatar.clone().unwrap())
2401 .availability_indicator(if online { Some(!busy) } else { None }),
2402 )
2403 .when(online && !busy, |el| {
2404 el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
2405 });
2406
2407 div()
2408 .id(github_login.clone())
2409 .group("")
2410 .child(item)
2411 .tooltip(move |cx| {
2412 let text = if !online {
2413 format!(" {} is offline", &github_login)
2414 } else if busy {
2415 format!(" {} is on a call", &github_login)
2416 } else {
2417 let room = ActiveCall::global(cx).read(cx).room();
2418 if room.is_some() {
2419 format!("Invite {} to join call", &github_login)
2420 } else {
2421 format!("Call {}", &github_login)
2422 }
2423 };
2424 Tooltip::text(text, cx)
2425 })
2426 }
2427
2428 fn render_contact_request(
2429 &mut self,
2430 user: Arc<User>,
2431 is_incoming: bool,
2432 is_selected: bool,
2433 cx: &mut ViewContext<Self>,
2434 ) -> impl IntoElement {
2435 let github_login = SharedString::from(user.github_login.clone());
2436 let user_id = user.id;
2437 let is_contact_request_pending = self.user_store.read(cx).is_contact_request_pending(&user);
2438 let color = if is_contact_request_pending {
2439 Color::Muted
2440 } else {
2441 Color::Default
2442 };
2443
2444 let controls = if is_incoming {
2445 vec![
2446 IconButton::new("remove_contact", Icon::Close)
2447 .on_click(cx.listener(move |this, _, cx| {
2448 this.respond_to_contact_request(user_id, false, cx);
2449 }))
2450 .icon_color(color)
2451 .tooltip(|cx| Tooltip::text("Decline invite", cx)),
2452 IconButton::new("remove_contact", Icon::Check)
2453 .on_click(cx.listener(move |this, _, cx| {
2454 this.respond_to_contact_request(user_id, true, cx);
2455 }))
2456 .icon_color(color)
2457 .tooltip(|cx| Tooltip::text("Accept invite", cx)),
2458 ]
2459 } else {
2460 let github_login = github_login.clone();
2461 vec![IconButton::new("remove_contact", Icon::Close)
2462 .on_click(cx.listener(move |this, _, cx| {
2463 this.remove_contact(user_id, &github_login, cx);
2464 }))
2465 .icon_color(color)
2466 .tooltip(|cx| Tooltip::text("Cancel invite", cx))]
2467 };
2468
2469 ListItem::new(github_login.clone())
2470 .child(
2471 h_stack()
2472 .w_full()
2473 .justify_between()
2474 .child(Label::new(github_login.clone()))
2475 .child(h_stack().children(controls)),
2476 )
2477 .when_some(user.avatar.clone(), |el, avatar| el.left_avatar(avatar))
2478 }
2479
2480 fn render_contact_placeholder(
2481 &self,
2482 is_selected: bool,
2483 cx: &mut ViewContext<Self>,
2484 ) -> impl IntoElement {
2485 ListItem::new("contact-placeholder")
2486 .child(IconElement::new(Icon::Plus))
2487 .child(Label::new("Add a Contact"))
2488 .selected(is_selected)
2489 .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
2490 }
2491
2492 fn render_channel(
2493 &self,
2494 channel: &Channel,
2495 depth: usize,
2496 has_children: bool,
2497 is_selected: bool,
2498 ix: usize,
2499 cx: &mut ViewContext<Self>,
2500 ) -> impl IntoElement {
2501 let channel_id = channel.id;
2502
2503 let is_active = maybe!({
2504 let call_channel = ActiveCall::global(cx)
2505 .read(cx)
2506 .room()?
2507 .read(cx)
2508 .channel_id()?;
2509 Some(call_channel == channel_id)
2510 })
2511 .unwrap_or(false);
2512 let is_public = self
2513 .channel_store
2514 .read(cx)
2515 .channel_for_id(channel_id)
2516 .map(|channel| channel.visibility)
2517 == Some(proto::ChannelVisibility::Public);
2518 let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
2519 let disclosed =
2520 has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
2521
2522 let has_messages_notification = channel.unseen_message_id.is_some();
2523 let has_notes_notification = channel.unseen_note_version.is_some();
2524
2525 const FACEPILE_LIMIT: usize = 3;
2526 let participants = self.channel_store.read(cx).channel_participants(channel_id);
2527
2528 let face_pile = if !participants.is_empty() {
2529 let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2530 let user = &participants[0];
2531
2532 let result = FacePile {
2533 faces: participants
2534 .iter()
2535 .filter_map(|user| Some(Avatar::data(user.avatar.clone()?).into_any_element()))
2536 .take(FACEPILE_LIMIT)
2537 .chain(if extra_count > 0 {
2538 // todo!() @nate - this label looks wrong.
2539 Some(Label::new(format!("+{}", extra_count)).into_any_element())
2540 } else {
2541 None
2542 })
2543 .collect::<Vec<_>>(),
2544 };
2545
2546 Some(result)
2547 } else {
2548 None
2549 };
2550
2551 let width = self.width.unwrap_or(px(240.));
2552
2553 div()
2554 .id(channel_id as usize)
2555 .group("")
2556 .on_drag({
2557 let channel = channel.clone();
2558 move |cx| {
2559 let channel = channel.clone();
2560 cx.build_view({ |cx| DraggedChannelView { channel, width } })
2561 }
2562 })
2563 .drag_over::<DraggedChannelView>(|style| {
2564 style.bg(cx.theme().colors().ghost_element_hover)
2565 })
2566 .on_drop(
2567 cx.listener(move |this, view: &View<DraggedChannelView>, cx| {
2568 this.channel_store
2569 .update(cx, |channel_store, cx| {
2570 channel_store.move_channel(
2571 view.read(cx).channel.id,
2572 Some(channel_id),
2573 cx,
2574 )
2575 })
2576 .detach_and_log_err(cx)
2577 }),
2578 )
2579 .child(
2580 ListItem::new(channel_id as usize)
2581 .indent_level(depth)
2582 .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to step over the disclosure toggle
2583 .left_icon(if is_public { Icon::Public } else { Icon::Hash })
2584 .selected(is_selected || is_active)
2585 .child(
2586 h_stack()
2587 .w_full()
2588 .justify_between()
2589 .child(
2590 h_stack()
2591 .id(channel_id as usize)
2592 .child(Label::new(channel.name.clone()))
2593 .children(face_pile.map(|face_pile| face_pile.render(cx))),
2594 )
2595 .child(
2596 h_stack()
2597 .child(
2598 div()
2599 .id("channel_chat")
2600 .when(!has_messages_notification, |el| el.invisible())
2601 .group_hover("", |style| style.visible())
2602 .child(
2603 IconButton::new(
2604 "channel_chat",
2605 Icon::MessageBubbles,
2606 )
2607 .icon_color(if has_messages_notification {
2608 Color::Default
2609 } else {
2610 Color::Muted
2611 })
2612 .on_click(cx.listener(move |this, _, cx| {
2613 this.join_channel_chat(channel_id, cx)
2614 }))
2615 .tooltip(|cx| {
2616 Tooltip::text("Open channel chat", cx)
2617 }),
2618 ),
2619 )
2620 .child(
2621 div()
2622 .id("channel_notes")
2623 .when(!has_notes_notification, |el| el.invisible())
2624 .group_hover("", |style| style.visible())
2625 .child(
2626 IconButton::new("channel_notes", Icon::File)
2627 .icon_color(if has_notes_notification {
2628 Color::Default
2629 } else {
2630 Color::Muted
2631 })
2632 .on_click(cx.listener(move |this, _, cx| {
2633 this.open_channel_notes(channel_id, cx)
2634 }))
2635 .tooltip(|cx| {
2636 Tooltip::text("Open channel notes", cx)
2637 }),
2638 ),
2639 ),
2640 ),
2641 )
2642 .toggle(disclosed)
2643 .on_toggle(
2644 cx.listener(move |this, _, cx| {
2645 this.toggle_channel_collapsed(channel_id, cx)
2646 }),
2647 )
2648 .on_click(cx.listener(move |this, _, cx| {
2649 if this.drag_target_channel == ChannelDragTarget::None {
2650 if is_active {
2651 this.open_channel_notes(channel_id, cx)
2652 } else {
2653 this.join_channel(channel_id, cx)
2654 }
2655 }
2656 }))
2657 .on_secondary_mouse_down(cx.listener(
2658 move |this, event: &MouseDownEvent, cx| {
2659 this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
2660 },
2661 )),
2662 )
2663 .tooltip(|cx| Tooltip::text("Join channel", cx))
2664
2665 // let channel_id = channel.id;
2666 // let collab_theme = &theme.collab_panel;
2667 // let is_public = self
2668 // .channel_store
2669 // .read(cx)
2670 // .channel_for_id(channel_id)
2671 // .map(|channel| channel.visibility)
2672 // == Some(proto::ChannelVisibility::Public);
2673 // let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
2674 // let disclosed =
2675 // has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
2676
2677 // enum ChannelCall {}
2678 // enum ChannelNote {}
2679 // enum NotesTooltip {}
2680 // enum ChatTooltip {}
2681 // enum ChannelTooltip {}
2682
2683 // let mut is_dragged_over = false;
2684 // if cx
2685 // .global::<DragAndDrop<Workspace>>()
2686 // .currently_dragged::<Channel>(cx.window())
2687 // .is_some()
2688 // && self.drag_target_channel == ChannelDragTarget::Channel(channel_id)
2689 // {
2690 // is_dragged_over = true;
2691 // }
2692
2693 // let has_messages_notification = channel.unseen_message_id.is_some();
2694
2695 // MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
2696 // let row_hovered = state.hovered();
2697
2698 // let mut select_state = |interactive: &Interactive<ContainerStyle>| {
2699 // if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() {
2700 // interactive.clicked.as_ref().unwrap().clone()
2701 // } else if state.hovered() || other_selected {
2702 // interactive
2703 // .hovered
2704 // .as_ref()
2705 // .unwrap_or(&interactive.default)
2706 // .clone()
2707 // } else {
2708 // interactive.default.clone()
2709 // }
2710 // };
2711
2712 // Flex::<Self>::row()
2713 // .with_child(
2714 // Svg::new(if is_public {
2715 // "icons/public.svg"
2716 // } else {
2717 // "icons/hash.svg"
2718 // })
2719 // .with_color(collab_theme.channel_hash.color)
2720 // .constrained()
2721 // .with_width(collab_theme.channel_hash.width)
2722 // .aligned()
2723 // .left(),
2724 // )
2725 // .with_child({
2726 // let style = collab_theme.channel_name.inactive_state();
2727 // Flex::row()
2728 // .with_child(
2729 // Label::new(channel.name.clone(), style.text.clone())
2730 // .contained()
2731 // .with_style(style.container)
2732 // .aligned()
2733 // .left()
2734 // .with_tooltip::<ChannelTooltip>(
2735 // ix,
2736 // "Join channel",
2737 // None,
2738 // theme.tooltip.clone(),
2739 // cx,
2740 // ),
2741 // )
2742 // .with_children({
2743 // let participants =
2744 // self.channel_store.read(cx).channel_participants(channel_id);
2745
2746 // if !participants.is_empty() {
2747 // let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2748
2749 // let result = FacePile::new(collab_theme.face_overlap)
2750 // .with_children(
2751 // participants
2752 // .iter()
2753 // .filter_map(|user| {
2754 // Some(
2755 // Image::from_data(user.avatar.clone()?)
2756 // .with_style(collab_theme.channel_avatar),
2757 // )
2758 // })
2759 // .take(FACEPILE_LIMIT),
2760 // )
2761 // .with_children((extra_count > 0).then(|| {
2762 // Label::new(
2763 // format!("+{}", extra_count),
2764 // collab_theme.extra_participant_label.text.clone(),
2765 // )
2766 // .contained()
2767 // .with_style(collab_theme.extra_participant_label.container)
2768 // }));
2769
2770 // Some(result)
2771 // } else {
2772 // None
2773 // }
2774 // })
2775 // .with_spacing(8.)
2776 // .align_children_center()
2777 // .flex(1., true)
2778 // })
2779 // .with_child(
2780 // MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| {
2781 // let container_style = collab_theme
2782 // .disclosure
2783 // .button
2784 // .style_for(mouse_state)
2785 // .container;
2786
2787 // if channel.unseen_message_id.is_some() {
2788 // Svg::new("icons/conversations.svg")
2789 // .with_color(collab_theme.channel_note_active_color)
2790 // .constrained()
2791 // .with_width(collab_theme.channel_hash.width)
2792 // .contained()
2793 // .with_style(container_style)
2794 // .with_uniform_padding(4.)
2795 // .into_any()
2796 // } else if row_hovered {
2797 // Svg::new("icons/conversations.svg")
2798 // .with_color(collab_theme.channel_hash.color)
2799 // .constrained()
2800 // .with_width(collab_theme.channel_hash.width)
2801 // .contained()
2802 // .with_style(container_style)
2803 // .with_uniform_padding(4.)
2804 // .into_any()
2805 // } else {
2806 // Empty::new().into_any()
2807 // }
2808 // })
2809 // .on_click(MouseButton::Left, move |_, this, cx| {
2810 // this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
2811 // })
2812 // .with_tooltip::<ChatTooltip>(
2813 // ix,
2814 // "Open channel chat",
2815 // None,
2816 // theme.tooltip.clone(),
2817 // cx,
2818 // )
2819 // .contained()
2820 // .with_margin_right(4.),
2821 // )
2822 // .with_child(
2823 // MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| {
2824 // let container_style = collab_theme
2825 // .disclosure
2826 // .button
2827 // .style_for(mouse_state)
2828 // .container;
2829 // if row_hovered || channel.unseen_note_version.is_some() {
2830 // Svg::new("icons/file.svg")
2831 // .with_color(if channel.unseen_note_version.is_some() {
2832 // collab_theme.channel_note_active_color
2833 // } else {
2834 // collab_theme.channel_hash.color
2835 // })
2836 // .constrained()
2837 // .with_width(collab_theme.channel_hash.width)
2838 // .contained()
2839 // .with_style(container_style)
2840 // .with_uniform_padding(4.)
2841 // .with_margin_right(collab_theme.channel_hash.container.margin.left)
2842 // .with_tooltip::<NotesTooltip>(
2843 // ix as usize,
2844 // "Open channel notes",
2845 // None,
2846 // theme.tooltip.clone(),
2847 // cx,
2848 // )
2849 // .into_any()
2850 // } else if has_messages_notification {
2851 // Empty::new()
2852 // .constrained()
2853 // .with_width(collab_theme.channel_hash.width)
2854 // .contained()
2855 // .with_uniform_padding(4.)
2856 // .with_margin_right(collab_theme.channel_hash.container.margin.left)
2857 // .into_any()
2858 // } else {
2859 // Empty::new().into_any()
2860 // }
2861 // })
2862 // .on_click(MouseButton::Left, move |_, this, cx| {
2863 // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
2864 // }),
2865 // )
2866 // .align_children_center()
2867 // .styleable_component()
2868 // .disclosable(
2869 // disclosed,
2870 // Box::new(ToggleCollapse {
2871 // location: channel.id.clone(),
2872 // }),
2873 // )
2874 // .with_id(ix)
2875 // .with_style(collab_theme.disclosure.clone())
2876 // .element()
2877 // .constrained()
2878 // .with_height(collab_theme.row_height)
2879 // .contained()
2880 // .with_style(select_state(
2881 // collab_theme
2882 // .channel_row
2883 // .in_state(is_selected || is_active || is_dragged_over),
2884 // ))
2885 // .with_padding_left(
2886 // collab_theme.channel_row.default_style().padding.left
2887 // + collab_theme.channel_indent * depth as f32,
2888 // )
2889 // })
2890 // .on_click(MouseButton::Left, move |_, this, cx| {
2891 // if this.
2892 // drag_target_channel == ChannelDragTarget::None {
2893 // if is_active {
2894 // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
2895 // } else {
2896 // this.join_channel(channel_id, cx)
2897 // }
2898 // }
2899 // })
2900 // .on_click(MouseButton::Right, {
2901 // let channel = channel.clone();
2902 // move |e, this, cx| {
2903 // this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx);
2904 // }
2905 // })
2906 // .on_up(MouseButton::Left, move |_, this, cx| {
2907 // if let Some((_, dragged_channel)) = cx
2908 // .global::<DragAndDrop<Workspace>>()
2909 // .currently_dragged::<Channel>(cx.window())
2910 // {
2911 // this.channel_store
2912 // .update(cx, |channel_store, cx| {
2913 // channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
2914 // })
2915 // .detach_and_log_err(cx)
2916 // }
2917 // })
2918 // .on_move({
2919 // let channel = channel.clone();
2920 // move |_, this, cx| {
2921 // if let Some((_, dragged_channel)) = cx
2922 // .global::<DragAndDrop<Workspace>>()
2923 // .currently_dragged::<Channel>(cx.window())
2924 // {
2925 // if channel.id != dragged_channel.id {
2926 // this.drag_target_channel = ChannelDragTarget::Channel(channel.id);
2927 // }
2928 // cx.notify()
2929 // }
2930 // }
2931 // })
2932 // .as_draggable::<_, Channel>(
2933 // channel.clone(),
2934 // move |_, channel, cx: &mut ViewContext<Workspace>| {
2935 // let theme = &theme::current(cx).collab_panel;
2936
2937 // Flex::<Workspace>::row()
2938 // .with_child(
2939 // Svg::new("icons/hash.svg")
2940 // .with_color(theme.channel_hash.color)
2941 // .constrained()
2942 // .with_width(theme.channel_hash.width)
2943 // .aligned()
2944 // .left(),
2945 // )
2946 // .with_child(
2947 // Label::new(channel.name.clone(), theme.channel_name.text.clone())
2948 // .contained()
2949 // .with_style(theme.channel_name.container)
2950 // .aligned()
2951 // .left(),
2952 // )
2953 // .align_children_center()
2954 // .contained()
2955 // .with_background_color(
2956 // theme
2957 // .container
2958 // .background_color
2959 // .unwrap_or(gpui::color::Color::transparent_black()),
2960 // )
2961 // .contained()
2962 // .with_padding_left(
2963 // theme.channel_row.default_style().padding.left
2964 // + theme.channel_indent * depth as f32,
2965 // )
2966 // .into_any()
2967 // },
2968 // )
2969 // .with_cursor_style(CursorStyle::PointingHand)
2970 // .into_any()
2971 }
2972
2973 fn render_channel_editor(
2974 &mut self,
2975 depth: usize,
2976 cx: &mut ViewContext<Self>,
2977 ) -> impl IntoElement {
2978 let item = ListItem::new("channel-editor")
2979 .inset(false)
2980 .indent_level(depth)
2981 .left_icon(Icon::Hash);
2982
2983 if let Some(pending_name) = self
2984 .channel_editing_state
2985 .as_ref()
2986 .and_then(|state| state.pending_name())
2987 {
2988 item.child(Label::new(pending_name))
2989 } else {
2990 item.child(
2991 div()
2992 .w_full()
2993 .py_1() // todo!() @nate this is a px off at the default font size.
2994 .child(self.channel_name_editor.clone()),
2995 )
2996 }
2997 }
2998}
2999
3000fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement {
3001 let rem_size = cx.rem_size();
3002 let line_height = cx.text_style().line_height_in_pixels(rem_size);
3003 let width = rem_size * 1.5;
3004 let thickness = px(2.);
3005 let color = cx.theme().colors().text;
3006
3007 canvas(move |bounds, cx| {
3008 let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
3009 let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
3010 let right = bounds.right();
3011 let top = bounds.top();
3012
3013 cx.paint_quad(
3014 Bounds::from_corners(
3015 point(start_x, top),
3016 point(
3017 start_x + thickness,
3018 if is_last { start_y } else { bounds.bottom() },
3019 ),
3020 ),
3021 Default::default(),
3022 color,
3023 Default::default(),
3024 Hsla::transparent_black(),
3025 );
3026 cx.paint_quad(
3027 Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
3028 Default::default(),
3029 color,
3030 Default::default(),
3031 Hsla::transparent_black(),
3032 );
3033 })
3034 .w(width)
3035 .h(line_height)
3036}
3037
3038impl Render for CollabPanel {
3039 type Element = Focusable<Div>;
3040
3041 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
3042 v_stack()
3043 .key_context("CollabPanel")
3044 .on_action(cx.listener(CollabPanel::cancel))
3045 .on_action(cx.listener(CollabPanel::select_next))
3046 .on_action(cx.listener(CollabPanel::select_prev))
3047 .on_action(cx.listener(CollabPanel::confirm))
3048 .on_action(cx.listener(CollabPanel::insert_space))
3049 // .on_action(cx.listener(CollabPanel::remove))
3050 .on_action(cx.listener(CollabPanel::remove_selected_channel))
3051 .on_action(cx.listener(CollabPanel::show_inline_context_menu))
3052 // .on_action(cx.listener(CollabPanel::new_subchannel))
3053 // .on_action(cx.listener(CollabPanel::invite_members))
3054 // .on_action(cx.listener(CollabPanel::manage_members))
3055 .on_action(cx.listener(CollabPanel::rename_selected_channel))
3056 // .on_action(cx.listener(CollabPanel::rename_channel))
3057 // .on_action(cx.listener(CollabPanel::toggle_channel_collapsed_action))
3058 .on_action(cx.listener(CollabPanel::collapse_selected_channel))
3059 .on_action(cx.listener(CollabPanel::expand_selected_channel))
3060 // .on_action(cx.listener(CollabPanel::open_channel_notes))
3061 // .on_action(cx.listener(CollabPanel::join_channel_chat))
3062 // .on_action(cx.listener(CollabPanel::copy_channel_link))
3063 .track_focus(&self.focus_handle)
3064 .size_full()
3065 .child(if self.user_store.read(cx).current_user().is_none() {
3066 self.render_signed_out(cx)
3067 } else {
3068 self.render_signed_in(cx)
3069 })
3070 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
3071 overlay()
3072 .position(*position)
3073 .anchor(gpui::AnchorCorner::TopLeft)
3074 .child(menu.clone())
3075 }))
3076 }
3077}
3078
3079// impl View for CollabPanel {
3080// fn ui_name() -> &'static str {
3081// "CollabPanel"
3082// }
3083
3084// fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
3085// if !self.has_focus {
3086// self.has_focus = true;
3087// if !self.context_menu.is_focused(cx) {
3088// if let Some(editing_state) = &self.channel_editing_state {
3089// if editing_state.pending_name().is_none() {
3090// cx.focus(&self.channel_name_editor);
3091// } else {
3092// cx.focus(&self.filter_editor);
3093// }
3094// } else {
3095// cx.focus(&self.filter_editor);
3096// }
3097// }
3098// cx.emit(Event::Focus);
3099// }
3100// }
3101
3102// fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
3103// self.has_focus = false;
3104// }
3105
3106// fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
3107// let theme = &theme::current(cx).collab_panel;
3108
3109// if self.user_store.read(cx).current_user().is_none() {
3110// enum LogInButton {}
3111
3112// return Flex::column()
3113// .with_child(
3114// MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
3115// let button = theme.log_in_button.style_for(state);
3116// Label::new("Sign in to collaborate", button.text.clone())
3117// .aligned()
3118// .left()
3119// .contained()
3120// .with_style(button.container)
3121// })
3122// .on_click(MouseButton::Left, |_, this, cx| {
3123// let client = this.client.clone();
3124// cx.spawn(|_, cx| async move {
3125// client.authenticate_and_connect(true, &cx).await.log_err();
3126// })
3127// .detach();
3128// })
3129// .with_cursor_style(CursorStyle::PointingHand),
3130// )
3131// .contained()
3132// .with_style(theme.container)
3133// .into_any();
3134// }
3135
3136// enum PanelFocus {}
3137// MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
3138// Stack::new()
3139// .with_child(
3140// Flex::column()
3141// .with_child(
3142// Flex::row().with_child(
3143// ChildView::new(&self.filter_editor, cx)
3144// .contained()
3145// .with_style(theme.user_query_editor.container)
3146// .flex(1.0, true),
3147// ),
3148// )
3149// .with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
3150// .contained()
3151// .with_style(theme.container)
3152// .into_any(),
3153// )
3154// .with_children(
3155// (!self.context_menu_on_selected)
3156// .then(|| ChildView::new(&self.context_menu, cx)),
3157// )
3158// .into_any()
3159// })
3160// .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
3161// .into_any_named("collab panel")
3162// }
3163
3164// fn update_keymap_context(
3165// &self,
3166// keymap: &mut gpui::keymap_matcher::KeymapContext,
3167// _: &AppContext,
3168// ) {
3169// Self::reset_to_default_keymap_context(keymap);
3170// if self.channel_editing_state.is_some() {
3171// keymap.add_identifier("editing");
3172// } else {
3173// keymap.add_identifier("not_editing");
3174// }
3175// }
3176// }
3177
3178impl EventEmitter<PanelEvent> for CollabPanel {}
3179
3180impl Panel for CollabPanel {
3181 fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
3182 CollaborationPanelSettings::get_global(cx).dock
3183 }
3184
3185 fn position_is_valid(&self, position: DockPosition) -> bool {
3186 matches!(position, DockPosition::Left | DockPosition::Right)
3187 }
3188
3189 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
3190 settings::update_settings_file::<CollaborationPanelSettings>(
3191 self.fs.clone(),
3192 cx,
3193 move |settings| settings.dock = Some(position),
3194 );
3195 }
3196
3197 fn size(&self, cx: &gpui::WindowContext) -> f32 {
3198 self.width.map_or_else(
3199 || CollaborationPanelSettings::get_global(cx).default_width,
3200 |width| width.0,
3201 )
3202 }
3203
3204 fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
3205 self.width = size.map(|s| px(s));
3206 self.serialize(cx);
3207 cx.notify();
3208 }
3209
3210 fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::Icon> {
3211 CollaborationPanelSettings::get_global(cx)
3212 .button
3213 .then(|| ui::Icon::Collab)
3214 }
3215
3216 fn toggle_action(&self) -> Box<dyn gpui::Action> {
3217 Box::new(ToggleFocus)
3218 }
3219
3220 fn persistent_name() -> &'static str {
3221 "CollabPanel"
3222 }
3223}
3224
3225impl FocusableView for CollabPanel {
3226 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
3227 self.filter_editor.focus_handle(cx).clone()
3228 }
3229}
3230
3231impl PartialEq for ListEntry {
3232 fn eq(&self, other: &Self) -> bool {
3233 match self {
3234 ListEntry::Header(section_1) => {
3235 if let ListEntry::Header(section_2) = other {
3236 return section_1 == section_2;
3237 }
3238 }
3239 ListEntry::CallParticipant { user: user_1, .. } => {
3240 if let ListEntry::CallParticipant { user: user_2, .. } = other {
3241 return user_1.id == user_2.id;
3242 }
3243 }
3244 ListEntry::ParticipantProject {
3245 project_id: project_id_1,
3246 ..
3247 } => {
3248 if let ListEntry::ParticipantProject {
3249 project_id: project_id_2,
3250 ..
3251 } = other
3252 {
3253 return project_id_1 == project_id_2;
3254 }
3255 }
3256 ListEntry::ParticipantScreen {
3257 peer_id: peer_id_1, ..
3258 } => {
3259 if let ListEntry::ParticipantScreen {
3260 peer_id: peer_id_2, ..
3261 } = other
3262 {
3263 return peer_id_1 == peer_id_2;
3264 }
3265 }
3266 ListEntry::Channel {
3267 channel: channel_1, ..
3268 } => {
3269 if let ListEntry::Channel {
3270 channel: channel_2, ..
3271 } = other
3272 {
3273 return channel_1.id == channel_2.id;
3274 }
3275 }
3276 ListEntry::ChannelNotes { channel_id } => {
3277 if let ListEntry::ChannelNotes {
3278 channel_id: other_id,
3279 } = other
3280 {
3281 return channel_id == other_id;
3282 }
3283 }
3284 ListEntry::ChannelChat { channel_id } => {
3285 if let ListEntry::ChannelChat {
3286 channel_id: other_id,
3287 } = other
3288 {
3289 return channel_id == other_id;
3290 }
3291 }
3292 // ListEntry::ChannelInvite(channel_1) => {
3293 // if let ListEntry::ChannelInvite(channel_2) = other {
3294 // return channel_1.id == channel_2.id;
3295 // }
3296 // }
3297 ListEntry::IncomingRequest(user_1) => {
3298 if let ListEntry::IncomingRequest(user_2) = other {
3299 return user_1.id == user_2.id;
3300 }
3301 }
3302 ListEntry::OutgoingRequest(user_1) => {
3303 if let ListEntry::OutgoingRequest(user_2) = other {
3304 return user_1.id == user_2.id;
3305 }
3306 }
3307 ListEntry::Contact {
3308 contact: contact_1, ..
3309 } => {
3310 if let ListEntry::Contact {
3311 contact: contact_2, ..
3312 } = other
3313 {
3314 return contact_1.user.id == contact_2.user.id;
3315 }
3316 }
3317 ListEntry::ChannelEditor { depth } => {
3318 if let ListEntry::ChannelEditor { depth: other_depth } = other {
3319 return depth == other_depth;
3320 }
3321 }
3322 ListEntry::ContactPlaceholder => {
3323 if let ListEntry::ContactPlaceholder = other {
3324 return true;
3325 }
3326 }
3327 }
3328 false
3329 }
3330}
3331
3332// fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
3333// Svg::new(svg_path)
3334// .with_color(style.color)
3335// .constrained()
3336// .with_width(style.icon_width)
3337// .aligned()
3338// .constrained()
3339// .with_width(style.button_width)
3340// .with_height(style.button_width)
3341// .contained()
3342// .with_style(style.container)
3343// }
3344
3345struct DraggedChannelView {
3346 channel: Channel,
3347 width: Pixels,
3348}
3349
3350impl Render for DraggedChannelView {
3351 type Element = Div;
3352
3353 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
3354 let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
3355 h_stack()
3356 .font(ui_font)
3357 .bg(cx.theme().colors().background)
3358 .w(self.width)
3359 .p_1()
3360 .gap_1()
3361 .child(
3362 IconElement::new(
3363 if self.channel.visibility == proto::ChannelVisibility::Public {
3364 Icon::Public
3365 } else {
3366 Icon::Hash
3367 },
3368 )
3369 .size(IconSize::Small)
3370 .color(Color::Muted),
3371 )
3372 .child(Label::new(self.channel.name.clone()))
3373 }
3374}