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