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