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