From a1574c6b8df080ecb27587beee3d860af88fe2f7 Mon Sep 17 00:00:00 2001 From: Ulyssa Date: Sun, 29 Jan 2023 18:07:00 -0800 Subject: [PATCH] Show current date and local time for messages (#30) --- src/message/mod.rs | 109 +++++++++++++++++++++++++++------ src/windows/room/scrollback.rs | 67 +++++++++++++++----- 2 files changed, 139 insertions(+), 37 deletions(-) diff --git a/src/message/mod.rs b/src/message/mod.rs index adce084..5fdfe6c 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -6,7 +6,7 @@ use std::convert::TryFrom; use std::hash::{Hash, Hasher}; use std::slice::Iter; -use chrono::{DateTime, NaiveDateTime, Utc}; +use chrono::{DateTime, Local as LocalTz, NaiveDateTime, TimeZone}; use unicode_width::UnicodeWidthStr; use matrix_sdk::ruma::{ @@ -68,6 +68,13 @@ const fn span_static(s: &'static str) -> Span<'static> { } } +const BOLD_STYLE: Style = Style { + fg: None, + bg: None, + add_modifier: StyleModifier::BOLD, + sub_modifier: StyleModifier::empty(), +}; + const USER_GUTTER: usize = 30; const TIME_GUTTER: usize = 12; const READ_GUTTER: usize = 5; @@ -79,6 +86,14 @@ const USER_GUTTER_EMPTY_SPAN: Span<'static> = span_static(USER_GUTTER_EMPTY); const TIME_GUTTER_EMPTY: &str = " "; const TIME_GUTTER_EMPTY_SPAN: Span<'static> = span_static(TIME_GUTTER_EMPTY); +#[inline] +fn millis_to_datetime(ms: UInt) -> DateTime { + let time = i64::from(ms) / 1000; + let time = NaiveDateTime::from_timestamp_opt(time, 0).unwrap_or_default(); + + LocalTz.from_utc_datetime(&time) +} + #[derive(thiserror::Error, Debug)] pub enum TimeStampIntError { #[error("Integer conversion error: {0}")] @@ -95,13 +110,30 @@ pub enum MessageTimeStamp { } impl MessageTimeStamp { - fn show(&self) -> Option { + fn as_datetime(&self) -> DateTime { match self { - MessageTimeStamp::OriginServer(ts) => { - let time = i64::from(*ts) / 1000; - let time = NaiveDateTime::from_timestamp_opt(time, 0)?; - let time = DateTime::::from_utc(time, Utc); - let time = time.format("%T"); + MessageTimeStamp::OriginServer(ms) => millis_to_datetime(*ms), + MessageTimeStamp::LocalEcho => LocalTz::now(), + } + } + + fn same_day(&self, other: &Self) -> bool { + let dt1 = self.as_datetime(); + let dt2 = other.as_datetime(); + + dt1.date_naive() == dt2.date_naive() + } + + fn show_date(&self) -> Option { + let time = self.as_datetime().format("%A, %B %d %Y").to_string(); + + Span::styled(time, BOLD_STYLE).into() + } + + fn show_time(&self) -> Option { + match self { + MessageTimeStamp::OriginServer(ms) => { + let time = millis_to_datetime(*ms).format("%T"); let time = format!(" [{}]", time); Span::raw(time).into() @@ -139,6 +171,12 @@ impl PartialOrd for MessageTimeStamp { } } +impl From for MessageTimeStamp { + fn from(millis: UInt) -> Self { + MessageTimeStamp::OriginServer(millis) + } +} + impl From for MessageTimeStamp { fn from(millis: MilliSecondsSinceUnixEpoch) -> Self { MessageTimeStamp::OriginServer(millis.0) @@ -168,7 +206,7 @@ impl TryFrom for MessageTimeStamp { let n = u64::try_from(u)?; let n = UInt::try_from(n).map_err(TimeStampIntError::UIntError)?; - Ok(MessageTimeStamp::OriginServer(n)) + Ok(MessageTimeStamp::from(n)) } } } @@ -388,10 +426,26 @@ enum MessageColumns { struct MessageFormatter<'a> { settings: &'a ApplicationSettings, + + /// How many columns to print. cols: MessageColumns, + + /// The full, original width. + orig: usize, + + /// The width that the message contents need to fill. fill: usize, + + /// The formatted Span for the message sender. user: Option>, + + /// The time the message was sent. time: Option>, + + /// The date the message was sent. + date: Option>, + + /// Iterator over the users who have read up to this message. read: Iter<'a, OwnedUserId>, } @@ -402,6 +456,15 @@ impl<'a> MessageFormatter<'a> { #[inline] fn push_spans(&mut self, spans: Spans<'a>, style: Style, text: &mut Text<'a>) { + if let Some(date) = self.date.take() { + let len = date.content.as_ref().len(); + let padding = self.orig.saturating_sub(len); + let leading = space_span(padding / 2, Style::default()); + let trailing = space_span(padding.saturating_sub(padding / 2), Style::default()); + + text.lines.push(Spans(vec![leading, date, trailing])); + } + match self.cols { MessageColumns::Four => { let settings = self.settings; @@ -517,27 +580,33 @@ impl Message { info: &'a RoomInfo, settings: &'a ApplicationSettings, ) -> MessageFormatter<'a> { + let orig = width; + let date = match &prev { + Some(prev) if prev.timestamp.same_day(&self.timestamp) => None, + _ => self.timestamp.show_date(), + }; + if USER_GUTTER + TIME_GUTTER + READ_GUTTER + MIN_MSG_LEN <= width && settings.tunables.read_receipt_display { let cols = MessageColumns::Four; let fill = width - USER_GUTTER - TIME_GUTTER - READ_GUTTER; let user = self.show_sender(prev, true, settings); - let time = self.timestamp.show(); + let time = self.timestamp.show_time(); let read = match info.receipts.get(self.event.event_id()) { Some(read) => read.iter(), None => [].iter(), }; - MessageFormatter { settings, cols, fill, user, time, read } + MessageFormatter { settings, cols, orig, fill, user, date, time, read } } else if USER_GUTTER + TIME_GUTTER + MIN_MSG_LEN <= width { let cols = MessageColumns::Three; let fill = width - USER_GUTTER - TIME_GUTTER; let user = self.show_sender(prev, true, settings); - let time = self.timestamp.show(); + let time = self.timestamp.show_time(); let read = [].iter(); - MessageFormatter { settings, cols, fill, user, time, read } + MessageFormatter { settings, cols, orig, fill, user, date, time, read } } else if USER_GUTTER + MIN_MSG_LEN <= width { let cols = MessageColumns::Two; let fill = width - USER_GUTTER; @@ -545,7 +614,7 @@ impl Message { let time = None; let read = [].iter(); - MessageFormatter { settings, cols, fill, user, time, read } + MessageFormatter { settings, cols, orig, fill, user, date, time, read } } else { let cols = MessageColumns::One; let fill = width.saturating_sub(2); @@ -553,7 +622,7 @@ impl Message { let time = None; let read = [].iter(); - MessageFormatter { settings, cols, fill, user, time, read } + MessageFormatter { settings, cols, orig, fill, user, date, time, read } } } @@ -640,13 +709,13 @@ impl Message { align_right: bool, settings: &ApplicationSettings, ) -> Option { - let user = if matches!(prev, Some(prev) if self.sender == prev.sender) { - return None; - } else { - self.sender_span(settings) - }; + if let Some(prev) = prev { + if self.sender == prev.sender && self.timestamp.same_day(&prev.timestamp) { + return None; + } + } - let Span { content, style } = user; + let Span { content, style } = self.sender_span(settings); let stop = content.len().min(28); let s = &content[..stop]; diff --git a/src/windows/room/scrollback.rs b/src/windows/room/scrollback.rs index be8b9c8..6a419a3 100644 --- a/src/windows/room/scrollback.rs +++ b/src/windows/room/scrollback.rs @@ -103,6 +103,10 @@ fn nth_after(pos: MessageKey, n: usize, info: &RoomInfo) -> MessageCursor { nth_key_after(pos, n, info).into() } +fn prevmsg<'a>(key: &MessageKey, info: &'a RoomInfo) -> Option<&'a Message> { + info.messages.range(..key).next_back().map(|(_, v)| v) +} + pub struct ScrollbackState { /// The room identifier. room_id: OwnedRoomId, @@ -214,7 +218,8 @@ impl ScrollbackState { for (key, item) in info.messages.range(..=&idx).rev() { let sel = selidx == key; - let len = item.show(None, sel, &self.viewctx, info, settings).lines.len(); + let prev = prevmsg(key, info); + let len = item.show(prev, sel, &self.viewctx, info, settings).lines.len(); if key == &idx { lines += len / 2; @@ -236,7 +241,8 @@ impl ScrollbackState { for (key, item) in info.messages.range(..=&idx).rev() { let sel = key == selidx; - let len = item.show(None, sel, &self.viewctx, info, settings).lines.len(); + let prev = prevmsg(key, info); + let len = item.show(prev, sel, &self.viewctx, info, settings).lines.len(); lines += len; @@ -269,6 +275,7 @@ impl ScrollbackState { let mut lines = 0; let cursor_key = self.cursor.timestamp.as_ref().unwrap_or(last_key); + let mut prev = prevmsg(cursor_key, info); for (idx, item) in info.messages.range(corner_key.clone()..) { if idx == cursor_key { @@ -276,13 +283,15 @@ impl ScrollbackState { break; } - lines += item.show(None, false, &self.viewctx, info, settings).height().max(1); + lines += item.show(prev, false, &self.viewctx, info, settings).height().max(1); if lines >= self.viewctx.get_height() { // We've reached the end of the viewport; move cursor into it. self.cursor = idx.clone().into(); break; } + + prev = Some(item); } } @@ -1009,7 +1018,8 @@ impl ScrollActions for ScrollbackState { for (key, item) in info.messages.range(..=&corner_key).rev() { let sel = key == cursor_key; - let txt = item.show(None, sel, &self.viewctx, info, settings); + let prev = prevmsg(key, info); + let txt = item.show(prev, sel, &self.viewctx, info, settings); let len = txt.height().max(1); let max = len.saturating_sub(1); @@ -1033,12 +1043,16 @@ impl ScrollActions for ScrollbackState { } }, MoveDir2D::Down => { + let mut prev = prevmsg(&corner_key, info); + for (key, item) in info.messages.range(&corner_key..) { let sel = key == cursor_key; - let txt = item.show(None, sel, &self.viewctx, info, settings); + let txt = item.show(prev, sel, &self.viewctx, info, settings); let len = txt.height().max(1); let max = len.saturating_sub(1); + prev = Some(item); + if key != &corner_key { corner.text_row = 0; } @@ -1214,7 +1228,7 @@ impl<'a> StatefulWidget for Scrollback<'a> { let full = std::mem::take(&mut state.show_full_on_redraw) || cursor.timestamp.is_none(); let mut lines = vec![]; let mut sawit = false; - let mut prev = None; + let mut prev = prevmsg(&corner_key, info); for (key, item) in info.messages.range(&corner_key..) { let sel = key == cursor_key; @@ -1373,10 +1387,11 @@ mod tests { assert_eq!(scrollback.viewctx.dimensions, (0, 0)); assert_eq!(scrollback.viewctx.corner, MessageCursor::latest()); - // Set a terminal width of 60, and height of 3, rendering in scrollback as: + // Set a terminal width of 60, and height of 4, rendering in scrollback as: // // |------------------------------------------------------------| - // MSG2: | @user2:example.com helium | + // MSG2: | Wednesday, December 31 1969 | + // | @user2:example.com helium | // MSG3: | @user2:example.com this | // | is | // | a | @@ -1384,14 +1399,15 @@ mod tests { // | message | // MSG4: | @user1:example.com help | // MSG5: | @user2:example.com character | - // MSG1: | @user1:example.com writhe | + // MSG1: | XXXday, Month NN 20XX | + // | @user1:example.com writhe | // |------------------------------------------------------------| - let area = Rect::new(0, 0, 60, 3); + let area = Rect::new(0, 0, 60, 4); let mut buffer = Buffer::empty(area); scrollback.draw(area, &mut buffer, true, &mut store); assert_eq!(scrollback.cursor, MessageCursor::latest()); - assert_eq!(scrollback.viewctx.dimensions, (60, 3)); + assert_eq!(scrollback.viewctx.dimensions, (60, 4)); assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG4_KEY.clone(), 0)); // Scroll up a line at a time until we hit the first message. @@ -1420,6 +1436,11 @@ mod tests { .unwrap(); assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 0)); + scrollback + .dirscroll(prev, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG2_KEY.clone(), 1)); + scrollback .dirscroll(prev, ScrollSize::Cell, &1.into(), &ctx, &mut store) .unwrap(); @@ -1432,6 +1453,11 @@ mod tests { assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG2_KEY.clone(), 0)); // Now scroll back down one line at a time. + scrollback + .dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG2_KEY.clone(), 1)); + scrollback .dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store) .unwrap(); @@ -1472,19 +1498,24 @@ mod tests { .unwrap(); assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG1_KEY.clone(), 0)); + scrollback + .dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG1_KEY.clone(), 1)); + // Cannot scroll down any further. scrollback .dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store) .unwrap(); - assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG1_KEY.clone(), 0)); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG1_KEY.clone(), 1)); - // Scroll up two Pages (six lines). + // Scroll up two Pages (eight lines). scrollback .dirscroll(prev, ScrollSize::Page, &2.into(), &ctx, &mut store) .unwrap(); - assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 1)); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 0)); - // Scroll down two HalfPages (three lines). + // Scroll down two HalfPages (four lines). scrollback .dirscroll(next, ScrollSize::HalfPage, &2.into(), &ctx, &mut store) .unwrap(); @@ -1503,7 +1534,8 @@ mod tests { // Set a terminal width of 60, and height of 3, rendering in scrollback as: // // |------------------------------------------------------------| - // MSG2: | @user2:example.com helium | + // MSG2: | Wednesday, December 31 1969 | + // | @user2:example.com helium | // MSG3: | @user2:example.com this | // | is | // | a | @@ -1511,7 +1543,8 @@ mod tests { // | message | // MSG4: | @user1:example.com help | // MSG5: | @user2:example.com character | - // MSG1: | @user1:example.com writhe | + // MSG1: | XXXday, Month NN 20XX | + // | @user1:example.com writhe | // |------------------------------------------------------------| let area = Rect::new(0, 0, 60, 3); let mut buffer = Buffer::empty(area);