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