Add move, link, and unlink operations

This commit is contained in:
Mikayla 2023-09-09 13:24:04 -07:00
parent 77cdbdb12a
commit 439f627d9a
No known key found for this signature in database
3 changed files with 275 additions and 69 deletions

View file

@ -146,7 +146,7 @@ impl ChannelStore {
}) })
} }
pub fn channel_at_index(&self, ix: usize) -> Option<(&Arc<Channel>, &Arc<[ChannelId]>)> { pub fn channel_at_index(&self, ix: usize) -> Option<(&Arc<Channel>, &ChannelPath)> {
let path = self.channel_index.get(ix)?; let path = self.channel_index.get(ix)?;
let id = path.last().unwrap(); let id = path.last().unwrap();
let channel = self.channel_for_id(*id).unwrap(); let channel = self.channel_for_id(*id).unwrap();

View file

@ -1,13 +1,38 @@
use std::sync::Arc; use std::{sync::Arc, ops::Deref};
use collections::HashMap; use collections::HashMap;
use rpc::proto; use rpc::proto;
use serde_derive::{Serialize, Deserialize};
use crate::{ChannelId, Channel}; use crate::{ChannelId, Channel};
pub type ChannelPath = Arc<[ChannelId]>;
pub type ChannelsById = HashMap<ChannelId, Arc<Channel>>; pub type ChannelsById = HashMap<ChannelId, Arc<Channel>>;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
pub struct ChannelPath(Arc<[ChannelId]>);
impl Deref for ChannelPath {
type Target = [ChannelId];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ChannelPath {
pub fn parent_id(&self) -> Option<ChannelId> {
self.0.len().checked_sub(2).map(|i| {
self.0[i]
})
}
}
impl Default for ChannelPath {
fn default() -> Self {
ChannelPath(Arc::from([]))
}
}
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct ChannelIndex { pub struct ChannelIndex {
paths: Vec<ChannelPath>, paths: Vec<ChannelPath>,
@ -99,7 +124,7 @@ impl<'a> ChannelPathsUpsertGuard<'a> {
if path.ends_with(&[parent_id]) { if path.ends_with(&[parent_id]) {
let mut new_path = path.to_vec(); let mut new_path = path.to_vec();
new_path.push(channel_id); new_path.push(channel_id);
self.paths.insert(ix + 1, new_path.into()); self.paths.insert(ix + 1, ChannelPath(new_path.into()));
ix += 1; ix += 1;
} }
ix += 1; ix += 1;
@ -107,7 +132,7 @@ impl<'a> ChannelPathsUpsertGuard<'a> {
} }
fn insert_root(&mut self, channel_id: ChannelId) { fn insert_root(&mut self, channel_id: ChannelId) {
self.paths.push(Arc::from([channel_id])); self.paths.push(ChannelPath(Arc::from([channel_id])));
} }
} }

View file

@ -11,6 +11,7 @@ use anyhow::Result;
use call::ActiveCall; use call::ActiveCall;
use channel::{Channel, ChannelEvent, ChannelId, ChannelStore, ChannelPath}; use channel::{Channel, ChannelEvent, ChannelId, ChannelStore, ChannelPath};
use channel_modal::ChannelModal; use channel_modal::ChannelModal;
use channel::{Channel, ChannelEvent, ChannelId, ChannelPath, ChannelStore};
use client::{proto::PeerId, Client, Contact, User, UserStore}; use client::{proto::PeerId, Client, Contact, User, UserStore};
use contact_finder::ContactFinder; use contact_finder::ContactFinder;
use context_menu::{ContextMenu, ContextMenuItem}; use context_menu::{ContextMenu, ContextMenuItem};
@ -40,7 +41,7 @@ use menu::{Confirm, SelectNext, SelectPrev};
use project::{Fs, Project}; use project::{Fs, Project};
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use settings::SettingsStore; use settings::SettingsStore;
use std::{borrow::Cow, mem, sync::Arc, hash::Hash}; use std::{borrow::Cow, hash::Hash, mem, sync::Arc};
use theme::{components::ComponentExt, IconButton}; use theme::{components::ComponentExt, IconButton};
use util::{iife, ResultExt, TryFutureExt}; use util::{iife, ResultExt, TryFutureExt};
use workspace::{ use workspace::{
@ -95,18 +96,25 @@ struct OpenChannelBuffer {
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct CopyChannel { struct LinkChannel {
channel_id: ChannelId, channel_id: ChannelId,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct CutChannel { struct MoveChannel {
channel_id: ChannelId, channel_id: ChannelId,
parent_id: Option<ChannelId>,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct PasteChannel { struct PutChannel {
to: ChannelId,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct UnlinkChannel {
channel_id: ChannelId, channel_id: ChannelId,
parent_id: ChannelId,
} }
actions!( actions!(
@ -132,9 +140,10 @@ impl_actions!(
OpenChannelNotes, OpenChannelNotes,
JoinChannelCall, JoinChannelCall,
OpenChannelBuffer, OpenChannelBuffer,
CopyChannel, LinkChannel,
CutChannel, MoveChannel,
PasteChannel, PutChannel,
UnlinkChannel
] ]
); );
@ -143,18 +152,24 @@ const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ChannelLocation<'a> { pub struct ChannelLocation<'a> {
channel: ChannelId, channel: ChannelId,
parent: Cow<'a, ChannelPath>, path: Cow<'a, ChannelPath>,
} }
impl From<(ChannelId, ChannelPath)> for ChannelLocation<'static> { impl From<(ChannelId, ChannelPath)> for ChannelLocation<'static> {
fn from(value: (ChannelId, ChannelPath)) -> Self { fn from(value: (ChannelId, ChannelPath)) -> Self {
ChannelLocation { channel: value.0, parent: Cow::Owned(value.1) } ChannelLocation {
channel: value.0,
path: Cow::Owned(value.1),
}
} }
} }
impl<'a> From<(ChannelId, &'a ChannelPath)> for ChannelLocation<'a> { impl<'a> From<(ChannelId, &'a ChannelPath)> for ChannelLocation<'a> {
fn from(value: (ChannelId, &'a ChannelPath)) -> Self { fn from(value: (ChannelId, &'a ChannelPath)) -> Self {
ChannelLocation { channel: value.0, parent: Cow::Borrowed(value.1) } ChannelLocation {
channel: value.0,
path: Cow::Borrowed(value.1),
}
} }
} }
@ -182,25 +197,54 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(CollabPanel::open_channel_notes); cx.add_action(CollabPanel::open_channel_notes);
cx.add_action(CollabPanel::open_channel_buffer); cx.add_action(CollabPanel::open_channel_buffer);
cx.add_action(|panel: &mut CollabPanel, action: &CopyChannel, _: &mut ViewContext<CollabPanel>| { cx.add_action(
panel.copy = Some(ChannelCopy::Copy(action.channel_id)); |panel: &mut CollabPanel, action: &LinkChannel, _: &mut ViewContext<CollabPanel>| {
}); panel.copy = Some(ChannelCopy::Link(action.channel_id));
},
);
cx.add_action(|panel: &mut CollabPanel, action: &CutChannel, _: &mut ViewContext<CollabPanel>| { cx.add_action(
// panel.copy = Some(ChannelCopy::Cut(action.channel_id)); |panel: &mut CollabPanel, action: &MoveChannel, _: &mut ViewContext<CollabPanel>| {
panel.copy = Some(ChannelCopy::Move {
channel_id: action.channel_id,
parent_id: action.parent_id,
}); });
},
);
cx.add_action(|panel: &mut CollabPanel, action: &PasteChannel, cx: &mut ViewContext<CollabPanel>| { cx.add_action(
if let Some(copy) = &panel.copy { |panel: &mut CollabPanel, action: &PutChannel, cx: &mut ViewContext<CollabPanel>| {
if let Some(copy) = panel.copy.take() {
match copy { match copy {
ChannelCopy::Cut {..} => todo!(), ChannelCopy::Move {
ChannelCopy::Copy(channel) => panel.channel_store.update(cx, |channel_store, cx| { channel_id,
channel_store.move_channel(*channel, None, Some(action.channel_id), cx).detach_and_log_err(cx) parent_id,
} => panel.channel_store.update(cx, |channel_store, cx| {
channel_store
.move_channel(channel_id, parent_id, Some(action.to), cx)
.detach_and_log_err(cx)
}), }),
ChannelCopy::Link(channel) => {
panel.channel_store.update(cx, |channel_store, cx| {
channel_store
.move_channel(channel, None, Some(action.to), cx)
.detach_and_log_err(cx)
})
} }
} }
}); }
},
);
cx.add_action(
|panel: &mut CollabPanel, action: &UnlinkChannel, cx: &mut ViewContext<CollabPanel>| {
panel.channel_store.update(cx, |channel_store, cx| {
channel_store
.move_channel(action.channel_id, Some(action.parent_id), None, cx)
.detach_and_log_err(cx)
})
},
);
} }
#[derive(Debug)] #[derive(Debug)]
@ -224,12 +268,22 @@ impl ChannelEditingState {
} }
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ChannelCopy { enum ChannelCopy {
Cut { Move {
channel_id: u64, channel_id: u64,
parent_id: Option<u64>, parent_id: Option<u64>,
}, },
Copy(u64), Link(u64),
}
impl ChannelCopy {
fn channel_id(&self) -> u64 {
match self {
ChannelCopy::Move { channel_id, .. } => *channel_id,
ChannelCopy::Link(channel_id) => *channel_id,
}
}
} }
pub struct CollabPanel { pub struct CollabPanel {
@ -304,7 +358,7 @@ enum ListEntry {
Channel { Channel {
channel: Arc<Channel>, channel: Arc<Channel>,
depth: usize, depth: usize,
path: Arc<[ChannelId]>, path: ChannelPath,
}, },
ChannelNotes { ChannelNotes {
channel_id: ChannelId, channel_id: ChannelId,
@ -422,7 +476,11 @@ impl CollabPanel {
cx, cx,
) )
} }
ListEntry::Channel { channel, depth, path } => { ListEntry::Channel {
channel,
depth,
path,
} => {
let channel_row = this.render_channel( let channel_row = this.render_channel(
&*channel, &*channel,
*depth, *depth,
@ -583,7 +641,13 @@ impl CollabPanel {
.log_err() .log_err()
.flatten() .flatten()
{ {
Some(serde_json::from_str::<SerializedCollabPanel>(&panel)?) match serde_json::from_str::<SerializedCollabPanel>(&panel) {
Ok(panel) => Some(panel),
Err(err) => {
log::error!("Failed to deserialize collaboration panel: {}", err);
None
}
}
} else { } else {
None None
}; };
@ -773,20 +837,13 @@ impl CollabPanel {
executor.clone(), executor.clone(),
)); ));
if let Some(state) = &self.channel_editing_state { if let Some(state) = &self.channel_editing_state {
if matches!( if matches!(state, ChannelEditingState::Create { location: None, .. }) {
state,
ChannelEditingState::Create {
location: None,
..
}
) {
self.entries.push(ListEntry::ChannelEditor { depth: 0 }); self.entries.push(ListEntry::ChannelEditor { depth: 0 });
} }
} }
let mut collapse_depth = None; let mut collapse_depth = None;
for mat in matches { for mat in matches {
let (channel, path) = let (channel, path) = channel_store.channel_at_index(mat.candidate_id).unwrap();
channel_store.channel_at_index(mat.candidate_id).unwrap();
let depth = path.len() - 1; let depth = path.len() - 1;
let location: ChannelLocation<'_> = (channel.id, path).into(); let location: ChannelLocation<'_> = (channel.id, path).into();
@ -805,9 +862,10 @@ impl CollabPanel {
} }
match &self.channel_editing_state { match &self.channel_editing_state {
Some(ChannelEditingState::Create { location: parent_id, .. }) Some(ChannelEditingState::Create {
if *parent_id == Some(location) => location: parent_id,
{ ..
}) if *parent_id == Some(location) => {
self.entries.push(ListEntry::Channel { self.entries.push(ListEntry::Channel {
channel: channel.clone(), channel: channel.clone(),
depth, depth,
@ -817,7 +875,8 @@ impl CollabPanel {
.push(ListEntry::ChannelEditor { depth: depth + 1 }); .push(ListEntry::ChannelEditor { depth: depth + 1 });
} }
Some(ChannelEditingState::Rename { location, .. }) Some(ChannelEditingState::Rename { location, .. })
if location.channel == channel.id && location.parent == Cow::Borrowed(path) => if location.channel == channel.id
&& location.path == Cow::Borrowed(path) =>
{ {
self.entries.push(ListEntry::ChannelEditor { depth }); self.entries.push(ListEntry::ChannelEditor { depth });
} }
@ -825,7 +884,7 @@ impl CollabPanel {
self.entries.push(ListEntry::Channel { self.entries.push(ListEntry::Channel {
channel: channel.clone(), channel: channel.clone(),
depth, depth,
path: path.clone() path: path.clone(),
}); });
} }
} }
@ -1638,7 +1697,7 @@ impl CollabPanel {
let disclosed = { let disclosed = {
let location = ChannelLocation { let location = ChannelLocation {
channel: channel_id, channel: channel_id,
parent: Cow::Borrowed(&path), path: Cow::Borrowed(&path),
}; };
has_children.then(|| !self.collapsed_channels.binary_search(&location).is_ok()) has_children.then(|| !self.collapsed_channels.binary_search(&location).is_ok())
}; };
@ -1725,7 +1784,12 @@ impl CollabPanel {
) )
.align_children_center() .align_children_center()
.styleable_component() .styleable_component()
.disclosable(disclosed, Box::new(ToggleCollapse { location: (channel_id, path.clone()).into() })) .disclosable(
disclosed,
Box::new(ToggleCollapse {
location: (channel_id, path.clone()).into(),
}),
)
.with_id(id(&path) as usize) .with_id(id(&path) as usize)
.with_style(theme.disclosure.clone()) .with_style(theme.disclosure.clone())
.element() .element()
@ -1742,7 +1806,11 @@ impl CollabPanel {
this.join_channel_chat(channel_id, cx); this.join_channel_chat(channel_id, cx);
}) })
.on_click(MouseButton::Right, move |e, this, cx| { .on_click(MouseButton::Right, move |e, this, cx| {
this.deploy_channel_context_menu(Some(e.position), &(channel_id, path.clone()).into(), cx); this.deploy_channel_context_menu(
Some(e.position),
&(channel_id, path.clone()).into(),
cx,
);
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.into_any() .into_any()
@ -1994,6 +2062,16 @@ impl CollabPanel {
) { ) {
self.context_menu_on_selected = position.is_none(); self.context_menu_on_selected = position.is_none();
let copy_channel = self
.copy
.as_ref()
.and_then(|copy| {
self.channel_store
.read(cx)
.channel_for_id(copy.channel_id())
})
.map(|channel| channel.name.clone());
self.context_menu.update(cx, |context_menu, cx| { self.context_menu.update(cx, |context_menu, cx| {
context_menu.set_position_mode(if self.context_menu_on_selected { context_menu.set_position_mode(if self.context_menu_on_selected {
OverlayPositionMode::Local OverlayPositionMode::Local
@ -2008,22 +2086,96 @@ impl CollabPanel {
}; };
let mut items = vec![ let mut items = vec![
ContextMenuItem::action(expand_action_name, ToggleCollapse { location: location.clone() }), ContextMenuItem::action(
ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id: location.channel }), expand_action_name,
ToggleCollapse {
location: location.clone(),
},
),
ContextMenuItem::action(
"Open Notes",
OpenChannelBuffer {
channel_id: location.channel,
},
),
]; ];
if self.channel_store.read(cx).is_user_admin(location.channel) { if self.channel_store.read(cx).is_user_admin(location.channel) {
let parent_id = location.path.parent_id();
items.extend([ items.extend([
ContextMenuItem::Separator, ContextMenuItem::Separator,
ContextMenuItem::action("New Subchannel", NewChannel { location: location.clone() }), ContextMenuItem::action(
ContextMenuItem::action("Rename", RenameChannel { location: location.clone() }), "New Subchannel",
ContextMenuItem::action("Copy", CopyChannel { channel_id: location.channel }), NewChannel {
ContextMenuItem::action("Paste", PasteChannel { channel_id: location.channel }), location: location.clone(),
},
),
ContextMenuItem::action(
"Rename",
RenameChannel {
location: location.clone(),
},
),
ContextMenuItem::Separator, ContextMenuItem::Separator,
ContextMenuItem::action("Invite Members", InviteMembers { channel_id: location.channel }), ]);
ContextMenuItem::action("Manage Members", ManageMembers { channel_id: location.channel }),
if let Some(parent) = parent_id {
items.push(ContextMenuItem::action(
"Unlink from parent",
UnlinkChannel {
channel_id: location.channel,
parent_id: parent,
},
))
}
items.extend([
ContextMenuItem::action(
"Link to new parent",
LinkChannel {
channel_id: location.channel,
},
),
ContextMenuItem::action(
"Move",
MoveChannel {
channel_id: location.channel,
parent_id,
},
),
]);
if let Some(copy_channel) = copy_channel {
items.push(ContextMenuItem::action(
format!("Put '#{}'", copy_channel),
PutChannel {
to: location.channel,
},
));
}
items.extend([
ContextMenuItem::Separator, ContextMenuItem::Separator,
ContextMenuItem::action("Delete", RemoveChannel { channel_id: location.channel }), ContextMenuItem::action(
"Invite Members",
InviteMembers {
channel_id: location.channel,
},
),
ContextMenuItem::action(
"Manage Members",
ManageMembers {
channel_id: location.channel,
},
),
ContextMenuItem::Separator,
ContextMenuItem::action(
"Delete",
RemoveChannel {
channel_id: location.channel,
},
),
]); ]);
} }
@ -2162,7 +2314,11 @@ impl CollabPanel {
self.channel_store self.channel_store
.update(cx, |channel_store, cx| { .update(cx, |channel_store, cx| {
channel_store.create_channel(&channel_name, location.as_ref().map(|location| location.channel), cx) channel_store.create_channel(
&channel_name,
location.as_ref().map(|location| location.channel),
cx,
)
}) })
.detach(); .detach();
cx.notify(); cx.notify();
@ -2206,7 +2362,10 @@ impl CollabPanel {
_: &CollapseSelectedChannel, _: &CollapseSelectedChannel,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
let Some((channel_id, path)) = self.selected_channel().map(|(channel, parent)| (channel.id, parent)) else { let Some((channel_id, path)) = self
.selected_channel()
.map(|(channel, parent)| (channel.id, parent))
else {
return; return;
}; };
@ -2216,11 +2375,19 @@ impl CollabPanel {
return; return;
} }
self.toggle_channel_collapsed(&ToggleCollapse { location: (channel_id, path).into() }, cx) self.toggle_channel_collapsed(
&ToggleCollapse {
location: (channel_id, path).into(),
},
cx,
)
} }
fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) { fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
let Some((channel_id, path)) = self.selected_channel().map(|(channel, parent)| (channel.id, parent)) else { let Some((channel_id, path)) = self
.selected_channel()
.map(|(channel, parent)| (channel.id, parent))
else {
return; return;
}; };
@ -2230,7 +2397,12 @@ impl CollabPanel {
return; return;
} }
self.toggle_channel_collapsed(&ToggleCollapse { location: (channel_id, path).into() }, cx) self.toggle_channel_collapsed(
&ToggleCollapse {
location: (channel_id, path).into(),
},
cx,
)
} }
fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext<Self>) { fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext<Self>) {
@ -2335,7 +2507,10 @@ impl CollabPanel {
if !channel_store.is_user_admin(action.location.channel) { if !channel_store.is_user_admin(action.location.channel) {
return; return;
} }
if let Some(channel) = channel_store.channel_for_id(action.location.channel).cloned() { if let Some(channel) = channel_store
.channel_for_id(action.location.channel)
.cloned()
{
self.channel_editing_state = Some(ChannelEditingState::Rename { self.channel_editing_state = Some(ChannelEditingState::Rename {
location: action.location.to_owned(), location: action.location.to_owned(),
pending_name: None, pending_name: None,
@ -2368,7 +2543,11 @@ impl CollabPanel {
self.selection self.selection
.and_then(|ix| self.entries.get(ix)) .and_then(|ix| self.entries.get(ix))
.and_then(|entry| match entry { .and_then(|entry| match entry {
ListEntry::Channel { channel, path: parent, .. } => Some((channel, parent)), ListEntry::Channel {
channel,
path: parent,
..
} => Some((channel, parent)),
_ => None, _ => None,
}) })
} }
@ -2759,7 +2938,9 @@ impl PartialEq for ListEntry {
path: parent_2, path: parent_2,
} = other } = other
{ {
return channel_1.id == channel_2.id && depth_1 == depth_2 && parent_1 == parent_2; return channel_1.id == channel_2.id
&& depth_1 == depth_2
&& parent_1 == parent_2;
} }
} }
ListEntry::ChannelNotes { channel_id } => { ListEntry::ChannelNotes { channel_id } => {