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 cx.stop_active_drag(window) {
1467 return;
1468 } else if self.take_editing_state(window, cx) {
1469 window.focus(&self.filter_editor.focus_handle(cx));
1470 } else if !self.reset_filter_editor_text(window, cx) {
1471 self.focus_handle.focus(window);
1472 }
1473
1474 if self.context_menu.is_some() {
1475 self.context_menu.take();
1476 cx.notify();
1477 }
1478
1479 self.update_entries(false, cx);
1480 }
1481
1482 fn select_next(&mut self, _: &SelectNext, _: &mut Window, cx: &mut Context<Self>) {
1483 let ix = self.selection.map_or(0, |ix| ix + 1);
1484 if ix < self.entries.len() {
1485 self.selection = Some(ix);
1486 }
1487
1488 if let Some(ix) = self.selection {
1489 self.scroll_to_item(ix)
1490 }
1491 cx.notify();
1492 }
1493
1494 fn select_previous(&mut self, _: &SelectPrevious, _: &mut Window, cx: &mut Context<Self>) {
1495 let ix = self.selection.take().unwrap_or(0);
1496 if ix > 0 {
1497 self.selection = Some(ix - 1);
1498 }
1499
1500 if let Some(ix) = self.selection {
1501 self.scroll_to_item(ix)
1502 }
1503 cx.notify();
1504 }
1505
1506 fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
1507 if self.confirm_channel_edit(window, cx) {
1508 return;
1509 }
1510
1511 if let Some(selection) = self.selection {
1512 if let Some(entry) = self.entries.get(selection) {
1513 match entry {
1514 ListEntry::Header(section) => match section {
1515 Section::ActiveCall => Self::leave_call(window, cx),
1516 Section::Channels => self.new_root_channel(window, cx),
1517 Section::Contacts => self.toggle_contact_finder(window, cx),
1518 Section::ContactRequests
1519 | Section::Online
1520 | Section::Offline
1521 | Section::ChannelInvites => {
1522 self.toggle_section_expanded(*section, cx);
1523 }
1524 },
1525 ListEntry::Contact { contact, calling } => {
1526 if contact.online && !contact.busy && !calling {
1527 self.call(contact.user.id, window, cx);
1528 }
1529 }
1530 ListEntry::ParticipantProject {
1531 project_id,
1532 host_user_id,
1533 ..
1534 } => {
1535 if let Some(workspace) = self.workspace.upgrade() {
1536 let app_state = workspace.read(cx).app_state().clone();
1537 workspace::join_in_room_project(
1538 *project_id,
1539 *host_user_id,
1540 app_state,
1541 cx,
1542 )
1543 .detach_and_prompt_err(
1544 "Failed to join project",
1545 window,
1546 cx,
1547 |_, _, _| None,
1548 );
1549 }
1550 }
1551 ListEntry::ParticipantScreen { peer_id, .. } => {
1552 let Some(peer_id) = peer_id else {
1553 return;
1554 };
1555 if let Some(workspace) = self.workspace.upgrade() {
1556 workspace.update(cx, |workspace, cx| {
1557 workspace.open_shared_screen(*peer_id, window, cx)
1558 });
1559 }
1560 }
1561 ListEntry::Channel { channel, .. } => {
1562 let is_active = maybe!({
1563 let call_channel = ActiveCall::global(cx)
1564 .read(cx)
1565 .room()?
1566 .read(cx)
1567 .channel_id()?;
1568
1569 Some(call_channel == channel.id)
1570 })
1571 .unwrap_or(false);
1572 if is_active {
1573 self.open_channel_notes(channel.id, window, cx)
1574 } else {
1575 self.join_channel(channel.id, window, cx)
1576 }
1577 }
1578 ListEntry::ContactPlaceholder => self.toggle_contact_finder(window, cx),
1579 ListEntry::CallParticipant { user, peer_id, .. } => {
1580 if Some(user) == self.user_store.read(cx).current_user().as_ref() {
1581 Self::leave_call(window, cx);
1582 } else if let Some(peer_id) = peer_id {
1583 self.workspace
1584 .update(cx, |workspace, cx| workspace.follow(*peer_id, window, cx))
1585 .ok();
1586 }
1587 }
1588 ListEntry::IncomingRequest(user) => {
1589 self.respond_to_contact_request(user.id, true, window, cx)
1590 }
1591 ListEntry::ChannelInvite(channel) => {
1592 self.respond_to_channel_invite(channel.id, true, cx)
1593 }
1594 ListEntry::ChannelNotes { channel_id } => {
1595 self.open_channel_notes(*channel_id, window, cx)
1596 }
1597 ListEntry::ChannelChat { channel_id } => {
1598 self.join_channel_chat(*channel_id, window, cx)
1599 }
1600 ListEntry::OutgoingRequest(_) => {}
1601 ListEntry::ChannelEditor { .. } => {}
1602 }
1603 }
1604 }
1605 }
1606
1607 fn insert_space(&mut self, _: &InsertSpace, window: &mut Window, cx: &mut Context<Self>) {
1608 if self.channel_editing_state.is_some() {
1609 self.channel_name_editor.update(cx, |editor, cx| {
1610 editor.insert(" ", window, cx);
1611 });
1612 }
1613 }
1614
1615 fn confirm_channel_edit(&mut self, window: &mut Window, cx: &mut Context<CollabPanel>) -> bool {
1616 if let Some(editing_state) = &mut self.channel_editing_state {
1617 match editing_state {
1618 ChannelEditingState::Create {
1619 location,
1620 pending_name,
1621 ..
1622 } => {
1623 if pending_name.is_some() {
1624 return false;
1625 }
1626 let channel_name = self.channel_name_editor.read(cx).text(cx);
1627
1628 *pending_name = Some(channel_name.clone());
1629
1630 let create = self.channel_store.update(cx, |channel_store, cx| {
1631 channel_store.create_channel(&channel_name, *location, cx)
1632 });
1633 if location.is_none() {
1634 cx.spawn_in(window, async move |this, cx| {
1635 let channel_id = create.await?;
1636 this.update_in(cx, |this, window, cx| {
1637 this.show_channel_modal(
1638 channel_id,
1639 channel_modal::Mode::InviteMembers,
1640 window,
1641 cx,
1642 )
1643 })
1644 })
1645 .detach_and_prompt_err(
1646 "Failed to create channel",
1647 window,
1648 cx,
1649 |_, _, _| None,
1650 );
1651 } else {
1652 create.detach_and_prompt_err(
1653 "Failed to create channel",
1654 window,
1655 cx,
1656 |_, _, _| None,
1657 );
1658 }
1659 cx.notify();
1660 }
1661 ChannelEditingState::Rename {
1662 location,
1663 pending_name,
1664 } => {
1665 if pending_name.is_some() {
1666 return false;
1667 }
1668 let channel_name = self.channel_name_editor.read(cx).text(cx);
1669 *pending_name = Some(channel_name.clone());
1670
1671 self.channel_store
1672 .update(cx, |channel_store, cx| {
1673 channel_store.rename(*location, &channel_name, cx)
1674 })
1675 .detach();
1676 cx.notify();
1677 }
1678 }
1679 cx.focus_self(window);
1680 true
1681 } else {
1682 false
1683 }
1684 }
1685
1686 fn toggle_section_expanded(&mut self, section: Section, cx: &mut Context<Self>) {
1687 if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
1688 self.collapsed_sections.remove(ix);
1689 } else {
1690 self.collapsed_sections.push(section);
1691 }
1692 self.update_entries(false, cx);
1693 }
1694
1695 fn collapse_selected_channel(
1696 &mut self,
1697 _: &CollapseSelectedChannel,
1698 window: &mut Window,
1699 cx: &mut Context<Self>,
1700 ) {
1701 let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
1702 return;
1703 };
1704
1705 if self.is_channel_collapsed(channel_id) {
1706 return;
1707 }
1708
1709 self.toggle_channel_collapsed(channel_id, window, cx);
1710 }
1711
1712 fn expand_selected_channel(
1713 &mut self,
1714 _: &ExpandSelectedChannel,
1715 window: &mut Window,
1716 cx: &mut Context<Self>,
1717 ) {
1718 let Some(id) = self.selected_channel().map(|channel| channel.id) else {
1719 return;
1720 };
1721
1722 if !self.is_channel_collapsed(id) {
1723 return;
1724 }
1725
1726 self.toggle_channel_collapsed(id, window, cx)
1727 }
1728
1729 fn toggle_channel_collapsed(
1730 &mut self,
1731 channel_id: ChannelId,
1732 window: &mut Window,
1733 cx: &mut Context<Self>,
1734 ) {
1735 match self.collapsed_channels.binary_search(&channel_id) {
1736 Ok(ix) => {
1737 self.collapsed_channels.remove(ix);
1738 }
1739 Err(ix) => {
1740 self.collapsed_channels.insert(ix, channel_id);
1741 }
1742 };
1743 self.serialize(cx);
1744 self.update_entries(true, cx);
1745 cx.notify();
1746 cx.focus_self(window);
1747 }
1748
1749 fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
1750 self.collapsed_channels.binary_search(&channel_id).is_ok()
1751 }
1752
1753 fn leave_call(window: &mut Window, cx: &mut App) {
1754 ActiveCall::global(cx)
1755 .update(cx, |call, cx| call.hang_up(cx))
1756 .detach_and_prompt_err("Failed to hang up", window, cx, |_, _, _| None);
1757 }
1758
1759 fn toggle_contact_finder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1760 if let Some(workspace) = self.workspace.upgrade() {
1761 workspace.update(cx, |workspace, cx| {
1762 workspace.toggle_modal(window, cx, |window, cx| {
1763 let mut finder = ContactFinder::new(self.user_store.clone(), window, cx);
1764 finder.set_query(self.filter_editor.read(cx).text(cx), window, cx);
1765 finder
1766 });
1767 });
1768 }
1769 }
1770
1771 fn new_root_channel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1772 self.channel_editing_state = Some(ChannelEditingState::Create {
1773 location: None,
1774 pending_name: None,
1775 });
1776 self.update_entries(false, cx);
1777 self.select_channel_editor();
1778 window.focus(&self.channel_name_editor.focus_handle(cx));
1779 cx.notify();
1780 }
1781
1782 fn select_channel_editor(&mut self) {
1783 self.selection = self.entries.iter().position(|entry| match entry {
1784 ListEntry::ChannelEditor { .. } => true,
1785 _ => false,
1786 });
1787 }
1788
1789 fn new_subchannel(
1790 &mut self,
1791 channel_id: ChannelId,
1792 window: &mut Window,
1793 cx: &mut Context<Self>,
1794 ) {
1795 self.collapsed_channels
1796 .retain(|channel| *channel != channel_id);
1797 self.channel_editing_state = Some(ChannelEditingState::Create {
1798 location: Some(channel_id),
1799 pending_name: None,
1800 });
1801 self.update_entries(false, cx);
1802 self.select_channel_editor();
1803 window.focus(&self.channel_name_editor.focus_handle(cx));
1804 cx.notify();
1805 }
1806
1807 fn manage_members(
1808 &mut self,
1809 channel_id: ChannelId,
1810 window: &mut Window,
1811 cx: &mut Context<Self>,
1812 ) {
1813 self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, window, cx);
1814 }
1815
1816 fn remove_selected_channel(&mut self, _: &Remove, window: &mut Window, cx: &mut Context<Self>) {
1817 if let Some(channel) = self.selected_channel() {
1818 self.remove_channel(channel.id, window, cx)
1819 }
1820 }
1821
1822 fn rename_selected_channel(
1823 &mut self,
1824 _: &SecondaryConfirm,
1825 window: &mut Window,
1826 cx: &mut Context<Self>,
1827 ) {
1828 if let Some(channel) = self.selected_channel() {
1829 self.rename_channel(channel.id, window, cx);
1830 }
1831 }
1832
1833 fn rename_channel(
1834 &mut self,
1835 channel_id: ChannelId,
1836 window: &mut Window,
1837 cx: &mut Context<Self>,
1838 ) {
1839 let channel_store = self.channel_store.read(cx);
1840 if !channel_store.is_channel_admin(channel_id) {
1841 return;
1842 }
1843 if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() {
1844 self.channel_editing_state = Some(ChannelEditingState::Rename {
1845 location: channel_id,
1846 pending_name: None,
1847 });
1848 self.channel_name_editor.update(cx, |editor, cx| {
1849 editor.set_text(channel.name.clone(), window, cx);
1850 editor.select_all(&Default::default(), window, cx);
1851 });
1852 window.focus(&self.channel_name_editor.focus_handle(cx));
1853 self.update_entries(false, cx);
1854 self.select_channel_editor();
1855 }
1856 }
1857
1858 fn set_channel_visibility(
1859 &mut self,
1860 channel_id: ChannelId,
1861 visibility: ChannelVisibility,
1862 window: &mut Window,
1863 cx: &mut Context<Self>,
1864 ) {
1865 self.channel_store
1866 .update(cx, |channel_store, cx| {
1867 channel_store.set_channel_visibility(channel_id, visibility, cx)
1868 })
1869 .detach_and_prompt_err("Failed to set channel visibility", window, cx, |e, _, _| match e.error_code() {
1870 ErrorCode::BadPublicNesting =>
1871 if e.error_tag("direction") == Some("parent") {
1872 Some("To make a channel public, its parent channel must be public.".to_string())
1873 } else {
1874 Some("To make a channel private, all of its subchannels must be private.".to_string())
1875 },
1876 _ => None
1877 });
1878 }
1879
1880 fn start_move_channel(
1881 &mut self,
1882 channel_id: ChannelId,
1883 _window: &mut Window,
1884 _cx: &mut Context<Self>,
1885 ) {
1886 self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
1887 }
1888
1889 fn start_move_selected_channel(
1890 &mut self,
1891 _: &StartMoveChannel,
1892 window: &mut Window,
1893 cx: &mut Context<Self>,
1894 ) {
1895 if let Some(channel) = self.selected_channel() {
1896 self.start_move_channel(channel.id, window, cx);
1897 }
1898 }
1899
1900 fn move_channel_on_clipboard(
1901 &mut self,
1902 to_channel_id: ChannelId,
1903 window: &mut Window,
1904 cx: &mut Context<CollabPanel>,
1905 ) {
1906 if let Some(clipboard) = self.channel_clipboard.take() {
1907 self.move_channel(clipboard.channel_id, to_channel_id, window, cx)
1908 }
1909 }
1910
1911 fn move_channel(
1912 &self,
1913 channel_id: ChannelId,
1914 to: ChannelId,
1915 window: &mut Window,
1916 cx: &mut Context<Self>,
1917 ) {
1918 self.channel_store
1919 .update(cx, |channel_store, cx| {
1920 channel_store.move_channel(channel_id, to, cx)
1921 })
1922 .detach_and_prompt_err("Failed to move channel", window, cx, |e, _, _| {
1923 match e.error_code() {
1924 ErrorCode::BadPublicNesting => {
1925 Some("Public channels must have public parents".into())
1926 }
1927 ErrorCode::CircularNesting => {
1928 Some("You cannot move a channel into itself".into())
1929 }
1930 ErrorCode::WrongMoveTarget => {
1931 Some("You cannot move a channel into a different root channel".into())
1932 }
1933 _ => None,
1934 }
1935 })
1936 }
1937
1938 fn open_channel_notes(
1939 &mut self,
1940 channel_id: ChannelId,
1941 window: &mut Window,
1942 cx: &mut Context<Self>,
1943 ) {
1944 if let Some(workspace) = self.workspace.upgrade() {
1945 ChannelView::open(channel_id, None, workspace, window, cx).detach();
1946 }
1947 }
1948
1949 fn show_inline_context_menu(
1950 &mut self,
1951 _: &menu::SecondaryConfirm,
1952 window: &mut Window,
1953 cx: &mut Context<Self>,
1954 ) {
1955 let Some(bounds) = self
1956 .selection
1957 .and_then(|ix| self.list_state.bounds_for_item(ix))
1958 else {
1959 return;
1960 };
1961
1962 if let Some(channel) = self.selected_channel() {
1963 self.deploy_channel_context_menu(
1964 bounds.center(),
1965 channel.id,
1966 self.selection.unwrap(),
1967 window,
1968 cx,
1969 );
1970 cx.stop_propagation();
1971 return;
1972 };
1973
1974 if let Some(contact) = self.selected_contact() {
1975 self.deploy_contact_context_menu(bounds.center(), contact, window, cx);
1976 cx.stop_propagation();
1977 }
1978 }
1979
1980 fn selected_channel(&self) -> Option<&Arc<Channel>> {
1981 self.selection
1982 .and_then(|ix| self.entries.get(ix))
1983 .and_then(|entry| match entry {
1984 ListEntry::Channel { channel, .. } => Some(channel),
1985 _ => None,
1986 })
1987 }
1988
1989 fn selected_contact(&self) -> Option<Arc<Contact>> {
1990 self.selection
1991 .and_then(|ix| self.entries.get(ix))
1992 .and_then(|entry| match entry {
1993 ListEntry::Contact { contact, .. } => Some(contact.clone()),
1994 _ => None,
1995 })
1996 }
1997
1998 fn show_channel_modal(
1999 &mut self,
2000 channel_id: ChannelId,
2001 mode: channel_modal::Mode,
2002 window: &mut Window,
2003 cx: &mut Context<Self>,
2004 ) {
2005 let workspace = self.workspace.clone();
2006 let user_store = self.user_store.clone();
2007 let channel_store = self.channel_store.clone();
2008
2009 cx.spawn_in(window, async move |_, cx| {
2010 workspace.update_in(cx, |workspace, window, cx| {
2011 workspace.toggle_modal(window, cx, |window, cx| {
2012 ChannelModal::new(
2013 user_store.clone(),
2014 channel_store.clone(),
2015 channel_id,
2016 mode,
2017 window,
2018 cx,
2019 )
2020 });
2021 })
2022 })
2023 .detach();
2024 }
2025
2026 fn leave_channel(&self, channel_id: ChannelId, window: &mut Window, cx: &mut Context<Self>) {
2027 let Some(user_id) = self.user_store.read(cx).current_user().map(|u| u.id) else {
2028 return;
2029 };
2030 let Some(channel) = self.channel_store.read(cx).channel_for_id(channel_id) else {
2031 return;
2032 };
2033 let prompt_message = format!("Are you sure you want to leave \"#{}\"?", channel.name);
2034 let answer = window.prompt(
2035 PromptLevel::Warning,
2036 &prompt_message,
2037 None,
2038 &["Leave", "Cancel"],
2039 cx,
2040 );
2041 cx.spawn_in(window, async move |this, cx| {
2042 if answer.await? != 0 {
2043 return Ok(());
2044 }
2045 this.update(cx, |this, cx| {
2046 this.channel_store.update(cx, |channel_store, cx| {
2047 channel_store.remove_member(channel_id, user_id, cx)
2048 })
2049 })?
2050 .await
2051 })
2052 .detach_and_prompt_err("Failed to leave channel", window, cx, |_, _, _| None)
2053 }
2054
2055 fn remove_channel(
2056 &mut self,
2057 channel_id: ChannelId,
2058 window: &mut Window,
2059 cx: &mut Context<Self>,
2060 ) {
2061 let channel_store = self.channel_store.clone();
2062 if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
2063 let prompt_message = format!(
2064 "Are you sure you want to remove the channel \"{}\"?",
2065 channel.name
2066 );
2067 let answer = window.prompt(
2068 PromptLevel::Warning,
2069 &prompt_message,
2070 None,
2071 &["Remove", "Cancel"],
2072 cx,
2073 );
2074 cx.spawn_in(window, async move |this, cx| {
2075 if answer.await? == 0 {
2076 channel_store
2077 .update(cx, |channels, _| channels.remove_channel(channel_id))?
2078 .await
2079 .notify_async_err(cx);
2080 this.update_in(cx, |_, window, cx| cx.focus_self(window))
2081 .ok();
2082 }
2083 anyhow::Ok(())
2084 })
2085 .detach();
2086 }
2087 }
2088
2089 fn remove_contact(
2090 &mut self,
2091 user_id: u64,
2092 github_login: &str,
2093 window: &mut Window,
2094 cx: &mut Context<Self>,
2095 ) {
2096 let user_store = self.user_store.clone();
2097 let prompt_message = format!(
2098 "Are you sure you want to remove \"{}\" from your contacts?",
2099 github_login
2100 );
2101 let answer = window.prompt(
2102 PromptLevel::Warning,
2103 &prompt_message,
2104 None,
2105 &["Remove", "Cancel"],
2106 cx,
2107 );
2108 cx.spawn_in(window, async move |_, cx| {
2109 if answer.await? == 0 {
2110 user_store
2111 .update(cx, |store, cx| store.remove_contact(user_id, cx))?
2112 .await
2113 .notify_async_err(cx);
2114 }
2115 anyhow::Ok(())
2116 })
2117 .detach_and_prompt_err("Failed to remove contact", window, cx, |_, _, _| None);
2118 }
2119
2120 fn respond_to_contact_request(
2121 &mut self,
2122 user_id: u64,
2123 accept: bool,
2124 window: &mut Window,
2125 cx: &mut Context<Self>,
2126 ) {
2127 self.user_store
2128 .update(cx, |store, cx| {
2129 store.respond_to_contact_request(user_id, accept, cx)
2130 })
2131 .detach_and_prompt_err(
2132 "Failed to respond to contact request",
2133 window,
2134 cx,
2135 |_, _, _| None,
2136 );
2137 }
2138
2139 fn respond_to_channel_invite(
2140 &mut self,
2141 channel_id: ChannelId,
2142 accept: bool,
2143 cx: &mut Context<Self>,
2144 ) {
2145 self.channel_store
2146 .update(cx, |store, cx| {
2147 store.respond_to_channel_invite(channel_id, accept, cx)
2148 })
2149 .detach();
2150 }
2151
2152 fn call(&mut self, recipient_user_id: u64, window: &mut Window, cx: &mut Context<Self>) {
2153 ActiveCall::global(cx)
2154 .update(cx, |call, cx| {
2155 call.invite(recipient_user_id, Some(self.project.clone()), cx)
2156 })
2157 .detach_and_prompt_err("Call failed", window, cx, |_, _, _| None);
2158 }
2159
2160 fn join_channel(&self, channel_id: ChannelId, window: &mut Window, cx: &mut Context<Self>) {
2161 let Some(workspace) = self.workspace.upgrade() else {
2162 return;
2163 };
2164 let Some(handle) = window.window_handle().downcast::<Workspace>() else {
2165 return;
2166 };
2167 workspace::join_channel(
2168 channel_id,
2169 workspace.read(cx).app_state().clone(),
2170 Some(handle),
2171 cx,
2172 )
2173 .detach_and_prompt_err("Failed to join channel", window, cx, |_, _, _| None)
2174 }
2175
2176 fn join_channel_chat(
2177 &mut self,
2178 channel_id: ChannelId,
2179 window: &mut Window,
2180 cx: &mut Context<Self>,
2181 ) {
2182 let Some(workspace) = self.workspace.upgrade() else {
2183 return;
2184 };
2185 window.defer(cx, move |window, cx| {
2186 workspace.update(cx, |workspace, cx| {
2187 if let Some(panel) = workspace.focus_panel::<ChatPanel>(window, cx) {
2188 panel.update(cx, |panel, cx| {
2189 panel
2190 .select_channel(channel_id, None, cx)
2191 .detach_and_notify_err(window, cx);
2192 });
2193 }
2194 });
2195 });
2196 }
2197
2198 fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut Context<Self>) {
2199 let channel_store = self.channel_store.read(cx);
2200 let Some(channel) = channel_store.channel_for_id(channel_id) else {
2201 return;
2202 };
2203 let item = ClipboardItem::new_string(channel.link(cx));
2204 cx.write_to_clipboard(item)
2205 }
2206
2207 fn render_signed_out(&mut self, cx: &mut Context<Self>) -> Div {
2208 let collab_blurb = "Work with your team in realtime with collaborative editing, voice, shared notes and more.";
2209
2210 v_flex()
2211 .gap_6()
2212 .p_4()
2213 .child(Label::new(collab_blurb))
2214 .child(
2215 v_flex()
2216 .gap_2()
2217 .child(
2218 Button::new("sign_in", "Sign in")
2219 .icon_color(Color::Muted)
2220 .icon(IconName::Github)
2221 .icon_position(IconPosition::Start)
2222 .style(ButtonStyle::Filled)
2223 .full_width()
2224 .on_click(cx.listener(|this, _, window, cx| {
2225 let client = this.client.clone();
2226 cx.spawn_in(window, async move |_, cx| {
2227 client
2228 .authenticate_and_connect(true, &cx)
2229 .await
2230 .into_response()
2231 .notify_async_err(cx);
2232 })
2233 .detach()
2234 })),
2235 )
2236 .child(
2237 div().flex().w_full().items_center().child(
2238 Label::new("Sign in to enable collaboration.")
2239 .color(Color::Muted)
2240 .size(LabelSize::Small),
2241 ),
2242 ),
2243 )
2244 }
2245
2246 fn render_list_entry(
2247 &mut self,
2248 ix: usize,
2249 window: &mut Window,
2250 cx: &mut Context<Self>,
2251 ) -> AnyElement {
2252 let entry = &self.entries[ix];
2253
2254 let is_selected = self.selection == Some(ix);
2255 match entry {
2256 ListEntry::Header(section) => {
2257 let is_collapsed = self.collapsed_sections.contains(section);
2258 self.render_header(*section, is_selected, is_collapsed, cx)
2259 .into_any_element()
2260 }
2261 ListEntry::Contact { contact, calling } => self
2262 .render_contact(contact, *calling, is_selected, cx)
2263 .into_any_element(),
2264 ListEntry::ContactPlaceholder => self
2265 .render_contact_placeholder(is_selected, cx)
2266 .into_any_element(),
2267 ListEntry::IncomingRequest(user) => self
2268 .render_contact_request(user, true, is_selected, cx)
2269 .into_any_element(),
2270 ListEntry::OutgoingRequest(user) => self
2271 .render_contact_request(user, false, is_selected, cx)
2272 .into_any_element(),
2273 ListEntry::Channel {
2274 channel,
2275 depth,
2276 has_children,
2277 } => self
2278 .render_channel(channel, *depth, *has_children, is_selected, ix, cx)
2279 .into_any_element(),
2280 ListEntry::ChannelEditor { depth } => self
2281 .render_channel_editor(*depth, window, cx)
2282 .into_any_element(),
2283 ListEntry::ChannelInvite(channel) => self
2284 .render_channel_invite(channel, is_selected, cx)
2285 .into_any_element(),
2286 ListEntry::CallParticipant {
2287 user,
2288 peer_id,
2289 is_pending,
2290 role,
2291 } => self
2292 .render_call_participant(user, *peer_id, *is_pending, *role, is_selected, cx)
2293 .into_any_element(),
2294 ListEntry::ParticipantProject {
2295 project_id,
2296 worktree_root_names,
2297 host_user_id,
2298 is_last,
2299 } => self
2300 .render_participant_project(
2301 *project_id,
2302 worktree_root_names,
2303 *host_user_id,
2304 *is_last,
2305 is_selected,
2306 window,
2307 cx,
2308 )
2309 .into_any_element(),
2310 ListEntry::ParticipantScreen { peer_id, is_last } => self
2311 .render_participant_screen(*peer_id, *is_last, is_selected, window, cx)
2312 .into_any_element(),
2313 ListEntry::ChannelNotes { channel_id } => self
2314 .render_channel_notes(*channel_id, is_selected, window, cx)
2315 .into_any_element(),
2316 ListEntry::ChannelChat { channel_id } => self
2317 .render_channel_chat(*channel_id, is_selected, window, cx)
2318 .into_any_element(),
2319 }
2320 }
2321
2322 fn render_signed_in(&mut self, _: &mut Window, cx: &mut Context<Self>) -> Div {
2323 self.channel_store.update(cx, |channel_store, _| {
2324 channel_store.initialize();
2325 });
2326 v_flex()
2327 .size_full()
2328 .child(list(self.list_state.clone()).size_full())
2329 .child(
2330 v_flex()
2331 .child(div().mx_2().border_primary(cx).border_t_1())
2332 .child(
2333 v_flex()
2334 .p_2()
2335 .child(self.render_filter_input(&self.filter_editor, cx)),
2336 ),
2337 )
2338 }
2339
2340 fn render_filter_input(
2341 &self,
2342 editor: &Entity<Editor>,
2343 cx: &mut Context<Self>,
2344 ) -> impl IntoElement {
2345 let settings = ThemeSettings::get_global(cx);
2346 let text_style = TextStyle {
2347 color: if editor.read(cx).read_only(cx) {
2348 cx.theme().colors().text_disabled
2349 } else {
2350 cx.theme().colors().text
2351 },
2352 font_family: settings.ui_font.family.clone(),
2353 font_features: settings.ui_font.features.clone(),
2354 font_fallbacks: settings.ui_font.fallbacks.clone(),
2355 font_size: rems(0.875).into(),
2356 font_weight: settings.ui_font.weight,
2357 font_style: FontStyle::Normal,
2358 line_height: relative(1.3),
2359 ..Default::default()
2360 };
2361
2362 EditorElement::new(
2363 editor,
2364 EditorStyle {
2365 local_player: cx.theme().players().local(),
2366 text: text_style,
2367 ..Default::default()
2368 },
2369 )
2370 }
2371
2372 fn render_header(
2373 &self,
2374 section: Section,
2375 is_selected: bool,
2376 is_collapsed: bool,
2377 cx: &mut Context<Self>,
2378 ) -> impl IntoElement {
2379 let mut channel_link = None;
2380 let mut channel_tooltip_text = None;
2381 let mut channel_icon = None;
2382
2383 let text = match section {
2384 Section::ActiveCall => {
2385 let channel_name = maybe!({
2386 let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
2387
2388 let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
2389
2390 channel_link = Some(channel.link(cx));
2391 (channel_icon, channel_tooltip_text) = match channel.visibility {
2392 proto::ChannelVisibility::Public => {
2393 (Some("icons/public.svg"), Some("Copy public channel link."))
2394 }
2395 proto::ChannelVisibility::Members => {
2396 (Some("icons/hash.svg"), Some("Copy private channel link."))
2397 }
2398 };
2399
2400 Some(channel.name.as_ref())
2401 });
2402
2403 if let Some(name) = channel_name {
2404 SharedString::from(name.to_string())
2405 } else {
2406 SharedString::from("Current Call")
2407 }
2408 }
2409 Section::ContactRequests => SharedString::from("Requests"),
2410 Section::Contacts => SharedString::from("Contacts"),
2411 Section::Channels => SharedString::from("Channels"),
2412 Section::ChannelInvites => SharedString::from("Invites"),
2413 Section::Online => SharedString::from("Online"),
2414 Section::Offline => SharedString::from("Offline"),
2415 };
2416
2417 let button = match section {
2418 Section::ActiveCall => channel_link.map(|channel_link| {
2419 let channel_link_copy = channel_link.clone();
2420 IconButton::new("channel-link", IconName::Copy)
2421 .icon_size(IconSize::Small)
2422 .size(ButtonSize::None)
2423 .visible_on_hover("section-header")
2424 .on_click(move |_, _, cx| {
2425 let item = ClipboardItem::new_string(channel_link_copy.clone());
2426 cx.write_to_clipboard(item)
2427 })
2428 .tooltip(Tooltip::text("Copy channel link"))
2429 .into_any_element()
2430 }),
2431 Section::Contacts => Some(
2432 IconButton::new("add-contact", IconName::Plus)
2433 .on_click(
2434 cx.listener(|this, _, window, cx| this.toggle_contact_finder(window, cx)),
2435 )
2436 .tooltip(Tooltip::text("Search for new contact"))
2437 .into_any_element(),
2438 ),
2439 Section::Channels => Some(
2440 IconButton::new("add-channel", IconName::Plus)
2441 .on_click(cx.listener(|this, _, window, cx| this.new_root_channel(window, cx)))
2442 .tooltip(Tooltip::text("Create a channel"))
2443 .into_any_element(),
2444 ),
2445 _ => None,
2446 };
2447
2448 let can_collapse = match section {
2449 Section::ActiveCall | Section::Channels | Section::Contacts => false,
2450 Section::ChannelInvites
2451 | Section::ContactRequests
2452 | Section::Online
2453 | Section::Offline => true,
2454 };
2455
2456 h_flex().w_full().group("section-header").child(
2457 ListHeader::new(text)
2458 .when(can_collapse, |header| {
2459 header.toggle(Some(!is_collapsed)).on_toggle(cx.listener(
2460 move |this, _, _, cx| {
2461 this.toggle_section_expanded(section, cx);
2462 },
2463 ))
2464 })
2465 .inset(true)
2466 .end_slot::<AnyElement>(button)
2467 .toggle_state(is_selected),
2468 )
2469 }
2470
2471 fn render_contact(
2472 &self,
2473 contact: &Arc<Contact>,
2474 calling: bool,
2475 is_selected: bool,
2476 cx: &mut Context<Self>,
2477 ) -> impl IntoElement {
2478 let online = contact.online;
2479 let busy = contact.busy || calling;
2480 let github_login = SharedString::from(contact.user.github_login.clone());
2481 let item = ListItem::new(github_login.clone())
2482 .indent_level(1)
2483 .indent_step_size(px(20.))
2484 .toggle_state(is_selected)
2485 .child(
2486 h_flex()
2487 .w_full()
2488 .justify_between()
2489 .child(Label::new(github_login.clone()))
2490 .when(calling, |el| {
2491 el.child(Label::new("Calling").color(Color::Muted))
2492 })
2493 .when(!calling, |el| {
2494 el.child(
2495 IconButton::new("contact context menu", IconName::Ellipsis)
2496 .icon_color(Color::Muted)
2497 .visible_on_hover("")
2498 .on_click(cx.listener({
2499 let contact = contact.clone();
2500 move |this, event: &ClickEvent, window, cx| {
2501 this.deploy_contact_context_menu(
2502 event.down.position,
2503 contact.clone(),
2504 window,
2505 cx,
2506 );
2507 }
2508 })),
2509 )
2510 }),
2511 )
2512 .on_secondary_mouse_down(cx.listener({
2513 let contact = contact.clone();
2514 move |this, event: &MouseDownEvent, window, cx| {
2515 this.deploy_contact_context_menu(event.position, contact.clone(), window, cx);
2516 }
2517 }))
2518 .start_slot(
2519 // todo handle contacts with no avatar
2520 Avatar::new(contact.user.avatar_uri.clone())
2521 .indicator::<AvatarAvailabilityIndicator>(if online {
2522 Some(AvatarAvailabilityIndicator::new(match busy {
2523 true => ui::CollaboratorAvailability::Busy,
2524 false => ui::CollaboratorAvailability::Free,
2525 }))
2526 } else {
2527 None
2528 }),
2529 );
2530
2531 div()
2532 .id(github_login.clone())
2533 .group("")
2534 .child(item)
2535 .tooltip(move |_, cx| {
2536 let text = if !online {
2537 format!(" {} is offline", &github_login)
2538 } else if busy {
2539 format!(" {} is on a call", &github_login)
2540 } else {
2541 let room = ActiveCall::global(cx).read(cx).room();
2542 if room.is_some() {
2543 format!("Invite {} to join call", &github_login)
2544 } else {
2545 format!("Call {}", &github_login)
2546 }
2547 };
2548 Tooltip::simple(text, cx)
2549 })
2550 }
2551
2552 fn render_contact_request(
2553 &self,
2554 user: &Arc<User>,
2555 is_incoming: bool,
2556 is_selected: bool,
2557 cx: &mut Context<Self>,
2558 ) -> impl IntoElement {
2559 let github_login = SharedString::from(user.github_login.clone());
2560 let user_id = user.id;
2561 let is_response_pending = self.user_store.read(cx).is_contact_request_pending(user);
2562 let color = if is_response_pending {
2563 Color::Muted
2564 } else {
2565 Color::Default
2566 };
2567
2568 let controls = if is_incoming {
2569 vec![
2570 IconButton::new("decline-contact", IconName::Close)
2571 .on_click(cx.listener(move |this, _, window, cx| {
2572 this.respond_to_contact_request(user_id, false, window, cx);
2573 }))
2574 .icon_color(color)
2575 .tooltip(Tooltip::text("Decline invite")),
2576 IconButton::new("accept-contact", IconName::Check)
2577 .on_click(cx.listener(move |this, _, window, cx| {
2578 this.respond_to_contact_request(user_id, true, window, cx);
2579 }))
2580 .icon_color(color)
2581 .tooltip(Tooltip::text("Accept invite")),
2582 ]
2583 } else {
2584 let github_login = github_login.clone();
2585 vec![
2586 IconButton::new("remove_contact", IconName::Close)
2587 .on_click(cx.listener(move |this, _, window, cx| {
2588 this.remove_contact(user_id, &github_login, window, cx);
2589 }))
2590 .icon_color(color)
2591 .tooltip(Tooltip::text("Cancel invite")),
2592 ]
2593 };
2594
2595 ListItem::new(github_login.clone())
2596 .indent_level(1)
2597 .indent_step_size(px(20.))
2598 .toggle_state(is_selected)
2599 .child(
2600 h_flex()
2601 .w_full()
2602 .justify_between()
2603 .child(Label::new(github_login.clone()))
2604 .child(h_flex().children(controls)),
2605 )
2606 .start_slot(Avatar::new(user.avatar_uri.clone()))
2607 }
2608
2609 fn render_channel_invite(
2610 &self,
2611 channel: &Arc<Channel>,
2612 is_selected: bool,
2613 cx: &mut Context<Self>,
2614 ) -> ListItem {
2615 let channel_id = channel.id;
2616 let response_is_pending = self
2617 .channel_store
2618 .read(cx)
2619 .has_pending_channel_invite_response(channel);
2620 let color = if response_is_pending {
2621 Color::Muted
2622 } else {
2623 Color::Default
2624 };
2625
2626 let controls = [
2627 IconButton::new("reject-invite", IconName::Close)
2628 .on_click(cx.listener(move |this, _, _, cx| {
2629 this.respond_to_channel_invite(channel_id, false, cx);
2630 }))
2631 .icon_color(color)
2632 .tooltip(Tooltip::text("Decline invite")),
2633 IconButton::new("accept-invite", IconName::Check)
2634 .on_click(cx.listener(move |this, _, _, cx| {
2635 this.respond_to_channel_invite(channel_id, true, cx);
2636 }))
2637 .icon_color(color)
2638 .tooltip(Tooltip::text("Accept invite")),
2639 ];
2640
2641 ListItem::new(("channel-invite", channel.id.0 as usize))
2642 .toggle_state(is_selected)
2643 .child(
2644 h_flex()
2645 .w_full()
2646 .justify_between()
2647 .child(Label::new(channel.name.clone()))
2648 .child(h_flex().children(controls)),
2649 )
2650 .start_slot(
2651 Icon::new(IconName::Hash)
2652 .size(IconSize::Small)
2653 .color(Color::Muted),
2654 )
2655 }
2656
2657 fn render_contact_placeholder(&self, is_selected: bool, cx: &mut Context<Self>) -> ListItem {
2658 ListItem::new("contact-placeholder")
2659 .child(Icon::new(IconName::Plus))
2660 .child(Label::new("Add a Contact"))
2661 .toggle_state(is_selected)
2662 .on_click(cx.listener(|this, _, window, cx| this.toggle_contact_finder(window, cx)))
2663 }
2664
2665 fn render_channel(
2666 &self,
2667 channel: &Channel,
2668 depth: usize,
2669 has_children: bool,
2670 is_selected: bool,
2671 ix: usize,
2672 cx: &mut Context<Self>,
2673 ) -> impl IntoElement {
2674 let channel_id = channel.id;
2675
2676 let is_active = maybe!({
2677 let call_channel = ActiveCall::global(cx)
2678 .read(cx)
2679 .room()?
2680 .read(cx)
2681 .channel_id()?;
2682 Some(call_channel == channel_id)
2683 })
2684 .unwrap_or(false);
2685 let channel_store = self.channel_store.read(cx);
2686 let is_public = channel_store
2687 .channel_for_id(channel_id)
2688 .map(|channel| channel.visibility)
2689 == Some(proto::ChannelVisibility::Public);
2690 let disclosed =
2691 has_children.then(|| self.collapsed_channels.binary_search(&channel.id).is_err());
2692
2693 let has_messages_notification = channel_store.has_new_messages(channel_id);
2694 let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);
2695
2696 const FACEPILE_LIMIT: usize = 3;
2697 let participants = self.channel_store.read(cx).channel_participants(channel_id);
2698
2699 let face_pile = if participants.is_empty() {
2700 None
2701 } else {
2702 let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2703 let result = Facepile::new(
2704 participants
2705 .iter()
2706 .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
2707 .take(FACEPILE_LIMIT)
2708 .chain(if extra_count > 0 {
2709 Some(
2710 Label::new(format!("+{extra_count}"))
2711 .ml_2()
2712 .into_any_element(),
2713 )
2714 } else {
2715 None
2716 })
2717 .collect::<SmallVec<_>>(),
2718 );
2719
2720 Some(result)
2721 };
2722
2723 let width = self.width.unwrap_or(px(240.));
2724 let root_id = channel.root_id();
2725
2726 div()
2727 .h_6()
2728 .id(channel_id.0 as usize)
2729 .group("")
2730 .flex()
2731 .w_full()
2732 .when(!channel.is_root_channel(), |el| {
2733 el.on_drag(channel.clone(), move |channel, _, _, cx| {
2734 cx.new(|_| DraggedChannelView {
2735 channel: channel.clone(),
2736 width,
2737 })
2738 })
2739 })
2740 .drag_over::<Channel>({
2741 move |style, dragged_channel: &Channel, _window, cx| {
2742 if dragged_channel.root_id() == root_id {
2743 style.bg(cx.theme().colors().ghost_element_hover)
2744 } else {
2745 style
2746 }
2747 }
2748 })
2749 .on_drop(
2750 cx.listener(move |this, dragged_channel: &Channel, window, cx| {
2751 if dragged_channel.root_id() != root_id {
2752 return;
2753 }
2754 this.move_channel(dragged_channel.id, channel_id, window, cx);
2755 }),
2756 )
2757 .child(
2758 ListItem::new(channel_id.0 as usize)
2759 // Add one level of depth for the disclosure arrow.
2760 .indent_level(depth + 1)
2761 .indent_step_size(px(20.))
2762 .toggle_state(is_selected || is_active)
2763 .toggle(disclosed)
2764 .on_toggle(cx.listener(move |this, _, window, cx| {
2765 this.toggle_channel_collapsed(channel_id, window, cx)
2766 }))
2767 .on_click(cx.listener(move |this, _, window, cx| {
2768 if is_active {
2769 this.open_channel_notes(channel_id, window, cx)
2770 } else {
2771 this.join_channel(channel_id, window, cx)
2772 }
2773 }))
2774 .on_secondary_mouse_down(cx.listener(
2775 move |this, event: &MouseDownEvent, window, cx| {
2776 this.deploy_channel_context_menu(
2777 event.position,
2778 channel_id,
2779 ix,
2780 window,
2781 cx,
2782 )
2783 },
2784 ))
2785 .start_slot(
2786 div()
2787 .relative()
2788 .child(
2789 Icon::new(if is_public {
2790 IconName::Public
2791 } else {
2792 IconName::Hash
2793 })
2794 .size(IconSize::Small)
2795 .color(Color::Muted),
2796 )
2797 .children(has_notes_notification.then(|| {
2798 div()
2799 .w_1p5()
2800 .absolute()
2801 .right(px(-1.))
2802 .top(px(-1.))
2803 .child(Indicator::dot().color(Color::Info))
2804 })),
2805 )
2806 .child(
2807 h_flex()
2808 .id(channel_id.0 as usize)
2809 .child(Label::new(channel.name.clone()))
2810 .children(face_pile.map(|face_pile| face_pile.p_1())),
2811 ),
2812 )
2813 .child(
2814 h_flex().absolute().right(rems(0.)).h_full().child(
2815 h_flex()
2816 .h_full()
2817 .gap_1()
2818 .px_1()
2819 .child(
2820 IconButton::new("channel_chat", IconName::MessageBubbles)
2821 .style(ButtonStyle::Filled)
2822 .shape(ui::IconButtonShape::Square)
2823 .icon_size(IconSize::Small)
2824 .icon_color(if has_messages_notification {
2825 Color::Default
2826 } else {
2827 Color::Muted
2828 })
2829 .on_click(cx.listener(move |this, _, window, cx| {
2830 this.join_channel_chat(channel_id, window, cx)
2831 }))
2832 .tooltip(Tooltip::text("Open channel chat"))
2833 .visible_on_hover(""),
2834 )
2835 .child(
2836 IconButton::new("channel_notes", IconName::File)
2837 .style(ButtonStyle::Filled)
2838 .shape(ui::IconButtonShape::Square)
2839 .icon_size(IconSize::Small)
2840 .icon_color(if has_notes_notification {
2841 Color::Default
2842 } else {
2843 Color::Muted
2844 })
2845 .on_click(cx.listener(move |this, _, window, cx| {
2846 this.open_channel_notes(channel_id, window, cx)
2847 }))
2848 .tooltip(Tooltip::text("Open channel notes"))
2849 .visible_on_hover(""),
2850 ),
2851 ),
2852 )
2853 .tooltip({
2854 let channel_store = self.channel_store.clone();
2855 move |_window, cx| {
2856 cx.new(|_| JoinChannelTooltip {
2857 channel_store: channel_store.clone(),
2858 channel_id,
2859 has_notes_notification,
2860 })
2861 .into()
2862 }
2863 })
2864 }
2865
2866 fn render_channel_editor(
2867 &self,
2868 depth: usize,
2869 _window: &mut Window,
2870 _cx: &mut Context<Self>,
2871 ) -> impl IntoElement {
2872 let item = ListItem::new("channel-editor")
2873 .inset(false)
2874 // Add one level of depth for the disclosure arrow.
2875 .indent_level(depth + 1)
2876 .indent_step_size(px(20.))
2877 .start_slot(
2878 Icon::new(IconName::Hash)
2879 .size(IconSize::Small)
2880 .color(Color::Muted),
2881 );
2882
2883 if let Some(pending_name) = self
2884 .channel_editing_state
2885 .as_ref()
2886 .and_then(|state| state.pending_name())
2887 {
2888 item.child(Label::new(pending_name))
2889 } else {
2890 item.child(self.channel_name_editor.clone())
2891 }
2892 }
2893}
2894
2895fn render_tree_branch(
2896 is_last: bool,
2897 overdraw: bool,
2898 window: &mut Window,
2899 cx: &mut App,
2900) -> impl IntoElement {
2901 let rem_size = window.rem_size();
2902 let line_height = window.text_style().line_height_in_pixels(rem_size);
2903 let width = rem_size * 1.5;
2904 let thickness = px(1.);
2905 let color = cx.theme().colors().text;
2906
2907 canvas(
2908 |_, _, _| {},
2909 move |bounds, _, window, _| {
2910 let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
2911 let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
2912 let right = bounds.right();
2913 let top = bounds.top();
2914
2915 window.paint_quad(fill(
2916 Bounds::from_corners(
2917 point(start_x, top),
2918 point(
2919 start_x + thickness,
2920 if is_last {
2921 start_y
2922 } else {
2923 bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
2924 },
2925 ),
2926 ),
2927 color,
2928 ));
2929 window.paint_quad(fill(
2930 Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
2931 color,
2932 ));
2933 },
2934 )
2935 .w(width)
2936 .h(line_height)
2937}
2938
2939impl Render for CollabPanel {
2940 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2941 v_flex()
2942 .key_context("CollabPanel")
2943 .on_action(cx.listener(CollabPanel::cancel))
2944 .on_action(cx.listener(CollabPanel::select_next))
2945 .on_action(cx.listener(CollabPanel::select_previous))
2946 .on_action(cx.listener(CollabPanel::confirm))
2947 .on_action(cx.listener(CollabPanel::insert_space))
2948 .on_action(cx.listener(CollabPanel::remove_selected_channel))
2949 .on_action(cx.listener(CollabPanel::show_inline_context_menu))
2950 .on_action(cx.listener(CollabPanel::rename_selected_channel))
2951 .on_action(cx.listener(CollabPanel::collapse_selected_channel))
2952 .on_action(cx.listener(CollabPanel::expand_selected_channel))
2953 .on_action(cx.listener(CollabPanel::start_move_selected_channel))
2954 .track_focus(&self.focus_handle(cx))
2955 .size_full()
2956 .child(if self.user_store.read(cx).current_user().is_none() {
2957 self.render_signed_out(cx)
2958 } else {
2959 self.render_signed_in(window, cx)
2960 })
2961 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
2962 deferred(
2963 anchored()
2964 .position(*position)
2965 .anchor(gpui::Corner::TopLeft)
2966 .child(menu.clone()),
2967 )
2968 .with_priority(1)
2969 }))
2970 }
2971}
2972
2973impl EventEmitter<PanelEvent> for CollabPanel {}
2974
2975impl Panel for CollabPanel {
2976 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
2977 CollaborationPanelSettings::get_global(cx).dock
2978 }
2979
2980 fn position_is_valid(&self, position: DockPosition) -> bool {
2981 matches!(position, DockPosition::Left | DockPosition::Right)
2982 }
2983
2984 fn set_position(
2985 &mut self,
2986 position: DockPosition,
2987 _window: &mut Window,
2988 cx: &mut Context<Self>,
2989 ) {
2990 settings::update_settings_file::<CollaborationPanelSettings>(
2991 self.fs.clone(),
2992 cx,
2993 move |settings, _| settings.dock = Some(position),
2994 );
2995 }
2996
2997 fn size(&self, _window: &Window, cx: &App) -> Pixels {
2998 self.width
2999 .unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width)
3000 }
3001
3002 fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
3003 self.width = size;
3004 self.serialize(cx);
3005 cx.notify();
3006 }
3007
3008 fn icon(&self, _window: &Window, cx: &App) -> Option<ui::IconName> {
3009 CollaborationPanelSettings::get_global(cx)
3010 .button
3011 .then_some(ui::IconName::UserGroup)
3012 }
3013
3014 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
3015 Some("Collab Panel")
3016 }
3017
3018 fn toggle_action(&self) -> Box<dyn gpui::Action> {
3019 Box::new(ToggleFocus)
3020 }
3021
3022 fn persistent_name() -> &'static str {
3023 "CollabPanel"
3024 }
3025
3026 fn activation_priority(&self) -> u32 {
3027 6
3028 }
3029}
3030
3031impl Focusable for CollabPanel {
3032 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
3033 self.filter_editor.focus_handle(cx).clone()
3034 }
3035}
3036
3037impl PartialEq for ListEntry {
3038 fn eq(&self, other: &Self) -> bool {
3039 match self {
3040 ListEntry::Header(section_1) => {
3041 if let ListEntry::Header(section_2) = other {
3042 return section_1 == section_2;
3043 }
3044 }
3045 ListEntry::CallParticipant { user: user_1, .. } => {
3046 if let ListEntry::CallParticipant { user: user_2, .. } = other {
3047 return user_1.id == user_2.id;
3048 }
3049 }
3050 ListEntry::ParticipantProject {
3051 project_id: project_id_1,
3052 ..
3053 } => {
3054 if let ListEntry::ParticipantProject {
3055 project_id: project_id_2,
3056 ..
3057 } = other
3058 {
3059 return project_id_1 == project_id_2;
3060 }
3061 }
3062 ListEntry::ParticipantScreen {
3063 peer_id: peer_id_1, ..
3064 } => {
3065 if let ListEntry::ParticipantScreen {
3066 peer_id: peer_id_2, ..
3067 } = other
3068 {
3069 return peer_id_1 == peer_id_2;
3070 }
3071 }
3072 ListEntry::Channel {
3073 channel: channel_1, ..
3074 } => {
3075 if let ListEntry::Channel {
3076 channel: channel_2, ..
3077 } = other
3078 {
3079 return channel_1.id == channel_2.id;
3080 }
3081 }
3082 ListEntry::ChannelNotes { channel_id } => {
3083 if let ListEntry::ChannelNotes {
3084 channel_id: other_id,
3085 } = other
3086 {
3087 return channel_id == other_id;
3088 }
3089 }
3090 ListEntry::ChannelChat { channel_id } => {
3091 if let ListEntry::ChannelChat {
3092 channel_id: other_id,
3093 } = other
3094 {
3095 return channel_id == other_id;
3096 }
3097 }
3098 ListEntry::ChannelInvite(channel_1) => {
3099 if let ListEntry::ChannelInvite(channel_2) = other {
3100 return channel_1.id == channel_2.id;
3101 }
3102 }
3103 ListEntry::IncomingRequest(user_1) => {
3104 if let ListEntry::IncomingRequest(user_2) = other {
3105 return user_1.id == user_2.id;
3106 }
3107 }
3108 ListEntry::OutgoingRequest(user_1) => {
3109 if let ListEntry::OutgoingRequest(user_2) = other {
3110 return user_1.id == user_2.id;
3111 }
3112 }
3113 ListEntry::Contact {
3114 contact: contact_1, ..
3115 } => {
3116 if let ListEntry::Contact {
3117 contact: contact_2, ..
3118 } = other
3119 {
3120 return contact_1.user.id == contact_2.user.id;
3121 }
3122 }
3123 ListEntry::ChannelEditor { depth } => {
3124 if let ListEntry::ChannelEditor { depth: other_depth } = other {
3125 return depth == other_depth;
3126 }
3127 }
3128 ListEntry::ContactPlaceholder => {
3129 if let ListEntry::ContactPlaceholder = other {
3130 return true;
3131 }
3132 }
3133 }
3134 false
3135 }
3136}
3137
3138struct DraggedChannelView {
3139 channel: Channel,
3140 width: Pixels,
3141}
3142
3143impl Render for DraggedChannelView {
3144 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3145 let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
3146 h_flex()
3147 .font_family(ui_font)
3148 .bg(cx.theme().colors().background)
3149 .w(self.width)
3150 .p_1()
3151 .gap_1()
3152 .child(
3153 Icon::new(
3154 if self.channel.visibility == proto::ChannelVisibility::Public {
3155 IconName::Public
3156 } else {
3157 IconName::Hash
3158 },
3159 )
3160 .size(IconSize::Small)
3161 .color(Color::Muted),
3162 )
3163 .child(Label::new(self.channel.name.clone()))
3164 }
3165}
3166
3167struct JoinChannelTooltip {
3168 channel_store: Entity<ChannelStore>,
3169 channel_id: ChannelId,
3170 #[allow(unused)]
3171 has_notes_notification: bool,
3172}
3173
3174impl Render for JoinChannelTooltip {
3175 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3176 tooltip_container(window, cx, |container, _, cx| {
3177 let participants = self
3178 .channel_store
3179 .read(cx)
3180 .channel_participants(self.channel_id);
3181
3182 container
3183 .child(Label::new("Join channel"))
3184 .children(participants.iter().map(|participant| {
3185 h_flex()
3186 .gap_2()
3187 .child(Avatar::new(participant.avatar_uri.clone()))
3188 .child(Label::new(participant.github_login.clone()))
3189 }))
3190 })
3191 }
3192}