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