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