2023-07-26 18:11:48 +00:00
|
|
|
|
mod channel_modal;
|
2023-07-25 03:00:31 +00:00
|
|
|
|
mod contact_finder;
|
2023-07-24 21:39:16 +00:00
|
|
|
|
|
2023-09-08 01:06:05 +00:00
|
|
|
|
use crate::{
|
|
|
|
|
channel_view::{self, ChannelView},
|
2023-09-09 00:06:39 +00:00
|
|
|
|
chat_panel::ChatPanel,
|
2023-09-08 01:06:05 +00:00
|
|
|
|
face_pile::FacePile,
|
2023-09-08 20:28:19 +00:00
|
|
|
|
CollaborationPanelSettings,
|
2023-09-08 01:06:05 +00:00
|
|
|
|
};
|
2023-07-19 01:55:54 +00:00
|
|
|
|
use anyhow::Result;
|
2023-07-25 03:00:31 +00:00
|
|
|
|
use call::ActiveCall;
|
2023-09-09 01:47:59 +00:00
|
|
|
|
use channel::{Channel, ChannelEvent, ChannelId, ChannelStore, ChannelPath};
|
2023-09-08 01:06:05 +00:00
|
|
|
|
use channel_modal::ChannelModal;
|
2023-08-21 23:30:57 +00:00
|
|
|
|
use client::{proto::PeerId, Client, Contact, User, UserStore};
|
2023-09-08 20:28:19 +00:00
|
|
|
|
use contact_finder::ContactFinder;
|
2023-08-01 23:06:21 +00:00
|
|
|
|
use context_menu::{ContextMenu, ContextMenuItem};
|
2023-07-19 01:55:54 +00:00
|
|
|
|
use db::kvp::KEY_VALUE_STORE;
|
2023-07-25 03:00:31 +00:00
|
|
|
|
use editor::{Cancel, Editor};
|
2023-08-26 00:00:53 +00:00
|
|
|
|
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
|
2023-07-25 03:00:31 +00:00
|
|
|
|
use futures::StreamExt;
|
|
|
|
|
use fuzzy::{match_strings, StringMatchCandidate};
|
2023-07-19 01:55:54 +00:00
|
|
|
|
use gpui::{
|
|
|
|
|
actions,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
elements::{
|
2023-08-19 12:18:53 +00:00
|
|
|
|
Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState,
|
2023-08-19 21:40:05 +00:00
|
|
|
|
MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable,
|
|
|
|
|
Stack, Svg,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
},
|
2023-08-24 22:00:54 +00:00
|
|
|
|
fonts::TextStyle,
|
2023-08-02 01:20:25 +00:00
|
|
|
|
geometry::{
|
|
|
|
|
rect::RectF,
|
|
|
|
|
vector::{vec2f, Vector2F},
|
|
|
|
|
},
|
2023-08-01 23:06:21 +00:00
|
|
|
|
impl_actions,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
platform::{CursorStyle, MouseButton, PromptLevel},
|
2023-08-24 22:00:54 +00:00
|
|
|
|
serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
2023-07-19 01:55:54 +00:00
|
|
|
|
};
|
2023-07-25 03:00:31 +00:00
|
|
|
|
use menu::{Confirm, SelectNext, SelectPrev};
|
|
|
|
|
use project::{Fs, Project};
|
2023-07-19 01:55:54 +00:00
|
|
|
|
use serde_derive::{Deserialize, Serialize};
|
|
|
|
|
use settings::SettingsStore;
|
2023-09-09 01:47:59 +00:00
|
|
|
|
use std::{borrow::Cow, mem, sync::Arc, hash::Hash};
|
2023-08-19 12:18:53 +00:00
|
|
|
|
use theme::{components::ComponentExt, IconButton};
|
2023-08-15 17:45:03 +00:00
|
|
|
|
use util::{iife, ResultExt, TryFutureExt};
|
2023-07-19 01:55:54 +00:00
|
|
|
|
use workspace::{
|
|
|
|
|
dock::{DockPosition, Panel},
|
2023-08-01 20:22:06 +00:00
|
|
|
|
item::ItemHandle,
|
2023-07-19 01:55:54 +00:00
|
|
|
|
Workspace,
|
|
|
|
|
};
|
|
|
|
|
|
2023-08-01 23:06:21 +00:00
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
|
|
|
struct RemoveChannel {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
channel_id: ChannelId,
|
2023-08-01 23:06:21 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-19 12:18:53 +00:00
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
2023-08-23 23:25:17 +00:00
|
|
|
|
struct ToggleCollapse {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
location: ChannelLocation<'static>,
|
2023-08-19 12:18:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-02 01:20:25 +00:00
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
|
|
|
struct NewChannel {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
location: ChannelLocation<'static>,
|
2023-08-02 01:20:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-03 17:59:09 +00:00
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
2023-08-08 00:14:09 +00:00
|
|
|
|
struct InviteMembers {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
channel_id: ChannelId,
|
2023-08-08 00:14:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
|
|
|
struct ManageMembers {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
channel_id: ChannelId,
|
2023-08-03 17:59:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-09 19:20:48 +00:00
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
|
|
|
struct RenameChannel {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
location: ChannelLocation<'static>,
|
2023-08-09 19:20:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-22 21:18:32 +00:00
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
2023-09-15 01:38:37 +00:00
|
|
|
|
pub struct OpenChannelNotes {
|
|
|
|
|
pub channel_id: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
|
|
|
pub struct JoinChannelCall {
|
|
|
|
|
pub channel_id: u64,
|
2023-08-22 21:18:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-09-09 01:47:59 +00:00
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
|
|
|
struct OpenChannelBuffer {
|
|
|
|
|
channel_id: ChannelId,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
|
|
|
struct CopyChannel {
|
|
|
|
|
channel_id: ChannelId,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
|
|
|
struct CutChannel {
|
|
|
|
|
channel_id: ChannelId,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
|
|
|
struct PasteChannel {
|
|
|
|
|
channel_id: ChannelId,
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-23 23:25:17 +00:00
|
|
|
|
actions!(
|
|
|
|
|
collab_panel,
|
|
|
|
|
[
|
|
|
|
|
ToggleFocus,
|
|
|
|
|
Remove,
|
|
|
|
|
Secondary,
|
|
|
|
|
CollapseSelectedChannel,
|
|
|
|
|
ExpandSelectedChannel
|
|
|
|
|
]
|
|
|
|
|
);
|
2023-07-19 01:55:54 +00:00
|
|
|
|
|
2023-08-08 00:14:09 +00:00
|
|
|
|
impl_actions!(
|
|
|
|
|
collab_panel,
|
2023-08-09 19:20:48 +00:00
|
|
|
|
[
|
|
|
|
|
RemoveChannel,
|
|
|
|
|
NewChannel,
|
|
|
|
|
InviteMembers,
|
|
|
|
|
ManageMembers,
|
2023-08-19 12:18:53 +00:00
|
|
|
|
RenameChannel,
|
2023-08-22 21:18:32 +00:00
|
|
|
|
ToggleCollapse,
|
2023-09-15 01:38:37 +00:00
|
|
|
|
OpenChannelNotes,
|
|
|
|
|
JoinChannelCall,
|
2023-09-09 01:47:59 +00:00
|
|
|
|
OpenChannelBuffer,
|
|
|
|
|
CopyChannel,
|
|
|
|
|
CutChannel,
|
|
|
|
|
PasteChannel,
|
2023-08-09 19:20:48 +00:00
|
|
|
|
]
|
2023-08-08 00:14:09 +00:00
|
|
|
|
);
|
2023-08-01 23:06:21 +00:00
|
|
|
|
|
2023-08-18 22:34:35 +00:00
|
|
|
|
const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
|
2023-07-19 01:55:54 +00:00
|
|
|
|
|
2023-09-09 01:47:59 +00:00
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
|
|
|
|
pub struct ChannelLocation<'a> {
|
|
|
|
|
channel: ChannelId,
|
|
|
|
|
parent: Cow<'a, ChannelPath>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<(ChannelId, ChannelPath)> for ChannelLocation<'static> {
|
|
|
|
|
fn from(value: (ChannelId, ChannelPath)) -> Self {
|
|
|
|
|
ChannelLocation { channel: value.0, parent: Cow::Owned(value.1) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a> From<(ChannelId, &'a ChannelPath)> for ChannelLocation<'a> {
|
|
|
|
|
fn from(value: (ChannelId, &'a ChannelPath)) -> Self {
|
|
|
|
|
ChannelLocation { channel: value.0, parent: Cow::Borrowed(value.1) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-08 01:06:05 +00:00
|
|
|
|
pub fn init(cx: &mut AppContext) {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
settings::register::<panel_settings::CollaborationPanelSettings>(cx);
|
2023-07-25 03:00:31 +00:00
|
|
|
|
contact_finder::init(cx);
|
2023-07-26 18:11:48 +00:00
|
|
|
|
channel_modal::init(cx);
|
2023-08-22 21:18:32 +00:00
|
|
|
|
channel_view::init(cx);
|
2023-07-25 03:00:31 +00:00
|
|
|
|
|
|
|
|
|
cx.add_action(CollabPanel::cancel);
|
|
|
|
|
cx.add_action(CollabPanel::select_next);
|
|
|
|
|
cx.add_action(CollabPanel::select_prev);
|
|
|
|
|
cx.add_action(CollabPanel::confirm);
|
2023-08-08 19:46:13 +00:00
|
|
|
|
cx.add_action(CollabPanel::remove);
|
2023-08-09 19:20:48 +00:00
|
|
|
|
cx.add_action(CollabPanel::remove_selected_channel);
|
2023-08-08 19:46:13 +00:00
|
|
|
|
cx.add_action(CollabPanel::show_inline_context_menu);
|
2023-08-02 01:20:25 +00:00
|
|
|
|
cx.add_action(CollabPanel::new_subchannel);
|
2023-08-08 00:14:09 +00:00
|
|
|
|
cx.add_action(CollabPanel::invite_members);
|
|
|
|
|
cx.add_action(CollabPanel::manage_members);
|
2023-08-09 19:20:48 +00:00
|
|
|
|
cx.add_action(CollabPanel::rename_selected_channel);
|
|
|
|
|
cx.add_action(CollabPanel::rename_channel);
|
2023-08-19 12:18:53 +00:00
|
|
|
|
cx.add_action(CollabPanel::toggle_channel_collapsed);
|
2023-08-23 23:25:17 +00:00
|
|
|
|
cx.add_action(CollabPanel::collapse_selected_channel);
|
2023-08-22 21:18:32 +00:00
|
|
|
|
cx.add_action(CollabPanel::expand_selected_channel);
|
2023-09-11 20:44:41 +00:00
|
|
|
|
cx.add_action(CollabPanel::open_channel_notes);
|
2023-09-09 01:47:59 +00:00
|
|
|
|
cx.add_action(CollabPanel::open_channel_buffer);
|
|
|
|
|
|
|
|
|
|
cx.add_action(|panel: &mut CollabPanel, action: &CopyChannel, _: &mut ViewContext<CollabPanel>| {
|
|
|
|
|
panel.copy = Some(ChannelCopy::Copy(action.channel_id));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
cx.add_action(|panel: &mut CollabPanel, action: &CutChannel, _: &mut ViewContext<CollabPanel>| {
|
|
|
|
|
// panel.copy = Some(ChannelCopy::Cut(action.channel_id));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
cx.add_action(|panel: &mut CollabPanel, action: &PasteChannel, cx: &mut ViewContext<CollabPanel>| {
|
|
|
|
|
if let Some(copy) = &panel.copy {
|
|
|
|
|
match copy {
|
|
|
|
|
ChannelCopy::Cut {..} => todo!(),
|
|
|
|
|
ChannelCopy::Copy(channel) => panel.channel_store.update(cx, |channel_store, cx| {
|
|
|
|
|
channel_store.move_channel(*channel, None, Some(action.channel_id), cx).detach_and_log_err(cx)
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2023-07-19 01:55:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-09 19:20:48 +00:00
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub enum ChannelEditingState {
|
2023-08-10 00:11:52 +00:00
|
|
|
|
Create {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
location: Option<ChannelLocation<'static>>,
|
2023-08-10 00:11:52 +00:00
|
|
|
|
pending_name: Option<String>,
|
|
|
|
|
},
|
|
|
|
|
Rename {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
location: ChannelLocation<'static>,
|
2023-08-10 00:11:52 +00:00
|
|
|
|
pending_name: Option<String>,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ChannelEditingState {
|
|
|
|
|
fn pending_name(&self) -> Option<&str> {
|
|
|
|
|
match self {
|
|
|
|
|
ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
|
|
|
|
|
ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-01 20:22:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-09-09 01:47:59 +00:00
|
|
|
|
enum ChannelCopy {
|
|
|
|
|
Cut {
|
|
|
|
|
channel_id: u64,
|
|
|
|
|
parent_id: Option<u64>,
|
|
|
|
|
},
|
|
|
|
|
Copy(u64),
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-24 21:39:16 +00:00
|
|
|
|
pub struct CollabPanel {
|
2023-07-19 01:55:54 +00:00
|
|
|
|
width: Option<f32>,
|
|
|
|
|
fs: Arc<dyn Fs>,
|
|
|
|
|
has_focus: bool,
|
2023-09-09 01:47:59 +00:00
|
|
|
|
copy: Option<ChannelCopy>,
|
2023-07-19 01:55:54 +00:00
|
|
|
|
pending_serialization: Task<Option<()>>,
|
|
|
|
|
context_menu: ViewHandle<ContextMenu>,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
filter_editor: ViewHandle<Editor>,
|
2023-08-01 20:22:06 +00:00
|
|
|
|
channel_name_editor: ViewHandle<Editor>,
|
|
|
|
|
channel_editing_state: Option<ChannelEditingState>,
|
2023-08-02 01:20:25 +00:00
|
|
|
|
entries: Vec<ListEntry>,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
selection: Option<usize>,
|
|
|
|
|
user_store: ModelHandle<UserStore>,
|
2023-08-07 23:27:47 +00:00
|
|
|
|
client: Arc<Client>,
|
2023-08-01 01:00:14 +00:00
|
|
|
|
channel_store: ModelHandle<ChannelStore>,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
project: ModelHandle<Project>,
|
|
|
|
|
match_candidates: Vec<StringMatchCandidate>,
|
|
|
|
|
list_state: ListState<Self>,
|
|
|
|
|
subscriptions: Vec<Subscription>,
|
|
|
|
|
collapsed_sections: Vec<Section>,
|
2023-09-09 01:47:59 +00:00
|
|
|
|
collapsed_channels: Vec<ChannelLocation<'static>>,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
workspace: WeakViewHandle<Workspace>,
|
2023-08-08 19:46:13 +00:00
|
|
|
|
context_menu_on_selected: bool,
|
2023-07-19 01:55:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize)]
|
2023-08-26 00:00:53 +00:00
|
|
|
|
struct SerializedCollabPanel {
|
2023-07-19 01:55:54 +00:00
|
|
|
|
width: Option<f32>,
|
2023-09-09 01:47:59 +00:00
|
|
|
|
collapsed_channels: Option<Vec<ChannelLocation<'static>>>,
|
2023-07-19 01:55:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub enum Event {
|
|
|
|
|
DockPositionChanged,
|
|
|
|
|
Focus,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
Dismissed,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
|
|
|
|
|
enum Section {
|
|
|
|
|
ActiveCall,
|
2023-07-26 00:29:09 +00:00
|
|
|
|
Channels,
|
2023-08-04 23:14:01 +00:00
|
|
|
|
ChannelInvites,
|
|
|
|
|
ContactRequests,
|
2023-07-26 00:29:09 +00:00
|
|
|
|
Contacts,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
Online,
|
|
|
|
|
Offline,
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-01 20:22:06 +00:00
|
|
|
|
#[derive(Clone, Debug)]
|
2023-08-02 01:20:25 +00:00
|
|
|
|
enum ListEntry {
|
2023-09-09 00:06:39 +00:00
|
|
|
|
Header(Section),
|
2023-07-25 03:00:31 +00:00
|
|
|
|
CallParticipant {
|
|
|
|
|
user: Arc<User>,
|
|
|
|
|
is_pending: bool,
|
|
|
|
|
},
|
|
|
|
|
ParticipantProject {
|
|
|
|
|
project_id: u64,
|
|
|
|
|
worktree_root_names: Vec<String>,
|
|
|
|
|
host_user_id: u64,
|
|
|
|
|
is_last: bool,
|
|
|
|
|
},
|
|
|
|
|
ParticipantScreen {
|
|
|
|
|
peer_id: PeerId,
|
|
|
|
|
is_last: bool,
|
|
|
|
|
},
|
|
|
|
|
IncomingRequest(Arc<User>),
|
|
|
|
|
OutgoingRequest(Arc<User>),
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ChannelInvite(Arc<Channel>),
|
2023-08-14 23:27:35 +00:00
|
|
|
|
Channel {
|
|
|
|
|
channel: Arc<Channel>,
|
|
|
|
|
depth: usize,
|
2023-09-09 01:47:59 +00:00
|
|
|
|
path: Arc<[ChannelId]>,
|
2023-08-14 23:27:35 +00:00
|
|
|
|
},
|
2023-08-24 22:00:54 +00:00
|
|
|
|
ChannelNotes {
|
|
|
|
|
channel_id: ChannelId,
|
|
|
|
|
},
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ChannelEditor {
|
|
|
|
|
depth: usize,
|
|
|
|
|
},
|
2023-07-25 03:00:31 +00:00
|
|
|
|
Contact {
|
|
|
|
|
contact: Arc<Contact>,
|
|
|
|
|
calling: bool,
|
|
|
|
|
},
|
2023-08-14 17:23:50 +00:00
|
|
|
|
ContactPlaceholder,
|
2023-07-19 01:55:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-24 21:39:16 +00:00
|
|
|
|
impl Entity for CollabPanel {
|
2023-07-19 01:55:54 +00:00
|
|
|
|
type Event = Event;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-24 21:39:16 +00:00
|
|
|
|
impl CollabPanel {
|
2023-07-19 01:55:54 +00:00
|
|
|
|
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
cx.add_view::<Self, _>(|cx| {
|
2023-07-19 01:55:54 +00:00
|
|
|
|
let view_id = cx.view_id();
|
2023-07-24 21:39:16 +00:00
|
|
|
|
|
2023-07-25 03:00:31 +00:00
|
|
|
|
let filter_editor = cx.add_view(|cx| {
|
|
|
|
|
let mut editor = Editor::single_line(
|
|
|
|
|
Some(Arc::new(|theme| {
|
2023-07-25 17:02:01 +00:00
|
|
|
|
theme.collab_panel.user_query_editor.clone()
|
2023-07-25 03:00:31 +00:00
|
|
|
|
})),
|
|
|
|
|
cx,
|
|
|
|
|
);
|
2023-07-26 00:29:09 +00:00
|
|
|
|
editor.set_placeholder_text("Filter channels, contacts", cx);
|
2023-07-25 03:00:31 +00:00
|
|
|
|
editor
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
cx.subscribe(&filter_editor, |this, _, event, cx| {
|
|
|
|
|
if let editor::Event::BufferEdited = event {
|
|
|
|
|
let query = this.filter_editor.read(cx).text(cx);
|
|
|
|
|
if !query.is_empty() {
|
|
|
|
|
this.selection.take();
|
|
|
|
|
}
|
2023-08-10 00:11:52 +00:00
|
|
|
|
this.update_entries(true, cx);
|
2023-07-25 03:00:31 +00:00
|
|
|
|
if !query.is_empty() {
|
|
|
|
|
this.selection = this
|
|
|
|
|
.entries
|
|
|
|
|
.iter()
|
2023-09-09 00:06:39 +00:00
|
|
|
|
.position(|entry| !matches!(entry, ListEntry::Header(_)));
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.detach();
|
|
|
|
|
|
2023-08-01 20:22:06 +00:00
|
|
|
|
let channel_name_editor = cx.add_view(|cx| {
|
|
|
|
|
Editor::single_line(
|
|
|
|
|
Some(Arc::new(|theme| {
|
|
|
|
|
theme.collab_panel.user_query_editor.clone()
|
|
|
|
|
})),
|
|
|
|
|
cx,
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
cx.subscribe(&channel_name_editor, |this, _, event, cx| {
|
|
|
|
|
if let editor::Event::Blurred = event {
|
2023-08-10 00:11:52 +00:00
|
|
|
|
if let Some(state) = &this.channel_editing_state {
|
|
|
|
|
if state.pending_name().is_some() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-01 20:22:06 +00:00
|
|
|
|
this.take_editing_state(cx);
|
2023-08-09 15:54:24 +00:00
|
|
|
|
this.update_entries(false, cx);
|
2023-08-01 20:22:06 +00:00
|
|
|
|
cx.notify();
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.detach();
|
|
|
|
|
|
2023-07-25 03:00:31 +00:00
|
|
|
|
let list_state =
|
|
|
|
|
ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
|
|
|
|
|
let theme = theme::current(cx).clone();
|
|
|
|
|
let is_selected = this.selection == Some(ix);
|
|
|
|
|
let current_project_id = this.project.read(cx).remote_id();
|
|
|
|
|
|
|
|
|
|
match &this.entries[ix] {
|
2023-09-09 00:06:39 +00:00
|
|
|
|
ListEntry::Header(section) => {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
let is_collapsed = this.collapsed_sections.contains(section);
|
2023-09-09 00:06:39 +00:00
|
|
|
|
this.render_header(*section, &theme, is_selected, is_collapsed, cx)
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::CallParticipant { user, is_pending } => {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
Self::render_call_participant(
|
|
|
|
|
user,
|
|
|
|
|
*is_pending,
|
|
|
|
|
is_selected,
|
2023-07-25 17:02:01 +00:00
|
|
|
|
&theme.collab_panel,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
)
|
|
|
|
|
}
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::ParticipantProject {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
project_id,
|
|
|
|
|
worktree_root_names,
|
|
|
|
|
host_user_id,
|
|
|
|
|
is_last,
|
|
|
|
|
} => Self::render_participant_project(
|
|
|
|
|
*project_id,
|
|
|
|
|
worktree_root_names,
|
|
|
|
|
*host_user_id,
|
|
|
|
|
Some(*project_id) == current_project_id,
|
|
|
|
|
*is_last,
|
|
|
|
|
is_selected,
|
2023-07-25 17:02:01 +00:00
|
|
|
|
&theme.collab_panel,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
cx,
|
|
|
|
|
),
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::ParticipantScreen { peer_id, is_last } => {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
Self::render_participant_screen(
|
|
|
|
|
*peer_id,
|
|
|
|
|
*is_last,
|
|
|
|
|
is_selected,
|
2023-07-25 17:02:01 +00:00
|
|
|
|
&theme.collab_panel,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
cx,
|
|
|
|
|
)
|
|
|
|
|
}
|
2023-09-09 01:47:59 +00:00
|
|
|
|
ListEntry::Channel { channel, depth, path } => {
|
2023-08-08 19:46:13 +00:00
|
|
|
|
let channel_row = this.render_channel(
|
|
|
|
|
&*channel,
|
2023-08-14 23:27:35 +00:00
|
|
|
|
*depth,
|
2023-09-09 01:47:59 +00:00
|
|
|
|
path.to_owned(),
|
2023-08-08 19:46:13 +00:00
|
|
|
|
&theme.collab_panel,
|
|
|
|
|
is_selected,
|
|
|
|
|
cx,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if is_selected && this.context_menu_on_selected {
|
|
|
|
|
Stack::new()
|
|
|
|
|
.with_child(channel_row)
|
|
|
|
|
.with_child(
|
|
|
|
|
ChildView::new(&this.context_menu, cx)
|
|
|
|
|
.aligned()
|
|
|
|
|
.bottom()
|
|
|
|
|
.right(),
|
|
|
|
|
)
|
|
|
|
|
.into_any()
|
|
|
|
|
} else {
|
|
|
|
|
return channel_row;
|
|
|
|
|
}
|
2023-08-01 01:00:14 +00:00
|
|
|
|
}
|
2023-08-24 22:00:54 +00:00
|
|
|
|
ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
|
|
|
|
|
*channel_id,
|
|
|
|
|
&theme.collab_panel,
|
|
|
|
|
is_selected,
|
|
|
|
|
cx,
|
|
|
|
|
),
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
|
2023-08-01 01:00:14 +00:00
|
|
|
|
channel.clone(),
|
|
|
|
|
this.channel_store.clone(),
|
|
|
|
|
&theme.collab_panel,
|
|
|
|
|
is_selected,
|
|
|
|
|
cx,
|
|
|
|
|
),
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::IncomingRequest(user) => Self::render_contact_request(
|
2023-07-25 03:00:31 +00:00
|
|
|
|
user.clone(),
|
|
|
|
|
this.user_store.clone(),
|
2023-07-25 17:02:01 +00:00
|
|
|
|
&theme.collab_panel,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
true,
|
|
|
|
|
is_selected,
|
|
|
|
|
cx,
|
|
|
|
|
),
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::OutgoingRequest(user) => Self::render_contact_request(
|
2023-07-25 03:00:31 +00:00
|
|
|
|
user.clone(),
|
|
|
|
|
this.user_store.clone(),
|
2023-07-25 17:02:01 +00:00
|
|
|
|
&theme.collab_panel,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
false,
|
|
|
|
|
is_selected,
|
|
|
|
|
cx,
|
|
|
|
|
),
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::Contact { contact, calling } => Self::render_contact(
|
2023-07-25 03:00:31 +00:00
|
|
|
|
contact,
|
|
|
|
|
*calling,
|
|
|
|
|
&this.project,
|
2023-07-25 17:02:01 +00:00
|
|
|
|
&theme.collab_panel,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
is_selected,
|
|
|
|
|
cx,
|
|
|
|
|
),
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::ChannelEditor { depth } => {
|
2023-08-08 19:46:13 +00:00
|
|
|
|
this.render_channel_editor(&theme, *depth, cx)
|
2023-08-02 01:20:25 +00:00
|
|
|
|
}
|
2023-08-14 17:23:50 +00:00
|
|
|
|
ListEntry::ContactPlaceholder => {
|
2023-08-14 17:34:00 +00:00
|
|
|
|
this.render_contact_placeholder(&theme.collab_panel, is_selected, cx)
|
2023-08-14 17:23:50 +00:00
|
|
|
|
}
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let mut this = Self {
|
2023-07-19 01:55:54 +00:00
|
|
|
|
width: None,
|
|
|
|
|
has_focus: false,
|
2023-09-09 01:47:59 +00:00
|
|
|
|
copy: None,
|
2023-07-19 01:55:54 +00:00
|
|
|
|
fs: workspace.app_state().fs.clone(),
|
|
|
|
|
pending_serialization: Task::ready(None),
|
|
|
|
|
context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
|
2023-08-01 20:22:06 +00:00
|
|
|
|
channel_name_editor,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
filter_editor,
|
|
|
|
|
entries: Vec::default(),
|
2023-08-01 20:22:06 +00:00
|
|
|
|
channel_editing_state: None,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
selection: None,
|
|
|
|
|
user_store: workspace.user_store().clone(),
|
2023-08-01 01:00:14 +00:00
|
|
|
|
channel_store: workspace.app_state().channel_store.clone(),
|
2023-07-25 03:00:31 +00:00
|
|
|
|
project: workspace.project().clone(),
|
|
|
|
|
subscriptions: Vec::default(),
|
|
|
|
|
match_candidates: Vec::default(),
|
2023-08-17 02:51:41 +00:00
|
|
|
|
collapsed_sections: vec![Section::Offline],
|
2023-08-19 12:18:53 +00:00
|
|
|
|
collapsed_channels: Vec::default(),
|
2023-07-25 03:00:31 +00:00
|
|
|
|
workspace: workspace.weak_handle(),
|
2023-08-07 23:27:47 +00:00
|
|
|
|
client: workspace.app_state().client.clone(),
|
2023-08-08 19:46:13 +00:00
|
|
|
|
context_menu_on_selected: true,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
list_state,
|
2023-07-19 01:55:54 +00:00
|
|
|
|
};
|
2023-08-08 00:14:09 +00:00
|
|
|
|
|
2023-08-09 15:54:24 +00:00
|
|
|
|
this.update_entries(false, cx);
|
2023-07-19 01:55:54 +00:00
|
|
|
|
|
|
|
|
|
// Update the dock position when the setting changes.
|
|
|
|
|
let mut old_dock_position = this.position(cx);
|
2023-07-25 03:00:31 +00:00
|
|
|
|
this.subscriptions
|
|
|
|
|
.push(
|
2023-09-08 20:28:19 +00:00
|
|
|
|
cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
let new_dock_position = this.position(cx);
|
|
|
|
|
if new_dock_position != old_dock_position {
|
|
|
|
|
old_dock_position = new_dock_position;
|
|
|
|
|
cx.emit(Event::DockPositionChanged);
|
|
|
|
|
}
|
2023-08-07 23:27:47 +00:00
|
|
|
|
cx.notify();
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let active_call = ActiveCall::global(cx);
|
|
|
|
|
this.subscriptions
|
2023-08-09 15:54:24 +00:00
|
|
|
|
.push(cx.observe(&this.user_store, |this, _, cx| {
|
2023-08-10 00:11:52 +00:00
|
|
|
|
this.update_entries(true, cx)
|
2023-08-09 15:54:24 +00:00
|
|
|
|
}));
|
2023-07-25 03:00:31 +00:00
|
|
|
|
this.subscriptions
|
2023-08-09 15:54:24 +00:00
|
|
|
|
.push(cx.observe(&this.channel_store, |this, _, cx| {
|
2023-08-10 00:11:52 +00:00
|
|
|
|
this.update_entries(true, cx)
|
2023-08-09 15:54:24 +00:00
|
|
|
|
}));
|
2023-08-07 23:45:13 +00:00
|
|
|
|
this.subscriptions
|
2023-08-10 00:11:52 +00:00
|
|
|
|
.push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
|
2023-08-26 00:00:53 +00:00
|
|
|
|
this.subscriptions
|
|
|
|
|
.push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
|
|
|
|
|
this.update_entries(true, cx)
|
|
|
|
|
}));
|
2023-08-10 00:11:52 +00:00
|
|
|
|
this.subscriptions.push(cx.subscribe(
|
|
|
|
|
&this.channel_store,
|
|
|
|
|
|this, _channel_store, e, cx| match e {
|
|
|
|
|
ChannelEvent::ChannelCreated(channel_id)
|
|
|
|
|
| ChannelEvent::ChannelRenamed(channel_id) => {
|
|
|
|
|
if this.take_editing_state(cx) {
|
|
|
|
|
this.update_entries(false, cx);
|
|
|
|
|
this.selection = this.entries.iter().position(|entry| {
|
2023-08-14 23:27:35 +00:00
|
|
|
|
if let ListEntry::Channel { channel, .. } = entry {
|
2023-08-10 00:11:52 +00:00
|
|
|
|
channel.id == *channel_id
|
|
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
));
|
2023-07-19 01:55:54 +00:00
|
|
|
|
|
|
|
|
|
this
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn load(
|
|
|
|
|
workspace: WeakViewHandle<Workspace>,
|
|
|
|
|
cx: AsyncAppContext,
|
|
|
|
|
) -> Task<Result<ViewHandle<Self>>> {
|
|
|
|
|
cx.spawn(|mut cx| async move {
|
|
|
|
|
let serialized_panel = if let Some(panel) = cx
|
|
|
|
|
.background()
|
2023-08-18 22:34:35 +00:00
|
|
|
|
.spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
|
2023-07-19 01:55:54 +00:00
|
|
|
|
.await
|
|
|
|
|
.log_err()
|
|
|
|
|
.flatten()
|
|
|
|
|
{
|
2023-08-26 00:00:53 +00:00
|
|
|
|
Some(serde_json::from_str::<SerializedCollabPanel>(&panel)?)
|
2023-07-19 01:55:54 +00:00
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
workspace.update(&mut cx, |workspace, cx| {
|
2023-07-24 21:39:16 +00:00
|
|
|
|
let panel = CollabPanel::new(workspace, cx);
|
2023-07-19 01:55:54 +00:00
|
|
|
|
if let Some(serialized_panel) = serialized_panel {
|
|
|
|
|
panel.update(cx, |panel, cx| {
|
|
|
|
|
panel.width = serialized_panel.width;
|
2023-08-26 00:00:53 +00:00
|
|
|
|
panel.collapsed_channels = serialized_panel
|
|
|
|
|
.collapsed_channels
|
|
|
|
|
.unwrap_or_else(|| Vec::new());
|
2023-07-19 01:55:54 +00:00
|
|
|
|
cx.notify();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
panel
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
|
|
|
|
|
let width = self.width;
|
2023-08-24 22:00:54 +00:00
|
|
|
|
let collapsed_channels = self.collapsed_channels.clone();
|
2023-07-19 01:55:54 +00:00
|
|
|
|
self.pending_serialization = cx.background().spawn(
|
|
|
|
|
async move {
|
|
|
|
|
KEY_VALUE_STORE
|
|
|
|
|
.write_kvp(
|
2023-08-18 22:34:35 +00:00
|
|
|
|
COLLABORATION_PANEL_KEY.into(),
|
2023-08-26 00:00:53 +00:00
|
|
|
|
serde_json::to_string(&SerializedCollabPanel {
|
2023-08-24 22:00:54 +00:00
|
|
|
|
width,
|
2023-08-26 00:00:53 +00:00
|
|
|
|
collapsed_channels: Some(collapsed_channels),
|
2023-08-24 22:00:54 +00:00
|
|
|
|
})?,
|
2023-07-19 01:55:54 +00:00
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
anyhow::Ok(())
|
|
|
|
|
}
|
|
|
|
|
.log_err(),
|
|
|
|
|
);
|
|
|
|
|
}
|
2023-07-25 03:00:31 +00:00
|
|
|
|
|
2023-08-10 00:11:52 +00:00
|
|
|
|
fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
|
2023-08-01 01:00:14 +00:00
|
|
|
|
let channel_store = self.channel_store.read(cx);
|
2023-07-25 03:00:31 +00:00
|
|
|
|
let user_store = self.user_store.read(cx);
|
|
|
|
|
let query = self.filter_editor.read(cx).text(cx);
|
|
|
|
|
let executor = cx.background().clone();
|
|
|
|
|
|
|
|
|
|
let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
|
|
|
|
|
let old_entries = mem::take(&mut self.entries);
|
|
|
|
|
|
|
|
|
|
if let Some(room) = ActiveCall::global(cx).read(cx).room() {
|
2023-09-09 00:06:39 +00:00
|
|
|
|
self.entries.push(ListEntry::Header(Section::ActiveCall));
|
2023-07-25 03:00:31 +00:00
|
|
|
|
|
2023-08-04 23:14:01 +00:00
|
|
|
|
if !self.collapsed_sections.contains(&Section::ActiveCall) {
|
|
|
|
|
let room = room.read(cx);
|
|
|
|
|
|
2023-08-24 22:00:54 +00:00
|
|
|
|
if let Some(channel_id) = room.channel_id() {
|
|
|
|
|
self.entries.push(ListEntry::ChannelNotes { channel_id })
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-04 23:14:01 +00:00
|
|
|
|
// Populate the active user.
|
|
|
|
|
if let Some(user) = user_store.current_user() {
|
|
|
|
|
self.match_candidates.clear();
|
|
|
|
|
self.match_candidates.push(StringMatchCandidate {
|
|
|
|
|
id: 0,
|
|
|
|
|
string: user.github_login.clone(),
|
|
|
|
|
char_bag: user.github_login.chars().collect(),
|
|
|
|
|
});
|
|
|
|
|
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,
|
|
|
|
|
is_pending: false,
|
|
|
|
|
});
|
|
|
|
|
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(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Populate remote participants.
|
2023-07-25 03:00:31 +00:00
|
|
|
|
self.match_candidates.clear();
|
2023-08-04 23:14:01 +00:00
|
|
|
|
self.match_candidates
|
|
|
|
|
.extend(room.remote_participants().iter().map(|(_, participant)| {
|
|
|
|
|
StringMatchCandidate {
|
|
|
|
|
id: participant.user.id as usize,
|
|
|
|
|
string: participant.user.github_login.clone(),
|
|
|
|
|
char_bag: participant.user.github_login.chars().collect(),
|
|
|
|
|
}
|
|
|
|
|
}));
|
2023-07-25 03:00:31 +00:00
|
|
|
|
let matches = executor.block(match_strings(
|
|
|
|
|
&self.match_candidates,
|
|
|
|
|
&query,
|
|
|
|
|
true,
|
|
|
|
|
usize::MAX,
|
|
|
|
|
&Default::default(),
|
|
|
|
|
executor.clone(),
|
|
|
|
|
));
|
2023-08-04 23:14:01 +00:00
|
|
|
|
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(),
|
2023-07-25 03:00:31 +00:00
|
|
|
|
is_pending: false,
|
|
|
|
|
});
|
2023-08-04 23:14:01 +00:00
|
|
|
|
let mut projects = participant.projects.iter().peekable();
|
2023-07-25 03:00:31 +00:00
|
|
|
|
while let Some(project) = projects.next() {
|
2023-08-04 23:14:01 +00:00
|
|
|
|
self.entries.push(ListEntry::ParticipantProject {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
project_id: project.id,
|
|
|
|
|
worktree_root_names: project.worktree_root_names.clone(),
|
2023-08-04 23:14:01 +00:00
|
|
|
|
host_user_id: participant.user.id,
|
|
|
|
|
is_last: projects.peek().is_none()
|
|
|
|
|
&& participant.video_tracks.is_empty(),
|
2023-07-25 03:00:31 +00:00
|
|
|
|
});
|
|
|
|
|
}
|
2023-08-04 23:14:01 +00:00
|
|
|
|
if !participant.video_tracks.is_empty() {
|
|
|
|
|
self.entries.push(ListEntry::ParticipantScreen {
|
|
|
|
|
peer_id: participant.peer_id,
|
|
|
|
|
is_last: true,
|
|
|
|
|
});
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-04 23:14:01 +00:00
|
|
|
|
// Populate pending participants.
|
|
|
|
|
self.match_candidates.clear();
|
|
|
|
|
self.match_candidates
|
|
|
|
|
.extend(room.pending_participants().iter().enumerate().map(
|
|
|
|
|
|(id, participant)| StringMatchCandidate {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
id,
|
|
|
|
|
string: participant.github_login.clone(),
|
|
|
|
|
char_bag: participant.github_login.chars().collect(),
|
2023-08-04 23:14:01 +00:00
|
|
|
|
},
|
|
|
|
|
));
|
|
|
|
|
let matches = executor.block(match_strings(
|
|
|
|
|
&self.match_candidates,
|
|
|
|
|
&query,
|
|
|
|
|
true,
|
|
|
|
|
usize::MAX,
|
|
|
|
|
&Default::default(),
|
|
|
|
|
executor.clone(),
|
|
|
|
|
));
|
|
|
|
|
self.entries
|
|
|
|
|
.extend(matches.iter().map(|mat| ListEntry::CallParticipant {
|
|
|
|
|
user: room.pending_participants()[mat.candidate_id].clone(),
|
|
|
|
|
is_pending: true,
|
|
|
|
|
}));
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-07 23:45:13 +00:00
|
|
|
|
let mut request_entries = Vec::new();
|
2023-08-26 00:00:53 +00:00
|
|
|
|
|
|
|
|
|
if cx.has_flag::<ChannelsAlpha>() {
|
2023-09-09 00:06:39 +00:00
|
|
|
|
self.entries.push(ListEntry::Header(Section::Channels));
|
2023-07-26 00:29:09 +00:00
|
|
|
|
|
2023-08-14 23:27:35 +00:00
|
|
|
|
if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
|
2023-08-07 23:45:13 +00:00
|
|
|
|
self.match_candidates.clear();
|
|
|
|
|
self.match_candidates
|
2023-08-14 23:27:35 +00:00
|
|
|
|
.extend(
|
|
|
|
|
channel_store
|
|
|
|
|
.channels()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.map(|(ix, (_, channel))| StringMatchCandidate {
|
|
|
|
|
id: ix,
|
|
|
|
|
string: channel.name.clone(),
|
|
|
|
|
char_bag: channel.name.chars().collect(),
|
|
|
|
|
}),
|
|
|
|
|
);
|
2023-08-07 23:45:13 +00:00
|
|
|
|
let matches = executor.block(match_strings(
|
|
|
|
|
&self.match_candidates,
|
|
|
|
|
&query,
|
|
|
|
|
true,
|
|
|
|
|
usize::MAX,
|
|
|
|
|
&Default::default(),
|
|
|
|
|
executor.clone(),
|
|
|
|
|
));
|
2023-08-02 01:20:25 +00:00
|
|
|
|
if let Some(state) = &self.channel_editing_state {
|
2023-08-10 00:11:52 +00:00
|
|
|
|
if matches!(
|
|
|
|
|
state,
|
|
|
|
|
ChannelEditingState::Create {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
location: None,
|
2023-08-10 00:11:52 +00:00
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
) {
|
2023-08-07 23:45:13 +00:00
|
|
|
|
self.entries.push(ListEntry::ChannelEditor { depth: 0 });
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-19 12:18:53 +00:00
|
|
|
|
let mut collapse_depth = None;
|
2023-08-07 23:45:13 +00:00
|
|
|
|
for mat in matches {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
let (depth, channel, path) =
|
2023-08-14 23:27:35 +00:00
|
|
|
|
channel_store.channel_at_index(mat.candidate_id).unwrap();
|
2023-08-09 19:20:48 +00:00
|
|
|
|
|
2023-09-09 01:47:59 +00:00
|
|
|
|
let location: ChannelLocation<'_> = (channel.id, path).into();
|
|
|
|
|
|
|
|
|
|
if collapse_depth.is_none() && self.is_channel_collapsed(&location) {
|
2023-08-19 12:18:53 +00:00
|
|
|
|
collapse_depth = Some(depth);
|
|
|
|
|
} else if let Some(collapsed_depth) = collapse_depth {
|
|
|
|
|
if depth > collapsed_depth {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2023-09-09 01:47:59 +00:00
|
|
|
|
if self.is_channel_collapsed(&location) {
|
2023-08-19 12:18:53 +00:00
|
|
|
|
collapse_depth = Some(depth);
|
|
|
|
|
} else {
|
|
|
|
|
collapse_depth = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-09 19:20:48 +00:00
|
|
|
|
match &self.channel_editing_state {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
Some(ChannelEditingState::Create { location: parent_id, .. })
|
|
|
|
|
if *parent_id == Some(location) =>
|
2023-08-09 19:20:48 +00:00
|
|
|
|
{
|
2023-08-14 23:27:35 +00:00
|
|
|
|
self.entries.push(ListEntry::Channel {
|
|
|
|
|
channel: channel.clone(),
|
|
|
|
|
depth,
|
2023-09-09 01:47:59 +00:00
|
|
|
|
path: path.clone(),
|
2023-08-07 23:45:13 +00:00
|
|
|
|
});
|
2023-08-14 23:27:35 +00:00
|
|
|
|
self.entries
|
|
|
|
|
.push(ListEntry::ChannelEditor { depth: depth + 1 });
|
2023-08-07 23:45:13 +00:00
|
|
|
|
}
|
2023-09-09 01:47:59 +00:00
|
|
|
|
Some(ChannelEditingState::Rename { location, .. })
|
|
|
|
|
if location.channel == channel.id && location.parent == Cow::Borrowed(path) =>
|
2023-08-09 19:20:48 +00:00
|
|
|
|
{
|
2023-08-14 23:27:35 +00:00
|
|
|
|
self.entries.push(ListEntry::ChannelEditor { depth });
|
2023-08-09 19:20:48 +00:00
|
|
|
|
}
|
|
|
|
|
_ => {
|
2023-08-14 23:27:35 +00:00
|
|
|
|
self.entries.push(ListEntry::Channel {
|
|
|
|
|
channel: channel.clone(),
|
|
|
|
|
depth,
|
2023-09-09 01:47:59 +00:00
|
|
|
|
path: path.clone()
|
2023-08-14 23:27:35 +00:00
|
|
|
|
});
|
2023-08-09 19:20:48 +00:00
|
|
|
|
}
|
2023-08-02 01:20:25 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-01 01:00:14 +00:00
|
|
|
|
|
2023-08-07 23:45:13 +00:00
|
|
|
|
let channel_invites = channel_store.channel_invitations();
|
|
|
|
|
if !channel_invites.is_empty() {
|
|
|
|
|
self.match_candidates.clear();
|
|
|
|
|
self.match_candidates
|
|
|
|
|
.extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
|
|
|
|
|
StringMatchCandidate {
|
|
|
|
|
id: ix,
|
|
|
|
|
string: channel.name.clone(),
|
|
|
|
|
char_bag: channel.name.chars().collect(),
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
let matches = executor.block(match_strings(
|
|
|
|
|
&self.match_candidates,
|
|
|
|
|
&query,
|
|
|
|
|
true,
|
|
|
|
|
usize::MAX,
|
|
|
|
|
&Default::default(),
|
|
|
|
|
executor.clone(),
|
|
|
|
|
));
|
|
|
|
|
request_entries.extend(matches.iter().map(|mat| {
|
|
|
|
|
ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
|
2023-08-01 01:00:14 +00:00
|
|
|
|
}));
|
2023-08-04 23:14:01 +00:00
|
|
|
|
|
2023-08-07 23:45:13 +00:00
|
|
|
|
if !request_entries.is_empty() {
|
|
|
|
|
self.entries
|
2023-09-09 00:06:39 +00:00
|
|
|
|
.push(ListEntry::Header(Section::ChannelInvites));
|
2023-08-07 23:45:13 +00:00
|
|
|
|
if !self.collapsed_sections.contains(&Section::ChannelInvites) {
|
|
|
|
|
self.entries.append(&mut request_entries);
|
|
|
|
|
}
|
2023-08-04 23:14:01 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-01 01:00:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-09-09 00:06:39 +00:00
|
|
|
|
self.entries.push(ListEntry::Header(Section::Contacts));
|
2023-08-04 23:14:01 +00:00
|
|
|
|
|
|
|
|
|
request_entries.clear();
|
2023-07-26 01:00:49 +00:00
|
|
|
|
let incoming = user_store.incoming_contact_requests();
|
|
|
|
|
if !incoming.is_empty() {
|
|
|
|
|
self.match_candidates.clear();
|
|
|
|
|
self.match_candidates
|
|
|
|
|
.extend(
|
|
|
|
|
incoming
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.map(|(ix, user)| StringMatchCandidate {
|
|
|
|
|
id: ix,
|
|
|
|
|
string: user.github_login.clone(),
|
|
|
|
|
char_bag: user.github_login.chars().collect(),
|
|
|
|
|
}),
|
2023-07-25 03:00:31 +00:00
|
|
|
|
);
|
2023-07-26 01:00:49 +00:00
|
|
|
|
let matches = executor.block(match_strings(
|
|
|
|
|
&self.match_candidates,
|
|
|
|
|
&query,
|
|
|
|
|
true,
|
|
|
|
|
usize::MAX,
|
|
|
|
|
&Default::default(),
|
|
|
|
|
executor.clone(),
|
|
|
|
|
));
|
|
|
|
|
request_entries.extend(
|
|
|
|
|
matches
|
|
|
|
|
.iter()
|
2023-08-02 01:20:25 +00:00
|
|
|
|
.map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
|
2023-07-26 01:00:49 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
2023-07-25 03:00:31 +00:00
|
|
|
|
|
2023-07-26 01:00:49 +00:00
|
|
|
|
let outgoing = user_store.outgoing_contact_requests();
|
|
|
|
|
if !outgoing.is_empty() {
|
|
|
|
|
self.match_candidates.clear();
|
|
|
|
|
self.match_candidates
|
|
|
|
|
.extend(
|
|
|
|
|
outgoing
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.map(|(ix, user)| StringMatchCandidate {
|
|
|
|
|
id: ix,
|
|
|
|
|
string: user.github_login.clone(),
|
|
|
|
|
char_bag: user.github_login.chars().collect(),
|
|
|
|
|
}),
|
2023-07-25 03:00:31 +00:00
|
|
|
|
);
|
2023-07-26 01:00:49 +00:00
|
|
|
|
let matches = executor.block(match_strings(
|
|
|
|
|
&self.match_candidates,
|
|
|
|
|
&query,
|
|
|
|
|
true,
|
|
|
|
|
usize::MAX,
|
|
|
|
|
&Default::default(),
|
|
|
|
|
executor.clone(),
|
|
|
|
|
));
|
|
|
|
|
request_entries.extend(
|
|
|
|
|
matches
|
|
|
|
|
.iter()
|
2023-08-02 01:20:25 +00:00
|
|
|
|
.map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
|
2023-07-26 01:00:49 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
2023-07-25 03:00:31 +00:00
|
|
|
|
|
2023-07-26 01:00:49 +00:00
|
|
|
|
if !request_entries.is_empty() {
|
2023-08-04 23:14:01 +00:00
|
|
|
|
self.entries
|
2023-09-09 00:06:39 +00:00
|
|
|
|
.push(ListEntry::Header(Section::ContactRequests));
|
2023-08-04 23:14:01 +00:00
|
|
|
|
if !self.collapsed_sections.contains(&Section::ContactRequests) {
|
2023-07-26 01:00:49 +00:00
|
|
|
|
self.entries.append(&mut request_entries);
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
2023-07-26 01:00:49 +00:00
|
|
|
|
}
|
2023-07-25 03:00:31 +00:00
|
|
|
|
|
2023-07-26 01:00:49 +00:00
|
|
|
|
let contacts = user_store.contacts();
|
|
|
|
|
if !contacts.is_empty() {
|
|
|
|
|
self.match_candidates.clear();
|
|
|
|
|
self.match_candidates
|
|
|
|
|
.extend(
|
|
|
|
|
contacts
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.map(|(ix, contact)| StringMatchCandidate {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
id: ix,
|
|
|
|
|
string: contact.user.github_login.clone(),
|
|
|
|
|
char_bag: contact.user.github_login.chars().collect(),
|
2023-07-26 01:00:49 +00:00
|
|
|
|
}),
|
|
|
|
|
);
|
2023-07-25 03:00:31 +00:00
|
|
|
|
|
2023-07-26 01:00:49 +00:00
|
|
|
|
let matches = executor.block(match_strings(
|
|
|
|
|
&self.match_candidates,
|
|
|
|
|
&query,
|
|
|
|
|
true,
|
|
|
|
|
usize::MAX,
|
|
|
|
|
&Default::default(),
|
|
|
|
|
executor.clone(),
|
|
|
|
|
));
|
2023-07-25 03:00:31 +00:00
|
|
|
|
|
2023-08-07 23:45:13 +00:00
|
|
|
|
let (online_contacts, offline_contacts) = matches
|
2023-07-26 01:00:49 +00:00
|
|
|
|
.iter()
|
|
|
|
|
.partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
|
2023-07-25 03:00:31 +00:00
|
|
|
|
|
2023-07-26 01:00:49 +00:00
|
|
|
|
for (matches, section) in [
|
|
|
|
|
(online_contacts, Section::Online),
|
|
|
|
|
(offline_contacts, Section::Offline),
|
|
|
|
|
] {
|
|
|
|
|
if !matches.is_empty() {
|
2023-09-09 00:06:39 +00:00
|
|
|
|
self.entries.push(ListEntry::Header(section));
|
2023-07-26 01:00:49 +00:00
|
|
|
|
if !self.collapsed_sections.contains(§ion) {
|
|
|
|
|
let active_call = &ActiveCall::global(cx).read(cx);
|
|
|
|
|
for mat in matches {
|
|
|
|
|
let contact = &contacts[mat.candidate_id];
|
2023-08-02 01:20:25 +00:00
|
|
|
|
self.entries.push(ListEntry::Contact {
|
2023-07-26 01:00:49 +00:00
|
|
|
|
contact: contact.clone(),
|
|
|
|
|
calling: active_call.pending_invites().contains(&contact.user.id),
|
|
|
|
|
});
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-14 17:23:50 +00:00
|
|
|
|
if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
|
|
|
|
|
self.entries.push(ListEntry::ContactPlaceholder);
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-10 00:11:52 +00:00
|
|
|
|
if select_same_item {
|
2023-08-09 15:54:24 +00:00
|
|
|
|
if let Some(prev_selected_entry) = prev_selected_entry {
|
|
|
|
|
self.selection.take();
|
|
|
|
|
for (ix, entry) in self.entries.iter().enumerate() {
|
|
|
|
|
if *entry == prev_selected_entry {
|
|
|
|
|
self.selection = Some(ix);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-10 00:11:52 +00:00
|
|
|
|
} else {
|
|
|
|
|
self.selection = self.selection.and_then(|prev_selection| {
|
|
|
|
|
if self.entries.is_empty() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(prev_selection.min(self.entries.len() - 1))
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let old_scroll_top = self.list_state.logical_scroll_top();
|
|
|
|
|
self.list_state.reset(self.entries.len());
|
|
|
|
|
|
|
|
|
|
// Attempt to maintain the same scroll position.
|
|
|
|
|
if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
|
|
|
|
|
let new_scroll_top = self
|
|
|
|
|
.entries
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|entry| entry == old_top_entry)
|
|
|
|
|
.map(|item_ix| ListOffset {
|
|
|
|
|
item_ix,
|
|
|
|
|
offset_in_item: old_scroll_top.offset_in_item,
|
|
|
|
|
})
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
|
|
|
|
|
let item_ix = self
|
|
|
|
|
.entries
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|entry| entry == entry_after_old_top)?;
|
|
|
|
|
Some(ListOffset {
|
|
|
|
|
item_ix,
|
|
|
|
|
offset_in_item: 0.,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
let entry_before_old_top =
|
|
|
|
|
old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
|
|
|
|
|
let item_ix = self
|
|
|
|
|
.entries
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|entry| entry == entry_before_old_top)?;
|
|
|
|
|
Some(ListOffset {
|
|
|
|
|
item_ix,
|
|
|
|
|
offset_in_item: 0.,
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
self.list_state
|
|
|
|
|
.scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cx.notify();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn render_call_participant(
|
|
|
|
|
user: &User,
|
|
|
|
|
is_pending: bool,
|
|
|
|
|
is_selected: bool,
|
2023-07-25 17:02:01 +00:00
|
|
|
|
theme: &theme::CollabPanel,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
) -> AnyElement<Self> {
|
|
|
|
|
Flex::row()
|
|
|
|
|
.with_children(user.avatar.clone().map(|avatar| {
|
|
|
|
|
Image::from_data(avatar)
|
|
|
|
|
.with_style(theme.contact_avatar)
|
|
|
|
|
.aligned()
|
|
|
|
|
.left()
|
|
|
|
|
}))
|
|
|
|
|
.with_child(
|
|
|
|
|
Label::new(
|
|
|
|
|
user.github_login.clone(),
|
|
|
|
|
theme.contact_username.text.clone(),
|
|
|
|
|
)
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(theme.contact_username.container)
|
|
|
|
|
.aligned()
|
|
|
|
|
.left()
|
|
|
|
|
.flex(1., true),
|
|
|
|
|
)
|
|
|
|
|
.with_children(if is_pending {
|
|
|
|
|
Some(
|
|
|
|
|
Label::new("Calling", theme.calling_indicator.text.clone())
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(theme.calling_indicator.container)
|
|
|
|
|
.aligned(),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
})
|
|
|
|
|
.constrained()
|
|
|
|
|
.with_height(theme.row_height)
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(
|
|
|
|
|
*theme
|
|
|
|
|
.contact_row
|
|
|
|
|
.in_state(is_selected)
|
|
|
|
|
.style_for(&mut Default::default()),
|
|
|
|
|
)
|
|
|
|
|
.into_any()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn render_participant_project(
|
|
|
|
|
project_id: u64,
|
|
|
|
|
worktree_root_names: &[String],
|
|
|
|
|
host_user_id: u64,
|
|
|
|
|
is_current: bool,
|
|
|
|
|
is_last: bool,
|
|
|
|
|
is_selected: bool,
|
2023-07-25 17:02:01 +00:00
|
|
|
|
theme: &theme::CollabPanel,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
cx: &mut ViewContext<Self>,
|
|
|
|
|
) -> AnyElement<Self> {
|
|
|
|
|
enum JoinProject {}
|
|
|
|
|
|
2023-08-24 22:00:54 +00:00
|
|
|
|
let host_avatar_width = theme
|
2023-07-25 03:00:31 +00:00
|
|
|
|
.contact_avatar
|
|
|
|
|
.width
|
|
|
|
|
.or(theme.contact_avatar.height)
|
|
|
|
|
.unwrap_or(0.);
|
|
|
|
|
let tree_branch = theme.tree_branch;
|
|
|
|
|
let project_name = if worktree_root_names.is_empty() {
|
|
|
|
|
"untitled".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
worktree_root_names.join(", ")
|
|
|
|
|
};
|
|
|
|
|
|
2023-08-24 22:00:54 +00:00
|
|
|
|
MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
|
|
|
|
|
let row = theme
|
|
|
|
|
.project_row
|
|
|
|
|
.in_state(is_selected)
|
|
|
|
|
.style_for(mouse_state);
|
|
|
|
|
|
|
|
|
|
Flex::row()
|
2023-08-24 22:00:54 +00:00
|
|
|
|
.with_child(render_tree_branch(
|
|
|
|
|
tree_branch,
|
|
|
|
|
&row.name.text,
|
|
|
|
|
is_last,
|
|
|
|
|
vec2f(host_avatar_width, theme.row_height),
|
|
|
|
|
cx.font_cache(),
|
|
|
|
|
))
|
2023-07-25 03:00:31 +00:00
|
|
|
|
.with_child(
|
2023-08-24 22:00:54 +00:00
|
|
|
|
Svg::new("icons/file_icons/folder.svg")
|
|
|
|
|
.with_color(theme.channel_hash.color)
|
2023-07-25 03:00:31 +00:00
|
|
|
|
.constrained()
|
2023-08-24 22:00:54 +00:00
|
|
|
|
.with_width(theme.channel_hash.width)
|
|
|
|
|
.aligned()
|
|
|
|
|
.left(),
|
2023-07-25 03:00:31 +00:00
|
|
|
|
)
|
|
|
|
|
.with_child(
|
|
|
|
|
Label::new(project_name, row.name.text.clone())
|
|
|
|
|
.aligned()
|
|
|
|
|
.left()
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(row.name.container)
|
|
|
|
|
.flex(1., false),
|
|
|
|
|
)
|
|
|
|
|
.constrained()
|
|
|
|
|
.with_height(theme.row_height)
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(row.container)
|
|
|
|
|
})
|
|
|
|
|
.with_cursor_style(if !is_current {
|
|
|
|
|
CursorStyle::PointingHand
|
|
|
|
|
} else {
|
|
|
|
|
CursorStyle::Arrow
|
|
|
|
|
})
|
|
|
|
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
|
|
|
|
if !is_current {
|
|
|
|
|
if let Some(workspace) = this.workspace.upgrade(cx) {
|
|
|
|
|
let app_state = workspace.read(cx).app_state().clone();
|
|
|
|
|
workspace::join_remote_project(project_id, host_user_id, app_state, cx)
|
|
|
|
|
.detach_and_log_err(cx);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.into_any()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn render_participant_screen(
|
|
|
|
|
peer_id: PeerId,
|
|
|
|
|
is_last: bool,
|
|
|
|
|
is_selected: bool,
|
2023-07-25 17:02:01 +00:00
|
|
|
|
theme: &theme::CollabPanel,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
cx: &mut ViewContext<Self>,
|
|
|
|
|
) -> AnyElement<Self> {
|
|
|
|
|
enum OpenSharedScreen {}
|
|
|
|
|
|
2023-08-30 21:23:12 +00:00
|
|
|
|
let host_avatar_width = theme
|
2023-07-25 03:00:31 +00:00
|
|
|
|
.contact_avatar
|
|
|
|
|
.width
|
|
|
|
|
.or(theme.contact_avatar.height)
|
|
|
|
|
.unwrap_or(0.);
|
|
|
|
|
let tree_branch = theme.tree_branch;
|
|
|
|
|
|
2023-08-15 10:25:45 +00:00
|
|
|
|
MouseEventHandler::new::<OpenSharedScreen, _>(
|
2023-07-25 03:00:31 +00:00
|
|
|
|
peer_id.as_u64() as usize,
|
|
|
|
|
cx,
|
2023-08-30 21:23:12 +00:00
|
|
|
|
|mouse_state, cx| {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
|
|
|
|
|
let row = theme
|
|
|
|
|
.project_row
|
|
|
|
|
.in_state(is_selected)
|
|
|
|
|
.style_for(mouse_state);
|
|
|
|
|
|
|
|
|
|
Flex::row()
|
2023-08-30 21:23:12 +00:00
|
|
|
|
.with_child(render_tree_branch(
|
|
|
|
|
tree_branch,
|
|
|
|
|
&row.name.text,
|
|
|
|
|
is_last,
|
|
|
|
|
vec2f(host_avatar_width, theme.row_height),
|
|
|
|
|
cx.font_cache(),
|
|
|
|
|
))
|
2023-07-25 03:00:31 +00:00
|
|
|
|
.with_child(
|
|
|
|
|
Svg::new("icons/disable_screen_sharing_12.svg")
|
2023-08-30 21:23:12 +00:00
|
|
|
|
.with_color(theme.channel_hash.color)
|
2023-07-25 03:00:31 +00:00
|
|
|
|
.constrained()
|
2023-08-30 21:23:12 +00:00
|
|
|
|
.with_width(theme.channel_hash.width)
|
2023-07-25 03:00:31 +00:00
|
|
|
|
.aligned()
|
2023-08-30 21:23:12 +00:00
|
|
|
|
.left(),
|
2023-07-25 03:00:31 +00:00
|
|
|
|
)
|
|
|
|
|
.with_child(
|
|
|
|
|
Label::new("Screen", row.name.text.clone())
|
|
|
|
|
.aligned()
|
|
|
|
|
.left()
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(row.name.container)
|
|
|
|
|
.flex(1., false),
|
|
|
|
|
)
|
|
|
|
|
.constrained()
|
|
|
|
|
.with_height(theme.row_height)
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(row.container)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.with_cursor_style(CursorStyle::PointingHand)
|
|
|
|
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
|
|
|
|
if let Some(workspace) = this.workspace.upgrade(cx) {
|
|
|
|
|
workspace.update(cx, |workspace, cx| {
|
|
|
|
|
workspace.open_shared_screen(peer_id, cx)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.into_any()
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-10 00:11:52 +00:00
|
|
|
|
fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
|
|
|
|
if let Some(_) = self.channel_editing_state.take() {
|
2023-08-09 19:20:48 +00:00
|
|
|
|
self.channel_name_editor.update(cx, |editor, cx| {
|
|
|
|
|
editor.set_text("", cx);
|
2023-08-10 00:11:52 +00:00
|
|
|
|
});
|
|
|
|
|
true
|
2023-08-09 19:20:48 +00:00
|
|
|
|
} else {
|
2023-08-10 00:11:52 +00:00
|
|
|
|
false
|
2023-08-09 19:20:48 +00:00
|
|
|
|
}
|
2023-08-01 20:22:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-25 03:00:31 +00:00
|
|
|
|
fn render_header(
|
2023-08-01 20:22:06 +00:00
|
|
|
|
&self,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
section: Section,
|
2023-07-26 00:29:09 +00:00
|
|
|
|
theme: &theme::Theme,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
is_selected: bool,
|
|
|
|
|
is_collapsed: bool,
|
|
|
|
|
cx: &mut ViewContext<Self>,
|
|
|
|
|
) -> AnyElement<Self> {
|
|
|
|
|
enum Header {}
|
|
|
|
|
enum LeaveCallContactList {}
|
2023-07-26 18:11:48 +00:00
|
|
|
|
enum AddChannel {}
|
2023-07-25 03:00:31 +00:00
|
|
|
|
|
2023-07-26 00:29:09 +00:00
|
|
|
|
let tooltip_style = &theme.tooltip;
|
2023-07-25 03:00:31 +00:00
|
|
|
|
let text = match section {
|
2023-08-15 17:45:03 +00:00
|
|
|
|
Section::ActiveCall => {
|
|
|
|
|
let channel_name = iife!({
|
2023-08-15 21:56:54 +00:00
|
|
|
|
let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
|
|
|
|
|
|
2023-08-15 17:45:03 +00:00
|
|
|
|
let name = self
|
|
|
|
|
.channel_store
|
|
|
|
|
.read(cx)
|
|
|
|
|
.channel_for_id(channel_id)?
|
|
|
|
|
.name
|
|
|
|
|
.as_str();
|
|
|
|
|
|
|
|
|
|
Some(name)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if let Some(name) = channel_name {
|
2023-08-24 22:00:54 +00:00
|
|
|
|
Cow::Owned(format!("#{}", name))
|
2023-08-15 17:45:03 +00:00
|
|
|
|
} else {
|
|
|
|
|
Cow::Borrowed("Current Call")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Section::ContactRequests => Cow::Borrowed("Requests"),
|
|
|
|
|
Section::Contacts => Cow::Borrowed("Contacts"),
|
|
|
|
|
Section::Channels => Cow::Borrowed("Channels"),
|
|
|
|
|
Section::ChannelInvites => Cow::Borrowed("Invites"),
|
|
|
|
|
Section::Online => Cow::Borrowed("Online"),
|
|
|
|
|
Section::Offline => Cow::Borrowed("Offline"),
|
2023-07-25 03:00:31 +00:00
|
|
|
|
};
|
2023-07-26 00:29:09 +00:00
|
|
|
|
|
|
|
|
|
enum AddContact {}
|
|
|
|
|
let button = match section {
|
|
|
|
|
Section::ActiveCall => Some(
|
2023-08-15 10:25:45 +00:00
|
|
|
|
MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
|
2023-07-26 00:29:09 +00:00
|
|
|
|
render_icon_button(
|
2023-08-09 16:44:34 +00:00
|
|
|
|
theme
|
|
|
|
|
.collab_panel
|
|
|
|
|
.leave_call_button
|
|
|
|
|
.style_for(is_selected, state),
|
2023-08-14 19:57:31 +00:00
|
|
|
|
"icons/exit.svg",
|
2023-07-26 00:29:09 +00:00
|
|
|
|
)
|
2023-07-25 03:00:31 +00:00
|
|
|
|
})
|
2023-07-26 00:29:09 +00:00
|
|
|
|
.with_cursor_style(CursorStyle::PointingHand)
|
2023-07-25 03:00:31 +00:00
|
|
|
|
.on_click(MouseButton::Left, |_, _, cx| {
|
2023-08-04 23:14:01 +00:00
|
|
|
|
Self::leave_call(cx);
|
2023-07-25 03:00:31 +00:00
|
|
|
|
})
|
2023-07-26 00:29:09 +00:00
|
|
|
|
.with_tooltip::<AddContact>(
|
|
|
|
|
0,
|
2023-08-12 19:44:22 +00:00
|
|
|
|
"Leave call",
|
2023-07-26 00:29:09 +00:00
|
|
|
|
None,
|
|
|
|
|
tooltip_style.clone(),
|
|
|
|
|
cx,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Section::Contacts => Some(
|
2023-08-15 10:25:45 +00:00
|
|
|
|
MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
|
2023-07-26 00:29:09 +00:00
|
|
|
|
render_icon_button(
|
2023-08-09 16:44:34 +00:00
|
|
|
|
theme
|
|
|
|
|
.collab_panel
|
|
|
|
|
.add_contact_button
|
|
|
|
|
.style_for(is_selected, state),
|
2023-09-15 16:50:49 +00:00
|
|
|
|
"icons/plus.svg",
|
2023-07-26 00:29:09 +00:00
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.with_cursor_style(CursorStyle::PointingHand)
|
|
|
|
|
.on_click(MouseButton::Left, |_, this, cx| {
|
|
|
|
|
this.toggle_contact_finder(cx);
|
|
|
|
|
})
|
|
|
|
|
.with_tooltip::<LeaveCallContactList>(
|
|
|
|
|
0,
|
2023-08-12 19:44:22 +00:00
|
|
|
|
"Search for new contact",
|
2023-07-26 00:29:09 +00:00
|
|
|
|
None,
|
|
|
|
|
tooltip_style.clone(),
|
|
|
|
|
cx,
|
|
|
|
|
),
|
|
|
|
|
),
|
2023-07-26 18:11:48 +00:00
|
|
|
|
Section::Channels => Some(
|
2023-08-15 10:25:45 +00:00
|
|
|
|
MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| {
|
2023-08-04 23:14:01 +00:00
|
|
|
|
render_icon_button(
|
2023-08-09 16:44:34 +00:00
|
|
|
|
theme
|
|
|
|
|
.collab_panel
|
|
|
|
|
.add_contact_button
|
|
|
|
|
.style_for(is_selected, state),
|
2023-08-14 19:57:31 +00:00
|
|
|
|
"icons/plus.svg",
|
2023-08-04 23:14:01 +00:00
|
|
|
|
)
|
2023-07-26 18:11:48 +00:00
|
|
|
|
})
|
|
|
|
|
.with_cursor_style(CursorStyle::PointingHand)
|
2023-08-02 01:20:25 +00:00
|
|
|
|
.on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx))
|
2023-07-26 18:11:48 +00:00
|
|
|
|
.with_tooltip::<AddChannel>(
|
|
|
|
|
0,
|
2023-08-15 20:08:44 +00:00
|
|
|
|
"Create a channel",
|
2023-07-26 18:11:48 +00:00
|
|
|
|
None,
|
|
|
|
|
tooltip_style.clone(),
|
|
|
|
|
cx,
|
|
|
|
|
),
|
|
|
|
|
),
|
2023-07-26 00:29:09 +00:00
|
|
|
|
_ => None,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
};
|
|
|
|
|
|
2023-09-09 00:06:39 +00:00
|
|
|
|
let can_collapse = match section {
|
|
|
|
|
Section::ActiveCall | Section::Channels | Section::Contacts => false,
|
|
|
|
|
Section::ChannelInvites
|
|
|
|
|
| Section::ContactRequests
|
|
|
|
|
| Section::Online
|
|
|
|
|
| Section::Offline => true,
|
|
|
|
|
};
|
2023-07-26 00:29:09 +00:00
|
|
|
|
let icon_size = (&theme.collab_panel).section_icon_size;
|
2023-08-15 17:58:24 +00:00
|
|
|
|
let mut result = MouseEventHandler::new::<Header, _>(section as usize, cx, |state, _| {
|
2023-07-26 01:00:49 +00:00
|
|
|
|
let header_style = if can_collapse {
|
|
|
|
|
theme
|
|
|
|
|
.collab_panel
|
|
|
|
|
.subheader_row
|
|
|
|
|
.in_state(is_selected)
|
|
|
|
|
.style_for(state)
|
2023-07-26 00:29:09 +00:00
|
|
|
|
} else {
|
|
|
|
|
&theme.collab_panel.header_row
|
2023-07-26 01:00:49 +00:00
|
|
|
|
};
|
2023-07-26 00:29:09 +00:00
|
|
|
|
|
2023-08-02 01:20:25 +00:00
|
|
|
|
Flex::row()
|
|
|
|
|
.with_children(if can_collapse {
|
|
|
|
|
Some(
|
|
|
|
|
Svg::new(if is_collapsed {
|
2023-08-14 19:57:31 +00:00
|
|
|
|
"icons/chevron_right.svg"
|
2023-07-26 00:29:09 +00:00
|
|
|
|
} else {
|
2023-08-14 19:57:31 +00:00
|
|
|
|
"icons/chevron_down.svg"
|
2023-07-26 00:29:09 +00:00
|
|
|
|
})
|
2023-08-02 01:20:25 +00:00
|
|
|
|
.with_color(header_style.text.color)
|
|
|
|
|
.constrained()
|
|
|
|
|
.with_max_width(icon_size)
|
|
|
|
|
.with_max_height(icon_size)
|
|
|
|
|
.aligned()
|
2023-07-26 00:29:09 +00:00
|
|
|
|
.constrained()
|
2023-08-02 01:20:25 +00:00
|
|
|
|
.with_width(icon_size)
|
2023-07-26 00:29:09 +00:00
|
|
|
|
.contained()
|
2023-08-02 01:20:25 +00:00
|
|
|
|
.with_margin_right(
|
|
|
|
|
theme.collab_panel.contact_username.container.margin.left,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
})
|
|
|
|
|
.with_child(
|
|
|
|
|
Label::new(text, header_style.text.clone())
|
|
|
|
|
.aligned()
|
|
|
|
|
.left()
|
|
|
|
|
.flex(1., true),
|
2023-07-25 03:00:31 +00:00
|
|
|
|
)
|
2023-08-02 01:20:25 +00:00
|
|
|
|
.with_children(button.map(|button| button.aligned().right()))
|
|
|
|
|
.constrained()
|
|
|
|
|
.with_height(theme.collab_panel.row_height)
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(header_style.container)
|
2023-08-15 17:58:24 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if can_collapse {
|
|
|
|
|
result = result
|
|
|
|
|
.with_cursor_style(CursorStyle::PointingHand)
|
|
|
|
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
|
|
|
|
if can_collapse {
|
2023-08-23 23:25:17 +00:00
|
|
|
|
this.toggle_section_expanded(section, cx);
|
2023-08-15 17:58:24 +00:00
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.into_any()
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn render_contact(
|
|
|
|
|
contact: &Contact,
|
|
|
|
|
calling: bool,
|
|
|
|
|
project: &ModelHandle<Project>,
|
2023-07-25 17:02:01 +00:00
|
|
|
|
theme: &theme::CollabPanel,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
is_selected: bool,
|
|
|
|
|
cx: &mut ViewContext<Self>,
|
|
|
|
|
) -> AnyElement<Self> {
|
|
|
|
|
let online = contact.online;
|
|
|
|
|
let busy = contact.busy || calling;
|
|
|
|
|
let user_id = contact.user.id;
|
|
|
|
|
let github_login = contact.user.github_login.clone();
|
|
|
|
|
let initial_project = project.clone();
|
|
|
|
|
let mut event_handler =
|
2023-08-15 10:25:45 +00:00
|
|
|
|
MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
Flex::row()
|
|
|
|
|
.with_children(contact.user.avatar.clone().map(|avatar| {
|
|
|
|
|
let status_badge = if contact.online {
|
|
|
|
|
Some(
|
|
|
|
|
Empty::new()
|
|
|
|
|
.collapsed()
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(if busy {
|
|
|
|
|
theme.contact_status_busy
|
|
|
|
|
} else {
|
|
|
|
|
theme.contact_status_free
|
|
|
|
|
})
|
|
|
|
|
.aligned(),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
Stack::new()
|
|
|
|
|
.with_child(
|
|
|
|
|
Image::from_data(avatar)
|
|
|
|
|
.with_style(theme.contact_avatar)
|
|
|
|
|
.aligned()
|
|
|
|
|
.left(),
|
|
|
|
|
)
|
|
|
|
|
.with_children(status_badge)
|
|
|
|
|
}))
|
|
|
|
|
.with_child(
|
|
|
|
|
Label::new(
|
|
|
|
|
contact.user.github_login.clone(),
|
|
|
|
|
theme.contact_username.text.clone(),
|
|
|
|
|
)
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(theme.contact_username.container)
|
|
|
|
|
.aligned()
|
|
|
|
|
.left()
|
|
|
|
|
.flex(1., true),
|
|
|
|
|
)
|
|
|
|
|
.with_child(
|
2023-08-15 10:25:45 +00:00
|
|
|
|
MouseEventHandler::new::<Cancel, _>(
|
2023-07-25 03:00:31 +00:00
|
|
|
|
contact.user.id as usize,
|
|
|
|
|
cx,
|
|
|
|
|
|mouse_state, _| {
|
|
|
|
|
let button_style = theme.contact_button.style_for(mouse_state);
|
2023-08-14 19:57:31 +00:00
|
|
|
|
render_icon_button(button_style, "icons/x.svg")
|
2023-07-25 03:00:31 +00:00
|
|
|
|
.aligned()
|
|
|
|
|
.flex_float()
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.with_padding(Padding::uniform(2.))
|
|
|
|
|
.with_cursor_style(CursorStyle::PointingHand)
|
|
|
|
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
|
|
|
|
this.remove_contact(user_id, &github_login, cx);
|
|
|
|
|
})
|
|
|
|
|
.flex_float(),
|
|
|
|
|
)
|
|
|
|
|
.with_children(if calling {
|
|
|
|
|
Some(
|
|
|
|
|
Label::new("Calling", theme.calling_indicator.text.clone())
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(theme.calling_indicator.container)
|
|
|
|
|
.aligned(),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
})
|
|
|
|
|
.constrained()
|
|
|
|
|
.with_height(theme.row_height)
|
|
|
|
|
.contained()
|
2023-07-26 00:29:09 +00:00
|
|
|
|
.with_style(*theme.contact_row.in_state(is_selected).style_for(state))
|
2023-07-25 03:00:31 +00:00
|
|
|
|
})
|
|
|
|
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
|
|
|
|
if online && !busy {
|
|
|
|
|
this.call(user_id, Some(initial_project.clone()), cx);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if online {
|
|
|
|
|
event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
event_handler.into_any()
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-14 17:34:00 +00:00
|
|
|
|
fn render_contact_placeholder(
|
|
|
|
|
&self,
|
|
|
|
|
theme: &theme::CollabPanel,
|
|
|
|
|
is_selected: bool,
|
|
|
|
|
cx: &mut ViewContext<Self>,
|
|
|
|
|
) -> AnyElement<Self> {
|
|
|
|
|
enum AddContacts {}
|
2023-08-15 10:25:45 +00:00
|
|
|
|
MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
|
2023-08-14 17:34:00 +00:00
|
|
|
|
let style = theme.list_empty_state.style_for(is_selected, state);
|
2023-08-14 18:57:15 +00:00
|
|
|
|
Flex::row()
|
|
|
|
|
.with_child(
|
2023-08-14 19:57:31 +00:00
|
|
|
|
Svg::new("icons/plus.svg")
|
2023-08-14 18:57:15 +00:00
|
|
|
|
.with_color(theme.list_empty_icon.color)
|
|
|
|
|
.constrained()
|
|
|
|
|
.with_width(theme.list_empty_icon.width)
|
|
|
|
|
.aligned()
|
|
|
|
|
.left(),
|
|
|
|
|
)
|
|
|
|
|
.with_child(
|
|
|
|
|
Label::new("Add a contact", style.text.clone())
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(theme.list_empty_label_container),
|
|
|
|
|
)
|
|
|
|
|
.align_children_center()
|
2023-08-14 17:34:00 +00:00
|
|
|
|
.contained()
|
|
|
|
|
.with_style(style.container)
|
|
|
|
|
.into_any()
|
|
|
|
|
})
|
|
|
|
|
.on_click(MouseButton::Left, |_, this, cx| {
|
|
|
|
|
this.toggle_contact_finder(cx);
|
|
|
|
|
})
|
2023-08-14 17:23:50 +00:00
|
|
|
|
.into_any()
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-02 01:20:25 +00:00
|
|
|
|
fn render_channel_editor(
|
|
|
|
|
&self,
|
2023-08-08 19:46:13 +00:00
|
|
|
|
theme: &theme::Theme,
|
|
|
|
|
depth: usize,
|
2023-08-02 01:20:25 +00:00
|
|
|
|
cx: &AppContext,
|
|
|
|
|
) -> AnyElement<Self> {
|
2023-08-08 19:46:13 +00:00
|
|
|
|
Flex::row()
|
2023-08-19 12:18:53 +00:00
|
|
|
|
.with_child(
|
|
|
|
|
Empty::new()
|
|
|
|
|
.constrained()
|
|
|
|
|
.with_width(theme.collab_panel.disclosure.button_space()),
|
|
|
|
|
)
|
2023-08-08 19:46:13 +00:00
|
|
|
|
.with_child(
|
2023-08-14 19:57:31 +00:00
|
|
|
|
Svg::new("icons/hash.svg")
|
2023-08-08 19:46:13 +00:00
|
|
|
|
.with_color(theme.collab_panel.channel_hash.color)
|
|
|
|
|
.constrained()
|
|
|
|
|
.with_width(theme.collab_panel.channel_hash.width)
|
|
|
|
|
.aligned()
|
|
|
|
|
.left(),
|
|
|
|
|
)
|
|
|
|
|
.with_child(
|
2023-08-10 00:11:52 +00:00
|
|
|
|
if let Some(pending_name) = self
|
|
|
|
|
.channel_editing_state
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|state| state.pending_name())
|
|
|
|
|
{
|
|
|
|
|
Label::new(
|
|
|
|
|
pending_name.to_string(),
|
|
|
|
|
theme.collab_panel.contact_username.text.clone(),
|
|
|
|
|
)
|
2023-08-08 19:46:13 +00:00
|
|
|
|
.contained()
|
2023-08-10 00:11:52 +00:00
|
|
|
|
.with_style(theme.collab_panel.contact_username.container)
|
|
|
|
|
.aligned()
|
|
|
|
|
.left()
|
|
|
|
|
.flex(1., true)
|
|
|
|
|
.into_any()
|
|
|
|
|
} else {
|
|
|
|
|
ChildView::new(&self.channel_name_editor, cx)
|
|
|
|
|
.aligned()
|
|
|
|
|
.left()
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(theme.collab_panel.channel_editor)
|
|
|
|
|
.flex(1.0, true)
|
|
|
|
|
.into_any()
|
|
|
|
|
},
|
2023-08-08 19:46:13 +00:00
|
|
|
|
)
|
|
|
|
|
.align_children_center()
|
2023-08-10 00:11:52 +00:00
|
|
|
|
.constrained()
|
|
|
|
|
.with_height(theme.collab_panel.row_height)
|
2023-08-08 19:46:13 +00:00
|
|
|
|
.contained()
|
2023-08-10 00:11:52 +00:00
|
|
|
|
.with_style(gpui::elements::ContainerStyle {
|
|
|
|
|
background_color: Some(theme.editor.background),
|
|
|
|
|
..*theme.collab_panel.contact_row.default_style()
|
|
|
|
|
})
|
2023-08-08 19:46:13 +00:00
|
|
|
|
.with_padding_left(
|
|
|
|
|
theme.collab_panel.contact_row.default_style().padding.left
|
|
|
|
|
+ theme.collab_panel.channel_indent * depth as f32,
|
|
|
|
|
)
|
|
|
|
|
.into_any()
|
2023-08-02 01:20:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-01 01:00:14 +00:00
|
|
|
|
fn render_channel(
|
2023-08-02 22:09:37 +00:00
|
|
|
|
&self,
|
2023-08-01 01:00:14 +00:00
|
|
|
|
channel: &Channel,
|
2023-08-14 23:27:35 +00:00
|
|
|
|
depth: usize,
|
2023-09-09 01:47:59 +00:00
|
|
|
|
path: ChannelPath,
|
2023-08-01 01:00:14 +00:00
|
|
|
|
theme: &theme::CollabPanel,
|
|
|
|
|
is_selected: bool,
|
|
|
|
|
cx: &mut ViewContext<Self>,
|
|
|
|
|
) -> AnyElement<Self> {
|
|
|
|
|
let channel_id = channel.id;
|
2023-08-19 12:18:53 +00:00
|
|
|
|
let has_children = self.channel_store.read(cx).has_children(channel_id);
|
2023-09-09 01:47:59 +00:00
|
|
|
|
|
|
|
|
|
let disclosed = {
|
|
|
|
|
let location = ChannelLocation {
|
|
|
|
|
channel: channel_id,
|
|
|
|
|
parent: Cow::Borrowed(&path),
|
|
|
|
|
};
|
|
|
|
|
has_children.then(|| !self.collapsed_channels.binary_search(&location).is_ok())
|
|
|
|
|
};
|
2023-08-19 12:18:53 +00:00
|
|
|
|
|
2023-09-11 20:44:41 +00:00
|
|
|
|
let is_active = iife!({
|
|
|
|
|
let call_channel = ActiveCall::global(cx)
|
|
|
|
|
.read(cx)
|
|
|
|
|
.room()?
|
|
|
|
|
.read(cx)
|
|
|
|
|
.channel_id()?;
|
|
|
|
|
Some(call_channel == channel_id)
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
|
|
|
|
const FACEPILE_LIMIT: usize = 3;
|
|
|
|
|
|
2023-09-09 00:06:39 +00:00
|
|
|
|
enum ChannelCall {}
|
2023-08-15 00:11:03 +00:00
|
|
|
|
|
2023-09-09 01:47:59 +00:00
|
|
|
|
MouseEventHandler::new::<Channel, _>(id(&path) as usize, cx, |state, cx| {
|
2023-09-11 20:44:41 +00:00
|
|
|
|
let row_hovered = state.hovered();
|
|
|
|
|
|
2023-08-19 12:18:53 +00:00
|
|
|
|
Flex::<Self>::row()
|
2023-08-04 21:12:08 +00:00
|
|
|
|
.with_child(
|
2023-08-14 19:57:31 +00:00
|
|
|
|
Svg::new("icons/hash.svg")
|
2023-08-04 23:14:01 +00:00
|
|
|
|
.with_color(theme.channel_hash.color)
|
2023-08-04 21:12:08 +00:00
|
|
|
|
.constrained()
|
2023-08-04 23:14:01 +00:00
|
|
|
|
.with_width(theme.channel_hash.width)
|
2023-08-04 21:12:08 +00:00
|
|
|
|
.aligned()
|
|
|
|
|
.left(),
|
|
|
|
|
)
|
2023-08-01 01:00:14 +00:00
|
|
|
|
.with_child(
|
2023-08-15 19:12:30 +00:00
|
|
|
|
Label::new(channel.name.clone(), theme.channel_name.text.clone())
|
2023-08-01 01:00:14 +00:00
|
|
|
|
.contained()
|
2023-08-15 19:12:30 +00:00
|
|
|
|
.with_style(theme.channel_name.container)
|
2023-08-01 01:00:14 +00:00
|
|
|
|
.aligned()
|
|
|
|
|
.left()
|
|
|
|
|
.flex(1., true),
|
|
|
|
|
)
|
2023-09-09 00:06:39 +00:00
|
|
|
|
.with_child(
|
2023-09-11 20:44:41 +00:00
|
|
|
|
MouseEventHandler::new::<ChannelCall, _>(
|
|
|
|
|
channel.id as usize,
|
|
|
|
|
cx,
|
|
|
|
|
move |_, cx| {
|
|
|
|
|
let participants =
|
|
|
|
|
self.channel_store.read(cx).channel_participants(channel_id);
|
|
|
|
|
if !participants.is_empty() {
|
|
|
|
|
let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
|
|
|
|
|
|
|
|
|
|
FacePile::new(theme.face_overlap)
|
|
|
|
|
.with_children(
|
|
|
|
|
participants
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|user| {
|
|
|
|
|
Some(
|
|
|
|
|
Image::from_data(user.avatar.clone()?)
|
|
|
|
|
.with_style(theme.channel_avatar),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.take(FACEPILE_LIMIT),
|
|
|
|
|
)
|
|
|
|
|
.with_children((extra_count > 0).then(|| {
|
|
|
|
|
Label::new(
|
|
|
|
|
format!("+{}", extra_count),
|
|
|
|
|
theme.extra_participant_label.text.clone(),
|
|
|
|
|
)
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(theme.extra_participant_label.container)
|
|
|
|
|
}))
|
|
|
|
|
.into_any()
|
|
|
|
|
} else if row_hovered {
|
2023-09-15 18:14:04 +00:00
|
|
|
|
Svg::new("icons/speaker-loud.svg")
|
2023-09-11 20:44:41 +00:00
|
|
|
|
.with_color(theme.channel_hash.color)
|
|
|
|
|
.constrained()
|
|
|
|
|
.with_width(theme.channel_hash.width)
|
|
|
|
|
.into_any()
|
|
|
|
|
} else {
|
|
|
|
|
Empty::new().into_any()
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
2023-09-09 00:06:39 +00:00
|
|
|
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
2023-09-11 20:44:41 +00:00
|
|
|
|
this.join_channel_call(channel_id, cx);
|
2023-09-09 00:06:39 +00:00
|
|
|
|
}),
|
|
|
|
|
)
|
2023-08-04 23:14:01 +00:00
|
|
|
|
.align_children_center()
|
2023-08-19 12:18:53 +00:00
|
|
|
|
.styleable_component()
|
2023-09-09 01:47:59 +00:00
|
|
|
|
.disclosable(disclosed, Box::new(ToggleCollapse { location: (channel_id, path.clone()).into() }))
|
|
|
|
|
.with_id(id(&path) as usize)
|
2023-08-19 12:18:53 +00:00
|
|
|
|
.with_style(theme.disclosure.clone())
|
|
|
|
|
.element()
|
2023-08-01 01:00:14 +00:00
|
|
|
|
.constrained()
|
|
|
|
|
.with_height(theme.row_height)
|
|
|
|
|
.contained()
|
2023-09-11 20:44:41 +00:00
|
|
|
|
.with_style(*theme.channel_row.style_for(is_selected || is_active, state))
|
2023-08-04 23:14:01 +00:00
|
|
|
|
.with_padding_left(
|
2023-08-15 19:12:30 +00:00
|
|
|
|
theme.channel_row.default_style().padding.left
|
2023-08-14 23:27:35 +00:00
|
|
|
|
+ theme.channel_indent * depth as f32,
|
2023-08-04 23:14:01 +00:00
|
|
|
|
)
|
2023-08-01 01:00:14 +00:00
|
|
|
|
})
|
|
|
|
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
2023-09-09 00:06:39 +00:00
|
|
|
|
this.join_channel_chat(channel_id, cx);
|
|
|
|
|
})
|
|
|
|
|
.on_click(MouseButton::Right, move |e, this, cx| {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
this.deploy_channel_context_menu(Some(e.position), &(channel_id, path.clone()).into(), cx);
|
2023-09-09 00:06:39 +00:00
|
|
|
|
})
|
|
|
|
|
.with_cursor_style(CursorStyle::PointingHand)
|
|
|
|
|
.into_any()
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-24 22:00:54 +00:00
|
|
|
|
fn render_channel_notes(
|
|
|
|
|
&self,
|
|
|
|
|
channel_id: ChannelId,
|
|
|
|
|
theme: &theme::CollabPanel,
|
|
|
|
|
is_selected: bool,
|
|
|
|
|
cx: &mut ViewContext<Self>,
|
|
|
|
|
) -> AnyElement<Self> {
|
|
|
|
|
enum ChannelNotes {}
|
|
|
|
|
let host_avatar_width = theme
|
|
|
|
|
.contact_avatar
|
|
|
|
|
.width
|
|
|
|
|
.or(theme.contact_avatar.height)
|
|
|
|
|
.unwrap_or(0.);
|
|
|
|
|
|
|
|
|
|
MouseEventHandler::new::<ChannelNotes, _>(channel_id as usize, cx, |state, cx| {
|
|
|
|
|
let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
|
|
|
|
|
let row = theme.project_row.in_state(is_selected).style_for(state);
|
|
|
|
|
|
|
|
|
|
Flex::<Self>::row()
|
|
|
|
|
.with_child(render_tree_branch(
|
|
|
|
|
tree_branch,
|
|
|
|
|
&row.name.text,
|
|
|
|
|
true,
|
|
|
|
|
vec2f(host_avatar_width, theme.row_height),
|
|
|
|
|
cx.font_cache(),
|
|
|
|
|
))
|
|
|
|
|
.with_child(
|
2023-09-15 16:50:49 +00:00
|
|
|
|
Svg::new("icons/file.svg")
|
2023-08-24 22:00:54 +00:00
|
|
|
|
.with_color(theme.channel_hash.color)
|
|
|
|
|
.constrained()
|
|
|
|
|
.with_width(theme.channel_hash.width)
|
|
|
|
|
.aligned()
|
|
|
|
|
.left(),
|
|
|
|
|
)
|
|
|
|
|
.with_child(
|
|
|
|
|
Label::new("notes", theme.channel_name.text.clone())
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(theme.channel_name.container)
|
|
|
|
|
.aligned()
|
|
|
|
|
.left()
|
|
|
|
|
.flex(1., true),
|
|
|
|
|
)
|
|
|
|
|
.constrained()
|
|
|
|
|
.with_height(theme.row_height)
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(*theme.channel_row.style_for(is_selected, state))
|
|
|
|
|
.with_padding_left(theme.channel_row.default_style().padding.left)
|
|
|
|
|
})
|
|
|
|
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
2023-09-11 20:44:41 +00:00
|
|
|
|
this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
|
2023-08-24 22:00:54 +00:00
|
|
|
|
})
|
|
|
|
|
.with_cursor_style(CursorStyle::PointingHand)
|
|
|
|
|
.into_any()
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-01 01:00:14 +00:00
|
|
|
|
fn render_channel_invite(
|
|
|
|
|
channel: Arc<Channel>,
|
2023-08-01 20:22:06 +00:00
|
|
|
|
channel_store: ModelHandle<ChannelStore>,
|
2023-08-01 01:00:14 +00:00
|
|
|
|
theme: &theme::CollabPanel,
|
|
|
|
|
is_selected: bool,
|
|
|
|
|
cx: &mut ViewContext<Self>,
|
|
|
|
|
) -> AnyElement<Self> {
|
|
|
|
|
enum Decline {}
|
|
|
|
|
enum Accept {}
|
|
|
|
|
|
|
|
|
|
let channel_id = channel.id;
|
2023-08-03 21:49:01 +00:00
|
|
|
|
let is_invite_pending = channel_store
|
|
|
|
|
.read(cx)
|
|
|
|
|
.has_pending_channel_invite_response(&channel);
|
2023-08-01 01:00:14 +00:00
|
|
|
|
let button_spacing = theme.contact_button_spacing;
|
|
|
|
|
|
|
|
|
|
Flex::row()
|
2023-08-04 23:14:01 +00:00
|
|
|
|
.with_child(
|
2023-08-14 19:57:31 +00:00
|
|
|
|
Svg::new("icons/hash.svg")
|
2023-08-04 23:14:01 +00:00
|
|
|
|
.with_color(theme.channel_hash.color)
|
|
|
|
|
.constrained()
|
|
|
|
|
.with_width(theme.channel_hash.width)
|
|
|
|
|
.aligned()
|
|
|
|
|
.left(),
|
|
|
|
|
)
|
2023-08-01 01:00:14 +00:00
|
|
|
|
.with_child(
|
|
|
|
|
Label::new(channel.name.clone(), theme.contact_username.text.clone())
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(theme.contact_username.container)
|
|
|
|
|
.aligned()
|
|
|
|
|
.left()
|
|
|
|
|
.flex(1., true),
|
|
|
|
|
)
|
|
|
|
|
.with_child(
|
2023-08-15 10:25:45 +00:00
|
|
|
|
MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
|
|
|
|
|
let button_style = if is_invite_pending {
|
|
|
|
|
&theme.disabled_button
|
|
|
|
|
} else {
|
|
|
|
|
theme.contact_button.style_for(mouse_state)
|
|
|
|
|
};
|
|
|
|
|
render_icon_button(button_style, "icons/x.svg").aligned()
|
|
|
|
|
})
|
2023-08-01 01:00:14 +00:00
|
|
|
|
.with_cursor_style(CursorStyle::PointingHand)
|
|
|
|
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
|
|
|
|
this.respond_to_channel_invite(channel_id, false, cx);
|
|
|
|
|
})
|
|
|
|
|
.contained()
|
|
|
|
|
.with_margin_right(button_spacing),
|
|
|
|
|
)
|
|
|
|
|
.with_child(
|
2023-08-15 10:25:45 +00:00
|
|
|
|
MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
|
|
|
|
|
let button_style = if is_invite_pending {
|
|
|
|
|
&theme.disabled_button
|
|
|
|
|
} else {
|
|
|
|
|
theme.contact_button.style_for(mouse_state)
|
|
|
|
|
};
|
2023-08-15 20:29:01 +00:00
|
|
|
|
render_icon_button(button_style, "icons/check.svg")
|
2023-08-15 10:25:45 +00:00
|
|
|
|
.aligned()
|
|
|
|
|
.flex_float()
|
|
|
|
|
})
|
2023-08-01 01:00:14 +00:00
|
|
|
|
.with_cursor_style(CursorStyle::PointingHand)
|
|
|
|
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
|
|
|
|
this.respond_to_channel_invite(channel_id, true, cx);
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.constrained()
|
|
|
|
|
.with_height(theme.row_height)
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(
|
|
|
|
|
*theme
|
|
|
|
|
.contact_row
|
|
|
|
|
.in_state(is_selected)
|
|
|
|
|
.style_for(&mut Default::default()),
|
|
|
|
|
)
|
2023-08-04 23:14:01 +00:00
|
|
|
|
.with_padding_left(
|
|
|
|
|
theme.contact_row.default_style().padding.left + theme.channel_indent,
|
|
|
|
|
)
|
2023-08-01 01:00:14 +00:00
|
|
|
|
.into_any()
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-25 03:00:31 +00:00
|
|
|
|
fn render_contact_request(
|
|
|
|
|
user: Arc<User>,
|
|
|
|
|
user_store: ModelHandle<UserStore>,
|
2023-07-25 17:02:01 +00:00
|
|
|
|
theme: &theme::CollabPanel,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
is_incoming: bool,
|
|
|
|
|
is_selected: bool,
|
|
|
|
|
cx: &mut ViewContext<Self>,
|
|
|
|
|
) -> AnyElement<Self> {
|
|
|
|
|
enum Decline {}
|
|
|
|
|
enum Accept {}
|
|
|
|
|
enum Cancel {}
|
|
|
|
|
|
|
|
|
|
let mut row = Flex::row()
|
|
|
|
|
.with_children(user.avatar.clone().map(|avatar| {
|
|
|
|
|
Image::from_data(avatar)
|
|
|
|
|
.with_style(theme.contact_avatar)
|
|
|
|
|
.aligned()
|
|
|
|
|
.left()
|
|
|
|
|
}))
|
|
|
|
|
.with_child(
|
|
|
|
|
Label::new(
|
|
|
|
|
user.github_login.clone(),
|
|
|
|
|
theme.contact_username.text.clone(),
|
|
|
|
|
)
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(theme.contact_username.container)
|
|
|
|
|
.aligned()
|
|
|
|
|
.left()
|
|
|
|
|
.flex(1., true),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let user_id = user.id;
|
|
|
|
|
let github_login = user.github_login.clone();
|
|
|
|
|
let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
|
|
|
|
|
let button_spacing = theme.contact_button_spacing;
|
|
|
|
|
|
|
|
|
|
if is_incoming {
|
|
|
|
|
row.add_child(
|
2023-08-15 10:25:45 +00:00
|
|
|
|
MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
let button_style = if is_contact_request_pending {
|
|
|
|
|
&theme.disabled_button
|
|
|
|
|
} else {
|
|
|
|
|
theme.contact_button.style_for(mouse_state)
|
|
|
|
|
};
|
2023-08-14 19:57:31 +00:00
|
|
|
|
render_icon_button(button_style, "icons/x.svg").aligned()
|
2023-07-25 03:00:31 +00:00
|
|
|
|
})
|
|
|
|
|
.with_cursor_style(CursorStyle::PointingHand)
|
|
|
|
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
|
|
|
|
this.respond_to_contact_request(user_id, false, cx);
|
|
|
|
|
})
|
|
|
|
|
.contained()
|
|
|
|
|
.with_margin_right(button_spacing),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
row.add_child(
|
2023-08-15 10:25:45 +00:00
|
|
|
|
MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
let button_style = if is_contact_request_pending {
|
|
|
|
|
&theme.disabled_button
|
|
|
|
|
} else {
|
|
|
|
|
theme.contact_button.style_for(mouse_state)
|
|
|
|
|
};
|
2023-08-15 20:29:01 +00:00
|
|
|
|
render_icon_button(button_style, "icons/check.svg")
|
2023-07-25 03:00:31 +00:00
|
|
|
|
.aligned()
|
|
|
|
|
.flex_float()
|
|
|
|
|
})
|
|
|
|
|
.with_cursor_style(CursorStyle::PointingHand)
|
|
|
|
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
|
|
|
|
this.respond_to_contact_request(user_id, true, cx);
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
row.add_child(
|
2023-08-15 10:25:45 +00:00
|
|
|
|
MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
let button_style = if is_contact_request_pending {
|
|
|
|
|
&theme.disabled_button
|
|
|
|
|
} else {
|
|
|
|
|
theme.contact_button.style_for(mouse_state)
|
|
|
|
|
};
|
2023-08-14 19:57:31 +00:00
|
|
|
|
render_icon_button(button_style, "icons/x.svg")
|
2023-07-25 03:00:31 +00:00
|
|
|
|
.aligned()
|
|
|
|
|
.flex_float()
|
|
|
|
|
})
|
|
|
|
|
.with_padding(Padding::uniform(2.))
|
|
|
|
|
.with_cursor_style(CursorStyle::PointingHand)
|
|
|
|
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
|
|
|
|
this.remove_contact(user_id, &github_login, cx);
|
|
|
|
|
})
|
|
|
|
|
.flex_float(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
row.constrained()
|
|
|
|
|
.with_height(theme.row_height)
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(
|
|
|
|
|
*theme
|
|
|
|
|
.contact_row
|
|
|
|
|
.in_state(is_selected)
|
|
|
|
|
.style_for(&mut Default::default()),
|
|
|
|
|
)
|
|
|
|
|
.into_any()
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-02 01:20:25 +00:00
|
|
|
|
fn deploy_channel_context_menu(
|
|
|
|
|
&mut self,
|
2023-08-08 19:46:13 +00:00
|
|
|
|
position: Option<Vector2F>,
|
2023-09-09 01:47:59 +00:00
|
|
|
|
location: &ChannelLocation<'static>,
|
2023-08-02 01:20:25 +00:00
|
|
|
|
cx: &mut ViewContext<Self>,
|
|
|
|
|
) {
|
2023-08-25 00:17:20 +00:00
|
|
|
|
self.context_menu_on_selected = position.is_none();
|
2023-08-08 19:46:13 +00:00
|
|
|
|
|
2023-08-25 00:17:20 +00:00
|
|
|
|
self.context_menu.update(cx, |context_menu, cx| {
|
|
|
|
|
context_menu.set_position_mode(if self.context_menu_on_selected {
|
|
|
|
|
OverlayPositionMode::Local
|
|
|
|
|
} else {
|
|
|
|
|
OverlayPositionMode::Window
|
|
|
|
|
});
|
2023-08-08 19:46:13 +00:00
|
|
|
|
|
2023-09-09 01:47:59 +00:00
|
|
|
|
let expand_action_name = if self.is_channel_collapsed(&location) {
|
2023-08-25 00:17:20 +00:00
|
|
|
|
"Expand Subchannels"
|
|
|
|
|
} else {
|
|
|
|
|
"Collapse Subchannels"
|
|
|
|
|
};
|
2023-08-23 23:25:17 +00:00
|
|
|
|
|
2023-08-25 00:17:20 +00:00
|
|
|
|
let mut items = vec![
|
2023-09-09 01:47:59 +00:00
|
|
|
|
ContextMenuItem::action(expand_action_name, ToggleCollapse { location: location.clone() }),
|
|
|
|
|
ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id: location.channel }),
|
2023-08-25 00:17:20 +00:00
|
|
|
|
];
|
|
|
|
|
|
2023-09-09 01:47:59 +00:00
|
|
|
|
if self.channel_store.read(cx).is_user_admin(location.channel) {
|
2023-08-25 00:17:20 +00:00
|
|
|
|
items.extend([
|
|
|
|
|
ContextMenuItem::Separator,
|
2023-09-09 01:47:59 +00:00
|
|
|
|
ContextMenuItem::action("New Subchannel", NewChannel { location: location.clone() }),
|
|
|
|
|
ContextMenuItem::action("Rename", RenameChannel { location: location.clone() }),
|
|
|
|
|
ContextMenuItem::action("Copy", CopyChannel { channel_id: location.channel }),
|
|
|
|
|
ContextMenuItem::action("Paste", PasteChannel { channel_id: location.channel }),
|
2023-08-25 00:17:20 +00:00
|
|
|
|
ContextMenuItem::Separator,
|
2023-09-09 01:47:59 +00:00
|
|
|
|
ContextMenuItem::action("Invite Members", InviteMembers { channel_id: location.channel }),
|
|
|
|
|
ContextMenuItem::action("Manage Members", ManageMembers { channel_id: location.channel }),
|
2023-08-25 00:17:20 +00:00
|
|
|
|
ContextMenuItem::Separator,
|
2023-09-09 01:47:59 +00:00
|
|
|
|
ContextMenuItem::action("Delete", RemoveChannel { channel_id: location.channel }),
|
2023-08-25 00:17:20 +00:00
|
|
|
|
]);
|
|
|
|
|
}
|
2023-08-08 19:46:13 +00:00
|
|
|
|
|
2023-08-25 00:17:20 +00:00
|
|
|
|
context_menu.show(
|
|
|
|
|
position.unwrap_or_default(),
|
|
|
|
|
if self.context_menu_on_selected {
|
|
|
|
|
gpui::elements::AnchorCorner::TopRight
|
|
|
|
|
} else {
|
|
|
|
|
gpui::elements::AnchorCorner::BottomLeft
|
|
|
|
|
},
|
|
|
|
|
items,
|
|
|
|
|
cx,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
cx.notify();
|
2023-08-02 01:20:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-25 03:00:31 +00:00
|
|
|
|
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
2023-08-10 00:11:52 +00:00
|
|
|
|
if self.take_editing_state(cx) {
|
2023-08-04 23:14:01 +00:00
|
|
|
|
cx.focus(&self.filter_editor);
|
|
|
|
|
} else {
|
|
|
|
|
self.filter_editor.update(cx, |editor, cx| {
|
|
|
|
|
if editor.buffer().read(cx).len(cx) > 0 {
|
|
|
|
|
editor.set_text("", cx);
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
2023-08-04 23:14:01 +00:00
|
|
|
|
|
2023-08-09 15:54:24 +00:00
|
|
|
|
self.update_entries(false, cx);
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
2023-08-04 23:14:01 +00:00
|
|
|
|
let ix = self.selection.map_or(0, |ix| ix + 1);
|
|
|
|
|
if ix < self.entries.len() {
|
|
|
|
|
self.selection = Some(ix);
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
2023-07-26 01:00:49 +00:00
|
|
|
|
|
2023-07-25 03:00:31 +00:00
|
|
|
|
self.list_state.reset(self.entries.len());
|
|
|
|
|
if let Some(ix) = self.selection {
|
|
|
|
|
self.list_state.scroll_to(ListOffset {
|
|
|
|
|
item_ix: ix,
|
|
|
|
|
offset_in_item: 0.,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
cx.notify();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
2023-08-04 23:14:01 +00:00
|
|
|
|
let ix = self.selection.take().unwrap_or(0);
|
|
|
|
|
if ix > 0 {
|
|
|
|
|
self.selection = Some(ix - 1);
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
2023-07-26 01:00:49 +00:00
|
|
|
|
|
2023-07-25 03:00:31 +00:00
|
|
|
|
self.list_state.reset(self.entries.len());
|
|
|
|
|
if let Some(ix) = self.selection {
|
|
|
|
|
self.list_state.scroll_to(ListOffset {
|
|
|
|
|
item_ix: ix,
|
|
|
|
|
offset_in_item: 0.,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
cx.notify();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
2023-08-09 19:20:48 +00:00
|
|
|
|
if self.confirm_channel_edit(cx) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-25 03:00:31 +00:00
|
|
|
|
if let Some(selection) = self.selection {
|
|
|
|
|
if let Some(entry) = self.entries.get(selection) {
|
|
|
|
|
match entry {
|
2023-09-09 00:06:39 +00:00
|
|
|
|
ListEntry::Header(section) => match section {
|
2023-08-04 23:14:01 +00:00
|
|
|
|
Section::ActiveCall => Self::leave_call(cx),
|
|
|
|
|
Section::Channels => self.new_root_channel(cx),
|
|
|
|
|
Section::Contacts => self.toggle_contact_finder(cx),
|
|
|
|
|
Section::ContactRequests
|
|
|
|
|
| Section::Online
|
|
|
|
|
| Section::Offline
|
|
|
|
|
| Section::ChannelInvites => {
|
2023-08-23 23:25:17 +00:00
|
|
|
|
self.toggle_section_expanded(*section, cx);
|
2023-08-04 23:14:01 +00:00
|
|
|
|
}
|
|
|
|
|
},
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::Contact { contact, calling } => {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
if contact.online && !contact.busy && !calling {
|
|
|
|
|
self.call(contact.user.id, Some(self.project.clone()), cx);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::ParticipantProject {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
project_id,
|
|
|
|
|
host_user_id,
|
|
|
|
|
..
|
|
|
|
|
} => {
|
|
|
|
|
if let Some(workspace) = self.workspace.upgrade(cx) {
|
|
|
|
|
let app_state = workspace.read(cx).app_state().clone();
|
|
|
|
|
workspace::join_remote_project(
|
|
|
|
|
*project_id,
|
|
|
|
|
*host_user_id,
|
|
|
|
|
app_state,
|
|
|
|
|
cx,
|
|
|
|
|
)
|
|
|
|
|
.detach_and_log_err(cx);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::ParticipantScreen { peer_id, .. } => {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
if let Some(workspace) = self.workspace.upgrade(cx) {
|
|
|
|
|
workspace.update(cx, |workspace, cx| {
|
|
|
|
|
workspace.open_shared_screen(*peer_id, cx)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-14 23:27:35 +00:00
|
|
|
|
ListEntry::Channel { channel, .. } => {
|
2023-09-09 00:06:39 +00:00
|
|
|
|
self.join_channel_chat(channel.id, cx);
|
|
|
|
|
}
|
2023-08-14 17:34:00 +00:00
|
|
|
|
ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
|
2023-07-25 03:00:31 +00:00
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-09 15:54:24 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-10 00:11:52 +00:00
|
|
|
|
fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
|
|
|
|
|
if let Some(editing_state) = &mut self.channel_editing_state {
|
2023-08-09 19:20:48 +00:00
|
|
|
|
match editing_state {
|
2023-08-10 00:11:52 +00:00
|
|
|
|
ChannelEditingState::Create {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
location,
|
2023-08-10 00:11:52 +00:00
|
|
|
|
pending_name,
|
|
|
|
|
..
|
|
|
|
|
} => {
|
|
|
|
|
if pending_name.is_some() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
let channel_name = self.channel_name_editor.read(cx).text(cx);
|
|
|
|
|
|
|
|
|
|
*pending_name = Some(channel_name.clone());
|
|
|
|
|
|
|
|
|
|
self.channel_store
|
|
|
|
|
.update(cx, |channel_store, cx| {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
channel_store.create_channel(&channel_name, location.as_ref().map(|location| location.channel), cx)
|
2023-08-09 19:20:48 +00:00
|
|
|
|
})
|
|
|
|
|
.detach();
|
2023-08-10 00:11:52 +00:00
|
|
|
|
cx.notify();
|
2023-08-09 19:20:48 +00:00
|
|
|
|
}
|
2023-08-10 00:11:52 +00:00
|
|
|
|
ChannelEditingState::Rename {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
location,
|
2023-08-10 00:11:52 +00:00
|
|
|
|
pending_name,
|
|
|
|
|
} => {
|
|
|
|
|
if pending_name.is_some() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
let channel_name = self.channel_name_editor.read(cx).text(cx);
|
|
|
|
|
*pending_name = Some(channel_name.clone());
|
|
|
|
|
|
2023-08-09 19:20:48 +00:00
|
|
|
|
self.channel_store
|
|
|
|
|
.update(cx, |channel_store, cx| {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
channel_store.rename(location.channel, &channel_name, cx)
|
2023-08-09 19:20:48 +00:00
|
|
|
|
})
|
|
|
|
|
.detach();
|
2023-08-10 00:11:52 +00:00
|
|
|
|
cx.notify();
|
2023-08-09 19:20:48 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-10 00:11:52 +00:00
|
|
|
|
cx.focus_self();
|
2023-08-09 19:20:48 +00:00
|
|
|
|
true
|
|
|
|
|
} else {
|
|
|
|
|
false
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-23 23:25:17 +00:00
|
|
|
|
fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
|
|
|
|
|
self.collapsed_sections.remove(ix);
|
|
|
|
|
} else {
|
|
|
|
|
self.collapsed_sections.push(section);
|
|
|
|
|
}
|
2023-08-09 15:54:24 +00:00
|
|
|
|
self.update_entries(false, cx);
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-23 23:25:17 +00:00
|
|
|
|
fn collapse_selected_channel(
|
|
|
|
|
&mut self,
|
|
|
|
|
_: &CollapseSelectedChannel,
|
|
|
|
|
cx: &mut ViewContext<Self>,
|
|
|
|
|
) {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
let Some((channel_id, path)) = self.selected_channel().map(|(channel, parent)| (channel.id, parent)) else {
|
2023-08-23 23:25:17 +00:00
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
2023-09-09 01:47:59 +00:00
|
|
|
|
let path = path.to_owned();
|
|
|
|
|
|
|
|
|
|
if self.is_channel_collapsed(&(channel_id, path.clone()).into()) {
|
2023-08-23 23:25:17 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-09 01:47:59 +00:00
|
|
|
|
self.toggle_channel_collapsed(&ToggleCollapse { location: (channel_id, path).into() }, cx)
|
2023-08-23 23:25:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
let Some((channel_id, path)) = self.selected_channel().map(|(channel, parent)| (channel.id, parent)) else {
|
2023-08-23 23:25:17 +00:00
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
2023-09-09 01:47:59 +00:00
|
|
|
|
let path = path.to_owned();
|
|
|
|
|
|
|
|
|
|
if !self.is_channel_collapsed(&(channel_id, path.clone()).into()) {
|
2023-08-23 23:25:17 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-09 01:47:59 +00:00
|
|
|
|
self.toggle_channel_collapsed(&ToggleCollapse { location: (channel_id, path).into() }, cx)
|
2023-08-23 23:25:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext<Self>) {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
let location = action.location.clone();
|
2023-08-23 23:25:17 +00:00
|
|
|
|
|
2023-09-09 01:47:59 +00:00
|
|
|
|
match self.collapsed_channels.binary_search(&location) {
|
2023-08-19 12:18:53 +00:00
|
|
|
|
Ok(ix) => {
|
|
|
|
|
self.collapsed_channels.remove(ix);
|
|
|
|
|
}
|
|
|
|
|
Err(ix) => {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
self.collapsed_channels.insert(ix, location);
|
2023-08-19 12:18:53 +00:00
|
|
|
|
}
|
|
|
|
|
};
|
2023-08-24 22:00:54 +00:00
|
|
|
|
self.serialize(cx);
|
2023-08-23 23:25:17 +00:00
|
|
|
|
self.update_entries(true, cx);
|
2023-08-19 12:18:53 +00:00
|
|
|
|
cx.notify();
|
2023-08-23 23:25:17 +00:00
|
|
|
|
cx.focus_self();
|
2023-08-19 12:18:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-09-09 01:47:59 +00:00
|
|
|
|
fn is_channel_collapsed(&self, location: &ChannelLocation) -> bool {
|
|
|
|
|
self.collapsed_channels.binary_search(location).is_ok()
|
2023-08-19 12:18:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-04 23:14:01 +00:00
|
|
|
|
fn leave_call(cx: &mut ViewContext<Self>) {
|
|
|
|
|
ActiveCall::global(cx)
|
|
|
|
|
.update(cx, |call, cx| call.hang_up(cx))
|
|
|
|
|
.detach_and_log_err(cx);
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-25 03:00:31 +00:00
|
|
|
|
fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
|
2023-07-26 00:29:09 +00:00
|
|
|
|
if let Some(workspace) = self.workspace.upgrade(cx) {
|
|
|
|
|
workspace.update(cx, |workspace, cx| {
|
|
|
|
|
workspace.toggle_modal(cx, |_, cx| {
|
|
|
|
|
cx.add_view(|cx| {
|
2023-08-14 18:36:49 +00:00
|
|
|
|
let mut finder = ContactFinder::new(self.user_store.clone(), cx);
|
2023-07-26 00:29:09 +00:00
|
|
|
|
finder.set_query(self.filter_editor.read(cx).text(cx), cx);
|
|
|
|
|
finder
|
|
|
|
|
})
|
|
|
|
|
});
|
2023-07-25 03:00:31 +00:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-02 01:20:25 +00:00
|
|
|
|
fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
|
2023-08-10 00:11:52 +00:00
|
|
|
|
self.channel_editing_state = Some(ChannelEditingState::Create {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
location: None,
|
2023-08-10 00:11:52 +00:00
|
|
|
|
pending_name: None,
|
|
|
|
|
});
|
|
|
|
|
self.update_entries(false, cx);
|
|
|
|
|
self.select_channel_editor();
|
2023-08-02 01:20:25 +00:00
|
|
|
|
cx.focus(self.channel_name_editor.as_any());
|
|
|
|
|
cx.notify();
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-10 00:11:52 +00:00
|
|
|
|
fn select_channel_editor(&mut self) {
|
|
|
|
|
self.selection = self.entries.iter().position(|entry| match entry {
|
|
|
|
|
ListEntry::ChannelEditor { .. } => true,
|
|
|
|
|
_ => false,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-02 01:20:25 +00:00
|
|
|
|
fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
|
2023-08-23 23:25:17 +00:00
|
|
|
|
self.collapsed_channels
|
2023-09-09 01:47:59 +00:00
|
|
|
|
.retain(|channel| *channel != action.location);
|
2023-08-09 19:20:48 +00:00
|
|
|
|
self.channel_editing_state = Some(ChannelEditingState::Create {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
location: Some(action.location.to_owned()),
|
2023-08-10 00:11:52 +00:00
|
|
|
|
pending_name: None,
|
2023-08-04 23:14:01 +00:00
|
|
|
|
});
|
2023-08-10 00:11:52 +00:00
|
|
|
|
self.update_entries(false, cx);
|
|
|
|
|
self.select_channel_editor();
|
2023-08-02 01:20:25 +00:00
|
|
|
|
cx.focus(self.channel_name_editor.as_any());
|
|
|
|
|
cx.notify();
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-08 00:14:09 +00:00
|
|
|
|
fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext<Self>) {
|
|
|
|
|
self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext<Self>) {
|
|
|
|
|
self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx);
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-08 19:46:13 +00:00
|
|
|
|
fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
if let Some((channel, _)) = self.selected_channel() {
|
2023-08-08 19:46:13 +00:00
|
|
|
|
self.remove_channel(channel.id, cx)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-09 19:20:48 +00:00
|
|
|
|
fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
if let Some((channel, parent)) = self.selected_channel() {
|
2023-08-09 19:20:48 +00:00
|
|
|
|
self.rename_channel(
|
|
|
|
|
&RenameChannel {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
location: (channel.id, parent.to_owned()).into(),
|
2023-08-09 19:20:48 +00:00
|
|
|
|
},
|
|
|
|
|
cx,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
|
2023-08-10 00:11:52 +00:00
|
|
|
|
let channel_store = self.channel_store.read(cx);
|
2023-09-09 01:47:59 +00:00
|
|
|
|
if !channel_store.is_user_admin(action.location.channel) {
|
2023-08-10 00:11:52 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
2023-09-09 01:47:59 +00:00
|
|
|
|
if let Some(channel) = channel_store.channel_for_id(action.location.channel).cloned() {
|
2023-08-09 19:20:48 +00:00
|
|
|
|
self.channel_editing_state = Some(ChannelEditingState::Rename {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
location: action.location.to_owned(),
|
2023-08-10 00:11:52 +00:00
|
|
|
|
pending_name: None,
|
2023-08-09 19:20:48 +00:00
|
|
|
|
});
|
|
|
|
|
self.channel_name_editor.update(cx, |editor, cx| {
|
|
|
|
|
editor.set_text(channel.name.clone(), cx);
|
|
|
|
|
editor.select_all(&Default::default(), cx);
|
|
|
|
|
});
|
2023-08-10 00:11:52 +00:00
|
|
|
|
cx.focus(self.channel_name_editor.as_any());
|
|
|
|
|
self.update_entries(false, cx);
|
|
|
|
|
self.select_channel_editor();
|
2023-08-09 19:20:48 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-08 19:46:13 +00:00
|
|
|
|
|
2023-09-11 20:44:41 +00:00
|
|
|
|
fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
|
2023-08-24 21:29:04 +00:00
|
|
|
|
if let Some(workspace) = self.workspace.upgrade(cx) {
|
2023-09-15 01:05:44 +00:00
|
|
|
|
ChannelView::deploy(action.channel_id, workspace, cx);
|
2023-08-24 21:29:04 +00:00
|
|
|
|
}
|
2023-08-22 21:18:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-08 19:46:13 +00:00
|
|
|
|
fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
let Some((channel, path)) = self.selected_channel() else {
|
2023-08-08 19:46:13 +00:00
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
2023-09-09 01:47:59 +00:00
|
|
|
|
self.deploy_channel_context_menu(None, &(channel.id, path.to_owned()).into(), cx);
|
2023-08-08 19:46:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-09-09 01:47:59 +00:00
|
|
|
|
fn selected_channel(&self) -> Option<(&Arc<Channel>, &ChannelPath)> {
|
2023-08-08 19:46:13 +00:00
|
|
|
|
self.selection
|
|
|
|
|
.and_then(|ix| self.entries.get(ix))
|
|
|
|
|
.and_then(|entry| match entry {
|
2023-09-09 01:47:59 +00:00
|
|
|
|
ListEntry::Channel { channel, path: parent, .. } => Some((channel, parent)),
|
2023-08-08 19:46:13 +00:00
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-08 00:14:09 +00:00
|
|
|
|
fn show_channel_modal(
|
|
|
|
|
&mut self,
|
|
|
|
|
channel_id: ChannelId,
|
|
|
|
|
mode: channel_modal::Mode,
|
|
|
|
|
cx: &mut ViewContext<Self>,
|
|
|
|
|
) {
|
2023-08-03 19:10:53 +00:00
|
|
|
|
let workspace = self.workspace.clone();
|
|
|
|
|
let user_store = self.user_store.clone();
|
|
|
|
|
let channel_store = self.channel_store.clone();
|
2023-08-03 21:49:01 +00:00
|
|
|
|
let members = self.channel_store.update(cx, |channel_store, cx| {
|
|
|
|
|
channel_store.get_channel_member_details(channel_id, cx)
|
|
|
|
|
});
|
|
|
|
|
|
2023-08-03 19:10:53 +00:00
|
|
|
|
cx.spawn(|_, mut cx| async move {
|
|
|
|
|
let members = members.await?;
|
|
|
|
|
workspace.update(&mut cx, |workspace, cx| {
|
2023-08-03 17:59:09 +00:00
|
|
|
|
workspace.toggle_modal(cx, |_, cx| {
|
|
|
|
|
cx.add_view(|cx| {
|
2023-08-04 21:12:08 +00:00
|
|
|
|
ChannelModal::new(
|
2023-08-03 19:10:53 +00:00
|
|
|
|
user_store.clone(),
|
|
|
|
|
channel_store.clone(),
|
|
|
|
|
channel_id,
|
2023-08-08 00:14:09 +00:00
|
|
|
|
mode,
|
2023-08-03 19:10:53 +00:00
|
|
|
|
members,
|
2023-08-03 18:40:55 +00:00
|
|
|
|
cx,
|
|
|
|
|
)
|
2023-08-03 17:59:09 +00:00
|
|
|
|
})
|
2023-08-03 19:10:53 +00:00
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.detach();
|
2023-08-03 17:59:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-09 19:20:48 +00:00
|
|
|
|
fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
|
2023-08-08 19:46:13 +00:00
|
|
|
|
self.remove_channel(action.channel_id, cx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
2023-08-01 23:06:21 +00:00
|
|
|
|
let channel_store = self.channel_store.clone();
|
|
|
|
|
if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
|
|
|
|
|
let prompt_message = format!(
|
|
|
|
|
"Are you sure you want to remove the channel \"{}\"?",
|
|
|
|
|
channel.name
|
|
|
|
|
);
|
|
|
|
|
let mut answer =
|
|
|
|
|
cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
|
2023-08-09 17:37:22 +00:00
|
|
|
|
let window = cx.window();
|
2023-08-15 23:14:24 +00:00
|
|
|
|
cx.spawn(|this, mut cx| async move {
|
2023-08-01 23:06:21 +00:00
|
|
|
|
if answer.next().await == Some(0) {
|
|
|
|
|
if let Err(e) = channel_store
|
2023-08-02 22:52:56 +00:00
|
|
|
|
.update(&mut cx, |channels, _| channels.remove_channel(channel_id))
|
2023-08-01 23:06:21 +00:00
|
|
|
|
.await
|
|
|
|
|
{
|
2023-08-09 17:37:22 +00:00
|
|
|
|
window.prompt(
|
2023-08-01 23:06:21 +00:00
|
|
|
|
PromptLevel::Info,
|
|
|
|
|
&format!("Failed to remove channel: {}", e),
|
|
|
|
|
&["Ok"],
|
2023-08-09 17:37:22 +00:00
|
|
|
|
&mut cx,
|
2023-08-01 23:06:21 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
2023-08-15 23:14:24 +00:00
|
|
|
|
this.update(&mut cx, |_, cx| cx.focus_self()).ok();
|
2023-08-01 23:06:21 +00:00
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.detach();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-08 19:46:13 +00:00
|
|
|
|
// Should move to the filter editor if clicking on it
|
|
|
|
|
// Should move selection to the channel editor if activating it
|
|
|
|
|
|
2023-07-25 03:00:31 +00:00
|
|
|
|
fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
|
|
|
|
|
let user_store = self.user_store.clone();
|
|
|
|
|
let prompt_message = format!(
|
|
|
|
|
"Are you sure you want to remove \"{}\" from your contacts?",
|
|
|
|
|
github_login
|
|
|
|
|
);
|
|
|
|
|
let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
|
2023-08-09 17:37:22 +00:00
|
|
|
|
let window = cx.window();
|
2023-07-25 03:00:31 +00:00
|
|
|
|
cx.spawn(|_, mut cx| async move {
|
|
|
|
|
if answer.next().await == Some(0) {
|
|
|
|
|
if let Err(e) = user_store
|
|
|
|
|
.update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
|
|
|
|
|
.await
|
|
|
|
|
{
|
2023-08-09 17:37:22 +00:00
|
|
|
|
window.prompt(
|
2023-07-25 03:00:31 +00:00
|
|
|
|
PromptLevel::Info,
|
|
|
|
|
&format!("Failed to remove contact: {}", e),
|
|
|
|
|
&["Ok"],
|
2023-08-09 17:37:22 +00:00
|
|
|
|
&mut cx,
|
2023-07-25 03:00:31 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.detach();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn respond_to_contact_request(
|
|
|
|
|
&mut self,
|
|
|
|
|
user_id: u64,
|
|
|
|
|
accept: bool,
|
|
|
|
|
cx: &mut ViewContext<Self>,
|
|
|
|
|
) {
|
|
|
|
|
self.user_store
|
|
|
|
|
.update(cx, |store, cx| {
|
|
|
|
|
store.respond_to_contact_request(user_id, accept, cx)
|
|
|
|
|
})
|
|
|
|
|
.detach();
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-01 01:00:14 +00:00
|
|
|
|
fn respond_to_channel_invite(
|
|
|
|
|
&mut self,
|
|
|
|
|
channel_id: u64,
|
|
|
|
|
accept: bool,
|
|
|
|
|
cx: &mut ViewContext<Self>,
|
|
|
|
|
) {
|
|
|
|
|
let respond = self.channel_store.update(cx, |store, _| {
|
|
|
|
|
store.respond_to_channel_invite(channel_id, accept)
|
|
|
|
|
});
|
|
|
|
|
cx.foreground().spawn(respond).detach();
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-25 03:00:31 +00:00
|
|
|
|
fn call(
|
|
|
|
|
&mut self,
|
|
|
|
|
recipient_user_id: u64,
|
|
|
|
|
initial_project: Option<ModelHandle<Project>>,
|
|
|
|
|
cx: &mut ViewContext<Self>,
|
|
|
|
|
) {
|
|
|
|
|
ActiveCall::global(cx)
|
|
|
|
|
.update(cx, |call, cx| {
|
|
|
|
|
call.invite(recipient_user_id, initial_project, cx)
|
|
|
|
|
})
|
|
|
|
|
.detach_and_log_err(cx);
|
|
|
|
|
}
|
2023-08-01 01:00:14 +00:00
|
|
|
|
|
2023-09-09 00:06:39 +00:00
|
|
|
|
fn join_channel_call(&self, channel: u64, cx: &mut ViewContext<Self>) {
|
2023-08-01 01:00:14 +00:00
|
|
|
|
ActiveCall::global(cx)
|
|
|
|
|
.update(cx, |call, cx| call.join_channel(channel, cx))
|
|
|
|
|
.detach_and_log_err(cx);
|
|
|
|
|
}
|
2023-09-09 00:06:39 +00:00
|
|
|
|
|
2023-09-11 20:44:41 +00:00
|
|
|
|
fn join_channel_chat(&mut self, channel_id: u64, cx: &mut ViewContext<Self>) {
|
2023-09-09 00:06:39 +00:00
|
|
|
|
if let Some(workspace) = self.workspace.upgrade(cx) {
|
|
|
|
|
cx.app_context().defer(move |cx| {
|
|
|
|
|
workspace.update(cx, |workspace, cx| {
|
|
|
|
|
if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
|
|
|
|
|
panel.update(cx, |panel, cx| {
|
|
|
|
|
panel.select_channel(channel_id, cx).detach_and_log_err(cx);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-19 01:55:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-24 22:00:54 +00:00
|
|
|
|
fn render_tree_branch(
|
|
|
|
|
branch_style: theme::TreeBranch,
|
|
|
|
|
row_style: &TextStyle,
|
|
|
|
|
is_last: bool,
|
|
|
|
|
size: Vector2F,
|
|
|
|
|
font_cache: &FontCache,
|
|
|
|
|
) -> gpui::elements::ConstrainedBox<CollabPanel> {
|
|
|
|
|
let line_height = row_style.line_height(font_cache);
|
|
|
|
|
let cap_height = row_style.cap_height(font_cache);
|
|
|
|
|
let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.;
|
|
|
|
|
|
2023-09-08 22:08:31 +00:00
|
|
|
|
Canvas::new(move |bounds, _, _, cx| {
|
|
|
|
|
cx.paint_layer(None, |cx| {
|
2023-08-24 22:00:54 +00:00
|
|
|
|
let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.);
|
|
|
|
|
let end_x = bounds.max_x();
|
|
|
|
|
let start_y = bounds.min_y();
|
|
|
|
|
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
|
|
|
|
|
|
2023-09-08 22:08:31 +00:00
|
|
|
|
cx.scene().push_quad(gpui::Quad {
|
2023-08-24 22:00:54 +00:00
|
|
|
|
bounds: RectF::from_points(
|
|
|
|
|
vec2f(start_x, start_y),
|
|
|
|
|
vec2f(
|
|
|
|
|
start_x + branch_style.width,
|
|
|
|
|
if is_last { end_y } else { bounds.max_y() },
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
background: Some(branch_style.color),
|
|
|
|
|
border: gpui::Border::default(),
|
|
|
|
|
corner_radii: (0.).into(),
|
|
|
|
|
});
|
2023-09-08 22:08:31 +00:00
|
|
|
|
cx.scene().push_quad(gpui::Quad {
|
2023-08-24 22:00:54 +00:00
|
|
|
|
bounds: RectF::from_points(
|
|
|
|
|
vec2f(start_x, end_y),
|
|
|
|
|
vec2f(end_x, end_y + branch_style.width),
|
|
|
|
|
),
|
|
|
|
|
background: Some(branch_style.color),
|
|
|
|
|
border: gpui::Border::default(),
|
|
|
|
|
corner_radii: (0.).into(),
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.constrained()
|
|
|
|
|
.with_width(size.x())
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-24 21:39:16 +00:00
|
|
|
|
impl View for CollabPanel {
|
2023-07-19 01:55:54 +00:00
|
|
|
|
fn ui_name() -> &'static str {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
"CollabPanel"
|
2023-07-19 01:55:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
|
|
|
|
if !self.has_focus {
|
|
|
|
|
self.has_focus = true;
|
2023-08-04 23:14:01 +00:00
|
|
|
|
if !self.context_menu.is_focused(cx) {
|
2023-08-10 00:11:52 +00:00
|
|
|
|
if let Some(editing_state) = &self.channel_editing_state {
|
|
|
|
|
if editing_state.pending_name().is_none() {
|
|
|
|
|
cx.focus(&self.channel_name_editor);
|
|
|
|
|
} else {
|
|
|
|
|
cx.focus(&self.filter_editor);
|
|
|
|
|
}
|
2023-08-04 23:14:01 +00:00
|
|
|
|
} else {
|
|
|
|
|
cx.focus(&self.filter_editor);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-19 01:55:54 +00:00
|
|
|
|
cx.emit(Event::Focus);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
|
|
|
|
self.has_focus = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
|
2023-07-26 00:29:09 +00:00
|
|
|
|
let theme = &theme::current(cx).collab_panel;
|
2023-07-19 01:55:54 +00:00
|
|
|
|
|
2023-08-07 23:27:47 +00:00
|
|
|
|
if self.user_store.read(cx).current_user().is_none() {
|
|
|
|
|
enum LogInButton {}
|
|
|
|
|
|
|
|
|
|
return Flex::column()
|
|
|
|
|
.with_child(
|
2023-08-15 10:25:45 +00:00
|
|
|
|
MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
|
2023-08-07 23:27:47 +00:00
|
|
|
|
let button = theme.log_in_button.style_for(state);
|
|
|
|
|
Label::new("Sign in to collaborate", button.text.clone())
|
2023-08-16 17:56:11 +00:00
|
|
|
|
.aligned()
|
|
|
|
|
.left()
|
2023-08-07 23:27:47 +00:00
|
|
|
|
.contained()
|
|
|
|
|
.with_style(button.container)
|
|
|
|
|
})
|
|
|
|
|
.on_click(MouseButton::Left, |_, this, cx| {
|
|
|
|
|
let client = this.client.clone();
|
2023-08-08 00:14:09 +00:00
|
|
|
|
cx.spawn(|_, cx| async move {
|
2023-08-07 23:45:13 +00:00
|
|
|
|
client.authenticate_and_connect(true, &cx).await.log_err();
|
2023-08-07 23:27:47 +00:00
|
|
|
|
})
|
|
|
|
|
.detach();
|
|
|
|
|
})
|
|
|
|
|
.with_cursor_style(CursorStyle::PointingHand),
|
|
|
|
|
)
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(theme.container)
|
|
|
|
|
.into_any();
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-26 18:11:48 +00:00
|
|
|
|
enum PanelFocus {}
|
2023-08-15 10:25:45 +00:00
|
|
|
|
MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
|
2023-07-26 18:11:48 +00:00
|
|
|
|
Stack::new()
|
|
|
|
|
.with_child(
|
|
|
|
|
Flex::column()
|
|
|
|
|
.with_child(
|
2023-08-30 21:35:02 +00:00
|
|
|
|
Flex::row().with_child(
|
|
|
|
|
ChildView::new(&self.filter_editor, cx)
|
|
|
|
|
.contained()
|
|
|
|
|
.with_style(theme.user_query_editor.container)
|
|
|
|
|
.flex(1.0, true),
|
|
|
|
|
),
|
2023-07-26 18:11:48 +00:00
|
|
|
|
)
|
2023-08-30 21:35:02 +00:00
|
|
|
|
.with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
|
2023-07-26 18:11:48 +00:00
|
|
|
|
.contained()
|
|
|
|
|
.with_style(theme.container)
|
|
|
|
|
.into_any(),
|
|
|
|
|
)
|
2023-08-08 19:46:13 +00:00
|
|
|
|
.with_children(
|
|
|
|
|
(!self.context_menu_on_selected)
|
|
|
|
|
.then(|| ChildView::new(&self.context_menu, cx)),
|
|
|
|
|
)
|
2023-07-26 18:11:48 +00:00
|
|
|
|
.into_any()
|
|
|
|
|
})
|
2023-07-27 00:20:43 +00:00
|
|
|
|
.on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
|
2023-08-18 21:53:30 +00:00
|
|
|
|
.into_any_named("collab panel")
|
2023-07-19 01:55:54 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-24 21:39:16 +00:00
|
|
|
|
impl Panel for CollabPanel {
|
2023-07-19 01:55:54 +00:00
|
|
|
|
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
|
2023-09-08 20:28:19 +00:00
|
|
|
|
settings::get::<CollaborationPanelSettings>(cx).dock
|
2023-07-19 01:55:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn position_is_valid(&self, position: DockPosition) -> bool {
|
|
|
|
|
matches!(position, DockPosition::Left | DockPosition::Right)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
2023-08-07 23:27:47 +00:00
|
|
|
|
settings::update_settings_file::<CollaborationPanelSettings>(
|
2023-07-19 01:55:54 +00:00
|
|
|
|
self.fs.clone(),
|
|
|
|
|
cx,
|
2023-09-08 20:28:19 +00:00
|
|
|
|
move |settings| settings.dock = Some(position),
|
2023-07-19 01:55:54 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn size(&self, cx: &gpui::WindowContext) -> f32 {
|
|
|
|
|
self.width
|
2023-08-07 23:27:47 +00:00
|
|
|
|
.unwrap_or_else(|| settings::get::<CollaborationPanelSettings>(cx).default_width)
|
2023-07-19 01:55:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-17 02:47:54 +00:00
|
|
|
|
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
|
|
|
|
|
self.width = size;
|
2023-07-19 01:55:54 +00:00
|
|
|
|
self.serialize(cx);
|
|
|
|
|
cx.notify();
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-07 23:27:47 +00:00
|
|
|
|
fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
|
|
|
|
|
settings::get::<CollaborationPanelSettings>(cx)
|
|
|
|
|
.button
|
2023-09-08 20:28:19 +00:00
|
|
|
|
.then(|| "icons/user_group_16.svg")
|
2023-07-19 01:55:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
|
2023-08-18 21:53:30 +00:00
|
|
|
|
(
|
|
|
|
|
"Collaboration Panel".to_string(),
|
|
|
|
|
Some(Box::new(ToggleFocus)),
|
|
|
|
|
)
|
2023-07-19 01:55:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn should_change_position_on_event(event: &Self::Event) -> bool {
|
|
|
|
|
matches!(event, Event::DockPositionChanged)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
|
|
|
|
|
self.has_focus
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_focus_event(event: &Self::Event) -> bool {
|
|
|
|
|
matches!(event, Event::Focus)
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-25 03:00:31 +00:00
|
|
|
|
|
2023-08-02 01:20:25 +00:00
|
|
|
|
impl PartialEq for ListEntry {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
fn eq(&self, other: &Self) -> bool {
|
|
|
|
|
match self {
|
2023-09-09 00:06:39 +00:00
|
|
|
|
ListEntry::Header(section_1) => {
|
|
|
|
|
if let ListEntry::Header(section_2) = other {
|
|
|
|
|
return section_1 == section_2;
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::CallParticipant { user: user_1, .. } => {
|
|
|
|
|
if let ListEntry::CallParticipant { user: user_2, .. } = other {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
return user_1.id == user_2.id;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::ParticipantProject {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
project_id: project_id_1,
|
|
|
|
|
..
|
|
|
|
|
} => {
|
2023-08-02 01:20:25 +00:00
|
|
|
|
if let ListEntry::ParticipantProject {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
project_id: project_id_2,
|
|
|
|
|
..
|
|
|
|
|
} = other
|
|
|
|
|
{
|
|
|
|
|
return project_id_1 == project_id_2;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::ParticipantScreen {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
peer_id: peer_id_1, ..
|
|
|
|
|
} => {
|
2023-08-02 01:20:25 +00:00
|
|
|
|
if let ListEntry::ParticipantScreen {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
peer_id: peer_id_2, ..
|
|
|
|
|
} = other
|
|
|
|
|
{
|
|
|
|
|
return peer_id_1 == peer_id_2;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-14 23:27:35 +00:00
|
|
|
|
ListEntry::Channel {
|
|
|
|
|
channel: channel_1,
|
|
|
|
|
depth: depth_1,
|
2023-09-09 01:47:59 +00:00
|
|
|
|
path: parent_1,
|
2023-08-14 23:27:35 +00:00
|
|
|
|
} => {
|
|
|
|
|
if let ListEntry::Channel {
|
|
|
|
|
channel: channel_2,
|
|
|
|
|
depth: depth_2,
|
2023-09-09 01:47:59 +00:00
|
|
|
|
path: parent_2,
|
2023-08-14 23:27:35 +00:00
|
|
|
|
} = other
|
|
|
|
|
{
|
2023-09-09 01:47:59 +00:00
|
|
|
|
return channel_1.id == channel_2.id && depth_1 == depth_2 && parent_1 == parent_2;
|
2023-08-01 01:00:14 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-24 22:00:54 +00:00
|
|
|
|
ListEntry::ChannelNotes { channel_id } => {
|
|
|
|
|
if let ListEntry::ChannelNotes {
|
|
|
|
|
channel_id: other_id,
|
|
|
|
|
} = other
|
|
|
|
|
{
|
|
|
|
|
return channel_id == other_id;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::ChannelInvite(channel_1) => {
|
|
|
|
|
if let ListEntry::ChannelInvite(channel_2) = other {
|
2023-08-01 01:00:14 +00:00
|
|
|
|
return channel_1.id == channel_2.id;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::IncomingRequest(user_1) => {
|
|
|
|
|
if let ListEntry::IncomingRequest(user_2) = other {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
return user_1.id == user_2.id;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::OutgoingRequest(user_1) => {
|
|
|
|
|
if let ListEntry::OutgoingRequest(user_2) = other {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
return user_1.id == user_2.id;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::Contact {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
contact: contact_1, ..
|
|
|
|
|
} => {
|
2023-08-02 01:20:25 +00:00
|
|
|
|
if let ListEntry::Contact {
|
2023-07-25 03:00:31 +00:00
|
|
|
|
contact: contact_2, ..
|
|
|
|
|
} = other
|
|
|
|
|
{
|
|
|
|
|
return contact_1.user.id == contact_2.user.id;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-02 01:20:25 +00:00
|
|
|
|
ListEntry::ChannelEditor { depth } => {
|
|
|
|
|
if let ListEntry::ChannelEditor { depth: other_depth } = other {
|
|
|
|
|
return depth == other_depth;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-14 17:23:50 +00:00
|
|
|
|
ListEntry::ContactPlaceholder => {
|
|
|
|
|
if let ListEntry::ContactPlaceholder = other {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
|
|
|
|
|
Svg::new(svg_path)
|
|
|
|
|
.with_color(style.color)
|
|
|
|
|
.constrained()
|
|
|
|
|
.with_width(style.icon_width)
|
|
|
|
|
.aligned()
|
|
|
|
|
.constrained()
|
|
|
|
|
.with_width(style.button_width)
|
|
|
|
|
.with_height(style.button_width)
|
2023-07-26 00:29:09 +00:00
|
|
|
|
.contained()
|
|
|
|
|
.with_style(style.container)
|
2023-07-25 03:00:31 +00:00
|
|
|
|
}
|
2023-09-09 01:47:59 +00:00
|
|
|
|
|
|
|
|
|
/// Hash a channel path to a u64, for use as a mouse id
|
|
|
|
|
/// Based on the Fowler–Noll–Vo hash:
|
|
|
|
|
/// https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
|
|
|
|
|
fn id(path: &[ChannelId]) -> u64 {
|
|
|
|
|
// I probably should have done this, but I didn't
|
|
|
|
|
// let hasher = DefaultHasher::new();
|
|
|
|
|
// let path = path.hash(&mut hasher);
|
|
|
|
|
// let x = hasher.finish();
|
|
|
|
|
|
|
|
|
|
const OFFSET: u64 = 14695981039346656037;
|
|
|
|
|
const PRIME: u64 = 1099511628211;
|
|
|
|
|
|
|
|
|
|
let mut hash = OFFSET;
|
|
|
|
|
for id in path.iter() {
|
|
|
|
|
for id in id.to_ne_bytes() {
|
|
|
|
|
hash = hash ^ (id as u64);
|
|
|
|
|
hash = (hash as u128 * PRIME as u128) as u64;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hash
|
|
|
|
|
}
|