1// mod channel_modal;
2// mod contact_finder;
3
4// use crate::{
5// channel_view::{self, ChannelView},
6// chat_panel::ChatPanel,
7// face_pile::FacePile,
8// panel_settings, CollaborationPanelSettings,
9// };
10// use anyhow::Result;
11// use call::ActiveCall;
12// use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
13// use channel_modal::ChannelModal;
14// use client::{
15// proto::{self, PeerId},
16// Client, Contact, User, UserStore,
17// };
18// use contact_finder::ContactFinder;
19// use context_menu::{ContextMenu, ContextMenuItem};
20// use db::kvp::KEY_VALUE_STORE;
21// use drag_and_drop::{DragAndDrop, Draggable};
22// use editor::{Cancel, Editor};
23// use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
24// use futures::StreamExt;
25// use fuzzy::{match_strings, StringMatchCandidate};
26// use gpui::{
27// actions,
28// elements::{
29// Canvas, ChildView, Component, ContainerStyle, Empty, Flex, Image, Label, List, ListOffset,
30// ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement,
31// SafeStylable, Stack, Svg,
32// },
33// fonts::TextStyle,
34// geometry::{
35// rect::RectF,
36// vector::{vec2f, Vector2F},
37// },
38// impl_actions,
39// platform::{CursorStyle, MouseButton, PromptLevel},
40// serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, FontCache,
41// ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
42// };
43// use menu::{Confirm, SelectNext, SelectPrev};
44// use project::{Fs, Project};
45// use serde_derive::{Deserialize, Serialize};
46// use settings::SettingsStore;
47// use std::{borrow::Cow, hash::Hash, mem, sync::Arc};
48// use theme::{components::ComponentExt, IconButton, Interactive};
49// use util::{maybe, ResultExt, TryFutureExt};
50// use workspace::{
51// dock::{DockPosition, Panel},
52// item::ItemHandle,
53// FollowNextCollaborator, Workspace,
54// };
55
56// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
57// struct ToggleCollapse {
58// location: ChannelId,
59// }
60
61// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
62// struct NewChannel {
63// location: ChannelId,
64// }
65
66// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
67// struct RenameChannel {
68// channel_id: ChannelId,
69// }
70
71// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
72// struct ToggleSelectedIx {
73// ix: usize,
74// }
75
76// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
77// struct RemoveChannel {
78// channel_id: ChannelId,
79// }
80
81// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
82// struct InviteMembers {
83// channel_id: ChannelId,
84// }
85
86// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
87// struct ManageMembers {
88// channel_id: ChannelId,
89// }
90
91// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
92// pub struct OpenChannelNotes {
93// pub channel_id: ChannelId,
94// }
95
96// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
97// pub struct JoinChannelCall {
98// pub channel_id: u64,
99// }
100
101// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
102// pub struct JoinChannelChat {
103// pub channel_id: u64,
104// }
105
106// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
107// pub struct CopyChannelLink {
108// pub channel_id: u64,
109// }
110
111// #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
112// struct StartMoveChannelFor {
113// channel_id: ChannelId,
114// }
115
116// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
117// struct MoveChannel {
118// to: ChannelId,
119// }
120
121// actions!(
122// collab_panel,
123// [
124// ToggleFocus,
125// Remove,
126// Secondary,
127// CollapseSelectedChannel,
128// ExpandSelectedChannel,
129// StartMoveChannel,
130// MoveSelected,
131// InsertSpace,
132// ]
133// );
134
135// impl_actions!(
136// collab_panel,
137// [
138// RemoveChannel,
139// NewChannel,
140// InviteMembers,
141// ManageMembers,
142// RenameChannel,
143// ToggleCollapse,
144// OpenChannelNotes,
145// JoinChannelCall,
146// JoinChannelChat,
147// CopyChannelLink,
148// StartMoveChannelFor,
149// MoveChannel,
150// ToggleSelectedIx
151// ]
152// );
153
154// #[derive(Debug, Copy, Clone, PartialEq, Eq)]
155// struct ChannelMoveClipboard {
156// channel_id: ChannelId,
157// }
158
159// const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
160
161// pub fn init(cx: &mut AppContext) {
162// settings::register::<panel_settings::CollaborationPanelSettings>(cx);
163// contact_finder::init(cx);
164// channel_modal::init(cx);
165// channel_view::init(cx);
166
167// cx.add_action(CollabPanel::cancel);
168// cx.add_action(CollabPanel::select_next);
169// cx.add_action(CollabPanel::select_prev);
170// cx.add_action(CollabPanel::confirm);
171// cx.add_action(CollabPanel::insert_space);
172// cx.add_action(CollabPanel::remove);
173// cx.add_action(CollabPanel::remove_selected_channel);
174// cx.add_action(CollabPanel::show_inline_context_menu);
175// cx.add_action(CollabPanel::new_subchannel);
176// cx.add_action(CollabPanel::invite_members);
177// cx.add_action(CollabPanel::manage_members);
178// cx.add_action(CollabPanel::rename_selected_channel);
179// cx.add_action(CollabPanel::rename_channel);
180// cx.add_action(CollabPanel::toggle_channel_collapsed_action);
181// cx.add_action(CollabPanel::collapse_selected_channel);
182// cx.add_action(CollabPanel::expand_selected_channel);
183// cx.add_action(CollabPanel::open_channel_notes);
184// cx.add_action(CollabPanel::join_channel_chat);
185// cx.add_action(CollabPanel::copy_channel_link);
186
187// cx.add_action(
188// |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
189// if panel.selection.take() != Some(action.ix) {
190// panel.selection = Some(action.ix)
191// }
192
193// cx.notify();
194// },
195// );
196
197// cx.add_action(
198// |panel: &mut CollabPanel,
199// action: &StartMoveChannelFor,
200// _: &mut ViewContext<CollabPanel>| {
201// panel.channel_clipboard = Some(ChannelMoveClipboard {
202// channel_id: action.channel_id,
203// });
204// },
205// );
206
207// cx.add_action(
208// |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext<CollabPanel>| {
209// if let Some(channel) = panel.selected_channel() {
210// panel.channel_clipboard = Some(ChannelMoveClipboard {
211// channel_id: channel.id,
212// })
213// }
214// },
215// );
216
217// cx.add_action(
218// |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| {
219// let Some(clipboard) = panel.channel_clipboard.take() else {
220// return;
221// };
222// let Some(selected_channel) = panel.selected_channel() else {
223// return;
224// };
225
226// panel
227// .channel_store
228// .update(cx, |channel_store, cx| {
229// channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx)
230// })
231// .detach_and_log_err(cx)
232// },
233// );
234
235// cx.add_action(
236// |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| {
237// if let Some(clipboard) = panel.channel_clipboard.take() {
238// panel.channel_store.update(cx, |channel_store, cx| {
239// channel_store
240// .move_channel(clipboard.channel_id, Some(action.to), cx)
241// .detach_and_log_err(cx)
242// })
243// }
244// },
245// );
246// }
247
248// #[derive(Debug)]
249// pub enum ChannelEditingState {
250// Create {
251// location: Option<ChannelId>,
252// pending_name: Option<String>,
253// },
254// Rename {
255// location: ChannelId,
256// pending_name: Option<String>,
257// },
258// }
259
260// impl ChannelEditingState {
261// fn pending_name(&self) -> Option<&str> {
262// match self {
263// ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
264// ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
265// }
266// }
267// }
268
269// pub struct CollabPanel {
270// width: Option<f32>,
271// fs: Arc<dyn Fs>,
272// has_focus: bool,
273// channel_clipboard: Option<ChannelMoveClipboard>,
274// pending_serialization: Task<Option<()>>,
275// context_menu: ViewHandle<ContextMenu>,
276// filter_editor: ViewHandle<Editor>,
277// channel_name_editor: ViewHandle<Editor>,
278// channel_editing_state: Option<ChannelEditingState>,
279// entries: Vec<ListEntry>,
280// selection: Option<usize>,
281// user_store: ModelHandle<UserStore>,
282// client: Arc<Client>,
283// channel_store: ModelHandle<ChannelStore>,
284// project: ModelHandle<Project>,
285// match_candidates: Vec<StringMatchCandidate>,
286// list_state: ListState<Self>,
287// subscriptions: Vec<Subscription>,
288// collapsed_sections: Vec<Section>,
289// collapsed_channels: Vec<ChannelId>,
290// drag_target_channel: ChannelDragTarget,
291// workspace: WeakViewHandle<Workspace>,
292// context_menu_on_selected: bool,
293// }
294
295// #[derive(PartialEq, Eq)]
296// enum ChannelDragTarget {
297// None,
298// Root,
299// Channel(ChannelId),
300// }
301
302// #[derive(Serialize, Deserialize)]
303// struct SerializedCollabPanel {
304// width: Option<f32>,
305// collapsed_channels: Option<Vec<ChannelId>>,
306// }
307
308// #[derive(Debug)]
309// pub enum Event {
310// DockPositionChanged,
311// Focus,
312// Dismissed,
313// }
314
315// #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
316// enum Section {
317// ActiveCall,
318// Channels,
319// ChannelInvites,
320// ContactRequests,
321// Contacts,
322// Online,
323// Offline,
324// }
325
326// #[derive(Clone, Debug)]
327// enum ListEntry {
328// Header(Section),
329// CallParticipant {
330// user: Arc<User>,
331// peer_id: Option<PeerId>,
332// is_pending: bool,
333// },
334// ParticipantProject {
335// project_id: u64,
336// worktree_root_names: Vec<String>,
337// host_user_id: u64,
338// is_last: bool,
339// },
340// ParticipantScreen {
341// peer_id: Option<PeerId>,
342// is_last: bool,
343// },
344// IncomingRequest(Arc<User>),
345// OutgoingRequest(Arc<User>),
346// ChannelInvite(Arc<Channel>),
347// Channel {
348// channel: Arc<Channel>,
349// depth: usize,
350// has_children: bool,
351// },
352// ChannelNotes {
353// channel_id: ChannelId,
354// },
355// ChannelChat {
356// channel_id: ChannelId,
357// },
358// ChannelEditor {
359// depth: usize,
360// },
361// Contact {
362// contact: Arc<Contact>,
363// calling: bool,
364// },
365// ContactPlaceholder,
366// }
367
368// impl Entity for CollabPanel {
369// type Event = Event;
370// }
371
372// impl CollabPanel {
373// pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
374// cx.add_view::<Self, _>(|cx| {
375// let view_id = cx.view_id();
376
377// let filter_editor = cx.add_view(|cx| {
378// let mut editor = Editor::single_line(
379// Some(Arc::new(|theme| {
380// theme.collab_panel.user_query_editor.clone()
381// })),
382// cx,
383// );
384// editor.set_placeholder_text("Filter channels, contacts", cx);
385// editor
386// });
387
388// cx.subscribe(&filter_editor, |this, _, event, cx| {
389// if let editor::Event::BufferEdited = event {
390// let query = this.filter_editor.read(cx).text(cx);
391// if !query.is_empty() {
392// this.selection.take();
393// }
394// this.update_entries(true, cx);
395// if !query.is_empty() {
396// this.selection = this
397// .entries
398// .iter()
399// .position(|entry| !matches!(entry, ListEntry::Header(_)));
400// }
401// } else if let editor::Event::Blurred = event {
402// let query = this.filter_editor.read(cx).text(cx);
403// if query.is_empty() {
404// this.selection.take();
405// this.update_entries(true, cx);
406// }
407// }
408// })
409// .detach();
410
411// let channel_name_editor = cx.add_view(|cx| {
412// Editor::single_line(
413// Some(Arc::new(|theme| {
414// theme.collab_panel.user_query_editor.clone()
415// })),
416// cx,
417// )
418// });
419
420// cx.subscribe(&channel_name_editor, |this, _, event, cx| {
421// if let editor::Event::Blurred = event {
422// if let Some(state) = &this.channel_editing_state {
423// if state.pending_name().is_some() {
424// return;
425// }
426// }
427// this.take_editing_state(cx);
428// this.update_entries(false, cx);
429// cx.notify();
430// }
431// })
432// .detach();
433
434// let list_state =
435// ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
436// let theme = theme::current(cx).clone();
437// let is_selected = this.selection == Some(ix);
438// let current_project_id = this.project.read(cx).remote_id();
439
440// match &this.entries[ix] {
441// ListEntry::Header(section) => {
442// let is_collapsed = this.collapsed_sections.contains(section);
443// this.render_header(*section, &theme, is_selected, is_collapsed, cx)
444// }
445// ListEntry::CallParticipant {
446// user,
447// peer_id,
448// is_pending,
449// } => Self::render_call_participant(
450// user,
451// *peer_id,
452// this.user_store.clone(),
453// *is_pending,
454// is_selected,
455// &theme,
456// cx,
457// ),
458// ListEntry::ParticipantProject {
459// project_id,
460// worktree_root_names,
461// host_user_id,
462// is_last,
463// } => Self::render_participant_project(
464// *project_id,
465// worktree_root_names,
466// *host_user_id,
467// Some(*project_id) == current_project_id,
468// *is_last,
469// is_selected,
470// &theme,
471// cx,
472// ),
473// ListEntry::ParticipantScreen { peer_id, is_last } => {
474// Self::render_participant_screen(
475// *peer_id,
476// *is_last,
477// is_selected,
478// &theme.collab_panel,
479// cx,
480// )
481// }
482// ListEntry::Channel {
483// channel,
484// depth,
485// has_children,
486// } => {
487// let channel_row = this.render_channel(
488// &*channel,
489// *depth,
490// &theme,
491// is_selected,
492// *has_children,
493// ix,
494// cx,
495// );
496
497// if is_selected && this.context_menu_on_selected {
498// Stack::new()
499// .with_child(channel_row)
500// .with_child(
501// ChildView::new(&this.context_menu, cx)
502// .aligned()
503// .bottom()
504// .right(),
505// )
506// .into_any()
507// } else {
508// return channel_row;
509// }
510// }
511// ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
512// *channel_id,
513// &theme.collab_panel,
514// is_selected,
515// ix,
516// cx,
517// ),
518// ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
519// *channel_id,
520// &theme.collab_panel,
521// is_selected,
522// ix,
523// cx,
524// ),
525// ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
526// channel.clone(),
527// this.channel_store.clone(),
528// &theme.collab_panel,
529// is_selected,
530// cx,
531// ),
532// ListEntry::IncomingRequest(user) => Self::render_contact_request(
533// user.clone(),
534// this.user_store.clone(),
535// &theme.collab_panel,
536// true,
537// is_selected,
538// cx,
539// ),
540// ListEntry::OutgoingRequest(user) => Self::render_contact_request(
541// user.clone(),
542// this.user_store.clone(),
543// &theme.collab_panel,
544// false,
545// is_selected,
546// cx,
547// ),
548// ListEntry::Contact { contact, calling } => Self::render_contact(
549// contact,
550// *calling,
551// &this.project,
552// &theme,
553// is_selected,
554// cx,
555// ),
556// ListEntry::ChannelEditor { depth } => {
557// this.render_channel_editor(&theme, *depth, cx)
558// }
559// ListEntry::ContactPlaceholder => {
560// this.render_contact_placeholder(&theme.collab_panel, is_selected, cx)
561// }
562// }
563// });
564
565// let mut this = Self {
566// width: None,
567// has_focus: false,
568// channel_clipboard: None,
569// fs: workspace.app_state().fs.clone(),
570// pending_serialization: Task::ready(None),
571// context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
572// channel_name_editor,
573// filter_editor,
574// entries: Vec::default(),
575// channel_editing_state: None,
576// selection: None,
577// user_store: workspace.user_store().clone(),
578// channel_store: ChannelStore::global(cx),
579// project: workspace.project().clone(),
580// subscriptions: Vec::default(),
581// match_candidates: Vec::default(),
582// collapsed_sections: vec![Section::Offline],
583// collapsed_channels: Vec::default(),
584// workspace: workspace.weak_handle(),
585// client: workspace.app_state().client.clone(),
586// context_menu_on_selected: true,
587// drag_target_channel: ChannelDragTarget::None,
588// list_state,
589// };
590
591// this.update_entries(false, cx);
592
593// // Update the dock position when the setting changes.
594// let mut old_dock_position = this.position(cx);
595// this.subscriptions
596// .push(
597// cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
598// let new_dock_position = this.position(cx);
599// if new_dock_position != old_dock_position {
600// old_dock_position = new_dock_position;
601// cx.emit(Event::DockPositionChanged);
602// }
603// cx.notify();
604// }),
605// );
606
607// let active_call = ActiveCall::global(cx);
608// this.subscriptions
609// .push(cx.observe(&this.user_store, |this, _, cx| {
610// this.update_entries(true, cx)
611// }));
612// this.subscriptions
613// .push(cx.observe(&this.channel_store, |this, _, cx| {
614// this.update_entries(true, cx)
615// }));
616// this.subscriptions
617// .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
618// this.subscriptions
619// .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
620// this.update_entries(true, cx)
621// }));
622// this.subscriptions.push(cx.subscribe(
623// &this.channel_store,
624// |this, _channel_store, e, cx| match e {
625// ChannelEvent::ChannelCreated(channel_id)
626// | ChannelEvent::ChannelRenamed(channel_id) => {
627// if this.take_editing_state(cx) {
628// this.update_entries(false, cx);
629// this.selection = this.entries.iter().position(|entry| {
630// if let ListEntry::Channel { channel, .. } = entry {
631// channel.id == *channel_id
632// } else {
633// false
634// }
635// });
636// }
637// }
638// },
639// ));
640
641// this
642// })
643// }
644
645// pub fn load(
646// workspace: WeakViewHandle<Workspace>,
647// cx: AsyncAppContext,
648// ) -> Task<Result<ViewHandle<Self>>> {
649// cx.spawn(|mut cx| async move {
650// let serialized_panel = if let Some(panel) = cx
651// .background()
652// .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
653// .await
654// .log_err()
655// .flatten()
656// {
657// match serde_json::from_str::<SerializedCollabPanel>(&panel) {
658// Ok(panel) => Some(panel),
659// Err(err) => {
660// log::error!("Failed to deserialize collaboration panel: {}", err);
661// None
662// }
663// }
664// } else {
665// None
666// };
667
668// workspace.update(&mut cx, |workspace, cx| {
669// let panel = CollabPanel::new(workspace, cx);
670// if let Some(serialized_panel) = serialized_panel {
671// panel.update(cx, |panel, cx| {
672// panel.width = serialized_panel.width;
673// panel.collapsed_channels = serialized_panel
674// .collapsed_channels
675// .unwrap_or_else(|| Vec::new());
676// cx.notify();
677// });
678// }
679// panel
680// })
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().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().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_header(
1445// &self,
1446// section: Section,
1447// theme: &theme::Theme,
1448// is_selected: bool,
1449// is_collapsed: bool,
1450// cx: &mut ViewContext<Self>,
1451// ) -> AnyElement<Self> {
1452// enum Header {}
1453// enum LeaveCallContactList {}
1454// enum AddChannel {}
1455
1456// let tooltip_style = &theme.tooltip;
1457// let mut channel_link = None;
1458// let mut channel_tooltip_text = None;
1459// let mut channel_icon = None;
1460// let mut is_dragged_over = false;
1461
1462// let text = match section {
1463// Section::ActiveCall => {
1464// let channel_name = maybe!({
1465// let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
1466
1467// let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
1468
1469// channel_link = Some(channel.link());
1470// (channel_icon, channel_tooltip_text) = match channel.visibility {
1471// proto::ChannelVisibility::Public => {
1472// (Some("icons/public.svg"), Some("Copy public channel link."))
1473// }
1474// proto::ChannelVisibility::Members => {
1475// (Some("icons/hash.svg"), Some("Copy private channel link."))
1476// }
1477// };
1478
1479// Some(channel.name.as_str())
1480// });
1481
1482// if let Some(name) = channel_name {
1483// Cow::Owned(format!("{}", name))
1484// } else {
1485// Cow::Borrowed("Current Call")
1486// }
1487// }
1488// Section::ContactRequests => Cow::Borrowed("Requests"),
1489// Section::Contacts => Cow::Borrowed("Contacts"),
1490// Section::Channels => Cow::Borrowed("Channels"),
1491// Section::ChannelInvites => Cow::Borrowed("Invites"),
1492// Section::Online => Cow::Borrowed("Online"),
1493// Section::Offline => Cow::Borrowed("Offline"),
1494// };
1495
1496// enum AddContact {}
1497// let button = match section {
1498// Section::ActiveCall => channel_link.map(|channel_link| {
1499// let channel_link_copy = channel_link.clone();
1500// MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
1501// render_icon_button(
1502// theme
1503// .collab_panel
1504// .leave_call_button
1505// .style_for(is_selected, state),
1506// "icons/link.svg",
1507// )
1508// })
1509// .with_cursor_style(CursorStyle::PointingHand)
1510// .on_click(MouseButton::Left, move |_, _, cx| {
1511// let item = ClipboardItem::new(channel_link_copy.clone());
1512// cx.write_to_clipboard(item)
1513// })
1514// .with_tooltip::<AddContact>(
1515// 0,
1516// channel_tooltip_text.unwrap(),
1517// None,
1518// tooltip_style.clone(),
1519// cx,
1520// )
1521// }),
1522// Section::Contacts => Some(
1523// MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
1524// render_icon_button(
1525// theme
1526// .collab_panel
1527// .add_contact_button
1528// .style_for(is_selected, state),
1529// "icons/plus.svg",
1530// )
1531// })
1532// .with_cursor_style(CursorStyle::PointingHand)
1533// .on_click(MouseButton::Left, |_, this, cx| {
1534// this.toggle_contact_finder(cx);
1535// })
1536// .with_tooltip::<LeaveCallContactList>(
1537// 0,
1538// "Search for new contact",
1539// None,
1540// tooltip_style.clone(),
1541// cx,
1542// ),
1543// ),
1544// Section::Channels => {
1545// if cx
1546// .global::<DragAndDrop<Workspace>>()
1547// .currently_dragged::<Channel>(cx.window())
1548// .is_some()
1549// && self.drag_target_channel == ChannelDragTarget::Root
1550// {
1551// is_dragged_over = true;
1552// }
1553
1554// Some(
1555// MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| {
1556// render_icon_button(
1557// theme
1558// .collab_panel
1559// .add_contact_button
1560// .style_for(is_selected, state),
1561// "icons/plus.svg",
1562// )
1563// })
1564// .with_cursor_style(CursorStyle::PointingHand)
1565// .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx))
1566// .with_tooltip::<AddChannel>(
1567// 0,
1568// "Create a channel",
1569// None,
1570// tooltip_style.clone(),
1571// cx,
1572// ),
1573// )
1574// }
1575// _ => None,
1576// };
1577
1578// let can_collapse = match section {
1579// Section::ActiveCall | Section::Channels | Section::Contacts => false,
1580// Section::ChannelInvites
1581// | Section::ContactRequests
1582// | Section::Online
1583// | Section::Offline => true,
1584// };
1585// let icon_size = (&theme.collab_panel).section_icon_size;
1586// let mut result = MouseEventHandler::new::<Header, _>(section as usize, cx, |state, _| {
1587// let header_style = if can_collapse {
1588// theme
1589// .collab_panel
1590// .subheader_row
1591// .in_state(is_selected)
1592// .style_for(state)
1593// } else {
1594// &theme.collab_panel.header_row
1595// };
1596
1597// Flex::row()
1598// .with_children(if can_collapse {
1599// Some(
1600// Svg::new(if is_collapsed {
1601// "icons/chevron_right.svg"
1602// } else {
1603// "icons/chevron_down.svg"
1604// })
1605// .with_color(header_style.text.color)
1606// .constrained()
1607// .with_max_width(icon_size)
1608// .with_max_height(icon_size)
1609// .aligned()
1610// .constrained()
1611// .with_width(icon_size)
1612// .contained()
1613// .with_margin_right(
1614// theme.collab_panel.contact_username.container.margin.left,
1615// ),
1616// )
1617// } else if let Some(channel_icon) = channel_icon {
1618// Some(
1619// Svg::new(channel_icon)
1620// .with_color(header_style.text.color)
1621// .constrained()
1622// .with_max_width(icon_size)
1623// .with_max_height(icon_size)
1624// .aligned()
1625// .constrained()
1626// .with_width(icon_size)
1627// .contained()
1628// .with_margin_right(
1629// theme.collab_panel.contact_username.container.margin.left,
1630// ),
1631// )
1632// } else {
1633// None
1634// })
1635// .with_child(
1636// Label::new(text, header_style.text.clone())
1637// .aligned()
1638// .left()
1639// .flex(1., true),
1640// )
1641// .with_children(button.map(|button| button.aligned().right()))
1642// .constrained()
1643// .with_height(theme.collab_panel.row_height)
1644// .contained()
1645// .with_style(if is_dragged_over {
1646// theme.collab_panel.dragged_over_header
1647// } else {
1648// header_style.container
1649// })
1650// });
1651
1652// result = result
1653// .on_move(move |_, this, cx| {
1654// if cx
1655// .global::<DragAndDrop<Workspace>>()
1656// .currently_dragged::<Channel>(cx.window())
1657// .is_some()
1658// {
1659// this.drag_target_channel = ChannelDragTarget::Root;
1660// cx.notify()
1661// }
1662// })
1663// .on_up(MouseButton::Left, move |_, this, cx| {
1664// if let Some((_, dragged_channel)) = cx
1665// .global::<DragAndDrop<Workspace>>()
1666// .currently_dragged::<Channel>(cx.window())
1667// {
1668// this.channel_store
1669// .update(cx, |channel_store, cx| {
1670// channel_store.move_channel(dragged_channel.id, None, cx)
1671// })
1672// .detach_and_log_err(cx)
1673// }
1674// });
1675
1676// if can_collapse {
1677// result = result
1678// .with_cursor_style(CursorStyle::PointingHand)
1679// .on_click(MouseButton::Left, move |_, this, cx| {
1680// if can_collapse {
1681// this.toggle_section_expanded(section, cx);
1682// }
1683// })
1684// }
1685
1686// result.into_any()
1687// }
1688
1689// fn render_contact(
1690// contact: &Contact,
1691// calling: bool,
1692// project: &ModelHandle<Project>,
1693// theme: &theme::Theme,
1694// is_selected: bool,
1695// cx: &mut ViewContext<Self>,
1696// ) -> AnyElement<Self> {
1697// enum ContactTooltip {}
1698
1699// let collab_theme = &theme.collab_panel;
1700// let online = contact.online;
1701// let busy = contact.busy || calling;
1702// let user_id = contact.user.id;
1703// let github_login = contact.user.github_login.clone();
1704// let initial_project = project.clone();
1705
1706// let event_handler =
1707// MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| {
1708// Flex::row()
1709// .with_children(contact.user.avatar.clone().map(|avatar| {
1710// let status_badge = if contact.online {
1711// Some(
1712// Empty::new()
1713// .collapsed()
1714// .contained()
1715// .with_style(if busy {
1716// collab_theme.contact_status_busy
1717// } else {
1718// collab_theme.contact_status_free
1719// })
1720// .aligned(),
1721// )
1722// } else {
1723// None
1724// };
1725// Stack::new()
1726// .with_child(
1727// Image::from_data(avatar)
1728// .with_style(collab_theme.contact_avatar)
1729// .aligned()
1730// .left(),
1731// )
1732// .with_children(status_badge)
1733// }))
1734// .with_child(
1735// Label::new(
1736// contact.user.github_login.clone(),
1737// collab_theme.contact_username.text.clone(),
1738// )
1739// .contained()
1740// .with_style(collab_theme.contact_username.container)
1741// .aligned()
1742// .left()
1743// .flex(1., true),
1744// )
1745// .with_children(if state.hovered() {
1746// Some(
1747// MouseEventHandler::new::<Cancel, _>(
1748// contact.user.id as usize,
1749// cx,
1750// |mouse_state, _| {
1751// let button_style =
1752// collab_theme.contact_button.style_for(mouse_state);
1753// render_icon_button(button_style, "icons/x.svg")
1754// .aligned()
1755// .flex_float()
1756// },
1757// )
1758// .with_padding(Padding::uniform(2.))
1759// .with_cursor_style(CursorStyle::PointingHand)
1760// .on_click(MouseButton::Left, move |_, this, cx| {
1761// this.remove_contact(user_id, &github_login, cx);
1762// })
1763// .flex_float(),
1764// )
1765// } else {
1766// None
1767// })
1768// .with_children(if calling {
1769// Some(
1770// Label::new("Calling", collab_theme.calling_indicator.text.clone())
1771// .contained()
1772// .with_style(collab_theme.calling_indicator.container)
1773// .aligned(),
1774// )
1775// } else {
1776// None
1777// })
1778// .constrained()
1779// .with_height(collab_theme.row_height)
1780// .contained()
1781// .with_style(
1782// *collab_theme
1783// .contact_row
1784// .in_state(is_selected)
1785// .style_for(state),
1786// )
1787// });
1788
1789// if online && !busy {
1790// let room = ActiveCall::global(cx).read(cx).room();
1791// let label = if room.is_some() {
1792// format!("Invite {} to join call", contact.user.github_login)
1793// } else {
1794// format!("Call {}", contact.user.github_login)
1795// };
1796
1797// event_handler
1798// .on_click(MouseButton::Left, move |_, this, cx| {
1799// this.call(user_id, Some(initial_project.clone()), cx);
1800// })
1801// .with_cursor_style(CursorStyle::PointingHand)
1802// .with_tooltip::<ContactTooltip>(
1803// contact.user.id as usize,
1804// label,
1805// None,
1806// theme.tooltip.clone(),
1807// cx,
1808// )
1809// .into_any()
1810// } else {
1811// event_handler
1812// .with_tooltip::<ContactTooltip>(
1813// contact.user.id as usize,
1814// format!(
1815// "{} is {}",
1816// contact.user.github_login,
1817// if busy { "on a call" } else { "offline" }
1818// ),
1819// None,
1820// theme.tooltip.clone(),
1821// cx,
1822// )
1823// .into_any()
1824// }
1825// }
1826
1827// fn render_contact_placeholder(
1828// &self,
1829// theme: &theme::CollabPanel,
1830// is_selected: bool,
1831// cx: &mut ViewContext<Self>,
1832// ) -> AnyElement<Self> {
1833// enum AddContacts {}
1834// MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
1835// let style = theme.list_empty_state.style_for(is_selected, state);
1836// Flex::row()
1837// .with_child(
1838// Svg::new("icons/plus.svg")
1839// .with_color(theme.list_empty_icon.color)
1840// .constrained()
1841// .with_width(theme.list_empty_icon.width)
1842// .aligned()
1843// .left(),
1844// )
1845// .with_child(
1846// Label::new("Add a contact", style.text.clone())
1847// .contained()
1848// .with_style(theme.list_empty_label_container),
1849// )
1850// .align_children_center()
1851// .contained()
1852// .with_style(style.container)
1853// .into_any()
1854// })
1855// .on_click(MouseButton::Left, |_, this, cx| {
1856// this.toggle_contact_finder(cx);
1857// })
1858// .into_any()
1859// }
1860
1861// fn render_channel_editor(
1862// &self,
1863// theme: &theme::Theme,
1864// depth: usize,
1865// cx: &AppContext,
1866// ) -> AnyElement<Self> {
1867// Flex::row()
1868// .with_child(
1869// Empty::new()
1870// .constrained()
1871// .with_width(theme.collab_panel.disclosure.button_space()),
1872// )
1873// .with_child(
1874// Svg::new("icons/hash.svg")
1875// .with_color(theme.collab_panel.channel_hash.color)
1876// .constrained()
1877// .with_width(theme.collab_panel.channel_hash.width)
1878// .aligned()
1879// .left(),
1880// )
1881// .with_child(
1882// if let Some(pending_name) = self
1883// .channel_editing_state
1884// .as_ref()
1885// .and_then(|state| state.pending_name())
1886// {
1887// Label::new(
1888// pending_name.to_string(),
1889// theme.collab_panel.contact_username.text.clone(),
1890// )
1891// .contained()
1892// .with_style(theme.collab_panel.contact_username.container)
1893// .aligned()
1894// .left()
1895// .flex(1., true)
1896// .into_any()
1897// } else {
1898// ChildView::new(&self.channel_name_editor, cx)
1899// .aligned()
1900// .left()
1901// .contained()
1902// .with_style(theme.collab_panel.channel_editor)
1903// .flex(1.0, true)
1904// .into_any()
1905// },
1906// )
1907// .align_children_center()
1908// .constrained()
1909// .with_height(theme.collab_panel.row_height)
1910// .contained()
1911// .with_style(ContainerStyle {
1912// background_color: Some(theme.editor.background),
1913// ..*theme.collab_panel.contact_row.default_style()
1914// })
1915// .with_padding_left(
1916// theme.collab_panel.contact_row.default_style().padding.left
1917// + theme.collab_panel.channel_indent * depth as f32,
1918// )
1919// .into_any()
1920// }
1921
1922// fn render_channel(
1923// &self,
1924// channel: &Channel,
1925// depth: usize,
1926// theme: &theme::Theme,
1927// is_selected: bool,
1928// has_children: bool,
1929// ix: usize,
1930// cx: &mut ViewContext<Self>,
1931// ) -> AnyElement<Self> {
1932// let channel_id = channel.id;
1933// let collab_theme = &theme.collab_panel;
1934// let is_public = self
1935// .channel_store
1936// .read(cx)
1937// .channel_for_id(channel_id)
1938// .map(|channel| channel.visibility)
1939// == Some(proto::ChannelVisibility::Public);
1940// let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
1941// let disclosed =
1942// has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
1943
1944// let is_active = maybe!({
1945// let call_channel = ActiveCall::global(cx)
1946// .read(cx)
1947// .room()?
1948// .read(cx)
1949// .channel_id()?;
1950// Some(call_channel == channel_id)
1951// })
1952// .unwrap_or(false);
1953
1954// const FACEPILE_LIMIT: usize = 3;
1955
1956// enum ChannelCall {}
1957// enum ChannelNote {}
1958// enum NotesTooltip {}
1959// enum ChatTooltip {}
1960// enum ChannelTooltip {}
1961
1962// let mut is_dragged_over = false;
1963// if cx
1964// .global::<DragAndDrop<Workspace>>()
1965// .currently_dragged::<Channel>(cx.window())
1966// .is_some()
1967// && self.drag_target_channel == ChannelDragTarget::Channel(channel_id)
1968// {
1969// is_dragged_over = true;
1970// }
1971
1972// let has_messages_notification = channel.unseen_message_id.is_some();
1973
1974// MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
1975// let row_hovered = state.hovered();
1976
1977// let mut select_state = |interactive: &Interactive<ContainerStyle>| {
1978// if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() {
1979// interactive.clicked.as_ref().unwrap().clone()
1980// } else if state.hovered() || other_selected {
1981// interactive
1982// .hovered
1983// .as_ref()
1984// .unwrap_or(&interactive.default)
1985// .clone()
1986// } else {
1987// interactive.default.clone()
1988// }
1989// };
1990
1991// Flex::<Self>::row()
1992// .with_child(
1993// Svg::new(if is_public {
1994// "icons/public.svg"
1995// } else {
1996// "icons/hash.svg"
1997// })
1998// .with_color(collab_theme.channel_hash.color)
1999// .constrained()
2000// .with_width(collab_theme.channel_hash.width)
2001// .aligned()
2002// .left(),
2003// )
2004// .with_child({
2005// let style = collab_theme.channel_name.inactive_state();
2006// Flex::row()
2007// .with_child(
2008// Label::new(channel.name.clone(), style.text.clone())
2009// .contained()
2010// .with_style(style.container)
2011// .aligned()
2012// .left()
2013// .with_tooltip::<ChannelTooltip>(
2014// ix,
2015// "Join channel",
2016// None,
2017// theme.tooltip.clone(),
2018// cx,
2019// ),
2020// )
2021// .with_children({
2022// let participants =
2023// self.channel_store.read(cx).channel_participants(channel_id);
2024
2025// if !participants.is_empty() {
2026// let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2027
2028// let result = FacePile::new(collab_theme.face_overlap)
2029// .with_children(
2030// participants
2031// .iter()
2032// .filter_map(|user| {
2033// Some(
2034// Image::from_data(user.avatar.clone()?)
2035// .with_style(collab_theme.channel_avatar),
2036// )
2037// })
2038// .take(FACEPILE_LIMIT),
2039// )
2040// .with_children((extra_count > 0).then(|| {
2041// Label::new(
2042// format!("+{}", extra_count),
2043// collab_theme.extra_participant_label.text.clone(),
2044// )
2045// .contained()
2046// .with_style(collab_theme.extra_participant_label.container)
2047// }));
2048
2049// Some(result)
2050// } else {
2051// None
2052// }
2053// })
2054// .with_spacing(8.)
2055// .align_children_center()
2056// .flex(1., true)
2057// })
2058// .with_child(
2059// MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| {
2060// let container_style = collab_theme
2061// .disclosure
2062// .button
2063// .style_for(mouse_state)
2064// .container;
2065
2066// if channel.unseen_message_id.is_some() {
2067// Svg::new("icons/conversations.svg")
2068// .with_color(collab_theme.channel_note_active_color)
2069// .constrained()
2070// .with_width(collab_theme.channel_hash.width)
2071// .contained()
2072// .with_style(container_style)
2073// .with_uniform_padding(4.)
2074// .into_any()
2075// } else if row_hovered {
2076// Svg::new("icons/conversations.svg")
2077// .with_color(collab_theme.channel_hash.color)
2078// .constrained()
2079// .with_width(collab_theme.channel_hash.width)
2080// .contained()
2081// .with_style(container_style)
2082// .with_uniform_padding(4.)
2083// .into_any()
2084// } else {
2085// Empty::new().into_any()
2086// }
2087// })
2088// .on_click(MouseButton::Left, move |_, this, cx| {
2089// this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
2090// })
2091// .with_tooltip::<ChatTooltip>(
2092// ix,
2093// "Open channel chat",
2094// None,
2095// theme.tooltip.clone(),
2096// cx,
2097// )
2098// .contained()
2099// .with_margin_right(4.),
2100// )
2101// .with_child(
2102// MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| {
2103// let container_style = collab_theme
2104// .disclosure
2105// .button
2106// .style_for(mouse_state)
2107// .container;
2108// if row_hovered || channel.unseen_note_version.is_some() {
2109// Svg::new("icons/file.svg")
2110// .with_color(if channel.unseen_note_version.is_some() {
2111// collab_theme.channel_note_active_color
2112// } else {
2113// collab_theme.channel_hash.color
2114// })
2115// .constrained()
2116// .with_width(collab_theme.channel_hash.width)
2117// .contained()
2118// .with_style(container_style)
2119// .with_uniform_padding(4.)
2120// .with_margin_right(collab_theme.channel_hash.container.margin.left)
2121// .with_tooltip::<NotesTooltip>(
2122// ix as usize,
2123// "Open channel notes",
2124// None,
2125// theme.tooltip.clone(),
2126// cx,
2127// )
2128// .into_any()
2129// } else if has_messages_notification {
2130// Empty::new()
2131// .constrained()
2132// .with_width(collab_theme.channel_hash.width)
2133// .contained()
2134// .with_uniform_padding(4.)
2135// .with_margin_right(collab_theme.channel_hash.container.margin.left)
2136// .into_any()
2137// } else {
2138// Empty::new().into_any()
2139// }
2140// })
2141// .on_click(MouseButton::Left, move |_, this, cx| {
2142// this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
2143// }),
2144// )
2145// .align_children_center()
2146// .styleable_component()
2147// .disclosable(
2148// disclosed,
2149// Box::new(ToggleCollapse {
2150// location: channel.id.clone(),
2151// }),
2152// )
2153// .with_id(ix)
2154// .with_style(collab_theme.disclosure.clone())
2155// .element()
2156// .constrained()
2157// .with_height(collab_theme.row_height)
2158// .contained()
2159// .with_style(select_state(
2160// collab_theme
2161// .channel_row
2162// .in_state(is_selected || is_active || is_dragged_over),
2163// ))
2164// .with_padding_left(
2165// collab_theme.channel_row.default_style().padding.left
2166// + collab_theme.channel_indent * depth as f32,
2167// )
2168// })
2169// .on_click(MouseButton::Left, move |_, this, cx| {
2170// if this.drag_target_channel == ChannelDragTarget::None {
2171// if is_active {
2172// this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
2173// } else {
2174// this.join_channel(channel_id, cx)
2175// }
2176// }
2177// })
2178// .on_click(MouseButton::Right, {
2179// let channel = channel.clone();
2180// move |e, this, cx| {
2181// this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx);
2182// }
2183// })
2184// .on_up(MouseButton::Left, move |_, this, cx| {
2185// if let Some((_, dragged_channel)) = cx
2186// .global::<DragAndDrop<Workspace>>()
2187// .currently_dragged::<Channel>(cx.window())
2188// {
2189// this.channel_store
2190// .update(cx, |channel_store, cx| {
2191// channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
2192// })
2193// .detach_and_log_err(cx)
2194// }
2195// })
2196// .on_move({
2197// let channel = channel.clone();
2198// move |_, this, cx| {
2199// if let Some((_, dragged_channel)) = cx
2200// .global::<DragAndDrop<Workspace>>()
2201// .currently_dragged::<Channel>(cx.window())
2202// {
2203// if channel.id != dragged_channel.id {
2204// this.drag_target_channel = ChannelDragTarget::Channel(channel.id);
2205// }
2206// cx.notify()
2207// }
2208// }
2209// })
2210// .as_draggable::<_, Channel>(
2211// channel.clone(),
2212// move |_, channel, cx: &mut ViewContext<Workspace>| {
2213// let theme = &theme::current(cx).collab_panel;
2214
2215// Flex::<Workspace>::row()
2216// .with_child(
2217// Svg::new("icons/hash.svg")
2218// .with_color(theme.channel_hash.color)
2219// .constrained()
2220// .with_width(theme.channel_hash.width)
2221// .aligned()
2222// .left(),
2223// )
2224// .with_child(
2225// Label::new(channel.name.clone(), theme.channel_name.text.clone())
2226// .contained()
2227// .with_style(theme.channel_name.container)
2228// .aligned()
2229// .left(),
2230// )
2231// .align_children_center()
2232// .contained()
2233// .with_background_color(
2234// theme
2235// .container
2236// .background_color
2237// .unwrap_or(gpui::color::Color::transparent_black()),
2238// )
2239// .contained()
2240// .with_padding_left(
2241// theme.channel_row.default_style().padding.left
2242// + theme.channel_indent * depth as f32,
2243// )
2244// .into_any()
2245// },
2246// )
2247// .with_cursor_style(CursorStyle::PointingHand)
2248// .into_any()
2249// }
2250
2251// fn render_channel_notes(
2252// &self,
2253// channel_id: ChannelId,
2254// theme: &theme::CollabPanel,
2255// is_selected: bool,
2256// ix: usize,
2257// cx: &mut ViewContext<Self>,
2258// ) -> AnyElement<Self> {
2259// enum ChannelNotes {}
2260// let host_avatar_width = theme
2261// .contact_avatar
2262// .width
2263// .or(theme.contact_avatar.height)
2264// .unwrap_or(0.);
2265
2266// MouseEventHandler::new::<ChannelNotes, _>(ix as usize, cx, |state, cx| {
2267// let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
2268// let row = theme.project_row.in_state(is_selected).style_for(state);
2269
2270// Flex::<Self>::row()
2271// .with_child(render_tree_branch(
2272// tree_branch,
2273// &row.name.text,
2274// false,
2275// vec2f(host_avatar_width, theme.row_height),
2276// cx.font_cache(),
2277// ))
2278// .with_child(
2279// Svg::new("icons/file.svg")
2280// .with_color(theme.channel_hash.color)
2281// .constrained()
2282// .with_width(theme.channel_hash.width)
2283// .aligned()
2284// .left(),
2285// )
2286// .with_child(
2287// Label::new("notes", theme.channel_name.text.clone())
2288// .contained()
2289// .with_style(theme.channel_name.container)
2290// .aligned()
2291// .left()
2292// .flex(1., true),
2293// )
2294// .constrained()
2295// .with_height(theme.row_height)
2296// .contained()
2297// .with_style(*theme.channel_row.style_for(is_selected, state))
2298// .with_padding_left(theme.channel_row.default_style().padding.left)
2299// })
2300// .on_click(MouseButton::Left, move |_, this, cx| {
2301// this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
2302// })
2303// .with_cursor_style(CursorStyle::PointingHand)
2304// .into_any()
2305// }
2306
2307// fn render_channel_chat(
2308// &self,
2309// channel_id: ChannelId,
2310// theme: &theme::CollabPanel,
2311// is_selected: bool,
2312// ix: usize,
2313// cx: &mut ViewContext<Self>,
2314// ) -> AnyElement<Self> {
2315// enum ChannelChat {}
2316// let host_avatar_width = theme
2317// .contact_avatar
2318// .width
2319// .or(theme.contact_avatar.height)
2320// .unwrap_or(0.);
2321
2322// MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
2323// let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
2324// let row = theme.project_row.in_state(is_selected).style_for(state);
2325
2326// Flex::<Self>::row()
2327// .with_child(render_tree_branch(
2328// tree_branch,
2329// &row.name.text,
2330// true,
2331// vec2f(host_avatar_width, theme.row_height),
2332// cx.font_cache(),
2333// ))
2334// .with_child(
2335// Svg::new("icons/conversations.svg")
2336// .with_color(theme.channel_hash.color)
2337// .constrained()
2338// .with_width(theme.channel_hash.width)
2339// .aligned()
2340// .left(),
2341// )
2342// .with_child(
2343// Label::new("chat", theme.channel_name.text.clone())
2344// .contained()
2345// .with_style(theme.channel_name.container)
2346// .aligned()
2347// .left()
2348// .flex(1., true),
2349// )
2350// .constrained()
2351// .with_height(theme.row_height)
2352// .contained()
2353// .with_style(*theme.channel_row.style_for(is_selected, state))
2354// .with_padding_left(theme.channel_row.default_style().padding.left)
2355// })
2356// .on_click(MouseButton::Left, move |_, this, cx| {
2357// this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
2358// })
2359// .with_cursor_style(CursorStyle::PointingHand)
2360// .into_any()
2361// }
2362
2363// fn render_channel_invite(
2364// channel: Arc<Channel>,
2365// channel_store: ModelHandle<ChannelStore>,
2366// theme: &theme::CollabPanel,
2367// is_selected: bool,
2368// cx: &mut ViewContext<Self>,
2369// ) -> AnyElement<Self> {
2370// enum Decline {}
2371// enum Accept {}
2372
2373// let channel_id = channel.id;
2374// let is_invite_pending = channel_store
2375// .read(cx)
2376// .has_pending_channel_invite_response(&channel);
2377// let button_spacing = theme.contact_button_spacing;
2378
2379// Flex::row()
2380// .with_child(
2381// Svg::new("icons/hash.svg")
2382// .with_color(theme.channel_hash.color)
2383// .constrained()
2384// .with_width(theme.channel_hash.width)
2385// .aligned()
2386// .left(),
2387// )
2388// .with_child(
2389// Label::new(channel.name.clone(), theme.contact_username.text.clone())
2390// .contained()
2391// .with_style(theme.contact_username.container)
2392// .aligned()
2393// .left()
2394// .flex(1., true),
2395// )
2396// .with_child(
2397// MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
2398// let button_style = if is_invite_pending {
2399// &theme.disabled_button
2400// } else {
2401// theme.contact_button.style_for(mouse_state)
2402// };
2403// render_icon_button(button_style, "icons/x.svg").aligned()
2404// })
2405// .with_cursor_style(CursorStyle::PointingHand)
2406// .on_click(MouseButton::Left, move |_, this, cx| {
2407// this.respond_to_channel_invite(channel_id, false, cx);
2408// })
2409// .contained()
2410// .with_margin_right(button_spacing),
2411// )
2412// .with_child(
2413// MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
2414// let button_style = if is_invite_pending {
2415// &theme.disabled_button
2416// } else {
2417// theme.contact_button.style_for(mouse_state)
2418// };
2419// render_icon_button(button_style, "icons/check.svg")
2420// .aligned()
2421// .flex_float()
2422// })
2423// .with_cursor_style(CursorStyle::PointingHand)
2424// .on_click(MouseButton::Left, move |_, this, cx| {
2425// this.respond_to_channel_invite(channel_id, true, cx);
2426// }),
2427// )
2428// .constrained()
2429// .with_height(theme.row_height)
2430// .contained()
2431// .with_style(
2432// *theme
2433// .contact_row
2434// .in_state(is_selected)
2435// .style_for(&mut Default::default()),
2436// )
2437// .with_padding_left(
2438// theme.contact_row.default_style().padding.left + theme.channel_indent,
2439// )
2440// .into_any()
2441// }
2442
2443// fn render_contact_request(
2444// user: Arc<User>,
2445// user_store: ModelHandle<UserStore>,
2446// theme: &theme::CollabPanel,
2447// is_incoming: bool,
2448// is_selected: bool,
2449// cx: &mut ViewContext<Self>,
2450// ) -> AnyElement<Self> {
2451// enum Decline {}
2452// enum Accept {}
2453// enum Cancel {}
2454
2455// let mut row = Flex::row()
2456// .with_children(user.avatar.clone().map(|avatar| {
2457// Image::from_data(avatar)
2458// .with_style(theme.contact_avatar)
2459// .aligned()
2460// .left()
2461// }))
2462// .with_child(
2463// Label::new(
2464// user.github_login.clone(),
2465// theme.contact_username.text.clone(),
2466// )
2467// .contained()
2468// .with_style(theme.contact_username.container)
2469// .aligned()
2470// .left()
2471// .flex(1., true),
2472// );
2473
2474// let user_id = user.id;
2475// let github_login = user.github_login.clone();
2476// let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
2477// let button_spacing = theme.contact_button_spacing;
2478
2479// if is_incoming {
2480// row.add_child(
2481// MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| {
2482// let button_style = if is_contact_request_pending {
2483// &theme.disabled_button
2484// } else {
2485// theme.contact_button.style_for(mouse_state)
2486// };
2487// render_icon_button(button_style, "icons/x.svg").aligned()
2488// })
2489// .with_cursor_style(CursorStyle::PointingHand)
2490// .on_click(MouseButton::Left, move |_, this, cx| {
2491// this.respond_to_contact_request(user_id, false, cx);
2492// })
2493// .contained()
2494// .with_margin_right(button_spacing),
2495// );
2496
2497// row.add_child(
2498// MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| {
2499// let button_style = if is_contact_request_pending {
2500// &theme.disabled_button
2501// } else {
2502// theme.contact_button.style_for(mouse_state)
2503// };
2504// render_icon_button(button_style, "icons/check.svg")
2505// .aligned()
2506// .flex_float()
2507// })
2508// .with_cursor_style(CursorStyle::PointingHand)
2509// .on_click(MouseButton::Left, move |_, this, cx| {
2510// this.respond_to_contact_request(user_id, true, cx);
2511// }),
2512// );
2513// } else {
2514// row.add_child(
2515// MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| {
2516// let button_style = if is_contact_request_pending {
2517// &theme.disabled_button
2518// } else {
2519// theme.contact_button.style_for(mouse_state)
2520// };
2521// render_icon_button(button_style, "icons/x.svg")
2522// .aligned()
2523// .flex_float()
2524// })
2525// .with_padding(Padding::uniform(2.))
2526// .with_cursor_style(CursorStyle::PointingHand)
2527// .on_click(MouseButton::Left, move |_, this, cx| {
2528// this.remove_contact(user_id, &github_login, cx);
2529// })
2530// .flex_float(),
2531// );
2532// }
2533
2534// row.constrained()
2535// .with_height(theme.row_height)
2536// .contained()
2537// .with_style(
2538// *theme
2539// .contact_row
2540// .in_state(is_selected)
2541// .style_for(&mut Default::default()),
2542// )
2543// .into_any()
2544// }
2545
2546// fn has_subchannels(&self, ix: usize) -> bool {
2547// self.entries.get(ix).map_or(false, |entry| {
2548// if let ListEntry::Channel { has_children, .. } = entry {
2549// *has_children
2550// } else {
2551// false
2552// }
2553// })
2554// }
2555
2556// fn deploy_channel_context_menu(
2557// &mut self,
2558// position: Option<Vector2F>,
2559// channel: &Channel,
2560// ix: usize,
2561// cx: &mut ViewContext<Self>,
2562// ) {
2563// self.context_menu_on_selected = position.is_none();
2564
2565// let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
2566// self.channel_store
2567// .read(cx)
2568// .channel_for_id(clipboard.channel_id)
2569// .map(|channel| channel.name.clone())
2570// });
2571
2572// self.context_menu.update(cx, |context_menu, cx| {
2573// context_menu.set_position_mode(if self.context_menu_on_selected {
2574// OverlayPositionMode::Local
2575// } else {
2576// OverlayPositionMode::Window
2577// });
2578
2579// let mut items = Vec::new();
2580
2581// let select_action_name = if self.selection == Some(ix) {
2582// "Unselect"
2583// } else {
2584// "Select"
2585// };
2586
2587// items.push(ContextMenuItem::action(
2588// select_action_name,
2589// ToggleSelectedIx { ix },
2590// ));
2591
2592// if self.has_subchannels(ix) {
2593// let expand_action_name = if self.is_channel_collapsed(channel.id) {
2594// "Expand Subchannels"
2595// } else {
2596// "Collapse Subchannels"
2597// };
2598// items.push(ContextMenuItem::action(
2599// expand_action_name,
2600// ToggleCollapse {
2601// location: channel.id,
2602// },
2603// ));
2604// }
2605
2606// items.push(ContextMenuItem::action(
2607// "Open Notes",
2608// OpenChannelNotes {
2609// channel_id: channel.id,
2610// },
2611// ));
2612
2613// items.push(ContextMenuItem::action(
2614// "Open Chat",
2615// JoinChannelChat {
2616// channel_id: channel.id,
2617// },
2618// ));
2619
2620// items.push(ContextMenuItem::action(
2621// "Copy Channel Link",
2622// CopyChannelLink {
2623// channel_id: channel.id,
2624// },
2625// ));
2626
2627// if self.channel_store.read(cx).is_channel_admin(channel.id) {
2628// items.extend([
2629// ContextMenuItem::Separator,
2630// ContextMenuItem::action(
2631// "New Subchannel",
2632// NewChannel {
2633// location: channel.id,
2634// },
2635// ),
2636// ContextMenuItem::action(
2637// "Rename",
2638// RenameChannel {
2639// channel_id: channel.id,
2640// },
2641// ),
2642// ContextMenuItem::action(
2643// "Move this channel",
2644// StartMoveChannelFor {
2645// channel_id: channel.id,
2646// },
2647// ),
2648// ]);
2649
2650// if let Some(channel_name) = clipboard_channel_name {
2651// items.push(ContextMenuItem::Separator);
2652// items.push(ContextMenuItem::action(
2653// format!("Move '#{}' here", channel_name),
2654// MoveChannel { to: channel.id },
2655// ));
2656// }
2657
2658// items.extend([
2659// ContextMenuItem::Separator,
2660// ContextMenuItem::action(
2661// "Invite Members",
2662// InviteMembers {
2663// channel_id: channel.id,
2664// },
2665// ),
2666// ContextMenuItem::action(
2667// "Manage Members",
2668// ManageMembers {
2669// channel_id: channel.id,
2670// },
2671// ),
2672// ContextMenuItem::Separator,
2673// ContextMenuItem::action(
2674// "Delete",
2675// RemoveChannel {
2676// channel_id: channel.id,
2677// },
2678// ),
2679// ]);
2680// }
2681
2682// context_menu.show(
2683// position.unwrap_or_default(),
2684// if self.context_menu_on_selected {
2685// gpui::elements::AnchorCorner::TopRight
2686// } else {
2687// gpui::elements::AnchorCorner::BottomLeft
2688// },
2689// items,
2690// cx,
2691// );
2692// });
2693
2694// cx.notify();
2695// }
2696
2697// fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
2698// if self.take_editing_state(cx) {
2699// cx.focus(&self.filter_editor);
2700// } else {
2701// self.filter_editor.update(cx, |editor, cx| {
2702// if editor.buffer().read(cx).len(cx) > 0 {
2703// editor.set_text("", cx);
2704// }
2705// });
2706// }
2707
2708// self.update_entries(false, cx);
2709// }
2710
2711// fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
2712// let ix = self.selection.map_or(0, |ix| ix + 1);
2713// if ix < self.entries.len() {
2714// self.selection = Some(ix);
2715// }
2716
2717// self.list_state.reset(self.entries.len());
2718// if let Some(ix) = self.selection {
2719// self.list_state.scroll_to(ListOffset {
2720// item_ix: ix,
2721// offset_in_item: 0.,
2722// });
2723// }
2724// cx.notify();
2725// }
2726
2727// fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
2728// let ix = self.selection.take().unwrap_or(0);
2729// if ix > 0 {
2730// self.selection = Some(ix - 1);
2731// }
2732
2733// self.list_state.reset(self.entries.len());
2734// if let Some(ix) = self.selection {
2735// self.list_state.scroll_to(ListOffset {
2736// item_ix: ix,
2737// offset_in_item: 0.,
2738// });
2739// }
2740// cx.notify();
2741// }
2742
2743// fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
2744// if self.confirm_channel_edit(cx) {
2745// return;
2746// }
2747
2748// if let Some(selection) = self.selection {
2749// if let Some(entry) = self.entries.get(selection) {
2750// match entry {
2751// ListEntry::Header(section) => match section {
2752// Section::ActiveCall => Self::leave_call(cx),
2753// Section::Channels => self.new_root_channel(cx),
2754// Section::Contacts => self.toggle_contact_finder(cx),
2755// Section::ContactRequests
2756// | Section::Online
2757// | Section::Offline
2758// | Section::ChannelInvites => {
2759// self.toggle_section_expanded(*section, cx);
2760// }
2761// },
2762// ListEntry::Contact { contact, calling } => {
2763// if contact.online && !contact.busy && !calling {
2764// self.call(contact.user.id, Some(self.project.clone()), cx);
2765// }
2766// }
2767// ListEntry::ParticipantProject {
2768// project_id,
2769// host_user_id,
2770// ..
2771// } => {
2772// if let Some(workspace) = self.workspace.upgrade(cx) {
2773// let app_state = workspace.read(cx).app_state().clone();
2774// workspace::join_remote_project(
2775// *project_id,
2776// *host_user_id,
2777// app_state,
2778// cx,
2779// )
2780// .detach_and_log_err(cx);
2781// }
2782// }
2783// ListEntry::ParticipantScreen { peer_id, .. } => {
2784// let Some(peer_id) = peer_id else {
2785// return;
2786// };
2787// if let Some(workspace) = self.workspace.upgrade(cx) {
2788// workspace.update(cx, |workspace, cx| {
2789// workspace.open_shared_screen(*peer_id, cx)
2790// });
2791// }
2792// }
2793// ListEntry::Channel { channel, .. } => {
2794// let is_active = maybe!({
2795// let call_channel = ActiveCall::global(cx)
2796// .read(cx)
2797// .room()?
2798// .read(cx)
2799// .channel_id()?;
2800
2801// Some(call_channel == channel.id)
2802// })
2803// .unwrap_or(false);
2804// if is_active {
2805// self.open_channel_notes(
2806// &OpenChannelNotes {
2807// channel_id: channel.id,
2808// },
2809// cx,
2810// )
2811// } else {
2812// self.join_channel(channel.id, cx)
2813// }
2814// }
2815// ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
2816// _ => {}
2817// }
2818// }
2819// }
2820// }
2821
2822// fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
2823// if self.channel_editing_state.is_some() {
2824// self.channel_name_editor.update(cx, |editor, cx| {
2825// editor.insert(" ", cx);
2826// });
2827// }
2828// }
2829
2830// fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
2831// if let Some(editing_state) = &mut self.channel_editing_state {
2832// match editing_state {
2833// ChannelEditingState::Create {
2834// location,
2835// pending_name,
2836// ..
2837// } => {
2838// if pending_name.is_some() {
2839// return false;
2840// }
2841// let channel_name = self.channel_name_editor.read(cx).text(cx);
2842
2843// *pending_name = Some(channel_name.clone());
2844
2845// self.channel_store
2846// .update(cx, |channel_store, cx| {
2847// channel_store.create_channel(&channel_name, *location, cx)
2848// })
2849// .detach();
2850// cx.notify();
2851// }
2852// ChannelEditingState::Rename {
2853// location,
2854// pending_name,
2855// } => {
2856// if pending_name.is_some() {
2857// return false;
2858// }
2859// let channel_name = self.channel_name_editor.read(cx).text(cx);
2860// *pending_name = Some(channel_name.clone());
2861
2862// self.channel_store
2863// .update(cx, |channel_store, cx| {
2864// channel_store.rename(*location, &channel_name, cx)
2865// })
2866// .detach();
2867// cx.notify();
2868// }
2869// }
2870// cx.focus_self();
2871// true
2872// } else {
2873// false
2874// }
2875// }
2876
2877// fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
2878// if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
2879// self.collapsed_sections.remove(ix);
2880// } else {
2881// self.collapsed_sections.push(section);
2882// }
2883// self.update_entries(false, cx);
2884// }
2885
2886// fn collapse_selected_channel(
2887// &mut self,
2888// _: &CollapseSelectedChannel,
2889// cx: &mut ViewContext<Self>,
2890// ) {
2891// let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
2892// return;
2893// };
2894
2895// if self.is_channel_collapsed(channel_id) {
2896// return;
2897// }
2898
2899// self.toggle_channel_collapsed(channel_id, cx);
2900// }
2901
2902// fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
2903// let Some(id) = self.selected_channel().map(|channel| channel.id) else {
2904// return;
2905// };
2906
2907// if !self.is_channel_collapsed(id) {
2908// return;
2909// }
2910
2911// self.toggle_channel_collapsed(id, cx)
2912// }
2913
2914// fn toggle_channel_collapsed_action(
2915// &mut self,
2916// action: &ToggleCollapse,
2917// cx: &mut ViewContext<Self>,
2918// ) {
2919// self.toggle_channel_collapsed(action.location, cx);
2920// }
2921
2922// fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2923// match self.collapsed_channels.binary_search(&channel_id) {
2924// Ok(ix) => {
2925// self.collapsed_channels.remove(ix);
2926// }
2927// Err(ix) => {
2928// self.collapsed_channels.insert(ix, channel_id);
2929// }
2930// };
2931// self.serialize(cx);
2932// self.update_entries(true, cx);
2933// cx.notify();
2934// cx.focus_self();
2935// }
2936
2937// fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
2938// self.collapsed_channels.binary_search(&channel_id).is_ok()
2939// }
2940
2941// fn leave_call(cx: &mut ViewContext<Self>) {
2942// ActiveCall::global(cx)
2943// .update(cx, |call, cx| call.hang_up(cx))
2944// .detach_and_log_err(cx);
2945// }
2946
2947// fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
2948// if let Some(workspace) = self.workspace.upgrade(cx) {
2949// workspace.update(cx, |workspace, cx| {
2950// workspace.toggle_modal(cx, |_, cx| {
2951// cx.add_view(|cx| {
2952// let mut finder = ContactFinder::new(self.user_store.clone(), cx);
2953// finder.set_query(self.filter_editor.read(cx).text(cx), cx);
2954// finder
2955// })
2956// });
2957// });
2958// }
2959// }
2960
2961// fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
2962// self.channel_editing_state = Some(ChannelEditingState::Create {
2963// location: None,
2964// pending_name: None,
2965// });
2966// self.update_entries(false, cx);
2967// self.select_channel_editor();
2968// cx.focus(self.channel_name_editor.as_any());
2969// cx.notify();
2970// }
2971
2972// fn select_channel_editor(&mut self) {
2973// self.selection = self.entries.iter().position(|entry| match entry {
2974// ListEntry::ChannelEditor { .. } => true,
2975// _ => false,
2976// });
2977// }
2978
2979// fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
2980// self.collapsed_channels
2981// .retain(|channel| *channel != action.location);
2982// self.channel_editing_state = Some(ChannelEditingState::Create {
2983// location: Some(action.location.to_owned()),
2984// pending_name: None,
2985// });
2986// self.update_entries(false, cx);
2987// self.select_channel_editor();
2988// cx.focus(self.channel_name_editor.as_any());
2989// cx.notify();
2990// }
2991
2992// fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext<Self>) {
2993// self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx);
2994// }
2995
2996// fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext<Self>) {
2997// self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx);
2998// }
2999
3000// fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
3001// if let Some(channel) = self.selected_channel() {
3002// self.remove_channel(channel.id, cx)
3003// }
3004// }
3005
3006// fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
3007// if let Some(channel) = self.selected_channel() {
3008// self.rename_channel(
3009// &RenameChannel {
3010// channel_id: channel.id,
3011// },
3012// cx,
3013// );
3014// }
3015// }
3016
3017// fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
3018// let channel_store = self.channel_store.read(cx);
3019// if !channel_store.is_channel_admin(action.channel_id) {
3020// return;
3021// }
3022// if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() {
3023// self.channel_editing_state = Some(ChannelEditingState::Rename {
3024// location: action.channel_id.to_owned(),
3025// pending_name: None,
3026// });
3027// self.channel_name_editor.update(cx, |editor, cx| {
3028// editor.set_text(channel.name.clone(), cx);
3029// editor.select_all(&Default::default(), cx);
3030// });
3031// cx.focus(self.channel_name_editor.as_any());
3032// self.update_entries(false, cx);
3033// self.select_channel_editor();
3034// }
3035// }
3036
3037// fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
3038// if let Some(workspace) = self.workspace.upgrade(cx) {
3039// ChannelView::open(action.channel_id, workspace, cx).detach();
3040// }
3041// }
3042
3043// fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
3044// let Some(channel) = self.selected_channel() else {
3045// return;
3046// };
3047
3048// self.deploy_channel_context_menu(None, &channel.clone(), self.selection.unwrap(), cx);
3049// }
3050
3051// fn selected_channel(&self) -> Option<&Arc<Channel>> {
3052// self.selection
3053// .and_then(|ix| self.entries.get(ix))
3054// .and_then(|entry| match entry {
3055// ListEntry::Channel { channel, .. } => Some(channel),
3056// _ => None,
3057// })
3058// }
3059
3060// fn show_channel_modal(
3061// &mut self,
3062// channel_id: ChannelId,
3063// mode: channel_modal::Mode,
3064// cx: &mut ViewContext<Self>,
3065// ) {
3066// let workspace = self.workspace.clone();
3067// let user_store = self.user_store.clone();
3068// let channel_store = self.channel_store.clone();
3069// let members = self.channel_store.update(cx, |channel_store, cx| {
3070// channel_store.get_channel_member_details(channel_id, cx)
3071// });
3072
3073// cx.spawn(|_, mut cx| async move {
3074// let members = members.await?;
3075// workspace.update(&mut cx, |workspace, cx| {
3076// workspace.toggle_modal(cx, |_, cx| {
3077// cx.add_view(|cx| {
3078// ChannelModal::new(
3079// user_store.clone(),
3080// channel_store.clone(),
3081// channel_id,
3082// mode,
3083// members,
3084// cx,
3085// )
3086// })
3087// });
3088// })
3089// })
3090// .detach();
3091// }
3092
3093// fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
3094// self.remove_channel(action.channel_id, cx)
3095// }
3096
3097// fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
3098// let channel_store = self.channel_store.clone();
3099// if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
3100// let prompt_message = format!(
3101// "Are you sure you want to remove the channel \"{}\"?",
3102// channel.name
3103// );
3104// let mut answer =
3105// cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
3106// let window = cx.window();
3107// cx.spawn(|this, mut cx| async move {
3108// if answer.next().await == Some(0) {
3109// if let Err(e) = channel_store
3110// .update(&mut cx, |channels, _| channels.remove_channel(channel_id))
3111// .await
3112// {
3113// window.prompt(
3114// PromptLevel::Info,
3115// &format!("Failed to remove channel: {}", e),
3116// &["Ok"],
3117// &mut cx,
3118// );
3119// }
3120// this.update(&mut cx, |_, cx| cx.focus_self()).ok();
3121// }
3122// })
3123// .detach();
3124// }
3125// }
3126
3127// // Should move to the filter editor if clicking on it
3128// // Should move selection to the channel editor if activating it
3129
3130// fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
3131// let user_store = self.user_store.clone();
3132// let prompt_message = format!(
3133// "Are you sure you want to remove \"{}\" from your contacts?",
3134// github_login
3135// );
3136// let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
3137// let window = cx.window();
3138// cx.spawn(|_, mut cx| async move {
3139// if answer.next().await == Some(0) {
3140// if let Err(e) = user_store
3141// .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
3142// .await
3143// {
3144// window.prompt(
3145// PromptLevel::Info,
3146// &format!("Failed to remove contact: {}", e),
3147// &["Ok"],
3148// &mut cx,
3149// );
3150// }
3151// }
3152// })
3153// .detach();
3154// }
3155
3156// fn respond_to_contact_request(
3157// &mut self,
3158// user_id: u64,
3159// accept: bool,
3160// cx: &mut ViewContext<Self>,
3161// ) {
3162// self.user_store
3163// .update(cx, |store, cx| {
3164// store.respond_to_contact_request(user_id, accept, cx)
3165// })
3166// .detach();
3167// }
3168
3169// fn respond_to_channel_invite(
3170// &mut self,
3171// channel_id: u64,
3172// accept: bool,
3173// cx: &mut ViewContext<Self>,
3174// ) {
3175// self.channel_store
3176// .update(cx, |store, cx| {
3177// store.respond_to_channel_invite(channel_id, accept, cx)
3178// })
3179// .detach();
3180// }
3181
3182// fn call(
3183// &mut self,
3184// recipient_user_id: u64,
3185// initial_project: Option<ModelHandle<Project>>,
3186// cx: &mut ViewContext<Self>,
3187// ) {
3188// ActiveCall::global(cx)
3189// .update(cx, |call, cx| {
3190// call.invite(recipient_user_id, initial_project, cx)
3191// })
3192// .detach_and_log_err(cx);
3193// }
3194
3195// fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
3196// let Some(workspace) = self.workspace.upgrade(cx) else {
3197// return;
3198// };
3199// let Some(handle) = cx.window().downcast::<Workspace>() else {
3200// return;
3201// };
3202// workspace::join_channel(
3203// channel_id,
3204// workspace.read(cx).app_state().clone(),
3205// Some(handle),
3206// cx,
3207// )
3208// .detach_and_log_err(cx)
3209// }
3210
3211// fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) {
3212// let channel_id = action.channel_id;
3213// if let Some(workspace) = self.workspace.upgrade(cx) {
3214// cx.app_context().defer(move |cx| {
3215// workspace.update(cx, |workspace, cx| {
3216// if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
3217// panel.update(cx, |panel, cx| {
3218// panel
3219// .select_channel(channel_id, None, cx)
3220// .detach_and_log_err(cx);
3221// });
3222// }
3223// });
3224// });
3225// }
3226// }
3227
3228// fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext<Self>) {
3229// let channel_store = self.channel_store.read(cx);
3230// let Some(channel) = channel_store.channel_for_id(action.channel_id) else {
3231// return;
3232// };
3233// let item = ClipboardItem::new(channel.link());
3234// cx.write_to_clipboard(item)
3235// }
3236// }
3237
3238// fn render_tree_branch(
3239// branch_style: theme::TreeBranch,
3240// row_style: &TextStyle,
3241// is_last: bool,
3242// size: Vector2F,
3243// font_cache: &FontCache,
3244// ) -> gpui::elements::ConstrainedBox<CollabPanel> {
3245// let line_height = row_style.line_height(font_cache);
3246// let cap_height = row_style.cap_height(font_cache);
3247// let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.;
3248
3249// Canvas::new(move |bounds, _, _, cx| {
3250// cx.paint_layer(None, |cx| {
3251// let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.);
3252// let end_x = bounds.max_x();
3253// let start_y = bounds.min_y();
3254// let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
3255
3256// cx.scene().push_quad(gpui::Quad {
3257// bounds: RectF::from_points(
3258// vec2f(start_x, start_y),
3259// vec2f(
3260// start_x + branch_style.width,
3261// if is_last { end_y } else { bounds.max_y() },
3262// ),
3263// ),
3264// background: Some(branch_style.color),
3265// border: gpui::Border::default(),
3266// corner_radii: (0.).into(),
3267// });
3268// cx.scene().push_quad(gpui::Quad {
3269// bounds: RectF::from_points(
3270// vec2f(start_x, end_y),
3271// vec2f(end_x, end_y + branch_style.width),
3272// ),
3273// background: Some(branch_style.color),
3274// border: gpui::Border::default(),
3275// corner_radii: (0.).into(),
3276// });
3277// })
3278// })
3279// .constrained()
3280// .with_width(size.x())
3281// }
3282
3283// impl View for CollabPanel {
3284// fn ui_name() -> &'static str {
3285// "CollabPanel"
3286// }
3287
3288// fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
3289// if !self.has_focus {
3290// self.has_focus = true;
3291// if !self.context_menu.is_focused(cx) {
3292// if let Some(editing_state) = &self.channel_editing_state {
3293// if editing_state.pending_name().is_none() {
3294// cx.focus(&self.channel_name_editor);
3295// } else {
3296// cx.focus(&self.filter_editor);
3297// }
3298// } else {
3299// cx.focus(&self.filter_editor);
3300// }
3301// }
3302// cx.emit(Event::Focus);
3303// }
3304// }
3305
3306// fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
3307// self.has_focus = false;
3308// }
3309
3310// fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
3311// let theme = &theme::current(cx).collab_panel;
3312
3313// if self.user_store.read(cx).current_user().is_none() {
3314// enum LogInButton {}
3315
3316// return Flex::column()
3317// .with_child(
3318// MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
3319// let button = theme.log_in_button.style_for(state);
3320// Label::new("Sign in to collaborate", button.text.clone())
3321// .aligned()
3322// .left()
3323// .contained()
3324// .with_style(button.container)
3325// })
3326// .on_click(MouseButton::Left, |_, this, cx| {
3327// let client = this.client.clone();
3328// cx.spawn(|_, cx| async move {
3329// client.authenticate_and_connect(true, &cx).await.log_err();
3330// })
3331// .detach();
3332// })
3333// .with_cursor_style(CursorStyle::PointingHand),
3334// )
3335// .contained()
3336// .with_style(theme.container)
3337// .into_any();
3338// }
3339
3340// enum PanelFocus {}
3341// MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
3342// Stack::new()
3343// .with_child(
3344// Flex::column()
3345// .with_child(
3346// Flex::row().with_child(
3347// ChildView::new(&self.filter_editor, cx)
3348// .contained()
3349// .with_style(theme.user_query_editor.container)
3350// .flex(1.0, true),
3351// ),
3352// )
3353// .with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
3354// .contained()
3355// .with_style(theme.container)
3356// .into_any(),
3357// )
3358// .with_children(
3359// (!self.context_menu_on_selected)
3360// .then(|| ChildView::new(&self.context_menu, cx)),
3361// )
3362// .into_any()
3363// })
3364// .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
3365// .into_any_named("collab panel")
3366// }
3367
3368// fn update_keymap_context(
3369// &self,
3370// keymap: &mut gpui::keymap_matcher::KeymapContext,
3371// _: &AppContext,
3372// ) {
3373// Self::reset_to_default_keymap_context(keymap);
3374// if self.channel_editing_state.is_some() {
3375// keymap.add_identifier("editing");
3376// } else {
3377// keymap.add_identifier("not_editing");
3378// }
3379// }
3380// }
3381
3382// impl Panel for CollabPanel {
3383// fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
3384// settings::get::<CollaborationPanelSettings>(cx).dock
3385// }
3386
3387// fn position_is_valid(&self, position: DockPosition) -> bool {
3388// matches!(position, DockPosition::Left | DockPosition::Right)
3389// }
3390
3391// fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
3392// settings::update_settings_file::<CollaborationPanelSettings>(
3393// self.fs.clone(),
3394// cx,
3395// move |settings| settings.dock = Some(position),
3396// );
3397// }
3398
3399// fn size(&self, cx: &gpui::WindowContext) -> f32 {
3400// self.width
3401// .unwrap_or_else(|| settings::get::<CollaborationPanelSettings>(cx).default_width)
3402// }
3403
3404// fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
3405// self.width = size;
3406// self.serialize(cx);
3407// cx.notify();
3408// }
3409
3410// fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
3411// settings::get::<CollaborationPanelSettings>(cx)
3412// .button
3413// .then(|| "icons/user_group_16.svg")
3414// }
3415
3416// fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
3417// (
3418// "Collaboration Panel".to_string(),
3419// Some(Box::new(ToggleFocus)),
3420// )
3421// }
3422
3423// fn should_change_position_on_event(event: &Self::Event) -> bool {
3424// matches!(event, Event::DockPositionChanged)
3425// }
3426
3427// fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
3428// self.has_focus
3429// }
3430
3431// fn is_focus_event(event: &Self::Event) -> bool {
3432// matches!(event, Event::Focus)
3433// }
3434// }
3435
3436// impl PartialEq for ListEntry {
3437// fn eq(&self, other: &Self) -> bool {
3438// match self {
3439// ListEntry::Header(section_1) => {
3440// if let ListEntry::Header(section_2) = other {
3441// return section_1 == section_2;
3442// }
3443// }
3444// ListEntry::CallParticipant { user: user_1, .. } => {
3445// if let ListEntry::CallParticipant { user: user_2, .. } = other {
3446// return user_1.id == user_2.id;
3447// }
3448// }
3449// ListEntry::ParticipantProject {
3450// project_id: project_id_1,
3451// ..
3452// } => {
3453// if let ListEntry::ParticipantProject {
3454// project_id: project_id_2,
3455// ..
3456// } = other
3457// {
3458// return project_id_1 == project_id_2;
3459// }
3460// }
3461// ListEntry::ParticipantScreen {
3462// peer_id: peer_id_1, ..
3463// } => {
3464// if let ListEntry::ParticipantScreen {
3465// peer_id: peer_id_2, ..
3466// } = other
3467// {
3468// return peer_id_1 == peer_id_2;
3469// }
3470// }
3471// ListEntry::Channel {
3472// channel: channel_1, ..
3473// } => {
3474// if let ListEntry::Channel {
3475// channel: channel_2, ..
3476// } = other
3477// {
3478// return channel_1.id == channel_2.id;
3479// }
3480// }
3481// ListEntry::ChannelNotes { channel_id } => {
3482// if let ListEntry::ChannelNotes {
3483// channel_id: other_id,
3484// } = other
3485// {
3486// return channel_id == other_id;
3487// }
3488// }
3489// ListEntry::ChannelChat { channel_id } => {
3490// if let ListEntry::ChannelChat {
3491// channel_id: other_id,
3492// } = other
3493// {
3494// return channel_id == other_id;
3495// }
3496// }
3497// ListEntry::ChannelInvite(channel_1) => {
3498// if let ListEntry::ChannelInvite(channel_2) = other {
3499// return channel_1.id == channel_2.id;
3500// }
3501// }
3502// ListEntry::IncomingRequest(user_1) => {
3503// if let ListEntry::IncomingRequest(user_2) = other {
3504// return user_1.id == user_2.id;
3505// }
3506// }
3507// ListEntry::OutgoingRequest(user_1) => {
3508// if let ListEntry::OutgoingRequest(user_2) = other {
3509// return user_1.id == user_2.id;
3510// }
3511// }
3512// ListEntry::Contact {
3513// contact: contact_1, ..
3514// } => {
3515// if let ListEntry::Contact {
3516// contact: contact_2, ..
3517// } = other
3518// {
3519// return contact_1.user.id == contact_2.user.id;
3520// }
3521// }
3522// ListEntry::ChannelEditor { depth } => {
3523// if let ListEntry::ChannelEditor { depth: other_depth } = other {
3524// return depth == other_depth;
3525// }
3526// }
3527// ListEntry::ContactPlaceholder => {
3528// if let ListEntry::ContactPlaceholder = other {
3529// return true;
3530// }
3531// }
3532// }
3533// false
3534// }
3535// }
3536
3537// fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
3538// Svg::new(svg_path)
3539// .with_color(style.color)
3540// .constrained()
3541// .with_width(style.icon_width)
3542// .aligned()
3543// .constrained()
3544// .with_width(style.button_width)
3545// .with_height(style.button_width)
3546// .contained()
3547// .with_style(style.container)
3548// }