WIP

Max Brunsfeld created

Change summary

Cargo.lock                   | 10 ++++
zed/Cargo.toml               |  1 
zed/assets/themes/_base.toml |  6 ++
zed/src/channel.rs           | 10 +++
zed/src/chat_panel.rs        | 93 ++++++++++++++++++++++++++++++++-----
zed/src/theme.rs             | 10 ++-
zed/src/theme_selector.rs    |  1 
zed/src/workspace.rs         |  4 
8 files changed, 114 insertions(+), 21 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5142,6 +5142,15 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "time"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e0a10c9a9fb3a5dce8c2239ed670f1a2569fcf42da035f5face1b19860d52b0"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "time-macros"
 version = "0.1.1"
@@ -5832,6 +5841,7 @@ dependencies = [
  "smol",
  "surf",
  "tempdir",
+ "time 0.3.2",
  "tiny_http",
  "toml 0.5.8",
  "tree-sitter",

zed/Cargo.toml 🔗

@@ -48,6 +48,7 @@ smallvec = { version = "1.6", features = ["union"] }
 smol = "1.2.5"
 surf = "2.2"
 tempdir = { version = "0.3.7", optional = true }
+time = { version = "0.3", features = ["local-offset"] }
 tiny_http = "0.8"
 toml = "0.5"
 tree-sitter = "0.19.5"

zed/assets/themes/_base.toml 🔗

@@ -22,8 +22,14 @@ color = "$text.2"
 [workspace.active_sidebar_icon]
 color = "$text.0"
 
+[chat_panel]
+padding = { top = 10.0, bottom = 10.0, left = 10.0, right = 10.0 }
+
 [chat_panel.message]
 body = "$text.0"
+sender.margin.right = 10.0
+sender.text = { color = "#ff0000", weight = "bold", italic = true }
+timestamp.text = "$text.2"
 
 [selector]
 background = "$surface.2"

zed/src/channel.rs 🔗

@@ -14,6 +14,7 @@ use std::{
     ops::Range,
     sync::Arc,
 };
+use time::OffsetDateTime;
 use zrpc::{
     proto::{self, ChannelMessageSent},
     TypedEnvelope,
@@ -47,6 +48,7 @@ pub struct Channel {
 pub struct ChannelMessage {
     pub id: u64,
     pub body: String,
+    pub timestamp: OffsetDateTime,
     pub sender: Arc<User>,
 }
 
@@ -261,14 +263,17 @@ impl Channel {
                         this.insert_message(
                             ChannelMessage {
                                 id: response.message_id,
+                                timestamp: OffsetDateTime::from_unix_timestamp(
+                                    response.timestamp as i64,
+                                )?,
                                 body,
                                 sender,
                             },
                             cx,
                         );
                     }
-                });
-                Ok(())
+                    Ok(())
+                })
             }
             .log_err()
         })
@@ -363,6 +368,7 @@ impl ChannelMessage {
         Ok(ChannelMessage {
             id: message.id,
             body: message.body,
+            timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?,
             sender,
         })
     }

zed/src/chat_panel.rs 🔗

@@ -9,6 +9,7 @@ use gpui::{
     Subscription, View, ViewContext, ViewHandle,
 };
 use postage::watch;
+use time::{OffsetDateTime, UtcOffset};
 
 pub struct ChatPanel {
     channel_list: ModelHandle<ChannelList>,
@@ -75,12 +76,13 @@ impl ChatPanel {
     fn set_active_channel(&mut self, channel: ModelHandle<Channel>, cx: &mut ViewContext<Self>) {
         if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) {
             let subscription = cx.subscribe(&channel, Self::channel_did_change);
+            let now = OffsetDateTime::now_utc();
             self.messages = ListState::new(
                 channel
                     .read(cx)
                     .messages()
                     .cursor::<(), ()>()
-                    .map(|m| self.render_message(m))
+                    .map(|m| self.render_message(m, now))
                     .collect(),
                 Orientation::Bottom,
             );
@@ -99,12 +101,13 @@ impl ChatPanel {
                 old_range,
                 new_count,
             } => {
+                let now = OffsetDateTime::now_utc();
                 self.messages.splice(
                     old_range.clone(),
                     channel
                         .read(cx)
                         .messages_in_range(old_range.start..(old_range.start + new_count))
-                        .map(|message| self.render_message(message)),
+                        .map(|message| self.render_message(message, now)),
                 );
             }
         }
@@ -115,15 +118,50 @@ impl ChatPanel {
         Expanded::new(1., List::new(self.messages.clone()).boxed()).boxed()
     }
 
-    fn render_message(&self, message: &ChannelMessage) -> ElementBox {
+    fn render_message(&self, message: &ChannelMessage, now: OffsetDateTime) -> ElementBox {
         let settings = self.settings.borrow();
-        Text::new(
-            message.body.clone(),
-            settings.ui_font_family,
-            settings.ui_font_size,
-        )
-        .with_style(&settings.theme.chat_panel.message.body)
-        .boxed()
+        let theme = &settings.theme.chat_panel.message;
+        Flex::column()
+            .with_child(
+                Flex::row()
+                    .with_child(
+                        Container::new(
+                            Label::new(
+                                message.sender.github_login.clone(),
+                                settings.ui_font_family,
+                                settings.ui_font_size,
+                            )
+                            .with_style(&theme.sender.label)
+                            .boxed(),
+                        )
+                        .with_style(&theme.sender.container)
+                        .boxed(),
+                    )
+                    .with_child(
+                        Container::new(
+                            Label::new(
+                                format_timestamp(message.timestamp, now),
+                                settings.ui_font_family,
+                                settings.ui_font_size,
+                            )
+                            .with_style(&theme.timestamp.label)
+                            .boxed(),
+                        )
+                        .with_style(&theme.timestamp.container)
+                        .boxed(),
+                    )
+                    .boxed(),
+            )
+            .with_child(
+                Text::new(
+                    message.body.clone(),
+                    settings.ui_font_family,
+                    settings.ui_font_size,
+                )
+                .with_style(&theme.body)
+                .boxed(),
+            )
+            .boxed()
     }
 
     fn render_input_box(&self) -> ElementBox {
@@ -157,9 +195,36 @@ impl View for ChatPanel {
     }
 
     fn render(&self, _: &RenderContext<Self>) -> ElementBox {
-        Flex::column()
-            .with_child(self.render_active_channel_messages())
-            .with_child(self.render_input_box())
-            .boxed()
+        let theme = &self.settings.borrow().theme;
+        Container::new(
+            Flex::column()
+                .with_child(self.render_active_channel_messages())
+                .with_child(self.render_input_box())
+                .boxed(),
+        )
+        .with_style(&theme.chat_panel.container)
+        .boxed()
+    }
+}
+
+fn format_timestamp(mut timestamp: OffsetDateTime, mut now: OffsetDateTime) -> String {
+    let local_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
+    timestamp = timestamp.to_offset(local_offset);
+    now = now.to_offset(local_offset);
+
+    let today = now.date();
+    let date = timestamp.date();
+    let mut hour = timestamp.hour();
+    let mut part = "am";
+    if hour > 12 {
+        hour -= 12;
+        part = "pm";
+    }
+    if date == today {
+        format!("{}:{}{}", hour, timestamp.minute(), part)
+    } else if date.next_day() == Some(today) {
+        format!("yesterday at {}:{}{}", hour, timestamp.minute(), part)
+    } else {
+        format!("{}/{}/{}", date.month(), date.day(), date.year())
     }
 }

zed/src/theme.rs 🔗

@@ -55,12 +55,16 @@ pub struct SidebarIcon {
 
 #[derive(Debug, Default, Deserialize)]
 pub struct ChatPanel {
+    #[serde(flatten)]
+    pub container: ContainerStyle,
     pub message: ChatMessage,
 }
 
 #[derive(Debug, Default, Deserialize)]
 pub struct ChatMessage {
     pub body: TextStyle,
+    pub sender: ContainedLabel,
+    pub timestamp: ContainedLabel,
 }
 
 #[derive(Debug, Default, Deserialize)]
@@ -70,12 +74,12 @@ pub struct Selector {
     #[serde(flatten)]
     pub label: LabelStyle,
 
-    pub item: SelectorItem,
-    pub active_item: SelectorItem,
+    pub item: ContainedLabel,
+    pub active_item: ContainedLabel,
 }
 
 #[derive(Debug, Default, Deserialize)]
-pub struct SelectorItem {
+pub struct ContainedLabel {
     #[serde(flatten)]
     pub container: ContainerStyle,
     #[serde(flatten)]

zed/src/theme_selector.rs 🔗

@@ -99,6 +99,7 @@ impl ThemeSelector {
             Ok(theme) => {
                 cx.notify_all();
                 action.0.settings_tx.lock().borrow_mut().theme = theme;
+                log::info!("reloaded theme {}", current_theme_name);
             }
             Err(error) => {
                 log::error!("failed to load theme {}: {:?}", current_theme_name, error)

zed/src/workspace.rs 🔗

@@ -958,7 +958,7 @@ impl View for Workspace {
                                 if let Some(panel) = self.left_sidebar.active_item() {
                                     content.add_child(
                                         ConstrainedBox::new(ChildView::new(panel.id()).boxed())
-                                            .with_width(200.0)
+                                            .with_width(300.0)
                                             .named("left panel"),
                                     );
                                 }
@@ -966,7 +966,7 @@ impl View for Workspace {
                                 if let Some(panel) = self.right_sidebar.active_item() {
                                     content.add_child(
                                         ConstrainedBox::new(ChildView::new(panel.id()).boxed())
-                                            .with_width(200.0)
+                                            .with_width(300.0)
                                             .named("right panel"),
                                     );
                                 }