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