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