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