3128 lines · 113.9 KB · Rust
RawDownload
mod channel_modal;mod contact_finder; use self::channel_modal::ChannelModal;use crate::{channel_view::ChannelView, chat_panel::ChatPanel, CollaborationPanelSettings};use call::ActiveCall;use channel::{Channel, ChannelEvent, ChannelStore};use client::{ChannelId, Client, Contact, User, UserStore};use contact_finder::ContactFinder;use db::kvp::KEY_VALUE_STORE;use editor::{Editor, EditorElement, EditorStyle};use fuzzy::{match_strings, StringMatchCandidate};use gpui::{    actions, anchored, canvas, deferred, div, fill, list, point, prelude::*, px, AnyElement, App,    AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, Context, DismissEvent, Div, Entity,    EventEmitter, FocusHandle, Focusable, FontStyle, InteractiveElement, IntoElement, ListOffset,    ListState, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, SharedString,    Styled, Subscription, Task, TextStyle, WeakEntity, Window,};use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};use project::{Fs, Project};use rpc::{    proto::{self, ChannelVisibility, PeerId},    ErrorCode, ErrorExt,};use serde_derive::{Deserialize, Serialize};use settings::Settings;use smallvec::SmallVec;use std::{mem, sync::Arc};use theme::{ActiveTheme, ThemeSettings};use ui::{    prelude::*, tooltip_container, Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu,    Facepile, Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem,    Tooltip,};use util::{maybe, ResultExt, TryFutureExt};use workspace::{    dock::{DockPosition, Panel, PanelEvent},    notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},    OpenChannelNotes, Workspace,}; actions!(    collab_panel,    [        ToggleFocus,        Remove,        Secondary,        CollapseSelectedChannel,        ExpandSelectedChannel,        StartMoveChannel,        MoveSelected,        InsertSpace,    ]); #[derive(Debug, Copy, Clone, PartialEq, Eq)]struct ChannelMoveClipboard {    channel_id: ChannelId,} const COLLABORATION_PANEL_KEY: &str = "CollaborationPanel"; pub fn init(cx: &mut App) {    cx.observe_new(|workspace: &mut Workspace, _, _| {        workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {            workspace.toggle_panel_focus::<CollabPanel>(window, cx);        });        workspace.register_action(|_, _: &OpenChannelNotes, window, cx| {            let channel_id = ActiveCall::global(cx)                .read(cx)                .room()                .and_then(|room| room.read(cx).channel_id());             if let Some(channel_id) = channel_id {                let workspace = cx.entity().clone();                window.defer(cx, move |window, cx| {                    ChannelView::open(channel_id, None, workspace, window, cx)                        .detach_and_log_err(cx)                });            }        });    })    .detach();} #[derive(Debug)]pub enum ChannelEditingState {    Create {        location: Option<ChannelId>,        pending_name: Option<String>,    },    Rename {        location: ChannelId,        pending_name: Option<String>,    },} impl ChannelEditingState {    fn pending_name(&self) -> Option<String> {        match self {            ChannelEditingState::Create { pending_name, .. } => pending_name.clone(),            ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(),        }    }} pub struct CollabPanel {    width: Option<Pixels>,    fs: Arc<dyn Fs>,    focus_handle: FocusHandle,    channel_clipboard: Option<ChannelMoveClipboard>,    pending_serialization: Task<Option<()>>,    context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,    list_state: ListState,    filter_editor: Entity<Editor>,    channel_name_editor: Entity<Editor>,    channel_editing_state: Option<ChannelEditingState>,    entries: Vec<ListEntry>,    selection: Option<usize>,    channel_store: Entity<ChannelStore>,    user_store: Entity<UserStore>,    client: Arc<Client>,    project: Entity<Project>,    match_candidates: Vec<StringMatchCandidate>,    subscriptions: Vec<Subscription>,    collapsed_sections: Vec<Section>,    collapsed_channels: Vec<ChannelId>,    workspace: WeakEntity<Workspace>,} #[derive(Serialize, Deserialize)]struct SerializedCollabPanel {    width: Option<Pixels>,    collapsed_channels: Option<Vec<u64>>,} #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]enum Section {    ActiveCall,    Channels,    ChannelInvites,    ContactRequests,    Contacts,    Online,    Offline,} #[derive(Clone, Debug)]enum ListEntry {    Header(Section),    CallParticipant {        user: Arc<User>,        peer_id: Option<PeerId>,        is_pending: bool,        role: proto::ChannelRole,    },    ParticipantProject {        project_id: u64,        worktree_root_names: Vec<String>,        host_user_id: u64,        is_last: bool,    },    ParticipantScreen {        peer_id: Option<PeerId>,        is_last: bool,    },    IncomingRequest(Arc<User>),    OutgoingRequest(Arc<User>),    ChannelInvite(Arc<Channel>),    Channel {        channel: Arc<Channel>,        depth: usize,        has_children: bool,    },    ChannelNotes {        channel_id: ChannelId,    },    ChannelChat {        channel_id: ChannelId,    },    ChannelEditor {        depth: usize,    },    Contact {        contact: Arc<Contact>,        calling: bool,    },    ContactPlaceholder,} impl CollabPanel {    pub fn new(        workspace: &mut Workspace,        window: &mut Window,        cx: &mut Context<Workspace>,    ) -> Entity<Self> {        cx.new(|cx| {            let filter_editor = cx.new(|cx| {                let mut editor = Editor::single_line(window, cx);                editor.set_placeholder_text("Filter...", cx);                editor            });             cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {                if let editor::EditorEvent::BufferEdited = event {                    let query = this.filter_editor.read(cx).text(cx);                    if !query.is_empty() {                        this.selection.take();                    }                    this.update_entries(true, cx);                    if !query.is_empty() {                        this.selection = this                            .entries                            .iter()                            .position(|entry| !matches!(entry, ListEntry::Header(_)));                    }                }            })            .detach();             let channel_name_editor = cx.new(|cx| Editor::single_line(window, cx));             cx.subscribe_in(                &channel_name_editor,                window,                |this: &mut Self, _, event, window, cx| {                    if let editor::EditorEvent::Blurred = event {                        if let Some(state) = &this.channel_editing_state {                            if state.pending_name().is_some() {                                return;                            }                        }                        this.take_editing_state(window, cx);                        this.update_entries(false, cx);                        cx.notify();                    }                },            )            .detach();             let entity = cx.entity().downgrade();            let list_state = ListState::new(                0,                gpui::ListAlignment::Top,                px(1000.),                move |ix, window, cx| {                    if let Some(entity) = entity.upgrade() {                        entity.update(cx, |this, cx| this.render_list_entry(ix, window, cx))                    } else {                        div().into_any()                    }                },            );             let mut this = Self {                width: None,                focus_handle: cx.focus_handle(),                channel_clipboard: None,                fs: workspace.app_state().fs.clone(),                pending_serialization: Task::ready(None),                context_menu: None,                list_state,                channel_name_editor,                filter_editor,                entries: Vec::default(),                channel_editing_state: None,                selection: None,                channel_store: ChannelStore::global(cx),                user_store: workspace.user_store().clone(),                project: workspace.project().clone(),                subscriptions: Vec::default(),                match_candidates: Vec::default(),                collapsed_sections: vec![Section::Offline],                collapsed_channels: Vec::default(),                workspace: workspace.weak_handle(),                client: workspace.app_state().client.clone(),            };             this.update_entries(false, cx);             let active_call = ActiveCall::global(cx);            this.subscriptions                .push(cx.observe(&this.user_store, |this, _, cx| {                    this.update_entries(true, cx)                }));            this.subscriptions                .push(cx.observe(&this.channel_store, move |this, _, cx| {                    this.update_entries(true, cx)                }));            this.subscriptions                .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));            this.subscriptions.push(cx.subscribe_in(                &this.channel_store,                window,                |this, _channel_store, e, window, cx| match e {                    ChannelEvent::ChannelCreated(channel_id)                    | ChannelEvent::ChannelRenamed(channel_id) => {                        if this.take_editing_state(window, cx) {                            this.update_entries(false, cx);                            this.selection = this.entries.iter().position(|entry| {                                if let ListEntry::Channel { channel, .. } = entry {                                    channel.id == *channel_id                                } else {                                    false                                }                            });                        }                    }                },            ));             this        })    }     pub async fn load(        workspace: WeakEntity<Workspace>,        mut cx: AsyncWindowContext,    ) -> anyhow::Result<Entity<Self>> {        let serialized_panel = cx            .background_spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })            .await            .map_err(|_| anyhow::anyhow!("Failed to read collaboration panel from key value store"))            .log_err()            .flatten()            .map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel))            .transpose()            .log_err()            .flatten();         workspace.update_in(&mut cx, |workspace, window, cx| {            let panel = CollabPanel::new(workspace, window, cx);            if let Some(serialized_panel) = serialized_panel {                panel.update(cx, |panel, cx| {                    panel.width = serialized_panel.width.map(|w| w.round());                    panel.collapsed_channels = serialized_panel                        .collapsed_channels                        .unwrap_or_else(Vec::new)                        .iter()                        .map(|cid| ChannelId(*cid))                        .collect();                    cx.notify();                });            }            panel        })    }     fn serialize(&mut self, cx: &mut Context<Self>) {        let width = self.width;        let collapsed_channels = self.collapsed_channels.clone();        self.pending_serialization = cx.background_spawn(            async move {                KEY_VALUE_STORE                    .write_kvp(                        COLLABORATION_PANEL_KEY.into(),                        serde_json::to_string(&SerializedCollabPanel {                            width,                            collapsed_channels: Some(                                collapsed_channels.iter().map(|cid| cid.0).collect(),                            ),                        })?,                    )                    .await?;                anyhow::Ok(())            }            .log_err(),        );    }     fn scroll_to_item(&mut self, ix: usize) {        self.list_state.scroll_to_reveal_item(ix)    }     fn update_entries(&mut self, select_same_item: bool, cx: &mut Context<Self>) {        let channel_store = self.channel_store.read(cx);        let user_store = self.user_store.read(cx);        let query = self.filter_editor.read(cx).text(cx);        let executor = cx.background_executor().clone();         let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());        let old_entries = mem::take(&mut self.entries);        let mut scroll_to_top = false;         if let Some(room) = ActiveCall::global(cx).read(cx).room() {            self.entries.push(ListEntry::Header(Section::ActiveCall));            if !old_entries                .iter()                .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))            {                scroll_to_top = true;            }             if !self.collapsed_sections.contains(&Section::ActiveCall) {                let room = room.read(cx);                 if query.is_empty() {                    if let Some(channel_id) = room.channel_id() {                        self.entries.push(ListEntry::ChannelNotes { channel_id });                        self.entries.push(ListEntry::ChannelChat { channel_id });                    }                }                 // Populate the active user.                if let Some(user) = user_store.current_user() {                    self.match_candidates.clear();                    self.match_candidates                        .push(StringMatchCandidate::new(0, &user.github_login));                    let matches = executor.block(match_strings(                        &self.match_candidates,                        &query,                        true,                        usize::MAX,                        &Default::default(),                        executor.clone(),                    ));                    if !matches.is_empty() {                        let user_id = user.id;                        self.entries.push(ListEntry::CallParticipant {                            user,                            peer_id: None,                            is_pending: false,                            role: room.local_participant().role,                        });                        let mut projects = room.local_participant().projects.iter().peekable();                        while let Some(project) = projects.next() {                            self.entries.push(ListEntry::ParticipantProject {                                project_id: project.id,                                worktree_root_names: project.worktree_root_names.clone(),                                host_user_id: user_id,                                is_last: projects.peek().is_none() && !room.is_screen_sharing(),                            });                        }                        if room.is_screen_sharing() {                            self.entries.push(ListEntry::ParticipantScreen {                                peer_id: None,                                is_last: true,                            });                        }                    }                }                 // Populate remote participants.                self.match_candidates.clear();                self.match_candidates                    .extend(room.remote_participants().values().map(|participant| {                        StringMatchCandidate::new(                            participant.user.id as usize,                            &participant.user.github_login,                        )                    }));                let mut matches = executor.block(match_strings(                    &self.match_candidates,                    &query,                    true,                    usize::MAX,                    &Default::default(),                    executor.clone(),                ));                matches.sort_by(|a, b| {                    let a_is_guest = room.role_for_user(a.candidate_id as u64)                        == Some(proto::ChannelRole::Guest);                    let b_is_guest = room.role_for_user(b.candidate_id as u64)                        == Some(proto::ChannelRole::Guest);                    a_is_guest                        .cmp(&b_is_guest)                        .then_with(|| a.string.cmp(&b.string))                });                for mat in matches {                    let user_id = mat.candidate_id as u64;                    let participant = &room.remote_participants()[&user_id];                    self.entries.push(ListEntry::CallParticipant {                        user: participant.user.clone(),                        peer_id: Some(participant.peer_id),                        is_pending: false,                        role: participant.role,                    });                    let mut projects = participant.projects.iter().peekable();                    while let Some(project) = projects.next() {                        self.entries.push(ListEntry::ParticipantProject {                            project_id: project.id,                            worktree_root_names: project.worktree_root_names.clone(),                            host_user_id: participant.user.id,                            is_last: projects.peek().is_none() && !participant.has_video_tracks(),                        });                    }                    if participant.has_video_tracks() {                        self.entries.push(ListEntry::ParticipantScreen {                            peer_id: Some(participant.peer_id),                            is_last: true,                        });                    }                }                 // Populate pending participants.                self.match_candidates.clear();                self.match_candidates                    .extend(room.pending_participants().iter().enumerate().map(                        |(id, participant)| {

Lines 1–500 of 3128.