Detailed changes
@@ -6,18 +6,19 @@ use rpc::{proto, TypedEnvelope};
use std::sync::Arc;
pub struct ChannelStore {
- channels: Vec<Channel>,
- channel_invitations: Vec<Channel>,
+ channels: Vec<Arc<Channel>>,
+ channel_invitations: Vec<Arc<Channel>>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
_rpc_subscription: Subscription,
}
-#[derive(Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq)]
pub struct Channel {
pub id: u64,
pub name: String,
pub parent_id: Option<u64>,
+ pub depth: usize,
}
impl Entity for ChannelStore {
@@ -41,11 +42,11 @@ impl ChannelStore {
}
}
- pub fn channels(&self) -> &[Channel] {
+ pub fn channels(&self) -> &[Arc<Channel>] {
&self.channels
}
- pub fn channel_invitations(&self) -> &[Channel] {
+ pub fn channel_invitations(&self) -> &[Arc<Channel>] {
&self.channel_invitations
}
@@ -97,6 +98,10 @@ impl ChannelStore {
}
}
+ pub fn is_channel_invite_pending(&self, channel: &Arc<Channel>) -> bool {
+ false
+ }
+
pub fn remove_member(
&self,
channel_id: u64,
@@ -124,66 +129,74 @@ impl ChannelStore {
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
- let payload = message.payload;
this.update(&mut cx, |this, cx| {
- this.channels
- .retain(|channel| !payload.remove_channels.contains(&channel.id));
- this.channel_invitations
- .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id));
-
- for channel in payload.channel_invitations {
- if let Some(existing_channel) = this
- .channel_invitations
- .iter_mut()
- .find(|c| c.id == channel.id)
- {
- existing_channel.name = channel.name;
- continue;
- }
+ this.update_channels(message.payload, cx);
+ });
+ Ok(())
+ }
- this.channel_invitations.insert(
- 0,
- Channel {
- id: channel.id,
- name: channel.name,
- parent_id: None,
- },
- );
+ pub(crate) fn update_channels(
+ &mut self,
+ payload: proto::UpdateChannels,
+ cx: &mut ModelContext<ChannelStore>,
+ ) {
+ self.channels
+ .retain(|channel| !payload.remove_channels.contains(&channel.id));
+ self.channel_invitations
+ .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id));
+
+ for channel in payload.channel_invitations {
+ if let Some(existing_channel) = self
+ .channel_invitations
+ .iter_mut()
+ .find(|c| c.id == channel.id)
+ {
+ Arc::make_mut(existing_channel).name = channel.name;
+ continue;
}
- for channel in payload.channels {
- if let Some(existing_channel) =
- this.channels.iter_mut().find(|c| c.id == channel.id)
- {
- existing_channel.name = channel.name;
- continue;
- }
+ self.channel_invitations.insert(
+ 0,
+ Arc::new(Channel {
+ id: channel.id,
+ name: channel.name,
+ parent_id: None,
+ depth: 0,
+ }),
+ );
+ }
+
+ for channel in payload.channels {
+ if let Some(existing_channel) = self.channels.iter_mut().find(|c| c.id == channel.id) {
+ Arc::make_mut(existing_channel).name = channel.name;
+ continue;
+ }
- if let Some(parent_id) = channel.parent_id {
- if let Some(ix) = this.channels.iter().position(|c| c.id == parent_id) {
- this.channels.insert(
- ix + 1,
- Channel {
- id: channel.id,
- name: channel.name,
- parent_id: Some(parent_id),
- },
- );
- }
- } else {
- this.channels.insert(
- 0,
- Channel {
+ if let Some(parent_id) = channel.parent_id {
+ if let Some(ix) = self.channels.iter().position(|c| c.id == parent_id) {
+ let depth = self.channels[ix].depth + 1;
+ self.channels.insert(
+ ix + 1,
+ Arc::new(Channel {
id: channel.id,
name: channel.name,
- parent_id: None,
- },
+ parent_id: Some(parent_id),
+ depth,
+ }),
);
}
+ } else {
+ self.channels.insert(
+ 0,
+ Arc::new(Channel {
+ id: channel.id,
+ name: channel.name,
+ parent_id: None,
+ depth: 0,
+ }),
+ );
}
- cx.notify();
- });
-
- Ok(())
+ }
+ cx.notify();
}
}
@@ -0,0 +1,95 @@
+use util::http::FakeHttpClient;
+
+use super::*;
+
+#[gpui::test]
+fn test_update_channels(cx: &mut AppContext) {
+ let http = FakeHttpClient::with_404_response();
+ let client = Client::new(http.clone(), cx);
+ let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
+
+ let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx));
+
+ update_channels(
+ &channel_store,
+ proto::UpdateChannels {
+ channels: vec![
+ proto::Channel {
+ id: 1,
+ name: "b".to_string(),
+ parent_id: None,
+ },
+ proto::Channel {
+ id: 2,
+ name: "a".to_string(),
+ parent_id: None,
+ },
+ ],
+ ..Default::default()
+ },
+ cx,
+ );
+ assert_channels(
+ &channel_store,
+ &[
+ //
+ (0, "a"),
+ (0, "b"),
+ ],
+ cx,
+ );
+
+ update_channels(
+ &channel_store,
+ proto::UpdateChannels {
+ channels: vec![
+ proto::Channel {
+ id: 3,
+ name: "x".to_string(),
+ parent_id: Some(1),
+ },
+ proto::Channel {
+ id: 4,
+ name: "y".to_string(),
+ parent_id: Some(2),
+ },
+ ],
+ ..Default::default()
+ },
+ cx,
+ );
+ assert_channels(
+ &channel_store,
+ &[
+ //
+ (0, "a"),
+ (1, "y"),
+ (0, "b"),
+ (1, "x"),
+ ],
+ cx,
+ );
+}
+
+fn update_channels(
+ channel_store: &ModelHandle<ChannelStore>,
+ message: proto::UpdateChannels,
+ cx: &mut AppContext,
+) {
+ channel_store.update(cx, |store, cx| store.update_channels(message, cx));
+}
+
+fn assert_channels(
+ channel_store: &ModelHandle<ChannelStore>,
+ expected_channels: &[(usize, &str)],
+ cx: &AppContext,
+) {
+ channel_store.read_with(cx, |store, _| {
+ let actual = store
+ .channels()
+ .iter()
+ .map(|c| (c.depth, c.name.as_str()))
+ .collect::<Vec<_>>();
+ assert_eq!(actual, expected_channels);
+ });
+}
@@ -1,6 +1,9 @@
#[cfg(any(test, feature = "test-support"))]
pub mod test;
+#[cfg(test)]
+mod channel_store_tests;
+
pub mod channel_store;
pub mod telemetry;
pub mod user;
@@ -193,6 +193,7 @@ impl TestServer {
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
+ channel_store: channel_store.clone(),
languages: Arc::new(LanguageRegistry::test()),
fs: fs.clone(),
build_window_options: |_, _, _| Default::default(),
@@ -29,11 +29,12 @@ async fn test_basic_channels(
client_a.channel_store.read_with(cx_a, |channels, _| {
assert_eq!(
channels.channels(),
- &[Channel {
+ &[Arc::new(Channel {
id: channel_a_id,
name: "channel-a".to_string(),
parent_id: None,
- }]
+ depth: 0,
+ })]
)
});
@@ -56,11 +57,12 @@ async fn test_basic_channels(
client_b.channel_store.read_with(cx_b, |channels, _| {
assert_eq!(
channels.channel_invitations(),
- &[Channel {
+ &[Arc::new(Channel {
id: channel_a_id,
name: "channel-a".to_string(),
parent_id: None,
- }]
+ depth: 0,
+ })]
)
});
@@ -76,11 +78,12 @@ async fn test_basic_channels(
assert_eq!(channels.channel_invitations(), &[]);
assert_eq!(
channels.channels(),
- &[Channel {
+ &[Arc::new(Channel {
id: channel_a_id,
name: "channel-a".to_string(),
parent_id: None,
- }]
+ depth: 0,
+ })]
)
});
}
@@ -4,7 +4,7 @@ mod panel_settings;
use anyhow::Result;
use call::ActiveCall;
-use client::{proto::PeerId, Client, Contact, User, UserStore};
+use client::{proto::PeerId, Channel, ChannelStore, Client, Contact, User, UserStore};
use contact_finder::build_contact_finder;
use context_menu::ContextMenu;
use db::kvp::KEY_VALUE_STORE;
@@ -62,6 +62,7 @@ pub struct CollabPanel {
entries: Vec<ContactEntry>,
selection: Option<usize>,
user_store: ModelHandle<UserStore>,
+ channel_store: ModelHandle<ChannelStore>,
project: ModelHandle<Project>,
match_candidates: Vec<StringMatchCandidate>,
list_state: ListState<Self>,
@@ -109,8 +110,10 @@ enum ContactEntry {
peer_id: PeerId,
is_last: bool,
},
+ ChannelInvite(Arc<Channel>),
IncomingRequest(Arc<User>),
OutgoingRequest(Arc<User>),
+ Channel(Arc<Channel>),
Contact {
contact: Arc<Contact>,
calling: bool,
@@ -204,6 +207,16 @@ impl CollabPanel {
cx,
)
}
+ ContactEntry::Channel(channel) => {
+ Self::render_channel(&*channel, &theme.collab_panel, is_selected, cx)
+ }
+ ContactEntry::ChannelInvite(channel) => Self::render_channel_invite(
+ channel.clone(),
+ this.channel_store.clone(),
+ &theme.collab_panel,
+ is_selected,
+ cx,
+ ),
ContactEntry::IncomingRequest(user) => Self::render_contact_request(
user.clone(),
this.user_store.clone(),
@@ -241,6 +254,7 @@ impl CollabPanel {
entries: Vec::default(),
selection: None,
user_store: workspace.user_store().clone(),
+ channel_store: workspace.app_state().channel_store.clone(),
project: workspace.project().clone(),
subscriptions: Vec::default(),
match_candidates: Vec::default(),
@@ -320,6 +334,7 @@ impl CollabPanel {
}
fn update_entries(&mut self, cx: &mut ViewContext<Self>) {
+ let channel_store = self.channel_store.read(cx);
let user_store = self.user_store.read(cx);
let query = self.filter_editor.read(cx).text(cx);
let executor = cx.background().clone();
@@ -445,10 +460,65 @@ impl CollabPanel {
self.entries
.push(ContactEntry::Header(Section::Channels, 0));
+ let channels = channel_store.channels();
+ if !channels.is_empty() {
+ self.match_candidates.clear();
+ self.match_candidates
+ .extend(
+ channels
+ .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(),
+ ));
+ self.entries.extend(
+ matches
+ .iter()
+ .map(|mat| ContactEntry::Channel(channels[mat.candidate_id].clone())),
+ );
+ }
+
self.entries
.push(ContactEntry::Header(Section::Contacts, 0));
let mut request_entries = Vec::new();
+ 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| {
+ ContactEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
+ }),
+ );
+ }
+
let incoming = user_store.incoming_contact_requests();
if !incoming.is_empty() {
self.match_candidates.clear();
@@ -1112,6 +1182,121 @@ impl CollabPanel {
event_handler.into_any()
}
+ fn render_channel(
+ channel: &Channel,
+ theme: &theme::CollabPanel,
+ is_selected: bool,
+ cx: &mut ViewContext<Self>,
+ ) -> AnyElement<Self> {
+ let channel_id = channel.id;
+ MouseEventHandler::<Channel, Self>::new(channel.id as usize, cx, |state, cx| {
+ Flex::row()
+ .with_child({
+ Svg::new("icons/hash")
+ // .with_style(theme.contact_avatar)
+ .aligned()
+ .left()
+ })
+ .with_child(
+ Label::new(channel.name.clone(), theme.contact_username.text.clone())
+ .contained()
+ .with_style(theme.contact_username.container)
+ .aligned()
+ .left()
+ .flex(1., true),
+ )
+ .constrained()
+ .with_height(theme.row_height)
+ .contained()
+ .with_style(*theme.contact_row.in_state(is_selected).style_for(state))
+ })
+ .on_click(MouseButton::Left, move |_, this, cx| {
+ this.join_channel(channel_id, cx);
+ })
+ .into_any()
+ }
+
+ fn render_channel_invite(
+ channel: Arc<Channel>,
+ user_store: ModelHandle<ChannelStore>,
+ theme: &theme::CollabPanel,
+ is_selected: bool,
+ cx: &mut ViewContext<Self>,
+ ) -> AnyElement<Self> {
+ enum Decline {}
+ enum Accept {}
+
+ let channel_id = channel.id;
+ let is_invite_pending = user_store.read(cx).is_channel_invite_pending(&channel);
+ let button_spacing = theme.contact_button_spacing;
+
+ Flex::row()
+ .with_child({
+ Svg::new("icons/hash")
+ // .with_style(theme.contact_avatar)
+ .aligned()
+ .left()
+ })
+ .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(
+ MouseEventHandler::<Decline, Self>::new(
+ 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_mark_8.svg").aligned()
+ },
+ )
+ .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(
+ MouseEventHandler::<Accept, Self>::new(
+ 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/check_8.svg")
+ .aligned()
+ .flex_float()
+ },
+ )
+ .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()),
+ )
+ .into_any()
+ }
+
fn render_contact_request(
user: Arc<User>,
user_store: ModelHandle<UserStore>,
@@ -1384,6 +1569,18 @@ impl CollabPanel {
.detach();
}
+ 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();
+ }
+
fn call(
&mut self,
recipient_user_id: u64,
@@ -1396,6 +1593,12 @@ impl CollabPanel {
})
.detach_and_log_err(cx);
}
+
+ fn join_channel(&self, channel: u64, cx: &mut ViewContext<Self>) {
+ ActiveCall::global(cx)
+ .update(cx, |call, cx| call.join_channel(channel, cx))
+ .detach_and_log_err(cx);
+ }
}
impl View for CollabPanel {
@@ -1557,6 +1760,16 @@ impl PartialEq for ContactEntry {
return peer_id_1 == peer_id_2;
}
}
+ ContactEntry::Channel(channel_1) => {
+ if let ContactEntry::Channel(channel_2) = other {
+ return channel_1.id == channel_2.id;
+ }
+ }
+ ContactEntry::ChannelInvite(channel_1) => {
+ if let ContactEntry::ChannelInvite(channel_2) = other {
+ return channel_1.id == channel_2.id;
+ }
+ }
ContactEntry::IncomingRequest(user_1) => {
if let ContactEntry::IncomingRequest(user_2) = other {
return user_1.id == user_2.id;
@@ -14,7 +14,7 @@ use anyhow::{anyhow, Context, Result};
use call::ActiveCall;
use client::{
proto::{self, PeerId},
- Client, TypedEnvelope, UserStore,
+ ChannelStore, Client, TypedEnvelope, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
use drag_and_drop::DragAndDrop;
@@ -400,8 +400,9 @@ pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
pub struct AppState {
pub languages: Arc<LanguageRegistry>,
- pub client: Arc<client::Client>,
- pub user_store: ModelHandle<client::UserStore>,
+ pub client: Arc<Client>,
+ pub user_store: ModelHandle<UserStore>,
+ pub channel_store: ModelHandle<ChannelStore>,
pub fs: Arc<dyn fs::Fs>,
pub build_window_options:
fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
@@ -424,6 +425,8 @@ impl AppState {
let http_client = util::http::FakeHttpClient::with_404_response();
let client = Client::new(http_client.clone(), cx);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
+ let channel_store =
+ cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
theme::init((), cx);
client::init(&client, cx);
@@ -434,6 +437,7 @@ impl AppState {
fs,
languages,
user_store,
+ channel_store,
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
build_window_options: |_, _, _| Default::default(),
background_actions: || &[],
@@ -3406,10 +3410,15 @@ impl Workspace {
#[cfg(any(test, feature = "test-support"))]
pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+ let client = project.read(cx).client();
+ let user_store = project.read(cx).user_store();
+ let channel_store =
+ cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
let app_state = Arc::new(AppState {
languages: project.read(cx).languages().clone(),
- client: project.read(cx).client(),
- user_store: project.read(cx).user_store(),
+ client,
+ user_store,
+ channel_store,
fs: project.read(cx).fs().clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
@@ -7,7 +7,9 @@ use cli::{
ipc::{self, IpcSender},
CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
};
-use client::{self, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
+use client::{
+ self, ChannelStore, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
+};
use db::kvp::KEY_VALUE_STORE;
use editor::{scroll::autoscroll::Autoscroll, Editor};
use futures::{
@@ -140,6 +142,8 @@ fn main() {
languages::init(languages.clone(), node_runtime.clone());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
+ let channel_store =
+ cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
cx.set_global(client.clone());
@@ -181,6 +185,7 @@ fn main() {
languages,
client: client.clone(),
user_store,
+ channel_store,
fs,
build_window_options,
initialize_workspace,