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