Restore chat functionality with a very rough UI
This commit is contained in:
parent
da5a77badf
commit
ddda5a559b
13 changed files with 440 additions and 185 deletions
|
@ -47,5 +47,6 @@ tempfile = "3"
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
rpc = { path = "../rpc", features = ["test-support"] }
|
rpc = { path = "../rpc", features = ["test-support"] }
|
||||||
|
client = { path = "../client", features = ["test-support"] }
|
||||||
settings = { path = "../settings", features = ["test-support"] }
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
util = { path = "../util", features = ["test-support"] }
|
util = { path = "../util", features = ["test-support"] }
|
||||||
|
|
|
@ -111,6 +111,10 @@ impl ChannelStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn client(&self) -> Arc<Client> {
|
||||||
|
self.client.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn has_children(&self, channel_id: ChannelId) -> bool {
|
pub fn has_children(&self, channel_id: ChannelId) -> bool {
|
||||||
self.channel_paths.iter().any(|path| {
|
self.channel_paths.iter().any(|path| {
|
||||||
if let Some(ix) = path.iter().position(|id| *id == channel_id) {
|
if let Some(ix) = path.iter().position(|id| *id == channel_id) {
|
||||||
|
|
|
@ -4,15 +4,12 @@ use super::*;
|
||||||
use client::{test::FakeServer, Client, UserStore};
|
use client::{test::FakeServer, Client, UserStore};
|
||||||
use gpui::{AppContext, ModelHandle, TestAppContext};
|
use gpui::{AppContext, ModelHandle, TestAppContext};
|
||||||
use rpc::proto;
|
use rpc::proto;
|
||||||
|
use settings::SettingsStore;
|
||||||
use util::http::FakeHttpClient;
|
use util::http::FakeHttpClient;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_update_channels(cx: &mut AppContext) {
|
fn test_update_channels(cx: &mut AppContext) {
|
||||||
let http = FakeHttpClient::with_404_response();
|
let channel_store = init_test(cx);
|
||||||
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(
|
update_channels(
|
||||||
&channel_store,
|
&channel_store,
|
||||||
|
@ -80,11 +77,7 @@ fn test_update_channels(cx: &mut AppContext) {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_dangling_channel_paths(cx: &mut AppContext) {
|
fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||||
let http = FakeHttpClient::with_404_response();
|
let channel_store = init_test(cx);
|
||||||
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(
|
update_channels(
|
||||||
&channel_store,
|
&channel_store,
|
||||||
|
@ -141,18 +134,11 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_channel_messages(cx: &mut TestAppContext) {
|
async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||||
cx.foreground().forbid_parking();
|
|
||||||
|
|
||||||
let user_id = 5;
|
let user_id = 5;
|
||||||
let http_client = FakeHttpClient::with_404_response();
|
|
||||||
let client = cx.update(|cx| Client::new(http_client.clone(), cx));
|
|
||||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
|
||||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
|
||||||
crate::init(&client);
|
|
||||||
|
|
||||||
let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx));
|
|
||||||
|
|
||||||
let channel_id = 5;
|
let channel_id = 5;
|
||||||
|
let channel_store = cx.update(init_test);
|
||||||
|
let client = channel_store.read_with(cx, |s, _| s.client());
|
||||||
|
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||||
|
|
||||||
// Get the available channels.
|
// Get the available channels.
|
||||||
server.send(proto::UpdateChannels {
|
server.send(proto::UpdateChannels {
|
||||||
|
@ -163,85 +149,71 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
channel_store.next_notification(cx).await;
|
cx.foreground().run_until_parked();
|
||||||
cx.read(|cx| {
|
cx.read(|cx| {
|
||||||
assert_channels(&channel_store, &[(0, "the-channel".to_string(), false)], cx);
|
assert_channels(&channel_store, &[(0, "the-channel".to_string(), false)], cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||||
assert_eq!(get_users.payload.user_ids, vec![5]);
|
assert_eq!(get_users.payload.user_ids, vec![5]);
|
||||||
server
|
server.respond(
|
||||||
.respond(
|
get_users.receipt(),
|
||||||
get_users.receipt(),
|
proto::UsersResponse {
|
||||||
proto::UsersResponse {
|
users: vec![proto::User {
|
||||||
users: vec![proto::User {
|
id: 5,
|
||||||
id: 5,
|
github_login: "nathansobo".into(),
|
||||||
github_login: "nathansobo".into(),
|
avatar_url: "http://avatar.com/nathansobo".into(),
|
||||||
avatar_url: "http://avatar.com/nathansobo".into(),
|
}],
|
||||||
}],
|
},
|
||||||
},
|
);
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Join a channel and populate its existing messages.
|
// Join a channel and populate its existing messages.
|
||||||
let channel = channel_store
|
let channel = channel_store.update(cx, |store, cx| {
|
||||||
.update(cx, |store, cx| {
|
let channel_id = store.channels().next().unwrap().1.id;
|
||||||
let channel_id = store.channels().next().unwrap().1.id;
|
store.open_channel_chat(channel_id, cx)
|
||||||
store.open_channel_chat(channel_id, cx)
|
});
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
channel.read_with(cx, |channel, _| assert!(channel.messages().is_empty()));
|
|
||||||
let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap();
|
let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap();
|
||||||
server
|
server.respond(
|
||||||
.respond(
|
join_channel.receipt(),
|
||||||
join_channel.receipt(),
|
proto::JoinChannelChatResponse {
|
||||||
proto::JoinChannelChatResponse {
|
messages: vec![
|
||||||
messages: vec![
|
proto::ChannelMessage {
|
||||||
proto::ChannelMessage {
|
id: 10,
|
||||||
id: 10,
|
body: "a".into(),
|
||||||
body: "a".into(),
|
timestamp: 1000,
|
||||||
timestamp: 1000,
|
sender_id: 5,
|
||||||
sender_id: 5,
|
nonce: Some(1.into()),
|
||||||
nonce: Some(1.into()),
|
},
|
||||||
},
|
proto::ChannelMessage {
|
||||||
proto::ChannelMessage {
|
id: 11,
|
||||||
id: 11,
|
body: "b".into(),
|
||||||
body: "b".into(),
|
timestamp: 1001,
|
||||||
timestamp: 1001,
|
sender_id: 6,
|
||||||
sender_id: 6,
|
nonce: Some(2.into()),
|
||||||
nonce: Some(2.into()),
|
},
|
||||||
},
|
],
|
||||||
],
|
done: false,
|
||||||
done: false,
|
},
|
||||||
},
|
);
|
||||||
)
|
|
||||||
.await;
|
cx.foreground().start_waiting();
|
||||||
|
|
||||||
// Client requests all users for the received messages
|
// Client requests all users for the received messages
|
||||||
let mut get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
let mut get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||||
get_users.payload.user_ids.sort();
|
get_users.payload.user_ids.sort();
|
||||||
assert_eq!(get_users.payload.user_ids, vec![6]);
|
assert_eq!(get_users.payload.user_ids, vec![6]);
|
||||||
server
|
server.respond(
|
||||||
.respond(
|
get_users.receipt(),
|
||||||
get_users.receipt(),
|
proto::UsersResponse {
|
||||||
proto::UsersResponse {
|
users: vec![proto::User {
|
||||||
users: vec![proto::User {
|
id: 6,
|
||||||
id: 6,
|
github_login: "maxbrunsfeld".into(),
|
||||||
github_login: "maxbrunsfeld".into(),
|
avatar_url: "http://avatar.com/maxbrunsfeld".into(),
|
||||||
avatar_url: "http://avatar.com/maxbrunsfeld".into(),
|
}],
|
||||||
}],
|
},
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
channel.next_event(cx).await,
|
|
||||||
ChannelChatEvent::MessagesUpdated {
|
|
||||||
old_range: 0..0,
|
|
||||||
new_count: 2,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let channel = channel.await.unwrap();
|
||||||
channel.read_with(cx, |channel, _| {
|
channel.read_with(cx, |channel, _| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
channel
|
channel
|
||||||
|
@ -270,18 +242,16 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||||
// Client requests user for message since they haven't seen them yet
|
// Client requests user for message since they haven't seen them yet
|
||||||
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||||
assert_eq!(get_users.payload.user_ids, vec![7]);
|
assert_eq!(get_users.payload.user_ids, vec![7]);
|
||||||
server
|
server.respond(
|
||||||
.respond(
|
get_users.receipt(),
|
||||||
get_users.receipt(),
|
proto::UsersResponse {
|
||||||
proto::UsersResponse {
|
users: vec![proto::User {
|
||||||
users: vec![proto::User {
|
id: 7,
|
||||||
id: 7,
|
github_login: "as-cii".into(),
|
||||||
github_login: "as-cii".into(),
|
avatar_url: "http://avatar.com/as-cii".into(),
|
||||||
avatar_url: "http://avatar.com/as-cii".into(),
|
}],
|
||||||
}],
|
},
|
||||||
},
|
);
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
channel.next_event(cx).await,
|
channel.next_event(cx).await,
|
||||||
|
@ -307,30 +277,28 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||||
let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
|
let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
|
||||||
assert_eq!(get_messages.payload.channel_id, 5);
|
assert_eq!(get_messages.payload.channel_id, 5);
|
||||||
assert_eq!(get_messages.payload.before_message_id, 10);
|
assert_eq!(get_messages.payload.before_message_id, 10);
|
||||||
server
|
server.respond(
|
||||||
.respond(
|
get_messages.receipt(),
|
||||||
get_messages.receipt(),
|
proto::GetChannelMessagesResponse {
|
||||||
proto::GetChannelMessagesResponse {
|
done: true,
|
||||||
done: true,
|
messages: vec![
|
||||||
messages: vec![
|
proto::ChannelMessage {
|
||||||
proto::ChannelMessage {
|
id: 8,
|
||||||
id: 8,
|
body: "y".into(),
|
||||||
body: "y".into(),
|
timestamp: 998,
|
||||||
timestamp: 998,
|
sender_id: 5,
|
||||||
sender_id: 5,
|
nonce: Some(4.into()),
|
||||||
nonce: Some(4.into()),
|
},
|
||||||
},
|
proto::ChannelMessage {
|
||||||
proto::ChannelMessage {
|
id: 9,
|
||||||
id: 9,
|
body: "z".into(),
|
||||||
body: "z".into(),
|
timestamp: 999,
|
||||||
timestamp: 999,
|
sender_id: 6,
|
||||||
sender_id: 6,
|
nonce: Some(5.into()),
|
||||||
nonce: Some(5.into()),
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
);
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
channel.next_event(cx).await,
|
channel.next_event(cx).await,
|
||||||
|
@ -353,6 +321,19 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn init_test(cx: &mut AppContext) -> ModelHandle<ChannelStore> {
|
||||||
|
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));
|
||||||
|
|
||||||
|
cx.foreground().forbid_parking();
|
||||||
|
cx.set_global(SettingsStore::test(cx));
|
||||||
|
crate::init(&client);
|
||||||
|
client::init(&client, cx);
|
||||||
|
|
||||||
|
cx.add_model(|cx| ChannelStore::new(client, user_store, cx))
|
||||||
|
}
|
||||||
|
|
||||||
fn update_channels(
|
fn update_channels(
|
||||||
channel_store: &ModelHandle<ChannelStore>,
|
channel_store: &ModelHandle<ChannelStore>,
|
||||||
message: proto::UpdateChannels,
|
message: proto::UpdateChannels,
|
||||||
|
|
|
@ -170,8 +170,7 @@ impl FakeServer {
|
||||||
staff: false,
|
staff: false,
|
||||||
flags: Default::default(),
|
flags: Default::default(),
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
.await;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,11 +181,7 @@ impl FakeServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn respond<T: proto::RequestMessage>(
|
pub fn respond<T: proto::RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) {
|
||||||
&self,
|
|
||||||
receipt: Receipt<T>,
|
|
||||||
response: T::Response,
|
|
||||||
) {
|
|
||||||
self.peer.respond(receipt, response).unwrap()
|
self.peer.respond(receipt, response).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,7 +82,7 @@ impl Database {
|
||||||
id: row.id.to_proto(),
|
id: row.id.to_proto(),
|
||||||
sender_id: row.sender_id.to_proto(),
|
sender_id: row.sender_id.to_proto(),
|
||||||
body: row.body,
|
body: row.body,
|
||||||
timestamp: row.sent_at.unix_timestamp() as u64,
|
timestamp: row.sent_at.assume_utc().unix_timestamp() as u64,
|
||||||
nonce: Some(proto::Nonce {
|
nonce: Some(proto::Nonce {
|
||||||
upper_half: nonce.0,
|
upper_half: nonce.0,
|
||||||
lower_half: nonce.1,
|
lower_half: nonce.1,
|
||||||
|
@ -124,6 +124,9 @@ impl Database {
|
||||||
Err(anyhow!("not a chat participant"))?;
|
Err(anyhow!("not a chat participant"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let timestamp = timestamp.to_offset(time::UtcOffset::UTC);
|
||||||
|
let timestamp = time::PrimitiveDateTime::new(timestamp.date(), timestamp.time());
|
||||||
|
|
||||||
let message = channel_message::Entity::insert(channel_message::ActiveModel {
|
let message = channel_message::Entity::insert(channel_message::ActiveModel {
|
||||||
channel_id: ActiveValue::Set(channel_id),
|
channel_id: ActiveValue::Set(channel_id),
|
||||||
sender_id: ActiveValue::Set(user_id),
|
sender_id: ActiveValue::Set(user_id),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::db::{ChannelId, MessageId, UserId};
|
use crate::db::{ChannelId, MessageId, UserId};
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use time::OffsetDateTime;
|
use time::PrimitiveDateTime;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "channel_messages")]
|
#[sea_orm(table_name = "channel_messages")]
|
||||||
|
@ -10,7 +10,7 @@ pub struct Model {
|
||||||
pub channel_id: ChannelId,
|
pub channel_id: ChannelId,
|
||||||
pub sender_id: UserId,
|
pub sender_id: UserId,
|
||||||
pub body: String,
|
pub body: String,
|
||||||
pub sent_at: OffsetDateTime,
|
pub sent_at: PrimitiveDateTime,
|
||||||
pub nonce: Uuid,
|
pub nonce: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,12 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
||||||
|
|
||||||
|
db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let msg1_id = db
|
let msg1_id = db
|
||||||
.create_channel_message(channel, user, "1", OffsetDateTime::now_utc(), 1)
|
.create_channel_message(channel, user, "1", OffsetDateTime::now_utc(), 1)
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -1,21 +1,33 @@
|
||||||
|
use crate::collab_panel::{CollaborationPanelDockPosition, CollaborationPanelSettings};
|
||||||
|
use anyhow::Result;
|
||||||
use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelStore};
|
use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelStore};
|
||||||
use client::Client;
|
use client::Client;
|
||||||
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
elements::*,
|
elements::*,
|
||||||
platform::{CursorStyle, MouseButton},
|
platform::{CursorStyle, MouseButton},
|
||||||
|
serde_json,
|
||||||
views::{ItemType, Select, SelectStyle},
|
views::{ItemType, Select, SelectStyle},
|
||||||
AnyViewHandle, AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle,
|
AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
|
||||||
|
ViewContext, ViewHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use language::language_settings::SoftWrap;
|
use language::language_settings::SoftWrap;
|
||||||
use menu::Confirm;
|
use menu::Confirm;
|
||||||
|
use project::Fs;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use theme::Theme;
|
use theme::Theme;
|
||||||
use time::{OffsetDateTime, UtcOffset};
|
use time::{OffsetDateTime, UtcOffset};
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
|
use workspace::{
|
||||||
|
dock::{DockPosition, Panel},
|
||||||
|
Workspace,
|
||||||
|
};
|
||||||
|
|
||||||
const MESSAGE_LOADING_THRESHOLD: usize = 50;
|
const MESSAGE_LOADING_THRESHOLD: usize = 50;
|
||||||
|
const CHAT_PANEL_KEY: &'static str = "ChatPanel";
|
||||||
|
|
||||||
pub struct ChatPanel {
|
pub struct ChatPanel {
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
|
@ -25,11 +37,25 @@ pub struct ChatPanel {
|
||||||
input_editor: ViewHandle<Editor>,
|
input_editor: ViewHandle<Editor>,
|
||||||
channel_select: ViewHandle<Select>,
|
channel_select: ViewHandle<Select>,
|
||||||
local_timezone: UtcOffset,
|
local_timezone: UtcOffset,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
width: Option<f32>,
|
||||||
|
pending_serialization: Task<Option<()>>,
|
||||||
|
has_focus: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Event {}
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct SerializedChatPanel {
|
||||||
|
width: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
actions!(chat_panel, [LoadMoreMessages]);
|
#[derive(Debug)]
|
||||||
|
pub enum Event {
|
||||||
|
DockPositionChanged,
|
||||||
|
Focus,
|
||||||
|
Dismissed,
|
||||||
|
}
|
||||||
|
|
||||||
|
actions!(chat_panel, [LoadMoreMessages, ToggleFocus]);
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(ChatPanel::send);
|
cx.add_action(ChatPanel::send);
|
||||||
|
@ -37,11 +63,11 @@ pub fn init(cx: &mut AppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatPanel {
|
impl ChatPanel {
|
||||||
pub fn new(
|
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
|
||||||
rpc: Arc<Client>,
|
let fs = workspace.app_state().fs.clone();
|
||||||
channel_list: ModelHandle<ChannelStore>,
|
let client = workspace.app_state().client.clone();
|
||||||
cx: &mut ViewContext<Self>,
|
let channel_store = workspace.app_state().channel_store.clone();
|
||||||
) -> Self {
|
|
||||||
let input_editor = cx.add_view(|cx| {
|
let input_editor = cx.add_view(|cx| {
|
||||||
let mut editor = Editor::auto_height(
|
let mut editor = Editor::auto_height(
|
||||||
4,
|
4,
|
||||||
|
@ -51,12 +77,13 @@ impl ChatPanel {
|
||||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
let channel_select = cx.add_view(|cx| {
|
let channel_select = cx.add_view(|cx| {
|
||||||
let channel_list = channel_list.clone();
|
let channel_store = channel_store.clone();
|
||||||
Select::new(0, cx, {
|
Select::new(0, cx, {
|
||||||
move |ix, item_type, is_hovered, cx| {
|
move |ix, item_type, is_hovered, cx| {
|
||||||
Self::render_channel_name(
|
Self::render_channel_name(
|
||||||
&channel_list,
|
&channel_store,
|
||||||
ix,
|
ix,
|
||||||
item_type,
|
item_type,
|
||||||
is_hovered,
|
is_hovered,
|
||||||
|
@ -85,45 +112,97 @@ impl ChatPanel {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut this = Self {
|
cx.add_view(|cx| {
|
||||||
client: rpc,
|
let mut this = Self {
|
||||||
channel_store: channel_list,
|
fs,
|
||||||
active_channel: Default::default(),
|
client,
|
||||||
message_list,
|
channel_store,
|
||||||
input_editor,
|
active_channel: Default::default(),
|
||||||
channel_select,
|
pending_serialization: Task::ready(None),
|
||||||
local_timezone: cx.platform().local_timezone(),
|
message_list,
|
||||||
};
|
input_editor,
|
||||||
|
channel_select,
|
||||||
|
local_timezone: cx.platform().local_timezone(),
|
||||||
|
has_focus: false,
|
||||||
|
width: None,
|
||||||
|
};
|
||||||
|
|
||||||
this.init_active_channel(cx);
|
|
||||||
cx.observe(&this.channel_store, |this, _, cx| {
|
|
||||||
this.init_active_channel(cx);
|
this.init_active_channel(cx);
|
||||||
})
|
cx.observe(&this.channel_store, |this, _, cx| {
|
||||||
.detach();
|
this.init_active_channel(cx);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
cx.observe(&this.channel_select, |this, channel_select, cx| {
|
cx.observe(&this.channel_select, |this, channel_select, cx| {
|
||||||
let selected_ix = channel_select.read(cx).selected_index();
|
let selected_ix = channel_select.read(cx).selected_index();
|
||||||
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_index(selected_ix)
|
||||||
.map(|e| e.1.id);
|
.map(|e| e.1.id);
|
||||||
if let Some(selected_channel_id) = selected_channel_id {
|
if let Some(selected_channel_id) = selected_channel_id {
|
||||||
let open_chat = this.channel_store.update(cx, |store, cx| {
|
let open_chat = this.channel_store.update(cx, |store, cx| {
|
||||||
store.open_channel_chat(selected_channel_id, cx)
|
store.open_channel_chat(selected_channel_id, cx)
|
||||||
});
|
});
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let chat = open_chat.await?;
|
let chat = open_chat.await?;
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.set_active_channel(chat, cx);
|
this.set_active_channel(chat, cx);
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
.detach_and_log_err(cx);
|
||||||
.detach_and_log_err(cx);
|
}
|
||||||
}
|
})
|
||||||
})
|
.detach();
|
||||||
.detach();
|
|
||||||
|
|
||||||
this
|
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()
|
||||||
|
.spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
|
||||||
|
.await
|
||||||
|
.log_err()
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
let panel = Self::new(workspace, cx);
|
||||||
|
if let Some(serialized_panel) = serialized_panel {
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
panel.width = serialized_panel.width;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
panel
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
let width = self.width;
|
||||||
|
self.pending_serialization = cx.background().spawn(
|
||||||
|
async move {
|
||||||
|
KEY_VALUE_STORE
|
||||||
|
.write_kvp(
|
||||||
|
CHAT_PANEL_KEY.into(),
|
||||||
|
serde_json::to_string(&SerializedChatPanel { width })?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
.log_err(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_active_channel(&mut self, cx: &mut ViewContext<Self>) {
|
fn init_active_channel(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -365,6 +444,68 @@ impl View for ChatPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Panel for ChatPanel {
|
||||||
|
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
|
||||||
|
match settings::get::<CollaborationPanelSettings>(cx).dock {
|
||||||
|
CollaborationPanelDockPosition::Left => DockPosition::Left,
|
||||||
|
CollaborationPanelDockPosition::Right => DockPosition::Right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>) {
|
||||||
|
settings::update_settings_file::<CollaborationPanelSettings>(
|
||||||
|
self.fs.clone(),
|
||||||
|
cx,
|
||||||
|
move |settings| {
|
||||||
|
let dock = match position {
|
||||||
|
DockPosition::Left | DockPosition::Bottom => {
|
||||||
|
CollaborationPanelDockPosition::Left
|
||||||
|
}
|
||||||
|
DockPosition::Right => CollaborationPanelDockPosition::Right,
|
||||||
|
};
|
||||||
|
settings.dock = Some(dock);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self, cx: &gpui::WindowContext) -> f32 {
|
||||||
|
self.width
|
||||||
|
.unwrap_or_else(|| settings::get::<CollaborationPanelSettings>(cx).default_width)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
|
||||||
|
self.width = size;
|
||||||
|
self.serialize(cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
|
||||||
|
settings::get::<CollaborationPanelSettings>(cx)
|
||||||
|
.button
|
||||||
|
.then(|| "icons/conversations.svg")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
|
||||||
|
("Chat Panel".to_string(), Some(Box::new(ToggleFocus)))
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn format_timestamp(
|
fn format_timestamp(
|
||||||
mut timestamp: OffsetDateTime,
|
mut timestamp: OffsetDateTime,
|
||||||
mut now: OffsetDateTime,
|
mut now: OffsetDateTime,
|
||||||
|
|
|
@ -2,14 +2,18 @@ mod channel_modal;
|
||||||
mod contact_finder;
|
mod contact_finder;
|
||||||
mod panel_settings;
|
mod panel_settings;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
channel_view::{self, ChannelView},
|
||||||
|
face_pile::FacePile,
|
||||||
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
|
use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
|
||||||
|
use channel_modal::ChannelModal;
|
||||||
use client::{proto::PeerId, Client, Contact, User, UserStore};
|
use client::{proto::PeerId, Client, Contact, User, UserStore};
|
||||||
use context_menu::{ContextMenu, ContextMenuItem};
|
use context_menu::{ContextMenu, ContextMenuItem};
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::{Cancel, Editor};
|
use editor::{Cancel, Editor};
|
||||||
|
|
||||||
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
|
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use fuzzy::{match_strings, StringMatchCandidate};
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
|
@ -31,7 +35,6 @@ use gpui::{
|
||||||
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use menu::{Confirm, SelectNext, SelectPrev};
|
use menu::{Confirm, SelectNext, SelectPrev};
|
||||||
use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings};
|
|
||||||
use project::{Fs, Project};
|
use project::{Fs, Project};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
|
@ -44,11 +47,7 @@ use workspace::{
|
||||||
Workspace,
|
Workspace,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
pub use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings};
|
||||||
channel_view::{self, ChannelView},
|
|
||||||
face_pile::FacePile,
|
|
||||||
};
|
|
||||||
use channel_modal::ChannelModal;
|
|
||||||
|
|
||||||
use self::contact_finder::ContactFinder;
|
use self::contact_finder::ContactFinder;
|
||||||
|
|
||||||
|
@ -113,7 +112,7 @@ impl_actions!(
|
||||||
|
|
||||||
const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
|
const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
|
||||||
|
|
||||||
pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
settings::register::<panel_settings::CollaborationPanelSettings>(cx);
|
settings::register::<panel_settings::CollaborationPanelSettings>(cx);
|
||||||
contact_finder::init(cx);
|
contact_finder::init(cx);
|
||||||
channel_modal::init(cx);
|
channel_modal::init(cx);
|
||||||
|
|
|
@ -32,7 +32,8 @@ actions!(
|
||||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||||
vcs_menu::init(cx);
|
vcs_menu::init(cx);
|
||||||
collab_titlebar_item::init(cx);
|
collab_titlebar_item::init(cx);
|
||||||
collab_panel::init(app_state.client.clone(), cx);
|
collab_panel::init(cx);
|
||||||
|
chat_panel::init(cx);
|
||||||
incoming_call_notification::init(&app_state, cx);
|
incoming_call_notification::init(&app_state, cx);
|
||||||
project_shared_notification::init(&app_state, cx);
|
project_shared_notification::init(&app_state, cx);
|
||||||
sharing_status_indicator::init(cx);
|
sharing_status_indicator::init(cx);
|
||||||
|
|
|
@ -214,6 +214,13 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
|
||||||
workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(cx);
|
workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(cx);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
cx.add_action(
|
||||||
|
|workspace: &mut Workspace,
|
||||||
|
_: &collab_ui::chat_panel::ToggleFocus,
|
||||||
|
cx: &mut ViewContext<Workspace>| {
|
||||||
|
workspace.toggle_panel_focus::<collab_ui::chat_panel::ChatPanel>(cx);
|
||||||
|
},
|
||||||
|
);
|
||||||
cx.add_action(
|
cx.add_action(
|
||||||
|workspace: &mut Workspace,
|
|workspace: &mut Workspace,
|
||||||
_: &terminal_panel::ToggleFocus,
|
_: &terminal_panel::ToggleFocus,
|
||||||
|
@ -338,11 +345,14 @@ pub fn initialize_workspace(
|
||||||
let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
|
let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
|
||||||
let channels_panel =
|
let channels_panel =
|
||||||
collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
|
collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
|
||||||
let (project_panel, terminal_panel, assistant_panel, channels_panel) = futures::try_join!(
|
let chat_panel =
|
||||||
|
collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
|
||||||
|
let (project_panel, terminal_panel, assistant_panel, channels_panel, chat_panel) = futures::try_join!(
|
||||||
project_panel,
|
project_panel,
|
||||||
terminal_panel,
|
terminal_panel,
|
||||||
assistant_panel,
|
assistant_panel,
|
||||||
channels_panel
|
channels_panel,
|
||||||
|
chat_panel,
|
||||||
)?;
|
)?;
|
||||||
workspace_handle.update(&mut cx, |workspace, cx| {
|
workspace_handle.update(&mut cx, |workspace, cx| {
|
||||||
let project_panel_position = project_panel.position(cx);
|
let project_panel_position = project_panel.position(cx);
|
||||||
|
@ -362,6 +372,7 @@ pub fn initialize_workspace(
|
||||||
workspace.add_panel(terminal_panel, cx);
|
workspace.add_panel(terminal_panel, cx);
|
||||||
workspace.add_panel(assistant_panel, cx);
|
workspace.add_panel(assistant_panel, cx);
|
||||||
workspace.add_panel(channels_panel, cx);
|
workspace.add_panel(channels_panel, cx);
|
||||||
|
workspace.add_panel(chat_panel, cx);
|
||||||
|
|
||||||
if !was_deserialized
|
if !was_deserialized
|
||||||
&& workspace
|
&& workspace
|
||||||
|
|
|
@ -12,6 +12,7 @@ import simple_message_notification from "./simple_message_notification"
|
||||||
import project_shared_notification from "./project_shared_notification"
|
import project_shared_notification from "./project_shared_notification"
|
||||||
import tooltip from "./tooltip"
|
import tooltip from "./tooltip"
|
||||||
import terminal from "./terminal"
|
import terminal from "./terminal"
|
||||||
|
import chat_panel from "./chat_panel"
|
||||||
import collab_panel from "./collab_panel"
|
import collab_panel from "./collab_panel"
|
||||||
import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
|
import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
|
||||||
import incoming_call_notification from "./incoming_call_notification"
|
import incoming_call_notification from "./incoming_call_notification"
|
||||||
|
@ -55,6 +56,7 @@ export default function app(): any {
|
||||||
terminal: terminal(),
|
terminal: terminal(),
|
||||||
assistant: assistant(),
|
assistant: assistant(),
|
||||||
feedback: feedback(),
|
feedback: feedback(),
|
||||||
|
chat_panel: chat_panel(),
|
||||||
component_test: component_test(),
|
component_test: component_test(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
111
styles/src/style_tree/chat_panel.ts
Normal file
111
styles/src/style_tree/chat_panel.ts
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import {
|
||||||
|
background,
|
||||||
|
border,
|
||||||
|
border_color,
|
||||||
|
foreground,
|
||||||
|
text,
|
||||||
|
} from "./components"
|
||||||
|
import { interactive, toggleable } from "../element"
|
||||||
|
import { useTheme } from "../theme"
|
||||||
|
import collab_modals from "./collab_modals"
|
||||||
|
import { icon_button, toggleable_icon_button } from "../component/icon_button"
|
||||||
|
import { indicator } from "../component/indicator"
|
||||||
|
|
||||||
|
export default function contacts_panel(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
const CHANNEL_SPACING = 4 as const
|
||||||
|
const NAME_MARGIN = 6 as const
|
||||||
|
const SPACING = 12 as const
|
||||||
|
const INDENT_SIZE = 8 as const
|
||||||
|
const ITEM_HEIGHT = 28 as const
|
||||||
|
|
||||||
|
const layer = theme.middle
|
||||||
|
|
||||||
|
const input_editor = {
|
||||||
|
background: background(layer, "on"),
|
||||||
|
corner_radius: 6,
|
||||||
|
text: text(layer, "sans", "base"),
|
||||||
|
placeholder_text: text(layer, "sans", "base", "disabled", {
|
||||||
|
size: "xs",
|
||||||
|
}),
|
||||||
|
selection: theme.players[0],
|
||||||
|
border: border(layer, "on"),
|
||||||
|
padding: {
|
||||||
|
bottom: 4,
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
top: 4,
|
||||||
|
},
|
||||||
|
margin: {
|
||||||
|
left: SPACING,
|
||||||
|
right: SPACING,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel_name = {
|
||||||
|
padding: {
|
||||||
|
top: 4,
|
||||||
|
bottom: 4,
|
||||||
|
left: 4,
|
||||||
|
right: 4,
|
||||||
|
},
|
||||||
|
hash: {
|
||||||
|
...text(layer, "sans", "base"),
|
||||||
|
},
|
||||||
|
name: text(layer, "sans", "base"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
background: background(layer),
|
||||||
|
channel_select: {
|
||||||
|
header: channel_name,
|
||||||
|
item: channel_name,
|
||||||
|
active_item: channel_name,
|
||||||
|
hovered_item: channel_name,
|
||||||
|
hovered_active_item: channel_name,
|
||||||
|
menu: {
|
||||||
|
padding: {
|
||||||
|
top: 10,
|
||||||
|
bottom: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
input_editor,
|
||||||
|
message: {
|
||||||
|
body: text(layer, "sans", "base"),
|
||||||
|
sender: {
|
||||||
|
padding: {
|
||||||
|
left: 4,
|
||||||
|
right: 4,
|
||||||
|
},
|
||||||
|
...text(layer, "sans", "base", "disabled"),
|
||||||
|
},
|
||||||
|
timestamp: text(layer, "sans", "base"),
|
||||||
|
},
|
||||||
|
pending_message: {
|
||||||
|
body: text(layer, "sans", "base"),
|
||||||
|
sender: {
|
||||||
|
padding: {
|
||||||
|
left: 4,
|
||||||
|
right: 4,
|
||||||
|
},
|
||||||
|
...text(layer, "sans", "base", "disabled"),
|
||||||
|
},
|
||||||
|
timestamp: text(layer, "sans", "base"),
|
||||||
|
},
|
||||||
|
sign_in_prompt: {
|
||||||
|
default: text(layer, "sans", "base"),
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
body: text(layer, "sans", "base"),
|
||||||
|
sender: {
|
||||||
|
padding: {
|
||||||
|
left: 4,
|
||||||
|
right: 4,
|
||||||
|
},
|
||||||
|
...text(layer, "sans", "base", "disabled"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue