Restore chat functionality with a very rough UI

This commit is contained in:
Max Brunsfeld 2023-09-07 18:06:05 -07:00
parent da5a77badf
commit ddda5a559b
13 changed files with 440 additions and 185 deletions

View file

@ -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"] }

View file

@ -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) {

View file

@ -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,

View file

@ -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()
} }

View file

@ -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),

View file

@ -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,
} }

View file

@ -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

View file

@ -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,

View file

@ -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);

View file

@ -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);

View file

@ -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

View file

@ -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(),
} }
} }

View 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"),
}
}
}
}