Show current date and local time for messages (#30)

This commit is contained in:
Ulyssa 2023-01-29 18:07:00 -08:00
parent e8205df21d
commit a1574c6b8d
No known key found for this signature in database
GPG key ID: 1B3965A3D18B9B64
2 changed files with 139 additions and 37 deletions

View file

@ -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<LocalTz> {
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<Span> {
fn as_datetime(&self) -> DateTime<LocalTz> {
match self {
MessageTimeStamp::OriginServer(ts) => {
let time = i64::from(*ts) / 1000;
let time = NaiveDateTime::from_timestamp_opt(time, 0)?;
let time = DateTime::<Utc>::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<Span> {
let time = self.as_datetime().format("%A, %B %d %Y").to_string();
Span::styled(time, BOLD_STYLE).into()
}
fn show_time(&self) -> Option<Span> {
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<UInt> for MessageTimeStamp {
fn from(millis: UInt) -> Self {
MessageTimeStamp::OriginServer(millis)
}
}
impl From<MilliSecondsSinceUnixEpoch> for MessageTimeStamp {
fn from(millis: MilliSecondsSinceUnixEpoch) -> Self {
MessageTimeStamp::OriginServer(millis.0)
@ -168,7 +206,7 @@ impl TryFrom<usize> 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<Span<'a>>,
/// The time the message was sent.
time: Option<Span<'a>>,
/// The date the message was sent.
date: Option<Span<'a>>,
/// 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<Span> {
let user = if matches!(prev, Some(prev) if self.sender == prev.sender) {
if let Some(prev) = prev {
if self.sender == prev.sender && self.timestamp.same_day(&prev.timestamp) {
return None;
} else {
self.sender_span(settings)
};
}
}
let Span { content, style } = user;
let Span { content, style } = self.sender_span(settings);
let stop = content.len().min(28);
let s = &content[..stop];

View file

@ -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<ProgramContext, ProgramStore, IambInfo> 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<ProgramContext, ProgramStore, IambInfo> 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);