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