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