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