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