Render chat messages in ChatPanel

This commit is contained in:
Antonio Scandurra 2021-08-24 12:23:50 +02:00
parent baded7d416
commit 405ff1d9db
4 changed files with 155 additions and 36 deletions

View file

@ -1520,7 +1520,7 @@ mod tests {
fn channel_messages(channel: &Channel) -> Vec<(u64, String)> {
channel
.messages()
.iter()
.cursor::<(), ()>()
.map(|m| (m.sender_id, m.body.clone()))
.collect()
}

View file

@ -3,10 +3,13 @@ use crate::{
util::log_async_errors,
};
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::{
cmp::Ordering,
collections::{hash_map, BTreeSet, HashMap},
collections::{hash_map, HashMap},
ops::Range,
sync::Arc,
};
use zrpc::{
@ -28,13 +31,14 @@ pub struct ChannelDetails {
pub struct Channel {
details: ChannelDetails,
messages: BTreeSet<ChannelMessage>,
messages: SumTree<ChannelMessage>,
pending_messages: Vec<PendingChannelMessage>,
next_local_message_id: u64,
rpc: Arc<Client>,
_subscription: rpc::Subscription,
}
#[derive(Clone, Debug)]
pub struct ChannelMessage {
pub id: u64,
pub sender_id: u64,
@ -46,10 +50,26 @@ pub struct PendingChannelMessage {
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 {
type Event = Event;
type Event = ChannelListEvent;
}
impl ChannelList {
@ -107,7 +127,7 @@ impl ChannelList {
}
impl Entity for Channel {
type Event = ();
type Event = ChannelEvent;
fn release(&mut self, cx: &mut MutableAppContext) {
let rpc = self.rpc.clone();
@ -132,7 +152,10 @@ impl Channel {
cx.spawn(|channel, mut cx| async move {
match rpc.request(proto::JoinChannel { channel_id }).await {
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();
}),
Err(error) => log::error!("error joining channel: {}", error),
@ -171,12 +194,14 @@ impl Channel {
.binary_search_by_key(&local_id, |msg| msg.local_id)
{
let body = this.pending_messages.remove(i).body;
this.messages.insert(ChannelMessage {
id: response.message_id,
sender_id: current_user_id,
body,
});
cx.notify();
this.insert_message(
ChannelMessage {
id: response.message_id,
sender_id: current_user_id,
body,
},
cx,
);
}
});
Ok(())
@ -187,7 +212,7 @@ impl Channel {
Ok(())
}
pub fn messages(&self) -> &BTreeSet<ChannelMessage> {
pub fn messages(&self) -> &SumTree<ChannelMessage> {
&self.messages
}
@ -209,10 +234,31 @@ impl Channel {
.payload
.message
.ok_or_else(|| anyhow!("empty message"))?;
self.messages.insert(message.into());
cx.notify();
self.insert_message(message.into(), cx);
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 {
@ -234,22 +280,35 @@ impl From<proto::ChannelMessage> for ChannelMessage {
}
}
impl PartialOrd for ChannelMessage {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
impl sum_tree::Item for ChannelMessage {
type Summary = ChannelMessageSummary;
fn summary(&self) -> Self::Summary {
ChannelMessageSummary {
max_id: self.id,
count: Count(1),
}
}
}
impl Ord for ChannelMessage {
fn cmp(&self, other: &Self) -> Ordering {
self.id.cmp(&other.id)
impl sum_tree::Summary for ChannelMessageSummary {
type Context = ();
fn add_summary(&mut self, summary: &Self, _: &()) {
self.max_id = summary.max_id;
self.count.0 += summary.count.0;
}
}
impl PartialEq for ChannelMessage {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for u64 {
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
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;
}
}

View file

@ -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 postage::watch;
pub struct ChatPanel {
channel_list: ModelHandle<ChannelList>,
active_channel: Option<(ModelHandle<Channel>, Subscription)>,
messages: ListState,
settings: watch::Receiver<Settings>,
}
pub enum Event {}
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 {
channel_list,
messages: ListState::new(Vec::new()),
active_channel: None,
settings,
};
this.assign_active_channel(cx);
@ -39,7 +49,15 @@ impl ChatPanel {
});
if let Some(channel) = 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));
}
} 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();
}
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 {
@ -61,7 +112,10 @@ impl View for ChatPanel {
"ChatPanel"
}
fn render(&self, _: &RenderContext<Self>) -> gpui::ElementBox {
List::new(self.messages.clone()).boxed()
fn render(&self, _: &RenderContext<Self>) -> ElementBox {
Flex::column()
.with_child(self.render_active_channel_messages())
.with_child(self.render_input_box())
.boxed()
}
}

View file

@ -374,8 +374,14 @@ impl Workspace {
let mut right_sidebar = Sidebar::new(Side::Right);
right_sidebar.add_item(
"icons/comment-16.svg",
cx.add_view(|cx| ChatPanel::new(app_state.channel_list.clone(), cx))
.into(),
cx.add_view(|cx| {
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());