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