Render chat messages in ChatPanel
This commit is contained in:
parent
baded7d416
commit
405ff1d9db
4 changed files with 155 additions and 36 deletions
|
@ -1520,7 +1520,7 @@ mod tests {
|
||||||
fn channel_messages(channel: &Channel) -> Vec<(u64, String)> {
|
fn channel_messages(channel: &Channel) -> Vec<(u64, String)> {
|
||||||
channel
|
channel
|
||||||
.messages()
|
.messages()
|
||||||
.iter()
|
.cursor::<(), ()>()
|
||||||
.map(|m| (m.sender_id, m.body.clone()))
|
.map(|m| (m.sender_id, m.body.clone()))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,13 @@ use crate::{
|
||||||
util::log_async_errors,
|
util::log_async_errors,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use gpui::{Entity, ModelContext, ModelHandle, MutableAppContext, WeakModelHandle};
|
use gpui::{
|
||||||
|
sum_tree::{self, Bias, SumTree},
|
||||||
|
Entity, ModelContext, ModelHandle, MutableAppContext, WeakModelHandle,
|
||||||
|
};
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
collections::{hash_map, HashMap},
|
||||||
collections::{hash_map, BTreeSet, HashMap},
|
ops::Range,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use zrpc::{
|
use zrpc::{
|
||||||
|
@ -28,13 +31,14 @@ pub struct ChannelDetails {
|
||||||
|
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
details: ChannelDetails,
|
details: ChannelDetails,
|
||||||
messages: BTreeSet<ChannelMessage>,
|
messages: SumTree<ChannelMessage>,
|
||||||
pending_messages: Vec<PendingChannelMessage>,
|
pending_messages: Vec<PendingChannelMessage>,
|
||||||
next_local_message_id: u64,
|
next_local_message_id: u64,
|
||||||
rpc: Arc<Client>,
|
rpc: Arc<Client>,
|
||||||
_subscription: rpc::Subscription,
|
_subscription: rpc::Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct ChannelMessage {
|
pub struct ChannelMessage {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub sender_id: u64,
|
pub sender_id: u64,
|
||||||
|
@ -46,10 +50,26 @@ pub struct PendingChannelMessage {
|
||||||
local_id: u64,
|
local_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Event {}
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct ChannelMessageSummary {
|
||||||
|
max_id: u64,
|
||||||
|
count: Count,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Default)]
|
||||||
|
struct Count(usize);
|
||||||
|
|
||||||
|
pub enum ChannelListEvent {}
|
||||||
|
|
||||||
|
pub enum ChannelEvent {
|
||||||
|
Message {
|
||||||
|
old_range: Range<usize>,
|
||||||
|
message: ChannelMessage,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
impl Entity for ChannelList {
|
impl Entity for ChannelList {
|
||||||
type Event = Event;
|
type Event = ChannelListEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChannelList {
|
impl ChannelList {
|
||||||
|
@ -107,7 +127,7 @@ impl ChannelList {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for Channel {
|
impl Entity for Channel {
|
||||||
type Event = ();
|
type Event = ChannelEvent;
|
||||||
|
|
||||||
fn release(&mut self, cx: &mut MutableAppContext) {
|
fn release(&mut self, cx: &mut MutableAppContext) {
|
||||||
let rpc = self.rpc.clone();
|
let rpc = self.rpc.clone();
|
||||||
|
@ -132,7 +152,10 @@ impl Channel {
|
||||||
cx.spawn(|channel, mut cx| async move {
|
cx.spawn(|channel, mut cx| async move {
|
||||||
match rpc.request(proto::JoinChannel { channel_id }).await {
|
match rpc.request(proto::JoinChannel { channel_id }).await {
|
||||||
Ok(response) => channel.update(&mut cx, |channel, cx| {
|
Ok(response) => channel.update(&mut cx, |channel, cx| {
|
||||||
channel.messages = response.messages.into_iter().map(Into::into).collect();
|
channel.messages = SumTree::new();
|
||||||
|
channel
|
||||||
|
.messages
|
||||||
|
.extend(response.messages.into_iter().map(Into::into), &());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}),
|
}),
|
||||||
Err(error) => log::error!("error joining channel: {}", error),
|
Err(error) => log::error!("error joining channel: {}", error),
|
||||||
|
@ -171,12 +194,14 @@ impl Channel {
|
||||||
.binary_search_by_key(&local_id, |msg| msg.local_id)
|
.binary_search_by_key(&local_id, |msg| msg.local_id)
|
||||||
{
|
{
|
||||||
let body = this.pending_messages.remove(i).body;
|
let body = this.pending_messages.remove(i).body;
|
||||||
this.messages.insert(ChannelMessage {
|
this.insert_message(
|
||||||
id: response.message_id,
|
ChannelMessage {
|
||||||
sender_id: current_user_id,
|
id: response.message_id,
|
||||||
body,
|
sender_id: current_user_id,
|
||||||
});
|
body,
|
||||||
cx.notify();
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -187,7 +212,7 @@ impl Channel {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn messages(&self) -> &BTreeSet<ChannelMessage> {
|
pub fn messages(&self) -> &SumTree<ChannelMessage> {
|
||||||
&self.messages
|
&self.messages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,10 +234,31 @@ impl Channel {
|
||||||
.payload
|
.payload
|
||||||
.message
|
.message
|
||||||
.ok_or_else(|| anyhow!("empty message"))?;
|
.ok_or_else(|| anyhow!("empty message"))?;
|
||||||
self.messages.insert(message.into());
|
self.insert_message(message.into(), cx);
|
||||||
cx.notify();
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn insert_message(&mut self, message: ChannelMessage, cx: &mut ModelContext<Self>) {
|
||||||
|
let mut old_cursor = self.messages.cursor::<u64, Count>();
|
||||||
|
let mut new_messages = old_cursor.slice(&message.id, Bias::Left, &());
|
||||||
|
let start_ix = old_cursor.sum_start().0;
|
||||||
|
let mut end_ix = start_ix;
|
||||||
|
if old_cursor.item().map_or(false, |m| m.id == message.id) {
|
||||||
|
old_cursor.next(&());
|
||||||
|
end_ix += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
new_messages.push(message.clone(), &());
|
||||||
|
new_messages.push_tree(old_cursor.suffix(&()), &());
|
||||||
|
drop(old_cursor);
|
||||||
|
self.messages = new_messages;
|
||||||
|
|
||||||
|
cx.emit(ChannelEvent::Message {
|
||||||
|
old_range: start_ix..end_ix,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<proto::Channel> for ChannelDetails {
|
impl From<proto::Channel> for ChannelDetails {
|
||||||
|
@ -234,22 +280,35 @@ impl From<proto::ChannelMessage> for ChannelMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for ChannelMessage {
|
impl sum_tree::Item for ChannelMessage {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
type Summary = ChannelMessageSummary;
|
||||||
Some(self.cmp(other))
|
|
||||||
|
fn summary(&self) -> Self::Summary {
|
||||||
|
ChannelMessageSummary {
|
||||||
|
max_id: self.id,
|
||||||
|
count: Count(1),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ord for ChannelMessage {
|
impl sum_tree::Summary for ChannelMessageSummary {
|
||||||
fn cmp(&self, other: &Self) -> Ordering {
|
type Context = ();
|
||||||
self.id.cmp(&other.id)
|
|
||||||
|
fn add_summary(&mut self, summary: &Self, _: &()) {
|
||||||
|
self.max_id = summary.max_id;
|
||||||
|
self.count.0 += summary.count.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for ChannelMessage {
|
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for u64 {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
|
||||||
self.id == other.id
|
debug_assert!(summary.max_id > *self);
|
||||||
|
*self = summary.max_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for ChannelMessage {}
|
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count {
|
||||||
|
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
|
||||||
|
self.0 += summary.count.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,20 +1,30 @@
|
||||||
use super::channel::{Channel, ChannelList};
|
use crate::{
|
||||||
|
channel::{Channel, ChannelEvent, ChannelList, ChannelMessage},
|
||||||
|
Settings,
|
||||||
|
};
|
||||||
use gpui::{elements::*, Entity, ModelHandle, RenderContext, Subscription, View, ViewContext};
|
use gpui::{elements::*, Entity, ModelHandle, RenderContext, Subscription, View, ViewContext};
|
||||||
|
use postage::watch;
|
||||||
|
|
||||||
pub struct ChatPanel {
|
pub struct ChatPanel {
|
||||||
channel_list: ModelHandle<ChannelList>,
|
channel_list: ModelHandle<ChannelList>,
|
||||||
active_channel: Option<(ModelHandle<Channel>, Subscription)>,
|
active_channel: Option<(ModelHandle<Channel>, Subscription)>,
|
||||||
messages: ListState,
|
messages: ListState,
|
||||||
|
settings: watch::Receiver<Settings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Event {}
|
pub enum Event {}
|
||||||
|
|
||||||
impl ChatPanel {
|
impl ChatPanel {
|
||||||
pub fn new(channel_list: ModelHandle<ChannelList>, cx: &mut ViewContext<Self>) -> Self {
|
pub fn new(
|
||||||
|
channel_list: ModelHandle<ChannelList>,
|
||||||
|
settings: watch::Receiver<Settings>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self {
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
channel_list,
|
channel_list,
|
||||||
messages: ListState::new(Vec::new()),
|
messages: ListState::new(Vec::new()),
|
||||||
active_channel: None,
|
active_channel: None,
|
||||||
|
settings,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.assign_active_channel(cx);
|
this.assign_active_channel(cx);
|
||||||
|
@ -39,7 +49,15 @@ impl ChatPanel {
|
||||||
});
|
});
|
||||||
if let Some(channel) = channel {
|
if let Some(channel) = channel {
|
||||||
if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) {
|
if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) {
|
||||||
let subscription = cx.observe(&channel, Self::channel_did_change);
|
let subscription = cx.subscribe(&channel, Self::channel_did_change);
|
||||||
|
self.messages = ListState::new(
|
||||||
|
channel
|
||||||
|
.read(cx)
|
||||||
|
.messages()
|
||||||
|
.cursor::<(), ()>()
|
||||||
|
.map(|m| self.render_message(m))
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
self.active_channel = Some((channel, subscription));
|
self.active_channel = Some((channel, subscription));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -47,9 +65,42 @@ impl ChatPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn channel_did_change(&mut self, _: ModelHandle<Channel>, cx: &mut ViewContext<Self>) {
|
fn channel_did_change(
|
||||||
|
&mut self,
|
||||||
|
_: ModelHandle<Channel>,
|
||||||
|
event: &ChannelEvent,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
ChannelEvent::Message { old_range, message } => {
|
||||||
|
self.messages
|
||||||
|
.splice(old_range.clone(), Some(self.render_message(message)));
|
||||||
|
}
|
||||||
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_active_channel_messages(&self) -> ElementBox {
|
||||||
|
Expanded::new(0.8, List::new(self.messages.clone()).boxed()).boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_message(&self, message: &ChannelMessage) -> ElementBox {
|
||||||
|
let settings = self.settings.borrow();
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Label::new(
|
||||||
|
message.body.clone(),
|
||||||
|
settings.ui_font_family,
|
||||||
|
settings.ui_font_size,
|
||||||
|
)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_input_box(&self) -> ElementBox {
|
||||||
|
Empty::new().boxed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for ChatPanel {
|
impl Entity for ChatPanel {
|
||||||
|
@ -61,7 +112,10 @@ impl View for ChatPanel {
|
||||||
"ChatPanel"
|
"ChatPanel"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&self, _: &RenderContext<Self>) -> gpui::ElementBox {
|
fn render(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||||
List::new(self.messages.clone()).boxed()
|
Flex::column()
|
||||||
|
.with_child(self.render_active_channel_messages())
|
||||||
|
.with_child(self.render_input_box())
|
||||||
|
.boxed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -374,8 +374,14 @@ impl Workspace {
|
||||||
let mut right_sidebar = Sidebar::new(Side::Right);
|
let mut right_sidebar = Sidebar::new(Side::Right);
|
||||||
right_sidebar.add_item(
|
right_sidebar.add_item(
|
||||||
"icons/comment-16.svg",
|
"icons/comment-16.svg",
|
||||||
cx.add_view(|cx| ChatPanel::new(app_state.channel_list.clone(), cx))
|
cx.add_view(|cx| {
|
||||||
.into(),
|
ChatPanel::new(
|
||||||
|
app_state.channel_list.clone(),
|
||||||
|
app_state.settings.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
);
|
);
|
||||||
right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into());
|
right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into());
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue