mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-27 02:48:34 +00:00
Wire up UI for requesting contacts and cancelling requests
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
parent
e4f1952657
commit
e3ee19b123
7 changed files with 212 additions and 32 deletions
|
@ -5,7 +5,7 @@ use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
|
||||||
use postage::{prelude::Stream, sink::Sink, watch};
|
use postage::{prelude::Stream, sink::Sink, watch};
|
||||||
use rpc::proto::{RequestMessage, UsersResponse};
|
use rpc::proto::{RequestMessage, UsersResponse};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{hash_map::Entry, HashMap, HashSet},
|
||||||
sync::{Arc, Weak},
|
sync::{Arc, Weak},
|
||||||
};
|
};
|
||||||
use util::TryFutureExt as _;
|
use util::TryFutureExt as _;
|
||||||
|
@ -31,6 +31,14 @@ pub struct ProjectMetadata {
|
||||||
pub guests: Vec<Arc<User>>,
|
pub guests: Vec<Arc<User>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum ContactRequestStatus {
|
||||||
|
None,
|
||||||
|
SendingRequest,
|
||||||
|
Requested,
|
||||||
|
RequestAccepted,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct UserStore {
|
pub struct UserStore {
|
||||||
users: HashMap<u64, Arc<User>>,
|
users: HashMap<u64, Arc<User>>,
|
||||||
update_contacts_tx: watch::Sender<Option<proto::UpdateContacts>>,
|
update_contacts_tx: watch::Sender<Option<proto::UpdateContacts>>,
|
||||||
|
@ -38,6 +46,7 @@ pub struct UserStore {
|
||||||
contacts: Vec<Arc<Contact>>,
|
contacts: Vec<Arc<Contact>>,
|
||||||
incoming_contact_requests: Vec<Arc<User>>,
|
incoming_contact_requests: Vec<Arc<User>>,
|
||||||
outgoing_contact_requests: Vec<Arc<User>>,
|
outgoing_contact_requests: Vec<Arc<User>>,
|
||||||
|
pending_contact_requests: HashMap<u64, usize>,
|
||||||
client: Weak<Client>,
|
client: Weak<Client>,
|
||||||
http: Arc<dyn HttpClient>,
|
http: Arc<dyn HttpClient>,
|
||||||
_maintain_contacts: Task<()>,
|
_maintain_contacts: Task<()>,
|
||||||
|
@ -100,6 +109,7 @@ impl UserStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
pending_contact_requests: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,23 +247,85 @@ impl UserStore {
|
||||||
&self.outgoing_contact_requests
|
&self.outgoing_contact_requests
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_outgoing_contact_request(&self, user: &User) -> bool {
|
pub fn contact_request_status(&self, user: &User) -> ContactRequestStatus {
|
||||||
self.outgoing_contact_requests
|
if self
|
||||||
.binary_search_by_key(&&user.github_login, |requested_user| {
|
.contacts
|
||||||
&requested_user.github_login
|
.binary_search_by_key(&&user.id, |contact| &contact.user.id)
|
||||||
})
|
|
||||||
.is_ok()
|
.is_ok()
|
||||||
|
{
|
||||||
|
ContactRequestStatus::RequestAccepted
|
||||||
|
} else if self
|
||||||
|
.outgoing_contact_requests
|
||||||
|
.binary_search_by_key(&&user.id, |user| &user.id)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
ContactRequestStatus::Requested
|
||||||
|
} else if self.pending_contact_requests.contains_key(&user.id) {
|
||||||
|
ContactRequestStatus::SendingRequest
|
||||||
|
} else {
|
||||||
|
ContactRequestStatus::None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_contact(&self, responder_id: u64) -> impl Future<Output = Result<()>> {
|
pub fn request_contact(
|
||||||
|
&mut self,
|
||||||
|
responder_id: u64,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
let client = self.client.upgrade();
|
let client = self.client.upgrade();
|
||||||
async move {
|
*self
|
||||||
client
|
.pending_contact_requests
|
||||||
.ok_or_else(|| anyhow!("not logged in"))?
|
.entry(responder_id)
|
||||||
.request(proto::RequestContact { responder_id })
|
.or_insert(0) += 1;
|
||||||
.await?;
|
cx.notify();
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let request = client
|
||||||
|
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
|
||||||
|
.request(proto::RequestContact { responder_id });
|
||||||
|
request.await?;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
if let Entry::Occupied(mut request_count) =
|
||||||
|
this.pending_contact_requests.entry(responder_id)
|
||||||
|
{
|
||||||
|
*request_count.get_mut() -= 1;
|
||||||
|
if *request_count.get() == 0 {
|
||||||
|
request_count.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_contact(
|
||||||
|
&mut self,
|
||||||
|
user_id: u64,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let client = self.client.upgrade();
|
||||||
|
*self.pending_contact_requests.entry(user_id).or_insert(0) += 1;
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let request = client
|
||||||
|
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
|
||||||
|
.request(proto::RemoveContact { user_id });
|
||||||
|
request.await?;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
if let Entry::Occupied(mut request_count) =
|
||||||
|
this.pending_contact_requests.entry(user_id)
|
||||||
|
{
|
||||||
|
*request_count.get_mut() -= 1;
|
||||||
|
if *request_count.get() == 0 {
|
||||||
|
request_count.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn respond_to_contact_request(
|
pub fn respond_to_contact_request(
|
||||||
|
|
|
@ -19,6 +19,7 @@ pub trait Db: Send + Sync {
|
||||||
|
|
||||||
async fn get_contacts(&self, id: UserId) -> Result<Contacts>;
|
async fn get_contacts(&self, id: UserId) -> Result<Contacts>;
|
||||||
async fn send_contact_request(&self, requester_id: UserId, responder_id: UserId) -> Result<()>;
|
async fn send_contact_request(&self, requester_id: UserId, responder_id: UserId) -> Result<()>;
|
||||||
|
async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()>;
|
||||||
async fn dismiss_contact_request(
|
async fn dismiss_contact_request(
|
||||||
&self,
|
&self,
|
||||||
responder_id: UserId,
|
responder_id: UserId,
|
||||||
|
@ -267,6 +268,30 @@ impl Db for PostgresDb {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> {
|
||||||
|
let (id_a, id_b, a_to_b) = if responder_id < requester_id {
|
||||||
|
(responder_id, requester_id, false)
|
||||||
|
} else {
|
||||||
|
(requester_id, responder_id, true)
|
||||||
|
};
|
||||||
|
let query = "
|
||||||
|
DELETE FROM contacts
|
||||||
|
WHERE user_id_a = $1 AND user_id_b = $2 AND a_to_b = $3;
|
||||||
|
";
|
||||||
|
let result = sqlx::query(query)
|
||||||
|
.bind(id_a.0)
|
||||||
|
.bind(id_b.0)
|
||||||
|
.bind(a_to_b)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if result.rows_affected() == 1 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("no such contact"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn respond_to_contact_request(
|
async fn respond_to_contact_request(
|
||||||
&self,
|
&self,
|
||||||
responder_id: UserId,
|
responder_id: UserId,
|
||||||
|
@ -1248,6 +1273,13 @@ pub mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> {
|
||||||
|
self.contacts.lock().retain(|contact| {
|
||||||
|
!(contact.requester_id == requester_id && contact.responder_id == responder_id)
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn dismiss_contact_request(
|
async fn dismiss_contact_request(
|
||||||
&self,
|
&self,
|
||||||
responder_id: UserId,
|
responder_id: UserId,
|
||||||
|
|
|
@ -155,6 +155,7 @@ impl Server {
|
||||||
.add_request_handler(Server::get_users)
|
.add_request_handler(Server::get_users)
|
||||||
.add_request_handler(Server::fuzzy_search_users)
|
.add_request_handler(Server::fuzzy_search_users)
|
||||||
.add_request_handler(Server::request_contact)
|
.add_request_handler(Server::request_contact)
|
||||||
|
.add_request_handler(Server::remove_contact)
|
||||||
.add_request_handler(Server::respond_to_contact_request)
|
.add_request_handler(Server::respond_to_contact_request)
|
||||||
.add_request_handler(Server::join_channel)
|
.add_request_handler(Server::join_channel)
|
||||||
.add_message_handler(Server::leave_channel)
|
.add_message_handler(Server::leave_channel)
|
||||||
|
@ -1048,6 +1049,43 @@ impl Server {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn remove_contact(
|
||||||
|
self: Arc<Server>,
|
||||||
|
request: TypedEnvelope<proto::RemoveContact>,
|
||||||
|
response: Response<proto::RemoveContact>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let requester_id = self
|
||||||
|
.store()
|
||||||
|
.await
|
||||||
|
.user_id_for_connection(request.sender_id)?;
|
||||||
|
let responder_id = UserId::from_proto(request.payload.user_id);
|
||||||
|
self.app_state
|
||||||
|
.db
|
||||||
|
.remove_contact(requester_id, responder_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Update outgoing contact requests of requester
|
||||||
|
let mut update = proto::UpdateContacts::default();
|
||||||
|
update
|
||||||
|
.remove_outgoing_requests
|
||||||
|
.push(responder_id.to_proto());
|
||||||
|
for connection_id in self.store().await.connection_ids_for_user(requester_id) {
|
||||||
|
self.peer.send(connection_id, update.clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update incoming contact requests of responder
|
||||||
|
let mut update = proto::UpdateContacts::default();
|
||||||
|
update
|
||||||
|
.remove_incoming_requests
|
||||||
|
.push(requester_id.to_proto());
|
||||||
|
for connection_id in self.store().await.connection_ids_for_user(responder_id) {
|
||||||
|
self.peer.send(connection_id, update.clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.send(proto::Ack {})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// #[instrument(skip(self, state, user_ids))]
|
// #[instrument(skip(self, state, user_ids))]
|
||||||
// fn update_contacts_for_users<'a>(
|
// fn update_contacts_for_users<'a>(
|
||||||
// self: &Arc<Self>,
|
// self: &Arc<Self>,
|
||||||
|
@ -5138,15 +5176,15 @@ mod tests {
|
||||||
// User A and User C request that user B become their contact.
|
// User A and User C request that user B become their contact.
|
||||||
client_a
|
client_a
|
||||||
.user_store
|
.user_store
|
||||||
.read_with(cx_a, |store, _| {
|
.update(cx_a, |store, cx| {
|
||||||
store.request_contact(client_b.user_id().unwrap())
|
store.request_contact(client_b.user_id().unwrap(), cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
client_c
|
client_c
|
||||||
.user_store
|
.user_store
|
||||||
.read_with(cx_c, |store, _| {
|
.update(cx_c, |store, cx| {
|
||||||
store.request_contact(client_b.user_id().unwrap())
|
store.request_contact(client_b.user_id().unwrap(), cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -6460,8 +6498,8 @@ mod tests {
|
||||||
for (client_b, cx_b) in &mut clients {
|
for (client_b, cx_b) in &mut clients {
|
||||||
client_a
|
client_a
|
||||||
.user_store
|
.user_store
|
||||||
.update(cx_a, |store, _| {
|
.update(cx_a, |store, cx| {
|
||||||
store.request_contact(client_b.user_id().unwrap())
|
store.request_contact(client_b.user_id().unwrap(), cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use client::{Contact, User, UserStore};
|
use client::{Contact, ContactRequestStatus, User, UserStore};
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use fuzzy::StringMatchCandidate;
|
use fuzzy::StringMatchCandidate;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -7,8 +7,8 @@ use gpui::{
|
||||||
geometry::{rect::RectF, vector::vec2f},
|
geometry::{rect::RectF, vector::vec2f},
|
||||||
impl_actions,
|
impl_actions,
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, Task,
|
Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext,
|
||||||
View, ViewContext, ViewHandle,
|
Subscription, Task, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
@ -16,7 +16,7 @@ use std::sync::Arc;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{AppState, JoinProject};
|
use workspace::{AppState, JoinProject};
|
||||||
|
|
||||||
impl_actions!(contacts_panel, [RequestContact]);
|
impl_actions!(contacts_panel, [RequestContact, RemoveContact]);
|
||||||
|
|
||||||
pub struct ContactsPanel {
|
pub struct ContactsPanel {
|
||||||
list_state: ListState,
|
list_state: ListState,
|
||||||
|
@ -31,6 +31,14 @@ pub struct ContactsPanel {
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
pub struct RequestContact(pub u64);
|
pub struct RequestContact(pub u64);
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct RemoveContact(pub u64);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
cx.add_action(ContactsPanel::request_contact);
|
||||||
|
cx.add_action(ContactsPanel::remove_contact);
|
||||||
|
}
|
||||||
|
|
||||||
impl ContactsPanel {
|
impl ContactsPanel {
|
||||||
pub fn new(app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> Self {
|
pub fn new(app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> Self {
|
||||||
let user_query_editor = cx.add_view(|cx| {
|
let user_query_editor = cx.add_view(|cx| {
|
||||||
|
@ -295,7 +303,7 @@ impl ContactsPanel {
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
enum RequestContactButton {}
|
enum RequestContactButton {}
|
||||||
|
|
||||||
let requested_contact = user_store.read(cx).has_outgoing_contact_request(&contact);
|
let request_status = user_store.read(cx).contact_request_status(&contact);
|
||||||
|
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_children(contact.avatar.clone().map(|avatar| {
|
.with_children(contact.avatar.clone().map(|avatar| {
|
||||||
|
@ -321,7 +329,13 @@ impl ContactsPanel {
|
||||||
contact.id as usize,
|
contact.id as usize,
|
||||||
cx,
|
cx,
|
||||||
|_, _| {
|
|_, _| {
|
||||||
let label = if requested_contact { "-" } else { "+" };
|
let label = match request_status {
|
||||||
|
ContactRequestStatus::None => "+",
|
||||||
|
ContactRequestStatus::SendingRequest => "…",
|
||||||
|
ContactRequestStatus::Requested => "-",
|
||||||
|
ContactRequestStatus::RequestAccepted => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
Label::new(label.to_string(), theme.edit_contact.text.clone())
|
Label::new(label.to_string(), theme.edit_contact.text.clone())
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(theme.edit_contact.container)
|
.with_style(theme.edit_contact.container)
|
||||||
|
@ -330,12 +344,16 @@ impl ContactsPanel {
|
||||||
.boxed()
|
.boxed()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.on_click(move |_, cx| {
|
.on_click(move |_, cx| match request_status {
|
||||||
if requested_contact {
|
ContactRequestStatus::None => {
|
||||||
} else {
|
|
||||||
cx.dispatch_action(RequestContact(contact.id));
|
cx.dispatch_action(RequestContact(contact.id));
|
||||||
}
|
}
|
||||||
|
ContactRequestStatus::Requested => {
|
||||||
|
cx.dispatch_action(RemoveContact(contact.id));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
})
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.constrained()
|
.constrained()
|
||||||
|
@ -415,6 +433,18 @@ impl ContactsPanel {
|
||||||
None
|
None
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn request_contact(&mut self, request: &RequestContact, cx: &mut ViewContext<Self>) {
|
||||||
|
self.user_store
|
||||||
|
.update(cx, |store, cx| store.request_contact(request.0, cx))
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext<Self>) {
|
||||||
|
self.user_store
|
||||||
|
.update(cx, |store, cx| store.remove_contact(request.0, cx))
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Event {}
|
pub enum Event {}
|
||||||
|
|
|
@ -91,11 +91,12 @@ message Envelope {
|
||||||
UsersResponse users_response = 78;
|
UsersResponse users_response = 78;
|
||||||
RequestContact request_contact = 79;
|
RequestContact request_contact = 79;
|
||||||
RespondToContactRequest respond_to_contact_request = 80;
|
RespondToContactRequest respond_to_contact_request = 80;
|
||||||
|
RemoveContact remove_contact = 81;
|
||||||
|
|
||||||
Follow follow = 81;
|
Follow follow = 82;
|
||||||
FollowResponse follow_response = 82;
|
FollowResponse follow_response = 83;
|
||||||
UpdateFollowers update_followers = 83;
|
UpdateFollowers update_followers = 84;
|
||||||
Unfollow unfollow = 84;
|
Unfollow unfollow = 85;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -553,6 +554,10 @@ message RequestContact {
|
||||||
uint64 responder_id = 1;
|
uint64 responder_id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message RemoveContact {
|
||||||
|
uint64 user_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message RespondToContactRequest {
|
message RespondToContactRequest {
|
||||||
uint64 requester_id = 1;
|
uint64 requester_id = 1;
|
||||||
ContactRequestResponse response = 2;
|
ContactRequestResponse response = 2;
|
||||||
|
|
|
@ -82,6 +82,7 @@ messages!(
|
||||||
(ApplyCompletionAdditionalEditsResponse, Background),
|
(ApplyCompletionAdditionalEditsResponse, Background),
|
||||||
(BufferReloaded, Foreground),
|
(BufferReloaded, Foreground),
|
||||||
(BufferSaved, Foreground),
|
(BufferSaved, Foreground),
|
||||||
|
(RemoveContact, Foreground),
|
||||||
(ChannelMessageSent, Foreground),
|
(ChannelMessageSent, Foreground),
|
||||||
(CreateProjectEntry, Foreground),
|
(CreateProjectEntry, Foreground),
|
||||||
(DeleteProjectEntry, Foreground),
|
(DeleteProjectEntry, Foreground),
|
||||||
|
@ -188,6 +189,7 @@ request_messages!(
|
||||||
(RegisterWorktree, Ack),
|
(RegisterWorktree, Ack),
|
||||||
(ReloadBuffers, ReloadBuffersResponse),
|
(ReloadBuffers, ReloadBuffersResponse),
|
||||||
(RequestContact, Ack),
|
(RequestContact, Ack),
|
||||||
|
(RemoveContact, Ack),
|
||||||
(RespondToContactRequest, Ack),
|
(RespondToContactRequest, Ack),
|
||||||
(RenameProjectEntry, ProjectEntryResponse),
|
(RenameProjectEntry, ProjectEntryResponse),
|
||||||
(SaveBuffer, BufferSaved),
|
(SaveBuffer, BufferSaved),
|
||||||
|
|
|
@ -146,6 +146,7 @@ fn main() {
|
||||||
go_to_line::init(cx);
|
go_to_line::init(cx);
|
||||||
file_finder::init(cx);
|
file_finder::init(cx);
|
||||||
chat_panel::init(cx);
|
chat_panel::init(cx);
|
||||||
|
contacts_panel::init(cx);
|
||||||
outline::init(cx);
|
outline::init(cx);
|
||||||
project_symbols::init(cx);
|
project_symbols::init(cx);
|
||||||
project_panel::init(cx);
|
project_panel::init(cx);
|
||||||
|
|
Loading…
Reference in a new issue