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