Skip to content

Commit

Permalink
Scrolling messages (#21)
Browse files Browse the repository at this point in the history
* Message scrolling using frequent clone; weekday timestamps
* Fix unwrap and out of bounds panics on empty channels
* Custom serde functions for backward compatibility of json
  • Loading branch information
cleeyv authored Mar 1, 2021
1 parent e4ef311 commit be210c3
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 61 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ anyhow = "1.0.38"
base64 = "0.13.0"
chrono = { version = "0.4.19", features = ["serde"] }
crossterm = { version = "0.18.2", features = ["event-stream"] }
derivative = "2.2.0"
dirs = "3.0.1"
itertools = "0.10.0"
log = "0.4.14"
Expand Down
50 changes: 42 additions & 8 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::util::StatefulList;
use anyhow::Context;
use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
use crossterm::event::KeyCode;
use derivative::Derivative;
use serde::{Deserialize, Serialize};
use unicode_width::UnicodeWidthStr;

Expand Down Expand Up @@ -72,7 +73,7 @@ impl AppData {
id: group_info.group_id,
name,
is_group: true,
messages: Vec::new(),
messages: StatefulList::with_items(Vec::new()),
unread_messages: 0,
}
});
Expand All @@ -84,7 +85,7 @@ impl AppData {
id: contact_info.phone_number,
name: contact_info.name,
is_group: false,
messages: Vec::new(),
messages: StatefulList::with_items(Vec::new()),
unread_messages: 0,
});

Expand Down Expand Up @@ -113,17 +114,39 @@ impl AppData {
}
}

#[derive(Debug, Serialize, Deserialize)]
#[derive(Derivative, Serialize, Deserialize)]
#[derivative(Debug)]
pub struct Channel {
/// Either phone number or group id
pub id: String,
pub name: String,
pub is_group: bool,
pub messages: Vec<Message>,
#[derivative(Debug = "ignore")]
#[serde(serialize_with = "Channel::serialize_msgs")]
#[serde(deserialize_with = "Channel::deserialize_msgs")]
pub messages: StatefulList<Message>,
#[serde(default)]
pub unread_messages: usize,
}

impl Channel {
fn serialize_msgs<S>(messages: &StatefulList<Message>, ser: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
// the messages StatefulList becomes the vec that was messages.items
messages.items.serialize(ser)
}

fn deserialize_msgs<'de, D>(deserializer: D) -> Result<StatefulList<Message>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let tmp: Vec<Message> = serde::de::Deserialize::deserialize(deserializer)?;
Ok(StatefulList::with_items(tmp))
}
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Message {
pub from: String,
Expand Down Expand Up @@ -248,7 +271,7 @@ impl App {
signal::SignalClient::send_group_message(&message, &channel.id);
}

channel.messages.push(Message {
channel.messages.items.push(Message {
from: self.config.user.name.clone(),
message: Some(message),
attachments: Vec::new(),
Expand Down Expand Up @@ -311,6 +334,16 @@ impl App {
}
}

pub fn on_pgup(&mut self) {
let select = self.data.channels.state.selected().unwrap_or_default();
self.data.channels.items[select].messages.next();
}

pub fn on_pgdn(&mut self) {
let select = self.data.channels.state.selected().unwrap_or_default();
self.data.channels.items[select].messages.previous();
}

pub fn reset_unread_messages(&mut self) -> bool {
if let Some(selected_idx) = self.data.channels.state.selected() {
if self.data.channels.items[selected_idx].unread_messages > 0 {
Expand Down Expand Up @@ -423,7 +456,7 @@ impl App {
id: channel_id.clone(),
name: channel_name,
is_group,
messages: Vec::new(),
messages: StatefulList::with_items(Vec::new()),
unread_messages: 0,
});
self.data.channels.items.len() - 1
Expand All @@ -450,6 +483,7 @@ impl App {

self.data.channels.items[channel_idx]
.messages
.items
.push(Message {
from: name,
message: text,
Expand Down Expand Up @@ -489,7 +523,7 @@ impl App {

if let Some(name) = name.as_ref() {
for channel in self.data.channels.items.iter_mut() {
for message in channel.messages.iter_mut() {
for message in channel.messages.items.iter_mut() {
if message.from == phone_number {
message.from = name.clone();
}
Expand All @@ -507,7 +541,7 @@ impl App {
id: phone_number,
name: name.clone(),
is_group: false,
messages: Vec::new(),
messages: StatefulList::with_items(Vec::new()),
unread_messages: 0,
})
}
Expand Down
4 changes: 4 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,12 @@ async fn main() -> anyhow::Result<()> {
},
MouseEvent::ScrollUp(col, _, _) => match col {
col if col < terminal.get_frame().size().width / 4 => app.on_up(),
col if col > terminal.get_frame().size().width / 4 => app.on_pgup(),
_ => {}
},
MouseEvent::ScrollDown(col, _, _) => match col {
col if col < terminal.get_frame().size().width / 4 => app.on_down(),
col if col > terminal.get_frame().size().width / 4 => app.on_pgdn(),
_ => {}
},
_ => {}
Expand All @@ -161,6 +163,8 @@ async fn main() -> anyhow::Result<()> {
app.on_right();
}
KeyCode::Down => app.on_down(),
KeyCode::PageUp => app.on_pgup(),
KeyCode::PageDown => app.on_pgdn(),
code => app.on_key(code),
},
Some(Event::Message { payload, message }) => {
Expand Down
127 changes: 74 additions & 53 deletions src/ui.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::signal;
use crate::util;
use crate::{app, App};

use anyhow::Context;
use chrono::Timelike;
use chrono::{Datelike, Timelike};
use tui::backend::Backend;
use tui::layout::{Constraint, Corner, Direction, Layout, Rect};
use tui::style::{Color, Style};
Expand Down Expand Up @@ -116,14 +117,14 @@ fn draw_chat<B: Backend>(f: &mut Frame<B>, app: &mut App, area: Rect) {
);
}

fn draw_messages<B: Backend>(f: &mut Frame<B>, app: &App, area: Rect) {
fn draw_messages<B: Backend>(f: &mut Frame<B>, app: &mut App, area: Rect) {
let messages = app
.data
.channels
.state
.selected()
.and_then(|idx| app.data.channels.items.get(idx))
.map(|channel| &channel.messages[..])
.map(|channel| &channel.messages.items[..])
.unwrap_or(&[]);

let max_username_width = messages
Expand All @@ -133,57 +134,56 @@ fn draw_messages<B: Backend>(f: &mut Frame<B>, app: &App, area: Rect) {
.unwrap_or(0);

let width = area.width - 2; // without borders
let max_lines = area.height;

let time_style = Style::default().fg(Color::Yellow);
let messages = messages
.iter()
.rev()
// we can't show more messages atm and don't have messages navigation
.take(max_lines as usize)
.map(|msg| {
let arrived_at = msg.arrived_at.with_timezone(&chrono::Local);

let time = Span::styled(
format!("{:02}:{:02} ", arrived_at.hour(), arrived_at.minute()),
time_style,
);
let from = displayed_name(&msg.from, app.config.first_name_only);
let from = Span::styled(
textwrap::indent(&from, &" ".repeat(max_username_width - from.width())),
Style::default().fg(user_color(&msg.from)),
);
let delimeter = Span::from(": ");

let displayed_message = displayed_message(&msg);

let prefix_width = (time.width() + from.width() + delimeter.width()) as u16;
let indent = " ".repeat(prefix_width.into());

let wrap_opts = textwrap::Options::new(width.into())
.initial_indent(&indent)
.subsequent_indent(&indent);
let lines = textwrap::wrap(displayed_message.as_str(), wrap_opts);

let spans: Vec<Spans> = lines
.into_iter()
.enumerate()
.map(|(idx, line)| {
let res = if idx == 0 {
vec![
time.clone(),
from.clone(),
delimeter.clone(),
Span::from(line.strip_prefix(&indent).unwrap().to_string()),
]
} else {
vec![Span::from(line.to_string())]
};
Spans::from(res)
})
.collect();
spans
});
let messages = messages.iter().rev().map(|msg| {
let arrived_at = msg.arrived_at.with_timezone(&chrono::Local);

let time = Span::styled(
format!(
"{:02} {:02}:{:02} ",
arrived_at.weekday(),
arrived_at.hour(),
arrived_at.minute()
),
time_style,
);
let from = displayed_name(&msg.from, app.config.first_name_only);
let from = Span::styled(
textwrap::indent(&from, &" ".repeat(max_username_width - from.width())),
Style::default().fg(user_color(&msg.from)),
);
let delimeter = Span::from(": ");

let displayed_message = displayed_message(&msg);

let prefix_width = (time.width() + from.width() + delimeter.width()) as u16;
let indent = " ".repeat(prefix_width.into());

let wrap_opts = textwrap::Options::new(width.into())
.initial_indent(&indent)
.subsequent_indent(&indent);
let lines = textwrap::wrap(displayed_message.as_str(), wrap_opts);

let spans: Vec<Spans> = lines
.into_iter()
.enumerate()
.map(|(idx, line)| {
let res = if idx == 0 {
vec![
time.clone(),
from.clone(),
delimeter.clone(),
Span::from(line.strip_prefix(&indent).unwrap().to_string()),
]
} else {
vec![Span::from(line.to_string())]
};
Spans::from(res)
})
.collect();
spans
});

let mut items: Vec<_> = messages.map(|s| ListItem::new(Text::from(s))).collect();

Expand All @@ -202,8 +202,29 @@ fn draw_messages<B: Backend>(f: &mut Frame<B>, app: &App, area: Rect) {
let list = List::new(items)
.block(Block::default().title("Messages").borders(Borders::ALL))
.style(Style::default().fg(Color::White))
.highlight_style(Style::default().fg(Color::Black).bg(Color::Gray))
.start_corner(Corner::BottomLeft);
f.render_widget(list, area);

let selected = app.data.channels.state.selected().unwrap_or_default();

let init = &mut app::Channel {
id: "default".to_string(),
name: " ".to_string(),
is_group: false,
messages: util::StatefulList::with_items(Vec::new()),
unread_messages: 0,
};

let state = &mut app
.data
.channels
.items
.get_mut(selected)
.unwrap_or(init)
.messages
.state;

f.render_stateful_widget(list, area, state);
}

// Randomly but deterministically choose a color for a username
Expand Down

0 comments on commit be210c3

Please sign in to comment.