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 self.list_state.reset(self.entries.len());
966
967 if select_same_item {
968 if let Some(prev_selected_entry) = prev_selected_entry {
969 self.selection.take();
970 for (ix, entry) in self.entries.iter().enumerate() {
971 if *entry == prev_selected_entry {
972 self.selection = Some(ix);
973 self.scroll_to_item(ix);
974 break;
975 }
976 }
977 }
978 } else {
979 self.selection = self.selection.and_then(|prev_selection| {
980 if self.entries.is_empty() {
981 None
982 } else {
983 let ix = prev_selection.min(self.entries.len() - 1);
984 self.scroll_to_item(ix);
985 Some(ix)
986 }
987 });
988 }
989
990 if scroll_to_top {
991 self.scroll_to_item(0)
992 } else {
993 let ListOffset {
994 item_ix: old_index,
995 offset_in_item: old_offset,
996 } = self.list_state.logical_scroll_top();
997 // Attempt to maintain the same scroll position.
998 if let Some(old_top_entry) = old_entries.get(old_index) {
999 let (new_index, new_offset) = self
1000 .entries
1001 .iter()
1002 .position(|entry| entry == old_top_entry)
1003 .map(|item_ix| (item_ix, old_offset))
1004 .or_else(|| {
1005 let entry_after_old_top = old_entries.get(old_index + 1)?;
1006 let item_ix = self
1007 .entries
1008 .iter()
1009 .position(|entry| entry == entry_after_old_top)?;
1010 Some((item_ix, px(0.)))
1011 })
1012 .or_else(|| {
1013 let entry_before_old_top = old_entries.get(old_index.saturating_sub(1))?;
1014 let item_ix = self
1015 .entries
1016 .iter()
1017 .position(|entry| entry == entry_before_old_top)?;
1018 Some((item_ix, px(0.)))
1019 })
1020 .unwrap_or_else(|| (old_index, old_offset));
1021
1022 self.list_state.scroll_to(ListOffset {
1023 item_ix: new_index,
1024 offset_in_item: new_offset,
1025 });
1026 }
1027 }
1028
1029 cx.notify();
1030 }
1031
1032 fn render_call_participant(
1033 &self,
1034 user: &Arc<User>,
1035 peer_id: Option<PeerId>,
1036 is_pending: bool,
1037 cx: &mut ViewContext<Self>,
1038 ) -> impl IntoElement {
1039 let is_current_user =
1040 self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
1041 let tooltip = format!("Follow {}", user.github_login);
1042
1043 ListItem::new(SharedString::from(user.github_login.clone()))
1044 .start_slot(Avatar::new(user.avatar_uri.clone()))
1045 .child(Label::new(user.github_login.clone()))
1046 .end_slot(if is_pending {
1047 Label::new("Calling").color(Color::Muted).into_any_element()
1048 } else if is_current_user {
1049 IconButton::new("leave-call", Icon::Exit)
1050 .style(ButtonStyle::Subtle)
1051 .on_click(cx.listener(move |this, _, cx| {
1052 Self::leave_call(cx);
1053 }))
1054 .tooltip(|cx| Tooltip::text("Leave Call", cx))
1055 .into_any_element()
1056 } else {
1057 div().into_any_element()
1058 })
1059 .when_some(peer_id, |this, peer_id| {
1060 this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
1061 .on_click(cx.listener(move |this, _, cx| {
1062 this.workspace
1063 .update(cx, |workspace, cx| workspace.follow(peer_id, cx));
1064 }))
1065 })
1066 }
1067
1068 fn render_participant_project(
1069 &self,
1070 project_id: u64,
1071 worktree_root_names: &[String],
1072 host_user_id: u64,
1073 // is_current: bool,
1074 is_last: bool,
1075 // is_selected: bool,
1076 // theme: &theme::Theme,
1077 cx: &mut ViewContext<Self>,
1078 ) -> impl IntoElement {
1079 let project_name: SharedString = if worktree_root_names.is_empty() {
1080 "untitled".to_string()
1081 } else {
1082 worktree_root_names.join(", ")
1083 }
1084 .into();
1085
1086 let theme = cx.theme();
1087
1088 ListItem::new(project_id as usize)
1089 .on_click(cx.listener(move |this, _, cx| {
1090 this.workspace.update(cx, |workspace, cx| {
1091 let app_state = workspace.app_state().clone();
1092 workspace::join_remote_project(project_id, host_user_id, app_state, cx)
1093 .detach_and_log_err(cx);
1094 });
1095 }))
1096 .start_slot(
1097 h_stack()
1098 .gap_1()
1099 .child(render_tree_branch(is_last, cx))
1100 .child(IconButton::new(0, Icon::Folder)),
1101 )
1102 .child(Label::new(project_name.clone()))
1103 .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
1104
1105 // enum JoinProject {}
1106 // enum JoinProjectTooltip {}
1107
1108 // let collab_theme = &theme.collab_panel;
1109 // let host_avatar_width = collab_theme
1110 // .contact_avatar
1111 // .width
1112 // .or(collab_theme.contact_avatar.height)
1113 // .unwrap_or(0.);
1114 // let tree_branch = collab_theme.tree_branch;
1115
1116 // let content =
1117 // MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
1118 // let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
1119 // let row = if is_current {
1120 // collab_theme
1121 // .project_row
1122 // .in_state(true)
1123 // .style_for(&mut Default::default())
1124 // } else {
1125 // collab_theme
1126 // .project_row
1127 // .in_state(is_selected)
1128 // .style_for(mouse_state)
1129 // };
1130
1131 // Flex::row()
1132 // .with_child(render_tree_branch(
1133 // tree_branch,
1134 // &row.name.text,
1135 // is_last,
1136 // vec2f(host_avatar_width, collab_theme.row_height),
1137 // cx.font_cache(),
1138 // ))
1139 // .with_child(
1140 // Svg::new("icons/file_icons/folder.svg")
1141 // .with_color(collab_theme.channel_hash.color)
1142 // .constrained()
1143 // .with_width(collab_theme.channel_hash.width)
1144 // .aligned()
1145 // .left(),
1146 // )
1147 // .with_child(
1148 // Label::new(project_name.clone(), row.name.text.clone())
1149 // .aligned()
1150 // .left()
1151 // .contained()
1152 // .with_style(row.name.container)
1153 // .flex(1., false),
1154 // )
1155 // .constrained()
1156 // .with_height(collab_theme.row_height)
1157 // .contained()
1158 // .with_style(row.container)
1159 // });
1160
1161 // if is_current {
1162 // return content.into_any();
1163 // }
1164
1165 // content
1166 // .with_cursor_style(CursorStyle::PointingHand)
1167 // .on_click(MouseButton::Left, move |_, this, cx| {
1168 // if let Some(workspace) = this.workspace.upgrade(cx) {
1169 // let app_state = workspace.read(cx).app_state().clone();
1170 // workspace::join_remote_project(project_id, host_user_id, app_state, cx)
1171 // .detach_and_log_err(cx);
1172 // }
1173 // })
1174 // .with_tooltip::<JoinProjectTooltip>(
1175 // project_id as usize,
1176 // format!("Open {}", project_name),
1177 // None,
1178 // theme.tooltip.clone(),
1179 // cx,
1180 // )
1181 // .into_any()
1182 }
1183
1184 fn render_participant_screen(
1185 &self,
1186 peer_id: Option<PeerId>,
1187 is_last: bool,
1188 cx: &mut ViewContext<Self>,
1189 ) -> impl IntoElement {
1190 let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
1191
1192 ListItem::new(("screen", id))
1193 .start_slot(
1194 h_stack()
1195 .gap_1()
1196 .child(render_tree_branch(is_last, cx))
1197 .child(IconButton::new(0, Icon::Screen)),
1198 )
1199 .child(Label::new("Screen"))
1200 .when_some(peer_id, |this, _| {
1201 this.on_click(cx.listener(move |this, _, cx| {
1202 this.workspace.update(cx, |workspace, cx| {
1203 workspace.open_shared_screen(peer_id.unwrap(), cx)
1204 });
1205 }))
1206 .tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx))
1207 })
1208 }
1209
1210 fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
1211 if let Some(_) = self.channel_editing_state.take() {
1212 self.channel_name_editor.update(cx, |editor, cx| {
1213 editor.set_text("", cx);
1214 });
1215 true
1216 } else {
1217 false
1218 }
1219 }
1220
1221 // fn render_contact_placeholder(
1222 // &self,
1223 // theme: &theme::CollabPanel,
1224 // is_selected: bool,
1225 // cx: &mut ViewContext<Self>,
1226 // ) -> AnyElement<Self> {
1227 // enum AddContacts {}
1228 // MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
1229 // let style = theme.list_empty_state.style_for(is_selected, state);
1230 // Flex::row()
1231 // .with_child(
1232 // Svg::new("icons/plus.svg")
1233 // .with_color(theme.list_empty_icon.color)
1234 // .constrained()
1235 // .with_width(theme.list_empty_icon.width)
1236 // .aligned()
1237 // .left(),
1238 // )
1239 // .with_child(
1240 // Label::new("Add a contact", style.text.clone())
1241 // .contained()
1242 // .with_style(theme.list_empty_label_container),
1243 // )
1244 // .align_children_center()
1245 // .contained()
1246 // .with_style(style.container)
1247 // .into_any()
1248 // })
1249 // .on_click(MouseButton::Left, |_, this, cx| {
1250 // this.toggle_contact_finder(cx);
1251 // })
1252 // .into_any()
1253 // }
1254
1255 fn render_channel_notes(
1256 &self,
1257 channel_id: ChannelId,
1258 cx: &mut ViewContext<Self>,
1259 ) -> impl IntoElement {
1260 ListItem::new("channel-notes")
1261 .on_click(cx.listener(move |this, _, cx| {
1262 this.open_channel_notes(channel_id, cx);
1263 }))
1264 .start_slot(
1265 h_stack()
1266 .gap_1()
1267 .child(render_tree_branch(false, cx))
1268 .child(IconButton::new(0, Icon::File)),
1269 )
1270 .child(div().h_7().w_full().child(Label::new("notes")))
1271 .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
1272 }
1273
1274 fn render_channel_chat(
1275 &self,
1276 channel_id: ChannelId,
1277 cx: &mut ViewContext<Self>,
1278 ) -> impl IntoElement {
1279 ListItem::new("channel-chat")
1280 .on_click(cx.listener(move |this, _, cx| {
1281 this.join_channel_chat(channel_id, cx);
1282 }))
1283 .start_slot(
1284 h_stack()
1285 .gap_1()
1286 .child(render_tree_branch(false, cx))
1287 .child(IconButton::new(0, Icon::MessageBubbles)),
1288 )
1289 .child(Label::new("chat"))
1290 .tooltip(move |cx| Tooltip::text("Open Chat", cx))
1291 }
1292
1293 // fn render_channel_invite(
1294 // channel: Arc<Channel>,
1295 // channel_store: ModelHandle<ChannelStore>,
1296 // theme: &theme::CollabPanel,
1297 // is_selected: bool,
1298 // cx: &mut ViewContext<Self>,
1299 // ) -> AnyElement<Self> {
1300 // enum Decline {}
1301 // enum Accept {}
1302
1303 // let channel_id = channel.id;
1304 // let is_invite_pending = channel_store
1305 // .read(cx)
1306 // .has_pending_channel_invite_response(&channel);
1307 // let button_spacing = theme.contact_button_spacing;
1308
1309 // Flex::row()
1310 // .with_child(
1311 // Svg::new("icons/hash.svg")
1312 // .with_color(theme.channel_hash.color)
1313 // .constrained()
1314 // .with_width(theme.channel_hash.width)
1315 // .aligned()
1316 // .left(),
1317 // )
1318 // .with_child(
1319 // Label::new(channel.name.clone(), theme.contact_username.text.clone())
1320 // .contained()
1321 // .with_style(theme.contact_username.container)
1322 // .aligned()
1323 // .left()
1324 // .flex(1., true),
1325 // )
1326 // .with_child(
1327 // MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
1328 // let button_style = if is_invite_pending {
1329 // &theme.disabled_button
1330 // } else {
1331 // theme.contact_button.style_for(mouse_state)
1332 // };
1333 // render_icon_button(button_style, "icons/x.svg").aligned()
1334 // })
1335 // .with_cursor_style(CursorStyle::PointingHand)
1336 // .on_click(MouseButton::Left, move |_, this, cx| {
1337 // this.respond_to_channel_invite(channel_id, false, cx);
1338 // })
1339 // .contained()
1340 // .with_margin_right(button_spacing),
1341 // )
1342 // .with_child(
1343 // MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
1344 // let button_style = if is_invite_pending {
1345 // &theme.disabled_button
1346 // } else {
1347 // theme.contact_button.style_for(mouse_state)
1348 // };
1349 // render_icon_button(button_style, "icons/check.svg")
1350 // .aligned()
1351 // .flex_float()
1352 // })
1353 // .with_cursor_style(CursorStyle::PointingHand)
1354 // .on_click(MouseButton::Left, move |_, this, cx| {
1355 // this.respond_to_channel_invite(channel_id, true, cx);
1356 // }),
1357 // )
1358 // .constrained()
1359 // .with_height(theme.row_height)
1360 // .contained()
1361 // .with_style(
1362 // *theme
1363 // .contact_row
1364 // .in_state(is_selected)
1365 // .style_for(&mut Default::default()),
1366 // )
1367 // .with_padding_left(
1368 // theme.contact_row.default_style().padding.left + theme.channel_indent,
1369 // )
1370 // .into_any()
1371 // }
1372
1373 fn has_subchannels(&self, ix: usize) -> bool {
1374 self.entries.get(ix).map_or(false, |entry| {
1375 if let ListEntry::Channel { has_children, .. } = entry {
1376 *has_children
1377 } else {
1378 false
1379 }
1380 })
1381 }
1382
1383 fn deploy_channel_context_menu(
1384 &mut self,
1385 position: Point<Pixels>,
1386 channel_id: ChannelId,
1387 ix: usize,
1388 cx: &mut ViewContext<Self>,
1389 ) {
1390 let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
1391 self.channel_store
1392 .read(cx)
1393 .channel_for_id(clipboard.channel_id)
1394 .map(|channel| channel.name.clone())
1395 });
1396 let this = cx.view().clone();
1397
1398 let context_menu = ContextMenu::build(cx, |mut context_menu, cx| {
1399 if self.has_subchannels(ix) {
1400 let expand_action_name = if self.is_channel_collapsed(channel_id) {
1401 "Expand Subchannels"
1402 } else {
1403 "Collapse Subchannels"
1404 };
1405 context_menu = context_menu.entry(
1406 expand_action_name,
1407 cx.handler_for(&this, move |this, cx| {
1408 this.toggle_channel_collapsed(channel_id, cx)
1409 }),
1410 );
1411 }
1412
1413 context_menu = context_menu
1414 .entry(
1415 "Open Notes",
1416 cx.handler_for(&this, move |this, cx| {
1417 this.open_channel_notes(channel_id, cx)
1418 }),
1419 )
1420 .entry(
1421 "Open Chat",
1422 cx.handler_for(&this, move |this, cx| {
1423 this.join_channel_chat(channel_id, cx)
1424 }),
1425 )
1426 .entry(
1427 "Copy Channel Link",
1428 cx.handler_for(&this, move |this, cx| {
1429 this.copy_channel_link(channel_id, cx)
1430 }),
1431 );
1432
1433 if self.channel_store.read(cx).is_channel_admin(channel_id) {
1434 context_menu = context_menu
1435 .separator()
1436 .entry(
1437 "New Subchannel",
1438 cx.handler_for(&this, move |this, cx| this.new_subchannel(channel_id, cx)),
1439 )
1440 .entry(
1441 "Rename",
1442 cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)),
1443 )
1444 .entry(
1445 "Move this channel",
1446 cx.handler_for(&this, move |this, cx| {
1447 this.start_move_channel(channel_id, cx)
1448 }),
1449 );
1450
1451 if let Some(channel_name) = clipboard_channel_name {
1452 context_menu = context_menu.separator().entry(
1453 format!("Move '#{}' here", channel_name),
1454 cx.handler_for(&this, move |this, cx| {
1455 this.move_channel_on_clipboard(channel_id, cx)
1456 }),
1457 );
1458 }
1459
1460 context_menu = context_menu
1461 .separator()
1462 .entry(
1463 "Invite Members",
1464 cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)),
1465 )
1466 .entry(
1467 "Manage Members",
1468 cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
1469 )
1470 .entry(
1471 "Delete",
1472 cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
1473 );
1474 }
1475
1476 context_menu
1477 });
1478
1479 cx.focus_view(&context_menu);
1480 let subscription =
1481 cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
1482 if this.context_menu.as_ref().is_some_and(|context_menu| {
1483 context_menu.0.focus_handle(cx).contains_focused(cx)
1484 }) {
1485 cx.focus_self();
1486 }
1487 this.context_menu.take();
1488 cx.notify();
1489 });
1490 self.context_menu = Some((context_menu, position, subscription));
1491
1492 cx.notify();
1493 }
1494
1495 fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
1496 if self.take_editing_state(cx) {
1497 cx.focus_view(&self.filter_editor);
1498 } else {
1499 self.filter_editor.update(cx, |editor, cx| {
1500 if editor.buffer().read(cx).len(cx) > 0 {
1501 editor.set_text("", cx);
1502 }
1503 });
1504 }
1505
1506 self.update_entries(false, cx);
1507 }
1508
1509 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
1510 let ix = self.selection.map_or(0, |ix| ix + 1);
1511 if ix < self.entries.len() {
1512 self.selection = Some(ix);
1513 }
1514
1515 if let Some(ix) = self.selection {
1516 self.scroll_to_item(ix)
1517 }
1518 cx.notify();
1519 }
1520
1521 fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
1522 let ix = self.selection.take().unwrap_or(0);
1523 if ix > 0 {
1524 self.selection = Some(ix - 1);
1525 }
1526
1527 if let Some(ix) = self.selection {
1528 self.scroll_to_item(ix)
1529 }
1530 cx.notify();
1531 }
1532
1533 fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
1534 if self.confirm_channel_edit(cx) {
1535 return;
1536 }
1537
1538 if let Some(selection) = self.selection {
1539 if let Some(entry) = self.entries.get(selection) {
1540 match entry {
1541 ListEntry::Header(section) => match section {
1542 Section::ActiveCall => Self::leave_call(cx),
1543 Section::Channels => self.new_root_channel(cx),
1544 Section::Contacts => self.toggle_contact_finder(cx),
1545 Section::ContactRequests
1546 | Section::Online
1547 | Section::Offline
1548 | Section::ChannelInvites => {
1549 self.toggle_section_expanded(*section, cx);
1550 }
1551 },
1552 ListEntry::Contact { contact, calling } => {
1553 if contact.online && !contact.busy && !calling {
1554 self.call(contact.user.id, cx);
1555 }
1556 }
1557 // ListEntry::ParticipantProject {
1558 // project_id,
1559 // host_user_id,
1560 // ..
1561 // } => {
1562 // if let Some(workspace) = self.workspace.upgrade(cx) {
1563 // let app_state = workspace.read(cx).app_state().clone();
1564 // workspace::join_remote_project(
1565 // *project_id,
1566 // *host_user_id,
1567 // app_state,
1568 // cx,
1569 // )
1570 // .detach_and_log_err(cx);
1571 // }
1572 // }
1573 // ListEntry::ParticipantScreen { peer_id, .. } => {
1574 // let Some(peer_id) = peer_id else {
1575 // return;
1576 // };
1577 // if let Some(workspace) = self.workspace.upgrade(cx) {
1578 // workspace.update(cx, |workspace, cx| {
1579 // workspace.open_shared_screen(*peer_id, cx)
1580 // });
1581 // }
1582 // }
1583 ListEntry::Channel { channel, .. } => {
1584 let is_active = maybe!({
1585 let call_channel = ActiveCall::global(cx)
1586 .read(cx)
1587 .room()?
1588 .read(cx)
1589 .channel_id()?;
1590
1591 Some(call_channel == channel.id)
1592 })
1593 .unwrap_or(false);
1594 if is_active {
1595 self.open_channel_notes(channel.id, cx)
1596 } else {
1597 self.join_channel(channel.id, cx)
1598 }
1599 }
1600 ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
1601 _ => {}
1602 }
1603 }
1604 }
1605 }
1606
1607 fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
1608 if self.channel_editing_state.is_some() {
1609 self.channel_name_editor.update(cx, |editor, cx| {
1610 editor.insert(" ", cx);
1611 });
1612 }
1613 }
1614
1615 fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
1616 if let Some(editing_state) = &mut self.channel_editing_state {
1617 match editing_state {
1618 ChannelEditingState::Create {
1619 location,
1620 pending_name,
1621 ..
1622 } => {
1623 if pending_name.is_some() {
1624 return false;
1625 }
1626 let channel_name = self.channel_name_editor.read(cx).text(cx);
1627
1628 *pending_name = Some(channel_name.clone());
1629
1630 self.channel_store
1631 .update(cx, |channel_store, cx| {
1632 channel_store.create_channel(&channel_name, *location, cx)
1633 })
1634 .detach();
1635 cx.notify();
1636 }
1637 ChannelEditingState::Rename {
1638 location,
1639 pending_name,
1640 } => {
1641 if pending_name.is_some() {
1642 return false;
1643 }
1644 let channel_name = self.channel_name_editor.read(cx).text(cx);
1645 *pending_name = Some(channel_name.clone());
1646
1647 self.channel_store
1648 .update(cx, |channel_store, cx| {
1649 channel_store.rename(*location, &channel_name, cx)
1650 })
1651 .detach();
1652 cx.notify();
1653 }
1654 }
1655 cx.focus_self();
1656 true
1657 } else {
1658 false
1659 }
1660 }
1661
1662 fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
1663 if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
1664 self.collapsed_sections.remove(ix);
1665 } else {
1666 self.collapsed_sections.push(section);
1667 }
1668 self.update_entries(false, cx);
1669 }
1670
1671 fn collapse_selected_channel(
1672 &mut self,
1673 _: &CollapseSelectedChannel,
1674 cx: &mut ViewContext<Self>,
1675 ) {
1676 let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
1677 return;
1678 };
1679
1680 if self.is_channel_collapsed(channel_id) {
1681 return;
1682 }
1683
1684 self.toggle_channel_collapsed(channel_id, cx);
1685 }
1686
1687 fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
1688 let Some(id) = self.selected_channel().map(|channel| channel.id) else {
1689 return;
1690 };
1691
1692 if !self.is_channel_collapsed(id) {
1693 return;
1694 }
1695
1696 self.toggle_channel_collapsed(id, cx)
1697 }
1698
1699 // fn toggle_channel_collapsed_action(
1700 // &mut self,
1701 // action: &ToggleCollapse,
1702 // cx: &mut ViewContext<Self>,
1703 // ) {
1704 // self.toggle_channel_collapsed(action.location, cx);
1705 // }
1706
1707 fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1708 match self.collapsed_channels.binary_search(&channel_id) {
1709 Ok(ix) => {
1710 self.collapsed_channels.remove(ix);
1711 }
1712 Err(ix) => {
1713 self.collapsed_channels.insert(ix, channel_id);
1714 }
1715 };
1716 self.serialize(cx);
1717 self.update_entries(true, cx);
1718 cx.notify();
1719 cx.focus_self();
1720 }
1721
1722 fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
1723 self.collapsed_channels.binary_search(&channel_id).is_ok()
1724 }
1725
1726 fn leave_call(cx: &mut ViewContext<Self>) {
1727 ActiveCall::global(cx)
1728 .update(cx, |call, cx| call.hang_up(cx))
1729 .detach_and_log_err(cx);
1730 }
1731
1732 fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
1733 if let Some(workspace) = self.workspace.upgrade() {
1734 workspace.update(cx, |workspace, cx| {
1735 workspace.toggle_modal(cx, |cx| {
1736 let mut finder = ContactFinder::new(self.user_store.clone(), cx);
1737 finder.set_query(self.filter_editor.read(cx).text(cx), cx);
1738 finder
1739 });
1740 });
1741 }
1742 }
1743
1744 fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
1745 self.channel_editing_state = Some(ChannelEditingState::Create {
1746 location: None,
1747 pending_name: None,
1748 });
1749 self.update_entries(false, cx);
1750 self.select_channel_editor();
1751 cx.focus_view(&self.channel_name_editor);
1752 cx.notify();
1753 }
1754
1755 fn select_channel_editor(&mut self) {
1756 self.selection = self.entries.iter().position(|entry| match entry {
1757 ListEntry::ChannelEditor { .. } => true,
1758 _ => false,
1759 });
1760 }
1761
1762 fn new_subchannel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1763 self.collapsed_channels
1764 .retain(|channel| *channel != channel_id);
1765 self.channel_editing_state = Some(ChannelEditingState::Create {
1766 location: Some(channel_id),
1767 pending_name: None,
1768 });
1769 self.update_entries(false, cx);
1770 self.select_channel_editor();
1771 cx.focus_view(&self.channel_name_editor);
1772 cx.notify();
1773 }
1774
1775 fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1776 self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
1777 }
1778
1779 fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1780 self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
1781 }
1782
1783 fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
1784 if let Some(channel) = self.selected_channel() {
1785 self.remove_channel(channel.id, cx)
1786 }
1787 }
1788
1789 fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
1790 if let Some(channel) = self.selected_channel() {
1791 self.rename_channel(channel.id, cx);
1792 }
1793 }
1794
1795 fn rename_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1796 let channel_store = self.channel_store.read(cx);
1797 if !channel_store.is_channel_admin(channel_id) {
1798 return;
1799 }
1800 if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() {
1801 self.channel_editing_state = Some(ChannelEditingState::Rename {
1802 location: channel_id,
1803 pending_name: None,
1804 });
1805 self.channel_name_editor.update(cx, |editor, cx| {
1806 editor.set_text(channel.name.clone(), cx);
1807 editor.select_all(&Default::default(), cx);
1808 });
1809 cx.focus_view(&self.channel_name_editor);
1810 self.update_entries(false, cx);
1811 self.select_channel_editor();
1812 }
1813 }
1814
1815 fn start_move_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1816 self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
1817 }
1818
1819 fn start_move_selected_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1820 if let Some(channel) = self.selected_channel() {
1821 self.channel_clipboard = Some(ChannelMoveClipboard {
1822 channel_id: channel.id,
1823 })
1824 }
1825 }
1826
1827 fn move_channel_on_clipboard(
1828 &mut self,
1829 to_channel_id: ChannelId,
1830 cx: &mut ViewContext<CollabPanel>,
1831 ) {
1832 if let Some(clipboard) = self.channel_clipboard.take() {
1833 self.channel_store.update(cx, |channel_store, cx| {
1834 channel_store
1835 .move_channel(clipboard.channel_id, Some(to_channel_id), cx)
1836 .detach_and_log_err(cx)
1837 })
1838 }
1839 }
1840
1841 fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1842 if let Some(workspace) = self.workspace.upgrade() {
1843 ChannelView::open(channel_id, workspace, cx).detach();
1844 }
1845 }
1846
1847 fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
1848 let Some(channel) = self.selected_channel() else {
1849 return;
1850 };
1851 let Some(bounds) = self
1852 .selection
1853 .and_then(|ix| self.list_state.bounds_for_item(ix))
1854 else {
1855 return;
1856 };
1857
1858 self.deploy_channel_context_menu(bounds.center(), channel.id, self.selection.unwrap(), cx);
1859 cx.stop_propagation();
1860 }
1861
1862 fn selected_channel(&self) -> Option<&Arc<Channel>> {
1863 self.selection
1864 .and_then(|ix| self.entries.get(ix))
1865 .and_then(|entry| match entry {
1866 ListEntry::Channel { channel, .. } => Some(channel),
1867 _ => None,
1868 })
1869 }
1870
1871 fn show_channel_modal(
1872 &mut self,
1873 channel_id: ChannelId,
1874 mode: channel_modal::Mode,
1875 cx: &mut ViewContext<Self>,
1876 ) {
1877 let workspace = self.workspace.clone();
1878 let user_store = self.user_store.clone();
1879 let channel_store = self.channel_store.clone();
1880 let members = self.channel_store.update(cx, |channel_store, cx| {
1881 channel_store.get_channel_member_details(channel_id, cx)
1882 });
1883
1884 cx.spawn(|_, mut cx| async move {
1885 let members = members.await?;
1886 workspace.update(&mut cx, |workspace, cx| {
1887 workspace.toggle_modal(cx, |cx| {
1888 ChannelModal::new(
1889 user_store.clone(),
1890 channel_store.clone(),
1891 channel_id,
1892 mode,
1893 members,
1894 cx,
1895 )
1896 });
1897 })
1898 })
1899 .detach();
1900 }
1901
1902 // fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
1903 // self.remove_channel(action.channel_id, cx)
1904 // }
1905
1906 fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1907 let channel_store = self.channel_store.clone();
1908 if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
1909 let prompt_message = format!(
1910 "Are you sure you want to remove the channel \"{}\"?",
1911 channel.name
1912 );
1913 let mut answer =
1914 cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
1915 let window = cx.window();
1916 cx.spawn(|this, mut cx| async move {
1917 if answer.await? == 0 {
1918 channel_store
1919 .update(&mut cx, |channels, _| channels.remove_channel(channel_id))?
1920 .await
1921 .notify_async_err(&mut cx);
1922 this.update(&mut cx, |_, cx| cx.focus_self()).ok();
1923 }
1924 anyhow::Ok(())
1925 })
1926 .detach();
1927 }
1928 }
1929
1930 // // Should move to the filter editor if clicking on it
1931 // // Should move selection to the channel editor if activating it
1932
1933 fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
1934 let user_store = self.user_store.clone();
1935 let prompt_message = format!(
1936 "Are you sure you want to remove \"{}\" from your contacts?",
1937 github_login
1938 );
1939 let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
1940 let window = cx.window();
1941 cx.spawn(|_, mut cx| async move {
1942 if answer.await? == 0 {
1943 user_store
1944 .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))?
1945 .await
1946 .notify_async_err(&mut cx);
1947 }
1948 anyhow::Ok(())
1949 })
1950 .detach_and_log_err(cx);
1951 }
1952
1953 fn respond_to_contact_request(
1954 &mut self,
1955 user_id: u64,
1956 accept: bool,
1957 cx: &mut ViewContext<Self>,
1958 ) {
1959 self.user_store
1960 .update(cx, |store, cx| {
1961 store.respond_to_contact_request(user_id, accept, cx)
1962 })
1963 .detach_and_log_err(cx);
1964 }
1965
1966 // fn respond_to_channel_invite(
1967 // &mut self,
1968 // channel_id: u64,
1969 // accept: bool,
1970 // cx: &mut ViewContext<Self>,
1971 // ) {
1972 // self.channel_store
1973 // .update(cx, |store, cx| {
1974 // store.respond_to_channel_invite(channel_id, accept, cx)
1975 // })
1976 // .detach();
1977 // }
1978
1979 fn call(&mut self, recipient_user_id: u64, cx: &mut ViewContext<Self>) {
1980 ActiveCall::global(cx)
1981 .update(cx, |call, cx| {
1982 call.invite(recipient_user_id, Some(self.project.clone()), cx)
1983 })
1984 .detach_and_log_err(cx);
1985 }
1986
1987 fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
1988 let Some(workspace) = self.workspace.upgrade() else {
1989 return;
1990 };
1991 let Some(handle) = cx.window_handle().downcast::<Workspace>() else {
1992 return;
1993 };
1994 workspace::join_channel(
1995 channel_id,
1996 workspace.read(cx).app_state().clone(),
1997 Some(handle),
1998 cx,
1999 )
2000 .detach_and_log_err(cx)
2001 }
2002
2003 fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2004 let Some(workspace) = self.workspace.upgrade() else {
2005 return;
2006 };
2007 cx.window_context().defer(move |cx| {
2008 workspace.update(cx, |workspace, cx| {
2009 if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
2010 panel.update(cx, |panel, cx| {
2011 panel
2012 .select_channel(channel_id, None, cx)
2013 .detach_and_log_err(cx);
2014 });
2015 }
2016 });
2017 });
2018 }
2019
2020 fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2021 let channel_store = self.channel_store.read(cx);
2022 let Some(channel) = channel_store.channel_for_id(channel_id) else {
2023 return;
2024 };
2025 let item = ClipboardItem::new(channel.link());
2026 cx.write_to_clipboard(item)
2027 }
2028
2029 fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div {
2030 v_stack()
2031 .items_center()
2032 .child(v_stack().gap_6().p_4()
2033 .child(
2034 Label::new("Work with your team in realtime with collaborative editing, voice, shared notes and more.")
2035 )
2036 .child(v_stack().gap_2()
2037
2038 .child(
2039 Button::new("sign_in", "Sign in")
2040 .icon_color(Color::Muted)
2041 .icon(Icon::Github)
2042 .icon_position(IconPosition::Start)
2043 .style(ButtonStyle::Filled)
2044 .full_width()
2045 .on_click(cx.listener(
2046 |this, _, cx| {
2047 let client = this.client.clone();
2048 cx.spawn(|_, mut cx| async move {
2049 client
2050 .authenticate_and_connect(true, &cx)
2051 .await
2052 .notify_async_err(&mut cx);
2053 })
2054 .detach()
2055 },
2056 )))
2057 .child(
2058 div().flex().w_full().items_center().child(
2059 Label::new("Sign in to enable collaboration.")
2060 .color(Color::Muted)
2061 .size(LabelSize::Small)
2062 )),
2063 ))
2064 }
2065
2066 fn render_list_entry(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
2067 let entry = &self.entries[ix];
2068
2069 let is_selected = self.selection == Some(ix);
2070 match entry {
2071 ListEntry::Header(section) => {
2072 let is_collapsed = self.collapsed_sections.contains(section);
2073 self.render_header(*section, is_selected, is_collapsed, cx)
2074 .into_any_element()
2075 }
2076 ListEntry::Contact { contact, calling } => self
2077 .render_contact(contact, *calling, is_selected, cx)
2078 .into_any_element(),
2079 ListEntry::ContactPlaceholder => self
2080 .render_contact_placeholder(is_selected, cx)
2081 .into_any_element(),
2082 ListEntry::IncomingRequest(user) => self
2083 .render_contact_request(user, true, is_selected, cx)
2084 .into_any_element(),
2085 ListEntry::OutgoingRequest(user) => self
2086 .render_contact_request(user, false, is_selected, cx)
2087 .into_any_element(),
2088 ListEntry::Channel {
2089 channel,
2090 depth,
2091 has_children,
2092 } => self
2093 .render_channel(channel, *depth, *has_children, is_selected, ix, cx)
2094 .into_any_element(),
2095 ListEntry::ChannelEditor { depth } => {
2096 self.render_channel_editor(*depth, cx).into_any_element()
2097 }
2098 ListEntry::CallParticipant {
2099 user,
2100 peer_id,
2101 is_pending,
2102 } => self
2103 .render_call_participant(user, *peer_id, *is_pending, cx)
2104 .into_any_element(),
2105 ListEntry::ParticipantProject {
2106 project_id,
2107 worktree_root_names,
2108 host_user_id,
2109 is_last,
2110 } => self
2111 .render_participant_project(
2112 *project_id,
2113 &worktree_root_names,
2114 *host_user_id,
2115 *is_last,
2116 cx,
2117 )
2118 .into_any_element(),
2119 ListEntry::ParticipantScreen { peer_id, is_last } => self
2120 .render_participant_screen(*peer_id, *is_last, cx)
2121 .into_any_element(),
2122 ListEntry::ChannelNotes { channel_id } => self
2123 .render_channel_notes(*channel_id, cx)
2124 .into_any_element(),
2125 ListEntry::ChannelChat { channel_id } => {
2126 self.render_channel_chat(*channel_id, cx).into_any_element()
2127 }
2128 }
2129 }
2130
2131 fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
2132 v_stack()
2133 .size_full()
2134 .child(list(self.list_state.clone()).full())
2135 .child(
2136 v_stack().p_2().child(
2137 v_stack()
2138 .border_primary(cx)
2139 .border_t()
2140 .child(self.filter_editor.clone()),
2141 ),
2142 )
2143 }
2144
2145 fn render_header(
2146 &self,
2147 section: Section,
2148 is_selected: bool,
2149 is_collapsed: bool,
2150 cx: &ViewContext<Self>,
2151 ) -> impl IntoElement {
2152 let mut channel_link = None;
2153 let mut channel_tooltip_text = None;
2154 let mut channel_icon = None;
2155 // let mut is_dragged_over = false;
2156
2157 let text = match section {
2158 Section::ActiveCall => {
2159 let channel_name = maybe!({
2160 let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
2161
2162 let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
2163
2164 channel_link = Some(channel.link());
2165 (channel_icon, channel_tooltip_text) = match channel.visibility {
2166 proto::ChannelVisibility::Public => {
2167 (Some("icons/public.svg"), Some("Copy public channel link."))
2168 }
2169 proto::ChannelVisibility::Members => {
2170 (Some("icons/hash.svg"), Some("Copy private channel link."))
2171 }
2172 };
2173
2174 Some(channel.name.as_ref())
2175 });
2176
2177 if let Some(name) = channel_name {
2178 SharedString::from(format!("{}", name))
2179 } else {
2180 SharedString::from("Current Call")
2181 }
2182 }
2183 Section::ContactRequests => SharedString::from("Requests"),
2184 Section::Contacts => SharedString::from("Contacts"),
2185 Section::Channels => SharedString::from("Channels"),
2186 Section::ChannelInvites => SharedString::from("Invites"),
2187 Section::Online => SharedString::from("Online"),
2188 Section::Offline => SharedString::from("Offline"),
2189 };
2190
2191 let button = match section {
2192 Section::ActiveCall => channel_link.map(|channel_link| {
2193 let channel_link_copy = channel_link.clone();
2194 IconButton::new("channel-link", Icon::Copy)
2195 .icon_size(IconSize::Small)
2196 .size(ButtonSize::None)
2197 .visible_on_hover("section-header")
2198 .on_click(move |_, cx| {
2199 let item = ClipboardItem::new(channel_link_copy.clone());
2200 cx.write_to_clipboard(item)
2201 })
2202 .tooltip(|cx| Tooltip::text("Copy channel link", cx))
2203 .into_any_element()
2204 }),
2205 Section::Contacts => Some(
2206 div()
2207 .border_1()
2208 .border_color(gpui::red())
2209 .child(
2210 IconButton::new("add-contact", Icon::Plus)
2211 .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
2212 .tooltip(|cx| Tooltip::text("Search for new contact", cx)),
2213 )
2214 .into_any_element(),
2215 ),
2216 Section::Channels => Some(
2217 IconButton::new("add-channel", Icon::Plus)
2218 .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
2219 .tooltip(|cx| Tooltip::text("Create a channel", cx))
2220 .into_any_element(),
2221 ),
2222 _ => None,
2223 };
2224
2225 let can_collapse = match section {
2226 Section::ActiveCall | Section::Channels | Section::Contacts => false,
2227 Section::ChannelInvites
2228 | Section::ContactRequests
2229 | Section::Online
2230 | Section::Offline => true,
2231 };
2232
2233 h_stack()
2234 .w_full()
2235 .group("section-header")
2236 .child(
2237 ListHeader::new(text)
2238 .when(can_collapse, |header| {
2239 header.toggle(Some(!is_collapsed)).on_toggle(cx.listener(
2240 move |this, event, cx| {
2241 this.toggle_section_expanded(section, cx);
2242 },
2243 ))
2244 })
2245 .inset(true)
2246 .end_slot::<AnyElement>(button)
2247 .selected(is_selected),
2248 )
2249 .when(section == Section::Channels, |el| {
2250 el.drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
2251 .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
2252 this.channel_store
2253 .update(cx, |channel_store, cx| {
2254 channel_store.move_channel(dragged_channel.id, None, cx)
2255 })
2256 .detach_and_log_err(cx)
2257 }))
2258 })
2259 }
2260
2261 fn render_contact(
2262 &self,
2263 contact: &Contact,
2264 calling: bool,
2265 is_selected: bool,
2266 cx: &mut ViewContext<Self>,
2267 ) -> impl IntoElement {
2268 let online = contact.online;
2269 let busy = contact.busy || calling;
2270 let user_id = contact.user.id;
2271 let github_login = SharedString::from(contact.user.github_login.clone());
2272 let mut item =
2273 ListItem::new(github_login.clone())
2274 .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
2275 .child(
2276 h_stack()
2277 .w_full()
2278 .justify_between()
2279 .child(Label::new(github_login.clone()))
2280 .when(calling, |el| {
2281 el.child(Label::new("Calling").color(Color::Muted))
2282 })
2283 .when(!calling, |el| {
2284 el.child(
2285 IconButton::new("remove_contact", Icon::Close)
2286 .icon_color(Color::Muted)
2287 .visible_on_hover("")
2288 .tooltip(|cx| Tooltip::text("Remove Contact", cx))
2289 .on_click(cx.listener({
2290 let github_login = github_login.clone();
2291 move |this, _, cx| {
2292 this.remove_contact(user_id, &github_login, cx);
2293 }
2294 })),
2295 )
2296 }),
2297 )
2298 .start_slot(
2299 // todo!() handle contacts with no avatar
2300 Avatar::new(contact.user.avatar_uri.clone())
2301 .availability_indicator(if online { Some(!busy) } else { None }),
2302 )
2303 .when(online && !busy, |el| {
2304 el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
2305 });
2306
2307 div()
2308 .id(github_login.clone())
2309 .group("")
2310 .child(item)
2311 .tooltip(move |cx| {
2312 let text = if !online {
2313 format!(" {} is offline", &github_login)
2314 } else if busy {
2315 format!(" {} is on a call", &github_login)
2316 } else {
2317 let room = ActiveCall::global(cx).read(cx).room();
2318 if room.is_some() {
2319 format!("Invite {} to join call", &github_login)
2320 } else {
2321 format!("Call {}", &github_login)
2322 }
2323 };
2324 Tooltip::text(text, cx)
2325 })
2326 }
2327
2328 fn render_contact_request(
2329 &self,
2330 user: &Arc<User>,
2331 is_incoming: bool,
2332 is_selected: bool,
2333 cx: &mut ViewContext<Self>,
2334 ) -> impl IntoElement {
2335 let github_login = SharedString::from(user.github_login.clone());
2336 let user_id = user.id;
2337 let is_contact_request_pending = self.user_store.read(cx).is_contact_request_pending(&user);
2338 let color = if is_contact_request_pending {
2339 Color::Muted
2340 } else {
2341 Color::Default
2342 };
2343
2344 let controls = if is_incoming {
2345 vec![
2346 IconButton::new("remove_contact", Icon::Close)
2347 .on_click(cx.listener(move |this, _, cx| {
2348 this.respond_to_contact_request(user_id, false, cx);
2349 }))
2350 .icon_color(color)
2351 .tooltip(|cx| Tooltip::text("Decline invite", cx)),
2352 IconButton::new("remove_contact", Icon::Check)
2353 .on_click(cx.listener(move |this, _, cx| {
2354 this.respond_to_contact_request(user_id, true, cx);
2355 }))
2356 .icon_color(color)
2357 .tooltip(|cx| Tooltip::text("Accept invite", cx)),
2358 ]
2359 } else {
2360 let github_login = github_login.clone();
2361 vec![IconButton::new("remove_contact", Icon::Close)
2362 .on_click(cx.listener(move |this, _, cx| {
2363 this.remove_contact(user_id, &github_login, cx);
2364 }))
2365 .icon_color(color)
2366 .tooltip(|cx| Tooltip::text("Cancel invite", cx))]
2367 };
2368
2369 ListItem::new(github_login.clone())
2370 .child(
2371 h_stack()
2372 .w_full()
2373 .justify_between()
2374 .child(Label::new(github_login.clone()))
2375 .child(h_stack().children(controls)),
2376 )
2377 .start_slot(Avatar::new(user.avatar_uri.clone()))
2378 }
2379
2380 fn render_contact_placeholder(
2381 &self,
2382 is_selected: bool,
2383 cx: &mut ViewContext<Self>,
2384 ) -> impl IntoElement {
2385 ListItem::new("contact-placeholder")
2386 .child(IconElement::new(Icon::Plus))
2387 .child(Label::new("Add a Contact"))
2388 .selected(is_selected)
2389 .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
2390 }
2391
2392 fn render_channel(
2393 &self,
2394 channel: &Channel,
2395 depth: usize,
2396 has_children: bool,
2397 is_selected: bool,
2398 ix: usize,
2399 cx: &mut ViewContext<Self>,
2400 ) -> impl IntoElement {
2401 let channel_id = channel.id;
2402
2403 let is_active = maybe!({
2404 let call_channel = ActiveCall::global(cx)
2405 .read(cx)
2406 .room()?
2407 .read(cx)
2408 .channel_id()?;
2409 Some(call_channel == channel_id)
2410 })
2411 .unwrap_or(false);
2412 let is_public = self
2413 .channel_store
2414 .read(cx)
2415 .channel_for_id(channel_id)
2416 .map(|channel| channel.visibility)
2417 == Some(proto::ChannelVisibility::Public);
2418 let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
2419 let disclosed =
2420 has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
2421
2422 let has_messages_notification = channel.unseen_message_id.is_some();
2423 let has_notes_notification = channel.unseen_note_version.is_some();
2424
2425 const FACEPILE_LIMIT: usize = 3;
2426 let participants = self.channel_store.read(cx).channel_participants(channel_id);
2427
2428 let face_pile = if !participants.is_empty() {
2429 let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2430 let user = &participants[0];
2431
2432 let result = FacePile {
2433 faces: participants
2434 .iter()
2435 .filter_map(|user| {
2436 Some(Avatar::new(user.avatar_uri.clone()).into_any_element())
2437 })
2438 .take(FACEPILE_LIMIT)
2439 .chain(if extra_count > 0 {
2440 // todo!() @nate - this label looks wrong.
2441 Some(Label::new(format!("+{}", extra_count)).into_any_element())
2442 } else {
2443 None
2444 })
2445 .collect::<SmallVec<_>>(),
2446 };
2447
2448 Some(result)
2449 } else {
2450 None
2451 };
2452
2453 let width = self.width.unwrap_or(px(240.));
2454
2455 div()
2456 .id(channel_id as usize)
2457 .group("")
2458 .flex()
2459 .w_full()
2460 .on_drag(channel.clone(), move |channel, cx| {
2461 cx.build_view(|cx| DraggedChannelView {
2462 channel: channel.clone(),
2463 width,
2464 })
2465 })
2466 .drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
2467 .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
2468 this.channel_store
2469 .update(cx, |channel_store, cx| {
2470 channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
2471 })
2472 .detach_and_log_err(cx)
2473 }))
2474 .child(
2475 ListItem::new(channel_id as usize)
2476 // Offset the indent depth by one to give us room to show the disclosure.
2477 .indent_level(depth + 1)
2478 .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to step over the disclosure toggle
2479 .selected(is_selected || is_active)
2480 .toggle(disclosed)
2481 .on_toggle(
2482 cx.listener(move |this, _, cx| {
2483 this.toggle_channel_collapsed(channel_id, cx)
2484 }),
2485 )
2486 .on_click(cx.listener(move |this, _, cx| {
2487 if this.drag_target_channel == ChannelDragTarget::None {
2488 if is_active {
2489 this.open_channel_notes(channel_id, cx)
2490 } else {
2491 this.join_channel(channel_id, cx)
2492 }
2493 }
2494 }))
2495 .on_secondary_mouse_down(cx.listener(
2496 move |this, event: &MouseDownEvent, cx| {
2497 this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
2498 },
2499 ))
2500 .start_slot(
2501 IconElement::new(if is_public { Icon::Public } else { Icon::Hash })
2502 .size(IconSize::Small)
2503 .color(Color::Muted),
2504 )
2505 .child(
2506 h_stack()
2507 .id(channel_id as usize)
2508 .child(Label::new(channel.name.clone()))
2509 .children(face_pile.map(|face_pile| face_pile.render(cx))),
2510 )
2511 .end_slot(
2512 h_stack()
2513 .child(
2514 IconButton::new("channel_chat", Icon::MessageBubbles)
2515 .icon_color(if has_messages_notification {
2516 Color::Default
2517 } else {
2518 Color::Muted
2519 })
2520 .when(!has_messages_notification, |this| {
2521 this.visible_on_hover("")
2522 })
2523 .on_click(cx.listener(move |this, _, cx| {
2524 this.join_channel_chat(channel_id, cx)
2525 }))
2526 .tooltip(|cx| Tooltip::text("Open channel chat", cx)),
2527 )
2528 .child(
2529 IconButton::new("channel_notes", Icon::File)
2530 .icon_color(if has_notes_notification {
2531 Color::Default
2532 } else {
2533 Color::Muted
2534 })
2535 .when(!has_notes_notification, |this| this.visible_on_hover(""))
2536 .on_click(cx.listener(move |this, _, cx| {
2537 this.open_channel_notes(channel_id, cx)
2538 }))
2539 .tooltip(|cx| Tooltip::text("Open channel notes", cx)),
2540 ),
2541 ),
2542 )
2543 .tooltip(|cx| Tooltip::text("Join channel", cx))
2544
2545 // let channel_id = channel.id;
2546 // let collab_theme = &theme.collab_panel;
2547 // let is_public = self
2548 // .channel_store
2549 // .read(cx)
2550 // .channel_for_id(channel_id)
2551 // .map(|channel| channel.visibility)
2552 // == Some(proto::ChannelVisibility::Public);
2553 // let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
2554 // let disclosed =
2555 // has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
2556
2557 // enum ChannelCall {}
2558 // enum ChannelNote {}
2559 // enum NotesTooltip {}
2560 // enum ChatTooltip {}
2561 // enum ChannelTooltip {}
2562
2563 // let mut is_dragged_over = false;
2564 // if cx
2565 // .global::<DragAndDrop<Workspace>>()
2566 // .currently_dragged::<Channel>(cx.window())
2567 // .is_some()
2568 // && self.drag_target_channel == ChannelDragTarget::Channel(channel_id)
2569 // {
2570 // is_dragged_over = true;
2571 // }
2572
2573 // let has_messages_notification = channel.unseen_message_id.is_some();
2574
2575 // MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
2576 // let row_hovered = state.hovered();
2577
2578 // let mut select_state = |interactive: &Interactive<ContainerStyle>| {
2579 // if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() {
2580 // interactive.clicked.as_ref().unwrap().clone()
2581 // } else if state.hovered() || other_selected {
2582 // interactive
2583 // .hovered
2584 // .as_ref()
2585 // .unwrap_or(&interactive.default)
2586 // .clone()
2587 // } else {
2588 // interactive.default.clone()
2589 // }
2590 // };
2591
2592 // Flex::<Self>::row()
2593 // .with_child(
2594 // Svg::new(if is_public {
2595 // "icons/public.svg"
2596 // } else {
2597 // "icons/hash.svg"
2598 // })
2599 // .with_color(collab_theme.channel_hash.color)
2600 // .constrained()
2601 // .with_width(collab_theme.channel_hash.width)
2602 // .aligned()
2603 // .left(),
2604 // )
2605 // .with_child({
2606 // let style = collab_theme.channel_name.inactive_state();
2607 // Flex::row()
2608 // .with_child(
2609 // Label::new(channel.name.clone(), style.text.clone())
2610 // .contained()
2611 // .with_style(style.container)
2612 // .aligned()
2613 // .left()
2614 // .with_tooltip::<ChannelTooltip>(
2615 // ix,
2616 // "Join channel",
2617 // None,
2618 // theme.tooltip.clone(),
2619 // cx,
2620 // ),
2621 // )
2622 // .with_children({
2623 // let participants =
2624 // self.channel_store.read(cx).channel_participants(channel_id);
2625
2626 // if !participants.is_empty() {
2627 // let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2628
2629 // let result = FacePile::new(collab_theme.face_overlap)
2630 // .with_children(
2631 // participants
2632 // .iter()
2633 // .filter_map(|user| {
2634 // Some(
2635 // Image::from_data(user.avatar.clone()?)
2636 // .with_style(collab_theme.channel_avatar),
2637 // )
2638 // })
2639 // .take(FACEPILE_LIMIT),
2640 // )
2641 // .with_children((extra_count > 0).then(|| {
2642 // Label::new(
2643 // format!("+{}", extra_count),
2644 // collab_theme.extra_participant_label.text.clone(),
2645 // )
2646 // .contained()
2647 // .with_style(collab_theme.extra_participant_label.container)
2648 // }));
2649
2650 // Some(result)
2651 // } else {
2652 // None
2653 // }
2654 // })
2655 // .with_spacing(8.)
2656 // .align_children_center()
2657 // .flex(1., true)
2658 // })
2659 // .with_child(
2660 // MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| {
2661 // let container_style = collab_theme
2662 // .disclosure
2663 // .button
2664 // .style_for(mouse_state)
2665 // .container;
2666
2667 // if channel.unseen_message_id.is_some() {
2668 // Svg::new("icons/conversations.svg")
2669 // .with_color(collab_theme.channel_note_active_color)
2670 // .constrained()
2671 // .with_width(collab_theme.channel_hash.width)
2672 // .contained()
2673 // .with_style(container_style)
2674 // .with_uniform_padding(4.)
2675 // .into_any()
2676 // } else if row_hovered {
2677 // Svg::new("icons/conversations.svg")
2678 // .with_color(collab_theme.channel_hash.color)
2679 // .constrained()
2680 // .with_width(collab_theme.channel_hash.width)
2681 // .contained()
2682 // .with_style(container_style)
2683 // .with_uniform_padding(4.)
2684 // .into_any()
2685 // } else {
2686 // Empty::new().into_any()
2687 // }
2688 // })
2689 // .on_click(MouseButton::Left, move |_, this, cx| {
2690 // this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
2691 // })
2692 // .with_tooltip::<ChatTooltip>(
2693 // ix,
2694 // "Open channel chat",
2695 // None,
2696 // theme.tooltip.clone(),
2697 // cx,
2698 // )
2699 // .contained()
2700 // .with_margin_right(4.),
2701 // )
2702 // .with_child(
2703 // MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| {
2704 // let container_style = collab_theme
2705 // .disclosure
2706 // .button
2707 // .style_for(mouse_state)
2708 // .container;
2709 // if row_hovered || channel.unseen_note_version.is_some() {
2710 // Svg::new("icons/file.svg")
2711 // .with_color(if channel.unseen_note_version.is_some() {
2712 // collab_theme.channel_note_active_color
2713 // } else {
2714 // collab_theme.channel_hash.color
2715 // })
2716 // .constrained()
2717 // .with_width(collab_theme.channel_hash.width)
2718 // .contained()
2719 // .with_style(container_style)
2720 // .with_uniform_padding(4.)
2721 // .with_margin_right(collab_theme.channel_hash.container.margin.left)
2722 // .with_tooltip::<NotesTooltip>(
2723 // ix as usize,
2724 // "Open channel notes",
2725 // None,
2726 // theme.tooltip.clone(),
2727 // cx,
2728 // )
2729 // .into_any()
2730 // } else if has_messages_notification {
2731 // Empty::new()
2732 // .constrained()
2733 // .with_width(collab_theme.channel_hash.width)
2734 // .contained()
2735 // .with_uniform_padding(4.)
2736 // .with_margin_right(collab_theme.channel_hash.container.margin.left)
2737 // .into_any()
2738 // } else {
2739 // Empty::new().into_any()
2740 // }
2741 // })
2742 // .on_click(MouseButton::Left, move |_, this, cx| {
2743 // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
2744 // }),
2745 // )
2746 // .align_children_center()
2747 // .styleable_component()
2748 // .disclosable(
2749 // disclosed,
2750 // Box::new(ToggleCollapse {
2751 // location: channel.id.clone(),
2752 // }),
2753 // )
2754 // .with_id(ix)
2755 // .with_style(collab_theme.disclosure.clone())
2756 // .element()
2757 // .constrained()
2758 // .with_height(collab_theme.row_height)
2759 // .contained()
2760 // .with_style(select_state(
2761 // collab_theme
2762 // .channel_row
2763 // .in_state(is_selected || is_active || is_dragged_over),
2764 // ))
2765 // .with_padding_left(
2766 // collab_theme.channel_row.default_style().padding.left
2767 // + collab_theme.channel_indent * depth as f32,
2768 // )
2769 // })
2770 // .on_click(MouseButton::Left, move |_, this, cx| {
2771 // if this.
2772 // drag_target_channel == ChannelDragTarget::None {
2773 // if is_active {
2774 // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
2775 // } else {
2776 // this.join_channel(channel_id, cx)
2777 // }
2778 // }
2779 // })
2780 // .on_click(MouseButton::Right, {
2781 // let channel = channel.clone();
2782 // move |e, this, cx| {
2783 // this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx);
2784 // }
2785 // })
2786 // .on_up(MouseButton::Left, move |_, this, cx| {
2787 // if let Some((_, dragged_channel)) = cx
2788 // .global::<DragAndDrop<Workspace>>()
2789 // .currently_dragged::<Channel>(cx.window())
2790 // {
2791 // this.channel_store
2792 // .update(cx, |channel_store, cx| {
2793 // channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
2794 // })
2795 // .detach_and_log_err(cx)
2796 // }
2797 // })
2798 // .on_move({
2799 // let channel = channel.clone();
2800 // move |_, this, cx| {
2801 // if let Some((_, dragged_channel)) = cx
2802 // .global::<DragAndDrop<Workspace>>()
2803 // .currently_dragged::<Channel>(cx.window())
2804 // {
2805 // if channel.id != dragged_channel.id {
2806 // this.drag_target_channel = ChannelDragTarget::Channel(channel.id);
2807 // }
2808 // cx.notify()
2809 // }
2810 // }
2811 // })
2812 // .as_draggable::<_, Channel>(
2813 // channel.clone(),
2814 // move |_, channel, cx: &mut ViewContext<Workspace>| {
2815 // let theme = &theme::current(cx).collab_panel;
2816
2817 // Flex::<Workspace>::row()
2818 // .with_child(
2819 // Svg::new("icons/hash.svg")
2820 // .with_color(theme.channel_hash.color)
2821 // .constrained()
2822 // .with_width(theme.channel_hash.width)
2823 // .aligned()
2824 // .left(),
2825 // )
2826 // .with_child(
2827 // Label::new(channel.name.clone(), theme.channel_name.text.clone())
2828 // .contained()
2829 // .with_style(theme.channel_name.container)
2830 // .aligned()
2831 // .left(),
2832 // )
2833 // .align_children_center()
2834 // .contained()
2835 // .with_background_color(
2836 // theme
2837 // .container
2838 // .background_color
2839 // .unwrap_or(gpui::color::Color::transparent_black()),
2840 // )
2841 // .contained()
2842 // .with_padding_left(
2843 // theme.channel_row.default_style().padding.left
2844 // + theme.channel_indent * depth as f32,
2845 // )
2846 // .into_any()
2847 // },
2848 // )
2849 // .with_cursor_style(CursorStyle::PointingHand)
2850 // .into_any()
2851 }
2852
2853 fn render_channel_editor(&self, depth: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
2854 let item = ListItem::new("channel-editor")
2855 .inset(false)
2856 .indent_level(depth)
2857 .start_slot(
2858 IconElement::new(Icon::Hash)
2859 .size(IconSize::Small)
2860 .color(Color::Muted),
2861 );
2862
2863 if let Some(pending_name) = self
2864 .channel_editing_state
2865 .as_ref()
2866 .and_then(|state| state.pending_name())
2867 {
2868 item.child(Label::new(pending_name))
2869 } else {
2870 item.child(
2871 div()
2872 .w_full()
2873 .py_1() // todo!() @nate this is a px off at the default font size.
2874 .child(self.channel_name_editor.clone()),
2875 )
2876 }
2877 }
2878}
2879
2880fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement {
2881 let rem_size = cx.rem_size();
2882 let line_height = cx.text_style().line_height_in_pixels(rem_size);
2883 let width = rem_size * 1.5;
2884 let thickness = px(2.);
2885 let color = cx.theme().colors().text;
2886
2887 canvas(move |bounds, cx| {
2888 let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
2889 let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
2890 let right = bounds.right();
2891 let top = bounds.top();
2892
2893 cx.paint_quad(fill(
2894 Bounds::from_corners(
2895 point(start_x, top),
2896 point(
2897 start_x + thickness,
2898 if is_last { start_y } else { bounds.bottom() },
2899 ),
2900 ),
2901 color,
2902 ));
2903 cx.paint_quad(fill(
2904 Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
2905 color,
2906 ));
2907 })
2908 .w(width)
2909 .h(line_height)
2910}
2911
2912impl Render for CollabPanel {
2913 type Element = Focusable<Div>;
2914
2915 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
2916 v_stack()
2917 .key_context("CollabPanel")
2918 .on_action(cx.listener(CollabPanel::cancel))
2919 .on_action(cx.listener(CollabPanel::select_next))
2920 .on_action(cx.listener(CollabPanel::select_prev))
2921 .on_action(cx.listener(CollabPanel::confirm))
2922 .on_action(cx.listener(CollabPanel::insert_space))
2923 // .on_action(cx.listener(CollabPanel::remove))
2924 .on_action(cx.listener(CollabPanel::remove_selected_channel))
2925 .on_action(cx.listener(CollabPanel::show_inline_context_menu))
2926 // .on_action(cx.listener(CollabPanel::new_subchannel))
2927 // .on_action(cx.listener(CollabPanel::invite_members))
2928 // .on_action(cx.listener(CollabPanel::manage_members))
2929 .on_action(cx.listener(CollabPanel::rename_selected_channel))
2930 // .on_action(cx.listener(CollabPanel::rename_channel))
2931 // .on_action(cx.listener(CollabPanel::toggle_channel_collapsed_action))
2932 .on_action(cx.listener(CollabPanel::collapse_selected_channel))
2933 .on_action(cx.listener(CollabPanel::expand_selected_channel))
2934 // .on_action(cx.listener(CollabPanel::open_channel_notes))
2935 // .on_action(cx.listener(CollabPanel::join_channel_chat))
2936 // .on_action(cx.listener(CollabPanel::copy_channel_link))
2937 .track_focus(&self.focus_handle)
2938 .size_full()
2939 .child(if self.user_store.read(cx).current_user().is_none() {
2940 self.render_signed_out(cx)
2941 } else {
2942 self.render_signed_in(cx)
2943 })
2944 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
2945 overlay()
2946 .position(*position)
2947 .anchor(gpui::AnchorCorner::TopLeft)
2948 .child(menu.clone())
2949 }))
2950 }
2951}
2952
2953// impl View for CollabPanel {
2954// fn ui_name() -> &'static str {
2955// "CollabPanel"
2956// }
2957
2958// fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
2959// if !self.has_focus {
2960// self.has_focus = true;
2961// if !self.context_menu.is_focused(cx) {
2962// if let Some(editing_state) = &self.channel_editing_state {
2963// if editing_state.pending_name().is_none() {
2964// cx.focus(&self.channel_name_editor);
2965// } else {
2966// cx.focus(&self.filter_editor);
2967// }
2968// } else {
2969// cx.focus(&self.filter_editor);
2970// }
2971// }
2972// cx.emit(Event::Focus);
2973// }
2974// }
2975
2976// fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
2977// self.has_focus = false;
2978// }
2979
2980// fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
2981// let theme = &theme::current(cx).collab_panel;
2982
2983// if self.user_store.read(cx).current_user().is_none() {
2984// enum LogInButton {}
2985
2986// return Flex::column()
2987// .with_child(
2988// MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
2989// let button = theme.log_in_button.style_for(state);
2990// Label::new("Sign in to collaborate", button.text.clone())
2991// .aligned()
2992// .left()
2993// .contained()
2994// .with_style(button.container)
2995// })
2996// .on_click(MouseButton::Left, |_, this, cx| {
2997// let client = this.client.clone();
2998// cx.spawn(|_, cx| async move {
2999// client.authenticate_and_connect(true, &cx).await.log_err();
3000// })
3001// .detach();
3002// })
3003// .with_cursor_style(CursorStyle::PointingHand),
3004// )
3005// .contained()
3006// .with_style(theme.container)
3007// .into_any();
3008// }
3009
3010// enum PanelFocus {}
3011// MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
3012// Stack::new()
3013// .with_child(
3014// Flex::column()
3015// .with_child(
3016// Flex::row().with_child(
3017// ChildView::new(&self.filter_editor, cx)
3018// .contained()
3019// .with_style(theme.user_query_editor.container)
3020// .flex(1.0, true),
3021// ),
3022// )
3023// .with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
3024// .contained()
3025// .with_style(theme.container)
3026// .into_any(),
3027// )
3028// .with_children(
3029// (!self.context_menu_on_selected)
3030// .then(|| ChildView::new(&self.context_menu, cx)),
3031// )
3032// .into_any()
3033// })
3034// .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
3035// .into_any_named("collab panel")
3036// }
3037
3038// fn update_keymap_context(
3039// &self,
3040// keymap: &mut gpui::keymap_matcher::KeymapContext,
3041// _: &AppContext,
3042// ) {
3043// Self::reset_to_default_keymap_context(keymap);
3044// if self.channel_editing_state.is_some() {
3045// keymap.add_identifier("editing");
3046// } else {
3047// keymap.add_identifier("not_editing");
3048// }
3049// }
3050// }
3051
3052impl EventEmitter<PanelEvent> for CollabPanel {}
3053
3054impl Panel for CollabPanel {
3055 fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
3056 CollaborationPanelSettings::get_global(cx).dock
3057 }
3058
3059 fn position_is_valid(&self, position: DockPosition) -> bool {
3060 matches!(position, DockPosition::Left | DockPosition::Right)
3061 }
3062
3063 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
3064 settings::update_settings_file::<CollaborationPanelSettings>(
3065 self.fs.clone(),
3066 cx,
3067 move |settings| settings.dock = Some(position),
3068 );
3069 }
3070
3071 fn size(&self, cx: &gpui::WindowContext) -> f32 {
3072 self.width.map_or_else(
3073 || CollaborationPanelSettings::get_global(cx).default_width,
3074 |width| width.0,
3075 )
3076 }
3077
3078 fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
3079 self.width = size.map(|s| px(s));
3080 self.serialize(cx);
3081 cx.notify();
3082 }
3083
3084 fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::Icon> {
3085 CollaborationPanelSettings::get_global(cx)
3086 .button
3087 .then(|| ui::Icon::Collab)
3088 }
3089
3090 fn toggle_action(&self) -> Box<dyn gpui::Action> {
3091 Box::new(ToggleFocus)
3092 }
3093
3094 fn persistent_name() -> &'static str {
3095 "CollabPanel"
3096 }
3097}
3098
3099impl FocusableView for CollabPanel {
3100 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
3101 self.filter_editor.focus_handle(cx).clone()
3102 }
3103}
3104
3105impl PartialEq for ListEntry {
3106 fn eq(&self, other: &Self) -> bool {
3107 match self {
3108 ListEntry::Header(section_1) => {
3109 if let ListEntry::Header(section_2) = other {
3110 return section_1 == section_2;
3111 }
3112 }
3113 ListEntry::CallParticipant { user: user_1, .. } => {
3114 if let ListEntry::CallParticipant { user: user_2, .. } = other {
3115 return user_1.id == user_2.id;
3116 }
3117 }
3118 ListEntry::ParticipantProject {
3119 project_id: project_id_1,
3120 ..
3121 } => {
3122 if let ListEntry::ParticipantProject {
3123 project_id: project_id_2,
3124 ..
3125 } = other
3126 {
3127 return project_id_1 == project_id_2;
3128 }
3129 }
3130 ListEntry::ParticipantScreen {
3131 peer_id: peer_id_1, ..
3132 } => {
3133 if let ListEntry::ParticipantScreen {
3134 peer_id: peer_id_2, ..
3135 } = other
3136 {
3137 return peer_id_1 == peer_id_2;
3138 }
3139 }
3140 ListEntry::Channel {
3141 channel: channel_1, ..
3142 } => {
3143 if let ListEntry::Channel {
3144 channel: channel_2, ..
3145 } = other
3146 {
3147 return channel_1.id == channel_2.id;
3148 }
3149 }
3150 ListEntry::ChannelNotes { channel_id } => {
3151 if let ListEntry::ChannelNotes {
3152 channel_id: other_id,
3153 } = other
3154 {
3155 return channel_id == other_id;
3156 }
3157 }
3158 ListEntry::ChannelChat { channel_id } => {
3159 if let ListEntry::ChannelChat {
3160 channel_id: other_id,
3161 } = other
3162 {
3163 return channel_id == other_id;
3164 }
3165 }
3166 // ListEntry::ChannelInvite(channel_1) => {
3167 // if let ListEntry::ChannelInvite(channel_2) = other {
3168 // return channel_1.id == channel_2.id;
3169 // }
3170 // }
3171 ListEntry::IncomingRequest(user_1) => {
3172 if let ListEntry::IncomingRequest(user_2) = other {
3173 return user_1.id == user_2.id;
3174 }
3175 }
3176 ListEntry::OutgoingRequest(user_1) => {
3177 if let ListEntry::OutgoingRequest(user_2) = other {
3178 return user_1.id == user_2.id;
3179 }
3180 }
3181 ListEntry::Contact {
3182 contact: contact_1, ..
3183 } => {
3184 if let ListEntry::Contact {
3185 contact: contact_2, ..
3186 } = other
3187 {
3188 return contact_1.user.id == contact_2.user.id;
3189 }
3190 }
3191 ListEntry::ChannelEditor { depth } => {
3192 if let ListEntry::ChannelEditor { depth: other_depth } = other {
3193 return depth == other_depth;
3194 }
3195 }
3196 ListEntry::ContactPlaceholder => {
3197 if let ListEntry::ContactPlaceholder = other {
3198 return true;
3199 }
3200 }
3201 }
3202 false
3203 }
3204}
3205
3206// fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
3207// Svg::new(svg_path)
3208// .with_color(style.color)
3209// .constrained()
3210// .with_width(style.icon_width)
3211// .aligned()
3212// .constrained()
3213// .with_width(style.button_width)
3214// .with_height(style.button_width)
3215// .contained()
3216// .with_style(style.container)
3217// }
3218
3219struct DraggedChannelView {
3220 channel: Channel,
3221 width: Pixels,
3222}
3223
3224impl Render for DraggedChannelView {
3225 type Element = Div;
3226
3227 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
3228 let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
3229 h_stack()
3230 .font(ui_font)
3231 .bg(cx.theme().colors().background)
3232 .w(self.width)
3233 .p_1()
3234 .gap_1()
3235 .child(
3236 IconElement::new(
3237 if self.channel.visibility == proto::ChannelVisibility::Public {
3238 Icon::Public
3239 } else {
3240 Icon::Hash
3241 },
3242 )
3243 .size(IconSize::Small)
3244 .color(Color::Muted),
3245 )
3246 .child(Label::new(self.channel.name.clone()))
3247 }
3248}