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