Add hover styles to channels matching the current selection

Fix chat desync from moving / linking channels
This commit is contained in:
Mikayla 2023-09-19 17:48:43 -07:00
parent d5f0ce0e20
commit ac65e7590c
No known key found for this signature in database
8 changed files with 284 additions and 94 deletions

View file

@ -585,6 +585,14 @@
"space": "menu::Confirm" "space": "menu::Confirm"
} }
}, },
{
"context": "CollabPanel > Editor",
"bindings": {
"cmd-c": "collab_panel::StartLinkChannel",
"cmd-x": "collab_panel::StartMoveChannel",
"cmd-v": "collab_panel::MoveOrLinkToSelected"
}
},
{ {
"context": "ChannelModal", "context": "ChannelModal",
"bindings": { "bindings": {

View file

@ -146,17 +146,26 @@ impl ChannelStore {
}) })
} }
/// Returns the number of unique channels in the store
pub fn channel_count(&self) -> usize { pub fn channel_count(&self) -> usize {
self.channel_index.len() self.channel_index.by_id().len()
} }
/// Returns the index of a channel ID in the list of unique channels
pub fn index_of_channel(&self, channel_id: ChannelId) -> Option<usize> { pub fn index_of_channel(&self, channel_id: ChannelId) -> Option<usize> {
self.channel_index self.channel_index
.iter() .by_id()
.position(|path| path.ends_with(&[channel_id])) .keys()
.position(|id| *id == channel_id)
} }
pub fn channels(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> { /// Returns an iterator over all unique channels
pub fn channels(&self) -> impl '_ + Iterator<Item = &Arc<Channel>> {
self.channel_index.by_id().values()
}
/// Iterate over all entries in the channel DAG
pub fn channel_dag_entries(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> {
self.channel_index.iter().map(move |path| { self.channel_index.iter().map(move |path| {
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();
@ -164,7 +173,7 @@ impl ChannelStore {
}) })
} }
pub fn channel_at_index(&self, ix: usize) -> Option<(&Arc<Channel>, &ChannelPath)> { pub fn channel_dag_entry_at(&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();
@ -172,6 +181,10 @@ impl ChannelStore {
Some((channel, path)) Some((channel, path))
} }
pub fn channel_at(&self, ix: usize) -> Option<&Arc<Channel>> {
self.channel_index.by_id().values().nth(ix)
}
pub fn channel_invitations(&self) -> &[Arc<Channel>] { pub fn channel_invitations(&self) -> &[Arc<Channel>] {
&self.channel_invitations &self.channel_invitations
} }

View file

@ -1,7 +1,7 @@
use std::{ops::Deref, sync::Arc}; use std::{ops::Deref, sync::Arc};
use crate::{Channel, ChannelId}; use crate::{Channel, ChannelId};
use collections::HashMap; use collections::BTreeMap;
use rpc::proto; use rpc::proto;
use super::ChannelPath; use super::ChannelPath;
@ -9,11 +9,11 @@ use super::ChannelPath;
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct ChannelIndex { pub struct ChannelIndex {
paths: Vec<ChannelPath>, paths: Vec<ChannelPath>,
channels_by_id: HashMap<ChannelId, Arc<Channel>>, channels_by_id: BTreeMap<ChannelId, Arc<Channel>>,
} }
impl ChannelIndex { impl ChannelIndex {
pub fn by_id(&self) -> &HashMap<ChannelId, Arc<Channel>> { pub fn by_id(&self) -> &BTreeMap<ChannelId, Arc<Channel>> {
&self.channels_by_id &self.channels_by_id
} }
@ -53,7 +53,7 @@ impl Deref for ChannelIndex {
#[derive(Debug)] #[derive(Debug)]
pub struct ChannelPathsInsertGuard<'a> { pub struct ChannelPathsInsertGuard<'a> {
paths: &'a mut Vec<ChannelPath>, paths: &'a mut Vec<ChannelPath>,
channels_by_id: &'a mut HashMap<ChannelId, Arc<Channel>>, channels_by_id: &'a mut BTreeMap<ChannelId, Arc<Channel>>,
} }
impl<'a> ChannelPathsInsertGuard<'a> { impl<'a> ChannelPathsInsertGuard<'a> {
@ -155,7 +155,7 @@ impl<'a> Drop for ChannelPathsInsertGuard<'a> {
fn channel_path_sorting_key<'a>( fn channel_path_sorting_key<'a>(
path: &'a [ChannelId], path: &'a [ChannelId],
channels_by_id: &'a HashMap<ChannelId, Arc<Channel>>, channels_by_id: &'a BTreeMap<ChannelId, Arc<Channel>>,
) -> impl 'a + Iterator<Item = Option<&'a str>> { ) -> impl 'a + Iterator<Item = Option<&'a str>> {
path.iter() path.iter()
.map(|id| Some(channels_by_id.get(id)?.name.as_str())) .map(|id| Some(channels_by_id.get(id)?.name.as_str()))

View file

@ -181,7 +181,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
// Join a channel and populate its existing messages. // Join a channel and populate its existing messages.
let channel = channel_store.update(cx, |store, cx| { let channel = channel_store.update(cx, |store, cx| {
let channel_id = store.channels().next().unwrap().1.id; let channel_id = store.channel_dag_entries().next().unwrap().1.id;
store.open_channel_chat(channel_id, cx) store.open_channel_chat(channel_id, cx)
}); });
let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap(); let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap();
@ -363,7 +363,7 @@ fn assert_channels(
) { ) {
let actual = channel_store.read_with(cx, |store, _| { let actual = channel_store.read_with(cx, |store, _| {
store store
.channels() .channel_dag_entries()
.map(|(depth, channel)| { .map(|(depth, channel)| {
( (
depth, depth,

View file

@ -56,7 +56,10 @@ async fn test_core_channels(
); );
client_b.channel_store().read_with(cx_b, |channels, _| { client_b.channel_store().read_with(cx_b, |channels, _| {
assert!(channels.channels().collect::<Vec<_>>().is_empty()) assert!(channels
.channel_dag_entries()
.collect::<Vec<_>>()
.is_empty())
}); });
// Invite client B to channel A as client A. // Invite client B to channel A as client A.
@ -1170,7 +1173,7 @@ fn assert_channels(
) { ) {
let actual = channel_store.read_with(cx, |store, _| { let actual = channel_store.read_with(cx, |store, _| {
store store
.channels() .channel_dag_entries()
.map(|(depth, channel)| ExpectedChannel { .map(|(depth, channel)| ExpectedChannel {
depth, depth,
name: channel.name.clone(), name: channel.name.clone(),
@ -1192,7 +1195,7 @@ fn assert_channels_list_shape(
let actual = channel_store.read_with(cx, |store, _| { let actual = channel_store.read_with(cx, |store, _| {
store store
.channels() .channel_dag_entries()
.map(|(depth, channel)| (channel.id, depth)) .map(|(depth, channel)| (channel.id, depth))
.collect::<Vec<_>>() .collect::<Vec<_>>()
}); });

View file

@ -86,7 +86,7 @@ impl RandomizedTest for RandomChannelBufferTest {
match rng.gen_range(0..100_u32) { match rng.gen_range(0..100_u32) {
0..=29 => { 0..=29 => {
let channel_name = client.channel_store().read_with(cx, |store, cx| { let channel_name = client.channel_store().read_with(cx, |store, cx| {
store.channels().find_map(|(_, channel)| { store.channel_dag_entries().find_map(|(_, channel)| {
if store.has_open_channel_buffer(channel.id, cx) { if store.has_open_channel_buffer(channel.id, cx) {
None None
} else { } else {
@ -133,7 +133,7 @@ impl RandomizedTest for RandomChannelBufferTest {
ChannelBufferOperation::JoinChannelNotes { channel_name } => { ChannelBufferOperation::JoinChannelNotes { channel_name } => {
let buffer = client.channel_store().update(cx, |store, cx| { let buffer = client.channel_store().update(cx, |store, cx| {
let channel_id = store let channel_id = store
.channels() .channel_dag_entries()
.find(|(_, c)| c.name == channel_name) .find(|(_, c)| c.name == channel_name)
.unwrap() .unwrap()
.1 .1

View file

@ -166,8 +166,8 @@ impl ChatPanel {
let selected_channel_id = this let selected_channel_id = this
.channel_store .channel_store
.read(cx) .read(cx)
.channel_at_index(selected_ix) .channel_at(selected_ix)
.map(|e| e.0.id); .map(|e| e.id);
if let Some(selected_channel_id) = selected_channel_id { if let Some(selected_channel_id) = selected_channel_id {
this.select_channel(selected_channel_id, cx) this.select_channel(selected_channel_id, cx)
.detach_and_log_err(cx); .detach_and_log_err(cx);
@ -391,7 +391,7 @@ impl ChatPanel {
(ItemType::Unselected, true) => &theme.channel_select.hovered_item, (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
}; };
let channel = &channel_store.read(cx).channel_at_index(ix).unwrap().0; let channel = &channel_store.read(cx).channel_at(ix).unwrap();
let channel_id = channel.id; let channel_id = channel.id;
let mut row = Flex::row() let mut row = Flex::row()

View file

@ -23,9 +23,9 @@ use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{ use gpui::{
actions, actions,
elements::{ elements::{
Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState, Canvas, ChildView, Component, ContainerStyle, Empty, Flex, Image, Label, List, ListOffset,
MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable, ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement,
Stack, Svg, SafeStylable, Stack, Svg,
}, },
fonts::TextStyle, fonts::TextStyle,
geometry::{ geometry::{
@ -42,7 +42,7 @@ use project::{Fs, Project};
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use settings::SettingsStore; use settings::SettingsStore;
use std::{borrow::Cow, hash::Hash, mem, sync::Arc}; use std::{borrow::Cow, hash::Hash, mem, sync::Arc};
use theme::{components::ComponentExt, IconButton}; use theme::{components::ComponentExt, IconButton, Interactive};
use util::{iife, ResultExt, TryFutureExt}; use util::{iife, ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, Panel}, dock::{DockPosition, Panel},
@ -65,6 +65,11 @@ struct RenameChannel {
location: ChannelPath, location: ChannelPath,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct ToggleSelectedIx {
ix: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct RemoveChannel { struct RemoveChannel {
channel_id: ChannelId, channel_id: ChannelId,
@ -96,7 +101,13 @@ struct OpenChannelBuffer {
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct StartMoveChannel { struct StartMoveChannelFor {
channel_id: ChannelId,
parent_id: Option<ChannelId>,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct StartLinkChannelFor {
channel_id: ChannelId, channel_id: ChannelId,
parent_id: Option<ChannelId>, parent_id: Option<ChannelId>,
} }
@ -126,7 +137,10 @@ actions!(
Remove, Remove,
Secondary, Secondary,
CollapseSelectedChannel, CollapseSelectedChannel,
ExpandSelectedChannel ExpandSelectedChannel,
StartMoveChannel,
StartLinkChannel,
MoveOrLinkToSelected,
] ]
); );
@ -143,12 +157,27 @@ impl_actions!(
JoinChannelCall, JoinChannelCall,
OpenChannelBuffer, OpenChannelBuffer,
LinkChannel, LinkChannel,
StartMoveChannel, StartMoveChannelFor,
StartLinkChannelFor,
MoveChannel, MoveChannel,
UnlinkChannel UnlinkChannel,
ToggleSelectedIx
] ]
); );
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
struct ChannelMoveClipboard {
channel_id: ChannelId,
parent_id: Option<ChannelId>,
intent: ClipboardIntent,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum ClipboardIntent {
Move,
Link,
}
const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
@ -175,17 +204,99 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(CollabPanel::open_channel_notes); cx.add_action(CollabPanel::open_channel_notes);
cx.add_action( cx.add_action(
|panel: &mut CollabPanel, action: &StartMoveChannel, _: &mut ViewContext<CollabPanel>| { |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
panel.channel_move = Some(*action); if panel.selection.take() != Some(action.ix) {
panel.selection = Some(action.ix)
}
cx.notify();
},
);
cx.add_action(
|panel: &mut CollabPanel,
action: &StartMoveChannelFor,
_: &mut ViewContext<CollabPanel>| {
panel.channel_clipboard = Some(ChannelMoveClipboard {
channel_id: action.channel_id,
parent_id: action.parent_id,
intent: ClipboardIntent::Move,
});
},
);
cx.add_action(
|panel: &mut CollabPanel,
action: &StartLinkChannelFor,
_: &mut ViewContext<CollabPanel>| {
panel.channel_clipboard = Some(ChannelMoveClipboard {
channel_id: action.channel_id,
parent_id: action.parent_id,
intent: ClipboardIntent::Link,
})
},
);
cx.add_action(
|panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext<CollabPanel>| {
if let Some((_, path)) = panel.selected_channel() {
panel.channel_clipboard = Some(ChannelMoveClipboard {
channel_id: path.channel_id(),
parent_id: path.parent_id(),
intent: ClipboardIntent::Move,
})
}
},
);
cx.add_action(
|panel: &mut CollabPanel, _: &StartLinkChannel, _: &mut ViewContext<CollabPanel>| {
if let Some((_, path)) = panel.selected_channel() {
panel.channel_clipboard = Some(ChannelMoveClipboard {
channel_id: path.channel_id(),
parent_id: path.parent_id(),
intent: ClipboardIntent::Link,
})
}
},
);
cx.add_action(
|panel: &mut CollabPanel, _: &MoveOrLinkToSelected, cx: &mut ViewContext<CollabPanel>| {
let clipboard = panel.channel_clipboard.take();
if let Some(((selected_channel, _), clipboard)) =
panel.selected_channel().zip(clipboard)
{
match clipboard.intent {
ClipboardIntent::Move if clipboard.parent_id.is_some() => {
let parent_id = clipboard.parent_id.unwrap();
panel.channel_store.update(cx, |channel_store, cx| {
channel_store
.move_channel(
clipboard.channel_id,
parent_id,
selected_channel.id,
cx,
)
.detach_and_log_err(cx)
})
}
_ => panel.channel_store.update(cx, |channel_store, cx| {
channel_store
.link_channel(clipboard.channel_id, selected_channel.id, cx)
.detach_and_log_err(cx)
}),
}
}
}, },
); );
cx.add_action( cx.add_action(
|panel: &mut CollabPanel, action: &LinkChannel, cx: &mut ViewContext<CollabPanel>| { |panel: &mut CollabPanel, action: &LinkChannel, cx: &mut ViewContext<CollabPanel>| {
if let Some(move_start) = panel.channel_move.take() { if let Some(clipboard) = panel.channel_clipboard.take() {
panel.channel_store.update(cx, |channel_store, cx| { panel.channel_store.update(cx, |channel_store, cx| {
channel_store channel_store
.link_channel(move_start.channel_id, action.to, cx) .link_channel(clipboard.channel_id, action.to, cx)
.detach_and_log_err(cx) .detach_and_log_err(cx)
}) })
} }
@ -194,15 +305,15 @@ pub fn init(cx: &mut AppContext) {
cx.add_action( cx.add_action(
|panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| { |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| {
if let Some(move_start) = panel.channel_move.take() { if let Some(clipboard) = panel.channel_clipboard.take() {
panel.channel_store.update(cx, |channel_store, cx| { panel.channel_store.update(cx, |channel_store, cx| {
if let Some(parent) = move_start.parent_id { if let Some(parent) = clipboard.parent_id {
channel_store channel_store
.move_channel(move_start.channel_id, parent, action.to, cx) .move_channel(clipboard.channel_id, parent, action.to, cx)
.detach_and_log_err(cx) .detach_and_log_err(cx)
} else { } else {
channel_store channel_store
.link_channel(move_start.channel_id, action.to, cx) .link_channel(clipboard.channel_id, action.to, cx)
.detach_and_log_err(cx) .detach_and_log_err(cx)
} }
}) })
@ -246,7 +357,7 @@ pub struct CollabPanel {
width: Option<f32>, width: Option<f32>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
has_focus: bool, has_focus: bool,
channel_move: Option<StartMoveChannel>, channel_clipboard: Option<ChannelMoveClipboard>,
pending_serialization: Task<Option<()>>, pending_serialization: Task<Option<()>>,
context_menu: ViewHandle<ContextMenu>, context_menu: ViewHandle<ContextMenu>,
filter_editor: ViewHandle<Editor>, filter_editor: ViewHandle<Editor>,
@ -444,6 +555,7 @@ impl CollabPanel {
path.to_owned(), path.to_owned(),
&theme.collab_panel, &theme.collab_panel,
is_selected, is_selected,
ix,
cx, cx,
); );
@ -510,7 +622,7 @@ impl CollabPanel {
let mut this = Self { let mut this = Self {
width: None, width: None,
has_focus: false, has_focus: false,
channel_move: None, channel_clipboard: None,
fs: workspace.app_state().fs.clone(), fs: workspace.app_state().fs.clone(),
pending_serialization: Task::ready(None), pending_serialization: Task::ready(None),
context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
@ -776,16 +888,13 @@ impl CollabPanel {
if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
self.match_candidates.clear(); self.match_candidates.clear();
self.match_candidates self.match_candidates
.extend( .extend(channel_store.channel_dag_entries().enumerate().map(
channel_store |(ix, (_, channel))| StringMatchCandidate {
.channels() id: ix,
.enumerate() string: channel.name.clone(),
.map(|(ix, (_, channel))| StringMatchCandidate { char_bag: channel.name.chars().collect(),
id: ix, },
string: channel.name.clone(), ));
char_bag: channel.name.chars().collect(),
}),
);
let matches = executor.block(match_strings( let matches = executor.block(match_strings(
&self.match_candidates, &self.match_candidates,
&query, &query,
@ -801,7 +910,9 @@ impl CollabPanel {
} }
let mut collapse_depth = None; let mut collapse_depth = None;
for mat in matches { for mat in matches {
let (channel, path) = channel_store.channel_at_index(mat.candidate_id).unwrap(); let (channel, path) = channel_store
.channel_dag_entry_at(mat.candidate_id)
.unwrap();
let depth = path.len() - 1; let depth = path.len() - 1;
if collapse_depth.is_none() && self.is_channel_collapsed(path) { if collapse_depth.is_none() && self.is_channel_collapsed(path) {
@ -1627,7 +1738,7 @@ impl CollabPanel {
.constrained() .constrained()
.with_height(theme.collab_panel.row_height) .with_height(theme.collab_panel.row_height)
.contained() .contained()
.with_style(gpui::elements::ContainerStyle { .with_style(ContainerStyle {
background_color: Some(theme.editor.background), background_color: Some(theme.editor.background),
..*theme.collab_panel.contact_row.default_style() ..*theme.collab_panel.contact_row.default_style()
}) })
@ -1645,11 +1756,13 @@ impl CollabPanel {
path: ChannelPath, path: ChannelPath,
theme: &theme::CollabPanel, theme: &theme::CollabPanel,
is_selected: bool, is_selected: bool,
ix: usize,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> AnyElement<Self> { ) -> AnyElement<Self> {
let channel_id = channel.id; let channel_id = channel.id;
let has_children = self.channel_store.read(cx).has_children(channel_id); let has_children = self.channel_store.read(cx).has_children(channel_id);
let other_selected =
self.selected_channel().map(|channel| channel.0.id) == Some(channel.id);
let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok()); let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok());
let is_active = iife!({ let is_active = iife!({
@ -1683,6 +1796,20 @@ impl CollabPanel {
MouseEventHandler::new::<Channel, _>(path.unique_id() as usize, cx, |state, cx| { MouseEventHandler::new::<Channel, _>(path.unique_id() as usize, cx, |state, cx| {
let row_hovered = state.hovered(); let row_hovered = state.hovered();
let mut select_state = |interactive: &Interactive<ContainerStyle>| {
if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() {
interactive.clicked.as_ref().unwrap().clone()
} else if state.hovered() || other_selected {
interactive
.hovered
.as_ref()
.unwrap_or(&interactive.default)
.clone()
} else {
interactive.default.clone()
}
};
Flex::<Self>::row() Flex::<Self>::row()
.with_child( .with_child(
Svg::new("icons/hash.svg") Svg::new("icons/hash.svg")
@ -1760,11 +1887,11 @@ impl CollabPanel {
.constrained() .constrained()
.with_height(theme.row_height) .with_height(theme.row_height)
.contained() .contained()
.with_style( .with_style(select_state(
*theme theme
.channel_row .channel_row
.style_for(is_selected || is_active || is_dragged_over, state), .in_state(is_selected || is_active || is_dragged_over),
) ))
.with_padding_left( .with_padding_left(
theme.channel_row.default_style().padding.left theme.channel_row.default_style().padding.left
+ theme.channel_indent * depth as f32, + theme.channel_indent * depth as f32,
@ -1778,7 +1905,7 @@ impl CollabPanel {
.on_click(MouseButton::Right, { .on_click(MouseButton::Right, {
let path = path.clone(); let path = path.clone();
move |e, this, cx| { move |e, this, cx| {
this.deploy_channel_context_menu(Some(e.position), &path, cx); this.deploy_channel_context_menu(Some(e.position), &path, ix, cx);
} }
}) })
.on_up(MouseButton::Left, move |e, this, cx| { .on_up(MouseButton::Left, move |e, this, cx| {
@ -2119,15 +2246,34 @@ impl CollabPanel {
.into_any() .into_any()
} }
fn has_subchannels(&self, ix: usize) -> bool {
self.entries
.get(ix)
.zip(self.entries.get(ix + 1))
.map(|entries| match entries {
(
ListEntry::Channel {
path: this_path, ..
},
ListEntry::Channel {
path: next_path, ..
},
) => next_path.starts_with(this_path),
_ => false,
})
.unwrap_or(false)
}
fn deploy_channel_context_menu( fn deploy_channel_context_menu(
&mut self, &mut self,
position: Option<Vector2F>, position: Option<Vector2F>,
path: &ChannelPath, path: &ChannelPath,
ix: usize,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
self.context_menu_on_selected = position.is_none(); self.context_menu_on_selected = position.is_none();
let channel_name = self.channel_move.as_ref().and_then(|channel| { let channel_name = self.channel_clipboard.as_ref().and_then(|channel| {
let channel_name = self let channel_name = self
.channel_store .channel_store
.read(cx) .read(cx)
@ -2145,42 +2291,37 @@ impl CollabPanel {
let mut items = Vec::new(); let mut items = Vec::new();
if let Some(channel_name) = channel_name { let select_action_name = if self.selection == Some(ix) {
items.push(ContextMenuItem::action( "Unselect"
format!("Move '#{}' here", channel_name),
MoveChannel {
to: path.channel_id(),
},
));
items.push(ContextMenuItem::action(
format!("Link '#{}' here", channel_name),
LinkChannel {
to: path.channel_id(),
},
));
items.push(ContextMenuItem::Separator)
}
let expand_action_name = if self.is_channel_collapsed(&path) {
"Expand Subchannels"
} else { } else {
"Collapse Subchannels" "Select"
}; };
items.extend([ items.push(ContextMenuItem::action(
ContextMenuItem::action( select_action_name,
ToggleSelectedIx { ix },
));
if self.has_subchannels(ix) {
let expand_action_name = if self.is_channel_collapsed(&path) {
"Expand Subchannels"
} else {
"Collapse Subchannels"
};
items.push(ContextMenuItem::action(
expand_action_name, expand_action_name,
ToggleCollapse { ToggleCollapse {
location: path.clone(), location: path.clone(),
}, },
), ));
ContextMenuItem::action( }
"Open Notes",
OpenChannelBuffer { items.push(ContextMenuItem::action(
channel_id: path.channel_id(), "Open Notes",
}, OpenChannelBuffer {
), channel_id: path.channel_id(),
]); },
));
if self.channel_store.read(cx).is_user_admin(path.channel_id()) { if self.channel_store.read(cx).is_user_admin(path.channel_id()) {
let parent_id = path.parent_id(); let parent_id = path.parent_id();
@ -2212,13 +2353,38 @@ impl CollabPanel {
)); ));
} }
items.extend([ContextMenuItem::action( items.extend([
"Move this channel", ContextMenuItem::action(
StartMoveChannel { "Move this channel",
channel_id: path.channel_id(), StartMoveChannelFor {
parent_id, channel_id: path.channel_id(),
}, parent_id,
)]); },
),
ContextMenuItem::action(
"Link this channel",
StartLinkChannelFor {
channel_id: path.channel_id(),
parent_id,
},
),
]);
if let Some(channel_name) = channel_name {
items.push(ContextMenuItem::Separator);
items.push(ContextMenuItem::action(
format!("Move '#{}' here", channel_name),
MoveChannel {
to: path.channel_id(),
},
));
items.push(ContextMenuItem::action(
format!("Link '#{}' here", channel_name),
LinkChannel {
to: path.channel_id(),
},
));
}
items.extend([ items.extend([
ContextMenuItem::Separator, ContextMenuItem::Separator,
@ -2598,7 +2764,7 @@ impl CollabPanel {
return; return;
}; };
self.deploy_channel_context_menu(None, &path.to_owned(), cx); self.deploy_channel_context_menu(None, &path.to_owned(), self.selection.unwrap(), cx);
} }
fn selected_channel(&self) -> Option<(&Arc<Channel>, &ChannelPath)> { fn selected_channel(&self) -> Option<(&Arc<Channel>, &ChannelPath)> {